mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
bc268a7e8392cc6ad739dda0eba719e83979d3d6
24923
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bc268a7e83 |
GameDB: Add Prioritize Lower depth fix to Steambot Chronicles
(cherry picked from commit cde2448a6cd0d27f6651c59b43102d27dcb45e48) |
||
|
|
34333fe6bd |
GS/HW: Avoid detecting shuffles as source of truth for target format
(cherry picked from commit ded263f98aa2862b56439ea3c0b552748e4e64f0) |
||
|
|
0d558ce69f |
GS/TC: Improve single pixel overlap detection in RT in RT
(cherry picked from commit ffa569065638312d9c01a47f760130a2109ce7cc) |
||
|
|
ae4da6a633 |
EE/IOP: Fix loading elf's through relative path
(cherry picked from commit ca03806a4c86e8fbfc948fcb110f3961be10e5b2) |
||
|
|
eeb3affb13 |
GS: FidelityFX Super Resolution 1 as an output-scaling mode
Adds FSR1 (EASU upscale + RCAS sharpen, two compute passes) to the Vulkan backend, so a game rendered below display size can be upscaled properly instead of bilinear-stretched at present. Slots in beside the existing MetalFX branch in GSRenderer rather than introducing a parallel abstraction: GSUpscaler and the non-pure DoXxx virtuals already occupy that design space, and OpenGL and Metal inherit a false return and need no change. FSR1 is MIT (AMD, 2021) and the tree already ships ffx_a.h and ffx_cas.h under the identical grant, so the headers are vendored verbatim with their licence blocks intact. ★ ffx_a.h is NOT replaced. FSR1 wants the 2021 header, ours is 2019, and the 2019 one has been locally patched for Metal Shading Language (A16, A_MSL, A_MAYBE_UNUSED) with ffx_cas.h depending on those. Swapping it would break the Metal backend. The 2021 copy ships alongside as ffx_a_fsr1.h, used only for GPU-side string substitution. The CPU-side FsrEasuConOffset/FsrRcasCon compile against the existing 2019 header — verified by compiling a probe, not by grepping, because AU1_AF1 and AU1_AH2_AF2 are functions and a grep for a #define reports a false negative. Two shader modules, not two specializations. FSR_EASU_F and FSR_RCAS_F are preprocessor gates deciding which function bodies ffx_fsr1.h emits at all, and specialization constants resolve after preprocessing, so CAS's constant_id trick would produce a shader calling undefined functions. Confirmed distinct: disassembly shows EASU with three OpImageGather and RCAS with none. Both passes push the full 80-byte constant block. With all five uvec4 declared so one layout serves both, Sample decorates to byte offset 64 — pushing the 32 bytes RCAS nominally needs would leave it undefined, and Sample gates a gamma-squaring branch, so garbage there squares the image. Binding 0 is a combined image sampler, unlike CAS's plain sampled image, because EASU uses textureGather. The EASU intermediate stays in GENERAL with explicit compute-to-compute barriers. Layout::ShaderReadOnly targets the FRAGMENT stage and TransitionToLayout early-outs when the layout already matches, so neither of the usual tools makes a compute write visible to a compute read. The barrier also covers frame N+1's EASU write against frame N's RCAS read, since the image is parked across frames. FSR and CAS are alternatives, not a chain: RCAS is itself a sharpener. Selecting FSR hides the CAS rows. Pipeline compilation failure is non-fatal and leaves Features().fsr1 false, matching the CAS path that exists because of an Adreno 650 crash. GSUpscaler::FSR1 is appended, not inserted, since the enum is persisted as an integer. Android clamps to the enum's own maximum rather than the count of options its picker shows — clamping to the picker would have rewritten FSR1 back to Off on every save, because MetalFX occupies value 1 and is never displayed. Verified: build clean, no C++ or Kotlin errors; all three resource files packaged into the APK; FSR code present in the core. NOT verified: anything on a GPU. No visual check, no perf numbers, and in particular no confirmation that textureGather in a compute shader works on the Adreno drivers this targets. |
||
|
|
dfd92a4f31 |
LSFG: persist settings and shaders, report status, add flow scale and 3.1p
Five changes, one of which is a plain bug in what shipped. ★ LSFG settings were never persisted. lsfgEnabled, lsfgMultiplier and lsfgDllPath were absent from toJson/fromJson, and that pair IS the persistence format — ConfigStore stores toJson().toString(). So every choice, including the Lossless.dll the user went and found, was discarded on restart. Added to the round-trip, the per-game override diff/merge, and gsDiffersFrom. Translated SPIR-V is now cached to disk. Extraction used to keep raw DXBC and translate inside the shader callback, so all 26 translations re-ran on the GS thread inside EndPresent after every enable, resize or multiplier change. Now ExtractShaders translates eagerly, drops the DXBC, and writes <cache>/lsfg_shaders.bin. The DLL's size and mtime go in the header and a mismatch re-extracts — ARMSX3's equivalent has no invalidation at all. Frame generation can no longer fail invisibly. GetStatusText() feeds one line to the performance overlay, empty ONLY when the user has not enabled it: unavailable / failed / no shaders / starting / a display rate. That rate counts frames actually PRESENTED, real plus generated, because the acquire loop can break early and assuming the multiplier would overstate it. FPS alone cannot show this — frame generation deliberately does not change the emulator's frame rate, so without a separate line 'working', 'broken' and 'unsupported' are all the same absent line. Flow scale and the 3.1p pipeline are exposed. flowScale is a DIVISOR — framegen computes flowExtent = inputExtent / flowScale — so the UI percentage is passed as clamp(100/percent, 1, 4). ARMSX3 passes percent/100, where only the default is right because 1.0 is its own reciprocal and every lower position makes it slower; that inversion is not copied here. 3.1p is a separate shader family with separate device state, so the shim fixes the choice at initialise and dispatches every entry point on it, and the name table gains the p_* resource IDs. Frames the game did not draw are no longer interpolated. PresentWithGeneration captured unconditionally, so pause menus and boot screens got interpolated at full GPU cost. It now takes frame_has_new_content, sourced from the condition GSRenderer already computes (current && !blank_frame) rather than a new heuristic, and consumed with std::exchange because RenderBlankFrame presents without going through BeginPresent. A false also resets the frame history, so the pair either side of a gap is never stitched into one bogus in-between frame. Verified in the built APK: four status states and lsfg_shaders.bin in the core, 26 p_* names in its table, 3.1p linked into the shim. Build clean, no C++ or Kotlin errors. NOT verified: any of it at runtime — no Adreno 7xx here, so the flow-scale direction, the cache round-trip and the capture gate are reasoned, not observed. |
||
|
|
7fde8ce980 | Merge remote-tracking branch 'origin/master' into jit-android-catchup-gv7 | ||
|
|
c66f28a395 |
LSFG: fix binary semaphore misuse and the unbounded acquire
Three defects in the frame-generation present path, found comparing it against ARMSX3's. 1. Binary semaphores signalled without being waited, and waited twice. The loop reassigned the real present's wait to the last post-copy semaphore. That left s_pre_copy_sem signalled and never waited, so the next frame signalled an already-signalled binary semaphore; and it made the last post-copy semaphore the target of two waits, its own present and the real one, when a binary semaphore's signal can be consumed exactly once. Both are spec violations, and the kind that work until a driver decides otherwise. The fix is to delete the reassignment, not to add semaphores. The real present must wait on s_pre_copy_sem specifically, because the pre-copy reads the real image as TRANSFER_SRC and returns it to PRESENT_SRC — presenting before that lands would present an image still being read. The generated presents are independent: different swapchain images, each gated by its own post-copy. Presents issued on one queue are processed in call order, which is what keeps them on screen ahead of the real frame. Every semaphore is now signalled once and waited once. 2. vkAcquireNextImageKHR with UINT64_MAX, on the present path. Two problems at once. It bypassed the bounded ACQUIRE_TIMEOUT_NS that VKSwapChain::AcquireNextImage deliberately adopted so a surface destroyed under the GS thread — background, rotate, fold — does not leave every thread asleep at 0% CPU with nothing in any log. And the swapchain is 2 or 3 images while this loop holds the real one and asks for multiplier-1 more, so at x3/x4 it serialised on a vblank per generated frame, spending exactly the time the feature exists to save. Now a zero timeout: an interpolated frame is a bonus, so if nothing is free the right move is to drop it and get the real frame out. VK_NOT_READY leaves the semaphore unsignalled, so the slot stays clean for the next frame. 3. The fetch was not actually pinned. LSFG_PIN was "release" — a branch — under a comment explaining that an unpinned fetch would let a remote push change what our core links. Now the commit we have actually built and verified against, with GIT_SHALLOW off because a shallow clone carries only branch tips and cannot resolve a SHA. Also caches the structural PE check. GetUnavailableReason() runs once per frame from EndPresent while the feature is on, and it was doing a full fopen/fread/fseek/fread/fclose on the GS thread every frame; the verdict can only change when the path does, which is where it is now invalidated. |
||
|
|
cafb5e758b |
Android: make the legacy tier actually run on ARMv8.0 cores
The legacy APK claimed Android 8 (minSdk 26) while being compiled
-march=armv8.1-a, which lets clang emit LSE atomics inline. Android 8 means
Cortex-A53/A72/A73 — ARMv8.0, no LSE — so the one tier whose entire purpose is
reach did not reach them.
This is not a theoretical concern. BuildParameters.cmake:145 already records it:
'proven by a casal SIGILL on a real A53 device'. The guard written in response
only applies the safe default when nobody passes an -march, and this script
always passes one, so the tier defeated the protection added for it.
Legacy now builds -march=armv8-a -moutline-atomics, which is exactly what that
comment prescribes. Outline atomics keep LSE on cores that have it via a
runtime HWCAP dispatch, so a modern phone loses nothing.
Verified on the built core rather than assumed. The flags reach 3990 and 2036
compile lines respectively, and disassembling an LSE site shows the dispatch:
bti c
adrp x16, ... ; __aarch64_have_lse_atomics
ldrb w16, [x16, #0xc10]
cbz w16, 0x10b7128 ; no LSE -> fall through to LL/SC
cas w0, w1, [x2]
0x10b7128:
ldxr w0, [x2] / cmp / stxr / cbnz
An A53 takes the branch and never reaches the cas. APK minSdk confirmed 26.
Also moves a11/a13/a15 onto ARMSX3's SDK/NDK pairs and pins all four to NDK 29.
The NDK is not a device-compatibility knob — API level and -march gate devices,
and nothing on the device can tell which toolchain built the binary — so one
toolchain across the matrix is what makes a cross-tier comparison mean anything,
and there is no reason to withhold the measured gain from the weakest tier.
Needs a new armsx2.marchExtra gradle property: -moutline-atomics has to be its
own token, and BuildParameters.cmake's escape hatch keys on CMAKE_CXX_FLAGS
matching '-march='.
Artifact renamed to ARMSX2-<VN>-legacy-armv8.0-sdk26.apk. The updater keys on
the -sdkNN suffix, which is unchanged, so no updater change is needed. The Play
AAB is untouched: build.gradle.kts defaults still say minSdk 26 / NDK 28, and
only the APK script ever passes the tier properties.
|
||
|
|
aeabe3e0e5 |
EE cache: fix the DXSTG tag lookup's 29-bit fold, and the tests around it (#568)
* EE: the D-cache store-tag lookup dropped the top three bits of the tag DXSTG takes a guest physical page from TagLo and has to turn it into the host pointer our tags carry. It did that by routing the page through its KSEG0 alias, which meant masking the tag to 29 bits first -- and KSEG0 is only 512 MB wide, so the mask was not a formality. Every physical page at or above 0x20000000 folded into the low half of the map and resolved to whatever happened to live at the folded address. The consequence that matters is that a page past the end of the physical map folded onto real memory: 0x60129000 resolved to 0x00129000, and the eviction wrote 64 bytes of cache line into guest RAM the tag never named. Use vtlb_GetPhyPtr instead, which is what the debugger and PSM already use to ask this question. It covers the whole 1 GB physical map and answers null both for a handler page and for an address off the end of the map, so the unbacked case is now decided by the same lookup that produces the pointer rather than by a truncation. Where a tag naming one of our main-RAM mirrors resolves changes as a side effect of that, and is deliberately left unpinned. Those mirrors are our physical map's, not a console's: an SCPH-30001 has no RAM at those physical addresses, and an eviction steered at one reached nothing at all. There is no hardware answer to hold us to, so nothing asserts one. * Tests: point the DXSTG unresolvable-page check at a page that is unresolvable The check named 0x1FFFF000, described as "BIOS/unmapped territory at the top of the physical map". That page is the last one of the 4 MB BIOS ROM mapped at 0x1FC00000, so it is real backing memory: the test took the backed branch every time, wrote 64 bytes into the loaded BIOS image, and asserted only that nothing faulted. The branch it was named for -- the one carrying the safety property -- had no coverage at all. Name 0x60129000 instead. It is past the end of the physical map, and it is the page with teeth, because the old 29-bit fold sent it to 0x00129000 in main RAM. A witness there turns "we did not fault" into "we did not write somewhere the guest never named", which is the property worth holding. An SCPH-30001 agrees with that much: an eviction steered above the end of RAM puts nothing into RAM. Nothing beyond it is asserted -- where a tag naming one of our main-RAM mirrors resolves is emulator-specific, so it stays unpinned, with a comment saying so and why. * Tests: stop the DXSTG write-back check skipping on 16K-page hosts MapAt's candidate addresses are 4K-aligned and none is 16K-aligned, so on a 16K-page kernel -- Asahi, Apple Silicon, some Android, and one of our own CI jobs -- the kernel rejects every one of them and the mapping fails. The write-back check treated that as a precondition and skipped outright, which took its guest-side assertions with it: the ones that actually pin where a DXSTG-steered eviction lands, none of which need anything from the host. The mapping is only the negative control, there to show the write-back did not ALSO reach the host page carrying the same number. Make it optional. The guest-side half now runs everywhere and only the control drops out. DxstgDirtyStaysInsideGuestMemory still skips, and should: it is entirely about the host page. That leaves one skip here on a 16K-page host instead of two, and none at all on a 4K one.nightly-20260815 |
||
|
|
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.
|
||
|
|
318588f7a6 |
Android: LSFG frame generation (github flavour only)
Drives Lossless Scaling's interpolation from our own Vulkan present path. Upstream's consumer app captures the screen with MediaProjection and composites over the target process, because Android 12+ forbids injecting code into a non-debuggable app. That constraint is not ours: ARMSX2 owns its swapchain, so it hands the library its own images through the AHardwareBuffer entry points. No screen capture, no overlay, no accessibility service. NOTHING PROPRIETARY SHIPS. The interpolation shaders are read at runtime out of the user's own Lossless.dll, supplied through SAF exactly as a PS2 BIOS is. The requirements dialog says so before the toggle commits, not after it silently fails. Only the MIT-licensed lsfg-vk-android framegen library is fetched; its sibling app carries a no-commercial-use licence and is not. framegen is ISOLATED IN ITS OWN .so BEHIND A C ABI. It links volk, which defines 759 globals named vkCreateImage, vkQueueSubmit and so on -- precisely the names VKLoader.cpp defines. In one library that is a duplicate-symbol error at best; at worst the linker merges them and framegen's volkLoadDevice() call, made against its OWN VkDevice, silently repoints every entry point the GS renderer uses, which would present as a driver crash with nothing pointing back at frame generation. libarmsx2_lsfg.so gives volk its own copies, and nm confirms only the eight armsx2_lsfg_* entry points are exported. The interface is C because the CMake project builds ANDROID_STL=c++_static, so an std::vector crossing that boundary would be two unrelated types sharing a name; errors come back as codes, never exceptions. The shader chain (pe-parse over the PE resources, then upstream's DXBC to SPIR-V translator) stays in the core -- neither half touches Vulkan symbols. GSLsfg.cpp is the one PCSX2 translation unit built with exceptions, because that translator throws and the alternative is std::terminate on exactly the paths a wrong DLL takes. Present path mirrors upstream's Android sequence: copy the rendered frame into shared storage, idle, interpolate, idle, present each generated frame, then the real one. The idles are not laziness -- Turnip rejects OPAQUE_FD on AHB-imported memory, so there is no cross-device semaphore and a device idle is the only barrier that exists. Every failure degrades to an ordinary present rather than taking the GS thread down. Gated on Vulkan + Adreno 7xx and newer, asked of the resolved driver profile rather than a GL_RENDERER substring. The UI reports WHY it is unavailable, since 'needs an Adreno 7xx' and 'you have not picked a DLL yet' are the same greyed row otherwise and only one is actionable. Rows live in All Settings > Performance and the in-game performance tab, from one shared section, wired to each host's own settings tier the same way ShaderChainSection is. Play builds compile the whole thing out -- gradle sets ARMSX2_ENABLE_LSFG=OFF, BuildConfig.LSFG is false, and build-play-aab.sh now fails closed if libarmsx2_lsfg.so ever appears in a bundle. Verified: github APK carries libarmsx2_lsfg.so and 13 live @@ANDROID_LSFG@@ strings in the core; the play variant configures with zero references to either. NOT verified: the present path itself, which needs an Adreno 7xx device, a real Lossless.dll and a running game. |
||
|
|
146bce27de |
Android: four release targets, keyed by minSdk suffix
Splits the ARMv8.2 build into three platform tiers instead of two, so the
Android 11 floor gets FP16 + DotProd as well:
legacy minSdk 26 NDK 28 armv8.1-a
a11 minSdk 30 NDK 28 armv8.2-a+fp16+dotprod
a13 minSdk 33 NDK 28 armv8.2-a+fp16+dotprod
a15 minSdk 35 NDK 29 armv8.2-a+fp16+dotprod
Artifacts are now ARMSX2-<VN>-{legacy-armv8.1-sdk26,a11-armv8.2-sdk30,
a13-armv8.2-sdk33,a15-armv8.2-sdk35}.apk, and the updater classifies on the
-sdkNN suffix alone. The old markers were -v82 and -v82-sdk35, where one was a
substring of the other and only a carefully ordered when-branch kept Android 15
devices off the standard build; that hazard grows with every tier. The four sdk
suffixes cannot overlap.
An asset with no recognised marker still counts as legacy, so releases published
before tiering keep resolving.
The release-shape check now requires all four and verifies each name carries
exactly one, distinct sdk marker, and prints the upload-order warning: every
updater up to 2.6.6.6 takes the first .apk asset in a release regardless of
name, so the legacy build has to go up first or those installs are handed an
APK that SIGILLs on its first hot path.
|
||
|
|
5ddbf8188f |
Android: ship three CPU targets, and teach the updater to pick between them
A release now carries three sideload APKs instead of one: ARMSX2-<VN>.apk legacy minSdk 26, NDK 28.2, armv8.1-a ARMSX2-<VN>-v82.apk standard minSdk 33, NDK 28.2, armv8.2-a+fp16+dotprod ARMSX2-<VN>-v82-sdk35.apk modern minSdk 35, NDK 29, armv8.2-a+fp16+dotprod build-release-targets.sh owns the matrix and the naming; build-release-apk.sh keeps the whole recipe (dual page-size cores, PGO, rotation signing) and gains a GRADLE_EXTRA_ARGS seam. minSdk, ndkVersion and march become gradle properties whose DEFAULTS are the legacy build, so an unqualified invocation still produces exactly what it did before. ndkVersion is now pinned rather than left to AGP: three targets are only comparable if the toolchain moves when we say it moves. march is passed through CMAKE_CXX_FLAGS rather than set in CMake, because BuildParameters.cmake applies its armv8.1-a default only when CMAKE_CXX_FLAGS does not already carry a -march. That escape hatch exists because LSE atomics SIGILL on in-order ARMv8.0 cores, and it is the seam this needs; add_compile_options would land after these flags and win. The updater can no longer take the first .apk it sees. It classifies assets by the filename markers above and gates on TWO independent things: the CPU must carry FEAT_FP16 and FEAT_DotProd (read as asimdhp/asimddp off /proc/cpuinfo — both are OPTIONAL at ARMv8.2, so the architecture level is not a usable proxy), and the OS must meet the build's minSdk. It walks down from the best qualifying tier, so a release missing one degrades instead of offering nothing. Detection fails closed, deliberately. The failure modes are not symmetric: a capable device given the legacy build is merely slower, while an incapable device given a v8.2 build takes a SIGILL on the first hot path — and a user whose emulator will not launch cannot reach the updater to escape it. Anything unreadable, unparseable or absent means legacy. The release-shape check refuses to publish a set without the legacy artifact for the same reason: without it, every older device silently stops receiving updates. Measured, so the next person does not have to: enabling armv8.2-a+fp16+dotprod changes this codebase's codegen by 295 instructions in 3.8 million (0.008%), emits ZERO sdot/udot, and leaves the half-precision count identical. -march is permission, not a transformation, and the hot paths here are JIT-emitted at runtime where it has no say. The tiers are in place for code that will use them; today they carry the same work. |
||
|
|
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. |