Skip to content

iOS Metal: fix debug-only first-paint crashes#4972

Merged
shai-almog merged 2 commits into
masterfrom
metal-debug-first-paint-fix
May 17, 2026
Merged

iOS Metal: fix debug-only first-paint crashes#4972
shai-almog merged 2 commits into
masterfrom
metal-debug-first-paint-fix

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Summary

When a user builds with ios.metal=true and launches from Xcode (debug build, default Main Thread Checker + Metal API Validation enabled), the app aborts before the first form renders. Two independent bugs both fire on the very first Graphics.drawShape that goes through the alpha-mask path — and CSSBorder.paintBorderBackground triggers that on every form paint, so the symptom is "Metal app starts but never shows the first screen."

  • CN1MetalDevice() accessed [UIView layer] off the main thread. Paint runs on the EDT (a GCD background queue), and Graphics.drawShape → createAlphaMask → nativePathRendererCreateTexture → CN1MetalCreateAlphaMaskTexture reaches CN1MetalDevice() from there. -[UIView layer] is documented main-thread-only — Main Thread Checker aborts. Fixed by caching the device + queue via dispatch_once + MTLCreateSystemDefaultDevice() so neither helper ever touches a UIView property again.
  • metalLayer.framebufferOnly = YES while presentFramebuffer blits into the drawable. Metal API Validation aborts with destinationTexture must not be a framebufferOnly texture because the drawable cannot be a copyFromTexture destination when framebufferOnly = YES. Release builds with validation off silently produced undefined-behaviour copies on some GPUs. Fixed by setting framebufferOnly = NO; we forfeit the small memoryless-storage benefit but get a working present path.

Reported in the forum against build 7.0.243 (ios.metal=true) with the crash trace ending in CN1MetalDevice + the destinationTexture must not be a framebufferOnly texture assertion.

Test plan

  • Build an ios.metal=true app from Xcode with Main Thread Checker + Metal API Validation enabled (Xcode defaults) and confirm the first form paints instead of aborting.
  • Run the existing iOS Metal screenshot suite — no rendering regressions on the present path now that the drawable is no longer framebufferOnly.
  • Sanity-check a release build (validation off): first paint still works and no GPU-specific glitches on the present blit.

🤖 Generated with Claude Code

Two independent Metal-debug-only bugs aborted ios.metal=true apps before
the first form rendered when launched from Xcode. Each fires on the very
first drawShape that hits the alpha-mask path (CSSBorder.paintBorderBackground
triggers it on every form paint), so the symptom is "Metal app starts but
never shows the first screen."

1. CN1MetalDevice() read [UIView layer] from the EDT.
   The function dereferenced ((CAMetalLayer *)mv.layer).device, but
   -[UIView layer] is a main-thread-only API. CN1's EDT runs on a GCD
   background queue, and Graphics.drawShape -> createAlphaMask ->
   nativePathRendererCreateTexture -> CN1MetalCreateAlphaMaskTexture
   reaches CN1MetalDevice from that thread. Main Thread Checker aborts
   the process before the first paint completes. Fix: dispatch_once
   cache populated via MTLCreateSystemDefaultDevice + newCommandQueue;
   CN1MetalDevice / CN1MetalCommandQueue now return cached statics
   without touching any UIView property.

2. metalLayer.framebufferOnly = YES while presentFramebuffer blits into
   the drawable. presentFramebuffer does
   [blit copyFromTexture:screenTexture toTexture:dr.texture], and Metal
   API Validation aborts with "destinationTexture must not be a
   framebufferOnly texture" when the destination drawable was created
   framebufferOnly. Release builds with validation off silently produced
   undefined-behaviour copies on some GPUs. Fix:
   metalLayer.framebufferOnly = NO, forfeiting the small
   memoryless-storage benefit for a working present path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 17, 2026

✅ 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
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 17, 2026

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 65000 ms
Simulator Boot (Run) 0 ms
App Install 13000 ms
App Launch 7000 ms
Test Execution 283000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1268.000 ms
Base64 CN1 encode 1432.000 ms
Base64 encode ratio (CN1/native) 1.129x (12.9% slower)
Base64 native decode 898.000 ms
Base64 CN1 decode 1211.000 ms
Base64 decode ratio (CN1/native) 1.349x (34.9% slower)
Base64 SIMD encode 399.000 ms
Base64 encode ratio (SIMD/native) 0.315x (68.5% faster)
Base64 encode ratio (SIMD/CN1) 0.279x (72.1% faster)
Base64 SIMD decode 430.000 ms
Base64 decode ratio (SIMD/native) 0.479x (52.1% faster)
Base64 decode ratio (SIMD/CN1) 0.355x (64.5% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 57.000 ms
Image createMask (SIMD on) 9.000 ms
Image createMask ratio (SIMD on/off) 0.158x (84.2% faster)
Image applyMask (SIMD off) 118.000 ms
Image applyMask (SIMD on) 63.000 ms
Image applyMask ratio (SIMD on/off) 0.534x (46.6% faster)
Image modifyAlpha (SIMD off) 174.000 ms
Image modifyAlpha (SIMD on) 58.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.333x (66.7% faster)
Image modifyAlpha removeColor (SIMD off) 144.000 ms
Image modifyAlpha removeColor (SIMD on) 90.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.625x (37.5% faster)
Image PNG encode (SIMD off) 937.000 ms
Image PNG encode (SIMD on) 825.000 ms
Image PNG encode ratio (SIMD on/off) 0.880x (12.0% faster)
Image JPEG encode 432.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 17, 2026

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 91000 ms
Simulator Boot (Run) 1000 ms
App Install 11000 ms
App Launch 9000 ms
Test Execution 253000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 2152.000 ms
Base64 CN1 encode 1955.000 ms
Base64 encode ratio (CN1/native) 0.908x (9.2% faster)
Base64 native decode 1141.000 ms
Base64 CN1 decode 1256.000 ms
Base64 decode ratio (CN1/native) 1.101x (10.1% slower)
Base64 SIMD encode 477.000 ms
Base64 encode ratio (SIMD/native) 0.222x (77.8% faster)
Base64 encode ratio (SIMD/CN1) 0.244x (75.6% faster)
Base64 SIMD decode 475.000 ms
Base64 decode ratio (SIMD/native) 0.416x (58.4% faster)
Base64 decode ratio (SIMD/CN1) 0.378x (62.2% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 60.000 ms
Image createMask (SIMD on) 53.000 ms
Image createMask ratio (SIMD on/off) 0.883x (11.7% faster)
Image applyMask (SIMD off) 343.000 ms
Image applyMask (SIMD on) 170.000 ms
Image applyMask ratio (SIMD on/off) 0.496x (50.4% faster)
Image modifyAlpha (SIMD off) 257.000 ms
Image modifyAlpha (SIMD on) 183.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.712x (28.8% faster)
Image modifyAlpha removeColor (SIMD off) 375.000 ms
Image modifyAlpha removeColor (SIMD on) 110.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.293x (70.7% faster)
Image PNG encode (SIMD off) 1510.000 ms
Image PNG encode (SIMD on) 1032.000 ms
Image PNG encode ratio (SIMD on/off) 0.683x (31.7% faster)
Image JPEG encode 855.000 ms

@shai-almog shai-almog linked an issue May 17, 2026 that may be closed by this pull request
The first revision spun up a fresh MTLCommandQueue (via
MTLCreateSystemDefaultDevice + newCommandQueue under dispatch_once) for
CN1MetalCommandQueue() so it could avoid touching [UIView layer] from the
EDT. That regressed the iOS Metal screenshot suite: with mutable-image
setup CBs and the screen render CB on different queues, the DialogTheme
TextureBackdropPainter's cached diagonal-stripe image only rendered
behind the dialog, not below it. The header comment for
CN1MetalCommandQueue() already spelled out the requirement -- "Mutable-
image command buffers allocate from this queue so they share scheduling
with screen drawing." Apple's cross-queue dependency tracker did not
preserve visible output for this workload.

Fix: METALView.initWithCoder publishes its own MTLDevice + MTLCommandQueue
to the cache via CN1MetalSetDeviceAndCommandQueue (main thread, exactly
once). CN1MetalDevice / CN1MetalCommandQueue return those statics, still
never touching a UIView property -- so the off-main [UIView layer] bug
stays fixed -- but mutable-image setup CBs now FIFO with screen rendering
on the single queue, matching pre-PR ordering.

framebufferOnly = NO is unchanged from the first revision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog shai-almog merged commit de1ac33 into master May 17, 2026
18 of 19 checks passed
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.

Some initial issues with Metal

1 participant