Per-section Reset did nothing on most tabs. The field lists describe the tabs
as they were before the PS3 rewrite, so Reset was clearing settings the tabs no
longer show while missing most of what they do: Performance listed 22 of 47,
Graphics 45 of 57, Audio 10 of 16 -- audioRenderer, audioFormat, audioChannels
and audioCubebBackend were absent, so changing the audio backend and pressing
Reset was a no-op. Regenerated from what each tab actually writes, mapping
ps3.foo to its ps3Foo key and validating every entry against the serialiser.
Five keys also moved off Graphics because another tab owns them, which was a
cross-tab clobber waiting to happen.
Full Diagonal Range, per stick, on by default. A full diagonal was capped to
the unit circle at ~0.707 per axis, which is what a circular-gated DualShock
really sends -- but games that deadzone each axis separately then ignore
diagonals, and Oblivion's camera crawled diagonally while the cardinals were
fine. Off restores the hardware curve.
Oboe is the default audio backend on Android, with a migration for anyone still
on the old Cubeb default; a deliberate choice of another backend is kept.
Enter Button Assignment (circle/cross) is exposed. The core has always had it
and Android never showed it.
Reset all settings, in General. Per-game overrides and controller binds are
deliberately left alone -- they are invisible from that page.
Oblivion's water did not draw on Vulkan and did draw on OpenGL. The only
Vulkan-only shader workaround in play is the blanket disable of native float16
on every mobile GPU, which emulates it with fp32; its own comment claimed that
"renders correctly", and it does not.
The disable exists for a real failure -- Qualcomm's compiler rejected SPIR-V
containing float16_t and every pipeline came back VK_ERROR_UNKNOWN, which
presents as a black screen with working audio and a working compile overlay,
so it reads as a renderer bug rather than a shader one. That is not worth
reintroducing blind, so this is a version gate rather than a removal:
Adreno on driver 512.676.53 or newer -> native fp16 (verified)
older Adreno -> unchanged
Mali, PowerVR, Xclipse, the rest -> unchanged, untested either way
Found by switching the renderer to OpenGL, which isolated it to the Vulkan path
in one run after the settings-level suspects had all come back empty.
sys_fs_unlink handled notdir and noent but not readonly, so it fell through to
fmt::throw_exception and killed the PPU main thread inside the syscall. The
emulator then sat with nothing to run: the game froze with the CPU at 1% and
nothing in the log but a stalled RSX. On Android /app_home is the mounted ISO,
which is read-only, so any game deleting a file in its own directory hit it --
Oblivion removes warnings.txt at startup and never got past it. Returns
CELL_EROFS now, which is already what sys_fs_write and friends do. sys_fs_mkdir
and sys_fs_rmdir carried the identical block and are fixed with it.
Three log floods, all of which stall the emulator outright because writing them
is not free on Android:
- sys_fs_utime logged two warning lines per call and rides a polling loop.
Oblivion's FileCaching thread hit it 7274 times in ten seconds on one .BSA,
~22k lines, and the frame loop stopped for over twenty seconds. Now trace.
- vm::lock_sudo reported a failed mlock on every mapping. Android never grants
RLIMIT_MEMLOCK to apps, so it fails forever while advising the user to raise
a limit they cannot raise -- 6470 lines in ten seconds here, and ~1200 in
every other game log looked at. Reported once per session now.
- sys_mmapper's map/unmap pair, 12431 lines over the same window. Now trace.
None of them lose information: raise the channel to Trace to get them back.
Testers consistently report the best performance on the build with the 0.6
renderer, so 0.7's graphics work goes back out. The Arkham City measurement
behind it (62.8 -> 51.2 ms) was one game on one device and did not survive
contact with a wider set of hardware.
Two files are kept from 0.7 because neither is render pass work and both are
measured wins on their own: RSXFIFO's idle spin plus WFE park, which took ~11%
of total CPU off sched_yield, and RSXThread's ADPF feed, without which the
performance-hint setting reports nothing and does nothing.
Everything else under Emu/RSX is byte-identical to 0.6. The removed work is not
lost -- it is in c4b45eee2 and can come back a piece at a time with testing
behind each one, which is how it should have gone in the first place.
The previous commit reverted all of Emu/RSX to 0.6, which was more than the
bug required. Bisecting had already shown the render pass work was not
responsible -- reverting it alone changed nothing, while removing retention
with the render pass work in place fixed both reported games.
So only retention goes. It reused vertex cache entries across frames, and on
0.6 the attribute ring was too small for it to engage; raising the ring to
192M switched an existing path on in every game at once and handed draws
stale geometry. Back to purging every frame, as 0.6 did.
This restores what the wider revert had taken out for no reason: the render
pass reduction, the RSX FIFO idle fix, ADPF frame timing, the ZCULL and
occlusion query fixes, the swapchain and surface lifetime ports, and VRAM
budgeting.
Sonic Unleashed still does not render FMV cutscenes. That reproduces with
the 0.6 renderer too, so it is unrelated and still open.
0.7 introduced corruption in several games that were fine on 0.6 -- flashing
and flickering in Sonic Unleashed and Dragon Ball among others. Emu/RSX is
returned to its 0.6 state in full; everything outside the renderer is kept.
The main cause was vertex cache retention. On 0.6 the attribute ring was too
small for retention to engage, so raising the ring to 192M did not add a code
path, it switched an existing one on in every game at once, and reusing stale
vertex data is what the flashing was.
Bisecting also showed the render pass work was not responsible: reverting it
alone changed nothing. It can come back, but on its own and with testing
behind it rather than as part of a batch.
Kept from 0.7: the ARM64 PPU float to integer fix, the PPU cache build
identity, the SPU checksum and block state fixes, Oboe, ADPF, and the crash
and stability ports.
Sonic Unleashed does not render FMV cutscenes. That reproduces with the 0.6
renderer as well, so it is not from any of this and is still open.
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.
FCTIW, FCTIWZ, FCTID and FCTIDZ carried a saturation correction that only
makes sense on x86. cvtsd2si returns 0x80000000 for any value it cannot
represent, so the result is XORed back into 0x7fffffff on overflow.
FCVTNS and FCVTZS already saturate on their own, so the same XOR turned a
correct result into its opposite: every overflowing conversion produced
INT_MIN where it should have produced INT_MAX. The mask is a no-op when
there is no overflow, so it never did anything except break that case.
Armored Core: For Answer put the player under the floor in the tutorial
because a coordinate that should have clamped high arrived clamped low.
The cache needed a build identity as well. Its key is the executable's
SHA-1 plus a settings bitset and nothing more, so the first attempt at this
fix silently reused objects compiled by the previous build and looked like
it had done nothing. Every earlier PPU codegen change had the same problem
for anyone with a warm cache.
Module loading also no longer abandons the remaining modules after one
object fails to load.
Pause reached the core for the first time. Rpcs3Bridge.pause() set a bool and
returned, on the belief that RPCS3 has no explicit pause entry point -- Emu.Pause()
exists and _rpcsx_surfaceEvent has always called it on surface loss, which is why
backgrounding the app was the only thing that paused. Exported as _rpcsx_pause
through all four layers; resume already reached the core, so the pair was asymmetric.
Restart no longer crashes: setCustomDriver dlclose'd the previous driver handle, and
applyRendererPrefs re-applies the driver on every start, so restart unloaded the
library VMA had resolved vkGetPhysicalDeviceMemoryProperties2 out of. ~VKGSRender then
freed its heaps and UpdateVulkanBudget called into an unmapped mapping. An ICD cannot
be unloaded while anything resolved from it is reachable, so it is no longer closed.
Restart no longer returns to the library either: shutdown() set stopRequested, called
kill() and returned with the VM still live, so the run loop's finally started the
replacement and the in-flight teardown killed it -- two BootGame calls, then Unloading
ISO, by which point the restart flag was spent. shutdown() now waits (bounded) for the
core to report Stopped, and the restart is queued on vmStopControl behind it.
FPS cap applies at every value. ConfigStore recorded a persistent core override of
Video@@Frame limit=60 and Settings rewrote it on every push, both from a migration
escaping a stored 120 -- but that node is the cap control, and overrides replay last,
so presets were pinned at 60 while 20 and 45 worked through Second Frame Limit. The
Vblank Rate force stays, since Frame limit Auto resolves to it. Stale overrides are
cleared once. 90 and 120 dropped from the row: the min() in the pacer discards them.
Cover art for PKG installs: the library grid's fallback chain stopped one leg short of
the extracted ICON0.PNG while the in-game menu's did not. Both now share one chain, so
they cannot diverge again. has()/discIconFile require bytes rather than existence, and
the staging rename is checked instead of discarded.
Licences are grouped per game and collapsed instead of a flat list of content ids.
Trophies: a library-wide browser and an in-game tab for the running title, reading
TROPCONF.SFM and TROPUSR.DAT directly -- no account, no network. The in-game set is
identified from the core's own current_trophy_name (try_get, since get<> would
construct it outside emulation and hand back an empty name), falling back to TROPDIR
on disk because a game registers its context lazily. Note the entry stride there is
16 + entries_size, not entries_size.
Also: renderer.upscale.label was defined twice, so Internal Resolution was dead.
PPU: a module that fails codegen no longer takes the boot with it. ppu_initialize2
called the fatal jit.add(); run_recoverable_llvm and the try_* pair already existed
in this tree but were used only by the SPU recompiler, so LLVM's fatal handler threw
on a thread with no recovery context and killed the worker. That is not one lost
module: g_progr_pdone is incremented in the compile loop's INCREMENT, so the module
the dead worker held was never accounted for, g_progr_ptotal could never reach zero,
and the boot waited on it forever. Saint Seiya: The Sanctuary (BLES01421, issue #25)
stopped at 133 of 134 on 'Cannot scavenge register without an emergency spill slot'.
Now routed through try_add on ARCH_ARM64, mirroring the SPU branch, with
ppu_initialize2 returning bool so the caller stops logging a dead module as compiled.
Losing the worker also halved the rate for everything left.
VK: a data_heap block no longer frees through an allocator that is not the current
one. Borrowed pointer, cached at construction with nothing tying it to the
allocator's lifetime; declining the free costs nothing the device teardown does not
already release.
Android: g_strings held 180 of localized_string_id's 323 entries and the callbacks
ignored their args entirely, so every string carrying a name, date, size or error
code lost it -- including CELL_SAVEDATA_LOAD, which is why the save prompt was Yes
and No over an empty message (open_msg_dialog logged msgString=""). All 322 the Qt
switch provides are present, in enum order, with QString::arg's %0 substitution
reproduced and utf8_to_u32string on the u32 path so trophy names survive. A
static_assert on the table size fails the build when upstream adds an id.
probeDiscInfo: set g_fxo up before mounting. vfs::mount lazily constructs vfs_manager
through manual_typemap::init<T>(), which writes *m_order++, and clear() nulls that
when a game stops -- so scanning a new disc image after playing anything wrote
through null. Emu.IsStopped() cannot guard it, because stopped is the cleared state.
libEGL_angle.so and libGLESv2_angle.so lived in armsx3-app, which stopped being the
built module, so selecting ANGLE for the OpenGL renderer silently fell back to the
system driver with nothing in any log to contradict it. Moved into armsx3-ui beside
the core, with the jniLibs .gitignore negations that keep them tracked.
verifyAngleLibs comes with them and now runs on the release graph ahead of
mergeReleaseJniLibFolders, so packaging an APK that offers ANGLE without shipping it
fails the build. The copy left behind in armsx3-app could never have protected
anything from there, and did not even compile -- its GradleException message escaped
'$' as if the file were a template, and the quotes inside the escaped interpolation
closed the string early, so the project failed to configure. Deleted rather than
fixed, with a comment pointing at the live one.
Version to 0.6 (versionCode 10).
Its comment was still there, above the OSD selector, describing a control that no
longer existed -- the row was lost in the port and the setting left with no writer,
so nobody could hide the glyph or bring it back. Reported as the option missing from
the menu, which is what it was.
Goes where the comment says rather than in the touch editor toolbar, which is where
I first put it: this is a pause-button behaviour toggle and belongs with the overlay
controls it was written for.
The setting has existed since the pause button moved to the top right, and is seeded
once from the old show/hide pref so anyone who had the button hidden keeps it hidden.
Nothing ever wrote it afterwards. A user whose button was visible had no way to hide
it and a user migrated into hidden had no way back, which is how it was reported:
the option is not in the in-game menu.
Sits with multi-touch, gliding and floating stick in the editor toolbar, since those
are the other whole-overlay behaviour toggles and setPauseTapToReveal already
existed to be called.
The freeze-with-audio in Ratchet & Clank is the RSX thread dying in the allocator,
and the heap growth log says why. The index buffer went 16M to 64M to 128M to 192M
to 256M inside 290ms, on requests of 2K, 4K, 5K and 3K; the attrib buffer did the
same and died growing to 192M. Kilobyte allocations cannot need a quarter gigabyte.
The rings were never wrapping, they were only ever growing.
frame_context_cleanup is what returns a frame's ring memory, and check_present_status
is what calls it. I removed that call from flush_command_queue in 0.5 because the
drain poked the oldest queued frame's fence and on Adreno vkGetFenceStatus blocks
until signalled instead of answering -- 14.6ms a frame, second only to the FIFO decode
loop. The reasoning was that the flip path retires frames anyway. It does, enough to
keep presenting, but not often enough to keep the rings bounded, and nothing else
reclaims them.
Restoring it costs nothing now. poke() no longer asks with vkGetFenceStatus: it uses
vkWaitForFences with a zero timeout, which is specified to return VK_TIMEOUT without
waiting. The measurement that motivated the removal was of the old implementation, so
the speedup stays and the reclaim comes back.
Keeps the heap growth log that found this. The allocator reports only a size and a
pool number, and pool 1 covers every data_heap, so three fixes were aimed at a target
that could not be seen. One line naming the heap settled it.
Ratchet & Clank freezes with audio still playing, which is the RSX thread dying:
'Failed to allocate 131072K of video memory (pool=1, pool total=561M, heap cap=
2048M)'. Pool 1 is VMM_ALLOCATION_POOL_SYSTEM, and 131072K is a data_heap taking
its second growth step, 64M to 128M.
The device is not out of memory. It holds 561M against a 2048M cap and cannot place
128M in one piece, which is a different failure from being full and has a different
fix. The heap grew by aligning up to 64M, so every growth demands a single
contiguous block of at least that size, and each step doubles what the allocator has
to find unbroken. A heap that fails to grow has nowhere to degrade to, so the
renderer ends there.
Android now grows in 16M steps and stops at 256M. The smaller granularity asks for a
quarter as much contiguous memory per step and lets the heap settle near the size
actually wanted rather than overshooting to the next 64M boundary. The ceiling comes
down to match: a 1GiB upload ring would exhaust the device long before it was
reached, so as written it was a limit only reachable by dying. Desktop keeps 64M and
1GiB.
Does not touch the separate pool-0 case fixed in the previous commit, where recovery
does run and the last-ditch eviction now gets a turn before the thread is killed.
Ratchet & Clank freezes with audio still playing, which is the RSX thread dying on
VK_ERROR_OUT_OF_DEVICE_MEMORY while the rest of the process lives. Caught on an
Adreno 740: 'Failed to allocate 86016K of video memory (pool=0, pool total=472M,
heap cap=2048M)'. One 84MB request refused while we held 472MB of a 2048MB cap, so
the heap was not full -- a single large allocation could not be placed.
The allocator already retries once after asking for pressure relief, and it did.
The relief is what fell short. on_vram_exhausted refuses the hard sync whenever the
RSX is uninterruptible, and clamps the request below fatal, so the eviction that
drops everything unlocked was unreachable from here. That refusal is right while
there is still a way out: eviction touches resources the driver may still be
reading. It is wrong on the last attempt, where the alternative is not a glitch but
the renderer ending.
So the final attempt is now exempt, through a thread-local set only for the width of
that call. Everything else keeps the existing behaviour, and a caller that opted out
of recovery is not handed it here by the back door. Recovery logs at error level and
says a visual glitch is the expected outcome, since a silent recovery that costs
texture quality reads as a new bug otherwise.
Measured against this failure the eviction ran once, six microseconds before the
allocation failed, and never in the five minutes before it -- so nothing was
reclaimed while it still would have been cheap. That part is not addressed here: the
budget-based ladder cannot see mobile unified memory, where the driver reports one
large shared heap and 472MB against it never crosses a threshold. This makes the
failure survivable rather than preventing it.
A heap profile of Ratchet & Clank on an Adreno 740 put the only real growth during
play on vk::descriptor_set: 56 sets created in half a session, 49MB, through
simple_array::reserve from descriptor_set::operator=. Nothing else grew that was
not one-time JIT or shader compilation.
Each set reserves m_pool_size entries in three pools the first time it is used --
16448 image infos, 16448 buffer infos, 16448 buffer views, about 920KB -- and there
is one set per shader program. simple_array::clear() only resets the size, so that
memory is held for the object's whole life, and the total climbs for as long as new
pipelines keep appearing. Ratchet compiles a lot of them.
It is also why this was invisible from the Vulkan side: these are plain malloc, not
device memory, so the VMM never sees them and no amount of texture eviction reclaims
them. The tester's log shows the shape exactly -- our pool at 516MB while the process
walked from 4448MB to 5626MB without ever dropping, then died on a 32MB allocation.
The reservation is a correctness requirement, not a tuning knob: push_*() hands
Vulkan the address of a pool entry and it has to stay valid until flush(), so the
pools must not reallocate while writes are pending. What makes it safe to shrink is
that max_cache_size is also the flush threshold, in both on_bind() and
storage_cache_pressure(), so the queue can never outrun the reservation. Moving it
takes the guard with it and leaves the same 64 entries of headroom.
1024 on Android: about 57KB a set instead of 920KB, for one extra
vkUpdateDescriptorSets per 1024 writes. Desktop keeps 16384.
The vector body stores vorrq(v, eq) into restart lanes, which is all-ones
regardless of what index_limit() returns -- it only matches the scalar
tail's index_limit store because index_limit is all bits set. Assert that
beside the splats so a change to index_limit fails the ARM64 build instead
of silently diverging the vector body from its own tail. No codegen change
(emitted assembly is identical).
Explain why the non-restart loop stays scalar (clang already
auto-vectorizes it) and spell out which allocations each caller passes,
so the no-overlap contract of upload_untouched_neon is checkable.
The primitive-restart variant of upload_untouched had no SIMD path on
ARM64: the asmjit builder is x86-only, and clang cannot auto-vectorize
the scalar loop (-Rpass-analysis: "value that could not be identified
as reduction is used outside the loop") because the min/max updates are
conditional on the restart compare -- while the non-restart loop next to
it does auto-vectorize. Net effect: 16 scalar instructions per index on
a path some titles saturate. Measured on a Snapdragon 8 Elite (Odin 3),
Virtua Tennis 4 routes its entire indexed-draw traffic through this
loop: 2.81 billion indices in a 9-minute match session, median 159k
indices per frame.
Port the x86 lane algebra to NEON, 8x u16 / 4x u32 per iteration: the
restart-equal mask ORs the lane to all-ones for the min accumulator and
the store (all-ones is index_limit, exactly what the scalar loop
writes) and BICs it to zero for the max accumulator, so restart lanes
can never win either reduction; UMINV/UMAXV reduce once at the end and
the tail stays scalar. Baseline v8.0 AdvSIMD only.
Supporting results, all on the Odin 3 with the system driver:
- Correctness: 216-case differential (scalar vs NEON vs the dispatched
path; every tail residue mod 8 and mod 4; restart index absent,
present, 0, index_limit, all-restart, and index_limit present while
not the restart value; u16 and u32) ran on device at RSX init in all
four A/B runs: 0 mismatches. An independent 65,000-case host-side
model of the same lane algebra also matched the scalar loop, and a
blind review of the diff could not construct a diverging input.
- Performance A/B (cntvct_el0 around the dispatch, null-region
calibration subtracted, per-window medians over 120-flip windows with
>10k restart indices/flip, runs interleaved scalar/NEON/NEON/scalar):
scalar: 0.02504 and 0.02464 ticks/index (1.30 ns/index)
NEON: 0.00346 and 0.00381 ticks/index (0.19 ns/index)
~6.8x faster per index; scalar-scalar repeatability 1.6%. Worst
single-frame cost in this loop fell from 3.64 ms to 0.99 ms. FPS
stayed 60/60 in all runs on this device; the win is RSX-thread
occupancy and worst-frame cost, and would be frame time where the
RSX thread is the bottleneck.
Titles that never enable primitive restart are unaffected: they route
through the untouched path, which clang already vectorizes.
vmm_determine_memory_load_severity is a set of thresholds on get_memory_usage,
which is usage/budget straight out of vmaGetHeapBudgets. VK_EXT_memory_budget was
never enabled, so VMA had no budget from the driver and used the heap size -- and
where pHeapSizeLimit is set, that limit, which is our own vram_allocation_limit.
An Adreno 740 log shows the consequence: 516MB against a 2048MB cap is 25%, below
even the 50% mark, so the allocator kept its fastest flags, severity stayed 'low',
and the 75/90/95 eviction ladder never fired. The first allocation the driver
refused was also the first sign of trouble, and that one is fatal.
Enabling the extension gives VMA the driver's own estimate. VMA takes the smaller
of it and pHeapSizeLimit, so the cap still caps -- it just stops being mistaken for
headroom that exists. Gated on support and logged when absent.
NOT a fix for the Ratchet & Clank crash this was found in, and it should not be
credited as one. That log leaks about 60MB per sample, 4448MB to 5626MB with no
drop, while our own pool sits at 516MB -- so nearly all of it is outside anything
VMA can see or evict, and a truthful budget only makes us give up our own memory
sooner. The tester reports 0.4 unaffected, which makes it a 0.5 regression still
to be found; see the Adreno per-vkCmdEndRenderPass allocation already recorded
against this codebase.
Also: licences can be removed. Installing one was one-way -- the row existed only
to prove the install had happened -- so a wrong or duplicate .rap could only be
cleared through a file manager, which on a scoped-storage device most people
cannot do at all. Confirmed before deleting, like uninstalling a title, because
content stops working without it.
Every pressure-capable button was fully digital. _rpcsx_overlayPadData ended with
btn.m_value = m_pressed ? 255 : 0, and that value is what cellPad copies into the
press byte a game reads for an analog button, so no half-press could ever reach
one. Rpcs3Bridge.setPadButton threw the magnitude away before that, using `range`
only for stick directions and calling applyButton -- pressed or not -- for
everything else.
Two features were silently dead as a result. A physical L2/R2 went 0 to 100 like
a digital button, reported on Iron Man, whose level-two hover tutorial cannot be
passed without a half-press; the trigger axis was read and scaled correctly all
the way to the JNI boundary and discarded there, which is why remapping and
recalibrating changed nothing. The touch overlay's pressure modifier had the same
end: it computes a range through pressureRangeFor and hands it to the same call.
Pressure now travels as its own export rather than widening overlayPadData, whose
signature is frozen -- the core is dlopen()ed and updated independently of the JNI
glue, so a wider existing export would have older glue passing a garbage argument.
Glue or core predating _rpcsx_overlayPadPressure keeps the old digital behaviour.
0 means "nothing analog drives this button", which is a safe sentinel rather than
a lost level: an unpressed button already reports 0, so a pressed button at 0
cannot occur, and the zero-initialised array is exactly the previous behaviour.
Pushed only when it changes, so an all-digital pad adds no JNI call per event.
All twelve buttons the PS3 pad reports pressure for, not just the triggers, since
the offsets are contiguous and cellPad already routes each one. sendTrigger also
floors to at least 1: the lightest real squeeze truncated to 0, which is the input
layer's "full press" convention and would have delivered the opposite of a
half-press.
Rebased by the author onto 0.5, so the occlusion-query bound we shipped stays as
it is and this only adds diagnostics on top of it: the fatal throw that ended the
session is gone, and with it the Web of Shadows regression that kept both PRs out
of 0.5. Also leaves the wait on shutdown, so a driver that never answers cannot
wedge the exit.
README.md is deliberately not taken from the PR -- it removed the whole status
and differences-from-upstream section.
adrenotools swallows this failure. When its dlopen of the custom driver fails it
logs to logcat and hands back the system driver, so the load looks successful
from here, the reason never reaches the emulator log, and the user runs a driver
they did not choose while believing otherwise. The existing dlerror() report
cannot fire, because the pointer that comes back is not null.
That cost real time. Mr Purple T29 fails on an Android 15 device with "cannot
locate symbol pthread_getaffinity_np", falls back, and every log looked exactly
like a successful custom-driver session -- I recorded it as passing a driver
comparison it had never taken. The only hint was its reported driver version
matching the system driver's, which took three saved logs side by side to spot.
The requirement is stated in the file. DT_VERNEED lists the libc versions a
binary needs, and T29 needs LIBC_36, meaning API 36, on a device that provides
35. Reading that before the attempt turns "failed to load" into the reason, and
covers the whole class of community drivers built against a newer NDK than the
device runs -- likely the most common way these packages fail.
Metadata cannot answer this: T29's own meta.json declares minApi 30. That field
is author-declared and unverified, so only the binary is trustworthy.
Reported through the emulator log as well as logcat. The UI glue can only reach
logcat, which is not the file anyone attaches to an issue -- the reason would
exist and no report would ever contain it. It lands beside the driver identity
that it explains.
Advisory on purpose. The load is still attempted and nothing is rejected, so a
wrong answer here costs one log line and never a working driver. It stays quiet
unless it positively finds a LIBC_<n> requirement above the running API, and
declines to answer at all when section headers are absent.
Verified on device both ways: T29 reports needing LIBC_36 against API 35 in
RPCSX.log, and stevenmxz v33, which loads correctly, produces nothing.
Upstream now bounds this wait itself: warn at one second, abandon at
three and use whatever the query holds. That replaces the fatal timeout
this commit previously carried, and it is the better answer -- the throw
could end a session over a driver that was merely slow, at worst wrong
culling for a frame was the actual cost. What remains here is the part
the bound does not cover:
- On abandonment, ask the driver once more directly with
VK_QUERY_RESULT_WITH_AVAILABILITY_BIT and log which way it is
stalling: VK_NOT_READY, or VK_SUCCESS with the availability word still
clear. The two are indistinguishable through poke_query and need
different conversations with whoever maintains the driver.
- Leave the loop when emulation is aborting. A driver that never answers
must not also wedge the exit path, and the value is irrelevant once
the session is going away.
Found chasing a Skate 3 freeze on an Adreno 830, where stevenmxz's gen8
driver builds accept occlusion queries and never complete them; the
system driver completes them in microseconds.
The startup log named the GPU and a driver version, and on Android neither
identifies the driver. adrenotools' hook falls back to the system driver when
its dlopen of the custom one fails, and reports that only to logcat, so a
session that silently ran the system driver logged exactly the same thing as
one that ran the custom driver it was asked for.
That is not hypothetical. Chasing a Skate 3 freeze on an Adreno 830 I recorded
a custom driver as passing a test it never took: it had failed to load with
"cannot locate symbol pthread_getaffinity_np", fallen back, and the log still
said the custom driver was bound. The only tell was that its reported version
matched the system driver's exactly, which needed three saved logs side by side
to notice.
Logs the driver identity Vulkan already reports -- name, driverID, info and
conformance version, all of which were being fetched and thrown away -- and
falls back to saying the identity is name-derived when VK_KHR_driver_properties
is missing, which is common on the older Android devices this matters most on.
Where a custom driver was requested and Qualcomm's own driver answered, that is
a silent fallback, since adrenotools installs Mesa/Turnip builds. It now says
so, and points at the logcat line carrying the actual reason.
The loader's own message no longer claims more than it knows: the handle it
binds is the one it was handed, and whether the driver behind it is the
intended one is not something it can see.
Raw core overrides re-push after the curated settings, so a stale one silently
beats the UI with nothing on screen to explain it: the settings screen read SPU
Block Size = Safe for hours while config.yml read Mega.
Mega is the one that mattered. It produces very large compilation units, and those
are what fail AArch64 register allocation with "Cannot scavenge register without
an emergency spill slot" -- which is what put SPU threads on the interpreter
fallback at all. With it cleared, no block fails to compile and the fallback never
engages. Every "cannot be compiled" chased in these sessions traces back to it.
Cleared in every scope, because a title can pin a key the global also pins: Arkham
City carried Accurate SPU Reservations true as a raw per-title override against
false globally, so clearing one scope did nothing and the two readings looked
contradictory.
Per-title Accurate SPU Reservations values go too, except Web of Shadows, which is
the title it was measured on. Off is off-spec -- it forces the SPURS scheduler to
HLE and bypasses the reservation lock -- and a title left that way desyncs until
its SPU threads execute whatever they land on, which is how Arkham City ended up
dying with "Unknown STOP code: 0x0".
Adds CoreSettingOverrides.forgetEverywhere for the all-scopes case.
3072 was set to get the God of War 3 demo past an allocation failure, but that
failure was measured before the uninterruptible reclaim fix landed, and the cap is
not coordinated with the texture cache, which budgets itself up to 2560MB on
Android. Raising one without the other let the total grow with it: Batman: Arkham
City reached 5596MB resident against a 6246MB peak on a 7.2GB device and stalled
after a while, with no allocation failure to point at.
2048 is the value that shipped before, and it is where the sum of the two sat when
that game worked. Budgeting the cap and the cache together is the actual fix and
is not attempted here.
The RSX profiler was still recorded as a raw core override from the debugging
work, so config.yml read "RSX Profiler: true" while nothing in the UI said so --
the same divergence as the relaxed-ZCULL one, since overrides re-push at the tail
of applyTo. It writes a bucket report every 300 frames and keeps per-scope timers
on the RSX thread, which is not something to ship enabled. The first purge had
already marked itself done, so this takes a new key.
VRAM allocation limit is applied as VMA's pHeapSizeLimit, which makes it a hard
ceiling rather than an eviction threshold: once total allocations reach it VMA
returns OUT_OF_DEVICE_MEMORY however much the device has free. Lowering it does
not make the cache release earlier, it makes allocation fail earlier. The God of
War 3 demo was measured failing a routine 24MB request at 1024 while the process
held 1.6GB resident and 280MB in that pool, and failing at 2048 one screen later.
3072 leaves the caches room while keeping the bound that stops an unbounded quota
driving the process to 4.3GB and getting it killed.
The allocation failure now names the request size and the cap alongside it, since
"Out of video memory" alone cannot separate a full device from an artificial
ceiling, and those need opposite fixes.
Refusing outright skipped the allocator's own recovery. That path is "if
OUT_OF_DEVICE_MEMORY and vmm_handle_memory_pressure(...) succeeds, retry the
allocation", so returning false meant the retry never ran and the allocation died
having freed nothing: God of War 3 reached it with zero reclaim attempts and zero
recoveries logged.
Only the fatal branch needs the queue idle, which is what the flush inside it is
for. The rest is reachable while uninterruptible: the texture cache purges its
unreleased pool, and at severe it also drops unlocked sections. RPCS3 already runs
exactly that with no flush whenever pressure is non-fatal, so this is the existing
contract rather than a new risk. Severity is clamped below fatal so the
flush-dependent path stays unreachable.
Measured after: eviction runs and reports releasing resources, and the allocator
retries. God of War 3 still fails, but now for the honest reason -- the device is
out of memory, with 123MB free of 7.2GB and the emulator resident at 4.3GB -- and
not because nothing was ever given the chance to run.
on_vram_exhausted asserted that the renderer was interruptible. Eviction really
cannot run in that state, since it would touch resources the driver may still be
reading, but that is a reason to refuse rather than to kill the thread -- and
refusing is already the supported answer: the OOM path in VKDraw treats false as
using placeholder textures, which it notes can cause graphics glitches but
should not crash otherwise.
God of War 3 hit it by skipping the intro screens, which pushes a burst of surface
and texture allocation through a point where the renderer is uninterruptible. The
RSX thread died there, audio kept playing, and it presented as a hang. With the
refusal in place the same run reports the real problem instead:
VK_ERROR_OUT_OF_DEVICE_MEMORY from the allocator.
Which it genuinely is. VRAM allocation limit was also lowered from 2048 to 1024:
the first value was still above what the device could give us -- 5355MB resident,
99MB free of 7.2GB -- so the budget was never reached before the system ran dry,
which defeats its only purpose. It has to sit below what allocation can actually
satisfy, so eviction starts while there is still room to allocate.
Three things, all found by measurement after the interpreter fallback started
being used in anger.
Marking only the entry point made the interpreter release the thread after one
instruction, whereupon the recompiler tried the next address, failed the same way
and marked that too. 111 consecutive entries were recorded walking two blocks four
bytes at a time, each step paying a full failed LLVM compile. The failed set now
holds ranges, so a thread stays interpreted for the whole block and leaves when
execution genuinely moves past it: 111 markings became 1.
The range test then ran per interpreted instruction and took a reader lock each
time, which put shared_mutex::imp_lock_shared at 28% of the whole process against
23% for the interpreter itself. The extent is now cached on the thread when the
fallback engages, so the loop compares two integers.
The switch was also logged once per thread, but the flag is cleared on every exit,
so the guard fired on every re-entry: God of War 3 wrote thousands of lines a
second ping-ponging between two addresses. Removed; the block is still recorded
once when it is marked.
Separately, VRAM allocation limit was left at upstream's 65536 MB, which means no
limit and assumes a discrete card. Here the GPU shares system memory with the OS
and our own host allocations, so the texture cache is never asked to evict and
grows until allocation fails -- and failing is fatal: God of War 3 dies in
on_vram_exhausted on ensure(!vk::is_uninterruptible() && ...), because VRAM ran
out where the renderer cannot safely evict. Measured at the crash: 5355MB
resident, 99MB free of 7.2GB. 2048 leaves room for the guest's own memory, the
host caches and the OS.
The fallback flag was set once and never cleared, so a thread that met a single
block it could not compile interpreted everything it ran from then on. Correct,
but these are SPURS kernels doing real work, and Sonic Unleashed reached its
loading screen that way and then crawled through it.
The failed set holds entry points, so this keeps interpreting while pc sits on the
bad entry -- which is where a branch-to-self idle loop stays -- and releases the
thread as soon as execution moves past it. Only the block that cannot be compiled
is interpreted; the rest of the thread runs recompiled.
Leaving is safe at any instruction boundary, since all SPU state lives in
spu_thread, which is the assumption the JIT dispatch already makes. Re-entering
the bad block sets the flag again.
A block that fails to compile switches its thread to the interpreter. On ARM64
that fallback called spu_runtime::g_interpreter, which with a recompiler selected
is the LLVM-built interpreter, and calling it there executes nothing: measured a
million consecutive calls on Sonic Unleashed's stuck SPURS kernel without pc
moving once. The thread then spins in that loop forever at a fixed pc with no
flags set, which reads as a busy SPU and hangs the title with no diagnostic at
all. Any block that fails to compile landed there, so this was not one game.
old_interpreter is what the static decoder ultimately runs, through
tr_interpreter, and it is self-contained -- opcode table, thread, local store. Its
static-decoder-only check rejected exactly the case that needs it, so it now also
accepts a thread already marked for fallback.
Getting there also needed the give-up paths fixed: the TBL2/TBX2 retry could
return null with an empty error and fall through every branch unmarked and
unlogged, so nothing recorded that a block had been abandoned.
The stall dump now carries SPU event, MFC and interrupt state, which is what made
this findable: parked kernels showed pending=0 (no lost wakeup), intr_en was 0 on
healthy threads too (not interrupts), mfc_q was 0 everywhere (no stuck transfer),
and interp_fb=1 on the frozen thread pointed at the fallback itself.
Both waits in writer_lock are unbounded and silent. The acquire loop spins until
every range lock bit clears, and the range_lock path then spins until every
registered PPU thread reaches cpu_flag::wait. A thread that never gets there hangs
every other thread that takes a reservation, and leaves nothing behind: from
outside it reads as a clean guest deadlock with everything in a legitimate wait.
Both now log once, far past any plausible contention, naming the held range locks
or the PPU thread being waited on.
They paid for themselves immediately on Sonic Unleashed, which deadlocks at the
SEGA logo. Both stayed silent across several boots, which ruled out the VM lock
entirely -- worth having, since main_thread was pinned in cellSpursRemoveWorkload
carrying cpu_flag::memory without cpu_flag::wait, which looks exactly like this
bug and is not. The game hangs in a different state on different boots, so it is a
race elsewhere in SPURS.
Issue #16, both halves.
Installing a .pkg or .rap off a USB-OTG drive already went through the system
picker, and the descriptor it returns is handed to the native installer as a raw
fd, so a 40 GB package costs no copy. That holds only while the provider is
backed by real storage. The third-party USB-OTG and cloud apps people reach for
when the platform will not mount their drive return a PIPE, and every install
entry point seeks -- getFileType sniffs the magic and rewinds, package_reader
jumps around the archive -- so lseek failed with ESPIPE and a perfectly good
package was reported as unsupported or broken. Those descriptors are now
detected with the same lseek the core will make, and only those are copied to
real storage first, onto whichever of the emulator's own storage and the app
cache has more room. The copy is checked against the size the provider reported:
a short copy does not throw, it produces a truncated package that fails much
later as "broken", which reads as a bug report about the package.
Split releases picked through the system picker arrived in the order the user
tapped them, and installSplitPkg takes the order given as the part order, so
picking part 2 first extracted into a broken install rather than failing. Both
pick paths now sort the parts, digit runs numerically, since plain string order
puts part 10 between part 1 and part 2.
Installed titles were listed by title id alone -- NPUB90434, BLES01807 -- next
to an Uninstall button, which is where it hurt most: choosing which of two demos
to reclaim space from meant looking the ids up elsewhere. TITLE now comes out of
the install's own PARAM.SFO, read off disk rather than through the library cache
so a title the scanner has not seen yet is still named. The id stays on a second
line because patches, cheats and compatibility lists are keyed by it. Licence
files carry the same id inside their content id, so a .rap can name the game it
unlocks instead of being one of a row of indistinguishable hex strings.
The SFO field reader is the scanner's CATEGORY reader generalised rather than a
second copy of the 16-byte index-entry layout.
Accurate SPU Reservations off is worth a large amount in Spider-Man: Web of
Shadows and is not safe globally, so it goes in that title's own override rather
than the default.
Its SPURS reservation traffic serialises behind the global exclusive
vm::writer_lock that every reservation_op takes, which no amount of CPU can help:
all six SPU threads and several PPUs were measured yielding at the same rate with
18.8% of total CPU in sched_yield. Off, SPURS takes the lock-free path and
vm::writer_lock fell from 8.06% to 0.96%.
Kept per-title because it is off-spec -- upstream defaults it on, and Sonic
Unleashed fails EARLIER with it off, reaching neither the loading icon nor the
logo, which is consistent with the bypass being the SPURS area itself.
The seed writes only fields a title does not already carry, so a deliberate
change is never overwritten, and it runs once. Other regions of the same game
need their own entry.
Two things, both about the RSX waiting rather than working.
flush_command_queue ended by draining the present queue in case a queued frame
still held a ref to the command buffer just taken. It cannot: next() hands them
out from a 512 entry ring and the queued list is bounded at flip to
m_max_async_frames - 1, so the buffer being reused is hundreds of frames retired.
The guard was unreachable and the cost was not -- check_present_status pokes the
oldest queued frame's swap command buffer, and on Adreno vkGetFenceStatus blocks
until signalled rather than returning VK_NOT_READY, so a poll written to be cheap
became a full GPU sync. 1.32 times a frame at about 11ms: Fence poll 14.6ms ->
0.033ms, frame 44.5ms -> 36.6ms. Same fault as the two sites removed earlier;
this one sat inside flush_command_queue rather than on the present path. Ruled
out first: identical frame time at quarter resolution, and forcing the swapchain
pre-transform to match the surface left it unchanged.
The empty-ring yield is now charged to idle. It sits inside fifo_decode, which is
the enclosing scope of the whole run loop, so waiting on an empty ring was
reported as decode work -- Idle 0.003ms against FIFO decode 20.4ms, while a
native profile of the same thread put 34% of its cycles in sched_yield. The
bucket report and the profiler disagreed and the bucket report was wrong, which
has now produced two wrong conclusions in one session.
The 50us backoff in the FIFO_EMPTY path was added when the RSX thread was
measured spending 66% of its cycles in sched_yield on a machine starved for
cores: the affinity mask confined six SPU threads to four cores, and the
reservation path serialised everything behind a global lock, so a spinning RSX
took a core from threads that needed it.
Neither holds now, and the trade inverted with them. Measured after both were
fixed: 34% of eight cores busy, two to four threads runnable, five idle. Nothing
wants the core the sleep gives back, and the RSX sits on the frame's dependency
chain, so sleeping only delays it noticing the guest has produced work.
Also log the surface transform at swapchain creation. 30% of the frame is now in
check_present_status waiting on acquire_next_swapchain_image, which is not GPU
work -- a quarter-resolution run measured the same frame rate. Declaring IDENTITY
while the surface is rotated hands the rotation to the compositor, which can hold
images longer before releasing them for acquire. Logged rather than changed:
matching currentTransform means applying the rotation ourselves across the blit
and the overlay pass, and that is only worth doing if the two actually differ.
Dropped to 20 while the emulator was starved for cores, reasoning that a spinning
SPU steals a core from threads doing real work. Two things have changed
underneath that: the affinity mask no longer confines six SPU threads to four
cores, and the reservation path no longer serialises everything behind a global
lock. Measured after both, in game: 34% of eight cores busy, two to four threads
runnable, five idle, and no thread near saturation.
Tested at 100 and at 20 with no difference, which fits -- the wait is no longer on
the critical path, so how it waits does not matter. Upstream's value stands
rather than carrying a divergence that buys nothing.
The affinity migration turned the scheduler on so the big.LITTLE mask would
apply, keeping SPU and RSX off the A510s that run at roughly 27% of prime-core
capacity. That reasoning holds for one thread per core and breaks down at six.
Measured in game on a Snapdragon 8 Gen 2, reading the masks the threads actually
carry:
app cpuset (top-app) 0-7 Android grants every core
SPU[0..5] 3-6 six threads, four cores
rsx::thread 3-7
Six SPU threads sharing four cores get about two thirds of a core each, which is
worse than one thread owning an A510 outright, and it caps the emulator: the
device sat at 60% busy with cores 0-2 idle while frames were slow. Spider-Man:
Web of Shadows is visibly better at OS.
Worth being clear that the mask is ours and not Android's -- the app is in
top-app with all eight cores granted -- which is also why these devices are
reported to run better under native Linux, where no such policy is applied.
The other modes stay selectable for anyone whose device disagrees.
A DualShock 3 sends pressure bytes in every packet. The press setting governs how
much of the buffer the game is told is valid, not whether the pad produced the
values, so clearing the area diverges from the hardware: a game that reads a
pressure byte without having asked for press mode gets 0 where it would see a
press on a console.
Spider-Man: Web of Shadows does exactly that for R2. Tracing both entry points
showed it never calls cellPadInfoPressMode or cellPadSetPressMode, so the setting
stays at 0, yet it reads the R2 pressure byte to decide whether the trigger is
held. R2 did nothing in that game while every other button worked, from the
controller and from the touch overlay and after remapping to a different physical
button, because the digital bit was delivered correctly the whole time and was
never what the game looked at.
len is unchanged, so a game that honours it sees what it saw before.
The out-buffer check answers 'unlikely to be a loop', and that answer is not
free. It resets the spin count and leaves the busy-waiting switch at umax, so the
caller skips busy_wait, skips the sleep path, and returns immediately: the SPU
re-executes GETLLAR at full rate with no backoff. The spin count never reaches 4,
so the spin optimisation is never evaluated, and the 400ms fallback that would
force a sleep is never reached either. One SPU in that state holds a core flat
out, and the setting meant to control this has nothing to act on.
Spider-Man: Web of Shadows sits in that case. Its GETLLAR sites use an LSA in the
top 64K of local store, which is what the check looks for, and process_mfc_cmd
measured 55% of all CPU across the process while the game ran at 10-15fps.
Re-entering the same site with the same stack 32 times is itself the evidence
that it is a loop, whatever the LSA looks like. After that the verdict is dropped
and the normal spin detection decides between busy-waiting and sleeping. Any real
change of site or stack resets the count, so a genuine OUT buffer still gets the
original treatment.
SPU GETLLAR Busy Waiting Percentage defaults to 100 upstream, meaning always
busy-wait. That suits a desktop, where the SPU threads have cores of their own
and spinning costs nothing else. Here six of them share eight cores with the PPUs
and the RSX, so a spinning SPU takes a core from the threads doing the work.
Measured on Spider-Man: Web of Shadows: process_mfc_cmd accounted for 55% of all
CPU across the process, and making its inner loop cheaper did not move the frame
rate -- the loop just ran more iterations in the same wall clock. That is what
identified it as a spin rather than as work, after two rounds of optimising the
iteration itself.
20 still favours a short busy-wait, so a reservation that frees quickly is caught
without a scheduler round trip, and only a wait that history says is long goes to
sleep. A deliberate change in All Core Settings still wins, since core overrides
replay after this.
Gating the check on getllar_spin_count was not enough. That counter is reset from
several other paths, so it is frequently zero and the callstack was still rebuilt
constantly: measured 19.6% of all CPU inclusive in dump_callstack_list, the
largest single item after process_mfc_cmd itself.
Key on the values the answer actually depends on instead -- pc, the stack pointer
and the link register -- and recompute only when one of them moves. Only the
innermost frame is ever used, so that is all the memo keeps.
A stale answer across an unrelated LS write is acceptable here. This decides only
whether the address looks like a caller's OUT buffer, on a heuristic whose own
comment calls it 'unlikely to be a loop'.
The out-buffer check in the GETLLAR spin detector rebuilt the callstack on every
iteration of a busy-wait loop. dump_callstack_list walks the stack and calls
is_exec_code for each candidate, which allocates a vector<bool> and scans for
branch targets, so the cost is large next to what it decides.
On a whole-process profile of Spider-Man: Web of Shadows those three came to
about 14% of all CPU -- more than the RSX thread spent on the frame -- because
the game's SPU code spins on GETLLAR with an LSA in the top 64K of local store,
which is exactly the case the check looks at.
Once per sequence is enough. pc, ch_mfc_cmd.lsa, gpr[1] and addr are all compared
against the previous iteration a few lines above and any change resets the
sequence, so the callstack cannot move underneath a spin.
sched_yield does not idle a core. With every core already busy it returns almost
immediately and the RSX thread takes it again, running flat out producing
nothing. A native profile of Web of Shadows put 66% of this thread's cycles in
sched_yield and its kernel path against 9% in run_FIFO, which the bucket profiler
reports as a busy RSX because the yield happens inside the fifo_decode scope.
That is not free even with an empty ring. The RSX affinity mask covers the whole
fast cluster while the SPU mask is that cluster minus the prime core, so any of
this that lands off the prime core is taken from the SPU threads, and those are
what the frame is actually waiting on at 61% of all CPU.
The spin still runs 64 times before sleeping, so a producer that is merely slow
is met without a scheduler round trip. Only a ring that has genuinely gone quiet
reaches the 50us sleep, which is far below the frame times where this matters.
Android only.
A query that begins inside a render pass instance has to end inside that same
instance. Ending the pass underneath an open one leaves it permanently
unavailable, and on Turnip it takes the device with it, reported later against
poke_query because that is the first call that waits on a result.
Queries do begin inside render passes here. VKDraw only lifts them out when
use_strict_query_scopes() is set, and that is wired to Strict Rendering Mode, a
user performance setting rather than a driver quirk, so it is off for almost
everyone.
Twenty-one call sites end a render pass and only one, in VKDraw, ever paired
itself with a cleanup. change_image_layout alone ends 41 passes a frame in Web of
Shadows, and any of them can land while a query is open, which is why fixing the
two sites in the query pool moved the device loss from one minute to nearly three
rather than removing it. Holding the invariant in end_renderpass covers all of
them, including any added later.
The VKDraw site now cleans up before the pass ends rather than after, which is
the order the spec asks for; its own call becomes a no-op.
The migration that turned relaxed ZCULL on recorded it twice: once in the curated
store and once as a raw core override. The migration that turned it back off only
corrected the curated field, so the two stores disagreed, and the override is the
one that reaches the core -- overrides re-push at the tail of applyTo, after the
curated store has written the setting.
The toggle therefore read OFF while config.yml read 'Relaxed ZCULL Sync: true' on
every boot, with no way to change it from the UI. That is not cosmetic: relaxed
sync is what allows queries to be read while still pending, which is the path
behind the 'Dubious query data pushed to cond render' warnings, and it also
selects emulated predication in the VK backend.
Emulated conditional rendering exists for hardware that never had the extension.
Turning the extension off as a driver workaround enabled it by accident, because
both are selected by the same test, and the two halves do not fit together:
begin_conditional_rendering returns early without building m_cond_render_buffer,
while the vertex shader still reads that buffer at offset 0. It gets a zeroed
scratch buffer, predicates every draw away, and the game renders black with audio
and overlays still running. The all-ones word that disables predication sits at
offset 4 and is never reached, since the fallback leaves hw_cond_active set.
Off on these drivers means occlusion results stop culling draws, which is the
trade the workaround already documents.
The vendor comes off the GPU rather than from get_driver_vendor(), whose cached
value is not assigned until later in the same function and would still hold the
previous device's.
The existing gate covered only the proprietary driver and said Turnip was left
alone until there was evidence about it. There is now.
Web of Shadows loses the Vulkan device about a minute into gameplay on Turnip 26
/ Adreno 740. The assertion names poke_query, which is the first call that reads
a result rather than the one at fault. Conditional rendering is the only place we
record vkCmdCopyQueryPoolResults with VK_QUERY_RESULT_WAIT_BIT, and that form
makes the GPU block until the query resolves, so a query that never resolves
hangs the device instead of the caller and the watchdog ends the session. The
same run logged 169 'Dubious query data pushed to cond render' warnings, which is
this code being handed queries that are still pending.
It is also the churn: the aggregation barriers closed 42 of the 91 render passes
in a measured frame, and ending a pass on a tiler costs a tile store and reload.
Both drivers now fall back to thread::begin_conditional_rendering, the path
desktop already takes wherever the extension is absent.
Fixing the device loss by ending the render pass before vkCmdCopyQueryPoolResults
introduced a hang in its place. A query that begins inside a render pass instance
has to end inside that same instance; ending the pass underneath an open one
leaves it permanently unavailable, so get_query_result spins on poke_query with
no way out and the RSX thread stops.
Nothing reported it. The submit-time ensure() only checks that the query was
closed, and end_occlusion_query closes it a moment later, so the assert passes
while the result never arrives. The stall detector runs from do_local_task in the
FIFO loop, which the spin has already left, so the profiler charged the wait to
FIFO decode and the frame read as CPU-bound -- 93% in a bucket that was really
the thread sitting in sched_yield. Web of Shadows locked up this way after
reaching gameplay, audio and vblank still running.
Both sites that end a pass from the query path now close an open query first,
which keeps begin and end within one pass. The query is cut short, as it is
anywhere do_query_cleanup is used.
The wait itself is now bounded as well. It warns at one second and abandons at
three, using whatever the query holds: wrong culling for a frame is a better
failure than a thread that never returns, and the log names the cause.
The drain fix made every path that can idle or block publish GET immediately,
which is required for correctness: a producer waiting on ring space needs to see
the progress we made before we stopped consuming.
It publishes far more often than that requires. The empty and busy cases return
straight to the run loop, so a ring that has gone quiet re-enters them once per
iteration with GET unmoved. Web of Shadows measured 137000 loop iterations per
frame against 46000 method dispatches; the remaining 91000 were republishing a
value the guest already had.
GET shares a 64-byte line with put, which the guest PPU writes from another
cluster, so each of those is a coherence miss taken against the thread feeding
the ring. The cost lands on the producer rather than on the RSX, which is why it
presented as a freeze with sound still playing: the PPU stalls on the contended
line while threads that never touch it keep running.
GET is ours to write, so tracking the last published value and skipping an
unchanged store keeps the guarantee -- progress is still announced exactly once
after the last advance -- without the repeats.
vkCmdCopyQueryPoolResults must be recorded outside a render pass instance. This
recorded it inside one, with a comment saying we are technically supposed to stop
the pass first but that it does not matter on IMR hardware. It is not a
technicality -- inside a pass it is undefined behaviour, and a desktop GPU
tolerating it says nothing about a tiler.
This device lost the Vulkan device over it. The fault surfaced later, in
poke_query, because that is the first call that waits on a GPU result, so it read
as the query READ being at fault when the damage was done at record time. Only
the RSX thread died, so the process kept running with audio and vblank alive and
it presented as a hard freeze rather than a crash. Verified gone: zero device
losses on a run that previously died within a minute.
The pass is ended only when one is actually open, on a path that already stalls
for a GPU result, so the flush the upstream comment worried about is paid where
we were blocking anyway -- and disabling occlusion queries is not the
alternative, measured here at 80ms frames with broken visuals.
The two by-pass tables were joined on counters that reset at different points.
tick_frame runs from on_frame_end, before flip; the GPU timer rotates its slot at
the top of flip and then drops every non-frame region on the fresh slot, which is
flip's own overlay and calibration passes -- and those still incremented the CPU
counter. So the CPU ordinal ran ahead by the number of present-path passes and
the two tables described different passes. A whole anomaly came out of that: a
pass whose GPU cost was joined to a neighbour's workload read as 36x the per-draw
cost of its peers. The comment claiming both reset on the same boundary was
wrong. Reset where the GPU slot actually rotates instead.
Also adds a Storage Access Framework route to the package installer. The in-app
browser walks java.io.File, which only reaches storage this process can open by
path, so a .pkg on a USB-OTG drive or an SD card was unreachable and had to be
copied to internal storage first. Packages are handed over as the descriptor SAF
already returned -- the native side takes a raw fd, so nothing is copied and a
4 GB package costs no extra space; licences are 16 bytes and their installer
wants a real file, so those alone are staged.
The low-memory serialisation added three days ago holds a std::mutex across the
LLVM compile itself. A worker that hits LLVM's fatal handler leaves through
pthread_exit, and bionic unwinds nothing on that path, so the mutex stays locked
by a thread that no longer exists and every remaining worker waits on it for the
rest of the session. Memory only falls as modules accumulate, so the tight-memory
branch is likeliest late in a run -- which is why it reads as the PPU cache
getting stuck at the very end, and why dropping to the interpreter avoids it.
Reported as Saint Seiya: The Sanctuary never finishing its module cache. A claim
taken by compare-and-swap and waited on with a timeout costs a stranded claim a
wait rather than the session; the memory back-pressure either side of it is
unchanged. Third time this fork has been bitten by an unbounded wait around a
thread bionic can kill without unwinding.
overlay_audio.cpp already accounts for a platform with no video source; the same
ensure() was left in overlay_video.cpp. Android's make_video_source returns
nullptr, and overlay_save_dialog builds a video_view for EVERY entry on all three
of its paths, so opening a save list aborted as soon as there was one save to
draw. It presents as the save menu never opening -- reported against Ratchet &
Clank: Tools of Destruction and Devil May Cry 4, and against Web of Shadows,
which stalls only once a save exists to be listed. Bundling the overlay icons
was necessary but not sufficient: the dialog still could not survive drawing.
The still image is what an entry needs; the animated ICON1.PAM is the part no
backend here can supply. Also dumps SPU thread pc and block hash alongside the
PPU dump when frames stop, which is what named the SPURS kernels as idle rather
than spinning in guest code.
overlay_controls.cpp loads a fixed set of PNGs -- button glyphs, save.png,
new.png, spinner -- through fs::get_config_dir() + Icons/ui/. Desktop ships them
beside the binary; nothing put them on Android, so every load failed and the log
said so on each boot. The visible cost was cellSaveData's list: it is a native
overlay that draws its rows with save.png/new.png, so the load-save menu a game
opens never appeared. Reported against Ratchet and Clank: Tools of Destruction
and Devil May Cry 4, both fine on emulators that ship the icons.
Bundled from bin/Icons/ui and staged into config/Icons/ui once, revision-guarded,
before the core can draw its first overlay.
The nineteen translation files were ARMSX2-era: about nine hundred of their keys
still existed and showed the old PS2 wording -- worse than the English fallback,
which is at least right -- and roughly eight hundred current keys had no
translation at all. Regenerated all nineteen from the 1041-key map, batch plus
per-line retry, with every %s/%d checked against the source so no broken format
string ships. A string that would not translate is omitted and falls back to
English rather than shipping wrong.
The memory-card and PNACH patch screens were PS2 concepts with no PS3 counterpart
and no caller left -- the drawer had already been cleaned, so they were dead code
holding dead strings. The PNACH downloader went with its only consumer, and the
PCSX2-Android.ini seed could never exist under this package. The session log now
announces ARMSX3_INIT instead of PCSX2_INIT, which had every bug report opening
with the name of a different emulator.
The English map drops 46 PS2 strings and 619 orphans nothing references (1705 ->
1041 keys), rewords the four live strings that still said memory card, and renames
about.pcsx2.* to about.rpcs3.* to match what they already said.
A guest that stops progressing presents nothing further, so whatever was drawn
last stays on screen for good. When that frame held the boot progress bar it read
as stuck compiling at 1s remaining, and it looked identical across five unrelated
faults -- it sent every report of them to the wrong place, including this week's.
Nothing contradicted it either, since the emulator has not crashed and logs no
error.
Reports once, to the log and to the screen, after thirty seconds with no frame
and nothing claiming to be in progress. The progress text is what separates
working quietly from stopped: a shader or PPU compile presents no frames for
minutes and holds a dialog saying so. Drawing it needs the native UI flip,
because the guest is not flipping -- which is the point.
GET goes out on a bounded lag, every eighth packet, to keep a cross-cluster
coherence miss off the per-packet path. That is only sound while more packets are
coming to flush it, and the paths that can block were given a forced publish for
exactly that reason -- but the one where the ring runs dry was not, and it is the
one where nothing further will ever flush it.
The guest reads GET to see how far the RSX has consumed. Draining the ring left
it up to seven packets behind with no more packets to publish, so the guest waited
on progress that had already been made and never announced.
It presents as a boot or a load that hangs with the RSX perfectly healthy and idle,
every guest thread in a legitimate wait, and no error anywhere: bisected to this
across six rounds after five wrong theories, because nothing is broken at the point
it stops. Only bites when the packet count is not a multiple of eight as the ring
drains, which is why it was game- and timing-dependent and why the same title could
boot yesterday and hang today.
Publishes on both paths that leave the consume loop. The lag stays.
The RSX-side stall report says what the RSX is doing, which on every hang chased
so far has been idling while the guest waits -- and nothing said which guest
thread or what it was in. The syscall stats name the syscall without the caller,
and a thread that has not started reads from /proc exactly like one that is
blocked.
One line per PPU thread with its name, state, PC and current function, on the
same condition and cadence as the RSX report. Reads the id map unlocked on
purpose: this runs on the RSX thread, and taking that lock to diagnose a hang
would add the kind of dependency being diagnosed.
The profiler arms and reports only from on_frame_end, and dumps once 300 frames
have accumulated, so a boot that hangs before presenting left it switched off and
silent however the setting was set -- the one case where what the RSX thread is
looping in is the whole question. Armed and polled from do_local_task as well,
which the FIFO loop reaches whether or not frames advance.
The vblank thread is the only source of the interrupt gcm waits on, and said
nothing about being alive, blocked or gone. A heartbeat and an exit reason
separate those, which are three different faults that look identical from
outside: on a Demon's Souls boot it delivered about 120 vblanks and then parked
in the send path with the queue undrained.
Waiting on the claim was untimed, so a waiter that missed the owner's transition
waited for the rest of the session, and the duplicate waiter could only leave on
the failure state -- an owner that published state 2 without publishing a
function left it waiting on something that was never coming.
SPURS brings all of its kernels to the same block at once, so this is four
threads at a time, and the PPU then blocks on SPUs that never answer. Measured
during one: the PPU thread took no CPU at all across eleven minutes while the
SPU threads churned two-to-one system time.
Both waits are bounded now and the duplicate leaves when the owner has finished
and published nothing.
Auto-save on exit, auto-load on boot and the interval auto-save were ARMSX2 shims
returning false that were never ported, so all three toggles persisted and read
back while doing nothing -- the interval job woke on schedule to call a function
that always failed. They now use a reserved slot above the ten the picker shows,
reusing the numbered-slot path rather than growing a second mechanism.
Import treated getGamePathSlot as a file path, but it answers with the title id:
File(id).exists() was false for every slot, so the first OCCUPIED slot read as
free and the destination resolved against the process working directory. It
copied the file nowhere useful and reported the slot it had not written.
Occupancy now comes from the core and the destination from the real path.
Also says how large a state is before the storage bill arrives, and stops the
interval description promising a pause when a PS3 save is a stop and a reload.
The reader rooted the path at systemDirPosix(), which is null unless a data
folder was explicitly picked, so on a default install it returned before it ever
looked and every slot drew as a blank tile with the thumbnail sitting on disk
beside the state it belongs to. Falls back to getExternalFilesDir, which is what
fs::get_config_dir() resolves to on that install and where the writer put it --
the same fallback inputProfilesDir() makes for the same reason.
CallFromMainThread without a wake_up is a post: upstream hands the callback to
the GUI thread and returns. This ran it inline instead, under whatever locks the
caller held.
lv2_obj::sleep_unlocked posts one while holding lv2_obj::g_mutex, which is what
the comment on that call site is about. The callback is FinalizeRunRequest, the
wake for a restored savestate, so it took g_mutex against itself and every thread
stopped there: the log reaches Final Thread and goes quiet with the SPUs spinning
and the progress overlay frozen on its last figure. It took out loading a state
and saving one alike, a save being a stop and a restore.
Callers passing wake_up are waiting on completion and still run inline.
The hash it changed is one of several computed over the same function data, and
only this one moved, so the cache rebuild it forced ran through a path whose
other sites disagreed. Loading a state then wedged in Building SPU cache with
nothing compiling.
Reuse of blocks across a change of the setting is still wrong, but it is an
upstream behaviour that predates this and is better addressed by invalidating
the object cache once when the mode changes than by moving one hash out from
under the others.
already_added reports that the title was already in games.yml, which is the
normal case for anything booted once before, and RPCS3's own front-end passes it
through for that reason. The bridge failed every result that was not NoErrors,
so the boot was abandoned and the user was returned to the library with "Game
failed to start: AlreadyAdded".
The bridge translates each section it knows and returns false for the rest, and
there was no Savestate case, so the write was dropped on the floor. The setting
could not be turned on at all -- not by the default, not from the settings row --
and savestates failed to lock the SPUs while telling the user to enable exactly
the option that was being discarded.
The setting changes the code generated for blocking channel reads -- the GPRs
are stored rather than the thread being marked unsavable -- but the cache key
was a hash of the guest code alone, and the compiled object is cached under it.
A block built in one mode was therefore reused unchanged in the other, so
turning the setting on left the old unsavable blocks in place and savestates
went on failing to lock the SPUs with a message telling the user to enable a
setting they had already enabled.
Mixed in only when set, so caches built in the default mode stay valid.
A save that fails with "missing SPU setting" reads as broken rather than as a
setting waiting to be found, and the setting is not one a player would think to
look for. Upstream defaults it off to protect SPU performance; here the feature
not working at all is the worse trade.
Costs are unchanged and still stated on the switch: it slows the SPUs while it
is on, and a PS3 state runs 500MB to 3GB. Turning it off restores upstream
behaviour and gives the performance back, and that choice is now respected on
every boot rather than overwritten.
Release 0.4.1.
Save states could not be taken at all. Saving has to stop every SPU somewhere it
can be serialised from, which is what Compatible Savestate Mode does, and this
port wrote that setting to false on every boot -- so the save failed with
"missing SPU setting" no matter what the user did, and nothing on screen
connected the two.
The reasoning was sound: the mode costs SPU performance, and a PS3 state runs
500MB to 3GB, so a few saves fill a phone. Both of those are costs to disclose,
not reasons to decide for someone. It is now a setting, still off by default, so
nobody pays for a feature they did not ask for and installs from the window when
it was pushed as true are corrected by the same write.
Placed with the SPU rows rather than under a savestate heading, because that is
where the cost lands, and the description says what both costs are before the
switch is touched.
run_recoverable_llvm runs code generation on a disposable thread and terminates
it through pthread_exit when LLVM invokes its fatal error handler. bionic does
not force-unwind C++ frames on pthread_exit, so the lock MCJIT holds over the
execution engine is never released and stays held by a thread that no longer
exists.
Every entry point into the engine takes that lock, so the next compile hangs and
so does teardown. One recovered error, and the emulator is finished until it is
killed -- and the error is recovered, which is the point: it is meant to be
survivable.
Their Kotlin log-channel screen is left out; this port has its own.
From MaxsTechReview in PS3Native.
Two places compute a size from values a file supplies and use it without
checking it is possible.
A SELF or SCE header gives the metadata offset and the header size, and the
buffer between them is sized by subtracting one from the other. Both are
unsigned, so a truncated or malformed dump that puts the offset past the header
end underflows into a near-SIZE_MAX allocation, which fails as an out-of-memory
rather than as the bad file it is. Reject the layout instead.
Texture uploads take the mip levels the guest describes and write them into an
image built from the destination's own dimensions, which can hold fewer. Drop
the levels that do not fit rather than writing past what was allocated.
From MaxsTechReview in PS3Native.
get_cycles passes the pthread handle to pthread_getcpuclockid, which glibc
answers with an error for a thread that has already exited -- the else branch
below returns the last known value for exactly that case. bionic instead looks
the handle up in its list of live threads and aborts the process when it is not
there.
m_thread is never cleared when a thread ends and the performance overlay samples
every PPU, SPU and RSX thread on a timer, so one finished thread is enough to
take the emulator down with it. Latent here rather than absent: it needs the
overlay on and a thread to have gone.
Record the kernel tid at initialize and build the per-thread clock id from it
the way bionic does once its own lookup succeeds, so clock_gettime simply fails
for a dead thread, which is what the surrounding code already expects. Cleared
at finalize so a thread stops being sampled before it goes away. Other platforms
keep the original path.
Found and fixed by Zulux91 in PS3Native.
A licence installed from a locked game's own menu reported success and left the
game locked. The install writes the file into exdata under its own name, because
a RAP's name is conventionally the content id it unlocks, and that convention is
the whole lookup: the core opens exdata/<content id>.rap and nothing else. A file
saved as "license(1).rap", renamed, or tidied up on the way over therefore lands
where nothing will look for it, and the only feedback is the game asking again.
Where the game is known, ask it. The native path decrypts the EBOOT's
supplemental header to read the content id, which works on a locked game because
that header is not what the licence protects, and names the file correctly
whatever the user's copy is called. Falls back to the name when there is no game
to ask or the header cannot be read.
The package screen keeps the name-based path: a licence installed on its own has
no game to resolve against.
Three things in the patch download path, all of them things desktop
RPCS3 already does.
The download URL had the patch schema version written into it as 1.2.
That is right today, but patch_engine::load rejects any file whose
Version header doesn't match the core's patch_engine_version, so the day
upstream bumps that constant every download starts failing to parse. The
core now hands the version out through patchEngineVersion() and the URL
is built from it. I also check the version the server echoes back, which
turns a several megabyte download into an early error instead of a
parser complaint.
Nothing verified the sha256 the server sends alongside the patch text.
Desktop checks it before it writes anything (patch_manager_dialog::
handle_json). Patches are writes into the guest executable, and
move_file/hide_file patches reach the emulator's own filesystem, so I'd
rather not import bytes that aren't what the server hashed. Mismatches
get their own message rather than being reported as a parse failure.
Last, the wildcard serial. patch_key::all is spelled "All", and
patchSetEnabled compared against a lowercase "all", so a patch carrying
a wildcard entry never had that entry written.
I first "fixed" the same typo in patchesList and let per-game lists match
the wildcard too. Ooops. Turns out that is not a typo doing nothing, it
is a typo doing the right thing by accident: wildcard patches are keyed
by SPU or PPU hash and leave the serial as "All" because the hash is the
filter, so they belong to no single game. There are 17 in the database,
and on device Skate 3 cheerfully offered me a pile of LittleBigPlanet
MLAA patches, where toggling one writes the shared entry and changes
every other game as well. Desktop shows them once under an "All titles"
node, so per-game lists stay serial-only here and the global list is the
equivalent. Only patchSetEnabled and the enabled-state read get the
spelling fix.
Two things in the patch list reported something the game wasn't getting.
First, patchesList marked a patch as enabled if any entry under its hash
was enabled, whichever serial that entry belonged to. patchSetEnabled
writes per serial, so a patch I switched on from one game's list showed
as on in every other game that patch covers. It now scans only the
requested serial, plus RPCS3's "all" wildcard, which really does apply to
the game being listed.
Second, the patch engine builds its map once while the game loads, then
writes the patches into each module as that module loads. Toggling a
patch only rewrites patch_config.yml, so nothing happens in a game that
is already running. The in-game tab never said so, which makes the switch
look broken. It says so now, above the list.
Those two are 7.4 ms a frame in Arkham City, a fifth of it, and the GPU side of
the same work is 0.76 ms, so it is host work. Each handler does several unrelated
things and nothing separates them: a read barrier that can force a readback, the
memory copy the transfer exists to perform, and in the blit engine a software
scale through ffmpeg for the cases the GPU path does not take.
Scope the three. The scale is scoped inside convert_scale_image rather than at
its four call sites in the blit engine, and only the RSX thread is ever reported,
so calls from elsewhere cost nothing to cover.
Also let a scope be closed early, so a region ending part-way through a function
does not need a block introduced purely to place a brace.
The name table is keyed by register index, and both method reports passed the
byte offset. A lookup therefore matched whichever unrelated method happened to
have that value as its enum, so the costliest entry in Arkham City came out as
NV4097_SET_CONTEXT_DMA_VERTEX_B, which has no handler and cannot cost anything.
It was NV406E_SEMAPHORE_ACQUIRE: the RSX waiting for the guest to signal, which
is the one entry in that list that is supposed to block and the one that should
not be optimised.
A wrong name is worse than none here. The hex fallback was right the whole time
and is left as the byte offset, which is what a reader looks up.
The handler bodies are 60% of the RSX thread in Arkham City and the dispatch
machinery around them is 3.5%, so the question is which handlers. The method
histogram cannot answer it: it counts calls, and the busiest method may be a
register write while a rare one does the work.
Keep the interval the dispatch site already measures. It brackets the call with
two counter reads to fill the method_call bucket and then throws the difference
away; billing it to the method's slot as well costs one add.
Inclusive of whatever the handler calls into, including scopes that charge
themselves elsewhere. For ranking handlers that is the useful reading, and the
per-bucket totals stay exclusive as they were.
Arkham City spends 38.5 ms a frame in FIFO decode, 58% of the RSX thread, at
164 ns a dispatch. Sonic manages 45 ns on the same loop, the same decode and the
same counters, so the difference is in what the handlers do rather than in the
dispatch. Nothing separates the two: fifo_decode encloses the whole loop, so it
holds every handler body as well as the machinery around them.
The two handlers already scoped, transform program and transform constant,
measure 0.02 and 0.06 ms here, which rules them out and leaves the rest of the
mix unaccounted for.
Wrap the handler call. This is the only per-dispatch scope in the profiler and an
earlier attempt at one measured mostly itself; it is affordable here because it
brackets a call rather than a loop iteration, and handlers carrying their own
scope still attribute inward. It costs a few percent of the bucket it splits, so
the split is the number to read, not the total.
Booting a second game without restarting the app builds a new RSX thread.
set_enabled is the only thing that binds the profiler to a thread, and it
early-returns when the setting has not changed, so it stayed bound to the
previous game's thread. Every scope then failed its owner check, nothing
switched buckets, and the whole window was charged to whichever bucket happened
to be current.
That prints as "FIFO decode 100.0%", which is indistinguishable from a genuine
finding about a command-bound title, and was briefly read as one.
Notice the change per frame and re-bind, dropping the accumulated window and the
per-pass counters: they belong to a thread that is gone, and keeping them would
blend two games into one report.
The include list came over from ARMSX2 and names sstates, memcards, gamesettings,
cheats and snaps. None of those exist here, so a backup collected a few kilobytes
of controller profiles, reported success, and left every save behind. Nothing
warned: skipping an absent folder silently is right for an optional one and wrong
for a list aimed at a different emulator.
Name RPCS3's paths instead. The part that matters is config/dev_hdd0/home, which
holds save data, trophies and licences, and is the only thing in here that cannot
be rebuilt or re-downloaded. Save states, input configs and patches come along.
Installed titles are left out on purpose, along with firmware and dev_hdd1: a PKG
reinstalls and a PUP reinstalls, a save does not, and that is the line this list
is drawn on. Including them would have taken the archive from tens of megabytes
to nearly five hundred on the device this was sized against.
The description on the screen said memory cards and artwork too, so the one place
a user could have noticed agreed with the bug.
Everything measured so far describes what a draw contains: vertices, pixels,
shader length, subdraws, barriers. By all of them pass six should be the
cheapest of the expensive passes, and it is the dearest by a factor of seven.
An occlusion query is none of those things. On a tiler it makes the visibility
stream resolve, it costs the same whatever the framebuffer size, and no counter
here would show it. That matches every property this pass has: indifferent to a
sixteen fold cut in pixels, indifferent to tiling being switched off, no
barriers, one subdraw per draw, shorter shaders than the passes it dwarfs.
ZCULL is active, and emit_geometry opens a query whenever the command buffer
carries the occlusion flag. Count them where they open.
Installing a .rap never worked at all. The package screen routed licences to
installKey, whose RAP branch works out the content id by decrypting the game's
EBOOT, so it needs a game path, and the only caller passed an empty one. Every
attempt died at "Failed to fetch NPDRM of SELF". A RAP's filename is the content
id it unlocks, which is why RPCS3 desktop's InstallFileInExData simply copies the
file into exdata. That is what this does now, lower case extension included,
because unself.cpp searches for it that way.
Picking a game together with its licence could not work either. The installer
routed on file count rather than file kind, so any multi file selection went to
installSplitPkg, whose first act is to reject anything that is not a .pkg part.
Multiple selection has been allowed since split packages landed, so the obvious
thing to do was the one thing guaranteed to fail. The selection is split by kind
now, packages first, since a licence unlocks content the package has to have
written already.
Both failures showed the same generic "Install failed. The file may be encrypted,
incomplete or not a PS3 package", which reads as a bad file rather than a bug in
the app. The reason the native side already reported now reaches the screen.
A licence-locked title also looked like any other until it refused to boot. The
core works that flag out by attempting decrypt_self on the EBOOT, but the library
never asked it. The scan asks now, and a locked game gets a badge on its cover, an
Install licence entry in its context menu, and a prompt instead of a doomed boot
from every launch path: the library cards, the context menu, the controller, and
the settings screen's Play button.
Boot failures were silent besides. Rpcs3Bridge.boot threw away BootGame's return
code and MainActivityRuntime dropped runVMThread's result, so a failed boot was
indistinguishable from a game that started and exited immediately. Both are
reported now, which is how I found the licence problem in the first place.
External intents and launcher shortcuts are not covered, because externalGameInfo
builds a fresh GameInfo where locked defaults to false. Those still fall back to
the boot failure message.
Uninstalling only ever removed dev_hdd0/game/<TITLEID>, so the title's compiled
code and shader cache stayed on disk forever. On my device that was between 7 and
58 MB per title, and one of those caches belonged to a game I had already removed.
I made it a checkbox on the existing confirmation rather than doing it silently,
defaulted on, which is how RPCS3 desktop's own remove dialog treats caches. The
row is hidden when there is no cache, and it shows the measured size so you can
see what you are freeing. The size is measured off the main thread because a cache
directory holds hundreds of files and this runs while the dialog is opening.
The cache goes only after the native uninstall reports success, since dropping the
cache for a title that is still installed would just cost a recompile. The title
id is validated before the recursive delete: it comes from a directory listing,
but a path separator or a dot dot in it would resolve outside the per title
folder, so anything that is not a single plain segment is refused.
Save data, trophies and licences are deliberately left alone. Those belong to the
user rather than to the install, and desktop does not offer to remove them either.
A session ended in a fatal VK_ERROR_OUT_OF_DEVICE_MEMORY, the first in any log
here. On this GPU that is system memory, and the device had two gigabytes free
of seven with the emulator holding most of the rest.
Two frames in flight is what makes the CPU and the GPU overlap, and it is also a
second frame's worth of resources alive before anything retires them. That trade
is worth making at rest and not worth making into a crash on a handheld sharing
memory with everything else.
Fall back to the single frame this used to run with when the memory load is
above low. Slower, and slower is recoverable.
Shader length settled that pass six is not the game's workload: it has the
shortest shaders of the expensive passes, a quarter of the vertices of a pass
that costs a seventh as much, no barriers, and no reaction to resolution or to
tiling being switched off. Every quantity measured so far says it should be
cheap, and it takes nine milliseconds.
The draw count is the one that has been lying. It counts clauses, and a clause
is expanded over its subranges, so a single entry can become thousands of draws.
Batching them through VK_EXT_multi_draw, which this device does support, saves
our command overhead and changes nothing about how many the GPU processes.
Count them at every submission site. Thousands of tiny draws at a fixed cost
each is the last shape that fits, and nothing else measured would reveal it.
Pass six costs about 26 times what pass eight does per vertex: 123 draws and 68
thousand vertices for 9.15 ms against 532 draws and 253 thousand vertices for
1.26 ms. It has no barriers, does not care about resolution, and does not change
when TU_DEBUG=sysmem takes tiling and binning out of the picture entirely. The
only thing left that behaves that way is the shader.
Record vertex and fragment ucode length per pass. This decides whether there is
a bug here at all, which nothing measured so far can: shaders genuinely that
much longer are the game's own workload and there is nothing to fix, while
comparable ones mean something is happening to those draws that should not be.
The previous commit read driver_env.txt before the log file was opened, so the
one thing worth knowing -- whether the option was applied -- was written into a
listener that did not exist yet and then thrown away when the log rotated.
Move the read to just after the log file is created and report each option by
reading it back rather than echoing what was meant to be set. There is no other
honest confirmation available: /proc/<pid>/environ is the snapshot taken at exec
and never reflects a runtime setenv, and Mesa's own logging goes to stderr,
which Android discards. Still long before any Vulkan instance exists, which is
the only ordering Mesa cares about.
This device needs Turnip; the stock Adreno driver does not render the game at
all. Turnip is steered by environment variables such as TU_DEBUG, and the usual
way to set one on Android, the wrap.<package> property, is ignored on a user
build. It can be set and read back while never reaching the process, which makes
a flag that never applied look exactly like a flag that made no difference. That
is how the first attempt at this measured stock Turnip twice and called it a
result.
Read NAME=VALUE lines from <root>/driver_env.txt during initialize, before any
Vulkan instance exists, since Mesa caches each option the first time it is read.
A missing file does nothing, which is the normal case.
Picking a scale and launching a game still rendered at native. The previous
attempt read the launch-time write from ps3.resolutionScale, which turns out to
be the wrong end of it: that field has no writer anywhere in the UI, so it holds
its default of 100 permanently.
applyTo pushed that default onto Video@@Resolution Scale, the same node the
upscale multiplier writes, and applyTo runs after the launch path, so the orphan
won every time. Changing the scale in game appeared to work only because nothing
calls applyTo again afterwards.
Emit the node from upscaleFloat instead, which is what the preset grid, the
custom percentage slider and the in-game overlay all write, using the same
conversion and clamp as the other writer so the two cannot disagree. Restores
the launch-time call to the multiplier it always used.
Picking a resolution scale and then launching a game ran at native. The UI kept
showing the chosen value, the config held the default, and changing it in game
worked, which made it look like the setting was not saving.
Both settings write the same native node. applyTo writes the PS3 percentage to
Video@@Resolution Scale, and renderUpscalemultiplier writes the ARMSX2-lineage
multiplier times a hundred to the same place, from the launch path, after
applyTo. So the last writer won and it was the one carrying a default of 1.0.
Changing the value in game appeared to work only because nothing writes the node
again afterwards.
Drive the launch-time write from the PS3 setting so the two agree. Same node,
one owner.
Pass six spends 9.8 ms on 44 draws and 33 thousand vertices at ordinary
resolution, which is 300 ns a vertex. That is not vertex work, and the 2048
square shadow map next to it costs under a quarter of a millisecond with twice
the geometry, so it is not target size either. What is left is the GPU being
serialised inside the pass.
texture_barrier keeps the pass open on Android and issues a by-region
self dependency instead, which was the right trade against a tile store and
reload. But that barrier still makes a tiler resolve the tile and fetch it back,
and one per draw would cost about what pass six is costing. Nothing counts them.
Count barriers issued while a pass is open, per pass, and how many came from a
cyclic reference. If pass six shows one per draw the mechanism is named; if it
shows none, the serialisation is somewhere else and this rules out the obvious
candidate cheaply.
Two passes hold 69% of GPU time and one of them, pass six, costs 76us a draw
against 2.4us in pass eight while holding 7% of the frame's draws. Rendering at
quarter resolution changed nothing, so it is not fragment work, and the ordinal
on its own says nothing about what the pass is for.
Record the render target size and the vertex count per pass alongside the draw
count. Size names the pass in the game's terms, since a shadow map, a reflection
and the main scene do not share dimensions. Vertices per draw separates a lot of
geometry from a lot of cost per vertex, which is the question the timing cannot
answer and which decides what a fix would even look like.
Rendering at a quarter resolution changed the GPU time not at all, which rules
out fill rate, fragment shading and tile traffic in one measurement, since all
three scale with pixels. What is left inside the passes is geometry, binning and
per-draw cost. It also retires the tile bandwidth theory the previous two
attempts were built on: that traffic would have fallen sixteen fold.
So the draw total needs splitting, and the timer already measures each pass
individually and only reports the sum. Report the distribution instead, keyed by
the pass ordinal within the frame: the frame structure is stable, so pass N is
the same logical pass each time, which is what makes it something to act on.
Count draws per pass alongside it, on the same ordinal. A pass that is expensive
holding few draws is expensive per draw; one holding most of the frame's draws
is carrying the geometry. Same milliseconds, opposite fixes.
Reporting only. No new timestamps and nothing recorded that was not already
being measured.
vkCmdClearAttachments needs the pass open, and the pass opens with LOAD_OP_LOAD,
so clearing a target reads the whole framebuffer into tile memory and then
throws it away. LOAD_OP_CLEAR skips the read. On a tiler that read is the whole
attachment every time, and this title runs about thirty passes a frame at 720p
with colour and depth.
Taken only when the clear covers the entire render area and no pass is already
open. A partial clear is not a load op, and ending an open pass to change its
load ops would store the framebuffer in order to discard it, which costs more
than it saves. Colour is all attachments or none, since a load op applies to the
attachment as a whole. Depth and stencil get separate bits because clearing one
and keeping the other is common.
Whether an open instance can serve a request now compares the key with the clear
bits masked off rather than the pass pointer. Load ops do not affect render pass
compatibility, so the two variants are interchangeable for an open instance and
for the pipelines inside it; comparing pointers would have ended the instance to
begin an equivalent one, paying the store and reload this is meant to avoid and
discarding the clear on the way. Callers that pass no key keep the old pointer
comparison.
The collector had gathered eight frames in five thousand flips and its ring was
parked on slot zero with every slot unreset and empty. All of that follows from
one thing: the frame region was opened at device init and in flush_command_queue
only, and this title takes that path roughly never, so the region opened once at
boot, closed on the first submit and was never opened again.
Everything else depends on it. The slot's query range is reset when the frame
region opens, the ring only advances past a slot once something in it has
completed, and collection refuses a slot that was never reset. So a timer that
initialised cleanly and logged its tick period produced no report for an entire
session, which reads the same as a GPU with nothing to do.
Open it where the primary command buffer is actually begun for the next frame.
The GPU timer initialises, reports its tick period, and then never produces a
report: eighteen RSX profiles came and went in one session against zero GPU
profiles. Collection has several preconditions and the report only prints once
three hundred frames have been gathered, so a collector stuck on any of them
prints nothing at all, which reads exactly like a GPU that is idle.
Log the collector's state periodically while it has nothing, with the slot
flags, the open regions and the drop count, so the precondition that is not
being met can be read instead of guessed at.
Also stop recording anything but the frame region into a slot that still needs
its reset. Writing a timestamp into a range that has not been reset is invalid,
and the reset only happens when the frame region opens, so a render pass that
begins first -- the ones flip() runs after next_frame has already rotated the
slot -- was writing into stale queries.
The GPU timer measures the whole frame, readbacks, blits and uploads, and the
one region it names but never records is draw. So the split it exists to provide
has been missing exactly where it matters: with the RSX thread no longer waiting
on a fence, the Adreno sits at 99% busy at its top clock and nothing says how
much of that is drawing the game.
Bracket the render pass at the only place one actually starts, not at the
wrapper, which early-outs when the same pass and framebuffer are already bound.
Roughly thirty passes a frame, comfortably inside the per-frame event cap.
Both timestamps sit outside the pass rather than inside it. On a tiler the load
at the start and the store at the end are the expensive part, and timing from
within would exclude the cost worth knowing about.
Take the command buffer by const reference, which is what the render pass
helpers hold and what the conversion operator already permits.
Ending the open pass to change an image layout costs a tile store and a reload
on a tiler, and it happens about twenty times a frame out of twenty nine passes.
The counter on it says how many and never which: change_image_layout is reached
from seventy five call sites, tagging them by hand would be tedious and would
still miss the next one added.
Record the return address instead. Two levels, because image::change_layout
funnels most callers and one level would name that function for nearly
everything. Only recorded when a pass is actually open, so the count is
teardowns caused rather than layout changes attempted, and only while profiling
is armed, on a path taken twenty times a frame.
Reported as a symbol where the dynamic table has one and as a module offset
otherwise, which llvm-symbolizer resolves against the unstripped core.
FIFO decode is 56.7% of the RSX thread now that it is no longer waiting on a
fence, and it is the enclosing scope of the dispatch loop, so it holds every
method handler body as well as the loop itself. 201 ns a dispatch is far too
much for reading a word and calling a handler, so the cost is in a handler or in
the per-dispatch machinery, and nothing in the report separates those.
Scope the two batching handlers and the FIFO cache refill. All three run a few
thousand times a frame at most rather than per dispatch, so unlike the earlier
attempt at a per-command scope none of them measures mostly itself.
Count calls, not methods. Both handlers consume a run and skip the rest, so
their share of the method histogram counts what they swallowed rather than how
often they ran, and dividing by it would price a batch as a single method.
Neither vkGetFenceStatus nor vkWaitForFences with a zero timeout returns without
waiting on this driver: both measured 17-28 ms a call and neither returned
not-ready once in 300 frames. So the poll cannot be made honest at the call
site, and the previous commit's zero timeout changed nothing.
What can move is where the wait happens. Draining the present queue at the first
draw of a frame meant the CPU started recording only once the GPU had finished,
and frame time became GPU plus CPU rather than the larger of the two: 42 ms made
of 27.7 GPU and 14.3 CPU, which is the two of them end to end. Stop draining
there and let the throttle at flip bound the pipeline, which is the same wait
placed after the frame's recording rather than in front of it, so recording runs
while the GPU is still busy.
The rotated context should already be retired by then. If it is not, borrow the
aux context as before, and if that is busy too, wait for this one specifically
rather than trip the ensure behind it.
vkGetFenceStatus measured 19.7 ms per call on Adreno and returned VK_NOT_READY
zero times in 300 frames. Every caller of poke() wants "is it done, do not
wait", so the one call per frame turned an intended poll into a full GPU sync
and ran the CPU and the GPU in series: frame time was CPU plus GPU rather than
the larger of the two, with the GPU only 66% busy at less than its top clock.
Use vkWaitForFences with a zero timeout, which is specified to answer without
waiting.
That poll was also the only thing bounding the pipeline, because a queue whose
oldest entry is always retired before the next is added never holds more than
one. With an honest answer the frames accumulate, so bound them on purpose, one
below the frame context count: the queue and the context rotation advance
together, so retiring the front is what frees the context about to be handed
out. Allowing the full count would route every frame through the single aux
context borrow and hit the ensure behind it.
The remaining wait is real frame pacing and is charged to swap_wait, where it
can be read.
The fallback CPU table was missing cores found in recent handheld SoCs
(Cortex-A510, A715, X3, A520, A720, X4). Because get_cpu_name() bails out
when any detected MIDR is unknown, a single missing core sent the whole
lookup to the cortex-a78 fallback whenever LLVM host detection returned
"generic".
Display names were also reused as LLVM -mcpu values, which happens to work
for the Cortex names but not for Qualcomm Oryon: the display name lowercased
to "x-elite", which is not an LLVM processor, so the JIT silently lost
per-CPU scheduling. Entries now carry an explicit canonical LLVM name
alongside the human-readable one; get_cpu_brand() keeps using the latter.
The Qualcomm entry is named "Oryon" rather than "X-Elite" because MIDR
0x51/0x001 only identifies an Oryon core, not the SoC it sits in.
MIDRs cannot identify the SoC at all, which made bug reports ambiguous.
Android's own SOC_MANUFACTURER/SOC_MODEL are now passed to the core and
logged as a separate "SoC:" line, so SoC identity, core topology and the
resolved LLVM target are three distinct values. The LLVM target reported by
system info now comes from the same resolution path the JIT uses, rather
than from the fallback alone, so it no longer disagrees with the target
actually compiled for.
The Vulkan renderer logs one verdict for the adapter it selected, recording
whether BC1-BC3 support keeps DXT textures compressed or whether they are
decoded on the CPU. It sits in render_device::create rather than where the
flag is resolved, because physical_device::create runs for every GPU of
every instance, and not in TextureUtils, whose fallback branches run per
texture and per mip level.
SoC information travels through a new optional _rpcsx_setSocInfo export
instead of an added _rpcsx_initialize parameter. The core is dlopen()ed and
can be updated independently of the JNI glue, so changing an existing
export's signature would make older glue call it with a garbage argument.
Older glue simply never calls the setter, and newer glue null-checks the
symbol against older cores.
No JIT feature policy and no texture decoding behaviour changed.
Verified on an AYN Odin 3 (ayn CQ8725S, 8x Oryon, Adreno 830) running
Turnip 26.2.99: SoC line reads "ayn CQ8725S (Snapdragon 8 Elite-class)",
the brand line reports Oryon rather than X-Elite, the JIT resolves to
oryon-1, and a single BC verdict reports the GPU path. That BC result
applies to the Turnip driver tested; stock-driver behaviour is unmeasured.
Resource destruction was the stated suspect and measured 0.056 ms, so the time
is in vkGetFenceStatus, which is over half the RSX thread. Every call site that
reaches it appears to run once or twice a frame, and a status query that blocks
for milliseconds would be a driver problem while one called a hundred thousand
times would be ours. Nothing in the report distinguishes those.
Count the calls and how many come back not ready. This is the same denominator
the FIFO buckets needed twice already, once for packets against commands and
once for draws against setup.
The present check kept its 18.9 ms after the fence wait and the reclaim both
measured zero, which leaves the poke, and the only thing in a poke that can
sleep is the event completion callback. Without multithreaded RSX that callback
runs inline, and popping an event scope runs the destructor for every GPU object
that event retired, so the RSX thread frees a frame's worth of images and memory
through the kernel driver before it can record the next draw.
Scope the destruction and the fence status query separately. If neither holds
the time, what is left is the lock at the top of the poke, and that is a
different bug again.
The mid-draw present check turned out to be two thirds of the RSX thread, which
the previous commit could only say as one number. It covers three things that
mean opposite things: a fence wait, a poke that takes a shared lock, and the
per-frame resource reclaim. A wait says the GPU is the bottleneck and every CPU
change aimed at this path was aimed at nothing; the reclaim says the opposite.
Scope the fence wait and the reclaim separately, so whatever is left in the
present check bucket is the poke and its lock. Count entries and cleanups too:
the block is written as a rare async flip fixup, so how often it runs is the
first thing worth knowing about it.
Draw setup was 66.7% of the RSX thread, but the bucket only ever held whatever
VKGSRender::begin and end did not charge to a nested scope. Everything with a
body of its own now carries one: the surface write barriers, the render target
on_write pass, the temporary texture release, the mid-draw present check, and
rsx::thread's own prologue and epilogue, which were unscoped on both backends.
Count draws too. A large per-draw bucket is a lot of draws at a fair price or a
few at an unfair one, and those want opposite fixes; the FIFO buckets already
learned that lesson the hard way when a per-packet figure was read per command.
Draw setup keeps the leftovers, which is now the draw clause loop and nothing
that can hide 23 ms.
The two-point probe read the incoming words as be_t<u64>, an eight byte swap,
and compared that against a destination written by copy_data_swap_u32, which
swaps each word on its own. The wide swap also exchanges the two words, so the
comparison was (w0,w1) against (w1,w0) and could only match when w0 equalled w1.
It never reported a match.
Every upload therefore set vertex_program_ucode_dirty. That forces a full vertex
program re-analysis per draw clause, drops the program cache hint, nulls the
bound program so load_program runs again, and re-uploads the transform constants
unconditionally. Sonic '06 issues 8088 of these a frame against 3429 draws, and
a corrected profile puts 24.7ms of a 36.4ms frame in draw setup, which is what
all of that lands in.
Rotating the source back by 32 bits puts both sides in the same word order. The
change can only remove spurious invalidations: a clean verdict from the probe is
still confirmed word for word by the full compare below it, so a false clean is
not reachable.
Upstream inherited, introduced in ae39c5b8cb.
fifo_decode is the enclosing scope of the whole RSX loop, and the profiler is
exclusive, so it holds whatever no nested scope claimed. VKGSRender::begin()
carries a scope and end() did not, so essentially every per-draw cost landed
there: load_texture_env's texture cache search and sampler lookup, the vertex
and fragment ucode analysis, and the write barriers. shader_translate and
barrier had no instrumentation sites anywhere in the tree.
That is why the bucket read as 100% of a 29ms frame while the decode loop itself
only accounts for a couple of milliseconds: 36728 packets and 48737 dispatches
cannot cost 29ms when the loop body is an inlined exchange, a table load and an
indirect call.
Scopes added to end(), load_texture_env and analyse_current_rsx_pipeline, so the
next capture shows where the frame actually goes.
Three changes to the same loop, all measured against 36728 packets and 48737
dispatches per frame in Sonic '06.
GET is published on a bounded lag rather than every packet. It is a release
store into guest DMA memory, and get shares a 64-byte line with put which the
guest PPU writes from another CPU cluster, so each publish was a cross-cluster
coherence miss. The guest reads GET to size its free ring space and is far ahead
of us here, FIFO stalls measuring 0.1 a frame, so lag is invisible to it.
Anything that can idle or block publishes immediately: the put wait in inc_get,
the NOP path, and set_get.
The FIFO accuracy setting is snapshotted once per packet instead of being read
per argument. Reading it goes through a seq_cst atomic load, which on ARM64 is
an ldar the compiler cannot hoist out of the loop.
The again poll is relaxed. That flag is only ever set by this thread, by the
handler invoked immediately before, so sequential consistency buys nothing and
cost another ldar per dispatch.
Three separate fixes to the same hot path.
The profiler's FIFO figure counted the wrong thing. g_fifo_commands is
incremented once per run_FIFO entry, and one of those drains a whole packet, so
dividing by it priced a packet rather than a method. Sonic '06 averages about 17
methods per packet, so the reported 612 ns per command was really 612 ns per
packet and the per-method cost was closer to 36 ns. Cross-checks: 413.6 FIFO
refills a frame at 4096 bytes is 1.69 MB, about 423000 words, which 24248
packets can only consume at roughly 17 words each. Dispatches are now counted
where they are dispatched, both figures are reported, and the per-method
histogram divides by the right one. The line is labelled packets, and notes that
fifo_decode is a catch-all holding every handler body too, since no handler
carries its own scope.
rsx_state::decode was a cross-TU call per dispatched method. The body is one
exchange, but the definition lived in rsx_methods.cpp and LTO is disabled
project-wide, so it never inlined, and being opaque it also forced the caller to
reload its context pointer afterwards. Moved to the header.
set_transform_constant and set_transform_program read ctrl->put through a
seq_cst load. That is an ldar on ARM64, on a cache line shared with ctrl->get
which the guest PPU writes from another cluster, in the two hottest handlers in
this title. Relaxed: a stale value only shrinks the batch, and the remainder is
picked up on the next call.
copy_data_swap_u32 and its compare variant are assembled by asmjit under
ARCH_X64 only. Every ARM64 build fell through to the scalar per-word loop,
reached through a function pointer so it could not be inlined, and LTO is
disabled project-wide so nothing recovered it afterwards.
It is not a cold path: transform constants, transform programs and vertex data
all upload through it, and a Sonic '06 profile on a Snapdragon 8 Gen 2 put 82.5%
of the RSX thread in FIFO decode with thousands of these blocks per frame.
vrev32q_u8 reverses bytes within each 32-bit lane, which is the same swap the
scalar path does per element. The compare variant accumulates differences and
reduces once at the end rather than branching per element. Checked against the
scalar version over 20000 randomised trials at counts 0 to 39, covering every
tail remainder, for both variants: identical output and identical return value.
transform_constant_load_modifier_barrier decoded its argument into
NV4097_SET_TRANSFORM_PROGRAM_LOAD. The barrier is pushed by
nv4097::set_transform_constant_load, so it should target
NV4097_SET_TRANSFORM_CONSTANT_LOAD.
A title that moves its constant load pointer mid-draw therefore had the move
dropped, leaving every constant after it written at the old offset, and had its
vertex program upload position overwritten with a constant index at the same
time.
An unbounded wait polled vkGetFenceStatus in a tight loop with nothing but a
pause hint between calls. command_buffer::flush() takes that path for the submit
fence, so it is what a frame does while it waits on the GPU: a core pinned at
100% for the whole wait, hammering a driver entry point while the driver is
trying to do the work being waited on.
Cheap on a desktop with cores to spare. Not here, where it competes with the SPU
and PPU threads for a handful of cores. Arkham City measured 24ms of a 53ms
frame in this function with the GPU only 71-77% busy, which is what a stall
looks like when the waiter is too busy spinning to prepare the next submission.
Polls briefly first, since most waits are for a fence about to signal and
blocking would cost a syscall and a wake-up for nothing, then hands the wait to
vkWaitForFences so the driver can sleep the thread. The blocking call was
already there, three lines up, used only when a finite timeout was supplied.
Same shape as wait_for_event below, which already had this treatment.
Installing a licence is a copy into exdata, a directory nothing on the screen
read, so a success looked exactly like a failure: a licence belongs to no title,
never appears under Installed titles, and left nothing visible anywhere in the
app. Reported as the .rap doing nothing, when the file had in fact been written
correctly.
Listed now beside the installed titles, refreshed on the same events.
Every directory under dev_hdd0/game was listed with an Uninstall button beside
it. RPCS3 keeps its own lock directory in there, get_hdd0_locks_dir() being
get_hdd0_game_dir() + "$locks/", so the screen offered to delete the emulator's
lock state, and any folder a failed install left behind was offered as a title.
A PARAM.SFO is the test now. Game data installs keep theirs and stay listed on
purpose: a 1.1GB BLUS30464_INSTALL is the kind of thing someone opens this
screen to reclaim, bootable or not.
sys_fs_readdir fires once per directory ENTRY, unlike opendir and closedir
either side of it which fire once per operation. A game scanning its own USRDIR
emits a line per file: one such scan measured 346 lines in 22ms, during boot,
for no diagnostic gain.
Moved to trace, which is where the other per-datum calls already sit
(sys_fs_read, sys_fs_write). opendir and closedir stay at warning, so a scan is
still visible in the log without being enumerated.
The instance asked for 1.2 unconditionally. A loader that predates it may answer
VK_ERROR_INCOMPATIBLE_DRIVER to a higher request, and the spec tells applications
to check the version first for that reason, so on those devices the renderer
never started at all.
Queried through the global procedure address, since vkEnumerateInstanceVersion is
itself a 1.1 entry point and its absence means 1.0, then clamped. A no-op wherever
1.2 or better is available, and logged when it is not so a device report says so.
Every JNI object handed to native code is a local reference, reclaimed only when
the frame that created it returns to Java. The frames this runs on do not return:
the main thread processor and the compilation queue are infinite loops inside a
single JNI call.
Progress took one reference per instance from FindClass and one per report() from
NewStringUTF, and released neither. There is no DeleteLocalRef, PushLocalFrame or
NewGlobalRef anywhere in the native tree. The progress dialog server pushes
several updates per tick and a firmware precompile emits thousands of ticks, so
ART's local reference table filled and the runtime aborted.
Time-proportional, which is why it showed as a crash during firmware install on
slower devices and not on faster ones.
Copy construction is deleted along with it: the class owns a reference now, and a
copy would have had its destructor release one the original still used.
The Android port keeps one global config.yml: settingsSet persists through
SaveSettings(g_cfg.to_string(), "") and an empty title id is the global path.
apply() returned early for any title without an entry, writing nothing, so
Uncharted 3's Stub PPU Traps = 1 stayed set once it had been booted and every
game launched afterwards ran with a PPU that silently skips an instruction on
any trap rather than stopping.
Nothing said so on screen and nothing else writes that node: it is not in the
curated push, and CoreSettingOverrides only replays paths the user recorded
themselves.
Every managed path is now written on every boot, this title's value where it has
one and the upstream stock value where it does not. Anything added to BY_SERIAL
has to gain its default in STOCK.
The recursive scan descends into anything that is not itself a game folder and
then accepts any file whose extension is in gameExtensions. "img" is one of
them and a title's own data is full of them, so once a package unpacked over
dev_hdd0/game the library filled with GTA IV's archives: manhat01, props_ab,
vehicles, script, weapons.
dev_hdd0/game is the emulator's own install root and holds one directory per
title, so it is now read that way. A direct child is a title or it is not
listed. Folders a user pointed us at keep the recursive scan, because games
legitimately sit at any depth there.
The extractor no longer unpacks into this directory either, but the library
should not have depended on that, and existing installs still have the debris.
A package's install directory is taken straight from its own metadata and was
never checked. Both sources can produce nothing: read_metadata sizes the string
to 9 and reads the title ID over it without testing the result, so a short read
leaves nine NUL bytes, and the DLC path takes c_str() + 8, which is empty
whenever byte 8 is a NUL.
Appending either left the destination as dev_hdd0/game itself, so the package
unpacked its contents over the games root. Users reported a library full of
asset directories, storage consumed with nothing listed as installed, and
folders that outlived uninstalling the title, because uninstall only removes
dev_hdd0/game/<TITLEID>. It looked random because it depends on the individual
package's metadata.
Checked against c_str() so the nine-NUL case reads as empty. Separators and dot
entries are refused as well: this is one path component chosen by the package
and it has no business pointing anywhere else.
While chasing an unrelated SPURS hang I noticed the JIT publishes
freshly written code on ARM64 with no instruction-cache maintenance at
all. A grep for clear_cache or flushInstructionCache over the JIT layer
comes back empty. The branch-rewrite sites only issue ISB; DSB ISH,
which performs no D-cache clean or I-cache invalidation and is ordered
backwards for self-modifying code besides. On ARMv8 a correct
publication needs the DC CVAU / IC IVAU broadcast sequence; x86 has a
coherent instruction cache, so none of this was ever visible there.
All sites use the bundled asmjit::VirtMem::flushInstructionCache(),
which emits that sequence portably across toolchains.
This covers every publication path I could find:
- MemoryManager1::finalizeMemory() and MemoryManager2::finalizeMemory()
were both no-ops. RuntimeDyld calls finalizeMemory() after writing
code and relies on it for cache maintenance, so LLVM emitted PPU and
SPU code was never flushed. MemoryManager1 serves the primary PPU
JIT, MemoryManager2 the SPU JIT and auxiliary engines. Both managers
now record code section allocations and flush them on finalize. I
confirmed at runtime that the MemoryManager2 path executes (about
12800 calls per cold boot).
- jit_runtime_base::_add() copies asmjit output into executable memory
with no flush.
- jit_runtime::finalize() restores an executable code snapshot in place
during emulator restart with only the ISB/DSB pair.
- spu_runtime::rebuild_ubertrampoline() publishes a hand-written
trampoline via CAS with no flush; the flush now happens before the
publication.
- spu_runtime::make_branch_patchpoint() writes a patchpoint byte by
byte and returns it with only the ISB/DSB pair.
- Both 16-byte branch-site rewrites (dispatch and branch) atomically
overwrite live code and only issued the ISB/DSB pair.
The ISB/DSB pairs adjacent to the new flushes are removed along with
their misleading "flush all cache lines" comments: the flush helper
already issues the trailing barriers, and the pairs never performed
any cache maintenance in the first place.
I want to be upfront that this was not the cause of the hang I was
debugging (a same-item compilation race, fixed separately), and I have
not observed a failure that this change alone fixes. It is a latent
correctness issue on any ARM64 host: nothing prevents another core
from fetching stale instruction bytes for freshly published code.
Five SPURS kernel threads executing the same uncached code at the same
address all reached spu_llvm_recompiler::compile() for one spu_item.
add_empty() returns the existing item for an identical program without
telling the caller it did not insert, and the entry-point dedup only
catches equivalent code at a different address. Each thread then
compiled the program with its own LLVM instance, racing the compiled
pointer publication, the ubertrampoline rebuild, and the waiter
notification.
On my 8-core ARM64 device this wedged SPURS bring-up on every single
cold-cache boot of Virtua Tennis 4 (BLUS30529). The kernels ended up
parked polling zeroed workload state, the PPU main thread blocked
forever in sys_event_queue_receive on a queue no SPU would ever signal,
and the title never reached the menu. Warm boots never hit it because
cached programs are compiled before SPU execution starts, one
presentation per program. When I instrumented the compile path I saw up
to five concurrent compilations of a single item, around 790 collision
events per boot, with duplicates accounting for roughly two thirds of
all cold compilation work.
This change gives spu_item an explicit LLVM compilation state
(unclaimed, compiling, complete, failed). The first compiler claims the
item; later arrivals wait and take the published result, mirroring the
existing dedup-wait path. I made the claim a state on the item rather
than an inserted-flag from add_empty() so that an item pre-inserted by
spu_fast is still claimed by the first LLVM worker, which preserves the
asynchronous optimized replacement on x86-64. A scope guard marks the
item failed on any early exit so waiters cannot be stranded.
The pre-existing wait for relocated duplicates (same program, different
entry point) is also covered: it still waits on the compiled pointer,
because that result can be published by spu_fast from the asmjit path
which never touches the LLVM state, but it now observes the failure
state on each wakeup with a bounded timeout, and the failure guard
wakes those waiters too. Without this an owner that bailed out early
would have stranded them forever.
I verified this on device: 11 out of 11 cold boots stalled before the
change, 7 out of 7 pass after it, plus 2 out of 2 warm controls, and
Mirror's Edge now reaches gameplay past its previous SPURS stall.
Cold-boot SPU compilation dropped from about 12900 blocks to 3900.
Every .rap failed. A .rap is 16 raw bytes of key and carries nothing that says
which content it unlocks; that lives in the filename, as the content id. Upstream
copies the file into dev_hdd0/home/<usr>/exdata/ under its own name and is done,
so the name is the whole mechanism.
This called the native installKey with an empty game path instead. That path
decrypts the GAME's EBOOT to read an NPDRM header out of it, so with no game path
there was no EBOOT, no header, and the install could never succeed. Handing it a
bare file descriptor had already thrown the name away regardless.
Licences are now handled before any descriptor is opened. Reported for Resident
Evil 4 HD and for DLC licences generally.
2026-08-09 22:14:40 -04:00
210 changed files with 29258 additions and 20777 deletions
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.