Add a codegen tool to generate a fast switch-tree interpreter dispatcher

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
This commit is contained in:
Henrik Rydgård
2026-08-12 10:24:01 +02:00
co-authored by Claude Sonnet 5
parent d718bda178
commit 65495c76c0
5 changed files with 162 additions and 4 deletions
+8 -3
View File
@@ -1,3 +1,4 @@
#include <algorithm>
#include <cstdlib>
#include "Core/Config.h"
@@ -188,6 +189,7 @@ static const CommandLineParam g_autoParams[] = {
{POFF(maxScreenshotError), CmdParamType::Double, "max-mse", '\0', "Maximum allowed MSE error for screenshot comparison", CmdLineMode::Headless},
{POFF(mountIso), CmdParamType::String, "mount", 'm', "Mount ISO/CSO on umd1:", CmdLineMode::Headless},
{POFF(odsLog), CmdParamType::Bool, "odslog", 'o', "Also log through OutputDebugString (Windows)", CmdLineMode::Headless},
{POFF(generateInterpreterDispatch), CmdParamType::Bool, "generate-interpreter-dispatch", '\0', "Generate C++ interpreter dispatch code (ExecInstruction) to stdout and exit", CmdLineMode::Headless},
{POFF(resolutionScale), CmdParamType::Int, "resolution-scale", '\0', "Set the resolution scale factor"},
{POFF(debuggerPort), CmdParamType::Int, "debugger", '\0', "Enable the WebSocket debugger on this port (0 = pick automatically); see docs/WebSocketDebugger.md"},
{POFF(bootVSH), CmdParamType::Bool, "vsh", '\0', "Boot the VSH (requires files dumped from a PSP in the flash0 directory)"},
@@ -257,12 +259,15 @@ int CommandLineOptions::PrintUsage(const char *progname, const char *situationTe
// Skip mode-irrelevant parameters in help.
continue;
}
char key[25]{};
char key[64]{};
snprintf(key, ARRAY_SIZE(key), " --%s%s%s", param.longName,
param.shortName ? ", -" : "",
param.shortName ? std::string(1, param.shortName).c_str() : "");
// Fill key with spacing.
for (size_t j = strlen(key); j < ARRAY_SIZE(key) - 1; ++j) {
// Fill key with spacing, keeping at least one space before the doc string
// even if the name itself ran past the normal column width.
size_t padTo = std::max(strlen(key) + 1, (size_t)24);
padTo = std::min(padTo, ARRAY_SIZE(key) - 1);
for (size_t j = strlen(key); j < padTo; ++j) {
key[j] = ' ';
}
if (param.type == CmdParamType::Enum) {
+2
View File
@@ -107,6 +107,8 @@ struct CommandLineOptions {
std::optional<double> maxScreenshotError;
// Headless: test names to skip. May be specified more than once.
std::vector<std::string> ignoredTests;
// Headless: generate C++ interpreter dispatch code to stdout and exit.
std::optional<bool> generateInterpreterDispatch;
// SDL only.
std::optional<int> xres;
+140 -1
View File
@@ -15,6 +15,8 @@
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <vector>
#include "Common/StringUtils.h"
#include "Core/Core.h"
@@ -72,6 +74,7 @@ struct MIPSInstruction {
MIPSComp::MIPSCompileFunc compile;
MIPSDisFunc disasm;
MIPSInterpretFunc interpret;
const char *interpretName;
//MIPSInstructionInfo information;
MIPSInfo flags;
};
@@ -80,7 +83,7 @@ struct MIPSInstruction {
#define INVALID_X_8 INVALID,INVALID,INVALID,INVALID,INVALID,INVALID,INVALID,INVALID
#define ENCODING(a) {a}
#define INSTR(name, comp, dis, inter, flags) {Instruc, name, comp, dis, inter, MIPSInfo(flags)}
#define INSTR(name, comp, dis, inter, flags) {Instruc, name, comp, dis, inter, #inter, MIPSInfo(flags)}
#define JITFUNC(f) (&MIPSFrontendInterface::f)
@@ -899,6 +902,142 @@ const MIPSInstruction *MIPSGetInstruction(MIPSOpcode op) {
return instr;
}
// Emits one level of the dispatch tree as a switch statement, recursing into nested
// switches for ENCODING() redirects. Mirrors the traversal MIPSGetInstruction() does at
// runtime, but resolves it into source text once instead of walking it on every call.
static void EmitDispatchLevel(std::string &out, MipsEncoding encoding, int indent) {
const MIPSInstruction *table = mipsTables[encoding];
const EncodingBitsInfo &bits = encodingBits[encoding];
_dbg_assert_(table != nullptr);
std::string ind(indent * 4, ' ');
std::string ind2((indent + 1) * 4, ' ');
// Group indices that share identical behavior (same leaf interpret function, or same
// redirect target) so they can share one case block instead of duplicating it.
u32 size = bits.mask + 1;
std::vector<bool> handled(size, false);
bool anyCases = false;
for (u32 i = 0; i < size; i++) {
const MIPSInstruction &probe = table[i];
bool isInvalid = probe.altEncoding == Inval || (probe.altEncoding == Instruc && probe.interpret == nullptr);
if (!isInvalid) {
anyCases = true;
break;
}
}
if (!anyCases) {
// Every slot in this level is invalid (or unimplemented) - no point emitting an
// empty switch (MSVC warns C4065 on a switch with only a default label).
out += ind + "goto slow_path;\n";
return;
}
out += ind + StringFromFormat("switch ((op.encoding >> %d) & 0x%x) {\n", bits.shift, bits.mask);
for (u32 i = 0; i < size; i++) {
if (handled[i])
continue;
const MIPSInstruction &instr = table[i];
if (instr.altEncoding == Inval)
continue; // Falls through to default (slow path).
if (instr.altEncoding == Instruc && instr.interpret == nullptr)
continue; // No interpreter implemented for this one (e.g. tge/tlt/teq/...) - slow path.
handled[i] = true;
std::vector<u32> group{ i };
for (u32 j = i + 1; j < size; j++) {
if (handled[j])
continue;
const MIPSInstruction &other = table[j];
bool same;
if (instr.altEncoding == Instruc && other.altEncoding == Instruc)
// Cycle count must match too - it's baked into the generated "return N;",
// so two leaves sharing a handler but not a cycle count can't be merged.
same = instr.interpret == other.interpret && instr.flags.cycles == other.flags.cycles;
else if (instr.altEncoding != Instruc && other.altEncoding != Instruc)
same = instr.altEncoding == other.altEncoding;
else
same = false;
if (same) {
group.push_back(j);
handled[j] = true;
}
}
if (instr.altEncoding == Instruc) {
std::string names;
for (u32 idx : group) {
if (!names.empty())
names += ", ";
names += table[idx].name;
}
out += ind + StringFromFormat("// %s\n", names.c_str());
for (u32 idx : group)
out += ind + StringFromFormat("case %d:\n", idx);
out += ind2 + StringFromFormat("MIPSInt::%s(op);\n", instr.interpretName);
out += ind2 + StringFromFormat("return %d;\n", (int)instr.flags.cycles);
} else {
for (u32 idx : group)
out += ind + StringFromFormat("case %d:\n", idx);
out += ind + "{\n";
EmitDispatchLevel(out, instr.altEncoding, indent + 1);
out += ind + "}\n";
// No trailing return here - the nested switch above always either returns
// a value directly from a case, or falls to its own default's "goto slow_path;".
}
}
out += ind + "default:\n";
out += ind2 + "goto slow_path;\n";
out += ind + "}\n";
}
// Generates a full, compilable Core/MIPS/InterpreterDispatch.cpp: a fast
// int ExecInstruction(MIPSOpcode op) dispatcher, built by resolving the tables above into
// a nested switch tree at generation time, so each real instruction is reached by a direct
// call instead of MIPSGetInstruction()'s per-instruction table walk plus indirect call
// through instr->interpret. Leaves call straight into the existing MIPSInt::Int_* handlers
// - the tables only record which handler an opcode maps to, not the handler's behavior, so
// that's the only thing there is to call - and return that instruction's fixed cycle count
// (baked in at generation time, same value MIPSGetInstructionCycleEstimate() would have
// returned) so the caller can still track downcount. Operates on the global currentMIPS,
// same as the Int_* handlers do; anything not recognized (including opcodes 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.
//
// Regenerate by running `PPSSPPHeadless --generate-interpreter-dispatch > Core/MIPS/InterpreterDispatch.cpp`
// after the tables above change. Do not hand-edit the generated file.
std::string GenerateInterpreterDispatch() {
std::string out;
out += "// Copyright (c) 2012- PPSSPP Project.\n\n";
out += "// This program is free software: you can redistribute it and/or modify\n";
out += "// it under the terms of the GNU General Public License as published by\n";
out += "// the Free Software Foundation, version 2.0 or later versions.\n\n";
out += "// This program is distributed in the hope that it will be useful,\n";
out += "// but WITHOUT ANY WARRANTY; without even the implied warranty of\n";
out += "// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n";
out += "// GNU General Public License 2.0 for more details.\n\n";
out += "// A copy of the GPL 2.0 should have been included with the program.\n";
out += "// If not, see http://www.gnu.org/licenses/\n\n";
out += "// Official git repository and contact information can be found at\n";
out += "// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.\n\n";
out += "// AUTO-GENERATED by `PPSSPPHeadless --generate-interpreter-dispatch` from the\n";
out += "// tables in MIPSTables.cpp - do not hand-edit, regenerate instead.\n\n";
out += "#include \"Core/MIPS/MIPS.h\"\n";
out += "#include \"Core/MIPS/MIPSTables.h\"\n";
out += "#include \"Core/MIPS/Interpreter.h\"\n";
out += "#include \"Core/MIPS/InterpreterVFPU.h\"\n\n";
out += "int ExecInstruction(MIPSOpcode op) {\n";
EmitDispatchLevel(out, Imme, 1);
out += "slow_path:\n";
out += "\tMIPSInterpret(op);\n";
out += "\treturn MIPSGetInstructionCycleEstimate(op);\n";
out += "}\n";
return out;
}
void MIPSCompileOp(MIPSOpcode op, MIPSComp::MIPSFrontendInterface *jit) {
if (op == 0)
return;
+5
View File
@@ -139,3 +139,8 @@ int MIPSGetInstructionCycleEstimate(MIPSOpcode op);
int MIPSGetMemoryAccessSize(MIPSOpcode op);
const char *MIPSGetName(MIPSOpcode op);
std::string MIPSDisasmAt(u32 compilerPC);
// Generates the full contents of Core/MIPS/InterpreterDispatch.cpp: a fast switch-tree
// int ExecInstruction(MIPSOpcode op) dispatcher, derived from the tables in MIPSTables.cpp.
// See GenerateInterpreterDispatch()'s comment (in the .cpp) for details.
std::string GenerateInterpreterDispatch();
+7
View File
@@ -54,6 +54,7 @@
#include "Core/Core.h"
#include "Core/CoreTiming.h"
#include "Core/EmuThread.h"
#include "Core/MIPS/MIPSTables.h"
#include "Core/System.h"
#include "Core/WebServer.h"
#include "Core/HLE/sceUtility.h"
@@ -550,6 +551,12 @@ int main(int argc, const char* argv[]) {
break;
}
if (cmdLineOptions.generateInterpreterDispatch.value_or(false)) {
std::string code = GenerateInterpreterDispatch();
fwrite(code.data(), 1, code.size(), stdout);
return 0;
}
// Needed before any sockets can be used (WSAStartup on Windows) - without this, the
// WebSocket debugger silently fails to listen. Only done when requested since headless
// otherwise has no use for networking.