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

321 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`](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`](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 `Selection``CurrentLine` 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.