Split out of #313, which is the build failure that led here and is fixed and released (6.12.1). This issue tracks what the 27 SDK change leaves behind, which is warnings only today.
What changed
In the macOS/iOS 27 beta 4 SDK, AVSampleBufferDisplayLayer is annotated @MainActor (whole class, reported by @jihongboo from the headers). The engine touches the layer from the decode thread, so every such access is now an isolation violation.
Why it is warnings and not errors
The annotation arrives through an ObjC import, which carries preconcurrency semantics. Measured against a ~20 line ObjC module (__attribute__((swift_attr("@MainActor"))) on the class, non-Sendable object property) with swiftc -typecheck -swift-version 6, Swift 6.3.3:
| shape |
severity |
| sync read of a main-actor property from a nonisolated context |
warning |
| sync mutation of a main-actor property |
warning |
| sync call of a main-actor method |
warning, group #ImplicitStrongCapture... see note |
non-Sendable value pulled out with await from nonisolated async |
error |
(the method call lands in the #ActorIsolatedCall group.)
Isolation violations against imported declarations are downgraded to warnings. The region rule that governs a non-Sendable value leaving the actor is not, which is why #313 saw exactly one error.
The hot path needs nothing
queueTarget is typed any AVQueuedSampleBufferRendering, and that protocol did not move to the main actor. In the reporter's build, queueTarget.isReadyForMoreMediaData (SampleBufferRenderer.swift:165), the enqueue path through let target = queueTarget (:342) and the diagnostic read (:368) produce no diagnostic at all. The per-frame calls from the decode thread are already legal.
The 12 sites
Acquisition of the target:
SampleBufferRenderer.swift :158, :170, :177, :300 (displayLayer.sampleBufferRenderer)
AudioOutput.swift :29, :43 (same, to hand the renderer to the synchronizer)
Layer-only API on the pre-18 branch:
SampleBufferRenderer.swift :172, :179 (displayLayer.status / .error), :303, :305 (flushAndRemoveImage() / flush())
Layer configuration in the factory:
SampleBufferRenderer.swift :184, :188 (videoGravity, preventsDisplaySleepDuringVideoPlayback)
Candidate shapes, not built
Caching sampleBufferRenderer in a stored let is the wrong shape: displayLayer is a private(set) var and is replaced when HDR output changes, so a stored renderer becomes a second source of truth that the next layer swap can forget to update. Same trap as #321.
What holds:
- A
@MainActor factory that produces the layer and its queue target together, so the pair can never disagree, and makeDisplayLayer becomes main-actor isolated on the way (covers :184, :188 for free).
- Raising the platform floor to tvOS 18 / iOS 18 / macOS 15 and deleting the legacy branch, which removes :172, :179, :303, :305 outright. That is a major, see the floor-raise rule in Package.swift.
Trigger
Nothing is built now: the SDK can still move before GM and the diagnostics cost nothing today. Act when a beta or the GM turns any of the 12 into an error, or when a floor raise is on the table for other reasons.
🤖 Generated with Claude Code
https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE
Split out of #313, which is the build failure that led here and is fixed and released (6.12.1). This issue tracks what the 27 SDK change leaves behind, which is warnings only today.
What changed
In the macOS/iOS 27 beta 4 SDK,
AVSampleBufferDisplayLayeris annotated@MainActor(whole class, reported by @jihongboo from the headers). The engine touches the layer from the decode thread, so every such access is now an isolation violation.Why it is warnings and not errors
The annotation arrives through an ObjC import, which carries preconcurrency semantics. Measured against a ~20 line ObjC module (
__attribute__((swift_attr("@MainActor")))on the class, non-Sendable object property) withswiftc -typecheck -swift-version 6, Swift 6.3.3:#ImplicitStrongCapture... see noteawaitfrom nonisolated async(the method call lands in the
#ActorIsolatedCallgroup.)Isolation violations against imported declarations are downgraded to warnings. The region rule that governs a non-Sendable value leaving the actor is not, which is why #313 saw exactly one error.
The hot path needs nothing
queueTargetis typedany AVQueuedSampleBufferRendering, and that protocol did not move to the main actor. In the reporter's build,queueTarget.isReadyForMoreMediaData(SampleBufferRenderer.swift:165), the enqueue path throughlet target = queueTarget(:342) and the diagnostic read (:368) produce no diagnostic at all. The per-frame calls from the decode thread are already legal.The 12 sites
Acquisition of the target:
SampleBufferRenderer.swift:158, :170, :177, :300 (displayLayer.sampleBufferRenderer)AudioOutput.swift:29, :43 (same, to hand the renderer to the synchronizer)Layer-only API on the pre-18 branch:
SampleBufferRenderer.swift:172, :179 (displayLayer.status/.error), :303, :305 (flushAndRemoveImage()/flush())Layer configuration in the factory:
SampleBufferRenderer.swift:184, :188 (videoGravity,preventsDisplaySleepDuringVideoPlayback)Candidate shapes, not built
Caching
sampleBufferRendererin a storedletis the wrong shape:displayLayeris aprivate(set) varand is replaced when HDR output changes, so a stored renderer becomes a second source of truth that the next layer swap can forget to update. Same trap as #321.What holds:
@MainActorfactory that produces the layer and its queue target together, so the pair can never disagree, andmakeDisplayLayerbecomes main-actor isolated on the way (covers :184, :188 for free).Trigger
Nothing is built now: the SDK can still move before GM and the diagnostics cost nothing today. Act when a beta or the GM turns any of the 12 into an error, or when a floor raise is on the table for other reasons.
🤖 Generated with Claude Code
https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE