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
66 changes: 66 additions & 0 deletions packages/core/src/runtime_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export function native_window_policy(request: Uint8Array): Uint8Array {
if (request[0] === 9) return nscvSemanticTree(request);
if (request[0] === 10) return nscvExtentPolicy(request);
if (request[0] === 11) return nscvTextCachePolicy(request);
if (request[0] === 12) return nscvRenderCachePolicy(request);
const data = new DataView(request.buffer, request.byteOffset, request.byteLength);
let at = 0;
const byte = (): number => {
Expand Down Expand Up @@ -1574,3 +1575,68 @@ function nscvTextCachePolicy(request: Uint8Array): Uint8Array {
for(let i=0;i<actions.length;i++)out.setUint32(16+entries.length*4+i*4,actions[i]!<0?4294967295:actions[i]!,true);
return result;
}

/** Copied render-cache keys. Native retains fingerprints, resources and GPU objects. */
function nscvRenderCachePolicy(request: Uint8Array): Uint8Array {
if (request.length < 24 || request[1]! > 5 || request[2] !== 0 || request[3] !== 0) throw new Error("invalid render cache request");
const input = new DataView(request.buffer, request.byteOffset, request.byteLength);
const current = input.getUint32(4, true), previous = input.getUint32(8, true);
const capacity = input.getUint32(12, true), actionCapacity = input.getUint32(16, true), total = current + previous;
if (total > 4294967295 || request.length !== 24 + total * 56 || input.getUint32(20, true) !== 0) throw new Error("invalid render cache shape");
for (let i = 0; i < total; i++) if (input.getUint32(24 + i * 56 + 52, true) !== 0) throw new Error("invalid render cache fact");
const indexed = request[1] === 4 && (current >= 64 || previous >= 64) && current <= 2048 && previous <= 2048;
const entries = new Uint32Array(Math.min(capacity, current));
const actions = new Uint32Array(Math.min(actionCapacity, total) * 3);
const previousHeads = new Uint32Array(indexed ? 4096 : 0), entryHeads = new Uint32Array(indexed ? 4096 : 0);
const previousNext = new Uint32Array(indexed ? previous : 0), entryNext = new Uint32Array(indexed ? Math.min(capacity, current) : 0);
if (indexed) for (let i = previous - 1; i >= 0; i--) {
const bucket = input.getUint32(24 + (current + i) * 56 + 48, true) & 4095;
previousNext[i] = previousHeads[bucket]!; previousHeads[bucket] = i + 1;
}
const equal = (a: number, b: number): boolean => {
const x = 24 + a * 56, y = 24 + b * 56;
for (let i = 0; i < 48; i++) if (request[x + i] !== request[y + i]) return false;
return true;
};
let entryCount = 0, actionCount = 0, failed = false;
const lookup = (source: number, old: boolean): number => {
if (indexed) {
const heads = old ? previousHeads : entryHeads, next = old ? previousNext : entryNext;
const bucket = input.getUint32(24 + source * 56 + 48, true) & 4095;
let match = -1;
for (let stored = heads[bucket]!; stored > 0; stored = next[stored - 1]!) {
const i = stored - 1;
if (equal(source, old ? current + i : entries[i]!) && (match < 0 || i < match)) match = i;
}
return match;
}
for (let i = 0; i < (old ? previous : entryCount); i++) if (equal(source, old ? current + i : entries[i]!)) return i;
return -1;
};
for (let i = 0; i < current; i++) {
if (lookup(i, false) >= 0) continue;
const old = lookup(i, true);
if (actionCount >= actionCapacity) { failed = true; break; }
actions[actionCount * 3] = old < 0 ? 0 : 1;
actions[actionCount * 3 + 1] = i; actions[actionCount * 3 + 2] = old < 0 ? 4294967295 : old; actionCount++;
// Native writes the action first, including when entry capacity then fails.
if (entryCount >= capacity) { failed = true; break; }
entries[entryCount] = i;
if (indexed) {
const bucket = input.getUint32(24 + i * 56 + 48, true) & 4095;
entryNext[entryCount] = entryHeads[bucket]!; entryHeads[bucket] = entryCount + 1;
}
entryCount++;
}
if (!failed) for (let i = 0; i < previous; i++) {
const source = current + i;
if (lookup(source, false) >= 0) continue;
if (actionCount >= actionCapacity) { failed = true; break; }
actions[actionCount * 3] = 2; actions[actionCount * 3 + 1] = source; actions[actionCount * 3 + 2] = i; actionCount++;
}
const result = new Uint8Array(16 + entryCount * 4 + actionCount * 12), output = new DataView(result.buffer);
output.setUint32(0, entryCount, true); output.setUint32(4, actionCount, true); output.setUint32(8, failed ? 1 : 0, true);
for (let i = 0; i < entryCount; i++) output.setUint32(16 + i * 4, entries[i]!, true);
for (let i = 0; i < actionCount * 3; i++) output.setUint32(16 + entryCount * 4 + i * 4, actions[i]!, true);
return result;
}
62 changes: 62 additions & 0 deletions packages/core/test/render_cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import assert from "node:assert/strict";
import test from "node:test";
import { native_window_policy } from "../src/runtime_policy.ts";

type Fact = { key: bigint; position?: bigint; id?: bigint | null; kind?: number; image?: bigint; font?: bigint; bucket?: number };
function request(family: number, current: Fact[], previous: Fact[], capacity = 100, actionCapacity = 100): Uint8Array {
const r = new Uint8Array(24 + (current.length + previous.length) * 56), w = new DataView(r.buffer);
r[0] = 12; r[1] = family;
[current.length, previous.length, capacity, actionCapacity].forEach((n, i) => w.setUint32(4 + i * 4, n, true));
[...current, ...previous].forEach((f, i) => {
const at = 24 + i * 56;
w.setUint32(at, f.kind ?? 0, true); w.setUint32(at + 4, f.id == null ? 0 : 1, true);
w.setBigUint64(at + 8, f.id ?? 0n, true); w.setBigUint64(at + 16, f.position ?? 0n, true);
w.setBigUint64(at + 24, f.image ?? 0n, true); w.setBigUint64(at + 32, f.font ?? 0n, true);
w.setBigUint64(at + 40, f.key, true); w.setUint32(at + 48, f.bucket ?? Number(f.key & 4095n), true);
});
return r;
}
function decode(r: Uint8Array) {
const w = new DataView(r.buffer), n = w.getUint32(0, true), m = w.getUint32(4, true);
return { failed: w.getUint32(8, true), entries: Array.from({ length: n }, (_, i) => w.getUint32(16 + i * 4, true)), actions: Array.from({ length: m }, (_, i) => Array.from({ length: 3 }, (_, j) => w.getUint32(16 + n * 4 + i * 12 + j * 4, true))) };
}
for (let family = 0; family < 6; family++) {
test(`render cache ${family} preserves ordering, first matches and every unused previous duplicate`, () => {
assert.deepEqual(decode(native_window_policy(request(family, [{ key: 2n }, { key: 2n }, { key: 3n }], [{ key: 2n }, { key: 2n }, { key: 4n }, { key: 4n }]))), { failed: 0, entries: [0, 2], actions: [[1, 0, 0], [0, 2, 4294967295], [2, 5, 2], [2, 6, 3]] });
});
test(`render cache ${family} writes actions before entries on capacity failures`, () => {
const c = [{ key: 1n }, { key: 2n }];
assert.deepEqual(decode(native_window_policy(request(family, c, [], 0, 9))), { failed: 1, entries: [], actions: [[0, 0, 4294967295]] });
assert.deepEqual(decode(native_window_policy(request(family, c, [], 1, 9))), { failed: 1, entries: [0], actions: [[0, 0, 4294967295], [0, 1, 4294967295]] });
assert.deepEqual(decode(native_window_policy(request(family, c, [], 9, 1))), { failed: 1, entries: [0], actions: [[0, 0, 4294967295]] });
assert.deepEqual(decode(native_window_policy(request(family, [], c, 0, 1))), { failed: 1, entries: [], actions: [[2, 0, 0]] });
});
test(`render cache ${family} compares every complete key field without uint64 rounding`, () => {
const base: Fact = { key: 9007199254740993n, id: 0n, position: 9007199254740993n, image: 18446744073709551615n, font: 9007199254740997n };
const facts = [base, { ...base, key: base.key + 1n }, { ...base, id: null }, { ...base, id: 1n }, { ...base, position: base.position! + 1n }, { ...base, image: base.image! - 1n }, { ...base, font: base.font! + 1n }, { ...base, kind: 1 }];
const out = decode(native_window_policy(request(family, facts, [base])));
assert.deepEqual(out.entries, facts.map((_, i) => i)); assert.equal(out.actions[0]![0], 1); assert.ok(out.actions.slice(1).every(a => a[0] === 0));
});
}
test("generic render resources preserve indexed collisions, threshold and oversized linear parity", () => {
for (const count of [63, 64, 2048, 2049]) {
const c = Array.from({ length: count }, (_, i) => ({ key: BigInt(i >> 1), bucket: 17 }));
const out = decode(native_window_policy(request(4, c, c, count, count * 2)));
assert.equal(out.entries.length, Math.ceil(count / 2)); assert.deepEqual(out.actions.map(a => a[2]), out.entries);
}
const previous = Array.from({ length: 64 }, (_, i) => ({ key: BigInt(i >> 1), bucket: 1 }));
assert.deepEqual(decode(native_window_policy(request(4, [], previous, 0, 64))).actions.map(a => a[2]), previous.map((_, i) => i));
});
test("render cache copied results survive later family calls and sliced wire views", () => {
const r = request(4, [{ key: 1n }], []), padded = new Uint8Array(r.length + 9); padded.set(r, 4);
const output = native_window_policy(padded.subarray(4, 4 + r.length)), before = output.slice();
for (let family = 0; family < 6; family++) native_window_policy(request(family, [], []));
assert.deepEqual(output, before); assert.equal(decode(output).entries.length, 1);
});
test("render cache wire refuses malformed shape, reserved fields and discriminators", () => {
const r = request(4, [{ key: 1n }], []);
for (let i = 1; i < r.length; i++) assert.throws(() => native_window_policy(r.subarray(0, i)));
assert.throws(() => native_window_policy(new Uint8Array([...r, 0])));
for (const at of [1, 2, 3, 20, 24 + 52]) { const bad = r.slice(); bad[at] = 255; assert.throws(() => native_window_policy(bad)); }
const bad = r.slice(); new DataView(bad.buffer).setUint32(4, 4294967295, true); assert.throws(() => native_window_policy(bad));
});
22 changes: 16 additions & 6 deletions src/primitives/canvas/frame.zig
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const render_cache = @import("render_cache_policy.zig");
const std = @import("std");
const geometry = @import("geometry");
const canvas = @import("root.zig");
Expand Down Expand Up @@ -454,6 +455,7 @@ pub const CanvasFrame = struct {

pub const CanvasFrameOptions = struct {
text_cache_policy: ?*const fn ([]const u8, []u8) usize = null,
render_cache_policy: ?*const fn ([]const u8, []u8) usize = null,
frame_index: u64 = 0,
timestamp_ns: u64 = 0,
surface_size: geometry.SizeF = .{},
Expand Down Expand Up @@ -516,11 +518,14 @@ pub fn buildCanvasFrame(previous: ?DisplayList, next: DisplayList, options: Canv
var render_plan = try next.renderPlan(storage.render_commands);
const render_override_dirty_bounds = renderOverrideDirtyBounds(render_plan.commands, options.previous_render_overrides, options.render_overrides);
render_plan.bounds = applyRenderOverrides(storage.render_commands[0..render_plan.commandCount()], options.render_overrides);
var cache_workspace = render_cache.Workspace.forFrame(options.render_cache_policy, storage, options);
defer if (cache_workspace) |*workspace| workspace.deinit();
const batch_plan = try render_plan.batchPlan(storage.render_batches);
const pipeline_cache_plan = if (storage.pipeline_cache_entries.len == 0 and storage.pipeline_cache_actions.len == 0)
RenderPipelineCachePlan{}
else
try batch_plan.cachePlan(
try batch_plan.cachePlanWithWorkspace(
if (cache_workspace) |*workspace| workspace else null,
options.previous_pipeline_cache,
options.frame_index,
storage.pipeline_cache_entries,
Expand All @@ -533,7 +538,8 @@ pub fn buildCanvasFrame(previous: ?DisplayList, next: DisplayList, options: Canv
const path_geometry_cache_plan = if (storage.path_geometry_cache_entries.len == 0 and storage.path_geometry_cache_actions.len == 0)
RenderPathGeometryCachePlan{}
else
try path_geometry_plan.cachePlan(
try path_geometry_plan.cachePlanWithWorkspace(
if (cache_workspace) |*workspace| workspace else null,
options.previous_path_geometry_cache,
options.frame_index,
storage.path_geometry_cache_entries,
Expand All @@ -546,7 +552,8 @@ pub fn buildCanvasFrame(previous: ?DisplayList, next: DisplayList, options: Canv
const image_cache_plan = if (storage.image_cache_entries.len == 0 and storage.image_cache_actions.len == 0)
RenderImageCachePlan{}
else
try image_plan.cachePlan(
try image_plan.cachePlanWithWorkspace(
if (cache_workspace) |*workspace| workspace else null,
options.previous_image_cache,
options.frame_index,
storage.image_cache_entries,
Expand All @@ -559,14 +566,16 @@ pub fn buildCanvasFrame(previous: ?DisplayList, next: DisplayList, options: Canv
const layer_cache_plan = if (storage.layer_cache_entries.len == 0 and storage.layer_cache_actions.len == 0)
RenderLayerCachePlan{}
else
try layer_plan.cachePlan(
try layer_plan.cachePlanWithWorkspace(
if (cache_workspace) |*workspace| workspace else null,
options.previous_layer_cache,
options.frame_index,
storage.layer_cache_entries,
storage.layer_cache_actions,
);
const resource_plan = try next.resourcePlan(storage.resources);
const resource_cache_plan = try resource_plan.cachePlan(
const resource_cache_plan = try resource_plan.cachePlanWithWorkspace(
if (cache_workspace) |*workspace| workspace else null,
options.previous_resource_cache,
options.frame_index,
storage.resource_cache_entries,
Expand All @@ -579,7 +588,8 @@ pub fn buildCanvasFrame(previous: ?DisplayList, next: DisplayList, options: Canv
const visual_effect_cache_plan = if (storage.visual_effect_cache_entries.len == 0 and storage.visual_effect_cache_actions.len == 0)
VisualEffectCachePlan{}
else
try visual_effect_plan.cachePlan(
try visual_effect_plan.cachePlanWithWorkspace(
if (cache_workspace) |*workspace| workspace else null,
options.previous_visual_effect_cache,
options.frame_index,
storage.visual_effect_cache_entries,
Expand Down
14 changes: 14 additions & 0 deletions src/primitives/canvas/render.zig
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const compiled_cache = @import("render_cache_policy.zig");
const std = @import("std");
const geometry = @import("geometry");
const canvas = @import("root.zig");
Expand Down Expand Up @@ -535,6 +536,11 @@ pub const RenderBatchPlan = struct {
var planner = RenderPipelineCachePlanner.init(entries, actions);
return planner.build(self, previous, frame_index);
}
pub fn cachePlanWithWorkspace(self: RenderBatchPlan, workspace: ?*compiled_cache.Workspace, previous: []const RenderPipelineCacheEntry, frame_index: u64, entries: []RenderPipelineCacheEntry, actions: []RenderPipelineCacheAction) Error!RenderPipelineCachePlan {
var planner = RenderPipelineCachePlanner.init(entries, actions);
if (workspace) |owner| return planner.buildCompiled(self, previous, frame_index, owner);
return planner.build(self, previous, frame_index);
}
};

pub const RenderBatchPlanner = struct {
Expand Down Expand Up @@ -648,6 +654,10 @@ pub const RenderPipelineCachePlanner = struct {
self.action_len = 0;
}

pub fn buildCompiled(self: *RenderPipelineCachePlanner, plan: RenderBatchPlan, previous: []const RenderPipelineCacheEntry, frame_index: u64, workspace: *compiled_cache.Workspace) Error!RenderPipelineCachePlan {
return compiled_cache.build(.pipeline, RenderPipelineCachePlan, self, plan.batches, previous, frame_index, workspace, renderPipelineCacheKey, null, error.RenderPipelineCacheListFull);
}

pub fn build(self: *RenderPipelineCachePlanner, batch_plan: RenderBatchPlan, previous: []const RenderPipelineCacheEntry, frame_index: u64) Error!RenderPipelineCachePlan {
self.reset();
for (batch_plan.batches, 0..) |batch, batch_index| {
Expand Down Expand Up @@ -745,3 +755,7 @@ fn unionOptionalBounds(a: ?geometry.RectF, b: ?geometry.RectF) ?geometry.RectF {
fn affinesEqual(a: Affine, b: Affine) bool {
return equality_model.affinesEqual(a, b);
}

fn renderPipelineCacheKey(batch: RenderBatch) RenderPipelineKind {
return batch.pipeline;
}
Loading
Loading