Commit Graph
24910 Commits
Author SHA1 Message Date
Brian Degenhardt bbda693a7f Android: let analog triggers be bound, and read the left one everywhere (#584)
Reported on an Xbox One controller: every control on the pad binds except
the triggers, which do nothing at the "press a button" prompt.

The binding model is keyed on Android keycodes, and most pads — an Xbox
controller among them — report their triggers ONLY as analog axes, never as
KEYCODE_BUTTON_L2/R2. The capture path already bridges motion to key for the
HAT and for stick deflection; triggers were simply never added, and since
that path consumes the motion event the press vanished without a trace. Pads
whose triggers do send key events were unaffected, which is why this only
surfaced now.

A pulled trigger now stands in for the keycode a key-emitting pad would
send, so it is an ordinary button to everything downstream: bindable to any
PS2 control, stealable by another row, assignable as a hotkey or a macro,
usable as a combo member. Gameplay resolves that same keycode back through
the binding table, so what the capture records is what gets honoured — the
two now share one axis resolver rather than each knowing its own list.

That makes trigger-bound hotkeys and macros reachable from the Hotkeys and
Pad tabs, so the gameplay side has to be able to fire them, or binding one
would be a dead end. Both act on the press and the release, which lets a
trigger drive the hold-type hotkeys (fast-forward, pressure modifier, gyro
hold) that a stick edge cannot.

Second fix, same area: the right trigger has a per-device fallback axis for
pads that report it on AXIS_RZ, and the left had none. A pad Android has no
vendor key layout for passes raw HID through, putting the triggers on plain
Z and RZ — so on those devices the right trigger worked and the LEFT ONE WAS
READ BY NOTHING, dead in gameplay rather than merely unbindable. Both sides
now take the fallback, gated on a 0..1 range so a stick axis (-1..1) can
never be mistaken for a trigger.
2026-08-14 21:50:01 -07:00
Brian Degenhardt c8b51438cc IPU: dither a whole row per deinterleaving load
ipu_dither has had an SSE2 path and a scalar reference since forever, and
arm64 took the reference. The compiler closes half of that gap on its own —
with dithering off the loop is simple enough that clang vectorises it, and
measured here the scalar and NEON versions come out cycle-identical. With
dithering on it closes none of it: the clamp is written as std::max/std::min
around a table lookup, the destination is a 5/5/5/1 bitfield, and between
them the vectoriser gives up entirely. That arm ran at about 36 instructions
per pixel.

The NEON version is not a transliteration of the SSE2 one. x86 needs six
unpacks to split a row into channels because it has no deinterleaving load;
NEON has VLD4, so a whole 16-pixel row arrives already split one register per
channel and the shuffle chain simply does not exist. The dither tables are
the reference's coefficients with the sign folded into the choice of
operation, which lets saturating byte arithmetic supply the clamp for free —
the same trick the SSE2 path uses, and the reason both agree with the
reference bit for bit.

Measured on an M2 Max P-core, 2M macroblocks, two runs each:

  dither on   reference  18.76G instructions / 3.372G cycles
              NEON        1.29G instructions / 0.293G cycles   (11.5x)
  dither off  reference   1.08G instructions / 0.247G cycles
              NEON        1.13G instructions / 0.247G cycles   (even)

Function size drops from 476 to 208 bytes.

The tests are the point of the commit as much as the code is. Three
implementations of one function existed and nothing had ever compared them,
which is a bad shape here: a wrong result does not crash, it tints an FMV,
and nobody reports that. The transform depends on nothing but a pixel's four
bytes and its position modulo four in each axis, so the suite sweeps every
byte value through every one of the sixteen dither cells rather than
sampling. It holds whichever path the host selected to the reference, so it
gates the SSE2 arm on x86 exactly as it gates NEON here.

Proven to discriminate by mutation: transposing the r and b channels fails
three of four cases (correctly not the sweep that holds the channels equal),
perturbing one dither cell by one fails two, and dropping saturation fails
all four.

ipu_dither_reference loses its __ri so that a symbol survives into Release
for the tests to call.
2026-08-14 21:25:30 -07:00
Brian Degenhardt a3bf73bf7a GS: AArch64 has no slow unaligned load to compile around
FAST_UNALIGNED was defined only inside the ARCH_X86 arm, where it records
that AVX-and-later cores stopped punishing unaligned vector loads. On ARM64
the macro was therefore undefined, which the preprocessor reads as zero, so
every arm64 build compiled the texture-upload path as though the punishment
existed.

It never did. LDR Q and LD1 take any address, and GSVector4i's load template
ignores its own `aligned` parameter and emits the same instruction either
way. So the callers were paying for a distinction with no machine behind it:
WriteImage tests the source address and the pitch on every call to choose
between three template instantiations of WriteImageBlock and
WriteImageColumn that, for 8- and 4-bit columns, compile to identical code.
For 32- and 16-bit columns the unaligned arm is not identical, but it is the
worse one — eight combining 64-bit loads instead of four 128-bit loads and a
swizzle.

Defining it collapses all of that. GSLocalMemoryMultiISA.cpp.o goes from
80,368 to 62,184 bytes of .text and from 58 emitted functions to 32, which
is what an I-cache on a handheld cares about. Only GSBlock.h and
GSLocalMemoryMultiISA.cpp read the macro, so nothing else moves.

The retained load strategy is not new code: whenever an upload happened to
land 32-byte aligned, arm64 already ran exactly this sequence. What goes
away is the arm that only ever ran when it did not.
2026-08-14 21:25:14 -07:00
Brian Degenhardt 6aa2fd79d9 Android: a Game-scope save must still write the process-wide fields
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.
2026-08-14 11:45:40 -07:00
Brian Degenhardt 58b6dd983c Core: apply a PINE toggle when it is toggled, not at the next game change
ReloadPINE() had two callers -- CPUThreadInitialize() and UpdateDiscDetails().
Neither is on the settings path, so turning PINE on did nothing until the next
app start or the next game boot. The switch stays on in the UI, because that
part persists correctly; it is only the server that never appears. Reported on
Android, where it is worst -- a handheld has no second window to restart into,
so there is nothing to reveal that the setting did land and simply was not acted
on -- but nothing about the gap is Android-specific. The desktop Big Picture
toggle in FullscreenUI_Settings has the same two-call-site problem behind it.

Discord presence is the same shape of feature: an optional external service
whose whole lifecycle is one bool, toggled from the same settings pages. It is
handled in CheckForMiscConfigChanges, three lines from where PINE was missing.
So put PINE beside it, which is also what the Android settings layer already
documents as the contract ("a commit is enough -- no game restart").

Called unconditionally rather than gated on old_config, because ReloadPINE()
already compares the request against the LIVE server -- whether one is
initialized and on which slot -- and that is strictly stronger than a config
diff. It early-returns when the two agree, so the common commit costs one
comparison, and it recovers a server whose bind failed earlier rather than
trusting a config value that never changed. The port sitting in TIME_WAIT after
a fast restart is the usual way a bind is lost, and it is exactly the case a
config diff cannot see.

CheckForMiscConfigChanges runs from ApplySettings/ApplyCoreSettings, which
assert the CPU thread -- the same thread CPUThreadInitialize already reloads
PINE from, so this adds no new threading contract. The iOS emulation-only path
is unaffected: ReleaseNonEssentialRuntimeResources runs after
CheckForConfigChanges and calls PINEServer::Deinitialize() itself, so a release
still ends with the server down.
2026-08-14 11:45:39 -07:00
Brian Degenhardt 0b9e9cdfcb GS/SW: the C++ rasteriser packs a colour gradient like the generators do
The per-lane colour offsets were packed with the signed saturating pack while
both code generators used the unsigned one. The mask above the pack has already
put every lane in 0..65535, which makes the unsigned pack the identity and makes
the signed pack flatten everything from 32768 up to 32767. A descending gouraud
gradient is how a lane gets there: its offset is negative, the mask turns it
into a large positive, and the pack saturates it. Every pixel of the group then
carries that instead of its own colour, for the whole scanline.

The mask and the unsigned pack were introduced together to fix exactly this, in
"GS/SW: Mask color gradients to prevent incorrect clamping"; a later refactor
that rewrote the same lines to change how the shift table is loaded retyped the
tail back to the signed pack. The generators were not part of that refactor,
which is why only the C++ path regressed and why nothing noticed.

Where the path is reachable, measured rather than argued: with the rasteriser
JIT on, a probe at the top of the C++ setup never fires across corpus replays
that generate tens of kilobytes of scanline code apiece. It is entered only when
there is no code memory to compile into at all, and that same condition turns off
the EE, IOP and VU recompilers, so it is not a configuration anyone plays in.

What it is, is the path a measurement runs under -- the only way to ask what the
renderer computes without a JIT in the way, and so the arbiter of a
generated-code question. It was about to arbitrate one, and would have lied: the
gs-shade console capture re-run under it differed from the generated arm in
42,240 bytes, concentrated in exactly the gouraud colour it was to be asked
about. It is now byte-identical, and the generated arm is byte-identical to
before the change, so nothing a shipping build renders moves.

The new suite runs both paths over the same spans and compares the setup state
and the stored pixels, so the next divergence anywhere in the scanline fails
loudly instead of waiting for a capture to find it.
2026-08-14 06:33:13 -07:00
Brian Degenhardt 02e93048d4 IOP: let an immediate jump to zero reach the handler we already wrote
The recompiler already has a policy for arriving at address zero. A fetch
at PC=0 raises an Address Error and the BIOS handler takes over (AX-11),
because PS1 mode drives the IOP there through a register jump often
enough to be worth modelling rather than asserting on.

The immediate form of the same event never got there. Emitting a jump
whose target is zero asserted instead, so the two ways of reaching the
same address behaved differently: through a register it is emulated and
the guest carries on, through `j 0` it aborts a Devel build one
instruction before the handler would have seen it. Dropping the assert
routes the immediate form into the existing path — the tail stores pc,
links the block at zero, and the dispatcher hands it to psxRecompile,
which raises the Address Error.

Unlike the EE, nothing here compiles a jump the guest does not take: the
IOP scanner ends every block at the first branch, so an unresolved weak
symbol's guarded `jal 0` is never emitted. Reaching this needs the guest
to genuinely jump to zero — an unguarded weak call, a branch target that
computes to zero in low RAM, or a corrupted code word.

The test runs the JIT arm alone, which is what the new harness mode is
for: the interpreter has no PC=0 model at all, so the arms are meant to
disagree here and the differential harness has nothing to say.
2026-08-13 23:01:02 -07:00
Brian Degenhardt cb56a72b26 EE: a jump to address zero is a target, not an impossibility
A call to an unresolved weak symbol links as `jal 0`, guarded by a null
test on the symbol's address that always skips it. PS2SDK's libc glue
ships four such sites, so every homebrew ELF built against it carries the
shape, and the recompiler asserted the moment it met one.

It meets one because SL-03 continuation compiles the skipped path: the
guard branch becomes a continuation site, the scan runs on through the
dead call, and the emitter is handed a zero target for code that never
executes. The assert (inherited from the x86 recompiler, which aborts on
the same ELF) then takes down any Devel build before the program starts.

Nothing needs to happen at that target. If something did jump there,
address zero resolves like every other address — a block in RAM page 0,
or the unmapped-page handler — so the three tails just emit it.

The shape only reaches the emitter when the guard cannot be resolved at
compile time; a constant address folds the branch and the dead call is
never emitted, which is why an ELF carrying it can run clean until one
block boundary lands between the address materialization and the test.
The tests pin the reachable half.
2026-08-13 22:23:14 -07:00
John Peter Sa 3929e78dc2 Complete Android Brazilian Portuguese translation Android (#578)
* Complete Android Brazilian Portuguese translation
2026-08-12 14:53:44 -07:00
Brian Degenhardt 85adcfe6df Merge branch 'upstream-sync-2026-08' 2026-08-12 12:22:45 -07:00
Brian Degenhardt 2d73c39f03 GS: lift the r44p1 GL fetch blocklist -- the field chose the fast path
Delete gl-arm-r44p1-attachment-self-read from the driver-bug database, so
r44p1 Mali takes GL_ARM_shader_framebuffer_fetch again on GLES and -- because
GSUtil::AndroidAutoPrefersVulkan asks the same table -- Auto resolves back to
OpenGL on those devices.

The rule was correct about the defect and wrong about the trade. Through
2.6.6.4 the gate it formalised was inert: the Mali profile block re-enabled
the ARM backend moments after the gate disabled it, so every r44p1 device
shipped on GL + fetch. 2.6.6.5 made the gate actually engage, and on GLES --
where fetch and the texture barrier are one capability -- every
self-referential draw became an RT copy plus a tile flush. Shadow of the
Colossus fell 30 -> 7 fps on the Anbernic RG 477V and users mass-downgraded
to 2.6.6.4. Offline replay of that scene under the device's feature shape
shows why no smaller fix could win the speed back: 890 render-target copies
and 938 render-pass breaks a frame against 1664 draws -- and a 2.6.6.4
replay under the same shape produces the same ledger (901/948/1664), so the
old build's speed WAS the in-tile read, not better GS decisions.

The known cost is unchanged from 2.6.6.4: r44p1's fetch corrupts some
content (MGS3 observed; most likely the driver grants the tile-read slot per
attachment format and silently degrades denied reads to memory fetches
inside a live feedback loop). Vulkan stays available as the
correct-rendering choice for those games, and its own r44p1 rule is
untouched -- there the in-tile read is a device loss, and the RT copy is an
ordinary image copy rather than a tile flush.

Unlike 2.6.6.4, the restored path is ordering-correct: db41082150 taught the
barrier-drop logic that ARM's fetch orders overlapping primitives by spec.

gs_vertex_tests 64/64, with the driver-profile pins flipped to assert the
restoration on GL and the copy path on Vulkan.
2.6.6.6
2026-08-12 10:57:40 -07:00
Brian Degenhardt b2e22efc45 Android: send Auto to Vulkan where GL cannot read the target in-tile
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.
2026-08-12 00:09:08 -07:00
Brian Degenhardt db41082150 GS/OpenGL: ARM framebuffer fetch does order overlapping primitives
d8e2741234 stopped dropping the full barrier under framebuffer fetch when a
draw's primitives overlap, because fetch replaces the destination read without
necessarily ordering fragments. That is true of the EXT extension and it is
where the defect was measured -- Mesa 25.3.6 / Apple M2, 18% of an MGS3 frame
changing between identical replays. The conclusion was then written as
framebuffer_fetch_orders_overlap = false for the whole GL backend.

ARM_shader_framebuffer_fetch guarantees the opposite. Its spec: "when an
individual sample is covered by multiple primitives, rendering for that sample
is performed sequentially in the order in which the primitives were submitted",
and a read of gl_LastFragColorARM "must wait for the processing of all previous
fragments destined for the current pixel to complete". That is the same contract
Vulkan's rasterization-order attachment access and Metal's programmable blending
provide, and both of those keep the barrier-free path.

Every Mali device on Android takes the ARM path -- it is the only one that
works there, which is why the Mali profile selects it even when EXT is also
advertised. So the blanket answer put the entire Mali install base on a split
draw, one draw call per primitive group, for every overlapping blended draw.
That is a large part of what made 2.6.6.5 slower than 2.6.6.4 on Mali, which is
the population that reported it.

So make the question per-extension, where it belongs: ARM orders, EXT does not.
A driver that violates the ARM guarantee is a driver bug and goes in the
driver-bug database as a fetch blocklist entry, which is the mechanism r44p1
already uses -- not a blanket rule that also penalises every healthy Mali.

Vulkan, Metal and the EXT path are all unchanged. 21/21 policy tests.
2026-08-11 23:41:16 -07:00
Brian Degenhardt 3e56da7f86 Merge upstream PCSX2 (2026-07-15 .. 2026-08-10)
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.
2026-08-10 18:24:24 -07:00
PCSX2 Bot 2cf8dabe6b [ci skip] PAD: Update to latest controller database. 2026-08-10 12:35:19 -04:00
Brian Degenhardt e509b17e7a Merge pull request #565 from pstef/tests
Assorted improvements
2026-08-09 19:48:45 -07:00
jpolo1224 dfb4263926 Android: regenerate the PGO profile against this tree, and let generate mode run
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.
2.6.6.5
2026-08-09 21:53:50 -04:00
Brian Degenhardt e9f8f83669 GS/HW: carry the blend-mix factor in the output alpha without dual-source blend
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.
2026-08-09 17:49:51 -07:00
Brian Degenhardt 89e51d93a1 GS/HW: split RGB_ONLY alpha test by channel without dual-source blend
AFAIL=RGB_ONLY means every fragment writes RGB and only the ones passing the
alpha test write A and Z. The accurate single-pass form of that carries the
pass/fail decision in the second blend source, so it needs a hardware
dual-source blend unit. Mali Vulkan stacks routinely report dualSrcBlend=false,
and there the draw fell back to pass/fail: one pass for the passing fragments,
another for the failing ones.

Pass/fail splits the draw by *fragment*, which puts RGB in both passes. Where
the primitives overlap each other, the two passes then composite out of order -
every failing fragment of the whole draw lands after every passing one, rather
than each primitive completing before the next begins.

Splitting by *channel* instead is exact and costs the same two passes: run one
pass with the alpha test off writing RGB, then one with the test on writing A
and Z. Both passes see the primitives in order, so overlap stops mattering.

Forced on over a dual-source GPU it reproduces the single-pass path byte for
byte - 33 frames across 11 dumps, no differing pixels. Against that reference
on a no-dual-source configuration it takes God of War II's pause menu from
2.064 to 0.893 mean per-pixel error.
2026-08-09 17:49:51 -07:00
Brian Degenhardt 9d7f8c2376 Translations: restore the pt-BR plural forms for the save-state delete count
The Brazilian Portuguese update flattened "%n save states deleted." into a
single string, but the message is declared numerus="yes", so its translation
may only hold <numerusform> children — one per plural form of the language.
Bare text there is a hard lrelease error, which stopped ninja and took down
every Qt desktop build (Linux 4k/16k, macOS, Windows); Android and iOS pass
only because they never run lrelease.

Give the message back its singular and plural forms. All translation files
now release clean.
2026-08-09 16:11:32 -07:00
lightningterror 8c1bb5742e GS: Fix unused variable warning. 2026-08-10 00:45:08 +02:00
Brian Degenhardt ebc4ee75f3 Merge pull request #563 from johnpetersa19/master
Complete Brazilian Portuguese graphics translations
2026-08-09 15:38:38 -07:00
Brian Degenhardt 0daaf5a6f7 GameDB overlay: stop erasing upstream fixes the overlay never meant to drop
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.
2026-08-09 15:13:46 -07:00
Brian Degenhardt ce3eac044e GS: stop taking a voluntary RT feedback read where it costs a render pass
An Ad blend with alpha writes masked can be substituted (Ad -> As) and run in
hardware if the draw reads the render target. The draw did not otherwise need
that read, so the substitution is only worth taking where reading is free.

The gate for "free" was !texture_barrier, written to mean D3D11, where the
fallback is a plain copy on an API with no render passes. It is equally true of
every driver carrying UseRenderTargetCopyForFeedback, where the fallback is a
per-draw copy bracketed by a render-pass break - the most expensive feedback
read we have. Widening that workaround to all of Adreno therefore handed those
drivers the whole optimization in its worst form, on thousands of draws that
never needed to read anything. This is the same regression fixed for the
framebuffer-fetch path in ec57f7f1c6, arriving by the other term.

Replayed on the same dumps and binaries, draws whose shader reads the render
target, per frame:

                barriers on   barriers off   with this change
  NFS U'ground           14            610                  1
  FlatOut 2             n/a            448                 17

Ask for the property being asserted instead. cheap_rt_feedback_read is set by
D3D11, and by Metal when programmable blending is available - a feedback draw
there binds the target and stays in the same render pass. Vulkan's
ordered-attachment-access spelling does not qualify: the loop is declared
through the pass configuration, so toggling it ends the pass.

Cost on the SD865 (Adreno 650, turnip, fan and governors pinned, median frame
time over 3 runs of 20 loops, 3x upscale):

                shipped   OverrideTextureBarriers=1   this change
  NFS U'ground  17.15 ms      12.86 ms (1.33x)     12.00 ms (1.43x)
  FlatOut 2     22.84 ms      21.93 ms (1.04x)     17.91 ms (1.28x)
  Katamari       1.42 ms       1.29 ms (1.10x)      1.40 ms (1.02x)

Katamari is the control: it has no Ad-masked draws, its population is unchanged
(49 -> 50 copies per frame) and so is its frame time. Render passes per frame on
NFSU go 390 -> 47 and copies 347 -> 4.

Correctness is unchanged, and specifically the workaround still applies wherever
it did. Scored per-pixel against the software rasteriser over frames verified
stable across runs, this change renders Tales of the Abyss and God of War II
byte-for-byte identically to the texture-barrier path - same tiers, same
worst-case pixel - and leaves OutRun 2006 and Katamari untouched. The Abyss
title screen text, the defect the workaround exists for, is unaffected. Ad
blends that genuinely need software blending are still forced into it by
blend_requires_barrier.
2026-08-09 14:59:55 -07:00
Brian Degenhardt b5415c8105 GS: stop a screenshot ending a GS dump that is already recording
A snapshot request and a running recording shared one frame counter. The
screenshot hotkey asks for zero dump frames, so pressing it mid-recording
zeroed the budget of the dump in progress and the next VSync closed it as
though the user had asked it to stop. A single-frame dump request did the
same thing one frame later. Both were silent; the file simply ended early.

Two fields now, so a request cannot reach into a recording at all: one for
what the queued request asked for, one for what the open dump still owes,
written only when that dump is created.

The two branches were also alternatives rather than independent, so the
frame a screenshot landed on never reached the dump and two guest frames
merged into one on replay. A recording now takes every frame it is open
for -- except the one it was opened on, whose state went into the dump's
header and whose replay therefore starts from the frame after.

A dump request arriving while one records still cannot open a second dump,
but it says so on the OSD instead of quietly writing only the screenshot.

The decision is extracted to a header-only policy with the usual
static_asserts, pinned by eight cases riding the GS test target. The
truncation is reachable only from the hotkeys and the Big Picture button --
PINE's dump opcode was written to refuse rather than trip over it -- so the
policy suite is the regression gate. Its refusal comment is updated: it now
rests on not handing back a path for a file that will never appear, which
was always the better half of the argument.
2026-08-09 14:39:29 -07:00