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 }