Compute shader debug problem

Godot Version

4.7

Question

I am having problems reproducing the effect of a heightfield texture in a compute shader - the values just don’t look right. There is enough shared memory, The second shader uses an additional 64 floats but the max might be 32 kb.

I have a 2 stage compute shader and the debugging process is difficult, are there any tools for the task?

1st pass… reduce 4096 x 4096 sized texture to 512 by 512 and only store the min and max heights from the 8 x 8 tiles. (santiy check, 512 x 8 = 4096)

#[compute]
#version 450

layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

layout(set = 0, binding = 0, r16) uniform image2D height_image;
layout(set = 0, binding = 1, rgba32f) uniform image2D data_output_image;

shared float sdata[64];

void main() {

	ivec2 UV = ivec2( int(gl_GlobalInvocationID.x), int(gl_GlobalInvocationID.y) ); 
	
	ivec2 UV_out = ivec2( int( float(UV.x) / 8), int( float(UV.y) / 8) );
	
	uint local_index = gl_LocalInvocationID.x + gl_LocalInvocationID.y * 8; 
	
	sdata[local_index] =  500.0*imageLoad(height_image, UV ).r;
	
	memoryBarrierShared();
	barrier();
	
	float min_h = 100000.0;
	float max_h = -100000.0;
	
	for (uint i = 0; i<64; i++){
		
		if ( sdata[i] > max_h ){
			max_h = sdata[i];
		} 
		if ( sdata[i] < min_h ){
			min_h = sdata[i];
		}	
	}
	memoryBarrierShared();
	barrier();
	


	vec4 output_data_vec = vec4( min_h, max_h, min_h, max_h);
	
	
	imageStore(data_output_image, UV_out, output_data_vec);
}

Second pass … take the 512 x 512 data array and compute the min and max values for 64 x 64 tiles

(sanity check 64 x 8 = 512 )

#[compute]
#version 450

layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

layout(set = 0, binding = 0, rgba32f) uniform image2D height_image;
layout(set = 0, binding = 1, rgba32f) uniform image2D data_output_image;


shared float sdata_min[64];
shared float sdata_max[64];




void main() {

	ivec2 UV = ivec2( int(gl_GlobalInvocationID.x), int(gl_GlobalInvocationID.y) ); 
	
	ivec2 UV_out = ivec2( int( float(UV.x) / 8), int( float(UV.y) / 8) );
	
	uint local_index = gl_LocalInvocationID.x + gl_LocalInvocationID.y * 8; 
	
	sdata_min[local_index] =  imageLoad(height_image, UV ).r;
	sdata_max[local_index] =  imageLoad(height_image, UV ).g;	
	
	memoryBarrierShared();
	barrier();
	
	float min_h = 100000.0;
	float max_h = -100000.0;
	
	for (uint i = 0; i<64; i++){
		
		if ( sdata_min[i] < min_h ){
			min_h = sdata_min[i];
		} 
		if ( sdata_max[i] > max_h ){
			max_h = sdata_max[i];
		}	
	}
	memoryBarrierShared();
	barrier();
	
	vec4 output_data_vec = vec4( min_h, max_h, min_h, max_h);
	
	imageStore(data_output_image, UV_out, output_data_vec);
}

The code (using the EasyCompute addon ). The values that are printed should be min and max near the center … but the heightfield is quite high altitude when rendered as a heightfield, the values printed by this function are like 5.0 and 6.0 not 400.0 . What could be going on?

func texture_fill2():
	compute.load_shader("fill", "res://examples/fill2.glsl")
	
	var tex :CompressedTexture2D= load("res://examples/height2.exr")
	var image = tex.get_image()
	if image.is_compressed():
		image.decompress()	
	image.convert(Image.FORMAT_R16)
	var texture = ImageTexture.create_from_image(image)
	var source_size = 4096
	compute.register_texture("height_image", 0, source_size, source_size, image.get_data(), RenderingDevice.DATA_FORMAT_R16_UNORM)

	compute.register_texture("data_output_image", 1, 512, 512,[], RenderingDevice.DATA_FORMAT_R32G32B32A32_SFLOAT)
	

	# FIRST PASS, Find min and max height values
	compute.execute("fill", 512,512)
	compute.sync()

	var image_data = compute.fetch_texture("data_output_image")
	#print(image_data.to_vector4_array())

	# SECOND PASS, Find min and max height values
	compute.load_shader("fill_min_max", "res://examples/fill_min_max2.glsl")
	compute.register_texture("height_image", 0, 512, 512, image_data, RenderingDevice.DATA_FORMAT_R32G32B32A32_SFLOAT)
	compute.register_texture("data_output_image", 0, 64, 64, [], RenderingDevice.DATA_FORMAT_R32G32B32A32_SFLOAT)
	compute.execute("fill_min_max", 64,64)
	compute.sync()
	var final_image_data = compute.fetch_texture("data_output_image")
	var float_data = final_image_data.to_vector4_array()
	
	var index1 = 32 + 32*64
	var index2 = 33 + 32*64
	var index3 = 32 + 33*64
	var index4 = 33 + 33*64
	
	var max:float = 0.0
	var min:float = 10000.0
	if float_data[index1].x < min:
		min =float_data[index1].x 
	if float_data[index2].x < min:
		min =float_data[index2].x		
	if float_data[index3].x < min:
		min =float_data[index3].x 
	if float_data[index4].x < min:
		min =float_data[index4].x		
	#print(float_data[index])
	if float_data[index1].y > max:
		max =float_data[index1].y 
	if float_data[index2].y > max:
		max =float_data[index2].y		
	if float_data[index3].y > max:
		max =float_data[index3].y 
	if float_data[index4].y > max:
		max =float_data[index4].y
		
	print(min)
	print(max)	
	
	var output_image = Image.create_from_data(64, 64, false, Image.FORMAT_RGBAF, final_image_data)
	var texture2 = ImageTexture.create_from_image(output_image)
 	
	sprite_2d.texture = texture2
	sprite_2d_2.texture = texture

The height map should produce heights of up to 500.0 when scaled by 500.0, instead the values are all less than that.

link to minimal reproduction project …

DarkGrey456/compute_AABB_heights

If you’re invoking on 512x512 grid, Shouldn’t UV be your output coordinate, and UV * 8 your input?

The UV is the threads global thread id …

gl_GlobalInvocationID.x and gl_GlobalInvocationID.y

The godot water demo does the same, but actually i was thinking it looked wrong, the gl_GlobalInvocationID.x is the equivalent to the Block index in cuda, or actually the thread index?

The invocation id coordinates will be in 0-511 range, yet you use it to read from the texture that’s 4096 in size.

Yeah i see, so im looking for

gl_GlobalInvocationID.x * local_group_size + gl_LocalInvocationID.x

?

I looked at the water demo but maybe i misread the meaning … I got the impression the global id was the thread id …

In one of the early tests i was getting mimics of the big texture in pure white showing up on the smaller sprite.

The reason i have to launch so many global thread groups is the shared memory limit …

i will investigate … thankyou

It is but, according to your gdscript code, you don’t invoke it on the size of the input 4096 texture. You invoke it on the size of the output 512 texture, yet your code seems to be assuming you’re doing the former.

Start debugging by making a very simple test shader that just copies data from one texture to another and see what gets copied.

Yeah i was going to say …

https://registry.khronos.org/OpenGL-Refpages/gl4/html/gl_GlobalInvocationID.xhtml

Yeah sorry if this is unclear … the output image uses these UV’s

ivec2 UV_out = ivec2( int( float(UV.x) / 8), int( float(UV.y) / 8) );

and the shared memory for each thread is computed from the local index

`uint local_index = gl_LocalInvocationID.x + gl_LocalInvocationID.y * 8;

sdata\[local_index\] =  500.0\*imageLoad(height_image, UV ).r;

`

The height is sampled with the global UV thats unique for each thread, stored /cached in the shared memory, then each thread computes the min and max height values for their local group… there is probably a better way to do it …

Yeah theres two invocations … first has 512 x 512 global groups of 8x8 threads … total of 4096x4096 threads the output size is 512 x512

The second shader takes the 512 x512 and runs it with 64 x64 groups of 8x8 threads

(At least thats what i thought )

Where do you specify that?

compute.execute("fill", 512,512)

i thought this launches 512 x 512 groups … and each group runs the shader with:

layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

at the top that says the local group size is 8x8, so 64 threads per group in this case.

Could be the conversion of 16 bit float to 32 bit … i thought a = b; would work …

Ok, but why do you need to run the shader per input texture pixel? Shouldn’t you run it per output texture pixel and sample 64 pixels in the input texture from there?

That is an idea, i suppose that solves thread redundancy - got to take quite a large number of samples from the texture in each thread but performance isnt an issue because its for an initialization process not for real time …

I think theres a maximum of 16 or 32 texture samples per shader, so the initial data could potentially be moved to a Shader storage buffer SSBO allowing 16-96 reads, depending on hardware.

With the textures, dropping to shared memory blocks of 4×4 size would also force multiple invocations to filter to the lower resolutions.

its okay I think its working now …

	var max:float = 0.0
	var min:float = 10000.0
	for i in float_data.size():
		if float_data[i].y > max:
			max = float_data[i].y
		if float_data[i].x < min:
			min = float_data[i].x	

    print(min)
    print(max)

prints:

0.0
500.0

I also changed the input texture to R32 but that might not be the reason this worked

image.convert(Image.FORMAT_RF)

The process can also probably be optimized to save power using

if (gl_LocalInvocationID.x == 0 and gl_LocalInvocation.y == 0)

after the first memory barrier then performing the logic for checking min and max in one thread per group, writing to the output in only one thread per group.

This method would use far less electricity.

The version here

compute_AABB_heights/README.md at main · DarkGrey456/compute_AABB_heights

worked … but there is a weird problem with corrupted byte codes, it might be the cache in EasyCompute or the way Godot handles the script editor - when I load the GLSL compute shader in the script editor the program sometimes complains that the shader is invalid, so I duplicate the shader in the file system and then click it to open the shader panel - this shows me whether the shader at least compiles. Then I have to change the script to load the duplicate.

I also thought I had issues with the names of the textures and the bindings.

Heres a screen shot of the terrain heightfield AABB’s.