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
25 changes: 25 additions & 0 deletions docs/backend/native.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,31 @@ The addon exposes a small set of functions at the N-API boundary:
- `engineGetCaps(engineId)` -- Returns a `TerminalCaps` object describing
detected terminal capabilities (color mode, mouse, paste, cursor shape, etc.).

## Native Resource Lifecycle

Drawlist execution in Zireael tracks persistent resources (images, glyph caches,
and link intern tables) through `zr_dl_resources_t`.

- `zr_dl_resources_init` -- Initializes empty owned resource storage.
- `zr_dl_resources_release` -- Releases owned resource memory and resets lengths.
- `zr_dl_resources_swap` -- Constant-time ownership swap between two stores.
- `zr_dl_resources_clone` -- Deep clone (duplicates owned bytes/arrays).
- `zr_dl_resources_clone_shallow` -- Shallow clone (borrows existing backing
storage and metadata references without duplicating payload).

`zr_dl_preflight_resources` validates and stages resource effects before execute.
Preflight uses shallow snapshots so stage resources can borrow baseline data
without duplicate allocations during validation. `zr_dl_execute` then applies
the already-validated command stream against that staged state. On successful
commit the engine swaps staged resources into the live set; on failure it
retains the pre-commit set and releases stage state.

Use deep clone when an independent lifetime is required (for example, caching
or cross-frame ownership transfer). Use shallow clone only for bounded
preflight/execute windows where the source lifetime is guaranteed to outlive the
borrow. Always pair `init`/`release` on owned stores and prefer `swap` for
commit paths to avoid duplicate frees and partial ownership transfer bugs.

### User Events

- `enginePostUserEvent(engineId, tag, payload)` -- Posts a custom user event
Expand Down
20 changes: 10 additions & 10 deletions examples/gallery/src/scenes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,22 +415,22 @@ export function themedOverrideShowcase(): VNode {
{
colors: {
bg: {
base: { r: 238, g: 242, b: 247 },
elevated: { r: 232, g: 237, b: 244 },
subtle: { r: 220, g: 228, b: 238 },
base: rgb(238, 242, 247),
elevated: rgb(232, 237, 244),
subtle: rgb(220, 228, 238),
},
fg: {
primary: { r: 28, g: 36, b: 49 },
secondary: { r: 63, g: 78, b: 97 },
muted: { r: 99, g: 113, b: 131 },
inverse: { r: 245, g: 248, b: 252 },
primary: rgb(28, 36, 49),
secondary: rgb(63, 78, 97),
muted: rgb(99, 113, 131),
inverse: rgb(245, 248, 252),
},
accent: {
primary: { r: 64, g: 120, b: 255 },
primary: rgb(64, 120, 255),
},
border: {
subtle: { r: 187, g: 198, b: 213 },
default: { r: 157, g: 173, b: 193 },
subtle: rgb(187, 198, 213),
default: rgb(157, 173, 193),
},
},
},
Expand Down
109 changes: 109 additions & 0 deletions packages/bench/src/profile-packed-style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Packed-style profiler: validates packed style merge counters and render timing.
*
* Usage:
* REZI_PERF=1 REZI_PERF_DETAIL=1 npx tsx src/profile-packed-style.ts
*/

import {
type TextStyle,
type VNode,
createApp,
perfReset,
perfSnapshot,
rgb,
ui,
} from "@rezi-ui/core";
import { BenchBackend } from "./backends.js";

const ROWS = 1200;
const WARMUP_ITERS = 20;
const MEASURE_ITERS = 80;

function makeRowStyle(index: number, tick: number): TextStyle {
return {
...((index & 1) === 0 ? { bold: true } : { dim: true }),
...(index % 3 === 0
? {
fg: rgb(
(index * 13 + tick * 7) & 0xff,
(index * 17 + tick * 5) & 0xff,
(index * 19 + tick * 3) & 0xff,
),
}
: {}),
...(index % 5 === 0
? {
bg: rgb(
(index * 11 + tick * 2) & 0xff,
(index * 7 + tick * 13) & 0xff,
(index * 3 + tick * 29) & 0xff,
),
}
: {}),
...(index % 7 === 0 ? { underline: true } : {}),
...(index % 11 === 0 ? { inverse: true } : {}),
};
}

function packedStyleTree(tick: number): VNode {
const rows: VNode[] = [];
for (let i = 0; i < ROWS; i++) {
rows.push(
ui.text(`row-${String(i).padStart(4, "0")} tick=${tick}`, { style: makeRowStyle(i, tick) }),
);
}
return ui.column({ p: 0, gap: 0 }, rows);
}

async function main() {
const backend = new BenchBackend(160, ROWS + 8);
type State = { tick: number };
const app = createApp<State>({ backend, initialState: { tick: 0 } });
app.view((state) => packedStyleTree(state.tick));

const initialFrame = backend.waitForFrame();
await app.start();
await initialFrame;

for (let i = 0; i < WARMUP_ITERS; i++) {
const frameP = backend.waitForFrame();
app.update((s) => ({ tick: s.tick + 1 }));
await frameP;
}

perfReset();

for (let i = 0; i < MEASURE_ITERS; i++) {
const frameP = backend.waitForFrame();
app.update((s) => ({ tick: s.tick + 1 }));
await frameP;
}

const snap = perfSnapshot();
const counters = snap.counters as {
style_merges_performed?: number;
style_objects_created?: number;
packRgb_calls?: number;
};
const merges = counters.style_merges_performed ?? 0;
const styleObjects = counters.style_objects_created ?? 0;
const packRgbCalls = counters.packRgb_calls ?? 0;
const renderAvgUs = ((snap.phases.render?.avg ?? 0) * 1000).toFixed(0);
const drawlistAvgUs = ((snap.phases.drawlist_build?.avg ?? 0) * 1000).toFixed(0);
const reusePct = merges > 0 ? (((merges - styleObjects) / merges) * 100).toFixed(2) : "0.00";

console.log("\n=== Packed style profile ===\n");
console.log(`iters: ${String(MEASURE_ITERS)}`);
console.log(`style_merges_performed: ${String(merges)}`);
console.log(`style_objects_created: ${String(styleObjects)}`);
console.log(`packRgb_calls: ${String(packRgbCalls)}`);
console.log(`style object reuse: ${reusePct}%`);
console.log(`render avg: ${renderAvgUs}µs`);
console.log(`drawlist_build avg: ${drawlistAvgUs}µs`);

await app.stop();
app.dispose();
}

main().catch(console.error);
4 changes: 4 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@
"types": "./dist/ui.d.ts",
"default": "./dist/ui.js"
},
"./pipeline": {
"types": "./dist/pipeline.d.ts",
"default": "./dist/pipeline.js"
},
"./widgets": {
"types": "./dist/widgets/index.d.ts",
"default": "./dist/widgets/index.js"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert, createRng, describe, test } from "@rezi-ui/testkit";
import { type VNode, createDrawlistBuilderV1 } from "../../index.js";
import { type VNode, createDrawlistBuilder } from "../../index.js";
import { layout } from "../../layout/layout.js";
import { renderToDrawlist } from "../../renderer/renderToDrawlist.js";
import { commitVNodeTree } from "../../runtime/commit.js";
Expand Down Expand Up @@ -164,7 +164,7 @@ function runTreeFuzz(seed: number, profile: TreeProfile): void {
continue;
}

const builder = createDrawlistBuilderV1();
const builder = createDrawlistBuilder();
renderToDrawlist({
tree: commitRes.value.root,
layout: layoutRes.value,
Expand Down
18 changes: 16 additions & 2 deletions packages/core/src/__tests__/stress/stress.large-trees.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import { assert, describe, test } from "@rezi-ui/testkit";
import { WidgetRenderer } from "../../app/widgetRenderer.js";
import type { RuntimeBackend } from "../../backend.js";
import type { DrawlistBuildResult, DrawlistBuilderV1 } from "../../drawlist/index.js";
import type { DrawlistBuildResult, DrawlistBuilder } from "../../drawlist/index.js";
import type { DrawlistTextRunSegment } from "../../drawlist/types.js";
import type { ZrevEvent } from "../../events.js";
import type { VNode } from "../../index.js";
Expand Down Expand Up @@ -53,7 +53,7 @@ function nowMs(): number {
return perf ? perf.now() : Date.now();
}

class CountingBuilder implements DrawlistBuilderV1 {
class CountingBuilder implements DrawlistBuilder {
private opCount = 0;
private lastBuiltCount = 0;

Expand Down Expand Up @@ -95,6 +95,20 @@ class CountingBuilder implements DrawlistBuilderV1 {

drawTextRun(_x: number, _y: number, _blobIndex: number): void {}

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 {
this.lastBuiltCount = this.opCount;
return { ok: true, bytes: new Uint8Array([this.opCount & 0xff]) };
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ export const ZR_ENGINE_ABI_PATCH = 0;

/**
* Binary format version pins.
* Drawlist is pre-alpha and currently pinned to v1.
*/
export const ZR_DRAWLIST_VERSION_V1 = 1;
export const ZR_DRAWLIST_VERSION = ZR_DRAWLIST_VERSION_V1;
export const ZR_EVENT_BATCH_VERSION_V1 = 1;

// =============================================================================
Expand Down
31 changes: 17 additions & 14 deletions packages/core/src/animation/__tests__/interpolate.easing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,32 +33,35 @@ describe("animation/interpolate", () => {
});

test("interpolateRgb interpolates channel values", () => {
assert.deepEqual(interpolateRgb({ r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 }, 0.5), {
r: 128,
g: 128,
b: 128,
});
assert.equal(
interpolateRgb((0 << 16) | (0 << 8) | 0, (255 << 16) | (255 << 8) | 255, 0.5),
(128 << 16) | (128 << 8) | 128,
);
});

test("interpolateRgb returns endpoints at t=0 and t=1", () => {
const from = { r: 3, g: 40, b: 200 };
const to = { r: 250, g: 100, b: 0 };
const from = (3 << 16) | (40 << 8) | 200;
const to = (250 << 16) | (100 << 8) | 0;
assert.deepEqual(interpolateRgb(from, to, 0), from);
assert.deepEqual(interpolateRgb(from, to, 1), to);
});

test("interpolateRgb clamps output channels to byte range integers", () => {
assert.deepEqual(
interpolateRgb({ r: -10, g: 400.4, b: Number.NaN }, { r: -10, g: 400.4, b: Number.NaN }, 1),
{ r: 0, g: 255, b: 0 },
test("interpolateRgb rounds channel interpolation to byte integers", () => {
assert.equal(
interpolateRgb((0 << 16) | (0 << 8) | 0, (1 << 16) | (1 << 8) | 1, 0.5),
(1 << 16) | (1 << 8) | 1,
);
assert.equal(
interpolateRgb((0 << 16) | (0 << 8) | 0, (2 << 16) | (2 << 8) | 2, 0.5),
(1 << 16) | (1 << 8) | 1,
);
});

test("interpolateRgbArray returns the requested number of steps", () => {
const steps = interpolateRgbArray({ r: 0, g: 0, b: 0 }, { r: 255, g: 0, b: 0 }, 4);
const steps = interpolateRgbArray((0 << 16) | (0 << 8) | 0, (255 << 16) | (0 << 8) | 0, 4);
assert.equal(steps.length, 4);
assert.deepEqual(steps[0], { r: 0, g: 0, b: 0 });
assert.deepEqual(steps[3], { r: 255, g: 0, b: 0 });
assert.deepEqual(steps[0], (0 << 16) | (0 << 8) | 0);
assert.deepEqual(steps[3], (255 << 16) | (0 << 8) | 0);
});
});

Expand Down
18 changes: 9 additions & 9 deletions packages/core/src/animation/interpolate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* packages/core/src/animation/interpolate.ts — Primitive interpolation helpers.
*/

import type { Rgb } from "../widgets/style.js";
import { type Rgb24, rgb, rgbB, rgbG, rgbR } from "../widgets/style.js";

/** Clamp a number into [0, 1]. */
export function clamp01(value: number): number {
Expand Down Expand Up @@ -32,20 +32,20 @@ function clampRgbChannel(channel: number): number {
}

/** Linear interpolation between two RGB colors. */
export function interpolateRgb(from: Rgb, to: Rgb, t: number): Rgb {
return Object.freeze({
r: clampRgbChannel(interpolateNumber(from.r, to.r, t)),
g: clampRgbChannel(interpolateNumber(from.g, to.g, t)),
b: clampRgbChannel(interpolateNumber(from.b, to.b, t)),
});
export function interpolateRgb(from: Rgb24, to: Rgb24, t: number): Rgb24 {
return rgb(
clampRgbChannel(interpolateNumber(rgbR(from), rgbR(to), t)),
clampRgbChannel(interpolateNumber(rgbG(from), rgbG(to), t)),
clampRgbChannel(interpolateNumber(rgbB(from), rgbB(to), t)),
);
}

/** Generate `steps` RGB samples between two colors (inclusive endpoints). */
export function interpolateRgbArray(from: Rgb, to: Rgb, steps: number): readonly Rgb[] {
export function interpolateRgbArray(from: Rgb24, to: Rgb24, steps: number): readonly Rgb24[] {
const count = Math.max(0, Math.trunc(steps));
if (count <= 0) return Object.freeze([]);
if (count === 1) return Object.freeze([interpolateRgb(from, to, 0)]);
const samples: Rgb[] = new Array(count);
const samples: Rgb24[] = new Array(count);
for (let i = 0; i < count; i++) {
samples[i] = interpolateRgb(from, to, i / (count - 1));
}
Expand Down
Loading
Loading