Commit Graph

78 Commits

Author SHA1 Message Date
Levi Neuwirth 7bde2e7079 session R — per-line incremental reshape (typing latency floor)
Per docs/pmacs-gpu-perline-reshape-framing.md. Every keystroke ran a
full visible-slice reshape: rebuild all rich chunks, set_rich_text
(resets every BufferLine's shape cache), shape_until_scroll re-shapes
every visible line with Shaping::Advanced. Now a single-line edit —
the keystroke case — rebuilds exactly ONE BufferLine; the other
lines' shape caches survive and shape_until_scroll touches only the
fresh line.

- Q#R1: clipped_chunks_for_range is the single chunk source both the
  full reshape and the surgery derive from (full = slice range,
  surgery = the line's content range), so the two paths cannot
  disagree about a line's content. Parity with cosmic-text's own
  line splitting verified against the vendored 0.18.2 source:
  BidiParagraphs strips the separator per line in both its ASCII and
  BidiInfo paths, creates no trailing empty line, and set_rich_text
  assigns LineEnding::Lf uniformly + adds attr spans only when they
  differ from the defaults — the surgery mirrors all three.
- Fallbacks to full reshape: slice origin moved, line count changed
  (Enter / multi-line deletes), '\n' in the inserted text, edited
  line outside the shaped slice (an edit entirely PAST the slice updates
  view_range + redraws without any shaping), exotic paragraph
  separators, multi-edit batches.
- Q#R2: the pointer hit map goes lazy — surgery marks it dirty and
  hit_test_source_byte rebuilds on demand from the same chunk fn
  (clicks are rare next to keystrokes; the rebuild is an O(slice)
  byte walk, no shaping).
- Pure parity test pins full-walk == concatenated per-line walks
  (text + colors), including the newline-anchored inlay-hint
  boundary case (predicted finding #1's most likely site).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:01:51 -04:00
Levi Neuwirth a27ff6232f session M-2 — pmacs-gpu pointer input: hit test, drag select, wheel scroll
The GPU resolves pixels to source bytes entirely locally (Q#M2) and
ships byte-position gestures (Q#M1, protocol v5):

- Hit chain: pixel − TEXT_LEFT/TOP → cosmic_text::Buffer::hit
  (shaped line + byte-within-line) → projected byte via the
  projected text's line table → source byte via the run map.
- The run map is built by reshape from the SAME RichChunks that feed
  glyphon (each chunk now tags its origin: verbatim source run vs
  injected adornment), so the map and the shaped buffer cannot
  disagree; hits inside inlay-hint text snap to the hint's source
  anchor.
- Gestures: left Down (with frontend-side double-click detection →
  DoubleDown — only the frontend knows pixel proximity), Drag
  coalesced on hit-byte change (predicted finding #4), Up. Sends are
  gated on the daemon's Hello.protocol_version >= 5; an outgoing
  pointer supersedes any unconfirmed optimistic-cursor floor (the
  daemon's CursorByte answer is the click, not the typing
  prediction).
- Wheel scroll: new local handler (no wire — the GPU owns the
  viewport): LineDelta×3 / PixelDelta÷line-height, clamped scroll_top
  adjust → reshape → scoped viewport re-declaration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:23:14 -04:00
Levi Neuwirth e8b5b94a4d style: cargo fmt over the optimistic-editing arc
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:53:50 -04:00
Levi Neuwirth 2339a10b03 pmacs-gpu: forward chorded deletion keys (C-BS word delete)
Backspace/Delete with modifiers were withheld by the chord filter,
so C-BS / C-DEL / M-BS — word-level deletes in the default keymap —
silently did nothing in the GPU while C-<left> word motion worked.
Deletion keys now forward with their modifiers exactly like motion
keys; an unbound chord is a harmless no-op at the daemon keymap, and
chorded deletes never apply optimistically (optimistic_delete_range
requires empty modifiers), so they always round-trip into their
bound commands. Acceptance test drives C-BS / C-DEL through the real
dispatch path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:48:23 -04:00
Levi Neuwirth 21671cc590 optimistic Backspace/Delete — single-codepoint deletes apply locally
The last round-tripping editing keys. Same latency profile Enter had:
mid-burst they deferred behind unconfirmed inserts and everything
typed after them flushed in a delayed lump.

Daemon: single-delete CRDT hot path in apply_remote_crdt_op. The
deletion's start byte converts through the post-import doc (the
prefix is untouched); the end byte comes from walking the still
pre-import rope over the deleted codepoint count (reads at most
4 bytes per codepoint, not the file). Compound updates keep the
materialize+diff fallback.

pmacs-gpu:
- optimistic_crdt_delete mirrors the insert path; the shared gates
  (optimistic_edit_eligible) and tail (finish_optimistic_edit) are
  factored out. optimistic_delete_range predicts exactly one
  codepoint — matching buffer.delete-backward/-forward's no-region
  behavior — and declines on buffer edges, modifier variants
  (C-BS word delete), or a mid-codepoint cursor. Region deletes
  keep round-tripping into delete_region via the selection gate.
- Cursor-floor semantics tightened for non-monotonic predictions:
  only the exact predicted byte (or another buffer) confirms; plus a
  500ms timeout escape hatch — an unconfirmed floor (op dropped by
  validation, peer racing the window cursor) now releases instead of
  wedging deferred keys forever, falling back to round-trip input
  until the next CursorByte resynchronizes.
- Unconfirmed-edit journal rebasing generalized from pure inserts to
  delete-shaped entries (old_end translates independently, clamped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:25:21 -04:00
Levi Neuwirth d380358f2c pmacs-gpu: optimistic typing — local CRDT inserts, writer thread, frame translation
Typing no longer round-trips. Plain chars, Enter, and Tab (whose
default bindings reduce to a plain insert_char) apply to the local
Loro replica immediately and ship as FrontendEvent::CrdtOp:

- optimistic_crdt_insert: gated on DispatchIdle + a fresh CursorByte
  + no own-window selection (CUA type-over must round-trip into the
  region-aware commands, which a raw op bypasses). Predicted-cursor
  floor ignores stale in-flight CursorBytes; round-trip keys typed
  behind unconfirmed inserts defer until the floor confirms.
  Optimistic Enter scroll-follows immediately and re-declares the
  scoped viewport.
- attach: a writer thread owns the socket write half, so the winit
  thread never blocks on daemon backpressure; send_crdt_op added.
- Incremental text maintenance: Loro text deltas patch current_text
  and per-line byte/char offset tables in place (no whole-rope
  materialization per keystroke); cached spans/decorations/adornments
  translate through each edit.
- Unconfirmed-edit journal: incoming StyleSpans/Decorations frames
  carry the daemon's CRDT version scalar as generation; the GPU
  computes the same per-peer counter sum locally, prunes confirmed
  entries, and translates the frame's ranges through the rest — a
  frame computed before an in-flight keystroke no longer repaints the
  viewport's colors a few bytes left for one frame.
- Extend-at-end heuristic: a span ending exactly at a pure insert
  point extends over the typed text, so new chars inherit the
  preceding token's color instead of blinking default until the next
  parse settles.
- Per-edit viewport re-declaration narrowed to origin moves;
  PMACS_GPU_DEBUG_APPLY times per-message apply cost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 10:49:34 -04:00
Levi Neuwirth ecb0a862fc session S1 — viewport-scoped rendering + scroll (large-file perf)
Fixes the O(file)-per-keystroke slowness that made large-file editing
unusable. pmacs-gpu now shapes only the visible byte slice instead of
the whole rope.

Core change (`reshape`): compute the visible byte range from
`scroll_top` + the window's visible line count (+ small overscan),
slice `current_text[vstart..vend]`, clip+rebase spans / decorations /
adornments onto the slice (subtract `vstart`), and feed only that to
`set_rich_text`. cosmic-text now touches ~screenful of lines, not 25k.
`set_rich_text` resets scroll to the slice top (verified), so the
slice renders from y=0.

Scroll (line-based, Q#S1):
- `scroll_top` source-line state; `visible_byte_range` line-aligns the
  slice (cosmic-text splits BufferLines on `\n`).
- `scroll_to_cursor` (Q#S2): on `CursorByte`, if the cursor leaves the
  visible window, scroll to follow, re-shape, and re-declare the scoped
  Viewport. PageUp/Down already forward → daemon moves the cursor →
  this follows. No GPU-local page math.
- Scoped `Viewport` declaration (Q#S5) via `viewport_send_if_changed`
  (coalesced): on snapshot, scroll, edit (bytes shift), and resize. The
  producer already clips `StyleSpans`/`Decorations` to `vp.visible`, so
  it now styles only what's on screen — no producer change.

Rebasing (bet S2, the QB3-class risk): one primitive,
`clip_rebase_range`, clips a whole-file `[start,end)` to the slice and
subtracts `vstart`, returning `None` when disjoint. Caret
(`caret_rect`) and both wash collectors route through it; `line_offsets`
are computed on the slice. Caret returns `None` when scrolled
off-screen.

Also resets `scroll_top` + `last_viewport_sent` on buffer switch.

Tests: `clip_rebase_range_clips_to_slice_and_subtracts_vstart`.
Gates: fmt; clippy --all-targets --workspace -D warnings; pmacs-gpu
unit 23 (+1). Daemon/lib untouched.

Per the framing's process rule, NOT merged until visually confirmed on
a large file AND after scrolling (the rebasing is only exercised once
vstart > 0): editing snappy; arrows/PageUp/PageDown navigate with the
caret staying visible; styling + caret correct at any scroll position;
TUI stays converged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:40:04 -04:00
Levi Neuwirth 0f5f4d1769 B2 fix — clamp rich-text slice boundaries to char boundaries (crash)
Typing in a large file crashed: "byte index N is not a char boundary;
it is inside '→'". `projected_rich_chunks` slices `current_text` at
span / decoration / adornment byte offsets, but those offsets come from
the daemon for a possibly-earlier generation than the rope this frame
holds (the one-frame edit race). After an edit a stale offset can land
inside a multi-byte codepoint, panicking `text[a..b]`.

Snap every boundary to the previous UTF-8 char boundary before slicing
(new stable `floor_char_boundary` helper; the older `style_runs_for_text`
path already did the equivalent `is_char_boundary` guard — this newer
adornment-aware path was missing it). Flooring only shifts a chunk edge
left to the start of the codepoint it fell inside; chunks still
reassemble the original text.

Tests: `projected_rich_chunks_tolerates_mid_codepoint_boundaries`
(span ending mid-'→' + a past-end diagnostic; chunks reassemble the
text) and `floor_char_boundary_snaps_into_multibyte_char`.

Gates: fmt; clippy --all-targets --workspace -D warnings; pmacs-gpu
unit 22 (+2).

NOTE: this fixes the crash, not the large-file slowness — that is the
whole-file reshape architecture (projected_rich_chunks + set_rich_text
are O(file), run per edit, and the daemon runs a whole-file tree-sitter
highlight query per edit). Making large-file editing usable needs
viewport-scoped rendering + scrolling, scoped on both the GPU and the
producer. That is its own session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:21:09 -04:00
Levi Neuwirth 5888ced15a session B2 — text editing in pmacs-gpu (Key round-trip)
Broadens the send gate from motion-only (B1) to plain text editing:
`should_forward_key` forwards Char / Backspace / Enter / Delete / Tab
in addition to motion keys. Editing rides the same round trip B1
proved — the daemon's `dispatch_key` self-inserts / deletes on the
viewport-aligned buffer, authors the CRDT op (`CrdtOpOrigin::DaemonKey`,
excluded from no recipient), and broadcasts it back; pmacs-gpu applies
it (existing session-3 CrdtOp path) and the edit also propagates to the
TUI. No editing logic in the frontend.

Ctrl/Alt/Meta chords are deliberately withheld: they drive commands and
minibuffer flows the GUI can't render or interact with yet (the
minibuffer is instance-side global state; a GUI frontend opening one
with no way to see/cancel it would wedge input). Those land in a later
command-parity session with GUI minibuffer rendering. Shift is not a
chord modifier — Shift+a already arrives as `Char('A')`.

Test `should_forward_key_gates_editing_keys_and_excludes_chords`:
editing keys + uppercase forward; Ctrl/Alt + char withheld; motion
keys forward regardless of modifiers.

Gates green: fmt; clippy --all-targets --workspace -D warnings;
pmacs-gpu unit 20 (+1). Daemon/lib untouched (it already dispatches
semantic-frontend keys, B1 fix).

Awaiting visual confirmation: typing in pmacs-gpu inserts text that
propagates to the TUI; backspace/enter/delete work; CRDT stays
converged. Per the framing's process rule, not merged until confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:13:43 -04:00
Levi Neuwirth da9a63aa10 B1 polish — fix cursor-motion slowness + whole-line "selection" look
Two issues from visual validation now that arrow keys work:

1. Far too slow. The `Decorations` arm called `self.reshape()`
   (set_rich_text + shape_until_scroll — a full text re-shape) on
   *every* decoration change. B1's own-window `CurrentLine` decoration
   changes on every up/down move, so each vertical cursor step forced a
   full re-shape. But only diagnostic decorations affect the rich text
   (they override glyph fg in `projected_rich_chunks`); Selection /
   CurrentLine / search are background quads rebuilt cheaply in
   `render()`. Now reshape runs only when the fg-affecting set changed
   (`fg_decoration_fingerprint` compares before/after); a
   background-only change just requests a redraw.

2. The entire line looked selected. The own-window `CurrentLine` wash
   paints the whole cursor line, which reads as a persistent selection
   — unwanted as default. The caret already marks the own cursor, so
   `collect_own_decoration_rects` now skips `CurrentLine` (renders only
   own `Selection`). Revises Q#B4: the caret is the own-cursor
   indicator, not a line wash. Peer presence still shows other
   frontends' lines.

Test `fg_fingerprint_ignores_background_decoration_changes`: a
CurrentLine-only change leaves the fingerprint equal (no reshape); a
diagnostic change alters it (reshape).

Gates green: fmt; clippy --all-targets --workspace -D warnings;
pmacs-gpu unit 19 (+1). pmacs lib / daemon untouched.

Deferred (noted for follow-up sessions, not B1):
- Mouse click → cursor: needs the Q#B5 wire decision (no
  FrontendEvent::SetCursor variant; semantic frontends can't use
  grid-cell Mouse coords). Its own session.
- PageUp/PageDown: keys are forwarded and move the daemon cursor, but
  pmacs-gpu renders from the top with no scroll, so the caret would
  leave the viewport. Needs GPU scrolling first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:06:59 -04:00
Levi Neuwirth 4cd968bc0c B1 fix — align semantic frontend's window to its displayed buffer
Picks up the "arrow keys do nothing in the GUI" investigation. Root
cause is the multi-buffer mismatch the manual investigation theorized,
now confirmed in code and tested:

- `build_fresh_frontend_view` binds an attaching frontend's window to
  LOCAL's active buffer (a scratch the TUI never switched LOCAL away
  from).
- `send_buffer_snapshots` ships a snapshot per buffer in registry
  order; pmacs-gpu treats each as "switch visible buffer", so its
  `current_buffer_id` (and what it displays) becomes the LAST one — the
  file the TUI opened.
- So the GUI displays the file, but its daemon-side window edits the
  scratch. Arrow keys → `dispatch_key` → move the scratch cursor →
  `CursorByte { buffer_id: scratch }` → pmacs-gpu ignores it (its
  `current_buffer_id` is the file). The caret never tracks.

Fix: the `Viewport` event already declares which buffer the frontend
is displaying. The daemon now calls `align_semantic_window_to_buffer`
on it — re-pointing the semantic frontend's window at the declared
buffer (rebuild the cheap `TextView` line index, reset cursor; a
semantic frontend has no grid overlays to migrate, it renders from the
wire). Input and the `CursorByte` it produces then target the buffer
the user is actually looking at. The guard makes it a no-op when the
buffer is unchanged (so per-edit Viewport re-declarations don't reset
the cursor).

Tests:
- `viewport_aligns_semantic_window_to_displayed_buffer` — window
  starts on scratch, declares the file via align, a key then
  self-inserts into the *file*.
- `semantic_frontend_key_event_reaches_the_core` (from the prior
  commit) still green.

Also adds `PMACS_GPU_DEBUG_INPUT=1`: logs keys sent and each
`CursorByte` with `buf`/`current`/`match` so the displayed-vs-edited
buffer alignment is visible at a glance on retest.

Gates green: fmt; clippy --all-targets --workspace -D warnings
(default + crdt); pmacs lib 1334; crdt daemon tests 7; pmacs-gpu unit
18; m4_acceptance 88; m11_5_semantic_acceptance 2.

Still needs visual confirmation (arrow keys move the caret in a
running pmacs-gpu) before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 09:49:53 -04:00
Levi Neuwirth 5eec8a6f10 session B1 — keyboard cursor motion in pmacs-gpu
First Phase B session: pmacs-gpu can move its own cursor. Consumer-only
(the daemon already dispatches FrontendEvent::Key through the same
keymap/command stack the TUI uses; verified in the Phase B framing).

- `AttachClient::send_key` emits `FrontendEvent::Key`.
- `translate_key` maps winit logical key + modifier state → protocol
  (Key, Modifiers). Covers the full editing set; `is_motion_key` gates
  B1 to cursor-motion keys only (arrows, Home/End, PageUp/PageDown) so
  no buffer mutation happens yet — editing keys open in B2 by dropping
  the gate. Modifiers tracked via winit `ModifiersChanged`.
- `window_event` rework: Escape stays a local quit; other pressed keys
  translate and (motion-gated) `send_key`.
- Consume `InstanceMessage::CursorByte` → `own_cursor` (Q#B3: the
  daemon is authoritative; the caret follows whatever it reports, incl.
  command-driven motion this frontend never interprets).
- Caret: a thin quad bar drawn *over* the text at the cursor glyph,
  byte→glyph mapping rebased per line via `line_byte_offsets[line_i]`
  (bet B4 / the QB3 lesson applied up front).
- Un-suppress own-window `Selection`/`CurrentLine` washes from
  `current_decorations` alongside peer presence (Q#B4): the QB1
  suppression lifts now that the own cursor is live. The bg-wash
  builder split into `collect_own_decoration_rects` +
  `collect_peer_rects`.
- own_cursor cleared on BufferSnapshot (prior-buffer offsets).

Tests: `translate_key_maps_motion_named_keys_and_chars`,
`translate_key_carries_modifiers`. pmacs-gpu unit 18 (+2).

Gates green: fmt; clippy --all-targets --workspace -D warnings (default
+ crdt); pmacs-gpu unit 18. Daemon/lib untouched.

NOT YET VISUALLY VALIDATED — per the Phase B framing's process
correction, this must be confirmed in a running pmacs-gpu (arrow keys
move the caret + own current-line wash; TUI unaffected) before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:15:33 -04:00
Levi Neuwirth 4dd2d7e889 9.3 fix — rebase glyph offsets to buffer-absolute (selections blind)
The debug aid confirmed peer presence arrives with real selections
(sel=Some { anchor: 1837, active: 1844 }), yet no wash drew — because
the rect geometry was computed against the wrong coordinate space.

cosmic-text's `LayoutGlyph::{start,end}` are byte offsets within the
*original line* (`LayoutRun::line_i`), not the whole buffer.
`push_glyph_extent_rects` was comparing those line-relative offsets
against whole-buffer byte ranges from presence/`source_line_range`.
They only coincide on line 0, so any Selection or CurrentLine past the
first line never matched a glyph and produced no rect — "blind." This
was latent since 9.1 (Selection was never visually validated) and was
masked in 9.2 whenever the cursor happened to sit on line 0.

Fix: build `line_byte_offsets(current_text)` — the buffer-absolute
start of each `\n`-delimited line — and rebase each run's glyphs by
`line_offsets[run.line_i]` before comparing. Computed once per
`peer_background_rects` call and threaded into
`push_glyph_extent_rects`.

New test `line_byte_offsets_indexes_each_logical_line`.

Gates: fmt clean; clippy -p pmacs-gpu -D warnings clean; pmacs-gpu
unit 16 (+1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:40:27 -04:00
Levi Neuwirth 8e29e8b90b 9.3 perf — collapse per-tick rope copy; add frame timing
Investigating the cursor slowdown reported after the wash became
visible.

Confident daemon-side win: `scoped_decorations` (run every tick per
semantic frontend, in the daemon's single-threaded loop that also
serves the TUI) was materializing the whole buffer via
`buffer_source_bytes` — an O(n) rope→Vec copy — TWICE per tick: once
in the 9.2 CurrentLine branch and again in the diagnostics branch. For
an LSP buffer (diagnostics present, the common case) that doubled the
per-tick copy cost, and the daemon's tick latency gates TUI cursor
responsiveness. Now the source + line-start table is materialized at
most once per call via `get_or_insert_with` and shared between both
branches (and skipped entirely when neither branch needs it).

Consumer instrumentation to localize any remaining cost:
- `PMACS_GPU_DEBUG_FRAME=1` logs per-`render()` sub-phase timings
  (background rects / minimap rects / glyph prepare+submit / total /
  peer count). winit defaults to ControlFlow::Wait, so renders are
  on-demand (one per coalesced redraw request), not a continuous
  loop — the timing isolates the cost of a single cursor-driven frame.
- The `PMACS_GPU_DEBUG_PRESENCE` check is now one-shot via OnceLock
  instead of a per-message `std::env::var_os` (which locks the global
  env table); same for the new frame flag.

No behavior change to the rendered output. `render()` gains the
clippy too_many_lines allow (now 115 lines with the timing block),
matching the precedent on the other linear GPU-setup functions.

Gates green:
- cargo fmt --all -- --check
- cargo clippy --all-targets --workspace -- -D warnings
- cargo clippy --all-targets --workspace --features crdt -- -D warnings
- pmacs lib 1329 + pmacs-protocol 11; pmacs-gpu unit 15
- m4_acceptance 88, m11_5_semantic_acceptance (--features crdt) 2

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:26:38 -04:00
Levi Neuwirth ffdd5d801a 9.3 follow-up — make CurrentLine wash visible + presence debug aid
Manual retest still showed nothing. Two changes to localize and fix:

1. CurrentLine alpha 0.08 → 0.22. The original value computed to only
   ~10/255 above the dark clear color and was swamped by glyphs on any
   text line — effectively invisible even when the wash was being
   drawn correctly. 0.22 reads as a current-line band while staying
   below Selection's 0.30. Static analysis of the full wire path
   (daemon sweep → multi_frontend broadcast → reader → apply_attach_
   message → peer_background_rects) found no break, so faint alpha is
   the leading explanation for "still nothing."

2. Env-gated diagnostic in the PresenceUpdate arm. Running with
   `PMACS_GPU_DEBUG_PRESENCE=1` prints each received presence
   (frontend, buffer, current buffer, cursor, selection). If presence
   lines appear, the wash geometry/alpha was the issue; if none appear,
   the broadcast isn't reaching the mirror and the next step moves to
   the daemon side. Off by default — no effect on normal runs.

Gates: fmt clean; clippy -p pmacs-gpu -D warnings clean; pmacs-gpu
unit 15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 14:57:26 -04:00
Levi Neuwirth 57feae1e2d session 9.3 — peer-presence cursor/selection (fixes QB1)
Manual validation of 9.2 surfaced finding QB1: the CurrentLine wash
never appeared, and 9.1's Selection never actually rendered either.

Root cause: `Selection` and `CurrentLine` are the only two
per-WINDOW-state decorations; every other rendered family (StyleSpans,
diagnostics, inlay hints, minimap) is keyed to the shared BUFFER. The
producer emits both from the viewing frontend's own window
(`active_window_for(self.frontend_id)`). pmacs-gpu is a read-only
mirror 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.
Both decorations are therefore inert in pmacs-gpu: CurrentLine paints
a static line-0 wash (invisible at alpha 0.08) and Selection never
appears. What the user actually watches is the *editing* frontend's
(their TUI's) cursor — which is peer presence.

Fix is consumer-only — no producer or protocol change. The wire
already carries it: `InstanceMessage::PresenceUpdate { frontend_id,
buffer_id, cursor, selection }` is broadcast by the daemon to every
`multi_frontend` recipient, pmacs-gpu already negotiates
`multi_frontend: true`, and it was simply dropping the message at its
`_ => None` catch-all.

Q#5 (recorded in the framing doc): peer presence is the authoritative
cursor/selection source for a read-only mirror.

- New `peer_presences: HashMap<FrontendId, PeerPresence>` state,
  cleared on BufferSnapshot (peer offsets are prior-buffer-relative).
- New `PresenceUpdate` arm stores per-peer (buffer_id, cursor,
  selection) and requests a redraw.
- `peer_background_rects` replaces the old
  `decoration_background_rects`: renders `CurrentLine` over the source
  line holding each peer's cursor (`source_line_range`) and
  `Selection` over each peer's selected range, both via the shared
  `push_glyph_extent_rects` (the former inline glyph-overlap loop,
  extracted). Own-window Selection/CurrentLine in `current_decorations`
  are no longer drawn as backgrounds — they're inert for a read-only
  mirror. Diagnostic (foreground) decorations are untouched.
- 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.

Deferred within the stance (documented): per-peer stable colors (single
peer reuses the Selection/CurrentLine colors), peer caret glyph +
"user N" label, and own-vs-peer cursor merge once input lands.

New tests: `source_line_range_locates_enclosing_line` +
`source_line_range_handles_empty_and_leading_newline`. The peer
rect generation itself needs a laid-out buffer (font system) and is
covered by the manual probe.

Two pre-existing functions tipped past clippy's 100-line limit by the
additions (`State::new` 101, `apply_attach_message` 116, a per-variant
match dispatcher); both get `#[allow(clippy::too_many_lines)]`,
matching the precedent on `semantic_render::render_frame`.

Gates green:
- cargo fmt --all -- --check
- cargo clippy --all-targets --workspace -- -D warnings
- cargo clippy --all-targets --workspace --features crdt -- -D warnings
- pmacs lib 1329 + pmacs-protocol 11
- pmacs-gpu unit 15 (+2 source_line_range tests)
- m4_acceptance 88, m11_5_semantic_acceptance (--features crdt) 2

Manual probe: daemon + TUI attach + pmacs-gpu attach. Move the cursor
in the TUI — pmacs-gpu's CurrentLine wash should track the TUI's line.
Select text in the TUI — the Selection wash should mirror it. Both
should now actually appear and follow the editing frontend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 14:42:52 -04:00
Levi Neuwirth 7dfcd79d72 session 9.2 — CurrentLine quad backgrounds
Closes the second half of Phase A's deferred finding A8. The producer
now emits DecorationKind::CurrentLine derived from the active window's
cursor; pmacs-gpu paints it as a very subtle blue-grey wash under the
line carrying the cursor.

## Q-stance implementation status

- **Q#1 stance (α) — producer-side emission**: `scoped_decorations`
  reads `core.active_window_for(self.frontend_id).cursor`, derives the
  enclosing line via a new `current_line_range` helper, and pushes a
  `Decoration { kind: CurrentLine, range }` clipped to the viewport.
  Same per-frontend access path used for Selection (line 378).
- **Q#3 stance (β) — per-line cadence**: implementation-revealed
  simplification. The framing doc proposed a `last_cursor_line` cache
  on SemanticRenderState; in practice the existing M11.4 diff
  (`changed_intervals`) already gives this for free. A same-line
  cursor move produces a byte-identical decoration Vec, so
  `changed_intervals` returns empty and nothing ships. A line change
  produces a different range and re-emission fires. No extra state
  needed. Recorded as a small finding under rule (iii); the stance
  holds, only the implementation tightens.
- **Q#2 (render order)** continues to apply from 9.1 — quad
  backgrounds first, text second, minimap last.
- **Q#4 (search backgrounds)** still deferred awaiting search.

## Producer

- New `current_line_range(line_starts, source_len, cursor) -> (u64,
  u64)` helper at `src/semantic_render.rs`: binary-searches line_starts
  for the largest `start <= cursor`, returns the half-open byte range
  `(line_start, next_line_start_or_source_len)`. Clamps to source_len
  so a cursor at or past EOF resolves to the last line cleanly.
- `scoped_decorations` restructured: the Selection branch and the new
  CurrentLine branch share the `win.buffer_id == vp.buffer_id` gate so
  per-window state never leaks into a viewport projecting a different
  buffer (the `decorations_use_vp_buffer_not_active_buffer` invariant).
- Four new tests:
  - `current_line_range_finds_enclosing_line` — unit test covering
    line-zero, mid-line, start-of-line, last-line, and past-EOF.
  - `current_line_projects_as_a_decoration_for_cursor_on_seed` —
    cursor at byte 0 of "abc\\nde" emits CurrentLine for [0, 4).
  - `current_line_skipped_when_active_window_is_a_different_buffer` —
    multi-frontend invariant: projecting a non-active buffer does not
    emit CurrentLine.
  - `same_line_cursor_motion_does_not_re_emit_decorations` — Q#3
    cadence: horizontal motion within a line is silent; crossing `\n`
    re-emits.
- Existing test `diagnostics_project_with_line_col_to_byte_and_severity`
  updated: the seeded "abc\\nde" buffer now produces both a
  DiagnosticWarning and a CurrentLine. The test now finds the warning
  by `kind` and asserts its byte range rather than asserting a total
  count of 1.

## Consumer

- `decoration_kind_to_bg_color` in pmacs-gpu/src/main.rs adds the
  CurrentLine arm: `[0.55, 0.60, 0.75, 0.08]` — a very subtle blue-grey
  with low alpha. CurrentLine is always on, so it wants to be visually
  quietest of the four background kinds; just enough tint to track
  cursor line, not enough to compete with Selection or syntax color.
- `bg_color_helper_covers_selection_and_returns_none_for_unrendered_kinds`
  renamed to `bg_color_helper_covers_selection_and_current_line` and
  updated to assert CurrentLine now returns Some.
- `fg_and_bg_helpers_are_disjoint_total_cover` updated: CurrentLine is
  no longer in the "deferred neither yet" set, only the search pair.

## Bet status

- **Bet #2 (overlap composition between Selection and CurrentLine)**:
  exercised. CurrentLine has alpha 0.08, Selection 0.30. When both
  cover the same bytes (cursor on a selected line), they alpha-blend
  in draw order. Composition is left to the M11.4 dirty-merge ordering
  (decorations sorted by range.start): CurrentLine paints first
  (covers the whole line, lower start), Selection paints on top. The
  resulting visual is selection-blue with a slight CurrentLine tint
  visible at the line's non-selected ends. Honest composition rule
  if surfaced as wrong: refine.
- **Bet #3 (cadence)**: predicted producer-side `last_cursor_line`
  cache; implementation revealed the M11.4 diff already throttles.
  Score: predicted category surfaced (true positive on the cadence
  concern), but the *implementation* category for the resolution did
  not match. Recorded as rule-(iii) small finding.

## Gates (all green)

- `cargo fmt --all -- --check`
- `cargo clippy --all-targets --workspace -- -D warnings`
- `cargo clippy --all-targets --workspace --features crdt -- -D warnings`
- pmacs lib + pmacs-protocol: **1329 + 11 = 1340** (+4 new producer
  tests)
- pmacs-gpu unit: **13** (unchanged count; one test renamed +
  re-scoped)
- m4_acceptance: **88**, m11_5_semantic_acceptance (--features crdt):
  **2**

## Manual validation walkthrough

Same daemon + TUI attach + pmacs-gpu attach shape. In the GPU window:

- Verify a subtle blue-grey wash appears under the cursor's line.
- Move the cursor up/down — the wash tracks the new line.
- Move the cursor left/right within a line — visible behavior should
  be identical (Q#3 cadence: no re-render needed).
- Select text crossing the current line — Selection paints over
  CurrentLine; both alpha-blends visible at the line's non-selected
  edges.
- Resize the window — both backgrounds reshape correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:20:26 -04:00
Levi Neuwirth 54991c89e4 session 9.1 — Selection quad backgrounds + framing doc
Retires the first half of Phase A's deferred finding A8 (background-
bearing decoration kinds couldn't render through cosmic-text's
foreground-only `Attrs`). `DecorationKind::Selection` now paints a
translucent blue rectangle under the selected glyphs in pmacs-gpu,
reusing the wgpu `QuadRenderer` that shipped for the minimap in
session 7.

The framing doc (`docs/pmacs-gpu-quad-backgrounds-framing.md`)
commits the load-bearing decisions before code lands: stance (α)
for Q#2 — single render pass, three draws in the order backgrounds
→ text → minimap — is what this commit implements. Q#1 (CurrentLine
source location, stance α: producer-side from
`core.active_window_for(self.frontend_id).cursor`) and Q#3 (per-line
cadence, stance β) are sketched for session 9.2; Q#4 defers search
backgrounds awaiting an upstream pmacs search feature.

Three components:

1. `decoration_kind_to_bg_color` helper, sibling of the existing
   `decoration_kind_to_color`. Returns `Some([f32; 4])` RGBA for
   Selection; `None` for CurrentLine (9.2), SearchMatch /
   SearchMatchActive (deferred), and the four diagnostic kinds
   (foreground-only). New unit tests assert disjoint total cover
   between the two helpers across the eight kinds.

2. `State::decoration_background_rects` walks
   `Buffer::layout_runs()`, finds glyphs whose `[start, end)`
   overlaps each background-bearing decoration's `ByteRange`, and
   produces one `MinimapRect` per laid-out visual line that
   contributes glyphs. Multi-line selections fan out as N rects.

3. Render-order change in `State::render`: a `bg_buffer` is built
   ahead of the minimap buffer and drawn first in the render pass
   (before `text_renderer.render`), so selection fills sit under
   the glyphs with the 0.30-alpha letting source color show through.
   Minimap continues to draw last.

Gates green:
- cargo fmt --all -- --check
- cargo clippy --all-targets --workspace -- -D warnings
- cargo clippy --all-targets --workspace --features crdt -- -D warnings
- pmacs-gpu unit: 13 (+2 new bg-color helper tests)
- pmacs lib: 1325, pmacs-protocol: 11
- m4_acceptance: 88, m11_5_semantic_acceptance (--features crdt): 2

Bet exercise so far: bet #1 (multi-line vertex decomposition) is
implicitly tested by the layout-run loop but waits on visual
validation for honest scoring. Bet #2 (overlap composition) is not
exercised in 9.1 — Selection is the only background kind, so no
overlaps with CurrentLine or future kinds. Bet #3 (cadence) is a
9.2 concern.

Manual probe: launch daemon + TUI attach + pmacs-gpu attach against
any file, select text in the TUI, verify the pmacs-gpu window paints
a translucent blue rectangle over the selected glyphs that tracks
selection extension. Multi-line selection should produce per-visual-
line rectangles.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:49:52 -04:00
Levi Neuwirth a67cb8a6f1 Close pmacs-gpu phase A audit 2026-05-28 12:49:23 -04:00
Levi Neuwirth a6503529ff Render file style summary minimap 2026-05-27 14:21:42 -04:00
Levi Neuwirth 71b21dee1e Render inline adornments in pmacs-gpu 2026-05-27 10:24:20 -04:00
Levi Neuwirth 0902a9e173 Revert "session 5 fixup: clear styling on CrdtOp ..."
The clear-on-CrdtOp change broke the producer's incremental-update
contract. The producer ships dirty-range spans only on `full=false`
frames; the frontend is expected to retain non-dirty spans across
edits. Emptying both vectors meant the frontend ended up with only
the small dirty-range spans, missing the rest of the viewport — all
colors disappeared after an edit.

Reverting here. The proper fix lives in pmacs core (T M11.7):
producer must force `full=true` on generation transitions so the
frontend gets a complete replacement set on every text edit. Once
that lands, session-5's CrdtOp handler doesn't need to clear
anything — the next frame's `full=true` does it via
`replace_style_spans` / `replace_decorations`.

This reverts commit 49785c4. Returns the consumer behavior to
session-5's original "one-frame stale" artifact pending the core
fix.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:32:47 -04:00
Levi Neuwirth abd6f46eee session 5 fixup: clear styling on CrdtOp to kill stale-position artifacts
Surfaced during session-5 manual validation (probe #3, the bet-#1
shape from the framing pass): editing at a diagnostic boundary
left stale color fragments visible against now-different text. The
session-4 PR documented this as a "one-frame stale" artifact, but
in practice the LSP re-analysis window stretches the wrong-color
period to 100ms–5s — human-perceptible and confusing.

Mechanism: CrdtOp updates `current_text` but `current_spans` /
`current_decorations` still index into pre-edit byte positions.
`reshape()` paints them at those stale positions against the new
text, producing colored fragments over wrong characters until the
producer ships an updated frame. Between CrdtOp arrival and
clangd's next publishDiagnostics (LSP debounce + re-analysis), no
Decorations frame ships at all (the producer's
`changed_intervals(prev, curr)` sees identical sets because clangd
hasn't republished yet).

Fix: drop both vectors in the CrdtOp arm before `set_text`. Tree-
sitter re-emits StyleSpans within ~one frame; LSP decorations
re-emit when clangd republishes. Cost = a brief uncolored window
per edit. Gain = no wrong-position color persists.

Manual revalidation (post-rebase on #44 + #45 + #46):
edit-at-boundary in the TUI now drops the old diagnostic color
cleanly. New diagnostic colors paint once clangd republishes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:32:47 -04:00
Levi Neuwirth 2076a3682d session 5: Phase A — Decorations consumption (diagnostics as fg)
Second Phase A session. pmacs-gpu now consumes
`InstanceMessage::Decorations` with the same M11.4 dirty-merge shape
as `StyleSpans`. Diagnostic kinds render as foreground color
overrides; background-needing kinds (selection, search match, current
line) accumulate in state but stay unpainted pending a quad pipeline.

What's wired

- `State.current_decorations: Vec<Decoration>`, sorted by
  `range.start`, cleared on `BufferSnapshot` like `current_spans`.
- `apply_attach_message` gains a `Decorations` arm — `full=true` →
  `replace_decorations`, `full=false` → `merge_decorations` (M11.4
  clip/drop/split, structurally identical to `merge_style_spans`).
- `reshape()` rewritten as a sorted-boundary sweep over both spans
  and decorations: every coverage edge becomes a chunk break.
  Effective fg color = first matching decoration with a renderable
  color, else span color, else default.
- `decoration_kind_to_color`: red error / yellow warning / blue info
  / dim hint; selection/search/current-line return `None`.

Session-5 findings (rule iii, both deferred)

- **M11.4 merge logic duplicated** between `StyleSpan` and
  `Decoration`. Structural-but-minor; defer until a third instance
  surfaces (peer-cursor decorations from `PresenceUpdate` are the
  likely third point) so the generic shape is inducted from three
  examples, not two.
- **Background-kind decorations need a wgpu quad pipeline**. glyphon
  0.11 / cosmic-text 0.18 `Attrs` is foreground-only. Structural —
  new render pass + composition story with text. Its own session,
  not absorbed into Phase A.

Adversarial-verification framing

Probe #5 (active diagnostics + multi-frontend `PresenceUpdate`
overlap) — the diagnostics half is exercised; PresenceUpdate is its
own family and isn't consumed yet. Probe #3 (viewport-boundary
edges) gets re-tested: `merge_decorations` is the same code shape as
`merge_style_spans`, so an edge-case finding there would replicate.

Gates

`cargo fmt`; `cargo clippy --all-targets --workspace -D warnings`
clean; lib 1303 + protocol 11 = 1314; m4 83; m11_5 (--features crdt)
2.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:32:47 -04:00
Levi Neuwirth 32b529eea5
session 4: Phase A — StyleSpans consumption + rich-text rendering (#42)
First Phase A session. pmacs-gpu now sends FrontendEvent::Viewport
back to the daemon after BufferSnapshot lands, receives the resulting
InstanceMessage::StyleSpans frames, and renders the rope with
per-span colors via cosmic-text's set_rich_text.

What's wired:
- AttachClient gains a write-side Arc<Mutex<UnixStream>> and the
  assigned FrontendId from Hello; new send_viewport method emits
  FrontendEvent::Viewport. The mutex is over-cautious for our
  single-threaded event loop but future-proofs against multi-window
  emission.
- State adds current_buffer_id and current_spans (sorted by
  range.start). BufferSnapshot bootstraps both, then the App's
  user_event handler emits a follow-up Viewport via
  AttachClient::send_viewport.
- StyleSpans handling distinguishes full vs incremental:
  * full=true: replace_style_spans drops prior, takes segments as
    authoritative for the declared viewport.
  * full=false: merge_style_spans applies the M11.4 dirty-segment
    rule — spans fully inside a dirty range drop; spans straddling
    a dirty edge get clipped to outside the range (with the
    straddles-both-edges case splitting into two); new spans append;
    re-sort by start.
- reshape() walks current_text + current_spans, emits (substr,
  Attrs) chunks at every span boundary (with .min(text_len) clamps
  for safety against stale spans past EOF), calls set_rich_text.
- cell_color_to_glyphon converts cell::Color to glyphon::Color via
  the standard xterm-style 256-color palette (16 ANSI + 6x6x6 cube +
  24-step grayscale). Default → None so the renderer's default
  Attrs color stays.

Adversarial verification scope (Phase A probes):
- #1 non-ASCII source: exercised through the UTF-16 col/byte
  conversion already in pmacs's producer side; pmacs-gpu just renders
  what the wire delivers. Non-ASCII files should show correct
  styling at the right byte positions.
- #3 viewport-boundary tokens: the merge_style_spans path is exactly
  bet #1 from the framing pass ('StyleSpans/Decorations dirty-segment
  edges at viewport boundaries — headless-test-blind-spot probe').
  Edits near a span edge exercise the clip-and-merge logic.
- #6 CRLF line endings: implicit — pmacs's rope uses byte offsets so
  styling spans naturally include or exclude the \r as the producer
  decided. pmacs-gpu doesn't special-case line endings.

Known limitation (session 4 acceptable artifact, documented in
set_text): CrdtOp + StyleSpans arrive separately. CrdtOp updates text;
StyleSpans for the new generation comes one tick later. Between the
two, current_spans points at pre-edit byte positions while the text
is post-edit — visually stale for one frame. The .min(text_len) clamp
in reshape() keeps it safe; the artifact is brief.

Headless test gap: there's no Rust-level test of merge_style_spans
or the rich-text segmentation. Phase A's framing intentionally
chose manual validation over headless tests for these paths (the
adversarial probes are visual). A Phase A audit doc lands at session
close with the predicted-vs-actual scoring; per-method unit tests
for the merge logic could land then if findings argue for them.

Gates: fmt; clippy --all-targets -D warnings clean across the whole
workspace; lib 1303 + pmacs-protocol 11 = 1314; m4_acceptance 83;
m11_5_semantic_acceptance --features crdt 2.

Manual validation pending — same daemon+TUI+pmacs-gpu setup as
session 3, now showing colored text in the GPU window.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 00:28:53 +00:00
Levi Neuwirth 1506975ddb session 3 commit 3/3: doc the daemon's --features crdt requirement
Surfaced during manual validation: the pmacs-gpu window sits on
'(connecting...)' forever when attaching to a daemon built without
--features crdt. Handshake succeeds (negotiation reports semantic_render
+ crdt_replica as agreed by both sides), but the daemon's
crdt_replica default is cfg!(feature='crdt')=false in that build, so
send_buffer_snapshots() never fires and pmacs-gpu has nothing to
render.

Classified small under rule (iii). The structural answer (should the
daemon return a clearer signal when crdt_replica was negotiated but
isn't actually compiled in?) is genuine but deferred; for session 3
the failure mode is now documented inline at the build-AttachRequest
site so the next user to hit it recognizes the symptom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:12:13 -04:00
Levi Neuwirth 62cee9118c session 3 commit 2/2: attach mode + loro rope reconstruction
pmacs-gpu now has two run modes:
- no args: hello-world (session-2 behavior preserved)
- --attach <socket>: connect to a pmacs daemon, negotiate
  semantic_render + crdt_replica, import BufferSnapshot into a local
  loro replica, render the rope text. Live CrdtOp updates apply as
  they arrive.

Architecture:
- pmacs-gpu/src/attach.rs (new): UnixStream connect + Hello /
  AttachRequest handshake on the main thread; spawns a reader thread
  that pumps decoded InstanceMessage frames through the winit
  EventLoopProxy as AppEvent::Attach(AttachEvent::Message). Clean EOF
  or transport errors surface as AttachEvent::Disconnected. Reader
  thread holds the read half of the stream; AttachClient retains the
  write half (unused yet — session 4 wires FrontendEvents back).
- pmacs-gpu/src/main.rs: ApplicationHandler<AppEvent> with a
  user_event handler that dispatches Message variants. BufferSnapshot
  builds a fresh LoroDoc, imports the snapshot bytes, extracts text
  via doc.get_text('body').to_string(), and re-shapes the glyphon
  buffer. CrdtOp passes the op bytes through doc.import (loro
  accepts both shapes), re-extracts text, re-shapes. Other
  InstanceMessage variants are intentionally ignored at session 3.
- Font size dropped from 48pt to 16pt now that we may render full
  files (the hello-world 48pt was fine for one line, awful for code).
- Initial text is '(connecting...)' in attach mode, 'hello, pmacs' in
  hello-world; attach failure falls back to '(attach failed; see
  stderr)' so the window still opens.

One small finding logged in attach.rs's connect() doc: AttachRequest's
initial_size field is a CellSize (rows × cols), nominally
TUI-shaped. Sent as a placeholder (24×80) — a structural answer
('what does initial size mean for a pixel frontend?') belongs in its
own protocol thread, not session 3. Classified under rule (iii) as
deferred.

Container id for the loro text container ('body') hardcoded to match
pmacs::crdt::CrdtState — second finding worth pre-recording: the
container name is a wire-adjacent convention that isn't carried on
the wire itself. Both ends have to agree out-of-band. Not blocking
for session 3 but a structural smell for the producer arc. Logged
as deferred (rule iii structural; the answer is probably 'thread the
container id through BufferSnapshot' but it's not session-3 scope).

Gates: cargo fmt, cargo clippy --all-targets -D warnings (whole
workspace) clean; lib 1303 + pmacs-protocol 11 = 1314 unchanged;
m4_acceptance 83; m11_5_semantic_acceptance --features crdt 2.

Manual validation pending — agent environment is headless. User
walks through: start a pmacs daemon, run pmacs-gpu --attach <socket>,
confirm the window renders the daemon's file contents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:23:04 -04:00
Levi Neuwirth a706864e04 session 2: pmacs-gpu workspace + wgpu/winit/glyphon hello-world
Adds the pmacs-gpu binary crate to the workspace. wgpu 29.0 + winit
0.30 + glyphon 0.11 (cosmic-text 0.18 via re-export) + pollster +
env_logger; pmacs-protocol in the dep graph but not consumed yet
(session 3 wires the attach loop).

The binary opens an 800x200 window titled 'pmacs-gpu hello-world',
sets up wgpu against its surface, configures glyphon with the bundled
JetBrains Mono Regular, and renders 'hello, pmacs' once per redraw.
Close button or Escape exits. Resize re-configures the surface and
glyphon viewport. Surface acquisition matches wgpu 29's
CurrentSurfaceTexture enum (success/suboptimal render through; lost/
outdated re-configure; timeout/occluded skip the frame).

Bundled assets: pmacs-gpu/fonts/JetBrainsMono-Regular.ttf (268 KB)
and pmacs-gpu/fonts/OFL.txt. Font shipped as required by the SIL
Open Font License 1.1.

One finding surfaced during the move and absorbed under rule (iii)
of the framing pass (small / no structural change): the design doc
recorded JetBrains Mono as Apache 2.0; the actual license has been
OFL since the family's open-source release. Doc corrected in
docs/pmacs-gpu-design.md.

Gates: cargo fmt + cargo clippy --all-targets -D warnings clean for
the whole workspace; cargo test --lib still 1314 (pmacs main crate
untouched); m4_acceptance 83; m11_5_semantic_acceptance --features
crdt 2.

Visual confirmation pending — agent environment is headless, so
'window opens, text renders' is user-side validation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:37:26 -04:00