It fired on its very first run against an APK that DID contain the library. `unzip -l | grep -q`
under `set -o pipefail` is a false-failure generator: grep exits on the first match, SIGPIPEs
unzip, and pipefail then reports the pipeline as failed. Capture the listing and match it with
`case` instead, which is what the existing notes on this already say to do.
2.6.6.8 shipped without Discord and it was only noticed after publication. The SDK is resolved
from $DISCORD_SDK_DIR at configure time and gated on include/discordpp.h existing, so with the
variable unset the build quietly omits it -- no error, no warning, nothing in the log to read
afterwards. Every check that already runs on these artifacts (both cores, alignment, signing,
package, MANAGE_EXTERNAL_STORAGE) would have caught this class of mistake if one had existed.
Both scripts now verify libdiscord_partner_sdk.so in the output. With DISCORD_SDK_DIR set and
the library absent, that is FATAL -- it means the staged directory was wrong, which is easy to
get wrong given the raw SDK download ships an x86-64 .so and only the .aar carries arm64. With
the variable unset it warns loudly instead of failing, because a Discord-less build is still a
legitimate thing to produce on purpose.
Completes the switch to the Eden port. GSLsfg keeps its entire public surface —
availability, status text, display FPS, the settings and OSD plumbing all
untouched — and only its internals change, so nothing above the renderer had to
move.
What actually changed on screen: the old implementation ran the interpolator on
a SECOND VkDevice and shared images as AHardwareBuffers, and because Android
offers no cross-device semaphore (Turnip rejects OPAQUE_FD export on AHB memory)
the only barrier available was a full device idle — twice per frame, every
frame. That is gone. Generation is now ordinary compute recorded into a command
buffer on the device we already have, and interpolated frames are written
STRAIGHT into an acquired swap chain image through a storage view, so the
intermediate copy is gone too.
The pacer comes with it, which is the fix for games that oscillate between 60
and 30fps on a 60Hz panel: the generation count now varies to hold the presented
rate near a target instead of blindly multiplying whatever the game produced.
★ ONE submit, N+1 semaphores. All the generation work goes into a single
command buffer, submitted once, waiting on the caller's render-finished
semaphore plus every acquire, and signalling one semaphore per present that
follows. The obvious alternative — a submit per generated frame — walks straight
back into the binary-semaphore bug this file was bitten by before, where the
real present and the first generated present both want to wait on the semaphore
that says the source has been read. A binary semaphore may be waited exactly
once.
★ The hook fires AFTER vkQueueSubmit, so FrameGen had to take its command
buffer as a parameter. It was written against GSDeviceVK::GetCurrentCommandBuffer(),
which at that point is in flight or already belongs to the next frame; recording
into it is undefined and the symptom would have been interpolation running a
frame late rather than anything resembling an error.
Layout bracketing is ours: the ported passes speak Eden's convention where a
presentable image lives in GENERAL, and PCSX2 hands them over in PRESENT_SRC_KHR
and needs them back in it.
The swap chain now requests VK_IMAGE_USAGE_STORAGE_BIT — but only when frame
generation is on AND both the surface and the chosen format allow it. Asking
unconditionally fails swap chain creation outright on drivers that do not, which
would take the whole renderer down for a feature that is switched off. The
format half is the easy one to miss: a surface can report STORAGE support while
the sRGB format picked for it has no STORAGE_IMAGE feature bit, and that only
shows up later as a validation error at image-view creation. Because usage is
fixed at creation, switching the feature on mid-session needs a renderer
restart; Initialize says so rather than failing silently.
DELETED: platforms/android/app/src/main/cpp/3rdparty/lsfg in full — the
lsfg-vk-android framegen library, the DXVK dxbc compiler, pe-parse, volk and its
759-symbol collision with VKLoader, the C ABI shim, the version script, the
separate .so and the dlopen that found it, and the -fexceptions carve-out they
needed. GSLsfg.cpp went from 1259 lines to 654. The ~130 MB configure-time fetch
goes with it.
build-play-aab.sh's guard was rewritten rather than dropped: it checked for a
file that can no longer exist either way, so it would have passed forever
without proving anything. It now looks inside the core for a symbol only the
ported implementation defines.
Verified: all 18 affected translation units compile without errors, with
ARMSX2_HAS_LSFG on AND off (the play flavour still compiles the feature out
entirely). Not yet run on hardware.
Play builds cannot carry LSFG at all, and they did. The gating was a
BuildConfig.LSFG check inside shared files, which is a weaker claim than it
reads as: the rows were never drawn, and all 22 frame-generation strings still
shipped in the Play dex in plain text — including "Lossless Scaling",
"Lossless.dll" and the requirements dialog naming the product, which is exactly
what a text search over the artifact finds. The native half was already
genuinely compiled out (-DARMSX2_ENABLE_LSFG=OFF); only the Kotlin half looked
like it was.
Moved to source sets, which is the arrangement that actually excludes:
LsfgSection.kt main -> github, with a no-op stub in play
the 22 EN strings -> I18nLsfg.kt, real in github and an EMPTY MAP in play
the 5 search rows -> SettingsSearchLsfg.kt, likewise
LsfgEmulationCard new, so the shared pause-menu file no longer even names
the section's string key (SectionCard became internal)
EN is now BASE_EN + LSFG_EN and the search index BASE + LSFG, so whichever
flavour is in scope supplies its half and no caller knows which build it is in.
Splitting the search rows is a behaviour fix as well: in the play build they were
indexed while the section they pointed at was compiled out, so searching would
offer a result that rendered its own key as its title and led nowhere.
The settings FIELDS stay shared on purpose — identifiers rather than product
names, and an identical config schema across flavours is what lets a config move
between builds without losing data.
Verified on compiled output rather than source: playDebug has zero class files
containing 'Lossless' and zero containing 'perf.lsfg'; githubDebug has 2 and 4.
I18nLsfgKt.class is 3633 bytes in github and 833 in play. build-play-aab.sh now
greps the AAB's dex for both strings and fails the build if either appears, so a
later edit to a shared file cannot quietly undo this.
★ That verification first came back clean for BOTH flavours, which was a false
negative: Xcode's strings(1) parses a .class as a Mach-O fat binary, errors, and
prints nothing — indistinguishable from a pass. LC_ALL=C grep -a is what the
check uses, and what the comment in the script warns about.
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.
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.
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.
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.
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.
The root tree declares ENABLE_RECOMPILER_TEST_HOOKS but the Android copy
of BuildParameters never did, so PCSX2_RECOMPILER_TESTS was always false
and the opcode-group interpreter bisect could not be compiled into an APK
at all. That bisect is what localises an EE codegen bug to one emitter
family, and on a device it is the only place some bugs reproduce.
Declares the option (default OFF, so nothing changes for a normal build)
and plumbs it through as -Parmsx2.recTestHooks / REC_TEST_HOOKS.
Adds a "Check for updates" panel to the top of the App settings tab, github
sideload flavor only. It queries the GitHub releases/latest API, semver-compares
the latest stable tag against the installed build, and offers to download the APK
and hand it to the system installer (progress bar + FileProvider). Nightly builds
(versionCode = Unix seconds, so > 1e6) are always ahead of any stable release, so
they short-circuit to "on the nightly channel" and are never prompted to a stable.
Kept entirely out of the Play build, the same way all-files access is:
- IN_APP_UPDATER BuildConfig flag (true github / false play) gates the App-tab hook.
- The real updater + REQUEST_INSTALL_PACKAGES + the FileProvider live in src/github;
src/play ships a no-op UpdaterEntry stub so shared code still compiles for play.
- build-play-aab.sh now FAILS CLOSED if REQUEST_INSTALL_PACKAGES appears in the AAB
(a self-updating app is a hard Play-policy violation).
Verified: the github APK ships the permission + FileProvider + updater code; the play
AAB has neither the permission, the FileProvider, nor the network/install code.
build-play-aab.sh hardcoded pgo=optimize, so a caller asking for a profile-free
build silently got one built against the profile anyway. Take PGO_MODE like the
sibling build-release-apk.sh does.
- Replace the in-game settings cog with a single top-right pause button (#357).
Single tap opens the menu; it renders outside the auto-hide/"Never" gate so
hiding the on-screen pad can no longer strand you without a way in.
- "Tap to reveal pause" replaces the old show/hide toggle, which could lock the
menu away entirely. Migrates old layouts, including per-game and per-orientation.
- Run the RetroArch shader chain at the frame's on-screen size instead of the
internal one, so CRT scanlines land at display pixel density. Sized to the
aspect-corrected draw rect, since librashader maps input to the whole viewport.
- Patch manager: stop one game's patches showing under another, de-duplicate
repeated cheats, split patches/cheats into collapsible sections, and drop the
lag on large lists. Rename the widescreen toggle to say it auto-applies.
- On-Screen settings: the top slider drove OSD scale while labelled "UI Size" and
shared a label key with the real UI slider. Renamed to "OSD Size" and grouped
the three size controls together. OSD now defaults to 65%.
- Per-game graphics API, rotation and GPU driver.
- Gate the Adreno push-descriptor disable on driverID so 8 Elite keeps them.
- Load/save state slots no longer squash on the Load screen.
- Sync GameDB and fix two entries that parsed as no-ops: Genji's vu0ClampMode
casing and DOA2's mis-indented minimumBlendingLevel.
Games that synchronously read GS memory back — occlusion tests such as sun glare
are the common case — fence-wait on everything recorded before the readback copy.
With one submit per frame the GPU only began executing at that wait, so the frame
degenerated into GS-thread recording time plus full GPU time, serialised. The
Vulkan backend now submits accumulated work at a render-pass boundary while such a
frame records, so the GPU runs concurrently and the wait finds the work already
done. It is gated to outside a render pass, so tilers take no forced flush, and to
frames near an actual readback, so games that never read back are untouched. The
kick never blocks: it submits only when the next command buffer is verifiably
complete, because cycling into a buffer whose previous submission is still
executing is a hidden GPU sync worse than the backlog being drained. A draw into a
recently read target predicts the next readback and kicks ahead of it regardless of
the render-pass threshold. Identical commands, split across submits, so nothing
about what is drawn changes.
Library titles now come from the game database, keyed by serial, with the filename
kept only as a fallback for discs the database does not know. Database titles are
the curated ones, free of dump markers, and for a Japanese release the title is the
Japanese one. Sorting uses the database's sort key, which for those games is the
kana reading — sorting the kanji sorts by codepoint and means nothing to a reader.
An English titles option in the library menu switches to the romanised name where
the database carries one, and search matches both forms either way, so a game
listed in Japanese still answers to its English name. Titles are picked up on the
next library scan.
Save states can be written on an interval while a game runs, from one to thirty
minutes, off by default. It writes the same dedicated auto-save slot that auto-save
on exit uses, so the numbered slots stay user-controlled and auto-load on boot needs
no change. It fires only while the game is actually running, never while paused or
behind a menu, where a save costs a hitch and buys nothing.
Skin packs that draw the left and right analog sticks differently now render both.
The two images were folded onto one slot, so the second overwrote the first as the
pack installed and the survivor was drawn under both sticks. Each stick now resolves
its own art, falling back to a pack's single shared image and then to the built-in,
so one-image packs and the bundled skins are unaffected. An affected pack needs
importing again, since only one of its two images was kept.
Also print the real PGO mode in the release build script, which reported optimize
even when generating an instrumented build.
- ci-nightly-dualcore.sh signs nightlies with the release rotation lineage
(debug<=API32 -> release>=API33) from repo secrets, so a nightly installs over
the existing com.armsx2 build; falls back to a throwaway key (with a warning)
- versionCode = Unix seconds since 2023-11, monotonic and always above the manual
10xx codes, so each nightly out-versions the last installed build
- versionName default 2.5.9 -> 2.6.0
- Discord announcement now includes the changelog (trimmed to Discord's limit)
- tools/ci-nightly-dualcore.sh builds the core at both host page sizes and
merges both .so into one APK so 16k-page devices load their native core
- nightly builds with PGO=optimize (committed pgo/armsx2.profdata)
- trigger the nightly on push to master (was schedule + dispatch only)
- plain-language changelog in the release body from filtered commit subjects
Port the complete JIT and write-xor-execute infrastructure to DarwinMisc with four JitModes (Simulator, Legacy, LuckTXM, LuckNoTXM), dual-mapping via vm_remap for writable code aliases, the csops CS_DEBUGGED probe for JIT availability detection, brk assembly helpers for the TXM protocol, and the W^X toggle functions. Connect the JIT foundation to the code emitters through AsmHelpers dual-map bridge, Memory.cpp MmapCodeDualMap allocation, and the aR5900 LegacyEnsureExecutable path. Refresh the iOS SwiftUI frontend from the iOS-refresh branch, bringing in 11 missing and 20 drifted Swift files plus ios_main.mm integration. Switch the CI to a real device build using the iphoneos SDK. Fix the Achievements crash by gracefully degrading when no HTTPDownloader is available (no CURL on iOS). Fix the Metal surface to reuse the UIView's existing CAMetalLayer instead of an orphaned allocation that caused half-screen crops. Fix GS memory allocation by using mmap and vm_remap instead of shm_open which is blocked by the iOS sandbox. Suppress the false positive Graphics not Automatic OSD warning. Merge upstream master and resolve all resulting compile errors.
Strip the legacy Android, React Native, Java, Gradle, and res/ directories from platforms/ios that were inherited from the original port. Configure the CMake build for the iOS target with local module discovery, PCAP and CURL guards, rapidyaml source path fixes, Vulkan disabled, lz4 build flags scoped, libjpeg-turbo skipped on iOS, and the Qt UI and test runners turned off. Set CMAKE_SYSTEM_PROCESSOR to arm64 for cross-compilation. Add a self-contained GitHub Actions workflow that builds an unsigned IPA for real devices using the iphoneos SDK, named with the commit SHA. Merge upstream master to stay current.
Snapshot the refresh-experimental Android app (Gradle + JNI + Android-only
3rdparty) into platforms/android/. Delete its vendored PCSX2 core copy and
relocate the ~24 genuinely Android-specific core additions (Oboe audio, Android
stubs, EGL-Android GL context, NEON SPU2, GSGPUProfile, VU1Fingerprint, Android
HTTP downloader) into the root core, guarded by if(ANDROID) in
pcsx2/CMakeLists.txt and common/CMakeLists.txt.
The superseded arm64 JIT experiment (arm64/mac/* IR-VU backend, split
aVU0/aDMAC/aVTLB/aR5900COP*) is dropped: the root macOS arm64 JIT (127 unique
commits, newer) is the canonical recompiler.
Rewire the Android native build to a thin CMakeLists that sources the root
{common,pcsx2,3rdparty} instead of the deleted vendored copy.
NOT yet compiled against a real NDK -- build validation is CI's job.
See REFACTOR_STATUS.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>