lib/gpu/src/vulkan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const choir_abi = @import("choir_abi");
3 const build_options = @import("build_options");
4 const sys = @import("sys");
5
6 const backend = @import("root.zig");
7 const runtime_root = @import("runtime/root.zig");
8
9 const launch_mod = runtime_root.vulkan.launch;
10 const raster_mod = runtime_root.vulkan.raster;
11 const dmabuf_mod = runtime_root.vulkan.dmabuf;
12 const memory_mod = runtime_root.vulkan.memory;
13 const runtime_mod = runtime_root.vulkan.runtime;
14 const driver_mod = sys.vulkan;
15
16 const Allocator = std.mem.Allocator;
17 const BackendObjectId = backend.BackendObjectId;
18 const Runtime = runtime_mod.Runtime;
19
20 const RuntimeStorage = union(enum) {
21 none,
22 borrowed: *Runtime,
23 owned: Runtime,
24
25 fn ptr(self: *RuntimeStorage) ?*Runtime {
26 return switch (self.*) {
27 .none => null,
28 .borrowed => |runtime| runtime,
29 .owned => |*runtime| runtime,
30 };
31 }
32
33 fn deinit(self: *RuntimeStorage) void {
34 switch (self.*) {
35 .none, .borrowed => {},
36 .owned => |*runtime| runtime.deinit(),
37 }
38 self.* = .none;
39 }
40 };
41
42 /// The raster state beside a runtime, made the first time a texture, pipeline or capability query
43 /// needs it. A runtime whose queue runs no graphics leaves it unavailable.
44 const RasterSlot = union(enum) {
45 untried,
46 unavailable,
47 ready: raster_mod.Raster,
48 };
49
50 pub const State = struct {
51 allocator: Allocator,
52 runtime: RuntimeStorage = .none,
53 next_id: BackendObjectId = 1,
54 objects: std.AutoHashMapUnmanaged(BackendObjectId, Object) = .{},
55 raster: RasterSlot = .untried,
56 /// A device's capabilities never change, so the first query answers every later one.
57 caps: ?backend.BackendCapabilities = null,
58 /// The dma-buf target whose frame is being recorded, the only time its images may be named.
59 presenting: ?BackendObjectId = null,
60
61 fn init(allocator: Allocator) State {
62 return .{
63 .allocator = allocator,
64 };
65 }
66
67 pub fn initDevice(allocator: Allocator, device_ordinal: i32) backend.BackendError!State {
68 var diagnostic: []const u8 = "none";
69 return initDeviceDiagnosed(allocator, device_ordinal, &diagnostic);
70 }
71
72 pub fn initDeviceDiagnosed(allocator: Allocator, device_ordinal: i32, diagnostic: *[]const u8) backend.BackendError!State {
73 const runtime = Runtime.init(allocator, device_ordinal) catch |err| {
74 diagnostic.* = @errorName(err);
75 return mapRuntimeError(err);
76 };
77 diagnostic.* = "none";
78 return .{
79 .allocator = allocator,
80 .runtime = .{ .owned = runtime },
81 };
82 }
83
84 fn initWithRuntime(allocator: Allocator, runtime: ?*Runtime) State {
85 return .{
86 .allocator = allocator,
87 .runtime = if (runtime) |rt| .{ .borrowed = rt } else .none,
88 };
89 }
90
91 pub fn deinit(self: *State) void {
92 var it = self.objects.iterator();
93 while (it.next()) |entry| {
94 deinitObject(self, entry.value_ptr);
95 }
96 self.objects.deinit(self.allocator);
97 self.objects = .{};
98 self.next_id = 1;
99 switch (self.raster) {
100 .ready => |*raster| raster.deinit(),
101 .untried, .unavailable => {},
102 }
103 self.raster = .untried;
104 self.caps = null;
105 self.runtime.deinit();
106 }
107
108 fn rasterState(self: *State) backend.BackendError!*raster_mod.Raster {
109 switch (self.raster) {
110 .ready => |*raster| return raster,
111 .unavailable => return error.CapabilityMismatch,
112 .untried => {},
113 }
114 const rt = self.runtime.ptr() orelse return error.RuntimeUnavailable;
115 const raster = raster_mod.Raster.init(rt) catch |err| switch (err) {
116 error.FeatureNotPresent => {
117 self.raster = .unavailable;
118 return error.CapabilityMismatch;
119 },
120 else => return mapRasterError(err, error.RuntimeUnavailable),
121 };
122 self.raster = .{ .ready = raster };
123 return &self.raster.ready;
124 }
125
126 /// The raster that made an existing raster object.
127 fn liveRaster(self: *State) *raster_mod.Raster {
128 return &self.raster.ready;
129 }
130
131 pub fn handle(self: *State) backend.BackendHandle {
132 return .{
133 .ptr = self,
134 .vtable = &vtable,
135 .kind = .vulkan,
136 };
137 }
138
139 fn putObject(self: *State, object: Object) backend.BackendError!BackendObjectId {
140 const id = self.next_id;
141 if (id == std.math.maxInt(BackendObjectId)) return error.OutOfMemory;
142 self.next_id += 1;
143 self.objects.put(self.allocator, id, object) catch return error.OutOfMemory;
144 return id;
145 }
146
147 fn getLoaded(self: *State, loaded: backend.LoadedArtifact) backend.BackendError!*LoadedKernel {
148 if (loaded.backend != .vulkan or loaded.format != .vulkan_spirv) return error.InvalidArtifact;
149 const object = self.objects.getPtr(loaded.id) orelse return error.InvalidArtifact;
150 return switch (object.*) {
151 .loaded_artifact => |*kernel| kernel,
152 else => error.InvalidArtifact,
153 };
154 }
155
156 fn getBuffer(self: *State, buffer_handle: backend.BufferHandle) backend.BackendError!*runtime_mod.DeviceBuffer {
157 if (buffer_handle.backend != .vulkan) return error.InvalidBuffer;
158 const object = self.objects.getPtr(buffer_handle.id) orelse return error.InvalidBuffer;
159 return switch (object.*) {
160 .loaded_artifact => error.InvalidBuffer,
161 .buffer => |*buffer| buffer,
162 else => error.InvalidBuffer,
163 };
164 }
165
166 fn getStream(self: *State, stream_handle: backend.StreamHandle) backend.BackendError!*runtime_mod.Stream {
167 if (stream_handle.backend != .vulkan) return error.InvalidStream;
168 const object = self.objects.getPtr(stream_handle.id) orelse return error.InvalidStream;
169 return switch (object.*) {
170 .stream => |stream| stream,
171 else => error.InvalidStream,
172 };
173 }
174
175 fn getEvent(self: *State, event_handle: backend.EventHandle) backend.BackendError!*runtime_mod.Event {
176 if (event_handle.backend != .vulkan) return error.InvalidEvent;
177 const object = self.objects.getPtr(event_handle.id) orelse return error.InvalidEvent;
178 return switch (object.*) {
179 .event => |event| event,
180 else => error.InvalidEvent,
181 };
182 }
183
184 fn getSurface(self: *State, surface_handle: backend.SurfaceHandle) backend.BackendError!*VulkanSurface {
185 if (surface_handle.backend != .vulkan) return error.InvalidSurface;
186 const object = self.objects.getPtr(surface_handle.id) orelse return error.InvalidSurface;
187 return switch (object.*) {
188 .surface => |*surface| surface,
189 else => error.InvalidSurface,
190 };
191 }
192
193 fn getTexture(self: *State, texture_handle: backend.TextureHandle) backend.BackendError!*VulkanTexture {
194 if (texture_handle.backend != .vulkan) return error.InvalidTexture;
195 const object = self.objects.getPtr(texture_handle.id) orelse return error.InvalidTexture;
196 return switch (object.*) {
197 .texture => |*texture| texture,
198 else => error.InvalidTexture,
199 };
200 }
201
202 /// An image the contract may name now. An image a dma-buf target shares with another process
203 /// is named only by its target's frame, since between frames that process owns it.
204 fn getImage(self: *State, texture_handle: backend.TextureHandle) backend.BackendError!*VulkanImage {
205 const image = try self.getAnyImage(texture_handle);
206 if (image.target) |owner| if (self.presenting != owner) return error.InvalidTexture;
207 return image;
208 }
209
210 fn getAnyImage(self: *State, texture_handle: backend.TextureHandle) backend.BackendError!*VulkanImage {
211 if (texture_handle.backend != .vulkan) return error.InvalidTexture;
212 const object = self.objects.getPtr(texture_handle.id) orelse return error.InvalidTexture;
213 return switch (object.*) {
214 .image => |*image| if (std.meta.eql(image.handle, texture_handle)) image else error.InvalidTexture,
215 else => error.InvalidTexture,
216 };
217 }
218
219 fn getRenderPipeline(self: *State, loaded: backend.LoadedRenderArtifact) backend.BackendError!*VulkanPipeline {
220 if (loaded.backend != .vulkan) return error.InvalidRenderArtifact;
221 const object = self.objects.getPtr(loaded.id) orelse return error.InvalidRenderArtifact;
222 return switch (object.*) {
223 .render_pipeline => |*pipeline| if (std.meta.eql(pipeline.loaded, loaded)) pipeline else error.InvalidRenderArtifact,
224 else => error.InvalidRenderArtifact,
225 };
226 }
227
228 fn getRenderBindings(self: *State, bindings: backend.RenderBindings) backend.BackendError!*VulkanBindings {
229 if (bindings.backend != .vulkan) return error.RenderArgumentMismatch;
230 const object = self.objects.getPtr(bindings.id) orelse return error.RenderArgumentMismatch;
231 return switch (object.*) {
232 .render_bindings => |*set| if (set.pipeline_id == bindings.pipeline) set else error.RenderArgumentMismatch,
233 else => error.RenderArgumentMismatch,
234 };
235 }
236
237 fn getRenderBundle(self: *State, bundle: backend.RenderBundle) backend.BackendError!*VulkanBundle {
238 if (bundle.backend != .vulkan) return error.RenderArgumentMismatch;
239 const object = self.objects.getPtr(bundle.id) orelse return error.RenderArgumentMismatch;
240 return switch (object.*) {
241 .render_bundle => |*recorded| if (recorded.draw_count == bundle.draw_count) recorded else error.RenderArgumentMismatch,
242 else => error.RenderArgumentMismatch,
243 };
244 }
245
246 fn submitStream(self: *State, stream_handle: ?backend.StreamHandle) backend.BackendError!*runtime_mod.Stream {
247 if (stream_handle) |handle_value| return self.getStream(handle_value);
248 const rt = self.runtime.ptr() orelse return error.RuntimeUnavailable;
249 return rt.defaultStream() catch |err| mapRuntimeError(err);
250 }
251
252 fn submitWaits(
253 self: *State,
254 handles: []const backend.EventHandle,
255 events: *[raster_mod.max_wait_events]*runtime_mod.Event,
256 ) backend.BackendError![]const *runtime_mod.Event {
257 if (handles.len > raster_mod.max_wait_events) return error.RenderArgumentMismatch;
258 for (handles, 0..) |event_handle, index| {
259 const event = try self.getEvent(event_handle);
260 if (!event.recorded) return error.InvalidEvent;
261 events[index] = event;
262 }
263 return events[0..handles.len];
264 }
265
266 /// Where the device placed a texture and a buffer, and which queue runs them. A witness reports
267 /// it so a run on another driver says what that driver chose.
268 pub fn placement(self: *State, texture: backend.TextureHandle, buffer: backend.BufferHandle) backend.BackendError!Placement {
269 const raster = try self.rasterState();
270 const rt = raster.runtime;
271 const image = try self.getImage(texture);
272 const device_buffer = try self.getBuffer(buffer);
273 return .{
274 .queue_family = rt.device.queueFamilyIndex(),
275 .graphics = rt.device.graphics(),
276 .buffer_image_granularity = raster.buffer_image_granularity,
277 .image_memory = switch (image.image.backing) {
278 .pooled => |allocation| memoryPlacement(rt, allocation),
279 .dedicated => return error.InvalidTexture,
280 },
281 .buffer_memory = memoryPlacement(rt, device_buffer._allocation),
282 };
283 }
284
285 /// Records `pass`'s targets and one bare draw command per draw into a command buffer it then
286 /// frees unsubmitted. It binds the first draw's pipeline, bindings and vertex buffers once and
287 /// checks nothing per draw, so its time is the least any path recording these draws on this
288 /// driver can take. The raster witness measures the contract's recording against it.
289 pub fn recordFloor(self: *State, pass: backend.RenderPass) backend.BackendError!void {
290 if (pass.draws.len == 0) return error.RenderArgumentMismatch;
291 const raster = try self.rasterState();
292 const drv = &raster.runtime._driver;
293 const cb = raster_mod.beginBundle(raster) catch |err| return mapRasterError(err, error.RenderFailed);
294 defer raster.discardBundle(cb);
295 const targets = try passTargets(self, pass);
296 const first = pass.draws[0];
297 const pipeline = try self.getRenderPipeline(first.pipeline);
298 raster_mod.beginPass(drv, cb, targets);
299 drv.vkCmdBindPipeline(cb, driver_mod.VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline.pipeline.pipeline);
300 if (first.bindings) |bindings| {
301 const set = try self.getRenderBindings(bindings);
302 drv.vkCmdBindDescriptorSets(cb, driver_mod.VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline.pipeline.layout, 0, 1, @ptrCast(&set.set.set), 0, null);
303 }
304 for (first.vertex_buffers, pipeline.layouts[0..first.vertex_buffers.len]) |range, layout| {
305 const buffer = try self.getBuffer(range.buffer);
306 drv.vkCmdBindVertexBuffers(cb, layout.binding, 1, @ptrCast(&buffer._buffer), @ptrCast(&range.offset));
307 }
308 for (pass.draws) |draw| {
309 if (draw.push_constants.len != 0) {
310 const size: u32 = @intCast(draw.push_constants.len);
311 drv.vkCmdPushConstants(cb, pipeline.pipeline.layout, raster_mod.push_constant_stages, 0, size, draw.push_constants.ptr);
312 }
313 drv.vkCmdDraw(cb, draw.range.vertex_count, draw.range.instance_count, draw.range.first_vertex, draw.range.first_instance);
314 }
315 raster_mod.endPass(drv, cb, targets);
316 drv.fromResult(drv.vkEndCommandBuffer(cb)) catch |err| return mapRasterError(err, error.RenderFailed);
317 }
318
319 /// Makes a ring of color images another process on `desc.device` can import as dma-bufs, each
320 /// registered as a texture a frame's pass renders into. See `runtime/vulkan/dmabuf.zig` for the
321 /// frame protocol.
322 pub fn createDmabufTarget(self: *State, desc: DmabufTargetDesc) DmabufError!BackendObjectId {
323 const raster = try self.rasterState();
324 if (desc.format.isDepth()) return error.CapabilityMismatch;
325 if (desc.image_count > dmabuf_mod.max_images) return error.CapabilityMismatch;
326 const id = self.next_id;
327 if (id > std.math.maxInt(BackendObjectId) - 1 - @as(BackendObjectId, desc.image_count)) return error.OutOfMemory;
328 try self.objects.ensureUnusedCapacity(self.allocator, 1 + desc.image_count);
329 const shared = try self.allocator.create(VulkanDmabufTarget);
330 errdefer self.allocator.destroy(shared);
331 var images: [dmabuf_mod.max_images]raster_mod.Image = undefined;
332 shared.* = .{
333 .target = dmabuf_mod.Target.create(raster, .{
334 .width = desc.width,
335 .height = desc.height,
336 .format = try vulkanFormat(desc.format),
337 .modifiers = desc.modifiers,
338 .image_count = desc.image_count,
339 .device = desc.device,
340 }, &images) catch |err| return mapDmabufError(err),
341 .textures = undefined,
342 };
343 self.next_id += 1 + desc.image_count;
344 self.objects.putAssumeCapacity(id, .{ .dmabuf_target = shared });
345 for (images[0..desc.image_count], shared.textures[0..desc.image_count], 1..) |image, *texture, offset| {
346 texture.* = .{
347 .id = id + @as(BackendObjectId, @intCast(offset)),
348 .backend = .vulkan,
349 .extent = .{ .width = desc.width, .height = desc.height, .depth = 1 },
350 .format = desc.format,
351 .usage = .{ .color_attachment = true },
352 .sample_count = 1,
353 .ownership = .backend,
354 };
355 self.objects.putAssumeCapacity(texture.id, .{ .image = .{ .handle = texture.*, .image = image, .target = id } });
356 }
357 return id;
358 }
359
360 /// Waits up to a second for the consumer to release every image, then frees the target and
361 /// its textures.
362 pub fn destroyDmabufTarget(self: *State, id: BackendObjectId) DmabufError!void {
363 const shared = try self.getDmabufTarget(id);
364 if (shared.target.open != null) return error.RenderArgumentMismatch;
365 const count = shared.target.count;
366 const textures = shared.textures;
367 var target = self.objects.fetchRemove(id).?.value;
368 deinitObject(self, &target);
369 for (textures[0..count]) |texture| {
370 var image = self.objects.fetchRemove(texture.id).?.value;
371 deinitObject(self, &image);
372 }
373 }
374
375 /// Image `index` of a target: its texture, its dma-buf and its timeline's descriptor. The
376 /// target owns both descriptors.
377 pub fn dmabufImage(self: *State, id: BackendObjectId, index: u32) DmabufError!DmabufImage {
378 const shared = try self.getDmabufTarget(id);
379 if (index >= shared.target.count) return error.InvalidTexture;
380 return .{
381 .texture = shared.textures[index],
382 .dmabuf = shared.target.dmabuf(index),
383 .timeline_fd = shared.target.timelineFd(index),
384 };
385 }
386
387 pub fn dmabufImageCount(self: *State, id: BackendObjectId) DmabufError!u32 {
388 return (try self.getDmabufTarget(id)).target.count;
389 }
390
391 /// The highest signalled point on image `index`'s timeline.
392 pub fn dmabufSignalledPoint(self: *State, id: BackendObjectId, index: u32) DmabufError!u64 {
393 const shared = try self.getDmabufTarget(id);
394 if (index >= shared.target.count) return error.InvalidTexture;
395 return shared.target.signalledPoint(index) catch |err| mapDmabufError(err);
396 }
397
398 /// Picks the target's next image, waiting until `deadline_ns` on `CLOCK_MONOTONIC` for the
399 /// consumer to release it. The frame's pass must render into the returned texture.
400 pub fn beginDmabufFrame(self: *State, id: BackendObjectId, deadline_ns: i64) DmabufError!DmabufFrame {
401 const shared = try self.getDmabufTarget(id);
402 if (shared.broken) return error.RenderFailed;
403 if (shared.target.open != null) return error.RenderArgumentMismatch;
404 const frame = shared.target.begin(deadline_ns) catch |err| return mapDmabufError(err);
405 return .{
406 .index = frame.index,
407 .texture = shared.textures[frame.index],
408 .acquire_point = frame.acquire_point,
409 .release_point = frame.release_point,
410 };
411 }
412
413 /// Records `pass` between the barriers that take the frame's image from the consumer and hand
414 /// it back, submits it on the default stream, and attaches its completion to the frame's
415 /// acquire point. A failure before the submission queues leaves the frame open to try again.
416 pub fn submitDmabufFrame(self: *State, id: BackendObjectId, frame: DmabufFrame, pass: backend.RenderPass) DmabufError!void {
417 const raster = try self.rasterState();
418 const shared = try self.getDmabufTarget(id);
419 const inner = dmabuf_mod.Frame{ .index = frame.index, .acquire_point = frame.acquire_point, .release_point = frame.release_point };
420 if (!std.meta.eql(shared.target.open, inner)) return error.RenderArgumentMismatch;
421 if (!std.meta.eql(pass.color.view.texture, shared.textures[frame.index])) return error.RenderArgumentMismatch;
422 const stream = raster.runtime.defaultStream() catch |err| return mapRuntimeError(err);
423 const cb = raster_mod.beginStreamPass(stream) catch |err| return mapRasterError(err, error.RenderFailed);
424 self.presenting = id;
425 defer self.presenting = null;
426 shared.target.recordAcquire(cb, inner);
427 recordPass(self, raster, cb, pass) catch |err| {
428 runtime_mod.retireStreamCommandBuffer(stream, cb, stream.counter);
429 return err;
430 };
431 shared.target.recordRelease(cb, inner);
432 const drv = &raster.runtime._driver;
433 drv.fromResult(drv.vkEndCommandBuffer(cb)) catch |err| {
434 runtime_mod.retireStreamCommandBuffer(stream, cb, stream.counter);
435 return mapRasterError(err, error.RenderFailed);
436 };
437 raster_mod.submit(raster, .{
438 .stream = stream,
439 .cb = cb,
440 .wait_events = &.{},
441 .signal_event = null,
442 .retire = true,
443 .signal_semaphore = shared.target.semaphore,
444 }) catch |err| return mapRasterError(err, error.RenderFailed);
445 shared.target.finish(inner) catch |err| {
446 shared.broken = true;
447 return mapDmabufError(err);
448 };
449 }
450
451 /// Signals the release point of a submitted frame the consumer never received, such as one a
452 /// window refused, so the image can be used again.
453 pub fn reclaimDmabufFrame(self: *State, id: BackendObjectId, frame: DmabufFrame) DmabufError!void {
454 const shared = try self.getDmabufTarget(id);
455 if (frame.index >= shared.target.count) return error.RenderArgumentMismatch;
456 if (frame.release_point != 2 * shared.target.slots[frame.index].uses) return error.RenderArgumentMismatch;
457 shared.target.reclaim(.{ .index = frame.index, .acquire_point = frame.acquire_point, .release_point = frame.release_point }) catch |err|
458 return mapDmabufError(err);
459 }
460
461 /// Submits an empty command buffer that signals the target's semaphore, and moves its fence
462 /// onto a scratch timeline as a frame's would be. It is the least a frame's submission and
463 /// sync bridge can cost on this driver, and a witness measures `submitDmabufFrame` against it.
464 pub fn dmabufBridgeFloor(self: *State, id: BackendObjectId) DmabufError!void {
465 const raster = try self.rasterState();
466 const shared = try self.getDmabufTarget(id);
467 if (shared.broken or shared.target.open != null) return error.RenderArgumentMismatch;
468 const stream = raster.runtime.defaultStream() catch |err| return mapRuntimeError(err);
469 const cb = raster_mod.beginStreamPass(stream) catch |err| return mapRasterError(err, error.RenderFailed);
470 const drv = &raster.runtime._driver;
471 drv.fromResult(drv.vkEndCommandBuffer(cb)) catch |err| {
472 runtime_mod.retireStreamCommandBuffer(stream, cb, stream.counter);
473 return mapRasterError(err, error.RenderFailed);
474 };
475 raster_mod.submit(raster, .{
476 .stream = stream,
477 .cb = cb,
478 .wait_events = &.{},
479 .signal_event = null,
480 .retire = true,
481 .signal_semaphore = shared.target.semaphore,
482 }) catch |err| return mapRasterError(err, error.RenderFailed);
483 shared.target.bridgeScratch() catch |err| {
484 shared.broken = true;
485 return mapDmabufError(err);
486 };
487 }
488
489 fn getDmabufTarget(self: *State, id: BackendObjectId) backend.BackendError!*VulkanDmabufTarget {
490 const object = self.objects.getPtr(id) orelse return error.InvalidTexture;
491 return switch (object.*) {
492 .dmabuf_target => |shared| shared,
493 else => error.InvalidTexture,
494 };
495 }
496
497 fn getFrame(self: *State, frame_handle: backend.SurfaceFrame) backend.BackendError!*VulkanSurfaceFrame {
498 if (frame_handle.backend != .vulkan) return error.InvalidSurfaceFrame;
499 const object = self.objects.getPtr(frame_handle.id) orelse return error.InvalidSurfaceFrame;
500 return switch (object.*) {
501 .surface_frame => |*frame| frame,
502 else => error.InvalidSurfaceFrame,
503 };
504 }
505 };
506
507 pub const DmabufError = backend.BackendError || error{
508 /// The device did not enable dma-buf export or cannot name its render node.
509 NoDmabufDevice,
510 /// The consumer wants buffers from another device.
511 DeviceMismatch,
512 /// The device can render the format with none of the modifiers the consumer lists.
513 NoSharedModifier,
514 /// The render node would not open for this process.
515 RenderNodeUnavailable,
516 /// The consumer had not released the image by the deadline.
517 Timeout,
518 };
519
520 pub const DmabufTargetDesc = struct {
521 width: u32,
522 height: u32,
523 format: backend.TextureFormat,
524 /// Modifiers of the format the consumer can import, such as a compositor's dma-buf feedback
525 /// lists for its main device.
526 modifiers: []const u64,
527 image_count: u32,
528 device: sys.drm.DeviceNumber,
529 };
530
531 pub const DmabufImage = struct {
532 texture: backend.TextureHandle,
533 dmabuf: dmabuf_mod.Dmabuf,
534 timeline_fd: i32,
535 };
536
537 /// One image's turn. The consumer may read the image once `acquire_point` on its timeline
538 /// signals, and signals `release_point` when it is done.
539 pub const DmabufFrame = struct {
540 index: u32,
541 texture: backend.TextureHandle,
542 acquire_point: u64,
543 release_point: u64,
544 };
545
546 fn mapDmabufError(err: dmabuf_mod.Error) DmabufError {
547 return switch (err) {
548 error.NoDmabufDevice, error.UnsupportedPlatform, error.Unsupported => error.NoDmabufDevice,
549 error.DeviceMismatch => error.DeviceMismatch,
550 error.NoSharedModifier => error.NoSharedModifier,
551 error.OpenFailed, error.AccessDenied => error.RenderNodeUnavailable,
552 error.Timeout => error.Timeout,
553 error.OutOfMemory, error.OutOfHostMemory, error.OutOfDeviceMemory => error.OutOfMemory,
554 error.DeviceLost => error.DeviceLost,
555 error.InvalidHandle, error.Failed => error.RenderFailed,
556 else => |vulkan_err| mapRasterError(vulkan_err, error.RenderFailed),
557 };
558 }
559
560 /// A memory type by index, with its property flags and the size of the heap it draws on.
561 pub const MemoryPlacement = struct {
562 type_index: u32,
563 property_flags: driver_mod.VkMemoryPropertyFlags,
564 heap_bytes: u64,
565 };
566
567 pub const Placement = struct {
568 queue_family: u32,
569 graphics: bool,
570 buffer_image_granularity: u64,
571 image_memory: MemoryPlacement,
572 buffer_memory: MemoryPlacement,
573 };
574
575 fn memoryPlacement(rt: *runtime_mod.Runtime, allocation: memory_mod.Allocation) MemoryPlacement {
576 const index = rt._memory.memoryTypeIndex(allocation);
577 const memory_type = rt._memory.properties.memoryTypes[index];
578 return .{
579 .type_index = index,
580 .property_flags = memory_type.propertyFlags,
581 .heap_bytes = rt._memory.properties.memoryHeaps[memory_type.heapIndex].size,
582 };
583 }
584
585 const LoadedKernel = struct {
586 kernel: launch_mod.Kernel,
587 entry_name: [:0]u8,
588 buffer_argument_count: u32,
589 scalar_argument_count: u32,
590 push_constants: choir_abi.PushConstants,
591 };
592
593 const Object = union(enum) {
594 loaded_artifact: LoadedKernel,
595 buffer: runtime_mod.DeviceBuffer,
596 stream: *runtime_mod.Stream,
597 event: *runtime_mod.Event,
598 surface: VulkanSurface,
599 texture: VulkanTexture,
600 surface_frame: VulkanSurfaceFrame,
601 image: VulkanImage,
602 render_pipeline: VulkanPipeline,
603 render_bindings: VulkanBindings,
604 render_bundle: VulkanBundle,
605 dmabuf_target: *VulkanDmabufTarget,
606 };
607
608 /// A texture this backend allocated, as opposed to a swapchain image a surface lent it.
609 const VulkanImage = struct {
610 handle: backend.TextureHandle,
611 image: raster_mod.Image,
612 /// The dma-buf target that shares the image, if one does.
613 target: ?BackendObjectId = null,
614 };
615
616 const VulkanDmabufTarget = struct {
617 target: dmabuf_mod.Target,
618 textures: [dmabuf_mod.max_images]backend.TextureHandle,
619 /// A frame whose submission queued but whose fence never reached the timeline. The target's
620 /// semaphore may still be signalled, so the target takes no more frames.
621 broken: bool = false,
622 };
623
624 const VertexLayout = struct {
625 binding: u32,
626 stride: u32,
627 per_instance: bool,
628 };
629
630 /// A graphics pipeline and the facts its draws are checked against: the loaded description, the
631 /// Vulkan binding each vertex layout feeds, and the pipeline's resource bindings in order.
632 const VulkanPipeline = struct {
633 pipeline: raster_mod.Pipeline,
634 loaded: backend.LoadedRenderArtifact,
635 layouts: [raster_mod.max_vertex_buffers]VertexLayout,
636 bindings: [raster_mod.max_descriptors]backend.RenderBindingDesc,
637 };
638
639 const VulkanBindings = struct {
640 set: raster_mod.BindingSet,
641 pipeline_id: BackendObjectId,
642 };
643
644 const VulkanBundle = struct {
645 command_buffer: driver_mod.VkCommandBuffer,
646 draw_count: u32,
647 };
648
649 const VulkanSurface = struct {
650 surface: runtime_mod.Surface,
651 handle: backend.SurfaceHandle,
652 acquired_frame: ?BackendObjectId = null,
653 acquired_texture: ?BackendObjectId = null,
654 };
655
656 const VulkanTexture = struct {
657 handle: backend.TextureHandle,
658 image: runtime_mod.SurfaceImage,
659 };
660
661 const VulkanSurfaceFrame = struct {
662 surface_id: BackendObjectId,
663 texture_id: BackendObjectId,
664 image_index: u32,
665 generation: u64,
666 written: bool = false,
667 presented: bool = false,
668 };
669
670 pub fn staticCapabilities() backend.BackendCapabilities {
671 return capabilitiesFrom(null, false);
672 }
673
674 fn capabilitiesFrom(caps: ?runtime_mod.Runtime.Caps, has_runtime: bool) backend.BackendCapabilities {
675 var dtypes = backend.DTypeSet.init(&.{ .i32, .u32, .f32 });
676 if (caps) |actual| {
677 if (actual.storage_buffer8) dtypes.insert(.i1);
678 if (actual.shader_float16 and actual.storage_buffer16) dtypes.insert(.f16);
679 if (actual.shader_float64) dtypes.insert(.f64);
680 if (actual.shader_int8 and actual.storage_buffer8) {
681 dtypes.insert(.i8);
682 dtypes.insert(.u8);
683 }
684 if (actual.shader_int16 and actual.storage_buffer16) {
685 dtypes.insert(.i16);
686 dtypes.insert(.u16);
687 }
688 if (actual.shader_int64) {
689 dtypes.insert(.i64);
690 dtypes.insert(.u64);
691 }
692 } else {
693 dtypes.insert(.i1);
694 }
695
696 const subgroup_size_reported = if (caps) |actual| actual.subgroup_size else 0;
697 const subgroup_stages = if (caps) |actual| actual.subgroup_supported_stages else 0;
698 const subgroup_operations = if (caps) |actual| actual.subgroup_supported_operations else 0;
699 const subgroup_supported = subgroup_size_reported != 0 and
700 hasAllBits(subgroup_stages, driver_mod.VK_SHADER_STAGE_COMPUTE_BIT) and
701 hasAllBits(subgroup_operations, driver_mod.VK_SUBGROUP_FEATURE_BASIC_BIT);
702 const subgroup_size = if (subgroup_supported) subgroup_size_reported else 0;
703 return .{
704 .identity = .{
705 .backend = .vulkan,
706 .family = .vulkan,
707 .name = if (caps) |actual| actual.vendor_name else "vulkan",
708 .vendor_id = if (caps) |actual| actual.vendor_id else null,
709 },
710 .memory = .{
711 .min_buffer_alignment = 16,
712 .host_visible_device_memory = true,
713 },
714 .subgroup = .{
715 .supported = subgroup_supported,
716 .size_min = subgroup_size,
717 .size_max = subgroup_size,
718 .shuffle = subgroup_supported and hasAllBits(
719 subgroup_operations,
720 driver_mod.VK_SUBGROUP_FEATURE_SHUFFLE_BIT |
721 driver_mod.VK_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT,
722 ),
723 .ballot = subgroup_supported and
724 hasAllBits(subgroup_operations, driver_mod.VK_SUBGROUP_FEATURE_BALLOT_BIT),
725 .vote = subgroup_supported and
726 hasAllBits(subgroup_operations, driver_mod.VK_SUBGROUP_FEATURE_VOTE_BIT),
727 .arithmetic = subgroup_supported and
728 hasAllBits(subgroup_operations, driver_mod.VK_SUBGROUP_FEATURE_ARITHMETIC_BIT),
729 .scan = subgroup_supported and
730 hasAllBits(subgroup_operations, driver_mod.VK_SUBGROUP_FEATURE_ARITHMETIC_BIT),
731 },
732 .threadgroup = .{
733 .max_threads = 1024,
734 .max_blocks = .{ 65_535, 65_535, 65_535 },
735 .max_threads_per_dim = .{ 1024, 1024, 64 },
736 .max_grid_per_dim = .{ 65_535, 65_535, 65_535 },
737 },
738 .dtypes = dtypes,
739 .layouts = .{
740 .row_major = true,
741 .compact_strides = true,
742 .broadcast_strides = true,
743 .tiled = true,
744 .opaque_backend_layouts = true,
745 },
746 .runtime = .{
747 .driver_loaded = has_runtime,
748 .device_context = has_runtime,
749 .streams = true,
750 .events = true,
751 .timeline_events = true,
752 },
753 .features = .{
754 .atomic_i32 = true,
755 .atomic_u32 = true,
756 .atomic_index = true,
757 },
758 .artifact_formats = backend.ArtifactFormatSet.init(&.{.vulkan_spirv}),
759 .surfaces = .{
760 .supported = has_runtime,
761 .platforms = backend.SurfacePlatformSet.init(&.{.x11}),
762 .formats = backend.TextureFormatSet.init(&.{ .rgba8_unorm, .bgra8_unorm }),
763 .color_spaces = backend.ColorSpaceSet.init(&.{.srgb}),
764 .present_modes = backend.PresentModeSet.init(&.{ .fifo, .mailbox, .immediate }),
765 .usages = .{
766 .copy_src = true,
767 .copy_dst = true,
768 .color_attachment = true,
769 .present = true,
770 },
771 .max_extent = .{ .width = 16_384, .height = 16_384 },
772 .max_frames_in_flight = 4,
773 },
774 };
775 }
776
777 fn hasAllBits(actual: u32, required: u32) bool {
778 return actual & required == required;
779 }
780
781 fn queryCapabilities(ptr: *anyopaque) backend.BackendError!backend.BackendCapabilities {
782 const state: *State = @ptrCast(@alignCast(ptr));
783 if (state.caps) |caps| return caps;
784 const rt = state.runtime.ptr() orelse return staticCapabilities();
785 const runtime_caps = rt.queryCapabilities() catch |err| return mapRuntimeError(err);
786 var caps = capabilitiesFrom(runtime_caps, true);
787 caps.float_controls = queryFloatControls(rt);
788 if (state.rasterState()) |raster| {
789 addRasterCapabilities(&caps, raster);
790 } else |err| switch (err) {
791 error.CapabilityMismatch => {},
792 else => return err,
793 }
794 state.caps = caps;
795 return caps;
796 }
797
798 /// VkPhysicalDeviceFloatControlsProperties is a Vulkan 1.2 properties-chain node.
799 const FloatProperties = extern struct {
800 sType: driver_mod.VkStructureType = 1000197000,
801 pNext: ?*anyopaque = null,
802 denormBehaviorIndependence: u32 = 0,
803 roundingModeIndependence: u32 = 0,
804 shaderSignedZeroInfNanPreserveFloat16: driver_mod.VkBool32 = 0,
805 shaderSignedZeroInfNanPreserveFloat32: driver_mod.VkBool32 = 0,
806 shaderSignedZeroInfNanPreserveFloat64: driver_mod.VkBool32 = 0,
807 shaderDenormPreserveFloat16: driver_mod.VkBool32 = 0,
808 shaderDenormPreserveFloat32: driver_mod.VkBool32 = 0,
809 shaderDenormPreserveFloat64: driver_mod.VkBool32 = 0,
810 shaderDenormFlushToZeroFloat16: driver_mod.VkBool32 = 0,
811 shaderDenormFlushToZeroFloat32: driver_mod.VkBool32 = 0,
812 shaderDenormFlushToZeroFloat64: driver_mod.VkBool32 = 0,
813 shaderRoundingModeRTEFloat16: driver_mod.VkBool32 = 0,
814 shaderRoundingModeRTEFloat32: driver_mod.VkBool32 = 0,
815 shaderRoundingModeRTEFloat64: driver_mod.VkBool32 = 0,
816 shaderRoundingModeRTZFloat16: driver_mod.VkBool32 = 0,
817 shaderRoundingModeRTZFloat32: driver_mod.VkBool32 = 0,
818 shaderRoundingModeRTZFloat64: driver_mod.VkBool32 = 0,
819 };
820
821 fn queryFloatControls(rt: *Runtime) backend.FloatControlFacts {
822 var float_props = FloatProperties{};
823 var props = driver_mod.VkPhysicalDeviceProperties2{
824 .sType = driver_mod.VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
825 .pNext = @ptrCast(&float_props),
826 .properties = undefined,
827 };
828 rt._driver.vkGetPhysicalDeviceProperties2(rt._physical_device, &props);
829 return floatFactsFrom(float_props);
830 }
831
832 fn floatFactsFrom(float_props: FloatProperties) backend.FloatControlFacts {
833 return .{
834 .denorm_preserve = .{
835 .f16 = float_props.shaderDenormPreserveFloat16 == driver_mod.VK_TRUE,
836 .f32 = float_props.shaderDenormPreserveFloat32 == driver_mod.VK_TRUE,
837 .f64 = float_props.shaderDenormPreserveFloat64 == driver_mod.VK_TRUE,
838 },
839 .signed_zero_inf_nan_preserve = .{
840 .f16 = float_props.shaderSignedZeroInfNanPreserveFloat16 == driver_mod.VK_TRUE,
841 .f32 = float_props.shaderSignedZeroInfNanPreserveFloat32 == driver_mod.VK_TRUE,
842 .f64 = float_props.shaderSignedZeroInfNanPreserveFloat64 == driver_mod.VK_TRUE,
843 },
844 .denorm_behavior_independence = switch (float_props.denormBehaviorIndependence) {
845 0 => .bit32_only,
846 1 => .all,
847 2 => .none,
848 else => null,
849 },
850 };
851 }
852
853 test "vulkan float controls report independent support for each width" {
854 var props = FloatProperties{};
855 props.denormBehaviorIndependence = @backingInt(backend.FloatControlIndependence.bit32_only);
856 props.shaderDenormPreserveFloat32 = driver_mod.VK_TRUE;
857 props.shaderSignedZeroInfNanPreserveFloat16 = driver_mod.VK_TRUE;
858 const facts = floatFactsFrom(props);
859 try std.testing.expectEqual(@as(?backend.FloatControlIndependence, .bit32_only), facts.denorm_behavior_independence);
860 try std.testing.expect(facts.denorm_preserve.f32);
861 try std.testing.expect(!facts.denorm_preserve.f16);
862 try std.testing.expect(facts.signed_zero_inf_nan_preserve.f16);
863 try std.testing.expect(!facts.signed_zero_inf_nan_preserve.f64);
864 props.denormBehaviorIndependence = 32;
865 try std.testing.expectEqual(null, floatFactsFrom(props).denorm_behavior_independence);
866 }
867
868 const color_formats = [_]backend.TextureFormat{ .rgba8_unorm, .bgra8_unorm, .rgba8_srgb, .bgra8_srgb };
869
870 /// Every feature a color texture needs for any usage the textures report: sampling with either
871 /// filter, blended attachment writes, and both transfer directions.
872 const color_features = driver_mod.VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
873 driver_mod.VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT |
874 driver_mod.VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT |
875 driver_mod.VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT |
876 driver_mod.VK_FORMAT_FEATURE_TRANSFER_SRC_BIT |
877 driver_mod.VK_FORMAT_FEATURE_TRANSFER_DST_BIT;
878
879 const depth_features = driver_mod.VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
880 driver_mod.VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT |
881 driver_mod.VK_FORMAT_FEATURE_TRANSFER_SRC_BIT |
882 driver_mod.VK_FORMAT_FEATURE_TRANSFER_DST_BIT;
883
884 /// Reports textures and rasterization for the formats whose every needed feature the device
885 /// reports, so a texture of a reported format serves every reported usage its aspect allows.
886 fn addRasterCapabilities(caps: *backend.BackendCapabilities, raster: *const raster_mod.Raster) void {
887 var targets: backend.TextureFormatSet = .{};
888 for (color_formats) |format| {
889 const vk_format = vulkanFormat(format) catch unreachable;
890 if (hasAllBits(raster.formatFeatures(vk_format), color_features)) targets.insert(format);
891 }
892 var depths: backend.TextureFormatSet = .{};
893 if (hasAllBits(raster.formatFeatures(driver_mod.VK_FORMAT_D32_SFLOAT), depth_features)) depths.insert(.depth32_float);
894 var formats = targets;
895 formats.bits |= depths.bits;
896 caps.textures = .{
897 .supported = true,
898 .formats = formats,
899 .usages = .{
900 .copy_src = true,
901 .copy_dst = true,
902 .sampled = true,
903 .color_attachment = true,
904 .depth_attachment = true,
905 },
906 .max_extent = .{ .width = raster.max_image_extent, .height = raster.max_image_extent, .depth = 1 },
907 .max_sample_count = 1,
908 };
909 caps.raster = .{
910 .supported = true,
911 .artifact_formats = backend.RenderArtifactFormatSet.init(&.{.vulkan_spirv}),
912 .target_formats = targets,
913 .depth_formats = depths,
914 .blend_modes = backend.RenderBlendModeSet.init(&.{ .replace, .alpha_premultiplied, .alpha_straight, .additive }),
915 .topologies = backend.RenderPrimitiveTopologySet.init(&.{ .triangle_list, .triangle_strip, .line_list, .line_strip }),
916 .vertex_formats = backend.RenderVertexFormatSet.init(&.{ .float32, .float32x2, .float32x3, .float32x4, .uint32, .uint32x2, .uint32x4 }),
917 .binding_kinds = backend.RenderBindingKindSet.init(&.{ .uniform_buffer, .sampled_texture }),
918 .index_formats = backend.RenderIndexFormatSet.init(&.{ .none, .u16, .u32 }),
919 .max_vertex_buffers = raster.max_vertex_buffers,
920 .max_vertex_attributes = raster.max_vertex_attributes,
921 .max_bindings = raster_mod.max_descriptors,
922 .instancing = true,
923 .max_push_constant_bytes = raster.max_push_constant_bytes,
924 .depth_bias = true,
925 .depth_bias_clamp = raster.runtime._depth_bias_clamp,
926 };
927 }
928
929 fn createArtifact(ptr: *anyopaque, request: backend.CompileRequest) backend.BackendError!backend.KernelArtifact {
930 if (request.requested_format != .vulkan_spirv) return error.UnsupportedOperation;
931 const state: *State = @ptrCast(@alignCast(ptr));
932 return switch (request.payload) {
933 .words_u32 => |words| createSpirvArtifact(state, request, words),
934 .none => error.UnsupportedOperation,
935 else => error.UnsupportedOperation,
936 };
937 }
938
939 fn createSpirvArtifact(
940 state: *State,
941 request: backend.CompileRequest,
942 words: []const u32,
943 ) backend.BackendError!backend.KernelArtifact {
944 if (request.kernel_name.len == 0) return error.InvalidArtifact;
945 if (words.len == 0) return error.InvalidArtifact;
946
947 var artifact = backend.KernelArtifact.init(state.allocator, .{
948 .backend = .vulkan,
949 .format = .vulkan_spirv,
950 .entry_name = request.kernel_name,
951 .argument_count = request.argument_count,
952 .scalar_argument_count = request.scalar_argument_count,
953 .diagnostic_id = request.diagnostic_id,
954 }) catch return error.OutOfMemory;
955 errdefer artifact.deinit();
956 try artifact.setOwnedWords(words);
957 return artifact;
958 }
959
960 fn loadArtifact(ptr: *anyopaque, artifact: *const backend.KernelArtifact) backend.BackendError!backend.LoadedArtifact {
961 const state: *State = @ptrCast(@alignCast(ptr));
962 if (artifact.backend != .vulkan) return error.CapabilityMismatch;
963 if (artifact.format != .vulkan_spirv) return error.UnsupportedArtifactFormat;
964 if (artifact.entry_name.len == 0) return error.InvalidArtifact;
965 const buffer_argument_count = try artifact.bufferArgumentCount();
966 if (buffer_argument_count > runtime_mod.max_buffers_per_set) return error.InvalidArtifact;
967
968 const words = switch (artifact.payload) {
969 .words_u32 => |words| words,
970 else => return error.InvalidArtifact,
971 };
972 if (words.len == 0) return error.InvalidArtifact;
973
974 const rt = state.runtime.ptr() orelse return error.RuntimeUnavailable;
975 const entry_name = state.allocator.dupeSentinel(u8, artifact.entry_name, 0) catch return error.OutOfMemory;
976 errdefer state.allocator.free(entry_name);
977
978 const push_constants = artifact.interface.push_constants;
979 if (push_constants.count != artifact.scalar_argument_count) return error.InvalidArtifact;
980 const push_constant_size: u32 = push_constants.byte_size;
981 var kernel = launch_mod.compile(rt, words, entry_name, buffer_argument_count, push_constant_size) catch |err| return mapArtifactError(err);
982 errdefer kernel.deinit();
983
984 const id = try state.putObject(.{ .loaded_artifact = .{
985 .kernel = kernel,
986 .entry_name = entry_name,
987 .buffer_argument_count = buffer_argument_count,
988 .scalar_argument_count = artifact.scalar_argument_count,
989 .push_constants = push_constants,
990 } });
991 return .{
992 .id = id,
993 .backend = .vulkan,
994 .format = .vulkan_spirv,
995 };
996 }
997
998 fn allocateBuffer(ptr: *anyopaque, request: backend.BufferAllocation) backend.BackendError!backend.BufferHandle {
999 const state: *State = @ptrCast(@alignCast(ptr));
1000 if (request.byte_size == 0) return error.InvalidBuffer;
1001 const rt = state.runtime.ptr() orelse return error.RuntimeUnavailable;
1002
1003 var buffer = runtime_mod.DeviceBuffer.alloc(rt, request.byte_size) catch |err| return mapAllocationError(err);
1004 errdefer buffer.deinit();
1005
1006 const id = try state.putObject(.{ .buffer = buffer });
1007 return .{
1008 .id = id,
1009 .backend = .vulkan,
1010 .byte_size = request.byte_size,
1011 .ownership = .backend,
1012 };
1013 }
1014
1015 fn createSurface(ptr: *anyopaque, request: backend.SurfaceCreationRequest) backend.BackendError!backend.SurfaceHandle {
1016 const state: *State = @ptrCast(@alignCast(ptr));
1017 const rt = state.runtime.ptr() orelse return error.RuntimeUnavailable;
1018 const desc = try surfaceDescFromRequest(request);
1019
1020 var surface = rt.createXlibSurface(desc) catch |err| return mapSurfaceRuntimeError(err);
1021 errdefer surface.deinit();
1022
1023 const handle = backend.SurfaceHandle{
1024 .id = 0,
1025 .backend = .vulkan,
1026 .platform = request.platform.kind(),
1027 .extent = .{ .width = surface.extent.width, .height = surface.extent.height },
1028 .format = request.format,
1029 .color_space = request.color_space,
1030 .present_mode = request.present_mode,
1031 .generation = surface.generation,
1032 };
1033 const id = try state.putObject(.{ .surface = .{
1034 .surface = surface,
1035 .handle = handle,
1036 } });
1037 const object = state.objects.getPtr(id).?;
1038 switch (object.*) {
1039 .surface => |*stored| stored.handle.id = id,
1040 else => unreachable,
1041 }
1042 return switch (object.*) {
1043 .surface => |*stored| stored.handle,
1044 else => unreachable,
1045 };
1046 }
1047
1048 fn destroySurface(ptr: *anyopaque, handle: backend.SurfaceHandle) backend.BackendError!void {
1049 const state: *State = @ptrCast(@alignCast(ptr));
1050 const surface = try state.getSurface(handle);
1051 if (surface.handle.generation != handle.generation) return error.SurfaceFrameExpired;
1052 if (surface.acquired_frame != null) return error.SurfaceAlreadyAcquired;
1053 const entry = state.objects.fetchRemove(handle.id) orelse return error.InvalidSurface;
1054 var object = entry.value;
1055 deinitObject(state, &object);
1056 }
1057
1058 fn destroyTexture(ptr: *anyopaque, handle: backend.TextureHandle) backend.BackendError!void {
1059 const state: *State = @ptrCast(@alignCast(ptr));
1060 if (state.getAnyImage(handle)) |image| {
1061 if (image.target != null) return error.InvalidTexture;
1062 const entry = state.objects.fetchRemove(handle.id).?;
1063 var object = entry.value;
1064 deinitObject(state, &object);
1065 return;
1066 } else |_| {}
1067 _ = try state.getTexture(handle);
1068 var it = state.objects.iterator();
1069 while (it.next()) |entry| {
1070 switch (entry.value_ptr.*) {
1071 .surface => |*surface| {
1072 if (surface.acquired_texture == handle.id) return error.SurfaceAlreadyAcquired;
1073 },
1074 else => {},
1075 }
1076 }
1077 const entry = state.objects.fetchRemove(handle.id) orelse return error.InvalidTexture;
1078 var object = entry.value;
1079 deinitObject(state, &object);
1080 }
1081
1082 fn acquireSurfaceFrame(
1083 ptr: *anyopaque,
1084 request: backend.SurfaceFrameAcquireRequest,
1085 ) backend.BackendError!backend.SurfaceFrame {
1086 const state: *State = @ptrCast(@alignCast(ptr));
1087 try state.objects.ensureUnusedCapacity(state.allocator, 2);
1088 const surface = try state.getSurface(request.surface);
1089 if (surface.handle.generation != request.surface.generation) return error.SurfaceFrameExpired;
1090 if (surface.acquired_frame != null) return error.SurfaceAlreadyAcquired;
1091
1092 const texture_id = state.next_id;
1093 if (texture_id == std.math.maxInt(BackendObjectId)) return error.OutOfMemory;
1094 const frame_id = texture_id + 1;
1095 if (frame_id == std.math.maxInt(BackendObjectId)) return error.OutOfMemory;
1096 state.next_id = frame_id + 1;
1097
1098 const image = surface.surface.acquire() catch |err| return mapSurfaceRuntimeError(err);
1099
1100 const texture = backend.TextureHandle{
1101 .id = texture_id,
1102 .backend = .vulkan,
1103 .extent = .{
1104 .width = image.extent.width,
1105 .height = image.extent.height,
1106 .depth = 1,
1107 },
1108 .format = request.surface.format,
1109 .usage = textureUsageFromImageUsage(image.usage),
1110 .sample_count = 1,
1111 .ownership = .acquired_surface,
1112 };
1113 const frame_surface = surface.handle;
1114 const view = backend.TextureView{
1115 .texture = texture,
1116 .format = texture.format,
1117 };
1118 const frame = backend.SurfaceFrame{
1119 .id = frame_id,
1120 .backend = .vulkan,
1121 .surface = frame_surface,
1122 .texture = texture,
1123 .view = view,
1124 .index = image.image_index,
1125 .generation = image.generation,
1126 .token = image.image,
1127 };
1128
1129 surface.acquired_frame = frame_id;
1130 surface.acquired_texture = texture_id;
1131 state.objects.putAssumeCapacityNoClobber(texture_id, .{ .texture = .{
1132 .handle = texture,
1133 .image = image,
1134 } });
1135 state.objects.putAssumeCapacityNoClobber(frame_id, .{ .surface_frame = .{
1136 .surface_id = request.surface.id,
1137 .texture_id = texture_id,
1138 .image_index = image.image_index,
1139 .generation = image.generation,
1140 } });
1141 return frame;
1142 }
1143
1144 fn presentSurfaceFrame(ptr: *anyopaque, request: backend.PresentRequest) backend.BackendError!void {
1145 const state: *State = @ptrCast(@alignCast(ptr));
1146 if (request.signal_event != null) return error.UnsupportedOperation;
1147 const surface = try state.getSurface(request.surface);
1148 const frame = try state.getFrame(request.frame);
1149 if (surface.handle.generation != request.surface.generation) return error.SurfaceFrameExpired;
1150 if (frame.generation != request.frame.generation) return error.SurfaceFrameExpired;
1151 if (frame.presented) return error.SurfaceFrameExpired;
1152 if (frame.surface_id != request.surface.id) return error.InvalidSurfaceFrame;
1153 if (frame.texture_id != request.frame.texture.id) return error.InvalidSurfaceFrame;
1154 if (surface.acquired_frame == null or surface.acquired_frame.? != request.frame.id) return error.InvalidSurfaceFrame;
1155
1156 for (request.wait_events) |event_handle| {
1157 const event = try state.getEvent(event_handle);
1158 if (!event.recorded) return error.InvalidEvent;
1159 event.synchronize() catch |err| return mapRuntimeError(err);
1160 }
1161
1162 surface.surface.present(frame.image_index, frame.generation) catch |err| return mapSurfaceRuntimeError(err);
1163 frame.presented = true;
1164 surface.acquired_frame = null;
1165 surface.acquired_texture = null;
1166 }
1167
1168 fn writeSurfaceFrame(ptr: *anyopaque, request: backend.SurfaceFrameWriteRequest) backend.BackendError!void {
1169 const state: *State = @ptrCast(@alignCast(ptr));
1170 const surface = try state.getSurface(request.surface);
1171 const frame = try state.getFrame(request.frame);
1172 if (surface.handle.generation != request.surface.generation) return error.SurfaceFrameExpired;
1173 if (frame.generation != request.frame.generation) return error.SurfaceFrameExpired;
1174 if (frame.presented or frame.written) return error.SurfaceFrameExpired;
1175 if (frame.surface_id != request.surface.id) return error.InvalidSurfaceFrame;
1176 if (frame.texture_id != request.frame.texture.id) return error.InvalidSurfaceFrame;
1177 if (surface.acquired_frame == null or surface.acquired_frame.? != request.frame.id) return error.InvalidSurfaceFrame;
1178 const texture = try state.getTexture(request.frame.texture);
1179
1180 if (request.wait_events.len > runtime_mod.max_surface_wait_events) return error.LaunchArgumentMismatch;
1181 var wait_events: [runtime_mod.max_surface_wait_events]*runtime_mod.Event = undefined;
1182 for (request.wait_events, 0..) |event_handle, index| {
1183 const event = try state.getEvent(event_handle);
1184 if (!event.recorded) return error.InvalidEvent;
1185 wait_events[index] = event;
1186 }
1187 const signal_event = if (request.signal_event) |event_handle| try state.getEvent(event_handle) else null;
1188
1189 const ops = state.allocator.alloc(runtime_mod.SurfaceWriteOp, request.operations.len) catch return error.OutOfMemory;
1190 defer state.allocator.free(ops);
1191 for (request.operations, 0..) |op, index| {
1192 ops[index] = switch (op) {
1193 .clear => |color| .{ .clear = .{ .r = color.r, .g = color.g, .b = color.b, .a = color.a } },
1194 .copy_buffer => |buffer_handle| .{ .copy_buffer = try state.getBuffer(buffer_handle) },
1195 };
1196 }
1197 surface.surface.write(texture.image, ops, wait_events[0..request.wait_events.len], signal_event) catch |err| return mapSurfaceRuntimeError(err);
1198 frame.written = true;
1199 }
1200
1201 fn createStream(ptr: *anyopaque, _: backend.StreamAllocation) backend.BackendError!backend.StreamHandle {
1202 const state: *State = @ptrCast(@alignCast(ptr));
1203 const rt = state.runtime.ptr() orelse return error.RuntimeUnavailable;
1204
1205 const stream = rt.createStream() catch |err| return mapRuntimeError(err);
1206 errdefer rt.destroyStream(stream);
1207
1208 const id = try state.putObject(.{ .stream = stream });
1209 return .{
1210 .id = id,
1211 .backend = .vulkan,
1212 };
1213 }
1214
1215 fn createEvent(ptr: *anyopaque, _: backend.EventAllocation) backend.BackendError!backend.EventHandle {
1216 const state: *State = @ptrCast(@alignCast(ptr));
1217 const rt = state.runtime.ptr() orelse return error.RuntimeUnavailable;
1218
1219 const event = rt.createEvent() catch |err| return mapRuntimeError(err);
1220 errdefer rt.destroyEvent(event);
1221
1222 const id = try state.putObject(.{ .event = event });
1223 return .{
1224 .id = id,
1225 .backend = .vulkan,
1226 };
1227 }
1228
1229 fn writeBuffer(ptr: *anyopaque, request: backend.BufferWriteRequest) backend.BackendError!void {
1230 const state: *State = @ptrCast(@alignCast(ptr));
1231 const buffer = try state.getBuffer(request.handle);
1232 if (request.bytes.len > buffer.size) return error.InvalidBuffer;
1233 buffer.copyFromHost(request.bytes) catch |err| return mapAllocationError(err);
1234 }
1235
1236 fn readBuffer(ptr: *anyopaque, request: backend.BufferReadRequest) backend.BackendError!void {
1237 const state: *State = @ptrCast(@alignCast(ptr));
1238 const buffer = try state.getBuffer(request.handle);
1239 if (request.bytes.len < buffer.size) return error.ReadBufferDestinationTooSmall;
1240 buffer.copyToHost(request.bytes) catch |err| return mapAllocationError(err);
1241 }
1242
1243 fn launch(ptr: *anyopaque, request: backend.LaunchRequest) backend.BackendError!void {
1244 const state: *State = @ptrCast(@alignCast(ptr));
1245 if (request.geometry.dynamic_shared_memory_bytes != 0) return error.UnsupportedOperation;
1246 if (request.artifact.backend != .vulkan or request.artifact.format != .vulkan_spirv) {
1247 return error.CapabilityMismatch;
1248 }
1249 const rt = state.runtime.ptr() orelse return error.RuntimeUnavailable;
1250 const loaded_handle = request.loaded_artifact orelse return error.InvalidArtifact;
1251 const loaded = try state.getLoaded(loaded_handle);
1252 if (request.buffers.len != @as(usize, @intCast(loaded.buffer_argument_count))) {
1253 return error.LaunchArgumentMismatch;
1254 }
1255 if (request.scalar_arguments.len != @as(usize, @intCast(loaded.scalar_argument_count))) {
1256 return error.LaunchArgumentMismatch;
1257 }
1258 if (request.buffers.len > runtime_mod.max_buffers_per_set) return error.LaunchArgumentMismatch;
1259
1260 var push_storage: [choir_abi.max_push_constant_bytes]u8 = undefined;
1261 const push_size = try loaded.push_constants.pack(request.scalar_arguments, &push_storage);
1262
1263 var args: [runtime_mod.max_buffers_per_set]*runtime_mod.DeviceBuffer = undefined;
1264 for (request.buffers, 0..) |binding, i| {
1265 if (binding.handle.backend != .vulkan or binding.ownership != .backend) return error.InvalidBuffer;
1266 const buffer = try state.getBuffer(binding.handle);
1267 if (binding.byte_size > buffer.size or binding.handle.byte_size != buffer.size) {
1268 return error.InvalidBuffer;
1269 }
1270 args[i] = buffer;
1271 }
1272
1273 const stream = if (request.stream) |stream_handle|
1274 try state.getStream(stream_handle)
1275 else
1276 rt.defaultStream() catch |err| return mapRuntimeError(err);
1277
1278 if (request.wait_events.len > launch_mod.max_wait_events) return error.LaunchArgumentMismatch;
1279 var wait_events: [launch_mod.max_wait_events]*runtime_mod.Event = undefined;
1280 for (request.wait_events, 0..) |event_handle, i| {
1281 const event = try state.getEvent(event_handle);
1282 if (!event.recorded) return error.InvalidEvent;
1283 wait_events[i] = event;
1284 }
1285
1286 const signal_event = if (request.signal_event) |event_handle|
1287 try state.getEvent(event_handle)
1288 else
1289 null;
1290
1291 loaded.kernel.launch(
1292 stream,
1293 args[0..request.buffers.len],
1294 push_storage[0..push_size],
1295 request.geometry.grid,
1296 request.geometry.threadgroup,
1297 wait_events[0..request.wait_events.len],
1298 signal_event,
1299 ) catch |err| return mapLaunchError(err);
1300 }
1301
1302 fn synchronize(ptr: *anyopaque, request: backend.SyncRequest) backend.BackendError!void {
1303 const state: *State = @ptrCast(@alignCast(ptr));
1304 switch (request.scope) {
1305 .default_stream => {
1306 const rt = state.runtime.ptr() orelse return error.RuntimeUnavailable;
1307 const stream = rt.defaultStream() catch |err| return mapRuntimeError(err);
1308 stream.synchronize() catch |err| return mapRuntimeError(err);
1309 },
1310 .device => {
1311 const rt = state.runtime.ptr() orelse return error.RuntimeUnavailable;
1312 rt.synchronize() catch |err| return mapRuntimeError(err);
1313 },
1314 .stream => {
1315 const stream = try state.getStream(request.stream.?);
1316 stream.synchronize() catch |err| return mapRuntimeError(err);
1317 },
1318 .event => {
1319 const event = try state.getEvent(request.event.?);
1320 if (!event.recorded) return error.InvalidEvent;
1321 event.synchronize() catch |err| return mapRuntimeError(err);
1322 },
1323 }
1324 }
1325
1326 fn queryEvent(ptr: *anyopaque, request: backend.EventQueryRequest) backend.BackendError!bool {
1327 const state: *State = @ptrCast(@alignCast(ptr));
1328 const event = try state.getEvent(request.event);
1329 if (!event.recorded) return false;
1330 return event.query() catch |err| return mapRuntimeError(err);
1331 }
1332
1333 fn recordEvent(ptr: *anyopaque, request: backend.EventRecordRequest) backend.BackendError!void {
1334 const state: *State = @ptrCast(@alignCast(ptr));
1335 const stream = try state.getStream(request.stream);
1336 const event = try state.getEvent(request.event);
1337 event.record(stream) catch |err| return mapRuntimeError(err);
1338 }
1339
1340 fn elapsedEventNs(ptr: *anyopaque, request: backend.EventElapsedRequest) backend.BackendError!u64 {
1341 const state: *State = @ptrCast(@alignCast(ptr));
1342 const start = try state.getEvent(request.start);
1343 const end = try state.getEvent(request.end);
1344 return end.elapsedNs(start) catch |err| return mapElapsedEventError(err);
1345 }
1346
1347 fn destroyObject(ptr: *anyopaque, id: BackendObjectId) void {
1348 const state: *State = @ptrCast(@alignCast(ptr));
1349 if (state.objects.fetchRemove(id)) |entry| {
1350 var object = entry.value;
1351 deinitObject(state, &object);
1352 }
1353 }
1354
1355 fn deinitHandle(ptr: *anyopaque, allocator: Allocator) void {
1356 _ = allocator;
1357 const state: *State = @ptrCast(@alignCast(ptr));
1358 state.deinit();
1359 }
1360
1361 fn deinitObject(state: *State, object: *Object) void {
1362 switch (object.*) {
1363 .loaded_artifact => |*loaded| {
1364 loaded.kernel.deinit();
1365 state.allocator.free(loaded.entry_name);
1366 },
1367 .image => |*texture| texture.image.destroy(state.liveRaster()),
1368 .render_pipeline => |*pipeline| pipeline.pipeline.destroy(state.liveRaster()),
1369 .render_bindings => |*bindings| bindings.set.destroy(state.liveRaster()),
1370 .render_bundle => |bundle| state.liveRaster().freeBundle(bundle.command_buffer),
1371 .dmabuf_target => |shared| {
1372 shared.target.destroy(state.liveRaster());
1373 state.allocator.destroy(shared);
1374 },
1375 .buffer => |*buffer| buffer.deinit(),
1376 .stream => |stream| stream._runtime.destroyStream(stream),
1377 .event => |event| event._runtime.destroyEvent(event),
1378 .surface => |*surface| surface.surface.deinit(),
1379 .texture, .surface_frame => {},
1380 }
1381 object.* = undefined;
1382 }
1383
1384 fn allocateTexture(ptr: *anyopaque, request: backend.TextureAllocation) backend.BackendError!backend.TextureHandle {
1385 const state: *State = @ptrCast(@alignCast(ptr));
1386 const raster = try state.rasterState();
1387 if (request.sample_count != 1 or request.extent.depth != 1) return error.CapabilityMismatch;
1388 if (request.usage.present or request.usage.storage) return error.CapabilityMismatch;
1389 const depth = request.format.isDepth();
1390 if (depth and request.usage.color_attachment) return error.CapabilityMismatch;
1391 if (!depth and request.usage.depth_attachment) return error.CapabilityMismatch;
1392 try state.objects.ensureUnusedCapacity(state.allocator, 1);
1393
1394 var image = raster_mod.Image.create(raster, .{
1395 .width = request.extent.width,
1396 .height = request.extent.height,
1397 .format = try vulkanFormat(request.format),
1398 .usage = imageUsageFromTextureUsage(request.usage),
1399 .aspect = if (depth) driver_mod.VK_IMAGE_ASPECT_DEPTH_BIT else driver_mod.VK_IMAGE_ASPECT_COLOR_BIT,
1400 .texel_bytes = request.format.texelBytes(),
1401 }) catch |err| return mapRasterError(err, error.InvalidTexture);
1402 errdefer image.destroy(raster);
1403
1404 var texture = backend.TextureHandle{
1405 .id = 0,
1406 .backend = .vulkan,
1407 .extent = request.extent,
1408 .format = request.format,
1409 .usage = request.usage,
1410 .sample_count = 1,
1411 .ownership = .backend,
1412 };
1413 texture.id = try state.putObject(.{ .image = .{ .handle = texture, .image = image } });
1414 state.objects.getPtr(texture.id).?.image.handle.id = texture.id;
1415 return texture;
1416 }
1417
1418 fn writeTexture(ptr: *anyopaque, request: backend.TextureWriteRequest) backend.BackendError!void {
1419 const state: *State = @ptrCast(@alignCast(ptr));
1420 const texture = try state.getImage(request.texture);
1421 texture.image.write(state.liveRaster(), request.bytes) catch |err| return mapRasterError(err, error.InvalidTexture);
1422 }
1423
1424 fn readTexture(ptr: *anyopaque, request: backend.TextureReadRequest) backend.BackendError!void {
1425 const state: *State = @ptrCast(@alignCast(ptr));
1426 const texture = try state.getImage(request.texture);
1427 texture.image.read(state.liveRaster(), request.bytes) catch |err| return mapRasterError(err, error.InvalidTexture);
1428 }
1429
1430 fn createRenderArtifact(ptr: *anyopaque, desc: backend.RenderPipelineDesc) backend.BackendError!backend.RenderArtifact {
1431 const state: *State = @ptrCast(@alignCast(ptr));
1432 if (desc.format != .vulkan_spirv) return error.UnsupportedArtifactFormat;
1433 const words = switch (desc.payload) {
1434 .words_u32 => |words| words,
1435 else => return error.InvalidRenderArtifact,
1436 };
1437 if (words.len == 0) return error.InvalidRenderArtifact;
1438 var artifact = backend.RenderArtifact.init(state.allocator, .{
1439 .backend = .vulkan,
1440 .pipeline = desc,
1441 }) catch return error.OutOfMemory;
1442 errdefer artifact.deinit();
1443 try artifact.setOwnedWords(words);
1444 return artifact;
1445 }
1446
1447 /// Builds the pipeline an artifact describes. Every resource binding must sit in group 0, and each
1448 /// vertex layout's binding must be below the vertex buffer limit and used once.
1449 fn loadRenderArtifact(ptr: *anyopaque, artifact: *const backend.RenderArtifact) backend.BackendError!backend.LoadedRenderArtifact {
1450 const state: *State = @ptrCast(@alignCast(ptr));
1451 if (artifact.backend != .vulkan) return error.CapabilityMismatch;
1452 if (artifact.format != .vulkan_spirv) return error.UnsupportedArtifactFormat;
1453 const words = switch (artifact.payload) {
1454 .words_u32 => |words| words,
1455 else => return error.InvalidRenderArtifact,
1456 };
1457 if (words.len == 0) return error.InvalidRenderArtifact;
1458 const raster = try state.rasterState();
1459 if (artifact.vertex_layouts.len > raster.max_vertex_buffers) return error.CapabilityMismatch;
1460 if (artifact.bindings.len > raster_mod.max_descriptors) return error.CapabilityMismatch;
1461 try state.objects.ensureUnusedCapacity(state.allocator, 1);
1462
1463 var object: VulkanPipeline = .{
1464 .pipeline = undefined,
1465 .loaded = undefined,
1466 .layouts = undefined,
1467 .bindings = undefined,
1468 };
1469 var vertex_bindings: [raster_mod.max_vertex_buffers]driver_mod.VkVertexInputBindingDescription = undefined;
1470 var vertex_attributes: [raster_mod.max_vertex_attributes]driver_mod.VkVertexInputAttributeDescription = undefined;
1471 var attribute_count: usize = 0;
1472 var used_vertex_bindings: u32 = 0;
1473 for (artifact.vertex_layouts, 0..) |layout, index| {
1474 if (layout.binding >= raster_mod.max_vertex_buffers) return error.CapabilityMismatch;
1475 const bit = @as(u32, 1) << @intCast(layout.binding);
1476 if (used_vertex_bindings & bit != 0) return error.InvalidRenderArtifact;
1477 used_vertex_bindings |= bit;
1478 const per_instance = layout.step_mode == .instance;
1479 vertex_bindings[index] = .{
1480 .binding = layout.binding,
1481 .stride = layout.stride,
1482 .inputRate = if (per_instance) driver_mod.VK_VERTEX_INPUT_RATE_INSTANCE else driver_mod.VK_VERTEX_INPUT_RATE_VERTEX,
1483 };
1484 object.layouts[index] = .{ .binding = layout.binding, .stride = layout.stride, .per_instance = per_instance };
1485 const start: usize = layout.attribute_start;
1486 const count: usize = layout.attribute_count;
1487 if (start > artifact.vertex_attributes.len or count > artifact.vertex_attributes.len - start) return error.InvalidRenderArtifact;
1488 for (artifact.vertex_attributes[start..][0..count]) |attribute| {
1489 if (attribute_count == raster.max_vertex_attributes) return error.CapabilityMismatch;
1490 vertex_attributes[attribute_count] = .{
1491 .location = attribute.location,
1492 .binding = layout.binding,
1493 .format = vertexFormat(attribute.format),
1494 .offset = attribute.offset,
1495 };
1496 attribute_count += 1;
1497 }
1498 }
1499 var descriptors: [raster_mod.max_descriptors]driver_mod.VkDescriptorSetLayoutBinding = undefined;
1500 var used_descriptor_bindings: u64 = 0;
1501 for (artifact.bindings, 0..) |binding, index| {
1502 if (binding.group != 0 or binding.binding >= 64) return error.CapabilityMismatch;
1503 const bit = @as(u64, 1) << @intCast(binding.binding);
1504 if (used_descriptor_bindings & bit != 0) return error.InvalidRenderArtifact;
1505 used_descriptor_bindings |= bit;
1506 descriptors[index] = .{
1507 .binding = binding.binding,
1508 .descriptorType = switch (binding.kind) {
1509 .uniform_buffer => driver_mod.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
1510 .sampled_texture => driver_mod.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
1511 .storage_buffer, .storage_texture => return error.CapabilityMismatch,
1512 },
1513 .descriptorCount = 1,
1514 .stageFlags = driver_mod.VK_SHADER_STAGE_VERTEX_BIT | driver_mod.VK_SHADER_STAGE_FRAGMENT_BIT,
1515 .pImmutableSamplers = null,
1516 };
1517 object.bindings[index] = binding;
1518 }
1519
1520 const vertex_entry = state.allocator.dupeSentinel(u8, artifact.vertex_entry_name, 0) catch return error.OutOfMemory;
1521 defer state.allocator.free(vertex_entry);
1522 const fragment_entry = state.allocator.dupeSentinel(u8, artifact.fragment_entry_name, 0) catch return error.OutOfMemory;
1523 defer state.allocator.free(fragment_entry);
1524 const depth: ?raster_mod.DepthTest = if (artifact.depth) |depth_state| .{
1525 .format = try vulkanFormat(depth_state.format),
1526 .compare = vulkanCompare(depth_state.compare),
1527 .write = depth_state.write,
1528 .bias_constant = depth_state.bias.constant,
1529 .bias_slope = depth_state.bias.slope,
1530 .bias_clamp = depth_state.bias.clamp,
1531 } else null;
1532 if (artifact.push_constant_bytes > raster.max_push_constant_bytes) return error.CapabilityMismatch;
1533 if (depth) |test_state| {
1534 if (test_state.bias_clamp != 0 and !raster.runtime._depth_bias_clamp) return error.CapabilityMismatch;
1535 }
1536 object.pipeline = raster_mod.Pipeline.create(raster, .{
1537 .words = words,
1538 .vertex_entry = vertex_entry.ptr,
1539 .fragment_entry = fragment_entry.ptr,
1540 .color_format = try vulkanFormat(artifact.target_format),
1541 .depth = depth,
1542 .blend = switch (artifact.blend_mode) {
1543 .replace => .replace,
1544 .alpha_premultiplied => .alpha_premultiplied,
1545 .alpha_straight => .alpha_straight,
1546 .additive => .additive,
1547 },
1548 .topology = vulkanTopology(artifact.topology),
1549 .vertex_bindings = vertex_bindings[0..artifact.vertex_layouts.len],
1550 .vertex_attributes = vertex_attributes[0..attribute_count],
1551 .descriptors = descriptors[0..artifact.bindings.len],
1552 .push_constant_bytes = artifact.push_constant_bytes,
1553 }) catch |err| return mapRasterError(err, error.InvalidRenderArtifact);
1554 errdefer object.pipeline.destroy(raster);
1555
1556 const id = try state.putObject(.{ .render_pipeline = object });
1557 const loaded = backend.LoadedRenderArtifact.describing(artifact, id);
1558 state.objects.getPtr(id).?.render_pipeline.loaded = loaded;
1559 return loaded;
1560 }
1561
1562 fn createRenderBindings(ptr: *anyopaque, request: backend.RenderBindingsRequest) backend.BackendError!backend.RenderBindings {
1563 const state: *State = @ptrCast(@alignCast(ptr));
1564 const raster = try state.rasterState();
1565 try state.objects.ensureUnusedCapacity(state.allocator, 1);
1566 const pipeline = try state.getRenderPipeline(request.pipeline);
1567 const count: usize = pipeline.loaded.binding_count;
1568 if (request.resources.len != count) return error.RenderArgumentMismatch;
1569 var descriptors: [raster_mod.max_descriptors]raster_mod.Descriptor = undefined;
1570 for (request.resources, pipeline.bindings[0..count], 0..) |resource, binding, index| {
1571 if (std.meta.activeTag(resource) != binding.kind) return error.RenderArgumentMismatch;
1572 descriptors[index] = .{ .binding = binding.binding, .resource = switch (resource) {
1573 .uniform_buffer => |buffer_handle| .{ .buffer = .{
1574 .buffer = (try state.getBuffer(buffer_handle))._buffer,
1575 .kind = driver_mod.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
1576 } },
1577 .sampled_texture => |sampled| blk: {
1578 const texture = try state.getImage(sampled.texture);
1579 if (!texture.handle.usage.sampled) return error.InvalidTexture;
1580 const sampler = raster.sampler(.{
1581 .filter = switch (sampled.sampler.filter) {
1582 .nearest => driver_mod.VK_FILTER_NEAREST,
1583 .linear => driver_mod.VK_FILTER_LINEAR,
1584 },
1585 .address = switch (sampled.sampler.address) {
1586 .clamp_to_edge => driver_mod.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
1587 .repeat => driver_mod.VK_SAMPLER_ADDRESS_MODE_REPEAT,
1588 },
1589 }) catch |err| return mapRasterError(err, error.RenderFailed);
1590 break :blk .{ .image = .{ .view = texture.image.view, .layout = texture.image.home, .sampler = sampler } };
1591 },
1592 .storage_buffer, .storage_texture => return error.CapabilityMismatch,
1593 } };
1594 }
1595 var set = raster_mod.BindingSet.create(raster, &pipeline.pipeline, descriptors[0..count]) catch |err| {
1596 return mapRasterError(err, error.RenderFailed);
1597 };
1598 errdefer set.destroy(raster);
1599 const id = try state.putObject(.{ .render_bindings = .{ .set = set, .pipeline_id = request.pipeline.id } });
1600 return .{ .id = id, .backend = .vulkan, .pipeline = request.pipeline.id };
1601 }
1602
1603 fn render(ptr: *anyopaque, request: backend.RenderRequest) backend.BackendError!void {
1604 const state: *State = @ptrCast(@alignCast(ptr));
1605 const raster = try state.rasterState();
1606 const stream = try state.submitStream(request.stream);
1607 var wait_storage: [raster_mod.max_wait_events]*runtime_mod.Event = undefined;
1608 const waits = try state.submitWaits(request.wait_events, &wait_storage);
1609 const signal = if (request.signal_event) |event_handle| try state.getEvent(event_handle) else null;
1610
1611 const cb = raster_mod.beginStreamPass(stream) catch |err| return mapRasterError(err, error.RenderFailed);
1612 recordPassAndEnd(state, raster, cb, request.pass) catch |err| {
1613 runtime_mod.retireStreamCommandBuffer(stream, cb, stream.counter);
1614 return err;
1615 };
1616 raster_mod.submit(raster, .{
1617 .stream = stream,
1618 .cb = cb,
1619 .wait_events = waits,
1620 .signal_event = signal,
1621 .retire = true,
1622 }) catch |err| return mapRasterError(err, error.RenderFailed);
1623 }
1624
1625 fn recordRenderBundle(ptr: *anyopaque, pass: backend.RenderPass) backend.BackendError!backend.RenderBundle {
1626 const state: *State = @ptrCast(@alignCast(ptr));
1627 const raster = try state.rasterState();
1628 try state.objects.ensureUnusedCapacity(state.allocator, 1);
1629 const cb = raster_mod.beginBundle(raster) catch |err| return mapRasterError(err, error.RenderFailed);
1630 errdefer raster.discardBundle(cb);
1631 try recordPassAndEnd(state, raster, cb, pass);
1632 const draw_count: u32 = @intCast(pass.draws.len);
1633 const id = try state.putObject(.{ .render_bundle = .{ .command_buffer = cb, .draw_count = draw_count } });
1634 return .{ .id = id, .backend = .vulkan, .draw_count = draw_count };
1635 }
1636
1637 fn submitRenderBundle(ptr: *anyopaque, request: backend.RenderBundleSubmit) backend.BackendError!void {
1638 const state: *State = @ptrCast(@alignCast(ptr));
1639 const raster = try state.rasterState();
1640 const bundle = try state.getRenderBundle(request.bundle);
1641 const stream = try state.submitStream(request.stream);
1642 var wait_storage: [raster_mod.max_wait_events]*runtime_mod.Event = undefined;
1643 const waits = try state.submitWaits(request.wait_events, &wait_storage);
1644 const signal = if (request.signal_event) |event_handle| try state.getEvent(event_handle) else null;
1645 raster_mod.submit(raster, .{
1646 .stream = stream,
1647 .cb = bundle.command_buffer,
1648 .wait_events = waits,
1649 .signal_event = signal,
1650 .retire = false,
1651 }) catch |err| return mapRasterError(err, error.RenderFailed);
1652 }
1653
1654 fn recordPassAndEnd(
1655 state: *State,
1656 raster: *raster_mod.Raster,
1657 cb: driver_mod.VkCommandBuffer,
1658 pass: backend.RenderPass,
1659 ) backend.BackendError!void {
1660 try recordPass(state, raster, cb, pass);
1661 const drv = &raster.runtime._driver;
1662 drv.fromResult(drv.vkEndCommandBuffer(cb)) catch |err| return mapRasterError(err, error.RenderFailed);
1663 }
1664
1665 const VertexSlot = struct {
1666 id: BackendObjectId,
1667 offset: u64,
1668 size: u64,
1669 };
1670
1671 const IndexSlot = struct {
1672 id: BackendObjectId,
1673 offset: u64,
1674 size: u64,
1675 format: backend.RenderIndexFormat,
1676 };
1677
1678 /// Records one pass. A draw binds only what differs from the draw before it, so a run of draws
1679 /// that share a pipeline, bindings and buffers costs one draw command each. Every range a draw
1680 /// reads is checked against its buffer, except the vertices an indexed draw reaches through its
1681 /// indices, which only the indices name.
1682 fn recordPass(
1683 state: *State,
1684 raster: *raster_mod.Raster,
1685 cb: driver_mod.VkCommandBuffer,
1686 pass: backend.RenderPass,
1687 ) backend.BackendError!void {
1688 const drv = &raster.runtime._driver;
1689 const targets = try passTargets(state, pass);
1690 raster_mod.beginPass(drv, cb, targets);
1691 var pipeline: ?*VulkanPipeline = null;
1692 var bindings_id: ?BackendObjectId = null;
1693 var vertex_slots: [raster_mod.max_vertex_buffers]?VertexSlot = @splat(null);
1694 var index_slot: ?IndexSlot = null;
1695 for (pass.draws) |draw| {
1696 const current = if (pipeline != null and pipeline.?.loaded.id == draw.pipeline.id) pipeline.? else blk: {
1697 const next = try state.getRenderPipeline(draw.pipeline);
1698 if (next.loaded.target_format != pass.color.view.format) return error.RenderArgumentMismatch;
1699 if ((next.loaded.depth != null) != (pass.depth != null)) return error.RenderArgumentMismatch;
1700 drv.vkCmdBindPipeline(cb, driver_mod.VK_PIPELINE_BIND_POINT_GRAPHICS, next.pipeline.pipeline);
1701 bindings_id = null;
1702 pipeline = next;
1703 break :blk next;
1704 };
1705 if (draw.vertex_buffers.len != current.loaded.vertex_buffer_count) return error.RenderArgumentMismatch;
1706 if ((draw.bindings != null) != (current.loaded.binding_count != 0)) return error.RenderArgumentMismatch;
1707 if (draw.bindings) |bindings| if (bindings_id != bindings.id) {
1708 const set = try state.getRenderBindings(bindings);
1709 if (set.pipeline_id != current.loaded.id) return error.RenderArgumentMismatch;
1710 drv.vkCmdBindDescriptorSets(
1711 cb,
1712 driver_mod.VK_PIPELINE_BIND_POINT_GRAPHICS,
1713 current.pipeline.layout,
1714 0,
1715 1,
1716 @ptrCast(&set.set.set),
1717 0,
1718 null,
1719 );
1720 bindings_id = bindings.id;
1721 };
1722 if (draw.push_constants.len != current.loaded.push_constant_bytes) return error.RenderArgumentMismatch;
1723 if (draw.push_constants.len != 0) {
1724 const size: u32 = @intCast(draw.push_constants.len);
1725 drv.vkCmdPushConstants(cb, current.pipeline.layout, raster_mod.push_constant_stages, 0, size, draw.push_constants.ptr);
1726 }
1727 const indexed = draw.range.index_format != .none;
1728 for (draw.vertex_buffers, current.layouts[0..draw.vertex_buffers.len]) |range, layout| {
1729 const slot = &vertex_slots[layout.binding];
1730 const bound = if (slot.*) |existing| existing.id == range.buffer.id and existing.offset == range.offset else false;
1731 if (!bound) {
1732 const buffer = try state.getBuffer(range.buffer);
1733 if (range.offset >= buffer.size) return error.RenderArgumentMismatch;
1734 drv.vkCmdBindVertexBuffers(cb, layout.binding, 1, @ptrCast(&buffer._buffer), @ptrCast(&range.offset));
1735 slot.* = .{ .id = range.buffer.id, .offset = range.offset, .size = buffer.size };
1736 }
1737 const elements: u64 = if (layout.per_instance)
1738 @as(u64, draw.range.first_instance) + draw.range.instance_count
1739 else if (indexed)
1740 0
1741 else
1742 @as(u64, draw.range.first_vertex) + draw.range.vertex_count;
1743 if (elements * layout.stride > slot.*.?.size - slot.*.?.offset) return error.RenderArgumentMismatch;
1744 }
1745 if (!indexed) {
1746 if (draw.index_buffer != null) return error.RenderArgumentMismatch;
1747 drv.vkCmdDraw(cb, draw.range.vertex_count, draw.range.instance_count, draw.range.first_vertex, draw.range.first_instance);
1748 continue;
1749 }
1750 const range = draw.index_buffer orelse return error.RenderArgumentMismatch;
1751 const index_bytes: u64 = if (draw.range.index_format == .u16) 2 else 4;
1752 const bound = if (index_slot) |existing|
1753 existing.id == range.buffer.id and existing.offset == range.offset and existing.format == draw.range.index_format
1754 else
1755 false;
1756 if (!bound) {
1757 const buffer = try state.getBuffer(range.buffer);
1758 if (range.offset >= buffer.size or range.offset % index_bytes != 0) return error.RenderArgumentMismatch;
1759 const index_type = if (index_bytes == 2) driver_mod.VK_INDEX_TYPE_UINT16 else driver_mod.VK_INDEX_TYPE_UINT32;
1760 drv.vkCmdBindIndexBuffer(cb, buffer._buffer, range.offset, index_type);
1761 index_slot = .{ .id = range.buffer.id, .offset = range.offset, .size = buffer.size, .format = draw.range.index_format };
1762 }
1763 const indices = @as(u64, draw.range.first_index) + draw.range.index_count;
1764 if (indices * index_bytes > index_slot.?.size - index_slot.?.offset) return error.RenderArgumentMismatch;
1765 drv.vkCmdDrawIndexed(
1766 cb,
1767 draw.range.index_count,
1768 draw.range.instance_count,
1769 draw.range.first_index,
1770 draw.range.base_vertex,
1771 draw.range.first_instance,
1772 );
1773 }
1774 raster_mod.endPass(drv, cb, targets);
1775 }
1776
1777 fn passTargets(state: *State, pass: backend.RenderPass) backend.BackendError!raster_mod.Targets {
1778 const color = try state.getImage(pass.color.view.texture);
1779 const depth = if (pass.depth) |attachment| try state.getImage(attachment.view.texture) else null;
1780 return .{
1781 .color = .{
1782 .image = &color.image,
1783 .clear = switch (pass.color.load) {
1784 .load => null,
1785 .clear => |value| .{ .color = .{ .float32 = .{ value.r, value.g, value.b, value.a } } },
1786 },
1787 },
1788 .depth = if (depth) |target| .{
1789 .image = &target.image,
1790 .clear = switch (pass.depth.?.load) {
1791 .load => null,
1792 .clear => |value| .{ .depthStencil = .{ .depth = value, .stencil = 0 } },
1793 },
1794 } else null,
1795 .viewport = .{
1796 .x = pass.viewport.x,
1797 .y = pass.viewport.y,
1798 .width = pass.viewport.width,
1799 .height = pass.viewport.height,
1800 .minDepth = pass.viewport.min_depth,
1801 .maxDepth = pass.viewport.max_depth,
1802 },
1803 .scissor = .{
1804 .offset = .{ .x = @intCast(pass.scissor.x), .y = @intCast(pass.scissor.y) },
1805 .extent = .{ .width = pass.scissor.width, .height = pass.scissor.height },
1806 },
1807 };
1808 }
1809
1810 fn vulkanCompare(compare: backend.RenderCompare) driver_mod.VkCompareOp {
1811 return switch (compare) {
1812 .never => driver_mod.VK_COMPARE_OP_NEVER,
1813 .less => driver_mod.VK_COMPARE_OP_LESS,
1814 .equal => driver_mod.VK_COMPARE_OP_EQUAL,
1815 .less_equal => driver_mod.VK_COMPARE_OP_LESS_OR_EQUAL,
1816 .greater => driver_mod.VK_COMPARE_OP_GREATER,
1817 .not_equal => driver_mod.VK_COMPARE_OP_NOT_EQUAL,
1818 .greater_equal => driver_mod.VK_COMPARE_OP_GREATER_OR_EQUAL,
1819 .always => driver_mod.VK_COMPARE_OP_ALWAYS,
1820 };
1821 }
1822
1823 fn vulkanTopology(topology: backend.RenderPrimitiveTopology) driver_mod.VkPrimitiveTopology {
1824 return switch (topology) {
1825 .triangle_list => driver_mod.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
1826 .triangle_strip => driver_mod.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP,
1827 .line_list => driver_mod.VK_PRIMITIVE_TOPOLOGY_LINE_LIST,
1828 .line_strip => driver_mod.VK_PRIMITIVE_TOPOLOGY_LINE_STRIP,
1829 };
1830 }
1831
1832 fn vertexFormat(format: backend.RenderVertexFormat) driver_mod.VkFormat {
1833 return switch (format) {
1834 .float32 => driver_mod.VK_FORMAT_R32_SFLOAT,
1835 .float32x2 => driver_mod.VK_FORMAT_R32G32_SFLOAT,
1836 .float32x3 => driver_mod.VK_FORMAT_R32G32B32_SFLOAT,
1837 .float32x4 => driver_mod.VK_FORMAT_R32G32B32A32_SFLOAT,
1838 .uint32 => driver_mod.VK_FORMAT_R32_UINT,
1839 .uint32x2 => driver_mod.VK_FORMAT_R32G32_UINT,
1840 .uint32x4 => driver_mod.VK_FORMAT_R32G32B32A32_UINT,
1841 };
1842 }
1843
1844 /// Maps a raster runtime failure, with `fallback` for failures that belong to the request itself.
1845 fn mapRasterError(err: raster_mod.Error, fallback: backend.BackendError) backend.BackendError {
1846 return switch (err) {
1847 error.OutOfHostMemory, error.OutOfDeviceMemory, error.FragmentedPool, error.OutOfPoolMemory, error.TooManyObjects => error.OutOfMemory,
1848 error.DeviceLost => error.DeviceLost,
1849 error.StreamPoisoned => error.RenderFailed,
1850 error.FeatureNotPresent,
1851 error.FormatNotSupported,
1852 error.ExtensionNotPresent,
1853 error.LayerNotPresent,
1854 => error.CapabilityMismatch,
1855 error.DriverUnavailable,
1856 error.SymbolMissing,
1857 error.NoDevice,
1858 error.InvalidDevice,
1859 error.TimelineSemaphoreUnavailable,
1860 error.IncompatibleDriver,
1861 => error.RuntimeUnavailable,
1862 else => fallback,
1863 };
1864 }
1865
1866 fn surfaceDescFromRequest(request: backend.SurfaceCreationRequest) backend.BackendError!runtime_mod.SurfaceDesc {
1867 const x11 = switch (request.platform) {
1868 .x11 => |x11| x11,
1869 else => return error.CapabilityMismatch,
1870 };
1871 if (x11.display == 0 or x11.window == 0) return error.InvalidSurface;
1872 const usage = imageUsageFromTextureUsage(request.usage);
1873 if (usage == 0) return error.InvalidSurface;
1874 return .{
1875 .display = x11.display,
1876 .window = x11.window,
1877 .extent = .{ .width = request.extent.width, .height = request.extent.height },
1878 .format = try vulkanFormat(request.format),
1879 .color_space = try vulkanColorSpace(request.color_space),
1880 .present_mode = vulkanPresentMode(request.present_mode),
1881 .usage = usage,
1882 .min_image_count = request.max_frames_in_flight,
1883 .composite_alpha = vulkanCompositeAlpha(request.alpha_mode),
1884 };
1885 }
1886
1887 fn vulkanFormat(format: backend.TextureFormat) backend.BackendError!driver_mod.VkFormat {
1888 return switch (format) {
1889 .rgba8_unorm => driver_mod.VK_FORMAT_R8G8B8A8_UNORM,
1890 .bgra8_unorm => driver_mod.VK_FORMAT_B8G8R8A8_UNORM,
1891 .rgba8_srgb => driver_mod.VK_FORMAT_R8G8B8A8_SRGB,
1892 .bgra8_srgb => driver_mod.VK_FORMAT_B8G8R8A8_SRGB,
1893 .depth32_float => driver_mod.VK_FORMAT_D32_SFLOAT,
1894 };
1895 }
1896
1897 fn vulkanColorSpace(color_space: backend.ColorSpace) backend.BackendError!driver_mod.VkColorSpaceKHR {
1898 return switch (color_space) {
1899 .srgb => driver_mod.VK_COLOR_SPACE_SRGB_NONLINEAR_KHR,
1900 .linear => error.CapabilityMismatch,
1901 };
1902 }
1903
1904 fn vulkanPresentMode(present_mode: backend.PresentMode) driver_mod.VkPresentModeKHR {
1905 return switch (present_mode) {
1906 .fifo => driver_mod.VK_PRESENT_MODE_FIFO_KHR,
1907 .mailbox => driver_mod.VK_PRESENT_MODE_MAILBOX_KHR,
1908 .immediate => driver_mod.VK_PRESENT_MODE_IMMEDIATE_KHR,
1909 };
1910 }
1911
1912 fn vulkanCompositeAlpha(alpha_mode: backend.SurfaceAlphaMode) driver_mod.VkCompositeAlphaFlagBitsKHR {
1913 return switch (alpha_mode) {
1914 .solid => driver_mod.VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
1915 .premultiplied => driver_mod.VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR,
1916 .postmultiplied => driver_mod.VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR,
1917 .inherit => driver_mod.VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR,
1918 };
1919 }
1920
1921 fn imageUsageFromTextureUsage(usage: backend.TextureUsage) driver_mod.VkImageUsageFlags {
1922 var flags: driver_mod.VkImageUsageFlags = 0;
1923 if (usage.copy_src) flags |= driver_mod.VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
1924 if (usage.copy_dst) flags |= driver_mod.VK_IMAGE_USAGE_TRANSFER_DST_BIT;
1925 if (usage.sampled) flags |= driver_mod.VK_IMAGE_USAGE_SAMPLED_BIT;
1926 if (usage.storage) flags |= driver_mod.VK_IMAGE_USAGE_STORAGE_BIT;
1927 if (usage.color_attachment) flags |= driver_mod.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
1928 if (usage.depth_attachment) flags |= driver_mod.VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
1929 return flags;
1930 }
1931
1932 fn textureUsageFromImageUsage(usage: driver_mod.VkImageUsageFlags) backend.TextureUsage {
1933 return .{
1934 .copy_src = (usage & driver_mod.VK_IMAGE_USAGE_TRANSFER_SRC_BIT) != 0,
1935 .copy_dst = (usage & driver_mod.VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0,
1936 .sampled = (usage & driver_mod.VK_IMAGE_USAGE_SAMPLED_BIT) != 0,
1937 .storage = (usage & driver_mod.VK_IMAGE_USAGE_STORAGE_BIT) != 0,
1938 .color_attachment = (usage & driver_mod.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0,
1939 .present = true,
1940 };
1941 }
1942
1943 fn mapRuntimeError(err: runtime_mod.Error) backend.BackendError {
1944 return switch (err) {
1945 error.OutOfHostMemory, error.OutOfDeviceMemory, error.FragmentedPool, error.OutOfPoolMemory => error.OutOfMemory,
1946 error.DeviceLost => error.DeviceLost,
1947 error.DriverUnavailable,
1948 error.SymbolMissing,
1949 error.NoDevice,
1950 error.InvalidDevice,
1951 error.TimelineSemaphoreUnavailable,
1952 error.IncompatibleDriver,
1953 => error.RuntimeUnavailable,
1954 error.LayerNotPresent,
1955 error.ExtensionNotPresent,
1956 error.FeatureNotPresent,
1957 => error.CapabilityMismatch,
1958 error.StreamPoisoned => error.LaunchFailed,
1959 else => error.RuntimeUnavailable,
1960 };
1961 }
1962
1963 fn mapSurfaceRuntimeError(err: runtime_mod.Error) backend.BackendError {
1964 return switch (err) {
1965 error.OutOfHostMemory, error.OutOfDeviceMemory, error.FragmentedPool, error.OutOfPoolMemory => error.OutOfMemory,
1966 error.DeviceLost => error.DeviceLost,
1967 error.SurfaceOutOfDate, error.SurfaceLost => error.SurfaceFrameExpired,
1968 error.NativeWindowInUse, error.InitializationFailed => error.InvalidSurface,
1969 error.FormatNotSupported,
1970 error.LayerNotPresent,
1971 error.ExtensionNotPresent,
1972 error.FeatureNotPresent,
1973 => error.CapabilityMismatch,
1974 error.DriverUnavailable,
1975 error.SymbolMissing,
1976 error.NoDevice,
1977 error.InvalidDevice,
1978 error.TimelineSemaphoreUnavailable,
1979 error.IncompatibleDriver,
1980 => error.RuntimeUnavailable,
1981 else => error.RuntimeUnavailable,
1982 };
1983 }
1984
1985 fn mapArtifactError(err: launch_mod.Error) backend.BackendError {
1986 return switch (err) {
1987 error.OutOfHostMemory, error.OutOfDeviceMemory, error.FragmentedPool, error.OutOfPoolMemory => error.OutOfMemory,
1988 error.DeviceLost => error.DeviceLost,
1989 error.FormatNotSupported, error.InitializationFailed => error.InvalidArtifact,
1990 error.DriverUnavailable,
1991 error.SymbolMissing,
1992 error.NoDevice,
1993 error.InvalidDevice,
1994 error.TimelineSemaphoreUnavailable,
1995 error.IncompatibleDriver,
1996 => error.RuntimeUnavailable,
1997 error.LayerNotPresent,
1998 error.ExtensionNotPresent,
1999 error.FeatureNotPresent,
2000 => error.CapabilityMismatch,
2001 else => error.InvalidArtifact,
2002 };
2003 }
2004
2005 fn mapAllocationError(err: runtime_mod.Error) backend.BackendError {
2006 return switch (err) {
2007 error.OutOfHostMemory, error.OutOfDeviceMemory, error.FragmentedPool, error.OutOfPoolMemory => error.OutOfMemory,
2008 error.DeviceLost => error.DeviceLost,
2009 error.MemoryMapFailed, error.InitializationFailed => error.InvalidBuffer,
2010 error.DriverUnavailable,
2011 error.SymbolMissing,
2012 error.NoDevice,
2013 error.InvalidDevice,
2014 error.TimelineSemaphoreUnavailable,
2015 error.IncompatibleDriver,
2016 => error.RuntimeUnavailable,
2017 else => error.InvalidBuffer,
2018 };
2019 }
2020
2021 fn mapLaunchError(err: launch_mod.Error) backend.BackendError {
2022 return switch (err) {
2023 error.OutOfHostMemory, error.OutOfDeviceMemory, error.FragmentedPool, error.OutOfPoolMemory => error.OutOfMemory,
2024 error.DeviceLost => error.DeviceLost,
2025 error.StreamPoisoned => error.LaunchFailed,
2026 error.InitializationFailed => error.LaunchArgumentMismatch,
2027 error.DriverUnavailable,
2028 error.SymbolMissing,
2029 error.NoDevice,
2030 error.InvalidDevice,
2031 error.TimelineSemaphoreUnavailable,
2032 error.IncompatibleDriver,
2033 => error.RuntimeUnavailable,
2034 else => error.LaunchFailed,
2035 };
2036 }
2037
2038 fn mapElapsedEventError(err: runtime_mod.Error) backend.BackendError {
2039 return switch (err) {
2040 error.OutOfHostMemory, error.OutOfDeviceMemory, error.FragmentedPool, error.OutOfPoolMemory => error.OutOfMemory,
2041 error.DeviceLost => error.DeviceLost,
2042 error.FeatureNotPresent => error.UnsupportedOperation,
2043 error.InitializationFailed => error.InvalidEvent,
2044 error.StreamPoisoned => error.LaunchFailed,
2045 error.DriverUnavailable,
2046 error.SymbolMissing,
2047 error.NoDevice,
2048 error.InvalidDevice,
2049 error.TimelineSemaphoreUnavailable,
2050 error.IncompatibleDriver,
2051 => error.RuntimeUnavailable,
2052 error.LayerNotPresent,
2053 error.ExtensionNotPresent,
2054 => error.CapabilityMismatch,
2055 else => error.RuntimeUnavailable,
2056 };
2057 }
2058
2059 const vtable = backend.BackendVTable{
2060 .query_capabilities = queryCapabilities,
2061 .create_artifact = createArtifact,
2062 .load_artifact = loadArtifact,
2063 .create_render_artifact = createRenderArtifact,
2064 .load_render_artifact = loadRenderArtifact,
2065 .allocate_buffer = allocateBuffer,
2066 .allocate_texture = allocateTexture,
2067 .create_surface = createSurface,
2068 .destroy_surface = destroySurface,
2069 .destroy_texture = destroyTexture,
2070 .acquire_surface_frame = acquireSurfaceFrame,
2071 .present_surface_frame = presentSurfaceFrame,
2072 .write_surface_frame = writeSurfaceFrame,
2073 .create_stream = createStream,
2074 .create_event = createEvent,
2075 .write_buffer = writeBuffer,
2076 .read_buffer = readBuffer,
2077 .launch = launch,
2078 .render = render,
2079 .create_render_bindings = createRenderBindings,
2080 .record_render_bundle = recordRenderBundle,
2081 .submit_render_bundle = submitRenderBundle,
2082 .write_texture = writeTexture,
2083 .read_texture = readTexture,
2084 .synchronize = synchronize,
2085 .query_event = queryEvent,
2086 .record_event = recordEvent,
2087 .elapsed_event_ns = elapsedEventNs,
2088 .destroy_object = destroyObject,
2089 .deinit = deinitHandle,
2090 };
2091
2092 test "vulkan contract reports capabilities without a live runtime" {
2093 var state = State.init(std.testing.allocator);
2094 defer state.deinit();
2095 const handle = state.handle();
2096
2097 const caps = try handle.queryCapabilities();
2098 try std.testing.expectEqual(backend.BackendKind.vulkan, caps.identity.backend);
2099 try std.testing.expectEqual(backend.DeviceFamily.vulkan, caps.identity.family);
2100 try std.testing.expect(caps.supportsDType(.i1));
2101 try std.testing.expect(caps.supportsDType(.f32));
2102 try std.testing.expect(caps.supportsArtifactFormat(.vulkan_spirv));
2103 try std.testing.expect(caps.features.atomic_i32);
2104 try std.testing.expect(caps.features.atomic_u32);
2105 try std.testing.expect(caps.features.atomic_index);
2106 try std.testing.expect(!caps.features.atomic_f32_add_device);
2107 try std.testing.expect(!caps.features.atomic_f32_add_shared);
2108 try std.testing.expect(!caps.runtime.driver_loaded);
2109 try std.testing.expect(caps.runtime.timeline_events);
2110 }
2111
2112 test "vulkan runtime capabilities gate narrow integer dtypes" {
2113 const caps_without_int16 = capabilitiesFrom(.{
2114 .shader_float16 = false,
2115 .shader_float64 = false,
2116 .shader_int8 = true,
2117 .shader_int16 = false,
2118 .shader_int64 = false,
2119 .storage_buffer8 = true,
2120 .storage_buffer16 = false,
2121 .subgroup_size = 0,
2122 .vendor_id = 0,
2123 .vendor_name = "stub",
2124 }, true);
2125 try std.testing.expect(caps_without_int16.supportsDType(.i8));
2126 try std.testing.expect(caps_without_int16.supportsDType(.u8));
2127 try std.testing.expect(!caps_without_int16.supportsDType(.i16));
2128 try std.testing.expect(!caps_without_int16.supportsDType(.u16));
2129
2130 const caps_with_int16 = capabilitiesFrom(.{
2131 .shader_float16 = false,
2132 .shader_float64 = false,
2133 .shader_int8 = false,
2134 .shader_int16 = true,
2135 .shader_int64 = false,
2136 .storage_buffer8 = false,
2137 .storage_buffer16 = true,
2138 .subgroup_size = 0,
2139 .vendor_id = 0,
2140 .vendor_name = "stub",
2141 }, true);
2142 try std.testing.expect(!caps_with_int16.supportsDType(.i8));
2143 try std.testing.expect(!caps_with_int16.supportsDType(.u8));
2144 try std.testing.expect(caps_with_int16.supportsDType(.i16));
2145 try std.testing.expect(caps_with_int16.supportsDType(.u16));
2146
2147 const caps_without_storage = capabilitiesFrom(.{
2148 .shader_float16 = true,
2149 .shader_float64 = false,
2150 .shader_int8 = true,
2151 .shader_int16 = true,
2152 .shader_int64 = false,
2153 .storage_buffer8 = false,
2154 .storage_buffer16 = false,
2155 .subgroup_size = 0,
2156 .vendor_id = 0,
2157 .vendor_name = "stub",
2158 }, true);
2159 try std.testing.expect(!caps_without_storage.supportsDType(.i1));
2160 try std.testing.expect(!caps_without_storage.supportsDType(.f16));
2161 try std.testing.expect(!caps_without_storage.supportsDType(.i8));
2162 try std.testing.expect(!caps_without_storage.supportsDType(.u8));
2163 try std.testing.expect(!caps_without_storage.supportsDType(.i16));
2164 try std.testing.expect(!caps_without_storage.supportsDType(.u16));
2165
2166 const caps_with_64 = capabilitiesFrom(.{
2167 .shader_float16 = false,
2168 .shader_float64 = true,
2169 .shader_int8 = false,
2170 .shader_int16 = false,
2171 .shader_int64 = true,
2172 .storage_buffer8 = false,
2173 .storage_buffer16 = false,
2174 .subgroup_size = 0,
2175 .vendor_id = 0,
2176 .vendor_name = "stub",
2177 }, true);
2178 try std.testing.expect(caps_with_64.supportsDType(.f64));
2179 try std.testing.expect(caps_with_64.supportsDType(.i64));
2180 try std.testing.expect(caps_with_64.supportsDType(.u64));
2181 }
2182
2183 test "vulkan subgroup capabilities preserve partial operation masks" {
2184 const base = Runtime.Caps{
2185 .shader_float16 = false,
2186 .shader_float64 = false,
2187 .shader_int8 = false,
2188 .shader_int16 = false,
2189 .shader_int64 = false,
2190 .storage_buffer8 = false,
2191 .storage_buffer16 = false,
2192 .subgroup_size = 32,
2193 .subgroup_supported_stages = driver_mod.VK_SHADER_STAGE_COMPUTE_BIT,
2194 .subgroup_supported_operations = driver_mod.VK_SUBGROUP_FEATURE_BASIC_BIT,
2195 .vendor_id = 0,
2196 .vendor_name = "stub",
2197 };
2198
2199 const basic = capabilitiesFrom(base, true).subgroup;
2200 try std.testing.expect(basic.supported);
2201 try std.testing.expectEqual(@as(u32, 32), basic.size_min);
2202 try std.testing.expect(!basic.vote);
2203 try std.testing.expect(!basic.ballot);
2204 try std.testing.expect(!basic.arithmetic);
2205 try std.testing.expect(!basic.scan);
2206 try std.testing.expect(!basic.shuffle);
2207
2208 var partial = base;
2209 partial.subgroup_supported_operations |= driver_mod.VK_SUBGROUP_FEATURE_VOTE_BIT |
2210 driver_mod.VK_SUBGROUP_FEATURE_BALLOT_BIT |
2211 driver_mod.VK_SUBGROUP_FEATURE_ARITHMETIC_BIT |
2212 driver_mod.VK_SUBGROUP_FEATURE_SHUFFLE_BIT;
2213 const partial_facts = capabilitiesFrom(partial, true).subgroup;
2214 try std.testing.expect(partial_facts.vote);
2215 try std.testing.expect(partial_facts.ballot);
2216 try std.testing.expect(partial_facts.arithmetic);
2217 try std.testing.expect(partial_facts.scan);
2218 try std.testing.expect(!partial_facts.shuffle);
2219
2220 partial.subgroup_supported_operations |= driver_mod.VK_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT;
2221 try std.testing.expect(capabilitiesFrom(partial, true).subgroup.shuffle);
2222
2223 var missing_basic = base;
2224 missing_basic.subgroup_supported_operations = driver_mod.VK_SUBGROUP_FEATURE_VOTE_BIT;
2225 const unsupported_operations = capabilitiesFrom(missing_basic, true).subgroup;
2226 try std.testing.expect(!unsupported_operations.supported);
2227 try std.testing.expectEqual(@as(u32, 0), unsupported_operations.size_min);
2228
2229 var missing_compute = base;
2230 missing_compute.subgroup_supported_stages = driver_mod.VK_SHADER_STAGE_FRAGMENT_BIT;
2231 try std.testing.expect(!capabilitiesFrom(missing_compute, true).subgroup.supported);
2232 }
2233
2234 test "vulkan contract records unsupported and unavailable phases" {
2235 var state = State.init(std.testing.allocator);
2236 defer state.deinit();
2237 const handle = state.handle();
2238
2239 try std.testing.expectError(error.UnsupportedOperation, handle.createArtifact(.{
2240 .kernel_name = "main",
2241 .requested_format = .vulkan_spirv,
2242 }));
2243 try std.testing.expectError(error.CapabilityMismatch, handle.createArtifact(.{
2244 .kernel_name = "atomic_add",
2245 .requested_format = .vulkan_spirv,
2246 .required_features = .{ .atomic_f32_add_device = true },
2247 }));
2248 try std.testing.expectError(error.RuntimeUnavailable, handle.allocateBuffer(.{
2249 .byte_size = 16,
2250 .alignment = 16,
2251 }));
2252 try std.testing.expectError(error.RuntimeUnavailable, handle.createStream(.{}));
2253 try std.testing.expectError(error.RuntimeUnavailable, handle.createEvent(.{}));
2254 try std.testing.expectError(error.RuntimeUnavailable, handle.synchronize(.{
2255 .scope = .device,
2256 }));
2257 try std.testing.expectError(error.InvalidEvent, handle.queryEvent(.{
2258 .event = .{
2259 .id = 1,
2260 .backend = .vulkan,
2261 },
2262 }));
2263
2264 var artifact = try backend.KernelArtifact.init(std.testing.allocator, .{
2265 .backend = .vulkan,
2266 .format = .vulkan_spirv,
2267 .entry_name = "main",
2268 .argument_count = 0,
2269 });
2270 defer artifact.deinit();
2271 const words = [_]u32{ 0x07230203, 0x00010000 };
2272 artifact.setBorrowedWords(&words);
2273
2274 try std.testing.expectError(error.RuntimeUnavailable, handle.loadArtifact(&artifact));
2275 }
2276
2277 test "vulkan backend reports elapsed event timing from timestamp markers" {
2278 runtime_mod.testing.resetFake();
2279 var rt = runtime_mod.testing.fakeRuntime(std.testing.allocator);
2280 defer rt._live_streams.deinit(rt.allocator);
2281
2282 var state = State.initWithRuntime(std.testing.allocator, &rt);
2283 defer state.deinit();
2284 const handle = state.handle();
2285
2286 const stream = try handle.createStream(.{});
2287 const start = try handle.createEvent(.{});
2288 const end = try handle.createEvent(.{});
2289
2290 try handle.recordEvent(.{ .stream = stream, .event = start });
2291 try handle.recordEvent(.{ .stream = stream, .event = end });
2292 runtime_mod.testing.setQueryResults(&.{ 80, 120 });
2293
2294 try std.testing.expectEqual(@as(u64, 100), try handle.elapsedEventNs(.{
2295 .start = start,
2296 .end = end,
2297 }));
2298
2299 const snapshot = runtime_mod.testing.snapshot();
2300 try std.testing.expectEqual(@as(u32, 2), snapshot.query_pool_creates);
2301 try std.testing.expectEqual(@as(u32, 2), snapshot.cmd_write_timestamps);
2302 try std.testing.expectEqual(@as(u32, 2), snapshot.query_pool_results);
2303 }
2304
2305 test "vulkan contract creates and presents X11 surface frames through the runtime" {
2306 runtime_mod.testing.resetFake();
2307 var rt = runtime_mod.testing.fakeRuntime(std.testing.allocator);
2308 defer rt._live_streams.deinit(rt.allocator);
2309 defer runtime_mod.testing.releaseDefaultStream(&rt);
2310
2311 var state = State.initWithRuntime(std.testing.allocator, &rt);
2312 defer state.deinit();
2313 const handle = state.handle();
2314
2315 const surface = try handle.createSurface(.{
2316 .platform = .{ .x11 = .{ .display = 0x5151_0000, .window = 0x6161 } },
2317 .extent = .{ .width = 640, .height = 480 },
2318 .format = .bgra8_unorm,
2319 .color_space = .srgb,
2320 .present_mode = .fifo,
2321 .usage = .{ .copy_dst = true, .color_attachment = true, .present = true },
2322 .max_frames_in_flight = 2,
2323 });
2324 try std.testing.expectEqual(backend.BackendKind.vulkan, surface.backend);
2325 try std.testing.expectEqual(backend.SurfacePlatformKind.x11, surface.platform);
2326 try std.testing.expectEqual(@as(u32, 640), surface.extent.width);
2327 try std.testing.expectEqual(@as(u32, 480), surface.extent.height);
2328
2329 const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
2330 try std.testing.expectEqual(surface.id, frame.surface.id);
2331 try std.testing.expectEqual(backend.TextureOwnership.acquired_surface, frame.texture.ownership);
2332 try std.testing.expect(frame.texture.usage.present);
2333 try std.testing.expect(frame.texture.usage.color_attachment);
2334 try std.testing.expectError(error.SurfaceAlreadyAcquired, handle.acquireSurfaceFrame(.{ .surface = surface }));
2335 try std.testing.expectError(error.SurfaceAlreadyAcquired, handle.destroyTexture(frame.texture));
2336 try std.testing.expectError(error.SurfaceAlreadyAcquired, handle.destroySurface(surface));
2337
2338 const wait_stream = try handle.createStream(.{});
2339 const wait_event = try handle.createEvent(.{});
2340 const signal_event = try handle.createEvent(.{});
2341 try handle.recordEvent(.{ .stream = wait_stream, .event = wait_event });
2342 const ops = [_]backend.SurfaceFrameWriteOp{
2343 .{ .clear = .{ .r = 0.02, .g = 0.04, .b = 0.08, .a = 1.0 } },
2344 };
2345 try handle.writeSurfaceFrame(.{
2346 .surface = surface,
2347 .frame = frame,
2348 .operations = &ops,
2349 .wait_events = &.{wait_event},
2350 .signal_event = signal_event,
2351 });
2352 const write_snap = runtime_mod.testing.snapshot();
2353 try std.testing.expectEqual(@as(u32, 0), write_snap.waits);
2354 try std.testing.expectEqual(@as(u32, 3), write_snap.last_submit_wait_count);
2355 try std.testing.expectEqual(@as(u32, 2), write_snap.last_submit_signal_count);
2356 runtime_mod.testing.setCounterValue(1);
2357 try std.testing.expect(try handle.queryEvent(.{ .event = signal_event }));
2358
2359 try handle.presentSurfaceFrame(.{
2360 .surface = surface,
2361 .frame = frame,
2362 });
2363 try std.testing.expectError(error.SurfaceFrameExpired, handle.presentSurfaceFrame(.{
2364 .surface = surface,
2365 .frame = frame,
2366 }));
2367 try handle.destroyTexture(frame.texture);
2368 try handle.destroySurface(surface);
2369
2370 const snap = runtime_mod.testing.snapshot();
2371 try std.testing.expectEqual(@as(u32, 1), snap.surface_creates);
2372 try std.testing.expectEqual(@as(u32, 1), snap.swapchain_creates);
2373 try std.testing.expectEqual(@as(u32, 1), snap.acquires);
2374 try std.testing.expectEqual(@as(u32, 1), snap.presents);
2375 try std.testing.expectEqual(@as(u32, 1), snap.swapchain_destroys);
2376 try std.testing.expectEqual(@as(u32, 1), snap.surface_destroys);
2377 }
2378
2379 test "vulkan contract replaces surface handles for resized swapchains" {
2380 runtime_mod.testing.resetFake();
2381 var rt = runtime_mod.testing.fakeRuntime(std.testing.allocator);
2382 defer rt._live_streams.deinit(rt.allocator);
2383
2384 var state = State.initWithRuntime(std.testing.allocator, &rt);
2385 defer state.deinit();
2386 const handle = state.handle();
2387
2388 const old_surface = try handle.createSurface(.{
2389 .platform = .{ .x11 = .{ .display = 0x5151_0000, .window = 0x6161 } },
2390 .extent = .{ .width = 320, .height = 180 },
2391 .format = .bgra8_unorm,
2392 .color_space = .srgb,
2393 .present_mode = .fifo,
2394 .usage = .{ .copy_dst = true, .color_attachment = true, .present = true },
2395 .max_frames_in_flight = 2,
2396 });
2397 const old_frame = try handle.acquireSurfaceFrame(.{ .surface = old_surface });
2398 try handle.presentSurfaceFrame(.{
2399 .surface = old_surface,
2400 .frame = old_frame,
2401 });
2402 try handle.destroyTexture(old_frame.texture);
2403 try handle.destroySurface(old_surface);
2404
2405 const surface = try handle.createSurface(.{
2406 .platform = .{ .x11 = .{ .display = 0x5151_0000, .window = 0x6161 } },
2407 .extent = .{ .width = 800, .height = 450 },
2408 .format = .bgra8_unorm,
2409 .color_space = .srgb,
2410 .present_mode = .fifo,
2411 .usage = .{ .copy_dst = true, .color_attachment = true, .present = true },
2412 .max_frames_in_flight = 2,
2413 });
2414 try std.testing.expect(surface.id != old_surface.id);
2415 try std.testing.expectEqual(@as(u32, 800), surface.extent.width);
2416 try std.testing.expectEqual(@as(u32, 450), surface.extent.height);
2417 try std.testing.expectError(error.InvalidSurfaceFrame, handle.presentSurfaceFrame(.{
2418 .surface = surface,
2419 .frame = old_frame,
2420 }));
2421 try std.testing.expectError(error.InvalidSurface, handle.acquireSurfaceFrame(.{ .surface = old_surface }));
2422
2423 const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
2424 try std.testing.expectEqual(@as(u32, 800), frame.texture.extent.width);
2425 try std.testing.expectEqual(@as(u32, 450), frame.texture.extent.height);
2426 try handle.presentSurfaceFrame(.{
2427 .surface = surface,
2428 .frame = frame,
2429 });
2430 try handle.destroyTexture(frame.texture);
2431 try handle.destroySurface(surface);
2432
2433 const snap = runtime_mod.testing.snapshot();
2434 try std.testing.expectEqual(@as(u32, 2), snap.surface_creates);
2435 try std.testing.expectEqual(@as(u32, 2), snap.swapchain_creates);
2436 try std.testing.expectEqual(@as(u32, 2), snap.acquires);
2437 try std.testing.expectEqual(@as(u32, 2), snap.presents);
2438 try std.testing.expectEqual(@as(u32, 2), snap.swapchain_destroys);
2439 try std.testing.expectEqual(@as(u32, 2), snap.surface_destroys);
2440 }
2441
2442 const RawX11Window = struct {
2443 connection: sys.x11.Connection,
2444 window: u32,
2445 width: u16,
2446 height: u16,
2447
2448 fn init(allocator: Allocator, width: u16, height: u16) !RawX11Window {
2449 const connection_capacity = try sys.x11.Capacity.derive(.{
2450 .reply_byte_count = sys.x11.minimum_reply_byte_count,
2451 });
2452 var connection = try sys.x11.Connection.connect(allocator, connection_capacity);
2453 errdefer connection.close();
2454 const setup = connection.setup;
2455 const window = connection.generateId();
2456 const request = connection.beginRequest();
2457 try sys.x11.protocol.createWindow(request, .{
2458 .window = window,
2459 .parent = setup.root,
2460 .depth = setup.root_depth,
2461 .visual = setup.root_visual,
2462 .width = width,
2463 .height = height,
2464 .events = sys.x11.protocol.event_mask.exposure | sys.x11.protocol.event_mask.structure_notify,
2465 });
2466 try sys.x11.protocol.windowRequest(request, .map_window, window);
2467 try connection.sendRequest();
2468 _ = waitForX11WindowEvent(&connection);
2469 return .{
2470 .connection = connection,
2471 .window = window,
2472 .width = width,
2473 .height = height,
2474 };
2475 }
2476
2477 fn deinit(self: *RawX11Window) void {
2478 const request = self.connection.beginRequest();
2479 sys.x11.protocol.windowRequest(request, .destroy_window, self.window) catch {};
2480 self.connection.sendRequest() catch {};
2481 self.connection.close();
2482 self.* = undefined;
2483 }
2484 };
2485
2486 fn waitForX11WindowEvent(connection: *sys.x11.Connection) bool {
2487 if (!(connection.waitReadable(1000) catch return false)) return false;
2488 _ = connection.nextMessage() catch return false;
2489 return true;
2490 }
2491
2492 test "vulkan live xlib surface clears copies and presents a raw x11 window" {
2493 if (!build_options.vulkan_tests) return error.SkipZigTest;
2494 if (sys.env.get("DISPLAY") == null) return error.SkipZigTest;
2495
2496 var raw = RawX11Window.init(std.testing.allocator, 64, 64) catch |err| switch (err) {
2497 error.MissingDisplay,
2498 error.RemoteDisplayUnsupported,
2499 error.InvalidDisplay,
2500 error.ConnectionFailed,
2501 error.AuthenticationFailed,
2502 error.SetupFailed,
2503 error.SetupTruncated,
2504 error.NoUsableScreen,
2505 error.NoUsableVisual,
2506 => return error.SkipZigTest,
2507 else => |actual| return actual,
2508 };
2509 defer raw.deinit();
2510
2511 var xlib = sys.x11.xlib.openTarget(raw.connection.target) catch |err| switch (err) {
2512 error.RuntimeUnavailable,
2513 error.SymbolMissing,
2514 error.InvalidDisplay,
2515 => return error.SkipZigTest,
2516 };
2517 defer xlib.close();
2518
2519 var state = State.initDevice(std.testing.allocator, 0) catch |err| switch (err) {
2520 error.RuntimeUnavailable => return error.SkipZigTest,
2521 else => |actual| return actual,
2522 };
2523 defer state.deinit();
2524 const handle = state.handle();
2525
2526 const surface = handle.createSurface(.{
2527 .platform = .{ .x11 = .{
2528 .display = @intFromPtr(xlib.display),
2529 .window = raw.window,
2530 .visual_id = raw.connection.setup.root_visual,
2531 .depth = raw.connection.setup.root_depth,
2532 } },
2533 .extent = .{ .width = raw.width, .height = raw.height },
2534 .format = .bgra8_unorm,
2535 .color_space = .srgb,
2536 .present_mode = .fifo,
2537 .usage = .{ .copy_dst = true, .color_attachment = true, .present = true },
2538 .max_frames_in_flight = 2,
2539 }) catch |err| switch (err) {
2540 error.RuntimeUnavailable,
2541 error.CapabilityMismatch,
2542 => return error.SkipZigTest,
2543 else => |actual| return actual,
2544 };
2545 errdefer handle.destroySurface(surface) catch {};
2546
2547 const frame = try handle.acquireSurfaceFrame(.{ .surface = surface });
2548 try std.testing.expectEqual(surface.id, frame.surface.id);
2549 try std.testing.expectEqual(backend.TextureOwnership.acquired_surface, frame.texture.ownership);
2550
2551 const pixel_count = try std.math.mul(
2552 usize,
2553 @as(usize, frame.texture.extent.width),
2554 @as(usize, frame.texture.extent.height),
2555 );
2556 const byte_count = try std.math.mul(usize, pixel_count, 4);
2557 const pixels = try std.testing.allocator.alloc(u8, byte_count);
2558 defer std.testing.allocator.free(pixels);
2559 var pixel: usize = 0;
2560 while (pixel < pixel_count) : (pixel += 1) {
2561 const offset = pixel * 4;
2562 pixels[offset + 0] = 0x10;
2563 pixels[offset + 1] = 0x80;
2564 pixels[offset + 2] = 0xd0;
2565 pixels[offset + 3] = 0xff;
2566 }
2567
2568 const staging = try handle.allocateBuffer(.{
2569 .byte_size = pixels.len,
2570 .alignment = 16,
2571 });
2572 defer handle.destroyObject(staging.id);
2573 var submitted_surface_work = false;
2574 defer if (submitted_surface_work) handle.synchronize(.{ .scope = .device }) catch {};
2575
2576 try handle.writeBuffer(.{
2577 .handle = staging,
2578 .bytes = pixels,
2579 });
2580
2581 const ops = [_]backend.SurfaceFrameWriteOp{
2582 .{ .clear = .{ .r = 0.02, .g = 0.04, .b = 0.08, .a = 1.0 } },
2583 .{ .copy_buffer = staging },
2584 };
2585 try handle.writeSurfaceFrame(.{
2586 .surface = surface,
2587 .frame = frame,
2588 .operations = &ops,
2589 });
2590 submitted_surface_work = true;
2591
2592 try handle.presentSurfaceFrame(.{
2593 .surface = surface,
2594 .frame = frame,
2595 });
2596 try handle.synchronize(.{ .scope = .device });
2597 submitted_surface_work = false;
2598 try handle.destroyTexture(frame.texture);
2599 try handle.destroySurface(surface);
2600 }
2601
2602 test "vulkan contract rejects direct artifact creation before runtime access" {
2603 var state = State.init(std.testing.allocator);
2604 defer state.deinit();
2605 const handle = state.handle();
2606
2607 try std.testing.expectError(error.UnsupportedOperation, handle.createArtifact(.{
2608 .kernel_name = "direct_add_f32",
2609 .requested_format = .vulkan_spirv,
2610 .required_dtypes = backend.DTypeSet.init(&.{.f32}),
2611 .diagnostic_id = "choir/kernel/0",
2612 }));
2613 }
2614
2615 test "vulkan contract creates SPIR-V artifact before runtime access" {
2616 var state = State.init(std.testing.allocator);
2617 defer state.deinit();
2618 const handle = state.handle();
2619
2620 const words = [_]u32{ 0x07230203, 0x00010000, 0, 0, 0, 0 };
2621 var artifact = try handle.createArtifact(.{
2622 .kernel_name = "vulkan_choir_copy_i32",
2623 .requested_format = .vulkan_spirv,
2624 .argument_count = 2,
2625 .required_dtypes = backend.DTypeSet.init(&.{.i32}),
2626 .diagnostic_id = "vulkan_choir_copy_i32",
2627 .payload = .{ .words_u32 = &words },
2628 });
2629 defer artifact.deinit();
2630
2631 try std.testing.expectEqual(backend.BackendKind.vulkan, artifact.backend);
2632 try std.testing.expectEqual(backend.ArtifactFormat.vulkan_spirv, artifact.format);
2633 try std.testing.expectEqualStrings("vulkan_choir_copy_i32", artifact.entry_name);
2634 try std.testing.expectEqual(@as(u32, 2), artifact.argument_count);
2635 try std.testing.expectEqualStrings("vulkan_choir_copy_i32", artifact.diagnostic_id.?);
2636 try std.testing.expect(artifact.payload.words_u32.len > 5);
2637 try std.testing.expectEqual(@as(u32, 0x07230203), artifact.payload.words_u32[0]);
2638 try std.testing.expectError(error.RuntimeUnavailable, handle.loadArtifact(&artifact));
2639 }
2640
2641 test "vulkan contract validates artifact format before runtime access" {
2642 var state = State.init(std.testing.allocator);
2643 defer state.deinit();
2644 const handle = state.handle();
2645
2646 var artifact = try backend.KernelArtifact.init(std.testing.allocator, .{
2647 .backend = .vulkan,
2648 .format = .cuda_ptx,
2649 .entry_name = "main",
2650 .argument_count = 0,
2651 });
2652 defer artifact.deinit();
2653 artifact.setBorrowedText("// ptx");
2654
2655 try std.testing.expectError(error.UnsupportedArtifactFormat, handle.loadArtifact(&artifact));
2656 }