768 lines
16 KiB
Markdown
768 lines
16 KiB
Markdown
# SPIR-V Interpreter Usage
|
|
|
|
## Concepts
|
|
|
|
A typical invocation has this lifecycle:
|
|
|
|
1. Load SPIR-V bytecode.
|
|
2. Create a `Module` from the bytecode.
|
|
3. Create one or more `Runtime` instances from the module.
|
|
4. Write inputs, built-ins, descriptors, push constants, or specialization constants.
|
|
5. Execute an entry point.
|
|
6. Read outputs or built-ins.
|
|
7. Destroy runtimes, then destroy the module.
|
|
|
|
A `Module` represents parsed SPIR-V bytecode. A `Runtime` represents one executable invocation state. Use separate runtimes when running invocations concurrently.
|
|
|
|
---
|
|
|
|
# Zig usage
|
|
|
|
## Add the dependency
|
|
|
|
With Zig `0.16.0` or newer:
|
|
|
|
```sh
|
|
zig fetch --save git+https://git.kbz8.me/kbz_8/SPIRV-Interpreter
|
|
```
|
|
|
|
Then import the package module from your `build.zig`:
|
|
|
|
```zig
|
|
const spv_dep = b.dependency("SPIRV_Interpreter", .{
|
|
.target = target,
|
|
.optimize = optimize,
|
|
});
|
|
|
|
exe.root_module.addImport("spv", spv_dep.module("spv"));
|
|
```
|
|
|
|
In your code:
|
|
|
|
```zig
|
|
const spv = @import("spv");
|
|
```
|
|
|
|
## Minimal Zig example
|
|
|
|
This example loads an embedded `.spv` file, calls the `main` entry point, and reads a `vec4<f32>` output named `color`.
|
|
|
|
```zig
|
|
const std = @import("std");
|
|
const spv = @import("spv");
|
|
|
|
const shader_source = @embedFile("shader.spv");
|
|
|
|
pub fn main() !void {
|
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
|
defer _ = gpa.deinit();
|
|
|
|
const allocator = gpa.allocator();
|
|
|
|
var module = try spv.Module.init(
|
|
allocator,
|
|
@ptrCast(@alignCast(shader_source)),
|
|
.{},
|
|
);
|
|
defer module.deinit(allocator);
|
|
|
|
// If the shader does not execute image operations, the image API is unused.
|
|
// For image load/store/sampling shaders, provide a real spv.Runtime.ImageAPI.
|
|
var rt = try spv.Runtime.init(allocator, &module, undefined);
|
|
defer rt.deinit(allocator);
|
|
|
|
const entry = try rt.getEntryPointByName("main");
|
|
const color_result = try rt.getResultByName("color");
|
|
|
|
try rt.callEntryPoint(allocator, entry);
|
|
|
|
var color: [4]f32 = undefined;
|
|
try rt.readOutput(std.mem.sliceAsBytes(color[0..]), color_result);
|
|
|
|
std.debug.print("color = {any}\n", .{color});
|
|
}
|
|
```
|
|
|
|
## Writing inputs
|
|
|
|
You can write an input by result id:
|
|
|
|
```zig
|
|
const pos_result = try rt.getResultByName("pos");
|
|
|
|
const pos = [_]f32{ 10.0, 20.0 };
|
|
try rt.writeInput(std.mem.sliceAsBytes(pos[0..]), pos_result);
|
|
```
|
|
|
|
Or by location:
|
|
|
|
```zig
|
|
const uv = [_]f32{ 0.25, 0.75 };
|
|
try rt.writeInputLocation(std.mem.sliceAsBytes(uv[0..]), 0);
|
|
```
|
|
|
|
For scalar or struct data, use `std.mem.asBytes`:
|
|
|
|
```zig
|
|
const time: f32 = 1.5;
|
|
const time_result = try rt.getResultByName("time");
|
|
|
|
try rt.writeInput(std.mem.asBytes(&time), time_result);
|
|
```
|
|
|
|
## Reading outputs
|
|
|
|
You can read an output by result id:
|
|
|
|
```zig
|
|
const color_result = try rt.getResultByName("color");
|
|
|
|
var color: [4]f32 = undefined;
|
|
try rt.readOutput(std.mem.sliceAsBytes(color[0..]), color_result);
|
|
```
|
|
|
|
Or locate an output by `Location` decoration:
|
|
|
|
```zig
|
|
const color_result = try rt.getResultByLocation(0, .output);
|
|
|
|
var color: [4]f32 = undefined;
|
|
try rt.readOutput(std.mem.sliceAsBytes(color[0..]), color_result);
|
|
```
|
|
|
|
## Push constants
|
|
|
|
Push constants are passed as raw bytes:
|
|
|
|
```zig
|
|
const PushConstants = extern struct {
|
|
time: f32,
|
|
scale: f32,
|
|
};
|
|
|
|
const push_constants = PushConstants{
|
|
.time = 1.0,
|
|
.scale = 2.0,
|
|
};
|
|
|
|
try rt.populatePushConstants(std.mem.asBytes(&push_constants));
|
|
```
|
|
|
|
## Descriptor sets
|
|
|
|
Descriptor writes are passed as raw bytes and selected by set, binding, and descriptor index:
|
|
|
|
```zig
|
|
try rt.writeDescriptorSet(
|
|
std.mem.sliceAsBytes(buffer[0..]),
|
|
0, // set
|
|
1, // binding
|
|
0, // descriptor index
|
|
);
|
|
```
|
|
|
|
For non-array descriptors, use descriptor index `0`.
|
|
|
|
After running a shader that writes through descriptor-backed memory, flush descriptor sets before reading the backing data:
|
|
|
|
```zig
|
|
try rt.callEntryPoint(allocator, entry);
|
|
try rt.flushDescriptorSets(allocator);
|
|
```
|
|
|
|
## Specialization constants
|
|
|
|
Specialization constants are selected by specialization id:
|
|
|
|
```zig
|
|
const value: u32 = 64;
|
|
|
|
try rt.addSpecializationInfo(
|
|
allocator,
|
|
.{
|
|
.id = 0,
|
|
.offset = 0,
|
|
.size = @sizeOf(u32),
|
|
},
|
|
std.mem.asBytes(&value),
|
|
);
|
|
```
|
|
|
|
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:
|
|
|
|
```zig
|
|
try rt.callEntryPoint(allocator, entry);
|
|
```
|
|
|
|
For shaders that may hit barriers, use `beginEntryPoint` and `continueEntryPoint`:
|
|
|
|
```zig
|
|
var status = try rt.beginEntryPoint(allocator, entry);
|
|
|
|
while (status == .barrier) {
|
|
// Synchronize other invocations here if needed.
|
|
status = try rt.continueEntryPoint(allocator);
|
|
}
|
|
```
|
|
|
|
## Multiple runtimes
|
|
|
|
A module can be shared by multiple runtimes. This is the preferred model for parallel execution:
|
|
|
|
```zig
|
|
var rt_a = try spv.Runtime.init(allocator, &module, undefined);
|
|
defer rt_a.deinit(allocator);
|
|
|
|
var rt_b = try spv.Runtime.init(allocator, &module, undefined);
|
|
defer rt_b.deinit(allocator);
|
|
```
|
|
|
|
Do not mutate the same runtime concurrently from multiple threads. Use one runtime per worker or invocation stream.
|
|
|
|
## Image operations
|
|
|
|
Shaders that use image load, image store, image sampling, or image-size queries need a real image API:
|
|
|
|
```zig
|
|
const image_api = spv.Runtime.ImageAPI{
|
|
.readImageFloat4 = readImageFloat4,
|
|
.readImageInt4 = readImageInt4,
|
|
.writeImageFloat4 = writeImageFloat4,
|
|
.writeImageInt4 = writeImageInt4,
|
|
.sampleImageFloat4 = sampleImageFloat4,
|
|
.sampleImageInt4 = sampleImageInt4,
|
|
.sampleImageDref = sampleImageDref,
|
|
.queryImageSize = queryImageSize,
|
|
};
|
|
|
|
var rt = try spv.Runtime.init(allocator, &module, image_api);
|
|
```
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
# C usage
|
|
|
|
## Build the C FFI
|
|
|
|
Build the static C FFI library:
|
|
|
|
```sh
|
|
zig build ffi-c --release=fast
|
|
```
|
|
|
|
Other release modes are also supported:
|
|
|
|
```sh
|
|
zig build ffi-c --release=safe
|
|
zig build ffi-c --release=small
|
|
```
|
|
|
|
To build a shared library instead of a static library:
|
|
|
|
```sh
|
|
zig build ffi-c --release=fast -Dffi-build-static=false
|
|
```
|
|
|
|
The library is installed into:
|
|
|
|
```text
|
|
zig-out/lib/
|
|
```
|
|
|
|
The public header is installed into:
|
|
|
|
```text
|
|
zig-out/include/SpirvInterpreter.h
|
|
```
|
|
|
|
The source header is also available in:
|
|
|
|
```text
|
|
ffi/SpirvInterpreter.h
|
|
```
|
|
|
|
## Minimal C example
|
|
|
|
```c
|
|
#include <stdio.h>
|
|
#include <SpirvInterpreter.h>
|
|
|
|
static const unsigned char shader_source[] = {
|
|
/* Shader bytecode */
|
|
};
|
|
|
|
int main(void)
|
|
{
|
|
SpvModule module;
|
|
SpvModuleOptions options;
|
|
options.use_simd_vectors_specializations = 1;
|
|
|
|
if(SpvInitModule(&module, (SpvWord*)shader_source, sizeof(shader_source) / 4, options) != SPV_RESULT_SUCCESS)
|
|
return -1;
|
|
|
|
SpvRuntime runtime;
|
|
/**
|
|
* A zeroed image API is only safe when the shader does not execute image
|
|
* load/store/sample/query operations.
|
|
*/
|
|
if(SpvInitRuntime(&runtime, module, (SpvImageAPI){0}) != SPV_RESULT_SUCCESS)
|
|
return -1;
|
|
|
|
SpvWord main_entry_index;
|
|
SpvGetEntryPointByName(runtime, "main", &main_entry_index);
|
|
SpvCallEntryPoint(runtime, main_entry_index);
|
|
|
|
float output[4];
|
|
SpvWord output_result;
|
|
SpvGetResultByName(runtime, "color", &output_result);
|
|
SpvReadOutput(runtime, (SpvByte*)output, sizeof(output), output_result);
|
|
|
|
printf("Output: Vec4[%f, %f, %f, %f]\n", output[0], output[1], output[2], output[3]);
|
|
|
|
SpvDeinitRuntime(runtime);
|
|
SpvDeinitModule(module);
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
## Module reflection from C
|
|
|
|
`SpvModuleGetReflectionInfos` returns shader-wide metadata collected while parsing the module:
|
|
|
|
```c
|
|
SpvModuleReflectionInfos infos = SpvModuleGetReflectionInfos(module);
|
|
|
|
if (infos.needs_derivatives)
|
|
{
|
|
/* Provide derivative data before executing derivative operations. */
|
|
}
|
|
|
|
if (infos.has_control_barriers)
|
|
{
|
|
/* Use SpvBeginEntryPoint/SpvContinueEntryPoint and synchronize invocations. */
|
|
}
|
|
|
|
if (infos.has_atomics)
|
|
{
|
|
/* Descriptor or workgroup-backed storage may be modified atomically. */
|
|
}
|
|
|
|
if (infos.early_fragment_tests)
|
|
{
|
|
/* Fragment shader requested early fragment tests. */
|
|
}
|
|
```
|
|
|
|
Compute and geometry execution metadata is also available:
|
|
|
|
```c
|
|
SpvWord local_x = infos.local_size_x;
|
|
SpvWord geometry_output_count = infos.geometry_output_count;
|
|
```
|
|
|
|
## Writing inputs from C
|
|
|
|
Write by result id:
|
|
|
|
```c
|
|
SpvWord pos_result = 0;
|
|
|
|
if (SpvGetResultByName(runtime, "pos", &pos_result) != SPV_RESULT_SUCCESS)
|
|
return 1;
|
|
|
|
float pos[2] = {10.0f, 20.0f};
|
|
|
|
if (SpvWriteInput(runtime, (const SpvByte*)pos, sizeof(pos), pos_result) != SPV_RESULT_SUCCESS)
|
|
return 1;
|
|
```
|
|
|
|
Write by input location:
|
|
|
|
```c
|
|
float uv[2] = {0.25f, 0.75f};
|
|
|
|
if (SpvWriteInputLocation(runtime, (const SpvByte*)uv, sizeof(uv), 0) != SPV_RESULT_SUCCESS)
|
|
return 1;
|
|
```
|
|
|
|
## Reading outputs from C
|
|
|
|
Read by result id:
|
|
|
|
```c
|
|
SpvWord color_result = 0;
|
|
float color[4] = {0};
|
|
|
|
SpvGetResultByName(runtime, "color", &color_result);
|
|
SpvReadOutput(runtime, (SpvByte*)color, sizeof(color), color_result);
|
|
```
|
|
|
|
Read by output location:
|
|
|
|
```c
|
|
SpvWord color_result = 0;
|
|
|
|
SpvGetResultByLocation(runtime, 0, SPV_LOCATION_OUTPUT, &color_result);
|
|
```
|
|
|
|
For component-qualified locations:
|
|
|
|
```c
|
|
SpvWord result = 0;
|
|
|
|
SpvGetResultByLocationComponent(runtime, 0, 1, SPV_LOCATION_OUTPUT, &result);
|
|
```
|
|
|
|
For built-ins:
|
|
|
|
```c
|
|
SpvWord workgroup_id_result = 0;
|
|
|
|
if (SpvGetBuiltinResult(runtime, SpvBuiltInWorkgroupId, &workgroup_id_result) == SPV_RESULT_SUCCESS)
|
|
{
|
|
unsigned int workgroup_id[3] = {0};
|
|
SpvReadBuiltIn(runtime, (SpvByte*)workgroup_id, sizeof(workgroup_id), SpvBuiltInWorkgroupId);
|
|
}
|
|
```
|
|
|
|
If a result's layout depends on specialization constants or runtime-array sizes, refresh it before querying or reading by result id:
|
|
|
|
```c
|
|
SpvRefreshResultValueLayout(runtime, result);
|
|
```
|
|
|
|
## Push constants from C
|
|
|
|
```c
|
|
typedef struct PushConstants
|
|
{
|
|
float time;
|
|
float scale;
|
|
} PushConstants;
|
|
|
|
PushConstants push_constants = {
|
|
.time = 1.0f,
|
|
.scale = 2.0f,
|
|
};
|
|
|
|
SpvPopulatePushConstants(runtime, (const SpvByte*)&push_constants, sizeof(push_constants));
|
|
```
|
|
|
|
## Descriptor sets from C
|
|
|
|
```c
|
|
SpvWriteDescriptorSet(runtime, (const SpvByte*)buffer, buffer_size,
|
|
0, /* set */
|
|
1, /* binding */
|
|
0 /* descriptor index */
|
|
);
|
|
```
|
|
|
|
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
|
|
SpvCallEntryPoint(runtime, entry);
|
|
SpvFlushDescriptorSets(runtime);
|
|
```
|
|
|
|
## Specialization constants from C
|
|
|
|
```c
|
|
unsigned int value = 64;
|
|
|
|
SpvRuntimeSpecializationEntry entry = {
|
|
.id = 0,
|
|
.offset = 0,
|
|
.size = sizeof(value),
|
|
};
|
|
|
|
SpvAddSpecializationInfo(runtime, entry, (const SpvByte*)&value, sizeof(value));
|
|
```
|
|
|
|
Add specialization constants before calling the entry point.
|
|
|
|
If specialization constants affect array sizes, workgroup size, or other layout-dependent metadata, apply the specialization layout after adding the values:
|
|
|
|
```c
|
|
SpvApplySpecializationLayout(runtime);
|
|
```
|
|
|
|
For a fresh invocation layout, use:
|
|
|
|
```c
|
|
SpvApplySpecializationInvocationLayout(runtime);
|
|
```
|
|
|
|
To duplicate specialization constants from another runtime:
|
|
|
|
```c
|
|
SpvCopySpecializationConstantsFrom(runtime, source_runtime);
|
|
```
|
|
|
|
## Entry points from C
|
|
|
|
Entry points can be looked up by name:
|
|
|
|
```c
|
|
SpvWord entry = 0;
|
|
SpvGetEntryPointByName(runtime, "main", &entry);
|
|
```
|
|
|
|
When a module has multiple entry points with the same name, include the execution model:
|
|
|
|
```c
|
|
SpvGetEntryPointByNameAndExecutionModel(
|
|
runtime,
|
|
"main",
|
|
SpvExecutionModelGLCompute,
|
|
&entry);
|
|
```
|
|
|
|
Selecting an entry point filters interface lookups such as input and output locations to that entry point:
|
|
|
|
```c
|
|
SpvSelectEntryPoint(runtime, entry);
|
|
```
|
|
|
|
## 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:
|
|
|
|
```c
|
|
SpvCallEntryPoint(runtime, entry);
|
|
```
|
|
|
|
For shaders that may hit barriers:
|
|
|
|
```c
|
|
SpvEntryPointStatus status = SPV_ENTRY_POINT_COMPLETED;
|
|
|
|
SpvBeginEntryPoint(runtime, entry, &status);
|
|
|
|
while (status == SPV_ENTRY_POINT_BARRIER)
|
|
{
|
|
/* Synchronize other invocations here if needed. */
|
|
SpvContinueEntryPoint(runtime, &status);
|
|
}
|
|
```
|
|
|
|
## Workgroup data from C
|
|
|
|
Compute shaders may declare a static workgroup size. Query it with:
|
|
|
|
```c
|
|
SpvWorkgroupSize size;
|
|
|
|
if (SpvGetWorkgroupSize(runtime, &size) == SPV_RESULT_SUCCESS && size.has_size)
|
|
{
|
|
printf("workgroup size = %lu, %lu, %lu\n", size.x, size.y, size.z);
|
|
}
|
|
```
|
|
|
|
Workgroup storage is allocated separately so multiple runtime invocations can bind the same shared memory during barrier-based execution:
|
|
|
|
```c
|
|
SpvWorkgroupMemory memory;
|
|
|
|
if (SpvCreateWorkgroupMemory(runtime, &memory) != SPV_RESULT_SUCCESS)
|
|
return 1;
|
|
|
|
SpvSize count = SpvGetWorkgroupMemoryCount(memory);
|
|
|
|
for (SpvSize i = 0; i < count; ++i)
|
|
{
|
|
SpvWorkgroupMemoryItem item;
|
|
|
|
if (SpvGetWorkgroupMemoryItem(memory, i, &item) == SPV_RESULT_SUCCESS)
|
|
{
|
|
/* item.result is the Workgroup variable result id.
|
|
item.data points to item.size bytes owned by memory. */
|
|
}
|
|
}
|
|
|
|
SpvBindWorkgroupMemory(runtime, memory);
|
|
|
|
/* Run one or more invocations that share this workgroup memory. */
|
|
|
|
SpvDestroyWorkgroupMemory(runtime, memory);
|
|
```
|
|
|
|
## Image API from C
|
|
|
|
Shaders that execute image operations must provide callbacks in `SpvImageAPI`.
|
|
|
|
```c
|
|
static SpvResult ReadImageFloat4(
|
|
SpvReadImageInfo info,
|
|
SpvVec4f* dst)
|
|
{
|
|
(void)info.driver_image;
|
|
(void)info.dim;
|
|
(void)info.x;
|
|
(void)info.y;
|
|
(void)info.z;
|
|
(void)info.has_lod;
|
|
(void)info.lod;
|
|
|
|
dst->x = 0.0f;
|
|
dst->y = 0.0f;
|
|
dst->z = 0.0f;
|
|
dst->w = 1.0f;
|
|
|
|
return SPV_RESULT_SUCCESS;
|
|
}
|
|
```
|
|
|
|
Sampling callbacks include an explicit-LOD flag/value and an offset:
|
|
|
|
```c
|
|
static SpvResult SampleImageFloat4(
|
|
SpvSampleImageInfo info,
|
|
SpvVec4f* dst)
|
|
{
|
|
(void)info.driver_image;
|
|
(void)info.driver_sampler;
|
|
(void)info.dim;
|
|
(void)info.has_lod;
|
|
(void)info.lod;
|
|
(void)info.offset;
|
|
|
|
dst->x = (float)info.x;
|
|
dst->y = (float)info.y;
|
|
dst->z = (float)info.z;
|
|
dst->w = 1.0f;
|
|
return SPV_RESULT_SUCCESS;
|
|
}
|
|
```
|
|
|
|
Depth-comparison samplers add `dref` and write one `float`:
|
|
|
|
```c
|
|
static SpvResult SampleImageDref(
|
|
SpvSampleImageInfo info,
|
|
float dref,
|
|
float* dst)
|
|
{
|
|
(void)info.driver_image;
|
|
(void)info.driver_sampler;
|
|
(void)info.dim;
|
|
(void)info.x;
|
|
(void)info.y;
|
|
(void)info.z;
|
|
(void)info.has_lod;
|
|
(void)info.lod;
|
|
(void)info.offset;
|
|
|
|
*dst = dref;
|
|
return SPV_RESULT_SUCCESS;
|
|
}
|
|
```
|
|
|
|
The image API table contains these callbacks:
|
|
|
|
```c
|
|
SpvImageAPI image_api = {
|
|
.SpvReadImageFloat4 = ReadImageFloat4,
|
|
.SpvReadImageInt4 = ReadImageInt4,
|
|
.SpvWriteImageFloat4 = WriteImageFloat4,
|
|
.SpvWriteImageInt4 = WriteImageInt4,
|
|
.SpvSampleImageFloat4 = SampleImageFloat4,
|
|
.SpvSampleImageInt4 = SampleImageInt4,
|
|
.SpvSampleImageDref = SampleImageDref,
|
|
.SpvQueryImageSize = QueryImageSize,
|
|
.SpvQueryImageLevels = QueryImageLevels,
|
|
.SpvQueryImageSamples = QueryImageSamples,
|
|
.SpvQueryImageLod = QueryImageLod,
|
|
};
|
|
```
|
|
|
|
Pass the table when creating the runtime:
|
|
|
|
```c
|
|
SpvInitRuntime(&runtime, module, image_api);
|
|
```
|
|
|
|
## Cleanup order
|
|
|
|
Always destroy runtimes before destroying the module they were created from:
|
|
|
|
```c
|
|
SpvDeinitRuntime(runtime);
|
|
SpvDeinitModule(module);
|
|
```
|