diff --git a/Packages/Acerola-Compute b/Packages/Acerola-Compute deleted file mode 160000 index 9a5aa69..0000000 --- a/Packages/Acerola-Compute +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9a5aa6925481aa9db1428f7c7b93d80565807152 diff --git a/Packages/Acerola-Compute/Examples/exposure_example.acompute b/Packages/Acerola-Compute/Examples/exposure_example.acompute new file mode 100644 index 0000000..f028b4e --- /dev/null +++ b/Packages/Acerola-Compute/Examples/exposure_example.acompute @@ -0,0 +1,24 @@ +#kernel ExposureExample + + +layout(rgba16f, set = 0, binding = 0) uniform image2D _RenderTarget; + +layout(binding = 1) uniform UniformBufferObject { + vec4 _Exposure; +}; + +layout(push_constant, std430) uniform Params { + vec2 raster_size; +}; + +[numthreads(8, 8, 1)] +void ExposureExample() { + ivec2 uv = ivec2(gl_GlobalInvocationID.xy); + ivec2 size = ivec2(raster_size); + + if (uv.x >= size.x || uv.y >= size.y) return; + + vec3 color = imageLoad(_RenderTarget, uv).rgb * _Exposure.gba * _Exposure.r; + + imageStore(_RenderTarget, uv, vec4(color, 1.0)); +} \ No newline at end of file diff --git a/Packages/Acerola-Compute/Examples/exposure_shader.gd b/Packages/Acerola-Compute/Examples/exposure_shader.gd new file mode 100644 index 0000000..0e7068b --- /dev/null +++ b/Packages/Acerola-Compute/Examples/exposure_shader.gd @@ -0,0 +1,64 @@ +@tool +extends CompositorEffect +class_name ExposureCompositorEffect + +@export_group("Shader Settings") +@export var exposure = Vector4(2, 1, 1, 1) + +var rd : RenderingDevice +var exposure_compute : ACompute + +func _init(): + effect_callback_type = EFFECT_CALLBACK_TYPE_POST_TRANSPARENT + rd = RenderingServer.get_rendering_device() + + # To make use of an existing ACompute shader we use its filename to access it, in this case, the example compute shader file is 'exposure_example.acompute' + exposure_compute = ACompute.new('exposure_example') + + +func _notification(what): + if what == NOTIFICATION_PREDELETE: + # ACompute will handle the freeing of any resources attached to it + exposure_compute.free() + + +func _render_callback(p_effect_callback_type, p_render_data): + if not enabled: return + if p_effect_callback_type != EFFECT_CALLBACK_TYPE_POST_TRANSPARENT: return + + if not rd: + push_error("No rendering device") + return + + var render_scene_buffers : RenderSceneBuffersRD = p_render_data.get_render_scene_buffers() + + if not render_scene_buffers: + push_error("No buffer to render to") + return + + + var size = render_scene_buffers.get_internal_size() + if size.x == 0 and size.y == 0: + push_error("Rendering to 0x0 buffer") + return + + var x_groups = (size.x - 1) / 8 + 1 + var y_groups = (size.y - 1) / 8 + 1 + var z_groups = 1 + + # Vulkan has a feature known as push constants which are like uniform sets but for very small amounts of data + var push_constant : PackedFloat32Array = PackedFloat32Array([size.x, size.y, 0.0, 0.0]) + + for view in range(render_scene_buffers.get_view_count()): + var input_image = render_scene_buffers.get_color_layer(view) + + # Pack the exposure vector into a byte array + var uniform_array = PackedFloat32Array([exposure.x, exposure.y, exposure.z, exposure.w]).to_byte_array() + + # ACompute handles uniform caching under the hood, as long as the exposure value doesn't change or the render target doesn't change, these functions will only do work once + exposure_compute.set_texture(0, input_image) + exposure_compute.set_uniform_buffer(1, uniform_array) + exposure_compute.set_push_constant(push_constant.to_byte_array()) + + # Dispatch the compute kernel + exposure_compute.dispatch(0, x_groups, y_groups, z_groups) diff --git a/Packages/Acerola-Compute/Examples/exposure_shader.gd.uid b/Packages/Acerola-Compute/Examples/exposure_shader.gd.uid new file mode 100644 index 0000000..b1826a1 --- /dev/null +++ b/Packages/Acerola-Compute/Examples/exposure_shader.gd.uid @@ -0,0 +1 @@ +uid://de55kuko5hepf diff --git a/Packages/Acerola-Compute/acerola_shader_compiler.gd b/Packages/Acerola-Compute/acerola_shader_compiler.gd new file mode 100644 index 0000000..cc6e0cc --- /dev/null +++ b/Packages/Acerola-Compute/acerola_shader_compiler.gd @@ -0,0 +1,237 @@ +@tool +extends Node + +var shader_file_regex = RegEx.new() + +var shader_files : Array = Array() +var compute_shader_file_paths : Array = Array() + +var rd : RenderingDevice + +var shader_compilations = {} +var shader_code_cache = {} + +var compute_shader_kernel_compilations = {} + +func find_files(dir_name) -> void: + var dir = DirAccess.open(dir_name) + + if dir: + dir.list_dir_begin() + var file_name = dir.get_next() + while file_name != "": + if dir.current_is_dir(): + find_files(dir_name + '/' + file_name) + else: + # if file_name.get_extension() == 'glsl'and shader_file_regex.search(file_name): + # shader_files.push_back(dir_name + '/' + file_name) + + if file_name.get_extension() == 'acompute': + compute_shader_file_paths.push_back(dir_name + '/' + file_name) + + file_name = dir.get_next() + + +func get_shader_name(file_path: String) -> String: + return file_path.get_file().split(".")[0] + + +func compile_shader(shader_file_path) -> void: + var shader_name = shader_file_path.split("/")[-1].split(".glsl")[0] + + if shader_compilations.has(shader_name): + if shader_compilations[shader_name].is_valid(): + print("Freeing: " + shader_name) + rd.free_rid(shader_compilations[shader_name]) + + var shader_code = FileAccess.open(shader_file_path, FileAccess.READ).get_as_text() + shader_code_cache[shader_name] = shader_code + + var shader_compilation = RID() + + var shader_source : RDShaderSource = RDShaderSource.new() + shader_source.language = RenderingDevice.SHADER_LANGUAGE_GLSL + shader_source.source_compute = shader_code + var shader_spirv : RDShaderSPIRV = rd.shader_compile_spirv_from_source(shader_source) + + if shader_spirv.compile_error_compute != "": + push_error(shader_spirv.compile_error_compute) + push_error("In: " + shader_code) + return + + print("Compiling: " + shader_name) + shader_compilation = rd.shader_create_from_spirv(shader_spirv) + + if not shader_compilation.is_valid(): + return + + shader_compilations[shader_name] = shader_compilation + + +func compile_compute_shader(compute_shader_file_path) -> void: + var compute_shader_name = get_shader_name(compute_shader_file_path) + + print("Compiling Compute Shader: " + compute_shader_name) + + var file = FileAccess.open(compute_shader_file_path, FileAccess.READ) + var raw_shader_code_string = file.get_as_text() + shader_code_cache[compute_shader_file_path] = raw_shader_code_string + + var raw_shader_code = raw_shader_code_string.split("\n") + + var kernel_names = Array() + + # Strip out kernel names + while file.get_position() < file.get_length(): + var line = file.get_line() + + if line.begins_with("#kernel "): + var kernel_name = line.split("#kernel")[1].strip_edges() + # print("Kernel Found: " + kernel_name) + kernel_names.push_back(kernel_name) + raw_shader_code.remove_at(0) + else: + break + + # If no kernels defined at top of file, fail to compile + if kernel_names.size() == 0: + push_error("Failed to compile: " + compute_shader_file_path) + push_error("Reason: No kernels found") + return + + + # If no code after kernel definitions or if nothing in file at all, fail to compile + if file.get_position() >= file.get_length(): + push_error("Failed to compile: " + compute_shader_file_path) + push_error("Reason: No shader code found") + return + + # Verify kernels exist + raw_shader_code_string = "\n".join(raw_shader_code) + for kernel_name in kernel_names: + if not raw_shader_code_string.contains(kernel_name): + push_error("Failed to compile: " + compute_shader_file_path) + push_error("Reason: " + kernel_name + " kernel not found!") + + var kernel_to_thread_group_count = {} + + # Find kernels and extract thread groups + for i in raw_shader_code.size(): + var line = raw_shader_code[i] + + for kernel_name in kernel_names: + if line.contains(kernel_name) and line.contains('void'): + # print("Found kernel " + kernel_name + " at line " + str(i + kernel_names.size() + 1)) + + # find thread group count by searching previous line of code from kernel function + var newLine = raw_shader_code[i - 1].strip_edges() + if newLine.contains('numthreads'): + var thread_groups = newLine.split('(')[-1].split(')')[0].split(',') + if thread_groups.size() != 3: + push_error("Failed to compile: " + compute_shader_file_path) + push_error("Reason: kernel thread group syntax error") + + kernel_to_thread_group_count[kernel_name] = Array() + for n in thread_groups.size(): + kernel_to_thread_group_count[kernel_name].push_back((thread_groups[n].strip_edges())) + + raw_shader_code.set(i - 1, "") + + # print(kernel_to_thread_group_count[kernel_name]) + else: + push_error("Failed to compile: " + compute_shader_file_path) + push_error("Reason: kernel thread group count not found") + return + + # Compile kernels + compute_shader_kernel_compilations[compute_shader_name] = Array() + for kernel_name in kernel_names: + var shader_code = PackedStringArray(raw_shader_code) + + # Insert GLSL thread group layout for the kernel + var thread_group = kernel_to_thread_group_count[kernel_name] + shader_code.insert(0, "layout(local_size_x = " + thread_group[0] + ", local_size_y = " + thread_group[1] + ", local_size_z = " + thread_group[2] + ") in;") + + # Insert GLSL version at top of file + shader_code.insert(0, "#version 450") + + # Replace kernel name with main + var shader_code_string = "\n".join(shader_code).replace(kernel_name, "main") + + # Compile shader + + var shader_compilation = RID() + + var shader_source : RDShaderSource = RDShaderSource.new() + shader_source.language = RenderingDevice.SHADER_LANGUAGE_GLSL + shader_source.source_compute = shader_code_string + var shader_spirv : RDShaderSPIRV = rd.shader_compile_spirv_from_source(shader_source) + + if shader_spirv.compile_error_compute != "": + push_error(shader_spirv.compile_error_compute) + push_error("In: " + shader_code_string) + return + + print("- Compiling Kernel: " + kernel_name) + shader_compilation = rd.shader_create_from_spirv(shader_spirv) + + if not shader_compilation.is_valid(): + return + + compute_shader_kernel_compilations[compute_shader_name].push_back(shader_compilation) + + # print(shader_code_string) + + # print("\n".join(raw_shader_code)) + + +func _init() -> void: + rd = RenderingServer.get_rendering_device() + + find_files("res://") + + for shader_file in shader_files: + compile_shader(shader_file) + + for file_path in compute_shader_file_paths: + compile_compute_shader(file_path) + + +func _process(delta: float) -> void: + # Compare current shader code with cached shader code and recompile if changed + for file_path in compute_shader_file_paths: + if shader_code_cache[file_path] != FileAccess.open(file_path, FileAccess.READ).get_as_text(): + var shader_name = get_shader_name(file_path) + + # Free existing kernels + for kernel in compute_shader_kernel_compilations[shader_name]: + rd.free_rid(kernel) + + compute_shader_kernel_compilations[shader_name].clear() + + compile_compute_shader(file_path) + + +func _notification(what): + if what == NOTIFICATION_PREDELETE or what == NOTIFICATION_WM_CLOSE_REQUEST: + var shader_names = shader_compilations.keys() + + for shader_name in shader_names: + var shader = shader_compilations[shader_name] + if shader.is_valid(): + print("Freeing: " + shader_name) + rd.free_rid(shader) + + for compute_shader in compute_shader_kernel_compilations.keys(): + for kernel in compute_shader_kernel_compilations[compute_shader]: + rd.free_rid(kernel) + + +func get_shader_compilation(shader_name: String) -> RID: + return shader_compilations[shader_name] + +func get_compute_kernel_compilation(shader_name, kernel_index): + return compute_shader_kernel_compilations[shader_name][kernel_index] + +func get_compute_kernel_compilations(shader_name): + return compute_shader_kernel_compilations[shader_name] diff --git a/Packages/Acerola-Compute/acerola_shader_compiler.gd.uid b/Packages/Acerola-Compute/acerola_shader_compiler.gd.uid new file mode 100644 index 0000000..ca63cb3 --- /dev/null +++ b/Packages/Acerola-Compute/acerola_shader_compiler.gd.uid @@ -0,0 +1 @@ +uid://dad3g6godfma diff --git a/Packages/Acerola-Compute/acompute.gd b/Packages/Acerola-Compute/acompute.gd new file mode 100644 index 0000000..6cf10d8 --- /dev/null +++ b/Packages/Acerola-Compute/acompute.gd @@ -0,0 +1,139 @@ +@tool +extends Object +class_name ACompute + + +var kernels = Array() +var rd : RenderingDevice +var shader_name : String +var shader_id : RID +var push_constant : PackedByteArray +var uniform_set_gpu_id : RID +var uniform_set_cache : Array +var current_bound_uniform_set_cpu_copy : Array + +var refresh_uniforms = true + +# Contains the contents of the uniform array itself +var uniform_buffer_cache = {} +# Contains the RIDs for the gpu versions of the uniform array +var uniform_buffer_id_cache = {} + +func get_kernel(index: int) -> RID: + return kernels[index] + + +func set_push_constant(_push_constant: PackedByteArray) -> void: + push_constant = PackedByteArray(_push_constant) + + +func set_texture(binding: int, texture: RID) -> void: + var u : RDUniform = RDUniform.new() + u.uniform_type = RenderingDevice.UNIFORM_TYPE_IMAGE + u.binding = binding + u.add_id(texture) + + cache_uniform(u) + + +func create_uniform_buffer(binding: int, uniform_array: PackedByteArray) -> void: + var uniform_buffer_id = rd.uniform_buffer_create(uniform_array.size(), uniform_array) + + var u : RDUniform = RDUniform.new() + + u.uniform_type = RenderingDevice.UNIFORM_TYPE_UNIFORM_BUFFER + u.binding = binding + u.add_id(uniform_buffer_id) + + uniform_buffer_id_cache[binding] = uniform_buffer_id + uniform_buffer_cache[binding] = PackedByteArray(uniform_array) + cache_uniform(u) + + +func set_uniform_buffer(binding: int, uniform_array: PackedByteArray) -> void: + # Instantiate GPU memory if first time setting uniform buffer + if not uniform_buffer_cache.has(binding): + create_uniform_buffer(binding, uniform_array) + return + + # Compare array contents with cache and skip update if nothing changed + if uniform_array == uniform_buffer_cache.get(binding): + return + + + # Update uniform buffer + rd.buffer_update(uniform_buffer_id_cache.get(binding), 0, uniform_array.size(), uniform_array) + + # Cache array contents + uniform_buffer_cache[binding] = PackedByteArray(uniform_array) + + +func cache_uniform(u: RDUniform) -> void: + if uniform_set_cache.size() - 1 < u.binding: + refresh_uniforms = true + uniform_set_cache.resize(u.binding + 1) + + # If uniform has had its info changed then set flag to refresh gpu side uniform data + if uniform_set_cache[u.binding]: + var old_uniform_ids = uniform_set_cache[u.binding].get_ids() + var new_uniform_ids = u.get_ids() + + if old_uniform_ids.size() != new_uniform_ids.size(): + refresh_uniforms = true + else: + for i in old_uniform_ids.size(): + if old_uniform_ids[i].get_id() != new_uniform_ids[i].get_id(): + refresh_uniforms = true + break + + + uniform_set_cache[u.binding] = u + + +func _init(_shader_name: String) -> void: + rd = RenderingServer.get_rendering_device() + + uniform_set_cache = Array() + + shader_name = _shader_name + + shader_id = AcerolaShaderCompiler.get_compute_kernel_compilation(shader_name, 0) + + for kernel in AcerolaShaderCompiler.get_compute_kernel_compilations(shader_name): + kernels.push_back(rd.compute_pipeline_create(kernel)) + + +func dispatch(kernel_index: int, x_groups: int, y_groups: int, z_groups: int) -> void: + var global_shader_id = AcerolaShaderCompiler.get_compute_kernel_compilation(shader_name, 0) + + # Recreate kernel pipelines if shader was recompiled + if shader_id != global_shader_id: + shader_id = global_shader_id + kernels.clear() + for kernel in AcerolaShaderCompiler.get_compute_kernel_compilations(shader_name): + kernels.push_back(rd.compute_pipeline_create(kernel)) + + uniform_set_gpu_id = rd.uniform_set_create(uniform_set_cache, global_shader_id, 0) + + # Reallocate GPU memory if uniforms need updating + if refresh_uniforms: + if uniform_set_gpu_id.is_valid(): rd.free_rid(uniform_set_gpu_id) + uniform_set_gpu_id = rd.uniform_set_create(uniform_set_cache, global_shader_id, 0) + refresh_uniforms = false + + var compute_list := rd.compute_list_begin() + rd.compute_list_bind_compute_pipeline(compute_list, kernels[kernel_index]) + rd.compute_list_bind_uniform_set(compute_list, uniform_set_gpu_id, 0) + rd.compute_list_set_push_constant(compute_list, push_constant, push_constant.size()) + rd.compute_list_dispatch(compute_list, x_groups, y_groups, z_groups) + rd.compute_list_end() + + +func free() -> void: + for kernel in kernels: + rd.free_rid(kernel) + + for binding in uniform_buffer_id_cache.keys(): + rd.free_rid(uniform_buffer_id_cache[binding]) + + if uniform_set_gpu_id.is_valid(): rd.free_rid(uniform_set_gpu_id) diff --git a/Packages/Acerola-Compute/acompute.gd.uid b/Packages/Acerola-Compute/acompute.gd.uid new file mode 100644 index 0000000..ac24cf4 --- /dev/null +++ b/Packages/Acerola-Compute/acompute.gd.uid @@ -0,0 +1 @@ +uid://dmhdvw6cfhyac