Skip to content

Commit dcd793a

Browse files
committed
Adding example code for compute shaders in the tutorials section
1 parent b0ebd8b commit dcd793a

File tree

2 files changed

+243
-0
lines changed

2 files changed

+243
-0
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
.. _doc_compute_shaders:
2+
3+
Using ComputeShaders
4+
====================
5+
6+
Intro
7+
-----
8+
9+
Unlike most shaders compute shaders don't have a defined functionality and thus are independent from the graphics pipeline.
10+
We can think of them as blocks of code executed on the GPU for about any purpose we want.
11+
The big benefit compared to code executed on a CPU is the high amount of parallelization that GPUs provide.
12+
13+
But because compute shaders are independent of the graphics pipeline we don't have any user defined inputs or outputs. Instead they make changes directly on the GPUs memory which we can read and write using scripts.
14+
15+
How they work
16+
-------------
17+
18+
Compute shaders can be thought of as a mass of small computers called work groups.
19+
Much like super computers they are aligned in rows and columns but also stacked on top of each other
20+
essentially forming a 3D array of them.
21+
22+
When creating a compute shader we can specify the number of work groups we wish to use.
23+
Keep in mind that these work groups are independent from each other and therefore can not depend on the result of another one.
24+
25+
In each work group we have another 3D array of threads called invocations, but unlike work groups, invocations can communicate with each other.
26+
27+
So now lets work with a compute shader to see how it really works.
28+
29+
Creating a ComputeShader
30+
------------------------
31+
32+
To begin using compute shaders, create a new text file called "compute_example.glsl" GLSL is the shader language used in Godot so if you are familiar with normal shaders the syntax below could look somehow familiar.
33+
34+
Let's take a look at this compute shader code:
35+
36+
.. code-block:: glsl
37+
38+
#[compute]
39+
#version 450
40+
41+
// Invocations in the (x, y, z) dimension
42+
layout(local_size_x = 4, local_size_y = 1, local_size_z = 1) in;
43+
44+
// A binding to the buffer we create in our script
45+
layout(set = 0, binding = 0, std430) restrict buffer MyDataBuffer {
46+
double data[];
47+
}
48+
my_data_buffer;
49+
50+
// The code we want to execute in each invocation
51+
void main() {
52+
// gl_GlobalInvocationID.x uniquely identifies this invocation within the work group
53+
my_data_buffer.data[gl_GlobalInvocationID.x] *= 2.0;
54+
}
55+
56+
This code takes an array of doubles, multiplies each element by 2 and store the results back in the buffer array.
57+
58+
To continue copy the code above in your newly created "compute_example.glsl" file.
59+
60+
Create a local RenderingDevice
61+
------------------------------
62+
63+
To interact and execute a compute shader we need a script. So go ahead and create a new script in the language of your choice and attach it to any Node in your scene.
64+
65+
Now to execute our shader we need a local :ref:`RenderingDevice <class_RenderingDevice>` which can be created using the :ref:`RenderingServer <class_RenderingServer>`:
66+
67+
.. tabs::
68+
.. code-tab:: gdscript GDScript
69+
70+
# Create a local rendering device.
71+
var rd := RenderingServer.create_local_rendering_device()
72+
73+
.. code-tab:: csharp
74+
75+
// Create a local rendering device.
76+
var rd = RenderingServer.CreateLocalRenderingDevice();
77+
78+
After that we can load the newly created shader file "compute_example.glsl" and create a pre-compiled version of it using this:
79+
80+
.. tabs::
81+
.. code-tab:: gdscript GDScript
82+
83+
# Load GLSL shader
84+
var shader_file := load("res://compute_example.glsl")
85+
var shader_spirv: RDShaderSPIRV = shader_file.get_spirv()
86+
var shader := rd.shader_create_from_spirv(shader_spirv)
87+
88+
.. code-tab:: csharp
89+
90+
// Load GLSL shader
91+
var shaderFile = GD.Load<RDShaderFile>("res://compute_example.glsl");
92+
var shaderBytecode = shaderFile.GetSpirv();
93+
var shader = rd.ShaderCreateFromSpirv(shaderBytecode);
94+
95+
96+
Provide input data
97+
------------------
98+
99+
As you might remember we want to pass an input array to our shader, multiply each element by 2 and get the results.
100+
101+
To pass values to a compute shader we need to create a buffer. We are dealing with primitives, so we will use a storage buffer for this example.
102+
A storage buffer takes an array of bytes and allows the CPU to transfer data to and from the GPU.
103+
104+
So let's initialize an array of doubles and create a storage buffer:
105+
106+
.. tabs::
107+
.. code-tab:: gdscript GDScript
108+
109+
# Prepare our data. We use doubles in the shader, so we need 64 bit.
110+
var input := PackedFloat64Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
111+
var input_bytes := input.to_byte_array()
112+
113+
# Create a storage buffer that can hold our double values.
114+
# Each double has 8 byte (64 bit) so 10 x 8 = 80 bytes
115+
var buffer := rd.storage_buffer_create(input_bytes.size(), input_bytes)
116+
117+
.. code-tab:: csharp
118+
119+
// Prepare our data. We use doubles in the shader, so we need 64 bit.
120+
var input = new double[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
121+
var inputBytes = new byte[input.Length * sizeof(double)];
122+
Buffer.BlockCopy(input, 0, inputBytes, 0, inputBytes.Length);
123+
124+
// Create a storage buffer that can hold our double values.
125+
// Each double has 8 byte (64 bit) so 10 x 8 = 80 bytes
126+
var buffer = rd.StorageBufferCreate((uint)inputBytes.Length, inputBytes);
127+
128+
With the buffer in place we need to tell the rendering device to use this buffer.
129+
To do that we will need to create a uniform (like in normal shaders) and assign it to a uniform set which we can pass to our shader later.
130+
131+
.. tabs::
132+
.. code-tab:: gdscript GDScript
133+
134+
# Create a uniform to assign the buffer to the rendering device
135+
var uniform := RDUniform.new()
136+
uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
137+
uniform.binding = 0
138+
uniform.add_id(buffer)
139+
var uniform_set := rd.uniform_set_create([uniform], shader, 0)
140+
141+
.. code-tab:: csharp
142+
143+
// Create a uniform to assign the buffer to the rendering device
144+
var uniform = new RDUniform
145+
{
146+
UniformType = RenderingDevice.UniformType.StorageBuffer,
147+
Binding = 0
148+
};
149+
uniform.AddId(buffer);
150+
var uniformSet = rd.UniformSetCreate(new Array<RDUniform> { uniform }, shader, 0);
151+
152+
153+
Defining a compute pipeline
154+
---------------------------
155+
The next step is to create a range of instructions our GPU can execute.
156+
We need a pipeline and a compute list for that.
157+
158+
The steps we need to do to compute our result are:
159+
160+
1. Create a new pipeline.
161+
2. Begin a list of instructions for our GPU to execute.
162+
3. Bind our compute list to our pipeline
163+
4. Bind our buffer uniform to our pipeline
164+
5. Execute the logic of our shader
165+
6. End the list of instructions
166+
167+
.. tabs::
168+
.. code-tab:: gdscript GDScript
169+
170+
# Create a compute pipeline
171+
var pipeline := rd.compute_pipeline_create(shader)
172+
var compute_list := rd.compute_list_begin()
173+
rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
174+
rd.compute_list_bind_uniform_set(compute_list, uniform_set, 0)
175+
rd.compute_list_dispatch(compute_list, 5, 1, 1)
176+
rd.compute_list_end()
177+
178+
.. code-tab:: csharp
179+
180+
// Create a compute pipeline
181+
var pipeline = rd.ComputePipelineCreate(shader);
182+
var computeList = rd.ComputeListBegin();
183+
rd.ComputeListBindComputePipeline(computeList, pipeline);
184+
rd.ComputeListBindUniformSet(computeList, uniformSet, 0);
185+
rd.ComputeListDispatch(computeList, xGroups: 5, yGroups: 1, zGroups: 1);
186+
rd.ComputeListEnd();
187+
188+
189+
190+
Execute a compute shader
191+
------------------------
192+
193+
After all of this we are done, kind of.
194+
We still need to execute our pipeline, everything we did so far was only definition not execution.
195+
196+
To execute our compute shader we just need to submit the pipeline to the GPU and wait for the execution to finish:
197+
198+
.. tabs::
199+
.. code-tab:: gdscript GDScript
200+
201+
# Submit to GPU and wait for sync
202+
rd.submit()
203+
rd.sync()
204+
205+
.. code-tab:: csharp
206+
207+
// Submit to GPU and wait for sync
208+
rd.Submit();
209+
rd.Sync();
210+
211+
Congrats you created and executed a compute shader. But wait, where are the results now.
212+
213+
Retrieving results
214+
-----------------
215+
216+
Let's remember the intro again, compute shaders don't have inputs and outputs, they simply change memory. This means we can retrieve the data from our buffer we created at the start of this tutorial.
217+
The shader read from an array and stored the data in the same array again so our results are already there.
218+
Let's retrieve the data and print the results to our console.
219+
220+
.. tabs::
221+
.. code-tab:: gdscript GDScript
222+
223+
# Read back the data from the buffer
224+
var output_bytes := rd.buffer_get_data(buffer)
225+
var output := output_bytes.to_float64_array()
226+
print("Input: ", input)
227+
print("Output: ", output)
228+
229+
.. code-tab:: csharp
230+
231+
// Read back the data from the buffers
232+
var outputBytes = rd.BufferGetData(outputBuffer);
233+
var output = new double[input.Length];
234+
Buffer.BlockCopy(outputBytes, 0, output, 0, outputBytes.Length);
235+
GD.Print("Input: ", input)
236+
GD.Print("Output: ", output)
237+
238+
Conclusion
239+
----------
240+
241+
Working with compute shaders could look tricky at the beginning, but they are pretty straight forward if you understood the basics.
242+
We will also cover more advanced scenarios in future tutorials

tutorials/shaders/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Shaders
1010
your_first_shader/index
1111
shader_materials
1212
visual_shaders
13+
compute_shaders
1314
screen-reading_shaders
1415
converting_glsl_to_godot_shaders
1516
shaders_style_guide

0 commit comments

Comments
 (0)