Skip to content

Commit 90227ac

Browse files
committed
feat: add visualizer list CLI option, improve visualizer selection, refactor shader culling/lighting, and update release profile configurations.
1 parent 2942847 commit 90227ac

7 files changed

Lines changed: 114 additions & 64 deletions

File tree

.github/workflows/linux-build.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ jobs:
3232
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
3333
3434
- name: Build RustTracker
35+
env:
36+
CARGO_PROFILE_RELEASE_LTO: "true"
37+
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "1"
38+
CARGO_PROFILE_RELEASE_STRIP: "true"
3539
run: |
3640
source "$HOME/.cargo/env"
3741
cargo build --release
@@ -65,6 +69,10 @@ jobs:
6569
sudo apt-get install -y gcc g++ binutils pkg-config libasound2-dev libwayland-dev libx11-dev libxkbcommon-x11-dev libudev-dev libavcodec-dev libavformat-dev libavutil-dev libavdevice-dev libavfilter-dev libopenmpt-dev clang libclang-dev
6670
6771
- name: Build RustTracker
72+
env:
73+
CARGO_PROFILE_RELEASE_LTO: "true"
74+
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "1"
75+
CARGO_PROFILE_RELEASE_STRIP: "true"
6876
run: |
6977
cargo build --release
7078
strip target/release/rusttracker

.github/workflows/macos.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jobs:
2222
CPATH: /opt/homebrew/include
2323
PKG_CONFIG_PATH: /opt/homebrew/lib/pkgconfig:/opt/homebrew/share/pkgconfig
2424
BINDGEN_EXTRA_CLANG_ARGS: "-I/opt/homebrew/include"
25+
CARGO_PROFILE_RELEASE_LTO: "true"
26+
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "1"
27+
CARGO_PROFILE_RELEASE_STRIP: "true"
2528
run: cargo build --release
2629

2730
- name: Package Application

.github/workflows/windows-build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ jobs:
3434
- name: Build RustTracker
3535
env:
3636
BINDGEN_EXTRA_CLANG_ARGS: "-target x86_64-pc-windows-gnu"
37+
CARGO_PROFILE_RELEASE_LTO: "true"
38+
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "1"
39+
CARGO_PROFILE_RELEASE_STRIP: "true"
3740
run: |
3841
cargo build --release
3942

Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ objc2 = "0.6.4"
6565
objc2-core-audio = "0.3.2"
6666

6767
[profile.release]
68-
opt-level = "z"
69-
lto = true
70-
codegen-units = 1
71-
strip = true
68+
opt-level = 3
69+
lto = false
70+
codegen-units = 16
71+
strip = false

src/engine.rs

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -773,12 +773,6 @@ impl<'a> VulkanEngine<'a> {
773773
),
774774
};
775775

776-
let blend_state = if vis_def.id == 17 {
777-
Some(wgpu::BlendState::ALPHA_BLENDING)
778-
} else {
779-
Some(wgpu::BlendState::REPLACE)
780-
};
781-
782776
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
783777
label: Some(&format!("Render Pipeline {}", i)),
784778
layout: Some(layout),
@@ -793,15 +787,15 @@ impl<'a> VulkanEngine<'a> {
793787
entry_point: Some("fs_main"),
794788
targets: &[Some(wgpu::ColorTargetState {
795789
format: config.format,
796-
blend: blend_state,
790+
blend: Some(wgpu::BlendState::REPLACE),
797791
write_mask: wgpu::ColorWrites::ALL,
798792
})],
799793
compilation_options: wgpu::PipelineCompilationOptions::default(),
800794
}),
801795
primitive,
802796
depth_stencil: Some(wgpu::DepthStencilState {
803797
format: wgpu::TextureFormat::Depth32Float,
804-
depth_write_enabled: Some(vis_def.id != 17),
798+
depth_write_enabled: Some(true),
805799
depth_compare: Some(wgpu::CompareFunction::LessEqual),
806800
stencil: wgpu::StencilState::default(),
807801
bias: wgpu::DepthBiasState::default(),
@@ -1093,17 +1087,12 @@ impl<'a> VulkanEngine<'a> {
10931087
10941088
@group(0) @binding(0) var<uniform> audio: AudioUniforms;
10951089
1096-
struct VertexOutput {
1097-
@builtin(position) clip_position: vec4<f32>,
1098-
@location(0) uv: vec2<f32>,
1099-
};
1100-
11011090
@vertex
1102-
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> VertexOutput {
1091+
fn vs_background(@builtin(vertex_index) in_vertex_index: u32) -> VertexOutput {
11031092
var out: VertexOutput;
11041093
let u = f32((in_vertex_index << 1u) & 2u);
11051094
let v = f32(in_vertex_index & 2u);
1106-
out.clip_position = vec4<f32>(u * 2.0 - 1.0, -(v * 2.0 - 1.0), 0.0, 1.0);
1095+
out.clip_position = vec4<f32>(u * 2.0 - 1.0, -(v * 2.0 - 1.0), 1.0, 1.0);
11071096
out.uv = vec2<f32>(u, v);
11081097
return out;
11091098
}
@@ -1202,7 +1191,7 @@ impl<'a> VulkanEngine<'a> {
12021191
layout: Some(&render_pipeline_layout),
12031192
vertex: wgpu::VertexState {
12041193
module: &crt_background_shader,
1205-
entry_point: Some("vs_main"),
1194+
entry_point: Some("vs_background"),
12061195
buffers: &[],
12071196
compilation_options: wgpu::PipelineCompilationOptions::default(),
12081197
},

src/main.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ struct Args {
4444
#[arg(long)]
4545
vis: Option<String>,
4646

47+
#[arg(long, default_value_t = false)]
48+
list_vis: bool,
49+
4750
#[arg(long, default_value_t = false)]
4851
gpu_fft: bool,
4952

@@ -94,6 +97,20 @@ fn main() -> Result<(), Box<dyn Error>> {
9497
}));
9598

9699
let args = Args::parse();
100+
if args.list_vis {
101+
println!("{:<4} {:<24} {:<28} {}", "ID", "Short Name", "Name", "Description");
102+
println!("{}", "-".repeat(90));
103+
for v in crate::state::VISUALIZERS {
104+
let short_name = v.filename
105+
.strip_prefix("vis_")
106+
.unwrap_or(v.filename)
107+
.strip_suffix(".wgsl")
108+
.unwrap_or(v.filename);
109+
println!("{:<4} {:<24} {:<28} {}", v.id, short_name, v.name, v.description);
110+
}
111+
return Ok(());
112+
}
113+
97114
let title = if args.mic {
98115
"Microphone Input".to_string()
99116
} else {
@@ -126,9 +143,23 @@ fn main() -> Result<(), Box<dyn Error>> {
126143

127144
if let Some(vis) = &args.vis {
128145
let vis_lower = vis.to_lowercase();
129-
if let Some(idx) = crate::state::VISUALIZERS.iter().position(|v| v.filename.to_lowercase().contains(&vis_lower) || v.name.to_lowercase().contains(&vis_lower)) {
146+
let matched_idx = crate::state::VISUALIZERS.iter().position(|v| {
147+
let short_name = v.filename
148+
.strip_prefix("vis_")
149+
.unwrap_or(v.filename)
150+
.strip_suffix(".wgsl")
151+
.unwrap_or(v.filename)
152+
.to_lowercase();
153+
v.id.to_string() == vis_lower
154+
|| v.name.to_lowercase() == vis_lower
155+
|| short_name == vis_lower
156+
});
157+
158+
if let Some(idx) = matched_idx {
130159
state.current_visualizer_idx = idx;
131160
state.visualizer_mode = crate::state::VISUALIZERS[idx].id;
161+
} else {
162+
eprintln!("Warning: Visualizer '{}' not found. Launching with default visualizer.", vis);
132163
}
133164
}
134165
}

src/shaders/vis_cuboids.wgsl

Lines changed: 59 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ struct VertexInput {
1818
struct VertexOutput3D {
1919
@builtin(position) clip_position: vec4<f32>,
2020
@location(0) local_pos: vec3<f32>,
21-
@location(1) ndc: vec2<f32>,
21+
@location(1) @interpolate(linear) ndc: vec2<f32>,
2222
@location(2) amp: f32,
23+
@location(3) depth: f32,
2324
}
2425

2526
@vertex
@@ -53,9 +54,9 @@ fn vs_main_3d(in: VertexInput, @builtin(instance_index) inst_idx: u32) -> Vertex
5354

5455
let y_center = select(-1.4 + shift, 4.4 - shift, is_top);
5556

56-
// Scale unit box to match raymarching size b = (0.22, 0.85, 0.22)
57+
// Scale unit box to match spacing exactly (eliminating gaps)
5758
// UnitBox is [-0.5, 0.5]
58-
let scaled = in.position * vec3<f32>(0.44, 1.7, 0.44);
59+
let scaled = in.position * vec3<f32>(x_spacing, 1.7, x_spacing);
5960
let world_pos = scaled + vec3<f32>(world_x, y_center, world_z);
6061

6162
// Store local position for face edge detection
@@ -65,12 +66,28 @@ fn vs_main_3d(in: VertexInput, @builtin(instance_index) inst_idx: u32) -> Vertex
6566
// Project position
6667
var clip_pos = camera.proj_matrix * camera.view_matrix * vec4<f32>(world_pos, 1.0);
6768

68-
// Apply barrel distortion in clip space (NDC)
69-
let ndc = clip_pos.xy / clip_pos.w;
70-
let r2 = dot(ndc, ndc);
71-
let distorted_ndc = ndc * (1.0 + r2 * 0.055);
69+
out.depth = clip_pos.w;
7270

73-
clip_pos = vec4<f32>(distorted_ndc * clip_pos.w, clip_pos.z, clip_pos.w);
71+
// Project instance center to evaluate culling
72+
let inst_center = vec3<f32>(world_x, y_center, world_z);
73+
let center_clip = camera.proj_matrix * camera.view_matrix * vec4<f32>(inst_center, 1.0);
74+
75+
var should_cull = center_clip.w < 1.0;
76+
if (!should_cull) {
77+
let center_ndc = center_clip.xy / center_clip.w;
78+
should_cull = abs(center_ndc.x) > 2.2 || abs(center_ndc.y) > 2.2;
79+
}
80+
81+
var distorted_ndc = vec2<f32>(0.0);
82+
if (should_cull) {
83+
clip_pos = vec4<f32>(0.0, 0.0, 0.0, 0.0);
84+
} else {
85+
// Apply barrel distortion in clip space (NDC)
86+
let ndc = clamp(clip_pos.xy / clip_pos.w, vec2<f32>(-2.0), vec2<f32>(2.0));
87+
let r2 = min(2.0, dot(ndc, ndc));
88+
distorted_ndc = ndc * (1.0 + r2 * 0.055);
89+
clip_pos = vec4<f32>(distorted_ndc * clip_pos.w, clip_pos.z, clip_pos.w);
90+
}
7491

7592
out.clip_position = clip_pos;
7693
out.ndc = distorted_ndc;
@@ -95,65 +112,64 @@ fn fs_main(in: VertexOutput3D) -> @location(0) vec4<f32> {
95112
let bezel_mask = smoothstep(0.0, 0.03, border_dist);
96113

97114
// 2. Wireframe / Edge Detection on the Box Face
98-
let box_half = vec3<f32>(0.22, 0.85, 0.22);
115+
let box_half = vec3<f32>(0.675, 0.85, 0.675);
99116
let d = box_half - abs(in.local_pos);
100117

101-
// Find the second smallest element of d (distance to nearest edge on this face)
102-
let dist_to_edge = min(max(d.x, d.y), max(min(d.x, d.y), d.z));
118+
// Compute distance to nearest edge in pixels using screen-space derivatives
119+
let fw = fwidth(in.local_pos);
120+
var dist_pixels = vec3<f32>(1e6, 1e6, 1e6);
121+
if (fw.x > 1e-5) { dist_pixels.x = d.x / fw.x; }
122+
if (fw.y > 1e-5) { dist_pixels.y = d.y / fw.y; }
123+
if (fw.z > 1e-5) { dist_pixels.z = d.z / fw.z; }
124+
let pixel_dist = min(min(dist_pixels.x, dist_pixels.y), dist_pixels.z);
103125

104-
let thick = 0.009 + clamp(in.amp / 100.0, 0.0, 1.0) * 0.007;
105-
// Wide glow boundary for rich visual aesthetics
106-
let glow_width = 0.09 + clamp(in.amp / 100.0, 0.0, 1.0) * 0.04;
126+
// 3. Core and bloom/glow calculations (reacting to audio amplitude)
127+
let amp_factor = 1.0 + clamp(in.amp / 30.0, 0.0, 3.0);
107128

108-
if (dist_to_edge > glow_width) {
109-
discard;
110-
}
129+
let line_width = 1.5; // 1.5 pixels wide core
130+
let core_glow = smoothstep(line_width + 1.0, line_width - 1.0, pixel_dist);
131+
132+
// Exponential bloom/glow falloffs
133+
let bloom_glow = exp(-pixel_dist * 0.15) * amp_factor; // neon green glow
134+
let wide_bloom = exp(-pixel_dist * 0.05) * 0.4 * amp_factor; // wide faint bloom
111135

112-
// 3. Color & Alpha calculation
136+
// 4. Color calculation: Emissive bright core -> neon green glow -> deep solid green base fill
113137
let bass = clamp(audio.spectrum[0].x + audio.spectrum[1].x + audio.spectrum[2].x, 0.0, 1.0);
114138

115-
let base_green = vec3<f32>(0.02, 1.0, 0.38);
116-
let neon_green = mix(base_green, vec3<f32>(1.0, 1.0, 1.0), clamp(bass * 0.45, 0.0, 1.0));
139+
let core_col = vec3<f32>(0.8, 1.0, 0.9); // bright green-white core
140+
let neon_green = vec3<f32>(0.0, 1.0, 0.4); // vibrant neon green
141+
let deep_green = vec3<f32>(0.0, 0.08, 0.03); // deep solid green base fill
117142

118-
var final_color = vec3<f32>(0.0);
119-
var alpha = 0.0;
143+
let base_fill = deep_green * (0.6 + 0.8 * bass);
120144

121-
if (dist_to_edge < thick) {
122-
// Bright phosphor core
123-
let core_intensity = 1.3 + clamp(in.amp / 100.0, 0.0, 1.0) * 0.7;
124-
final_color = vec3<f32>(0.7, 1.0, 0.88) * core_intensity;
125-
alpha = 1.0;
126-
} else {
127-
// Outer glow
128-
let glow_factor = smoothstep(glow_width, thick, dist_to_edge);
129-
let glow_intensity = 0.35 + clamp(bass * 0.35, 0.0, 0.6);
130-
final_color = neon_green * glow_factor * glow_intensity;
131-
alpha = 0.85 * glow_factor;
132-
}
145+
var final_color = base_fill + neon_green * (bloom_glow * 1.5 + wide_bloom * 0.8) + core_col * (core_glow * 2.0);
146+
147+
// 5. Distance fade out (depth fog)
148+
let depth = in.depth;
149+
// Fade out to black between depth 8.0 and 32.0
150+
let fade = clamp((32.0 - depth) / (32.0 - 8.0), 0.0, 1.0);
151+
let fade_smooth = smoothstep(0.0, 1.0, fade);
152+
final_color = final_color * fade_smooth;
133153

134-
// 4. CRT Filter: Scanlines (applied to color & alpha so background shows through gaps)
154+
// 6. CRT Filter: Scanlines
135155
let scanline = 0.86 + 0.14 * cos(in.clip_position.y * 3.14159);
136156
final_color = final_color * scanline;
137-
alpha = alpha * scanline;
138157

139-
// 5. CRT Filter: Flicker
158+
// 7. CRT Filter: Flicker
140159
let flicker = 0.98 + 0.02 * sin(audio.time * 115.0);
141160
final_color = final_color * flicker;
142-
alpha = alpha * flicker;
143161

144-
// 6. CRT Filter: Analog static noise
162+
// 8. CRT Filter: Analog static noise
145163
let noise_val = hash21(in.clip_position.xy + fract(audio.smooth_time) * 149.0);
146164
let static_noise = noise_val * 0.022 * bezel_mask;
147165
final_color = final_color + vec3<f32>(static_noise);
148-
alpha = clamp(alpha + static_noise * 0.5, 0.0, 1.0);
149166

150167
// Apply bezel mask
151168
final_color = final_color * bezel_mask;
152-
alpha = alpha * bezel_mask;
153169

154-
// 7. Fitted ACES Tonemap
170+
// 9. Fitted ACES Tonemap
155171
var final_col = (final_color * (2.51 * final_color + 0.03)) / (final_color * (2.43 * final_color + 0.59) + 0.14);
156172
final_col = max(final_col, vec3<f32>(0.0));
157173

158-
return vec4<f32>(final_col, alpha);
174+
return vec4<f32>(final_col, 1.0);
159175
}

0 commit comments

Comments
 (0)