render: move the cell painter into felis-render-core behind a display list #173

Merged
natsukium merged 2 commits from render/core-painter into main 2026-10-18 20:08:28 +09:00
Owner

Depends on #172 (and #171 under it); review the last two commits.

Last of four PRs extracting the wgpu-free half of felis-render-wgpu into felis-render-core. After this, a CPU-rendered client reuses how a cell looks (colors, cursor, selection, decorations, overlays, clipping) instead of re-deriving it.

97cb66c7 — paint into a display list (painter still in wgpu)

  • Core display_list: RowPaint { rects, glyphs, decos } in physical pixels, linear RGBA. GlyphDraw carries its source in atlas pixels (src_min_px/src_max_px, crop_src_x/y), like image_layout's ImageDraw carries image pixels.
  • GlyphSlot switches from normalized uv to atlas_px + size_px; AtlasView::sheet_side gives the encoder each sheet's side.
  • wgpu gpu_instances.rs: the POD instance types plus encode(RowPaint) → InstanceBuffers; RowCache encodes each repainted row once.

00080118 — move the painter

  • instances.rs → core painter.rs (98% rename); Boundary API made pub (extend_row, resolved, push_scrollbar_lane, cursor(), CursorPaint::cell(), cursor_rect_px, clip_extent_at, CURSOR_MARKER_PX).
  • Renames: extend_instances → extend_screen, extend_*_instances → extend_*, clip_cell_instances_above → clip_cells_above.
  • Docs: overview.md records the display list as data next to renderer_effect/image_layout (a per-draw renderer trait rejected: the core would dictate call order, the GPU would re-buffer, RowCache needs per-row data); workspace.md and pipeline.md.

Evidence

  • #170's golden fixtures pass untouched (not regenerated).
  • Proptest: pixel crop then divide matches the old normalize-then-mul_add arithmetic within 1e-6 at sides 256–2048, bit-exact at power-of-two sides.
  • paint_frame (full runs, alternating): full ≈0%, one dirty row +2.6%, cursor move +3.2%.
  • 111 painter tests moved and assert on the display list; just check green; sol review PASS.
  • Not checked in a real window (headless only): the golden and offscreen frame tests are the evidence.
Depends on #172 (and #171 under it); review the last two commits. Last of four PRs extracting the wgpu-free half of `felis-render-wgpu` into `felis-render-core`. After this, a CPU-rendered client reuses how a cell looks (colors, cursor, selection, decorations, overlays, clipping) instead of re-deriving it. **97cb66c7 — paint into a display list (painter still in wgpu)** - Core `display_list`: `RowPaint { rects, glyphs, decos }` in physical pixels, linear RGBA. `GlyphDraw` carries its source in atlas pixels (`src_min_px`/`src_max_px`, `crop_src_x/y`), like `image_layout`'s `ImageDraw` carries image pixels. - `GlyphSlot` switches from normalized uv to `atlas_px` + `size_px`; `AtlasView::sheet_side` gives the encoder each sheet's side. - wgpu `gpu_instances.rs`: the POD instance types plus `encode(RowPaint) → InstanceBuffers`; `RowCache` encodes each repainted row once. **00080118 — move the painter** - `instances.rs` → core `painter.rs` (98% rename); Boundary API made `pub` (`extend_row`, `resolved`, `push_scrollbar_lane`, `cursor()`, `CursorPaint::cell()`, `cursor_rect_px`, `clip_extent_at`, `CURSOR_MARKER_PX`). - Renames: `extend_instances` → `extend_screen`, `extend_*_instances` → `extend_*`, `clip_cell_instances_above` → `clip_cells_above`. - Docs: overview.md records the display list as data next to `renderer_effect`/`image_layout` (a per-draw renderer trait rejected: the core would dictate call order, the GPU would re-buffer, `RowCache` needs per-row data); workspace.md and pipeline.md. **Evidence** - #170's golden fixtures pass untouched (not regenerated). - Proptest: pixel crop then divide matches the old normalize-then-`mul_add` arithmetic within 1e-6 at sides 256–2048, bit-exact at power-of-two sides. - `paint_frame` (full runs, alternating): full ≈0%, one dirty row +2.6%, cursor move +3.2%. - 111 painter tests moved and assert on the display list; `just check` green; sol review PASS. - Not checked in a real window (headless only): the golden and offscreen frame tests are the evidence.
Half of felis-render-wgpu never names a wgpu type, yet a client that does
not use wgpu has to re-derive it, and the browser client's hand port of the
box-drawing rasterizer already lags the native one. A crate without wgpu
or winit is the seam a CPU backend and other clients can reuse instead.
The palette, box drawing, shelf atlas, selection, overlay and image-source
types move first, unchanged; the pinned instance bytes stay identical.

The no-wgpu/no-winit rule is a deny.toml wrappers entry rather than a
cargo-tree test inside the crate: such a test reaches above its own crate
root and would ship in the package once the crate is published.

Assisted-by: Claude Code
docs: describe what felis-render-core holds today
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 10s
windows / windows docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 46s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 50s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 2m40s
pr / nix flake check (pull_request) Successful in 1m18s
windows / cargo nextest (Windows) (pull_request) Successful in 2m47s
windows / frontend smoke (Windows) (pull_request) Successful in 1m3s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m58s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 31s
windows / cargo clippy (Windows cross) (pull_request) Successful in 28s
203dbea197
The crate-boundary record claimed the core already spares a CPU
backend from re-deriving how a cell looks, but the cell painter is
still in felis-render-wgpu; name the pieces the core holds instead.

Assisted-by: Claude Code
render, shaping, docs: move the glyph index into felis-render-core
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 10s
windows / windows docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 46s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 51s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 2m38s
windows / cargo nextest (Windows) (pull_request) Successful in 3m0s
pr / nix flake check (pull_request) Successful in 1m51s
windows / frontend smoke (Windows) (pull_request) Successful in 56s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m38s
pr / wire schema is compatible with the base (pull_request) Successful in 4s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 28s
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
6a1a169253
A backend without wgpu still needs the whole glyph index: shaping memo,
rasterization, slot table, shelf packing and recycling. A CPU backend
keeps its glyphs in an atlas image in RAM and drains the same pending
uploads into it that the wgpu backend writes into a texture, so the index
moves to the core whole and the only per-backend part is that drain loop.
GlyphCache stays in felis-render-wgpu as the texture and bind-group
wrapper around it; the module takes a new name so the moved file keeps
its history.

Glyph slots keep normalized uv for now: the painter still writes slot
coordinates straight into the fg instances, so switching to atlas pixels
waits for an encoder that divides by the atlas side.

Assisted-by: Claude Code
The cell painter wrote GPU instance records directly, so a backend
without wgpu could not reuse it. It now emits rectangles, glyph draws
and decorations in physical pixels, and felis-render-wgpu encodes each
repainted row into its instance records once before caching it.

Glyph sources are atlas pixels rather than normalized uv, as image
draw sources already are: slicing and clipping scale a pixel rect, and
the encoder divides by the sheet side once. The encoded bytes stay
identical at power-of-two sides (division by them is exact) and within
1e-6 uv at any other side the atlas accepts; a proptest pins both
against the uv arithmetic it replaces.

Assisted-by: Claude Code
render, client, docs: move the cell painter into felis-render-core
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 9s
windows / windows docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 46s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 50s
pr / nix flake check (pull_request) Successful in 38s
windows / cargo nextest (Windows) (pull_request) Successful in 2m42s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m35s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
windows / frontend smoke (Windows) (pull_request) Successful in 1m7s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 26s
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 6m7s
0008011859
The painter is where a terminal's cell rules live: reverse video,
cursor styles, selection, the bidi warning, faint, decorations, the
overlays and bottom bars, the chrome clip. With it in the wgpu crate,
a CPU backend or another client still had to re-derive all of them,
and a port that misses one renders the session differently. Now that
it emits a renderer-neutral display list, it moves to the core with
its tests; felis-render-wgpu keeps the instance records, the encoder
and the row cache.

The display list is data rather than a trait the painter calls per
draw: the painter would dictate call order, the wgpu backend would
only re-buffer the calls, and the row cache needs a row's output as a
value it keeps across frames.

Assisted-by: Claude Code
natsukium force-pushed render/core-painter from 0008011859
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 9s
windows / windows docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 46s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 50s
pr / nix flake check (pull_request) Successful in 38s
windows / cargo nextest (Windows) (pull_request) Successful in 2m42s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m35s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
windows / frontend smoke (Windows) (pull_request) Successful in 1m7s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 26s
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
darwin / build felis (aarch64-darwin) (pull_request) Successful in 6m7s
to 3056186e62
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m9s
windows / detect relevant changes (pull_request) Successful in 10s
windows / windows docs-only no-op (pull_request) Has been skipped
bench / instruction-count regression gate (pull_request) Successful in 46s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 50s
pr / nix flake check (pull_request) Successful in 8s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m36s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 27s
windows / cargo clippy (Windows cross) (pull_request) Successful in 15s
windows / cargo nextest (Windows) (pull_request) Successful in 2m23s
windows / frontend smoke (Windows) (pull_request) Successful in 35s
2026-10-18 19:49:15 +09:00
Compare
natsukium deleted branch render/core-painter 2026-10-18 20:08:28 +09:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
felis-terminal/felis!173
No description provided.