forked from unom/punktfunk
WP6 of design/presenter-cadence-rework-implementation-plan.md — the Apple
binding of the CadenceClock landed in e207d0d7.
Apple's video pipeline is Swift and does not link the Rust core, so this is a
hand-written twin the way AudioRing is of JitterPolicy. That kind of port is only
as good as its evidence of agreement, so beyond the ten mirrored unit tests the
two implementations were run against each other numerically: disposable
harnesses on both sides, importing the REAL Rust type, printing eight vectors —
LCG output, the 400-frame settle state, head and tail due times, the ramp's
type-2 versus type-1 error, a lumpy source's dues, cushion and jitter under wide
jitter, and an outlier + gap + regression + off-cadence event sequence. All eight
lines diffed identical, down to `offset=-1784999999987888109 skew=946
jitter=514645`. Both scaffolds are deleted; the doc comments name the Rust type
and those vectors as the contract.
The clock-domain trap is the whole risk here and it is worth stating plainly:
`presentAtMediaTime` consumes `CACurrentMediaTime`, while `decodedNs` is
CLOCK_REALTIME, and §2.2's rule is one domain in, same domain out. So
`mediaTimeNs(forRealtimeNs:)` is written as the exact inverse of the existing
media→realtime bridge, reading the two clocks in the same order so the sub-µs
skew between them cancels on a round trip. The conversion happens ONCE, on the
way in; the due time comes back in media time and is consumed by `nextVsync` and
the store predicate with no second conversion — which is precisely what
`domainOffsetIsAbsorbed` licenses. It is per frame rather than per session
because the two clocks diverge across device sleep, the one case where the offset
is not constant.
The deadline presenter (iPhone/iPad) is included even though WP6 names only
`presentAt`, which that loop has no equivalent of: its link vend IS the grid
snap, so the due-gated `take` alone makes it cadence-driven. Leaving it out would
have made WP8's "Mac + iPhone" leg test nothing on half its hardware.
Preroll retires only on the due-gated path; plain `take()` and its tests are
untouched. On the cadence path `underflows` is deliberately not counted — an
empty store is the normal steady state once frames are held until due, and
`CadenceHealth.late` is the honest starvation signal instead.
Smoothness only: the clock exists iff the store policy is FIFO, so under
`latency` `takeReady` is the old `ring.take()`, `dueMediaTime` is nil and
`max(now, now)` is the old expression, and the stats line has no cadence segment.
Verified: `swift test` 319 passing, 0 failures, including the ten new ones.
⚠ iOS/tvOS could not be typechecked — the checked-in xcframework's Info.plist
declares only `macos-arm64`, which is pre-existing and unrelated. Confirmed
instead that no added line sits inside any `#if os(...)`, so the macOS build
typechecks the entire diff.