diff --git a/Cargo.lock b/Cargo.lock index 8f06876..023dbe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.92" @@ -245,6 +294,46 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e10e7569f6ca78ef7664d7d651115172d4875c4410c050306bccde856a99a49" +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -255,6 +344,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "com" version = "0.6.0" @@ -583,6 +678,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.4.0" @@ -652,6 +753,12 @@ dependencies = [ "hashbrown 0.15.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "jni" version = "0.21.1" @@ -1383,6 +1490,7 @@ version = "0.1.0" dependencies = [ "anyhow", "cc", + "clap", "imgui", "imgui-wgpu", "imgui-winit-support", @@ -1470,6 +1578,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "1.0.109" @@ -1609,6 +1723,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index c764a56..2b1203f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] anyhow = "1" +clap = { version = "4", features = ["derive"] } imgui = "0.12" imgui-wgpu = { git = "https://github.com/Yatekii/imgui-wgpu-rs", rev = "2edd348" } imgui-winit-support = "0.13" @@ -13,4 +14,7 @@ wgpu = "22.1" winit = "0.30" [build-dependencies] -cc = "1" \ No newline at end of file +cc = "1" + +[profile.release] +lto = "thin" \ No newline at end of file diff --git a/build.rs b/build.rs index ed0f5b1..17bd565 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,10 @@ use std::path::Path; fn main() { + println!("cargo::rerun-if-changed=shrooms-vb-core"); cc::Build::new() .include(Path::new("shrooms-vb-core/core")) - .opt_level(3) + .opt_level(2) .flag_if_supported("-flto") .flag_if_supported("-fno-strict-aliasing") .file(Path::new("shrooms-vb-core/core/vb.c")) diff --git a/src/anaglyph.wgsl b/src/anaglyph.wgsl new file mode 100644 index 0000000..0015101 --- /dev/null +++ b/src/anaglyph.wgsl @@ -0,0 +1,49 @@ +// Vertex shader + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) tex_coords: vec2, +}; + +@vertex +fn vs_main( + @builtin(vertex_index) in_vertex_index: u32, +) -> VertexOutput { + var out: VertexOutput; + var x: f32; + var y: f32; + switch in_vertex_index { + case 0u, 3u: { + x = -1.0; + y = 1.0; + } + case 1u: { + x = -1.0; + y = -1.0; + } + case 2u, 4u: { + x = 1.0; + y = -1.0; + } + default: { + x = 1.0; + y = 1.0; + } + } + out.clip_position = vec4(x, y, 0.0, 1.0); + out.tex_coords = vec2((x + 1.0) / 2.0, (1.0 - y) / 2.0); + return out; +} + +// Fragment shader +@group(0) @binding(0) +var u_textures: binding_array>; +@group(0) @binding(1) +var u_sampler: sampler; + + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return textureSample(u_textures[0], u_sampler, in.tex_coords); + // return vec4(0.3, 0.2, 0.1, 1.0); +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..7919c6b --- /dev/null +++ b/src/app.rs @@ -0,0 +1,450 @@ +use imgui::*; +use imgui_wgpu::{Renderer, RendererConfig}; +use imgui_winit_support::WinitPlatform; +use pollster::block_on; +use std::{num::NonZero, sync::Arc, time::Instant}; +use winit::{ + application::ApplicationHandler, + dpi::LogicalSize, + event::{ElementState, Event, WindowEvent}, + event_loop::ActiveEventLoop, + keyboard::Key, + platform::windows::{CornerPreference, WindowAttributesExtWindows}, + window::Window, +}; + +use crate::{ + emulator::{EmulatorClient, EmulatorCommand}, + renderer::GameRenderer, +}; + +struct ImguiState { + context: imgui::Context, + platform: WinitPlatform, + renderer: Renderer, + clear_color: wgpu::Color, + last_frame: Instant, + last_cursor: Option, +} + +struct AppWindow { + device: wgpu::Device, + queue: Arc, + window: Arc, + surface_desc: wgpu::SurfaceConfiguration, + surface: wgpu::Surface<'static>, + hidpi_factor: f64, + pipeline: wgpu::RenderPipeline, + bind_group: wgpu::BindGroup, + imgui: Option, +} + +impl AppWindow { + fn setup_gpu(event_loop: &ActiveEventLoop, client: &EmulatorClient) -> Self { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::PRIMARY, + ..Default::default() + }); + + let window = { + let size = LogicalSize::new(384, 244); + + let attributes = Window::default_attributes() + .with_inner_size(size) + .with_title("Shrooms VB") + .with_corner_preference(CornerPreference::DoNotRound); + Arc::new(event_loop.create_window(attributes).unwrap()) + }; + + let size = window.inner_size(); + let hidpi_factor = window.scale_factor(); + let surface = instance.create_surface(window.clone()).unwrap(); + + let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + })) + .unwrap(); + + let (device, queue) = block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + required_features: wgpu::Features::TEXTURE_BINDING_ARRAY, + ..wgpu::DeviceDescriptor::default() + }, + None, + )) + .unwrap(); + let queue = Arc::new(queue); + let eyes = [ + Arc::new(GameRenderer::create_texture(&device, "left eye")), + Arc::new(GameRenderer::create_texture(&device, "right eye")), + ]; + client.send_command(EmulatorCommand::SetRenderer(GameRenderer { + queue: queue.clone(), + eyes: eyes.clone(), + })); + let eyes = [ + eyes[0].create_view(&wgpu::TextureViewDescriptor::default()), + eyes[1].create_view(&wgpu::TextureViewDescriptor::default()), + ]; + let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default()); + let texture_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("texture bind group layout"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: NonZero::new(2), + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("bind group"), + layout: &texture_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureViewArray(&[&eyes[0], &eyes[1]]), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + ], + }); + + // Set up swap chain + let surface_desc = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Fifo, + desired_maximum_frame_latency: 2, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], + }; + + surface.configure(&device, &surface_desc); + + let shader = device.create_shader_module(wgpu::include_wgsl!("anaglyph.wgsl")); + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("render pipeline layout"), + bind_group_layouts: &[&texture_bind_group_layout], + push_constant_ranges: &[], + }); + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("render pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Bgra8UnormSrgb, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + + let imgui = None; + Self { + device, + queue, + window, + surface_desc, + surface, + hidpi_factor, + pipeline: render_pipeline, + bind_group, + imgui, + } + } + + fn setup_imgui(&mut self) { + let mut context = imgui::Context::create(); + let mut platform = imgui_winit_support::WinitPlatform::new(&mut context); + platform.attach_window( + context.io_mut(), + &self.window, + imgui_winit_support::HiDpiMode::Default, + ); + context.set_ini_filename(None); + + let font_size = (13.0 * self.hidpi_factor) as f32; + context.io_mut().font_global_scale = (1.0 / self.hidpi_factor) as f32; + + context.fonts().add_font(&[FontSource::DefaultFontData { + config: Some(imgui::FontConfig { + oversample_h: 1, + pixel_snap_h: true, + size_pixels: font_size, + ..Default::default() + }), + }]); + + // + // Set up dear imgui wgpu renderer + // + let clear_color = wgpu::Color::BLACK; + + let renderer_config = RendererConfig { + texture_format: self.surface_desc.format, + ..Default::default() + }; + + let renderer = Renderer::new(&mut context, &self.device, &self.queue, renderer_config); + + let last_frame = Instant::now(); + let last_cursor = None; + + self.imgui = Some(ImguiState { + context, + platform, + renderer, + clear_color, + last_frame, + last_cursor, + }) + } + + fn new(event_loop: &ActiveEventLoop, client: &EmulatorClient) -> Self { + let mut window = Self::setup_gpu(event_loop, client); + window.setup_imgui(); + window + } +} + +pub struct App { + window: Option, + client: EmulatorClient, +} + +impl App { + pub fn new(client: EmulatorClient) -> Self { + Self { + window: None, + client, + } + } +} + +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + self.window = Some(AppWindow::new(event_loop, &self.client)); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: winit::window::WindowId, + event: WindowEvent, + ) { + let window = self.window.as_mut().unwrap(); + let imgui = window.imgui.as_mut().unwrap(); + + match &event { + WindowEvent::Resized(size) => { + window.surface_desc = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Fifo, + desired_maximum_frame_latency: 2, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], + }; + + window + .surface + .configure(&window.device, &window.surface_desc); + } + WindowEvent::CloseRequested => event_loop.exit(), + WindowEvent::KeyboardInput { event, .. } => { + if let Key::Character("s") = event.logical_key.as_ref() { + match event.state { + ElementState::Pressed => { + self.client.send_command(EmulatorCommand::PressStart) + } + ElementState::Released => { + self.client.send_command(EmulatorCommand::ReleaseStart) + } + } + } + } + WindowEvent::RedrawRequested => { + let now = Instant::now(); + imgui + .context + .io_mut() + .update_delta_time(now - imgui.last_frame); + imgui.last_frame = now; + + let frame = match window.surface.get_current_texture() { + Ok(frame) => frame, + Err(e) => { + eprintln!("dropped frame: {e:?}"); + return; + } + }; + imgui + .platform + .prepare_frame(imgui.context.io_mut(), &window.window) + .expect("Failed to prepare frame"); + let ui = imgui.context.frame(); + let mut height = 0.0; + ui.main_menu_bar(|| { + height = ui.window_size()[1]; + ui.menu("ROM", || { + if ui.menu_item("Open ROM") { + println!("clicked"); + } + if ui.menu_item("Quit") { + event_loop.exit(); + } + }); + ui.menu("Emulation", || { + if ui.menu_item("Pause") { + println!("clicked"); + } + if ui.menu_item("Reset") { + println!("clicked"); + } + }); + }); + + let mut encoder: wgpu::CommandEncoder = window + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + + if imgui.last_cursor != ui.mouse_cursor() { + imgui.last_cursor = ui.mouse_cursor(); + imgui.platform.prepare_render(ui, &window.window); + } + + let view = frame + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(imgui.clear_color), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + rpass.set_pipeline(&window.pipeline); + let hidpi = window.hidpi_factor as f32; + rpass.set_viewport(0.0, height * hidpi, 384.0 * hidpi, 224.0 * hidpi, 0.0, 1.0); + rpass.set_bind_group(0, &window.bind_group, &[]); + rpass.draw(0..6, 0..1); + + rpass.set_viewport(0.0, 0.0, 384.0 * hidpi, 224.0 * hidpi, 0.0, 1.0); + imgui + .renderer + .render( + imgui.context.render(), + &window.queue, + &window.device, + &mut rpass, + ) + .expect("Rendering failed"); + + drop(rpass); + + window.queue.submit(Some(encoder.finish())); + + frame.present(); + } + _ => (), + } + + imgui.platform.handle_event::<()>( + imgui.context.io_mut(), + &window.window, + &Event::WindowEvent { window_id, event }, + ); + } + + fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: ()) { + let window = self.window.as_mut().unwrap(); + let imgui = window.imgui.as_mut().unwrap(); + imgui.platform.handle_event::<()>( + imgui.context.io_mut(), + &window.window, + &Event::UserEvent(event), + ); + } + + fn device_event( + &mut self, + _event_loop: &ActiveEventLoop, + device_id: winit::event::DeviceId, + event: winit::event::DeviceEvent, + ) { + let window = self.window.as_mut().unwrap(); + let imgui = window.imgui.as_mut().unwrap(); + imgui.platform.handle_event::<()>( + imgui.context.io_mut(), + &window.window, + &Event::DeviceEvent { device_id, event }, + ); + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + let window = self.window.as_mut().unwrap(); + let imgui = window.imgui.as_mut().unwrap(); + window.window.request_redraw(); + imgui.platform.handle_event::<()>( + imgui.context.io_mut(), + &window.window, + &Event::AboutToWait, + ); + } +} diff --git a/src/emulator.rs b/src/emulator.rs new file mode 100644 index 0000000..5f1e201 --- /dev/null +++ b/src/emulator.rs @@ -0,0 +1,93 @@ +use std::{ + fs, + path::Path, + sync::mpsc::{self, TryRecvError}, +}; + +use anyhow::Result; + +use crate::{renderer::GameRenderer, shrooms_vb_core::CoreVB}; + +pub struct Emulator { + sim: CoreVB, + commands: mpsc::Receiver, + renderer: Option, +} + +impl Emulator { + pub fn new() -> (Self, EmulatorClient) { + let (sink, source) = mpsc::channel(); + let emu = Emulator { + sim: CoreVB::new(), + commands: source, + renderer: None, + }; + let queue = EmulatorClient { queue: sink }; + (emu, queue) + } + + pub fn load_rom(&mut self, path: &Path) -> Result<()> { + let bytes = fs::read(path)?; + self.sim.load_rom(bytes)?; + Ok(()) + } + + pub fn run(&mut self) { + let mut eye_contents = [vec![0u8; 384 * 224], vec![0u8; 384 * 224]]; + loop { + self.sim.emulate_frame(); + if let Some(renderer) = &mut self.renderer { + if self.sim.read_pixels(&mut eye_contents) { + renderer.render(&eye_contents); + } + } + loop { + match self.commands.try_recv() { + Ok(command) => self.handle_command(command), + Err(TryRecvError::Empty) => { + break; + } + Err(TryRecvError::Disconnected) => { + return; + } + } + } + } + } + + fn handle_command(&mut self, command: EmulatorCommand) { + match command { + EmulatorCommand::SetRenderer(renderer) => { + self.renderer = Some(renderer); + } + EmulatorCommand::PressStart => { + self.sim.set_keys(0x1003); + } + EmulatorCommand::ReleaseStart => { + self.sim.set_keys(0x0003); + } + } + } +} + +#[derive(Debug)] +pub enum EmulatorCommand { + SetRenderer(GameRenderer), + PressStart, + ReleaseStart, +} + +pub struct EmulatorClient { + queue: mpsc::Sender, +} + +impl EmulatorClient { + pub fn send_command(&self, command: EmulatorCommand) { + if let Err(err) = self.queue.send(command) { + eprintln!( + "could not send command {:?} as emulator is shut down", + err.0 + ); + } + } +} diff --git a/src/main.rs b/src/main.rs index b9de0c2..8f625d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,325 +1,32 @@ -use imgui::*; -use imgui_wgpu::{Renderer, RendererConfig}; -use imgui_winit_support::WinitPlatform; -use pollster::block_on; -use std::{sync::Arc, time::Instant}; -use winit::{ - application::ApplicationHandler, - dpi::LogicalSize, - event::{Event, WindowEvent}, - event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, - keyboard::{Key, NamedKey}, - window::Window, -}; +use std::{path::PathBuf, thread}; -struct ImguiState { - context: imgui::Context, - platform: WinitPlatform, - renderer: Renderer, - clear_color: wgpu::Color, - last_frame: Instant, - last_cursor: Option, +use anyhow::Result; +use app::App; +use clap::Parser; +use emulator::Emulator; +use winit::event_loop::{ControlFlow, EventLoop}; + +mod app; +mod emulator; +mod renderer; +mod shrooms_vb_core; + +#[derive(Parser)] +struct Args { + rom: PathBuf, } -struct AppWindow { - device: wgpu::Device, - queue: wgpu::Queue, - window: Arc, - surface_desc: wgpu::SurfaceConfiguration, - surface: wgpu::Surface<'static>, - hidpi_factor: f64, - imgui: Option, -} +fn main() -> Result<()> { + let args = Args::parse(); -#[derive(Default)] -struct App { - window: Option, -} + let (mut emulator, client) = Emulator::new(); + emulator.load_rom(&args.rom)?; + thread::spawn(move || { + emulator.run(); + }); -impl AppWindow { - fn setup_gpu(event_loop: &ActiveEventLoop) -> Self { - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::PRIMARY, - ..Default::default() - }); - - let window = { - let size = LogicalSize::new(384, 244); - - let attributes = Window::default_attributes() - .with_inner_size(size) - .with_title("Shrooms VB"); - Arc::new(event_loop.create_window(attributes).unwrap()) - }; - - let size = window.inner_size(); - let hidpi_factor = window.scale_factor(); - let surface = instance.create_surface(window.clone()).unwrap(); - - let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::HighPerformance, - compatible_surface: Some(&surface), - force_fallback_adapter: false, - })) - .unwrap(); - - let (device, queue) = - block_on(adapter.request_device(&wgpu::DeviceDescriptor::default(), None)).unwrap(); - - // Set up swap chain - let surface_desc = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: wgpu::TextureFormat::Bgra8UnormSrgb, - width: size.width, - height: size.height, - present_mode: wgpu::PresentMode::Fifo, - desired_maximum_frame_latency: 2, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], - }; - - surface.configure(&device, &surface_desc); - - let imgui = None; - Self { - device, - queue, - window, - surface_desc, - surface, - hidpi_factor, - imgui, - } - } - - fn setup_imgui(&mut self) { - let mut context = imgui::Context::create(); - let mut platform = imgui_winit_support::WinitPlatform::new(&mut context); - platform.attach_window( - context.io_mut(), - &self.window, - imgui_winit_support::HiDpiMode::Default, - ); - context.set_ini_filename(None); - - let font_size = (13.0 * self.hidpi_factor) as f32; - context.io_mut().font_global_scale = (1.0 / self.hidpi_factor) as f32; - - context.fonts().add_font(&[FontSource::DefaultFontData { - config: Some(imgui::FontConfig { - oversample_h: 1, - pixel_snap_h: true, - size_pixels: font_size, - ..Default::default() - }), - }]); - - // - // Set up dear imgui wgpu renderer - // - let clear_color = wgpu::Color::BLACK; - - let renderer_config = RendererConfig { - texture_format: self.surface_desc.format, - ..Default::default() - }; - - let renderer = Renderer::new(&mut context, &self.device, &self.queue, renderer_config); - let last_frame = Instant::now(); - let last_cursor = None; - - self.imgui = Some(ImguiState { - context, - platform, - renderer, - clear_color, - last_frame, - last_cursor, - }) - } - - fn new(event_loop: &ActiveEventLoop) -> Self { - let mut window = Self::setup_gpu(event_loop); - window.setup_imgui(); - window - } -} - -impl ApplicationHandler for App { - fn resumed(&mut self, event_loop: &ActiveEventLoop) { - self.window = Some(AppWindow::new(event_loop)); - } - - fn window_event( - &mut self, - event_loop: &ActiveEventLoop, - window_id: winit::window::WindowId, - event: WindowEvent, - ) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - let mut quit = false; - - match &event { - WindowEvent::Resized(size) => { - window.surface_desc = wgpu::SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: wgpu::TextureFormat::Bgra8UnormSrgb, - width: size.width, - height: size.height, - present_mode: wgpu::PresentMode::Fifo, - desired_maximum_frame_latency: 2, - alpha_mode: wgpu::CompositeAlphaMode::Auto, - view_formats: vec![wgpu::TextureFormat::Bgra8Unorm], - }; - - window - .surface - .configure(&window.device, &window.surface_desc); - } - WindowEvent::CloseRequested => event_loop.exit(), - WindowEvent::KeyboardInput { event, .. } => { - if let Key::Named(NamedKey::Escape) = event.logical_key { - if event.state.is_pressed() { - quit = true; - } - } - } - WindowEvent::RedrawRequested => { - let now = Instant::now(); - imgui - .context - .io_mut() - .update_delta_time(now - imgui.last_frame); - imgui.last_frame = now; - - let frame = match window.surface.get_current_texture() { - Ok(frame) => frame, - Err(e) => { - eprintln!("dropped frame: {e:?}"); - return; - } - }; - imgui - .platform - .prepare_frame(imgui.context.io_mut(), &window.window) - .expect("Failed to prepare frame"); - let ui = imgui.context.frame(); - ui.main_menu_bar(|| { - ui.menu("ROM", || { - if ui.menu_item("Open ROM") { - println!("clicked"); - } - if ui.menu_item("Quit") { - event_loop.exit(); - } - }); - ui.menu("Emulation", || { - if ui.menu_item("Pause") { - println!("clicked"); - } - if ui.menu_item("Reset") { - println!("clicked"); - } - }); - }); - - let mut encoder: wgpu::CommandEncoder = window - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - - if imgui.last_cursor != ui.mouse_cursor() { - imgui.last_cursor = ui.mouse_cursor(); - imgui.platform.prepare_render(ui, &window.window); - } - - let view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); - let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - label: None, - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(imgui.clear_color), - store: wgpu::StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }); - - imgui - .renderer - .render( - imgui.context.render(), - &window.queue, - &window.device, - &mut rpass, - ) - .expect("Rendering failed"); - - drop(rpass); - - window.queue.submit(Some(encoder.finish())); - - frame.present(); - } - _ => (), - } - - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::WindowEvent { window_id, event }, - ); - - if quit { - self.window.take(); - } - } - - fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: ()) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::UserEvent(event), - ); - } - - fn device_event( - &mut self, - _event_loop: &ActiveEventLoop, - device_id: winit::event::DeviceId, - event: winit::event::DeviceEvent, - ) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::DeviceEvent { device_id, event }, - ); - } - - fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { - let window = self.window.as_mut().unwrap(); - let imgui = window.imgui.as_mut().unwrap(); - window.window.request_redraw(); - imgui.platform.handle_event::<()>( - imgui.context.io_mut(), - &window.window, - &Event::AboutToWait, - ); - } -} - -fn main() { let event_loop = EventLoop::new().unwrap(); event_loop.set_control_flow(ControlFlow::Poll); - event_loop.run_app(&mut App::default()).unwrap(); + event_loop.run_app(&mut App::new(client))?; + Ok(()) } diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..06dbe91 --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,58 @@ +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; 2], +} + +impl GameRenderer { + pub fn render(&self, buffers: &[Vec; 2]) { + for (texture, buffer) in self.eyes.iter().zip(buffers) { + self.update_texture(texture, buffer); + } + } + fn update_texture(&self, texture: &Texture, buffer: &[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), + 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::R8Unorm, + usage: TextureUsages::COPY_SRC + | TextureUsages::COPY_DST + | TextureUsages::TEXTURE_BINDING, + view_formats: &[TextureFormat::R8Unorm], + }; + device.create_texture(&desc) + } +} diff --git a/src/shrooms_vb_core.rs b/src/shrooms_vb_core.rs index 2df7589..fa085ed 100644 --- a/src/shrooms_vb_core.rs +++ b/src/shrooms_vb_core.rs @@ -1,37 +1,159 @@ +use std::{ffi::c_void, ptr}; + +use anyhow::{anyhow, Result}; #[repr(C)] -struct VB { _data: [u8; 0] } +struct VB { + _data: [u8; 0], +} + +#[allow(non_camel_case_types)] +type c_int = i32; +type OnFrame = extern "C" fn(sim: *mut VB) -> c_int; #[link(name = "vb")] extern "C" { + #[link_name = "vbEmulate"] + fn vb_emulate(sim: *mut VB, cycles: *mut u32) -> c_int; + #[link_name = "vbGetCartROM"] + fn vb_get_cart_rom(sim: *mut VB, size: *mut u32) -> *mut c_void; + #[link_name = "vbGetPixels"] + fn vb_get_pixels( + sim: *mut VB, + left: *mut c_void, + left_stride_x: c_int, + left_stride_y: c_int, + right: *mut c_void, + right_stride_x: c_int, + right_stride_y: c_int, + ); + #[link_name = "vbGetUserData"] + fn vb_get_user_data(sim: *mut VB) -> *mut c_void; #[link_name = "vbInit"] fn vb_init(sim: *mut VB) -> *mut VB; - + #[link_name = "vbReset"] + fn vb_reset(sim: *mut VB); + #[link_name = "vbSetCartROM"] + fn vb_set_cart_rom(sim: *mut VB, rom: *mut c_void, size: u32) -> c_int; + #[link_name = "vbSetKeys"] + fn vb_set_keys(sim: *mut VB, keys: u16) -> u16; + #[link_name = "vbSetFrameCallback"] + fn vb_set_frame_callback(sim: *mut VB, on_frame: OnFrame); + #[link_name = "vbSetUserData"] + fn vb_set_user_data(sim: *mut VB, tag: *mut c_void); #[link_name = "vbSizeOf"] fn vb_size_of() -> usize; +} - #[link_name = "vbGetKeys"] - fn vb_get_keys(sim: *mut VB) -> u16; +extern "C" fn on_frame(sim: *mut VB) -> i32 { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() }; + data.frame_seen = true; + 1 +} + +struct VBState { + frame_seen: bool, } pub struct CoreVB { sim: *mut VB, - _data: Box<[u8]>, } +// SAFETY: the memory pointed to by sim is valid +unsafe impl Send for CoreVB {} + impl CoreVB { pub fn new() -> Self { + // init the VB instance itself let size = unsafe { vb_size_of() }; - let mut data = vec![0; size].into_boxed_slice(); - let sim = data.as_mut_ptr() as *mut VB; + // allocate a vec of u64 so that this memory is 8-byte aligned + let memory = vec![0u64; size.div_ceil(4)]; + let sim: *mut VB = Box::into_raw(memory.into_boxed_slice()).cast(); unsafe { vb_init(sim) }; - CoreVB { - sim, - _data: data + unsafe { vb_reset(sim) }; + + // set up userdata + let state = VBState { frame_seen: false }; + unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) }; + unsafe { vb_set_frame_callback(sim, on_frame) }; + + CoreVB { sim } + } + + pub fn load_rom(&mut self, rom: Vec) -> Result<()> { + self.unload_rom(); + + let size = rom.len() as u32; + let rom = Box::into_raw(rom.into_boxed_slice()).cast(); + let status = unsafe { vb_set_cart_rom(self.sim, rom, size) }; + if status == 0 { + Ok(()) + } else { + let _: Vec = + unsafe { Vec::from_raw_parts(rom.cast(), size as usize, size as usize) }; + Err(anyhow!("Invalid ROM size of {} bytes", size)) } } - pub fn keys(&self) -> u16 { - unsafe { vb_get_keys(self.sim) } + fn unload_rom(&mut self) -> Option> { + let mut size = 0; + let rom = unsafe { vb_get_cart_rom(self.sim, &mut size) }; + if rom.is_null() { + return None; + } + unsafe { vb_set_cart_rom(self.sim, ptr::null_mut(), 0) }; + let vec = unsafe { Vec::from_raw_parts(rom.cast(), size as usize, size as usize) }; + Some(vec) } -} \ No newline at end of file + + pub fn emulate_frame(&mut self) { + let mut cycles = 20_000_000; + unsafe { vb_emulate(self.sim, &mut cycles) }; + } + + pub fn read_pixels(&mut self, buffers: &mut [Vec; 2]) -> bool { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + let data: &mut VBState = unsafe { &mut *vb_get_user_data(self.sim).cast() }; + if !data.frame_seen { + return false; + } + data.frame_seen = false; + + assert_eq!(buffers[0].len(), 384 * 224); + assert_eq!(buffers[1].len(), 384 * 224); + unsafe { + vb_get_pixels( + self.sim, + buffers[0].as_mut_ptr().cast(), + 1, + 384, + buffers[1].as_mut_ptr().cast(), + 1, + 384, + ); + }; + true + } + + pub fn set_keys(&mut self, keys: u16) { + unsafe { vb_set_keys(self.sim, keys) }; + } +} + +impl Drop for CoreVB { + fn drop(&mut self) { + // SAFETY: the *mut VB owns its userdata. + // There is no way for the userdata to be null or otherwise invalid. + let ptr: *mut VBState = unsafe { vb_get_user_data(self.sim).cast() }; + // SAFETY: we made this pointer ourselves, we can for sure free it + unsafe { drop(Box::from_raw(ptr)) }; + + let len = unsafe { vb_size_of() }.div_ceil(4); + // SAFETY: the sim's memory originally came from a Vec + let bytes: Vec = unsafe { Vec::from_raw_parts(self.sim.cast(), len, len) }; + drop(bytes); + } +}