Compare commits

..

No commits in common. "9ff62af310505bf90f1b9a7c9396941023c9a2c5" and "4b34d138acbc6c5f07f3e56e47b14f83abba9e67" have entirely different histories.

12 changed files with 1210 additions and 2080 deletions

1494
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,20 +9,19 @@ bitflags = "2"
bytemuck = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] }
cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" }
egui = "0.29"
egui_extras = "0.29"
egui-winit = "0.29"
egui-wgpu = { version = "0.29", features = ["winit"] }
gilrs = "0.11"
imgui = { version = "0.12", features = ["tables-api"] }
imgui-wgpu = { git = "https://github.com/SupernaviX/imgui-wgpu-rs", rev = "5bb8673" }
imgui-winit-support = "0.13"
itertools = "0.13"
native-dialog = "0.7"
num-derive = "0.4"
num-traits = "0.2"
pollster = "0.4"
rfd = "0.15"
rtrb = "0.3"
rubato = "0.16"
thread-priority = "1"
wgpu = "22.1"
wgpu = "23.0"
winit = "0.30"
[target.'cfg(windows)'.dependencies]

View File

@ -1,35 +1,35 @@
use std::{collections::HashSet, num::NonZero, sync::Arc, thread};
use std::{collections::HashMap, fmt::Debug, thread};
use egui::{
ahash::{HashMap, HashMapExt},
Context, FontData, FontDefinitions, FontFamily, TextWrapMode, ViewportBuilder, ViewportCommand,
ViewportId, ViewportInfo,
};
use game::GameWindow;
use gilrs::{EventType, Gilrs};
use input::InputWindow;
use winit::{
application::ApplicationHandler,
event::{KeyEvent, WindowEvent},
event::{Event, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
window::Window,
window::WindowId,
};
use crate::{
controller::ControllerManager,
emulator::{EmulatorClient, SimId},
input::MappingProvider,
window::{AppWindow, GameWindow, InputWindow},
};
pub struct Application {
mod common;
mod game;
mod input;
pub struct App {
windows: HashMap<WindowId, Box<dyn AppWindow>>,
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
mappings: MappingProvider,
controllers: ControllerManager,
viewports: HashMap<ViewportId, Viewport>,
focused: Option<ViewportId>,
proxy: EventLoopProxy<UserEvent>,
player_2_window: Option<WindowId>,
}
impl Application {
impl App {
pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
let mappings = MappingProvider::new();
let controllers = ControllerManager::new(client.clone(), &mappings);
@ -39,304 +39,109 @@ impl Application {
thread::spawn(|| process_gamepad_input(mappings, proxy));
}
Self {
windows: HashMap::new(),
client,
proxy,
mappings,
controllers,
viewports: HashMap::new(),
focused: None,
proxy,
player_2_window: None,
}
}
fn open(&mut self, event_loop: &ActiveEventLoop, window: Box<dyn AppWindow>) {
let viewport_id = window.viewport_id();
if self.viewports.contains_key(&viewport_id) {
return;
}
self.viewports
.insert(viewport_id, Viewport::new(event_loop, window));
}
}
impl ApplicationHandler<UserEvent> for Application {
impl ApplicationHandler<UserEvent> for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let app = GameWindow::new(self.client.clone(), self.proxy.clone(), SimId::Player1);
let wrapper = Viewport::new(event_loop, Box::new(app));
self.focused = Some(wrapper.id());
self.viewports.insert(wrapper.id(), wrapper);
let window = GameWindow::new(
event_loop,
SimId::Player1,
self.client.clone(),
self.proxy.clone(),
);
self.windows.insert(window.id(), Box::new(window));
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: winit::window::WindowId,
window_id: WindowId,
event: WindowEvent,
) {
let Some(viewport) = self
.viewports
.values_mut()
.find(|v| v.window.id() == window_id)
else {
if let WindowEvent::KeyboardInput { event, .. } = &event {
self.controllers.handle_key_event(event);
}
let Some(window) = self.windows.get_mut(&window_id) else {
return;
};
let viewport_id = viewport.id();
match &event {
WindowEvent::KeyboardInput { event, .. } => {
self.controllers.handle_key_event(event);
viewport.handle_key_event(event);
window.handle_event(event_loop, &Event::WindowEvent { window_id, event });
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
match event {
UserEvent::OpenInputWindow => {
let window =
InputWindow::new(event_loop, self.mappings.clone(), self.proxy.clone());
self.windows.insert(window.id(), Box::new(window));
}
WindowEvent::Focused(new_focused) => {
self.focused = new_focused.then_some(viewport_id);
}
_ => {}
}
let mut queue_redraw = false;
let mut inactive_viewports = HashSet::new();
match viewport.on_window_event(event) {
Some(Action::Redraw) => {
for viewport in self.viewports.values_mut() {
match viewport.redraw(event_loop) {
Some(Action::Redraw) => {
queue_redraw = true;
}
Some(Action::Close) => {
inactive_viewports.insert(viewport.id());
}
None => {}
}
UserEvent::OpenPlayer2Window => {
if self.player_2_window.is_some() {
return;
}
let window = GameWindow::new(
event_loop,
SimId::Player2,
self.client.clone(),
self.proxy.clone(),
);
self.player_2_window = Some(window.id());
self.windows.insert(window.id(), Box::new(window));
}
Some(Action::Close) => {
inactive_viewports.insert(viewport_id);
}
None => {}
}
self.viewports
.retain(|k, _| !inactive_viewports.contains(k));
match self.viewports.get(&ViewportId::ROOT) {
Some(viewport) => {
if queue_redraw {
viewport.window.request_redraw();
UserEvent::Close(window_id) => {
if self.player_2_window == Some(window_id) {
self.player_2_window.take();
}
self.windows.remove(&window_id);
}
UserEvent::GamepadEvent(event) => {
self.controllers.handle_gamepad_event(&event);
}
None => event_loop.exit(),
}
}
fn device_event(
&mut self,
_event_loop: &ActiveEventLoop,
_device_id: winit::event::DeviceId,
event_loop: &ActiveEventLoop,
device_id: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
let Some(viewport) = self
.focused
.as_ref()
.and_then(|id| self.viewports.get_mut(id))
else {
return;
};
viewport.state.on_mouse_motion(delta);
for window in self.windows.values_mut() {
window.handle_event(
event_loop,
&Event::DeviceEvent {
device_id,
event: event.clone(),
},
);
}
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
match event {
UserEvent::GamepadEvent(event) => self.controllers.handle_gamepad_event(&event),
UserEvent::OpenInput => {
let input = InputWindow::new(self.mappings.clone());
self.open(event_loop, Box::new(input));
}
UserEvent::OpenPlayer2 => {
let p2 = GameWindow::new(self.client.clone(), self.proxy.clone(), SimId::Player2);
self.open(event_loop, Box::new(p2));
}
}
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
if let Some(viewport) = self.viewports.get(&ViewportId::ROOT) {
viewport.window.request_redraw();
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
for window in self.windows.values_mut() {
window.handle_event(event_loop, &Event::AboutToWait);
}
}
}
struct Viewport {
painter: egui_wgpu::winit::Painter,
ctx: Context,
info: ViewportInfo,
commands: Vec<ViewportCommand>,
builder: ViewportBuilder,
window: Arc<Window>,
state: egui_winit::State,
app: Box<dyn AppWindow>,
}
impl Viewport {
pub fn new(event_loop: &ActiveEventLoop, mut app: Box<dyn AppWindow>) -> Self {
let mut painter = egui_wgpu::winit::Painter::new(
egui_wgpu::WgpuConfiguration::default(),
1,
None,
false,
true,
);
let ctx = Context::default();
let mut fonts = FontDefinitions::empty();
fonts.font_data.insert(
"Selawik".into(),
FontData::from_static(include_bytes!("../assets/selawik.ttf")),
);
fonts
.families
.get_mut(&FontFamily::Proportional)
.unwrap()
.insert(0, "Selawik".into());
ctx.set_fonts(fonts);
ctx.style_mut(|s| {
s.wrap_mode = Some(TextWrapMode::Extend);
s.visuals.menu_rounding = Default::default();
});
let mut info = ViewportInfo::default();
let builder = app.initial_viewport();
let (window, state) = create_window_and_state(&ctx, event_loop, &builder, &mut painter);
egui_winit::update_viewport_info(&mut info, &ctx, &window, true);
app.on_init(painter.render_state().as_ref().unwrap());
Self {
painter,
ctx,
info,
commands: vec![],
builder,
window,
state,
app,
}
}
pub fn id(&self) -> ViewportId {
self.app.viewport_id()
}
pub fn on_window_event(&mut self, event: WindowEvent) -> Option<Action> {
let response = self.state.on_window_event(&self.window, &event);
egui_winit::update_viewport_info(
&mut self.info,
self.state.egui_ctx(),
&self.window,
false,
);
match event {
WindowEvent::RedrawRequested => Some(Action::Redraw),
WindowEvent::CloseRequested => Some(Action::Close),
WindowEvent::Resized(size) => {
let (Some(width), Some(height)) =
(NonZero::new(size.width), NonZero::new(size.height))
else {
return None;
};
self.painter
.on_window_resized(ViewportId::ROOT, width, height);
None
}
_ if response.repaint => Some(Action::Redraw),
_ => None,
}
}
pub fn handle_key_event(&mut self, event: &KeyEvent) {
self.app.handle_key_event(event);
}
fn redraw(&mut self, event_loop: &ActiveEventLoop) -> Option<Action> {
let mut input = self.state.take_egui_input(&self.window);
input.viewports = std::iter::once((ViewportId::ROOT, self.info.clone())).collect();
let mut output = self.ctx.run(input, |ctx| {
self.app.show(ctx);
});
let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point);
self.painter.paint_and_update_textures(
ViewportId::ROOT,
output.pixels_per_point,
[0.0, 0.0, 0.0, 0.0],
&clipped_primitives,
&output.textures_delta,
false,
);
self.state
.handle_platform_output(&self.window, output.platform_output);
let Some(mut viewport_output) = output.viewport_output.remove(&ViewportId::ROOT) else {
return Some(Action::Close);
};
let (mut deferred_commands, recreate) = self.builder.patch(viewport_output.builder);
if recreate {
let (window, state) =
create_window_and_state(&self.ctx, event_loop, &self.builder, &mut self.painter);
egui_winit::update_viewport_info(&mut self.info, &self.ctx, &window, true);
self.window = window;
self.state = state;
}
self.commands.append(&mut deferred_commands);
self.commands.append(&mut viewport_output.commands);
egui_winit::process_viewport_commands(
&self.ctx,
&mut self.info,
std::mem::take(&mut self.commands),
&self.window,
&mut HashSet::default(),
);
if self.info.close_requested() {
Some(Action::Close)
} else {
Some(Action::Redraw)
}
}
}
impl Drop for Viewport {
fn drop(&mut self) {
self.app.on_destroy();
}
pub trait AppWindow {
fn id(&self) -> WindowId;
fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event<UserEvent>);
}
#[derive(Debug)]
pub enum UserEvent {
OpenInputWindow,
OpenPlayer2Window,
Close(WindowId),
GamepadEvent(gilrs::Event),
OpenInput,
OpenPlayer2,
}
pub enum Action {
Redraw,
Close,
}
fn create_window_and_state(
ctx: &Context,
event_loop: &ActiveEventLoop,
builder: &ViewportBuilder,
painter: &mut egui_wgpu::winit::Painter,
) -> (Arc<Window>, egui_winit::State) {
pollster::block_on(painter.set_window(ViewportId::ROOT, None)).unwrap();
let window = Arc::new(egui_winit::create_window(ctx, event_loop, builder).unwrap());
pollster::block_on(painter.set_window(ViewportId::ROOT, Some(window.clone()))).unwrap();
let state = egui_winit::State::new(
ctx.clone(),
ViewportId::ROOT,
event_loop,
Some(window.scale_factor() as f32),
event_loop.system_theme(),
painter.max_texture_side(),
);
(window, state)
}
fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy<UserEvent>) {

264
src/app/common.rs Normal file
View File

@ -0,0 +1,264 @@
use std::{
ops::{Deref, DerefMut},
sync::Arc,
time::Instant,
};
use imgui::{FontSource, MouseCursor, SuspendedContext, WindowToken};
use imgui_wgpu::{Renderer, RendererConfig};
use imgui_winit_support::WinitPlatform;
use pollster::block_on;
#[cfg(target_os = "windows")]
use winit::platform::windows::{CornerPreference, WindowAttributesExtWindows as _};
use winit::{
dpi::{LogicalSize, PhysicalSize, Size},
event_loop::ActiveEventLoop,
window::{Window, WindowAttributes},
};
pub struct WindowStateBuilder<'a> {
event_loop: &'a ActiveEventLoop,
attributes: WindowAttributes,
}
impl<'a> WindowStateBuilder<'a> {
pub fn new(event_loop: &'a ActiveEventLoop) -> Self {
let attributes = Window::default_attributes();
#[cfg(target_os = "windows")]
let attributes = attributes.with_corner_preference(CornerPreference::DoNotRound);
Self {
event_loop,
attributes,
}
}
pub fn with_title<T: Into<String>>(self, title: T) -> Self {
Self {
attributes: self.attributes.with_title(title),
..self
}
}
pub fn with_inner_size<S: Into<Size>>(self, size: S) -> Self {
Self {
attributes: self.attributes.with_inner_size(size),
..self
}
}
pub fn build(self) -> WindowState {
WindowState::new(self.event_loop, self.attributes)
}
}
#[derive(Debug)]
pub struct WindowState {
pub device: wgpu::Device,
pub queue: Arc<wgpu::Queue>,
pub window: Arc<Window>,
pub surface_desc: wgpu::SurfaceConfiguration,
pub surface: wgpu::Surface<'static>,
pub hidpi_factor: f64,
pub minimized: bool,
}
impl WindowState {
fn new(event_loop: &ActiveEventLoop, attributes: WindowAttributes) -> Self {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let window = 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();
let queue = Arc::new(queue);
// 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);
Self {
device,
queue,
window,
surface_desc,
surface,
hidpi_factor,
minimized: false,
}
}
pub fn logical_size(&self) -> LogicalSize<u32> {
PhysicalSize::new(self.surface_desc.width, self.surface_desc.height)
.to_logical(self.hidpi_factor)
}
pub fn handle_resize(&mut self, size: &PhysicalSize<u32>) {
if size.width > 0 && size.height > 0 {
self.minimized = false;
self.surface_desc.width = size.width;
self.surface_desc.height = size.height;
self.surface.configure(&self.device, &self.surface_desc);
} else {
self.minimized = true;
}
}
}
pub struct ImguiState {
pub context: ContextGuard,
pub platform: WinitPlatform,
pub renderer: Renderer,
pub clear_color: wgpu::Color,
pub last_frame: Instant,
pub last_cursor: Option<MouseCursor>,
}
impl ImguiState {
pub fn new(window: &WindowState) -> Self {
let mut context_guard = ContextGuard::new();
let mut context = context_guard.lock().unwrap();
let mut platform = imgui_winit_support::WinitPlatform::new(&mut context);
platform.attach_window(
context.io_mut(),
&window.window,
imgui_winit_support::HiDpiMode::Default,
);
context.set_ini_filename(None);
let font_size = (16.0 * window.hidpi_factor) as f32;
context.io_mut().font_global_scale = (1.0 / window.hidpi_factor) as f32;
context.fonts().add_font(&[FontSource::TtfData {
data: include_bytes!("../../assets/selawk.ttf"),
size_pixels: font_size,
config: Some(imgui::FontConfig {
oversample_h: 1,
pixel_snap_h: true,
size_pixels: font_size,
..Default::default()
}),
}]);
let style = context.style_mut();
style.use_light_colors();
//
// Set up dear imgui wgpu renderer
//
let renderer_config = RendererConfig {
texture_format: window.surface_desc.format,
..Default::default()
};
let renderer = Renderer::new(&mut context, &window.device, &window.queue, renderer_config);
let last_frame = Instant::now();
let last_cursor = None;
drop(context);
Self {
context: context_guard,
platform,
renderer,
clear_color: wgpu::Color::BLACK,
last_frame,
last_cursor,
}
}
}
pub struct ContextGuard {
value: Option<SuspendedContext>,
}
impl ContextGuard {
fn new() -> Self {
Self {
value: Some(SuspendedContext::create()),
}
}
pub fn lock(&mut self) -> Option<ContextLock<'_>> {
let sus = self.value.take()?;
match sus.activate() {
Ok(ctx) => Some(ContextLock {
ctx: Some(ctx),
holder: self,
}),
Err(sus) => {
self.value = Some(sus);
None
}
}
}
}
pub struct ContextLock<'a> {
ctx: Option<imgui::Context>,
holder: &'a mut ContextGuard,
}
impl<'a> Deref for ContextLock<'a> {
type Target = imgui::Context;
fn deref(&self) -> &Self::Target {
self.ctx.as_ref().unwrap()
}
}
impl<'a> DerefMut for ContextLock<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.ctx.as_mut().unwrap()
}
}
impl<'a> Drop for ContextLock<'a> {
fn drop(&mut self) {
self.holder.value = self.ctx.take().map(|c| c.suspend())
}
}
pub trait UiExt {
fn fullscreen_window(&self) -> Option<WindowToken<'_>>;
fn right_align_text<T: AsRef<str>>(&self, text: T, space: f32);
}
impl UiExt for imgui::Ui {
fn fullscreen_window(&self) -> Option<WindowToken<'_>> {
self.window("fullscreen")
.position([0.0, 0.0], imgui::Condition::Always)
.size(self.io().display_size, imgui::Condition::Always)
.flags(imgui::WindowFlags::NO_DECORATION)
.begin()
}
fn right_align_text<T: AsRef<str>>(&self, text: T, space: f32) {
let width = self.calc_text_size(text.as_ref())[0];
let [left, y] = self.cursor_pos();
let right = left + space;
self.set_cursor_pos([right - width, y]);
self.text(text);
}
}

408
src/app/game.rs Normal file
View File

@ -0,0 +1,408 @@
use std::time::Instant;
use wgpu::util::DeviceExt as _;
use winit::{
dpi::LogicalSize,
event::{Event, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
window::WindowId,
};
use crate::{
emulator::{EmulatorClient, EmulatorCommand, SimId},
graphics::TextureSink,
};
use super::{
common::{ImguiState, WindowState, WindowStateBuilder},
AppWindow, UserEvent,
};
pub struct GameWindow {
window: WindowState,
imgui: ImguiState,
pipeline: wgpu::RenderPipeline,
bind_group: wgpu::BindGroup,
sim_id: SimId,
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
paused_due_to_minimize: bool,
}
impl GameWindow {
pub fn new(
event_loop: &ActiveEventLoop,
sim_id: SimId,
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
) -> Self {
let title = if sim_id == SimId::Player2 {
"Shrooms VB (Player 2)"
} else {
"Shrooms VB"
};
let window = WindowStateBuilder::new(event_loop)
.with_title(title)
.with_inner_size(LogicalSize::new(384, 244))
.build();
let device = &window.device;
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],
right: [0.0, 0.7734375, 0.9375, 1.0],
};
let color_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("colors"),
contents: bytemuck::bytes_of(&colors),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
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: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
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::TextureView(&texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: color_buf.as_entire_binding(),
},
],
});
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: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("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 = ImguiState::new(&window);
Self {
window,
imgui,
pipeline: render_pipeline,
bind_group,
sim_id,
client,
proxy,
paused_due_to_minimize: false,
}
}
fn draw(&mut self, event_loop: &ActiveEventLoop) {
let window = &mut self.window;
let imgui = &mut self.imgui;
let mut context = imgui.context.lock().unwrap();
let mut new_size = None;
let now = Instant::now();
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) => {
if !self.window.minimized {
eprintln!("dropped frame: {e:?}");
}
return;
}
};
imgui
.platform
.prepare_frame(context.io_mut(), &window.window)
.expect("Failed to prepare frame");
let ui = context.new_frame();
let mut menu_height = 0.0;
ui.main_menu_bar(|| {
menu_height = ui.window_size()[1];
ui.menu("ROM", || {
if ui.menu_item("Open ROM") {
let rom = native_dialog::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.show_open_single_file()
.unwrap();
if let Some(path) = rom {
self.client
.send_command(EmulatorCommand::LoadGame(self.sim_id, path));
}
}
if ui.menu_item("Quit") {
event_loop.exit();
}
});
ui.menu("Emulation", || {
let has_game = self.client.has_game(self.sim_id);
if self.client.is_running(self.sim_id) {
if ui.menu_item_config("Pause").enabled(has_game).build() {
self.client.send_command(EmulatorCommand::Pause);
}
} else if ui.menu_item_config("Resume").enabled(has_game).build() {
self.client.send_command(EmulatorCommand::Resume);
}
if ui.menu_item_config("Reset").enabled(has_game).build() {
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
}
});
ui.menu("Video", || {
let current_dims = window.logical_size();
for scale in 1..=4 {
let label = format!("x{scale}");
let dims = LogicalSize::new(384 * scale, 224 * scale + 20);
let selected = dims == current_dims;
if ui.menu_item_config(label).selected(selected).build() {
if let Some(size) = window.window.request_inner_size(dims) {
window.handle_resize(&size);
new_size = Some(size);
}
}
}
});
ui.menu("Audio", || {
let p1_enabled = self.client.is_audio_enabled(SimId::Player1);
let p2_enabled = self.client.is_audio_enabled(SimId::Player2);
if ui.menu_item_config("Player 1").selected(p1_enabled).build() {
self.client
.send_command(EmulatorCommand::SetAudioEnabled(!p1_enabled, p2_enabled));
}
if ui.menu_item_config("Player 2").selected(p2_enabled).build() {
self.client
.send_command(EmulatorCommand::SetAudioEnabled(p1_enabled, !p2_enabled));
}
});
ui.menu("Input", || {
if ui.menu_item("Bind Inputs") {
self.proxy.send_event(UserEvent::OpenInputWindow).unwrap();
}
});
ui.menu("Multiplayer", || {
if self.sim_id == SimId::Player1
&& !self.client.has_player_2()
&& ui.menu_item("Open Player 2")
{
self.client
.send_command(EmulatorCommand::StartSecondSim(None));
self.proxy.send_event(UserEvent::OpenPlayer2Window).unwrap();
}
if self.client.has_player_2() {
let linked = self.client.are_sims_linked();
if linked && ui.menu_item("Unlink") {
self.client.send_command(EmulatorCommand::Unlink);
}
if !linked && ui.menu_item("Link") {
self.client.send_command(EmulatorCommand::Link);
}
}
});
});
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,
});
// Draw the game
rpass.set_pipeline(&self.pipeline);
let window_width = window.surface_desc.width as f32;
let window_height = window.surface_desc.height as f32;
let menu_height = menu_height * window.hidpi_factor as f32;
let ((x, y), (width, height)) =
compute_game_bounds(window_width, window_height, menu_height);
rpass.set_viewport(x, y, width, height, 0.0, 1.0);
rpass.set_bind_group(0, &self.bind_group, &[]);
rpass.draw(0..6, 0..1);
// Draw the menu on top of the game
rpass.set_viewport(0.0, 0.0, window_width, window_height, 0.0, 1.0);
imgui
.renderer
.render(context.render(), &window.queue, &window.device, &mut rpass)
.expect("Rendering failed");
drop(rpass);
if let Some(size) = new_size {
imgui.platform.handle_event::<UserEvent>(
context.io_mut(),
&window.window,
&Event::WindowEvent {
window_id: window.window.id(),
event: WindowEvent::Resized(size),
},
);
}
window.queue.submit(Some(encoder.finish()));
frame.present();
}
}
impl AppWindow for GameWindow {
fn id(&self) -> WindowId {
self.window.window.id()
}
fn handle_event(&mut self, event_loop: &ActiveEventLoop, event: &Event<UserEvent>) {
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(size) => {
self.window.handle_resize(size);
if self.window.minimized {
if self.client.is_running(self.sim_id) {
self.client.send_command(EmulatorCommand::Pause);
self.paused_due_to_minimize = true;
}
} else if self.paused_due_to_minimize {
self.client.send_command(EmulatorCommand::Resume);
self.paused_due_to_minimize = false;
}
}
WindowEvent::CloseRequested => {
if self.sim_id == SimId::Player2 {
self.client.send_command(EmulatorCommand::StopSecondSim);
self.proxy.send_event(UserEvent::Close(self.id())).unwrap();
} else {
event_loop.exit();
}
}
WindowEvent::RedrawRequested => self.draw(event_loop),
_ => (),
},
Event::AboutToWait => {
self.window.window.request_redraw();
}
_ => (),
}
let window = &self.window;
let imgui = &mut self.imgui;
let mut context = imgui.context.lock().unwrap();
imgui
.platform
.handle_event(context.io_mut(), &window.window, event);
}
}
fn compute_game_bounds(
window_width: f32,
window_height: f32,
menu_height: f32,
) -> ((f32, f32), (f32, f32)) {
let available_width = window_width;
let available_height = window_height - menu_height;
let width = available_width.min(available_height * 384.0 / 224.0);
let height = available_height.min(available_width * 224.0 / 384.0);
let x = (available_width - width) / 2.0;
let y = menu_height + (available_height - height) / 2.0;
((x, y), (width, height))
}
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
struct Colors {
left: [f32; 4],
right: [f32; 4],
}

220
src/app/input.rs Normal file
View File

@ -0,0 +1,220 @@
use std::time::Instant;
use winit::{
dpi::LogicalSize,
event::{Event, KeyEvent, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
};
use crate::{
emulator::{SimId, VBKey},
input::MappingProvider,
};
use super::{
common::{ImguiState, UiExt, WindowState, WindowStateBuilder},
AppWindow, UserEvent,
};
pub struct InputWindow {
window: WindowState,
imgui: ImguiState,
mappings: MappingProvider,
proxy: EventLoopProxy<UserEvent>,
now_binding: Option<(SimId, VBKey)>,
}
const KEY_NAMES: [(VBKey, &str); 14] = [
(VBKey::LU, "Up"),
(VBKey::LD, "Down"),
(VBKey::LL, "Left"),
(VBKey::LR, "Right"),
(VBKey::SEL, "Select"),
(VBKey::STA, "Start"),
(VBKey::B, "B"),
(VBKey::A, "A"),
(VBKey::LT, "L-Trigger"),
(VBKey::RT, "R-Trigger"),
(VBKey::RU, "R-Up"),
(VBKey::RD, "R-Down"),
(VBKey::RL, "R-Left"),
(VBKey::RR, "R-Right"),
];
impl InputWindow {
pub fn new(
event_loop: &ActiveEventLoop,
mappings: MappingProvider,
proxy: EventLoopProxy<UserEvent>,
) -> Self {
let window = WindowStateBuilder::new(event_loop)
.with_title("Bind Inputs")
.with_inner_size(LogicalSize::new(600, 400))
.build();
let imgui = ImguiState::new(&window);
Self {
window,
imgui,
mappings,
now_binding: None,
proxy,
}
}
fn draw(&mut self) {
let window = &mut self.window;
let imgui = &mut self.imgui;
let mut context = imgui.context.lock().unwrap();
let now = Instant::now();
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) => {
if !self.window.minimized {
eprintln!("dropped frame: {e:?}");
}
return;
}
};
imgui
.platform
.prepare_frame(context.io_mut(), &window.window)
.expect("Failed to prepare frame");
let ui = context.new_frame();
let mut render_key_bindings = |sim_id: SimId| {
let mappings = self.mappings.for_sim(sim_id);
if let Some(table) = ui.begin_table("controls", 2) {
let binding_names = {
let mapping = mappings.read().unwrap();
mapping.keyboard_mapping_names()
};
ui.table_next_row();
for (key, name) in KEY_NAMES {
let binding = binding_names.get(&key).map(|s| s.as_str());
ui.table_next_column();
let [space, _] = ui.content_region_avail();
ui.group(|| {
ui.right_align_text(name, space * 0.20);
ui.same_line();
let label_text = if self.now_binding == Some((sim_id, key)) {
"Press any input"
} else {
binding.unwrap_or("")
};
let label = format!("{}##{}", label_text, name);
if ui.button_with_size(label, [space * 0.60, 0.0]) {
self.now_binding = Some((sim_id, key));
}
});
ui.same_line();
if ui.button(format!("Clear##{name}")) {
let mut mapping = mappings.write().unwrap();
mapping.clear_keyboard_mappings(key);
}
}
table.end();
}
};
if let Some(window) = ui.fullscreen_window() {
if let Some(tabs) = ui.tab_bar("tabs") {
if let Some(tab) = ui.tab_item("Player 1") {
render_key_bindings(SimId::Player1);
tab.end();
}
if let Some(tab) = ui.tab_item("Player 2") {
render_key_bindings(SimId::Player2);
tab.end();
}
tabs.end();
}
window.end();
}
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,
});
// Draw the game
imgui
.renderer
.render(context.render(), &window.queue, &window.device, &mut rpass)
.expect("Rendering failed");
drop(rpass);
window.queue.submit(Some(encoder.finish()));
frame.present();
}
fn try_bind_key(&mut self, event: &KeyEvent) {
if !event.state.is_pressed() {
return;
}
let Some((sim_id, vb)) = self.now_binding.take() else {
return;
};
let mut mappings = self.mappings.for_sim(sim_id).write().unwrap();
mappings.add_keyboard_mapping(vb, event.physical_key);
}
}
impl AppWindow for InputWindow {
fn id(&self) -> winit::window::WindowId {
self.window.window.id()
}
fn handle_event(&mut self, _: &ActiveEventLoop, event: &Event<UserEvent>) {
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(size) => self.window.handle_resize(size),
WindowEvent::CloseRequested => {
self.proxy.send_event(UserEvent::Close(self.id())).unwrap()
}
WindowEvent::KeyboardInput { event, .. } => self.try_bind_key(event),
WindowEvent::RedrawRequested => self.draw(),
_ => (),
},
Event::AboutToWait => {
self.window.window.request_redraw();
}
_ => (),
}
let window = &self.window;
let imgui = &mut self.imgui;
let mut context = imgui.context.lock().unwrap();
imgui
.platform
.handle_event(context.io_mut(), &window.window, event);
}
}

View File

@ -1,7 +1,7 @@
use std::{path::PathBuf, process};
use anyhow::Result;
use app::Application;
use app::App;
use clap::Parser;
use emulator::EmulatorBuilder;
use thread_priority::{ThreadBuilder, ThreadPriority};
@ -13,7 +13,6 @@ mod controller;
mod emulator;
mod graphics;
mod input;
mod window;
#[derive(Parser)]
struct Args {
@ -57,6 +56,6 @@ fn main() -> Result<()> {
let event_loop = EventLoop::with_user_event().build().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
let proxy = event_loop.create_proxy();
event_loop.run_app(&mut Application::new(client, proxy))?;
event_loop.run_app(&mut App::new(client, proxy))?;
Ok(())
}

View File

@ -1,21 +0,0 @@
use egui::{Context, ViewportBuilder, ViewportId};
pub use game::GameWindow;
pub use input::InputWindow;
use winit::event::KeyEvent;
mod game;
mod game_screen;
mod input;
pub trait AppWindow {
fn viewport_id(&self) -> ViewportId;
fn initial_viewport(&self) -> ViewportBuilder;
fn show(&mut self, ctx: &Context);
fn on_init(&mut self, render_state: &egui_wgpu::RenderState) {
let _ = render_state;
}
fn on_destroy(&mut self) {}
fn handle_key_event(&mut self, event: &KeyEvent) {
let _ = event;
}
}

View File

@ -1,181 +0,0 @@
use crate::{
app::UserEvent,
emulator::{EmulatorClient, EmulatorCommand, SimId},
};
use egui::{
menu, Button, CentralPanel, Color32, Context, Frame, Response, TopBottomPanel, Ui,
ViewportBuilder, ViewportCommand, ViewportId, WidgetText,
};
use winit::event_loop::EventLoopProxy;
use super::{game_screen::GameScreen, AppWindow};
pub struct GameWindow {
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
sim_id: SimId,
screen: Option<GameScreen>,
}
impl GameWindow {
pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>, sim_id: SimId) -> Self {
Self {
client,
proxy,
sim_id,
screen: None,
}
}
fn show_menu(&mut self, ctx: &Context, ui: &mut Ui) {
ui.menu_button("ROM", |ui| {
if ui.button("Open ROM").clicked() {
let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.pick_file();
if let Some(path) = rom {
self.client
.send_command(EmulatorCommand::LoadGame(SimId::Player1, path));
}
ui.close_menu();
}
if ui.button("Quit").clicked() {
ctx.send_viewport_cmd(ViewportCommand::Close);
}
});
ui.menu_button("Emulation", |ui| {
let has_game = self.client.has_game(self.sim_id);
if self.client.is_running(self.sim_id) {
if ui.add_enabled(has_game, Button::new("Pause")).clicked() {
self.client.send_command(EmulatorCommand::Pause);
ui.close_menu();
}
} else if ui.add_enabled(has_game, Button::new("Resume")).clicked() {
self.client.send_command(EmulatorCommand::Resume);
ui.close_menu();
}
if ui.add_enabled(has_game, Button::new("Reset")).clicked() {
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
ui.close_menu();
}
});
ui.menu_button("Video", |ui| {
let current_dims = ctx.input(|i| i.viewport().inner_rect.unwrap());
let current_dims = current_dims.max - current_dims.min;
for scale in 1..=4 {
let label = format!("x{scale}");
let scale = scale as f32;
let dims = (384.0 * scale, 224.0 * scale + 22.0).into();
if ui
.selectable_button((current_dims - dims).length() < 1.0, label)
.clicked()
{
ctx.send_viewport_cmd(ViewportCommand::InnerSize(dims));
ui.close_menu();
}
}
});
ui.menu_button("Audio", |ui| {
let p1_enabled = self.client.is_audio_enabled(SimId::Player1);
let p2_enabled = self.client.is_audio_enabled(SimId::Player2);
if ui.selectable_button(p1_enabled, "Player 1").clicked() {
self.client
.send_command(EmulatorCommand::SetAudioEnabled(!p1_enabled, p2_enabled));
ui.close_menu();
}
if ui.selectable_button(p2_enabled, "Player 2").clicked() {
self.client
.send_command(EmulatorCommand::SetAudioEnabled(p1_enabled, !p2_enabled));
ui.close_menu();
}
});
ui.menu_button("Input", |ui| {
if ui.button("Bind Inputs").clicked() {
self.proxy.send_event(UserEvent::OpenInput).unwrap();
ui.close_menu();
}
});
ui.menu_button("Multiplayer", |ui| {
if self.sim_id == SimId::Player1
&& !self.client.has_player_2()
&& ui.button("Open Player 2").clicked()
{
self.client
.send_command(EmulatorCommand::StartSecondSim(None));
self.proxy.send_event(UserEvent::OpenPlayer2).unwrap();
ui.close_menu();
}
if self.client.has_player_2() {
let linked = self.client.are_sims_linked();
if linked && ui.button("Unlink").clicked() {
self.client.send_command(EmulatorCommand::Unlink);
ui.close_menu();
}
if !linked && ui.button("Link").clicked() {
self.client.send_command(EmulatorCommand::Link);
ui.close_menu();
}
}
});
}
}
impl AppWindow for GameWindow {
fn viewport_id(&self) -> ViewportId {
match self.sim_id {
SimId::Player1 => ViewportId::ROOT,
SimId::Player2 => ViewportId::from_hash_of("Player2"),
}
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Shrooms VB")
.with_inner_size((384.0, 246.0))
}
fn show(&mut self, ctx: &Context) {
TopBottomPanel::top("menubar")
.exact_height(22.0)
.show(ctx, |ui| {
menu::bar(ui, |ui| {
self.show_menu(ctx, ui);
});
});
let frame = Frame::central_panel(&ctx.style()).fill(Color32::BLACK);
CentralPanel::default().frame(frame).show(ctx, |ui| {
if let Some(screen) = self.screen.as_ref() {
ui.add(screen);
}
});
}
fn on_init(&mut self, render_state: &egui_wgpu::RenderState) {
let (screen, sink) = GameScreen::init(render_state);
self.client
.send_command(EmulatorCommand::SetRenderer(self.sim_id, sink));
self.screen = Some(screen)
}
fn on_destroy(&mut self) {
if self.sim_id == SimId::Player2 {
self.client.send_command(EmulatorCommand::StopSecondSim);
}
}
}
trait UiExt {
fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response;
}
impl UiExt for Ui {
fn selectable_button(&mut self, selected: bool, text: impl Into<WidgetText>) -> Response {
self.style_mut().visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
self.style_mut().visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT;
self.style_mut().visuals.widgets.active.bg_fill = Color32::TRANSPARENT;
let mut selected = selected;
self.checkbox(&mut selected, text)
}
}

View File

@ -1,206 +0,0 @@
use std::sync::Arc;
use egui::Widget;
use wgpu::{util::DeviceExt as _, BindGroup, BindGroupLayout, RenderPipeline};
use crate::graphics::TextureSink;
pub struct GameScreen {
bind_group: Arc<BindGroup>,
}
impl GameScreen {
fn init_pipeline(render_state: &egui_wgpu::RenderState) {
let device = &render_state.device;
let 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: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
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: &[&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::Bgra8Unorm,
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,
});
render_state
.renderer
.write()
.callback_resources
.insert(SharedGameScreenResources {
pipeline: render_pipeline,
bind_group_layout,
});
}
pub fn init(render_state: &egui_wgpu::RenderState) -> (Self, TextureSink) {
Self::init_pipeline(render_state);
let device = &render_state.device;
let queue = &render_state.queue;
let (sink, texture_view) = TextureSink::new(device, queue.clone());
let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default());
let colors = Colors {
left: [1.0, 0.0, 0.0, 1.0],
right: [0.0, 0.7734375, 0.9375, 1.0],
};
let color_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("colors"),
contents: bytemuck::bytes_of(&colors),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let renderer = render_state.renderer.read();
let resources: &SharedGameScreenResources = renderer.callback_resources.get().unwrap();
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("bind group"),
layout: &resources.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: color_buf.as_entire_binding(),
},
],
});
(
Self {
bind_group: Arc::new(bind_group),
},
sink,
)
}
}
impl Widget for &GameScreen {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
let response = ui.allocate_rect(ui.clip_rect(), egui::Sense::hover());
let callback = egui_wgpu::Callback::new_paint_callback(
response.rect,
GameScreenCallback {
bind_group: self.bind_group.clone(),
},
);
ui.painter().add(callback);
response
}
}
struct GameScreenCallback {
bind_group: Arc<BindGroup>,
}
impl egui_wgpu::CallbackTrait for GameScreenCallback {
fn paint(
&self,
info: egui::PaintCallbackInfo,
render_pass: &mut wgpu::RenderPass<'static>,
callback_resources: &egui_wgpu::CallbackResources,
) {
let resources: &SharedGameScreenResources = callback_resources.get().unwrap();
let viewport = info.viewport_in_pixels();
let left = viewport.left_px as f32;
let top = viewport.top_px as f32;
let width = viewport.width_px as f32;
let height = viewport.height_px as f32;
let aspect_ratio = 384.0 / 224.0;
let w = width.min(height * aspect_ratio);
let h = height.min(width / aspect_ratio);
let x = left + (width - w) / 2.0;
let y = top + (height - h) / 2.0;
render_pass.set_pipeline(&resources.pipeline);
render_pass.set_bind_group(0, &self.bind_group, &[]);
render_pass.set_viewport(x, y, w, h, 0.0, 1.0);
render_pass.draw(0..6, 0..1);
}
}
struct SharedGameScreenResources {
pipeline: RenderPipeline,
bind_group_layout: BindGroupLayout,
}
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
struct Colors {
left: [f32; 4],
right: [f32; 4],
}

View File

@ -1,133 +0,0 @@
use egui::{
Button, CentralPanel, Context, Label, Layout, TopBottomPanel, Ui, ViewportBuilder, ViewportId,
};
use egui_extras::{Column, TableBuilder};
use crate::{
emulator::{SimId, VBKey},
input::MappingProvider,
};
use super::AppWindow;
pub struct InputWindow {
mappings: MappingProvider,
now_binding: Option<(SimId, VBKey)>,
active_tab: InputTab,
}
const KEY_NAMES: [(VBKey, &str); 14] = [
(VBKey::LU, "Up"),
(VBKey::LD, "Down"),
(VBKey::LL, "Left"),
(VBKey::LR, "Right"),
(VBKey::SEL, "Select"),
(VBKey::STA, "Start"),
(VBKey::B, "B"),
(VBKey::A, "A"),
(VBKey::LT, "L-Trigger"),
(VBKey::RT, "R-Trigger"),
(VBKey::RU, "R-Up"),
(VBKey::RD, "R-Down"),
(VBKey::RL, "R-Left"),
(VBKey::RR, "R-Right"),
];
impl InputWindow {
pub fn new(mappings: MappingProvider) -> Self {
Self {
mappings,
now_binding: None,
active_tab: InputTab::Player1,
}
}
fn render_key_bindings(&mut self, ui: &mut Ui, sim_id: SimId) {
let mappings = self.mappings.for_sim(sim_id);
let binding_names = {
let mapping = mappings.read().unwrap();
mapping.keyboard_mapping_names()
};
TableBuilder::new(ui)
.column(Column::remainder())
.column(Column::remainder())
.cell_layout(Layout::left_to_right(egui::Align::Center))
.body(|mut body| {
for keys in KEY_NAMES.chunks_exact(2) {
body.row(20.0, |mut row| {
for (key, name) in keys {
let binding = binding_names.get(key).map(|s| s.as_str());
row.col(|ui| {
let size = ui.available_size_before_wrap();
let width = size.x;
let height = size.y;
ui.add_sized((width * 0.2, height), Label::new(*name));
let label_text = if self.now_binding == Some((sim_id, *key)) {
"Press any input"
} else {
binding.unwrap_or("")
};
if ui
.add_sized((width * 0.6, height), Button::new(label_text))
.clicked()
{
self.now_binding = Some((sim_id, *key))
}
if ui
.add_sized(ui.available_size(), Button::new("Clear"))
.clicked()
{
let mut mapping = mappings.write().unwrap();
mapping.clear_keyboard_mappings(*key);
}
});
}
});
}
});
}
}
impl AppWindow for InputWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of("input")
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Bind Inputs")
.with_inner_size((600.0, 400.0))
}
fn show(&mut self, ctx: &Context) {
TopBottomPanel::top("options").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut self.active_tab, InputTab::Player1, "Player 1");
ui.selectable_value(&mut self.active_tab, InputTab::Player2, "Player 2");
});
});
CentralPanel::default().show(ctx, |ui| {
match self.active_tab {
InputTab::Player1 => self.render_key_bindings(ui, SimId::Player1),
InputTab::Player2 => self.render_key_bindings(ui, SimId::Player2),
};
});
}
fn handle_key_event(&mut self, event: &winit::event::KeyEvent) {
if !event.state.is_pressed() {
return;
}
let Some((sim_id, vb)) = self.now_binding.take() else {
return;
};
let mut mappings = self.mappings.for_sim(sim_id).write().unwrap();
mappings.add_keyboard_mapping(vb, event.physical_key);
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum InputTab {
Player1,
Player2,
}