lib/gui/src/paint/accy.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const gpu = @import("gpu");
   3 const accy = @import("accy");
   4 
   5 const command = @import("command.zig");
   6 const cpu = @import("cpu/root.zig");
   7 const gui = @import("../root.zig");
   8 
   9 const Allocator = std.mem.Allocator;
  10 const Color = gui.model.UiColor;
  11 const Command = command.Command;
  12 const ImageSet = command.ImageSet;
  13 const Region = cpu.Region;
  14 const Rect = gui.layout.Rect;
  15 const kernel = accy.kernel;
  16 const scan_library = kernel.library.scan;
  17 const Value = kernel.Value;
  18 
  19 pub const kernel_name = "gui_paint_rgba8_packed";
  20 pub const default_threads: u32 = 128;
  21 pub const float_lanes: usize = 18;
  22 pub const word_lanes: usize = 8;
  23 pub const image_lanes: usize = 3;
  24 pub const tile_range_kernel_name = "gui_paint_tile_ranges";
  25 pub const tile_count_kernel_name = "gui_paint_tile_counts";
  26 pub const tile_index_kernel_name = "gui_paint_tile_indices";
  27 pub const tile_sort_kernel_name = "gui_paint_tile_sort";
  28 pub const tile_range_lanes: usize = 4;
  29 pub const tile_size_log2: u5 = 4;
  30 pub const tile_size: u32 = 1 << tile_size_log2;
  31 
  32 const arg_pixels: usize = 0;
  33 const arg_floats: usize = 1;
  34 const arg_words: usize = 2;
  35 const arg_images: usize = 3;
  36 const arg_image_pixels: usize = 4;
  37 const arg_tile_offsets: usize = 5;
  38 const arg_tile_indices: usize = 6;
  39 const arg_image_count: usize = 7;
  40 const arg_clear_r: usize = 8;
  41 const arg_clear_g: usize = 9;
  42 const arg_clear_b: usize = 10;
  43 const arg_clear_a: usize = 11;
  44 const arg_view_x: usize = 12;
  45 const arg_view_y: usize = 13;
  46 const arg_view_width: usize = 14;
  47 const arg_pixel_count: usize = 15;
  48 const arg_tiles_x: usize = 16;
  49 const arg_output_format: usize = 17;
  50 
  51 const range_arg_ranges: usize = 0;
  52 const range_arg_floats: usize = 1;
  53 const range_arg_words: usize = 2;
  54 const range_arg_command_count: usize = 3;
  55 const range_arg_width: usize = 4;
  56 const range_arg_height: usize = 5;
  57 const range_arg_region_x: usize = 6;
  58 const range_arg_region_y: usize = 7;
  59 const range_arg_region_width: usize = 8;
  60 const range_arg_region_height: usize = 9;
  61 const range_arg_tiles_x: usize = 10;
  62 const range_arg_tiles_y: usize = 11;
  63 
  64 const count_arg_offsets: usize = 0;
  65 const count_arg_ranges: usize = 1;
  66 const count_arg_command_count: usize = 2;
  67 const count_arg_tiles_x: usize = 3;
  68 const count_arg_tiles_y: usize = 4;
  69 
  70 const index_arg_indices: usize = 0;
  71 const index_arg_cursors: usize = 1;
  72 const index_arg_offsets: usize = 2;
  73 const index_arg_ranges: usize = 3;
  74 const index_arg_words: usize = 4;
  75 const index_arg_command_count: usize = 5;
  76 const index_arg_tiles_x: usize = 6;
  77 const index_arg_tiles_y: usize = 7;
  78 
  79 const sort_arg_indices: usize = 0;
  80 const sort_arg_offsets: usize = 1;
  81 const sort_arg_words: usize = 2;
  82 const sort_arg_tile_count: usize = 3;
  83 
  84 const float_rect_x: usize = 0;
  85 const float_rect_y: usize = 1;
  86 const float_rect_w: usize = 2;
  87 const float_rect_h: usize = 3;
  88 const float_clip_x: usize = 4;
  89 const float_clip_y: usize = 5;
  90 const float_clip_w: usize = 6;
  91 const float_clip_h: usize = 7;
  92 const float_radius: usize = 8;
  93 const float_width: usize = 9;
  94 const float_source_x: usize = 10;
  95 const float_source_y: usize = 11;
  96 const float_source_w: usize = 12;
  97 const float_source_h: usize = 13;
  98 const float_gradient_start_x: usize = 14;
  99 const float_gradient_start_y: usize = 15;
 100 const float_gradient_end_x: usize = 16;
 101 const float_gradient_end_y: usize = 17;
 102 
 103 const word_kind: usize = 0;
 104 const word_r: usize = 1;
 105 const word_g: usize = 2;
 106 const word_b: usize = 3;
 107 const word_a: usize = 4;
 108 const word_image: usize = 5;
 109 const word_order: usize = 6;
 110 const word_color_end: usize = 7;
 111 
 112 const image_width: usize = 0;
 113 const image_height: usize = 1;
 114 const image_offset: usize = 2;
 115 
 116 const tile_range_x0: usize = 0;
 117 const tile_range_y0: usize = 1;
 118 const tile_range_x1: usize = 2;
 119 const tile_range_y1: usize = 3;
 120 
 121 pub const PackedCommands = struct {
 122     floats: []f32,
 123     words: []u32,
 124 
 125     pub fn deinit(self: *PackedCommands, allocator: Allocator) void {
 126         allocator.free(self.floats);
 127         allocator.free(self.words);
 128         self.* = undefined;
 129     }
 130 };
 131 
 132 pub const PackedImages = struct {
 133     metadata: []u32,
 134     pixels: []u32,
 135 
 136     pub fn deinit(self: *PackedImages, allocator: Allocator) void {
 137         allocator.free(self.metadata);
 138         allocator.free(self.pixels);
 139         self.* = undefined;
 140     }
 141 };
 142 
 143 pub const Bins = struct {
 144     offsets: []u32,
 145     indices: []u32,
 146     tiles_x: u32,
 147     tiles_y: u32,
 148 
 149     pub fn tileCount(self: Bins) usize {
 150         return @as(usize, self.tiles_x) * @as(usize, self.tiles_y);
 151     }
 152 
 153     pub fn pairCount(self: Bins) usize {
 154         return self.indices.len;
 155     }
 156 
 157     pub fn view(self: Bins) BinView {
 158         return .{
 159             .offsets = self.offsets,
 160             .indices = self.indices,
 161             .tiles_x = self.tiles_x,
 162             .tiles_y = self.tiles_y,
 163         };
 164     }
 165 
 166     pub fn deinit(self: *Bins, allocator: Allocator) void {
 167         allocator.free(self.offsets);
 168         allocator.free(self.indices);
 169         self.* = undefined;
 170     }
 171 };
 172 
 173 pub const BinView = struct {
 174     offsets: []u32,
 175     indices: []u32,
 176     tiles_x: u32,
 177     tiles_y: u32,
 178 
 179     pub fn tileCount(self: BinView) usize {
 180         return @as(usize, self.tiles_x) * @as(usize, self.tiles_y);
 181     }
 182 
 183     pub fn pairCount(self: BinView) usize {
 184         return self.indices.len;
 185     }
 186 };
 187 
 188 pub const OutputFormat = enum(u32) {
 189     rgba = 0,
 190     bgra = 1,
 191 };
 192 
 193 pub fn commandVisits(bins: BinView, region: Region) usize {
 194     var total: usize = 0;
 195     var tile_y: u32 = 0;
 196     while (tile_y < bins.tiles_y) : (tile_y += 1) {
 197         const local_y = tile_y * tile_size;
 198         const tile_height = @min(tile_size, region.height - local_y);
 199         var tile_x: u32 = 0;
 200         while (tile_x < bins.tiles_x) : (tile_x += 1) {
 201             const local_x = tile_x * tile_size;
 202             const tile_width = @min(tile_size, region.width - local_x);
 203             const tile = @as(usize, tile_y) * bins.tiles_x + tile_x;
 204             const command_count = bins.offsets[tile + 1] - bins.offsets[tile];
 205             total += @as(usize, tile_width) * @as(usize, tile_height) * @as(usize, command_count);
 206         }
 207     }
 208     return total;
 209 }
 210 
 211 pub const BinScratch = struct {
 212     ranges: []u32 = &.{},
 213     offsets: []u32 = &.{},
 214     indices: []u32 = &.{},
 215     cursors: []u32 = &.{},
 216 
 217     pub fn binCommands(
 218         self: *BinScratch,
 219         commands: []const Command,
 220         width: u32,
 221         height: u32,
 222         region: Region,
 223     ) !BinView {
 224         const shape = binShape(width, height, region);
 225         const range_count = tileRangeValueCount(commands.len);
 226         if (self.ranges.len < range_count) return error.BufferTooSmall;
 227         if (self.offsets.len < shape.tile_count + 1) return error.BufferTooSmall;
 228         const ranges = self.ranges[0..range_count];
 229         fillCommandTileRanges(commands, width, height, shape, ranges);
 230         const total = countBinPairs(ranges, shape, self.offsets[0 .. shape.tile_count + 1]);
 231         if (self.indices.len < total) return error.BufferTooSmall;
 232         if (shape.active_region.pixelCount() != 0) {
 233             if (self.cursors.len < shape.tile_count) return error.BufferTooSmall;
 234             fillBinIndices(commands, ranges, shape, self.offsets[0 .. shape.tile_count + 1], self.cursors[0..shape.tile_count], self.indices[0..total]);
 235         }
 236         return .{
 237             .offsets = self.offsets[0 .. shape.tile_count + 1],
 238             .indices = self.indices[0..total],
 239             .tiles_x = shape.tiles_x,
 240             .tiles_y = shape.tiles_y,
 241         };
 242     }
 243 };
 244 
 245 pub fn tilesForExtent(extent: u32) u32 {
 246     if (extent == 0) return 1;
 247     return (extent + tile_size - 1) >> tile_size_log2;
 248 }
 249 
 250 pub fn tileRangeValueCount(command_count: usize) usize {
 251     return command_count * tile_range_lanes;
 252 }
 253 
 254 pub fn binPairCount(
 255     commands: []const Command,
 256     width: u32,
 257     height: u32,
 258     region: Region,
 259 ) error{BufferTooLarge}!usize {
 260     const shape = binShape(width, height, region);
 261     if (shape.active_region.pixelCount() == 0) return 0;
 262     var total: usize = 0;
 263     for (commands) |paint| {
 264         const range = commandTileRange(
 265             paint,
 266             width,
 267             height,
 268             shape.active_region,
 269             shape.tiles_x,
 270             shape.tiles_y,
 271         ) orelse continue;
 272         const columns = @as(usize, range.x1 - range.x0);
 273         const rows = @as(usize, range.y1 - range.y0);
 274         const pairs = std.math.mul(usize, columns, rows) catch return error.BufferTooLarge;
 275         total = std.math.add(usize, total, pairs) catch return error.BufferTooLarge;
 276     }
 277     return total;
 278 }
 279 
 280 pub const BinShape = struct {
 281     active_region: Region,
 282     tiles_x: u32,
 283     tiles_y: u32,
 284     tile_count: usize,
 285 };
 286 
 287 const TileRange = struct {
 288     x0: u32,
 289     y0: u32,
 290     x1: u32,
 291     y1: u32,
 292 
 293     fn empty(self: TileRange) bool {
 294         return self.x0 >= self.x1 or self.y0 >= self.y1;
 295     }
 296 
 297     fn count(self: TileRange) usize {
 298         if (self.empty()) return 0;
 299         return @as(usize, self.x1 - self.x0) * @as(usize, self.y1 - self.y0);
 300     }
 301 };
 302 
 303 pub fn binShape(width: u32, height: u32, region: Region) BinShape {
 304     const active_region = region.clamped(width, height);
 305     const tiles_x = tilesForExtent(active_region.width);
 306     const tiles_y = tilesForExtent(active_region.height);
 307     return .{
 308         .active_region = active_region,
 309         .tiles_x = tiles_x,
 310         .tiles_y = tiles_y,
 311         .tile_count = @as(usize, tiles_x) * @as(usize, tiles_y),
 312     };
 313 }
 314 
 315 fn fillCommandTileRanges(commands: []const Command, width: u32, height: u32, shape: BinShape, ranges: []u32) void {
 316     std.debug.assert(ranges.len >= tileRangeValueCount(commands.len));
 317     const active_ranges = ranges[0..tileRangeValueCount(commands.len)];
 318     if (shape.active_region.pixelCount() == 0) {
 319         @memset(active_ranges, 0);
 320         return;
 321     }
 322     for (commands, 0..) |paint, command_index| {
 323         storeTileRange(
 324             active_ranges,
 325             command_index,
 326             commandTileRange(paint, width, height, shape.active_region, shape.tiles_x, shape.tiles_y),
 327         );
 328     }
 329 }
 330 
 331 fn commandTileRange(paint: Command, width: u32, height: u32, region: Region, tiles_x: u32, tiles_y: u32) ?TileRange {
 332     const bounds = cpu.clippedBounds(width, height, paint, region) orelse return null;
 333     const local_x0 = bounds.x0 - region.x;
 334     const local_y0 = bounds.y0 - region.y;
 335     const local_x1 = bounds.x1 - region.x;
 336     const local_y1 = bounds.y1 - region.y;
 337     return .{
 338         .x0 = @min(local_x0 >> tile_size_log2, tiles_x - 1),
 339         .y0 = @min(local_y0 >> tile_size_log2, tiles_y - 1),
 340         .x1 = @min(((local_x1 - 1) >> tile_size_log2) + 1, tiles_x),
 341         .y1 = @min(((local_y1 - 1) >> tile_size_log2) + 1, tiles_y),
 342     };
 343 }
 344 
 345 pub fn binCommandsAlloc(
 346     allocator: Allocator,
 347     commands: []const Command,
 348     width: u32,
 349     height: u32,
 350     region: Region,
 351 ) !Bins {
 352     const shape = binShape(width, height, region);
 353     const ranges = try allocator.alloc(u32, tileRangeValueCount(commands.len));
 354     defer allocator.free(ranges);
 355     fillCommandTileRanges(commands, width, height, shape, ranges);
 356 
 357     const offsets = try allocator.alloc(u32, shape.tile_count + 1);
 358     errdefer allocator.free(offsets);
 359     const total = countBinPairs(ranges, shape, offsets);
 360     if (shape.active_region.pixelCount() == 0) {
 361         const indices = try allocator.alloc(u32, 0);
 362         return .{ .offsets = offsets, .indices = indices, .tiles_x = shape.tiles_x, .tiles_y = shape.tiles_y };
 363     }
 364 
 365     const indices = try allocator.alloc(u32, total);
 366     errdefer allocator.free(indices);
 367     const cursors = try allocator.alloc(u32, shape.tile_count);
 368     defer allocator.free(cursors);
 369     fillBinIndices(commands, ranges, shape, offsets, cursors, indices);
 370 
 371     return .{ .offsets = offsets, .indices = indices, .tiles_x = shape.tiles_x, .tiles_y = shape.tiles_y };
 372 }
 373 
 374 fn countBinPairs(ranges: []const u32, shape: BinShape, offsets: []u32) usize {
 375     countTilePairs(ranges, shape, offsets);
 376     return prefixBinOffsets(offsets[0 .. shape.tile_count + 1]);
 377 }
 378 
 379 fn countTilePairs(ranges: []const u32, shape: BinShape, offsets: []u32) void {
 380     @memset(offsets, 0);
 381     std.debug.assert(offsets.len >= shape.tile_count + 1);
 382     if (shape.active_region.pixelCount() == 0) return;
 383     std.debug.assert(ranges.len % tile_range_lanes == 0);
 384     const command_count = ranges.len / tile_range_lanes;
 385     var command_index: usize = 0;
 386     while (command_index < command_count) : (command_index += 1) {
 387         const range = loadTileRange(ranges, command_index);
 388         var ty = range.y0;
 389         while (ty < range.y1) : (ty += 1) {
 390             var tx = range.x0;
 391             while (tx < range.x1) : (tx += 1) {
 392                 offsets[@as(usize, ty) * shape.tiles_x + tx + 1] += 1;
 393             }
 394         }
 395     }
 396 }
 397 
 398 fn prefixBinOffsets(offsets: []u32) usize {
 399     var total: u32 = 0;
 400     for (offsets[1..]) |*entry| {
 401         total += entry.*;
 402         entry.* = total;
 403     }
 404     return total;
 405 }
 406 
 407 fn fillBinIndices(commands: []const Command, ranges: []const u32, shape: BinShape, offsets: []const u32, cursors: []u32, indices: []u32) void {
 408     fillBinIndicesUnsorted(commands, ranges, shape, offsets, cursors, indices);
 409     sortBins(commands, offsets, indices);
 410 }
 411 
 412 fn fillBinIndicesUnsorted(commands: []const Command, ranges: []const u32, shape: BinShape, offsets: []const u32, cursors: []u32, indices: []u32) void {
 413     @memcpy(cursors, offsets[0..shape.tile_count]);
 414     std.debug.assert(ranges.len >= tileRangeValueCount(commands.len));
 415     for (0..commands.len) |command_index| {
 416         const range = loadTileRange(ranges, command_index);
 417         var ty = range.y0;
 418         while (ty < range.y1) : (ty += 1) {
 419             var tx = range.x0;
 420             while (tx < range.x1) : (tx += 1) {
 421                 const tile = @as(usize, ty) * shape.tiles_x + tx;
 422                 indices[cursors[tile]] = @intCast(command_index);
 423                 cursors[tile] += 1;
 424             }
 425         }
 426     }
 427 }
 428 
 429 fn storeTileRange(ranges: []u32, command_index: usize, maybe_range: ?TileRange) void {
 430     const base = tileRangeBase(command_index);
 431     const range = maybe_range orelse TileRange{ .x0 = 0, .y0 = 0, .x1 = 0, .y1 = 0 };
 432     ranges[base + tile_range_x0] = range.x0;
 433     ranges[base + tile_range_y0] = range.y0;
 434     ranges[base + tile_range_x1] = range.x1;
 435     ranges[base + tile_range_y1] = range.y1;
 436 }
 437 
 438 fn loadTileRange(ranges: []const u32, command_index: usize) TileRange {
 439     const base = tileRangeBase(command_index);
 440     return .{
 441         .x0 = ranges[base + tile_range_x0],
 442         .y0 = ranges[base + tile_range_y0],
 443         .x1 = ranges[base + tile_range_x1],
 444         .y1 = ranges[base + tile_range_y1],
 445     };
 446 }
 447 
 448 fn tileRangeBase(command_index: usize) usize {
 449     return command_index * tile_range_lanes;
 450 }
 451 
 452 fn sortBins(commands: []const Command, offsets: []const u32, indices: []u32) void {
 453     if (indices.len <= 1) return;
 454     var tile: usize = 0;
 455     while (tile + 1 < offsets.len) : (tile += 1) {
 456         const start: usize = @intCast(offsets[tile]);
 457         const end: usize = @intCast(offsets[tile + 1]);
 458         sortTile(commands, indices[start..end]);
 459     }
 460 }
 461 
 462 fn sortTile(commands: []const Command, indices: []u32) void {
 463     var index: usize = 1;
 464     while (index < indices.len) : (index += 1) {
 465         const value = indices[index];
 466         var cursor = index;
 467         while (cursor > 0 and before(commands, value, indices[cursor - 1])) : (cursor -= 1) {
 468             indices[cursor] = indices[cursor - 1];
 469         }
 470         indices[cursor] = value;
 471     }
 472 }
 473 
 474 fn before(commands: []const Command, left_index: u32, right_index: u32) bool {
 475     const left = commands[@intCast(left_index)];
 476     const right = commands[@intCast(right_index)];
 477     if (left.order != right.order) return left.order < right.order;
 478     return left_index < right_index;
 479 }
 480 
 481 pub fn packCommandsAlloc(allocator: Allocator, commands: []const Command) !PackedCommands {
 482     const floats = try allocator.alloc(f32, commands.len * float_lanes);
 483     errdefer allocator.free(floats);
 484     const words = try allocator.alloc(u32, commands.len * word_lanes);
 485     errdefer allocator.free(words);
 486     packCommands(commands, floats, words);
 487     return .{ .floats = floats, .words = words };
 488 }
 489 
 490 pub fn packCommands(commands: []const Command, floats: []f32, words: []u32) void {
 491     std.debug.assert(floats.len >= commands.len * float_lanes);
 492     std.debug.assert(words.len >= commands.len * word_lanes);
 493     for (commands, 0..) |paint, index| {
 494         const float_base = index * float_lanes;
 495         const word_base = index * word_lanes;
 496         packCommand(paint, floats[float_base..][0..float_lanes], words[word_base..][0..word_lanes]);
 497     }
 498 }
 499 
 500 pub fn commandsEqualPacked(commands: []const Command, floats: []const f32, words: []const u32) bool {
 501     std.debug.assert(floats.len >= commands.len * float_lanes);
 502     std.debug.assert(words.len >= commands.len * word_lanes);
 503     for (commands, 0..) |paint, index| {
 504         var command_floats: [float_lanes]f32 = undefined;
 505         var command_words: [word_lanes]u32 = undefined;
 506         packCommand(paint, command_floats[0..], command_words[0..]);
 507         const float_base = index * float_lanes;
 508         const word_base = index * word_lanes;
 509         if (!std.mem.eql(u8, std.mem.asBytes(&command_floats), std.mem.sliceAsBytes(floats[float_base..][0..float_lanes]))) return false;
 510         if (!std.mem.eql(u32, command_words[0..], words[word_base..][0..word_lanes])) return false;
 511     }
 512     return true;
 513 }
 514 
 515 fn packCommand(paint: Command, floats: []f32, words: []u32) void {
 516     std.debug.assert(floats.len >= float_lanes);
 517     std.debug.assert(words.len >= word_lanes);
 518     const finite_gradient = std.math.isFinite(paint.gradient_start.x) and
 519         std.math.isFinite(paint.gradient_start.y) and
 520         std.math.isFinite(paint.gradient_end.x) and
 521         std.math.isFinite(paint.gradient_end.y);
 522     floats[float_rect_x] = paint.rect.x;
 523     floats[float_rect_y] = paint.rect.y;
 524     floats[float_rect_w] = paint.rect.width;
 525     floats[float_rect_h] = paint.rect.height;
 526     floats[float_clip_x] = paint.clip.x;
 527     floats[float_clip_y] = paint.clip.y;
 528     floats[float_clip_w] = paint.clip.width;
 529     floats[float_clip_h] = paint.clip.height;
 530     floats[float_radius] = @max(paint.radius, 0);
 531     floats[float_width] = @max(paint.width, 0);
 532     floats[float_source_x] = paint.source.x;
 533     floats[float_source_y] = paint.source.y;
 534     floats[float_source_w] = paint.source.width;
 535     floats[float_source_h] = paint.source.height;
 536     floats[float_gradient_start_x] = if (finite_gradient) paint.gradient_start.x else 0;
 537     floats[float_gradient_start_y] = if (finite_gradient) paint.gradient_start.y else 0;
 538     floats[float_gradient_end_x] = if (finite_gradient) paint.gradient_end.x else 0;
 539     floats[float_gradient_end_y] = if (finite_gradient) paint.gradient_end.y else 0;
 540 
 541     words[word_kind] = @backingInt(paint.kind);
 542     words[word_r] = paint.color.r;
 543     words[word_g] = paint.color.g;
 544     words[word_b] = paint.color.b;
 545     words[word_a] = paint.color.a;
 546     words[word_image] = paint.image_index;
 547     words[word_order] = paint.order;
 548     words[word_color_end] = packColor(paint.color_end);
 549 }
 550 
 551 fn packColor(color: Color) u32 {
 552     return @as(u32, color.r) |
 553         (@as(u32, color.g) << 8) |
 554         (@as(u32, color.b) << 16) |
 555         (@as(u32, color.a) << 24);
 556 }
 557 
 558 pub fn packImagesAlloc(allocator: Allocator, images: ImageSet) !PackedImages {
 559     const metadata_count = try imageMetadataCount(images.images.len);
 560     const pixel_count = @max(try imagePixelCount(images), 1);
 561     const metadata = try allocator.alloc(u32, metadata_count);
 562     errdefer allocator.free(metadata);
 563     const pixels = try allocator.alloc(u32, pixel_count);
 564     errdefer allocator.free(pixels);
 565     @memset(metadata, 0);
 566     @memset(pixels, 0);
 567     try packImages(images, metadata, pixels);
 568     return .{ .metadata = metadata, .pixels = pixels };
 569 }
 570 
 571 pub fn imagePixelCount(images: ImageSet) !usize {
 572     var total: usize = 0;
 573     for (images.images) |image| {
 574         try image.validate();
 575         total = std.math.add(usize, total, image.pixelCount()) catch return error.BufferTooSmall;
 576     }
 577     return total;
 578 }
 579 
 580 pub fn packImages(images: ImageSet, metadata: []u32, pixels: []u32) !void {
 581     if (metadata.len < try imageMetadataCount(images.images.len)) return error.BufferTooSmall;
 582     if (pixels.len < @max(try imagePixelCount(images), 1)) return error.BufferTooSmall;
 583     var pixel_offset: usize = 0;
 584     for (images.images, 0..) |image, index| {
 585         const meta_base = index * image_lanes;
 586         metadata[meta_base + image_width] = image.width;
 587         metadata[meta_base + image_height] = image.height;
 588         metadata[meta_base + image_offset] = std.math.cast(u32, pixel_offset) orelse return error.BufferTooSmall;
 589         const count = image.pixelCount();
 590         @memcpy(pixels[pixel_offset .. pixel_offset + count], image.pixels[0..count]);
 591         pixel_offset += count;
 592     }
 593 }
 594 
 595 pub fn imagesEqualPacked(images: ImageSet, metadata: []const u32, pixels: []const u32) !bool {
 596     if (!(try imageMetadataEqualPacked(images, metadata))) return false;
 597     if (pixels.len < @max(try imagePixelCount(images), 1)) return error.BufferTooSmall;
 598     var pixel_offset: usize = 0;
 599     for (images.images) |image| {
 600         const count = image.pixelCount();
 601         if (!std.mem.eql(u32, pixels[pixel_offset .. pixel_offset + count], image.pixels[0..count])) return false;
 602         pixel_offset += count;
 603     }
 604     return true;
 605 }
 606 
 607 pub fn imageMetadataEqualPacked(images: ImageSet, metadata: []const u32) !bool {
 608     if (metadata.len < try imageMetadataCount(images.images.len)) return error.BufferTooSmall;
 609     var pixel_offset: usize = 0;
 610     for (images.images, 0..) |image, index| {
 611         try image.validate();
 612         const meta_base = index * image_lanes;
 613         if (metadata[meta_base + image_width] != image.width) return false;
 614         if (metadata[meta_base + image_height] != image.height) return false;
 615         if (metadata[meta_base + image_offset] != pixel_offset) return false;
 616         pixel_offset = std.math.add(usize, pixel_offset, image.pixelCount()) catch return error.BufferTooSmall;
 617     }
 618     return true;
 619 }
 620 
 621 fn imageMetadataCount(image_count: usize) !usize {
 622     return std.math.mul(usize, @max(image_count, 1), image_lanes) catch error.BufferTooSmall;
 623 }
 624 
 625 pub fn renderCommandsCpu(
 626     allocator: Allocator,
 627     commands: []const Command,
 628     width: u32,
 629     height: u32,
 630     pixels: []u32,
 631     clear: Color,
 632 ) !void {
 633     try renderCommandsCpuRegionWithImages(allocator, commands, width, height, pixels, clear, .{}, Region.full(width, height));
 634 }
 635 
 636 pub fn renderCommandsCpuWithImages(
 637     allocator: Allocator,
 638     commands: []const Command,
 639     width: u32,
 640     height: u32,
 641     pixels: []u32,
 642     clear: Color,
 643     images: ImageSet,
 644 ) !void {
 645     try renderCommandsCpuRegionWithImages(allocator, commands, width, height, pixels, clear, images, Region.full(width, height));
 646 }
 647 
 648 pub fn renderCommandsCpuRegionWithImages(
 649     allocator: Allocator,
 650     commands: []const Command,
 651     width: u32,
 652     height: u32,
 653     pixels: []u32,
 654     clear: Color,
 655     images: ImageSet,
 656     region: Region,
 657 ) !void {
 658     const target_pixel_count = @as(usize, width) * @as(usize, height);
 659     if (pixels.len < target_pixel_count) return error.BufferTooSmall;
 660     const active_region = region.clamped(width, height);
 661     if (active_region.pixelCount() == 0) return;
 662     var encoded = try packCommandsAlloc(allocator, commands);
 663     defer encoded.deinit(allocator);
 664     var encoded_images = try packImagesAlloc(allocator, images);
 665     defer encoded_images.deinit(allocator);
 666     var bins = try binCommandsAlloc(allocator, commands, width, height, active_region);
 667     defer bins.deinit(allocator);
 668 
 669     var graph = try buildGraph(allocator, default_threads);
 670     defer graph.deinit();
 671     try runPackedCommandsCpuRegion(allocator, &graph, encoded, encoded_images, bins, images.images.len, commands.len, width, height, pixels, clear, active_region);
 672 }
 673 
 674 pub fn runPackedCommandsCpu(
 675     allocator: Allocator,
 676     graph: *kernel.Graph,
 677     encoded: PackedCommands,
 678     images: PackedImages,
 679     bins: Bins,
 680     image_count: usize,
 681     command_count: usize,
 682     width: u32,
 683     height: u32,
 684     pixels: []u32,
 685     clear: Color,
 686 ) !void {
 687     try runPackedCommandsCpuRegion(allocator, graph, encoded, images, bins, image_count, command_count, width, height, pixels, clear, Region.full(width, height));
 688 }
 689 
 690 pub fn runPackedCommandsCpuRegion(
 691     allocator: Allocator,
 692     graph: *kernel.Graph,
 693     encoded: PackedCommands,
 694     images: PackedImages,
 695     bins: Bins,
 696     image_count: usize,
 697     command_count: usize,
 698     width: u32,
 699     height: u32,
 700     pixels: []u32,
 701     clear: Color,
 702     region: Region,
 703 ) !void {
 704     const target_pixel_count = @as(usize, width) * @as(usize, height);
 705     if (pixels.len < target_pixel_count) return error.BufferTooSmall;
 706     const active_region = region.clamped(width, height);
 707     const pixel_count = active_region.pixelCount();
 708     if (pixel_count == 0) return;
 709     const full_region = active_region.x == 0 and active_region.y == 0 and active_region.width == width and active_region.height == height;
 710     if (encoded.floats.len < command_count * float_lanes) return error.BufferTooSmall;
 711     if (encoded.words.len < command_count * word_lanes) return error.BufferTooSmall;
 712     if (images.metadata.len < try imageMetadataCount(image_count)) return error.BufferTooSmall;
 713     if (bins.tiles_x != tilesForExtent(active_region.width)) return error.BinGeometryMismatch;
 714     if (bins.tiles_y != tilesForExtent(active_region.height)) return error.BinGeometryMismatch;
 715     if (bins.offsets.len < bins.tileCount() + 1) return error.BufferTooSmall;
 716     const output = if (full_region)
 717         pixels[0..target_pixel_count]
 718     else
 719         try allocator.alloc(u32, pixel_count);
 720     defer if (!full_region) allocator.free(output);
 721 
 722     const indices: []const u32 = if (bins.indices.len == 0) &.{0} else bins.indices;
 723     try graph.runCpuWithLaunch(allocator, &.{
 724         kernel.argumentBuffer(u32, output),
 725         kernel.argumentBuffer(f32, encoded.floats[0 .. command_count * float_lanes]),
 726         kernel.argumentBuffer(u32, encoded.words[0 .. command_count * word_lanes]),
 727         kernel.argumentBuffer(u32, images.metadata),
 728         kernel.argumentBuffer(u32, images.pixels),
 729         kernel.argumentBuffer(u32, bins.offsets),
 730         kernel.argumentBuffer(u32, @constCast(indices)),
 731         kernel.argumentU32(@intCast(image_count)),
 732         kernel.argumentU32(clear.r),
 733         kernel.argumentU32(clear.g),
 734         kernel.argumentU32(clear.b),
 735         kernel.argumentU32(clear.a),
 736         kernel.argumentU32(active_region.x),
 737         kernel.argumentU32(active_region.y),
 738         kernel.argumentU32(active_region.width),
 739         kernel.argumentU32(@intCast(pixel_count)),
 740         kernel.argumentU32(bins.tiles_x),
 741         kernel.argumentU32(@backingInt(OutputFormat.rgba)),
 742     }, .{
 743         .grid = .{ gridFor(pixel_count, default_threads), 1, 1 },
 744         .block = .{ default_threads, 1, 1 },
 745     });
 746     if (!full_region) copyPackedRegion(pixels, width, active_region, output);
 747 }
 748 
 749 pub fn runPackedTileRangesCpu(
 750     allocator: Allocator,
 751     graph: *kernel.Graph,
 752     encoded: PackedCommands,
 753     command_count: usize,
 754     width: u32,
 755     height: u32,
 756     ranges: []u32,
 757 ) !void {
 758     try runPackedTileRangesCpuRegion(allocator, graph, encoded, command_count, width, height, ranges, Region.full(width, height));
 759 }
 760 
 761 pub fn runPackedTileRangesCpuRegion(
 762     allocator: Allocator,
 763     graph: *kernel.Graph,
 764     encoded: PackedCommands,
 765     command_count: usize,
 766     width: u32,
 767     height: u32,
 768     ranges: []u32,
 769     region: Region,
 770 ) !void {
 771     if (encoded.floats.len < command_count * float_lanes) return error.BufferTooSmall;
 772     if (encoded.words.len < command_count * word_lanes) return error.BufferTooSmall;
 773     if (ranges.len < tileRangeValueCount(command_count)) return error.BufferTooSmall;
 774     const active_region = region.clamped(width, height);
 775     const output = ranges[0..tileRangeValueCount(command_count)];
 776     if (command_count == 0 or active_region.pixelCount() == 0) {
 777         @memset(output, 0);
 778         return;
 779     }
 780     const shape = binShape(width, height, active_region);
 781     try graph.runCpuWithLaunch(allocator, &.{
 782         kernel.argumentBuffer(u32, output),
 783         kernel.argumentBuffer(f32, encoded.floats[0 .. command_count * float_lanes]),
 784         kernel.argumentBuffer(u32, encoded.words[0 .. command_count * word_lanes]),
 785         kernel.argumentU32(@intCast(command_count)),
 786         kernel.argumentU32(width),
 787         kernel.argumentU32(height),
 788         kernel.argumentU32(shape.active_region.x),
 789         kernel.argumentU32(shape.active_region.y),
 790         kernel.argumentU32(shape.active_region.width),
 791         kernel.argumentU32(shape.active_region.height),
 792         kernel.argumentU32(shape.tiles_x),
 793         kernel.argumentU32(shape.tiles_y),
 794     }, .{
 795         .grid = .{ gridFor(command_count, default_threads), 1, 1 },
 796         .block = .{ default_threads, 1, 1 },
 797     });
 798 }
 799 
 800 pub fn runPackedTileCountsCpu(
 801     allocator: Allocator,
 802     graph: *kernel.Graph,
 803     ranges: []u32,
 804     command_count: usize,
 805     tiles_x: u32,
 806     tiles_y: u32,
 807     offsets: []u32,
 808 ) !usize {
 809     if (ranges.len < tileRangeValueCount(command_count)) return error.BufferTooSmall;
 810     const tile_count = @as(usize, tiles_x) * @as(usize, tiles_y);
 811     if (offsets.len < tile_count + 1) return error.BufferTooSmall;
 812     const output = offsets[0 .. tile_count + 1];
 813     @memset(output, 0);
 814     if (command_count == 0 or tile_count == 0) return 0;
 815     try graph.runCpuWithLaunch(allocator, &.{
 816         kernel.argumentBuffer(u32, output),
 817         kernel.argumentBuffer(u32, ranges[0..tileRangeValueCount(command_count)]),
 818         kernel.argumentU32(@intCast(command_count)),
 819         kernel.argumentU32(tiles_x),
 820         kernel.argumentU32(tiles_y),
 821     }, .{
 822         .grid = .{ gridFor(tile_count, default_threads), 1, 1 },
 823         .block = .{ default_threads, 1, 1 },
 824     });
 825     var total: usize = 0;
 826     for (output[1..]) |count| total += count;
 827     return total;
 828 }
 829 
 830 pub fn runPackedTileOffsetsCpu(
 831     allocator: Allocator,
 832     graph: *kernel.Graph,
 833     ranges: []u32,
 834     command_count: usize,
 835     tiles_x: u32,
 836     tiles_y: u32,
 837     offsets: []u32,
 838 ) !usize {
 839     const tile_count = @as(usize, tiles_x) * @as(usize, tiles_y);
 840     if (offsets.len < tile_count + 1) return error.BufferTooSmall;
 841     const output = offsets[0 .. tile_count + 1];
 842     _ = try runPackedTileCountsCpu(allocator, graph, ranges, command_count, tiles_x, tiles_y, output);
 843     const counts = try allocator.dupe(u32, output);
 844     defer allocator.free(counts);
 845     try scanTileOffsetCountsCpu(allocator, counts, output);
 846     return output[tile_count];
 847 }
 848 
 849 pub fn runPackedTileIndicesCpu(
 850     allocator: Allocator,
 851     graph: *kernel.Graph,
 852     ranges: []u32,
 853     words: []u32,
 854     command_count: usize,
 855     tiles_x: u32,
 856     tiles_y: u32,
 857     offsets: []u32,
 858     cursors: []u32,
 859     indices: []u32,
 860 ) !void {
 861     if (ranges.len < tileRangeValueCount(command_count)) return error.BufferTooSmall;
 862     if (words.len < command_count * word_lanes) return error.BufferTooSmall;
 863     const tile_count = @as(usize, tiles_x) * @as(usize, tiles_y);
 864     if (offsets.len < tile_count + 1) return error.BufferTooSmall;
 865     if (cursors.len < tile_count) return error.BufferTooSmall;
 866     const total: usize = @intCast(offsets[tile_count]);
 867     if (indices.len < total) return error.BufferTooSmall;
 868     @memset(cursors[0..tile_count], 0);
 869     if (command_count == 0 or tile_count == 0 or total == 0) return;
 870     const pair_count = std.math.mul(usize, command_count, tile_count) catch return error.BufferTooLarge;
 871     try graph.runCpuWithLaunch(allocator, &.{
 872         kernel.argumentBuffer(u32, indices[0..total]),
 873         kernel.argumentBuffer(u32, cursors[0..tile_count]),
 874         kernel.argumentBuffer(u32, offsets[0 .. tile_count + 1]),
 875         kernel.argumentBuffer(u32, ranges[0..tileRangeValueCount(command_count)]),
 876         kernel.argumentBuffer(u32, words[0 .. command_count * word_lanes]),
 877         kernel.argumentU32(@intCast(command_count)),
 878         kernel.argumentU32(tiles_x),
 879         kernel.argumentU32(tiles_y),
 880     }, .{
 881         .grid = .{ gridFor(pair_count, default_threads), 1, 1 },
 882         .block = .{ default_threads, 1, 1 },
 883     });
 884 }
 885 
 886 pub fn runPackedTileSortCpu(
 887     allocator: Allocator,
 888     graph: *kernel.Graph,
 889     words: []u32,
 890     command_count: usize,
 891     offsets: []u32,
 892     indices: []u32,
 893     tile_count: usize,
 894 ) !void {
 895     if (words.len < command_count * word_lanes) return error.BufferTooSmall;
 896     if (offsets.len < tile_count + 1) return error.BufferTooSmall;
 897     const total: usize = @intCast(offsets[tile_count]);
 898     if (indices.len < total) return error.BufferTooSmall;
 899     if (tile_count == 0 or total <= 1) return;
 900     try graph.runCpuWithLaunch(allocator, &.{
 901         kernel.argumentBuffer(u32, indices[0..total]),
 902         kernel.argumentBuffer(u32, offsets[0 .. tile_count + 1]),
 903         kernel.argumentBuffer(u32, words[0 .. command_count * word_lanes]),
 904         kernel.argumentU32(@intCast(tile_count)),
 905     }, .{
 906         .grid = .{ gridFor(tile_count, default_threads), 1, 1 },
 907         .block = .{ default_threads, 1, 1 },
 908     });
 909 }
 910 
 911 fn scanTileOffsetCountsCpu(allocator: Allocator, counts: []u32, offsets: []u32) !void {
 912     if (counts.len != offsets.len) return error.BufferTooSmall;
 913     const extent = counts.len;
 914     if (extent == 0) return error.BufferTooSmall;
 915     if (scan_library.prefixSumThreadsForExtent(extent)) |threads| {
 916         try scanTileOffsetCountsPrefixCpu(allocator, counts, offsets, extent, threads);
 917         return;
 918     }
 919     const threads = scan_library.deviceScanThreadsForExtent(extent) orelse return error.UnsupportedDeviceScanInstance;
 920     try scanTileOffsetCountsDeviceCpu(allocator, counts, offsets, extent, threads);
 921 }
 922 
 923 fn scanTileOffsetCountsPrefixCpu(
 924     allocator: Allocator,
 925     counts: []u32,
 926     offsets: []u32,
 927     extent: usize,
 928     threads: u32,
 929 ) !void {
 930     var graph = try scan_library.PrefixSumRuntimeFamilyU32.build(allocator, kernel.Limits.standard, .{
 931         .extent = 1,
 932         .dtype = .u32,
 933         .mode = .inclusive,
 934         .threads = threads,
 935     });
 936     defer graph.deinit();
 937     try graph.runCpuWithLaunch(allocator, &.{
 938         kernel.argumentBuffer(u32, offsets),
 939         kernel.argumentBuffer(u32, counts),
 940         kernel.argumentI32(@intCast(extent)),
 941     }, .{
 942         .grid = .{ 1, 1, 1 },
 943         .block = .{ threads, 1, 1 },
 944     });
 945 }
 946 
 947 fn scanTileOffsetCountsDeviceCpu(
 948     allocator: Allocator,
 949     counts: []u32,
 950     offsets: []u32,
 951     extent: usize,
 952     threads: u32,
 953 ) !void {
 954     const compiled = scan_library.DeviceScan{ .extent = 1, .dtype = .u32, .threads = threads, .mode = .inclusive };
 955     const runtime = scan_library.DeviceScan{ .extent = extent, .dtype = .u32, .threads = threads, .mode = .inclusive };
 956     const stages = try scan_library.deviceScanStages(runtime);
 957     const sums = try allocator.alloc(u32, stages.block_count);
 958     defer allocator.free(sums);
 959     @memset(sums, 0);
 960     const bases = try allocator.alloc(u32, stages.block_count);
 961     defer allocator.free(bases);
 962     @memset(bases, 0);
 963 
 964     var block_scan = try scan_library.DeviceScanBlockScanRuntimeFamilyU32.build(allocator, kernel.Limits.standard, compiled);
 965     defer block_scan.deinit();
 966     try block_scan.runCpuWithLaunch(allocator, &.{
 967         kernel.argumentBuffer(u32, offsets),
 968         kernel.argumentBuffer(u32, counts),
 969         kernel.argumentBuffer(u32, sums),
 970         kernel.argumentI32(@intCast(extent)),
 971     }, .{
 972         .grid = .{ stages.block_count, 1, 1 },
 973         .block = .{ runtime.threads, 1, 1 },
 974     });
 975 
 976     var sums_scan = try scan_library.PrefixSumRuntimeFamilyU32.build(allocator, kernel.Limits.standard, .{
 977         .extent = 1,
 978         .dtype = .u32,
 979         .mode = .exclusive,
 980         .threads = stages.sums_scan.threads,
 981     });
 982     defer sums_scan.deinit();
 983     try sums_scan.runCpuWithLaunch(allocator, &.{
 984         kernel.argumentBuffer(u32, bases),
 985         kernel.argumentBuffer(u32, sums),
 986         kernel.argumentI32(@intCast(stages.block_count)),
 987     }, .{
 988         .grid = .{ 1, 1, 1 },
 989         .block = .{ stages.sums_scan.threads, 1, 1 },
 990     });
 991 
 992     var add_base = try scan_library.DeviceScanAddBaseRuntimeFamilyU32.build(allocator, kernel.Limits.standard, compiled);
 993     defer add_base.deinit();
 994     try add_base.runCpuWithLaunch(allocator, &.{
 995         kernel.argumentBuffer(u32, offsets),
 996         kernel.argumentBuffer(u32, bases),
 997         kernel.argumentI32(@intCast(extent)),
 998     }, .{
 999         .grid = .{ stages.block_count, 1, 1 },
1000         .block = .{ runtime.threads, 1, 1 },
1001     });
1002 }
1003 
1004 fn emit_pixel_guard(inner: anytype, ctx: anytype) !void {
1005     try emitPixel(
1006         inner,
1007         ctx.pixels,
1008         ctx.floats,
1009         ctx.words,
1010         ctx.images,
1011         ctx.image_pixels,
1012         ctx.tile_offsets,
1013         ctx.tile_indices,
1014         ctx.gid,
1015     );
1016 }
1017 
1018 fn emit_tile_range_guard(inner: anytype, ctx: anytype) !void {
1019     try emitTileRange(inner, ctx.ranges, ctx.floats, ctx.words, ctx.gid);
1020 }
1021 
1022 fn emit_tile_count_guard(inner: anytype, ctx: anytype) !void {
1023     try emitTileCount(inner, ctx.offsets, ctx.ranges, ctx.tile);
1024 }
1025 
1026 fn emit_tile_index_guard(inner: anytype, ctx: anytype) !void {
1027     try emitTileIndex(
1028         inner,
1029         ctx.indices,
1030         ctx.cursors,
1031         ctx.offsets,
1032         ctx.ranges,
1033         ctx.words,
1034         ctx.pair,
1035     );
1036 }
1037 
1038 fn emit_tile_sort_guard(inner: anytype, ctx: anytype) !void {
1039     try emitTileSort(inner, ctx.indices, ctx.offsets, ctx.words, ctx.tile);
1040 }
1041 
1042 fn count_tile_command(
1043     loop_builder: anytype,
1044     command_index: Value,
1045     current: Value,
1046     ctx: anytype,
1047 ) !Value {
1048     const covered = try tileCoversCommand(
1049         loop_builder,
1050         ctx.ranges,
1051         command_index,
1052         ctx.tile,
1053         ctx.tiles_x,
1054     );
1055     const increment = try loop_builder.select(covered, ctx.one, ctx.zero);
1056     return loop_builder.add(current, increment);
1057 }
1058 
1059 fn count_prior_command(
1060     loop_builder: anytype,
1061     prior_index: Value,
1062     current: Value,
1063     ctx: anytype,
1064 ) !Value {
1065     const prior_covered = try tileCoversCommand(
1066         loop_builder,
1067         ctx.ranges,
1068         prior_index,
1069         ctx.tile,
1070         ctx.tiles_x,
1071     );
1072     const prior_before = try commandBeforeDevice(
1073         loop_builder,
1074         ctx.words,
1075         prior_index,
1076         ctx.command_index,
1077     );
1078     const counts_before = try loop_builder.and_(prior_covered, prior_before);
1079     const increment = try loop_builder.select(counts_before, ctx.one, ctx.zero);
1080     return loop_builder.add(current, increment);
1081 }
1082 
1083 fn write_tile_command(inner: anytype, ctx: anytype) !void {
1084     const slot = try inner.castIndex(try inner.add(ctx.start, ctx.local_count));
1085     const command_u32 = try inner.cast(ctx.command_index, .u32);
1086     try ctx.indices.store(inner, command_u32, slot);
1087 }
1088 
1089 fn write_tile_cursor(inner: anytype, ctx: anytype) !void {
1090     try ctx.cursors.store(inner, ctx.tile_total, ctx.tile);
1091 }
1092 
1093 fn find_tile_insertion(
1094     loop_builder: anytype,
1095     candidate: Value,
1096     position: Value,
1097     ctx: anytype,
1098 ) !Value {
1099     const existing = (try ctx.indices.load(loop_builder, candidate)).raw();
1100     const before_existing = try commandBeforeDevice(
1101         loop_builder,
1102         ctx.words,
1103         ctx.value,
1104         existing,
1105     );
1106     const unset = try loop_builder.compare(.eq, position, ctx.cursor);
1107     const take = try loop_builder.and_(unset, before_existing);
1108     return loop_builder.select(take, candidate, position);
1109 }
1110 
1111 fn shift_tile_index_right(
1112     loop_builder: anytype,
1113     offset: Value,
1114     accumulator: Value,
1115     ctx: anytype,
1116 ) !Value {
1117     const from = try loop_builder.sub(try loop_builder.sub(ctx.cursor, ctx.one), offset);
1118     const to = try loop_builder.sub(ctx.cursor, offset);
1119     const shifted = (try ctx.indices.load(loop_builder, from)).raw();
1120     try ctx.indices.store(loop_builder, shifted, to);
1121     return accumulator;
1122 }
1123 
1124 pub fn buildGraph(allocator: Allocator, threads: u32) !kernel.Graph {
1125     var b = try kernel.Builder.init(allocator, kernel.Limits.standard, kernel_name, &.{
1126         kernel.dynamicBuffer(.u32),
1127         kernel.dynamicBuffer(.f32),
1128         kernel.dynamicBuffer(.u32),
1129         kernel.dynamicBuffer(.u32),
1130         kernel.dynamicBuffer(.u32),
1131         kernel.dynamicBuffer(.u32),
1132         kernel.dynamicBuffer(.u32),
1133         kernel.scalar(.u32),
1134         kernel.scalar(.u32),
1135         kernel.scalar(.u32),
1136         kernel.scalar(.u32),
1137         kernel.scalar(.u32),
1138         kernel.scalar(.u32),
1139         kernel.scalar(.u32),
1140         kernel.scalar(.u32),
1141         kernel.scalar(.u32),
1142         kernel.scalar(.u32),
1143         kernel.scalar(.u32),
1144     });
1145     errdefer b.deinit();
1146 
1147     const axis = try b.axis("pixel", @max(threads, 1));
1148     try b.bind(axis, .thread_x);
1149 
1150     const gid = try b.globalId(.x);
1151     const pixel_count = try b.castIndex(b.argument(arg_pixel_count));
1152     const active = try b.compare(.lt, gid, pixel_count);
1153     try b.guardDo(active, .{
1154         .pixels = b.bufferArgument(.u32, arg_pixels),
1155         .floats = b.bufferArgument(.f32, arg_floats),
1156         .words = b.bufferArgument(.u32, arg_words),
1157         .images = b.bufferArgument(.u32, arg_images),
1158         .image_pixels = b.bufferArgument(.u32, arg_image_pixels),
1159         .tile_offsets = b.bufferArgument(.u32, arg_tile_offsets),
1160         .tile_indices = b.bufferArgument(.u32, arg_tile_indices),
1161         .gid = gid,
1162     }, emit_pixel_guard);
1163     try b.return_();
1164     return b.finish();
1165 }
1166 
1167 pub fn buildTileRangeGraph(allocator: Allocator, threads: u32) !kernel.Graph {
1168     var b = try kernel.Builder.init(allocator, kernel.Limits.standard, tile_range_kernel_name, &.{
1169         kernel.dynamicBuffer(.u32),
1170         kernel.dynamicBuffer(.f32),
1171         kernel.dynamicBuffer(.u32),
1172         kernel.scalar(.u32),
1173         kernel.scalar(.u32),
1174         kernel.scalar(.u32),
1175         kernel.scalar(.u32),
1176         kernel.scalar(.u32),
1177         kernel.scalar(.u32),
1178         kernel.scalar(.u32),
1179         kernel.scalar(.u32),
1180         kernel.scalar(.u32),
1181     });
1182     errdefer b.deinit();
1183 
1184     const axis = try b.axis("command", @max(threads, 1));
1185     try b.bind(axis, .thread_x);
1186 
1187     const gid = try b.globalId(.x);
1188     const command_count = try b.castIndex(b.argument(range_arg_command_count));
1189     const active = try b.compare(.lt, gid, command_count);
1190     try b.guardDo(active, .{
1191         .ranges = b.bufferArgument(.u32, range_arg_ranges),
1192         .floats = b.bufferArgument(.f32, range_arg_floats),
1193         .words = b.bufferArgument(.u32, range_arg_words),
1194         .gid = gid,
1195     }, emit_tile_range_guard);
1196     try b.return_();
1197     return b.finish();
1198 }
1199 
1200 pub fn buildTileCountGraph(allocator: Allocator, threads: u32) !kernel.Graph {
1201     var b = try kernel.Builder.init(allocator, kernel.Limits.standard, tile_count_kernel_name, &.{
1202         kernel.dynamicBuffer(.u32),
1203         kernel.dynamicBuffer(.u32),
1204         kernel.scalar(.u32),
1205         kernel.scalar(.u32),
1206         kernel.scalar(.u32),
1207     });
1208     errdefer b.deinit();
1209 
1210     const axis = try b.axis("tile", @max(threads, 1));
1211     try b.bind(axis, .thread_x);
1212 
1213     const gid = try b.globalId(.x);
1214     const tile_count = try b.mul(try b.castIndex(b.argument(count_arg_tiles_x)), try b.castIndex(b.argument(count_arg_tiles_y)));
1215     const active = try b.compare(.lt, gid, tile_count);
1216     try b.guardDo(active, .{
1217         .offsets = b.bufferArgument(.u32, count_arg_offsets),
1218         .ranges = b.bufferArgument(.u32, count_arg_ranges),
1219         .tile = gid,
1220     }, emit_tile_count_guard);
1221     try b.return_();
1222     return b.finish();
1223 }
1224 
1225 pub fn buildTileIndexGraph(allocator: Allocator, threads: u32) !kernel.Graph {
1226     var b = try kernel.Builder.init(allocator, kernel.Limits.standard, tile_index_kernel_name, &.{
1227         kernel.dynamicBuffer(.u32),
1228         kernel.dynamicBuffer(.u32),
1229         kernel.dynamicBuffer(.u32),
1230         kernel.dynamicBuffer(.u32),
1231         kernel.dynamicBuffer(.u32),
1232         kernel.scalar(.u32),
1233         kernel.scalar(.u32),
1234         kernel.scalar(.u32),
1235     });
1236     errdefer b.deinit();
1237 
1238     const axis = try b.axis("tile", @max(threads, 1));
1239     try b.bind(axis, .thread_x);
1240 
1241     const gid = try b.globalId(.x);
1242     const command_count = try b.castIndex(b.argument(index_arg_command_count));
1243     const tile_count = try b.mul(try b.castIndex(b.argument(index_arg_tiles_x)), try b.castIndex(b.argument(index_arg_tiles_y)));
1244     const pair_count = try b.mul(tile_count, command_count);
1245     const active = try b.compare(.lt, gid, pair_count);
1246     try b.guardDo(active, .{
1247         .indices = b.bufferArgument(.u32, index_arg_indices),
1248         .cursors = b.bufferArgument(.u32, index_arg_cursors),
1249         .offsets = b.bufferArgument(.u32, index_arg_offsets),
1250         .ranges = b.bufferArgument(.u32, index_arg_ranges),
1251         .words = b.bufferArgument(.u32, index_arg_words),
1252         .pair = gid,
1253     }, emit_tile_index_guard);
1254     try b.return_();
1255     return b.finish();
1256 }
1257 
1258 pub fn buildTileSortGraph(allocator: Allocator, threads: u32) !kernel.Graph {
1259     var b = try kernel.Builder.init(allocator, kernel.Limits.standard, tile_sort_kernel_name, &.{
1260         kernel.dynamicBuffer(.u32),
1261         kernel.dynamicBuffer(.u32),
1262         kernel.dynamicBuffer(.u32),
1263         kernel.scalar(.u32),
1264     });
1265     errdefer b.deinit();
1266 
1267     const axis = try b.axis("tile", @max(threads, 1));
1268     try b.bind(axis, .thread_x);
1269 
1270     const gid = try b.globalId(.x);
1271     const tile_count = try b.castIndex(b.argument(sort_arg_tile_count));
1272     const active = try b.compare(.lt, gid, tile_count);
1273     try b.guardDo(active, .{
1274         .indices = b.bufferArgument(.u32, sort_arg_indices),
1275         .offsets = b.bufferArgument(.u32, sort_arg_offsets),
1276         .words = b.bufferArgument(.u32, sort_arg_words),
1277         .tile = gid,
1278     }, emit_tile_sort_guard);
1279     try b.return_();
1280     return b.finish();
1281 }
1282 
1283 pub fn createRecordingArtifact(
1284     allocator: Allocator,
1285     format: gpu.ArtifactFormat,
1286 ) !gpu.KernelArtifact {
1287     var graph = try buildGraph(allocator, default_threads);
1288     defer graph.deinit();
1289     var state = recordingBackendState(allocator, format);
1290     return kernel.createKernelArtifact(allocator, state.handle(), &graph, .{
1291         .artifact_format = format,
1292         .authored_kernel_diagnostic_id = "gui/paint/accy/rgba8-packed",
1293     });
1294 }
1295 
1296 pub fn createTileRangeRecordingArtifact(
1297     allocator: Allocator,
1298     format: gpu.ArtifactFormat,
1299 ) !gpu.KernelArtifact {
1300     var graph = try buildTileRangeGraph(allocator, default_threads);
1301     defer graph.deinit();
1302     var state = recordingBackendState(allocator, format);
1303     return kernel.createKernelArtifact(allocator, state.handle(), &graph, .{
1304         .artifact_format = format,
1305         .authored_kernel_diagnostic_id = "gui/paint/accy/tile-ranges",
1306     });
1307 }
1308 
1309 pub fn createTileCountRecordingArtifact(
1310     allocator: Allocator,
1311     format: gpu.ArtifactFormat,
1312 ) !gpu.KernelArtifact {
1313     var graph = try buildTileCountGraph(allocator, default_threads);
1314     defer graph.deinit();
1315     var state = recordingBackendState(allocator, format);
1316     return kernel.createKernelArtifact(allocator, state.handle(), &graph, .{
1317         .artifact_format = format,
1318         .authored_kernel_diagnostic_id = "gui/paint/accy/tile-counts",
1319     });
1320 }
1321 
1322 pub fn createTileIndexRecordingArtifact(
1323     allocator: Allocator,
1324     format: gpu.ArtifactFormat,
1325 ) !gpu.KernelArtifact {
1326     var graph = try buildTileIndexGraph(allocator, default_threads);
1327     defer graph.deinit();
1328     var state = recordingBackendState(allocator, format);
1329     return kernel.createKernelArtifact(allocator, state.handle(), &graph, .{
1330         .artifact_format = format,
1331         .authored_kernel_diagnostic_id = "gui/paint/accy/tile-indices",
1332     });
1333 }
1334 
1335 pub fn createTileSortRecordingArtifact(
1336     allocator: Allocator,
1337     format: gpu.ArtifactFormat,
1338 ) !gpu.KernelArtifact {
1339     var graph = try buildTileSortGraph(allocator, default_threads);
1340     defer graph.deinit();
1341     var state = recordingBackendState(allocator, format);
1342     return kernel.createKernelArtifact(allocator, state.handle(), &graph, .{
1343         .artifact_format = format,
1344         .authored_kernel_diagnostic_id = "gui/paint/accy/tile-sort",
1345     });
1346 }
1347 
1348 fn recordingBackendState(allocator: Allocator, format: gpu.ArtifactFormat) gpu.recording.BackendState {
1349     return .{
1350         .allocator = allocator,
1351         .kind = switch (format) {
1352             .cuda_ptx => .cuda,
1353             .vulkan_spirv => .vulkan,
1354             .metal_msl => .metal,
1355             else => .external,
1356         },
1357         .format = format,
1358     };
1359 }
1360 
1361 pub fn gridFor(items: usize, threads: u32) u32 {
1362     if (items == 0) return 1;
1363     return @intCast((items + threads - 1) / threads);
1364 }
1365 
1366 fn emitPixel(
1367     k: anytype,
1368     pixels: kernel.BufferView(.u32),
1369     floats: kernel.BufferView(.f32),
1370     words: kernel.BufferView(.u32),
1371     images: kernel.BufferView(.u32),
1372     image_pixels: kernel.BufferView(.u32),
1373     tile_offsets: kernel.BufferView(.u32),
1374     tile_indices: kernel.BufferView(.u32),
1375     gid: Value,
1376 ) !void {
1377     const pixel = try pixelForGid(k, gid);
1378     const one = try k.constantIndex(1);
1379     const tile_shift = try u32v(k, tile_size_log2);
1380     const tile_x = try k.ushr(pixel.local_x, tile_shift);
1381     const tile_y = try k.ushr(pixel.local_y, tile_shift);
1382     const tile = try k.add(tile_x, try k.mul(tile_y, k.argument(arg_tiles_x)));
1383     const tile_index = try k.castIndex(tile);
1384     const list_start = try k.castIndex((try tile_offsets.load(k, tile_index)).raw());
1385     const list_end = try k.castIndex((try tile_offsets.load(k, try k.add(tile_index, one))).raw());
1386     const clear = Channels{
1387         .r = k.argument(arg_clear_r),
1388         .g = k.argument(arg_clear_g),
1389         .b = k.argument(arg_clear_b),
1390         .a = k.argument(arg_clear_a),
1391     };
1392 
1393     var scope = try k.forScope(
1394         list_start,
1395         list_end,
1396         one,
1397         &.{ clear.r, clear.g, clear.b, clear.a },
1398         &.{ clear.r.valueType(), clear.g.valueType(), clear.b.valueType(), clear.a.valueType() },
1399     );
1400     errdefer scope.abort();
1401     const command_index = try k.castIndex((try tile_indices.load(k, scope.inductionVar())).raw());
1402     const next = try emitCommand(k, floats, words, images, image_pixels, command_index, pixel.coord, .{
1403         .r = scope.iterArg(0).?,
1404         .g = scope.iterArg(1).?,
1405         .b = scope.iterArg(2).?,
1406         .a = scope.iterArg(3).?,
1407     });
1408     try scope.leave(&.{ next.r, next.g, next.b, next.a });
1409 
1410     try pixels.store(k, try packPixel(k, .{
1411         .r = scope.result(0).?,
1412         .g = scope.result(1).?,
1413         .b = scope.result(2).?,
1414         .a = scope.result(3).?,
1415     }, k.argument(arg_output_format)), pixel.index);
1416 }
1417 
1418 fn emitTileRange(
1419     k: anytype,
1420     ranges: kernel.BufferView(.u32),
1421     floats: kernel.BufferView(.f32),
1422     words: kernel.BufferView(.u32),
1423     command_index: Value,
1424 ) !void {
1425     const rect = try loadRect(k, floats, command_index, float_rect_x);
1426     const clip = try loadRect(k, floats, command_index, float_clip_x);
1427     const width = try loadFloat(k, floats, command_index, float_width);
1428     const kind = try loadWord(k, words, command_index, word_kind);
1429     const shadow = try k.compare(.eq, kind, try u32v(k, @backingInt(command.Kind.shadow)));
1430     const bounds = try commandBoundsValue(k, rect, shadow, width);
1431     const region = RectValue{
1432         .x = try k.cast(k.argument(range_arg_region_x), .f32),
1433         .y = try k.cast(k.argument(range_arg_region_y), .f32),
1434         .width = try k.cast(k.argument(range_arg_region_width), .f32),
1435         .height = try k.cast(k.argument(range_arg_region_height), .f32),
1436     };
1437     const x0 = try floorClampValue(k, try k.max(try k.max(bounds.x, clip.x), region.x), k.argument(range_arg_width));
1438     const y0 = try floorClampValue(k, try k.max(try k.max(bounds.y, clip.y), region.y), k.argument(range_arg_height));
1439     const x1 = try ceilClampValue(k, try k.min(try k.min(try k.add(bounds.x, bounds.width), try k.add(clip.x, clip.width)), try k.add(region.x, region.width)), k.argument(range_arg_width));
1440     const y1 = try ceilClampValue(k, try k.min(try k.min(try k.add(bounds.y, bounds.height), try k.add(clip.y, clip.height)), try k.add(region.y, region.height)), k.argument(range_arg_height));
1441     const valid = try k.and_(try k.compare(.lt, x0, x1), try k.compare(.lt, y0, y1));
1442     const tile_x0 = try tileStart(k, x0, k.argument(range_arg_region_x), k.argument(range_arg_tiles_x));
1443     const tile_y0 = try tileStart(k, y0, k.argument(range_arg_region_y), k.argument(range_arg_tiles_y));
1444     const tile_x1 = try tileEnd(k, x1, k.argument(range_arg_region_x), k.argument(range_arg_tiles_x));
1445     const tile_y1 = try tileEnd(k, y1, k.argument(range_arg_region_y), k.argument(range_arg_tiles_y));
1446     const zero = try u32v(k, 0);
1447     const base = try laneIndex(k, command_index, tile_range_lanes, 0);
1448     try ranges.store(k, try k.select(valid, tile_x0, zero), base);
1449     try ranges.store(k, try k.select(valid, tile_y0, zero), try k.add(base, try k.constantIndex(tile_range_y0)));
1450     try ranges.store(k, try k.select(valid, tile_x1, zero), try k.add(base, try k.constantIndex(tile_range_x1)));
1451     try ranges.store(k, try k.select(valid, tile_y1, zero), try k.add(base, try k.constantIndex(tile_range_y1)));
1452 }
1453 
1454 fn emitTileCount(
1455     k: anytype,
1456     offsets: kernel.BufferView(.u32),
1457     ranges: kernel.BufferView(.u32),
1458     tile: Value,
1459 ) !void {
1460     const zero_index = try k.constantIndex(0);
1461     const one_index = try k.constantIndex(1);
1462     const zero = try u32v(k, 0);
1463     const one = try u32v(k, 1);
1464     const command_count = try k.castIndex(k.argument(count_arg_command_count));
1465     const tiles_x = try k.castIndex(k.argument(count_arg_tiles_x));
1466     const count = try k.fold(zero_index, command_count, one_index, zero, .{
1467         .ranges = ranges,
1468         .tile = tile,
1469         .tiles_x = tiles_x,
1470         .zero = zero,
1471         .one = one,
1472     }, count_tile_command);
1473     try offsets.store(k, count, try k.add(tile, one_index));
1474 }
1475 
1476 fn emitTileIndex(
1477     k: anytype,
1478     indices: kernel.BufferView(.u32),
1479     cursors: kernel.BufferView(.u32),
1480     offsets: kernel.BufferView(.u32),
1481     ranges: kernel.BufferView(.u32),
1482     words: kernel.BufferView(.u32),
1483     pair: Value,
1484 ) !void {
1485     const zero_index = try k.constantIndex(0);
1486     const one_index = try k.constantIndex(1);
1487     const zero = try u32v(k, 0);
1488     const one = try u32v(k, 1);
1489     const command_count = try k.castIndex(k.argument(index_arg_command_count));
1490     const tiles_x = try k.castIndex(k.argument(index_arg_tiles_x));
1491     const tile = try k.div(pair, command_count);
1492     const command_index = try k.sub(pair, try k.mul(tile, command_count));
1493     const start = (try offsets.load(k, tile)).raw();
1494     const end = (try offsets.load(k, try k.add(tile, one_index))).raw();
1495     const local_count = try k.fold(zero_index, command_count, one_index, zero, .{
1496         .ranges = ranges,
1497         .words = words,
1498         .tile = tile,
1499         .tiles_x = tiles_x,
1500         .command_index = command_index,
1501         .zero = zero,
1502         .one = one,
1503     }, count_prior_command);
1504     const covered = try tileCoversCommand(k, ranges, command_index, tile, tiles_x);
1505     try k.guardDo(covered, .{
1506         .indices = indices,
1507         .start = start,
1508         .local_count = local_count,
1509         .command_index = command_index,
1510     }, write_tile_command);
1511     const tile_total = try k.sub(end, start);
1512     const last_command = try k.sub(command_count, one_index);
1513     const writes_cursor = try k.compare(.eq, command_index, last_command);
1514     try k.guardDo(writes_cursor, .{
1515         .cursors = cursors,
1516         .tile_total = tile_total,
1517         .tile = tile,
1518     }, write_tile_cursor);
1519 }
1520 
1521 fn tileCoversCommand(
1522     k: anytype,
1523     ranges: kernel.BufferView(.u32),
1524     command_index: Value,
1525     tile: Value,
1526     tiles_x: Value,
1527 ) !Value {
1528     const base = try laneIndex(k, command_index, tile_range_lanes, 0);
1529     const x0 = try k.castIndex((try ranges.load(k, base)).raw());
1530     const y0 = try k.castIndex((try ranges.load(k, try k.add(base, try k.constantIndex(tile_range_y0)))).raw());
1531     const x1 = try k.castIndex((try ranges.load(k, try k.add(base, try k.constantIndex(tile_range_x1)))).raw());
1532     const y1 = try k.castIndex((try ranges.load(k, try k.add(base, try k.constantIndex(tile_range_y1)))).raw());
1533     const tile_y = try k.div(tile, tiles_x);
1534     const tile_x = try k.sub(tile, try k.mul(tile_y, tiles_x));
1535     const x_lower = try k.compare(.le, x0, tile_x);
1536     const x_upper = try k.compare(.lt, tile_x, x1);
1537     const y_lower = try k.compare(.le, y0, tile_y);
1538     const y_upper = try k.compare(.lt, tile_y, y1);
1539     return k.and_(try k.and_(x_lower, x_upper), try k.and_(y_lower, y_upper));
1540 }
1541 
1542 fn emitTileSort(
1543     k: anytype,
1544     indices: kernel.BufferView(.u32),
1545     offsets: kernel.BufferView(.u32),
1546     words: kernel.BufferView(.u32),
1547     tile: Value,
1548 ) !void {
1549     const one = try k.constantIndex(1);
1550     const zero = try k.constantIndex(0);
1551     const start = try k.castIndex((try offsets.load(k, tile)).raw());
1552     const end = try k.castIndex((try offsets.load(k, try k.add(tile, one))).raw());
1553     const first = try k.min(try k.add(start, one), end);
1554 
1555     var outer = try k.forScope(first, end, one, &.{}, &.{});
1556     errdefer outer.abort();
1557     const cursor = outer.inductionVar();
1558     const value = (try indices.load(k, cursor)).raw();
1559     const insert = try k.fold(start, cursor, one, cursor, .{
1560         .indices = indices,
1561         .words = words,
1562         .value = value,
1563         .cursor = cursor,
1564     }, find_tile_insertion);
1565 
1566     const shift_count = try k.sub(cursor, insert);
1567     _ = try k.fold(zero, shift_count, one, zero, .{
1568         .indices = indices,
1569         .cursor = cursor,
1570         .one = one,
1571     }, shift_tile_index_right);
1572     try indices.store(k, value, insert);
1573     try outer.leave(&.{});
1574 }
1575 
1576 fn commandBeforeDevice(
1577     k: anytype,
1578     words: kernel.BufferView(.u32),
1579     left_index: Value,
1580     right_index: Value,
1581 ) !Value {
1582     const left_order = try loadWord(k, words, try k.castIndex(left_index), word_order);
1583     const right_order = try loadWord(k, words, try k.castIndex(right_index), word_order);
1584     const order_before = try k.compare(.lt, left_order, right_order);
1585     const order_equal = try k.compare(.eq, left_order, right_order);
1586     const index_before = try k.compare(.lt, left_index, right_index);
1587     return k.or_(order_before, try k.and_(order_equal, index_before));
1588 }
1589 
1590 const Pixel = struct {
1591     coord: Coord,
1592     index: Value,
1593     local_x: Value,
1594     local_y: Value,
1595 };
1596 
1597 const Coord = struct {
1598     x: Value,
1599     y: Value,
1600 };
1601 
1602 const Channels = struct {
1603     r: Value,
1604     g: Value,
1605     b: Value,
1606     a: Value,
1607 };
1608 
1609 fn emitCommand(
1610     k: anytype,
1611     floats: kernel.BufferView(.f32),
1612     words: kernel.BufferView(.u32),
1613     images: kernel.BufferView(.u32),
1614     image_pixels: kernel.BufferView(.u32),
1615     index: Value,
1616     coord: Coord,
1617     dst: Channels,
1618 ) !Channels {
1619     const rect = try loadRect(k, floats, index, float_rect_x);
1620     const clip = try loadRect(k, floats, index, float_clip_x);
1621     const radius = try loadFloat(k, floats, index, float_radius);
1622     const width = try loadFloat(k, floats, index, float_width);
1623     const source = try loadRect(k, floats, index, float_source_x);
1624     const gradient_start = Coord{
1625         .x = try loadFloat(k, floats, index, float_gradient_start_x),
1626         .y = try loadFloat(k, floats, index, float_gradient_start_y),
1627     };
1628     const gradient_end = Coord{
1629         .x = try loadFloat(k, floats, index, float_gradient_end_x),
1630         .y = try loadFloat(k, floats, index, float_gradient_end_y),
1631     };
1632     const kind = try loadWord(k, words, index, word_kind);
1633     const image_index = try loadWord(k, words, index, word_image);
1634     const src = Channels{
1635         .r = try loadWord(k, words, index, word_r),
1636         .g = try loadWord(k, words, index, word_g),
1637         .b = try loadWord(k, words, index, word_b),
1638         .a = try loadWord(k, words, index, word_a),
1639     };
1640     const packed_end = try loadWord(k, words, index, word_color_end);
1641     const end = Channels{
1642         .r = try channel(k, packed_end, 0),
1643         .g = try channel(k, packed_end, 8),
1644         .b = try channel(k, packed_end, 16),
1645         .a = try channel(k, packed_end, 24),
1646     };
1647 
1648     const fill = try k.compare(.eq, kind, try u32v(k, @backingInt(command.Kind.fill)));
1649     const linear_gradient = try k.compare(.eq, kind, try u32v(k, @backingInt(command.Kind.linear_gradient)));
1650     const stroke = try k.compare(.eq, kind, try u32v(k, @backingInt(command.Kind.stroke)));
1651     const image = try k.compare(.eq, kind, try u32v(k, @backingInt(command.Kind.image)));
1652     const shadow = try k.compare(.eq, kind, try u32v(k, @backingInt(command.Kind.shadow)));
1653     const glyph = try k.compare(.eq, kind, try u32v(k, @backingInt(command.Kind.glyph)));
1654     const image_like = try k.or_(image, glyph);
1655     const fill_like = try k.or_(fill, linear_gradient);
1656     const coverage = try coverageCount(k, rect, clip, radius, width, fill_like, stroke, image, shadow, glyph, coord);
1657     const covered = try k.compare(.gt, coverage, try u32v(k, 0));
1658     const sampled_image = try imageColor(k, images, image_pixels, image_index, source, rect, coord, coverage, src.a);
1659     const sampled_shadow = try shadowColor(k, src, rect, radius, width, coord, coverage);
1660     const sampled_gradient = try linearGradientColor(k, src, end, gradient_start, gradient_end, coord);
1661     const sampled = Channels{
1662         .r = try k.select(linear_gradient, sampled_gradient.r, try k.select(image, sampled_image.r, src.r)),
1663         .g = try k.select(linear_gradient, sampled_gradient.g, try k.select(image, sampled_image.g, src.g)),
1664         .b = try k.select(linear_gradient, sampled_gradient.b, try k.select(image, sampled_image.b, src.b)),
1665         .a = try k.select(
1666             linear_gradient,
1667             try coverageAlpha(k, sampled_gradient.a, coverage),
1668             try k.select(shadow, sampled_shadow.a, try k.select(image_like, sampled_image.a, try coverageAlpha(k, src.a, coverage))),
1669         ),
1670     };
1671     const alpha = try k.compare(.gt, sampled.a, try u32v(k, 0));
1672     const paint = try k.and_(covered, alpha);
1673     const blended = try blend(k, dst, sampled);
1674     return .{
1675         .r = try k.select(paint, blended.r, dst.r),
1676         .g = try k.select(paint, blended.g, dst.g),
1677         .b = try k.select(paint, blended.b, dst.b),
1678         .a = try k.select(paint, blended.a, dst.a),
1679     };
1680 }
1681 
1682 fn linearGradientColor(k: anytype, start: Channels, end: Channels, from: Coord, to: Coord, coord: Coord) !Channels {
1683     const zero = try f32v(k, 0);
1684     const one = try f32v(k, 1);
1685     const dx = try k.sub(to.x, from.x);
1686     const dy = try k.sub(to.y, from.y);
1687     const length_squared = try k.add(try k.mul(dx, dx), try k.mul(dy, dy));
1688     const degenerate = try k.compare(.le, length_squared, try f32v(k, std.math.floatEps(f32)));
1689     const safe_length = try k.select(degenerate, one, length_squared);
1690     const projected = try k.div(
1691         try k.add(
1692             try k.mul(try k.sub(coord.x, from.x), dx),
1693             try k.mul(try k.sub(coord.y, from.y), dy),
1694         ),
1695         safe_length,
1696     );
1697     const t = try k.select(degenerate, one, try k.min(one, try k.max(zero, projected)));
1698     const alpha_value = try interpolateChannelFloat(k, start.a, end.a, t);
1699     const positive_alpha = try k.compare(.gt, alpha_value, zero);
1700     const safe_alpha = try k.select(positive_alpha, alpha_value, one);
1701     return .{
1702         .r = try unpremultipliedChannel(k, start.r, start.a, end.r, end.a, t, safe_alpha, positive_alpha),
1703         .g = try unpremultipliedChannel(k, start.g, start.a, end.g, end.a, t, safe_alpha, positive_alpha),
1704         .b = try unpremultipliedChannel(k, start.b, start.a, end.b, end.a, t, safe_alpha, positive_alpha),
1705         .a = try k.select(positive_alpha, try roundedChannel(k, alpha_value), try u32v(k, 0)),
1706     };
1707 }
1708 
1709 fn unpremultipliedChannel(k: anytype, start: Value, start_alpha: Value, end: Value, end_alpha: Value, t: Value, alpha_value: Value, positive_alpha: Value) !Value {
1710     const start_value = try k.mul(try k.cast(start, .f32), try k.cast(start_alpha, .f32));
1711     const end_value = try k.mul(try k.cast(end, .f32), try k.cast(end_alpha, .f32));
1712     const value = try k.div(try interpolateFloat(k, start_value, end_value, t), alpha_value);
1713     return k.select(positive_alpha, try roundedChannel(k, value), try u32v(k, 0));
1714 }
1715 
1716 fn interpolateChannelFloat(k: anytype, start: Value, end: Value, t: Value) !Value {
1717     return interpolateFloat(k, try k.cast(start, .f32), try k.cast(end, .f32), t);
1718 }
1719 
1720 fn interpolateFloat(k: anytype, start: Value, end: Value, t: Value) !Value {
1721     return k.add(start, try k.mul(try k.sub(end, start), t));
1722 }
1723 
1724 fn roundedChannel(k: anytype, value: Value) !Value {
1725     const clamped = try k.min(try f32v(k, 255), try k.max(try f32v(k, 0), value));
1726     return k.cast(try k.floor(try k.add(clamped, try f32v(k, 0.5))), .u32);
1727 }
1728 
1729 fn pixelForGid(k: anytype, gid: Value) !Pixel {
1730     const gid_u32 = try k.cast(gid, .u32);
1731     const view_width = k.argument(arg_view_width);
1732     const local_y = try k.div(gid_u32, view_width);
1733     const local_x = try k.sub(gid_u32, try k.mul(local_y, view_width));
1734     const target_x = try k.add(k.argument(arg_view_x), local_x);
1735     const target_y = try k.add(k.argument(arg_view_y), local_y);
1736     const half = try f32v(k, 0.5);
1737     return .{
1738         .coord = .{
1739             .x = try k.add(try k.cast(target_x, .f32), half),
1740             .y = try k.add(try k.cast(target_y, .f32), half),
1741         },
1742         .index = gid,
1743         .local_x = local_x,
1744         .local_y = local_y,
1745     };
1746 }
1747 
1748 fn copyPackedRegion(dst: []u32, width: u32, region: Region, src: []const u32) void {
1749     var y: u32 = 0;
1750     while (y < region.height) : (y += 1) {
1751         const dst_start = @as(usize, region.y + y) * width + region.x;
1752         const src_start = @as(usize, y) * region.width;
1753         @memcpy(dst[dst_start .. dst_start + region.width], src[src_start .. src_start + region.width]);
1754     }
1755 }
1756 
1757 const RectValue = struct {
1758     x: Value,
1759     y: Value,
1760     width: Value,
1761     height: Value,
1762 };
1763 
1764 fn commandBoundsValue(k: anytype, rect: RectValue, shadow: Value, width: Value) !RectValue {
1765     const blur = try k.max(width, try f32v(k, 0));
1766     const shadow_bounds = RectValue{
1767         .x = try k.sub(rect.x, blur),
1768         .y = try k.sub(rect.y, blur),
1769         .width = try k.add(rect.width, try k.mul(blur, try f32v(k, 2))),
1770         .height = try k.add(rect.height, try k.mul(blur, try f32v(k, 2))),
1771     };
1772     return selectRect(k, shadow, shadow_bounds, rect);
1773 }
1774 
1775 fn selectRect(k: anytype, condition: Value, true_rect: RectValue, false_rect: RectValue) !RectValue {
1776     return .{
1777         .x = try k.select(condition, true_rect.x, false_rect.x),
1778         .y = try k.select(condition, true_rect.y, false_rect.y),
1779         .width = try k.select(condition, true_rect.width, false_rect.width),
1780         .height = try k.select(condition, true_rect.height, false_rect.height),
1781     };
1782 }
1783 
1784 fn loadRect(k: anytype, floats: kernel.BufferView(.f32), command_index: Value, comptime lane: usize) !RectValue {
1785     return .{
1786         .x = try loadFloat(k, floats, command_index, lane),
1787         .y = try loadFloat(k, floats, command_index, lane + 1),
1788         .width = try loadFloat(k, floats, command_index, lane + 2),
1789         .height = try loadFloat(k, floats, command_index, lane + 3),
1790     };
1791 }
1792 
1793 fn loadFloat(k: anytype, floats: kernel.BufferView(.f32), command_index: Value, comptime lane: usize) !Value {
1794     return (try floats.load(k, try laneIndex(k, command_index, float_lanes, lane))).raw();
1795 }
1796 
1797 fn loadWord(k: anytype, words: kernel.BufferView(.u32), command_index: Value, comptime lane: usize) !Value {
1798     return (try words.load(k, try laneIndex(k, command_index, word_lanes, lane))).raw();
1799 }
1800 
1801 fn laneIndex(k: anytype, command_index: Value, comptime stride: usize, comptime lane: usize) !Value {
1802     const base = try k.mul(command_index, try k.constantIndex(stride));
1803     return k.add(base, try k.constantIndex(lane));
1804 }
1805 
1806 const ClampRound = enum { floor, ceil };
1807 
1808 fn floorClampValue(k: anytype, value: Value, limit: Value) !Value {
1809     return roundedClampValue(k, value, limit, .floor);
1810 }
1811 
1812 fn ceilClampValue(k: anytype, value: Value, limit: Value) !Value {
1813     return roundedClampValue(k, value, limit, .ceil);
1814 }
1815 
1816 fn roundedClampValue(k: anytype, value: Value, limit: Value, comptime round: ClampRound) !Value {
1817     const zero = try f32v(k, 0);
1818     const limit_f = try k.cast(limit, .f32);
1819     const max_finite = try f32v(k, std.math.floatMax(f32));
1820     const min_finite = try k.neg(max_finite);
1821     const finite = try k.and_(
1822         try k.and_(try k.compare(.eq, value, value), try k.compare(.le, value, max_finite)),
1823         try k.compare(.ge, value, min_finite),
1824     );
1825     const safe_value = try k.select(finite, value, zero);
1826     const rounded = switch (round) {
1827         .floor => try k.floor(safe_value),
1828         .ceil => try k.neg(try k.floor(try k.neg(safe_value))),
1829     };
1830     const clamped = try k.min(try k.max(rounded, zero), limit_f);
1831     const base = try k.cast(clamped, .u32);
1832     const over_limit = try k.compare(.gt, value, limit_f);
1833     return k.select(over_limit, limit, base);
1834 }
1835 
1836 fn tileStart(k: anytype, coord: Value, region_start: Value, tiles: Value) !Value {
1837     const one = try u32v(k, 1);
1838     const local = try k.sub(try k.max(coord, region_start), region_start);
1839     const raw = try k.ushr(local, try u32v(k, tile_size_log2));
1840     return k.min(raw, try k.sub(tiles, one));
1841 }
1842 
1843 fn tileEnd(k: anytype, coord: Value, region_start: Value, tiles: Value) !Value {
1844     const one = try u32v(k, 1);
1845     const local = try k.sub(try k.max(coord, region_start), region_start);
1846     const range_end = try k.add(try k.ushr(try k.sub(try k.max(local, one), one), try u32v(k, tile_size_log2)), one);
1847     return k.min(range_end, tiles);
1848 }
1849 
1850 fn coverageCount(
1851     k: anytype,
1852     rect: RectValue,
1853     clip: RectValue,
1854     radius: Value,
1855     width: Value,
1856     fill: Value,
1857     stroke: Value,
1858     image: Value,
1859     shadow: Value,
1860     glyph: Value,
1861     coord: Coord,
1862 ) !Value {
1863     const offset = try f32v(k, 0.25);
1864     const left = try k.sub(coord.x, offset);
1865     const right = try k.add(coord.x, offset);
1866     const top = try k.sub(coord.y, offset);
1867     const bottom = try k.add(coord.y, offset);
1868     const a = try coverageBit(k, try sampleCovers(k, rect, clip, radius, width, fill, stroke, image, shadow, glyph, left, top));
1869     const b = try coverageBit(k, try sampleCovers(k, rect, clip, radius, width, fill, stroke, image, shadow, glyph, right, top));
1870     const c = try coverageBit(k, try sampleCovers(k, rect, clip, radius, width, fill, stroke, image, shadow, glyph, left, bottom));
1871     const d = try coverageBit(k, try sampleCovers(k, rect, clip, radius, width, fill, stroke, image, shadow, glyph, right, bottom));
1872     return k.add(try k.add(a, b), try k.add(c, d));
1873 }
1874 
1875 fn sampleCovers(
1876     k: anytype,
1877     rect: RectValue,
1878     clip: RectValue,
1879     radius: Value,
1880     width: Value,
1881     fill: Value,
1882     stroke: Value,
1883     image: Value,
1884     shadow: Value,
1885     glyph: Value,
1886     x: Value,
1887     y: Value,
1888 ) !Value {
1889     const in_clip = try inside(k, clip, x, y);
1890     const fill_cover = try k.and_(fill, try insideRounded(k, rect, radius, x, y));
1891     const stroke_cover = try k.and_(stroke, try strokeInside(k, rect, radius, width, x, y));
1892     const image_cover = try k.and_(image, try inside(k, rect, x, y));
1893     const shadow_cover = try k.and_(shadow, try shadowInside(k, rect, radius, width, x, y));
1894     const glyph_cover = try k.and_(glyph, try inside(k, rect, x, y));
1895     return k.and_(in_clip, try k.or_(glyph_cover, try k.or_(shadow_cover, try k.or_(image_cover, try k.or_(fill_cover, stroke_cover)))));
1896 }
1897 
1898 fn coverageBit(k: anytype, covered: Value) !Value {
1899     return k.select(covered, try u32v(k, 1), try u32v(k, 0));
1900 }
1901 
1902 fn imageColor(
1903     k: anytype,
1904     images: kernel.BufferView(.u32),
1905     pixels: kernel.BufferView(.u32),
1906     image_index: Value,
1907     source: RectValue,
1908     rect: RectValue,
1909     coord: Coord,
1910     coverage: Value,
1911     opacity: Value,
1912 ) !Channels {
1913     const image_count = k.argument(arg_image_count);
1914     const valid_index = try k.compare(.lt, image_index, image_count);
1915     const safe_image = try k.select(valid_index, image_index, try u32v(k, 0));
1916     const safe_image_index = try k.castIndex(safe_image);
1917     const meta_base = try k.mul(safe_image_index, try k.constantIndex(image_lanes));
1918     const width = (try images.load(k, try k.add(meta_base, try k.constantIndex(image_width)))).raw();
1919     const height = (try images.load(k, try k.add(meta_base, try k.constantIndex(image_height)))).raw();
1920     const offset = (try images.load(k, try k.add(meta_base, try k.constantIndex(image_offset)))).raw();
1921     const one = try u32v(k, 1);
1922     const safe_width = try k.max(width, one);
1923     const safe_height = try k.max(height, one);
1924     const valid_dims = try k.and_(try k.compare(.gt, width, try u32v(k, 0)), try k.compare(.gt, height, try u32v(k, 0)));
1925     const valid = try k.and_(valid_index, valid_dims);
1926     const source_rect = try imageSourceRect(k, source, safe_width, safe_height);
1927     const u = try k.max(try f32v(k, 0), try k.min(try f32v(k, 0.999999), try k.div(try k.sub(coord.x, rect.x), rect.width)));
1928     const v = try k.max(try f32v(k, 0), try k.min(try f32v(k, 0.999999), try k.div(try k.sub(coord.y, rect.y), rect.height)));
1929     const sx = try imageSampleIndex(k, try k.add(source_rect.x, try k.mul(u, source_rect.width)), safe_width);
1930     const sy = try imageSampleIndex(k, try k.add(source_rect.y, try k.mul(v, source_rect.height)), safe_height);
1931     const pixel_index = try k.add(try k.castIndex(offset), try k.add(sx, try k.mul(sy, try k.castIndex(safe_width))));
1932     const pixel = (try pixels.load(k, pixel_index)).raw();
1933     const image_alpha = try div255(k, try k.mul(try channel(k, pixel, 24), opacity));
1934     const alpha = try coverageAlpha(k, image_alpha, coverage);
1935     return .{
1936         .r = try k.select(valid, try channel(k, pixel, 0), try u32v(k, 0)),
1937         .g = try k.select(valid, try channel(k, pixel, 8), try u32v(k, 0)),
1938         .b = try k.select(valid, try channel(k, pixel, 16), try u32v(k, 0)),
1939         .a = try k.select(valid, alpha, try u32v(k, 0)),
1940     };
1941 }
1942 
1943 fn imageSourceRect(k: anytype, source: RectValue, width: Value, height: Value) !RectValue {
1944     const explicit = try k.and_(try k.compare(.gt, source.width, try f32v(k, 0)), try k.compare(.gt, source.height, try f32v(k, 0)));
1945     return .{
1946         .x = try k.select(explicit, source.x, try f32v(k, 0)),
1947         .y = try k.select(explicit, source.y, try f32v(k, 0)),
1948         .width = try k.select(explicit, source.width, try k.cast(width, .f32)),
1949         .height = try k.select(explicit, source.height, try k.cast(height, .f32)),
1950     };
1951 }
1952 
1953 fn imageSampleIndex(k: anytype, value: Value, limit: Value) !Value {
1954     const limit_f = try k.cast(limit, .f32);
1955     const max_value = try k.sub(limit_f, try f32v(k, 1));
1956     const clamped = try k.min(try k.max(try k.floor(value), try f32v(k, 0)), max_value);
1957     return k.castIndex(try k.cast(clamped, .u32));
1958 }
1959 
1960 fn shadowColor(k: anytype, src: Channels, rect: RectValue, radius: Value, blur: Value, coord: Coord, coverage: Value) !Channels {
1961     const zero = try f32v(k, 0);
1962     const one = try f32v(k, 1);
1963     const distance = try roundedRectOutsideDistance(k, rect, radius, coord.x, coord.y);
1964     const positive_blur = try k.compare(.gt, blur, zero);
1965     const safe_blur = try k.max(blur, one);
1966     const t = try k.max(zero, try k.min(one, try k.sub(one, try k.div(distance, safe_blur))));
1967     const alpha_scale = try k.mul(t, t);
1968     const alpha_f = try k.mul(try k.cast(src.a, .f32), alpha_scale);
1969     const alpha = try k.cast(try k.floor(try k.add(alpha_f, try f32v(k, 0.5))), .u32);
1970     const hard = try k.compare(.le, distance, zero);
1971     const soft_alpha = try coverageAlpha(k, alpha, coverage);
1972     const hard_alpha = try coverageAlpha(k, src.a, coverage);
1973     return .{
1974         .r = src.r,
1975         .g = src.g,
1976         .b = src.b,
1977         .a = try k.select(positive_blur, soft_alpha, try k.select(hard, hard_alpha, try u32v(k, 0))),
1978     };
1979 }
1980 
1981 fn shadowInside(k: anytype, rect: RectValue, radius: Value, blur: Value, x: Value, y: Value) !Value {
1982     const zero = try f32v(k, 0);
1983     const distance = try roundedRectOutsideDistance(k, rect, radius, x, y);
1984     const positive_blur = try k.compare(.gt, blur, zero);
1985     const soft = try k.compare(.lt, distance, blur);
1986     const hard = try k.compare(.le, distance, zero);
1987     return k.or_(try k.and_(positive_blur, soft), try k.and_(try k.not(positive_blur), hard));
1988 }
1989 
1990 fn inside(k: anytype, rect: RectValue, x: Value, y: Value) !Value {
1991     const x0 = try k.compare(.ge, x, rect.x);
1992     const y0 = try k.compare(.ge, y, rect.y);
1993     const x1 = try k.compare(.lt, x, try k.add(rect.x, rect.width));
1994     const y1 = try k.compare(.lt, y, try k.add(rect.y, rect.height));
1995     return k.and_(try k.and_(x0, y0), try k.and_(x1, y1));
1996 }
1997 
1998 fn roundedRectOutsideDistance(k: anytype, rect: RectValue, radius: Value, x: Value, y: Value) !Value {
1999     const zero = try f32v(k, 0);
2000     const half = try f32v(k, 0.5);
2001     const clamped_radius = try k.min(
2002         try k.max(radius, zero),
2003         try k.mul(try k.min(rect.width, rect.height), half),
2004     );
2005     const inner_x0 = try k.add(rect.x, clamped_radius);
2006     const inner_y0 = try k.add(rect.y, clamped_radius);
2007     const inner_x1 = try k.sub(try k.add(rect.x, rect.width), clamped_radius);
2008     const inner_y1 = try k.sub(try k.add(rect.y, rect.height), clamped_radius);
2009     const cx = try k.min(try k.max(x, inner_x0), inner_x1);
2010     const cy = try k.min(try k.max(y, inner_y0), inner_y1);
2011     const dx = try k.sub(x, cx);
2012     const dy = try k.sub(y, cy);
2013     const dist = try k.sqrt(try k.add(try k.mul(dx, dx), try k.mul(dy, dy)));
2014     return k.max(try k.sub(dist, clamped_radius), zero);
2015 }
2016 
2017 fn insideRounded(k: anytype, rect: RectValue, radius: Value, x: Value, y: Value) !Value {
2018     const in_rect = try inside(k, rect, x, y);
2019     const zero = try f32v(k, 0);
2020     const half = try f32v(k, 0.5);
2021     const clamped_radius = try k.min(
2022         try k.max(radius, zero),
2023         try k.mul(try k.min(rect.width, rect.height), half),
2024     );
2025     const inner_x0 = try k.add(rect.x, clamped_radius);
2026     const inner_y0 = try k.add(rect.y, clamped_radius);
2027     const inner_x1 = try k.sub(try k.add(rect.x, rect.width), clamped_radius);
2028     const inner_y1 = try k.sub(try k.add(rect.y, rect.height), clamped_radius);
2029     const cx = try k.min(try k.max(x, inner_x0), inner_x1);
2030     const cy = try k.min(try k.max(y, inner_y0), inner_y1);
2031     const dx = try k.sub(x, cx);
2032     const dy = try k.sub(y, cy);
2033     const dist2 = try k.add(try k.mul(dx, dx), try k.mul(dy, dy));
2034     const radius2 = try k.mul(clamped_radius, clamped_radius);
2035     return k.and_(in_rect, try k.compare(.le, dist2, radius2));
2036 }
2037 
2038 fn strokeInside(k: anytype, rect: RectValue, radius: Value, width: Value, x: Value, y: Value) !Value {
2039     const zero = try f32v(k, 0);
2040     const outer = try insideRounded(k, rect, radius, x, y);
2041     const positive = try k.compare(.gt, width, zero);
2042     const inner = RectValue{
2043         .x = try k.add(rect.x, width),
2044         .y = try k.add(rect.y, width),
2045         .width = try k.max(try k.sub(rect.width, try k.mul(width, try f32v(k, 2))), zero),
2046         .height = try k.max(try k.sub(rect.height, try k.mul(width, try f32v(k, 2))), zero),
2047     };
2048     const has_inner = try k.and_(try k.compare(.gt, inner.width, zero), try k.compare(.gt, inner.height, zero));
2049     const inner_radius = try k.max(try k.sub(radius, width), zero);
2050     const inside_inner = try k.and_(has_inner, try insideRounded(k, inner, inner_radius, x, y));
2051     return k.and_(positive, try k.and_(outer, try k.not(inside_inner)));
2052 }
2053 
2054 fn blend(k: anytype, dst: Channels, src: Channels) !Channels {
2055     const inv = try k.sub(try u32v(k, 255), src.a);
2056     return .{
2057         .r = try blendChannel(k, src.r, src.a, dst.r, inv),
2058         .g = try blendChannel(k, src.g, src.a, dst.g, inv),
2059         .b = try blendChannel(k, src.b, src.a, dst.b, inv),
2060         .a = try k.add(src.a, try div255(k, try k.mul(dst.a, inv))),
2061     };
2062 }
2063 
2064 fn coverageAlpha(k: anytype, alpha: Value, coverage: Value) !Value {
2065     return k.div(try k.add(try k.mul(alpha, coverage), try u32v(k, 2)), try u32v(k, 4));
2066 }
2067 
2068 fn blendChannel(k: anytype, src: Value, alpha: Value, dst: Value, inv: Value) !Value {
2069     return div255(k, try k.add(try k.mul(src, alpha), try k.mul(dst, inv)));
2070 }
2071 
2072 fn div255(k: anytype, value: Value) !Value {
2073     return k.div(try k.add(value, try u32v(k, 127)), try u32v(k, 255));
2074 }
2075 
2076 fn packPixel(k: anytype, channels: Channels, format: Value) !Value {
2077     const bgra = try k.compare(.eq, format, try u32v(k, @backingInt(OutputFormat.bgra)));
2078     const first = try k.select(bgra, channels.b, channels.r);
2079     const third = try k.select(bgra, channels.r, channels.b);
2080     return k.or_(
2081         try k.or_(first, try k.shl(channels.g, try u32v(k, 8))),
2082         try k.or_(try k.shl(third, try u32v(k, 16)), try k.shl(channels.a, try u32v(k, 24))),
2083     );
2084 }
2085 
2086 fn channel(k: anytype, pixel: Value, comptime shift: u32) !Value {
2087     return k.and_(try k.ushr(pixel, try u32v(k, shift)), try u32v(k, 0xff));
2088 }
2089 
2090 fn u32v(k: anytype, value: u32) !Value {
2091     return k.constantInt(.u32, value);
2092 }
2093 
2094 fn f32v(k: anytype, value: f32) !Value {
2095     return k.constantFloat(.f32, value);
2096 }
2097 
2098 test "packCommands encodes geometry and style lanes" {
2099     const commands = [_]Command{.{
2100         .kind = .stroke,
2101         .rect = .{ .x = 1, .y = 2, .width = 3, .height = 4 },
2102         .clip = .{ .x = 5, .y = 6, .width = 7, .height = 8 },
2103         .color = .{ .r = 9, .g = 10, .b = 11, .a = 12 },
2104         .radius = 13,
2105         .width = 14,
2106         .source = .{ .x = 15, .y = 16, .width = 17, .height = 18 },
2107         .color_end = .{ .r = 21, .g = 22, .b = 23, .a = 24 },
2108         .gradient_start = .{ .x = 25, .y = 26 },
2109         .gradient_end = .{ .x = 27, .y = 28 },
2110         .image_index = 19,
2111         .order = 20,
2112     }};
2113     var floats = @as([float_lanes]f32, @splat(0));
2114     var words = @as([word_lanes]u32, @splat(0));
2115 
2116     packCommands(commands[0..], floats[0..], words[0..]);
2117 
2118     try std.testing.expectEqual(@as(f32, 1), floats[float_rect_x]);
2119     try std.testing.expectEqual(@as(f32, 8), floats[float_clip_h]);
2120     try std.testing.expectEqual(@as(f32, 15), floats[float_source_x]);
2121     try std.testing.expectEqual(@as(f32, 18), floats[float_source_h]);
2122     try std.testing.expectEqual(@as(f32, 25), floats[float_gradient_start_x]);
2123     try std.testing.expectEqual(@as(f32, 28), floats[float_gradient_end_y]);
2124     try std.testing.expectEqual(@as(u32, @backingInt(command.Kind.stroke)), words[word_kind]);
2125     try std.testing.expectEqual(@as(u32, 12), words[word_a]);
2126     try std.testing.expectEqual(@as(u32, 19), words[word_image]);
2127     try std.testing.expectEqual(@as(u32, 20), words[word_order]);
2128     try std.testing.expectEqual(@as(u32, 0x1817_1615), words[word_color_end]);
2129     try std.testing.expect(commandsEqualPacked(commands[0..], floats[0..], words[0..]));
2130 
2131     const changed = [_]Command{.{
2132         .kind = .stroke,
2133         .rect = .{ .x = 1, .y = 2, .width = 3, .height = 4 },
2134         .clip = .{ .x = 5, .y = 6, .width = 7, .height = 8 },
2135         .color = .{ .r = 9, .g = 10, .b = 11, .a = 13 },
2136         .radius = 13,
2137         .width = 14,
2138         .source = .{ .x = 15, .y = 16, .width = 17, .height = 18 },
2139         .color_end = .{ .r = 21, .g = 22, .b = 23, .a = 24 },
2140         .gradient_start = .{ .x = 25, .y = 26 },
2141         .gradient_end = .{ .x = 27, .y = 28 },
2142         .image_index = 19,
2143         .order = 20,
2144     }};
2145     try std.testing.expect(!commandsEqualPacked(changed[0..], floats[0..], words[0..]));
2146 }
2147 
2148 test "gradient command packing reuses reserved storage without allocation" {
2149     const commands = [_]Command{.{
2150         .kind = .linear_gradient,
2151         .rect = .{ .width = 16, .height = 8 },
2152         .clip = .{ .width = 16, .height = 8 },
2153         .color = .{ .r = 20, .g = 40, .b = 80, .a = 255 },
2154         .color_end = .{ .r = 120, .g = 160, .b = 220, .a = 255 },
2155         .gradient_end = .{ .x = 16, .y = 8 },
2156     }};
2157     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
2158     const allocator = failing.allocator();
2159     const floats = try allocator.alloc(f32, commands.len * float_lanes);
2160     defer allocator.free(floats);
2161     const words = try allocator.alloc(u32, commands.len * word_lanes);
2162     defer allocator.free(words);
2163     packCommands(commands[0..], floats, words);
2164 
2165     failing.fail_index = failing.alloc_index;
2166     failing.resize_fail_index = failing.resize_index;
2167     for (0..8) |_| {
2168         packCommands(commands[0..], floats, words);
2169         try std.testing.expect(commandsEqualPacked(commands[0..], floats, words));
2170     }
2171     try std.testing.expect(!failing.has_induced_failure);
2172 }
2173 
2174 test "gradient command packing makes non-finite geometry degenerate" {
2175     const commands = [_]Command{.{
2176         .kind = .linear_gradient,
2177         .rect = .{ .width = 16, .height = 8 },
2178         .clip = .{ .width = 16, .height = 8 },
2179         .color = .{ .r = 10, .g = 20, .b = 40, .a = 255 },
2180         .color_end = .{ .r = 20, .g = 40, .b = 80, .a = 255 },
2181         .gradient_start = .{ .x = std.math.nan(f32), .y = 4 },
2182         .gradient_end = .{ .x = 12, .y = std.math.inf(f32) },
2183     }};
2184     var floats = @as([float_lanes]f32, @splat(1));
2185     var words = @as([word_lanes]u32, @splat(0));
2186 
2187     packCommands(commands[0..], floats[0..], words[0..]);
2188 
2189     try std.testing.expectEqual(@as(f32, 0), floats[float_gradient_start_x]);
2190     try std.testing.expectEqual(@as(f32, 0), floats[float_gradient_start_y]);
2191     try std.testing.expectEqual(@as(f32, 0), floats[float_gradient_end_x]);
2192     try std.testing.expectEqual(@as(f32, 0), floats[float_gradient_end_y]);
2193 }
2194 
2195 test "packImages encodes metadata and pixels" {
2196     const first = [_]u32{ 0x1122_3344, 0x5566_7788 };
2197     const second = [_]u32{0x99aa_bbcc};
2198     const images = ImageSet{ .images = &.{
2199         .{ .width = 2, .height = 1, .pixels = first[0..] },
2200         .{ .width = 1, .height = 1, .pixels = second[0..] },
2201     } };
2202     var metadata = @as([(2 * image_lanes)]u32, @splat(0));
2203     var pixels = @as([3]u32, @splat(0));
2204 
2205     try packImages(images, metadata[0..], pixels[0..]);
2206 
2207     try std.testing.expectEqual(@as(u32, 2), metadata[image_width]);
2208     try std.testing.expectEqual(@as(u32, 1), metadata[image_height]);
2209     try std.testing.expectEqual(@as(u32, 0), metadata[image_offset]);
2210     try std.testing.expectEqual(@as(u32, 1), metadata[image_lanes + image_width]);
2211     try std.testing.expectEqual(@as(u32, 1), metadata[image_lanes + image_height]);
2212     try std.testing.expectEqual(@as(u32, 2), metadata[image_lanes + image_offset]);
2213     try std.testing.expectEqualSlices(u32, &.{ 0x1122_3344, 0x5566_7788, 0x99aa_bbcc }, pixels[0..]);
2214     try std.testing.expect(try imageMetadataEqualPacked(images, metadata[0..]));
2215     try std.testing.expect(try imagesEqualPacked(images, metadata[0..], pixels[0..]));
2216 
2217     const changed_extent = ImageSet{ .images = &.{
2218         .{ .width = 1, .height = 2, .pixels = first[0..] },
2219         .{ .width = 1, .height = 1, .pixels = second[0..] },
2220     } };
2221     const changed_pixels = [_]u32{ 0x1122_3344, 0x5566_7789 };
2222     const changed_content = ImageSet{ .images = &.{
2223         .{ .width = 2, .height = 1, .pixels = changed_pixels[0..] },
2224         .{ .width = 1, .height = 1, .pixels = second[0..] },
2225     } };
2226     try std.testing.expect(!try imageMetadataEqualPacked(changed_extent, metadata[0..]));
2227     try std.testing.expect(try imageMetadataEqualPacked(changed_content, metadata[0..]));
2228     try std.testing.expect(!try imagesEqualPacked(changed_extent, metadata[0..], pixels[0..]));
2229     try std.testing.expect(!try imagesEqualPacked(changed_content, metadata[0..], pixels[0..]));
2230 }
2231 
2232 test "binCommandsAlloc orders tile command lists by paint order" {
2233     const allocator = std.testing.allocator;
2234     var commands = [_]Command{
2235         .{
2236             .kind = .fill,
2237             .rect = .{ .x = 0, .y = 0, .width = 6, .height = 6 },
2238             .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 },
2239             .color = .{ .r = 200, .g = 0, .b = 0, .a = 255 },
2240             .order = 1,
2241         },
2242         .{
2243             .kind = .fill,
2244             .rect = .{ .x = 10, .y = 0, .width = 4, .height = 4 },
2245             .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 },
2246             .color = .{ .r = 0, .g = 200, .b = 0, .a = 255 },
2247             .order = 0,
2248         },
2249         .{
2250             .kind = .fill,
2251             .rect = .{ .x = 1, .y = 1, .width = 3, .height = 3 },
2252             .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 },
2253             .color = .{ .r = 0, .g = 0, .b = 200, .a = 255 },
2254             .order = 2,
2255         },
2256     };
2257 
2258     var bins = try binCommandsAlloc(allocator, commands[0..], 16, 16, Region.full(16, 16));
2259     defer bins.deinit(allocator);
2260 
2261     try std.testing.expectEqualSlices(u32, &.{ 1, 0, 2 }, bins.indices);
2262 }
2263 
2264 test "commandVisits counts per-pixel tile command work" {
2265     const allocator = std.testing.allocator;
2266     var commands = [_]Command{
2267         .{
2268             .kind = .fill,
2269             .rect = .{ .x = 0, .y = 0, .width = 32, .height = 16 },
2270             .clip = .{ .x = 0, .y = 0, .width = 32, .height = 16 },
2271             .color = .{ .r = 200, .g = 0, .b = 0, .a = 255 },
2272             .order = 0,
2273         },
2274         .{
2275             .kind = .fill,
2276             .rect = .{ .x = 16, .y = 0, .width = 16, .height = 16 },
2277             .clip = .{ .x = 0, .y = 0, .width = 32, .height = 16 },
2278             .color = .{ .r = 0, .g = 200, .b = 0, .a = 255 },
2279             .order = 1,
2280         },
2281     };
2282 
2283     var bins = try binCommandsAlloc(allocator, commands[0..], 32, 16, Region.full(32, 16));
2284     defer bins.deinit(allocator);
2285 
2286     try std.testing.expectEqual(@as(usize, 768), commandVisits(bins.view(), Region.full(32, 16)));
2287 }
2288 
2289 test "BinScratch reuses tile bin storage across builds" {
2290     var commands = [_]Command{
2291         .{
2292             .kind = .fill,
2293             .rect = .{ .x = 0, .y = 0, .width = 6, .height = 6 },
2294             .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 },
2295             .color = .{ .r = 200, .g = 0, .b = 0, .a = 255 },
2296             .order = 1,
2297         },
2298         .{
2299             .kind = .fill,
2300             .rect = .{ .x = 10, .y = 0, .width = 4, .height = 4 },
2301             .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 },
2302             .color = .{ .r = 0, .g = 200, .b = 0, .a = 255 },
2303             .order = 0,
2304         },
2305         .{
2306             .kind = .fill,
2307             .rect = .{ .x = 1, .y = 1, .width = 3, .height = 3 },
2308             .clip = .{ .x = 0, .y = 0, .width = 16, .height = 16 },
2309             .color = .{ .r = 0, .g = 0, .b = 200, .a = 255 },
2310             .order = 2,
2311         },
2312     };
2313     var ranges: [3 * tile_range_lanes]u32 = undefined;
2314     var offsets: [2]u32 = undefined;
2315     var indices: [3]u32 = undefined;
2316     var cursors: [1]u32 = undefined;
2317     var scratch = BinScratch{
2318         .ranges = ranges[0..],
2319         .offsets = offsets[0..],
2320         .indices = indices[0..],
2321         .cursors = cursors[0..],
2322     };
2323 
2324     const first = try scratch.binCommands(commands[0..], 16, 16, Region.full(16, 16));
2325     const ranges_ptr = @intFromPtr(scratch.ranges.ptr);
2326     const offsets_ptr = @intFromPtr(scratch.offsets.ptr);
2327     const indices_ptr = @intFromPtr(scratch.indices.ptr);
2328     const cursors_ptr = @intFromPtr(scratch.cursors.ptr);
2329     try std.testing.expectEqualSlices(u32, &.{ 1, 0, 2 }, first.indices);
2330 
2331     const second = try scratch.binCommands(commands[0..], 16, 16, Region.full(16, 16));
2332     try std.testing.expectEqual(ranges_ptr, @intFromPtr(scratch.ranges.ptr));
2333     try std.testing.expectEqual(offsets_ptr, @intFromPtr(scratch.offsets.ptr));
2334     try std.testing.expectEqual(indices_ptr, @intFromPtr(scratch.indices.ptr));
2335     try std.testing.expectEqual(cursors_ptr, @intFromPtr(scratch.cursors.ptr));
2336     try std.testing.expectEqualSlices(u32, &.{ 1, 0, 2 }, second.indices);
2337 }
2338 
2339 test "binPairCount surveys exact command tile pairs without scratch" {
2340     const commands = [_]Command{
2341         .{
2342             .kind = .fill,
2343             .rect = .{ .x = 0, .y = 0, .width = 32, .height = 16 },
2344             .clip = .{ .x = 0, .y = 0, .width = 32, .height = 16 },
2345             .color = .{},
2346         },
2347         .{
2348             .kind = .fill,
2349             .rect = .{ .x = 16, .y = 0, .width = 16, .height = 16 },
2350             .clip = .{ .x = 0, .y = 0, .width = 32, .height = 16 },
2351             .color = .{},
2352         },
2353     };
2354     try std.testing.expectEqual(
2355         @as(usize, 3),
2356         try binPairCount(commands[0..], 32, 16, Region.full(32, 16)),
2357     );
2358 }
2359 
2360 test "Accy tile range graph matches host command ranges" {
2361     const allocator = std.testing.allocator;
2362     const inf = std.math.inf(f32);
2363     var commands = [_]Command{
2364         .{
2365             .kind = .fill,
2366             .rect = .{ .x = 5, .y = 4, .width = 20, .height = 18 },
2367             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 48 },
2368             .color = .{ .r = 200, .g = 0, .b = 0, .a = 255 },
2369         },
2370         .{
2371             .kind = .shadow,
2372             .rect = .{ .x = 34, .y = 26, .width = 8, .height = 7 },
2373             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 48 },
2374             .color = .{ .r = 0, .g = 0, .b = 0, .a = 120 },
2375             .width = 6,
2376         },
2377         .{
2378             .kind = .image,
2379             .rect = .{ .x = 0, .y = 0, .width = 2, .height = 2 },
2380             .clip = .{ .x = 50, .y = 0, .width = 4, .height = 4 },
2381             .color = .{ .r = 255, .g = 255, .b = 255, .a = 255 },
2382         },
2383         .{
2384             .kind = .stroke,
2385             .rect = .{ .x = 18, .y = 7, .width = 19, .height = 4 },
2386             .clip = .{ .x = 16, .y = 0, .width = 12, .height = 40 },
2387             .color = .{ .r = 0, .g = 120, .b = 200, .a = 255 },
2388             .width = 2,
2389         },
2390         .{
2391             .kind = .fill,
2392             .rect = .{ .x = inf, .y = 1, .width = 5, .height = 5 },
2393             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 48 },
2394             .color = .{ .r = 20, .g = 40, .b = 60, .a = 255 },
2395         },
2396     };
2397     const width: u32 = 64;
2398     const height: u32 = 48;
2399     const region = Region{ .x = 4, .y = 3, .width = 40, .height = 32 };
2400     const range_count = tileRangeValueCount(commands.len);
2401 
2402     const expected = try allocator.alloc(u32, range_count);
2403     defer allocator.free(expected);
2404     fillCommandTileRanges(commands[0..], width, height, binShape(width, height, region), expected);
2405 
2406     var encoded = try packCommandsAlloc(allocator, commands[0..]);
2407     defer encoded.deinit(allocator);
2408 
2409     const actual = try allocator.alloc(u32, range_count);
2410     defer allocator.free(actual);
2411     @memset(actual, 0xffff_ffff);
2412 
2413     var graph = try buildTileRangeGraph(allocator, default_threads);
2414     defer graph.deinit();
2415     try runPackedTileRangesCpuRegion(allocator, &graph, encoded, commands.len, width, height, actual, region);
2416 
2417     try std.testing.expectEqualSlices(u32, expected, actual);
2418 }
2419 
2420 test "Accy tile count graph matches host range counts" {
2421     const allocator = std.testing.allocator;
2422     const commands = [_]Command{
2423         .{
2424             .kind = .fill,
2425             .rect = .{ .x = 5, .y = 4, .width = 20, .height = 18 },
2426             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 48 },
2427             .color = .{ .r = 200, .g = 0, .b = 0, .a = 255 },
2428         },
2429         .{
2430             .kind = .shadow,
2431             .rect = .{ .x = 34, .y = 26, .width = 8, .height = 7 },
2432             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 48 },
2433             .color = .{ .r = 0, .g = 0, .b = 0, .a = 120 },
2434             .width = 6,
2435         },
2436         .{
2437             .kind = .image,
2438             .rect = .{ .x = 0, .y = 0, .width = 2, .height = 2 },
2439             .clip = .{ .x = 50, .y = 0, .width = 4, .height = 4 },
2440             .color = .{ .r = 255, .g = 255, .b = 255, .a = 255 },
2441         },
2442         .{
2443             .kind = .stroke,
2444             .rect = .{ .x = 18, .y = 7, .width = 19, .height = 4 },
2445             .clip = .{ .x = 16, .y = 0, .width = 12, .height = 40 },
2446             .color = .{ .r = 0, .g = 120, .b = 200, .a = 255 },
2447             .width = 2,
2448         },
2449     };
2450     const width: u32 = 64;
2451     const height: u32 = 48;
2452     const region = Region{ .x = 4, .y = 3, .width = 40, .height = 32 };
2453     const shape = binShape(width, height, region);
2454 
2455     const ranges = try allocator.alloc(u32, tileRangeValueCount(commands.len));
2456     defer allocator.free(ranges);
2457     fillCommandTileRanges(commands[0..], width, height, shape, ranges);
2458 
2459     const expected = try allocator.alloc(u32, shape.tile_count + 1);
2460     defer allocator.free(expected);
2461     countTilePairs(ranges, shape, expected);
2462 
2463     var expected_total: usize = 0;
2464     for (expected[1..]) |count| expected_total += count;
2465 
2466     const actual = try allocator.alloc(u32, shape.tile_count + 1);
2467     defer allocator.free(actual);
2468     @memset(actual, 0xffff_ffff);
2469 
2470     var graph = try buildTileCountGraph(allocator, default_threads);
2471     defer graph.deinit();
2472     const actual_total = try runPackedTileCountsCpu(allocator, &graph, ranges, commands.len, shape.tiles_x, shape.tiles_y, actual);
2473 
2474     try std.testing.expectEqual(expected_total, actual_total);
2475     try std.testing.expectEqualSlices(u32, expected, actual);
2476 
2477     const prefixed = try allocator.dupe(u32, actual);
2478     defer allocator.free(prefixed);
2479     try std.testing.expectEqual(actual_total, prefixBinOffsets(prefixed));
2480 }
2481 
2482 test "Accy tile count scan matches host CSR offsets" {
2483     const allocator = std.testing.allocator;
2484     const commands = [_]Command{
2485         .{
2486             .kind = .fill,
2487             .rect = .{ .x = 5, .y = 4, .width = 20, .height = 18 },
2488             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 48 },
2489             .color = .{ .r = 200, .g = 0, .b = 0, .a = 255 },
2490         },
2491         .{
2492             .kind = .shadow,
2493             .rect = .{ .x = 34, .y = 26, .width = 8, .height = 7 },
2494             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 48 },
2495             .color = .{ .r = 0, .g = 0, .b = 0, .a = 120 },
2496             .width = 6,
2497         },
2498         .{
2499             .kind = .stroke,
2500             .rect = .{ .x = 18, .y = 7, .width = 19, .height = 4 },
2501             .clip = .{ .x = 16, .y = 0, .width = 12, .height = 40 },
2502             .color = .{ .r = 0, .g = 120, .b = 200, .a = 255 },
2503             .width = 2,
2504         },
2505     };
2506     const width: u32 = 64;
2507     const height: u32 = 48;
2508     const region = Region{ .x = 4, .y = 3, .width = 40, .height = 32 };
2509     const shape = binShape(width, height, region);
2510 
2511     const ranges = try allocator.alloc(u32, tileRangeValueCount(commands.len));
2512     defer allocator.free(ranges);
2513     fillCommandTileRanges(commands[0..], width, height, shape, ranges);
2514 
2515     const expected = try allocator.alloc(u32, shape.tile_count + 1);
2516     defer allocator.free(expected);
2517     countTilePairs(ranges, shape, expected);
2518     const expected_total = prefixBinOffsets(expected);
2519 
2520     const actual = try allocator.alloc(u32, shape.tile_count + 1);
2521     defer allocator.free(actual);
2522     @memset(actual, 0xffff_ffff);
2523 
2524     var graph = try buildTileCountGraph(allocator, default_threads);
2525     defer graph.deinit();
2526     const actual_total = try runPackedTileOffsetsCpu(allocator, &graph, ranges, commands.len, shape.tiles_x, shape.tiles_y, actual);
2527 
2528     try std.testing.expectEqual(expected_total, actual_total);
2529     try std.testing.expectEqualSlices(u32, expected, actual);
2530 }
2531 
2532 test "Accy tile count scan covers multi-block CSR offsets" {
2533     const allocator = std.testing.allocator;
2534     const width: u32 = tile_size * 33;
2535     const height: u32 = tile_size * 32;
2536     const commands = [_]Command{.{
2537         .kind = .fill,
2538         .rect = .{ .x = 0, .y = 0, .width = @floatFromInt(width), .height = @floatFromInt(height) },
2539         .clip = .{ .x = 0, .y = 0, .width = @floatFromInt(width), .height = @floatFromInt(height) },
2540         .color = .{ .r = 20, .g = 80, .b = 160, .a = 255 },
2541     }};
2542     const shape = binShape(width, height, Region.full(width, height));
2543     try std.testing.expect(shape.tile_count + 1 > scan_library.prefix_sum_max_threads);
2544 
2545     const ranges = try allocator.alloc(u32, tileRangeValueCount(commands.len));
2546     defer allocator.free(ranges);
2547     fillCommandTileRanges(commands[0..], width, height, shape, ranges);
2548 
2549     const expected = try allocator.alloc(u32, shape.tile_count + 1);
2550     defer allocator.free(expected);
2551     countTilePairs(ranges, shape, expected);
2552     const expected_total = prefixBinOffsets(expected);
2553 
2554     const actual = try allocator.alloc(u32, shape.tile_count + 1);
2555     defer allocator.free(actual);
2556     @memset(actual, 0xffff_ffff);
2557 
2558     var graph = try buildTileCountGraph(allocator, default_threads);
2559     defer graph.deinit();
2560     const actual_total = try runPackedTileOffsetsCpu(allocator, &graph, ranges, commands.len, shape.tiles_x, shape.tiles_y, actual);
2561 
2562     try std.testing.expectEqual(expected_total, actual_total);
2563     try std.testing.expectEqual(@as(usize, shape.tile_count), actual_total);
2564     try std.testing.expectEqualSlices(u32, expected, actual);
2565 }
2566 
2567 test "Accy tile index graph fills scanned CSR command lists" {
2568     const allocator = std.testing.allocator;
2569     const commands = [_]Command{
2570         .{
2571             .kind = .fill,
2572             .rect = .{ .x = 5, .y = 4, .width = 20, .height = 18 },
2573             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 48 },
2574             .color = .{ .r = 200, .g = 0, .b = 0, .a = 255 },
2575             .order = 2,
2576         },
2577         .{
2578             .kind = .shadow,
2579             .rect = .{ .x = 34, .y = 26, .width = 8, .height = 7 },
2580             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 48 },
2581             .color = .{ .r = 0, .g = 0, .b = 0, .a = 120 },
2582             .width = 6,
2583             .order = 0,
2584         },
2585         .{
2586             .kind = .image,
2587             .rect = .{ .x = 0, .y = 0, .width = 2, .height = 2 },
2588             .clip = .{ .x = 50, .y = 0, .width = 4, .height = 4 },
2589             .color = .{ .r = 255, .g = 255, .b = 255, .a = 255 },
2590             .order = 1,
2591         },
2592         .{
2593             .kind = .stroke,
2594             .rect = .{ .x = 18, .y = 7, .width = 19, .height = 4 },
2595             .clip = .{ .x = 16, .y = 0, .width = 12, .height = 40 },
2596             .color = .{ .r = 0, .g = 120, .b = 200, .a = 255 },
2597             .width = 2,
2598             .order = 3,
2599         },
2600     };
2601     const width: u32 = 64;
2602     const height: u32 = 48;
2603     const region = Region{ .x = 4, .y = 3, .width = 40, .height = 32 };
2604     const shape = binShape(width, height, region);
2605 
2606     var encoded = try packCommandsAlloc(allocator, commands[0..]);
2607     defer encoded.deinit(allocator);
2608 
2609     const ranges = try allocator.alloc(u32, tileRangeValueCount(commands.len));
2610     defer allocator.free(ranges);
2611     fillCommandTileRanges(commands[0..], width, height, shape, ranges);
2612 
2613     const offsets = try allocator.alloc(u32, shape.tile_count + 1);
2614     defer allocator.free(offsets);
2615     var count_graph = try buildTileCountGraph(allocator, default_threads);
2616     defer count_graph.deinit();
2617     const total = try runPackedTileOffsetsCpu(allocator, &count_graph, ranges, commands.len, shape.tiles_x, shape.tiles_y, offsets);
2618 
2619     const expected_indices = try allocator.alloc(u32, total);
2620     defer allocator.free(expected_indices);
2621     const expected_cursors = try allocator.alloc(u32, shape.tile_count);
2622     defer allocator.free(expected_cursors);
2623     fillBinIndices(commands[0..], ranges, shape, offsets, expected_cursors, expected_indices);
2624 
2625     const actual_indices = try allocator.alloc(u32, total);
2626     defer allocator.free(actual_indices);
2627     @memset(actual_indices, 0xffff_ffff);
2628     const actual_cursors = try allocator.alloc(u32, shape.tile_count);
2629     defer allocator.free(actual_cursors);
2630     @memset(actual_cursors, 0xffff_ffff);
2631 
2632     var index_graph = try buildTileIndexGraph(allocator, default_threads);
2633     defer index_graph.deinit();
2634     try runPackedTileIndicesCpu(allocator, &index_graph, ranges, encoded.words, commands.len, shape.tiles_x, shape.tiles_y, offsets, actual_cursors, actual_indices);
2635 
2636     try std.testing.expectEqualSlices(u32, expected_indices, actual_indices);
2637     var tile: usize = 0;
2638     while (tile < shape.tile_count) : (tile += 1) {
2639         try std.testing.expectEqual(offsets[tile + 1] - offsets[tile], actual_cursors[tile]);
2640     }
2641 }
2642 
2643 test "Accy tile sort graph orders scattered CSR command lists" {
2644     const allocator = std.testing.allocator;
2645     const commands = [_]Command{
2646         .{
2647             .kind = .fill,
2648             .rect = .{ .x = 5, .y = 4, .width = 20, .height = 18 },
2649             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 48 },
2650             .color = .{ .r = 200, .g = 0, .b = 0, .a = 255 },
2651             .order = 2,
2652         },
2653         .{
2654             .kind = .shadow,
2655             .rect = .{ .x = 34, .y = 26, .width = 8, .height = 7 },
2656             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 48 },
2657             .color = .{ .r = 0, .g = 0, .b = 0, .a = 120 },
2658             .width = 6,
2659             .order = 0,
2660         },
2661         .{
2662             .kind = .image,
2663             .rect = .{ .x = 0, .y = 0, .width = 2, .height = 2 },
2664             .clip = .{ .x = 50, .y = 0, .width = 4, .height = 4 },
2665             .color = .{ .r = 255, .g = 255, .b = 255, .a = 255 },
2666             .order = 1,
2667         },
2668         .{
2669             .kind = .stroke,
2670             .rect = .{ .x = 18, .y = 7, .width = 19, .height = 4 },
2671             .clip = .{ .x = 16, .y = 0, .width = 12, .height = 40 },
2672             .color = .{ .r = 0, .g = 120, .b = 200, .a = 255 },
2673             .width = 2,
2674             .order = 3,
2675         },
2676         .{
2677             .kind = .fill,
2678             .rect = .{ .x = 20, .y = 8, .width = 5, .height = 5 },
2679             .clip = .{ .x = 0, .y = 0, .width = 64, .height = 48 },
2680             .color = .{ .r = 60, .g = 40, .b = 20, .a = 255 },
2681             .order = 1,
2682         },
2683     };
2684     const width: u32 = 64;
2685     const height: u32 = 48;
2686     const region = Region{ .x = 4, .y = 3, .width = 40, .height = 32 };
2687     const shape = binShape(width, height, region);
2688 
2689     var encoded = try packCommandsAlloc(allocator, commands[0..]);
2690     defer encoded.deinit(allocator);
2691 
2692     const ranges = try allocator.alloc(u32, tileRangeValueCount(commands.len));
2693     defer allocator.free(ranges);
2694     fillCommandTileRanges(commands[0..], width, height, shape, ranges);
2695 
2696     const offsets = try allocator.alloc(u32, shape.tile_count + 1);
2697     defer allocator.free(offsets);
2698     var count_graph = try buildTileCountGraph(allocator, default_threads);
2699     defer count_graph.deinit();
2700     const total = try runPackedTileOffsetsCpu(allocator, &count_graph, ranges, commands.len, shape.tiles_x, shape.tiles_y, offsets);
2701 
2702     const expected_indices = try allocator.alloc(u32, total);
2703     defer allocator.free(expected_indices);
2704     const expected_cursors = try allocator.alloc(u32, shape.tile_count);
2705     defer allocator.free(expected_cursors);
2706     fillBinIndices(commands[0..], ranges, shape, offsets, expected_cursors, expected_indices);
2707 
2708     const actual_indices = try allocator.alloc(u32, total);
2709     defer allocator.free(actual_indices);
2710     @memset(actual_indices, 0xffff_ffff);
2711     const actual_cursors = try allocator.alloc(u32, shape.tile_count);
2712     defer allocator.free(actual_cursors);
2713     @memset(actual_cursors, 0xffff_ffff);
2714     fillBinIndicesUnsorted(commands[0..], ranges, shape, offsets, actual_cursors, actual_indices);
2715 
2716     var sort_graph = try buildTileSortGraph(allocator, default_threads);
2717     defer sort_graph.deinit();
2718     try runPackedTileSortCpu(allocator, &sort_graph, encoded.words, commands.len, offsets, actual_indices, shape.tile_count);
2719 
2720     try std.testing.expectEqualSlices(u32, expected_indices, actual_indices);
2721 }
2722 
2723 test "Accy compositor matches CPU packed rasterization" {
2724     const allocator = std.testing.allocator;
2725     const image_pixels = [_]u32{
2726         cpu.packRgba(.{ .r = 255, .g = 0, .b = 0, .a = 255 }),
2727         cpu.packRgba(.{ .r = 0, .g = 255, .b = 0, .a = 255 }),
2728         cpu.packRgba(.{ .r = 0, .g = 0, .b = 255, .a = 128 }),
2729         cpu.packRgba(.{ .r = 255, .g = 255, .b = 255, .a = 255 }),
2730     };
2731     const images = ImageSet{ .images = &.{.{ .width = 2, .height = 2, .pixels = image_pixels[0..] }} };
2732     const commands = [_]Command{
2733         .{
2734             .kind = .shadow,
2735             .rect = .{ .x = 1, .y = 1, .width = 5, .height = 3 },
2736             .clip = .{ .x = 0, .y = 0, .width = 8, .height = 5 },
2737             .color = .{ .r = 0, .g = 0, .b = 0, .a = 90 },
2738             .radius = 1,
2739             .width = 2,
2740         },
2741         .{
2742             .kind = .fill,
2743             .rect = .{ .x = 1, .y = 1, .width = 5, .height = 3 },
2744             .clip = .{ .x = 0, .y = 0, .width = 8, .height = 5 },
2745             .color = .{ .r = 220, .g = 40, .b = 20, .a = 255 },
2746         },
2747         .{
2748             .kind = .stroke,
2749             .rect = .{ .x = 2, .y = 1, .width = 5, .height = 4 },
2750             .clip = .{ .x = 0, .y = 0, .width = 8, .height = 5 },
2751             .color = .{ .r = 0, .g = 80, .b = 210, .a = 160 },
2752             .radius = 2,
2753             .width = 1,
2754         },
2755         .{
2756             .kind = .fill,
2757             .rect = .{ .x = 0.5, .y = 0.5, .width = 2.25, .height = 2.25 },
2758             .clip = .{ .x = 0, .y = 0, .width = 8, .height = 5 },
2759             .color = .{ .r = 40, .g = 200, .b = 90, .a = 220 },
2760         },
2761         .{
2762             .kind = .image,
2763             .rect = .{ .x = 5, .y = 1, .width = 2, .height = 2 },
2764             .clip = .{ .x = 0, .y = 0, .width = 8, .height = 5 },
2765             .source = .{ .x = 0, .y = 0, .width = 2, .height = 2 },
2766             .color = .{ .r = 255, .g = 255, .b = 255, .a = 255 },
2767             .image_index = 0,
2768         },
2769         .{
2770             .kind = .glyph,
2771             .rect = .{ .x = 6, .y = 3, .width = 1, .height = 1 },
2772             .clip = .{ .x = 0, .y = 0, .width = 8, .height = 5 },
2773             .source = .{ .x = 0, .y = 1, .width = 1, .height = 1 },
2774             .color = .{ .r = 230, .g = 20, .b = 40, .a = 200 },
2775             .image_index = 0,
2776         },
2777     };
2778     var expected = @as([(8 * 5)]u32, @splat(0));
2779     var actual = @as([(8 * 5)]u32, @splat(0));
2780 
2781     try cpu.renderCommandsPackedWithImages(commands[0..], .{ .width = 8, .height = 5, .pixels = expected[0..] }, .{ .a = 0 }, images);
2782     try renderCommandsCpuWithImages(allocator, commands[0..], 8, 5, actual[0..], .{ .a = 0 }, images);
2783 
2784     try std.testing.expectEqualSlices(u32, expected[0..], actual[0..]);
2785 }
2786 
2787 test "Accy compositor region preserves pixels outside damage" {
2788     const allocator = std.testing.allocator;
2789     const commands = [_]Command{.{
2790         .kind = .fill,
2791         .rect = .{ .x = 0, .y = 0, .width = 6, .height = 4 },
2792         .clip = .{ .x = 0, .y = 0, .width = 6, .height = 4 },
2793         .color = .{ .r = 80, .g = 90, .b = 100, .a = 255 },
2794     }};
2795     const region = Region{ .x = 2, .y = 1, .width = 2, .height = 2 };
2796     var expected = @as([(6 * 4)]u32, @splat(0x1122_3344));
2797     var actual = @as([(6 * 4)]u32, @splat(0x1122_3344));
2798 
2799     try cpu.renderCommandsPackedRegionWithImages(commands[0..], .{ .width = 6, .height = 4, .pixels = expected[0..] }, .{ .a = 0 }, .{}, region);
2800     try renderCommandsCpuRegionWithImages(allocator, commands[0..], 6, 4, actual[0..], .{ .a = 0 }, .{}, region);
2801 
2802     try std.testing.expectEqualSlices(u32, expected[0..], actual[0..]);
2803     try std.testing.expectEqual(@as(u32, 0x1122_3344), actual[0]);
2804 }
2805 
2806 const steady_frame_commands = [_]Command{
2807     .{
2808         .kind = .fill,
2809         .rect = .{ .x = 0, .y = 0, .width = 8, .height = 5 },
2810         .clip = .{ .x = 0, .y = 0, .width = 8, .height = 5 },
2811         .color = .{ .r = 18, .g = 22, .b = 28, .a = 255 },
2812     },
2813     .{
2814         .kind = .fill,
2815         .rect = .{ .x = 1, .y = 1, .width = 5, .height = 3 },
2816         .clip = .{ .x = 0, .y = 0, .width = 8, .height = 5 },
2817         .color = .{ .r = 220, .g = 40, .b = 20, .a = 255 },
2818         .radius = 1,
2819     },
2820     .{
2821         .kind = .stroke,
2822         .rect = .{ .x = 2, .y = 1, .width = 5, .height = 4 },
2823         .clip = .{ .x = 0, .y = 0, .width = 8, .height = 5 },
2824         .color = .{ .r = 0, .g = 80, .b = 210, .a = 160 },
2825         .radius = 2,
2826         .width = 1,
2827     },
2828 };
2829 
2830 test "Accy steady frames make no allocator calls at a fixed surface epoch" {
2831     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
2832     const allocator = failing.allocator();
2833     var graph = try buildGraph(allocator, default_threads);
2834     defer graph.deinit();
2835     var encoded = try packCommandsAlloc(allocator, steady_frame_commands[0..]);
2836     defer encoded.deinit(allocator);
2837     var encoded_images = try packImagesAlloc(allocator, .{});
2838     defer encoded_images.deinit(allocator);
2839     var bins = try binCommandsAlloc(allocator, steady_frame_commands[0..], 8, 5, Region.full(8, 5));
2840     defer bins.deinit(allocator);
2841 
2842     var expected = @as([(8 * 5)]u32, @splat(0));
2843     try cpu.renderCommandsPacked(steady_frame_commands[0..], .{ .width = 8, .height = 5, .pixels = expected[0..] }, .{ .a = 0 });
2844 
2845     var actual = @as([(8 * 5)]u32, @splat(0));
2846     try runPackedCommandsCpu(allocator, &graph, encoded, encoded_images, bins, 0, steady_frame_commands.len, 8, 5, actual[0..], .{ .a = 0 });
2847     try std.testing.expectEqualSlices(u32, expected[0..], actual[0..]);
2848 
2849     failing.fail_index = failing.alloc_index;
2850     failing.resize_fail_index = failing.resize_index;
2851     for (0..8) |_| {
2852         @memset(actual[0..], 0xffff_ffff);
2853         try runPackedCommandsCpu(allocator, &graph, encoded, encoded_images, bins, 0, steady_frame_commands.len, 8, 5, actual[0..], .{ .a = 0 });
2854         try std.testing.expectEqualSlices(u32, expected[0..], actual[0..]);
2855     }
2856     try std.testing.expect(!failing.has_induced_failure);
2857 }
2858 
2859 test "Accy surface epochs stay allocation-free after one full-block frame" {
2860     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
2861     const allocator = failing.allocator();
2862     var graph = try buildGraph(allocator, default_threads);
2863     defer graph.deinit();
2864     var encoded = try packCommandsAlloc(allocator, steady_frame_commands[0..]);
2865     defer encoded.deinit(allocator);
2866     var encoded_images = try packImagesAlloc(allocator, .{});
2867     defer encoded_images.deinit(allocator);
2868     var warm_bins = try binCommandsAlloc(allocator, steady_frame_commands[0..], 16, 8, Region.full(16, 8));
2869     defer warm_bins.deinit(allocator);
2870     var shrunk_bins = try binCommandsAlloc(allocator, steady_frame_commands[0..], 8, 5, Region.full(8, 5));
2871     defer shrunk_bins.deinit(allocator);
2872     var grown_bins = try binCommandsAlloc(allocator, steady_frame_commands[0..], 32, 20, Region.full(32, 20));
2873     defer grown_bins.deinit(allocator);
2874 
2875     comptime std.debug.assert(16 * 8 == default_threads);
2876     var warm_pixels = @as([(16 * 8)]u32, @splat(0));
2877     try runPackedCommandsCpu(allocator, &graph, encoded, encoded_images, warm_bins, 0, steady_frame_commands.len, 16, 8, warm_pixels[0..], .{ .a = 0 });
2878 
2879     var shrunk_expected = @as([(8 * 5)]u32, @splat(0));
2880     try cpu.renderCommandsPacked(steady_frame_commands[0..], .{ .width = 8, .height = 5, .pixels = shrunk_expected[0..] }, .{ .a = 0 });
2881     var grown_expected = @as([(32 * 20)]u32, @splat(0));
2882     try cpu.renderCommandsPacked(steady_frame_commands[0..], .{ .width = 32, .height = 20, .pixels = grown_expected[0..] }, .{ .a = 0 });
2883 
2884     failing.fail_index = failing.alloc_index;
2885     failing.resize_fail_index = failing.resize_index;
2886     var shrunk_pixels = @as([(8 * 5)]u32, @splat(0));
2887     try runPackedCommandsCpu(allocator, &graph, encoded, encoded_images, shrunk_bins, 0, steady_frame_commands.len, 8, 5, shrunk_pixels[0..], .{ .a = 0 });
2888     try std.testing.expectEqualSlices(u32, shrunk_expected[0..], shrunk_pixels[0..]);
2889 
2890     var grown_pixels = @as([(32 * 20)]u32, @splat(0));
2891     try runPackedCommandsCpu(allocator, &graph, encoded, encoded_images, grown_bins, 0, steady_frame_commands.len, 32, 20, grown_pixels[0..], .{ .a = 0 });
2892     try std.testing.expectEqualSlices(u32, grown_expected[0..], grown_pixels[0..]);
2893     try std.testing.expect(!failing.has_induced_failure);
2894 }
2895 
2896 test "Accy failed frame acquisition leaves the graph reusable" {
2897     var graph = try buildGraph(std.testing.allocator, default_threads);
2898     defer graph.deinit();
2899     var encoded = try packCommandsAlloc(std.testing.allocator, steady_frame_commands[0..]);
2900     defer encoded.deinit(std.testing.allocator);
2901     var encoded_images = try packImagesAlloc(std.testing.allocator, .{});
2902     defer encoded_images.deinit(std.testing.allocator);
2903     var bins = try binCommandsAlloc(std.testing.allocator, steady_frame_commands[0..], 8, 5, Region.full(8, 5));
2904     defer bins.deinit(std.testing.allocator);
2905 
2906     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
2907     var actual = @as([(8 * 5)]u32, @splat(0));
2908     try std.testing.expectError(error.OutOfMemory, runPackedCommandsCpu(failing.allocator(), &graph, encoded, encoded_images, bins, 0, steady_frame_commands.len, 8, 5, actual[0..], .{ .a = 0 }));
2909     try std.testing.expect(failing.has_induced_failure);
2910 
2911     failing.fail_index = std.math.maxInt(usize);
2912     try runPackedCommandsCpu(failing.allocator(), &graph, encoded, encoded_images, bins, 0, steady_frame_commands.len, 8, 5, actual[0..], .{ .a = 0 });
2913 
2914     var expected = @as([(8 * 5)]u32, @splat(0));
2915     try cpu.renderCommandsPacked(steady_frame_commands[0..], .{ .width = 8, .height = 5, .pixels = expected[0..] }, .{ .a = 0 });
2916     try std.testing.expectEqualSlices(u32, expected[0..], actual[0..]);
2917 }
2918 
2919 fn runPaintFrameLifecycle(allocator: Allocator, expected: *const [8 * 5]u32) !void {
2920     var graph = try buildGraph(std.testing.allocator, default_threads);
2921     defer graph.deinit();
2922     var encoded = try packCommandsAlloc(allocator, steady_frame_commands[0..]);
2923     defer encoded.deinit(allocator);
2924     var encoded_images = try packImagesAlloc(allocator, .{});
2925     defer encoded_images.deinit(allocator);
2926     var bins = try binCommandsAlloc(allocator, steady_frame_commands[0..], 8, 5, Region.full(8, 5));
2927     defer bins.deinit(allocator);
2928 
2929     var actual = @as([(8 * 5)]u32, @splat(0));
2930     try runPackedCommandsCpu(allocator, &graph, encoded, encoded_images, bins, 0, steady_frame_commands.len, 8, 5, actual[0..], .{ .a = 0 });
2931     if (!std.mem.eql(u32, expected[0..], actual[0..])) return error.TestUnexpectedResult;
2932 }
2933 
2934 test "Accy frame lifecycle releases every acquisition failure and retries" {
2935     var expected = @as([(8 * 5)]u32, @splat(0));
2936     try cpu.renderCommandsPacked(steady_frame_commands[0..], .{ .width = 8, .height = 5, .pixels = expected[0..] }, .{ .a = 0 });
2937 
2938     try runPaintFrameLifecycle(std.testing.allocator, &expected);
2939     try std.testing.checkAllAllocationFailures(std.testing.allocator, runPaintFrameLifecycle, .{&expected});
2940     try runPaintFrameLifecycle(std.testing.allocator, &expected);
2941 }
2942 
2943 test "Accy zero-size regions render without allocator use" {
2944     var graph = try buildGraph(std.testing.allocator, default_threads);
2945     defer graph.deinit();
2946     var encoded = try packCommandsAlloc(std.testing.allocator, steady_frame_commands[0..]);
2947     defer encoded.deinit(std.testing.allocator);
2948     var encoded_images = try packImagesAlloc(std.testing.allocator, .{});
2949     defer encoded_images.deinit(std.testing.allocator);
2950     var bins = try binCommandsAlloc(std.testing.allocator, steady_frame_commands[0..], 8, 5, Region.full(8, 5));
2951     defer bins.deinit(std.testing.allocator);
2952 
2953     var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 });
2954     var pixels = @as([(8 * 5)]u32, @splat(0xdead_beef));
2955     try runPackedCommandsCpuRegion(failing.allocator(), &graph, encoded, encoded_images, bins, 0, steady_frame_commands.len, 8, 5, pixels[0..], .{ .a = 0 }, .{ .x = 0, .y = 0, .width = 0, .height = 0 });
2956     try std.testing.expect(!failing.has_induced_failure);
2957     try std.testing.expectEqualSlices(u32, &(@as([(8 * 5)]u32, @splat(0xdead_beef))), pixels[0..]);
2958 }
2959 
2960 test "Accy compositor matches recorded frame rasterization" {
2961     const allocator = std.testing.allocator;
2962     const rows = [_]gui.model.UiNode{
2963         .{
2964             .widget_id = 4,
2965             .paint = .{
2966                 .background = .{ .r = 60, .g = 90, .b = 132, .a = 210 },
2967                 .border = .{ .r = 20, .g = 30, .b = 42, .a = 120 },
2968                 .border_width = 1,
2969                 .corner_roundness = 3,
2970             },
2971             .size = .{ .height = 8 },
2972         },
2973         .{
2974             .widget_id = 5,
2975             .paint = .{
2976                 .background = .{ .r = 112, .g = 78, .b = 132, .a = 190 },
2977                 .border = .{ .r = 30, .g = 24, .b = 44, .a = 100 },
2978                 .border_width = 1,
2979                 .corner_roundness = 3,
2980             },
2981             .size = .{ .height = 8 },
2982         },
2983     };
2984     const children = [_]gui.model.UiNode{
2985         .{
2986             .widget_id = 2,
2987             .paint = .{
2988                 .background = .{ .r = 34, .g = 40, .b = 54, .a = 240 },
2989                 .border = .{ .r = 90, .g = 120, .b = 150, .a = 160 },
2990                 .border_width = 1,
2991                 .corner_roundness = 4,
2992             },
2993             .size = .{ .height = 10 },
2994         },
2995         .{
2996             .widget_id = 3,
2997             .style = .{
2998                 .flex_direction = .column,
2999                 .gap = 2,
3000                 .padding = .{ .top = 2, .left = 2, .right = 2, .bottom = 2 },
3001                 .flex_grow = 1,
3002             },
3003             .paint = .{
3004                 .background = .{ .r = 225, .g = 230, .b = 236, .a = 240 },
3005                 .border = .{ .r = 40, .g = 50, .b = 62, .a = 100 },
3006                 .border_width = 1,
3007                 .corner_roundness = 5,
3008                 .shadow = .{
3009                     .color = .{ .r = 0, .g = 0, .b = 0, .a = 64 },
3010                     .offset_y = 1,
3011                     .blur_radius = 3,
3012                 },
3013             },
3014             .children = rows[0..],
3015         },
3016     };
3017     const surface = gui.model.UiSurfaceTree{
3018         .available_size = .{ .width = 48, .height = 40 },
3019         .root = .{
3020             .widget_id = 1,
3021             .style = .{
3022                 .flex_direction = .column,
3023                 .gap = 3,
3024                 .padding = .{ .top = 3, .left = 3, .right = 3, .bottom = 3 },
3025             },
3026             .paint = .{ .background = .{ .r = 16, .g = 18, .b = 22, .a = 255 } },
3027             .children = children[0..],
3028         },
3029     };
3030     var frame_workspace = gui.frame.Workspace.init(allocator);
3031     defer frame_workspace.deinit();
3032     const frame = try frame_workspace.buildSurface(&surface, .{});
3033     var commands = command.CommandBuffer.init(allocator);
3034     defer commands.deinit();
3035     try commands.appendFrame(frame, 1, 48, 40, .{});
3036     var expected = @as([(48 * 40)]u32, @splat(0));
3037     var actual = @as([(48 * 40)]u32, @splat(0));
3038 
3039     try cpu.renderCommandsPacked(commands.items(), .{ .width = 48, .height = 40, .pixels = expected[0..] }, .{ .a = 0 });
3040     try renderCommandsCpu(allocator, commands.items(), 48, 40, actual[0..], .{ .a = 0 });
3041 
3042     try std.testing.expect(commands.items().len > 0);
3043     try std.testing.expectEqualSlices(u32, expected[0..], actual[0..]);
3044 }
3045 
3046 test "Accy compositor creates recording artifacts for native GPU targets" {
3047     const allocator = std.testing.allocator;
3048     inline for (.{ gpu.ArtifactFormat.cuda_ptx, .vulkan_spirv, .metal_msl }) |format| {
3049         var artifact = try createRecordingArtifact(allocator, format);
3050         defer artifact.deinit();
3051         try std.testing.expectEqual(format, artifact.format);
3052         try std.testing.expectEqualStrings(kernel_name, artifact.entry_name);
3053         var range_artifact = try createTileRangeRecordingArtifact(allocator, format);
3054         defer range_artifact.deinit();
3055         try std.testing.expectEqual(format, range_artifact.format);
3056         try std.testing.expectEqualStrings(tile_range_kernel_name, range_artifact.entry_name);
3057         var count_artifact = try createTileCountRecordingArtifact(allocator, format);
3058         defer count_artifact.deinit();
3059         try std.testing.expectEqual(format, count_artifact.format);
3060         try std.testing.expectEqualStrings(tile_count_kernel_name, count_artifact.entry_name);
3061         var index_artifact = try createTileIndexRecordingArtifact(allocator, format);
3062         defer index_artifact.deinit();
3063         try std.testing.expectEqual(format, index_artifact.format);
3064         try std.testing.expectEqualStrings(tile_index_kernel_name, index_artifact.entry_name);
3065         var sort_artifact = try createTileSortRecordingArtifact(allocator, format);
3066         defer sort_artifact.deinit();
3067         try std.testing.expectEqual(format, sort_artifact.format);
3068         try std.testing.expectEqualStrings(tile_sort_kernel_name, sort_artifact.entry_name);
3069     }
3070 }