Compare commits
13 Commits
Author | SHA1 | Date |
---|---|---|
|
0a8da6bff0 | |
|
73e8234b5b | |
|
7b590dde14 | |
|
73ccc4054a | |
|
efcffed848 | |
|
58bb34e3cc | |
|
132d494cc2 | |
|
30ee8c06f7 | |
|
1c146590fe | |
|
d610e3de14 | |
|
f7f112f7c1 | |
|
db482b5c21 | |
|
3e90d7df6d |
|
@ -2165,7 +2165,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lemur"
|
name = "lemur"
|
||||||
version = "0.8.1"
|
version = "0.9.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"atoi",
|
"atoi",
|
||||||
|
|
|
@ -4,7 +4,7 @@ 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.8.1"
|
version = "0.9.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
|
@ -41,6 +41,7 @@ ENV PATH="/osxcross/bin:$PATH" \
|
||||||
SHROOMS_CFLAGS_x86_64-unknown-linux-gnu="-flto" \
|
SHROOMS_CFLAGS_x86_64-unknown-linux-gnu="-flto" \
|
||||||
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
|
CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
|
||||||
SHROOMS_CFLAGS_x86_64-pc-windows-msvc="-flto" \
|
SHROOMS_CFLAGS_x86_64-pc-windows-msvc="-flto" \
|
||||||
|
CFLAGS_x86_64-pc-windows-msvc="-I/xwin/crt/include -I/xwin/sdk/include/ucrt -I/xwin/sdk/include/um -I/xwin/sdk/include/shared" \
|
||||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64 -Clinker-plugin-lto -Clink-arg=-fuse-ld=lld" \
|
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64 -Clinker-plugin-lto -Clink-arg=-fuse-ld=lld" \
|
||||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-20" \
|
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-20" \
|
||||||
CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \
|
CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \
|
||||||
|
|
4
build.rs
4
build.rs
|
@ -18,6 +18,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
};
|
};
|
||||||
builder
|
builder
|
||||||
.include(Path::new("shrooms-vb-core/core"))
|
.include(Path::new("shrooms-vb-core/core"))
|
||||||
|
.include(Path::new("shrooms-vb-core/util"))
|
||||||
.opt_level(opt_level)
|
.opt_level(opt_level)
|
||||||
.flag_if_supported("-fno-strict-aliasing")
|
.flag_if_supported("-fno-strict-aliasing")
|
||||||
.define("VB_LITTLE_ENDIAN", None)
|
.define("VB_LITTLE_ENDIAN", None)
|
||||||
|
@ -29,7 +30,10 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
.define("VB_DIRECT_FRAME", "on_frame")
|
.define("VB_DIRECT_FRAME", "on_frame")
|
||||||
.define("VB_DIRECT_READ", "on_read")
|
.define("VB_DIRECT_READ", "on_read")
|
||||||
.define("VB_DIRECT_WRITE", "on_write")
|
.define("VB_DIRECT_WRITE", "on_write")
|
||||||
|
.define("VBU_REALLOC", "vbu_realloc_shim")
|
||||||
.file(Path::new("shrooms-vb-core/core/vb.c"))
|
.file(Path::new("shrooms-vb-core/core/vb.c"))
|
||||||
|
.file(Path::new("shrooms-vb-core/util/isx.c"))
|
||||||
|
.file(Path::new("shrooms-vb-core/util/vbu.c"))
|
||||||
.compile("vb");
|
.compile("vb");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 8598eab087cced12b92a411f6c7f2eb9de51310f
|
Subproject commit 4ed3b7299507b8ea0079a0965f33b0c8a6886572
|
12
src/app.rs
12
src/app.rs
|
@ -24,8 +24,8 @@ use crate::{
|
||||||
persistence::Persistence,
|
persistence::Persistence,
|
||||||
window::{
|
window::{
|
||||||
AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow,
|
AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GameWindow,
|
||||||
GdbServerWindow, HotkeysWindow, InputWindow, ObjectWindow, ProfileWindow, RegisterWindow,
|
GdbServerWindow, HotkeysWindow, InitArgs, InputWindow, ObjectWindow, ProfileWindow,
|
||||||
TerminalWindow, WorldWindow,
|
RegisterWindow, TerminalWindow, WorldWindow,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -423,7 +423,13 @@ impl Viewport {
|
||||||
let (window, state) = create_window_and_state(&ctx, event_loop, &builder, &mut painter);
|
let (window, state) = create_window_and_state(&ctx, event_loop, &builder, &mut painter);
|
||||||
egui_winit::update_viewport_info(&mut info, &ctx, &window, true);
|
egui_winit::update_viewport_info(&mut info, &ctx, &window, true);
|
||||||
|
|
||||||
app.on_init(&ctx, painter.render_state().as_ref().unwrap());
|
let render_state = painter.render_state();
|
||||||
|
let args = InitArgs {
|
||||||
|
ctx: &ctx,
|
||||||
|
window: &window,
|
||||||
|
render_state: render_state.as_ref().unwrap(),
|
||||||
|
};
|
||||||
|
app.on_init(args);
|
||||||
Self {
|
Self {
|
||||||
painter,
|
painter,
|
||||||
ctx,
|
ctx,
|
||||||
|
|
|
@ -33,6 +33,7 @@ mod cart;
|
||||||
mod game_info;
|
mod game_info;
|
||||||
mod inline_stack_map;
|
mod inline_stack_map;
|
||||||
mod shrooms_vb_core;
|
mod shrooms_vb_core;
|
||||||
|
mod shrooms_vb_util;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
|
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
|
||||||
pub enum SimId {
|
pub enum SimId {
|
||||||
|
|
|
@ -7,7 +7,7 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::emulator::{SimId, game_info::GameInfo};
|
use crate::emulator::{SimId, game_info::GameInfo, shrooms_vb_util::rom_from_isx};
|
||||||
|
|
||||||
pub struct Cart {
|
pub struct Cart {
|
||||||
pub file_path: PathBuf,
|
pub file_path: PathBuf,
|
||||||
|
@ -20,8 +20,9 @@ pub struct Cart {
|
||||||
impl Cart {
|
impl Cart {
|
||||||
pub fn load(file_path: &Path, sim_id: SimId) -> Result<Self> {
|
pub fn load(file_path: &Path, sim_id: SimId) -> Result<Self> {
|
||||||
let rom = fs::read(file_path)?;
|
let rom = fs::read(file_path)?;
|
||||||
let (rom, info) =
|
let (rom, info) = try_parse_isx(file_path, &rom)
|
||||||
try_parse_elf(file_path, &rom).unwrap_or_else(|| (rom, GameInfo::empty(file_path)));
|
.or_else(|| try_parse_elf(file_path, &rom))
|
||||||
|
.unwrap_or_else(|| (rom, GameInfo::empty(file_path)));
|
||||||
|
|
||||||
let mut sram_file = File::options()
|
let mut sram_file = File::options()
|
||||||
.read(true)
|
.read(true)
|
||||||
|
@ -60,6 +61,12 @@ impl Cart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn try_parse_isx(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
|
||||||
|
let rom = rom_from_isx(data)?;
|
||||||
|
let info = GameInfo::from_isx(file_path, data);
|
||||||
|
Some((rom, info))
|
||||||
|
}
|
||||||
|
|
||||||
fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
|
fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
|
||||||
use object::read::elf::FileHeader;
|
use object::read::elf::FileHeader;
|
||||||
let program = match object::FileKind::parse(data).ok()? {
|
let program = match object::FileKind::parse(data).ok()? {
|
||||||
|
@ -73,7 +80,7 @@ fn try_parse_elf(file_path: &Path, data: &[u8]) -> Option<(Vec<u8>, GameInfo)> {
|
||||||
}
|
}
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
let info = GameInfo::new(file_path, data).unwrap_or_else(|_| GameInfo::empty(file_path));
|
let info = GameInfo::from_elf(file_path, data).unwrap_or_else(|_| GameInfo::empty(file_path));
|
||||||
Some((program, info))
|
Some((program, info))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ pub struct GameInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameInfo {
|
impl GameInfo {
|
||||||
pub fn new(file_path: &Path, input: &[u8]) -> Result<Self> {
|
pub fn from_elf(file_path: &Path, input: &[u8]) -> Result<Self> {
|
||||||
let file = object::File::parse(input)?;
|
let file = object::File::parse(input)?;
|
||||||
|
|
||||||
let (name, path) = name_and_path(file_path);
|
let (name, path) = name_and_path(file_path);
|
||||||
|
@ -52,6 +52,26 @@ impl GameInfo {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_isx(file_path: &Path, input: &[u8]) -> Self {
|
||||||
|
let (name, path) = name_and_path(file_path);
|
||||||
|
let symbols = extract_isx_symbols(input);
|
||||||
|
let library_info = LibraryInfo {
|
||||||
|
name: name.clone(),
|
||||||
|
debug_name: name,
|
||||||
|
path: path.clone(),
|
||||||
|
debug_path: path,
|
||||||
|
debug_id: DebugId::default(),
|
||||||
|
code_id: None,
|
||||||
|
arch: None,
|
||||||
|
symbol_table: symbols.map(|syms| Arc::new(SymbolTable::new(syms))),
|
||||||
|
};
|
||||||
|
let inline_stack_map = InlineStackMap::empty();
|
||||||
|
Self {
|
||||||
|
library_info,
|
||||||
|
inline_stack_map,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn empty(file_path: &Path) -> Self {
|
pub fn empty(file_path: &Path) -> Self {
|
||||||
let (name, path) = name_and_path(file_path);
|
let (name, path) = name_and_path(file_path);
|
||||||
let library_info = LibraryInfo {
|
let library_info = LibraryInfo {
|
||||||
|
@ -115,6 +135,66 @@ fn build_inline_stack_map(file: object::File) -> Result<InlineStackMap> {
|
||||||
Ok(frames.build())
|
Ok(frames.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn extract_isx_symbols(input: &[u8]) -> Option<Vec<Symbol>> {
|
||||||
|
let mut syms = vec![];
|
||||||
|
let (_, mut buf) = input.split_at_checked(32)?;
|
||||||
|
while !buf.is_empty() {
|
||||||
|
let typ;
|
||||||
|
(typ, buf) = buf.split_first()?;
|
||||||
|
match typ {
|
||||||
|
0x11 => {
|
||||||
|
// Code (Virtual Boy)
|
||||||
|
(_, buf) = buf.split_at_checked(4)?;
|
||||||
|
let len_bytes;
|
||||||
|
(len_bytes, buf) = buf.split_first_chunk()?;
|
||||||
|
let len = u32::from_le_bytes(*len_bytes);
|
||||||
|
(_, buf) = buf.split_at_checked(len as usize)?;
|
||||||
|
}
|
||||||
|
0x13 => {
|
||||||
|
// Range (Virtual Boy)
|
||||||
|
let count_bytes;
|
||||||
|
(count_bytes, buf) = buf.split_first_chunk()?;
|
||||||
|
let count = u16::from_le_bytes(*count_bytes);
|
||||||
|
(_, buf) = buf.split_at_checked(count as usize * 9)?;
|
||||||
|
}
|
||||||
|
0x14 => {
|
||||||
|
// Symbol (Virtual Boy)
|
||||||
|
let count_bytes;
|
||||||
|
(count_bytes, buf) = buf.split_first_chunk()?;
|
||||||
|
let count = u16::from_le_bytes(*count_bytes);
|
||||||
|
for _ in 0..count {
|
||||||
|
let name_len;
|
||||||
|
(name_len, buf) = buf.split_first()?;
|
||||||
|
let name_bytes;
|
||||||
|
(name_bytes, buf) = buf.split_at_checked(*name_len as usize)?;
|
||||||
|
(_, buf) = buf.split_at_checked(2)?;
|
||||||
|
let address_bytes;
|
||||||
|
(address_bytes, buf) = buf.split_first_chunk()?;
|
||||||
|
|
||||||
|
let name_str = String::from_utf8_lossy(name_bytes);
|
||||||
|
let address = u32::from_le_bytes(*address_bytes);
|
||||||
|
syms.push(Symbol {
|
||||||
|
address: address & 0x07ffffff,
|
||||||
|
size: Some(4),
|
||||||
|
name: demangle_any(&name_str),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0x20..=0x22 => {
|
||||||
|
// System (undocumented)
|
||||||
|
let length_bytes;
|
||||||
|
(length_bytes, buf) = buf.split_first_chunk()?;
|
||||||
|
let length = u32::from_le_bytes(*length_bytes);
|
||||||
|
(_, buf) = buf.split_at_checked(length as usize)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(syms)
|
||||||
|
}
|
||||||
|
|
||||||
type Reader<'a> = gimli::EndianSlice<'a, gimli::RunTimeEndian>;
|
type Reader<'a> = gimli::EndianSlice<'a, gimli::RunTimeEndian>;
|
||||||
|
|
||||||
struct ParseContext<'a> {
|
struct ParseContext<'a> {
|
||||||
|
@ -173,8 +253,8 @@ fn parse_inline(ctx: &mut ParseContext, node: gimli::EntriesTreeNode<Reader>) ->
|
||||||
let name = Arc::new(name);
|
let name = Arc::new(name);
|
||||||
let mut ranges = ctx.dorf.die_ranges(ctx.unit, node.entry())?;
|
let mut ranges = ctx.dorf.die_ranges(ctx.unit, node.entry())?;
|
||||||
while let Some(range) = ranges.next()? {
|
while let Some(range) = ranges.next()? {
|
||||||
let start = range.begin as u32;
|
let start = range.begin as u32 & 0x07ffffff;
|
||||||
let end = range.end as u32;
|
let end = range.end as u32 & 0x07ffffff;
|
||||||
ctx.frames.add(start, end, name.clone());
|
ctx.frames.add(start, end, name.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -261,7 +261,7 @@ extern "C" fn on_exception(sim: *mut VB, cause: *mut u16) -> c_int {
|
||||||
};
|
};
|
||||||
data.monitor.event = data.monitor.queued_event.take();
|
data.monitor.event = data.monitor.queued_event.take();
|
||||||
data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(pc);
|
data.monitor.new_inline_stack = data.monitor.detect_new_inline_stack(pc);
|
||||||
data.monitor.queued_event = Some(SimEvent::Interrupt(cause, pc));
|
data.monitor.queued_event = Some(SimEvent::Interrupt(cause, pc & 0x07ffffff));
|
||||||
unsafe { vb_set_exception_callback(sim, None) };
|
unsafe { vb_set_exception_callback(sim, None) };
|
||||||
if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() {
|
if data.monitor.event.is_some() || data.monitor.new_inline_stack.is_some() {
|
||||||
1
|
1
|
||||||
|
@ -439,7 +439,9 @@ impl EventMonitor {
|
||||||
// JAL .+4 is how programs get r31 to a known value for indirect calls
|
// JAL .+4 is how programs get r31 to a known value for indirect calls
|
||||||
// (which we detect later.)
|
// (which we detect later.)
|
||||||
// Any other JAL is a function call.
|
// Any other JAL is a function call.
|
||||||
return Some(SimEvent::Call(address.wrapping_add_signed(disp)));
|
return Some(SimEvent::Call(
|
||||||
|
address.wrapping_add_signed(disp) & 0x07ffffff,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,7 +455,7 @@ impl EventMonitor {
|
||||||
if r31 as u32 == address.wrapping_add(2) {
|
if r31 as u32 == address.wrapping_add(2) {
|
||||||
// JMP anywhere else, if r31 points to after the JMP, is an indirect call
|
// JMP anywhere else, if r31 points to after the JMP, is an indirect call
|
||||||
let target = unsafe { vb_get_program_register(sim, jmp_reg as u32) };
|
let target = unsafe { vb_get_program_register(sim, jmp_reg as u32) };
|
||||||
return Some(SimEvent::Call(target as u32));
|
return Some(SimEvent::Call(target as u32 & 0x07ffffff));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
unsafe fn vbu_realloc_shim(ptr: *mut c_void, new_size: usize) -> *mut c_void {
|
||||||
|
if !ptr.is_null() {
|
||||||
|
// not supporting proper realloc because it needs bookkeeping and it's unnecessary
|
||||||
|
return std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
let allocation = vec![0u8; new_size].into_boxed_slice();
|
||||||
|
Box::into_raw(allocation).cast()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[link(name = "vb")]
|
||||||
|
unsafe extern "C" {
|
||||||
|
#[link_name = "vbuFromISX"]
|
||||||
|
fn vbu_from_isx(bytes: *const c_void, length: usize, rom_length: *mut usize) -> *mut c_void;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rom_from_isx(bytes: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
if !bytes.starts_with(b"ISX") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut rom_length = 0;
|
||||||
|
let raw_rom = unsafe { vbu_from_isx(bytes.as_ptr().cast(), bytes.len(), &mut rom_length) };
|
||||||
|
if raw_rom.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// SAFETY: the rom was allocated by vbu_realloc_shim, which created it from a Vec<u8>.
|
||||||
|
let rom = unsafe { Vec::from_raw_parts(raw_rom.cast(), rom_length, rom_length) };
|
||||||
|
Some(rom)
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use about::AboutWindow;
|
pub use about::AboutWindow;
|
||||||
use egui::{Context, ViewportBuilder, ViewportId};
|
use egui::{Context, ViewportBuilder, ViewportId};
|
||||||
pub use game::GameWindow;
|
pub use game::GameWindow;
|
||||||
|
@ -9,7 +11,7 @@ pub use terminal::TerminalWindow;
|
||||||
pub use vip::{
|
pub use vip::{
|
||||||
BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow,
|
BgMapWindow, CharacterDataWindow, FrameBufferWindow, ObjectWindow, RegisterWindow, WorldWindow,
|
||||||
};
|
};
|
||||||
use winit::event::KeyEvent;
|
use winit::{event::KeyEvent, window::Window};
|
||||||
|
|
||||||
use crate::emulator::SimId;
|
use crate::emulator::SimId;
|
||||||
|
|
||||||
|
@ -31,9 +33,8 @@ pub trait AppWindow {
|
||||||
}
|
}
|
||||||
fn initial_viewport(&self) -> ViewportBuilder;
|
fn initial_viewport(&self) -> ViewportBuilder;
|
||||||
fn show(&mut self, ctx: &Context);
|
fn show(&mut self, ctx: &Context);
|
||||||
fn on_init(&mut self, ctx: &Context, render_state: &egui_wgpu::RenderState) {
|
fn on_init(&mut self, args: InitArgs) {
|
||||||
let _ = ctx;
|
let _ = args;
|
||||||
let _ = render_state;
|
|
||||||
}
|
}
|
||||||
fn on_destroy(&mut self) {}
|
fn on_destroy(&mut self) {}
|
||||||
fn handle_key_event(&mut self, event: &KeyEvent) -> bool {
|
fn handle_key_event(&mut self, event: &KeyEvent) -> bool {
|
||||||
|
@ -45,3 +46,9 @@ pub trait AppWindow {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct InitArgs<'a> {
|
||||||
|
pub ctx: &'a Context,
|
||||||
|
pub window: &'a Arc<Window>,
|
||||||
|
pub render_state: &'a egui_wgpu::RenderState,
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
use std::{sync::mpsc, time::Duration};
|
use std::{
|
||||||
|
sync::{Arc, mpsc},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::UserEvent,
|
app::UserEvent,
|
||||||
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
|
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
|
||||||
input::{Command, ShortcutProvider},
|
input::{Command, ShortcutProvider},
|
||||||
persistence::Persistence,
|
persistence::Persistence,
|
||||||
|
window::InitArgs,
|
||||||
};
|
};
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use egui::{
|
use egui::{
|
||||||
|
@ -47,6 +51,7 @@ pub struct GameWindow {
|
||||||
screen: Option<GameScreen>,
|
screen: Option<GameScreen>,
|
||||||
messages: Option<mpsc::Receiver<Toast>>,
|
messages: Option<mpsc::Receiver<Toast>>,
|
||||||
color_picker: Option<ColorPickerState>,
|
color_picker: Option<ColorPickerState>,
|
||||||
|
window: Option<Arc<winit::window::Window>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameWindow {
|
impl GameWindow {
|
||||||
|
@ -73,6 +78,7 @@ impl GameWindow {
|
||||||
screen: None,
|
screen: None,
|
||||||
messages: None,
|
messages: None,
|
||||||
color_picker: None,
|
color_picker: None,
|
||||||
|
window: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +93,7 @@ impl GameWindow {
|
||||||
match command {
|
match command {
|
||||||
Command::OpenRom => {
|
Command::OpenRom => {
|
||||||
let rom = rfd::FileDialog::new()
|
let rom = rfd::FileDialog::new()
|
||||||
.add_filter("Virtual Boy ROMs", &["vb", "vbrom"])
|
.add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"])
|
||||||
.pick_file();
|
.pick_file();
|
||||||
if let Some(path) = rom {
|
if let Some(path) = rom {
|
||||||
self.client
|
self.client
|
||||||
|
@ -139,7 +145,7 @@ impl GameWindow {
|
||||||
.clicked()
|
.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", "elf", "isx"])
|
||||||
.pick_file();
|
.pick_file();
|
||||||
if let Some(path) = rom {
|
if let Some(path) = rom {
|
||||||
self.client
|
self.client
|
||||||
|
@ -298,10 +304,13 @@ impl GameWindow {
|
||||||
self.client
|
self.client
|
||||||
.send_command(EmulatorCommand::Screenshot(self.sim_id, tx));
|
.send_command(EmulatorCommand::Screenshot(self.sim_id, tx));
|
||||||
let bytes = rx.await.context("Could not take screenshot")?;
|
let bytes = rx.await.context("Could not take screenshot")?;
|
||||||
let file = rfd::FileDialog::new()
|
let mut file_dialog = rfd::FileDialog::new()
|
||||||
.add_filter("PNG images", &["png"])
|
.add_filter("PNG images", &["png"])
|
||||||
.set_file_name("screenshot.png")
|
.set_file_name("screenshot.png");
|
||||||
.save_file();
|
if let Some(window) = self.window.as_ref() {
|
||||||
|
file_dialog = file_dialog.set_parent(window);
|
||||||
|
}
|
||||||
|
let file = file_dialog.save_file();
|
||||||
let Some(path) = file else {
|
let Some(path) = file else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
@ -545,8 +554,8 @@ impl AppWindow for GameWindow {
|
||||||
self.toasts.show(ctx);
|
self.toasts.show(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_init(&mut self, _ctx: &Context, render_state: &egui_wgpu::RenderState) {
|
fn on_init(&mut self, args: InitArgs) {
|
||||||
let (screen, sink) = GameScreen::init(render_state);
|
let (screen, sink) = GameScreen::init(args.render_state);
|
||||||
let (message_sink, message_source) = mpsc::channel();
|
let (message_sink, message_source) = mpsc::channel();
|
||||||
self.client.send_command(EmulatorCommand::ConnectToSim(
|
self.client.send_command(EmulatorCommand::ConnectToSim(
|
||||||
self.sim_id,
|
self.sim_id,
|
||||||
|
@ -555,6 +564,7 @@ impl AppWindow for GameWindow {
|
||||||
));
|
));
|
||||||
self.screen = Some(screen);
|
self.screen = Some(screen);
|
||||||
self.messages = Some(message_source);
|
self.messages = Some(message_source);
|
||||||
|
self.window = Some(args.window.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_destroy(&mut self) {
|
fn on_destroy(&mut self) {
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
use std::{fs, time::Duration};
|
use std::{fs, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use egui::{Button, CentralPanel, Checkbox, Label, ViewportBuilder, ViewportId};
|
use egui::{Button, CentralPanel, Checkbox, Label, ViewportBuilder, ViewportId};
|
||||||
use egui_notify::{Anchor, Toast, Toasts};
|
use egui_notify::{Anchor, Toast, Toasts};
|
||||||
|
use winit::window::Window;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId},
|
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId},
|
||||||
profiler::{Profiler, ProfilerStatus},
|
profiler::{Profiler, ProfilerStatus},
|
||||||
window::AppWindow,
|
window::{AppWindow, InitArgs},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ProfileWindow {
|
pub struct ProfileWindow {
|
||||||
|
@ -15,6 +16,7 @@ pub struct ProfileWindow {
|
||||||
client: EmulatorClient,
|
client: EmulatorClient,
|
||||||
profiler: Profiler,
|
profiler: Profiler,
|
||||||
toasts: Toasts,
|
toasts: Toasts,
|
||||||
|
window: Option<Arc<Window>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProfileWindow {
|
impl ProfileWindow {
|
||||||
|
@ -27,6 +29,7 @@ impl ProfileWindow {
|
||||||
.with_anchor(Anchor::BottomLeft)
|
.with_anchor(Anchor::BottomLeft)
|
||||||
.with_margin((10.0, 10.0).into())
|
.with_margin((10.0, 10.0).into())
|
||||||
.reverse(true),
|
.reverse(true),
|
||||||
|
window: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,12 +62,16 @@ impl ProfileWindow {
|
||||||
|
|
||||||
fn try_finish_recording(&mut self) -> Result<Option<String>> {
|
fn try_finish_recording(&mut self) -> Result<Option<String>> {
|
||||||
let bytes_receiver = self.profiler.finish_recording();
|
let bytes_receiver = self.profiler.finish_recording();
|
||||||
let file = rfd::FileDialog::new()
|
let mut file_dialog = rfd::FileDialog::new()
|
||||||
.add_filter("Profiler files", &["json"])
|
.add_filter("Profiler files", &["json"])
|
||||||
.set_file_name("profile.json")
|
.set_file_name("profile.json");
|
||||||
.save_file();
|
if let Some(window) = self.window.as_ref() {
|
||||||
|
file_dialog = file_dialog.set_parent(window);
|
||||||
|
}
|
||||||
|
let file = file_dialog.save_file();
|
||||||
if let Some(path) = file {
|
if let Some(path) = file {
|
||||||
let bytes = pollster::block_on(bytes_receiver)?;
|
let bytes = pollster::block_on(bytes_receiver)?;
|
||||||
|
let _ = fs::remove_file(&path);
|
||||||
fs::write(&path, bytes)?;
|
fs::write(&path, bytes)?;
|
||||||
Ok(Some(path.display().to_string()))
|
Ok(Some(path.display().to_string()))
|
||||||
} else {
|
} else {
|
||||||
|
@ -159,4 +166,8 @@ impl AppWindow for ProfileWindow {
|
||||||
});
|
});
|
||||||
self.toasts.show(ctx);
|
self.toasts.show(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_init(&mut self, args: InitArgs) {
|
||||||
|
self.window = Some(args.window.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
||||||
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
|
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
|
||||||
memory::{MemoryClient, MemoryView},
|
memory::{MemoryClient, MemoryView},
|
||||||
window::{
|
window::{
|
||||||
AppWindow,
|
AppWindow, InitArgs,
|
||||||
utils::{NumberEdit, UiExt},
|
utils::{NumberEdit, UiExt},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -187,8 +187,8 @@ impl AppWindow for BgMapWindow {
|
||||||
.with_inner_size((640.0, 480.0))
|
.with_inner_size((640.0, 480.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) {
|
fn on_init(&mut self, args: InitArgs) {
|
||||||
ctx.add_texture_loader(self.loader.clone());
|
args.ctx.add_texture_loader(self.loader.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&mut self, ctx: &Context) {
|
fn show(&mut self, ctx: &Context) {
|
||||||
|
|
|
@ -12,7 +12,7 @@ use crate::{
|
||||||
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
|
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
|
||||||
memory::{MemoryClient, MemoryView},
|
memory::{MemoryClient, MemoryView},
|
||||||
window::{
|
window::{
|
||||||
AppWindow,
|
AppWindow, InitArgs,
|
||||||
utils::{NumberEdit, UiExt as _},
|
utils::{NumberEdit, UiExt as _},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -251,8 +251,8 @@ impl AppWindow for CharacterDataWindow {
|
||||||
.with_inner_size((640.0, 480.0))
|
.with_inner_size((640.0, 480.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) {
|
fn on_init(&mut self, args: InitArgs) {
|
||||||
ctx.add_texture_loader(self.loader.clone());
|
args.ctx.add_texture_loader(self.loader.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&mut self, ctx: &Context) {
|
fn show(&mut self, ctx: &Context) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
||||||
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
|
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
|
||||||
memory::{MemoryClient, MemoryView},
|
memory::{MemoryClient, MemoryView},
|
||||||
window::{
|
window::{
|
||||||
AppWindow,
|
AppWindow, InitArgs,
|
||||||
utils::{NumberEdit, UiExt as _},
|
utils::{NumberEdit, UiExt as _},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -153,8 +153,8 @@ impl AppWindow for FrameBufferWindow {
|
||||||
.with_inner_size((640.0, 480.0))
|
.with_inner_size((640.0, 480.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) {
|
fn on_init(&mut self, args: InitArgs) {
|
||||||
ctx.add_texture_loader(self.loader.clone());
|
args.ctx.add_texture_loader(self.loader.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&mut self, ctx: &Context) {
|
fn show(&mut self, ctx: &Context) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
||||||
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
|
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
|
||||||
memory::{MemoryClient, MemoryView},
|
memory::{MemoryClient, MemoryView},
|
||||||
window::{
|
window::{
|
||||||
AppWindow,
|
AppWindow, InitArgs,
|
||||||
utils::{NumberEdit, UiExt as _},
|
utils::{NumberEdit, UiExt as _},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -208,8 +208,8 @@ impl AppWindow for ObjectWindow {
|
||||||
.with_inner_size((640.0, 500.0))
|
.with_inner_size((640.0, 500.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_init(&mut self, ctx: &Context, _render_state: &egui_wgpu::RenderState) {
|
fn on_init(&mut self, args: InitArgs) {
|
||||||
ctx.add_texture_loader(self.loader.clone());
|
args.ctx.add_texture_loader(self.loader.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&mut self, ctx: &Context) {
|
fn show(&mut self, ctx: &Context) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ use crate::{
|
||||||
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
|
images::{ImageBuffer, ImageParams, ImageProcessor, ImageRenderer, ImageTextureLoader},
|
||||||
memory::{MemoryClient, MemoryRef, MemoryView},
|
memory::{MemoryClient, MemoryRef, MemoryView},
|
||||||
window::{
|
window::{
|
||||||
AppWindow,
|
AppWindow, InitArgs,
|
||||||
utils::{NumberEdit, UiExt as _},
|
utils::{NumberEdit, UiExt as _},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -523,8 +523,8 @@ impl AppWindow for WorldWindow {
|
||||||
.with_inner_size((640.0, 520.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, args: InitArgs) {
|
||||||
ctx.add_texture_loader(self.loader.clone());
|
args.ctx.add_texture_loader(self.loader.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show(&mut self, ctx: &Context) {
|
fn show(&mut self, ctx: &Context) {
|
||||||
|
|
Loading…
Reference in New Issue