321 lines
16 KiB
Markdown
321 lines
16 KiB
Markdown
# 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 QB1–QB3 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 60–120 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.
|