iOS Metal: fix debug-only first-paint crashes#4972
Merged
Conversation
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>
Contributor
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Collaborator
Author
|
Compared 107 screenshots: 107 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
Collaborator
Author
|
Compared 107 screenshots: 107 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
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>
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.
Summary
When a user builds with
ios.metal=trueand 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 firstGraphics.drawShapethat goes through the alpha-mask path — andCSSBorder.paintBorderBackgroundtriggers 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), andGraphics.drawShape → createAlphaMask → nativePathRendererCreateTexture → CN1MetalCreateAlphaMaskTexturereachesCN1MetalDevice()from there.-[UIView layer]is documented main-thread-only — Main Thread Checker aborts. Fixed by caching the device + queue viadispatch_once+MTLCreateSystemDefaultDevice()so neither helper ever touches aUIViewproperty again.metalLayer.framebufferOnly = YESwhilepresentFramebufferblits into the drawable. Metal API Validation aborts withdestinationTexture must not be a framebufferOnly texturebecause the drawable cannot be acopyFromTexturedestination whenframebufferOnly = YES. Release builds with validation off silently produced undefined-behaviour copies on some GPUs. Fixed by settingframebufferOnly = 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 inCN1MetalDevice+ thedestinationTexture must not be a framebufferOnly textureassertion.Test plan
ios.metal=trueapp from Xcode with Main Thread Checker + Metal API Validation enabled (Xcode defaults) and confirm the first form paints instead of aborting.framebufferOnly.🤖 Generated with Claude Code