lib/gpu/src/recording.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const choir_abi = @import("choir_abi");
   3 
   4 const backend = @import("root.zig");
   5 
   6 const RecordedSurface = struct {
   7     id: ?backend.BackendObjectId = null,
   8     platform: backend.SurfacePlatformKind = .headless,
   9     extent: backend.SurfaceExtent = .{},
  10     format: backend.TextureFormat = .rgba8_unorm,
  11     color_space: backend.ColorSpace = .srgb,
  12     present_mode: backend.PresentMode = .fifo,
  13     generation: u64 = 1,
  14     acquired_frame: ?backend.BackendObjectId = null,
  15     acquired_texture: ?backend.BackendObjectId = null,
  16     destroyed: bool = false,
  17 };
  18 
  19 const RecordedTexture = struct {
  20     id: ?backend.BackendObjectId = null,
  21     extent: backend.TextureExtent = .{},
  22     format: backend.TextureFormat = .rgba8_unorm,
  23     usage: backend.TextureUsage = .{},
  24     sample_count: u32 = 1,
  25     ownership: backend.TextureOwnership = .backend,
  26     destroyed: bool = false,
  27 };
  28 
  29 const RecordedFrame = struct {
  30     id: ?backend.BackendObjectId = null,
  31     surface_id: backend.BackendObjectId = 0,
  32     texture_id: backend.BackendObjectId = 0,
  33     generation: u64 = 1,
  34     written: bool = false,
  35     presented: bool = false,
  36 };
  37 
  38 const RecordedRenderArtifact = struct {
  39     id: ?backend.BackendObjectId = null,
  40     format: backend.RenderArtifactFormat = .external,
  41     target_format: backend.TextureFormat = .rgba8_unorm,
  42     blend_mode: backend.RenderBlendMode = .replace,
  43     topology: backend.RenderPrimitiveTopology = .triangle_list,
  44     destroyed: bool = false,
  45 };
  46 
  47 pub const BackendState = struct {
  48     allocator: std.mem.Allocator,
  49     kind: backend.BackendKind = .cuda,
  50     format: backend.ArtifactFormat = .cuda_ptx,
  51     next_id: backend.BackendObjectId = 1,
  52     create_count: usize = 0,
  53     last_create_had_payload: bool = false,
  54     last_create_argument_count: u32 = 0,
  55     last_create_required_dtype_bits: u64 = 0,
  56     load_count: usize = 0,
  57     fail_load_after_count: ?usize = null,
  58     fail_launch_after_count: ?usize = null,
  59     fail_record: bool = false,
  60     fail_sync: bool = false,
  61     fail_read: bool = false,
  62     launch_count: usize = 0,
  63     destroy_count: usize = 0,
  64     destroyed_ids: [64]backend.BackendObjectId = @splat(0),
  65     last_loaded_id: ?backend.BackendObjectId = null,
  66     last_destroyed_id: ?backend.BackendObjectId = null,
  67     last_launch_loaded_id: ?backend.BackendObjectId = null,
  68     last_launch_buffer_count: usize = 0,
  69     last_launch_scalar_count: usize = 0,
  70     last_launch_scalar_u32: ?u32 = null,
  71     last_launch_scalar_u32_values: [16]u32 = @splat(0),
  72     last_launch_scalar_f32_values: [16]f32 = @splat(0),
  73     last_launch_grid: [3]u32 = .{ 0, 0, 0 },
  74     last_launch_threadgroup: [3]u32 = .{ 0, 0, 0 },
  75     last_launch_dynamic_shared_memory_bytes: u32 = 0,
  76     last_buffer_ids: [8]backend.BackendObjectId = @splat(0),
  77     last_buffer_access: [8]backend.BufferAccess = @splat(.read_only),
  78     last_launch_stream: ?backend.BackendObjectId = null,
  79     last_launch_wait_count: usize = 0,
  80     last_launch_wait_events: [8]backend.BackendObjectId = @splat(0),
  81     last_launch_signal_event: ?backend.BackendObjectId = null,
  82     buffer_allocate_count: usize = 0,
  83     fail_buffer_allocate_after_count: ?usize = null,
  84     allocated_buffer_ids: [64]backend.BackendObjectId = @splat(0),
  85     allocated_buffer_byte_sizes: [64]usize = @splat(0),
  86     last_buffer_allocation_byte_size: usize = 0,
  87     last_buffer_allocation_alignment: u32 = 0,
  88     last_buffer_allocation_element_count: ?u64 = null,
  89     write_count: usize = 0,
  90     last_write_buffer_id: ?backend.BackendObjectId = null,
  91     last_write_byte_count: usize = 0,
  92     last_write_u32: ?u32 = null,
  93     launch_streams: [8]?backend.BackendObjectId = @splat(null),
  94     launch_grids: [8][3]u32 = @splat(.{ 0, 0, 0 }),
  95     launch_scalar_u32s: [8]?u32 = @splat(null),
  96     launch_wait_counts: [8]usize = @splat(0),
  97     launch_wait_events: [8][4]backend.BackendObjectId = @splat(@splat(0)),
  98     launch_signal_events: [8]?backend.BackendObjectId = @splat(null),
  99     launch_buffer_counts: [8]usize = @splat(0),
 100     launch_buffer_ids: [8][8]backend.BackendObjectId = @splat(@splat(0)),
 101     launch_buffer_access: [8][8]backend.BufferAccess = @splat(@splat(.read_only)),
 102     created_stream_count: usize = 0,
 103     created_streams: [8]backend.BackendObjectId = @splat(0),
 104     created_event_count: usize = 0,
 105     created_events: [8]backend.BackendObjectId = @splat(0),
 106     ready_event_count: usize = 0,
 107     ready_events: [16]backend.BackendObjectId = @splat(0),
 108     last_event_query_id: ?backend.BackendObjectId = null,
 109     record_event_count: usize = 0,
 110     record_streams: [8]?backend.BackendObjectId = @splat(null),
 111     record_events: [8]?backend.BackendObjectId = @splat(null),
 112     elapsed_event_count: usize = 0,
 113     elapsed_start_events: [8]?backend.BackendObjectId = @splat(null),
 114     elapsed_end_events: [8]?backend.BackendObjectId = @splat(null),
 115     event_elapsed_ns: u64 = 0,
 116     sync_count: usize = 0,
 117     last_sync_scope: ?backend.SyncScope = null,
 118     last_sync_stream: ?backend.BackendObjectId = null,
 119     last_sync_event: ?backend.BackendObjectId = null,
 120     read_count: usize = 0,
 121     last_read_buffer_id: ?backend.BackendObjectId = null,
 122     last_read_byte_count: usize = 0,
 123     surface_create_count: usize = 0,
 124     texture_allocate_count: usize = 0,
 125     surface_destroy_count: usize = 0,
 126     texture_destroy_count: usize = 0,
 127     surface_acquire_count: usize = 0,
 128     surface_present_count: usize = 0,
 129     surface_write_count: usize = 0,
 130     render_create_count: usize = 0,
 131     render_load_count: usize = 0,
 132     render_count: usize = 0,
 133     render_bindings_count: usize = 0,
 134     bundle_record_count: usize = 0,
 135     bundle_submit_count: usize = 0,
 136     texture_write_count: usize = 0,
 137     surfaces: [16]RecordedSurface = @as([16]RecordedSurface, @splat(.{})),
 138     textures: [32]RecordedTexture = @as([32]RecordedTexture, @splat(.{})),
 139     frames: [16]RecordedFrame = @as([16]RecordedFrame, @splat(.{})),
 140     render_artifacts: [16]RecordedRenderArtifact = @as([16]RecordedRenderArtifact, @splat(.{})),
 141     last_surface_id: ?backend.BackendObjectId = null,
 142     last_texture_id: ?backend.BackendObjectId = null,
 143     last_surface_frame_id: ?backend.BackendObjectId = null,
 144     last_presented_frame_id: ?backend.BackendObjectId = null,
 145     last_present_wait_count: usize = 0,
 146     last_present_wait_events: [8]backend.BackendObjectId = @splat(0),
 147     last_written_frame_id: ?backend.BackendObjectId = null,
 148     last_surface_write_op_count: usize = 0,
 149     last_surface_write_copy_buffer_id: ?backend.BackendObjectId = null,
 150     last_surface_write_wait_count: usize = 0,
 151     last_surface_write_wait_events: [8]backend.BackendObjectId = @splat(0),
 152     last_surface_write_signal_event: ?backend.BackendObjectId = null,
 153     last_render_artifact_id: ?backend.BackendObjectId = null,
 154     last_loaded_render_artifact_id: ?backend.BackendObjectId = null,
 155     last_render_loaded_artifact_id: ?backend.BackendObjectId = null,
 156     last_render_target_texture_id: ?backend.BackendObjectId = null,
 157     last_render_draw_count: usize = 0,
 158     last_render_vertex_buffer_count: usize = 0,
 159     last_render_indexed: bool = false,
 160     last_render_vertex_count: u32 = 0,
 161     last_render_index_count: u32 = 0,
 162     last_render_instance_count: u32 = 0,
 163     last_render_viewport: backend.RenderViewport = .{ .width = 1, .height = 1 },
 164     last_render_scissor: backend.RenderScissor = .{ .width = 1, .height = 1 },
 165     last_surface_platform: ?backend.SurfacePlatformKind = null,
 166     last_surface_format: ?backend.TextureFormat = null,
 167     last_texture_format: ?backend.TextureFormat = null,
 168     last_render_format: ?backend.RenderArtifactFormat = null,
 169 
 170     pub fn handle(self: *BackendState) backend.BackendHandle {
 171         return .{
 172             .ptr = self,
 173             .vtable = &vtable,
 174             .kind = self.kind,
 175         };
 176     }
 177 };
 178 
 179 fn queryCapabilities(ptr: *anyopaque) backend.BackendError!backend.BackendCapabilities {
 180     const state: *BackendState = @ptrCast(@alignCast(ptr));
 181     const family: backend.DeviceFamily = switch (state.kind) {
 182         .cuda => .nvidia_cuda,
 183         .vulkan => .vulkan,
 184         .metal => .apple_metal,
 185         .webgpu => .webgpu,
 186         .wasm => .webassembly,
 187         else => .external,
 188     };
 189     return .{
 190         .identity = .{
 191             .backend = state.kind,
 192             .family = family,
 193             .name = "recording",
 194         },
 195         .subgroup = switch (state.kind) {
 196             .cuda => .{
 197                 .supported = true,
 198                 .size_min = 32,
 199                 .size_max = 32,
 200                 .shuffle = true,
 201                 .ballot = true,
 202                 .vote = true,
 203                 .arithmetic = true,
 204                 .scan = true,
 205             },
 206             .metal, .vulkan => .{
 207                 .supported = true,
 208                 .size_min = 32,
 209                 .size_max = 32,
 210                 .shuffle = true,
 211                 .ballot = true,
 212                 .vote = true,
 213                 .arithmetic = true,
 214                 .scan = true,
 215             },
 216             else => .{},
 217         },
 218         .threadgroup = .{
 219             .max_threads = 1024,
 220             .max_blocks = .{ 65_535, 65_535, 65_535 },
 221             .max_threads_per_dim = .{ 1024, 1024, 64 },
 222             .max_grid_per_dim = .{ 65_535, 65_535, 65_535 },
 223             .shared_memory_bytes = 48 * 1024,
 224         },
 225         .dtypes = dtypes(state.kind),
 226         .features = .{
 227             .atomic_i32 = state.kind != .webgpu and state.kind != .wasm,
 228             .atomic_u32 = state.kind != .webgpu and state.kind != .wasm,
 229             .atomic_index = state.kind != .webgpu and state.kind != .wasm,
 230             .atomic_f32_add_device = state.kind == .cuda or state.kind == .metal,
 231             .atomic_f32_add_shared = state.kind == .cuda,
 232             .async_copy = state.kind == .metal,
 233             .tensor_cores = state.kind == .cuda,
 234             .dynamic_shared_memory = state.kind == .cuda,
 235         },
 236         .runtime = .{
 237             .driver_loaded = true,
 238             .device_context = true,
 239             .streams = true,
 240             .events = true,
 241         },
 242         .textures = switch (state.kind) {
 243             .vulkan, .metal, .webgpu => .{
 244                 .supported = true,
 245                 .formats = backend.TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm, .depth32_float }),
 246                 .usages = .{
 247                     .copy_src = true,
 248                     .copy_dst = true,
 249                     .sampled = true,
 250                     .storage = true,
 251                     .color_attachment = true,
 252                     .depth_attachment = true,
 253                     .present = true,
 254                 },
 255                 .max_extent = .{ .width = 16_384, .height = 16_384, .depth = 256 },
 256                 .max_sample_count = 4,
 257             },
 258             else => .{},
 259         },
 260         .surfaces = switch (state.kind) {
 261             .vulkan => .{
 262                 .supported = true,
 263                 .platforms = backend.SurfacePlatformSet.init(&.{ .x11, .headless }),
 264                 .formats = backend.TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm }),
 265                 .color_spaces = backend.ColorSpaceSet.init(&.{ .srgb, .linear }),
 266                 .present_modes = backend.PresentModeSet.init(&.{ .fifo, .mailbox, .immediate }),
 267                 .usages = .{ .copy_dst = true, .storage = true, .color_attachment = true, .present = true },
 268                 .max_extent = .{ .width = 16_384, .height = 16_384 },
 269                 .max_frames_in_flight = 3,
 270             },
 271             .metal => .{
 272                 .supported = true,
 273                 .platforms = backend.SurfacePlatformSet.init(&.{ .cocoa, .headless }),
 274                 .formats = backend.TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm }),
 275                 .color_spaces = backend.ColorSpaceSet.init(&.{ .srgb, .linear }),
 276                 .present_modes = backend.PresentModeSet.init(&.{ .fifo, .mailbox }),
 277                 .usages = .{ .copy_dst = true, .color_attachment = true, .present = true },
 278                 .max_extent = .{ .width = 16_384, .height = 16_384 },
 279                 .max_frames_in_flight = 3,
 280             },
 281             .webgpu => .{
 282                 .supported = true,
 283                 .platforms = backend.SurfacePlatformSet.init(&.{ .webgpu_canvas, .headless }),
 284                 .formats = backend.TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm }),
 285                 .color_spaces = backend.ColorSpaceSet.init(&.{.srgb}),
 286                 .present_modes = backend.PresentModeSet.init(&.{.fifo}),
 287                 .usages = .{ .copy_dst = true, .storage = true, .color_attachment = true, .present = true },
 288                 .max_extent = .{ .width = 16_384, .height = 16_384 },
 289                 .max_frames_in_flight = 2,
 290             },
 291             else => .{},
 292         },
 293         .raster = switch (state.kind) {
 294             .vulkan, .metal, .webgpu => .{
 295                 .supported = true,
 296                 .artifact_formats = backend.RenderArtifactFormatSet.init(&.{renderFormatForKind(state.kind)}),
 297                 .target_formats = backend.TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm }),
 298                 .depth_formats = backend.TextureFormatSet.init(&.{.depth32_float}),
 299                 .blend_modes = backend.RenderBlendModeSet.init(&.{ .replace, .alpha_premultiplied, .alpha_straight, .additive }),
 300                 .topologies = backend.RenderPrimitiveTopologySet.init(&.{ .triangle_list, .triangle_strip, .line_list, .line_strip }),
 301                 .vertex_formats = backend.RenderVertexFormatSet.init(&.{ .float32, .float32x2, .float32x3, .float32x4, .uint32, .uint32x2, .uint32x4 }),
 302                 .binding_kinds = backend.RenderBindingKindSet.init(&.{ .uniform_buffer, .storage_buffer, .sampled_texture, .storage_texture }),
 303                 .index_formats = backend.RenderIndexFormatSet.init(&.{ .none, .u16, .u32 }),
 304                 .max_vertex_buffers = 16,
 305                 .max_vertex_attributes = 32,
 306                 .max_bindings = 32,
 307                 .instancing = true,
 308             },
 309             else => .{},
 310         },
 311         .artifact_formats = backend.ArtifactFormatSet.init(&.{state.format}),
 312     };
 313 }
 314 
 315 fn dtypes(kind: backend.BackendKind) backend.DTypeSet {
 316     return switch (kind) {
 317         .cuda => backend.DTypeSet.init(&.{
 318             .i1,  .i8,   .i16, .i32, .i64, .u8, .u16, .u32, .u64,
 319             .f16, .bf16, .f32, .f64, .key,
 320         }),
 321         .webgpu => backend.DTypeSet.init(&.{ .i1, .f32, .i32, .u32 }),
 322         .wasm => backend.DTypeSet.init(&.{ .i1, .f32, .f64, .i32, .u32, .i64, .u64 }),
 323         else => backend.DTypeSet.init(&.{ .i1, .f32, .f16, .i32, .u32, .u64, .key }),
 324     };
 325 }
 326 
 327 fn renderFormatForKind(kind: backend.BackendKind) backend.RenderArtifactFormat {
 328     return switch (kind) {
 329         .vulkan => .vulkan_spirv,
 330         .metal => .metal_msl,
 331         .webgpu => .webgpu_wgsl,
 332         else => .external,
 333     };
 334 }
 335 
 336 fn createArtifact(
 337     ptr: *anyopaque,
 338     request: backend.CompileRequest,
 339 ) backend.BackendError!backend.KernelArtifact {
 340     const state: *BackendState = @ptrCast(@alignCast(ptr));
 341     if (request.requested_format != state.format) return error.UnsupportedArtifactFormat;
 342     state.create_count += 1;
 343     state.last_create_had_payload = switch (request.payload) {
 344         .none => false,
 345         else => true,
 346     };
 347     state.last_create_argument_count = request.argument_count;
 348     state.last_create_required_dtype_bits = request.required_dtypes.bits;
 349     var artifact = backend.KernelArtifact.init(state.allocator, .{
 350         .backend = state.kind,
 351         .format = state.format,
 352         .entry_name = request.kernel_name,
 353         .argument_count = request.argument_count,
 354         .scalar_argument_count = request.scalar_argument_count,
 355         .diagnostic_id = request.diagnostic_id,
 356     }) catch return error.OutOfMemory;
 357     errdefer artifact.deinit();
 358     switch (state.format) {
 359         .cuda_ptx, .metal_msl, .webgpu_wgsl => artifact.setBorrowedText("recording"),
 360         .vulkan_spirv => artifact.setBorrowedWords(&spirv),
 361         .webassembly_module => artifact.setBorrowedBytes(&.{ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00 }),
 362         else => return error.UnsupportedArtifactFormat,
 363     }
 364     return artifact;
 365 }
 366 
 367 const spirv = [_]u32{ 0x07230203, 0x00010000, 0, 0 };
 368 
 369 fn loadArtifact(
 370     ptr: *anyopaque,
 371     artifact: *const backend.KernelArtifact,
 372 ) backend.BackendError!backend.LoadedArtifact {
 373     const state: *BackendState = @ptrCast(@alignCast(ptr));
 374     if (artifact.backend != state.kind or artifact.format != state.format) return error.InvalidArtifact;
 375     if (state.fail_load_after_count) |limit| {
 376         if (state.load_count >= limit) return error.RuntimeUnavailable;
 377     }
 378     const id = state.next_id;
 379     state.next_id += 1;
 380     state.load_count += 1;
 381     state.last_loaded_id = id;
 382     return .{
 383         .id = id,
 384         .backend = state.kind,
 385         .format = state.format,
 386     };
 387 }
 388 
 389 fn createRenderArtifact(
 390     ptr: *anyopaque,
 391     desc: backend.RenderPipelineDesc,
 392 ) backend.BackendError!backend.RenderArtifact {
 393     const state: *BackendState = @ptrCast(@alignCast(ptr));
 394     if (desc.format != renderFormatForKind(state.kind)) return error.UnsupportedArtifactFormat;
 395     const slot = freeRenderArtifactSlot(state) orelse return error.OutOfMemory;
 396     const id = state.next_id;
 397     state.next_id += 1;
 398     slot.* = .{
 399         .id = id,
 400         .format = desc.format,
 401         .target_format = desc.target_format,
 402         .blend_mode = desc.blend_mode,
 403         .topology = desc.topology,
 404     };
 405     state.render_create_count += 1;
 406     state.last_render_artifact_id = id;
 407     state.last_render_format = desc.format;
 408     var artifact = backend.RenderArtifact.init(state.allocator, .{
 409         .backend = state.kind,
 410         .pipeline = desc,
 411     }) catch return error.OutOfMemory;
 412     errdefer artifact.deinit();
 413     switch (desc.format) {
 414         .vulkan_spirv => artifact.setBorrowedWords(&spirv),
 415         .metal_msl, .webgpu_wgsl => artifact.setBorrowedText("recording-render"),
 416         .metal_metallib => artifact.setBorrowedBytes(&.{ 0xca, 0xfe, 0xba, 0xbe }),
 417         .external => artifact.setBorrowedText("recording-render-external"),
 418         .cpu_object => artifact.setBorrowedBytes(&.{ 0x7f, 'E', 'L', 'F' }),
 419     }
 420     return artifact;
 421 }
 422 
 423 fn loadRenderArtifact(
 424     ptr: *anyopaque,
 425     artifact: *const backend.RenderArtifact,
 426 ) backend.BackendError!backend.LoadedRenderArtifact {
 427     const state: *BackendState = @ptrCast(@alignCast(ptr));
 428     if (artifact.backend != state.kind or artifact.format != renderFormatForKind(state.kind)) return error.InvalidRenderArtifact;
 429     const id = state.next_id;
 430     state.next_id += 1;
 431     state.render_load_count += 1;
 432     state.last_loaded_render_artifact_id = id;
 433     state.last_render_format = artifact.format;
 434     return backend.LoadedRenderArtifact.describing(artifact, id);
 435 }
 436 
 437 /// Records the pass's last draw. The recording backend holds no texels, so it draws nothing and
 438 /// refuses texture reads.
 439 fn render(
 440     ptr: *anyopaque,
 441     request: backend.RenderRequest,
 442 ) backend.BackendError!void {
 443     const state: *BackendState = @ptrCast(@alignCast(ptr));
 444     try recordPass(state, request.pass);
 445     if (request.signal_event) |event| try markEventReady(state, event);
 446 }
 447 
 448 fn recordPass(state: *BackendState, pass: backend.RenderPass) backend.BackendError!void {
 449     _ = try recordedTexture(state, pass.color.view.texture.id);
 450     if (pass.depth) |depth| _ = try recordedTexture(state, depth.view.texture.id);
 451     state.render_count += 1;
 452     state.last_render_draw_count = pass.draws.len;
 453     state.last_render_target_texture_id = pass.color.view.texture.id;
 454     state.last_render_viewport = pass.viewport;
 455     state.last_render_scissor = pass.scissor;
 456     for (pass.draws) |draw| {
 457         if (draw.pipeline.backend != state.kind) return error.InvalidRenderArtifact;
 458         state.last_render_loaded_artifact_id = draw.pipeline.id;
 459         state.last_render_vertex_buffer_count = draw.vertex_buffers.len;
 460         state.last_render_indexed = draw.range.index_count != 0;
 461         state.last_render_vertex_count = draw.range.vertex_count;
 462         state.last_render_index_count = draw.range.index_count;
 463         state.last_render_instance_count = draw.range.instance_count;
 464         state.last_render_format = draw.pipeline.format;
 465     }
 466 }
 467 
 468 fn createRenderBindings(
 469     ptr: *anyopaque,
 470     request: backend.RenderBindingsRequest,
 471 ) backend.BackendError!backend.RenderBindings {
 472     const state: *BackendState = @ptrCast(@alignCast(ptr));
 473     const id = state.next_id;
 474     state.next_id += 1;
 475     state.render_bindings_count += 1;
 476     return .{ .id = id, .backend = state.kind, .pipeline = request.pipeline.id };
 477 }
 478 
 479 fn recordRenderBundle(
 480     ptr: *anyopaque,
 481     pass: backend.RenderPass,
 482 ) backend.BackendError!backend.RenderBundle {
 483     const state: *BackendState = @ptrCast(@alignCast(ptr));
 484     const id = state.next_id;
 485     state.next_id += 1;
 486     state.bundle_record_count += 1;
 487     return .{ .id = id, .backend = state.kind, .draw_count = @intCast(pass.draws.len) };
 488 }
 489 
 490 fn submitRenderBundle(
 491     ptr: *anyopaque,
 492     request: backend.RenderBundleSubmit,
 493 ) backend.BackendError!void {
 494     const state: *BackendState = @ptrCast(@alignCast(ptr));
 495     state.bundle_submit_count += 1;
 496     if (request.signal_event) |event| try markEventReady(state, event);
 497 }
 498 
 499 fn writeTexture(
 500     ptr: *anyopaque,
 501     request: backend.TextureWriteRequest,
 502 ) backend.BackendError!void {
 503     const state: *BackendState = @ptrCast(@alignCast(ptr));
 504     _ = try recordedTexture(state, request.texture.id);
 505     state.texture_write_count += 1;
 506 }
 507 
 508 fn allocateBuffer(
 509     ptr: *anyopaque,
 510     request: backend.BufferAllocation,
 511 ) backend.BackendError!backend.BufferHandle {
 512     const state: *BackendState = @ptrCast(@alignCast(ptr));
 513     if (state.fail_buffer_allocate_after_count) |limit| {
 514         if (state.buffer_allocate_count >= limit) return error.OutOfMemory;
 515     }
 516     const id = state.next_id;
 517     state.next_id += 1;
 518     if (state.buffer_allocate_count >= state.allocated_buffer_ids.len) return error.OutOfMemory;
 519     state.allocated_buffer_ids[state.buffer_allocate_count] = id;
 520     state.allocated_buffer_byte_sizes[state.buffer_allocate_count] = request.byte_size;
 521     state.buffer_allocate_count += 1;
 522     state.last_buffer_allocation_byte_size = request.byte_size;
 523     state.last_buffer_allocation_alignment = request.alignment;
 524     state.last_buffer_allocation_element_count = request.element_count;
 525     return .{
 526         .id = id,
 527         .backend = state.kind,
 528         .byte_size = request.byte_size,
 529         .ownership = .backend,
 530     };
 531 }
 532 
 533 fn createSurface(
 534     ptr: *anyopaque,
 535     request: backend.SurfaceCreationRequest,
 536 ) backend.BackendError!backend.SurfaceHandle {
 537     const state: *BackendState = @ptrCast(@alignCast(ptr));
 538     const slot = freeSurfaceSlot(state) orelse return error.OutOfMemory;
 539     const id = state.next_id;
 540     state.next_id += 1;
 541     slot.* = .{
 542         .id = id,
 543         .platform = request.platform.kind(),
 544         .extent = request.extent,
 545         .format = request.format,
 546         .color_space = request.color_space,
 547         .present_mode = request.present_mode,
 548         .generation = 1,
 549     };
 550     state.surface_create_count += 1;
 551     state.last_surface_id = id;
 552     state.last_surface_platform = request.platform.kind();
 553     state.last_surface_format = request.format;
 554     return .{
 555         .id = id,
 556         .backend = state.kind,
 557         .platform = request.platform.kind(),
 558         .extent = request.extent,
 559         .format = request.format,
 560         .color_space = request.color_space,
 561         .present_mode = request.present_mode,
 562         .generation = 1,
 563     };
 564 }
 565 
 566 fn destroySurface(
 567     ptr: *anyopaque,
 568     handle: backend.SurfaceHandle,
 569 ) backend.BackendError!void {
 570     const state: *BackendState = @ptrCast(@alignCast(ptr));
 571     const surface = try recordedSurface(state, handle.id);
 572     if (surface.generation != handle.generation) return error.SurfaceFrameExpired;
 573     if (surface.acquired_frame != null) return error.SurfaceAlreadyAcquired;
 574     surface.destroyed = true;
 575     state.surface_destroy_count += 1;
 576     state.last_destroyed_id = handle.id;
 577 }
 578 
 579 fn allocateTexture(
 580     ptr: *anyopaque,
 581     request: backend.TextureAllocation,
 582 ) backend.BackendError!backend.TextureHandle {
 583     const state: *BackendState = @ptrCast(@alignCast(ptr));
 584     const slot = freeTextureSlot(state) orelse return error.OutOfMemory;
 585     const id = state.next_id;
 586     state.next_id += 1;
 587     slot.* = .{
 588         .id = id,
 589         .extent = request.extent,
 590         .format = request.format,
 591         .usage = request.usage,
 592         .sample_count = request.sample_count,
 593         .ownership = .backend,
 594     };
 595     state.texture_allocate_count += 1;
 596     state.last_texture_id = id;
 597     state.last_texture_format = request.format;
 598     return .{
 599         .id = id,
 600         .backend = state.kind,
 601         .extent = request.extent,
 602         .format = request.format,
 603         .usage = request.usage,
 604         .sample_count = request.sample_count,
 605         .ownership = .backend,
 606     };
 607 }
 608 
 609 fn destroyTexture(
 610     ptr: *anyopaque,
 611     handle: backend.TextureHandle,
 612 ) backend.BackendError!void {
 613     const state: *BackendState = @ptrCast(@alignCast(ptr));
 614     const texture = try recordedTexture(state, handle.id);
 615     for (state.surfaces) |surface| {
 616         if (surface.id != null and !surface.destroyed and surface.acquired_texture == handle.id) return error.SurfaceAlreadyAcquired;
 617     }
 618     texture.destroyed = true;
 619     state.texture_destroy_count += 1;
 620     state.last_destroyed_id = handle.id;
 621 }
 622 
 623 fn acquireSurfaceFrame(
 624     ptr: *anyopaque,
 625     request: backend.SurfaceFrameAcquireRequest,
 626 ) backend.BackendError!backend.SurfaceFrame {
 627     const state: *BackendState = @ptrCast(@alignCast(ptr));
 628     const surface = try recordedSurface(state, request.surface.id);
 629     if (surface.generation != request.surface.generation) return error.SurfaceFrameExpired;
 630     if (surface.acquired_frame != null) return error.SurfaceAlreadyAcquired;
 631     const texture_slot = freeTextureSlot(state) orelse return error.OutOfMemory;
 632     const frame_slot = freeFrameSlot(state) orelse return error.OutOfMemory;
 633     const texture_id = state.next_id;
 634     state.next_id += 1;
 635     const frame_id = state.next_id;
 636     state.next_id += 1;
 637     const texture = backend.TextureHandle{
 638         .id = texture_id,
 639         .backend = state.kind,
 640         .extent = .{
 641             .width = surface.extent.width,
 642             .height = surface.extent.height,
 643             .depth = 1,
 644         },
 645         .format = surface.format,
 646         .usage = .{ .copy_dst = true, .storage = true, .color_attachment = true, .present = true },
 647         .sample_count = 1,
 648         .ownership = .acquired_surface,
 649     };
 650     texture_slot.* = .{
 651         .id = texture_id,
 652         .extent = texture.extent,
 653         .format = texture.format,
 654         .usage = texture.usage,
 655         .sample_count = texture.sample_count,
 656         .ownership = texture.ownership,
 657     };
 658     frame_slot.* = .{
 659         .id = frame_id,
 660         .surface_id = request.surface.id,
 661         .texture_id = texture_id,
 662         .generation = surface.generation,
 663     };
 664     surface.acquired_frame = frame_id;
 665     surface.acquired_texture = texture_id;
 666     state.surface_acquire_count += 1;
 667     state.last_surface_frame_id = frame_id;
 668     state.last_texture_id = texture_id;
 669     const surface_handle = backend.SurfaceHandle{
 670         .id = request.surface.id,
 671         .backend = state.kind,
 672         .platform = surface.platform,
 673         .extent = surface.extent,
 674         .format = surface.format,
 675         .color_space = surface.color_space,
 676         .present_mode = surface.present_mode,
 677         .generation = surface.generation,
 678     };
 679     const view = backend.TextureView{
 680         .texture = texture,
 681         .format = texture.format,
 682     };
 683     return .{
 684         .id = frame_id,
 685         .backend = state.kind,
 686         .surface = surface_handle,
 687         .texture = texture,
 688         .view = view,
 689         .index = @intCast(state.surface_acquire_count - 1),
 690         .generation = surface.generation,
 691         .token = frame_id,
 692     };
 693 }
 694 
 695 fn presentSurfaceFrame(
 696     ptr: *anyopaque,
 697     request: backend.PresentRequest,
 698 ) backend.BackendError!void {
 699     const state: *BackendState = @ptrCast(@alignCast(ptr));
 700     if (request.signal_event != null) return error.UnsupportedOperation;
 701     const surface = try recordedSurface(state, request.surface.id);
 702     const frame = try recordedFrame(state, request.frame.id);
 703     if (surface.generation != request.surface.generation) return error.SurfaceFrameExpired;
 704     if (surface.generation != frame.generation or request.frame.generation != frame.generation) return error.SurfaceFrameExpired;
 705     if (frame.presented) return error.SurfaceFrameExpired;
 706     if (frame.surface_id != surface.id.?) return error.InvalidSurfaceFrame;
 707     if (surface.acquired_frame == null or surface.acquired_frame.? != request.frame.id) return error.InvalidSurfaceFrame;
 708     _ = try recordedTexture(state, frame.texture_id);
 709     if (request.wait_events.len > state.last_present_wait_events.len) return error.LaunchArgumentMismatch;
 710     for (request.wait_events) |event| {
 711         if (!eventReady(state, event)) return error.InvalidEvent;
 712     }
 713     state.last_present_wait_count = request.wait_events.len;
 714     for (request.wait_events, 0..) |event, index| {
 715         state.last_present_wait_events[index] = event.id;
 716     }
 717     frame.presented = true;
 718     surface.acquired_frame = null;
 719     surface.acquired_texture = null;
 720     state.surface_present_count += 1;
 721     state.last_presented_frame_id = request.frame.id;
 722 }
 723 
 724 fn writeSurfaceFrame(
 725     ptr: *anyopaque,
 726     request: backend.SurfaceFrameWriteRequest,
 727 ) backend.BackendError!void {
 728     const state: *BackendState = @ptrCast(@alignCast(ptr));
 729     const surface = try recordedSurface(state, request.surface.id);
 730     const frame = try recordedFrame(state, request.frame.id);
 731     if (surface.generation != request.surface.generation) return error.SurfaceFrameExpired;
 732     if (surface.generation != frame.generation or request.frame.generation != frame.generation) return error.SurfaceFrameExpired;
 733     if (frame.presented or frame.written) return error.SurfaceFrameExpired;
 734     if (frame.surface_id != surface.id.?) return error.InvalidSurfaceFrame;
 735     if (frame.texture_id != request.frame.texture.id) return error.InvalidSurfaceFrame;
 736     if (surface.acquired_frame == null or surface.acquired_frame.? != request.frame.id) return error.InvalidSurfaceFrame;
 737     const texture = try recordedTexture(state, frame.texture_id);
 738     if (!texture.usage.copy_dst) return error.InvalidTexture;
 739     var copy_buffer_id: ?backend.BackendObjectId = null;
 740     for (request.operations) |op| switch (op) {
 741         .clear => {},
 742         .copy_buffer => |buffer| copy_buffer_id = buffer.id,
 743     };
 744     if (request.wait_events.len > state.last_surface_write_wait_events.len) return error.LaunchArgumentMismatch;
 745     state.last_surface_write_wait_count = request.wait_events.len;
 746     for (request.wait_events, 0..) |event, index| {
 747         state.last_surface_write_wait_events[index] = event.id;
 748     }
 749     state.last_surface_write_signal_event = if (request.signal_event) |event| event.id else null;
 750     if (request.signal_event) |event| try markEventReady(state, event);
 751     frame.written = true;
 752     state.surface_write_count += 1;
 753     state.last_written_frame_id = request.frame.id;
 754     state.last_surface_write_op_count = request.operations.len;
 755     state.last_surface_write_copy_buffer_id = copy_buffer_id;
 756 }
 757 
 758 fn freeSurfaceSlot(state: *BackendState) ?*RecordedSurface {
 759     for (&state.surfaces) |*slot| {
 760         if (slot.id == null or slot.destroyed) return slot;
 761     }
 762     return null;
 763 }
 764 
 765 fn freeTextureSlot(state: *BackendState) ?*RecordedTexture {
 766     for (&state.textures) |*slot| {
 767         if (slot.id == null or slot.destroyed) return slot;
 768     }
 769     return null;
 770 }
 771 
 772 fn freeFrameSlot(state: *BackendState) ?*RecordedFrame {
 773     for (&state.frames) |*slot| {
 774         if (slot.id == null or slot.presented) return slot;
 775     }
 776     return null;
 777 }
 778 
 779 fn freeRenderArtifactSlot(state: *BackendState) ?*RecordedRenderArtifact {
 780     for (&state.render_artifacts) |*slot| {
 781         if (slot.id == null or slot.destroyed) return slot;
 782     }
 783     return null;
 784 }
 785 
 786 fn recordedSurface(state: *BackendState, id: backend.BackendObjectId) backend.BackendError!*RecordedSurface {
 787     for (&state.surfaces) |*slot| {
 788         if (slot.id) |actual| {
 789             if (actual == id and !slot.destroyed) return slot;
 790         }
 791     }
 792     return error.InvalidSurface;
 793 }
 794 
 795 fn recordedTexture(state: *BackendState, id: backend.BackendObjectId) backend.BackendError!*RecordedTexture {
 796     for (&state.textures) |*slot| {
 797         if (slot.id) |actual| {
 798             if (actual == id and !slot.destroyed) return slot;
 799         }
 800     }
 801     return error.InvalidTexture;
 802 }
 803 
 804 fn recordedFrame(state: *BackendState, id: backend.BackendObjectId) backend.BackendError!*RecordedFrame {
 805     for (&state.frames) |*slot| {
 806         if (slot.id) |actual| {
 807             if (actual == id) return slot;
 808         }
 809     }
 810     return error.InvalidSurfaceFrame;
 811 }
 812 
 813 fn createStream(ptr: *anyopaque, _: backend.StreamAllocation) backend.BackendError!backend.StreamHandle {
 814     const state: *BackendState = @ptrCast(@alignCast(ptr));
 815     const id = state.next_id;
 816     state.next_id += 1;
 817     if (state.created_stream_count >= state.created_streams.len) return error.OutOfMemory;
 818     state.created_streams[state.created_stream_count] = id;
 819     state.created_stream_count += 1;
 820     return .{
 821         .id = id,
 822         .backend = state.kind,
 823     };
 824 }
 825 
 826 fn createEvent(ptr: *anyopaque, _: backend.EventAllocation) backend.BackendError!backend.EventHandle {
 827     const state: *BackendState = @ptrCast(@alignCast(ptr));
 828     const id = state.next_id;
 829     state.next_id += 1;
 830     if (state.created_event_count >= state.created_events.len) return error.OutOfMemory;
 831     state.created_events[state.created_event_count] = id;
 832     state.created_event_count += 1;
 833     return .{
 834         .id = id,
 835         .backend = state.kind,
 836     };
 837 }
 838 
 839 fn markEventReady(state: *BackendState, event: backend.EventHandle) backend.BackendError!void {
 840     for (state.ready_events[0..state.ready_event_count]) |ready| {
 841         if (ready == event.id) return;
 842     }
 843     if (state.ready_event_count >= state.ready_events.len) return error.OutOfMemory;
 844     state.ready_events[state.ready_event_count] = event.id;
 845     state.ready_event_count += 1;
 846 }
 847 
 848 fn eventReady(state: *const BackendState, event: backend.EventHandle) bool {
 849     for (state.ready_events[0..state.ready_event_count]) |ready| {
 850         if (ready == event.id) return true;
 851     }
 852     return false;
 853 }
 854 
 855 fn queryEvent(ptr: *anyopaque, request: backend.EventQueryRequest) backend.BackendError!bool {
 856     const state: *BackendState = @ptrCast(@alignCast(ptr));
 857     state.last_event_query_id = request.event.id;
 858     return eventReady(state, request.event);
 859 }
 860 
 861 fn launch(ptr: *anyopaque, request: backend.LaunchRequest) backend.BackendError!void {
 862     const state: *BackendState = @ptrCast(@alignCast(ptr));
 863     const loaded = request.loaded_artifact orelse return error.InvalidArtifact;
 864     if (state.fail_launch_after_count) |limit| {
 865         if (state.launch_count >= limit) return error.RuntimeUnavailable;
 866     }
 867     const launch_index = state.launch_count;
 868     state.launch_count += 1;
 869     state.last_launch_loaded_id = loaded.id;
 870     state.last_launch_buffer_count = request.buffers.len;
 871     state.last_launch_scalar_count = request.scalar_arguments.len;
 872     state.last_launch_grid = request.geometry.grid;
 873     state.last_launch_threadgroup = request.geometry.threadgroup;
 874     state.last_launch_dynamic_shared_memory_bytes = request.geometry.dynamic_shared_memory_bytes;
 875     state.last_launch_scalar_u32 = null;
 876     for (request.scalar_arguments, 0..) |arg, index| {
 877         if (index >= state.last_launch_scalar_u32_values.len) return error.LaunchArgumentMismatch;
 878         state.last_launch_scalar_u32_values[index] = 0;
 879         state.last_launch_scalar_f32_values[index] = 0;
 880         switch (arg) {
 881             .u32 => |value| state.last_launch_scalar_u32_values[index] = value,
 882             .f32 => |value| state.last_launch_scalar_f32_values[index] = value,
 883             else => {},
 884         }
 885         if (index == 0) state.last_launch_scalar_u32 = state.last_launch_scalar_u32_values[index];
 886     }
 887     for (request.buffers, 0..) |binding, index| {
 888         if (index >= state.last_buffer_ids.len) return error.LaunchArgumentMismatch;
 889         state.last_buffer_ids[index] = binding.handle.id;
 890         state.last_buffer_access[index] = binding.access;
 891     }
 892     state.last_launch_stream = if (request.stream) |stream| stream.id else null;
 893     state.last_launch_wait_count = request.wait_events.len;
 894     for (request.wait_events, 0..) |event, index| {
 895         if (index >= state.last_launch_wait_events.len) return error.LaunchArgumentMismatch;
 896         state.last_launch_wait_events[index] = event.id;
 897     }
 898     state.last_launch_signal_event = if (request.signal_event) |event| event.id else null;
 899     if (request.signal_event) |event| try markEventReady(state, event);
 900     if (launch_index < state.launch_streams.len) {
 901         state.launch_streams[launch_index] = state.last_launch_stream;
 902         state.launch_grids[launch_index] = request.geometry.grid;
 903         state.launch_scalar_u32s[launch_index] = state.last_launch_scalar_u32;
 904         state.launch_wait_counts[launch_index] = request.wait_events.len;
 905         if (request.wait_events.len > state.launch_wait_events[launch_index].len) return error.LaunchArgumentMismatch;
 906         for (request.wait_events, 0..) |event, index| {
 907             state.launch_wait_events[launch_index][index] = event.id;
 908         }
 909         state.launch_signal_events[launch_index] = state.last_launch_signal_event;
 910         state.launch_buffer_counts[launch_index] = request.buffers.len;
 911         if (request.buffers.len > state.launch_buffer_ids[launch_index].len) return error.LaunchArgumentMismatch;
 912         for (request.buffers, 0..) |binding, index| {
 913             state.launch_buffer_ids[launch_index][index] = binding.handle.id;
 914             state.launch_buffer_access[launch_index][index] = binding.access;
 915         }
 916     }
 917 }
 918 
 919 fn writeBuffer(
 920     ptr: *anyopaque,
 921     request: backend.BufferWriteRequest,
 922 ) backend.BackendError!void {
 923     const state: *BackendState = @ptrCast(@alignCast(ptr));
 924     if (request.bytes.len > request.handle.byte_size) return error.InvalidBuffer;
 925     state.write_count += 1;
 926     state.last_write_buffer_id = request.handle.id;
 927     state.last_write_byte_count = request.bytes.len;
 928     state.last_write_u32 = null;
 929     if (request.bytes.len == @sizeOf(u32)) {
 930         var value: u32 = 0;
 931         @memcpy(std.mem.asBytes(&value), request.bytes);
 932         state.last_write_u32 = value;
 933     }
 934 }
 935 
 936 fn fillBuffer(
 937     ptr: *anyopaque,
 938     request: backend.BufferFillRequest,
 939 ) backend.BackendError!void {
 940     const state: *BackendState = @ptrCast(@alignCast(ptr));
 941     state.write_count += 1;
 942     state.last_write_buffer_id = request.handle.id;
 943     state.last_write_byte_count = request.handle.byte_size;
 944     state.last_write_u32 = request.pattern;
 945 }
 946 
 947 fn readBuffer(
 948     ptr: *anyopaque,
 949     request: backend.BufferReadRequest,
 950 ) backend.BackendError!void {
 951     const state: *BackendState = @ptrCast(@alignCast(ptr));
 952     if (request.bytes.len < request.handle.byte_size) return error.ReadBufferDestinationTooSmall;
 953     state.read_count += 1;
 954     if (state.fail_read) return error.DeviceLost;
 955     state.last_read_buffer_id = request.handle.id;
 956     state.last_read_byte_count = request.bytes.len;
 957     @memset(request.bytes, 0);
 958 }
 959 
 960 fn recordEvent(ptr: *anyopaque, request: backend.EventRecordRequest) backend.BackendError!void {
 961     const state: *BackendState = @ptrCast(@alignCast(ptr));
 962     if (state.fail_record) return error.DeviceLost;
 963     if (state.record_event_count >= state.record_events.len) return error.OutOfMemory;
 964     state.record_streams[state.record_event_count] = request.stream.id;
 965     state.record_events[state.record_event_count] = request.event.id;
 966     state.record_event_count += 1;
 967     try markEventReady(state, request.event);
 968 }
 969 
 970 fn elapsedEventNs(ptr: *anyopaque, request: backend.EventElapsedRequest) backend.BackendError!u64 {
 971     const state: *BackendState = @ptrCast(@alignCast(ptr));
 972     if (state.elapsed_event_count >= state.elapsed_start_events.len) return error.OutOfMemory;
 973     state.elapsed_start_events[state.elapsed_event_count] = request.start.id;
 974     state.elapsed_end_events[state.elapsed_event_count] = request.end.id;
 975     state.elapsed_event_count += 1;
 976     return state.event_elapsed_ns;
 977 }
 978 
 979 fn destroyObject(ptr: *anyopaque, id: backend.BackendObjectId) void {
 980     const state: *BackendState = @ptrCast(@alignCast(ptr));
 981     if (state.destroy_count < state.destroyed_ids.len) state.destroyed_ids[state.destroy_count] = id;
 982     state.destroy_count += 1;
 983     state.last_destroyed_id = id;
 984 }
 985 
 986 fn synchronize(ptr: *anyopaque, request: backend.SyncRequest) backend.BackendError!void {
 987     const state: *BackendState = @ptrCast(@alignCast(ptr));
 988     state.sync_count += 1;
 989     if (state.fail_sync) return error.DeviceLost;
 990     state.last_sync_scope = request.scope;
 991     state.last_sync_stream = if (request.stream) |stream| stream.id else null;
 992     state.last_sync_event = if (request.event) |event| event.id else null;
 993 }
 994 
 995 const vtable = backend.BackendVTable{
 996     .query_capabilities = queryCapabilities,
 997     .create_artifact = createArtifact,
 998     .load_artifact = loadArtifact,
 999     .create_render_artifact = createRenderArtifact,
1000     .load_render_artifact = loadRenderArtifact,
1001     .allocate_buffer = allocateBuffer,
1002     .allocate_texture = allocateTexture,
1003     .create_surface = createSurface,
1004     .destroy_surface = destroySurface,
1005     .destroy_texture = destroyTexture,
1006     .acquire_surface_frame = acquireSurfaceFrame,
1007     .present_surface_frame = presentSurfaceFrame,
1008     .write_surface_frame = writeSurfaceFrame,
1009     .create_stream = createStream,
1010     .create_event = createEvent,
1011     .write_buffer = writeBuffer,
1012     .fill_buffer = fillBuffer,
1013     .read_buffer = readBuffer,
1014     .launch = launch,
1015     .render = render,
1016     .create_render_bindings = createRenderBindings,
1017     .record_render_bundle = recordRenderBundle,
1018     .submit_render_bundle = submitRenderBundle,
1019     .write_texture = writeTexture,
1020     .synchronize = synchronize,
1021     .query_event = queryEvent,
1022     .record_event = recordEvent,
1023     .elapsed_event_ns = elapsedEventNs,
1024     .destroy_object = destroyObject,
1025 };
1026 
1027 test "recording backend reports accelerator feature support by backend kind" {
1028     const allocator = std.testing.allocator;
1029 
1030     var cuda_state = BackendState{
1031         .allocator = allocator,
1032         .kind = .cuda,
1033         .format = .cuda_ptx,
1034     };
1035     var metal_state = BackendState{
1036         .allocator = allocator,
1037         .kind = .metal,
1038         .format = .metal_msl,
1039     };
1040     var vulkan_state = BackendState{
1041         .allocator = allocator,
1042         .kind = .vulkan,
1043         .format = .vulkan_spirv,
1044     };
1045 
1046     try std.testing.expect((try cuda_state.handle().queryCapabilities()).features.atomic_i32);
1047     try std.testing.expect((try metal_state.handle().queryCapabilities()).features.atomic_i32);
1048     try std.testing.expect((try vulkan_state.handle().queryCapabilities()).features.atomic_i32);
1049     try std.testing.expect((try cuda_state.handle().queryCapabilities()).features.atomic_f32_add_device);
1050     try std.testing.expect((try metal_state.handle().queryCapabilities()).features.atomic_f32_add_device);
1051     try std.testing.expect(!(try vulkan_state.handle().queryCapabilities()).features.atomic_f32_add_device);
1052     try std.testing.expect((try cuda_state.handle().queryCapabilities()).features.atomic_f32_add_shared);
1053     try std.testing.expect(!(try metal_state.handle().queryCapabilities()).features.atomic_f32_add_shared);
1054     try std.testing.expect(!(try vulkan_state.handle().queryCapabilities()).features.atomic_f32_add_shared);
1055     try std.testing.expect(!(try cuda_state.handle().queryCapabilities()).features.async_copy);
1056     try std.testing.expect((try metal_state.handle().queryCapabilities()).features.async_copy);
1057     try std.testing.expect(!(try vulkan_state.handle().queryCapabilities()).features.async_copy);
1058     try std.testing.expect((try cuda_state.handle().queryCapabilities()).features.tensor_cores);
1059     try std.testing.expect(!(try metal_state.handle().queryCapabilities()).features.tensor_cores);
1060     try std.testing.expect(!(try vulkan_state.handle().queryCapabilities()).features.tensor_cores);
1061     try std.testing.expect((try cuda_state.handle().queryCapabilities()).features.dynamic_shared_memory);
1062     try std.testing.expect(!(try metal_state.handle().queryCapabilities()).features.dynamic_shared_memory);
1063     try std.testing.expect(!(try vulkan_state.handle().queryCapabilities()).features.dynamic_shared_memory);
1064 }
1065 
1066 test "recording backend records mixed launch scalar arguments" {
1067     const allocator = std.testing.allocator;
1068     var state = BackendState{
1069         .allocator = allocator,
1070         .kind = .vulkan,
1071         .format = .vulkan_spirv,
1072     };
1073     const handle = state.handle();
1074     var artifact = try handle.createArtifact(.{
1075         .kernel_name = "mixed_scalars",
1076         .requested_format = .vulkan_spirv,
1077         .argument_count = 3,
1078         .scalar_argument_count = 2,
1079     });
1080     defer artifact.deinit();
1081     const loaded = try handle.loadArtifact(&artifact);
1082     const buffer = try handle.allocateBuffer(.{
1083         .byte_size = 16,
1084         .alignment = 4,
1085     });
1086     const bindings = [_]backend.BufferBinding{.{
1087         .handle = buffer,
1088         .access = .read_write,
1089         .ownership = buffer.ownership,
1090         .byte_size = buffer.byte_size,
1091     }};
1092     const scalars = [_]choir_abi.ScalarArgument{
1093         .{ .u32 = 17 },
1094         .{ .f32 = 1.25 },
1095     };
1096     const signal = try handle.createEvent(.{});
1097     try std.testing.expect(!try handle.queryEvent(.{ .event = signal }));
1098     try std.testing.expectEqual(signal.id, state.last_event_query_id.?);
1099 
1100     try handle.launch(.{
1101         .artifact = &artifact,
1102         .loaded_artifact = loaded,
1103         .buffers = bindings[0..],
1104         .scalar_arguments = scalars[0..],
1105         .geometry = .{
1106             .grid = .{ 1, 1, 1 },
1107             .threadgroup = .{ 1, 1, 1 },
1108         },
1109         .signal_event = signal,
1110     });
1111 
1112     try std.testing.expectEqual(@as(usize, 1), state.launch_count);
1113     try std.testing.expectEqual(@as(usize, 2), state.last_launch_scalar_count);
1114     try std.testing.expectEqual(@as(u32, 17), state.last_launch_scalar_u32_values[0]);
1115     try std.testing.expectEqual(@as(f32, 1.25), state.last_launch_scalar_f32_values[1]);
1116     try std.testing.expectEqual(signal.id, state.last_launch_signal_event.?);
1117     try std.testing.expect(try handle.queryEvent(.{ .event = signal }));
1118 }
1119 
1120 test "recording backend records elapsed event timing requests" {
1121     const allocator = std.testing.allocator;
1122     var state = BackendState{
1123         .allocator = allocator,
1124         .kind = .cuda,
1125         .format = .cuda_ptx,
1126         .event_elapsed_ns = 123_456,
1127     };
1128     const handle = state.handle();
1129 
1130     const start = try handle.createEvent(.{});
1131     const end = try handle.createEvent(.{});
1132 
1133     try std.testing.expectEqual(@as(u64, 123_456), try handle.elapsedEventNs(.{
1134         .start = start,
1135         .end = end,
1136     }));
1137     try std.testing.expectEqual(@as(usize, 1), state.elapsed_event_count);
1138     try std.testing.expectEqual(start.id, state.elapsed_start_events[0].?);
1139     try std.testing.expectEqual(end.id, state.elapsed_end_events[0].?);
1140 }
1141 
1142 test "recording backend reports presentation capabilities by backend kind" {
1143     const allocator = std.testing.allocator;
1144 
1145     var cuda_state = BackendState{
1146         .allocator = allocator,
1147         .kind = .cuda,
1148         .format = .cuda_ptx,
1149     };
1150     var vulkan_state = BackendState{
1151         .allocator = allocator,
1152         .kind = .vulkan,
1153         .format = .vulkan_spirv,
1154     };
1155     var metal_state = BackendState{
1156         .allocator = allocator,
1157         .kind = .metal,
1158         .format = .metal_msl,
1159     };
1160     var webgpu_state = BackendState{
1161         .allocator = allocator,
1162         .kind = .webgpu,
1163         .format = .webgpu_wgsl,
1164     };
1165 
1166     try std.testing.expect(!(try cuda_state.handle().queryCapabilities()).surfaces.supported);
1167     try std.testing.expect(!(try cuda_state.handle().queryCapabilities()).textures.supported);
1168     try std.testing.expect((try vulkan_state.handle().queryCapabilities()).surfaces.supportsPlatform(.{ .headless = .{} }));
1169     try std.testing.expect((try vulkan_state.handle().queryCapabilities()).surfaces.supportsPlatform(.{ .x11 = .{ .display = 1, .window = 2 } }));
1170     try std.testing.expect((try metal_state.handle().queryCapabilities()).surfaces.supportsPlatform(.{ .cocoa = .{ .layer = 1 } }));
1171     try std.testing.expect((try webgpu_state.handle().queryCapabilities()).surfaces.supportsPlatform(.{ .webgpu_canvas = .{ .context = 1 } }));
1172     try std.testing.expect((try vulkan_state.handle().queryCapabilities()).textures.supportsFormat(.rgba8_unorm));
1173 }
1174 
1175 test "recording backend reports raster capabilities by backend kind" {
1176     const allocator = std.testing.allocator;
1177 
1178     var cuda_state = BackendState{
1179         .allocator = allocator,
1180         .kind = .cuda,
1181         .format = .cuda_ptx,
1182     };
1183     var vulkan_state = BackendState{
1184         .allocator = allocator,
1185         .kind = .vulkan,
1186         .format = .vulkan_spirv,
1187     };
1188     var metal_state = BackendState{
1189         .allocator = allocator,
1190         .kind = .metal,
1191         .format = .metal_msl,
1192     };
1193     var webgpu_state = BackendState{
1194         .allocator = allocator,
1195         .kind = .webgpu,
1196         .format = .webgpu_wgsl,
1197     };
1198 
1199     try std.testing.expect(!(try cuda_state.handle().queryCapabilities()).raster.supported);
1200     try std.testing.expect((try vulkan_state.handle().queryCapabilities()).raster.supportsArtifactFormat(.vulkan_spirv));
1201     try std.testing.expect((try metal_state.handle().queryCapabilities()).raster.supportsArtifactFormat(.metal_msl));
1202     try std.testing.expect((try webgpu_state.handle().queryCapabilities()).raster.supportsArtifactFormat(.webgpu_wgsl));
1203     try std.testing.expect((try vulkan_state.handle().queryCapabilities()).raster.instancing);
1204     try std.testing.expect((try vulkan_state.handle().queryCapabilities()).raster.supportsTopology(.triangle_list));
1205     try std.testing.expect((try vulkan_state.handle().queryCapabilities()).raster.supportsBlendMode(.alpha_premultiplied));
1206 }
1207 
1208 test "recording backend records instanced raster draws into surface frames" {
1209     const allocator = std.testing.allocator;
1210     var state = BackendState{
1211         .allocator = allocator,
1212         .kind = .vulkan,
1213         .format = .vulkan_spirv,
1214     };
1215     const handle = state.handle();
1216 
1217     const vertex_attributes = [_]backend.RenderVertexAttribute{
1218         .{ .location = 0, .format = .float32x2, .offset = 0 },
1219         .{ .location = 1, .format = .float32x4, .offset = 8 },
1220     };
1221     const vertex_layouts = [_]backend.RenderVertexBufferLayout{
1222         .{
1223             .binding = 0,
1224             .stride = 24,
1225             .step_mode = .instance,
1226             .attribute_start = 0,
1227             .attribute_count = vertex_attributes.len,
1228         },
1229     };
1230     var artifact = try handle.createRenderArtifact(.{
1231         .format = .vulkan_spirv,
1232         .vertex_entry_name = "quad_vs",
1233         .fragment_entry_name = "quad_fs",
1234         .target_format = .rgba8_unorm,
1235         .push_extent = 0,
1236         .blend_mode = .alpha_premultiplied,
1237         .topology = .triangle_list,
1238         .vertex_layouts = vertex_layouts[0..],
1239         .vertex_attributes = vertex_attributes[0..],
1240     });
1241     defer artifact.deinit();
1242     try std.testing.expectEqual(@as(usize, 1), state.render_create_count);
1243     try std.testing.expectEqual(backend.RenderArtifactFormat.vulkan_spirv, state.last_render_format.?);
1244 
1245     const loaded = try handle.loadRenderArtifact(&artifact);
1246     try std.testing.expectEqual(@as(usize, 1), state.render_load_count);
1247     try std.testing.expectEqual(loaded.id, state.last_loaded_render_artifact_id.?);
1248 
1249     const instances = try handle.allocateBuffer(.{
1250         .byte_size = 24 * 3,
1251         .alignment = 8,
1252     });
1253     const surface = try handle.createSurface(.{
1254         .platform = .{ .headless = .{} },
1255         .extent = .{ .width = 320, .height = 180 },
1256         .format = .rgba8_unorm,
1257         .usage = .{ .present = true, .copy_dst = true, .color_attachment = true },
1258     });
1259     const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
1260     const render_done = try handle.createEvent(.{});
1261     try std.testing.expect(!try handle.queryEvent(.{ .event = render_done }));
1262 
1263     const pass = backend.RenderPass{
1264         .color = .{ .view = frame.view, .load = .{ .clear = .{} } },
1265         .viewport = .{ .width = 320, .height = 180 },
1266         .scissor = .{ .width = 320, .height = 180 },
1267         .draws = &.{.{
1268             .pipeline = loaded,
1269             .vertex_buffers = &.{.{ .buffer = instances }},
1270             .range = .{ .vertex_count = 6, .instance_count = 3 },
1271         }},
1272     };
1273     try handle.render(.{ .pass = pass, .signal_event = render_done });
1274     try std.testing.expectEqual(@as(usize, 1), state.render_count);
1275     try std.testing.expectEqual(@as(usize, 1), state.last_render_draw_count);
1276     try std.testing.expectEqual(loaded.id, state.last_render_loaded_artifact_id.?);
1277     try std.testing.expectEqual(frame.texture.id, state.last_render_target_texture_id.?);
1278     try std.testing.expectEqual(@as(usize, 1), state.last_render_vertex_buffer_count);
1279     try std.testing.expect(!state.last_render_indexed);
1280     try std.testing.expectEqual(@as(u32, 6), state.last_render_vertex_count);
1281     try std.testing.expectEqual(@as(u32, 3), state.last_render_instance_count);
1282     try std.testing.expect(try handle.queryEvent(.{ .event = render_done }));
1283 
1284     try handle.presentSurfaceFrame(.{
1285         .surface = surface,
1286         .frame = frame,
1287         .wait_events = &.{render_done},
1288     });
1289     try std.testing.expectEqual(@as(usize, 1), state.surface_present_count);
1290     try std.testing.expectEqual(@as(usize, 1), state.last_present_wait_count);
1291     try std.testing.expectEqual(render_done.id, state.last_present_wait_events[0]);
1292 
1293     const bundle = try handle.recordRenderBundle(pass);
1294     try handle.submitRenderBundle(.{ .bundle = bundle });
1295     try handle.submitRenderBundle(.{ .bundle = bundle });
1296     try std.testing.expectEqual(@as(usize, 1), state.bundle_record_count);
1297     try std.testing.expectEqual(@as(usize, 2), state.bundle_submit_count);
1298     const readable = try handle.allocateTexture(.{
1299         .extent = .{ .width = 4, .height = 4, .depth = 1 },
1300         .format = .rgba8_unorm,
1301         .usage = .{ .color_attachment = true, .copy_src = true },
1302     });
1303     var texels: [4 * 4 * 4]u8 = undefined;
1304     try std.testing.expectError(error.UnsupportedOperation, handle.readTexture(.{
1305         .texture = readable,
1306         .bytes = &texels,
1307     }));
1308 }
1309 
1310 test "recording backend records texture surface acquire and present calls" {
1311     const allocator = std.testing.allocator;
1312     var state = BackendState{
1313         .allocator = allocator,
1314         .kind = .vulkan,
1315         .format = .vulkan_spirv,
1316     };
1317     const handle = state.handle();
1318 
1319     const texture = try handle.allocateTexture(.{
1320         .extent = .{ .width = 128, .height = 64 },
1321         .format = .rgba8_unorm,
1322         .usage = .{ .sampled = true, .copy_dst = true },
1323     });
1324     try std.testing.expectEqual(@as(usize, 1), state.texture_allocate_count);
1325     try std.testing.expectEqual(texture.id, state.last_texture_id.?);
1326 
1327     const surface = try handle.createSurface(.{
1328         .platform = .{ .headless = .{} },
1329         .extent = .{ .width = 320, .height = 180 },
1330         .format = .rgba8_unorm,
1331         .usage = .{ .present = true, .copy_dst = true },
1332     });
1333     try std.testing.expectEqual(@as(usize, 1), state.surface_create_count);
1334     try std.testing.expectEqual(surface.id, state.last_surface_id.?);
1335     try std.testing.expectEqual(backend.SurfacePlatformKind.headless, state.last_surface_platform.?);
1336 
1337     const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
1338     try std.testing.expectEqual(@as(usize, 1), state.surface_acquire_count);
1339     try std.testing.expectEqual(frame.id, state.last_surface_frame_id.?);
1340     try std.testing.expectEqual(backend.TextureOwnership.acquired_surface, frame.texture.ownership);
1341 
1342     const frame_pixels = try handle.allocateBuffer(.{
1343         .byte_size = @as(usize, frame.texture.extent.width) * @as(usize, frame.texture.extent.height) * 4,
1344         .alignment = 16,
1345     });
1346     const stream = try handle.createStream(.{});
1347     const wait_a = try handle.createEvent(.{});
1348     const wait_b = try handle.createEvent(.{});
1349     const signal = try handle.createEvent(.{});
1350     try handle.recordEvent(.{ .stream = stream, .event = wait_a });
1351     try handle.recordEvent(.{ .stream = stream, .event = wait_b });
1352     try std.testing.expect(try handle.queryEvent(.{ .event = wait_a }));
1353     try std.testing.expect(try handle.queryEvent(.{ .event = wait_b }));
1354     try std.testing.expect(!try handle.queryEvent(.{ .event = signal }));
1355     const ops = [_]backend.SurfaceFrameWriteOp{
1356         .{ .clear = .{ .r = 0, .g = 0, .b = 0, .a = 1 } },
1357         .{ .copy_buffer = frame_pixels },
1358     };
1359     try handle.writeSurfaceFrame(.{
1360         .surface = surface,
1361         .frame = frame,
1362         .operations = &ops,
1363         .wait_events = &.{ wait_a, wait_b },
1364         .signal_event = signal,
1365     });
1366     try std.testing.expectEqual(@as(usize, 1), state.surface_write_count);
1367     try std.testing.expectEqual(frame.id, state.last_written_frame_id.?);
1368     try std.testing.expectEqual(@as(usize, 2), state.last_surface_write_op_count);
1369     try std.testing.expectEqual(frame_pixels.id, state.last_surface_write_copy_buffer_id.?);
1370     try std.testing.expectEqual(@as(usize, 2), state.last_surface_write_wait_count);
1371     try std.testing.expectEqual(wait_a.id, state.last_surface_write_wait_events[0]);
1372     try std.testing.expectEqual(wait_b.id, state.last_surface_write_wait_events[1]);
1373     try std.testing.expectEqual(signal.id, state.last_surface_write_signal_event.?);
1374     try std.testing.expect(try handle.queryEvent(.{ .event = signal }));
1375     try std.testing.expectEqual(@as(usize, 0), state.sync_count);
1376     try std.testing.expectError(error.SurfaceFrameExpired, handle.writeSurfaceFrame(.{
1377         .surface = surface,
1378         .frame = frame,
1379         .operations = &ops,
1380         .wait_events = &.{ wait_a, wait_b },
1381     }));
1382     try std.testing.expectEqual(@as(usize, 1), state.surface_write_count);
1383 
1384     const unready_present_wait = try handle.createEvent(.{});
1385     try std.testing.expectError(error.InvalidEvent, handle.presentSurfaceFrame(.{
1386         .surface = surface,
1387         .frame = frame,
1388         .wait_events = &.{unready_present_wait},
1389     }));
1390     try std.testing.expectEqual(@as(usize, 0), state.surface_present_count);
1391 
1392     const present_signal = try handle.createEvent(.{});
1393     try std.testing.expectError(error.UnsupportedOperation, handle.presentSurfaceFrame(.{
1394         .surface = surface,
1395         .frame = frame,
1396         .signal_event = present_signal,
1397     }));
1398     try std.testing.expectEqual(@as(usize, 0), state.surface_present_count);
1399 
1400     try handle.presentSurfaceFrame(.{
1401         .surface = surface,
1402         .frame = frame,
1403         .wait_events = &.{signal},
1404     });
1405     try std.testing.expectEqual(@as(usize, 1), state.surface_present_count);
1406     try std.testing.expectEqual(frame.id, state.last_presented_frame_id.?);
1407     try std.testing.expectEqual(@as(usize, 1), state.last_present_wait_count);
1408     try std.testing.expectEqual(signal.id, state.last_present_wait_events[0]);
1409 
1410     try handle.destroyTexture(texture);
1411     try std.testing.expectEqual(@as(usize, 1), state.texture_destroy_count);
1412     try handle.destroySurface(surface);
1413     try std.testing.expectEqual(@as(usize, 1), state.surface_destroy_count);
1414 }
1415 
1416 test "recording backend rejects invalid surface frame bracketing" {
1417     const allocator = std.testing.allocator;
1418     var state = BackendState{
1419         .allocator = allocator,
1420         .kind = .vulkan,
1421         .format = .vulkan_spirv,
1422     };
1423     const handle = state.handle();
1424 
1425     const surface = try handle.createSurface(.{
1426         .platform = .{ .headless = .{} },
1427         .extent = .{ .width = 320, .height = 180 },
1428         .format = .rgba8_unorm,
1429         .usage = .{ .present = true, .copy_dst = true },
1430     });
1431     const fake_texture = backend.TextureHandle{
1432         .id = 99,
1433         .backend = .vulkan,
1434         .extent = .{ .width = 320, .height = 180 },
1435         .format = .rgba8_unorm,
1436         .usage = .{ .present = true },
1437         .ownership = .acquired_surface,
1438     };
1439     const fake_frame = backend.SurfaceFrame{
1440         .id = 77,
1441         .backend = .vulkan,
1442         .surface = surface,
1443         .texture = fake_texture,
1444         .view = .{ .texture = fake_texture, .format = .rgba8_unorm },
1445         .generation = surface.generation,
1446     };
1447     try std.testing.expectError(error.InvalidSurfaceFrame, handle.presentSurfaceFrame(.{
1448         .surface = surface,
1449         .frame = fake_frame,
1450     }));
1451 
1452     const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
1453     try std.testing.expectError(error.SurfaceAlreadyAcquired, handle.acquireSurfaceFrame(.{ .surface = surface }));
1454     try std.testing.expectError(error.SurfaceAlreadyAcquired, handle.destroySurface(surface));
1455     try std.testing.expectError(error.SurfaceAlreadyAcquired, handle.destroyTexture(frame.texture));
1456 
1457     try handle.presentSurfaceFrame(.{
1458         .surface = surface,
1459         .frame = frame,
1460     });
1461     try std.testing.expectError(error.SurfaceFrameExpired, handle.presentSurfaceFrame(.{
1462         .surface = surface,
1463         .frame = frame,
1464     }));
1465 }
1466 
1467 test "recording backend rejects stale surface frame generations" {
1468     const allocator = std.testing.allocator;
1469     var state = BackendState{
1470         .allocator = allocator,
1471         .kind = .vulkan,
1472         .format = .vulkan_spirv,
1473     };
1474     const handle = state.handle();
1475 
1476     const surface = try handle.createSurface(.{
1477         .platform = .{ .headless = .{} },
1478         .extent = .{ .width = 320, .height = 180 },
1479         .format = .rgba8_unorm,
1480         .usage = .{ .present = true, .copy_dst = true },
1481     });
1482     const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
1483     state.surfaces[0].generation += 1;
1484     try std.testing.expectError(error.SurfaceFrameExpired, handle.presentSurfaceFrame(.{
1485         .surface = surface,
1486         .frame = frame,
1487     }));
1488 }
1489 
1490 test "recording backend replaces surfaces for resized presentation" {
1491     const allocator = std.testing.allocator;
1492     var state = BackendState{
1493         .allocator = allocator,
1494         .kind = .vulkan,
1495         .format = .vulkan_spirv,
1496     };
1497     const handle = state.handle();
1498 
1499     const old_surface = try handle.createSurface(.{
1500         .platform = .{ .headless = .{} },
1501         .extent = .{ .width = 320, .height = 180 },
1502         .format = .rgba8_unorm,
1503         .usage = .{ .present = true, .copy_dst = true },
1504     });
1505     const old_frame = try handle.acquireSurfaceFrame(.{ .surface = old_surface });
1506     try handle.presentSurfaceFrame(.{
1507         .surface = old_surface,
1508         .frame = old_frame,
1509     });
1510     try handle.destroyTexture(old_frame.texture);
1511     try handle.destroySurface(old_surface);
1512 
1513     const surface = try handle.createSurface(.{
1514         .platform = .{ .headless = .{} },
1515         .extent = .{ .width = 640, .height = 360 },
1516         .format = .rgba8_unorm,
1517         .usage = .{ .present = true, .copy_dst = true },
1518     });
1519     try std.testing.expect(surface.id != old_surface.id);
1520     try std.testing.expectEqual(@as(u32, 640), surface.extent.width);
1521     try std.testing.expectEqual(@as(u32, 360), surface.extent.height);
1522     try std.testing.expectError(error.InvalidSurfaceFrame, handle.presentSurfaceFrame(.{
1523         .surface = surface,
1524         .frame = old_frame,
1525     }));
1526     try std.testing.expectError(error.InvalidSurface, handle.acquireSurfaceFrame(.{ .surface = old_surface }));
1527 
1528     const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
1529     try std.testing.expectEqual(@as(u32, 640), frame.texture.extent.width);
1530     try std.testing.expectEqual(@as(u32, 360), frame.texture.extent.height);
1531     try handle.presentSurfaceFrame(.{
1532         .surface = surface,
1533         .frame = frame,
1534     });
1535 
1536     try std.testing.expectEqual(@as(usize, 2), state.surface_create_count);
1537     try std.testing.expectEqual(@as(usize, 2), state.surface_present_count);
1538     try std.testing.expectEqual(@as(usize, 1), state.surface_destroy_count);
1539 }