Snapdragon Game Super Resolution 1.0, mobile variant: a single-pass
edge-directed spatial upscaler Qualcomm wrote for Adreno. Against FSR1 it is
one dispatch instead of two and one target instead of two, which is what makes
it worth having on a phone -- cheaper, not better.
Licensing is the reason this is a reimplementation rather than a port.
Suggested by CamilleLaVey, who made the same filter work in Eden, but Eden's
glue is GPL-3.0-or-later and RPCS3 is GPL-2.0-ONLY, so none of it is used --
the same blocker that stopped the LSFG adoption, and permission cannot fix it
because Eden has other contributors. What IS used is Qualcomm's BSD-3-Clause
release, which is GPL-2.0 compatible, with the copyright notice kept. The crop
mapping and the widened sharpness range are reimplemented from a description
of what they do, which is not copyrightable.
Qualcomm ship it as a fragment shader over a fullscreen triangle; this is a
compute pass because that is what the VK device layer already schedules. The
interpolated texcoord becomes a UV from the invocation id and the fragment
output becomes an imageStore, with a bounds check because a dispatch rounds up
to whole workgroups.
The push constant offsets were read out of the compiled SPIR-V rather than
derived from the struct -- 0/8/16/24/32/40, 44 bytes -- because a mismatch
there produces garbage that looks exactly like a shader bug. glslc also
type-checks the GLSL, which the native build cannot: shaders here are compiled
at runtime, so a broken one builds fine and fails on device.
No vendor gate, deliberately: its requirements are a strict subset of FSR1's
(textureGather with a constant component and no offset, an rgba8 storage
image, one descriptor set), so anywhere FSR1 runs, this runs. It no-ops when
the frame is already at or above output resolution, which is correct and
indistinguishable from broken, so the setting text says so.
Wired at all five places the mode is represented: the enum (appended, never
inserted -- it is serialised by ordinal in savestates), the fmt_class_string
case (a missing one serialises as 'unknown' and the mode is silently never
selectable), the VK dispatch, both Kotlin pickers, and the persisted clamp
that would otherwise rewrite the new value straight back to FSR.
14 of the 15 upstream commits since bab81aa23. The fifteenth, 3aea3b15d 'Fix
ISO timestamps', is deliberately left out: it touches rpcs3/Loader/ISO.cpp,
and the Aug-2026 upstream ISO refactor is already reverted here because it
breaks some images (region_count reads 0 and the disc will not mount). It is
the tip commit, so merging its parent excluded it exactly, with no surgery.
Four conflicts, all of them ours-and-theirs rather than either-or:
nv4097.cpp conflicted whole-file. Took ours and applied upstream 071c9f10f's
set_shading_mode by hand -- an earlier merge of this same file lost two hunks
by resolving it wholesale, and that is recorded in c6a0878a9.
VKPipelineCompiler.cpp: ours has the mobile dynamic-state work (topology-class
collapsing, normalize_dynamic_pipeline_state, compiler thread affinity),
upstream adds a provoking-vertex chain for flat shading. They are independent,
so both are in, with the rasterization state rebased as upstream needs.
device.cpp: three hunks, all parallel feature queries -- extended dynamic
state and the Android LSFG feature bits on our side, provoking vertex on
theirs. All kept; ours' extension push needed its own closing brace.
BUILDING.md stays deleted: 30fc4e566 folded it into the README, and upstream
merely edited it.
Core builds and links.
Rumble always went to the phone's own motor: the vibrator lookup asked
the system service and never considered the connected pad. On a handheld
that is the wrong motor outright, and on a phone-plus-controller setup it
buzzes the device sitting in a dock while the pad in hand stays still.
Prefer the first connected gamepad or joystick reporting a working motor,
falling back to the phone. The target is resolved per state change rather
than cached for the pump's lifetime, so connecting or disconnecting a pad
mid-session moves rumble with it; the motor that was last started is
tracked separately so unplugging mid-rumble cannot leave one buzzing.
Add a 'Vibrate the phone' toggle gating only that fallback, so playing on
a pad need not mean the phone rumbles along with it. Touch haptics stay
on the phone deliberately -- the finger is on the phone's screen.
Closes#89
Both changes that were brought back are out again. Neither recovered the frame
rate after switching frame generation off, and both broke rendering.
That is every attempt at this reverted. What remains is the code as it stood
before any of it: frame generation is slow, and switching it off does not
release what it allocated.
The GLES DSA shim is kept, because it belongs to the upstream merge and not to
this work -- it landed in the wrong commit and reverting that commit took it
with it, which broke the build for a reason unrelated to frame generation.
The findings from the three audits are still correct as descriptions of what
the code does. Acting on them, one at a time and with the reasoning written
down each time, still made the result worse on every attempt. So the fault is
not in the individual fixes but in something about this subsystem that reading
it has not revealed, and the next attempt should start from a measurement on
the device rather than from another reading of the source.
This is the change that stopped turning frame generation off leaving the
frame rate tanked -- confirmed by which build that was reported on, and by the
SUBOPTIMAL acquire fix not reproducing it on its own.
It replaces the per-frame vkDeviceWaitIdle on framegen's device with a poll on
a sync fd exported from a fence framegen already creates. Reasons it is safe
to bring back while the other three stay reverted: it only runs when frame
generation is producing frames, every failure path falls back to the device
wait, and it changes no layout, no ownership and no barrier -- which is where
the rendering damage came from.
Still out, and staying out: the cross-device ownership barrier, the
generated-frame acquire semaphore with its teardown, and the deferred-present
guard in frame_context_cleanup.
Catches up 98 commits from RPCS3/rpcs3. Seven files conflicted:
PPUTranslator.cpp -- upstream fixed the ARM64 float-to-int saturation
inversion independently, and the two fixes are the same fix. Took upstream's
ordering so it stops re-conflicting, kept one line of the reasoning.
rpcn_types.h, rpcn_client.cpp -- our hand-rolled protocol 31 bump was a
stopgap to stop the server refusing us. Upstream's trophy sync (cb175278b) is
the real implementation, so it replaces ours outright.
nv0039.cpp -- upstream refactored the strided copy and extracted
validate_buffer_notify; ours was the older code plus a profiler include. Took
upstream's, re-added the include.
VKQueryPool.cpp -- both sides had real changes. Upstream added a lock around
the pool cache; ours has the bounded occlusion-query wait and the render-pass
fix for the Adreno device loss. Kept ours, re-applied their lock.
VKResourceManager.cpp -- upstream moved GC completion onto a
driver_manager_thread, which supersedes our flush parameter and offloader
dispatch entirely. Took theirs; no callers passed the second argument.
VKGSRender.cpp -- our flush-site counter and their driver-manager drain are
independent. Both kept.
Upstream also began attaching 3D and array levels through DSA, which
EXT_direct_state_access has no NamedFramebufferTextureLayer for, so the GLES
shim gains one that binds and uses the non-DSA entry point.
Reverted in the same commit, because they are what is on the tester's device
and it is broken: the sync-fd handoff, the SUBOPTIMAL acquire fix, the
cross-device ownership barrier, the generated-frame acquire semaphore, and the
deferred-present guard in frame_context_cleanup. Each was defensible on its
own reading of the code and the result was worse every time -- slower, judder
described as nauseating, and rendering faults that outlast switching frame
generation off. Five attempts is enough to stop treating the next theory as
better than the last.
What stays fixed is everything outside the present path. What goes back is the
state before tonight: frame generation is slow and does not release its memory
when switched off.
Every generated frame ended with vkDeviceWaitIdle on framegen's device. It is
the hardest sync primitive Vulkan has and it ran once per frame, which is a
large part of why frame generation cost more than it returned.
It was there because the two devices share no semaphore. The upstream
semaphore path cannot supply one on Android: framegen's device deliberately
does not enable VK_KHR_external_semaphore_fd -- it shares through
AHardwareBuffer instead -- so vkImportSemaphoreFdKHR resolves to nullptr there
and every semaphore handed across is -1. That is not a flag that can be
flipped.
A fence can be exported where a semaphore cannot. framegen already creates one
per generation pass, so this adds VK_KHR_external_fence_fd, exports a sync fd
after the passes are submitted, and hands it back through a new
armsx3_lsfg_present_fenced. Our side polls that fd instead of idling a device.
Three things this deliberately does not do:
The existing completion fence is not the one exported. SYNC_FD export has copy
transference, which resets the source fence, so exporting it would leave the
slot-reuse wait at the top of present() blocking on a fence with no pending
signal -- a hang traded for a stall. A dedicated fence signalled by a
zero-batch submit is used instead, which signals once all previously submitted
queue work completes.
The extension is probed, not required. framegen's required-extension list
throws on a miss, so a driver without it would fail vkCreateDevice and lose
frame generation altogether -- a regression rather than a degradation.
The device wait stays as the fallback. A null entry point, a failed export, a
poll timeout or a poll error all fall back to it, and the warning for that is
emitted once per session rather than once per frame.
armsx3_lsfg_present keeps its signature and forwards to the new path with a
null fd, so the two bodies cannot drift. ABI goes to 3; the version check is
left exact rather than loosened.
Interpolates frames between the ones the game draws, at x2/x3/x4. The shaders
come from the user's own Lossless.dll; nothing is bundled or downloaded.
framegen runs on its OWN VkDevice and statically links volk, which defines 655
globals named vkCreateImage, vkQueueSubmit and so on -- including all 124 our
loader declares. Linked into the core those either fail to link or, worse,
merge, and framegen's volkLoadDevice() then repoints the whole RSX renderer at
framegen's device. So it lives in libarmsx3_lsfg.so, reached only by dlopen
with RTLD_LOCAL, behind a C ABI and a version script that exports eleven
symbols and nothing else. Verify with llvm-nm --dynamic --defined-only: only
armsx3_lsfg_* may appear.
Two devices with no shared semaphore means images cross as AHardwareBuffer --
Adreno and Mali both refuse vkGetMemoryFdKHR(OPAQUE_FD) on AHB-imported memory,
so upstream's FD path does not work on this hardware. Capture costs 0.007
ms/frame CPU, measured; the cost is the synchronisation, not the copies.
Notes for anyone reading this later:
* The shader loader's user pointer must outlive initialize(). framegen copies
the callback into ShaderPool::source and resolves shaders lazily while
BUILDING THE CONTEXT, so a stack local there is read back from a dead frame
-- a segfault executing at a mapped, non-executable address.
* The "device UUID" is not one. framegen matches (vendorID << 32) | deviceID.
Zero matches nothing.
* Imported shaders are cached to disk. They used to live only in the library's
map, so every restart silently had none and generate() returned 0 before
doing any work.
* Capture takes the COMPOSITED swapchain image, after overlays. Capturing the
game image put the perf overlay on real frames only, so it blinked at half
the display rate.
* generate() runs only on a frame the game actually drew, or the PPU/SPU
compilation screen gets interpolated too.
The pipelined path that would take waitIdle off the critical path is present but
disabled behind k_framegen_pipelining_enabled: holding a frame back conflicts
with frame-context recycling, and at least one reclaim path has not been found.
The serialised path is what works. Frame generation costs some real framerate
and wants a steady one -- interpolating an unstable rate reads as judder -- so
it is labelled experimental in the UI.
Saint Seiya: Sanctuary Battle (BLES01421) stalled partway through PPU
compilation and booted to a black screen. The failure was in LLVM, not here:
on AArch64 the register scavenger ran out of registers under the GHC calling
convention, which pins most of the GPRs to guest state, and
AArch64FrameLowering::determineCalleeSaves returns early for GHC before it can
create the emergency spill slot the scavenger falls back on. The scavenger then
aborts, and because that takes down the whole MODULE rather than one function,
every function in it drops to the interpreter -- the boot never finishes, or
the game runs at interpreter speed with nothing in the log to explain it.
Fix creates the spill slot for GHC frames that actually need stack. 231/231
modules compile for Saint Seiya, and Sonic Unleashed's FMVs work for the same
reason. Because it is a codegen fix rather than a per-game workaround, any
title that hit this benefits.
The change itself lives in the LLVM submodule, whose remote is upstream
llvm/llvm-project, so it cannot travel in this repository. It is preserved
here as 3rdparty/llvm/armsx3-aarch64-ghc-emergency-spill.patch, applied
against the pinned submodule commit; a build without it applied will exhibit
the original stall.
Also bumps the ARM64 codegen cache version so caches produced before the fix
are not reused, and carries the PPUTranslator changes the same work needed.
SPU: the ARM64 block checksum folded two thirds of every block through
absolute difference, which is not injective, so adding the same value to
two words left the checksum unchanged and similar job binaries hashed
alike. Plain summation now. This is what Precise SPU Verification was
working around, and that setting is exposed properly instead of only being
reachable by hand editing the config.
SPU: a block is no longer marked permanently failed when the trampoline
rebuild fails. The compiled function was live, the state was not
recoverable for the rest of the session, and the claim could never be
retaken.
RSX: render pass churn cut in heavy scenes, roughly 113 to 85 passes per
frame. On a tile based GPU every pass boundary is a full tile store and
reload. Two Vulkan specification violations fixed, and a read/write hazard
on the render pass path.
RSX: the FIFO no longer burns a core on sched_yield while idle.
Android: ADPF is implemented rather than an inert setting, logcat no longer
allocates and makes an IPC call per line, and Silence All Logs is available
for playable titles.
Audio: Oboe backend, for the per device quirks database and stream recovery
on disconnect and route change.
Ported from ouroboros420/rpcsx: GPU Turbo, power and thermal handling, the
crash and freeze fixes, savestate and WSI surface lifetime, honest RAM VRAM
budgeting, the persistent SPU object cache design, occlusion query and RSX
fixes, frame pacing and tiler tuning.
Ported from rfandango/rpcsx: the Turnip ZCULL deadlock fix and ARM64 SPU
checksum handling.
Individual commits are credited in comments at each site.
Adds an Android build of the RPCS3 core plus a Compose UI, and fixes several
things that stopped it working on ARM64.
Renderer:
- Emit concrete bounds for runtime sized arrays in uniform blocks when
VK_EXT_shader_uniform_buffer_unsized_array is missing. Adreno does not have
the extension, so every game pipeline failed with VK_ERROR_UNKNOWN and only
overlays drew.
- Probe and request that extension properly instead of chaining its feature
struct unconditionally.
- Hand VMA the Vulkan function pointers it needs under VK_NO_PROTOTYPES.
- Rebuild the surface and swapchain when the window is lost instead of killing
the RSX thread.
- Only create a GLES context when the GL renderer is actually selected.
SPU:
- Sum instead of taking an absolute difference in the ARM64 block verification
checksum. The difference collides on the near identical job binaries an SPU
job manager streams through one local store address, so a cached block could
run against another job's code.
Threading:
- Implement thread affinity on Android using sched_setaffinity.
- Add an ARM big.LITTLE core arrangement so SPU and RSX threads land on the
fast cores.
Misc:
- Detect the host CPU for the LLVM JIT instead of pinning cortex-a34.
- Fall back to the default audio device when cubeb cannot enumerate.