Headless hardcoded its memory stick to "memstick" next to the executable, so
testing a real game there meant copying the game in - 85MB for the one that
prompted this. --memstick=DIR points it at any directory with the usual
PSP/GAME layout, including the one the app build already uses, so the two can
share.
Two things worth noting in the implementation. The headless default is applied
*after* CommandLineOptions::ApplyToConfig() runs, so it has to check whether
the flag was given - otherwise it silently overwrites it, which is how the
first version of this failed: the game still booted (its path was absolute) but
from the wrong stick. And there's no DoNotSaveSetting() call for
memStickDirectory, unlike the settings around it: it isn't an ordinary setting
and never reaches ppsspp.ini, because the ini lives inside the memory stick
directory it would be describing.
Verified by deleting the copied game and booting it from the repo's memstick
with --memstick, running to exactly 2s of emulated time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
UnitTest.exe runs on CI and from tooling, where an assert or an abort() puts
up a message box that nothing will ever click, and the run just hangs until it
is killed. Headless already solved this; move its SetupCRT() into Common
(ExceptionHandlerSetup, which is where the rest of the process-level fault
setup lives) and call it from the unit tests too.
No behaviour change for headless. The OS-level SetErrorMode() call is now
guarded for UWP, which doesn't have it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
Adds Core/MIPS/InterpreterDispatch.cpp, the checked-in output of
GenerateInterpreterDispatch() (see the previous commit), and hooks it
into RunUntilFast() in MIPSTables.cpp in place of the old
MIPSGetInstruction()-based table walk + indirect call through
instr->interpret. The checked-with-breakpoints/memchecks path
(RunUntilWithChecks) is untouched for now, since it inspects
MIPSInstruction flags directly and correctness there matters most.
Also fixes a real crash in headless.cpp found while testing this:
cmdLineOptions.gpuBackend.value() would throw when unset (e.g. with
--graphics=software), now uses value_or().
Verified with `test.py -g --cpu=interpreter --graphics=software`:
313/314 pass; the one failure (cpu/fpu/fpu) is a pre-existing
interpreter-vs-JIT denormal (flush-to-zero) difference, confirmed to
fail identically with the old table-walking dispatch, so unrelated to
this change.
Adds Tools/update-dispatcher.py to regenerate InterpreterDispatch.cpp
from a built PPSSPPHeadless binary whenever the MIPSTables.cpp tables
change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SKhm9wKEQzRUsx9mTrrtQ
MIPSTables.cpp has walked a tree of tables on every single interpreted
instruction since forever, with a standing TODO asking for exactly this:
"generate smart dispatcher functions from above tables instead of this
slow method." GenerateInterpreterDispatch() does that - it walks the
same tables MIPSGetInstruction() walks at runtime, but resolves the
walk into a nested switch tree once, at generation time, with each
leaf calling straight into the existing MIPSInt::Int_* handlers and
returning that instruction's fixed cycle count. Anything not covered
(invalid opcodes, and the handful of instructions with no interpreter
implemented at all, e.g. tge/tlt/teq) falls back to the existing
MIPSInterpret()/MIPSGetInstructionCycleEstimate() slow path, so the
result is total over all 32-bit inputs, same as the table-walking path.
Wired up via a new headless --generate-interpreter-dispatch flag,
which prints the generated Core/MIPS/InterpreterDispatch.cpp source to
stdout and exits.
Also widens CmdLine.cpp's --help column formatting, which silently
truncated any option name longer than 24 characters - the new option's
name was the first to hit it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SKhm9wKEQzRUsx9mTrrtQ
Games often use the framebuffer alpha channel for non-visual purposes,
so saved PNGs could look fully transparent in image viewers. Force alpha
to 255 when writing PNG screenshots unless --screenshot-keep-alpha is
passed. The MSE comparison ignores alpha either way.
- Add frametests.py: walks a dump tree, renders each dump per config variant
through PPSSPPHeadless, generates reference images when missing and compares
MSE when present, and writes a self-contained HTML report. The JSON config
(which lives with the test set, not in the repo) points at the data tree
and defines variants as suffix -> CLI args, e.g. 'soft': '--graphics=software'.
- Headless: --screenshot-save saves PNG when the path ends in .png; new
--screenshot-diff always writes a visual comparison when comparing;
screenshot comparison failures (mismatch or unloadable reference) now fail
the test instead of passing silently.
- Read back framebuffers top-down, flipping only for BMP output/input
(fixes upside-down PNG references). Sync libretro copy accordingly.
- Document the system in docs/frametest.md; add AGENTS.md reference.
DisassemblyManager used to fuse lui+addiu/load/store into single pseudo-
instructions ("li", fused loads/stores) for display. This only applied to a
handful of opcodes, complicated DisassemblyManager, and was the root cause of
a stepping bug: Core_PerformCPUStep's Into/Over cases treated stepSize as a
byte count, while the WebSocket cpu.stepInto handler computed it as an
instruction count (needed to step over a whole fused macro in one go) - so a
plain, non-fused stepInto silently executed zero instructions.
Removed the fusion logic entirely (DisassemblyMacro, DISTYPE_MACRO) - every
disassembly line is now exactly one 4-byte instruction. With that,
"how many instructions does this line span" is always 1, so the
getInstructionSizeAt() byte-size queries in the legacy Windows and ImGui
debuggers are gone too; step requests just pass 1. Core_RequestCPUStep's
stepSize is now consistently in instructions everywhere.
Also fixes the PPSSPPHeadless build, broken since 0ed1f3e added
OpenWebDebugger() (which calls System_LaunchUrl) without a headless stub.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
Headless.cpp, NativeApp.cpp, and SDLMain.cpp each still hand-parsed a few
argv flags directly (mount/log/state/ignore/loglevel in headless and the
app, xres/yres/dpi/scale in SDL), duplicating and in some cases conflicting
with the shared CommandLineOptions parser. Consolidate all of it into
CmdLine.cpp/.h so there's a single source of truth, and drop the now-dead
remain_argc/remain_argv filtering in SDLMain.cpp since NativeInit no longer
reads argv itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PfFvWzpHxErWgRhKqqSewN
Reordered main() to: g_Config.RestoreDefaults() -> headless's own hardcoded
overrides for deterministic test execution -> cmdLineOptions.ApplyToConfig()
-> CoreParameter setup -> launch. Previously ApplyToConfig() ran first and
got silently overwritten by both RestoreDefaults() and the hardcoded
overrides, so no command-line flag touching a g_Config setting actually took
effect for a real run (this is what required the "set iRemoteISOPort again"
workaround for --debugger=PORT, now removed).
The two hardcoded lines that used to read coreParameter.renderScaleFactor/
gpuCore now compute the same values directly from cmdLineOptions/glWorking/
gpuCore instead, since CoreParameter is no longer built yet at that point -
it's now constructed after ApplyToConfig(), which is what let those two
comments' "above"/"below" wording get corrected too.
Verified: --debugger=PORT still binds the exact requested port and pauses
the CPU, and a normal --compare test run (cpu_alu) still passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6
The headless build's own --debugger=PORT never actually worked on
Windows: headless/Headless.cpp never called net::Init() (WSAStartup),
so socket binding silently failed ("Unable to listen on any port
(debugger - webserver)"). Fixed by calling net::Init()/net::Shutdown()
there, gated on --debugger being passed since headless has no other use
for networking.
With that confirmed working, move parsing into the shared
Core/CmdLine.cpp auto-param table as a single --debugger=PORT option
(0 = pick automatically) available on both CmdLineMode::Application and
CmdLineMode::Headless, replacing the previous app-only boolean
--debugger flag and headless's separate ad hoc argv scan for it.
Behavior differs deliberately by build, same as before:
- App: just starts the debugger, game boots and runs normally.
- Headless: also forces coreParameter.startBreak = true (break before
anything runs), as it always has. Headless re-applies iRemoteISOPort
after its own g_Config.RestoreDefaults() call, which runs after
ApplyToConfig() and would otherwise wipe the requested port.
Verified live: headless now binds the requested port and responds to
cpu.status/game.status; the app build binds the requested port and
runs the game normally (not paused).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDNwPPuidmNxQGRJxBuRL6