iOS Metal: rebuild private-storage textures after suspend (#5349) - #5382
Merged
Conversation
Contributor
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Collaborator
Author
|
Compared 143 screenshots: 143 matched. Benchmark Results
Detailed Performance Metrics
|
Collaborator
Author
|
Compared 216 screenshots: 216 matched. |
Collaborator
Author
|
Compared 144 screenshots: 144 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
Collaborator
Author
|
Compared 139 screenshots: 139 matched. |
Fixes rare violet/magenta fills and partially-drawn components (FAB shadow, Switch thumb, unselected Tabs, Toolbar) that appear after bringing the app back to the foreground on the Metal backend. Root cause: iOS discards the contents of MTLStorageModePrivate textures while the app is suspended (and under memory pressure). CN1 caches such textures per image and per glyph atlas, plus a persistent screenTexture, so on resume it samples the discarded garbage on any surface the diff-painter does not fully repaint that frame. Fix (iOS Metal path only, no portable/core changes): - screenTexture (METALView.m): invalidateRetainedFramebuffer now forces a full clear on the next frame, instead of only clearing when the first post-foreground repaint happens to cover the whole screen (a partial animation repaint routinely landed first and pinned the stale contents). - Read-only image textures (GLUIImage + CN1Metalcompat): a texture-validate generation counter, bumped on foreground and on memory warning, makes getMTLTexture re-decode its cached read-only texture from the retained UIImage the first time it is sampled in a newer generation. Every GPU-backed image (not just mutable render targets) is now registered so the suspend backup drops its texture, and glyph atlases are dropped too. - Mutable images (existing #5153 backup): unchanged; still read back to a UIImage on resign-active. A generation compare is the only recovery signal used on the hot path: a per-draw purgeable-state probe (setPurgeableState) was tried and dropped because it trips Metal's commit-time lockPurgeableObjects validation on a texture already referenced by an in-flight command buffer. Verified with a texture fault-injector that fills every private texture on suspend: pre-fix the Toolbar returned 10-79% magenta on resume; with the fix it is 0% across repeated runs including a max-corruption pass. The app also renders the full iOS Metal screenshot suite (143 tests) under Metal API validation in assert mode with no crash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
shai-almog
force-pushed
the
fix-ios-5349-metal-texture-discard
branch
from
July 14, 2026 18:22
176e918 to
3da84d3
Compare
Collaborator
Author
|
Compared 138 screenshots: 138 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
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.
Fixes #5349 — rare violet/magenta fills and partially-drawn components (FAB shadow, Switch thumb, unselected Tabs, Toolbar) that appear on the Metal backend after bringing the app back to the foreground.
Root cause
iOS discards the contents of
MTLStorageModePrivatetextures while the app is suspended (and can reclaim them under memory pressure). CN1 caches such textures per image and per glyph atlas, plus a persistent full-screenscreenTexture. On resume it samples the discarded garbage on any surface the diff-painter does not fully repaint that frame — which renders as the violet fill / "not drawn until you touch it" the reporter described. This is legal-but-undefined Metal, so Apple's API validation never flagged it (the CI Metal-validation job stays green), and it only reproduces on a real device.Extends the partial #5153 fix, which only protected registered mutable images; static/theme image textures, glyph atlases, and the
screenTexturewere left exposed.The fix (iOS Metal path only — no portable/core changes)
screenTexture(METALView.m):invalidateRetainedFramebuffernow forces a full clear on the next frame instead of only clearing when the first post-foreground repaint happens to cover the whole screen (a partial animation repaint routinely landed first and pinned the stale contents).GLUIImage+CN1Metalcompat): a texture-validate generation counter, bumped on foreground and on memory warning, makesgetMTLTexturere-decode its cached read-only texture from the retainedUIImagethe first time it is sampled in a newer generation. Every GPU-backed image (not just mutable render targets) is now registered so the suspend backup drops its texture, and glyph atlases are dropped too.A plain generation compare is the only recovery signal on the hot path. A per-draw purgeable-state probe (
setPurgeableState) was tried and dropped — it trips Metal's commit-timelockPurgeableObjectsvalidation on a texture already referenced by an in-flight command buffer (it crashed the first CI run in exactly that assertion).Verification
Reproduced deterministically with a texture fault-injector (injected via
DYLD_INSERT_LIBRARIES, no framework changes) that fills every private-storage texture with magenta on suspend:The app also renders the full iOS Metal screenshot suite (143 tests) under Metal API + shader validation in assert mode with no crash.
🤖 Generated with Claude Code