diff --git a/.github/workflows/build-linux-arm.yml b/.github/workflows/build-linux-arm.yml index e4d9b832..1fc2e47c 100644 --- a/.github/workflows/build-linux-arm.yml +++ b/.github/workflows/build-linux-arm.yml @@ -1,6 +1,7 @@ name: Linux ARM on: + workflow_dispatch: push: branches: - "main" @@ -14,104 +15,168 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name != 'release' }} +env: + # The oldest userland we target: ArkOS and AmberELEC are glibc 2.28-2.30. zig + # supplies stubs for it, so this is the whole floor decision — no archived + # distro to build in. Keep in step with tools/arm/glibc-floor. + GLIBC_FLOOR: "2.28" + ZIG_VERSION: "0.15.2" + ZIGBUILD_VERSION: "0.23.0" + SDL_VERSION: "2.26.5+dfsg-1" + jobs: - build-aarch64: - # Native arm64 runner (free for public repos): no cross-compile plumbing, - # native codegen, and an older glibc (22.04) so the binary runs on more - # devices. Keeps sdl2-bundled so the binary is self-contained (no runtime - # SDL2 dependency). file-dialog stays off (it shells out to zenity/kdialog, - # absent on handhelds); file-browser is the in-app picker. - runs-on: ubuntu-22.04-arm + build: + # aarch64 is what most handhelds run; armhf is the older RK3326 devices. One + # build per arch, shared by every package below, so the binary in the + # PortMaster zip is the same file the plain Linux zip ships. + runs-on: ubuntu-22.04 + strategy: + matrix: + include: + - arch: aarch64 + target: aarch64-unknown-linux-gnu + libdir: aarch64-linux-gnu + deb_arch: arm64 + - arch: armhf + target: armv7-unknown-linux-gnueabihf + libdir: arm-linux-gnueabihf + deb_arch: armhf + env: + TARGET: ${{ matrix.target }} + LIBDIR: ${{ matrix.libdir }} + DEB_ARCH: ${{ matrix.deb_arch }} steps: - uses: actions/checkout@v4 - - name: Install build dependencies - run: sudo apt-get update && sudo apt-get install -y build-essential cmake zip + - name: Rust toolchain + run: | + set -eux + rustup target add "$TARGET" + rustc --version + + - name: zig and cargo-zigbuild + run: | + set -eux + curl --retry 5 -fsSL \ + "https://ziglang.org/download/$ZIG_VERSION/zig-x86_64-linux-$ZIG_VERSION.tar.xz" \ + | tar -xJ -C /opt + echo "/opt/zig-x86_64-linux-$ZIG_VERSION" >> "$GITHUB_PATH" + curl --retry 5 -fsSL \ + "https://github.com/rust-cross/cargo-zigbuild/releases/download/v$ZIGBUILD_VERSION/cargo-zigbuild-x86_64-unknown-linux-musl.tar.xz" \ + | sudo tar -xJ -C /usr/local/bin --strip-components=1 \ + "cargo-zigbuild-x86_64-unknown-linux-musl/cargo-zigbuild" - name: Cache Rust build uses: Swatinem/rust-cache@v2 with: - key: aarch64 + key: ${{ matrix.arch }} + + - name: SDL2 for linking + run: | + set -eux + # Only its symbols matter; the device loads its own SDL2, which every CFW + # patches for its panel and pad. Ubuntu's is 2.0.20, which predates + # SDL_RenderGeometry — what the UI's canvas painter draws through — so + # take Debian's. + pool=http://deb.debian.org/debian/pool/main/libs/libsdl2 + mkdir -p /opt/sdl2 + for pkg in libsdl2-2.0-0 libsdl2-dev; do + curl --retry 5 -fsSLO "$pool/${pkg}_${SDL_VERSION}_${DEB_ARCH}.deb" + dpkg -x "${pkg}_${SDL_VERSION}_${DEB_ARCH}.deb" /opt/sdl2 + done + ls -l /opt/sdl2/usr/lib/$LIBDIR/libSDL2* - name: Build + env: + # No linker override: cargo-zigbuild points the linker and cc at zig, + # and a -C linker of our own would opt out of it. + # --allow-shlib-undefined: this libSDL2 pulls in X11/wayland/alsa/gbm, + # which the sysroot lacks and we never call. + RUSTFLAGS: "-L /opt/sdl2/usr/lib/${{ matrix.libdir }} -C link-arg=-Wl,--allow-shlib-undefined" run: | - cargo build --release --no-default-features --features "frontend-modern file-browser sdl2-bundled" -p desktop + set -eux + # What a handheld build is, shared with tools/arm/build.sh. + cargo zigbuild --release --target "$TARGET.$GLIBC_FLOOR" $(cat tools/arm/cargo-args) - - name: Prepare release + - name: Stage binary run: | + set -eux mkdir -p dist - cp target/release/oxgbc dist/ - - - name: Zip release - run: cd dist && zip -r oxgbc-linux-aarch64.zip . + cp "target/$TARGET/release/oxgbc" dist/ + ls -la dist/ - - name: Upload artifact - if: github.event_name != 'release' + - name: Check the glibc floor + run: | + set -eux + # Above the floor the loader refuses the binary on the device. Fail + # here instead of there. + newer=$(readelf -V dist/oxgbc | grep -o 'GLIBC_2\.[0-9]*' | sort -uV \ + | awk -F. -v f="${GLIBC_FLOOR#2.}" '$2 > f' | tr '\n' ' ') + if [ -n "$newer" ]; then + echo "binary requires $newer; the floor is GLIBC_$GLIBC_FLOOR" >&2 + exit 1 + fi + + - name: Upload binary artifact uses: actions/upload-artifact@v4 with: - name: oxgbc-linux-aarch64.zip - path: dist/oxgbc-linux-aarch64.zip - env: - GITHUB_TOKEN: ${{ secrets.TOKEN }} + name: oxgbc-linux-${{ matrix.arch }} + path: dist/oxgbc + + - name: Zip for release + if: github.event_name == 'release' + run: | + set -eux + (cd dist && zip "../oxgbc-linux-${{ matrix.arch }}.zip" oxgbc) - name: Upload to GitHub Release if: github.event_name == 'release' uses: softprops/action-gh-release@v2 with: - files: dist/oxgbc-linux-aarch64.zip + files: oxgbc-linux-${{ matrix.arch }}.zip env: GITHUB_TOKEN: ${{ secrets.TOKEN }} - build-armv7: - # 32-bit ARM has no native runner, so cross-compile on x86_64. - runs-on: ubuntu-latest - env: - CC_armv7_unknown_linux_gnueabihf: arm-linux-gnueabihf-gcc - CXX_armv7_unknown_linux_gnueabihf: arm-linux-gnueabihf-g++ - AR_armv7_unknown_linux_gnueabihf: arm-linux-gnueabihf-ar + portmaster: + needs: build + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - - name: Install cross toolchain - run: | - sudo apt-get update - sudo apt-get install -y gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf zip - rustup target add armv7-unknown-linux-gnueabihf + - uses: actions/download-artifact@v4 + with: + name: oxgbc-linux-aarch64 + path: bin/aarch64 - - name: Cache Rust build - uses: Swatinem/rust-cache@v2 + - uses: actions/download-artifact@v4 with: - key: armv7 + name: oxgbc-linux-armhf + path: bin/armhf - - name: Build + - name: Assemble the port layout + # The layout and the zip live in the script, so a change to either is a + # change in one place rather than in two. Artifacts travel as zips, which + # drop file modes; the script sets them. env: - RUSTFLAGS: "-C linker=arm-linux-gnueabihf-gcc" - PKG_CONFIG_ALLOW_CROSS: "1" + OXGBC_AARCH64: bin/aarch64/oxgbc + OXGBC_ARMHF: bin/armhf/oxgbc run: | - cargo build --release --no-default-features --features "frontend-modern file-browser sdl2-bundled" \ - --target=armv7-unknown-linux-gnueabihf -p desktop + set -eux + tools/package.sh . + ls -laR portmaster-dist - - name: Prepare release - run: | - mkdir -p dist - cp target/armv7-unknown-linux-gnueabihf/release/oxgbc dist/ - - - name: Zip release - run: cd dist && zip -r oxgbc-linux-armv7.zip . - - - name: Upload artifact - if: github.event_name != 'release' + - name: Upload package artifact uses: actions/upload-artifact@v4 with: - name: oxgbc-linux-armv7.zip - path: dist/oxgbc-linux-armv7.zip - env: - GITHUB_TOKEN: ${{ secrets.TOKEN }} + name: oxgbc-portmaster + path: portmaster-dist - name: Upload to GitHub Release if: github.event_name == 'release' uses: softprops/action-gh-release@v2 with: - files: dist/oxgbc-linux-armv7.zip + files: | + oxgbc-portmaster.zip + oxgbc-portmaster.zip.sha256 env: GITHUB_TOKEN: ${{ secrets.TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index a04b0c53..1be4e918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,47 @@ All notable changes to oxGBC are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to a simple incrementing release number. +## [Unreleased] + +### Added +- **A PortMaster package for the Linux handhelds** — `oxgbc-portmaster.zip`, + carrying both ARM binaries, a launcher that shelves the card's Game Boy folder + and keeps saves in the port's own directory, and the port's metadata. +- `OXGBC_ROMS_DIR` names the folder a fresh install shelves and browses first. +- **Select + Y opens the menu** as well as Select + Start, which a handheld never + sees — there it closes whatever is running. +- A direction held on the pad repeats in the menus, as one held on the keyboard + always has: a shelf of a hundred carts no longer takes a hundred presses. + +### Fixed +- Back out of the settings returns to the screen they were opened from. Opened from + the shelf with a game loaded, it landed on the pause overlay instead. +- A shader the device cannot compile falls back to Passthrough instead of taking + the app down with it — GLSL ES 1.00 has no bitwise operators, which several of + them use. +- The shelf lists zipped ROMs, and matches an extension whatever its case: a + folder of zips — how a collection usually arrives — came out empty, though both + file browsers opened them. One list of extensions now serves all three. +- The retro frontend's file browser offers zips too. + +### Changed +- **New picture defaults**: integer scaling, the Mono LCD shader on the GL backend, + and the grid filter on the SDL2 one — whole pixels, and pixel edges either way + the frame is drawn. +- **Start and Select have menu roles of their own.** Start opens whatever else can be + done with the focused item — a cartridge's sheet, a save slot's, the typed name of a + rename — and Select reaches the settings from any screen, which took walking the + library's header to the gear before. Start used to be a second Confirm, which is + also what made the menu hotkey fire the highlighted row on its way through. +- Rewind moved to L1 and slow motion to Y, so the shoulders run time the way every + other pad does: L1 back, R1 forward. +- The emulated model defaults to auto — a cart runs as the machine its header names. + Forcing CGB, which colorizes DMG games, is now a setting rather than the default. +- The ARM builds link against the device's SDL2 instead of bundling one, and are + cross-built against glibc 2.28: the previous ones needed 2.35 (aarch64) or 2.39 + (armhf), which no handheld userland has. `oxgbc-linux-armv7.zip` is now + `oxgbc-linux-armhf.zip`. + ## [0.22] - 2026-08-14 ### Added diff --git a/Makefile b/Makefile index e540b078..f29c9a3f 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ PORT ?= 8080 WEB := crates/web .DEFAULT_GOAL := help -.PHONY: help serve web bench-ab per-tick-cli +.PHONY: help serve web bench-ab per-tick-cli portmaster help: ## List available targets @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ @@ -25,6 +25,9 @@ web: ## Build the WASM module + JS bindings into crates/web/pkg bench-ab: ## Interleaved A/B bench matrix: make bench-ab A= [B=] [PAIRS=5]; games via OXGBC_BENCH_GB_ROM/OXGBC_BENCH_GBC_ROM @A="$(A)" B="$(B)" PAIRS="$(or $(PAIRS),5)" ./scripts/bench-ab.sh +portmaster: ## Build the PortMaster zip into target/ (cross-builds both ARM binaries in docker) + ./tools/package.sh + per-tick-cli: ## Build the reference oxgbc-cli (pre-scheduler per-tick chain) into target-per-tick/release cargo build --release -p cli --features core/per-tick-clock --target-dir target-per-tick @echo "reference binary: target-per-tick/release/oxgbc-cli" diff --git a/README.md b/README.md index 6db8fbb3..04b1a0da 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,10 @@ The emulator passes a wide range of community test suites and is continuously va Every binding is customizable from the in-app settings menu (or by editing `config.json`), for both keyboard and gamepad. +In the menus the d-pad moves and A confirms, B backs out, **Start** opens whatever +else can be done with the focused item — a cartridge's sheet, a save slot's — and +**Select** goes straight to the settings. +
Default control mappings (click to expand) @@ -114,17 +118,17 @@ Every binding is customizable from the in-app settings menu (or by editing | A | X | A | | Start | Enter or S | Start | | Select | Backspace or A | Select | -| Rewind (hold) | R | Y | +| Rewind (hold) | R | LB | | Turbo mode (hold) | Tab | RB | -| Slow mode (hold) | Space | LB | -| Main menu | Esc or Q | Select + Start | +| Slow mode (hold) | Space | Y | +| Main menu | Esc or Q | Select + Start or Select + Y | | Screen scale Up and Down | + (Equals) and - (Minus) | | | Fullscreen Toggle | F11 | | | Mute audio | M | | | Invert palette | I | Select + X | | Next palette | P | X | -| Load save state (1–4) | F1–F4 | RT or Select + RB | -| Create save state (1–9) | 1–9 | LT or Select + LB | +| Load save state (1–4) | F1–F4 | Select + LB | +| Create save state (1–9) | 1–9 | Select + RB | | Volume Up and Down | PageUp and PageDown | Start + D-pad Up and Start + D-pad Down | | Prev and Next Save State Slot | | Start + D-pad Right and Start + D-pad Left | | Prev and Next Shader | [ and ] | Select + B and Select + A | @@ -143,6 +147,14 @@ ARM for retro handhelds), or Android — from the [**Releases**](https://github.com/mxmgorin/oxgbc/releases/latest) page, or [**play online**](https://mxmgorin.github.io/oxgbc/) with nothing to install. +### Retro handhelds + +`oxgbc-portmaster.zip` is a [PortMaster](https://portmaster.games) port for the +Linux handhelds (aarch64 and armhf, glibc 2.28 and up): unzip it into `ports/` +on the card, and oxGBC appears in the Ports menu. It shelves your `gb`, `gbc` or +`gameboy` folder on the first launch and keeps its saves and settings in +`ports/oxgbc`. `make portmaster` builds the same zip locally. + ### macOS first launch Because the app is only ad-hoc signed (no paid Apple Developer ID), Gatekeeper diff --git a/crates/app/src/app/mod.rs b/crates/app/src/app/mod.rs index cc28b1d4..5e64f580 100644 --- a/crates/app/src/app/mod.rs +++ b/crates/app/src/app/mod.rs @@ -148,6 +148,7 @@ where tick = now; input.handle_events(self, emu); + input.handle_repeat(self, emu); match self.state { AppState::Quitting => break, diff --git a/crates/app/src/config.rs b/crates/app/src/config.rs index 2f94f09c..b0aa8d5f 100644 --- a/crates/app/src/config.rs +++ b/crates/app/src/config.rs @@ -4,7 +4,7 @@ use crate::video::frame_blend::FrameBlendMode; use crate::video::palette::LcdPalette; use crate::video::shader::{ShaderFrameBlendMode, ShaderPrecision}; use core::apu::apu::ApuConfig; -use core::emu::config::{EmuConfig, GbModel}; +use core::emu::config::EmuConfig; use core::ppu::tile::PixelColor; use core::ppu::LCD_X_RES; use core::ppu::LCD_Y_RES; @@ -301,11 +301,10 @@ impl Default for AppConfig { auto_save_state: false, current_save_slot: 0, current_load_slot: 0, - // Default to CGB so DMG games are colorized out of the box, like a - // real Game Boy Color. DMG-only carts still render on the DMG-compat - // path (see App::apply_dmg_palette); users can pick DMG for mono. + // Auto: a cart runs as the machine its header names. Forcing CGB instead + // colorizes DMG games, which is a setting rather than a default. emulation: EmuConfig { - model: Some(GbModel::Cgb), + model: None, ..Default::default() }, audio: AudioConfig { @@ -324,7 +323,9 @@ impl Default for AppConfig { interface: InterfaceConfig { selected_palette_idx: 0, scale: 5.0, - scale_mode: ScaleMode::Fit, + // Whole pixels: a fractional stretch gives rows of uneven + // thickness, which is what Fit does on a handheld's 640x480. + scale_mode: ScaleMode::Integer, is_fullscreen: true, show_fps: false, show_tiles: false, @@ -335,15 +336,17 @@ impl Default for AppConfig { frame_blend_mode: FrameBlendMode::None, blend_dim: 1.0, backend: VideoBackendType::Gl, + // The grid is this backend's answer to the shader below: no GL + // to run one, so the pixel edges come from a filter instead. sdl2: Sdl2Config { - grid_enabled: false, + grid_enabled: true, subpixel_enabled: false, dot_matrix_enabled: false, scanline_enabled: false, vignette_enabled: false, }, gl: GlConfig { - shader_name: "passthrough".to_string(), + shader_name: "Mono LCD".to_string(), shader_frame_blend_mode: ShaderFrameBlendMode::Simple, shader_precision: ShaderPrecision::Auto, }, diff --git a/crates/app/src/frontend/mod.rs b/crates/app/src/frontend/mod.rs index 2485e6da..22841a3c 100644 --- a/crates/app/src/frontend/mod.rs +++ b/crates/app/src/frontend/mod.rs @@ -37,9 +37,13 @@ pub enum NavAction { Right, Confirm, Back, - /// Whatever else can be done with the focused item — the Select button, which - /// no menu needs for anything else. + /// Whatever else can be done with the focused item — the Start button, which + /// A already covers as a confirm. Options, + /// The settings, from wherever a menu is — the Select button, which no menu + /// needs for anything else. Walking a header to them is several presses on a + /// device with no pointer. + Settings, } /// What the rebinding flow did with a raw input. @@ -109,6 +113,10 @@ pub trait Frontend { /// Offer a raw input to the rebinding flow before it reaches the emulator. fn capture_bind(&mut self, input: I, pressed: bool) -> Capture; + /// Whether that flow is waiting for an input. Nothing may be synthesized while it + /// is: a repeat of the held direction would bind itself to the row. + fn is_capturing(&self) -> bool; + /// Mark the UI dirty — app state it displays changed underneath it. fn request_update(&mut self, what: UiUpdate); diff --git a/crates/app/src/frontend/modern/browse.rs b/crates/app/src/frontend/modern/browse.rs index d1d04eb2..f4dcff3c 100644 --- a/crates/app/src/frontend/modern/browse.rs +++ b/crates/app/src/frontend/modern/browse.rs @@ -4,10 +4,10 @@ use crate::frontend::BrowseTarget; use crate::storage::browser::{FileBrowser, FILE_BROWSER_BACK_ITEM}; +use crate::storage::ROM_EXTENSIONS; use std::path::{Path, PathBuf}; /// A walk shows only what it can pick: games, or pictures, or nothing but folders. -const ROM_EXTENSIONS: &[&str] = &["gb", "gbc", "zip"]; const COVER_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg"]; const FOLDERS_ONLY: &[&str] = &[]; /// The walk keeps its own selection for the text menu's paging; the modern screen diff --git a/crates/app/src/frontend/modern/mod.rs b/crates/app/src/frontend/modern/mod.rs index d3263f1e..7facd2e8 100644 --- a/crates/app/src/frontend/modern/mod.rs +++ b/crates/app/src/frontend/modern/mod.rs @@ -217,6 +217,10 @@ impl Frontend for ModernFrontend { Capture::Took(settings::bind(id, input)) } + fn is_capturing(&self) -> bool { + self.capturing.row.is_some() + } + fn request_update(&mut self, what: UiUpdate) { // Even an update no view is built from still has to reach the screen. self.unpainted = true; @@ -819,6 +823,7 @@ fn into_nav(action: NavAction) -> ui::NavAction { NavAction::Confirm => ui::NavAction::Confirm, NavAction::Back => ui::NavAction::Back, NavAction::Options => ui::NavAction::Options, + NavAction::Settings => ui::NavAction::Settings, } } diff --git a/crates/app/src/frontend/retro/menu/files.rs b/crates/app/src/frontend/retro/menu/files.rs index a3ed6cfa..d836c3eb 100644 --- a/crates/app/src/frontend/retro/menu/files.rs +++ b/crates/app/src/frontend/retro/menu/files.rs @@ -2,6 +2,7 @@ use super::{SubMenu, MAX_MENU_ITEMS_PER_PAGE, MAX_MENU_ITEM_CHARS}; use crate::cmd::AppCmd; use crate::config::AppConfig; use crate::storage::browser::{FileBrowser, FILE_BROWSER_BACK_ITEM}; +use crate::storage::ROM_EXTENSIONS; use crate::video::truncate_text; use std::path::Path; @@ -12,7 +13,7 @@ pub struct FilesMenu { impl FilesMenu { pub fn new(last_path: Option>) -> Self { - let extensions = &["gb", "gbc"]; + let extensions = ROM_EXTENSIONS; Self { fb: if let Some(last_path) = last_path { diff --git a/crates/app/src/frontend/retro/menu/mod.rs b/crates/app/src/frontend/retro/menu/mod.rs index b96a9baf..fc9db0ab 100644 --- a/crates/app/src/frontend/retro/menu/mod.rs +++ b/crates/app/src/frontend/retro/menu/mod.rs @@ -37,6 +37,14 @@ impl AppMenu { } } + /// Its bind screen is a menu item like any other: the selected one waiting. + pub fn is_waiting_input(&self) -> bool { + matches!( + self.items.get(self.selected_index), + Some(AppMenuItem::WaitInput(_)) + ) + } + pub fn handle_input(&mut self, input: I, pressed: bool) -> Option { let item = self.items.get(self.selected_index).unwrap(); diff --git a/crates/app/src/frontend/retro/mod.rs b/crates/app/src/frontend/retro/mod.rs index a1a4f4d5..91e27d16 100644 --- a/crates/app/src/frontend/retro/mod.rs +++ b/crates/app/src/frontend/retro/mod.rs @@ -46,11 +46,14 @@ impl Frontend for RetroFrontend { NavAction::Down => self.menu.move_down(), NavAction::Left => return self.menu.move_left(ctx.config), NavAction::Right => return self.menu.move_right(ctx.config), - NavAction::Confirm => return self.menu.select(ctx.config, ctx.fs, ctx.roms), + // Every item of a text menu shows its own options in place, so the options + // button is another way of picking the item rather than a screen of its own. + NavAction::Confirm | NavAction::Options => { + return self.menu.select(ctx.config, ctx.fs, ctx.roms) + } NavAction::Back => self.menu.back(), - // Every item of a text menu shows its own options in place; there is - // nothing behind one to open. - NavAction::Options => {} + // This menu's root is the settings; there is nowhere to jump to. + NavAction::Settings => {} } None @@ -71,6 +74,10 @@ impl Frontend for RetroFrontend { } } + fn is_capturing(&self) -> bool { + self.menu.is_waiting_input() + } + /// Every screen of it is one buffer of text, so there is nothing to rebuild /// short of all of it — the overlay included, which paints over that buffer. #[inline(always)] diff --git a/crates/app/src/input/combo.rs b/crates/app/src/input/combo.rs index 398107bd..daa558a7 100644 --- a/crates/app/src/input/combo.rs +++ b/crates/app/src/input/combo.rs @@ -50,6 +50,13 @@ impl ComboHandler { Self { states } } + /// When `button` went down, while it is still down. + pub fn held_since(&self, button: Button) -> Option { + let state = self.states[button.code()]; + + state.pressed.then_some(state.last_pressed) + } + pub fn handle( &mut self, button: Button, @@ -197,6 +204,9 @@ impl Default for ButtonComboBindings { bindings.add_cmd(Button::Start, Button::Back, AppCmd::ToggleMenu); bindings.add_cmd(Button::Start, Button::Guide, AppCmd::ToggleMenu); + // A handheld never sees Start + Select: there it closes whatever is running. + bindings.add_cmd(Button::Back, Button::Y, AppCmd::ToggleMenu); + bindings.add_cmd(Button::Guide, Button::Y, AppCmd::ToggleMenu); bindings.add_cmd( Button::Guide, diff --git a/crates/app/src/input/emu.rs b/crates/app/src/input/emu.rs index fb41e71c..3512e515 100644 --- a/crates/app/src/input/emu.rs +++ b/crates/app/src/input/emu.rs @@ -17,7 +17,7 @@ where { match btn { JoypadButton::Start => return handle_start(pressed, app, emu), - JoypadButton::Select => handle_select(pressed, app, emu), + JoypadButton::Select => return handle_select(pressed, app, emu), JoypadButton::A => return handle_a(pressed, app, emu), JoypadButton::B => handle_b(pressed, app, emu), JoypadButton::Up => handle_up(pressed, app, emu), @@ -123,13 +123,15 @@ where } } +/// Start is the options button while a menu is up: A confirms and B backs out, so +/// a second confirm is all it would otherwise be. pub fn handle_start(pressed: bool, app: &mut App, emu: &mut Emu) -> Option where FS: PlatformFileSystem, FD: PlatformFileDialog, { if app.state == AppState::Paused && pressed { - return nav(NavAction::Confirm, app); + return nav(NavAction::Options, app); } else { emu.runtime.cpu.clock.bus.io.joypad.start = pressed; } @@ -137,16 +139,18 @@ where None } -/// Select is the options button while a menu is up: B already backs out, so -/// nothing else needs it. -pub fn handle_select(pressed: bool, app: &mut App, emu: &mut Emu) +/// Select reaches the settings while a menu is up, which are otherwise several +/// presses of walking a header away. +pub fn handle_select(pressed: bool, app: &mut App, emu: &mut Emu) -> Option where FS: PlatformFileSystem, FD: PlatformFileDialog, { if app.state == AppState::Paused && pressed { - nav(NavAction::Options, app); + return nav(NavAction::Settings, app); } else { emu.runtime.cpu.clock.bus.io.joypad.select = pressed; } + + None } diff --git a/crates/app/src/input/gamepad.rs b/crates/app/src/input/gamepad.rs index 5d820301..48a40d57 100644 --- a/crates/app/src/input/gamepad.rs +++ b/crates/app/src/input/gamepad.rs @@ -55,6 +55,17 @@ impl BindableInput for Button { } } +const DIRECTIONS: [Button; 4] = [ + Button::DPadUp, + Button::DPadDown, + Button::DPadLeft, + Button::DPadRight, +]; + +/// The buttons a direction pairs with in the combo table, which is what makes a held +/// direction something other than a direction. +const MODIFIERS: [Button; 3] = [Button::Back, Button::Guide, Button::Start]; + pub struct GamepadHandler { combo_handler: ComboHandler, } @@ -66,6 +77,24 @@ impl GamepadHandler { } } + /// The direction the pad is holding — the last one pressed while it holds two, or + /// a diagonal would walk the focus along both axes at once. + pub fn held_direction(&self) -> Option