Skip to content

Publish the display size the software path settled on - #355

Merged
superuser404notfound merged 3 commits into
mainfrom
feat/software-display-size
Aug 9, 2026
Merged

Publish the display size the software path settled on#355
superuser404notfound merged 3 commits into
mainfrom
feat/software-display-size

Conversation

@superuser404notfound

Copy link
Copy Markdown
Owner

Raised by @edde746 while adopting #311: with the clock and the frame boundaries in place, an overlay still had no rectangle to draw into.

What it adds

AetherEngine.softwareDisplaySize (CGSize?, @Published): the size the software path's picture presents at, the coded frame under the pixel aspect ratio the decoder attached.

The only public size was sourceVideoWidth / sourceVideoHeight, which are the CODED dimensions, so anamorphic content was laid out against the wrong rectangle (720x576 at 64:45 presents as 1024x576), and AVSampleBufferDisplayLayer carries no videoRect to measure instead. A host cannot compute it either: the ratio is resolved per frame across three sources (#177) and one whose display aspect is impossible is dropped in favour of square pixels (#290), so a reconstruction from container metadata disagrees with the screen in exactly the cases that policy exists for.

It is read off the format description the renderer enqueues, via CMVideoFormatDescriptionGetPresentationDimensions, rather than recomputed from the SAR: that description is the object the layer is handed, and the cache key it lives behind already invalidates on a dimension or PAR change, which is exactly when the value must be republished.

Optional rather than .zero (the argument that shaped SoftwareVideoFrameTime in #311), mirrored rather than latched (a live source can re-shape the picture mid-stream), cleared with the session.

Two defects found while measuring it

The software load path cancelled the sinks it had just wired. softwareCancellables.removeAll() stood between two groups of .store(in:) calls, so everything attached above it died on arrival. What was above it is the SW-PiP cue mirror: subtitles in a software-path PiP window froze at whatever was on screen when PiP started. No unit test can see this, both halves of the wiring work in isolation; it took a session that runs long enough to notice nothing arrives.

Anamorphic HEVC rendered at coded dimensions (#354). The VT-backed decoder attached no pixel aspect ratio at all, and the renderer builds its format description from the delivered pixel buffer, so nothing carried the ratio to the layer. The libavcodec decoder on the same host has attached it since #177, so the gap was one decoder wide. Same resolution order, same two gates, applied on the decoder that was missing them.

Measured

aetherctl play --sw --frame-times, which now reports disp=WxH per tick and the settled size next to the coded one. One synthetic clip, encoded four ways:

source decoder before after
720x576 SAR 64:45 VT (hevc) 720x576 1024x576
720x576 SAR 64:45 libavcodec (h264) 1024x576 1024x576
1280x720 square VT (hevc) 1280x720 1280x720
1280x720 square libavcodec (h264) 1280x720 1280x720

Full suite green: 1677 tests, 245 suites.

Re #353
Re #354

superuser404notfound and others added 3 commits August 9, 2026 17:45
…one's

softwareCancellables.removeAll() stood in the middle of loadSoftware's
wiring, between two groups of .store(in:) calls, so every sink attached
above it was cancelled the moment it ran. What was above it is the
SW-PiP cue mirror: after the frame compositor is armed, the CombineLatest
over subtitleCues + secondarySubtitleCues never delivered again, and the
only cues the compositor ever saw were the snapshot pushed by the
pictureInPictureActive didSet. Subtitles in a software-path PiP window
therefore froze at whatever was on screen when PiP started.

Hoisted above the wiring, where a reset of the outgoing session belongs.
The teardown in stopInternal already covers the ordinary path; this one
covers a load that reaches here without one.

Nothing catches this in unit tests, because both halves work in
isolation: the publisher publishes, the sink is correct, and the cancel
is invisible from either end. It surfaced while measuring a new sink on
the same path with aetherctl play, which is where a session runs long
enough to notice that nothing arrives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE
A host laying something out over the picture (a libass overlay, the case
#311 gave a clock and frame boundaries to) needs the rectangle the frames
land in. The only public size was sourceVideoWidth / sourceVideoHeight,
which are the CODED dimensions, so anamorphic content was laid out
against the wrong rectangle: 720x576 at 64:45 presents as 1024x576, and
an overlay sized 5:4 sits inside a 16:9 picture. There is nothing to
measure on the layer either, AVSampleBufferDisplayLayer has no videoRect.

A host cannot compute it. The ratio is resolved per frame across three
sources, first sane wins (#177), and one whose display aspect is
impossible is dropped in favour of square pixels (#290), so a host
reconstructing it from container metadata disagrees with the screen in
exactly the cases that policy exists for.

softwareDisplaySize is therefore read off the format description the
renderer enqueues, via CMVideoFormatDescriptionGetPresentationDimensions,
rather than recomputed from the SAR: that description is the object the
layer is handed, and the cache key it lives behind already invalidates on
a dimension or PAR change, which is exactly when the size has to be
republished.

Optional rather than .zero, the argument that shaped SoftwareVideoFrameTime
in #311: there is no display size before the first frame is built, and a
placeholder in a non-optional field is indistinguishable from a
measurement at the call site. Mirrored rather than latched, unlike
hasFirstFrameReadyForDisplay: a live source that switches resolution
re-shapes the rectangle under a host that already laid out against it.
Cleared with the session, so the next source is not laid out against this
one's picture.

Measured with aetherctl play --sw --frame-times, which now reports
disp=WxH per tick and the settled size next to the coded one:

  720x576 SAR 64:45  ->  disp=1024x576 (coded 720x576)
  1280x720 square    ->  disp=1280x720 (coded 1280x720)

Re #353

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE
Anamorphic HEVC on the software host reached the layer at its coded
dimensions: 720x576 declaring 64:45 presented as 720x576, a 16:9 picture
squashed into 5:4. HardwareVideoDecoder attached no pixel aspect ratio,
and the renderer builds its format description from the delivered
CVPixelBuffer, so a ratio that is not an attachment on that buffer never
reaches the layer at all.

The libavcodec decoder on the same host has attached it since #177. The
two are chosen per codec inside one host, so the defect was one decoder
wide and invisible from every source that takes the other one.

Resolved once at open() rather than per frame, because VT delivers pixel
buffers and not AVFrames: the per-frame source SoftwareVideoDecoder
prefers does not exist here, which also makes the #177 latch unnecessary
since one resolution cannot oscillate. Both declared sources are read,
since only one of them is the container's, and both existing gates run
unchanged: the #177 component bound and the #290 display aspect. Square
pixels attach nothing, and a stale attachment on a recycled pool buffer
is removed, mirroring the libavcodec path.

Measured with aetherctl play --sw --frame-times, one synthetic clip
encoded four ways, reading the settled size published by #353:

  720x576 SAR 64:45  hevc (VT)          720x576 -> 1024x576   fixed
  720x576 SAR 64:45  h264 (libavcodec)  1024x576              unchanged
  1280x720 square    hevc (VT)          1280x720              unchanged
  1280x720 square    h264 (libavcodec)  1280x720              unchanged

Reached in production by the interlaced detour (field order or an SPS
that indicates interlacing) and by forward-only sources. Broadcast SD is
where interlacing and a non-square SAR occur together.

Re #354

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HX5zV3Fcf7Nzq4DYGdXvQE
@superuser404notfound
superuser404notfound merged commit afb36ff into main Aug 9, 2026
3 checks passed
@superuser404notfound
superuser404notfound deleted the feat/software-display-size branch August 9, 2026 15:57
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