diff --git a/src/app/game.rs b/src/app/game.rs index ad3f6af..9dbaa47 100644 --- a/src/app/game.rs +++ b/src/app/game.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Instant}; +use std::time::Instant; use wgpu::util::DeviceExt as _; use winit::{ dpi::LogicalSize, @@ -9,7 +9,7 @@ use winit::{ use crate::{ emulator::{EmulatorClient, EmulatorCommand, SimId}, - renderer::GameRenderer, + graphics::TextureSink, }; use super::{ @@ -46,15 +46,8 @@ impl GameWindow { .build(); let device = &window.device; - let eyes = Arc::new(GameRenderer::create_texture(device, "eye")); - client.send_command(EmulatorCommand::SetRenderer( - sim_id, - GameRenderer { - queue: window.queue.clone(), - eyes: eyes.clone(), - }, - )); - let eyes = eyes.create_view(&wgpu::TextureViewDescriptor::default()); + let (sink, texture_view) = TextureSink::new(device, window.queue.clone()); + client.send_command(EmulatorCommand::SetRenderer(sim_id, sink)); let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); let colors = Colors { left: [1.0, 0.0, 0.0, 1.0], @@ -103,7 +96,7 @@ impl GameWindow { entries: &[ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::TextureView(&eyes), + resource: wgpu::BindingResource::TextureView(&texture_view), }, wgpu::BindGroupEntry { binding: 1, diff --git a/src/emulator.rs b/src/emulator.rs index 4bb8386..7339c45 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -11,7 +11,7 @@ use std::{ use anyhow::Result; -use crate::{audio::Audio, renderer::GameRenderer}; +use crate::{audio::Audio, graphics::TextureSink}; use shrooms_vb_core::Sim; pub use shrooms_vb_core::VBKey; @@ -30,7 +30,10 @@ pub enum SimId { Player2, } impl SimId { - pub fn to_index(self) -> usize { + pub const fn values() -> [Self; 2] { + [Self::Player1, Self::Player2] + } + pub const fn to_index(self) -> usize { match self { Self::Player1 => 0, Self::Player2 => 1, @@ -75,7 +78,7 @@ pub struct Emulator { sims: Vec, audio: Audio, commands: mpsc::Receiver, - renderers: HashMap, + renderers: HashMap, running: Arc, has_game: Arc, } @@ -140,11 +143,17 @@ impl Emulator { idle = false; Sim::emulate_many(&mut self.sims); } - for (sim_id, renderer) in self.renderers.iter_mut() { - if let Some(sim) = self.sims.get_mut(sim_id.to_index()) { - if sim.read_pixels(&mut eye_contents) { - idle = false; - renderer.render(&eye_contents); + for sim_id in SimId::values() { + let Some(renderer) = self.renderers.get_mut(&sim_id) else { + continue; + }; + let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { + continue; + }; + if sim.read_pixels(&mut eye_contents) { + idle = false; + if renderer.queue_render(&eye_contents).is_err() { + self.renderers.remove(&sim_id); } } } @@ -226,7 +235,7 @@ impl Emulator { #[derive(Debug)] pub enum EmulatorCommand { - SetRenderer(SimId, GameRenderer), + SetRenderer(SimId, TextureSink), LoadGame(SimId, PathBuf), StartSecondSim(Option), StopSecondSim, diff --git a/src/graphics.rs b/src/graphics.rs new file mode 100644 index 0000000..4c0f75f --- /dev/null +++ b/src/graphics.rs @@ -0,0 +1,143 @@ +use std::{ + sync::{ + atomic::{AtomicU64, Ordering}, + mpsc, Arc, Mutex, MutexGuard, + }, + thread, +}; + +use anyhow::{bail, Result}; +use itertools::Itertools as _; +use wgpu::{ + Device, Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, Texture, + TextureDescriptor, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor, +}; + +#[derive(Debug)] +pub struct TextureSink { + buffers: Arc, + sink: mpsc::Sender, +} + +impl TextureSink { + pub fn new(device: &Device, queue: Arc) -> (Self, TextureView) { + let texture = Self::create_texture(device); + let view = texture.create_view(&TextureViewDescriptor::default()); + let buffers = Arc::new(BufferPool::new()); + let (sink, source) = mpsc::channel(); + let bufs = buffers.clone(); + thread::spawn(move || { + let mut local_buf = vec![0; 384 * 224 * 2]; + while let Ok(id) = source.recv() { + { + let Some(bytes) = bufs.read(id) else { + continue; + }; + local_buf.copy_from_slice(bytes.as_slice()); + } + Self::write_texture(&queue, &texture, local_buf.as_slice()); + } + }); + let sink = Self { buffers, sink }; + (sink, view) + } + + pub fn queue_render(&mut self, bytes: &[u8]) -> Result<()> { + let id = { + let (mut buf, id) = self.buffers.write()?; + buf.copy_from_slice(bytes); + id + }; + self.sink.send(id)?; + Ok(()) + } + + fn create_texture(device: &Device) -> Texture { + let desc = TextureDescriptor { + label: Some("eyes"), + size: Extent3d { + width: 384, + height: 224, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: TextureFormat::Rg8Unorm, + usage: TextureUsages::COPY_SRC + | TextureUsages::COPY_DST + | TextureUsages::TEXTURE_BINDING, + view_formats: &[TextureFormat::Rg8Unorm], + }; + device.create_texture(&desc) + } + + fn write_texture(queue: &Queue, texture: &Texture, bytes: &[u8]) { + let texture = ImageCopyTexture { + texture, + mip_level: 0, + origin: Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }; + let size = Extent3d { + width: 384, + height: 224, + depth_or_array_layers: 1, + }; + let data_layout = ImageDataLayout { + offset: 0, + bytes_per_row: Some(384 * 2), + rows_per_image: Some(224), + }; + queue.write_texture(texture, bytes, data_layout, size); + } +} + +#[derive(Debug)] +struct BufferPool { + buffers: [Buffer; 3], +} +impl BufferPool { + fn new() -> Self { + Self { + buffers: std::array::from_fn(|i| Buffer::new(i as u64)), + } + } + + fn read(&self, id: u64) -> Option>> { + let buf = self + .buffers + .iter() + .find(|buf| buf.id.load(Ordering::Acquire) == id)?; + buf.data.lock().ok() + } + + fn write(&self) -> Result<(MutexGuard<'_, Vec>, u64)> { + let (min, max) = self + .buffers + .iter() + .minmax_by_key(|buf| buf.id.load(Ordering::Acquire)) + .into_option() + .unwrap(); + let Ok(lock) = min.data.lock() else { + bail!("lock was poisoned") + }; + let id = max.id.load(Ordering::Acquire) + 1; + min.id.store(id, Ordering::Release); + Ok((lock, id)) + } +} + +#[derive(Debug)] +struct Buffer { + data: Mutex>, + id: AtomicU64, +} +impl Buffer { + fn new(id: u64) -> Self { + Self { + data: Mutex::new(vec![0; 384 * 224 * 2]), + id: AtomicU64::new(id), + } + } +} diff --git a/src/main.rs b/src/main.rs index 9297ee4..c0fc4b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,8 +11,8 @@ mod app; mod audio; mod controller; mod emulator; +mod graphics; mod input; -mod renderer; #[derive(Parser)] struct Args { diff --git a/src/renderer.rs b/src/renderer.rs deleted file mode 100644 index a97e9e7..0000000 --- a/src/renderer.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::sync::Arc; - -use wgpu::{ - Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, Texture, TextureDescriptor, - TextureFormat, TextureUsages, -}; - -#[derive(Debug)] -pub struct GameRenderer { - pub queue: Arc, - pub eyes: Arc, -} - -impl GameRenderer { - pub fn render(&self, buffer: &[u8]) { - let texture = ImageCopyTexture { - texture: &self.eyes, - mip_level: 0, - origin: Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }; - let size = Extent3d { - width: 384, - height: 224, - depth_or_array_layers: 1, - }; - let data_layout = ImageDataLayout { - offset: 0, - bytes_per_row: Some(384 * 2), - rows_per_image: Some(224), - }; - self.queue.write_texture(texture, buffer, data_layout, size); - } - pub fn create_texture(device: &wgpu::Device, name: &str) -> Texture { - let desc = TextureDescriptor { - label: Some(name), - size: Extent3d { - width: 384, - height: 224, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: TextureFormat::Rg8Unorm, - usage: TextureUsages::COPY_SRC - | TextureUsages::COPY_DST - | TextureUsages::TEXTURE_BINDING, - view_formats: &[TextureFormat::Rg8Unorm], - }; - device.create_texture(&desc) - } -}