8000 metal: build pipeline states for render targets' attachment formats by benface · Pull Request #678 · not-fl3/miniquad · GitHub
[go: up one dir, main page]

Skip to content

metal: build pipeline states for render targets' attachment formats - #678

Open
benface wants to merge 1 commit into
not-fl3:masterfrom
benface:metal-pipeline-target-formats
Open

benface wants to merge 1 commit into
not-fl3:masterfrom
benface:metal-pipeline-target-formats

Conversation

@benface
@benface benface commented Oct 9, 2026 •
Copy link
Copy Markdown
Contributor

Why

A Metal pipeline state is built for one set of attachment formats, and Metal only draws with it into a render pass whose attachments match. Every pipeline state is built for the view's: its color format, its depth/stencil format and its sample count. So a render target that differs in any of them fails validation as soon as a pipeline is applied in it.

On macOS, where the view has a depth/stencil buffer, that's every render target made without a depth texture, macroquad's render_target among them:

For depth attachment, the renderPipelineState pixelFormat must be MTLPixelFormatInvalid, as no texture is set.
For stencil attachment, the renderPipelineState pixelFormat must be MTLPixelFormatInvalid, as no texture is set.

With sample_count: 4, every 1x render target fails too, and with the default of 1, every multisampled one:

For color attachment 0, the texture sample count (1) does not match the renderPipelineState colorSampleCount (4).

Change

  • A pipeline keeps its descriptor, and a pipeline state for each set of attachment formats it has drawn with.
  • new_pipeline still builds the view's pipeline state. So an app that only draws to the screen compiles the same state at the same time as before, and nothing moves into its first frame.
  • begin_pass reads the pass's formats from its attachments. The first time a pass whose formats differ applies the pipeline, it builds a state for those formats and keeps it for later passes.

Why only render targets' formats are built on first use

A pipeline doesn't know which render targets it will draw into. Building every combination of color format, depth/stencil format and sample count up front would make every app compile states it never uses, and each compile takes milliseconds: 3–25 ms on an iPhone 17 Pro with nothing cached, in a macroquad game (CHOMP). The view's formats are known when the pipeline is made, so its state is still built there. An app that wants no compile mid-frame can draw into each kind of render target once while it loads.

Verification

On macOS with MTL_DEBUG_LAYER=1, a small program draws a triangle into the window and into 1x and 4x render targets, with and without depth, in the same frame. It's a debug build with #673 applied, since without it the first draw fails validation for another reason.

  • On master, it fails at the first render target that differs from the view, with the errors above, whether the view is 1x or 4x.
  • With this PR, it raises no validation error. One pipeline state is built in new_pipeline, one for each render target whose formats differ from the view's during the first frame, and none after.
  • With only the window drawn into, no pipeline state is built after new_pipeline.

CHOMP draws into multisampled render targets without depth every frame, 8000 for its shadow masks and its recordings. With this change, five of its levels, 2,579 frames, play through under validation on macOS with no error.

cargo check passes for aarch64-apple-darwin, aarch64-apple-ios and aarch64-apple-tvos.

🤖 Generated with Claude Code

@benface
benface commented Oct 9, 2026
Copy link
Copy Markdown
Contributor Author

The Android check failed before it checked out the code: Docker Hub refused to pull the notfl3/cargo-apk image ("toomanyrequests: You have reached your unauthenticated pull rate limit"). It's unrelated to this change, and a re-run of the job should pass.

A Metal pipeline state is built for one set of attachment formats, and
Metal only draws with it into a render pass whose attachments match.
Every pipeline state was built for the view's: its color format, its
depth/stencil format and its sample count. A render target that
differs in any of them failed validation as soon as a pipeline was
applied in it. On macOS, where the view has a depth/stencil buffer,
that's every render target made without a depth texture:

    For depth attachment, the renderPipelineState pixelFormat must be
    MTLPixelFormatInvalid, as no texture is set.
    For stencil attachment, the renderPipelineState pixelFormat must
    be MTLPixelFormatInvalid, as no texture is set.

With `sample_count: 4`, every 1x render target failed too, and with
the default of 1, every multisampled one:

    For color attachment 0, the texture sample count (1) does not
    match the renderPipelineState colorSampleCount (4).

A pipeline now keeps its descriptor. It still builds the view's
pipeline state when it's created, so an app that only draws to the
screen compiles the same state at the same time as before. The first
time a pass whose attachments differ applies it, it builds a state for
those formats too, and keeps it for later passes.

Only other formats are built on first use: a pipeline doesn't know
which render targets it will draw into, and building every combination
up front would make every app compile states it never uses. An app
that doesn't want a compile mid-frame can draw into each kind of
target once while it loads.

Verified on macOS with `MTL_DEBUG_LAYER=1`: with the view at 1x and at
4x, drawing into the window and into 1x and 4x render targets, with
and without depth, in the same frame raises no validation error. With
only the window drawn into, no pipeline state is built after
`new_pipeline`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
@benface
benface force-pushed the metal-pipeline-target-formats branch from 5336a21 to f6d66e7 Compare October 18, 2026 18:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0