Repository navigation
Conversation
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>
Building a Metal pipeline state takes milliseconds: 3–25 ms each on an iPhone 17 Pro with nothing cached, in a macroquad game (CHOMP). A pipeline builds one for the view as it's made and one for each other set of attachment formats it draws into, each with the synchronous `newRenderPipelineStateWithDescriptor:error:`, so an app that makes or prepares many pipelines while it loads waits for each in turn. Each state is now built with `newRenderPipelineStateWithDescriptor:completionHandler:`, which Metal runs on threads of its own, so states started together build at once. The first draw that needs one waits for it, if it isn't done yet. `new_pipeline` starts the view's as before, without waiting for it. `RenderingBackend::prepare_pipeline` starts a pipeline's state for a render pass's attachment formats, or the view's, ahead of its first draw there. An app that doesn't want a build mid-frame can start every pipeline it has for every kind of target it draws into, then draw into each once while it loads. It does nothing on OpenGL, which has no such states. The completion handler is a block literal laid out by hand, with no copy or dispose helpers: it holds a copy of the descriptor and a counted reference to where the result goes, and releases both when Metal calls it, once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #678, which builds a pipeline state for each set of attachment formats a pipeline draws into: this branch's first commit is #678's, and its second is this change.
Why
Building a Metal pipeline state takes milliseconds when the system hasn't cached it: 3–25 ms each on an iPhone 17 Pro, in a macroquad game (CHOMP). With #678, a pipeline builds one for the view as it's made, and one for each other set of attachment formats the first time it draws into them, each with the synchronous
newRenderPipelineStateWithDescriptor:error:. An app that makes many pipelines and draws with each into each kind of target while it loads, so that nothing builds mid-frame, waits for every build in turn.Metal's asynchronous
newRenderPipelineStateWithDescriptor:completionHandler:builds on threads of its own, so builds started together run at once.Change
newRenderPipelineStateWithDescriptor:completionHandler:, from a copy of the pipeline's descriptor with the formats filled in. The first draw that needs a state waits for it, if it isn't done yet.new_pipelinestarts the view's state as before, without waiting for it.RenderingBackend::prepare_pipeline(pipeline, pass)starts a pipeline's state for a render pass's attachment formats, or the view's withNone, ahead of its first draw there. Its default does nothing, which is all OpenGL needs.The completion handler is a block literal laid out by hand, as miniquad has no blocks crate. It has no copy or dispose helpers: it holds the descriptor's copy and an
Arcto where the result goes, as raw pointers, and releases both when Metal calls it, which Metal does once.Measurements
CHOMP makes 31 materials while it loads, then draws a pixel with each into every kind of target it draws into: about 96 pipeline states. Its loading screen times that phase.
On an iPhone 17 Pro, with the app uninstalled before each first launch:
On a Mac (M2 Max), with a fresh bundle id for each first launch so Metal's cache for it starts empty:
Verification
cargo checkpasses foraarch64-apple-darwin,aarch64-apple-ios,aarch64-apple-tvosandaarch64-linux-android.MTL_DEBUG_LAYER=1, CHOMP runs through its frame bench, which plays several levels, with no validation error.🤖 Generated with Claude Code