use std::sync::{mpsc, Arc, Mutex}; use std::time::Duration; use std::{thread, time::Instant}; use anyhow::{anyhow, bail, Result}; use cpal::traits::*; use rubato::Resampler; use uw8_window::{Window, WindowConfig}; use wasmtime::{ Engine, Func, GlobalType, Memory, MemoryType, Module, Mutability, Store, TypedFunc, ValType, }; pub struct MicroW8 { window: Window, stream: Option, engine: Engine, loader_module: Module, disable_audio: bool, module_data: Option>, timeout: u32, instance: Option, } struct UW8Instance { store: Store<()>, memory: Memory, end_frame: TypedFunc<(), ()>, update: Option>, start_time: Instant, watchdog: Arc>, sound_tx: Option>, } impl Drop for UW8Instance { fn drop(&mut self) { if let Ok(mut watchdog) = self.watchdog.lock() { watchdog.stop = true; } } } struct UW8WatchDog { engine: Engine, stop: bool, } impl MicroW8 { pub fn new(timeout: Option, window_config: WindowConfig) -> Result { let mut config = wasmtime::Config::new(); config.cranelift_opt_level(wasmtime::OptLevel::Speed); if timeout.is_some() { config.epoch_interruption(true); } let engine = wasmtime::Engine::new(&config)?; let loader_module = wasmtime::Module::new(&engine, include_bytes!("../platform/bin/loader.wasm"))?; let window = Window::new(window_config)?; Ok(MicroW8 { window, stream: None, engine, loader_module, disable_audio: false, module_data: None, timeout: timeout.unwrap_or(0), instance: None, }) } pub fn disable_audio(&mut self) { self.disable_audio = true; } } impl super::Runtime for MicroW8 { fn is_open(&self) -> bool { self.window.is_open() } fn load(&mut self, module_data: &[u8]) -> Result<()> { self.stream = None; self.instance = None; let mut store = wasmtime::Store::new(&self.engine, ()); store.set_epoch_deadline(60); let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?; let mut linker = wasmtime::Linker::new(&self.engine); linker.define(&store, "env", "memory", memory)?; let loader_instance = linker.instantiate(&mut store, &self.loader_module)?; let load_uw8 = loader_instance.get_typed_func::(&mut store, "load_uw8")?; let platform_data = include_bytes!("../platform/bin/platform.uw8"); memory.data_mut(&mut store)[..platform_data.len()].copy_from_slice(platform_data); let platform_length = load_uw8.call(&mut store, platform_data.len() as i32)? as u32 as usize; let platform_module = wasmtime::Module::new(&self.engine, &memory.data(&store)[..platform_length])?; memory.data_mut(&mut store)[..module_data.len()].copy_from_slice(module_data); let module_length = load_uw8.call(&mut store, module_data.len() as i32)? as u32 as usize; let module = wasmtime::Module::new(&self.engine, &memory.data(&store)[..module_length])?; add_native_functions(&mut linker, &mut store)?; let platform_instance = instantiate_platform(&mut linker, &mut store, &platform_module)?; let watchdog = Arc::new(Mutex::new(UW8WatchDog { engine: self.engine.clone(), stop: false, })); { let watchdog = watchdog.clone(); thread::spawn(move || loop { thread::sleep(Duration::from_millis(17)); if let Ok(watchdog) = watchdog.lock() { if watchdog.stop { break; } watchdog.engine.increment_epoch(); } else { break; } }); } let instance = linker.instantiate(&mut store, &module)?; let end_frame = platform_instance.get_typed_func::<(), ()>(&mut store, "endFrame")?; let update = instance.get_typed_func::<(), ()>(&mut store, "upd").ok(); if let Some(start) = instance.get_typed_func::<(), ()>(&mut store, "start").ok() { start.call(&mut store, ())?; } let (sound_tx, stream) = if self.disable_audio { (None, None) } else { match init_sound(&self.engine, &platform_module, &module) { Ok(sound) => { sound.stream.play()?; (Some(sound.tx), Some(sound.stream)) } Err(err) => { eprintln!("Failed to init sound: {}", err); (None, None) } } }; self.instance = Some(UW8Instance { store, memory, end_frame, update, start_time: Instant::now(), watchdog, sound_tx, }); self.stream = stream; self.module_data = Some(module_data.into()); Ok(()) } fn run_frame(&mut self) -> Result<()> { let input = self.window.begin_frame(); if input.reset { if let Some(module_data) = self.module_data.take() { self.load(&module_data)?; } } let now = Instant::now(); let mut result = Ok(()); if let Some(mut instance) = self.instance.take() { let time = (now - instance.start_time).as_millis() as i32; let next_frame = { let offset = ((time as u32 as i64 * 6) % 100 - 50) / 6; let max = now + Duration::from_millis(17); let next_center = now + Duration::from_millis((16 - offset) as u64); next_center.min(max) }; { let mem = instance.memory.data_mut(&mut instance.store); mem[64..68].copy_from_slice(&time.to_le_bytes()); mem[68..72].copy_from_slice(&input.gamepads); } instance.store.set_epoch_deadline(self.timeout as u64); if let Some(ref update) = instance.update { if let Err(err) = update.call(&mut instance.store, ()) { result = Err(err); } } instance.end_frame.call(&mut instance.store, ())?; let memory = instance.memory.data(&instance.store); let mut sound_regs = [0u8; 32]; sound_regs.copy_from_slice(&memory[80..112]); if let Some(ref sound_tx) = instance.sound_tx { let _ = sound_tx.send(RegisterUpdate { time, data: sound_regs, }); } let framebuffer_mem = &memory[120..(120 + 320 * 240)]; let palette_mem = &memory[0x13000..]; self.window .end_frame(framebuffer_mem, palette_mem, next_frame); if result.is_ok() { self.instance = Some(instance); } } result?; Ok(()) } } fn add_native_functions( linker: &mut wasmtime::Linker<()>, store: &mut wasmtime::Store<()>, ) -> Result<()> { linker.func_wrap("env", "acos", |v: f32| v.acos())?; linker.func_wrap("env", "asin", |v: f32| v.asin())?; linker.func_wrap("env", "atan", |v: f32| v.atan())?; linker.func_wrap("env", "atan2", |x: f32, y: f32| x.atan2(y))?; linker.func_wrap("env", "cos", |v: f32| v.cos())?; linker.func_wrap("env", "exp", |v: f32| v.exp())?; linker.func_wrap("env", "log", |v: f32| v.ln())?; linker.func_wrap("env", "sin", |v: f32| v.sin())?; linker.func_wrap("env", "tan", |v: f32| v.tan())?; linker.func_wrap("env", "pow", |a: f32, b: f32| a.powf(b))?; for i in 10..64 { linker.func_wrap("env", &format!("reserved{}", i), || {})?; } let log_line = std::sync::Mutex::new(String::new()); linker.func_wrap("env", "logChar", move |c: i32| { let mut log_line = log_line.lock().unwrap(); if c == 10 { println!("{}", log_line); log_line.clear(); } else { log_line.push(c as u8 as char); } })?; for i in 0..16 { let global = wasmtime::Global::new( &mut *store, GlobalType::new(ValType::I32, Mutability::Const), 0.into(), )?; linker.define(&store, "env", &format!("g_reserved{}", i), global)?; } Ok(()) } fn instantiate_platform( linker: &mut wasmtime::Linker<()>, store: &mut wasmtime::Store<()>, platform_module: &wasmtime::Module, ) -> Result { let platform_instance = linker.instantiate(&mut *store, &platform_module)?; let exports: Vec<(String, Func)> = platform_instance .exports(&mut *store) .map(|e| { ( e.name().to_owned(), e.into_func() .expect("platform surely only exports functions"), ) }) .collect(); for (name, func) in exports { linker.define(&store, "env", &name, func)?; } Ok(platform_instance) } struct RegisterUpdate { time: i32, data: [u8; 32], } struct Uw8Sound { stream: cpal::Stream, tx: mpsc::SyncSender, } fn init_sound( engine: &wasmtime::Engine, platform_module: &wasmtime::Module, module: &wasmtime::Module, ) -> Result { let mut store = wasmtime::Store::new(engine, ()); store.set_epoch_deadline(60); let memory = wasmtime::Memory::new(&mut store, MemoryType::new(4, Some(4)))?; let mut linker = wasmtime::Linker::new(engine); linker.define(&store, "env", "memory", memory)?; add_native_functions(&mut linker, &mut store)?; let platform_instance = instantiate_platform(&mut linker, &mut store, platform_module)?; let instance = linker.instantiate(&mut store, module)?; let snd = instance .get_typed_func::<(i32,), f32>(&mut store, "snd") .or_else(|_| platform_instance.get_typed_func::<(i32,), f32>(&mut store, "sndGes"))?; let host = cpal::default_host(); let device = host .default_output_device() .ok_or_else(|| anyhow!("No audio output device available"))?; let mut configs: Vec<_> = device .supported_output_configs()? .filter(|config| { config.sample_format() == cpal::SampleFormat::F32 || config.sample_format() == cpal::SampleFormat::I16 }) .collect(); if configs.is_empty() { eprintln!( "No suitable audio output config found on device \"{}\", available configs:", device.name()? ); for config in device.supported_output_configs()? { eprintln!(" {}ch {}", config.channels(), config.sample_format()); } bail!("Failed to configure audio out"); } configs.sort_by_key(|config| { let rate = 44100 .max(config.min_sample_rate().0) .min(config.max_sample_rate().0); let rate_prio = if rate >= 44100 { rate - 44100 } else { (44100 - rate) * 1000 }; let format_prio = (config.sample_format() == cpal::SampleFormat::I16) as u32; let channels_prio = (config.channels() != 2) as u32 * 16777216; rate_prio + format_prio + channels_prio }); let config = configs.into_iter().next().unwrap(); let sample_rate = cpal::SampleRate(44100) .max(config.min_sample_rate()) .min(config.max_sample_rate()); let config = config.with_sample_rate(sample_rate); let buffer_size = match *config.buffer_size() { cpal::SupportedBufferSize::Unknown => cpal::BufferSize::Default, cpal::SupportedBufferSize::Range { min, max } => { cpal::BufferSize::Fixed(256.max(min).min(max)) } }; let sample_format = config.sample_format(); let num_channels = config.channels(); let config = cpal::StreamConfig { buffer_size, ..config.config() }; let sample_rate = config.sample_rate.0 as usize; let (tx, rx) = mpsc::sync_channel::(30); struct Resampler { resampler: rubato::FftFixedIn, input_buffers: Vec>, output_buffers: Vec>, output_index: usize, } let mut resampler: Option = if sample_rate == 44100 { None } else { let rs = rubato::FftFixedIn::new(44100, sample_rate, 128, 1, 2)?; let input_buffers = rs.input_buffer_allocate(true); let output_buffers = rs.output_buffer_allocate(true); Some(Resampler { resampler: rs, input_buffers, output_buffers, output_index: usize::MAX, }) }; let mut sample_index = 0; let mut pending_updates: Vec = Vec::with_capacity(30); let mut current_time = 0; let mut callback = move |mut outer_buffer: &mut [f32]| { let mut first_update = true; while let Ok(update) = rx.try_recv() { if first_update { current_time += update.time.wrapping_sub(current_time) / 8; first_update = false; } pending_updates.push(update); } while !outer_buffer.is_empty() { store.set_epoch_deadline(30); while pending_updates .first() .into_iter() .any(|u| u.time.wrapping_sub(current_time) <= 0) { let update = pending_updates.remove(0); memory.write(&mut store, 80, &update.data).unwrap(); } let duration = if let Some(update) = pending_updates.first() { ((update.time.wrapping_sub(current_time) as usize) * sample_rate + 999) / 1000 } else { outer_buffer.len() }; let step_size = (duration.max(64) * 2).min(outer_buffer.len()); let mut buffer = &mut outer_buffer[..step_size]; { let mem = memory.data_mut(&mut store); mem[64..68].copy_from_slice(¤t_time.to_le_bytes()); } fn clamp_sample(s: f32) -> f32 { if s.is_nan() { 0.0 } else { s.max(-1.0).min(1.0) } } if let Some(ref mut resampler) = resampler { while !buffer.is_empty() { let copy_size = resampler.output_buffers[0] .len() .saturating_sub(resampler.output_index) .min(buffer.len() / 2); if copy_size == 0 { resampler.input_buffers[0].clear(); resampler.input_buffers[1].clear(); for _ in 0..resampler.resampler.input_frames_next() { resampler.input_buffers[0].push(clamp_sample( snd.call(&mut store, (sample_index,)).unwrap_or(0.0), )); resampler.input_buffers[1].push(clamp_sample( snd.call(&mut store, (sample_index + 1,)).unwrap_or(0.0), )); sample_index = sample_index.wrapping_add(2); } resampler .resampler .process_into_buffer( &resampler.input_buffers, &mut resampler.output_buffers, None, ) .unwrap(); resampler.output_index = 0; } else { for i in 0..copy_size { buffer[i * 2] = resampler.output_buffers[0][resampler.output_index + i]; buffer[i * 2 + 1] = resampler.output_buffers[1][resampler.output_index + i]; } resampler.output_index += copy_size; buffer = &mut buffer[copy_size * 2..]; } } } else { for v in buffer { *v = clamp_sample(snd.call(&mut store, (sample_index,)).unwrap_or(0.0)); sample_index = sample_index.wrapping_add(1); } } outer_buffer = &mut outer_buffer[step_size..]; current_time = current_time.wrapping_add((step_size * 500 / sample_rate).max(1) as i32); } }; fn f32_to_i16(mut buffer: &mut [i16], callback: &mut F) where F: FnMut(&mut [f32]), { let mut float_buffer = [0f32; 256]; while !buffer.is_empty() { let step_size = buffer.len().min(float_buffer.len()); let step_buffer = &mut float_buffer[..step_size]; callback(step_buffer); for (dest, src) in buffer.iter_mut().take(step_size).zip(step_buffer.iter()) { *dest = (src.max(-1.0).min(1.0) * 32767.0) as i16; } buffer = &mut buffer[step_size..]; } } fn stereo_to_mono(mut buffer: &mut [f32], callback: &mut F) where F: FnMut(&mut [f32]), { let mut in_buffer = [0f32; 256]; while !buffer.is_empty() { let step_size = buffer.len().min(in_buffer.len() / 2); let step_buffer = &mut in_buffer[..step_size * 2]; callback(step_buffer); for (index, dest) in buffer.iter_mut().take(step_size).enumerate() { *dest = (step_buffer[index * 2] + step_buffer[index * 2 + 1]) * 0.5; } buffer = &mut buffer[step_size..]; } } fn stereo_to_surround(mut buffer: &mut [f32], num_channels: usize, callback: &mut F) where F: FnMut(&mut [f32]), { let mut in_buffer = [0f32; 256]; buffer.fill(0.); while !buffer.is_empty() { let step_size = (buffer.len() / num_channels).min(in_buffer.len() / 2); let step_buffer = &mut in_buffer[..step_size * 2]; callback(step_buffer); for index in 0..step_size { buffer[index * num_channels + 0] = step_buffer[index * 2 + 0]; buffer[index * num_channels + 1] = step_buffer[index * 2 + 1]; } buffer = &mut buffer[step_size * num_channels..]; } } let stream = if sample_format == cpal::SampleFormat::F32 { if num_channels == 2 { device.build_output_stream( &config, move |buffer: &mut [f32], _| callback(buffer), move |err| { dbg!(err); }, None, )? } else if num_channels == 1 { device.build_output_stream( &config, move |buffer: &mut [f32], _| stereo_to_mono(buffer, &mut callback), move |err| { dbg!(err); }, None, )? } else { device.build_output_stream( &config, move |buffer: &mut [f32], _| { stereo_to_surround(buffer, num_channels as usize, &mut callback) }, move |err| { dbg!(err); }, None, )? } } else { if num_channels == 2 { device.build_output_stream( &config, move |buffer: &mut [i16], _| f32_to_i16(buffer, &mut callback), move |err| { dbg!(err); }, None, )? } else if num_channels == 1 { device.build_output_stream( &config, move |buffer: &mut [i16], _| { f32_to_i16(buffer, &mut |b| stereo_to_mono(b, &mut callback)) }, move |err| { dbg!(err); }, None, )? } else { device.build_output_stream( &config, move |buffer: &mut [i16], _| { f32_to_i16(buffer, &mut |b| { stereo_to_surround(b, num_channels as usize, &mut callback) }) }, move |err| { dbg!(err); }, None, )? } }; Ok(Uw8Sound { stream, tx }) }