Conversation
added 30 commits
July 1, 2026 20:05
…t, shutdown+flush tests)
…st-tested on a real ROM
…me in emulator test
…te, NULL ctx, JNI OOM, no-audio fallback)
…ls, battery saves
…ncel) + surface battery-save errors
…ul skip w/o rgbds)
… SAF display-name saves, JNI/ROM guards, copy_front test
…PIs + iOS surface)
…ate ops + concurrency test
added 21 commits
July 3, 2026 05:55
…al control sprites
…ait play, 8-way d-pad, surface follows layout
…EmulatorActivity has no AppCompat night resources)
…o the d-pad clears the well (found on Moto G4, 360x580dp)
…n short screens (5x not 4x on 360x580dp); menu pill centers between Select/Start
The core is audio-clocked at the Game Boy's 59.7275fps; the render thread free-runs into eglSwapBuffers at the panel rate. On fixed-60Hz panels (Moto G4) 60/59.73 is ~1:1 and smooth, but on variable-refresh LTPO panels (Pixel 8 Pro) SurfaceFlinger presents at 120Hz where 120/59.73 ~= 2.009 -- the 2:1 frame-doubling slips a frame ~once a second (scrolling judder) and the panel rate oscillates hunting a match. Call Surface.setFrameRate(59.7275, FIXED_SOURCE) on surface-ready (API 30+, guarded) so the compositor locks to a clean multiple. API verified against compileSdk 34; not yet verified on-device (Pixel 8 Pro).
…ate via env
The Decode keystore step used `if: ${{ secrets.SAMEBOY_KEYSTORE_BASE64 }}`.
The secrets context is not available in step if: conditionals, so the whole
workflow file was rejected at startup (startup_failure) — taking the build job
down with it, even on PRs. Map the secret into env and gate in shell instead;
export SAMEBOY_KEYSTORE via GITHUB_ENV only when present (else debug-key fallback).
CI had no rgbds, so generateBootRoms skipped and the APK shipped an empty assets/bootroms/. Boot-ROM-less boot hangs some CGB titles (Pokémon Crystal) on a white screen — emu thread runs but the ROM loops at startup. Install rgbds (Ubuntu 24.04 ships 0.7.0) so boot ROMs build, and add a verify step asserting the .bin assets are present so an empty bundle can never ship again.
…stutter) Measured on a Pixel 8 Pro: the emu, render and audio-producer threads all ran pinned to cpu0 (a 1.70GHz A510 little core) at default priority, while the 2.37GHz mid cores and 2.91GHz big core sat idle. Android's EAS scheduler parks default-priority app threads on a little core and lets it idle/throttle between the audio-clocked emu loop's bursty per-frame wakeups, so the loop wakes late — underrunning the audio ring (crackle) AND missing the frame deadline (hitch) at once. Homogeneous SoCs (Moto G4, all A53) show no penalty, which is why it was Pixel-only. Add sched_hint: raise the emu thread to URGENT_AUDIO (-19) and render to DISPLAY (-4), and bias both off the slowest core tier (detected via cpufreq sysfs; a no-op on homogeneous SoCs). No emulation-accuracy change — native 59.7275fps and correct pitch are preserved. Host session test links the new TU.
…he beat Surface.setFrameRate is only a content hint; SurfaceFlinger's touch-boost overrides it and kicks the LTPO panel to 120Hz during play, reintroducing the 120/59.7275 frame-duplication beat. WindowManager preferredMin/MaxDisplayRefreshRate is a DisplayModeDirector vote that survives touch-boost — clamp both to 60 (the lowest mode >= the GB's 59.7275fps). API 30+, no-op on fixed-60 panels (Moto G4). Reasserted in onResume since window attrs can reset across pause.
…n't in SDK 34) preferredMin/MaxDisplayRefreshRate aren't public in compileSdk 34 (build broke: cannot find symbol). Use preferredDisplayModeId instead — it selects an exact Display.Mode and is the strongest app-level refresh lever, surviving touch-boost. Pick the mode at the current resolution with refresh closest to 59.7275fps (=60Hz) so resolution never changes. API 23+, no-op on fixed-rate panels.
…e + min/max reflection) preferredDisplayModeId alone pins the physical mode but LTPO render-rate governors still drift: OnePlus Pad 2 idles the RENDER rate to 50Hz while the emu produces 59.73fps, dropping ~10 frames/s (steady video hitch). Stack all levers: modeId + preferredRefreshRate (public) + preferredMin/MaxDisplayRefreshRate=[hz,hz] via reflection (public since API 30, missing from some SDK stubs) to clamp the render rate to exactly 60. No-op on fixed-rate panels.
…render to 50 setFrameRate(59.7275, FIXED_SOURCE) makes OnePlus's render governor map to a 50Hz render rate, dropping ~10 emu frames/s (steady video hitch). Request 60.0 DEFAULT instead — SurfaceFlinger picks the 60 render rate; emu still runs 59.7275fps (1 dup/3.7s, accurate).
… hitch) The render thread free-ran and re-sampled the framebuffer at the panel's vsync, beating against the emu's audio clock and posting duplicate frames. That made SurfaceFlinger's content-rate detector see the panel rate (OnePlus: idles render to 50Hz -> drops ~10 emu frames/s -> hitch). Couple render to production: emu bumps frame_seq + signals frame_cv per completed frame; the render thread blocks in sb_emu_wait_frame until a new frame exists (100ms timeout bounds shutdown/pause), then presents exactly that frame. Now it posts at the true ~59.73fps in-order, no duplicates, no re-sample beat — and SF sees ~60fps content. sb_emu_wake() unblocks the waiter on stop.
On-device finding: targeting 60Hz is a trap — it's just above the GB's 59.7275, so LTPO governors idle the render rate BELOW it (OnePlus Pad 2 → 50Hz), dropping ~10 emu frames/s (persistent video hitch that no 60-targeting lever fixed). Retarget the mode selector to the HIGHEST near-integer-multiple refresh (≈120 on 120/144 panels; 60 fallback on 60-only). With present-on-produce the emu posts 59.7275 unique frames and every one is shown (rate ≥ GB rate → zero drops). Reject 144 (2.41× → uneven scanout). Pin mode + request pinnedHz.
…-produce at 120Hz) Measured on OnePlus Pad 2 with Game Space forcing 120Hz: present-on-produce posts each frame at production time, async to the 120Hz vsync grid, so frames land on 1/2/3-tick boundaries unevenly (stdev 3.58ms, 15 hitches/6s). Free-running re-sample posts every vsync and lets the compositor hold each unique emu frame a consistent run of refreshes (2 at 120Hz) — Pixel-proven stdev 1.17ms. Correct approach once the panel holds a rate >= the GB rate.
Owner
|
This will probably work better as a separate repo with SameBoy being a subrepo in it, since I won't be able to maintain and support this frontend. |
added 8 commits
August 31, 2026 20:20
…ixes, release CI hardening Native (jni/): - AAudio error callback + emu-loop stream reopen: headphone unplug / BT route change no longer permanently freezes emulation (ring flushed to unblock a stuck producer; reopen on new default device from the emu thread) - Unchecked mallocs in sb_emu_create / sb_emu_set_boot_rom / sb_emu_save_battery (OOM crash + battery-save data-loss paths) App: - UAF: Printer/Link activities validate stale native ctx from Intent extras after EmulatorActivity destruction (activeCtx liveness token) - ANR: SAF display-name binder query moved off the main thread - Data-loss race: MainActivity onResume library reload no longer wipes an in-flight scan's unsaved adds - Thread leak: io executors shut down in onDestroy CI: - Release job installs rgbds + verifies boot ROMs are bundled in the APK (release APK previously shipped boot-ROM-less: white screen on CGB titles) Bump version.mk to 1.0.4 (versionName 1.0.4 / versionCode 10004)
…B, LCD filter Targets physical-controller handhelds (e.g. Retroid Pocket Classic) where on-screen touch controls waste space and D-pad navigation had no visible focus. - Fill-screen: 'On-screen controls: Auto/Always/Never' setting. Auto hides the touch overlay when a physical controller is connected; the emulator screen then fills the display (aspect-correct, immersive bars). Renderer letterbox made fractional so the full panel is used (windowed console layouts stay bit-identical: their rects are exact 160/144 multiples). A gamepad button (default L1) opens the in-game menu so hidden controls never trap the user. Controller detection guards against Android's synthetic-keyboard DPAD. - ROM picker: tiles are now focusable with a high-contrast focus ring + scale; first tile focused on show; D-pad traverses, A/Enter plays, Menu/Y = context. - Super Game Boy: MODEL_SGB (0x004) selectable in Settings and the in-game model switcher, making the existing SGB border option render borders (core + boot ROMs already present; no native change needed). - LCD dot-matrix filter: new GLES2 shader (step-based per-source-pixel grid, gated to >=2x, highp) with 'Screen filter: Off/LCD', plumbed via sb_session_set_filter / nativeSetFilter. Bump version.mk to 1.0.5. Native host tests pass.
Borders became reachable in 1.0.5 (SGB model), but the layout still assumed 160:144 everywhere, so a bordered frame (256x224) was letterboxed inside a wrongly-shaped surface: smaller image, black bars. - Geometry now derives from the Core's actual output size. New cheap nativeScreenSize (packed (w<<16)|h, no frame copy) feeds GBLayout's fullscreen, landscape and portrait paths; 160x144 output stays bit-identical. Size is re-checked after start, resume, menu close and via the existing 2s battery poll, so a border appearing mid-boot re-lays out. - TouchOverlayView tracks the source size so the console body/bezel follows the wider well. - Settings: clearer border labels + a footnote that 'SGB games only' needs the SGB model, that 'Always' works on any model (console art, or the game's own SGB border), that the first load can pause while that border is extracted, and that a border shrinks the picture. - In-game menu gains a Border switcher (live; no reboot needed -- GB_set_border_mode applies between frames). Native audit found the 256x224 path already correct end to end (buffers, texture re-upload, save-state/rewind). Adds host tests for screen size: default, live NEVER->ALWAYS->NEVER, full 256x224 coverage, save-state roundtrip. All host tests pass. Bump version.mk to 1.0.6.
- In-game menu: action-sheet rows and save/load slot cards now show a focus highlight and the first usable row takes focus on open, so a D-pad/A/B controller can drive every screen (previously the cursor was invisible and nothing was focused, so only touch worked). Gameplay consumes d-pad keys, which keeps the window in touch mode where Compose rows refuse focus; openMenu() leaves touch mode first when a gamepad is present. - LCD filter: replace the home-grown 1px step grid (jittered at fractional fill-screen scales) with a straight GLES2 port of Shaders/MonoLCD.fsh incl. MasterShader gamma, i.e. the desktop 'Monochrome LCD' look. No scale gate. Bump version.mk to 1.0.7.
Many pads (and Retroid firmware in Xbox mode) report the d-pad only as AXIS_HAT_X/Y; only gameplay translated that, so the in-game menu, library, settings, remap, link and printer screens ignored the d-pad entirely. GamepadMapper.hatToDpadKeys synthesizes DPAD key events from hat edges; GameMenuDialog and a new DpadActivity base (all non-gameplay activities) route generic motion through it.
Activity/lifecycle - EmulatorActivity survives every config change (BT pad pairing flips navigation/keyboard, dark-theme schedule flips uiMode, split-screen/unfold): previously each one recreated the Activity, freed the Core and rebooted the ROM from its last .sav. onConfigurationChanged re-themes the overlay. - Hat/stick d-pad latch released on window focus loss (menu, shade): the centre MOVE goes to the new window, so the Core kept walking. - Unbound gamepad buttons are swallowed: Generic.kcm's BUTTON_B->BACK fallback finish()ed the game while the ROM was loading or after a remap. - Menu->Settings/Printer/Link handoff drops the battery poll so it can't briefly unpause behind the closing dialog; accessory sheet no longer shows a stale printer tick after hosting/joining a link. - Camera permission: check the grant before the denied latch (a later grant in system Settings now works), and request exactly once per foreground. - Game Boy Camera frames rotated to the display (SENSOR_ORIENTATION) and mirrored for a front lens; they were sideways in portrait. - In-game menu honours the in-app Light/Dark override. - Hat-switch d-pads now work inside Compose Dialog windows (Settings pickers, palette editor, library sheet) via a Window.Callback bridge. Native - Link: an unarmed slave answers every master byte with 0 bits (Core semantics) instead of leaving it unanswered; the master only latches dead after 4 consecutive failed exchanges. One side reaching link mode first, or the peer sitting in its menu, no longer kills the link after 1.5 s. Host test added (parked slave; fails on the old code). - Link connect: AI_NUMERICHOST so a typo can't start an uncancellable DNS lookup that disconnect() then joins on the UI thread (ANR). - AAudio: keep retrying open (~1/s) instead of staying silent for the session when the new route wasn't ready at the moment the old stream died. Storage - SaveStore.write is atomic (tmp+fsync+rename) and reports failure; a kill mid-write no longer destroys the previous state/battery, and a failed state write no longer leaves a thumbnail for a state that doesn't exist. - Library survives MainActivity recreation mid-scan (process-scoped), so a theme toggle during import no longer loses the scanned entries. - Re-importing a renamed/moved ROM refreshes its SAF URI on CRC match. - Zips with non-UTF-8 (Windows OEM code page) entry names no longer throw MALFORMED and get dropped. - Printer 'Save to Pictures' on API 26-28 notifies MediaStore.
… asserts - GB_DISABLE_TIMEKEEPING: the blocking audio ring is now the only clock. With the Core's gettimeofday pacer also compiled in, the two fought — on a device whose audio HAL clock runs faster than the wall clock the ring never filled and every burst underran: a periodic click (confirmed on Pixel 8 Pro). The turbo speed cap moves into emu_loop, enforced against CLOCK_MONOTONIC. - NDEBUG: upstream's release CONF sets it; our NDK build never did, so GB_CONTEXT_SAFETY and every Core assert() were live in the shipped APK and would SIGABRT instead of being no-ops. Host tests keep asserts on purpose. Bump version.mk to 1.0.8.
- Auto-save: onPause writes a state to states/<rom>.auto; next launch asks 'Continue where you left off?' (controller-navigable, non-cancelable). The emu stays parked behind the prompt; a failed load deletes the file. - Fast forward: new gamepad binding (default R1, remappable next to Menu); hold to turbo, released on focus loss / pad unplug like the Core keys. - Cheats: GameShark / Game Genie via GB_import_cheat; per-ROM list in cheats/<rom>.json; in-game menu > Cheats (toggle, add with inline validation, remove). Cheats live in the Core's unsaved section so they survive reset, model switch and state load (host-tested). - MBC7 tilt: phone accelerometer -> GB_set_accelerometer_values, registered only while resumed with an MBC7 cart, axes mapped per display rotation. Native: 5 JNI entries (cheats park; accelerometer direct — no Core assert). Host tests cover add/reject/persistence/enable/remove-all and accel guards.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A native Android frontend for SameBoy
This adds a standalone Android app under
Android/. The portable CCoreis reusedunmodified — it's driven through a thin C/JNI bridge, with the app layer in Java/Kotlin.
The diff is additive only: every file lives under
Android/ordocs/, plus one newCI workflow (
.github/workflows/android.yml). No existing SameBoy source is touched.Opening this for your consideration — happy to adjust scope, split it up, or maintain it out
of tree if you'd prefer it not live in the main repo.
What it does (parity program M1–M9)
Architecture
Android/jni/is the whole native surface:ring_buffer.*— SPSC audio ring; a full ring blocks the producer, so audio paces emulation.emulator.*— ownsGB_gameboy_t, callbacks, double-buffered framebuffer.audio_aaudio.*— AAudio output stream draining the ring.render_gles.*— EGL/GLES2 render thread (integer-scaled blit from a locked copy).session.*— ties emu thread + render + audio into one session.sameboy_jni.c— JNI entry points.Android.mkbuildslibsameboy_core.sofrom../Core/*.cunmodified. Boot ROMs aregenerated at build time from
../BootROMs/*.asm(build succeeds boot-ROM-less if rgbds isabsent). Native libs are 16 KB-page-aligned for Android 15+/Play.
Verification
jni/test/run_host_tests.sh, no device): ring buffer (FIFO, underrunzero-fill, blocking-pacing, teardown), emulator-on-a-real-ROM (60 frames → opaque 160×144
framebuffer + audio produced), link-cable two-core byte exchange.
renders GB/GBC, live AAudio, touch + hardware input;
dmg-acid2pixel-correct on arm64;commercial CGB titles run in color (Pokémon Crystal, Pokémon Pinball rumble cart, Tetris DX);
save states persist; R8 release build runs on both ABIs; TCP link reaches Connected with
graceful teardown. Also smoke-tested on a physical Moto G4 (short-screen portrait layout)
and a Pixel 8 Pro.
Known limitations / follow-ups
Note on one commit
The final commit (
fix(android): hint true 59.7275fps cadence…) addresses scrolling stutteron variable-refresh (LTPO/120 Hz) panels via
Surface.setFrameRate(59.7275, FIXED_SOURCE).It's API-verified against
compileSdk 34but not yet verified on the Pixel 8 Pro hardware —called out for transparency.