Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Shader Storage Buffer Objects (SSBO) #7516

Open
2fd5 opened this issue Aug 16, 2023 · 12 comments
Open

Add support for Shader Storage Buffer Objects (SSBO) #7516

2fd5 opened this issue Aug 16, 2023 · 12 comments

Comments

@2fd5
Copy link

2fd5 commented Aug 16, 2023

Describe the project you are working on

I'm working on a 2D game with lots of procedural generation and data-driven visual effects. Being able to efficiently pass arrays and struct data to shaders would allow more flexibility.

Describe the problem or limitation you are having in your project

Currently in Godot, the options for passing dynamic data to shaders are limited. Textures/samplers can work but require encoding data to textures. Uniforms only allow single values, not arrays or structs. This makes certain shader techniques difficult.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Implement shader storage buffer objects (SSBOs) to allow passing arrays, structs, and other custom data layouts to shaders.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

The proposed interface is:

# GDScript

var ssbo = SSBO.new()
ssbo.layout = {
  step = 8, 
  stride = 8,
  attributes = [
    {name="velocities", type=TYPE_VEC3, count=1024},
    {name="matrices", type=TYPE_MAT4, count=16}
  ] 
}

ssbo.data = # array of data matching layout

material.set_shader_param("ssbo", ssbo)
//Shader

layout(std140) buffer Velocities {
  vec3 velocities[];
}; 

layout(std140) buffer Matrices {
  mat4 matrices[];
};

void fragment() {
  vec3 vel = velocities[index];
  mat4 mat = matrices[otherIndex]; 
}

Here is an example of generating arrays for two attributes in an SSBO and assigning them to ssbo.data:

# Generate velocity data
var vel_data = []
for i in range(1024):
  vel_data.append(Vector3(randf(), randf(), randf()))

# Generate matrix data  
var mat_data = []
for i in range(16):
  mat_data.append(Matrix4()) # initialize to identity 

# Assign to SSBO  
ssbo.data = {
  "velocities": vel_data,
  "matrices": mat_data
}

The key points:

  • Create a separate array for each attribute name
  • Populate the arrays (random data here for example)
  • Assign as a dictionary to ssbo.data, with attribute name keys
    This allows you to flexibly generate arrays matching the SSBO layout, then assign the data collectively.

The shader can then index into each array by name.

Allow to access data directly in SSBO like

# Access the velocities array 
ssbo.data["velocities"][0] = Vector3(10, 0, 0)

# Access the matrices array
ssbo.data["matrices"][5] = Matrix4.IDENTITY 

# Get a matrix
var m = ssbo.data["matrices"][2]

If this enhancement will not be used often, can it be worked around with a few lines of script?

This could be worked around by encoding data in textures, but that is less flexible and optimal. Uniforms are limited to single values. SSBO provides the right level of abstraction.

Is there a reason why this should be core and not an add-on in the asset library?

Efficient data passing is a core graphics need. Textures work for some cases but are not optimal for all data. SSBO fills this gap and brings Godot up to par with other modern engines.

@Calinou
Copy link
Member

Calinou commented Aug 16, 2023

Note that SSBOs don't have an equivalent in OpenGL ES 3.0, so any shaders using those will not work when using the Compatibility rendering method.

Uniforms only allow single values, not arrays or structs.

Uniform arrays are supported since 4.0, though only with a predetermined size: #931

@2fd5
Copy link
Author

2fd5 commented Aug 16, 2023

Hello, I appreciate you pointing out the existing uniform array functionality added in Godot 4.0. I was unaware of that addition previously, so thank you for bringing it to my attention.

As you noted, shader storage buffers do not have an OpenGL ES 3.0 equivalent. I did not realize this initially. As such, any shaders utilizing SSBOs would only be compatible with the Forward+ rendering path in Godot.

With that technical context in mind, here is an updated summary of the proposal:

Add support for shader storage buffers (SSBO) in Godot's Vulkan renderer, to enable passing of dynamically-sized arrays, structs, and custom data layouts to shaders. This would facilitate more advanced data-driven effects compared to predefined uniform arrays. The limitation is shaders using SSBOs would only work with Forward+ rendering.

Please provide any additional feedback on improving this proposal while preserving the core intent! I'm open to refining it based on the helpful information you've provided regarding Godot's rendering architecture and capabilities.

@clayjohn
Copy link
Member

The current state of things right now is that Godot does support SSBO's, but only with the RD renderers and only when using RD rendering commands (i.e. they don't work in GDShaders).

That being said, I am wary of adding significant complexity to GDShaders that will be inaccessible to many users. Especially since it is unclear what benefit SSBO's would have in this context.

As a reminder, SSBOs function like Uniform Buffers except they are (potentially) slower on some hardware, and have a much larger size limit. You can also write to them from compute shaders just like images, in fact, most GPUs use the same instructions for reading/writing SSBOs as they do for reading/writing images (the performance is the same).

In light of the above, it seems like the benefit of SSBOs is that they are a little easier to write to from CPU-side than images (but more cumbersome than uniform arrays), and they don't have the size limit of uniform arrays.

To me, that niche benefit doesn't really seem worth adding the complexity. Especially since uniform buffers and images are supported on all hardware and all of our backends.

@PureAsbestos
Copy link

Is this related to #6989?

@BovineOx
Copy link

+1 ideally would be good - I am looking to port my compute shader and frag shaders from Unity and I am reliant on structured buffers, though I did have a hacky version working using textures, I suppose I could use that or I could use lots of structured buffers.

@bndbsh
Copy link

bndbsh commented Dec 26, 2023

As a reminder, SSBOs function like Uniform Buffers except they are (potentially) slower on some hardware, and have a much larger size limit. You can also write to them from compute shaders just like images, in fact, most GPUs use the same instructions for reading/writing SSBOs as they do for reading/writing images (the performance is the same).

In light of the above, it seems like the benefit of SSBOs is that they are a little easier to write to from CPU-side than images (but more cumbersome than uniform arrays), and they don't have the size limit of uniform arrays.

The main benefit for me is that they are possible to write to from other shaders, making it possible to offload more work to the GPU and share state across stages and passes. In addition to not having the same size limit, they are dynamically sized which allows for more generic shaders.

To me, that niche benefit doesn't really seem worth adding the complexity. Especially since uniform buffers and images are supported on all hardware and all of our backends.

I use shader buffers all the time and would not consider them niche, but perhaps I'm an outlier. I agree that their overall benefits are less clear when one is not in control of the full rendering pipeline as is the case with Godot, but I still think a simple interface (e.g. as flat buffers of primitives like float rather than structs as in GLSL) to shader buffers can bring a lot to the table.

@Calinou Calinou changed the title Add Shader Storage Buffer Objects (SSBO) to Godot Add support for Shader Storage Buffer Objects (SSBO) Mar 15, 2024
@Khasehemwy
Copy link

Khasehemwy commented Apr 29, 2024

Yes, we do need similar functions. The current uniform size limit is quite strict.
For example, I want to draw dynamic movements of many instances, I'm going to pass two large buffers (vec3, represent positions for every instance) to the shader, and pass a ratio (float) to interpolate the position. It will be much simpler if the shader supports Storage Buffer. Now I still don't know how to implement this conveniently (not consider particles).

@ODtian
Copy link

ODtian commented May 3, 2024

Seems storage buffer (If i recognize it correctly, it is ssbo) is already defined for compute shader and can be add to uniform set just like other uniforms. Maybe the only issue here is define those we need in gdshader. Looks good!

@lemonJumps
Copy link

I'm not gonna lie, but it would be nice to be able to use them in gdshader.
the structured data is one thing, but for me it's the ability to use these buffers as persistent storage for filtering between frames or share data across other shaders.
I'd love to use them for things like thermal or infrared night vision, or custom on screen effects.
And in combination with rendering server hooks you could tell the post processing shaders where easch object/object type is in the image. Just imagine the possibilities.
The other thing is, using purely rendering device, in my limited experience with it, essentially removes everything that rendering server does for you, including the preview in editor.
Which is fine if you're writing graphics from ground up, but it really sucks if all you want is to add few effects like hologram borders.

@ODtian
Copy link

ODtian commented May 3, 2024

I'm not gonna lie, but it would be nice to be able to use them in gdshader. the structured data is one thing, but for me it's the ability to use these buffers as persistent storage for filtering between frames or share data across other shaders. I'd love to use them for things like thermal or infrared night vision, or custom on screen effects. And in combination with rendering server hooks you could tell the post processing shaders where easch object/object type is in the image. Just imagine the possibilities. The other thing is, using purely rendering device, in my limited experience with it, essentially removes everything that rendering server does for you, including the preview in editor. Which is fine if you're writing graphics from ground up, but it really sucks if all you want is to add few effects like hologram borders.

@lemonJumps
As what I know, maybe you can try the new CompositorEffect in 4.3. Basically you can insert some render device code into pipeline every frame and get exist buffer like depth/scene color or calculate yours using compute shader, you can also bind a rd buffer to a Texture2DRD to use these buffers (without writing back to CPU) in gd shader.

I haven't tried these already, but probably it will solve your problem.

@AJ213
Copy link

AJ213 commented Oct 8, 2024

+1 The math for grabbing 8-16 bits in a NxM Texture can get complicated quickly and has caused a lot of headache in my project. In fact, I have not even figured out how I can store 3 floats worth of information in a texture and access those floats. To mitigate this complexity I have had to write unit tests to make sure I am doing the math correctly.

Here is a taste of the complexity I am talking about, I do this several times throughout my shader.

int i = (VERTEX_ID/4) * FACE_BUFFER_LEN;
ivec2 face_index = ivec2(
(i/4) % FACE_TEX_WIDTH,
(i/4) / FACE_TEX_WIDTH);

float tex_id_quad_id = texelFetch(_faceBuffer, face_index, 0)[(i) % 4];
int texture_buffer_index = int(float(floatBitsToUint(tex_id_quad_id) >> uint(16)));
int quad = int(float(floatBitsToUint(tex_id_quad_id) & uint(0xFFFF))) * QUAD_DATA_SIZE;

int block_buffer_index = int(floatBitsToUint(texelFetch(_faceBuffer, face_index, 0)[(i+1) % 4])); // note, this won't work for i+2 or an odd number of floats
float pos = texelFetch(_blockBuffer, ivec2(block_buffer_index/4, 0), 0)[(block_buffer_index % 4)];
float custom_data = texelFetch(_blockBuffer, ivec2(block_buffer_index/4, 0), 0)[((block_buffer_index + 1) % 4)];

SSBOs or even an array of floats would make such code much more simple and reliable. Unfortunately uniform arrays would not work for my use case unless I want to allocate 256kb of vram per instance of my shader (done that, cost me GiBs of vram).

@MueShen
Copy link

MueShen commented Oct 17, 2024

+1 on the idea of SSBO implementation, its would allow for greater capabilities of interaction with memory and possible variable length arrays too for universal shader for variable mediums (like point clouds with dozens of attributes to each)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests