Macroquad port (#10)

I ported the gui to the macroquad crate.
This has the following advantages:

    We can easily draw text. This means we can show the number of instructions on screen.
    The filter mode can be set to linear for a different look.
    The dependencies are a lot cleaner now. Just depending on the latest stable version of macroquad makes it a lot more likely that the app builds properly on different platforms (and will continue to do so in the future).

I also use the gilrs crate directly for the gamepad input.
This commit is contained in:
JanNeuendorf 2025-01-05 16:36:51 +01:00 committed by GitHub
parent 24c192f129
commit 6abfcbc811
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 353 additions and 2437 deletions

2288
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "svc16"
version = "0.7.3"
version = "0.8.0"
edition = "2021"
authors = ["Jan Neuendorf"]
description = "An emulator for a simple virtual computer"
@ -11,13 +11,9 @@ anyhow = "1.0.93"
clap = { version = "4.5.21", features = ["derive"] }
flate2 = "1.0.35"
gilrs = { version = "0.11.0",optional=true}
# There seems to be some incompatibility with the latest crates.io version of pixels?
pixels = { git = "https://github.com/parasyte/pixels.git", rev = "d4df286"}
macroquad = "0.4.13"
thiserror = "2.0.3"
winit = "0.29.15"
winit-input-map = {version="0.4.1",optional=true}
winit_input_helper = "0.16.0"
[features]
default=[]
gamepad = ["gilrs","winit-input-map"]
gamepad = ["gilrs"]

View File

@ -7,7 +7,7 @@ pub struct Cli {
pub program: String,
#[arg(short, long, default_value = "1", help = "Set initial window scaling")]
pub scaling: u32,
pub scaling: i32,
#[arg(
short,
@ -27,14 +27,14 @@ pub struct Cli {
short,
long,
default_value_t = false,
help = "Output performance metrics"
help = "Show performance metrics"
)]
pub verbose: bool,
#[arg(
short,
long,
default_value = "3000000",
help = "Change the maximum instructions per frame"
short,
default_value_t = false,
help = "Use linear filtering (instead of pixel-perfect) this enables fractional scaling"
)]
pub max_ipf: usize,
pub linear_filtering: bool,
}

View File

@ -1,138 +1,148 @@
mod cli;
mod engine;
mod ui;
mod utils;
use anyhow::{anyhow, Result};
#[allow(unused)]
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use cli::Cli;
use engine::Engine;
#[cfg(feature = "gamepad")]
use gilrs::Gilrs;
use pixels::{Pixels, SurfaceTexture};
use macroquad::prelude::*;
use std::time::{Duration, Instant};
use ui::Layout;
use utils::*;
use winit::dpi::LogicalSize;
use winit::event_loop::EventLoop;
use winit::keyboard::{Key, KeyCode};
use winit::window::WindowBuilder;
use winit_input_helper::WinitInputHelper;
const RES: usize = 256;
const MAX_IPF: usize = 3000000;
const FRAMETIME: Duration = Duration::from_nanos((1000000000. / 30.) as u64);
fn main() -> Result<()> {
fn window_conf() -> Conf {
let cli = Cli::parse();
#[cfg(feature = "gamepad")]
let mut girls = Gilrs::new().expect("Could not read gamepad inputs.");
if cli.fullscreen {}
Conf {
window_title: "SVC16".to_owned(),
window_width: 256 * cli.scaling,
window_height: 256 * cli.scaling,
fullscreen: cli.fullscreen,
..Default::default()
}
}
#[macroquad::main(window_conf)]
async fn main() -> Result<()> {
let mut cli = Cli::parse();
let mut buffer = [Color::from_rgba(255, 255, 255, 255); 256 * 256];
let mut image = Image::gen_image_color(256, 256, Color::from_rgba(0, 0, 0, 255));
let texture = Texture2D::from_image(&image);
if cli.linear_filtering {
texture.set_filter(FilterMode::Linear);
} else {
texture.set_filter(FilterMode::Nearest);
}
let mut raw_buffer = [0 as u16; 256 * 256];
let initial_state = read_u16s_from_file(&cli.program)?;
// The initial state is cloned, so we keep it around for a restart.
let mut engine = Engine::new(initial_state.clone());
let event_loop = EventLoop::new()?;
let mut input = WinitInputHelper::new();
#[cfg(feature = "gamepad")]
let mut gamepad = build_gamepad_map();
if cli.scaling < 1 {
return Err(anyhow!("The minimal scaling factor is 1"));
}
let window = {
let size = LogicalSize::new(
(RES as u32 * cli.scaling) as f64,
(RES as u32 * cli.scaling) as f64,
);
let min_size = LogicalSize::new((RES) as f64, (RES) as f64);
WindowBuilder::new()
.with_title("SVC16")
.with_inner_size(size)
.with_min_inner_size(min_size)
.build(&event_loop)?
};
window.set_cursor_visible(cli.cursor);
if cli.fullscreen {
window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
}
let mut pixels = {
let window_size = window.inner_size();
let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window);
Pixels::new(RES as u32, RES as u32, surface_texture)?
};
let mut raw_buffer = [0 as u16; engine::MEMSIZE];
let mut paused = false;
let mut ipf = 0;
event_loop.run(|event, elwt| {
#[cfg(feature = "gamepad")]
let mut gilrs = match Gilrs::new() {
Ok(g) => g,
_ => return Err(anyhow!("Gamepad could not be loaded")),
};
loop {
let start_time = Instant::now();
if input.update(&event) {
if input.key_pressed(KeyCode::Escape) || input.close_requested() {
elwt.exit();
return;
}
if input.key_pressed_logical(Key::Character("p")) {
paused = !paused;
if paused {
window.set_title("SVC16 (paused)");
} else {
window.set_title("SVC16");
}
}
if input.key_pressed_logical(Key::Character("r")) {
engine = Engine::new(initial_state.clone());
paused = false;
}
if is_key_pressed(KeyCode::Escape) {
break;
}
if is_key_pressed(KeyCode::P) {
paused = !paused;
}
if is_key_pressed(KeyCode::R) {
engine = Engine::new(initial_state.clone());
paused = false;
}
if is_key_pressed(KeyCode::V) {
cli.verbose = !cli.verbose;
}
if is_key_pressed(KeyCode::C) {
cli.cursor = !cli.cursor;
}
if let Some(size) = input.window_resized() {
if let Err(_) = pixels.resize_surface(size.width, size.height) {
handle_event_loop_error(&elwt, "Resize error");
return;
}
}
let mut ipf = 0;
let engine_start = Instant::now();
while !engine.wants_to_sync() && ipf <= cli.max_ipf && !paused {
match engine.step() {
Err(e) => {
handle_event_loop_error(
&elwt,
format!("{} (after {} instructions)", e, ipf),
);
return;
}
_ => {}
}
let layout = Layout::generate(cli.linear_filtering);
if !paused {
ipf = 0;
while !engine.wants_to_sync() && ipf <= MAX_IPF {
engine.step()?;
ipf += 1;
}
let engine_elapsed = engine_start.elapsed();
#[cfg(feature = "gamepad")]
while let Some(event) = gilrs.next_event() {
gilrs.update(&event);
}
#[cfg(not(feature = "gamepad"))]
let (c1, c2) = get_input_code(&input, &pixels);
#[cfg(feature = "gamepad")]
gamepad.update_with_gilrs(&mut girls);
#[cfg(feature = "gamepad")]
let (c1, c2) = get_input_code_gamepad(&input, &gamepad, &pixels);
engine.perform_sync(c1, c2, &mut raw_buffer);
update_image_buffer(pixels.frame_mut(), &raw_buffer);
let (mpos, keycode) = get_input_code_no_gamepad(&layout);
let elapsed = start_time.elapsed();
if cli.verbose {
println!(
"Instructions: {} Frametime: {}ms (Engine only: {}ms)",
ipf,
elapsed.as_millis(),
engine_elapsed.as_millis()
);
}
if elapsed < FRAMETIME {
std::thread::sleep(FRAMETIME - elapsed);
}
window.request_redraw();
match pixels.render() {
Err(_) => {
handle_event_loop_error(&elwt, "Rendering error");
return;
}
_ => {}
};
#[cfg(feature = "gamepad")]
let (mpos, keycode) = get_input_code_gamepad(&layout, &gilrs);
engine.perform_sync(mpos, keycode, &mut raw_buffer);
update_image_buffer(&mut buffer, &raw_buffer);
image.update(&buffer);
texture.update(&image);
}
})?;
clear_background(BLACK);
if layout.cursor_in_window() {
show_mouse(cli.cursor);
} else {
show_mouse(true);
}
draw_texture_ex(
&texture,
layout.x,
layout.y,
WHITE,
DrawTextureParams {
dest_size: Some(vec2(layout.size, layout.size)),
..Default::default()
},
);
if cli.verbose {
draw_rectangle(
layout.rect_x,
layout.rect_y,
0.25 * layout.size,
layout.font_size,
Color::from_rgba(0, 0, 0, 200),
);
draw_text(
&format!("{}", ipf),
layout.font_x,
layout.font_y,
layout.font_size,
LIME,
);
}
// Wait for the next frame
let elapsed = start_time.elapsed();
if elapsed < FRAMETIME {
std::thread::sleep(FRAMETIME - elapsed);
} else {
if cli.verbose {
println!("Frame was not processed in time");
}
}
next_frame().await;
}
Ok(())
}

47
src/ui.rs Normal file
View File

@ -0,0 +1,47 @@
use macroquad::prelude::*;
pub struct Layout {
pub x: f32,
pub y: f32,
pub size: f32,
pub font_y: f32,
pub font_x: f32,
pub rect_x: f32,
pub rect_y: f32,
pub font_size: f32,
}
impl Layout {
pub fn generate(linear: bool) -> Self {
let (width, height) = (screen_width(), screen_height());
let minsize = width.min(height);
let image_size = match linear {
false => ((minsize / 256.).floor() * 256.).max(256.),
true => minsize.max(256.),
};
let x = (0. as f32).max((width - image_size) / 2.);
let y = (0. as f32).max((height - image_size) / 2.);
let font_y = y + image_size / 15.;
Self {
x,
y,
size: image_size,
font_y,
font_x: x + 0.01 * image_size,
rect_x: x + 0.005 * image_size,
rect_y: y + 0.01 * image_size,
font_size: image_size / 15.,
}
}
pub fn clamp_mouse(&self) -> (f32, f32) {
let (raw_x, raw_y) = mouse_position();
let clamped_x = (raw_x.clamp(self.x, self.x + self.size) - self.x) / self.size * 255.;
let clamped_y = (raw_y.clamp(self.y, self.y + self.size) - self.y) / self.size * 255.;
(clamped_x, clamped_y)
}
pub fn cursor_in_window(&self) -> bool {
let mp = mouse_position();
mp.0 >= self.x
&& mp.0 < (self.x + self.size)
&& mp.1 >= self.y
&& mp.1 < (self.y + self.size)
}
}

View File

@ -1,61 +1,14 @@
use std::io::Read;
use crate::RES;
#[allow(unused)]
use crate::ui::Layout;
use anyhow::Result;
use flate2::read::GzDecoder;
use pixels::Pixels;
use macroquad::color::Color;
use macroquad::prelude::*;
use std::fs::File;
use winit::{
event::MouseButton,
event_loop::EventLoopWindowTarget,
keyboard::{Key, KeyCode},
};
use winit_input_helper::WinitInputHelper;
use std::io::Read;
const RES: usize = 256;
#[cfg(feature = "gamepad")]
use winit_input_map::{input_map, GamepadAxis, GamepadButton, InputCode, InputMap};
#[cfg(feature = "gamepad")]
#[derive(Debug, std::hash::Hash, PartialEq, Eq, Clone, Copy)]
pub enum NesInput {
Up,
Down,
Left,
Right,
A,
B,
Start,
Select,
}
#[cfg(feature = "gamepad")]
pub fn build_gamepad_map() -> InputMap<NesInput> {
input_map!(
(NesInput::A, GamepadButton::East),
(NesInput::B, GamepadButton::South),
(NesInput::Select, GamepadButton::Select),
(NesInput::Start, GamepadButton::Start),
(
NesInput::Up,
GamepadButton::DPadUp,
InputCode::gamepad_axis_pos(GamepadAxis::LeftStickY)
),
(
NesInput::Down,
GamepadButton::DPadDown,
InputCode::gamepad_axis_neg(GamepadAxis::LeftStickY)
),
(
NesInput::Left,
GamepadButton::DPadLeft,
InputCode::gamepad_axis_neg(GamepadAxis::LeftStickX)
),
(
NesInput::Right,
GamepadButton::DPadRight,
InputCode::gamepad_axis_pos(GamepadAxis::LeftStickX)
)
)
}
use gilrs::{Axis, Button, Gilrs};
pub fn read_u16s_from_file(file_path: &str) -> Result<Vec<u16>> {
let mut file = File::open(file_path)?;
@ -86,110 +39,116 @@ fn rgb565_to_argb(rgb565: u16) -> (u8, u8, u8) {
(r, g, b)
}
pub fn update_image_buffer(imbuff: &mut [u8], screen: &[u16; RES * RES]) {
pub fn update_image_buffer(imbuff: &mut [Color; RES * RES], screen: &[u16; RES * RES]) {
for i in 0..RES * RES {
let col = rgb565_to_argb(screen[i]);
*imbuff.get_mut(4 * i).expect("Error with image buffer") = col.0;
*imbuff.get_mut(4 * i + 1).expect("Error with image buffer") = col.1;
*imbuff.get_mut(4 * i + 2).expect("Error with image buffer") = col.2;
*imbuff.get_mut(4 * i + 3).expect("Error with image buffer") = 255;
imbuff[i] = Color {
r: col.0 as f32 / 255.,
g: col.1 as f32 / 255.,
b: col.2 as f32 / 255.,
a: 1.,
}
}
}
#[cfg(feature = "gamepad")]
pub fn get_input_code_gamepad(
input: &WinitInputHelper,
gamepad: &InputMap<NesInput>,
pxls: &Pixels,
) -> (u16, u16) {
let raw_mp = input.cursor().unwrap_or((0., 0.));
let mp = match pxls.window_pos_to_pixel(raw_mp) {
Ok(p) => p,
Err(ev) => pxls.clamp_pixel_pos(ev),
};
let pos_code = (mp.1 as u16 * 256) + mp.0 as u16;
pub fn get_input_code_gamepad(layout: &Layout, gilrs: &Gilrs) -> (u16, u16) {
#[cfg(not(feature = "gamepad"))]
return get_input_code_no_gamepad();
let mut key_code = 0_u16;
if input.key_held(KeyCode::Space)
|| input.mouse_held(MouseButton::Left)
|| gamepad.pressing(NesInput::A)
let mp = layout.clamp_mouse();
let pos_code = (mp.1 as u16 * 256) + mp.0 as u16;
let Some(gamepad) = gilrs.gamepads().next().map(|t| t.1) else {
return get_input_code_no_gamepad(layout);
};
let tol = 0.5;
let axis_horizontal = gamepad
.axis_data(Axis::LeftStickX)
.map(|a| a.value())
.unwrap_or(0.);
let axis_vertical = gamepad
.axis_data(Axis::LeftStickY)
.map(|a| a.value())
.unwrap_or(0.);
if is_key_down(KeyCode::Space)
|| is_mouse_button_down(MouseButton::Left)
|| gamepad.is_pressed(Button::East)
{
key_code += 1;
}
if input.key_held_logical(Key::Character("b"))
|| input.mouse_held(MouseButton::Right)
|| gamepad.pressing(NesInput::B)
if is_key_down(KeyCode::B)
|| is_mouse_button_down(MouseButton::Right)
|| gamepad.is_pressed(Button::South)
{
key_code += 2;
}
if input.key_held_logical(Key::Character("w"))
|| input.key_held(KeyCode::ArrowUp)
|| gamepad.pressing(NesInput::Up)
if is_key_down(KeyCode::W)
|| is_key_down(KeyCode::Up)
|| gamepad.is_pressed(Button::DPadUp)
|| axis_vertical > tol
{
key_code += 4;
key_code += 4
}
if input.key_held_logical(Key::Character("s"))
|| input.key_held(KeyCode::ArrowDown)
|| gamepad.pressing(NesInput::Down)
if is_key_down(KeyCode::S)
|| is_key_down(KeyCode::Down)
|| gamepad.is_pressed(Button::DPadDown)
|| axis_vertical < -tol
{
key_code += 8;
key_code += 8
}
if input.key_held_logical(Key::Character("a"))
|| input.key_held(KeyCode::ArrowLeft)
|| gamepad.pressing(NesInput::Left)
if is_key_down(KeyCode::A)
|| is_key_down(KeyCode::Left)
|| gamepad.is_pressed(Button::DPadLeft)
|| axis_horizontal < -tol
{
key_code += 16;
key_code += 16
}
if input.key_held_logical(Key::Character("d"))
|| input.key_held(KeyCode::ArrowRight)
|| gamepad.pressing(NesInput::Right)
if is_key_down(KeyCode::D)
|| is_key_down(KeyCode::Right)
|| gamepad.is_pressed(Button::DPadRight)
|| axis_horizontal > tol
{
key_code += 32;
key_code += 32
}
if input.key_held_logical(Key::Character("n")) || gamepad.pressing(NesInput::Select) {
key_code += 64;
if is_key_down(KeyCode::N) || gamepad.is_pressed(Button::Select) {
key_code += 64
}
if input.key_held_logical(Key::Character("m")) || gamepad.pressing(NesInput::Start) {
key_code += 128;
if is_key_down(KeyCode::M) || gamepad.is_pressed(Button::Start) {
key_code += 128
}
(pos_code, key_code)
}
pub fn handle_event_loop_error(handle: &EventLoopWindowTarget<()>, msg: impl AsRef<str>) {
eprintln!("{}", msg.as_ref());
handle.exit();
}
pub fn get_input_code_no_gamepad(layout: &Layout) -> (u16, u16) {
let mp = layout.clamp_mouse();
#[cfg(not(feature = "gamepad"))]
pub fn get_input_code(input: &WinitInputHelper, pxls: &Pixels) -> (u16, u16) {
let raw_mp = input.cursor().unwrap_or((0., 0.));
let mp = match pxls.window_pos_to_pixel(raw_mp) {
Ok(p) => p,
Err(ev) => pxls.clamp_pixel_pos(ev),
};
let pos_code = (mp.1 as u16 * 256) + mp.0 as u16;
let mut key_code = 0_u16;
if input.key_held(KeyCode::Space) || input.mouse_held(MouseButton::Left) {
if is_key_down(KeyCode::Space) || is_mouse_button_down(MouseButton::Left) {
key_code += 1;
}
if input.key_held_logical(Key::Character("b")) || input.mouse_held(MouseButton::Right) {
if is_key_down(KeyCode::B) || is_mouse_button_down(MouseButton::Right) {
key_code += 2;
}
if input.key_held_logical(Key::Character("w")) || input.key_held(KeyCode::ArrowUp) {
key_code += 4;
if is_key_down(KeyCode::W) || is_key_down(KeyCode::Up) {
key_code += 4
}
if input.key_held_logical(Key::Character("s")) || input.key_held(KeyCode::ArrowDown) {
key_code += 8;
if is_key_down(KeyCode::S) || is_key_down(KeyCode::Down) {
key_code += 8
}
if input.key_held_logical(Key::Character("a")) || input.key_held(KeyCode::ArrowLeft) {
key_code += 16;
if is_key_down(KeyCode::A) || is_key_down(KeyCode::Left) {
key_code += 16
}
if input.key_held_logical(Key::Character("d")) || input.key_held(KeyCode::ArrowRight) {
key_code += 32;
if is_key_down(KeyCode::D) || is_key_down(KeyCode::Right) {
key_code += 32
}
if input.key_held_logical(Key::Character("n")) {
key_code += 64;
if is_key_down(KeyCode::N) {
key_code += 64
}
if input.key_held_logical(Key::Character("m")) {
key_code += 128;
if is_key_down(KeyCode::M) {
key_code += 128
}
(pos_code, key_code)
}