added monolith

This commit is contained in:
2026-05-20 16:08:00 +02:00
parent b818f4bc43
commit 2c3418f608
15 changed files with 1872 additions and 1352 deletions
+81 -78
View File
@@ -1,63 +1,58 @@
//! 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 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.
//!
//! 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.
//! 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; each visualiser's
//! `update` still advances its `Rng`/integration once per frame exactly as
//! before, so `--render` stays bit-reproducible.
//! 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 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 }`).
/// borrows the palette).
#[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,
/// 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. `Draw`-based modes implement [`draw`](Self::draw);
/// a GPU mode sets [`is_gpu`](Self::is_gpu) and implements
/// 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) instead — the bin dispatches on
/// `is_gpu()`. Object-safe: held as `Box<dyn Visualizer>`.
/// [`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 (tendrils / beam points / capsules).
/// 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`/`Post`.
/// below; `false` ⇒ it draws through `Draw`.
fn is_gpu(&self) -> bool {
false
}
@@ -66,7 +61,7 @@ pub trait Visualizer {
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).
/// `None` for `Draw`-based modes.
fn render_gpu(
&mut self,
_device: &wgpu::Device,
@@ -76,8 +71,7 @@ pub trait Visualizer {
None
}
/// The texture to present this frame (GPU modes only; else `None` and the
/// bin presents the `Post` accumulator).
/// The texture to present this frame (GPU modes only).
fn current_tex(&self) -> Option<&wgpu::Texture> {
None
}
@@ -90,47 +84,18 @@ pub trait Visualizer {
) -> 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)
}
}
/// 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) {}
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)
/// `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
}
}
@@ -161,6 +126,7 @@ impl Visualizer for Breakcore {
) -> 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> {
@@ -175,13 +141,50 @@ impl Visualizer for Breakcore {
}
}
/// 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)),
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()
}
}