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
40 changes: 40 additions & 0 deletions docs/guide/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,46 @@ Conservative fallback:
- `button` `id`-only changes do not trigger relayout; label/padding changes do.
- If any committed node is outside signature coverage, relayout is forced conservatively.

## ZRDL Binary Format Invariants

These invariants describe current builder + engine behavior for ZRDL bytes.

### 4-byte alignment rules

- Header size is fixed at 64 bytes; when `cmdCount > 0`, `cmdOffset` is 64.
- `totalSize`, `cmdBytes`, `stringsBytesLen`, and `blobsBytesLen` are 4-byte aligned.
- Section offsets (`cmdOffset`, `stringsSpanOffset`, `stringsBytesOffset`, `blobsSpanOffset`, `blobsBytesOffset`) are 4-byte aligned.
- Command records start on 4-byte boundaries. Command `size` must be 4-byte aligned; any command padding bytes are zeroed.
- `addBlob(...)` requires `bytes.byteLength % 4 === 0`. String entries are not individually aligned; the strings section as a whole is padded to 4-byte alignment.

### String interning guarantees

- Interning is by exact string value within one builder epoch (from construction/reset to the next `reset()`).
- Repeated equal strings across `drawText(...)` and `addTextRunBlob(...)` reuse one `string_index` and one string-table entry.
- New string indices are assigned in first-seen order.
- `reset()` clears interning state (and command/blob/string sections), so indices are rebuilt on the next frame.

### Limit and overflow behavior

- Builder caps (`maxDrawlistBytes`, `maxCmdCount`, `maxStrings`, `maxStringBytes`, `maxBlobs`, `maxBlobBytes`) fail with `ZRDL_TOO_LARGE` when exceeded.
- Builder failures are sticky: first error is retained and later commands no-op until `reset()`.
- Engine validation enforces runtime limits (`dl_max_total_bytes`, `dl_max_cmds`, `dl_max_strings`, `dl_max_blobs`, `dl_max_clip_depth`, `dl_max_text_run_segments`) and returns `ZR_ERR_LIMIT` when exceeded.
- Offset/length arithmetic that overflows during validation is treated as invalid format (`ZR_ERR_FORMAT`), not wraparound.

### Encoded string cache semantics (including reset)

- Encoded string caching is optional: `encodedStringCacheCap = 0` disables it.
- On a cache miss with caching enabled, if cache size is already `>= cap`, the cache is cleared, then the new entry is inserted.
- `reset()` does not clear the encoded-string cache. It clears per-drawlist state only, so cached encodings can persist across frames while the same builder instance is reused.
- v1 caches only strings with `text.length <= 96`; v2 has no length filter for cache eligibility.

### v1 vs v2 differences (cursor command)

- v1 and v2 share the same header shape and opcodes `1..6`.
- v2 sets header `version = 2` and adds `OP_SET_CURSOR` (opcode `7`), encoded as a 20-byte command (`8` byte header + `12` byte payload).
- `SET_CURSOR` payload fields are `x:int32`, `y:int32`, `shape:u8`, `visible:u8`, `blink:u8`, `reserved0:u8`. `x` and `y` allow `-1` (leave unchanged).
- v1 command validation/execution does not allow `OP_SET_CURSOR`; it is rejected as unsupported.

## Borders

`ui.box` can draw a border around its content:
Expand Down
291 changes: 291 additions & 0 deletions packages/core/src/drawlist/__tests__/builder.alignment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import { assert, describe, test } from "@rezi-ui/testkit";
import { createDrawlistBuilderV1 } from "../builder_v1.js";
import type { DrawlistBuildResult } from "../types.js";

const HEADER = {
TOTAL_SIZE: 12,
CMD_OFFSET: 16,
CMD_BYTES: 20,
CMD_COUNT: 24,
STRINGS_SPAN_OFFSET: 28,
STRINGS_COUNT: 32,
STRINGS_BYTES_OFFSET: 36,
STRINGS_BYTES_LEN: 40,
BLOBS_SPAN_OFFSET: 44,
BLOBS_COUNT: 48,
BLOBS_BYTES_OFFSET: 52,
BLOBS_BYTES_LEN: 56,
SIZE: 64,
} as const;

const CMD = {
SIZE: 4,
HEADER_SIZE: 8,
} as const;

const SPAN_SIZE = 8;

type ParsedHeader = Readonly<{
totalSize: number;
cmdOffset: number;
cmdBytes: number;
cmdCount: number;
stringsSpanOffset: number;
stringsCount: number;
stringsBytesOffset: number;
stringsBytesLen: number;
blobsSpanOffset: number;
blobsCount: number;
blobsBytesOffset: number;
blobsBytesLen: number;
}>;

function align4(n: number): number {
return (n + 3) & ~3;
}

function toView(bytes: Uint8Array): DataView {
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
}

function parseHeader(bytes: Uint8Array): ParsedHeader {
const dv = toView(bytes);
return {
totalSize: dv.getUint32(HEADER.TOTAL_SIZE, true),
cmdOffset: dv.getUint32(HEADER.CMD_OFFSET, true),
cmdBytes: dv.getUint32(HEADER.CMD_BYTES, true),
cmdCount: dv.getUint32(HEADER.CMD_COUNT, true),
stringsSpanOffset: dv.getUint32(HEADER.STRINGS_SPAN_OFFSET, true),
stringsCount: dv.getUint32(HEADER.STRINGS_COUNT, true),
stringsBytesOffset: dv.getUint32(HEADER.STRINGS_BYTES_OFFSET, true),
stringsBytesLen: dv.getUint32(HEADER.STRINGS_BYTES_LEN, true),
blobsSpanOffset: dv.getUint32(HEADER.BLOBS_SPAN_OFFSET, true),
blobsCount: dv.getUint32(HEADER.BLOBS_COUNT, true),
blobsBytesOffset: dv.getUint32(HEADER.BLOBS_BYTES_OFFSET, true),
blobsBytesLen: dv.getUint32(HEADER.BLOBS_BYTES_LEN, true),
};
}

function expectOk(result: DrawlistBuildResult): Uint8Array {
assert.equal(result.ok, true);
if (!result.ok) throw new Error("expected build() to succeed");
return result.bytes;
}

function readStringSpan(dv: DataView, stringsSpanOffset: number, index: number): { off: number; len: number } {
const spanOff = stringsSpanOffset + index * SPAN_SIZE;
return {
off: dv.getUint32(spanOff, true),
len: dv.getUint32(spanOff + 4, true),
};
}

function commandStarts(bytes: Uint8Array, h: ParsedHeader): readonly number[] {
const dv = toView(bytes);
if (h.cmdCount === 0) return [];

let cursor = h.cmdOffset;
const starts: number[] = [];
for (let i = 0; i < h.cmdCount; i++) {
starts.push(cursor);
const size = dv.getUint32(cursor + CMD.SIZE, true);
assert.equal(size >= CMD.HEADER_SIZE, true, `command ${i} has invalid size`);
cursor += align4(size);
Comment on lines +91 to +93

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Assert command size alignment when walking command stream

This helper advances with align4(size) but never asserts that size itself is 4-byte aligned, so a malformed command header with an unaligned size can still pass these tests even though the runtime validator rejects such streams (ch.size & 3u is checked in packages/native/vendor/zireael/src/core/zr_drawlist.c). Because this suite is explicitly documenting alignment invariants, it should fail when a command advertises a non-aligned size instead of normalizing it during traversal.

Useful? React with 👍 / 👎.

}
assert.equal(cursor, h.cmdOffset + h.cmdBytes);
return starts;
}

describe("DrawlistBuilderV1 - alignment and padding", () => {
test("empty drawlist has aligned total size and zero section offsets", () => {
const bytes = expectOk(createDrawlistBuilderV1().build());
const h = parseHeader(bytes);

assert.equal(h.totalSize, HEADER.SIZE);
assert.equal((h.totalSize & 3) === 0, true);
assert.equal(h.cmdOffset, 0);
assert.equal(h.cmdBytes, 0);
assert.equal(h.cmdCount, 0);
assert.equal(h.stringsSpanOffset, 0);
assert.equal(h.stringsBytesOffset, 0);
assert.equal(h.blobsSpanOffset, 0);
assert.equal(h.blobsBytesOffset, 0);
});

test("near-empty clear drawlist keeps command start and section layout aligned", () => {
const b = createDrawlistBuilderV1();
b.clear();
const bytes = expectOk(b.build());
const h = parseHeader(bytes);

assert.equal(h.cmdOffset, HEADER.SIZE);
assert.equal((h.cmdOffset & 3) === 0, true);
assert.equal((h.cmdBytes & 3) === 0, true);
assert.equal(h.cmdCount, 1);
assert.equal(h.stringsCount, 0);
assert.equal(h.blobsCount, 0);
});

test("all command starts are 4-byte aligned in a mixed stream", () => {
const b = createDrawlistBuilderV1();
b.clear();
b.fillRect(0, 0, 3, 2);
b.drawText(1, 1, "abc");
b.pushClip(0, 0, 3, 2);
b.popClip();
const bytes = expectOk(b.build());
const h = parseHeader(bytes);
const starts = commandStarts(bytes, h);

assert.equal(starts.length, h.cmdCount);
for (const start of starts) {
assert.equal((start & 3) === 0, true);
}
});

test("walking command sizes lands exactly on cmdOffset + cmdBytes", () => {
const b = createDrawlistBuilderV1();
const blobIndex = b.addBlob(new Uint8Array([1, 2, 3, 4]));
assert.equal(blobIndex, 0);
b.clear();
b.drawText(0, 0, "x");
b.drawTextRun(2, 1, 0);
const bytes = expectOk(b.build());
const h = parseHeader(bytes);
const starts = commandStarts(bytes, h);

assert.equal(starts.length, 3);
assert.equal(starts[0], HEADER.SIZE);
});

test("section offsets are aligned and ordered when strings and blobs exist", () => {
const b = createDrawlistBuilderV1();
b.drawText(0, 0, "abc");
const blobIndex = b.addBlob(new Uint8Array([9, 8, 7, 6]));
assert.equal(blobIndex, 0);
b.drawTextRun(1, 0, 0);
const bytes = expectOk(b.build());
const h = parseHeader(bytes);

assert.equal((h.cmdOffset & 3) === 0, true);
assert.equal((h.stringsSpanOffset & 3) === 0, true);
assert.equal((h.stringsBytesOffset & 3) === 0, true);
assert.equal((h.blobsSpanOffset & 3) === 0, true);
assert.equal((h.blobsBytesOffset & 3) === 0, true);

assert.equal(h.stringsSpanOffset, HEADER.SIZE + h.cmdBytes);
assert.equal(h.stringsBytesOffset, h.stringsSpanOffset + h.stringsCount * SPAN_SIZE);
assert.equal(h.blobsSpanOffset, h.stringsBytesOffset + h.stringsBytesLen);
assert.equal(h.blobsBytesOffset, h.blobsSpanOffset + h.blobsCount * SPAN_SIZE);
});

test("odd-length text: 1-byte string gets 3 zero padding bytes", () => {
const b = createDrawlistBuilderV1();
b.drawText(0, 0, "a");
const bytes = expectOk(b.build());
const h = parseHeader(bytes);
const dv = toView(bytes);
const span = readStringSpan(dv, h.stringsSpanOffset, 0);

assert.equal(span.off, 0);
assert.equal(span.len, 1);
assert.equal(h.stringsBytesLen, 4);
assert.equal(bytes[h.stringsBytesOffset], 0x61);
assert.equal(bytes[h.stringsBytesOffset + 1], 0);
assert.equal(bytes[h.stringsBytesOffset + 2], 0);
assert.equal(bytes[h.stringsBytesOffset + 3], 0);
});

test("odd-length text: 2-byte string gets 2 zero padding bytes", () => {
const b = createDrawlistBuilderV1();
b.drawText(0, 0, "ab");
const bytes = expectOk(b.build());
const h = parseHeader(bytes);
const dv = toView(bytes);
const span = readStringSpan(dv, h.stringsSpanOffset, 0);

assert.equal(span.off, 0);
assert.equal(span.len, 2);
assert.equal(h.stringsBytesLen, 4);
assert.equal(bytes[h.stringsBytesOffset], 0x61);
assert.equal(bytes[h.stringsBytesOffset + 1], 0x62);
assert.equal(bytes[h.stringsBytesOffset + 2], 0);
assert.equal(bytes[h.stringsBytesOffset + 3], 0);
});

test("odd-length text: 3-byte string gets 1 zero padding byte", () => {
const b = createDrawlistBuilderV1();
b.drawText(0, 0, "abc");
const bytes = expectOk(b.build());
const h = parseHeader(bytes);
const dv = toView(bytes);
const span = readStringSpan(dv, h.stringsSpanOffset, 0);

assert.equal(span.off, 0);
assert.equal(span.len, 3);
assert.equal(h.stringsBytesLen, 4);
assert.equal(bytes[h.stringsBytesOffset], 0x61);
assert.equal(bytes[h.stringsBytesOffset + 1], 0x62);
assert.equal(bytes[h.stringsBytesOffset + 2], 0x63);
assert.equal(bytes[h.stringsBytesOffset + 3], 0);
});

test("empty string still has aligned string section with zero raw bytes", () => {
const b = createDrawlistBuilderV1();
b.drawText(0, 0, "");
const bytes = expectOk(b.build());
const h = parseHeader(bytes);
const dv = toView(bytes);
const span = readStringSpan(dv, h.stringsSpanOffset, 0);

assert.equal(h.stringsCount, 1);
assert.equal((h.stringsSpanOffset & 3) === 0, true);
assert.equal((h.stringsBytesOffset & 3) === 0, true);
assert.equal(h.stringsBytesOffset, h.stringsSpanOffset + SPAN_SIZE);
assert.equal(span.off, 0);
assert.equal(span.len, 0);
assert.equal(h.stringsBytesLen, 0);
});

test("multiple odd-length strings keep contiguous raw spans and aligned tail padding", () => {
const b = createDrawlistBuilderV1();
b.drawText(0, 0, "a");
b.drawText(0, 1, "bb");
b.drawText(0, 2, "ccc");
const bytes = expectOk(b.build());
const h = parseHeader(bytes);
const dv = toView(bytes);
const s0 = readStringSpan(dv, h.stringsSpanOffset, 0);
const s1 = readStringSpan(dv, h.stringsSpanOffset, 1);
const s2 = readStringSpan(dv, h.stringsSpanOffset, 2);

assert.equal(s0.off, 0);
assert.equal(s0.len, 1);
assert.equal(s1.off, 1);
assert.equal(s1.len, 2);
assert.equal(s2.off, 3);
assert.equal(s2.len, 3);

assert.equal(h.stringsBytesLen, 8);
assert.equal(bytes[h.stringsBytesOffset + 6], 0);
assert.equal(bytes[h.stringsBytesOffset + 7], 0);
});

test("reuseOutputBuffer keeps odd-string padding zeroed across reset/build cycles", () => {
const b = createDrawlistBuilderV1({ reuseOutputBuffer: true });

b.drawText(0, 0, "abcd");
expectOk(b.build());

b.reset();
b.drawText(0, 0, "a");
const bytes = expectOk(b.build());
const h = parseHeader(bytes);

assert.equal(h.stringsBytesLen, 4);
assert.equal(bytes[h.stringsBytesOffset], 0x61);
assert.equal(bytes[h.stringsBytesOffset + 1], 0);
assert.equal(bytes[h.stringsBytesOffset + 2], 0);
assert.equal(bytes[h.stringsBytesOffset + 3], 0);
});
});
Loading
Loading