Toggling PINE from the in-game menu wrote it nowhere, so the setting was gone
at the next launch while the switch still read as enabled -- saveSettings had
already updated the in-memory Settings, and only a process restart exposed that
the store never agreed.
PINE is one server for the whole process, so "this game runs with PINE on" is
not a thing that can be true. Settings.merge therefore pins pineEnabled and
pineSlot to the global value, and Settings.diff never emits either key, so a
per-game file can never acquire them. Both are deliberate and both are right.
What was missing is the other half: a Game-scope save writes ONLY the override
file. So for these two fields the write had no destination at all -- the
override file structurally refuses them, and global was never touched. Every
other field is fine, because every other field is one the override file accepts.
The in-game menu saves in Game scope whenever a game is running, which is
exactly when someone reaches for PINE, so the toggle looked simply broken.
So promote those fields to global on a Game-scope save. Copied onto the loaded
global rather than saving `updated` wholesale: `updated` is the game's RESOLVED
settings, so writing all of it to global would push every per-game value into
the global layer. The diff below is unaffected -- it reads the pre-promotion
`global`, and the keys involved are precisely the ones it never emits.
Pairs with the core fix that makes a commit act on the value; without this the
value never survived to be acted on a second time.
The Auto renderer resolution picked Vulkan on Adreno and OpenGL everywhere
else, on the reasoning that Mali runs GL_ARM_shader_framebuffer_fetch and so
has the in-tile fast path on GL. That holds for a healthy Mali. It does not
hold for a driver on the fetch blocklist, and the two decisions were made in
different places, so nothing noticed when they disagreed.
On GLES framebuffer fetch and the texture barrier are one capability -- there
is no ARB or NV barrier extension -- so a blocklisted driver loses both. That
is not a mild fallback on a tiler: it is not only accurate blending that starts
reading the render target from a copy, it is every self-referential draw, and
each copy forces the tile to flush and resolve to main memory. Measured on an
Anbernic RG 477V (Mali-G615, r44p1) with Shadow of the Colossus: 7 fps on
OpenGL against ~30 on Vulkan, same device, same settings. Vulkan reaches the
same copy-based concept with an ordinary image copy and no tile flush.
So Auto now also prefers Vulkan when the device's OpenGL driver profile carries
UseRenderTargetCopyForFeedback. Both halves of the question are asked of the
driver database rather than of substrings, which also retires the case-sensitive
search for "Adreno" in GL_RENDERER in favour of the resolved runtime profile.
The decision has to be native, because the database is: rules match a PARSED
driver revision, which is what lets one say "exactly r44p1". The app cannot do
that, so it now hands over the GL strings it already probes -- GL_VERSION is
where the driver revision lives, and the probe was reading GL_RENDERER and
throwing the rest away -- and GSUtil::AndroidAutoPrefersVulkan answers.
setPreferVulkan(boolean) is replaced by setAutoRendererGpuStrings(3 strings)
rather than kept alongside it; there was one call site.
An explicit Vulkan/OpenGL/SW pick still wins, as before. The only devices this
moves are the ones whose GL is degraded: currently r44p1 Mali and nothing else.
The committed profile was generated from ARMSX2-mono-recovered and last refreshed
on 2026-07-13 -- its own function paths name that tree. Building against it costs
7.7% of .text (15,262,600 -> 16,433,556 bytes) versus a matched profile, because
every function the profile does not cover falls back to static inlining
heuristics. Size is the visible symptom; the risk is speed, and this is the same
class of defect as the #165 VU slam, where a profile predating recompiler churn
made LTO optimise the hot VU paths the wrong way.
This one is captured from armsx2-push-staging at e9f8f8366 -- the first profile
ever taken from the tree it is used to build. 38,219 functions against the old
profile's 36,807, with microVU (292 entries), recExecuteBlock, the recompiler
dispatchers, GSRendererHW::Draw and the VIF/GIF transfer loops all covered.
Rebuilding with it lands .text at 15,173,960, 0.6% BELOW the last known-good
build despite carrying more code -- a matched profile inlines selectively where
an unmatched one inlines blindly.
build-release-apk.sh required PROF unconditionally, which made regenerating
impossible: PGO_MODE=generate builds the instrumented APK you play in order to
CREATE a profile, so demanding one up front failed instantly with a FATAL naming
a file that run never reads. Require it only in optimize mode.
EnablePINE and PINESlot were already INI-backed and VMManager::ReloadPINE
already starts and stops the server when they change, but the Android frontend
never surfaced them, so there was no way to switch PINE on from the device.
It sits beside the recompiler switches because it is the same class of control:
a developer tool a player has no reason to find, next to the other things you
turn on to diagnose rather than to play. The row states the address and, once
enabled, the adb forward line -- the listener is on loopback, so it does nothing
until a workstation bridges the port, and a port nobody tells you about cannot
be bridged.
The port itself gets no editing widget. The only reason to move it is running
two emulators on one machine, which does not happen on a handheld, and a
free-entry port field is a support burden for a knob nobody turns; it stays
readable from the INI. It is still carried in the settings model so the row can
state the real port rather than assume the default.
Note the per-game merge is a full constructor, so a field omitted there resets
to its default instead of inheriting: PINE is a process-wide server and cannot
be per-game, so it is absent from the diff (no game file ever acquires the key)
but explicitly carried through the merge.
435f8bd9fd marshalled the numbered-slot save/load JNI entry points onto
the CPU thread but left the autosave pair running directly on the JNI
thread with only the park. The park stops the EE, but it does not confer
thread identity: the freeze pushes to the single-producer MTGS ring, and
the load additionally pushes micro/data memory into the MTVU ring and
resets the recompiler caches, all owned by the CPU thread. The autosave
pair is reachable from Save State And Exit, auto-load-on-boot, and the
load picker's autosave tile, so those flows kept the unpoliced races the
slot paths were cured of.
Same treatment as the slot paths: marshal via Host::RunOnCPUThread with
the park retained, and run the loadAutosaveState present in the same
task so it cannot race the resume in the pause guard's destructor.
Makes every modal in the Android UI reachable from a gamepad. Android windows
take focus and consume key events before dispatchKeyEvent, so every AlertDialog,
ModalBottomSheet and DropdownMenu was a dead end on a handheld: both exit
confirms, the hardcore confirms, BIOS and memory-card delete, the manager
error dialogs, the stick-target and macro pickers, the per-game sheet, both
overflow menus, and the memory-card and network text entry. 24 window-modal
call sites across 14 files, now zero, with a preBuild check to keep it that way.
Also carries the pause menu drawing its highlight from the nav registry it
actually moves, tab navigation along the axis the strip is drawn on (every
sub-700dp device walked it across the short axis), nav-layer containment so a
selection cannot step through a scrim onto the row behind it, and the raw NUL
in SettingsSearchOverlay.kt escaped so the file stops being invisible to grep.
This branch was PR #526, closed unmerged. An audit of all 19 commits against
master found none of them present, in whole or in part -- the window-modal call
site count on master was still identical to the merge base. Merged now with
jpolo's agreement.
Its save/load CPU-thread marshal was cherry-picked ahead of this merge as
435f8bd9fd, because it was aborting assert-enabled builds on device.
Conflict resolution: SettingsSearchOverlay.kt only, and only because the raw NUL
made git treat it as binary and refuse the three-way merge. Resolved as text
with the NUL held aside: master's additions retained in full, plus the branch's
two changes to that file (escape the NUL, drop the duplicate keyboard host).
Saving a state from the pause menu aborted every assert-enabled build. The
screenshot the save embeds goes through MTGS::RunOnGSThread, which asserts it
is on the CPU thread, and the JNI entry point ran the whole save inline on
whatever thread the picker dispatched it from.
Parking the VM first, which is what these two entry points did, is not the
same guarantee. It stops the EE, but the MTGS ring's write position is
single-producer and owned by the CPU thread, and the CPU thread does not stop
producing when the VM is paused: its idle loop keeps draining
Host::PumpMessagesOnCPUThread() every 16 ms, so any GS-settings apply or window
resize queued from the UI pushes to the same ring the save is pushing to. Two
producers claiming one slot drops a packet, and a dropped data-packet header
leaves the GS thread parsing payload qwords as command tags.
So marshal both entry points with a blocking Host::RunOnCPUThread, matching
what commitSettings and changeDisc in the same file already do. The park stays:
it stops the EE for the inline zip and holds the audio pause the picker is
built around. Thread identity is what makes the ring pushes legal.
The load path is fixed alongside it. It has the identical violation — Freeze on
the way in, plus a recompiler cache reset — and goes unreported only because
MTGS::Freeze pushes its packet directly rather than through RunOnGSThread. Its
follow-up present moves into the same task, which also stops it racing the
resume in the pause guard's destructor.
A Use Global, Off, On row in the per game Graphics tab, under Hardware
Fixes and Display beside Hardware Download Mode. The generic write
helpers already keep the claim mask in step for it, so the setting
survives the mask and the game database without manual hacks mode.
Asked for by a user who noticed the global screen had it and the per
game panel did not.
The eleven newly claimable hacks join the pinned key table and the hack
state table, the five picker and number rows go through the claiming
binding, and the bool rows claim from their shared write funnel.
Unpinning any of them puts the default back, and resetting all settings
drops the claims along with the values.
Every one of these rows wrote its value and then watched the mask throw
it away on the next settings load. The hack state table entries are
what give them the effective value note and the way back to the
database value.
Tightens the new comments this branch added and the longest blocks it
sits next to, so every file it touches lands at or under the comment
density it started with.
Two of the trimmed blocks described what the code no longer does, which
is the kind of comment that goes stale without anyone noticing.
Three source assertions in the style of the descriptor tests: every
hack the bridge reports pins from its global row, every reported hack
is in the per game claim derivation table, and the writer never grows
a global claim merge again.
Texture Inside RT, Native Scaling and Bilinear Upscale go through the
claiming binding, the three bool hacks with pin bits claim on write,
and the bridge's hack table grows their entries so the notes and the
unpin button work for them too.
These had pin support in the core since the claim mask landed, but no
frontend ever set their bits, so the database kept overwriting them
the same way it overwrote align sprite.
Use the game database value now writes the hack's default back through
the store property before dropping the pin, so the row keeps showing
what the INI actually asks for.
Unpinning alone left the old value in the INI while the core masked it
out and the database re decided, which read as a toggle that says on
and does nothing.
Half pixel offset, round sprite, the three sprite pickers and both
texture offsets now only need the per game master toggle, since a per
game value claims its own fix and the database steps aside for it.
Skipdraw keeps the manual hacks gate, and the captions now say only
skipdraw needs it.
The old gate is the closed loop from the report: rows went grey while
still showing on, the save kept writing them, and there was no way to
turn the hack off without flipping the whole panel off.
Align Sprite, Merge Sprite and Wild Arms Offset drop their override
plus value toggle pairs for the same Use Global, Off, On picker every
neighbouring row uses. The bridge takes one int with the use global
sentinel per hack, and the generic per game helpers keep the claim
mask in step whenever they touch a pinned hack key, which also covers
Texture Inside RT from the compatibility tab.
Two toggles per hack left an on state visible and stuck whenever the
rows were disabled, which is the shape of the report that keeps coming
back. One picker always names its state, and Use Global is always one
tap away.
Both per game reload paths now recapture the effective hack state after
the settings apply, the same call the global apply already makes.
Without it the graphics screen's hack notes kept describing the world
before the save, so even a change that worked read as stuck.
The per game writer derives the claim mask from the hack keys actually
present in the file instead of enumerating them, and stops folding the
global mask in, since the core now carries that across the layers. A
file whose stored mask disagrees with its keys is repaired when the
panel reads it, the same way the stale MTVU key already is.
Freezing the global claims into every game file meant a later global
unpin never reached those games, so the database fixes they should have
gone back to stayed suppressed for good.
The reveal tap moves onto a clear SwiftUI shape overlaid on the game
view, and the dynamic input zones report their classified taps through
a notification the game screen listens for.
The old gesture sat on the Metal render view, which iOS 27 makes non
interactive so the SwiftUI overlays own touch. On that OS the tap
could never fire, and once the controller poll was gone there was no
path left to bring a hidden menu button back. The zones matter for the
same reason: with dynamic thumbsticks or swipe camera on they tile the
whole landscape screen and eat every tap before it reaches anything
below.
The picker sits at the top of the Graphics section now, above Internal
Resolution, where the global screen keeps its own renderer section.
Fixes and Compatibility starts at Accurate Alpha Test, and its footer
drops the clause about the renderer needing a reset.
Testers kept looking for it under Graphics, which is where every other
renderer adjacent setting already lives.
The renderer write goes through the same useCurrent helpers as every
other per game key, and CheckForConfigChanges keeps a running game on
the renderer it booted with, iOS only.
Mid game the old forISO write resolved identity through a cache only
lookup that can silently miss, and even a hit can land the key in a
different file than the panel reads back and the boot path loads, so
the selection quietly vanished. The file only bypass never dodged the
live apply either: sibling writes in the same save trigger the same
debounced reload, and a renderer change in that reload would reopen GS
and tear the Metal device down under the running game.
Hide Menu Button is a real preference now. A tap on the game view shows
the button for four seconds and it fades back out, the controller input
poll is gone, and nothing writes the setting off behind your back. The
quick menu hint and the settings screen caption describe the new
behaviour, and the orphaned status message leaves the translation
tables.
Any controller input restored the button through a tenth of a second
poll, and any stray touch outside the pad brought it back too, each
time flipping the persisted setting off with it. The poll also never
bought a controller player anything: restoring the button still took a
screen tap to actually open the menu.
Move the orientation snap into syncRootViewToWindow and call it before
makeKeyAndVisible, after the SwiftUI menu is attached, and on
sceneWillEnterForeground as well as sceneDidBecomeActive. Compare the
two sizes rather than their orientation class.
A host container connects the scene while the app is still backgrounded,
where UIKit has no interface orientation to resolve, so the window takes
the real landscape bounds while its root view controller starts portrait.
Correcting that only once the scene was active left the menu squeezed
into a portrait strip for everything drawn before it.
Cache the preview images instead of reopening every PNG on each pass of
the Form. Size and clip the thumbnail in one place so a non-square
preview cannot spill over the row. Drop AppIconManager for the async
setAlternateIconName the SDK already exposes, fold the failure alert
onto the option it belongs to, drop a section header that repeated the
navigation title, and add the inline title the other settings screens
use.