Skip to content

Commit c178252

Browse files
committed
perf: optimize 3D CRT Oscilloscope and Neon Cuboids shaders, add benchmarking
1 parent 4c019f4 commit c178252

5 files changed

Lines changed: 201 additions & 53 deletions

File tree

src/main.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ struct Args {
4646

4747
#[arg(long, default_value_t = false)]
4848
gpu_fft: bool,
49+
50+
#[arg(long)]
51+
bench: Option<u32>,
4952
}
5053

5154
struct Tui {
@@ -159,14 +162,14 @@ fn main() -> Result<(), Box<dyn Error>> {
159162
eprintln!("App error: {:?}", err);
160163
}
161164
} else {
162-
pollster::block_on(run_gui(app_state, initial_stream, args.fullscreen, args.gpu_fft));
165+
pollster::block_on(run_gui(app_state, initial_stream, args.fullscreen, args.gpu_fft, args.bench));
163166
}
164167

165168
Ok(())
166169
}
167170

168171
#[allow(unused_variables, unused_assignments)]
169-
async fn run_gui(app_state: Arc<Mutex<AppState>>, mut active_stream: Option<audio::PlaybackHandle>, is_fullscreen: bool, use_gpu_fft: bool) {
172+
async fn run_gui(app_state: Arc<Mutex<AppState>>, mut active_stream: Option<audio::PlaybackHandle>, is_fullscreen: bool, use_gpu_fft: bool, bench: Option<u32>) {
170173
if use_gpu_fft {
171174
let mut state = app_state.lock().unwrap();
172175
state.gpu_fft = true;
@@ -258,6 +261,7 @@ async fn run_gui(app_state: Arc<Mutex<AppState>>, mut active_stream: Option<audi
258261
let mut rfd_pending = false;
259262

260263
let mut modifiers = winit::keyboard::ModifiersState::empty();
264+
let mut frame_count = 0u32;
261265

262266
#[allow(deprecated)]
263267
let _ = event_loop.run(move |event, elwt| {
@@ -803,6 +807,19 @@ async fn run_gui(app_state: Arc<Mutex<AppState>>, mut active_stream: Option<audi
803807
state.stats.gpu_fft_us = state.stats.gpu_fft_us * 0.9 + ft * 0.1;
804808
}
805809
}
810+
811+
if let Some(bench_frames) = bench {
812+
frame_count += 1;
813+
if frame_count >= bench_frames {
814+
let vis_def = &crate::state::VISUALIZERS[state.current_visualizer_idx];
815+
println!("BENCHMARK_RESULT_VISUALIZER: {}", vis_def.name);
816+
println!("BENCHMARK_RESULT_FPS: {:.2}", state.current_fps);
817+
println!("BENCHMARK_RESULT_SHADER_US: {:.2}", state.stats.shader_us);
818+
println!("BENCHMARK_RESULT_RENDER_US: {:.2}", state.stats.render_us);
819+
elwt.exit();
820+
return;
821+
}
822+
}
806823

807824
// Process engine actions
808825
match action {

src/shaders/vis_3doscilloscope.wgsl

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ fn project_3d(p3: vec3<f32>, ro: vec3<f32>, u: vec3<f32>, v_cam: vec3<f32>, w: v
3333
let dir = p3 - ro;
3434
let dist_w = dot(dir, w);
3535
if dist_w <= 0.001 { return vec3<f32>(999.0, 999.0, dist_w); }
36-
let proj_x = dot(dir, u) / dist_w;
37-
let proj_y = dot(dir, v_cam) / dist_w;
36+
let inv_dist_w = 1.0 / dist_w;
37+
let proj_x = dot(dir, u) * inv_dist_w;
38+
let proj_y = dot(dir, v_cam) * inv_dist_w;
3839
// Negate proj_y so +Z (up) maps to -Y (top of screen in our coords)
3940
return vec3<f32>(proj_x, -proj_y, dist_w);
4041
}
@@ -74,20 +75,25 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
7475

7576
let num_lines = min(max(1u, audio.waveform_history_size), 144u);
7677
let res = f32(max(audio.waveform_resolution, 128u));
78+
let inv_res_minus_1 = 19.2 / (res - 1.0);
7779

78-
for (var i = 0u; i < num_lines; i = i + 1u) {
79-
// Z layout: back to front. i=0 is back (oldest), i=num_lines-1 is front (newest).
80-
let y_line = mix(9.48, -1.8, f32(i) / f32(num_lines - 1u));
81-
let hist_idx = i;
82-
83-
let t = (y_line - ro.y) / rd.y;
84-
if t <= 0.0 {
85-
// Lines are iterated back-to-front (decreasing Y). When rd.y > 0,
86-
// once t goes negative all remaining lines are also behind the camera.
87-
if rd.y > 0.0 { break; }
88-
continue;
89-
}
90-
{
80+
if rd.y > 0.0 {
81+
let t_min = max(0.0, (ro.z - 1.5) / max(0.2 - rd.z, 0.00001));
82+
let t_max = (ro.z + 1.5) / max(-0.2 - rd.z, 0.00001);
83+
84+
for (var i = 0u; i < num_lines; i = i + 1u) {
85+
// Z layout: back to front. i=0 is back (oldest), i=num_lines-1 is front (newest).
86+
let y_line = mix(9.48, -1.8, f32(i) / f32(num_lines - 1u));
87+
let hist_idx = i;
88+
89+
let t = (y_line - ro.y) / rd.y;
90+
if t < t_min {
91+
break;
92+
}
93+
if t > t_max {
94+
continue;
95+
}
96+
9197
let hit_x = ro.x + rd.x * t;
9298

9399
let edge_fade = smoothstep(9.6, 6.6, abs(hit_x));
@@ -103,23 +109,25 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
103109
// Scale down slightly since we have 3x more lines now
104110
let search_radius = clamp(i32(t * 0.4), 1, 6);
105111
let start_idx = max(0, idx - search_radius);
106-
let end_idx = min(i32(res) - 1i, idx + search_radius);
112+
let end_idx = min(i32(res) - 2, idx + search_radius);
107113

108114
var min_dist = 1000.0;
109115

116+
let hist_offset = clamp(hist_idx, 0u, max(1u, audio.waveform_history_size) - 1u) * 2048u;
117+
110118
var j_u = u32(start_idx);
111-
var x_prev = -9.6 + f32(j_u) / (res - 1.0) * 19.2;
119+
var x_prev = -9.6 + f32(j_u) * inv_res_minus_1;
112120
var mask_prev = smoothstep(9.6, 6.6, abs(x_prev));
113-
var v_prev = get_waveform(hist_idx, j_u);
121+
var v_prev = waveform_history[hist_offset + j_u];
114122
var p_prev = v_prev * mask_prev * 1.2;
115123
var p3_prev = vec3<f32>(x_prev, y_line, p_prev);
116124
var proj_prev = project_3d(p3_prev, ro, u, v_cam, w);
117125

118126
for (var j = start_idx; j <= end_idx; j = j + 1) {
119127
j_u = u32(j + 1);
120-
let x_curr = -9.6 + f32(j_u) / (res - 1.0) * 19.2;
128+
let x_curr = -9.6 + f32(j_u) * inv_res_minus_1;
121129
let mask_curr = smoothstep(9.6, 6.6, abs(x_curr));
122-
let v_curr = get_waveform(hist_idx, j_u);
130+
let v_curr = waveform_history[hist_offset + j_u];
123131
let p_curr = v_curr * mask_curr * 1.2;
124132
let p3_curr = vec3<f32>(x_curr, y_line, p_curr);
125133
let proj_curr = project_3d(p3_curr, ro, u, v_cam, w);
@@ -150,7 +158,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
150158

151159
// Sample waveform height at hit point for color grading
152160
let hit_wave_idx = u32(clamp((hit_x + 9.6) / 19.2 * (res - 1.0), 0.0, res - 1.0));
153-
let wave_height = clamp(abs(get_waveform(hist_idx, hit_wave_idx)) * 2.0, 0.0, 1.0);
161+
let wave_height = clamp(abs(waveform_history[hist_offset + hit_wave_idx]) * 2.0, 0.0, 1.0);
154162
let line_amber = mix(amber_lo, amber_hi, wave_height);
155163

156164
accumulated_color += line_amber * (core + bloom) * depth_fade * edge_fade * age_fade;

src/shaders/vis_cuboids.wgsl

Lines changed: 92 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@ fn sdWireframeBox(p: vec3<f32>, b: vec3<f32>, e: f32) -> f32 {
2121
}
2222

2323
fn get_history_amplitude(bin: u32, steps_ago: u32) -> f32 {
24-
let row = (audio.heatmap_row + 1024u - (steps_ago % 1024u)) % 1024u;
25-
let clamped_bin = clamp(bin, 0u, 255u);
26-
return textureLoad(history_tex, vec2<i32>(i32(clamped_bin), i32(row)), 0).x;
24+
let row = (audio.heatmap_row + 1024u - (steps_ago & 1023u)) & 1023u;
25+
return textureLoad(history_tex, vec2<i32>(i32(bin), i32(row)), 0).x;
2726
}
2827

2928
struct SceneResult {
@@ -32,6 +31,44 @@ struct SceneResult {
3231
amp: f32,
3332
};
3433

34+
fn evaluate_cell(p: vec3<f32>, ix: i32, iz: i32, res_in: SceneResult) -> SceneResult {
35+
var res = res_in;
36+
let x_spacing = 1.35;
37+
let z_spacing = 1.35;
38+
let cx = f32(ix) * x_spacing;
39+
let cz = f32(iz) * z_spacing;
40+
41+
let dist_from_center = sqrt(f32(ix * ix + iz * iz));
42+
let bin = clamp(u32(dist_from_center * 10.0) + 4u, 0u, 255u);
43+
let steps_ago = u32(dist_from_center * 3.0);
44+
let amp = get_history_amplitude(bin, steps_ago);
45+
let shift = clamp(amp / 100.0, 0.0, 1.0) * 1.5;
46+
47+
let thick = 0.009 + clamp(amp / 100.0, 0.0, 1.0) * 0.007;
48+
let b = vec3<f32>(0.22, 0.85, 0.22);
49+
50+
// Mathematically proven: Only the box on the same side of y=0.0 as p.y can possibly be the closest.
51+
if (p.y < 0.0) {
52+
let y_center_b = -2.9 + shift;
53+
let d_b = sdWireframeBox(p - vec3<f32>(cx, y_center_b, cz), b, thick);
54+
if (d_b < res.d) {
55+
res.d = d_b;
56+
res.hit_type = 1;
57+
res.amp = amp;
58+
}
59+
} else {
60+
let y_center_t = 2.9 - shift;
61+
let d_t = sdWireframeBox(p - vec3<f32>(cx, y_center_t, cz), b, thick);
62+
if (d_t < res.d) {
63+
res.d = d_t;
64+
res.hit_type = 2;
65+
res.amp = amp;
66+
}
67+
}
68+
69+
return res;
70+
}
71+
3572
fn map_scene(p: vec3<f32>) -> SceneResult {
3673
let x_spacing = 1.35;
3774
let z_spacing = 1.35;
@@ -44,8 +81,54 @@ fn map_scene(p: vec3<f32>) -> SceneResult {
4481
res.hit_type = 0;
4582
res.amp = 0.0;
4683

47-
for (var dx = -1; dx <= 1; dx = dx + 1) {
48-
for (var dz = -1; dz <= 1; dz = dz + 1) {
84+
// Early return if completely out of bounds of the grid
85+
if (ix_center < -16 || ix_center > 16 || iz_center < -21 || iz_center > 7) {
86+
return res;
87+
}
88+
89+
let cx_center = f32(ix_center) * x_spacing;
90+
let cz_center = f32(iz_center) * z_spacing;
91+
92+
// 1. Evaluate center cell first to establish a tight bound
93+
if (ix_center >= -15 && ix_center <= 15 && iz_center >= -20 && iz_center <= 6) {
94+
res = evaluate_cell(p, ix_center, iz_center, res);
95+
}
96+
97+
// 2. Early return if the current best distance is closer than any neighbor cell could possibly be.
98+
// The closest horizontal distance to any box in a neighbor is at least 1.13 - distance_to_center_cell.
99+
let dx_center_dist = abs(p.x - cx_center);
100+
let dz_center_dist = abs(p.z - cz_center);
101+
let d_neighbor_min = min(1.13 - dx_center_dist, 1.13 - dz_center_dist);
102+
if (res.d < d_neighbor_min) {
103+
return res;
104+
}
105+
106+
// 3. Evaluate neighbor cells only if they are within the dynamic bounds and closer.
107+
// If res.d <= 1.13, we can skip neighbors on the opposite side of the center cell.
108+
var dx_start = -1;
109+
var dx_end = 1;
110+
if (res.d <= 1.13) {
111+
if (p.x >= cx_center) {
112+
dx_start = 0;
113+
} else {
114+
dx_end = 0;
115+
}
116+
}
117+
118+
var dz_start = -1;
119+
var dz_end = 1;
120+
if (res.d <= 1.13) {
121+
if (p.z >= cz_center) {
122+
dz_start = 0;
123+
} else {
124+
dz_end = 0;
125+
}
126+
}
127+
128+
for (var dx = dx_start; dx <= dx_end; dx = dx + 1) {
129+
for (var dz = dz_start; dz <= dz_end; dz = dz + 1) {
130+
if (dx == 0 && dz == 0) { continue; }
131+
49132
let ix = ix_center + dx;
50133
let iz = iz_center + dz;
51134

@@ -54,32 +137,11 @@ fn map_scene(p: vec3<f32>) -> SceneResult {
54137
let cx = f32(ix) * x_spacing;
55138
let cz = f32(iz) * z_spacing;
56139

57-
let dist_from_center = length(vec2<f32>(f32(ix), f32(iz)));
58-
let bin = clamp(u32(dist_from_center * 10.0) + 4u, 0u, 255u);
59-
let steps_ago = u32(dist_from_center * 3.0);
60-
let amp = get_history_amplitude(bin, steps_ago);
61-
let shift = clamp(amp / 100.0, 0.0, 1.0) * 1.5;
62-
63-
let thick = 0.009 + clamp(amp / 100.0, 0.0, 1.0) * 0.007;
64-
let b = vec3<f32>(0.22, 0.85, 0.22);
65-
66-
// Bottom Box
67-
let y_center_b = -2.9 + shift;
68-
let d_b = sdWireframeBox(p - vec3<f32>(cx, y_center_b, cz), b, thick);
69-
if (d_b < res.d) {
70-
res.d = d_b;
71-
res.hit_type = 1;
72-
res.amp = amp;
73-
}
140+
// Strict mathematical lower bound check: distance to cell boundary
141+
let d_horizontal = max(abs(p.x - cx) - 0.22, abs(p.z - cz) - 0.22);
142+
if (d_horizontal >= res.d) { continue; }
74143

75-
// Top Box
76-
let y_center_t = 2.9 - shift;
77-
let d_t = sdWireframeBox(p - vec3<f32>(cx, y_center_t, cz), b, thick);
78-
if (d_t < res.d) {
79-
res.d = d_t;
80-
res.hit_type = 2;
81-
res.amp = amp;
82-
}
144+
res = evaluate_cell(p, ix, iz, res);
83145
}
84146
}
85147

test_sine.wav

172 KB
Binary file not shown.

tests/test_benchmark.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use std::process::Command;
2+
use std::str;
3+
4+
#[test]
5+
fn test_visualizer_benchmark() {
6+
let visualizers = vec!["3doscilloscope", "3doscilloscope_freq"];
7+
8+
println!("\n==================================================");
9+
println!("RUNNING VISUALIZER BENCHMARKS (500 FRAMES)...");
10+
println!("==================================================");
11+
12+
for vis in visualizers {
13+
let output = Command::new("cargo")
14+
.args(&[
15+
"run",
16+
"--release",
17+
"--",
18+
"test_sine.wav",
19+
"--vis",
20+
vis,
21+
"--bench",
22+
"500",
23+
])
24+
.output()
25+
.expect("Failed to execute cargo run");
26+
27+
let stdout = str::from_utf8(&output.stdout).unwrap();
28+
let stderr = str::from_utf8(&output.stderr).unwrap();
29+
30+
if !output.status.success() {
31+
eprintln!("Benchmark failed for visualizer: {}", vis);
32+
eprintln!("STDOUT:\n{}", stdout);
33+
eprintln!("STDERR:\n{}", stderr);
34+
panic!("Visualizer benchmark command exited with error status");
35+
}
36+
37+
// Parse benchmark results from stdout
38+
let mut parsed_name = String::new();
39+
let mut parsed_fps = String::new();
40+
let mut parsed_shader_us = String::new();
41+
let mut parsed_render_us = String::new();
42+
43+
for line in stdout.lines() {
44+
if let Some(val) = line.strip_prefix("BENCHMARK_RESULT_VISUALIZER: ") {
45+
parsed_name = val.to_string();
46+
} else if let Some(val) = line.strip_prefix("BENCHMARK_RESULT_FPS: ") {
47+
parsed_fps = val.to_string();
48+
} else if let Some(val) = line.strip_prefix("BENCHMARK_RESULT_SHADER_US: ") {
49+
parsed_shader_us = val.to_string();
50+
} else if let Some(val) = line.strip_prefix("BENCHMARK_RESULT_RENDER_US: ") {
51+
parsed_render_us = val.to_string();
52+
}
53+
}
54+
55+
println!("Visualizer: {}", parsed_name);
56+
println!("FPS: {}", parsed_fps);
57+
println!("Shader Time: {} us", parsed_shader_us);
58+
println!("Render Time: {} us", parsed_render_us);
59+
println!("--------------------------------------------------");
60+
}
61+
}

0 commit comments

Comments
 (0)