lib/filigree/src/fallback/core.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const alloc_phase = @import("alloc_phase");
   3 const font = @import("../font/root.zig");
   4 const shape = @import("../shape/root.zig");
   5 const unicode_data = @import("unicode");
   6 const fallback_model = @import("model.zig");
   7 const workspace_mod = @import("workspace.zig");
   8 
   9 const Allocator = std.mem.Allocator;
  10 
  11 pub const Candidate = struct {
  12     font: *const shape.Font,
  13     variations: []const shape.VariationSetting = &.{},
  14 };
  15 
  16 pub const Input = struct {
  17     base: shape.Input,
  18     candidates: []const Candidate = &.{},
  19 };
  20 
  21 pub const Segment = struct {
  22     source: shape.SourceRange,
  23     glyphs: shape.GlyphSpan,
  24     candidate_index: usize,
  25     missing_everywhere: bool = false,
  26 };
  27 
  28 const SelectedCandidate = struct {
  29     font: *const shape.Font,
  30     variations: []const shape.VariationSetting,
  31 };
  32 
  33 const CandidateSelection = struct {
  34     candidate_index: usize,
  35     missing_everywhere: bool,
  36 };
  37 
  38 const Scalar = fallback_model.Scalar;
  39 const SourceCluster = fallback_model.SourceCluster;
  40 const SegmentRange = fallback_model.SegmentRange;
  41 
  42 const ContextOptions = struct {
  43     script_runs: unicode_data.ScriptRunLimits,
  44     output: shape.Output.Limits,
  45 };
  46 
  47 pub const Context = struct {
  48     allocator: Allocator,
  49     shaper: shape.Context,
  50     script_context: unicode_data.ScriptRunContext,
  51     segment_output: shape.Output,
  52     workspace: workspace_mod.Workspace,
  53 
  54     pub const Options: type = ContextOptions;
  55 
  56     pub fn init(allocator: Allocator, options: Options) !Context {
  57         var workspace = try workspace_mod.Workspace.init(allocator, .{
  58             .max_source_units = options.script_runs.max_source_units,
  59         });
  60         errdefer workspace.deinit(allocator);
  61         workspace.activate();
  62         var script_context = try unicode_data.ScriptRunContext.init(allocator, options.script_runs);
  63         errdefer script_context.deinit(allocator);
  64         script_context.activate();
  65         return .{
  66             .allocator = allocator,
  67             .shaper = shape.Context.init(allocator, .{}),
  68             .script_context = script_context,
  69             .segment_output = try shape.Output.init(allocator, options.output),
  70             .workspace = workspace,
  71         };
  72     }
  73 
  74     pub fn deinit(self: *Context) void {
  75         self.workspace.deinit(self.allocator);
  76         self.segment_output.deinit(self.allocator);
  77         self.script_context.deinit(self.allocator);
  78         self.shaper.deinit();
  79         self.* = undefined;
  80     }
  81 
  82     pub fn reset(self: *Context) void {
  83         self.workspace.reset();
  84         self.segment_output.clearRetainingCapacity();
  85         self.script_context.reset();
  86         self.shaper.reset();
  87     }
  88 
  89     pub fn scriptRunStatus(self: *const Context) unicode_data.ScriptRunStatus {
  90         return self.script_context.status();
  91     }
  92 
  93     pub fn workspaceStatus(self: *const Context) workspace_mod.Status {
  94         return self.workspace.status();
  95     }
  96 
  97     pub fn shapeRun(self: *Context, input: Input, output: *shape.Output) !void {
  98         return self.shapeRunWithSegments(input, output, self.allocator, null);
  99     }
 100 
 101     /// Reported segments grow through segment_allocator and must be released through it.
 102     pub fn shapeRunSegmented(
 103         self: *Context,
 104         input: Input,
 105         output: *shape.Output,
 106         segment_allocator: Allocator,
 107         segments: *std.ArrayListUnmanaged(Segment),
 108     ) !void {
 109         return self.shapeRunWithSegments(input, output, segment_allocator, segments);
 110     }
 111 
 112     fn shapeRunWithSegments(
 113         self: *Context,
 114         input: Input,
 115         output: *shape.Output,
 116         segment_allocator: Allocator,
 117         segments: ?*std.ArrayListUnmanaged(Segment),
 118     ) !void {
 119         const automatic_script = usesAutomaticScript(input.base);
 120         if (input.candidates.len == 0 and !automatic_script) {
 121             if (segments) |items| items.clearRetainingCapacity();
 122             try self.shaper.shapeRun(input.base, output);
 123             if (segments != null) {
 124                 try appendOutputSegment(segment_allocator, segments, try sourceRange(input.base), 0, false, 0, output.glyphs.items.len);
 125             }
 126             return;
 127         }
 128 
 129         const source_byte_len = try input.base.text.byteLen();
 130         if (source_byte_len > std.math.maxInt(u32)) return error.SourceTooLong;
 131         const remaining_source_bytes: usize = std.math.maxInt(u32) - input.base.source_offset;
 132         if (source_byte_len > remaining_source_bytes) return error.SourceTooLong;
 133         try self.workspace.begin(input.base.text.scalarCapacityHint());
 134 
 135         output.clearRetainingCapacity();
 136         self.segment_output.clearRetainingCapacity();
 137         self.script_context.reset();
 138         self.shaper.reset();
 139         if (segments) |items| items.clearRetainingCapacity();
 140         errdefer output.clearRetainingCapacity();
 141         errdefer self.reset();
 142         errdefer if (segments) |items| items.clearRetainingCapacity();
 143 
 144         output.direction = input.base.direction;
 145         output.writing_mode = input.base.writing_mode;
 146         output.output_order = input.base.output_order;
 147 
 148         if (source_byte_len == 0) return;
 149 
 150         try self.collectClusters(input.base.text);
 151         if (automatic_script) try self.assignScripts(input.base.text);
 152         const uses_fallback = self.assignCandidates(input);
 153         if (!uses_fallback and !automatic_script) {
 154             try self.shaper.shapeRun(input.base, output);
 155             try appendOutputSegment(
 156                 segment_allocator,
 157                 segments,
 158                 .{
 159                     .start = input.base.source_offset,
 160                     .end = @intCast(input.base.source_offset + source_byte_len),
 161                 },
 162                 0,
 163                 false,
 164                 0,
 165                 output.glyphs.items.len,
 166             );
 167             return;
 168         }
 169 
 170         try self.buildRanges(automatic_script);
 171         if (input.base.direction == .rtl and input.base.output_order == .visual) {
 172             var range_index = self.workspace.ranges.items.len;
 173             while (range_index > 0) {
 174                 range_index -= 1;
 175                 try self.shapeSegment(input, self.workspace.ranges.items[range_index], output, segment_allocator, segments);
 176             }
 177         } else {
 178             for (self.workspace.ranges.items) |range| {
 179                 try self.shapeSegment(input, range, output, segment_allocator, segments);
 180             }
 181         }
 182     }
 183 
 184     fn assignScripts(self: *Context, source: shape.Source) !void {
 185         const runs = try self.script_context.itemize(.{ .text = source });
 186         var run_index: usize = 0;
 187         for (self.workspace.clusters.items) |*cluster| {
 188             while (run_index + 1 < runs.len and cluster.source.start >= runs[run_index].source.end) run_index += 1;
 189             cluster.script = if (run_index < runs.len and cluster.source.start >= runs[run_index].source.start and cluster.source.end <= runs[run_index].source.end)
 190                 runs[run_index].script
 191             else
 192                 .unknown;
 193         }
 194     }
 195 
 196     fn collectClusters(self: *Context, source: shape.Source) !void {
 197         self.workspace.scalars.clearRetainingCapacity();
 198         self.workspace.clusters.clearRetainingCapacity();
 199         var grapheme_state: unicode_data.GraphemeState = .{};
 200         var iterator = try unicode_data.SourceIterator.init(source, 0);
 201         while (try iterator.next()) |scalar| {
 202             try self.appendScalar(.{
 203                 .codepoint = scalar.codepoint,
 204                 .start = scalar.source.start,
 205                 .end = scalar.source.end,
 206             }, &grapheme_state);
 207         }
 208         self.mergeJoiningClusters();
 209     }
 210 
 211     fn appendScalar(self: *Context, scalar: Scalar, grapheme_state: *unicode_data.GraphemeState) !void {
 212         const scalar_index: u32 = @intCast(self.workspace.scalars.items.len);
 213         std.debug.assert(self.workspace.scalars.items.len < self.workspace.scalars.capacity);
 214         self.workspace.scalars.appendAssumeCapacity(scalar);
 215         const scalar_end = scalar_index + 1;
 216         const merge = grapheme_state.consume(scalar.codepoint);
 217         if (self.workspace.clusters.items.len == 0 or !merge) {
 218             std.debug.assert(self.workspace.clusters.items.len < self.workspace.clusters.capacity);
 219             self.workspace.clusters.appendAssumeCapacity(.{
 220                 .source = .{ .start = scalar.start, .end = scalar.end },
 221                 .scalars = .{ .start = scalar_index, .end = scalar_end },
 222             });
 223             return;
 224         }
 225         const last = &self.workspace.clusters.items[self.workspace.clusters.items.len - 1];
 226         last.source.end = scalar.end;
 227         last.scalars.end = scalar_end;
 228     }
 229 
 230     fn mergeJoiningClusters(self: *Context) void {
 231         if (self.workspace.clusters.items.len < 2) return;
 232         var write_index: usize = 0;
 233         var read_index: usize = 1;
 234         while (read_index < self.workspace.clusters.items.len) : (read_index += 1) {
 235             if (self.clustersJoin(self.workspace.clusters.items[write_index], self.workspace.clusters.items[read_index])) {
 236                 self.workspace.clusters.items[write_index].source.end = self.workspace.clusters.items[read_index].source.end;
 237                 self.workspace.clusters.items[write_index].scalars.end = self.workspace.clusters.items[read_index].scalars.end;
 238             } else {
 239                 write_index += 1;
 240                 if (write_index != read_index) self.workspace.clusters.items[write_index] = self.workspace.clusters.items[read_index];
 241             }
 242         }
 243         self.workspace.clusters.shrinkRetainingCapacity(write_index + 1);
 244     }
 245 
 246     fn clustersJoin(self: *const Context, left: SourceCluster, right: SourceCluster) bool {
 247         const previous = self.lastJoiningType(left) orelse return false;
 248         const next = self.firstJoiningType(right) orelse return false;
 249         return shape.joiningTypesConnect(previous, next);
 250     }
 251 
 252     fn firstJoiningType(self: *const Context, cluster: SourceCluster) ?shape.JoiningType {
 253         var scalar_index: usize = @intCast(cluster.scalars.start);
 254         const scalar_end: usize = @intCast(cluster.scalars.end);
 255         while (scalar_index < scalar_end) : (scalar_index += 1) {
 256             const found = shape.joiningType(self.workspace.scalars.items[scalar_index].codepoint);
 257             if (found != .transparent) return found;
 258         }
 259         return null;
 260     }
 261 
 262     fn lastJoiningType(self: *const Context, cluster: SourceCluster) ?shape.JoiningType {
 263         var scalar_index: usize = @intCast(cluster.scalars.end);
 264         const scalar_start: usize = @intCast(cluster.scalars.start);
 265         while (scalar_index > scalar_start) {
 266             scalar_index -= 1;
 267             const found = shape.joiningType(self.workspace.scalars.items[scalar_index].codepoint);
 268             if (found != .transparent) return found;
 269         }
 270         return null;
 271     }
 272 
 273     fn assignCandidates(self: *Context, input: Input) bool {
 274         var uses_fallback = false;
 275         for (self.workspace.clusters.items) |*cluster| {
 276             const selected = self.bestCandidate(input, cluster.*);
 277             cluster.candidate_index = selected.candidate_index;
 278             cluster.missing_everywhere = selected.missing_everywhere;
 279             uses_fallback = uses_fallback or cluster.candidate_index != 0 or cluster.missing_everywhere;
 280         }
 281         return uses_fallback;
 282     }
 283 
 284     fn bestCandidate(self: *const Context, input: Input, cluster: SourceCluster) CandidateSelection {
 285         if (self.clusterCoveredBy(candidateAt(input, 0), cluster)) return .{ .candidate_index = 0, .missing_everywhere = false };
 286         for (input.candidates, 0..) |_, candidate_index| {
 287             const selected_index = candidate_index + 1;
 288             if (self.clusterCoveredBy(candidateAt(input, selected_index), cluster)) {
 289                 return .{ .candidate_index = selected_index, .missing_everywhere = false };
 290             }
 291         }
 292         return .{ .candidate_index = 0, .missing_everywhere = true };
 293     }
 294 
 295     fn clusterCoveredBy(self: *const Context, candidate: SelectedCandidate, cluster: SourceCluster) bool {
 296         var scalar_index: usize = @intCast(cluster.scalars.start);
 297         const scalar_end: usize = @intCast(cluster.scalars.end);
 298         while (scalar_index < scalar_end) {
 299             const scalar = self.workspace.scalars.items[scalar_index];
 300             if (scalar_index + 1 < scalar_end) {
 301                 const next = self.workspace.scalars.items[scalar_index + 1];
 302                 if (isVariationSelector(next.codepoint)) {
 303                     if (candidate.font.face.glyphIdForVariation(scalar.codepoint, next.codepoint)) |glyph_id| {
 304                         if (glyph_id == 0) return false;
 305                         scalar_index += 2;
 306                         continue;
 307                     }
 308                 }
 309             }
 310             if (!coverageIgnoresScalar(scalar.codepoint) and candidate.font.face.glyphId(scalar.codepoint) == 0) return false;
 311             scalar_index += 1;
 312         }
 313         return true;
 314     }
 315 
 316     fn buildRanges(self: *Context, split_by_script: bool) !void {
 317         self.workspace.ranges.clearRetainingCapacity();
 318         var cluster_start: usize = 0;
 319         while (cluster_start < self.workspace.clusters.items.len) {
 320             const candidate_index = self.workspace.clusters.items[cluster_start].candidate_index;
 321             const missing_everywhere = self.workspace.clusters.items[cluster_start].missing_everywhere;
 322             const range_script = self.workspace.clusters.items[cluster_start].script;
 323             var cluster_end = cluster_start + 1;
 324             while (cluster_end < self.workspace.clusters.items.len and
 325                 self.workspace.clusters.items[cluster_end].candidate_index == candidate_index and
 326                 self.workspace.clusters.items[cluster_end].missing_everywhere == missing_everywhere and
 327                 (!split_by_script or self.workspace.clusters.items[cluster_end].script == range_script))
 328             {
 329                 cluster_end += 1;
 330             }
 331             std.debug.assert(self.workspace.ranges.items.len < self.workspace.ranges.capacity);
 332             self.workspace.ranges.appendAssumeCapacity(.{
 333                 .cluster_start = @intCast(cluster_start),
 334                 .cluster_end = @intCast(cluster_end),
 335                 .candidate_index = candidate_index,
 336                 .missing_everywhere = missing_everywhere,
 337                 .script = range_script,
 338             });
 339             cluster_start = cluster_end;
 340         }
 341     }
 342 
 343     fn shapeSegment(
 344         self: *Context,
 345         input: Input,
 346         range: SegmentRange,
 347         output: *shape.Output,
 348         segment_allocator: Allocator,
 349         segments: ?*std.ArrayListUnmanaged(Segment),
 350     ) !void {
 351         const cluster_start: usize = @intCast(range.cluster_start);
 352         const cluster_end: usize = @intCast(range.cluster_end);
 353         const source_start = self.workspace.clusters.items[cluster_start].source.start;
 354         const source_end = self.workspace.clusters.items[cluster_end - 1].source.end;
 355         const selected = candidateAt(input, range.candidate_index);
 356         const glyph_start = output.glyphs.items.len;
 357         var segment_input = input.base;
 358         segment_input.font = selected.font;
 359         segment_input.variations = selected.variations;
 360         segment_input.text = sliceSource(input.base.text, source_start, source_end);
 361         segment_input.source_offset = try addSourceOffset(input.base.source_offset, source_start);
 362         segment_input.pre_context = null;
 363         segment_input.post_context = null;
 364 
 365         if (usesAutomaticScript(input.base)) {
 366             const script_tags = unicode_data.scriptOpenTypeTags(range.script);
 367             segment_input.script = script_tags.tags[0];
 368             segment_input.script_tags = script_tags.slice();
 369             try self.shaper.shapeRun(segment_input, &self.segment_output);
 370         } else {
 371             try self.shaper.shapeRun(segment_input, &self.segment_output);
 372         }
 373         try appendSegmentOutput(output, &self.segment_output);
 374         try appendOutputSegment(
 375             segment_allocator,
 376             segments,
 377             .{
 378                 .start = try addSourceOffset(input.base.source_offset, source_start),
 379                 .end = try addSourceOffset(input.base.source_offset, source_end),
 380             },
 381             range.candidate_index,
 382             range.missing_everywhere,
 383             glyph_start,
 384             output.glyphs.items.len,
 385         );
 386     }
 387 };
 388 
 389 fn usesAutomaticScript(input: shape.Input) bool {
 390     return input.script == font.defaultScriptTag and input.script_tags.len == 0;
 391 }
 392 
 393 fn candidateAt(input: Input, index: usize) SelectedCandidate {
 394     if (index == 0) {
 395         return .{
 396             .font = input.base.font,
 397             .variations = input.base.variations,
 398         };
 399     }
 400     const candidate = input.candidates[index - 1];
 401     return .{
 402         .font = candidate.font,
 403         .variations = candidate.variations,
 404     };
 405 }
 406 
 407 fn appendSegmentOutput(output: *shape.Output, segment_output: *const shape.Output) !void {
 408     const glyph_offset = output.glyphs.items.len;
 409     const cluster_offset = output.clusters.items.len;
 410     const caret_offset = output.ligature_carets.items.len;
 411 
 412     if (glyph_offset > std.math.maxInt(u32) or cluster_offset > std.math.maxInt(u32) or caret_offset > std.math.maxInt(u32)) return error.OutputTooLarge;
 413     try output.ensureUnused(
 414         segment_output.glyphs.items.len,
 415         segment_output.clusters.items.len,
 416         segment_output.ligature_carets.items.len,
 417     );
 418     output.ligature_carets.appendSliceAssumeCapacity(segment_output.ligature_carets.items);
 419 
 420     const glyph_offset_u32: u32 = @intCast(glyph_offset);
 421     const cluster_offset_u32: u32 = @intCast(cluster_offset);
 422     const caret_offset_u32: u32 = @intCast(caret_offset);
 423 
 424     for (segment_output.glyphs.items) |glyph| {
 425         var appended = glyph;
 426         appended.cluster_index = try addU32(appended.cluster_index, cluster_offset_u32);
 427         if (appended.ligature_caret_count != 0) appended.ligature_caret_start = try addU32(appended.ligature_caret_start, caret_offset_u32);
 428         output.glyphs.appendAssumeCapacity(appended);
 429     }
 430 
 431     for (segment_output.clusters.items) |cluster| {
 432         var appended = cluster;
 433         appended.glyphs.start = try addU32(appended.glyphs.start, glyph_offset_u32);
 434         appended.glyphs.end = try addU32(appended.glyphs.end, glyph_offset_u32);
 435         output.clusters.appendAssumeCapacity(appended);
 436     }
 437 
 438     output.total_x_advance = addClampedI32(output.total_x_advance, segment_output.total_x_advance);
 439     output.total_y_advance = addClampedI32(output.total_y_advance, segment_output.total_y_advance);
 440 }
 441 
 442 fn appendOutputSegment(
 443     segment_allocator: Allocator,
 444     segments: ?*std.ArrayListUnmanaged(Segment),
 445     source: shape.SourceRange,
 446     candidate_index: usize,
 447     missing_everywhere: bool,
 448     glyph_start: usize,
 449     glyph_end: usize,
 450 ) !void {
 451     if (glyph_start == glyph_end) return;
 452     if (segments) |items| {
 453         if (glyph_start > std.math.maxInt(u32) or glyph_end > std.math.maxInt(u32)) return error.OutputTooLarge;
 454         try items.append(segment_allocator, .{
 455             .source = source,
 456             .glyphs = .{
 457                 .start = @intCast(glyph_start),
 458                 .end = @intCast(glyph_end),
 459             },
 460             .candidate_index = candidate_index,
 461             .missing_everywhere = missing_everywhere,
 462         });
 463     }
 464 }
 465 
 466 fn sourceRange(input: shape.Input) !shape.SourceRange {
 467     const source_byte_len = try input.text.byteLen();
 468     if (source_byte_len > std.math.maxInt(u32)) return error.SourceTooLong;
 469     if (source_byte_len > std.math.maxInt(u32) - input.source_offset) return error.SourceTooLong;
 470     return .{
 471         .start = input.source_offset,
 472         .end = @intCast(input.source_offset + source_byte_len),
 473     };
 474 }
 475 
 476 fn addSourceOffset(source_offset: u32, relative_start: u32) !u32 {
 477     if (relative_start > std.math.maxInt(u32) - source_offset) return error.SourceTooLong;
 478     return source_offset + relative_start;
 479 }
 480 
 481 fn sliceSource(source: shape.Source, start: u32, end: u32) shape.Source {
 482     const slice_start: usize = @intCast(start);
 483     const slice_end: usize = @intCast(end);
 484     return switch (source) {
 485         .utf8 => |text| .{ .utf8 = text[slice_start..slice_end] },
 486         .utf16 => |text| .{ .utf16 = text[slice_start / 2 .. slice_end / 2] },
 487         .utf32 => |text| .{ .utf32 = text[slice_start / 4 .. slice_end / 4] },
 488     };
 489 }
 490 
 491 fn addU32(a: u32, b: u32) !u32 {
 492     if (b > std.math.maxInt(u32) - a) return error.OutputTooLarge;
 493     return a + b;
 494 }
 495 
 496 fn addClampedI32(a: i32, b: i32) i32 {
 497     const sum = @as(i64, a) + @as(i64, b);
 498     if (sum > std.math.maxInt(i32)) return std.math.maxInt(i32);
 499     if (sum < std.math.minInt(i32)) return std.math.minInt(i32);
 500     return @intCast(sum);
 501 }
 502 
 503 fn coverageIgnoresScalar(codepoint: u21) bool {
 504     return isVariationSelector(codepoint) or
 505         codepoint == 0x034f or
 506         codepoint == 0x061c or
 507         codepoint == 0x180e or
 508         (codepoint >= 0x200b and codepoint <= 0x200f) or
 509         (codepoint >= 0x202a and codepoint <= 0x202e) or
 510         (codepoint >= 0x2060 and codepoint <= 0x206f) or
 511         codepoint == 0x3164 or
 512         codepoint == 0xfeff or
 513         codepoint == 0xffa0 or
 514         (codepoint >= 0xfff0 and codepoint <= 0xfff8) or
 515         (codepoint >= 0x1bca0 and codepoint <= 0x1bca3) or
 516         (codepoint >= 0x1d173 and codepoint <= 0x1d17a) or
 517         (codepoint >= 0xe0000 and codepoint <= 0xe0fff);
 518 }
 519 
 520 fn isVariationSelector(codepoint: u21) bool {
 521     return (codepoint >= 0xfe00 and codepoint <= 0xfe0f) or
 522         (codepoint >= 0xe0100 and codepoint <= 0xe01ef);
 523 }
 524 
 525 test "Filigree fallback exposes script source unit capacity" {
 526     comptime {
 527         alloc_phase.capacity.record(
 528             alloc_phase.capacity.witness(@import("unicode").ScriptRunContext, "unicode_script_run_consumer"),
 529         );
 530     }
 531 
 532     var context = try Context.init(std.testing.allocator, .{
 533         .script_runs = .{ .max_source_units = 3 },
 534         .output = .{ .max_glyphs = 256, .max_ligature_carets = 256 },
 535     });
 536     defer context.deinit();
 537     const status = context.scriptRunStatus();
 538     try std.testing.expectEqual(@as(usize, 3), status.max_source_units);
 539     try std.testing.expectEqual(
 540         (try unicode_data.ScriptRunCapacity.derive(.{ .max_source_units = 3 })).storage_bytes,
 541         status.storage_bytes,
 542     );
 543 }
 544 
 545 test "Context uses fallback font for missing utf8 clusters" {
 546     const allocator = std.testing.allocator;
 547     const primary_bytes = try asciiRangeFont(allocator, 'A', 'A', false);
 548     defer allocator.free(primary_bytes);
 549     const fallback_bytes = try fixture_font.create(allocator);
 550     defer allocator.free(fallback_bytes);
 551 
 552     var primary_font = shape.Font.initFromBytes(primary_bytes.ptr, primary_bytes.len).?;
 553     defer primary_font.deinit();
 554     var fallback_font = shape.Font.initFromBytes(fallback_bytes.ptr, fallback_bytes.len).?;
 555     defer fallback_font.deinit();
 556 
 557     var context = try Context.init(allocator, .{
 558         .script_runs = .{ .max_source_units = 2 },
 559         .output = .{ .max_glyphs = 256, .max_ligature_carets = 256 },
 560     });
 561     defer context.deinit();
 562     var output = try shape.Output.init(allocator, .{
 563         .max_glyphs = 256,
 564         .max_ligature_carets = 256,
 565     });
 566     defer output.deinit(allocator);
 567 
 568     const candidates = [_]Candidate{.{ .font = &fallback_font }};
 569     try context.shapeRun(.{
 570         .base = .{ .font = &primary_font, .text = .{ .utf8 = "AB" } },
 571         .candidates = &candidates,
 572     }, &output);
 573 
 574     const run = output.run();
 575     try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
 576     try std.testing.expectEqual(@as(u32, 'A' - 31), run.glyphs[0].glyph_id);
 577     try std.testing.expectEqual(@as(u32, 'B' - 31), run.glyphs[1].glyph_id);
 578     try std.testing.expect(!run.glyphs[1].flags.missing_glyph);
 579     try std.testing.expectEqual(shape.SourceRange{ .start = 0, .end = 1 }, run.clusters[0].source);
 580     try std.testing.expectEqual(shape.SourceRange{ .start = 1, .end = 2 }, run.clusters[1].source);
 581 }
 582 
 583 test "Context reports segmented fallback font selection" {
 584     const allocator = std.testing.allocator;
 585     const primary_bytes = try asciiRangeFont(allocator, 'A', 'A', false);
 586     defer allocator.free(primary_bytes);
 587     const fallback_bytes = try fixture_font.create(allocator);
 588     defer allocator.free(fallback_bytes);
 589 
 590     var primary_font = shape.Font.initFromBytes(primary_bytes.ptr, primary_bytes.len).?;
 591     defer primary_font.deinit();
 592     var fallback_font = shape.Font.initFromBytes(fallback_bytes.ptr, fallback_bytes.len).?;
 593     defer fallback_font.deinit();
 594 
 595     var context = try Context.init(allocator, .{
 596         .script_runs = .{ .max_source_units = 2 },
 597         .output = .{ .max_glyphs = 256, .max_ligature_carets = 256 },
 598     });
 599     defer context.deinit();
 600     var output = try shape.Output.init(allocator, .{
 601         .max_glyphs = 256,
 602         .max_ligature_carets = 256,
 603     });
 604     defer output.deinit(allocator);
 605     var segments: std.ArrayListUnmanaged(Segment) = .empty;
 606     defer segments.deinit(allocator);
 607 
 608     const candidates = [_]Candidate{.{ .font = &fallback_font }};
 609     try context.shapeRunSegmented(.{
 610         .base = .{ .font = &primary_font, .text = .{ .utf8 = "AB" } },
 611         .candidates = &candidates,
 612     }, &output, allocator, &segments);
 613 
 614     try std.testing.expectEqual(@as(usize, 2), segments.items.len);
 615     try std.testing.expectEqual(shape.SourceRange{ .start = 0, .end = 1 }, segments.items[0].source);
 616     try std.testing.expectEqual(shape.GlyphSpan{ .start = 0, .end = 1 }, segments.items[0].glyphs);
 617     try std.testing.expectEqual(@as(usize, 0), segments.items[0].candidate_index);
 618     try std.testing.expect(!segments.items[0].missing_everywhere);
 619     try std.testing.expectEqual(shape.SourceRange{ .start = 1, .end = 2 }, segments.items[1].source);
 620     try std.testing.expectEqual(shape.GlyphSpan{ .start = 1, .end = 2 }, segments.items[1].glyphs);
 621     try std.testing.expectEqual(@as(usize, 1), segments.items[1].candidate_index);
 622     try std.testing.expect(!segments.items[1].missing_everywhere);
 623 }
 624 
 625 test "Context reports a final notdef segment when every face is missing" {
 626     const allocator = std.testing.allocator;
 627     const primary_bytes = try asciiRangeFont(allocator, 'A', 'A', false);
 628     defer allocator.free(primary_bytes);
 629     const fallback_bytes = try fixture_font.create(allocator);
 630     defer allocator.free(fallback_bytes);
 631     var primary_font = shape.Font.initFromBytes(primary_bytes.ptr, primary_bytes.len).?;
 632     defer primary_font.deinit();
 633     var fallback_font = shape.Font.initFromBytes(fallback_bytes.ptr, fallback_bytes.len).?;
 634     defer fallback_font.deinit();
 635     var context = try Context.init(allocator, .{
 636         .script_runs = .{ .max_source_units = 2 },
 637         .output = .{ .max_glyphs = 256, .max_ligature_carets = 256 },
 638     });
 639     defer context.deinit();
 640     var output = try shape.Output.init(allocator, .{
 641         .max_glyphs = 256,
 642         .max_ligature_carets = 256,
 643     });
 644     defer output.deinit(allocator);
 645     var segments: std.ArrayListUnmanaged(Segment) = .empty;
 646     defer segments.deinit(allocator);
 647     const candidates = [_]Candidate{.{ .font = &fallback_font }};
 648 
 649     try context.shapeRunSegmented(.{
 650         .base = .{ .font = &primary_font, .text = .{ .utf8 = "\u{3b2}" } },
 651         .candidates = &candidates,
 652     }, &output, allocator, &segments);
 653 
 654     try std.testing.expectEqual(@as(usize, 1), segments.items.len);
 655     try std.testing.expectEqual(shape.SourceRange{ .start = 0, .end = 2 }, segments.items[0].source);
 656     try std.testing.expectEqual(@as(usize, 0), segments.items[0].candidate_index);
 657     try std.testing.expect(segments.items[0].missing_everywhere);
 658     try std.testing.expectEqual(@as(u32, 0), output.run().glyphs[0].glyph_id);
 659 }
 660 
 661 test "Context accepts exact fallback source capacity and rejects max plus one transactionally" {
 662     const allocator = std.testing.allocator;
 663     const primary_bytes = try asciiRangeFont(allocator, 'A', 'A', false);
 664     defer allocator.free(primary_bytes);
 665     const fallback_bytes = try fixture_font.create(allocator);
 666     defer allocator.free(fallback_bytes);
 667     var primary_font = shape.Font.initFromBytes(primary_bytes.ptr, primary_bytes.len).?;
 668     defer primary_font.deinit();
 669     var fallback_font = shape.Font.initFromBytes(fallback_bytes.ptr, fallback_bytes.len).?;
 670     defer fallback_font.deinit();
 671     var context = try Context.init(allocator, .{
 672         .script_runs = .{ .max_source_units = 2 },
 673         .output = .{ .max_glyphs = 256, .max_ligature_carets = 256 },
 674     });
 675     defer context.deinit();
 676     var output = try shape.Output.init(allocator, .{
 677         .max_glyphs = 256,
 678         .max_ligature_carets = 256,
 679     });
 680     defer output.deinit(allocator);
 681     var segments: std.ArrayListUnmanaged(Segment) = .empty;
 682     defer segments.deinit(allocator);
 683     const candidates = [_]Candidate{.{ .font = &fallback_font }};
 684     try context.shapeRunSegmented(.{
 685         .base = .{ .font = &primary_font, .text = .{ .utf8 = "AB" } },
 686         .candidates = &candidates,
 687     }, &output, allocator, &segments);
 688     const prior_glyphs = output.glyphs.items[0..2].*;
 689     const prior_segments = segments.items[0..2].*;
 690     const output_pointer = output.glyphs.items.ptr;
 691     const segment_pointer = segments.items.ptr;
 692     const segment_capacity = segments.capacity;
 693     const workspace_status = context.workspaceStatus();
 694 
 695     try std.testing.expectError(error.SourceUnitCapacityExceeded, context.shapeRunSegmented(.{
 696         .base = .{ .font = &primary_font, .text = .{ .utf8 = "ABC" } },
 697         .candidates = &candidates,
 698     }, &output, allocator, &segments));
 699 
 700     try std.testing.expectEqualSlices(shape.ShapedGlyph, &prior_glyphs, output.glyphs.items);
 701     try std.testing.expectEqualSlices(Segment, &prior_segments, segments.items);
 702     try std.testing.expectEqual(output_pointer, output.glyphs.items.ptr);
 703     try std.testing.expectEqual(segment_pointer, segments.items.ptr);
 704     try std.testing.expectEqual(segment_capacity, segments.capacity);
 705     try std.testing.expectEqual(workspace_status, context.workspaceStatus());
 706 }
 707 
 708 test "Context uses fallback font for missing utf16 clusters" {
 709     const allocator = std.testing.allocator;
 710     const primary_bytes = try asciiRangeFont(allocator, 'A', 'A', false);
 711     defer allocator.free(primary_bytes);
 712     const fallback_bytes = try fixture_font.create(allocator);
 713     defer allocator.free(fallback_bytes);
 714 
 715     var primary_font = shape.Font.initFromBytes(primary_bytes.ptr, primary_bytes.len).?;
 716     defer primary_font.deinit();
 717     var fallback_font = shape.Font.initFromBytes(fallback_bytes.ptr, fallback_bytes.len).?;
 718     defer fallback_font.deinit();
 719 
 720     var context = try Context.init(allocator, .{
 721         .script_runs = .{ .max_source_units = 2 },
 722         .output = .{ .max_glyphs = 256, .max_ligature_carets = 256 },
 723     });
 724     defer context.deinit();
 725     var output = try shape.Output.init(allocator, .{
 726         .max_glyphs = 256,
 727         .max_ligature_carets = 256,
 728     });
 729     defer output.deinit(allocator);
 730 
 731     const text = [_]u16{ 'A', 'B' };
 732     const candidates = [_]Candidate{.{ .font = &fallback_font }};
 733     try context.shapeRun(.{
 734         .base = .{ .font = &primary_font, .text = .{ .utf16 = &text } },
 735         .candidates = &candidates,
 736     }, &output);
 737 
 738     const run = output.run();
 739     try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
 740     try std.testing.expectEqual(@as(u32, 'A' - 31), run.glyphs[0].glyph_id);
 741     try std.testing.expectEqual(@as(u32, 'B' - 31), run.glyphs[1].glyph_id);
 742     try std.testing.expect(!run.glyphs[1].flags.missing_glyph);
 743     try std.testing.expectEqual(shape.SourceRange{ .start = 0, .end = 2 }, run.clusters[0].source);
 744     try std.testing.expectEqual(shape.SourceRange{ .start = 2, .end = 4 }, run.clusters[1].source);
 745 }
 746 
 747 test "Context uses fallback font for missing utf32 clusters" {
 748     const allocator = std.testing.allocator;
 749     const primary_bytes = try asciiRangeFont(allocator, 'A', 'A', false);
 750     defer allocator.free(primary_bytes);
 751     const fallback_bytes = try fixture_font.create(allocator);
 752     defer allocator.free(fallback_bytes);
 753 
 754     var primary_font = shape.Font.initFromBytes(primary_bytes.ptr, primary_bytes.len).?;
 755     defer primary_font.deinit();
 756     var fallback_font = shape.Font.initFromBytes(fallback_bytes.ptr, fallback_bytes.len).?;
 757     defer fallback_font.deinit();
 758 
 759     var context = try Context.init(allocator, .{
 760         .script_runs = .{ .max_source_units = 2 },
 761         .output = .{ .max_glyphs = 256, .max_ligature_carets = 256 },
 762     });
 763     defer context.deinit();
 764     var output = try shape.Output.init(allocator, .{
 765         .max_glyphs = 256,
 766         .max_ligature_carets = 256,
 767     });
 768     defer output.deinit(allocator);
 769 
 770     const text = [_]u32{ 'A', 'B' };
 771     const candidates = [_]Candidate{.{ .font = &fallback_font }};
 772     try context.shapeRun(.{
 773         .base = .{ .font = &primary_font, .text = .{ .utf32 = &text } },
 774         .candidates = &candidates,
 775     }, &output);
 776 
 777     const run = output.run();
 778     try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
 779     try std.testing.expectEqual(@as(u32, 'A' - 31), run.glyphs[0].glyph_id);
 780     try std.testing.expectEqual(@as(u32, 'B' - 31), run.glyphs[1].glyph_id);
 781     try std.testing.expect(!run.glyphs[1].flags.missing_glyph);
 782     try std.testing.expectEqual(shape.SourceRange{ .start = 0, .end = 4 }, run.clusters[0].source);
 783     try std.testing.expectEqual(shape.SourceRange{ .start = 4, .end = 8 }, run.clusters[1].source);
 784 }
 785 
 786 test "Context keeps fallback selection on grapheme boundaries" {
 787     const allocator = std.testing.allocator;
 788     const primary_bytes = try asciiRangeFont(allocator, 'A', 'A', false);
 789     defer allocator.free(primary_bytes);
 790     const fallback_bytes = try fixture_font.create(allocator);
 791     defer allocator.free(fallback_bytes);
 792 
 793     var primary_font = shape.Font.initFromBytes(primary_bytes.ptr, primary_bytes.len).?;
 794     defer primary_font.deinit();
 795     var fallback_font = shape.Font.initFromBytes(fallback_bytes.ptr, fallback_bytes.len).?;
 796     defer fallback_font.deinit();
 797 
 798     var context = try Context.init(allocator, .{
 799         .script_runs = .{ .max_source_units = 4 },
 800         .output = .{ .max_glyphs = 256, .max_ligature_carets = 256 },
 801     });
 802     defer context.deinit();
 803     var output = try shape.Output.init(allocator, .{
 804         .max_glyphs = 256,
 805         .max_ligature_carets = 256,
 806     });
 807     defer output.deinit(allocator);
 808 
 809     const candidates = [_]Candidate{.{ .font = &fallback_font }};
 810     try context.shapeRun(.{
 811         .base = .{
 812             .font = &primary_font,
 813             .text = .{ .utf8 = "A\u{301}B" },
 814             .cluster_mode = .monotone_graphemes,
 815         },
 816         .candidates = &candidates,
 817     }, &output);
 818 
 819     const run = output.run();
 820     try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
 821     try std.testing.expectEqual(@as(usize, 2), run.clusters.len);
 822     try std.testing.expectEqual(shape.SourceRange{ .start = 0, .end = 3 }, run.clusters[0].source);
 823     try std.testing.expectEqual(shape.GlyphSpan{ .start = 0, .end = 2 }, run.clusters[0].glyphs);
 824     try std.testing.expectEqual(@as(u32, 'B' - 31), run.glyphs[2].glyph_id);
 825     try std.testing.expect(!run.glyphs[2].flags.missing_glyph);
 826 }
 827 
 828 test "Context keeps variation sequences on one fallback font" {
 829     const allocator = std.testing.allocator;
 830     const primary_bytes = try asciiRangeFont(allocator, 'C', 'C', false);
 831     defer allocator.free(primary_bytes);
 832     const fallback_bytes = try fixture_font.createWithVariationSequences(allocator);
 833     defer allocator.free(fallback_bytes);
 834 
 835     var primary_font = shape.Font.initFromBytes(primary_bytes.ptr, primary_bytes.len).?;
 836     defer primary_font.deinit();
 837     var fallback_font = shape.Font.initFromBytes(fallback_bytes.ptr, fallback_bytes.len).?;
 838     defer fallback_font.deinit();
 839 
 840     var context = try Context.init(allocator, .{
 841         .script_runs = .{ .max_source_units = 5 },
 842         .output = .{ .max_glyphs = 256, .max_ligature_carets = 256 },
 843     });
 844     defer context.deinit();
 845     var output = try shape.Output.init(allocator, .{
 846         .max_glyphs = 256,
 847         .max_ligature_carets = 256,
 848     });
 849     defer output.deinit(allocator);
 850 
 851     const candidates = [_]Candidate{.{ .font = &fallback_font }};
 852     try context.shapeRun(.{
 853         .base = .{
 854             .font = &primary_font,
 855             .text = .{ .utf8 = "A\u{fe0f}C" },
 856             .cluster_mode = .monotone_graphemes,
 857         },
 858         .candidates = &candidates,
 859     }, &output);
 860 
 861     const run = output.run();
 862     try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
 863     try std.testing.expectEqual(@as(u32, 96), run.glyphs[0].glyph_id);
 864     try std.testing.expectEqual(shape.SourceRange{ .start = 0, .end = 4 }, run.clusters[0].source);
 865     try std.testing.expectEqual(@as(u32, 'C' - 31), run.glyphs[1].glyph_id);
 866     try std.testing.expectEqual(shape.SourceRange{ .start = 4, .end = 5 }, run.clusters[1].source);
 867 }
 868 
 869 test "Context keeps Arabic joining sequences on one fallback font" {
 870     const allocator = std.testing.allocator;
 871     const primary_bytes = try fixture_font.createWithGsubArabicJoiningForms(allocator);
 872     defer allocator.free(primary_bytes);
 873     try patchFormat12GlyphId(primary_bytes, 0x0628, 0);
 874     const fallback_bytes = try fixture_font.createWithGsubArabicJoiningForms(allocator);
 875     defer allocator.free(fallback_bytes);
 876 
 877     var primary_font = shape.Font.initFromBytes(primary_bytes.ptr, primary_bytes.len).?;
 878     defer primary_font.deinit();
 879     var fallback_font = shape.Font.initFromBytes(fallback_bytes.ptr, fallback_bytes.len).?;
 880     defer fallback_font.deinit();
 881 
 882     var context = try Context.init(allocator, .{
 883         .script_runs = .{ .max_source_units = 4 },
 884         .output = .{ .max_glyphs = 256, .max_ligature_carets = 256 },
 885     });
 886     defer context.deinit();
 887     var output = try shape.Output.init(allocator, .{
 888         .max_glyphs = 256,
 889         .max_ligature_carets = 256,
 890     });
 891     defer output.deinit(allocator);
 892 
 893     const candidates = [_]Candidate{.{ .font = &fallback_font }};
 894     try context.shapeRun(.{
 895         .base = .{
 896             .font = &primary_font,
 897             .text = .{ .utf8 = "\u{0628}\u{0627}" },
 898             .direction = .rtl,
 899             .output_order = .logical,
 900         },
 901         .candidates = &candidates,
 902     }, &output);
 903 
 904     const run = output.run();
 905     try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
 906     try std.testing.expectEqual(@as(u32, 'I' - 31), run.glyphs[0].glyph_id);
 907     try std.testing.expectEqual(@as(u32, 'B' - 31), run.glyphs[1].glyph_id);
 908     try std.testing.expectEqual(shape.SourceRange{ .start = 0, .end = 2 }, run.clusters[0].source);
 909     try std.testing.expectEqual(shape.SourceRange{ .start = 2, .end = 4 }, run.clusters[1].source);
 910 }
 911 
 912 test "Context itemizes script before shaping without fallback candidates" {
 913     const allocator = std.testing.allocator;
 914     const bytes = try fixture_font.createWithGsubLigature(allocator);
 915     defer allocator.free(bytes);
 916     try fixture_binary.replaceLayoutScript(bytes, "GSUB", "latn");
 917 
 918     var shaped_font = shape.Font.initFromBytes(bytes.ptr, bytes.len).?;
 919     defer shaped_font.deinit();
 920 
 921     var direct_context = shape.Context.init(allocator, .{});
 922     defer direct_context.deinit();
 923     var direct_output = try shape.Output.init(allocator, .{
 924         .max_glyphs = 256,
 925         .max_ligature_carets = 256,
 926     });
 927     defer direct_output.deinit(allocator);
 928 
 929     try direct_context.shapeRun(.{ .font = &shaped_font, .text = .{ .utf8 = "fi" } }, &direct_output);
 930     try std.testing.expectEqual(@as(usize, 2), direct_output.run().glyphs.len);
 931 
 932     var context = try Context.init(allocator, .{
 933         .script_runs = .{ .max_source_units = 2 },
 934         .output = .{ .max_glyphs = 256, .max_ligature_carets = 256 },
 935     });
 936     defer context.deinit();
 937     var output = try shape.Output.init(allocator, .{
 938         .max_glyphs = 256,
 939         .max_ligature_carets = 256,
 940     });
 941     defer output.deinit(allocator);
 942 
 943     try context.shapeRun(.{
 944         .base = .{ .font = &shaped_font, .text = .{ .utf8 = "fi" } },
 945     }, &output);
 946 
 947     const run = output.run();
 948     try std.testing.expectEqual(@as(usize, 1), run.glyphs.len);
 949     try std.testing.expectEqual(@as(u32, 96), run.glyphs[0].glyph_id);
 950     try std.testing.expectEqual(shape.SourceRange{ .start = 0, .end = 2 }, run.clusters[0].source);
 951 }
 952 
 953 const fixture_font = @import("../fixture/root.zig");
 954 const fixture_binary = fixture_font.binary;
 955 
 956 fn asciiRangeFont(allocator: Allocator, start_code: u16, end_code: u16, variation_sequences: bool) ![]u8 {
 957     const bytes = if (variation_sequences)
 958         try fixture_font.createWithVariationSequences(allocator)
 959     else
 960         try fixture_font.create(allocator);
 961     errdefer allocator.free(bytes);
 962     try patchAsciiRange(bytes, start_code, end_code);
 963     return bytes;
 964 }
 965 
 966 fn patchAsciiRange(bytes: []u8, start_code: u16, end_code: u16) !void {
 967     const cmap_offset = try fixture_binary.tableOffset(bytes, "cmap");
 968     if (cmap_offset > bytes.len or bytes.len - cmap_offset < 4) return error.InvalidFont;
 969     const subtable_count = readU16(bytes, cmap_offset + 2);
 970     if (@as(usize, subtable_count) > (bytes.len - cmap_offset - 4) / 8) return error.InvalidFont;
 971 
 972     for (0..subtable_count) |subtable_index| {
 973         const record = cmap_offset + 4 + subtable_index * 8;
 974         const subtable_offset = readU32(bytes, record + 4);
 975         const subtable_start = cmap_offset + @as(usize, @intCast(subtable_offset));
 976         if (subtable_start > bytes.len or bytes.len - subtable_start < 2) return error.InvalidFont;
 977         if (readU16(bytes, subtable_start) != 4) continue;
 978         if (bytes.len - subtable_start < 40) return error.InvalidFont;
 979         writeU16(bytes, subtable_start + 14, end_code);
 980         writeU16(bytes, subtable_start + 22, start_code);
 981         return;
 982     }
 983 
 984     return error.MissingCmapFormat4;
 985 }
 986 
 987 fn patchFormat12GlyphId(bytes: []u8, codepoint: u32, glyph_id: u32) !void {
 988     const cmap_offset = try fixture_binary.tableOffset(bytes, "cmap");
 989     if (cmap_offset > bytes.len or bytes.len - cmap_offset < 4) return error.InvalidFont;
 990     const subtable_count = readU16(bytes, cmap_offset + 2);
 991     if (@as(usize, subtable_count) > (bytes.len - cmap_offset - 4) / 8) return error.InvalidFont;
 992 
 993     for (0..subtable_count) |subtable_index| {
 994         const record = cmap_offset + 4 + subtable_index * 8;
 995         const subtable_offset = readU32(bytes, record + 4);
 996         const subtable_start = cmap_offset + @as(usize, @intCast(subtable_offset));
 997         if (subtable_start > bytes.len or bytes.len - subtable_start < 16) return error.InvalidFont;
 998         if (readU16(bytes, subtable_start) != 12) continue;
 999         const group_count = readU32(bytes, subtable_start + 12);
1000         if (@as(usize, @intCast(group_count)) > (bytes.len - subtable_start - 16) / 12) return error.InvalidFont;
1001         for (0..group_count) |group_index| {
1002             const group = subtable_start + 16 + group_index * 12;
1003             const start = readU32(bytes, group);
1004             const end = readU32(bytes, group + 4);
1005             if (codepoint < start or codepoint > end) continue;
1006             if (start != end) return error.UnsupportedFixturePatch;
1007             writeU32(bytes, group + 8, glyph_id);
1008             return;
1009         }
1010     }
1011 
1012     return error.MissingCmapFormat12;
1013 }
1014 
1015 fn readU16(bytes: []const u8, offset: usize) u16 {
1016     return std.mem.readInt(u16, bytes[offset..][0..2], .big);
1017 }
1018 
1019 fn readU32(bytes: []const u8, offset: usize) u32 {
1020     return std.mem.readInt(u32, bytes[offset..][0..4], .big);
1021 }
1022 
1023 fn writeU16(bytes: []u8, offset: usize, value: u16) void {
1024     std.mem.writeInt(u16, bytes[offset..][0..2], value, .big);
1025 }
1026 
1027 fn writeU32(bytes: []u8, offset: usize, value: u32) void {
1028     std.mem.writeInt(u32, bytes[offset..][0..4], value, .big);
1029 }