Compare commits
8
Commits
55d3cf737d
...
v0.16.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af08badf45 | ||
|
|
3aacb84615 | ||
|
|
3cf95772f5 | ||
|
|
833214b759 | ||
|
|
6e08830b43 | ||
|
|
8083bc8069 | ||
|
|
63d923ad10 | ||
|
|
bdec3baa74 |
Generated
+1
-1
@@ -2040,7 +2040,7 @@ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
|
||||
|
||||
[[package]]
|
||||
name = "lemur"
|
||||
version = "0.15.2"
|
||||
version = "0.16.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"atoi",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ description = "An emulator for the Virtual Boy."
|
||||
repository = "https://git.virtual-boy.com/PVB/lemur"
|
||||
publish = false
|
||||
license = "MIT"
|
||||
version = "0.15.2"
|
||||
version = "0.16.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ RUN apt-get update && \
|
||||
ln -s $(which ld64.lld-23) /usr/bin/ld64.lld && \
|
||||
SDK_VERSION=14.5 UNATTENDED=1 ENABLE_COMPILER_RT_INSTALL=1 TARGET_DIR=/osxcross ./build.sh
|
||||
|
||||
FROM rust:1.99-trixie
|
||||
FROM rust:1.99.1-trixie
|
||||
ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc
|
||||
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
|
||||
RUN rustup target add x86_64-pc-windows-msvc && \
|
||||
@@ -24,7 +24,7 @@ RUN rustup target add x86_64-pc-windows-msvc && \
|
||||
rustup target add aarch64-apple-darwin && \
|
||||
apt-get update && \
|
||||
apt-get install -y clang-23 lld-23 libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \
|
||||
cargo install cargo-bundle xwin && \
|
||||
cargo install cargo-bundle xwin --locked && \
|
||||
xwin --accept-license splat --output xwin && \
|
||||
rm -rf .xwin-cache && \
|
||||
ln -s $(which clang-23) /usr/bin/clang && \
|
||||
|
||||
+4
-7
@@ -179,6 +179,9 @@ impl Application {
|
||||
if args.registers {
|
||||
initial_windows.push(ChildWindow::Registers);
|
||||
}
|
||||
if args.memory {
|
||||
initial_windows.push(ChildWindow::Memory);
|
||||
}
|
||||
if args.terminal {
|
||||
initial_windows.push(ChildWindow::Terminal);
|
||||
}
|
||||
@@ -391,12 +394,6 @@ impl ApplicationHandler<UserEvent> for Application {
|
||||
}
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
if let Some(viewport) = self.viewports.get(&ViewportId::ROOT) {
|
||||
viewport.window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
@@ -415,7 +412,7 @@ impl ApplicationHandler<UserEvent> for Application {
|
||||
if close_requested && viewport_id == ViewportId::ROOT {
|
||||
event_loop.exit();
|
||||
}
|
||||
if response.repaint {
|
||||
if response.repaint && !matches!(event, WindowEvent::RedrawRequested) {
|
||||
viewport.window.request_redraw();
|
||||
}
|
||||
if !response.consumed {
|
||||
|
||||
@@ -36,6 +36,9 @@ pub struct CliArgs {
|
||||
/// Open registers window
|
||||
#[arg(short, long)]
|
||||
pub registers: bool,
|
||||
/// Open memory window
|
||||
#[arg(short, long)]
|
||||
pub memory: bool,
|
||||
/// Open terminal
|
||||
#[arg(short, long)]
|
||||
pub terminal: bool,
|
||||
|
||||
+4
-3
@@ -518,11 +518,12 @@ impl Emulator {
|
||||
let Some(region) = region.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
let Some(sim) = self.sims.get_mut(range.sim.to_index()) else {
|
||||
return false;
|
||||
};
|
||||
self.buffer.clear();
|
||||
if let Some(sim) = self.sims.get_mut(range.sim.to_index()) {
|
||||
sim.read_memory(range.start, range.length, &mut self.buffer);
|
||||
} else {
|
||||
self.buffer.extend(std::iter::repeat_n(0, range.length));
|
||||
}
|
||||
region.update(&self.buffer);
|
||||
true
|
||||
});
|
||||
|
||||
+56
-32
@@ -1,10 +1,11 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
any::Any,
|
||||
collections::{HashMap, hash_map::Entry},
|
||||
fs,
|
||||
ops::Deref,
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
Arc, Mutex, RwLock,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
};
|
||||
@@ -16,25 +17,22 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Clone)]
|
||||
pub struct Persistence {
|
||||
config_dir: Option<PathBuf>,
|
||||
versions: Arc<Mutex<HashMap<String, Arc<AtomicU64>>>>,
|
||||
states: Arc<Mutex<HashMap<String, Arc<dyn Any + Send + Sync>>>>,
|
||||
}
|
||||
|
||||
impl Persistence {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
config_dir: init_config_dir(),
|
||||
versions: Arc::new(Mutex::new(HashMap::new())),
|
||||
states: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config<T: ConfigCompatible>(&self, file: &str) -> Config<T> {
|
||||
let latest_version = self
|
||||
.versions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(file.to_string())
|
||||
.or_default()
|
||||
.clone();
|
||||
let state = {
|
||||
let mut lock = self.states.lock().unwrap();
|
||||
match lock.entry(file.to_string()) {
|
||||
Entry::Vacant(e) => {
|
||||
let path = self
|
||||
.config_dir
|
||||
.as_ref()
|
||||
@@ -47,11 +45,28 @@ impl Persistence {
|
||||
} else {
|
||||
T::default()
|
||||
};
|
||||
|
||||
let state = Arc::new(ConfigState {
|
||||
current: RwLock::new(value),
|
||||
version: AtomicU64::new(0),
|
||||
path,
|
||||
});
|
||||
e.insert(state.clone());
|
||||
state
|
||||
}
|
||||
Entry::Occupied(e) => e
|
||||
.get()
|
||||
.clone()
|
||||
.downcast()
|
||||
.expect("same config file accessed with two types"),
|
||||
}
|
||||
};
|
||||
let value = state.current.read().unwrap().clone();
|
||||
let version = state.version.load(Ordering::Relaxed);
|
||||
Config {
|
||||
value,
|
||||
version: latest_version.load(Ordering::Relaxed),
|
||||
latest_version,
|
||||
path,
|
||||
version,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,16 +105,27 @@ fn project_dirs() -> Option<ProjectDirs> {
|
||||
ProjectDirs::from("com", "virtual-boy", "Lemur")
|
||||
}
|
||||
|
||||
pub trait ConfigCompatible: Clone + Default + Eq + Serialize + for<'a> Deserialize<'a> {}
|
||||
impl<T: Clone + Default + Eq + Serialize + for<'a> Deserialize<'a>> ConfigCompatible for T {}
|
||||
pub trait ConfigCompatible:
|
||||
Send + Sync + 'static + Clone + Default + Eq + Serialize + for<'a> Deserialize<'a>
|
||||
{
|
||||
}
|
||||
impl<T: Send + Sync + 'static + Clone + Default + Eq + Serialize + for<'a> Deserialize<'a>>
|
||||
ConfigCompatible for T
|
||||
{
|
||||
}
|
||||
|
||||
// A wrapper for a T (probably) stored on the filesystem.
|
||||
// Cheap to dereference, easy to sync changes to all copies.
|
||||
struct ConfigState<T> {
|
||||
current: RwLock<T>,
|
||||
version: AtomicU64,
|
||||
path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
// A wrapper for a config struct, loaded from disk and referenced across the codebase.
|
||||
// Cheap to read, and lets you sync changes to all copies.
|
||||
pub struct Config<T> {
|
||||
value: T,
|
||||
version: u64,
|
||||
latest_version: Arc<AtomicU64>,
|
||||
path: Option<PathBuf>,
|
||||
state: Arc<ConfigState<T>>,
|
||||
}
|
||||
|
||||
impl<T> Deref for Config<T> {
|
||||
@@ -112,32 +138,30 @@ impl<T> Deref for Config<T> {
|
||||
|
||||
impl<T: ConfigCompatible> Config<T> {
|
||||
pub fn refresh(&mut self) {
|
||||
let version = self.latest_version.load(Ordering::Relaxed);
|
||||
let version = self.state.version.load(Ordering::Relaxed);
|
||||
if self.version == version {
|
||||
return;
|
||||
}
|
||||
if let Some(path) = &self.path
|
||||
&& let Ok(bytes) = fs::read(path)
|
||||
&& let Ok(latest) = serde_json::from_slice(&bytes)
|
||||
{
|
||||
self.value = latest;
|
||||
}
|
||||
self.value = self.state.current.read().unwrap().clone();
|
||||
self.version = version;
|
||||
}
|
||||
|
||||
pub fn update(&mut self, update: impl FnOnce(&mut T)) {
|
||||
pub fn update(&mut self, mut update: impl FnMut(&mut T)) {
|
||||
let mut new_value = self.value.clone();
|
||||
update(&mut new_value);
|
||||
if self.value == new_value {
|
||||
return;
|
||||
}
|
||||
if let Some(path) = &self.path
|
||||
&& let Ok(bytes) = serde_json::to_vec(&new_value)
|
||||
|
||||
let mut current = self.state.current.write().unwrap();
|
||||
update(&mut current);
|
||||
if let Some(path) = &self.state.path
|
||||
&& let Ok(bytes) = serde_json::to_vec(&*current)
|
||||
{
|
||||
let _ = fs::write(path, bytes);
|
||||
}
|
||||
let version = self.latest_version.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
self.value = new_value.clone();
|
||||
let version = self.state.version.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
self.value = current.clone();
|
||||
self.version = version;
|
||||
}
|
||||
}
|
||||
|
||||
+23
-2
@@ -5,14 +5,19 @@ use std::{
|
||||
mpsc,
|
||||
},
|
||||
thread,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use egui::ViewportId;
|
||||
use itertools::Itertools as _;
|
||||
use wgpu::{
|
||||
Device, Extent3d, Origin3d, Queue, TexelCopyBufferLayout, TexelCopyTextureInfo, Texture,
|
||||
TextureDescriptor, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor,
|
||||
};
|
||||
use winit::event_loop::EventLoopProxy;
|
||||
|
||||
use crate::app::UserEvent;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextureSink {
|
||||
@@ -21,7 +26,11 @@ pub struct TextureSink {
|
||||
}
|
||||
|
||||
impl TextureSink {
|
||||
pub fn new(device: &Device, queue: Queue) -> (Self, TextureView) {
|
||||
pub fn new(
|
||||
device: &Device,
|
||||
queue: Queue,
|
||||
proxy: EventLoopProxy<UserEvent>,
|
||||
) -> (Self, TextureView) {
|
||||
let texture = Self::create_texture(device);
|
||||
let view = texture.create_view(&TextureViewDescriptor::default());
|
||||
let buffers = Arc::new(BufferPool::new());
|
||||
@@ -37,6 +46,12 @@ impl TextureSink {
|
||||
local_buf.copy_from_slice(bytes.as_slice());
|
||||
}
|
||||
Self::write_texture(&queue, &texture, local_buf.as_slice());
|
||||
if proxy
|
||||
.send_event(UserEvent::RequestRedraw(ViewportId::ROOT, Instant::now()))
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
let sink = Self { buffers, sink };
|
||||
@@ -127,7 +142,6 @@ impl BufferPool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Buffer {
|
||||
data: Mutex<Vec<u8>>,
|
||||
id: AtomicU64,
|
||||
@@ -140,3 +154,10 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::fmt::Debug for Buffer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Buffer")
|
||||
.field("id", &self.id)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-5
@@ -220,11 +220,7 @@ impl ImageBuffer {
|
||||
pub fn add(&mut self, coords: (usize, usize), pixel: Color32) {
|
||||
let index = coords.1 * self.size[0] + coords.0;
|
||||
let old = self.pixels[index];
|
||||
self.pixels[index] = Color32::from_rgb(
|
||||
old.r() + pixel.r(),
|
||||
old.g() + pixel.g(),
|
||||
old.b() + pixel.b(),
|
||||
);
|
||||
self.pixels[index] = old + pixel;
|
||||
}
|
||||
|
||||
pub fn changed(&self, image: &ColorImage) -> bool {
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ fn main() -> Result<()> {
|
||||
})?;
|
||||
|
||||
let event_loop = EventLoop::with_user_event().build().unwrap();
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
event_loop.set_control_flow(ControlFlow::Wait);
|
||||
let proxy = event_loop.create_proxy();
|
||||
let handle = event_loop.owned_display_handle();
|
||||
event_loop.run_app(&mut Application::new(
|
||||
|
||||
+66
-5
@@ -37,7 +37,53 @@ impl MemoryClient {
|
||||
.send_command(EmulatorCommand::WatchMemory(range, Arc::downgrade(®ion)));
|
||||
region
|
||||
});
|
||||
MemoryView { region }
|
||||
MemoryView {
|
||||
region,
|
||||
sim,
|
||||
start,
|
||||
length,
|
||||
}
|
||||
}
|
||||
|
||||
// Update an existing view to point to a new region of memory.
|
||||
// If the old and new regions overlap, it'll retain any old values.
|
||||
pub fn retarget(&self, view: &mut MemoryView, sim: SimId, start: u32, length: usize) {
|
||||
let range = MemoryRange { sim, start, length };
|
||||
let mut regions = self.regions.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let region = regions
|
||||
.get(&range)
|
||||
.and_then(|r| r.upgrade())
|
||||
.unwrap_or_else(|| {
|
||||
let region = Arc::new(MemoryRegion::new(start, length));
|
||||
if view.range().overlaps(range) {
|
||||
let (old_index, new_index) = if start < view.start {
|
||||
(0, view.start as usize - start as usize)
|
||||
} else {
|
||||
(start as usize - view.start as usize, 0)
|
||||
};
|
||||
let len = (view.length - old_index).min(length - new_index);
|
||||
let mut new_data = vec![0u8; length];
|
||||
let old_data = view.borrow();
|
||||
for (dst, src) in new_data[new_index..(new_index + len)]
|
||||
.iter_mut()
|
||||
.zip(old_data.range::<u8>(old_index, len))
|
||||
{
|
||||
*dst = src;
|
||||
}
|
||||
drop(old_data);
|
||||
region.update(&new_data);
|
||||
}
|
||||
regions.insert(range, Arc::downgrade(®ion));
|
||||
self.client
|
||||
.send_command(EmulatorCommand::WatchMemory(range, Arc::downgrade(®ion)));
|
||||
region
|
||||
});
|
||||
*view = MemoryView {
|
||||
region,
|
||||
sim,
|
||||
start,
|
||||
length,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write<T: MemoryValue>(&self, sim: SimId, address: u32, data: &T) {
|
||||
@@ -64,12 +110,23 @@ fn aligned_memory(start: u32, length: usize) -> BoxBytes {
|
||||
|
||||
pub struct MemoryView {
|
||||
region: Arc<MemoryRegion>,
|
||||
sim: SimId,
|
||||
start: u32,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl MemoryView {
|
||||
pub fn borrow(&self) -> MemoryRef<'_> {
|
||||
self.region.borrow()
|
||||
}
|
||||
|
||||
pub fn range(&self) -> MemoryRange {
|
||||
MemoryRange {
|
||||
sim: self.sim,
|
||||
start: self.start,
|
||||
length: self.length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MemoryRef<'a> {
|
||||
@@ -183,6 +240,14 @@ pub struct MemoryRange {
|
||||
pub length: usize,
|
||||
}
|
||||
|
||||
impl MemoryRange {
|
||||
pub fn overlaps(&self, other: MemoryRange) -> bool {
|
||||
self.sim == other.sim
|
||||
&& (self.start as usize) < other.start as usize + other.length
|
||||
&& (other.start as usize) < self.start as usize + self.length
|
||||
}
|
||||
}
|
||||
|
||||
const BUFFERS: usize = 4;
|
||||
pub struct MemoryRegion {
|
||||
gens: [AtomicU64; BUFFERS],
|
||||
@@ -195,10 +260,6 @@ impl Debug for MemoryRegion {
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
// SAFETY: BoxBytes is meant to be Send+Sync, will be in a future version
|
||||
unsafe impl Send for MemoryRegion {}
|
||||
// SAFETY: BoxBytes is meant to be Send+Sync, will be in a future version
|
||||
unsafe impl Sync for MemoryRegion {}
|
||||
|
||||
impl MemoryRegion {
|
||||
fn new(start: u32, length: usize) -> Self {
|
||||
|
||||
@@ -7,6 +7,7 @@ pub use game_screen::{DisplayMode, GameScreen};
|
||||
pub use gdb::GdbServerWindow;
|
||||
pub use hotkeys::HotkeysWindow;
|
||||
pub use input::InputWindow;
|
||||
pub use memory::MemoryWindow;
|
||||
pub use profile::ProfileWindow;
|
||||
pub use terminal::TerminalWindow;
|
||||
pub use vip::{
|
||||
@@ -22,6 +23,7 @@ mod game_screen;
|
||||
mod gdb;
|
||||
mod hotkeys;
|
||||
mod input;
|
||||
mod memory;
|
||||
mod profile;
|
||||
mod terminal;
|
||||
mod utils;
|
||||
|
||||
+9
-4
@@ -16,8 +16,8 @@ use crate::{
|
||||
memory::MemoryClient,
|
||||
window::{
|
||||
AboutWindow, BgMapWindow, CharacterDataWindow, FrameBufferWindow, GdbServerWindow,
|
||||
HotkeysWindow, InitArgs, InputWindow, ObjectWindow, ProfileWindow, RegisterWindow,
|
||||
TerminalWindow, WorldWindow, utils::UiData,
|
||||
HotkeysWindow, InitArgs, InputWindow, MemoryWindow, ObjectWindow, ProfileWindow,
|
||||
RegisterWindow, TerminalWindow, WorldWindow, utils::UiData,
|
||||
},
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
@@ -156,6 +156,7 @@ impl GameWindow {
|
||||
&self.memory,
|
||||
&self.persistence,
|
||||
)),
|
||||
ChildWindow::Memory => AppWrapper::of_dyn(MemoryWindow::new(self.sim_id, &self.memory)),
|
||||
ChildWindow::Terminal => {
|
||||
AppWrapper::of_dyn(TerminalWindow::new(self.sim_id, &self.client))
|
||||
}
|
||||
@@ -445,6 +446,9 @@ impl GameWindow {
|
||||
if ui.button("GDB Server").clicked() {
|
||||
self.open(ChildWindow::Debugger { port: None });
|
||||
}
|
||||
if ui.button("Memory").clicked() {
|
||||
self.open(ChildWindow::Memory);
|
||||
}
|
||||
ui.separator();
|
||||
if ui.button("Character Data").clicked() {
|
||||
self.open(ChildWindow::CharacterData);
|
||||
@@ -799,12 +803,11 @@ impl AppWindow for GameWindow {
|
||||
true
|
||||
});
|
||||
self.child_states.save(ui);
|
||||
ui.request_repaint_after(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
fn on_init(&mut self, args: InitArgs) {
|
||||
if self.screen.is_none() {
|
||||
let (screen, sink) = GameScreen::init(args.render_state);
|
||||
let (screen, sink) = GameScreen::init(args.render_state, self.proxy.clone());
|
||||
self.client.send_command(EmulatorCommand::ConnectToSim(
|
||||
self.sim_id,
|
||||
sink,
|
||||
@@ -878,6 +881,7 @@ impl DerefMut for AppWrapper {
|
||||
pub enum ChildWindow {
|
||||
About,
|
||||
CharacterData,
|
||||
Memory,
|
||||
BgMap,
|
||||
Objects,
|
||||
Worlds,
|
||||
@@ -898,6 +902,7 @@ impl ChildWindow {
|
||||
match self {
|
||||
Self::About => "About",
|
||||
Self::CharacterData => "CharacterData",
|
||||
Self::Memory => "Memory",
|
||||
Self::BgMap => "BgMap",
|
||||
Self::Objects => "Objects",
|
||||
Self::Worlds => "Worlds",
|
||||
|
||||
@@ -3,8 +3,9 @@ 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 winit::event_loop::EventLoopProxy;
|
||||
|
||||
use crate::graphics::TextureSink;
|
||||
use crate::{app::UserEvent, graphics::TextureSink};
|
||||
|
||||
pub struct GameScreen {
|
||||
bind_group: Arc<BindGroup>,
|
||||
@@ -116,11 +117,14 @@ impl GameScreen {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn init(render_state: &egui_wgpu::RenderState) -> (Self, TextureSink) {
|
||||
pub fn init(
|
||||
render_state: &egui_wgpu::RenderState,
|
||||
proxy: EventLoopProxy<UserEvent>,
|
||||
) -> (Self, TextureSink) {
|
||||
let device = &render_state.device;
|
||||
let queue = &render_state.queue;
|
||||
|
||||
let (sink, texture_view) = TextureSink::new(device, queue.clone());
|
||||
let (sink, texture_view) = TextureSink::new(device, queue.clone(), proxy);
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor::default());
|
||||
let colors = Colors::new(
|
||||
Color32::from_rgb(0xff, 0x00, 0x00),
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
use std::{fmt::LowerHex, sync::Arc};
|
||||
|
||||
use egui::{
|
||||
Align, CentralPanel, FontSelection, Label, Layout, Response, RichText, Separator, TextEdit, Ui,
|
||||
ViewportBuilder, Widget, text::LayoutJob,
|
||||
};
|
||||
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
emulator::SimId,
|
||||
memory::{MemoryClient, MemoryRef, MemoryValue, MemoryView},
|
||||
window::{AppWindow, utils::UiData},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
enum ValueSize {
|
||||
Byte,
|
||||
Halfword,
|
||||
Word,
|
||||
}
|
||||
impl ValueSize {
|
||||
fn size(self) -> usize {
|
||||
match self {
|
||||
Self::Byte => 1,
|
||||
Self::Halfword => 2,
|
||||
Self::Word => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HexEditResponse {
|
||||
submitted: bool,
|
||||
response: Response,
|
||||
}
|
||||
|
||||
struct HexEdit<'a> {
|
||||
text: &'a mut String,
|
||||
button_label: &'static str,
|
||||
}
|
||||
|
||||
impl<'a> HexEdit<'a> {
|
||||
#[must_use]
|
||||
fn new(text: &'a mut String, button_label: &'static str) -> Self {
|
||||
Self { text, button_label }
|
||||
}
|
||||
|
||||
fn show(self, ui: &mut Ui) -> HexEditResponse {
|
||||
const HEX_WORD_WIDTH: f32 = 80.0;
|
||||
let text_response = TextEdit::singleline(self.text)
|
||||
.desired_width(HEX_WORD_WIDTH)
|
||||
.ui(ui);
|
||||
let button_response = ui.button(self.button_label);
|
||||
let submitted = (text_response.lost_focus()
|
||||
&& text_response.ctx.input(|i| i.key_pressed(egui::Key::Enter)))
|
||||
|| button_response.clicked();
|
||||
let response = text_response.union(button_response);
|
||||
HexEditResponse {
|
||||
submitted,
|
||||
response,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for HexEdit<'_> {
|
||||
fn ui(self, ui: &mut Ui) -> egui::Response {
|
||||
self.show(ui).response
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||
struct State {
|
||||
address: u32,
|
||||
value_size: ValueSize,
|
||||
}
|
||||
impl Default for State {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
address: 0x05000000,
|
||||
value_size: ValueSize::Byte,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MemoryWindow {
|
||||
sim_id: SimId,
|
||||
memory: Arc<MemoryClient>,
|
||||
view: MemoryView,
|
||||
scroll_to: Option<u32>,
|
||||
jump_to: String,
|
||||
editing_buffer: Vec<u8>,
|
||||
state: UiData<State>,
|
||||
}
|
||||
|
||||
impl MemoryWindow {
|
||||
pub fn new(sim_id: SimId, memory: &Arc<MemoryClient>) -> Self {
|
||||
let state: UiData<State> = UiData::new();
|
||||
Self {
|
||||
sim_id,
|
||||
memory: memory.clone(),
|
||||
view: memory.watch(sim_id, 0x0, 0),
|
||||
scroll_to: None,
|
||||
jump_to: String::new(),
|
||||
editing_buffer: vec![],
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
fn show_top_bar(&mut self, ui: &mut Ui) {
|
||||
if ui
|
||||
.radio_value(&mut self.state.value_size, ValueSize::Byte, "8-bit")
|
||||
.clicked()
|
||||
{
|
||||
self.editing_buffer.clear();
|
||||
}
|
||||
if ui
|
||||
.radio_value(&mut self.state.value_size, ValueSize::Halfword, "16-bit")
|
||||
.clicked()
|
||||
{
|
||||
self.editing_buffer.clear();
|
||||
}
|
||||
if ui
|
||||
.radio_value(&mut self.state.value_size, ValueSize::Word, "32-bit")
|
||||
.clicked()
|
||||
{
|
||||
self.editing_buffer.clear();
|
||||
}
|
||||
self.state.address &= match self.state.value_size {
|
||||
ValueSize::Byte => 0x07ffffff,
|
||||
ValueSize::Halfword => 0x07fffffe,
|
||||
ValueSize::Word => 0x07fffffc,
|
||||
};
|
||||
if HexEdit::new(&mut self.jump_to, "Jump to address")
|
||||
.show(ui)
|
||||
.submitted
|
||||
{
|
||||
let address_str = std::mem::take(&mut self.jump_to);
|
||||
if let Ok(address) = u64::from_str_radix(&address_str, 16) {
|
||||
let address = (address & 0x07ffffff) as u32;
|
||||
self.state.address = address;
|
||||
self.editing_buffer.clear();
|
||||
self.scroll_to = Some(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_memory(&mut self, ui: &mut Ui) {
|
||||
let row_height_sans_spacing = ui.text_style_height(&egui::TextStyle::Monospace);
|
||||
let max_height = ui.max_rect().height();
|
||||
|
||||
let something_has_focus = ui.memory(|m| m.focused().is_some());
|
||||
let mut selected_changed = false;
|
||||
ui.input(|i| {
|
||||
if !i.focused || something_has_focus {
|
||||
return;
|
||||
}
|
||||
let arrow_keys: [(egui::Key, i32); 4] = [
|
||||
(egui::Key::ArrowUp, -0x10),
|
||||
(egui::Key::ArrowDown, 0x10),
|
||||
(egui::Key::ArrowLeft, -(self.state.value_size.size() as i32)),
|
||||
(egui::Key::ArrowRight, self.state.value_size.size() as i32),
|
||||
];
|
||||
for (key, delta) in arrow_keys {
|
||||
if !selected_changed && i.key_pressed(key) {
|
||||
self.state.address = self.state.address.wrapping_add_signed(delta) & 0x07ffffff;
|
||||
self.editing_buffer.clear();
|
||||
selected_changed = true;
|
||||
}
|
||||
}
|
||||
if !self.editing_buffer.is_empty() && i.key_pressed(egui::Key::Enter) {
|
||||
let full_size = self.state.value_size.size() * 2;
|
||||
while self.editing_buffer.len() < full_size {
|
||||
self.editing_buffer.insert(0, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut table = TableBuilder::new(ui)
|
||||
.column(Column::auto())
|
||||
.column(Column::initial(0.0))
|
||||
.column(Column::auto())
|
||||
.column(Column::initial(0.0))
|
||||
.column(Column::remainder())
|
||||
.max_scroll_height(max_height)
|
||||
.animate_scrolling(false);
|
||||
|
||||
if let Some(scroll_to) = self.scroll_to.take() {
|
||||
table = table.scroll_to_row(
|
||||
(scroll_to as usize / 0x10).saturating_sub(2),
|
||||
Some(Align::Min),
|
||||
);
|
||||
}
|
||||
|
||||
table.body(|body| {
|
||||
let total_rows = 0x00800000; // whole 27-bit non-mirrored address space, with 16 bytes per row
|
||||
let range = self.view.range();
|
||||
let current_start = range.start as usize;
|
||||
let current_length = range.length;
|
||||
let rows = self.view.borrow();
|
||||
let mut new_start = None;
|
||||
let mut new_length = 0;
|
||||
|
||||
body.rows(row_height_sans_spacing, total_rows, |mut row| {
|
||||
let address = row.index() * 0x10;
|
||||
let offset = address
|
||||
.checked_sub(current_start)
|
||||
.filter(|offset| offset + 0x10 <= current_length);
|
||||
|
||||
if new_start.is_none() {
|
||||
new_start = Some(address);
|
||||
}
|
||||
new_length += 0x10;
|
||||
row.col(|ui| {
|
||||
let label = Label::new(RichText::new(format!("{address:08x}")).monospace());
|
||||
if ui.add(label).clicked() {
|
||||
self.state.address = address as u32;
|
||||
self.editing_buffer.clear();
|
||||
selected_changed = true;
|
||||
};
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.separator();
|
||||
});
|
||||
row.col(|ui| {
|
||||
if let Some(offset) = offset {
|
||||
fn show_memory<T: MemoryValue + LowerHex>(
|
||||
ui: &mut Ui,
|
||||
rows: &MemoryRef,
|
||||
(start, offset): (usize, usize),
|
||||
selected: u32,
|
||||
memory: &MemoryClient,
|
||||
sim: SimId,
|
||||
buffer: &mut Vec<u8>,
|
||||
) -> Option<u32> {
|
||||
let mut new_selected = None;
|
||||
for (i, value) in rows
|
||||
.range::<T>(offset / size_of::<T>(), 0x10 / size_of::<T>())
|
||||
.enumerate()
|
||||
{
|
||||
let address = (start + offset + i * size_of::<T>()) as u32;
|
||||
let mut job = LayoutJob::default();
|
||||
|
||||
let rich_text = |value: &str| {
|
||||
let text = RichText::new(value).monospace();
|
||||
if address == selected {
|
||||
text.background_color(ui.visuals().selection.bg_fill)
|
||||
.color(ui.visuals().selection.stroke.color)
|
||||
} else {
|
||||
text
|
||||
}
|
||||
};
|
||||
|
||||
let edit_len = if address == selected { buffer.len() } else { 0 };
|
||||
|
||||
if edit_len > 0 {
|
||||
let buffer_str =
|
||||
buffer.iter().map(|d| format!("{d:x}")).join("");
|
||||
let text = rich_text(&buffer_str).underline();
|
||||
text.append_to(
|
||||
&mut job,
|
||||
ui.style(),
|
||||
FontSelection::default(),
|
||||
Align::Center,
|
||||
);
|
||||
}
|
||||
|
||||
let val_str = format!("{value:08x}")
|
||||
.split_off(8 - size_of::<T>() * 2 + edit_len);
|
||||
let text = rich_text(&val_str);
|
||||
text.append_to(
|
||||
&mut job,
|
||||
ui.style(),
|
||||
FontSelection::default(),
|
||||
Align::Center,
|
||||
);
|
||||
|
||||
let response = ui.label(job);
|
||||
if response.clicked() {
|
||||
new_selected = Some(address);
|
||||
}
|
||||
if ui.memory(|m| m.focused().is_none())
|
||||
&& ui.input(|i| i.focused)
|
||||
&& address == selected
|
||||
{
|
||||
const KEYS: [egui::Key; 16] = [
|
||||
egui::Key::Num0,
|
||||
egui::Key::Num1,
|
||||
egui::Key::Num2,
|
||||
egui::Key::Num3,
|
||||
egui::Key::Num4,
|
||||
egui::Key::Num5,
|
||||
egui::Key::Num6,
|
||||
egui::Key::Num7,
|
||||
egui::Key::Num8,
|
||||
egui::Key::Num9,
|
||||
egui::Key::A,
|
||||
egui::Key::B,
|
||||
egui::Key::C,
|
||||
egui::Key::D,
|
||||
egui::Key::E,
|
||||
egui::Key::F,
|
||||
];
|
||||
let pressed = ui.input(|i| {
|
||||
for (index, key) in KEYS.into_iter().enumerate() {
|
||||
if i.key_pressed(key) {
|
||||
return Some(index as u8);
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
if let Some(value) = pressed {
|
||||
buffer.push(value);
|
||||
}
|
||||
if buffer.len() == size_of::<T>() * 2 {
|
||||
let bytes = buffer
|
||||
.chunks(2)
|
||||
.map(|letters| (letters[0] << 4) | letters[1])
|
||||
.rev()
|
||||
.collect::<Vec<u8>>();
|
||||
let data = T::from_bytes(&bytes);
|
||||
memory.write(sim, address, &data);
|
||||
new_selected = Some(
|
||||
address.wrapping_add(size_of::<T>() as u32)
|
||||
& 0x07ffffff,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
new_selected
|
||||
}
|
||||
let selected = match self.state.value_size {
|
||||
ValueSize::Byte => show_memory::<u8>(
|
||||
ui,
|
||||
&rows,
|
||||
(current_start, offset),
|
||||
self.state.address,
|
||||
&self.memory,
|
||||
self.sim_id,
|
||||
&mut self.editing_buffer,
|
||||
),
|
||||
ValueSize::Halfword => show_memory::<u16>(
|
||||
ui,
|
||||
&rows,
|
||||
(current_start, offset),
|
||||
self.state.address,
|
||||
&self.memory,
|
||||
self.sim_id,
|
||||
&mut self.editing_buffer,
|
||||
),
|
||||
ValueSize::Word => show_memory::<u32>(
|
||||
ui,
|
||||
&rows,
|
||||
(current_start, offset),
|
||||
self.state.address,
|
||||
&self.memory,
|
||||
self.sim_id,
|
||||
&mut self.editing_buffer,
|
||||
),
|
||||
};
|
||||
if let Some(selected) = selected {
|
||||
self.state.address = selected;
|
||||
selected_changed = true;
|
||||
}
|
||||
};
|
||||
if selected_changed {
|
||||
self.editing_buffer.clear();
|
||||
}
|
||||
});
|
||||
row.col(|ui| {
|
||||
ui.separator();
|
||||
});
|
||||
row.col(|ui| {
|
||||
if let Some(offset) = offset {
|
||||
let printable: String = rows
|
||||
.range::<u8>(offset, 0x10)
|
||||
.map(|c| {
|
||||
if (32..128).contains(&c) {
|
||||
c as char
|
||||
} else {
|
||||
'.'
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
ui.label(RichText::new(printable).monospace());
|
||||
}
|
||||
});
|
||||
});
|
||||
drop(rows);
|
||||
if let Some(start) = new_start
|
||||
&& (start != current_start || new_length != current_length)
|
||||
{
|
||||
self.memory
|
||||
.retarget(&mut self.view, self.sim_id, start as u32, new_length);
|
||||
}
|
||||
});
|
||||
|
||||
if selected_changed && self.scroll_to.is_none() {
|
||||
let range = self.view.range();
|
||||
if self.state.address < range.start {
|
||||
self.scroll_to = Some(self.state.address);
|
||||
} else if let Some(delta) = self
|
||||
.state
|
||||
.address
|
||||
.checked_sub(range.start + range.length as u32 - 0x20)
|
||||
{
|
||||
self.scroll_to = Some(range.start + delta + 0x40);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppWindow for MemoryWindow {
|
||||
fn sim_id(&self) -> Option<SimId> {
|
||||
Some(self.sim_id)
|
||||
}
|
||||
|
||||
fn initial_viewport(&self) -> ViewportBuilder {
|
||||
ViewportBuilder::default()
|
||||
.with_title("Memory")
|
||||
.with_inner_size((640.0, 480.0))
|
||||
}
|
||||
|
||||
fn show(&mut self, ui: &mut Ui) {
|
||||
let row_height =
|
||||
ui.text_style_height(&egui::TextStyle::Monospace) + ui.spacing().item_spacing.y;
|
||||
let table_height = ui.available_height() - (row_height * 2.5);
|
||||
if self.state.load(ui) {
|
||||
self.scroll_to = Some(self.state.address);
|
||||
}
|
||||
CentralPanel::default().show(ui, |ui| {
|
||||
StripBuilder::new(ui)
|
||||
.size(Size::exact(row_height))
|
||||
.size(Size::initial(0.0))
|
||||
.size(Size::exact(table_height))
|
||||
.cell_layout(Layout::left_to_right(Align::Center))
|
||||
.vertical(|mut strip| {
|
||||
strip.cell(|ui| self.show_top_bar(ui));
|
||||
strip.cell(|ui| {
|
||||
Separator::default().horizontal().ui(ui);
|
||||
});
|
||||
strip.cell(|ui| self.show_memory(ui));
|
||||
});
|
||||
});
|
||||
self.state.save(ui);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -432,13 +432,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&mut self, ui: &Ui) {
|
||||
pub fn load(&mut self, ui: &Ui) -> bool {
|
||||
let just_loaded = !self.loaded;
|
||||
if !self.loaded {
|
||||
self.current = ui
|
||||
.data_mut(|d| d.get_persisted(ui.id()))
|
||||
.unwrap_or_default();
|
||||
self.loaded = true;
|
||||
}
|
||||
just_loaded
|
||||
}
|
||||
|
||||
pub fn save(&mut self, ui: &Ui) {
|
||||
|
||||
@@ -266,11 +266,7 @@ impl ImageRenderer<1> for FrameBufferRenderer {
|
||||
} else {
|
||||
Color32::BLACK
|
||||
};
|
||||
Color32::from_rgb(
|
||||
left.r() + right.r(),
|
||||
left.g() + right.g(),
|
||||
left.b() + right.b(),
|
||||
)
|
||||
left + right
|
||||
});
|
||||
let x = index / 64;
|
||||
for (i, pixel) in pixels.into_iter().enumerate() {
|
||||
|
||||
+2
-11
@@ -477,12 +477,7 @@ impl WorldWindow {
|
||||
|
||||
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 both_color = left_color + right_color;
|
||||
let painter = ui.painter();
|
||||
let draw_rect = |x1: f32, x2: f32, color: Color32| {
|
||||
painter.line(
|
||||
@@ -702,11 +697,7 @@ impl WorldRenderer {
|
||||
for (dst, shades) in image.pixels.iter_mut().zip(self.buffer.iter()) {
|
||||
let left = colors[0][shades[0] as usize];
|
||||
let right = colors[1][shades[1] as usize];
|
||||
*dst = Color32::from_rgb(
|
||||
left.r() + right.r(),
|
||||
left.g() + right.g(),
|
||||
left.b() + right.b(),
|
||||
)
|
||||
*dst = left + right;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user