lib/gpu/src/contract.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const choir_abi = @import("choir_abi");
3
4 const Allocator = std.mem.Allocator;
5 const DType = choir_abi.DType;
6
7 pub const BackendError = error{
8 UnsupportedOperation,
9 UnsupportedArtifactFormat,
10 CapabilityMismatch,
11 CompilationFailed,
12 InvalidArtifact,
13 InvalidBuffer,
14 ReadBufferDestinationTooSmall,
15 InvalidSurface,
16 InvalidTexture,
17 InvalidSurfaceFrame,
18 InvalidRenderArtifact,
19 SurfaceAlreadyAcquired,
20 SurfaceFrameExpired,
21 InvalidStream,
22 InvalidEvent,
23 LaunchArgumentMismatch,
24 LaunchFailed,
25 RenderArgumentMismatch,
26 /// A render module's stages read push constants past the pipeline's declared bytes.
27 PushConstantRangeExceeded,
28 RenderFailed,
29 ResultMismatch,
30 RuntimeUnavailable,
31 DeviceLost,
32 MissingPayloadDeinit,
33 OutOfMemory,
34 };
35
36 pub const BackendKind = enum(u8) {
37 cuda = 0,
38 vulkan = 1,
39 metal = 2,
40 external = 3,
41 webgpu = 4,
42 cpu = 5,
43 wasm = 6,
44 };
45
46 pub const DeviceFamily = enum(u8) {
47 nvidia_cuda = 0,
48 vulkan = 1,
49 apple_metal = 2,
50 external = 3,
51 webgpu = 4,
52 native_cpu = 5,
53 webassembly = 6,
54 };
55
56 pub const ArtifactFormat = enum(u8) {
57 cuda_ptx = 0,
58 cuda_cubin = 1,
59 vulkan_spirv = 2,
60 metal_msl = 3,
61 metal_metallib = 4,
62 external = 5,
63 webgpu_wgsl = 6,
64 cpu_machine_code = 7,
65 cpu_object = 8,
66 webassembly_module = 9,
67 };
68
69 /// A caller picks the math tier, the arithmetic contract compiled code follows, before compiling
70 /// for a device. Two choices exist: `exact`, the default, and `tf32_tensor`, which uses the
71 /// reduced-precision TF32 format on NVIDIA tensor cores. The option `tf32_tensor` is accepted only
72 /// for a CUDA backend, one device runtime behind the common interface, emitting PTX on a device
73 /// with tensor cores. Compiling fails with an error when a kernel cannot be compiled under
74 /// `tf32_tensor`, and the code never falls back to another tier on its own. The chosen tier is
75 /// hashed into the identity of the compiled artifact, so code compiled under one tier remains
76 /// distinct from code compiled under the other. A tier chooses arithmetic precision alone, and it
77 /// gives no license for algebraic rewrites that change which values are NaN, infinities or the sign
78 /// of zero.
79 pub const BackendMathTier = enum(u8) {
80 exact = 0,
81 tf32_tensor = 1,
82
83 pub fn parse(text: []const u8) ?BackendMathTier {
84 inline for (
85 @typeInfo(BackendMathTier).@"enum".field_names,
86 @typeInfo(BackendMathTier).@"enum".field_values,
87 ) |field_name, field_name_value| {
88 const field = .{ .name = field_name, .value = field_name_value };
89 const value: BackendMathTier = @fromBackingInt(@intCast(field.value));
90 if (std.mem.eql(u8, text, @tagName(value))) return value;
91 }
92 return null;
93 }
94 };
95
96 pub fn familyForBackendKind(kind: BackendKind) DeviceFamily {
97 return switch (kind) {
98 .cuda => .nvidia_cuda,
99 .vulkan => .vulkan,
100 .metal => .apple_metal,
101 .external => .external,
102 .webgpu => .webgpu,
103 .cpu => .native_cpu,
104 .wasm => .webassembly,
105 };
106 }
107
108 pub fn artifactFormatIsNativeCpu(format: ArtifactFormat) bool {
109 return switch (format) {
110 .cpu_machine_code, .cpu_object => true,
111 else => false,
112 };
113 }
114
115 pub fn artifactFormatUsesHostLoopLaunch(format: ArtifactFormat) bool {
116 return switch (format) {
117 .cpu_machine_code, .cpu_object, .webassembly_module => true,
118 else => false,
119 };
120 }
121
122 pub const PayloadOwnership = enum {
123 borrowed,
124 owned,
125 };
126
127 pub const BufferOwnership = enum {
128 host,
129 backend,
130 borrowed_external,
131 };
132
133 pub const BufferAccess = enum {
134 read_only,
135 write_only,
136 read_write,
137 };
138
139 pub const TextureOwnership = enum {
140 backend,
141 acquired_surface,
142 borrowed_external,
143 };
144
145 pub const TextureFormat = enum(u8) {
146 rgba8_unorm = 0,
147 bgra8_unorm = 1,
148 rgba8_srgb = 2,
149 bgra8_srgb = 3,
150 depth32_float = 4,
151
152 /// Bytes one texel occupies in a tightly packed transfer.
153 pub fn texelBytes(self: TextureFormat) u32 {
154 return switch (self) {
155 .rgba8_unorm, .bgra8_unorm, .rgba8_srgb, .bgra8_srgb, .depth32_float => 4,
156 };
157 }
158
159 pub fn isDepth(self: TextureFormat) bool {
160 return self == .depth32_float;
161 }
162 };
163
164 pub const ColorSpace = enum(u8) {
165 srgb = 0,
166 linear = 1,
167 };
168
169 pub const PresentMode = enum(u8) {
170 fifo = 0,
171 mailbox = 1,
172 immediate = 2,
173 };
174
175 pub const SurfaceAlphaMode = enum(u8) {
176 solid = 0,
177 premultiplied = 1,
178 postmultiplied = 2,
179 inherit = 3,
180 };
181
182 pub const SurfacePlatformKind = enum(u8) {
183 x11 = 0,
184 cocoa = 1,
185 webgpu_canvas = 2,
186 headless = 3,
187 external = 4,
188 };
189
190 pub const X11Surface = struct {
191 display: usize,
192 window: u64,
193 visual_id: u32 = 0,
194 depth: u8 = 0,
195 };
196
197 pub const CocoaSurface = struct {
198 app: usize = 0,
199 window: usize = 0,
200 layer: usize,
201 };
202
203 pub const WebGpuCanvasSurface = struct {
204 context: usize,
205 canvas_id: u64 = 0,
206 };
207
208 pub const HeadlessSurface = struct {
209 token: u64 = 0,
210 };
211
212 pub const ExternalSurface = struct {
213 ptr: *anyopaque,
214 type_id: []const u8,
215 };
216
217 pub const SurfacePlatform = union(SurfacePlatformKind) {
218 x11: X11Surface,
219 cocoa: CocoaSurface,
220 webgpu_canvas: WebGpuCanvasSurface,
221 headless: HeadlessSurface,
222 external: ExternalSurface,
223
224 pub fn kind(self: SurfacePlatform) SurfacePlatformKind {
225 return switch (self) {
226 .x11 => .x11,
227 .cocoa => .cocoa,
228 .webgpu_canvas => .webgpu_canvas,
229 .headless => .headless,
230 .external => .external,
231 };
232 }
233 };
234
235 pub const SurfaceExtent = struct {
236 width: u32 = 0,
237 height: u32 = 0,
238
239 pub fn valid(self: SurfaceExtent) bool {
240 return self.width != 0 and self.height != 0;
241 }
242 };
243
244 pub const TextureExtent = struct {
245 width: u32 = 0,
246 height: u32 = 0,
247 depth: u32 = 1,
248
249 pub fn valid(self: TextureExtent) bool {
250 return self.width != 0 and self.height != 0 and self.depth != 0;
251 }
252 };
253
254 pub const TextureUsage = struct {
255 copy_src: bool = false,
256 copy_dst: bool = false,
257 sampled: bool = false,
258 storage: bool = false,
259 color_attachment: bool = false,
260 depth_attachment: bool = false,
261 present: bool = false,
262
263 pub fn any(self: TextureUsage) bool {
264 return self.copy_src or self.copy_dst or self.sampled or self.storage or
265 self.color_attachment or self.depth_attachment or self.present;
266 }
267
268 pub fn containsAll(self: TextureUsage, required: TextureUsage) bool {
269 if (required.copy_src and !self.copy_src) return false;
270 if (required.copy_dst and !self.copy_dst) return false;
271 if (required.sampled and !self.sampled) return false;
272 if (required.storage and !self.storage) return false;
273 if (required.color_attachment and !self.color_attachment) return false;
274 if (required.depth_attachment and !self.depth_attachment) return false;
275 if (required.present and !self.present) return false;
276 return true;
277 }
278 };
279
280 pub const RenderArtifactFormat = enum(u8) {
281 vulkan_spirv = 0,
282 metal_msl = 1,
283 metal_metallib = 2,
284 webgpu_wgsl = 3,
285 external = 4,
286 /// One relocatable object for the host that defines both stages as functions of the
287 /// `choir_abi.stage` layout.
288 cpu_object = 5,
289 };
290
291 /// How a fragment's color `s` combines with the target's color `d`. Each mode applies one equation
292 /// per channel, with `as` the fragment's alpha:
293 /// - `replace`: s.
294 /// - `alpha_premultiplied`: s + d·(1 − as), for color and alpha alike.
295 /// - `alpha_straight`: color s·as + d·(1 − as), alpha as + ad·(1 − as).
296 /// - `additive`: s + d, for color and alpha alike.
297 ///
298 /// An 8-bit unorm target stores each result as a byte. A color k/255 written without blending
299 /// lands on byte k on every backend, since interpolation, sampling and the conversion move it by
300 /// far less than half a step. A blend's exact result can fall between two bytes, and Vulkan only
301 /// recommends rounding to nearest when it converts, so a blended byte may differ between
302 /// backends by one step. The CPU backend rounds to nearest.
303 pub const RenderBlendMode = enum(u8) {
304 replace = 0,
305 alpha_premultiplied = 1,
306 alpha_straight = 2,
307 additive = 3,
308 };
309
310 pub const RenderPrimitiveTopology = enum(u8) {
311 triangle_list = 0,
312 triangle_strip = 1,
313 line_list = 2,
314 line_strip = 3,
315 };
316
317 pub const RenderVertexStepMode = enum(u8) {
318 vertex = 0,
319 instance = 1,
320 };
321
322 pub const RenderVertexFormat = enum(u8) {
323 float32 = 0,
324 float32x2 = 1,
325 float32x3 = 2,
326 float32x4 = 3,
327 uint32 = 4,
328 uint32x2 = 5,
329 uint32x4 = 6,
330 };
331
332 pub const RenderBindingKind = enum(u8) {
333 uniform_buffer = 0,
334 storage_buffer = 1,
335 sampled_texture = 2,
336 storage_texture = 3,
337 };
338
339 pub const RenderIndexFormat = enum(u8) {
340 none = 0,
341 u16 = 1,
342 u32 = 2,
343 };
344
345 pub const RenderVertexAttribute = struct {
346 location: u32,
347 format: RenderVertexFormat,
348 offset: u32,
349 };
350
351 pub const RenderVertexBufferLayout = struct {
352 binding: u32,
353 stride: u32,
354 step_mode: RenderVertexStepMode = .vertex,
355 attribute_start: u32,
356 attribute_count: u32,
357 };
358
359 pub const RenderBindingDesc = struct {
360 group: u32 = 0,
361 binding: u32,
362 kind: RenderBindingKind,
363 access: BufferAccess = .read_only,
364 };
365
366 pub const RenderViewport = struct {
367 x: f32 = 0,
368 y: f32 = 0,
369 width: f32,
370 height: f32,
371 min_depth: f32 = 0,
372 max_depth: f32 = 1,
373
374 pub fn valid(self: RenderViewport) bool {
375 if (!std.math.isFinite(self.x) or !std.math.isFinite(self.y)) return false;
376 if (!std.math.isFinite(self.width) or !std.math.isFinite(self.height)) return false;
377 if (!std.math.isFinite(self.min_depth) or !std.math.isFinite(self.max_depth)) return false;
378 if (self.width <= 0 or self.height <= 0) return false;
379 if (self.min_depth < 0 or self.max_depth > 1 or self.min_depth > self.max_depth) return false;
380 return true;
381 }
382 };
383
384 pub const RenderScissor = struct {
385 x: u32 = 0,
386 y: u32 = 0,
387 width: u32,
388 height: u32,
389
390 pub fn valid(self: RenderScissor) bool {
391 return self.width != 0 and self.height != 0;
392 }
393 };
394
395 pub const RenderDrawRange = struct {
396 first_vertex: u32 = 0,
397 vertex_count: u32 = 0,
398 first_index: u32 = 0,
399 index_count: u32 = 0,
400 base_vertex: i32 = 0,
401 first_instance: u32 = 0,
402 instance_count: u32 = 1,
403 index_format: RenderIndexFormat = .none,
404
405 pub fn valid(self: RenderDrawRange) bool {
406 if (self.instance_count == 0) return false;
407 const indexed = self.index_count != 0 or self.index_format != .none;
408 if (indexed) return self.index_count != 0 and self.index_format != .none and self.vertex_count == 0;
409 return self.vertex_count != 0;
410 }
411 };
412
413 /// The comparison a depth test applies as `fragment op stored`.
414 pub const RenderCompare = enum(u8) {
415 never = 0,
416 less = 1,
417 equal = 2,
418 less_equal = 3,
419 greater = 4,
420 not_equal = 5,
421 greater_equal = 6,
422 always = 7,
423 };
424
425 /// A pipeline's depth test. Its draws need a pass whose depth attachment has `format`. A fragment
426 /// that fails `compare` is discarded, and one that passes writes its depth when `write` is set.
427 pub const RenderDepthState = struct {
428 format: TextureFormat = .depth32_float,
429 compare: RenderCompare = .less,
430 write: bool = true,
431 bias: RenderDepthBias = .{},
432 };
433
434 /// An offset added to each fragment's depth before the depth test, so that a decal drawn over a
435 /// coplanar surface passes a test the surface's own depth would tie. With `m` the triangle's
436 /// depth slope in pixels and `r` the step of depth32_float at the triangle's largest vertex depth
437 /// `z`, the offset is `o = m * slope + r * constant`, where `r = 2^(e - 23)` for `z = f * 2^e`
438 /// with `f` in [1, 2). A positive `clamp` caps `o` at `clamp`, a negative one floors `o` at
439 /// `clamp`, and zero leaves `o` unclamped. A bias that moves a depth outside [0, 1] leaves what
440 /// is stored to the backend: the CPU backend stores it as computed.
441 ///
442 /// Vulkan lets `m` be `sqrt(dz/dx^2 + dz/dy^2)` or `max(|dz/dx|, |dz/dy|)`. The CPU backend takes
443 /// the maximum. The two agree when depth changes along one screen axis, and otherwise the maximum is
444 /// at most the root and at least 1/sqrt(2) of it, so a sloped bias matches another backend only
445 /// to that factor.
446 pub const RenderDepthBias = struct {
447 constant: f32 = 0,
448 slope: f32 = 0,
449 clamp: f32 = 0,
450
451 pub fn valid(self: RenderDepthBias) bool {
452 if (!std.math.isFinite(self.constant)) return false;
453 if (!std.math.isFinite(self.slope)) return false;
454 return std.math.isFinite(self.clamp);
455 }
456
457 pub fn enabled(self: RenderDepthBias) bool {
458 return self.constant != 0 or self.slope != 0;
459 }
460 };
461
462 pub const RenderFilter = enum(u8) {
463 nearest = 0,
464 linear = 1,
465 };
466
467 pub const RenderAddressMode = enum(u8) {
468 clamp_to_edge = 0,
469 repeat = 1,
470 };
471
472 /// How a sampled texture reads between and beyond its texels.
473 pub const RenderSampler = struct {
474 filter: RenderFilter = .nearest,
475 address: RenderAddressMode = .clamp_to_edge,
476 };
477
478 pub const RenderSampledTexture = struct {
479 texture: TextureHandle,
480 sampler: RenderSampler = .{},
481 };
482
483 /// One resource for one binding of a pipeline, of the kind that binding names.
484 pub const RenderResource = union(RenderBindingKind) {
485 uniform_buffer: BufferHandle,
486 storage_buffer: BufferHandle,
487 sampled_texture: RenderSampledTexture,
488 storage_texture: TextureHandle,
489 };
490
491 /// Resources for every binding of `pipeline`, in the order of `artifact.bindings`.
492 pub const RenderBindingsRequest = struct {
493 artifact: *const RenderArtifact,
494 pipeline: LoadedRenderArtifact,
495 resources: []const RenderResource,
496 };
497
498 /// A pipeline's resources, written once and bound by every draw that names them. The resources
499 /// must outlive it, and `destroyObject` releases it.
500 pub const RenderBindings = struct {
501 id: BackendObjectId,
502 backend: BackendKind,
503 pipeline: BackendObjectId,
504 };
505
506 pub const RenderColorLoad = union(enum) {
507 load,
508 clear: SurfaceClearColor,
509 };
510
511 pub const RenderDepthLoad = union(enum) {
512 load,
513 clear: f32,
514 };
515
516 pub const RenderColorAttachment = struct {
517 view: TextureView,
518 load: RenderColorLoad = .load,
519 };
520
521 pub const RenderDepthAttachment = struct {
522 view: TextureView,
523 load: RenderDepthLoad = .{ .clear = 1 },
524 };
525
526 /// A buffer bound to one vertex layout, read from `offset` bytes on.
527 pub const RenderBufferRange = struct {
528 buffer: BufferHandle,
529 offset: u64 = 0,
530 };
531
532 /// One draw of a pass. `vertex_buffers` follows the pipeline's vertex layouts in order, and an
533 /// indexed range reads `index_buffer` in the range's index format. A backend checks every range it
534 /// can see without reading buffer contents: instances, a non-indexed draw's vertices and the
535 /// indices. The vertices an index names are the caller's to keep inside each per-vertex buffer.
536 /// `push_constants` holds exactly the pipeline's `push_constant_bytes`, which both stages read for
537 /// this draw alone. A bundle copies them when it records the draw.
538 pub const RenderDraw = struct {
539 pipeline: LoadedRenderArtifact,
540 bindings: ?RenderBindings = null,
541 vertex_buffers: []const RenderBufferRange = &.{},
542 index_buffer: ?RenderBufferRange = null,
543 range: RenderDrawRange,
544 push_constants: []const u8 = &.{},
545 };
546
547 /// Draws into one color target and an optional depth target, in order. Every draw's pipeline
548 /// targets the color format, and a pipeline with a depth state needs the depth attachment. A clear
549 /// covers the whole target, whatever the scissor, and draws touch only pixels inside the scissor.
550 pub const RenderPass = struct {
551 color: RenderColorAttachment,
552 depth: ?RenderDepthAttachment = null,
553 viewport: RenderViewport,
554 scissor: RenderScissor,
555 draws: []const RenderDraw,
556 diagnostic_id: ?[]const u8 = null,
557 };
558
559 /// A pass recorded once and submitted any number of times. The objects it names must outlive it,
560 /// and their contents may change between submissions.
561 pub const RenderBundle = struct {
562 id: BackendObjectId,
563 backend: BackendKind,
564 draw_count: u32,
565 };
566
567 pub const SyncScope = enum {
568 default_stream,
569 stream,
570 event,
571 device,
572 };
573
574 pub const BackendObjectId = u64;
575
576 pub const DTypeSet = struct {
577 bits: u64 = 0,
578
579 pub fn init(values: []const DType) DTypeSet {
580 var set: DTypeSet = .{};
581 for (values) |value| set.insert(value);
582 return set;
583 }
584
585 pub fn insert(self: *DTypeSet, value: DType) void {
586 self.bits |= bitForDType(value);
587 }
588
589 pub fn contains(self: DTypeSet, value: DType) bool {
590 return (self.bits & bitForDType(value)) != 0;
591 }
592
593 pub fn containsAll(self: DTypeSet, required: DTypeSet) bool {
594 return (self.bits & required.bits) == required.bits;
595 }
596 };
597
598 pub const ArtifactFormatSet = struct {
599 bits: u64 = 0,
600
601 pub fn init(values: []const ArtifactFormat) ArtifactFormatSet {
602 var set: ArtifactFormatSet = .{};
603 for (values) |value| set.insert(value);
604 return set;
605 }
606
607 pub fn insert(self: *ArtifactFormatSet, value: ArtifactFormat) void {
608 self.bits |= bitForArtifactFormat(value);
609 }
610
611 pub fn contains(self: ArtifactFormatSet, value: ArtifactFormat) bool {
612 return (self.bits & bitForArtifactFormat(value)) != 0;
613 }
614 };
615
616 pub const TextureFormatSet = struct {
617 bits: u64 = 0,
618
619 pub fn init(values: []const TextureFormat) TextureFormatSet {
620 var set: TextureFormatSet = .{};
621 for (values) |value| set.insert(value);
622 return set;
623 }
624
625 pub fn insert(self: *TextureFormatSet, value: TextureFormat) void {
626 self.bits |= bitForTextureFormat(value);
627 }
628
629 pub fn contains(self: TextureFormatSet, value: TextureFormat) bool {
630 return (self.bits & bitForTextureFormat(value)) != 0;
631 }
632 };
633
634 pub const PresentModeSet = struct {
635 bits: u64 = 0,
636
637 pub fn init(values: []const PresentMode) PresentModeSet {
638 var set: PresentModeSet = .{};
639 for (values) |value| set.insert(value);
640 return set;
641 }
642
643 pub fn insert(self: *PresentModeSet, value: PresentMode) void {
644 self.bits |= bitForPresentMode(value);
645 }
646
647 pub fn contains(self: PresentModeSet, value: PresentMode) bool {
648 return (self.bits & bitForPresentMode(value)) != 0;
649 }
650 };
651
652 pub const ColorSpaceSet = struct {
653 bits: u64 = 0,
654
655 pub fn init(values: []const ColorSpace) ColorSpaceSet {
656 var set: ColorSpaceSet = .{};
657 for (values) |value| set.insert(value);
658 return set;
659 }
660
661 pub fn insert(self: *ColorSpaceSet, value: ColorSpace) void {
662 self.bits |= bitForColorSpace(value);
663 }
664
665 pub fn contains(self: ColorSpaceSet, value: ColorSpace) bool {
666 return (self.bits & bitForColorSpace(value)) != 0;
667 }
668 };
669
670 pub const SurfacePlatformSet = struct {
671 bits: u64 = 0,
672
673 pub fn init(values: []const SurfacePlatformKind) SurfacePlatformSet {
674 var set: SurfacePlatformSet = .{};
675 for (values) |value| set.insert(value);
676 return set;
677 }
678
679 pub fn insert(self: *SurfacePlatformSet, value: SurfacePlatformKind) void {
680 self.bits |= bitForSurfacePlatformKind(value);
681 }
682
683 pub fn contains(self: SurfacePlatformSet, value: SurfacePlatformKind) bool {
684 return (self.bits & bitForSurfacePlatformKind(value)) != 0;
685 }
686 };
687
688 pub const RenderArtifactFormatSet = struct {
689 bits: u64 = 0,
690
691 pub fn init(values: []const RenderArtifactFormat) RenderArtifactFormatSet {
692 var set: RenderArtifactFormatSet = .{};
693 for (values) |value| set.insert(value);
694 return set;
695 }
696
697 pub fn insert(self: *RenderArtifactFormatSet, value: RenderArtifactFormat) void {
698 self.bits |= bitForRenderArtifactFormat(value);
699 }
700
701 pub fn contains(self: RenderArtifactFormatSet, value: RenderArtifactFormat) bool {
702 return (self.bits & bitForRenderArtifactFormat(value)) != 0;
703 }
704 };
705
706 pub const RenderBlendModeSet = struct {
707 bits: u64 = 0,
708
709 pub fn init(values: []const RenderBlendMode) RenderBlendModeSet {
710 var set: RenderBlendModeSet = .{};
711 for (values) |value| set.insert(value);
712 return set;
713 }
714
715 pub fn insert(self: *RenderBlendModeSet, value: RenderBlendMode) void {
716 self.bits |= bitForRenderBlendMode(value);
717 }
718
719 pub fn contains(self: RenderBlendModeSet, value: RenderBlendMode) bool {
720 return (self.bits & bitForRenderBlendMode(value)) != 0;
721 }
722 };
723
724 pub const RenderPrimitiveTopologySet = struct {
725 bits: u64 = 0,
726
727 pub fn init(values: []const RenderPrimitiveTopology) RenderPrimitiveTopologySet {
728 var set: RenderPrimitiveTopologySet = .{};
729 for (values) |value| set.insert(value);
730 return set;
731 }
732
733 pub fn insert(self: *RenderPrimitiveTopologySet, value: RenderPrimitiveTopology) void {
734 self.bits |= bitForRenderPrimitiveTopology(value);
735 }
736
737 pub fn contains(self: RenderPrimitiveTopologySet, value: RenderPrimitiveTopology) bool {
738 return (self.bits & bitForRenderPrimitiveTopology(value)) != 0;
739 }
740 };
741
742 pub const RenderVertexFormatSet = struct {
743 bits: u64 = 0,
744
745 pub fn init(values: []const RenderVertexFormat) RenderVertexFormatSet {
746 var set: RenderVertexFormatSet = .{};
747 for (values) |value| set.insert(value);
748 return set;
749 }
750
751 pub fn insert(self: *RenderVertexFormatSet, value: RenderVertexFormat) void {
752 self.bits |= bitForRenderVertexFormat(value);
753 }
754
755 pub fn contains(self: RenderVertexFormatSet, value: RenderVertexFormat) bool {
756 return (self.bits & bitForRenderVertexFormat(value)) != 0;
757 }
758 };
759
760 pub const RenderBindingKindSet = struct {
761 bits: u64 = 0,
762
763 pub fn init(values: []const RenderBindingKind) RenderBindingKindSet {
764 var set: RenderBindingKindSet = .{};
765 for (values) |value| set.insert(value);
766 return set;
767 }
768
769 pub fn insert(self: *RenderBindingKindSet, value: RenderBindingKind) void {
770 self.bits |= bitForRenderBindingKind(value);
771 }
772
773 pub fn contains(self: RenderBindingKindSet, value: RenderBindingKind) bool {
774 return (self.bits & bitForRenderBindingKind(value)) != 0;
775 }
776 };
777
778 pub const RenderIndexFormatSet = struct {
779 bits: u64 = 0,
780
781 pub fn init(values: []const RenderIndexFormat) RenderIndexFormatSet {
782 var set: RenderIndexFormatSet = .{};
783 for (values) |value| set.insert(value);
784 return set;
785 }
786
787 pub fn insert(self: *RenderIndexFormatSet, value: RenderIndexFormat) void {
788 self.bits |= bitForRenderIndexFormat(value);
789 }
790
791 pub fn contains(self: RenderIndexFormatSet, value: RenderIndexFormat) bool {
792 return (self.bits & bitForRenderIndexFormat(value)) != 0;
793 }
794 };
795
796 pub const DeviceIdentity = struct {
797 backend: BackendKind,
798 family: DeviceFamily,
799 name: []const u8 = "unknown",
800 vendor_id: ?u32 = null,
801 device_id: ?u32 = null,
802 driver_version: ?[]const u8 = null,
803 };
804
805 pub const MemoryLimits = struct {
806 global_bytes: ?u64 = null,
807 max_allocation_bytes: ?u64 = null,
808 shared_memory_per_threadgroup_bytes: ?u32 = null,
809 constant_memory_bytes: ?u64 = null,
810 min_buffer_alignment: u32 = 1,
811 unified_memory: bool = false,
812 host_visible_device_memory: bool = false,
813 };
814
815 pub const SubgroupFacts = struct {
816 supported: bool = false,
817 size_min: u32 = 0,
818 size_max: u32 = 0,
819 shuffle: bool = false,
820 ballot: bool = false,
821 vote: bool = false,
822 arithmetic: bool = false,
823 scan: bool = false,
824
825 pub fn satisfies(self: SubgroupFacts, required: choir_abi.SubgroupRequirements) bool {
826 if (required.supported and !self.supported) return false;
827 if (required.size_min != 0 and (!self.supported or self.size_max < required.size_min)) return false;
828 if (required.size_max != 0 and (!self.supported or self.size_min > required.size_max)) return false;
829 if (required.shuffle and (!self.supported or !self.shuffle)) return false;
830 if (required.ballot and (!self.supported or !self.ballot)) return false;
831 if (required.vote and (!self.supported or !self.vote)) return false;
832 if (required.arithmetic and (!self.supported or !self.arithmetic)) return false;
833 if (required.scan and (!self.supported or !self.scan)) return false;
834 return true;
835 }
836 };
837
838 pub const ThreadgroupFacts = struct {
839 max_threads: u32 = 1,
840 max_blocks: [3]u32 = .{ 1, 1, 1 },
841 max_threads_per_dim: [3]u32 = .{ 1, 1, 1 },
842 max_grid_per_dim: [3]u32 = .{ 1, 1, 1 },
843 shared_memory_bytes: u32 = 0,
844 };
845
846 pub const LayoutFeatures = struct {
847 row_major: bool = true,
848 column_major: bool = false,
849 compact_strides: bool = true,
850 arbitrary_strides: bool = false,
851 broadcast_strides: bool = false,
852 tiled: bool = false,
853 vectorized: bool = false,
854 opaque_backend_layouts: bool = false,
855 };
856
857 pub const RuntimeRequirements = struct {
858 driver_loaded: bool = false,
859 device_context: bool = false,
860 streams: bool = false,
861 events: bool = false,
862 timeline_events: bool = false,
863 host_pinned_memory: bool = false,
864 external_allocator: bool = false,
865
866 pub fn containsAll(self: RuntimeRequirements, required: RuntimeRequirements) bool {
867 if (required.driver_loaded and !self.driver_loaded) return false;
868 if (required.device_context and !self.device_context) return false;
869 if (required.streams and !self.streams) return false;
870 if (required.events and !self.events) return false;
871 if (required.timeline_events and !self.timeline_events) return false;
872 if (required.host_pinned_memory and !self.host_pinned_memory) return false;
873 if (required.external_allocator and !self.external_allocator) return false;
874 return true;
875 }
876 };
877
878 pub const TextureCapabilities = struct {
879 supported: bool = false,
880 formats: TextureFormatSet = .{},
881 usages: TextureUsage = .{},
882 max_extent: TextureExtent = .{},
883 max_sample_count: u32 = 1,
884
885 pub fn supportsFormat(self: TextureCapabilities, format: TextureFormat) bool {
886 return self.formats.contains(format);
887 }
888
889 pub fn supportsUsage(self: TextureCapabilities, usage: TextureUsage) bool {
890 return self.usages.containsAll(usage);
891 }
892
893 pub fn supportsExtent(self: TextureCapabilities, extent: TextureExtent) bool {
894 if (!extent.valid()) return false;
895 if (self.max_extent.width != 0 and extent.width > self.max_extent.width) return false;
896 if (self.max_extent.height != 0 and extent.height > self.max_extent.height) return false;
897 if (self.max_extent.depth != 0 and extent.depth > self.max_extent.depth) return false;
898 return true;
899 }
900 };
901
902 pub const SurfaceCapabilities = struct {
903 supported: bool = false,
904 platforms: SurfacePlatformSet = .{},
905 formats: TextureFormatSet = .{},
906 color_spaces: ColorSpaceSet = .{},
907 present_modes: PresentModeSet = .{},
908 usages: TextureUsage = .{},
909 max_extent: SurfaceExtent = .{},
910 max_frames_in_flight: u32 = 0,
911
912 pub fn supportsPlatform(self: SurfaceCapabilities, platform: SurfacePlatform) bool {
913 return self.platforms.contains(platform.kind());
914 }
915
916 pub fn supportsFormat(self: SurfaceCapabilities, format: TextureFormat) bool {
917 return self.formats.contains(format);
918 }
919
920 pub fn supportsColorSpace(self: SurfaceCapabilities, color_space: ColorSpace) bool {
921 return self.color_spaces.contains(color_space);
922 }
923
924 pub fn supportsPresentMode(self: SurfaceCapabilities, present_mode: PresentMode) bool {
925 return self.present_modes.contains(present_mode);
926 }
927
928 pub fn supportsUsage(self: SurfaceCapabilities, usage: TextureUsage) bool {
929 return self.usages.containsAll(usage);
930 }
931
932 pub fn supportsExtent(self: SurfaceCapabilities, extent: SurfaceExtent) bool {
933 if (!extent.valid()) return false;
934 if (self.max_extent.width != 0 and extent.width > self.max_extent.width) return false;
935 if (self.max_extent.height != 0 and extent.height > self.max_extent.height) return false;
936 return true;
937 }
938 };
939
940 pub const RasterCapabilities = struct {
941 supported: bool = false,
942 artifact_formats: RenderArtifactFormatSet = .{},
943 target_formats: TextureFormatSet = .{},
944 depth_formats: TextureFormatSet = .{},
945 blend_modes: RenderBlendModeSet = .{},
946 topologies: RenderPrimitiveTopologySet = .{},
947 vertex_formats: RenderVertexFormatSet = .{},
948 binding_kinds: RenderBindingKindSet = .{},
949 index_formats: RenderIndexFormatSet = .{},
950 max_vertex_buffers: u32 = 0,
951 max_vertex_attributes: u32 = 0,
952 max_bindings: u32 = 0,
953 instancing: bool = false,
954 /// The largest push-constant block a pipeline may declare, in bytes.
955 max_push_constant_bytes: u32 = 0,
956 depth_bias: bool = false,
957 depth_bias_clamp: bool = false,
958
959 pub fn supportsArtifactFormat(self: RasterCapabilities, format: RenderArtifactFormat) bool {
960 return self.artifact_formats.contains(format);
961 }
962
963 pub fn supportsTargetFormat(self: RasterCapabilities, format: TextureFormat) bool {
964 return self.target_formats.contains(format);
965 }
966
967 pub fn supportsDepthFormat(self: RasterCapabilities, format: TextureFormat) bool {
968 return self.depth_formats.contains(format);
969 }
970
971 pub fn supportsBlendMode(self: RasterCapabilities, blend_mode: RenderBlendMode) bool {
972 return self.blend_modes.contains(blend_mode);
973 }
974
975 pub fn supportsTopology(self: RasterCapabilities, topology: RenderPrimitiveTopology) bool {
976 return self.topologies.contains(topology);
977 }
978
979 pub fn supportsVertexFormat(self: RasterCapabilities, format: RenderVertexFormat) bool {
980 return self.vertex_formats.contains(format);
981 }
982
983 pub fn supportsBindingKind(self: RasterCapabilities, kind: RenderBindingKind) bool {
984 return self.binding_kinds.contains(kind);
985 }
986
987 pub fn supportsIndexFormat(self: RasterCapabilities, format: RenderIndexFormat) bool {
988 return self.index_formats.contains(format);
989 }
990 };
991
992 pub const FloatControlWidths = struct {
993 f16: bool = false,
994 f32: bool = false,
995 f64: bool = false,
996 };
997
998 pub const FloatControlIndependence = enum(u32) {
999 bit32_only = 0,
1000 all = 1,
1001 none = 2,
1002 };
1003
1004 /// The f32 contract a compiler may claim for this device. In the flush profile,
1005 /// any subnormal operand, result, or intermediate may become zero. Stage 0 must
1006 /// refuse constant folds whose operand or result is subnormal under it. Both
1007 /// profiles preserve signed zero, infinities and NaNs.
1008 pub const FloatArithmeticProfile = enum {
1009 exact,
1010 flush_permitting,
1011 };
1012
1013 /// Properties reported by the selected Vulkan physical device.
1014 pub const FloatControlFacts = struct {
1015 denorm_preserve: FloatControlWidths = .{},
1016 signed_zero_inf_nan_preserve: FloatControlWidths = .{},
1017 denorm_behavior_independence: ?FloatControlIndependence = null,
1018
1019 /// A missing signed-zero/Inf/NaN guarantee refuses the f32 target by name.
1020 pub fn f32Profile(self: FloatControlFacts) BackendError!FloatArithmeticProfile {
1021 if (!self.signed_zero_inf_nan_preserve.f32) return error.CapabilityMismatch;
1022 return if (self.denorm_preserve.f32) .exact else .flush_permitting;
1023 }
1024 };
1025
1026 test "f32 float-control facts select exact or flush profile explicitly" {
1027 const exact = FloatControlFacts{
1028 .denorm_preserve = .{ .f32 = true },
1029 .signed_zero_inf_nan_preserve = .{ .f32 = true },
1030 };
1031 try std.testing.expectEqual(FloatArithmeticProfile.exact, try exact.f32Profile());
1032 const flush = FloatControlFacts{ .signed_zero_inf_nan_preserve = .{ .f32 = true } };
1033 try std.testing.expectEqual(FloatArithmeticProfile.flush_permitting, try flush.f32Profile());
1034 try std.testing.expectError(error.CapabilityMismatch, (FloatControlFacts{}).f32Profile());
1035 }
1036
1037 pub const BackendCapabilities = struct {
1038 identity: DeviceIdentity,
1039 memory: MemoryLimits = .{},
1040 subgroup: SubgroupFacts = .{},
1041 float_controls: FloatControlFacts = .{},
1042 threadgroup: ThreadgroupFacts = .{},
1043 dtypes: DTypeSet = .{},
1044 layouts: LayoutFeatures = .{},
1045 runtime: RuntimeRequirements = .{},
1046 features: choir_abi.Features = .{},
1047 artifact_formats: ArtifactFormatSet = .{},
1048 textures: TextureCapabilities = .{},
1049 surfaces: SurfaceCapabilities = .{},
1050 raster: RasterCapabilities = .{},
1051
1052 pub fn supportsDType(self: BackendCapabilities, value: DType) bool {
1053 return self.dtypes.contains(value);
1054 }
1055
1056 pub fn supportsArtifactFormat(self: BackendCapabilities, format: ArtifactFormat) bool {
1057 return self.artifact_formats.contains(format);
1058 }
1059
1060 pub fn supportsTextureFormat(self: BackendCapabilities, format: TextureFormat) bool {
1061 return self.textures.supportsFormat(format);
1062 }
1063
1064 pub fn supportsSurfaceFormat(self: BackendCapabilities, format: TextureFormat) bool {
1065 return self.surfaces.supportsFormat(format);
1066 }
1067
1068 pub fn supportsRenderArtifactFormat(self: BackendCapabilities, format: RenderArtifactFormat) bool {
1069 return self.raster.supportsArtifactFormat(format);
1070 }
1071
1072 pub fn supportsFeatures(self: BackendCapabilities, required: choir_abi.Features) bool {
1073 return self.features.containsAll(required);
1074 }
1075
1076 pub fn supportsSubgroup(self: BackendCapabilities, required: choir_abi.SubgroupRequirements) bool {
1077 return self.subgroup.satisfies(required);
1078 }
1079
1080 pub fn supportsRuntime(self: BackendCapabilities, required: RuntimeRequirements) bool {
1081 return self.runtime.containsAll(required);
1082 }
1083
1084 pub fn validateCompileRequest(self: BackendCapabilities, request: CompileRequest) BackendError!void {
1085 if (!self.supportsArtifactFormat(request.requested_format)) return error.UnsupportedArtifactFormat;
1086 if (!self.dtypes.containsAll(request.required_dtypes)) return error.CapabilityMismatch;
1087 if (!self.supportsFeatures(request.required_features)) return error.CapabilityMismatch;
1088 if (!self.supportsSubgroup(request.required_subgroup)) return error.CapabilityMismatch;
1089 }
1090
1091 pub fn validateRuntimeRequirements(self: BackendCapabilities, required: RuntimeRequirements) BackendError!void {
1092 if (!self.supportsRuntime(required)) return error.CapabilityMismatch;
1093 }
1094
1095 pub fn validateBufferAllocation(self: BackendCapabilities, request: BufferAllocation) BackendError!void {
1096 if (request.byte_size == 0) return error.InvalidBuffer;
1097 if (request.alignment == 0) return error.InvalidBuffer;
1098 if ((request.alignment & (request.alignment - 1)) != 0) return error.InvalidBuffer;
1099 if (request.alignment < self.memory.min_buffer_alignment) return error.CapabilityMismatch;
1100
1101 const byte_size = std.math.cast(u64, request.byte_size) orelse return error.CapabilityMismatch;
1102 if (self.memory.max_allocation_bytes) |limit| {
1103 if (byte_size > limit) return error.CapabilityMismatch;
1104 }
1105 if (self.memory.global_bytes) |limit| {
1106 if (byte_size > limit) return error.CapabilityMismatch;
1107 }
1108 if (request.dtype) |dtype| {
1109 if (!self.supportsDType(dtype)) return error.CapabilityMismatch;
1110 if (request.element_count) |count| {
1111 const required_bytes = std.math.mul(u64, count, dtype.sizeOf()) catch return error.CapabilityMismatch;
1112 if (required_bytes > byte_size) return error.InvalidBuffer;
1113 }
1114 }
1115 }
1116
1117 pub fn validateTextureAllocation(self: BackendCapabilities, request: TextureAllocation) BackendError!void {
1118 if (!request.extent.valid()) return error.InvalidTexture;
1119 if (!request.usage.any()) return error.InvalidTexture;
1120 if (request.sample_count == 0) return error.InvalidTexture;
1121 if (!self.textures.supported) return error.CapabilityMismatch;
1122 if (!self.textures.supportsFormat(request.format)) return error.CapabilityMismatch;
1123 if (!self.textures.supportsUsage(request.usage)) return error.CapabilityMismatch;
1124 if (!self.textures.supportsExtent(request.extent)) return error.CapabilityMismatch;
1125 if (request.sample_count > self.textures.max_sample_count) return error.CapabilityMismatch;
1126 }
1127
1128 pub fn validateSurfaceCreation(self: BackendCapabilities, request: SurfaceCreationRequest) BackendError!void {
1129 if (!request.extent.valid()) return error.InvalidSurface;
1130 if (!request.usage.any() or !request.usage.present) return error.InvalidSurface;
1131 if (request.max_frames_in_flight == 0) return error.InvalidSurface;
1132 if (!self.surfaces.supported) return error.CapabilityMismatch;
1133 if (!self.surfaces.supportsPlatform(request.platform)) return error.CapabilityMismatch;
1134 if (!self.surfaces.supportsFormat(request.format)) return error.CapabilityMismatch;
1135 if (!self.surfaces.supportsColorSpace(request.color_space)) return error.CapabilityMismatch;
1136 if (!self.surfaces.supportsPresentMode(request.present_mode)) return error.CapabilityMismatch;
1137 if (!self.surfaces.supportsUsage(request.usage)) return error.CapabilityMismatch;
1138 if (!self.surfaces.supportsExtent(request.extent)) return error.CapabilityMismatch;
1139 if (request.max_frames_in_flight > self.surfaces.max_frames_in_flight) return error.CapabilityMismatch;
1140 }
1141
1142 pub fn validateRenderPipelineDesc(self: BackendCapabilities, desc: RenderPipelineDesc) BackendError!void {
1143 if (desc.vertex_entry_name.len == 0 or desc.fragment_entry_name.len == 0) return error.InvalidRenderArtifact;
1144 if (!self.raster.supported) return error.CapabilityMismatch;
1145 if (!self.raster.supportsArtifactFormat(desc.format)) return error.CapabilityMismatch;
1146 if (desc.target_format.isDepth()) return error.InvalidRenderArtifact;
1147 if (!self.raster.supportsTargetFormat(desc.target_format)) return error.CapabilityMismatch;
1148 if (desc.depth) |depth| {
1149 if (!depth.format.isDepth()) return error.InvalidRenderArtifact;
1150 if (!self.raster.supportsDepthFormat(depth.format)) return error.CapabilityMismatch;
1151 if (!depth.bias.valid()) return error.InvalidRenderArtifact;
1152 if (depth.bias.enabled() and !self.raster.depth_bias) return error.CapabilityMismatch;
1153 if (depth.bias.clamp != 0 and !self.raster.depth_bias_clamp) return error.CapabilityMismatch;
1154 }
1155 if (desc.push_constant_bytes % 4 != 0) return error.InvalidRenderArtifact;
1156 if (desc.push_constant_bytes > self.raster.max_push_constant_bytes) return error.CapabilityMismatch;
1157 if (desc.push_extent > desc.push_constant_bytes) return error.PushConstantRangeExceeded;
1158 if (!self.raster.supportsBlendMode(desc.blend_mode)) return error.CapabilityMismatch;
1159 if (!self.raster.supportsTopology(desc.topology)) return error.CapabilityMismatch;
1160 if (desc.vertex_layouts.len > self.raster.max_vertex_buffers) return error.CapabilityMismatch;
1161 if (desc.vertex_attributes.len > self.raster.max_vertex_attributes) return error.CapabilityMismatch;
1162 if (desc.bindings.len > self.raster.max_bindings) return error.CapabilityMismatch;
1163
1164 for (desc.vertex_layouts) |layout| {
1165 if (layout.stride == 0) return error.InvalidRenderArtifact;
1166 if (layout.step_mode == .instance and !self.raster.instancing) return error.CapabilityMismatch;
1167 const start: usize = @intCast(layout.attribute_start);
1168 const count: usize = @intCast(layout.attribute_count);
1169 if (count == 0) return error.InvalidRenderArtifact;
1170 if (start > desc.vertex_attributes.len or count > desc.vertex_attributes.len - start) return error.InvalidRenderArtifact;
1171 for (desc.vertex_attributes[start..][0..count]) |attribute| {
1172 if (!self.raster.supportsVertexFormat(attribute.format)) return error.CapabilityMismatch;
1173 const size = renderVertexFormatByteSize(attribute.format);
1174 if (attribute.offset > layout.stride or size > layout.stride - attribute.offset) return error.InvalidRenderArtifact;
1175 }
1176 }
1177
1178 for (desc.bindings) |binding| {
1179 if (!self.raster.supportsBindingKind(binding.kind)) return error.CapabilityMismatch;
1180 }
1181 }
1182
1183 /// Checks a pass against these capabilities and against the facts each draw's pipeline
1184 /// reported. A draw's pipeline declares a depth state exactly when the pass has a depth
1185 /// attachment, because a pipeline is built for the attachments it draws into.
1186 pub fn validateRenderPass(self: BackendCapabilities, pass: RenderPass) BackendError!void {
1187 if (!self.raster.supported) return error.CapabilityMismatch;
1188 if (!pass.viewport.valid()) return error.RenderArgumentMismatch;
1189 if (!pass.scissor.valid()) return error.RenderArgumentMismatch;
1190 const color = pass.color.view;
1191 if (color.texture.format != color.format) return error.InvalidTexture;
1192 if (!color.texture.usage.color_attachment) return error.InvalidTexture;
1193 if (!self.raster.supportsTargetFormat(color.format)) return error.CapabilityMismatch;
1194 switch (pass.color.load) {
1195 .load => {},
1196 .clear => |clear| if (!clear.valid()) return error.RenderArgumentMismatch,
1197 }
1198 const extent = color.texture.extent;
1199 if (!scissorWithin(pass.scissor, extent)) return error.RenderArgumentMismatch;
1200 if (pass.depth) |depth| {
1201 if (depth.view.texture.format != depth.view.format) return error.InvalidTexture;
1202 if (!depth.view.texture.usage.depth_attachment) return error.InvalidTexture;
1203 if (!self.raster.supportsDepthFormat(depth.view.format)) return error.CapabilityMismatch;
1204 if (!sameTextureExtent(depth.view.texture.extent, extent)) return error.RenderArgumentMismatch;
1205 switch (depth.load) {
1206 .load => {},
1207 .clear => |clear| if (!(clear >= 0 and clear <= 1)) return error.RenderArgumentMismatch,
1208 }
1209 }
1210 for (pass.draws) |draw| try self.validateRenderDraw(pass, draw);
1211 }
1212
1213 fn validateRenderDraw(self: BackendCapabilities, pass: RenderPass, draw: RenderDraw) BackendError!void {
1214 const pipeline = draw.pipeline;
1215 if (!draw.range.valid()) return error.RenderArgumentMismatch;
1216 if (pipeline.target_format != pass.color.view.format) return error.RenderArgumentMismatch;
1217 if ((pipeline.depth == null) != (pass.depth == null)) return error.RenderArgumentMismatch;
1218 if (pipeline.depth) |depth| {
1219 if (depth.format != pass.depth.?.view.format) return error.RenderArgumentMismatch;
1220 }
1221 if (draw.vertex_buffers.len != pipeline.vertex_buffer_count) return error.RenderArgumentMismatch;
1222 if ((pipeline.binding_count == 0) != (draw.bindings == null)) return error.RenderArgumentMismatch;
1223 if (draw.bindings) |bindings| {
1224 if (bindings.pipeline != pipeline.id) return error.RenderArgumentMismatch;
1225 }
1226 const range = draw.range;
1227 if (range.instance_count > 1 and !self.raster.instancing) return error.CapabilityMismatch;
1228 if (range.index_format != .none and !self.raster.supportsIndexFormat(range.index_format)) {
1229 return error.CapabilityMismatch;
1230 }
1231 if ((range.index_count != 0) != (draw.index_buffer != null)) return error.RenderArgumentMismatch;
1232 if (draw.push_constants.len != pipeline.push_constant_bytes) return error.RenderArgumentMismatch;
1233 }
1234
1235 /// Checks that `request` names one resource of the right kind for each pipeline binding.
1236 pub fn validateRenderBindings(self: BackendCapabilities, request: RenderBindingsRequest) BackendError!void {
1237 if (!self.raster.supported) return error.CapabilityMismatch;
1238 const bindings = request.artifact.bindings;
1239 if (request.pipeline.binding_count != bindings.len) return error.RenderArgumentMismatch;
1240 if (request.resources.len != bindings.len) return error.RenderArgumentMismatch;
1241 for (bindings, request.resources) |binding, resource| {
1242 if (std.meta.activeTag(resource) != binding.kind) return error.RenderArgumentMismatch;
1243 if (!self.raster.supportsBindingKind(binding.kind)) return error.CapabilityMismatch;
1244 switch (resource) {
1245 .uniform_buffer, .storage_buffer => {},
1246 .sampled_texture => |sampled| if (!sampled.texture.usage.sampled) return error.InvalidTexture,
1247 .storage_texture => |texture| if (!texture.usage.storage) return error.InvalidTexture,
1248 }
1249 }
1250 }
1251
1252 /// Checks that `byte_count` covers every texel of `texture` exactly once.
1253 pub fn validateTextureTransfer(texture: TextureHandle, byte_count: usize, usage: TextureUsage) BackendError!void {
1254 if (!texture.usage.containsAll(usage)) return error.InvalidTexture;
1255 if (byte_count != try textureByteSize(texture)) return error.InvalidTexture;
1256 }
1257
1258 pub fn validateLaunchRuntime(self: BackendCapabilities, request: LaunchRequest) BackendError!void {
1259 if (request.stream != null) try self.validateRuntimeRequirements(.{ .streams = true });
1260 if (request.wait_events.len != 0 or request.signal_event != null) try self.validateRuntimeRequirements(.{ .events = true });
1261 }
1262
1263 pub fn validateSubmitRuntime(
1264 self: BackendCapabilities,
1265 stream: ?StreamHandle,
1266 wait_events: []const EventHandle,
1267 signal_event: ?EventHandle,
1268 ) BackendError!void {
1269 if (stream != null) try self.validateRuntimeRequirements(.{ .streams = true });
1270 if (wait_events.len != 0 or signal_event != null) try self.validateRuntimeRequirements(.{ .events = true });
1271 }
1272
1273 pub fn validateSyncRuntime(self: BackendCapabilities, request: SyncRequest) BackendError!void {
1274 switch (request.scope) {
1275 .default_stream, .device => {},
1276 .stream => try self.validateRuntimeRequirements(.{ .streams = true }),
1277 .event => try self.validateRuntimeRequirements(.{ .events = true }),
1278 }
1279 }
1280
1281 pub fn validateLaunchGeometry(self: BackendCapabilities, geometry: choir_abi.LaunchGeometry) BackendError!void {
1282 if (geometry.grid[0] == 0 or geometry.grid[1] == 0 or geometry.grid[2] == 0) {
1283 return error.LaunchArgumentMismatch;
1284 }
1285 if (geometry.threadgroup[0] == 0 or geometry.threadgroup[1] == 0 or geometry.threadgroup[2] == 0) {
1286 return error.LaunchArgumentMismatch;
1287 }
1288
1289 const total_threads =
1290 @as(u64, geometry.threadgroup[0]) *
1291 @as(u64, geometry.threadgroup[1]) *
1292 @as(u64, geometry.threadgroup[2]);
1293 if (total_threads > self.threadgroup.max_threads) return error.CapabilityMismatch;
1294
1295 for (geometry.threadgroup, self.threadgroup.max_threads_per_dim) |requested, limit| {
1296 if (requested > limit) return error.CapabilityMismatch;
1297 }
1298 for (geometry.grid, self.threadgroup.max_blocks) |requested, limit| {
1299 if (requested > limit) return error.CapabilityMismatch;
1300 }
1301 for (geometry.grid, self.threadgroup.max_grid_per_dim) |requested, limit| {
1302 if (requested > limit) return error.CapabilityMismatch;
1303 }
1304 if (geometry.dynamic_shared_memory_bytes != 0 and !self.features.dynamic_shared_memory) {
1305 return error.CapabilityMismatch;
1306 }
1307 if (geometry.dynamic_shared_memory_bytes > self.threadgroup.shared_memory_bytes) {
1308 return error.CapabilityMismatch;
1309 }
1310 }
1311 };
1312
1313 pub const ArtifactPayload = union(enum) {
1314 none,
1315 bytes: []const u8,
1316 words_u32: []const u32,
1317 text: []const u8,
1318 external: ExternalPayload,
1319 };
1320
1321 pub const ExternalPayload = struct {
1322 ptr: *anyopaque,
1323 type_id: []const u8,
1324 deinit_fn: ?*const fn (Allocator, *anyopaque) void = null,
1325 };
1326
1327 pub const KernelArtifactDesc = struct {
1328 backend: BackendKind,
1329 format: ArtifactFormat,
1330 entry_name: []const u8,
1331 argument_count: u32,
1332 scalar_argument_count: u32 = 0,
1333 diagnostic_id: ?[]const u8 = null,
1334 interface: choir_abi.Interface = .{},
1335 };
1336
1337 pub const KernelArtifact = struct {
1338 allocator: Allocator,
1339 backend: BackendKind,
1340 format: ArtifactFormat,
1341 entry_name: []const u8,
1342 argument_count: u32,
1343 scalar_argument_count: u32 = 0,
1344 diagnostic_id: ?[]const u8 = null,
1345 /// Requirements and push-constant layout the compiler recorded for this
1346 /// entry. Load checks the requirements; launch packs through the layout.
1347 interface: choir_abi.Interface = .{},
1348 payload: ArtifactPayload = .none,
1349 payload_ownership: PayloadOwnership = .borrowed,
1350
1351 pub fn init(allocator: Allocator, desc: KernelArtifactDesc) Allocator.Error!KernelArtifact {
1352 const entry_name = try dupeString(allocator, desc.entry_name);
1353 errdefer freeString(allocator, entry_name);
1354
1355 return .{
1356 .allocator = allocator,
1357 .backend = desc.backend,
1358 .format = desc.format,
1359 .entry_name = entry_name,
1360 .argument_count = desc.argument_count,
1361 .scalar_argument_count = desc.scalar_argument_count,
1362 .diagnostic_id = try dupeOpt(allocator, desc.diagnostic_id),
1363 .interface = desc.interface,
1364 };
1365 }
1366
1367 pub fn bufferArgumentCount(self: *const KernelArtifact) BackendError!u32 {
1368 if (self.scalar_argument_count > self.argument_count) return error.InvalidArtifact;
1369 return self.argument_count - self.scalar_argument_count;
1370 }
1371
1372 pub fn deinit(self: *KernelArtifact) void {
1373 self.clearPayload();
1374 freeString(self.allocator, self.entry_name);
1375 freeOpt(self.allocator, self.diagnostic_id);
1376 self.* = undefined;
1377 }
1378
1379 pub fn setBorrowedBytes(self: *KernelArtifact, bytes: []const u8) void {
1380 self.clearPayload();
1381 self.payload = .{ .bytes = bytes };
1382 self.payload_ownership = .borrowed;
1383 }
1384
1385 pub fn setOwnedBytes(self: *KernelArtifact, bytes: []const u8) BackendError!void {
1386 const owned = self.allocator.dupe(u8, bytes) catch return error.OutOfMemory;
1387 self.clearPayload();
1388 self.payload = .{ .bytes = owned };
1389 self.payload_ownership = .owned;
1390 }
1391
1392 pub fn setBorrowedWords(self: *KernelArtifact, words: []const u32) void {
1393 self.clearPayload();
1394 self.payload = .{ .words_u32 = words };
1395 self.payload_ownership = .borrowed;
1396 }
1397
1398 pub fn setOwnedWords(self: *KernelArtifact, words: []const u32) BackendError!void {
1399 const owned = self.allocator.dupe(u32, words) catch return error.OutOfMemory;
1400 self.clearPayload();
1401 self.payload = .{ .words_u32 = owned };
1402 self.payload_ownership = .owned;
1403 }
1404
1405 pub fn setBorrowedText(self: *KernelArtifact, text: []const u8) void {
1406 self.clearPayload();
1407 self.payload = .{ .text = text };
1408 self.payload_ownership = .borrowed;
1409 }
1410
1411 pub fn setOwnedText(self: *KernelArtifact, text: []const u8) BackendError!void {
1412 const owned = self.allocator.dupe(u8, text) catch return error.OutOfMemory;
1413 self.clearPayload();
1414 self.payload = .{ .text = owned };
1415 self.payload_ownership = .owned;
1416 }
1417
1418 pub fn setExternalPayload(self: *KernelArtifact, payload: ExternalPayload, ownership: PayloadOwnership) BackendError!void {
1419 if (ownership == .owned and payload.deinit_fn == null) return error.MissingPayloadDeinit;
1420 self.clearPayload();
1421 self.payload = .{ .external = payload };
1422 self.payload_ownership = ownership;
1423 }
1424
1425 fn clearPayload(self: *KernelArtifact) void {
1426 if (self.payload_ownership == .owned) {
1427 switch (self.payload) {
1428 .bytes => |bytes| freeString(self.allocator, bytes),
1429 .words_u32 => |words| self.allocator.free(@constCast(words)),
1430 .text => |text| freeString(self.allocator, text),
1431 .external => |payload| if (payload.deinit_fn) |deinit_fn| deinit_fn(self.allocator, payload.ptr),
1432 .none => {},
1433 }
1434 }
1435 self.payload = .none;
1436 self.payload_ownership = .borrowed;
1437 }
1438 };
1439
1440 /// A caller holds this value for compiled code loaded on a device and uses it to launch that code.
1441 /// The value names compiled code that the backend owns, by id, backend kind and code format. The
1442 /// caller releases the id through the backend handle that loaded it, only after all queued work
1443 /// that can refer to it has finished. That handle is the interface value through which a caller
1444 /// creates buffers, loads compiled code and launches kernels.
1445 pub const LoadedArtifact = struct {
1446 id: BackendObjectId,
1447 backend: BackendKind,
1448 format: ArtifactFormat,
1449 };
1450
1451 /// A graphics pipeline. Clip space puts x = −1 at the target's left edge, y = −1 at its top edge
1452 /// and depth z in [0, 1], so y grows downward in pixels. Triangles are drawn whatever their
1453 /// winding. A pixel is covered when its center lies inside a triangle, with centers on a shared
1454 /// edge going to exactly one triangle by the top-left rule.
1455 pub const RenderPipelineDesc = struct {
1456 format: RenderArtifactFormat,
1457 vertex_entry_name: []const u8,
1458 fragment_entry_name: []const u8,
1459 target_format: TextureFormat,
1460 blend_mode: RenderBlendMode = .replace,
1461 topology: RenderPrimitiveTopology = .triangle_list,
1462 depth: ?RenderDepthState = null,
1463 vertex_layouts: []const RenderVertexBufferLayout = &.{},
1464 vertex_attributes: []const RenderVertexAttribute = &.{},
1465 bindings: []const RenderBindingDesc = &.{},
1466 /// Bytes of the push-constant block both stages read, a multiple of 4. Each draw supplies
1467 /// exactly this many, and a stage must read inside them.
1468 push_constant_bytes: u32 = 0,
1469 /// Bytes of the push-constant block the payload's stages read, as the emitter reported them
1470 /// with the payload, the way `CompileRequest.push_constants` carries a kernel's layout. It has
1471 /// no default, so a caller states it, and it may not exceed `push_constant_bytes`.
1472 push_extent: u32,
1473 diagnostic_id: ?[]const u8 = null,
1474 payload: CompilePayload = .none,
1475 };
1476
1477 pub const RenderArtifactDesc = struct {
1478 backend: BackendKind,
1479 pipeline: RenderPipelineDesc,
1480 };
1481
1482 pub const RenderArtifact = struct {
1483 allocator: Allocator,
1484 backend: BackendKind,
1485 format: RenderArtifactFormat,
1486 vertex_entry_name: []const u8,
1487 fragment_entry_name: []const u8,
1488 target_format: TextureFormat,
1489 blend_mode: RenderBlendMode,
1490 topology: RenderPrimitiveTopology,
1491 depth: ?RenderDepthState,
1492 vertex_layouts: []const RenderVertexBufferLayout,
1493 vertex_attributes: []const RenderVertexAttribute,
1494 bindings: []const RenderBindingDesc,
1495 push_constant_bytes: u32 = 0,
1496 push_extent: u32 = 0,
1497 diagnostic_id: ?[]const u8 = null,
1498 payload: ArtifactPayload = .none,
1499 payload_ownership: PayloadOwnership = .borrowed,
1500
1501 pub fn init(allocator: Allocator, desc: RenderArtifactDesc) Allocator.Error!RenderArtifact {
1502 const vertex_entry_name = try dupeString(allocator, desc.pipeline.vertex_entry_name);
1503 errdefer freeString(allocator, vertex_entry_name);
1504 const fragment_entry_name = try dupeString(allocator, desc.pipeline.fragment_entry_name);
1505 errdefer freeString(allocator, fragment_entry_name);
1506 const vertex_layouts = try allocator.dupe(RenderVertexBufferLayout, desc.pipeline.vertex_layouts);
1507 errdefer allocator.free(vertex_layouts);
1508 const vertex_attributes = try allocator.dupe(RenderVertexAttribute, desc.pipeline.vertex_attributes);
1509 errdefer allocator.free(vertex_attributes);
1510 const bindings = try allocator.dupe(RenderBindingDesc, desc.pipeline.bindings);
1511 errdefer allocator.free(bindings);
1512
1513 return .{
1514 .allocator = allocator,
1515 .backend = desc.backend,
1516 .format = desc.pipeline.format,
1517 .vertex_entry_name = vertex_entry_name,
1518 .fragment_entry_name = fragment_entry_name,
1519 .target_format = desc.pipeline.target_format,
1520 .blend_mode = desc.pipeline.blend_mode,
1521 .topology = desc.pipeline.topology,
1522 .depth = desc.pipeline.depth,
1523 .vertex_layouts = vertex_layouts,
1524 .vertex_attributes = vertex_attributes,
1525 .bindings = bindings,
1526 .push_constant_bytes = desc.pipeline.push_constant_bytes,
1527 .push_extent = desc.pipeline.push_extent,
1528 .diagnostic_id = try dupeOpt(allocator, desc.pipeline.diagnostic_id),
1529 };
1530 }
1531
1532 pub fn deinit(self: *RenderArtifact) void {
1533 self.clearPayload();
1534 freeString(self.allocator, self.vertex_entry_name);
1535 freeString(self.allocator, self.fragment_entry_name);
1536 self.allocator.free(@constCast(self.vertex_layouts));
1537 self.allocator.free(@constCast(self.vertex_attributes));
1538 self.allocator.free(@constCast(self.bindings));
1539 freeOpt(self.allocator, self.diagnostic_id);
1540 self.* = undefined;
1541 }
1542
1543 pub fn setBorrowedBytes(self: *RenderArtifact, bytes: []const u8) void {
1544 self.clearPayload();
1545 self.payload = .{ .bytes = bytes };
1546 self.payload_ownership = .borrowed;
1547 }
1548
1549 pub fn setOwnedBytes(self: *RenderArtifact, bytes: []const u8) BackendError!void {
1550 const owned = self.allocator.dupe(u8, bytes) catch return error.OutOfMemory;
1551 self.clearPayload();
1552 self.payload = .{ .bytes = owned };
1553 self.payload_ownership = .owned;
1554 }
1555
1556 pub fn setBorrowedWords(self: *RenderArtifact, words: []const u32) void {
1557 self.clearPayload();
1558 self.payload = .{ .words_u32 = words };
1559 self.payload_ownership = .borrowed;
1560 }
1561
1562 pub fn setOwnedWords(self: *RenderArtifact, words: []const u32) BackendError!void {
1563 const owned = self.allocator.dupe(u32, words) catch return error.OutOfMemory;
1564 self.clearPayload();
1565 self.payload = .{ .words_u32 = owned };
1566 self.payload_ownership = .owned;
1567 }
1568
1569 pub fn setBorrowedText(self: *RenderArtifact, text: []const u8) void {
1570 self.clearPayload();
1571 self.payload = .{ .text = text };
1572 self.payload_ownership = .borrowed;
1573 }
1574
1575 pub fn setOwnedText(self: *RenderArtifact, text: []const u8) BackendError!void {
1576 const owned = self.allocator.dupe(u8, text) catch return error.OutOfMemory;
1577 self.clearPayload();
1578 self.payload = .{ .text = owned };
1579 self.payload_ownership = .owned;
1580 }
1581
1582 pub fn setExternalPayload(self: *RenderArtifact, payload: ExternalPayload, ownership: PayloadOwnership) BackendError!void {
1583 if (ownership == .owned and payload.deinit_fn == null) return error.MissingPayloadDeinit;
1584 self.clearPayload();
1585 self.payload = .{ .external = payload };
1586 self.payload_ownership = ownership;
1587 }
1588
1589 fn clearPayload(self: *RenderArtifact) void {
1590 if (self.payload_ownership == .owned) {
1591 switch (self.payload) {
1592 .bytes => |bytes| freeString(self.allocator, bytes),
1593 .words_u32 => |words| self.allocator.free(@constCast(words)),
1594 .text => |text| freeString(self.allocator, text),
1595 .external => |payload| if (payload.deinit_fn) |deinit_fn| deinit_fn(self.allocator, payload.ptr),
1596 .none => {},
1597 }
1598 }
1599 self.payload = .none;
1600 self.payload_ownership = .borrowed;
1601 }
1602 };
1603
1604 /// A pipeline ready to draw, with the facts a pass checks its draws against.
1605 pub const LoadedRenderArtifact = struct {
1606 id: BackendObjectId,
1607 backend: BackendKind,
1608 format: RenderArtifactFormat,
1609 target_format: TextureFormat,
1610 depth: ?RenderDepthState = null,
1611 vertex_buffer_count: u32 = 0,
1612 binding_count: u32 = 0,
1613 push_constant_bytes: u32 = 0,
1614
1615 /// The facts a backend must report for a pipeline it loaded from `artifact`.
1616 pub fn describing(artifact: *const RenderArtifact, id: BackendObjectId) LoadedRenderArtifact {
1617 return .{
1618 .id = id,
1619 .backend = artifact.backend,
1620 .format = artifact.format,
1621 .target_format = artifact.target_format,
1622 .depth = artifact.depth,
1623 .vertex_buffer_count = @intCast(artifact.vertex_layouts.len),
1624 .binding_count = @intCast(artifact.bindings.len),
1625 .push_constant_bytes = artifact.push_constant_bytes,
1626 };
1627 }
1628 };
1629
1630 /// A caller holds this value to name one device buffer in later transfers and launches. The value
1631 /// names one buffer by id, with its backend, its size in bytes and who owns its memory. The backend
1632 /// handle that created the buffer owns the id until the caller passes it to `destroyObject`.
1633 pub const BufferHandle = struct {
1634 id: BackendObjectId,
1635 backend: BackendKind,
1636 byte_size: usize,
1637 ownership: BufferOwnership,
1638 };
1639
1640 /// A caller holds this value to order work on one device queue. The value names one ordered queue
1641 /// of device work by id and backend. The backend handle that created the queue owns the id until
1642 /// the caller passes it to `destroyObject`.
1643 pub const StreamHandle = struct {
1644 id: BackendObjectId,
1645 backend: BackendKind,
1646 };
1647
1648 /// A caller holds this value to mark a point in queued device work and wait for it. The value names
1649 /// one device event by id and backend. The backend handle that created the event owns the id until
1650 /// the caller passes it to `destroyObject`.
1651 pub const EventHandle = struct {
1652 id: BackendObjectId,
1653 backend: BackendKind,
1654 };
1655
1656 pub const SurfaceHandle = struct {
1657 id: BackendObjectId,
1658 backend: BackendKind,
1659 platform: SurfacePlatformKind,
1660 extent: SurfaceExtent,
1661 format: TextureFormat,
1662 color_space: ColorSpace = .srgb,
1663 present_mode: PresentMode = .fifo,
1664 generation: u64 = 1,
1665 };
1666
1667 pub const TextureHandle = struct {
1668 id: BackendObjectId,
1669 backend: BackendKind,
1670 extent: TextureExtent,
1671 format: TextureFormat,
1672 usage: TextureUsage,
1673 sample_count: u32 = 1,
1674 ownership: TextureOwnership = .backend,
1675 };
1676
1677 pub const TextureView = struct {
1678 texture: TextureHandle,
1679 format: TextureFormat,
1680 base_mip_level: u32 = 0,
1681 mip_level_count: u32 = 1,
1682 base_array_layer: u32 = 0,
1683 array_layer_count: u32 = 1,
1684 };
1685
1686 pub const SurfaceFrame = struct {
1687 id: BackendObjectId,
1688 backend: BackendKind,
1689 surface: SurfaceHandle,
1690 texture: TextureHandle,
1691 view: TextureView,
1692 index: u32 = 0,
1693 generation: u64 = 1,
1694 token: u64 = 0,
1695 };
1696
1697 pub const BufferAllocation = struct {
1698 byte_size: usize,
1699 alignment: u32 = 1,
1700 dtype: ?DType = null,
1701 element_count: ?u64 = null,
1702 };
1703
1704 /// A caller fills this request to let a backend use caller memory as a buffer without copying. The
1705 /// request carries the caller's bytes, their alignment, and an optional element type and element
1706 /// count. The caller keeps the bytes alive and at the same address until `destroyObject` releases
1707 /// the handle the import returned. The backend never frees the bytes, and the CPU backend frees
1708 /// nothing when it destroys a borrowed buffer.
1709 pub const BufferImport = struct {
1710 bytes: []u8,
1711 alignment: u32 = 1,
1712 dtype: ?DType = null,
1713 element_count: ?u64 = null,
1714 };
1715
1716 pub const TextureAllocation = struct {
1717 extent: TextureExtent,
1718 format: TextureFormat,
1719 usage: TextureUsage,
1720 sample_count: u32 = 1,
1721 };
1722
1723 pub const SurfaceCreationRequest = struct {
1724 platform: SurfacePlatform,
1725 extent: SurfaceExtent,
1726 format: TextureFormat,
1727 color_space: ColorSpace = .srgb,
1728 present_mode: PresentMode = .fifo,
1729 alpha_mode: SurfaceAlphaMode = .solid,
1730 usage: TextureUsage = .{ .present = true, .copy_dst = true },
1731 max_frames_in_flight: u32 = 2,
1732 };
1733
1734 pub const SurfaceFrameAcquireRequest = struct {
1735 surface: SurfaceHandle,
1736 };
1737
1738 pub const PresentRequest = struct {
1739 surface: SurfaceHandle,
1740 frame: SurfaceFrame,
1741 wait_events: []const EventHandle = &.{},
1742 signal_event: ?EventHandle = null,
1743 };
1744
1745 pub const SurfaceClearColor = struct {
1746 r: f32 = 0,
1747 g: f32 = 0,
1748 b: f32 = 0,
1749 a: f32 = 1,
1750
1751 pub fn valid(self: SurfaceClearColor) bool {
1752 return std.math.isFinite(self.r) and
1753 std.math.isFinite(self.g) and
1754 std.math.isFinite(self.b) and
1755 std.math.isFinite(self.a);
1756 }
1757 };
1758
1759 pub const SurfaceFrameWriteOp = union(enum) {
1760 clear: SurfaceClearColor,
1761 copy_buffer: BufferHandle,
1762 };
1763
1764 pub const SurfaceFrameWriteRequest = struct {
1765 surface: SurfaceHandle,
1766 frame: SurfaceFrame,
1767 operations: []const SurfaceFrameWriteOp,
1768 wait_events: []const EventHandle = &.{},
1769 signal_event: ?EventHandle = null,
1770 };
1771
1772 pub const StreamAllocation = struct {};
1773
1774 pub const EventAllocation = struct {};
1775
1776 pub const BufferBinding = struct {
1777 handle: BufferHandle,
1778 access: BufferAccess,
1779 ownership: BufferOwnership,
1780 byte_size: usize,
1781 };
1782
1783 test "bufferArgumentCount subtracts scalars and rejects inverted counts" {
1784 var artifact = try KernelArtifact.init(std.testing.allocator, .{
1785 .backend = .vulkan,
1786 .format = .vulkan_spirv,
1787 .entry_name = "kernel",
1788 .argument_count = 5,
1789 .scalar_argument_count = 3,
1790 });
1791 defer artifact.deinit();
1792 try std.testing.expectEqual(@as(u32, 2), try artifact.bufferArgumentCount());
1793
1794 var inverted = try KernelArtifact.init(std.testing.allocator, .{
1795 .backend = .vulkan,
1796 .format = .vulkan_spirv,
1797 .entry_name = "kernel",
1798 .argument_count = 2,
1799 .scalar_argument_count = 3,
1800 });
1801 defer inverted.deinit();
1802 try std.testing.expectError(error.InvalidArtifact, inverted.bufferArgumentCount());
1803 }
1804
1805 pub const BufferWriteRequest = struct {
1806 handle: BufferHandle,
1807 bytes: []const u8,
1808 };
1809
1810 pub const BufferFillRequest = struct {
1811 handle: BufferHandle,
1812 pattern: u32,
1813 };
1814
1815 pub const BufferReadRequest = struct {
1816 handle: BufferHandle,
1817 bytes: []u8,
1818 };
1819
1820 pub const LaunchRequest = struct {
1821 artifact: *const KernelArtifact,
1822 loaded_artifact: ?LoadedArtifact = null,
1823 buffers: []const BufferBinding,
1824 scalar_arguments: []const choir_abi.ScalarArgument = &.{},
1825 geometry: choir_abi.LaunchGeometry,
1826 stream: ?StreamHandle = null,
1827 wait_events: []const EventHandle = &.{},
1828 signal_event: ?EventHandle = null,
1829 diagnostic_id: ?[]const u8 = null,
1830 };
1831
1832 /// Records `pass` and submits it once.
1833 pub const RenderRequest = struct {
1834 pass: RenderPass,
1835 stream: ?StreamHandle = null,
1836 wait_events: []const EventHandle = &.{},
1837 signal_event: ?EventHandle = null,
1838 };
1839
1840 /// Submits a recorded pass again.
1841 pub const RenderBundleSubmit = struct {
1842 bundle: RenderBundle,
1843 stream: ?StreamHandle = null,
1844 wait_events: []const EventHandle = &.{},
1845 signal_event: ?EventHandle = null,
1846 };
1847
1848 /// Replaces every texel of `texture` with `bytes`, rows tightly packed from the top.
1849 pub const TextureWriteRequest = struct {
1850 texture: TextureHandle,
1851 bytes: []const u8,
1852 };
1853
1854 /// Copies every texel of `texture` into `bytes`, rows tightly packed from the top.
1855 pub const TextureReadRequest = struct {
1856 texture: TextureHandle,
1857 bytes: []u8,
1858 };
1859
1860 pub const CompileRequest = struct {
1861 kernel_name: []const u8,
1862 requested_format: ArtifactFormat,
1863 argument_count: u32 = 0,
1864 scalar_argument_count: u32 = 0,
1865 required_dtypes: DTypeSet = .{},
1866 required_features: choir_abi.Features = .{},
1867 required_subgroup: choir_abi.SubgroupRequirements = .{},
1868 /// Layout the emitter gave the entry's push-constant block, if any.
1869 push_constants: choir_abi.PushConstants = .{},
1870 diagnostic_id: ?[]const u8 = null,
1871 payload: CompilePayload = .none,
1872 };
1873
1874 pub const CompilePayload = union(enum) {
1875 none,
1876 bytes: []const u8,
1877 words_u32: []const u32,
1878 text: []const u8,
1879 };
1880
1881 pub const SyncRequest = struct {
1882 scope: SyncScope,
1883 stream: ?StreamHandle = null,
1884 event: ?EventHandle = null,
1885
1886 pub fn valid(self: SyncRequest) bool {
1887 return switch (self.scope) {
1888 .default_stream, .device => self.stream == null and self.event == null,
1889 .stream => self.stream != null and self.event == null,
1890 .event => self.stream == null and self.event != null,
1891 };
1892 }
1893 };
1894
1895 pub const EventQueryRequest = struct {
1896 event: EventHandle,
1897 };
1898
1899 pub const EventRecordRequest = struct {
1900 stream: StreamHandle,
1901 event: EventHandle,
1902 };
1903
1904 pub const EventElapsedRequest = struct {
1905 start: EventHandle,
1906 end: EventHandle,
1907 };
1908
1909 pub const BackendVTable = struct {
1910 query_capabilities: *const fn (*anyopaque) BackendError!BackendCapabilities,
1911 create_artifact: ?*const fn (*anyopaque, CompileRequest) BackendError!KernelArtifact = null,
1912 load_artifact: ?*const fn (*anyopaque, *const KernelArtifact) BackendError!LoadedArtifact = null,
1913 create_render_artifact: ?*const fn (*anyopaque, RenderPipelineDesc) BackendError!RenderArtifact = null,
1914 load_render_artifact: ?*const fn (*anyopaque, *const RenderArtifact) BackendError!LoadedRenderArtifact = null,
1915 allocate_buffer: ?*const fn (*anyopaque, BufferAllocation) BackendError!BufferHandle = null,
1916 import_buffer: ?*const fn (*anyopaque, BufferImport) BackendError!BufferHandle = null,
1917 allocate_texture: ?*const fn (*anyopaque, TextureAllocation) BackendError!TextureHandle = null,
1918 create_surface: ?*const fn (*anyopaque, SurfaceCreationRequest) BackendError!SurfaceHandle = null,
1919 destroy_surface: ?*const fn (*anyopaque, SurfaceHandle) BackendError!void = null,
1920 destroy_texture: ?*const fn (*anyopaque, TextureHandle) BackendError!void = null,
1921 acquire_surface_frame: ?*const fn (*anyopaque, SurfaceFrameAcquireRequest) BackendError!SurfaceFrame = null,
1922 present_surface_frame: ?*const fn (*anyopaque, PresentRequest) BackendError!void = null,
1923 write_surface_frame: ?*const fn (*anyopaque, SurfaceFrameWriteRequest) BackendError!void = null,
1924 create_stream: ?*const fn (*anyopaque, StreamAllocation) BackendError!StreamHandle = null,
1925 create_event: ?*const fn (*anyopaque, EventAllocation) BackendError!EventHandle = null,
1926 write_buffer: ?*const fn (*anyopaque, BufferWriteRequest) BackendError!void = null,
1927 fill_buffer: ?*const fn (*anyopaque, BufferFillRequest) BackendError!void = null,
1928 read_buffer: ?*const fn (*anyopaque, BufferReadRequest) BackendError!void = null,
1929 launch: ?*const fn (*anyopaque, LaunchRequest) BackendError!void = null,
1930 render: ?*const fn (*anyopaque, RenderRequest) BackendError!void = null,
1931 create_render_bindings: ?*const fn (*anyopaque, RenderBindingsRequest) BackendError!RenderBindings = null,
1932 record_render_bundle: ?*const fn (*anyopaque, RenderPass) BackendError!RenderBundle = null,
1933 submit_render_bundle: ?*const fn (*anyopaque, RenderBundleSubmit) BackendError!void = null,
1934 write_texture: ?*const fn (*anyopaque, TextureWriteRequest) BackendError!void = null,
1935 read_texture: ?*const fn (*anyopaque, TextureReadRequest) BackendError!void = null,
1936 synchronize: ?*const fn (*anyopaque, SyncRequest) BackendError!void = null,
1937 query_event: ?*const fn (*anyopaque, EventQueryRequest) BackendError!bool = null,
1938 record_event: ?*const fn (*anyopaque, EventRecordRequest) BackendError!void = null,
1939 elapsed_event_ns: ?*const fn (*anyopaque, EventElapsedRequest) BackendError!u64 = null,
1940 destroy_object: ?*const fn (*anyopaque, BackendObjectId) void = null,
1941 deinit: ?*const fn (*anyopaque, Allocator) void = null,
1942 };
1943
1944 pub const BackendHandle = struct {
1945 ptr: *anyopaque,
1946 vtable: *const BackendVTable,
1947 kind: ?BackendKind = null,
1948
1949 pub fn backendKind(self: BackendHandle) ?BackendKind {
1950 return self.kind;
1951 }
1952
1953 pub fn queryCapabilities(self: BackendHandle) BackendError!BackendCapabilities {
1954 return self.vtable.query_capabilities(self.ptr);
1955 }
1956
1957 pub fn createArtifact(self: BackendHandle, request: CompileRequest) BackendError!KernelArtifact {
1958 const caps = try self.queryCapabilities();
1959 try caps.validateCompileRequest(request);
1960
1961 const create = self.vtable.create_artifact orelse return error.UnsupportedOperation;
1962 var artifact = try create(self.ptr, request);
1963 artifact.interface = .{
1964 .features = request.required_features,
1965 .subgroup = request.required_subgroup,
1966 .push_constants = request.push_constants,
1967 };
1968 return artifact;
1969 }
1970
1971 pub fn loadArtifact(self: BackendHandle, artifact: *const KernelArtifact) BackendError!LoadedArtifact {
1972 try self.expectArtifactBackend(artifact);
1973 if (!artifact.interface.push_constants.valid()) return error.InvalidArtifact;
1974 const caps = try self.queryCapabilities();
1975 if (!caps.supportsFeatures(artifact.interface.features)) return error.CapabilityMismatch;
1976 if (!caps.supportsSubgroup(artifact.interface.subgroup)) return error.CapabilityMismatch;
1977 const load = self.vtable.load_artifact orelse return error.UnsupportedOperation;
1978 return load(self.ptr, artifact);
1979 }
1980
1981 pub fn createRenderArtifact(self: BackendHandle, desc: RenderPipelineDesc) BackendError!RenderArtifact {
1982 const caps = try self.queryCapabilities();
1983 try caps.validateRenderPipelineDesc(desc);
1984
1985 const create = self.vtable.create_render_artifact orelse return error.UnsupportedOperation;
1986 var artifact = try create(self.ptr, desc);
1987 errdefer artifact.deinit();
1988 try self.expectRenderArtifactBackend(&artifact);
1989 if (artifact.format != desc.format) return error.InvalidRenderArtifact;
1990 if (artifact.target_format != desc.target_format) return error.InvalidRenderArtifact;
1991 if (artifact.blend_mode != desc.blend_mode) return error.InvalidRenderArtifact;
1992 if (artifact.topology != desc.topology) return error.InvalidRenderArtifact;
1993 return artifact;
1994 }
1995
1996 pub fn loadRenderArtifact(self: BackendHandle, artifact: *const RenderArtifact) BackendError!LoadedRenderArtifact {
1997 try self.expectRenderArtifactBackend(artifact);
1998 if (artifact.push_extent > artifact.push_constant_bytes) return error.PushConstantRangeExceeded;
1999 const load = self.vtable.load_render_artifact orelse return error.UnsupportedOperation;
2000 const loaded = try load(self.ptr, artifact);
2001 try self.expectLoadedRenderArtifactBackend(loaded);
2002 const expected = LoadedRenderArtifact.describing(artifact, loaded.id);
2003 if (!std.meta.eql(loaded, expected)) return error.InvalidRenderArtifact;
2004 return loaded;
2005 }
2006
2007 pub fn createRenderBindings(self: BackendHandle, request: RenderBindingsRequest) BackendError!RenderBindings {
2008 try self.expectRenderArtifactBackend(request.artifact);
2009 try self.expectLoadedRenderArtifactBackend(request.pipeline);
2010 for (request.resources) |resource| switch (resource) {
2011 .uniform_buffer, .storage_buffer => |buffer| try self.expectBufferBackend(buffer),
2012 .sampled_texture => |sampled| try self.expectTextureBackend(sampled.texture),
2013 .storage_texture => |texture| try self.expectTextureBackend(texture),
2014 };
2015 const caps = try self.queryCapabilities();
2016 try caps.validateRenderBindings(request);
2017 const create = self.vtable.create_render_bindings orelse return error.UnsupportedOperation;
2018 const bindings = try create(self.ptr, request);
2019 if (self.kind) |kind| {
2020 if (bindings.backend != kind) return error.CapabilityMismatch;
2021 }
2022 if (bindings.pipeline != request.pipeline.id) return error.InvalidRenderArtifact;
2023 return bindings;
2024 }
2025
2026 pub fn allocateBuffer(self: BackendHandle, request: BufferAllocation) BackendError!BufferHandle {
2027 const caps = try self.queryCapabilities();
2028 try caps.validateBufferAllocation(request);
2029 const allocate = self.vtable.allocate_buffer orelse return error.UnsupportedOperation;
2030 const handle = try allocate(self.ptr, request);
2031 try self.expectBufferBackend(handle);
2032 if (handle.byte_size < request.byte_size) return error.InvalidBuffer;
2033 return handle;
2034 }
2035
2036 /// A caller uses this to hand its own memory to a backend as a buffer without copying it. The
2037 /// call binds the caller's bytes and returns a buffer whose ownership is `borrowed_external`
2038 /// and whose size equals the length of the caller's bytes. The call first checks the size,
2039 /// element type and alignment against the backend's limits, and returns `error.InvalidBuffer`
2040 /// when the pointer lacks the requested alignment. A backend that does not offer imports
2041 /// returns `error.UnsupportedOperation`, and at present only the CPU backend offers them.
2042 pub fn importBuffer(self: BackendHandle, request: BufferImport) BackendError!BufferHandle {
2043 const caps = try self.queryCapabilities();
2044 try caps.validateBufferAllocation(.{
2045 .byte_size = request.bytes.len,
2046 .alignment = request.alignment,
2047 .dtype = request.dtype,
2048 .element_count = request.element_count,
2049 });
2050 std.debug.assert(std.math.isPowerOfTwo(request.alignment));
2051 if (!std.mem.isAligned(@intFromPtr(request.bytes.ptr), request.alignment)) {
2052 return error.InvalidBuffer;
2053 }
2054 const import_fn = self.vtable.import_buffer orelse return error.UnsupportedOperation;
2055 const handle = try import_fn(self.ptr, request);
2056 try self.expectBufferBackend(handle);
2057 if (handle.ownership != .borrowed_external) return error.InvalidBuffer;
2058 if (handle.byte_size != request.bytes.len) return error.InvalidBuffer;
2059 return handle;
2060 }
2061
2062 pub fn allocateTexture(self: BackendHandle, request: TextureAllocation) BackendError!TextureHandle {
2063 const caps = try self.queryCapabilities();
2064 try caps.validateTextureAllocation(request);
2065 const allocate = self.vtable.allocate_texture orelse return error.UnsupportedOperation;
2066 const handle = try allocate(self.ptr, request);
2067 try self.expectTextureBackend(handle);
2068 if (!sameTextureExtent(handle.extent, request.extent)) return error.InvalidTexture;
2069 if (handle.format != request.format) return error.InvalidTexture;
2070 if (!handle.usage.containsAll(request.usage)) return error.InvalidTexture;
2071 if (handle.sample_count != request.sample_count) return error.InvalidTexture;
2072 return handle;
2073 }
2074
2075 pub fn createSurface(self: BackendHandle, request: SurfaceCreationRequest) BackendError!SurfaceHandle {
2076 const caps = try self.queryCapabilities();
2077 try caps.validateSurfaceCreation(request);
2078 const create = self.vtable.create_surface orelse return error.UnsupportedOperation;
2079 const handle = try create(self.ptr, request);
2080 try self.expectSurfaceBackend(handle);
2081 if (handle.platform != request.platform.kind()) return error.InvalidSurface;
2082 if (!caps.surfaces.supportsExtent(handle.extent)) return error.InvalidSurface;
2083 if (handle.format != request.format) return error.InvalidSurface;
2084 if (handle.color_space != request.color_space) return error.InvalidSurface;
2085 if (handle.present_mode != request.present_mode) return error.InvalidSurface;
2086 return handle;
2087 }
2088
2089 pub fn destroySurface(self: BackendHandle, surface: SurfaceHandle) BackendError!void {
2090 try self.expectSurfaceBackend(surface);
2091 const destroy = self.vtable.destroy_surface orelse return error.UnsupportedOperation;
2092 return destroy(self.ptr, surface);
2093 }
2094
2095 pub fn destroyTexture(self: BackendHandle, texture: TextureHandle) BackendError!void {
2096 try self.expectTextureBackend(texture);
2097 const destroy = self.vtable.destroy_texture orelse return error.UnsupportedOperation;
2098 return destroy(self.ptr, texture);
2099 }
2100
2101 pub fn acquireSurfaceFrame(self: BackendHandle, request: SurfaceFrameAcquireRequest) BackendError!SurfaceFrame {
2102 try self.expectSurfaceBackend(request.surface);
2103 const acquire = self.vtable.acquire_surface_frame orelse return error.UnsupportedOperation;
2104 const frame = try acquire(self.ptr, request);
2105 try self.expectSurfaceFrameBackend(frame);
2106 try expectFrameMatchesSurface(frame, request.surface);
2107 return frame;
2108 }
2109
2110 pub fn presentSurfaceFrame(self: BackendHandle, request: PresentRequest) BackendError!void {
2111 try self.expectSurfaceBackend(request.surface);
2112 try self.expectSurfaceFrameBackend(request.frame);
2113 try expectFrameMatchesSurface(request.frame, request.surface);
2114 for (request.wait_events) |event| try self.expectEventBackend(event);
2115 if (request.signal_event) |event| try self.expectEventBackend(event);
2116 const present = self.vtable.present_surface_frame orelse return error.UnsupportedOperation;
2117 return present(self.ptr, request);
2118 }
2119
2120 pub fn writeSurfaceFrame(self: BackendHandle, request: SurfaceFrameWriteRequest) BackendError!void {
2121 try self.expectSurfaceBackend(request.surface);
2122 try self.expectSurfaceFrameBackend(request.frame);
2123 try expectFrameMatchesSurface(request.frame, request.surface);
2124 try expectSurfaceFrameWriteRequest(request);
2125 for (request.operations) |op| switch (op) {
2126 .clear => {},
2127 .copy_buffer => |buffer| try self.expectBufferBackend(buffer),
2128 };
2129 for (request.wait_events) |event| try self.expectEventBackend(event);
2130 if (request.signal_event) |event| try self.expectEventBackend(event);
2131 const write = self.vtable.write_surface_frame orelse return error.UnsupportedOperation;
2132 return write(self.ptr, request);
2133 }
2134
2135 pub fn createStream(self: BackendHandle, request: StreamAllocation) BackendError!StreamHandle {
2136 const caps = try self.queryCapabilities();
2137 try caps.validateRuntimeRequirements(.{ .streams = true });
2138 const create = self.vtable.create_stream orelse return error.UnsupportedOperation;
2139 return create(self.ptr, request);
2140 }
2141
2142 pub fn createEvent(self: BackendHandle, request: EventAllocation) BackendError!EventHandle {
2143 const caps = try self.queryCapabilities();
2144 try caps.validateRuntimeRequirements(.{ .events = true });
2145 const create = self.vtable.create_event orelse return error.UnsupportedOperation;
2146 return create(self.ptr, request);
2147 }
2148
2149 pub fn writeBuffer(self: BackendHandle, request: BufferWriteRequest) BackendError!void {
2150 try self.expectBufferBackend(request.handle);
2151 const write = self.vtable.write_buffer orelse return error.UnsupportedOperation;
2152 return write(self.ptr, request);
2153 }
2154
2155 pub fn fillBuffer(self: BackendHandle, request: BufferFillRequest) BackendError!void {
2156 try self.expectBufferBackend(request.handle);
2157 const fill = self.vtable.fill_buffer orelse return error.UnsupportedOperation;
2158 return fill(self.ptr, request);
2159 }
2160
2161 pub fn readBuffer(self: BackendHandle, request: BufferReadRequest) BackendError!void {
2162 try self.expectBufferBackend(request.handle);
2163 if (request.bytes.len < request.handle.byte_size) return error.ReadBufferDestinationTooSmall;
2164 const read = self.vtable.read_buffer orelse return error.UnsupportedOperation;
2165 return read(self.ptr, request);
2166 }
2167
2168 pub fn launch(self: BackendHandle, request: LaunchRequest) BackendError!void {
2169 try self.expectLaunchBackends(request);
2170 try expectLaunchArgumentCount(request);
2171 const caps = try self.queryCapabilities();
2172 try caps.validateLaunchGeometry(request.geometry);
2173 try caps.validateLaunchRuntime(request);
2174
2175 const launch_fn = self.vtable.launch orelse return error.UnsupportedOperation;
2176 return launch_fn(self.ptr, request);
2177 }
2178
2179 pub fn render(self: BackendHandle, request: RenderRequest) BackendError!void {
2180 try self.expectRenderPassBackends(request.pass);
2181 try self.expectSubmitBackends(request.stream, request.wait_events, request.signal_event);
2182 const caps = try self.queryCapabilities();
2183 try caps.validateRenderPass(request.pass);
2184 try caps.validateSubmitRuntime(request.stream, request.wait_events, request.signal_event);
2185
2186 const render_fn = self.vtable.render orelse return error.UnsupportedOperation;
2187 return render_fn(self.ptr, request);
2188 }
2189
2190 pub fn recordRenderBundle(self: BackendHandle, pass: RenderPass) BackendError!RenderBundle {
2191 try self.expectRenderPassBackends(pass);
2192 const caps = try self.queryCapabilities();
2193 try caps.validateRenderPass(pass);
2194 const record = self.vtable.record_render_bundle orelse return error.UnsupportedOperation;
2195 const bundle = try record(self.ptr, pass);
2196 if (self.kind) |kind| {
2197 if (bundle.backend != kind) return error.CapabilityMismatch;
2198 }
2199 if (bundle.draw_count != pass.draws.len) return error.RenderFailed;
2200 return bundle;
2201 }
2202
2203 pub fn submitRenderBundle(self: BackendHandle, request: RenderBundleSubmit) BackendError!void {
2204 if (self.kind) |kind| {
2205 if (request.bundle.backend != kind) return error.RenderArgumentMismatch;
2206 }
2207 try self.expectSubmitBackends(request.stream, request.wait_events, request.signal_event);
2208 const caps = try self.queryCapabilities();
2209 try caps.validateSubmitRuntime(request.stream, request.wait_events, request.signal_event);
2210 const submit = self.vtable.submit_render_bundle orelse return error.UnsupportedOperation;
2211 return submit(self.ptr, request);
2212 }
2213
2214 pub fn writeTexture(self: BackendHandle, request: TextureWriteRequest) BackendError!void {
2215 try self.expectTextureBackend(request.texture);
2216 try BackendCapabilities.validateTextureTransfer(request.texture, request.bytes.len, .{ .copy_dst = true });
2217 const write = self.vtable.write_texture orelse return error.UnsupportedOperation;
2218 return write(self.ptr, request);
2219 }
2220
2221 pub fn readTexture(self: BackendHandle, request: TextureReadRequest) BackendError!void {
2222 try self.expectTextureBackend(request.texture);
2223 try BackendCapabilities.validateTextureTransfer(request.texture, request.bytes.len, .{ .copy_src = true });
2224 const read = self.vtable.read_texture orelse return error.UnsupportedOperation;
2225 return read(self.ptr, request);
2226 }
2227
2228 pub fn synchronize(self: BackendHandle, request: SyncRequest) BackendError!void {
2229 if (!request.valid()) return error.UnsupportedOperation;
2230 try self.expectSyncBackends(request);
2231 const caps = try self.queryCapabilities();
2232 try caps.validateSyncRuntime(request);
2233 const sync = self.vtable.synchronize orelse return error.UnsupportedOperation;
2234 return sync(self.ptr, request);
2235 }
2236
2237 pub fn queryEvent(self: BackendHandle, request: EventQueryRequest) BackendError!bool {
2238 try self.expectEventBackend(request.event);
2239 const caps = try self.queryCapabilities();
2240 try caps.validateRuntimeRequirements(.{ .events = true });
2241 const query = self.vtable.query_event orelse return error.UnsupportedOperation;
2242 return query(self.ptr, request);
2243 }
2244
2245 pub fn recordEvent(self: BackendHandle, request: EventRecordRequest) BackendError!void {
2246 try self.expectStreamBackend(request.stream);
2247 try self.expectEventBackend(request.event);
2248 const caps = try self.queryCapabilities();
2249 try caps.validateRuntimeRequirements(.{ .streams = true, .events = true });
2250 const record = self.vtable.record_event orelse return error.UnsupportedOperation;
2251 return record(self.ptr, request);
2252 }
2253
2254 pub fn elapsedEventNs(self: BackendHandle, request: EventElapsedRequest) BackendError!u64 {
2255 try self.expectEventBackend(request.start);
2256 try self.expectEventBackend(request.end);
2257 const caps = try self.queryCapabilities();
2258 try caps.validateRuntimeRequirements(.{ .events = true });
2259 const elapsed = self.vtable.elapsed_event_ns orelse return error.UnsupportedOperation;
2260 return elapsed(self.ptr, request);
2261 }
2262
2263 /// A caller uses this to release a buffer, queue, event or loaded code that this backend handle
2264 /// created. The call releases one object this handle created. Before the call, the caller makes
2265 /// sure all launches, copies, queues and events still in flight have finished using the object.
2266 /// The call leaves state unchanged on a backend lacking a release function.
2267 pub fn destroyObject(self: BackendHandle, id: BackendObjectId) void {
2268 if (self.vtable.destroy_object) |destroy| destroy(self.ptr, id);
2269 }
2270
2271 pub fn deinit(self: BackendHandle, allocator: Allocator) void {
2272 if (self.vtable.deinit) |deinit_fn| deinit_fn(self.ptr, allocator);
2273 }
2274
2275 fn expectArtifactBackend(self: BackendHandle, artifact: *const KernelArtifact) BackendError!void {
2276 const kind = self.kind orelse return;
2277 if (artifact.backend != kind) return error.CapabilityMismatch;
2278 }
2279
2280 fn expectLoadedArtifactBackend(self: BackendHandle, loaded: LoadedArtifact) BackendError!void {
2281 const kind = self.kind orelse return;
2282 if (loaded.backend != kind) return error.InvalidArtifact;
2283 }
2284
2285 fn expectRenderArtifactBackend(self: BackendHandle, artifact: *const RenderArtifact) BackendError!void {
2286 const kind = self.kind orelse return;
2287 if (artifact.backend != kind) return error.CapabilityMismatch;
2288 }
2289
2290 fn expectLoadedRenderArtifactBackend(self: BackendHandle, loaded: LoadedRenderArtifact) BackendError!void {
2291 const kind = self.kind orelse return;
2292 if (loaded.backend != kind) return error.InvalidRenderArtifact;
2293 }
2294
2295 fn expectBufferBackend(self: BackendHandle, handle: BufferHandle) BackendError!void {
2296 const kind = self.kind orelse return;
2297 if (handle.backend != kind) return error.InvalidBuffer;
2298 }
2299
2300 fn expectSurfaceBackend(self: BackendHandle, handle: SurfaceHandle) BackendError!void {
2301 const kind = self.kind orelse return;
2302 if (handle.backend != kind) return error.InvalidSurface;
2303 }
2304
2305 fn expectTextureBackend(self: BackendHandle, handle: TextureHandle) BackendError!void {
2306 const kind = self.kind orelse return;
2307 if (handle.backend != kind) return error.InvalidTexture;
2308 }
2309
2310 fn expectTextureViewBackend(self: BackendHandle, view: TextureView) BackendError!void {
2311 try self.expectTextureBackend(view.texture);
2312 }
2313
2314 fn expectSurfaceFrameBackend(self: BackendHandle, frame: SurfaceFrame) BackendError!void {
2315 const kind = self.kind orelse return;
2316 if (frame.backend != kind) return error.InvalidSurfaceFrame;
2317 try self.expectSurfaceBackend(frame.surface);
2318 try self.expectTextureBackend(frame.texture);
2319 try self.expectTextureViewBackend(frame.view);
2320 }
2321
2322 fn expectStreamBackend(self: BackendHandle, handle: StreamHandle) BackendError!void {
2323 const kind = self.kind orelse return;
2324 if (handle.backend != kind) return error.InvalidStream;
2325 }
2326
2327 fn expectEventBackend(self: BackendHandle, handle: EventHandle) BackendError!void {
2328 const kind = self.kind orelse return;
2329 if (handle.backend != kind) return error.InvalidEvent;
2330 }
2331
2332 fn expectLaunchBackends(self: BackendHandle, request: LaunchRequest) BackendError!void {
2333 try self.expectArtifactBackend(request.artifact);
2334 if (request.loaded_artifact) |loaded| {
2335 try self.expectLoadedArtifactBackend(loaded);
2336 if (loaded.backend != request.artifact.backend or loaded.format != request.artifact.format) {
2337 return error.InvalidArtifact;
2338 }
2339 }
2340 for (request.buffers) |binding| {
2341 try self.expectBufferBackend(binding.handle);
2342 }
2343 if (request.stream) |stream| try self.expectStreamBackend(stream);
2344 for (request.wait_events) |event| try self.expectEventBackend(event);
2345 if (request.signal_event) |event| try self.expectEventBackend(event);
2346 }
2347
2348 fn expectRenderPassBackends(self: BackendHandle, pass: RenderPass) BackendError!void {
2349 try self.expectTextureViewBackend(pass.color.view);
2350 if (pass.depth) |depth| try self.expectTextureViewBackend(depth.view);
2351 for (pass.draws) |draw| {
2352 try self.expectLoadedRenderArtifactBackend(draw.pipeline);
2353 if (draw.bindings) |bindings| {
2354 if (self.kind) |kind| {
2355 if (bindings.backend != kind) return error.RenderArgumentMismatch;
2356 }
2357 }
2358 for (draw.vertex_buffers) |range| try self.expectBufferBackend(range.buffer);
2359 if (draw.index_buffer) |range| try self.expectBufferBackend(range.buffer);
2360 }
2361 }
2362
2363 fn expectSubmitBackends(
2364 self: BackendHandle,
2365 stream: ?StreamHandle,
2366 wait_events: []const EventHandle,
2367 signal_event: ?EventHandle,
2368 ) BackendError!void {
2369 if (stream) |handle| try self.expectStreamBackend(handle);
2370 for (wait_events) |event| try self.expectEventBackend(event);
2371 if (signal_event) |event| try self.expectEventBackend(event);
2372 }
2373
2374 fn expectLaunchArgumentCount(request: LaunchRequest) BackendError!void {
2375 const actual = request.buffers.len + request.scalar_arguments.len;
2376 const expected: usize = @intCast(request.artifact.argument_count);
2377 if (actual != expected) return error.LaunchArgumentMismatch;
2378 }
2379
2380 fn expectSyncBackends(self: BackendHandle, request: SyncRequest) BackendError!void {
2381 switch (request.scope) {
2382 .default_stream, .device => {},
2383 .stream => if (request.stream) |stream| try self.expectStreamBackend(stream),
2384 .event => if (request.event) |event| try self.expectEventBackend(event),
2385 }
2386 }
2387 };
2388
2389 fn sameSurfaceExtent(a: SurfaceExtent, b: SurfaceExtent) bool {
2390 return a.width == b.width and a.height == b.height;
2391 }
2392
2393 fn scissorWithin(scissor: RenderScissor, extent: TextureExtent) bool {
2394 const right = @as(u64, scissor.x) + scissor.width;
2395 const bottom = @as(u64, scissor.y) + scissor.height;
2396 return right <= extent.width and bottom <= extent.height;
2397 }
2398
2399 fn sameTextureExtent(a: TextureExtent, b: TextureExtent) bool {
2400 return a.width == b.width and a.height == b.height and a.depth == b.depth;
2401 }
2402
2403 fn expectFrameMatchesSurface(frame: SurfaceFrame, surface: SurfaceHandle) BackendError!void {
2404 if (frame.surface.id != surface.id) return error.InvalidSurfaceFrame;
2405 if (frame.surface.backend != surface.backend) return error.InvalidSurfaceFrame;
2406 if (frame.surface.generation != surface.generation) return error.SurfaceFrameExpired;
2407 if (frame.generation != surface.generation) return error.SurfaceFrameExpired;
2408 if (!sameSurfaceExtent(frame.surface.extent, surface.extent)) return error.SurfaceFrameExpired;
2409 if (frame.surface.format != surface.format) return error.SurfaceFrameExpired;
2410 if (frame.texture.backend != surface.backend) return error.InvalidTexture;
2411 if (frame.texture.format != surface.format) return error.InvalidTexture;
2412 if (frame.view.texture.id != frame.texture.id) return error.InvalidTexture;
2413 if (frame.view.format != frame.texture.format) return error.InvalidTexture;
2414 }
2415
2416 fn expectSurfaceFrameWriteRequest(request: SurfaceFrameWriteRequest) BackendError!void {
2417 if (request.operations.len == 0) return error.InvalidSurfaceFrame;
2418 if (!request.frame.texture.usage.copy_dst) return error.InvalidTexture;
2419 const required_bytes = try textureByteSize(request.frame.texture);
2420 for (request.operations) |op| switch (op) {
2421 .clear => |color| if (!color.valid()) return error.InvalidSurfaceFrame,
2422 .copy_buffer => |buffer| if (buffer.byte_size < required_bytes) return error.InvalidBuffer,
2423 };
2424 }
2425
2426 fn textureByteSize(texture: TextureHandle) BackendError!usize {
2427 if (!texture.extent.valid()) return error.InvalidTexture;
2428 const width: usize = @intCast(texture.extent.width);
2429 const height: usize = @intCast(texture.extent.height);
2430 const depth: usize = @intCast(texture.extent.depth);
2431 const wh = std.math.mul(usize, width, height) catch return error.InvalidTexture;
2432 const pixels = std.math.mul(usize, wh, depth) catch return error.InvalidTexture;
2433 return std.math.mul(usize, pixels, texture.format.texelBytes()) catch return error.InvalidTexture;
2434 }
2435
2436 fn bitForDType(value: DType) u64 {
2437 const shift: u6 = @intCast(@backingInt(value));
2438 return @as(u64, 1) << shift;
2439 }
2440
2441 fn bitForArtifactFormat(value: ArtifactFormat) u64 {
2442 const shift: u6 = @intCast(@backingInt(value));
2443 return @as(u64, 1) << shift;
2444 }
2445
2446 fn bitForTextureFormat(value: TextureFormat) u64 {
2447 const shift: u6 = @intCast(@backingInt(value));
2448 return @as(u64, 1) << shift;
2449 }
2450
2451 fn bitForPresentMode(value: PresentMode) u64 {
2452 const shift: u6 = @intCast(@backingInt(value));
2453 return @as(u64, 1) << shift;
2454 }
2455
2456 fn bitForColorSpace(value: ColorSpace) u64 {
2457 const shift: u6 = @intCast(@backingInt(value));
2458 return @as(u64, 1) << shift;
2459 }
2460
2461 fn bitForSurfacePlatformKind(value: SurfacePlatformKind) u64 {
2462 const shift: u6 = @intCast(@backingInt(value));
2463 return @as(u64, 1) << shift;
2464 }
2465
2466 fn bitForRenderArtifactFormat(value: RenderArtifactFormat) u64 {
2467 const shift: u6 = @intCast(@backingInt(value));
2468 return @as(u64, 1) << shift;
2469 }
2470
2471 fn bitForRenderBlendMode(value: RenderBlendMode) u64 {
2472 const shift: u6 = @intCast(@backingInt(value));
2473 return @as(u64, 1) << shift;
2474 }
2475
2476 fn bitForRenderPrimitiveTopology(value: RenderPrimitiveTopology) u64 {
2477 const shift: u6 = @intCast(@backingInt(value));
2478 return @as(u64, 1) << shift;
2479 }
2480
2481 fn bitForRenderVertexFormat(value: RenderVertexFormat) u64 {
2482 const shift: u6 = @intCast(@backingInt(value));
2483 return @as(u64, 1) << shift;
2484 }
2485
2486 fn bitForRenderBindingKind(value: RenderBindingKind) u64 {
2487 const shift: u6 = @intCast(@backingInt(value));
2488 return @as(u64, 1) << shift;
2489 }
2490
2491 fn bitForRenderIndexFormat(value: RenderIndexFormat) u64 {
2492 const shift: u6 = @intCast(@backingInt(value));
2493 return @as(u64, 1) << shift;
2494 }
2495
2496 fn renderVertexFormatByteSize(format: RenderVertexFormat) u32 {
2497 return switch (format) {
2498 .float32, .uint32 => 4,
2499 .float32x2, .uint32x2 => 8,
2500 .float32x3 => 12,
2501 .float32x4, .uint32x4 => 16,
2502 };
2503 }
2504
2505 fn dupeOpt(allocator: Allocator, value: ?[]const u8) Allocator.Error!?[]const u8 {
2506 return if (value) |actual| try dupeString(allocator, actual) else null;
2507 }
2508
2509 fn freeOpt(allocator: Allocator, value: ?[]const u8) void {
2510 if (value) |actual| freeString(allocator, actual);
2511 }
2512
2513 fn dupeString(allocator: Allocator, value: []const u8) Allocator.Error![]const u8 {
2514 return try allocator.dupe(u8, value);
2515 }
2516
2517 fn freeString(allocator: Allocator, value: []const u8) void {
2518 allocator.free(@constCast(value));
2519 }
2520
2521 test "capabilities record accelerator facts in machine-readable sets" {
2522 const caps = BackendCapabilities{
2523 .identity = .{
2524 .backend = .vulkan,
2525 .family = .vulkan,
2526 .name = "test-vulkan-device",
2527 .vendor_id = 0x10de,
2528 },
2529 .memory = .{
2530 .global_bytes = 8 * 1024 * 1024 * 1024,
2531 .max_allocation_bytes = 1024 * 1024 * 1024,
2532 .shared_memory_per_threadgroup_bytes = 32 * 1024,
2533 .min_buffer_alignment = 256,
2534 .host_visible_device_memory = true,
2535 },
2536 .subgroup = .{
2537 .supported = true,
2538 .size_min = 32,
2539 .size_max = 32,
2540 .shuffle = true,
2541 .ballot = true,
2542 .arithmetic = true,
2543 },
2544 .threadgroup = .{
2545 .max_threads = 256,
2546 .max_blocks = .{ 65535, 65535, 65535 },
2547 .max_threads_per_dim = .{ 256, 256, 64 },
2548 .max_grid_per_dim = .{ 65535, 65535, 65535 },
2549 .shared_memory_bytes = 32 * 1024,
2550 },
2551 .dtypes = DTypeSet.init(&.{ .f16, .f32, .i32 }),
2552 .layouts = .{
2553 .row_major = true,
2554 .compact_strides = true,
2555 .broadcast_strides = true,
2556 .tiled = true,
2557 },
2558 .runtime = .{
2559 .driver_loaded = true,
2560 .device_context = true,
2561 .streams = true,
2562 .events = true,
2563 .timeline_events = true,
2564 },
2565 .features = .{
2566 .atomic_i32 = true,
2567 .atomic_u32 = true,
2568 .atomic_index = true,
2569 .atomic_f32_add_device = true,
2570 .atomic_f32_add_shared = true,
2571 .async_copy = true,
2572 },
2573 .artifact_formats = ArtifactFormatSet.init(&.{.vulkan_spirv}),
2574 };
2575
2576 try std.testing.expect(caps.supportsDType(.f32));
2577 try std.testing.expect(!caps.supportsDType(.f64));
2578 try std.testing.expect(caps.supportsArtifactFormat(.vulkan_spirv));
2579 try std.testing.expect(!caps.supportsArtifactFormat(.cuda_ptx));
2580 try std.testing.expect(caps.runtime.timeline_events);
2581 try std.testing.expect(caps.layouts.tiled);
2582 }
2583
2584 test "backend contract keeps native cpu distinct from external" {
2585 try std.testing.expectEqual(@as(u8, 0), @backingInt(BackendKind.cuda));
2586 try std.testing.expectEqual(@as(u8, 1), @backingInt(BackendKind.vulkan));
2587 try std.testing.expectEqual(@as(u8, 2), @backingInt(BackendKind.metal));
2588 try std.testing.expectEqual(@as(u8, 3), @backingInt(BackendKind.external));
2589 try std.testing.expectEqual(@as(u8, 4), @backingInt(BackendKind.webgpu));
2590 try std.testing.expectEqual(@as(u8, 5), @backingInt(BackendKind.cpu));
2591 try std.testing.expectEqual(@as(u8, 6), @backingInt(BackendKind.wasm));
2592
2593 try std.testing.expectEqual(@as(u8, 0), @backingInt(DeviceFamily.nvidia_cuda));
2594 try std.testing.expectEqual(@as(u8, 1), @backingInt(DeviceFamily.vulkan));
2595 try std.testing.expectEqual(@as(u8, 2), @backingInt(DeviceFamily.apple_metal));
2596 try std.testing.expectEqual(@as(u8, 3), @backingInt(DeviceFamily.external));
2597 try std.testing.expectEqual(@as(u8, 4), @backingInt(DeviceFamily.webgpu));
2598 try std.testing.expectEqual(@as(u8, 5), @backingInt(DeviceFamily.native_cpu));
2599 try std.testing.expectEqual(@as(u8, 6), @backingInt(DeviceFamily.webassembly));
2600
2601 try std.testing.expectEqual(@as(u8, 0), @backingInt(ArtifactFormat.cuda_ptx));
2602 try std.testing.expectEqual(@as(u8, 1), @backingInt(ArtifactFormat.cuda_cubin));
2603 try std.testing.expectEqual(@as(u8, 2), @backingInt(ArtifactFormat.vulkan_spirv));
2604 try std.testing.expectEqual(@as(u8, 3), @backingInt(ArtifactFormat.metal_msl));
2605 try std.testing.expectEqual(@as(u8, 4), @backingInt(ArtifactFormat.metal_metallib));
2606 try std.testing.expectEqual(@as(u8, 5), @backingInt(ArtifactFormat.external));
2607 try std.testing.expectEqual(@as(u8, 6), @backingInt(ArtifactFormat.webgpu_wgsl));
2608 try std.testing.expectEqual(@as(u8, 7), @backingInt(ArtifactFormat.cpu_machine_code));
2609 try std.testing.expectEqual(@as(u8, 8), @backingInt(ArtifactFormat.cpu_object));
2610 try std.testing.expectEqual(@as(u8, 9), @backingInt(ArtifactFormat.webassembly_module));
2611
2612 try std.testing.expectEqual(DeviceFamily.native_cpu, familyForBackendKind(.cpu));
2613 try std.testing.expectEqual(DeviceFamily.webgpu, familyForBackendKind(.webgpu));
2614 try std.testing.expectEqual(DeviceFamily.webassembly, familyForBackendKind(.wasm));
2615 try std.testing.expectEqual(DeviceFamily.external, familyForBackendKind(.external));
2616 try std.testing.expect(artifactFormatIsNativeCpu(.cpu_object));
2617 try std.testing.expect(artifactFormatIsNativeCpu(.cpu_machine_code));
2618 try std.testing.expect(!artifactFormatIsNativeCpu(.cuda_ptx));
2619 try std.testing.expect(!artifactFormatIsNativeCpu(.external));
2620 try std.testing.expect(artifactFormatUsesHostLoopLaunch(.cpu_object));
2621 try std.testing.expect(artifactFormatUsesHostLoopLaunch(.cpu_machine_code));
2622 try std.testing.expect(artifactFormatUsesHostLoopLaunch(.webassembly_module));
2623 try std.testing.expect(!artifactFormatUsesHostLoopLaunch(.webgpu_wgsl));
2624 }
2625
2626 test "native cpu capabilities validate object and machine-code compile requests" {
2627 const caps = BackendCapabilities{
2628 .identity = .{
2629 .backend = .cpu,
2630 .family = .native_cpu,
2631 .name = "native-cpu",
2632 },
2633 .threadgroup = .{
2634 .max_threads = 1,
2635 .max_blocks = .{ 1, 1, 1 },
2636 .max_threads_per_dim = .{ 1, 1, 1 },
2637 .max_grid_per_dim = .{ 1, 1, 1 },
2638 },
2639 .dtypes = DTypeSet.init(&.{ .i32, .u32, .f32, .f64 }),
2640 .artifact_formats = ArtifactFormatSet.init(&.{ .cpu_object, .cpu_machine_code }),
2641 };
2642
2643 try caps.validateCompileRequest(.{
2644 .kernel_name = "add",
2645 .requested_format = .cpu_object,
2646 .required_dtypes = DTypeSet.init(&.{ .f32, .f64 }),
2647 });
2648 try caps.validateCompileRequest(.{
2649 .kernel_name = "add",
2650 .requested_format = .cpu_machine_code,
2651 .required_dtypes = DTypeSet.init(&.{.i32}),
2652 });
2653 try std.testing.expectError(error.UnsupportedArtifactFormat, caps.validateCompileRequest(.{
2654 .kernel_name = "add",
2655 .requested_format = .cuda_ptx,
2656 }));
2657 }
2658
2659 test "capabilities validate compile requests and launch geometry" {
2660 const caps = BackendCapabilities{
2661 .identity = .{
2662 .backend = .cuda,
2663 .family = .nvidia_cuda,
2664 .name = "test-cuda-device",
2665 },
2666 .threadgroup = .{
2667 .max_threads = 256,
2668 .max_blocks = .{ 65_535, 65_535, 65_535 },
2669 .max_threads_per_dim = .{ 256, 16, 16 },
2670 .max_grid_per_dim = .{ 65_535, 65_535, 64 },
2671 .shared_memory_bytes = 48 * 1024,
2672 },
2673 .subgroup = .{
2674 .supported = true,
2675 .size_min = 32,
2676 .size_max = 32,
2677 .shuffle = true,
2678 .ballot = true,
2679 .vote = true,
2680 .arithmetic = true,
2681 },
2682 .dtypes = DTypeSet.init(&.{ .f32, .i32 }),
2683 .features = .{
2684 .atomic_i32 = true,
2685 .atomic_u32 = true,
2686 .atomic_index = true,
2687 .atomic_f32_add_device = true,
2688 .atomic_f32_add_shared = true,
2689 .dynamic_shared_memory = true,
2690 },
2691 .artifact_formats = ArtifactFormatSet.init(&.{.cuda_ptx}),
2692 };
2693
2694 try caps.validateCompileRequest(.{
2695 .kernel_name = "add",
2696 .requested_format = .cuda_ptx,
2697 .required_dtypes = DTypeSet.init(&.{.f32}),
2698 .required_features = .{ .atomic_i32 = true },
2699 .required_subgroup = .{ .supported = true, .arithmetic = true },
2700 });
2701 try std.testing.expectError(error.UnsupportedArtifactFormat, caps.validateCompileRequest(.{
2702 .kernel_name = "add",
2703 .requested_format = .vulkan_spirv,
2704 }));
2705 try std.testing.expectError(error.CapabilityMismatch, caps.validateCompileRequest(.{
2706 .kernel_name = "add",
2707 .requested_format = .cuda_ptx,
2708 .required_dtypes = DTypeSet.init(&.{.f64}),
2709 }));
2710 try std.testing.expectError(error.CapabilityMismatch, caps.validateCompileRequest(.{
2711 .kernel_name = "add",
2712 .requested_format = .cuda_ptx,
2713 .required_features = .{ .async_copy = true },
2714 }));
2715 try std.testing.expectError(error.CapabilityMismatch, caps.validateCompileRequest(.{
2716 .kernel_name = "add",
2717 .requested_format = .cuda_ptx,
2718 .required_subgroup = .{ .scan = true },
2719 }));
2720
2721 try caps.validateLaunchGeometry(.{
2722 .grid = .{ 16, 4, 1 },
2723 .threadgroup = .{ 128, 1, 1 },
2724 .dynamic_shared_memory_bytes = 1024,
2725 });
2726 var no_dynamic_shared = caps;
2727 no_dynamic_shared.features.dynamic_shared_memory = false;
2728 try std.testing.expectError(error.CapabilityMismatch, no_dynamic_shared.validateLaunchGeometry(.{
2729 .grid = .{ 16, 4, 1 },
2730 .threadgroup = .{ 128, 1, 1 },
2731 .dynamic_shared_memory_bytes = 1024,
2732 }));
2733 try std.testing.expectError(error.LaunchArgumentMismatch, caps.validateLaunchGeometry(.{
2734 .grid = .{ 0, 1, 1 },
2735 .threadgroup = .{ 1, 1, 1 },
2736 }));
2737 try std.testing.expectError(error.LaunchArgumentMismatch, caps.validateLaunchGeometry(.{
2738 .grid = .{ 1, 1, 1 },
2739 .threadgroup = .{ 0, 1, 1 },
2740 }));
2741 try std.testing.expectError(error.CapabilityMismatch, caps.validateLaunchGeometry(.{
2742 .grid = .{ 1, 1, 1 },
2743 .threadgroup = .{ 512, 1, 1 },
2744 }));
2745 try std.testing.expectError(error.CapabilityMismatch, caps.validateLaunchGeometry(.{
2746 .grid = .{ 1, 1, 65 },
2747 .threadgroup = .{ 1, 1, 1 },
2748 }));
2749 try std.testing.expectError(error.CapabilityMismatch, caps.validateLaunchGeometry(.{
2750 .grid = .{ 1, 1, 1 },
2751 .threadgroup = .{ 1, 1, 1 },
2752 .dynamic_shared_memory_bytes = 64 * 1024,
2753 }));
2754 }
2755
2756 test "kernel artifacts represent cuda vulkan metal and external payloads" {
2757 var ptx = try KernelArtifact.init(std.testing.allocator, .{
2758 .backend = .cuda,
2759 .format = .cuda_ptx,
2760 .entry_name = "add_f32",
2761 .argument_count = 3,
2762 .diagnostic_id = "cuda/add_f32",
2763 });
2764 defer ptx.deinit();
2765 try ptx.setOwnedText("// ptx");
2766 try std.testing.expectEqual(ArtifactFormat.cuda_ptx, ptx.format);
2767 try std.testing.expectEqualStrings("// ptx", ptx.payload.text);
2768
2769 var spirv = try KernelArtifact.init(std.testing.allocator, .{
2770 .backend = .vulkan,
2771 .format = .vulkan_spirv,
2772 .entry_name = "main",
2773 .argument_count = 4,
2774 });
2775 defer spirv.deinit();
2776 const words = [_]u32{ 0x07230203, 0x00010000 };
2777 spirv.setBorrowedWords(&words);
2778 try std.testing.expectEqual(ArtifactFormat.vulkan_spirv, spirv.format);
2779 try std.testing.expectEqual(@as(u32, 0x07230203), spirv.payload.words_u32[0]);
2780
2781 var metallib = try KernelArtifact.init(std.testing.allocator, .{
2782 .backend = .metal,
2783 .format = .metal_metallib,
2784 .entry_name = "main0",
2785 .argument_count = 2,
2786 });
2787 defer metallib.deinit();
2788 try metallib.setOwnedBytes(&.{ 0xca, 0xfe, 0xba, 0xbe });
2789 try std.testing.expectEqual(ArtifactFormat.metal_metallib, metallib.format);
2790 try std.testing.expectEqual(@as(u8, 0xca), metallib.payload.bytes[0]);
2791 }
2792
2793 const OwnedExternalPayloadState = struct {
2794 destroyed: *bool,
2795
2796 fn destroy(allocator: Allocator, ptr: *anyopaque) void {
2797 const state: *@This() = @ptrCast(@alignCast(ptr));
2798 state.destroyed.* = true;
2799 allocator.destroy(state);
2800 }
2801 };
2802
2803 test "owned external payload requires and runs destructor" {
2804 var artifact = try KernelArtifact.init(std.testing.allocator, .{
2805 .backend = .external,
2806 .format = .external,
2807 .entry_name = "external",
2808 .argument_count = 0,
2809 });
2810 var did_deinit = false;
2811 defer if (!did_deinit) artifact.deinit();
2812
2813 var marker: u8 = 0;
2814 try std.testing.expectError(error.MissingPayloadDeinit, artifact.setExternalPayload(.{
2815 .ptr = &marker,
2816 .type_id = "test.Payload",
2817 }, .owned));
2818
2819 var destroyed = false;
2820 const state = try std.testing.allocator.create(OwnedExternalPayloadState);
2821 state.* = .{ .destroyed = &destroyed };
2822 try artifact.setExternalPayload(.{
2823 .ptr = state,
2824 .type_id = "test.Payload",
2825 .deinit_fn = OwnedExternalPayloadState.destroy,
2826 }, .owned);
2827
2828 artifact.deinit();
2829 did_deinit = true;
2830 try std.testing.expect(destroyed);
2831 }
2832
2833 const FakeBackendState = struct {
2834 launched: bool = false,
2835 last_buffer_ownership: ?BufferOwnership = null,
2836 event_ready: bool = false,
2837 queried_event: ?BackendObjectId = null,
2838 recorded_event: ?BackendObjectId = null,
2839 record_stream: ?BackendObjectId = null,
2840 elapsed_start_event: ?BackendObjectId = null,
2841 elapsed_end_event: ?BackendObjectId = null,
2842 elapsed_ns: u64 = 0,
2843 next_id: BackendObjectId = 1,
2844 allocation_count: usize = 0,
2845 last_allocation: ?BufferAllocation = null,
2846 allocated_byte_size: ?usize = null,
2847 allocated_backend: BackendKind = .cuda,
2848 created_stream: bool = false,
2849 created_event: bool = false,
2850 supports_streams: bool = true,
2851 supports_events: bool = true,
2852 supports_timeline_events: bool = false,
2853 sync_count: usize = 0,
2854 last_sync_request: ?SyncRequest = null,
2855 global_bytes: ?u64 = null,
2856 max_allocation_bytes: ?u64 = null,
2857 min_buffer_alignment: u32 = 1,
2858 supports_textures: bool = true,
2859 supports_surfaces: bool = true,
2860 texture_allocate_count: usize = 0,
2861 created_surface_count: usize = 0,
2862 acquired_frame_count: usize = 0,
2863 present_count: usize = 0,
2864 surface_write_count: usize = 0,
2865 last_surface_write_frame_id: ?BackendObjectId = null,
2866 last_surface_write_op_count: usize = 0,
2867 destroyed_surface_count: usize = 0,
2868 destroyed_texture_count: usize = 0,
2869 allocated_texture_backend: BackendKind = .cuda,
2870 created_surface_backend: BackendKind = .cuda,
2871 created_surface_extent: ?SurfaceExtent = null,
2872 supports_raster: bool = true,
2873 render_create_count: usize = 0,
2874 render_load_count: usize = 0,
2875 render_count: usize = 0,
2876 created_render_backend: BackendKind = .cuda,
2877 loaded_render_backend: BackendKind = .cuda,
2878 last_render_draw_count: usize = 0,
2879 last_render_vertex_count: u32 = 0,
2880 last_render_instance_count: u32 = 0,
2881 render_bindings_count: usize = 0,
2882 bundle_record_count: usize = 0,
2883 bundle_submit_count: usize = 0,
2884 texture_write_count: usize = 0,
2885 texture_read_count: usize = 0,
2886 };
2887
2888 fn fakeQueryCapabilities(ptr: *anyopaque) BackendError!BackendCapabilities {
2889 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
2890 return .{
2891 .identity = .{
2892 .backend = .cuda,
2893 .family = .nvidia_cuda,
2894 .name = "fake-cuda",
2895 },
2896 .memory = .{
2897 .global_bytes = state.global_bytes,
2898 .max_allocation_bytes = state.max_allocation_bytes,
2899 .min_buffer_alignment = state.min_buffer_alignment,
2900 },
2901 .dtypes = DTypeSet.init(&.{ .f32, .u32 }),
2902 .artifact_formats = ArtifactFormatSet.init(&.{.cuda_ptx}),
2903 .runtime = .{
2904 .streams = state.supports_streams,
2905 .events = state.supports_events,
2906 .timeline_events = state.supports_timeline_events,
2907 },
2908 .features = .{
2909 .dynamic_shared_memory = true,
2910 },
2911 .textures = if (state.supports_textures) .{
2912 .supported = true,
2913 .formats = TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm, .depth32_float }),
2914 .usages = .{
2915 .copy_src = true,
2916 .copy_dst = true,
2917 .sampled = true,
2918 .storage = true,
2919 .color_attachment = true,
2920 .depth_attachment = true,
2921 .present = true,
2922 },
2923 .max_extent = .{ .width = 8192, .height = 8192, .depth = 16 },
2924 .max_sample_count = 4,
2925 } else .{},
2926 .surfaces = if (state.supports_surfaces) .{
2927 .supported = true,
2928 .platforms = SurfacePlatformSet.init(&.{.headless}),
2929 .formats = TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm }),
2930 .color_spaces = ColorSpaceSet.init(&.{ .srgb, .linear }),
2931 .present_modes = PresentModeSet.init(&.{ .fifo, .mailbox }),
2932 .usages = .{
2933 .copy_dst = true,
2934 .storage = true,
2935 .color_attachment = true,
2936 .present = true,
2937 },
2938 .max_extent = .{ .width = 8192, .height = 8192 },
2939 .max_frames_in_flight = 3,
2940 } else .{},
2941 .raster = if (state.supports_raster) .{
2942 .supported = true,
2943 .artifact_formats = RenderArtifactFormatSet.init(&.{.external}),
2944 .target_formats = TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm }),
2945 .depth_formats = TextureFormatSet.init(&.{.depth32_float}),
2946 .blend_modes = RenderBlendModeSet.init(&.{ .replace, .alpha_premultiplied }),
2947 .topologies = RenderPrimitiveTopologySet.init(&.{ .triangle_list, .triangle_strip }),
2948 .vertex_formats = RenderVertexFormatSet.init(&.{ .float32x2, .float32x4, .uint32 }),
2949 .binding_kinds = RenderBindingKindSet.init(&.{ .uniform_buffer, .sampled_texture }),
2950 .index_formats = RenderIndexFormatSet.init(&.{ .none, .u16, .u32 }),
2951 .max_vertex_buffers = 4,
2952 .max_vertex_attributes = 8,
2953 .max_bindings = 8,
2954 .instancing = true,
2955 } else .{},
2956 .threadgroup = .{
2957 .max_threads = 256,
2958 .max_blocks = .{ 65_535, 65_535, 65_535 },
2959 .max_threads_per_dim = .{ 256, 16, 16 },
2960 .max_grid_per_dim = .{ 65_535, 65_535, 64 },
2961 .shared_memory_bytes = 48 * 1024,
2962 },
2963 };
2964 }
2965
2966 fn fakeLaunch(ptr: *anyopaque, request: LaunchRequest) BackendError!void {
2967 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
2968 if (request.buffers.len + request.scalar_arguments.len != request.artifact.argument_count) {
2969 return error.LaunchArgumentMismatch;
2970 }
2971 state.launched = true;
2972 state.last_buffer_ownership = request.buffers[0].ownership;
2973 }
2974
2975 fn fakeAllocateBuffer(ptr: *anyopaque, request: BufferAllocation) BackendError!BufferHandle {
2976 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
2977 defer state.next_id += 1;
2978 state.allocation_count += 1;
2979 state.last_allocation = request;
2980 return .{
2981 .id = state.next_id,
2982 .backend = state.allocated_backend,
2983 .byte_size = state.allocated_byte_size orelse request.byte_size,
2984 .ownership = .backend,
2985 };
2986 }
2987
2988 fn fakeAllocateTexture(ptr: *anyopaque, request: TextureAllocation) BackendError!TextureHandle {
2989 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
2990 defer state.next_id += 1;
2991 state.texture_allocate_count += 1;
2992 return .{
2993 .id = state.next_id,
2994 .backend = state.allocated_texture_backend,
2995 .extent = request.extent,
2996 .format = request.format,
2997 .usage = request.usage,
2998 .sample_count = request.sample_count,
2999 .ownership = .backend,
3000 };
3001 }
3002
3003 fn fakeCreateSurface(ptr: *anyopaque, request: SurfaceCreationRequest) BackendError!SurfaceHandle {
3004 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3005 defer state.next_id += 1;
3006 state.created_surface_count += 1;
3007 const extent = state.created_surface_extent orelse request.extent;
3008 return .{
3009 .id = state.next_id,
3010 .backend = state.created_surface_backend,
3011 .platform = request.platform.kind(),
3012 .extent = extent,
3013 .format = request.format,
3014 .color_space = request.color_space,
3015 .present_mode = request.present_mode,
3016 .generation = 1,
3017 };
3018 }
3019
3020 fn fakeDestroySurface(ptr: *anyopaque, _: SurfaceHandle) BackendError!void {
3021 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3022 state.destroyed_surface_count += 1;
3023 }
3024
3025 fn fakeDestroyTexture(ptr: *anyopaque, _: TextureHandle) BackendError!void {
3026 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3027 state.destroyed_texture_count += 1;
3028 }
3029
3030 fn fakeAcquireSurfaceFrame(ptr: *anyopaque, request: SurfaceFrameAcquireRequest) BackendError!SurfaceFrame {
3031 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3032 const texture_id = state.next_id;
3033 state.next_id += 1;
3034 const frame_id = state.next_id;
3035 state.next_id += 1;
3036 state.acquired_frame_count += 1;
3037 const texture = TextureHandle{
3038 .id = texture_id,
3039 .backend = request.surface.backend,
3040 .extent = .{
3041 .width = request.surface.extent.width,
3042 .height = request.surface.extent.height,
3043 .depth = 1,
3044 },
3045 .format = request.surface.format,
3046 .usage = .{ .present = true, .color_attachment = true, .copy_dst = true },
3047 .sample_count = 1,
3048 .ownership = .acquired_surface,
3049 };
3050 const view = TextureView{
3051 .texture = texture,
3052 .format = texture.format,
3053 };
3054 return .{
3055 .id = frame_id,
3056 .backend = request.surface.backend,
3057 .surface = request.surface,
3058 .texture = texture,
3059 .view = view,
3060 .generation = request.surface.generation,
3061 };
3062 }
3063
3064 fn fakePresentSurfaceFrame(ptr: *anyopaque, _: PresentRequest) BackendError!void {
3065 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3066 state.present_count += 1;
3067 }
3068
3069 fn fakeWriteSurfaceFrame(ptr: *anyopaque, request: SurfaceFrameWriteRequest) BackendError!void {
3070 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3071 state.surface_write_count += 1;
3072 state.last_surface_write_frame_id = request.frame.id;
3073 state.last_surface_write_op_count = request.operations.len;
3074 }
3075
3076 fn fakeCreateRenderArtifact(ptr: *anyopaque, desc: RenderPipelineDesc) BackendError!RenderArtifact {
3077 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3078 state.render_create_count += 1;
3079 var artifact = RenderArtifact.init(std.testing.allocator, .{
3080 .backend = state.created_render_backend,
3081 .pipeline = desc,
3082 }) catch return error.OutOfMemory;
3083 errdefer artifact.deinit();
3084 artifact.setBorrowedText("fake-render");
3085 return artifact;
3086 }
3087
3088 fn fakeLoadRenderArtifact(ptr: *anyopaque, artifact: *const RenderArtifact) BackendError!LoadedRenderArtifact {
3089 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3090 defer state.next_id += 1;
3091 state.render_load_count += 1;
3092 var loaded = LoadedRenderArtifact.describing(artifact, state.next_id);
3093 loaded.backend = state.loaded_render_backend;
3094 return loaded;
3095 }
3096
3097 fn fakeRender(ptr: *anyopaque, request: RenderRequest) BackendError!void {
3098 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3099 state.render_count += 1;
3100 state.last_render_draw_count = request.pass.draws.len;
3101 const first = request.pass.draws[0].range;
3102 state.last_render_vertex_count = first.vertex_count;
3103 state.last_render_instance_count = first.instance_count;
3104 }
3105
3106 fn fakeCreateRenderBindings(ptr: *anyopaque, request: RenderBindingsRequest) BackendError!RenderBindings {
3107 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3108 defer state.next_id += 1;
3109 state.render_bindings_count += 1;
3110 return .{ .id = state.next_id, .backend = .cuda, .pipeline = request.pipeline.id };
3111 }
3112
3113 fn fakeRecordRenderBundle(ptr: *anyopaque, pass: RenderPass) BackendError!RenderBundle {
3114 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3115 defer state.next_id += 1;
3116 state.bundle_record_count += 1;
3117 return .{ .id = state.next_id, .backend = .cuda, .draw_count = @intCast(pass.draws.len) };
3118 }
3119
3120 fn fakeSubmitRenderBundle(ptr: *anyopaque, _: RenderBundleSubmit) BackendError!void {
3121 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3122 state.bundle_submit_count += 1;
3123 }
3124
3125 fn fakeWriteTexture(ptr: *anyopaque, _: TextureWriteRequest) BackendError!void {
3126 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3127 state.texture_write_count += 1;
3128 }
3129
3130 fn fakeReadTexture(ptr: *anyopaque, _: TextureReadRequest) BackendError!void {
3131 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3132 state.texture_read_count += 1;
3133 }
3134
3135 fn fakeCreateStream(ptr: *anyopaque, _: StreamAllocation) BackendError!StreamHandle {
3136 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3137 defer state.next_id += 1;
3138 state.created_stream = true;
3139 return .{
3140 .id = state.next_id,
3141 .backend = .cuda,
3142 };
3143 }
3144
3145 fn fakeCreateEvent(ptr: *anyopaque, _: EventAllocation) BackendError!EventHandle {
3146 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3147 defer state.next_id += 1;
3148 state.created_event = true;
3149 return .{
3150 .id = state.next_id,
3151 .backend = .cuda,
3152 };
3153 }
3154
3155 fn fakeQueryEvent(ptr: *anyopaque, request: EventQueryRequest) BackendError!bool {
3156 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3157 state.queried_event = request.event.id;
3158 return state.event_ready;
3159 }
3160
3161 fn fakeRecordEvent(ptr: *anyopaque, request: EventRecordRequest) BackendError!void {
3162 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3163 state.recorded_event = request.event.id;
3164 state.record_stream = request.stream.id;
3165 }
3166
3167 fn fakeElapsedEventNs(ptr: *anyopaque, request: EventElapsedRequest) BackendError!u64 {
3168 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3169 state.elapsed_start_event = request.start.id;
3170 state.elapsed_end_event = request.end.id;
3171 return state.elapsed_ns;
3172 }
3173
3174 fn fakeSynchronize(ptr: *anyopaque, request: SyncRequest) BackendError!void {
3175 const state: *FakeBackendState = @ptrCast(@alignCast(ptr));
3176 state.sync_count += 1;
3177 state.last_sync_request = request;
3178 }
3179
3180 const fake_vtable = BackendVTable{
3181 .query_capabilities = fakeQueryCapabilities,
3182 .launch = fakeLaunch,
3183 .allocate_buffer = fakeAllocateBuffer,
3184 .allocate_texture = fakeAllocateTexture,
3185 .create_surface = fakeCreateSurface,
3186 .destroy_surface = fakeDestroySurface,
3187 .destroy_texture = fakeDestroyTexture,
3188 .acquire_surface_frame = fakeAcquireSurfaceFrame,
3189 .present_surface_frame = fakePresentSurfaceFrame,
3190 .write_surface_frame = fakeWriteSurfaceFrame,
3191 .create_render_artifact = fakeCreateRenderArtifact,
3192 .load_render_artifact = fakeLoadRenderArtifact,
3193 .render = fakeRender,
3194 .create_render_bindings = fakeCreateRenderBindings,
3195 .record_render_bundle = fakeRecordRenderBundle,
3196 .submit_render_bundle = fakeSubmitRenderBundle,
3197 .write_texture = fakeWriteTexture,
3198 .read_texture = fakeReadTexture,
3199 .create_stream = fakeCreateStream,
3200 .create_event = fakeCreateEvent,
3201 .query_event = fakeQueryEvent,
3202 .record_event = fakeRecordEvent,
3203 .elapsed_event_ns = fakeElapsedEventNs,
3204 .synchronize = fakeSynchronize,
3205 };
3206
3207 test "capabilities validate buffer allocation requests" {
3208 const caps = BackendCapabilities{
3209 .identity = .{
3210 .backend = .cuda,
3211 .family = .nvidia_cuda,
3212 .name = "test-cuda-device",
3213 },
3214 .memory = .{
3215 .global_bytes = 256,
3216 .max_allocation_bytes = 128,
3217 .min_buffer_alignment = 64,
3218 },
3219 .dtypes = DTypeSet.init(&.{ .f32, .u32 }),
3220 };
3221
3222 try caps.validateBufferAllocation(.{
3223 .byte_size = 128,
3224 .alignment = 64,
3225 .dtype = .f32,
3226 .element_count = 32,
3227 });
3228 try std.testing.expectError(error.InvalidBuffer, caps.validateBufferAllocation(.{
3229 .byte_size = 0,
3230 .alignment = 64,
3231 }));
3232 try std.testing.expectError(error.InvalidBuffer, caps.validateBufferAllocation(.{
3233 .byte_size = 16,
3234 .alignment = 0,
3235 }));
3236 try std.testing.expectError(error.InvalidBuffer, caps.validateBufferAllocation(.{
3237 .byte_size = 16,
3238 .alignment = 96,
3239 }));
3240 try std.testing.expectError(error.CapabilityMismatch, caps.validateBufferAllocation(.{
3241 .byte_size = 16,
3242 .alignment = 32,
3243 }));
3244 try std.testing.expectError(error.CapabilityMismatch, caps.validateBufferAllocation(.{
3245 .byte_size = 129,
3246 .alignment = 64,
3247 }));
3248
3249 const smaller_global = BackendCapabilities{
3250 .identity = caps.identity,
3251 .memory = .{
3252 .global_bytes = 64,
3253 .min_buffer_alignment = 64,
3254 },
3255 .dtypes = DTypeSet.init(&.{.f32}),
3256 };
3257 try std.testing.expectError(error.CapabilityMismatch, smaller_global.validateBufferAllocation(.{
3258 .byte_size = 65,
3259 .alignment = 64,
3260 }));
3261 try std.testing.expectError(error.CapabilityMismatch, caps.validateBufferAllocation(.{
3262 .byte_size = 8,
3263 .alignment = 64,
3264 .dtype = .f64,
3265 .element_count = 1,
3266 }));
3267 try std.testing.expectError(error.InvalidBuffer, caps.validateBufferAllocation(.{
3268 .byte_size = 12,
3269 .alignment = 64,
3270 .dtype = .f32,
3271 .element_count = 4,
3272 }));
3273 }
3274
3275 test "allocateBuffer validates capabilities and returned handles" {
3276 var state = FakeBackendState{
3277 .max_allocation_bytes = 128,
3278 .min_buffer_alignment = 64,
3279 };
3280 const handle = BackendHandle{
3281 .ptr = &state,
3282 .vtable = &fake_vtable,
3283 .kind = .cuda,
3284 };
3285
3286 try std.testing.expectError(error.CapabilityMismatch, handle.allocateBuffer(.{
3287 .byte_size = 256,
3288 .alignment = 64,
3289 .dtype = .f32,
3290 .element_count = 64,
3291 }));
3292 try std.testing.expectEqual(@as(usize, 0), state.allocation_count);
3293
3294 try std.testing.expectError(error.CapabilityMismatch, handle.allocateBuffer(.{
3295 .byte_size = 16,
3296 .alignment = 32,
3297 .dtype = .f32,
3298 .element_count = 4,
3299 }));
3300 try std.testing.expectEqual(@as(usize, 0), state.allocation_count);
3301
3302 const buffer = try handle.allocateBuffer(.{
3303 .byte_size = 64,
3304 .alignment = 64,
3305 .dtype = .f32,
3306 .element_count = 16,
3307 });
3308 try std.testing.expectEqual(@as(BackendObjectId, 1), buffer.id);
3309 try std.testing.expectEqual(@as(usize, 1), state.allocation_count);
3310 try std.testing.expectEqual(@as(usize, 64), state.last_allocation.?.byte_size);
3311
3312 state.allocated_backend = .vulkan;
3313 try std.testing.expectError(error.InvalidBuffer, handle.allocateBuffer(.{
3314 .byte_size = 64,
3315 .alignment = 64,
3316 .dtype = .f32,
3317 .element_count = 16,
3318 }));
3319 try std.testing.expectEqual(@as(usize, 2), state.allocation_count);
3320
3321 state.allocated_backend = .cuda;
3322 state.allocated_byte_size = 32;
3323 try std.testing.expectError(error.InvalidBuffer, handle.allocateBuffer(.{
3324 .byte_size = 64,
3325 .alignment = 64,
3326 .dtype = .f32,
3327 .element_count = 16,
3328 }));
3329 try std.testing.expectEqual(@as(usize, 3), state.allocation_count);
3330 }
3331
3332 test "capabilities validate texture allocation and surface creation requests" {
3333 const caps = BackendCapabilities{
3334 .identity = .{
3335 .backend = .vulkan,
3336 .family = .vulkan,
3337 .name = "test-vulkan-device",
3338 },
3339 .textures = .{
3340 .supported = true,
3341 .formats = TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm }),
3342 .usages = .{ .copy_src = true, .copy_dst = true, .sampled = true, .storage = true, .present = true },
3343 .max_extent = .{ .width = 4096, .height = 4096, .depth = 4 },
3344 .max_sample_count = 4,
3345 },
3346 .surfaces = .{
3347 .supported = true,
3348 .platforms = SurfacePlatformSet.init(&.{ .x11, .headless }),
3349 .formats = TextureFormatSet.init(&.{.bgra8_unorm}),
3350 .color_spaces = ColorSpaceSet.init(&.{.srgb}),
3351 .present_modes = PresentModeSet.init(&.{.fifo}),
3352 .usages = .{ .copy_dst = true, .color_attachment = true, .present = true },
3353 .max_extent = .{ .width = 3840, .height = 2160 },
3354 .max_frames_in_flight = 2,
3355 },
3356 };
3357
3358 try caps.validateTextureAllocation(.{
3359 .extent = .{ .width = 64, .height = 64, .depth = 1 },
3360 .format = .rgba8_unorm,
3361 .usage = .{ .sampled = true, .copy_dst = true },
3362 });
3363 try std.testing.expectError(error.InvalidTexture, caps.validateTextureAllocation(.{
3364 .extent = .{ .width = 0, .height = 64 },
3365 .format = .rgba8_unorm,
3366 .usage = .{ .sampled = true },
3367 }));
3368 try std.testing.expectError(error.InvalidTexture, caps.validateTextureAllocation(.{
3369 .extent = .{ .width = 64, .height = 64 },
3370 .format = .rgba8_unorm,
3371 .usage = .{},
3372 }));
3373 try std.testing.expectError(error.CapabilityMismatch, caps.validateTextureAllocation(.{
3374 .extent = .{ .width = 64, .height = 64 },
3375 .format = .rgba8_srgb,
3376 .usage = .{ .sampled = true },
3377 }));
3378 try std.testing.expectError(error.CapabilityMismatch, caps.validateTextureAllocation(.{
3379 .extent = .{ .width = 64, .height = 64 },
3380 .format = .rgba8_unorm,
3381 .usage = .{ .color_attachment = true },
3382 }));
3383
3384 try caps.validateSurfaceCreation(.{
3385 .platform = .{ .x11 = .{ .display = 1, .window = 2 } },
3386 .extent = .{ .width = 800, .height = 600 },
3387 .format = .bgra8_unorm,
3388 .usage = .{ .present = true, .copy_dst = true },
3389 });
3390 try std.testing.expectError(error.InvalidSurface, caps.validateSurfaceCreation(.{
3391 .platform = .{ .x11 = .{ .display = 1, .window = 2 } },
3392 .extent = .{ .width = 800, .height = 0 },
3393 .format = .bgra8_unorm,
3394 .usage = .{ .present = true },
3395 }));
3396 try std.testing.expectError(error.InvalidSurface, caps.validateSurfaceCreation(.{
3397 .platform = .{ .x11 = .{ .display = 1, .window = 2 } },
3398 .extent = .{ .width = 800, .height = 600 },
3399 .format = .bgra8_unorm,
3400 .usage = .{ .copy_dst = true },
3401 }));
3402 try std.testing.expectError(error.CapabilityMismatch, caps.validateSurfaceCreation(.{
3403 .platform = .{ .cocoa = .{ .layer = 3 } },
3404 .extent = .{ .width = 800, .height = 600 },
3405 .format = .bgra8_unorm,
3406 .usage = .{ .present = true },
3407 }));
3408 try std.testing.expectError(error.CapabilityMismatch, caps.validateSurfaceCreation(.{
3409 .platform = .{ .x11 = .{ .display = 1, .window = 2 } },
3410 .extent = .{ .width = 800, .height = 600 },
3411 .format = .rgba8_unorm,
3412 .usage = .{ .present = true },
3413 }));
3414 }
3415
3416 test "surface and texture handle methods validate capabilities and backend ownership" {
3417 var state = FakeBackendState{};
3418 const handle = BackendHandle{
3419 .ptr = &state,
3420 .vtable = &fake_vtable,
3421 .kind = .cuda,
3422 };
3423
3424 const texture = try handle.allocateTexture(.{
3425 .extent = .{ .width = 128, .height = 64 },
3426 .format = .rgba8_unorm,
3427 .usage = .{ .sampled = true, .copy_dst = true },
3428 });
3429 try std.testing.expectEqual(@as(usize, 1), state.texture_allocate_count);
3430 try std.testing.expectEqual(BackendKind.cuda, texture.backend);
3431 try std.testing.expectEqual(TextureOwnership.backend, texture.ownership);
3432
3433 state.supports_textures = false;
3434 try std.testing.expectError(error.CapabilityMismatch, handle.allocateTexture(.{
3435 .extent = .{ .width = 128, .height = 64 },
3436 .format = .rgba8_unorm,
3437 .usage = .{ .sampled = true },
3438 }));
3439 try std.testing.expectEqual(@as(usize, 1), state.texture_allocate_count);
3440 state.supports_textures = true;
3441
3442 const surface = try handle.createSurface(.{
3443 .platform = .{ .headless = .{} },
3444 .extent = .{ .width = 320, .height = 180 },
3445 .format = .rgba8_unorm,
3446 .usage = .{ .present = true, .copy_dst = true },
3447 });
3448 try std.testing.expectEqual(@as(usize, 1), state.created_surface_count);
3449 try std.testing.expectEqual(SurfacePlatformKind.headless, surface.platform);
3450
3451 state.created_surface_extent = .{ .width = 640, .height = 360 };
3452 const actual_extent_surface = try handle.createSurface(.{
3453 .platform = .{ .headless = .{} },
3454 .extent = .{ .width = 320, .height = 180 },
3455 .format = .rgba8_unorm,
3456 .usage = .{ .present = true, .copy_dst = true },
3457 });
3458 try std.testing.expectEqual(@as(u32, 640), actual_extent_surface.extent.width);
3459 state.created_surface_extent = null;
3460
3461 const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
3462 try std.testing.expectEqual(@as(usize, 1), state.acquired_frame_count);
3463 try std.testing.expectEqual(surface.id, frame.surface.id);
3464 try std.testing.expectEqual(TextureOwnership.acquired_surface, frame.texture.ownership);
3465
3466 const pixels = try handle.allocateBuffer(.{
3467 .byte_size = @as(usize, frame.texture.extent.width) * @as(usize, frame.texture.extent.height) * 4,
3468 .alignment = 16,
3469 });
3470 const ops = [_]SurfaceFrameWriteOp{
3471 .{ .clear = .{ .r = 0.1, .g = 0.2, .b = 0.3, .a = 1.0 } },
3472 .{ .copy_buffer = pixels },
3473 };
3474 try handle.writeSurfaceFrame(.{
3475 .surface = surface,
3476 .frame = frame,
3477 .operations = &ops,
3478 });
3479 try std.testing.expectEqual(@as(usize, 1), state.surface_write_count);
3480 try std.testing.expectEqual(frame.id, state.last_surface_write_frame_id.?);
3481 try std.testing.expectEqual(@as(usize, 2), state.last_surface_write_op_count);
3482
3483 const small_buffer = BufferHandle{
3484 .id = 900,
3485 .backend = .cuda,
3486 .byte_size = 4,
3487 .ownership = .backend,
3488 };
3489 const small_ops = [_]SurfaceFrameWriteOp{.{ .copy_buffer = small_buffer }};
3490 try std.testing.expectError(error.InvalidBuffer, handle.writeSurfaceFrame(.{
3491 .surface = surface,
3492 .frame = frame,
3493 .operations = &small_ops,
3494 }));
3495 try std.testing.expectEqual(@as(usize, 1), state.surface_write_count);
3496
3497 var no_copy_frame = frame;
3498 no_copy_frame.texture.usage.copy_dst = false;
3499 try std.testing.expectError(error.InvalidTexture, handle.writeSurfaceFrame(.{
3500 .surface = surface,
3501 .frame = no_copy_frame,
3502 .operations = &ops,
3503 }));
3504 try std.testing.expectEqual(@as(usize, 1), state.surface_write_count);
3505
3506 var stale_surface = surface;
3507 stale_surface.generation += 1;
3508 try std.testing.expectError(error.SurfaceFrameExpired, handle.presentSurfaceFrame(.{
3509 .surface = stale_surface,
3510 .frame = frame,
3511 }));
3512 try std.testing.expectEqual(@as(usize, 0), state.present_count);
3513
3514 try handle.presentSurfaceFrame(.{
3515 .surface = surface,
3516 .frame = frame,
3517 });
3518 try std.testing.expectEqual(@as(usize, 1), state.present_count);
3519
3520 try handle.destroyTexture(texture);
3521 try std.testing.expectEqual(@as(usize, 1), state.destroyed_texture_count);
3522 try handle.destroySurface(surface);
3523 try std.testing.expectEqual(@as(usize, 1), state.destroyed_surface_count);
3524
3525 state.allocated_texture_backend = .vulkan;
3526 try std.testing.expectError(error.InvalidTexture, handle.allocateTexture(.{
3527 .extent = .{ .width = 128, .height = 64 },
3528 .format = .rgba8_unorm,
3529 .usage = .{ .sampled = true },
3530 }));
3531 state.allocated_texture_backend = .cuda;
3532
3533 state.created_surface_backend = .vulkan;
3534 try std.testing.expectError(error.InvalidSurface, handle.createSurface(.{
3535 .platform = .{ .headless = .{} },
3536 .extent = .{ .width = 320, .height = 180 },
3537 .format = .rgba8_unorm,
3538 .usage = .{ .present = true },
3539 }));
3540 }
3541
3542 test "capabilities validate render pipeline descriptors and requests" {
3543 const caps = BackendCapabilities{
3544 .identity = .{
3545 .backend = .vulkan,
3546 .family = .vulkan,
3547 .name = "test-vulkan-device",
3548 },
3549 .raster = .{
3550 .supported = true,
3551 .artifact_formats = RenderArtifactFormatSet.init(&.{.vulkan_spirv}),
3552 .target_formats = TextureFormatSet.init(&.{.rgba8_unorm}),
3553 .blend_modes = RenderBlendModeSet.init(&.{ .replace, .alpha_premultiplied }),
3554 .topologies = RenderPrimitiveTopologySet.init(&.{.triangle_list}),
3555 .vertex_formats = RenderVertexFormatSet.init(&.{ .float32x2, .float32x4 }),
3556 .binding_kinds = RenderBindingKindSet.init(&.{ .uniform_buffer, .sampled_texture }),
3557 .index_formats = RenderIndexFormatSet.init(&.{ .none, .u16 }),
3558 .max_vertex_buffers = 2,
3559 .max_vertex_attributes = 4,
3560 .max_bindings = 4,
3561 .instancing = true,
3562 },
3563 };
3564 const attributes = [_]RenderVertexAttribute{
3565 .{ .location = 0, .format = .float32x2, .offset = 0 },
3566 .{ .location = 1, .format = .float32x4, .offset = 8 },
3567 };
3568 const layouts = [_]RenderVertexBufferLayout{.{
3569 .binding = 0,
3570 .stride = 24,
3571 .step_mode = .instance,
3572 .attribute_start = 0,
3573 .attribute_count = attributes.len,
3574 }};
3575 const bindings = [_]RenderBindingDesc{.{
3576 .group = 0,
3577 .binding = 0,
3578 .kind = .uniform_buffer,
3579 .access = .read_only,
3580 }};
3581 const desc = RenderPipelineDesc{
3582 .format = .vulkan_spirv,
3583 .vertex_entry_name = "quad_vs",
3584 .fragment_entry_name = "quad_fs",
3585 .target_format = .rgba8_unorm,
3586 .push_extent = 0,
3587 .blend_mode = .alpha_premultiplied,
3588 .topology = .triangle_list,
3589 .vertex_layouts = layouts[0..],
3590 .vertex_attributes = attributes[0..],
3591 .bindings = bindings[0..],
3592 };
3593
3594 try caps.validateRenderPipelineDesc(desc);
3595
3596 var empty_entry = desc;
3597 empty_entry.fragment_entry_name = "";
3598 try std.testing.expectError(error.InvalidRenderArtifact, caps.validateRenderPipelineDesc(empty_entry));
3599
3600 var bad_format = desc;
3601 bad_format.format = .webgpu_wgsl;
3602 try std.testing.expectError(error.CapabilityMismatch, caps.validateRenderPipelineDesc(bad_format));
3603
3604 const bad_attributes = [_]RenderVertexAttribute{
3605 .{ .location = 0, .format = .float32x4, .offset = 16 },
3606 };
3607 const bad_layouts = [_]RenderVertexBufferLayout{.{
3608 .binding = 0,
3609 .stride = 24,
3610 .attribute_start = 0,
3611 .attribute_count = bad_attributes.len,
3612 }};
3613 var bad_offset = desc;
3614 bad_offset.vertex_layouts = bad_layouts[0..];
3615 bad_offset.vertex_attributes = bad_attributes[0..];
3616 try std.testing.expectError(error.InvalidRenderArtifact, caps.validateRenderPipelineDesc(bad_offset));
3617
3618 var color_as_depth = desc;
3619 color_as_depth.depth = .{ .format = .rgba8_unorm };
3620 try std.testing.expectError(error.InvalidRenderArtifact, caps.validateRenderPipelineDesc(color_as_depth));
3621 var unsupported_depth = desc;
3622 unsupported_depth.depth = .{};
3623 try std.testing.expectError(error.CapabilityMismatch, caps.validateRenderPipelineDesc(unsupported_depth));
3624
3625 var depth_caps = caps;
3626 depth_caps.raster.depth_formats = TextureFormatSet.init(&.{.depth32_float});
3627 try depth_caps.validateRenderPipelineDesc(unsupported_depth);
3628
3629 var artifact = try RenderArtifact.init(std.testing.allocator, .{
3630 .backend = .vulkan,
3631 .pipeline = desc,
3632 });
3633 defer artifact.deinit();
3634 const pipeline = LoadedRenderArtifact.describing(&artifact, 7);
3635 const target = TextureView{
3636 .texture = .{
3637 .id = 1,
3638 .backend = .vulkan,
3639 .extent = .{ .width = 320, .height = 180, .depth = 1 },
3640 .format = .rgba8_unorm,
3641 .usage = .{ .color_attachment = true, .present = true },
3642 },
3643 .format = .rgba8_unorm,
3644 };
3645 const vertex_buffer = RenderBufferRange{ .buffer = .{
3646 .id = 2,
3647 .backend = .vulkan,
3648 .byte_size = 240,
3649 .ownership = .backend,
3650 } };
3651 const pipeline_bindings = RenderBindings{ .id = 3, .backend = .vulkan, .pipeline = pipeline.id };
3652 const draw = RenderDraw{
3653 .pipeline = pipeline,
3654 .bindings = pipeline_bindings,
3655 .vertex_buffers = &.{vertex_buffer},
3656 .range = .{ .vertex_count = 6, .instance_count = 2 },
3657 };
3658 const pass = RenderPass{
3659 .color = .{ .view = target, .load = .{ .clear = .{} } },
3660 .viewport = .{ .width = 320, .height = 180 },
3661 .scissor = .{ .width = 320, .height = 180 },
3662 .draws = &.{draw},
3663 };
3664 try caps.validateRenderPass(pass);
3665
3666 var non_attachment = pass;
3667 non_attachment.color.view.texture.usage = .{ .present = true };
3668 try std.testing.expectError(error.InvalidTexture, caps.validateRenderPass(non_attachment));
3669
3670 var mismatched_texture = pass;
3671 mismatched_texture.color.view.texture.format = .bgra8_unorm;
3672 try std.testing.expectError(error.InvalidTexture, caps.validateRenderPass(mismatched_texture));
3673
3674 var outside = pass;
3675 outside.scissor = .{ .x = 1, .width = 320, .height = 180 };
3676 try std.testing.expectError(error.RenderArgumentMismatch, caps.validateRenderPass(outside));
3677
3678 const bad_draws = [_]RenderDraw{
3679 .{ .pipeline = pipeline, .bindings = pipeline_bindings, .range = .{ .vertex_count = 6 } },
3680 .{ .pipeline = pipeline, .vertex_buffers = &.{vertex_buffer}, .range = .{ .vertex_count = 6 } },
3681 .{
3682 .pipeline = pipeline,
3683 .bindings = pipeline_bindings,
3684 .vertex_buffers = &.{vertex_buffer},
3685 .range = .{ .index_count = 6, .index_format = .u16 },
3686 },
3687 .{
3688 .pipeline = pipeline,
3689 .bindings = .{ .id = 3, .backend = .vulkan, .pipeline = 8 },
3690 .vertex_buffers = &.{vertex_buffer},
3691 .range = .{ .vertex_count = 6 },
3692 },
3693 };
3694 for (bad_draws) |bad| {
3695 var one = pass;
3696 one.draws = &.{bad};
3697 try std.testing.expectError(error.RenderArgumentMismatch, caps.validateRenderPass(one));
3698 }
3699
3700 var depth_artifact = try RenderArtifact.init(std.testing.allocator, .{
3701 .backend = .vulkan,
3702 .pipeline = unsupported_depth,
3703 });
3704 defer depth_artifact.deinit();
3705 var depth_draw = draw;
3706 depth_draw.pipeline = LoadedRenderArtifact.describing(&depth_artifact, 7);
3707 var no_depth_attachment = pass;
3708 no_depth_attachment.draws = &.{depth_draw};
3709 try std.testing.expectError(error.RenderArgumentMismatch, depth_caps.validateRenderPass(no_depth_attachment));
3710 var with_depth = no_depth_attachment;
3711 with_depth.depth = .{ .view = .{
3712 .texture = .{
3713 .id = 4,
3714 .backend = .vulkan,
3715 .extent = target.texture.extent,
3716 .format = .depth32_float,
3717 .usage = .{ .depth_attachment = true },
3718 },
3719 .format = .depth32_float,
3720 } };
3721 try depth_caps.validateRenderPass(with_depth);
3722 var depth_without_pipeline_depth = with_depth;
3723 depth_without_pipeline_depth.draws = &.{draw};
3724 try std.testing.expectError(error.RenderArgumentMismatch, depth_caps.validateRenderPass(depth_without_pipeline_depth));
3725 var far_clear = with_depth;
3726 far_clear.depth.?.load = .{ .clear = 2 };
3727 try std.testing.expectError(error.RenderArgumentMismatch, depth_caps.validateRenderPass(far_clear));
3728 }
3729
3730 test "a render module reading push constants past the declared bytes is refused before the backend creates or loads it" {
3731 var state = FakeBackendState{};
3732 const handle = BackendHandle{
3733 .ptr = &state,
3734 .vtable = &fake_vtable,
3735 .kind = .cuda,
3736 };
3737 const desc = RenderPipelineDesc{
3738 .format = .external,
3739 .vertex_entry_name = "quad_vs",
3740 .fragment_entry_name = "quad_fs",
3741 .target_format = .rgba8_unorm,
3742 .push_extent = 4,
3743 };
3744 try std.testing.expectError(error.PushConstantRangeExceeded, handle.createRenderArtifact(desc));
3745 try std.testing.expectEqual(@as(usize, 0), state.render_create_count);
3746
3747 var honest = desc;
3748 honest.push_extent = 0;
3749 var artifact = try handle.createRenderArtifact(honest);
3750 defer artifact.deinit();
3751 try std.testing.expectEqual(@as(usize, 1), state.render_create_count);
3752 artifact.push_extent = 4;
3753 try std.testing.expectError(error.PushConstantRangeExceeded, handle.loadRenderArtifact(&artifact));
3754 try std.testing.expectEqual(@as(usize, 0), state.render_load_count);
3755 }
3756
3757 test "render handle methods validate capabilities and backend ownership" {
3758 var state = FakeBackendState{};
3759 const handle = BackendHandle{
3760 .ptr = &state,
3761 .vtable = &fake_vtable,
3762 .kind = .cuda,
3763 };
3764 const attributes = [_]RenderVertexAttribute{.{
3765 .location = 0,
3766 .format = .float32x2,
3767 .offset = 0,
3768 }};
3769 const layouts = [_]RenderVertexBufferLayout{.{
3770 .binding = 0,
3771 .stride = 8,
3772 .attribute_start = 0,
3773 .attribute_count = attributes.len,
3774 }};
3775 var artifact = try handle.createRenderArtifact(.{
3776 .format = .external,
3777 .vertex_entry_name = "quad_vs",
3778 .fragment_entry_name = "quad_fs",
3779 .target_format = .rgba8_unorm,
3780 .push_extent = 0,
3781 .vertex_layouts = layouts[0..],
3782 .vertex_attributes = attributes[0..],
3783 });
3784 defer artifact.deinit();
3785 try std.testing.expectEqual(@as(usize, 1), state.render_create_count);
3786
3787 const loaded = try handle.loadRenderArtifact(&artifact);
3788 try std.testing.expectEqual(@as(usize, 1), state.render_load_count);
3789
3790 const target_texture = try handle.allocateTexture(.{
3791 .extent = .{ .width = 128, .height = 64, .depth = 1 },
3792 .format = .rgba8_unorm,
3793 .usage = .{ .color_attachment = true, .present = true },
3794 });
3795 const vertex_buffer = try handle.allocateBuffer(.{ .byte_size = 64 });
3796 const draws = [_]RenderDraw{
3797 .{
3798 .pipeline = loaded,
3799 .vertex_buffers = &.{.{ .buffer = vertex_buffer }},
3800 .range = .{ .vertex_count = 6, .instance_count = 4 },
3801 },
3802 .{
3803 .pipeline = loaded,
3804 .vertex_buffers = &.{.{ .buffer = vertex_buffer }},
3805 .range = .{ .vertex_count = 3 },
3806 },
3807 };
3808 const pass = RenderPass{
3809 .color = .{ .view = .{ .texture = target_texture, .format = .rgba8_unorm } },
3810 .viewport = .{ .width = 128, .height = 64 },
3811 .scissor = .{ .width = 128, .height = 64 },
3812 .draws = draws[0..],
3813 };
3814 try handle.render(.{ .pass = pass });
3815 try std.testing.expectEqual(@as(usize, 1), state.render_count);
3816 try std.testing.expectEqual(@as(usize, 2), state.last_render_draw_count);
3817 try std.testing.expectEqual(@as(u32, 6), state.last_render_vertex_count);
3818 try std.testing.expectEqual(@as(u32, 4), state.last_render_instance_count);
3819
3820 const bundle = try handle.recordRenderBundle(pass);
3821 try std.testing.expectEqual(@as(u32, 2), bundle.draw_count);
3822 try handle.submitRenderBundle(.{ .bundle = bundle });
3823 try std.testing.expectEqual(@as(usize, 1), state.bundle_submit_count);
3824
3825 var texels: [128 * 64 * 4]u8 = undefined;
3826 try std.testing.expectError(error.InvalidTexture, handle.readTexture(.{ .texture = target_texture, .bytes = &texels }));
3827 const readable = try handle.allocateTexture(.{
3828 .extent = .{ .width = 128, .height = 64, .depth = 1 },
3829 .format = .rgba8_unorm,
3830 .usage = .{ .color_attachment = true, .copy_src = true, .copy_dst = true },
3831 });
3832 try handle.readTexture(.{ .texture = readable, .bytes = &texels });
3833 try std.testing.expectError(error.InvalidTexture, handle.writeTexture(.{ .texture = readable, .bytes = texels[1..] }));
3834 try handle.writeTexture(.{ .texture = readable, .bytes = &texels });
3835 try std.testing.expectEqual(@as(usize, 1), state.texture_read_count);
3836 try std.testing.expectEqual(@as(usize, 1), state.texture_write_count);
3837
3838 state.supports_raster = false;
3839 try std.testing.expectError(error.CapabilityMismatch, handle.createRenderArtifact(.{
3840 .format = .external,
3841 .vertex_entry_name = "quad_vs",
3842 .fragment_entry_name = "quad_fs",
3843 .target_format = .rgba8_unorm,
3844 .push_extent = 0,
3845 .vertex_layouts = layouts[0..],
3846 .vertex_attributes = attributes[0..],
3847 }));
3848 try std.testing.expectEqual(@as(usize, 1), state.render_create_count);
3849
3850 state.supports_raster = true;
3851 state.created_render_backend = .vulkan;
3852 try std.testing.expectError(error.CapabilityMismatch, handle.createRenderArtifact(.{
3853 .format = .external,
3854 .vertex_entry_name = "quad_vs",
3855 .fragment_entry_name = "quad_fs",
3856 .target_format = .rgba8_unorm,
3857 .push_extent = 0,
3858 .vertex_layouts = layouts[0..],
3859 .vertex_attributes = attributes[0..],
3860 }));
3861 try std.testing.expectEqual(@as(usize, 2), state.render_create_count);
3862 state.created_render_backend = .cuda;
3863
3864 state.loaded_render_backend = .vulkan;
3865 try std.testing.expectError(error.InvalidRenderArtifact, handle.loadRenderArtifact(&artifact));
3866 try std.testing.expectEqual(@as(usize, 2), state.render_load_count);
3867 }
3868
3869 test "createArtifact validates requested capabilities before dispatch" {
3870 var state = FakeBackendState{};
3871 const backend = BackendHandle{
3872 .ptr = &state,
3873 .vtable = &fake_vtable,
3874 .kind = .cuda,
3875 };
3876
3877 try std.testing.expectError(error.UnsupportedOperation, backend.createArtifact(.{
3878 .kernel_name = "add",
3879 .requested_format = .cuda_ptx,
3880 .required_dtypes = DTypeSet.init(&.{.f32}),
3881 }));
3882 try std.testing.expectError(error.UnsupportedArtifactFormat, backend.createArtifact(.{
3883 .kernel_name = "add",
3884 .requested_format = .vulkan_spirv,
3885 }));
3886 try std.testing.expectError(error.CapabilityMismatch, backend.createArtifact(.{
3887 .kernel_name = "add",
3888 .requested_format = .cuda_ptx,
3889 .required_dtypes = DTypeSet.init(&.{.f64}),
3890 }));
3891 try std.testing.expectError(error.CapabilityMismatch, backend.createArtifact(.{
3892 .kernel_name = "add",
3893 .requested_format = .cuda_ptx,
3894 .required_features = .{ .async_copy = true },
3895 }));
3896 try std.testing.expectError(error.CapabilityMismatch, backend.createArtifact(.{
3897 .kernel_name = "add",
3898 .requested_format = .cuda_ptx,
3899 .required_subgroup = .{ .scan = true },
3900 }));
3901 }
3902
3903 test "launch request makes buffer ownership explicit" {
3904 var state = FakeBackendState{};
3905 const backend = BackendHandle{
3906 .ptr = &state,
3907 .vtable = &fake_vtable,
3908 .kind = .cuda,
3909 };
3910
3911 try std.testing.expectEqual(BackendKind.cuda, backend.backendKind().?);
3912
3913 const caps = try backend.queryCapabilities();
3914 try std.testing.expect(caps.supportsDType(.f32));
3915
3916 try std.testing.expectError(error.UnsupportedOperation, backend.writeBuffer(.{
3917 .handle = .{
3918 .id = 42,
3919 .backend = .cuda,
3920 .byte_size = 4096,
3921 .ownership = .backend,
3922 },
3923 .bytes = &.{},
3924 }));
3925
3926 var artifact = try KernelArtifact.init(std.testing.allocator, .{
3927 .backend = .cuda,
3928 .format = .cuda_ptx,
3929 .entry_name = "add_f32",
3930 .argument_count = 1,
3931 });
3932 defer artifact.deinit();
3933 artifact.setBorrowedText("// ptx");
3934
3935 const binding = BufferBinding{
3936 .handle = .{
3937 .id = 42,
3938 .backend = .cuda,
3939 .byte_size = 4096,
3940 .ownership = .backend,
3941 },
3942 .access = .read_write,
3943 .ownership = .backend,
3944 .byte_size = 4096,
3945 };
3946
3947 try backend.launch(.{
3948 .artifact = &artifact,
3949 .buffers = &.{binding},
3950 .geometry = .{
3951 .grid = .{ 16, 1, 1 },
3952 .threadgroup = .{ 64, 1, 1 },
3953 },
3954 });
3955 try std.testing.expect(state.launched);
3956 try std.testing.expectEqual(BufferOwnership.backend, state.last_buffer_ownership.?);
3957
3958 state.launched = false;
3959 try std.testing.expectError(error.LaunchArgumentMismatch, backend.launch(.{
3960 .artifact = &artifact,
3961 .buffers = &.{},
3962 .geometry = .{},
3963 }));
3964 try std.testing.expect(!state.launched);
3965 }
3966
3967 test "launch validates geometry before backend dispatch" {
3968 var state = FakeBackendState{};
3969 const backend = BackendHandle{
3970 .ptr = &state,
3971 .vtable = &fake_vtable,
3972 .kind = .cuda,
3973 };
3974
3975 var artifact = try KernelArtifact.init(std.testing.allocator, .{
3976 .backend = .cuda,
3977 .format = .cuda_ptx,
3978 .entry_name = "add_f32",
3979 .argument_count = 1,
3980 });
3981 defer artifact.deinit();
3982 artifact.setBorrowedText("// ptx");
3983
3984 const binding = BufferBinding{
3985 .handle = .{
3986 .id = 42,
3987 .backend = .cuda,
3988 .byte_size = 4096,
3989 .ownership = .backend,
3990 },
3991 .access = .read_write,
3992 .ownership = .backend,
3993 .byte_size = 4096,
3994 };
3995
3996 try std.testing.expectError(error.LaunchArgumentMismatch, backend.launch(.{
3997 .artifact = &artifact,
3998 .buffers = &.{binding},
3999 .geometry = .{ .grid = .{ 0, 1, 1 }, .threadgroup = .{ 1, 1, 1 } },
4000 }));
4001 try std.testing.expectError(error.LaunchArgumentMismatch, backend.launch(.{
4002 .artifact = &artifact,
4003 .buffers = &.{binding},
4004 .geometry = .{ .grid = .{ 1, 1, 1 }, .threadgroup = .{ 0, 1, 1 } },
4005 }));
4006 try std.testing.expectError(error.CapabilityMismatch, backend.launch(.{
4007 .artifact = &artifact,
4008 .buffers = &.{binding},
4009 .geometry = .{ .grid = .{ 1, 1, 1 }, .threadgroup = .{ 512, 1, 1 } },
4010 }));
4011 try std.testing.expectError(error.CapabilityMismatch, backend.launch(.{
4012 .artifact = &artifact,
4013 .buffers = &.{binding},
4014 .geometry = .{ .grid = .{ 1, 1, 65 }, .threadgroup = .{ 1, 1, 1 } },
4015 }));
4016 try std.testing.expectError(error.CapabilityMismatch, backend.launch(.{
4017 .artifact = &artifact,
4018 .buffers = &.{binding},
4019 .geometry = .{
4020 .grid = .{ 1, 1, 1 },
4021 .threadgroup = .{ 1, 1, 1 },
4022 .dynamic_shared_memory_bytes = 64 * 1024,
4023 },
4024 }));
4025 try std.testing.expect(!state.launched);
4026 }
4027
4028 test "backend handle rejects cross-backend objects before dispatch" {
4029 var state = FakeBackendState{};
4030 const backend = BackendHandle{
4031 .ptr = &state,
4032 .vtable = &fake_vtable,
4033 .kind = .cuda,
4034 };
4035
4036 var artifact = try KernelArtifact.init(std.testing.allocator, .{
4037 .backend = .cuda,
4038 .format = .cuda_ptx,
4039 .entry_name = "add_f32",
4040 .argument_count = 1,
4041 });
4042 defer artifact.deinit();
4043 artifact.setBorrowedText("// ptx");
4044
4045 var no_arg_artifact = try KernelArtifact.init(std.testing.allocator, .{
4046 .backend = .cuda,
4047 .format = .cuda_ptx,
4048 .entry_name = "noop",
4049 .argument_count = 0,
4050 });
4051 defer no_arg_artifact.deinit();
4052 no_arg_artifact.setBorrowedText("// ptx");
4053
4054 var foreign_artifact = try KernelArtifact.init(std.testing.allocator, .{
4055 .backend = .vulkan,
4056 .format = .vulkan_spirv,
4057 .entry_name = "main",
4058 .argument_count = 0,
4059 });
4060 defer foreign_artifact.deinit();
4061 foreign_artifact.setBorrowedWords(&.{0x07230203});
4062
4063 try std.testing.expectError(error.CapabilityMismatch, backend.loadArtifact(&foreign_artifact));
4064
4065 const cuda_buffer = BufferHandle{
4066 .id = 42,
4067 .backend = .cuda,
4068 .byte_size = 4096,
4069 .ownership = .backend,
4070 };
4071 const foreign_buffer = BufferHandle{
4072 .id = 43,
4073 .backend = .vulkan,
4074 .byte_size = 4096,
4075 .ownership = .backend,
4076 };
4077 const cuda_stream = StreamHandle{ .id = 11, .backend = .cuda };
4078 const foreign_stream = StreamHandle{ .id = 12, .backend = .vulkan };
4079 const cuda_event = EventHandle{ .id = 21, .backend = .cuda };
4080 const foreign_event = EventHandle{ .id = 22, .backend = .vulkan };
4081
4082 try std.testing.expectError(error.InvalidBuffer, backend.writeBuffer(.{
4083 .handle = foreign_buffer,
4084 .bytes = &.{},
4085 }));
4086 try std.testing.expectError(error.InvalidBuffer, backend.readBuffer(.{
4087 .handle = foreign_buffer,
4088 .bytes = &.{},
4089 }));
4090 try std.testing.expectError(error.ReadBufferDestinationTooSmall, backend.readBuffer(.{
4091 .handle = cuda_buffer,
4092 .bytes = &.{},
4093 }));
4094 try std.testing.expectError(error.InvalidArtifact, backend.launch(.{
4095 .artifact = &artifact,
4096 .loaded_artifact = .{ .id = 1, .backend = .vulkan, .format = .cuda_ptx },
4097 .buffers = &.{.{ .handle = cuda_buffer, .access = .read_write, .ownership = .backend, .byte_size = 4096 }},
4098 .geometry = .{},
4099 }));
4100 try std.testing.expectError(error.InvalidArtifact, backend.launch(.{
4101 .artifact = &artifact,
4102 .loaded_artifact = .{ .id = 1, .backend = .cuda, .format = .cuda_cubin },
4103 .buffers = &.{.{ .handle = cuda_buffer, .access = .read_write, .ownership = .backend, .byte_size = 4096 }},
4104 .geometry = .{},
4105 }));
4106 try std.testing.expectError(error.InvalidBuffer, backend.launch(.{
4107 .artifact = &artifact,
4108 .buffers = &.{.{ .handle = foreign_buffer, .access = .read_write, .ownership = .backend, .byte_size = 4096 }},
4109 .geometry = .{},
4110 }));
4111 try std.testing.expectError(error.InvalidStream, backend.launch(.{
4112 .artifact = &no_arg_artifact,
4113 .buffers = &.{},
4114 .geometry = .{},
4115 .stream = foreign_stream,
4116 }));
4117 try std.testing.expectError(error.InvalidEvent, backend.launch(.{
4118 .artifact = &no_arg_artifact,
4119 .buffers = &.{},
4120 .geometry = .{},
4121 .wait_events = &.{foreign_event},
4122 }));
4123 try std.testing.expectError(error.InvalidEvent, backend.launch(.{
4124 .artifact = &no_arg_artifact,
4125 .buffers = &.{},
4126 .geometry = .{},
4127 .signal_event = foreign_event,
4128 }));
4129
4130 try std.testing.expectError(error.InvalidStream, backend.synchronize(.{
4131 .scope = .stream,
4132 .stream = foreign_stream,
4133 }));
4134 try std.testing.expectError(error.InvalidEvent, backend.synchronize(.{
4135 .scope = .event,
4136 .event = foreign_event,
4137 }));
4138 try std.testing.expectError(error.InvalidEvent, backend.queryEvent(.{ .event = foreign_event }));
4139 try std.testing.expectError(error.InvalidEvent, backend.elapsedEventNs(.{
4140 .start = foreign_event,
4141 .end = cuda_event,
4142 }));
4143 try std.testing.expectError(error.InvalidEvent, backend.elapsedEventNs(.{
4144 .start = cuda_event,
4145 .end = foreign_event,
4146 }));
4147 try std.testing.expectError(error.InvalidStream, backend.recordEvent(.{
4148 .stream = foreign_stream,
4149 .event = cuda_event,
4150 }));
4151 try std.testing.expectError(error.InvalidEvent, backend.recordEvent(.{
4152 .stream = cuda_stream,
4153 .event = foreign_event,
4154 }));
4155 try std.testing.expect(!state.launched);
4156 }
4157
4158 test "event query request is nonblocking and optional" {
4159 var state = FakeBackendState{};
4160 const backend = BackendHandle{
4161 .ptr = &state,
4162 .vtable = &fake_vtable,
4163 .kind = .cuda,
4164 };
4165
4166 const event = EventHandle{
4167 .id = 7,
4168 .backend = .cuda,
4169 };
4170
4171 try std.testing.expect(!try backend.queryEvent(.{ .event = event }));
4172 try std.testing.expectEqual(@as(BackendObjectId, 7), state.queried_event.?);
4173
4174 state.event_ready = true;
4175 try std.testing.expect(try backend.queryEvent(.{ .event = event }));
4176
4177 const unsupported_vtable = BackendVTable{
4178 .query_capabilities = fakeQueryCapabilities,
4179 };
4180 const unsupported = BackendHandle{
4181 .ptr = &state,
4182 .vtable = &unsupported_vtable,
4183 .kind = .cuda,
4184 };
4185 try std.testing.expectError(error.UnsupportedOperation, unsupported.queryEvent(.{ .event = event }));
4186 }
4187
4188 test "event elapsed request returns backend nanoseconds and is optional" {
4189 var state = FakeBackendState{
4190 .elapsed_ns = 42_000,
4191 };
4192 const backend = BackendHandle{
4193 .ptr = &state,
4194 .vtable = &fake_vtable,
4195 .kind = .cuda,
4196 };
4197
4198 const start = EventHandle{
4199 .id = 7,
4200 .backend = .cuda,
4201 };
4202 const end = EventHandle{
4203 .id = 8,
4204 .backend = .cuda,
4205 };
4206
4207 try std.testing.expectEqual(@as(u64, 42_000), try backend.elapsedEventNs(.{
4208 .start = start,
4209 .end = end,
4210 }));
4211 try std.testing.expectEqual(@as(BackendObjectId, 7), state.elapsed_start_event.?);
4212 try std.testing.expectEqual(@as(BackendObjectId, 8), state.elapsed_end_event.?);
4213
4214 const unsupported_vtable = BackendVTable{
4215 .query_capabilities = fakeQueryCapabilities,
4216 };
4217 const unsupported = BackendHandle{
4218 .ptr = &state,
4219 .vtable = &unsupported_vtable,
4220 .kind = .cuda,
4221 };
4222 try std.testing.expectError(error.UnsupportedOperation, unsupported.elapsedEventNs(.{
4223 .start = start,
4224 .end = end,
4225 }));
4226 }
4227
4228 test "synchronization scopes have one explicit valid handle shape" {
4229 var state = FakeBackendState{};
4230 const handle = BackendHandle{
4231 .ptr = &state,
4232 .vtable = &fake_vtable,
4233 .kind = .cuda,
4234 };
4235 const stream = StreamHandle{ .id = 11, .backend = .cuda };
4236 const event = EventHandle{ .id = 21, .backend = .cuda };
4237
4238 try handle.synchronize(.{ .scope = .default_stream });
4239 try std.testing.expectEqual(SyncScope.default_stream, state.last_sync_request.?.scope);
4240 try handle.synchronize(.{ .scope = .device });
4241 try std.testing.expectEqual(SyncScope.device, state.last_sync_request.?.scope);
4242 try handle.synchronize(.{ .scope = .stream, .stream = stream });
4243 try std.testing.expectEqual(stream.id, state.last_sync_request.?.stream.?.id);
4244 try handle.synchronize(.{ .scope = .event, .event = event });
4245 try std.testing.expectEqual(event.id, state.last_sync_request.?.event.?.id);
4246 try std.testing.expectEqual(@as(usize, 4), state.sync_count);
4247
4248 const invalid = [_]SyncRequest{
4249 .{ .scope = .default_stream, .stream = stream },
4250 .{ .scope = .device, .event = event },
4251 .{ .scope = .stream },
4252 .{ .scope = .stream, .stream = stream, .event = event },
4253 .{ .scope = .event },
4254 .{ .scope = .event, .stream = stream, .event = event },
4255 };
4256 for (invalid) |request| {
4257 try std.testing.expectError(error.UnsupportedOperation, handle.synchronize(request));
4258 }
4259 try std.testing.expectEqual(@as(usize, 4), state.sync_count);
4260 }
4261
4262 test "stream and event creation requests are optional backend objects" {
4263 var state = FakeBackendState{};
4264 const backend = BackendHandle{
4265 .ptr = &state,
4266 .vtable = &fake_vtable,
4267 .kind = .cuda,
4268 };
4269
4270 const stream = try backend.createStream(.{});
4271 try std.testing.expect(state.created_stream);
4272 try std.testing.expectEqual(BackendKind.cuda, stream.backend);
4273 try std.testing.expectEqual(@as(BackendObjectId, 1), stream.id);
4274
4275 const event = try backend.createEvent(.{});
4276 try std.testing.expect(state.created_event);
4277 try std.testing.expectEqual(BackendKind.cuda, event.backend);
4278 try std.testing.expectEqual(@as(BackendObjectId, 2), event.id);
4279
4280 const unsupported_vtable = BackendVTable{
4281 .query_capabilities = fakeQueryCapabilities,
4282 };
4283 const unsupported = BackendHandle{
4284 .ptr = &state,
4285 .vtable = &unsupported_vtable,
4286 .kind = .cuda,
4287 };
4288 try std.testing.expectError(error.UnsupportedOperation, unsupported.createStream(.{}));
4289 try std.testing.expectError(error.UnsupportedOperation, unsupported.createEvent(.{}));
4290 }
4291
4292 test "runtime capabilities gate stream and event operations before dispatch" {
4293 var state = FakeBackendState{
4294 .supports_streams = false,
4295 .supports_events = false,
4296 };
4297 const backend = BackendHandle{
4298 .ptr = &state,
4299 .vtable = &fake_vtable,
4300 .kind = .cuda,
4301 };
4302
4303 var artifact = try KernelArtifact.init(std.testing.allocator, .{
4304 .backend = .cuda,
4305 .format = .cuda_ptx,
4306 .entry_name = "noop",
4307 .argument_count = 0,
4308 });
4309 defer artifact.deinit();
4310 artifact.setBorrowedText("// ptx");
4311
4312 const stream = StreamHandle{ .id = 11, .backend = .cuda };
4313 const event = EventHandle{ .id = 21, .backend = .cuda };
4314
4315 try std.testing.expectError(error.CapabilityMismatch, backend.createStream(.{}));
4316 try std.testing.expect(!state.created_stream);
4317
4318 try std.testing.expectError(error.CapabilityMismatch, backend.createEvent(.{}));
4319 try std.testing.expect(!state.created_event);
4320
4321 try std.testing.expectError(error.CapabilityMismatch, backend.launch(.{
4322 .artifact = &artifact,
4323 .buffers = &.{},
4324 .geometry = .{},
4325 .stream = stream,
4326 }));
4327 try std.testing.expectError(error.CapabilityMismatch, backend.launch(.{
4328 .artifact = &artifact,
4329 .buffers = &.{},
4330 .geometry = .{},
4331 .wait_events = &.{event},
4332 }));
4333 try std.testing.expectError(error.CapabilityMismatch, backend.launch(.{
4334 .artifact = &artifact,
4335 .buffers = &.{},
4336 .geometry = .{},
4337 .signal_event = event,
4338 }));
4339 try std.testing.expect(!state.launched);
4340
4341 try std.testing.expectError(error.CapabilityMismatch, backend.synchronize(.{
4342 .scope = .stream,
4343 .stream = stream,
4344 }));
4345 try std.testing.expectError(error.CapabilityMismatch, backend.synchronize(.{
4346 .scope = .event,
4347 .event = event,
4348 }));
4349 try std.testing.expectError(error.CapabilityMismatch, backend.queryEvent(.{ .event = event }));
4350 try std.testing.expectEqual(@as(?BackendObjectId, null), state.queried_event);
4351 try std.testing.expectError(error.CapabilityMismatch, backend.elapsedEventNs(.{
4352 .start = event,
4353 .end = event,
4354 }));
4355 try std.testing.expectEqual(@as(?BackendObjectId, null), state.elapsed_start_event);
4356
4357 try std.testing.expectError(error.CapabilityMismatch, backend.recordEvent(.{
4358 .stream = stream,
4359 .event = event,
4360 }));
4361 try std.testing.expectEqual(@as(?BackendObjectId, null), state.record_stream);
4362 try std.testing.expectEqual(@as(?BackendObjectId, null), state.recorded_event);
4363 }
4364
4365 test "event record request is explicit and optional" {
4366 var state = FakeBackendState{};
4367 const backend = BackendHandle{
4368 .ptr = &state,
4369 .vtable = &fake_vtable,
4370 .kind = .cuda,
4371 };
4372
4373 const stream = StreamHandle{
4374 .id = 11,
4375 .backend = .cuda,
4376 };
4377 const event = EventHandle{
4378 .id = 12,
4379 .backend = .cuda,
4380 };
4381
4382 try backend.recordEvent(.{
4383 .stream = stream,
4384 .event = event,
4385 });
4386 try std.testing.expectEqual(@as(BackendObjectId, 11), state.record_stream.?);
4387 try std.testing.expectEqual(@as(BackendObjectId, 12), state.recorded_event.?);
4388
4389 const unsupported_vtable = BackendVTable{
4390 .query_capabilities = fakeQueryCapabilities,
4391 };
4392 const unsupported = BackendHandle{
4393 .ptr = &state,
4394 .vtable = &unsupported_vtable,
4395 .kind = .cuda,
4396 };
4397 try std.testing.expectError(error.UnsupportedOperation, unsupported.recordEvent(.{
4398 .stream = stream,
4399 .event = event,
4400 }));
4401 }
4402
4403 test "loadArtifact refuses an artifact whose interface the device cannot satisfy" {
4404 var state = FakeBackendState{};
4405 const handle = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda };
4406 var artifact = try KernelArtifact.init(std.testing.allocator, .{
4407 .backend = .cuda,
4408 .format = .cuda_ptx,
4409 .entry_name = "kernel",
4410 .argument_count = 1,
4411 .interface = .{ .features = .{ .async_copy = true } },
4412 });
4413 defer artifact.deinit();
4414 try std.testing.expectError(error.CapabilityMismatch, handle.loadArtifact(&artifact));
4415
4416 artifact.interface = .{ .subgroup = .{ .scan = true } };
4417 try std.testing.expectError(error.CapabilityMismatch, handle.loadArtifact(&artifact));
4418
4419 artifact.interface = .{};
4420 artifact.interface.push_constants.byte_size = 3;
4421 try std.testing.expectError(error.InvalidArtifact, handle.loadArtifact(&artifact));
4422
4423 artifact.interface = .{ .features = .{ .dynamic_shared_memory = true } };
4424 try std.testing.expectError(error.UnsupportedOperation, handle.loadArtifact(&artifact));
4425 }
4426
4427 test "capabilities bound push constants and depth bias, and draws carry the declared push bytes" {
4428 const caps = BackendCapabilities{
4429 .identity = .{ .backend = .vulkan, .family = .vulkan, .name = "test-vulkan-device" },
4430 .raster = .{
4431 .supported = true,
4432 .artifact_formats = RenderArtifactFormatSet.init(&.{.vulkan_spirv}),
4433 .target_formats = TextureFormatSet.init(&.{.rgba8_unorm}),
4434 .depth_formats = TextureFormatSet.init(&.{.depth32_float}),
4435 .blend_modes = RenderBlendModeSet.init(&.{.replace}),
4436 .topologies = RenderPrimitiveTopologySet.init(&.{.triangle_list}),
4437 .index_formats = RenderIndexFormatSet.init(&.{.none}),
4438 .max_push_constant_bytes = 16,
4439 .depth_bias = true,
4440 },
4441 };
4442 const desc = RenderPipelineDesc{
4443 .format = .vulkan_spirv,
4444 .vertex_entry_name = "vs",
4445 .fragment_entry_name = "fs",
4446 .target_format = .rgba8_unorm,
4447 .push_constant_bytes = 16,
4448 .push_extent = 16,
4449 };
4450 try caps.validateRenderPipelineDesc(desc);
4451 var short = desc;
4452 short.push_constant_bytes = 12;
4453 try std.testing.expectError(error.PushConstantRangeExceeded, caps.validateRenderPipelineDesc(short));
4454 var unaligned = desc;
4455 unaligned.push_constant_bytes = 6;
4456 try std.testing.expectError(error.InvalidRenderArtifact, caps.validateRenderPipelineDesc(unaligned));
4457 var oversized = desc;
4458 oversized.push_constant_bytes = 20;
4459 try std.testing.expectError(error.CapabilityMismatch, caps.validateRenderPipelineDesc(oversized));
4460
4461 var biased = desc;
4462 biased.depth = .{ .bias = .{ .constant = -16, .slope = -1 } };
4463 try caps.validateRenderPipelineDesc(biased);
4464 var clamped = biased;
4465 clamped.depth.?.bias.clamp = -0x1p-20;
4466 try std.testing.expectError(error.CapabilityMismatch, caps.validateRenderPipelineDesc(clamped));
4467 var clamp_caps = caps;
4468 clamp_caps.raster.depth_bias_clamp = true;
4469 try clamp_caps.validateRenderPipelineDesc(clamped);
4470 var infinite = biased;
4471 infinite.depth.?.bias.slope = std.math.inf(f32);
4472 try std.testing.expectError(error.InvalidRenderArtifact, caps.validateRenderPipelineDesc(infinite));
4473 var no_bias_caps = caps;
4474 no_bias_caps.raster.depth_bias = false;
4475 try std.testing.expectError(error.CapabilityMismatch, no_bias_caps.validateRenderPipelineDesc(biased));
4476
4477 var artifact = try RenderArtifact.init(std.testing.allocator, .{ .backend = .vulkan, .pipeline = desc });
4478 defer artifact.deinit();
4479 const pipeline = LoadedRenderArtifact.describing(&artifact, 7);
4480 try std.testing.expectEqual(@as(u32, 16), pipeline.push_constant_bytes);
4481 const target = TextureView{
4482 .texture = .{
4483 .id = 1,
4484 .backend = .vulkan,
4485 .extent = .{ .width = 8, .height = 8, .depth = 1 },
4486 .format = .rgba8_unorm,
4487 .usage = .{ .color_attachment = true },
4488 },
4489 .format = .rgba8_unorm,
4490 };
4491 const push: [16]u8 = @splat(0);
4492 var draws = [_]RenderDraw{.{ .pipeline = pipeline, .range = .{ .vertex_count = 3 }, .push_constants = &push }};
4493 const pass = RenderPass{
4494 .color = .{ .view = target },
4495 .viewport = .{ .width = 8, .height = 8 },
4496 .scissor = .{ .width = 8, .height = 8 },
4497 .draws = &draws,
4498 };
4499 try caps.validateRenderPass(pass);
4500 draws[0].push_constants = push[0..12];
4501 try std.testing.expectError(error.RenderArgumentMismatch, caps.validateRenderPass(pass));
4502 draws[0].push_constants = &.{};
4503 try std.testing.expectError(error.RenderArgumentMismatch, caps.validateRenderPass(pass));
4504 }