use crate::rng::{ShaderStream, StreamRng, hue_offsets};
use crate::runtime::{Env, NUM_Q, PerFrame, PerPixel, ShapeRt, WaveRt, compile_opt};
use crate::textures::{Image, TextureProvider, pick_random, rand_slot};
use crate::{
EngineConfig, EngineError, ForcedFrame, FrameClock, PresetReport, ShaderFallback,
ShaderOutcome, Transition, TransitionInfo,
};
use lactea_abi::{PassKind, ShaderInterface, TexSource};
use lactea_audio::{Analyzer, AudioFrame};
use lactea_eel::GlobalMemory;
use lactea_hlsl::{Options as HlslOptions, default_comp_shader, default_warp_shader, translate};
use lactea_preset::{
BaseValues, Flavor, MAX_CUSTOM_SHAPES, MAX_CUSTOM_WAVES, Preset, code_to_source,
};
use lactea_render::warp_mesh::{self, VertexVars, WarpAnim};
use lactea_render::wavegen::{CustomWaveSampling, NUM_WAVEFORM_SAMPLES};
use lactea_render::{
Compat, CompiledShader, CustomWave as RenderWave, FrameInput, MeshGeometry, PassRandom,
RenderConfig, Renderer, ShapeInstance, TransitionBlender, TransitionKind, TransitionParams,
WarpGpu, WavePoint,
};
use std::collections::HashMap;
use std::sync::Arc;
use wgpu::util::DeviceExt;
mod md2_blend;
use md2_blend::{BlendBase, BlendState, build_blend_frame};
const PASSTHROUGH_COMP: &str = "shader_body\n{\nret = tex2D(sampler_main, uv).xyz;\n}";
const ROT_NAMES: [&str; 24] = [
"rot_s1",
"rot_s2",
"rot_s3",
"rot_s4",
"rot_d1",
"rot_d2",
"rot_d3",
"rot_d4",
"rot_f1",
"rot_f2",
"rot_f3",
"rot_f4",
"rot_vf1",
"rot_vf2",
"rot_vf3",
"rot_vf4",
"rot_uf1",
"rot_uf2",
"rot_uf3",
"rot_uf4",
"rot_rand1",
"rot_rand2",
"rot_rand3",
"rot_rand4",
];
struct Loaded {
name: String,
preset: Preset,
pf: PerFrame,
pp: PerPixel,
waves: Vec<WaveRt>,
shapes: Vec<ShapeRt>,
frame_q: [f64; NUM_Q],
warp_stream: Option<ShaderStream>,
comp_stream: Option<ShaderStream>,
hue_offsets: [f32; 4],
has_warp: bool,
has_comp: bool,
frames: u64,
floats: BlendBase,
}
#[derive(Default)]
struct LastFrame {
regs: Vec<f64>,
audio: Option<AudioFrame>,
time: f32,
frame: u32,
progress: f32,
warp: Option<PassRandom>,
comp: Option<PassRandom>,
}
struct Slot {
renderer: Renderer,
loaded: Loaded,
last: LastFrame,
}
#[derive(Clone, Copy, Debug)]
struct PresetClock {
now: f64,
preset_time_a: f64,
preset_time_b: f64,
smoothing: bool,
soft_cut: f64,
preset_duration: f64,
}
impl PresetClock {
fn start_preset(&mut self) {
self.smoothing = false;
self.preset_time_a = self.now;
}
fn start_smoothing(&mut self, soft_cut: f64) {
self.smoothing = true;
self.preset_time_b = self.now;
self.soft_cut = soft_cut;
}
fn end_smoothing(&mut self) {
self.smoothing = false;
self.preset_time_a = self.preset_time_b;
}
fn smooth_ratio(&self) -> f64 {
(self.now - self.preset_time_b) / self.soft_cut
}
fn progress_a(&self) -> f64 {
if self.smoothing {
1.0
} else {
((self.now - self.preset_time_a) / self.preset_duration).min(1.0)
}
}
}
#[derive(Clone, Copy, Debug)]
struct ActiveTransition {
kind: TransitionKind,
rand_static: [i32; 4],
start: f64,
duration: f64,
last_draw: f64,
}
impl ActiveTransition {
fn is_done(&self, now: f64) -> bool {
self.duration <= 0.0 || now - self.start >= self.duration
}
fn progress(&self, now: f64) -> f64 {
((now - self.start) / self.duration).clamp(0.0, 1.0)
}
}
pub struct Engine {
cfg: EngineConfig,
device: wgpu::Device,
queue: wgpu::Queue,
analyzer: Analyzer,
provider: Option<Box<dyn TextureProvider>>,
active: Option<Slot>,
incoming: Option<Slot>,
spare: Option<Renderer>,
blender: Option<TransitionBlender>,
transition: Option<ActiveTransition>,
clock: PresetClock,
rng_owner: GlobalMemory,
transition_select: StreamRng,
transition_static: StreamRng,
forced_kind: Option<TransitionKind>,
forced_static: Option<[i32; 4]>,
last_time: f64,
last_frame_no: u32,
forced: Option<ForcedFrame>,
forced_incoming: Option<ForcedFrame>,
forced_blend: Option<(Vec<u8>, Vec<u8>)>,
md2: Option<BlendState>,
md2_vertinfo: Vec<[f32; 2]>,
md2_pattern_rng: StreamRng,
md2_clocks: md2_blend::PatternClocks,
md2_forced_pattern: Option<u32>,
on_switch_failed: crate::callbacks::Hook<crate::callbacks::SwitchFailure>,
}
fn eel_seed(run_seed: u64) -> u64 {
let s =
0x4141_f00du32.wrapping_add((run_seed.wrapping_sub(1)).wrapping_mul(0x9E37_79B9) as u32);
u64::from(s)
}
impl Engine {
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, cfg: EngineConfig) -> Self {
let rc = RenderConfig {
width: cfg.width.max(1),
height: cfg.height.max(1),
mesh: cfg.mesh.map(|m| m.max(2).next_multiple_of(2)),
texel_offset: cfg.texel_offset,
output_format: cfg.output_format,
seed: cfg.seed,
compat: cfg.compat,
stroke: Default::default(),
};
let renderer =
Renderer::new(device, queue, rc).expect("the render config was sanitised above");
let rng_owner = GlobalMemory::new();
rng_owner.set_rng_seed(eel_seed(cfg.seed));
Self {
analyzer: Analyzer::new(cfg.sample_rate),
device: device.clone(),
queue: queue.clone(),
provider: None,
active: None,
incoming: None,
spare: Some(renderer),
blender: None,
transition: None,
clock: PresetClock {
now: 0.0,
preset_time_a: 0.0,
preset_time_b: 0.0,
smoothing: false,
soft_cut: 3.0,
preset_duration: cfg.preset_duration,
},
rng_owner,
transition_select: StreamRng::new(cfg.seed, "transition_select"),
transition_static: StreamRng::new(cfg.seed, "transition_static"),
forced_kind: None,
forced_static: None,
last_time: 0.0,
last_frame_no: 0,
forced: None,
forced_incoming: None,
forced_blend: None,
md2: None,
md2_vertinfo: Vec::new(),
md2_pattern_rng: StreamRng::new(cfg.seed, "md2_blend_pattern"),
md2_clocks: Default::default(),
md2_forced_pattern: None,
on_switch_failed: Default::default(),
cfg,
}
}
pub fn config(&self) -> &EngineConfig {
&self.cfg
}
pub fn renderer(&self) -> &Renderer {
self.active
.as_ref()
.map(|s| &s.renderer)
.or(self.spare.as_ref())
.expect("engine always owns a renderer")
}
pub fn renderer_mut(&mut self) -> &mut Renderer {
match self.active.as_mut() {
Some(s) => &mut s.renderer,
None => self.spare.as_mut().expect("engine always owns a renderer"),
}
}
pub fn incoming_renderer(&self) -> Option<&Renderer> {
self.incoming.as_ref().map(|s| &s.renderer)
}
pub fn audio_frame(&self) -> &AudioFrame {
self.analyzer.frame()
}
pub fn preset_name(&self) -> Option<&str> {
self.incoming
.as_ref()
.or(self.active.as_ref())
.map(|s| s.loaded.name.as_str())
}
pub fn active_preset_name(&self) -> Option<&str> {
self.active.as_ref().map(|s| s.loaded.name.as_str())
}
pub fn transition_progress(&self) -> Option<f64> {
if let Some(m) = &self.md2 {
return Some(f64::from(m.progress).clamp(0.0, 1.0));
}
match (&self.transition, &self.incoming) {
(Some(t), Some(_)) => Some(t.progress(self.clock.now)),
_ => None,
}
}
pub fn set_md2_blend_pattern(&mut self, pattern: Option<u32>) {
self.md2_forced_pattern = pattern.map(|k| k.min(md2_blend::TRANSITION_LIMIT));
}
pub fn md2_blend_pattern(&self) -> Option<u32> {
self.md2.as_ref().map(|m| m.pattern_kind)
}
pub fn transition_info(&self) -> Option<TransitionInfo> {
match (&self.transition, &self.incoming) {
(Some(t), Some(_)) => Some(TransitionInfo {
kind: t.kind,
rand_static: t.rand_static,
start_time: t.start,
duration: t.duration,
}),
_ => None,
}
}
pub fn force_next_transition(
&mut self,
kind: Option<TransitionKind>,
rand_static: Option<[i32; 4]>,
) {
self.forced_kind = kind;
self.forced_static = rand_static;
}
pub fn set_texture_provider(&mut self, provider: Box<dyn TextureProvider>) {
self.provider = Some(provider);
}
pub fn push_pcm_f32(&mut self, interleaved: &[f32], channels: usize) {
self.analyzer.push_pcm_f32(interleaved, channels);
}
pub(crate) fn all_renderers_mut(&mut self) -> Vec<&mut Renderer> {
let mut v: Vec<&mut Renderer> = Vec::new();
if let Some(s) = self.active.as_mut() {
v.push(&mut s.renderer);
}
if let Some(s) = self.incoming.as_mut() {
v.push(&mut s.renderer);
}
if let Some(r) = self.spare.as_mut() {
v.push(r);
}
v
}
pub fn resize(&mut self, width: u32, height: u32) {
let (width, height) = (width.max(1), height.max(1));
self.cfg.width = width;
self.cfg.height = height;
for r in self.all_renderers_mut() {
let _ = r.resize(width, height);
}
}
pub fn force_next_frame(&mut self, forced: Option<ForcedFrame>) {
self.forced = forced;
}
pub fn force_next_blend_inputs(&mut self, inputs: Option<(Vec<u8>, Vec<u8>)>) {
self.forced_blend = inputs;
}
pub fn force_next_incoming_frame(&mut self, forced: Option<ForcedFrame>) {
self.forced_incoming = forced;
}
pub fn load_preset(
&mut self,
bytes: &[u8],
name: &str,
transition: Transition,
) -> Result<PresetReport, EngineError> {
let result = self.load_preset_inner(bytes, name, transition);
if let Err(e) = &result {
self.on_switch_failed
.fire(&crate::callbacks::SwitchFailure {
name: name.to_string(),
message: e.to_string(),
});
}
result
}
fn load_preset_inner(
&mut self,
bytes: &[u8],
name: &str,
transition: Transition,
) -> Result<PresetReport, EngineError> {
let transition = if self.cfg.compat.rules().md2_preset_blend {
match transition {
Transition::Soft { seconds } if seconds > 0.0 && self.active.is_some() => {
return self.load_preset_md2_soft(bytes, name, seconds);
}
Transition::Soft { .. } => Transition::Cut,
t => t,
}
} else {
transition
};
let mut renderer = match self.spare.take() {
Some(r) => r,
None => self
.active
.as_ref()
.expect("engine always owns a renderer")
.renderer
.sibling(),
};
renderer.reset_images();
match self.build_loaded(bytes, name, &mut renderer) {
Ok((loaded, report)) => {
self.commit(
Slot {
renderer,
loaded,
last: LastFrame::default(),
},
transition,
);
Ok(report)
}
Err(e) => {
self.spare = Some(renderer);
Err(e)
}
}
}
fn commit(&mut self, mut slot: Slot, transition: Transition) {
let soft = match transition {
Transition::Soft { seconds } if self.active.is_some() => Some(seconds),
_ => None,
};
if self.incoming.is_some() {
self.promote_incoming();
}
if let Some(act) = self.active.as_ref().filter(|_| !self.cfg.start_clean) {
let mut enc = self.device.create_command_encoder(&Default::default());
let _ = slot.renderer.draw_initial_image(&mut enc, &act.renderer);
self.queue.submit(Some(enc.finish()));
}
match soft {
None => {
if let Some(mut old) = self.active.replace(slot) {
self.md2 = None;
old.renderer.set_old_shader_state(None);
self.spare = Some(old.renderer);
}
self.clock.start_preset();
}
Some(seconds) => {
let idx = self.transition_select.next32();
let mut rand_static = [0i32; 4];
for r in rand_static.iter_mut() {
*r = self.transition_static.next32() as i32;
}
self.incoming = Some(slot);
self.clock.start_smoothing(seconds);
self.transition = Some(ActiveTransition {
kind: self
.forced_kind
.take()
.unwrap_or_else(|| TransitionKind::from_index(idx)),
rand_static: self.forced_static.take().unwrap_or(rand_static),
start: self.clock.now,
duration: seconds,
last_draw: 0.0,
});
}
}
}
fn promote_incoming(&mut self) {
if let Some(inc) = self.incoming.take()
&& let Some(old) = self.active.replace(inc)
{
self.spare = Some(old.renderer);
}
self.transition = None;
}
fn build_loaded(
&mut self,
bytes: &[u8],
name: &str,
renderer: &mut Renderer,
) -> Result<(Loaded, PresetReport), EngineError> {
let flavor = match self.cfg.compat {
Compat::MilkDrop2 => Flavor::Md2,
Compat::ProjectM => Flavor::ProjectM,
};
let preset = Preset::parse_with(bytes, flavor)?;
let mut report = PresetReport {
name: name.to_string(),
preset_version: preset.milkdrop_preset_version,
..Default::default()
};
let gm = Arc::new(GlobalMemory::sharing_rng_with(&self.rng_owner));
let env = self.init_env();
let mut pf = PerFrame::new(&gm);
let mut pp = PerPixel::new(&gm);
let base = preset.base.clone();
pf.load_state(&base, &env);
if let Some(prog) = compile_opt(&mut pf.ctx, &preset.per_frame_init)
.map_err(|e| EngineError::compile("per-frame init code", e))?
{
pf.ctx.run(&prog);
}
for i in 0..NUM_Q {
pf.q_after_init[i] = pf.q(i);
}
let mut frame_q = pf.q_after_init;
pf.prog = compile_opt(&mut pf.ctx, &preset.per_frame)
.map_err(|e| EngineError::compile("per-frame code", e))?;
pp.prog = compile_opt(&mut pp.ctx, &preset.per_pixel)
.map_err(|e| EngineError::compile("per-pixel code", e))?;
let mut waves = Vec::with_capacity(MAX_CUSTOM_WAVES);
let slots = self.cfg.compat.rules().custom_slots;
for (i, cfg) in preset.waves.iter().take(slots).enumerate() {
let mut w = WaveRt::new(&gm, cfg.clone());
let enabled = w.enabled();
let step = |what: &str, r: Result<(), EngineError>, report: &mut PresetReport| match r {
Ok(()) => Ok(()),
Err(e) if !enabled => {
report
.warnings
.push(format!("custom wave {i} (disabled) {what}: {e}"));
Ok(())
}
Err(e) => Err(e),
};
w.load_state(&pf, &env);
let init = compile_opt(&mut w.pf, &cfg.init)
.map_err(|e| EngineError::compile(&format!("custom wave {i} init code"), e));
let r = init.map(|p| {
if let Some(p) = p {
w.pf.run(&p);
}
});
step("init", r, &mut report)?;
for t in 0..crate::runtime::NUM_T {
w.t_after_init[t] = w.pf.get(w.pfv.t[t]);
}
let r = compile_opt(&mut w.pf, &cfg.per_frame)
.map(|p| w.pf_prog = p)
.map_err(|e| EngineError::compile(&format!("custom wave {i} per-frame code"), e));
step("per-frame", r, &mut report)?;
let r = compile_opt(&mut w.pp, &cfg.per_point)
.map(|p| w.pp_prog = p)
.map_err(|e| EngineError::compile(&format!("custom wave {i} per-point code"), e));
step("per-point", r, &mut report)?;
waves.push(w);
}
let mut shapes = Vec::with_capacity(MAX_CUSTOM_SHAPES);
for (i, cfg) in preset.shapes.iter().take(slots).enumerate() {
let mut s = ShapeRt::new(&gm, cfg.clone());
let enabled = s.enabled();
let step = |what: &str, r: Result<(), EngineError>, report: &mut PresetReport| match r {
Ok(()) => Ok(()),
Err(e) if !enabled => {
report
.warnings
.push(format!("custom shape {i} (disabled) {what}: {e}"));
Ok(())
}
Err(e) => Err(e),
};
s.load_state(&frame_q, &env, 0);
let init = compile_opt(&mut s.ctx, &cfg.init)
.map_err(|e| EngineError::compile(&format!("custom shape {i} init code"), e));
let r = init.map(|p| {
if let Some(p) = p {
s.ctx.run(&p);
}
});
step("init", r, &mut report)?;
for t in 0..crate::runtime::NUM_T {
s.t_after_init[t] = s.ctx.get(s.v.t[t]);
}
let r = compile_opt(&mut s.ctx, &cfg.per_frame)
.map(|p| s.prog = p)
.map_err(|e| EngineError::compile(&format!("custom shape {i} per-frame code"), e));
step("per-frame", r, &mut report)?;
shapes.push(s);
}
frame_q = pf.q_after_init;
let (warp, comp) = self.build_shaders(&preset, &mut report);
let warp_iface = warp.as_ref().map(|s| s.interface.clone());
let comp_iface = comp.as_ref().map(|s| s.interface.clone());
let (has_warp, has_comp) = Self::install_shaders(renderer, warp, comp, &mut report);
let ifaces: Vec<ShaderInterface> = warp_iface
.filter(|_| has_warp)
.into_iter()
.chain(comp_iface.filter(|_| has_comp))
.collect();
self.resolve_textures(renderer, &ifaces, &mut report);
self.resolve_shape_images(renderer, &preset, &mut report);
let compat = self.cfg.compat;
let (warp_stream, comp_stream) = if compat == Compat::MilkDrop2 {
let shared = ShaderStream::new(self.cfg.seed, "shader_warp");
(
has_warp.then(|| shared.clone()),
has_comp.then(|| shared.clone()),
)
} else {
(
has_warp.then(|| ShaderStream::new(self.cfg.seed, "shader_warp")),
has_comp.then(|| ShaderStream::new(self.cfg.seed, "shader_comp")),
)
};
let loaded = Loaded {
name: name.to_string(),
preset,
pf,
pp,
waves,
shapes,
frame_q,
warp_stream,
comp_stream,
hue_offsets: hue_offsets(self.cfg.seed),
has_warp,
has_comp,
frames: 0,
floats: Default::default(),
};
Ok((loaded, report))
}
fn init_env(&self) -> Env {
let (w, h) = (self.cfg.width, self.cfg.height);
let (ax, ay) = warp_mesh::aspect_xy(w, h);
Env {
time: self.last_time as f32,
fps: self.cfg.fps as f32,
frame: self.last_frame_no,
progress: self.clock.progress_a() as f32,
mesh: self.cfg.mesh,
pixels: [w, h],
aspect: [ax, ay],
inv_aspect: [1.0 / ax, 1.0 / ay],
..Default::default()
}
}
fn build_shaders(
&self,
p: &Preset,
report: &mut PresetReport,
) -> (Option<CompiledShader>, Option<CompiledShader>) {
let md2 = self.cfg.compat == Compat::MilkDrop2;
let mut opts = HlslOptions::default();
opts.floor_mod = !md2;
opts.projectm_flavor = !md2;
let to_compiled = |t: lactea_hlsl::Translated, report: &mut PresetReport| {
report.warnings.extend(t.warnings.iter().cloned());
CompiledShader {
wgsl: t.wgsl,
interface: t.interface,
}
};
let mut warp = None;
if p.warp_ps_version > 0 {
let text = p.warp_shader.as_deref().map(code_to_source);
match text.filter(|t| !t.trim().is_empty()) {
Some(src) => match translate(PassKind::Warp, &src, &opts) {
Ok(t) => {
report.warp = ShaderOutcome::User;
warp = Some(to_compiled(t, report));
}
Err(e) => {
report.warp = ShaderOutcome::Fallback;
report.shader_fallbacks.push(ShaderFallback {
pass: PassKind::Warp,
reason: e.to_string(),
fallback: "default (no-shader) warp".into(),
});
}
},
None if md2 => {
let src = default_warp_shader(p.base.decay, p.base.tex_wrap);
match translate(PassKind::Warp, &src, &opts) {
Ok(t) => {
report.warp = ShaderOutcome::Default;
warp = Some(to_compiled(t, report));
}
Err(e) => report.warnings.push(format!("default warp shader: {e}")),
}
}
None => report.warp = ShaderOutcome::Default,
}
}
let mut comp = None;
if p.comp_ps_version > 0 {
let text = p.comp_shader.as_deref().map(code_to_source);
let default_src = if md2 {
default_comp_shader(
p.base.gamma_adj,
p.base.video_echo_alpha,
p.base.video_echo_zoom,
p.base.video_echo_orientation,
p.base.shader,
p.base.brighten,
p.base.darken,
p.base.solarize,
p.base.invert,
)
} else {
PASSTHROUGH_COMP.to_string()
};
let user = text.filter(|t| !t.trim().is_empty());
let mut used_default = user.is_none();
if let Some(src) = &user {
match translate(PassKind::Comp, src, &opts) {
Ok(t) => {
report.comp = ShaderOutcome::User;
comp = Some(to_compiled(t, report));
}
Err(e) => {
report.comp = ShaderOutcome::Fallback;
report.shader_fallbacks.push(ShaderFallback {
pass: PassKind::Comp,
reason: e.to_string(),
fallback: "default composite shader".into(),
});
used_default = true;
}
}
}
if used_default {
if report.comp != ShaderOutcome::Fallback {
report.comp = ShaderOutcome::Default;
}
match translate(PassKind::Comp, &default_src, &opts) {
Ok(t) => comp = Some(to_compiled(t, report)),
Err(e) => report
.warnings
.push(format!("default composite shader: {e}")),
}
}
}
(warp, comp)
}
fn install_shaders(
renderer: &mut Renderer,
warp: Option<CompiledShader>,
comp: Option<CompiledShader>,
report: &mut PresetReport,
) -> (bool, bool) {
let mut warp_ok = false;
let mut comp_ok = false;
let had_warp = warp.is_some();
if had_warp {
match renderer.set_warp_shader(warp) {
Ok(()) => warp_ok = true,
Err(e) => {
report.warp = ShaderOutcome::Fallback;
report.shader_fallbacks.push(ShaderFallback {
pass: PassKind::Warp,
reason: e.to_string(),
fallback: "default (no-shader) warp".into(),
});
let _ = renderer.set_warp_shader(None);
}
}
} else {
let _ = renderer.set_warp_shader(None);
}
if let Some(c) = comp {
match renderer.set_comp_shader(Some(c)) {
Ok(()) => comp_ok = true,
Err(e) => {
report.comp = ShaderOutcome::Fallback;
report.shader_fallbacks.push(ShaderFallback {
pass: PassKind::Comp,
reason: e.to_string(),
fallback: "default composite shader".into(),
});
let passthrough =
translate(PassKind::Comp, PASSTHROUGH_COMP, &HlslOptions::default())
.ok()
.map(|t| CompiledShader {
wgsl: t.wgsl,
interface: t.interface,
});
if let Some(p) = passthrough {
comp_ok = renderer.set_comp_shader(Some(p)).is_ok();
}
}
}
} else {
let _ = renderer.set_comp_shader(None);
}
(warp_ok, comp_ok)
}
fn resolve_textures(
&mut self,
renderer: &mut Renderer,
ifaces: &[ShaderInterface],
report: &mut PresetReport,
) {
let mut wanted: Vec<(String, String)> = Vec::new(); for iface in ifaces {
for t in &iface.textures {
if let TexSource::User(name) = &t.source {
if wanted.iter().any(|(n, _)| n == name) {
continue;
}
let pm = t
.hlsl_name
.strip_prefix("sampler_")
.unwrap_or(&t.hlsl_name)
.to_string();
wanted.push((name.clone(), pm));
}
}
}
if wanted.is_empty() {
return;
}
let seed = self.cfg.seed;
let Some(provider) = self.provider.as_mut() else {
for (name, _) in wanted {
report.unresolved_textures.push(name);
}
return;
};
let pool = provider.random_pool();
let mut slot_pick: HashMap<usize, Option<String>> = HashMap::new();
let mut cache: HashMap<String, Option<Image>> = HashMap::new();
for (name, pm_name) in wanted {
let provider_name = match rand_slot(&name) {
Some(slot) => slot_pick
.entry(slot)
.or_insert_with(|| pick_random(seed, &pm_name, &pool))
.clone(),
None => Some(name.clone()),
};
let Some(pn) = provider_name else {
report.unresolved_textures.push(name);
continue;
};
let img = cache
.entry(pn.clone())
.or_insert_with(|| provider.load(&pn))
.clone();
match img {
Some(img)
if img.width > 0
&& img.height > 0
&& img.rgba8.len() == (img.width * img.height * 4) as usize =>
{
match renderer.set_user_texture(&name, img.width, img.height, &img.rgba8) {
Ok(()) => report.resolved_textures.push((name, pn)),
Err(e) => {
report.warnings.push(format!("texture {name}: {e}"));
report.unresolved_textures.push(name);
}
}
}
_ => report.unresolved_textures.push(name),
}
}
}
pub fn render(
&mut self,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
clock: FrameClock,
) {
let dt = (clock.time - self.last_time).max(0.0);
self.last_time = clock.time;
self.last_frame_no = clock.frame;
self.analyzer.update_dt(dt);
let mut audio = self.analyzer.frame().clone();
let forced = self.forced.take();
let forced_incoming = self.forced_incoming.take();
let forced_blend = self.forced_blend.take();
if let Some(f) = &forced {
f.override_audio(&mut audio);
}
self.clock.now = clock.time;
if self.clock.smoothing && self.incoming.is_some() && self.clock.smooth_ratio() >= 1.0 {
self.clock.end_smoothing();
}
let mut progress = self.clock.progress_a() as f32;
if let Some(m) = self.md2.as_mut() {
if m.update(clock.time as f32) {
progress = m.preset_progress(clock.time, self.cfg.preset_duration);
} else {
self.md2 = None;
if let Some(a) = self.active.as_mut() {
a.renderer.set_old_shader_state(None);
}
}
}
if self.incoming.is_some()
&& self
.transition
.as_ref()
.is_some_and(|t| t.is_done(clock.time))
{
self.promote_incoming();
}
let blending = self.incoming.is_some() && self.transition.is_some();
if let Some(inc) = self.incoming.as_mut().filter(|_| blending) {
let fi = prepare_slot(
inc,
&self.cfg,
clock,
progress,
&audio,
forced_incoming.as_ref(),
None,
);
let _ = inc.renderer.render_frame_to_output(encoder, &fi);
}
let cfg = &self.cfg;
match self.active.as_mut() {
Some(act) => {
let fi = prepare_slot(
act,
cfg,
clock,
progress,
&audio,
forced.as_ref(),
self.md2.as_mut(),
);
let _ = if blending {
act.renderer.render_frame_to_output(encoder, &fi)
} else {
act.renderer.render_frame(encoder, &fi, target)
};
}
None => {
let fi = FrameInput {
time: f64::from(clock.time as f32),
frame: clock.frame,
fps: cfg.fps as f32,
..Default::default()
};
if let Some(r) = self.spare.as_mut() {
let _ = r.render_frame(encoder, &fi, target);
}
}
}
if blending {
self.draw_transition(encoder, target, clock.time, forced_blend);
}
}
fn draw_transition(
&mut self,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
now: f64,
forced: Option<(Vec<u8>, Vec<u8>)>,
) {
let (Some(act), Some(inc), Some(tr)) = (
self.active.as_ref(),
self.incoming.as_ref(),
self.transition.as_mut(),
) else {
return;
};
let blender = self
.blender
.get_or_insert_with(|| TransitionBlender::new(&self.device, self.cfg.output_format));
let params = TransitionParams::new(
tr.kind,
tr.rand_static,
tr.duration,
now - tr.start,
now - tr.last_draw,
);
tr.last_draw = now;
let size = (self.cfg.width, self.cfg.height);
match forced {
Some((old, new)) => {
let mk = |data: &[u8]| {
let t = self.device.create_texture_with_data(
&self.queue,
&wgpu::TextureDescriptor {
label: Some("forced transition input"),
size: wgpu::Extent3d {
width: size.0,
height: size.1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
wgpu::util::TextureDataOrder::LayerMajor,
data,
);
t.create_view(&Default::default())
};
let (o, n) = (mk(&old), mk(&new));
blender.draw(encoder, &o, &n, target, size, ¶ms);
}
None => blender.draw(
encoder,
act.renderer.output_view(),
inc.renderer.output_view(),
target,
size,
¶ms,
),
}
}
pub fn frame_vars(&self) -> Vec<(String, f64)> {
self.active
.as_ref()
.map(slot_frame_vars)
.unwrap_or_default()
}
pub fn incoming_frame_vars(&self) -> Vec<(String, f64)> {
self.incoming
.as_ref()
.map(slot_frame_vars)
.unwrap_or_default()
}
}
fn slot_frame_vars(slot: &Slot) -> Vec<(String, f64)> {
let l = &slot.loaded;
let last = &slot.last;
let mut out: Vec<(String, f64)> = l.pf.ctx.vars().map(|(n, v)| (n.to_string(), v)).collect();
for (i, v) in last.regs.iter().enumerate() {
out.push((format!("reg{i:02}"), *v));
}
if let Some(a) = &last.audio {
for (n, v) in [
("bass", a.bass),
("mid", a.mid),
("treb", a.treb),
("vol", a.vol),
("bass_att", a.bass_att),
("mid_att", a.mid_att),
("treb_att", a.treb_att),
("vol_att", a.vol_att),
] {
out.push((format!("audio.{n}"), f64::from(v)));
}
}
out.push(("render.time".into(), f64::from(last.time)));
out.push(("render.frame".into(), f64::from(last.frame)));
for (i, h) in l.hue_offsets.iter().enumerate() {
out.push((format!("hue_offset.{i}"), f64::from(*h)));
}
let xyzw = ['x', 'y', 'z', 'w'];
let mut push_pass = |prefix: &str, p: &PassRandom| {
for (k, axis) in xyzw.iter().enumerate() {
out.push((
format!("{prefix}rand_frame.{axis}"),
f64::from(p.rand_frame[k]),
));
out.push((
format!("{prefix}rand_preset.{axis}"),
f64::from(p.rand_preset[k]),
));
}
if !prefix.is_empty() {
let ft = last.time;
let wrapped = ft - ((f64::from(ft) / 10000.0) as i32 * 10000) as f32;
out.push((format!("{prefix}time"), f64::from(wrapped)));
out.push((format!("{prefix}progress"), f64::from(last.progress)));
if let Some(rot) = &p.rot {
for (m, name) in ROT_NAMES.iter().enumerate() {
for col in 0..3 {
for row in 0..4 {
out.push((
format!("{prefix}{name}.{}", col * 4 + row),
f64::from(rot[m][col][row]),
));
}
}
}
}
}
};
if let Some(w) = &last.warp {
push_pass("warp.", w);
}
if let Some(c) = &last.comp {
push_pass("comp.", c);
}
if let Some(p) = last.comp.as_ref().or(last.warp.as_ref()) {
push_pass("", p);
}
out
}
fn prepare_slot(
slot: &mut Slot,
cfg: &EngineConfig,
clock: FrameClock,
progress: f32,
audio: &AudioFrame,
forced: Option<&ForcedFrame>,
md2: Option<&mut BlendState>,
) -> FrameInput {
let g = slot.renderer.mesh_geometry();
let fi = match md2 {
Some(m) => {
let cx = FrameCtx {
cfg,
g,
clock,
progress,
audio,
};
build_blend_frame(&mut slot.loaded, &mut slot.last, m, &cx, forced)
}
None => build_frame(
&mut slot.loaded,
cfg,
g,
clock,
progress,
audio,
forced,
&mut slot.last,
),
};
slot.last.audio = Some(audio.clone());
slot.last.time = clock.time as f32;
slot.last.frame = clock.frame;
#[cfg(feature = "diagnostics")]
if let Some(f) = forced {
if let Some(m) = &f.prev_main
&& let Err(e) = slot.renderer.inject_main(m)
{
eprintln!("lactea: teacher forcing: {e}");
}
if let Some(m) = &f.comp_main
&& let Err(e) = slot.renderer.inject_comp_source(m)
{
eprintln!("lactea: teacher forcing (comp source): {e}");
}
}
fi
}
#[derive(Clone, Copy)]
struct FrameCtx<'a> {
cfg: &'a EngineConfig,
g: &'a MeshGeometry,
clock: FrameClock,
progress: f32,
audio: &'a AudioFrame,
}
#[allow(clippy::too_many_arguments)]
fn build_frame(
l: &mut Loaded,
cfg: &EngineConfig,
g: &MeshGeometry,
clock: FrameClock,
progress: f32,
audio: &AudioFrame,
forced: Option<&ForcedFrame>,
last: &mut LastFrame,
) -> FrameInput {
let cx = FrameCtx {
cfg,
g,
clock,
progress,
audio,
};
let base = l.preset.base.clone();
let env = frame_env(&cx);
eval_per_frame(l, &cx, &env, &base, forced, last);
let anim = warp_anim(&cx, &base);
let toff = warp_mesh::texel_offset_uv(cfg.texel_offset, cfg.width, cfg.height);
let (vertex_vars, warp_uv) = eval_vertices(l, g, &anim, toff);
let mut fi = frame_head(l, &cx, &base, vertex_vars, warp_uv, anim, true, None);
push_shapes(l, &env, &mut fi.shapes);
push_waves(l, &cx, &env, &mut fi.waves);
finish_frame(l, &cx, forced, last, &mut fi);
fi
}
fn frame_env(cx: &FrameCtx) -> Env {
let (cfg, g) = (cx.cfg, cx.g);
let (w, h) = (cfg.width, cfg.height);
let (ax, ay) = (g.aspect_x(), g.aspect_y());
Env {
time: cx.clock.time as f32,
fps: cfg.fps as f32,
frame: cx.clock.frame,
progress: cx.progress,
bass: cx.audio.bass,
mid: cx.audio.mid,
treb: cx.audio.treb,
bass_att: cx.audio.bass_att,
mid_att: cx.audio.mid_att,
treb_att: cx.audio.treb_att,
mesh: cfg.mesh,
pixels: [w, h],
aspect: [ax, ay],
inv_aspect: [1.0 / ax, 1.0 / ay],
}
}
fn eval_per_frame(
l: &mut Loaded,
cx: &FrameCtx,
env: &Env,
base: &BaseValues,
forced: Option<&ForcedFrame>,
last: &mut LastFrame,
) {
l.pf.load_state(base, env);
l.pp.load_readonly(&l.pf, env);
l.pf.run();
if let Some(f) = forced {
f.apply_vars(&mut l.pf.ctx);
}
l.pp.load_q(&l.pf);
for i in 0..NUM_Q {
l.frame_q[i] = l.pf.q(i);
}
l.pf.clamp();
last.regs.clear();
last.regs.extend((0..100).map(|i| l.pf.ctx.global().reg(i)));
last.progress = cx.progress;
}
fn warp_anim(cx: &FrameCtx, base: &BaseValues) -> WarpAnim {
let time = cx.clock.time as f32;
if cx.cfg.compat == Compat::MilkDrop2 {
WarpAnim::new(time, base.warp_anim_speed, base.warp_scale)
} else {
WarpAnim::projectm(time, base.warp_anim_speed, base.warp_scale)
}
}
fn eval_vertices(
l: &mut Loaded,
g: &MeshGeometry,
anim: &WarpAnim,
toff: [f32; 2],
) -> (Vec<VertexVars>, Vec<[f32; 2]>) {
let vertex_vars: Vec<VertexVars> = if l.pp.prog.is_some() {
per_pixel_vars(l, g)
} else {
let pv = |v| l.pf.ctx.get(v);
let vars = &l.pf.v;
let vv = VertexVars {
zoom: pv(vars.zoom) as f32,
zoomexp: pv(vars.zoomexp) as f32,
rot: pv(vars.rot) as f32,
warp: pv(vars.warp) as f32,
cx: pv(vars.cx) as f32,
cy: pv(vars.cy) as f32,
dx: pv(vars.dx) as f32,
dy: pv(vars.dy) as f32,
sx: pv(vars.sx) as f32,
sy: pv(vars.sy) as f32,
};
vec![vv; g.vertex_count()]
};
let warp_uv: Vec<[f32; 2]> = vertex_vars
.iter()
.enumerate()
.map(|(n, vv)| warp_mesh::warp_vertex_uv(g, n, vv, anim, toff))
.collect();
(vertex_vars, warp_uv)
}
#[allow(clippy::too_many_arguments)]
fn frame_head(
l: &mut Loaded,
cx: &FrameCtx,
base: &BaseValues,
vertex_vars: Vec<VertexVars>,
warp_uv: Vec<[f32; 2]>,
anim: WarpAnim,
gpu: bool,
wrap_snap: Option<f32>,
) -> FrameInput {
let (cfg, audio, clock, progress) = (cx.cfg, cx.audio, cx.clock, cx.progress);
let md2 = cfg.compat == Compat::MilkDrop2;
let time = clock.time as f32;
let mut fi = FrameInput {
time: f64::from(time),
fps: cfg.fps as f32,
frame: clock.frame,
progress,
bass: audio.bass,
mid: audio.mid,
treb: audio.treb,
bass_att: audio.bass_att,
mid_att: audio.mid_att,
treb_att: audio.treb_att,
imm_abs: [audio.bass, audio.mid, audio.treb],
..Default::default()
};
let pf = &l.pf;
for i in 0..NUM_Q {
fi.q[i] = pf.q(i) as f32;
}
fi.warp_uv = Some(warp_uv);
if gpu {
fi.warp_gpu = Some(WarpGpu {
vars: vertex_vars,
anim,
});
}
let vars = &pf.v;
let f32v = |v| pf.ctx.get(v) as f32;
fi.post.decay = f32v(vars.decay);
fi.post.wrap = match wrap_snap {
Some(snap) => pf.ctx.get(vars.wrap) > f64::from(snap),
None => pf.ctx.get(vars.wrap) > 0.0001,
};
fi.post.fshader = base.shader;
if md2 {
fi.post.gamma = f32v(vars.gamma);
fi.post.echo_zoom = f32v(vars.echo_zoom);
fi.post.echo_alpha = f32v(vars.echo_alpha);
fi.post.echo_orient = pf.ctx.get(vars.echo_orient) as i32;
fi.post.brighten = pf.ctx.get(vars.brighten) != 0.0;
fi.post.darken = pf.ctx.get(vars.darken) != 0.0;
fi.post.solarize = pf.ctx.get(vars.solarize) != 0.0;
fi.post.invert = pf.ctx.get(vars.invert) != 0.0;
} else {
fi.post.gamma = base.gamma_adj;
fi.post.echo_zoom = base.video_echo_zoom;
fi.post.echo_alpha = base.video_echo_alpha;
fi.post.echo_orient = base.video_echo_orientation;
fi.post.brighten = base.brighten;
fi.post.darken = base.darken;
fi.post.solarize = base.solarize;
fi.post.invert = base.invert;
}
fi.blur.min = [
f32v(vars.blur1_min),
f32v(vars.blur2_min),
f32v(vars.blur3_min),
];
fi.blur.max = [
f32v(vars.blur1_max),
f32v(vars.blur2_max),
f32v(vars.blur3_max),
];
fi.blur.edge_darken = f32v(vars.blur1_edge_darken);
fi.borders.ob_size = f32v(vars.ob_size);
fi.borders.ob_rgba = [
f32v(vars.ob_r),
f32v(vars.ob_g),
f32v(vars.ob_b),
f32v(vars.ob_a),
];
fi.borders.ib_size = f32v(vars.ib_size);
fi.borders.ib_rgba = [
f32v(vars.ib_r),
f32v(vars.ib_g),
f32v(vars.ib_b),
f32v(vars.ib_a),
];
fi.mv.x = f32v(vars.mv_x);
fi.mv.y = f32v(vars.mv_y);
fi.mv.dx = f32v(vars.mv_dx);
fi.mv.dy = f32v(vars.mv_dy);
fi.mv.l = f32v(vars.mv_l);
fi.mv.rgba = [
f32v(vars.mv_r),
f32v(vars.mv_g),
f32v(vars.mv_b),
f32v(vars.mv_a),
];
fi.darken_center = pf.ctx.get(vars.darken_center) > 0.0;
let mw = &mut fi.main_wave;
let (mode_mod, thick, dots, additive) = if md2 {
(
18,
pf.ctx.get(vars.wave_thick) != 0.0,
pf.ctx.get(vars.wave_usedots) != 0.0,
pf.ctx.get(vars.wave_additive) != 0.0,
)
} else {
(16, base.wave_thick, base.wave_dots, base.additive_waves)
};
let mode_src = if md2 {
pf.ctx.get(vars.wave_mode) as i32
} else {
base.wave_mode
};
mw.mode = mode_src.rem_euclid(mode_mod);
mw.rgba = [
f32v(vars.wave_r),
f32v(vars.wave_g),
f32v(vars.wave_b),
f32v(vars.wave_a),
];
mw.x = f32v(vars.wave_x);
mw.y = f32v(vars.wave_y);
mw.mystery = f32v(vars.wave_mystery);
mw.usedots = dots;
mw.thick = thick;
mw.additive = additive;
mw.brighten = pf.ctx.get(vars.wave_brighten) > 0.0;
mw.scale = base.wave_scale;
mw.smoothing = base.wave_smoothing;
mw.mod_alpha_by_volume = base.mod_wave_alpha_by_volume;
mw.mod_alpha_start = base.mod_wave_alpha_start;
mw.mod_alpha_end = base.mod_wave_alpha_end;
mw.wave = audio.wave;
mw.spectrum = audio.spectrum;
fi
}
fn push_shapes(l: &mut Loaded, env: &Env, out: &mut Vec<ShapeInstance>) {
let frame_q = l.frame_q;
for s in l.shapes.iter_mut() {
if !s.enabled() {
continue;
}
let n = s.cfg.instances.clamp(0, 1024);
for inst in 0..n {
s.load_state(&frame_q, env, inst);
if let Some(p) = &s.prog {
s.ctx.run(p);
}
let v = &s.v;
let c = |x| s.ctx.get(x) as f32;
out.push(ShapeInstance {
sides: s.ctx.get(v.sides) as i32,
x: s.ctx.get(v.x),
y: s.ctx.get(v.y),
rad: c(v.rad),
ang: c(v.ang),
rgba: [c(v.r), c(v.g), c(v.b), c(v.a)],
rgba2: [c(v.r2), c(v.g2), c(v.b2), c(v.a2)],
border_rgba: [c(v.border_r), c(v.border_g), c(v.border_b), c(v.border_a)],
additive: s.ctx.get(v.additive) as i32 != 0,
thick: s.cfg.thick_outline != 0,
textured: s.ctx.get(v.textured) as i32 != 0,
tex_zoom: c(v.tex_zoom),
tex_ang: c(v.tex_ang),
image: crate::textures::shape_image(&s.cfg.image),
});
}
}
}
fn push_waves(l: &mut Loaded, cx: &FrameCtx, env: &Env, out: &mut Vec<RenderWave>) {
let audio = cx.audio;
let wave_scale = l.preset.base.wave_scale;
for wv in l.waves.iter_mut() {
if !wv.enabled() {
continue;
}
wv.load_state(&l.pf, env);
wv.load_point_readonly(&l.pf);
if let Some(p) = &wv.pf_prog {
wv.pf.run(p);
}
wv.init_point_qt();
let samples = (wv.pf.get(wv.pfv.samples) as i32).clamp(0, 512);
let use_dots = wv.cfg.b_use_dots != 0;
if (use_dots && samples < 1) || samples < 2 {
continue;
}
let spectrum = wv.cfg.b_spectrum != 0;
let sampling = CustomWaveSampling {
spectrum,
samples,
sep: wv.cfg.sep,
scaling: wv.cfg.scaling,
smoothing: wv.cfg.smoothing,
wave_scale,
};
let (d1, d2): (&[f32], &[f32]) = if spectrum {
(&audio.spectrum[0], &audio.spectrum[1])
} else {
(
&audio.wave[0][..NUM_WAVEFORM_SAMPLES],
&audio.wave[1][..NUM_WAVEFORM_SAMPLES],
)
};
let values = lactea_render::wavegen::custom_wave_values(&sampling, d1, d2);
let n = values.len();
let mult = if n > 1 { 1.0 / (n - 1) as f32 } else { 0.0 };
let (r, g, b, a) = (
wv.pf.get(wv.pfv.r),
wv.pf.get(wv.pfv.g),
wv.pf.get(wv.pfv.b),
wv.pf.get(wv.pfv.a),
);
let mut points = Vec::with_capacity(n);
for (i, [v1, v2]) in values.iter().enumerate() {
let pv = &wv.ppv;
wv.pp.set(pv.sample, f64::from(i as f32 * mult));
wv.pp.set(pv.value1, f64::from(*v1));
wv.pp.set(pv.value2, f64::from(*v2));
wv.pp.set(pv.x, f64::from(0.5 + *v1));
wv.pp.set(pv.y, f64::from(0.5 + *v2));
wv.pp.set(pv.r, r);
wv.pp.set(pv.g, g);
wv.pp.set(pv.b, b);
wv.pp.set(pv.a, a);
if let Some(p) = &wv.pp_prog {
wv.pp.run(p);
}
let c = |x| wv.pp.get(x) as f32;
points.push(WavePoint {
x: wv.pp.get(pv.x),
y: wv.pp.get(pv.y),
rgba: [c(pv.r), c(pv.g), c(pv.b), c(pv.a)],
});
}
out.push(RenderWave {
points,
use_dots,
thick: wv.cfg.b_draw_thick != 0,
additive: wv.cfg.b_additive != 0,
});
}
}
fn finish_frame(
l: &mut Loaded,
cx: &FrameCtx,
forced: Option<&ForcedFrame>,
last: &mut LastFrame,
fi: &mut FrameInput,
) {
let md2 = cx.cfg.compat == Compat::MilkDrop2;
let time = cx.clock.time as f32;
let drop = !md2;
let (mut warp_r, mut comp_r) = (None, None);
if md2 {
if let Some(s) = l.warp_stream.as_mut() {
let p = s.frame(time, drop);
if l.has_warp {
warp_r = Some(p.clone());
}
if l.has_comp {
comp_r = Some(p);
}
}
} else {
warp_r = l.warp_stream.as_mut().map(|s| s.frame(time, drop));
comp_r = l.comp_stream.as_mut().map(|s| s.frame(time, drop));
}
let mut hue = l.hue_offsets;
if let Some(f) = forced {
if let Some(p) = f.pass_random("warp.")
&& warp_r.is_some()
{
warp_r = Some(p);
}
if let Some(p) = f.pass_random("comp.")
&& comp_r.is_some()
{
comp_r = Some(p);
}
if let Some(h) = f.hue_offsets() {
hue = h;
}
}
if let Some(p) = comp_r.as_ref().or(warp_r.as_ref()) {
fi.rand_frame = p.rand_frame;
fi.rand_preset = p.rand_preset;
}
l.frames += 1;
fi.warp_random = warp_r.clone();
fi.comp_random = comp_r.clone();
fi.hue_offsets = Some(hue);
last.warp = warp_r;
last.comp = comp_r;
l.hue_offsets = hue;
}
fn per_pixel_inputs(g: &MeshGeometry, x: u32, row_pm: u32) -> [f32; 4] {
let vx = x as f32 / g.gx() as f32 * 2.0 - 1.0;
let vy = row_pm as f32 / g.gy() as f32 * 2.0 - 1.0;
let (ax, ay) = (g.aspect_x(), g.aspect_y());
let rad = (vx * ax).hypot(vy * ay);
let ang = if row_pm == g.gy() / 2 && x == g.gx() / 2 {
0.0
} else {
(vy * ay).atan2(vx * ax)
};
[vx * 0.5 * ax + 0.5, vy * 0.5 * ay + 0.5, rad, -ang]
}
fn per_pixel_vars(l: &mut Loaded, g: &MeshGeometry) -> Vec<VertexVars> {
let mut out = vec![VertexVars::default(); g.vertex_count()];
let pfv = &l.pf.v;
let get = |v| l.pf.ctx.get(v);
let frame_vals = [
get(pfv.zoom),
get(pfv.zoomexp),
get(pfv.rot),
get(pfv.warp),
get(pfv.cx),
get(pfv.cy),
get(pfv.dx),
get(pfv.dy),
get(pfv.sx),
get(pfv.sy),
];
let pp = &mut l.pp;
let prog = pp.prog.take();
let v = &pp.v;
for row_pm in 0..=g.gy() {
let row = g.gy() - row_pm;
for x in 0..=g.gx() {
let n = g.index(x, row);
let c = &mut pp.ctx;
let [px, py, prad, pang] = per_pixel_inputs(g, x, row_pm);
c.set(v.x, f64::from(px));
c.set(v.y, f64::from(py));
c.set(v.rad, f64::from(prad));
c.set(v.ang, f64::from(pang));
c.set(v.zoom, frame_vals[0]);
c.set(v.zoomexp, frame_vals[1]);
c.set(v.rot, frame_vals[2]);
c.set(v.warp, frame_vals[3]);
c.set(v.cx, frame_vals[4]);
c.set(v.cy, frame_vals[5]);
c.set(v.dx, frame_vals[6]);
c.set(v.dy, frame_vals[7]);
c.set(v.sx, frame_vals[8]);
c.set(v.sy, frame_vals[9]);
if let Some(p) = &prog {
c.run(p);
}
let vv = VertexVars {
zoom: c.get(v.zoom) as f32,
zoomexp: c.get(v.zoomexp) as f32,
rot: c.get(v.rot) as f32,
warp: c.get(v.warp) as f32,
cx: c.get(v.cx) as f32,
cy: c.get(v.cy) as f32,
dx: c.get(v.dx) as f32,
dy: c.get(v.dy) as f32,
sx: c.get(v.sx) as f32,
sy: c.get(v.sy) as f32,
};
out[n] = vv;
}
}
pp.prog = prog;
out
}
#[cfg(test)]
mod per_pixel_input_tests {
use super::*;
#[test]
fn per_pixel_inputs_follow_projectm_float_rounding() {
let g = MeshGeometry::new(48, 36, 640, 360).unwrap();
let top = per_pixel_inputs(&g, 24, 2);
let bottom = per_pixel_inputs(&g, 24, 34);
assert_eq!(top[2], 0.5);
assert!(bottom[2] < 0.5);
assert!(top[1] < 0.5 && bottom[1] > 0.5);
assert!(top[3] > 0.0 && bottom[3] < 0.0);
}
#[test]
fn per_pixel_ang_centre_row_is_minus_pi() {
let g = MeshGeometry::new(48, 36, 640, 360).unwrap();
let left = per_pixel_inputs(&g, 3, 18);
assert_eq!(left[3], -std::f32::consts::PI);
let right = per_pixel_inputs(&g, 40, 18);
assert_eq!(right[3].abs(), 0.0);
assert_eq!(per_pixel_inputs(&g, 24, 18)[3], 0.0);
}
}
#[path = "extras.rs"]
mod extras;
#[path = "params.rs"]
mod params;
#[path = "shape_images.rs"]
mod shape_images;
pub use extras::{DebugImage, TouchType};