Godot Version
4.4
Question
I’m testing RenderingDevice.BufferGetDataAsync() for Godot c#
It works the same as BufferGetData, but it asynchronously reads the data from the GPU, calling a callback with the data when done.
It doesn’t work for me, even when the callback is set up correctly, as in the code below. This is just using the compute shader from the godot compute tutorial, i.e. doubling each item in an array of floats.
public void Test()
{
var shaderFile = GD.Load<RDShaderFile>("res://voxel_system/test.glsl");
var _rd = RenderingServer.CreateLocalRenderingDevice();
var shader_rid = _rd.ShaderCreateFromSpirV(shaderFile.GetSpirV());
var data = new float[10000];
for (int i = 0; i < data.Length; i++)
{
data[i] = i;
}
var data_bytes = MemoryMarshal.AsBytes(data.AsSpan()).ToArray();
var data_buffer_rid = _rd.StorageBufferCreate((uint)data_bytes.Length, data_bytes);
var _buffer_uniform = GenerateUniform(data_buffer_rid, RenderingDevice.UniformType.StorageBuffer, 0);
var bindings = new Godot.Collections.Array<RDUniform> {_buffer_uniform };
var uniform_set = _rd.UniformSetCreate(bindings, shader_rid, 0);
var compute_pipeline = _rd.ComputePipelineCreate(shader_rid);
var compute_list = _rd.ComputeListBegin();
_rd.ComputeListBindComputePipeline(compute_list, compute_pipeline);
_rd.ComputeListBindUniformSet(compute_list, uniform_set, 0);
_rd.ComputeListDispatch(compute_list, 100, 1, 1);
_rd.ComputeListEnd();
_rd.Submit();
var _rids_to_free = new List<Rid>();
_rids_to_free.AddRange([compute_pipeline, uniform_set, data_buffer_rid, shader_rid]);
var data_fetch_timer = new Stopwatch();
var lambda = (byte[] data) =>
{
data_fetch_timer.Stop();
GD.Print($"Data fetch took {data_fetch_timer.ElapsedMilliseconds}ms");
GD.Print("called callback");
var data_float = MemoryMarshal.Cast<byte, float>(data).ToArray();
GD.Print(data_float.Take(10).ToArray().Join(","));
foreach (var rid in _rids_to_free)
{
_rd.FreeRid(rid);
}
_rd.Free();
};
var callable = Callable.From(lambda);
var sync_timer = new Stopwatch();
sync_timer.Start();
_rd.Sync();
sync_timer.Stop();
GD.Print($"Sync took {sync_timer.ElapsedMilliseconds}ms");
GD.Print("Fetching buffer data...");
/// FETCH DATA
RenderingServer.CallOnRenderThread(Callable.From(() => {
data_fetch_timer.Start();
_rd.BufferGetDataAsync(data_buffer_rid, callable);
}));
}
The output is:
Sync took 0ms
Fetching buffer data...
… and then the callable is never called, the data is not processed.
If, on the other hand, I get the data non-asyncronously, it works:
/// FETCH DATA
RenderingServer.CallOnRenderThread(Callable.From(() => {
data_fetch_timer.Start();
var data = _rd.BufferGetData(data_buffer_rid);
data_fetch_timer.Stop();
GD.Print($"Data fetch took {data_fetch_timer.ElapsedMilliseconds}ms");
callable.Call(data);
}));
The output is:
Sync took 0ms
Fetching buffer data...
Data fetch took 1ms
called callback
0,2,4,6,8,10,12,14,16,18
Does anyone know why this might be? As far as I can tell, I’m not using BufferGetDataAsync() incorrectly. Does anyone experienced with the RenderingDevice know what I’m doing wrong?
Strangely, if I force RenderingDevice to do more work, it sometimes calls the callback. Using the pattern below with a different test, I was able to fetch the data from both:
RenderingServer.CallOnRenderThread(Callable.From(() => {
data_fetch_timer.Start();
_rd.BufferGetDataAsync(data_buffer_rid, callable);
_rd.BufferGetData(placeholder_rid); // new, placeholder buffer
}));
However, I tried this again with the setup given above, and it caused the game to stall and crash.
Edit: Calling on render thread or calling on main thread gives the same result. Also, this GDScript implementation similarly fails.
extends Node
var rd := RenderingServer.create_local_rendering_device()
func _buffer_get_data_callback(array):
print("called callback function")
var output = array.to_float32_array()
print("Callable Output: ", output)
pass
func _run_compute_shader():
print("running gdscript buffer get async")
# Create a rendering device
var shader_file := load("res://COMPUTE_ASYNC_TEST.glsl")
var shader_spirv: RDShaderSPIRV = shader_file.get_spirv()
var shader := rd.shader_create_from_spirv(shader_spirv)
# Prepare our data. We use floats in the shader, so we need 32 bit.
var input := PackedFloat32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
var input_bytes := input.to_byte_array()
# Create a storage buffer that can hold our float values.
# Each float has 4 bytes (32 bit) so 10 x 4 = 40 bytes
var buffer := rd.storage_buffer_create(input_bytes.size(), input_bytes)
# Create a uniform to assign the buffer to the rendering device
var uniform := RDUniform.new()
uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER
uniform.binding = 0 # this needs to match the "binding" in our shader file
uniform.add_id(buffer)
var uniform_set := rd.uniform_set_create([uniform], shader, 0) # the last parameter (the 0) needs to match the "set" in our shader file
# Create a compute pipeline
var pipeline := rd.compute_pipeline_create(shader)
var compute_list := rd.compute_list_begin()
rd.compute_list_bind_compute_pipeline(compute_list, pipeline)
rd.compute_list_bind_uniform_set(compute_list, uniform_set, 0)
rd.compute_list_dispatch(compute_list, 5, 1, 1)
rd.compute_list_end()
rd.submit()
rd.sync()
rd.buffer_get_data_async(buffer, _buffer_get_data_callback)
pass
func _ready():
_run_compute_shader()
pass