Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/core/src/app/widgetRenderer/damageTracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export function propagateDirtyFromPredicate(
isNodeDirty: (node: RuntimeInstance) => boolean,
pooledRuntimeStack: RuntimeInstance[],
pooledPrevRuntimeStack: RuntimeInstance[],
markSelfDirty = true,
): void {
pooledRuntimeStack.length = 0;
pooledPrevRuntimeStack.length = 0;
Expand All @@ -252,9 +253,9 @@ export function propagateDirtyFromPredicate(
for (let i = pooledPrevRuntimeStack.length - 1; i >= 0; i--) {
const node = pooledPrevRuntimeStack[i];
if (!node) continue;
const markedSelfDirty = isNodeDirty(node);
if (markedSelfDirty) node.selfDirty = true;
let dirty = node.dirty || markedSelfDirty;
const predicateDirty = isNodeDirty(node);
if (markSelfDirty && predicateDirty) node.selfDirty = true;
let dirty = node.dirty || predicateDirty;
for (const child of node.children) {
if (child.dirty) {
dirty = true;
Expand All @@ -278,6 +279,7 @@ export function markLayoutDirtyNodes(params: MarkLayoutDirtyNodesParams): void {
},
params.pooledRuntimeStack,
params.pooledPrevRuntimeStack,
false,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ function runtimeNode(
children,
dirty: false,
selfDirty: false,
renderPacketKey: 0,
renderPacket: null,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ function runtimeNode(
children: Object.freeze([...children]),
dirty: false,
selfDirty: false,
renderPacketKey: 0,
renderPacket: null,
};
}

Expand Down Expand Up @@ -130,11 +132,20 @@ function createCountingRuntimeFactory(): CountingRuntimeFactory {
children: readonly RuntimeInstance[] = [],
): RuntimeInstance => {
const frozenChildren = Object.freeze([...children]);
const node = { instanceId, vnode, dirty: false, selfDirty: false } as {
const node = {
instanceId,
vnode,
dirty: false,
selfDirty: false,
renderPacketKey: 0,
renderPacket: null,
} as {
instanceId: InstanceId;
vnode: VNode;
dirty: boolean;
selfDirty: boolean;
renderPacketKey: number;
renderPacket: null;
children?: readonly RuntimeInstance[];
};
Object.defineProperty(node, "children", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ function runtimeNode(
children: Object.freeze([...children]),
dirty: false,
selfDirty: false,
renderPacketKey: 0,
renderPacket: null,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ function runtimeNode(
children: Object.freeze([...children]),
dirty: false,
selfDirty: false,
renderPacketKey: 0,
renderPacket: null,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ describe("renderer blob usage", () => {
children: [],
dirty: false,
selfDirty: false,
renderPacketKey: 0,
renderPacket: null,
};

const imageNode = {
Expand All @@ -118,6 +120,8 @@ describe("renderer blob usage", () => {
children: [],
dirty: false,
selfDirty: false,
renderPacketKey: 0,
renderPacket: null,
};

renderCanvasWidgets(
Expand Down
188 changes: 188 additions & 0 deletions packages/core/src/renderer/__tests__/renderPackets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { assert, describe, test } from "@rezi-ui/testkit";
import type { DrawlistBuildResult, DrawlistBuilder } from "../../drawlist/index.js";
import type { DrawlistTextRunSegment } from "../../drawlist/types.js";
import { ui } from "../../index.js";
import { type LayoutTree, layout } from "../../layout/layout.js";
import { type RuntimeInstance, commitVNodeTree } from "../../runtime/commit.js";
import { createInstanceIdAllocator } from "../../runtime/instance.js";
import { defaultTheme } from "../../theme/defaultTheme.js";
import { indexIdRects, indexLayoutRects } from "../renderToDrawlist/indices.js";
import { renderTree } from "../renderToDrawlist/renderTree.js";
import { DEFAULT_BASE_STYLE } from "../renderToDrawlist/textStyle.js";

type RecordedOp =
| Readonly<{ kind: "fillRect"; x: number; y: number; w: number; h: number }>
| Readonly<{ kind: "drawText"; x: number; y: number; text: string }>
| Readonly<{ kind: "drawTextRun"; x: number; y: number; blobIndex: number }>
| Readonly<{ kind: "pushClip"; x: number; y: number; w: number; h: number }>
| Readonly<{ kind: "popClip" }>;

class RecordingBuilder implements DrawlistBuilder {
readonly ops: RecordedOp[] = [];
private nextBlob = 1;

clear(): void {}
clearTo(_cols: number, _rows: number): void {}

fillRect(x: number, y: number, w: number, h: number): void {
this.ops.push({ kind: "fillRect", x, y, w, h });
}

drawText(x: number, y: number, text: string): void {
this.ops.push({ kind: "drawText", x, y, text });
}

pushClip(x: number, y: number, w: number, h: number): void {
this.ops.push({ kind: "pushClip", x, y, w, h });
}

popClip(): void {
this.ops.push({ kind: "popClip" });
}

addBlob(_bytes: Uint8Array): number | null {
const id = this.nextBlob;
this.nextBlob += 1;
return id;
}

addTextRunBlob(_segments: readonly DrawlistTextRunSegment[]): number | null {
const id = this.nextBlob;
this.nextBlob += 1;
return id;
}

drawTextRun(x: number, y: number, blobIndex: number): void {
this.ops.push({ kind: "drawTextRun", x, y, blobIndex });
}

setCursor(..._args: Parameters<DrawlistBuilder["setCursor"]>): void {}
hideCursor(): void {}
setLink(..._args: Parameters<DrawlistBuilder["setLink"]>): void {}
drawCanvas(..._args: Parameters<DrawlistBuilder["drawCanvas"]>): void {}
drawImage(..._args: Parameters<DrawlistBuilder["drawImage"]>): void {}

buildInto(_dst: Uint8Array): DrawlistBuildResult {
return this.build();
}

build(): DrawlistBuildResult {
return { ok: true, bytes: new Uint8Array([0]) };
}

reset(): void {}
}

type Scene = Readonly<{
tree: RuntimeInstance;
layoutTree: LayoutTree;
}>;

function createScene(root: RuntimeInstance, x: number, y: number): Scene {
const laidOut = layout(root.vnode, x, y, 80, 24, "column");
assert.equal(laidOut.ok, true, "layout should succeed");
if (!laidOut.ok) {
throw new Error("layout should succeed");
}
return { tree: root, layoutTree: laidOut.value };
}

function renderScene(scene: Scene): RecordingBuilder {
const layoutIndex = indexLayoutRects(scene.layoutTree, scene.tree);
const idRectIndex = indexIdRects(scene.tree, layoutIndex);
const builder = new RecordingBuilder();
renderTree(
builder,
Object.freeze({ focusedId: null }),
scene.layoutTree,
idRectIndex,
Object.freeze({ cols: 80, rows: 24 }),
defaultTheme,
0,
DEFAULT_BASE_STYLE,
scene.tree,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
null,
undefined,
undefined,
null,
);
return builder;
}

function firstDrawText(ops: readonly RecordedOp[]): Extract<RecordedOp, { kind: "drawText" }> {
const op = ops.find((entry): entry is Extract<RecordedOp, { kind: "drawText" }> => {
return entry.kind === "drawText";
});
if (op === undefined) {
throw new Error("expected at least one drawText op");
}
return op;
}

describe("render packet retention", () => {
test("reuses retained packet for layout-only dirty text node", () => {
const committed = commitVNodeTree(null, ui.text("retained"), {
allocator: createInstanceIdAllocator(1),
});
assert.equal(committed.ok, true, "commit should succeed");
if (!committed.ok) return;

const root = committed.value.root;
const firstFrame = renderScene(createScene(root, 0, 0));
const firstPacket = root.renderPacket;
const firstKey = root.renderPacketKey;
assert.ok(firstPacket !== null, "expected packet after first render");
assert.notEqual(firstKey, 0, "expected non-zero packet key");

root.dirty = true;
root.selfDirty = false;
const secondFrame = renderScene(createScene(root, 7, 3));

assert.equal(root.renderPacket, firstPacket, "layout-only frame should reuse cached packet");
assert.equal(
root.renderPacketKey,
firstKey,
"packet key should stay stable across layout move",
);

const drawText = firstDrawText(secondFrame.ops);
assert.equal(drawText.x, 7);
assert.equal(drawText.y, 3);
assert.equal(drawText.text, "retained");
});

test("selfDirty rebuilds packet", () => {
const committed = commitVNodeTree(null, ui.text("rebuild"), {
allocator: createInstanceIdAllocator(1),
});
assert.equal(committed.ok, true, "commit should succeed");
if (!committed.ok) return;

const root = committed.value.root;
renderScene(createScene(root, 0, 0));
const firstPacket = root.renderPacket;
assert.ok(firstPacket !== null, "expected packet after first render");

root.dirty = true;
root.selfDirty = true;
renderScene(createScene(root, 0, 0));
assert.notEqual(root.renderPacket, firstPacket, "selfDirty should rebuild retained packet");
});
});
Loading
Loading