It patched one product, 0.25 * pi, from the correctly-rounded 0x3f490fdb
to the 0x3f490fda the EE's multiplier returns, so Tales of Destiny stops
hanging. That product is one sample of the multiplier's one-ULP deficit,
and eeClampMode 3 models the deficit for every operand pair -- including
the asymmetry the gamefix reproduced by comparing fs and ft against
their own constants, which falls out of a predicate that reads ft alone.
The seven serials carry the clamp mode instead, and iFPU-arm64.cpp's
multiply is a bare Fmul again.
The toggle goes from the Qt, FullscreenUI, Android and iOS front ends,
along with the GameDB schema enum and the harness helper that set it.
Fix_FpuMultiply and the config bit stay: vu_capture's on-disk gamefix
mask is bit-indexed by GamefixId, so dropping ordinal 0 would
reinterpret every capture already recorded. tbl_GamefixNames keeps the
name, so a GameDB that still lists it parses and does nothing.
71 commits from 474ad59818 to 2cf8dabe6b, triaged rather than taken wholesale.
Declined, resolved to ours:
- AGENTS.md: upstream's AI-agent instructions; we carry our own and do not
want a second, conflicting policy file.
- CI deps bump (setup-node, labeler): both target workflows are absent here,
and the labeler job is gated on the repository being PCSX2/pcsx2.
- KDDockWidgets 2.4.1: two of the six files do not exist here; we already
build 2.4.0 against a 2.3.0 floor, so there is nothing to gain.
- The FullscreenUI Achievements-layout realignment: our section already
carries the same settings, and ours is the branded copy.
- The GS draw/vertex-buffer cluster (7887919e74, b2fa00844e, 99cfbb49c1,
5c611f85e1, 9945046a49, af48193ebb, d88510e3a6, 8c1bb5742e). Our vertex
kick is an ARM64 rewrite of the same hot path -- register-resident cursor,
fused min/max with a rewind watermark, and a scalar cull mirror that
dual-issues against the NEON parse -- so upstream's generic pointer-logic
optimisation is a variant of work already banked here, and their growth
restructure replaces per-buffer capacity with a single global value, which
the pooled draw-node model cannot express. Two of the four August commits
in that cluster repair regressions the July rewrite introduced, and the
third's genuine fix (staging arrays sized from an unrelated buffer) we had
already made independently.
Taken with adjustment:
- EATAN coefficients (aae9438f98). Upstream relabelled mVU_Globals so the
names match the powers; we had fixed the same defect by ordering the arm64
call sites by power instead. Both fixes are correct alone and CANCEL when
combined, so the arm64 call sites move to plain ascending order in the same
commit. The values never moved, so this emits an identical instruction
sequence. Their fix also repairs the x86 mVU we still carry.
- Shader cache version: upstream numbered their tfx.glsl change 109, which is
below our 110. Taking their value would hand every user a stale blob, so
this lands as 111.
- FullscreenUI: took the two readback-spin toggles, placed outside our
non-Apple guard rather than inside upstream's unguarded run.
- Restored tools/generate_fullscreen_ui_translation_strings.py, dropped by
431ca0c063, and regenerated both string areas. That also registers the Big
Picture setup-wizard strings, which had never been extractable.
GameDB: the three serials upstream gave gsHWFixes (SLES-53869, PAPX-90020,
SCPS-15064) are absent from the mobile overlay, so no fix is silently erased
on handhelds.
A blend mix hands the blend unit exactly one number - the alpha factor, on the
PS2's 0..2 scale where 128 is opaque. A second fragment output is the usual way
to carry a value on that scale, but it is not the only one: fixed-function
SRC_ALPHA reads the first output's alpha, and the shader can put the factor
there instead. Two cases make that free.
When the target holds its alpha double-scaled, the alpha the shader would write
IS the factor - tfx computes both as C.a/128 under RTA correction - so scaling
the target is the whole change. Otherwise the substitution is free whenever the
pass writes no alpha at all, because the output alpha is discarded on the way to
the target: a draw whose alpha is masked outright, or one whose alpha write has
moved into a second pass under SPLIT_RGB_ONLY.
Only the plain mix1 shape qualifies. The other mix cases rewrite the second
output's RGB independently of its alpha, so there the two outputs really do
carry different values and no substitution exists.
Without this, a GPU with no dual-source blend emulates the equation in the
shader, which needs a fresh destination read per primitive. With neither a
texture barrier nor a multidraw framebuffer copy available, all it gets is one
snapshot taken before the draw, so every primitive after the first composites
against stale pixels. That is what hollowed out God of War II's menu glyphs on
Mali r44p1, where the whole text is a single draw whose drop-shadow and bright
quads overlap each other 200 times.
Measured against a dual-source GPU rendering the same dump: over the text the
mean per-pixel error falls from 3.351 to 0.109 and the worst pixel from 163 to
25, with the lit-pixel count landing on 3896 against the reference's 3898.
Frame-wide it removes 25k of the 49k differing pixels and introduces 22. It
needs no barriers and no target copies at all, where matching this by refreshing
the snapshot per primitive group cost ~1000 render-pass breaks a frame and two
thirds of the frame rate on device.
No effect where dual-source blending exists: 33 frames across 11 dumps are byte
identical.
The mobile overlay layers onto bin/resources/GameIndex.yaml, and the loader
clears-then-replaces each map rather than merging: an entry that lists one
gsHWFix erases every other fix upstream sets for that serial. The file header
states the invariant - each entry must carry the complete block - but nothing
enforces it and nothing warns when it is broken. 115 serials were silently
dropping at least one upstream fix.
The bulk of it is one generation defect, not sync drift. Android used to carry
a forked copy of the GameDB; 54f0f8ba91 generated this overlay by diffing that
stale copy against bin and treating every difference as an intentional
override. Where the stale copy merely lacked a fix, the generator promoted the
absence into a deliberate-looking one, and the replace semantics then erased
the upstream value at runtime. Nothing upstream added after generation is
involved: every fix upstream sets today it already set on 2026-07-22.
Three changes here.
drawBuffering restored on 63 serials. All of them are entries the stale copy
also lacked, so the class has a single cause and no residue; before this,
exactly one overlay entry carried the key at all. It is a pure performance fix
lost on the tier that needs it most. Measured on NFS Underground 2 (SLUS-21065),
GS-dump replay on the SD865 at 2x, fan pinned, 3 interleaved reps per arm with
disjoint ranges:
draws/frame passes RT copies frame ms
as shipped 6250 2640 2630 29.1
+ drawBuffering 3559 1633 1623 18.9
-35% frame time, 1.55x, and visually free: deterministic in both arms with
0.12% of pixels differing by 2/255 or less.
Delta Force: Black Hawk Down (SLUS-21124, SLES-53299) restated complete. That
entry listed hwDownloadMode alone and thereby erased upstream's autoFlush,
halfPixelOffset, textureInsideRT and nativeScaling - the bloom, sky-bloom and
post-processing fixes. Both commit messages behind it describe only an
addition, and the entry is hand-appended above the sorted body, so this was an
accident rather than a decision. It keeps its out-of-sorted position; moving it
risks a future regeneration adding a second SLUS-21124 in the sorted slot.
Valkyrie Profile 2 (11 serials) keeps its configuration and regains the comment
explaining it. Upstream's halfPixelOffset:4 with nativeScaling:2 blows out the
render target when upscaling on Adreno and Mali; only nativeScaling:1 with
roundSprite:1 renders cleanly, so the drop is the point. The original entry
said so in an 8-line comment that 54f0f8ba91 stripped when it re-sorted the
file, which is why the entry has read as unexplained collateral since. The
intent was never lost, only the record of it - so the rationale now lives next
to the entry, where a regeneration cannot separate them, and it warns that an
audit will flag it.
57 entries still drop some other upstream fix and are deliberately untouched.
That residue is a mix of causes and needs per-fix judgement: the 7
preloadFrameData removals are the Rogue Galaxy see-through-wall fix and must
stay dropped, 10 more are advisory-only keys that change no setting, and the
cpuSpriteRender and minimumBlendingLevel drops would cost performance on this
exact tier if restored. drawBuffering was the one class safe to restore
wholesale.
Audit re-run clean: no serial drops drawBuffering, none sets it where upstream
does not, and no other fix class was touched.
Rogue Galaxy blocks the GS thread 7.5 ms every frame to read back sixty-four
pixels. It is an 8x8 patch of the depth buffer at a fixed screen position, read
once a frame -- a depth occlusion probe, the test a game does before deciding
whether to draw a lens flare. The cost is entirely GPU-fence synchronisation, so
it does not scale with the payload: the emulator submits, waits for the GPU to
finish, and reads 256 bytes.
The asynchronous download mode issues the same copy into a throwaway staging
texture and retires it at a later vsync, so the GS thread never waits. Measured
on the M2 by timing the readback directly rather than the frame, which keeps the
number clear of the gsrunner -perf perturbation: 7.2-7.8 ms becomes 0.005-0.008
ms. On a 45 ms frame that stall was around a fifth of the time.
It is not the blunt option. NoReadbacks and Unsynchronized, which this overlay
already ships for other titles, either skip the readback or race it. Async does
the real download and merely serves it a frame late, and it drops a late result
outright when an EE upload or a local-to-local move has since claimed those
pages, so stale data cannot overwrite newer contents.
Output is unchanged on every frame we can compare: four captured scenes, thirty
two colour frames, all pixel-identical, each scored against three baseline runs
of the unmodified binary first so run-to-run dump nondeterminism could not be
read as a result.
What that evidence cannot cover is motion. A frame-old occlusion probe differs
from a fresh one exactly when the probe's answer changes, which needs the camera
or the light to move, and every capture we hold is near-static. The visible
failure would be a lens flare blinking a frame late as it passes behind
scenery. That is the thing to watch for in play, and reverting is a one-line
change if anyone sees it.
Seven serials, every Rogue Galaxy entry the overlay carries, including the three
Japanese ones -- SCPS-15102, SCPS-17013 and SCPS-19254 are the same game under
its Japanese title.
OutRun 2006 is the only other title of the eight we profile that reads back at
all, three times a frame for 3.3-4.2 ms, and it is deliberately not included
here. Its readbacks are small colour buffers rather than depth, and there is an
open unexplained brightness bug in that game; feeding frame-old data into what
may be an adaptive-exposure loop is not something to do before understanding it.
Rogue Galaxy is the slowest title we track on handhelds and users report it as
such. Turning autoFlush off is the largest lever we have found for it: render
passes -38%, texture copies -74%. On the Adreno 610, which has no headroom, that
is -1.82 ms/frame and +2.6 fps. On the Adreno 650 it is -1.67 ms banked as
headroom, both arms already at 100% speed.
This is not free and should not be recorded as if it were. The software
renderer, an exact per-pixel GS model and an independent oracle here because
AutoFlushSW is a separate setting, scores level 0 3.4x further from truth than
level 2 on the contested pixels (mean error 9.436 vs 2.808; level 2 is closer on
17312 of 22424). What degrades is the light a lamp contributes to nearby lit
surfaces, so chests, blades and floors read slightly bright and warm. The glow
cones themselves are pixel-identical.
It is taken because the error is imperceptible in practice: bounded at 21-23/255
in all three captured scenes, diffuse rather than a missing object, and four
independent side-by-side looks at 1:1 failed to distinguish the two. Revert to
level 1 -- not 2 -- if anyone reports a regression: level 1 is pixel- and
cost-identical to 2 on Rogue Galaxy at 1x, 3x and 6x, and since 381bc41ded it is
also worth -5.8% of GS-thread cycles because it moves the game's non-sprite prims
onto the direct vertex kick. Level 2 buys nothing measurable over level 1 here.
Seven serials, which is every Rogue Galaxy entry the overlay carries. The Korean
release is SCKA-30005 and upstream gives it no gsHWFixes at all, so it is absent
here too rather than newly missed.
⚠ SLKA-25372 is Black, not Rogue Galaxy -- it is Criterion's Burnout engine,
which is why it carries OI_BurnoutGames. An earlier working copy had it in the
Rogue Galaxy set and flipped it to 0; it stays at 2.
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.
preloadFrameData primes every newly created render target from the game's
GS local memory. Where the frame's geometry doesn't cover the target, those
preloaded pixels stay visible: in the church interior the outdoor town shows
through the rear wall, washed out and semi-transparent. Bisected on a
Snapdragon 865 with the other six fixes held either way — the artifact
tracks preloadFrameData alone, and the three alignment fixes are innocent.
Upstream added it in f5570b7f40 next to roundSprite, and that commit
message justifies only roundSprite; the preload line came with no stated
reason beyond "Fixes corrupt textures especially on water". Dragon Quest
VIII and Dark Cloud 2 run the same Level-5 engine and share this title's
roundSprite, halfPixelOffset and nativeScaling values, but neither enables
the preload — both handle their water and sprite errors with CPU sprite
rendering. So if the water corruption does resurface, that targeted pair is
the replacement rather than this.
Dropped from all seven serials. The remaining six fixes are restated because
an override replaces the fixes map wholesale; names, compat and kozarovv's
out-of-bounds patches still inherit from bin.
Ported from EmuCoreX with sashkinbro's approval. The GPU family alone was
never enough to decide behaviour: the same Mali part behaves differently
under ARM's proprietary driver than under Mesa PanVK, a lesson this tree
learned twice the expensive way - the r44p1 DEVICE_LOST fix had to be
gated on driverID rather than vendorID, and the 8 Elite push-descriptor
disable likewise. Driver identity, version and a bug set are now recorded
in a table, so the next device quirk is an entry rather than another
bespoke branch. Twenty-five rules, with match confidence so a vendor-wide
rule never overrides a driver-version-specific one.
The shaders gain gpu_bitwise_and / gpu_bitwise_not / gpu_boolean_not,
whose bodies the device emits: plain a & b normally, scalarized where the
database says the driver miscompiles vector bitwise ops. A device matching
no rule renders exactly as before.
opengl/tfx_fs.glsl already carried this fix as IAND3/UAND2/UAND4 macros
gated on GPU_PROFILE_MALI. Those are replaced by the shared helpers, but
the GL macro is deliberately (workaround || IsMaliGPUProfile()): Mali
reached through ANGLE or Panfrost resolves a non-ARM driver and matches no
rule, so a database-only gate would have silently removed a fix those
users have today. Widening only.
SHADER_CACHE_VERSION 108 -> 109. Every TFX and convert shader's source
text changed, so a blob cached from 108 no longer matches the source that
produced it; without the bump users would get stale binaries and garbage
rendering after updating.
RewriteConstantLoads is deliberately absent - the database records
BrokenConstantLoad with no workaround bits, so the macro would be
permanently zero and the shader code dead.
Shaders validated offline with glslc across 136 Vulkan and 296 GL/GLES
permutations, every macro on and off; the failure set is byte-identical to
the pre-change baseline.
Three follow-ups from the graphics live-apply work.
The two iOS guard tests had been failing with FileNotFoundError since the core
moved out of platforms/ios -- both still pointed at app/src/main/cpp/pcsx2 and
app/src/main/assets. Dead tests read as coverage, so repoint them.
Repointing the True Crime one turned up a real regression. It guards
"mvuFlag: 0 # Fixes texture flickering caused by the VU Flag Hack" on the six
New York City serials, and that fix is gone: the iOS build used to bundle its own
GameIndex.yaml carrying it, and when we converged on the overlay only the
gameFixes came across. The speedHack has been missing ever since, unnoticed
because the test that would have caught it broke in the same move. Restored in
the overlay, which is where our GameDB changes live now, and the test follows it
there.
The renderer picker stayed live with a game loaded. Returning to the menu only
pauses the VM, so you could switch renderer, change any other graphics setting,
and have the next apply take GSUpdateConfig down the reopen path -- a full Metal
teardown under a loaded game. The picker already said "Requires restart"; now it
means it.
applyOsdPreset and setPerformanceOverlayVisible wrote GSConfig and EmuConfig.GS
from the UI thread. Those flags are bitfield members sharing storage with the GS
device-restart flags, so a read-modify-write off-thread can write back a stale
neighbour and flip RestartOptionsAreEqual -- the same teardown, from a stats
toggle. Both compute their values up front and hand the writes to the CPU thread.
isPerformanceOverlayVisible reads the INI now rather than GSConfig, which the
setter no longer updates synchronously.
Render-pass coalescing (Brian Degenhardt) carries the cost the readback
workaround was compensating for, so drop hwDownloadMode from all six regional
serials and let them run with readbacks on. coalesceRenderPasses stays.
Dirge alternates 1:1 between a colour target and a mask target for most of the
frame, which is the exact pattern the scheduler exists for. On a four-frame
capture it takes the frame from 8035 render passes to 523, with every frame hash
identical to the unscheduled path, and on an Adreno 650 handheld the difference
is plainly visible.
All six regional serials get it. This goes in the mobile overlay rather than the
canonical GameIndex, because it is worth nothing on a desktop GPU where a pass
boundary is cheap - the overlay ships on Android, iOS and ARM64 Linux handhelds,
which is exactly the set of targets that pay for tile load and store.
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).
The mobile GameDB base predated upstream PCSX2's addition of InstantDMAHack
("Stops hang when entering Sword Select screen") to Samurai: Complete Edition
and never resynced, so the Android/iOS copies carried EETimingHack alone. The
generated overlay faithfully reproduced that stale state, which the override
loader's clear-then-replace on gameFixes would re-inflict on mobile/ARM64-Linux.
The drop was never intentional (no commit in the Android line ever touched this
entry's fixes; the fix is an arch-neutral DMA-timing hang fix). Remove the
overlay entry so the mobile tiers inherit bin's full [EETimingHack,
InstantDMAHack].
The Android shader tree carried capability-gated blocks that bin was missing,
even though the shared C++ backends emit every gating macro on all platforms
(GSDeviceOGL.cpp:2006-2010, GSDeviceVK.cpp:4769 — GPU_PROFILE_MALI,
HAS_ARM/EXT_SHADER_FRAMEBUFFER_FETCH, HAS_ARM_DEPTH_FETCH, HAS_CLIP_CONTROL,
DISABLE_DUAL_SOURCE). bin was simply the stale copy; the desktop GL backend is
already wired to consume the mobile shader structure (see the GPU_PROFILE_MALI
reference at GSDeviceOGL.cpp:1067). Collapse the two to one superset so drift
becomes impossible.
- opengl/{convert,interlace,present,tfx_fs,tfx_vgs}.glsl, vulkan/present.glsl:
adopt the Android version verbatim. Every difference is either macro-gated
(desktop path preserved in the #else — e.g. the tfx_vgs z-remap lives under
#else of #if HAS_CLIP_CONTROL, dead on desktop which has ARB_clip_control) or
a GLES-strict float/cast tweak that is behaviorally identical on desktop.
- vulkan/tfx.glsl: hand-merged. Union in the additive tiler blocks (Mali
EQUAL_WZ z-nudge on both the direct and VS_EXPAND paths, the
VS_PROVOKING_VERTEX_LAST and !VS_IIP provoking-vertex handling) and drop
layout(depth_less) per its float32-rounding correctness rationale, while
KEEPING bin's newer two-flag ROV implementation (rov_discard_color/depth) —
that path is dead on mobile since ROV is now off on all tilers. Preprocessor
structure verified balanced and identical to Android's directive counts.
dx11/tfx.fx is Windows-only (never compiled on mobile) and the two remaining
vulkan files differ only in trailing whitespace, so they stay as-is; the mobile
copies get removed and generated from bin in the build-plumbing step.