lib/gui/src/paint/retained.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const gui = @import("../root.zig");
4 const paint = @import("root.zig");
5
6 const Allocator = std.mem.Allocator;
7 const Command = paint.command.Command;
8 const CommandBuffer = paint.command.CommandBuffer;
9 const Fragment = paint.command.Fragment;
10 const FragmentId = paint.command.FragmentId;
11 const Rect = gui.layout.Rect;
12 const Region = paint.cpu.Region;
13 const RegionSet = paint.regions.Set;
14
15 pub const Damage = union(enum) {
16 semantic,
17 narrowed: RegionSet,
18 };
19
20 pub const Commands = struct {
21 pub const Error = Allocator.Error || error{
22 DuplicateFragmentId,
23 TooManyFragments,
24 UnfragmentedCommands,
25 };
26
27 allocator: Allocator,
28 previous: CommandBuffer,
29 previous_index: std.AutoHashMapUnmanaged(FragmentId, u32) = .empty,
30 current_index: std.AutoHashMapUnmanaged(FragmentId, u32) = .empty,
31 seen_previous: std.ArrayListUnmanaged(bool) = .empty,
32 indexed_current: ?*const CommandBuffer = null,
33 indexed_command_count: usize = 0,
34 indexed_fragment_count: usize = 0,
35 valid: bool = false,
36
37 pub fn init(allocator: Allocator) Commands {
38 return .{
39 .allocator = allocator,
40 .previous = CommandBuffer.init(allocator),
41 };
42 }
43
44 pub fn deinit(self: *Commands) void {
45 self.previous.deinit();
46 self.previous_index.deinit(self.allocator);
47 self.current_index.deinit(self.allocator);
48 self.seen_previous.deinit(self.allocator);
49 self.* = undefined;
50 }
51
52 pub fn invalidate(self: *Commands) void {
53 self.cancelPrepared();
54 self.valid = false;
55 }
56
57 pub fn hasBaseline(self: *const Commands) bool {
58 return self.valid;
59 }
60
61 pub fn diff(
62 self: *Commands,
63 current: *const CommandBuffer,
64 limits: []const Region,
65 width: u32,
66 height: u32,
67 ) Error!Damage {
68 if (!self.valid) return .semantic;
69 try requireComplete(&self.previous);
70 try requireComplete(current);
71 try self.indexCurrentFragments(current);
72 try self.seen_previous.resize(
73 self.allocator,
74 self.previous.fragmentItems().len,
75 );
76 @memset(self.seen_previous.items, false);
77 if (!orderedLikePrevious(
78 self.previous_index,
79 current.fragmentItems(),
80 )) return .semantic;
81 const fragment_damage = try self.fragmentDamage(
82 current,
83 limits,
84 width,
85 height,
86 );
87 const command_damage = commandDamage(
88 &self.previous,
89 current,
90 limits,
91 width,
92 height,
93 );
94 return narrowerDamage(fragment_damage, command_damage);
95 }
96
97 pub fn retain(
98 self: *Commands,
99 current: *CommandBuffer,
100 ) (CommandBuffer.Error || Error)!void {
101 try self.prepare(current);
102 self.commit(current);
103 }
104
105 pub fn prepare(
106 self: *Commands,
107 current: *const CommandBuffer,
108 ) Error!void {
109 try requireComplete(current);
110 if (!self.currentIsIndexed(current)) {
111 try self.indexCurrentFragments(current);
112 }
113 try self.previous.ensureCapacity(current.items().len);
114 try self.previous.ensureFragmentCapacity(current.fragmentItems().len);
115 }
116
117 pub fn commit(
118 self: *Commands,
119 current: *CommandBuffer,
120 ) void {
121 std.debug.assert(current.fragmentsComplete());
122 std.debug.assert(self.previous.commands.capacity >= current.items().len);
123 std.debug.assert(
124 self.previous.fragments.capacity >= current.fragmentItems().len,
125 );
126 std.debug.assert(self.currentIsIndexed(current));
127 std.mem.swap(
128 std.AutoHashMapUnmanaged(FragmentId, u32),
129 &self.previous_index,
130 &self.current_index,
131 );
132 self.cancelPrepared();
133 std.mem.swap(CommandBuffer, &self.previous, current);
134 current.reset();
135 self.valid = true;
136 }
137
138 pub fn cancelPrepared(self: *Commands) void {
139 self.indexed_current = null;
140 self.indexed_command_count = 0;
141 self.indexed_fragment_count = 0;
142 }
143
144 fn indexCurrentFragments(
145 self: *Commands,
146 current: *const CommandBuffer,
147 ) Error!void {
148 self.cancelPrepared();
149 try reserveMap(
150 &self.current_index,
151 self.allocator,
152 current.fragmentItems().len,
153 );
154 self.current_index.clearRetainingCapacity();
155 for (current.fragmentItems(), 0..) |fragment, index| {
156 const value = std.math.cast(u32, index) orelse
157 return error.TooManyFragments;
158 try insertUnique(
159 &self.current_index,
160 self.allocator,
161 fragment.id,
162 value,
163 );
164 }
165 self.indexed_current = current;
166 self.indexed_command_count = current.items().len;
167 self.indexed_fragment_count = current.fragmentItems().len;
168 }
169
170 fn currentIsIndexed(
171 self: *const Commands,
172 current: *const CommandBuffer,
173 ) bool {
174 return self.indexed_current == current and
175 self.indexed_command_count == current.items().len and
176 self.indexed_fragment_count == current.fragmentItems().len;
177 }
178
179 fn fragmentDamage(
180 self: *Commands,
181 current: *const CommandBuffer,
182 limits: []const Region,
183 width: u32,
184 height: u32,
185 ) Error!Damage {
186 var regions = RegionSet{};
187 var changed = false;
188 for (current.fragmentItems()) |fragment| {
189 const previous_index = self.previous_index.get(fragment.id);
190 if (previous_index == null) {
191 changed = true;
192 includeFragment(®ions, fragment, limits, width, height);
193 continue;
194 }
195 self.seen_previous.items[previous_index.?] = true;
196 const old = self.previous.fragmentItems()[previous_index.?];
197 if (fragmentsEqual(&self.previous, old, current, fragment)) {
198 continue;
199 }
200 changed = true;
201 includeChangedCommands(
202 ®ions,
203 &self.previous,
204 old,
205 current,
206 fragment,
207 limits,
208 width,
209 height,
210 );
211 }
212 changed = includeRemoved(self, ®ions, limits, width, height) or changed;
213 if (!changed) return .semantic;
214 return .{ .narrowed = regions };
215 }
216 };
217
218 fn reserveMap(
219 map: anytype,
220 allocator: Allocator,
221 count: usize,
222 ) Allocator.Error!void {
223 const capacity = std.math.cast(u32, count) orelse
224 return error.OutOfMemory;
225 try map.ensureTotalCapacity(allocator, capacity);
226 }
227
228 fn insertUnique(
229 map: anytype,
230 allocator: Allocator,
231 id: FragmentId,
232 value: anytype,
233 ) Commands.Error!void {
234 const entry = try map.getOrPut(allocator, id);
235 if (entry.found_existing) return error.DuplicateFragmentId;
236 entry.value_ptr.* = value;
237 }
238
239 fn orderedLikePrevious(
240 previous: std.AutoHashMapUnmanaged(FragmentId, u32),
241 current: []const Fragment,
242 ) bool {
243 var last: ?u32 = null;
244 for (current) |fragment| {
245 const index = previous.get(fragment.id) orelse continue;
246 if (last) |prior| {
247 if (index <= prior) return false;
248 }
249 last = index;
250 }
251 return true;
252 }
253
254 fn includeRemoved(
255 self: *Commands,
256 regions: *RegionSet,
257 limits: []const Region,
258 width: u32,
259 height: u32,
260 ) bool {
261 var changed = false;
262 for (self.previous.fragmentItems(), 0..) |fragment, index| {
263 if (self.seen_previous.items[index]) continue;
264 includeFragment(regions, fragment, limits, width, height);
265 changed = true;
266 }
267 return changed;
268 }
269
270 fn includeFragment(
271 regions: *RegionSet,
272 fragment: Fragment,
273 limits: []const Region,
274 width: u32,
275 height: u32,
276 ) void {
277 includeRect(regions, fragment.bounds, limits, width, height);
278 }
279
280 fn includeCommand(
281 regions: *RegionSet,
282 value: Command,
283 limits: []const Region,
284 width: u32,
285 height: u32,
286 ) void {
287 includeRect(
288 regions,
289 paint.command.visibleBounds(value),
290 limits,
291 width,
292 height,
293 );
294 }
295
296 fn includeRect(
297 regions: *RegionSet,
298 rect: Rect,
299 limits: []const Region,
300 width: u32,
301 height: u32,
302 ) void {
303 const raw = paint.regions.fromRect(rect, 1, width, height) orelse return;
304 for (limits) |limit| {
305 const clipped = paint.regions.intersect(raw, limit);
306 if (clipped.pixelCount() != 0) regions.append(clipped);
307 }
308 }
309
310 fn includeChangedCommands(
311 regions: *RegionSet,
312 previous: *const CommandBuffer,
313 old: Fragment,
314 current: *const CommandBuffer,
315 new: Fragment,
316 limits: []const Region,
317 width: u32,
318 height: u32,
319 ) void {
320 const old_commands = fragmentCommands(previous, old);
321 const new_commands = fragmentCommands(current, new);
322 const count = @max(old_commands.len, new_commands.len);
323 for (0..count) |index| {
324 const before = if (index < old_commands.len)
325 old_commands[index]
326 else
327 null;
328 const after = if (index < new_commands.len)
329 new_commands[index]
330 else
331 null;
332 if (before != null and
333 after != null and
334 paint.command.visuallyEqual(before.?, after.?))
335 {
336 continue;
337 }
338 if (before) |value| {
339 includeCommand(regions, value, limits, width, height);
340 }
341 if (after) |value| {
342 includeCommand(regions, value, limits, width, height);
343 }
344 }
345 }
346
347 fn commandDamage(
348 previous: *const CommandBuffer,
349 current: *const CommandBuffer,
350 limits: []const Region,
351 width: u32,
352 height: u32,
353 ) Damage {
354 var regions = RegionSet{};
355 var changed = false;
356 const count = @max(previous.items().len, current.items().len);
357 for (0..count) |index| {
358 const before = if (index < previous.items().len)
359 previous.items()[index]
360 else
361 null;
362 const after = if (index < current.items().len)
363 current.items()[index]
364 else
365 null;
366 if (before != null and
367 after != null and
368 paint.command.visuallyEqual(before.?, after.?))
369 {
370 continue;
371 }
372 changed = true;
373 if (before) |value| {
374 includeCommand(®ions, value, limits, width, height);
375 }
376 if (after) |value| {
377 includeCommand(®ions, value, limits, width, height);
378 }
379 }
380 if (!changed) return .semantic;
381 return .{ .narrowed = regions };
382 }
383
384 fn narrowerDamage(fragment: Damage, command: Damage) Damage {
385 if (command == .semantic) return .semantic;
386 if (fragment == .semantic) return command;
387 const fragment_area = damageArea(fragment.narrowed);
388 const command_area = damageArea(command.narrowed);
389 return if (fragment_area <= command_area) fragment else command;
390 }
391
392 fn damageArea(regions: RegionSet) usize {
393 return if (regions.bounding()) |region| region.pixelCount() else 0;
394 }
395
396 fn fragmentsEqual(
397 previous: *const CommandBuffer,
398 old: Fragment,
399 current: *const CommandBuffer,
400 new: Fragment,
401 ) bool {
402 if (old.command_count != new.command_count) return false;
403 const old_commands = fragmentCommands(previous, old);
404 const new_commands = fragmentCommands(current, new);
405 for (old_commands, new_commands) |left, right| {
406 if (!paint.command.visuallyEqual(left, right)) return false;
407 }
408 return true;
409 }
410
411 fn fragmentCommands(
412 buffer: *const CommandBuffer,
413 fragment: Fragment,
414 ) []const Command {
415 const start: usize = fragment.command_start;
416 const end = start + fragment.command_count;
417 std.debug.assert(end <= buffer.items().len);
418 return buffer.items()[start..end];
419 }
420
421 fn requireComplete(
422 buffer: *const CommandBuffer,
423 ) error{UnfragmentedCommands}!void {
424 if (!buffer.fragmentsComplete()) return error.UnfragmentedCommands;
425 }
426
427 fn testId(value: u64) FragmentId {
428 return .{
429 .root_id = 1,
430 .element_id = value,
431 .namespace = 99,
432 .part = 1,
433 };
434 }
435
436 fn appendTestFragment(
437 buffer: *CommandBuffer,
438 id: FragmentId,
439 x: f32,
440 color: u8,
441 ) !void {
442 const start = buffer.items().len;
443 try buffer.append(.{
444 .kind = .fill,
445 .rect = .{ .x = x, .width = 8, .height = 8 },
446 .clip = .{ .width = 128, .height = 32 },
447 .color = .{ .r = color, .a = 255 },
448 });
449 try buffer.commitFragment(id, start);
450 }
451
452 fn appendTestCommandFragment(
453 buffer: *CommandBuffer,
454 id: FragmentId,
455 value: Command,
456 ) !void {
457 const start = buffer.items().len;
458 try buffer.append(value);
459 try buffer.commitFragment(id, start);
460 }
461
462 fn appendTestPairFragment(
463 buffer: *CommandBuffer,
464 id: FragmentId,
465 second_color: u8,
466 ) !void {
467 const start = buffer.items().len;
468 try buffer.append(.{
469 .kind = .fill,
470 .rect = .{ .x = 4, .width = 8, .height = 8 },
471 .clip = .{ .width = 128, .height = 32 },
472 .color = .{ .r = 10, .a = 255 },
473 });
474 try buffer.append(.{
475 .kind = .fill,
476 .rect = .{ .x = 80, .width = 8, .height = 8 },
477 .clip = .{ .width = 128, .height = 32 },
478 .color = .{ .r = second_color, .a = 255 },
479 });
480 try buffer.commitFragment(id, start);
481 }
482
483 test "retained insertion damages only the inserted fragment" {
484 var retained = Commands.init(std.testing.allocator);
485 defer retained.deinit();
486 var current = CommandBuffer.init(std.testing.allocator);
487 defer current.deinit();
488 try appendTestFragment(¤t, testId(1), 20, 10);
489 try appendTestFragment(¤t, testId(2), 40, 20);
490 try retained.retain(¤t);
491 try appendTestFragment(¤t, testId(9), 2, 90);
492 try appendTestFragment(¤t, testId(1), 20, 10);
493 try appendTestFragment(¤t, testId(2), 40, 20);
494
495 const damage = try retained.diff(
496 ¤t,
497 &.{Region.full(128, 32)},
498 128,
499 32,
500 );
501
502 try std.testing.expectEqual(
503 Region{ .x = 1, .width = 10, .height = 9 },
504 damage.narrowed.bounding().?,
505 );
506 }
507
508 test "retained matched fragment damages only changed commands" {
509 var retained = Commands.init(std.testing.allocator);
510 defer retained.deinit();
511 var current = CommandBuffer.init(std.testing.allocator);
512 defer current.deinit();
513 try appendTestPairFragment(¤t, testId(1), 20);
514 try retained.retain(¤t);
515 try appendTestPairFragment(¤t, testId(1), 30);
516
517 const damage = try retained.diff(
518 ¤t,
519 &.{Region.full(128, 32)},
520 128,
521 32,
522 );
523
524 try std.testing.expectEqual(
525 Region{ .x = 79, .width = 10, .height = 9 },
526 damage.narrowed.bounding().?,
527 );
528 }
529
530 test "retained identity churn keeps narrower positional command damage" {
531 var retained = Commands.init(std.testing.allocator);
532 defer retained.deinit();
533 var current = CommandBuffer.init(std.testing.allocator);
534 defer current.deinit();
535 try appendTestPairFragment(¤t, testId(1), 20);
536 try retained.retain(¤t);
537 try appendTestPairFragment(¤t, testId(2), 30);
538
539 const damage = try retained.diff(
540 ¤t,
541 &.{Region.full(128, 32)},
542 128,
543 32,
544 );
545
546 try std.testing.expectEqual(
547 Region{ .x = 79, .width = 10, .height = 9 },
548 damage.narrowed.bounding().?,
549 );
550 }
551
552 test "retained reorder preserves semantic damage" {
553 var retained = Commands.init(std.testing.allocator);
554 defer retained.deinit();
555 var current = CommandBuffer.init(std.testing.allocator);
556 defer current.deinit();
557 try appendTestFragment(¤t, testId(1), 0, 10);
558 try appendTestFragment(¤t, testId(2), 4, 20);
559 try retained.retain(¤t);
560 try appendTestFragment(¤t, testId(2), 4, 20);
561 try appendTestFragment(¤t, testId(1), 0, 10);
562
563 const damage = try retained.diff(
564 ¤t,
565 &.{Region.full(32, 16)},
566 32,
567 16,
568 );
569
570 try std.testing.expect(damage == .semantic);
571 }
572
573 test "retained duplicate fragment identities reject" {
574 var retained = Commands.init(std.testing.allocator);
575 defer retained.deinit();
576 var current = CommandBuffer.init(std.testing.allocator);
577 defer current.deinit();
578 try appendTestFragment(¤t, testId(1), 0, 10);
579 try retained.retain(¤t);
580 try appendTestFragment(¤t, testId(2), 0, 10);
581 try appendTestFragment(¤t, testId(2), 16, 20);
582
583 try std.testing.expectError(
584 error.DuplicateFragmentId,
585 retained.diff(¤t, &.{Region.full(32, 16)}, 32, 16),
586 );
587 current.reset();
588 try appendTestFragment(¤t, testId(3), 8, 30);
589 try retained.retain(¤t);
590 try std.testing.expect(retained.hasBaseline());
591 }
592
593 test "retained baseline rejects duplicate fragment identities" {
594 var retained = Commands.init(std.testing.allocator);
595 defer retained.deinit();
596 var current = CommandBuffer.init(std.testing.allocator);
597 defer current.deinit();
598 try appendTestFragment(¤t, testId(2), 0, 10);
599 try appendTestFragment(¤t, testId(2), 16, 20);
600
601 try std.testing.expectError(
602 error.DuplicateFragmentId,
603 retained.retain(¤t),
604 );
605 try std.testing.expect(!retained.hasBaseline());
606 }
607
608 test "retained damage includes removed effects and current geometry" {
609 var retained = Commands.init(std.testing.allocator);
610 defer retained.deinit();
611 var current = CommandBuffer.init(std.testing.allocator);
612 defer current.deinit();
613 try appendTestCommandFragment(¤t, testId(1), .{
614 .kind = .shadow,
615 .rect = .{ .x = 10, .y = 10, .width = 10, .height = 10 },
616 .clip = .{ .width = 64, .height = 64 },
617 .color = .{ .a = 255 },
618 .width = 4,
619 });
620 try retained.retain(¤t);
621 try appendTestCommandFragment(¤t, testId(1), .{
622 .kind = .fill,
623 .rect = .{ .x = 20, .y = 20, .width = 10, .height = 10 },
624 .clip = .{ .width = 64, .height = 64 },
625 .color = .{ .a = 255 },
626 });
627
628 const damage = try retained.diff(
629 ¤t,
630 &.{Region.full(64, 64)},
631 64,
632 64,
633 );
634
635 try std.testing.expectEqual(
636 Region{ .x = 5, .y = 5, .width = 26, .height = 26 },
637 damage.narrowed.bounding().?,
638 );
639 }
640
641 test "retained clip shrink damages pixels uncovered by the old clip" {
642 var retained = Commands.init(std.testing.allocator);
643 defer retained.deinit();
644 var current = CommandBuffer.init(std.testing.allocator);
645 defer current.deinit();
646 try appendTestCommandFragment(¤t, testId(1), .{
647 .kind = .fill,
648 .rect = .{ .width = 16, .height = 8 },
649 .clip = .{ .width = 16, .height = 8 },
650 .color = .{ .a = 255 },
651 });
652 try retained.retain(¤t);
653 try appendTestCommandFragment(¤t, testId(1), .{
654 .kind = .fill,
655 .rect = .{ .width = 16, .height = 8 },
656 .clip = .{ .x = 4, .width = 12, .height = 8 },
657 .color = .{ .a = 255 },
658 });
659
660 const damage = try retained.diff(
661 ¤t,
662 &.{Region.full(32, 16)},
663 32,
664 16,
665 );
666
667 try std.testing.expect(damage == .narrowed);
668 try std.testing.expectEqual(
669 Region{ .width = 17, .height = 9 },
670 damage.narrowed.bounding().?,
671 );
672 }
673
674 test "retained linear gradient changes damage its widget bounds" {
675 const base = Command{
676 .kind = .linear_gradient,
677 .rect = .{ .x = 8, .y = 4, .width = 24, .height = 12 },
678 .clip = .{ .width = 64, .height = 32 },
679 .color = .{ .r = 20, .g = 40, .b = 80, .a = 255 },
680 .color_end = .{ .r = 80, .g = 140, .b = 220, .a = 255 },
681 .gradient_start = .{ .x = 8, .y = 4 },
682 .gradient_end = .{ .x = 32, .y = 16 },
683 .radius = 4,
684 };
685 var retained = Commands.init(std.testing.allocator);
686 defer retained.deinit();
687 var current = CommandBuffer.init(std.testing.allocator);
688 defer current.deinit();
689 try appendTestCommandFragment(¤t, testId(1), base);
690 try retained.retain(¤t);
691 var changed = base;
692 changed.color_end.g = 180;
693 changed.gradient_end.y = 4;
694 try appendTestCommandFragment(¤t, testId(1), changed);
695
696 const damage = try retained.diff(
697 ¤t,
698 &.{Region.full(64, 32)},
699 64,
700 32,
701 );
702
703 try std.testing.expect(damage == .narrowed);
704 try std.testing.expectEqual(
705 Region{ .x = 7, .y = 3, .width = 26, .height = 14 },
706 damage.narrowed.bounding().?,
707 );
708 }
709
710 test "retained equal commands preserve caller semantic damage" {
711 var retained = Commands.init(std.testing.allocator);
712 defer retained.deinit();
713 var current = CommandBuffer.init(std.testing.allocator);
714 defer current.deinit();
715 try appendTestFragment(¤t, testId(1), 4, 10);
716 try retained.retain(¤t);
717 try appendTestFragment(¤t, testId(1), 4, 10);
718
719 const damage = try retained.diff(
720 ¤t,
721 &.{Region{ .x = 2, .width = 12, .height = 8 }},
722 32,
723 16,
724 );
725
726 try std.testing.expect(damage == .semantic);
727 }
728
729 test "retained semantic damage repaints changed image content" {
730 const red = [_]u32{paint.packRgba(.{ .r = 255, .a = 255 })};
731 const blue = [_]u32{paint.packRgba(.{ .b = 255, .a = 255 })};
732 const old_images = paint.ImageSet{
733 .images = &.{.{ .width = 1, .height = 1, .pixels = &red }},
734 };
735 const new_images = paint.ImageSet{
736 .images = &.{.{ .width = 1, .height = 1, .pixels = &blue }},
737 };
738 const image_command = Command{
739 .kind = .image,
740 .rect = .{ .width = 1, .height = 1 },
741 .clip = .{ .width = 1, .height = 1 },
742 .color = .{ .r = 255, .g = 255, .b = 255, .a = 255 },
743 };
744 var retained = Commands.init(std.testing.allocator);
745 defer retained.deinit();
746 var current = CommandBuffer.init(std.testing.allocator);
747 defer current.deinit();
748 try appendTestCommandFragment(¤t, testId(1), image_command);
749 var actual = [_]u32{0};
750 try paint.renderCommandsPackedWithImages(
751 current.items(),
752 .{ .width = 1, .height = 1, .pixels = &actual },
753 .{ .a = 255 },
754 old_images,
755 );
756 try retained.retain(¤t);
757 try appendTestCommandFragment(¤t, testId(1), image_command);
758
759 const semantic = Region.full(1, 1);
760 const damage = try retained.diff(
761 ¤t,
762 &.{semantic},
763 1,
764 1,
765 );
766 try std.testing.expect(damage == .semantic);
767 try paint.renderCommandsPackedRegionWithImages(
768 current.items(),
769 .{ .width = 1, .height = 1, .pixels = &actual },
770 .{ .a = 255 },
771 new_images,
772 semantic,
773 );
774 var expected = [_]u32{0};
775 try paint.renderCommandsPackedWithImages(
776 current.items(),
777 .{ .width = 1, .height = 1, .pixels = &expected },
778 .{ .a = 255 },
779 new_images,
780 );
781 try std.testing.expectEqualSlices(u32, &expected, &actual);
782 }
783
784 test "retained preflight failure preserves a reusable baseline" {
785 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
786 const allocator = failing.allocator();
787 var retained = Commands.init(allocator);
788 defer retained.deinit();
789 var current = CommandBuffer.init(allocator);
790 defer current.deinit();
791 try appendTestFragment(¤t, testId(1), 0, 10);
792 try retained.retain(¤t);
793 for (0..16) |index| {
794 try appendTestFragment(
795 ¤t,
796 testId(@intCast(index + 2)),
797 @floatFromInt(index * 4),
798 @intCast(index + 20),
799 );
800 }
801
802 failing.fail_index = failing.alloc_index;
803 failing.resize_fail_index = failing.resize_index;
804 try std.testing.expectError(error.OutOfMemory, retained.prepare(¤t));
805 try std.testing.expect(failing.has_induced_failure);
806 try std.testing.expect(retained.hasBaseline());
807
808 failing.fail_index = std.math.maxInt(usize);
809 failing.resize_fail_index = std.math.maxInt(usize);
810 const damage = try retained.diff(
811 ¤t,
812 &.{Region.full(128, 32)},
813 128,
814 32,
815 );
816 try std.testing.expect(damage == .narrowed);
817 try retained.retain(¤t);
818 try std.testing.expect(retained.hasBaseline());
819 }
820
821 test "retained command bookkeeping needs no allocation after warmup" {
822 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
823 var retained = Commands.init(failing.allocator());
824 defer retained.deinit();
825 var current = CommandBuffer.init(failing.allocator());
826 defer current.deinit();
827
828 try appendTestFragment(¤t, testId(1), 4, 10);
829 try appendTestFragment(¤t, testId(2), 20, 20);
830 try retained.retain(¤t);
831 try appendTestFragment(¤t, testId(1), 4, 10);
832 try appendTestFragment(¤t, testId(2), 20, 20);
833 _ = try retained.diff(¤t, &.{Region.full(64, 16)}, 64, 16);
834 try retained.retain(¤t);
835
836 failing.fail_index = failing.alloc_index;
837 failing.resize_fail_index = failing.resize_index;
838 for (0..8) |_| {
839 try appendTestFragment(¤t, testId(1), 4, 10);
840 try appendTestFragment(¤t, testId(2), 20, 20);
841 _ = try retained.diff(¤t, &.{Region.full(64, 16)}, 64, 16);
842 try retained.retain(¤t);
843 }
844 try std.testing.expect(!failing.has_induced_failure);
845 }