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 }