Learning

GPU Programming · Reference

RenderingDevice cheat sheet

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

01 Choose the device

Question Local RenderingDevice Global RenderingDevice
Get it create_local_rendering_device() get_rendering_device()
Resources Private to that device Shareable with the renderer
Execution context Your script controls it Use call_on_render_thread()
Submission Call submit() Godot submits the render frame
Waiting sync(), only when necessary Use asynchronous readback
# Persistent global-device loop
RenderingServer.call_on_render_thread(_gpu_step.bind(dt))

func _gpu_step(dt: float) -> void:
    var rd := RenderingServer.get_rendering_device()
    # Bind persistent RIDs, push this tick's dt, record dispatch.
    # No submit() or sync(): both are local-device-only.

02 The seven local-device 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 input := PackedFloat32Array([...])
var element_count := input.size()
var bytes := input.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
const LOCAL_SIZE := 64
var x_groups := ceili(float(element_count) / LOCAL_SIZE)
var params := PackedByteArray()
params.resize(16)
params.encode_u32(0, element_count)       # logical elements, not bytes/components

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_set_push_constant(list, params, params.size())
rd.compute_list_dispatch(list, x_groups, 1, 1)
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.

03 The 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;

layout(push_constant, std430) uniform Params {
    uvec4 values;                         // x = logical element count
} params;

void main() {
    uint id = gl_GlobalInvocationID.x;
    uint element_count = params.values.x;
    if (id >= element_count) { return; }          // always
    buf.data[id] *= 2.0;
}

data.length() is the bound runtime array’s physical length by specification. Pass the logical count explicitly: a fixed-capacity simulation buffer can use a shorter logical range than its allocation. The count’s unit must match data[id]—records, not bytes or scalar components.

04 Dispatch arithmetic

groups n + local_size − 1local_size  =  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.

05 Why it printed zeros

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 On a local device, forgot submit(); on either device, read 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.
Too many or too few records changed The pushed count is in bytes/components instead of storage-buffer array elements, or differs from the count used to dispatch.
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.

06 Byte-contract checklist

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.