Skip to content

metal: honor Conf.sample_count for MSAA on iOS + matching pipelines#640

Merged
not-fl3 merged 3 commits into
not-fl3:masterfrom
benface:metal-msaa-support
Jul 25, 2026
Merged

metal: honor Conf.sample_count for MSAA on iOS + matching pipelines#640
not-fl3 merged 3 commits into
not-fl3:masterfrom
benface:metal-msaa-support

Conversation

@benface

@benface benface commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Three coupled changes that together light up MSAA on the iOS Metal backend.

1. create_metal_view calls setSampleCount: on MTKView

The sample_count argument was prefixed _ and silently dropped, so Conf.sample_count had no effect on iOS Metal. macOS already does this.

2. new_pipeline matches the view's sampleCount

A pipeline's sampleCount must match every render-pass color attachment it draws to. Reading the view's value at pipeline-creation time keeps the main drawable and render_target_msaa() targets compatible with the same pipeline.

3. new_texture creates D2Multisample textures when sample_count > 1

For RenderTarget access with sample_count > 1, the color texture has to be a D2Multisample with matching setSampleCount: and mipmapLevelCount = 1, and can only carry RenderTarget usage (not ShaderRead/ShaderWrite). The single-sample resolve texture for the same target is created separately by the caller (e.g. macroquad's render_target_ex) and passed in via new_render_pass_mrt's resolve_img slice — handled in #638.

Constraint

Every render-pass color attachment must use the view's sample count. Mixing single-sample and multisample targets in one app would require per-target pipeline states, which Metal allows but miniquad's "one pipeline per material" model does not currently express. Callers that opt into Conf.sample_count > 1 should use render_target_msaa() (or pass a matching sample_count) for every offscreen color target.

Builds on #634, #635, #636, #637, #638, #639.

@benface
benface force-pushed the metal-msaa-support branch from 246c371 to 7c74c42 Compare June 13, 2026 15:30
Benoît Rouleau added 3 commits July 23, 2026 14:52
Three coupled changes that together light up MSAA on the iOS Metal
backend:

1. `create_metal_view` calls `setSampleCount:` on `MTKView` from
   `Conf.sample_count` (the param was previously prefixed `_` and
   silently dropped). macOS already did this.

2. `new_pipeline` reads the view's `sampleCount` and sets it on
   `MTLRenderPipelineDescriptor`. A pipeline's sampleCount must match
   every render-pass color attachment it draws to; using the view's
   value as the canonical app-wide count keeps the main drawable and
   `render_target_msaa()` targets compatible with the same pipeline.

3. `new_texture` for `RenderTarget` access with `sample_count > 1`
   creates the texture as `MTLTextureType::D2Multisample` with the
   matching `setSampleCount:`, `setMipmapLevelCount:1`, and
   `RenderTarget`-only usage. The single-sample resolve texture for
   the same render target is created separately by the caller
   (e.g. `render_target_ex` in macroquad) and passed in via
   `new_render_pass_mrt`'s `resolve_img` slice.

Constraint: every render-pass color attachment must use the view's
sample count. Mixing single-sample and multisample targets in one app
would require per-target pipeline states, which Metal allows but
miniquad's "one pipeline per material" model does not currently
express. Callers that opt into `Conf.sample_count > 1` should use
`render_target_msaa()` (or matching) for every offscreen color target.
`begin_pass` unconditionally set the color attachment's storeAction to
`MTLStoreAction::Store`. That clobbered:

  - MTKView's `currentRenderPassDescriptor`, which already wires a
    `resolveTexture` (and matching `MultisampleResolve` storeAction)
    on the color attachment when the view's `sampleCount > 1`.
  - The descriptor built in `new_render_pass_mrt` for offscreen MSAA
    targets, which has the same shape.

Both paths require `MultisampleResolve` — `Store` on a color attachment
with a resolve texture trips Metal validation:

    RenderPass Descriptor Validation
    MTLRenderPassAttachmentDescriptor resolveTexture must have
    storeAction of MTLStoreActionMultisampleResolve,
    MTLStoreActionStoreAndMultisampleResolve or MTLStoreActionUnknown

Inspect the attachment's `resolveTexture` and pick the storeAction
accordingly.
…eResolve)

The previous commit replaced an unconditional `Store` with
`MultisampleResolve` whenever the color attachment had a
resolveTexture, fixing the validation crash. But Metal allows
`MultisampleResolve` to discard the multisample texture's contents
after the resolve, and macroquad issues one Metal pass per draw call
with `loadAction = Load` so it can accumulate draws on top of a single
clear at frame start.

On the iOS 27 simulator the discarded contents are loaded back as
magenta, so every draw after the clear renders on top of magenta and
the frame ends up magenta-tinted everywhere except the pixels touched
by the very last draw call.

`StoreAndMultisampleResolve` both writes the resolved data to the
drawable AND preserves the multisample texture, so a subsequent
`Load` reads back the accumulated frame.
@benface
benface force-pushed the metal-msaa-support branch from 7c74c42 to 7ce5c1a Compare July 23, 2026 19:21
@benface

benface commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@not-fl3 – I just rebased this, since it had conflicts. Ready for your review. 🙏

benface added a commit to benface/miniquad that referenced this pull request Jul 25, 2026
`aarch64-apple-tvos` and `aarch64-apple-tvos-sim` are stable Rust
tier-3 targets installable via plain `rustup target add`, but
miniquad currently misses tvOS from every `target_os = "ios"` cfg
gate, so a tvOS build fails to compile.

This commit widens the Apple-family gates so tvOS reuses the iOS
event loop (`native::ios::run`), UIKit link, Objective-C runtime,
and Metal storage-mode selection. No new tvOS-specific code is
added — the same iOS surface is compiled and dispatched for tvOS.

Files touched:

- `Cargo.toml` — pull `objc-rs` on tvOS as well.
- `src/lib.rs` — `start()` dispatch and `apple_view_ctrl()`.
  Without the dispatch widen, `start()` returns immediately on
  tvOS (the enclosing app exits voluntarily on launch).
- `src/native.rs` — `pub mod apple`, `pub mod ios`, and the
  `NativeDisplayData::view_ctrl` field + its initializer.
- `src/native/apple/frameworks.rs` — UIKit link (GLKit stays
  iOS-only since tvOS has no OpenGL ES).
- `src/graphics/metal.rs` — `UNIFORM_BUFFER_ALIGN` selection
  and the three `MTLResourceOptions` branches (matched to the
  post-not-fl3#640 shape now on master).

Verified with `cargo build` against all five Apple targets:

- `aarch64-apple-darwin` — clean
- `aarch64-apple-ios` — clean
- `aarch64-apple-ios-sim` — clean
- `aarch64-apple-tvos` — now builds
- `aarch64-apple-tvos-sim` — now builds

Not in scope (deliberate):

- **Input.** miniquad's iOS event loop reacts to `UITouch` on the
  `MTKView`. On tvOS, touch events only arrive from the Siri
  Remote's touch surface; menu / D-pad navigation goes through
  `UIPress` + the focus engine and is not wired here. An app
  built with this patch will render but won't accept
  remote-native input yet.
- **App-level scaffolding.** Info.plist scene manifest,
  `main.m` bridging, asset catalogs, LaunchScreen setup — all
  app-owned and out of scope for miniquad itself.

Sibling PRs of the same shape:
- raphamorim/objc-rs#3 — Apple runtime cfg widen (merged)
- not-fl3/macroquad#1056 — `load_file` CWD chdir for tvOS
@not-fl3

not-fl3 commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Thanks for PR!

@not-fl3
not-fl3 merged commit cec3dcc into not-fl3:master Jul 25, 2026
11 checks passed
not-fl3 pushed a commit that referenced this pull request Jul 25, 2026
`aarch64-apple-tvos` and `aarch64-apple-tvos-sim` are stable Rust
tier-3 targets installable via plain `rustup target add`, but
miniquad currently misses tvOS from every `target_os = "ios"` cfg
gate, so a tvOS build fails to compile.

This commit widens the Apple-family gates so tvOS reuses the iOS
event loop (`native::ios::run`), UIKit link, Objective-C runtime,
and Metal storage-mode selection. No new tvOS-specific code is
added — the same iOS surface is compiled and dispatched for tvOS.

Files touched:

- `Cargo.toml` — pull `objc-rs` on tvOS as well.
- `src/lib.rs` — `start()` dispatch and `apple_view_ctrl()`.
  Without the dispatch widen, `start()` returns immediately on
  tvOS (the enclosing app exits voluntarily on launch).
- `src/native.rs` — `pub mod apple`, `pub mod ios`, and the
  `NativeDisplayData::view_ctrl` field + its initializer.
- `src/native/apple/frameworks.rs` — UIKit link (GLKit stays
  iOS-only since tvOS has no OpenGL ES).
- `src/graphics/metal.rs` — `UNIFORM_BUFFER_ALIGN` selection
  and the three `MTLResourceOptions` branches (matched to the
  post-#640 shape now on master).

Verified with `cargo build` against all five Apple targets:

- `aarch64-apple-darwin` — clean
- `aarch64-apple-ios` — clean
- `aarch64-apple-ios-sim` — clean
- `aarch64-apple-tvos` — now builds
- `aarch64-apple-tvos-sim` — now builds

Not in scope (deliberate):

- **Input.** miniquad's iOS event loop reacts to `UITouch` on the
  `MTKView`. On tvOS, touch events only arrive from the Siri
  Remote's touch surface; menu / D-pad navigation goes through
  `UIPress` + the focus engine and is not wired here. An app
  built with this patch will render but won't accept
  remote-native input yet.
- **App-level scaffolding.** Info.plist scene manifest,
  `main.m` bridging, asset catalogs, LaunchScreen setup — all
  app-owned and out of scope for miniquad itself.

Sibling PRs of the same shape:
- raphamorim/objc-rs#3 — Apple runtime cfg widen (merged)
- not-fl3/macroquad#1056 — `load_file` CWD chdir for tvOS
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.

2 participants