mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
bbda693a7fd39e0d72a7dfdf5cdc392655917210
24910
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
3929e78dc2 |
Complete Android Brazilian Portuguese translation Android (#578)
* Complete Android Brazilian Portuguese translation |
||
|
|
85adcfe6df | Merge branch 'upstream-sync-2026-08' | ||
|
|
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:
2.6.6.6
|
||
|
|
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. |
||
|
|
db41082150 |
GS/OpenGL: ARM framebuffer fetch does order overlapping primitives
|
||
|
|
3e56da7f86 |
Merge upstream PCSX2 (2026-07-15 .. 2026-08-10)
71 commits from |
||
|
|
2cf8dabe6b | [ci skip] PAD: Update to latest controller database. | ||
|
|
e509b17e7a |
Merge pull request #565 from pstef/tests
Assorted improvements |
||
|
|
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
2.6.6.5
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
8c1bb5742e | GS: Fix unused variable warning. | ||
|
|
ebc4ee75f3 |
Merge pull request #563 from johnpetersa19/master
Complete Brazilian Portuguese graphics translations |
||
|
|
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; |
||
|
|
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
|
||
|
|
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. |