Skip to content
Merged
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
181 changes: 123 additions & 58 deletions .github/workflows/build-linux-arm.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Linux ARM

on:
workflow_dispatch:
push:
branches:
- "main"
Expand All @@ -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 }}
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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) \
Expand All @@ -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=<cli> [B=<cli>] [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"
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<details>
<summary><b>Default control mappings</b> (click to expand)</summary>

Expand All @@ -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 |
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/app/src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ where

tick = now;
input.handle_events(self, emu);
input.handle_repeat(self, emu);

match self.state {
AppState::Quitting => break,
Expand Down
Loading
Loading