lib/gui/src/paint/fallback.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_phase = @import("alloc_phase");
3 const filigree = @import("filigree");
4
5 const Allocator = std.mem.Allocator;
6
7 pub const FaceSegment = struct {
8 source: filigree.SourceRange,
9 candidate_index: usize,
10 missing_everywhere: bool,
11 };
12
13 pub const Key = struct {
14 content: []const u8,
15 primary_identity: usize,
16 fallback_identity: usize,
17 };
18
19 const CacheEntry = struct {
20 hash: u64,
21 primary_identity: usize,
22 fallback_identity: usize,
23 content_offset: usize,
24 content_len: usize,
25 segment_offset: usize,
26 segment_count: usize,
27 };
28
29 pub const Limits = struct {
30 max_source_units: usize,
31 cache_entries: usize,
32 cache_payload_bytes: usize,
33 };
34
35 pub const DeriveError = error{CapacityOverflow};
36
37 const storage_alignment: usize = @max(
38 @max(@alignOf(filigree.FallbackSegment), @alignOf(u32)),
39 @max(@alignOf(CacheEntry), @alignOf(FaceSegment)),
40 );
41
42 pub const Capacity = struct {
43 limits: Limits,
44 segment_offset: usize,
45 segment_bytes: usize,
46 index_slots: usize,
47 index_offset: usize,
48 index_bytes: usize,
49 entry_offset: usize,
50 entry_bytes: usize,
51 payload_offset: usize,
52 payload_bytes: usize,
53 storage_bytes: usize,
54
55 pub fn derive(limits: Limits) DeriveError!Capacity {
56 const segments = try placed(filigree.FallbackSegment, 0, limits.max_source_units);
57 const slots = try indexSlotCount(limits.cache_entries);
58 const index = try placed(u32, segments.end, slots);
59 const entries = try placed(CacheEntry, index.end, limits.cache_entries);
60 const payload = try placed(u8, try alignOffset(entries.end, @alignOf(FaceSegment)), limits.cache_payload_bytes);
61 return .{
62 .limits = limits,
63 .segment_offset = segments.start,
64 .segment_bytes = segments.bytes,
65 .index_slots = slots,
66 .index_offset = index.start,
67 .index_bytes = index.bytes,
68 .entry_offset = entries.start,
69 .entry_bytes = entries.bytes,
70 .payload_offset = payload.start,
71 .payload_bytes = payload.bytes,
72 .storage_bytes = payload.end,
73 };
74 }
75 };
76
77 pub const Exhaustion = error{
78 SourceUnitCapacityExceeded,
79 SegmentCapacityExceeded,
80 CacheEntryCapacityExceeded,
81 CachePayloadCapacityExceeded,
82 };
83
84 pub const Status = struct {
85 phase: alloc_phase.capacity.Phase,
86 capacity: StorageCapacity,
87 storage_bytes: usize,
88 entries: usize,
89 payload_bytes: usize,
90 high_water_entries: usize,
91 high_water_payload_bytes: usize,
92 source_rejections: u64,
93 segment_rejections: u64,
94 cache_entry_rejections: u64,
95 cache_payload_rejections: u64,
96 rollovers: u64,
97 };
98
99 const StorageLimits = Limits;
100 const StorageCapacity = Capacity;
101 const StorageExhaustion = Exhaustion;
102
103 pub const Storage = struct {
104 phase: alloc_phase.capacity.Phase,
105 capacity: StorageCapacity,
106 bytes: []align(storage_alignment) u8,
107 segment_scratch: []filigree.FallbackSegment,
108 index: []u32,
109 entries: []CacheEntry,
110 payload: []u8,
111 entry_count: usize = 0,
112 payload_used: usize = 0,
113 high_water_entries: usize = 0,
114 high_water_payload_bytes: usize = 0,
115 source_rejections: u64 = 0,
116 segment_rejections: u64 = 0,
117 cache_entry_rejections: u64 = 0,
118 cache_payload_rejections: u64 = 0,
119 rollovers: u64 = 0,
120
121 pub const Limits: type = StorageLimits;
122 pub const Capacity: type = StorageCapacity;
123 pub const Exhaustion: type = StorageExhaustion;
124 pub const InitError = Allocator.Error || DeriveError;
125
126 pub const claim: alloc_phase.capacity.Declaration = .{
127 .source = .{
128 .id = "gui.text_fallback_segment_storage",
129 .kind = .phase_static,
130 .limit_source = .caller,
131 .storage = .{
132 .covered = &.{
133 .{
134 .id = "fallback_reported_segment_scratch",
135 .lifetime = .steady,
136 .detail = "reported Filigree fallback face segments",
137 },
138 .{
139 .id = "fallback_segment_cache_index_and_entries",
140 .lifetime = .steady,
141 .detail = "fixed fallback segment cache index and dense entries",
142 },
143 .{
144 .id = "fallback_segment_cache_payload",
145 .lifetime = .steady,
146 .detail = "source keys and aligned face segment payloads",
147 },
148 },
149 .excluded = &.{
150 "caller-owned UTF-8 content and font atlases",
151 "Filigree fallback context and temporary shaped output",
152 "per-atlas shape caches and composite layout workspace",
153 },
154 },
155 .capacity = .{
156 .inputs = &.{
157 alloc_phase.capacity.bindInput(StorageLimits, "max_source_units", "max_source_units"),
158 alloc_phase.capacity.bindInput(StorageLimits, "cache_entries", "cache_entries"),
159 alloc_phase.capacity.bindInput(StorageLimits, "cache_payload_bytes", "cache_payload_bytes"),
160 },
161 .type_selectors = &.{
162 alloc_phase.capacity.bindType(filigree.FallbackSegment, "fallbacksegment"),
163 alloc_phase.capacity.bindType(u32, "u32"),
164 alloc_phase.capacity.bindType(CacheEntry, "cacheentry"),
165 alloc_phase.capacity.bindType(FaceSegment, "facesegment"),
166 },
167 .nodes = &.{
168 .{ .input = 0 },
169 .{ .scale = .{ .node = 0, .coefficient = .{ .size_of_concrete_type = 0 } } },
170 .{ .alignment = .{ .node = 1, .alignment = .{ .concrete_type = 1 } } },
171 .{ .input = 1 },
172 .{ .scale = .{ .node = 3, .coefficient = .{ .literal = 2 } } },
173 .{ .next_power_of_two = 4 },
174 .{ .scale = .{ .node = 5, .coefficient = .{ .size_of_concrete_type = 1 } } },
175 .{ .add = .{ .left = 2, .right = 6 } },
176 .{ .alignment = .{ .node = 7, .alignment = .{ .concrete_type = 2 } } },
177 .{ .scale = .{ .node = 3, .coefficient = .{ .size_of_concrete_type = 2 } } },
178 .{ .add = .{ .left = 8, .right = 9 } },
179 .{ .alignment = .{ .node = 10, .alignment = .{ .concrete_type = 3 } } },
180 .{ .input = 2 },
181 .{ .add = .{ .left = 11, .right = 12 } },
182 },
183 .assertions = &.{.{
184 .scope = .closure_total,
185 .measure = .retained,
186 .relation = .exact,
187 .expression = 13,
188 }},
189 },
190 .overload = .{
191 .kind = .reject_before_mutation,
192 .detail = "source, segment, entry, and payload max-plus-one requests reject before cached records or returned pointers change",
193 },
194 .risks = .{
195 .transitive = .{
196 .status = .witnessed,
197 .detail = "activated lookup, insertion, and explicit rollover use only the acquired typed regions",
198 },
199 .foreign = .{
200 .status = .excluded,
201 .detail = "cache operations cross no operating-system or foreign callback boundary",
202 },
203 },
204 .obligations = &.{
205 .{ .key = "gui_text_fallback_capacity", .role = .capacity_model },
206 .{ .key = "gui_text_fallback_acquisition", .role = .custom },
207 .{ .key = "gui_text_fallback_oom", .role = .custom },
208 .{ .key = "gui_text_fallback_boundaries", .role = .overload },
209 .{ .key = "gui_text_fallback_reuse", .role = .overload },
210 .{ .key = "gui_text_fallback_sealed_transitive_risk", .role = .transitive_risk },
211 .{ .key = "gui_text_fallback_sealed_foreign_risk", .role = .foreign_risk },
212 .{ .key = "gui_text_fallback_root", .role = .custom },
213 },
214 },
215 .bindings = .{
216 .owner = @This(),
217 .seal = .{
218 .family = alloc_phase.capacity.selector(@This().activate),
219 .premise = .{
220 .class = .checked_semantic_fact,
221 .authority = .checker,
222 },
223 },
224 .teardown = .{
225 .family = alloc_phase.capacity.selector(@This().deinit),
226 .premise = .{
227 .class = .checked_semantic_fact,
228 .authority = .checker,
229 },
230 },
231 },
232 };
233
234 pub fn init(allocator: Allocator, limits: StorageLimits) InitError!Storage {
235 const capacity = try StorageCapacity.derive(limits);
236 const bytes = try allocator.alignedAlloc(u8, .fromByteUnits(storage_alignment), capacity.storage_bytes);
237 return .{
238 .phase = .initialization,
239 .capacity = capacity,
240 .bytes = bytes,
241 .segment_scratch = typedSlice(filigree.FallbackSegment, bytes, capacity.segment_offset, limits.max_source_units),
242 .index = typedSlice(u32, bytes, capacity.index_offset, capacity.index_slots),
243 .entries = typedSlice(CacheEntry, bytes, capacity.entry_offset, limits.cache_entries),
244 .payload = bytes[capacity.payload_offset..][0..capacity.payload_bytes],
245 };
246 }
247
248 pub fn activate(self: *Storage) void {
249 std.debug.assert(self.phase == .initialization);
250 self.assertStorage();
251 @memset(self.index, 0);
252 self.phase = .steady;
253 }
254
255 pub fn deinit(self: *Storage, allocator: Allocator) void {
256 std.debug.assert(self.phase != .teardown);
257 self.assertStorage();
258 self.phase = .teardown;
259 allocator.free(self.bytes);
260 self.bytes = &.{};
261 self.segment_scratch = &.{};
262 self.index = &.{};
263 self.entries = &.{};
264 self.payload = &.{};
265 }
266
267 pub fn segmentList(self: *Storage, source_units: usize) StorageExhaustion!std.ArrayListUnmanaged(filigree.FallbackSegment) {
268 std.debug.assert(self.phase == .steady);
269 if (source_units > self.capacity.limits.max_source_units) {
270 self.source_rejections +|= 1;
271 return error.SourceUnitCapacityExceeded;
272 }
273 return .{ .items = self.segment_scratch[0..0], .capacity = self.segment_scratch.len };
274 }
275
276 pub fn lookup(self: *const Storage, key: Key) ?[]const FaceSegment {
277 std.debug.assert(self.phase == .steady);
278 if (self.index.len == 0) return null;
279 const hash = hashKey(key);
280 var slot = @as(usize, @truncate(hash)) & (self.index.len - 1);
281 var probes: usize = 0;
282 while (probes < self.index.len) : (probes += 1) {
283 const stored = self.index[slot];
284 if (stored == 0) return null;
285 const entry = self.entries[stored - 1];
286 if (self.entryMatches(entry, hash, key)) return self.entrySegments(entry);
287 slot = (slot + 1) & (self.index.len - 1);
288 }
289 return null;
290 }
291
292 pub fn store(self: *Storage, key: Key, reported: []const filigree.FallbackSegment) StorageExhaustion![]const FaceSegment {
293 std.debug.assert(self.phase == .steady);
294 if (reported.len > self.segment_scratch.len) {
295 self.segment_rejections +|= 1;
296 return error.SegmentCapacityExceeded;
297 }
298 if (self.entry_count >= self.entries.len) {
299 self.cache_entry_rejections +|= 1;
300 return error.CacheEntryCapacityExceeded;
301 }
302 const placement = payloadPlacement(self.payload_used, key.content.len, reported.len) catch {
303 self.cache_payload_rejections +|= 1;
304 return error.CachePayloadCapacityExceeded;
305 };
306 if (placement.end > self.payload.len) {
307 self.cache_payload_rejections +|= 1;
308 return error.CachePayloadCapacityExceeded;
309 }
310 const hash = hashKey(key);
311 const slot = self.emptySlot(hash) orelse {
312 self.cache_entry_rejections +|= 1;
313 return error.CacheEntryCapacityExceeded;
314 };
315 return self.commitStore(key, reported, hash, slot, placement);
316 }
317
318 pub fn reset(self: *Storage) void {
319 std.debug.assert(self.phase == .steady);
320 @memset(self.index, 0);
321 self.entry_count = 0;
322 self.payload_used = 0;
323 self.rollovers +|= 1;
324 }
325
326 pub fn status(self: *const Storage) Status {
327 return .{
328 .phase = self.phase,
329 .capacity = self.capacity,
330 .storage_bytes = self.capacity.storage_bytes,
331 .entries = self.entry_count,
332 .payload_bytes = self.payload_used,
333 .high_water_entries = self.high_water_entries,
334 .high_water_payload_bytes = self.high_water_payload_bytes,
335 .source_rejections = self.source_rejections,
336 .segment_rejections = self.segment_rejections,
337 .cache_entry_rejections = self.cache_entry_rejections,
338 .cache_payload_rejections = self.cache_payload_rejections,
339 .rollovers = self.rollovers,
340 };
341 }
342
343 fn commitStore(self: *Storage, key: Key, reported: []const filigree.FallbackSegment, hash: u64, slot: usize, placement: PayloadPlacement) []const FaceSegment {
344 const content = self.payload[placement.content_offset..placement.content_end];
345 @memcpy(content, key.content);
346 const segments = payloadSegments(self.payload, placement.segment_offset, reported.len);
347 for (reported, segments) |source, *target| {
348 target.* = .{
349 .source = source.source,
350 .candidate_index = source.candidate_index,
351 .missing_everywhere = source.missing_everywhere,
352 };
353 }
354 self.entries[self.entry_count] = .{
355 .hash = hash,
356 .primary_identity = key.primary_identity,
357 .fallback_identity = key.fallback_identity,
358 .content_offset = placement.content_offset,
359 .content_len = key.content.len,
360 .segment_offset = placement.segment_offset,
361 .segment_count = reported.len,
362 };
363 self.index[slot] = @intCast(self.entry_count + 1);
364 self.entry_count += 1;
365 self.payload_used = placement.end;
366 self.high_water_entries = @max(self.high_water_entries, self.entry_count);
367 self.high_water_payload_bytes = @max(self.high_water_payload_bytes, self.payload_used);
368 return segments;
369 }
370
371 fn emptySlot(self: *const Storage, hash: u64) ?usize {
372 if (self.index.len == 0) return null;
373 var slot = @as(usize, @truncate(hash)) & (self.index.len - 1);
374 var probes: usize = 0;
375 while (probes < self.index.len) : (probes += 1) {
376 if (self.index[slot] == 0) return slot;
377 slot = (slot + 1) & (self.index.len - 1);
378 }
379 return null;
380 }
381
382 fn entryMatches(self: *const Storage, entry: CacheEntry, hash: u64, key: Key) bool {
383 if (entry.hash != hash or entry.primary_identity != key.primary_identity or entry.fallback_identity != key.fallback_identity) return false;
384 if (entry.content_len != key.content.len) return false;
385 return std.mem.eql(u8, self.payload[entry.content_offset..][0..entry.content_len], key.content);
386 }
387
388 fn entrySegments(self: *const Storage, entry: CacheEntry) []const FaceSegment {
389 return payloadSegments(self.payload, entry.segment_offset, entry.segment_count);
390 }
391
392 fn assertStorage(self: *const Storage) void {
393 std.debug.assert(self.bytes.len == self.capacity.storage_bytes);
394 std.debug.assert(self.segment_scratch.len == self.capacity.limits.max_source_units);
395 std.debug.assert(self.index.len == self.capacity.index_slots);
396 std.debug.assert(self.entries.len == self.capacity.limits.cache_entries);
397 std.debug.assert(self.payload.len == self.capacity.payload_bytes);
398 std.debug.assert(self.entry_count <= self.entries.len);
399 std.debug.assert(self.payload_used <= self.payload.len);
400 }
401 };
402
403 comptime {
404 alloc_phase.capacity.requireAllocatorRejectingOwnerShape(Storage);
405 }
406
407 pub const Engine = struct {
408 allocator: Allocator,
409 context: filigree.FallbackContext,
410 output: filigree.Output,
411 storage: Storage,
412
413 pub fn init(allocator: Allocator, limits: Limits, output_limits: filigree.Output.Limits) !Engine {
414 var storage = try Storage.init(allocator, limits);
415 errdefer storage.deinit(allocator);
416 storage.activate();
417 var context = try filigree.FallbackContext.init(allocator, .{
418 .script_runs = .{ .max_source_units = limits.max_source_units },
419 .output = output_limits,
420 });
421 errdefer context.deinit();
422 return .{
423 .allocator = allocator,
424 .context = context,
425 .output = try filigree.Output.init(allocator, output_limits),
426 .storage = storage,
427 };
428 }
429
430 pub fn deinit(self: *Engine) void {
431 self.storage.deinit(self.allocator);
432 self.output.deinit(self.allocator);
433 self.context.deinit();
434 self.* = undefined;
435 }
436
437 pub fn segments(self: *Engine, primary: *const filigree.Font, fallback: *const filigree.Font, content: []const u8) ![]const FaceSegment {
438 const key = Key{
439 .content = content,
440 .primary_identity = @intFromPtr(primary),
441 .fallback_identity = @intFromPtr(fallback),
442 };
443 if (self.storage.lookup(key)) |cached| return cached;
444 var reported = try self.storage.segmentList(content.len);
445 const candidates = [_]filigree.FallbackCandidate{.{ .font = fallback }};
446 try self.context.shapeRunSegmented(.{
447 .base = .{ .font = primary, .text = .{ .utf8 = content } },
448 .candidates = &candidates,
449 }, &self.output, self.allocator, &reported);
450 return try self.storage.store(key, reported.items);
451 }
452
453 pub fn status(self: *const Engine) Status {
454 return self.storage.status();
455 }
456 };
457
458 const Region = struct {
459 start: usize,
460 bytes: usize,
461 end: usize,
462 };
463
464 const PayloadPlacement = struct {
465 content_offset: usize,
466 content_end: usize,
467 segment_offset: usize,
468 end: usize,
469 };
470
471 fn placed(comptime T: type, offset: usize, count: usize) DeriveError!Region {
472 const start = try alignOffset(offset, @alignOf(T));
473 const bytes = std.math.mul(usize, count, @sizeOf(T)) catch return error.CapacityOverflow;
474 return .{ .start = start, .bytes = bytes, .end = std.math.add(usize, start, bytes) catch return error.CapacityOverflow };
475 }
476
477 fn alignOffset(offset: usize, alignment: usize) DeriveError!usize {
478 const padded = std.math.add(usize, offset, alignment - 1) catch return error.CapacityOverflow;
479 return padded & ~(alignment - 1);
480 }
481
482 fn indexSlotCount(entries: usize) DeriveError!usize {
483 if (entries == 0) return 0;
484 const doubled = std.math.mul(usize, entries, 2) catch return error.CapacityOverflow;
485 return std.math.ceilPowerOfTwo(usize, doubled) catch return error.CapacityOverflow;
486 }
487
488 fn payloadPlacement(offset: usize, content_len: usize, segment_count: usize) DeriveError!PayloadPlacement {
489 const content_end = std.math.add(usize, offset, content_len) catch return error.CapacityOverflow;
490 const segment_offset = try alignOffset(content_end, @alignOf(FaceSegment));
491 const segment_bytes = std.math.mul(usize, segment_count, @sizeOf(FaceSegment)) catch return error.CapacityOverflow;
492 return .{
493 .content_offset = offset,
494 .content_end = content_end,
495 .segment_offset = segment_offset,
496 .end = std.math.add(usize, segment_offset, segment_bytes) catch return error.CapacityOverflow,
497 };
498 }
499
500 fn typedSlice(comptime T: type, bytes: []align(storage_alignment) u8, offset: usize, count: usize) []T {
501 const byte_count = count * @sizeOf(T);
502 const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
503 return std.mem.bytesAsSlice(T, region);
504 }
505
506 fn payloadSegments(payload: []u8, offset: usize, count: usize) []FaceSegment {
507 const byte_count = count * @sizeOf(FaceSegment);
508 const region: []align(@alignOf(FaceSegment)) u8 = @alignCast(payload[offset..][0..byte_count]);
509 return std.mem.bytesAsSlice(FaceSegment, region);
510 }
511
512 fn hashKey(key: Key) u64 {
513 var hash = std.hash.Wyhash.init(0x17d6_43c1_a948_5ef2);
514 hash.update(std.mem.asBytes(&key.primary_identity));
515 hash.update(std.mem.asBytes(&key.fallback_identity));
516 hash.update(key.content);
517 return hash.final();
518 }
519
520 fn modelCapacity(limits: Limits) DeriveError!Capacity {
521 const segment_bytes = @as(u128, limits.max_source_units) * @sizeOf(filigree.FallbackSegment);
522 const index_slots = if (limits.cache_entries == 0) 0 else std.math.ceilPowerOfTwo(u128, @as(u128, limits.cache_entries) * 2) catch return error.CapacityOverflow;
523 const index_offset = modelAlign(segment_bytes, @alignOf(u32));
524 const index_bytes = index_slots * @sizeOf(u32);
525 const entry_offset = modelAlign(index_offset + index_bytes, @alignOf(CacheEntry));
526 const entry_bytes = @as(u128, limits.cache_entries) * @sizeOf(CacheEntry);
527 const payload_offset = modelAlign(entry_offset + entry_bytes, @alignOf(FaceSegment));
528 const storage_bytes = payload_offset + limits.cache_payload_bytes;
529 const values = [_]u128{ segment_bytes, index_slots, index_offset, index_bytes, entry_offset, entry_bytes, payload_offset, storage_bytes };
530 for (values) |value| if (value > std.math.maxInt(usize)) return error.CapacityOverflow;
531 return .{
532 .limits = limits,
533 .segment_offset = 0,
534 .segment_bytes = @intCast(segment_bytes),
535 .index_slots = @intCast(index_slots),
536 .index_offset = @intCast(index_offset),
537 .index_bytes = @intCast(index_bytes),
538 .entry_offset = @intCast(entry_offset),
539 .entry_bytes = @intCast(entry_bytes),
540 .payload_offset = @intCast(payload_offset),
541 .payload_bytes = limits.cache_payload_bytes,
542 .storage_bytes = @intCast(storage_bytes),
543 };
544 }
545
546 fn modelAlign(offset: u128, alignment: u128) u128 {
547 return ((offset + alignment - 1) / alignment) * alignment;
548 }
549
550 fn activatedStorage(allocator: Allocator, limits: Limits) !Storage {
551 var storage = try Storage.init(allocator, limits);
552 storage.activate();
553 return storage;
554 }
555
556 fn checkInitFailures(allocator: Allocator) !void {
557 var storage = try Storage.init(allocator, .{ .max_source_units = 16, .cache_entries = 4, .cache_payload_bytes = 256 });
558 storage.deinit(allocator);
559 }
560
561 test "fallback segment capacity matches an independent aligned byte model" {
562 comptime {
563 @stardustClaim(alloc_phase.capacity.witness(Storage, "gui_text_fallback_capacity"), null, null, null, null, null, null);
564 }
565 const limits = Limits{ .max_source_units = 13, .cache_entries = 7, .cache_payload_bytes = 513 };
566 try std.testing.expectEqual(try modelCapacity(limits), try Capacity.derive(limits));
567 }
568
569 test "fallback segment storage acquires one exact region and retries OOM" {
570 comptime {
571 @stardustClaim(
572 alloc_phase.capacity.witness(Storage, "gui_text_fallback_acquisition"),
573 null,
574 null,
575 null,
576 null,
577 null,
578 null,
579 );
580 }
581 comptime {
582 @stardustClaim(
583 alloc_phase.capacity.witness(Storage, "gui_text_fallback_oom"),
584 null,
585 null,
586 null,
587 null,
588 null,
589 null,
590 );
591 }
592 const limits = Limits{ .max_source_units = 13, .cache_entries = 7, .cache_payload_bytes = 513 };
593 const capacity = try Capacity.derive(limits);
594 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
595 var storage = try Storage.init(counting.allocator(), limits);
596 defer storage.deinit(counting.allocator());
597 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
598 try std.testing.expectEqual(capacity.storage_bytes, counting.allocated_bytes);
599 storage.activate();
600 try std.testing.checkAllAllocationFailures(std.testing.allocator, checkInitFailures, .{});
601 }
602
603 test "fallback segment storage accepts exact maxima and rejects max plus one transactionally" {
604 comptime {
605 @stardustClaim(
606 alloc_phase.capacity.witness(Storage, "gui_text_fallback_boundaries"),
607 null,
608 null,
609 null,
610 null,
611 null,
612 null,
613 );
614 }
615 comptime {
616 @stardustClaim(
617 alloc_phase.capacity.witness(Storage, "gui_text_fallback_reuse"),
618 null,
619 null,
620 null,
621 null,
622 null,
623 null,
624 );
625 }
626 comptime {
627 @stardustClaim(
628 alloc_phase.capacity.witness(Storage, "gui_text_fallback_sealed_transitive_risk"),
629 null,
630 null,
631 null,
632 null,
633 null,
634 null,
635 );
636 }
637 comptime {
638 @stardustClaim(
639 alloc_phase.capacity.witness(Storage, "gui_text_fallback_sealed_foreign_risk"),
640 null,
641 null,
642 null,
643 null,
644 null,
645 null,
646 );
647 }
648 const segment = filigree.FallbackSegment{
649 .source = .{ .start = 0, .end = 1 },
650 .glyphs = .{ .start = 0, .end = 1 },
651 .candidate_index = 1,
652 .missing_everywhere = false,
653 };
654 const placement = try payloadPlacement(0, 1, 1);
655 var storage = try activatedStorage(std.testing.allocator, .{ .max_source_units = 1, .cache_entries = 1, .cache_payload_bytes = placement.end });
656 defer storage.deinit(std.testing.allocator);
657 const key = Key{ .content = "x", .primary_identity = 11, .fallback_identity = 22 };
658 const stored = try storage.store(key, &.{segment});
659 const stored_pointer = stored.ptr;
660 const prior = stored[0];
661 const prior_status = storage.status();
662
663 try std.testing.expectError(error.SourceUnitCapacityExceeded, storage.segmentList(2));
664 try std.testing.expectError(error.SegmentCapacityExceeded, storage.store(key, &.{ segment, segment }));
665 try std.testing.expectError(error.CacheEntryCapacityExceeded, storage.store(.{ .content = "y", .primary_identity = 11, .fallback_identity = 22 }, &.{segment}));
666 try std.testing.expectEqual(prior, storage.lookup(key).?[0]);
667 try std.testing.expectEqual(stored_pointer, storage.lookup(key).?.ptr);
668 try std.testing.expectEqual(prior_status.entries, storage.status().entries);
669 try std.testing.expectEqual(prior_status.payload_bytes, storage.status().payload_bytes);
670
671 storage.reset();
672 try std.testing.expectEqual(@as(usize, 0), storage.status().entries);
673 try std.testing.expectEqual(@as(u64, 1), storage.status().rollovers);
674 _ = try storage.store(key, &.{segment});
675 }
676
677 test "fallback segment payload max plus one rejects without cache mutation" {
678 const segment = filigree.FallbackSegment{
679 .source = .{ .start = 0, .end = 1 },
680 .glyphs = .{ .start = 0, .end = 1 },
681 .candidate_index = 1,
682 .missing_everywhere = false,
683 };
684 const placement = try payloadPlacement(0, 1, 1);
685 var storage = try activatedStorage(std.testing.allocator, .{ .max_source_units = 2, .cache_entries = 2, .cache_payload_bytes = placement.end });
686 defer storage.deinit(std.testing.allocator);
687 const key = Key{ .content = "x", .primary_identity = 11, .fallback_identity = 22 };
688 const prior = try storage.store(key, &.{segment});
689 const pointer = prior.ptr;
690 try std.testing.expectError(
691 error.CachePayloadCapacityExceeded,
692 storage.store(.{ .content = "yy", .primary_identity = 11, .fallback_identity = 22 }, &.{segment}),
693 );
694 try std.testing.expectEqual(pointer, storage.lookup(key).?.ptr);
695 try std.testing.expectEqual(@as(usize, 1), storage.status().entries);
696 try std.testing.expectEqual(placement.end, storage.status().payload_bytes);
697 }
698
699 test "fallback segment storage is exported through paint root" {
700 comptime {
701 @stardustClaim(alloc_phase.capacity.witness(Storage, "gui_text_fallback_root"), null, null, null, null, null, null);
702 }
703 try std.testing.expect(@import("root.zig").TextFallbackStorage == Storage);
704 }