lib/gpu/src/cpu.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const builtin = @import("builtin");
   3 const choir_abi = @import("choir_abi");
   4 const sys = @import("sys");
   5 const tldr = @import("tldr");
   6 
   7 const backend = @import("root.zig");
   8 /// The rasterizer this backend's passes run on.
   9 pub const raster = @import("runtime/cpu/raster.zig");
  10 
  11 const elf_object = tldr.formats.elf.object;
  12 
  13 const max_launch_args = 32;
  14 
  15 /// The number of absolute symbols a loaded kernel or stage links against: one
  16 /// per function `sys.math.symbols` lists, `malloc` and `free` from `sys.heap`,
  17 /// and the sampler a lowered stage calls. `runtimeSymbolsObject` sizes its
  18 /// symbol array with it, so the array holds every name a kernel may call.
  19 const runtime_symbol_count = sys.math.symbols.len + 3;
  20 
  21 const LoadedKernel = struct {
  22     mapping: []align(std.heap.page_size_min) u8,
  23     entry_address: usize,
  24     code_len: usize,
  25     argument_count: u32,
  26 };
  27 
  28 const BufferRecord = struct {
  29     bytes: []u8,
  30     byte_size: usize,
  31     alignment: std.mem.Alignment,
  32     ownership: backend.BufferOwnership,
  33 };
  34 
  35 const TextureRecord = struct {
  36     handle: backend.TextureHandle,
  37     texture: raster.Texture,
  38 };
  39 
  40 /// A render pipeline: the object linked twice, once with each stage as its entry, and the vertex
  41 /// layouts, attributes and texture keys the rasterizer reads.
  42 const PipelineRecord = struct {
  43     loaded: backend.LoadedRenderArtifact,
  44     images: [2][]align(std.heap.page_size_min) u8,
  45     layouts: [raster.max_vertex_buffers]raster.Layout,
  46     attributes: [raster.max_vertex_attributes]raster.Attribute,
  47     keys: [raster.max_bindings]i32,
  48     pipeline: raster.Pipeline,
  49 };
  50 
  51 const BindingsRecord = struct {
  52     pipeline_id: backend.BackendObjectId,
  53     sampled: [raster.max_bindings]raster.Sampled,
  54     uniforms: [raster.max_bindings]raster.Uniform,
  55     resources: raster.Resources,
  56 };
  57 
  58 /// A recorded pass, copied with the draws, vertex buffer ranges and push constants it names.
  59 const BundleRecord = struct {
  60     pass: backend.RenderPass,
  61     draws: []backend.RenderDraw,
  62     ranges: []backend.RenderBufferRange,
  63     push_constants: []u8,
  64 };
  65 
  66 /// Each render object lives at a fixed address, since pipelines, bindings and passes point into
  67 /// one another.
  68 const RenderObject = union(enum) {
  69     texture: *TextureRecord,
  70     pipeline: *PipelineRecord,
  71     bindings: *BindingsRecord,
  72     bundle: *BundleRecord,
  73 };
  74 
  75 pub const Stages = struct {
  76     vertex: raster.Stage,
  77     fragment: raster.Fragment,
  78 };
  79 
  80 pub const State = struct {
  81     allocator: std.mem.Allocator,
  82     next_id: backend.BackendObjectId = 1,
  83     loaded: std.AutoHashMapUnmanaged(backend.BackendObjectId, LoadedKernel) = .empty,
  84     buffers: std.AutoHashMapUnmanaged(backend.BackendObjectId, BufferRecord) = .empty,
  85     render_objects: std.AutoHashMapUnmanaged(backend.BackendObjectId, RenderObject) = .empty,
  86     /// The triangles and fragments every pass so far has rasterized.
  87     counters: raster.Counters = .{},
  88     /// The context every draw's stages read, refilled per draw.
  89     frame: raster.Frame = .init(&no_resources),
  90 
  91     pub fn init(allocator: std.mem.Allocator) State {
  92         return .{ .allocator = allocator };
  93     }
  94 
  95     pub fn deinit(self: *State) void {
  96         var loaded_iter = self.loaded.valueIterator();
  97         while (loaded_iter.next()) |kernel| {
  98             sys.memory.unmap(kernel.mapping);
  99         }
 100         self.loaded.deinit(self.allocator);
 101 
 102         var buffer_iter = self.buffers.valueIterator();
 103         while (buffer_iter.next()) |record| freeBufferRecord(self.allocator, record.*);
 104         self.buffers.deinit(self.allocator);
 105 
 106         var render_iter = self.render_objects.valueIterator();
 107         while (render_iter.next()) |object| freeRenderObject(self.allocator, object.*);
 108         self.render_objects.deinit(self.allocator);
 109         self.* = undefined;
 110     }
 111 
 112     /// The lowered stages a loaded pipeline calls, for a caller that times them alone.
 113     pub fn pipelineStages(self: *State, pipeline: backend.LoadedRenderArtifact) backend.BackendError!Stages {
 114         const record = try getPipeline(self, pipeline.id);
 115         return .{ .vertex = record.pipeline.vertex, .fragment = record.pipeline.fragment };
 116     }
 117 
 118     pub fn handle(self: *State) backend.BackendHandle {
 119         return .{
 120             .ptr = self,
 121             .vtable = &vtable,
 122             .kind = .cpu,
 123         };
 124     }
 125 };
 126 
 127 fn queryCapabilities(_: *anyopaque) backend.BackendError!backend.BackendCapabilities {
 128     return .{
 129         .identity = .{
 130             .backend = .cpu,
 131             .family = .native_cpu,
 132             .name = "native-cpu",
 133         },
 134         .memory = .{
 135             .min_buffer_alignment = 1,
 136             .host_visible_device_memory = true,
 137             .unified_memory = true,
 138         },
 139         .threadgroup = .{
 140             .max_threads = 1024,
 141             .max_blocks = .{ std.math.maxInt(u32), std.math.maxInt(u32), std.math.maxInt(u32) },
 142             .max_threads_per_dim = .{ 1024, 1024, 1024 },
 143             .max_grid_per_dim = .{ std.math.maxInt(u32), std.math.maxInt(u32), std.math.maxInt(u32) },
 144         },
 145         .dtypes = backend.DTypeSet.init(&.{ .i1, .i32, .u32, .i64, .u64, .f32, .f64, .key }),
 146         .layouts = .{
 147             .row_major = true,
 148             .compact_strides = true,
 149             .broadcast_strides = true,
 150         },
 151         .runtime = .{
 152             .driver_loaded = true,
 153             .device_context = false,
 154             .streams = false,
 155             .events = false,
 156         },
 157         .artifact_formats = backend.ArtifactFormatSet.init(&.{ .cpu_machine_code, .cpu_object }),
 158         .textures = .{
 159             .supported = true,
 160             .formats = backend.TextureFormatSet.init(&(color_formats ++ [_]backend.TextureFormat{.depth32_float})),
 161             .usages = .{
 162                 .copy_src = true,
 163                 .copy_dst = true,
 164                 .sampled = true,
 165                 .color_attachment = true,
 166                 .depth_attachment = true,
 167             },
 168             .max_extent = .{ .width = raster.max_extent, .height = raster.max_extent, .depth = 1 },
 169             .max_sample_count = 1,
 170         },
 171         .raster = .{
 172             .supported = true,
 173             .artifact_formats = backend.RenderArtifactFormatSet.init(&.{.cpu_object}),
 174             .target_formats = backend.TextureFormatSet.init(&color_formats),
 175             .depth_formats = backend.TextureFormatSet.init(&.{.depth32_float}),
 176             .blend_modes = backend.RenderBlendModeSet.init(&.{ .replace, .alpha_premultiplied, .alpha_straight, .additive }),
 177             .topologies = backend.RenderPrimitiveTopologySet.init(&.{ .triangle_list, .triangle_strip }),
 178             .vertex_formats = backend.RenderVertexFormatSet.init(&.{ .float32, .float32x2, .float32x3, .float32x4, .uint32, .uint32x2, .uint32x4 }),
 179             .binding_kinds = backend.RenderBindingKindSet.init(&.{ .uniform_buffer, .sampled_texture }),
 180             .index_formats = backend.RenderIndexFormatSet.init(&.{ .none, .u16, .u32 }),
 181             .max_vertex_buffers = raster.max_vertex_buffers,
 182             .max_vertex_attributes = raster.max_vertex_attributes,
 183             .max_bindings = raster.max_bindings,
 184             .instancing = true,
 185             .max_push_constant_bytes = choir_abi.stage.push_words * 4,
 186             .depth_bias = true,
 187             .depth_bias_clamp = true,
 188         },
 189     };
 190 }
 191 
 192 const color_formats = [_]backend.TextureFormat{ .rgba8_unorm, .bgra8_unorm, .rgba8_srgb, .bgra8_srgb };
 193 
 194 fn createArtifact(
 195     ptr: *anyopaque,
 196     request: backend.CompileRequest,
 197 ) backend.BackendError!backend.KernelArtifact {
 198     const state: *State = @ptrCast(@alignCast(ptr));
 199     switch (request.requested_format) {
 200         .cpu_machine_code, .cpu_object => {},
 201         else => return error.UnsupportedArtifactFormat,
 202     }
 203     var artifact = backend.KernelArtifact.init(state.allocator, .{
 204         .backend = .cpu,
 205         .format = request.requested_format,
 206         .entry_name = request.kernel_name,
 207         .argument_count = request.argument_count,
 208         .scalar_argument_count = request.scalar_argument_count,
 209         .diagnostic_id = request.diagnostic_id,
 210     }) catch return error.OutOfMemory;
 211     errdefer artifact.deinit();
 212     switch (request.payload) {
 213         .bytes => |bytes| try artifact.setOwnedBytes(bytes),
 214         else => return error.InvalidArtifact,
 215     }
 216     return artifact;
 217 }
 218 
 219 fn loadArtifact(
 220     ptr: *anyopaque,
 221     artifact: *const backend.KernelArtifact,
 222 ) backend.BackendError!backend.LoadedArtifact {
 223     const state: *State = @ptrCast(@alignCast(ptr));
 224     if (artifact.backend != .cpu) return error.InvalidArtifact;
 225     const code = switch (artifact.payload) {
 226         .bytes => |bytes| bytes,
 227         else => return error.InvalidArtifact,
 228     };
 229     return switch (artifact.format) {
 230         .cpu_machine_code => loadNativeCode(state, code, artifact.argument_count, artifact.format),
 231         .cpu_object => loadObjectCode(state, code, artifact.entry_name, artifact.argument_count, artifact.format),
 232         else => error.UnsupportedArtifactFormat,
 233     };
 234 }
 235 
 236 fn loadNativeCode(
 237     state: *State,
 238     code: []const u8,
 239     argument_count: u32,
 240     format: backend.ArtifactFormat,
 241 ) backend.BackendError!backend.LoadedArtifact {
 242     if (code.len == 0) return error.InvalidArtifact;
 243 
 244     const mapping = sys.memory.mapAnonymous(code.len, .{ .read = true, .write = true }) catch return error.RuntimeUnavailable;
 245     errdefer sys.memory.unmap(mapping);
 246     @memcpy(mapping[0..code.len], code);
 247     sys.memory.protect(mapping, .{ .read = true, .execute = true }) catch return error.RuntimeUnavailable;
 248 
 249     const id = nextObjectId(state);
 250     state.loaded.put(state.allocator, id, .{
 251         .mapping = mapping,
 252         .entry_address = @intFromPtr(mapping.ptr),
 253         .code_len = code.len,
 254         .argument_count = argument_count,
 255     }) catch return error.OutOfMemory;
 256 
 257     return .{
 258         .id = id,
 259         .backend = .cpu,
 260         .format = format,
 261     };
 262 }
 263 
 264 fn loadObjectCode(
 265     state: *State,
 266     object: []const u8,
 267     entry_name: []const u8,
 268     argument_count: u32,
 269     format: backend.ArtifactFormat,
 270 ) backend.BackendError!backend.LoadedArtifact {
 271     if (object.len == 0 or entry_name.len == 0) return error.InvalidArtifact;
 272 
 273     var loaded_image = try linkObject(state.allocator, object, entry_name);
 274     errdefer loaded_image.deinit();
 275 
 276     const id = nextObjectId(state);
 277     state.loaded.put(state.allocator, id, .{
 278         .mapping = loaded_image.memory,
 279         .entry_address = loaded_image.entry_address,
 280         .code_len = loaded_image.memory.len,
 281         .argument_count = argument_count,
 282     }) catch return error.OutOfMemory;
 283 
 284     return .{
 285         .id = id,
 286         .backend = .cpu,
 287         .format = format,
 288     };
 289 }
 290 
 291 /// Links `object` beside the runtime symbols and maps it with `entry_name` as its entry.
 292 fn linkObject(allocator: std.mem.Allocator, object: []const u8, entry_name: []const u8) backend.BackendError!tldr.LoadedImage {
 293     const runtime_symbols = try runtimeSymbolsObject(allocator);
 294     defer allocator.free(runtime_symbols);
 295 
 296     const inputs = [_]tldr.Input{
 297         .{
 298             .name = "gpu-cpu-kernel.o",
 299             .bytes = object,
 300         },
 301         .{
 302             .name = "gpu-cpu-runtime.o",
 303             .bytes = runtime_symbols,
 304         },
 305     };
 306 
 307     return tldr.loadExecutable(allocator, &inputs, .{
 308         .entry_symbol = entry_name,
 309         .incremental_mode = .off,
 310     }) catch |err| cpuLoadError(err);
 311 }
 312 
 313 /// Returns the bytes of one relocatable object that defines each function
 314 /// `sys.math.symbols` lists, plus `malloc`, `free` and the stage sampler, as a
 315 /// global absolute symbol whose value is the address of the host's own
 316 /// function. `linkObject` builds it for each kernel or stage object it loads,
 317 /// links it beside the object as `gpu-cpu-runtime.o`, and frees it once the
 318 /// link returns. The
 319 /// function gives the object writer a fixed count of global symbols with no
 320 /// section and no relocation, all in one value that tells the writer what one
 321 /// object holds (description). Running out of memory is therefore the one
 322 /// failure the object writer can report here, and the function returns it as
 323 /// `error.OutOfMemory`. The caller owns the returned bytes and frees them with
 324 /// `allocator`.
 325 fn runtimeSymbolsObject(allocator: std.mem.Allocator) backend.BackendError![]u8 {
 326     var symbols: [runtime_symbol_count]elf_object.Symbol = undefined;
 327     const math_symbols = symbols[0..sys.math.symbols.len];
 328     for (sys.math.symbols, math_symbols) |symbol, *entry| {
 329         entry.* = elf_object.Symbol.absoluteObject(symbol.name(), symbol.address(), 0);
 330     }
 331     const heap_symbols = symbols[sys.math.symbols.len..];
 332     heap_symbols[0] = elf_object.Symbol.absoluteObject("malloc", @intFromPtr(&sys.heap.malloc), 0);
 333     heap_symbols[1] = elf_object.Symbol.absoluteObject("free", @intFromPtr(&sys.heap.free), 0);
 334     heap_symbols[2] = elf_object.Symbol.absoluteObject(choir_abi.stage.sample_symbol, @intFromPtr(&raster.sample), 0);
 335 
 336     const description = elf_object.Description{ .sections = &.{}, .symbols = &symbols };
 337     return elf_object.build(allocator, description) catch |err| switch (err) {
 338         error.OutOfMemory => error.OutOfMemory,
 339         else => unreachable,
 340     };
 341 }
 342 
 343 fn allocateBuffer(
 344     ptr: *anyopaque,
 345     request: backend.BufferAllocation,
 346 ) backend.BackendError!backend.BufferHandle {
 347     const state: *State = @ptrCast(@alignCast(ptr));
 348     const allocation_len = @max(request.byte_size, 1);
 349     const alignment = std.mem.Alignment.fromByteUnits(request.alignment);
 350     const memory = state.allocator.rawAlloc(allocation_len, alignment, @returnAddress()) orelse return error.OutOfMemory;
 351     const bytes = memory[0..allocation_len];
 352     errdefer state.allocator.rawFree(bytes, alignment, @returnAddress());
 353     @memset(bytes, 0);
 354 
 355     const id = nextObjectId(state);
 356     state.buffers.put(state.allocator, id, .{
 357         .bytes = bytes,
 358         .byte_size = request.byte_size,
 359         .alignment = alignment,
 360         .ownership = .backend,
 361     }) catch return error.OutOfMemory;
 362 
 363     return .{
 364         .id = id,
 365         .backend = .cpu,
 366         .byte_size = request.byte_size,
 367         .ownership = .backend,
 368     };
 369 }
 370 
 371 fn importBuffer(
 372     ptr: *anyopaque,
 373     request: backend.BufferImport,
 374 ) backend.BackendError!backend.BufferHandle {
 375     const state: *State = @ptrCast(@alignCast(ptr));
 376     std.debug.assert(request.bytes.len != 0);
 377     std.debug.assert(std.mem.isAligned(@intFromPtr(request.bytes.ptr), request.alignment));
 378     const id = nextObjectId(state);
 379     state.buffers.put(state.allocator, id, .{
 380         .bytes = request.bytes,
 381         .byte_size = request.bytes.len,
 382         .alignment = std.mem.Alignment.fromByteUnits(request.alignment),
 383         .ownership = .borrowed_external,
 384     }) catch return error.OutOfMemory;
 385     return .{
 386         .id = id,
 387         .backend = .cpu,
 388         .byte_size = request.bytes.len,
 389         .ownership = .borrowed_external,
 390     };
 391 }
 392 
 393 fn freeBufferRecord(allocator: std.mem.Allocator, record: BufferRecord) void {
 394     switch (record.ownership) {
 395         .backend => allocator.rawFree(record.bytes, record.alignment, @returnAddress()),
 396         .borrowed_external => {},
 397         .host => unreachable,
 398     }
 399 }
 400 
 401 fn writeBuffer(
 402     ptr: *anyopaque,
 403     request: backend.BufferWriteRequest,
 404 ) backend.BackendError!void {
 405     const state: *State = @ptrCast(@alignCast(ptr));
 406     const record = state.buffers.getPtr(request.handle.id) orelse return error.InvalidBuffer;
 407     if (request.bytes.len > record.byte_size) return error.InvalidBuffer;
 408     @memcpy(record.bytes[0..request.bytes.len], request.bytes);
 409 }
 410 
 411 fn readBuffer(
 412     ptr: *anyopaque,
 413     request: backend.BufferReadRequest,
 414 ) backend.BackendError!void {
 415     const state: *State = @ptrCast(@alignCast(ptr));
 416     const record = state.buffers.getPtr(request.handle.id) orelse return error.InvalidBuffer;
 417     if (request.bytes.len < record.byte_size) return error.ReadBufferDestinationTooSmall;
 418     @memcpy(request.bytes[0..record.byte_size], record.bytes[0..record.byte_size]);
 419 }
 420 
 421 fn launch(ptr: *anyopaque, request: backend.LaunchRequest) backend.BackendError!void {
 422     const state: *State = @ptrCast(@alignCast(ptr));
 423     const loaded = request.loaded_artifact orelse return error.InvalidArtifact;
 424     if (loaded.backend != .cpu or !backend.artifactFormatIsNativeCpu(loaded.format)) return error.InvalidArtifact;
 425     const kernel = state.loaded.get(loaded.id) orelse return error.InvalidArtifact;
 426     if (request.buffers.len + request.scalar_arguments.len != kernel.argument_count) return error.LaunchArgumentMismatch;
 427     if (kernel.code_len == 0) return error.InvalidArtifact;
 428 
 429     var args: [max_launch_args]i64 = undefined;
 430     var index: usize = 0;
 431     for (request.buffers) |binding| {
 432         if (index >= args.len) return error.LaunchArgumentMismatch;
 433         const record = state.buffers.get(binding.handle.id) orelse return error.InvalidBuffer;
 434         if (binding.byte_size > record.byte_size) return error.InvalidBuffer;
 435         args[index] = @bitCast(@intFromPtr(record.bytes.ptr));
 436         index += 1;
 437     }
 438     for (request.scalar_arguments) |argument| {
 439         if (index >= args.len) return error.LaunchArgumentMismatch;
 440         args[index] = try scalarArgumentBits(argument);
 441         index += 1;
 442     }
 443 
 444     callVoidKernel(kernel.entry_address, args[0..index]) catch return error.LaunchFailed;
 445 }
 446 
 447 fn destroyObject(ptr: *anyopaque, id: backend.BackendObjectId) void {
 448     const state: *State = @ptrCast(@alignCast(ptr));
 449     if (state.loaded.fetchRemove(id)) |entry| {
 450         sys.memory.unmap(entry.value.mapping);
 451         return;
 452     }
 453     if (state.buffers.fetchRemove(id)) |entry| {
 454         freeBufferRecord(state.allocator, entry.value);
 455         return;
 456     }
 457     if (state.render_objects.fetchRemove(id)) |entry| freeRenderObject(state.allocator, entry.value);
 458 }
 459 
 460 fn freeRenderObject(allocator: std.mem.Allocator, object: RenderObject) void {
 461     switch (object) {
 462         .texture => |record| {
 463             allocator.free(record.texture.bytes);
 464             allocator.destroy(record);
 465         },
 466         .pipeline => |record| {
 467             for (record.images) |image| sys.memory.unmap(image);
 468             allocator.destroy(record);
 469         },
 470         .bindings => |record| allocator.destroy(record),
 471         .bundle => |record| {
 472             allocator.free(record.draws);
 473             allocator.free(record.ranges);
 474             allocator.free(record.push_constants);
 475             allocator.destroy(record);
 476         },
 477     }
 478 }
 479 
 480 fn synchronize(_: *anyopaque, _: backend.SyncRequest) backend.BackendError!void {}
 481 
 482 fn getTexture(state: *State, handle: backend.TextureHandle) backend.BackendError!*TextureRecord {
 483     return switch (state.render_objects.get(handle.id) orelse return error.InvalidTexture) {
 484         .texture => |record| record,
 485         else => error.InvalidTexture,
 486     };
 487 }
 488 
 489 fn getPipeline(state: *State, id: backend.BackendObjectId) backend.BackendError!*PipelineRecord {
 490     return switch (state.render_objects.get(id) orelse return error.InvalidRenderArtifact) {
 491         .pipeline => |record| record,
 492         else => error.InvalidRenderArtifact,
 493     };
 494 }
 495 
 496 fn getBindings(state: *State, id: backend.BackendObjectId) backend.BackendError!*BindingsRecord {
 497     return switch (state.render_objects.get(id) orelse return error.RenderArgumentMismatch) {
 498         .bindings => |record| record,
 499         else => error.RenderArgumentMismatch,
 500     };
 501 }
 502 
 503 fn getBundle(state: *State, id: backend.BackendObjectId) backend.BackendError!*BundleRecord {
 504     return switch (state.render_objects.get(id) orelse return error.RenderArgumentMismatch) {
 505         .bundle => |record| record,
 506         else => error.RenderArgumentMismatch,
 507     };
 508 }
 509 
 510 /// Makes a zeroed texture of one level and one sample. A depth texture is never a color
 511 /// attachment and a color texture never a depth attachment, as on the Vulkan path.
 512 fn allocateTexture(ptr: *anyopaque, request: backend.TextureAllocation) backend.BackendError!backend.TextureHandle {
 513     const state: *State = @ptrCast(@alignCast(ptr));
 514     if (request.sample_count != 1 or request.extent.depth != 1) return error.CapabilityMismatch;
 515     if (request.usage.present or request.usage.storage) return error.CapabilityMismatch;
 516     const depth = request.format.isDepth();
 517     if (depth and request.usage.color_attachment) return error.CapabilityMismatch;
 518     if (!depth and request.usage.depth_attachment) return error.CapabilityMismatch;
 519     try state.render_objects.ensureUnusedCapacity(state.allocator, 1);
 520 
 521     const byte_count = @as(usize, request.extent.width) * request.extent.height * request.format.texelBytes();
 522     const bytes = state.allocator.alignedAlloc(u8, .@"4", byte_count) catch return error.OutOfMemory;
 523     errdefer state.allocator.free(bytes);
 524     @memset(bytes, 0);
 525     const record = state.allocator.create(TextureRecord) catch return error.OutOfMemory;
 526     const handle = backend.TextureHandle{
 527         .id = nextObjectId(state),
 528         .backend = .cpu,
 529         .extent = request.extent,
 530         .format = request.format,
 531         .usage = request.usage,
 532         .sample_count = 1,
 533         .ownership = .backend,
 534     };
 535     record.* = .{
 536         .handle = handle,
 537         .texture = .{ .width = request.extent.width, .height = request.extent.height, .format = request.format, .bytes = bytes },
 538     };
 539     state.render_objects.putAssumeCapacity(handle.id, .{ .texture = record });
 540     return handle;
 541 }
 542 
 543 fn destroyTexture(ptr: *anyopaque, handle: backend.TextureHandle) backend.BackendError!void {
 544     const state: *State = @ptrCast(@alignCast(ptr));
 545     _ = try getTexture(state, handle);
 546     freeRenderObject(state.allocator, state.render_objects.fetchRemove(handle.id).?.value);
 547 }
 548 
 549 fn writeTexture(ptr: *anyopaque, request: backend.TextureWriteRequest) backend.BackendError!void {
 550     const state: *State = @ptrCast(@alignCast(ptr));
 551     const record = try getTexture(state, request.texture);
 552     if (request.bytes.len != record.texture.bytes.len) return error.InvalidTexture;
 553     @memcpy(record.texture.bytes, request.bytes);
 554 }
 555 
 556 fn readTexture(ptr: *anyopaque, request: backend.TextureReadRequest) backend.BackendError!void {
 557     const state: *State = @ptrCast(@alignCast(ptr));
 558     const record = try getTexture(state, request.texture);
 559     if (request.bytes.len != record.texture.bytes.len) return error.InvalidTexture;
 560     @memcpy(request.bytes, record.texture.bytes);
 561 }
 562 
 563 fn createRenderArtifact(ptr: *anyopaque, desc: backend.RenderPipelineDesc) backend.BackendError!backend.RenderArtifact {
 564     const state: *State = @ptrCast(@alignCast(ptr));
 565     if (desc.format != .cpu_object) return error.UnsupportedArtifactFormat;
 566     const object = switch (desc.payload) {
 567         .bytes => |bytes| bytes,
 568         else => return error.InvalidRenderArtifact,
 569     };
 570     if (object.len == 0) return error.InvalidRenderArtifact;
 571     var artifact = backend.RenderArtifact.init(state.allocator, .{
 572         .backend = .cpu,
 573         .pipeline = desc,
 574     }) catch return error.OutOfMemory;
 575     errdefer artifact.deinit();
 576     try artifact.setOwnedBytes(object);
 577     return artifact;
 578 }
 579 
 580 /// Links the artifact's object once per stage. Each vertex layout's binding must be below the
 581 /// vertex buffer limit and used once, every attribute location below Choir's interface limit, and
 582 /// every resource binding a sampled texture or a uniform buffer whose group and binding form a
 583 /// distinct key. A uniform buffer must sit in group 0 below `choir_abi.stage.uniform_bindings`,
 584 /// where the stage's context has a window for it. A fragment entry the object defines under
 585 /// `choir_abi.stage.quad_suffix` reads its quad, and the rasterizer shades quads for it.
 586 fn loadRenderArtifact(ptr: *anyopaque, artifact: *const backend.RenderArtifact) backend.BackendError!backend.LoadedRenderArtifact {
 587     const state: *State = @ptrCast(@alignCast(ptr));
 588     if (artifact.backend != .cpu) return error.CapabilityMismatch;
 589     if (artifact.format != .cpu_object) return error.UnsupportedArtifactFormat;
 590     const object = switch (artifact.payload) {
 591         .bytes => |bytes| bytes,
 592         else => return error.InvalidRenderArtifact,
 593     };
 594     if (object.len == 0) return error.InvalidRenderArtifact;
 595     switch (artifact.topology) {
 596         .triangle_list, .triangle_strip => {},
 597         .line_list, .line_strip => return error.CapabilityMismatch,
 598     }
 599     if (artifact.vertex_layouts.len > raster.max_vertex_buffers) return error.CapabilityMismatch;
 600     if (artifact.bindings.len > raster.max_bindings) return error.CapabilityMismatch;
 601     if (artifact.push_constant_bytes > choir_abi.stage.push_words * 4) return error.CapabilityMismatch;
 602 
 603     var layouts: [raster.max_vertex_buffers]raster.Layout = undefined;
 604     var attributes: [raster.max_vertex_attributes]raster.Attribute = undefined;
 605     var attribute_count: u32 = 0;
 606     var used_vertex_bindings: u32 = 0;
 607     for (artifact.vertex_layouts, layouts[0..artifact.vertex_layouts.len]) |layout, *out| {
 608         if (layout.binding >= raster.max_vertex_buffers) return error.CapabilityMismatch;
 609         const bit = @as(u32, 1) << @intCast(layout.binding);
 610         if (used_vertex_bindings & bit != 0) return error.InvalidRenderArtifact;
 611         used_vertex_bindings |= bit;
 612         const start: usize = layout.attribute_start;
 613         const count: usize = layout.attribute_count;
 614         if (start > artifact.vertex_attributes.len or count > artifact.vertex_attributes.len - start) return error.InvalidRenderArtifact;
 615         out.* = .{
 616             .stride = layout.stride,
 617             .per_instance = layout.step_mode == .instance,
 618             .attribute_start = attribute_count,
 619             .attribute_count = @intCast(count),
 620         };
 621         for (artifact.vertex_attributes[start..][0..count]) |attribute| {
 622             if (attribute_count == raster.max_vertex_attributes) return error.CapabilityMismatch;
 623             if (attribute.location >= choir_abi.stage.max_locations) return error.CapabilityMismatch;
 624             attributes[attribute_count] = .{ .location = attribute.location, .format = attribute.format, .offset = attribute.offset };
 625             attribute_count += 1;
 626         }
 627     }
 628     var keys: [raster.max_bindings]i32 = undefined;
 629     for (artifact.bindings, keys[0..artifact.bindings.len], 0..) |binding, *key, index| {
 630         switch (binding.kind) {
 631             .sampled_texture => {},
 632             .uniform_buffer => {
 633                 if (binding.group != 0) return error.CapabilityMismatch;
 634                 if (binding.binding >= choir_abi.stage.uniform_bindings) return error.CapabilityMismatch;
 635             },
 636             .storage_buffer, .storage_texture => return error.CapabilityMismatch,
 637         }
 638         if (binding.group > std.math.maxInt(i16) or binding.binding > std.math.maxInt(u16)) return error.CapabilityMismatch;
 639         key.* = choir_abi.stage.textureKey(binding.group, binding.binding);
 640         if (std.mem.indexOfScalar(i32, keys[0..index], key.*) != null) return error.InvalidRenderArtifact;
 641     }
 642     try state.render_objects.ensureUnusedCapacity(state.allocator, 1);
 643 
 644     var vertex_image = try linkStage(state.allocator, object, artifact.vertex_entry_name);
 645     errdefer vertex_image.deinit();
 646     var quad_buffer: [max_entry_bytes]u8 = undefined;
 647     const quad_name = std.fmt.bufPrint(&quad_buffer, "{s}{s}", .{ artifact.fragment_entry_name, choir_abi.stage.quad_suffix }) catch
 648         return error.InvalidRenderArtifact;
 649     const quad = try definesSymbol(state.allocator, object, quad_name);
 650     var fragment_image = try linkStage(state.allocator, object, if (quad) quad_name else artifact.fragment_entry_name);
 651     errdefer fragment_image.deinit();
 652     const record = state.allocator.create(PipelineRecord) catch return error.OutOfMemory;
 653     const id = nextObjectId(state);
 654     record.* = .{
 655         .loaded = backend.LoadedRenderArtifact.describing(artifact, id),
 656         .images = .{ vertex_image.memory, fragment_image.memory },
 657         .layouts = layouts,
 658         .attributes = attributes,
 659         .keys = keys,
 660         .pipeline = undefined,
 661     };
 662     record.pipeline = .{
 663         .vertex = @ptrFromInt(vertex_image.entry_address),
 664         .fragment = if (quad)
 665             .{ .quad = @ptrFromInt(fragment_image.entry_address) }
 666         else
 667             .{ .pixel = @ptrFromInt(fragment_image.entry_address) },
 668         .blend = artifact.blend_mode,
 669         .topology = artifact.topology,
 670         .depth = artifact.depth,
 671         .target_format = artifact.target_format,
 672         .layouts = record.layouts[0..artifact.vertex_layouts.len],
 673         .attributes = record.attributes[0..attribute_count],
 674     };
 675     state.render_objects.putAssumeCapacity(id, .{ .pipeline = record });
 676     return record.loaded;
 677 }
 678 
 679 /// Bytes a fragment entry's quad symbol may take.
 680 const max_entry_bytes = 256;
 681 
 682 /// Whether `object` defines the symbol `name`. A fragment stage that reads its quad is defined
 683 /// under its entry name and `choir_abi.stage.quad_suffix`, and one that does not under its name.
 684 fn definesSymbol(allocator: std.mem.Allocator, object: []const u8, name: []const u8) backend.BackendError!bool {
 685     var parsed = tldr.parseObject(allocator, .{ .name = "gpu-cpu-kernel.o", .bytes = object }) catch |err| switch (err) {
 686         error.OutOfMemory => return error.OutOfMemory,
 687         else => return error.InvalidRenderArtifact,
 688     };
 689     defer parsed.deinit(allocator);
 690     for (parsed.symbols) |symbol| {
 691         if (!symbol.undefined and std.mem.eql(u8, symbol.name, name)) return true;
 692     }
 693     return false;
 694 }
 695 
 696 fn linkStage(allocator: std.mem.Allocator, object: []const u8, entry_name: []const u8) backend.BackendError!tldr.LoadedImage {
 697     return linkObject(allocator, object, entry_name) catch |err| switch (err) {
 698         error.InvalidArtifact => error.InvalidRenderArtifact,
 699         else => err,
 700     };
 701 }
 702 
 703 fn createRenderBindings(ptr: *anyopaque, request: backend.RenderBindingsRequest) backend.BackendError!backend.RenderBindings {
 704     const state: *State = @ptrCast(@alignCast(ptr));
 705     const pipeline = try getPipeline(state, request.pipeline.id);
 706     const count: usize = pipeline.loaded.binding_count;
 707     if (request.resources.len != count) return error.RenderArgumentMismatch;
 708     var sampled: [raster.max_bindings]raster.Sampled = undefined;
 709     var sampled_count: usize = 0;
 710     var uniforms: [raster.max_bindings]raster.Uniform = undefined;
 711     var uniform_count: usize = 0;
 712     for (request.resources, pipeline.keys[0..count]) |resource, key| switch (resource) {
 713         .sampled_texture => |binding| {
 714             const texture = try getTexture(state, binding.texture);
 715             if (!texture.handle.usage.sampled) return error.InvalidTexture;
 716             sampled[sampled_count] = .{ .key = key, .texture = &texture.texture, .sampler = binding.sampler };
 717             sampled_count += 1;
 718         },
 719         .uniform_buffer => |buffer| {
 720             const record = state.buffers.getPtr(buffer.id) orelse return error.InvalidBuffer;
 721             const binding: u32 = @intCast(key & 0xffff);
 722             uniforms[uniform_count] = .{ .binding = binding, .bytes = record.bytes[0..record.byte_size] };
 723             uniform_count += 1;
 724         },
 725         .storage_buffer, .storage_texture => return error.RenderArgumentMismatch,
 726     };
 727     try state.render_objects.ensureUnusedCapacity(state.allocator, 1);
 728     const record = state.allocator.create(BindingsRecord) catch return error.OutOfMemory;
 729     record.* = .{
 730         .pipeline_id = pipeline.loaded.id,
 731         .sampled = sampled,
 732         .uniforms = uniforms,
 733         .resources = undefined,
 734     };
 735     record.resources = .{
 736         .sampled = record.sampled[0..sampled_count],
 737         .uniforms = record.uniforms[0..uniform_count],
 738     };
 739     const id = nextObjectId(state);
 740     state.render_objects.putAssumeCapacity(id, .{ .bindings = record });
 741     return .{ .id = id, .backend = .cpu, .pipeline = pipeline.loaded.id };
 742 }
 743 
 744 fn render(ptr: *anyopaque, request: backend.RenderRequest) backend.BackendError!void {
 745     const state: *State = @ptrCast(@alignCast(ptr));
 746     try runPass(state, request.pass);
 747 }
 748 
 749 /// Checks the pass as `render` would and keeps a copy of it. The copy names the same objects,
 750 /// which must outlive the bundle.
 751 fn recordRenderBundle(ptr: *anyopaque, pass: backend.RenderPass) backend.BackendError!backend.RenderBundle {
 752     const state: *State = @ptrCast(@alignCast(ptr));
 753     _ = try passTargets(state, pass);
 754     var range_count: usize = 0;
 755     var push_bytes: usize = 0;
 756     for (pass.draws) |draw| {
 757         var resolved: Resolved = undefined;
 758         try resolveDraw(state, pass, draw, &resolved);
 759         range_count += draw.vertex_buffers.len;
 760         push_bytes += draw.push_constants.len;
 761     }
 762     try state.render_objects.ensureUnusedCapacity(state.allocator, 1);
 763     const draws = state.allocator.dupe(backend.RenderDraw, pass.draws) catch return error.OutOfMemory;
 764     errdefer state.allocator.free(draws);
 765     const ranges = state.allocator.alloc(backend.RenderBufferRange, range_count) catch return error.OutOfMemory;
 766     errdefer state.allocator.free(ranges);
 767     const push_constants = state.allocator.alloc(u8, push_bytes) catch return error.OutOfMemory;
 768     errdefer state.allocator.free(push_constants);
 769     var next: usize = 0;
 770     var next_push: usize = 0;
 771     for (draws) |*draw| {
 772         const copy = ranges[next..][0..draw.vertex_buffers.len];
 773         @memcpy(copy, draw.vertex_buffers);
 774         draw.vertex_buffers = copy;
 775         next += copy.len;
 776         const push_copy = push_constants[next_push..][0..draw.push_constants.len];
 777         @memcpy(push_copy, draw.push_constants);
 778         draw.push_constants = push_copy;
 779         next_push += push_copy.len;
 780     }
 781     const record = state.allocator.create(BundleRecord) catch return error.OutOfMemory;
 782     record.* = .{ .pass = pass, .draws = draws, .ranges = ranges, .push_constants = push_constants };
 783     record.pass.draws = draws;
 784     record.pass.diagnostic_id = null;
 785     const id = nextObjectId(state);
 786     state.render_objects.putAssumeCapacity(id, .{ .bundle = record });
 787     return .{ .id = id, .backend = .cpu, .draw_count = @intCast(draws.len) };
 788 }
 789 
 790 fn submitRenderBundle(ptr: *anyopaque, request: backend.RenderBundleSubmit) backend.BackendError!void {
 791     const state: *State = @ptrCast(@alignCast(ptr));
 792     const bundle = try getBundle(state, request.bundle.id);
 793     try runPass(state, bundle.pass);
 794 }
 795 
 796 const Targets = struct {
 797     color: *TextureRecord,
 798     depth: ?*TextureRecord,
 799 };
 800 
 801 fn passTargets(state: *State, pass: backend.RenderPass) backend.BackendError!Targets {
 802     const color = try getTexture(state, pass.color.view.texture);
 803     if (!color.handle.usage.color_attachment or color.texture.format != pass.color.view.format) return error.InvalidTexture;
 804     const attachment = pass.depth orelse return .{ .color = color, .depth = null };
 805     const depth = try getTexture(state, attachment.view.texture);
 806     if (!depth.handle.usage.depth_attachment or depth.texture.format != attachment.view.format) return error.InvalidTexture;
 807     if (depth.texture.width != color.texture.width or depth.texture.height != color.texture.height) return error.RenderArgumentMismatch;
 808     return .{ .color = color, .depth = depth };
 809 }
 810 
 811 /// Resolves every draw before touching a target, so a pass that names a missing or mismatched
 812 /// object fails whole, then clears and draws in order.
 813 fn runPass(state: *State, pass: backend.RenderPass) backend.BackendError!void {
 814     const targets = try passTargets(state, pass);
 815     for (pass.draws) |draw| {
 816         var resolved: Resolved = undefined;
 817         try resolveDraw(state, pass, draw, &resolved);
 818     }
 819     switch (pass.color.load) {
 820         .load => {},
 821         .clear => |color| raster.clearColor(&targets.color.texture, color),
 822     }
 823     if (pass.depth) |attachment| switch (attachment.load) {
 824         .load => {},
 825         .clear => |value| raster.clearDepth(&targets.depth.?.texture, value),
 826     };
 827     const raster_pass = raster.Pass{
 828         .color = &targets.color.texture,
 829         .depth = if (targets.depth) |target| &target.texture else null,
 830         .viewport = pass.viewport,
 831         .scissor = pass.scissor,
 832     };
 833     for (pass.draws) |draw| {
 834         var resolved: Resolved = undefined;
 835         resolveDraw(state, pass, draw, &resolved) catch unreachable;
 836         raster.draw(raster_pass, &resolved.draw, &state.frame, &state.counters);
 837     }
 838 }
 839 
 840 const no_resources = raster.Resources{};
 841 
 842 /// A draw resolved against the backend's objects. `draw.vertex_buffers` points into `buffers`.
 843 const Resolved = struct {
 844     draw: raster.Draw,
 845     buffers: [raster.max_vertex_buffers][]const u8,
 846 };
 847 
 848 /// Checks one draw the way the Vulkan path records it and resolves it for the rasterizer: every
 849 /// buffer range a draw reads by its range is checked, and the vertices an indexed draw reaches
 850 /// through its indices are checked by the rasterizer as it reads each one.
 851 fn resolveDraw(state: *State, pass: backend.RenderPass, draw: backend.RenderDraw, out: *Resolved) backend.BackendError!void {
 852     const pipeline = try getPipeline(state, draw.pipeline.id);
 853     if (pipeline.loaded.target_format != pass.color.view.format) return error.RenderArgumentMismatch;
 854     if ((pipeline.loaded.depth != null) != (pass.depth != null)) return error.RenderArgumentMismatch;
 855     const layouts = pipeline.pipeline.layouts;
 856     if (draw.vertex_buffers.len != layouts.len) return error.RenderArgumentMismatch;
 857     const resources: *const raster.Resources = if (draw.bindings) |bindings| blk: {
 858         const record = try getBindings(state, bindings.id);
 859         if (record.pipeline_id != pipeline.loaded.id) return error.RenderArgumentMismatch;
 860         break :blk &record.resources;
 861     } else if (pipeline.loaded.binding_count != 0) return error.RenderArgumentMismatch else &no_resources;
 862 
 863     if (draw.push_constants.len != pipeline.loaded.push_constant_bytes) return error.RenderArgumentMismatch;
 864     const range = draw.range;
 865     const indexed = range.index_format != .none;
 866     for (draw.vertex_buffers, layouts, out.buffers[0..layouts.len]) |buffer_range, layout, *bytes| {
 867         bytes.* = try bufferFrom(state, buffer_range);
 868         const elements: u64 = if (layout.per_instance)
 869             @as(u64, range.first_instance) + range.instance_count
 870         else if (indexed)
 871             0
 872         else
 873             @as(u64, range.first_vertex) + range.vertex_count;
 874         const needed = std.math.mul(u64, elements, layout.stride) catch return error.RenderArgumentMismatch;
 875         if (needed > bytes.len) return error.RenderArgumentMismatch;
 876     }
 877     const indices: raster.Indices = if (!indexed) blk: {
 878         if (draw.index_buffer != null) return error.RenderArgumentMismatch;
 879         break :blk .none;
 880     } else blk: {
 881         const buffer_range = draw.index_buffer orelse return error.RenderArgumentMismatch;
 882         const index_bytes: u64 = if (range.index_format == .u16) 2 else 4;
 883         if (buffer_range.offset % index_bytes != 0) return error.RenderArgumentMismatch;
 884         const bytes = try bufferFrom(state, buffer_range);
 885         if ((@as(u64, range.first_index) + range.index_count) * index_bytes > bytes.len) return error.RenderArgumentMismatch;
 886         break :blk if (range.index_format == .u16) .{ .u16 = bytes } else .{ .u32 = bytes };
 887     };
 888     out.draw = .{
 889         .pipeline = &pipeline.pipeline,
 890         .resources = resources,
 891         .vertex_buffers = out.buffers[0..layouts.len],
 892         .indices = indices,
 893         .range = range,
 894         .push_constants = draw.push_constants,
 895     };
 896 }
 897 
 898 /// The bytes of a buffer from a range's offset on, which must fall inside the buffer.
 899 fn bufferFrom(state: *State, range: backend.RenderBufferRange) backend.BackendError![]const u8 {
 900     const record = state.buffers.getPtr(range.buffer.id) orelse return error.InvalidBuffer;
 901     if (range.offset >= record.byte_size) return error.RenderArgumentMismatch;
 902     return record.bytes[@intCast(range.offset)..record.byte_size];
 903 }
 904 
 905 fn nextObjectId(state: *State) backend.BackendObjectId {
 906     const id = state.next_id;
 907     state.next_id += 1;
 908     return id;
 909 }
 910 
 911 fn scalarArgumentBits(argument: choir_abi.ScalarArgument) backend.BackendError!i64 {
 912     return switch (argument) {
 913         .i32 => |value| value,
 914         .u32 => |value| @intCast(value),
 915         .i64 => |value| value,
 916         .u64 => |value| @bitCast(value),
 917         .f32 => |value| @intCast(@as(u32, @bitCast(value))),
 918         .f64 => |value| @bitCast(@as(u64, @bitCast(value))),
 919     };
 920 }
 921 
 922 fn cpuLoadError(err: tldr.LoadError) backend.BackendError {
 923     return switch (err) {
 924         error.OutOfMemory => error.OutOfMemory,
 925         error.AccessDenied,
 926         error.InvalidMapping,
 927         error.MapFailed,
 928         error.PermissionDenied,
 929         error.ProtectFailed,
 930         error.UnsupportedPlatform,
 931         => error.RuntimeUnavailable,
 932         else => error.InvalidArtifact,
 933     };
 934 }
 935 
 936 fn callVoidKernel(address: usize, args: []const i64) backend.BackendError!void {
 937     switch (args.len) {
 938         0 => {
 939             const func: *const fn () callconv(.c) void = @ptrFromInt(address);
 940             func();
 941         },
 942         1 => {
 943             const func: *const fn (i64) callconv(.c) void = @ptrFromInt(address);
 944             func(args[0]);
 945         },
 946         2 => {
 947             const func: *const fn (i64, i64) callconv(.c) void = @ptrFromInt(address);
 948             func(args[0], args[1]);
 949         },
 950         3 => {
 951             const func: *const fn (i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
 952             func(args[0], args[1], args[2]);
 953         },
 954         4 => {
 955             const func: *const fn (i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
 956             func(args[0], args[1], args[2], args[3]);
 957         },
 958         5 => {
 959             const func: *const fn (i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
 960             func(args[0], args[1], args[2], args[3], args[4]);
 961         },
 962         6 => {
 963             const func: *const fn (i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
 964             func(args[0], args[1], args[2], args[3], args[4], args[5]);
 965         },
 966         7 => {
 967             const func: *const fn (i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
 968             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
 969         },
 970         8 => {
 971             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
 972             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]);
 973         },
 974         9 => {
 975             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
 976             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]);
 977         },
 978         10 => {
 979             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
 980             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]);
 981         },
 982         11 => {
 983             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
 984             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10]);
 985         },
 986         12 => {
 987             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
 988             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11]);
 989         },
 990         13 => {
 991             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
 992             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12]);
 993         },
 994         14 => {
 995             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
 996             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13]);
 997         },
 998         15 => {
 999             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1000             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14]);
1001         },
1002         16 => {
1003             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1004             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15]);
1005         },
1006         17 => {
1007             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1008             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16]);
1009         },
1010         18 => {
1011             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1012             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17]);
1013         },
1014         19 => {
1015             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1016             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18]);
1017         },
1018         20 => {
1019             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1020             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19]);
1021         },
1022         21 => {
1023             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1024             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], args[20]);
1025         },
1026         22 => {
1027             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1028             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], args[20], args[21]);
1029         },
1030         23 => {
1031             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1032             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], args[20], args[21], args[22]);
1033         },
1034         24 => {
1035             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1036             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], args[20], args[21], args[22], args[23]);
1037         },
1038         25 => {
1039             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1040             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], args[20], args[21], args[22], args[23], args[24]);
1041         },
1042         26 => {
1043             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1044             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], args[20], args[21], args[22], args[23], args[24], args[25]);
1045         },
1046         27 => {
1047             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1048             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], args[20], args[21], args[22], args[23], args[24], args[25], args[26]);
1049         },
1050         28 => {
1051             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1052             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], args[20], args[21], args[22], args[23], args[24], args[25], args[26], args[27]);
1053         },
1054         29 => {
1055             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1056             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], args[20], args[21], args[22], args[23], args[24], args[25], args[26], args[27], args[28]);
1057         },
1058         30 => {
1059             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1060             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], args[20], args[21], args[22], args[23], args[24], args[25], args[26], args[27], args[28], args[29]);
1061         },
1062         31 => {
1063             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1064             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], args[20], args[21], args[22], args[23], args[24], args[25], args[26], args[27], args[28], args[29], args[30]);
1065         },
1066         32 => {
1067             const func: *const fn (i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64, i64) callconv(.c) void = @ptrFromInt(address);
1068             func(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9], args[10], args[11], args[12], args[13], args[14], args[15], args[16], args[17], args[18], args[19], args[20], args[21], args[22], args[23], args[24], args[25], args[26], args[27], args[28], args[29], args[30], args[31]);
1069         },
1070         else => return error.LaunchArgumentMismatch,
1071     }
1072 }
1073 
1074 const vtable = backend.BackendVTable{
1075     .query_capabilities = queryCapabilities,
1076     .create_artifact = createArtifact,
1077     .load_artifact = loadArtifact,
1078     .allocate_buffer = allocateBuffer,
1079     .import_buffer = importBuffer,
1080     .write_buffer = writeBuffer,
1081     .read_buffer = readBuffer,
1082     .launch = launch,
1083     .create_render_artifact = createRenderArtifact,
1084     .load_render_artifact = loadRenderArtifact,
1085     .allocate_texture = allocateTexture,
1086     .destroy_texture = destroyTexture,
1087     .write_texture = writeTexture,
1088     .read_texture = readTexture,
1089     .render = render,
1090     .create_render_bindings = createRenderBindings,
1091     .record_render_bundle = recordRenderBundle,
1092     .submit_render_bundle = submitRenderBundle,
1093     .synchronize = synchronize,
1094     .destroy_object = destroyObject,
1095 };
1096 
1097 test "native cpu backend reports native code support" {
1098     var state = State.init(std.testing.allocator);
1099     defer state.deinit();
1100 
1101     const caps = try state.handle().queryCapabilities();
1102     try std.testing.expectEqual(backend.BackendKind.cpu, caps.identity.backend);
1103     try std.testing.expectEqual(backend.DeviceFamily.native_cpu, caps.identity.family);
1104     try std.testing.expect(caps.supportsDType(.u32));
1105     try std.testing.expect(caps.supportsDType(.u64));
1106     try std.testing.expect(caps.supportsDType(.key));
1107     try std.testing.expect(caps.supportsArtifactFormat(.cpu_machine_code));
1108     try std.testing.expect(caps.supportsArtifactFormat(.cpu_object));
1109 }
1110 
1111 var trampoline_probe: i64 = 0;
1112 
1113 fn recordThirtyTwoArgs(
1114     a0: i64,
1115     a1: i64,
1116     a2: i64,
1117     a3: i64,
1118     a4: i64,
1119     a5: i64,
1120     a6: i64,
1121     a7: i64,
1122     a8: i64,
1123     a9: i64,
1124     a10: i64,
1125     a11: i64,
1126     a12: i64,
1127     a13: i64,
1128     a14: i64,
1129     a15: i64,
1130     a16: i64,
1131     a17: i64,
1132     a18: i64,
1133     a19: i64,
1134     a20: i64,
1135     a21: i64,
1136     a22: i64,
1137     a23: i64,
1138     a24: i64,
1139     a25: i64,
1140     a26: i64,
1141     a27: i64,
1142     a28: i64,
1143     a29: i64,
1144     a30: i64,
1145     a31: i64,
1146 ) callconv(.c) void {
1147     trampoline_probe = a0 + a7 + a15 + a23 + a31;
1148     _ = a1;
1149     _ = a2;
1150     _ = a3;
1151     _ = a4;
1152     _ = a5;
1153     _ = a6;
1154     _ = a8;
1155     _ = a9;
1156     _ = a10;
1157     _ = a11;
1158     _ = a12;
1159     _ = a13;
1160     _ = a14;
1161     _ = a16;
1162     _ = a17;
1163     _ = a18;
1164     _ = a19;
1165     _ = a20;
1166     _ = a21;
1167     _ = a22;
1168     _ = a24;
1169     _ = a25;
1170     _ = a26;
1171     _ = a27;
1172     _ = a28;
1173     _ = a29;
1174     _ = a30;
1175 }
1176 
1177 test "native cpu trampoline accepts executable launch argument limit" {
1178     var args: [max_launch_args]i64 = undefined;
1179     for (&args, 0..) |*arg, index| arg.* = @intCast(index);
1180 
1181     trampoline_probe = -1;
1182     try callVoidKernel(@intFromPtr(&recordThirtyTwoArgs), args[0..]);
1183 
1184     try std.testing.expectEqual(@as(i64, 76), trampoline_probe);
1185 }
1186 
1187 test "native cpu backend binds imported caller memory without copying" {
1188     var state = State.init(std.testing.allocator);
1189     defer state.deinit();
1190     const handle = state.handle();
1191 
1192     var caller = [_]u32{ 1, 2, 3, 4 };
1193     const bytes = std.mem.sliceAsBytes(caller[0..]);
1194     const buffer = try handle.importBuffer(.{
1195         .bytes = bytes,
1196         .alignment = @alignOf(u32),
1197         .dtype = .u32,
1198         .element_count = caller.len,
1199     });
1200     try std.testing.expectEqual(backend.BufferOwnership.borrowed_external, buffer.ownership);
1201     try std.testing.expectEqual(bytes.len, buffer.byte_size);
1202     try std.testing.expectEqual(bytes.ptr, state.buffers.get(buffer.id).?.bytes.ptr);
1203 
1204     const replacement = [_]u32{ 9, 8, 7, 6 };
1205     try handle.writeBuffer(.{ .handle = buffer, .bytes = std.mem.sliceAsBytes(&replacement) });
1206     try std.testing.expectEqualSlices(u32, &replacement, &caller);
1207 
1208     caller[0] = 42;
1209     var read: [4]u32 = undefined;
1210     try handle.readBuffer(.{ .handle = buffer, .bytes = std.mem.sliceAsBytes(&read) });
1211     try std.testing.expectEqual(@as(u32, 42), read[0]);
1212 
1213     handle.destroyObject(buffer.id);
1214     try std.testing.expect(state.buffers.get(buffer.id) == null);
1215     try std.testing.expectEqual(@as(u32, 42), caller[0]);
1216 }
1217 
1218 test "native cpu backend rejects empty and misaligned imports" {
1219     var state = State.init(std.testing.allocator);
1220     defer state.deinit();
1221     const handle = state.handle();
1222 
1223     var caller = [_]u32{ 1, 2, 3, 4 };
1224     const bytes = std.mem.sliceAsBytes(caller[0..]);
1225     const empty = bytes[0..0];
1226     try std.testing.expectError(error.InvalidBuffer, handle.importBuffer(.{ .bytes = empty }));
1227     try std.testing.expectError(
1228         error.InvalidBuffer,
1229         handle.importBuffer(.{ .bytes = bytes[1..], .alignment = @alignOf(u32) }),
1230     );
1231     try std.testing.expectEqual(@as(u32, 0), state.buffers.count());
1232 }
1233 
1234 /// x86-64 code that jumps to `target`: `movabs rax, target; jmp rax`, padded with `int3`.
1235 fn trampoline(target: usize) [16]u8 {
1236     var code: [16]u8 = @splat(0xcc);
1237     code[0..2].* = .{ 0x48, 0xb8 };
1238     std.mem.writeInt(u64, code[2..10], target, .little);
1239     code[10..12].* = .{ 0xff, 0xe0 };
1240     return code;
1241 }
1242 
1243 /// A vertex stage placing location 0 as the clip position and passing location 1 on.
1244 fn testVertexStage(inputs: [*]const f32, outputs: [*]f32, _: *anyopaque) callconv(.c) void {
1245     const stage = choir_abi.stage;
1246     outputs[stage.smooth_mask_low] = @bitCast(@as(u32, 0x3));
1247     outputs[stage.smooth_mask_high] = @bitCast(@as(u32, 0));
1248     outputs[stage.flat_mask_low] = @bitCast(@as(u32, 0));
1249     outputs[stage.flat_mask_high] = @bitCast(@as(u32, 0));
1250     outputs[stage.position..][0..4].* = .{ inputs[stage.slot(stage.smooth, 0, 0)], inputs[stage.slot(stage.smooth, 0, 1)], 0, 1 };
1251     outputs[stage.smooth] = inputs[stage.slot(stage.smooth, 1, 0)];
1252     outputs[stage.smooth + 1] = inputs[stage.slot(stage.smooth, 1, 1)];
1253 }
1254 
1255 /// A fragment stage writing the texel of binding 3 at the coordinate it receives.
1256 fn testFragmentStage(inputs: [*]const f32, outputs: [*]f32, context: *anyopaque) callconv(.c) void {
1257     const stage = choir_abi.stage;
1258     outputs[stage.sample..][0..3].* = .{ inputs[stage.smooth], inputs[stage.smooth + 1], 0 };
1259     raster.sample(context, stage.textureKey(0, 3), outputs);
1260     outputs[stage.smooth..][0..4].* = outputs[stage.sample..][0..4].*;
1261 }
1262 
1263 fn expectHalves(texels: *const [4 * 4 * 4]u8) !void {
1264     for (0..4) |y| for (0..4) |x| {
1265         const want: [4]u8 = if (x < 2) .{ 255, 0, 0, 255 } else .{ 0, 0, 255, 255 };
1266         try std.testing.expectEqual(want, texels[(y * 4 + x) * 4 ..][0..4].*);
1267     };
1268 }
1269 
1270 test "native cpu backend links stage objects and draws a textured quad as a pass and a bundle" {
1271     if (builtin.cpu.arch != .x86_64 or builtin.os.tag != .linux) return error.SkipZigTest;
1272     const allocator = std.testing.allocator;
1273     const code = trampoline(@intFromPtr(&testVertexStage)) ++ trampoline(@intFromPtr(&testFragmentStage));
1274     const object = try elf_object.build(allocator, .{
1275         .sections = &.{elf_object.Section.progbits(".text", &code, std.elf.SHF_EXECINSTR, 16)},
1276         .symbols = &.{
1277             elf_object.Symbol.function("quad_vertex", 1, 0, 16),
1278             elf_object.Symbol.function("quad_fragment", 1, 16, 16),
1279         },
1280     });
1281     defer allocator.free(object);
1282 
1283     var state = State.init(allocator);
1284     defer state.deinit();
1285     const handle = state.handle();
1286     const color = try handle.allocateTexture(.{
1287         .extent = .{ .width = 4, .height = 4 },
1288         .format = .rgba8_unorm,
1289         .usage = .{ .color_attachment = true, .copy_src = true, .copy_dst = true },
1290     });
1291     const sampled = try handle.allocateTexture(.{
1292         .extent = .{ .width = 2, .height = 1 },
1293         .format = .bgra8_unorm,
1294         .usage = .{ .sampled = true, .copy_dst = true },
1295     });
1296     try handle.writeTexture(.{ .texture = sampled, .bytes = &.{ 0, 0, 255, 255, 255, 0, 0, 255 } });
1297 
1298     const Vertex = extern struct { position: [2]f32, uv: [2]f32 };
1299     const vertices = [_]Vertex{
1300         .{ .position = .{ -1, -1 }, .uv = .{ 0, 0 } },
1301         .{ .position = .{ -1, 1 }, .uv = .{ 0, 1 } },
1302         .{ .position = .{ 1, 1 }, .uv = .{ 1, 1 } },
1303         .{ .position = .{ 1, -1 }, .uv = .{ 1, 0 } },
1304     };
1305     const indices = [_]u16{ 0, 1, 2, 0, 2, 3 };
1306     const vertex_buffer = try handle.allocateBuffer(.{ .byte_size = @sizeOf(@TypeOf(vertices)), .alignment = 16 });
1307     try handle.writeBuffer(.{ .handle = vertex_buffer, .bytes = std.mem.asBytes(&vertices) });
1308     const index_buffer = try handle.allocateBuffer(.{ .byte_size = @sizeOf(@TypeOf(indices)), .alignment = 4 });
1309     try handle.writeBuffer(.{ .handle = index_buffer, .bytes = std.mem.asBytes(&indices) });
1310 
1311     var artifact = try handle.createRenderArtifact(.{
1312         .format = .cpu_object,
1313         .vertex_entry_name = "quad_vertex",
1314         .fragment_entry_name = "quad_fragment",
1315         .target_format = .rgba8_unorm,
1316         .vertex_layouts = &.{.{ .binding = 0, .stride = @sizeOf(Vertex), .attribute_start = 0, .attribute_count = 2 }},
1317         .vertex_attributes = &.{
1318             .{ .location = 0, .format = .float32x2, .offset = 0 },
1319             .{ .location = 1, .format = .float32x2, .offset = 8 },
1320         },
1321         .bindings = &.{.{ .binding = 3, .kind = .sampled_texture }},
1322         .push_extent = 0,
1323         .payload = .{ .bytes = object },
1324     });
1325     defer artifact.deinit();
1326     const pipeline = try handle.loadRenderArtifact(&artifact);
1327     const bindings = try handle.createRenderBindings(.{
1328         .artifact = &artifact,
1329         .pipeline = pipeline,
1330         .resources = &.{.{ .sampled_texture = .{ .texture = sampled } }},
1331     });
1332     const pass = backend.RenderPass{
1333         .color = .{ .view = .{ .texture = color, .format = .rgba8_unorm }, .load = .{ .clear = .{ .a = 0 } } },
1334         .viewport = .{ .width = 4, .height = 4 },
1335         .scissor = .{ .width = 4, .height = 4 },
1336         .draws = &.{.{
1337             .pipeline = pipeline,
1338             .bindings = bindings,
1339             .vertex_buffers = &.{.{ .buffer = vertex_buffer }},
1340             .index_buffer = .{ .buffer = index_buffer },
1341             .range = .{ .index_count = 6, .index_format = .u16 },
1342         }},
1343     };
1344 
1345     var texels: [4 * 4 * 4]u8 = undefined;
1346     try handle.render(.{ .pass = pass });
1347     try handle.readTexture(.{ .texture = color, .bytes = &texels });
1348     try expectHalves(&texels);
1349 
1350     const bundle = try handle.recordRenderBundle(pass);
1351     try handle.writeTexture(.{ .texture = color, .bytes = &@as([4 * 4 * 4]u8, @splat(7)) });
1352     try handle.submitRenderBundle(.{ .bundle = bundle });
1353     try handle.readTexture(.{ .texture = color, .bytes = &texels });
1354     try expectHalves(&texels);
1355     try std.testing.expectEqual(@as(u64, 4), state.counters.triangles);
1356     try std.testing.expectEqual(@as(u64, 32), state.counters.fragments);
1357 
1358     handle.destroyObject(bundle.id);
1359     handle.destroyObject(bindings.id);
1360     handle.destroyObject(pipeline.id);
1361     try handle.destroyTexture(sampled);
1362     try std.testing.expectError(error.InvalidTexture, handle.readTexture(.{ .texture = sampled, .bytes = texels[0..8] }));
1363 }
1364 
1365 /// A fragment stage writing its first push-constant word as red and the first word of uniform
1366 /// binding 2 as green.
1367 fn blockFragmentStage(_: [*]const f32, outputs: [*]f32, context: *anyopaque) callconv(.c) void {
1368     const stage = choir_abi.stage;
1369     const words: [*]const f32 = @ptrCast(@alignCast(context));
1370     outputs[stage.smooth..][0..4].* = .{ words[stage.push], words[stage.uniformWord(2, 0)], 0, 1 };
1371 }
1372 
1373 test "native cpu bundles copy push constants when recorded and read uniforms when submitted" {
1374     if (builtin.cpu.arch != .x86_64 or builtin.os.tag != .linux) return error.SkipZigTest;
1375     const allocator = std.testing.allocator;
1376     const code = trampoline(@intFromPtr(&testVertexStage)) ++ trampoline(@intFromPtr(&blockFragmentStage));
1377     const object = try elf_object.build(allocator, .{
1378         .sections = &.{elf_object.Section.progbits(".text", &code, std.elf.SHF_EXECINSTR, 16)},
1379         .symbols = &.{
1380             elf_object.Symbol.function("block_vertex", 1, 0, 16),
1381             elf_object.Symbol.function("block_fragment", 1, 16, 16),
1382         },
1383     });
1384     defer allocator.free(object);
1385 
1386     var state = State.init(allocator);
1387     defer state.deinit();
1388     const handle = state.handle();
1389     const color = try handle.allocateTexture(.{
1390         .extent = .{ .width = 2, .height = 2 },
1391         .format = .rgba8_unorm,
1392         .usage = .{ .color_attachment = true, .copy_src = true },
1393     });
1394     defer handle.destroyTexture(color) catch {};
1395     const Vertex = extern struct { position: [2]f32, uv: [2]f32 };
1396     const vertices = [_]Vertex{
1397         .{ .position = .{ -1, -1 }, .uv = .{ 0, 0 } },
1398         .{ .position = .{ -1, 3 }, .uv = .{ 0, 0 } },
1399         .{ .position = .{ 3, -1 }, .uv = .{ 0, 0 } },
1400     };
1401     const vertex_buffer = try handle.allocateBuffer(.{ .byte_size = @sizeOf(@TypeOf(vertices)), .alignment = 16 });
1402     defer handle.destroyObject(vertex_buffer.id);
1403     try handle.writeBuffer(.{ .handle = vertex_buffer, .bytes = std.mem.asBytes(&vertices) });
1404     const one: f32 = 1;
1405     const zero: f32 = 0;
1406     const uniform = try handle.allocateBuffer(.{ .byte_size = 16, .alignment = 16 });
1407     defer handle.destroyObject(uniform.id);
1408     try handle.writeBuffer(.{ .handle = uniform, .bytes = &(std.mem.toBytes(one) ++ std.mem.toBytes(zero) ++ std.mem.toBytes(zero) ++ std.mem.toBytes(zero)) });
1409 
1410     const desc: backend.RenderPipelineDesc = .{
1411         .format = .cpu_object,
1412         .vertex_entry_name = "block_vertex",
1413         .fragment_entry_name = "block_fragment",
1414         .target_format = .rgba8_unorm,
1415         .vertex_layouts = &.{.{ .binding = 0, .stride = @sizeOf(Vertex), .attribute_start = 0, .attribute_count = 2 }},
1416         .vertex_attributes = &.{
1417             .{ .location = 0, .format = .float32x2, .offset = 0 },
1418             .{ .location = 1, .format = .float32x2, .offset = 8 },
1419         },
1420         .bindings = &.{.{ .binding = 2, .kind = .uniform_buffer }},
1421         .push_constant_bytes = 4,
1422         .push_extent = 4,
1423         .payload = .{ .bytes = object },
1424     };
1425     var overrun = desc;
1426     overrun.push_extent = 8;
1427     try std.testing.expectError(error.PushConstantRangeExceeded, handle.createRenderArtifact(overrun));
1428     var artifact = try handle.createRenderArtifact(desc);
1429     defer artifact.deinit();
1430     const pipeline = try handle.loadRenderArtifact(&artifact);
1431     defer handle.destroyObject(pipeline.id);
1432     const bindings = try handle.createRenderBindings(.{
1433         .artifact = &artifact,
1434         .pipeline = pipeline,
1435         .resources = &.{.{ .uniform_buffer = uniform }},
1436     });
1437     defer handle.destroyObject(bindings.id);
1438     var push = std.mem.toBytes(one);
1439     var draws = [_]backend.RenderDraw{.{
1440         .pipeline = pipeline,
1441         .bindings = bindings,
1442         .vertex_buffers = &.{.{ .buffer = vertex_buffer }},
1443         .range = .{ .vertex_count = 3 },
1444         .push_constants = &push,
1445     }};
1446     const pass = backend.RenderPass{
1447         .color = .{ .view = .{ .texture = color, .format = .rgba8_unorm }, .load = .{ .clear = .{ .a = 0 } } },
1448         .viewport = .{ .width = 2, .height = 2 },
1449         .scissor = .{ .width = 2, .height = 2 },
1450         .draws = &draws,
1451     };
1452     var texels: [2 * 2 * 4]u8 = undefined;
1453     try handle.render(.{ .pass = pass });
1454     try handle.readTexture(.{ .texture = color, .bytes = &texels });
1455     try std.testing.expectEqual([4]u8{ 255, 255, 0, 255 }, texels[0..4].*);
1456 
1457     const bundle = try handle.recordRenderBundle(pass);
1458     defer handle.destroyObject(bundle.id);
1459     push = std.mem.toBytes(zero);
1460     try handle.writeBuffer(.{ .handle = uniform, .bytes = &@as([16]u8, @splat(0)) });
1461     try handle.submitRenderBundle(.{ .bundle = bundle });
1462     try handle.readTexture(.{ .texture = color, .bytes = &texels });
1463     try std.testing.expectEqual([4]u8{ 255, 0, 0, 255 }, texels[0..4].*);
1464 
1465     draws[0].push_constants = push[0..2];
1466     try std.testing.expectError(error.RenderArgumentMismatch, handle.render(.{ .pass = pass }));
1467 }