Swiping out with the pause menu up left the track playing on the OS home
screen. Two paths caused it: backgrounding a running game calls
InGameOverlay.open() from onPause, which sets overlayVisible = true and
re-fires the pause-music LaunchedEffect — that effect then ran start()
after onPause had returned, beginning playback while backgrounded — and
more generally nothing stopped a late start() from a background thread (a
MediaPlayer with USAGE_MEDIA is not auto-paused by the system).
Add a foreground guard: onResume sets it true, onPause sets it false
(early, before open() flips the overlay state) and pauses any current
playback. start() no-ops whenever it is false, so no effect, resume, or
toggle path can begin playback behind a backgrounded app. Coming back,
onResume clears the guard and restarts the track if a menu is still up.
Swap the bundled pause-menu track for the slower, more ambient edit
(res/raw/pause_music.ogg -> .mp3; R.raw.pause_music is unchanged), and
ease it in instead of starting at full volume — the abrupt onset was the
complaint.
start() now begins the player silent and fadeIn() ramps to the set volume
over ~1.6s, reading the volume each step so a live slider change tracks.
stop() mirrors it with a ~0.5s fade-out then release, so resuming the game
isn't a hard cut; ownership of the player is handed to the fade coroutine
(player = null) up front and released in a finally, so a reopen mid-fade
builds a fresh player, the two cross-fade, and nothing leaks. A manual
volume change cancels an in-progress fade so the slider never fights it.
The pause-menu track never played. It deferred to active audio the way
LibraryMusic does (to stay out of Spotify's way), but that check is wrong
here: on an overlay pause the game keeps its audio DEVICE open and just
underruns to silence — pauseForOverlay calls setOutputPauseSuppressed(true)
so Android does not reclaim the idle stream and stall the resume (#333).
So AudioManager reports the game's own stream as active the whole time the
menu is up, even though nothing is audible, and start() deferred forever.
Drop the isMusicActive() guard: play a second stream over the silent game
one. No audio focus is requested either, so the game's stream and its
resume are left untouched. Also default the toggle on — the menu was
silent and this fills it; the switch turns it off. The effect's retry loop
shrinks accordingly (start now plays immediately; a couple of light
retries only cover a transient MediaPlayer prepare hiccup).
RetroAchievements toasts (and other OSD) were invisible on first boot
with Skip BIOS on, until you opened the pause menu — at which point the
toast appeared, and vanished again when you backed out.
Cause is the Android startup blank-frame suppression (GSPresentationPolicy).
With Skip BIOS on there is no boot animation, so the game shows a black
screen with no GS output for a while, and RA posts its "achievements
loaded" summary toast into exactly that window. ShouldSkipAndroidBlankFrame
returns true for every one of those frames (Vulkan + blank + no current
output), so the present is skipped — BeginPresentFrame(true) reports
FrameSkipped and EndPresentFrame() never runs, which means neither
RenderOSD() nor FullscreenUI::Render() (where notifications are drawn)
executes. The toast just sits queued in s_notifications. Opening the pause
menu forces real presents so it finally draws; closing it resumes the
skip. With BIOS on, the boot animation produces GS output before RA
posts, so has_current_output is already true and the toast presents
normally — which is why the bug only showed with Skip BIOS.
Gate the skip on new ImGuiManager::HasPresentableOverlayContent(): an OSD
message (pending or active) or an open FullscreenUI window or a queued
toast now forces the blank frame down the normal present path, where
EndPresentFrame() draws the overlay over black and presents it — exactly
what the pause menu already does during boot. Once the game produces its
first frame the suppression is moot anyway (has_current_output true), so
this only ever presents the few black boot frames that actually have
something to show, and is a no-op when there is nothing queued.
The pause menu was silent, and people sit in it for minutes browsing
settings, achievements or memory cards mid-game.
New PauseMusic object, deliberately separate from LibraryMusic rather
than a second gate on it: LibraryMusic refuses to play unless
eState == STOPPED, which is the exact opposite condition, and the two
have opposite lifetimes. One player serving both would spend its life
fighting the other's start conditions.
Driven off WindowImpl.overlayVisible || inGameScreen != null rather than
from InGameOverlay.open()/close(), for the same reason as the effects
around it: many paths reach each state (back, menu button, hotkey,
dismissInGameScreen, a boot that force-closes the overlay) and hooking
them one by one always misses one. Including inGameScreen matters
because openInGameScreen() closes the menu as it opens Settings, and
sitting in Settings is the long silence this exists to fill.
Start retries for ~3s like the library track: pausing suspends SPU2 but
Oboe takes a moment to actually go idle, and start() politely defers
while AudioManager still reports audio active, so a single attempt would
lose that race every time. Audio focus is deliberately NOT requested —
the game's own audio is already suspended, so there is nothing to duck,
and grabbing focus for a menu track would stop whatever the player has
going in another app.
Toggleable and volume-adjustable in App settings like the library music,
off by default (audio starting when you open a menu is startling if you
didn't ask for it), and a custom track can be imported the same way.
onPause/onResume mirror the library track's handling so it never plays
behind a backgrounded app; onResume restarts explicitly because the
overlay states don't change while backgrounded, so the effect won't
re-fire on its own.
GunCon 2 alone was half the answer. The core registers eighteen USB devices — Buzz
buzzers, a Rock Band drum kit, Keyboardmania, BeatMania, a DJ turntable, the Printer,
EyeToy, Gametrak, RealPlay, Train controller, mic, headset, HID keyboard/mouse — and
none of them were reachable on Android.
The list is enumerated FROM RegisterDevice rather than hardcoded, so it cannot drift
from what a given build actually supports, and subtypes (different wheels, different
turntables) come along with it.
Making them USABLE is the real work, and it did not need a second binding editor.
Every InputBindingInfo already declares a generic_mapping (Cross, DPadUp, L1, ...), so
on attach native builds GenericInputBinding -> bind_index for the device and
applyPadButton forwards each press to the matching bind. The player's existing controls
— physical pad, on-screen buttons, macros, anything that funnels through that one
chokepoint — drive the device with nothing extra to configure. Bindings with no generic
equivalent (Gametrak's axes, the printer) get nothing, which is correct: there is no
sensible pad button for them.
Aiming stays special-cased, because a pointer is not a button: Lightgun owns it, and
UsbDevices.setType now tells it when a port changes so the aim layer cannot stay live
over a port that has become a drum kit — otherwise every touch would be swallowed as a
shot at a device that is no longer attached.
Lightgun: the core already emulates the device (usb_lightgun::GunCon2Device,
DEVTYPE_GUNCON2). What Android lacked were the three things that feed it, because our
input path is bespoke rather than InputManager/SDL:
* device selection -> USB{n}/Type via USB::SetConfigDevice
* aiming -> InputManager::UpdatePointerAbsolutePosition, which is what
GunCon2State reads through GetPointerAbsolutePosition(0) when
it has no relative binds. Window pixels, and our SurfaceView
spans the window, so touch coordinates pass straight through.
* buttons -> USB::SetDeviceBindValue(port, BID_*, 0/1)
Touch aims continuously rather than only on tap, so you can lead a target before
firing, and the aim is pushed BEFORE the trigger because the core samples the pointer
when the trigger goes down. A touch within 6% of a screen edge fires OFF-SCREEN
instead: that is how these games reload, and without it they are unplayable past the
first magazine. A/B/C, Start, Select and Cal (recalibrate) sit down the right edge,
composed above the aim layer so pressing one is a button press and not a shot;
recalibrate matters because several of these games open with a calibration step.
The aim layer consumes its pointers, unlike the gesture layer — with a gun attached a
touch on empty screen IS the shot, so there is nothing else it could belong to. It
still ignores a DOWN a widget already claimed, so the gun buttons and pause still work.
Device type is restart-required and says so: swapping a USB device on a live VM is the
emulated equivalent of yanking the plug from a port the game has already probed.
Gestures also reachable in-game: swipe distance and the Tap/Hold mode are values you
only find by playing, and walking out to the settings tree to nudge them loses the
moment. The six button assignments stay in All Settings — they are set once, and six
pickers would swamp that pane.
Gesture control (PPSSPP-style): swipes and a double-tap on empty screen area fire a
PS2 button. The double-tap takes a Tap/Hold mode — Tap pulses (NFS nitro), Hold
latches until you double-tap again (ARPG camera lock). The layer composes below every
widget, rejects a DOWN a control already consumed, and never consumes anything itself,
so it cannot swallow a press. Pulses hold 40ms because the emulated pad drops an
instant down+up.
Clock + battery readout in the library toolbar and the in-game menu header. The glyph
drains with charge (green/amber/red) and shows a bolt while charging. Time-remaining
appears only while charging: Android has computeChargeTimeRemaining() but no public
discharge-time API, and inventing an estimate would be worse than omitting it.
Texture packs:
- write() did delete()+renameTo(); if the rename failed the whole install record was
gone, and runCatching swallowed it while still bumping revision. Now an atomic
Files.move with a .bak fallback. This is "packs forget being installed after ~57
downloads", and very likely also "delete does nothing" / "still says Installed".
- reconcile() refuses to wipe records when a scan reports zero packs but records
exist — that is a failed listFiles(), not sixty simultaneous deletions.
- refresh() walked every file of every pack on every open (~250k stats at 60 packs).
Sizes now come from an mtime-keyed cache.
- The catalogue paged 20 rows at a time with a "Show N more" that names the
remainder; it cannot be a LazyColumn inside the existing verticalScroll.
- Installed packs show game names, and the catalogue sorts by game or serial.
Controls reset now exists: resetTunables() hand-listed keys, drifted behind every
setting added after it, and had ZERO call sites. Replaced with resetAllControls(),
which sweeps by key prefix so it cannot rot, scoped global or per-game.
Confirmations are inline overlays claiming an exclusive nav layer, not Compose
dialogs — a dialog is its own window and swallows controller keys, so those prompts
were touch-only. Adds Clear All for Recently Played and a full app reset; the reset
also purges the in-folder settings mirror and gamesettings INIs, without which the
next launch would silently restore everything it just wiped.
Motion control falls back to the accelerometer where there is no gyroscope, with the
tilt limitation stated in the UI (gravity cannot observe yaw). Adds a Motion Recenter
hotkey — recenter() previously had no call site at all.
Aspect ratios: added 20:9, 19.5:9 and a user-entered Custom ratio
(GSOptions::CustomAspectRatio, clamped 0.5..5.0). All APPENDED, never inserted —
these values are persisted as raw ints in the ini and in the Android prefs, so
slotting one in mid-enum would silently repoint every saved config at a different
ratio. Also filled in the two ultrawide cases RequestDisplaySize was missing.
Interlace/presentation: ported sashkinbro's EmuCoreX 30799e4. SelectGSInterlaceMode
centralises the mode choice and keeps shader_mode -1 for automatic full-frame output
(a deinterlace pass must not run over progressive output during a video-mode
transition); our formula already agreed, so this is centralisation plus
static_asserts rather than a behaviour change. ShouldSkipAndroidBlankFrame is new
behaviour: Vulkan now suppresses only the startup blank, so a mid-game fade reaches
the normal present path and its recorded command buffer is submitted.
Vulkan: declare the attachment feedback loops on the PIPELINE, not just on the image
layout and render pass. We put attachments into FEEDBACK_LOOP_OPTIMAL without ever
setting VK_PIPELINE_CREATE_{COLOR,DEPTH_STENCIL}_ATTACHMENT_FEEDBACK_LOOP_BIT_EXT,
which the spec requires — undefined behaviour rather than a missed optimisation, and
strict mobile drivers are where undefined shows up as stale attachment reads.
Reported as "patches apply with every patch setting off, and won't turn off" —
SOTC/KH2/GOW2. One chain of defects, verified on device:
- PatchManagerViewModel.refresh() called syncAllEnableLists() unconditionally, so
merely OPENING the Patch Manager persisted every uncommented group of every
on-disk .pnach as enabled. Community pnach files ship uncommented, and patches
are matched by NAME, so a name like "60 FPS" then armed the same-named group in
any of the ~4000 bundled files, for games never opened. Removed; import still
registers its own file, which was the only legitimate use.
- EnumeratePnachFiles fell back to the bundled zip even when disk files existed,
contradicting its own "prefer files on disk" comment. Deleting a pnach silently
promoted the identically-named bundled group in its place.
- delete() removed the file but never dropped its names from the enable list, so
they stayed armed forever.
- ReloadPatchAffectingOptions never reset CurrentCustomAspectRatio, which only
ever gets set, so 16:9 survived disabling widescreen.
- LocalCheatRow and OnlineEntryRow armed the row under the cursor on D-pad Right,
so scrolling a cheat list enabled everything you passed. Confirm only now.
Patches cannot be un-applied without a reboot: PatchCommand has no original-value
field and UnloadPatches never touches guest RAM, so disabling one mid-session only
stops it being re-written.
The note-highway render target is never sampled back, so the GPU->CPU download is
pure cost on a tiler. Covers GH2 (SLES-54442, SLUS-21447 — the latter is also the
serial GH2 Deluxe ships under) and GH3 (SLES-54962, SLES-54974, SLKA-25363,
SLKA-25414, SLUS-21672).
gsHWFixes is a clear-then-replace map in this overlay, so each GH3 entry restates
every upstream key. Dropping one would have silently undone the crowd-texture,
bloom and post-processing fixes those entries already carry.
Exit returns to the library's overflow menu. It had moved to the navigation
drawer, which put it below every other destination -- so quitting, one of the
most frequent things anyone does from that screen, meant opening the drawer and
scrolling to the bottom every time. Reported as issue #460 by shinobumaehara,
whose point is simply that frequency should decide placement. It stays in the
drawer as well; this is the short path, not a replacement.
Smaller than it looked: onExitApp was still a parameter and its confirmation
dialog was still wired up. Only the row that reached them had been deleted.
LibraryOverflowItem gained optional iconRes/iconTint for it, because the power
symbol (U+23FB) is not in the bundled font and rendered as a tofu box -- it now
uses the same ic_power drawable and red as the drawer's row, so the two entries
match. Every other row keeps the text-glyph path untouched.
Skins moves to sit after Shortcuts and before Network. It is controller artwork,
so people look for it beside Controls and Shortcuts rather than past On-Screen.
Suggested by Isshin.
Finishing a game submits every leaderboard in the same frame. AddNotification
gave each one start_time = current_time, so they all began at once -- Final
Fantasy XII posted six, which covered the screen and pushed the mastery unlock
out of sight before it could be read. Reported with a screenshot showing exactly
that.
Three on screen at most now, at least 0.4s apart, and anything past the limit
waits its turn. A QUEUED notification does not age while it waits: its duration
starts when it actually appears, so nothing expires unseen in the backlog.
Also guards the same-key replacement path. It recomputes start_time from elapsed
time, which is NEGATIVE for a notification that has not appeared yet -- and
Timer::Value is unsigned, so subtracting it would have wrapped and flung the
notification years into the future. It now keeps the scheduled start instead.
Ordering is still insertion order, so an unlock posted after a batch of
leaderboards still comes last. Visible rather than buried, but not prioritised.
Three touch-overlay changes that share the same files.
SCREENSHOT is now an on-screen button next to SAVE and LOAD, off by default.
It started as a pause-menu entry, which was the wrong place: the core writes the
PNG and confirms on the OSD, and the OSD is hidden while the menu is up -- so you
tapped it, saw nothing, and only learned it had worked after backing out.
Macro buttons can take skin artwork. A macro can already fire any pad input,
including L-Stick Left/Right, so a racing layout of steer/steer/accelerate/brake
was buildable -- but the four buttons were stuck with the generic M1-M4 labels
because there was no skin slot for them. ic_controller_macro1_button.png through
macro4, and m1-m4 too, since that is the name David reached for first.
That also exposed a silent truncation: the skin import cap was 24 while the key
list is now 28, so a complete pack would have had images dropped on import with
no error at all -- they simply would not appear. Raised to 40, with a comment
tying it to the key count so the next slot added does not repeat it.
Grid snap now commits on Save. The editor draws widgets snapped while leaving the
underlying fraction raw (snapping live fights the transform gesture's delta
accumulation and makes dragging feel stuck), and the commit was supposed to
happen on finger-up. But that lived in detectTapGestures' tryAwaitRelease, and an
actual DRAG is consumed by the neighbouring detectTransformGestures, which
cancels the tap detector and makes tryAwaitRelease return false. So it only ever
committed if you tapped without dragging: the layout looked aligned the whole
time you were editing and reverted the moment you saved.
Reported by David (SSR) and by a user who caught the grid reverting.
Requested by David (SSR), who noted no PS2 emulator offers one: without it the
only way to use an ultrawide patch was Stretch, which distorts. Useful on folds,
tablets, DeX and anything driving a 21:9 panel.
Added to the generic aspect AND the FMV override, since a game that wants
ultrawide gameplay usually wants it during cutscenes too. Adding it to the FMV
enum also shifted MaxCount, which the name array is sized from -- that array had
to grow with it or the last entry would have been a hole.
The Kotlin side needed SIX edits for one new enum value, and getting five of them
right still left the feature completely dead:
RendererTab options list + clamp
RendererTab FMV options list + clamp
EmulationMenu setAspectRatio clamp
EmulationMenuScreen the pause menu's own options list
Settings NativeApp.setAspectRatio(coerceIn(0, 4)) <-- the killer
Settings INI name<->index, both directions
That fifth one clamped on the way to the core, so the picker highlighted 21:9
while the emulator was told 10:7 -- UI correct, nothing happens, no error. The
sixth meant the choice would not have survived a reload even once it applied.
The NATIVE clamp needed no change at all, because it derives its bound from
AspectRatioType::MaxCount instead of hard-coding it. That is the pattern the
Kotlin side should follow; four literal 4s in four files is why this was a
six-site change instead of a one-site one.
An advertised extension does not guarantee its feature bit, and requesting a
feature the driver does not have fails vkCreateDevice outright with
VK_ERROR_FEATURE_NOT_PRESENT. So a driver that offers, say, VK_EXT_line_
rasterization while reporting bresenhamLines as false did not cost us one
optional nicety -- it took Vulkan down completely, and the renderer refused to
start with "Failed to create render device".
Reported on a PowerVR BXM-8-256, which is neither Adreno nor Mali and so had
never been through this path. Deterministic: three identical failures in a row,
and the only way out was switching to OpenGL.
The file already knew about this trap. A comment above the depth-ROAA probe spells
it out exactly -- but the lesson had been applied to that one sub-feature and
nowhere else, so the other six were still requested blind. ProcessDeviceExtensions
does perform the same reconcile, and would have caught it, except that it runs
AFTER vkCreateDevice and can therefore only ever describe the failure.
Probe all six up front in one vkGetPhysicalDeviceFeatures2 call -- provoking
vertex, line rasterization, ROAA colour and depth, feedback-loop layout,
swapchain maintenance1, fragment shader interlock -- and drop whatever is not
really there. Dropping is logged WITH THE FEATURE NAME, because "Vulkan works but
one thing is off" is a completely different bug report from "Vulkan does not
start", and the next person needs to know which feature the driver misrepresented
rather than guessing from a bare error code.
Confirmed fixed on the reporting device.
The process split was only half done. discord_bridge.cpp still carried PCSX2's
"GPL-3.0+ / PCSX2 Dev Team" header -- wrong on both counts, since it is neither
PCSX2's code nor compatible with what it links -- and the four helper Kotlin/Java
files carried no header at all, so they inherited the repository's GPL by
default. A GPL file that links Discord's proprietary SDK is exactly the defect
the separate process exists to prevent; the boundary has to hold in the licence
headers as well as in the linker.
Everything on the helper side of that boundary is now MIT with accurate
copyright: the bridge, the native wrapper, the service, the auth activity and
the shared IPC definitions. MIT rather than Apache-2.0 because it has to work in
both directions -- the emulator side (DiscordPresence and the Friends UI, which
stay GPL) consumes the shared IPC definitions, and that only holds if this side
is permissive.
Each header says why, so it does not get "fixed" back to the PCSX2 boilerplate
by someone tidying up.
No functional change: comments only, both sides still compile.
Two reports from Rei Ayanami, one of which turned out to be a real gap rather
than a misunderstanding.
A game could report "3 game patches are active" with an empty patch list and
nothing anywhere to turn them off. Those patches come from the ~2 MB patches.zip
we ship, and the manager only ever listed files in the patches folder, so
everything inside the zip applied invisibly. Worse, the core auto-applies any
group with no [Name] ("we auto enable anything that's not labelled" --
Patch.cpp), and a group with no name has nothing for a toggle to hang off.
There is now a card showing exactly what the bundled zip contributes to this
game, with unlabelled groups marked as always-on, and a button to copy the file
into the patches folder. That is not convenience: the core prefers a pnach on
disk over the zip and explicitly disables the bundled copy when it finds an
unlabelled patch on disk, so extracting both makes the cheats individually
switchable AND takes the invisible copy out of play. It is the only way to turn
an unlabelled bundled patch off at all. The card is hidden when the game already
has a pnach on disk, because then the zip contributes nothing and showing it
would be a lie.
All on / All off sits above the cheat list, not below: a community pnach can run
to a hundred entries, and a control you have to scroll past all of them to reach
does not solve the problem it exists for. It rewrites the file once rather than
once per cheat -- a hundred read-modify-writes is a hundred chances to
half-apply -- and each button disables itself when there is nothing left to do.
A PNACH is named <SERIAL>_<CRC>.pnach, so the two values needed to name one
should not live on separate screens. The CRC now appears in the pause-menu
header, in the game details you get by long-pressing in the library, and as a
row that stays put in the Info tab instead of appearing mid-identification and
shoving everything below it down.
Based on Splaser's PR #459, with three changes:
- The long-press sheet showed "CRC ..." forever for an image that could not be
identified, because produceState starts at null and RESOLVES to null; those
two states are indistinguishable. Now tri-state: "..." while identifying, an
em dash when identified as unknown.
- The VM-then-identify fallback was copy-pasted into a second place. One
DiscIdentity.resolve now, keeping the serial-match guard -- without it a
different running game lends its CRC to whatever you long-pressed.
- The pause header cannot trust the live VM CRC alone. On ISO boots the core
hands ELFLoadingOnCPUThread an empty path, UpdateELFInfo takes its failure
branch, and s_current_crc stays 0 -- the emulog shows the loader computing the
real CRC and the VM then reporting 00000000. It falls back to identifying the
image, same as the other two screens.
Co-authored-by: Splaser <splaser@users.noreply.github.com>
Contributors were derived from commit authorship between tags, which is not who
a release credits. Work ported in from other projects lands as commits authored
by whoever did the porting, so the people the notes thank never appeared, while
anyone who happened to commit inside that tag range did -- credited in a release
that says nothing about them.
Read the @mentions in the release notes instead. That is the credit somebody
deliberately wrote. Avatars come from github.com/<login>.png, a plain redirect,
so this costs no API calls at all: no rate limit, nothing to cache, and it works
offline from cached notes. The tag-to-tag comparison it replaces cost one
request per release against an unauthenticated budget of sixty an hour.
Each card also shows the release author's avatar, which was already in the
releases payload and free.
ARMSX2 is GPL-3.0+ and the Discord Social SDK is proprietary. Linking them into
one binary would make the emulator a combined work with a library whose
corresponding source cannot be supplied, so the SDK now runs as a separate
program in its own process and the two talk over a deliberately dumb message
interface: a title, a serial, an image URL, a list of names. No emulator type
crosses that line.
libemucore has no DT_NEEDED on the SDK and never loads it; the bridge builds
into its own libarmsx2_discord.so, loaded only in :discord. With the feature
switched off that process does not exist and the library is never mapped at all.
Verify with: llvm-readelf -d libemucore_4k.so | grep -i discord (must be empty)
The SDK itself is no longer in this repository. Discord's terms permit shipping
it inside a working application but not republishing the raw SDK, so it is an
optional private build input via DISCORD_SDK_DIR. Unset -- every public clone --
the feature compiles out and the UI reports itself unavailable. Deliberately not
a product flavour: one release variant, and the only difference is whether that
directory was present at build time.
Also here, from testing the feature into shape:
- Presence uses the game's own cover art, and our logo (an Art Asset key, not a
repo URL, so it tracks the current mark) when idle.
- Friends show their avatar AND the cover of what they are playing, never one
instead of the other -- swapping the face out for box art loses the identity
exactly when the row gets interesting.
- A count badge on the Friends entry point, in the drawer and in the in-game
header, because the point of it is to be seen by someone not looking at the
friends list.
- Friends moved off the in-game tab rail into a header button with its own
panel. It was last on a rail that scrolls, so reaching it meant knowing it was
there. The panel is composed, not a Dialog: a Dialog takes its own focused
window and swallows gamepad keys before our input plumbing sees them.
- "<friend> is now online" is one Compose banner at the Activity root, replacing
a split between an emulator OSD message in game and nothing at all in the
library. The OSD is text-only and could never show an avatar.
- Reconnect after a drop, with backoff. The SDK does not retry and its
connection does not survive being backgrounded, so a drop used to be terminal
until the app was restarted.
Crash and correctness
- Fix a crash when backgrounding the app mid-game: onPause flushed the Vulkan
pipeline cache from the UI thread while the GS thread was creating pipelines
into the same VkPipelineCache. Vulkan requires that handle to be externally
synchronised, so this was a driver-level data race and crashed on Adreno and
Xclipse alike. The flush now runs on the GS thread, posted via the CPU thread
so it does not race the EE-owned MTGS ring.
- Fix an unbounded out-of-bounds vertex read in the GSRendererHW sprite-merge
paving path: the inner loop advanced i instead of j, so j stayed loop-invariant
and the scan walked past m_vertex->tail.
- Fix per-game settings being silently ignored: gamesettings/<serial>_<CRC>.ini
loads into a higher-priority layer than anything the app writes, and saves made
from the library never regenerated it, so any key already in that file
overrode the user permanently. Only the category-Reset path rewrote it, which
is why Reset appeared to be the only thing that worked.
- Fix screen rotation: the BIOS followed the launcher rotation instead of the
renderer's (it has no GameInfo, and the tier was keyed on that), and the
launcher stayed locked in a game's orientation after exit because the cleanup
lived only inside stop()'s vmRunLoopActive-guarded branch, which loses a race
against the VM thread's own finally. Rotation tier is now an explicit flag and
the cleanup runs on every terminal path.
- Discard the Vulkan pipeline blob whenever the SPIR-V cache is discarded. It was
validated only against the device header (vendor/device/pipelineCacheUUID),
which is identical across an app update, so a SHADER_CACHE_VERSION bump kept
every pipeline built from the old shaders and nothing pruned it.
- Make eeRecExitRequested atomic: it was a plain bool written from the JNI thread
and read on the CPU thread.
- OpenGL: restore GL_PACK_ALIGNMENT after readback, add the missing memory
barrier after the CAS dispatch, and initialise GLState::depth_mask to GL's
actual default.
- DEV9: log the GetNetAdapter default: bail and the InitNet skip. Both returned
silently, so a settings mistake surfaced as missing hardware three layers away.
Local Link (new)
- New DEV9 backend bridging emulated PS2 Ethernet between devices over
authenticated local UDP, so games with a built-in LAN / System Link mode can
play together. Ported from EmuCoreX (sashkinbro) with the wire format
unchanged, so peers remain compatible across both forks.
- Network mode picker (Online / Host / Join), host address readout, auto-derived
peer ids, generated room codes, hostname support alongside numeric IPv4, and a
link to the supported-games list. Fully controller-navigable.
Performance
- Asynchronous hardware download mode (experimental, opt-in): non-blocking
GPU->CPU readback so the EE thread no longer waits on the GS thread. Ported
from EmuCoreX. Appending Asynchronous to GSHardwareDownloadMode makes the enum
non-ordered, so the relational comparisons on it are replaced with
IsHardwareDownloadReadbackEnabled / IsHardwareDownloadEEThreadRead.
- Affinity Control Mode (experimental, opt-in): EE/VU/GS priority orders plus a
Performance Cores mode. Android otherwise leaves these threads unpinned.
- Raise the texture-replacement cache ceiling from 6 to 16 GB; RAM/2 remains the
real limiter, so this only binds at 12 GB RAM and up.
- Low Latency frame pacing is no longer the default, with a one-time migration
for installs that took the earlier flip.
Features
- Auto renderer resolves to Vulkan HW on Adreno.
- Auto Progressive Scan (per-game): holds Triangle+Cross through boot.
- OLED black as a modifier over any accent colour, including Custom and RGB.
- Optional system keyboard instead of the built-in on-screen one.
Game compatibility
- Everybody's Golf 4 / Hot Shots Golf Fore! hwDownloadMode across all regions
(PR #421, XDarkFallenX).
- Delta Force: Black Hawk Down (PR #401, XDarkFallenX).
- Reduced input latency and input handling improvements (PR #403, Splaser).
RetroAchievements
- Inject the client version from a build-time secret kept out of public source,
with a stock-PCSX2 fallback for secret-less builds, so third parties cannot
copy the client identity. Covers the iOS token too.
Rendering
- Auto renderer now resolves to Vulkan HW on Adreno (OpenGL elsewhere).
- Mobile hardware ROV (Phase 0): tile-native depth feedback behind the ROV toggle.
Performance & input
- Low Latency frame pacing is the default on capable devices, with a one-time
migration for existing installs; low-end devices keep the queued pacing.
- Reduce Android input latency and improve input handling (PR #403, Splaser).
- Experimental CPU clock hint (ADPF) toggle in Performance settings (default off).
Audio & UI
- Pop-up open/close sound cues (info, hardcore confirm, patches & cheats).
- Alternating controller navigation / slider tick sounds.
RetroAchievements
- Inject the RA client version from a build-time secret kept out of public source,
with a stock-PCSX2 fallback (no hardcore) for secret-less builds. Applies to the
iOS client token too. Prevents third parties from copying our User-Agent.
Game compatibility
- Delta Force: Black Hawk Down (SLUS-21124 / SLES-53299) GameDB fixes
(PR #401, XDarkFallenX).
Adds a "Check on launch" toggle to the App-tab updater panel, default OFF. When
enabled, a silent GitHub check runs once at boot (AutoUpdateGate, mounted at the app
root in setContent, gated on IN_APP_UPDATER) and pops the update prompt only if a
newer release exists -- no "up to date" popup on every launch, and nightly builds are
skipped via checkForUpdate's versionCode guard. Reuses the manual button's exact
check/download/install path.
Github flavor only, like the rest of the updater: AutoUpdateGate is real in src/github
and a no-op stub in src/play, so the boot-check + network code never enters the Play
AAB (build-play-aab.sh still fails closed on REQUEST_INSTALL_PACKAGES).
Requested by takanome9104.
Adds a "Check for updates" panel to the top of the App settings tab, github
sideload flavor only. It queries the GitHub releases/latest API, semver-compares
the latest stable tag against the installed build, and offers to download the APK
and hand it to the system installer (progress bar + FileProvider). Nightly builds
(versionCode = Unix seconds, so > 1e6) are always ahead of any stable release, so
they short-circuit to "on the nightly channel" and are never prompted to a stable.
Kept entirely out of the Play build, the same way all-files access is:
- IN_APP_UPDATER BuildConfig flag (true github / false play) gates the App-tab hook.
- The real updater + REQUEST_INSTALL_PACKAGES + the FileProvider live in src/github;
src/play ships a no-op UpdaterEntry stub so shared code still compiles for play.
- build-play-aab.sh now FAILS CLOSED if REQUEST_INSTALL_PACKAGES appears in the AAB
(a self-updating app is a hard Play-policy violation).
Verified: the github APK ships the permission + FileProvider + updater code; the play
AAB has neither the permission, the FileProvider, nor the network/install code.
The generateSharedResources wiring failed the Android build on our pinned
AGP 9.2.1 / Gradle 9.4.1:
- assets.srcDir was passed a Provider (generateSharedResources.map { it... }),
which AGP 9.2.1 rejects at configuration time ("cannot add Provider instances
to the Android SourceSet API") -- so the whole Android build was red. Use a
concrete build-dir path and wire the task dependency explicitly instead.
- the dx11 exclude ("dx11/**") is relative to bin/resources and never matched
shaders/dx11/, so 8 Windows-only DX11 shaders leaked into the APK. Fixed to
"**/dx11/**".
- declared generateSharedResources as a dependency of the asset-merge AND lint
model/analyze tasks (they consume the generated assets dir), so Gradle 9's
strict implicit-dependency validation passes.
Verified: dual-core release APK builds clean (vc1301); the generated tree carries
bin GameIndex.yaml + the overlay, Dirge hwDownloadMode:2 intact, dx11 trimmed.
- RetroAchievements: an All/Unlocked/Locked filter over the achievement list,
plus DuckStation-style notification settings -- notification and leaderboard
duration sliders and on-screen position pickers (a 3x3 grid) for both the
toast notifications and the challenge/progress indicators. Backed by a new
setAchievementsOptionInt JNI bridge over the existing [Achievements] config.
- Touch layout editor: snap-to-grid (widgets snap by their centre so they line
up cleanly), and a movable + resizable settings panel -- drag the grip to
move it, -/+ to resize, double-tap to reset -- with per-orientation placement
so it stops covering the buttons being edited.
- The Android system Back button / gesture opens the in-game menu (#384),
toggle in Settings > Hotkeys (default on). Only the system back is routed;
controller Circle stays the PS2 button.
Requested in #384 by resurrectdev1 and Leunamme30.
The Mali-G615 r44p1 blob loses the rendering context under the in-tile
feedback-loop blend path used for accurate blending: VK_ERROR_DEVICE_LOST on
Vulkan (attachment-feedback-loop) and an equivalent context loss on OpenGL ES
(ARM framebuffer-fetch), crashing effectively every game at any resolution.
It is specific to this driver build -- other Mali blobs, including other
Mali-G615 units on different drivers, run the fast path fine, and NetherSX2's
older renderer is unaffected.
Gate only r44p1 off that path, narrowing by driver version rather than
re-blocking the vendor -- exactly what the extension-select comment above the
Vulkan site anticipated. Vulkan falls back to the per-primitive barrier path
(verified on hardware: no measurable slowdown, God of War holds 60fps);
GLES has no texture-barrier extension so it falls to the framebuffer-copy
path (stable, marginally slower) rather than crashing.
Reported and confirmed on-device by Aryan3472.
- Library opacity slider (Settings -> App) fades the game rows and cards so the
wallpaper shows through the list.
- Long-press -> Remove from Recently Played drops a single game from the shelf;
the game menu now scrolls so no action is clipped in landscape.
- The recently-played list is mirrored to recent_games.json in the data root for
companion apps, reworked from misantronic's PR #391 to run off the UI thread
and to update on removal too.
- A notice on the Patches & cheats screen warns that outdated cheats/patches are
the most common cause of false bug reports.
hwDownloadMode was parsed as an invalid GS HW fix and dropped, silently
ignoring the entries that used it. Make it a real fix that sets HWDownloadMode,
applied as a default only so a player's own Hardware Download Mode still wins.
Dirge of Cerberus (all regions) now ships with no-readbacks, holding full speed
on GPU-bound devices.
Android ships no key layout for the Joy-Con (vendor 0x057E), so every button
arrives as KEYCODE_UNKNOWN and the d-pad could never be mapped or dispatched.
Synthesise a stable, distinct keycode from each button's scanCode at the entry
of dispatchKeyEvent and re-dispatch once, so bind-capture, menu nav and the
in-game lookup all see the same real, matchable key. Gated to 0x057E so no
other controller is affected; scanCode added to the @@JOYCON@@ diagnostic.
- Fast-Forward Speed slider in the pause menu (On-Screen tab, under Frame Limit):
2x-10x, or Unlimited (the default, unchanged behaviour). Below the top it runs
Turbo at the chosen multiplier via a new setTurboScalar JNI; at the top it uses
the uncapped path. Every fast-forward entry point (toggle, hold, hotkey,
settings re-apply) now routes through one ffLimiterMode() helper.
- Fixed the game sometimes staying paused after backing out of the in-game menu.
Reverts the brief menu-pause audio-keepalive added earlier — its purpose (the
fast-forward-from-menu hitch) turned out to be a GLES shader-compile stall, not
audio — restoring the previous pause/resume behaviour. closeAndResume also now
resumes on RUNNING as well as PAUSED, covering the open-then-close race where the
asynchronous pause hasn't flipped the state yet.
- The RetroAchievements menus now show the user's profile picture (RA UserPic) and
both point totals — hardcore and softcore — on the full RA screen and in the
in-game pause panel. Hardcore reads "HC" in red, softcore "SC" in blue. The
avatar URL is emitted in the achievements JSON: from the live client while a game
is loaded, or rebuilt from the saved account username so the library RA menu can
show it with no game running.
- The Vibration Strength slider (the global 0-200% haptic multiplier over both
controller rumble and on-screen touch feedback) is now reachable in-game from the
pause menu's Controls pane, not only from All Settings.
Audio
- Optional OpenSL ES output backend for devices where the default AAudio path
crackles, glitches or won't initialise (Settings -> Audio), plus a lightweight
SPU2 mode that skips the reverb pipeline to free CPU on low-end devices.
- Keep the audio device alive across the in-game menu pause so Android no longer
reclaims the idle stream and drops sound after the menu sits open (#333).
Settings
- Restored the per-setting descriptions under every GameDB Fix and Advanced
Speedhack toggle (lost in the settings redesign).
- Per-game Reset now clears the native per-game INI, so it truly reverts to the
global values instead of the game keeping stale overrides.
- On-screen display now defaults off; Custom stats appear on boot without a
reset (#385).
Controls / RetroAchievements
- Vibration Strength slider scaling all rumble and touch haptics 0-200%.
- Achievement Sound Volume slider; points now show in the menu before a game
loads; unlock sounds play with Do Not Disturb enabled.
Misc
- Drop the compiled GS shader/pipeline cache automatically on app update to
avoid post-update graphical corruption.
- Animated XMB library-background fallback for GPUs without float-texture
filtering.
GS correctness (ported from sashkinbro/EmuCoreX)
- Reset per-game hardware-hack HLE state on game change (Burnout bloom,
IRem/GT channel-shuffle) so it no longer leaks across in-app game switches.
- Fix a non-strict-weak-ordering comparator in SortMultiStretchRects.
- Free the leaked m_expand_vao on the OpenGL device teardown path.
- OSD hotkey now cycles Full / Minimal / Custom / Off instead of a plain
on/off, mirrored by a selector in the in-game On-Screen menu; the mode
persists and every mode drives the GPU-stats line so "Off" is truly off (the
VSI/PSI leak). (Cotcho)
- Library music: a volume slider (default 15%), a user-chosen custom track with
reset-to-default, and device-volume support. (KamFretoZ)
- Display Zoom: one AetherSX2-style slider that trims every edge by the same
fraction to zoom in without distortion, in place of juggling the four manual
crops. (#383)
- Drop the iOS-layout note from the skin downloader.
build-play-aab.sh hardcoded pgo=optimize, so a caller asking for a profile-free
build silently got one built against the profile anyway. Take PGO_MODE like the
sibling build-release-apk.sh does.
Ports linkev's PlayStation-3-XMB (the same wave iOS renders in Metal): a flat
grid displaced in a GLES 3.0 vertex shader by a base spline curve plus flow /
tension / FFD terms, shaded with a fresnel-edged translucent white. Runs on a
TextureView so it composites under the Compose library, capped at ~30 fps to
stay fan-friendly, with the bundled still as a fallback if GL init fails. It is
only the default: a user-picked image (still / GIF / WebP) still overrides it
and clearing that returns to the wave. The readability scrim is dropped to a
whisper over the wave so its blue reads vivid.
RAM/4 gave a 7 GB phone only 1.75 GB, so the 2.97 GB God of War 1 pack evicted
mid-preload ("cache budget reached") and thrashed, felt as a sustained FPS drop
(#376). RAM/2 gives 3.5 GB and holds that pack whole; the 6 GB ceiling lets a
big tablet keep the 5 GB Persona 3 FES pack resident. Still bounded so low-RAM
devices stay clear of the OOM killer. 6 GB is safe here: Android is arm64-only,
so size_t is 64-bit and cannot wrap.
On VK_ERROR_SURFACE_LOST_KHR the recreate can hit NATIVE_WINDOW_IN_USE on the
stale Android window; the old code kept the half-dead swap chain and retried
the same inline recreate every frame, so a game relaunch stayed black forever
on stock Qualcomm Adreno (#380 / #374 — Turnip tolerates it and recovers).
Fully drop the swap chain on that failure so the next onNativeSurfaceChanged
rebuilds from a fresh surface instead of hammering the in-use one.
Android's Auto DNS returned an empty list before 2.6.4; 2.6.4 started falling
back to public resolvers (1.1.1.1 / 8.8.8.8), which the console reaches over a
real UDP round-trip through the sockets forward path. Fast-forward outruns that
round-trip's wall-clock timing and DNS fails (#379). Restore the pre-2.6.4
behaviour by leaving Android out of the public-resolver fallback; iOS is
unchanged, and the separate GetAdapterAuto gateway-probe skip stays.
Plays a looping ambient track on the library, like a console dashboard, and
stops the moment a game boots. Toggle in App settings, on by default.
Library-only is deliberate. SPU2 output goes through Oboe and Android has
been seen reclaiming that stream when the VM pauses, so a second long-lived
stream over gameplay would land on top of an existing problem. Gating on the
VM state also matches what the hardware dashboards do.
Defers to whatever is already playing rather than starting a second stream
over a podcast, and handles audio focus: permanent loss releases the player,
transient loss pauses and resumes.
Starting is retried on a timer because the boot splash video carries its own
audio track and MainActivity is launched from its completion callback, so the
first attempt races the splash stream tearing down. A single attempt lost
that race and never retried, leaving music working only after a game had been
launched and exited.
The player is built by hand instead of MediaPlayer.create, which prepares
internally and would leave the media audio attributes ignored.
Track: Calm Ambient 1 (Synthwave 4k) by The Cynic Project, released CC0.
Attribution is not required by that licence but the author asks for it, so it
is credited in the About screen.
Adds SkinRepo, following the same shape as the driver and patch browsers:
read an index from the skins repository, download the archive, and hand it to
the existing skin installer so extraction and validation stay in one place.
Skins are listed with a preview image, name and size. The manifest is
preferred and the git tree is a fallback, so the browser still works if the
index is missing. Archives under 1 KB are skipped — the repository briefly
carried two-byte placeholders that downloaded fine and installed nothing.
Every path segment is percent-encoded because almost every skin filename
contains spaces. Downloaded archives import through a new entry point that
takes an explicit name: the picker path resolves names through a document
URI, which yields nothing for a downloaded file, so every download would
otherwise have installed as "skin", "skin_1", "skin_2".
The list notes that these packs carry iOS-only layouts, so a downloaded skin
changes how the buttons look and not where they sit.
Android soft keyboards commit text through an InputConnection rather than
sending KeyEvents, so the on-screen keyboard did nothing for games that read a
USB keyboard while a physical or Bluetooth one worked. Host an invisible text
editor view, convert the committed text back into key events with
KeyCharacterMap (which also emits the shift presses that make capitals and
symbols come out right), and feed the existing usbKeyboardKey path.
Bound to a hotkey rather than a setting so chat can be opened without pausing
the game. The close path lives in onKeyPreIme because soft keyboards claim
gamepad buttons for their own navigation and would otherwise swallow the
hotkey, leaving no way to dismiss the keyboard. TOGGLE_KEYBOARD is appended
last in SysHotkey because bindings resolve by ordinal.
Key states are paced on a worker thread: usbKeyboardKey only sets a bind value
that the VM samples on its own schedule, so a press released immediately can
fall between two samples and never register.
Compatibility only has a value where the GameDB carries one, so the row read
"—" for most of the library. Play time is populated for anything actually
played, and the per-serial totals were already being recorded.
The per-serial tracking never stopped - PlayTime.startSession and
endSession still bracket the running VM, and PlayTime's own comment says
it is shown in the info tab. Only the two rows that displayed it were
lost in the interface rebuild, so existing users already have totals
recorded and will see them as soon as they open the tab.
Material You uses the wallpaper-derived dynamic palette. It needs Android
12 while minSdk is 26, so the option is hidden below that rather than
falling back silently - picking a theme and getting a different one reads
as a bug. It also follows the system light/dark setting, so it joins
System in the two places that decide system-bar contrast; left in the
dark-theme branch it would have put dark status-bar icons on a light
palette.
RGB cycles the hue continuously the way peripheral lighting does, accent
and surfaces together. The hue is quantised before the scheme is rebuilt:
a ColorScheme change re-runs MaterialTheme and recomposes the whole tree,
so animating it per frame would repaint every screen at display rate for
a decorative effect.
Custom derives a scheme from a colour picked with RGB sliders. The hue is
kept exactly while saturation and brightness are clamped into a legible
band - a raw accent lets you choose near-black or a muddy brown and get
unreadable chips, which comes back as a bug report rather than as a bad
choice. Surfaces take the same hue, as the fixed palettes do.
Patch manager: the install action rendered below the full cheat list, so
with fifty-odd cheats it sat off the bottom of the screen and a ticked
patch was never installed - the tick is only a selection. It now sits
above the lists. Installed files are listed for the running game rather
than every game at once, failing open when there is no serial to scope
by, since hiding a file the user just installed is worse than listing a
few extra. An install with no known CRC now reports that instead of
writing a filename the core will never load.
Memory cards: with a game in context the slot buttons write a per-game
card and can be cleared back to the global one. Previously both slots
were always global and a separate button covered slot 1 only, so "this
card in slot 2 for this game" could not be expressed at all. Folder
cards are created through the Java file API, and are shown as folders
rather than as zero-byte files.
Also: achievements in progress sort above the rest, and the library
keyboard gains a shift key.
The right stick's directions could not be bound on a Joy-Con while R3
bound normally, because R3 is a keycode and the directions are axes: a
Joy-Con reports its right stick on AXIS_RX/RY and every right-stick path
read AXIS_Z/RZ, so the axes were simply invisible. Resolve the pair per
device (cached - getDevice is a binder call and motion events are far
too frequent to query per event) and use it for bind capture, dispatch,
stick hotkeys and both D-pad folds. Pads that report Z/RZ are untouched.
Adds a per-stick response curve, a left-stick-as-D-pad mode, and routes
both halves of a Joy-Con pair to one port so a game sees one controller.
L1/R1 now flick between settings tabs. It is handled in the key
dispatcher rather than in Compose because shoulder buttons never reach a
Composable, and the hook is registered only while the settings screen is
showing, so in-game shoulder presses still reach the pad.
Launching a .cue from a frontend resolves the sheet's first track and
boots that instead; the core has no cue parser and .cue is not in its
disc whitelist, so the file could never boot directly.
Reset restored the entire scope - Settings() globally, or the whole
per-game override blob - so pressing it on the Renderer page also wiped
Audio, Network, Performance and Fixes. It now resets only the tab being
shown, the confirm dialog names that tab, and it is hidden on tabs that
own no settings at all. Per-game scope prunes just that tab's override
keys instead of deleting every override the game had.
Themes: "Dark" was always the blue-tinted dark theme, which only became
confusing once other hues existed, so it is renamed to Blue and joined
by Purple, Pink, Red, Orange, Green and Teal. Each is built from the
night scheme the way Black and OLED already were, so surfaces carry the
accent's tint rather than leaving blue chrome under a different accent.
The stored preference is the enum name, so an existing "Dark" simply
falls through to the Blue default - same colours, nothing to migrate.
The picker is now a wrapping chip group driven off the enum; a segmented
row is fixed-width and would squeeze eleven options into slivers.
Also: a GS multi-threading toggle, a portrait top/centre option, an
honest 59.94 Hz NTSC framerate stop (an integer slider could neither
display nor re-select the true default once dragged), and round action
buttons centre their glyph rather than its padded layout box.
libEGL_angle.so and libGLESv2_angle.so were never tracked, so a fresh
clone built without them. applyAngleEnv silently unsets its environment
variables when the libraries are absent and the OpenGL renderer falls
back to the system GLES driver with no error, which meant the ANGLE
option quietly did nothing from the first build made in a clean checkout
onward - on precisely the devices whose native GLES stack it exists to
work around.
These are prebuilt vendor binaries, not build output, so the usual rule
of keeping jniLibs out of the tree does not apply to them.
Two fixes that both live in the JNI layer.
Folder memory cards on a user-chosen data folder crashed on the first
new save. FUSE-backed shared storage denies libc file CREATION even
though mkdir is already routed through Java, so SaveYAMLToFile opened a
not-yet-existing _pcsx2_index with an unchecked OpenCFile and then
dereferenced null. Existing saves reuse that file, which is exactly why
only new saves crashed. Null-check the write, and add a
CreateFileViaJava fallback in OpenCFile so a denied create is retried
through the Java file API - the same libc/Java asymmetry that
CreateDirectoryPath already relies on.
Patch and cheat enable-state was written to the base settings layer,
keyed only by patch name, so enabling e.g. "Widescreen 16:9" for one
game switched on the identically named patch in every other game.
LayeredSettingsInterface returns the first non-empty layer with the game
layer ahead of the base one, so upstream keys this per serial and CRC;
do the same, and strip the migrated names from the base list so an empty
per-game list cannot fall back through to it.
The per-game INI exporter also rebuilt the file from scratch, dropping
every key it does not own - the patch lists above, and per-game
MemoryCards and Gamefixes overrides. Load the existing file and clear
only the sections the exporter actually writes.
In portrait the image was always centred vertically, leaving the game
floating in the middle of a tall screen. Allow pinning it to the top,
which is what a phone held upright generally wants.
The replacement cache had no size limit and no eviction. It was only
ever cleared wholesale on shutdown or game change, so every replacement
a game touched stayed resident until the process was killed: a 5 GB
uncompressed DDS pack OOM-killed the emulator mid-load, and turning
precache off only changed how quickly memory filled.
Track bytes and evict least-recently-used entries past a budget derived
from physical memory - a quarter of RAM, capped at 3 GB so a pack that
would otherwise fit is not evicted needlessly. A texture larger than the
whole budget is uploaded once without being cached, rather than evicting
everything to make room for something that cannot help.
Also pace the uploads. ProcessAsyncLoadedTextures uploaded every pending
replacement in a single VSync while holding the cache lock. Entering a
new area streams a batch in at once and an uncompressed 2048x2048
replacement is 16 MB, so ten arriving together meant roughly 160 MB of
GPU upload inside one frame. Spread them across frames instead; the cost
is a frame or two of pop-in rather than a dropped frame.
The mobile fallbacks were gated on TARGET_OS_IPHONE only. On Android
GetAdapterAuto still required a host default gateway, which the app
sandbox cannot read out of /proc/net/route, so auto-selection failed and
InitNet force-disabled Ethernet for the session - reported as
"connection device not found" with the interface plainly visible in the
picker. GetDNS likewise returned an empty list because there is no
/etc/resolv.conf.
Extend both gates to __ANDROID__: select on a usable IPv4 interface and
fall back to public resolvers. iOS and desktop behaviour is unchanged.
Off/On toggle (On = Pipelined, enum 3) for GSBackThreadMode, default Off:
- Renderer settings tab, under the graphics-API/driver picker
- In-game quick menu (Graphics), grouped with renderer + Apply & Restart
(MenuSwitchRow gains an optional inline description)
- Settings model with per-game override, i18n strings, search index entry
- native-lib snapshots the field across a live settings apply so the
restart-required option can't trigger a mid-game device recreate
The Android achievements UI already reads encoreMode, spectatorMode and
unofficialTestMode out of the options JSON and offers toggles for them, but the
native emitter never wrote those fields, so all three always read false
regardless of the underlying setting. The rc_client wiring behind them already
exists; this only exposes their state.
Applied as a hunk rather than a file copy so the iOS UserStats/GameStats/
AchievementList stubs in this file are preserved.
(cherry picked from commit a0bdcbe917)
Pause button: one clean top-right glyph, single tap opens the menu, and the
on/off toggle is replaced by a tap-to-reveal option that cannot lock the user
out. OSD defaults to 65%, migrating anyone still on the old 100 default, and the
mislabelled On-Screen slider is renamed with the two UI-size sliders moved up
beside it. Widescreen patches relabelled to state that they auto-apply.
Custom game names, editable per title from the Info tab.
The in-game keyboard is hosted once at the top level. It was mounted inside the
in-game screen, but Settings is a separate nav destination that unmounts it, so
the keyboard only appeared after backing out of per-game settings.
Removes the duplicate All Settings shortcut from the in-game menu rail (the
Options tab already has a labelled one) and centres the remaining tabs, which
were left top-aligned with dead space beneath them.
GameDB: Everybody's Golf 4, Magna Carta, Rumble Racing and others from the merged
android-v24 batch, plus four repairs to it - a misspelled gsHWFixes key and a
value missing its '#' comment marker, each of which silently dropped that game's
entire fixes block. Ratchet: Deadlocked / Gladiator get FullVU0SyncHack across
all six SKUs, which is what clears the in-game lockup.
(cherry picked from commit 42baebb860)
Patches under RetroAchievements hardcore: the gate dropped every on-disk pnach,
which is asymmetric with the fallback below it - the bundled patches.zip stays
enabled in hardcore, so a widescreen or bug-fix patch worked from the archive and
silently did nothing from disk, killing everything the in-app Patch Manager
writes. Gate cheats only; they remain blocked at enumeration and in
ReloadEnabledLists, and the two feed separate stores.
Overscan crop (issue #293): the core has always honoured GSConfig.Crop but
Android never exposed it. Four sliders in native PS2 pixels, so a value means the
same thing at any upscale multiplier.
Custom OSD colour: new GSOptions::OsdColor, with the ImGui overlay drawing from
it instead of a hardcoded white.
Fast-forward now uses Unlimited rather than Turbo. Turbo caps at
EmulationSpeed.TurboScalar (2.0x) and produced no visible speed-up on these
devices while "frame limit off" - the same Unlimited mode - demonstrably did.
Settings.applyTo() also forced the limiter back to 0/3 on every apply, cancelling
an active fast-forward while the UI still reported it on; it now preserves the
latch like the in-game overlay path already did.
Per-game settings when launching from a frontend: an external launch passed a
null GameInfo, so settingsKey was null and launchGame resolved global settings -
per-game settings, per-game memory cards and per-game orientation all ignored,
while the same title from our own library applied them. Build a GameInfo for the
incoming URI, probing the serial off the image the way the library scan does.
(cherry picked from commit 7c16cbfa13)
Mobile GPUs frequently expose no block-compression support at all (Vulkan
textureCompressionBC false on Adreno 650 / Snapdragon 865, and on Mesa Turnip
for any Adreno; OpenGL gates S3TC and BPTC on separate extensions). The DDS
loader rejected those files outright, so an entire pack silently did nothing.
Worse for packs that also ship game-side data: the P3P Slim Font mod pairs new
FONT0.FNT glyph metrics with 1467 BC7 replacement glyphs, so with the textures
dropped the game indexes new narrow metrics into the old wide atlas and renders
letters sliced in half. Decode BC1/2/3/BC7 on the CPU when the GPU cannot sample
them; the decoders were already built (common/TextureDecompress.cpp, previously
used only for alpha min/max).
Every failure path out of ReloadReplacementMap was silent and they all look
identical from outside - feature off, wrong serial, empty folder, unparseable
names - so pack problems were unanswerable without an instrumented build. Log
the indexed count and the exact directory scanned ("indexed", not "loaded": the
number only proves filename discovery and parsing).
The Texture Manager now shows the serial the CORE will scan, read live from
VMManager::GetDiscSerial(), and warns when no installed pack matches it. Booting
a raw .ELF takes the elf-override branch where the serial becomes the ELF's
filename, so packs installed under the disc serial were never found. Import now
uses that runtime serial too. Texture Packs is reachable from the in-game menu
with a restart button, since the replacement map is only built at boot.
(cherry picked from commit 566ef031d8)
libc mkdir() is denied on the FUSE-backed emulated storage Android hands out for
a user-chosen data folder, while java.io.File.mkdirs() on the same path succeeds.
FileSystem::CreateDirectoryPath went straight to mkdir() and returned failure, so
every folder-memory-card save-data creation failed: "Format failed", and a crash
on first save in Soul Calibur 2 / Ratchet & Clank / GT4. Reproduced only with a
custom data folder, never with internal app storage.
A Java bridge for exactly this existed (NativeApp.createDirectoryPath plus the
FileSystem::CreateDirectoryViaJava JNI) but nothing called it after the monorepo
migration - the linker was dropping it as dead code. Wire it in as a fallback on
EPERM/EACCES, in both the flat and per-segment recursive paths.
Also adds folder-card import, which had no working route at all: a folder card is
a directory plus a _pcsx2_superblock marker, but the picker was OpenDocument()
(files only), so people zipped them and the importer appended ".ps2" to the
archive and copied it verbatim - producing a card the core read as unformatted.
Directories can now be imported directly, zips are unpacked, and both validate
the superblock instead of silently producing a broken card.
(cherry picked from commit 265ddb7657)
The unlock / message / leaderboard-submit sounds were caught by the blanket
*.wav ignore, so the assets folder shipped empty from a clean clone even though
local builds had the files on disk. Add them and un-ignore that folder so future
sounds aren't dropped the same way.
- Replace the in-game settings cog with a single top-right pause button (#357).
Single tap opens the menu; it renders outside the auto-hide/"Never" gate so
hiding the on-screen pad can no longer strand you without a way in.
- "Tap to reveal pause" replaces the old show/hide toggle, which could lock the
menu away entirely. Migrates old layouts, including per-game and per-orientation.
- Run the RetroArch shader chain at the frame's on-screen size instead of the
internal one, so CRT scanlines land at display pixel density. Sized to the
aspect-corrected draw rect, since librashader maps input to the whole viewport.
- Patch manager: stop one game's patches showing under another, de-duplicate
repeated cheats, split patches/cheats into collapsible sections, and drop the
lag on large lists. Rename the widescreen toggle to say it auto-applies.
- On-Screen settings: the top slider drove OSD scale while labelled "UI Size" and
shared a label key with the real UI slider. Renamed to "OSD Size" and grouped
the three size controls together. OSD now defaults to 65%.
- Per-game graphics API, rotation and GPU driver.
- Gate the Adreno push-descriptor disable on driverID so 8 Elite keeps them.
- Load/save state slots no longer squash on the Load screen.
- Sync GameDB and fix two entries that parsed as no-ops: Genji's vu0ClampMode
casing and DOA2's mis-indented minimumBlendingLevel.
Follow-up to #334. The carry sits inside a condition that only requires ONE of rt/ds to match the current target, so a draw that kept the RT but swapped the depth target could still inherit a stale depth feedback layout - the exact flicker mode the previous draw-local comment warned about. Gate each carry on its own target instead.
Also restore the non-Broadcom flicker warning, including the note that a vendor-scoped carry was tried and reverted once, so the global draw-local behaviour does not get removed again by mistake.
IsDeviceBroadcom now records which build actually reaches it (Linux arm64 / Raspberry Pi V3DV), since this is otherwise surprising in an Android-focused tree.
- Gyro aim/steer now sums with the physical stick (coarse stick + fine gyro) instead of clobbering it
- Per-game stick invert/swap and D-pad-as-left-stick now scope per game (were global-only)
- Online patch browser reads the serial from the disc image, so it works from the library, not only in-game
- Per-game BIOS override, assignable from the library long-press, applied at boot with a global fallback
- ANGLE for OpenGL moved into the graphics-API driver picker, shown when OpenGL is selected (Render tab + in-game)
- Add WearyConcern1165/ExynosTools as a GPU driver download source
- In-game menu: one-tap Fast-Forward in the Session tab (enables max speed + resumes; shows On state)
- Driver picker: show the detected GPU model + recommended driver source (Adreno 8xx/7xx/6xx map to a tuned Turnip pack, other GPUs to the built-in system driver) — appears in both the in-game menu and full Settings
- Gyro: Aim mode can now drive the Right or Left analog stick (per-game aware), for games that aim with the left stick such as Resident Evil 4
- Cheats: fix the per-cheat on/off toggle on PNACH files saved with CRLF/CR line endings (was failing with "unusual formatting")
- OSD: reload-immune visibility snapshot (fixes won't-turn-off); on/off hotkey now keeps the user's chosen stats
- Library: hide/unhide games; optional titles under shelf covers; Clear cached data in the App tab; refreshed drawer icons
- Per-game settings: fix scope stickiness after closing a game + open correctly from the shelf layout; green Play button that actually launches
- RetroAchievements: custom achievement-unlock sound picker
- Vulkan: re-enable attachment_feedback_loop_layout on Mali so accurate blending runs in-tile instead of the per-primitive barrier fallback (big speedup)
- ANGLE (GLES 3.1): emit GL_EXT_shader_io_blocks for interface blocks + alias glColorMaski to OES/EXT — fixes Mali-G77 black screen and mid-render crash
- In-game menu: Performance tab is now a yellow lightning icon
- OpenGL-via-ANGLE renderer toggle for broken native GLES drivers, with a
driver-keyed GL shader cache so switching drivers recompiles instead of
feeding foreign program binaries to glProgramBinary (fixes Mali-G77 crash)
- Gyroscope input (aim/steering modes, sensitivity, smoothing, invert) shared
between the Pad settings tab and the in-game Controls tab
- Re-add disc swap without closing the game, and per-cheat PNACH enable
- Xclipse GPU profile + Mali-G615 freeze gate; MediaTek Tekken 5 override
- GameDB: KH2, Tekken 5, Rumble Racing, MK Shaolin, Avatar
- Folder-reuse settings recovery (reverse-map INI + config mirror)
- In-game pause menu rail icons; make new settings searchable
- Resume/auto-load: wait for the renderer to present before restoring state
and force a present after load (reduces black screen on resume)
- Boot crash guards (pad state before VM); FXAA + CAS sharpening
- ci-nightly-dualcore.sh signs nightlies with the release rotation lineage
(debug<=API32 -> release>=API33) from repo secrets, so a nightly installs over
the existing com.armsx2 build; falls back to a throwaway key (with a warning)
- versionCode = Unix seconds since 2023-11, monotonic and always above the manual
10xx codes, so each nightly out-versions the last installed build
- versionName default 2.5.9 -> 2.6.0
- Discord announcement now includes the changelog (trimmed to Discord's limit)
- tools/ci-nightly-dualcore.sh builds the core at both host page sizes and
merges both .so into one APK so 16k-page devices load their native core
- nightly builds with PGO=optimize (committed pgo/armsx2.profdata)
- trigger the nightly on push to master (was schedule + dispatch only)
- plain-language changelog in the release body from filtered commit subjects
- Global/per-game settings scope toggle at the top of the settings rail,
plus an All Settings shortcut at the bottom
- Settings search overlay + index
- BIOS manager: scrollable list so controller navigation reaches every entry
- Close-app entry with a confirm dialog
- i18n updates
- Mali Vulkan: drop the old blend-clamp band-aid; rely on dual_source_blend
feature detection so only SRC1/blend-mix draws take the SW-blend path
(no more forced Blending=Max on Mali)
- Fix intermittent stale-tile reads: input-attachment descriptor type for the
texture-barrier feedback path, vendor-scoped to Mali
- broken_mad_deinterlace weave fallback for Mali-G57 FastMAD
- Add per-vendor mobile GPU profile system (Mali/Adreno/PowerVR)
- Tie test_and_sample_depth to texture_barrier (was forced on)
- Richer Vulkan device telemetry for field diagnosis
The looping MP4 wallpaper hurt in-library performance (sbro review). Replace it with a bundled static XMB-wave still (R.drawable.library_bg_xmb, drawn edge-to-edge via ArmsBackdrop's backgroundLayer). Remove VideoBackground.kt, res/raw/xmb_wave.mp4, and the video option from the background picker.
OSD now hides via RenderOverlays mirror of EmuConfig.GS->GSConfig + seed-false on first launch. Rumble: forward SetPadVibrationIntensity to Native::onPadRumble on Android (mono core had no call site). Library: bundled PS3 XMB-wave MP4 as default background, drawn edge-to-edge via ArmsBackdrop backgroundLayer (fixes landscape strip). In-app on-screen keyboard for library search; Recently Played shelf selection highlight; settings category tabs reachable via Row+horizontalScroll. Persian (fa) translation; gold RetroAchievements trophy.
The pinned bottom toolbar rendered edge-to-edge (full width) while the top one is
inset by the grid's 8dp + cutout content padding, so it read wider. Apply the same
side inset to the bottom bar's Box so both placements are identical width (vivi).
- Add a Library-toolbar position setting (App tab, Top/Bottom). Both placements
render the SAME ArmsTopBar rounded-pill (bottomEdge just swaps the status-bar
inset for the navigation-bar inset), so top and bottom look identical — the old
bottom bar's top-only-rounded shape was the mismatch vivi flagged.
- At the bottom, the controller reaches the toolbar by pressing Down off the grid's
last row; the grid reserves the matching top/bottom content inset per position.
- Fix Recently Played being skipped in shelf view for real: replace the
selectedIndex<columns 'top row' guess (fragile when the per-row count is off) with
a move-then-check — attempt moveSelection, and if the selection didn't move we're
on the edge row, so step into the chrome zone. Works regardless of shelf/grid
column count in either direction (up->chrome, down->bottom toolbar).
- Revert the library toolbar back to the top (bottom bar looked worse).
- Camera-follow: the grid only scrolled to the selection while searching, so
normal controller browsing never followed the selector. Now it follows always,
targeting the selected cover's real lazy-grid index (accounts for the leading
toolbar/search/recents/header items and shelf rows) and only scrolls when the
cover is off-screen. This also lets the selector visibly climb to the top row
and into the Recently Played shelf in shelf view.
- Make the search field a controller Search zone (Toolbar -> Search -> Recents ->
Grid): highlights when focused, A focuses it and opens the keyboard.
- Language screen: LazyColumn -> scrolling Column so every language row composes
and registers in the nav registry — controller nav no longer sticks on the last
on-screen language (Italian) and scrolls to follow the selection.
- Add a Fixes tab to the in-game pause overlay rendering the full FixesTab,
live-applying via InGameOverlay's shared Settings; its controls are
SettingsControllerNav items so the overlay's content-pane nav drives them.
- Move the library view toolbar (menu/refresh/sort/layout/2D-3D/background) to a
pinned bottom bar (vivi's mockup). Controller reaches it by pressing Down off
the grid's last row; grid reserves bottom padding + owns the status-bar inset.
- Shift the night palette toward the OG ARMSX2 blue (deep navy backdrop/surfaces).
- BootSplashActivity plays the refresh intro video (res/raw/boot_intro.mp4)
once per process with tap/back/timeout/error fallthrough; new App-settings
toggle (ui.bootLogo, default on) skips it straight to Main when off.
- Shelf view: report per-shelf column count so D-pad up/down move between
rows and the top row can reach the Recently Played zone (was left/right only).
- Controller-nav skip fixes: Language row, split GPU-driver source groups,
UI-size + in-game framerate sliders now register/adjust.
- Add Ukrainian + Kurdish languages; re-translate Russian for accuracy.
- Drop a duplicate EN i18n key (games.toolbar.background).
Re-apply the Android-specific GS deltas on top of the canonical GS as guarded hunks so
PC/mac/Linux/Windows stay byte-for-canonical:
- VK: restore the push-descriptor fallback (Adreno/Mali stall inside
vkCmdPushDescriptorSetKHR -> textures never bind -> black screen) as a capability-gated
path; desktop keeps push descriptors unchanged. Mobile vendor gates
(Mali/Adreno/PowerVR/Xclipse), dyn_shaderc, FIFO_RELAXED present mode, PowerVR swapchain
width-align. depth_feedback forced off only under #if __ANDROID__ (desktop keeps
feedback_loops()).
- OGL: re-apply GLES support (EGL context, is_gles shader branches, GLES query objects),
runtime-gated by is_gles; guard 3 desktop-reachable riders (present-path
glInvalidateFramebuffer -> is_gles, EGL SetDisplay body -> #if __ANDROID__, restore the
negative-swap-interval probe).
- RA toast: guard the AddRect call on IMGUI_VERSION_NUM (desktop imgui 1.92.8 swapped the
thickness/flags args; Android vendors 1.92.6) so the toast border renders on both.
- Suppress the 'Graphics API is not set to Automatic' OSD warning on Android
(#if !__ANDROID__); desktop keeps the canonical warning.
- REFACTOR_STATUS: mark the GS re-apply resolved; note the imgui two-copies version skew.
Drop the aR5900*.android.cpp EE fork; Android now builds the canonical Phase-7 aR5900* like every other arm64 target. Device A/B (canonical EE+VU + force-float, PGO tuned for the mac-port so rigged against canonical) showed canonical EE at refresh parity on Android: GoW2 combat EE ~12.8ms, 100% speed / 60fps. The real Android perf lever is the force-float in VMManager::SetEmuThreadAffinities, not the backend. Restores the single shared-core goal.
Back-port the set_user_data() setter onto the Android-vendored rapidyaml 0.10.0 so common/YAML.cpp (from the pcsx2master merge) compiles; the PC/mac build resolves a newer system ryml via find_package. Minimal shim pending the 3rdparty rapidyaml de-duplication (REFACTOR_STATUS #4).
Vulkan render fixes (GS device backends), Oboe audio backend, GameDB armsx2_overrides.yaml override loader, EE+VU mac-port recompiler graft, PGO=optimize, and the VU-slam fix: an Android-only force-float in VMManager::SetEmuThreadAffinities (the slam was thread pinning locking the VU1 worker off the prime core, not the VU codegen). Also: CPU-name SoC-property fallback, Mali VK attachment-feedback-loop crash gate, MediaTek fbfetch disable, and on-device thread-placement diagnostic helpers.
The recompiler grafts and GS deltas are unguarded and land in the shared core (they reach the mac/Linux arm64 builds) - see REFACTOR_STATUS.md items 2 and 3 for the reconciliation this still needs; the VU graft is reducible to just the force-float fix.
- Host::GetHTTPUserAgent: report ARMSX2/2.7.407.0 (the RA-registered client)
instead of 'PCSX2 <gitrev>', which RA rejects as an outdated emulator and so
disables hardcore unlocks.
- ImGuiFullscreen: fix ImGui 1.92 AddRect arg order (flags/thickness swapped)
that drew a ~240px border on the RetroAchievements toast.
- VMManager: suppress the false 'Controller 1 has no input bindings' warning
(Android injects pad state directly, bypassing InputManager bindings).
SetFonts() is never called on Android (no Qt/language layer) so s_font_info
stayed empty and AddTextFont() returned null -> blocky/blank OSD. Seed it from
the bundled Roboto-Regular.ttf, and fall back to the built-in font if the atlas
still fails so the GS device can initialise regardless.
- R5900 _cpuEventTest_Shared: bail out early on VMState::Stopping/Shutdown and
on IsExecutionInterrupted() so the EE leaves Execute() promptly on stop
(previously hit the ~5s shutdown timeout).
- native-lib Host::RequestVMShutdown: no-op if already Shutdown. The queued
task could re-set Stopping after the run loop already reached Shutdown,
sticking s_state at Stopping so the next Initialize failed 'already running'
-- kick back to library on the 2nd game boot.
The IOP dispatch loop lost its 'if (iopRecNeedsReset)' guard, leaving a bare
recResetRaw() that ran a full IOP recompiler reset on EVERY block (~800/s).
recClearLUT zeroing the LUT then dominated ~97% of EE-thread CPU in memset, so
every game crawled ~10-50x too slow (black screen / Speed 0%). Also drops a
duplicated recPtr/const_pool.Reset/recClearLUT block in the same function.