Skip to content

fix(compositor): anchor Linux annotations on the un-zoomed screen rect - #398

Merged
EtienneLescot merged 2 commits into
mainfrom
claude/optimistic-yonath-b830e6
Aug 20, 2026
Merged

fix(compositor): anchor Linux annotations on the un-zoomed screen rect#398
EtienneLescot merged 2 commits into
mainfrom
claude/optimistic-yonath-b830e6

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

On Linux the compositor anchored annotations — and therefore captions, which are projected into text annotations — on s_dst, the screen box that the zoom moves and grows, instead of s_ann, the same box before the zoom. So a caption drifted and swelled under a zoom, ran past the frame edge at a high scale, and moved again under a 3D rotation preset (which is a property of a zoom region, so it brings the box with it). Windows and macOS render the same project with the caption nailed in place. Since the native compositor is the preview's sole pixel source, this was visible while editing, not only at export.

This is #179's regression, fixed once in 81839f0 for the two backends that existed then. That commit touched compositor_macos.rs and compositor_windows.rs; the Linux backend was ported from the pre-fix code and kept the old formula.

The fix is to stop letting each backend redo the arithmetic. It now lives once, in frame_geometry::annotation_dst_in, and all three backends call it: Linux through FrameGeometry::annotation_dst, Metal and D3D directly. annotation_anchor_h_px does the same for the font size, which is a fraction of that rect's height.

What the second commit changes, and why

The first commit fixed Linux by adding two accessors and calling them there. Reviewing it turned up that the two claims holding it up were both weaker than stated, so a31452b makes them true rather than leaving the comments asserting them.

The arithmetic was centralized for one backend of three. annotation_dst's doc said it existed « pour que le backend n'ait pas le choix », but compositor_macos.rs and compositor_windows.rs still open-coded the same four lines and the same font_size_rel * rect[3] * rh. Worse, both received the anchor as a parameter named screen_dst — named after the wrong field, one token away from re-shipping #179 on two more platforms. Both now call the shared function and both parameters are renamed s_ann.

It is a free function rather than a method because Windows destructures FrameGeometry on arrival at compose_frame and has no &self left to offer by the time it draws annotations; the method forwards to it for Linux, which does hold the geometry.

What is still not enforced, now stated in the doc instead of contradicted by it: the choice of rect at the Metal and D3D call sites. Their draw_annotations takes the anchor as a parameter, so passing s_dst remains possible on two backends of three. Only rendering pixels can catch that, which is the opt-in Linux test below. The parameter name and the tests are what stand in for a type-level guarantee.

The unit test was close to a tautology. annotation_dst/annotation_anchor_h_px are pure functions of s_ann, and the_annotation_anchor_ignores_the_zoom already proved s_ann ignores the zoom — so the equalities held by construction, and the rotation half could not fail at all (s_ann is s_base, which has no rotation term). It now also asserts that annotation_dst_in agrees with the method — putting the path Metal and D3D take under test on all three CI jobs, where it previously had none — and that the same function fed s_dst returns a different rect, without which a constant implementation would satisfy everything above it.

Four comments said things that were not true.

  • compose_linux.rs claimed nobody noticed the bug because « la preview (web) reste juste » and only the export diverged. The native compositor is the preview's sole pixel source — AnnotationOverlay.tsx paints the selection chrome, not the annotation pixels — so the drift was visible while editing. The claim also contradicted the comment this same branch added to compositor_linux.rs.
  • « son empreinte est le rect entier, pas la silhouette des glyphes » — the plate rect comes from glyphs.plate, hugged to the laid-out lines, not the 0.92x0.22 box. The measurement is still sound (the plate is a deterministic function of dst and font_size_px, both s_ann-anchored); the stated reason was not.
  • « frame 90 tombe au coeur de la region » was wrong twice: FPS is a hard-coded 60, so frame 90 is 1.5 s, and set_timeline_time(Some(3.0)) is what actually drives sampling — the 90.0 argument reaches nothing in that test. Corrected here and in compose_linux_ecran_tilte, where the error was copied from.
  • The 2 px tolerance note said the error « se compte en dizaines de pixels ». Under zoom 2.5 the band drops ~450 px and leaves the 540 px frame, so the assert that actually fires is bande absente — which now names s_dst instead of leaving the reader to guess why a band went missing.

One seek_to for the six renders instead of six identical ones, matching compose_linux_ecran_tilte: same AVFrame, so the decoder cannot contribute a difference that reads as a displacement.

Related issue

Fixes #397

Type of change

  • Bug fix

Release impact

  • Patch

Desktop impact

  • Linux
  • Windows and macOS — refactor only, no behaviour change (both already passed s_ann; the arithmetic they now call is identical to what they open-coded)

Testing

  • the_annotation_rect_and_font_ignore_zoom_and_rotation (frame_geometry.rs) — runs in CI on all three platforms. Plans the golden scene plain, zoomed, and zoomed+iso, then asserts annotation_dst, annotation_dst_in and annotation_anchor_h_px return the same rect and height in all three. Four guards keep it honest: s_dst must actually differ under zoom, the iso preset must produce a non-identity rotation, the free function must agree with the method, and feeding it s_dst must move the rect.
  • compose_linux_annotation_ancree_hors_zoom (tests/compose_linux.rs) — the on-device proof, and the only test that covers the call site: renders a caption band with an opaque plate three times (no zoom / zoom 2.5 / zoom 2.5 + iso), each diffed against its own no-annotation render, asserting the bounding box lands within 2 px of the un-zoomed one. Opt-in (OPENSCREEN_LINUX_COMPOSE=1 + crates/fixture/screen.mp4), so CI compiles it but does not run it.
  • annotation_scene grew an annotation_scene_with_zoom sibling; the four existing callers are unchanged and delegate through it.

Not verified locally, and I want to be explicit about it. Written on Windows: compositor_linux.rs and tests/compose_linux.rs are cfg-gated to Linux and cannot be compiled here, compositor_macos.rs likewise, and the crate's build.rs needs vendored ffmpeg headers this machine doesn't have — so cargo check does not run either. All four files were syntax-checked with rustfmt, which is not a substitute for the borrow checker. The three ci.yml Rust jobs (Linux test, Windows check, macOS test) are the verification; the frame_geometry test runs in all three and the Linux job compiles the new compose_linux case. If anything is red I'll fix it on this branch.

No TypeScript changed, so the JS suite and lint are untouched.

Summary by CodeRabbit

  • Bug Fixes

    • Improved annotation and subtitle positioning across Linux, macOS, and Windows so they remain correctly anchored during screen zoom.
    • Kept annotation text sizing consistent and independent of screen zoom.
    • Reduced annotation drift and incorrect placement caused by transformed screen bounds.
  • Tests

    • Added coverage for stable annotation placement and sizing across zoomed scenes and different rendering conditions.

Rapport utilisateur : « les captions sont sensibles au zoom et rotation 3d sous
linux, alors que ce n'est pas le cas pour les autres plateformes ». Les captions
ne sont pas un calque a part -- `captionCuesToTextRegions` les projette en
annotations texte -- donc c'est le placement des annotations qui derive, et
seulement sur ce backend.

`plan_frame` expose deux rects pour cette raison precise : `s_dst`, la boite
ecran, que le zoom deplace et agrandit depuis l'issue #179, et `s_ann`, la meme
boite AVANT le `remap_box` du zoom. Le contrat de `SceneAnnotation` veut le
second (« deliberately NOT affected by the zoom crop »). 81839f0 l'avait pose
et corrige les deux backends qui existaient alors ; il touche exactement
`compositor_macos.rs` et `compositor_windows.rs`. Le port Linux a repris la
formule d'avant le correctif et calculait ses rects, ainsi que sa taille de
police, sur `s_dst` : sous un zoom 2.5 la bande deborde du cadre, et un preset
de rotation 3D -- propriete d'une region de zoom -- la deplace lui aussi sans
pour autant la poser sur le plan incline.

Le natif etant desormais la seule source de pixels de l'apercu, la derive se
voyait des l'edition, pas seulement a l'export.

Plutot que de reparer la troisieme copie de la meme arithmetique, `FrameGeometry`
expose ce dont un backend a besoin -- `annotation_dst` et
`annotation_anchor_h_px` --, qui ne lisent que `s_ann` : le choix disparait du
backend, ce qui est ce qui a rate ici.

Deux tests. `the_annotation_rect_and_font_ignore_zoom_and_rotation`
(`frame_geometry.rs`) tourne en CI sur les trois plateformes et epingle le
contrat sur les accesseurs, avec ses deux garde-fous (le zoom doit bouger
`s_dst`, le preset iso doit produire une rotation) sans lesquels les egalites
seraient vraies pour la mauvaise raison. L'existant
`the_annotation_anchor_ignores_the_zoom` prouvait que `plan_frame` CALCULE la
bonne ancre, jamais qu'un backend s'en sert -- c'est exactement l'interstice ou
ce bug a vecu. `compose_linux_annotation_ancree_hors_zoom`
(`tests/compose_linux.rs`) le mesure sur GPU : une bande a plaque opaque rendue
sans zoom, avec zoom, puis avec zoom + iso, chacune diffee contre son propre
rendu sans annotation, boite englobante a 2 px pres. Opt-in comme tout ce
fichier.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7219f516-e3b6-44df-a4aa-d5982a9dd94e

📥 Commits

Reviewing files that changed from the base of the PR and between 6b15bd9 and a31452b.

📒 Files selected for processing (4)
  • crates/compositor/src/compositor_macos.rs
  • crates/compositor/src/compositor_windows.rs
  • crates/compositor/src/frame_geometry.rs
  • crates/compositor/tests/compose_linux.rs

Included review availability: Your plan provides up to 4 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Annotation placement and text sizing now use zoom-independent geometry on all native compositor platforms. Shared geometry tests and opt-in Linux composition tests cover zoomed and rotated scenes.

Changes

Annotation anchoring

Layer / File(s) Summary
Shared annotation geometry
crates/compositor/src/frame_geometry.rs
annotation_dst_in centralizes annotation rectangle calculations. FrameGeometry::annotation_dst delegates to it. Tests cover zoom and rotation cases.
Cross-platform compositor rendering
crates/compositor/src/compositor_linux.rs, crates/compositor/src/compositor_macos.rs, crates/compositor/src/compositor_windows.rs
All native compositor paths use unzoomed annotation anchors for placement and text sizing.
Linux annotation rendering tests
crates/compositor/tests/compose_linux.rs
Opt-in tests compare caption bounds across plain, zoomed, and rotated renders using a fixed timeline position.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to a3145

The change is localized, and no actionable merge-blocking risk remains after the normal required test checks are completed.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies issue #397 by anchoring Linux annotations and font sizing to s_ann and adding geometry and Linux rendering coverage.
Out of Scope Changes check ✅ Passed The Windows and macOS changes centralize existing arithmetic, while tests and comment fixes directly support the stated compositor objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly identifies the primary change: fixing Linux annotation anchoring to the unzoomed screen rectangle.
Description check ✅ Passed The description covers the change, issue, bug-fix and patch classifications, platform impact, and detailed testing information.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/optimistic-yonath-b830e6

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ends

Review of #398 found the fix correct but its two supporting claims not.

`annotation_dst`'s doc said it existed « pour que le backend n'ait pas le
choix », yet only Linux was converted: Metal and D3D still open-coded the
same four lines and the same `font_size_rel * rect[3] * rh`, and both took
the anchor as a parameter named `screen_dst` — pointing at the wrong field,
one token away from re-shipping #179 on two more platforms.

The arithmetic now lives once, in `annotation_dst_in`. It is a free function
and not a method because Windows destructures `FrameGeometry` on arrival and
has no `&self` left to offer; the method forwards to it for Linux, which does
hold the geometry. Both `draw_annotations` parameters are renamed `s_ann`, so
the name states the contract instead of contradicting it.

What this does NOT do, and the doc now says so: remove the rect CHOICE from
the Metal and D3D call sites. Only rendering pixels can catch that, which is
the opt-in Linux test.

The unit test grew the two assertions that make it more than a tautology
over `s_ann`: that `annotation_dst_in` agrees with the method (the path
Metal and D3D take is now covered on all three CI jobs), and that the same
function fed `s_dst` returns a DIFFERENT rect — without which a constant
implementation would satisfy everything above it.

Comment corrections, all of them things the review caught as false:

- `compose_linux.rs` claimed the drift was invisible in the preview and hit
  only the export. The native compositor is the preview's sole pixel source
  (`AnnotationOverlay.tsx` paints selection chrome, not pixels), so it was
  visible while editing — and the claim contradicted the comment this same
  branch added to `compositor_linux.rs`.
- « son empreinte est le rect entier » was wrong: the plate comes from
  `glyphs.plate`, hugged to the laid-out lines. The measurement is still
  sound, the stated reason was not.
- « frame 90 tombe au coeur de la region » was wrong twice: `FPS` is a hard
  60, so frame 90 is 1.5 s, and `set_timeline_time(3.0)` is what actually
  drives sampling. Fixed here and in `compose_linux_ecran_tilte`, which is
  where the error was copied from.
- The 2 px tolerance note undersold the error by an order of magnitude: the
  band drops ~450 px and leaves the frame, so « bande absente » is what
  fires. That assert now names `s_dst` instead of leaving the reader to
  guess why a band went missing.

One `seek_to` for the six renders instead of six identical ones, as
`compose_linux_ecran_tilte` already does — same AVFrame, so the decoder
cannot contribute a difference that reads as a displacement.

Not compiled locally: the crate's build.rs needs vendored ffmpeg headers
absent from this machine. The three CI Rust jobs are the verification.

Co-Authored-By: Claude <noreply@anthropic.com>
@EtienneLescot
EtienneLescot merged commit 621333d into main Aug 20, 2026
27 of 29 checks passed
@EtienneLescot
EtienneLescot deleted the claude/optimistic-yonath-b830e6 branch August 20, 2026 13:11
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.

[Bug]: on Linux, captions follow the zoom and the 3D rotation — Windows and macOS keep them in place

1 participant