Skip to content

[energy] GPU: opaque compositing, batch runs, prerender pills - #398

Merged
gregoreesmaa merged 16 commits into
mainfrom
energy/389-gpu-compositing
Oct 4, 2026
Merged

gregoreesmaa merged 16 commits into
mainfrom
energy/389-gpu-compositing

Conversation

@gregoreesmaa

Copy link
Copy Markdown
Owner

Fixes #389.

What changed (all in worktree, branch energy/389-gpu-compositing):

  • Opaque compositing: ReadView -isOpaque YES, window.opaque=YES, view wantsLayer=NO; single opaque theme bg fill per pass (no whole-window composite, no double bg fill).
  • Fill-color batching: consecutive same-color glyph runs share one primed SetFillColor + Interpolation (draw_run_batched in macos.m); legacy fallbacks and pass ends close via new idempotent platform_batch_end FFI (bridge.zig/platform.h, closed in onDraw + drawRect + headless paths).
  • 1x atlas path: white coverage masks tint via ClipToMask+FillRect on every scale (no per-run CTLineCreate/CGColorSpace/Color on 1x); NSFont cache per (style, quarter-point size) kills 2-7 fontWithName: probes per miss.
  • Prerendered pills: inline-code rounded rects rasterize once per unique (geometry+colors) into an 8-slot EOF cache (SIZE NOTE honored); copy button damage-culls before NSBezierPath/NSString work and caches its 11pt label metrics.
  • Overdraw: selection wash damage-gated + per-record culled; link hover invalidates old+new run boxes (was full redraw); cursor setters skipped unless shape flips; help overlay + remote indicator per-command damage-gated with record-only fallback; math blit uses None when snapped 1:1, Default only while downsampling (Math glyph sharpness on Retina: origin snap done, atlas option open #355 crisp budget still pins both arms).
  • Tests: ColorGroup/batchCount model + 3 tests in glyph_cache.zig.

Evidence:

  • glyph_cache.zig: 11/11 pass on this host (incl. 3 new batching tests).
  • strict_benchmarks.zig: untouched (never loosened per AGENTS.md section 2).
  • PLATFORM-BLOCKED on this Windows host: zig build test (read-module binary needs POSIX std.c/mmap, fails pre-existing, unrelated to this diff), macOS ship build, screenshot_suite.sh, size_gate.sh twin-delta, damage_parity.sh. Must run on macOS CI/host before merge; screenshots must be regenerated + staged there and confirmed byte-identical.

gregoreesmaa added a commit that referenced this pull request Oct 3, 2026
…ples never matched fresh renders (PR #398 residue)

Root cause: #389 routed 1x headless glyphs through the 2x white-mask blit (downsample 2:1). Incremental drag phases carry a damage clip (CGContextClipToRect over old+new selection union); CoreGraphics resamples masked blits under a clip up to 1 LSB differently than unclipped, so every incremental repaint differed from the fresh render - extend phase and drag-back alike, across all gestures. The Zig/ObjC damage-union logic was already correct (old+new bounds everywhere); only the glyph pixels diverged.

Fix: blit_tinted_mask_batched declines below 1.5x scale (clip-parity gate) and platform_draw_text passes rasterize=0 on 1x, restoring the pre-#389 legacy direct path pixel-exactly on 1x. 2x keeps the full GPU win (textured quads + batching + rasterize). Shaping cache still serves measure/hit-test on all scales. Batching (primed fill groups), opaque compositing, pill prerender, font cache untouched.

Tests: new blitScaleParityOk unit test pins the 1.5x gate (glyph_cache.zig, 12/12 pass on Windows host). Fuzz oracles untouched. Static screenshots byte-identical by construction (1x path now executes main's legacy pixels verbatim).
gregoreesmaa added a commit that referenced this pull request Oct 3, 2026
… (PR #398 gestures 5/12/13)

Root cause: paint_selection_highlight filled char-range wash rects at raw
fractional coords (x1/x2 from CoreText advances, view_y = doc_y - scroll).
Fresh renders paint those rects unclipped; incremental drag phases paint
them under a CGContextClipToRect damage rect. CoreGraphics blends a
translucent (0.32 alpha) fractional-edge fill differently clipped vs
unclipped - up to 1 LSB on the AA edge pixels - so extend-phase fringe
and drag-back residue survived round 1 on exactly the fractional-coord
gestures (5: A=805.3,891.2... / 12: A=311.0,557.7... collapse-to-empty
B=0,0,0,0 / 13: A=349.7,621.2...). Gesture 12's collapse takes the same
path: its extend phase (A) already differs, B only needs the old-box
repaint.

Fix: floor/ceil the two wash fills outward once at paint time (char-range
fill + inter-word gap bridge), the same rule selection_bounds_expanded
already applies to damage boxes - every consumer agrees, clipped and
fresh land on identical device pixels.

Tests: no Zig touched (macos.m only); oracles untouched; static
screenshots take the same path (1x legacy text, wash fills now land on
the same integral edges the damage boxes already used).
gregoreesmaa added a commit that referenced this pull request Oct 4, 2026
gregoreesmaa added a commit that referenced this pull request Oct 4, 2026
…stures rendered at scroll 0 (PR #398 gestures 5/12/13)
gregoreesmaa added a commit that referenced this pull request Oct 4, 2026
- Opaque compositing: ReadView -isOpaque YES, window.opaque=YES,
  view wantsLayer=NO; single opaque bg fill (no whole-window composite).
- Fill-color batching: consecutive same-color glyph runs share one
  primed SetFillColor/Interpolation (draw_run_batched); legacy fallbacks
  and pass ends close via platform_batch_end (new FFI, idempotent).
- 1x atlas path: white coverage masks tint via ClipToMask+FillRect on
  every scale (no per-run CTLineCreate/CGColorSpace on 1x); NSFont cache
  per (style, quarter-point size) kills 2-7 fontWithName: probes per miss.
- Prerendered pills: inline-code rounded rects rasterize once per unique
  (geometry+colors) into an 8-slot cache at EOF (SIZE NOTE honored);
  copy button damage-culls before NSBezierPath/NSString work and caches
  its 11pt label metrics.
- Overdraw: selection wash damage-gated + per-record culled; link hover
  invalidates old+new run boxes (was full redraw); help overlay and
  remote indicator per-command damage-gated with record-only fallback;
  math blit uses None when snapped 1:1, Default only when downsampling.
- Tests: ColorGroup/batchCount model + 3 tests in glyph_cache.zig
  (11/11 pass); strict benchmarks untouched.

Platform-blocked on this Windows host: macOS build, screenshots,
size_gate.sh, damage_parity.sh must run on macOS CI/host.
…ples never matched fresh renders (PR #398 residue)

Root cause: #389 routed 1x headless glyphs through the 2x white-mask blit (downsample 2:1). Incremental drag phases carry a damage clip (CGContextClipToRect over old+new selection union); CoreGraphics resamples masked blits under a clip up to 1 LSB differently than unclipped, so every incremental repaint differed from the fresh render - extend phase and drag-back alike, across all gestures. The Zig/ObjC damage-union logic was already correct (old+new bounds everywhere); only the glyph pixels diverged.

Fix: blit_tinted_mask_batched declines below 1.5x scale (clip-parity gate) and platform_draw_text passes rasterize=0 on 1x, restoring the pre-#389 legacy direct path pixel-exactly on 1x. 2x keeps the full GPU win (textured quads + batching + rasterize). Shaping cache still serves measure/hit-test on all scales. Batching (primed fill groups), opaque compositing, pill prerender, font cache untouched.

Tests: new blitScaleParityOk unit test pins the 1.5x gate (glyph_cache.zig, 12/12 pass on Windows host). Fuzz oracles untouched. Static screenshots byte-identical by construction (1x path now executes main's legacy pixels verbatim).
… (PR #398 gestures 5/12/13)

Root cause: paint_selection_highlight filled char-range wash rects at raw
fractional coords (x1/x2 from CoreText advances, view_y = doc_y - scroll).
Fresh renders paint those rects unclipped; incremental drag phases paint
them under a CGContextClipToRect damage rect. CoreGraphics blends a
translucent (0.32 alpha) fractional-edge fill differently clipped vs
unclipped - up to 1 LSB on the AA edge pixels - so extend-phase fringe
and drag-back residue survived round 1 on exactly the fractional-coord
gestures (5: A=805.3,891.2... / 12: A=311.0,557.7... collapse-to-empty
B=0,0,0,0 / 13: A=349.7,621.2...). Gesture 12's collapse takes the same
path: its extend phase (A) already differs, B only needs the old-box
repaint.

Fix: floor/ceil the two wash fills outward once at paint time (char-range
fill + inter-word gap bridge), the same rule selection_bounds_expanded
already applies to damage boxes - every consumer agrees, clipped and
fresh land on identical device pixels.

Tests: no Zig touched (macos.m only); oracles untouched; static
screenshots take the same path (1x legacy text, wash fills now land on
the same integral edges the damage boxes already used).
…stures rendered at scroll 0 (PR #398 gestures 5/12/13)
Main landed its own font cache (#388) plus the #391 diet resolver since the branch base; the branch EOF copy is now dead (duplicate symbol) so it is dropped in favor of main's. Conflict was confined to get_font_for_style.
@gregoreesmaa
gregoreesmaa force-pushed the energy/389-gpu-compositing branch from ddb815c to 3aa06d4 Compare October 4, 2026 19:00
gregoreesmaa added a commit that referenced this pull request Oct 4, 2026
damage_parity cmp says WHICH gesture mismatches, not WHERE. fringe_localize.py (stdlib PNG decode, same pattern as copy_button_damage.sh) prints diff bbox + count + max delta + sample pixels per failing phase so the fringe maps onto --dump-commands text_run rects. Diagnostic only: || true at call sites, strict cmp oracles unchanged. Remove before merge.
gregoreesmaa added a commit that referenced this pull request Oct 4, 2026
damage_parity cmp says WHICH gesture mismatches, not WHERE. fringe_localize.py (stdlib PNG decode, same pattern as copy_button_damage.sh) prints diff bbox + count + max delta + sample pixels per failing phase so the fringe maps onto --dump-commands text_run rects. Diagnostic only: || true at call sites, strict cmp oracles unchanged. Remove before merge.
@gregoreesmaa
gregoreesmaa force-pushed the energy/389-gpu-compositing branch from 3aa06d4 to d2d3f43 Compare October 4, 2026 19:05
damage_parity cmp says WHICH gesture mismatches, not WHERE. fringe_localize.py (stdlib PNG decode, same pattern as copy_button_damage.sh) prints diff bbox + count + max delta + sample pixels per failing phase so the fringe maps onto --dump-commands text_run rects. Diagnostic only: || true at call sites, strict cmp oracles unchanged. Remove before merge.

Also restores font_resolve_uncached dropped by the rebase conflict resolution (linker break caught by CI run 37226572120); the branch EOF duplicate stays deleted.
@gregoreesmaa
gregoreesmaa force-pushed the energy/389-gpu-compositing branch from d2d3f43 to 6b9b956 Compare October 4, 2026 19:06
DRAGDMG stderr lines expose per-phase damage; the sweep replays phase-1 damage on a fresh bitmap via --damage and reports which arm fresh+damage matches (cross-phase state vs damage-path splitter). Diagnostic only: strict cmp oracles unchanged. Remove before merge.
Whole-image fresh+damage cmp proved nothing (outside-damage zeros vs phase-0 content differ by construction). Mask all bisect comparisons to within the phase-1 damage rect; add bisA-vs-fresh path-consistency check; print DRAGDMG lines. Strict oracles unchanged. Remove before merge.
DRAGDMG field scan started at phase= token; start at the x= token. Strict oracles unchanged. Remove before merge.
Dump command stream at failing gesture scrolls (full + under phase damage): lists inline_code_bg rects and damage-culled text runs at the fringe site. Read-only; strict oracles unchanged. Remove before merge.
…/13/18)

Root cause: the pill cache holds 2x rasters; on 1x dests the cached blit downsamples (InterpolationQuality None) under the damage clip and never matches a fresh direct render pixel-exactly. Incremental drag phases blit (warm cache) while fresh renders draw direct (cold cache): pill fringe rows differ (ROWPROBE: inline_code_bg exactly at all three fringe bboxes; text runs unculled; bisA-vs-fresh identical proving the damage path consistent). Same clip-parity rule as the 1x text revert (2740b05): gate cache use+fill on g_output_scale >= 1.5. 2x live path unchanged; 1x is now exactly the pre-#389 direct path, so static screenshots stay byte-identical.
The fringe localizer, damage-replay bisect, row-content probe, and DRAGDMG harness prints served their purpose: they identified the pill-cache 1x blit as the residue source and the 1x-direct fix turned the sweep green. Remove all of it; the strict cmp oracles stay as the permanent regression pin.
@gregoreesmaa
gregoreesmaa merged commit d02133b into main Oct 4, 2026
3 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.

[energy] GPU: opaque compositing, batch runs, atlas for 1x, prerender pills, wash-before-glyph

1 participant