Commit Graph
127 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 d0e6e0a7c7 Docs: CPU equivalence state, including ARM32
Where the cores now agree, what the hardware tests answered, what's still
open (the NaN handling waits on the cpu-equiv-4 branch), and the ARM32 cores
as measured on a Galaxy S8+ and A21.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:29:57 +02:00
Henrik RydgårdandClaude Opus 5.5 b1b4f26b89 Docs: The JITs' rounding mode gaps are fixed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 09:26:48 +02:00
Henrik Rydgård 9b07e78604 Merge remote-tracking branch 'origin/master' into cpu-equiv-1
# Conflicts:
#	pspautotests
2026-10-14 01:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 d0567e2dfb GE: Nest CALLs only two deep, like the hardware
- Fixes #7395

The GE has two return address registers, and a CALL beyond them does
nothing: the list carries on after it. A signal CALL is done by the
firmware, which saves and clears them, so it starts the count over,
and the signal RET restores it. Shown on a PSP by the new
gpu/callstack/depth test.

Soul Calibur: Broken Destiny turns lighting on for its weapon trails
from a third-level CALL, so on hardware the trails stay unlit and take
their colors from the vertices. We ran that CALL and drew them white.

Plain CALLs now store a 0 base address in their stack entry, which a
signal CALL never does, so the depth can be counted without changing
the savestate layout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:54:01 +02:00
Henrik RydgårdandClaude Opus 5.5 23da98199e CPUEquivalence: Dump every core's end state, not only the interpreter's
So a diff between hosts covers the JITs too, without minimizing every case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 5495a6df90 Docs: What the PSP does where the CPU cores disagree
Fills in docs/cpu-equivalence.md with the answers from the new
pspautotests (shift_encodings, denormal, fcr31, signed_zero, nan_gen,
undefined), and how far each of the four cores is from them on ARM64
and on x86-64 under Rosetta.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 8935bfe41a Docs: CPU core equivalence findings, and what needs a hardware test
docs/cpu-equivalence.md describes the CPUEquivalence fuzzer, what it
fixed, and the differences still open between the cores and between
x64 and ARM64 hosts, several of which need a test on a PSP first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:10:00 +02:00
Henrik RydgårdandClaude Opus 5.5 eab9a69521 frametests.py --adb: Apply compat settings on both sides by default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:44:34 +02:00
Henrik RydgårdandClaude Opus 5.5 c7d60a5cfb Headless: Vulkan and OpenGL ES on Android, and frametests.py --adb
The Android headless build only did software rendering. Vulkan takes the
offscreen path desktop headless already uses, and OpenGL ES renders into an
EGL pbuffer, so both run from adb shell with no window. Assets are found next
to the executable, like on desktop, instead of in an APK path that no longer
exists.

frametests.py --adb pushes the binary, assets and dumps to a device, runs each
dump there with Vulkan and GLES, and compares the screenshots with the host's
Vulkan render using Tools/image_compare.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:44:34 +02:00
Henrik RydgårdandClaude Opus 5.5 56c522260e tvOS: Usable remote, smaller UI, keep file browsing in the container
Tested on an Apple TV 4K. The micro gamepad's dpad buttons flicker with every
wobble on the touch surface, so read the finger position instead: swipes step
once per fixed distance, and clicks are arrows at the edges and enter in the
middle. The UI scale now matches a 1080p Android TV (320 dpi). b-tvos.sh
can sign, install and launch on a device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 01:52:48 +02:00
Henrik RydgårdandClaude Opus 5.5 bfdf532965 CI: Build the tvOS app and the iOS and tvOS libretro cores
Adds b-tvos.sh and b.sh --tvos.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 01:15:41 +02:00
Henrik Rydgård 42afcc9fd7 Merge pull request #22516 from hrydgard/savestate-stress
Savestate stress test - results and fixes
2026-10-12 11:22:32 +02:00
Henrik Rydgård fd778f6599 Merge pull request #22515 from hrydgard/ssx-video
SSX On Tour: Recognize the game's memcpy, so its videos show
2026-10-12 10:59:05 +02:00
Henrik RydgårdandClaude Opus 5.5 1f30595b8a Docs: Mention --frame-stats for checking a game after a state load
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:52:46 +02:00
Henrik RydgårdandClaude Opus 5.5 b83f2ba7c6 Debugger: Add hle.func.hash, to get a function's hash for ReplaceTables
Returns the analyzed function's start, size and code hash at an address,
and the name of the hardcodedHashes entry already matching it, if any.
Replaces patching a printf into HashFunctions() in the docs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 10:06:16 +02:00
Henrik RydgårdandClaude Opus 5.5 c5ba9bead0 docs: How to find and hash a game function for ReplaceTables
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 09:58:38 +02:00
Henrik RydgårdandClaude Opus 5.5 81efd601f0 iOS: Merge the Metal view controller into the base one
With the GL view controller gone, PPSSPPViewControllerMetal was the only
subclass of PPSSPPBaseViewController. They're now one class,
PPSSPPViewController, in ios/ViewController.mm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:13:29 +02:00
Henrik RydgårdandClaude Opus 5.5 9720041ecf Debugger: Add game.state.load
Queues loading a savestate file, for scripting repros that load a state partway
through a run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 14:50:19 +02:00
Henrik Rydgård c51daff839 Merge pull request #22498 from hrydgard/sce-audio-reschedule-fix
sceAudio rescheduling fix
2026-10-11 09:22:18 +02:00
Henrik RydgårdandClaude Opus 5.5 2cf8b69bab VSH: Drop the 6.6x alarm-category patch
6.60 and 6.61 crashed in vsh_module's alarm-task scan on a count nothing
initialized, and a data patch zeroed it. With the shell's modules started
in order, both reach the XMB, show icons and launch homebrew without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:32:46 +02:00
Henrik RydgårdandClaude Opus 5.5 09974f9e82 VSH: Start the shell's modules one at a time, fixing game icons
The kernel starts modules one by one, each module_start returning before
the next starts. We started them all as threads side by side, and
vsh_module's start ran before paf's. Its 12MB request for game_plugin's
surface pools then went to the 8.3MB heap we pre-filled scePaf's pool
slot with, failed, and game_plugin threw away every decoded ICON0 and
PIC1 (white tiles on 3.80-5.05, grey placeholders from 5.50).

heaparea1, vshbridge, paf, common_gui and common_util now start in order,
and vsh_module waits for the last. The kd/ drivers still start side by
side, since syscon's start blocks on its hardware interrupt. paf fills
its own pool slot now, so the pre-fill is gone. Icons show on every
version from 1.50 to 6.61, and homebrew launches on the same ones as
before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 22:32:46 +02:00
Henrik RydgårdandClaude Opus 5.5 3dddacbfe0 Headless: Add --dump-audio=FILE.wav to record the mixed audio output
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 20:04:37 +02:00
Henrik RydgårdandClaude Opus 5 0c7990783d Docs: How to test an Android build on a real device
Written while testing the activity lifecycle work on a Galaxy S8 and a Quest 2.
Mostly about ways the tooling answers a question you didn't ask.

The expensive one: versionCode is the git commit count, so installing a feature
branch over a longer branch fails with INSTALL_FAILED_VERSION_DOWNGRADE, which
-r does not override. The device keeps running the previous build, and if the
install output went through a pipe the failure is never seen. An A/B where both
installs failed that way agrees with itself perfectly and means nothing.

Also: dumpsys meminfo counts activities that are garbage but not yet collected,
so the obvious reading looks like a leak whether or not there is one - trim and
re-read until it settles; how to force an activity recreate over adb when the
device won't rotate on command; why screencap is black on a Quest and what to
read instead; and how to tell whether a device exercises the content-URI path at
all before concluding a run covered it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 18:54:46 +02:00
Henrik RydgårdandClaude Opus 5 bd94e8141f Android: Converge the GL and Vulkan lifecycles on a shared RunGraphicsLoop
Stage 5 of the JavaGL removal. Now that the GL path owns its own EGL context and
render thread, Android's two lifecycle branches and the Win32 one in
MainThreadFunc were the same code three times over, so this moves the branch into
one place - RunGraphicsLoop() in Core/EmuThread.cpp. It picks the thread
arrangement the context asks for: either this thread becomes the render thread
and spawns the emu thread (OpenGL), or it just runs the main loop because the
backend spawns its own render thread (Vulkan, D3D11). Android, Windows and
headless all go through it now, which is what the "TODO: Merge with the Win32
EmuThread and so on" in app-android.cpp was asking for.

Falls out of having one implementation instead of three:

- MainThreadFunc called graphicsContext->ThreadStart() right after
  EmuThread_Start(), which already calls it, and ThreadEnd() right after
  EmuThread_Join(), which already calls that too. So Windows and headless GL ran
  CreateDeviceObjects()/DestroyDeviceObjects() twice per context, leaking a VAO
  on the way in and double-deleting it on the way out. Dates back to 16206d18ab,
  which moved those calls into EmuThread_Start/Join without removing the
  originals.

- RECREATE_VIEWS is gone. Only the GL path ever posted it; Vulkan goes through
  the same destroy/recreate without it and always has. Verified on device: views
  survive pause/resume, and both a real rotation and a density change - each one
  an activity destroy and recreate - lay them out again by themselves.

- The non-separate path is now named EmuThread rather than MainThread on
  Windows. It's the thread that runs NativeFrame(), which is what
  DebuggerThreading.md calls the emu thread, and the real main thread there is
  the GUI message pump.

The loop keeps an explicit exit flag, as a shouldExit callback, rather than
relying on ThreadFrame() returning false. In VR we skip ThreadFrame() entirely
while the XR session is idle, and the session going idle is exactly what
backgrounding the app does - so the exit would never be observed, the render
thread would sit in that sleep forever, and the UI thread joining it would hang.
That is not hypothetical: it deadlocked a Quest 2 on the first try.

Also drops a dead EmuThreadState enum and an unused JNIEnv local in
app-android.cpp, both orphaned when the JavaGL JNI entry points went away.

Tested on a nubia Z2464N (Mali-G57, Android 15) on both backends: cold start,
booting a homebrew, pause/resume in the menu, in a settings screen and in-game,
rotation back and forth, a density-change activity recreate with the game live,
the pause menu, and exit to menu. ThreadStart/ThreadEnd log exactly once per
context, and every one of those cycles tears the EGL context down and builds a
new one.

Also tested on a Quest 2 with the vr flavor, which is what shook out the deadlock
above: cold start into a focused XR session at 72fps, backgrounding (render
thread joins in 27ms where it used to never return), resuming back into VR with
a fresh EGL context, and exiting from in-app. xrCreateSession runs on the render
loop thread, which is where the EGL context is current - VRBase.cpp builds its
graphics binding from eglGetCurrentDisplay/eglGetCurrentContext, so that ordering
matters.

Windows x64 debug and the Android gradle builds are clean.

Not converged, and not a small change: NeedsSeparateEmuThread() itself. GL needs
its API calls on the thread its context is current on, so somebody has to pump
ThreadFrame() there, whereas VulkanRenderManager owns its thread outright.
Getting rid of the virtual means teaching GLRenderManager to spawn and own its
own render thread, for every GL platform at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01152igCdjTNhT29qvphPyPC
2026-10-08 15:29:12 +02:00
Henrik Rydgård 8e596032f1 Merge pull request #22446 from hrydgard/misc-rendering-fixes
Misc rendering fixes
2026-10-08 11:16:14 +02:00
Henrik RydgårdandClaude Opus 5.5 d717a38a34 Tools: Add image_compare.py, comparing renderers' screenshots past rasterizer differences
A pixel counts as different only if it's outside the range of the other
image's 3x3 neighborhood (widened by a tolerance) and enough of its neighbors
are too, so edges a pixel off, filtering rounding and speckle drop out.
8x8 block averages catch broad shifts separately, and heatmaps show where.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 10:36:19 +02:00
Henrik RydgårdandClaude Opus 5.5 6c04b9f068 Headless: Save the depth buffer from the hardware renderers too
--depth-save converts their float depth (z / 65536) to the PSP's 16-bit
values, or writes the unrounded values when the path ends in .f32.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:36:19 +02:00
Henrik RydgårdandClaude Opus 5.5 2db66333d4 Tools: Add headless_bench.py and dump_compare.py
headless_bench.py compares the speed of headless binaries on savestates
or frame dumps. It alternates the binaries between rounds and reports
medians of wall time, CPU time, and instructions and cycles where the OS
counts them. Instructions vary by well under 0.1% between identical runs,
where wall time swings by several percent. It checks for mains power
first, and stops on a run that failed or didn't get where it should.

dump_compare.py replays every frame dump in a directory with two binaries
and reports the dumps that render differently, or differently from run
to run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 14:55:04 -06:00
Henrik RydgårdandClaude Opus 5.5 5de2a5070f test.py: Add the gpu/exact tests
Nine tests of GE probe jobs that the software renderer reproduces bit for
bit (transform, planes, coverage, lines, texture, lighting, curves, nearclip,
pixel), recorded on a PSP.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:43 -06:00
Henrik RydgårdandClaude Opus 5.5 95fec4785c docs: Tool notes for PSP runs, replays, other-arch checks and game A/B runs
Bounded PSP liveness checks and the usbhostfs restart, stuck utility dialogs,
debugging replay hangs and recording a dump through wsdbg, syntax-checking the
RISC-V/LoongArch JITs and the no-SIMD build without a cross toolchain,
non-deterministic games in screenshot comparisons, and --dump-file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:43 -06:00
Henrik RydgårdandClaude Opus 5.5 2c5ff39238 Headless: Add --screenshot-render-target
Lets a dump replay stopped with --replay-end be compared against the PSP on
an offscreen buffer. The framebuffer commands after the draw limit are
dropped too, so it shows the buffer the last drawn primitive went to.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 fbd2b73b92 docs: Replaying a frame dump on a real PSP
Document pspautotests' ppdmp-playback and its new run.py, which replays a
frame dump over PSPLink and compares it with PPSSPPHeadless.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 15:09:42 -06:00
Henrik RydgårdandClaude Opus 5.5 cfde58575a wsdbg: Add :nowait, for requests that answer only once the CPU runs
Under --sync, input.buttons.press while stepping waited out the whole timeout,
and so did a cpu.resume issued to get it going. With :nowait the press is sent
and the next line (a cpu.runUntilTime) runs the CPU. Unanswered tickets are
now tracked in one place, so a reply that arrives during a later wait still
counts for :quit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 13:30:42 -06:00
Henrik RydgårdandClaude Opus 5.5 27116cdfa2 docs: The ARM64 fix for PCH build failures is the arm64 MSBuild
PreferredToolArchitecture has no effect; the MSBuild.exe that runs decides
the compiler host. The earlier note claimed otherwise, from a build that
passed by luck.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 10:26:00 -06:00
Henrik RydgårdandClaude Opus 5.5 c27a79363d docs: UWP build command, and the x86-hosted compiler PCH failure on ARM64
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 10:26:00 -06:00
Henrik RydgårdandClaude Opus 5.5 51bac9349e docs: Never run two PSP hardware tests at once
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 09:06:30 -06:00
Henrik RydgårdandClaude Opus 5.5 2a5febd634 wsdbg: Add :screenshot, and document nested key=value params
:screenshot saves gpu.buffer.screenshot as a PNG without dumping the data
URI into the output. The docs claimed nested parameters need a raw JSON
line, which gets no ticket; a single-quoted JSON value in key=value form
works and keeps it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:19:18 -06:00
Henrik RydgårdandClaude Opus 5.5 5be18675e0 docs: Rebuild and re-record a whole pspautotests directory at a time
Rebuilding one test with the current SDK while its neighbours keep old
binaries hides toolchain differences until someone happens to touch
them, like the tests/intr module manager stubs that did nothing. So the
policy is now to rebuild every .prx in the directory and re-record them
all, diffing each .expected against the old one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:19:32 -06:00
Henrik RydgårdandClaude Opus 5.5 8ed2d170f5 Audio: Don't emulate a failed blocking wait leaving the channel busy for good
When sceAudioOutputBlocking has to wait and the wait fails at once
(interrupts or dispatch disabled, inside an interrupt), the firmware
returns the error but leaves the channel's waiting flag set, and the
channel can never be used or released again. Keep the error, drop the
rest: whether the channel was busy at that moment is timing, and a
small difference in ours could lose a channel for the rest of a game
where hardware wouldn't. No game can depend on losing one.

Savestates made while this was emulated have the flag cleared on load.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:19:32 -06:00
Henrik RydgårdandClaude Opus 5.5 a74882b013 Audio/VolatileMem: Match hardware when a blocking call can't wait
From pspautotests intr/waits:

- sceAudioOutputBlocking sets the channel's waiting flag before its
  event flag wait, and when that wait fails at once (interrupts or
  dispatch disabled, or inside an interrupt) it returns the error
  without clearing the flag. The channel stays busy from then on, and
  can't be released.
- The SRC blocking output fails the same way even when a completion is
  already there, leaving the buffer armed.
- After a block that had samples in it, the mixer DMA is still playing
  it out, so a buffer arriving then isn't read early or restarts it.
- sceKernelVolatileMemLock only writes the fake address and size
  through pointers that are there, instead of faulting on NULL.

intr/waits now runs to the end; one scheduling marker still differs, from
async IO timing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:19:32 -06:00
Henrik RydgårdandClaude Opus 5.5 aad83c044f docs: Native debuggers vs. the memory fault handler
How to keep lldb and gdb from stopping on the faults the handler is meant
to catch, and the crash_*.prx tests as a quick check that it works.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 13:58:29 -06:00
Henrik RydgårdandClaude Opus 5.5 2b954b6a37 docs: Replace the headless hang history with what each GPU backend does
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 09:47:58 -06:00
Henrik RydgårdandClaude Opus 5.5 31dc2d47b5 docs: Headless OpenGL no longer hangs, and runs offscreen on macOS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 09:47:58 -06:00
Henrik RydgårdandClaude Opus 5.5 bc8f0f29c4 docs: Headless Vulkan runs offscreen, and is the one to use for games
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 09:07:23 -06:00
Henrik RydgårdandClaude Opus 5.5 8f804d6f0f Document the LoongArch LSX lane trap and testing under qemu
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 15:42:38 -06:00
Henrik RydgårdandClaude Opus 5.5 a293221fe2 Headless: Default to software rendering again
Since 551e4cd0ab, headless without --graphics silently used the OpenGL
backend, since bSoftwareRendering defaulted to false. Under Mesa llvmpipe
on Linux/WSL, that hangs games early in boot (and test.py, which passes no
--graphics). The README already documents software as the default.

Also documents the traps hit while chasing this in docs/debugging.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 19:25:24 -06:00
Henrik Rydgård 7b95ff2808 Merge pull request #22325 from hrydgard/vertex-decoder-jit-match
Vertex decoder: New test, make the JITs match the C++ decoder closely
2026-09-21 16:28:27 -06:00
Henrik Rydgård be62c09fe5 Merge pull request #22327 from hrydgard/sce-ge-queue
Claude code review: Rework sceGe
2026-09-21 16:28:15 -06:00
Henrik RydgårdandClaude Opus 5 333d84935e Add --force-hle, and stop reporting a normal sceAudiocodec re-init
--disable-hle had no counterpart, which made "is this our fault or the game's?"
awkward to answer: the only ways to put our HLE back were a per-game config or
hiding the firmware, and neither works from a script - the setting is per-game
and the firmware gets found anyway. --force-hle takes the same bitmask and runs
our HLE for those libraries even where the real module is now the default, so
the same repro can be run both ways and the logs diffed.

Used it on the warnings left over in Tekken 6 under the real mpeg.prx. Three of
them appear identically with --force-hle=16, so they are the game's own and
match what the hardware answers: sceKernelChangeThreadPriority(-1) eight times
in a row (pspautotests/threads/threads/change says hardware returns
UNKNOWN_THID for -1 too), sceAtracAddStreamData on a released id with a zero
byte count, and a sceKernelDeleteMutex on a garbage id.

The fourth only happens under the real module and is ours. mpeg.prx sizes its
allocation through a scratch context in its own bss and calls
sceAudiocodecReleaseEDRAM on that one, while decoding through a different
context that never gets released - so the next movie's sceAudiocodecInit finds
a live decoder and replaces it. That is once per video on every game running
the real module, and it was a WARN_LOG_REPORT, so it would have reported from
everyone's machine. It is bounded - removeDecoder deletes the old one and Init
makes exactly one more - so it is an INFO_LOG now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 15:26:51 -06:00
Henrik RydgårdandClaude Fable 5.1 69b7380d26 docs/sceGe.md: when the GE counts as busy, contexts and callbacks, LoadExec
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 13:21:03 -06:00