Skip to content

EPIC 7: retained sub-display-lists via per-instance render packets#220

Merged
RtlZeroMemory merged 6 commits intomainfrom
epic7-retained-render-packets
Feb 26, 2026
Merged

EPIC 7: retained sub-display-lists via per-instance render packets#220
RtlZeroMemory merged 6 commits intomainfrom
epic7-retained-render-packets

Conversation

@RtlZeroMemory
Copy link
Owner

@RtlZeroMemory RtlZeroMemory commented Feb 26, 2026

Summary

  • add an internal RenderPacket model for local-coordinate draw ops (FILL_RECT, DRAW_TEXT_SLICE, DRAW_TEXT_RUN, PUSH_CLIP, POP_CLIP, DRAW_CANVAS, DRAW_IMAGE) plus packet resources
  • extend RuntimeInstance with renderPacketKey and renderPacket and preserve packet state across reusable instance commits
  • add packet capture/replay in renderTree() for cacheable basic widgets so layout-only movement can re-emit retained ops at a new origin
  • compute stable packet keys from widget kind + vnode identity/props refs + theme/style hashes + focus/pressed flags + tick for spinner
  • stop forcing selfDirty during render-time descendant propagation and mark layout-only dirtiness as dirty without selfDirty to unlock packet reuse on motion
  • add renderer tests for retained packet reuse/rebuild and update RuntimeInstance test fixtures for new fields

Validation

  • npm run build
  • node --test --test-concurrency=1 packages/core/dist/renderer/__tests__/renderPackets.test.js packages/core/dist/__tests__/integration/integration.reflow.test.js packages/core/dist/__tests__/integration/integration.resize.test.js
  • npm test ❌ (pre-existing failures in packages/node worker integration tests in this environment)
  • node --test --test-concurrency=1 packages/core/dist/renderer/__tests__/render.golden.test.js ❌ with 6 pre-existing golden mismatches; reproduced on a pristine origin/main worktree (/tmp/rezi-baseline-epic7)

Summary by CodeRabbit

  • New Features

    • Packet-based render cache: runtime nodes can record and replay rendered draw operations to improve rendering.
  • Performance

    • Reuse retained render packets for layout-only moves to reduce redraw work.
    • Avoid marking leaf-only nodes as self-dirty to cut unnecessary renders.
  • Bug Fixes

    • More precise dirty-state propagation to prevent spurious rebuilds.
  • Tests

    • Added unit and end-to-end tests for packet retention, reuse, rebuilds, and drawlist alignment behavior.

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

Adds a render-packet system (types, recorder, key computation, emission), threads renderPacketKey/renderPacket through RuntimeInstance and commit, integrates packet reuse into renderTree for selected widgets, refines damage-tracking to optionally avoid marking selfDirty, and updates tests and drawlist utilities.

Changes

Cohort / File(s) Summary
Render packet types & runtime shape
packages/core/src/runtime/renderPacket.ts, packages/core/src/runtime/commit.ts
Add RenderPacket types; extend RuntimeInstance with renderPacketKey: number and `renderPacket: RenderPacket
Render packet recorder & key/emit logic
packages/core/src/renderer/renderToDrawlist/renderPackets.ts
Add RenderPacketRecorder plus computeRenderPacketKey and emitRenderPacket for recording ops/resources, deterministic packet keys, and emitting packets to a DrawlistBuilder.
Renderer integration
packages/core/src/renderer/renderToDrawlist/renderTree.ts, packages/core/src/renderer/renderToDrawlist/widgets/containers.ts
Integrate packet-based rendering path for sparkline/barChart/miniChart: compute key, reuse via emitRenderPacket when matching, otherwise record/store packet; avoid unconditional selfDirty marking for forced subtree renders on leaves.
Damage tracking
packages/core/src/app/widgetRenderer/damageTracking.ts
Change propagateDirtyFromPredicate signature to add markSelfDirty: boolean = true; compute predicateDirty separately and only set selfDirty when markSelfDirty is true. Update markLayoutDirtyNodes to call with false.
Commit & runtime plumbing
packages/core/src/runtime/commit.ts
Thread render-packet metadata through commit flows and root/container creation; import render packet type for RuntimeInstance fields.
Renderer tests & packet tests
packages/core/src/renderer/__tests__/renderPackets.test.ts, packages/core/src/renderer/__tests__/persistentBlobKeys.test.ts
Add end-to-end render-packet retention tests and scaffolding (RecordingBuilder, scene/render helpers); update test node mocks to include renderPacketKey/renderPacket.
Layout tests updates
packages/core/src/layout/__tests__/*
Update multiple layout test factories to add renderPacketKey and renderPacket fields to test RuntimeInstance shapes and typings.
Drawlist utilities & tests
packages/core/src/drawlist/builderBase.ts, packages/core/src/drawlist/__tests__/builder.align4.test.ts
Rework align4 implementation to explicitly handle non-positive inputs and add tests covering small, near-INT32_MAX, and negative cases.
New runtime->renderer imports
packages/core/src/renderer/renderToDrawlist/renderTree.ts
Import and use RenderPacketRecorder, computeRenderPacketKey, emitRenderPacket to support packet path.

Sequence Diagram

sequenceDiagram
    participant RT as renderTree
    participant Node as RuntimeInstance
    participant RPR as RenderPacketRecorder
    participant DLB as DrawlistBuilder

    rect rgba(100,200,100,0.5)
    Note over RT: Render widget (sparkline / barChart / miniChart)
    RT->>RT: computeRenderPacketKey(node, theme, style, layout, state)
    RT->>Node: read node.renderPacketKey & node.renderPacket
    alt cache hit (key matches & packet exists)
        RT->>DLB: emitRenderPacket(packet, originX, originY)
        DLB->>DLB: replay recorded ops -> draw
    else cache miss or invalid
        RT->>RPR: new RenderPacketRecorder(target=builder, origin)
        RT->>RPR: render widget into recorder (record ops/resources)
        RPR->>RPR: buildPacket() -> packet
        RPR-->>RT: packet
        RT->>Node: store renderPacket & renderPacketKey
        RPR->>DLB: emit recorded ops -> draw
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nibbled bytes and hopped around,

Packets folded, snug and sound,
Sparklines skip with cached delight,
Fewer redraws beneath moonlight,
Rabbit cheers — builds take flight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and specifically summarizes the main change: introducing a render packet system for caching and reusing sub-display-lists per instance.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch epic7-retained-render-packets

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/core/src/app/widgetRenderer/damageTracking.ts (1)

256-258: Consider renaming markedSelfDirty for clarity.

The variable name markedSelfDirty is now slightly misleading since it represents whether the predicate considers the node dirty, not whether selfDirty was actually set (which now depends on the markSelfDirty parameter). A name like predicateDirty or isDirtyByPredicate would better reflect its purpose.

♻️ Suggested rename for clarity
-    const markedSelfDirty = isNodeDirty(node);
-    if (markSelfDirty && markedSelfDirty) node.selfDirty = true;
-    let dirty = node.dirty || markedSelfDirty;
+    const predicateDirty = isNodeDirty(node);
+    if (markSelfDirty && predicateDirty) node.selfDirty = true;
+    let dirty = node.dirty || predicateDirty;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/app/widgetRenderer/damageTracking.ts` around lines 256 -
258, Rename the local variable markedSelfDirty to a clearer name (e.g.,
predicateDirty or isDirtyByPredicate) to reflect that it holds the result of the
isNodeDirty(node) predicate rather than whether node.selfDirty was set; update
its occurrences in the same block (where it is assigned from isNodeDirty(node),
used in the conditional that sets node.selfDirty conditioned on markSelfDirty,
and when computing let dirty = node.dirty || markedSelfDirty) so the logic
remains the same but the variable name matches its meaning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/core/src/app/widgetRenderer/damageTracking.ts`:
- Around line 256-258: Rename the local variable markedSelfDirty to a clearer
name (e.g., predicateDirty or isDirtyByPredicate) to reflect that it holds the
result of the isNodeDirty(node) predicate rather than whether node.selfDirty was
set; update its occurrences in the same block (where it is assigned from
isNodeDirty(node), used in the conditional that sets node.selfDirty conditioned
on markSelfDirty, and when computing let dirty = node.dirty || markedSelfDirty)
so the logic remains the same but the variable name matches its meaning.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a55167 and 0a2c262.

📒 Files selected for processing (12)
  • packages/core/src/app/widgetRenderer/damageTracking.ts
  • packages/core/src/layout/__tests__/incrementalLayout.phase3.test.ts
  • packages/core/src/layout/__tests__/layout.perf-invariants.test.ts
  • packages/core/src/layout/__tests__/layout.stability-coverage.test.ts
  • packages/core/src/layout/__tests__/layout.stability-signature.test.ts
  • packages/core/src/renderer/__tests__/persistentBlobKeys.test.ts
  • packages/core/src/renderer/__tests__/renderPackets.test.ts
  • packages/core/src/renderer/renderToDrawlist/renderPackets.ts
  • packages/core/src/renderer/renderToDrawlist/renderTree.ts
  • packages/core/src/renderer/renderToDrawlist/widgets/containers.ts
  • packages/core/src/runtime/commit.ts
  • packages/core/src/runtime/renderPacket.ts
💤 Files with no reviewable changes (1)
  • packages/core/src/renderer/renderToDrawlist/widgets/containers.ts

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0a2c2623a5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@RtlZeroMemory
Copy link
Owner Author

Addressed review feedback in commit ec8ce1d:

  • included rect.w/rect.h in renderPacketKey so retained packets rebuild when widget size changes
  • restored recursive forced-subtree propagation by setting selfDirty on forced descendants that have children (prevents nested incremental stale renders)
  • renamed markedSelfDirty to predicateDirty in damage tracking for clarity

Validation re-run:

  • npm run build
  • node --test --test-concurrency=1 packages/core/dist/renderer/__tests__/renderPackets.test.js packages/core/dist/__tests__/integration/integration.reflow.test.js packages/core/dist/__tests__/integration/integration.resize.test.js
  • Result: pass 29 / fail 0

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/core/src/renderer/renderToDrawlist/renderTree.ts (1)

296-362: Please attach full-suite verification evidence for this renderer hot path.

This path changes packet reuse/rebuild behavior in renderTree(), so I’d like explicit node scripts/run-tests.mjs evidence (or a clear baseline of pre-existing failures vs new failures) in the PR thread before merge.

As per coding guidelines: “After any code change to commit, reconcile, layout, or renderer files, run the full test suite with node scripts/run-tests.mjs before committing—subtle regressions are common.”
Based on learnings: “Changes to drawlist generation code are a danger zone requiring extra care and full test suite verification.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/renderer/renderToDrawlist/renderTree.ts` around lines 296 -
362, Run the full test suite and attach verification evidence for the changed
renderer hot path: execute node scripts/run-tests.mjs and include the complete
test output (or a clear before/after baseline) in the PR thread to show there
are no regressions caused by the packet reuse/rebuild changes around
renderTree(), computeRenderPacketKey(), RenderPacketRecorder,
renderBasicWidget(), and the node.renderPacket/node.renderPacketKey logic;
ensure the report highlights any failing tests, stack traces, and timestamps so
reviewers can confirm the drawlist generation changes are safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/renderer/renderToDrawlist/renderPackets.ts`:
- Around line 389-391: The reset() method currently only calls
this.target.reset() but must also clear the recorder-local packet state to avoid
stale data when reusing the recorder; update renderPackets' reset() to also
reset/clear the recorder's ops, resources, maps, and valid state (the fields
used by buildPacket()) so that ops/resources/maps/valid are emptied/reset
alongside this.target; locate the reset() method and add explicit clears for
those recorder-local members referenced by buildPacket() so subsequent
buildPacket() calls start from a clean state.

---

Nitpick comments:
In `@packages/core/src/renderer/renderToDrawlist/renderTree.ts`:
- Around line 296-362: Run the full test suite and attach verification evidence
for the changed renderer hot path: execute node scripts/run-tests.mjs and
include the complete test output (or a clear before/after baseline) in the PR
thread to show there are no regressions caused by the packet reuse/rebuild
changes around renderTree(), computeRenderPacketKey(), RenderPacketRecorder,
renderBasicWidget(), and the node.renderPacket/node.renderPacketKey logic;
ensure the report highlights any failing tests, stack traces, and timestamps so
reviewers can confirm the drawlist generation changes are safe.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a2c262 and ec8ce1d.

📒 Files selected for processing (4)
  • packages/core/src/app/widgetRenderer/damageTracking.ts
  • packages/core/src/renderer/renderToDrawlist/renderPackets.ts
  • packages/core/src/renderer/renderToDrawlist/renderTree.ts
  • packages/core/src/renderer/renderToDrawlist/widgets/containers.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/core/src/renderer/renderToDrawlist/widgets/containers.ts
  • packages/core/src/app/widgetRenderer/damageTracking.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/core/src/renderer/renderToDrawlist/renderPackets.ts (1)

296-305: Minor inconsistency: addTextRunBlob doesn't store in resources array.

Unlike addBlob which stores bytes in both blobResourceById and resources, addTextRunBlob only stores in textRunByBlobId. This is intentional since text run segments are stored directly in the op rather than as a resource blob, but this asymmetry could be confusing during future maintenance.

Consider adding a brief comment explaining why text runs are handled differently from binary blobs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/renderer/renderToDrawlist/renderPackets.ts` around lines
296 - 305, The addTextRunBlob method stores copied text segments only in
textRunByBlobId (unlike addBlob which also records bytes in blobResourceById and
resources), which can confuse future readers; add a concise inline comment
inside addTextRunBlob explaining that text run segments are embedded directly in
ops (so they are not stored as binary resources) and reference the contrast with
addBlob/blobResourceById/resources to make the asymmetry explicit for
maintainers.
packages/core/src/renderer/renderToDrawlist/renderTree.ts (1)

311-311: Optional: Consider pooling RenderPacketRecorder instances.

A new RenderPacketRecorder is allocated for each widget that needs packet recording. While this only occurs when content changes (selfDirty), high-frequency updates (e.g., spinners, progress bars) could benefit from recorder pooling. This is a minor optimization opportunity for a future iteration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/renderer/renderToDrawlist/renderTree.ts` at line 311, The
code creates a new RenderPacketRecorder per widget (const recorder = new
RenderPacketRecorder(builder, rect.x, rect.y)), which can be pooled to reduce
allocations for high-frequency updates; implement a simple pooling utility
(e.g., RenderPacketRecorderPool with acquire(builder, x, y) and
release(recorder)) and replace direct construction in renderTree (where recorder
is created) with pool.acquire(...) and ensure recorder.reset/clear is called on
release so state isn't leaked, and call pool.release(recorder) after use.
packages/core/src/renderer/__tests__/renderPackets.test.ts (1)

90-127: Consider adding a helper comment for the long parameter list.

The renderTree call has many undefined parameters which is acceptable for test scaffolding but could be confusing for future maintainers. Consider adding a brief comment listing which parameters are being intentionally omitted, or extracting default test options.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/renderer/__tests__/renderPackets.test.ts` around lines 90 -
127, The renderTree call in renderScene passes many trailing undefineds which is
confusing; update renderScene to either add a concise inline comment next to the
renderTree invocation listing which parameters are intentionally omitted (by
name, matching renderTree’s signature) or extract a small test helper/wrapper
(e.g., renderTreeForTest or defaultRenderTreeArgs) that supplies the common
default/undefined values so the call becomes renderTree(builder, ...,
defaultRenderTreeArgs); reference renderScene and renderTree when making the
change so future readers can see which parameters are intentionally left unset.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/core/src/renderer/__tests__/renderPackets.test.ts`:
- Around line 90-127: The renderTree call in renderScene passes many trailing
undefineds which is confusing; update renderScene to either add a concise inline
comment next to the renderTree invocation listing which parameters are
intentionally omitted (by name, matching renderTree’s signature) or extract a
small test helper/wrapper (e.g., renderTreeForTest or defaultRenderTreeArgs)
that supplies the common default/undefined values so the call becomes
renderTree(builder, ..., defaultRenderTreeArgs); reference renderScene and
renderTree when making the change so future readers can see which parameters are
intentionally left unset.

In `@packages/core/src/renderer/renderToDrawlist/renderPackets.ts`:
- Around line 296-305: The addTextRunBlob method stores copied text segments
only in textRunByBlobId (unlike addBlob which also records bytes in
blobResourceById and resources), which can confuse future readers; add a concise
inline comment inside addTextRunBlob explaining that text run segments are
embedded directly in ops (so they are not stored as binary resources) and
reference the contrast with addBlob/blobResourceById/resources to make the
asymmetry explicit for maintainers.

In `@packages/core/src/renderer/renderToDrawlist/renderTree.ts`:
- Line 311: The code creates a new RenderPacketRecorder per widget (const
recorder = new RenderPacketRecorder(builder, rect.x, rect.y)), which can be
pooled to reduce allocations for high-frequency updates; implement a simple
pooling utility (e.g., RenderPacketRecorderPool with acquire(builder, x, y) and
release(recorder)) and replace direct construction in renderTree (where recorder
is created) with pool.acquire(...) and ensure recorder.reset/clear is called on
release so state isn't leaked, and call pool.release(recorder) after use.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb79c28 and b4eade8.

📒 Files selected for processing (5)
  • packages/core/src/app/widgetRenderer/damageTracking.ts
  • packages/core/src/renderer/__tests__/renderPackets.test.ts
  • packages/core/src/renderer/renderToDrawlist/renderPackets.ts
  • packages/core/src/renderer/renderToDrawlist/renderTree.ts
  • packages/core/src/runtime/commit.ts

@RtlZeroMemory RtlZeroMemory merged commit dd264a0 into main Feb 26, 2026
7 of 8 checks passed
@RtlZeroMemory RtlZeroMemory deleted the epic7-retained-render-packets branch February 28, 2026 04:18
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.

1 participant