Compare commits

..
13 Commits
10 changed files with 363 additions and 252 deletions
Generated
+181 -184
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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.12.1" version = "0.12.4"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
+11 -11
View File
@@ -13,12 +13,12 @@ COPY install-llvm.sh .
RUN apt-get update && \ RUN apt-get update && \
./install-llvm.sh && \ ./install-llvm.sh && \
apt-get install -y bash bzip2 git make patch xz-utils && \ apt-get install -y bash bzip2 git make patch xz-utils && \
ln -s $(which clang-21) /usr/bin/clang && \ ln -s $(which clang-22) /usr/bin/clang && \
ln -s $(which clang++-21) /usr/bin/clang++ && \ ln -s $(which clang++-22) /usr/bin/clang++ && \
ln -s $(which ld64.lld-21) /usr/bin/ld64.lld && \ ln -s $(which ld64.lld-22) /usr/bin/ld64.lld && \
SDK_VERSION=14.5 UNATTENDED=1 ENABLE_COMPILER_RT_INSTALL=1 TARGET_DIR=/osxcross ./build.sh SDK_VERSION=14.5 UNATTENDED=1 ENABLE_COMPILER_RT_INSTALL=1 TARGET_DIR=/osxcross ./build.sh
FROM rust:1.94-bookworm FROM rust:1.95-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 . COPY install-llvm.sh .
@@ -31,13 +31,13 @@ RUN rustup target add x86_64-pc-windows-msvc && \
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 && \
ln -s $(which clang-21) /usr/bin/clang && \ ln -s $(which clang-22) /usr/bin/clang && \
ln -s $(which clang++-21) /usr/bin/clang++ ln -s $(which clang++-22) /usr/bin/clang++
COPY --from=osxcross /osxcross /osxcross COPY --from=osxcross /osxcross /osxcross
ENV PATH="/osxcross/bin:$PATH" \ ENV PATH="/osxcross/bin:$PATH" \
LD_LIBRARY_PATH="/osxcross/lib" \ LD_LIBRARY_PATH="/osxcross/lib" \
CC="clang-21" CXX="clang++-21" AR="llvm-ar-21" \ CC="clang-22" CXX="clang++-22" AR="llvm-ar-22" \
CC_x86_64-apple-darwin="o64-clang" \ CC_x86_64-apple-darwin="o64-clang" \
CXX_x86_64-apple-darwin="o64-clang++" \ CXX_x86_64-apple-darwin="o64-clang++" \
CC_aarch64-apple-darwin="oa64-clang" \ CC_aarch64-apple-darwin="oa64-clang" \
@@ -47,11 +47,11 @@ ENV PATH="/osxcross/bin:$PATH" \
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" \ 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-21" \ CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link-22" \
CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \ CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER="o64-clang" \
CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-21" \ CARGO_TARGET_X86_64_APPLE_DARWIN_AR="llvm-ar-22" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER="oa64-clang" \
CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-21" \ CARGO_TARGET_AARCH64_APPLE_DARWIN_AR="llvm-ar-22" \
RC_PATH="llvm-rc-21" \ RC_PATH="llvm-rc-22" \
MACOSX_DEPLOYMENT_TARGET="14.5" MACOSX_DEPLOYMENT_TARGET="14.5"
ENTRYPOINT ["bash"] ENTRYPOINT ["bash"]
+4 -4
View File
@@ -4,11 +4,11 @@
# The apt index for these is broken in bookworm, and upgrading to trixie # The apt index for these is broken in bookworm, and upgrading to trixie
# would make them depend on too new of a libc version. # 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 # https://apt.llvm.org/bookworm/pool/main/l/llvm-toolchain-22/clang-22_22.1.0~%2B%2B20260223093050%2Bcd5897175d0d-1~exp1~20260223093246.28_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') PACKAGES=('clang-22' 'clang-tools-22' 'libclang-common-22-dev' 'libclang-cpp22' 'libclang-rt-22-dev' 'libclang1-22' 'libllvm22' 'lld-22' 'llvm-22' 'llvm-22-dev' 'llvm-22-linker-tools' 'llvm-22-runtime' 'llvm-22-tools')
FILES=() FILES=()
URL='https://apt.llvm.org/bookworm/pool/main/l/llvm-toolchain-21' URL='https://apt.llvm.org/bookworm/pool/main/l/llvm-toolchain-22'
VERSION='21.1.5~%2B%2B20251023083151%2B45afac62e373-1~exp1~20251023083333.51_amd64.deb' VERSION='22.1.0~%2B%2B20260223093050%2Bcd5897175d0d-1~exp1~20260223093246.28_amd64.deb'
apt-get install -y curl python3 apt-get install -y curl python3
for package in "${PACKAGES[@]}"; do for package in "${PACKAGES[@]}"; do
+1 -1
View File
@@ -1,4 +1,4 @@
Types: deb deb-src Types: deb deb-src
URIs: http://apt.llvm.org/bookworm/ URIs: http://apt.llvm.org/bookworm/
Suites: llvm-toolchain-bookworm-21 Suites: llvm-toolchain-bookworm-22
Components: main Components: main
+10 -7
View File
@@ -288,6 +288,7 @@ impl Application {
} }
} }
let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point); let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point);
viewport.window.pre_present_notify();
self.shared.painter.paint_and_update_textures( self.shared.painter.paint_and_update_textures(
viewport.id, viewport.id,
output.pixels_per_point, output.pixels_per_point,
@@ -296,7 +297,6 @@ impl Application {
&output.textures_delta, &output.textures_delta,
vec![], vec![],
); );
viewport.window.pre_present_notify();
} }
let mut active_viewports = ViewportIdSet::default(); let mut active_viewports = ViewportIdSet::default();
for (id, update) in updates { for (id, update) in updates {
@@ -398,22 +398,25 @@ impl ApplicationHandler<UserEvent> for Application {
if close_requested && viewport_id == ViewportId::ROOT { if close_requested && viewport_id == ViewportId::ROOT {
event_loop.exit(); event_loop.exit();
} }
if response.repaint {
viewport.window.request_redraw();
}
if !response.consumed { if !response.consumed {
match event { match event {
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. }
if !self.app.handle_key_event(viewport_id, &event) { if !self.app.handle_key_event(viewport_id, &event) =>
{
self.controllers.handle_key_event(&event); self.controllers.handle_key_event(&event);
} }
}
WindowEvent::Focused(new_focused) => { WindowEvent::Focused(new_focused) => {
self.focused = new_focused.then_some(viewport_id); self.focused = new_focused.then_some(viewport_id);
} }
WindowEvent::RedrawRequested => {
self.repaint_all(event_loop);
}
_ => {} _ => {}
} }
} }
if response.repaint {
self.repaint_all(event_loop);
}
self.check_repaint(event_loop); self.check_repaint(event_loop);
} }
+117 -17
View File
@@ -1,4 +1,7 @@
use std::sync::{Arc, RwLock}; use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
use gilrs::{Event as GamepadEvent, EventType, GamepadId, ev::Code}; use gilrs::{Event as GamepadEvent, EventType, GamepadId, ev::Code};
use winit::{ use winit::{
@@ -8,12 +11,13 @@ use winit::{
use crate::{ use crate::{
emulator::{EmulatorClient, EmulatorCommand, SimId, VBKey}, emulator::{EmulatorClient, EmulatorCommand, SimId, VBKey},
input::{InputMapping, MappingProvider}, input::{AxisMapping, InputMapping, MappingProvider},
}; };
pub struct Controller { pub struct Controller {
pub sim_id: SimId, pub sim_id: SimId,
state: VBKey, state: VBKey,
axis_values: HashMap<(GamepadId, Code), f32>,
mapping: Arc<RwLock<InputMapping>>, mapping: Arc<RwLock<InputMapping>>,
} }
@@ -22,6 +26,7 @@ impl Controller {
Self { Self {
sim_id, sim_id,
state: VBKey::SGN, state: VBKey::SGN,
axis_values: HashMap::new(),
mapping: mappings.for_sim(sim_id).clone(), mapping: mappings.for_sim(sim_id).clone(),
} }
} }
@@ -45,20 +50,23 @@ impl Controller {
(VBKey::empty(), mappings) (VBKey::empty(), mappings)
} }
EventType::AxisChanged(_, value, code) => { EventType::AxisChanged(_, value, code) => {
let (neg, pos) = self.map_axis(&event.id, &code)?; let mapping = self.map_axis(&event.id, &code)?;
let mut pressed = VBKey::empty(); self.axis_values.insert((event.id, code), value);
let mut released = VBKey::empty();
if value < -0.75 { let pair_value = mapping
pressed = pressed.union(neg); .pair
} .and_then(|p| self.axis_values.get(&(event.id, p)))
if value > 0.75 { .copied()
pressed = pressed.union(pos); .unwrap_or_default();
} let neg = mapping.neg;
if value > -0.65 { let pos = mapping.pos;
released = released.union(neg);
} let (mut pressed, mut released) = axis_presses(value, pair_value, neg, pos);
if value < 0.65 { if let Some(other) = mapping.pair.and_then(|p| self.map_axis(&event.id, &p)) {
released = released.union(pos); let (other_pressed, other_released) =
axis_presses(pair_value, value, other.neg, other.pos);
pressed = pressed.union(other_pressed);
released = released.union(other_released);
} }
(pressed, released) (pressed, released)
} }
@@ -87,7 +95,7 @@ impl Controller {
self.mapping.read().unwrap().map_button(id, code) self.mapping.read().unwrap().map_button(id, code)
} }
fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<(VBKey, VBKey)> { fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<AxisMapping> {
self.mapping.read().unwrap().map_axis(id, code) self.mapping.read().unwrap().map_axis(id, code)
} }
} }
@@ -126,3 +134,95 @@ impl ControllerManager {
} }
} }
} }
fn axis_presses(value: f32, pair_value: f32, neg: VBKey, pos: VBKey) -> (VBKey, VBKey) {
use std::f32::consts::FRAC_PI_3;
let mut pressed = VBKey::empty();
let mut released = VBKey::empty();
let magnitude = value.hypot(pair_value);
let abs_angle = pair_value.atan2(value).abs();
if magnitude < 0.65 {
released = released.union(neg).union(pos);
} else if abs_angle <= FRAC_PI_3 {
// stick tilted towards positive
released = released.union(neg);
if magnitude >= 0.75 {
pressed = pressed.union(pos);
}
} else if abs_angle >= 2.0 * FRAC_PI_3 {
// stick tilted towards negative
released = released.union(pos);
if magnitude >= 0.75 {
pressed = pressed.union(neg);
}
} else {
released = released.union(neg).union(pos);
}
(pressed, released)
}
#[cfg(test)]
mod tests {
use super::{VBKey, axis_presses};
const NEG: VBKey = VBKey::LL;
const POS: VBKey = VBKey::LR;
const NONE: VBKey = VBKey::empty();
const BOTH: VBKey = NEG.union(POS);
#[test]
fn detects_no_input() {
let (pressed, released) = axis_presses(0.0, 0.0, NEG, POS);
assert_eq!(pressed, NONE);
assert_eq!(released, BOTH);
}
#[test]
fn detects_pos_input() {
let (pressed, released) = axis_presses(1.0, 0.0, NEG, POS);
assert_eq!(pressed, POS);
assert_eq!(released, NEG);
}
#[test]
fn detects_neg_input() {
let (pressed, released) = axis_presses(-1.0, 0.0, NEG, POS);
assert_eq!(pressed, NEG);
assert_eq!(released, POS);
}
#[test]
fn respects_dead_zone() {
let (pressed, released) = axis_presses(0.70, 0.0, NEG, POS);
assert_eq!(pressed, NONE);
assert_eq!(released, NEG);
}
#[test]
fn handles_diagonals_ok() {
let (pressed, released) = axis_presses(0.6, 0.6, NEG, POS);
assert_eq!(pressed, POS);
assert_eq!(released, NEG);
}
#[test]
fn ignores_diagonal_noise() {
let (pressed, released) = axis_presses(-1.0, -0.1484245, NEG, POS);
assert_eq!(pressed, NEG);
assert_eq!(released, POS);
}
#[test]
fn ignores_small_values() {
let (pressed, released) = axis_presses(-0.1484245, -1.0, NEG, POS);
assert_eq!(pressed, NONE);
assert_eq!(released, BOTH);
}
}
+34 -23
View File
@@ -52,12 +52,28 @@ pub trait Mappings {
fn use_default_mappings(&mut self); fn use_default_mappings(&mut self);
} }
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct AxisMapping {
pub neg: VBKey,
pub pos: VBKey,
pub pair: Option<Code>,
}
impl Default for AxisMapping {
fn default() -> Self {
Self {
neg: VBKey::empty(),
pos: VBKey::empty(),
pair: None,
}
}
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct GamepadMapping { pub struct GamepadMapping {
buttons: HashMap<Code, VBKey>, buttons: HashMap<Code, VBKey>,
axes: HashMap<Code, (VBKey, VBKey)>, axes: HashMap<Code, AxisMapping>,
default_buttons: HashMap<Code, VBKey>, default_buttons: HashMap<Code, VBKey>,
default_axes: HashMap<Code, (VBKey, VBKey)>, default_axes: HashMap<Code, AxisMapping>,
} }
impl GamepadMapping { impl GamepadMapping {
@@ -78,7 +94,8 @@ impl GamepadMapping {
let mut default_axes = HashMap::new(); let mut default_axes = HashMap::new();
let mut default_axis = |axis: Axis, neg: VBKey, pos: VBKey| { let mut default_axis = |axis: Axis, neg: VBKey, pos: VBKey| {
if let Some(code) = gamepad.axis_code(axis) { if let Some(code) = gamepad.axis_code(axis) {
default_axes.insert(code, (neg, pos)); let pair = axis.second_axis().and_then(|a| gamepad.axis_code(a));
default_axes.insert(code, AxisMapping { neg, pos, pair });
} }
}; };
default_axis(Axis::LeftStickX, VBKey::LL, VBKey::LR); default_axis(Axis::LeftStickX, VBKey::LL, VBKey::LR);
@@ -102,19 +119,13 @@ impl GamepadMapping {
} }
pub fn add_axis_neg_mapping(&mut self, key: VBKey, code: Code) { pub fn add_axis_neg_mapping(&mut self, key: VBKey, code: Code) {
let entry = self let entry = self.axes.entry(code).or_default();
.axes entry.neg = entry.neg.union(key);
.entry(code)
.or_insert((VBKey::empty(), VBKey::empty()));
entry.0 = entry.0.union(key);
} }
pub fn add_axis_pos_mapping(&mut self, key: VBKey, code: Code) { pub fn add_axis_pos_mapping(&mut self, key: VBKey, code: Code) {
let entry = self let entry = self.axes.entry(code).or_default();
.axes entry.pos = entry.pos.union(key);
.entry(code)
.or_insert((VBKey::empty(), VBKey::empty()));
entry.1 = entry.1.union(key);
} }
fn save_mappings(&self) -> PersistedGamepadMapping { fn save_mappings(&self) -> PersistedGamepadMapping {
@@ -145,11 +156,11 @@ impl GamepadMapping {
impl Mappings for GamepadMapping { impl Mappings for GamepadMapping {
fn mapping_names(&self) -> HashMap<VBKey, Vec<String>> { fn mapping_names(&self) -> HashMap<VBKey, Vec<String>> {
let mut results: HashMap<VBKey, Vec<String>> = HashMap::new(); let mut results: HashMap<VBKey, Vec<String>> = HashMap::new();
for (axis, (left_keys, right_keys)) in &self.axes { for (axis, mapping) in &self.axes {
for key in left_keys.iter() { for key in mapping.neg.iter() {
results.entry(key).or_default().push(format!("-{axis}")); results.entry(key).or_default().push(format!("-{axis}"));
} }
for key in right_keys.iter() { for key in mapping.pos.iter() {
results.entry(key).or_default().push(format!("+{axis}")); results.entry(key).or_default().push(format!("+{axis}"));
} }
} }
@@ -162,10 +173,10 @@ impl Mappings for GamepadMapping {
} }
fn clear_mappings(&mut self, key: VBKey) { fn clear_mappings(&mut self, key: VBKey) {
self.axes.retain(|_, (left, right)| { self.axes.retain(|_, mapping| {
*left = left.difference(key); mapping.neg = mapping.neg.difference(key);
*right = right.difference(key); mapping.pos = mapping.pos.difference(key);
!(left.is_empty() && right.is_empty()) !(mapping.neg.is_empty() && mapping.pos.is_empty())
}); });
self.buttons.retain(|_, keys| { self.buttons.retain(|_, keys| {
*keys = keys.difference(key); *keys = keys.difference(key);
@@ -200,7 +211,7 @@ impl InputMapping {
mappings.buttons.get(code).copied() mappings.buttons.get(code).copied()
} }
pub fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<(VBKey, VBKey)> { pub fn map_axis(&self, id: &GamepadId, code: &Code) -> Option<AxisMapping> {
let mappings = self.gamepads.get(id)?.read().unwrap(); let mappings = self.gamepads.get(id)?.read().unwrap();
mappings.axes.get(code).copied() mappings.axes.get(code).copied()
} }
@@ -459,9 +470,9 @@ struct PersistedKeyboardMapping {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct PersistedGamepadMapping { struct PersistedGamepadMapping {
buttons: Vec<(Code, VBKey)>, buttons: Vec<(Code, VBKey)>,
axes: Vec<(Code, (VBKey, VBKey))>, axes: Vec<(Code, AxisMapping)>,
default_buttons: Vec<(Code, VBKey)>, default_buttons: Vec<(Code, VBKey)>,
default_axes: Vec<(Code, (VBKey, VBKey))>, default_axes: Vec<(Code, AxisMapping)>,
} }
#[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
+2 -2
View File
@@ -1081,8 +1081,8 @@ impl<'a> SourceCoordCalculator<'a> {
(sx, sy) (sx, sy)
} }
SourceParam::Affine(affine) => { SourceParam::Affine(affine) => {
let sx = affine_coord(affine.src_x, x, affine.dx, affine.src_parallax.min(0)); let sx = affine_coord(affine.src_x, x, affine.dx, -(affine.src_parallax.min(0)));
let sy = affine_coord(affine.src_y, x, affine.dy, affine.src_parallax.min(0)); let sy = affine_coord(affine.src_y, x, affine.dy, -(affine.src_parallax.min(0)));
(sx, sy) (sx, sy)
} }
} }