1#![deny(
30 clippy::unwrap_used,
31 clippy::expect_used,
32 clippy::indexing_slicing,
33 clippy::panic,
34 clippy::unreachable
35)]
36
37use crate::render::gpu;
38
39use super::common;
40use super::{Scene, SeededRng};
41use crate::dsp::AnalysisFrame;
42use crate::render::feedback::PingPongField;
43use crate::render::palette::{self, Palette};
44use crate::render::scenes::{ParamGroup, ParamKind, ParamSpec, default_of};
45
46const GRID: u32 = 256;
52
53const FIXED_STEP: f32 = 1.0 / 720.0;
58
59const MAX_SUBSTEPS: u32 = 40;
64
65const SEED_BLOBS: usize = 30;
67const MAX_BLOBS: usize = 32;
68const SEED: u64 = 0x4C4D_565F_5244_5F31;
72
73const DEFAULT_FEED: f32 = default_of(PARAMS, "feed");
77const DEFAULT_KILL: f32 = default_of(PARAMS, "kill");
78const DIFFUSE_U: f32 = 0.16;
83const DIFFUSE_V: f32 = 0.08;
84const DEFAULT_FLOW: f32 = default_of(PARAMS, "flow");
86
87const DEFAULT_HUE: f32 = 0.0;
90const DEFAULT_CONTOUR: f32 = default_of(PARAMS, "contour");
91const DEFAULT_HATCH: f32 = default_of(PARAMS, "hatch");
92const DEFAULT_GLOW: f32 = default_of(PARAMS, "glow");
93const DEFAULT_COLOR_SPAN: f32 = default_of(PARAMS, "color_span");
98const DEFAULT_COLOR_CENTER: f32 = default_of(PARAMS, "color_center");
99const DEFAULT_ZOOM: f32 = 1.0;
102
103const INJECT_RADIUS: f32 = 0.045;
107const INJECT_AMOUNT: f32 = 0.85;
108const INJECT_SEED: u64 = 0x4C4D_5244_494E_4A31; const INJECT_THRESHOLD: f32 = 0.5;
111
112const INIT_SHADER: &str = r#"
114struct Init {
115 blobs: array<vec4<f32>, 32>, // xy: center (uv), z: radius, w: unused
116 count: vec4<u32>, // x: active blob count
117}
118@group(0) @binding(0) var<uniform> init: Init;
119
120@fragment
121fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
122 var v = 0.0;
123 let n = init.count.x;
124 for (var i = 0u; i < n; i = i + 1u) {
125 let b = init.blobs[i];
126 if (distance(in.uv, b.xy) < b.z) {
127 v = 1.0;
128 }
129 }
130 return vec4<f32>(1.0 - v, v, 0.0, 1.0);
131}
132"#;
133
134const SIM_SHADER: &str = r#"
136struct Sim {
137 p: vec4<f32>, // x: feed, y: kill, z: diffuse_u, w: diffuse_v
138 inj: vec4<f32>, // xy: stamp center (uv), z: radius, w: amount (0 = no stamp)
139}
140@group(0) @binding(0) var<uniform> sim: Sim;
141@group(0) @binding(1) var field: texture_2d<f32>;
142
143// Toroidal texel fetch (wrap at the edges) of the (U, V) pair.
144fn ld(c: vec2<i32>, size: vec2<i32>) -> vec2<f32> {
145 let w = ((c % size) + size) % size;
146 return textureLoad(field, w, 0).xy;
147}
148
149@fragment
150fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
151 let size = vec2<i32>(textureDimensions(field));
152 let c = vec2<i32>(i32(in.pos.x), i32(in.pos.y));
153 let m = ld(c, size);
154 let u = m.x;
155 let v = m.y;
156
157 // 3×3 Laplacian: orthogonal 0.2, diagonal 0.05, center -1.
158 var lap = ld(c + vec2<i32>(-1, 0), size) * 0.2
159 + ld(c + vec2<i32>(1, 0), size) * 0.2
160 + ld(c + vec2<i32>(0, -1), size) * 0.2
161 + ld(c + vec2<i32>(0, 1), size) * 0.2
162 + ld(c + vec2<i32>(-1, -1), size) * 0.05
163 + ld(c + vec2<i32>(1, -1), size) * 0.05
164 + ld(c + vec2<i32>(-1, 1), size) * 0.05
165 + ld(c + vec2<i32>(1, 1), size) * 0.05;
166 lap = lap - m;
167
168 let feed = sim.p.x;
169 let kill = sim.p.y;
170 let du = sim.p.z;
171 let dv = sim.p.w;
172 let reaction = u * v * v;
173 let nu = u + du * lap.x - reaction + feed * (1.0 - u);
174 var nv = v + dv * lap.y + reaction - (kill + feed) * v;
175
176 // Beat-stamped seed injection (Phase 3), folded into the sim so no extra
177 // pipeline is needed. `inj.w` is non-zero only on the stamp frame; it is
178 // applied on every sub-step of that frame, so V saturates at the stamp.
179 let stamp = sim.inj.w * (1.0 - smoothstep(sim.inj.z * 0.4, sim.inj.z, distance(in.uv, sim.inj.xy)));
180 nv = nv + stamp;
181
182 return vec4<f32>(clamp(nu, 0.0, 1.0), clamp(nv, 0.0, 1.0), 0.0, 1.0);
183}
184"#;
185
186const PRESENT_SHADER: &str = r#"
190struct Present {
191 // x: hue, y: contour density, z: hatch frequency (texels), w: glow
192 a: vec4<f32>,
193 // x: color_span, y: color_center, z: saturation, w: palette_mix
194 b: vec4<f32>,
195 // x: zoom, yz: pan (field-space view transform, ADR-0018), w: occlude (ADR-0085)
196 c: vec4<f32>,
197 // x: palette_steps (integral, quantized CPU-side), y: palette_contour
198 // (ADR-0078), z: palette_contour_style (integral, rounded CPU-side),
199 // w: palette_contour_ink (ADR-0197)
200 d: vec4<f32>,
201}
202@group(0) @binding(0) var present_field: texture_2d<f32>;
203@group(0) @binding(1) var present_samp: sampler;
204@group(0) @binding(2) var<uniform> pp: Present;
205// Shared gradient LUTs (ADR-0021): A/B for the `palette_mix` crossfade, one
206// repeat sampler. Kept in this present bind group (a unique 6-entry layout) so it
207// never matches another pipeline's layout on the DX12 WARP software adapter
208// (where matching layouts alias; a WARP reading, unverified on lavapipe as of
209// 2026-10-06).
210@group(0) @binding(3) var lut_a: texture_2d<f32>;
211@group(0) @binding(4) var lut_b: texture_2d<f32>;
212@group(0) @binding(5) var lut_samp: sampler;
213
214// Shared `saturation` (mirrors core/src/render/palette.rs::desaturate verbatim).
215fn apply_saturation(c: vec3<f32>, s: f32) -> vec3<f32> {
216 let luma = dot(c, vec3<f32>(0.299, 0.587, 0.114));
217 return vec3<f32>(luma) + (c - vec3<f32>(luma)) * s;
218}
219
220// Shared `palette_steps` (mirrors core/src/render/palette.rs::band_coord
221// verbatim, ADR-0078): snap the palette coordinate to a band centre before the
222// LUT read. Below 1.5 steps it is the exact identity, not a one-band degenerate.
223fn band_coord(t: f32, steps: f32) -> f32 {
224 if (steps < 1.5) {
225 return t;
226 }
227 return (floor(t * steps) + 0.5) / steps;
228}
229
230// Shared `palette_contour` (ADR-0078 / ADR-0133; the WGSL is the implementation,
231// copied verbatim at each fragment-stage site — palette.rs has no CPU
232// counterpart to be canonical, since `fwidth` exists only here).
233//
234// Darkens within one PIXEL of a band edge, so the line has the same weight where
235// the field is shallow and where it is steep — AND ONLY WHERE THE INK ACTUALLY
236// CHANGES (ADR-0133). It samples the two band centres either side of the nearest
237// edge and returns unchanged when they resolve to the same colour within half a
238// code value, which is below the LUT's own 8-bit quantization. On a smooth
239// palette two distinct centres always differ by at least one code value, so
240// every edge draws exactly as it did at any `palette_steps`; inside a plateau
241// the LUT is literally constant and the samples are bit-equal, so the line
242// vanishes there and survives at the run boundaries. One rule, both behaviours,
243// no new parameter.
244//
245// The two LUTs, the sampler and `palette_mix` are EXPLICIT parameters rather
246// than module-scope globals this happens to find: all six sites name them the
247// same today, so implicit capture would compile — and would silently bind the
248// shared function to whatever a future site called its textures.
249//
250// `textureSampleLevel`, not `textureSample`: the LUT has one mip, and an
251// explicit LOD keeps these reads free of the uniformity requirement that a
252// sample after a conditional return would otherwise carry.
253//
254// **What the line is drawn in is `style`** (ADR-0197): `0` the soft darkening
255// above, `1` a hard darkening over the same footprint, `2` a soft line in the
256// palette's own colour at `ink_t` and `3` a hard one. `style` arrives rounded to
257// a whole number from the CPU (`palette::band_contour_style`), so the equality
258// comparisons below are exact. The `style < 0.5` arm is the expression that
259// shipped before the other three existed, which is what keeps every golden still.
260fn band_contour_ink(
261 col: vec3<f32>,
262 t: f32,
263 steps: f32,
264 amount: f32,
265 style: f32,
266 ink_t: f32,
267 lut_a: texture_2d<f32>,
268 lut_b: texture_2d<f32>,
269 lut_samp: sampler,
270 mix_ab: f32,
271) -> vec3<f32> {
272 let f = t * steps;
273 let w = max(fwidth(f), 1e-5);
274 if (steps < 1.5 || amount <= 0.0) {
275 return col;
276 }
277 let n = round(f);
278 let m = clamp(mix_ab, 0.0, 1.0);
279 let lo = mix(
280 textureSampleLevel(lut_a, lut_samp, vec2<f32>((n - 0.5) / steps, 0.5), 0.0).rgb,
281 textureSampleLevel(lut_b, lut_samp, vec2<f32>((n - 0.5) / steps, 0.5), 0.0).rgb,
282 m
283 );
284 let hi = mix(
285 textureSampleLevel(lut_a, lut_samp, vec2<f32>((n + 0.5) / steps, 0.5), 0.0).rgb,
286 textureSampleLevel(lut_b, lut_samp, vec2<f32>((n + 0.5) / steps, 0.5), 0.0).rgb,
287 m
288 );
289 if (all(abs(hi - lo) < vec3<f32>(0.5 / 255.0))) {
290 return col;
291 }
292 let d = min(fract(f), 1.0 - fract(f));
293 if (style < 0.5) {
294 return col * (1.0 - clamp(amount, 0.0, 1.0) * (1.0 - smoothstep(0.0, w, d)));
295 }
296 let hard = style == 1.0 || style == 3.0;
297 let cover = select(1.0 - smoothstep(0.0, w, d), f32(d < w), hard);
298 let ink_lut = mix(
299 textureSampleLevel(lut_a, lut_samp, vec2<f32>(ink_t, 0.5), 0.0).rgb,
300 textureSampleLevel(lut_b, lut_samp, vec2<f32>(ink_t, 0.5), 0.0).rgb,
301 m
302 );
303 let ink = select(vec3<f32>(0.0), ink_lut, style >= 2.0);
304 return mix(col, ink, clamp(amount, 0.0, 1.0) * cover);
305}
306
307fn tap_v(uv: vec2<f32>) -> f32 {
308 return textureSampleLevel(present_field, present_samp, uv, 0.0).y;
309}
310
311// C1 reconstruction of the finite field (Plan 0033 Phase 3, ADR-0034).
312//
313// Hardware bilinear is C0: the value is continuous, its gradient is not. This
314// pass runs analytic iso-contours and a central-difference gradient over exactly
315// that field, and `line_d` divides by `fwidth`, so a slope discontinuity far
316// below the 8-bit output quantum is amplified into a visible tangent kink —
317// which is why an 8x upscale of a smooth field read as angular facets.
318//
319// Catmull-Rom, not a smoothed texel coordinate. Warping the fractional
320// coordinate by a quintic is the cheap trick and it is *wrong here*: a
321// smoothstep-family warp has zero derivative at both ends, so it pins the
322// reconstruction's gradient to zero at every texel centre. That is C1, but the
323// derivative then oscillates once per cell, and a pass with this much gradient
324// gain renders it as one scalloped step per texel — measurably worse than the
325// faceting it replaces. Only a genuine higher-order filter has a smooth,
326// non-degenerate derivative.
327//
328// Nine taps rather than sixteen: each pair of neighbouring weights is folded
329// into one hardware-bilinear fetch at the weighted midpoint (`offset12`), which
330// is exact for a separable cubic. `w1 + w2 >= 1` over the whole cell, so the
331// division is safe.
332//
333// **Every** read of the field goes through here — value, gradient, contour and
334// hatch — because fixing only the value tap fixes nothing: the gradient is where
335// the discontinuity becomes visible.
336fn sample_v(uv: vec2<f32>) -> f32 {
337 let dims = vec2<f32>(textureDimensions(present_field));
338 let sample_pos = uv * dims;
339 let pos1 = floor(sample_pos - 0.5) + 0.5;
340 let f = sample_pos - pos1;
341
342 let w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
343 let w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
344 let w2 = f * (0.5 + f * (2.0 - 1.5 * f));
345 let w3 = f * f * (-0.5 + 0.5 * f);
346 let w12 = w1 + w2;
347
348 let p0 = (pos1 - 1.0) / dims;
349 let p3 = (pos1 + 2.0) / dims;
350 let p12 = (pos1 + w2 / w12) / dims;
351
352 var acc = 0.0;
353 acc = acc + tap_v(vec2<f32>(p0.x, p0.y)) * w0.x * w0.y;
354 acc = acc + tap_v(vec2<f32>(p12.x, p0.y)) * w12.x * w0.y;
355 acc = acc + tap_v(vec2<f32>(p3.x, p0.y)) * w3.x * w0.y;
356
357 acc = acc + tap_v(vec2<f32>(p0.x, p12.y)) * w0.x * w12.y;
358 acc = acc + tap_v(vec2<f32>(p12.x, p12.y)) * w12.x * w12.y;
359 acc = acc + tap_v(vec2<f32>(p3.x, p12.y)) * w3.x * w12.y;
360
361 acc = acc + tap_v(vec2<f32>(p0.x, p3.y)) * w0.x * w3.y;
362 acc = acc + tap_v(vec2<f32>(p12.x, p3.y)) * w12.x * w3.y;
363 acc = acc + tap_v(vec2<f32>(p3.x, p3.y)) * w3.x * w3.y;
364 return acc;
365}
366
367@fragment
368fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
369 let dims = vec2<f32>(textureDimensions(present_field));
370 let texel = 1.0 / dims;
371
372 // View transform (ADR-0018): scale the sampled window about its centre by
373 // `zoom`, then offset by `pan`. Default zoom = 1, pan = 0 leaves `in.uv`
374 // untouched, so an unbound preset renders identically. Same shape fragment_field
375 // uses for its field-space zoom/pan.
376 let zoom = pp.c.x;
377 // `pan.y` is negated because `in.uv` is now Y-flipped (ADR-0070): this pass
378 // moved from the retired unflipped prelude, which reversed the direction a
379 // positive `pan_y` scrolls the field. Every other scene applies pan in clip
380 // space, where +y is up, and all four RD presets were authored against that
381 // agreement — so the sign restores the shipped behaviour rather than changing
382 // it. Measured both ways: pan_y = +0.12 moves the field 86 px, up before this
383 // and down without this negation.
384 let pan = vec2<f32>(pp.c.y, -pp.c.z);
385 let uv = (in.uv - vec2<f32>(0.5, 0.5)) * zoom + vec2<f32>(0.5, 0.5) + pan;
386
387 let v = sample_v(uv);
388
389 // Central-difference gradient of the field (for hatch orientation + edges).
390 let gx = sample_v(uv + vec2<f32>(texel.x, 0.0)) - sample_v(uv - vec2<f32>(texel.x, 0.0));
391 let gy = sample_v(uv + vec2<f32>(0.0, texel.y)) - sample_v(uv - vec2<f32>(0.0, texel.y));
392 let grad = vec2<f32>(gx, gy);
393 let gmag = length(grad);
394
395 let hue = pp.a.x;
396 let density = pp.a.y;
397 let hatch_freq = pp.a.z;
398 let glow = pp.a.w;
399 let color_span = pp.b.x;
400 let color_center = pp.b.y;
401 let saturation = pp.b.z;
402 let palette_mix = pp.b.w;
403
404 // Slope mask: contours and hatch only appear where the field actually
405 // slopes, so the flat V=0 background stays dark (V=0 is itself an iso-level,
406 // which would otherwise flood the flats).
407 let slope = smoothstep(0.0008, 0.004, gmag);
408
409 // Iso-contour lines: distance (in pixels) to the nearest V = k/density level,
410 // anti-aliased by fwidth. `contour` is ~1 on a line, 0 between them.
411 let f = v * density;
412 let line_d = abs(fract(f - 0.5) - 0.5) / max(fwidth(f), 1e-4);
413 let contour = (1.0 - clamp(line_d, 0.0, 1.0)) * slope;
414
415 // Palette by field level so the nested loops read as coloured bands. The
416 // field level `v` is the gradient coordinate: `color_span` (was a fixed 0.85)
417 // sets the spanned range, `color_center`/`hue` slide the window, and the A/B
418 // LUTs crossfade by `palette_mix` before the shared `saturation`.
419 let coord = v * color_span + color_center + hue;
420 // Hard bands, then the contour from the SAME coordinate (ADR-0078), so the
421 // dark line follows the palette's iso-lines through the field.
422 let banded = band_coord(coord, pp.d.x);
423 let ca = textureSample(lut_a, lut_samp, vec2<f32>(banded, 0.5)).rgb;
424 let cb = textureSample(lut_b, lut_samp, vec2<f32>(banded, 0.5)).rgb;
425 let mixed = band_contour_ink(
426 mix(ca, cb, clamp(palette_mix, 0.0, 1.0)),
427 coord, pp.d.x, pp.d.y, pp.d.z, pp.d.w, lut_a, lut_b, lut_samp, palette_mix
428 );
429 let col = apply_saturation(mixed, saturation);
430
431 // Hatch/comb: stripes along the contour tangent (perpendicular to grad),
432 // gated to the slopes so flats stay clean.
433 let tang = normalize(vec2<f32>(-grad.y, grad.x) + vec2<f32>(1e-5, 1e-5));
434 let s = dot(uv * dims, tang) / max(hatch_freq, 1.0);
435 let hatch = smoothstep(0.30, 0.5, abs(fract(s) - 0.5));
436 let hatch_amt = hatch * slope;
437
438 // Compose: dark bed, a coloured fill only where the field lives, bright
439 // contour loops, hatch ticks that darken along the slopes, and a soft glow.
440 let structure = smoothstep(0.04, 0.45, v);
441 var out_col = col * structure * 0.5;
442 out_col = out_col + col * contour * 0.9;
443 out_col = out_col * (1.0 - hatch_amt * 0.4);
444 out_col = out_col + col * v * glow * 0.22;
445
446 // Alpha carries scene presence (the V-field `structure` term) so V=0 voids are
447 // transparent and the `bg_*` backdrop shows through (ADR-0026). The present
448 // pipeline blends premultiplied-OVER: `out_col` is emitted as-is (added over the
449 // backdrop, so bright contours keep full brightness), and alpha only gates how
450 // much backdrop reveals. Over the default black backdrop this is byte-identical
451 // to the prior opaque present.
452 //
453 // `occlude` (pp.c.w) scales that coverage: how much of the backdrop the field
454 // holds out where it does have presence (ADR-0085). Reached only when no post
455 // stage is active — the chain's last stage owns the seam otherwise, and the
456 // renderer hands a literal 1.0 here.
457 return vec4<f32>(out_col, structure * pp.c.w);
458}
459"#;
460
461#[repr(C)]
462#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
463struct InitParams {
464 blobs: [[f32; 4]; MAX_BLOBS],
465 count: [u32; 4],
466}
467
468#[repr(C)]
469#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
470struct SimParams {
471 p: [f32; 4],
473 inj: [f32; 4],
475}
476
477#[repr(C)]
478#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
479struct PresentParams {
480 a: [f32; 4],
482 b: [f32; 4],
484 c: [f32; 4],
486 d: [f32; 4],
489}
490
491struct Resources {
493 field: PingPongField,
494 sim_pipeline: wgpu::RenderPipeline,
495 init_pipeline: wgpu::RenderPipeline,
496 present_pipeline: wgpu::RenderPipeline,
497 sim_uniform: wgpu::Buffer,
498 init_uniform: wgpu::Buffer,
499 present_uniform: wgpu::Buffer,
500 sim_bg_a: wgpu::BindGroup,
503 sim_bg_b: wgpu::BindGroup,
504 init_bg: wgpu::BindGroup,
505 present_bg_a: wgpu::BindGroup,
506 present_bg_b: wgpu::BindGroup,
507 luts: palette::LutPair,
511}
512
513impl Resources {
514 fn build(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
516 let init_shader = gpu::fullscreen_shader(
517 device,
518 "rd-init-shader",
519 gpu::FULLSCREEN_VS_UV_FLIPPED,
520 INIT_SHADER,
521 );
522 let sim_shader = gpu::fullscreen_shader(
523 device,
524 "rd-sim-shader",
525 gpu::FULLSCREEN_VS_UV_FLIPPED,
526 SIM_SHADER,
527 );
528 let present_shader = gpu::fullscreen_shader(
529 device,
530 "rd-present-shader",
531 gpu::FULLSCREEN_VS_UV_FLIPPED,
532 PRESENT_SHADER,
533 );
534
535 let field = PingPongField::new(device, GRID, GRID);
536
537 let sim_uniform =
538 gpu::uniform_buffer(device, "rd-sim-params", std::mem::size_of::<SimParams>());
539 let init_uniform =
540 gpu::uniform_buffer(device, "rd-init-params", std::mem::size_of::<InitParams>());
541 let present_uniform = gpu::uniform_buffer(
542 device,
543 "rd-present-params",
544 std::mem::size_of::<PresentParams>(),
545 );
546 let init_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
548 label: Some("rd-init-layout"),
549 entries: &[gpu::uniform(0, wgpu::ShaderStages::FRAGMENT)],
550 });
551 let init_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
552 label: Some("rd-init-bg"),
553 layout: &init_layout,
554 entries: &[wgpu::BindGroupEntry {
555 binding: 0,
556 resource: init_uniform.as_entire_binding(),
557 }],
558 });
559 let init_pipeline = gpu::fullscreen_pipeline(
560 device,
561 &init_shader,
562 &[&init_layout],
563 PingPongField::FORMAT,
564 wgpu::BlendState::REPLACE,
565 "rd-init",
566 );
567
568 let sim_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
570 label: Some("rd-sim-layout"),
571 entries: &[
572 gpu::uniform(0, wgpu::ShaderStages::FRAGMENT),
573 gpu::texture(1, false),
574 ],
575 });
576 let sim_bg_a = sim_bind_group(device, &sim_layout, &sim_uniform, field.view_a());
577 let sim_bg_b = sim_bind_group(device, &sim_layout, &sim_uniform, field.view_b());
578 let sim_pipeline = gpu::fullscreen_pipeline(
579 device,
580 &sim_shader,
581 &[&sim_layout],
582 PingPongField::FORMAT,
583 wgpu::BlendState::REPLACE,
584 "rd-sim",
585 );
586
587 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
603 label: Some("rd-present-sampler"),
604 address_mode_u: wgpu::AddressMode::Repeat,
605 address_mode_v: wgpu::AddressMode::Repeat,
606 address_mode_w: wgpu::AddressMode::Repeat,
607 mag_filter: wgpu::FilterMode::Linear,
608 min_filter: wgpu::FilterMode::Linear,
609 ..Default::default()
610 });
611 let luts = palette::LutPair::new(device, "rd");
614 let present_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
615 label: Some("rd-present-layout"),
616 entries: &[
617 gpu::texture(0, true),
618 gpu::sampler(1),
619 gpu::uniform(2, wgpu::ShaderStages::FRAGMENT),
620 gpu::texture(3, true),
621 gpu::texture(4, true),
622 gpu::sampler(5),
623 ],
624 });
625 let present_bg_a = present_bind_group(
626 device,
627 &present_layout,
628 field.view_a(),
629 &sampler,
630 &present_uniform,
631 &luts,
632 );
633 let present_bg_b = present_bind_group(
634 device,
635 &present_layout,
636 field.view_b(),
637 &sampler,
638 &present_uniform,
639 &luts,
640 );
641 let present_pipeline = gpu::fullscreen_pipeline(
642 device,
643 &present_shader,
644 &[&present_layout],
645 surface_format,
646 wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
651 "rd-present",
652 );
653
654 Self {
655 field,
656 sim_pipeline,
657 init_pipeline,
658 present_pipeline,
659 sim_uniform,
660 init_uniform,
661 present_uniform,
662 sim_bg_a,
663 sim_bg_b,
664 init_bg,
665 present_bg_a,
666 present_bg_b,
667 luts,
668 }
669 }
670
671 fn encode_seed(&self, encoder: &mut wgpu::CommandEncoder) {
674 let mut pass = gpu::color_pass(
675 encoder,
676 "rd-seed-pass",
677 self.field.read_view(),
678 wgpu::LoadOp::Clear(wgpu::Color::BLACK),
679 );
680 pass.set_pipeline(&self.init_pipeline);
681 pass.set_bind_group(0, &self.init_bg, &[]);
682 pass.draw(0..3, 0..1);
683 }
684}
685
686pub struct ReactionDiffusionScene {
690 device: wgpu::Device,
694 surface_format: wgpu::TextureFormat,
695 res: Option<Resources>,
696 init_params: InitParams,
699 needs_seed: bool,
700 fixed_step: gpu::FixedStep,
703 pending_substeps: u32,
705 stamp_rng: SeededRng,
708 pending_stamp: Option<[f32; 4]>,
711 prev_inject: f32,
713 time: f32,
715 feed: f32,
716 kill: f32,
717 flow: f32,
719 inject: f32,
721 colour: common::PaletteParams,
723 pan: common::PanParams,
725 contour: f32,
726 hatch: f32,
727 glow: f32,
728 color_span: f32,
730 color_center: f32,
731 zoom: f32,
734 occlude: f32,
739 palette: Palette,
744}
745
746impl ReactionDiffusionScene {
747 pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
750 let mut init_params = InitParams {
751 blobs: [[0.0; 4]; MAX_BLOBS],
752 count: [0; 4],
753 };
754 let mut rng = SeededRng::new(SEED);
755 let n = SEED_BLOBS.min(MAX_BLOBS);
756 for slot in init_params.blobs.iter_mut().take(n) {
757 let x = rng.next_f32();
758 let y = rng.next_f32();
759 let r = rng.range(0.02, 0.045);
760 *slot = [x, y, r, 0.0];
761 }
762 init_params.count = [n as u32, 0, 0, 0];
763
764 Self {
765 device: device.clone(),
766 surface_format,
767 res: None,
768 init_params,
769 needs_seed: true,
770 fixed_step: gpu::FixedStep::new(FIXED_STEP, MAX_SUBSTEPS),
771 pending_substeps: 0,
772 stamp_rng: SeededRng::new(INJECT_SEED),
773 pending_stamp: None,
774 prev_inject: 0.0,
775 time: 0.0,
776 feed: DEFAULT_FEED,
777 kill: DEFAULT_KILL,
778 flow: DEFAULT_FLOW,
779 inject: 0.0,
780 colour: common::PaletteParams::new(DEFAULT_HUE, common::DEFAULT_BRIGHTNESS),
781 pan: common::PanParams::default(),
782 contour: DEFAULT_CONTOUR,
783 hatch: DEFAULT_HATCH,
784 glow: DEFAULT_GLOW,
785 color_span: DEFAULT_COLOR_SPAN,
786 color_center: DEFAULT_COLOR_CENTER,
787 zoom: DEFAULT_ZOOM,
788 occlude: crate::render::post::DEFAULT_OCCLUDE,
789 palette: Palette::default_spectrum(),
790 }
791 }
792}
793
794fn sim_bind_group(
795 device: &wgpu::Device,
796 layout: &wgpu::BindGroupLayout,
797 uniform: &wgpu::Buffer,
798 input: &wgpu::TextureView,
799) -> wgpu::BindGroup {
800 device.create_bind_group(&wgpu::BindGroupDescriptor {
801 label: Some("rd-sim-bg"),
802 layout,
803 entries: &[
804 wgpu::BindGroupEntry {
805 binding: 0,
806 resource: uniform.as_entire_binding(),
807 },
808 wgpu::BindGroupEntry {
809 binding: 1,
810 resource: wgpu::BindingResource::TextureView(input),
811 },
812 ],
813 })
814}
815
816fn present_bind_group(
817 device: &wgpu::Device,
818 layout: &wgpu::BindGroupLayout,
819 input: &wgpu::TextureView,
820 sampler: &wgpu::Sampler,
821 uniform: &wgpu::Buffer,
822 luts: &palette::LutPair,
823) -> wgpu::BindGroup {
824 let [lut_a, lut_b, lut_sampler] = luts.bind_entries(3, 4, 5);
825 device.create_bind_group(&wgpu::BindGroupDescriptor {
826 label: Some("rd-present-bg"),
827 layout,
828 entries: &[
829 wgpu::BindGroupEntry {
830 binding: 0,
831 resource: wgpu::BindingResource::TextureView(input),
832 },
833 wgpu::BindGroupEntry {
834 binding: 1,
835 resource: wgpu::BindingResource::Sampler(sampler),
836 },
837 wgpu::BindGroupEntry {
838 binding: 2,
839 resource: uniform.as_entire_binding(),
840 },
841 lut_a,
842 lut_b,
843 lut_sampler,
844 ],
845 })
846}
847
848pub const PARAMS: &[ParamSpec] = &[
851 ParamSpec {
852 name: "feed",
853 default: 0.0367,
854 range: Some([0.01, 0.09]),
855 doc: "Feed rate of the reaction - with `kill`, it is what decides whether you get spots, stripes or mitosis.",
856 kind: ParamKind::Modal,
857 group: ParamGroup::Shape,
858 main: true,
859 },
860 ParamSpec {
861 name: "kill",
862 default: 0.0649,
863 range: Some([0.03, 0.07]),
864 doc: "Kill rate of the reaction; small moves here change the pattern's whole character.",
865 kind: ParamKind::Modal,
866 group: ParamGroup::Shape,
867 main: true,
868 },
869 ParamSpec {
870 name: "flow",
871 default: 1.0,
872 range: Some([0.0, 4.0]),
873 doc: "How fast the simulation advances per second.",
874 kind: ParamKind::Modal,
875 group: ParamGroup::Motion,
876 main: true,
877 },
878 ParamSpec {
879 name: "inject",
880 default: 0.0,
881 range: Some([0.0, 1.0]),
882 doc: "Drops fresh reagent into the field, which is how a beat seeds new growth.",
883 kind: ParamKind::Modal,
884 group: ParamGroup::Motion,
885 main: false,
886 },
887 crate::render::scenes::common::hue(DEFAULT_HUE),
888 ParamSpec {
889 name: "contour",
890 default: 6.0,
891 range: Some([0.0, 24.0]),
892 doc: "How many bands the concentration is drawn as, as a real density: a fraction \
893 slides the whole set of iso-lines. 0 is a smooth gradient.",
894 kind: ParamKind::Modal,
895 group: ParamGroup::Shape,
896 main: false,
897 },
898 ParamSpec {
899 name: "hatch",
900 default: 5.0,
901 range: Some([0.0, 24.0]),
902 doc: "Density of the hatching drawn along the concentration gradient.",
903 kind: ParamKind::Modal,
904 group: ParamGroup::Shape,
905 main: false,
906 },
907 ParamSpec {
908 name: "glow",
909 default: 1.0,
910 range: Some([0.0, 2.0]),
911 doc: "Overall light the field emits.",
912 kind: ParamKind::Modal,
913 group: ParamGroup::Light,
914 main: false,
915 },
916 ParamSpec {
917 name: "color_span",
918 default: 0.85,
919 range: Some([0.0, 1.0]),
920 doc: "How much of the palette the concentration range covers.",
921 kind: ParamKind::Modal,
922 group: ParamGroup::Colour,
923 main: false,
924 },
925 ParamSpec {
926 name: "color_center",
927 default: 0.0,
928 range: Some([-1.0, 1.0]),
929 doc: "Shifts which concentration lands in the middle of the palette.",
930 kind: ParamKind::Modal,
931 group: ParamGroup::Colour,
932 main: false,
933 },
934 crate::render::scenes::common::SATURATION,
935 crate::render::scenes::common::PALETTE_MIX,
936 crate::render::scenes::common::PALETTE_STEPS,
937 crate::render::scenes::common::PALETTE_CONTOUR,
938 crate::render::scenes::common::PALETTE_CONTOUR_STYLE,
939 crate::render::scenes::common::PALETTE_CONTOUR_INK,
940 crate::render::scenes::common::field_zoom(DEFAULT_ZOOM),
941 crate::render::scenes::common::PAN_X,
942 crate::render::scenes::common::PAN_Y,
943];
944
945impl Scene for ReactionDiffusionScene {
946 fn name(&self) -> &'static str {
947 "reaction diffusion"
948 }
949
950 fn advance(&mut self, dt: f32) {
951 self.pending_substeps = self.fixed_step.advance(dt);
957 }
958
959 fn set_time(&mut self, time: f32) {
960 self.time = time;
961 }
962
963 fn set_occlude(&mut self, occlude: f32) {
964 self.occlude = occlude;
965 }
966
967 fn set_palette(&mut self, palette: &Palette) {
968 self.palette = palette.clone();
971 if let Some(res) = self.res.as_mut() {
972 res.luts.set(palette);
973 }
974 }
975
976 fn reset_params(&mut self) {
977 self.feed = DEFAULT_FEED;
978 self.kill = DEFAULT_KILL;
979 self.flow = DEFAULT_FLOW;
980 self.inject = 0.0;
981 self.colour.reset();
982 self.pan.reset();
983 self.contour = DEFAULT_CONTOUR;
984 self.hatch = DEFAULT_HATCH;
985 self.glow = DEFAULT_GLOW;
986 self.color_span = DEFAULT_COLOR_SPAN;
987 self.color_center = DEFAULT_COLOR_CENTER;
988 self.zoom = DEFAULT_ZOOM;
989 }
990
991 fn set_param(&mut self, name: &str, value: f32) {
992 if self.colour.set(name, value) || self.pan.set(name, value) {
995 return;
996 }
997 match name {
1002 "feed" => self.feed = value,
1003 "kill" => self.kill = value,
1004 "flow" => self.flow = value,
1005 "inject" => self.inject = value,
1006 "contour" => self.contour = value,
1007 "hatch" => self.hatch = value,
1008 "glow" => self.glow = value,
1009 "color_span" => self.color_span = value,
1010 "color_center" => self.color_center = value,
1011 "zoom" => self.zoom = value,
1012 _ => {}
1013 }
1014 }
1015
1016 fn update(&mut self, _frame: &AnalysisFrame) {
1017 if self.inject >= INJECT_THRESHOLD && self.prev_inject < INJECT_THRESHOLD {
1022 let cx = self.stamp_rng.next_f32();
1023 let cy = self.stamp_rng.next_f32();
1024 self.pending_stamp = Some([cx, cy, INJECT_RADIUS, INJECT_AMOUNT]);
1025 }
1026 self.prev_inject = self.inject;
1027 }
1028
1029 fn render(
1030 &mut self,
1031 queue: &wgpu::Queue,
1032 encoder: &mut wgpu::CommandEncoder,
1033 view: &wgpu::TextureView,
1034 _aspect: f32,
1035 ) {
1036 if self.res.is_none() {
1040 let mut built = Resources::build(&self.device, self.surface_format);
1041 built.luts.set(&self.palette);
1042 self.res = Some(built);
1043 }
1044 let Self {
1045 res,
1046 init_params,
1047 needs_seed,
1048 pending_substeps,
1049 pending_stamp,
1050 feed,
1051 kill,
1052 flow,
1053 contour,
1054 hatch,
1055 glow,
1056 color_span,
1057 color_center,
1058 colour,
1059 zoom,
1060 pan,
1061 occlude,
1062 ..
1063 } = self;
1064 let Some(res) = res.as_mut() else {
1065 return;
1066 };
1067
1068 res.luts.flush(queue);
1071
1072 queue.write_buffer(
1073 &res.present_uniform,
1074 0,
1075 bytemuck::bytes_of(&PresentParams {
1076 a: [colour.hue, *contour, *hatch, *glow],
1077 b: [*color_span, *color_center, colour.saturation, colour.mix],
1078 c: [*zoom, pan.x, pan.y, *occlude],
1079 d: [
1080 palette::band_steps(colour.steps),
1081 palette::band_contour(colour.contour),
1082 palette::band_contour_style(colour.contour_style),
1083 colour.contour_ink,
1084 ],
1085 }),
1086 );
1087
1088 let inj = pending_stamp.take().unwrap_or([0.0; 4]);
1092 queue.write_buffer(
1093 &res.sim_uniform,
1094 0,
1095 bytemuck::bytes_of(&SimParams {
1096 p: [*feed, *kill, DIFFUSE_U * *flow, DIFFUSE_V * *flow],
1099 inj,
1100 }),
1101 );
1102
1103 if *needs_seed {
1105 queue.write_buffer(&res.init_uniform, 0, bytemuck::bytes_of(init_params));
1106 res.encode_seed(encoder);
1107 *needs_seed = false;
1108 }
1109
1110 for _ in 0..*pending_substeps {
1113 let sim_bg = if res.field.reading_a() {
1114 &res.sim_bg_a
1115 } else {
1116 &res.sim_bg_b
1117 };
1118 {
1119 let mut pass = gpu::color_pass(
1121 encoder,
1122 "rd-sim-pass",
1123 res.field.write_view(),
1124 wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1125 );
1126 pass.set_pipeline(&res.sim_pipeline);
1127 pass.set_bind_group(0, sim_bg, &[]);
1128 pass.draw(0..3, 0..1);
1129 }
1130 res.field.swap();
1131 }
1132
1133 let present_bg = if res.field.reading_a() {
1135 &res.present_bg_a
1136 } else {
1137 &res.present_bg_b
1138 };
1139 let mut pass = gpu::color_pass(encoder, "rd-present-pass", view, wgpu::LoadOp::Load);
1146 pass.set_pipeline(&res.present_pipeline);
1147 pass.set_bind_group(0, present_bg, &[]);
1148 pass.draw(0..3, 0..1);
1149 }
1150}