Skip to content
Closed
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
20 changes: 16 additions & 4 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,17 @@ pub fn build(b: *std.Build) void {
.windows => "windows",
};

const appkit_cell_grid_host_test_run = if (b.graph.host.result.os.tag == .macos) blk: {
const run = b.addSystemCommand(&.{ "sh", "scripts/test-appkit-cell-grid-host.sh" });
run.setCwd(b.path("."));
const step = b.step(
"test-appkit-cell-grid-host",
"Run the real AppKit binary cell-grid decoder, CoreText raster, and cache tests",
);
step.dependOn(&run.step);
break :blk run;
} else null;

const test_step = b.step("test", "Run package and framework tests");
test_step.dependOn(&invalid_import_compile.step);
test_step.dependOn(&b.addRunArtifact(build_graph_tests).step);
Expand All @@ -723,6 +734,7 @@ pub fn build(b: *std.Build) void {
test_step.dependOn(&gtk_pixels_test_run.step);
test_step.dependOn(&b.addRunArtifact(record_store_tests).step);
test_step.dependOn(&file_crash_run.step);
if (appkit_cell_grid_host_test_run) |run| test_step.dependOn(&run.step);
for (desktop_test_shards) |shard_tests| {
test_step.dependOn(&b.addRunArtifact(shard_tests).step);
}
Expand Down Expand Up @@ -1394,10 +1406,10 @@ pub fn build(b: *std.Build) void {
// this step until the encoder comment, the host decoder comment, and
// the patterns below move with it.
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-wire-format-version-prose", "Verify wire-format version prose matches the packet version constant", &.{
.{ .path = "src/primitives/canvas/serialization.zig", .pattern = "pub const binary_packet_version: u8 = 5;" },
.{ .path = "src/primitives/canvas/serialization.zig", .pattern = "Compact binary gpu-surface packet encoding (wire format v5)." },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "Compact binary gpu-surface packet decoding (wire format v5)." },
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "Compact binary gpu-surface packet decoding (wire format v5)." },
.{ .path = "src/primitives/canvas/serialization.zig", .pattern = "pub const binary_packet_version: u8 = 7;" },
.{ .path = "src/primitives/canvas/serialization.zig", .pattern = "Compact binary gpu-surface packet encoding (wire format v7)." },
.{ .path = "src/platform/macos/appkit_host.m", .pattern = "Compact binary gpu-surface packet decoding (wire format v7)." },
.{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "Compact binary gpu-surface packet decoding (wire format v7)." },
});
addFileContainsCheckStep(b, file_contains_checker, test_step, "test-windows-gpu-packet-presenter", "Verify Windows uses retained Direct2D packets with recovery, bounded resources, and dirty-region pixel fallback", &.{
.{ .path = "src/platform/windows/root.zig", .pattern = ".present_gpu_surface_packet_binary_fn = presentGpuSurfacePacketBinary" },
Expand Down
40 changes: 40 additions & 0 deletions scripts/test-appkit-cell-grid-host.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/sh
# Compile the AppKit host into a headless executable and exercise the real
# binary cell-grid decoder, CoreText raster path, and retained raster cache.
set -eu

repo_root="$(cd "$(dirname "$0")/.." && pwd)"
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/native-cell-grid-host.XXXXXX")"
trap 'rm -rf "$tmp_dir"' EXIT HUP INT TERM

xcrun clang \
-fobjc-arc \
-fno-sanitize=builtin \
-ObjC \
-mmacosx-version-min=11.0 \
-o "$tmp_dir/cell-grid-host-test" \
"$repo_root/src/platform/macos/cell_grid_host_test.m" \
-framework Foundation \
-framework AppKit \
-framework Metal \
-framework QuartzCore \
-framework CoreText \
-framework CoreGraphics \
-framework ImageIO \
-framework AVFoundation \
-framework UniformTypeIdentifiers \
-framework WebKit \
-framework Security \
-framework ScreenCaptureKit \
-framework CoreMedia \
-framework CoreVideo \
-framework IOKit \
-framework Carbon \
-framework Accelerate \
-framework MediaToolbox

"$tmp_dir/cell-grid-host-test"
NATIVE_SDK_GPU_SHOT_EVERY_EXPECT=30 "$tmp_dir/cell-grid-host-test"
NATIVE_SDK_GPU_SHOT_EVERY=1 NATIVE_SDK_GPU_SHOT_EVERY_EXPECT=1 "$tmp_dir/cell-grid-host-test"
NATIVE_SDK_GPU_SHOT_EVERY=0 NATIVE_SDK_GPU_SHOT_EVERY_EXPECT=1 "$tmp_dir/cell-grid-host-test"
NATIVE_SDK_GPU_SHOT_EVERY=invalid NATIVE_SDK_GPU_SHOT_EVERY_EXPECT=30 "$tmp_dir/cell-grid-host-test"
2 changes: 1 addition & 1 deletion skill-data/native-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ The desktop list convention — click selects, the primary action (open the reco

## Widget budgets and virtualization

Every view has fixed per-view capacities (`src/runtime/canvas_limits.zig`): **1024 retained widget nodes** (`max_canvas_widget_nodes_per_view` — the budget that matters for tree design; semantics and spans match it), 64 KiB retained widget text, **512 declared context-menu items** summed across all widgets of the view (`max_canvas_widget_context_menu_items_per_view` — separators count as items), **64 chart series / 16384 chart points** summed across all charts of the view (`max_canvas_widget_chart_*` — `ui.chart` downsamples every series to 256 points, so this is 64 maximal series or hundreds of sparklines), and per-frame content budgets (2048 commands, 8192 glyphs, 32 KiB frame text, 2048 path elements shared by icons and charts). Overflow is loud: `error.WidgetLayoutListFull` / `error.WidgetNodeLimitReached` / `error.WidgetContextMenuLimitReached` / `error.WidgetAnchoredSurfaceLimitReached` (at most **16 anchored floating surfaces** mounted per view — `max_canvas_widget_anchored_per_view`) fail tests under the harness's propagate policy and log a teaching diagnostic naming the budget in production (the app degrades to the previous frame). Watch headroom without overflowing: automation snapshots report `widget_nodes=N/1024 widget_semantics=N/1024 context_menu_items=N/512` on every gpu_surface view line.
Every view has fixed per-view capacities (`src/runtime/canvas_limits.zig`): **1024 retained widget nodes** (`max_canvas_widget_nodes_per_view` — the budget that matters for tree design; semantics and spans match it), 64 KiB retained widget text, **512 declared context-menu items** summed across all widgets of the view (`max_canvas_widget_context_menu_items_per_view` — separators count as items), **64 chart series / 16384 chart points** summed across all charts of the view (`max_canvas_widget_chart_*` — `ui.chart` downsamples every series to 256 points, so this is 64 maximal series or hundreds of sparklines), and per-frame content budgets (2048 commands, 8192 glyphs, 64 KiB frame text, 2048 path elements shared by icons and charts, 32768 packed terminal cells). The `.terminal` widget does not spend the command budget: a screen is packed `cell_grid` commands, one per row, (`canvas.cell_grid`) carrying every cell's background, cluster, foreground and style, which every renderer expands itself. Its budget is **32768 cells** per view (`max_canvas_cells_per_view`, 20 B each), so a 300x100 truecolor viewport costs 103 commands (one per row, the retained-patch granularity: a keystroke re-encodes one row) and 585 KB. Past the cell budget it DEGRADES rather than failing — fewer complete rows from the top, and it reports what it dropped (`canvas.terminal_grid.paintReport`, `Builder.degradation`, and a teaching log line naming the budget), so a half-blank terminal is never silent. Overflow is loud: `error.WidgetLayoutListFull` / `error.WidgetNodeLimitReached` / `error.WidgetContextMenuLimitReached` / `error.WidgetAnchoredSurfaceLimitReached` (at most **16 anchored floating surfaces** mounted per view — `max_canvas_widget_anchored_per_view`) fail tests under the harness's propagate policy and log a teaching diagnostic naming the budget in production (the app degrades to the previous frame). Watch headroom without overflowing: automation snapshots report `widget_nodes=N/1024 widget_semantics=N/1024 context_menu_items=N/512` on every gpu_surface view line.

Budget rules of thumb: 1024 nodes is roomy for a three-pane desktop app (~500 nodes measured for a dense sidebar + markdown detail + run surface), but node count scales with what is MOUNTED, not what is visible — so bound every unbounded collection:

Expand Down
3 changes: 2 additions & 1 deletion src/automation/protocol.zig
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ pub const fingerprint: u64 = layout_fingerprint.hash(layoutDescription(semantic_
/// runtime, including command ids and enabled/checked/key state.
/// Epoch 4: command and app-menu catalog strings in snapshots use
/// JSON-style escapes so every catalog entry remains exactly one line.
pub const semantic_epoch: u32 = 4;
/// Epoch 5: widget snapshots expose the non-default context-menu policy.
pub const semantic_epoch: u32 = 5;

/// The canonical description the protocol fingerprint hashes: the
/// command vocabulary (the `Action` enum, reflected — names and values,
Expand Down
9 changes: 9 additions & 0 deletions src/automation/snapshot.zig
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ pub const Widget = struct {
/// fallback surface) and what `widget-context-menu` invokes by
/// index.
context_menu: []const WidgetContextMenuItem = &.{},
/// Non-default context-menu policy name. Empty means `automatic`.
context_menu_policy: []const u8 = "",
};

/// One status-item dropdown row as the runtime last applied it. Slices
Expand Down Expand Up @@ -603,6 +605,7 @@ pub fn writeText(input: Input, writer: anytype) !void {
try writeWidgetActions(widget.actions, writer);
try writeWidgetTextRanges(widget, writer);
try writeWidgetContextMenu(widget, writer);
try writeWidgetContextMenuPolicy(widget, writer);
try writer.writeByte('\n');
}
for (input.commands) |command| {
Expand Down Expand Up @@ -802,6 +805,7 @@ pub fn writeA11yText(input: Input, writer: anytype) !void {
try writeWidgetActions(widget.actions, writer);
try writeWidgetTextRanges(widget, writer);
try writeWidgetContextMenu(widget, writer);
try writeWidgetContextMenuPolicy(widget, writer);
try writer.writeByte('\n');
}
}
Expand Down Expand Up @@ -974,6 +978,11 @@ fn writeWidgetContextMenu(widget: Widget, writer: anytype) !void {
try writer.writeByte(']');
}

fn writeWidgetContextMenuPolicy(widget: Widget, writer: anytype) !void {
if (widget.context_menu_policy.len == 0) return;
try writer.print(" context_menu_policy={s}", .{widget.context_menu_policy});
}

test "snapshot emits window and source" {
var buffer: [512]u8 = undefined;
var writer = std.Io.Writer.fixed(&buffer);
Expand Down
Loading