pmacs/docs/pmacs-gpu-quad-backgrounds-...

16 KiB
Raw Permalink Blame History

pmacs-gpu — quad-background framing

Status: CLOSED 2026-05-29. Sessions 9.1 (Selection), 9.2 (CurrentLine), 9.3 (peer-presence sourcing) all merged; finding A8 retired. Scoring + the QB1QB3 follow-on findings are in pmacs-gpu-quad-backgrounds-audit.md. Search backgrounds (Q#4) remain deferred to a later arc. The sections below are the original framing pass, preserved.

This is the per-milestone framing artifact for the quad-pipeline work that closes Phase A's one deferred structural finding. It inherits the framing discipline of pmacs-gpu-design.md and records load-bearing commitments before any session-9 code lands. Audit material at Phase-A-finalization close goes to docs/pmacs-gpu-quad-backgrounds-audit.md (future).

Why this exists

Phase A's audit (pmacs-gpu-phase-a-audit.md) scored A8 as a single structural finding deferred from Phase A by design: foreground-only glyph attributes (cosmic-text 0.18's Attrs) cannot render the background visual that Selection, SearchMatch, SearchMatchActive, and CurrentLine need. The wire and consumer-cache infrastructure for these decoration kinds already exists; the render pass does not.

The framing claim, load-bearing for the rest of this doc: the expensive part of this milestone is gone before it starts. Session 7 shipped a wgpu quad pipeline for the minimap; this milestone reuses that pipeline for a second purpose. There is no new shader to author, no new vertex-buffer abstraction to design, no new blend-mode story. The work decomposes into vertex generation + render-order discipline + one missing producer call (CurrentLine). That smallness is what qualifies this as Phase A finalization rather than a Phase B prerequisite.

Contract inheritance

From pmacs-gpu-design.md: the instance never learns a pixel. This milestone strengthens that invariant rather than testing it. The producer emits Decoration { range: ByteRange, kind: DecorationKind }; the consumer translates byte ranges into pixel rectangles entirely from local glyph layout. The pixel-pure-instance invariant is not at risk in any session-9 work; if it appears to be, classify as a structural finding and pause.

Scope inheritance from Phase A

What already flows on the wire and in pmacs-gpu state:

Field Status at Phase A close
DecorationKind::Selection producer emission Shipped (src/semantic_render.rs:383, from win.region()).
DecorationKind::DiagnosticError/Warning/Info/Hint producer emission Shipped; renders as foreground override (not in scope here).
current_decorations: Vec<Decoration> consumer state Shipped; populated by the M11.4 dirty-merge path.
wgpu quad pipeline Shipped; minimap rendering uses it.
Glyph layout access (Buffer::layout_runs()) Available; cosmic-text 0.18 stable API.
decoration_kind_to_color foreground helper Shipped; returns None for background kinds (correct per its contract).

What is NOT closed by Phase A:

Gap Resolution path
DecorationKind::Selection rendering Session 9.1. Data flows; render pass is missing.
DecorationKind::CurrentLine producer emission Session 9.2. Derivable from vp.cursor; trivial producer change.
DecorationKind::CurrentLine rendering Session 9.2. Same render path as 9.1.
DecorationKind::SearchMatch{,Active} producer emission Deferred. Requires a search feature in pmacs core (Lua command + core state + producer wiring). Belongs to a later arc; not this milestone.
DecorationKind::SearchMatch{,Active} rendering Deferred with the producer.

Toolkit (recurrence)

Component Status
wgpu quad pipeline Reuse Session 7's QuadRenderer (pmacs-gpu/src/main.rs:375-428). Same shader, same vertex layout, same alpha-blending state.
cosmic-text glyph layout Buffer::layout_runs() yields per-visual-line layout; each LayoutRun exposes glyphs: &[LayoutGlyph] with start_byte, end_byte, x, w.
pmacs-protocol No wire changes. DecorationKind already enumerates all four background kinds. Decoration already carries range: ByteRange.

No new dependencies. No new wire-format decisions.

Predicted findings — categorical bets

Three named bets, each probing a categorically different failure surface. Committed before session-9 code lands so the post-milestone scoring is honest:

# Bet Category
1 Multi-line vertex generation produces wrong number of quads on selections that cross visual-line boundaries (soft-wrapped lines, lines wider than viewport). Geometric-decomposition probe
2 Overlap composition between Selection and a future CurrentLine (or between Selection and a diagnostic-foreground decoration) needs an explicit precedence rule the producer arc didn't commit to. Convention-vs-contract probe
3 CurrentLine emitted from the active window's cursor re-emits a full Decorations family every cursor-byte tick; the consumer churns its quad buffer on horizontal cursor motion that doesn't change the rendered line. Producer-cadence-vs-consumer-cost probe

Unpredicted categories may surface. The audit doc records them at classification-time per rule (iii).

Scoring methodology (committed before data lands)

Same shape as Phase A: category matrix, not a count. At session-9 arc close, report:

  • Predicted categories that surfaced (true positives).
  • Predicted categories that didn't surface (false positives).
  • Unpredicted categories that surfaced (false negatives).
  • Count distribution within each category.

Recorded before the work to prevent the M10.10 Day-5 reconciliation trap.

Forced decisions

These are decisions session 9.x will need to make. The framing pass commits each so sessions don't rediscover them mid-implementation. Each follows pmacs-gpu-design.md's Q-numbering convention.

Q#1 — CurrentLine source location: stance (α)

Stance (α): the producer emits DecorationKind::CurrentLine derived from the active window's cursor. Concretely, scoped_decorations reads core.active_window_for(self.frontend_id).cursor — the same per-frontend access path it already uses for Selection at src/semantic_render.rs:378 — and converts that byte position into a line range via line_starts (the line-offset table already built in the diagnostic branch of the same function). The consumer treats CurrentLine like any other decoration; no consumer-side cursor → line-range derivation.

Reasoning: centralizing the line-derivation in the producer means the TUI's future CurrentLine highlight (if it ships) reuses the same derivation. The TUI today does not paint a current-line background; when it does, deriving it consumer-side in two places would be a duplication M10's discipline rejects. Producer-side derivation also gives the producer a place to throttle (Q#3 below).

Stance (α) commits the producer to emit CurrentLine even when no consumer renders it. The wire cost is one extra Decoration per Decorations frame, ~16 bytes. Negligible.

Q#2 — render order: stance (α)

Stance (α): single render pass, two pass.draw() calls. Quad backgrounds first, text second. The minimap continues to render last (at the right margin, after text, in the same pass).

Reasoning: a single render pass per frame is the standard wgpu shape and matches the existing structure. Adding a second pre-text pass introduces a second begin_render_pass per frame with no correctness benefit — the same pipeline can be issued twice in one pass with different vertex-buffer ranges or different set_vertex_buffer calls. The minimap's existing draw-after-text behavior is preserved.

If session 9.1 surfaces a transparency-correctness issue (decoration backgrounds blending against the clear color instead of against text), that's a structural finding — classify and consider stance (β) (separate pre-text pass with LoadOp::Load for the text pass).

Q#3 — CurrentLine producer cadence: stance (β)

Stance (β): the producer emits CurrentLine once per visible-line change, not once per cursor tick. Horizontal cursor motion within the same source line is a no-op; only motion that crosses a \n triggers a fresh Decorations family.

Implementation: the producer's per-frontend SemanticRenderState tracks last_cursor_line: Option<u64>; on each render_frame it reads the active window's cursor, computes the line via the line_starts table, and compares against the cached value. Same line → suppress the CurrentLine portion of the Decorations emission (but Selection/diagnostic portions still emit normally). Different line → emit a full Decorations frame.

Reasoning: cursor moves 60120 Hz under arrow-key autorepeat; rebuilding the consumer's quad-vertex buffer that often is wasted work the bet #3 predicts. Throttling at the producer is cheaper than at every consumer. Same-line motion still updates CursorByte; only the CurrentLine decoration is suppressed.

If session 9.2 surfaces a freshness gap (e.g. Decorations family arrives with stale Selection after a CurrentLine emission), that's a structural finding — likely indicates the per-line throttle needs to emit a no-op Decorations { full: true, segments: [] } to clear other decoration kinds.

Q#4 — search backgrounds: deferred

Search (Find / FindReplace) is a load-bearing pmacs feature with no current implementation in pmacs core. The producer cannot emit SearchMatch until search state exists; the consumer cannot test SearchMatch rendering without the producer emitting it.

Defer to the editing-parity arc (pmacs-gpu-design.md's Phase B). Phase A finalization closes with SearchMatch{,Active} rendering documented-but-not-implemented; the rule-(iii) classification is "small finding deferred awaiting upstream feature."

Q#5 — read-only mirror cursor source: stance (peer-presence)

Surfaced during 9.2 manual validation (structural finding QB1). Sessions 9.1 and 9.2 emit Selection / CurrentLine from the viewing frontend's own window (scoped_decorations reads active_window_for(self.frontend_id)). pmacs-gpu is a read-only viewer with no input path: it never sends Key/cursor events, so its own window's cursor stays pinned at 0 and its selection stays None. The two per-window decoration kinds are therefore inert in pmacs-gpu — CurrentLine paints a static line-0 wash and Selection never appears. Every other rendered family (StyleSpans, diagnostics, inlay hints, minimap) is keyed to the buffer (shared), which is why only these two are affected.

Stance: peer presence is the authoritative cursor/selection source for a read-only mirror. What the user watches in pmacs-gpu is the editing frontend's (their TUI's) cursor and selection. That is PresenceUpdate — already on the wire (InstanceMessage::PresenceUpdate { frontend_id, buffer_id, cursor, selection }), already broadcast by the daemon to every multi_frontend recipient, and pmacs-gpu already negotiates multi_frontend: true. It simply drops the message at its _ => None catch-all today.

The fix is consumer-only — no producer or protocol change:

  • pmacs-gpu consumes PresenceUpdate, storing per-peer (buffer_id, cursor, selection).
  • The quad-background path renders Selection / CurrentLine washes from peer presence (the editing peer's cursor line + selection) rather than from the inert own-window current_decorations of those two kinds. Diagnostic (foreground) decorations are buffer-keyed and unaffected.
  • The producer keeps emitting own-window Selection / CurrentLine (9.1/9.2) unchanged — correct and forward-looking for when pmacs-gpu gains its own input in Phase B; simply unconsumed-for-backgrounds by the mirror today.

Deliberately deferred within this stance:

  • Per-peer stable colors (the audit's PresenceUpdate color-stability item). The single-peer mirror reuses the Selection / CurrentLine colors so the visual reads as "my editing, mirrored." Multiple distinct peers each getting a stable color is a later refinement.
  • Peer caret glyph + label ("user N editing here"). This session renders the line/selection backgrounds only; the caret bar and name label are future presence work.
  • Own-cursor vs peer-cursor merge. Once pmacs-gpu has input, its own CurrentLine (now meaningful) and peer presences coexist with distinct colors. Out of scope until input lands.

This is session 9.3.

Finding feedback loop

Rule (iii) from pmacs-gpu-design.md carries forward unchanged:

  • Small finding (≤ half-day patch, no structural change, no contract violation): absorb into the current session. Patch, verify, continue.
  • Structural finding (changes a contract, ripples across producers or consumers, invalidates a v1.0 assumption, or breaks the pixel-pure-instance invariant): pause; classify; defer to its own scoped milestone or session.

Classification happens at surface-time. The session 9.x audit doc records classification and resolution.

Rhythm

The session-anchored cadence from pmacs-gpu-design.md applies. Two sessions are framed for this arc:

  • Session 9.1 — Selection quad backgrounds. Vertex generation from current_decorations filtered to background kinds; render-order change per Q#2; decoration_kind_to_bg_color helper; visual probe (TUI selection → pmacs-gpu rectangle). Exercises bet #1 directly; exercises bet #2 only insofar as Selection may overlap diagnostic foregrounds.
  • Session 9.2 — CurrentLine producer + consumer. Producer emission per Q#1 + Q#3; consumer renders via session-9.1's path. Exercises bets #2 (now SelectionCurrentLine overlap) and #3 (cadence).

Each session ends in a session-end commit. Worktree-per-step applies unless a session is short enough to land on main directly.

Deliberately not committed (framing-pass scope)

The framing pass closes with the following deferred to session 9.x or later:

  • Exact quad colors per kind. Stance β-ish defaults will be picked in session 9.1 (probably Indexed(4) translucent for Selection, Indexed(0) slightly-lighter for CurrentLine). Real users will prefer theme-driven; expose via pmacs.theme in a follow-up if needed. Not framing-time work.
  • Alpha vs reverse-foreground for selection. Some editors render selection by inverting the underlying text color; others by drawing a semi-transparent rectangle. Stance: rectangle (matches existing quad pipeline; reverse-foreground would require text-renderer cooperation we don't want yet). Decided session 9.1 if it surfaces.
  • SelectionSnapshot vs Decorations::Selection reconciliation. pmacs-protocol has both a SelectionSnapshot family and a DecorationKind::Selection. Phase A used the decoration path. If a finding emerges that the snapshot path should drive backgrounds instead, classify per rule (iii). Out of framing scope.
  • Acceptance-test shape for quad rendering. Headless wgpu golden- frame comparison was Phase A's deferred decision; quad backgrounds inherit the same defer. Session 9.x may surface complications; classify at surface-time.

Phase A finalization criterion

Phase A finalizes when:

  1. Session 9.1 ships and Selection renders correctly in pmacs-gpu against the Phase A test corpus.
  2. Session 9.2 ships and CurrentLine renders correctly with the Q#3 cadence holding.
  3. The session-9 audit doc records the predicted-vs-actual scoring, matching pmacs-gpu-phase-a-audit.md's shape.
  4. docs/pmacs-gpu-design.md's "Phase A" reference updates to point at the finalization audit doc.

The structural finding A8 from Phase A is then closed. SearchMatch{,Active} remains documented-but-not-implemented, queued for Phase B's editing-parity arc.