Compare commits

..

No commits in common. "main" and "v0.4.2" have entirely different histories.
main ... v0.4.2

31 changed files with 469 additions and 1623 deletions

1077
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"
publish = false
license = "MIT"
version = "0.6.0"
edition = "2024"
version = "0.4.2"
edition = "2021"
[dependencies]
anyhow = "1"
@ -15,7 +15,7 @@ bitflags = { version = "2", features = ["serde"] }
bytemuck = { version = "1", features = ["derive"] }
clap = { version = "4", features = ["derive"] }
cpal = { git = "https://github.com/sidit77/cpal.git", rev = "66ed6be" }
directories = "6"
directories = "5"
egui = { version = "0.30", features = ["serde"] }
egui_extras = { version = "0.30", features = ["image"] }
egui-toast = { git = "https://github.com/urholaukkarinen/egui-toast.git", rev = "d0bcf97" }
@ -30,7 +30,7 @@ num-derive = "0.4"
num-traits = "0.2"
oneshot = "0.1"
pollster = "0.4"
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "async-std"]}
rfd = "0.15"
rtrb = "0.3"
rubato = "0.16"
serde = { version = "1", features = ["derive"] }
@ -43,7 +43,7 @@ wgpu = "23"
winit = { version = "0.30", features = ["serde"] }
[target.'cfg(windows)'.dependencies]
windows = { version = "0.59", features = ["Win32_System_Threading"] }
windows = { version = "0.58", features = ["Win32_System_Threading"] }
[build-dependencies]
cc = "1"

View File

@ -6,8 +6,6 @@ A Virtual Boy emulator built around the shrooms-vb core. Written in Rust, using
Install the following dependencies:
- `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
```sh

View File

@ -32,5 +32,6 @@ ENV PATH="/osxcross/bin:$PATH" \
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-19" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-19" \
CROSS_COMPILE="setting-this-to-silence-a-warning-" \
RC_PATH="llvm-rc-19" \
MACOSX_DEPLOYMENT_TARGET="14.5"

@ -1 +1 @@
Subproject commit ecbd103917315e3aa24fd2a682208f5548ec5d1b
Subproject commit 459a16076a1e58f503e9278a41f633e2e870c28e

View File

@ -1,10 +1,9 @@
use std::{collections::HashSet, num::NonZero, sync::Arc, thread, time::Duration};
use egui::{
ahash::{HashMap, HashMapExt},
Context, FontData, FontDefinitions, FontFamily, IconData, TextWrapMode, ViewportBuilder,
ViewportCommand, ViewportId, ViewportInfo,
ahash::{HashMap, HashMapExt},
style::ScrollStyle,
};
use gilrs::{EventType, Gilrs};
use tracing::{error, warn};
@ -19,13 +18,12 @@ use crate::{
controller::ControllerManager,
emulator::{EmulatorClient, EmulatorCommand, SimId},
images::ImageProcessor,
input::{MappingProvider, ShortcutProvider},
input::MappingProvider,
memory::MemoryClient,
persistence::Persistence,
window::{
AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow,
GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, RegisterWindow, TerminalWindow,
WorldWindow,
GdbServerWindow, InputWindow, ObjectWindow, RegisterWindow, WorldWindow,
},
};
@ -46,7 +44,6 @@ pub struct Application {
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
mappings: MappingProvider,
shortcuts: ShortcutProvider,
controllers: ControllerManager,
memory: Arc<MemoryClient>,
images: ImageProcessor,
@ -66,7 +63,6 @@ impl Application {
let icon = load_icon().ok().map(Arc::new);
let persistence = Persistence::new();
let mappings = MappingProvider::new(persistence.clone());
let shortcuts = ShortcutProvider::new(persistence.clone());
let controllers = ControllerManager::new(client.clone(), &mappings);
let memory = Arc::new(MemoryClient::new(client.clone()));
let images = ImageProcessor::new();
@ -81,7 +77,6 @@ impl Application {
client,
proxy,
mappings,
shortcuts,
memory,
images,
controllers,
@ -94,8 +89,7 @@ impl Application {
fn open(&mut self, event_loop: &ActiveEventLoop, window: Box<dyn AppWindow>) {
let viewport_id = window.viewport_id();
if let Some(viewport) = self.viewports.get(&viewport_id) {
viewport.window.focus_window();
if self.viewports.contains_key(&viewport_id) {
return;
}
self.viewports.insert(
@ -117,7 +111,6 @@ impl ApplicationHandler<UserEvent> for Application {
self.client.clone(),
self.proxy.clone(),
self.persistence.clone(),
self.shortcuts.clone(),
SimId::Player1,
);
self.open(event_loop, Box::new(app));
@ -244,10 +237,6 @@ impl ApplicationHandler<UserEvent> for Application {
let registers = RegisterWindow::new(sim_id, &self.memory);
self.open(event_loop, Box::new(registers));
}
UserEvent::OpenTerminal(sim_id) => {
let terminal = TerminalWindow::new(sim_id, &self.client);
self.open(event_loop, Box::new(terminal));
}
UserEvent::OpenDebugger(sim_id) => {
let debugger =
GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone());
@ -257,16 +246,11 @@ impl ApplicationHandler<UserEvent> for Application {
let input = InputWindow::new(self.mappings.clone());
self.open(event_loop, Box::new(input));
}
UserEvent::OpenHotkeys => {
let hotkeys = HotkeysWindow::new(self.shortcuts.clone());
self.open(event_loop, Box::new(hotkeys));
}
UserEvent::OpenPlayer2 => {
let p2 = GameWindow::new(
self.client.clone(),
self.proxy.clone(),
self.persistence.clone(),
self.shortcuts.clone(),
SimId::Player2,
);
self.open(event_loop, Box::new(p2));
@ -383,7 +367,6 @@ impl Viewport {
ctx.style_mut(|s| {
s.wrap_mode = Some(TextWrapMode::Extend);
s.visuals.menu_rounding = Default::default();
s.spacing.scroll = ScrollStyle::thin();
});
egui_extras::install_image_loaders(&ctx);
@ -518,10 +501,8 @@ pub enum UserEvent {
OpenWorlds(SimId),
OpenFrameBuffers(SimId),
OpenRegisters(SimId),
OpenTerminal(SimId),
OpenDebugger(SimId),
OpenInput,
OpenHotkeys,
OpenPlayer2,
Quit(SimId),
}

View File

@ -1,22 +1,20 @@
use std::time::Duration;
use anyhow::{Result, bail};
use anyhow::{bail, Result};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use itertools::Itertools;
use rubato::{FastFixedOut, Resampler};
use rubato::{FftFixedInOut, Resampler};
use tracing::error;
pub struct Audio {
#[allow(unused)]
stream: cpal::Stream,
sampler: FastFixedOut<f32>,
sampler: FftFixedInOut<f32>,
input_buffer: Vec<Vec<f32>>,
output_buffer: Vec<Vec<f32>>,
sample_sink: rtrb::Producer<f32>,
}
const VB_FREQUENCY: usize = 41700;
impl Audio {
pub fn init() -> Result<Self> {
let host = cpal::default_host();
@ -30,15 +28,7 @@ impl Audio {
bail!("No suitable output config available");
};
let mut config = config.with_max_sample_rate().config();
let resample_ratio = config.sample_rate.0 as f64 / VB_FREQUENCY as f64;
let chunk_size = (834.0 * resample_ratio) as usize;
let sampler = FastFixedOut::new(
resample_ratio,
64.0,
rubato::PolynomialDegree::Cubic,
chunk_size,
2,
)?;
let sampler = FftFixedInOut::new(41700, config.sample_rate.0 as usize, 834, 2)?;
config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32);
let input_buffer = sampler.input_buffer_allocate(true);
@ -111,10 +101,4 @@ impl Audio {
std::thread::sleep(Duration::from_micros(500));
}
}
pub fn set_speed(&mut self, speed: f64) -> Result<()> {
self.sampler
.set_resample_ratio_relative(1.0 / speed, false)?;
Ok(())
}
}

View File

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

View File

@ -5,9 +5,9 @@ use std::{
io::{Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
sync::{
Arc, Weak,
atomic::{AtomicBool, Ordering},
mpsc::{self, RecvError, TryRecvError},
Arc, Weak,
},
};
@ -22,7 +22,7 @@ use crate::{
graphics::TextureSink,
memory::{MemoryRange, MemoryRegion},
};
use shrooms_vb_core::{EXPECTED_FRAME_SIZE, Sim, StopReason};
use shrooms_vb_core::{Sim, StopReason, EXPECTED_FRAME_SIZE};
pub use shrooms_vb_core::{VBKey, VBRegister, VBWatchpointType};
mod address_set;
@ -43,13 +43,6 @@ impl SimId {
Self::Player2 => 1,
}
}
pub const fn from_index(index: usize) -> Option<Self> {
match index {
0 => Some(Self::Player1),
1 => Some(Self::Player2),
_ => None,
}
}
}
impl Display for SimId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -176,7 +169,6 @@ pub struct Emulator {
renderers: HashMap<SimId, TextureSink>,
messages: HashMap<SimId, mpsc::Sender<Toast>>,
debuggers: HashMap<SimId, DebugInfo>,
stdouts: HashMap<SimId, mpsc::Sender<String>>,
watched_regions: HashMap<MemoryRange, Weak<MemoryRegion>>,
eye_contents: Vec<u8>,
audio_samples: Vec<f32>,
@ -203,7 +195,6 @@ impl Emulator {
renderers: HashMap::new(),
messages: HashMap::new(),
debuggers: HashMap::new(),
stdouts: HashMap::new(),
watched_regions: HashMap::new(),
eye_contents: vec![0u8; 384 * 224 * 2],
audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE),
@ -237,15 +228,8 @@ impl Emulator {
let index = sim_id.to_index();
while self.sims.len() <= index {
let new_index = self.sims.len();
self.sims.push(Sim::new());
if self
.stdouts
.contains_key(&SimId::from_index(new_index).unwrap())
{
self.sims[new_index].watch_stdout(true);
}
self.sim_state[new_index].store(SimState::NoGame, Ordering::Release);
self.sim_state[index].store(SimState::NoGame, Ordering::Release);
}
let sim = &mut self.sims[index];
sim.reset();
@ -327,10 +311,6 @@ impl Emulator {
}
}
fn set_speed(&mut self, speed: f64) -> Result<()> {
self.audio.set_speed(speed)
}
fn save_sram(&mut self, sim_id: SimId) -> Result<()> {
let sim = self.sims.get_mut(sim_id.to_index());
let cart = self.carts[sim_id.to_index()].as_mut();
@ -487,20 +467,6 @@ impl Emulator {
self.state.store(EmulatorState::Paused, Ordering::Release);
}
// stdout
self.stdouts.retain(|sim_id, stdout| {
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return true;
};
if let Some(text) = sim.take_stdout() {
if stdout.send(text).is_err() {
sim.watch_stdout(false);
return false;
}
}
true
});
// Debug state
if state == EmulatorState::Debugging {
for sim_id in SimId::values() {
@ -601,11 +567,6 @@ impl Emulator {
EmulatorCommand::FrameAdvance => {
self.frame_advance();
}
EmulatorCommand::SetSpeed(speed) => {
if let Err(error) = self.set_speed(speed) {
self.report_error(SimId::Player1, format!("Error setting speed: {error}"));
}
}
EmulatorCommand::StartDebugging(sim_id, debugger) => {
self.start_debugging(sim_id, debugger);
}
@ -675,13 +636,6 @@ impl Emulator {
};
sim.remove_watchpoint(address, length, watch);
}
EmulatorCommand::WatchStdout(sim_id, stdout_sink) => {
self.stdouts.insert(sim_id, stdout_sink);
let Some(sim) = self.sims.get_mut(sim_id.to_index()) else {
return;
};
sim.watch_stdout(true);
}
EmulatorCommand::SetAudioEnabled(p1, p2) => {
self.audio_on[SimId::Player1.to_index()].store(p1, Ordering::Release);
self.audio_on[SimId::Player2.to_index()].store(p2, Ordering::Release);
@ -740,7 +694,6 @@ pub enum EmulatorCommand {
Pause,
Resume,
FrameAdvance,
SetSpeed(f64),
StartDebugging(SimId, DebugSender),
StopDebugging(SimId),
DebugInterrupt(SimId),
@ -755,7 +708,6 @@ pub enum EmulatorCommand {
RemoveBreakpoint(SimId, u32),
AddWatchpoint(SimId, u32, usize, VBWatchpointType),
RemoveWatchpoint(SimId, u32, usize, VBWatchpointType),
WatchStdout(SimId, mpsc::Sender<String>),
SetAudioEnabled(bool, bool),
Link,
Unlink,

View File

@ -1,6 +1,6 @@
use std::{ffi::c_void, ptr, slice};
use anyhow::{Result, anyhow};
use anyhow::{anyhow, Result};
use bitflags::bitflags;
use num_derive::{FromPrimitive, ToPrimitive};
use serde::{Deserialize, Serialize};
@ -91,7 +91,7 @@ type OnWrite = extern "C" fn(
) -> c_int;
#[link(name = "vb")]
unsafe extern "C" {
extern "C" {
#[link_name = "vbEmulate"]
fn vb_emulate(sim: *mut VB, cycles: *mut u32) -> c_int;
#[link_name = "vbEmulateEx"]
@ -170,7 +170,7 @@ unsafe extern "C" {
fn vb_write(sim: *mut VB, address: u32, _type: VBDataType, value: i32) -> i32;
}
#[unsafe(no_mangle)]
#[no_mangle]
extern "C" fn on_frame(sim: *mut VB) -> c_int {
// SAFETY: the *mut VB owns its userdata.
// 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
}
#[unsafe(no_mangle)]
#[no_mangle]
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.
// There is no way for the userdata to be null or otherwise invalid.
@ -196,10 +196,14 @@ extern "C" fn on_execute(sim: *mut VB, address: u32, _code: *const u16, _length:
stopped = true;
}
if stopped { 1 } else { 0 }
if stopped {
1
} else {
0
}
}
#[unsafe(no_mangle)]
#[no_mangle]
extern "C" fn on_read(
sim: *mut VB,
address: u32,
@ -225,12 +229,12 @@ extern "C" fn on_read(
0
}
#[unsafe(no_mangle)]
#[no_mangle]
extern "C" fn on_write(
sim: *mut VB,
address: u32,
_type: VBDataType,
value: *mut i32,
_value: *mut i32,
_cycles: *mut u32,
_cancel: *mut c_int,
) -> c_int {
@ -238,14 +242,6 @@ extern "C" fn on_write(
// There is no way for the userdata to be null or otherwise invalid.
let data: &mut VBState = unsafe { &mut *vb_get_user_data(sim).cast() };
// If we're monitoring stdout, track this write
if let Some(stdout) = data.stdout.as_mut() {
let normalized_hw_address = address & 0x0700003f;
if normalized_hw_address == 0x02000030 {
stdout.push(unsafe { *value } as u8);
}
}
if let Some(start) = data.write_watchpoints.start_of_range_containing(address) {
let watch = if data.read_watchpoints.contains(address) {
VBWatchpointType::Access
@ -271,7 +267,6 @@ struct VBState {
breakpoints: Vec<u32>,
read_watchpoints: AddressSet,
write_watchpoints: AddressSet,
stdout: Option<Vec<u8>>,
}
impl VBState {
@ -315,7 +310,6 @@ impl Sim {
breakpoints: vec![],
read_watchpoints: AddressSet::new(),
write_watchpoints: AddressSet::new(),
stdout: None,
};
unsafe { vb_set_user_data(sim, Box::into_raw(Box::new(state)).cast()) };
unsafe { vb_set_frame_callback(sim, Some(on_frame)) };
@ -574,9 +568,7 @@ impl Sim {
state.write_watchpoints.remove(address, length);
let needs_execute = state.needs_execute_callback();
if state.write_watchpoints.is_empty() {
if state.stdout.is_none() {
unsafe { vb_set_write_callback(self.sim, None) };
}
unsafe { vb_set_write_callback(self.sim, None) };
if !needs_execute {
unsafe { vb_set_execute_callback(self.sim, None) };
}
@ -598,40 +590,11 @@ impl Sim {
data.breakpoints.clear();
data.read_watchpoints.clear();
data.write_watchpoints.clear();
let needs_write = data.stdout.is_some();
unsafe { vb_set_read_callback(self.sim, None) };
if !needs_write {
unsafe { vb_set_write_callback(self.sim, None) };
}
unsafe { vb_set_write_callback(self.sim, None) };
unsafe { vb_set_execute_callback(self.sim, None) };
}
pub fn watch_stdout(&mut self, watch: bool) {
let data = self.get_state();
if watch {
if data.stdout.is_none() {
data.stdout = Some(vec![]);
unsafe { vb_set_write_callback(self.sim, Some(on_write)) };
}
} else {
data.stdout.take();
if data.write_watchpoints.is_empty() {
unsafe { vb_set_write_callback(self.sim, None) };
}
}
}
pub fn take_stdout(&mut self) -> Option<String> {
let data = self.get_state();
let stdout = data.stdout.take()?;
let string = match String::from_utf8(stdout) {
Ok(str) => str,
Err(err) => String::from_utf8_lossy(err.as_bytes()).into_owned(),
};
data.stdout = Some(vec![]);
Some(string)
}
pub fn stop_reason(&mut self) -> Option<StopReason> {
let data = self.get_state();
let reason = data.stop_reason.take();

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,12 @@
use std::{
cmp::Ordering,
collections::{HashMap, hash_map::Entry},
collections::{hash_map::Entry, HashMap},
fmt::Display,
str::FromStr,
sync::{Arc, Mutex, RwLock},
sync::{Arc, RwLock},
};
use anyhow::anyhow;
use egui::{Event, Key, KeyboardShortcut, Modifiers};
use gilrs::{Axis, Button, Gamepad, GamepadId, ev::Code};
use gilrs::{ev::Code, Axis, Button, Gamepad, GamepadId};
use serde::{Deserialize, Serialize};
use winit::keyboard::{KeyCode, PhysicalKey};
@ -456,308 +454,3 @@ struct PersistedGamepadMapping {
default_buttons: Vec<(Code, 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,
FastForward(u32),
Reset,
PauseResume,
// if you update this, update Command::all and add a default
}
impl Command {
pub fn all() -> [Self; 6] {
[
Self::OpenRom,
Self::Quit,
Self::PauseResume,
Self::Reset,
Self::FrameAdvance,
Self::FastForward(0),
]
}
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",
Self::FastForward(_) => "Fast Forward",
}
}
}
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.set(
Command::FastForward(0),
KeyboardShortcut::new(Modifiers::NONE, Key::Space),
);
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, saved: &mut PersistedSettings) {
for command in Command::all() {
let shortcut = self.by_command.get(&command).copied();
saved.shortcuts.push((command, shortcut));
}
}
}
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, Default)]
struct PersistedSettings {
shortcuts: Vec<(Command, Option<KeyboardShortcut>)>,
#[serde(default)]
ff_settings: FastForwardSettings,
}
#[derive(Default, Clone)]
struct ShortcutState {
ff_toggled: bool,
}
#[derive(Default)]
struct Settings {
shortcuts: Shortcuts,
ff_settings: FastForwardSettings,
state: ShortcutState,
}
impl Settings {
fn save(&self) -> PersistedSettings {
let mut saved = PersistedSettings {
shortcuts: vec![],
ff_settings: self.ff_settings.clone(),
};
self.shortcuts.save(&mut saved);
saved
}
}
#[derive(Clone)]
pub struct ShortcutProvider {
persistence: Persistence,
settings: Arc<Mutex<Settings>>,
}
impl ShortcutProvider {
pub fn new(persistence: Persistence) -> Self {
let mut settings = Settings::default();
if let Ok(saved) = persistence.load_config::<PersistedSettings>("shortcuts") {
for (command, shortcut) in saved.shortcuts {
if let Some(shortcut) = shortcut {
settings.shortcuts.set(command, shortcut);
} else {
settings.shortcuts.unset(command);
}
}
settings.ff_settings = saved.ff_settings;
};
Self {
persistence,
settings: Arc::new(Mutex::new(settings)),
}
}
pub fn shortcut_for(&self, command: Command) -> Option<KeyboardShortcut> {
let lock = self.settings.lock().unwrap();
lock.shortcuts.by_command.get(&command).copied()
}
pub fn ff_settings(&self) -> FastForwardSettings {
let lock = self.settings.lock().unwrap();
lock.ff_settings.clone()
}
pub fn consume_all(&self, input: &mut egui::InputState) -> Vec<Command> {
let mut lock = self.settings.lock().unwrap();
let mut state = lock.state.clone();
let mut consumed = vec![];
for (command, shortcut) in &lock.shortcuts.all {
input.events.retain(|event| {
let Event::Key {
key,
pressed,
repeat,
modifiers,
..
} = event
else {
return true;
};
if shortcut.logical_key != *key || !shortcut.modifiers.contains(*modifiers) {
return true;
}
if matches!(command, Command::FastForward(_)) {
if *repeat {
return true;
}
let sped_up = if lock.ff_settings.toggle {
if !*pressed {
return true;
}
state.ff_toggled = !state.ff_toggled;
state.ff_toggled
} else {
*pressed
};
let speed = if sped_up { lock.ff_settings.speed } else { 1 };
consumed.push(Command::FastForward(speed));
false
} else {
if !*pressed {
return true;
}
consumed.push(*command);
false
}
});
}
lock.state = state;
consumed
}
pub fn set(&self, command: Command, shortcut: KeyboardShortcut) {
let updated = {
let mut lock = self.settings.lock().unwrap();
lock.shortcuts.set(command, shortcut);
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
pub fn unset(&self, command: Command) {
let updated = {
let mut lock = self.settings.lock().unwrap();
lock.shortcuts.unset(command);
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
pub fn update_ff_settings(&self, ff_settings: FastForwardSettings) {
let updated = {
let mut lock = self.settings.lock().unwrap();
lock.ff_settings = ff_settings;
if !lock.ff_settings.toggle {
lock.state.ff_toggled = false;
}
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
pub fn reset(&self) {
let updated = {
let mut lock = self.settings.lock().unwrap();
*lock = Settings::default();
lock.save()
};
let _ = self.persistence.save_config("shortcuts", &updated);
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct FastForwardSettings {
pub toggle: bool,
pub speed: u32,
}
impl Default for FastForwardSettings {
fn default() -> Self {
Self {
toggle: false,
speed: 10,
}
}
}

View File

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

View File

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

View File

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

View File

@ -2,9 +2,7 @@ pub use about::AboutWindow;
use egui::{Context, ViewportBuilder, ViewportId};
pub use game::GameWindow;
pub use gdb::GdbServerWindow;
pub use hotkeys::HotkeysWindow;
pub use input::InputWindow;
pub use terminal::TerminalWindow;
pub use vip::{
BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow,
};
@ -16,9 +14,7 @@ mod about;
mod game;
mod game_screen;
mod gdb;
mod hotkeys;
mod input;
mod terminal;
mod utils;
mod vip;

View File

@ -3,21 +3,20 @@ use std::sync::mpsc;
use crate::{
app::UserEvent,
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
input::{Command, ShortcutProvider},
persistence::Persistence,
};
use egui::{
Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui, Vec2,
ViewportBuilder, ViewportCommand, ViewportId, Window, menu,
menu, Align2, Button, CentralPanel, Color32, Context, Direction, Frame, TopBottomPanel, Ui,
Vec2, ViewportBuilder, ViewportCommand, ViewportId, Window,
};
use egui_toast::{Toast, Toasts};
use serde::{Deserialize, Serialize};
use winit::event_loop::EventLoopProxy;
use super::{
AppWindow,
game_screen::{DisplayMode, GameScreen},
utils::UiExt as _,
AppWindow,
};
const COLOR_PRESETS: [[Color32; 2]; 3] = [
@ -39,7 +38,6 @@ pub struct GameWindow {
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
shortcuts: ShortcutProvider,
sim_id: SimId,
config: GameConfig,
screen: Option<GameScreen>,
@ -52,7 +50,6 @@ impl GameWindow {
client: EmulatorClient,
proxy: EventLoopProxy<UserEvent>,
persistence: Persistence,
shortcuts: ShortcutProvider,
sim_id: SimId,
) -> Self {
let config = load_config(&persistence, sim_id);
@ -60,7 +57,6 @@ impl GameWindow {
client,
proxy,
persistence,
shortcuts,
sim_id,
config,
screen: None,
@ -70,57 +66,8 @@ impl GameWindow {
}
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);
}
}
Command::FastForward(speed) => {
self.client
.send_command(EmulatorCommand::SetSpeed(speed as f64));
}
}
}
ui.menu_button("ROM", |ui| {
if ui
.add(self.button_for(ui.ctx(), "Open ROM", Command::OpenRom))
.clicked()
{
if ui.button("Open ROM").clicked() {
let rom = rfd::FileDialog::new()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
.pick_file();
@ -130,49 +77,33 @@ impl GameWindow {
}
ui.close_menu();
}
if ui
.add(self.button_for(ui.ctx(), "Quit", Command::Quit))
.clicked()
{
if ui.button("Quit").clicked() {
let _ = self.proxy.send_event(UserEvent::Quit(self.sim_id));
}
});
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;
let can_frame_advance = is_ready && state != EmulatorState::Debugging;
if state == EmulatorState::Running {
if ui
.add_enabled(
can_pause,
self.button_for(ui.ctx(), "Pause", Command::PauseResume),
)
.clicked()
{
if ui.add_enabled(can_pause, Button::new("Pause")).clicked() {
self.client.send_command(EmulatorCommand::Pause);
ui.close_menu();
}
} else if ui
.add_enabled(
can_resume,
self.button_for(ui.ctx(), "Resume", Command::PauseResume),
)
.clicked()
{
} else if ui.add_enabled(can_resume, Button::new("Resume")).clicked() {
self.client.send_command(EmulatorCommand::Resume);
ui.close_menu();
}
if ui
.add_enabled(is_ready, self.button_for(ui.ctx(), "Reset", Command::Reset))
.clicked()
{
if ui.add_enabled(is_ready, Button::new("Reset")).clicked() {
self.client
.send_command(EmulatorCommand::Reset(self.sim_id));
ui.close_menu();
}
ui.separator();
if ui
.add_enabled(
can_frame_advance,
self.button_for(ui.ctx(), "Frame Advance", Command::FrameAdvance),
)
.add_enabled(can_frame_advance, Button::new("Frame Advance"))
.clicked()
{
self.client.send_command(EmulatorCommand::FrameAdvance);
@ -204,12 +135,6 @@ impl GameWindow {
}
});
ui.menu_button("Tools", |ui| {
if ui.button("Terminal").clicked() {
self.proxy
.send_event(UserEvent::OpenTerminal(self.sim_id))
.unwrap();
ui.close_menu();
}
if ui.button("GDB Server").clicked() {
self.proxy
.send_event(UserEvent::OpenDebugger(self.sim_id))
@ -368,10 +293,6 @@ impl GameWindow {
ui.close_menu();
}
});
if ui.button("Hotkeys").clicked() {
self.proxy.send_event(UserEvent::OpenHotkeys).unwrap();
ui.close_menu();
}
}
fn show_color_picker(&mut self, ui: &mut Ui) {
@ -413,14 +334,6 @@ impl GameWindow {
}
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 {

View File

@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc};
use egui::{Color32, Rgba, Vec2, Widget};
use serde::{Deserialize, Serialize};
use wgpu::{BindGroup, BindGroupLayout, Buffer, RenderPipeline, util::DeviceExt as _};
use wgpu::{util::DeviceExt as _, BindGroup, BindGroupLayout, Buffer, RenderPipeline};
use crate::graphics::TextureSink;
@ -71,7 +71,7 @@ impl GameScreen {
module: &shader,
entry_point: Some(entry_point),
targets: &[Some(wgpu::ColorTargetState {
format: render_state.target_format,
format: wgpu::TextureFormat::Bgra8Unorm,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})],

View File

@ -1,150 +0,0 @@
use egui::{
Button, CentralPanel, Context, Event, KeyboardShortcut, Label, Layout, Slider, Ui,
ViewportBuilder, ViewportId,
};
use egui_extras::{Column, TableBuilder};
use crate::input::{Command, ShortcutProvider};
use super::{AppWindow, utils::UiExt};
pub struct HotkeysWindow {
shortcuts: ShortcutProvider,
now_binding: Option<Command>,
}
impl HotkeysWindow {
pub fn new(shortcuts: ShortcutProvider) -> Self {
Self {
shortcuts,
now_binding: None,
}
}
fn show_shortcuts(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.section("Shortcuts", |ui| {
let width = ui.available_width() - 16.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;
}
}
}
fn show_ff_settings(&mut self, ui: &mut Ui) {
let row_height = ui.spacing().interact_size.y;
ui.section("Fast Forward", |ui| {
let width = ui.available_width() - 8.0;
let mut ff_settings = self.shortcuts.ff_settings();
let mut updated = false;
TableBuilder::new(ui)
.column(Column::exact(width * 0.5))
.column(Column::exact(width * 0.5))
.body(|mut body| {
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Treat button as toggle");
});
row.col(|ui| {
if ui.checkbox(&mut ff_settings.toggle, "").changed() {
updated = true;
}
});
});
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("Speed multiplier");
});
row.col(|ui| {
if ui
.add_sized(
ui.available_size(),
Slider::new(&mut ff_settings.speed, 1..=15),
)
.changed()
{
updated = true;
}
});
});
});
if updated {
self.shortcuts.update_ff_settings(ff_settings);
}
});
}
}
impl AppWindow for HotkeysWindow {
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| {
ui.horizontal(|ui| {
if ui.button("Use defaults").clicked() {
self.shortcuts.reset();
}
});
ui.separator();
self.show_shortcuts(ui);
self.show_ff_settings(ui);
});
}
}

View File

@ -1,79 +0,0 @@
use std::{collections::VecDeque, sync::mpsc};
use egui::{
Align, CentralPanel, Context, FontFamily, Label, RichText, ScrollArea, ViewportBuilder,
ViewportId,
};
use crate::emulator::{EmulatorClient, EmulatorCommand, SimId};
use super::AppWindow;
const SCROLLBACK: usize = 1000;
pub struct TerminalWindow {
sim_id: SimId,
receiver: mpsc::Receiver<String>,
lines: VecDeque<String>,
}
impl TerminalWindow {
pub fn new(sim_id: SimId, client: &EmulatorClient) -> Self {
let (sender, receiver) = mpsc::channel();
client.send_command(EmulatorCommand::WatchStdout(sim_id, sender));
let mut lines = VecDeque::new();
lines.push_back(String::new());
Self {
sim_id,
receiver,
lines,
}
}
}
impl AppWindow for TerminalWindow {
fn viewport_id(&self) -> ViewportId {
ViewportId::from_hash_of(format!("terminal-{}", self.sim_id))
}
fn sim_id(&self) -> SimId {
self.sim_id
}
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Terminal ({})", self.sim_id))
.with_inner_size((640.0, 480.0))
}
fn show(&mut self, ctx: &Context) {
if let Ok(text) = self.receiver.try_recv() {
let mut rest = text.as_str();
while let Some(index) = rest.find('\n') {
let (line, lines) = rest.split_at(index);
let current = self.lines.back_mut().unwrap();
current.push_str(line);
self.lines.push_back(String::new());
if self.lines.len() > SCROLLBACK {
self.lines.pop_front();
}
rest = &lines[1..];
}
self.lines.back_mut().unwrap().push_str(rest);
}
CentralPanel::default().show(ctx, |ui| {
ScrollArea::vertical()
.stick_to_bottom(true)
.auto_shrink([false, false])
.animated(false)
.show(ui, |ui| {
for line in &self.lines {
let label = Label::new(RichText::new(line).family(FontFamily::Monospace))
.halign(Align::LEFT)
.wrap();
ui.add(label);
}
});
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -124,7 +124,7 @@ impl CellData {
}
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.vflip { 0x1000 } else { 0x0000 }
| (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) {
let p1 = (res.rect.min.x + x, res.rect.min.y).into();
let p2 = (res.rect.min.x + x, res.rect.max.y).into();
painter.line_segment([p1, p2], stroke);
painter.line(vec![p1, p2], stroke);
}
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 p2 = (res.rect.max.x, res.rect.min.y + y).into();
painter.line_segment([p1, p2], stroke);
painter.line(vec![p1, p2], stroke);
}
}
if let Some(selected) = self.selected {

View File

@ -6,8 +6,8 @@ use egui::{
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use fixed::{
FixedI32,
types::extra::{U3, U9},
FixedI32,
};
use num_derive::{FromPrimitive, ToPrimitive};
use num_traits::{FromPrimitive, ToPrimitive};
@ -17,12 +17,12 @@ use crate::{
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
memory::{MemoryClient, MemoryRef, MemoryView},
window::{
AppWindow,
utils::{NumberEdit, UiExt as _},
AppWindow,
},
};
use super::utils::{self, CellData, Object, shade};
use super::utils::{self, shade, CellData, Object};
pub struct WorldWindow {
sim_id: SimId,
@ -33,7 +33,6 @@ pub struct WorldWindow {
index: usize,
param_index: usize,
generic_palette: bool,
show_extents: bool,
params: ImageParams<WorldParams>,
scale: f32,
}
@ -58,7 +57,6 @@ impl WorldWindow {
index: params.index,
param_index: 0,
generic_palette: params.generic_palette,
show_extents: false,
params,
scale: 1.0,
}
@ -415,7 +413,6 @@ impl WorldWindow {
ui.add(slider);
});
ui.checkbox(&mut self.generic_palette, "Generic palette");
ui.checkbox(&mut self.show_extents, "Show extents");
});
});
self.params.write(WorldParams {
@ -429,82 +426,7 @@ impl WorldWindow {
let image = Image::new("vip://world")
.fit_to_original_size(self.scale)
.texture_options(TextureOptions::NEAREST);
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),
);
}
}
}
ui.add(image);
}
}
@ -520,7 +442,7 @@ impl AppWindow for WorldWindow {
fn initial_viewport(&self) -> ViewportBuilder {
ViewportBuilder::default()
.with_title(format!("Worlds ({})", self.sim_id))
.with_inner_size((640.0, 520.0))
.with_inner_size((640.0, 500.0))
}
fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) {
@ -948,7 +870,7 @@ impl WorldHeader {
let new_value = (*source & 0x0030)
| if self.lon { 0x8000 } 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.scy as u16) << 8)
| if self.over { 0x0080 } else { 0x0000 }