Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ libloading = "0.9.0"
libretro-ffi = { version = "0.1.0", path = "libretro-ffi" }
reqwest = "0.13.1"
rfd = "0.17.1"
rhai = { version = "1.24.0", features = ["sync"] }
rubato = "1.0.0"
rust-ini = "0.21.0"
serde = { version = "1.0.193", features = ["derive"] }
Expand Down
24 changes: 20 additions & 4 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub struct Core {
savestate_buffer: Box<[u8]>,
}
unsafe impl Send for Core {}
unsafe impl Sync for Core {}

#[expect(clippy::type_complexity)]
pub struct CoreImpl {
Expand All @@ -47,14 +48,20 @@ pub struct CoreImpl {
frame: Frame,
input: *const [u8],
input_ports: *const [InputPort],
audio_callback: Option<*mut dyn FnMut(&[AudioFrame])>,
audio_callback: Option<*mut (dyn FnMut(&[AudioFrame]) + Send)>,
trace_context: *mut retro_trace_ctx_t,
trace_buffer: Vec<u8>,
}

// The input_callback cannot be safely sent across threads, but we are careful to never do so.
unsafe impl Send for CoreImpl {}

impl Drop for CoreImpl {
fn drop(&mut self) {
unsafe { (symbols().retro_deinit)() }
}
}

pub struct Frame {
pub width: usize,
pub height: usize,
Expand Down Expand Up @@ -171,11 +178,20 @@ impl Core {
lock().get_memory_byte(addr).copied()
}

pub fn read_memory_le(&self, addr: usize, width: u8) -> Option<u64> {
let mut core = lock();
let mut v = 0;
for offset in 0..width as usize {
v |= (*core.get_memory_byte(addr + offset)? as u64) << (offset * 8);
}
Some(v)
}

pub fn run_frame(
&mut self,
input: &[u8],
input_ports: &[crate::tas::input::InputPort],
mut audio_callback: impl FnMut(&[AudioFrame]),
mut audio_callback: impl FnMut(&[AudioFrame]) + Send,
) {
let run = *symbols().retro_run;
unsafe {
Expand All @@ -186,8 +202,8 @@ impl Core {
// used during the execution of this function. The explicit transmute is needed
// due to https://github.com/rust-lang/rust/pull/136776
core.audio_callback = Some(std::mem::transmute::<
*mut dyn FnMut(&[AudioFrame]),
*mut (dyn FnMut(&[AudioFrame]) + 'static),
*mut (dyn FnMut(&[AudioFrame]) + Send),
*mut (dyn FnMut(&[AudioFrame]) + Send + 'static),
>(
std::ptr::addr_of_mut!(audio_callback) as *mut _
));
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use anyhow::Result;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

mod core;
mod rhai;
mod tas;
mod ui;

Expand Down
73 changes: 73 additions & 0 deletions src/rhai.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use crate::core::Core;

/// Extension trait for [`rhai::Engine`].
pub trait EngineExt {
/// Register the `mem` rhai module.
///
/// This module contains functions to interact with the guest's memory.
fn register_mem_module(&mut self);
}

impl EngineExt for rhai::Engine {
fn register_mem_module(&mut self) {
self.register_static_module("mem", mem().into());
}
}

type RhaiResult<T> = Result<T, Box<rhai::EvalAltResult>>;

fn mem() -> rhai::Module {
let mut module = rhai::Module::new();
rhai::FuncRegistration::new("u8")
.with_volatility(true)
.set_into_module(
&mut module,
|ctx: rhai::NativeCallContext, address: i64| -> RhaiResult<u8> {
let c: &Core = ctx.tag().unwrap().clone().cast();
usize::try_from(address)
.ok()
.and_then(|address| c.read_memory_byte(address))
.ok_or_else(|| format!("out of bounds read at {address}").into())
},
);
rhai::FuncRegistration::new("u16le")
.with_volatility(true)
.set_into_module(
&mut module,
|ctx: rhai::NativeCallContext, address: i64| -> RhaiResult<u16> {
let c: &Core = ctx.tag().unwrap().clone().cast();
let res = usize::try_from(address)
.ok()
.and_then(|address| c.read_memory_le(address, 2))
.ok_or_else(|| format!("out of bounds read of 2 bytes at {address}"))?;
Ok(res as u16)
},
);
rhai::FuncRegistration::new("u32le")
.with_volatility(true)
.set_into_module(
&mut module,
|ctx: rhai::NativeCallContext, address: i64| -> RhaiResult<u32> {
let c: &Core = ctx.tag().unwrap().clone().cast();
let res = usize::try_from(address)
.ok()
.and_then(|address| c.read_memory_le(address, 4))
.ok_or_else(|| format!("out of bounds read of 4 bytes at {address}"))?;
Ok(res as u32)
},
);
rhai::FuncRegistration::new("u64le")
.with_volatility(true)
.set_into_module(
&mut module,
|ctx: rhai::NativeCallContext, address: i64| -> RhaiResult<u64> {
let c: &Core = ctx.tag().unwrap().clone().cast();
usize::try_from(address)
.ok()
.and_then(|address| c.read_memory_le(address, 8))
.ok_or_else(|| format!("out of bounds read of 8 bytes at {address}").into())
},
);

module
}
16 changes: 8 additions & 8 deletions src/tas/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,12 @@ impl Tas {
&self.movie
}

pub fn read_ram_watch(&self, watch: &movie::RamWatch) -> Option<u64> {
let mut v = 0;
for offset in 0..watch.format.width as usize {
v |= (self.core.read_memory_byte(watch.address + offset)? as u64) << (offset * 8);
}
Some(v)
pub fn read_ram_watch(
&self,
watch: &movie::RamWatch,
rhai_engine: &mut rhai::Engine,
) -> anyhow::Result<String> {
watch.value.execute(&self.core, rhai_engine)
}

pub fn ramwatches_mut(&mut self) -> &mut Vec<movie::RamWatch> {
Expand Down Expand Up @@ -195,7 +195,7 @@ impl Tas {

pub fn run_guest_frame(
&mut self,
audio_callback: &mut impl FnMut(&[core::AudioFrame]),
audio_callback: &mut (impl FnMut(&[core::AudioFrame]) + Send),
) -> &core::Frame {
self.trace = None;
if self
Expand Down Expand Up @@ -237,7 +237,7 @@ impl Tas {

pub fn run_host_frame(
&mut self,
mut audio_callback: impl FnMut(&[core::AudioFrame]),
mut audio_callback: impl FnMut(&[core::AudioFrame]) + Send,
) -> &core::Frame {
// Determine how many guest frames have elapsed since the last host frame
let time = Instant::now();
Expand Down
59 changes: 54 additions & 5 deletions src/tas/movie/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use std::rc::Rc;
use std::{rc::Rc, sync::OnceLock};

use anyhow::Context;
use serde::{Deserialize, Serialize};

use crate::{
core,
core::{self, Core},
tas::edit::{Pattern, PatternBuf},
};

Expand Down Expand Up @@ -35,11 +35,60 @@ pub struct Movie {
#[derive(Serialize, Deserialize, Clone)]
pub struct RamWatch {
pub name: String,
pub address: usize,
pub format: RamWatchFormat,
#[serde(flatten)]
pub value: RamWatchValue,
}

#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RamWatchValue {
/// Simple RAM watch value that fetches from an address using a specific format.
Simple {
address: usize,
format: RamWatchFormat,
},
/// Complex RAM watch value that runs a rhai script to compute the value
RhaiScript {
source: String,
/// The AST is initialized on demand.
/// Since the source may be invalid (e.g. from a hand-edited file), the ast may fail to
/// compile and this is stored as a `Result`.
#[serde(skip)]
ast: OnceLock<Result<rhai::AST, rhai::ParseError>>,
},
}

impl RamWatchValue {
pub fn execute(&self, core: &Core, rhai_engine: &mut rhai::Engine) -> anyhow::Result<String> {
match self {
Self::Simple { address, format } => Ok(format.format_value(
core.read_memory_le(*address, format.width)
.with_context(|| {
format!("out of bounds read at {address} (format: {format:?})")
})?,
)),
Self::RhaiScript { source, ast } => {
let ast = match ast.get_or_init(|| rhai_engine.compile(source)) {
Ok(v) => v,
Err(err) => {
return Err(err.clone())
.context("failed to compile RAM watch expression")?;
}
};
rhai_engine.set_default_tag(rhai::Dynamic::from(unsafe {
std::mem::transmute::<&Core, &'static Core>(core)
}));
let result = rhai_engine.eval_ast::<rhai::Dynamic>(ast);
rhai_engine.set_default_tag(0);
Ok(result
.context("failed to execute RAM watch script")?
.to_string())
}
}
}
}

#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct RamWatchFormat {
pub width: u8,
pub hex: bool,
Expand Down
Loading