Files
sigil/src/viz/core.rs
T
2026-05-20 16:08:00 +02:00

191 lines
6.0 KiB
Rust

//! The visualiser contract: one trait the bin drives, one render context.
//!
//! The bin holds a `Box<dyn Visualizer>` and talks to this trait instead of
//! hard-coding a mode. `breakcore` is the only mode today; the trait is kept
//! deliberately general so a future GPU visualiser is a new `impl Visualizer`
//! over the shared aspect-aware [`crate::viz::shader::ShaderPipeline`] base —
//! nothing in the bin's hot path changes.
//!
//! Every mode is GPU-driven now (`is_gpu` → true): it owns a wgpu pipeline,
//! renders into its own non-square target and presents/captures it directly.
//! The trait still carries the `Draw` no-op default so a future CPU mode can
//! be added without reshaping the contract.
//!
//! Determinism is unaffected: dispatch is pure indirection; the visualiser's
//! `update` still advances its `Rng`/integration once per frame, so `--render`
//! stays bit-reproducible.
use crate::audio::Bands;
use crate::viz::breakcore::Breakcore;
use crate::viz::fingerprint::Fingerprint;
use crate::viz::monolith::Monolith;
use crate::viz::palette::Palette;
use nannou::prelude::Draw;
use nannou::wgpu;
/// Per-frame shared tunables handed to a visualiser. Cheap to copy (it only
/// borrows the palette).
#[derive(Clone, Copy)]
pub struct RenderContext<'a> {
pub pal: &'a Palette,
pub scale: f32,
pub warp: f32,
pub feedback: bool,
pub fade: f32,
pub ca_px: f32,
pub drive: f32,
/// Hard march-step ceiling for this run (preset/cfg gated, ≤ the shader's
/// own absolute cap). The visualiser must not request more.
pub march_cap: u32,
}
/// One visualiser mode. GPU modes set [`is_gpu`](Self::is_gpu) and implement
/// [`render_gpu`](Self::render_gpu)/[`current_tex`](Self::current_tex)/
/// [`capture_raw`](Self::capture_raw); a future `Draw` mode would implement
/// [`draw`](Self::draw) instead. 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 (capsule control points).
fn element_count(&self) -> usize;
/// `true` ⇒ this mode owns a wgpu pipeline and uses the GPU methods
/// below; `false` ⇒ it draws through `Draw`.
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.
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).
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
}
/// Commit a song-fingerprint to this visualiser. Modes that pick their
/// archetype from per-track stats use it; modes that don't ignore it.
/// The bin calls this once in `--render` (pre-computed from the
/// `Timeline`) and once in live as soon as the live accumulator is
/// ready. Default = no-op so existing modes need no edits.
fn install_fingerprint(&mut self, _fp: Fingerprint) {}
/// `true` once the visualiser has committed a fingerprint. Stays `true`
/// for modes that don't use one (their archetype is always "ready").
fn fingerprint_ready(&self) -> bool {
true
}
}
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,
c.march_cap,
))
}
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))
}
}
impl Visualizer for Monolith {
fn name(&self) -> &'static str {
"monolith"
}
fn seed(&self) -> u64 {
self.seed
}
fn reseed(&mut self, seed: u64) {
Monolith::reseed(self, seed)
}
fn update(&mut self, b: &Bands, dt: f32) {
Monolith::update(self, b, dt)
}
fn element_count(&self) -> usize {
Monolith::element_count(self)
}
fn is_gpu(&self) -> bool {
true
}
fn render_gpu(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
c: &RenderContext,
) -> Option<&wgpu::Texture> {
Some(Monolith::render(
self, device, queue, c.pal, c.scale, c.warp, c.feedback, c.fade, c.ca_px, c.drive,
c.march_cap,
))
}
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(Monolith::capture_raw(self, device, queue))
}
fn install_fingerprint(&mut self, fp: Fingerprint) {
Monolith::install_fingerprint(self, fp)
}
fn fingerprint_ready(&self) -> bool {
self.fingerprint_committed()
}
}