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 }