GPU Programming · Reference

RenderingDevice cheat sheet

The compute ritual, the dispatch arithmetic, and why it printed zeros.

01The seven steps

# 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.

02The shader skeleton

#[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;
}

03Dispatch arithmetic

groups n + local_size − 1local_size  =  ceili(float(n) / LOCAL_SIZE)
Work shapeTypical local sizeDispatch
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.

04Why it printed zeros

SymptomUsual cause
Nothing runs; errors about RenderingDeviceProject is on the Compatibility renderer. Compute needs Forward+ or Mobile.
Shader fails to import or compileMissing #[compute] on line 1, or the file is not saved with a .glsl extension.
Output identical to inputForgot submit()/sync(), or read the buffer before the GPU finished.
Output all zerosset/binding in GLSL does not match RDUniform.binding and the set index in uniform_set_create().
Only the first chunk of data changedDispatched invocations instead of workgroups — divide by local_size.
Garbage floats, or fields shiftedstd430 padding mismatch. A vec3 aligns to 16 bytes; pack as vec4 or use scalars.
Works here, corrupts elsewhereMissing bounds guard. Surplus invocations wrote past the end of the buffer.
Frame rate collapsessync() 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.

05Byte-contract checklist

QuestionCheck
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.