tiny.gpu.contract
Defined in tiny.gpu.
API (129)
Actions
Public operations.
Types and contracts
Public types and contracts.
ArtifactFormatArtifactFormatSetArtifactPayloadBackendCapabilitiesBackendErrorBackendHandleBackendKindBackendMathTier: A caller picks the math tier, the arithmetic contract compiled code follows, before compiling for a device.BackendObjectIdBackendVTableBufferAccessBufferAllocationBufferBindingBufferFillRequestBufferHandle: A caller holds this value to name one device buffer in later transfers and launches.BufferImport: A caller fills this request to let a backend use caller memory as a buffer without copying.BufferOwnershipBufferReadRequestBufferWriteRequestCocoaSurfaceColorSpaceColorSpaceSetCompilePayloadCompileRequestDTypeSetDeviceFamilyDeviceIdentityEventAllocationEventElapsedRequestEventHandle: A caller holds this value to mark a point in queued device work and wait for it.EventQueryRequestEventRecordRequestExternalPayloadExternalSurfaceFloatArithmeticProfile: The f32 contract a compiler may claim for this device.FloatControlFacts: Properties reported by the selected Vulkan physical device.FloatControlIndependenceFloatControlWidthsHeadlessSurfaceKernelArtifactKernelArtifactDescLaunchRequestLayoutFeaturesLoadedArtifact: A caller holds this value for compiled code loaded on a device and uses it to launch that code.LoadedRenderArtifact: A pipeline ready to draw, with the facts a pass checks its draws against.MemoryLimitsPayloadOwnershipPresentModePresentModeSetPresentRequestRasterCapabilitiesRenderAddressModeRenderArtifactRenderArtifactDescRenderArtifactFormatRenderArtifactFormatSetRenderBindingDescRenderBindingKindRenderBindingKindSetRenderBindings: A pipeline's resources, written once and bound by every draw that names them.RenderBindingsRequest: Resources for every binding ofpipeline, in the order ofartifact.bindings.RenderBlendMode: How a fragment's colorscombines with the target's colord.RenderBlendModeSetRenderBufferRange: A buffer bound to one vertex layout, read fromoffsetbytes on.RenderBundle: A pass recorded once and submitted any number of times.RenderBundleSubmit: Submits a recorded pass again.RenderColorAttachmentRenderColorLoadRenderCompare: The comparison a depth test applies asfragment op stored.RenderDepthAttachmentRenderDepthBias: An offset added to each fragment's depth before the depth test, so that a decal drawn over a coplanar surface passes a test the surface's own depth would tie.RenderDepthLoadRenderDepthState: A pipeline's depth test.RenderDraw: One draw of a pass.RenderDrawRangeRenderFilterRenderIndexFormatRenderIndexFormatSetRenderPass: Draws into one color target and an optional depth target, in order.RenderPipelineDesc: A graphics pipeline.RenderPrimitiveTopologyRenderPrimitiveTopologySetRenderRequest: Recordspassand submits it once.RenderResource: One resource for one binding of a pipeline, of the kind that binding names.RenderSampledTextureRenderSampler: How a sampled texture reads between and beyond its texels.RenderScissorRenderVertexAttributeRenderVertexBufferLayoutRenderVertexFormatRenderVertexFormatSetRenderVertexStepModeRenderViewportRuntimeRequirementsStreamAllocationStreamHandle: A caller holds this value to order work on one device queue.SubgroupFactsSurfaceAlphaModeSurfaceCapabilitiesSurfaceClearColorSurfaceCreationRequestSurfaceExtentSurfaceFrameSurfaceFrameAcquireRequestSurfaceFrameWriteOpSurfaceFrameWriteRequestSurfaceHandleSurfacePlatformSurfacePlatformKindSurfacePlatformSetSyncRequestSyncScopeTextureAllocationTextureCapabilitiesTextureExtentTextureFormatTextureFormatSetTextureHandleTextureOwnershipTextureReadRequest: Copies every texel oftextureintobytes, rows tightly packed from the top.TextureUsageTextureViewTextureWriteRequest: Replaces every texel oftexturewithbytes, rows tightly packed from the top.ThreadgroupFactsWebGpuCanvasSurfaceX11Surface
Source
Source: lib/gpu/src/contract.zig
zig
const std = @import("std");const choir_abi = @import("choir_abi");const Allocator = std.mem.Allocator;const DType = choir_abi.DType;pub const BackendError = error{ UnsupportedOperation, UnsupportedArtifactFormat, CapabilityMismatch, CompilationFailed, InvalidArtifact, InvalidBuffer, ReadBufferDestinationTooSmall, InvalidSurface, InvalidTexture, InvalidSurfaceFrame, InvalidRenderArtifact, SurfaceAlreadyAcquired, SurfaceFrameExpired, InvalidStream, InvalidEvent, LaunchArgumentMismatch, LaunchFailed, RenderArgumentMismatch, /// A render module's stages read push constants past the pipeline's declared bytes. PushConstantRangeExceeded, RenderFailed, ResultMismatch, RuntimeUnavailable, DeviceLost, MissingPayloadDeinit, OutOfMemory,};pub const BackendKind = enum(u8) { cuda = 0, vulkan = 1, metal = 2, external = 3, webgpu = 4, cpu = 5, wasm = 6,};pub const DeviceFamily = enum(u8) { nvidia_cuda = 0, vulkan = 1, apple_metal = 2, external = 3, webgpu = 4, native_cpu = 5, webassembly = 6,};pub const ArtifactFormat = enum(u8) { cuda_ptx = 0, cuda_cubin = 1, vulkan_spirv = 2, metal_msl = 3, metal_metallib = 4, external = 5, webgpu_wgsl = 6, cpu_machine_code = 7, cpu_object = 8, webassembly_module = 9,};/// A caller picks the math tier, the arithmetic contract compiled code follows, before compiling/// for a device. Two choices exist: `exact`, the default, and `tf32_tensor`, which uses the/// reduced-precision TF32 format on NVIDIA tensor cores. The option `tf32_tensor` is accepted only/// for a CUDA backend, one device runtime behind the common interface, emitting PTX on a device/// with tensor cores. Compiling fails with an error when a kernel cannot be compiled under/// `tf32_tensor`, and the code never falls back to another tier on its own. The chosen tier is/// hashed into the identity of the compiled artifact, so code compiled under one tier remains/// distinct from code compiled under the other. A tier chooses arithmetic precision alone, and it/// gives no license for algebraic rewrites that change which values are NaN, infinities or the sign/// of zero.pub const BackendMathTier = enum(u8) { exact = 0, tf32_tensor = 1, pub fn parse(text: []const u8) ?BackendMathTier { inline for ( @typeInfo(BackendMathTier).@"enum".field_names, @typeInfo(BackendMathTier).@"enum".field_values, ) |field_name, field_name_value| { const field = .{ .name = field_name, .value = field_name_value }; const value: BackendMathTier = @fromBackingInt(@intCast(field.value)); if (std.mem.eql(u8, text, @tagName(value))) return value; } return null; }};pub fn familyForBackendKind(kind: BackendKind) DeviceFamily { return switch (kind) { .cuda => .nvidia_cuda, .vulkan => .vulkan, .metal => .apple_metal, .external => .external, .webgpu => .webgpu, .cpu => .native_cpu, .wasm => .webassembly, };}pub fn artifactFormatIsNativeCpu(format: ArtifactFormat) bool { return switch (format) { .cpu_machine_code, .cpu_object => true, else => false, };}pub fn artifactFormatUsesHostLoopLaunch(format: ArtifactFormat) bool { return switch (format) { .cpu_machine_code, .cpu_object, .webassembly_module => true, else => false, };}pub const PayloadOwnership = enum { borrowed, owned,};pub const BufferOwnership = enum { host, backend, borrowed_external,};pub const BufferAccess = enum { read_only, write_only, read_write,};pub const TextureOwnership = enum { backend, acquired_surface, borrowed_external,};pub const TextureFormat = enum(u8) { rgba8_unorm = 0, bgra8_unorm = 1, rgba8_srgb = 2, bgra8_srgb = 3, depth32_float = 4, /// Bytes one texel occupies in a tightly packed transfer. pub fn texelBytes(self: TextureFormat) u32 { return switch (self) { .rgba8_unorm, .bgra8_unorm, .rgba8_srgb, .bgra8_srgb, .depth32_float => 4, }; } pub fn isDepth(self: TextureFormat) bool { return self == .depth32_float; }};pub const ColorSpace = enum(u8) { srgb = 0, linear = 1,};pub const PresentMode = enum(u8) { fifo = 0, mailbox = 1, immediate = 2,};pub const SurfaceAlphaMode = enum(u8) { solid = 0, premultiplied = 1, postmultiplied = 2, inherit = 3,};pub const SurfacePlatformKind = enum(u8) { x11 = 0, cocoa = 1, webgpu_canvas = 2, headless = 3, external = 4,};pub const X11Surface = struct { display: usize, window: u64, visual_id: u32 = 0, depth: u8 = 0,};pub const CocoaSurface = struct { app: usize = 0, window: usize = 0, layer: usize,};pub const WebGpuCanvasSurface = struct { context: usize, canvas_id: u64 = 0,};pub const HeadlessSurface = struct { token: u64 = 0,};pub const ExternalSurface = struct { ptr: *anyopaque, type_id: []const u8,};pub const SurfacePlatform = union(SurfacePlatformKind) { x11: X11Surface, cocoa: CocoaSurface, webgpu_canvas: WebGpuCanvasSurface, headless: HeadlessSurface, external: ExternalSurface, pub fn kind(self: SurfacePlatform) SurfacePlatformKind { return switch (self) { .x11 => .x11, .cocoa => .cocoa, .webgpu_canvas => .webgpu_canvas, .headless => .headless, .external => .external, }; }};pub const SurfaceExtent = struct { width: u32 = 0, height: u32 = 0, pub fn valid(self: SurfaceExtent) bool { return self.width != 0 and self.height != 0; }};pub const TextureExtent = struct { width: u32 = 0, height: u32 = 0, depth: u32 = 1, pub fn valid(self: TextureExtent) bool { return self.width != 0 and self.height != 0 and self.depth != 0; }};pub const TextureUsage = struct { copy_src: bool = false, copy_dst: bool = false, sampled: bool = false, storage: bool = false, color_attachment: bool = false, depth_attachment: bool = false, present: bool = false, pub fn any(self: TextureUsage) bool { return self.copy_src or self.copy_dst or self.sampled or self.storage or self.color_attachment or self.depth_attachment or self.present; } pub fn containsAll(self: TextureUsage, required: TextureUsage) bool { if (required.copy_src and !self.copy_src) return false; if (required.copy_dst and !self.copy_dst) return false; if (required.sampled and !self.sampled) return false; if (required.storage and !self.storage) return false; if (required.color_attachment and !self.color_attachment) return false; if (required.depth_attachment and !self.depth_attachment) return false; if (required.present and !self.present) return false; return true; }};pub const RenderArtifactFormat = enum(u8) { vulkan_spirv = 0, metal_msl = 1, metal_metallib = 2, webgpu_wgsl = 3, external = 4, /// One relocatable object for the host that defines both stages as functions of the /// `choir_abi.stage` layout. cpu_object = 5,};/// How a fragment's color `s` combines with the target's color `d`. Each mode applies one equation/// per channel, with `as` the fragment's alpha:/// - `replace`: s./// - `alpha_premultiplied`: s + d·(1 − as), for color and alpha alike./// - `alpha_straight`: color s·as + d·(1 − as), alpha as + ad·(1 − as)./// - `additive`: s + d, for color and alpha alike.////// An 8-bit unorm target stores each result as a byte. A color k/255 written without blending/// lands on byte k on every backend, since interpolation, sampling and the conversion move it by/// far less than half a step. A blend's exact result can fall between two bytes, and Vulkan only/// recommends rounding to nearest when it converts, so a blended byte may differ between/// backends by one step. The CPU backend rounds to nearest.pub const RenderBlendMode = enum(u8) { replace = 0, alpha_premultiplied = 1, alpha_straight = 2, additive = 3,};pub const RenderPrimitiveTopology = enum(u8) { triangle_list = 0, triangle_strip = 1, line_list = 2, line_strip = 3,};pub const RenderVertexStepMode = enum(u8) { vertex = 0, instance = 1,};pub const RenderVertexFormat = enum(u8) { float32 = 0, float32x2 = 1, float32x3 = 2, float32x4 = 3, uint32 = 4, uint32x2 = 5, uint32x4 = 6,};pub const RenderBindingKind = enum(u8) { uniform_buffer = 0, storage_buffer = 1, sampled_texture = 2, storage_texture = 3,};pub const RenderIndexFormat = enum(u8) { none = 0, u16 = 1, u32 = 2,};pub const RenderVertexAttribute = struct { location: u32, format: RenderVertexFormat, offset: u32,};pub const RenderVertexBufferLayout = struct { binding: u32, stride: u32, step_mode: RenderVertexStepMode = .vertex, attribute_start: u32, attribute_count: u32,};pub const RenderBindingDesc = struct { group: u32 = 0, binding: u32, kind: RenderBindingKind, access: BufferAccess = .read_only,};pub const RenderViewport = struct { x: f32 = 0, y: f32 = 0, width: f32, height: f32, min_depth: f32 = 0, max_depth: f32 = 1, pub fn valid(self: RenderViewport) bool { if (!std.math.isFinite(self.x) or !std.math.isFinite(self.y)) return false; if (!std.math.isFinite(self.width) or !std.math.isFinite(self.height)) return false; if (!std.math.isFinite(self.min_depth) or !std.math.isFinite(self.max_depth)) return false; if (self.width <= 0 or self.height <= 0) return false; if (self.min_depth < 0 or self.max_depth > 1 or self.min_depth > self.max_depth) return false; return true; }};pub const RenderScissor = struct { x: u32 = 0, y: u32 = 0, width: u32, height: u32, pub fn valid(self: RenderScissor) bool { return self.width != 0 and self.height != 0; }};pub const RenderDrawRange = struct { first_vertex: u32 = 0, vertex_count: u32 = 0, first_index: u32 = 0, index_count: u32 = 0, base_vertex: i32 = 0, first_instance: u32 = 0, instance_count: u32 = 1, index_format: RenderIndexFormat = .none, pub fn valid(self: RenderDrawRange) bool { if (self.instance_count == 0) return false; const indexed = self.index_count != 0 or self.index_format != .none; if (indexed) return self.index_count != 0 and self.index_format != .none and self.vertex_count == 0; return self.vertex_count != 0; }};/// The comparison a depth test applies as `fragment op stored`.pub const RenderCompare = enum(u8) { never = 0, less = 1, equal = 2, less_equal = 3, greater = 4, not_equal = 5, greater_equal = 6, always = 7,};/// A pipeline's depth test. Its draws need a pass whose depth attachment has `format`. A fragment/// that fails `compare` is discarded, and one that passes writes its depth when `write` is set.pub const RenderDepthState = struct { format: TextureFormat = .depth32_float, compare: RenderCompare = .less, write: bool = true, bias: RenderDepthBias = .{},};/// An offset added to each fragment's depth before the depth test, so that a decal drawn over a/// coplanar surface passes a test the surface's own depth would tie. With `m` the triangle's/// depth slope in pixels and `r` the step of depth32_float at the triangle's largest vertex depth/// `z`, the offset is `o = m * slope + r * constant`, where `r = 2^(e - 23)` for `z = f * 2^e`/// with `f` in [1, 2). A positive `clamp` caps `o` at `clamp`, a negative one floors `o` at/// `clamp`, and zero leaves `o` unclamped. A bias that moves a depth outside [0, 1] leaves what/// is stored to the backend: the CPU backend stores it as computed.////// Vulkan lets `m` be `sqrt(dz/dx^2 + dz/dy^2)` or `max(|dz/dx|, |dz/dy|)`. The CPU backend takes/// the maximum. The two agree when depth changes along one screen axis, and otherwise the maximum is/// at most the root and at least 1/sqrt(2) of it, so a sloped bias matches another backend only/// to that factor.pub const RenderDepthBias = struct { constant: f32 = 0, slope: f32 = 0, clamp: f32 = 0, pub fn valid(self: RenderDepthBias) bool { if (!std.math.isFinite(self.constant)) return false; if (!std.math.isFinite(self.slope)) return false; return std.math.isFinite(self.clamp); } pub fn enabled(self: RenderDepthBias) bool { return self.constant != 0 or self.slope != 0; }};pub const RenderFilter = enum(u8) { nearest = 0, linear = 1,};pub const RenderAddressMode = enum(u8) { clamp_to_edge = 0, repeat = 1,};/// How a sampled texture reads between and beyond its texels.pub const RenderSampler = struct { filter: RenderFilter = .nearest, address: RenderAddressMode = .clamp_to_edge,};pub const RenderSampledTexture = struct { texture: TextureHandle, sampler: RenderSampler = .{},};/// One resource for one binding of a pipeline, of the kind that binding names.pub const RenderResource = union(RenderBindingKind) { uniform_buffer: BufferHandle, storage_buffer: BufferHandle, sampled_texture: RenderSampledTexture, storage_texture: TextureHandle,};/// Resources for every binding of `pipeline`, in the order of `artifact.bindings`.pub const RenderBindingsRequest = struct { artifact: *const RenderArtifact, pipeline: LoadedRenderArtifact, resources: []const RenderResource,};/// A pipeline's resources, written once and bound by every draw that names them. The resources/// must outlive it, and `destroyObject` releases it.pub const RenderBindings = struct { id: BackendObjectId, backend: BackendKind, pipeline: BackendObjectId,};pub const RenderColorLoad = union(enum) { load, clear: SurfaceClearColor,};pub const RenderDepthLoad = union(enum) { load, clear: f32,};pub const RenderColorAttachment = struct { view: TextureView, load: RenderColorLoad = .load,};pub const RenderDepthAttachment = struct { view: TextureView, load: RenderDepthLoad = .{ .clear = 1 },};/// A buffer bound to one vertex layout, read from `offset` bytes on.pub const RenderBufferRange = struct { buffer: BufferHandle, offset: u64 = 0,};/// One draw of a pass. `vertex_buffers` follows the pipeline's vertex layouts in order, and an/// indexed range reads `index_buffer` in the range's index format. A backend checks every range it/// can see without reading buffer contents: instances, a non-indexed draw's vertices and the/// indices. The vertices an index names are the caller's to keep inside each per-vertex buffer./// `push_constants` holds exactly the pipeline's `push_constant_bytes`, which both stages read for/// this draw alone. A bundle copies them when it records the draw.pub const RenderDraw = struct { pipeline: LoadedRenderArtifact, bindings: ?RenderBindings = null, vertex_buffers: []const RenderBufferRange = &.{}, index_buffer: ?RenderBufferRange = null, range: RenderDrawRange, push_constants: []const u8 = &.{},};/// Draws into one color target and an optional depth target, in order. Every draw's pipeline/// targets the color format, and a pipeline with a depth state needs the depth attachment. A clear/// covers the whole target, whatever the scissor, and draws touch only pixels inside the scissor.pub const RenderPass = struct { color: RenderColorAttachment, depth: ?RenderDepthAttachment = null, viewport: RenderViewport, scissor: RenderScissor, draws: []const RenderDraw, diagnostic_id: ?[]const u8 = null,};/// A pass recorded once and submitted any number of times. The objects it names must outlive it,/// and their contents may change between submissions.pub const RenderBundle = struct { id: BackendObjectId, backend: BackendKind, draw_count: u32,};pub const SyncScope = enum { default_stream, stream, event, device,};pub const BackendObjectId = u64;pub const DTypeSet = struct { bits: u64 = 0, pub fn init(values: []const DType) DTypeSet { var set: DTypeSet = .{}; for (values) |value| set.insert(value); return set; } pub fn insert(self: *DTypeSet, value: DType) void { self.bits |= bitForDType(value); } pub fn contains(self: DTypeSet, value: DType) bool { return (self.bits & bitForDType(value)) != 0; } pub fn containsAll(self: DTypeSet, required: DTypeSet) bool { return (self.bits & required.bits) == required.bits; }};pub const ArtifactFormatSet = struct { bits: u64 = 0, pub fn init(values: []const ArtifactFormat) ArtifactFormatSet { var set: ArtifactFormatSet = .{}; for (values) |value| set.insert(value); return set; } pub fn insert(self: *ArtifactFormatSet, value: ArtifactFormat) void { self.bits |= bitForArtifactFormat(value); } pub fn contains(self: ArtifactFormatSet, value: ArtifactFormat) bool { return (self.bits & bitForArtifactFormat(value)) != 0; }};pub const TextureFormatSet = struct { bits: u64 = 0, pub fn init(values: []const TextureFormat) TextureFormatSet { var set: TextureFormatSet = .{}; for (values) |value| set.insert(value); return set; } pub fn insert(self: *TextureFormatSet, value: TextureFormat) void { self.bits |= bitForTextureFormat(value); } pub fn contains(self: TextureFormatSet, value: TextureFormat) bool { return (self.bits & bitForTextureFormat(value)) != 0; }};pub const PresentModeSet = struct { bits: u64 = 0, pub fn init(values: []const PresentMode) PresentModeSet { var set: PresentModeSet = .{}; for (values) |value| set.insert(value); return set; } pub fn insert(self: *PresentModeSet, value: PresentMode) void { self.bits |= bitForPresentMode(value); } pub fn contains(self: PresentModeSet, value: PresentMode) bool { return (self.bits & bitForPresentMode(value)) != 0; }};pub const ColorSpaceSet = struct { bits: u64 = 0, pub fn init(values: []const ColorSpace) ColorSpaceSet { var set: ColorSpaceSet = .{}; for (values) |value| set.insert(value); return set; } pub fn insert(self: *ColorSpaceSet, value: ColorSpace) void { self.bits |= bitForColorSpace(value); } pub fn contains(self: ColorSpaceSet, value: ColorSpace) bool { return (self.bits & bitForColorSpace(value)) != 0; }};pub const SurfacePlatformSet = struct { bits: u64 = 0, pub fn init(values: []const SurfacePlatformKind) SurfacePlatformSet { var set: SurfacePlatformSet = .{}; for (values) |value| set.insert(value); return set; } pub fn insert(self: *SurfacePlatformSet, value: SurfacePlatformKind) void { self.bits |= bitForSurfacePlatformKind(value); } pub fn contains(self: SurfacePlatformSet, value: SurfacePlatformKind) bool { return (self.bits & bitForSurfacePlatformKind(value)) != 0; }};pub const RenderArtifactFormatSet = struct { bits: u64 = 0, pub fn init(values: []const RenderArtifactFormat) RenderArtifactFormatSet { var set: RenderArtifactFormatSet = .{}; for (values) |value| set.insert(value); return set; } pub fn insert(self: *RenderArtifactFormatSet, value: RenderArtifactFormat) void { self.bits |= bitForRenderArtifactFormat(value); } pub fn contains(self: RenderArtifactFormatSet, value: RenderArtifactFormat) bool { return (self.bits & bitForRenderArtifactFormat(value)) != 0; }};pub const RenderBlendModeSet = struct { bits: u64 = 0, pub fn init(values: []const RenderBlendMode) RenderBlendModeSet { var set: RenderBlendModeSet = .{}; for (values) |value| set.insert(value); return set; } pub fn insert(self: *RenderBlendModeSet, value: RenderBlendMode) void { self.bits |= bitForRenderBlendMode(value); } pub fn contains(self: RenderBlendModeSet, value: RenderBlendMode) bool { return (self.bits & bitForRenderBlendMode(value)) != 0; }};pub const RenderPrimitiveTopologySet = struct { bits: u64 = 0, pub fn init(values: []const RenderPrimitiveTopology) RenderPrimitiveTopologySet { var set: RenderPrimitiveTopologySet = .{}; for (values) |value| set.insert(value); return set; } pub fn insert(self: *RenderPrimitiveTopologySet, value: RenderPrimitiveTopology) void { self.bits |= bitForRenderPrimitiveTopology(value); } pub fn contains(self: RenderPrimitiveTopologySet, value: RenderPrimitiveTopology) bool { return (self.bits & bitForRenderPrimitiveTopology(value)) != 0; }};pub const RenderVertexFormatSet = struct { bits: u64 = 0, pub fn init(values: []const RenderVertexFormat) RenderVertexFormatSet { var set: RenderVertexFormatSet = .{}; for (values) |value| set.insert(value); return set; } pub fn insert(self: *RenderVertexFormatSet, value: RenderVertexFormat) void { self.bits |= bitForRenderVertexFormat(value); } pub fn contains(self: RenderVertexFormatSet, value: RenderVertexFormat) bool { return (self.bits & bitForRenderVertexFormat(value)) != 0; }};pub const RenderBindingKindSet = struct { bits: u64 = 0, pub fn init(values: []const RenderBindingKind) RenderBindingKindSet { var set: RenderBindingKindSet = .{}; for (values) |value| set.insert(value); return set; } pub fn insert(self: *RenderBindingKindSet, value: RenderBindingKind) void { self.bits |= bitForRenderBindingKind(value); } pub fn contains(self: RenderBindingKindSet, value: RenderBindingKind) bool { return (self.bits & bitForRenderBindingKind(value)) != 0; }};pub const RenderIndexFormatSet = struct { bits: u64 = 0, pub fn init(values: []const RenderIndexFormat) RenderIndexFormatSet { var set: RenderIndexFormatSet = .{}; for (values) |value| set.insert(value); return set; } pub fn insert(self: *RenderIndexFormatSet, value: RenderIndexFormat) void { self.bits |= bitForRenderIndexFormat(value); } pub fn contains(self: RenderIndexFormatSet, value: RenderIndexFormat) bool { return (self.bits & bitForRenderIndexFormat(value)) != 0; }};pub const DeviceIdentity = struct { backend: BackendKind, family: DeviceFamily, name: []const u8 = "unknown", vendor_id: ?u32 = null, device_id: ?u32 = null, driver_version: ?[]const u8 = null,};pub const MemoryLimits = struct { global_bytes: ?u64 = null, max_allocation_bytes: ?u64 = null, shared_memory_per_threadgroup_bytes: ?u32 = null, constant_memory_bytes: ?u64 = null, min_buffer_alignment: u32 = 1, unified_memory: bool = false, host_visible_device_memory: bool = false,};pub const SubgroupFacts = struct { supported: bool = false, size_min: u32 = 0, size_max: u32 = 0, shuffle: bool = false, ballot: bool = false, vote: bool = false, arithmetic: bool = false, scan: bool = false, pub fn satisfies(self: SubgroupFacts, required: choir_abi.SubgroupRequirements) bool { if (required.supported and !self.supported) return false; if (required.size_min != 0 and (!self.supported or self.size_max < required.size_min)) return false; if (required.size_max != 0 and (!self.supported or self.size_min > required.size_max)) return false; if (required.shuffle and (!self.supported or !self.shuffle)) return false; if (required.ballot and (!self.supported or !self.ballot)) return false; if (required.vote and (!self.supported or !self.vote)) return false; if (required.arithmetic and (!self.supported or !self.arithmetic)) return false; if (required.scan and (!self.supported or !self.scan)) return false; return true; }};pub const ThreadgroupFacts = struct { max_threads: u32 = 1, max_blocks: [3]u32 = .{ 1, 1, 1 }, max_threads_per_dim: [3]u32 = .{ 1, 1, 1 }, max_grid_per_dim: [3]u32 = .{ 1, 1, 1 }, shared_memory_bytes: u32 = 0,};pub const LayoutFeatures = struct { row_major: bool = true, column_major: bool = false, compact_strides: bool = true, arbitrary_strides: bool = false, broadcast_strides: bool = false, tiled: bool = false, vectorized: bool = false, opaque_backend_layouts: bool = false,};pub const RuntimeRequirements = struct { driver_loaded: bool = false, device_context: bool = false, streams: bool = false, events: bool = false, timeline_events: bool = false, host_pinned_memory: bool = false, external_allocator: bool = false, pub fn containsAll(self: RuntimeRequirements, required: RuntimeRequirements) bool { if (required.driver_loaded and !self.driver_loaded) return false; if (required.device_context and !self.device_context) return false; if (required.streams and !self.streams) return false; if (required.events and !self.events) return false; if (required.timeline_events and !self.timeline_events) return false; if (required.host_pinned_memory and !self.host_pinned_memory) return false; if (required.external_allocator and !self.external_allocator) return false; return true; }};pub const TextureCapabilities = struct { supported: bool = false, formats: TextureFormatSet = .{}, usages: TextureUsage = .{}, max_extent: TextureExtent = .{}, max_sample_count: u32 = 1, pub fn supportsFormat(self: TextureCapabilities, format: TextureFormat) bool { return self.formats.contains(format); } pub fn supportsUsage(self: TextureCapabilities, usage: TextureUsage) bool { return self.usages.containsAll(usage); } pub fn supportsExtent(self: TextureCapabilities, extent: TextureExtent) bool { if (!extent.valid()) return false; if (self.max_extent.width != 0 and extent.width > self.max_extent.width) return false; if (self.max_extent.height != 0 and extent.height > self.max_extent.height) return false; if (self.max_extent.depth != 0 and extent.depth > self.max_extent.depth) return false; return true; }};pub const SurfaceCapabilities = struct { supported: bool = false, platforms: SurfacePlatformSet = .{}, formats: TextureFormatSet = .{}, color_spaces: ColorSpaceSet = .{}, present_modes: PresentModeSet = .{}, usages: TextureUsage = .{}, max_extent: SurfaceExtent = .{}, max_frames_in_flight: u32 = 0, pub fn supportsPlatform(self: SurfaceCapabilities, platform: SurfacePlatform) bool { return self.platforms.contains(platform.kind()); } pub fn supportsFormat(self: SurfaceCapabilities, format: TextureFormat) bool { return self.formats.contains(format); } pub fn supportsColorSpace(self: SurfaceCapabilities, color_space: ColorSpace) bool { return self.color_spaces.contains(color_space); } pub fn supportsPresentMode(self: SurfaceCapabilities, present_mode: PresentMode) bool { return self.present_modes.contains(present_mode); } pub fn supportsUsage(self: SurfaceCapabilities, usage: TextureUsage) bool { return self.usages.containsAll(usage); } pub fn supportsExtent(self: SurfaceCapabilities, extent: SurfaceExtent) bool { if (!extent.valid()) return false; if (self.max_extent.width != 0 and extent.width > self.max_extent.width) return false; if (self.max_extent.height != 0 and extent.height > self.max_extent.height) return false; return true; }};pub const RasterCapabilities = struct { supported: bool = false, artifact_formats: RenderArtifactFormatSet = .{}, target_formats: TextureFormatSet = .{}, depth_formats: TextureFormatSet = .{}, blend_modes: RenderBlendModeSet = .{}, topologies: RenderPrimitiveTopologySet = .{}, vertex_formats: RenderVertexFormatSet = .{}, binding_kinds: RenderBindingKindSet = .{}, index_formats: RenderIndexFormatSet = .{}, max_vertex_buffers: u32 = 0, max_vertex_attributes: u32 = 0, max_bindings: u32 = 0, instancing: bool = false, /// The largest push-constant block a pipeline may declare, in bytes. max_push_constant_bytes: u32 = 0, depth_bias: bool = false, depth_bias_clamp: bool = false, pub fn supportsArtifactFormat(self: RasterCapabilities, format: RenderArtifactFormat) bool { return self.artifact_formats.contains(format); } pub fn supportsTargetFormat(self: RasterCapabilities, format: TextureFormat) bool { return self.target_formats.contains(format); } pub fn supportsDepthFormat(self: RasterCapabilities, format: TextureFormat) bool { return self.depth_formats.contains(format); } pub fn supportsBlendMode(self: RasterCapabilities, blend_mode: RenderBlendMode) bool { return self.blend_modes.contains(blend_mode); } pub fn supportsTopology(self: RasterCapabilities, topology: RenderPrimitiveTopology) bool { return self.topologies.contains(topology); } pub fn supportsVertexFormat(self: RasterCapabilities, format: RenderVertexFormat) bool { return self.vertex_formats.contains(format); } pub fn supportsBindingKind(self: RasterCapabilities, kind: RenderBindingKind) bool { return self.binding_kinds.contains(kind); } pub fn supportsIndexFormat(self: RasterCapabilities, format: RenderIndexFormat) bool { return self.index_formats.contains(format); }};pub const FloatControlWidths = struct { f16: bool = false, f32: bool = false, f64: bool = false,};pub const FloatControlIndependence = enum(u32) { bit32_only = 0, all = 1, none = 2,};/// The f32 contract a compiler may claim for this device. In the flush profile,/// any subnormal operand, result, or intermediate may become zero. Stage 0 must/// refuse constant folds whose operand or result is subnormal under it. Both/// profiles preserve signed zero, infinities and NaNs.pub const FloatArithmeticProfile = enum { exact, flush_permitting,};/// Properties reported by the selected Vulkan physical device.pub const FloatControlFacts = struct { denorm_preserve: FloatControlWidths = .{}, signed_zero_inf_nan_preserve: FloatControlWidths = .{}, denorm_behavior_independence: ?FloatControlIndependence = null, /// A missing signed-zero/Inf/NaN guarantee refuses the f32 target by name. pub fn f32Profile(self: FloatControlFacts) BackendError!FloatArithmeticProfile { if (!self.signed_zero_inf_nan_preserve.f32) return error.CapabilityMismatch; return if (self.denorm_preserve.f32) .exact else .flush_permitting; }};test "f32 float-control facts select exact or flush profile explicitly" { const exact = FloatControlFacts{ .denorm_preserve = .{ .f32 = true }, .signed_zero_inf_nan_preserve = .{ .f32 = true }, }; try std.testing.expectEqual(FloatArithmeticProfile.exact, try exact.f32Profile()); const flush = FloatControlFacts{ .signed_zero_inf_nan_preserve = .{ .f32 = true } }; try std.testing.expectEqual(FloatArithmeticProfile.flush_permitting, try flush.f32Profile()); try std.testing.expectError(error.CapabilityMismatch, (FloatControlFacts{}).f32Profile());}pub const BackendCapabilities = struct { identity: DeviceIdentity, memory: MemoryLimits = .{}, subgroup: SubgroupFacts = .{}, float_controls: FloatControlFacts = .{}, threadgroup: ThreadgroupFacts = .{}, dtypes: DTypeSet = .{}, layouts: LayoutFeatures = .{}, runtime: RuntimeRequirements = .{}, features: choir_abi.Features = .{}, artifact_formats: ArtifactFormatSet = .{}, textures: TextureCapabilities = .{}, surfaces: SurfaceCapabilities = .{}, raster: RasterCapabilities = .{}, pub fn supportsDType(self: BackendCapabilities, value: DType) bool { return self.dtypes.contains(value); } pub fn supportsArtifactFormat(self: BackendCapabilities, format: ArtifactFormat) bool { return self.artifact_formats.contains(format); } pub fn supportsTextureFormat(self: BackendCapabilities, format: TextureFormat) bool { return self.textures.supportsFormat(format); } pub fn supportsSurfaceFormat(self: BackendCapabilities, format: TextureFormat) bool { return self.surfaces.supportsFormat(format); } pub fn supportsRenderArtifactFormat(self: BackendCapabilities, format: RenderArtifactFormat) bool { return self.raster.supportsArtifactFormat(format); } pub fn supportsFeatures(self: BackendCapabilities, required: choir_abi.Features) bool { return self.features.containsAll(required); } pub fn supportsSubgroup(self: BackendCapabilities, required: choir_abi.SubgroupRequirements) bool { return self.subgroup.satisfies(required); } pub fn supportsRuntime(self: BackendCapabilities, required: RuntimeRequirements) bool { return self.runtime.containsAll(required); } pub fn validateCompileRequest(self: BackendCapabilities, request: CompileRequest) BackendError!void { if (!self.supportsArtifactFormat(request.requested_format)) return error.UnsupportedArtifactFormat; if (!self.dtypes.containsAll(request.required_dtypes)) return error.CapabilityMismatch; if (!self.supportsFeatures(request.required_features)) return error.CapabilityMismatch; if (!self.supportsSubgroup(request.required_subgroup)) return error.CapabilityMismatch; } pub fn validateRuntimeRequirements(self: BackendCapabilities, required: RuntimeRequirements) BackendError!void { if (!self.supportsRuntime(required)) return error.CapabilityMismatch; } pub fn validateBufferAllocation(self: BackendCapabilities, request: BufferAllocation) BackendError!void { if (request.byte_size == 0) return error.InvalidBuffer; if (request.alignment == 0) return error.InvalidBuffer; if ((request.alignment & (request.alignment - 1)) != 0) return error.InvalidBuffer; if (request.alignment < self.memory.min_buffer_alignment) return error.CapabilityMismatch; const byte_size = std.math.cast(u64, request.byte_size) orelse return error.CapabilityMismatch; if (self.memory.max_allocation_bytes) |limit| { if (byte_size > limit) return error.CapabilityMismatch; } if (self.memory.global_bytes) |limit| { if (byte_size > limit) return error.CapabilityMismatch; } if (request.dtype) |dtype| { if (!self.supportsDType(dtype)) return error.CapabilityMismatch; if (request.element_count) |count| { const required_bytes = std.math.mul(u64, count, dtype.sizeOf()) catch return error.CapabilityMismatch; if (required_bytes > byte_size) return error.InvalidBuffer; } } } pub fn validateTextureAllocation(self: BackendCapabilities, request: TextureAllocation) BackendError!void { if (!request.extent.valid()) return error.InvalidTexture; if (!request.usage.any()) return error.InvalidTexture; if (request.sample_count == 0) return error.InvalidTexture; if (!self.textures.supported) return error.CapabilityMismatch; if (!self.textures.supportsFormat(request.format)) return error.CapabilityMismatch; if (!self.textures.supportsUsage(request.usage)) return error.CapabilityMismatch; if (!self.textures.supportsExtent(request.extent)) return error.CapabilityMismatch; if (request.sample_count > self.textures.max_sample_count) return error.CapabilityMismatch; } pub fn validateSurfaceCreation(self: BackendCapabilities, request: SurfaceCreationRequest) BackendError!void { if (!request.extent.valid()) return error.InvalidSurface; if (!request.usage.any() or !request.usage.present) return error.InvalidSurface; if (request.max_frames_in_flight == 0) return error.InvalidSurface; if (!self.surfaces.supported) return error.CapabilityMismatch; if (!self.surfaces.supportsPlatform(request.platform)) return error.CapabilityMismatch; if (!self.surfaces.supportsFormat(request.format)) return error.CapabilityMismatch; if (!self.surfaces.supportsColorSpace(request.color_space)) return error.CapabilityMismatch; if (!self.surfaces.supportsPresentMode(request.present_mode)) return error.CapabilityMismatch; if (!self.surfaces.supportsUsage(request.usage)) return error.CapabilityMismatch; if (!self.surfaces.supportsExtent(request.extent)) return error.CapabilityMismatch; if (request.max_frames_in_flight > self.surfaces.max_frames_in_flight) return error.CapabilityMismatch; } pub fn validateRenderPipelineDesc(self: BackendCapabilities, desc: RenderPipelineDesc) BackendError!void { if (desc.vertex_entry_name.len == 0 or desc.fragment_entry_name.len == 0) return error.InvalidRenderArtifact; if (!self.raster.supported) return error.CapabilityMismatch; if (!self.raster.supportsArtifactFormat(desc.format)) return error.CapabilityMismatch; if (desc.target_format.isDepth()) return error.InvalidRenderArtifact; if (!self.raster.supportsTargetFormat(desc.target_format)) return error.CapabilityMismatch; if (desc.depth) |depth| { if (!depth.format.isDepth()) return error.InvalidRenderArtifact; if (!self.raster.supportsDepthFormat(depth.format)) return error.CapabilityMismatch; if (!depth.bias.valid()) return error.InvalidRenderArtifact; if (depth.bias.enabled() and !self.raster.depth_bias) return error.CapabilityMismatch; if (depth.bias.clamp != 0 and !self.raster.depth_bias_clamp) return error.CapabilityMismatch; } if (desc.push_constant_bytes % 4 != 0) return error.InvalidRenderArtifact; if (desc.push_constant_bytes > self.raster.max_push_constant_bytes) return error.CapabilityMismatch; if (desc.push_extent > desc.push_constant_bytes) return error.PushConstantRangeExceeded; if (!self.raster.supportsBlendMode(desc.blend_mode)) return error.CapabilityMismatch; if (!self.raster.supportsTopology(desc.topology)) return error.CapabilityMismatch; if (desc.vertex_layouts.len > self.raster.max_vertex_buffers) return error.CapabilityMismatch; if (desc.vertex_attributes.len > self.raster.max_vertex_attributes) return error.CapabilityMismatch; if (desc.bindings.len > self.raster.max_bindings) return error.CapabilityMismatch; for (desc.vertex_layouts) |layout| { if (layout.stride == 0) return error.InvalidRenderArtifact; if (layout.step_mode == .instance and !self.raster.instancing) return error.CapabilityMismatch; const start: usize = @intCast(layout.attribute_start); const count: usize = @intCast(layout.attribute_count); if (count == 0) return error.InvalidRenderArtifact; if (start > desc.vertex_attributes.len or count > desc.vertex_attributes.len - start) return error.InvalidRenderArtifact; for (desc.vertex_attributes[start..][0..count]) |attribute| { if (!self.raster.supportsVertexFormat(attribute.format)) return error.CapabilityMismatch; const size = renderVertexFormatByteSize(attribute.format); if (attribute.offset > layout.stride or size > layout.stride - attribute.offset) return error.InvalidRenderArtifact; } } for (desc.bindings) |binding| { if (!self.raster.supportsBindingKind(binding.kind)) return error.CapabilityMismatch; } } /// Checks a pass against these capabilities and against the facts each draw's pipeline /// reported. A draw's pipeline declares a depth state exactly when the pass has a depth /// attachment, because a pipeline is built for the attachments it draws into. pub fn validateRenderPass(self: BackendCapabilities, pass: RenderPass) BackendError!void { if (!self.raster.supported) return error.CapabilityMismatch; if (!pass.viewport.valid()) return error.RenderArgumentMismatch; if (!pass.scissor.valid()) return error.RenderArgumentMismatch; const color = pass.color.view; if (color.texture.format != color.format) return error.InvalidTexture; if (!color.texture.usage.color_attachment) return error.InvalidTexture; if (!self.raster.supportsTargetFormat(color.format)) return error.CapabilityMismatch; switch (pass.color.load) { .load => {}, .clear => |clear| if (!clear.valid()) return error.RenderArgumentMismatch, } const extent = color.texture.extent; if (!scissorWithin(pass.scissor, extent)) return error.RenderArgumentMismatch; if (pass.depth) |depth| { if (depth.view.texture.format != depth.view.format) return error.InvalidTexture; if (!depth.view.texture.usage.depth_attachment) return error.InvalidTexture; if (!self.raster.supportsDepthFormat(depth.view.format)) return error.CapabilityMismatch; if (!sameTextureExtent(depth.view.texture.extent, extent)) return error.RenderArgumentMismatch; switch (depth.load) { .load => {}, .clear => |clear| if (!(clear >= 0 and clear <= 1)) return error.RenderArgumentMismatch, } } for (pass.draws) |draw| try self.validateRenderDraw(pass, draw); } fn validateRenderDraw(self: BackendCapabilities, pass: RenderPass, draw: RenderDraw) BackendError!void { const pipeline = draw.pipeline; if (!draw.range.valid()) return error.RenderArgumentMismatch; if (pipeline.target_format != pass.color.view.format) return error.RenderArgumentMismatch; if ((pipeline.depth == null) != (pass.depth == null)) return error.RenderArgumentMismatch; if (pipeline.depth) |depth| { if (depth.format != pass.depth.?.view.format) return error.RenderArgumentMismatch; } if (draw.vertex_buffers.len != pipeline.vertex_buffer_count) return error.RenderArgumentMismatch; if ((pipeline.binding_count == 0) != (draw.bindings == null)) return error.RenderArgumentMismatch; if (draw.bindings) |bindings| { if (bindings.pipeline != pipeline.id) return error.RenderArgumentMismatch; } const range = draw.range; if (range.instance_count > 1 and !self.raster.instancing) return error.CapabilityMismatch; if (range.index_format != .none and !self.raster.supportsIndexFormat(range.index_format)) { return error.CapabilityMismatch; } if ((range.index_count != 0) != (draw.index_buffer != null)) return error.RenderArgumentMismatch; if (draw.push_constants.len != pipeline.push_constant_bytes) return error.RenderArgumentMismatch; } /// Checks that `request` names one resource of the right kind for each pipeline binding. pub fn validateRenderBindings(self: BackendCapabilities, request: RenderBindingsRequest) BackendError!void { if (!self.raster.supported) return error.CapabilityMismatch; const bindings = request.artifact.bindings; if (request.pipeline.binding_count != bindings.len) return error.RenderArgumentMismatch; if (request.resources.len != bindings.len) return error.RenderArgumentMismatch; for (bindings, request.resources) |binding, resource| { if (std.meta.activeTag(resource) != binding.kind) return error.RenderArgumentMismatch; if (!self.raster.supportsBindingKind(binding.kind)) return error.CapabilityMismatch; switch (resource) { .uniform_buffer, .storage_buffer => {}, .sampled_texture => |sampled| if (!sampled.texture.usage.sampled) return error.InvalidTexture, .storage_texture => |texture| if (!texture.usage.storage) return error.InvalidTexture, } } } /// Checks that `byte_count` covers every texel of `texture` exactly once. pub fn validateTextureTransfer(texture: TextureHandle, byte_count: usize, usage: TextureUsage) BackendError!void { if (!texture.usage.containsAll(usage)) return error.InvalidTexture; if (byte_count != try textureByteSize(texture)) return error.InvalidTexture; } pub fn validateLaunchRuntime(self: BackendCapabilities, request: LaunchRequest) BackendError!void { if (request.stream != null) try self.validateRuntimeRequirements(.{ .streams = true }); if (request.wait_events.len != 0 or request.signal_event != null) try self.validateRuntimeRequirements(.{ .events = true }); } pub fn validateSubmitRuntime( self: BackendCapabilities, stream: ?StreamHandle, wait_events: []const EventHandle, signal_event: ?EventHandle, ) BackendError!void { if (stream != null) try self.validateRuntimeRequirements(.{ .streams = true }); if (wait_events.len != 0 or signal_event != null) try self.validateRuntimeRequirements(.{ .events = true }); } pub fn validateSyncRuntime(self: BackendCapabilities, request: SyncRequest) BackendError!void { switch (request.scope) { .default_stream, .device => {}, .stream => try self.validateRuntimeRequirements(.{ .streams = true }), .event => try self.validateRuntimeRequirements(.{ .events = true }), } } pub fn validateLaunchGeometry(self: BackendCapabilities, geometry: choir_abi.LaunchGeometry) BackendError!void { if (geometry.grid[0] == 0 or geometry.grid[1] == 0 or geometry.grid[2] == 0) { return error.LaunchArgumentMismatch; } if (geometry.threadgroup[0] == 0 or geometry.threadgroup[1] == 0 or geometry.threadgroup[2] == 0) { return error.LaunchArgumentMismatch; } const total_threads = @as(u64, geometry.threadgroup[0]) * @as(u64, geometry.threadgroup[1]) * @as(u64, geometry.threadgroup[2]); if (total_threads > self.threadgroup.max_threads) return error.CapabilityMismatch; for (geometry.threadgroup, self.threadgroup.max_threads_per_dim) |requested, limit| { if (requested > limit) return error.CapabilityMismatch; } for (geometry.grid, self.threadgroup.max_blocks) |requested, limit| { if (requested > limit) return error.CapabilityMismatch; } for (geometry.grid, self.threadgroup.max_grid_per_dim) |requested, limit| { if (requested > limit) return error.CapabilityMismatch; } if (geometry.dynamic_shared_memory_bytes != 0 and !self.features.dynamic_shared_memory) { return error.CapabilityMismatch; } if (geometry.dynamic_shared_memory_bytes > self.threadgroup.shared_memory_bytes) { return error.CapabilityMismatch; } }};pub const ArtifactPayload = union(enum) { none, bytes: []const u8, words_u32: []const u32, text: []const u8, external: ExternalPayload,};pub const ExternalPayload = struct { ptr: *anyopaque, type_id: []const u8, deinit_fn: ?*const fn (Allocator, *anyopaque) void = null,};pub const KernelArtifactDesc = struct { backend: BackendKind, format: ArtifactFormat, entry_name: []const u8, argument_count: u32, scalar_argument_count: u32 = 0, diagnostic_id: ?[]const u8 = null, interface: choir_abi.Interface = .{},};pub const KernelArtifact = struct { allocator: Allocator, backend: BackendKind, format: ArtifactFormat, entry_name: []const u8, argument_count: u32, scalar_argument_count: u32 = 0, diagnostic_id: ?[]const u8 = null, /// Requirements and push-constant layout the compiler recorded for this /// entry. Load checks the requirements; launch packs through the layout. interface: choir_abi.Interface = .{}, payload: ArtifactPayload = .none, payload_ownership: PayloadOwnership = .borrowed, pub fn init(allocator: Allocator, desc: KernelArtifactDesc) Allocator.Error!KernelArtifact { const entry_name = try dupeString(allocator, desc.entry_name); errdefer freeString(allocator, entry_name); return .{ .allocator = allocator, .backend = desc.backend, .format = desc.format, .entry_name = entry_name, .argument_count = desc.argument_count, .scalar_argument_count = desc.scalar_argument_count, .diagnostic_id = try dupeOpt(allocator, desc.diagnostic_id), .interface = desc.interface, }; } pub fn bufferArgumentCount(self: *const KernelArtifact) BackendError!u32 { if (self.scalar_argument_count > self.argument_count) return error.InvalidArtifact; return self.argument_count - self.scalar_argument_count; } pub fn deinit(self: *KernelArtifact) void { self.clearPayload(); freeString(self.allocator, self.entry_name); freeOpt(self.allocator, self.diagnostic_id); self.* = undefined; } pub fn setBorrowedBytes(self: *KernelArtifact, bytes: []const u8) void { self.clearPayload(); self.payload = .{ .bytes = bytes }; self.payload_ownership = .borrowed; } pub fn setOwnedBytes(self: *KernelArtifact, bytes: []const u8) BackendError!void { const owned = self.allocator.dupe(u8, bytes) catch return error.OutOfMemory; self.clearPayload(); self.payload = .{ .bytes = owned }; self.payload_ownership = .owned; } pub fn setBorrowedWords(self: *KernelArtifact, words: []const u32) void { self.clearPayload(); self.payload = .{ .words_u32 = words }; self.payload_ownership = .borrowed; } pub fn setOwnedWords(self: *KernelArtifact, words: []const u32) BackendError!void { const owned = self.allocator.dupe(u32, words) catch return error.OutOfMemory; self.clearPayload(); self.payload = .{ .words_u32 = owned }; self.payload_ownership = .owned; } pub fn setBorrowedText(self: *KernelArtifact, text: []const u8) void { self.clearPayload(); self.payload = .{ .text = text }; self.payload_ownership = .borrowed; } pub fn setOwnedText(self: *KernelArtifact, text: []const u8) BackendError!void { const owned = self.allocator.dupe(u8, text) catch return error.OutOfMemory; self.clearPayload(); self.payload = .{ .text = owned }; self.payload_ownership = .owned; } pub fn setExternalPayload(self: *KernelArtifact, payload: ExternalPayload, ownership: PayloadOwnership) BackendError!void { if (ownership == .owned and payload.deinit_fn == null) return error.MissingPayloadDeinit; self.clearPayload(); self.payload = .{ .external = payload }; self.payload_ownership = ownership; } fn clearPayload(self: *KernelArtifact) void { if (self.payload_ownership == .owned) { switch (self.payload) { .bytes => |bytes| freeString(self.allocator, bytes), .words_u32 => |words| self.allocator.free(@constCast(words)), .text => |text| freeString(self.allocator, text), .external => |payload| if (payload.deinit_fn) |deinit_fn| deinit_fn(self.allocator, payload.ptr), .none => {}, } } self.payload = .none; self.payload_ownership = .borrowed; }};/// A caller holds this value for compiled code loaded on a device and uses it to launch that code./// The value names compiled code that the backend owns, by id, backend kind and code format. The/// caller releases the id through the backend handle that loaded it, only after all queued work/// that can refer to it has finished. That handle is the interface value through which a caller/// creates buffers, loads compiled code and launches kernels.pub const LoadedArtifact = struct { id: BackendObjectId, backend: BackendKind, format: ArtifactFormat,};/// A graphics pipeline. Clip space puts x = −1 at the target's left edge, y = −1 at its top edge/// and depth z in [0, 1], so y grows downward in pixels. Triangles are drawn whatever their/// winding. A pixel is covered when its center lies inside a triangle, with centers on a shared/// edge going to exactly one triangle by the top-left rule.pub const RenderPipelineDesc = struct { format: RenderArtifactFormat, vertex_entry_name: []const u8, fragment_entry_name: []const u8, target_format: TextureFormat, blend_mode: RenderBlendMode = .replace, topology: RenderPrimitiveTopology = .triangle_list, depth: ?RenderDepthState = null, vertex_layouts: []const RenderVertexBufferLayout = &.{}, vertex_attributes: []const RenderVertexAttribute = &.{}, bindings: []const RenderBindingDesc = &.{}, /// Bytes of the push-constant block both stages read, a multiple of 4. Each draw supplies /// exactly this many, and a stage must read inside them. push_constant_bytes: u32 = 0, /// Bytes of the push-constant block the payload's stages read, as the emitter reported them /// with the payload, the way `CompileRequest.push_constants` carries a kernel's layout. It has /// no default, so a caller states it, and it may not exceed `push_constant_bytes`. push_extent: u32, diagnostic_id: ?[]const u8 = null, payload: CompilePayload = .none,};pub const RenderArtifactDesc = struct { backend: BackendKind, pipeline: RenderPipelineDesc,};pub const RenderArtifact = struct { allocator: Allocator, backend: BackendKind, format: RenderArtifactFormat, vertex_entry_name: []const u8, fragment_entry_name: []const u8, target_format: TextureFormat, blend_mode: RenderBlendMode, topology: RenderPrimitiveTopology, depth: ?RenderDepthState, vertex_layouts: []const RenderVertexBufferLayout, vertex_attributes: []const RenderVertexAttribute, bindings: []const RenderBindingDesc, push_constant_bytes: u32 = 0, push_extent: u32 = 0, diagnostic_id: ?[]const u8 = null, payload: ArtifactPayload = .none, payload_ownership: PayloadOwnership = .borrowed, pub fn init(allocator: Allocator, desc: RenderArtifactDesc) Allocator.Error!RenderArtifact { const vertex_entry_name = try dupeString(allocator, desc.pipeline.vertex_entry_name); errdefer freeString(allocator, vertex_entry_name); const fragment_entry_name = try dupeString(allocator, desc.pipeline.fragment_entry_name); errdefer freeString(allocator, fragment_entry_name); const vertex_layouts = try allocator.dupe(RenderVertexBufferLayout, desc.pipeline.vertex_layouts); errdefer allocator.free(vertex_layouts); const vertex_attributes = try allocator.dupe(RenderVertexAttribute, desc.pipeline.vertex_attributes); errdefer allocator.free(vertex_attributes); const bindings = try allocator.dupe(RenderBindingDesc, desc.pipeline.bindings); errdefer allocator.free(bindings); return .{ .allocator = allocator, .backend = desc.backend, .format = desc.pipeline.format, .vertex_entry_name = vertex_entry_name, .fragment_entry_name = fragment_entry_name, .target_format = desc.pipeline.target_format, .blend_mode = desc.pipeline.blend_mode, .topology = desc.pipeline.topology, .depth = desc.pipeline.depth, .vertex_layouts = vertex_layouts, .vertex_attributes = vertex_attributes, .bindings = bindings, .push_constant_bytes = desc.pipeline.push_constant_bytes, .push_extent = desc.pipeline.push_extent, .diagnostic_id = try dupeOpt(allocator, desc.pipeline.diagnostic_id), }; } pub fn deinit(self: *RenderArtifact) void { self.clearPayload(); freeString(self.allocator, self.vertex_entry_name); freeString(self.allocator, self.fragment_entry_name); self.allocator.free(@constCast(self.vertex_layouts)); self.allocator.free(@constCast(self.vertex_attributes)); self.allocator.free(@constCast(self.bindings)); freeOpt(self.allocator, self.diagnostic_id); self.* = undefined; } pub fn setBorrowedBytes(self: *RenderArtifact, bytes: []const u8) void { self.clearPayload(); self.payload = .{ .bytes = bytes }; self.payload_ownership = .borrowed; } pub fn setOwnedBytes(self: *RenderArtifact, bytes: []const u8) BackendError!void { const owned = self.allocator.dupe(u8, bytes) catch return error.OutOfMemory; self.clearPayload(); self.payload = .{ .bytes = owned }; self.payload_ownership = .owned; } pub fn setBorrowedWords(self: *RenderArtifact, words: []const u32) void { self.clearPayload(); self.payload = .{ .words_u32 = words }; self.payload_ownership = .borrowed; } pub fn setOwnedWords(self: *RenderArtifact, words: []const u32) BackendError!void { const owned = self.allocator.dupe(u32, words) catch return error.OutOfMemory; self.clearPayload(); self.payload = .{ .words_u32 = owned }; self.payload_ownership = .owned; } pub fn setBorrowedText(self: *RenderArtifact, text: []const u8) void { self.clearPayload(); self.payload = .{ .text = text }; self.payload_ownership = .borrowed; } pub fn setOwnedText(self: *RenderArtifact, text: []const u8) BackendError!void { const owned = self.allocator.dupe(u8, text) catch return error.OutOfMemory; self.clearPayload(); self.payload = .{ .text = owned }; self.payload_ownership = .owned; } pub fn setExternalPayload(self: *RenderArtifact, payload: ExternalPayload, ownership: PayloadOwnership) BackendError!void { if (ownership == .owned and payload.deinit_fn == null) return error.MissingPayloadDeinit; self.clearPayload(); self.payload = .{ .external = payload }; self.payload_ownership = ownership; } fn clearPayload(self: *RenderArtifact) void { if (self.payload_ownership == .owned) { switch (self.payload) { .bytes => |bytes| freeString(self.allocator, bytes), .words_u32 => |words| self.allocator.free(@constCast(words)), .text => |text| freeString(self.allocator, text), .external => |payload| if (payload.deinit_fn) |deinit_fn| deinit_fn(self.allocator, payload.ptr), .none => {}, } } self.payload = .none; self.payload_ownership = .borrowed; }};/// A pipeline ready to draw, with the facts a pass checks its draws against.pub const LoadedRenderArtifact = struct { id: BackendObjectId, backend: BackendKind, format: RenderArtifactFormat, target_format: TextureFormat, depth: ?RenderDepthState = null, vertex_buffer_count: u32 = 0, binding_count: u32 = 0, push_constant_bytes: u32 = 0, /// The facts a backend must report for a pipeline it loaded from `artifact`. pub fn describing(artifact: *const RenderArtifact, id: BackendObjectId) LoadedRenderArtifact { return .{ .id = id, .backend = artifact.backend, .format = artifact.format, .target_format = artifact.target_format, .depth = artifact.depth, .vertex_buffer_count = @intCast(artifact.vertex_layouts.len), .binding_count = @intCast(artifact.bindings.len), .push_constant_bytes = artifact.push_constant_bytes, }; }};/// A caller holds this value to name one device buffer in later transfers and launches. The value/// names one buffer by id, with its backend, its size in bytes and who owns its memory. The backend/// handle that created the buffer owns the id until the caller passes it to `destroyObject`.pub const BufferHandle = struct { id: BackendObjectId, backend: BackendKind, byte_size: usize, ownership: BufferOwnership,};/// A caller holds this value to order work on one device queue. The value names one ordered queue/// of device work by id and backend. The backend handle that created the queue owns the id until/// the caller passes it to `destroyObject`.pub const StreamHandle = struct { id: BackendObjectId, backend: BackendKind,};/// A caller holds this value to mark a point in queued device work and wait for it. The value names/// one device event by id and backend. The backend handle that created the event owns the id until/// the caller passes it to `destroyObject`.pub const EventHandle = struct { id: BackendObjectId, backend: BackendKind,};pub const SurfaceHandle = struct { id: BackendObjectId, backend: BackendKind, platform: SurfacePlatformKind, extent: SurfaceExtent, format: TextureFormat, color_space: ColorSpace = .srgb, present_mode: PresentMode = .fifo, generation: u64 = 1,};pub const TextureHandle = struct { id: BackendObjectId, backend: BackendKind, extent: TextureExtent, format: TextureFormat, usage: TextureUsage, sample_count: u32 = 1, ownership: TextureOwnership = .backend,};pub const TextureView = struct { texture: TextureHandle, format: TextureFormat, base_mip_level: u32 = 0, mip_level_count: u32 = 1, base_array_layer: u32 = 0, array_layer_count: u32 = 1,};pub const SurfaceFrame = struct { id: BackendObjectId, backend: BackendKind, surface: SurfaceHandle, texture: TextureHandle, view: TextureView, index: u32 = 0, generation: u64 = 1, token: u64 = 0,};pub const BufferAllocation = struct { byte_size: usize, alignment: u32 = 1, dtype: ?DType = null, element_count: ?u64 = null,};/// A caller fills this request to let a backend use caller memory as a buffer without copying. The/// request carries the caller's bytes, their alignment, and an optional element type and element/// count. The caller keeps the bytes alive and at the same address until `destroyObject` releases/// the handle the import returned. The backend never frees the bytes, and the CPU backend frees/// nothing when it destroys a borrowed buffer.pub const BufferImport = struct { bytes: []u8, alignment: u32 = 1, dtype: ?DType = null, element_count: ?u64 = null,};pub const TextureAllocation = struct { extent: TextureExtent, format: TextureFormat, usage: TextureUsage, sample_count: u32 = 1,};pub const SurfaceCreationRequest = struct { platform: SurfacePlatform, extent: SurfaceExtent, format: TextureFormat, color_space: ColorSpace = .srgb, present_mode: PresentMode = .fifo, alpha_mode: SurfaceAlphaMode = .solid, usage: TextureUsage = .{ .present = true, .copy_dst = true }, max_frames_in_flight: u32 = 2,};pub const SurfaceFrameAcquireRequest = struct { surface: SurfaceHandle,};pub const PresentRequest = struct { surface: SurfaceHandle, frame: SurfaceFrame, wait_events: []const EventHandle = &.{}, signal_event: ?EventHandle = null,};pub const SurfaceClearColor = struct { r: f32 = 0, g: f32 = 0, b: f32 = 0, a: f32 = 1, pub fn valid(self: SurfaceClearColor) bool { return std.math.isFinite(self.r) and std.math.isFinite(self.g) and std.math.isFinite(self.b) and std.math.isFinite(self.a); }};pub const SurfaceFrameWriteOp = union(enum) { clear: SurfaceClearColor, copy_buffer: BufferHandle,};pub const SurfaceFrameWriteRequest = struct { surface: SurfaceHandle, frame: SurfaceFrame, operations: []const SurfaceFrameWriteOp, wait_events: []const EventHandle = &.{}, signal_event: ?EventHandle = null,};pub const StreamAllocation = struct {};pub const EventAllocation = struct {};pub const BufferBinding = struct { handle: BufferHandle, access: BufferAccess, ownership: BufferOwnership, byte_size: usize,};test "bufferArgumentCount subtracts scalars and rejects inverted counts" { var artifact = try KernelArtifact.init(std.testing.allocator, .{ .backend = .vulkan, .format = .vulkan_spirv, .entry_name = "kernel", .argument_count = 5, .scalar_argument_count = 3, }); defer artifact.deinit(); try std.testing.expectEqual(@as(u32, 2), try artifact.bufferArgumentCount()); var inverted = try KernelArtifact.init(std.testing.allocator, .{ .backend = .vulkan, .format = .vulkan_spirv, .entry_name = "kernel", .argument_count = 2, .scalar_argument_count = 3, }); defer inverted.deinit(); try std.testing.expectError(error.InvalidArtifact, inverted.bufferArgumentCount());}pub const BufferWriteRequest = struct { handle: BufferHandle, bytes: []const u8,};pub const BufferFillRequest = struct { handle: BufferHandle, pattern: u32,};pub const BufferReadRequest = struct { handle: BufferHandle, bytes: []u8,};pub const LaunchRequest = struct { artifact: *const KernelArtifact, loaded_artifact: ?LoadedArtifact = null, buffers: []const BufferBinding, scalar_arguments: []const choir_abi.ScalarArgument = &.{}, geometry: choir_abi.LaunchGeometry, stream: ?StreamHandle = null, wait_events: []const EventHandle = &.{}, signal_event: ?EventHandle = null, diagnostic_id: ?[]const u8 = null,};/// Records `pass` and submits it once.pub const RenderRequest = struct { pass: RenderPass, stream: ?StreamHandle = null, wait_events: []const EventHandle = &.{}, signal_event: ?EventHandle = null,};/// Submits a recorded pass again.pub const RenderBundleSubmit = struct { bundle: RenderBundle, stream: ?StreamHandle = null, wait_events: []const EventHandle = &.{}, signal_event: ?EventHandle = null,};/// Replaces every texel of `texture` with `bytes`, rows tightly packed from the top.pub const TextureWriteRequest = struct { texture: TextureHandle, bytes: []const u8,};/// Copies every texel of `texture` into `bytes`, rows tightly packed from the top.pub const TextureReadRequest = struct { texture: TextureHandle, bytes: []u8,};pub const CompileRequest = struct { kernel_name: []const u8, requested_format: ArtifactFormat, argument_count: u32 = 0, scalar_argument_count: u32 = 0, required_dtypes: DTypeSet = .{}, required_features: choir_abi.Features = .{}, required_subgroup: choir_abi.SubgroupRequirements = .{}, /// Layout the emitter gave the entry's push-constant block, if any. push_constants: choir_abi.PushConstants = .{}, diagnostic_id: ?[]const u8 = null, payload: CompilePayload = .none,};pub const CompilePayload = union(enum) { none, bytes: []const u8, words_u32: []const u32, text: []const u8,};pub const SyncRequest = struct { scope: SyncScope, stream: ?StreamHandle = null, event: ?EventHandle = null, pub fn valid(self: SyncRequest) bool { return switch (self.scope) { .default_stream, .device => self.stream == null and self.event == null, .stream => self.stream != null and self.event == null, .event => self.stream == null and self.event != null, }; }};pub const EventQueryRequest = struct { event: EventHandle,};pub const EventRecordRequest = struct { stream: StreamHandle, event: EventHandle,};pub const EventElapsedRequest = struct { start: EventHandle, end: EventHandle,};pub const BackendVTable = struct { query_capabilities: *const fn (*anyopaque) BackendError!BackendCapabilities, create_artifact: ?*const fn (*anyopaque, CompileRequest) BackendError!KernelArtifact = null, load_artifact: ?*const fn (*anyopaque, *const KernelArtifact) BackendError!LoadedArtifact = null, create_render_artifact: ?*const fn (*anyopaque, RenderPipelineDesc) BackendError!RenderArtifact = null, load_render_artifact: ?*const fn (*anyopaque, *const RenderArtifact) BackendError!LoadedRenderArtifact = null, allocate_buffer: ?*const fn (*anyopaque, BufferAllocation) BackendError!BufferHandle = null, import_buffer: ?*const fn (*anyopaque, BufferImport) BackendError!BufferHandle = null, allocate_texture: ?*const fn (*anyopaque, TextureAllocation) BackendError!TextureHandle = null, create_surface: ?*const fn (*anyopaque, SurfaceCreationRequest) BackendError!SurfaceHandle = null, destroy_surface: ?*const fn (*anyopaque, SurfaceHandle) BackendError!void = null, destroy_texture: ?*const fn (*anyopaque, TextureHandle) BackendError!void = null, acquire_surface_frame: ?*const fn (*anyopaque, SurfaceFrameAcquireRequest) BackendError!SurfaceFrame = null, present_surface_frame: ?*const fn (*anyopaque, PresentRequest) BackendError!void = null, write_surface_frame: ?*const fn (*anyopaque, SurfaceFrameWriteRequest) BackendError!void = null, create_stream: ?*const fn (*anyopaque, StreamAllocation) BackendError!StreamHandle = null, create_event: ?*const fn (*anyopaque, EventAllocation) BackendError!EventHandle = null, write_buffer: ?*const fn (*anyopaque, BufferWriteRequest) BackendError!void = null, fill_buffer: ?*const fn (*anyopaque, BufferFillRequest) BackendError!void = null, read_buffer: ?*const fn (*anyopaque, BufferReadRequest) BackendError!void = null, launch: ?*const fn (*anyopaque, LaunchRequest) BackendError!void = null, render: ?*const fn (*anyopaque, RenderRequest) BackendError!void = null, create_render_bindings: ?*const fn (*anyopaque, RenderBindingsRequest) BackendError!RenderBindings = null, record_render_bundle: ?*const fn (*anyopaque, RenderPass) BackendError!RenderBundle = null, submit_render_bundle: ?*const fn (*anyopaque, RenderBundleSubmit) BackendError!void = null, write_texture: ?*const fn (*anyopaque, TextureWriteRequest) BackendError!void = null, read_texture: ?*const fn (*anyopaque, TextureReadRequest) BackendError!void = null, synchronize: ?*const fn (*anyopaque, SyncRequest) BackendError!void = null, query_event: ?*const fn (*anyopaque, EventQueryRequest) BackendError!bool = null, record_event: ?*const fn (*anyopaque, EventRecordRequest) BackendError!void = null, elapsed_event_ns: ?*const fn (*anyopaque, EventElapsedRequest) BackendError!u64 = null, destroy_object: ?*const fn (*anyopaque, BackendObjectId) void = null, deinit: ?*const fn (*anyopaque, Allocator) void = null,};pub const BackendHandle = struct { ptr: *anyopaque, vtable: *const BackendVTable, kind: ?BackendKind = null, pub fn backendKind(self: BackendHandle) ?BackendKind { return self.kind; } pub fn queryCapabilities(self: BackendHandle) BackendError!BackendCapabilities { return self.vtable.query_capabilities(self.ptr); } pub fn createArtifact(self: BackendHandle, request: CompileRequest) BackendError!KernelArtifact { const caps = try self.queryCapabilities(); try caps.validateCompileRequest(request); const create = self.vtable.create_artifact orelse return error.UnsupportedOperation; var artifact = try create(self.ptr, request); artifact.interface = .{ .features = request.required_features, .subgroup = request.required_subgroup, .push_constants = request.push_constants, }; return artifact; } pub fn loadArtifact(self: BackendHandle, artifact: *const KernelArtifact) BackendError!LoadedArtifact { try self.expectArtifactBackend(artifact); if (!artifact.interface.push_constants.valid()) return error.InvalidArtifact; const caps = try self.queryCapabilities(); if (!caps.supportsFeatures(artifact.interface.features)) return error.CapabilityMismatch; if (!caps.supportsSubgroup(artifact.interface.subgroup)) return error.CapabilityMismatch; const load = self.vtable.load_artifact orelse return error.UnsupportedOperation; return load(self.ptr, artifact); } pub fn createRenderArtifact(self: BackendHandle, desc: RenderPipelineDesc) BackendError!RenderArtifact { const caps = try self.queryCapabilities(); try caps.validateRenderPipelineDesc(desc); const create = self.vtable.create_render_artifact orelse return error.UnsupportedOperation; var artifact = try create(self.ptr, desc); errdefer artifact.deinit(); try self.expectRenderArtifactBackend(&artifact); if (artifact.format != desc.format) return error.InvalidRenderArtifact; if (artifact.target_format != desc.target_format) return error.InvalidRenderArtifact; if (artifact.blend_mode != desc.blend_mode) return error.InvalidRenderArtifact; if (artifact.topology != desc.topology) return error.InvalidRenderArtifact; return artifact; } pub fn loadRenderArtifact(self: BackendHandle, artifact: *const RenderArtifact) BackendError!LoadedRenderArtifact { try self.expectRenderArtifactBackend(artifact); if (artifact.push_extent > artifact.push_constant_bytes) return error.PushConstantRangeExceeded; const load = self.vtable.load_render_artifact orelse return error.UnsupportedOperation; const loaded = try load(self.ptr, artifact); try self.expectLoadedRenderArtifactBackend(loaded); const expected = LoadedRenderArtifact.describing(artifact, loaded.id); if (!std.meta.eql(loaded, expected)) return error.InvalidRenderArtifact; return loaded; } pub fn createRenderBindings(self: BackendHandle, request: RenderBindingsRequest) BackendError!RenderBindings { try self.expectRenderArtifactBackend(request.artifact); try self.expectLoadedRenderArtifactBackend(request.pipeline); for (request.resources) |resource| switch (resource) { .uniform_buffer, .storage_buffer => |buffer| try self.expectBufferBackend(buffer), .sampled_texture => |sampled| try self.expectTextureBackend(sampled.texture), .storage_texture => |texture| try self.expectTextureBackend(texture), }; const caps = try self.queryCapabilities(); try caps.validateRenderBindings(request); const create = self.vtable.create_render_bindings orelse return error.UnsupportedOperation; const bindings = try create(self.ptr, request); if (self.kind) |kind| { if (bindings.backend != kind) return error.CapabilityMismatch; } if (bindings.pipeline != request.pipeline.id) return error.InvalidRenderArtifact; return bindings; } pub fn allocateBuffer(self: BackendHandle, request: BufferAllocation) BackendError!BufferHandle { const caps = try self.queryCapabilities(); try caps.validateBufferAllocation(request); const allocate = self.vtable.allocate_buffer orelse return error.UnsupportedOperation; const handle = try allocate(self.ptr, request); try self.expectBufferBackend(handle); if (handle.byte_size < request.byte_size) return error.InvalidBuffer; return handle; } /// A caller uses this to hand its own memory to a backend as a buffer without copying it. The /// call binds the caller's bytes and returns a buffer whose ownership is `borrowed_external` /// and whose size equals the length of the caller's bytes. The call first checks the size, /// element type and alignment against the backend's limits, and returns `error.InvalidBuffer` /// when the pointer lacks the requested alignment. A backend that does not offer imports /// returns `error.UnsupportedOperation`, and at present only the CPU backend offers them. pub fn importBuffer(self: BackendHandle, request: BufferImport) BackendError!BufferHandle { const caps = try self.queryCapabilities(); try caps.validateBufferAllocation(.{ .byte_size = request.bytes.len, .alignment = request.alignment, .dtype = request.dtype, .element_count = request.element_count, }); std.debug.assert(std.math.isPowerOfTwo(request.alignment)); if (!std.mem.isAligned(@intFromPtr(request.bytes.ptr), request.alignment)) { return error.InvalidBuffer; } const import_fn = self.vtable.import_buffer orelse return error.UnsupportedOperation; const handle = try import_fn(self.ptr, request); try self.expectBufferBackend(handle); if (handle.ownership != .borrowed_external) return error.InvalidBuffer; if (handle.byte_size != request.bytes.len) return error.InvalidBuffer; return handle; } pub fn allocateTexture(self: BackendHandle, request: TextureAllocation) BackendError!TextureHandle { const caps = try self.queryCapabilities(); try caps.validateTextureAllocation(request); const allocate = self.vtable.allocate_texture orelse return error.UnsupportedOperation; const handle = try allocate(self.ptr, request); try self.expectTextureBackend(handle); if (!sameTextureExtent(handle.extent, request.extent)) return error.InvalidTexture; if (handle.format != request.format) return error.InvalidTexture; if (!handle.usage.containsAll(request.usage)) return error.InvalidTexture; if (handle.sample_count != request.sample_count) return error.InvalidTexture; return handle; } pub fn createSurface(self: BackendHandle, request: SurfaceCreationRequest) BackendError!SurfaceHandle { const caps = try self.queryCapabilities(); try caps.validateSurfaceCreation(request); const create = self.vtable.create_surface orelse return error.UnsupportedOperation; const handle = try create(self.ptr, request); try self.expectSurfaceBackend(handle); if (handle.platform != request.platform.kind()) return error.InvalidSurface; if (!caps.surfaces.supportsExtent(handle.extent)) return error.InvalidSurface; if (handle.format != request.format) return error.InvalidSurface; if (handle.color_space != request.color_space) return error.InvalidSurface; if (handle.present_mode != request.present_mode) return error.InvalidSurface; return handle; } pub fn destroySurface(self: BackendHandle, surface: SurfaceHandle) BackendError!void { try self.expectSurfaceBackend(surface); const destroy = self.vtable.destroy_surface orelse return error.UnsupportedOperation; return destroy(self.ptr, surface); } pub fn destroyTexture(self: BackendHandle, texture: TextureHandle) BackendError!void { try self.expectTextureBackend(texture); const destroy = self.vtable.destroy_texture orelse return error.UnsupportedOperation; return destroy(self.ptr, texture); } pub fn acquireSurfaceFrame(self: BackendHandle, request: SurfaceFrameAcquireRequest) BackendError!SurfaceFrame { try self.expectSurfaceBackend(request.surface); const acquire = self.vtable.acquire_surface_frame orelse return error.UnsupportedOperation; const frame = try acquire(self.ptr, request); try self.expectSurfaceFrameBackend(frame); try expectFrameMatchesSurface(frame, request.surface); return frame; } pub fn presentSurfaceFrame(self: BackendHandle, request: PresentRequest) BackendError!void { try self.expectSurfaceBackend(request.surface); try self.expectSurfaceFrameBackend(request.frame); try expectFrameMatchesSurface(request.frame, request.surface); for (request.wait_events) |event| try self.expectEventBackend(event); if (request.signal_event) |event| try self.expectEventBackend(event); const present = self.vtable.present_surface_frame orelse return error.UnsupportedOperation; return present(self.ptr, request); } pub fn writeSurfaceFrame(self: BackendHandle, request: SurfaceFrameWriteRequest) BackendError!void { try self.expectSurfaceBackend(request.surface); try self.expectSurfaceFrameBackend(request.frame); try expectFrameMatchesSurface(request.frame, request.surface); try expectSurfaceFrameWriteRequest(request); for (request.operations) |op| switch (op) { .clear => {}, .copy_buffer => |buffer| try self.expectBufferBackend(buffer), }; for (request.wait_events) |event| try self.expectEventBackend(event); if (request.signal_event) |event| try self.expectEventBackend(event); const write = self.vtable.write_surface_frame orelse return error.UnsupportedOperation; return write(self.ptr, request); } pub fn createStream(self: BackendHandle, request: StreamAllocation) BackendError!StreamHandle { const caps = try self.queryCapabilities(); try caps.validateRuntimeRequirements(.{ .streams = true }); const create = self.vtable.create_stream orelse return error.UnsupportedOperation; return create(self.ptr, request); } pub fn createEvent(self: BackendHandle, request: EventAllocation) BackendError!EventHandle { const caps = try self.queryCapabilities(); try caps.validateRuntimeRequirements(.{ .events = true }); const create = self.vtable.create_event orelse return error.UnsupportedOperation; return create(self.ptr, request); } pub fn writeBuffer(self: BackendHandle, request: BufferWriteRequest) BackendError!void { try self.expectBufferBackend(request.handle); const write = self.vtable.write_buffer orelse return error.UnsupportedOperation; return write(self.ptr, request); } pub fn fillBuffer(self: BackendHandle, request: BufferFillRequest) BackendError!void { try self.expectBufferBackend(request.handle); const fill = self.vtable.fill_buffer orelse return error.UnsupportedOperation; return fill(self.ptr, request); } pub fn readBuffer(self: BackendHandle, request: BufferReadRequest) BackendError!void { try self.expectBufferBackend(request.handle); if (request.bytes.len < request.handle.byte_size) return error.ReadBufferDestinationTooSmall; const read = self.vtable.read_buffer orelse return error.UnsupportedOperation; return read(self.ptr, request); } pub fn launch(self: BackendHandle, request: LaunchRequest) BackendError!void { try self.expectLaunchBackends(request); try expectLaunchArgumentCount(request); const caps = try self.queryCapabilities(); try caps.validateLaunchGeometry(request.geometry); try caps.validateLaunchRuntime(request); const launch_fn = self.vtable.launch orelse return error.UnsupportedOperation; return launch_fn(self.ptr, request); } pub fn render(self: BackendHandle, request: RenderRequest) BackendError!void { try self.expectRenderPassBackends(request.pass); try self.expectSubmitBackends(request.stream, request.wait_events, request.signal_event); const caps = try self.queryCapabilities(); try caps.validateRenderPass(request.pass); try caps.validateSubmitRuntime(request.stream, request.wait_events, request.signal_event); const render_fn = self.vtable.render orelse return error.UnsupportedOperation; return render_fn(self.ptr, request); } pub fn recordRenderBundle(self: BackendHandle, pass: RenderPass) BackendError!RenderBundle { try self.expectRenderPassBackends(pass); const caps = try self.queryCapabilities(); try caps.validateRenderPass(pass); const record = self.vtable.record_render_bundle orelse return error.UnsupportedOperation; const bundle = try record(self.ptr, pass); if (self.kind) |kind| { if (bundle.backend != kind) return error.CapabilityMismatch; } if (bundle.draw_count != pass.draws.len) return error.RenderFailed; return bundle; } pub fn submitRenderBundle(self: BackendHandle, request: RenderBundleSubmit) BackendError!void { if (self.kind) |kind| { if (request.bundle.backend != kind) return error.RenderArgumentMismatch; } try self.expectSubmitBackends(request.stream, request.wait_events, request.signal_event); const caps = try self.queryCapabilities(); try caps.validateSubmitRuntime(request.stream, request.wait_events, request.signal_event); const submit = self.vtable.submit_render_bundle orelse return error.UnsupportedOperation; return submit(self.ptr, request); } pub fn writeTexture(self: BackendHandle, request: TextureWriteRequest) BackendError!void { try self.expectTextureBackend(request.texture); try BackendCapabilities.validateTextureTransfer(request.texture, request.bytes.len, .{ .copy_dst = true }); const write = self.vtable.write_texture orelse return error.UnsupportedOperation; return write(self.ptr, request); } pub fn readTexture(self: BackendHandle, request: TextureReadRequest) BackendError!void { try self.expectTextureBackend(request.texture); try BackendCapabilities.validateTextureTransfer(request.texture, request.bytes.len, .{ .copy_src = true }); const read = self.vtable.read_texture orelse return error.UnsupportedOperation; return read(self.ptr, request); } pub fn synchronize(self: BackendHandle, request: SyncRequest) BackendError!void { if (!request.valid()) return error.UnsupportedOperation; try self.expectSyncBackends(request); const caps = try self.queryCapabilities(); try caps.validateSyncRuntime(request); const sync = self.vtable.synchronize orelse return error.UnsupportedOperation; return sync(self.ptr, request); } pub fn queryEvent(self: BackendHandle, request: EventQueryRequest) BackendError!bool { try self.expectEventBackend(request.event); const caps = try self.queryCapabilities(); try caps.validateRuntimeRequirements(.{ .events = true }); const query = self.vtable.query_event orelse return error.UnsupportedOperation; return query(self.ptr, request); } pub fn recordEvent(self: BackendHandle, request: EventRecordRequest) BackendError!void { try self.expectStreamBackend(request.stream); try self.expectEventBackend(request.event); const caps = try self.queryCapabilities(); try caps.validateRuntimeRequirements(.{ .streams = true, .events = true }); const record = self.vtable.record_event orelse return error.UnsupportedOperation; return record(self.ptr, request); } pub fn elapsedEventNs(self: BackendHandle, request: EventElapsedRequest) BackendError!u64 { try self.expectEventBackend(request.start); try self.expectEventBackend(request.end); const caps = try self.queryCapabilities(); try caps.validateRuntimeRequirements(.{ .events = true }); const elapsed = self.vtable.elapsed_event_ns orelse return error.UnsupportedOperation; return elapsed(self.ptr, request); } /// A caller uses this to release a buffer, queue, event or loaded code that this backend handle /// created. The call releases one object this handle created. Before the call, the caller makes /// sure all launches, copies, queues and events still in flight have finished using the object. /// The call leaves state unchanged on a backend lacking a release function. pub fn destroyObject(self: BackendHandle, id: BackendObjectId) void { if (self.vtable.destroy_object) |destroy| destroy(self.ptr, id); } pub fn deinit(self: BackendHandle, allocator: Allocator) void { if (self.vtable.deinit) |deinit_fn| deinit_fn(self.ptr, allocator); } fn expectArtifactBackend(self: BackendHandle, artifact: *const KernelArtifact) BackendError!void { const kind = self.kind orelse return; if (artifact.backend != kind) return error.CapabilityMismatch; } fn expectLoadedArtifactBackend(self: BackendHandle, loaded: LoadedArtifact) BackendError!void { const kind = self.kind orelse return; if (loaded.backend != kind) return error.InvalidArtifact; } fn expectRenderArtifactBackend(self: BackendHandle, artifact: *const RenderArtifact) BackendError!void { const kind = self.kind orelse return; if (artifact.backend != kind) return error.CapabilityMismatch; } fn expectLoadedRenderArtifactBackend(self: BackendHandle, loaded: LoadedRenderArtifact) BackendError!void { const kind = self.kind orelse return; if (loaded.backend != kind) return error.InvalidRenderArtifact; } fn expectBufferBackend(self: BackendHandle, handle: BufferHandle) BackendError!void { const kind = self.kind orelse return; if (handle.backend != kind) return error.InvalidBuffer; } fn expectSurfaceBackend(self: BackendHandle, handle: SurfaceHandle) BackendError!void { const kind = self.kind orelse return; if (handle.backend != kind) return error.InvalidSurface; } fn expectTextureBackend(self: BackendHandle, handle: TextureHandle) BackendError!void { const kind = self.kind orelse return; if (handle.backend != kind) return error.InvalidTexture; } fn expectTextureViewBackend(self: BackendHandle, view: TextureView) BackendError!void { try self.expectTextureBackend(view.texture); } fn expectSurfaceFrameBackend(self: BackendHandle, frame: SurfaceFrame) BackendError!void { const kind = self.kind orelse return; if (frame.backend != kind) return error.InvalidSurfaceFrame; try self.expectSurfaceBackend(frame.surface); try self.expectTextureBackend(frame.texture); try self.expectTextureViewBackend(frame.view); } fn expectStreamBackend(self: BackendHandle, handle: StreamHandle) BackendError!void { const kind = self.kind orelse return; if (handle.backend != kind) return error.InvalidStream; } fn expectEventBackend(self: BackendHandle, handle: EventHandle) BackendError!void { const kind = self.kind orelse return; if (handle.backend != kind) return error.InvalidEvent; } fn expectLaunchBackends(self: BackendHandle, request: LaunchRequest) BackendError!void { try self.expectArtifactBackend(request.artifact); if (request.loaded_artifact) |loaded| { try self.expectLoadedArtifactBackend(loaded); if (loaded.backend != request.artifact.backend or loaded.format != request.artifact.format) { return error.InvalidArtifact; } } for (request.buffers) |binding| { try self.expectBufferBackend(binding.handle); } if (request.stream) |stream| try self.expectStreamBackend(stream); for (request.wait_events) |event| try self.expectEventBackend(event); if (request.signal_event) |event| try self.expectEventBackend(event); } fn expectRenderPassBackends(self: BackendHandle, pass: RenderPass) BackendError!void { try self.expectTextureViewBackend(pass.color.view); if (pass.depth) |depth| try self.expectTextureViewBackend(depth.view); for (pass.draws) |draw| { try self.expectLoadedRenderArtifactBackend(draw.pipeline); if (draw.bindings) |bindings| { if (self.kind) |kind| { if (bindings.backend != kind) return error.RenderArgumentMismatch; } } for (draw.vertex_buffers) |range| try self.expectBufferBackend(range.buffer); if (draw.index_buffer) |range| try self.expectBufferBackend(range.buffer); } } fn expectSubmitBackends( self: BackendHandle, stream: ?StreamHandle, wait_events: []const EventHandle, signal_event: ?EventHandle, ) BackendError!void { if (stream) |handle| try self.expectStreamBackend(handle); for (wait_events) |event| try self.expectEventBackend(event); if (signal_event) |event| try self.expectEventBackend(event); } fn expectLaunchArgumentCount(request: LaunchRequest) BackendError!void { const actual = request.buffers.len + request.scalar_arguments.len; const expected: usize = @intCast(request.artifact.argument_count); if (actual != expected) return error.LaunchArgumentMismatch; } fn expectSyncBackends(self: BackendHandle, request: SyncRequest) BackendError!void { switch (request.scope) { .default_stream, .device => {}, .stream => if (request.stream) |stream| try self.expectStreamBackend(stream), .event => if (request.event) |event| try self.expectEventBackend(event), } }};fn sameSurfaceExtent(a: SurfaceExtent, b: SurfaceExtent) bool { return a.width == b.width and a.height == b.height;}fn scissorWithin(scissor: RenderScissor, extent: TextureExtent) bool { const right = @as(u64, scissor.x) + scissor.width; const bottom = @as(u64, scissor.y) + scissor.height; return right <= extent.width and bottom <= extent.height;}fn sameTextureExtent(a: TextureExtent, b: TextureExtent) bool { return a.width == b.width and a.height == b.height and a.depth == b.depth;}fn expectFrameMatchesSurface(frame: SurfaceFrame, surface: SurfaceHandle) BackendError!void { if (frame.surface.id != surface.id) return error.InvalidSurfaceFrame; if (frame.surface.backend != surface.backend) return error.InvalidSurfaceFrame; if (frame.surface.generation != surface.generation) return error.SurfaceFrameExpired; if (frame.generation != surface.generation) return error.SurfaceFrameExpired; if (!sameSurfaceExtent(frame.surface.extent, surface.extent)) return error.SurfaceFrameExpired; if (frame.surface.format != surface.format) return error.SurfaceFrameExpired; if (frame.texture.backend != surface.backend) return error.InvalidTexture; if (frame.texture.format != surface.format) return error.InvalidTexture; if (frame.view.texture.id != frame.texture.id) return error.InvalidTexture; if (frame.view.format != frame.texture.format) return error.InvalidTexture;}fn expectSurfaceFrameWriteRequest(request: SurfaceFrameWriteRequest) BackendError!void { if (request.operations.len == 0) return error.InvalidSurfaceFrame; if (!request.frame.texture.usage.copy_dst) return error.InvalidTexture; const required_bytes = try textureByteSize(request.frame.texture); for (request.operations) |op| switch (op) { .clear => |color| if (!color.valid()) return error.InvalidSurfaceFrame, .copy_buffer => |buffer| if (buffer.byte_size < required_bytes) return error.InvalidBuffer, };}fn textureByteSize(texture: TextureHandle) BackendError!usize { if (!texture.extent.valid()) return error.InvalidTexture; const width: usize = @intCast(texture.extent.width); const height: usize = @intCast(texture.extent.height); const depth: usize = @intCast(texture.extent.depth); const wh = std.math.mul(usize, width, height) catch return error.InvalidTexture; const pixels = std.math.mul(usize, wh, depth) catch return error.InvalidTexture; return std.math.mul(usize, pixels, texture.format.texelBytes()) catch return error.InvalidTexture;}fn bitForDType(value: DType) u64 { const shift: u6 = @intCast(@backingInt(value)); return @as(u64, 1) << shift;}fn bitForArtifactFormat(value: ArtifactFormat) u64 { const shift: u6 = @intCast(@backingInt(value)); return @as(u64, 1) << shift;}fn bitForTextureFormat(value: TextureFormat) u64 { const shift: u6 = @intCast(@backingInt(value)); return @as(u64, 1) << shift;}fn bitForPresentMode(value: PresentMode) u64 { const shift: u6 = @intCast(@backingInt(value)); return @as(u64, 1) << shift;}fn bitForColorSpace(value: ColorSpace) u64 { const shift: u6 = @intCast(@backingInt(value)); return @as(u64, 1) << shift;}fn bitForSurfacePlatformKind(value: SurfacePlatformKind) u64 { const shift: u6 = @intCast(@backingInt(value)); return @as(u64, 1) << shift;}fn bitForRenderArtifactFormat(value: RenderArtifactFormat) u64 { const shift: u6 = @intCast(@backingInt(value)); return @as(u64, 1) << shift;}fn bitForRenderBlendMode(value: RenderBlendMode) u64 { const shift: u6 = @intCast(@backingInt(value)); return @as(u64, 1) << shift;}fn bitForRenderPrimitiveTopology(value: RenderPrimitiveTopology) u64 { const shift: u6 = @intCast(@backingInt(value)); return @as(u64, 1) << shift;}fn bitForRenderVertexFormat(value: RenderVertexFormat) u64 { const shift: u6 = @intCast(@backingInt(value)); return @as(u64, 1) << shift;}fn bitForRenderBindingKind(value: RenderBindingKind) u64 { const shift: u6 = @intCast(@backingInt(value)); return @as(u64, 1) << shift;}fn bitForRenderIndexFormat(value: RenderIndexFormat) u64 { const shift: u6 = @intCast(@backingInt(value)); return @as(u64, 1) << shift;}fn renderVertexFormatByteSize(format: RenderVertexFormat) u32 { return switch (format) { .float32, .uint32 => 4, .float32x2, .uint32x2 => 8, .float32x3 => 12, .float32x4, .uint32x4 => 16, };}fn dupeOpt(allocator: Allocator, value: ?[]const u8) Allocator.Error!?[]const u8 { return if (value) |actual| try dupeString(allocator, actual) else null;}fn freeOpt(allocator: Allocator, value: ?[]const u8) void { if (value) |actual| freeString(allocator, actual);}fn dupeString(allocator: Allocator, value: []const u8) Allocator.Error![]const u8 { return try allocator.dupe(u8, value);}fn freeString(allocator: Allocator, value: []const u8) void { allocator.free(@constCast(value));}test "capabilities record accelerator facts in machine-readable sets" { const caps = BackendCapabilities{ .identity = .{ .backend = .vulkan, .family = .vulkan, .name = "test-vulkan-device", .vendor_id = 0x10de, }, .memory = .{ .global_bytes = 8 * 1024 * 1024 * 1024, .max_allocation_bytes = 1024 * 1024 * 1024, .shared_memory_per_threadgroup_bytes = 32 * 1024, .min_buffer_alignment = 256, .host_visible_device_memory = true, }, .subgroup = .{ .supported = true, .size_min = 32, .size_max = 32, .shuffle = true, .ballot = true, .arithmetic = true, }, .threadgroup = .{ .max_threads = 256, .max_blocks = .{ 65535, 65535, 65535 }, .max_threads_per_dim = .{ 256, 256, 64 }, .max_grid_per_dim = .{ 65535, 65535, 65535 }, .shared_memory_bytes = 32 * 1024, }, .dtypes = DTypeSet.init(&.{ .f16, .f32, .i32 }), .layouts = .{ .row_major = true, .compact_strides = true, .broadcast_strides = true, .tiled = true, }, .runtime = .{ .driver_loaded = true, .device_context = true, .streams = true, .events = true, .timeline_events = true, }, .features = .{ .atomic_i32 = true, .atomic_u32 = true, .atomic_index = true, .atomic_f32_add_device = true, .atomic_f32_add_shared = true, .async_copy = true, }, .artifact_formats = ArtifactFormatSet.init(&.{.vulkan_spirv}), }; try std.testing.expect(caps.supportsDType(.f32)); try std.testing.expect(!caps.supportsDType(.f64)); try std.testing.expect(caps.supportsArtifactFormat(.vulkan_spirv)); try std.testing.expect(!caps.supportsArtifactFormat(.cuda_ptx)); try std.testing.expect(caps.runtime.timeline_events); try std.testing.expect(caps.layouts.tiled);}test "backend contract keeps native cpu distinct from external" { try std.testing.expectEqual(@as(u8, 0), @backingInt(BackendKind.cuda)); try std.testing.expectEqual(@as(u8, 1), @backingInt(BackendKind.vulkan)); try std.testing.expectEqual(@as(u8, 2), @backingInt(BackendKind.metal)); try std.testing.expectEqual(@as(u8, 3), @backingInt(BackendKind.external)); try std.testing.expectEqual(@as(u8, 4), @backingInt(BackendKind.webgpu)); try std.testing.expectEqual(@as(u8, 5), @backingInt(BackendKind.cpu)); try std.testing.expectEqual(@as(u8, 6), @backingInt(BackendKind.wasm)); try std.testing.expectEqual(@as(u8, 0), @backingInt(DeviceFamily.nvidia_cuda)); try std.testing.expectEqual(@as(u8, 1), @backingInt(DeviceFamily.vulkan)); try std.testing.expectEqual(@as(u8, 2), @backingInt(DeviceFamily.apple_metal)); try std.testing.expectEqual(@as(u8, 3), @backingInt(DeviceFamily.external)); try std.testing.expectEqual(@as(u8, 4), @backingInt(DeviceFamily.webgpu)); try std.testing.expectEqual(@as(u8, 5), @backingInt(DeviceFamily.native_cpu)); try std.testing.expectEqual(@as(u8, 6), @backingInt(DeviceFamily.webassembly)); try std.testing.expectEqual(@as(u8, 0), @backingInt(ArtifactFormat.cuda_ptx)); try std.testing.expectEqual(@as(u8, 1), @backingInt(ArtifactFormat.cuda_cubin)); try std.testing.expectEqual(@as(u8, 2), @backingInt(ArtifactFormat.vulkan_spirv)); try std.testing.expectEqual(@as(u8, 3), @backingInt(ArtifactFormat.metal_msl)); try std.testing.expectEqual(@as(u8, 4), @backingInt(ArtifactFormat.metal_metallib)); try std.testing.expectEqual(@as(u8, 5), @backingInt(ArtifactFormat.external)); try std.testing.expectEqual(@as(u8, 6), @backingInt(ArtifactFormat.webgpu_wgsl)); try std.testing.expectEqual(@as(u8, 7), @backingInt(ArtifactFormat.cpu_machine_code)); try std.testing.expectEqual(@as(u8, 8), @backingInt(ArtifactFormat.cpu_object)); try std.testing.expectEqual(@as(u8, 9), @backingInt(ArtifactFormat.webassembly_module)); try std.testing.expectEqual(DeviceFamily.native_cpu, familyForBackendKind(.cpu)); try std.testing.expectEqual(DeviceFamily.webgpu, familyForBackendKind(.webgpu)); try std.testing.expectEqual(DeviceFamily.webassembly, familyForBackendKind(.wasm)); try std.testing.expectEqual(DeviceFamily.external, familyForBackendKind(.external)); try std.testing.expect(artifactFormatIsNativeCpu(.cpu_object)); try std.testing.expect(artifactFormatIsNativeCpu(.cpu_machine_code)); try std.testing.expect(!artifactFormatIsNativeCpu(.cuda_ptx)); try std.testing.expect(!artifactFormatIsNativeCpu(.external)); try std.testing.expect(artifactFormatUsesHostLoopLaunch(.cpu_object)); try std.testing.expect(artifactFormatUsesHostLoopLaunch(.cpu_machine_code)); try std.testing.expect(artifactFormatUsesHostLoopLaunch(.webassembly_module)); try std.testing.expect(!artifactFormatUsesHostLoopLaunch(.webgpu_wgsl));}test "native cpu capabilities validate object and machine-code compile requests" { const caps = BackendCapabilities{ .identity = .{ .backend = .cpu, .family = .native_cpu, .name = "native-cpu", }, .threadgroup = .{ .max_threads = 1, .max_blocks = .{ 1, 1, 1 }, .max_threads_per_dim = .{ 1, 1, 1 }, .max_grid_per_dim = .{ 1, 1, 1 }, }, .dtypes = DTypeSet.init(&.{ .i32, .u32, .f32, .f64 }), .artifact_formats = ArtifactFormatSet.init(&.{ .cpu_object, .cpu_machine_code }), }; try caps.validateCompileRequest(.{ .kernel_name = "add", .requested_format = .cpu_object, .required_dtypes = DTypeSet.init(&.{ .f32, .f64 }), }); try caps.validateCompileRequest(.{ .kernel_name = "add", .requested_format = .cpu_machine_code, .required_dtypes = DTypeSet.init(&.{.i32}), }); try std.testing.expectError(error.UnsupportedArtifactFormat, caps.validateCompileRequest(.{ .kernel_name = "add", .requested_format = .cuda_ptx, }));}test "capabilities validate compile requests and launch geometry" { const caps = BackendCapabilities{ .identity = .{ .backend = .cuda, .family = .nvidia_cuda, .name = "test-cuda-device", }, .threadgroup = .{ .max_threads = 256, .max_blocks = .{ 65_535, 65_535, 65_535 }, .max_threads_per_dim = .{ 256, 16, 16 }, .max_grid_per_dim = .{ 65_535, 65_535, 64 }, .shared_memory_bytes = 48 * 1024, }, .subgroup = .{ .supported = true, .size_min = 32, .size_max = 32, .shuffle = true, .ballot = true, .vote = true, .arithmetic = true, }, .dtypes = DTypeSet.init(&.{ .f32, .i32 }), .features = .{ .atomic_i32 = true, .atomic_u32 = true, .atomic_index = true, .atomic_f32_add_device = true, .atomic_f32_add_shared = true, .dynamic_shared_memory = true, }, .artifact_formats = ArtifactFormatSet.init(&.{.cuda_ptx}), }; try caps.validateCompileRequest(.{ .kernel_name = "add", .requested_format = .cuda_ptx, .required_dtypes = DTypeSet.init(&.{.f32}), .required_features = .{ .atomic_i32 = true }, .required_subgroup = .{ .supported = true, .arithmetic = true }, }); try std.testing.expectError(error.UnsupportedArtifactFormat, caps.validateCompileRequest(.{ .kernel_name = "add", .requested_format = .vulkan_spirv, })); try std.testing.expectError(error.CapabilityMismatch, caps.validateCompileRequest(.{ .kernel_name = "add", .requested_format = .cuda_ptx, .required_dtypes = DTypeSet.init(&.{.f64}), })); try std.testing.expectError(error.CapabilityMismatch, caps.validateCompileRequest(.{ .kernel_name = "add", .requested_format = .cuda_ptx, .required_features = .{ .async_copy = true }, })); try std.testing.expectError(error.CapabilityMismatch, caps.validateCompileRequest(.{ .kernel_name = "add", .requested_format = .cuda_ptx, .required_subgroup = .{ .scan = true }, })); try caps.validateLaunchGeometry(.{ .grid = .{ 16, 4, 1 }, .threadgroup = .{ 128, 1, 1 }, .dynamic_shared_memory_bytes = 1024, }); var no_dynamic_shared = caps; no_dynamic_shared.features.dynamic_shared_memory = false; try std.testing.expectError(error.CapabilityMismatch, no_dynamic_shared.validateLaunchGeometry(.{ .grid = .{ 16, 4, 1 }, .threadgroup = .{ 128, 1, 1 }, .dynamic_shared_memory_bytes = 1024, })); try std.testing.expectError(error.LaunchArgumentMismatch, caps.validateLaunchGeometry(.{ .grid = .{ 0, 1, 1 }, .threadgroup = .{ 1, 1, 1 }, })); try std.testing.expectError(error.LaunchArgumentMismatch, caps.validateLaunchGeometry(.{ .grid = .{ 1, 1, 1 }, .threadgroup = .{ 0, 1, 1 }, })); try std.testing.expectError(error.CapabilityMismatch, caps.validateLaunchGeometry(.{ .grid = .{ 1, 1, 1 }, .threadgroup = .{ 512, 1, 1 }, })); try std.testing.expectError(error.CapabilityMismatch, caps.validateLaunchGeometry(.{ .grid = .{ 1, 1, 65 }, .threadgroup = .{ 1, 1, 1 }, })); try std.testing.expectError(error.CapabilityMismatch, caps.validateLaunchGeometry(.{ .grid = .{ 1, 1, 1 }, .threadgroup = .{ 1, 1, 1 }, .dynamic_shared_memory_bytes = 64 * 1024, }));}test "kernel artifacts represent cuda vulkan metal and external payloads" { var ptx = try KernelArtifact.init(std.testing.allocator, .{ .backend = .cuda, .format = .cuda_ptx, .entry_name = "add_f32", .argument_count = 3, .diagnostic_id = "cuda/add_f32", }); defer ptx.deinit(); try ptx.setOwnedText("// ptx"); try std.testing.expectEqual(ArtifactFormat.cuda_ptx, ptx.format); try std.testing.expectEqualStrings("// ptx", ptx.payload.text); var spirv = try KernelArtifact.init(std.testing.allocator, .{ .backend = .vulkan, .format = .vulkan_spirv, .entry_name = "main", .argument_count = 4, }); defer spirv.deinit(); const words = [_]u32{ 0x07230203, 0x00010000 }; spirv.setBorrowedWords(&words); try std.testing.expectEqual(ArtifactFormat.vulkan_spirv, spirv.format); try std.testing.expectEqual(@as(u32, 0x07230203), spirv.payload.words_u32[0]); var metallib = try KernelArtifact.init(std.testing.allocator, .{ .backend = .metal, .format = .metal_metallib, .entry_name = "main0", .argument_count = 2, }); defer metallib.deinit(); try metallib.setOwnedBytes(&.{ 0xca, 0xfe, 0xba, 0xbe }); try std.testing.expectEqual(ArtifactFormat.metal_metallib, metallib.format); try std.testing.expectEqual(@as(u8, 0xca), metallib.payload.bytes[0]);}const OwnedExternalPayloadState = struct { destroyed: *bool, fn destroy(allocator: Allocator, ptr: *anyopaque) void { const state: *@This() = @ptrCast(@alignCast(ptr)); state.destroyed.* = true; allocator.destroy(state); }};test "owned external payload requires and runs destructor" { var artifact = try KernelArtifact.init(std.testing.allocator, .{ .backend = .external, .format = .external, .entry_name = "external", .argument_count = 0, }); var did_deinit = false; defer if (!did_deinit) artifact.deinit(); var marker: u8 = 0; try std.testing.expectError(error.MissingPayloadDeinit, artifact.setExternalPayload(.{ .ptr = &marker, .type_id = "test.Payload", }, .owned)); var destroyed = false; const state = try std.testing.allocator.create(OwnedExternalPayloadState); state.* = .{ .destroyed = &destroyed }; try artifact.setExternalPayload(.{ .ptr = state, .type_id = "test.Payload", .deinit_fn = OwnedExternalPayloadState.destroy, }, .owned); artifact.deinit(); did_deinit = true; try std.testing.expect(destroyed);}const FakeBackendState = struct { launched: bool = false, last_buffer_ownership: ?BufferOwnership = null, event_ready: bool = false, queried_event: ?BackendObjectId = null, recorded_event: ?BackendObjectId = null, record_stream: ?BackendObjectId = null, elapsed_start_event: ?BackendObjectId = null, elapsed_end_event: ?BackendObjectId = null, elapsed_ns: u64 = 0, next_id: BackendObjectId = 1, allocation_count: usize = 0, last_allocation: ?BufferAllocation = null, allocated_byte_size: ?usize = null, allocated_backend: BackendKind = .cuda, created_stream: bool = false, created_event: bool = false, supports_streams: bool = true, supports_events: bool = true, supports_timeline_events: bool = false, sync_count: usize = 0, last_sync_request: ?SyncRequest = null, global_bytes: ?u64 = null, max_allocation_bytes: ?u64 = null, min_buffer_alignment: u32 = 1, supports_textures: bool = true, supports_surfaces: bool = true, texture_allocate_count: usize = 0, created_surface_count: usize = 0, acquired_frame_count: usize = 0, present_count: usize = 0, surface_write_count: usize = 0, last_surface_write_frame_id: ?BackendObjectId = null, last_surface_write_op_count: usize = 0, destroyed_surface_count: usize = 0, destroyed_texture_count: usize = 0, allocated_texture_backend: BackendKind = .cuda, created_surface_backend: BackendKind = .cuda, created_surface_extent: ?SurfaceExtent = null, supports_raster: bool = true, render_create_count: usize = 0, render_load_count: usize = 0, render_count: usize = 0, created_render_backend: BackendKind = .cuda, loaded_render_backend: BackendKind = .cuda, last_render_draw_count: usize = 0, last_render_vertex_count: u32 = 0, last_render_instance_count: u32 = 0, render_bindings_count: usize = 0, bundle_record_count: usize = 0, bundle_submit_count: usize = 0, texture_write_count: usize = 0, texture_read_count: usize = 0,};fn fakeQueryCapabilities(ptr: *anyopaque) BackendError!BackendCapabilities { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); return .{ .identity = .{ .backend = .cuda, .family = .nvidia_cuda, .name = "fake-cuda", }, .memory = .{ .global_bytes = state.global_bytes, .max_allocation_bytes = state.max_allocation_bytes, .min_buffer_alignment = state.min_buffer_alignment, }, .dtypes = DTypeSet.init(&.{ .f32, .u32 }), .artifact_formats = ArtifactFormatSet.init(&.{.cuda_ptx}), .runtime = .{ .streams = state.supports_streams, .events = state.supports_events, .timeline_events = state.supports_timeline_events, }, .features = .{ .dynamic_shared_memory = true, }, .textures = if (state.supports_textures) .{ .supported = true, .formats = TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm, .depth32_float }), .usages = .{ .copy_src = true, .copy_dst = true, .sampled = true, .storage = true, .color_attachment = true, .depth_attachment = true, .present = true, }, .max_extent = .{ .width = 8192, .height = 8192, .depth = 16 }, .max_sample_count = 4, } else .{}, .surfaces = if (state.supports_surfaces) .{ .supported = true, .platforms = SurfacePlatformSet.init(&.{.headless}), .formats = TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm }), .color_spaces = ColorSpaceSet.init(&.{ .srgb, .linear }), .present_modes = PresentModeSet.init(&.{ .fifo, .mailbox }), .usages = .{ .copy_dst = true, .storage = true, .color_attachment = true, .present = true, }, .max_extent = .{ .width = 8192, .height = 8192 }, .max_frames_in_flight = 3, } else .{}, .raster = if (state.supports_raster) .{ .supported = true, .artifact_formats = RenderArtifactFormatSet.init(&.{.external}), .target_formats = TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm }), .depth_formats = TextureFormatSet.init(&.{.depth32_float}), .blend_modes = RenderBlendModeSet.init(&.{ .replace, .alpha_premultiplied }), .topologies = RenderPrimitiveTopologySet.init(&.{ .triangle_list, .triangle_strip }), .vertex_formats = RenderVertexFormatSet.init(&.{ .float32x2, .float32x4, .uint32 }), .binding_kinds = RenderBindingKindSet.init(&.{ .uniform_buffer, .sampled_texture }), .index_formats = RenderIndexFormatSet.init(&.{ .none, .u16, .u32 }), .max_vertex_buffers = 4, .max_vertex_attributes = 8, .max_bindings = 8, .instancing = true, } else .{}, .threadgroup = .{ .max_threads = 256, .max_blocks = .{ 65_535, 65_535, 65_535 }, .max_threads_per_dim = .{ 256, 16, 16 }, .max_grid_per_dim = .{ 65_535, 65_535, 64 }, .shared_memory_bytes = 48 * 1024, }, };}fn fakeLaunch(ptr: *anyopaque, request: LaunchRequest) BackendError!void { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); if (request.buffers.len + request.scalar_arguments.len != request.artifact.argument_count) { return error.LaunchArgumentMismatch; } state.launched = true; state.last_buffer_ownership = request.buffers[0].ownership;}fn fakeAllocateBuffer(ptr: *anyopaque, request: BufferAllocation) BackendError!BufferHandle { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); defer state.next_id += 1; state.allocation_count += 1; state.last_allocation = request; return .{ .id = state.next_id, .backend = state.allocated_backend, .byte_size = state.allocated_byte_size orelse request.byte_size, .ownership = .backend, };}fn fakeAllocateTexture(ptr: *anyopaque, request: TextureAllocation) BackendError!TextureHandle { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); defer state.next_id += 1; state.texture_allocate_count += 1; return .{ .id = state.next_id, .backend = state.allocated_texture_backend, .extent = request.extent, .format = request.format, .usage = request.usage, .sample_count = request.sample_count, .ownership = .backend, };}fn fakeCreateSurface(ptr: *anyopaque, request: SurfaceCreationRequest) BackendError!SurfaceHandle { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); defer state.next_id += 1; state.created_surface_count += 1; const extent = state.created_surface_extent orelse request.extent; return .{ .id = state.next_id, .backend = state.created_surface_backend, .platform = request.platform.kind(), .extent = extent, .format = request.format, .color_space = request.color_space, .present_mode = request.present_mode, .generation = 1, };}fn fakeDestroySurface(ptr: *anyopaque, _: SurfaceHandle) BackendError!void { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.destroyed_surface_count += 1;}fn fakeDestroyTexture(ptr: *anyopaque, _: TextureHandle) BackendError!void { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.destroyed_texture_count += 1;}fn fakeAcquireSurfaceFrame(ptr: *anyopaque, request: SurfaceFrameAcquireRequest) BackendError!SurfaceFrame { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); const texture_id = state.next_id; state.next_id += 1; const frame_id = state.next_id; state.next_id += 1; state.acquired_frame_count += 1; const texture = TextureHandle{ .id = texture_id, .backend = request.surface.backend, .extent = .{ .width = request.surface.extent.width, .height = request.surface.extent.height, .depth = 1, }, .format = request.surface.format, .usage = .{ .present = true, .color_attachment = true, .copy_dst = true }, .sample_count = 1, .ownership = .acquired_surface, }; const view = TextureView{ .texture = texture, .format = texture.format, }; return .{ .id = frame_id, .backend = request.surface.backend, .surface = request.surface, .texture = texture, .view = view, .generation = request.surface.generation, };}fn fakePresentSurfaceFrame(ptr: *anyopaque, _: PresentRequest) BackendError!void { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.present_count += 1;}fn fakeWriteSurfaceFrame(ptr: *anyopaque, request: SurfaceFrameWriteRequest) BackendError!void { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.surface_write_count += 1; state.last_surface_write_frame_id = request.frame.id; state.last_surface_write_op_count = request.operations.len;}fn fakeCreateRenderArtifact(ptr: *anyopaque, desc: RenderPipelineDesc) BackendError!RenderArtifact { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.render_create_count += 1; var artifact = RenderArtifact.init(std.testing.allocator, .{ .backend = state.created_render_backend, .pipeline = desc, }) catch return error.OutOfMemory; errdefer artifact.deinit(); artifact.setBorrowedText("fake-render"); return artifact;}fn fakeLoadRenderArtifact(ptr: *anyopaque, artifact: *const RenderArtifact) BackendError!LoadedRenderArtifact { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); defer state.next_id += 1; state.render_load_count += 1; var loaded = LoadedRenderArtifact.describing(artifact, state.next_id); loaded.backend = state.loaded_render_backend; return loaded;}fn fakeRender(ptr: *anyopaque, request: RenderRequest) BackendError!void { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.render_count += 1; state.last_render_draw_count = request.pass.draws.len; const first = request.pass.draws[0].range; state.last_render_vertex_count = first.vertex_count; state.last_render_instance_count = first.instance_count;}fn fakeCreateRenderBindings(ptr: *anyopaque, request: RenderBindingsRequest) BackendError!RenderBindings { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); defer state.next_id += 1; state.render_bindings_count += 1; return .{ .id = state.next_id, .backend = .cuda, .pipeline = request.pipeline.id };}fn fakeRecordRenderBundle(ptr: *anyopaque, pass: RenderPass) BackendError!RenderBundle { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); defer state.next_id += 1; state.bundle_record_count += 1; return .{ .id = state.next_id, .backend = .cuda, .draw_count = @intCast(pass.draws.len) };}fn fakeSubmitRenderBundle(ptr: *anyopaque, _: RenderBundleSubmit) BackendError!void { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.bundle_submit_count += 1;}fn fakeWriteTexture(ptr: *anyopaque, _: TextureWriteRequest) BackendError!void { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.texture_write_count += 1;}fn fakeReadTexture(ptr: *anyopaque, _: TextureReadRequest) BackendError!void { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.texture_read_count += 1;}fn fakeCreateStream(ptr: *anyopaque, _: StreamAllocation) BackendError!StreamHandle { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); defer state.next_id += 1; state.created_stream = true; return .{ .id = state.next_id, .backend = .cuda, };}fn fakeCreateEvent(ptr: *anyopaque, _: EventAllocation) BackendError!EventHandle { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); defer state.next_id += 1; state.created_event = true; return .{ .id = state.next_id, .backend = .cuda, };}fn fakeQueryEvent(ptr: *anyopaque, request: EventQueryRequest) BackendError!bool { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.queried_event = request.event.id; return state.event_ready;}fn fakeRecordEvent(ptr: *anyopaque, request: EventRecordRequest) BackendError!void { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.recorded_event = request.event.id; state.record_stream = request.stream.id;}fn fakeElapsedEventNs(ptr: *anyopaque, request: EventElapsedRequest) BackendError!u64 { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.elapsed_start_event = request.start.id; state.elapsed_end_event = request.end.id; return state.elapsed_ns;}fn fakeSynchronize(ptr: *anyopaque, request: SyncRequest) BackendError!void { const state: *FakeBackendState = @ptrCast(@alignCast(ptr)); state.sync_count += 1; state.last_sync_request = request;}const fake_vtable = BackendVTable{ .query_capabilities = fakeQueryCapabilities, .launch = fakeLaunch, .allocate_buffer = fakeAllocateBuffer, .allocate_texture = fakeAllocateTexture, .create_surface = fakeCreateSurface, .destroy_surface = fakeDestroySurface, .destroy_texture = fakeDestroyTexture, .acquire_surface_frame = fakeAcquireSurfaceFrame, .present_surface_frame = fakePresentSurfaceFrame, .write_surface_frame = fakeWriteSurfaceFrame, .create_render_artifact = fakeCreateRenderArtifact, .load_render_artifact = fakeLoadRenderArtifact, .render = fakeRender, .create_render_bindings = fakeCreateRenderBindings, .record_render_bundle = fakeRecordRenderBundle, .submit_render_bundle = fakeSubmitRenderBundle, .write_texture = fakeWriteTexture, .read_texture = fakeReadTexture, .create_stream = fakeCreateStream, .create_event = fakeCreateEvent, .query_event = fakeQueryEvent, .record_event = fakeRecordEvent, .elapsed_event_ns = fakeElapsedEventNs, .synchronize = fakeSynchronize,};test "capabilities validate buffer allocation requests" { const caps = BackendCapabilities{ .identity = .{ .backend = .cuda, .family = .nvidia_cuda, .name = "test-cuda-device", }, .memory = .{ .global_bytes = 256, .max_allocation_bytes = 128, .min_buffer_alignment = 64, }, .dtypes = DTypeSet.init(&.{ .f32, .u32 }), }; try caps.validateBufferAllocation(.{ .byte_size = 128, .alignment = 64, .dtype = .f32, .element_count = 32, }); try std.testing.expectError(error.InvalidBuffer, caps.validateBufferAllocation(.{ .byte_size = 0, .alignment = 64, })); try std.testing.expectError(error.InvalidBuffer, caps.validateBufferAllocation(.{ .byte_size = 16, .alignment = 0, })); try std.testing.expectError(error.InvalidBuffer, caps.validateBufferAllocation(.{ .byte_size = 16, .alignment = 96, })); try std.testing.expectError(error.CapabilityMismatch, caps.validateBufferAllocation(.{ .byte_size = 16, .alignment = 32, })); try std.testing.expectError(error.CapabilityMismatch, caps.validateBufferAllocation(.{ .byte_size = 129, .alignment = 64, })); const smaller_global = BackendCapabilities{ .identity = caps.identity, .memory = .{ .global_bytes = 64, .min_buffer_alignment = 64, }, .dtypes = DTypeSet.init(&.{.f32}), }; try std.testing.expectError(error.CapabilityMismatch, smaller_global.validateBufferAllocation(.{ .byte_size = 65, .alignment = 64, })); try std.testing.expectError(error.CapabilityMismatch, caps.validateBufferAllocation(.{ .byte_size = 8, .alignment = 64, .dtype = .f64, .element_count = 1, })); try std.testing.expectError(error.InvalidBuffer, caps.validateBufferAllocation(.{ .byte_size = 12, .alignment = 64, .dtype = .f32, .element_count = 4, }));}test "allocateBuffer validates capabilities and returned handles" { var state = FakeBackendState{ .max_allocation_bytes = 128, .min_buffer_alignment = 64, }; const handle = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; try std.testing.expectError(error.CapabilityMismatch, handle.allocateBuffer(.{ .byte_size = 256, .alignment = 64, .dtype = .f32, .element_count = 64, })); try std.testing.expectEqual(@as(usize, 0), state.allocation_count); try std.testing.expectError(error.CapabilityMismatch, handle.allocateBuffer(.{ .byte_size = 16, .alignment = 32, .dtype = .f32, .element_count = 4, })); try std.testing.expectEqual(@as(usize, 0), state.allocation_count); const buffer = try handle.allocateBuffer(.{ .byte_size = 64, .alignment = 64, .dtype = .f32, .element_count = 16, }); try std.testing.expectEqual(@as(BackendObjectId, 1), buffer.id); try std.testing.expectEqual(@as(usize, 1), state.allocation_count); try std.testing.expectEqual(@as(usize, 64), state.last_allocation.?.byte_size); state.allocated_backend = .vulkan; try std.testing.expectError(error.InvalidBuffer, handle.allocateBuffer(.{ .byte_size = 64, .alignment = 64, .dtype = .f32, .element_count = 16, })); try std.testing.expectEqual(@as(usize, 2), state.allocation_count); state.allocated_backend = .cuda; state.allocated_byte_size = 32; try std.testing.expectError(error.InvalidBuffer, handle.allocateBuffer(.{ .byte_size = 64, .alignment = 64, .dtype = .f32, .element_count = 16, })); try std.testing.expectEqual(@as(usize, 3), state.allocation_count);}test "capabilities validate texture allocation and surface creation requests" { const caps = BackendCapabilities{ .identity = .{ .backend = .vulkan, .family = .vulkan, .name = "test-vulkan-device", }, .textures = .{ .supported = true, .formats = TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm }), .usages = .{ .copy_src = true, .copy_dst = true, .sampled = true, .storage = true, .present = true }, .max_extent = .{ .width = 4096, .height = 4096, .depth = 4 }, .max_sample_count = 4, }, .surfaces = .{ .supported = true, .platforms = SurfacePlatformSet.init(&.{ .x11, .headless }), .formats = TextureFormatSet.init(&.{.bgra8_unorm}), .color_spaces = ColorSpaceSet.init(&.{.srgb}), .present_modes = PresentModeSet.init(&.{.fifo}), .usages = .{ .copy_dst = true, .color_attachment = true, .present = true }, .max_extent = .{ .width = 3840, .height = 2160 }, .max_frames_in_flight = 2, }, }; try caps.validateTextureAllocation(.{ .extent = .{ .width = 64, .height = 64, .depth = 1 }, .format = .rgba8_unorm, .usage = .{ .sampled = true, .copy_dst = true }, }); try std.testing.expectError(error.InvalidTexture, caps.validateTextureAllocation(.{ .extent = .{ .width = 0, .height = 64 }, .format = .rgba8_unorm, .usage = .{ .sampled = true }, })); try std.testing.expectError(error.InvalidTexture, caps.validateTextureAllocation(.{ .extent = .{ .width = 64, .height = 64 }, .format = .rgba8_unorm, .usage = .{}, })); try std.testing.expectError(error.CapabilityMismatch, caps.validateTextureAllocation(.{ .extent = .{ .width = 64, .height = 64 }, .format = .rgba8_srgb, .usage = .{ .sampled = true }, })); try std.testing.expectError(error.CapabilityMismatch, caps.validateTextureAllocation(.{ .extent = .{ .width = 64, .height = 64 }, .format = .rgba8_unorm, .usage = .{ .color_attachment = true }, })); try caps.validateSurfaceCreation(.{ .platform = .{ .x11 = .{ .display = 1, .window = 2 } }, .extent = .{ .width = 800, .height = 600 }, .format = .bgra8_unorm, .usage = .{ .present = true, .copy_dst = true }, }); try std.testing.expectError(error.InvalidSurface, caps.validateSurfaceCreation(.{ .platform = .{ .x11 = .{ .display = 1, .window = 2 } }, .extent = .{ .width = 800, .height = 0 }, .format = .bgra8_unorm, .usage = .{ .present = true }, })); try std.testing.expectError(error.InvalidSurface, caps.validateSurfaceCreation(.{ .platform = .{ .x11 = .{ .display = 1, .window = 2 } }, .extent = .{ .width = 800, .height = 600 }, .format = .bgra8_unorm, .usage = .{ .copy_dst = true }, })); try std.testing.expectError(error.CapabilityMismatch, caps.validateSurfaceCreation(.{ .platform = .{ .cocoa = .{ .layer = 3 } }, .extent = .{ .width = 800, .height = 600 }, .format = .bgra8_unorm, .usage = .{ .present = true }, })); try std.testing.expectError(error.CapabilityMismatch, caps.validateSurfaceCreation(.{ .platform = .{ .x11 = .{ .display = 1, .window = 2 } }, .extent = .{ .width = 800, .height = 600 }, .format = .rgba8_unorm, .usage = .{ .present = true }, }));}test "surface and texture handle methods validate capabilities and backend ownership" { var state = FakeBackendState{}; const handle = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; const texture = try handle.allocateTexture(.{ .extent = .{ .width = 128, .height = 64 }, .format = .rgba8_unorm, .usage = .{ .sampled = true, .copy_dst = true }, }); try std.testing.expectEqual(@as(usize, 1), state.texture_allocate_count); try std.testing.expectEqual(BackendKind.cuda, texture.backend); try std.testing.expectEqual(TextureOwnership.backend, texture.ownership); state.supports_textures = false; try std.testing.expectError(error.CapabilityMismatch, handle.allocateTexture(.{ .extent = .{ .width = 128, .height = 64 }, .format = .rgba8_unorm, .usage = .{ .sampled = true }, })); try std.testing.expectEqual(@as(usize, 1), state.texture_allocate_count); state.supports_textures = true; const surface = try handle.createSurface(.{ .platform = .{ .headless = .{} }, .extent = .{ .width = 320, .height = 180 }, .format = .rgba8_unorm, .usage = .{ .present = true, .copy_dst = true }, }); try std.testing.expectEqual(@as(usize, 1), state.created_surface_count); try std.testing.expectEqual(SurfacePlatformKind.headless, surface.platform); state.created_surface_extent = .{ .width = 640, .height = 360 }; const actual_extent_surface = try handle.createSurface(.{ .platform = .{ .headless = .{} }, .extent = .{ .width = 320, .height = 180 }, .format = .rgba8_unorm, .usage = .{ .present = true, .copy_dst = true }, }); try std.testing.expectEqual(@as(u32, 640), actual_extent_surface.extent.width); state.created_surface_extent = null; const frame = try handle.acquireSurfaceFrame(.{ .surface = surface }); try std.testing.expectEqual(@as(usize, 1), state.acquired_frame_count); try std.testing.expectEqual(surface.id, frame.surface.id); try std.testing.expectEqual(TextureOwnership.acquired_surface, frame.texture.ownership); const pixels = try handle.allocateBuffer(.{ .byte_size = @as(usize, frame.texture.extent.width) * @as(usize, frame.texture.extent.height) * 4, .alignment = 16, }); const ops = [_]SurfaceFrameWriteOp{ .{ .clear = .{ .r = 0.1, .g = 0.2, .b = 0.3, .a = 1.0 } }, .{ .copy_buffer = pixels }, }; try handle.writeSurfaceFrame(.{ .surface = surface, .frame = frame, .operations = &ops, }); try std.testing.expectEqual(@as(usize, 1), state.surface_write_count); try std.testing.expectEqual(frame.id, state.last_surface_write_frame_id.?); try std.testing.expectEqual(@as(usize, 2), state.last_surface_write_op_count); const small_buffer = BufferHandle{ .id = 900, .backend = .cuda, .byte_size = 4, .ownership = .backend, }; const small_ops = [_]SurfaceFrameWriteOp{.{ .copy_buffer = small_buffer }}; try std.testing.expectError(error.InvalidBuffer, handle.writeSurfaceFrame(.{ .surface = surface, .frame = frame, .operations = &small_ops, })); try std.testing.expectEqual(@as(usize, 1), state.surface_write_count); var no_copy_frame = frame; no_copy_frame.texture.usage.copy_dst = false; try std.testing.expectError(error.InvalidTexture, handle.writeSurfaceFrame(.{ .surface = surface, .frame = no_copy_frame, .operations = &ops, })); try std.testing.expectEqual(@as(usize, 1), state.surface_write_count); var stale_surface = surface; stale_surface.generation += 1; try std.testing.expectError(error.SurfaceFrameExpired, handle.presentSurfaceFrame(.{ .surface = stale_surface, .frame = frame, })); try std.testing.expectEqual(@as(usize, 0), state.present_count); try handle.presentSurfaceFrame(.{ .surface = surface, .frame = frame, }); try std.testing.expectEqual(@as(usize, 1), state.present_count); try handle.destroyTexture(texture); try std.testing.expectEqual(@as(usize, 1), state.destroyed_texture_count); try handle.destroySurface(surface); try std.testing.expectEqual(@as(usize, 1), state.destroyed_surface_count); state.allocated_texture_backend = .vulkan; try std.testing.expectError(error.InvalidTexture, handle.allocateTexture(.{ .extent = .{ .width = 128, .height = 64 }, .format = .rgba8_unorm, .usage = .{ .sampled = true }, })); state.allocated_texture_backend = .cuda; state.created_surface_backend = .vulkan; try std.testing.expectError(error.InvalidSurface, handle.createSurface(.{ .platform = .{ .headless = .{} }, .extent = .{ .width = 320, .height = 180 }, .format = .rgba8_unorm, .usage = .{ .present = true }, }));}test "capabilities validate render pipeline descriptors and requests" { const caps = BackendCapabilities{ .identity = .{ .backend = .vulkan, .family = .vulkan, .name = "test-vulkan-device", }, .raster = .{ .supported = true, .artifact_formats = RenderArtifactFormatSet.init(&.{.vulkan_spirv}), .target_formats = TextureFormatSet.init(&.{.rgba8_unorm}), .blend_modes = RenderBlendModeSet.init(&.{ .replace, .alpha_premultiplied }), .topologies = RenderPrimitiveTopologySet.init(&.{.triangle_list}), .vertex_formats = RenderVertexFormatSet.init(&.{ .float32x2, .float32x4 }), .binding_kinds = RenderBindingKindSet.init(&.{ .uniform_buffer, .sampled_texture }), .index_formats = RenderIndexFormatSet.init(&.{ .none, .u16 }), .max_vertex_buffers = 2, .max_vertex_attributes = 4, .max_bindings = 4, .instancing = true, }, }; const attributes = [_]RenderVertexAttribute{ .{ .location = 0, .format = .float32x2, .offset = 0 }, .{ .location = 1, .format = .float32x4, .offset = 8 }, }; const layouts = [_]RenderVertexBufferLayout{.{ .binding = 0, .stride = 24, .step_mode = .instance, .attribute_start = 0, .attribute_count = attributes.len, }}; const bindings = [_]RenderBindingDesc{.{ .group = 0, .binding = 0, .kind = .uniform_buffer, .access = .read_only, }}; const desc = RenderPipelineDesc{ .format = .vulkan_spirv, .vertex_entry_name = "quad_vs", .fragment_entry_name = "quad_fs", .target_format = .rgba8_unorm, .push_extent = 0, .blend_mode = .alpha_premultiplied, .topology = .triangle_list, .vertex_layouts = layouts[0..], .vertex_attributes = attributes[0..], .bindings = bindings[0..], }; try caps.validateRenderPipelineDesc(desc); var empty_entry = desc; empty_entry.fragment_entry_name = ""; try std.testing.expectError(error.InvalidRenderArtifact, caps.validateRenderPipelineDesc(empty_entry)); var bad_format = desc; bad_format.format = .webgpu_wgsl; try std.testing.expectError(error.CapabilityMismatch, caps.validateRenderPipelineDesc(bad_format)); const bad_attributes = [_]RenderVertexAttribute{ .{ .location = 0, .format = .float32x4, .offset = 16 }, }; const bad_layouts = [_]RenderVertexBufferLayout{.{ .binding = 0, .stride = 24, .attribute_start = 0, .attribute_count = bad_attributes.len, }}; var bad_offset = desc; bad_offset.vertex_layouts = bad_layouts[0..]; bad_offset.vertex_attributes = bad_attributes[0..]; try std.testing.expectError(error.InvalidRenderArtifact, caps.validateRenderPipelineDesc(bad_offset)); var color_as_depth = desc; color_as_depth.depth = .{ .format = .rgba8_unorm }; try std.testing.expectError(error.InvalidRenderArtifact, caps.validateRenderPipelineDesc(color_as_depth)); var unsupported_depth = desc; unsupported_depth.depth = .{}; try std.testing.expectError(error.CapabilityMismatch, caps.validateRenderPipelineDesc(unsupported_depth)); var depth_caps = caps; depth_caps.raster.depth_formats = TextureFormatSet.init(&.{.depth32_float}); try depth_caps.validateRenderPipelineDesc(unsupported_depth); var artifact = try RenderArtifact.init(std.testing.allocator, .{ .backend = .vulkan, .pipeline = desc, }); defer artifact.deinit(); const pipeline = LoadedRenderArtifact.describing(&artifact, 7); const target = TextureView{ .texture = .{ .id = 1, .backend = .vulkan, .extent = .{ .width = 320, .height = 180, .depth = 1 }, .format = .rgba8_unorm, .usage = .{ .color_attachment = true, .present = true }, }, .format = .rgba8_unorm, }; const vertex_buffer = RenderBufferRange{ .buffer = .{ .id = 2, .backend = .vulkan, .byte_size = 240, .ownership = .backend, } }; const pipeline_bindings = RenderBindings{ .id = 3, .backend = .vulkan, .pipeline = pipeline.id }; const draw = RenderDraw{ .pipeline = pipeline, .bindings = pipeline_bindings, .vertex_buffers = &.{vertex_buffer}, .range = .{ .vertex_count = 6, .instance_count = 2 }, }; const pass = RenderPass{ .color = .{ .view = target, .load = .{ .clear = .{} } }, .viewport = .{ .width = 320, .height = 180 }, .scissor = .{ .width = 320, .height = 180 }, .draws = &.{draw}, }; try caps.validateRenderPass(pass); var non_attachment = pass; non_attachment.color.view.texture.usage = .{ .present = true }; try std.testing.expectError(error.InvalidTexture, caps.validateRenderPass(non_attachment)); var mismatched_texture = pass; mismatched_texture.color.view.texture.format = .bgra8_unorm; try std.testing.expectError(error.InvalidTexture, caps.validateRenderPass(mismatched_texture)); var outside = pass; outside.scissor = .{ .x = 1, .width = 320, .height = 180 }; try std.testing.expectError(error.RenderArgumentMismatch, caps.validateRenderPass(outside)); const bad_draws = [_]RenderDraw{ .{ .pipeline = pipeline, .bindings = pipeline_bindings, .range = .{ .vertex_count = 6 } }, .{ .pipeline = pipeline, .vertex_buffers = &.{vertex_buffer}, .range = .{ .vertex_count = 6 } }, .{ .pipeline = pipeline, .bindings = pipeline_bindings, .vertex_buffers = &.{vertex_buffer}, .range = .{ .index_count = 6, .index_format = .u16 }, }, .{ .pipeline = pipeline, .bindings = .{ .id = 3, .backend = .vulkan, .pipeline = 8 }, .vertex_buffers = &.{vertex_buffer}, .range = .{ .vertex_count = 6 }, }, }; for (bad_draws) |bad| { var one = pass; one.draws = &.{bad}; try std.testing.expectError(error.RenderArgumentMismatch, caps.validateRenderPass(one)); } var depth_artifact = try RenderArtifact.init(std.testing.allocator, .{ .backend = .vulkan, .pipeline = unsupported_depth, }); defer depth_artifact.deinit(); var depth_draw = draw; depth_draw.pipeline = LoadedRenderArtifact.describing(&depth_artifact, 7); var no_depth_attachment = pass; no_depth_attachment.draws = &.{depth_draw}; try std.testing.expectError(error.RenderArgumentMismatch, depth_caps.validateRenderPass(no_depth_attachment)); var with_depth = no_depth_attachment; with_depth.depth = .{ .view = .{ .texture = .{ .id = 4, .backend = .vulkan, .extent = target.texture.extent, .format = .depth32_float, .usage = .{ .depth_attachment = true }, }, .format = .depth32_float, } }; try depth_caps.validateRenderPass(with_depth); var depth_without_pipeline_depth = with_depth; depth_without_pipeline_depth.draws = &.{draw}; try std.testing.expectError(error.RenderArgumentMismatch, depth_caps.validateRenderPass(depth_without_pipeline_depth)); var far_clear = with_depth; far_clear.depth.?.load = .{ .clear = 2 }; try std.testing.expectError(error.RenderArgumentMismatch, depth_caps.validateRenderPass(far_clear));}test "a render module reading push constants past the declared bytes is refused before the backend creates or loads it" { var state = FakeBackendState{}; const handle = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; const desc = RenderPipelineDesc{ .format = .external, .vertex_entry_name = "quad_vs", .fragment_entry_name = "quad_fs", .target_format = .rgba8_unorm, .push_extent = 4, }; try std.testing.expectError(error.PushConstantRangeExceeded, handle.createRenderArtifact(desc)); try std.testing.expectEqual(@as(usize, 0), state.render_create_count); var honest = desc; honest.push_extent = 0; var artifact = try handle.createRenderArtifact(honest); defer artifact.deinit(); try std.testing.expectEqual(@as(usize, 1), state.render_create_count); artifact.push_extent = 4; try std.testing.expectError(error.PushConstantRangeExceeded, handle.loadRenderArtifact(&artifact)); try std.testing.expectEqual(@as(usize, 0), state.render_load_count);}test "render handle methods validate capabilities and backend ownership" { var state = FakeBackendState{}; const handle = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; const attributes = [_]RenderVertexAttribute{.{ .location = 0, .format = .float32x2, .offset = 0, }}; const layouts = [_]RenderVertexBufferLayout{.{ .binding = 0, .stride = 8, .attribute_start = 0, .attribute_count = attributes.len, }}; var artifact = try handle.createRenderArtifact(.{ .format = .external, .vertex_entry_name = "quad_vs", .fragment_entry_name = "quad_fs", .target_format = .rgba8_unorm, .push_extent = 0, .vertex_layouts = layouts[0..], .vertex_attributes = attributes[0..], }); defer artifact.deinit(); try std.testing.expectEqual(@as(usize, 1), state.render_create_count); const loaded = try handle.loadRenderArtifact(&artifact); try std.testing.expectEqual(@as(usize, 1), state.render_load_count); const target_texture = try handle.allocateTexture(.{ .extent = .{ .width = 128, .height = 64, .depth = 1 }, .format = .rgba8_unorm, .usage = .{ .color_attachment = true, .present = true }, }); const vertex_buffer = try handle.allocateBuffer(.{ .byte_size = 64 }); const draws = [_]RenderDraw{ .{ .pipeline = loaded, .vertex_buffers = &.{.{ .buffer = vertex_buffer }}, .range = .{ .vertex_count = 6, .instance_count = 4 }, }, .{ .pipeline = loaded, .vertex_buffers = &.{.{ .buffer = vertex_buffer }}, .range = .{ .vertex_count = 3 }, }, }; const pass = RenderPass{ .color = .{ .view = .{ .texture = target_texture, .format = .rgba8_unorm } }, .viewport = .{ .width = 128, .height = 64 }, .scissor = .{ .width = 128, .height = 64 }, .draws = draws[0..], }; try handle.render(.{ .pass = pass }); try std.testing.expectEqual(@as(usize, 1), state.render_count); try std.testing.expectEqual(@as(usize, 2), state.last_render_draw_count); try std.testing.expectEqual(@as(u32, 6), state.last_render_vertex_count); try std.testing.expectEqual(@as(u32, 4), state.last_render_instance_count); const bundle = try handle.recordRenderBundle(pass); try std.testing.expectEqual(@as(u32, 2), bundle.draw_count); try handle.submitRenderBundle(.{ .bundle = bundle }); try std.testing.expectEqual(@as(usize, 1), state.bundle_submit_count); var texels: [128 * 64 * 4]u8 = undefined; try std.testing.expectError(error.InvalidTexture, handle.readTexture(.{ .texture = target_texture, .bytes = &texels })); const readable = try handle.allocateTexture(.{ .extent = .{ .width = 128, .height = 64, .depth = 1 }, .format = .rgba8_unorm, .usage = .{ .color_attachment = true, .copy_src = true, .copy_dst = true }, }); try handle.readTexture(.{ .texture = readable, .bytes = &texels }); try std.testing.expectError(error.InvalidTexture, handle.writeTexture(.{ .texture = readable, .bytes = texels[1..] })); try handle.writeTexture(.{ .texture = readable, .bytes = &texels }); try std.testing.expectEqual(@as(usize, 1), state.texture_read_count); try std.testing.expectEqual(@as(usize, 1), state.texture_write_count); state.supports_raster = false; try std.testing.expectError(error.CapabilityMismatch, handle.createRenderArtifact(.{ .format = .external, .vertex_entry_name = "quad_vs", .fragment_entry_name = "quad_fs", .target_format = .rgba8_unorm, .push_extent = 0, .vertex_layouts = layouts[0..], .vertex_attributes = attributes[0..], })); try std.testing.expectEqual(@as(usize, 1), state.render_create_count); state.supports_raster = true; state.created_render_backend = .vulkan; try std.testing.expectError(error.CapabilityMismatch, handle.createRenderArtifact(.{ .format = .external, .vertex_entry_name = "quad_vs", .fragment_entry_name = "quad_fs", .target_format = .rgba8_unorm, .push_extent = 0, .vertex_layouts = layouts[0..], .vertex_attributes = attributes[0..], })); try std.testing.expectEqual(@as(usize, 2), state.render_create_count); state.created_render_backend = .cuda; state.loaded_render_backend = .vulkan; try std.testing.expectError(error.InvalidRenderArtifact, handle.loadRenderArtifact(&artifact)); try std.testing.expectEqual(@as(usize, 2), state.render_load_count);}test "createArtifact validates requested capabilities before dispatch" { var state = FakeBackendState{}; const backend = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; try std.testing.expectError(error.UnsupportedOperation, backend.createArtifact(.{ .kernel_name = "add", .requested_format = .cuda_ptx, .required_dtypes = DTypeSet.init(&.{.f32}), })); try std.testing.expectError(error.UnsupportedArtifactFormat, backend.createArtifact(.{ .kernel_name = "add", .requested_format = .vulkan_spirv, })); try std.testing.expectError(error.CapabilityMismatch, backend.createArtifact(.{ .kernel_name = "add", .requested_format = .cuda_ptx, .required_dtypes = DTypeSet.init(&.{.f64}), })); try std.testing.expectError(error.CapabilityMismatch, backend.createArtifact(.{ .kernel_name = "add", .requested_format = .cuda_ptx, .required_features = .{ .async_copy = true }, })); try std.testing.expectError(error.CapabilityMismatch, backend.createArtifact(.{ .kernel_name = "add", .requested_format = .cuda_ptx, .required_subgroup = .{ .scan = true }, }));}test "launch request makes buffer ownership explicit" { var state = FakeBackendState{}; const backend = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; try std.testing.expectEqual(BackendKind.cuda, backend.backendKind().?); const caps = try backend.queryCapabilities(); try std.testing.expect(caps.supportsDType(.f32)); try std.testing.expectError(error.UnsupportedOperation, backend.writeBuffer(.{ .handle = .{ .id = 42, .backend = .cuda, .byte_size = 4096, .ownership = .backend, }, .bytes = &.{}, })); var artifact = try KernelArtifact.init(std.testing.allocator, .{ .backend = .cuda, .format = .cuda_ptx, .entry_name = "add_f32", .argument_count = 1, }); defer artifact.deinit(); artifact.setBorrowedText("// ptx"); const binding = BufferBinding{ .handle = .{ .id = 42, .backend = .cuda, .byte_size = 4096, .ownership = .backend, }, .access = .read_write, .ownership = .backend, .byte_size = 4096, }; try backend.launch(.{ .artifact = &artifact, .buffers = &.{binding}, .geometry = .{ .grid = .{ 16, 1, 1 }, .threadgroup = .{ 64, 1, 1 }, }, }); try std.testing.expect(state.launched); try std.testing.expectEqual(BufferOwnership.backend, state.last_buffer_ownership.?); state.launched = false; try std.testing.expectError(error.LaunchArgumentMismatch, backend.launch(.{ .artifact = &artifact, .buffers = &.{}, .geometry = .{}, })); try std.testing.expect(!state.launched);}test "launch validates geometry before backend dispatch" { var state = FakeBackendState{}; const backend = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; var artifact = try KernelArtifact.init(std.testing.allocator, .{ .backend = .cuda, .format = .cuda_ptx, .entry_name = "add_f32", .argument_count = 1, }); defer artifact.deinit(); artifact.setBorrowedText("// ptx"); const binding = BufferBinding{ .handle = .{ .id = 42, .backend = .cuda, .byte_size = 4096, .ownership = .backend, }, .access = .read_write, .ownership = .backend, .byte_size = 4096, }; try std.testing.expectError(error.LaunchArgumentMismatch, backend.launch(.{ .artifact = &artifact, .buffers = &.{binding}, .geometry = .{ .grid = .{ 0, 1, 1 }, .threadgroup = .{ 1, 1, 1 } }, })); try std.testing.expectError(error.LaunchArgumentMismatch, backend.launch(.{ .artifact = &artifact, .buffers = &.{binding}, .geometry = .{ .grid = .{ 1, 1, 1 }, .threadgroup = .{ 0, 1, 1 } }, })); try std.testing.expectError(error.CapabilityMismatch, backend.launch(.{ .artifact = &artifact, .buffers = &.{binding}, .geometry = .{ .grid = .{ 1, 1, 1 }, .threadgroup = .{ 512, 1, 1 } }, })); try std.testing.expectError(error.CapabilityMismatch, backend.launch(.{ .artifact = &artifact, .buffers = &.{binding}, .geometry = .{ .grid = .{ 1, 1, 65 }, .threadgroup = .{ 1, 1, 1 } }, })); try std.testing.expectError(error.CapabilityMismatch, backend.launch(.{ .artifact = &artifact, .buffers = &.{binding}, .geometry = .{ .grid = .{ 1, 1, 1 }, .threadgroup = .{ 1, 1, 1 }, .dynamic_shared_memory_bytes = 64 * 1024, }, })); try std.testing.expect(!state.launched);}test "backend handle rejects cross-backend objects before dispatch" { var state = FakeBackendState{}; const backend = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; var artifact = try KernelArtifact.init(std.testing.allocator, .{ .backend = .cuda, .format = .cuda_ptx, .entry_name = "add_f32", .argument_count = 1, }); defer artifact.deinit(); artifact.setBorrowedText("// ptx"); var no_arg_artifact = try KernelArtifact.init(std.testing.allocator, .{ .backend = .cuda, .format = .cuda_ptx, .entry_name = "noop", .argument_count = 0, }); defer no_arg_artifact.deinit(); no_arg_artifact.setBorrowedText("// ptx"); var foreign_artifact = try KernelArtifact.init(std.testing.allocator, .{ .backend = .vulkan, .format = .vulkan_spirv, .entry_name = "main", .argument_count = 0, }); defer foreign_artifact.deinit(); foreign_artifact.setBorrowedWords(&.{0x07230203}); try std.testing.expectError(error.CapabilityMismatch, backend.loadArtifact(&foreign_artifact)); const cuda_buffer = BufferHandle{ .id = 42, .backend = .cuda, .byte_size = 4096, .ownership = .backend, }; const foreign_buffer = BufferHandle{ .id = 43, .backend = .vulkan, .byte_size = 4096, .ownership = .backend, }; const cuda_stream = StreamHandle{ .id = 11, .backend = .cuda }; const foreign_stream = StreamHandle{ .id = 12, .backend = .vulkan }; const cuda_event = EventHandle{ .id = 21, .backend = .cuda }; const foreign_event = EventHandle{ .id = 22, .backend = .vulkan }; try std.testing.expectError(error.InvalidBuffer, backend.writeBuffer(.{ .handle = foreign_buffer, .bytes = &.{}, })); try std.testing.expectError(error.InvalidBuffer, backend.readBuffer(.{ .handle = foreign_buffer, .bytes = &.{}, })); try std.testing.expectError(error.ReadBufferDestinationTooSmall, backend.readBuffer(.{ .handle = cuda_buffer, .bytes = &.{}, })); try std.testing.expectError(error.InvalidArtifact, backend.launch(.{ .artifact = &artifact, .loaded_artifact = .{ .id = 1, .backend = .vulkan, .format = .cuda_ptx }, .buffers = &.{.{ .handle = cuda_buffer, .access = .read_write, .ownership = .backend, .byte_size = 4096 }}, .geometry = .{}, })); try std.testing.expectError(error.InvalidArtifact, backend.launch(.{ .artifact = &artifact, .loaded_artifact = .{ .id = 1, .backend = .cuda, .format = .cuda_cubin }, .buffers = &.{.{ .handle = cuda_buffer, .access = .read_write, .ownership = .backend, .byte_size = 4096 }}, .geometry = .{}, })); try std.testing.expectError(error.InvalidBuffer, backend.launch(.{ .artifact = &artifact, .buffers = &.{.{ .handle = foreign_buffer, .access = .read_write, .ownership = .backend, .byte_size = 4096 }}, .geometry = .{}, })); try std.testing.expectError(error.InvalidStream, backend.launch(.{ .artifact = &no_arg_artifact, .buffers = &.{}, .geometry = .{}, .stream = foreign_stream, })); try std.testing.expectError(error.InvalidEvent, backend.launch(.{ .artifact = &no_arg_artifact, .buffers = &.{}, .geometry = .{}, .wait_events = &.{foreign_event}, })); try std.testing.expectError(error.InvalidEvent, backend.launch(.{ .artifact = &no_arg_artifact, .buffers = &.{}, .geometry = .{}, .signal_event = foreign_event, })); try std.testing.expectError(error.InvalidStream, backend.synchronize(.{ .scope = .stream, .stream = foreign_stream, })); try std.testing.expectError(error.InvalidEvent, backend.synchronize(.{ .scope = .event, .event = foreign_event, })); try std.testing.expectError(error.InvalidEvent, backend.queryEvent(.{ .event = foreign_event })); try std.testing.expectError(error.InvalidEvent, backend.elapsedEventNs(.{ .start = foreign_event, .end = cuda_event, })); try std.testing.expectError(error.InvalidEvent, backend.elapsedEventNs(.{ .start = cuda_event, .end = foreign_event, })); try std.testing.expectError(error.InvalidStream, backend.recordEvent(.{ .stream = foreign_stream, .event = cuda_event, })); try std.testing.expectError(error.InvalidEvent, backend.recordEvent(.{ .stream = cuda_stream, .event = foreign_event, })); try std.testing.expect(!state.launched);}test "event query request is nonblocking and optional" { var state = FakeBackendState{}; const backend = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; const event = EventHandle{ .id = 7, .backend = .cuda, }; try std.testing.expect(!try backend.queryEvent(.{ .event = event })); try std.testing.expectEqual(@as(BackendObjectId, 7), state.queried_event.?); state.event_ready = true; try std.testing.expect(try backend.queryEvent(.{ .event = event })); const unsupported_vtable = BackendVTable{ .query_capabilities = fakeQueryCapabilities, }; const unsupported = BackendHandle{ .ptr = &state, .vtable = &unsupported_vtable, .kind = .cuda, }; try std.testing.expectError(error.UnsupportedOperation, unsupported.queryEvent(.{ .event = event }));}test "event elapsed request returns backend nanoseconds and is optional" { var state = FakeBackendState{ .elapsed_ns = 42_000, }; const backend = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; const start = EventHandle{ .id = 7, .backend = .cuda, }; const end = EventHandle{ .id = 8, .backend = .cuda, }; try std.testing.expectEqual(@as(u64, 42_000), try backend.elapsedEventNs(.{ .start = start, .end = end, })); try std.testing.expectEqual(@as(BackendObjectId, 7), state.elapsed_start_event.?); try std.testing.expectEqual(@as(BackendObjectId, 8), state.elapsed_end_event.?); const unsupported_vtable = BackendVTable{ .query_capabilities = fakeQueryCapabilities, }; const unsupported = BackendHandle{ .ptr = &state, .vtable = &unsupported_vtable, .kind = .cuda, }; try std.testing.expectError(error.UnsupportedOperation, unsupported.elapsedEventNs(.{ .start = start, .end = end, }));}test "synchronization scopes have one explicit valid handle shape" { var state = FakeBackendState{}; const handle = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; const stream = StreamHandle{ .id = 11, .backend = .cuda }; const event = EventHandle{ .id = 21, .backend = .cuda }; try handle.synchronize(.{ .scope = .default_stream }); try std.testing.expectEqual(SyncScope.default_stream, state.last_sync_request.?.scope); try handle.synchronize(.{ .scope = .device }); try std.testing.expectEqual(SyncScope.device, state.last_sync_request.?.scope); try handle.synchronize(.{ .scope = .stream, .stream = stream }); try std.testing.expectEqual(stream.id, state.last_sync_request.?.stream.?.id); try handle.synchronize(.{ .scope = .event, .event = event }); try std.testing.expectEqual(event.id, state.last_sync_request.?.event.?.id); try std.testing.expectEqual(@as(usize, 4), state.sync_count); const invalid = [_]SyncRequest{ .{ .scope = .default_stream, .stream = stream }, .{ .scope = .device, .event = event }, .{ .scope = .stream }, .{ .scope = .stream, .stream = stream, .event = event }, .{ .scope = .event }, .{ .scope = .event, .stream = stream, .event = event }, }; for (invalid) |request| { try std.testing.expectError(error.UnsupportedOperation, handle.synchronize(request)); } try std.testing.expectEqual(@as(usize, 4), state.sync_count);}test "stream and event creation requests are optional backend objects" { var state = FakeBackendState{}; const backend = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; const stream = try backend.createStream(.{}); try std.testing.expect(state.created_stream); try std.testing.expectEqual(BackendKind.cuda, stream.backend); try std.testing.expectEqual(@as(BackendObjectId, 1), stream.id); const event = try backend.createEvent(.{}); try std.testing.expect(state.created_event); try std.testing.expectEqual(BackendKind.cuda, event.backend); try std.testing.expectEqual(@as(BackendObjectId, 2), event.id); const unsupported_vtable = BackendVTable{ .query_capabilities = fakeQueryCapabilities, }; const unsupported = BackendHandle{ .ptr = &state, .vtable = &unsupported_vtable, .kind = .cuda, }; try std.testing.expectError(error.UnsupportedOperation, unsupported.createStream(.{})); try std.testing.expectError(error.UnsupportedOperation, unsupported.createEvent(.{}));}test "runtime capabilities gate stream and event operations before dispatch" { var state = FakeBackendState{ .supports_streams = false, .supports_events = false, }; const backend = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; var artifact = try KernelArtifact.init(std.testing.allocator, .{ .backend = .cuda, .format = .cuda_ptx, .entry_name = "noop", .argument_count = 0, }); defer artifact.deinit(); artifact.setBorrowedText("// ptx"); const stream = StreamHandle{ .id = 11, .backend = .cuda }; const event = EventHandle{ .id = 21, .backend = .cuda }; try std.testing.expectError(error.CapabilityMismatch, backend.createStream(.{})); try std.testing.expect(!state.created_stream); try std.testing.expectError(error.CapabilityMismatch, backend.createEvent(.{})); try std.testing.expect(!state.created_event); try std.testing.expectError(error.CapabilityMismatch, backend.launch(.{ .artifact = &artifact, .buffers = &.{}, .geometry = .{}, .stream = stream, })); try std.testing.expectError(error.CapabilityMismatch, backend.launch(.{ .artifact = &artifact, .buffers = &.{}, .geometry = .{}, .wait_events = &.{event}, })); try std.testing.expectError(error.CapabilityMismatch, backend.launch(.{ .artifact = &artifact, .buffers = &.{}, .geometry = .{}, .signal_event = event, })); try std.testing.expect(!state.launched); try std.testing.expectError(error.CapabilityMismatch, backend.synchronize(.{ .scope = .stream, .stream = stream, })); try std.testing.expectError(error.CapabilityMismatch, backend.synchronize(.{ .scope = .event, .event = event, })); try std.testing.expectError(error.CapabilityMismatch, backend.queryEvent(.{ .event = event })); try std.testing.expectEqual(@as(?BackendObjectId, null), state.queried_event); try std.testing.expectError(error.CapabilityMismatch, backend.elapsedEventNs(.{ .start = event, .end = event, })); try std.testing.expectEqual(@as(?BackendObjectId, null), state.elapsed_start_event); try std.testing.expectError(error.CapabilityMismatch, backend.recordEvent(.{ .stream = stream, .event = event, })); try std.testing.expectEqual(@as(?BackendObjectId, null), state.record_stream); try std.testing.expectEqual(@as(?BackendObjectId, null), state.recorded_event);}test "event record request is explicit and optional" { var state = FakeBackendState{}; const backend = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda, }; const stream = StreamHandle{ .id = 11, .backend = .cuda, }; const event = EventHandle{ .id = 12, .backend = .cuda, }; try backend.recordEvent(.{ .stream = stream, .event = event, }); try std.testing.expectEqual(@as(BackendObjectId, 11), state.record_stream.?); try std.testing.expectEqual(@as(BackendObjectId, 12), state.recorded_event.?); const unsupported_vtable = BackendVTable{ .query_capabilities = fakeQueryCapabilities, }; const unsupported = BackendHandle{ .ptr = &state, .vtable = &unsupported_vtable, .kind = .cuda, }; try std.testing.expectError(error.UnsupportedOperation, unsupported.recordEvent(.{ .stream = stream, .event = event, }));}test "loadArtifact refuses an artifact whose interface the device cannot satisfy" { var state = FakeBackendState{}; const handle = BackendHandle{ .ptr = &state, .vtable = &fake_vtable, .kind = .cuda }; var artifact = try KernelArtifact.init(std.testing.allocator, .{ .backend = .cuda, .format = .cuda_ptx, .entry_name = "kernel", .argument_count = 1, .interface = .{ .features = .{ .async_copy = true } }, }); defer artifact.deinit(); try std.testing.expectError(error.CapabilityMismatch, handle.loadArtifact(&artifact)); artifact.interface = .{ .subgroup = .{ .scan = true } }; try std.testing.expectError(error.CapabilityMismatch, handle.loadArtifact(&artifact)); artifact.interface = .{}; artifact.interface.push_constants.byte_size = 3; try std.testing.expectError(error.InvalidArtifact, handle.loadArtifact(&artifact)); artifact.interface = .{ .features = .{ .dynamic_shared_memory = true } }; try std.testing.expectError(error.UnsupportedOperation, handle.loadArtifact(&artifact));}test "capabilities bound push constants and depth bias, and draws carry the declared push bytes" { const caps = BackendCapabilities{ .identity = .{ .backend = .vulkan, .family = .vulkan, .name = "test-vulkan-device" }, .raster = .{ .supported = true, .artifact_formats = RenderArtifactFormatSet.init(&.{.vulkan_spirv}), .target_formats = TextureFormatSet.init(&.{.rgba8_unorm}), .depth_formats = TextureFormatSet.init(&.{.depth32_float}), .blend_modes = RenderBlendModeSet.init(&.{.replace}), .topologies = RenderPrimitiveTopologySet.init(&.{.triangle_list}), .index_formats = RenderIndexFormatSet.init(&.{.none}), .max_push_constant_bytes = 16, .depth_bias = true, }, }; const desc = RenderPipelineDesc{ .format = .vulkan_spirv, .vertex_entry_name = "vs", .fragment_entry_name = "fs", .target_format = .rgba8_unorm, .push_constant_bytes = 16, .push_extent = 16, }; try caps.validateRenderPipelineDesc(desc); var short = desc; short.push_constant_bytes = 12; try std.testing.expectError(error.PushConstantRangeExceeded, caps.validateRenderPipelineDesc(short)); var unaligned = desc; unaligned.push_constant_bytes = 6; try std.testing.expectError(error.InvalidRenderArtifact, caps.validateRenderPipelineDesc(unaligned)); var oversized = desc; oversized.push_constant_bytes = 20; try std.testing.expectError(error.CapabilityMismatch, caps.validateRenderPipelineDesc(oversized)); var biased = desc; biased.depth = .{ .bias = .{ .constant = -16, .slope = -1 } }; try caps.validateRenderPipelineDesc(biased); var clamped = biased; clamped.depth.?.bias.clamp = -0x1p-20; try std.testing.expectError(error.CapabilityMismatch, caps.validateRenderPipelineDesc(clamped)); var clamp_caps = caps; clamp_caps.raster.depth_bias_clamp = true; try clamp_caps.validateRenderPipelineDesc(clamped); var infinite = biased; infinite.depth.?.bias.slope = std.math.inf(f32); try std.testing.expectError(error.InvalidRenderArtifact, caps.validateRenderPipelineDesc(infinite)); var no_bias_caps = caps; no_bias_caps.raster.depth_bias = false; try std.testing.expectError(error.CapabilityMismatch, no_bias_caps.validateRenderPipelineDesc(biased)); var artifact = try RenderArtifact.init(std.testing.allocator, .{ .backend = .vulkan, .pipeline = desc }); defer artifact.deinit(); const pipeline = LoadedRenderArtifact.describing(&artifact, 7); try std.testing.expectEqual(@as(u32, 16), pipeline.push_constant_bytes); const target = TextureView{ .texture = .{ .id = 1, .backend = .vulkan, .extent = .{ .width = 8, .height = 8, .depth = 1 }, .format = .rgba8_unorm, .usage = .{ .color_attachment = true }, }, .format = .rgba8_unorm, }; const push: [16]u8 = @splat(0); var draws = [_]RenderDraw{.{ .pipeline = pipeline, .range = .{ .vertex_count = 3 }, .push_constants = &push }}; const pass = RenderPass{ .color = .{ .view = target }, .viewport = .{ .width = 8, .height = 8 }, .scissor = .{ .width = 8, .height = 8 }, .draws = &draws, }; try caps.validateRenderPass(pass); draws[0].push_constants = push[0..12]; try std.testing.expectError(error.RenderArgumentMismatch, caps.validateRenderPass(pass)); draws[0].push_constants = &.{}; try std.testing.expectError(error.RenderArgumentMismatch, caps.validateRenderPass(pass));}Source: lib/gpu/src/root.zig:8
zig
pub const contract = @import("contract.zig");Audit
| Definitions | 1 |
|---|---|
| Public names | 1 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |