Each module load hashed every function found so far, rebuilt a hash-to-
function multimap of all of them to apply the hash map, reinstalled every
replacement and reloaded the builtin hash table. Unloading rebuilt the
multimap too.
Now FinalizeScan() takes the scanned range and does that work only for the
functions there (and any never hashed), looking each up in the hash map
directly, and the builtin table is loaded once. With knownfuncs.ini enabled
it still names functions everywhere, since the file can add names for any.
On a Galaxy S8, Tekken 6's sceUtilityLoadModule went from 67 ms to 13-23 ms,
its LoadModuleDone events from 48-57 ms to 12-18 ms, and unloading modules
from 13-22 ms to under 10.
Regression risk: code that changed in memory outside a module load is no
longer rehashed as a side effect of the next load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loading or unloading a module flagged the active symbol copies for a full
rebuild, a std::map insert per symbol of every module ever loaded, which the
next lookup paid. MIPSAnalyst's ApplyHashMap looks up labels right after a
load, so that was half the time of loading a firmware module.
Now just that module's symbols are added to or removed from the active
copies, without renumbering every function (the index only alternates
disassembly colors, and SortSymbols() still renumbers). On a Galaxy S8,
Tekken 6's sceUtilityLoadModule went from 126-139 ms to 67 ms, and its
LoadModuleDone events from 68-94 ms to 48-57 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
sceMpegAvcDecode called again on an access unit it has already decoded passes
the ME an empty one, and on a PSP that fails with 0x80628002
(pspautotests video/mpeg/decoderepeat). We returned success with no picture.
A movie player that retries the same access unit until a picture comes back
then never got out of its loop, and as the decode took no time, it starved the
lower-priority thread that feeds the ringbuffer.
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>