Skip to content

The master-refusal latch goes stale after an output-format change, and costs more than the master route #588

Description

@superuser404notfound

panelRefusedHDRMaster is latched for the life of the process, and nothing ever clears it. The latch is correct at the moment it is set and stale from the moment the user changes the Apple TV's output format, which they have to leave the app to do and which the platform never reports. An Apple TV can stay in the same app process for days, so the window for the stale state is effectively unbounded, the user has no indication anything is wrong, and the only cure is force-quitting.

The code already predicts this, in its own comment on AetherEngine.swift:

Not cleared when the user changes the output format mid-process, because nothing reports that either. A stale refusal costs the master route until the app restarts, which is the behaviour this replaces rather than a regression from it.

That trade was made against a cost of "the master route". Measurements on #459 show the cost is larger than that and all of it is user-visible.

The measurement

All of this is classicjazz's work, reported in #459 (comment). Sodalite 1.0.0 (37), AetherEngine 7.10.0, tvOS 27.0 (24J361), two Apple TV 4K (AppleTV14,1), two LG OLEDs, same HDR10 title throughout.

Same file, same box, 100 seconds apart. Box B, healthy process:

19:03:52.136  panel unproven but HDR-eligible: serving the master and letting
              AVPlayer answer (refusal costs one in-place media fallback)
19:03:52.163  serving on .../master.m3u8 (panelIsHDR=true ... useMaster=true)
19:03:53.565  republishing videoFormat sdr -> hdr10: AVFoundation accepted the
              HDR master (#459)

Box A, identical panel readout at load, no attempt line at all:

19:05:33.842  serving on .../media.m3u8 (panelIsHDR=false ... useMaster=false)
19:05:47.405  playback probe: no HDR reading in 12158ms ...

Of the four terms in sessionRoutesAsHDRPanel, three are identical across the two runs: panelPresentsHDR false on both, attemptsHDRMasterOnUnprovenPanel defaults true and the host never sets it, supportsHDR true on both per DisplayCapabilities observed. That leaves the latch. A force-quit and a fresh process on the same box, same file, same television confirms it:

19:11:36.580  serving on .../master.m3u8 (panelIsHDR=true ... useMaster=true)
19:11:37.911  republishing videoFormat sdr -> hdr10: AVFoundation accepted the
              HDR master (#459)

The afternoon before those captures had been spent switching the box's output format and Match Dynamic Range back and forth, so at some point the process genuinely was in a mode that refused the master. The latch recorded a true fact about a configuration that no longer exists.

Worth stating plainly because it bounds the fix: the headroom leg is dead on both of these panels. currentEDR=1.00 potentialEDR=1.00 at load and at teardown, and the explicit probe verdict after 12 seconds of PQ on screen is no HDR reading in 12158ms (max headroom 1.00, 48 samples). On this hardware everything that works is master acceptance, so losing the master route loses the whole answer.

What a stale latch costs

  • The label. Stats for Nerds reads "Dolby Vision -> SDR" or "HDR10 -> SDR" for every title in the process, which is the complaint Stats report wrong current display mode: HDR10+ -> SDR (when display is HDR locked) #459 was opened about in the first place.
  • Dolby Vision signaling. On a Profile 7 source the engine builds the supplemental descriptor and then has nowhere to put it, because a media playlist has no variant to carry it:
    18:35:49.351  DV source: profile=7 compat=6 level=6 rpu=1 el=1 bl=1
    18:35:49.354  prepared: ... supplemental=dvh1.08.06/db1p ... DV=profile7
    18:35:49.355  serving on .../media.m3u8 (... useMaster=false dvVariant=profile7)
    18:35:49.685  item videoFormat=hdr10 subType='hvc1'
    
  • Subtitles in PiP. No master means no EXT-X-MEDIA:TYPE=SUBTITLES rendition. Cues kept publishing to the on-frame overlay for the whole time PiP was up (seven outcome=published in thirty seconds, subActive=true) and nothing was visible in the window. Controlled against SDR on the same display with the same PGS path: SDR is routing-safe without panel proof, so it gets a master, the rendition is served, and AVKit fetches subs_0_1294.vtt and renders it in PiP.

That last one is a different surface from Sodalite#34, which was a wired external display on iOS and found master-served subtitles not reaching the screen. tvOS PiP behaves the other way here. It adds a row to that matrix rather than disputing one.

The fix

Clear the latch on foreground return.

A user who changes the output format has to leave the app to reach Settings, so UIApplication.didBecomeActiveNotification is the one event guaranteed to follow such a change. The engine already observes it (setupLifecycleObservers, under #if os(iOS) || os(tvOS)), so the hook exists and costs nothing to reach.

What the clear costs when it is wrong, which is the case of a genuinely SDR panel that will refuse again: one in-place media fallback, measured at 223 ms with no visible black frame, paid at the next HDR load after a foregrounding rather than once per process. The latch keeps doing its job inside a viewing session, which is where the repetition it was built to prevent would actually have happened.

What this deliberately does not do is guess. The alternative of keying the latch to a fingerprint of the display configuration would be more precise, but the latch can only be set while eligibleForHDRPlayback is true, so the capability table at refusal time may well be identical to the table afterwards, and a fingerprint built on that assumption would silently keep the stale latch. That would need measuring on the device before anything is built on it. Foreground return needs no such assumption.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions