Git authorship covers the commits; the feature's own source should say where
it came from too, since that is what someone reading the code later actually
sees.
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.
Seven detectors over as many reproductions, none of which caught it. The
final one settles why: with cia sampled once a second, the guest threads are
found at a DIFFERENT address every time -- 0x011f63ac, 0x00278268, 0x008ac3a4,
0x0141512c -- so the best repeat count never exceeded 1.
The thread is not parked anywhere. It executes a great deal of varied guest
code at over 100% of a core while making no progress, consistent with
sys_ppu_thread_yield at ~100 million. A busy-wait that does real work each
iteration cannot be found by watching for something to stop, which is what
every one of these tried, in a different place each time.
What was learned and is worth keeping is recorded in the commits: frames keep
flipping throughout (so no frame-based check can see it), lock traffic never
ramps up because the hang precedes any workload, rsx::thread spins in
NV406E_SEMAPHORE_ACQUIRE, and PPU[0x1000000] burns 4.36s of CPU per 4s of wall
clock. The next attempt should start from a guest-side breakpoint or an
instruction trace, not from another liveness heuristic.
The structural fixes found along the way stay: on_frame_end no longer counts
forced frames as guest progress, and check_frame_stall dumps guest threads
rather than only reporting. Both are correct independently of this hunt.
The range-window version reset on every excursion, and its own report made
that look like success. widest_range=0x0 does not mean an identical cia -- it
means the entry had just been reset, so lo==hi. I read it the other way and
concluded the threshold was fine.
The thread mostly sits in a small loop (one sample caught it inside 0x500) and
occasionally wanders far enough -- a helper, a syscall handler -- to blow any
fixed window. So every all-or-nothing scheme measured nothing: hard reset on
an out-of-range sample threw away all the evidence collected before it, every
time.
Count how often each thread is found at the same cia and decay by one on a
miss instead. An occasional excursion now costs a point rather than the whole
history, so a thread parked at one address 90% of the time still accumulates,
while a thread genuinely making progress still falls to zero. Threads parked
in a syscall are skipped, never counted against, so idle still cannot look
like spin.
The state line reports the best count and the address it is stuck at, so a
miss says how close it got and where.
Erasing the tracked state whenever cpu_flag::wait was set is what stopped
this firing. The loop dips into a syscall regularly -- almost certainly
sys_ppu_thread_yield, seen at ~100 million -- and every dip cleared the
accumulated seconds, so the counter never got past 2.
The device reported it precisely once the range was instrumented:
widest_range=0x0, meaning an identical cia on every single sample. As tight a
spin as can exist, and invisible purely because of that erase.
A wait sample is now skipped rather than treated as evidence against a spin.
A thread genuinely parked in a syscall never accumulates running samples at
all, so idle threads still cannot trip it.
The 1 KiB base+offset window measured nothing. On the hung device it reported
tracked=1 longest=0s: one running thread, whose cia left the window every
second, so the window reset on every tick and the counter never advanced past
zero. The loop is wider than a handful of instructions -- it polls and calls
helpers -- which the previous shape could not represent at all.
Track the RANGE cia has covered instead, and allow 64 KiB of it. A polling
loop that calls helpers stays within tens of KiB; ordinary execution covers
megabytes in a second.
The state line now reports the widest range being tracked. If this still does
not fire, that number is itself the answer -- it says how large the loop
really is, and therefore what the threshold must be, instead of costing
another reproduction to find out.
Five earlier attempts at catching this hang keyed on something STOPPING --
frames, then lock traffic -- and every one missed it, because nothing stops.
Measured on the hung device: PPU[0x1000000] burning 4.36s of CPU across 4s of
wall clock, more than a full core, while rsx::thread spun on
NV406E_SEMAPHORE_ACQUIRE. Tales of Xillia 2 white-screens when its Bandai logo
is skipped, and the guest's main thread is not blocked at all -- it sits in a
tight guest-side wait loop, making no syscalls, taking no locks and writing no
log lines. A frame-based detector saw frames still flipping, a lock-based one
saw a peak of 10 locks/sec to fall from, and neither was wrong about what it
measured. They were measuring the wrong thing.
So look for the opposite of a stall: a thread that is RUNNING -- no
cpu_flag::wait, meaning not parked in a syscall -- whose cia has not left a
1 KiB window for 30 seconds. A spin loop is a few instructions branching to
themselves; ordinary execution walks cia across the binary many times a
second. Threads waiting in a syscall are skipped, so an idle game cannot trip
it.
Dumps twice, 15s apart, so the second shows whether cia moved at all between
them, and reports its own state every 10s: threads tracked, longest spin,
dumps taken.
Two corrections to the previous attempt, both measured rather than reasoned.
Including _sys_lwmutex_lock broke it. That counter keeps climbing straight
through the hang at a flat ~375 per 10s -- the idle loops take lightweight
mutexes -- so the 'unchanged' test re-armed on every tick and the detector
never fired. sys_mutex_lock alone froze outright, at 22.
And 'exactly zero' only fits Xillia 2. Kane & Lynch collapsed from ~200,000
per 10s to ~300, which is just as dead and never reaches zero. So the test is
now relative: remember the busiest rate this game has reached, decay it
slowly, and call it a hang when the current rate stays under a fiftieth of
that for 30 seconds. A title that has never been busy has no peak to fall from
and cannot trip it; the 5000/s floor sits far below every busy rate measured
(20,000+) and far above anything idle.
The syscall code is resolved by name from g_ppu_syscall_table once, rather
than hardcoded, so it cannot silently come to mean a different syscall.
Also logs its own state every 10s -- rate, peak, quiet seconds, dumps taken.
Five attempts at detecting this hang have now failed, every one of them
silently, and each cost a reproduction to discover. The detector reporting
what it sees is worth more than the detector being clever.
The frame-based check cannot see this class of hang at all. Tales of Xillia 2
white-screens with its RENDER loop still running: it submits real, non-forced
flips every ~10ms forever, so 'no frame presented' is never true while the
game logic behind them is dead. Measured on device -- g_last_frame_time was
9-12ms old on every sample taken across the hang. Four fixes to the
frame-based detector were all fixing the wrong instrument.
What actually stops is lock traffic. Both hangs seen so far -- Xillia 2's
white screen and Kane & Lynch's freeze -- show mutex acquisition at exactly
zero for minutes while sys_timer_usleep and sys_event_queue_receive continue
at flat, identical rates, which is idle service loops and nothing else. Both
games were taking 100k+ locks per 10s until the moment they stopped.
Polled from the PPU syscall usage thread, which already holds the counters and
is independent of both the RSX thread and the guest. Bounded the same way as
the other path: two dumps, the second 15s after the first so a cia that has
not moved between them is distinguishable from slow progress, re-armed only
when lock traffic resumes.
The watchdog was called under !Emu.IsPaused() && !Emu.IsStopped(). The
default IsStopped() overload is m_state <= system_state::stopping, and the
enum orders stopped, loading, stopping, running -- so it reports true for a
game that is LOADING.
A hang during a load is exactly what this watches for. Tales of Xillia 2
white-screens mid-load once its logos are skipped, so the guard skipped the
watchdog on every tick of the precise case it exists for, and skipped it
silently: not a declined decision anyone could read, just no call at all.
Nine minutes of held white screen produced no output whatsoever.
Use IsStopped(true), which is the fully-stopped test.
Also log the watchdog's decision once every 10s -- progress flag, last frame
timestamp, its age, dumps taken. Three attempts at this detector have failed
silently on a reproducible hang; the only evidence each time was an absence,
which cannot say which branch won. One line per ten seconds makes the next
failure a fact rather than another guess.
poll_frame_stall_watchdog returned early when g_last_frame_time was zero,
where the RSX-side check seeds it -- and seeding is what starts the clock.
Since the whole reason the watchdog exists is an RSX thread too stuck to run
that check, nothing ever seeded it: the value stayed zero and the watchdog
bailed on every tick forever, blocked in exactly the scenario it was written
for.
Reproduced on Tales of Xillia 2: white screen held for nearly nine minutes,
guest mutex traffic zero throughout, and not one dump.
check_frame_stall() runs from do_local_task, on the RSX thread's own FIFO
loop. That works for a guest-side hang with the RSX idle -- every hang chased
so far -- and is useless for the opposite case, where the RSX thread is the
one stuck. It never returns to do_local_task, so the detector that would
report the hang is starved by the hang.
Tales of Xillia 2 (BLUS31397) is exactly that. Reproduced on device: guest
mutex traffic at zero for minutes while sys_event_queue_receive and
sys_timer_usleep tick at flat identical rates, rsx::thread accumulating 4.99s
of CPU per 5s of wall clock, and NV406E_SEMAPHORE_ACQUIRE its costliest method
at 1.59ms a call. The RSX is spinning on a guest semaphore the stopped guest
will never write, and nothing reported any of it.
The same condition is now polled once a second from the PPU syscall usage
thread, which is independent and keeps running. That half only dumps; the
on-screen message and the native-UI flip stay on the RSX side, because the
overlay is not safe to drive from another thread. Both share one dump budget
so they cannot produce four dumps between them.
check_frame_stall() arms native-UI flipping when it reports a stall, so the
guest has something on screen while it is hung. Those synthetic flips reach
flip(), which finds nothing queued and calls on_frame_end(buffer, true), and
on_frame_end refreshed g_last_frame_time unconditionally. So the first hang of
a session switched on a flip source that then refreshed the timestamp forever,
and no later hang in that session could be detected at all.
Only a frame the guest actually produced counts as guest progress now.
Found on Tales of Xillia 2 (BLUS31397), which reproduces the same white screen
as Xillia 1: a stall was reported at 0:29:06, that game was closed, another was
booted, and when it hung 90 seconds later nothing fired. Guest mutex traffic
sat at exactly zero for over two minutes -- sys_event_queue_receive and
sys_timer_usleep continuing at flat, identical rates every interval, which is
idle service loops and nothing else -- while VKGSRender::flip kept running and
the detector kept believing frames were landing.
That is the one case this was written for: a hang where something still flips
looks perfectly healthy to a detector that only watches frames.
A callback the backend cannot fill completely is a hole in the output: the
device asked for N frames, the emulator did not have them, and the gap is
filled by repeating the last sample. That is what crackling IS, and nothing
counted it. The buffer-level report samples every ten seconds while a starved
callback lasts milliseconds, so every transient underrun passed between
samples unseen -- a Call of Duty: World at War capture shows a perfectly
healthy buffer (queued 22.7-48ms against a 36.7ms target, never near dry)
through a session where the audio was audibly breaking up.
Counted in AudioBackend rather than per backend so Cubeb and Oboe report the
same number the same way, one count per starved callback rather than per
padded frame -- the audible event is the gap, and its length is already
implied by how much of the callback had to be invented.
Reported on the existing audio line as a delta since the previous one, not a
running total: what matters is whether the output is breaking up now, and a
total from a rough patch minutes ago hides that.
It stays a feature -- the hotkey, the touch button and the OSD indicator are
untouched -- it just no longer occupies a tile in either panel.
Removing it also repairs the Session grid's controller dispatch, which was
broken. EmulationMenuViewModel indexes three things by the same number: the
grid in SessionPane, actionCount(), and activateSelection(). Fast-forward sat
at index 1 in the grid and had no entry in activateSelection, so every index
from 1 up dispatched to its neighbour -- a pad press on Fast Forward restarted
the game, Restart swapped the disc, Swap Disc closed the game, and Close could
not be activated at all. actionCount had been corrected to 5 to let the pad
reach the last tile, which made the misalignment reachable rather than fixing
it. With the tile gone the grid is resume/restart/swap/close, exactly what
activateSelection already dispatched, and actionCount goes to 4.
Touch users are unaffected: the grid is tapped by index through onSelect, so
it was always correct there. This only ever misfired for a controller.
Upstream bc22df8ba skips waking waiters after a SUCCESSFUL conditional store
when the SPU sits at pc 0x11e4 with the SPURS control block reserved, unless
byte 0x73 of that block shows this thread going running->idle. It is a
throughput optimisation: SPURS kernels store to that block constantly and
waking every waiter each time is a thundering herd.
Both constants are assumptions about one specific SPURS kernel build. 0x11e4
is a guest code address and 0x73 an offset inside the guest's control block,
and SPURS ships in many versions across titles. On a kernel whose layout
differs, the running->idle test reads the wrong byte, answers no forever, and
the store succeeds while every waiter stays asleep -- reported by nothing.
Tales of Xillia (BLUS31006) hangs with all five graphics SPUs reserving that
block, each suppressing ~100,000 notifications, ~4.85M conditional stores at
a 16.5% failure rate. Its SPURS kernel then executes its own HALT at
pc=0x00f00: r16 & r17 = 0x40000000, two workload masks that must be disjoint
both claiming workload bit 30. The group never joins, no frame is ever
presented again, and every PPU thread parks.
Whether the missed wakeups cause that inconsistency or merely accompany it is
NOT established. What is established is that notifying is correct and
suppressing is the optimisation, so the optimisation goes. Expect a
throughput cost on SPURS-heavy titles; it is measurable and revertable.
The counter is kept, now recording how often the heuristic would have
suppressed, so that cost shows up in a log instead of being guessed at.
The stall report picked its detailed SPU by raddr == spurs_addr -- a kernel
waiting on its own control block, i.e. an IDLE one -- on the assumption that
every kernel parks at the same pc so any of them would do. Tales of Xillia
is the counter-example: one graphics kernel executed a guest HALT and sits
stopped at pc=0x00f00 while the other four idle normally at 0x011a8, so the
rule picked an idle SPU. The only thread in the process with anything to say
printed no registers at all.
A stopped, halted or exited SPU now wins outright, chosen in a pass before
the walk. Nothing else in a SPURS group stops on its own, so if one has, it
is why the group never joined and the rest are only waiting on it.
Also dump all 128 GPRs rather than the first 16. Xillia's assertion is a
validity check over r12/r16/r17/r19/r33/r34, so the sixteen that were printed
did not include a single operand of the test the log had just disassembled.
average_playtime_ratio divided m_average_playtime -- a rolling average of
get_enqueued_playtime(), in microseconds -- by audio_buffer_length, which is
a sample count (AUDIO_BUFFER_SAMPLES * channels, 512 for stereo). The result
was microseconds per sample: roughly 78 on a healthy stereo buffer and never
below 1, so the 'not as full as desired' branch it gates has not executed
once since it was written. The buffer target has therefore always been the
fixed desired_buffer_duration + half a block, with the adaptive widening
silently inert.
Wrong from introduction rather than drifted: m_average_playtime has been a
duration since before that line existed. audio_buffer_length had no other
reader anywhere in the tree, which is why nothing caught it; it is deleted
here so it cannot be picked up again by mistake.
The denominator is now desired_buffer_duration, which is what the comment
above it names and the only choice that makes the branch meaningful --
audio_block_period would leave the ratio near 7 and the branch just as dead.
Guarded against a zero denominator, and the existing max(ratio, 0.25) clamp
still bounds the widening at 4x.
This arms a path that has effectively never run, so it changes behaviour for
every game: a consistently under-filled buffer now raises the target instead
of being ignored, trading latency for fewer dropouts. That is what it was
written to do. The audio buffer report added alongside prints the ratio, so
the effect is visible in a log rather than inferred.
Progressive audio delay keeps being reported (#87: Guitar Hero titles, where
audio starts synchronised and falls further behind the notes the longer a
song runs, and pausing resets it). It reproduces on both Cubeb and Oboe, so
it is not the backend -- it is this ring, and how full this ring is IS the
delay the player hears. It was recorded nowhere, so every theory about it is
unfalsifiable from the logs we receive.
One line per 10s: queued depth, the target the algorithm is aiming for, and
the dynamic period as a percentage of nominal, which says how hard it is
correcting. Enough to see the curve across a song; few enough that the log
volume cannot become a stall in its own right.
Noted while reading the algorithm, deliberately NOT changed here:
average_playtime_ratio divides m_average_playtime (microseconds) by
audio_buffer_length (samples times channels, 512 for stereo), so it reads
about 78 when healthy and its 'not as full as desired' branch has never once
executed. The intended denominator is a duration. Correcting it would arm a
widening path that has effectively never run, which raises latency in exactly
the under-buffered case -- the opposite of the symptom being chased. It wants
its own change, with the numbers this report will provide.
check_frame_stall() already detects a hang reliably and always-on: it
reports "No frame presented in 30s" and puts a message on screen. But the
one thing that says WHERE the guest is parked -- dump_guest_threads_stalled(),
which prints every PPU thread's registers, guest call stack and the
instructions around its cia, plus SPU state -- was only ever reached from the
RSX profiler's poll_stall(), and that returns false immediately unless the
profiler is switched on. Testers do not switch it on.
So every freeze report arrives with the detection line and nothing behind
it. Confirmed against a Kane & Lynch (BLUS30102) capture: guest execution
collapsed at 0:04:15 -- sys_mutex_lock fell from ~200,000 per 10s to ~300,
CPU from 43% to 5.5% -- and this fired at 0:04:45 having recorded none of
it. Three such reports across two different SoCs, none reproducible locally,
all equally undiagnosable.
Call it from the detector instead. Twice, ~15s apart, because one sample
cannot tell a thread spinning from one making very slow progress -- a cia
that has not moved between two samples is itself the finding. Twice and no
more, and re-armed only when a frame actually lands: the detail runs to
hundreds of lines per thread, and log volume alone is enough to stall the
emulator on Android. The profiler path is unchanged.