GPU Programming · Reference
The compute ritual, the dispatch arithmetic, and why it printed zeros.
# 1 device
var rd := RenderingServer.create_local_rendering_device()
# 2 shader: .glsl file -> SPIR-V -> RID
var shader_file: RDShaderFile = load("res://my_shader.glsl")
var shader := rd.shader_create_from_spirv(shader_file.get_spirv())
# 3 memory: bytes in, RID out
var bytes := PackedFloat32Array([...]).to_byte_array()
var buffer := rd.storage_buffer_create(bytes.size(), bytes)
# 4 binding: must mirror layout(set = 0, binding = 0) in the shader
var u := RDUniform.new()
u.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
u.binding = 0
u.add_id(buffer)
var uniform_set := rd.uniform_set_create([u], shader, 0) # last arg = set index
# 5 pipeline: create once, reuse forever
var pipeline := rd.compute_pipeline_create(shader)
# 6 record + dispatch: counts are WORKGROUPS, not invocations
var list := rd.compute_list_begin()
rd.compute_list_bind_compute_pipeline(list, pipeline)
rd.compute_list_bind_uniform_set(list, uniform_set, 0)
rd.compute_list_dispatch(list, x_groups, y_groups, z_groups)
rd.compute_list_end()
# 7 execute and (only if you must) read back
rd.submit()
rd.sync() # blocks the CPU — avoid in the frame loop
var out := rd.buffer_get_data(buffer).to_float32_array()
# cleanup: RIDs are not reference-counted
rd.free_rid(uniform_set); rd.free_rid(pipeline)
rd.free_rid(buffer); rd.free_rid(shader)
Several of these methods take extra optional arguments (buffer offsets, specialization constants, texture views) that change between 4.x point releases. When in doubt, check the class reference for your exact Godot version rather than trusting a tutorial.
#[compute] // mandatory, line 1, Godot-specific
#version 450
layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in;
layout(set = 0, binding = 0, std430) restrict buffer DataBuffer {
float data[]; // runtime-sized: must be the last member
} buf;
void main() {
uint id = gl_GlobalInvocationID.x;
if (id >= buf.data.length()) { return; } // always
buf.data[id] *= 2.0;
}
ceili(float(n) / LOCAL_SIZE)
| Work shape | Typical local size | Dispatch |
|---|---|---|
| 1D array of n elements | (64, 1, 1) | ceil(n/64), 1, 1 |
| 2D image, w × h | (8, 8, 1) | ceil(w/8), ceil(h/8), 1 |
| 3D volume, w×h×d | (4, 4, 4) | ceil(w/4), ceil(h/4), ceil(d/4) |
Local size should be a multiple of the hardware bundle width — 32 on NVIDIA, 64 on AMD — so
64 total invocations per group is the safe floor
(NVIDIA CUDA C++
Best Practices Guide). Note that 8 × 8 = 64 and 4 × 4 × 4 = 64:
the multiple applies to the product, not to each axis.
| Symptom | Usual cause |
|---|---|
| Nothing runs; errors about RenderingDevice | Project is on the Compatibility renderer. Compute needs Forward+ or Mobile. |
| Shader fails to import or compile | Missing #[compute] on line 1, or the file is not saved with a .glsl extension. |
| Output identical to input | Forgot submit()/sync(), or read the buffer before the GPU finished. |
| Output all zeros | set/binding in GLSL does not match RDUniform.binding and the set index in uniform_set_create(). |
| Only the first chunk of data changed | Dispatched invocations instead of workgroups — divide by local_size. |
| Garbage floats, or fields shifted | std430 padding mismatch. A vec3 aligns to 16 bytes; pack as vec4 or use scalars. |
| Works here, corrupts elsewhere | Missing bounds guard. Surplus invocations wrote past the end of the buffer. |
| Frame rate collapses | sync() every frame, or a readback every frame. Keep data resident on the GPU. |
| Driver resets mid-run (Windows) | A single dispatch ran too long and hit TDR. Split the work across several dispatches. |
| Question | Check |
|---|---|
| Where does record i begin? | i * STRIDE on the CPU; the same array stride under GLSL std430. |
| Do types have the same width? | Use encode_float/encode_u32 for GLSL 32-bit float/uint. |
| Does each field begin aligned? | Scalar: 4 bytes; vec2: 8; vec3/vec4: 16 under std430. |
| Is tail padding explicit? | Make the stride visible with padding fields or a documented constant on both sides. |
| Why is body zero right but body one wrong? | The first record starts at zero under either interpretation; suspect a stride mismatch first. |