lib/gui/src/paint/executor.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const gpu = @import("gpu");
3 const choir_abi = @import("choir_abi");
4 const accy = @import("accy");
5 const alloc_phase = @import("alloc_phase");
6
7 const accy_paint = @import("accy.zig");
8 const command = @import("command.zig");
9 const cpu = @import("cpu/root.zig");
10 const gui = @import("../root.zig");
11 const paint_image = @import("image.zig");
12
13 const Allocator = std.mem.Allocator;
14 const Color = gui.model.UiColor;
15 const Command = command.Command;
16 const ImageSet = command.ImageSet;
17 const kernel = accy.kernel;
18 const scan_library = kernel.library.scan;
19 const host_loop_launch_shape_arg_count: usize = choir_abi.launch_shape_argument_count;
20
21 const PaintTarget = struct {
22 width: u32,
23 height: u32,
24 output_format: accy_paint.OutputFormat,
25 };
26
27 const LoadedKernel = struct {
28 artifact: gpu.KernelArtifact,
29 loaded: gpu.LoadedArtifact,
30
31 fn deinit(self: *LoadedKernel, handle: gpu.BackendHandle) void {
32 handle.destroyObject(self.loaded.id);
33 self.artifact.deinit();
34 self.* = undefined;
35 }
36 };
37
38 const DeviceScanKernels = struct {
39 instance: scan_library.DeviceScan,
40 stages: scan_library.DeviceScanStages,
41 block_scan: LoadedKernel,
42 sums_scan: LoadedKernel,
43 add_base: LoadedKernel,
44
45 fn deinit(self: *DeviceScanKernels, handle: gpu.BackendHandle) void {
46 self.add_base.deinit(handle);
47 self.sums_scan.deinit(handle);
48 self.block_scan.deinit(handle);
49 self.* = undefined;
50 }
51 };
52
53 pub const PreparedPackedLaunch = struct {
54 pixels: gpu.BufferHandle,
55 pixel_count: usize,
56 target_width: u32,
57 target_height: u32,
58 region_x: u32,
59 region_y: u32,
60 region_width: u32,
61 region_height: u32,
62 output_format: accy_paint.OutputFormat,
63 generation: u64,
64 loaded_artifact_id: gpu.BackendObjectId,
65 bindings: [7]gpu.BufferBinding,
66 scalar_storage: [11 + host_loop_launch_shape_arg_count]choir_abi.ScalarArgument,
67 scalar_count: usize,
68 geometry: choir_abi.LaunchGeometry,
69 command_visits: usize,
70 device_csr_prepared: bool,
71
72 fn request(
73 self: *const PreparedPackedLaunch,
74 artifact: *gpu.KernelArtifact,
75 loaded: gpu.LoadedArtifact,
76 ) gpu.LaunchRequest {
77 return .{
78 .artifact = artifact,
79 .loaded_artifact = loaded,
80 .buffers = self.bindings[0..],
81 .scalar_arguments = self.scalar_storage[0..self.scalar_count],
82 .geometry = self.geometry,
83 };
84 }
85 };
86
87 pub const PreparedLaunchTiming = struct {
88 elapsed_ns: u64,
89 stream_id: gpu.BackendObjectId,
90 };
91
92 pub const PreparedLaunchInfo = struct {
93 entry_name: []const u8,
94 artifact_format: gpu.ArtifactFormat,
95 artifact_payload_bytes: usize,
96 pixel_count: usize,
97 geometry: choir_abi.LaunchGeometry,
98 device_csr_prepared: bool,
99 };
100
101 pub const StorageMode = enum {
102 host_bins,
103 device_csr,
104 };
105
106 pub fn storageModeForArtifactFormat(format: gpu.ArtifactFormat) StorageMode {
107 return if (gpu.artifactFormatUsesHostLoopLaunch(format)) .host_bins else .device_csr;
108 }
109
110 const ExecutorLimits = struct {
111 mode: StorageMode,
112 commands: usize = 0,
113 pixels: usize = 0,
114 images: usize = 0,
115 image_pixels: usize = 0,
116 tiles: usize = 0,
117 tile_pairs: usize = 0,
118
119 pub fn worstCase(
120 mode: StorageMode,
121 commands: usize,
122 pixels: usize,
123 images: usize,
124 image_pixels: usize,
125 tiles: usize,
126 ) error{CapacityOverflow}!Limits {
127 return .{
128 .mode = mode,
129 .commands = commands,
130 .pixels = pixels,
131 .images = images,
132 .image_pixels = image_pixels,
133 .tiles = tiles,
134 .tile_pairs = std.math.mul(usize, commands, tiles) catch return error.CapacityOverflow,
135 };
136 }
137
138 fn merged(self: Limits, demand: Limits) error{StorageModeMismatch}!Limits {
139 if (self.mode != demand.mode) return error.StorageModeMismatch;
140 return .{
141 .mode = self.mode,
142 .commands = @max(self.commands, demand.commands),
143 .pixels = @max(self.pixels, demand.pixels),
144 .images = @max(self.images, demand.images),
145 .image_pixels = @max(self.image_pixels, demand.image_pixels),
146 .tiles = @max(self.tiles, demand.tiles),
147 .tile_pairs = @max(self.tile_pairs, demand.tile_pairs),
148 };
149 }
150 };
151 pub const Limits = ExecutorLimits;
152
153 const ExecutorCapacity = struct {
154 limits: Limits,
155 command_capacity: usize,
156 pixel_capacity: usize,
157 image_capacity: usize,
158 image_pixel_capacity: usize,
159 tile_range_capacity: usize,
160 tile_offset_capacity: usize,
161 tile_count_capacity: usize,
162 tile_index_capacity: usize,
163 float_count: usize,
164 word_count: usize,
165 image_metadata_count: usize,
166 host_range_count: usize,
167 host_offset_count: usize,
168 host_index_count: usize,
169 host_cursor_count: usize,
170 host_storage_bytes: usize,
171 device_storage_bytes: usize,
172 device_buffer_count: usize,
173 total_storage_bytes: usize,
174
175 pub fn derive(limits: Limits) error{CapacityOverflow}!Capacity {
176 const command_capacity = @max(limits.commands, 1);
177 const pixel_capacity = @max(limits.pixels, 1);
178 const image_capacity = @max(limits.images, 1);
179 const image_pixel_capacity = @max(limits.image_pixels, 1);
180 const tile_range_capacity = @max(
181 std.math.mul(usize, limits.commands, accy_paint.tile_range_lanes) catch return error.CapacityOverflow,
182 1,
183 );
184 const tile_offset_capacity = @max(
185 std.math.add(usize, limits.tiles, 1) catch return error.CapacityOverflow,
186 2,
187 );
188 const tile_count_capacity = @max(limits.tiles, 1);
189 const tile_index_capacity = @max(limits.tile_pairs, 1);
190 const float_count = std.math.mul(usize, command_capacity, accy_paint.float_lanes) catch return error.CapacityOverflow;
191 const word_count = std.math.mul(usize, command_capacity, accy_paint.word_lanes) catch return error.CapacityOverflow;
192 const image_metadata_count = std.math.mul(usize, image_capacity, accy_paint.image_lanes) catch return error.CapacityOverflow;
193 const host_range_count = if (limits.mode == .host_bins) tile_range_capacity else 0;
194 const host_offset_count = if (limits.mode == .host_bins) tile_offset_capacity else 0;
195 const host_index_count = if (limits.mode == .host_bins) tile_index_capacity else 0;
196 const host_cursor_count = if (limits.mode == .host_bins) tile_count_capacity else 0;
197 const host_element_count = try sumChecked(&.{
198 float_count,
199 word_count,
200 image_metadata_count,
201 image_pixel_capacity,
202 pixel_capacity,
203 tile_offset_capacity,
204 host_range_count,
205 host_offset_count,
206 host_index_count,
207 host_cursor_count,
208 });
209 const host_storage_bytes = std.math.mul(usize, host_element_count, @sizeOf(u32)) catch return error.CapacityOverflow;
210 const common_device_elements = try sumChecked(&.{
211 pixel_capacity,
212 float_count,
213 word_count,
214 image_metadata_count,
215 image_pixel_capacity,
216 tile_offset_capacity,
217 tile_index_capacity,
218 });
219 const device_csr_elements = if (limits.mode == .device_csr)
220 try sumChecked(&.{
221 tile_range_capacity,
222 tile_offset_capacity,
223 tile_count_capacity,
224 scan_library.device_scan_max_blocks,
225 scan_library.device_scan_max_blocks,
226 })
227 else
228 0;
229 const device_elements = std.math.add(usize, common_device_elements, device_csr_elements) catch return error.CapacityOverflow;
230 const device_storage_bytes = std.math.mul(usize, device_elements, @sizeOf(u32)) catch return error.CapacityOverflow;
231 return .{
232 .limits = limits,
233 .command_capacity = command_capacity,
234 .pixel_capacity = pixel_capacity,
235 .image_capacity = image_capacity,
236 .image_pixel_capacity = image_pixel_capacity,
237 .tile_range_capacity = tile_range_capacity,
238 .tile_offset_capacity = tile_offset_capacity,
239 .tile_count_capacity = tile_count_capacity,
240 .tile_index_capacity = tile_index_capacity,
241 .float_count = float_count,
242 .word_count = word_count,
243 .image_metadata_count = image_metadata_count,
244 .host_range_count = host_range_count,
245 .host_offset_count = host_offset_count,
246 .host_index_count = host_index_count,
247 .host_cursor_count = host_cursor_count,
248 .host_storage_bytes = host_storage_bytes,
249 .device_storage_bytes = device_storage_bytes,
250 .device_buffer_count = if (limits.mode == .device_csr) 12 else 7,
251 .total_storage_bytes = std.math.add(usize, host_storage_bytes, device_storage_bytes) catch return error.CapacityOverflow,
252 };
253 }
254
255 pub fn admits(self: Capacity, demand: Limits) bool {
256 return self.limits.mode == demand.mode and
257 self.limits.commands >= demand.commands and
258 self.limits.pixels >= demand.pixels and
259 self.limits.images >= demand.images and
260 self.limits.image_pixels >= demand.image_pixels and
261 self.limits.tiles >= demand.tiles and
262 self.limits.tile_pairs >= demand.tile_pairs;
263 }
264 };
265 pub const Capacity = ExecutorCapacity;
266
267 pub const StorageStatus = struct {
268 limits: ?Limits,
269 capacity: ?Capacity,
270 replacements: usize,
271 image_processor: ?paint_image.StorageStatus,
272 };
273
274 fn sumChecked(values: []const usize) error{CapacityOverflow}!usize {
275 var total: usize = 0;
276 for (values) |value| {
277 total = std.math.add(usize, total, value) catch return error.CapacityOverflow;
278 }
279 return total;
280 }
281
282 pub const Options = struct {
283 artifact_format: ?gpu.ArtifactFormat = null,
284 threads: u32 = accy_paint.default_threads,
285 initial_storage: ?Limits = null,
286 image_storage: ?paint_image.Limits = null,
287 };
288
289 pub const Executor = struct {
290 allocator: Allocator,
291 handle: gpu.BackendHandle,
292 artifact: gpu.KernelArtifact,
293 loaded: gpu.LoadedArtifact,
294 tile_range: ?LoadedKernel = null,
295 tile_count: ?LoadedKernel = null,
296 tile_index: ?LoadedKernel = null,
297 tile_scan: ?DeviceScanKernels = null,
298 threads: u32,
299 buffers: ?Buffers = null,
300 image_processor: ?paint_image.Processor = null,
301 shadow_expansion: ?CachedShadowExpansion = null,
302 storage_replacements: usize = 0,
303 prepared_generation: u64 = 0,
304
305 pub fn init(allocator: Allocator, handle: gpu.BackendHandle, options: Options) !Executor {
306 const format = options.artifact_format orelse try defaultFormat(handle);
307 const threads = @max(options.threads, 1);
308 var graph = try accy_paint.buildGraph(allocator, threads);
309 defer graph.deinit();
310
311 var artifact = try kernel.createKernelArtifact(allocator, handle, &graph, .{
312 .artifact_format = format,
313 .authored_kernel_diagnostic_id = "gui/paint/executor/rgba8-packed",
314 });
315 errdefer artifact.deinit();
316
317 const loaded = try handle.loadArtifact(&artifact);
318 errdefer handle.destroyObject(loaded.id);
319
320 var tile_range: ?LoadedKernel = null;
321 errdefer if (tile_range) |*value| value.deinit(handle);
322 var tile_count: ?LoadedKernel = null;
323 errdefer if (tile_count) |*value| value.deinit(handle);
324 var tile_index: ?LoadedKernel = null;
325 errdefer if (tile_index) |*value| value.deinit(handle);
326 var tile_scan: ?DeviceScanKernels = null;
327 errdefer if (tile_scan) |*value| value.deinit(handle);
328
329 if (!gpu.artifactFormatUsesHostLoopLaunch(format)) {
330 tile_range = try loadGraphKernel(allocator, handle, format, threads, accy_paint.buildTileRangeGraph, "gui/paint/executor/tile-ranges");
331 tile_count = try loadGraphKernel(allocator, handle, format, threads, accy_paint.buildTileCountGraph, "gui/paint/executor/tile-counts");
332 tile_index = try loadGraphKernel(allocator, handle, format, threads, accy_paint.buildTileIndexGraph, "gui/paint/executor/tile-indices");
333 tile_scan = try loadDeviceScanKernels(allocator, handle, format);
334 }
335
336 var result = Executor{
337 .allocator = allocator,
338 .handle = handle,
339 .artifact = artifact,
340 .loaded = loaded,
341 .tile_range = tile_range,
342 .tile_count = tile_count,
343 .tile_index = tile_index,
344 .tile_scan = tile_scan,
345 .threads = threads,
346 };
347 if (options.initial_storage) |limits| {
348 if (limits.mode != storageModeForArtifactFormat(format)) return error.StorageModeMismatch;
349 result.buffers = try Buffers.init(allocator, handle, limits);
350 }
351 errdefer result.releaseBuffers();
352 if (options.image_storage) |limits| {
353 result.image_processor = try paint_image.Processor.init(allocator, handle, .{
354 .artifact_format = format,
355 .initial_storage = limits,
356 });
357 }
358 return result;
359 }
360
361 pub fn deinit(self: *Executor) void {
362 if (self.shadow_expansion) |*cached| cached.deinit(self.allocator);
363 if (self.image_processor) |*processor| processor.deinit();
364 self.releaseBuffers();
365 if (self.tile_scan) |*value| value.deinit(self.handle);
366 if (self.tile_index) |*value| value.deinit(self.handle);
367 if (self.tile_count) |*value| value.deinit(self.handle);
368 if (self.tile_range) |*value| value.deinit(self.handle);
369 self.handle.destroyObject(self.loaded.id);
370 self.artifact.deinit();
371 self.* = undefined;
372 }
373
374 pub fn storageStatus(self: *const Executor) StorageStatus {
375 return .{
376 .limits = if (self.buffers) |buffers| buffers.capacity.limits else null,
377 .capacity = if (self.buffers) |buffers| buffers.capacity else null,
378 .replacements = self.storage_replacements,
379 .image_processor = if (self.image_processor) |*processor| processor.storageStatus() else null,
380 };
381 }
382
383 pub fn renderCommands(self: *Executor, commands: []const Command, target: cpu.Target, clear: Color) !void {
384 try self.renderCommandsWithImages(commands, target, clear, .{});
385 }
386
387 pub fn renderCommandsWithImageShadows(
388 self: *Executor,
389 commands: []const Command,
390 target: cpu.Target,
391 clear: Color,
392 images: ImageSet,
393 shadow_options: paint_image.ShadowExpansionOptions,
394 ) !void {
395 const expansion = try self.cachedImageShadows(commands, images, shadow_options);
396 try self.renderCommandsWithImages(expansion.commands, target, clear, expansion.imageSet());
397 }
398
399 pub fn renderCommandsWithImages(self: *Executor, commands: []const Command, target: cpu.Target, clear: Color, images: ImageSet) !void {
400 try target.validate();
401 const pixel_count = try pixelCount(target.width, target.height);
402 if (pixel_count == 0) return;
403 const pixels = try self.renderPacked(commands, target.width, target.height, clear, images, cpu.Region.full(target.width, target.height));
404 try cpu.rgba8FromPacked(target.rgba8, pixels);
405 }
406
407 pub fn renderCommandsRegionWithImageShadows(
408 self: *Executor,
409 commands: []const Command,
410 target: cpu.Target,
411 clear: Color,
412 images: ImageSet,
413 region: cpu.Region,
414 shadow_options: paint_image.ShadowExpansionOptions,
415 ) !void {
416 const expansion = try self.cachedImageShadows(commands, images, shadow_options);
417 try self.renderCommandsRegionWithImages(expansion.commands, target, clear, expansion.imageSet(), region);
418 }
419
420 pub fn renderCommandsRegionWithImages(self: *Executor, commands: []const Command, target: cpu.Target, clear: Color, images: ImageSet, region: cpu.Region) !void {
421 try target.validate();
422 const pixel_count = try pixelCount(target.width, target.height);
423 if (pixel_count == 0) return;
424 const active_region = region.clamped(target.width, target.height);
425 if (active_region.pixelCount() == 0) return;
426 const pixels = try self.renderPacked(commands, target.width, target.height, clear, images, active_region);
427 try copyRgba8Region(target.rgba8, target.width, active_region, pixels);
428 }
429
430 pub fn renderCommandsPacked(self: *Executor, commands: []const Command, target: cpu.PackedTarget, clear: Color) !void {
431 try self.renderCommandsPackedWithImages(commands, target, clear, .{});
432 }
433
434 pub fn renderCommandsPackedWithImageShadows(
435 self: *Executor,
436 commands: []const Command,
437 target: cpu.PackedTarget,
438 clear: Color,
439 images: ImageSet,
440 shadow_options: paint_image.ShadowExpansionOptions,
441 ) !void {
442 const expansion = try self.cachedImageShadows(commands, images, shadow_options);
443 try self.renderCommandsPackedWithImages(expansion.commands, target, clear, expansion.imageSet());
444 }
445
446 pub fn renderCommandsPackedWithImages(self: *Executor, commands: []const Command, target: cpu.PackedTarget, clear: Color, images: ImageSet) !void {
447 try target.validate();
448 const pixel_count = try pixelCount(target.width, target.height);
449 if (pixel_count == 0) return;
450 const pixels = try self.renderPacked(commands, target.width, target.height, clear, images, cpu.Region.full(target.width, target.height));
451 @memcpy(target.pixels[0..pixel_count], pixels);
452 }
453
454 pub fn renderCommandsSurfaceFrame(self: *Executor, commands: []const Command, surface_frame: gpu.SurfaceFrame, clear: Color) !void {
455 try self.renderCommandsSurfaceFrameWithImages(commands, surface_frame, clear, .{});
456 }
457
458 pub fn renderCommandsSurfaceFrameWithImageShadows(
459 self: *Executor,
460 commands: []const Command,
461 surface_frame: gpu.SurfaceFrame,
462 clear: Color,
463 images: ImageSet,
464 shadow_options: paint_image.ShadowExpansionOptions,
465 ) !void {
466 var prepared = (try self.prepareCommandsSurfaceLaunchWithImageShadows(commands, surface_frame.surface, clear, images, shadow_options)) orelse return;
467 try self.submitPreparedLaunchQueued(&prepared);
468 try self.writePreparedSurfaceFrame(surface_frame, &prepared);
469 }
470
471 pub fn renderCommandsSurfaceFrameWithImages(self: *Executor, commands: []const Command, surface_frame: gpu.SurfaceFrame, clear: Color, images: ImageSet) !void {
472 const target = try surfaceFramePaintTarget(surface_frame);
473 var prepared = (try self.prepareCommandsFormattedLaunch(commands, target.width, target.height, clear, images, cpu.Region.full(target.width, target.height), target.output_format)) orelse return;
474 try self.submitPreparedLaunchQueued(&prepared);
475 try self.writePreparedSurfaceFrame(surface_frame, &prepared);
476 }
477
478 pub fn renderCommandsPackedRegionWithImageShadows(
479 self: *Executor,
480 commands: []const Command,
481 target: cpu.PackedTarget,
482 clear: Color,
483 images: ImageSet,
484 region: cpu.Region,
485 shadow_options: paint_image.ShadowExpansionOptions,
486 ) !void {
487 const expansion = try self.cachedImageShadows(commands, images, shadow_options);
488 try self.renderCommandsPackedRegionWithImages(expansion.commands, target, clear, expansion.imageSet(), region);
489 }
490
491 pub fn renderCommandsPackedRegionWithImages(self: *Executor, commands: []const Command, target: cpu.PackedTarget, clear: Color, images: ImageSet, region: cpu.Region) !void {
492 try target.validate();
493 const pixel_count = try pixelCount(target.width, target.height);
494 if (pixel_count == 0) return;
495 const active_region = region.clamped(target.width, target.height);
496 if (active_region.pixelCount() == 0) return;
497 const pixels = try self.renderPacked(commands, target.width, target.height, clear, images, active_region);
498 copyPackedRegion(target.pixels, target.width, active_region, pixels);
499 }
500
501 pub fn expandImageShadowsAlloc(
502 self: *Executor,
503 commands: []const Command,
504 images: ImageSet,
505 options: paint_image.ShadowExpansionOptions,
506 ) !paint_image.ShadowExpansion {
507 const processor = try self.imageProcessor();
508 return processor.expandShadowsAlloc(commands, images, options);
509 }
510
511 fn cachedImageShadows(
512 self: *Executor,
513 commands: []const Command,
514 images: ImageSet,
515 options: paint_image.ShadowExpansionOptions,
516 ) !*const paint_image.ShadowExpansion {
517 if (self.shadow_expansion) |*cached| {
518 if (try cached.matches(commands, images, options)) return &cached.expansion;
519 if (try cached.refreshReusable(commands, images, options)) return &cached.expansion;
520 }
521
522 const processor = try self.imageProcessor();
523 var next = try CachedShadowExpansion.init(self.allocator, processor, commands, images, options);
524 errdefer next.deinit(self.allocator);
525 if (self.shadow_expansion) |*cached| {
526 cached.deinit(self.allocator);
527 self.shadow_expansion = null;
528 }
529 self.shadow_expansion = next;
530 if (self.shadow_expansion) |*cached| return &cached.expansion;
531 unreachable;
532 }
533
534 fn imageProcessor(self: *Executor) !*paint_image.Processor {
535 if (self.image_processor) |*processor| return processor;
536 self.image_processor = try paint_image.Processor.init(self.allocator, self.handle, .{
537 .artifact_format = self.artifact.format,
538 });
539 if (self.image_processor) |*processor| return processor;
540 unreachable;
541 }
542
543 fn renderPacked(self: *Executor, commands: []const Command, width: u32, height: u32, clear: Color, images: ImageSet, region: cpu.Region) ![]const u32 {
544 var prepared = (try self.prepareCommandsPackedLaunch(commands, width, height, clear, images, region)) orelse return &.{};
545 try self.submitPreparedLaunch(&prepared);
546 return self.readPreparedPackedPixels(&prepared);
547 }
548
549 pub fn prepareCommandsPackedLaunch(self: *Executor, commands: []const Command, width: u32, height: u32, clear: Color, images: ImageSet, region: cpu.Region) !?PreparedPackedLaunch {
550 return self.prepareCommandsFormattedLaunch(commands, width, height, clear, images, region, .rgba);
551 }
552
553 pub fn prepareCommandsPackedLaunchWithImageShadows(
554 self: *Executor,
555 commands: []const Command,
556 width: u32,
557 height: u32,
558 clear: Color,
559 images: ImageSet,
560 region: cpu.Region,
561 shadow_options: paint_image.ShadowExpansionOptions,
562 ) !?PreparedPackedLaunch {
563 const expansion = try self.cachedImageShadows(commands, images, shadow_options);
564 return self.prepareCommandsPackedLaunch(expansion.commands, width, height, clear, expansion.imageSet(), region);
565 }
566
567 pub fn prepareCommandsSurfaceLaunch(self: *Executor, commands: []const Command, surface: gpu.SurfaceHandle, clear: Color, images: ImageSet) !?PreparedPackedLaunch {
568 const target = try surfacePaintTarget(surface);
569 return self.prepareCommandsFormattedLaunch(commands, target.width, target.height, clear, images, cpu.Region.full(target.width, target.height), target.output_format);
570 }
571
572 pub fn prepareCommandsSurfaceLaunchWithImageShadows(
573 self: *Executor,
574 commands: []const Command,
575 surface: gpu.SurfaceHandle,
576 clear: Color,
577 images: ImageSet,
578 shadow_options: paint_image.ShadowExpansionOptions,
579 ) !?PreparedPackedLaunch {
580 const target = try surfacePaintTarget(surface);
581 const expansion = try self.cachedImageShadows(commands, images, shadow_options);
582 return self.prepareCommandsFormattedLaunch(expansion.commands, target.width, target.height, clear, expansion.imageSet(), cpu.Region.full(target.width, target.height), target.output_format);
583 }
584
585 fn preparedLaunchRequest(self: *Executor, prepared: *const PreparedPackedLaunch) !gpu.LaunchRequest {
586 try self.validatePreparedLaunch(prepared);
587 return prepared.request(&self.artifact, self.loaded);
588 }
589
590 pub fn submitPreparedLaunchQueuedWithEvents(
591 self: *Executor,
592 prepared: *const PreparedPackedLaunch,
593 wait_events: []const gpu.EventHandle,
594 signal_event: ?gpu.EventHandle,
595 ) !void {
596 var request = try self.preparedLaunchRequest(prepared);
597 request.wait_events = wait_events;
598 request.signal_event = signal_event;
599 try self.handle.launch(request);
600 }
601
602 pub fn submitPreparedLaunchQueued(self: *Executor, prepared: *const PreparedPackedLaunch) !void {
603 try self.submitPreparedLaunchQueuedWithEvents(prepared, &.{}, null);
604 }
605
606 pub fn submitPreparedLaunchTimed(self: *Executor, prepared: *const PreparedPackedLaunch) !PreparedLaunchTiming {
607 try self.validatePreparedLaunch(prepared);
608 const stream = try self.handle.createStream(.{});
609 defer self.handle.destroyObject(stream.id);
610 const start = try self.handle.createEvent(.{});
611 defer self.handle.destroyObject(start.id);
612 const end = try self.handle.createEvent(.{});
613 defer self.handle.destroyObject(end.id);
614
615 try self.handle.recordEvent(.{
616 .stream = stream,
617 .event = start,
618 });
619 var request = prepared.request(&self.artifact, self.loaded);
620 request.stream = stream;
621 try self.handle.launch(request);
622 try self.handle.recordEvent(.{
623 .stream = stream,
624 .event = end,
625 });
626 try self.handle.synchronize(.{
627 .scope = .event,
628 .event = end,
629 });
630 const elapsed_ns = try self.handle.elapsedEventNs(.{
631 .start = start,
632 .end = end,
633 });
634 return .{
635 .elapsed_ns = elapsed_ns,
636 .stream_id = stream.id,
637 };
638 }
639
640 pub fn preparedLaunchInfo(self: *const Executor, prepared: *const PreparedPackedLaunch) !PreparedLaunchInfo {
641 try self.validatePreparedLaunch(prepared);
642 return .{
643 .entry_name = self.artifact.entry_name,
644 .artifact_format = self.artifact.format,
645 .artifact_payload_bytes = artifactPayloadBytes(&self.artifact),
646 .pixel_count = prepared.pixel_count,
647 .geometry = prepared.geometry,
648 .device_csr_prepared = prepared.device_csr_prepared,
649 };
650 }
651
652 pub fn submitPreparedLaunch(self: *Executor, prepared: *const PreparedPackedLaunch) !void {
653 try self.submitPreparedLaunchQueued(prepared);
654 try self.handle.synchronize(.{ .scope = .device });
655 }
656
657 pub fn readPreparedPackedPixels(self: *Executor, prepared: *const PreparedPackedLaunch) ![]const u32 {
658 try self.validatePreparedLaunch(prepared);
659 const buffers = if (self.buffers) |*value| value else unreachable;
660 try self.handle.readBuffer(.{
661 .handle = prepared.pixels,
662 .bytes = std.mem.sliceAsBytes(buffers.readback[0..buffers.pixel_capacity]),
663 });
664 return buffers.readback[0..prepared.pixel_count];
665 }
666
667 pub fn writePreparedSurfaceFrameWithEvents(
668 self: *Executor,
669 surface_frame: gpu.SurfaceFrame,
670 prepared: *const PreparedPackedLaunch,
671 wait_events: []const gpu.EventHandle,
672 signal_event: ?gpu.EventHandle,
673 ) !void {
674 try self.validatePreparedLaunch(prepared);
675 const target = try surfaceFramePaintTarget(surface_frame);
676 try validatePreparedSurfaceTarget(prepared, target);
677 const operations = [_]gpu.SurfaceFrameWriteOp{.{ .copy_buffer = prepared.pixels }};
678 try self.handle.writeSurfaceFrame(.{
679 .surface = surface_frame.surface,
680 .frame = surface_frame,
681 .operations = operations[0..],
682 .wait_events = wait_events,
683 .signal_event = signal_event,
684 });
685 }
686
687 pub fn writePreparedSurfaceFrame(self: *Executor, surface_frame: gpu.SurfaceFrame, prepared: *const PreparedPackedLaunch) !void {
688 try self.writePreparedSurfaceFrameWithEvents(surface_frame, prepared, &.{}, null);
689 }
690
691 fn prepareCommandsFormattedLaunch(
692 self: *Executor,
693 commands: []const Command,
694 width: u32,
695 height: u32,
696 clear: Color,
697 images: ImageSet,
698 region: cpu.Region,
699 output_format: accy_paint.OutputFormat,
700 ) !?PreparedPackedLaunch {
701 const target_pixel_count = try pixelCount(width, height);
702 if (target_pixel_count == 0) return null;
703 const active_region = region.clamped(width, height);
704 const region_pixel_count = active_region.pixelCount();
705 if (region_pixel_count == 0) return null;
706 const command_count_u32 = std.math.cast(u32, commands.len) orelse return error.CommandCountTooLarge;
707 const image_count = std.math.cast(u32, images.images.len) orelse return error.ImageCountTooLarge;
708 const region_pixel_count_u32 = std.math.cast(u32, region_pixel_count) orelse return error.DimensionsTooLarge;
709 const image_pixels = try accy_paint.imagePixelCount(images);
710 const shape = accy_paint.binShape(width, height, active_region);
711 const tile_offset_count = std.math.add(usize, shape.tile_count, 1) catch return error.BufferTooLarge;
712 const tile_pair_capacity = std.math.mul(usize, commands.len, shape.tile_count) catch return error.BufferTooLarge;
713 _ = std.math.cast(u32, tile_pair_capacity) orelse return error.BufferTooLarge;
714 const use_device_csr = !gpu.artifactFormatUsesHostLoopLaunch(self.artifact.format);
715 const storage_mode: StorageMode = if (use_device_csr) .device_csr else .host_bins;
716 if (use_device_csr and tile_offset_count > scan_library.deviceScanMaxExtent(self.tile_scan.?.instance)) return error.UnsupportedDeviceScanInstance;
717 var host_bins: accy_paint.BinView = undefined;
718 var reuse_host_tile_csr = false;
719 var host_tile_index_count: usize = 0;
720 if (!use_device_csr) {
721 if (self.buffers) |*existing| {
722 const resident_demand = Limits{
723 .mode = storage_mode,
724 .commands = commands.len,
725 .pixels = region_pixel_count,
726 .images = images.images.len,
727 .image_pixels = image_pixels,
728 .tiles = shape.tile_count,
729 .tile_pairs = existing.tile_csr.tile_index_count,
730 };
731 if (existing.commandsResident(commands) and
732 existing.tile_csr.matches(commands.len, width, height, active_region, shape) and
733 existing.capacity.admits(resident_demand))
734 {
735 reuse_host_tile_csr = true;
736 host_tile_index_count = existing.tile_csr.tile_index_count;
737 }
738 }
739 if (!reuse_host_tile_csr) {
740 host_tile_index_count = try accy_paint.binPairCount(commands, width, height, active_region);
741 }
742 }
743 const tile_index_count = if (use_device_csr) tile_pair_capacity else host_tile_index_count;
744 const demand = Limits{
745 .mode = storage_mode,
746 .commands = commands.len,
747 .pixels = region_pixel_count,
748 .images = images.images.len,
749 .image_pixels = image_pixels,
750 .tiles = shape.tile_count,
751 .tile_pairs = tile_index_count,
752 };
753 const capacity_changed = try self.ensureCapacity(demand);
754 if (reuse_host_tile_csr) std.debug.assert(!capacity_changed);
755 const buffers = if (self.buffers) |*value| value else unreachable;
756 if (!use_device_csr and !reuse_host_tile_csr) {
757 host_bins = try buffers.host_bins.binCommands(commands, width, height, active_region);
758 std.debug.assert(host_bins.indices.len == host_tile_index_count);
759 }
760 const upload_commands = if (reuse_host_tile_csr) false else buffers.prepareCommandUpload(commands);
761 const image_upload = try buffers.prepareImageUpload(images, image_pixels);
762 const reuse_tile_csr = !upload_commands and buffers.tile_csr.matches(commands.len, width, height, active_region, shape);
763 const resident_changed = capacity_changed or
764 upload_commands or
765 image_upload != .none or
766 !reuse_tile_csr;
767 const generation = if (resident_changed) generation: {
768 try self.ensurePreparedGenerationAvailable();
769 break :generation self.advancePreparedGeneration();
770 } else self.prepared_generation;
771
772 if (commands.len != 0 and upload_commands) {
773 try self.handle.writeBuffer(.{
774 .handle = buffers.floats,
775 .bytes = std.mem.sliceAsBytes(buffers.staging.floats[0 .. commands.len * accy_paint.float_lanes]),
776 });
777 try self.handle.writeBuffer(.{
778 .handle = buffers.words,
779 .bytes = std.mem.sliceAsBytes(buffers.staging.words[0 .. commands.len * accy_paint.word_lanes]),
780 });
781 }
782 if (image_upload == .metadata_and_pixels) {
783 try self.handle.writeBuffer(.{
784 .handle = buffers.image_metadata,
785 .bytes = std.mem.sliceAsBytes(buffers.images.metadata[0 .. images.images.len * accy_paint.image_lanes]),
786 });
787 }
788 if (image_upload != .none) {
789 try self.handle.writeBuffer(.{
790 .handle = buffers.image_pixels,
791 .bytes = std.mem.sliceAsBytes(buffers.images.pixels[0..@max(image_pixels, 1)]),
792 });
793 buffers.markImageUpload(images.images.len, image_pixels);
794 }
795 var device_csr_prepared = false;
796 var command_visits: usize = 0;
797 if (use_device_csr) {
798 if (!reuse_tile_csr) {
799 try self.prepareTileCsr(commands.len, command_count_u32, width, height, active_region, shape, tile_offset_count);
800 buffers.markTileCsr(commands.len, width, height, active_region, shape, tile_pair_capacity, 0);
801 device_csr_prepared = true;
802 }
803 } else if (!reuse_tile_csr) {
804 try self.handle.writeBuffer(.{
805 .handle = buffers.tile_offsets,
806 .bytes = std.mem.sliceAsBytes(host_bins.offsets),
807 });
808 if (host_bins.indices.len != 0) {
809 try self.handle.writeBuffer(.{
810 .handle = buffers.tile_indices,
811 .bytes = std.mem.sliceAsBytes(host_bins.indices),
812 });
813 }
814 command_visits = accy_paint.commandVisits(host_bins, active_region);
815 buffers.markTileCsr(commands.len, width, height, active_region, shape, host_bins.indices.len, command_visits);
816 } else if (!use_device_csr) {
817 command_visits = buffers.tile_csr.command_visits;
818 }
819
820 const bindings = [_]gpu.BufferBinding{
821 bufferBinding(buffers.pixels, .read_write),
822 bufferBinding(buffers.floats, .read_only),
823 bufferBinding(buffers.words, .read_only),
824 bufferBinding(buffers.image_metadata, .read_only),
825 bufferBinding(buffers.image_pixels, .read_only),
826 bufferBinding(buffers.tile_offsets, .read_only),
827 bufferBinding(buffers.tile_indices, .read_only),
828 };
829 const base_scalars = [_]choir_abi.ScalarArgument{
830 .{ .u32 = image_count },
831 .{ .u32 = clear.r },
832 .{ .u32 = clear.g },
833 .{ .u32 = clear.b },
834 .{ .u32 = clear.a },
835 .{ .u32 = active_region.x },
836 .{ .u32 = active_region.y },
837 .{ .u32 = active_region.width },
838 .{ .u32 = region_pixel_count_u32 },
839 .{ .u32 = shape.tiles_x },
840 .{ .u32 = @backingInt(output_format) },
841 };
842 var scalar_storage: [base_scalars.len + host_loop_launch_shape_arg_count]choir_abi.ScalarArgument = undefined;
843 @memcpy(scalar_storage[0..base_scalars.len], base_scalars[0..]);
844 var scalar_count: usize = base_scalars.len;
845 const geometry = choir_abi.LaunchGeometry{
846 .grid = .{ accy_paint.gridFor(region_pixel_count, self.threads), 1, 1 },
847 .threadgroup = .{ self.threads, 1, 1 },
848 };
849 if (gpu.artifactFormatUsesHostLoopLaunch(self.artifact.format)) {
850 const launch_shape = try choir_abi.launchShape(region_pixel_count, geometry);
851 try launch_shape.scalarArguments(scalar_storage[scalar_count..]);
852 scalar_count += host_loop_launch_shape_arg_count;
853 }
854 return .{
855 .pixels = buffers.pixels,
856 .pixel_count = region_pixel_count,
857 .target_width = width,
858 .target_height = height,
859 .region_x = active_region.x,
860 .region_y = active_region.y,
861 .region_width = active_region.width,
862 .region_height = active_region.height,
863 .output_format = output_format,
864 .generation = generation,
865 .loaded_artifact_id = self.loaded.id,
866 .bindings = bindings,
867 .scalar_storage = scalar_storage,
868 .scalar_count = scalar_count,
869 .geometry = geometry,
870 .command_visits = command_visits,
871 .device_csr_prepared = device_csr_prepared,
872 };
873 }
874
875 fn prepareTileCsr(
876 self: *Executor,
877 command_count: usize,
878 command_count_u32: u32,
879 width: u32,
880 height: u32,
881 active_region: cpu.Region,
882 shape: accy_paint.BinShape,
883 tile_offset_count: usize,
884 ) !void {
885 const buffers = if (self.buffers) |*value| value else unreachable;
886 if (command_count == 0) {
887 try buffers.writeZeroes(self.handle, buffers.tile_offsets, tile_offset_count);
888 return;
889 }
890
891 try buffers.writeZeroes(self.handle, buffers.tile_counts.?, tile_offset_count);
892
893 const range_bindings = [_]gpu.BufferBinding{
894 bufferBinding(buffers.tile_ranges.?, .read_write),
895 bufferBinding(buffers.floats, .read_only),
896 bufferBinding(buffers.words, .read_only),
897 };
898 const range_scalars = [_]choir_abi.ScalarArgument{
899 .{ .u32 = command_count_u32 },
900 .{ .u32 = width },
901 .{ .u32 = height },
902 .{ .u32 = active_region.x },
903 .{ .u32 = active_region.y },
904 .{ .u32 = active_region.width },
905 .{ .u32 = active_region.height },
906 .{ .u32 = shape.tiles_x },
907 .{ .u32 = shape.tiles_y },
908 };
909 try self.launchLoadedKernel(&self.tile_range.?, range_bindings[0..], range_scalars[0..], .{
910 .grid = .{ accy_paint.gridFor(command_count, self.threads), 1, 1 },
911 .threadgroup = .{ self.threads, 1, 1 },
912 }, command_count);
913
914 const count_bindings = [_]gpu.BufferBinding{
915 bufferBinding(buffers.tile_counts.?, .read_write),
916 bufferBinding(buffers.tile_ranges.?, .read_only),
917 };
918 const count_scalars = [_]choir_abi.ScalarArgument{
919 .{ .u32 = command_count_u32 },
920 .{ .u32 = shape.tiles_x },
921 .{ .u32 = shape.tiles_y },
922 };
923 try self.launchLoadedKernel(&self.tile_count.?, count_bindings[0..], count_scalars[0..], .{
924 .grid = .{ accy_paint.gridFor(shape.tile_count, self.threads), 1, 1 },
925 .threadgroup = .{ self.threads, 1, 1 },
926 }, shape.tile_count);
927
928 try self.launchTileOffsetScan(buffers, tile_offset_count);
929 try buffers.writeZeroes(self.handle, buffers.tile_cursors.?, shape.tile_count);
930 const tile_pair_count = std.math.mul(usize, command_count, shape.tile_count) catch return error.BufferTooLarge;
931
932 const index_bindings = [_]gpu.BufferBinding{
933 bufferBinding(buffers.tile_indices, .read_write),
934 bufferBinding(buffers.tile_cursors.?, .read_write),
935 bufferBinding(buffers.tile_offsets, .read_only),
936 bufferBinding(buffers.tile_ranges.?, .read_only),
937 bufferBinding(buffers.words, .read_only),
938 };
939 const index_scalars = [_]choir_abi.ScalarArgument{
940 .{ .u32 = command_count_u32 },
941 .{ .u32 = shape.tiles_x },
942 .{ .u32 = shape.tiles_y },
943 };
944 try self.launchLoadedKernel(&self.tile_index.?, index_bindings[0..], index_scalars[0..], .{
945 .grid = .{ accy_paint.gridFor(tile_pair_count, self.threads), 1, 1 },
946 .threadgroup = .{ self.threads, 1, 1 },
947 }, tile_pair_count);
948 }
949
950 fn launchTileOffsetScan(self: *Executor, buffers: *Buffers, tile_offset_count: usize) !void {
951 const extent_i32 = std.math.cast(i32, tile_offset_count) orelse return error.BufferTooLarge;
952 const tile_scan = &self.tile_scan.?;
953 const block_count = std.math.cast(u32, scan_library.deviceScanBlockCount(tile_offset_count, tile_scan.instance.threads)) orelse return error.UnsupportedDeviceScanInstance;
954
955 const block_bindings = [_]gpu.BufferBinding{
956 bufferBinding(buffers.tile_offsets, .read_write),
957 bufferBinding(buffers.tile_counts.?, .read_only),
958 bufferBinding(buffers.scan_sums.?, .read_write),
959 };
960 const block_scalars = [_]choir_abi.ScalarArgument{
961 .{ .i32 = extent_i32 },
962 };
963 const block_geometry = choir_abi.LaunchGeometry{
964 .grid = .{ block_count, 1, 1 },
965 .threadgroup = .{ tile_scan.instance.threads, 1, 1 },
966 };
967 try self.launchLoadedKernel(&tile_scan.block_scan, block_bindings[0..], block_scalars[0..], block_geometry, try block_geometry.threadCount());
968
969 const sums_bindings = [_]gpu.BufferBinding{
970 bufferBinding(buffers.scan_bases.?, .read_write),
971 bufferBinding(buffers.scan_sums.?, .read_only),
972 };
973 const sums_scalars = [_]choir_abi.ScalarArgument{
974 .{ .i32 = @intCast(block_count) },
975 };
976 const sums_geometry = choir_abi.LaunchGeometry{
977 .grid = .{ 1, 1, 1 },
978 .threadgroup = .{ tile_scan.stages.sums_scan.threads, 1, 1 },
979 };
980 try self.launchLoadedKernel(&tile_scan.sums_scan, sums_bindings[0..], sums_scalars[0..], sums_geometry, try sums_geometry.threadCount());
981
982 const add_bindings = [_]gpu.BufferBinding{
983 bufferBinding(buffers.tile_offsets, .read_write),
984 bufferBinding(buffers.scan_bases.?, .read_only),
985 };
986 const add_scalars = [_]choir_abi.ScalarArgument{
987 .{ .i32 = extent_i32 },
988 };
989 const add_geometry = choir_abi.LaunchGeometry{
990 .grid = .{ block_count, 1, 1 },
991 .threadgroup = .{ tile_scan.instance.threads, 1, 1 },
992 };
993 try self.launchLoadedKernel(&tile_scan.add_base, add_bindings[0..], add_scalars[0..], add_geometry, try add_geometry.threadCount());
994 }
995
996 fn launchLoadedKernel(
997 self: *Executor,
998 loaded_kernel: *LoadedKernel,
999 bindings: []const gpu.BufferBinding,
1000 scalars: []const choir_abi.ScalarArgument,
1001 geometry: choir_abi.LaunchGeometry,
1002 total_count: u64,
1003 ) !void {
1004 var scalar_storage: [16 + host_loop_launch_shape_arg_count]choir_abi.ScalarArgument = undefined;
1005 if (scalars.len > 16) return error.LaunchArgumentMismatch;
1006 @memcpy(scalar_storage[0..scalars.len], scalars);
1007 var scalar_count = scalars.len;
1008 if (gpu.artifactFormatUsesHostLoopLaunch(loaded_kernel.artifact.format)) {
1009 const shape = try choir_abi.launchShape(total_count, geometry);
1010 try shape.scalarArguments(scalar_storage[scalar_count..]);
1011 scalar_count += host_loop_launch_shape_arg_count;
1012 }
1013 try self.handle.launch(.{
1014 .artifact = &loaded_kernel.artifact,
1015 .loaded_artifact = loaded_kernel.loaded,
1016 .buffers = bindings,
1017 .scalar_arguments = scalar_storage[0..scalar_count],
1018 .geometry = geometry,
1019 });
1020 }
1021
1022 fn ensureCapacity(self: *Executor, demand: Limits) !bool {
1023 if (self.buffers) |buffers| {
1024 if (buffers.capacity.admits(demand)) return false;
1025 }
1026
1027 const next_limits = if (self.buffers) |buffers|
1028 try buffers.capacity.limits.merged(demand)
1029 else
1030 demand;
1031 const next = try Buffers.init(self.allocator, self.handle, next_limits);
1032 const replacing = self.buffers != null;
1033 self.releaseBuffers();
1034 self.buffers = next;
1035 if (replacing) self.storage_replacements += 1;
1036 return true;
1037 }
1038
1039 fn ensurePreparedGenerationAvailable(self: *const Executor) !void {
1040 if (self.prepared_generation == std.math.maxInt(u64)) return error.PreparedLaunchGenerationExhausted;
1041 }
1042
1043 fn advancePreparedGeneration(self: *Executor) u64 {
1044 std.debug.assert(self.prepared_generation != std.math.maxInt(u64));
1045 self.prepared_generation += 1;
1046 return self.prepared_generation;
1047 }
1048
1049 fn validatePreparedLaunch(self: *const Executor, prepared: *const PreparedPackedLaunch) !void {
1050 if (prepared.loaded_artifact_id != self.loaded.id) return error.InvalidPreparedLaunch;
1051 if (prepared.generation != self.prepared_generation) return error.StalePreparedLaunch;
1052 }
1053
1054 fn releaseBuffers(self: *Executor) void {
1055 if (self.buffers) |*buffers| {
1056 buffers.deinit(self.allocator, self.handle);
1057 self.buffers = null;
1058 }
1059 }
1060 };
1061
1062 pub const ExecutorHostStorage = struct {
1063 pub const Limits = ExecutorLimits;
1064 pub const Capacity = ExecutorCapacity;
1065
1066 pub const claim: alloc_phase.capacity.Declaration = .{
1067 .source = .{
1068 .id = "gui.paint_executor_host_storage",
1069 .kind = .phase_static,
1070 .limit_source = .caller,
1071 .storage = .{
1072 .covered = &.{
1073 .{
1074 .id = "command_and_image_upload_staging_for_one_prepared_paint_epoch",
1075 .lifetime = .steady,
1076 .detail = "command and image upload staging for one prepared paint epoch",
1077 },
1078 .{
1079 .id = "packed_readback_zero_fill_and_host_tile_bin_scratch",
1080 .lifetime = .steady,
1081 .detail = "packed readback zero-fill and host tile-bin scratch",
1082 },
1083 },
1084 .excluded = &.{
1085 "common and mode-specific backend buffers and their foreign allocation",
1086 "compiled and loaded paint kernel artifacts",
1087 "image Processor and cached shadow expansion storage",
1088 "caller-owned commands images targets surfaces and prepared values",
1089 "transactional complete-epoch replacement by Executor",
1090 },
1091 },
1092 .capacity = .{
1093 .inputs = &.{
1094 alloc_phase.capacity.bindInput(ExecutorLimits, "commands", "commands"),
1095 alloc_phase.capacity.bindInput(ExecutorLimits, "image_pixels", "image_pixels"),
1096 alloc_phase.capacity.bindInput(ExecutorLimits, "images", "images"),
1097 alloc_phase.capacity.bindInput(ExecutorLimits, "pixels", "pixels"),
1098 alloc_phase.capacity.bindInput(ExecutorLimits, "tile_pairs", "tile_pairs"),
1099 alloc_phase.capacity.bindInput(ExecutorLimits, "tiles", "tiles"),
1100 },
1101 .type_selectors = &.{},
1102 .nodes = &.{
1103 .{ .input = 0 },
1104 .{ .input = 1 },
1105 .{ .input = 2 },
1106 .{ .input = 3 },
1107 .{ .input = 4 },
1108 .{ .input = 5 },
1109 .{ .add = .{ .left = 0, .right = 1 } },
1110 .{ .add = .{ .left = 6, .right = 2 } },
1111 .{ .add = .{ .left = 7, .right = 3 } },
1112 .{ .add = .{ .left = 8, .right = 4 } },
1113 .{ .add = .{ .left = 9, .right = 5 } },
1114 },
1115 .assertions = &.{.{
1116 .scope = .closure_total,
1117 .measure = .retained,
1118 .relation = .upper_bound,
1119 .expression = 10,
1120 }},
1121 },
1122 .overload = .{
1123 .kind = .reject_before_seal,
1124 .detail = "checked capacity derivation and allocation failure reject before host storage activation; Executor may acquire a separate larger epoch",
1125 },
1126 .risks = .{
1127 .transitive = .{
1128 .status = .open,
1129 .detail = "packing binning and backend transfer helpers are exercised but lack a machine-checked call-graph closure certificate",
1130 },
1131 .foreign = .{
1132 .status = .open,
1133 .detail = "backend buffer acquisition is capacity-accounted by Executor but remains outside this host-storage claim",
1134 },
1135 },
1136 .obligations = &.{
1137 .{ .key = "gui_paint_executor_capacity_capacity_model", .role = .capacity_model },
1138 .{ .key = "gui_paint_executor_capacity_overload", .role = .overload },
1139 .{ .key = "gui_paint_executor_acquisition", .role = .custom },
1140 .{ .key = "gui_paint_executor_host_oom", .role = .overload },
1141 .{ .key = "gui_paint_executor_boundary", .role = .custom },
1142 .{ .key = "gui_paint_executor_atomic", .role = .custom },
1143 .{ .key = "gui_paint_executor_steady", .role = .custom },
1144 },
1145 },
1146 .bindings = .{
1147 .owner = @This(),
1148 .seal = .{
1149 .family = alloc_phase.capacity.selector(@This().activate),
1150 .premise = .{
1151 .class = .checked_semantic_fact,
1152 .authority = .checker,
1153 },
1154 },
1155 .teardown = .{
1156 .family = alloc_phase.capacity.selector(@This().deinit),
1157 .premise = .{
1158 .class = .checked_semantic_fact,
1159 .authority = .checker,
1160 },
1161 },
1162 },
1163 };
1164
1165 phase: alloc_phase.capacity.Phase,
1166 capacity: ExecutorCapacity,
1167 limits: ExecutorLimits,
1168 bytes: []align(@alignOf(u32)) u8,
1169
1170 pub fn init(allocator: Allocator, limits: ExecutorLimits) !ExecutorHostStorage {
1171 const capacity = try ExecutorCapacity.derive(limits);
1172 const bytes = if (capacity.host_storage_bytes == 0)
1173 @as([]align(@alignOf(u32)) u8, &.{})
1174 else
1175 try allocator.alignedAlloc(
1176 u8,
1177 .fromByteUnits(@alignOf(u32)),
1178 capacity.host_storage_bytes,
1179 );
1180 return .{
1181 .phase = .initialization,
1182 .capacity = capacity,
1183 .limits = limits,
1184 .bytes = bytes,
1185 };
1186 }
1187
1188 pub fn activate(self: *ExecutorHostStorage) void {
1189 std.debug.assert(self.phase == .initialization);
1190 std.debug.assert(std.meta.eql(self.capacity.limits, self.limits));
1191 std.debug.assert(self.bytes.len == self.capacity.host_storage_bytes);
1192 self.phase = .steady;
1193 }
1194
1195 pub fn deinit(self: *ExecutorHostStorage, allocator: Allocator) void {
1196 std.debug.assert(self.phase != .teardown);
1197 self.phase = .teardown;
1198 allocator.free(self.bytes);
1199 self.* = undefined;
1200 }
1201 };
1202
1203 comptime {
1204 alloc_phase.capacity.requireAllocatorExactOwnerShape(ExecutorHostStorage);
1205 }
1206
1207 const HostCursor = struct {
1208 bytes: []align(@alignOf(u32)) u8,
1209 offset: usize = 0,
1210
1211 fn take(self: *HostCursor, comptime T: type, count: usize) []T {
1212 comptime std.debug.assert(@sizeOf(T) == @sizeOf(u32));
1213 comptime std.debug.assert(@alignOf(T) <= @alignOf(u32));
1214 const byte_count = count * @sizeOf(T);
1215 std.debug.assert(self.offset + byte_count <= self.bytes.len);
1216 const pointer: [*]T = @ptrCast(@alignCast(self.bytes.ptr + self.offset));
1217 self.offset += byte_count;
1218 return pointer[0..count];
1219 }
1220 };
1221
1222 const Buffers = struct {
1223 capacity: Capacity,
1224 command_capacity: usize,
1225 pixel_capacity: usize,
1226 image_capacity: usize,
1227 image_pixel_capacity: usize,
1228 tile_range_capacity: usize,
1229 tile_offset_capacity: usize,
1230 tile_count_capacity: usize,
1231 tile_index_capacity: usize,
1232 pixels: gpu.BufferHandle,
1233 floats: gpu.BufferHandle,
1234 words: gpu.BufferHandle,
1235 image_metadata: gpu.BufferHandle,
1236 image_pixels: gpu.BufferHandle,
1237 tile_ranges: ?gpu.BufferHandle,
1238 tile_counts: ?gpu.BufferHandle,
1239 tile_offsets: gpu.BufferHandle,
1240 tile_cursors: ?gpu.BufferHandle,
1241 tile_indices: gpu.BufferHandle,
1242 scan_sums: ?gpu.BufferHandle,
1243 scan_bases: ?gpu.BufferHandle,
1244 host: ExecutorHostStorage,
1245 host_bins: accy_paint.BinScratch,
1246 readback: []u32,
1247 zeroes: []u32,
1248 staging: accy_paint.PackedCommands,
1249 images: accy_paint.PackedImages,
1250 command_upload: CommandUploadState = .{},
1251 tile_csr: TileCsrState = .{},
1252 image_upload: ImageUploadState = .{},
1253
1254 fn init(
1255 allocator: Allocator,
1256 handle: gpu.BackendHandle,
1257 limits: Limits,
1258 ) !Buffers {
1259 var host_storage = try ExecutorHostStorage.init(allocator, limits);
1260 errdefer host_storage.deinit(allocator);
1261 const capacity = host_storage.capacity;
1262 var host = HostCursor{ .bytes = host_storage.bytes };
1263 const floats_staging = host.take(f32, capacity.float_count);
1264 const words_staging = host.take(u32, capacity.word_count);
1265 const image_metadata_staging = host.take(u32, capacity.image_metadata_count);
1266 const image_pixels_staging = host.take(u32, capacity.image_pixel_capacity);
1267 const readback = host.take(u32, capacity.pixel_capacity);
1268 const zeroes = host.take(u32, capacity.tile_offset_capacity);
1269 const host_ranges = host.take(u32, capacity.host_range_count);
1270 const host_offsets = host.take(u32, capacity.host_offset_count);
1271 const host_indices = host.take(u32, capacity.host_index_count);
1272 const host_cursors = host.take(u32, capacity.host_cursor_count);
1273 std.debug.assert(host.offset == host_storage.bytes.len);
1274
1275 const pixels = try allocateDeviceBuffer(handle, u32, .u32, capacity.pixel_capacity);
1276 errdefer handle.destroyObject(pixels.id);
1277 const floats = try allocateDeviceBuffer(handle, f32, .f32, capacity.float_count);
1278 errdefer handle.destroyObject(floats.id);
1279 const words = try allocateDeviceBuffer(handle, u32, .u32, capacity.word_count);
1280 errdefer handle.destroyObject(words.id);
1281 const image_metadata = try allocateDeviceBuffer(handle, u32, .u32, capacity.image_metadata_count);
1282 errdefer handle.destroyObject(image_metadata.id);
1283 const image_pixels = try allocateDeviceBuffer(handle, u32, .u32, capacity.image_pixel_capacity);
1284 errdefer handle.destroyObject(image_pixels.id);
1285 const tile_offsets = try allocateDeviceBuffer(handle, u32, .u32, capacity.tile_offset_capacity);
1286 errdefer handle.destroyObject(tile_offsets.id);
1287 const tile_indices = try allocateDeviceBuffer(handle, u32, .u32, capacity.tile_index_capacity);
1288 errdefer handle.destroyObject(tile_indices.id);
1289 var tile_ranges: ?gpu.BufferHandle = null;
1290 errdefer if (tile_ranges) |buffer| handle.destroyObject(buffer.id);
1291 var tile_counts: ?gpu.BufferHandle = null;
1292 errdefer if (tile_counts) |buffer| handle.destroyObject(buffer.id);
1293 var tile_cursors: ?gpu.BufferHandle = null;
1294 errdefer if (tile_cursors) |buffer| handle.destroyObject(buffer.id);
1295 var scan_sums: ?gpu.BufferHandle = null;
1296 errdefer if (scan_sums) |buffer| handle.destroyObject(buffer.id);
1297 var scan_bases: ?gpu.BufferHandle = null;
1298 errdefer if (scan_bases) |buffer| handle.destroyObject(buffer.id);
1299 if (limits.mode == .device_csr) {
1300 tile_ranges = try allocateDeviceBuffer(handle, u32, .u32, capacity.tile_range_capacity);
1301 tile_counts = try allocateDeviceBuffer(handle, u32, .u32, capacity.tile_offset_capacity);
1302 tile_cursors = try allocateDeviceBuffer(handle, u32, .u32, capacity.tile_count_capacity);
1303 scan_sums = try allocateDeviceBuffer(handle, u32, .u32, scan_library.device_scan_max_blocks);
1304 scan_bases = try allocateDeviceBuffer(handle, u32, .u32, scan_library.device_scan_max_blocks);
1305 }
1306 host_storage.activate();
1307
1308 return .{
1309 .capacity = capacity,
1310 .command_capacity = capacity.command_capacity,
1311 .pixel_capacity = capacity.pixel_capacity,
1312 .image_capacity = capacity.image_capacity,
1313 .image_pixel_capacity = capacity.image_pixel_capacity,
1314 .tile_range_capacity = capacity.tile_range_capacity,
1315 .tile_offset_capacity = capacity.tile_offset_capacity,
1316 .tile_count_capacity = capacity.tile_count_capacity,
1317 .tile_index_capacity = capacity.tile_index_capacity,
1318 .pixels = pixels,
1319 .floats = floats,
1320 .words = words,
1321 .image_metadata = image_metadata,
1322 .image_pixels = image_pixels,
1323 .tile_ranges = tile_ranges,
1324 .tile_counts = tile_counts,
1325 .tile_offsets = tile_offsets,
1326 .tile_cursors = tile_cursors,
1327 .tile_indices = tile_indices,
1328 .scan_sums = scan_sums,
1329 .scan_bases = scan_bases,
1330 .host = host_storage,
1331 .host_bins = .{
1332 .ranges = host_ranges,
1333 .offsets = host_offsets,
1334 .indices = host_indices,
1335 .cursors = host_cursors,
1336 },
1337 .readback = readback,
1338 .zeroes = zeroes,
1339 .staging = .{ .floats = floats_staging, .words = words_staging },
1340 .images = .{ .metadata = image_metadata_staging, .pixels = image_pixels_staging },
1341 };
1342 }
1343
1344 fn deinit(self: *Buffers, allocator: Allocator, handle: gpu.BackendHandle) void {
1345 handle.destroyObject(self.pixels.id);
1346 handle.destroyObject(self.floats.id);
1347 handle.destroyObject(self.words.id);
1348 handle.destroyObject(self.image_metadata.id);
1349 handle.destroyObject(self.image_pixels.id);
1350 if (self.tile_ranges) |buffer| handle.destroyObject(buffer.id);
1351 if (self.tile_counts) |buffer| handle.destroyObject(buffer.id);
1352 handle.destroyObject(self.tile_offsets.id);
1353 if (self.tile_cursors) |buffer| handle.destroyObject(buffer.id);
1354 handle.destroyObject(self.tile_indices.id);
1355 if (self.scan_sums) |buffer| handle.destroyObject(buffer.id);
1356 if (self.scan_bases) |buffer| handle.destroyObject(buffer.id);
1357 self.host.deinit(allocator);
1358 self.* = undefined;
1359 }
1360
1361 fn writeZeroes(self: *Buffers, handle: gpu.BackendHandle, buffer: gpu.BufferHandle, count: usize) !void {
1362 if (count == 0) return;
1363 if (self.zeroes.len < count) return error.BufferTooSmall;
1364 @memset(self.zeroes[0..count], 0);
1365 try handle.writeBuffer(.{
1366 .handle = buffer,
1367 .bytes = std.mem.sliceAsBytes(self.zeroes[0..count]),
1368 });
1369 }
1370
1371 fn commandsResident(self: *const Buffers, commands: []const Command) bool {
1372 if (!self.command_upload.valid or self.command_upload.command_count != commands.len) return false;
1373 const float_count = std.math.mul(usize, commands.len, accy_paint.float_lanes) catch return false;
1374 const word_count = std.math.mul(usize, commands.len, accy_paint.word_lanes) catch return false;
1375 if (float_count > self.staging.floats.len or word_count > self.staging.words.len) return false;
1376 return accy_paint.commandsEqualPacked(commands, self.staging.floats[0..float_count], self.staging.words[0..word_count]);
1377 }
1378
1379 fn prepareCommandUpload(self: *Buffers, commands: []const Command) bool {
1380 const floats = self.staging.floats[0 .. commands.len * accy_paint.float_lanes];
1381 const words = self.staging.words[0 .. commands.len * accy_paint.word_lanes];
1382 const upload = !self.command_upload.valid or
1383 self.command_upload.command_count != commands.len or
1384 !accy_paint.commandsEqualPacked(commands, floats, words);
1385 if (upload) {
1386 accy_paint.packCommands(commands, floats, words);
1387 self.command_upload = .{
1388 .valid = true,
1389 .command_count = commands.len,
1390 };
1391 self.tile_csr.valid = false;
1392 }
1393 return upload;
1394 }
1395
1396 fn markTileCsr(self: *Buffers, command_count: usize, width: u32, height: u32, active_region: cpu.Region, shape: accy_paint.BinShape, tile_index_count: usize, command_visits: usize) void {
1397 self.tile_csr = .{
1398 .valid = true,
1399 .command_count = command_count,
1400 .width = width,
1401 .height = height,
1402 .active_region = active_region,
1403 .tiles_x = shape.tiles_x,
1404 .tiles_y = shape.tiles_y,
1405 .tile_count = shape.tile_count,
1406 .tile_index_count = tile_index_count,
1407 .command_visits = command_visits,
1408 };
1409 }
1410
1411 fn prepareImageUpload(self: *Buffers, images: ImageSet, image_pixels: usize) !ImageUploadPlan {
1412 if (images.images.len == 0) return .none;
1413 if (!self.image_upload.valid or
1414 self.image_upload.image_count != images.images.len or
1415 self.image_upload.pixel_count != image_pixels)
1416 {
1417 try accy_paint.packImages(images, self.images.metadata, self.images.pixels);
1418 return .metadata_and_pixels;
1419 }
1420 if (try accy_paint.imagesEqualPacked(images, self.images.metadata, self.images.pixels)) return .none;
1421 const metadata_matches = try accy_paint.imageMetadataEqualPacked(images, self.images.metadata);
1422 try accy_paint.packImages(images, self.images.metadata, self.images.pixels);
1423 return if (metadata_matches) .pixels else .metadata_and_pixels;
1424 }
1425
1426 fn markImageUpload(self: *Buffers, image_count: usize, pixel_count: usize) void {
1427 self.image_upload = .{
1428 .valid = true,
1429 .image_count = image_count,
1430 .pixel_count = pixel_count,
1431 };
1432 }
1433 };
1434
1435 const CommandUploadState = struct {
1436 valid: bool = false,
1437 command_count: usize = 0,
1438 };
1439
1440 const TileCsrState = struct {
1441 valid: bool = false,
1442 command_count: usize = 0,
1443 width: u32 = 0,
1444 height: u32 = 0,
1445 active_region: cpu.Region = .{},
1446 tiles_x: u32 = 0,
1447 tiles_y: u32 = 0,
1448 tile_count: usize = 0,
1449 tile_index_count: usize = 0,
1450 command_visits: usize = 0,
1451
1452 fn matches(self: TileCsrState, command_count: usize, width: u32, height: u32, active_region: cpu.Region, shape: accy_paint.BinShape) bool {
1453 return self.valid and
1454 self.command_count == command_count and
1455 self.width == width and
1456 self.height == height and
1457 self.active_region.x == active_region.x and
1458 self.active_region.y == active_region.y and
1459 self.active_region.width == active_region.width and
1460 self.active_region.height == active_region.height and
1461 self.tiles_x == shape.tiles_x and
1462 self.tiles_y == shape.tiles_y and
1463 self.tile_count == shape.tile_count;
1464 }
1465 };
1466
1467 const ImageUploadState = struct {
1468 valid: bool = false,
1469 image_count: usize = 0,
1470 pixel_count: usize = 0,
1471 };
1472
1473 const ImageUploadPlan = enum {
1474 none,
1475 pixels,
1476 metadata_and_pixels,
1477 };
1478
1479 const CachedShadowExpansion = struct {
1480 source_commands: []Command,
1481 source_images: []command.Image,
1482 source_pixels: []u32,
1483 options: paint_image.ShadowExpansionOptions,
1484 expansion: paint_image.ShadowExpansion,
1485
1486 fn init(
1487 allocator: Allocator,
1488 processor: *paint_image.Processor,
1489 commands: []const Command,
1490 images: ImageSet,
1491 options: paint_image.ShadowExpansionOptions,
1492 ) !CachedShadowExpansion {
1493 const source_commands = try allocator.dupe(Command, commands);
1494 errdefer allocator.free(source_commands);
1495
1496 var owned_images = try copyImageSet(allocator, images);
1497 errdefer owned_images.deinit(allocator);
1498
1499 var expansion = try processor.expandShadowsAlloc(commands, .{ .images = owned_images.images }, options);
1500 errdefer expansion.deinit();
1501
1502 return .{
1503 .source_commands = source_commands,
1504 .source_images = owned_images.images,
1505 .source_pixels = owned_images.pixels,
1506 .options = options,
1507 .expansion = expansion,
1508 };
1509 }
1510
1511 fn deinit(self: *CachedShadowExpansion, allocator: Allocator) void {
1512 self.expansion.deinit();
1513 allocator.free(self.source_commands);
1514 allocator.free(self.source_images);
1515 allocator.free(self.source_pixels);
1516 self.* = undefined;
1517 }
1518
1519 fn matches(self: *const CachedShadowExpansion, commands: []const Command, images: ImageSet, options: paint_image.ShadowExpansionOptions) !bool {
1520 return shadowExpansionOptionsEqual(self.options, options) and
1521 commandsEqual(self.source_commands, commands) and
1522 try imageSetsEqual(self.source_images, images);
1523 }
1524
1525 fn refreshReusable(self: *CachedShadowExpansion, commands: []const Command, images: ImageSet, options: paint_image.ShadowExpansionOptions) !bool {
1526 if (!shadowExpansionOptionsEqual(self.options, options)) return false;
1527 if (!(try imageSetsEqual(self.source_images, images))) return false;
1528 if (!commandsReusableForShadowExpansion(self.source_commands, commands)) return false;
1529 try self.expansion.refreshReusableCommands(commands);
1530 @memcpy(self.source_commands, commands);
1531 return true;
1532 }
1533 };
1534
1535 const OwnedImageSet = struct {
1536 images: []command.Image,
1537 pixels: []u32,
1538
1539 fn deinit(self: *OwnedImageSet, allocator: Allocator) void {
1540 allocator.free(self.images);
1541 allocator.free(self.pixels);
1542 self.* = undefined;
1543 }
1544 };
1545
1546 fn copyImageSet(allocator: Allocator, images: ImageSet) !OwnedImageSet {
1547 const image_entries = try allocator.alloc(command.Image, images.images.len);
1548 errdefer allocator.free(image_entries);
1549 const pixel_count = try accy_paint.imagePixelCount(images);
1550 const image_pixels = try allocator.alloc(u32, pixel_count);
1551 errdefer allocator.free(image_pixels);
1552
1553 var pixel_offset: usize = 0;
1554 for (images.images, 0..) |image, index| {
1555 try image.validate();
1556 const count = image.pixelCount();
1557 @memcpy(image_pixels[pixel_offset .. pixel_offset + count], image.pixels[0..count]);
1558 image_entries[index] = .{
1559 .width = image.width,
1560 .height = image.height,
1561 .pixels = image_pixels[pixel_offset .. pixel_offset + count],
1562 };
1563 pixel_offset += count;
1564 }
1565 return .{ .images = image_entries, .pixels = image_pixels };
1566 }
1567
1568 fn shadowExpansionOptionsEqual(left: paint_image.ShadowExpansionOptions, right: paint_image.ShadowExpansionOptions) bool {
1569 return optionalF32Equal(left.sigma, right.sigma);
1570 }
1571
1572 fn optionalF32Equal(left: ?f32, right: ?f32) bool {
1573 if (left) |left_value| {
1574 if (right) |right_value| return left_value == right_value;
1575 return false;
1576 }
1577 return right == null;
1578 }
1579
1580 fn commandsEqual(left: []const Command, right: []const Command) bool {
1581 if (left.len != right.len) return false;
1582 for (left, right) |left_command, right_command| {
1583 if (!commandEqual(left_command, right_command)) return false;
1584 }
1585 return true;
1586 }
1587
1588 fn commandsReusableForShadowExpansion(left: []const Command, right: []const Command) bool {
1589 if (left.len != right.len) return false;
1590 for (left, right) |left_command, right_command| {
1591 const left_shadow = left_command.kind == .shadow;
1592 const right_shadow = right_command.kind == .shadow;
1593 if (left_shadow != right_shadow) return false;
1594 if (left_shadow) {
1595 if (!shadowCommandReusableForRetint(left_command, right_command)) return false;
1596 }
1597 }
1598 return true;
1599 }
1600
1601 fn shadowCommandReusableForRetint(left: Command, right: Command) bool {
1602 return left.kind == right.kind and
1603 rectEqual(left.rect, right.rect) and
1604 rectEqual(left.clip, right.clip) and
1605 rectEqual(left.source, right.source) and
1606 left.color.a == right.color.a and
1607 colorEqual(left.color_end, right.color_end) and
1608 pointEqual(left.gradient_start, right.gradient_start) and
1609 pointEqual(left.gradient_end, right.gradient_end) and
1610 left.radius == right.radius and
1611 left.width == right.width and
1612 left.image_index == right.image_index and
1613 left.order == right.order;
1614 }
1615
1616 fn commandEqual(left: Command, right: Command) bool {
1617 return left.kind == right.kind and
1618 rectEqual(left.rect, right.rect) and
1619 rectEqual(left.clip, right.clip) and
1620 rectEqual(left.source, right.source) and
1621 colorEqual(left.color, right.color) and
1622 colorEqual(left.color_end, right.color_end) and
1623 pointEqual(left.gradient_start, right.gradient_start) and
1624 pointEqual(left.gradient_end, right.gradient_end) and
1625 left.radius == right.radius and
1626 left.width == right.width and
1627 left.image_index == right.image_index and
1628 left.order == right.order;
1629 }
1630
1631 fn rectEqual(left: gui.layout.Rect, right: gui.layout.Rect) bool {
1632 return left.x == right.x and
1633 left.y == right.y and
1634 left.width == right.width and
1635 left.height == right.height;
1636 }
1637
1638 fn colorEqual(left: Color, right: Color) bool {
1639 return left.r == right.r and left.g == right.g and left.b == right.b and left.a == right.a;
1640 }
1641
1642 fn pointEqual(left: gui.model.UiPoint, right: gui.model.UiPoint) bool {
1643 return left.x == right.x and left.y == right.y;
1644 }
1645
1646 fn expectShadowPixelsTinted(pixels: []const u32, color: Color) !void {
1647 try std.testing.expect(pixels.len != 0);
1648 for (pixels) |pixel| {
1649 const unpacked = cpu.pixel.unpackRgba(pixel);
1650 try std.testing.expectEqual(color.r, unpacked.r);
1651 try std.testing.expectEqual(color.g, unpacked.g);
1652 try std.testing.expectEqual(color.b, unpacked.b);
1653 }
1654 }
1655
1656 fn imageSetsEqual(cached: []const command.Image, images: ImageSet) !bool {
1657 if (cached.len != images.images.len) return false;
1658 for (cached, images.images) |left, right| {
1659 try right.validate();
1660 if (left.width != right.width or left.height != right.height) return false;
1661 const count = right.pixelCount();
1662 if (!std.mem.eql(u32, left.pixels[0..count], right.pixels[0..count])) return false;
1663 }
1664 return true;
1665 }
1666
1667 fn defaultFormat(handle: gpu.BackendHandle) !gpu.ArtifactFormat {
1668 const kind = handle.backendKind() orelse return error.UnsupportedArtifactFormat;
1669 return switch (kind) {
1670 .cuda => .cuda_ptx,
1671 .vulkan => .vulkan_spirv,
1672 .metal => .metal_msl,
1673 else => error.UnsupportedArtifactFormat,
1674 };
1675 }
1676
1677 fn loadGraphKernel(
1678 allocator: Allocator,
1679 handle: gpu.BackendHandle,
1680 format: gpu.ArtifactFormat,
1681 threads: u32,
1682 comptime build: anytype,
1683 diagnostic_id: []const u8,
1684 ) !LoadedKernel {
1685 var graph = try build(allocator, threads);
1686 defer graph.deinit();
1687 var artifact = try kernel.createKernelArtifact(allocator, handle, &graph, .{
1688 .artifact_format = format,
1689 .authored_kernel_diagnostic_id = diagnostic_id,
1690 });
1691 errdefer artifact.deinit();
1692 const loaded = try handle.loadArtifact(&artifact);
1693 errdefer handle.destroyObject(loaded.id);
1694 return .{ .artifact = artifact, .loaded = loaded };
1695 }
1696
1697 fn loadDeviceScanKernels(
1698 allocator: Allocator,
1699 handle: gpu.BackendHandle,
1700 format: gpu.ArtifactFormat,
1701 ) !DeviceScanKernels {
1702 const instance = scan_library.DeviceScan{
1703 .extent = @as(u64, scan_library.prefix_sum_max_threads) * scan_library.device_scan_max_blocks,
1704 .dtype = .u32,
1705 .threads = scan_library.prefix_sum_max_threads,
1706 .mode = .inclusive,
1707 };
1708 const stages = try scan_library.deviceScanStages(instance);
1709 var artifacts = try scan_library.createDeviceScanPipelineArtifacts(allocator, handle, instance, .{
1710 .limits = kernel.Limits.standard,
1711 .format = format,
1712 });
1713 defer artifacts.deinit();
1714 const entries = artifacts.entries();
1715
1716 var block_scan = try loadKernelCallEntry(allocator, handle, entries[0], "gui/paint/executor/tile-offset-scan-block");
1717 errdefer block_scan.deinit(handle);
1718 var sums_scan = try loadKernelCallEntry(allocator, handle, entries[1], "gui/paint/executor/tile-offset-scan-sums");
1719 errdefer sums_scan.deinit(handle);
1720 const add_base = try loadKernelCallEntry(allocator, handle, entries[2], "gui/paint/executor/tile-offset-scan-add");
1721
1722 return .{
1723 .instance = instance,
1724 .stages = stages,
1725 .block_scan = block_scan,
1726 .sums_scan = sums_scan,
1727 .add_base = add_base,
1728 };
1729 }
1730
1731 fn loadKernelCallEntry(
1732 allocator: Allocator,
1733 handle: gpu.BackendHandle,
1734 entry: anytype,
1735 diagnostic_id: []const u8,
1736 ) !LoadedKernel {
1737 var artifact = try kernel.createBackendArtifactFromKernelCallEntry(allocator, handle, entry, diagnostic_id);
1738 errdefer artifact.deinit();
1739 const loaded = try handle.loadArtifact(&artifact);
1740 errdefer handle.destroyObject(loaded.id);
1741 return .{ .artifact = artifact, .loaded = loaded };
1742 }
1743
1744 fn pixelCount(width: u32, height: u32) !usize {
1745 return std.math.mul(usize, @as(usize, width), @as(usize, height)) catch return error.DimensionsTooLarge;
1746 }
1747
1748 fn allocateDeviceBuffer(handle: gpu.BackendHandle, comptime T: type, dtype: choir_abi.DType, count: usize) !gpu.BufferHandle {
1749 const byte_size = std.math.mul(usize, count, @sizeOf(T)) catch return error.BufferTooLarge;
1750 return handle.allocateBuffer(.{
1751 .byte_size = byte_size,
1752 .alignment = 256,
1753 .dtype = dtype,
1754 .element_count = std.math.cast(u64, count) orelse return error.BufferTooLarge,
1755 });
1756 }
1757
1758 fn surfacePaintTarget(surface: gpu.SurfaceHandle) !PaintTarget {
1759 if (!surface.extent.valid()) return error.InvalidSurface;
1760 return .{
1761 .width = surface.extent.width,
1762 .height = surface.extent.height,
1763 .output_format = outputFormatForTexture(surface.format) orelse return error.InvalidSurface,
1764 };
1765 }
1766
1767 fn surfaceFramePaintTarget(frame: gpu.SurfaceFrame) !PaintTarget {
1768 if (!frame.texture.extent.valid()) return error.InvalidSurfaceFrame;
1769 if (frame.texture.extent.depth != 1) return error.InvalidSurfaceFrame;
1770 return .{
1771 .width = frame.texture.extent.width,
1772 .height = frame.texture.extent.height,
1773 .output_format = outputFormatForTexture(frame.texture.format) orelse return error.InvalidSurfaceFrame,
1774 };
1775 }
1776
1777 fn validatePreparedSurfaceTarget(prepared: *const PreparedPackedLaunch, target: PaintTarget) !void {
1778 const target_pixel_count = try pixelCount(target.width, target.height);
1779 if (prepared.target_width != target.width) return error.InvalidSurfaceFrame;
1780 if (prepared.target_height != target.height) return error.InvalidSurfaceFrame;
1781 if (prepared.output_format != target.output_format) return error.InvalidSurfaceFrame;
1782 if (prepared.region_x != 0 or prepared.region_y != 0) return error.InvalidSurfaceFrame;
1783 if (prepared.region_width != target.width or prepared.region_height != target.height) return error.InvalidSurfaceFrame;
1784 if (prepared.pixel_count != target_pixel_count) return error.InvalidSurfaceFrame;
1785 }
1786
1787 fn artifactPayloadBytes(artifact: *const gpu.KernelArtifact) usize {
1788 return switch (artifact.payload) {
1789 .none => 0,
1790 .bytes => |bytes| bytes.len,
1791 .words_u32 => |words| words.len * @sizeOf(u32),
1792 .text => |text| text.len,
1793 .external => 0,
1794 };
1795 }
1796
1797 /// The paint output a color texture holds; a depth texture holds none.
1798 fn outputFormatForTexture(format: gpu.TextureFormat) ?accy_paint.OutputFormat {
1799 return switch (format) {
1800 .rgba8_unorm, .rgba8_srgb => .rgba,
1801 .bgra8_unorm, .bgra8_srgb => .bgra,
1802 .depth32_float => null,
1803 };
1804 }
1805
1806 fn bufferBinding(buffer: gpu.BufferHandle, access: gpu.BufferAccess) gpu.BufferBinding {
1807 return .{
1808 .handle = buffer,
1809 .access = access,
1810 .ownership = buffer.ownership,
1811 .byte_size = buffer.byte_size,
1812 };
1813 }
1814
1815 fn copyRgba8Region(dst: []u8, width: u32, region: cpu.Region, src: []const u32) !void {
1816 const required_pixels = @as(usize, region.y + region.height) * width;
1817 const required_bytes = std.math.mul(usize, required_pixels, 4) catch return error.BufferTooSmall;
1818 if (dst.len < required_bytes) return error.BufferTooSmall;
1819 var y: u32 = 0;
1820 while (y < region.height) : (y += 1) {
1821 var x: u32 = 0;
1822 while (x < region.width) : (x += 1) {
1823 const src_index = @as(usize, y) * region.width + x;
1824 const dst_index = @as(usize, region.y + y) * width + region.x + x;
1825 writePackedRgba8(dst, dst_index, src[src_index]);
1826 }
1827 }
1828 }
1829
1830 fn copyPackedRegion(dst: []u32, width: u32, region: cpu.Region, src: []const u32) void {
1831 var y: u32 = 0;
1832 while (y < region.height) : (y += 1) {
1833 const dst_start = @as(usize, region.y + y) * width + region.x;
1834 const src_start = @as(usize, y) * region.width;
1835 @memcpy(dst[dst_start .. dst_start + region.width], src[src_start .. src_start + region.width]);
1836 }
1837 }
1838
1839 fn writePackedRgba8(dst: []u8, index: usize, pixel: u32) void {
1840 const base = index * 4;
1841 dst[base] = @truncate(pixel);
1842 dst[base + 1] = @truncate(pixel >> 8);
1843 dst[base + 2] = @truncate(pixel >> 16);
1844 dst[base + 3] = @truncate(pixel >> 24);
1845 }
1846
1847 test "paint Executor capacity matches an independent host and device byte model" {
1848 comptime {
1849 @stardustClaim(
1850 @import("alloc_phase").capacity.witness(ExecutorHostStorage, "gui_paint_executor_capacity_capacity_model"),
1851 null,
1852 null,
1853 null,
1854 null,
1855 null,
1856 null,
1857 );
1858 }
1859 comptime {
1860 @stardustClaim(
1861 @import("alloc_phase").capacity.witness(ExecutorHostStorage, "gui_paint_executor_capacity_overload"),
1862 null,
1863 null,
1864 null,
1865 null,
1866 null,
1867 null,
1868 );
1869 }
1870
1871 const cases = [_]Limits{
1872 .{ .mode = .host_bins },
1873 .{ .mode = .host_bins, .commands = 7, .pixels = 4096, .images = 3, .image_pixels = 600, .tiles = 16, .tile_pairs = 41 },
1874 .{ .mode = .device_csr, .commands = 11, .pixels = 2304, .images = 2, .image_pixels = 257, .tiles = 9, .tile_pairs = 99 },
1875 };
1876 for (cases) |limits| {
1877 const capacity = try Capacity.derive(limits);
1878 const commands = @max(@as(u128, limits.commands), 1);
1879 const pixels = @max(@as(u128, limits.pixels), 1);
1880 const images = @max(@as(u128, limits.images), 1);
1881 const image_pixels = @max(@as(u128, limits.image_pixels), 1);
1882 const tile_ranges = @max(@as(u128, limits.commands) * accy_paint.tile_range_lanes, 1);
1883 const tile_offsets = @max(@as(u128, limits.tiles) + 1, 2);
1884 const tiles = @max(@as(u128, limits.tiles), 1);
1885 const tile_pairs = @max(@as(u128, limits.tile_pairs), 1);
1886 const floats = commands * accy_paint.float_lanes;
1887 const words = commands * accy_paint.word_lanes;
1888 const metadata = images * accy_paint.image_lanes;
1889 const host_bins = if (limits.mode == .host_bins) tile_ranges + tile_offsets + tile_pairs + tiles else 0;
1890 const host_elements = floats + words + metadata + image_pixels + pixels + tile_offsets + host_bins;
1891 const common_device = pixels + floats + words + metadata + image_pixels + tile_offsets + tile_pairs;
1892 const device_csr = if (limits.mode == .device_csr)
1893 tile_ranges + tile_offsets + tiles + 2 * scan_library.device_scan_max_blocks
1894 else
1895 0;
1896 try std.testing.expectEqual(@as(usize, @intCast(host_elements * @sizeOf(u32))), capacity.host_storage_bytes);
1897 try std.testing.expectEqual(@as(usize, @intCast((common_device + device_csr) * @sizeOf(u32))), capacity.device_storage_bytes);
1898 try std.testing.expectEqual(
1899 capacity.host_storage_bytes + capacity.device_storage_bytes,
1900 capacity.total_storage_bytes,
1901 );
1902 try std.testing.expectEqual(@as(usize, if (limits.mode == .device_csr) 12 else 7), capacity.device_buffer_count);
1903 }
1904 }
1905
1906 test "paint Executor rejects overflowing storage limits" {
1907 try std.testing.expectError(
1908 error.CapacityOverflow,
1909 Limits.worstCase(.host_bins, std.math.maxInt(usize), 1, 0, 0, 2),
1910 );
1911 try std.testing.expectError(
1912 error.CapacityOverflow,
1913 Capacity.derive(.{ .mode = .host_bins, .commands = std.math.maxInt(usize) }),
1914 );
1915 }
1916
1917 test "paint Executor rejects initial storage for another artifact mode" {
1918 const allocator = std.testing.allocator;
1919 var state = gpu.recording.BackendState{
1920 .allocator = allocator,
1921 .kind = .vulkan,
1922 .format = .vulkan_spirv,
1923 };
1924 try std.testing.expectError(
1925 error.StorageModeMismatch,
1926 Executor.init(allocator, state.handle(), .{
1927 .artifact_format = .vulkan_spirv,
1928 .initial_storage = .{ .mode = .host_bins },
1929 }),
1930 );
1931 }
1932
1933 test "paint Executor host storage rejects initialization OOM and seals on retry" {
1934 comptime {
1935 @stardustClaim(
1936 @import("alloc_phase").capacity.witness(ExecutorHostStorage, "gui_paint_executor_host_oom"),
1937 null,
1938 null,
1939 null,
1940 null,
1941 null,
1942 null,
1943 );
1944 }
1945
1946 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
1947 const limits = Limits{
1948 .mode = .device_csr,
1949 .commands = 1,
1950 .pixels = 16,
1951 .tiles = 1,
1952 .tile_pairs = 1,
1953 };
1954 failing.fail_index = failing.alloc_index;
1955 try std.testing.expectError(
1956 error.OutOfMemory,
1957 ExecutorHostStorage.init(failing.allocator(), limits),
1958 );
1959 try std.testing.expect(failing.has_induced_failure);
1960 failing.fail_index = std.math.maxInt(usize);
1961 var storage = try ExecutorHostStorage.init(failing.allocator(), limits);
1962 defer storage.deinit(failing.allocator());
1963 try std.testing.expectEqual(alloc_phase.capacity.Phase.initialization, storage.phase);
1964 storage.activate();
1965 try std.testing.expectEqual(alloc_phase.capacity.Phase.steady, storage.phase);
1966 try std.testing.expectEqual(storage.capacity.host_storage_bytes, storage.bytes.len);
1967 }
1968
1969 test "paint Executor initial storage exposes the exact chosen capacity" {
1970 comptime {
1971 @stardustClaim(
1972 @import("alloc_phase").capacity.witness(ExecutorHostStorage, "gui_paint_executor_acquisition"),
1973 null,
1974 null,
1975 null,
1976 null,
1977 null,
1978 null,
1979 );
1980 }
1981
1982 const allocator = std.testing.allocator;
1983 var state = gpu.recording.BackendState{
1984 .allocator = allocator,
1985 .kind = .vulkan,
1986 .format = .vulkan_spirv,
1987 };
1988 const limits = Limits{
1989 .mode = .device_csr,
1990 .commands = 3,
1991 .pixels = 64,
1992 .images = 2,
1993 .image_pixels = 32,
1994 .tiles = 4,
1995 .tile_pairs = 12,
1996 };
1997 var executor = try Executor.init(allocator, state.handle(), .{ .initial_storage = limits });
1998 defer executor.deinit();
1999 const status = executor.storageStatus();
2000 try std.testing.expectEqual(limits, status.limits.?);
2001 try std.testing.expectEqual(try Capacity.derive(limits), status.capacity.?);
2002 try std.testing.expectEqual(@as(usize, 0), status.replacements);
2003 try std.testing.expectEqual(status.capacity.?.host_storage_bytes, executor.buffers.?.host.bytes.len);
2004 try std.testing.expectEqual(status.capacity.?.device_buffer_count, state.buffer_allocate_count);
2005 }
2006
2007 test "paint Executor eagerly acquires and reports configured image storage" {
2008 const allocator = std.testing.allocator;
2009 var state = gpu.recording.BackendState{
2010 .allocator = allocator,
2011 .kind = .vulkan,
2012 .format = .vulkan_spirv,
2013 };
2014 const limits = paint_image.Limits{
2015 .dst_pixels = 64,
2016 .src_pixels = 32,
2017 .scratch_pixels = 64,
2018 .weight_taps = 7,
2019 };
2020 var executor = try Executor.init(allocator, state.handle(), .{ .image_storage = limits });
2021 defer executor.deinit();
2022 const status = executor.storageStatus();
2023 try std.testing.expectEqual(@as(?Limits, null), status.limits);
2024 try std.testing.expectEqual(limits, status.image_processor.?.limits.?);
2025 try std.testing.expectEqual(try paint_image.Capacity.derive(limits), status.image_processor.?.capacity.?);
2026 try std.testing.expectEqual(status.image_processor.?.capacity.?.device_buffer_count, state.buffer_allocate_count);
2027 }
2028
2029 test "paint Executor replaces storage at max plus one and retains its high water mark" {
2030 comptime {
2031 @stardustClaim(
2032 @import("alloc_phase").capacity.witness(ExecutorHostStorage, "gui_paint_executor_boundary"),
2033 null,
2034 null,
2035 null,
2036 null,
2037 null,
2038 null,
2039 );
2040 }
2041
2042 const allocator = std.testing.allocator;
2043 var state = gpu.recording.BackendState{
2044 .allocator = allocator,
2045 .kind = .vulkan,
2046 .format = .vulkan_spirv,
2047 };
2048 const limits = Limits{
2049 .mode = .device_csr,
2050 .commands = 1,
2051 .pixels = 16,
2052 .tiles = 1,
2053 .tile_pairs = 1,
2054 };
2055 var executor = try Executor.init(allocator, state.handle(), .{ .initial_storage = limits });
2056 defer executor.deinit();
2057 const commands = [_]Command{.{
2058 .kind = .fill,
2059 .rect = .{ .x = 0, .y = 0, .width = 5, .height = 5 },
2060 .clip = .{ .x = 0, .y = 0, .width = 5, .height = 5 },
2061 .color = .{ .a = 255 },
2062 }};
2063 _ = try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{}, .{}, cpu.Region.full(4, 4));
2064 try std.testing.expectEqual(@as(usize, 0), executor.storageStatus().replacements);
2065 _ = try executor.prepareCommandsPackedLaunch(commands[0..], 5, 5, .{}, .{}, cpu.Region.full(5, 5));
2066 const grown = executor.storageStatus();
2067 try std.testing.expectEqual(@as(usize, 1), grown.replacements);
2068 try std.testing.expectEqual(@as(usize, 25), grown.limits.?.pixels);
2069 _ = try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{}, .{}, cpu.Region.full(4, 4));
2070 try std.testing.expectEqual(grown, executor.storageStatus());
2071 }
2072
2073 test "paint Executor failed storage replacement preserves the prior epoch" {
2074 comptime {
2075 @stardustClaim(
2076 @import("alloc_phase").capacity.witness(ExecutorHostStorage, "gui_paint_executor_atomic"),
2077 null,
2078 null,
2079 null,
2080 null,
2081 null,
2082 null,
2083 );
2084 }
2085
2086 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
2087 var state = gpu.recording.BackendState{
2088 .allocator = failing.allocator(),
2089 .kind = .vulkan,
2090 .format = .vulkan_spirv,
2091 };
2092 const limits = Limits{
2093 .mode = .device_csr,
2094 .commands = 1,
2095 .pixels = 16,
2096 .tiles = 1,
2097 .tile_pairs = 1,
2098 };
2099 var executor = try Executor.init(failing.allocator(), state.handle(), .{ .initial_storage = limits });
2100 defer executor.deinit();
2101 const before = executor.storageStatus();
2102 const pixels_id = executor.buffers.?.pixels.id;
2103 const commands = [_]Command{.{
2104 .kind = .fill,
2105 .rect = .{ .x = 0, .y = 0, .width = 17, .height = 17 },
2106 .clip = .{ .x = 0, .y = 0, .width = 17, .height = 17 },
2107 .color = .{ .a = 255 },
2108 }};
2109 failing.fail_index = failing.alloc_index;
2110 try std.testing.expectError(
2111 error.OutOfMemory,
2112 executor.prepareCommandsPackedLaunch(commands[0..], 17, 17, .{}, .{}, cpu.Region.full(17, 17)),
2113 );
2114 try std.testing.expect(failing.has_induced_failure);
2115 try std.testing.expectEqual(before, executor.storageStatus());
2116 try std.testing.expectEqual(pixels_id, executor.buffers.?.pixels.id);
2117 failing.fail_index = std.math.maxInt(usize);
2118 _ = try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{}, .{}, cpu.Region.full(4, 4));
2119 }
2120
2121 test "paint Executor admitted prepared launch makes no allocator calls" {
2122 comptime {
2123 @stardustClaim(
2124 @import("alloc_phase").capacity.witness(ExecutorHostStorage, "gui_paint_executor_steady"),
2125 null,
2126 null,
2127 null,
2128 null,
2129 null,
2130 null,
2131 );
2132 }
2133
2134 var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{});
2135 var state = gpu.recording.BackendState{
2136 .allocator = failing.allocator(),
2137 .kind = .vulkan,
2138 .format = .vulkan_spirv,
2139 };
2140 const limits = Limits{
2141 .mode = .device_csr,
2142 .commands = 1,
2143 .pixels = 16,
2144 .tiles = 1,
2145 .tile_pairs = 1,
2146 };
2147 var executor = try Executor.init(failing.allocator(), state.handle(), .{ .initial_storage = limits });
2148 defer executor.deinit();
2149 const commands = [_]Command{.{
2150 .kind = .fill,
2151 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2152 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2153 .color = .{ .a = 255 },
2154 }};
2155 failing.fail_index = failing.alloc_index;
2156 failing.resize_fail_index = failing.resize_index;
2157 _ = try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{}, .{}, cpu.Region.full(4, 4));
2158 try std.testing.expect(!failing.has_induced_failure);
2159 }
2160
2161 test "paint executor initializes native recording artifact" {
2162 const allocator = std.testing.allocator;
2163 inline for (.{ gpu.ArtifactFormat.cuda_ptx, .vulkan_spirv, .metal_msl }) |format| {
2164 var state = gpu.recording.BackendState{
2165 .allocator = allocator,
2166 .kind = switch (format) {
2167 .cuda_ptx => .cuda,
2168 .vulkan_spirv => .vulkan,
2169 .metal_msl => .metal,
2170 else => unreachable,
2171 },
2172 .format = format,
2173 };
2174 var executor = try Executor.init(allocator, state.handle(), .{});
2175 var cleanup = true;
2176 defer if (cleanup) executor.deinit();
2177 const loaded_id = executor.loaded.id;
2178 try std.testing.expectEqual(format, executor.artifact.format);
2179 try std.testing.expectEqual(@as(usize, 4), state.create_count);
2180 try std.testing.expectEqual(@as(usize, 7), state.load_count);
2181 executor.deinit();
2182 cleanup = false;
2183 try std.testing.expectEqual(@as(usize, 7), state.destroy_count);
2184 try std.testing.expectEqual(loaded_id, state.last_destroyed_id.?);
2185 }
2186 }
2187
2188 test "paint executor initializes native CPU object artifact" {
2189 const allocator = std.testing.allocator;
2190 var state = gpu.cpu.State.init(allocator);
2191 defer state.deinit();
2192
2193 var executor = try Executor.init(allocator, state.handle(), .{
2194 .artifact_format = .cpu_object,
2195 });
2196 defer executor.deinit();
2197
2198 try std.testing.expectEqual(gpu.ArtifactFormat.cpu_object, executor.artifact.format);
2199 }
2200
2201 test "paint executor renders packed commands with native CPU object artifact" {
2202 const allocator = std.testing.allocator;
2203 var state = gpu.cpu.State.init(allocator);
2204 defer state.deinit();
2205
2206 var executor = try Executor.init(allocator, state.handle(), .{
2207 .artifact_format = .cpu_object,
2208 });
2209 defer executor.deinit();
2210
2211 const commands = [_]Command{.{
2212 .kind = .fill,
2213 .rect = .{ .x = 1, .y = 1, .width = 2, .height = 2 },
2214 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2215 .color = .{ .r = 20, .g = 40, .b = 60, .a = 255 },
2216 }};
2217 var expected = @as([(4 * 4)]u32, @splat(0));
2218 var actual = @as([(4 * 4)]u32, @splat(0));
2219 const clear = Color{ .r = 1, .g = 2, .b = 3, .a = 255 };
2220
2221 try cpu.renderCommandsPacked(commands[0..], .{ .width = 4, .height = 4, .pixels = expected[0..] }, clear);
2222 try executor.renderCommandsPacked(commands[0..], .{ .width = 4, .height = 4, .pixels = actual[0..] }, clear);
2223
2224 try std.testing.expectEqualSlices(u32, expected[0..], actual[0..]);
2225 }
2226
2227 test "paint executor renders packed commands with image shadows through CPU object artifact" {
2228 const allocator = std.testing.allocator;
2229 var state = gpu.cpu.State.init(allocator);
2230 defer state.deinit();
2231
2232 var actual_executor = try Executor.init(allocator, state.handle(), .{
2233 .artifact_format = .cpu_object,
2234 });
2235 defer actual_executor.deinit();
2236 var expected_executor = try Executor.init(allocator, state.handle(), .{
2237 .artifact_format = .cpu_object,
2238 });
2239 defer expected_executor.deinit();
2240 var processor = try paint_image.Processor.init(allocator, state.handle(), .{
2241 .artifact_format = .cpu_object,
2242 });
2243 defer processor.deinit();
2244
2245 const commands = [_]Command{
2246 .{
2247 .kind = .shadow,
2248 .rect = .{ .x = 3, .y = 2, .width = 4, .height = 3 },
2249 .clip = .{ .x = 0, .y = 0, .width = 12, .height = 10 },
2250 .color = .{ .r = 90, .g = 10, .b = 20, .a = 180 },
2251 .radius = 1,
2252 .width = 1,
2253 .order = 0,
2254 },
2255 .{
2256 .kind = .fill,
2257 .rect = .{ .x = 5, .y = 4, .width = 3, .height = 2 },
2258 .clip = .{ .x = 0, .y = 0, .width = 12, .height = 10 },
2259 .color = .{ .r = 20, .g = 120, .b = 60, .a = 220 },
2260 .order = 1,
2261 },
2262 };
2263 const clear = Color{ .r = 1, .g = 2, .b = 3, .a = 255 };
2264 var actual = @as([(12 * 10)]u32, @splat(0));
2265 var expected = @as([(12 * 10)]u32, @splat(0));
2266
2267 try actual_executor.renderCommandsPackedWithImageShadows(commands[0..], .{
2268 .width = 12,
2269 .height = 10,
2270 .pixels = actual[0..],
2271 }, clear, .{}, .{});
2272
2273 var expansion = try processor.expandShadowsAlloc(commands[0..], .{}, .{});
2274 defer expansion.deinit();
2275 try expected_executor.renderCommandsPackedWithImages(expansion.commands, .{
2276 .width = 12,
2277 .height = 10,
2278 .pixels = expected[0..],
2279 }, clear, expansion.imageSet());
2280
2281 try std.testing.expectEqualSlices(u32, expected[0..], actual[0..]);
2282 try std.testing.expect(actual[3 * 12 + 4] != cpu.packRgba(clear));
2283 }
2284
2285 test "paint executor launches packed commands through backend handle" {
2286 const allocator = std.testing.allocator;
2287 var state = gpu.recording.BackendState{
2288 .allocator = allocator,
2289 .kind = .vulkan,
2290 .format = .vulkan_spirv,
2291 };
2292 var executor = try Executor.init(allocator, state.handle(), .{});
2293 defer executor.deinit();
2294 var pixels = @as([(8 * 5)]u32, @splat(0xffff_ffff));
2295 const commands = [_]Command{
2296 .{
2297 .kind = .fill,
2298 .rect = .{ .x = 1, .y = 1, .width = 3, .height = 2 },
2299 .clip = .{ .x = 0, .y = 0, .width = 8, .height = 5 },
2300 .color = .{ .r = 11, .g = 22, .b = 33, .a = 44 },
2301 },
2302 .{
2303 .kind = .stroke,
2304 .rect = .{ .x = 2, .y = 1, .width = 4, .height = 3 },
2305 .clip = .{ .x = 0, .y = 0, .width = 8, .height = 5 },
2306 .color = .{ .r = 99, .g = 88, .b = 77, .a = 66 },
2307 .radius = 2,
2308 .width = 1,
2309 },
2310 };
2311
2312 try executor.renderCommandsPacked(commands[0..], .{ .width = 8, .height = 5, .pixels = pixels[0..] }, .{
2313 .r = 1,
2314 .g = 2,
2315 .b = 3,
2316 .a = 4,
2317 });
2318
2319 try std.testing.expectEqual(@as(usize, 12), state.buffer_allocate_count);
2320 try std.testing.expectEqual(@as(usize, 4), state.write_count);
2321 try std.testing.expectEqual(@as(usize, 7), state.launch_count);
2322 try std.testing.expectEqual(@as(usize, 1), state.sync_count);
2323 try std.testing.expectEqual(@as(usize, 1), state.read_count);
2324 try std.testing.expectEqual(@as(usize, 7), state.last_launch_buffer_count);
2325 try std.testing.expectEqual(@as(usize, 11), state.last_launch_scalar_count);
2326 try std.testing.expectEqual(gpu.BufferAccess.read_write, state.last_buffer_access[0]);
2327 try std.testing.expectEqual(gpu.BufferAccess.read_only, state.last_buffer_access[1]);
2328 try std.testing.expectEqual(gpu.BufferAccess.read_only, state.last_buffer_access[2]);
2329 try std.testing.expectEqual(gpu.BufferAccess.read_only, state.last_buffer_access[3]);
2330 try std.testing.expectEqual(gpu.BufferAccess.read_only, state.last_buffer_access[4]);
2331 try std.testing.expectEqual(gpu.BufferAccess.read_only, state.last_buffer_access[5]);
2332 try std.testing.expectEqual(gpu.BufferAccess.read_only, state.last_buffer_access[6]);
2333 try std.testing.expectEqual(@as(u32, 0), state.last_launch_scalar_u32_values[0]);
2334 try std.testing.expectEqual(@as(u32, 1), state.last_launch_scalar_u32_values[1]);
2335 try std.testing.expectEqual(@as(u32, 2), state.last_launch_scalar_u32_values[2]);
2336 try std.testing.expectEqual(@as(u32, 3), state.last_launch_scalar_u32_values[3]);
2337 try std.testing.expectEqual(@as(u32, 4), state.last_launch_scalar_u32_values[4]);
2338 try std.testing.expectEqual(@as(u32, 0), state.last_launch_scalar_u32_values[5]);
2339 try std.testing.expectEqual(@as(u32, 0), state.last_launch_scalar_u32_values[6]);
2340 try std.testing.expectEqual(@as(u32, 8), state.last_launch_scalar_u32_values[7]);
2341 try std.testing.expectEqual(@as(u32, 40), state.last_launch_scalar_u32_values[8]);
2342 try std.testing.expectEqual(@as(u32, 1), state.last_launch_scalar_u32_values[9]);
2343 try std.testing.expectEqual(@as(u32, @backingInt(accy_paint.OutputFormat.rgba)), state.last_launch_scalar_u32_values[10]);
2344 try std.testing.expectEqual(@as(u32, 1), state.last_launch_grid[0]);
2345 try std.testing.expectEqual(@as(u32, accy_paint.default_threads), state.last_launch_threadgroup[0]);
2346 try std.testing.expectEqual(@as(usize, pixels.len * @sizeOf(u32)), state.last_read_byte_count);
2347 try std.testing.expectEqualSlices(u32, &@as([(8 * 5)]u32, @splat(0)), pixels[0..]);
2348 }
2349
2350 test "paint executor exposes prepared packed launch after device CSR preparation" {
2351 const allocator = std.testing.allocator;
2352 var state = gpu.recording.BackendState{
2353 .allocator = allocator,
2354 .kind = .vulkan,
2355 .format = .vulkan_spirv,
2356 };
2357 var executor = try Executor.init(allocator, state.handle(), .{});
2358 defer executor.deinit();
2359 const commands = [_]Command{.{
2360 .kind = .fill,
2361 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2362 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2363 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
2364 }};
2365
2366 var prepared = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2367
2368 try std.testing.expectEqual(@as(usize, 4), state.write_count);
2369 try std.testing.expectEqual(@as(usize, 6), state.launch_count);
2370 try std.testing.expectEqual(@as(usize, 0), state.sync_count);
2371 try std.testing.expectEqual(@as(usize, 0), state.read_count);
2372 try std.testing.expectEqual(executor.buffers.?.tile_cursors.?.id, state.last_write_buffer_id.?);
2373 try std.testing.expectEqual(executor.tile_index.?.loaded.id, state.last_launch_loaded_id.?);
2374 try std.testing.expectEqual(@as(usize, 7), prepared.bindings.len);
2375 try std.testing.expectEqual(@as(usize, 11), prepared.scalar_count);
2376 try std.testing.expectEqual(@as(u32, 16), prepared.scalar_storage[8].u32);
2377 try std.testing.expectEqual(@as(u32, @backingInt(accy_paint.OutputFormat.rgba)), prepared.scalar_storage[10].u32);
2378 try std.testing.expectEqual(@as(u32, 1), prepared.geometry.grid[0]);
2379 try std.testing.expectEqual(@as(u32, accy_paint.default_threads), prepared.geometry.threadgroup[0]);
2380 try std.testing.expectEqual(@as(usize, 0), prepared.command_visits);
2381 try std.testing.expect(prepared.device_csr_prepared);
2382
2383 try executor.submitPreparedLaunchQueued(&prepared);
2384
2385 try std.testing.expectEqual(@as(usize, 7), state.launch_count);
2386 try std.testing.expectEqual(@as(usize, 0), state.sync_count);
2387 try std.testing.expectEqual(@as(usize, 0), state.read_count);
2388 try std.testing.expectEqual(@as(usize, 7), state.last_launch_buffer_count);
2389 try std.testing.expectEqual(@as(usize, 11), state.last_launch_scalar_count);
2390
2391 try executor.submitPreparedLaunch(&prepared);
2392
2393 try std.testing.expectEqual(@as(usize, 8), state.launch_count);
2394 try std.testing.expectEqual(@as(usize, 1), state.sync_count);
2395 try std.testing.expectEqual(@as(usize, 0), state.read_count);
2396 }
2397
2398 test "paint executor preserves prepared launch generations across resident device prepares" {
2399 const allocator = std.testing.allocator;
2400 var state = gpu.recording.BackendState{
2401 .allocator = allocator,
2402 .kind = .vulkan,
2403 .format = .vulkan_spirv,
2404 };
2405 var executor = try Executor.init(allocator, state.handle(), .{});
2406 defer executor.deinit();
2407 const commands = [_]Command{.{
2408 .kind = .fill,
2409 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2410 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2411 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
2412 }};
2413
2414 var first = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2415 try std.testing.expect(first.device_csr_prepared);
2416 const first_generation = first.generation;
2417 const first_write_count = state.write_count;
2418 const first_launch_count = state.launch_count;
2419
2420 const second = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2421 try std.testing.expectEqual(first_generation, second.generation);
2422 try std.testing.expect(!second.device_csr_prepared);
2423 try std.testing.expectEqual(first_write_count, state.write_count);
2424 try std.testing.expectEqual(first_launch_count, state.launch_count);
2425
2426 const info = try executor.preparedLaunchInfo(&first);
2427 try std.testing.expectEqual(@as(usize, 16), info.pixel_count);
2428 try executor.submitPreparedLaunchQueued(&first);
2429 try std.testing.expectEqual(first_launch_count + 1, state.launch_count);
2430
2431 const changed = [_]Command{.{
2432 .kind = .fill,
2433 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2434 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2435 .color = .{ .r = 8, .g = 8, .b = 9, .a = 255 },
2436 }};
2437 const third = (try executor.prepareCommandsPackedLaunch(changed[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2438 try std.testing.expectEqual(first_generation + 1, third.generation);
2439 try std.testing.expect(third.device_csr_prepared);
2440 try std.testing.expect(state.write_count > first_write_count);
2441 try std.testing.expectError(error.StalePreparedLaunch, executor.preparedLaunchInfo(&first));
2442 }
2443
2444 test "paint executor preserves prepared launch generations across resident host-loop prepares" {
2445 const allocator = std.testing.allocator;
2446 var state = gpu.cpu.State.init(allocator);
2447 defer state.deinit();
2448
2449 var executor = try Executor.init(allocator, state.handle(), .{
2450 .artifact_format = .cpu_object,
2451 });
2452 defer executor.deinit();
2453 const commands = [_]Command{};
2454 const first_clear = Color{ .r = 3, .g = 5, .b = 7, .a = 255 };
2455 const second_clear = Color{ .r = 11, .g = 13, .b = 17, .a = 255 };
2456
2457 var first = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, first_clear, .{}, cpu.Region.full(4, 4))).?;
2458 try std.testing.expect(!first.device_csr_prepared);
2459 try std.testing.expectEqual(@as(usize, 0), first.command_visits);
2460 const first_generation = first.generation;
2461
2462 var second = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, second_clear, .{}, cpu.Region.full(4, 4))).?;
2463 try std.testing.expectEqual(first_generation, second.generation);
2464 try std.testing.expect(!second.device_csr_prepared);
2465 try std.testing.expectEqual(@as(usize, 0), second.command_visits);
2466
2467 try executor.submitPreparedLaunch(&first);
2468 const first_pixels = try executor.readPreparedPackedPixels(&first);
2469 try std.testing.expectEqual(cpu.packRgba(first_clear), first_pixels[0]);
2470
2471 try executor.submitPreparedLaunch(&second);
2472 const second_pixels = try executor.readPreparedPackedPixels(&second);
2473 try std.testing.expectEqual(cpu.packRgba(second_clear), second_pixels[0]);
2474
2475 const changed = [_]Command{.{
2476 .kind = .fill,
2477 .rect = .{ .x = 0, .y = 0, .width = 2, .height = 2 },
2478 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2479 .color = .{ .r = 19, .g = 23, .b = 29, .a = 255 },
2480 }};
2481 const third = (try executor.prepareCommandsPackedLaunch(changed[0..], 4, 4, first_clear, .{}, cpu.Region.full(4, 4))).?;
2482 try std.testing.expectEqual(first_generation + 1, third.generation);
2483 try std.testing.expectError(error.StalePreparedLaunch, executor.preparedLaunchInfo(&first));
2484 }
2485
2486 test "paint executor reuses resident host-loop tile bins without rebuilding scratch" {
2487 const allocator = std.testing.allocator;
2488 var state = gpu.cpu.State.init(allocator);
2489 defer state.deinit();
2490
2491 var executor = try Executor.init(allocator, state.handle(), .{
2492 .artifact_format = .cpu_object,
2493 });
2494 defer executor.deinit();
2495 const commands = [_]Command{.{
2496 .kind = .fill,
2497 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2498 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2499 .color = .{ .r = 19, .g = 23, .b = 29, .a = 255 },
2500 }};
2501 const clear = Color{ .a = 0 };
2502
2503 const first = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, clear, .{}, cpu.Region.full(4, 4))).?;
2504 try std.testing.expect(first.command_visits > 0);
2505 try std.testing.expect(executor.buffers.?.host_bins.ranges.len != 0);
2506 const first_generation = first.generation;
2507 const first_tile_index_count = executor.buffers.?.tile_csr.tile_index_count;
2508 const first_command_visits = executor.buffers.?.tile_csr.command_visits;
2509
2510 const retained_host_bins = executor.buffers.?.host_bins;
2511 executor.buffers.?.host_bins = .{};
2512
2513 const second = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, clear, .{}, cpu.Region.full(4, 4))).?;
2514 try std.testing.expectEqual(first_generation, second.generation);
2515 try std.testing.expectEqual(first_tile_index_count, executor.buffers.?.tile_csr.tile_index_count);
2516 try std.testing.expectEqual(first_command_visits, second.command_visits);
2517 try std.testing.expectEqual(@as(usize, 0), executor.buffers.?.host_bins.ranges.len);
2518 executor.buffers.?.host_bins = retained_host_bins;
2519
2520 try executor.submitPreparedLaunch(&second);
2521 const pixels = try executor.readPreparedPackedPixels(&second);
2522 try std.testing.expectEqual(cpu.packRgba(commands[0].color), pixels[0]);
2523 }
2524
2525 test "paint executor times prepared launches through retained owner" {
2526 const allocator = std.testing.allocator;
2527 var state = gpu.recording.BackendState{
2528 .allocator = allocator,
2529 .kind = .vulkan,
2530 .format = .vulkan_spirv,
2531 .event_elapsed_ns = 123_456,
2532 };
2533 var executor = try Executor.init(allocator, state.handle(), .{});
2534 defer executor.deinit();
2535 const commands = [_]Command{.{
2536 .kind = .fill,
2537 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2538 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2539 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
2540 }};
2541
2542 var prepared = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2543 const launch_count = state.launch_count;
2544 const timing = try executor.submitPreparedLaunchTimed(&prepared);
2545
2546 try std.testing.expectEqual(@as(u64, 123_456), timing.elapsed_ns);
2547 try std.testing.expectEqual(@as(usize, 1), state.created_stream_count);
2548 try std.testing.expectEqual(@as(usize, 2), state.created_event_count);
2549 try std.testing.expectEqual(@as(usize, 2), state.record_event_count);
2550 try std.testing.expectEqual(@as(usize, 1), state.elapsed_event_count);
2551 try std.testing.expectEqual(@as(usize, 1), state.sync_count);
2552 try std.testing.expectEqual(launch_count + 1, state.launch_count);
2553 try std.testing.expectEqual(timing.stream_id, state.last_launch_stream.?);
2554 try std.testing.expectEqual(timing.stream_id, state.record_streams[0].?);
2555 try std.testing.expectEqual(timing.stream_id, state.record_streams[1].?);
2556 try std.testing.expectEqual(state.record_events[0].?, state.elapsed_start_events[0].?);
2557 try std.testing.expectEqual(state.record_events[1].?, state.elapsed_end_events[0].?);
2558 }
2559
2560 test "paint executor describes prepared launches through retained owner" {
2561 const allocator = std.testing.allocator;
2562 var state = gpu.recording.BackendState{
2563 .allocator = allocator,
2564 .kind = .vulkan,
2565 .format = .vulkan_spirv,
2566 };
2567 var executor = try Executor.init(allocator, state.handle(), .{});
2568 defer executor.deinit();
2569 const commands = [_]Command{.{
2570 .kind = .fill,
2571 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2572 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2573 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
2574 }};
2575
2576 const prepared = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2577 const info = try executor.preparedLaunchInfo(&prepared);
2578
2579 try std.testing.expect(info.entry_name.len != 0);
2580 try std.testing.expectEqual(gpu.ArtifactFormat.vulkan_spirv, info.artifact_format);
2581 try std.testing.expectEqual(artifactPayloadBytes(&executor.artifact), info.artifact_payload_bytes);
2582 try std.testing.expectEqual(@as(usize, 16), info.pixel_count);
2583 try std.testing.expectEqual(@as(u32, 1), info.geometry.grid[0]);
2584 try std.testing.expectEqual(@as(u32, accy_paint.default_threads), info.geometry.threadgroup[0]);
2585 try std.testing.expect(info.device_csr_prepared);
2586 }
2587
2588 test "paint executor reads prepared packed launches through retained capacity" {
2589 const allocator = std.testing.allocator;
2590 var state = gpu.recording.BackendState{
2591 .allocator = allocator,
2592 .kind = .vulkan,
2593 .format = .vulkan_spirv,
2594 };
2595 var executor = try Executor.init(allocator, state.handle(), .{});
2596 defer executor.deinit();
2597 const commands = [_]Command{};
2598
2599 var large = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2600 try executor.submitPreparedLaunch(&large);
2601 const large_pixels = try executor.readPreparedPackedPixels(&large);
2602 try std.testing.expectEqual(@as(usize, 16), large_pixels.len);
2603 try std.testing.expectEqual(@as(usize, 1), state.read_count);
2604 try std.testing.expectEqual(@as(usize, 16 * @sizeOf(u32)), state.last_read_byte_count);
2605 const retained_pixel_capacity = executor.buffers.?.pixel_capacity;
2606 try std.testing.expectEqual(@as(usize, 16), retained_pixel_capacity);
2607
2608 var small = (try executor.prepareCommandsPackedLaunch(commands[0..], 2, 2, .{ .a = 0 }, .{}, cpu.Region.full(2, 2))).?;
2609 try executor.submitPreparedLaunch(&small);
2610 const small_pixels = try executor.readPreparedPackedPixels(&small);
2611 try std.testing.expectEqual(@as(usize, 4), small_pixels.len);
2612 try std.testing.expectEqual(@as(usize, 2), state.read_count);
2613 try std.testing.expectEqual(@as(usize, 16 * @sizeOf(u32)), state.last_read_byte_count);
2614 try std.testing.expectEqual(retained_pixel_capacity, executor.buffers.?.pixel_capacity);
2615 }
2616
2617 test "paint executor rejects stale prepared launch generations" {
2618 const allocator = std.testing.allocator;
2619 var state = gpu.recording.BackendState{
2620 .allocator = allocator,
2621 .kind = .vulkan,
2622 .format = .vulkan_spirv,
2623 };
2624 const handle = state.handle();
2625 var executor = try Executor.init(allocator, handle, .{});
2626 defer executor.deinit();
2627 const commands = [_]Command{.{
2628 .kind = .fill,
2629 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2630 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2631 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
2632 }};
2633
2634 var first = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2635 try std.testing.expectEqual(@as(u64, 1), first.generation);
2636 const first_pixels_id = first.pixels.id;
2637
2638 var second = (try executor.prepareCommandsPackedLaunch(commands[0..], 8, 8, .{ .a = 0 }, .{}, cpu.Region.full(8, 8))).?;
2639 try std.testing.expectEqual(@as(u64, 2), second.generation);
2640 try std.testing.expect(first_pixels_id != second.pixels.id);
2641
2642 const launch_count = state.launch_count;
2643 const read_count = state.read_count;
2644 const surface_write_count = state.surface_write_count;
2645 try std.testing.expectError(error.StalePreparedLaunch, executor.preparedLaunchInfo(&first));
2646 try std.testing.expectError(error.StalePreparedLaunch, executor.submitPreparedLaunchTimed(&first));
2647 try std.testing.expectError(error.StalePreparedLaunch, executor.submitPreparedLaunchQueued(&first));
2648 try std.testing.expectError(error.StalePreparedLaunch, executor.readPreparedPackedPixels(&first));
2649
2650 const surface = try handle.createSurface(.{
2651 .platform = .{ .headless = .{} },
2652 .extent = .{ .width = 4, .height = 4 },
2653 .format = .rgba8_unorm,
2654 .usage = .{ .present = true, .copy_dst = true },
2655 });
2656 const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
2657 try std.testing.expectError(error.StalePreparedLaunch, executor.writePreparedSurfaceFrame(frame, &first));
2658 try std.testing.expectEqual(launch_count, state.launch_count);
2659 try std.testing.expectEqual(read_count, state.read_count);
2660 try std.testing.expectEqual(surface_write_count, state.surface_write_count);
2661
2662 try executor.submitPreparedLaunchQueued(&second);
2663 try std.testing.expectEqual(launch_count + 1, state.launch_count);
2664
2665 try handle.presentSurfaceFrame(.{
2666 .surface = surface,
2667 .frame = frame,
2668 });
2669 try handle.destroySurface(surface);
2670 }
2671
2672 test "paint executor rejects prepared launches from another executor" {
2673 const allocator = std.testing.allocator;
2674 var state = gpu.recording.BackendState{
2675 .allocator = allocator,
2676 .kind = .vulkan,
2677 .format = .vulkan_spirv,
2678 };
2679 const handle = state.handle();
2680 var first_executor = try Executor.init(allocator, handle, .{});
2681 defer first_executor.deinit();
2682 var second_executor = try Executor.init(allocator, handle, .{});
2683 defer second_executor.deinit();
2684 const commands = [_]Command{.{
2685 .kind = .fill,
2686 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2687 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2688 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
2689 }};
2690
2691 var first_prepared = (try first_executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2692 var second_prepared = (try second_executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2693 try std.testing.expectEqual(@as(u64, 1), first_prepared.generation);
2694 try std.testing.expectEqual(first_prepared.generation, second_prepared.generation);
2695 try std.testing.expect(first_prepared.loaded_artifact_id != second_prepared.loaded_artifact_id);
2696
2697 const launch_count = state.launch_count;
2698 const read_count = state.read_count;
2699 const surface_write_count = state.surface_write_count;
2700 try std.testing.expectError(error.InvalidPreparedLaunch, second_executor.preparedLaunchInfo(&first_prepared));
2701 try std.testing.expectError(error.InvalidPreparedLaunch, second_executor.submitPreparedLaunchTimed(&first_prepared));
2702 try std.testing.expectError(error.InvalidPreparedLaunch, second_executor.submitPreparedLaunchQueued(&first_prepared));
2703 try std.testing.expectError(error.InvalidPreparedLaunch, second_executor.readPreparedPackedPixels(&first_prepared));
2704
2705 const surface = try handle.createSurface(.{
2706 .platform = .{ .headless = .{} },
2707 .extent = .{ .width = 4, .height = 4 },
2708 .format = .rgba8_unorm,
2709 .usage = .{ .present = true, .copy_dst = true },
2710 });
2711 const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
2712 try std.testing.expectError(error.InvalidPreparedLaunch, second_executor.writePreparedSurfaceFrame(frame, &first_prepared));
2713 try std.testing.expectEqual(launch_count, state.launch_count);
2714 try std.testing.expectEqual(read_count, state.read_count);
2715 try std.testing.expectEqual(surface_write_count, state.surface_write_count);
2716
2717 try second_executor.submitPreparedLaunchQueued(&second_prepared);
2718 try std.testing.expectEqual(launch_count + 1, state.launch_count);
2719
2720 try handle.presentSurfaceFrame(.{
2721 .surface = surface,
2722 .frame = frame,
2723 });
2724 try handle.destroySurface(surface);
2725 }
2726
2727 test "paint executor reuses prepared device CSR for unchanged commands" {
2728 const allocator = std.testing.allocator;
2729 var state = gpu.recording.BackendState{
2730 .allocator = allocator,
2731 .kind = .vulkan,
2732 .format = .vulkan_spirv,
2733 };
2734 var executor = try Executor.init(allocator, state.handle(), .{});
2735 defer executor.deinit();
2736 const commands = [_]Command{.{
2737 .kind = .fill,
2738 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2739 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2740 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
2741 }};
2742 const changed_commands = [_]Command{.{
2743 .kind = .fill,
2744 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2745 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2746 .color = .{ .r = 7, .g = 8, .b = 10, .a = 255 },
2747 }};
2748
2749 const first = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2750 const first_writes = state.write_count;
2751 const first_launches = state.launch_count;
2752 try std.testing.expect(first.device_csr_prepared);
2753 try std.testing.expectEqual(@as(usize, 4), first_writes);
2754 try std.testing.expectEqual(@as(usize, 6), first_launches);
2755
2756 const second = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2757 try std.testing.expect(!second.device_csr_prepared);
2758 try std.testing.expectEqual(first_writes, state.write_count);
2759 try std.testing.expectEqual(first_launches, state.launch_count);
2760
2761 const changed = (try executor.prepareCommandsPackedLaunch(changed_commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
2762 try std.testing.expect(changed.device_csr_prepared);
2763 try std.testing.expectEqual(first_writes + 4, state.write_count);
2764 try std.testing.expectEqual(first_launches + 6, state.launch_count);
2765
2766 const region_changed = (try executor.prepareCommandsPackedLaunch(changed_commands[0..], 4, 4, .{ .a = 0 }, .{}, .{ .x = 1, .y = 1, .width = 2, .height = 2 })).?;
2767 try std.testing.expect(region_changed.device_csr_prepared);
2768 try std.testing.expectEqual(first_writes + 6, state.write_count);
2769 try std.testing.expectEqual(first_launches + 12, state.launch_count);
2770 }
2771
2772 test "paint executor writes packed commands into surface frame without readback" {
2773 const allocator = std.testing.allocator;
2774 var state = gpu.recording.BackendState{
2775 .allocator = allocator,
2776 .kind = .vulkan,
2777 .format = .vulkan_spirv,
2778 };
2779 const handle = state.handle();
2780 var executor = try Executor.init(allocator, handle, .{});
2781 defer executor.deinit();
2782 const surface = try handle.createSurface(.{
2783 .platform = .{ .headless = .{} },
2784 .extent = .{ .width = 4, .height = 4 },
2785 .format = .rgba8_unorm,
2786 .usage = .{ .present = true, .copy_dst = true },
2787 });
2788 const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
2789 const image_pixels = [_]u32{cpu.packRgba(.{ .r = 30, .g = 40, .b = 50, .a = 255 })};
2790 const images = ImageSet{ .images = &.{.{ .width = 1, .height = 1, .pixels = image_pixels[0..] }} };
2791 const commands = [_]Command{
2792 .{
2793 .kind = .fill,
2794 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2795 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2796 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
2797 },
2798 .{
2799 .kind = .image,
2800 .rect = .{ .x = 1, .y = 1, .width = 1, .height = 1 },
2801 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2802 .source = .{ .x = 0, .y = 0, .width = 1, .height = 1 },
2803 .color = .{ .r = 255, .g = 255, .b = 255, .a = 255 },
2804 .image_index = 0,
2805 },
2806 };
2807
2808 try executor.renderCommandsSurfaceFrameWithImages(commands[0..], frame, .{ .r = 1, .g = 2, .b = 3, .a = 255 }, images);
2809
2810 const buffers = executor.buffers.?;
2811 try std.testing.expectEqual(@as(usize, 7), state.launch_count);
2812 try std.testing.expectEqual(@as(usize, 0), state.sync_count);
2813 try std.testing.expectEqual(@as(usize, 0), state.read_count);
2814 try std.testing.expectEqual(@as(usize, 1), state.surface_write_count);
2815 try std.testing.expectEqual(frame.id, state.last_written_frame_id.?);
2816 try std.testing.expectEqual(@as(usize, 1), state.last_surface_write_op_count);
2817 try std.testing.expectEqual(buffers.pixels.id, state.last_surface_write_copy_buffer_id.?);
2818 try std.testing.expectEqual(@as(u32, 1), state.last_launch_scalar_u32_values[0]);
2819 try std.testing.expectEqual(@as(u32, 4), state.last_launch_scalar_u32_values[7]);
2820 try std.testing.expectEqual(@as(u32, 16), state.last_launch_scalar_u32_values[8]);
2821 try std.testing.expectEqual(@as(u32, @backingInt(accy_paint.OutputFormat.rgba)), state.last_launch_scalar_u32_values[10]);
2822
2823 try handle.presentSurfaceFrame(.{
2824 .surface = surface,
2825 .frame = frame,
2826 });
2827 try std.testing.expectEqual(@as(usize, 1), state.surface_present_count);
2828 try handle.destroySurface(surface);
2829 }
2830
2831 test "paint executor writes prepared surface frames with events" {
2832 const allocator = std.testing.allocator;
2833 var state = gpu.recording.BackendState{
2834 .allocator = allocator,
2835 .kind = .vulkan,
2836 .format = .vulkan_spirv,
2837 };
2838 const handle = state.handle();
2839 var executor = try Executor.init(allocator, handle, .{});
2840 defer executor.deinit();
2841 const surface = try handle.createSurface(.{
2842 .platform = .{ .headless = .{} },
2843 .extent = .{ .width = 4, .height = 4 },
2844 .format = .rgba8_unorm,
2845 .usage = .{ .present = true, .copy_dst = true },
2846 });
2847 const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
2848 const launch_done = try handle.createEvent(.{});
2849 const write_done = try handle.createEvent(.{});
2850 const commands = [_]Command{.{
2851 .kind = .fill,
2852 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2853 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2854 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
2855 }};
2856
2857 var prepared = (try executor.prepareCommandsSurfaceLaunch(commands[0..], surface, .{ .a = 0 }, .{})).?;
2858 const prepare_launches = state.launch_count;
2859 try executor.submitPreparedLaunchQueuedWithEvents(&prepared, &.{}, launch_done);
2860 try std.testing.expectEqual(prepare_launches + 1, state.launch_count);
2861 try std.testing.expectEqual(launch_done.id, state.last_launch_signal_event.?);
2862 try std.testing.expect(try handle.queryEvent(.{ .event = launch_done }));
2863 try std.testing.expect(!try handle.queryEvent(.{ .event = write_done }));
2864
2865 try executor.writePreparedSurfaceFrameWithEvents(frame, &prepared, &.{launch_done}, write_done);
2866 try std.testing.expectEqual(@as(usize, 1), state.surface_write_count);
2867 try std.testing.expectEqual(@as(usize, 1), state.last_surface_write_wait_count);
2868 try std.testing.expectEqual(launch_done.id, state.last_surface_write_wait_events[0]);
2869 try std.testing.expectEqual(write_done.id, state.last_surface_write_signal_event.?);
2870 try std.testing.expect(try handle.queryEvent(.{ .event = write_done }));
2871 try std.testing.expectEqual(@as(usize, 0), state.read_count);
2872 try std.testing.expectEqual(@as(usize, 0), state.sync_count);
2873
2874 try handle.presentSurfaceFrame(.{
2875 .surface = surface,
2876 .frame = frame,
2877 });
2878 try handle.destroySurface(surface);
2879 }
2880
2881 test "paint executor writes bgra surface frame with bgra output format" {
2882 const allocator = std.testing.allocator;
2883 var state = gpu.recording.BackendState{
2884 .allocator = allocator,
2885 .kind = .vulkan,
2886 .format = .vulkan_spirv,
2887 };
2888 const handle = state.handle();
2889 var executor = try Executor.init(allocator, handle, .{});
2890 defer executor.deinit();
2891 const surface = try handle.createSurface(.{
2892 .platform = .{ .headless = .{} },
2893 .extent = .{ .width = 4, .height = 4 },
2894 .format = .bgra8_unorm,
2895 .usage = .{ .present = true, .copy_dst = true },
2896 });
2897 const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
2898 const commands = [_]Command{.{
2899 .kind = .fill,
2900 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2901 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2902 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
2903 }};
2904
2905 var prepared = (try executor.prepareCommandsSurfaceLaunch(commands[0..], surface, .{ .r = 1, .g = 2, .b = 3, .a = 255 }, .{})).?;
2906 try executor.submitPreparedLaunchQueued(&prepared);
2907 try executor.writePreparedSurfaceFrame(frame, &prepared);
2908
2909 const buffers = executor.buffers.?;
2910 try std.testing.expectEqual(@as(usize, 16), prepared.pixel_count);
2911 try std.testing.expectEqual(@as(u32, 4), prepared.target_width);
2912 try std.testing.expectEqual(@as(u32, 4), prepared.target_height);
2913 try std.testing.expectEqual(@as(u32, 0), prepared.region_x);
2914 try std.testing.expectEqual(@as(u32, 0), prepared.region_y);
2915 try std.testing.expectEqual(@as(u32, 4), prepared.region_width);
2916 try std.testing.expectEqual(@as(u32, 4), prepared.region_height);
2917 try std.testing.expectEqual(accy_paint.OutputFormat.bgra, prepared.output_format);
2918 try std.testing.expectEqual(@as(usize, 7), state.launch_count);
2919 try std.testing.expectEqual(@as(usize, 0), state.sync_count);
2920 try std.testing.expectEqual(@as(usize, 0), state.read_count);
2921 try std.testing.expectEqual(@as(usize, 1), state.surface_write_count);
2922 try std.testing.expectEqual(frame.id, state.last_written_frame_id.?);
2923 try std.testing.expectEqual(@as(usize, 1), state.last_surface_write_op_count);
2924 try std.testing.expectEqual(buffers.pixels.id, state.last_surface_write_copy_buffer_id.?);
2925 try std.testing.expectEqual(@as(usize, 11), state.last_launch_scalar_count);
2926 try std.testing.expectEqual(@as(u32, 16), state.last_launch_scalar_u32_values[8]);
2927 try std.testing.expectEqual(@as(u32, @backingInt(accy_paint.OutputFormat.bgra)), state.last_launch_scalar_u32_values[10]);
2928
2929 try handle.presentSurfaceFrame(.{
2930 .surface = surface,
2931 .frame = frame,
2932 });
2933 try handle.destroySurface(surface);
2934 }
2935
2936 test "paint executor writes smaller surface frames through retained pixel capacity" {
2937 const allocator = std.testing.allocator;
2938 var state = gpu.recording.BackendState{
2939 .allocator = allocator,
2940 .kind = .vulkan,
2941 .format = .vulkan_spirv,
2942 };
2943 const handle = state.handle();
2944 var executor = try Executor.init(allocator, handle, .{});
2945 defer executor.deinit();
2946 const commands = [_]Command{.{
2947 .kind = .fill,
2948 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2949 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
2950 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
2951 }};
2952
2953 const large_surface = try handle.createSurface(.{
2954 .platform = .{ .headless = .{} },
2955 .extent = .{ .width = 4, .height = 4 },
2956 .format = .rgba8_unorm,
2957 .usage = .{ .present = true, .copy_dst = true },
2958 });
2959 const large_frame = try handle.acquireSurfaceFrame(.{ .surface = large_surface });
2960 var large = (try executor.prepareCommandsSurfaceLaunch(commands[0..], large_surface, .{ .a = 0 }, .{})).?;
2961 try executor.submitPreparedLaunchQueued(&large);
2962 try executor.writePreparedSurfaceFrame(large_frame, &large);
2963 try handle.presentSurfaceFrame(.{
2964 .surface = large_surface,
2965 .frame = large_frame,
2966 });
2967 try handle.destroySurface(large_surface);
2968
2969 const retained_pixel_capacity = executor.buffers.?.pixel_capacity;
2970 const retained_pixels = executor.buffers.?.pixels;
2971 try std.testing.expectEqual(@as(usize, 16), large.pixel_count);
2972 try std.testing.expectEqual(@as(usize, 16), retained_pixel_capacity);
2973 try std.testing.expectEqual(@as(usize, 1), state.surface_write_count);
2974 try std.testing.expectEqual(retained_pixels.id, state.last_surface_write_copy_buffer_id.?);
2975
2976 const small_surface = try handle.createSurface(.{
2977 .platform = .{ .headless = .{} },
2978 .extent = .{ .width = 2, .height = 2 },
2979 .format = .rgba8_unorm,
2980 .usage = .{ .present = true, .copy_dst = true },
2981 });
2982 const small_frame = try handle.acquireSurfaceFrame(.{ .surface = small_surface });
2983 var small = (try executor.prepareCommandsSurfaceLaunch(commands[0..], small_surface, .{ .a = 0 }, .{})).?;
2984 try executor.submitPreparedLaunchQueued(&small);
2985 try executor.writePreparedSurfaceFrame(small_frame, &small);
2986
2987 try std.testing.expectEqual(@as(usize, 4), small.pixel_count);
2988 try std.testing.expectEqual(retained_pixel_capacity, executor.buffers.?.pixel_capacity);
2989 try std.testing.expectEqual(retained_pixels.id, executor.buffers.?.pixels.id);
2990 try std.testing.expectEqual(@as(usize, 2), state.surface_write_count);
2991 try std.testing.expectEqual(small_frame.id, state.last_written_frame_id.?);
2992 try std.testing.expectEqual(retained_pixels.id, state.last_surface_write_copy_buffer_id.?);
2993
2994 try handle.presentSurfaceFrame(.{
2995 .surface = small_surface,
2996 .frame = small_frame,
2997 });
2998 try handle.destroySurface(small_surface);
2999 }
3000
3001 test "paint executor rejects prepared surface frame target mismatches" {
3002 const allocator = std.testing.allocator;
3003 var state = gpu.recording.BackendState{
3004 .allocator = allocator,
3005 .kind = .vulkan,
3006 .format = .vulkan_spirv,
3007 };
3008 const handle = state.handle();
3009 var executor = try Executor.init(allocator, handle, .{});
3010 defer executor.deinit();
3011 const commands = [_]Command{.{
3012 .kind = .fill,
3013 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
3014 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
3015 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
3016 }};
3017
3018 const bgra_surface = try handle.createSurface(.{
3019 .platform = .{ .headless = .{} },
3020 .extent = .{ .width = 4, .height = 4 },
3021 .format = .bgra8_unorm,
3022 .usage = .{ .present = true, .copy_dst = true },
3023 });
3024 const bgra_frame = try handle.acquireSurfaceFrame(.{ .surface = bgra_surface });
3025 var rgba_prepared = (try executor.prepareCommandsPackedLaunch(commands[0..], 4, 4, .{ .a = 0 }, .{}, cpu.Region.full(4, 4))).?;
3026 try std.testing.expectEqual(accy_paint.OutputFormat.rgba, rgba_prepared.output_format);
3027 try std.testing.expectError(error.InvalidSurfaceFrame, executor.writePreparedSurfaceFrame(bgra_frame, &rgba_prepared));
3028 try std.testing.expectEqual(@as(usize, 0), state.surface_write_count);
3029 try handle.presentSurfaceFrame(.{
3030 .surface = bgra_surface,
3031 .frame = bgra_frame,
3032 });
3033 try handle.destroySurface(bgra_surface);
3034
3035 const rgba_surface = try handle.createSurface(.{
3036 .platform = .{ .headless = .{} },
3037 .extent = .{ .width = 4, .height = 4 },
3038 .format = .rgba8_unorm,
3039 .usage = .{ .present = true, .copy_dst = true },
3040 });
3041 const rgba_frame = try handle.acquireSurfaceFrame(.{ .surface = rgba_surface });
3042 var small_prepared = (try executor.prepareCommandsPackedLaunch(commands[0..], 2, 2, .{ .a = 0 }, .{}, cpu.Region.full(2, 2))).?;
3043 try std.testing.expectEqual(@as(u32, 2), small_prepared.target_width);
3044 try std.testing.expectError(error.InvalidSurfaceFrame, executor.writePreparedSurfaceFrame(rgba_frame, &small_prepared));
3045 try std.testing.expectEqual(@as(usize, 0), state.surface_write_count);
3046 try handle.presentSurfaceFrame(.{
3047 .surface = rgba_surface,
3048 .frame = rgba_frame,
3049 });
3050 try handle.destroySurface(rgba_surface);
3051 }
3052
3053 test "paint executor writes image shadows into surface frame without paint readback" {
3054 const allocator = std.testing.allocator;
3055 var state = gpu.recording.BackendState{
3056 .allocator = allocator,
3057 .kind = .vulkan,
3058 .format = .vulkan_spirv,
3059 };
3060 const handle = state.handle();
3061 var executor = try Executor.init(allocator, handle, .{});
3062 defer executor.deinit();
3063 const surface = try handle.createSurface(.{
3064 .platform = .{ .headless = .{} },
3065 .extent = .{ .width = 10, .height = 10 },
3066 .format = .rgba8_unorm,
3067 .usage = .{ .present = true, .copy_dst = true },
3068 });
3069 const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
3070 const commands = [_]Command{.{
3071 .kind = .shadow,
3072 .rect = .{ .x = 2, .y = 2, .width = 4, .height = 3 },
3073 .clip = .{ .x = 0, .y = 0, .width = 10, .height = 10 },
3074 .color = .{ .r = 30, .g = 40, .b = 50, .a = 210 },
3075 .radius = 1,
3076 .width = 2,
3077 .order = 4,
3078 }};
3079 const load_before = state.load_count;
3080 const launch_before = state.launch_count;
3081 const sync_before = state.sync_count;
3082 const read_before = state.read_count;
3083
3084 try executor.renderCommandsSurfaceFrameWithImageShadows(commands[0..], frame, .{ .a = 0 }, .{}, .{});
3085
3086 const buffers = executor.buffers.?;
3087 try std.testing.expectEqual(@as(usize, 2), state.load_count - load_before);
3088 try std.testing.expectEqual(@as(usize, 9), state.launch_count - launch_before);
3089 try std.testing.expectEqual(@as(usize, 1), state.sync_count - sync_before);
3090 try std.testing.expectEqual(@as(usize, 1), state.read_count - read_before);
3091 try std.testing.expectEqual(@as(usize, 1), state.surface_write_count);
3092 try std.testing.expectEqual(frame.id, state.last_written_frame_id.?);
3093 try std.testing.expectEqual(buffers.pixels.id, state.last_surface_write_copy_buffer_id.?);
3094 try std.testing.expectEqual(@as(u32, 1), state.last_launch_scalar_u32_values[0]);
3095 try std.testing.expectEqual(@as(u32, @backingInt(accy_paint.OutputFormat.rgba)), state.last_launch_scalar_u32_values[10]);
3096
3097 try handle.presentSurfaceFrame(.{
3098 .surface = surface,
3099 .frame = frame,
3100 });
3101 try handle.destroySurface(surface);
3102 }
3103
3104 test "paint executor prepares image-shadow surface launches with cached expansion" {
3105 const allocator = std.testing.allocator;
3106 var state = gpu.recording.BackendState{
3107 .allocator = allocator,
3108 .kind = .vulkan,
3109 .format = .vulkan_spirv,
3110 };
3111 const handle = state.handle();
3112 var executor = try Executor.init(allocator, handle, .{});
3113 defer executor.deinit();
3114 const surface = try handle.createSurface(.{
3115 .platform = .{ .headless = .{} },
3116 .extent = .{ .width = 10, .height = 10 },
3117 .format = .rgba8_unorm,
3118 .usage = .{ .present = true, .copy_dst = true },
3119 });
3120 const commands = [_]Command{.{
3121 .kind = .shadow,
3122 .rect = .{ .x = 2, .y = 2, .width = 4, .height = 3 },
3123 .clip = .{ .x = 0, .y = 0, .width = 10, .height = 10 },
3124 .color = .{ .r = 30, .g = 40, .b = 50, .a = 210 },
3125 .radius = 1,
3126 .width = 2,
3127 .order = 4,
3128 }};
3129 const load_before = state.load_count;
3130 const launch_before = state.launch_count;
3131 const sync_before = state.sync_count;
3132 const read_before = state.read_count;
3133
3134 var first = (try executor.prepareCommandsSurfaceLaunchWithImageShadows(commands[0..], surface, .{ .a = 0 }, .{}, .{})).?;
3135
3136 try std.testing.expect(first.device_csr_prepared);
3137 try std.testing.expectEqual(@as(usize, 100), first.pixel_count);
3138 try std.testing.expectEqual(@as(usize, 2), state.load_count - load_before);
3139 try std.testing.expectEqual(@as(usize, 8), state.launch_count - launch_before);
3140 try std.testing.expectEqual(@as(usize, 1), state.sync_count - sync_before);
3141 try std.testing.expectEqual(@as(usize, 1), state.read_count - read_before);
3142
3143 try executor.submitPreparedLaunchQueued(&first);
3144 const first_frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
3145 try executor.writePreparedSurfaceFrame(first_frame, &first);
3146 try handle.presentSurfaceFrame(.{
3147 .surface = surface,
3148 .frame = first_frame,
3149 });
3150
3151 const load_after_first = state.load_count;
3152 const launch_after_first = state.launch_count;
3153 const sync_after_first = state.sync_count;
3154 const read_after_first = state.read_count;
3155 const write_after_first = state.write_count;
3156 const surface_write_after_first = state.surface_write_count;
3157 var second = (try executor.prepareCommandsSurfaceLaunchWithImageShadows(commands[0..], surface, .{ .a = 0 }, .{}, .{})).?;
3158
3159 try std.testing.expect(!second.device_csr_prepared);
3160 try std.testing.expectEqual(@as(usize, 100), second.pixel_count);
3161 try std.testing.expectEqual(load_after_first, state.load_count);
3162 try std.testing.expectEqual(launch_after_first, state.launch_count);
3163 try std.testing.expectEqual(sync_after_first, state.sync_count);
3164 try std.testing.expectEqual(read_after_first, state.read_count);
3165 try std.testing.expectEqual(write_after_first, state.write_count);
3166 try std.testing.expectEqual(surface_write_after_first, state.surface_write_count);
3167
3168 try executor.submitPreparedLaunchQueued(&second);
3169 const second_frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
3170 try executor.writePreparedSurfaceFrame(second_frame, &second);
3171 try handle.presentSurfaceFrame(.{
3172 .surface = surface,
3173 .frame = second_frame,
3174 });
3175
3176 try std.testing.expectEqual(launch_after_first + 1, state.launch_count);
3177 try std.testing.expectEqual(sync_after_first, state.sync_count);
3178 try std.testing.expectEqual(read_after_first, state.read_count);
3179 try std.testing.expectEqual(surface_write_after_first + 1, state.surface_write_count);
3180
3181 const packed_launch_before = state.launch_count;
3182 const packed_write_before = state.write_count;
3183 const packed_launch = (try executor.prepareCommandsPackedLaunchWithImageShadows(commands[0..], 10, 10, .{ .a = 0 }, .{}, cpu.Region.full(10, 10), .{})).?;
3184 try std.testing.expect(!packed_launch.device_csr_prepared);
3185 try std.testing.expectEqual(@as(usize, 100), packed_launch.pixel_count);
3186 try std.testing.expectEqual(packed_launch_before, state.launch_count);
3187 try std.testing.expectEqual(packed_write_before, state.write_count);
3188 try handle.destroySurface(surface);
3189 }
3190
3191 test "paint executor reuses image-shadow family kernels across renders" {
3192 const allocator = std.testing.allocator;
3193 var state = gpu.recording.BackendState{
3194 .allocator = allocator,
3195 .kind = .vulkan,
3196 .format = .vulkan_spirv,
3197 };
3198 var executor = try Executor.init(allocator, state.handle(), .{});
3199 defer executor.deinit();
3200 const commands = [_]Command{.{
3201 .kind = .shadow,
3202 .rect = .{ .x = 2, .y = 2, .width = 4, .height = 3 },
3203 .clip = .{ .x = 0, .y = 0, .width = 10, .height = 10 },
3204 .color = .{ .r = 30, .g = 40, .b = 50, .a = 210 },
3205 .radius = 1,
3206 .width = 2,
3207 .order = 4,
3208 }};
3209 var changed = commands;
3210 changed[0].width = 3;
3211 var pixels = @as([(10 * 10)]u32, @splat(0));
3212 const load_before = state.load_count;
3213
3214 try executor.renderCommandsPackedWithImageShadows(commands[0..], .{
3215 .width = 10,
3216 .height = 10,
3217 .pixels = pixels[0..],
3218 }, .{ .a = 0 }, .{}, .{});
3219 const first_load_count = state.load_count;
3220 try std.testing.expectEqual(@as(usize, 2), first_load_count - load_before);
3221
3222 try executor.renderCommandsPackedWithImageShadows(commands[0..], .{
3223 .width = 10,
3224 .height = 10,
3225 .pixels = pixels[0..],
3226 }, .{ .a = 0 }, .{}, .{});
3227 try std.testing.expectEqual(first_load_count, state.load_count);
3228
3229 try executor.renderCommandsPackedWithImageShadows(changed[0..], .{
3230 .width = 10,
3231 .height = 10,
3232 .pixels = pixels[0..],
3233 }, .{ .a = 0 }, .{}, .{});
3234 try std.testing.expectEqual(first_load_count + 2, state.load_count);
3235 }
3236
3237 test "paint executor reuses image-shadow expansions across renders" {
3238 const allocator = std.testing.allocator;
3239 var state = gpu.recording.BackendState{
3240 .allocator = allocator,
3241 .kind = .vulkan,
3242 .format = .vulkan_spirv,
3243 };
3244 var executor = try Executor.init(allocator, state.handle(), .{});
3245 defer executor.deinit();
3246 const commands = [_]Command{.{
3247 .kind = .shadow,
3248 .rect = .{ .x = 2, .y = 2, .width = 4, .height = 3 },
3249 .clip = .{ .x = 0, .y = 0, .width = 10, .height = 10 },
3250 .color = .{ .r = 30, .g = 40, .b = 50, .a = 210 },
3251 .radius = 1,
3252 .width = 2,
3253 .order = 4,
3254 }};
3255 var changed = commands;
3256 changed[0].color.r = 31;
3257 var changed_alpha = changed;
3258 changed_alpha[0].color.a = 211;
3259 var pixels = @as([(10 * 10)]u32, @splat(0));
3260
3261 try executor.renderCommandsPackedWithImageShadows(commands[0..], .{
3262 .width = 10,
3263 .height = 10,
3264 .pixels = pixels[0..],
3265 }, .{ .a = 0 }, .{}, .{});
3266 const cached = &executor.shadow_expansion.?;
3267 const cached_commands_ptr = cached.expansion.commands.ptr;
3268 const cached_images_ptr = cached.expansion.image_entries.ptr;
3269 const cached_shadows_ptr = cached.expansion.shadows.ptr;
3270 const cached_shadow_pixels_ptr = cached.expansion.shadows[0].pixels.ptr;
3271 const first_launch_count = state.launch_count;
3272 const first_write_count = state.write_count;
3273 const first_read_count = state.read_count;
3274
3275 try executor.renderCommandsPackedWithImageShadows(commands[0..], .{
3276 .width = 10,
3277 .height = 10,
3278 .pixels = pixels[0..],
3279 }, .{ .a = 0 }, .{}, .{});
3280 try std.testing.expectEqual(first_launch_count + 1, state.launch_count);
3281 try std.testing.expectEqual(first_write_count, state.write_count);
3282 try std.testing.expectEqual(first_read_count + 1, state.read_count);
3283
3284 try executor.renderCommandsPackedWithImageShadows(changed[0..], .{
3285 .width = 10,
3286 .height = 10,
3287 .pixels = pixels[0..],
3288 }, .{ .a = 0 }, .{}, .{});
3289 const changed_cached = &executor.shadow_expansion.?;
3290 try std.testing.expectEqual(cached_commands_ptr, changed_cached.expansion.commands.ptr);
3291 try std.testing.expectEqual(cached_images_ptr, changed_cached.expansion.image_entries.ptr);
3292 try std.testing.expectEqual(cached_shadows_ptr, changed_cached.expansion.shadows.ptr);
3293 try std.testing.expectEqual(cached_shadow_pixels_ptr, changed_cached.expansion.shadows[0].pixels.ptr);
3294 try expectShadowPixelsTinted(changed_cached.expansion.shadows[0].pixels, changed[0].color);
3295 try std.testing.expectEqual(first_launch_count + 2, state.launch_count);
3296 try std.testing.expectEqual(first_write_count + 1, state.write_count);
3297 try std.testing.expectEqual(first_read_count + 2, state.read_count);
3298
3299 try executor.renderCommandsPackedWithImageShadows(changed[0..], .{
3300 .width = 10,
3301 .height = 10,
3302 .pixels = pixels[0..],
3303 }, .{ .a = 0 }, .{}, .{});
3304 try std.testing.expectEqual(cached_commands_ptr, executor.shadow_expansion.?.expansion.commands.ptr);
3305 try std.testing.expectEqual(first_launch_count + 3, state.launch_count);
3306 try std.testing.expectEqual(first_write_count + 1, state.write_count);
3307 try std.testing.expectEqual(first_read_count + 3, state.read_count);
3308
3309 try executor.renderCommandsPackedWithImageShadows(changed_alpha[0..], .{
3310 .width = 10,
3311 .height = 10,
3312 .pixels = pixels[0..],
3313 }, .{ .a = 0 }, .{}, .{});
3314 try std.testing.expect(cached_commands_ptr != executor.shadow_expansion.?.expansion.commands.ptr);
3315 }
3316
3317 test "paint executor converts readback into rgba8 targets" {
3318 const allocator = std.testing.allocator;
3319 var state = gpu.recording.BackendState{
3320 .allocator = allocator,
3321 .kind = .cuda,
3322 .format = .cuda_ptx,
3323 };
3324 var executor = try Executor.init(allocator, state.handle(), .{});
3325 defer executor.deinit();
3326 var rgba = @as([(4 * 4)]u8, @splat(0xff));
3327 const commands = [_]Command{};
3328
3329 try executor.renderCommands(commands[0..], .{ .width = 2, .height = 2, .rgba8 = rgba[0..] }, .{
3330 .r = 9,
3331 .g = 8,
3332 .b = 7,
3333 .a = 6,
3334 });
3335
3336 try std.testing.expectEqual(@as(usize, 1), state.write_count);
3337 try std.testing.expectEqual(@as(usize, 1), state.launch_count);
3338 try std.testing.expectEqual(@as(u32, 0), state.last_launch_scalar_u32_values[0]);
3339 try std.testing.expectEqual(@as(u32, 9), state.last_launch_scalar_u32_values[1]);
3340 try std.testing.expectEqual(@as(u32, 8), state.last_launch_scalar_u32_values[2]);
3341 try std.testing.expectEqual(@as(u32, 7), state.last_launch_scalar_u32_values[3]);
3342 try std.testing.expectEqual(@as(u32, 6), state.last_launch_scalar_u32_values[4]);
3343 try std.testing.expectEqual(@as(u32, 0), state.last_launch_scalar_u32_values[5]);
3344 try std.testing.expectEqual(@as(u32, 2), state.last_launch_scalar_u32_values[7]);
3345 try std.testing.expectEqual(@as(u32, 4), state.last_launch_scalar_u32_values[8]);
3346 try std.testing.expectEqual(@as(u32, 1), state.last_launch_scalar_u32_values[9]);
3347 try std.testing.expectEqual(@as(u32, @backingInt(accy_paint.OutputFormat.rgba)), state.last_launch_scalar_u32_values[10]);
3348 try std.testing.expectEqualSlices(u8, &@as([(4 * 4)]u8, @splat(0)), rgba[0..]);
3349 }
3350
3351 test "paint executor reads full reused pixel buffers" {
3352 const allocator = std.testing.allocator;
3353 var state = gpu.recording.BackendState{
3354 .allocator = allocator,
3355 .kind = .vulkan,
3356 .format = .vulkan_spirv,
3357 };
3358 var executor = try Executor.init(allocator, state.handle(), .{});
3359 defer executor.deinit();
3360 const commands = [_]Command{};
3361 var large = @as([16]u32, @splat(0xffff_ffff));
3362 var small = @as([4]u32, @splat(0xffff_ffff));
3363
3364 try executor.renderCommandsPacked(commands[0..], .{ .width = 4, .height = 4, .pixels = large[0..] }, .{ .a = 0 });
3365 try std.testing.expectEqual(@as(usize, 16 * @sizeOf(u32)), state.last_read_byte_count);
3366
3367 try executor.renderCommandsPacked(commands[0..], .{ .width = 2, .height = 2, .pixels = small[0..] }, .{ .a = 0 });
3368 try std.testing.expectEqual(@as(usize, 16 * @sizeOf(u32)), state.last_read_byte_count);
3369 try std.testing.expectEqualSlices(u32, &@as([4]u32, @splat(0)), small[0..]);
3370 }
3371
3372 test "paint executor uploads image metadata and pixels" {
3373 const allocator = std.testing.allocator;
3374 var state = gpu.recording.BackendState{
3375 .allocator = allocator,
3376 .kind = .vulkan,
3377 .format = .vulkan_spirv,
3378 };
3379 var executor = try Executor.init(allocator, state.handle(), .{});
3380 defer executor.deinit();
3381 const image_pixels = [_]u32{
3382 cpu.packRgba(.{ .r = 10, .g = 20, .b = 30, .a = 255 }),
3383 cpu.packRgba(.{ .r = 40, .g = 50, .b = 60, .a = 255 }),
3384 cpu.packRgba(.{ .r = 70, .g = 80, .b = 90, .a = 255 }),
3385 cpu.packRgba(.{ .r = 100, .g = 110, .b = 120, .a = 255 }),
3386 };
3387 const images = ImageSet{ .images = &.{.{ .width = 2, .height = 2, .pixels = image_pixels[0..] }} };
3388 const changed_image_pixels = [_]u32{
3389 cpu.packRgba(.{ .r = 10, .g = 20, .b = 30, .a = 255 }),
3390 cpu.packRgba(.{ .r = 40, .g = 50, .b = 61, .a = 255 }),
3391 cpu.packRgba(.{ .r = 70, .g = 80, .b = 90, .a = 255 }),
3392 cpu.packRgba(.{ .r = 100, .g = 110, .b = 120, .a = 255 }),
3393 };
3394 const changed_images = ImageSet{ .images = &.{.{ .width = 2, .height = 2, .pixels = changed_image_pixels[0..] }} };
3395 const second_image_pixels = [_]u32{
3396 cpu.packRgba(.{ .r = 1, .g = 2, .b = 3, .a = 255 }),
3397 cpu.packRgba(.{ .r = 4, .g = 5, .b = 6, .a = 255 }),
3398 cpu.packRgba(.{ .r = 7, .g = 8, .b = 9, .a = 255 }),
3399 cpu.packRgba(.{ .r = 10, .g = 11, .b = 12, .a = 255 }),
3400 };
3401 const two_images = ImageSet{ .images = &.{
3402 .{ .width = 2, .height = 2, .pixels = image_pixels[0..] },
3403 .{ .width = 2, .height = 2, .pixels = second_image_pixels[0..] },
3404 } };
3405 const commands = [_]Command{.{
3406 .kind = .image,
3407 .rect = .{ .x = 0, .y = 0, .width = 2, .height = 2 },
3408 .clip = .{ .x = 0, .y = 0, .width = 2, .height = 2 },
3409 .source = .{ .x = 0, .y = 0, .width = 2, .height = 2 },
3410 .color = .{ .r = 255, .g = 255, .b = 255, .a = 255 },
3411 .image_index = 0,
3412 }};
3413 var pixels = @as([4]u32, @splat(0));
3414 var larger_pixels = @as([16]u32, @splat(0));
3415
3416 try executor.renderCommandsPackedWithImages(commands[0..], .{ .width = 2, .height = 2, .pixels = pixels[0..] }, .{ .a = 0 }, images);
3417
3418 try std.testing.expectEqual(@as(usize, 6), state.write_count);
3419 try std.testing.expectEqual(@as(u32, 1), state.last_launch_scalar_u32_values[0]);
3420 try std.testing.expectEqual(@as(usize, 1 * @sizeOf(u32)), state.last_write_byte_count);
3421 const first_buffers = executor.buffers.?;
3422 try std.testing.expect(first_buffers.image_upload.valid);
3423 try std.testing.expect(try accy_paint.imagesEqualPacked(images, first_buffers.images.metadata, first_buffers.images.pixels));
3424
3425 try executor.renderCommandsPackedWithImages(commands[0..], .{ .width = 2, .height = 2, .pixels = pixels[0..] }, .{ .a = 0 }, images);
3426 try std.testing.expectEqual(@as(usize, 6), state.write_count);
3427
3428 try executor.renderCommandsPackedWithImages(commands[0..], .{ .width = 4, .height = 4, .pixels = larger_pixels[0..] }, .{ .a = 0 }, images);
3429 try std.testing.expectEqual(@as(usize, 12), state.write_count);
3430 const resized_buffers = executor.buffers.?;
3431 try std.testing.expect(try accy_paint.imagesEqualPacked(images, resized_buffers.images.metadata, resized_buffers.images.pixels));
3432
3433 try executor.renderCommandsPackedWithImages(commands[0..], .{ .width = 4, .height = 4, .pixels = larger_pixels[0..] }, .{ .a = 0 }, changed_images);
3434 try std.testing.expectEqual(@as(usize, 13), state.write_count);
3435
3436 try executor.renderCommandsPackedWithImages(commands[0..], .{ .width = 4, .height = 4, .pixels = larger_pixels[0..] }, .{ .a = 0 }, two_images);
3437 try std.testing.expectEqual(@as(usize, 19), state.write_count);
3438
3439 try executor.renderCommandsPackedWithImages(commands[0..], .{ .width = 4, .height = 4, .pixels = larger_pixels[0..] }, .{ .a = 0 }, images);
3440 try std.testing.expectEqual(@as(usize, 21), state.write_count);
3441 }
3442
3443 test "paint executor launches packed damage region without seed pixels" {
3444 const allocator = std.testing.allocator;
3445 var state = gpu.recording.BackendState{
3446 .allocator = allocator,
3447 .kind = .vulkan,
3448 .format = .vulkan_spirv,
3449 };
3450 var executor = try Executor.init(allocator, state.handle(), .{});
3451 defer executor.deinit();
3452 var pixels = @as([(4 * 4)]u32, @splat(0x1122_3344));
3453 const commands = [_]Command{.{
3454 .kind = .fill,
3455 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
3456 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
3457 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
3458 }};
3459
3460 try executor.renderCommandsPackedRegionWithImages(commands[0..], .{ .width = 4, .height = 4, .pixels = pixels[0..] }, .{ .a = 0 }, .{}, .{ .x = 1, .y = 1, .width = 2, .height = 2 });
3461
3462 try std.testing.expectEqual(@as(usize, 4), state.write_count);
3463 try std.testing.expectEqual(@as(usize, 11), state.last_launch_scalar_count);
3464 try std.testing.expectEqual(@as(u32, 1), state.last_launch_scalar_u32_values[5]);
3465 try std.testing.expectEqual(@as(u32, 1), state.last_launch_scalar_u32_values[6]);
3466 try std.testing.expectEqual(@as(u32, 2), state.last_launch_scalar_u32_values[7]);
3467 try std.testing.expectEqual(@as(u32, 4), state.last_launch_scalar_u32_values[8]);
3468 try std.testing.expectEqual(@as(u32, @backingInt(accy_paint.OutputFormat.rgba)), state.last_launch_scalar_u32_values[10]);
3469 try std.testing.expectEqual(@as(usize, 4 * @sizeOf(u32)), state.last_read_byte_count);
3470 }
3471
3472 test "paint executor reuses device CSR buffers across same shape renders" {
3473 const allocator = std.testing.allocator;
3474 var state = gpu.recording.BackendState{
3475 .allocator = allocator,
3476 .kind = .vulkan,
3477 .format = .vulkan_spirv,
3478 };
3479 var executor = try Executor.init(allocator, state.handle(), .{});
3480 defer executor.deinit();
3481 var pixels = @as([16]u32, @splat(0));
3482 const commands = [_]Command{.{
3483 .kind = .fill,
3484 .rect = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
3485 .clip = .{ .x = 0, .y = 0, .width = 4, .height = 4 },
3486 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
3487 }};
3488
3489 try executor.renderCommandsPacked(commands[0..], .{ .width = 4, .height = 4, .pixels = pixels[0..] }, .{ .a = 0 });
3490 const buffer_allocate_count = state.buffer_allocate_count;
3491 const first_buffers = executor.buffers.?;
3492 const ranges_id = first_buffers.tile_ranges.?.id;
3493 const counts_id = first_buffers.tile_counts.?.id;
3494 const offsets_id = first_buffers.tile_offsets.id;
3495 const cursors_id = first_buffers.tile_cursors.?.id;
3496 const indices_id = first_buffers.tile_indices.id;
3497 const sums_id = first_buffers.scan_sums.?.id;
3498 const bases_id = first_buffers.scan_bases.?.id;
3499
3500 try executor.renderCommandsPacked(commands[0..], .{ .width = 4, .height = 4, .pixels = pixels[0..] }, .{ .a = 0 });
3501
3502 const second_buffers = executor.buffers.?;
3503 try std.testing.expectEqual(buffer_allocate_count, state.buffer_allocate_count);
3504 try std.testing.expectEqual(ranges_id, second_buffers.tile_ranges.?.id);
3505 try std.testing.expectEqual(counts_id, second_buffers.tile_counts.?.id);
3506 try std.testing.expectEqual(offsets_id, second_buffers.tile_offsets.id);
3507 try std.testing.expectEqual(cursors_id, second_buffers.tile_cursors.?.id);
3508 try std.testing.expectEqual(indices_id, second_buffers.tile_indices.id);
3509 try std.testing.expectEqual(sums_id, second_buffers.scan_sums.?.id);
3510 try std.testing.expectEqual(bases_id, second_buffers.scan_bases.?.id);
3511 }
3512
3513 test "paint executor grows target buffers without rebuilding artifact" {
3514 const allocator = std.testing.allocator;
3515 var state = gpu.recording.BackendState{
3516 .allocator = allocator,
3517 .kind = .metal,
3518 .format = .metal_msl,
3519 };
3520 var executor = try Executor.init(allocator, state.handle(), .{});
3521 defer executor.deinit();
3522 const load_count = state.load_count;
3523 var small = @as([4]u32, @splat(0));
3524 var large = @as([64]u32, @splat(0));
3525 const commands = [_]Command{.{
3526 .kind = .fill,
3527 .rect = .{ .x = 0, .y = 0, .width = 8, .height = 8 },
3528 .clip = .{ .x = 0, .y = 0, .width = 8, .height = 8 },
3529 .color = .{ .r = 7, .g = 8, .b = 9, .a = 255 },
3530 }};
3531
3532 try executor.renderCommandsPacked(commands[0..], .{ .width = 2, .height = 2, .pixels = small[0..] }, .{ .a = 0 });
3533 try executor.renderCommandsPacked(commands[0..], .{ .width = 8, .height = 8, .pixels = large[0..] }, .{ .a = 0 });
3534
3535 try std.testing.expectEqual(@as(usize, 4), state.create_count);
3536 try std.testing.expectEqual(load_count, state.load_count);
3537 try std.testing.expectEqual(@as(usize, 24), state.buffer_allocate_count);
3538 try std.testing.expect(state.destroy_count >= 11);
3539 }