Skip to content

macOS/iOS 27 SDK: AVSampleBufferDisplayLayer is main-actor isolated, 12 warning sites in the renderer #351

Description

@superuser404notfound

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:

  1. 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).
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions