Skip to content

fix: CI green again — Swift 6.2 type-checker limits, duplicated subtitle cues, AV1 test on VMs - #41

Merged
bilipp merged 4 commits into
mainfrom
fix/ci-swift-6.2-typecheck
Sep 29, 2026
Merged

bilipp merged 4 commits into
mainfrom
fix/ci-swift-6.2-typecheck

Conversation

@bilipp

@bilipp bilipp commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

CI on main has been red since 069a818, and #39/#40 added more failures. None of it was a logic bug in those PRs. Three separate problems:

1. fix(ci): keep the sources within Swift 6.2's type checker

The workflow asks for Xcode_26.app, which the macos-15 image no longer has, so the fallback selects Xcode 26.3 (Swift 6.2). Local development uses Xcode 27 (Swift 6.4), and code that 6.4 accepts instantly, 6.2 rejects:

Where 6.2 error Fix
Tests/.../Support/ChannelDrain.swift:88 (on main since 069a818) initializer for conditional binding must have Optional type: inside the initializer of the local result, a bare result binds to that local if let stored = self.result
DolbyVisionMapping.swift:174 Matrix3 (#39) unable to type-check this expression in reasonable time: nested reduce(0) closures over untyped literals plain loops, explicit types
DolbyVisionTests.extraction (#39) 2.4 s to type-check even on 6.4: #expect(pieces == [.polynomial(…)]) typed expected values
HEVCBaseLayerFilterTests hvcC builder (#40) ~0.1 s on 6.4 loop

I found the first slow spots by compiling with -warn-long-expression-type-checking=25 on 6.4. But 6.4's timings turned out to be a poor guide: CI's first run on this PR rejected three more test expressions that 6.4 checks in milliseconds. They're fixed by 84f8466:

  • DolbyVisionTests' Reference.multiply/apply, the same reduce(0) shape;
  • the HEVCBaseLayerFilterTests + chains over untyped literals.

Swift 6.2 isn't installed locally, so CI is the real check.

3. fix(test): don't take VideoToolbox's word on AV1 hardware

Once everything compiled, the AV1 test failed on CI: 72 of ~96 frames, all from software, although VTIsHardwareDecodeSupported(AV1) returned true. The runner is a virtualized Mac whose VideoToolbox claims AV1 and then fails it. The engine did what its recovery policy says:

  • rebuild on dav1d,
  • emit .downgradedToSoftware,
  • resume at the next keyframe, one 24-frame GOP later.

The test now distinguishes three cases:

  • no AV1 hardware claimed (the Apple TV case): dav1d from the first frame, nothing lost;
  • hardware claimed and working: hardware frames;
  • hardware claimed but failing: a reported downgrade that loses at most one GOP.

2. fix(subtitle): store a re-read cue once, not again

"embedded SRT track produces timed cues" failed intermittently, including on clean main: one run in 3 to 9 under full-suite load. Printing the store on a failing run showed every cue stored twice. That's an engine bug, not a test problem.

SubtitleStore is a timeline that deliberately survives seeks, but the decoder inserted every cue it decoded. Whenever the demuxer re-read subtitle packets it had already delivered, the cues were stored again:

  • on every backward seek: users saw that range's lines doubled;
  • in the backfill seek of a track selected late (selectSubtitleTrack), the test's path: when live delivery won the race against that seek, both copies landed.

insert() now drops a cue identical to one it already holds (same start, end and trimmed text). Different cues that share a start both stay.

Type of change

  • fix — bug fix
  • feat — new capability
  • perf — performance improvement
  • refactor — neither fixes a bug nor adds a feature
  • docs — documentation only
  • test — adding or fixing tests
  • build / chore — build pipeline, tooling, or maintenance

Invariants

None affected: the only data-plane change is the dedup inside SubtitleStore.insert, under its existing lock.

  • No blocking FFmpeg call was added to an actor, and no stage bypasses Channel.
  • No rebuild-in-place: a new URL is still a new session object.
  • Raw container timestamps still stop at TimestampUnwrapper.
  • A/V sync is still owned by AVSampleBufferRenderSynchronizer (no new heuristics or clocks).
  • No force unwraps, as!, fatalError, or assertionFailure in engine paths; failures are typed.
  • No new global mutable state; configuration still flows through PlayerConfiguration.
  • No control flow derived from av_log strings.

Verification

  • swift build is warning-free. Nothing new warns; Render/SystemRenderer.swift:63-64 still has its two existing main-actor isolation warnings.
  • swift test is green, six runs out of six (Swift 6.4, local dav1d xcframework):
Test run with 104 tests in 16 suites passed after 19.169 seconds.
  • I added or updated tests for this change.
    • storeDeduplicates: a store unit test.
    • seekBackKeepsCuesUnique: reproduces the doubling deterministically.
    • Both fail with the dedup reverted (5 issues).
    • The AV1 decode test now separates "VT claims AV1" from "VT decodes AV1".
  • New media fixtures: none.

Platforms verified

  • macOS
  • iOS / iPadOS: CI build job
  • tvOS: CI build job
  • visionOS

Not in this PR

  • The workflow still asks for Xcode_26.app and silently falls back to the newest Xcode on the image, so the CI toolchain can change without notice. It may be worth pinning it explicitly, e.g. to Xcode_26.3.app, or to whatever minimum toolchain the package should support.

CI has been red since 069a818, and #39/#40 added two more failures of
the same kind. The workflow asks for Xcode_26.app, which the macos-15
image no longer has; the fallback selects Xcode 26.3 — Swift 6.2 —
while local development runs Xcode 27 (Swift 6.4). Code that 6.4
accepts at once, 6.2 rejects:

- ChannelDrain.value: a bare `result` inside the initializer of the
  local `result` binds to that local on 6.2 ("initializer for
  conditional binding must have Optional type"). Now `self.result`.
- Matrix3.multiply/inverse: nested `reduce(0)` closures over untyped
  literals exceed 6.2's solver ("unable to type-check this expression
  in reasonable time"). Plain loops with explicit types.
- DolbyVisionTests.extraction: `#expect(pieces == [.polynomial(...)])`
  took 6.4 2.4 s to type-check; the expected values are now typed
  locals. HEVCBaseLayerFilterTests' hvcC array builder (0.1 s) is
  a loop.

Found by compiling with -warn-long-expression-type-checking=25 on 6.4:
nothing is above 40 ms any more, the level the existing tests already
passed CI at. Behaviour is unchanged. The suite passes apart from the intermittent
SRT cue test, a real duplicate-cue bug fixed in the next commit.
"embedded SRT track produces timed cues" failed intermittently — on
clean main as well, one run in three to nine. Printing the store on a
failing run showed every cue twice (ids 0/2 and 1/3), and the bug is
not the test's.

SubtitleStore is a timeline that deliberately survives seeks, but the
decoder inserted every cue it decoded. So whenever the demuxer re-read
subtitle packets it had already delivered, the cues were stored again:
- on every backward seek — users saw the lines of that range doubled;
- in the backfill seek of a track selected after the demuxer had read
  past it (selectSubtitleTrack), which is the test's path: when live
  delivery won the race against that seek, both copies landed.

insert() now drops a cue identical to one it holds (same start, end,
and trimmed text); cues sharing a start but differing otherwise both
stay. The same-start run sits right before the insertion point, so the
check costs nothing extra.

New tests: a store unit test, and "seeking back does not duplicate
cues", which reproduces the doubling deterministically — both fail
without the fix. The full suite passed six runs out of six afterwards.
With the engine fixed, CI's Xcode 26.3 got as far as the tests and
rejected three more expressions that Swift 6.4 checks in a few
milliseconds — so -warn-long-expression-type-checking on 6.4 is no
guide to what 6.2 accepts:

- DolbyVisionTests' Reference.multiply/apply: the same nested
  `map { reduce(0) { … } }` shape Matrix3 had; now loops. Its MMR sum,
  the same shape, goes with them.
- HEVCBaseLayerFilterTests.hvcC: long `header + [6] + array(…) + …`
  chains over untyped literals; now joined through a typed variadic.
On CI the AV1 test failed with 72 of ~96 frames, all from software,
although VTIsHardwareDecodeSupported(AV1) said true. The runner is a
virtualized Mac whose VideoToolbox claims AV1 and then fails it; the
engine did what its recovery policy says — rebuilt on dav1d, emitted
.downgradedToSoftware, resumed at the next keyframe, one 24-frame GOP
later. The test assumed the claim always holds.

It now tells the cases apart. Without claimed AV1 hardware — the case
that played black (Apple TV 4K, M1) — dav1d must decode from the first
frame, with no hardware attempt and nothing lost. With claimed hardware
it accepts either hardware frames, or a reported downgrade that loses
at most one GOP; never a silent failure.
@bilipp bilipp changed the title fix: CI green again — Swift 6.2 type-checker limits, and duplicated subtitle cues fix: CI green again — Swift 6.2 type-checker limits, duplicated subtitle cues, AV1 test on VMs Sep 29, 2026
@bilipp
bilipp merged commit 577a6e8 into main Sep 29, 2026
4 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.

1 participant