extends Control signal render_initial_ready(key) # A small chunk at the start has been rendered and is ready to play signal render_complete(key) # The full track has been rendered and is ready to pop-in const INPUT_TEX_WIDTH := 2048 const INPUT_FORMAT := Image.FORMAT_RGBA8 # Image.FORMAT_LA8 const INPUT_BYTES_PER_TEXEL := 4 # 2 const OUTPUT_BYTES_PER_TEXEL := 4 const QUAD_COLOR := PoolColorArray([Color.white, Color.white, Color.white, Color.white]) var OUTPUT_FRAMEBUFFER_SIZE: Vector2 var OUTPUT_WIDTH: int var OUTPUT_HEIGHT: int onready var viewport: Viewport = self.get_parent() onready var render_queue: Array = [] # of [desc key, remaining_samples] onready var cached_midis: Dictionary = {} # desc: [target_samples, ImageTexture] onready var cached_renders: Dictionary = {} # desc: [remaining_samples, PoolByteArray] onready var current_textures: Array = [] # of ImageTextures - Needed to prevent GC before draw onready var waiting_for_viewport: Array = [] onready var done_first_draw: bool = false func _ready() -> void: self._update_viewport(4096, 4096) func _process(_delta) -> void: update() func _draw() -> void: # Seems like the first one always fails if not self.done_first_draw: self.done_first_draw = true return if self.waiting_for_viewport: # Another node later in the draw sequence can call this within the same frame, # otherwise, this picks it up the following frame get_result() if self.render_queue: self._render_in_batch() # self._render_one_at_a_time() func _update_viewport(width: int, height: int) -> void: self.OUTPUT_WIDTH = width self.OUTPUT_HEIGHT = height self.OUTPUT_FRAMEBUFFER_SIZE = Vector2(width, height) self.viewport.size = OUTPUT_FRAMEBUFFER_SIZE self.material.set_shader_param('OUTPUT_FRAMEBUFFER_SIZE', OUTPUT_FRAMEBUFFER_SIZE) self.material.set_shader_param('INT_OUTPUT_WIDTH', OUTPUT_WIDTH) func _render_midi(key: String, output_rows_drawn_including_this: int, rows_to_draw: int) -> void: var target_samples_and_tex = self.cached_midis[key] var target_samples: int = target_samples_and_tex[0] var tex: ImageTexture = target_samples_and_tex[1] var y_top: int = OUTPUT_HEIGHT - output_rows_drawn_including_this var y_bot: int = y_top + rows_to_draw var uv_inv_v: float = 1 - (rows_to_draw / OUTPUT_FRAMEBUFFER_SIZE.y) var uvs := PoolVector2Array([Vector2(0, uv_inv_v), Vector2(1, uv_inv_v), Vector2(1, 1), Vector2(0, 1)]) var points := PoolVector2Array([Vector2(0, y_top), Vector2(OUTPUT_WIDTH, y_top), Vector2(OUTPUT_WIDTH, y_bot), Vector2(0, y_bot)]) draw_primitive(points, QUAD_COLOR, uvs, tex) self.waiting_for_viewport.append([rows_to_draw, key]) # Grab the result next draw func _render_in_batch() -> void: # self._update_viewport(4096, 4096) self.waiting_for_viewport = [] var rows_drawn := 0 while self.render_queue: var target_samples: int = self.render_queue[0][1] var rows_to_draw := int(ceil(target_samples/float(OUTPUT_WIDTH))) rows_drawn += rows_to_draw if rows_drawn > OUTPUT_HEIGHT: if self.waiting_for_viewport.empty(): print('Could not fit %s into %dx%d output framebuffer, it needs %d rows'%[self.render_queue[0][2], OUTPUT_WIDTH, OUTPUT_HEIGHT, rows_to_draw]) self.render_queue.pop_front() break # Draw the next ImageTexture self._render_midi(self.render_queue.pop_front()[0], rows_drawn, rows_to_draw) func _render_one_at_a_time() -> void: # Non power-of-two dimensioned textures should be restricted to GLES3 self.waiting_for_viewport = [] var rows_drawn := 0 if self.render_queue: var target_samples: int = self.render_queue[0][1] var rows_to_draw := int(ceil(target_samples/float(OUTPUT_WIDTH))) self._update_viewport(4096, rows_to_draw) rows_drawn += rows_to_draw # Draw the next ImageTexture self._render_midi(self.render_queue.pop_front()[0], rows_drawn, rows_to_draw) func push_image(image: Image, target_samples: int, desc: String) -> void: var tex := ImageTexture.new() tex.create_from_image(image, 0) self.cached_midis[desc] = [target_samples, tex] self.material.set_shader_param('midi_events_size', tex.get_size()) # Should all be the same size for now, revisit if we need mixed sizes. self.render_queue.append([desc, target_samples]) func push_bytes(data: PoolByteArray, target_samples: int, desc: String) -> void: var rows = int(pow(2, ceil(log((len(data)/INPUT_BYTES_PER_TEXEL) / INPUT_TEX_WIDTH)/log(2)))) var target_length = rows * INPUT_BYTES_PER_TEXEL * INPUT_FORMAT while len(data) < target_length: # This is inefficient, but this function should be called with pre-padded data anyway data.append(0) var image := Image.new() image.create_from_data(INPUT_TEX_WIDTH, rows, false, INPUT_FORMAT, data) self.push_image(image, target_samples, desc) func get_result() -> void: var result_texture := self.viewport.get_texture() var result_image := result_texture.get_data() var result_bytes := result_image.get_data() var retrieved_rows := 0 for rows_and_desc in self.waiting_for_viewport: var entry_rows: int = rows_and_desc[0] var key: String = rows_and_desc[1] var bytes_start := retrieved_rows * OUTPUT_WIDTH * OUTPUT_BYTES_PER_TEXEL var bytes_end := (retrieved_rows + entry_rows) * OUTPUT_WIDTH * OUTPUT_BYTES_PER_TEXEL var entry_bytes := result_bytes.subarray(bytes_start, bytes_end-1) self.cached_renders[key] = [0, entry_bytes] emit_signal('render_initial_ready', key) emit_signal('render_complete', key) retrieved_rows += entry_rows # result_bytes.resize(result_byte_count) self.waiting_for_viewport = [] # # Debugging: compare a sequence of all the possible 16bit integers # print_debug('result_image format is %d and has size'%result_image.get_format(), result_image.get_size(), result_bytes.subarray(0, 11)) # test_readback(result_bytes) func test_readback(result_bytes: PoolByteArray): # Debugging: compare a sequence of all the possible 16bit integers var buff := StreamPeerBuffer.new() buff.set_data_array(result_bytes) var tex_readback = 0 var uv_readback = 0 for i in 0x1000: tex_readback = buff.get_u16() uv_readback = buff.get_u16() if tex_readback != i: print('tex readback %d (0x%04x) was instead %d (0x%04x)'%[i, i, tex_readback, tex_readback]) if uv_readback != i: print('uv readback %d (0x%04x) was instead %d (0x%04x)'%[i, i, uv_readback, uv_readback])