Skip to content

iOS Metal: rebuild private-storage textures after suspend (#5349) - #5382

Merged
shai-almog merged 1 commit into
masterfrom
fix-ios-5349-metal-texture-discard
Jul 15, 2026
Merged

iOS Metal: rebuild private-storage textures after suspend (#5349)#5382
shai-almog merged 1 commit into
masterfrom
fix-ios-5349-metal-texture-discard

Conversation

@shai-almog

@shai-almog shai-almog commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

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 MTLStorageModePrivate textures 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-screen screenTexture. 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 screenTexture were left exposed.

The 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 Metal issue: on iPhone 15 the FloatingActionButton is sometimes shown with violet background #5153 readback-on-resign-active, unchanged.

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-time lockPurgeableObjects validation 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:

Toolbar on resume
pre-fix 10–79% magenta
screenTexture clear only still magenta
full fix 0% (clean) across repeated runs incl. a max-corruption pass

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

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 143 screenshots: 143 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 244 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 93ms / native 4ms = 23.2x speedup
SIMD float-mul (64K x300) java 62ms / native 3ms = 20.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 235.000 ms
Base64 CN1 decode 124.000 ms
Base64 native encode 828.000 ms
Base64 encode ratio (CN1/native) 0.284x (71.6% faster)
Base64 native decode 16030.000 ms
Base64 decode ratio (CN1/native) 0.008x (99.2% faster)
Base64 SIMD encode 50.000 ms
Base64 encode ratio (SIMD/CN1) 0.213x (78.7% faster)
Base64 SIMD decode 57.000 ms
Base64 decode ratio (SIMD/CN1) 0.460x (54.0% faster)
Base64 encode ratio (SIMD/native) 0.060x (94.0% faster)
Base64 decode ratio (SIMD/native) 0.004x (99.6% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 10.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.200x (80.0% faster)
Image applyMask (SIMD off) 80.000 ms
Image applyMask (SIMD on) 70.000 ms
Image applyMask ratio (SIMD on/off) 0.875x (12.5% faster)
Image modifyAlpha (SIMD off) 91.000 ms
Image modifyAlpha (SIMD on) 153.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.681x (68.1% slower)
Image modifyAlpha removeColor (SIMD off) 197.000 ms
Image modifyAlpha removeColor (SIMD on) 137.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.695x (30.5% faster)

@shai-almog

shai-almog commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 216 screenshots: 216 matched.
✅ Native Apple Watch (watchOS, Core Graphics) screenshot tests passed.

@shai-almog

shai-almog commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 144 screenshots: 144 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 245 seconds

Build and Run Timing

Metric Duration
Simulator Boot 53000 ms
Simulator Boot (Run) 1000 ms
App Install 12000 ms
App Launch 2000 ms
Test Execution 347000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 59ms / native 3ms = 19.6x speedup
SIMD float-mul (64K x300) java 56ms / native 3ms = 18.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 1774.000 ms
Base64 CN1 decode 521.000 ms
Base64 native encode 1770.000 ms
Base64 encode ratio (CN1/native) 1.002x (0.2% slower)
Base64 native decode 1086.000 ms
Base64 decode ratio (CN1/native) 0.480x (52.0% faster)
Base64 SIMD encode 161.000 ms
Base64 encode ratio (SIMD/CN1) 0.091x (90.9% faster)
Base64 SIMD decode 261.000 ms
Base64 decode ratio (SIMD/CN1) 0.501x (49.9% faster)
Base64 encode ratio (SIMD/native) 0.091x (90.9% faster)
Base64 decode ratio (SIMD/native) 0.240x (76.0% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 9.000 ms
Image createMask (SIMD on) 8.000 ms
Image createMask ratio (SIMD on/off) 0.889x (11.1% faster)
Image applyMask (SIMD off) 256.000 ms
Image applyMask (SIMD on) 270.000 ms
Image applyMask ratio (SIMD on/off) 1.055x (5.5% slower)
Image modifyAlpha (SIMD off) 51.000 ms
Image modifyAlpha (SIMD on) 69.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.353x (35.3% slower)
Image modifyAlpha removeColor (SIMD off) 315.000 ms
Image modifyAlpha removeColor (SIMD on) 174.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.552x (44.8% faster)

@shai-almog

shai-almog commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 139 screenshots: 139 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

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
shai-almog force-pushed the fix-ios-5349-metal-texture-discard branch from 176e918 to 3da84d3 Compare July 14, 2026 18:22
@shai-almog

shai-almog commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 138 screenshots: 138 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 400 seconds

Build and Run Timing

Metric Duration
Simulator Boot 89000 ms
Simulator Boot (Run) 2000 ms
App Install 18000 ms
App Launch 1000 ms
Test Execution 560000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 59ms / native 4ms = 14.7x speedup
SIMD float-mul (64K x300) java 55ms / native 2ms = 27.5x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 201.000 ms
Base64 CN1 decode 376.000 ms
Base64 native encode 1389.000 ms
Base64 encode ratio (CN1/native) 0.145x (85.5% faster)
Base64 native decode 1235.000 ms
Base64 decode ratio (CN1/native) 0.304x (69.6% faster)
Base64 SIMD encode 65.000 ms
Base64 encode ratio (SIMD/CN1) 0.323x (67.7% faster)
Base64 SIMD decode 71.000 ms
Base64 decode ratio (SIMD/CN1) 0.189x (81.1% faster)
Base64 encode ratio (SIMD/native) 0.047x (95.3% faster)
Base64 decode ratio (SIMD/native) 0.057x (94.3% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 10.000 ms
Image createMask (SIMD on) 3.000 ms
Image createMask ratio (SIMD on/off) 0.300x (70.0% faster)
Image applyMask (SIMD off) 127.000 ms
Image applyMask (SIMD on) 114.000 ms
Image applyMask ratio (SIMD on/off) 0.898x (10.2% faster)
Image modifyAlpha (SIMD off) 209.000 ms
Image modifyAlpha (SIMD on) 208.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.995x (0.5% faster)
Image modifyAlpha removeColor (SIMD off) 213.000 ms
Image modifyAlpha removeColor (SIMD on) 447.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 2.099x (109.9% slower)

@shai-almog
shai-almog merged commit dec3d17 into master Jul 15, 2026
27 of 28 checks passed
@shai-almog
shai-almog deleted the fix-ios-5349-metal-texture-discard branch July 15, 2026 01:04
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.

Still seeing some rare Metal issues such as FAB or switches with violet background or a component not being fully drawn until touching the UI

1 participant