use super::Engine;
use lactea_audio::MAX_SAMPLES;
pub fn clamp_mesh_size(width: u32, height: u32) -> [u32; 2] {
let f = |v: u32| v.saturating_add(v & 1).clamp(8, 300);
[f(width), f(height)]
}
impl Engine {
pub const fn pcm_max_samples() -> usize {
MAX_SAMPLES
}
pub fn push_pcm_i16(&mut self, interleaved: &[i16], channels: usize) {
self.analyzer.push_pcm_i16(interleaved, channels);
}
pub fn push_pcm_u8(&mut self, interleaved: &[u8], channels: usize) {
self.analyzer.push_pcm_u8(interleaved, channels);
}
pub fn last_frame_time(&self) -> f64 {
self.last_time
}
pub fn window_size(&self) -> (u32, u32) {
(self.cfg.width, self.cfg.height)
}
pub fn fps(&self) -> u32 {
self.cfg.fps
}
pub fn set_fps(&mut self, fps: u32) {
self.cfg.fps = fps;
}
pub fn preset_duration(&self) -> f64 {
self.cfg.preset_duration
}
pub fn set_preset_duration(&mut self, seconds: f64) {
self.cfg.preset_duration = seconds;
self.clock.preset_duration = seconds;
}
pub fn mesh_size(&self) -> [u32; 2] {
self.cfg.mesh
}
pub fn set_mesh_size(&mut self, width: u32, height: u32) -> [u32; 2] {
let mesh = clamp_mesh_size(width, height);
self.cfg.mesh = mesh;
for r in self.all_renderers_mut() {
let _ = r.set_mesh_size(mesh);
}
mesh
}
pub fn texel_offset(&self) -> [f32; 2] {
self.cfg.texel_offset
}
pub fn set_texel_offset(&mut self, offset: [f32; 2]) {
self.cfg.texel_offset = offset;
for r in self.all_renderers_mut() {
r.set_texel_offset(offset);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{EngineConfig, FrameClock};
fn gpu() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::default();
let adapter =
pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
}
#[test]
fn mesh_clamp_follows_projectm() {
assert_eq!(clamp_mesh_size(32, 24), [32, 24]);
assert_eq!(clamp_mesh_size(33, 25), [34, 26]); assert_eq!(clamp_mesh_size(0, 1), [8, 8]); assert_eq!(clamp_mesh_size(7, 9), [8, 10]);
assert_eq!(clamp_mesh_size(299, 301), [300, 300]); assert_eq!(clamp_mesh_size(10_000, u32::MAX), [300, 300]); assert_eq!(Engine::pcm_max_samples(), 576);
}
#[test]
fn runtime_parameters_round_trip_and_take_effect() {
let Some((device, queue)) = gpu() else {
eprintln!("skipping: no wgpu adapter");
return;
};
let cfg = EngineConfig {
width: 64,
height: 36,
mesh: [16, 12],
..Default::default()
};
let mut e = Engine::new(&device, &queue, cfg);
assert_eq!(
(
e.fps(),
e.preset_duration(),
e.mesh_size(),
e.texel_offset()
),
(60, 1e9, [16, 12], [0.0, 0.0])
);
assert_eq!(e.window_size(), (64, 36));
e.set_fps(35);
e.set_preset_duration(30.0);
e.set_texel_offset([0.5, 0.25]);
assert_eq!(e.set_mesh_size(33, 5), [34, 8]);
assert_eq!(
(
e.fps(),
e.preset_duration(),
e.texel_offset(),
e.mesh_size()
),
(35, 30.0, [0.5, 0.25], [34, 8])
);
assert_eq!(
(
e.renderer().mesh_geometry().gx(),
e.renderer().mesh_geometry().gy()
),
(34, 8)
);
assert_eq!(e.renderer().config().texel_offset, [0.5, 0.25]);
let milk = "[preset00]\nper_frame_1=q1 = fps; q2 = meshx; q3 = meshy; q4 = progress;\n";
e.load_preset(milk.as_bytes(), "params", crate::Transition::Cut)
.unwrap();
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 64,
height: 36,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = tex.create_view(&Default::default());
for (frame, time) in [(0u32, 10.0f64), (1, 25.0)] {
let mut enc = device.create_command_encoder(&Default::default());
e.render(&mut enc, &view, FrameClock { time, frame });
queue.submit(Some(enc.finish()));
}
assert_eq!(e.last_frame_time(), 25.0);
let vars: std::collections::HashMap<String, f64> = e.frame_vars().into_iter().collect();
assert_eq!(vars["q1"], 35.0);
assert_eq!((vars["q2"], vars["q3"]), (34.0, 8.0));
assert!(
(vars["q4"] - 25.0 / 30.0).abs() < 1e-6,
"progress {}",
vars["q4"]
);
e.push_pcm_i16(&[i16::MAX; 64], 2);
e.push_pcm_u8(&[255; 64], 1);
}
#[test]
fn idle_preset_loads_and_renders_something() {
let Some((device, queue)) = gpu() else {
eprintln!("skipping: no wgpu adapter");
return;
};
let cfg = EngineConfig {
width: 96,
height: 54,
..Default::default()
};
let mut e = Engine::new(&device, &queue, cfg);
assert_eq!(e.preset_name(), None);
let report = e.load_idle_preset().expect("idle preset loads");
assert!(
report.warnings.is_empty() && report.unresolved_textures.is_empty(),
"{report:?}"
);
assert_eq!(e.preset_name(), Some(crate::idle::IDLE_PRESET_NAME));
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: None,
size: wgpu::Extent3d {
width: 96,
height: 54,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = tex.create_view(&Default::default());
let pcm: Vec<f32> = (0..1470).map(|i| (i as f32 * 0.05).sin() * 0.5).collect();
for frame in 0..30u32 {
e.push_pcm_f32(&pcm, 2);
let mut enc = device.create_command_encoder(&Default::default());
e.render(
&mut enc,
&view,
FrameClock {
time: f64::from(frame) / 60.0,
frame,
},
);
queue.submit(Some(enc.finish()));
}
let (_, _, rgba) = e
.renderer()
.read_stage(lactea_abi::dump::Stage::Final)
.expect("read back");
let lit = rgba
.chunks(4)
.filter(|p| p[0] > 8 || p[1] > 8 || p[2] > 8)
.count();
assert!(lit > 50, "idle preset drew only {lit} lit pixels");
}
}