8000 metal: implement texture_read_pixels by benface · Pull Request #674 · not-fl3/miniquad · GitHub
[go: up one dir, main page]

Skip to content

metal: implement texture_read_pixels - #674

Open
benface wants to merge 1 commit into
not-fl3:masterfrom
benface:metal-texture-read-pixels
Open

benface wants to merge 1 commit into
not-fl3:masterfrom
benface:metal-texture-read-pixels

Conversation

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

Why

The Metal backend leaves texture_read_pixels unimplemented!(), so nothing drawn through Metal can be read back to the CPU: no screenshots, recordings or frame comparisons in tests.

Change

  • Render targets live in private storage, out of the CPU's reach, so the texture is blitted into a shared buffer and copied out of it.
  • It waits for the GPU, as a read-back does on GL. The blit has to run after whatever the frame already drew into the texture, so it joins the frame's command buffer, which is committed and waited on. A new command buffer carries the rest of the frame.
  • Pixel formats:
    • RGBA8 textures come back as stored.
    • Color render targets come back in RGBA order. Metal gives them the view's BGRA8Unorm format, so their pixels are swapped to the order TextureFormat::RGBA8 describes, which is what GL returns.
    • Multisampled textures aren't handled, and assert. Depth textures aren't handled either.
  • As on GL, rows come back in the order texture_update takes them. A render pass draws the top of its viewport into a texture's first row on Metal and into its last row on GL, so a caller that reads back a render target flips the rows on one of the two.

Verification

  • On macOS with MTL_DEBUG_LAYER=1: a texture created from bytes reads back the same bytes, and a render target cleared to red reads back as red in RGBA order in the middle of a frame, which then carries on and presents.
  • A macroquad game (CHOMP) records its frames on Metal this way: each one is drawn into a render target and read back. Five of its levels, 2,579 frames, ran under validation with no error, and repeated runs give byte-identical frames.
  • 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.

The Metal backend left `texture_read_pixels` `unimplemented!()`, so
nothing drawn through Metal could be read back to the CPU.

Render targets live in private storage, so the texture is blitted into
a shared buffer and copied out of it. The blit joins the frame's
command buffer, so it runs after whatever the frame already drew into
the texture; that command buffer is committed and waited on, and a
fresh one carries the rest of the frame. Render targets take the
view's BGRA pixel format, so their pixels are swapped to the RGBA
order `TextureFormat::RGBA8` describes, which is what OpenGL returns.

As on OpenGL, rows come back in the order `texture_update` takes them.
A render pass draws the top of its viewport into a texture's first row
on Metal and into its last row on OpenGL, so a caller that reads back
a render target flips the rows on one of the two.

Verified on macOS: a texture created from bytes reads back the same
bytes, and a render target cleared to red reads back as red in RGBA
order in the middle of a frame, which then carries on and presents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@benface
benface force-pushed the metal-texture-read-pixels branch from 5e8ecc2 to 4bec0ef Compare October 18, 2026 17:53
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