lib/gui/src/surface/storage.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const capacity_mod = @import("capacity.zig");
4 const command = @import("command.zig");
5 const decode = @import("decode.zig");
6 const gui = @import("../root.zig");
7 const survey = @import("survey.zig");
8 const types = @import("types.zig");
9
10 const Surface = types.Surface;
11 const UiImage = gui.model.UiImage;
12 const UiImageSet = gui.model.UiImageSet;
13 const UiNode = gui.model.UiNode;
14 const UiSurfaceTree = gui.model.UiSurfaceTree;
15 const UiText = gui.model.UiText;
16 const UiTextRun = gui.model.UiTextRun;
17 const UiTextStyle = gui.model.UiTextStyle;
18
19 pub const Exhaustion = survey.Error || error{SurfaceCapacityExceeded};
20 pub const CloneError = Exhaustion;
21
22 pub const PlacementStatus = struct {
23 capacity: capacity_mod.Capacity,
24 facts: capacity_mod.Facts,
25 has_surface: bool,
26 };
27
28 pub const Placement = struct {
29 limits: capacity_mod.Limits,
30 bytes: []align(capacity_mod.storage_alignment) u8,
31 surface: ?Surface = null,
32 facts: capacity_mod.Facts = .{},
33
34 pub fn init(
35 bytes: []align(capacity_mod.storage_alignment) u8,
36 capacity: capacity_mod.Capacity,
37 ) Placement {
38 std.debug.assert(bytes.len == capacity.storage_bytes);
39 return .{ .limits = capacity.toLimits(), .bytes = bytes };
40 }
41
42 pub inline fn cloneSurface(self: *Placement, src: Surface) CloneError!Surface {
43 const facts = try survey.surface(src);
44 if (!self.limits.admits(facts)) return error.SurfaceCapacityExceeded;
45 return self.placeSurface(src, facts);
46 }
47
48 pub fn cloneCommandSurface(
49 self: *Placement,
50 src: *const command.CommandUiSurface,
51 ) CloneError!Surface {
52 const facts = try survey.commandSurface(src);
53 if (!self.limits.admits(facts)) return error.SurfaceCapacityExceeded;
54 return self.placeCommandSurface(src, facts);
55 }
56
57 pub fn reset(self: *Placement) void {
58 self.surface = null;
59 self.facts = .{};
60 }
61
62 pub fn current(self: *const Placement) ?Surface {
63 return self.surface;
64 }
65
66 pub inline fn currentPtr(self: *const Placement) ?*const Surface {
67 if (self.surface) |*value| return value;
68 return null;
69 }
70
71 pub inline fn hasSurface(self: *const Placement) bool {
72 return self.surface != null;
73 }
74
75 pub fn status(self: *const Placement) PlacementStatus {
76 return .{
77 .capacity = self.derivedCapacity(),
78 .facts = self.facts,
79 .has_surface = self.surface != null,
80 };
81 }
82
83 fn derivedCapacity(self: *const Placement) capacity_mod.Capacity {
84 return capacity_mod.Capacity.derive(self.limits) catch unreachable;
85 }
86
87 fn placeSurface(
88 self: *Placement,
89 src: Surface,
90 facts: capacity_mod.Facts,
91 ) Surface {
92 var cursor = Cursor.init(self, self.derivedCapacity());
93 const tree = cursor.surface();
94 tree.* = .{
95 .available_size = src.tree.available_size,
96 .root = cursor.cloneNode(&src.tree.root),
97 };
98 const result = Surface{
99 .tree = tree,
100 .images = cursor.cloneImages(src.images),
101 };
102 cursor.finish(facts);
103 self.surface = result;
104 self.facts = facts;
105 return result;
106 }
107
108 fn placeCommandSurface(
109 self: *Placement,
110 src: *const command.CommandUiSurface,
111 facts: capacity_mod.Facts,
112 ) Surface {
113 var cursor = Cursor.init(self, self.derivedCapacity());
114 const tree = cursor.surface();
115 tree.* = .{
116 .available_size = .{
117 .width = src.available_width,
118 .height = src.available_height,
119 },
120 .root = cursor.cloneCommandNode(src.root_ptr.?),
121 };
122 const result = Surface{
123 .tree = tree,
124 .images = cursor.cloneCommandImages(src),
125 };
126 cursor.finish(facts);
127 self.surface = result;
128 self.facts = facts;
129 return result;
130 }
131 };
132
133 const Cursor = struct {
134 surface_offset: usize,
135 bytes: []align(capacity_mod.storage_alignment) u8,
136 nodes: []UiNode,
137 nodes_used: usize = 0,
138 runs: []UiTextRun,
139 runs_used: usize = 0,
140 styles: []UiTextStyle,
141 styles_used: usize = 0,
142 images: []UiImage,
143 images_used: usize = 0,
144 image_pixels: []u32,
145 image_pixels_used: usize = 0,
146 semantics: []u8,
147 semantics_used: usize = 0,
148
149 fn init(placement: *Placement, capacity: capacity_mod.Capacity) Cursor {
150 return .{
151 .surface_offset = capacity.surface_offset,
152 .bytes = placement.bytes,
153 .nodes = typedSlice(
154 UiNode,
155 placement.bytes,
156 capacity.node_offset,
157 capacity.node_slots,
158 ),
159 .runs = typedSlice(
160 UiTextRun,
161 placement.bytes,
162 capacity.run_offset,
163 capacity.run_slots,
164 ),
165 .styles = typedSlice(
166 UiTextStyle,
167 placement.bytes,
168 capacity.style_offset,
169 capacity.style_slots,
170 ),
171 .images = typedSlice(
172 UiImage,
173 placement.bytes,
174 capacity.image_offset,
175 capacity.image_slots,
176 ),
177 .image_pixels = typedSlice(
178 u32,
179 placement.bytes,
180 capacity.image_pixel_offset,
181 capacity.image_pixel_slots,
182 ),
183 .semantics = placement.bytes[capacity.semantic_offset..][0..capacity.semantic_bytes],
184 };
185 }
186
187 fn surface(self: *Cursor) *UiSurfaceTree {
188 return typedOne(UiSurfaceTree, self.bytes, self.surface_offset);
189 }
190
191 fn cloneNode(self: *Cursor, src: *const UiNode) UiNode {
192 var node = UiNode{
193 .widget_id = src.widget_id,
194 .kind = src.kind,
195 .style = src.style,
196 .size = src.size,
197 .paint = src.paint,
198 .scroll = src.scroll,
199 .focusable = src.focusable,
200 .layer = src.layer,
201 .text_selection = src.text_selection,
202 .state_flags = src.state_flags,
203 };
204 if (src.text) |text| node.text = self.cloneText(text);
205 node.action = self.cloneBytes(src.action);
206 node.role = self.cloneBytes(src.role);
207 if (src.children.len > 0) {
208 const children = self.carveNodes(src.children.len);
209 node.children = children;
210 for (src.children, children) |*source, *destination| {
211 destination.* = self.cloneNode(source);
212 }
213 }
214 return node;
215 }
216
217 fn cloneCommandNode(self: *Cursor, src: *const command.CommandUiNode) UiNode {
218 var node = UiNode{
219 .widget_id = src.widget_id,
220 .kind = decode.widgetKind(src.kind) catch unreachable,
221 .style = decode.commandStyle(src.style) catch unreachable,
222 .size = .{
223 .width = decode.optionalF32(src.size.width),
224 .height = decode.optionalF32(src.size.height),
225 },
226 .paint = decode.commandPaint(src.paint),
227 .scroll = decode.commandScroll(src.style) catch unreachable,
228 .focusable = src.focusable != 0,
229 .state_flags = src.state_flags,
230 };
231 if (src.text_ptr) |text| node.text = self.cloneCommandText(text);
232 node.text_selection = decode.commandTextSelection(
233 src.text_selection,
234 if (node.text) |text| text.content.len else 0,
235 ) catch unreachable;
236 if (src.action_len > 0) node.action = self.cloneBytes(src.action_ptr.?[0..src.action_len]);
237 if (src.role_len > 0) node.role = self.cloneBytes(src.role_ptr.?[0..src.role_len]);
238 if (src.children_len > 0) {
239 const source = src.children_ptr.?[0..src.children_len];
240 const children = self.carveNodes(src.children_len);
241 node.children = children;
242 for (source, children) |*source_child, *destination| {
243 destination.* = self.cloneCommandNode(source_child);
244 }
245 }
246 return node;
247 }
248
249 fn cloneCommandText(self: *Cursor, src: *const command.CommandUiText) UiText {
250 return .{
251 .content = if (src.content_len == 0) &.{} else self.cloneBytes(src.content_ptr.?[0..src.content_len]),
252 .runs = self.cloneCommandRuns(src),
253 .styles = self.cloneCommandStyles(src),
254 .font_asset_id = src.font_asset_id,
255 .font_weight = decode.normalizeFontWeight(src.font_weight),
256 .point_size = src.point_size,
257 .line_height = src.line_height,
258 .wrap_width = src.wrap_width,
259 .horizontal_align = decode.textAlign(src.horizontal_align) catch unreachable,
260 .vertical_align = decode.textAlign(src.vertical_align) catch unreachable,
261 };
262 }
263
264 fn cloneText(self: *Cursor, src: UiText) UiText {
265 return .{
266 .content = self.cloneBytes(src.content),
267 .runs = self.cloneRuns(src.runs),
268 .styles = self.cloneStyles(src.styles),
269 .font_asset_id = src.font_asset_id,
270 .font_weight = src.font_weight,
271 .point_size = src.point_size,
272 .line_height = src.line_height,
273 .wrap_width = src.wrap_width,
274 .horizontal_align = src.horizontal_align,
275 .vertical_align = src.vertical_align,
276 };
277 }
278
279 fn carveNodes(self: *Cursor, count: usize) []UiNode {
280 std.debug.assert(self.nodes_used + count <= self.nodes.len);
281 const result = self.nodes[self.nodes_used..][0..count];
282 self.nodes_used += count;
283 return result;
284 }
285
286 fn cloneRuns(self: *Cursor, source: []const UiTextRun) []const UiTextRun {
287 if (source.len == 0) return &.{};
288 const result = self.carveRuns(source.len);
289 @memcpy(result, source);
290 return result;
291 }
292
293 fn cloneCommandRuns(self: *Cursor, source: *const command.CommandUiText) []const UiTextRun {
294 if (source.runs_len == 0) return &.{};
295 const input = source.runs_ptr.?[0..source.runs_len];
296 const result = self.carveRuns(input.len);
297 for (input, result) |run, *destination| destination.* = decode.commandTextRun(run);
298 return result;
299 }
300
301 fn carveRuns(self: *Cursor, count: usize) []UiTextRun {
302 std.debug.assert(self.runs_used + count <= self.runs.len);
303 const result = self.runs[self.runs_used..][0..count];
304 self.runs_used += count;
305 return result;
306 }
307
308 fn cloneStyles(self: *Cursor, source: []const UiTextStyle) []const UiTextStyle {
309 if (source.len == 0) return &.{};
310 const result = self.carveStyles(source.len);
311 @memcpy(result, source);
312 return result;
313 }
314
315 fn cloneCommandStyles(self: *Cursor, source: *const command.CommandUiText) []const UiTextStyle {
316 if (source.styles_len == 0) return &.{};
317 const input = source.styles_ptr.?[0..source.styles_len];
318 const result = self.carveStyles(input.len);
319 for (input, result) |style, *destination| destination.* = decode.commandTextStyle(style);
320 return result;
321 }
322
323 fn cloneImages(self: *Cursor, source: UiImageSet) UiImageSet {
324 if (source.images.len == 0) return .{};
325 const result = self.carveImages(source.images.len);
326 for (source.images, result) |image, *destination| {
327 const count = image.pixelCount();
328 destination.* = .{
329 .width = image.width,
330 .height = image.height,
331 .pixels = self.cloneImagePixels(image.pixels[0..count]),
332 };
333 }
334 return .{ .images = result };
335 }
336
337 fn cloneCommandImages(
338 self: *Cursor,
339 source: *const command.CommandUiSurface,
340 ) UiImageSet {
341 if (source.images_len == 0) return .{};
342 const input = source.images_ptr.?[0..source.images_len];
343 const result = self.carveImages(input.len);
344 for (input, result) |image, *destination| {
345 const count = @as(usize, image.width) * @as(usize, image.height);
346 destination.* = .{
347 .width = image.width,
348 .height = image.height,
349 .pixels = self.cloneImagePixels(image.pixels_ptr.?[0..count]),
350 };
351 }
352 return .{ .images = result };
353 }
354
355 fn carveImages(self: *Cursor, count: usize) []UiImage {
356 std.debug.assert(self.images_used + count <= self.images.len);
357 const result = self.images[self.images_used..][0..count];
358 self.images_used += count;
359 return result;
360 }
361
362 fn cloneImagePixels(self: *Cursor, source: []const u32) []const u32 {
363 std.debug.assert(self.image_pixels_used + source.len <= self.image_pixels.len);
364 const result = self.image_pixels[self.image_pixels_used..][0..source.len];
365 @memcpy(result, source);
366 self.image_pixels_used += source.len;
367 return result;
368 }
369
370 fn carveStyles(self: *Cursor, count: usize) []UiTextStyle {
371 std.debug.assert(self.styles_used + count <= self.styles.len);
372 const result = self.styles[self.styles_used..][0..count];
373 self.styles_used += count;
374 return result;
375 }
376
377 fn cloneBytes(self: *Cursor, source: []const u8) []const u8 {
378 if (source.len == 0) return &.{};
379 std.debug.assert(self.semantics_used + source.len <= self.semantics.len);
380 const result = self.semantics[self.semantics_used..][0..source.len];
381 @memcpy(result, source);
382 self.semantics_used += source.len;
383 return result;
384 }
385
386 fn finish(self: *const Cursor, facts: capacity_mod.Facts) void {
387 std.debug.assert(self.nodes_used + 1 == facts.nodes);
388 std.debug.assert(self.runs_used == facts.runs);
389 std.debug.assert(self.styles_used == facts.styles);
390 std.debug.assert(self.images_used == facts.images);
391 std.debug.assert(self.image_pixels_used == facts.image_pixels);
392 std.debug.assert(self.semantics_used == facts.semantic_bytes);
393 }
394 };
395
396 pub const Storage = struct {
397 phase: alloc_phase.capacity.Phase,
398 capacity: capacity_mod.Capacity,
399 bytes: []align(capacity_mod.storage_alignment) u8,
400 placement: Placement,
401
402 pub const Limits: type = capacity_mod.Limits;
403 pub const Capacity: type = capacity_mod.Capacity;
404 pub const Exhaustion: type = @import("storage.zig").Exhaustion;
405 pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError;
406
407 pub const claim: alloc_phase.capacity.Declaration = .{
408 .source = .{
409 .id = "gui.surface_clone_storage",
410 .kind = .phase_static,
411 .limit_source = .caller,
412 .storage = .{
413 .covered = &.{
414 .{
415 .id = "one_cloned_uisurfacetree_header_and_caller_sized_ty_33bebc07fe23",
416 .lifetime = .steady,
417 .detail = "one cloned UiSurfaceTree header and caller-sized typed node arena",
418 },
419 .{
420 .id = "caller_sized_cloned_uitextrun_arena",
421 .lifetime = .steady,
422 .detail = "caller-sized cloned UiTextRun arena",
423 },
424 .{
425 .id = "caller_sized_cloned_uitextstyle_arena",
426 .lifetime = .steady,
427 .detail = "caller-sized cloned UiTextStyle arena",
428 },
429 .{
430 .id = "caller_sized_cloned_ui_image_descriptor_arena",
431 .lifetime = .steady,
432 .detail = "caller-sized cloned UI image descriptor arena",
433 },
434 .{
435 .id = "caller_sized_cloned_packed_rgba8_image_pixels",
436 .lifetime = .steady,
437 .detail = "caller-sized cloned packed RGBA8 image pixels",
438 },
439 .{
440 .id = "caller_sized_cloned_text_action_and_role_bytes",
441 .lifetime = .steady,
442 .detail = "caller-sized cloned text action and role bytes",
443 },
444 },
445 .excluded = &.{
446 "caller-owned source trees and command pointer memory",
447 "sdfii retained root lookup and transactional replacement slots",
448 "frame layout render input and paint storage",
449 },
450 },
451 .capacity = .{
452 .inputs = &.{
453 alloc_phase.capacity.bindInput(Limits, "nodes", "nodes"),
454 alloc_phase.capacity.bindInput(Limits, "runs", "runs"),
455 alloc_phase.capacity.bindInput(Limits, "styles", "styles"),
456 alloc_phase.capacity.bindInput(Limits, "images", "images"),
457 alloc_phase.capacity.bindInput(Limits, "image_pixels", "image_pixels"),
458 alloc_phase.capacity.bindInput(Limits, "semantic_bytes", "semantic_bytes"),
459 },
460 .type_selectors = &.{
461 alloc_phase.capacity.bindType(UiSurfaceTree, "uisurfacetree"),
462 alloc_phase.capacity.bindType(UiNode, "uinode"),
463 alloc_phase.capacity.bindType(UiTextRun, "uitextrun"),
464 alloc_phase.capacity.bindType(UiTextStyle, "uitextstyle"),
465 alloc_phase.capacity.bindType(UiImage, "uiimage"),
466 alloc_phase.capacity.bindType(u32, "u32"),
467 },
468 .nodes = &.{
469 .{ .constant = 1 },
470 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
471 .{ .alignment = .{ .node = 1, .alignment = .{ .literal = 16 } } },
472 .{ .input = 0 },
473 .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 1 } } },
474 .{ .alignment = .{ .node = 4, .alignment = .{ .literal = 16 } } },
475 .{ .input = 1 },
476 .{ .scale = .{ .node = 6, .coefficient = .{ .size_of_concrete_type = 2 } } },
477 .{ .alignment = .{ .node = 7, .alignment = .{ .literal = 16 } } },
478 .{ .input = 2 },
479 .{ .scale = .{ .node = 9, .coefficient = .{ .size_of_concrete_type = 3 } } },
480 .{ .alignment = .{ .node = 10, .alignment = .{ .literal = 16 } } },
481 .{ .input = 3 },
482 .{ .scale = .{ .node = 12, .coefficient = .{ .size_of_concrete_type = 4 } } },
483 .{ .alignment = .{ .node = 13, .alignment = .{ .literal = 16 } } },
484 .{ .input = 4 },
485 .{ .scale = .{ .node = 15, .coefficient = .{ .size_of_concrete_type = 5 } } },
486 .{ .alignment = .{ .node = 16, .alignment = .{ .literal = 16 } } },
487 .{ .input = 5 },
488 .{ .alignment = .{ .node = 18, .alignment = .{ .literal = 16 } } },
489 .{ .add = .{ .left = 2, .right = 5 } },
490 .{ .add = .{ .left = 20, .right = 8 } },
491 .{ .add = .{ .left = 21, .right = 11 } },
492 .{ .add = .{ .left = 22, .right = 14 } },
493 .{ .add = .{ .left = 23, .right = 17 } },
494 .{ .add = .{ .left = 24, .right = 19 } },
495 },
496 .assertions = &.{.{
497 .scope = .closure_total,
498 .measure = .retained,
499 .relation = .upper_bound,
500 .expression = 25,
501 }},
502 },
503 .overload = .{
504 .kind = .reject_before_mutation,
505 .detail = "depth invalid command facts and max-plus-one node run style image pixel or semantic demand reject during survey before placement state changes",
506 },
507 .risks = .{
508 .transitive = .{
509 .status = .witnessed,
510 .detail = "typed native and command placement traversals carry no allocator capability after storage activation",
511 },
512 .foreign = .{
513 .status = .open,
514 .detail = "command trees and image tables are foreign pointer graphs whose memory must remain immutable and valid through survey and placement",
515 },
516 },
517 .obligations = &.{
518 .{ .key = "gui_surface_clone_capacity", .role = .capacity_model },
519 .{ .key = "gui_surface_clone_survey", .role = .overload },
520 .{ .key = "gui_surface_clone_acquisition", .role = .custom },
521 .{ .key = "gui_surface_clone_boundary", .role = .overload },
522 .{ .key = "gui_surface_clone_command", .role = .foreign_risk },
523 .{ .key = "gui_surface_clone_images", .role = .overload },
524 .{ .key = "gui_surface_clone_command_images_overload", .role = .overload },
525 .{ .key = "gui_surface_clone_command_images_foreign_risk", .role = .foreign_risk },
526 .{ .key = "gui_surface_clone_sealed", .role = .transitive_risk },
527 },
528 },
529 .bindings = .{
530 .owner = @This(),
531 .seal = .{
532 .family = alloc_phase.capacity.selector(@This().activate),
533 .premise = .{
534 .class = .checked_semantic_fact,
535 .authority = .checker,
536 },
537 },
538 .teardown = .{
539 .family = alloc_phase.capacity.selector(@This().deinit),
540 .premise = .{
541 .class = .checked_semantic_fact,
542 .authority = .checker,
543 },
544 },
545 },
546 };
547
548 pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage {
549 const capacity = try Capacity.derive(limits);
550 const bytes = try allocator.alignedAlloc(
551 u8,
552 .fromByteUnits(capacity_mod.storage_alignment),
553 capacity.storage_bytes,
554 );
555 return .{
556 .phase = .initialization,
557 .capacity = capacity,
558 .bytes = bytes,
559 .placement = Placement.init(bytes, capacity),
560 };
561 }
562
563 pub fn activate(self: *Storage) void {
564 std.debug.assert(self.phase == .initialization);
565 self.assertStorage();
566 self.phase = .steady;
567 }
568
569 pub fn cloneSurface(self: *Storage, src: Surface) CloneError!Surface {
570 std.debug.assert(self.phase == .steady);
571 self.assertStorage();
572 return self.placement.cloneSurface(src);
573 }
574
575 pub fn cloneCommandSurface(
576 self: *Storage,
577 src: *const command.CommandUiSurface,
578 ) CloneError!Surface {
579 std.debug.assert(self.phase == .steady);
580 self.assertStorage();
581 return self.placement.cloneCommandSurface(src);
582 }
583
584 pub fn current(self: *const Storage) ?Surface {
585 return self.placement.current();
586 }
587
588 pub fn status(self: *const Storage) PlacementStatus {
589 self.assertStorage();
590 return self.placement.status();
591 }
592
593 pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void {
594 std.debug.assert(self.phase != .teardown);
595 self.assertStorage();
596 self.phase = .teardown;
597 allocator.free(self.bytes);
598 self.bytes = &.{};
599 self.placement.bytes = &.{};
600 self.placement.reset();
601 }
602
603 fn assertStorage(self: *const Storage) void {
604 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
605 std.debug.assert(self.placement.bytes.ptr == self.bytes.ptr);
606 std.debug.assert(self.placement.bytes.len == self.bytes.len);
607 std.debug.assert(std.meta.eql(self.placement.limits, self.capacity.toLimits()));
608 }
609 };
610
611 fn typedOne(
612 comptime T: type,
613 bytes: []align(capacity_mod.storage_alignment) u8,
614 offset: usize,
615 ) *T {
616 const region: *align(@alignOf(T)) [@sizeOf(T)]u8 = @ptrCast(@alignCast(bytes[offset..][0..@sizeOf(T)]));
617 return @ptrCast(region);
618 }
619
620 fn typedSlice(
621 comptime T: type,
622 bytes: []align(capacity_mod.storage_alignment) u8,
623 offset: usize,
624 count: usize,
625 ) []T {
626 const byte_count = count * @sizeOf(T);
627 const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
628 return std.mem.bytesAsSlice(T, region);
629 }
630
631 fn checkInitFailures(allocator: std.mem.Allocator) !void {
632 var storage = try Storage.init(allocator, .{ .nodes = 7, .semantic_bytes = 33 });
633 storage.deinit(allocator);
634 }
635
636 test "surface clone storage acquires one exact aligned region" {
637 comptime {
638 @stardustClaim(
639 @import("alloc_phase").capacity.witness(Storage, "gui_surface_clone_acquisition"),
640 null,
641 null,
642 null,
643 null,
644 null,
645 null,
646 );
647 }
648
649 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
650 var storage = try Storage.init(counting.allocator(), .{ .nodes = 7, .semantic_bytes = 33 });
651 defer storage.deinit(counting.allocator());
652 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
653 try std.testing.expectEqual(storage.capacity.storage_bytes, counting.allocated_bytes);
654 }
655
656 test "surface clone storage retries after every allocation failure" {
657 try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
658 }
659
660 test "surface clone storage owns nested surface slices" {
661 const allocator = std.testing.allocator;
662 var source_text = try allocator.dupe(u8, "Run");
663 defer allocator.free(source_text);
664 var source_runs = try allocator.dupe(UiTextRun, &.{.{
665 .byte_start = 0,
666 .byte_end = 3,
667 .style_slot = 1,
668 .foreground = .{ .r = 190, .g = 30, .b = 20 },
669 .underline = true,
670 }});
671 defer allocator.free(source_runs);
672 var source_styles = try allocator.dupe(UiTextStyle, &.{
673 .{ .font_asset_id = 8, .point_size = 18 },
674 });
675 defer allocator.free(source_styles);
676 var source_action = try allocator.dupe(u8, "chrome.quick-bar.command.run");
677 defer allocator.free(source_action);
678 var children = [_]UiNode{.{
679 .widget_id = 2,
680 .kind = .button,
681 .layer = 7,
682 .size = .{ .width = 40, .height = 20 },
683 .text = .{ .content = source_text, .runs = source_runs, .styles = source_styles },
684 .action = source_action,
685 }};
686 const surface = UiSurfaceTree{
687 .available_size = .{ .width = 200, .height = 40 },
688 .root = .{ .widget_id = 1, .children = &children },
689 };
690 const facts = try survey.surface(.{ .tree = &surface });
691 var storage = try Storage.init(allocator, .{
692 .nodes = facts.nodes,
693 .runs = facts.runs,
694 .styles = facts.styles,
695 .semantic_bytes = facts.semantic_bytes,
696 });
697 defer storage.deinit(allocator);
698 storage.activate();
699 const cloned = try storage.cloneSurface(.{ .tree = &surface });
700
701 source_text[0] = 'X';
702 source_runs[0].foreground.?.r = 0;
703 source_styles[0].point_size = 9;
704 source_action[0] = 'x';
705
706 try std.testing.expectEqual(@as(f32, 200), cloned.tree.available_size.width);
707 try std.testing.expectEqual(@as(usize, 1), cloned.tree.root.children.len);
708 try std.testing.expectEqual(@as(u8, 7), cloned.tree.root.children[0].layer);
709 try std.testing.expectEqualStrings("Run", cloned.tree.root.children[0].text.?.content);
710 try std.testing.expectEqual(@as(usize, 1), cloned.tree.root.children[0].text.?.runs.len);
711 try std.testing.expectEqual(@as(u16, 1), cloned.tree.root.children[0].text.?.runs[0].style_slot);
712 try std.testing.expectEqual(@as(f64, 18), cloned.tree.root.children[0].text.?.styles[0].point_size);
713 try std.testing.expectEqual(@as(u8, 190), cloned.tree.root.children[0].text.?.runs[0].foreground.?.r);
714 try std.testing.expect(cloned.tree.root.children[0].text.?.runs[0].underline);
715 try std.testing.expectEqualStrings("chrome.quick-bar.command.run", cloned.tree.root.children[0].action);
716 }
717
718 test "surface clone storage rejects max plus one before replacing its current tree" {
719 comptime {
720 @stardustClaim(
721 @import("alloc_phase").capacity.witness(Storage, "gui_surface_clone_boundary"),
722 null,
723 null,
724 null,
725 null,
726 null,
727 null,
728 );
729 }
730
731 const initial = UiSurfaceTree{
732 .available_size = .{ .width = 10, .height = 10 },
733 .root = .{ .widget_id = 7, .text = .{ .content = "1234" } },
734 };
735 const extra = [_]UiNode{.{ .widget_id = 9 }};
736 const too_many = UiSurfaceTree{
737 .available_size = .{ .width = 10, .height = 10 },
738 .root = .{ .widget_id = 8, .text = .{ .content = "1234" }, .children = &extra },
739 };
740 const too_many_bytes = UiSurfaceTree{
741 .available_size = .{ .width = 10, .height = 10 },
742 .root = .{ .widget_id = 8, .text = .{ .content = "12345" } },
743 };
744 var storage = try Storage.init(std.testing.allocator, .{ .nodes = 1, .semantic_bytes = 4 });
745 defer storage.deinit(std.testing.allocator);
746 storage.activate();
747 const cloned = try storage.cloneSurface(.{ .tree = &initial });
748 try std.testing.expectError(error.SurfaceCapacityExceeded, storage.cloneSurface(.{ .tree = &too_many }));
749 try std.testing.expectError(error.SurfaceCapacityExceeded, storage.cloneSurface(.{ .tree = &too_many_bytes }));
750 try std.testing.expectEqual(@as(u64, 7), cloned.tree.root.widget_id);
751 try std.testing.expectEqualStrings("1234", cloned.tree.root.text.?.content);
752 try std.testing.expectEqual(@as(u64, 7), storage.current().?.tree.root.widget_id);
753 }
754
755 test "surface clone command survey rejects invalid input before replacement" {
756 comptime {
757 @stardustClaim(
758 @import("alloc_phase").capacity.witness(Storage, "gui_surface_clone_command"),
759 null,
760 null,
761 null,
762 null,
763 null,
764 null,
765 );
766 }
767
768 const initial = UiSurfaceTree{
769 .available_size = .{ .width = 10, .height = 10 },
770 .root = .{ .widget_id = 7, .text = .{ .content = "1234" } },
771 };
772 var invalid_root = std.mem.zeroes(command.CommandUiNode);
773 invalid_root.kind = 255;
774 const invalid = command.CommandUiSurface{
775 .available_width = 10,
776 .available_height = 10,
777 .root_ptr = &invalid_root,
778 .images_ptr = null,
779 .images_len = 0,
780 };
781 var storage = try Storage.init(std.testing.allocator, .{ .nodes = 1, .semantic_bytes = 4 });
782 defer storage.deinit(std.testing.allocator);
783 storage.activate();
784 _ = try storage.cloneSurface(.{ .tree = &initial });
785 try std.testing.expectError(error.InvalidCommand, storage.cloneCommandSurface(&invalid));
786 try std.testing.expectEqual(@as(u64, 7), storage.current().?.tree.root.widget_id);
787 try std.testing.expectEqualStrings("1234", storage.current().?.tree.root.text.?.content);
788 }
789
790 test "surface command decodes linear gradient paint" {
791 var root = std.mem.zeroes(command.CommandUiNode);
792 root.widget_id = 7;
793 root.paint.linear_gradient = .{
794 .is_set = 1,
795 .start = .{ .x = std.math.nan(f32), .y = 0.25 },
796 .end = .{ .x = 0.8, .y = std.math.inf(f32) },
797 .start_color = .{ .r = 10, .g = 20, .b = 30, .a = 210 },
798 .end_color = .{ .r = 90, .g = 120, .b = 180, .a = 160 },
799 };
800 const source = command.CommandUiSurface{
801 .available_width = 20,
802 .available_height = 12,
803 .root_ptr = &root,
804 .images_ptr = null,
805 .images_len = 0,
806 };
807 const facts = try survey.commandSurface(&source);
808 var storage = try Storage.init(std.testing.allocator, .{
809 .nodes = facts.nodes,
810 .runs = facts.runs,
811 .styles = facts.styles,
812 .semantic_bytes = facts.semantic_bytes,
813 });
814 defer storage.deinit(std.testing.allocator);
815 storage.activate();
816 const cloned = try storage.cloneCommandSurface(&source);
817 const gradient = cloned.tree.root.paint.linear_gradient.?;
818 try std.testing.expectEqual(@as(f32, 0), gradient.start.x);
819 try std.testing.expectEqual(@as(f32, 0.25), gradient.start.y);
820 try std.testing.expectEqual(@as(f32, 0.8), gradient.end.x);
821 try std.testing.expectEqual(@as(f32, 0), gradient.end.y);
822 try std.testing.expectEqual(@as(u8, 10), gradient.start_color.r);
823 try std.testing.expectEqual(@as(u8, 160), gradient.end_color.a);
824 }
825
826 test "surface command owns and decodes validated text runs" {
827 const content = "A\u{00e9}B";
828 const styles = [_]command.CommandUiTextStyle{
829 .{ .font_asset_id = 12, .point_size = 18 },
830 };
831 const runs = [_]command.CommandUiTextRun{
832 .{
833 .byte_start = 0,
834 .byte_end = 1,
835 .style_slot = 1,
836 .foreground = .{
837 .is_set = 1,
838 .value = .{ .r = 10, .g = 20, .b = 30, .a = 220 },
839 },
840 .background = std.mem.zeroes(command.CommandUiOptionalColor),
841 .underline = 1,
842 .strikethrough = 0,
843 },
844 .{
845 .byte_start = 1,
846 .byte_end = 3,
847 .style_slot = 0,
848 .foreground = std.mem.zeroes(command.CommandUiOptionalColor),
849 .background = .{
850 .is_set = 1,
851 .value = .{ .r = 40, .g = 50, .b = 60, .a = 180 },
852 },
853 .underline = 0,
854 .strikethrough = 1,
855 },
856 };
857 var text = std.mem.zeroes(command.CommandUiText);
858 text.content_ptr = content.ptr;
859 text.content_len = content.len;
860 text.runs_ptr = &runs;
861 text.runs_len = runs.len;
862 text.styles_ptr = &styles;
863 text.styles_len = styles.len;
864 var root = std.mem.zeroes(command.CommandUiNode);
865 root.widget_id = 9;
866 root.kind = 1;
867 root.text_ptr = &text;
868 const source = command.CommandUiSurface{
869 .available_width = 80,
870 .available_height = 20,
871 .root_ptr = &root,
872 .images_ptr = null,
873 .images_len = 0,
874 };
875 const facts = try survey.commandSurface(&source);
876 try std.testing.expectEqual(@as(usize, 2), facts.runs);
877 try std.testing.expectEqual(@as(usize, 1), facts.styles);
878 var storage = try Storage.init(std.testing.allocator, .{
879 .nodes = facts.nodes,
880 .runs = facts.runs,
881 .styles = facts.styles,
882 .semantic_bytes = facts.semantic_bytes,
883 });
884 defer storage.deinit(std.testing.allocator);
885 storage.activate();
886 const cloned = try storage.cloneCommandSurface(&source);
887 try std.testing.expectEqualStrings(content, cloned.tree.root.text.?.content);
888 try std.testing.expectEqual(@as(usize, 2), cloned.tree.root.text.?.runs.len);
889 try std.testing.expectEqual(@as(u64, 12), cloned.tree.root.text.?.styles[0].font_asset_id);
890 try std.testing.expectEqual(@as(f64, 18), cloned.tree.root.text.?.styles[0].point_size);
891 try std.testing.expectEqual(@as(u16, 1), cloned.tree.root.text.?.runs[0].style_slot);
892 try std.testing.expectEqual(@as(u8, 10), cloned.tree.root.text.?.runs[0].foreground.?.r);
893 try std.testing.expect(cloned.tree.root.text.?.runs[0].underline);
894 try std.testing.expectEqual(@as(u8, 180), cloned.tree.root.text.?.runs[1].background.?.a);
895 try std.testing.expect(cloned.tree.root.text.?.runs[1].strikethrough);
896
897 var invalid_runs = runs;
898 invalid_runs[1].byte_start = 2;
899 text.runs_ptr = &invalid_runs;
900 try std.testing.expectError(error.InvalidCommand, storage.cloneCommandSurface(&source));
901 try std.testing.expectEqual(@as(u64, 9), storage.current().?.tree.root.widget_id);
902 try std.testing.expectEqual(@as(u8, 10), storage.current().?.tree.root.text.?.runs[0].foreground.?.r);
903 }
904
905 test "surface clone owns image descriptors and pixels across failed replacement" {
906 comptime {
907 @stardustClaim(
908 @import("alloc_phase").capacity.witness(Storage, "gui_surface_clone_images"),
909 null,
910 null,
911 null,
912 null,
913 null,
914 null,
915 );
916 }
917
918 var source_pixels = [_]u32{ 0xff00_00ff, 0xff00_ff00, 0xffff_0000, 0xffff_ffff };
919 var source_images = [_]UiImage{.{
920 .width = 2,
921 .height = 2,
922 .pixels = &source_pixels,
923 }};
924 const tree = UiSurfaceTree{
925 .available_size = .{ .width = 12, .height = 12 },
926 .root = .{
927 .widget_id = 4,
928 .paint = .{ .image = .{
929 .index = 0,
930 .source = .{ .x = 0.5, .y = 1, .width = 1, .height = 1 },
931 .opacity = 190,
932 } },
933 },
934 };
935 var storage = try Storage.init(std.testing.allocator, .{
936 .nodes = 1,
937 .images = 1,
938 .image_pixels = 4,
939 .semantic_bytes = 0,
940 });
941 defer storage.deinit(std.testing.allocator);
942 storage.activate();
943 const cloned = try storage.cloneSurface(.{
944 .tree = &tree,
945 .images = .{ .images = &source_images },
946 });
947 source_pixels[0] = 0;
948 source_images[0].width = 1;
949 try std.testing.expectEqual(@as(usize, 1), cloned.images.images.len);
950 try std.testing.expectEqual(@as(u32, 2), cloned.images.images[0].width);
951 try std.testing.expectEqual(@as(u32, 0xff00_00ff), cloned.images.images[0].pixels[0]);
952 try std.testing.expectEqual(@as(u8, 190), cloned.tree.root.paint.image.?.opacity);
953
954 const oversized_pixels = @as([5]u32, @splat(0));
955 const oversized_images = [_]UiImage{.{
956 .width = 5,
957 .height = 1,
958 .pixels = &oversized_pixels,
959 }};
960 try std.testing.expectError(error.SurfaceCapacityExceeded, storage.cloneSurface(.{
961 .tree = &tree,
962 .images = .{ .images = &oversized_images },
963 }));
964 try std.testing.expectEqual(@as(u32, 0xff00_00ff), storage.current().?.images.images[0].pixels[0]);
965
966 const descriptor_pixels = [_]u32{ 1, 2 };
967 const too_many_images = [_]UiImage{
968 .{ .width = 1, .height = 1, .pixels = descriptor_pixels[0..1] },
969 .{ .width = 1, .height = 1, .pixels = descriptor_pixels[1..2] },
970 };
971 try std.testing.expectError(error.SurfaceCapacityExceeded, storage.cloneSurface(.{
972 .tree = &tree,
973 .images = .{ .images = &too_many_images },
974 }));
975 try std.testing.expectEqual(@as(u32, 0xff00_00ff), storage.current().?.images.images[0].pixels[0]);
976 }
977
978 test "surface command owns image pixels and normalizes image paint" {
979 comptime {
980 @stardustClaim(
981 @import("alloc_phase").capacity.witness(Storage, "gui_surface_clone_command_images_overload"),
982 null,
983 null,
984 null,
985 null,
986 null,
987 null,
988 );
989 }
990 comptime {
991 @stardustClaim(
992 @import("alloc_phase").capacity.witness(Storage, "gui_surface_clone_command_images_foreign_risk"),
993 null,
994 null,
995 null,
996 null,
997 null,
998 null,
999 );
1000 }
1001
1002 var source_pixels = [_]u32{ 0xff10_2030, 0xff40_5060, 0xff70_8090, 0xffa0_b0c0 };
1003 var source_image = command.CommandUiImage{
1004 .width = 2,
1005 .height = 2,
1006 .pixels_ptr = &source_pixels,
1007 .pixels_len = source_pixels.len,
1008 };
1009 var root = std.mem.zeroes(command.CommandUiNode);
1010 root.widget_id = 8;
1011 root.paint.image = .{
1012 .is_set = 1,
1013 .index = 0,
1014 .source = .{
1015 .x = std.math.nan(f32),
1016 .y = 0.5,
1017 .width = std.math.inf(f32),
1018 .height = 1,
1019 },
1020 .tint = .{
1021 .is_set = 1,
1022 .value = .{ .r = 20, .g = 40, .b = 60, .a = 180 },
1023 },
1024 .opacity = 210,
1025 };
1026 var source = command.CommandUiSurface{
1027 .available_width = 12,
1028 .available_height = 12,
1029 .root_ptr = &root,
1030 .images_ptr = @ptrCast(&source_image),
1031 .images_len = 1,
1032 };
1033 const facts = try survey.commandSurface(&source);
1034 var storage = try Storage.init(std.testing.allocator, .{
1035 .nodes = facts.nodes,
1036 .images = facts.images,
1037 .image_pixels = facts.image_pixels,
1038 .semantic_bytes = facts.semantic_bytes,
1039 });
1040 defer storage.deinit(std.testing.allocator);
1041 storage.activate();
1042 const cloned = try storage.cloneCommandSurface(&source);
1043 source_pixels[0] = 0;
1044 source_image.width = 1;
1045 try std.testing.expectEqual(@as(u32, 2), cloned.images.images[0].width);
1046 try std.testing.expectEqual(@as(u32, 0xff10_2030), cloned.images.images[0].pixels[0]);
1047 const paint = cloned.tree.root.paint.image.?;
1048 try std.testing.expectEqual(@as(f32, 0), paint.source.x);
1049 try std.testing.expectEqual(@as(f32, 0), paint.source.width);
1050 try std.testing.expectEqual(@as(f32, 1), paint.source.height);
1051 try std.testing.expectEqual(@as(u8, 20), paint.tint.?.r);
1052 try std.testing.expectEqual(@as(u8, 210), paint.opacity);
1053
1054 source_image.width = 2;
1055 const second_pixels = [_]u32{1};
1056 const too_many_images = [_]command.CommandUiImage{
1057 source_image,
1058 .{
1059 .width = 1,
1060 .height = 1,
1061 .pixels_ptr = &second_pixels,
1062 .pixels_len = second_pixels.len,
1063 },
1064 };
1065 source.images_ptr = &too_many_images;
1066 source.images_len = too_many_images.len;
1067 try std.testing.expectError(error.SurfaceCapacityExceeded, storage.cloneCommandSurface(&source));
1068 try std.testing.expectEqual(@as(u32, 0xff10_2030), storage.current().?.images.images[0].pixels[0]);
1069
1070 const oversized_pixels = [_]u32{ 1, 2, 3, 4, 5 };
1071 const oversized_image = command.CommandUiImage{
1072 .width = 5,
1073 .height = 1,
1074 .pixels_ptr = &oversized_pixels,
1075 .pixels_len = oversized_pixels.len,
1076 };
1077 source.images_ptr = @ptrCast(&oversized_image);
1078 source.images_len = 1;
1079 try std.testing.expectError(error.SurfaceCapacityExceeded, storage.cloneCommandSurface(&source));
1080 try std.testing.expectEqual(@as(u32, 0xff10_2030), storage.current().?.images.images[0].pixels[0]);
1081 }
1082
1083 test "surface clone maximum placement makes no backing allocation" {
1084 comptime {
1085 @stardustClaim(
1086 @import("alloc_phase").capacity.witness(Storage, "gui_surface_clone_sealed"),
1087 null,
1088 null,
1089 null,
1090 null,
1091 null,
1092 null,
1093 );
1094 }
1095
1096 const children = [_]UiNode{
1097 .{ .widget_id = 2, .text = .{ .content = "abc" }, .action = "def" },
1098 .{ .widget_id = 3, .role = "ghi" },
1099 };
1100 const surface = UiSurfaceTree{
1101 .available_size = .{ .width = 10, .height = 10 },
1102 .root = .{
1103 .widget_id = 1,
1104 .paint = .{ .image = .{ .index = 0 } },
1105 .children = &children,
1106 },
1107 };
1108 const image_pixels = [_]u32{ 0xff00_00ff, 0xff00_ff00, 0xffff_0000, 0xffff_ffff };
1109 const images = [_]UiImage{.{
1110 .width = 2,
1111 .height = 2,
1112 .pixels = &image_pixels,
1113 }};
1114 const source = Surface{
1115 .tree = &surface,
1116 .images = .{ .images = &images },
1117 };
1118 const facts = try survey.surface(source);
1119 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
1120 var storage = try Storage.init(counting.allocator(), .{
1121 .nodes = facts.nodes,
1122 .runs = facts.runs,
1123 .styles = facts.styles,
1124 .images = facts.images,
1125 .image_pixels = facts.image_pixels,
1126 .semantic_bytes = facts.semantic_bytes,
1127 });
1128 defer storage.deinit(counting.allocator());
1129 storage.activate();
1130 const allocations = counting.alloc_index;
1131 const bytes = counting.allocated_bytes;
1132 _ = try storage.cloneSurface(source);
1133 _ = try storage.cloneSurface(source);
1134 try std.testing.expectEqual(allocations, counting.alloc_index);
1135 try std.testing.expectEqual(bytes, counting.allocated_bytes);
1136 }
1137
1138 comptime {
1139 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
1140 }