From 80cbfa7ab88aefa1796e744aa9f3dd1de7171380 Mon Sep 17 00:00:00 2001 From: Luke Hubmayer-Werner Date: Wed, 10 Jul 2024 00:35:29 +0930 Subject: [PATCH] Some plumbing for upcoming fake compute shaders --- scripts/loaders/snes/music.gd | 23 ++++++----- shaders/audio_renderer.gdshader | 57 +++++++++++++++++++++++++++ test/audio_renderer.gd | 70 +++++++++++++++++++++++++++++++++ test/audio_system.tscn | 23 ++++++++++- 4 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 shaders/audio_renderer.gdshader create mode 100644 test/audio_renderer.gd diff --git a/scripts/loaders/snes/music.gd b/scripts/loaders/snes/music.gd index 63c296b..01ceaa3 100644 --- a/scripts/loaders/snes/music.gd +++ b/scripts/loaders/snes/music.gd @@ -120,6 +120,11 @@ var EVENT_MAP: Dictionary var NOTE_DURATIONS: PoolByteArray # This might need to be made untyped if a future addition uses PoolIntArray instead var REFERENCE_NOTE: int +const LOGGING_LEVEL_INFO: bool = false +func print_info(s: String) -> void: + if LOGGING_LEVEL_INFO: + print(s) + # These would be static methods if NOTE_DURATIONS and EVENT_MAP could be static func translate_instruction(buffer: StreamPeer) -> Array: var instruction := buffer.get_u8() @@ -189,7 +194,7 @@ func unroll_track(buffer: StreamPeerBuffer, bgm_start_pos: int, track_start_pos: var break_pos := loop_break_positions[loop_level] var jump_pos := loop_break_targets[loop_level] if jump_pos - pos != 1: # This happens once on track 68:06 FFV SNES BGM - print('LOOP_END at 0x%06X would break instead from 0x%06X to 0x%06X (%d past end)' % [pos, break_pos, jump_pos, jump_pos-pos]) + print_debug('LOOP_END at 0x%06X would break instead from 0x%06X to 0x%06X (%d past end)' % [pos, break_pos, jump_pos, jump_pos-pos]) buffer.seek(jump_pos) var loop_events := events.slice(rom_address_to_event_index[loop_positions[loop_level]], -1) var partial_loop_events := events.slice(rom_address_to_event_index[loop_positions[loop_level]], rom_address_to_event_index[break_pos]-1) @@ -211,7 +216,7 @@ func unroll_track(buffer: StreamPeerBuffer, bgm_start_pos: int, track_start_pos: loop_level -= 1 else: # Infinite loop, convert it to a GOTO and return - print('ended with infinite loop') + print_info('ended with infinite loop') events.append([EventType.GOTO, rom_address_to_event_index[loop_break_targets[loop_level]]]) loop_level -= 1 break @@ -225,10 +230,10 @@ func unroll_track(buffer: StreamPeerBuffer, bgm_start_pos: int, track_start_pos: loop_break_positions[loop_level] = pos loop_break_targets[loop_level] = target_pos if on_loop != loop_counts[loop_level]: - print('LOOP_BREAK found in track %s at 0x%06X:0x%06X:0x%06X: 0x%06X to 0x%06X on loop %d of %d' % [bgm_id, bgm_start_pos, track_start_pos, bgm_end_pos, pos, target_pos, on_loop, loop_counts[loop_level]]) + print_info('LOOP_BREAK found in track %s at 0x%06X:0x%06X:0x%06X: 0x%06X to 0x%06X on loop %d of %d' % [bgm_id, bgm_start_pos, track_start_pos, bgm_end_pos, pos, target_pos, on_loop, loop_counts[loop_level]]) loop_counts[loop_level] = on_loop else: - print('LOOP_BREAK found in track %s at 0x%06X:0x%06X:0x%06X: 0x%06X to 0x%06X on last loop' % [bgm_id, bgm_start_pos, track_start_pos, bgm_end_pos, pos, target_pos]) + print_info('LOOP_BREAK found in track %s at 0x%06X:0x%06X:0x%06X: 0x%06X to 0x%06X on last loop' % [bgm_id, bgm_start_pos, track_start_pos, bgm_end_pos, pos, target_pos]) if target_pos in rom_address_to_event_index: print_debug('LOOP_BREAK is a past address, this is either malformed or an infinite loop') break @@ -242,7 +247,7 @@ func unroll_track(buffer: StreamPeerBuffer, bgm_start_pos: int, track_start_pos: target_pos += 0x010000 var event_num = rom_address_to_event_index.get(target_pos, -1) if event_num >= 0: - print('Infinite GOTO found in track %s at 0x%06X:0x%06X:0x%06X: 0x%06X to 0x%06X (event number %d)' % [bgm_id, bgm_start_pos, track_start_pos, bgm_end_pos, pos, target_pos, event_num]) + print_info('Infinite GOTO found in track %s at 0x%06X:0x%06X:0x%06X: 0x%06X to 0x%06X (event number %d)' % [bgm_id, bgm_start_pos, track_start_pos, bgm_end_pos, pos, target_pos, event_num]) events.append([EventType.GOTO, rom_address_to_event_index[target_pos]]) break else: @@ -253,10 +258,10 @@ func unroll_track(buffer: StreamPeerBuffer, bgm_start_pos: int, track_start_pos: event_idx += 1 # DEBUG: Report if track doesn't end all the contained loops if loop_level >= 0: - print('track %s ended on loop_level %d (0x%06X)' % [bgm_id, loop_level, loop_positions[loop_level]]) + print_info('track %s ended on loop_level %d (0x%06X)' % [bgm_id, loop_level, loop_positions[loop_level]]) # DEBUG: Report total note duration and repeat note duration if events: - print('%d events' % len(events)) + print_info('%d events' % len(events)) var total_note_dur := 0 var total_notes := 0 var total_rests := 0 @@ -272,7 +277,7 @@ func unroll_track(buffer: StreamPeerBuffer, bgm_start_pos: int, track_start_pos: for e in events.slice(events[-1][1], -1): if e[0] == EventType.NOTE: repeat_note_dur += e[2] - print('track %s has duration %d and repeat duration %d (intro %d) - %d notes %d rests' % [bgm_id, total_note_dur, repeat_note_dur, total_note_dur-repeat_note_dur, total_notes, total_rests]) + print_info('track %s has duration %d and repeat duration %d (intro %d) - %d notes %d rests' % [bgm_id, total_note_dur, repeat_note_dur, total_note_dur-repeat_note_dur, total_notes, total_rests]) else: - print('track %s has duration %d - %d notes %d rests' % [bgm_id, total_note_dur, total_notes, total_rests]) + print_info('track %s has duration %d - %d notes %d rests' % [bgm_id, total_note_dur, total_notes, total_rests]) return events diff --git a/shaders/audio_renderer.gdshader b/shaders/audio_renderer.gdshader new file mode 100644 index 0000000..7392a4e --- /dev/null +++ b/shaders/audio_renderer.gdshader @@ -0,0 +1,57 @@ +shader_type canvas_item; +render_mode blend_premul_alpha; +const float TEX_SIZE = 4096.0; +const float UV_QUANTIZE = TEX_SIZE; +// I feel like these magic numbers are a bit more intuitive in hex +const float x00FF = float(0x00FF); // 255.0 +const float x0100 = float(0x0100); // 256.0 +const float x7FFF = float(0x7FFF); // 32767.0 +const float x8000 = float(0x8000); // 32768.0 +const float xFF00 = float(0xFF00); // 65280.0 +const float xFFFF = float(0xFFFF); // 65535.0 +const float x10000 = float(0x10000); // 65536.0 + +const vec2 INT16_DOT_BE = vec2(xFF00, x00FF); +const vec2 INT16_DOT_LE = vec2(x00FF, xFF00); +uniform sampler2D tex : hint_normal; + +float unpack_int16(vec2 int16) { + // Convert packed 2byte integer, sampled as two [0.0, 1.0] range floats, + // to the original int value [-32768, 32767] or [0, 65535] but in float32 + float unsigned = dot(int16, INT16_DOT_LE); + return unsigned - (unsigned < x7FFF ? 0.0 : x10000); +} + +float rescale_int16(float int16) { + // Rescale from [-32768, 32767] to [-1.0, 1.0) + return int16 / x8000; +} + +vec2 pack_float_to_int16(float value) { + // Convert a float in range [-1.0, 1.0) to a signed 2byte integer [-32768, 32767] packed into two [0.0, 1.0] floats + float scaled = value * x8000; + float unsigned = scaled + (scaled < 0.0 ? x10000 : 0.0); + float unsigned_div_256 = unsigned / x0100; + float MSB = trunc(unsigned_div_256) / x00FF; + float LSB = fract(unsigned_div_256) * x0100 / x00FF; + return vec2(LSB, MSB); +} + +vec4 test_writeback(vec2 uv) { + // Test importing and exporting the samples, + // and exporting a value derived from the UV + vec4 output; + float sample_1 = rescale_int16(unpack_int16(texture(tex, uv).xw)); + float sample_2 = rescale_int16(dot(trunc(uv*TEX_SIZE), vec2(1.0, TEX_SIZE))); + output.xy = pack_float_to_int16(sample_1); + output.zw = pack_float_to_int16(sample_2); + return output; +} + +void fragment() { + // GLES2 + vec2 uv = vec2(UV.x, 1.0-UV.y); + uv = (trunc(uv*UV_QUANTIZE)+0.5)/UV_QUANTIZE; + + COLOR.xyzw = test_writeback(uv); +} diff --git a/test/audio_renderer.gd b/test/audio_renderer.gd new file mode 100644 index 0000000..8aadbe1 --- /dev/null +++ b/test/audio_renderer.gd @@ -0,0 +1,70 @@ +extends Control + +var viewport: Viewport +var render_queue: Array # of PoolByteArrays +var result_queue: Array # of PoolByteArrays +var current_image: Image +var current_tex: ImageTexture # Needed to prevent GC before draw +var waiting_for_viewport: bool +var done_first_draw: bool + +func _ready() -> void: + self.viewport = get_parent() + self.render_queue = [] + self.result_queue = [] + self.waiting_for_viewport = false + self.done_first_draw = false + self.current_image = Image.new() + self.current_tex = ImageTexture.new() + +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 not self.render_queue: + return + + # Draw the next ImageTexture + var data: PoolByteArray = self.render_queue.pop_front() + print(data.subarray(0, 15)) + self.current_image.create_from_data(4096, 4096, false, Image.FORMAT_LA8, data) + self.current_tex.create_from_image(self.current_image, Texture.FLAG_FILTER) + self.material.set_shader_param('tex', self.current_tex) + draw_texture(self.current_tex, Vector2.ZERO) + self.waiting_for_viewport = true # Grab the result next draw + +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() + + # 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) + + self.result_queue.append(result_bytes) + self.waiting_for_viewport = false + +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]) diff --git a/test/audio_system.tscn b/test/audio_system.tscn index c562c74..c92fcd3 100644 --- a/test/audio_system.tscn +++ b/test/audio_system.tscn @@ -1,11 +1,31 @@ -[gd_scene load_steps=3 format=2] +[gd_scene load_steps=6 format=2] [ext_resource path="res://test/audio_system.gd" type="Script" id=1] [ext_resource path="res://theme/menu_theme.tres" type="Theme" id=2] +[ext_resource path="res://test/audio_renderer.gd" type="Script" id=3] +[ext_resource path="res://shaders/audio_renderer.gdshader" type="Shader" id=4] + +[sub_resource type="ShaderMaterial" id=2] +shader = ExtResource( 4 ) [node name="audio_system" type="Node2D"] script = ExtResource( 1 ) +[node name="viewport_audio_renderer" type="Viewport" parent="."] +size = Vector2( 4096, 4096 ) +size_override_stretch = true +transparent_bg = true +handle_input_locally = false +hdr = false +render_target_update_mode = 3 + +[node name="audio_renderer" type="Control" parent="viewport_audio_renderer"] +unique_name_in_owner = true +material = SubResource( 2 ) +margin_right = 40.0 +margin_bottom = 40.0 +script = ExtResource( 3 ) + [node name="inst_buttons" type="ReferenceRect" parent="."] margin_right = 348.0 margin_bottom = 118.0 @@ -42,4 +62,5 @@ margin_top = 192.0 margin_right = 315.0 margin_bottom = 216.0 theme = ExtResource( 2 ) +pressed = true text = "HACK: Loop Extension"