From 65495c76c0aa731cbfdfd471dcd6d5222078c959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 12 Aug 2026 10:24:01 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_019SKhm9wKEQzRUsx9mTrrtQ --- Core/CmdLine.cpp | 11 ++- Core/CmdLine.h | 2 + Core/MIPS/MIPSTables.cpp | 141 ++++++++++++++++++++++++++++++++++++++- Core/MIPS/MIPSTables.h | 5 ++ headless/Headless.cpp | 7 ++ 5 files changed, 162 insertions(+), 4 deletions(-) diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index ef0db77bd8..564b5dd35a 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -1,3 +1,4 @@ +#include #include #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) { diff --git a/Core/CmdLine.h b/Core/CmdLine.h index 89fe4cc995..d9bc472fd5 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -107,6 +107,8 @@ struct CommandLineOptions { std::optional maxScreenshotError; // Headless: test names to skip. May be specified more than once. std::vector ignoredTests; + // Headless: generate C++ interpreter dispatch code to stdout and exit. + std::optional generateInterpreterDispatch; // SDL only. std::optional xres; diff --git a/Core/MIPS/MIPSTables.cpp b/Core/MIPS/MIPSTables.cpp index fb717fb219..69abf3041b 100644 --- a/Core/MIPS/MIPSTables.cpp +++ b/Core/MIPS/MIPSTables.cpp @@ -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 + #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 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 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; diff --git a/Core/MIPS/MIPSTables.h b/Core/MIPS/MIPSTables.h index 0a3c39519b..8c55645776 100644 --- a/Core/MIPS/MIPSTables.h +++ b/Core/MIPS/MIPSTables.h @@ -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(); diff --git a/headless/Headless.cpp b/headless/Headless.cpp index a61aac1ffc..9958232639 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -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.