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 }