11const LOG10_E : f32 = 0 .4342944819 ;
2+ const LN_TO_DB : f32 = 4 .342944819 ;
3+ const DB_TO_LOG2 : f32 = 0 .3321928095 ;
4+ // Rg16Float cannot hold -140 dB power in one channel; G is a scaled fallback.
5+ const LOW_POWER_SCALE : f32 = 16777216 .0 ;
6+ const INV_LOW_POWER_SCALE : f32 = 0 .000000059604644775390625 ;
7+ const F16_MAX : f32 = 65504 .0 ;
28const LOG_KNEE_HZ : f32 = 20 .0 ;
39
410// Classic storage domain -- keep in sync with processor.rs CLASSIC_DB_STORE_*.
@@ -36,10 +42,9 @@ struct Uniforms {
3642 col_stride_u16 : u32 ,
3743 // FFT bin spacing (sample_rate / fft_size); only used by classic sampling.
3844 bin_hz : f32 ,
39- // Three u32 pads (vec3 would align to 16 B and desync the Rust layout).
45+ accum_width : f32 ,
46+ accum_height : f32 ,
4047 _pad0 : u32 ,
41- _pad1 : u32 ,
42- _pad2 : u32 ,
4348
4449 // (pos1, pos2, pos3, spread0), (spread1, spread2, spread3, spread4).
4550 // Stops 0 and 4 are constant 0.0 / 1.0
@@ -48,13 +53,23 @@ struct Uniforms {
4853 palette : array <vec4 <f32 >, 5 >,
4954}
5055
51- struct VertexOutput {
56+ struct AccumOutput {
5257 @builtin (position ) position : vec4 <f32 >,
53- @location (0 ) magnitude_db : f32 ,
54- @location (1 ) freq_hz : f32 ,
58+ @location (0 ) @interpolate (flat ) magnitude_db : f32 ,
59+ @location (1 ) @interpolate (flat ) freq_hz : f32 ,
60+ }
61+
62+ struct ClassicOutput {
63+ @builtin (position ) position : vec4 <f32 >,
64+ }
65+
66+ struct ResolveOutput {
67+ @builtin (position ) position : vec4 <f32 >,
68+ @location (0 ) accum_pos : vec2 <f32 >,
5569}
5670
5771@group (0 ) @binding (0 ) var < uniform > u : Uniforms ;
72+ @group (0 ) @binding (1 ) var accum_tex : texture_2d < f32 > ;
5873@group (0 ) @binding (2 ) var < storage , read > mags : array <u32 >;
5974
6075// (Glasberg & Moore 1990)
@@ -127,19 +142,6 @@ fn extents() -> vec2<f32> {
127142 );
128143}
129144
130- // Pre-rotation (time, freq) pos -> clip-space NDC under u.rotation, u.bounds.
131- fn place (pos : vec2 <f32 >, ext : vec2 <f32 >) -> vec4 <f32 > {
132- var rotated : vec2 <f32 >;
133- switch u . rotation {
134- case 1u : { rotated = vec2 <f32 >(ext . y - pos . y , pos . x ); }
135- case 2u : { rotated = vec2 <f32 >(ext . x - pos . x , ext . y - pos . y ); }
136- case 3u : { rotated = vec2 <f32 >(pos . y , ext . x - pos . x ); }
137- default : { rotated = pos ; }
138- }
139- rotated += u . bounds . xy ;
140- return vec4 <f32 >(rotated . x * u . clip_scale . x - 1 .0 , 1 .0 - rotated . y * u . clip_scale . y , 0 .0 , 1 .0 );
141- }
142-
143145// `col_stride_u16` rounds ppc up to even so u16 pairs never straddle u32 words.
144146fn unpack_mag (slot : u32 , bin_in_col : u32 ) -> f32 {
145147 let idx = slot * u . col_stride_u16 + bin_in_col ;
@@ -150,30 +152,43 @@ fn unpack_mag(slot: u32, bin_in_col: u32) -> f32 {
150152const CULL_POS : vec4 <f32 > = vec4 <f32 >(0 .0 , 0 .0 , 2 .0 , 1 .0 );
151153const CLASSIC_SENTINEL_DB : f32 = - 10000 .0 ;
152154
155+ fn place_accum (pos : vec2 <f32 >) -> vec4 <f32 > {
156+ let size = vec2 <f32 >(max (u . accum_width , 1 .0 ), max (u . accum_height , 1 .0 ));
157+ return vec4 <f32 >(pos . x / size . x * 2 .0 - 1 .0 , 1 .0 - pos . y / size . y * 2 .0 , 0 .0 , 1 .0 );
158+ }
159+
153160@vertex
154- fn vs_splat (
161+ fn vs_accum_splat (
155162 @location (0 ) corner : vec2 <f32 >,
156163 @location (1 ) time_offset : f32 ,
157164 @location (2 ) freq_hz : f32 ,
158165 @location (3 ) magnitude_db : f32 ,
159166 @builtin (instance_index ) inst : u32 ,
160- ) -> VertexOutput {
167+ ) -> AccumOutput {
161168 let zoomed = (freq_to_norm (freq_hz ) - u . uv_y_range . x ) * u . inv_uv_range ;
162169 if magnitude_db < - 900 .0 || zoomed < - 0 .01 || zoomed > 1 .01 {
163- return VertexOutput (CULL_POS , magnitude_db , freq_hz );
170+ return AccumOutput (CULL_POS , magnitude_db , freq_hz );
164171 }
165172 let ext = extents ();
166173 let age = compute_age (inst / max (u . points_per_col , 1u ));
167174 let pos = vec2 <f32 >(ext . x - (f32 (age ) - time_offset ) * u . scale_factor , (1 .0 - zoomed ) * ext . y )
168175 + corner * u . scale_factor ;
169- return VertexOutput ( place (pos , ext ), magnitude_db , freq_hz );
176+ return AccumOutput ( place_accum (pos ), magnitude_db , freq_hz );
170177}
171178
172179@vertex
173- fn vs_classic (@location (0 ) corner : vec2 <f32 >) -> VertexOutput {
180+ fn vs_classic (@location (0 ) corner : vec2 <f32 >) -> ClassicOutput {
174181 let px = u . bounds . xy + (corner + vec2 <f32 >(0 .5 )) * u . bounds . zw ;
175182 let clip = vec4 <f32 >(px . x * u . clip_scale . x - 1 .0 , 1 .0 - px . y * u . clip_scale . y , 0 .0 , 1 .0 );
176- return VertexOutput (clip , CLASSIC_SENTINEL_DB , 0 .0 );
183+ return ClassicOutput (clip );
184+ }
185+
186+ @vertex
187+ fn vs_resolve (@location (0 ) corner : vec2 <f32 >) -> ResolveOutput {
188+ let local = (corner + vec2 <f32 >(0 .5 )) * u . bounds . zw ;
189+ let px = u . bounds . xy + local ;
190+ let clip = vec4 <f32 >(px . x * u . clip_scale . x - 1 .0 , 1 .0 - px . y * u . clip_scale . y , 0 .0 , 1 .0 );
191+ return ResolveOutput (clip , unrotate (local , extents ()));
177192}
178193
179194fn norm_to_freq (norm : f32 ) -> f32 {
@@ -231,22 +246,27 @@ fn classic_sample(frag_xy: vec2<f32>) -> vec2<f32> {
231246 return vec2 <f32 >(mag , freq_hz );
232247}
233248
234- fn shade (mut_mag : f32 , freq_hz : f32 ) -> vec4 < f32 > {
249+ fn apply_tilt (mut_mag : f32 , freq_hz : f32 ) -> f32 {
235250 var mag = mut_mag ;
236-
237251 // dB/octave tilt relative to 1 kHz. Do not lift sentinels/floor bins.
238252 if u . tilt_db != 0 .0 {
239253 if ! (mag > DB_ANALYSIS_FLOOR + DB_FLOOR_EPS ) {
240- return vec4 < f32 >( 0 .0 ) ;
254+ return CLASSIC_SENTINEL_DB ;
241255 }
242256 if freq_hz > 0 .0 {
243257 mag += u . tilt_db * log2 (freq_hz / 1000 .0 );
244258 }
245259 }
260+ return mag ;
261+ }
246262
263+ fn shade_db (mag : f32 ) -> vec4 <f32 > {
247264 let range = max (u . ceiling_db - u . floor_db , 0 .001 );
248265 let normalized = clamp ((mag - u . floor_db ) / range , 0 .0 , 1 .0 );
249- let adjusted = pow (normalized , max (u . contrast , 0 .01 ));
266+ var adjusted = normalized ;
267+ if abs (u . contrast - 1 .0 ) > 1e-4 {
268+ adjusted = pow (normalized , max (u . contrast , 0 .01 ));
269+ }
250270
251271 // Mix palette stops in sRGB space (web-colors pipeline).
252272 let color = palette_color (adjusted );
@@ -255,13 +275,53 @@ fn shade(mut_mag: f32, freq_hz: f32) -> vec4<f32> {
255275 return vec4 <f32 >(color . rgb * color . a , color . a );
256276}
257277
278+ fn shade (mut_mag : f32 , freq_hz : f32 ) -> vec4 <f32 > {
279+ let mag = apply_tilt (mut_mag , freq_hz );
280+ if mag < - 9000 .0 {
281+ return vec4 <f32 >(0 .0 );
282+ }
283+ return shade_db (mag );
284+ }
285+
286+ fn db_to_power (db : f32 ) -> f32 {
287+ return exp2 (db * DB_TO_LOG2 );
288+ }
289+
290+ fn power_to_db (power : f32 ) -> f32 {
291+ return max (log (max (power , 1e-20 )) * LN_TO_DB , DB_ANALYSIS_FLOOR );
292+ }
293+
258294@fragment
259- fn fs_splat (in : VertexOutput ) -> @location (0 ) vec4 <f32 > {
260- return shade (in . magnitude_db , in . freq_hz );
295+ fn fs_accum (in : AccumOutput ) -> @location (0 ) vec2 <f32 > {
296+ let mag = apply_tilt (in . magnitude_db , in . freq_hz );
297+ if mag < - 9000 .0 {
298+ return vec2 <f32 >(0 .0 );
299+ }
300+ let power = db_to_power (mag );
301+ return vec2 <f32 >(power , power * LOW_POWER_SCALE );
302+ }
303+
304+ fn accumulated_power (pos : vec2 <f32 >) -> f32 {
305+ if pos . x < 0 .0 || pos . y < 0 .0 || pos . x >= u . accum_width || pos . y >= u . accum_height {
306+ return 0 .0 ;
307+ }
308+
309+ let scaled = textureLoad (accum_tex , vec2 <i32 >(pos ), 0 ). rg ;
310+ let scaled_low_power = scaled . g > 0 .0 && scaled . g < F16_MAX ;
311+ return select (scaled . r , scaled . g * INV_LOW_POWER_SCALE , scaled_low_power );
312+ }
313+
314+ @fragment
315+ fn fs_resolve (in : ResolveOutput ) -> @location (0 ) vec4 <f32 > {
316+ let power = accumulated_power (in . accum_pos );
317+ if power <= 0 .0 {
318+ return vec4 <f32 >(0 .0 );
319+ }
320+ return shade_db (power_to_db (power ));
261321}
262322
263323@fragment
264- fn fs_classic (in : VertexOutput ) -> @location (0 ) vec4 <f32 > {
324+ fn fs_classic (in : ClassicOutput ) -> @location (0 ) vec4 <f32 > {
265325 let sample = classic_sample (in . position . xy );
266326 if sample . x < - 9000 .0 {
267327 return vec4 <f32 >(0 .0 );
0 commit comments