This commit is contained in:
2026-05-19 19:56:25 +02:00
parent 686d538743
commit b818f4bc43
7 changed files with 553 additions and 359 deletions
+187
View File
@@ -0,0 +1,187 @@
//! The visualiser contract: one trait the bin drives, one render context.
//!
//! `sigil.rs` no longer hard-codes a `Visual { Sigil, Scope, Breakcore }`
//! enum + an inherent `match` per call. It holds a `Box<dyn Visualizer>` and
//! talks to this trait; adding a mode is a new `impl Visualizer` + one arm in
//! [`next_visual`], nothing in the bin's hot path.
//!
//! The CPU/GPU split is *relocated, not deleted* (it is a real dichotomy, not
//! accidental complexity): `Draw`-based modes (`Sigil`/`Scope`) emit through
//! the shared chromatic-aberration channel passes + `Post`; `Breakcore`
//! presents/captures its own raymarch target. The caller still branches on
//! [`Visualizer::is_gpu`] — the trait just standardises the contract and
//! moves the `match` out of the bin.
//!
//! Determinism is unaffected: dispatch is pure indirection; each visualiser's
//! `update` still advances its `Rng`/integration once per frame exactly as
//! before, so `--render` stays bit-reproducible.
use crate::audio::Bands;
use crate::viz::breakcore::Breakcore;
use crate::viz::palette::Palette;
use crate::viz::scope::Scope;
use crate::viz::sigil::Sigil;
use nannou::prelude::Draw;
use nannou::wgpu;
/// Per-frame shared tunables handed to a visualiser. Cheap to copy (it only
/// borrows the palette) so the bin can spin one per chromatic-aberration
/// channel pass with just `tint` swapped (`RenderContext { tint, ..ctx }`).
#[derive(Clone, Copy)]
pub struct RenderContext<'a> {
pub pal: &'a Palette,
pub fit: f32,
pub scale: f32,
pub warp: f32,
pub glow: bool,
pub seg: usize,
pub tint: [f32; 3],
// GPU-path extras (ignored by the Draw-based modes).
pub feedback: bool,
pub fade: f32,
pub ca_px: f32,
pub drive: f32,
}
/// One visualiser mode. `Draw`-based modes implement [`draw`](Self::draw);
/// a GPU mode sets [`is_gpu`](Self::is_gpu) and implements
/// [`render_gpu`](Self::render_gpu)/[`current_tex`](Self::current_tex)/
/// [`capture_raw`](Self::capture_raw) instead — the bin dispatches on
/// `is_gpu()`. Object-safe: held as `Box<dyn Visualizer>`.
pub trait Visualizer {
fn name(&self) -> &'static str;
fn seed(&self) -> u64;
fn reseed(&mut self, seed: u64);
fn update(&mut self, b: &Bands, dt: f32);
/// Element count for the HUD (tendrils / beam points / capsules).
fn element_count(&self) -> usize;
/// `true` ⇒ this mode owns a wgpu pipeline and uses the GPU methods
/// below; `false` ⇒ it draws through `Draw`/`Post`.
fn is_gpu(&self) -> bool {
false
}
/// CPU/`Draw` path. No-op for GPU-driven modes.
fn draw(&self, _d: &Draw, _ctx: &RenderContext) {}
/// GPU path: render this frame's own pipeline target and return it.
/// `None` for `Draw`-based modes (the bin uses the `Post` chain).
fn render_gpu(
&mut self,
_device: &wgpu::Device,
_queue: &wgpu::Queue,
_ctx: &RenderContext,
) -> Option<&wgpu::Texture> {
None
}
/// The texture to present this frame (GPU modes only; else `None` and the
/// bin presents the `Post` accumulator).
fn current_tex(&self) -> Option<&wgpu::Texture> {
None
}
/// Synchronous RGBA readback of the current target (GPU modes only).
fn capture_raw(
&self,
_device: &wgpu::Device,
_queue: &wgpu::Queue,
) -> Option<anyhow::Result<Vec<u8>>> {
None
}
}
impl Visualizer for Sigil {
fn name(&self) -> &'static str {
"sigil"
}
fn seed(&self) -> u64 {
self.seed
}
fn reseed(&mut self, seed: u64) {
Sigil::reseed(self, seed)
}
fn update(&mut self, b: &Bands, dt: f32) {
Sigil::update(self, b, dt)
}
fn element_count(&self) -> usize {
self.tendril_count()
}
fn draw(&self, d: &Draw, c: &RenderContext) {
Sigil::draw(self, d, c.pal, c.fit, c.scale, c.warp, c.glow, c.seg, c.tint)
}
}
impl Visualizer for Scope {
fn name(&self) -> &'static str {
"scope"
}
fn seed(&self) -> u64 {
self.seed
}
fn reseed(&mut self, seed: u64) {
Scope::reseed(self, seed)
}
fn update(&mut self, b: &Bands, dt: f32) {
Scope::update(self, b, dt)
}
fn element_count(&self) -> usize {
self.point_count()
}
fn draw(&self, d: &Draw, c: &RenderContext) {
Scope::draw(self, d, c.pal, c.fit, c.scale, c.warp, c.glow, c.seg, c.tint)
}
}
impl Visualizer for Breakcore {
fn name(&self) -> &'static str {
"breakcore"
}
fn seed(&self) -> u64 {
self.seed
}
fn reseed(&mut self, seed: u64) {
Breakcore::reseed(self, seed)
}
fn update(&mut self, b: &Bands, dt: f32) {
Breakcore::update(self, b, dt)
}
fn element_count(&self) -> usize {
self.point_count()
}
fn is_gpu(&self) -> bool {
true
}
fn render_gpu(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
c: &RenderContext,
) -> Option<&wgpu::Texture> {
Some(Breakcore::render(
self, device, queue, c.pal, c.scale, c.warp, c.feedback, c.fade, c.ca_px, c.drive,
))
}
fn current_tex(&self) -> Option<&wgpu::Texture> {
Some(self.current())
}
fn capture_raw(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
) -> Option<anyhow::Result<Vec<u8>>> {
Some(Breakcore::capture_raw(self, device, queue))
}
}
/// Live `M`-key cycle: Sigil → Scope → Breakcore → Sigil, keeping the seed.
/// `Breakcore` needs the device to (re)build its pipeline.
pub fn next_visual(cur: &dyn Visualizer, device: &wgpu::Device) -> Box<dyn Visualizer> {
let s = cur.seed();
match cur.name() {
"sigil" => Box::new(Scope::new(s)),
"scope" => Box::new(Breakcore::new(s, device)),
_ => Box::new(Sigil::new(s)),
}
}