Compare commits

...
10 Commits
10 changed files with 567 additions and 236 deletions
Generated
+464 -188
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -4,12 +4,13 @@ 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.10.1" version = "0.11.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
atoi = "2" atoi = "2"
audioadapter-buffers = "2"
atomic = "0.6" atomic = "0.6"
bitflags = { version = "2", features = ["serde"] } bitflags = { version = "2", features = ["serde"] }
bytemuck = { version = "1", features = ["derive"] } bytemuck = { version = "1", features = ["derive"] }
@@ -24,7 +25,7 @@ egui-wgpu = { version = "0.33", features = ["winit"] }
fxprof-processed-profile = "0.8" fxprof-processed-profile = "0.8"
fixed = { version = "1.28", features = ["num-traits"] } fixed = { version = "1.28", features = ["num-traits"] }
gilrs = { version = "0.11", features = ["serde-serialize"] } gilrs = { version = "0.11", features = ["serde-serialize"] }
gimli = "0.32" gimli = "0.33"
hex = "0.4" hex = "0.4"
image = { version = "0.25", default-features = false, features = ["png"] } image = { version = "0.25", default-features = false, features = ["png"] }
itertools = "0.14" itertools = "0.14"
@@ -35,10 +36,10 @@ num-traits = "0.2"
object = "0.38" object = "0.38"
oneshot = "0.1" oneshot = "0.1"
pollster = "0.4" pollster = "0.4"
rand = "0.9" rand = "0.10"
rfd = "0.17" rfd = "0.17"
rtrb = "0.3" rtrb = "0.3"
rubato = "0.16" rubato = "1"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
thread-priority = "3" thread-priority = "3"
+6 -2
View File
@@ -9,8 +9,10 @@ ADD "https://github.com/joseluisq/macosx-sdks/releases/download/14.5/MacOSX14.5.
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y ca-certificates apt-get install -y ca-certificates
COPY llvm.sources /etc/apt/sources.list.d/llvm.sources COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
COPY install-llvm.sh .
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y bash bzip2 clang-21 git lld-21 llvm-21 make patch xz-utils && \ ./install-llvm.sh && \
apt-get install -y bash bzip2 git make patch xz-utils && \
ln -s $(which clang-21) /usr/bin/clang && \ ln -s $(which clang-21) /usr/bin/clang && \
ln -s $(which clang++-21) /usr/bin/clang++ && \ ln -s $(which clang++-21) /usr/bin/clang++ && \
ln -s $(which ld64.lld-21) /usr/bin/ld64.lld && \ ln -s $(which ld64.lld-21) /usr/bin/ld64.lld && \
@@ -19,11 +21,13 @@ RUN apt-get update && \
FROM rust:1.93-bookworm FROM rust:1.93-bookworm
ADD --chmod=644 "https://apt.llvm.org/llvm-snapshot.gpg.key" /etc/apt/trusted.gpg.d/apt.llvm.org.asc 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 COPY llvm.sources /etc/apt/sources.list.d/llvm.sources
COPY install-llvm.sh .
RUN rustup target add x86_64-pc-windows-msvc && \ RUN rustup target add x86_64-pc-windows-msvc && \
rustup target add x86_64-apple-darwin && \ rustup target add x86_64-apple-darwin && \
rustup target add aarch64-apple-darwin && \ rustup target add aarch64-apple-darwin && \
apt-get update && \ apt-get update && \
apt-get install -y clang-21 lld-21 libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \ ./install-llvm.sh && \
apt-get install -y libc6-dev libasound2-dev libudev-dev genisoimage mingw-w64 && \
cargo install cargo-bundle xwin && \ cargo install cargo-bundle xwin && \
xwin --accept-license splat --output xwin && \ xwin --accept-license splat --output xwin && \
rm -rf .xwin-cache && \ rm -rf .xwin-cache && \
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Hopefully temporary script to manually install working llvm packages.
# The apt index for these is broken in bookworm, and upgrading to trixie
# would make them depend on too new of a libc version.
# https://apt.llvm.org/bookworm/pool/main/l/llvm-toolchain-21/clang-21_21.1.5~%2B%2B20251023083151%2B45afac62e373-1~exp1~20251023083333.51_amd64.deb
PACKAGES=('clang-21' 'clang-tools-21' 'libclang-common-21-dev' 'libclang-cpp21' 'libclang-rt-21-dev' 'libclang1-21' 'libllvm21' 'lld-21' 'llvm-21' 'llvm-21-dev' 'llvm-21-linker-tools' 'llvm-21-runtime' 'llvm-21-tools')
FILES=()
URL='https://apt.llvm.org/bookworm/pool/main/l/llvm-toolchain-21'
VERSION='21.1.5~%2B%2B20251023083151%2B45afac62e373-1~exp1~20251023083333.51_amd64.deb'
apt-get install -y curl python3
for package in "${PACKAGES[@]}"; do
curl -O -L "$URL/${package}_$VERSION"
FILES+=("./${package}_$VERSION")
done
apt-get install -y "${FILES[@]}"
+14 -4
View File
@@ -74,7 +74,7 @@ impl Application {
) -> Self { ) -> Self {
let wgpu = WgpuState::new(); let wgpu = WgpuState::new();
let icon = load_icon().ok().map(Arc::new); let icon = load_icon().ok().map(Arc::new);
let mappings = MappingProvider::new(persistence.clone()); let mappings = MappingProvider::new(persistence.clone(), args.player2_controller);
let shortcuts = ShortcutProvider::new(persistence.clone()); let shortcuts = ShortcutProvider::new(persistence.clone());
let controllers = ControllerManager::new(client.clone(), &mappings); let controllers = ControllerManager::new(client.clone(), &mappings);
let memory = Arc::new(MemoryClient::new(client.clone())); let memory = Arc::new(MemoryClient::new(client.clone()));
@@ -84,7 +84,7 @@ impl Application {
let proxy = proxy.clone(); let proxy = proxy.clone();
thread::spawn(|| process_gamepad_input(mappings, proxy)); thread::spawn(|| process_gamepad_input(mappings, proxy));
} }
Self { let app = Self {
icon, icon,
wgpu, wgpu,
client, client,
@@ -106,7 +106,16 @@ impl Application {
init_framebuffers: args.frame_buffers, init_framebuffers: args.frame_buffers,
init_registers: args.registers, init_registers: args.registers,
init_terminal: args.terminal, init_terminal: args.terminal,
};
if args.player2 {
app.client
.send_command(EmulatorCommand::StartSecondSim(args.rom.clone()));
app.proxy
.send_event(UserEvent::OpenPlayer2)
.expect("Failed to open Player 2 window");
} }
app
} }
fn open(&mut self, event_loop: &ActiveEventLoop, window: Box<dyn AppWindow>) { fn open(&mut self, event_loop: &ActiveEventLoop, window: Box<dyn AppWindow>) {
@@ -382,7 +391,8 @@ impl WgpuState {
}); });
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance, power_preference:
wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::LowPower),
compatible_surface: None, compatible_surface: None,
force_fallback_adapter: false, force_fallback_adapter: false,
})) }))
@@ -439,7 +449,7 @@ impl Viewport {
egui_extras::install_image_loaders(&ctx); egui_extras::install_image_loaders(&ctx);
let wgpu_config = egui_wgpu::WgpuConfiguration { let wgpu_config = egui_wgpu::WgpuConfiguration {
present_mode: wgpu::PresentMode::AutoNoVsync, present_mode: wgpu::PresentMode::AutoVsync,
wgpu_setup: egui_wgpu::WgpuSetup::Existing(egui_wgpu::WgpuSetupExisting { wgpu_setup: egui_wgpu::WgpuSetup::Existing(egui_wgpu::WgpuSetupExisting {
instance: wgpu.instance.clone(), instance: wgpu.instance.clone(),
adapter: wgpu.adapter.clone(), adapter: wgpu.adapter.clone(),
+38 -33
View File
@@ -1,17 +1,17 @@
use std::time::Duration; use std::time::Duration;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use audioadapter_buffers::direct::InterleavedSlice;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use itertools::Itertools; use rubato::Resampler;
use rubato::{FastFixedOut, Resampler};
use tracing::error; use tracing::error;
pub struct Audio { pub struct Audio {
#[allow(unused)] #[allow(unused)]
stream: cpal::Stream, stream: cpal::Stream,
sampler: FastFixedOut<f32>, sampler: rubato::Async<f32>,
input_buffer: Vec<Vec<f32>>, input_buffer: Vec<f32>,
output_buffer: Vec<Vec<f32>>, output_buffer: Vec<f32>,
sample_sink: rtrb::Producer<f32>, sample_sink: rtrb::Producer<f32>,
} }
@@ -32,17 +32,18 @@ impl Audio {
let mut config = config.with_max_sample_rate().config(); let mut config = config.with_max_sample_rate().config();
let resample_ratio = config.sample_rate.0 as f64 / VB_FREQUENCY as f64; let resample_ratio = config.sample_rate.0 as f64 / VB_FREQUENCY as f64;
let chunk_size = (834.0 * resample_ratio) as usize; let chunk_size = (834.0 * resample_ratio) as usize;
let sampler = FastFixedOut::new( let sampler = rubato::Async::new_poly(
resample_ratio, resample_ratio,
64.0, 64.0,
rubato::PolynomialDegree::Cubic, rubato::PolynomialDegree::Cubic,
chunk_size, chunk_size,
2, 2,
rubato::FixedAsync::Output,
)?; )?;
config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32); config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32);
let input_buffer = sampler.input_buffer_allocate(true); let input_buffer = Vec::with_capacity(sampler.nbr_channels() * sampler.input_frames_max());
let output_buffer = sampler.output_buffer_allocate(true); let output_buffer = vec![0.0; sampler.nbr_channels() * sampler.output_frames_max()];
let (sample_sink, mut sample_source) = let (sample_sink, mut sample_source) =
rtrb::RingBuffer::new(sampler.output_frames_max() * 4); rtrb::RingBuffer::new(sampler.output_frames_max() * 4);
@@ -78,34 +79,38 @@ impl Audio {
}) })
} }
pub fn update(&mut self, samples: &[f32]) { pub fn update(&mut self, mut samples: &[f32]) {
for sample in samples.chunks_exact(2) { while self.input_buffer.len() + samples.len() >= self.sampler.input_frames_next() * 2 {
for (channel, value) in self.input_buffer.iter_mut().zip(sample) { let samples_needed =
channel.push(*value); (self.sampler.input_frames_next() * 2).saturating_sub(self.input_buffer.len());
} let (current_samples, future_samples) = samples.split_at(samples_needed);
if self.input_buffer[0].len() >= self.sampler.input_frames_next() { self.input_buffer.extend_from_slice(current_samples);
let (_, output_samples) = self samples = future_samples;
.sampler
.process_into_buffer(&self.input_buffer, &mut self.output_buffer, None) let buffer_in =
InterleavedSlice::new(&self.input_buffer, 2, self.sampler.input_frames_next())
.unwrap(); .unwrap();
let mut buffer_out = InterleavedSlice::new_mut(
let chunk = match self.sample_sink.write_chunk_uninit(output_samples * 2) { &mut self.output_buffer,
Ok(c) => c, 2,
Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => { self.sampler.output_frames_next(),
self.sample_sink.write_chunk_uninit(n).unwrap() )
} .unwrap();
}; let (_, output_samples) = self
let interleaved = self.output_buffer[0] .sampler
.iter() .process_into_buffer(&buffer_in, &mut buffer_out, None)
.interleave(self.output_buffer[1].iter()) .unwrap();
.cloned(); let chunk = match self.sample_sink.write_chunk_uninit(output_samples * 2) {
chunk.fill_from_iter(interleaved); Ok(c) => c,
Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => {
for channel in &mut self.input_buffer { self.sample_sink.write_chunk_uninit(n).unwrap()
channel.clear();
} }
} };
chunk.fill_from_iter(self.output_buffer[..output_samples * 2].iter().copied());
self.input_buffer.clear();
} }
self.input_buffer.extend_from_slice(samples);
while self.sample_sink.slots() < self.sampler.output_frames_max() * 2 { while self.sample_sink.slots() < self.sampler.output_frames_max() * 2 {
std::thread::sleep(Duration::from_micros(500)); std::thread::sleep(Duration::from_micros(500));
+6
View File
@@ -40,6 +40,12 @@ pub struct CliArgs {
/// Watch ROM files for changes, automatically reload /// Watch ROM files for changes, automatically reload
#[arg(short, long)] #[arg(short, long)]
pub watch: bool, pub watch: bool,
/// Automatically open Player 2 for multiplayer
#[arg(long)]
pub player2: bool,
/// Map the first connected controller to Player 2
#[arg(long)]
pub player2_controller: bool,
} }
pub const COLOR_PRESETS: [[Color32; 2]; 3] = [ pub const COLOR_PRESETS: [[Color32; 2]; 3] = [
+1 -1
View File
@@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use notify::Watcher; use notify::Watcher;
use rand::Rng; use rand::RngExt;
use std::{ use std::{
fs::{self, File}, fs::{self, File},
io::{Read, Seek as _, SeekFrom, Write as _}, io::{Read, Seek as _, SeekFrom, Write as _},
+5 -2
View File
@@ -245,9 +245,12 @@ impl ParseContext<'_> {
} }
} }
fn parse_inline(ctx: &mut ParseContext, node: gimli::EntriesTreeNode<Reader>) -> Result<()> { fn parse_inline<'a>(
ctx: &mut ParseContext<'a>,
node: gimli::EntriesTreeNode<'a, '_, Reader<'a>>,
) -> Result<()> {
if node.entry().tag() == gimli::DW_TAG_inlined_subroutine if node.entry().tag() == gimli::DW_TAG_inlined_subroutine
&& let Some(attr) = node.entry().attr_value(gimli::DW_AT_abstract_origin)? && let Some(attr) = node.entry().attr_value(gimli::DW_AT_abstract_origin)
&& let Some(name) = ctx.name_attr(attr)? && let Some(name) = ctx.name_attr(attr)?
{ {
let name = Arc::new(name); let name = Arc::new(name);
+9 -2
View File
@@ -272,13 +272,14 @@ impl Mappings for InputMapping {
#[derive(Clone)] #[derive(Clone)]
pub struct MappingProvider { pub struct MappingProvider {
persistence: Persistence, persistence: Persistence,
first_gamepad_is_p2: bool,
device_mappings: Arc<RwLock<HashMap<DeviceId, Arc<RwLock<GamepadMapping>>>>>, device_mappings: Arc<RwLock<HashMap<DeviceId, Arc<RwLock<GamepadMapping>>>>>,
sim_mappings: HashMap<SimId, Arc<RwLock<InputMapping>>>, sim_mappings: HashMap<SimId, Arc<RwLock<InputMapping>>>,
gamepad_info: Arc<RwLock<HashMap<GamepadId, GamepadInfo>>>, gamepad_info: Arc<RwLock<HashMap<GamepadId, GamepadInfo>>>,
} }
impl MappingProvider { impl MappingProvider {
pub fn new(persistence: Persistence) -> Self { pub fn new(persistence: Persistence, first_gamepad_is_p2: bool) -> Self {
let mut sim_mappings = HashMap::new(); let mut sim_mappings = HashMap::new();
let mut device_mappings = HashMap::new(); let mut device_mappings = HashMap::new();
@@ -307,6 +308,7 @@ impl MappingProvider {
device_mappings: Arc::new(RwLock::new(device_mappings)), device_mappings: Arc::new(RwLock::new(device_mappings)),
gamepad_info: Arc::new(RwLock::new(HashMap::new())), gamepad_info: Arc::new(RwLock::new(HashMap::new())),
sim_mappings, sim_mappings,
first_gamepad_is_p2,
} }
} }
@@ -338,7 +340,12 @@ impl MappingProvider {
.clone(); .clone();
drop(lock); drop(lock);
let mut lock = self.gamepad_info.write().unwrap(); let mut lock = self.gamepad_info.write().unwrap();
let bound_to = SimId::values() let players = if self.first_gamepad_is_p2 {
vec![SimId::Player2, SimId::Player1]
} else {
vec![SimId::Player1, SimId::Player2]
};
let bound_to = players
.into_iter() .into_iter()
.find(|sim_id| lock.values().all(|info| info.bound_to != Some(*sim_id))); .find(|sim_id| lock.values().all(|info| info.bound_to != Some(*sim_id)));
if let Entry::Vacant(entry) = lock.entry(gamepad.id()) { if let Entry::Vacant(entry) = lock.entry(gamepad.id()) {