Skip to content

feat(render): add transactional strip rendering - #190

Merged
doodlewind merged 2 commits into
pocket-stack:mainfrom
HalfSweet:feat/esp32p4-strip-transactions
Jul 27, 2026
Merged

feat(render): add transactional strip rendering#190
doodlewind merged 2 commits into
pocket-stack:mainfrom
HalfSweet:feat/esp32p4-strip-transactions

Conversation

@HalfSweet

@HalfSweet HalfSweet commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What changed

  • add a compact RGB565 window target that preserves full-viewport raster coordinates
  • expose the last built DrawList for stable multi-strip replay
  • expose split prepare_damage, commit_damage, and abort_damage operations on the ESP32-P4 PPA renderer
  • add render_strip for full-viewport-width, tightly packed dirty strips
  • translate PPA rectangles, A8 masks, texture destinations, and software fallback writes into strip-local storage
  • retain allocation capacity while allowing the active A8 mask length to shrink with each strip

Why

Asynchronous display hosts must render and submit several reusable strips before they can advance a persistent framebuffer's damage history. Coupling history updates to render_incremental cannot represent a failed or partially submitted display transaction.

The split API keeps history unchanged until presentation succeeds, invalidates it on abort, and preserves global gradient, glyph, texture, and triangle sampling when software fallback writes into a compact strip.

Impact

Existing full-frame and incremental callers are unchanged. The new API is backend-neutral and keeps board/display ownership outside PocketJS Core.

Validation

  • cargo test --locked --manifest-path engine/core/Cargo.toml --features std — 100 passed
  • cargo test --locked --manifest-path engine/backends/esp32p4-ppa/Cargo.toml --features std — 20 passed
  • cargo check --locked --manifest-path engine/backends/esp32p4-ppa/Cargo.toml --features esp-idf
  • git diff --check

@HalfSweet
HalfSweet force-pushed the feat/esp32p4-strip-transactions branch from 8e4fb0b to a4e1547 Compare July 26, 2026 06:57
@HalfSweet
HalfSweet marked this pull request as ready for review July 26, 2026 07:02
…er op family

Rgb565WindowTarget::blend duplicated Rgb565Target::blend verbatim modulo the
local_offset translation — the exact place where the strip-equals-full-window
invariant would drift silently if one copy were ever edited alone. Extract
blend_rgb565_pixel and use it from both targets so the parity holds by
construction.

Pin that invariant with an integration suite: strip tilings (uneven, per-row,
scale 2) reassemble byte-identically to a full render across every DrawList op
family (RECT, GRAD_RECT both axes, SCISSOR, TRI, TEX_QUAD 8888, TEX_TRI,
GLYPH_RUN, SRM-eligible 5650 TEX_QUAD), through the all-software window
fallback and through SRM-over-PPA including a band split across the quad;
offset windows leave the exterior untouched, and repeated strips stay
byte-identical across mask reallocation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@doodlewind

Copy link
Copy Markdown
Collaborator

Verdict: fit to merge. Reviewed with the repo review workflow (skills/pocketjs-review-pr); one dedup + one test suite pushed as c2b187a, everything else verified as-is.

What was verified

The strip ≡ full-window invariant — empirically, past the PR's own matrix. The PR claims global gradient, glyph, texture, and triangle sampling survive the compact window, but its parity test only exercised RECT/GRAD_RECT at scale 1. I probed a 16×10 scene touching every DrawList op family (RECT, GRAD_RECT both axes, SCISSOR/SCISSOR_POP, TRI, TEX_QUAD 8888, TEX_TRI, GLYPH_RUN with a density-2 atlas, and an SRM-eligible one-to-one 5650 TEX_QUAD), with ops deliberately straddling strip boundaries:

  • uneven 3-band tiling, per-row 1px strips, and scale-2 tiling all reassemble byte-identical to a full render (all-software fallback via a declining PPA);
  • SRM-through-PPA lands in strip-local coordinates and matches the software path, including a band split across the middle of the quad (source rect advances with the band);
  • offset windows leave the exterior untouched; re-rendering the same strip after mask realloc churn is byte-identical.

7/7 pass — committed as engine/backends/esp32p4-ppa/tests/strip_parity.rs so the invariant stays pinned.

Window-target asserts are unreachable on valid input. Every rasterizer (fill_rect, grad_rect, glyph_run, tri, tex_tri, tex_quad) hard-clips to clip before writing, and render_scaled_clipped bounds SCISSOR with screen.intersect(...) — so with screen = physical window, the two Rgb565WindowTarget asserts are genuine dead-man protection, not a device-panic risk. Same reasoning holds in the backend: render_region keeps clip = screen.intersect(scissor) ⊆ region ⊆ surface, so the mask's local indices can't go negative.

Existing paths are behavior-preserving. render_incremental's refactor onto prepare_damage/commit_damage computes the same DamageTarget (target_screen already rejects any width/height that disagrees with the viewport before prepare_damage runs). Full-frame callers see surface = (0,0,w,h), making every local_physical_rect an identity. The ensure_mask capacity/len split only changes strip callers (exact-length masks); full-frame lengths are constant per renderer.

Determinism / golden architecture: no impact. engine/core changes are purely additive (current_draw_list is a genuinely new &self accessor — draw(&mut self) rebuilds, so multi-strip replay had no stable snapshot before; render_scaled_rgb565_window_over + Rgb565WindowTarget are new code off the existing paths). Rebuilt hosts/web/pocketjs.wasm from this branch (reproducibly byte-identical, 207855 bytes) and ran the full TS gate: 450 pass, 0 fail — all goldens byte-identical. No time, randomness, or map-iteration order anywhere in the diff; a DrawList snapshot replayed across strips is more deterministic-testing-friendly than re-drawing per strip.

What was pushed (c2b187a)

  • Rgb565WindowTarget::blend was a verbatim copy of Rgb565Target::blend modulo local_offset — the exact spot where strip-vs-full parity would drift silently if one copy were edited alone. Extracted blend_rgb565_pixel, used by both, so parity holds by construction.
  • The parity suite above.

Left alone, with reasons

  • local_offset's per-pixel div/mod on the fallback path: it's already the slow path behind PPA, and changing the RenderTarget offset contract is out of scope.
  • Pre-existing rustfmt drift in engine/core (e.g. anim.rs, raster.rs test module) — not this PR's.

Local gate

  • cargo test --locked engine/core100 pass; engine/backends/esp32p4-ppa --features std20 + 7 pass; cargo check --features esp-idf clean
  • bun run test (branch-built wasm, dist cleared) → 450 pass, 0 fail; bunx tsc --noEmit clean

@doodlewind
doodlewind merged commit 522bd91 into pocket-stack:main Jul 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants