Initial port to macroquad (WIP)

This commit is contained in:
JanNeuendorf 2025-01-04 20:27:52 +01:00
parent 24c192f129
commit 9ed60f1ff9
6 changed files with 228 additions and 2465 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,
@ -30,11 +30,4 @@ pub struct Cli {
help = "Output performance metrics"
)]
pub verbose: bool,
#[arg(
short,
long,
default_value = "3000000",
help = "Change the maximum instructions per frame"
)]
pub max_ipf: usize,
}

View File

@ -1,138 +1,94 @@
mod cli;
mod engine;
mod ui;
mod utils;
use anyhow::{anyhow, Result};
use anyhow::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.");
Conf {
window_title: "SVC16".to_owned(),
window_width: 512 * cli.scaling,
window_height: 512 * cli.scaling,
fullscreen: cli.fullscreen,
..Default::default()
}
}
#[macroquad::main(window_conf)]
async fn main() -> Result<()> {
let cli = Cli::parse();
show_mouse(cli.cursor);
let mut buffer = [Color::from_rgba(255, 255, 255, 255); 256 * 256];
let texture = Texture2D::from_image(&Image::gen_image_color(256, 256, BLACK));
texture.set_filter(FilterMode::Nearest);
let mut image = Image::gen_image_color(256, 256, Color::from_rgba(0, 0, 0, 255));
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;
event_loop.run(|event, elwt| {
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 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;
}
_ => {}
}
ipf += 1;
}
let engine_elapsed = engine_start.elapsed();
#[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 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;
}
_ => {}
};
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;
}
})?;
let mut ipf = 0;
let engine_start = Instant::now();
while !engine.wants_to_sync() && ipf <= MAX_IPF && !paused {
engine.step()?;
ipf += 1;
}
let _engine_elapsed = engine_start.elapsed();
let (mpos, keycode) = get_input_code();
engine.perform_sync(mpos, keycode, &mut raw_buffer);
update_image_buffer(&mut buffer, &raw_buffer);
image.update(&buffer);
texture.update(&image);
clear_background(BLACK);
let layout = Layout::generate();
draw_texture_ex(
&texture,
layout.x,
layout.y,
WHITE,
DrawTextureParams {
dest_size: Some(vec2(layout.size, layout.size)),
..Default::default()
},
);
// Wait for the next frame
let elapsed = start_time.elapsed();
if elapsed < FRAMETIME {
std::thread::sleep(FRAMETIME - elapsed);
}
next_frame().await;
}
Ok(())
}

27
src/ui.rs Normal file
View File

@ -0,0 +1,27 @@
use macroquad::prelude::*;
pub struct Layout {
pub x: f32,
pub y: f32,
pub size: f32,
}
impl Layout {
pub fn generate() -> Self {
let (x, y, size) = place(screen_width(), screen_height());
Self { x, y, size }
}
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)
}
}
fn place(width: f32, height: f32) -> (f32, f32, f32) {
let minsize = width.min(height);
let power_two = minsize.log2().floor() as u32;
let image_size = (2 as usize).pow(power_two) as f32;
let startx = (width - image_size) / 2.;
let starty = (height - image_size) / 2.;
(startx, starty, image_size)
}

View File

@ -1,61 +1,10 @@
use std::io::Read;
use crate::RES;
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;
#[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 std::io::Read;
const RES: usize = 256;
pub fn read_u16s_from_file(file_path: &str) -> Result<Vec<u16>> {
let mut file = File::open(file_path)?;
@ -86,110 +35,48 @@ 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;
let mut key_code = 0_u16;
if input.key_held(KeyCode::Space)
|| input.mouse_held(MouseButton::Left)
|| gamepad.pressing(NesInput::A)
{
key_code += 1;
}
if input.key_held_logical(Key::Character("b"))
|| input.mouse_held(MouseButton::Right)
|| gamepad.pressing(NesInput::B)
{
key_code += 2;
}
if input.key_held_logical(Key::Character("w"))
|| input.key_held(KeyCode::ArrowUp)
|| gamepad.pressing(NesInput::Up)
{
key_code += 4;
}
if input.key_held_logical(Key::Character("s"))
|| input.key_held(KeyCode::ArrowDown)
|| gamepad.pressing(NesInput::Down)
{
key_code += 8;
}
if input.key_held_logical(Key::Character("a"))
|| input.key_held(KeyCode::ArrowLeft)
|| gamepad.pressing(NesInput::Left)
{
key_code += 16;
}
if input.key_held_logical(Key::Character("d"))
|| input.key_held(KeyCode::ArrowRight)
|| gamepad.pressing(NesInput::Right)
{
key_code += 32;
}
if input.key_held_logical(Key::Character("n")) || gamepad.pressing(NesInput::Select) {
key_code += 64;
}
if input.key_held_logical(Key::Character("m")) || gamepad.pressing(NesInput::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();
}
#[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),
};
pub fn get_input_code() -> (u16, u16) {
use crate::ui::Layout;
let mp = Layout::generate().clamp_mouse();
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)
}