diff --git a/Usage.md b/Usage.md index 243ae9a..d0dfc01 100644 --- a/Usage.md +++ b/Usage.md @@ -190,6 +190,12 @@ try rt.addSpecializationInfo( Add specialization constants before calling the entry point. +To copy specialization constants between runtimes: + +```zig +try rt.copySpecializationConstantsFrom(allocator, &source_rt); +``` + ## Entry points and barriers For most shaders, `callEntryPoint` is enough: @@ -235,13 +241,50 @@ const image_api = spv.Runtime.ImageAPI{ .writeImageInt4 = writeImageInt4, .sampleImageFloat4 = sampleImageFloat4, .sampleImageInt4 = sampleImageInt4, + .sampleImageDref = sampleImageDref, .queryImageSize = queryImageSize, }; var rt = try spv.Runtime.init(allocator, &module, image_api); ``` -Each callback receives your driver-side image or sampler pointer and returns either a `Vec4` value or an error. +Sample callbacks receive optional explicit LOD and an integer texel offset: + +```zig +fn sampleImageFloat4( + driver_image: *anyopaque, + driver_sampler: *anyopaque, + dim: spv.SpvDim, + x: f32, + y: f32, + z: f32, + lod: ?f32, + offset: spv.Runtime.ImageOffset, +) spv.Runtime.RuntimeError!spv.Runtime.Vec4(f32) { + _ = .{ driver_image, driver_sampler, dim, x, y, z, lod, offset }; + return .{ .x = 0, .y = 0, .z = 0, .w = 1 }; +} +``` + +Depth-comparison samplers call `sampleImageDref` and return a scalar `f32`. + +## Derivatives + +Fragment shaders using derivative operations need derivative data on the source result: + +```zig +try rt.setDerivativeFromMemory( + allocator, + input_result, + std.mem.asBytes(&dx), + std.mem.asBytes(&dy), +); + +try rt.copyDerivative(allocator, dst_result, input_result); +rt.clearDerivative(allocator, dst_result); +``` + +For low-level integrations, `setDerivative` accepts interpreter `Value` objects directly. --- @@ -294,7 +337,7 @@ ffi/SpirvInterpreter.h static const unsigned char shader_source[] = { /* Shader bytecode */ -} +}; int main(void) { @@ -310,7 +353,7 @@ int main(void) * A zeroed image API is only safe when the shader does not execute image * load/store/sample/query operations. */ - if(SpvInitRuntime(&runtime, module) != SPV_RESULT_SUCCESS) + if(SpvInitRuntime(&runtime, module, (SpvImageAPI){0}) != SPV_RESULT_SUCCESS) return -1; SpvWord main_entry_index; @@ -337,25 +380,13 @@ Write by result id: ```c SpvWord pos_result = 0; -if (!CheckSpv( - SpvGetResultByName(runtime, "pos", &pos_result), - "SpvGetResultByName")) -{ +if (SpvGetResultByName(runtime, "pos", &pos_result) != SPV_RESULT_SUCCESS) return 1; -} float pos[2] = {10.0f, 20.0f}; -if (!CheckSpv( - SpvWriteInput( - runtime, - (const SpvByte*)pos, - sizeof(pos), - pos_result), - "SpvWriteInput")) -{ +if (SpvWriteInput(runtime, (const SpvByte*)pos, sizeof(pos), pos_result) != SPV_RESULT_SUCCESS) return 1; -} ``` Write by input location: @@ -363,10 +394,8 @@ Write by input location: ```c float uv[2] = {0.25f, 0.75f}; -if (!SpvWriteInputLocation(runtime, (const SpvByte*)uv, sizeof(uv), 0)) -{ +if (SpvWriteInputLocation(runtime, (const SpvByte*)uv, sizeof(uv), 0) != SPV_RESULT_SUCCESS) return 1; -} ``` ## Reading outputs from C @@ -411,7 +440,7 @@ PushConstants push_constants = { .scale = 2.0f, }; -SpvPopulatePushConstants(runtime, (const SpvByte*)&push_constants, sizeof(push_constants); +SpvPopulatePushConstants(runtime, (const SpvByte*)&push_constants, sizeof(push_constants)); ``` ## Descriptor sets from C @@ -426,6 +455,13 @@ SpvWriteDescriptorSet(runtime, (const SpvByte*)buffer, buffer_size, For non-array descriptors, use descriptor index `0`. +You can query a descriptor binding from a module: + +```c +SpvWord result = 0; +SpvResult status = SpvModuleGetBindingResult(module, 0, 1, &result); +``` + After a shader writes through descriptor-backed memory, flush descriptor sets before reading the backing data: ```c @@ -449,6 +485,30 @@ SpvAddSpecializationInfo(runtime, entry, (const SpvByte*)&value, sizeof(value)); Add specialization constants before calling the entry point. +To duplicate specialization constants from another runtime: + +```c +SpvCopySpecializationConstantsFrom(runtime, source_runtime); +``` + +## Derivatives from C + +```c +float dx[4] = {1.0f, 0.0f, 0.0f, 0.0f}; +float dy[4] = {0.0f, 1.0f, 0.0f, 0.0f}; + +SpvSetDerivativeFromMemory( + runtime, + result, + (const SpvByte*)dx, + sizeof(dx), + (const SpvByte*)dy, + sizeof(dy)); + +SpvCopyDerivative(runtime, dst_result, result); +SpvClearDerivative(runtime, dst_result); +``` + ## Barriers from C For most shaders: @@ -499,6 +559,67 @@ static SpvResult ReadImageFloat4( } ``` +Sampling callbacks include an explicit-LOD flag/value and an offset: + +```c +static SpvResult SampleImageFloat4( + void* driver_image, + void* driver_sampler, + SpvDim dim, + float x, + float y, + float z, + SpvBool has_lod, + float lod, + SpvImageOffset offset, + SpvVec4f* dst) +{ + (void)driver_image; + (void)driver_sampler; + (void)dim; + (void)has_lod; + (void)lod; + (void)offset; + + dst->x = x; + dst->y = y; + dst->z = z; + dst->w = 1.0f; + return SPV_RESULT_SUCCESS; +} +``` + +Depth-comparison samplers add `dref` and write one `float`: + +```c +static SpvResult SampleImageDref( + void* driver_image, + void* driver_sampler, + SpvDim dim, + float x, + float y, + float z, + float dref, + SpvBool has_lod, + float lod, + SpvImageOffset offset, + float* dst) +{ + (void)driver_image; + (void)driver_sampler; + (void)dim; + (void)x; + (void)y; + (void)z; + (void)has_lod; + (void)lod; + (void)offset; + + *dst = dref; + return SPV_RESULT_SUCCESS; +} +``` + The image API table contains these callbacks: ```c @@ -509,6 +630,7 @@ SpvImageAPI image_api = { .SpvWriteImageInt4 = WriteImageInt4, .SpvSampleImageFloat4 = SampleImageFloat4, .SpvSampleImageInt4 = SampleImageInt4, + .SpvSampleImageDref = SampleImageDref, .SpvQueryImageSize = QueryImageSize, }; ``` diff --git a/ffi/SpirvInterpreter.h b/ffi/SpirvInterpreter.h index 5fd7ef7..6350022 100644 --- a/ffi/SpirvInterpreter.h +++ b/ffi/SpirvInterpreter.h @@ -497,12 +497,20 @@ typedef struct unsigned int w; } SpvVec4u; +typedef struct +{ + int x; + int y; + int z; +} SpvImageOffset; + typedef SpvResult (*SpvReadImageFloat4_PFN)(void* driver_image, SpvDim dim, int x, int y, int z, SpvVec4f* dst); typedef SpvResult (*SpvReadImageInt4_PFN)(void* driver_image, SpvDim dim, int x, int y, int z, SpvVec4u* dst); typedef SpvResult (*SpvWriteImageFloat4_PFN)(void* driver_image, SpvDim dim, int x, int y, int z, SpvVec4f src); typedef SpvResult (*SpvWriteImageInt4_PFN)(void* driver_image, SpvDim dim, int x, int y, int z, SpvVec4u src); -typedef SpvResult (*SpvSampleImageFloat4_PFN)(void* driver_image, void* driver_sampler, SpvDim dim, float x, float y, float z, float lod, SpvVec4f* dst); -typedef SpvResult (*SpvSampleImageInt4_PFN)(void* driver_image, void* driver_sampler, SpvDim dim, float x, float y, float z, float lod, SpvVec4u* dst); +typedef SpvResult (*SpvSampleImageFloat4_PFN)(void* driver_image, void* driver_sampler, SpvDim dim, float x, float y, float z, SpvBool has_lod, float lod, SpvImageOffset offset, SpvVec4f* dst); +typedef SpvResult (*SpvSampleImageInt4_PFN)(void* driver_image, void* driver_sampler, SpvDim dim, float x, float y, float z, SpvBool has_lod, float lod, SpvImageOffset offset, SpvVec4u* dst); +typedef SpvResult (*SpvSampleImageDref_PFN)(void* driver_image, void* driver_sampler, SpvDim dim, float x, float y, float z, float dref, SpvBool has_lod, float lod, SpvImageOffset offset, float* dst); typedef SpvResult (*SpvQueryImageSize_PFN)(void* driver_image, SpvDim dim, SpvBool arrayed, SpvVec4u* dst); typedef struct @@ -513,6 +521,7 @@ typedef struct SpvWriteImageInt4_PFN SpvWriteImageInt4; SpvSampleImageFloat4_PFN SpvSampleImageFloat4; SpvSampleImageInt4_PFN SpvSampleImageInt4; + SpvSampleImageDref_PFN SpvSampleImageDref; SpvQueryImageSize_PFN SpvQueryImageSize; } SpvImageAPI; @@ -523,6 +532,7 @@ SPV_API SpvResult SpvInitModule(SpvModule* module, const SpvWord* source, SpvSiz SPV_API void SpvDeinitModule(SpvModule module); SPV_API SpvModuleReflectionInfos SpvModuleGetReflectionInfos(SpvModule module); +SPV_API SpvResult SpvModuleGetBindingResult(SpvModule module, SpvWord set, SpvWord binding, SpvWord* result); SPV_API SpvResult SpvInitRuntime(SpvRuntime* runtime, SpvModule module, SpvImageAPI image_api); SPV_API void SpvDeinitRuntime(SpvRuntime runtime); @@ -530,6 +540,10 @@ SPV_API void SpvDeinitRuntime(SpvRuntime runtime); SPV_API SpvResult SpvFlushDescriptorSets(SpvRuntime runtime); SPV_API SpvResult SpvAddSpecializationInfo(SpvRuntime runtime, SpvRuntimeSpecializationEntry entry, const SpvByte* data, SpvSize data_size); +SPV_API SpvResult SpvCopySpecializationConstantsFrom(SpvRuntime runtime, SpvRuntime other); +SPV_API SpvResult SpvSetDerivativeFromMemory(SpvRuntime runtime, SpvWord result, const SpvByte* dx, SpvSize dx_size, const SpvByte* dy, SpvSize dy_size); +SPV_API void SpvClearDerivative(SpvRuntime runtime, SpvWord result); +SPV_API SpvResult SpvCopyDerivative(SpvRuntime runtime, SpvWord dst, SpvWord src); SPV_API SpvResult SpvPopulatePushConstants(SpvRuntime runtime, const SpvByte* data, SpvSize data_size); SPV_API SpvResult SpvGetResultByName(SpvRuntime runtime, const char* name, SpvWord* result); @@ -540,7 +554,6 @@ SPV_API SpvResult SpvGetEntryPointByName(SpvRuntime runtime, const char* name, S SPV_API SpvResult SpvGetResultMemorySize(SpvRuntime runtime, SpvWord result, SpvSize* size); SPV_API SpvResult SpvGetInputLocationMemorySize(SpvRuntime runtime, SpvWord location, SpvSize* size); SPV_API SpvResult SpvGetResultPrimitiveType(SpvRuntime runtime, SpvWord result, SpvPrimitiveType* primitive_type); -SPV_API SpvBool SpvResultIsInteger(SpvRuntime runtime, SpvWord result); SPV_API SpvBool SpvHasResultDecoration(SpvRuntime runtime, SpvWord result, SpvDecoration decoration); SPV_API SpvResult SpvCallEntryPoint(SpvRuntime runtime, SpvWord entry_point_index); diff --git a/ffi/module.zig b/ffi/module.zig index 61d6893..c9510e1 100644 --- a/ffi/module.zig +++ b/ffi/module.zig @@ -68,3 +68,8 @@ export fn SpvModuleGetReflectionInfos(module: *spv.Module) callconv(.c) Reflecti .has_control_barriers = if (module.reflection_infos.has_control_barriers) 1 else 0, }; } + +export fn SpvModuleGetBindingResult(module: *const spv.Module, set: ffi.SpvCWord, binding: ffi.SpvCWord, result: *ffi.SpvCWord) callconv(.c) ffi.Result { + result.* = module.getBindingResult(@intCast(set), @intCast(binding)) orelse return .NotFound; + return .Success; +} diff --git a/ffi/runtime.zig b/ffi/runtime.zig index 81823c9..64769af 100644 --- a/ffi/runtime.zig +++ b/ffi/runtime.zig @@ -39,12 +39,19 @@ const Vec4u = extern struct { w: c_uint, }; +const ImageOffset = extern struct { + x: c_int, + y: c_int, + z: c_int, +}; + const readImageFloat4_PFN = *const fn (driver_image: ?*anyopaque, dim: spv.spv.SpvDim, x: c_int, y: c_int, z: c_int, dst: *Vec4f) callconv(.c) ffi.Result; const readImageInt4_PFN = *const fn (driver_image: ?*anyopaque, dim: spv.spv.SpvDim, x: c_int, y: c_int, z: c_int, dst: *Vec4u) callconv(.c) ffi.Result; const writeImageFloat4_PFN = *const fn (driver_image: ?*anyopaque, dim: spv.spv.SpvDim, x: c_int, y: c_int, z: c_int, src: Vec4f) callconv(.c) ffi.Result; const writeImageInt4_PFN = *const fn (driver_image: ?*anyopaque, dim: spv.spv.SpvDim, x: c_int, y: c_int, z: c_int, src: Vec4u) callconv(.c) ffi.Result; -const sampleImageFloat4_PFN = *const fn (driver_image: ?*anyopaque, driver_sampler: ?*anyopaque, dim: spv.spv.SpvDim, x: f32, y: f32, z: f32, lod: f32, dst: *Vec4f) callconv(.c) ffi.Result; -const sampleImageInt4_PFN = *const fn (driver_image: ?*anyopaque, driver_sampler: ?*anyopaque, dim: spv.spv.SpvDim, x: f32, y: f32, z: f32, lod: f32, dst: *Vec4u) callconv(.c) ffi.Result; +const sampleImageFloat4_PFN = *const fn (driver_image: ?*anyopaque, driver_sampler: ?*anyopaque, dim: spv.spv.SpvDim, x: f32, y: f32, z: f32, has_lod: ffi.SpvCBool, lod: f32, offset: ImageOffset, dst: *Vec4f) callconv(.c) ffi.Result; +const sampleImageInt4_PFN = *const fn (driver_image: ?*anyopaque, driver_sampler: ?*anyopaque, dim: spv.spv.SpvDim, x: f32, y: f32, z: f32, has_lod: ffi.SpvCBool, lod: f32, offset: ImageOffset, dst: *Vec4u) callconv(.c) ffi.Result; +const sampleImageDref_PFN = *const fn (driver_image: ?*anyopaque, driver_sampler: ?*anyopaque, dim: spv.spv.SpvDim, x: f32, y: f32, z: f32, dref: f32, has_lod: ffi.SpvCBool, lod: f32, offset: ImageOffset, dst: *f32) callconv(.c) ffi.Result; const queryImageSize_PFN = *const fn (driver_image: ?*anyopaque, dim: spv.spv.SpvDim, arrayed: ffi.SpvCBool, dst: *Vec4u) callconv(.c) ffi.Result; const ImageAPI = extern struct { @@ -54,6 +61,7 @@ const ImageAPI = extern struct { writeImageInt4: writeImageInt4_PFN, sampleImageFloat4: sampleImageFloat4_PFN, sampleImageInt4: sampleImageInt4_PFN, + sampleImageDref: sampleImageDref_PFN, queryImageSize: queryImageSize_PFN, }; @@ -159,11 +167,19 @@ const ImageAPIBridge = struct { try fromCResult(result); } - fn sampleImageFloat4(driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32) spv.Runtime.RuntimeError!spv.Runtime.Vec4(f32) { + fn toCImageOffset(offset: spv.Runtime.ImageOffset) ImageOffset { + return .{ + .x = @intCast(offset.x), + .y = @intCast(offset.y), + .z = @intCast(offset.z), + }; + } + + fn sampleImageFloat4(driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32, offset: spv.Runtime.ImageOffset) spv.Runtime.RuntimeError!spv.Runtime.Vec4(f32) { const image_api = try getImageAPI(); var dst: Vec4f = undefined; - const result = image_api.sampleImageFloat4(driver_image, driver_sampler, dim, x, y, z, lod orelse 0.0, &dst); + const result = image_api.sampleImageFloat4(driver_image, driver_sampler, dim, x, y, z, if (lod == null) 0 else 1, lod orelse 0.0, toCImageOffset(offset), &dst); try fromCResult(result); @@ -175,11 +191,11 @@ const ImageAPIBridge = struct { }; } - fn sampleImageInt4(driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32) spv.Runtime.RuntimeError!spv.Runtime.Vec4(u32) { + fn sampleImageInt4(driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32, offset: spv.Runtime.ImageOffset) spv.Runtime.RuntimeError!spv.Runtime.Vec4(u32) { const image_api = try getImageAPI(); var dst: Vec4u = undefined; - const result = image_api.sampleImageInt4(driver_image, driver_sampler, dim, x, y, z, lod orelse 0.0, &dst); + const result = image_api.sampleImageInt4(driver_image, driver_sampler, dim, x, y, z, if (lod == null) 0 else 1, lod orelse 0.0, toCImageOffset(offset), &dst); try fromCResult(result); @@ -191,6 +207,17 @@ const ImageAPIBridge = struct { }; } + fn sampleImageDref(driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.spv.SpvDim, x: f32, y: f32, z: f32, dref: f32, lod: ?f32, offset: spv.Runtime.ImageOffset) spv.Runtime.RuntimeError!f32 { + const image_api = try getImageAPI(); + + var dst: f32 = undefined; + const result = image_api.sampleImageDref(driver_image, driver_sampler, dim, x, y, z, dref, if (lod == null) 0 else 1, lod orelse 0.0, toCImageOffset(offset), &dst); + + try fromCResult(result); + + return dst; + } + fn queryImageSize(driver_image: *anyopaque, dim: spv.spv.SpvDim, arrayed: bool) spv.Runtime.RuntimeError!spv.Runtime.Vec4(u32) { const image_api = try getImageAPI(); @@ -229,6 +256,7 @@ export fn SpvInitRuntime(rt: **RuntimeWrapper, module: *spv.Module, image_api: I .writeImageInt4 = ImageAPIBridge.writeImageInt4, .sampleImageFloat4 = ImageAPIBridge.sampleImageFloat4, .sampleImageInt4 = ImageAPIBridge.sampleImageInt4, + .sampleImageDref = ImageAPIBridge.sampleImageDref, .queryImageSize = ImageAPIBridge.queryImageSize, }, ) catch |err| { @@ -264,6 +292,29 @@ export fn SpvAddSpecializationInfo(rt: *RuntimeWrapper, entry: CSpecializationEn return .Success; } +export fn SpvCopySpecializationConstantsFrom(rt: *RuntimeWrapper, other: *const RuntimeWrapper) callconv(.c) ffi.Result { + const allocator = std.heap.c_allocator; + rt.rt.copySpecializationConstantsFrom(allocator, &other.rt) catch |err| return toCResult(err); + return .Success; +} + +export fn SpvSetDerivativeFromMemory(rt: *RuntimeWrapper, result: spv.SpvWord, dx: [*]const u8, dx_size: c_ulong, dy: [*]const u8, dy_size: c_ulong) callconv(.c) ffi.Result { + const allocator = std.heap.c_allocator; + rt.rt.setDerivativeFromMemory(allocator, result, dx[0..dx_size], dy[0..dy_size]) catch |err| return toCResult(err); + return .Success; +} + +export fn SpvClearDerivative(rt: *RuntimeWrapper, result: spv.SpvWord) callconv(.c) void { + const allocator = std.heap.c_allocator; + rt.rt.clearDerivative(allocator, result); +} + +export fn SpvCopyDerivative(rt: *RuntimeWrapper, dst: spv.SpvWord, src: spv.SpvWord) callconv(.c) ffi.Result { + const allocator = std.heap.c_allocator; + rt.rt.copyDerivative(allocator, dst, src) catch |err| return toCResult(err); + return .Success; +} + export fn SpvPopulatePushConstants(rt: *RuntimeWrapper, data: [*]const u8, data_size: c_ulong) callconv(.c) ffi.Result { rt.rt.populatePushConstants(data[0..data_size]) catch |err| return toCResult(err); return .Success; @@ -319,10 +370,6 @@ export fn SpvGetResultPrimitiveType(rt: *RuntimeWrapper, result: spv.SpvWord, pr return .Success; } -export fn SpvResultIsInteger(rt: *RuntimeWrapper, result: spv.SpvWord) callconv(.c) c_int { - return if (rt.rt.resultIsInteger(result)) 1 else 0; -} - export fn SpvHasResultDecoration(rt: *RuntimeWrapper, result: spv.SpvWord, decoration: spv.spv.SpvDecoration) callconv(.c) c_int { return if (rt.rt.hasResultDecoration(result, decoration)) 1 else 0; } diff --git a/src/Result.zig b/src/Result.zig index 2170364..c477d29 100644 --- a/src/Result.zig +++ b/src/Result.zig @@ -478,6 +478,12 @@ pub fn getMemberCounts(self: *const Self) usize { } pub inline fn flushPtr(self: *Self, allocator: std.mem.Allocator) RuntimeError!void { - const value = self.getValue() catch return; - try value.flushPtr(allocator); + if (self.variant) |*variant| switch (variant.*) { + .Variable => |*v| try v.value.flushPtr(allocator), + .AccessChain => |*a| { + if (!std.mem.allEqual(u8, std.mem.asBytes(&a.value), 0xaa)) + try a.value.flushPtr(allocator); + }, + else => {}, + }; } diff --git a/src/Runtime.zig b/src/Runtime.zig index dd9472a..068b630 100644 --- a/src/Runtime.zig +++ b/src/Runtime.zig @@ -81,13 +81,20 @@ pub fn Vec4(comptime T: type) type { }; } +pub const ImageOffset = struct { + x: i32 = 0, + y: i32 = 0, + z: i32 = 0, +}; + pub const ImageAPI = struct { readImageFloat4: *const fn (driver_image: *anyopaque, dim: spv.SpvDim, x: i32, y: i32, z: i32) RuntimeError!Vec4(f32), readImageInt4: *const fn (driver_image: *anyopaque, dim: spv.SpvDim, x: i32, y: i32, z: i32) RuntimeError!Vec4(u32), writeImageFloat4: *const fn (driver_image: *anyopaque, dim: spv.SpvDim, x: i32, y: i32, z: i32, pixel: Vec4(f32)) RuntimeError!void, writeImageInt4: *const fn (driver_image: *anyopaque, dim: spv.SpvDim, x: i32, y: i32, z: i32, pixel: Vec4(u32)) RuntimeError!void, - sampleImageFloat4: *const fn (driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32) RuntimeError!Vec4(f32), - sampleImageInt4: *const fn (driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32) RuntimeError!Vec4(u32), + sampleImageFloat4: *const fn (driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32, offset: ImageOffset) RuntimeError!Vec4(f32), + sampleImageInt4: *const fn (driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32, offset: ImageOffset) RuntimeError!Vec4(u32), + sampleImageDref: *const fn (driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, dref: f32, lod: ?f32, offset: ImageOffset) RuntimeError!f32, queryImageSize: *const fn (driver_image: *anyopaque, dim: spv.SpvDim, arrayed: bool) RuntimeError!Vec4(u32), }; @@ -287,13 +294,6 @@ pub fn getResultPrimitiveType(self: *const Self, result: SpvWord) RuntimeError!P return (try self.results[result].getConstValue()).resolvePrimitiveType(); } -pub fn resultIsInteger(self: *const Self, result: SpvWord) bool { - return switch (self.getResultPrimitiveType(result) catch return false) { - .SInt, .UInt => true, - else => false, - }; -} - pub fn dumpResultsTable(self: *Self, allocator: std.mem.Allocator, writer: *std.Io.Writer) RuntimeError!void { const dump = pretty.dump(allocator, self.results, .{ .tab_size = 4, @@ -618,17 +618,10 @@ pub fn resetInvocation(self: *Self, allocator: std.mem.Allocator) void { if (result.variant) |*variant| { switch (variant.*) { .AccessChain => |*access_chain| { - access_chain.value.flushPtr(allocator) catch {}; - }, - else => {}, - } - } - } - - for (self.results) |*result| { - if (result.variant) |*variant| { - switch (variant.*) { - .AccessChain => |*access_chain| { + if (std.mem.allEqual(u8, std.mem.asBytes(&access_chain.value), 0xaa)) { + result.variant = null; + continue; + } access_chain.value.deinit(allocator); allocator.free(access_chain.indexes); result.variant = null; @@ -645,6 +638,7 @@ pub fn resetInvocation(self: *Self, allocator: std.mem.Allocator) void { } fn reset(self: *Self) void { + self.it = self.mod.it; self.function_stack.clearRetainingCapacity(); self.current_parameter_index = 0; self.current_function = null; diff --git a/src/opcodes.zig b/src/opcodes.zig index 490304a..906d6f3 100644 --- a/src/opcodes.zig +++ b/src/opcodes.zig @@ -74,8 +74,14 @@ const ImageOp = enum { QuerySizeLod, Read, Resolve, + SampleDrefExplicitLod, + SampleDrefImplicitLod, SampleExplicitLod, SampleImplicitLod, + SampleProjDrefExplicitLod, + SampleProjDrefImplicitLod, + SampleProjExplicitLod, + SampleProjImplicitLod, Write, }; @@ -198,6 +204,12 @@ pub const SetupDispatcher = block: { .ImageRead = autoSetupConstant, .ImageSampleExplicitLod = autoSetupConstant, .ImageSampleImplicitLod = autoSetupConstant, + .ImageSampleDrefExplicitLod = autoSetupConstant, + .ImageSampleDrefImplicitLod = autoSetupConstant, + .ImageSampleProjDrefExplicitLod = autoSetupConstant, + .ImageSampleProjDrefImplicitLod = autoSetupConstant, + .ImageSampleProjExplicitLod = autoSetupConstant, + .ImageSampleProjImplicitLod = autoSetupConstant, .ImageTexelPointer = autoSetupConstant, .InBoundsAccessChain = setupAccessChain, .IsFinite = autoSetupConstant, @@ -366,6 +378,12 @@ pub fn initRuntimeDispatcher() void { runtime_dispatcher[@intFromEnum(spv.SpvOp.ImageRead)] = ImageEngine(.Read).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.ImageSampleExplicitLod)] = ImageEngine(.SampleExplicitLod).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.ImageSampleImplicitLod)] = ImageEngine(.SampleImplicitLod).op; + runtime_dispatcher[@intFromEnum(spv.SpvOp.ImageSampleDrefExplicitLod)] = ImageEngine(.SampleDrefExplicitLod).op; + runtime_dispatcher[@intFromEnum(spv.SpvOp.ImageSampleDrefImplicitLod)] = ImageEngine(.SampleDrefImplicitLod).op; + runtime_dispatcher[@intFromEnum(spv.SpvOp.ImageSampleProjDrefExplicitLod)] = ImageEngine(.SampleProjDrefExplicitLod).op; + runtime_dispatcher[@intFromEnum(spv.SpvOp.ImageSampleProjDrefImplicitLod)] = ImageEngine(.SampleProjDrefImplicitLod).op; + runtime_dispatcher[@intFromEnum(spv.SpvOp.ImageSampleProjExplicitLod)] = ImageEngine(.SampleProjExplicitLod).op; + runtime_dispatcher[@intFromEnum(spv.SpvOp.ImageSampleProjImplicitLod)] = ImageEngine(.SampleProjImplicitLod).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.ImageTexelPointer)] = opImageTexelPointer; runtime_dispatcher[@intFromEnum(spv.SpvOp.ImageWrite)] = ImageEngine(.Write).op; runtime_dispatcher[@intFromEnum(spv.SpvOp.InBoundsAccessChain)] = opAccessChain; @@ -1332,6 +1350,94 @@ fn ImageEngine(comptime Op: ImageOp) type { }; } + fn imageOperandPresent(image_operands: SpvWord, mask: spv.SpvImageOperandsMask) bool { + return (image_operands & @intFromEnum(mask)) != 0; + } + + fn readImageOffset(rt: *Runtime, offset_id: SpvWord) RuntimeError!Runtime.ImageOffset { + const offset = try rt.results[offset_id].getValue(); + return .{ + .x = try readStorageCoordLane(offset, 0), + .y = readStorageCoordLane(offset, 1) catch 0, + .z = readStorageCoordLane(offset, 2) catch 0, + }; + } + + fn valueLaneCount(value: *const Value) RuntimeError!usize { + return switch (value.*) { + .Vector => |lanes| lanes.len, + .Vector2f32, .Vector2i32, .Vector2u32 => 2, + .Vector3f32, .Vector3i32, .Vector3u32 => 3, + .Vector4f32, .Vector4i32, .Vector4u32 => 4, + .Float, .Int => 1, + else => RuntimeError.InvalidValueType, + }; + } + + fn readProjectedSampleCoords(coordinate: *const Value) RuntimeError!struct { x: f32, y: f32, z: f32 } { + const lane_count = try valueLaneCount(coordinate); + if (lane_count < 2) + return RuntimeError.InvalidSpirV; + + const q_lane = lane_count - 1; + const q = try readProjectionDivisor(coordinate); + return .{ + .x = try readSampleCoordLane(coordinate, 0) / q, + .y = if (q_lane > 1) (readSampleCoordLane(coordinate, 1) catch 0.0) / q else 0.0, + .z = if (q_lane > 2) (readSampleCoordLane(coordinate, 2) catch 0.0) / q else 0.0, + }; + } + + fn readProjectionDivisor(coordinate: *const Value) RuntimeError!f32 { + const lane_count = try valueLaneCount(coordinate); + if (lane_count < 2) + return RuntimeError.InvalidSpirV; + return readSampleCoordLane(coordinate, lane_count - 1); + } + + const ParsedImageOperands = struct { + lod: ?f32 = null, + offset: Runtime.ImageOffset = .{}, + }; + + fn parseImageOperands(rt: *Runtime, image_operands: SpvWord) RuntimeError!ParsedImageOperands { + var parsed: ParsedImageOperands = .{}; + + if (imageOperandPresent(image_operands, .BiasMask)) { + _ = try rt.it.next(); + } + if (imageOperandPresent(image_operands, .LodMask)) { + parsed.lod = try readFloatLane(try rt.results[try rt.it.next()].getValue(), 0); + } + if (imageOperandPresent(image_operands, .GradMask)) { + _ = try rt.it.next(); + _ = try rt.it.next(); + } + if (imageOperandPresent(image_operands, .ConstOffsetMask) or imageOperandPresent(image_operands, .OffsetMask)) { + parsed.offset = try readImageOffset(rt, try rt.it.next()); + } + if (imageOperandPresent(image_operands, .ConstOffsetsMask)) { + _ = try rt.it.next(); + } + if (imageOperandPresent(image_operands, .SampleMask)) { + _ = try rt.it.next(); + } + if (imageOperandPresent(image_operands, .MinLodMask)) { + _ = try rt.it.next(); + } + if (imageOperandPresent(image_operands, .MakeTexelAvailableMask)) { + _ = try rt.it.next(); + } + if (imageOperandPresent(image_operands, .MakeTexelVisibleMask)) { + _ = try rt.it.next(); + } + if (imageOperandPresent(image_operands, .OffsetsMask)) { + _ = try rt.it.next(); + } + + return parsed; + } + fn writeFloatTexel(dst: *Value, texel: Runtime.Vec4(f32)) RuntimeError!void { switch (dst.*) { .Vector4f32 => |*v| v.* = .{ texel.x, texel.y, texel.z, texel.w }, @@ -1377,6 +1483,20 @@ fn ImageEngine(comptime Op: ImageOp) type { } } + fn writeFloatScalar(dst: *Value, value: f32) RuntimeError!void { + switch (dst.*) { + .Float => |*f| f.value.float32 = value, + .Vector => |lanes| { + if (lanes.len != 1) return RuntimeError.InvalidValueType; + switch (lanes[0]) { + .Float => |*f| f.value.float32 = value, + else => return RuntimeError.InvalidValueType, + } + }, + else => return RuntimeError.InvalidValueType, + } + } + fn readImage(rt: *Runtime, dst: *Value, driver_image: *anyopaque, dim: spv.SpvDim, x: i32, y: i32, z: i32) RuntimeError!void { switch (dst.*) { .Vector4f32, @@ -1405,25 +1525,25 @@ fn ImageEngine(comptime Op: ImageOp) type { } } - fn sampleImageImplicitLod(rt: *Runtime, dst: *Value, driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32) RuntimeError!void { + fn sampleImageImplicitLod(rt: *Runtime, dst: *Value, driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, offset: Runtime.ImageOffset) RuntimeError!void { switch (dst.*) { .Vector4f32, .Vector3f32, .Vector2f32, - => try writeFloatTexel(dst, try rt.image_api.sampleImageFloat4(driver_image, driver_sampler, dim, x, y, z, null)), + => try writeFloatTexel(dst, try rt.image_api.sampleImageFloat4(driver_image, driver_sampler, dim, x, y, z, null, offset)), .Vector4i32, .Vector3i32, .Vector2i32, .Vector4u32, .Vector3u32, .Vector2u32, - => try writeIntTexel(dst, try rt.image_api.sampleImageInt4(driver_image, driver_sampler, dim, x, y, z, null)), + => try writeIntTexel(dst, try rt.image_api.sampleImageInt4(driver_image, driver_sampler, dim, x, y, z, null, offset)), .Vector => |lanes| { if (lanes.len == 0) return RuntimeError.InvalidSpirV; switch (lanes[0]) { - .Float => try writeFloatTexel(dst, try rt.image_api.sampleImageFloat4(driver_image, driver_sampler, dim, x, y, z, null)), - .Int => try writeIntTexel(dst, try rt.image_api.sampleImageInt4(driver_image, driver_sampler, dim, x, y, z, null)), + .Float => try writeFloatTexel(dst, try rt.image_api.sampleImageFloat4(driver_image, driver_sampler, dim, x, y, z, null, offset)), + .Int => try writeIntTexel(dst, try rt.image_api.sampleImageInt4(driver_image, driver_sampler, dim, x, y, z, null, offset)), else => return RuntimeError.InvalidValueType, } }, @@ -1445,6 +1565,7 @@ fn ImageEngine(comptime Op: ImageOp) type { x: f32, y: f32, z: f32, + offset: Runtime.ImageOffset, ) RuntimeError!void { const coord_derivative = rt.derivatives.get(coordinate_id) orelse { rt.clearDerivative(allocator, result_id); @@ -1463,8 +1584,8 @@ fn ImageEngine(comptime Op: ImageOp) type { const coord_dy_y = readSampleCoordLane(&coord_derivative.dy, 1) catch 0.0; const coord_dy_z = readSampleCoordLane(&coord_derivative.dy, 2) catch 0.0; - try sampleImageImplicitLod(rt, &dx_sample, driver_image, driver_sampler, dim, x + coord_dx_x, y + coord_dx_y, z + coord_dx_z); - try sampleImageImplicitLod(rt, &dy_sample, driver_image, driver_sampler, dim, x + coord_dy_x, y + coord_dy_y, z + coord_dy_z); + try sampleImageImplicitLod(rt, &dx_sample, driver_image, driver_sampler, dim, x + coord_dx_x, y + coord_dx_y, z + coord_dx_z, offset); + try sampleImageImplicitLod(rt, &dy_sample, driver_image, driver_sampler, dim, x + coord_dy_x, y + coord_dy_y, z + coord_dy_z, offset); var dx = try Value.init(allocator, rt.results, result_type_word, false); defer dx.deinit(allocator); @@ -1491,25 +1612,25 @@ fn ImageEngine(comptime Op: ImageOp) type { try rt.setDerivative(allocator, result_id, &dx, &dy); } - fn sampleImageExplicitLod(rt: *Runtime, dst: *Value, driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32) RuntimeError!void { + fn sampleImageExplicitLod(rt: *Runtime, dst: *Value, driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32, offset: Runtime.ImageOffset) RuntimeError!void { switch (dst.*) { .Vector4f32, .Vector3f32, .Vector2f32, - => try writeFloatTexel(dst, try rt.image_api.sampleImageFloat4(driver_image, driver_sampler, dim, x, y, z, lod)), + => try writeFloatTexel(dst, try rt.image_api.sampleImageFloat4(driver_image, driver_sampler, dim, x, y, z, lod, offset)), .Vector4i32, .Vector3i32, .Vector2i32, .Vector4u32, .Vector3u32, .Vector2u32, - => try writeIntTexel(dst, try rt.image_api.sampleImageInt4(driver_image, driver_sampler, dim, x, y, z, lod)), + => try writeIntTexel(dst, try rt.image_api.sampleImageInt4(driver_image, driver_sampler, dim, x, y, z, lod, offset)), .Vector => |lanes| { if (lanes.len == 0) return RuntimeError.InvalidSpirV; switch (lanes[0]) { - .Float => try writeFloatTexel(dst, try rt.image_api.sampleImageFloat4(driver_image, driver_sampler, dim, x, y, z, lod)), - .Int => try writeIntTexel(dst, try rt.image_api.sampleImageInt4(driver_image, driver_sampler, dim, x, y, z, lod)), + .Float => try writeFloatTexel(dst, try rt.image_api.sampleImageFloat4(driver_image, driver_sampler, dim, x, y, z, lod, offset)), + .Int => try writeIntTexel(dst, try rt.image_api.sampleImageInt4(driver_image, driver_sampler, dim, x, y, z, lod, offset)), else => return RuntimeError.InvalidValueType, } }, @@ -1518,6 +1639,10 @@ fn ImageEngine(comptime Op: ImageOp) type { } } + fn sampleImageDref(rt: *Runtime, dst: *Value, driver_image: *anyopaque, driver_sampler: *anyopaque, dim: spv.SpvDim, x: f32, y: f32, z: f32, dref: f32, lod: ?f32, offset: Runtime.ImageOffset) RuntimeError!void { + try writeFloatScalar(dst, try rt.image_api.sampleImageDref(driver_image, driver_sampler, dim, x, y, z, dref, lod, offset)); + } + fn writeImage(rt: *Runtime, texel: *const Value, driver_image: *anyopaque, dim: spv.SpvDim, x: i32, y: i32, z: i32) RuntimeError!void { switch (texel.*) { .Float, @@ -1629,11 +1754,20 @@ fn ImageEngine(comptime Op: ImageOp) type { try readImage(rt, dst, image_operand.driver_image, image_operand.dim, x, y, z); }, - .SampleImplicitLod => { + .SampleImplicitLod, + .SampleProjImplicitLod, + => { const sampled_image_operand = try resolveSampledImage(image, rt); - const x = try readSampleCoordLane(coordinate, 0); - const y = readSampleCoordLane(coordinate, 1) catch 0; - const z = readSampleCoordLane(coordinate, 2) catch 0; + const coords = if (comptime Op == .SampleProjImplicitLod) + try readProjectedSampleCoords(coordinate) + else + .{ + .x = try readSampleCoordLane(coordinate, 0), + .y = readSampleCoordLane(coordinate, 1) catch 0, + .z = readSampleCoordLane(coordinate, 2) catch 0, + }; + const image_operands = if (word_count > 4) try rt.it.next() else 0; + const parsed_operands = try parseImageOperands(rt, image_operands); try sampleImageImplicitLod( rt, @@ -1641,9 +1775,10 @@ fn ImageEngine(comptime Op: ImageOp) type { sampled_image_operand.driver_image, sampled_image_operand.driver_sampler, sampled_image_operand.dim, - x, - y, - z, + coords.x, + coords.y, + coords.z, + parsed_operands.offset, ); try setImplicitSampleDerivative( allocator, @@ -1655,22 +1790,62 @@ fn ImageEngine(comptime Op: ImageOp) type { sampled_image_operand.driver_image, sampled_image_operand.driver_sampler, sampled_image_operand.dim, - x, - y, - z, + coords.x, + coords.y, + coords.z, + parsed_operands.offset, ); }, - .SampleExplicitLod => { + .SampleDrefImplicitLod, + .SampleProjDrefImplicitLod, + => { const sampled_image_operand = try resolveSampledImage(image, rt); - const x = try readSampleCoordLane(coordinate, 0); - const y = readSampleCoordLane(coordinate, 1) catch 0; - const z = readSampleCoordLane(coordinate, 2) catch 0; - const image_operands = if (word_count > 4) try rt.it.next() else 0; - const lod = if ((image_operands & @intFromEnum(spv.SpvImageOperandsMask.LodMask)) != 0) - try readFloatLane(try rt.results[try rt.it.next()].getValue(), 0) + const coords = if (comptime Op == .SampleProjDrefImplicitLod) + try readProjectedSampleCoords(coordinate) else - null; + .{ + .x = try readSampleCoordLane(coordinate, 0), + .y = readSampleCoordLane(coordinate, 1) catch 0, + .z = readSampleCoordLane(coordinate, 2) catch 0, + }; + const raw_dref = try readFloatLane(try rt.results[try rt.it.next()].getValue(), 0); + const dref = if (comptime Op == .SampleProjDrefImplicitLod) + raw_dref / try readProjectionDivisor(coordinate) + else + raw_dref; + const image_operands = if (word_count > 5) try rt.it.next() else 0; + const parsed_operands = try parseImageOperands(rt, image_operands); + + try sampleImageDref( + rt, + dst, + sampled_image_operand.driver_image, + sampled_image_operand.driver_sampler, + sampled_image_operand.dim, + coords.x, + coords.y, + coords.z, + dref, + null, + parsed_operands.offset, + ); + }, + + .SampleExplicitLod, + .SampleProjExplicitLod, + => { + const sampled_image_operand = try resolveSampledImage(image, rt); + const coords = if (comptime Op == .SampleProjExplicitLod) + try readProjectedSampleCoords(coordinate) + else + .{ + .x = try readSampleCoordLane(coordinate, 0), + .y = readSampleCoordLane(coordinate, 1) catch 0, + .z = readSampleCoordLane(coordinate, 2) catch 0, + }; + const image_operands = if (word_count > 4) try rt.it.next() else 0; + const parsed_operands = try parseImageOperands(rt, image_operands); try sampleImageExplicitLod( rt, @@ -1678,10 +1853,46 @@ fn ImageEngine(comptime Op: ImageOp) type { sampled_image_operand.driver_image, sampled_image_operand.driver_sampler, sampled_image_operand.dim, - x, - y, - z, - lod, + coords.x, + coords.y, + coords.z, + parsed_operands.lod, + parsed_operands.offset, + ); + }, + + .SampleDrefExplicitLod, + .SampleProjDrefExplicitLod, + => { + const sampled_image_operand = try resolveSampledImage(image, rt); + const coords = if (comptime Op == .SampleProjDrefExplicitLod) + try readProjectedSampleCoords(coordinate) + else + .{ + .x = try readSampleCoordLane(coordinate, 0), + .y = readSampleCoordLane(coordinate, 1) catch 0, + .z = readSampleCoordLane(coordinate, 2) catch 0, + }; + const raw_dref = try readFloatLane(try rt.results[try rt.it.next()].getValue(), 0); + const dref = if (comptime Op == .SampleProjDrefExplicitLod) + raw_dref / try readProjectionDivisor(coordinate) + else + raw_dref; + const image_operands = if (word_count > 5) try rt.it.next() else 0; + const parsed_operands = try parseImageOperands(rt, image_operands); + + try sampleImageDref( + rt, + dst, + sampled_image_operand.driver_image, + sampled_image_operand.driver_sampler, + sampled_image_operand.dim, + coords.x, + coords.y, + coords.z, + dref, + parsed_operands.lod, + parsed_operands.offset, ); }, @@ -1782,8 +1993,26 @@ fn opImageTexelPointer(allocator: std.mem.Allocator, word_count: SpvWord, rt: *R }; errdefer backing.deinit(allocator); - if (rt.results[result_id].variant) |*variant| { - switch (variant.*) { + const indexes = allocator.alloc(SpvWord, 0) catch return RuntimeError.OutOfMemory; + errdefer allocator.free(indexes); + + const new_value: Value = .{ .Pointer = .{ + .ptr = .{ .common = backing }, + .image_texel = .{ + .driver_image = image.driver_image, + .dim = dim, + .x = x, + .y = y, + .z = z, + }, + .uniform_backing_value = backing, + .owns_uniform_backing_value = true, + } }; + + if (rt.results[result_id].variant) |variant| { + rt.results[result_id].variant = null; + var old_variant = variant; + switch (old_variant) { .AccessChain => |*a| { try a.value.flushPtr(allocator); allocator.free(a.indexes); @@ -1793,24 +2022,12 @@ fn opImageTexelPointer(allocator: std.mem.Allocator, word_count: SpvWord, rt: *R } } - const indexes = allocator.alloc(SpvWord, 0) catch return RuntimeError.OutOfMemory; rt.results[result_id].variant = .{ .AccessChain = .{ .target = result_type, .base = image_id, .indexes = indexes, - .value = .{ .Pointer = .{ - .ptr = .{ .common = backing }, - .image_texel = .{ - .driver_image = image.driver_image, - .dim = dim, - .x = x, - .y = y, - .z = z, - }, - .uniform_backing_value = backing, - .owns_uniform_backing_value = true, - } }, + .value = new_value, }, }; } @@ -2003,7 +2220,9 @@ fn MathEngine(comptime T: PrimitiveType, comptime Op: MathOp, comptime IsAtomic: .Sub => if (comptime is_int) @subWithOverflow(op1, op2)[0] else op1 - op2, .Mul, .MatrixTimesMatrix, + .MatrixTimesScalar, .MatrixTimesVector, + .VectorTimesMatrix, => if (comptime is_int) @mulWithOverflow(op1, op2)[0] else op1 * op2, .Div => blk: { if (op2_is_zero) return RuntimeError.DivisionByZero; @@ -2044,18 +2263,123 @@ fn MathEngine(comptime T: PrimitiveType, comptime Op: MathOp, comptime IsAtomic: } } + inline fn matrixRows(matrix: *const Value) RuntimeError!usize { + const columns = switch (matrix.*) { + .Matrix => |columns| columns, + else => return RuntimeError.InvalidSpirV, + }; + if (columns.len == 0) return RuntimeError.InvalidSpirV; + return try columns[0].resolveLaneCount(); + } + + fn applyMatrixTimesVectorFloat(comptime bits: SpvWord, dst_vec: []Value, matrix: *const Value, vector: *const Value) RuntimeError!void { + const columns = switch (matrix.*) { + .Matrix => |columns| columns, + else => return RuntimeError.InvalidSpirV, + }; + const rows = try matrixRows(matrix); + if (dst_vec.len != rows or try vector.resolveLaneCount() != columns.len) return RuntimeError.InvalidSpirV; + + const FloatT = Value.getPrimitiveFieldType(.Float, bits); + for (dst_vec, 0..) |*dst_lane, row_index| { + var sum: FloatT = 0; + for (columns, 0..) |*column, column_index| { + const l = try Value.readLane(.Float, bits, column, row_index); + const r = try Value.readLane(.Float, bits, vector, column_index); + sum += l * r; + } + try Value.writeLane(.Float, bits, dst_lane, 0, sum); + } + } + + fn applyVectorTimesMatrixFloat(comptime bits: SpvWord, dst_vec: []Value, vector: *const Value, matrix: *const Value) RuntimeError!void { + const columns = switch (matrix.*) { + .Matrix => |columns| columns, + else => return RuntimeError.InvalidSpirV, + }; + if (dst_vec.len != columns.len) return RuntimeError.InvalidSpirV; + const rows = try matrixRows(matrix); + if (try vector.resolveLaneCount() != rows) return RuntimeError.InvalidSpirV; + + const FloatT = Value.getPrimitiveFieldType(.Float, bits); + for (dst_vec, columns, 0..) |*dst_lane, *column, column_index| { + _ = column_index; + var sum: FloatT = 0; + for (0..rows) |row_index| { + const l = try Value.readLane(.Float, bits, vector, row_index); + const r = try Value.readLane(.Float, bits, column, row_index); + sum += l * r; + } + try Value.writeLane(.Float, bits, dst_lane, 0, sum); + } + } + + fn applyMatrixTimesMatrixFloat(comptime bits: SpvWord, dst_matrix: []Value, lhs_matrix: *const Value, rhs_matrix: *const Value) RuntimeError!void { + const lhs_columns = switch (lhs_matrix.*) { + .Matrix => |columns| columns, + else => return RuntimeError.InvalidSpirV, + }; + const rhs_columns = switch (rhs_matrix.*) { + .Matrix => |columns| columns, + else => return RuntimeError.InvalidSpirV, + }; + if (dst_matrix.len != rhs_columns.len) return RuntimeError.InvalidSpirV; + + const rows = try matrixRows(lhs_matrix); + if (lhs_columns.len != try matrixRows(rhs_matrix)) return RuntimeError.InvalidSpirV; + + const FloatT = Value.getPrimitiveFieldType(.Float, bits); + for (dst_matrix, rhs_columns) |*dst_column, *rhs_column| { + if (try dst_column.resolveLaneCount() != rows) return RuntimeError.InvalidSpirV; + + for (0..rows) |row_index| { + var sum: FloatT = 0; + for (lhs_columns, 0..) |*lhs_column, inner_index| { + const l = try Value.readLane(.Float, bits, lhs_column, row_index); + const r = try Value.readLane(.Float, bits, rhs_column, inner_index); + sum += l * r; + } + try Value.writeLane(.Float, bits, dst_column, row_index, sum); + } + } + } + inline fn applySIMDVector(comptime ElemT: type, comptime N: usize, d: *@Vector(N, ElemT), l: @Vector(N, ElemT), r: @Vector(N, ElemT)) RuntimeError!void { d.* = try operation(@Vector(N, ElemT), l, r); } fn applySIMDVectorf32(comptime N: usize, d: *@Vector(N, f32), l: *const Value, r: *const Value) RuntimeError!void { switch (Op) { - .MatrixTimesVector => inline for (0..N) |i| { - const l_vec = l.Matrix[i].getVectorSpecialization(N, f32); - const r_vec = r.getVectorSpecialization(N, f32); - d[i] = 0; - inline for (0..N) |j| { - d[i] += l_vec[j] * r_vec[j]; + .MatrixTimesVector => { + const columns = switch (l.*) { + .Matrix => |columns| columns, + else => return RuntimeError.InvalidSpirV, + }; + if (try r.resolveLaneCount() != columns.len) return RuntimeError.InvalidSpirV; + + inline for (0..N) |row_index| { + d[row_index] = 0; + for (columns, 0..) |*column, column_index| { + d[row_index] += try Value.readLane(.Float, 32, column, row_index) * + try Value.readLane(.Float, 32, r, column_index); + } + } + }, + .VectorTimesMatrix => { + const columns = switch (r.*) { + .Matrix => |columns| columns, + else => return RuntimeError.InvalidSpirV, + }; + if (columns.len != N) return RuntimeError.InvalidSpirV; + const rows = try matrixRows(r); + if (try l.resolveLaneCount() != rows) return RuntimeError.InvalidSpirV; + + inline for (0..N) |column_index| { + d[column_index] = 0; + for (0..rows) |row_index| { + d[column_index] += try Value.readLane(.Float, 32, l, row_index) * + try Value.readLane(.Float, 32, &columns[column_index], row_index); + } } }, else => try applyDirectSIMDVectorf32(N, d, l.getVectorSpecialization(N, f32), r), @@ -2214,24 +2538,17 @@ fn MathEngine(comptime T: PrimitiveType, comptime Op: MathOp, comptime IsAtomic: fn routines(dst2: *Value, lhs2: *const Value, rhs2: *const Value, lane_bits2: SpvWord) RuntimeError!void { switch (dst2.*) { .Vector => |dst_vec| switch (Op) { - .VectorTimesScalar => switch (lane_bits2) { + .VectorTimesScalar, .MatrixTimesScalar => switch (lane_bits2) { inline 16, 32, 64 => |bits_count| try applyVectorTimesScalarFloat(bits_count, dst_vec, lhs2.Vector, rhs2), else => return RuntimeError.UnsupportedSpirV, }, - .MatrixTimesVector => for (dst_vec, lhs2.Matrix) |*d_lane, *l_mat| { - switch (lane_bits2) { - inline 8, 16, 32, 64 => |bits| { - if (comptime bits == 8 and T == .Float) return RuntimeError.UnsupportedSpirV; - const d_field = try Value.getPrimitiveField(T, bits, d_lane); - - d_field.* = 0; - - for (l_mat.Vector[0..], rhs2.Vector) |*l_lane, *r_lane| { - d_field.* += try applyScalarRaw(bits, l_lane, r_lane); - } - }, - else => return RuntimeError.UnsupportedSpirV, - } + .MatrixTimesVector => switch (lane_bits2) { + inline 16, 32, 64 => |bits_count| try applyMatrixTimesVectorFloat(bits_count, dst_vec, lhs2, rhs2), + else => return RuntimeError.UnsupportedSpirV, + }, + .VectorTimesMatrix => switch (lane_bits2) { + inline 16, 32, 64 => |bits_count| try applyVectorTimesMatrixFloat(bits_count, dst_vec, lhs2, rhs2), + else => return RuntimeError.UnsupportedSpirV, }, else => for (dst_vec, lhs2.Vector, rhs2.Vector) |*d_lane, *l_lane, *r_lane| { try applyScalar(lane_bits2, d_lane, l_lane, r_lane); @@ -2259,10 +2576,9 @@ fn MathEngine(comptime T: PrimitiveType, comptime Op: MathOp, comptime IsAtomic: .Int, .Float => try applyScalar(lane_bits, dst, lhs, rhs), .Matrix => |dst_m| switch (Op) { - .MatrixTimesMatrix => { - for (dst_m, lhs.Matrix, rhs.Matrix) |*dst_vec, *lhs_vec, *rhs_vec| { - try vectorRoutines(dst_vec, lhs_vec, rhs_vec, lane_bits); - } + .MatrixTimesMatrix => switch (lane_bits) { + inline 16, 32, 64 => |bits_count| try applyMatrixTimesMatrixFloat(bits_count, dst_m, lhs, rhs), + else => return RuntimeError.UnsupportedSpirV, }, .MatrixTimesScalar => { for (dst_m, lhs.Matrix) |*dst_vec, *lhs_vec| { @@ -2385,8 +2701,13 @@ fn setupAccessChain(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runt index.* = try rt.it.next(); } - if (rt.results[id].variant) |*variant| { - switch (variant.*) { + var new_value = try Value.initUnresolved(allocator, rt.results, var_type, false); + errdefer new_value.deinit(allocator); + + if (rt.results[id].variant) |variant| { + rt.results[id].variant = null; + var old_variant = variant; + switch (old_variant) { .AccessChain => |*a| { try a.value.flushPtr(allocator); allocator.free(a.indexes); @@ -2401,7 +2722,7 @@ fn setupAccessChain(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runt .target = var_type, .base = base_id, .indexes = indexes, - .value = try Value.initUnresolved(allocator, rt.results, var_type, false), + .value = new_value, }, }; } @@ -2587,8 +2908,10 @@ fn opAccessChain(allocator: std.mem.Allocator, word_count: SpvWord, rt: *Runtime const index_count: usize = @intCast(word_count - 3); const indexes, const free_responsability = blk: { - if (rt.results[id].variant) |*variant| { - switch (variant.*) { + if (rt.results[id].variant) |variant| { + rt.results[id].variant = null; + var old_variant = variant; + switch (old_variant) { .AccessChain => |*a| { if (a.indexes.len != index_count) return RuntimeError.InvalidSpirV; diff --git a/test/api.zig b/test/api.zig new file mode 100644 index 0000000..4c46c3b --- /dev/null +++ b/test/api.zig @@ -0,0 +1,510 @@ +const std = @import("std"); +const spv = @import("spv"); +const root = @import("root.zig"); + +const compileNzsl = root.compileNzsl; + +const ImageState = struct { + expected_sampler: *anyopaque, + sample_calls: usize = 0, + dref_calls: usize = 0, + last_x: f32 = 0, + last_y: f32 = 0, + last_z: f32 = 0, + last_dref: f32 = 0, + last_lod: ?f32 = null, + last_offset: spv.Runtime.ImageOffset = .{}, +}; + +fn readImageFloat4(_: *anyopaque, _: spv.spv.SpvDim, _: i32, _: i32, _: i32) spv.Runtime.RuntimeError!spv.Runtime.Vec4(f32) { + return spv.Runtime.RuntimeError.UnsupportedSpirV; +} + +fn readImageInt4(_: *anyopaque, _: spv.spv.SpvDim, _: i32, _: i32, _: i32) spv.Runtime.RuntimeError!spv.Runtime.Vec4(u32) { + return spv.Runtime.RuntimeError.UnsupportedSpirV; +} + +fn writeImageFloat4(_: *anyopaque, _: spv.spv.SpvDim, _: i32, _: i32, _: i32, _: spv.Runtime.Vec4(f32)) spv.Runtime.RuntimeError!void { + return spv.Runtime.RuntimeError.UnsupportedSpirV; +} + +fn writeImageInt4(_: *anyopaque, _: spv.spv.SpvDim, _: i32, _: i32, _: i32, _: spv.Runtime.Vec4(u32)) spv.Runtime.RuntimeError!void { + return spv.Runtime.RuntimeError.UnsupportedSpirV; +} + +fn sampleImageFloat4(driver_image: *anyopaque, driver_sampler: *anyopaque, _: spv.spv.SpvDim, x: f32, y: f32, z: f32, lod: ?f32, offset: spv.Runtime.ImageOffset) spv.Runtime.RuntimeError!spv.Runtime.Vec4(f32) { + const state: *ImageState = @ptrCast(@alignCast(driver_image)); + if (state.expected_sampler != driver_sampler) return spv.Runtime.RuntimeError.InvalidSpirV; + state.sample_calls += 1; + state.last_x = x; + state.last_y = y; + state.last_z = z; + state.last_lod = lod; + state.last_offset = offset; + return .{ .x = x, .y = y, .z = 9.0, .w = 1.0 }; +} + +fn sampleImageInt4(_: *anyopaque, _: *anyopaque, _: spv.spv.SpvDim, _: f32, _: f32, _: f32, _: ?f32, _: spv.Runtime.ImageOffset) spv.Runtime.RuntimeError!spv.Runtime.Vec4(u32) { + return spv.Runtime.RuntimeError.UnsupportedSpirV; +} + +fn sampleImageDref(driver_image: *anyopaque, driver_sampler: *anyopaque, _: spv.spv.SpvDim, x: f32, y: f32, z: f32, dref: f32, lod: ?f32, offset: spv.Runtime.ImageOffset) spv.Runtime.RuntimeError!f32 { + const state: *ImageState = @ptrCast(@alignCast(driver_image)); + if (state.expected_sampler != driver_sampler) return spv.Runtime.RuntimeError.InvalidSpirV; + state.dref_calls += 1; + state.last_x = x; + state.last_y = y; + state.last_z = z; + state.last_dref = dref; + state.last_lod = lod; + state.last_offset = offset; + return dref + x + y; +} + +fn queryImageSize(_: *anyopaque, _: spv.spv.SpvDim, _: bool) spv.Runtime.RuntimeError!spv.Runtime.Vec4(u32) { + return spv.Runtime.RuntimeError.UnsupportedSpirV; +} + +const image_api: spv.Runtime.ImageAPI = .{ + .readImageFloat4 = readImageFloat4, + .readImageInt4 = readImageInt4, + .writeImageFloat4 = writeImageFloat4, + .writeImageInt4 = writeImageInt4, + .sampleImageFloat4 = sampleImageFloat4, + .sampleImageInt4 = sampleImageInt4, + .sampleImageDref = sampleImageDref, + .queryImageSize = queryImageSize, +}; + +fn initModule(allocator: std.mem.Allocator, shader: []const u8) !struct { code: []const u32, module: spv.Module } { + const code = try compileNzsl(allocator, shader); + errdefer allocator.free(code); + + const module = try spv.Module.init(allocator, code, .{ + .use_simd_vectors_specializations = false, + }); + return .{ .code = code, .module = module }; +} + +test "Runtime API lifecycle" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct FragIn + \\ { + \\ [location(0)] color: vec4[f32] + \\ } + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[f32] + \\ } + \\ + \\ [entry(frag)] + \\ fn main(input: FragIn) -> FragOut + \\ { + \\ let output: FragOut; + \\ output.color = input.color; + \\ return output; + \\ } + ; + + var compiled = try initModule(allocator, shader); + defer allocator.free(compiled.code); + defer compiled.module.deinit(allocator); + + var rt = try spv.Runtime.init(allocator, &compiled.module, image_api); + defer rt.deinit(allocator); + + const input_result = try rt.getResultByLocationComponent(0, 0, .input); + const output_result = try rt.getResultByName("color"); + const output_location_result = try rt.getResultByLocationComponent(0, 0, .output); + try std.testing.expectEqual(input_result, try rt.getResultByLocation(0, .input)); + try std.testing.expectEqual(output_location_result, try rt.getResultByLocation(0, .output)); + try std.testing.expectEqual(@as(usize, 16), try rt.getInputLocationMemorySize(0)); + try std.testing.expectEqual(@as(usize, 16), try rt.getResultMemorySize(output_result)); + try std.testing.expectEqual(@as(usize, 16), try rt.getResultMemorySize(output_location_result)); + try std.testing.expectEqual(.Float, try rt.getResultPrimitiveType(output_result)); + try std.testing.expect(rt.hasResultDecoration(output_result, .Location)); + + const input = [_]f32{ 10.0, 20.0, 30.0, 40.0 }; + try rt.writeInputLocation(std.mem.asBytes(&input), 0); + + const entry = try rt.getEntryPointByName("main"); + try std.testing.expectEqual(.completed, try rt.beginEntryPoint(allocator, entry)); + + var output: [4]f32 = undefined; + try rt.readOutput(std.mem.asBytes(&output), output_location_result); + try std.testing.expectEqualSlices(f32, &input, &output); + + rt.resetInvocation(allocator); +} + +test "Module binding writes" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ [layout(std430)] + \\ struct SSBO + \\ { + \\ value: u32 + \\ } + \\ + \\ external + \\ { + \\ [set(2), binding(3)] ssbo: storage[SSBO], + \\ } + \\ + \\ [entry(compute)] + \\ [workgroup(1, 1, 1)] + \\ fn main() + \\ { + \\ ssbo.value = ssbo.value + 7; + \\ } + ; + + var compiled = try initModule(allocator, shader); + defer allocator.free(compiled.code); + defer compiled.module.deinit(allocator); + + const binding_result = compiled.module.getBindingResult(2, 3) orelse return error.TestExpectedEqual; + try std.testing.expectEqual(@as(?spv.SpvWord, null), compiled.module.getBindingResult(0, 0)); + + var rt = try spv.Runtime.init(allocator, &compiled.module, image_api); + defer rt.deinit(allocator); + + var storage: u32 = 35; + try rt.writeDescriptorSet(std.mem.asBytes(&storage), 2, 3, 0); + try std.testing.expectEqual(error.NotFound, rt.writeDescriptorSet(std.mem.asBytes(&storage), 2, 4, 0)); + + try rt.callEntryPoint(allocator, try rt.getEntryPointByName("main")); + try rt.flushDescriptorSets(allocator); + + _ = binding_result; + try std.testing.expectEqual(@as(u32, 42), storage); +} + +test "Push constants" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct Data + \\ { + \\ color: vec4[f32] + \\ } + \\ + \\ external + \\ { + \\ data: push_constant[Data] + \\ } + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[f32] + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let output: FragOut; + \\ output.color = data.color; + \\ return output; + \\ } + ; + + var compiled = try initModule(allocator, shader); + defer allocator.free(compiled.code); + defer compiled.module.deinit(allocator); + + var rt = try spv.Runtime.init(allocator, &compiled.module, image_api); + defer rt.deinit(allocator); + + const push_constants = [_]f32{ 0.125, 0.25, 0.5, 1.0 }; + try rt.populatePushConstants(std.mem.asBytes(&push_constants)); + try rt.callEntryPoint(allocator, try rt.getEntryPointByName("main")); + + var output: [4]f32 = undefined; + try rt.readOutput(std.mem.asBytes(&output), try rt.getResultByLocation(0, .output)); + try std.testing.expectEqualSlices(f32, &push_constants, &output); +} + +test "Built-in inputs and outputs" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct VertIn + \\ { + \\ [builtin(vertex_index)] vertex_index: i32 + \\ } + \\ + \\ struct VertOut + \\ { + \\ [builtin(position)] position: vec4[f32] + \\ } + \\ + \\ [entry(vert)] + \\ fn main(input: VertIn) -> VertOut + \\ { + \\ let value = f32(input.vertex_index); + \\ let output: VertOut; + \\ output.position = vec4[f32](value, value + 1.0, value + 2.0, 1.0); + \\ return output; + \\ } + ; + + var compiled = try initModule(allocator, shader); + defer allocator.free(compiled.code); + defer compiled.module.deinit(allocator); + + var rt = try spv.Runtime.init(allocator, &compiled.module, image_api); + defer rt.deinit(allocator); + + const vertex_index: i32 = 7; + try rt.writeBuiltIn(std.mem.asBytes(&vertex_index), .VertexIndex); + try rt.callEntryPoint(allocator, try rt.getEntryPointByName("main")); + + var position: [4]f32 = undefined; + try rt.readBuiltIn(std.mem.asBytes(&position), .Position); + try std.testing.expectEqualSlices(f32, &.{ 7.0, 8.0, 9.0, 1.0 }, &position); +} + +test "Integer output metadata" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct FragOut + \\ { + \\ [location(0)] value: u32 + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let output: FragOut; + \\ output.value = 0xA5A5_A5A5; + \\ return output; + \\ } + ; + + var compiled = try initModule(allocator, shader); + defer allocator.free(compiled.code); + defer compiled.module.deinit(allocator); + + var rt = try spv.Runtime.init(allocator, &compiled.module, image_api); + defer rt.deinit(allocator); + + const output_result = try rt.getResultByLocation(0, .output); + try std.testing.expectEqual(.UInt, try rt.getResultPrimitiveType(output_result)); + try std.testing.expectEqual(@as(usize, 4), try rt.getResultMemorySize(output_result)); + + try rt.callEntryPoint(allocator, try rt.getEntryPointByName("main")); + + var output: u32 = 0; + try rt.readOutput(std.mem.asBytes(&output), output_result); + try std.testing.expectEqual(@as(u32, 0xA5A5_A5A5), output); +} + +test "Runtime API error paths" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[f32] + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let output: FragOut; + \\ output.color = vec4[f32](1.0, 2.0, 3.0, 4.0); + \\ return output; + \\ } + ; + + var compiled = try initModule(allocator, shader); + defer allocator.free(compiled.code); + defer compiled.module.deinit(allocator); + + var rt = try spv.Runtime.init(allocator, &compiled.module, image_api); + defer rt.deinit(allocator); + + try std.testing.expectEqual(error.NotFound, rt.getEntryPointByName("missing")); + try std.testing.expectEqual(error.NotFound, rt.getResultByName("missing")); + try std.testing.expectEqual(error.NotFound, rt.getResultByLocation(31, .input)); + try std.testing.expectEqual(error.NotFound, rt.getInputLocationMemorySize(31)); + + try rt.callEntryPoint(allocator, try rt.getEntryPointByName("main")); + + var too_small: [3]u8 = undefined; + try std.testing.expectEqual(error.OutOfBounds, rt.readOutput(&too_small, try rt.getResultByLocation(0, .output))); +} + +test "Derivative memory buffers" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct FragIn + \\ { + \\ [location(0)] normal: vec3[f32] + \\ } + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[f32] + \\ } + \\ + \\ [entry(frag)] + \\ fn main(input: FragIn) -> FragOut + \\ { + \\ let output: FragOut; + \\ output.color = vec4[f32](input.normal.x, input.normal.y, input.normal.z, 1.0); + \\ return output; + \\ } + ; + + var compiled = try initModule(allocator, shader); + defer allocator.free(compiled.code); + defer compiled.module.deinit(allocator); + + var rt = try spv.Runtime.init(allocator, &compiled.module, image_api); + defer rt.deinit(allocator); + + const input_result = try rt.getResultByLocation(0, .input); + const dx = [_]f32{ -1.0, 2.0, -3.0 }; + const dy = [_]f32{ 4.0, -5.0, 6.0 }; + const short_dx = [_]f32{ -1.0, 2.0 }; + try std.testing.expectEqual(error.OutOfBounds, rt.setDerivativeFromMemory(allocator, input_result, std.mem.asBytes(&short_dx), std.mem.asBytes(&dy))); + + const output_result = try rt.getResultByName("color"); + try rt.setDerivativeFromMemory(allocator, input_result, std.mem.asBytes(&dx), std.mem.asBytes(&dy)); + try rt.copyDerivative(allocator, output_result, input_result); + rt.clearDerivative(allocator, input_result); + rt.clearDerivative(allocator, output_result); + + const input = [_]f32{ 1.0, 2.0, 3.0 }; + try rt.writeInput(std.mem.asBytes(&input), input_result); + try rt.callEntryPoint(allocator, try rt.getEntryPointByName("main")); + + var output: [4]f32 = undefined; + try rt.readOutput(std.mem.asBytes(&output), try rt.getResultByLocation(0, .output)); + try std.testing.expectEqualSlices(f32, &.{ 1.0, 2.0, 3.0, 1.0 }, &output); + + try rt.setDerivativeFromMemory(allocator, input_result, std.mem.asBytes(&dx), std.mem.asBytes(&dy)); + try rt.copyDerivative(allocator, output_result, input_result); + rt.clearDerivative(allocator, output_result); +} + +test "Image sampling callback" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ external + \\ { + \\ [set(0), binding(0)] tex: sampler2D[f32], + \\ } + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[f32] + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let output: FragOut; + \\ output.color = tex.Sample(vec2[f32](0.25, 0.75)); + \\ return output; + \\ } + ; + + var compiled = try initModule(allocator, shader); + defer allocator.free(compiled.code); + defer compiled.module.deinit(allocator); + + var sampler: u8 = 0; + var image_state: ImageState = .{ .expected_sampler = &sampler }; + var descriptor = [_]usize{ + @intFromPtr(&image_state), + @intFromPtr(&sampler), + }; + + var rt = try spv.Runtime.init(allocator, &compiled.module, image_api); + defer rt.deinit(allocator); + + try rt.writeDescriptorSet(std.mem.asBytes(&descriptor), 0, 0, 0); + try rt.callEntryPoint(allocator, try rt.getEntryPointByName("main")); + + var output: [4]f32 = undefined; + try rt.readOutput(std.mem.asBytes(&output), try rt.getResultByName("color")); + try std.testing.expectEqualSlices(f32, &.{ 0.25, 0.75, 9.0, 1.0 }, &output); + try std.testing.expectEqual(@as(usize, 1), image_state.sample_calls); + try std.testing.expectEqual(@as(?f32, null), image_state.last_lod); + try std.testing.expectEqual(spv.Runtime.ImageOffset{}, image_state.last_offset); +} + +test "Depth sampling dref callback" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ external + \\ { + \\ [set(0), binding(0)] tex: depth_sampler2D[f32], + \\ } + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[f32] + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let value = tex.SampleDepthComp(vec2[f32](0.25, 0.75), 0.5); + \\ let output: FragOut; + \\ output.color = vec4[f32](value, 0.0, 0.0, 1.0); + \\ return output; + \\ } + ; + + var compiled = try initModule(allocator, shader); + defer allocator.free(compiled.code); + defer compiled.module.deinit(allocator); + + var sampler: u8 = 0; + var image_state: ImageState = .{ .expected_sampler = &sampler }; + var descriptor = [_]usize{ + @intFromPtr(&image_state), + @intFromPtr(&sampler), + }; + + var rt = try spv.Runtime.init(allocator, &compiled.module, image_api); + defer rt.deinit(allocator); + + try rt.writeDescriptorSet(std.mem.asBytes(&descriptor), 0, 0, 0); + try rt.callEntryPoint(allocator, try rt.getEntryPointByName("main")); + + var output: [4]f32 = undefined; + try rt.readOutput(std.mem.asBytes(&output), try rt.getResultByName("color")); + try std.testing.expectEqualSlices(f32, &.{ 1.5, 0.0, 0.0, 1.0 }, &output); + try std.testing.expectEqual(@as(usize, 1), image_state.dref_calls); + try std.testing.expectEqual(@as(f32, 0.5), image_state.last_dref); +} diff --git a/test/arrays.zig b/test/arrays.zig index 0a7ccb7..06e14a3 100644 --- a/test/arrays.zig +++ b/test/arrays.zig @@ -33,3 +33,42 @@ test "Simple array" { }, }); } + +test "Array fold" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[f32] + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let values = array[f32](1.0, 2.0, 3.0, 4.0); + \\ let sum = 0.0; + \\ let weighted = 0.0; + \\ for i in u32(0) -> values.Size() + \\ { + \\ sum += values[i]; + \\ weighted += values[i] * f32(i + 1); + \\ } + \\ + \\ let output: FragOut; + \\ output.color = vec4[f32](sum, weighted, values[2], f32(values.Size())); + \\ return output; + \\ } + ; + const code = try compileNzsl(allocator, shader); + defer allocator.free(code); + + try case.expect(.{ + .source = code, + .expected_outputs = &.{ + std.mem.asBytes(&[_]f32{ 10.0, 30.0, 3.0, 4.0 }), + }, + }); +} diff --git a/test/bitwise.zig b/test/bitwise.zig index 076123b..c12e48a 100644 --- a/test/bitwise.zig +++ b/test/bitwise.zig @@ -157,3 +157,42 @@ test "Bitwise vectors" { } } } + +test "Bit mix" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[u32] + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let a: u32 = 0xF0F0_F0F0; + \\ let b: u32 = 0x0F0F_00FF; + \\ let c = ((a & b) << 4) | ((a ^ b) >> 8); + \\ let d = (c & 0xFFFF) ^ 0x55AA; + \\ let output: FragOut; + \\ output.color = vec4[u32](a & b, a | b, c, d); + \\ return output; + \\ } + ; + const code = try compileNzsl(allocator, shader); + defer allocator.free(code); + + try case.expect(.{ + .source = code, + .expected_outputs = &.{ + std.mem.asBytes(&[_]u32{ + 0x0000_00F0, + 0xFFFF_F0FF, + ((0xF0F0_F0F0 & 0x0F0F_00FF) << 4) | ((0xF0F0_F0F0 ^ 0x0F0F_00FF) >> 8), + ((((0xF0F0_F0F0 & 0x0F0F_00FF) << 4) | ((0xF0F0_F0F0 ^ 0x0F0F_00FF) >> 8)) & 0xFFFF) ^ 0x55AA, + }), + }, + }); +} diff --git a/test/branching.zig b/test/branching.zig index 0c00f7f..f15d923 100644 --- a/test/branching.zig +++ b/test/branching.zig @@ -103,3 +103,53 @@ test "Simple branching" { } } } + +test "Nested if" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[i32] + \\ } + \\ + \\ fn classify(value: i32) -> i32 + \\ { + \\ if (value < 0) + \\ { + \\ if ((value % 2) == 0) + \\ return -2; + \\ else + \\ return -1; + \\ } + \\ else if (value == 0) + \\ return 0; + \\ else + \\ { + \\ if ((value % 2) == 0) + \\ return 2; + \\ else + \\ return 1; + \\ } + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let output: FragOut; + \\ output.color = vec4[i32](classify(-4), classify(-3), classify(0), classify(5)); + \\ return output; + \\ } + ; + const code = try compileNzsl(allocator, shader); + defer allocator.free(code); + + try case.expect(.{ + .source = code, + .expected_outputs = &.{ + std.mem.asBytes(&[_]i32{ -2, -1, 0, 1 }), + }, + }); +} diff --git a/test/casts.zig b/test/casts.zig index 16683bd..994efca 100644 --- a/test/casts.zig +++ b/test/casts.zig @@ -116,3 +116,35 @@ test "Primitives bitcasts" { }); } } + +test "Cast chain" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[i32] + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let v = vec4[f32](1.25, 2.75, -3.25, 4.5); + \\ let a = vec4[i32](v); + \\ let output: FragOut; + \\ output.color = vec4[i32](a.x, a.y, a.z, a.w); + \\ return output; + \\ } + ; + const code = try compileNzsl(allocator, shader); + defer allocator.free(code); + + try case.expect(.{ + .source = code, + .expected_outputs = &.{ + std.mem.asBytes(&[_]i32{ 1, 2, -3, 4 }), + }, + }); +} diff --git a/test/functions.zig b/test/functions.zig index 441205b..8524c9d 100644 --- a/test/functions.zig +++ b/test/functions.zig @@ -108,3 +108,90 @@ test "Nested function calls" { }); } } + +test "Function params" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[f32] + \\ } + \\ + \\ fn affine(value: f32, scale: f32, bias: f32) -> f32 + \\ { + \\ return value * scale + bias; + \\ } + \\ + \\ fn combine(a: vec2[f32], b: vec2[f32]) -> vec4[f32] + \\ { + \\ let left = affine(a.x, b.x, b.y); + \\ let right = affine(a.y, b.y, b.x); + \\ return vec4[f32](left, right, left + right, left - right); + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let output: FragOut; + \\ output.color = combine(vec2[f32](2.0, 3.0), vec2[f32](4.0, 5.0)); + \\ return output; + \\ } + ; + const code = try compileNzsl(allocator, shader); + defer allocator.free(code); + + try case.expect(.{ + .source = code, + .expected_outputs = &.{ + std.mem.asBytes(&[_]f32{ 13.0, 19.0, 32.0, -6.0 }), + }, + }); +} + +test "Struct logic" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct Pair + \\ { + \\ a: f32, + \\ b: f32 + \\ } + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[f32] + \\ } + \\ + \\ fn eval(pair: Pair) -> vec2[f32] + \\ { + \\ return vec2[f32](pair.a + pair.b, pair.a * pair.b); + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let pair: Pair; + \\ pair.a = 3.0; + \\ pair.b = 4.0; + \\ let v = eval(pair); + \\ let output: FragOut; + \\ output.color = vec4[f32](v.x, v.y, v.y - v.x, v.x + v.y); + \\ return output; + \\ } + ; + const code = try compileNzsl(allocator, shader); + defer allocator.free(code); + + try case.expect(.{ + .source = code, + .expected_outputs = &.{ + std.mem.asBytes(&[_]f32{ 7.0, 12.0, 5.0, 19.0 }), + }, + }); +} diff --git a/test/loops.zig b/test/loops.zig index 6598165..0b52bd6 100644 --- a/test/loops.zig +++ b/test/loops.zig @@ -54,3 +54,47 @@ test "Simple while loop" { }, }); } + +test "For filter" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[u32] + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let even_sum: u32 = 0; + \\ let odd_sum: u32 = 0; + \\ let product: u32 = 1; + \\ for i in u32(1) -> u32(8) + \\ { + \\ if ((i % u32(2)) == u32(0)) + \\ { + \\ even_sum += i; + \\ product *= i; + \\ } + \\ else + \\ odd_sum += i; + \\ } + \\ + \\ let output: FragOut; + \\ output.color = vec4[u32](even_sum, odd_sum, product, even_sum + odd_sum); + \\ return output; + \\ } + ; + const code = try compileNzsl(allocator, shader); + defer allocator.free(code); + + try case.expect(.{ + .source = code, + .expected_outputs = &.{ + std.mem.asBytes(&[_]u32{ 12, 16, 48, 28 }), + }, + }); +} diff --git a/test/maths.zig b/test/maths.zig index 757f3b5..e45ac29 100644 --- a/test/maths.zig +++ b/test/maths.zig @@ -245,12 +245,22 @@ test "Maths matrices" { e.* = switch (op.key) { .Add => b + r, .Sub => b - r, - .Mul => b * r, + .Mul => 0, else => unreachable, }; } } + if (op.key == .Mul) { + for (0..L) |column_index| { + for (0..L) |row_index| { + for (0..L) |inner_index| { + expected.val[column_index][row_index] += base.val[inner_index][row_index] * ratio.val[column_index][inner_index]; + } + } + } + } + const shader = try std.fmt.allocPrint( allocator, \\ [nzsl_version("1.1")] @@ -307,11 +317,12 @@ test "Maths matrices with vectors" { const ratio: case.Vec(L, T) = .{ .val = case.random(@Vector(L, T)) }; var expected: @Vector(L, T) = undefined; - expected[0] = (base.val[0][0] * ratio.val[0]) + (base.val[0][1] * ratio.val[1]) + (base.val[0][2] * ratio.val[2]) + if (L == 4) (base.val[0][3] * ratio.val[3]) else 0.0; - expected[1] = (base.val[1][0] * ratio.val[0]) + (base.val[1][1] * ratio.val[1]) + (base.val[1][2] * ratio.val[2]) + if (L == 4) (base.val[1][3] * ratio.val[3]) else 0.0; - expected[2] = (base.val[2][0] * ratio.val[0]) + (base.val[2][1] * ratio.val[1]) + (base.val[2][2] * ratio.val[2]) + if (L == 4) (base.val[2][3] * ratio.val[3]) else 0.0; - if (L == 4) - expected[3] = (base.val[3][0] * ratio.val[0]) + (base.val[3][1] * ratio.val[1]) + (base.val[3][2] * ratio.val[2]) + (base.val[3][3] * ratio.val[3]); + expected = @splat(0); + inline for (0..L) |row_index| { + inline for (0..L) |column_index| { + expected[row_index] += base.val[column_index][row_index] * ratio.val[column_index]; + } + } const shader = try std.fmt.allocPrint( allocator, @@ -355,3 +366,36 @@ test "Maths matrices with vectors" { } } } + +test "Swizzle" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ struct FragOut + \\ { + \\ [location(0)] color: vec4[f32] + \\ } + \\ + \\ [entry(frag)] + \\ fn main() -> FragOut + \\ { + \\ let v = vec4[f32](1.0, 2.0, 3.0, 4.0); + \\ let a = v.yx; + \\ let b = v.wz; + \\ let output: FragOut; + \\ output.color = vec4[f32](a.x, a.y, b.x, b.y); + \\ return output; + \\ } + ; + const code = try compileNzsl(allocator, shader); + defer allocator.free(code); + + try case.expect(.{ + .source = code, + .expected_outputs = &.{ + std.mem.asBytes(&[_]f32{ 2.0, 1.0, 4.0, 3.0 }), + }, + }); +} diff --git a/test/root.zig b/test/root.zig index 9c3d671..b11252f 100644 --- a/test/root.zig +++ b/test/root.zig @@ -132,6 +132,7 @@ pub const case = struct { }; test { + std.testing.refAllDecls(@import("api.zig")); std.testing.refAllDecls(@import("arrays.zig")); std.testing.refAllDecls(@import("basics.zig")); std.testing.refAllDecls(@import("bitwise.zig")); diff --git a/test/ssbo.zig b/test/ssbo.zig index 7f9bae7..e7a69c4 100644 --- a/test/ssbo.zig +++ b/test/ssbo.zig @@ -3,7 +3,7 @@ const root = @import("root.zig"); const compileNzsl = root.compileNzsl; const case = root.case; -test "Simple SSBO" { +test "SSBO read" { const allocator = std.testing.allocator; const shader = \\ [nzsl_version("1.1")] @@ -58,3 +58,51 @@ test "Simple SSBO" { }, }); } + +test "SSBO write" { + const allocator = std.testing.allocator; + const shader = + \\ [nzsl_version("1.1")] + \\ module; + \\ + \\ [layout(std430)] + \\ struct SSBO + \\ { + \\ data: dyn_array[u32] + \\ } + \\ + \\ external + \\ { + \\ [set(0), binding(0)] ssbo: storage[SSBO], + \\ } + \\ + \\ [entry(compute)] + \\ [workgroup(1, 1, 1)] + \\ fn main() + \\ { + \\ for i in u32(2) -> u32(8) + \\ { + \\ ssbo.data[i] = ssbo.data[i - u32(1)] + ssbo.data[i - u32(2)]; + \\ } + \\ } + ; + const code = try compileNzsl(allocator, shader); + defer allocator.free(code); + + var ssbo = [_]u32{ 1, 1, 0, 0, 0, 0, 0, 0 }; + const expected = [_]u32{ 1, 1, 2, 3, 5, 8, 13, 21 }; + + try case.expect(.{ + .source = code, + .descriptor_sets = &.{ + &.{ + std.mem.asBytes(&ssbo), + }, + }, + .expected_descriptor_sets = &.{ + &.{ + std.mem.asBytes(&expected), + }, + }, + }); +} diff --git a/test_c/main.c b/test_c/main.c index fbdfc9f..fe8dc34 100644 --- a/test_c/main.c +++ b/test_c/main.c @@ -1,6 +1,114 @@ #include #include +#define CHECK_RESULT(expr) do { \ + SpvResult check_result = (expr); \ + if (check_result != SPV_RESULT_SUCCESS) \ + { \ + fprintf(stderr, "%s failed with %d\n", #expr, check_result); \ + return -1; \ + } \ +} while (0) + +static SpvResult ReadImageFloat4(void* driver_image, SpvDim dim, int x, int y, int z, SpvVec4f* dst) +{ + (void)driver_image; + (void)dim; + (void)x; + (void)y; + (void)z; + (void)dst; + return SPV_RESULT_UNSUPPORTED_SPIRV; +} + +static SpvResult ReadImageInt4(void* driver_image, SpvDim dim, int x, int y, int z, SpvVec4u* dst) +{ + (void)driver_image; + (void)dim; + (void)x; + (void)y; + (void)z; + (void)dst; + return SPV_RESULT_UNSUPPORTED_SPIRV; +} + +static SpvResult WriteImageFloat4(void* driver_image, SpvDim dim, int x, int y, int z, SpvVec4f src) +{ + (void)driver_image; + (void)dim; + (void)x; + (void)y; + (void)z; + (void)src; + return SPV_RESULT_UNSUPPORTED_SPIRV; +} + +static SpvResult WriteImageInt4(void* driver_image, SpvDim dim, int x, int y, int z, SpvVec4u src) +{ + (void)driver_image; + (void)dim; + (void)x; + (void)y; + (void)z; + (void)src; + return SPV_RESULT_UNSUPPORTED_SPIRV; +} + +static SpvResult SampleImageFloat4(void* driver_image, void* driver_sampler, SpvDim dim, float x, float y, float z, SpvBool has_lod, float lod, SpvImageOffset offset, SpvVec4f* dst) +{ + (void)driver_image; + (void)driver_sampler; + (void)dim; + (void)x; + (void)y; + (void)z; + (void)has_lod; + (void)lod; + (void)offset; + (void)dst; + return SPV_RESULT_UNSUPPORTED_SPIRV; +} + +static SpvResult SampleImageInt4(void* driver_image, void* driver_sampler, SpvDim dim, float x, float y, float z, SpvBool has_lod, float lod, SpvImageOffset offset, SpvVec4u* dst) +{ + (void)driver_image; + (void)driver_sampler; + (void)dim; + (void)x; + (void)y; + (void)z; + (void)has_lod; + (void)lod; + (void)offset; + (void)dst; + return SPV_RESULT_UNSUPPORTED_SPIRV; +} + +static SpvResult SampleImageDref(void* driver_image, void* driver_sampler, SpvDim dim, float x, float y, float z, float dref, SpvBool has_lod, float lod, SpvImageOffset offset, float* dst) +{ + (void)driver_image; + (void)driver_sampler; + (void)dim; + (void)x; + (void)y; + (void)z; + (void)dref; + (void)has_lod; + (void)lod; + (void)offset; + (void)dst; + return SPV_RESULT_UNSUPPORTED_SPIRV; +} + +static SpvResult QueryImageSize(void* driver_image, SpvDim dim, SpvBool arrayed, SpvVec4u* dst) +{ + (void)driver_image; + (void)dim; + (void)arrayed; + (void)dst; + return SPV_RESULT_UNSUPPORTED_SPIRV; +} + static const unsigned char shader_source[] = { 0x03, 0x02, 0x23, 0x07, 0x00, 0x00, 0x01, 0x00, 0x82, 0x10, 0x27, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x03, 0x00, @@ -53,24 +161,79 @@ int main(void) return -1; } + SpvWord binding_result = 0; + if (SpvModuleGetBindingResult(module, 0, 0, &binding_result) != SPV_RESULT_NOT_FOUND) + { + fprintf(stderr, "Unexpected binding lookup result\n"); + return -1; + } + + SpvImageAPI image_api = { + .SpvReadImageFloat4 = ReadImageFloat4, + .SpvReadImageInt4 = ReadImageInt4, + .SpvWriteImageFloat4 = WriteImageFloat4, + .SpvWriteImageInt4 = WriteImageInt4, + .SpvSampleImageFloat4 = SampleImageFloat4, + .SpvSampleImageInt4 = SampleImageInt4, + .SpvSampleImageDref = SampleImageDref, + .SpvQueryImageSize = QueryImageSize + }; + SpvRuntime runtime; - if(SpvInitRuntime(&runtime, module, (SpvImageAPI){0}) != SPV_RESULT_SUCCESS) + if(SpvInitRuntime(&runtime, module, image_api) != SPV_RESULT_SUCCESS) { fprintf(stderr, "Runtime init failed\n"); return -1; } + SpvRuntime second_runtime; + if(SpvInitRuntime(&second_runtime, module, image_api) != SPV_RESULT_SUCCESS) + { + fprintf(stderr, "Second runtime init failed\n"); + SpvDeinitRuntime(runtime); + return -1; + } + + unsigned int spec_value = 64; + SpvRuntimeSpecializationEntry spec_entry = { + .id = 0, + .offset = 0, + .size = sizeof(spec_value) + }; + CHECK_RESULT(SpvAddSpecializationInfo(runtime, spec_entry, (const SpvByte*)&spec_value, sizeof(spec_value))); + CHECK_RESULT(SpvCopySpecializationConstantsFrom(second_runtime, runtime)); + SpvWord main_entry_index; - SpvGetEntryPointByName(runtime, "main", &main_entry_index); - SpvCallEntryPoint(runtime, main_entry_index); + CHECK_RESULT(SpvGetEntryPointByName(runtime, "main", &main_entry_index)); + CHECK_RESULT(SpvCallEntryPoint(runtime, main_entry_index)); float output[4]; SpvWord output_result; - SpvGetResultByName(runtime, "color", &output_result); - SpvReadOutput(runtime, (SpvByte*)output, sizeof(output), output_result); + CHECK_RESULT(SpvGetResultByName(runtime, "color", &output_result)); + CHECK_RESULT(SpvReadOutput(runtime, (SpvByte*)output, sizeof(output), output_result)); + + SpvSize output_size = 0; + SpvPrimitiveType primitive_type; + CHECK_RESULT(SpvGetResultMemorySize(runtime, output_result, &output_size)); + CHECK_RESULT(SpvGetResultPrimitiveType(runtime, output_result, &primitive_type)); + if (output_size != sizeof(output) || primitive_type != SPV_PRIMITIVE_FLOAT || primitive_type == SPV_PRIMITIVE_SINT || primitive_type == SPV_PRIMITIVE_UINT) + { + fprintf(stderr, "Unexpected output metadata\n"); + SpvDeinitRuntime(second_runtime); + SpvDeinitRuntime(runtime); + SpvDeinitModule(module); + return -1; + } + + float dx[4] = { 1.0f, 2.0f, 3.0f, 4.0f }; + float dy[4] = { 5.0f, 6.0f, 7.0f, 8.0f }; + CHECK_RESULT(SpvSetDerivativeFromMemory(runtime, output_result, (const SpvByte*)dx, sizeof(dx), (const SpvByte*)dy, sizeof(dy))); + CHECK_RESULT(SpvCopyDerivative(runtime, output_result, output_result)); + SpvClearDerivative(runtime, output_result); printf("Output: Vec4[%f, %f, %f, %f]\n", output[0], output[1], output[2], output[3]); + SpvDeinitRuntime(second_runtime); SpvDeinitRuntime(runtime); SpvDeinitModule(module); return 0;