OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / assets / ocean / render_context.gd

147 lines · gd ref:
class_name RenderingContext extends Object
## A wrapper around [RenderingDevice] that handles basic memory management/allocation

class DeletionQueue:
	var queue : Array[RID] = []

	func push(rid : RID) -> RID:
		queue.push_back(rid)
		return rid

	func flush(device : RenderingDevice) -> void:
		# We work backwards in order of allocation when freeing resources
		for i in range(queue.size() - 1, -1, -1):
			if not queue[i].is_valid(): continue
			device.free_rid(queue[i])
		queue.clear()

	func free_rid(device : RenderingDevice, rid : RID) -> void:
		var rid_idx := queue.find(rid)
		assert(rid_idx != -1, 'RID was not found in deletion queue!')
		device.free_rid(queue.pop_at(rid_idx))

class Descriptor:
	var rid : RID
	var type : RenderingDevice.UniformType

	func _init(rid_ : RID, type_ : RenderingDevice.UniformType) -> void:
		rid = rid_; type = type_

var device : RenderingDevice
var deletion_queue := DeletionQueue.new()
var shader_cache : Dictionary
var needs_sync := false

static func create(device : RenderingDevice=null) -> RenderingContext:
	var context := RenderingContext.new()
	context.device = RenderingServer.create_local_rendering_device() if not device else device
	return context

func _notification(what):
	if what == NOTIFICATION_PREDELETE:
		# All resources must be freed after use to avoid memory leaks.
		deletion_queue.flush(device)
		shader_cache.clear()
		if device != RenderingServer.get_rendering_device():
			device.free()

# --- WRAPPER FUNCTIONS ---
func submit() -> void: device.submit(); needs_sync = true
func sync() -> void: device.sync(); needs_sync = false
func compute_list_begin() -> int: return device.compute_list_begin()
func compute_list_end() -> void: device.compute_list_end()
func compute_list_add_barrier(compute_list : int) -> void: device.compute_list_add_barrier(compute_list)

# --- HELPER FUNCTIONS ---
func load_shader(path : String) -> RID:
	if not shader_cache.has(path):
		var shader_file := load(path)
		var shader_spirv : RDShaderSPIRV = shader_file.get_spirv()
		shader_cache[path] = deletion_queue.push(device.shader_create_from_spirv(shader_spirv))
	return shader_cache[path]

func create_storage_buffer(size : int, data : PackedByteArray=[], usage:=0) -> Descriptor:
	if size > len(data):
		var padding := PackedByteArray(); padding.resize(size - len(data))
		data += padding
	return Descriptor.new(deletion_queue.push(device.storage_buffer_create(max(size, len(data)), data, usage)), RenderingDevice.UNIFORM_TYPE_STORAGE_BUFFER)

func create_uniform_buffer(size : int, data : PackedByteArray=[]) -> Descriptor:
	size = max(16, size)
	if size > len(data):
		var padding := PackedByteArray(); padding.resize(size - len(data))
		data += padding
	return Descriptor.new(deletion_queue.push(device.uniform_buffer_create(max(size, len(data)), data)), RenderingDevice.UNIFORM_TYPE_UNIFORM_BUFFER)

func create_texture(dimensions : Vector2i, format : RenderingDevice.DataFormat, usage:=0x18B, num_layers:=0, view:=RDTextureView.new(), data : PackedByteArray=[]) -> Descriptor:
	assert(num_layers >= 1)
	var texture_format := RDTextureFormat.new()
	texture_format.array_layers = 1 if num_layers == 0 else num_layers
	texture_format.format = format
	texture_format.width = dimensions.x
	texture_format.height = dimensions.y
	texture_format.texture_type = RenderingDevice.TEXTURE_TYPE_2D if num_layers == 0 else RenderingDevice.TEXTURE_TYPE_2D_ARRAY
	texture_format.usage_bits = usage # Default: RenderingDevice.TEXTURE_USAGE_SAMPLING_BIT | RenderingDevice.TEXTURE_USAGE_COLOR_ATTACHMENT_BIT | RenderingDevice.TEXTURE_USAGE_STORAGE_BIT | RenderingDevice.TEXTURE_USAGE_CAN_COPY_TO_BIT | RenderingDevice.TEXTURE_USAGE_CAN_COPY_FROM_BIT
	return Descriptor.new(deletion_queue.push(device.texture_create(texture_format, view, data)), RenderingDevice.UNIFORM_TYPE_IMAGE)

## Creates a descriptor set. The ordering of the provided descriptors matches the binding ordering
## within the shader.
func create_descriptor_set(descriptors : Array[Descriptor], shader : RID, descriptor_set_index :=0) -> RID:
	var uniforms : Array[RDUniform]
	for i in range(len(descriptors)):
		var uniform := RDUniform.new()
		uniform.uniform_type = descriptors[i].type
		uniform.binding = i  # This matches the binding in the shader.
		uniform.add_id(descriptors[i].rid)
		uniforms.push_back(uniform)
	return deletion_queue.push(device.uniform_set_create(uniforms, shader, descriptor_set_index))

## Returns a [Callable] which will dispatch a compute pipeline (within a compute) list based on the
## provided block dimensions. The ordering of the provided descriptor sets matches the set ordering
## within the shader.
func create_pipeline(block_dimensions : Array, descriptor_sets : Array, shader : RID) -> Callable:
	var pipeline = deletion_queue.push(device.compute_pipeline_create(shader))
	return func(context : RenderingContext, compute_list : int, push_constant : PackedByteArray=[], descriptor_set_overwrites:=[], block_dimensions_overwrite_buffer:=RID(), block_dimensions_overwrite_buffer_byte_offset:=0) -> void:
		var device := context.device
		var sets = descriptor_sets if descriptor_set_overwrites.is_empty() else descriptor_set_overwrites
		assert(len(block_dimensions) == 3 or block_dimensions_overwrite_buffer.is_valid(), 'Must specify block dimensions or specify a dispatch indirect buffer!')
		assert(len(sets) >= 1, 'Must specify at least on descriptor set!')

		device.compute_list_bind_compute_pipeline(compute_list, pipeline)
		device.compute_list_set_push_constant(compute_list, push_constant, push_constant.size())
		for i in range(len(sets)):
			device.compute_list_bind_uniform_set(compute_list, sets[i], i)

		if block_dimensions_overwrite_buffer.is_valid():
			device.compute_list_dispatch_indirect(compute_list, block_dimensions_overwrite_buffer, block_dimensions_overwrite_buffer_byte_offset)
		else:
			device.compute_list_dispatch(compute_list, block_dimensions[0], block_dimensions[1], block_dimensions[2])

## Returns a [PackedFloat32Array] from the provided data, whose size is rounded up to the nearest
## multiple of 16
static func create_push_constant(data : Array) -> PackedByteArray:
	var packed_size := len(data)*4
	assert(packed_size <= 128, 'Push constant size must be at most 128 bytes!')

	# hyper-tps: NO 16-BYTE PADDING. Upstream rounded up to a 16-byte multiple,
	# which older Godot tolerated; 4.7 validates the exact declared size and
	# rejects every dispatch:
	#
	#   This compute pipeline requires (52) bytes of push constant data,
	#   supplied: (64)
	#
	# The rejection is silent as far as output goes -- the maps come back
	# perfectly zero, which reads as "the FFT produced nothing" rather than as a
	# version incompatibility. Vulkan only requires a multiple of 4, and
	# len(data)*4 is always that and matches what the shaders declare (13 -> 52,
	# 5 -> 20, 1 -> 4).
	var packed_data := PackedByteArray();
	packed_data.resize(packed_size);
	packed_data.fill(0);

	for i in range(len(data)):
		match typeof(data[i]):
			TYPE_INT, TYPE_BOOL:   packed_data.encode_s32(i*4, data[i])
			TYPE_FLOAT: packed_data.encode_float(i*4, data[i])
	return packed_data