53 Commits
Author SHA1 Message Date
jpolo1224 93c6df7e42 Settings: clear the core tuning left pinned while debugging
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.
2026-08-11 15:54:54 -04:00
jpolo1224 f056d6fc86 Video: keep the VRAM heap cap at 2048
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.
2026-08-11 15:10:41 -04:00
jpolo1224 ae1caf915b Release 0.5: purge the profiling overrides, raise the VRAM heap cap
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.
2026-08-11 14:06:11 -04:00
jpolo1224 9128a75c0e VK: reclaim what can be reclaimed when the renderer is uninterruptible
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.
2026-08-11 13:55:43 -04:00
jpolo1224 785c4d5627 VK: decline video memory pressure instead of aborting, and budget it lower
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.
2026-08-11 13:45:54 -04:00
jpolo1224 38424a59bd SPU: mark failed program ranges; Video: budget VRAM on mobile
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.
2026-08-11 13:39:05 -04:00
jpolo1224 8c707648cb SPU: leave the interpreter once the uncompilable block is behind us
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.
2026-08-11 12:55:02 -04:00
jpolo1224 9543378661 SPU: route the uncompilable-block fallback to the interpreter that works
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.
2026-08-11 12:44:04 -04:00
jpolo1224 0b5a43c602 vm: report who a stuck writer_lock is waiting on
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.
2026-08-11 10:50:08 -04:00
jpolo1224 5997681fed Packages: install from storage that cannot be read directly, and name what is installed
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.
2026-08-11 10:43:11 -04:00
jpolo1224 49012ba969 Settings: seed per-title core settings, starting with Web of Shadows
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.
2026-08-11 10:26:35 -04:00
jpolo1224 0909f77a95 RSX: charge the empty-ring yield to idle, and stop draining the present queue on flush
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.
2026-08-11 10:13:53 -04:00
jpolo1224 0cf85cb902 RSX: drop the FIFO idle sleep; log the swapchain pre-transform
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.
2026-08-11 10:02:54 -04:00
jpolo1224 cb3670e2db Settings: restore upstream's GETLLAR busy-wait percentage
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.
2026-08-11 09:52:02 -04:00
jpolo1224 5f346b5e2c Settings: default the thread scheduler back to the OS
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.
2026-08-11 09:33:32 -04:00
jpolo1224 1c2d44502c cellPad: keep pressure values in the buffer when press mode is off
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.
2026-08-11 09:22:58 -04:00
jpolo1224 ba5e4ebd66 SPU: stop an out-buffer verdict from spinning GETLLAR forever
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.
2026-08-11 00:01:45 -04:00
jpolo1224 a873ae8250 Settings: let idle SPUs sleep instead of spinning on a reservation
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.
2026-08-10 23:56:15 -04:00
jpolo1224 e8c499056b SPU: memoise the GETLLAR out-buffer check on its inputs
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'.
2026-08-10 23:52:23 -04:00
jpolo1224 88f4162883 SPU: evaluate the GETLLAR stack heuristic once per spin sequence
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.
2026-08-10 23:43:41 -04:00
jpolo1224 771a0dc74e RSX: back off to a sleep when the FIFO ring has gone quiet
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.
2026-08-10 23:37:48 -04:00
jpolo1224 1458d5f2d9 VK: end open occlusion queries in end_renderpass, for every caller
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.
2026-08-10 23:27:17 -04:00
jpolo1224 2cd341e257 Settings: purge the stale raw Relaxed ZCULL Sync override
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.
2026-08-10 23:17:53 -04:00
jpolo1224 fa97d01b0d VK: do not emulate predication where we disabled the extension ourselves
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.
2026-08-10 23:14:07 -04:00
jpolo1224 703d98ae9f VK: disable conditional rendering on Turnip as well as Adreno
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.
2026-08-10 23:10:32 -04:00
jpolo1224 cf2481fc11 VK: close open occlusion queries before ending the render pass
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.
2026-08-10 23:01:36 -04:00
jpolo1224 8041edf5bc RSX: publish GET only when it has advanced
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.
2026-08-10 22:50:14 -04:00
jpolo1224 d909537f3f Stop copying query results from inside a render pass
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.
2026-08-10 22:34:20 -04:00
jpolo1224 0cbab0c3ae Join the GPU and CPU pass tables on the same ordinal, and reach external storage
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.
2026-08-10 22:28:33 -04:00
jpolo1224 3338eedfcd Bound the PPU compile serialisation so a dead worker cannot strand it
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.
2026-08-10 22:13:01 -04:00
jpolo1224 062cde277a Stop the save-data list aborting where there is no media backend
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.
2026-08-10 22:09:59 -04:00
jpolo1224 7b0f1cd6de Ship the overlay icons the native UI has been drawing without
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.
2026-08-10 18:36:17 -04:00
jpolo1224 05842b3115 Retranslate every language from the current English map
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.
2026-08-10 18:36:17 -04:00
jpolo1224 d6235c5802 Remove the PS2 leftovers a PS3 emulator was still carrying
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.
2026-08-10 18:36:17 -04:00
jpolo1224 94b12dc216 Release 0.4.2 2026-08-10 16:20:10 -04:00
jpolo1224 ac7639457f Say when the game has stopped drawing instead of leaving the last frame up
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.
2026-08-10 16:20:10 -04:00
jpolo1224 84db19dc3e Publish GET before the RSX stops consuming
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.
2026-08-10 16:14:18 -04:00
jpolo1224 5eb3d64ed0 Name where each guest thread is parked when frames stop
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.
2026-08-10 15:49:20 -04:00
jpolo1224 d82f1df96b Report RSX stalls and vblank liveness without needing a frame
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.
2026-08-10 15:41:38 -04:00
jpolo1224 697cacd854 Bound the waits on an SPU compile claim
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.
2026-08-10 15:12:16 -04:00
jpolo1224 ab3335b8d6 Make the auto-save options and save-state import do what they say
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.
2026-08-10 13:20:38 -04:00
jpolo1224 99b6b47ee1 Look for slot thumbnails where the core actually wrote them
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.
2026-08-10 13:03:07 -04:00
jpolo1224 6255ce5840 Run posted main-thread callbacks off the caller's thread
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.
2026-08-10 12:52:51 -04:00
jpolo1224 87b0992fdd Revert keying the SPU cache on savestate-compatible mode
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.
2026-08-10 12:40:22 -04:00
jpolo1224 2a0c04ade7 Stop treating an already-registered game as a boot failure
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".
2026-08-10 12:38:35 -04:00
jpolo1224 5b8ff01fe2 Route the savestate-compatible setting to the core
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.
2026-08-10 12:23:55 -04:00
jpolo1224 51d465f193 Key the SPU cache on savestate-compatible mode
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.
2026-08-10 12:17:53 -04:00
jpolo1224 6cd5b8b986 Turn save states on by default
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.
2026-08-10 11:51:09 -04:00
jpolo1224 44f62f310c Let the user turn save states on
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.
2026-08-10 11:47:41 -04:00
jpolo1224 431b6d0925 Stop a recovered LLVM fatal error from wedging the SPU JIT
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.
2026-08-10 11:44:11 -04:00
jpolo1224 4a73773cee Bound two unchecked sizes reached from file contents
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.
2026-08-10 11:43:05 -04:00
Zulux91 e16f0fcd3d Sample thread CPU time by tid on Android
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.
2026-08-10 11:42:14 -04:00
jpolo1224 6463d010e9 Ask the game for its content id when installing its licence
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.
2026-08-10 11:36:42 -04:00
135 changed files with 20985 additions and 18720 deletions
+7
View File
@@ -545,11 +545,18 @@ class jit_compiler final
// Disk Space left
atomic_t<usz> m_disk_space = umax;
bool m_poisoned = false;
public:
jit_compiler(const std::unordered_map<std::string, u64>& _link, std::string_view _cpu, u32 flags = 0, std::function<u64(const std::string&)> symbols_cement = {}) noexcept;
jit_compiler& operator=(thread_state) noexcept;
~jit_compiler() noexcept;
bool is_poisoned() const noexcept
{
return m_poisoned;
}
// Get LLVM context
auto& get_context()
{
+24 -2
View File
@@ -872,17 +872,26 @@ jit_compiler& jit_compiler::operator=(thread_state s) noexcept
jit_compiler::~jit_compiler() noexcept
{
if (m_poisoned)
{
jit_log.error("Abandoning poisoned LLVM execution engine (leaked to avoid a deadlock in ~MCJIT)");
static_cast<void>(m_engine.release());
static_cast<void>(m_context.release());
}
}
void jit_compiler::add(std::unique_ptr<llvm::Module> _module, const std::string& path)
{
ObjectCache cache{path, this};
m_poisoned = true;
m_engine->setObjectCache(&cache);
const auto ptr = _module.get();
m_engine->addModule(std::move(_module));
m_engine->generateCodeForModule(ptr);
m_engine->setObjectCache(nullptr);
m_poisoned = false;
for (auto& func : ptr->functions())
{
@@ -904,6 +913,7 @@ bool jit_compiler::try_add(std::unique_ptr<llvm::Module> _module, const std::str
m_engine->generateCodeForModule(ptr);
}, error))
{
m_poisoned = true;
return false;
}
@@ -921,8 +931,11 @@ bool jit_compiler::try_add(std::unique_ptr<llvm::Module> _module, const std::str
void jit_compiler::add(std::unique_ptr<llvm::Module> _module)
{
const auto ptr = _module.get();
m_poisoned = true;
m_engine->addModule(std::move(_module));
m_engine->generateCodeForModule(ptr);
m_poisoned = false;
for (auto& func : ptr->functions())
{
@@ -941,6 +954,7 @@ bool jit_compiler::try_add(std::unique_ptr<llvm::Module> _module, std::string& e
m_engine->generateCodeForModule(ptr);
}, error))
{
m_poisoned = true;
return false;
}
@@ -1001,15 +1015,23 @@ void jit_compiler::update_global_mapping(const std::string& name, u64 addr)
void jit_compiler::fin()
{
m_poisoned = true;
m_engine->finalizeObject();
m_poisoned = false;
}
bool jit_compiler::try_fin(std::string& error)
{
return run_recoverable_llvm([&]()
if (!run_recoverable_llvm([&]()
{
m_engine->finalizeObject();
}, error);
}, error))
{
m_poisoned = true;
return false;
}
return true;
}
u64 jit_compiler::get(const std::string& name)
+17 -2
View File
@@ -2844,6 +2844,7 @@ void thread_base::initialize(void (*error_cb)())
[[maybe_unused]] u64 new_tid = 0;
#elif defined(ANDROID)
const u64 new_tid = pthread_self();
m_native_tid = static_cast<u32>(gettid());
#else
const u64 new_tid = reinterpret_cast<u64>(pthread_self());
#endif
@@ -2998,6 +2999,10 @@ u64 thread_base::finalize(thread_state result_state) noexcept
// Avoid race with the destructor
const u64 _self = m_thread;
#ifdef ANDROID
m_native_tid = 0;
#endif
// Set result state (errored or finalized)
m_sync.fetch_op([&](u32& v)
{
@@ -3373,11 +3378,21 @@ u64 thread_base::get_cycles()
clockid_t _clock;
struct timespec thread_time;
#ifdef ANDROID
pthread_t thread_id = handle;
const u32 native_tid = m_native_tid;
if (!handle || !native_tid)
{
return m_cycles;
}
_clock = (~static_cast<clockid_t>(native_tid) << 3) | 6;
if (!clock_gettime(_clock, &thread_time))
#else
pthread_t thread_id = reinterpret_cast<pthread_t>(handle);
#endif
if (!pthread_getcpuclockid(thread_id, &_clock) && !clock_gettime(_clock, &thread_time))
#endif
{
cycles = static_cast<u64>(thread_time.tv_sec) * 1'000'000'000 + thread_time.tv_nsec;
#endif
+4
View File
@@ -139,6 +139,10 @@ private:
// Thread handle (platform-specific)
atomic_t<u64> m_thread{0};
#ifdef ANDROID
atomic_t<u32> m_native_tid{0};
#endif
// Thread cycles
atomic_t<u64> m_cycles{0};
+2 -2
View File
@@ -29,8 +29,8 @@ android {
applicationId = "com.armsx3"
minSdk = 26
targetSdk = 37
versionCode = 6
versionName = "0.4"
versionCode = 9
versionName = "0.5"
// ARMSX2's UI reads these. STORAGE_ALL_FILES gates the all-files storage path in
// onboarding; IN_APP_UPDATER gates the in-app GitHub-release updater.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Some files were not shown because too many files have changed in this diff Show More