lib/ui/src/asset/registry.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 
  4 const capacity_mod = @import("capacity.zig");
  5 const model = @import("model.zig");
  6 
  7 const Allocator = std.mem.Allocator;
  8 const AssetHandle = model.AssetHandle;
  9 const Bytes = model.Bytes;
 10 const FontDescriptor = model.FontDescriptor;
 11 const ImageDescriptor = model.ImageDescriptor;
 12 
 13 pub const Registry = struct {
 14     phase: alloc_phase.capacity.Phase,
 15     limits: Limits,
 16     capacity: Capacity,
 17     storage: []align(8) u8,
 18     fonts: []FontDescriptor,
 19     font_bytes: []Bytes,
 20     font_generation: []u32,
 21     font_used: u32,
 22     images: []ImageDescriptor,
 23     image_bytes: []Bytes,
 24     image_generation: []u32,
 25     image_used: u32,
 26     strings: []u8,
 27     owned_bytes: []u8,
 28     font_occupied: []u64,
 29     image_occupied: []u64,
 30     epoch: u64,
 31 
 32     pub const Limits: type = capacity_mod.Limits;
 33     pub const Capacity: type = capacity_mod.Capacity;
 34     pub const Exhaustion: type = model.Error;
 35     pub const InitError: type = Allocator.Error || capacity_mod.Error;
 36     pub const claim: alloc_phase.capacity.Declaration = .{
 37         .source = .{
 38             .id = "ui.asset_table",
 39             .kind = .phase_static,
 40             .limit_source = .caller,
 41             .storage = .{
 42                 .covered = &.{.{
 43                     .id = "asset_table_and_owned_payload_pool",
 44                     .lifetime = .steady,
 45                     .detail = "font and image descriptors, byte views, generation and occupancy arrays, family strings, and the copied payload pool in one allocation",
 46                 }},
 47                 .excluded = &.{
 48                     "borrowed font and image bytes kept alive by the caller",
 49                     "embedded fallback face bytes held in startup static storage",
 50                 },
 51             },
 52             .capacity = .{
 53                 .inputs = &.{
 54                     alloc_phase.capacity.bindInput(Limits, "fonts", "fonts"),
 55                     alloc_phase.capacity.bindInput(Limits, "images", "images"),
 56                     alloc_phase.capacity.bindInput(Limits, "owned_bytes", "owned_bytes"),
 57                 },
 58                 .type_selectors = &.{
 59                     alloc_phase.capacity.bindType(FontDescriptor, "font_descriptor"),
 60                     alloc_phase.capacity.bindType(ImageDescriptor, "image_descriptor"),
 61                     alloc_phase.capacity.bindType(Bytes, "bytes"),
 62                     alloc_phase.capacity.bindType(u32, "generation"),
 63                     alloc_phase.capacity.bindType(u64, "occupancy_word"),
 64                 },
 65                 .nodes = &.{
 66                     .{ .input = 0 },
 67                     .{ .input = 1 },
 68                     .{ .input = 2 },
 69                     .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
 70                     .{ .alignment = .{ .node = 3, .alignment = .{ .concrete_type = 2 } } },
 71                     .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 2 } } },
 72                     .{ .add = .{ .left = 4, .right = 5 } },
 73                     .{ .alignment = .{ .node = 6, .alignment = .{ .concrete_type = 3 } } },
 74                     .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 3 } } },
 75                     .{ .add = .{ .left = 7, .right = 8 } },
 76                     .{ .alignment = .{ .node = 9, .alignment = .{ .concrete_type = 4 } } },
 77                     .{ .constant = 64 },
 78                     .{ .ceiling_division = .{ .left = 0, .right = 11 } },
 79                     .{ .scale = .{ .node = 12, .coefficient = .{ .size_of_concrete_type = 4 } } },
 80                     .{ .add = .{ .left = 10, .right = 13 } },
 81                     .{ .alignment = .{ .node = 14, .alignment = .{ .concrete_type = 1 } } },
 82                     .{ .scale = .{ .node = 1, .coefficient = .{ .size_of_concrete_type = 1 } } },
 83                     .{ .add = .{ .left = 15, .right = 16 } },
 84                     .{ .alignment = .{ .node = 17, .alignment = .{ .concrete_type = 2 } } },
 85                     .{ .scale = .{ .node = 1, .coefficient = .{ .size_of_concrete_type = 2 } } },
 86                     .{ .add = .{ .left = 18, .right = 19 } },
 87                     .{ .alignment = .{ .node = 20, .alignment = .{ .concrete_type = 3 } } },
 88                     .{ .scale = .{ .node = 1, .coefficient = .{ .size_of_concrete_type = 3 } } },
 89                     .{ .add = .{ .left = 21, .right = 22 } },
 90                     .{ .alignment = .{ .node = 23, .alignment = .{ .concrete_type = 4 } } },
 91                     .{ .ceiling_division = .{ .left = 1, .right = 11 } },
 92                     .{ .scale = .{ .node = 25, .coefficient = .{ .size_of_concrete_type = 4 } } },
 93                     .{ .add = .{ .left = 24, .right = 26 } },
 94                     .{ .scale = .{ .node = 0, .coefficient = .{ .literal = capacity_mod.family_bytes_per_font } } },
 95                     .{ .add = .{ .left = 27, .right = 28 } },
 96                     .{ .add = .{ .left = 29, .right = 2 } },
 97                 },
 98                 .assertions = &.{.{
 99                     .scope = .closure_total,
100                     .measure = .retained,
101                     .relation = .exact,
102                     .expression = 30,
103                 }},
104             },
105             .overload = .{
106                 .kind = .reject_before_mutation,
107                 .detail = "slot, family length, and owned payload quota checks run before changing table fields",
108             },
109             .risks = .{
110                 .transitive = .{
111                     .status = .open,
112                     .detail = "owned bytes use one allocation at initialization; the bounded registration scan takes no allocator",
113                 },
114                 .foreign = .{
115                     .status = .excluded,
116                     .detail = "registration and lookup operate on caller and table bytes without an operating-system call",
117                 },
118             },
119             .obligations = &.{
120                 .{ .key = "ui_asset_capacity", .role = .capacity_model },
121                 .{ .key = "ui_asset_acquisition", .role = .acquisition },
122                 .{ .key = "ui_asset_generation", .role = .integration },
123                 .{ .key = "ui_asset_oom", .role = .overload },
124                 .{ .key = "ui_asset_teardown", .role = .teardown },
125             },
126         },
127         .bindings = .{
128             .owner = @This(),
129             .seal = .{
130                 .family = alloc_phase.capacity.selector(@This().activate),
131                 .premise = .{ .class = .checked_semantic_fact, .authority = .checker },
132             },
133             .teardown = .{
134                 .family = alloc_phase.capacity.selector(@This().deinit),
135                 .premise = .{ .class = .checked_semantic_fact, .authority = .checker },
136             },
137         },
138     };
139 
140     pub fn init(allocator: Allocator, limits: Limits) InitError!Registry {
141         const capacity = try Capacity.derive(limits);
142         const storage = try allocator.alignedAlloc(u8, .fromByteUnits(8), capacity.total_bytes);
143         var self = Registry{
144             .phase = .initialization,
145             .limits = limits,
146             .capacity = capacity,
147             .storage = storage,
148             .fonts = &.{},
149             .font_bytes = &.{},
150             .font_generation = &.{},
151             .font_used = 0,
152             .images = &.{},
153             .image_bytes = &.{},
154             .image_generation = &.{},
155             .image_used = 0,
156             .strings = &.{},
157             .owned_bytes = &.{},
158             .font_occupied = &.{},
159             .image_occupied = &.{},
160             .epoch = 0,
161         };
162         self.place();
163         @memset(self.storage, 0);
164         for (self.font_bytes) |*entry| entry.* = .{ .borrowed = &.{} };
165         for (self.image_bytes) |*entry| entry.* = .{ .borrowed = &.{} };
166         @memset(self.font_generation, 1);
167         @memset(self.image_generation, 1);
168         return self;
169     }
170 
171     pub fn activate(self: *Registry) void {
172         std.debug.assert(self.phase == .initialization);
173         std.debug.assert(self.storage.len == self.capacity.total_bytes);
174         self.phase = .steady;
175     }
176 
177     pub fn deinit(self: *Registry, allocator: Allocator) void {
178         std.debug.assert(self.phase != .teardown);
179         allocator.free(self.storage);
180         self.phase = .teardown;
181         self.storage = &.{};
182         self.fonts = &.{};
183         self.font_bytes = &.{};
184         self.font_generation = &.{};
185         self.images = &.{};
186         self.image_bytes = &.{};
187         self.image_generation = &.{};
188         self.strings = &.{};
189         self.owned_bytes = &.{};
190         self.font_occupied = &.{};
191         self.image_occupied = &.{};
192         self.font_used = 0;
193         self.image_used = 0;
194         self.epoch = 0;
195     }
196 
197     pub fn registerFont(self: *Registry, family: []const u8, input: FontDescriptor, bytes: Bytes) model.Error!AssetHandle {
198         std.debug.assert(self.phase == .steady);
199         if (family.len > capacity_mod.family_bytes_per_font) return error.FamilyTooLong;
200         const index = firstFree(self.font_occupied, self.limits.fonts) orelse return error.FontCapacityExceeded;
201         const source = bytes.slice();
202         const offset = switch (bytes) {
203             .borrowed => null,
204             .owned => self.findOwned(source.len) orelse return error.OwnedBytesExceeded,
205         };
206         const family_offset = @as(usize, index) * capacity_mod.family_bytes_per_font;
207         var descriptor = input;
208         descriptor.family_offset = @intCast(family_offset);
209         descriptor.family_len = @intCast(family.len);
210         @memcpy(self.strings[family_offset..][0..family.len], family);
211         self.fonts[index] = descriptor;
212         self.font_bytes[index] = self.storeBytes(bytes, offset);
213         setOccupied(self.font_occupied, index, true);
214         self.font_used += 1;
215         self.epoch +%= 1;
216         return .{ .index = index, .generation = self.font_generation[index] };
217     }
218 
219     pub fn registerImage(self: *Registry, descriptor: ImageDescriptor, bytes: Bytes) model.Error!AssetHandle {
220         std.debug.assert(self.phase == .steady);
221         const index = firstFree(self.image_occupied, self.limits.images) orelse return error.ImageCapacityExceeded;
222         const source = bytes.slice();
223         const offset = switch (bytes) {
224             .borrowed => null,
225             .owned => self.findOwned(source.len) orelse return error.OwnedBytesExceeded,
226         };
227         self.images[index] = descriptor;
228         self.image_bytes[index] = self.storeBytes(bytes, offset);
229         setOccupied(self.image_occupied, index, true);
230         self.image_used += 1;
231         self.epoch +%= 1;
232         return .{ .index = index, .generation = self.image_generation[index] };
233     }
234 
235     pub fn release(self: *Registry, kind: model.Kind, handle: AssetHandle) model.Error!void {
236         std.debug.assert(self.phase == .steady);
237         switch (kind) {
238             .font => {
239                 if (!self.hasFont(handle)) return error.StaleAsset;
240                 if (handle.generation == std.math.maxInt(u32)) return error.GenerationExhausted;
241                 clearOwned(self.font_bytes[handle.index]);
242                 const start = @as(usize, handle.index) * capacity_mod.family_bytes_per_font;
243                 @memset(self.strings[start..][0..capacity_mod.family_bytes_per_font], 0);
244                 self.fonts[handle.index] = .{};
245                 self.font_bytes[handle.index] = .{ .borrowed = &.{} };
246                 self.font_generation[handle.index] += 1;
247                 setOccupied(self.font_occupied, handle.index, false);
248                 self.font_used -= 1;
249                 self.epoch +%= 1;
250             },
251             .image => {
252                 if (!self.hasImage(handle)) return error.StaleAsset;
253                 if (handle.generation == std.math.maxInt(u32)) return error.GenerationExhausted;
254                 clearOwned(self.image_bytes[handle.index]);
255                 self.images[handle.index] = .{};
256                 self.image_bytes[handle.index] = .{ .borrowed = &.{} };
257                 self.image_generation[handle.index] += 1;
258                 setOccupied(self.image_occupied, handle.index, false);
259                 self.image_used -= 1;
260                 self.epoch +%= 1;
261             },
262         }
263     }
264 
265     pub fn hasFont(self: *const Registry, handle: AssetHandle) bool {
266         return handle.index < self.limits.fonts and occupied(self.font_occupied, handle.index) and self.font_generation[handle.index] == handle.generation;
267     }
268 
269     pub fn hasImage(self: *const Registry, handle: AssetHandle) bool {
270         return handle.index < self.limits.images and occupied(self.image_occupied, handle.index) and self.image_generation[handle.index] == handle.generation;
271     }
272 
273     pub fn font(self: *const Registry, handle: AssetHandle) model.Error!model.Font {
274         if (!self.hasFont(handle)) return error.StaleAsset;
275         const descriptor = self.fonts[handle.index];
276         return .{
277             .descriptor = descriptor,
278             .family = self.strings[descriptor.family_offset..][0..descriptor.family_len],
279             .bytes = self.font_bytes[handle.index].slice(),
280         };
281     }
282 
283     pub fn image(self: *const Registry, handle: AssetHandle) model.Error!model.Image {
284         if (!self.hasImage(handle)) return error.StaleAsset;
285         return .{ .descriptor = self.images[handle.index], .bytes = self.image_bytes[handle.index].slice() };
286     }
287 
288     fn storeBytes(self: *Registry, bytes: Bytes, offset: ?usize) Bytes {
289         return switch (bytes) {
290             .borrowed => bytes,
291             .owned => |source| blk: {
292                 const start = offset.?;
293                 @memcpy(self.owned_bytes[start..][0..source.len], source);
294                 break :blk .{ .owned = self.owned_bytes[start..][0..source.len] };
295             },
296         };
297     }
298 
299     fn findOwned(self: *const Registry, len: usize) ?usize {
300         if (len > self.owned_bytes.len) return null;
301         var candidate: usize = 0;
302         while (candidate <= self.owned_bytes.len - len) {
303             var conflict_end: usize = candidate;
304             for (self.font_bytes, 0..) |entry, index| {
305                 if (!occupied(self.font_occupied, @intCast(index))) continue;
306                 overlapEnd(self.owned_bytes, entry, candidate, len, &conflict_end);
307             }
308             for (self.image_bytes, 0..) |entry, index| {
309                 if (!occupied(self.image_occupied, @intCast(index))) continue;
310                 overlapEnd(self.owned_bytes, entry, candidate, len, &conflict_end);
311             }
312             if (conflict_end == candidate) return candidate;
313             candidate = conflict_end;
314         }
315         return null;
316     }
317 
318     fn place(self: *Registry) void {
319         var cursor: usize = 0;
320         self.fonts = take(self.storage, &cursor, FontDescriptor, self.limits.fonts);
321         self.font_bytes = take(self.storage, &cursor, Bytes, self.limits.fonts);
322         self.font_generation = take(self.storage, &cursor, u32, self.limits.fonts);
323         self.font_occupied = take(self.storage, &cursor, u64, std.math.divCeil(usize, self.limits.fonts, 64) catch unreachable);
324         self.images = take(self.storage, &cursor, ImageDescriptor, self.limits.images);
325         self.image_bytes = take(self.storage, &cursor, Bytes, self.limits.images);
326         self.image_generation = take(self.storage, &cursor, u32, self.limits.images);
327         self.image_occupied = take(self.storage, &cursor, u64, std.math.divCeil(usize, self.limits.images, 64) catch unreachable);
328         self.strings = take(self.storage, &cursor, u8, self.capacity.string_bytes);
329         self.owned_bytes = take(self.storage, &cursor, u8, self.limits.owned_bytes);
330         std.debug.assert(cursor == self.capacity.total_bytes);
331     }
332 };
333 
334 fn take(storage: []align(8) u8, cursor: *usize, comptime T: type, count: usize) []T {
335     cursor.* = std.mem.alignForward(usize, cursor.*, @alignOf(T));
336     const size = count * @sizeOf(T);
337     const bytes: []align(@alignOf(T)) u8 = @alignCast(storage[cursor.*..][0..size]);
338     cursor.* += size;
339     return std.mem.bytesAsSlice(T, bytes);
340 }
341 
342 fn occupied(words: []const u64, index: u32) bool {
343     return (words[index / 64] & (@as(u64, 1) << @intCast(index % 64))) != 0;
344 }
345 
346 fn setOccupied(words: []u64, index: u32, value: bool) void {
347     const mask = @as(u64, 1) << @intCast(index % 64);
348     if (value) words[index / 64] |= mask else words[index / 64] &= ~mask;
349 }
350 
351 fn firstFree(words: []const u64, count: u32) ?u32 {
352     for (words, 0..) |word, word_index| {
353         const first = word_index * 64;
354         const valid = @min(64, count - @as(u32, @intCast(first)));
355         const mask: u64 = if (valid == 64) std.math.maxInt(u64) else (@as(u64, 1) << @intCast(valid)) - 1;
356         const free = ~word & mask;
357         if (free != 0) return @intCast(first + @ctz(free));
358     }
359     return null;
360 }
361 
362 fn overlapEnd(pool: []const u8, entry: Bytes, candidate: usize, len: usize, conflict_end: *usize) void {
363     const bytes = switch (entry) {
364         .borrowed => return,
365         .owned => |value| value,
366     };
367     if (bytes.len == 0) return;
368     const start = @intFromPtr(bytes.ptr) - @intFromPtr(pool.ptr);
369     const end = start + bytes.len;
370     if (start < candidate + len and candidate < end) conflict_end.* = @max(conflict_end.*, end);
371 }
372 
373 fn clearOwned(entry: Bytes) void {
374     switch (entry) {
375         .borrowed => {},
376         .owned => |bytes| @memset(@constCast(bytes), 0),
377     }
378 }
379 
380 comptime {
381     alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Registry);
382 }
383 
384 test "ui_asset_capacity default table fits 47104 bytes" {
385     comptime {
386         @stardustClaim(alloc_phase.capacity.witness(Registry, "ui_asset_capacity"), null, null, null, null, null, null);
387     }
388     const derived = try capacity_mod.Capacity.derive(.{});
389     try std.testing.expectEqual(capacity_mod.default_table_bytes, derived.total_bytes);
390     try std.testing.expectError(
391         error.CapacityOverflow,
392         capacity_mod.Capacity.derive(.{ .fonts = std.math.maxInt(u32) }),
393     );
394 }
395 
396 fn openAndClose(allocator: Allocator) !void {
397     var registry = try Registry.init(allocator, .{});
398     registry.activate();
399     registry.deinit(allocator);
400 }
401 
402 test "ui_asset_acquisition retries after allocation failure" {
403     comptime {
404         @stardustClaim(alloc_phase.capacity.witness(Registry, "ui_asset_acquisition"), null, null, null, null, null, null);
405     }
406     try std.testing.checkAllAllocationFailures(std.testing.allocator, openAndClose, .{});
407 }
408 
409 test "ui_asset_generation register release and reuse" {
410     comptime {
411         @stardustClaim(alloc_phase.capacity.witness(Registry, "ui_asset_generation"), null, null, null, null, null, null);
412     }
413     var registry = try Registry.init(std.testing.allocator, .{ .fonts = 1, .images = 0, .owned_bytes = 8 });
414     defer registry.deinit(std.testing.allocator);
415     registry.activate();
416     var caller = [_]u8{ 1, 2, 3 };
417     const first = try registry.registerFont("Mono", .{}, .{ .borrowed = &caller });
418     try std.testing.expectEqualSlices(u8, &caller, (try registry.font(first)).bytes);
419     caller[0] = 4;
420     try std.testing.expectEqual(@as(u8, 4), (try registry.font(first)).bytes[0]);
421     try registry.release(.font, first);
422     try std.testing.expectError(error.StaleAsset, registry.font(first));
423     const next = try registry.registerFont("Mono", .{}, .{ .borrowed = &caller });
424     try std.testing.expectEqual(first.index, next.index);
425     try std.testing.expectEqual(first.generation + 1, next.generation);
426 }
427 
428 test "ui_asset_oom registration rejects before mutation" {
429     comptime {
430         @stardustClaim(alloc_phase.capacity.witness(Registry, "ui_asset_oom"), null, null, null, null, null, null);
431     }
432     var registry = try Registry.init(std.testing.allocator, .{ .fonts = 1, .images = 1, .owned_bytes = 3 });
433     defer registry.deinit(std.testing.allocator);
434     registry.activate();
435     const first = try registry.registerFont("A", .{}, .{ .owned = "abc" });
436     try std.testing.expectError(error.FontCapacityExceeded, registry.registerFont("B", .{}, .{ .borrowed = "" }));
437     try std.testing.expectError(error.OwnedBytesExceeded, registry.registerImage(.{}, .{ .owned = "d" }));
438     try std.testing.expectEqual(@as(u32, 1), registry.font_used);
439     try std.testing.expectEqual(@as(u32, 0), registry.image_used);
440     try std.testing.expectEqualStrings("abc", (try registry.font(first)).bytes);
441 }
442 
443 test "ui_asset_capacity accepts each maximum and rejects maximum plus one" {
444     var registry = try Registry.init(std.testing.allocator, .{ .fonts = 2, .images = 1, .owned_bytes = 4 });
445     defer registry.deinit(std.testing.allocator);
446     registry.activate();
447     _ = try registry.registerFont("A", .{}, .{ .owned = "ab" });
448     _ = try registry.registerFont("B", .{}, .{ .borrowed = "" });
449     _ = try registry.registerImage(.{}, .{ .owned = "cd" });
450     try std.testing.expectEqual(@as(u32, 2), registry.font_used);
451     try std.testing.expectEqual(@as(u32, 1), registry.image_used);
452     try std.testing.expectError(error.FontCapacityExceeded, registry.registerFont("C", .{}, .{ .borrowed = "" }));
453     try std.testing.expectError(error.ImageCapacityExceeded, registry.registerImage(.{}, .{ .borrowed = "" }));
454     try std.testing.expectEqual(@as(u32, 2), registry.font_used);
455     try std.testing.expectEqual(@as(u32, 1), registry.image_used);
456 }
457 
458 test "ui_asset_capacity default slot and owned byte maxima" {
459     var registry = try Registry.init(std.testing.allocator, .{});
460     defer registry.deinit(std.testing.allocator);
461     registry.activate();
462     for (0..registry.limits.fonts) |_| {
463         _ = try registry.registerFont("A", .{}, .{ .borrowed = "" });
464     }
465     try std.testing.expectError(error.FontCapacityExceeded, registry.registerFont("A", .{}, .{ .borrowed = "" }));
466     for (0..registry.limits.images) |_| {
467         _ = try registry.registerImage(.{}, .{ .borrowed = "" });
468     }
469     try std.testing.expectError(error.ImageCapacityExceeded, registry.registerImage(.{}, .{ .borrowed = "" }));
470     try std.testing.expectEqual(registry.limits.fonts, registry.font_used);
471     try std.testing.expectEqual(registry.limits.images, registry.image_used);
472 
473     var bytes_registry = try Registry.init(std.testing.allocator, .{ .fonts = 1, .images = 1 });
474     defer bytes_registry.deinit(std.testing.allocator);
475     bytes_registry.activate();
476     const full: [41_328]u8 = @splat(0xA5);
477     const handle = try bytes_registry.registerFont("A", .{}, .{ .owned = &full });
478     try std.testing.expectEqualSlices(u8, &full, (try bytes_registry.font(handle)).bytes);
479     try std.testing.expectError(error.OwnedBytesExceeded, bytes_registry.registerImage(.{}, .{ .owned = "x" }));
480     try std.testing.expectEqual(@as(u32, 0), bytes_registry.image_used);
481 }
482 
483 test "ui_asset_teardown borrows caller bytes and returns owned table once" {
484     comptime {
485         @stardustClaim(alloc_phase.capacity.witness(Registry, "ui_asset_teardown"), null, null, null, null, null, null);
486     }
487     var borrowed = [_]u8{ 5, 6 };
488     var registry = try Registry.init(std.testing.allocator, .{ .fonts = 1, .images = 1, .owned_bytes = 3 });
489     registry.activate();
490     const font = try registry.registerFont("A", .{}, .{ .borrowed = &borrowed });
491     var source = [_]u8{ 'x', 'y', 'z' };
492     const image = try registry.registerImage(.{}, .{ .owned = &source });
493     source[0] = 'q';
494     try std.testing.expectEqualSlices(u8, &borrowed, (try registry.font(font)).bytes);
495     try std.testing.expectEqualStrings("xyz", (try registry.image(image)).bytes);
496     try registry.release(.image, image);
497     registry.deinit(std.testing.allocator);
498     borrowed[0] = 7;
499     try std.testing.expectEqual(@as(u8, 7), borrowed[0]);
500     try std.testing.expectEqual(alloc_phase.capacity.Phase.teardown, registry.phase);
501 }