FindPreloadBlock (and HashMatches, which only it used) had no callers, and a
few comments still mentioned preloading.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JitBlock's move constructor didn't copy profileStats_, so every time the
vector of blocks reallocated, all execution counts and times went back to
zero, and --jit-profile under the IR interpreter only counted what ran since.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like the other command-line overrides (--graphics etc). Running once with
--cpu=ir otherwise left the IR interpreter as the saved setting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exits and barriers read every reg, and filled all 256 lastReadFrom entries
twice each, for GPRs and FPRs. That's now one index. The FPR lists are only
built for instructions with an FPR operand, and the working list holds IRInsts
rather than IRInstMetas (a quarter of the size). The output is the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The compiler didn't inline std::vector<IRInst>::push_back into IRWriter::Write,
which the frontend and every pass call for each instruction. FastVec's append
is a compare and a store.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PurgeTemps' and OptimizeLoadsAfterStores' working lists, IRApplyPasses'
buffers, the frontend's output writer, IRJit::Compile's instruction list and
the backends' per-instruction address list all keep their capacity between
blocks. The pass ones are static, since compiling only happens on the
emulation thread.
--jit-bench on a Galaxy S8: 21.4-22.4 -> 20.1-20.6 us per block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- IRImmRegCache keeps which regs are known and written as bit masks rather
than bool arrays, so they're quick to clear for each block, and FlushAll
walks the set bits instead of memchr'ing.
- RegStatusMIPS is packed to 12 bytes from 24. Start() copies all 256 of them
for each block.
- ReduceVec4Flush returns early for blocks without a Vec4 or matrix operand,
which is most of them, rather than clearing its three 256-entry arrays.
With the inlining: --jit-bench on a Galaxy S8, 23.5-24.3 -> 21.4-22.4 us per
block. The IR is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GetIRMeta(IROp), the IRAnalysis read/write queries (IRReadsFromGPR/FPR,
IRDestGPR, IRDestFPRs, ...), JitOptions::Disabled and the ARM64 emitter's code
pointer getters move to the headers. The passes call the IRAnalysis ones for
each instruction and each register they track.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FindBlockByStart runs for every exit and link. IsValidGPR/IsValidFPR are asked
all the time by the register cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PurgeTemps went through every check since the first live one for each
instruction, though most end soon after they start. Ended ones are now
dropped. OptimizeLoadsAfterStores skips looking at what an instruction writes
when it knows of no loads or stores. The output is the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calls between functions in the library went through PLT stubs, since it's
built with default visibility. The stubs were about 5% of JIT compile time on
a Galaxy S8 (the IR passes call many small helpers in other files), and a few
percent of the emulator thread in general. --jit-bench: 27 -> 25 us per block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since decccf199a, FlushAll's needsFlush() only accepted regs in memory, so no
pair was ever stored with STP, and the loop over all 256 regs found nothing.
It now takes regs that are out of memory (an IMM always counts as dirty),
respects the gprs/fprs arguments, only looks where STP can reach (regs 1-63),
and stores adjacent IMMs in the upper GPR range (like HI and LO) together.
A REG_AS_PTR reg is also unadjusted by its native reg, not the IR reg.
The x86 IR JIT has the same inverted check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured with --jit-bench on a Galaxy S8, recompiling the 11243 blocks of a
Tekken 6 state: 363 ms -> 300-310 ms, with the ARM64 pairing fix.
- FlushAll, which runs at every exit, no longer looks at all 256 regs. FPRs are
found from the native regs, and the rest are skipped quickly when in memory.
- IRApplyPasses alternates between two buffers that keep their capacity,
instead of allocating a new one for each pass.
- The ARM64 backend flushes the icache once per block, for the block's code and
every exit linked to and from it, with one set of barriers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--jit-bench=N recompiles every block in the cache five times after N flips
(at the next safe point), and logs each run and the best, split by phase. With
a state loaded, it gives the same set of blocks every time, so changes to the
compiler can be compared run to run. Game frames vary too much for that.
The compile timing reads cntvct_el0 on ARM64 Linux/Android rather than calling
clock_gettime(), through time_now_fast_ticks(). The counter's rate is measured
against time_now_raw(), since a Galaxy S8 reports 0 in cntfrq_el0 (it runs at
26 MHz there).
The IR pass list in DoJit is a fixed array instead of a vector.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Totals per phase (frontend, passes, backend, finalize/linking, invalidation and
full clears) go in the JIT stats overlay, and the frame graph draws each frame's
compile time in orange at the bottom of its bar. A frame that spends 2 ms or
more compiling logs a "JIT stall" line with the breakdown.
Each phase boundary reads the clock once, since that costs around 0.3 us on a
Galaxy S8.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
X11, which the Vulkan headers bring in, defines Screen. NativeApp gets the
custom driver functions from VulkanDriverVars.h instead of VulkanLoader.h,
and the GPU debug overlays take their font from the caller instead of
including View.h for the theme.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There the backend is never given up on, but a custom driver that crashed
Vulkan last time is cleared, and Vulkan is tried again with the system's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before falling back to OpenGL. Covers the availability check, a library
that won't load, a failed InitAPI and a crash on the last run. The setting
is cleared and a notice names the driver.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Installing from an updater file left it out, so Android's media scanner
indexed the firmware. Also only creates it when missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What a draw used depended on whatever was set last, often by another view,
so several draws only got the right font because something before them
happened to reset it. Those now pass the theme's font, which is what they
got in practice.
DrawTextRectSqueeze also measured with the theme's font whatever style it
drew with, so small text could be squeezed more than it needed to be.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each hint quotes the old label as that language showed it. Khmer and Lao
get the English text for now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Force real clock sync (slower, less lag)" no longer described it: it costs
little CPU, and falls back to normal timing by itself when the host is busy.
The config key stays until it's on by default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A slow display list can hold up emulation for several milliseconds on a slow
device. Lag sync catches up afterwards, but the output keeps pulling in the
meantime, and the fixed 4.4 ms over the output's period wasn't enough. Each
sync now records how far behind the wall clock emulation is, and the most
over the last five seconds is added to the target.
The granular mixer's queue cap no longer makes room for a frame's burst of
audio while pacing; it adds the room for stalls to that for two output reads
instead, up to a frame. The classic resampler drops a lasting excess above
5 ms rather than 10, to settle sooner after the room shrinks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--audio-granular picks the granular mixer (the Audio sync mode setting), and
the sink then reports the queue's level and cap, and its underruns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While lag sync paces, audio arrives every millisecond or two instead of a
frame's worth at a time, so the resampler's target drops from 40 ms to the
output's period plus 4.4 ms (or 20 ms where the platform doesn't report a
period), and goes back on a fallback to bursting.
The rate adjustment took around ten seconds to drain a startup fill down to
a small target, so a lasting excess (the lowest level over a quarter second
more than 10 ms over the target) is now dropped in one go.
The audio debug overlay shows the target in ms, the lag sync mode and the
dropped excess.
Headless gets --audio-sink=FRAMES, which pulls audio like an output device
with that period and prints how full the buffer ran and how often it ran
dry. docs/lag-sync.md has how to use it, and the Windows measurements: with
a 10 ms period, the buffer averages about 16 ms paced against 35-38 ms
bursting, with a 3 ms period about 9-10 ms, without underruns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sleep_precise and sleep_approx shared one waitable timer. It has one due
time and releases one waiter, so when two threads slept at once, the second
one's due time replaced the first's and one of them waited forever (and the
other woke at the wrong time). Lag sync sleeping on the emulator thread while
another thread slept was enough.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lag sync waited with sleep_precise, which on Windows spins the last
millisecond, so its 1 ms waits were nearly all spinning. It now sleeps
with a new sleep_approx (no spin), only once 2 ms or more ahead, and asks
for as much less as the OS has been waking it late. Being late doesn't
add up, since the goal is measured from a fixed base.
Pacing every millisecond only pays with time to spare. Each second, the
share of wall time spent waiting (in lag sync or in the frame throttle)
is measured. Under 10%, it falls back to bursting with the frame
throttle, as without lag sync, and goes back after two seconds over 25%.
GTA VCS on an M1, at --speed values around the host's limit, now runs
as fast as without lag sync or slightly faster. At 1x it costs about 1%
of a core in cycles over bursting, the same as before.
Headless --frame-stats prints lag sync's sleeps, lateness and fallbacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The IR frontend dropped a branch or jump in a delay slot (and reported it),
or approximated it with a link to +12 and an exit into the delay slot. Such
a pair now ends the block with the new BranchPair pseudo-op, which the
interpreter runs until it's out of the delay slot, so every core does what
tests/cpu/delayslot/branch recorded. It's an ordinary Interpret, so the
native backends need nothing new, and other branches compile as before.
This removes the delay-slot special cases from the branch compilers and
ResolveNotTakenTarget(), with the eight reports about them. The CPU fuzzer
now runs its d class (branches in delay slots) on every core, and they
agree, cycles included. cpu/delayslot/branch moves to tests_good.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As tests/cpu/delayslot/branch shows: in a jump's delay slot, a jump or a
taken branch replaces the jump's target, and a link there saves that target.
In a conditional branch's delay slot, a jump is ignored but still links. A
jump skipped by a not-taken likely branch still jumps, with the next
instruction as its delay slot, but doesn't link (Brooktown High relies on
the jump).
The CPU fuzzer gets a d class, which puts branches and jumps in delay slots.
Only the interpreter runs those cases until the IR frontend handles them;
PPSSPP_CPUFUZZ_ALLCORES runs them on every core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Branches and jumps in the delay slot of another branch, recorded on a PSP.
Neither the interpreter nor the IR frontend matches it yet: a jump in a
jump's delay slot should take its own target, links in a jump's delay slot
go to the outer jump's target, a conditional branch in a jump's slot should
override it in IR, and a jump annulled by a not-taken likely branch still
jumps but doesn't link.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Queue fill and cap (in granules and ms), under/overruns, output rate, read
size and frame time, replacing the "no stats yet" placeholder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The queue cap was four output callbacks plus a frame of audio. Bluetooth and
other outputs without a fast path call back with large chunks, so that came
to about 100 ms there. Now it's the classic target (which also honors Extra
audio buffering), raised for slow games' per-frame bursts and to keep half
the queue above one callback's read, plus a granule for the overlap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>