Implement multiplayer #2

Merged
SonicSwordcane merged 21 commits from multiplayer into main 2024-11-30 00:31:10 +00:00
5 changed files with 167 additions and 75 deletions
Showing only changes of commit d306f19297 - Show all commits

View File

@ -1,4 +1,4 @@
use std::{sync::Arc, time::Instant}; use std::time::Instant;
use wgpu::util::DeviceExt as _; use wgpu::util::DeviceExt as _;
use winit::{ use winit::{
dpi::LogicalSize, dpi::LogicalSize,
@ -9,7 +9,7 @@ use winit::{
use crate::{ use crate::{
emulator::{EmulatorClient, EmulatorCommand, SimId}, emulator::{EmulatorClient, EmulatorCommand, SimId},
renderer::GameRenderer, graphics::TextureSink,
}; };
use super::{ use super::{
@ -46,15 +46,8 @@ impl GameWindow {
.build(); .build();
let device = &window.device; let device = &window.device;
let eyes = Arc::new(GameRenderer::create_texture(device, "eye")); let (sink, texture_view) = TextureSink::new(device, window.queue.clone());
client.send_command(EmulatorCommand::SetRenderer( client.send_command(EmulatorCommand::SetRenderer(sim_id, sink));
sim_id,
GameRenderer {
queue: window.queue.clone(),
eyes: eyes.clone(),
},
));
let eyes = eyes.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default());
let colors = Colors { let colors = Colors {
left: [1.0, 0.0, 0.0, 1.0], left: [1.0, 0.0, 0.0, 1.0],
@ -103,7 +96,7 @@ impl GameWindow {
entries: &[ entries: &[
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
binding: 0, binding: 0,
resource: wgpu::BindingResource::TextureView(&eyes), resource: wgpu::BindingResource::TextureView(&texture_view),
}, },
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
binding: 1, binding: 1,

View File

@ -11,7 +11,7 @@ use std::{
use anyhow::Result; use anyhow::Result;
use crate::{audio::Audio, renderer::GameRenderer}; use crate::{audio::Audio, graphics::TextureSink};
use shrooms_vb_core::Sim; use shrooms_vb_core::Sim;
pub use shrooms_vb_core::VBKey; pub use shrooms_vb_core::VBKey;
@ -30,7 +30,10 @@ pub enum SimId {
Player2, Player2,
} }
impl SimId { 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 { match self {
Self::Player1 => 0, Self::Player1 => 0,
Self::Player2 => 1, Self::Player2 => 1,
@ -75,7 +78,7 @@ pub struct Emulator {
sims: Vec<Sim>, sims: Vec<Sim>,
audio: Audio, audio: Audio,
commands: mpsc::Receiver<EmulatorCommand>, commands: mpsc::Receiver<EmulatorCommand>,
renderers: HashMap<SimId, GameRenderer>, renderers: HashMap<SimId, TextureSink>,
running: Arc<AtomicBool>, running: Arc<AtomicBool>,
has_game: Arc<AtomicBool>, has_game: Arc<AtomicBool>,
} }
@ -140,11 +143,17 @@ impl Emulator {
idle = false; idle = false;
Sim::emulate_many(&mut self.sims); Sim::emulate_many(&mut self.sims);
} }
for (sim_id, renderer) in self.renderers.iter_mut() { for sim_id in SimId::values() {
if let Some(sim) = self.sims.get_mut(sim_id.to_index()) { 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) { if sim.read_pixels(&mut eye_contents) {
idle = false; idle = false;
renderer.render(&eye_contents); if renderer.queue_render(&eye_contents).is_err() {
self.renderers.remove(&sim_id);
} }
} }
} }
@ -226,7 +235,7 @@ impl Emulator {
#[derive(Debug)] #[derive(Debug)]
pub enum EmulatorCommand { pub enum EmulatorCommand {
SetRenderer(SimId, GameRenderer), SetRenderer(SimId, TextureSink),
LoadGame(SimId, PathBuf), LoadGame(SimId, PathBuf),
StartSecondSim(Option<PathBuf>), StartSecondSim(Option<PathBuf>),
StopSecondSim, StopSecondSim,

143
src/graphics.rs Normal file
View File

@ -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<BufferPool>,
sink: mpsc::Sender<u64>,
}
impl TextureSink {
pub fn new(device: &Device, queue: Arc<Queue>) -> (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<MutexGuard<'_, Vec<u8>>> {
let buf = self
.buffers
.iter()
.find(|buf| buf.id.load(Ordering::Acquire) == id)?;
buf.data.lock().ok()
}
fn write(&self) -> Result<(MutexGuard<'_, Vec<u8>>, 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<Vec<u8>>,
id: AtomicU64,
}
impl Buffer {
fn new(id: u64) -> Self {
Self {
data: Mutex::new(vec![0; 384 * 224 * 2]),
id: AtomicU64::new(id),
}
}
}

View File

@ -11,8 +11,8 @@ mod app;
mod audio; mod audio;
mod controller; mod controller;
mod emulator; mod emulator;
mod graphics;
mod input; mod input;
mod renderer;
#[derive(Parser)] #[derive(Parser)]
struct Args { struct Args {

View File

@ -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<Queue>,
pub eyes: Arc<Texture>,
}
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)
}
}