Compare commits

..

17 Commits
v0.4.1 ... main

29 changed files with 1068 additions and 519 deletions

875
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,8 @@ description = "An emulator for the Virtual Boy."
repository = "https://git.virtual-boy.com/PVB/lemur" repository = "https://git.virtual-boy.com/PVB/lemur"
publish = false publish = false
license = "MIT" license = "MIT"
version = "0.4.1" version = "0.4.6"
edition = "2021" edition = "2024"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
@ -15,7 +15,7 @@ bitflags = { version = "2", features = ["serde"] }
bytemuck = { version = "1", features = ["derive"] } bytemuck = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" } cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" }
directories = "5" directories = "6"
egui = { version = "0.30", features = ["serde"] } egui = { version = "0.30", features = ["serde"] }
egui_extras = { version = "0.30", features = ["image"] } egui_extras = { version = "0.30", features = ["image"] }
egui-toast = { git = "https://github.com/urholaukkarinen/egui-toast.git", rev = "d0bcf97" } egui-toast = { git = "https://github.com/urholaukkarinen/egui-toast.git", rev = "d0bcf97" }
@ -30,7 +30,7 @@ num-derive = "0.4"
num-traits = "0.2" num-traits = "0.2"
oneshot = "0.1" oneshot = "0.1"
pollster = "0.4" pollster = "0.4"
rfd = "0.15" rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "tokio"]}
rtrb = "0.3" rtrb = "0.3"
rubato = "0.16" rubato = "0.16"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@ -43,7 +43,7 @@ wgpu = "23"
winit = { version = "0.30", features = ["serde"] } winit = { version = "0.30", features = ["serde"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows = { version = "0.58", features = ["Win32_System_Threading"] } windows = { version = "0.59", features = ["Win32_System_Threading"] }
[build-dependencies] [build-dependencies]
cc = "1" cc = "1"

View File

@ -6,6 +6,8 @@ A Virtual Boy emulator built around the shrooms-vb core. Written in Rust, using
Install the following dependencies: Install the following dependencies:
- `cargo` (via [rustup](https://rustup.rs/), the version from your package manager is too old) - `cargo` (via [rustup](https://rustup.rs/), the version from your package manager is too old)
- a C compiler (any will do, [the build script](https://docs.rs/cc/latest/cc/#compile-time-requirements) will find it automatically)
- (on linux) `libasound2-dev` and `libudev-dev`
Run Run
```sh ```sh

@ -1 +1 @@
Subproject commit 185362e6cd1c0f668a186adde2522da2f0d45307 Subproject commit ecbd103917315e3aa24fd2a682208f5548ec5d1b

View File

@ -1,9 +1,9 @@
use std::{collections::HashSet, num::NonZero, sync::Arc, thread, time::Duration}; use std::{collections::HashSet, num::NonZero, sync::Arc, thread, time::Duration};
use egui::{ use egui::{
ahash::{HashMap, HashMapExt},
Context, FontData, FontDefinitions, FontFamily, IconData, TextWrapMode, ViewportBuilder, Context, FontData, FontDefinitions, FontFamily, IconData, TextWrapMode, ViewportBuilder,
ViewportCommand, ViewportId, ViewportInfo, ViewportCommand, ViewportId, ViewportInfo,
ahash::{HashMap, HashMapExt},
}; };
use gilrs::{EventType, Gilrs}; use gilrs::{EventType, Gilrs};
use tracing::{error, warn}; use tracing::{error, warn};
@ -18,12 +18,12 @@ use crate::{
controller::ControllerManager, controller::ControllerManager,
emulator::{EmulatorClient, EmulatorCommand, SimId}, emulator::{EmulatorClient, EmulatorCommand, SimId},
images::ImageProcessor, images::ImageProcessor,
input::MappingProvider, input::{MappingProvider, ShortcutProvider},
memory::MemoryClient, memory::MemoryClient,
persistence::Persistence, persistence::Persistence,
window::{ window::{
AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow, AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow,
GdbServerWindow, InputWindow, ObjectWindow, RegisterWindow, WorldWindow, GdbServerWindow, InputWindow, ObjectWindow, RegisterWindow, ShortcutsWindow, WorldWindow,
}, },
}; };
@ -44,6 +44,7 @@ pub struct Application {
client: EmulatorClient, client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
mappings: MappingProvider, mappings: MappingProvider,
shortcuts: ShortcutProvider,
controllers: ControllerManager, controllers: ControllerManager,
memory: Arc<MemoryClient>, memory: Arc<MemoryClient>,
images: ImageProcessor, images: ImageProcessor,
@ -63,6 +64,7 @@ impl Application {
let icon = load_icon().ok().map(Arc::new); let icon = load_icon().ok().map(Arc::new);
let persistence = Persistence::new(); let persistence = Persistence::new();
let mappings = MappingProvider::new(persistence.clone()); let mappings = MappingProvider::new(persistence.clone());
let shortcuts = ShortcutProvider::new(persistence.clone());
let controllers = ControllerManager::new(client.clone(), &mappings); let controllers = ControllerManager::new(client.clone(), &mappings);
let memory = Arc::new(MemoryClient::new(client.clone())); let memory = Arc::new(MemoryClient::new(client.clone()));
let images = ImageProcessor::new(); let images = ImageProcessor::new();
@ -77,6 +79,7 @@ impl Application {
client, client,
proxy, proxy,
mappings, mappings,
shortcuts,
memory, memory,
images, images,
controllers, controllers,
@ -89,7 +92,8 @@ impl Application {
fn open(&mut self, event_loop: &ActiveEventLoop, window: Box<dyn AppWindow>) { fn open(&mut self, event_loop: &ActiveEventLoop, window: Box<dyn AppWindow>) {
let viewport_id = window.viewport_id(); let viewport_id = window.viewport_id();
if self.viewports.contains_key(&viewport_id) { if let Some(viewport) = self.viewports.get(&viewport_id) {
viewport.window.focus_window();
return; return;
} }
self.viewports.insert( self.viewports.insert(
@ -111,6 +115,7 @@ impl ApplicationHandler<UserEvent> for Application {
self.client.clone(), self.client.clone(),
self.proxy.clone(), self.proxy.clone(),
self.persistence.clone(), self.persistence.clone(),
self.shortcuts.clone(),
SimId::Player1, SimId::Player1,
); );
self.open(event_loop, Box::new(app)); self.open(event_loop, Box::new(app));
@ -246,11 +251,16 @@ impl ApplicationHandler<UserEvent> for Application {
let input = InputWindow::new(self.mappings.clone()); let input = InputWindow::new(self.mappings.clone());
self.open(event_loop, Box::new(input)); self.open(event_loop, Box::new(input));
} }
UserEvent::OpenShortcuts => {
let shortcuts = ShortcutsWindow::new(self.shortcuts.clone());
self.open(event_loop, Box::new(shortcuts));
}
UserEvent::OpenPlayer2 => { UserEvent::OpenPlayer2 => {
let p2 = GameWindow::new( let p2 = GameWindow::new(
self.client.clone(), self.client.clone(),
self.proxy.clone(), self.proxy.clone(),
self.persistence.clone(), self.persistence.clone(),
self.shortcuts.clone(),
SimId::Player2, SimId::Player2,
); );
self.open(event_loop, Box::new(p2)); self.open(event_loop, Box::new(p2));
@ -503,6 +513,7 @@ pub enum UserEvent {
OpenRegisters(SimId), OpenRegisters(SimId),
OpenDebugger(SimId), OpenDebugger(SimId),
OpenInput, OpenInput,
OpenShortcuts,
OpenPlayer2, OpenPlayer2,
Quit(SimId), Quit(SimId),
} }

View File

@ -1,6 +1,6 @@
use std::time::Duration; use std::time::Duration;
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use itertools::Itertools; use itertools::Itertools;
use rubato::{FftFixedInOut, Resampler}; use rubato::{FftFixedInOut, Resampler};

View File

@ -1,6 +1,6 @@
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use gilrs::{ev::Code, Event as GamepadEvent, EventType, GamepadId}; use gilrs::{Event as GamepadEvent, EventType, GamepadId, ev::Code};
use winit::{ use winit::{
event::{ElementState, KeyEvent}, event::{ElementState, KeyEvent},
keyboard::PhysicalKey, keyboard::PhysicalKey,

View File

@ -5,9 +5,9 @@ use std::{
io::{Read, Seek, SeekFrom, Write}, io::{Read, Seek, SeekFrom, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{ sync::{
Arc, Weak,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
mpsc::{self, RecvError, TryRecvError}, mpsc::{self, RecvError, TryRecvError},
Arc, Weak,
}, },
}; };
@ -22,7 +22,7 @@ use crate::{
graphics::TextureSink, graphics::TextureSink,
memory::{MemoryRange, MemoryRegion}, memory::{MemoryRange, MemoryRegion},
}; };
use shrooms_vb_core::{Sim, StopReason, EXPECTED_FRAME_SIZE}; use shrooms_vb_core::{EXPECTED_FRAME_SIZE, Sim, StopReason};
pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType}; pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType};
mod address_set; mod address_set;
@ -291,6 +291,26 @@ impl Emulator {
); );
} }
fn frame_advance(&mut self) {
if self
.state
.compare_exchange(
EmulatorState::Paused,
EmulatorState::Stepping,
Ordering::AcqRel,
Ordering::Acquire,
)
.is_err_and(|s| s == EmulatorState::Running)
{
let _ = self.state.compare_exchange(
EmulatorState::Running,
EmulatorState::Stepping,
Ordering::AcqRel,
Ordering::Relaxed,
);
}
}
fn save_sram(&mut self, sim_id: SimId) -> Result<()> { fn save_sram(&mut self, sim_id: SimId) -> Result<()> {
let sim = self.sims.get_mut(sim_id.to_index()); let sim = self.sims.get_mut(sim_id.to_index());
let cart = self.carts[sim_id.to_index()].as_mut(); let cart = self.carts[sim_id.to_index()].as_mut();
@ -366,6 +386,7 @@ impl Emulator {
debugger.stop_reason = None; debugger.stop_reason = None;
true true
} }
fn debug_step(&mut self, sim_id: SimId) { fn debug_step(&mut self, sim_id: SimId) {
if self.debug_continue(sim_id) { if self.debug_continue(sim_id) {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else { let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
@ -428,7 +449,7 @@ impl Emulator {
// Don't emulate if the state is "paused", or if any sim is paused in the debugger // Don't emulate if the state is "paused", or if any sim is paused in the debugger
let running = match state { let running = match state {
EmulatorState::Paused => false, EmulatorState::Paused => false,
EmulatorState::Running => true, EmulatorState::Running | EmulatorState::Stepping => true,
EmulatorState::Debugging => self.debuggers.values().all(|d| d.stop_reason.is_none()), EmulatorState::Debugging => self.debuggers.values().all(|d| d.stop_reason.is_none()),
}; };
let p1_running = running && p1_state == SimState::Ready; let p1_running = running && p1_state == SimState::Ready;
@ -442,6 +463,10 @@ impl Emulator {
self.sims[SimId::Player2.to_index()].emulate(); self.sims[SimId::Player2.to_index()].emulate();
} }
if state == EmulatorState::Stepping {
self.state.store(EmulatorState::Paused, Ordering::Release);
}
// Debug state // Debug state
if state == EmulatorState::Debugging { if state == EmulatorState::Debugging {
for sim_id in SimId::values() { for sim_id in SimId::values() {
@ -539,6 +564,9 @@ impl Emulator {
EmulatorCommand::Resume => { EmulatorCommand::Resume => {
self.resume_sims(); self.resume_sims();
} }
EmulatorCommand::FrameAdvance => {
self.frame_advance();
}
EmulatorCommand::StartDebugging(sim_id, debugger) => { EmulatorCommand::StartDebugging(sim_id, debugger) => {
self.start_debugging(sim_id, debugger); self.start_debugging(sim_id, debugger);
} }
@ -665,6 +693,7 @@ pub enum EmulatorCommand {
StopSecondSim, StopSecondSim,
Pause, Pause,
Resume, Resume,
FrameAdvance,
StartDebugging(SimId, DebugSender), StartDebugging(SimId, DebugSender),
StopDebugging(SimId), StopDebugging(SimId),
DebugInterrupt(SimId), DebugInterrupt(SimId),
@ -700,6 +729,7 @@ pub enum SimState {
pub enum EmulatorState { pub enum EmulatorState {
Paused, Paused,
Running, Running,
Stepping,
Debugging, Debugging,
} }

View File

@ -1,6 +1,6 @@
use std::{ffi::c_void, ptr, slice}; use std::{ffi::c_void, ptr, slice};
use anyhow::{anyhow, Result}; use anyhow::{Result, anyhow};
use bitflags::bitflags; use bitflags::bitflags;
use num_derive::{FromPrimitive, ToPrimitive}; use num_derive::{FromPrimitive, ToPrimitive};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -91,7 +91,7 @@ type OnWrite = extern "C" fn(
) -> c_int; ) -> c_int;
#[link(name = "vb")] #[link(name = "vb")]
extern "C" { unsafe extern "C" {
#[link_name = "vbEmulate"] #[link_name = "vbEmulate"]
fn vb_emulate(sim: *mut VB, cycles: *mut u32) -> c_int; fn vb_emulate(sim: *mut VB, cycles: *mut u32) -> c_int;
#[link_name = "vbEmulateEx"] #[link_name = "vbEmulateEx"]
@ -170,7 +170,7 @@ extern "C" {
fn vb_write(sim: *mut VB, address: u32, _type: VBDataType, value: i32) -> i32; fn vb_write(sim: *mut VB, address: u32, _type: VBDataType, value: i32) -> i32;
} }
#[no_mangle] #[unsafe(no_mangle)]
extern "C" fn on_frame(sim: *mut VB) -> c_int { extern "C" fn on_frame(sim: *mut VB) -> c_int {
// SAFETY: the *mut VB owns its userdata. // SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid. // There is no way for the userdata to be null or otherwise invalid.
@ -179,7 +179,7 @@ extern "C" fn on_frame(sim: *mut VB) -> c_int {
1 1
} }
#[no_mangle] #[unsafe(no_mangle)]
extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: c_int) -> c_int { extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length: c_int) -> c_int {
// SAFETY: the *mut VB owns its userdata. // SAFETY: the *mut VB owns its userdata.
// There is no way for the userdata to be null or otherwise invalid. // There is no way for the userdata to be null or otherwise invalid.
@ -196,14 +196,10 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length:
stopped = true; stopped = true;
} }
if stopped { if stopped { 1 } else { 0 }
1
} else {
0
}
} }
#[no_mangle] #[unsafe(no_mangle)]
extern "C" fn on_read( extern "C" fn on_read(
sim: *mut VB, sim: *mut VB,
address: u32, address: u32,
@ -229,7 +225,7 @@ extern "C" fn on_read(
0 0
} }
#[no_mangle] #[unsafe(no_mangle)]
extern "C" fn on_write( extern "C" fn on_write(
sim: *mut VB, sim: *mut VB,
address: u32, address: u32,

View File

@ -1,4 +1,4 @@
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use registers::REGISTERS; use registers::REGISTERS;
use request::{Request, RequestKind, RequestSource}; use request::{Request, RequestKind, RequestSource};
use response::Response; use response::Response;
@ -12,7 +12,7 @@ use tokio::{
pin, select, pin, select,
sync::{mpsc, oneshot}, sync::{mpsc, oneshot},
}; };
use tracing::{debug, enabled, error, info, Level}; use tracing::{Level, debug, enabled, error, info};
use crate::emulator::{ use crate::emulator::{
DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId, VBWatchpointType, DebugEvent, DebugStopReason, EmulatorClient, EmulatorCommand, SimId, VBWatchpointType,

View File

@ -1,4 +1,4 @@
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use atoi::FromRadix16; use atoi::FromRadix16;
use tokio::io::{AsyncRead, AsyncReadExt as _}; use tokio::io::{AsyncRead, AsyncReadExt as _};

View File

@ -1,12 +1,13 @@
use std::{ use std::{
sync::{ sync::{
Arc, Mutex, MutexGuard,
atomic::{AtomicU64, Ordering}, atomic::{AtomicU64, Ordering},
mpsc, Arc, Mutex, MutexGuard, mpsc,
}, },
thread, thread,
}; };
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use itertools::Itertools as _; use itertools::Itertools as _;
use wgpu::{ use wgpu::{
Device, Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, Texture, Device, Extent3d, ImageCopyTexture, ImageDataLayout, Origin3d, Queue, Texture,

View File

@ -7,9 +7,9 @@ use std::{
}; };
use egui::{ use egui::{
Color32, ColorImage, TextureHandle, TextureOptions,
epaint::ImageDelta, epaint::ImageDelta,
load::{LoadError, SizedTexture, TextureLoader, TexturePoll}, load::{LoadError, SizedTexture, TextureLoader, TexturePoll},
Color32, ColorImage, TextureHandle, TextureOptions,
}; };
use tokio::{sync::mpsc, time::timeout}; use tokio::{sync::mpsc, time::timeout};

View File

@ -1,12 +1,14 @@
use std::{ use std::{
collections::{hash_map::Entry, HashMap}, cmp::Ordering,
collections::{HashMap, HashSet, hash_map::Entry},
fmt::Display, fmt::Display,
str::FromStr, str::FromStr,
sync::{Arc, RwLock}, sync::{Arc, Mutex, RwLock},
}; };
use anyhow::anyhow; use anyhow::anyhow;
use gilrs::{ev::Code, Axis, Button, Gamepad, GamepadId}; use egui::{Key, KeyboardShortcut, Modifiers};
use gilrs::{Axis, Button, Gamepad, GamepadId, ev::Code};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use winit::keyboard::{KeyCode, PhysicalKey}; use winit::keyboard::{KeyCode, PhysicalKey};
@ -454,3 +456,205 @@ struct PersistedGamepadMapping {
default_buttons: Vec<(Code, VBKey)>, default_buttons: Vec<(Code, VBKey)>,
default_axes: Vec<(Code, (VBKey, VBKey))>, default_axes: Vec<(Code, (VBKey, VBKey))>,
} }
#[derive(Serialize, Deserialize)]
pub struct Shortcut {
pub shortcut: KeyboardShortcut,
pub command: Command,
}
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum Command {
OpenRom,
Quit,
FrameAdvance,
Reset,
PauseResume,
// if you update this, update Command::all and add a default
}
impl Command {
pub fn all() -> [Self; 5] {
[
Self::OpenRom,
Self::Quit,
Self::PauseResume,
Self::Reset,
Self::FrameAdvance,
]
}
pub fn name(self) -> &'static str {
match self {
Self::OpenRom => "Open ROM",
Self::Quit => "Exit",
Self::PauseResume => "Pause/Resume",
Self::Reset => "Reset",
Self::FrameAdvance => "Frame Advance",
}
}
}
impl Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
struct Shortcuts {
all: Vec<(Command, KeyboardShortcut)>,
by_command: HashMap<Command, KeyboardShortcut>,
}
impl Default for Shortcuts {
fn default() -> Self {
let mut shortcuts = Shortcuts {
all: vec![],
by_command: HashMap::new(),
};
shortcuts.set(
Command::OpenRom,
KeyboardShortcut::new(Modifiers::COMMAND, Key::O),
);
shortcuts.set(
Command::Quit,
KeyboardShortcut::new(Modifiers::COMMAND, Key::Q),
);
shortcuts.set(
Command::PauseResume,
KeyboardShortcut::new(Modifiers::NONE, Key::F5),
);
shortcuts.set(
Command::Reset,
KeyboardShortcut::new(Modifiers::SHIFT, Key::F5),
);
shortcuts.set(
Command::FrameAdvance,
KeyboardShortcut::new(Modifiers::NONE, Key::F6),
);
shortcuts
}
}
impl Shortcuts {
fn set(&mut self, command: Command, shortcut: KeyboardShortcut) {
if self.by_command.insert(command, shortcut).is_some() {
for (cmd, sht) in &mut self.all {
if *cmd == command {
*sht = shortcut;
break;
}
}
} else {
self.all.push((command, shortcut));
}
self.all.sort_by(|l, r| order_shortcut(l.1, r.1));
}
fn unset(&mut self, command: Command) {
if self.by_command.remove(&command).is_some() {
self.all.retain(|(c, _)| *c != command);
}
}
fn save(&self) -> PersistedShortcuts {
let mut shortcuts = PersistedShortcuts { shortcuts: vec![] };
for command in Command::all() {
let shortcut = self.by_command.get(&command).copied();
shortcuts.shortcuts.push((command, shortcut));
}
shortcuts
}
}
fn order_shortcut(left: KeyboardShortcut, right: KeyboardShortcut) -> Ordering {
left.logical_key.cmp(&right.logical_key).then_with(|| {
specificity(left.modifiers)
.cmp(&specificity(right.modifiers))
.reverse()
})
}
fn specificity(modifiers: egui::Modifiers) -> usize {
let mut mods = 0;
if modifiers.alt {
mods += 1;
}
if modifiers.command || modifiers.ctrl {
mods += 1;
}
if modifiers.shift {
mods += 1;
}
mods
}
#[derive(Serialize, Deserialize)]
struct PersistedShortcuts {
shortcuts: Vec<(Command, Option<KeyboardShortcut>)>,
}
#[derive(Clone)]
pub struct ShortcutProvider {
persistence: Persistence,
shortcuts: Arc<Mutex<Shortcuts>>,
}
impl ShortcutProvider {
pub fn new(persistence: Persistence) -> Self {
let mut shortcuts = Shortcuts::default();
if let Ok(saved) = persistence.load_config::<PersistedShortcuts>("shortcuts") {
for (command, shortcut) in saved.shortcuts {
if let Some(shortcut) = shortcut {
shortcuts.set(command, shortcut);
} else {
shortcuts.unset(command);
}
}
}
Self {
persistence,
shortcuts: Arc::new(Mutex::new(shortcuts)),
}
}
pub fn shortcut_for(&self, command: Command) -> Option<KeyboardShortcut> {
let lock = self.shortcuts.lock().unwrap();
lock.by_command.get(&command).copied()
}
pub fn consume_all(&self, input: &mut egui::InputState) -> HashSet<Command> {
let lock = self.shortcuts.lock().unwrap();
lock.all
.iter()
.filter_map(|(command, shortcut)| input.consume_shortcut(shortcut).then_some(*command))
.collect()
}
pub fn set(&self, command: Command, shortcut: KeyboardShortcut) {
let updated = {
let mut lock = self.shortcuts.lock().unwrap();
lock.set(command, shortcut);
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
pub fn unset(&self, command: Command) {
let updated = {
let mut lock = self.shortcuts.lock().unwrap();
lock.unset(command);
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
pub fn reset(&self) {
let updated = {
let mut lock = self.shortcuts.lock().unwrap();
*lock = Shortcuts::default();
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
}

View File

@ -3,13 +3,13 @@
use std::{path::PathBuf, process, time::SystemTime}; use std::{path::PathBuf, process, time::SystemTime};
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use app::Application; use app::Application;
use clap::Parser; use clap::Parser;
use emulator::EmulatorBuilder; use emulator::EmulatorBuilder;
use thread_priority::{ThreadBuilder, ThreadPriority}; use thread_priority::{ThreadBuilder, ThreadPriority};
use tracing::error; use tracing::error;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
use winit::event_loop::{ControlFlow, EventLoop}; use winit::event_loop::{ControlFlow, EventLoop};
mod app; mod app;

View File

@ -2,7 +2,7 @@ use std::{
collections::HashMap, collections::HashMap,
fmt::Debug, fmt::Debug,
iter::FusedIterator, iter::FusedIterator,
sync::{atomic::AtomicU64, Arc, Mutex, RwLock, RwLockReadGuard, TryLockError, Weak}, sync::{Arc, Mutex, RwLock, RwLockReadGuard, TryLockError, Weak, atomic::AtomicU64},
}; };
use bytemuck::BoxBytes; use bytemuck::BoxBytes;
@ -223,7 +223,7 @@ impl MemoryRegion {
.iter() .iter()
.map(|i| i.load(std::sync::atomic::Ordering::Acquire)) .map(|i| i.load(std::sync::atomic::Ordering::Acquire))
.enumerate() .enumerate()
.max_by_key(|(_, gen)| *gen) .max_by_key(|(_, g)| *g)
.map(|(i, _)| i) .map(|(i, _)| i)
.unwrap(); .unwrap();
let inner = match self.bufs[newest_index].try_read() { let inner = match self.bufs[newest_index].try_read() {

View File

@ -1,6 +1,6 @@
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use anyhow::{bail, Result}; use anyhow::{Result, bail};
use directories::ProjectDirs; use directories::ProjectDirs;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -3,6 +3,7 @@ use egui::{Context, ViewportBuilder, ViewportId};
pub use game::GameWindow; pub use game::GameWindow;
pub use gdb::GdbServerWindow; pub use gdb::GdbServerWindow;
pub use input::InputWindow; pub use input::InputWindow;
pub use shortcuts::ShortcutsWindow;
pub use vip::{ pub use vip::{
BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow,
}; };
@ -15,6 +16,7 @@ mod game;
mod game_screen; mod game_screen;
mod gdb; mod gdb;
mod input; mod input;
mod shortcuts;
mod utils; mod utils;
mod vip; mod vip;

View File

@ -3,20 +3,21 @@ use std::sync::mpsc;
use crate::{ use crate::{
app::UserEvent, app::UserEvent,
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState}, emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
input::{Command, ShortcutProvider},
persistence::Persistence, persistence::Persistence,
}; };
use egui::{ use egui::{
menu, Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui, Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui, Vec2,
Vec2, ViewportBuilder, ViewportCommand, ViewportId, Window, ViewportBuilder, ViewportCommand, ViewportId, Window, menu,
}; };
use egui_toast::{Toast, Toasts}; use egui_toast::{Toast, Toasts};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use winit::event_loop::EventLoopProxy; use winit::event_loop::EventLoopProxy;
use super::{ use super::{
AppWindow,
game_screen::{DisplayMode, GameScreen}, game_screen::{DisplayMode, GameScreen},
utils::UiExt as _, utils::UiExt as _,
AppWindow,
}; };
const COLOR_PRESETS: [[Color32; 2]; 3] = [ const COLOR_PRESETS: [[Color32; 2]; 3] = [
@ -38,6 +39,7 @@ pub struct GameWindow {
client: EmulatorClient, client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
persistence: Persistence, persistence: Persistence,
shortcuts: ShortcutProvider,
sim_id: SimId, sim_id: SimId,
config: GameConfig, config: GameConfig,
screen: Option<GameScreen>, screen: Option<GameScreen>,
@ -50,6 +52,7 @@ impl GameWindow {
client: EmulatorClient, client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>, proxy: EventLoopProxy<UserEvent>,
persistence: Persistence, persistence: Persistence,
shortcuts: ShortcutProvider,
sim_id: SimId, sim_id: SimId,
) -> Self { ) -> Self {
let config = load_config(&persistence, sim_id); let config = load_config(&persistence, sim_id);
@ -57,6 +60,7 @@ impl GameWindow {
client, client,
proxy, proxy,
persistence, persistence,
shortcuts,
sim_id, sim_id,
config, config,
screen: None, screen: None,
@ -66,8 +70,53 @@ impl GameWindow {
} }
fn show_menu(&mut self, ctx: &Context, ui: &mut Ui) { fn show_menu(&mut self, ctx: &Context, ui: &mut Ui) {
let state = self.client.emulator_state();
let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready;
let can_pause = is_ready && state == EmulatorState::Running;
let can_resume = is_ready && state == EmulatorState::Paused;
let can_frame_advance = is_ready && state != EmulatorState::Debugging;
for command in ui.input_mut(|input| self.shortcuts.consume_all(input)) {
match command {
Command::OpenRom => {
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(self.sim_id, path));
}
}
Command::Quit => {
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
Command::PauseResume => {
if state == EmulatorState::Paused && can_resume {
self.client.send_command(EmulatorCommand::Resume);
}
if state == EmulatorState::Running && can_pause {
self.client.send_command(EmulatorCommand::Pause);
}
}
Command::Reset => {
if is_ready {
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
}
}
Command::FrameAdvance => {
if can_frame_advance {
self.client.send_command(EmulatorCommand::FrameAdvance);
}
}
}
}
ui.menu_button("ROM", |ui| { ui.menu_button("ROM", |ui| {
if ui.button("Open ROM").clicked() { if ui
.add(self.button_for(ui.ctx(), "Open ROM", Command::OpenRom))
.clicked()
{
let rom = rfd::FileDialog::new() let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom"]) .add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.pick_file(); .pick_file();
@ -77,29 +126,54 @@ impl GameWindow {
} }
ui.close_menu(); ui.close_menu();
} }
if ui.button("Quit").clicked() { if ui
.add(self.button_for(ui.ctx(), "Quit", Command::Quit))
.clicked()
{
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id)); let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
} }
}); });
ui.menu_button("Emulation", |ui| { ui.menu_button("Emulation", |ui| {
let state = self.client.emulator_state();
let is_ready = self.client.sim_state(self.sim_id) == SimState::Ready;
let can_pause = is_ready && state == EmulatorState::Running;
let can_resume = is_ready && state == EmulatorState::Paused;
if state == EmulatorState::Running { if state == EmulatorState::Running {
if ui.add_enabled(can_pause, Button::new("Pause")).clicked() { if ui
.add_enabled(
can_pause,
self.button_for(ui.ctx(), "Pause", Command::PauseResume),
)
.clicked()
{
self.client.send_command(EmulatorCommand::Pause); self.client.send_command(EmulatorCommand::Pause);
ui.close_menu(); ui.close_menu();
} }
} else if ui.add_enabled(can_resume, Button::new("Resume")).clicked() { } else if ui
.add_enabled(
can_resume,
self.button_for(ui.ctx(), "Resume", Command::PauseResume),
)
.clicked()
{
self.client.send_command(EmulatorCommand::Resume); self.client.send_command(EmulatorCommand::Resume);
ui.close_menu(); ui.close_menu();
} }
if ui.add_enabled(is_ready, Button::new("Reset")).clicked() { if ui
.add_enabled(is_ready, self.button_for(ui.ctx(), "Reset", Command::Reset))
.clicked()
{
self.client self.client
.send_command(EmulatorCommand::Reset(self.sim_id)); .send_command(EmulatorCommand::Reset(self.sim_id));
ui.close_menu(); ui.close_menu();
} }
ui.separator();
if ui
.add_enabled(
can_frame_advance,
self.button_for(ui.ctx(), "Frame Advance", Command::FrameAdvance),
)
.clicked()
{
self.client.send_command(EmulatorCommand::FrameAdvance);
ui.close_menu();
}
}); });
ui.menu_button("Options", |ui| self.show_options_menu(ctx, ui)); ui.menu_button("Options", |ui| self.show_options_menu(ctx, ui));
ui.menu_button("Multiplayer", |ui| { ui.menu_button("Multiplayer", |ui| {
@ -284,6 +358,10 @@ impl GameWindow {
ui.close_menu(); ui.close_menu();
} }
}); });
if ui.button("Key Shortcuts").clicked() {
self.proxy.send_event(UserEvent::OpenShortcuts).unwrap();
ui.close_menu();
}
} }
fn show_color_picker(&mut self, ui: &mut Ui) { fn show_color_picker(&mut self, ui: &mut Ui) {
@ -325,6 +403,14 @@ impl GameWindow {
} }
self.config = new_config; self.config = new_config;
} }
fn button_for(&self, ctx: &Context, text: &str, command: Command) -> Button {
let button = Button::new(text);
match self.shortcuts.shortcut_for(command) {
Some(shortcut) => button.shortcut_text(ctx.format_shortcut(&shortcut)),
None => button,
}
}
} }
fn config_filename(sim_id: SimId) -> &'static str { fn config_filename(sim_id: SimId) -> &'static str {

View File

@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc};
use egui::{Color32, Rgba, Vec2, Widget}; use egui::{Color32, Rgba, Vec2, Widget};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use wgpu::{util::DeviceExt as _, BindGroup, BindGroupLayout, Buffer, RenderPipeline}; use wgpu::{BindGroup, BindGroupLayout, Buffer, RenderPipeline, util::DeviceExt as _};
use crate::graphics::TextureSink; use crate::graphics::TextureSink;

103
src/window/shortcuts.rs Normal file
View File

@ -0,0 +1,103 @@
use egui::{
Button, CentralPanel, Context, Event, KeyboardShortcut, Label, Layout, Ui, ViewportBuilder,
ViewportId,
};
use egui_extras::{Column, TableBuilder};
use crate::input::{Command, ShortcutProvider};
use super::AppWindow;
pub struct ShortcutsWindow {
shortcuts: ShortcutProvider,
now_binding: Option<Command>,
}
impl ShortcutsWindow {
pub fn new(shortcuts: ShortcutProvider) -> Self {
Self {
shortcuts,
now_binding: None,
}
}
fn show_shortcuts(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| {
if ui.button("Use defaults").clicked() {
self.shortcuts.reset();
}
});
ui.separator();
let row_height = ui.spacing().interact_size.y;
let width = ui.available_width() - 20.0;
TableBuilder::new(ui)
.column(Column::exact(width * 0.3))
.column(Column::exact(width * 0.5))
.column(Column::exact(width * 0.2))
.cell_layout(Layout::left_to_right(egui::Align::Center))
.body(|mut body| {
for command in Command::all() {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.add_sized(ui.available_size(), Label::new(command.name()));
});
row.col(|ui| {
let button = if self.now_binding == Some(command) {
Button::new("Binding...")
} else if let Some(shortcut) = self.shortcuts.shortcut_for(command) {
Button::new(ui.ctx().format_shortcut(&shortcut))
} else {
Button::new("")
};
if ui.add_sized(ui.available_size(), button).clicked() {
self.now_binding = Some(command);
}
});
row.col(|ui| {
if ui
.add_sized(ui.available_size(), Button::new("Clear"))
.clicked()
{
self.shortcuts.unset(command);
self.now_binding = None;
}
});
});
}
});
if let Some(command) = self.now_binding {
if let Some(shortcut) = ui.input_mut(|i| {
i.events.iter().find_map(|event| match event {
Event::Key {
key,
pressed: true,
modifiers,
..
} => Some(KeyboardShortcut::new(*modifiers, *key)),
_ => None,
})
}) {
self.shortcuts.set(command, shortcut);
self.now_binding = None;
}
}
}
}
impl AppWindow for ShortcutsWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of("shortcuts")
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title("Keyboard Shortcuts")
.with_inner_size((400.0, 400.0))
}
fn show(&mut self, ctx: &Context) {
CentralPanel::default().show(ctx, |ui| {
self.show_shortcuts(ui);
});
}
}

View File

@ -6,9 +6,9 @@ use std::{
use atoi::FromRadix16; use atoi::FromRadix16;
use egui::{ use egui::{
ecolor::HexColor, Align, Color32, CursorIcon, Event, Frame, Key, Layout, Margin, Rect, Align, Color32, CursorIcon, Event, Frame, Key, Layout, Margin, Rect, Response, RichText,
Response, RichText, Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, Rounding, Sense, Shape, Stroke, TextEdit, Ui, UiBuilder, Vec2, Widget, WidgetText,
WidgetText, ecolor::HexColor,
}; };
use num_traits::{CheckedAdd, CheckedSub, One}; use num_traits::{CheckedAdd, CheckedSub, One};
@ -119,20 +119,20 @@ pub trait Number:
{ {
} }
impl< impl<
T: Copy T: Copy
+ One + One
+ CheckedAdd + CheckedAdd
+ CheckedSub + CheckedSub
+ Eq + Eq
+ Ord + Ord
+ Display + Display
+ FromStr + FromStr
+ FromRadix16 + FromRadix16
+ UpperHex + UpperHex
+ Send + Send
+ Sync + Sync
+ 'static, + 'static,
> Number for T > Number for T
{ {
} }

View File

@ -11,8 +11,8 @@ use crate::{
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView}, memory::{MemoryClient, MemoryView},
window::{ window::{
utils::{NumberEdit, UiExt},
AppWindow, AppWindow,
utils::{NumberEdit, UiExt},
}, },
}; };

View File

@ -12,8 +12,8 @@ use crate::{
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView}, memory::{MemoryClient, MemoryView},
window::{ window::{
utils::{NumberEdit, UiExt as _},
AppWindow, AppWindow,
utils::{NumberEdit, UiExt as _},
}, },
}; };

View File

@ -11,8 +11,8 @@ use crate::{
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView}, memory::{MemoryClient, MemoryView},
window::{ window::{
utils::{NumberEdit, UiExt as _},
AppWindow, AppWindow,
utils::{NumberEdit, UiExt as _},
}, },
}; };

View File

@ -11,8 +11,8 @@ use crate::{
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryView}, memory::{MemoryClient, MemoryView},
window::{ window::{
utils::{NumberEdit, UiExt as _},
AppWindow, AppWindow,
utils::{NumberEdit, UiExt as _},
}, },
}; };

View File

@ -10,8 +10,8 @@ use crate::{
emulator::SimId, emulator::SimId,
memory::{MemoryClient, MemoryRef, MemoryValue, MemoryView}, memory::{MemoryClient, MemoryRef, MemoryValue, MemoryView},
window::{ window::{
utils::{NumberEdit, UiExt},
AppWindow, AppWindow,
utils::{NumberEdit, UiExt},
}, },
}; };

View File

@ -3,12 +3,12 @@ use egui::{Color32, Image, ImageSource, Response, Sense, TextureOptions, Ui, Wid
pub const GENERIC_PALETTE: [u8; 4] = [0, 32, 64, 128]; pub const GENERIC_PALETTE: [u8; 4] = [0, 32, 64, 128];
pub fn shade(brt: u8, color: Color32) -> Color32 { pub fn shade(brt: u8, color: Color32) -> Color32 {
let corrected = if brt & 0x80 != 0 { let corrected = if brt > 132 {
255 255.0
} else { } else {
(brt << 1) | (brt >> 6) (brt as f32 * 255.0 / 133.0).round()
}; };
color.gamma_multiply(corrected as f32 / 255.0) color.gamma_multiply(corrected / 255.0)
} }
pub fn generic_palette(color: Color32) -> [Color32; 4] { pub fn generic_palette(color: Color32) -> [Color32; 4] {
@ -124,7 +124,7 @@ impl CellData {
} }
pub fn update(&self, source: &mut u16) -> bool { pub fn update(&self, source: &mut u16) -> bool {
let new_value = (self.palette_index as u16) << 14 let new_value = ((self.palette_index as u16) << 14)
| if self.hflip { 0x2000 } else { 0x0000 } | if self.hflip { 0x2000 } else { 0x0000 }
| if self.vflip { 0x1000 } else { 0x0000 } | if self.vflip { 0x1000 } else { 0x0000 }
| (self.char_index as u16 & 0x07ff) | (self.char_index as u16 & 0x07ff)
@ -222,12 +222,12 @@ impl Widget for CharacterGrid<'_> {
for x in (1..grid_width_cells).map(|i| (i as f32) * cell_size) { for x in (1..grid_width_cells).map(|i| (i as f32) * cell_size) {
let p1 = (res.rect.min.x + x, res.rect.min.y).into(); let p1 = (res.rect.min.x + x, res.rect.min.y).into();
let p2 = (res.rect.min.x + x, res.rect.max.y).into(); let p2 = (res.rect.min.x + x, res.rect.max.y).into();
painter.line(vec![p1, p2], stroke); painter.line_segment([p1, p2], stroke);
} }
for y in (1..grid_height_cells).map(|i| (i as f32) * cell_size) { for y in (1..grid_height_cells).map(|i| (i as f32) * cell_size) {
let p1 = (res.rect.min.x, res.rect.min.y + y).into(); let p1 = (res.rect.min.x, res.rect.min.y + y).into();
let p2 = (res.rect.max.x, res.rect.min.y + y).into(); let p2 = (res.rect.max.x, res.rect.min.y + y).into();
painter.line(vec![p1, p2], stroke); painter.line_segment([p1, p2], stroke);
} }
} }
if let Some(selected) = self.selected { if let Some(selected) = self.selected {

View File

@ -6,8 +6,8 @@ use egui::{
}; };
use egui_extras::{Column, Size, StripBuilder, TableBuilder}; use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use fixed::{ use fixed::{
types::extra::{U3, U9},
FixedI32, FixedI32,
types::extra::{U3, U9},
}; };
use num_derive::{FromPrimitive, ToPrimitive}; use num_derive::{FromPrimitive, ToPrimitive};
use num_traits::{FromPrimitive, ToPrimitive}; use num_traits::{FromPrimitive, ToPrimitive};
@ -17,12 +17,12 @@ use crate::{
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader}, images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryRef, MemoryView}, memory::{MemoryClient, MemoryRef, MemoryView},
window::{ window::{
utils::{NumberEdit, UiExt as _},
AppWindow, AppWindow,
utils::{NumberEdit, UiExt as _},
}, },
}; };
use super::utils::{self, shade, CellData, Object}; use super::utils::{self, CellData, Object, shade};
pub struct WorldWindow { pub struct WorldWindow {
sim_id: SimId, sim_id: SimId,
@ -33,6 +33,7 @@ pub struct WorldWindow {
index: usize, index: usize,
param_index: usize, param_index: usize,
generic_palette: bool, generic_palette: bool,
show_extents: bool,
params: ImageParams<WorldParams>, params: ImageParams<WorldParams>,
scale: f32, scale: f32,
} }
@ -57,6 +58,7 @@ impl WorldWindow {
index: params.index, index: params.index,
param_index: 0, param_index: 0,
generic_palette: params.generic_palette, generic_palette: params.generic_palette,
show_extents: false,
params, params,
scale: 1.0, scale: 1.0,
} }
@ -413,6 +415,7 @@ impl WorldWindow {
ui.add(slider); ui.add(slider);
}); });
ui.checkbox(&mut self.generic_palette, "Generic palette"); ui.checkbox(&mut self.generic_palette, "Generic palette");
ui.checkbox(&mut self.show_extents, "Show extents");
}); });
}); });
self.params.write(WorldParams { self.params.write(WorldParams {
@ -426,7 +429,82 @@ impl WorldWindow {
let image = Image::new("vip://world") let image = Image::new("vip://world")
.fit_to_original_size(self.scale) .fit_to_original_size(self.scale)
.texture_options(TextureOptions::NEAREST); .texture_options(TextureOptions::NEAREST);
ui.add(image); let res = ui.add(image);
if self.show_extents {
let world = {
let worlds = self.worlds.borrow();
let data = worlds.read(self.index);
World::parse(&data)
};
if world.header.mode == WorldMode::Object {
return;
}
let lx1 = (world.dst_x - world.dst_parallax) as f32 * self.scale;
let lx2 = lx1 + world.width as f32 * self.scale;
let rx1 = (world.dst_x + world.dst_parallax) as f32 * self.scale;
let rx2 = rx1 + world.width as f32 * self.scale;
let y1 = world.dst_y as f32 * self.scale;
let y2 = y1 + world.height as f32 * self.scale;
let left_color = self.params.left_color;
let right_color = self.params.right_color;
let both_color = Color32::from_rgb(
left_color.r() + right_color.r(),
left_color.g() + right_color.g(),
left_color.b() + right_color.b(),
);
let painter = ui.painter();
let draw_rect = |x1: f32, x2: f32, color: Color32| {
painter.line(
vec![
res.rect.min + (x1, y1).into(),
res.rect.min + (x2, y1).into(),
res.rect.min + (x2, y2).into(),
res.rect.min + (x1, y2).into(),
res.rect.min + (x1, y1).into(),
],
(2.0, color),
)
};
match (world.header.lon, world.header.ron) {
(false, false) => {}
(true, false) => {
draw_rect(lx1, lx2, left_color);
}
(false, true) => {
draw_rect(rx1, rx2, right_color);
}
(true, true) if world.dst_parallax == 0 => {
draw_rect(lx1, lx2, both_color);
}
(true, true) => {
draw_rect(lx1, lx2, left_color);
draw_rect(rx1, rx2, right_color);
let (x1, x2) = if world.dst_parallax < 0 {
(lx1, rx2)
} else {
(rx1, lx2)
};
painter.line_segment(
[
res.rect.min + (x1, y1).into(),
res.rect.min + (x2 + 1.0, y1).into(),
],
(2.0, both_color),
);
painter.line_segment(
[
res.rect.min + (x1, y2).into(),
res.rect.min + (x2 + 1.0, y2).into(),
],
(2.0, both_color),
);
}
}
}
} }
} }
@ -442,7 +520,7 @@ impl AppWindow for WorldWindow {
fn initial_viewport(&self) -> ViewportBuilder { fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default() ViewportBuilder::default()
.with_title(format!("Worlds ({})", self.sim_id)) .with_title(format!("Worlds ({})", self.sim_id))
.with_inner_size((640.0, 500.0)) .with_inner_size((640.0, 520.0))
} }
fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) { fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) {
@ -643,6 +721,15 @@ impl WorldRenderer {
shades.map(|s| shade(s, params.right_color)), shades.map(|s| shade(s, params.right_color)),
] ]
}; };
let palettes = {
let palettes = self.palettes.borrow().read::<[u8; 8]>(0);
[
utils::parse_palette(palettes[0]),
utils::parse_palette(palettes[2]),
utils::parse_palette(palettes[4]),
utils::parse_palette(palettes[6]),
]
};
let chardata = self.chardata.borrow(); let chardata = self.chardata.borrow();
let bgmaps = self.bgmaps.borrow(); let bgmaps = self.bgmaps.borrow();
@ -667,7 +754,8 @@ impl WorldRenderer {
let row = (sy & 0x7) as usize; let row = (sy & 0x7) as usize;
let col = (sx & 0x7) as usize; let col = (sx & 0x7) as usize;
let pixel = utils::read_char_pixel(char, cell.hflip, cell.vflip, row, col); let pixel = utils::read_char_pixel(char, cell.hflip, cell.vflip, row, col);
image.add((dx as usize, dy as usize), colors[0][pixel as usize]); let shade = palettes[cell.palette_index][pixel as usize];
image.add((dx as usize, dy as usize), colors[0][shade as usize]);
} }
let dx = x + world.dst_x + world.dst_parallax; let dx = x + world.dst_x + world.dst_parallax;
@ -680,7 +768,8 @@ impl WorldRenderer {
let row = (sy & 0x7) as usize; let row = (sy & 0x7) as usize;
let col = (sx & 0x7) as usize; let col = (sx & 0x7) as usize;
let pixel = utils::read_char_pixel(char, cell.hflip, cell.vflip, row, col); let pixel = utils::read_char_pixel(char, cell.hflip, cell.vflip, row, col);
image.add((dx as usize, dy as usize), colors[1][pixel as usize]); let shade = palettes[cell.palette_index][pixel as usize];
image.add((dx as usize, dy as usize), colors[1][shade as usize]);
} }
} }
} }
@ -859,7 +948,7 @@ impl WorldHeader {
let new_value = (*source & 0x0030) let new_value = (*source & 0x0030)
| if self.lon { 0x8000 } else { 0x0000 } | if self.lon { 0x8000 } else { 0x0000 }
| if self.ron { 0x4000 } else { 0x0000 } | if self.ron { 0x4000 } else { 0x0000 }
| self.mode.to_u16().unwrap() << 12 | (self.mode.to_u16().unwrap() << 12)
| ((self.scx as u16) << 10) | ((self.scx as u16) << 10)
| ((self.scy as u16) << 8) | ((self.scy as u16) << 8)
| if self.over { 0x0080 } else { 0x0000 } | if self.over { 0x0080 } else { 0x0000 }
@ -976,8 +1065,8 @@ impl<'a> SourceCoordCalculator<'a> {
(sx, sy) (sx, sy)
} }
SourceParam::Affine(affine) => { SourceParam::Affine(affine) => {
let sx = affine_coord(affine.src_x, x, affine.dx, affine.src_parallax.min(0).abs()); let sx = affine_coord(affine.src_x, x, affine.dx, affine.src_parallax.min(0));
let sy = affine_coord(affine.src_y, x, affine.dy, affine.src_parallax.min(0).abs()); let sy = affine_coord(affine.src_y, x, affine.dy, affine.src_parallax.min(0));
(sx, sy) (sx, sy)
} }
} }
@ -997,8 +1086,8 @@ impl<'a> SourceCoordCalculator<'a> {
(sx, sy) (sx, sy)
} }
SourceParam::Affine(affine) => { SourceParam::Affine(affine) => {
let sx = affine_coord(affine.src_x, x, affine.dx, -affine.src_parallax.max(0)); let sx = affine_coord(affine.src_x, x, affine.dx, affine.src_parallax.max(0));
let sy = affine_coord(affine.src_y, x, affine.dy, -affine.src_parallax.max(0)); let sy = affine_coord(affine.src_y, x, affine.dy, affine.src_parallax.max(0));
(sx, sy) (sx, sy)
} }
} }