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