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.
I left this out when porting the collapse toggle, on the grounds that ARMSX2
found docking alone did not answer the complaint. That was right about docking
being insufficient and wrong about it being unnecessary -- the two cover
different halves of the same problem. Collapse answers 'I cannot select what
is under you in the first place'; docking answers 'I have selected it and now
you are sitting on top of it'.
Selecting a widget in the top half moves the panel to the bottom, and back
again otherwise. Halves rather than real overlap maths, on purpose: a panel
that darts about as rectangles graze each other is less predictable than one
that is simply never on the side being worked on.
The stored drag offset means 'away from the anchored edge', so it flips sign
with the anchor. ARMSX3 anchors the panel top-centre and offsets downward, so
while docked to the bottom a +dy the user had nudged in would otherwise push
the panel straight off the screen.
Note for testing: this is only visible when selecting a widget in the TOP
half. Most on-screen controls live in the lower half, so selecting a face
button correctly leaves the panel where it is -- which is indistinguishable
from nothing happening. Use a shoulder button.
The slider was missing from the in-game menu entirely -- it only existed in
the settings screen. That is the wrong way round: the quick menu is where the
upscaler gets changed mid-game, and having to leave the game to tune what you
just switched to defeats the point of it being there.
Shown only for FSR and SGSR, since nearest and bilinear have nothing to
sharpen.
The label follows the selection in both places, because the number does not
mean the same thing to each: it is an RCAS stop to FSR and an edge factor to
SGSR, and a slider named for the upscaler that is not running is simply
wrong. One control rather than two, deliberately -- both upscalers want the
same thing from the user, and a second slider only lets them disagree.
Ported from ARMSX2, which built it against a user complaint worth restating:
the panel covers the thing you are trying to edit, and the only remedy on
offer was to drag it away, every time.
ARMSX2's first answer was auto-docking the panel to the opposite half of the
screen from the selected widget. It works, and it did NOT fix the complaint --
selecting a widget under the panel means touching through the panel first, so
a reactive fix cannot help with an obstruction that happens strictly before
there is anything to react to.
What fixed it was a collapse toggle: one tap leaves the grip strip and
uncovers everything beneath it, one tap brings the controls back. It is first
in the grip row so it sits in the same place whether the panel is open or
shut, and it is neither persisted nor carried between sessions -- it is a
momentary 'let me see under this', and an editor that opened to a panel with
no controls on it would look broken.
Column is an inline composable, so the collapsed path returns out of it and
genuinely stops emitting the rest rather than drawing it invisibly.
Auto-dock is not ported. It is the half that demonstrably did not solve the
problem, it needs an anchor flip this panel does not have (top-anchored with a
downward offset, where ARMSX2 switches edges), and the offset sign trap that
comes with it is real. Worth revisiting only if collapse turns out to be
insufficient.
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.
It was written but never staged: 'commit -a' does not pick up an untracked
file, so the temperature feature went in as its wiring only -- the settings
row, the startup hook and the native push all referencing a class that is not
in the repository. Builds here kept working because the file exists on disk;
a fresh clone would not have compiled.
Resumes the public version line after 0.9.4.3. The 0.9.4.4 through 0.9.5.7
bumps were internal test builds for the Xillia 2 hang hunt and were never
released; versionCode keeps climbing past them so a tester carrying one can
still update rather than having to uninstall.
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.