Godot Version
v4.6.stable.mono.official [89cea1439]
Question
For context, I’m trying to create a simple full-screen blur effect using Godot’s compositor feature. I’ve been following the documentation (see here) and everything is now set up as instructed.
However, I’m getting an error saying that the TEXTURE_USAGE_STORAGE_BIT is not set on the color image (RenderSceneBuffersRD.GetColorLayer()) – a texture that is initialized by the engine, not me.
The errors logged when I run the game.
Stack trace of the logged error concerning the TEXTURE_USAGE_STORAGE_BIT.
Given that I’m using C#, I tried to make the same basic CompositorEffect in GDScript which actually worked without issue. This leaves me wondering whether the C# build of the engine is not setting the correct usage flags on the textures inside the RenderSceneBuffersRD retrieved from the RenderData passed to the CompositorEffect’s _RenderCallback method.
It’s worth nothing that the error does not occur when simply viewing the effect in the preview image located in the Camera3D’s inspector. It only happens when I run the game. In other words, it works in the editor but not in the game.
I haven’t been able to find a post anywhere else describing this specific problem with the color texture – only the depth texture:
- Compositor Effect Throwing Error When Getting Depth Texture RID
- Problem with Compositor shader texture loading
I feel like I’ve triple-checked any differences there may exist between the C# script and the GDScript script. Am I missing something here?
C# CompositorEffect
using Godot;
using System;
[Tool, GlobalClass]
public partial class CompositorEffectSplitScreen : CompositorEffect
{
const EffectCallbackTypeEnum EFFECT_TYPE = EffectCallbackTypeEnum.PostTransparent;
const int MAX_SPLIT_SCREENS = 4;
const string SHADER_FILE_PATH = "res://shaders/glsl_shaders/zone_blur.glsl";
RDShaderFile shaderFile = ResourceLoader.Load<RDShaderFile>(SHADER_FILE_PATH);
const string contextName = "blur";
const string blurTextureName = "blurTex";
RenderingDevice rd;
Rid shader;
Rid pipelineCompute;
Rid pipelineRender;
// Mutex mutex = new Mutex();
[Export] private bool useHalfRes = false;
public CompositorEffectSplitScreen()
{
EffectCallbackType = EFFECT_TYPE;
// Get the global rendering device
rd = RenderingServer.GetRenderingDevice();
// Initialize compute shader on the rendering thread
RenderingServer.CallOnRenderThread(Callable.From(InitComputeShader));
}
private string DebugTime()
{
float ms = Time.GetTicksMsec();
TimeSpan ts = TimeSpan.FromMilliseconds(ms);
return ts.ToString(@"hh\:mm\:ss\.fffffff");
}
private bool InitComputeShader()
{
if (!rd.IsValid())
return false;
GD.Print($"{DebugTime()} - Shader file loaded: {(shaderFile.IsValid() ? "true" : "false")}");
// Compile shader from shader file
var shaderSpirv = shaderFile.GetSpirV();
shader = rd.ShaderCreateFromSpirV(shaderSpirv);
GD.Print($"{DebugTime()} - Compiled shader is valid: {(shader.IsValid ? "true" : "false")}");
if (shader.IsValid)
pipelineCompute = rd.ComputePipelineCreate(shader);
GD.Print($"{DebugTime()} - Compute pipeline is valid: {(pipelineCompute.IsValid ? "true" : "false")}");
return true;
}
// private void InitRenderShader()
// {
// var rasterState = new RDPipelineRasterizationState();
// var sampleState = new RDPipelineMultisampleState();
// var stencilState = new RDPipelineDepthStencilState();
// var colorState = new RDPipelineColorBlendState();
// pipelineRender = rd.RenderPipelineCreate(shader, default, default, RenderingDevice.RenderPrimitive.Triangles,
// rasterState, sampleState, stencilState, colorState, RenderingDevice.PipelineDynamicStateFlags.BlendConstants);
// }
public override void _RenderCallback(int effectCallbackType, RenderData renderData)
{
if (rd.IsValid() && effectCallbackType == (int)EFFECT_TYPE)
{
// Retrieve the scene buffer data from the render data
var buffers = renderData.GetRenderSceneBuffers() as RenderSceneBuffersRD;
var data = renderData.GetRenderSceneData() as RenderSceneDataRD;
var renderSize = buffers.GetInternalSize();
var effectSize = renderSize;
if (useHalfRes)
effectSize = new Vector2I(Mathf.RoundToInt(effectSize.X * 0.5f), Mathf.RoundToInt(effectSize.Y * 0.5f));
// Don't render NOTHING
if (effectSize.X == 0 && effectSize.Y == 0)
return;
// ===================== CREATE / REFRESH TEXTURES =====================
if (buffers.HasTexture(contextName, blurTextureName))
{
// If the size for the texture no longer matches the target size, clear all textures within the context
// NOTE: What is a context?
var tf = buffers.GetTextureFormat(contextName, blurTextureName);
if (tf.Width != effectSize.X || tf.Height != effectSize.Y)
{
buffers.ClearContext(contextName);
}
}
if (!buffers.HasTexture(contextName, blurTextureName))
{
uint usageBits = (uint)RenderingDevice.TextureUsageBits.SamplingBit | (uint)RenderingDevice.TextureUsageBits.StorageBit;
buffers.CreateTexture(contextName, blurTextureName, RenderingDevice.DataFormat.R16Unorm, usageBits, RenderingDevice.TextureSamples.Samples1, effectSize, 1, 1, true, false);
}
// =====================================================================
// Compute shader settings (NOTE: u-literal implies uint type)
uint xGroups = (uint)(effectSize.X - 1) / 8u + 1u;
uint yGroups = (uint)(effectSize.Y - 1) / 8u + 1u;
uint zGroups = 1u;
// Push constant
float[] pushConstant = new float[4];
pushConstant[0] = renderSize.X;
pushConstant[1] = renderSize.Y;
pushConstant[2] = effectSize.X;
pushConstant[3] = effectSize.Y;
// Docs: [...] we (may) have to pad this array with a 16 byte alignment.
// the length of our array needs to be a multiple of 4.
int pushByteSize = pushConstant.Length * sizeof(float);
byte[] pushBytes = new byte[pushByteSize];
Buffer.BlockCopy(pushConstant, 0, pushBytes, 0, pushBytes.Length);
// NOTE: A float array is created to store the floating-point numbers.
// However, because the compute list (further down) expects a byte array,
// a byte array is produced from the float array via Buffer.BlockCopy.
// Loop through views (for cases when stereo rendering is enabled)
// NOTE: This will never be the case for this game. It's just a formality
var viewCount = buffers.GetViewCount();
for (uint view = 0; view < viewCount; view++)
{
// Prime buffers for uniform set
Rid colorBuffer = buffers.GetColorLayer(view);
Rid blurBuffer = buffers.GetTextureSlice(contextName, blurTextureName, view, 0, 1, 1);
// var samplerState = new RDSamplerState();
// samplerState.MinFilter = RenderingDevice.SamplerFilter.Linear;
// samplerState.MagFilter = RenderingDevice.SamplerFilter.Linear;
// var sampler = rd.SamplerCreate(samplerState);
// DOCS: Note the use of our UniformSetCacheRD cache which ensures we can check for our
// uniform set each frame. As our color buffer can change from frame to frame and our uniform
// cache will automatically clean up uniform sets when buffers are freed, this is the safe way
// to ensure we do not leak memory or use an outdated set.
RDUniform uniformColor = new RDUniform();
uniformColor.UniformType = RenderingDevice.UniformType.Image;
uniformColor.Binding = 0;
uniformColor.AddId(colorBuffer);
var uniformColorSet = UniformSetCacheRD.GetCache(shader, 0, [uniformColor]);
// RDUniform uniformBlur = new RDUniform();
// uniformBlur.UniformType = RenderingDevice.UniformType.Image;
// uniformBlur.Binding = 1;
// uniformBlur.AddId(blurBuffer);
// var uniformBlurSet = UniformSetCacheRD.GetCache(shader, 1, [uniformBlur]);
var computeList = rd.ComputeListBegin();
rd.ComputeListBindComputePipeline(computeList, pipelineCompute);
rd.ComputeListBindUniformSet(computeList, uniformColorSet, 0);
// rd.ComputeListBindUniformSet(computeList, uniformBlurSet, 1);
rd.ComputeListSetPushConstant(computeList, pushBytes, (uint)pushByteSize);
rd.ComputeListDispatch(computeList, xGroups, yGroups, zGroups);
rd.ComputeListEnd();
}
}
}
public override void _Notification(int what)
{
if (what == (int)NotificationPredelete)
{
if (shader.IsValid)
{
rd.FreeRid(shader);
}
}
}
}
GDScript CompositorEffect
@tool
class_name CompositorEffectTesting
extends CompositorEffect
var rd: RenderingDevice
var shader: RID
var pipeline: RID
func _init() -> void:
effect_callback_type = EFFECT_CALLBACK_TYPE_POST_TRANSPARENT
rd = RenderingServer.get_rendering_device()
RenderingServer.call_on_render_thread(_initialize_compute)
# System notifications, we want to react on the notification that
# alerts us we are about to be destroyed.
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
if shader.is_valid():
# Freeing our shader will also free any dependents such as the pipeline!
rd.free_rid(shader)
#region Code in this region runs on the rendering thread.
# Compile our shader at initialization.
func _initialize_compute() -> void:
rd = RenderingServer.get_rendering_device()
if not rd:
return
# Compile our shader.
var shader_file := load("res://shaders/glsl_shaders/zone_blur.glsl")
var shader_spirv: RDShaderSPIRV = shader_file.get_spirv()
shader = rd.shader_create_from_spirv(shader_spirv)
if shader.is_valid():
pipeline = rd.compute_pipeline_create(shader)
# Called by the rendering thread every frame.
func _render_callback(p_effect_callback_type: EffectCallbackType, p_render_data: RenderData) -> void:
if rd and p_effect_callback_type == EFFECT_CALLBACK_TYPE_POST_TRANSPARENT and pipeline.is_valid():
# Get our render scene buffers object, this gives us access to our render buffers.
# Note that implementation differs per renderer hence the need for the cast.
var render_scene_buffers := p_render_data.get_render_scene_buffers()
if render_scene_buffers:
# Get our render size, this is the 3D render resolution!
var size: Vector2i = render_scene_buffers.get_internal_size()
if size.x == 0 and size.y == 0:
return
# We can use a compute shader here.
@warning_ignore("integer_division")
var x_groups := (size.x - 1) / 8 + 1
@warning_ignore("integer_division")
var y_groups := (size.y - 1) / 8 + 1
var z_groups := 1
# Create push constant.
# Must be aligned to 16 bytes and be in the same order as defined in the shader.
var push_constant := PackedFloat32Array([
size.x,
size.y,
0.0,
0.0,
])
# Loop through views just in case we're doing stereo rendering. No extra cost if this is mono.
var view_count: int = render_scene_buffers.get_view_count()
for view in view_count:
# Get the RID for our color image, we will be reading from and writing to it.
var input_image: RID = render_scene_buffers.get_color_layer(view)
# Create a uniform set, this will be cached, the cache will be cleared if our viewports configuration is changed.
var uniform := RDUniform.new()
uniform.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE
uniform.binding = 0
uniform.add_id(input_image)
var uniform_set := UniformSetCacheRD.get_cache(shader, 0, [uniform])
# Run our compute 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_set_push_constant(compute_list, push_constant.to_byte_array(), push_constant.size() * 4)
rd.compute_list_dispatch(compute_list, x_groups, y_groups, z_groups)
rd.compute_list_end()
#endregion
GLSL Shader
#[compute]
#version 460
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
layout(rgba8, set = 0, binding = 0) uniform image2D colorImage;
// layout(rgba16f, set = 1, binding = 1) uniform image2D blurBuffer;
layout(push_constant, std430) uniform Params
{
vec2 raster_size;
vec2 effect_size;
} params;
void main()
{
ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
vec2 uv_norm = vec2(uv) / params.raster_size;
ivec2 size = ivec2(params.raster_size);
// Prevent reading/writing out of bounds.
if (uv.x >= size.x || uv.y >= size.y) {
return;
}
vec4 color = imageLoad(colorImage, uv);
// Apply our changes.
float gray = color.r * 0.2125 + color.g * 0.7154 + color.b * 0.0721;
color.rgb = vec3(gray);
// Write back to our color buffer.
imageStore(colorImage, uv, color);
}
EDIT: The C# script in question makes use of an extension method to test for null instances.
Extension method
public static bool IsValid(this GodotObject go)
{
return (go != null) && Godot.GodotObject.IsInstanceValid(go);
}

