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 }