build: MSVC portability for runtime and generated code

- psx_cyc.h / psx_cycles.c: _BitScanForward/_BitScanReverse shims for
  __builtin_ctz/__builtin_clz under _MSC_VER
- full_function_emitter.cpp / main_psx.cpp: emit a portable .CRT$XCU
  init-pointer instead of GCC-only __attribute__((constructor)) in
  generated dispatch/game code
- main.cpp: declare cross-language globals extern "C" so MSVC links
  them against their C definitions (no-op for GCC/Clang)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Martin Penkava
2026-07-12 16:53:38 +02:00
committed by Matthew Stanley
parent dde268dc0f
commit 05d4f35bd6
7 changed files with 190 additions and 2 deletions
@@ -0,0 +1,41 @@
# MSVC portability from Shaneomac PR #16
## Source and credit
- Source PR: [mstan/psxrecomp#16](https://github.com/mstan/psxrecomp/pull/16)
- Source commit: [`df4cc5c70ab1e5d0fd2a8f817dd346905d4d0eeb`](https://github.com/mstan/psxrecomp/commit/df4cc5c70ab1e5d0fd2a8f817dd346905d4d0eeb)
- Author preserved by cherry-pick: Martin Penkava
`<mpenkava1337@gmail.com>`.
- The source commit's Claude Fable 5 co-author trailer is also preserved.
## Adapted scope
The source commit was applied as a focused portability change:
- MSVC bit-scan intrinsics replace unavailable GCC/Clang builtins;
- generated CPS startup markers use an MSVC `.CRT$XCU` initializer and retain
the constructor path for GCC/Clang; and
- runtime globals defined by C translation units receive explicit C linkage in
the C++ entry point.
`recompiler/tests/test_msvc_cps_codegen.py` generates a minimal game dispatch
and checks that both compiler startup paths are emitted.
## Validation in this branch
- Visual Studio 2022/MSVC 19.41 built `psxrecomp-game`, `psxrecomp-bios`, and
`l2_structural_test`; the structural suite passed 44/44.
- The MSVC-built game recompiler passed
`test_msvc_cps_codegen.py` on generated output.
- A freshly generated BIOS runtime compiled all 63 C/C++ object steps under
MSVC, including the new intrinsic and C-linkage paths. The final BIOS-only
link remains blocked by the baseline's unrelated missing
`psx_game_address_in_text` and `psx_game_text_native_ok` definitions. No
change for that pre-existing game-symbol contract is included here.
## Explicitly excluded
No other PR #16 material was imported. In particular, this branch excludes the
SmackDown-specific batch files, dynamic-code discovery fallback, SPU changes,
primitive rejection, Vulkan diagnostics/optimization, and CRT shaders. Those
items require independent review and provenance records.
+9 -1
View File
@@ -1656,9 +1656,17 @@ void FullFunctionEmitter::emit_dispatch(
// RECURSION_BUG.md §25 — mark CPS mode at startup for runtime code that
// must emit the CPS contract (the overlay sljit JIT, overlay_sljit.c).
out += "\n/* CPS runtime-mode marker (overlay sljit JIT reads g_psx_cps_mode). */\n";
out += "__attribute__((constructor)) static void psx_cps_mark_bios(void) {\n";
out += "static void psx_cps_mark_bios(void) {\n";
out += " extern int g_psx_cps_mode; g_psx_cps_mode = 1;\n";
out += "}\n";
// Run psx_cps_mark_bios before main(). __attribute__((constructor)) is
// GCC/Clang-only; MSVC uses a static initializer pointer in .CRT$XCU.
out += "#if defined(_MSC_VER)\n";
out += "#pragma section(\".CRT$XCU\", read)\n";
out += "__declspec(allocate(\".CRT$XCU\")) static void (*psx_cps_mark_bios_ctor)(void) = psx_cps_mark_bios;\n";
out += "#else\n";
out += "__attribute__((constructor)) static void psx_cps_mark_bios_ctor(void) { psx_cps_mark_bios(); }\n";
out += "#endif\n";
}
}
+9 -1
View File
@@ -1086,9 +1086,17 @@ int main(int argc, char** argv) {
// JIT (overlay_sljit.c) emits the CPS contract. Static ctor: no clash
// with the BIOS dispatch's marker.
ds << "\n/* CPS runtime-mode marker (overlay sljit JIT reads g_psx_cps_mode). */\n";
ds << "__attribute__((constructor)) static void psx_cps_mark_game(void) {\n";
ds << "static void psx_cps_mark_game(void) {\n";
ds << " extern int g_psx_cps_mode; g_psx_cps_mode = 1;\n";
ds << "}\n";
// Run psx_cps_mark_game before main(). __attribute__((constructor)) is
// GCC/Clang-only; MSVC uses a static initializer pointer in .CRT$XCU.
ds << "#if defined(_MSC_VER)\n";
ds << "#pragma section(\".CRT$XCU\", read)\n";
ds << "__declspec(allocate(\".CRT$XCU\")) static void (*psx_cps_mark_game_ctor)(void) = psx_cps_mark_game;\n";
ds << "#else\n";
ds << "__attribute__((constructor)) static void psx_cps_mark_game_ctor(void) { psx_cps_mark_game(); }\n";
ds << "#endif\n";
}
std::ofstream dispatch_file(dispatch_filename);
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Verify that CPS game output carries both MSVC and GCC/Clang startup hooks.
Usage: python test_msvc_cps_codegen.py [--recompiler <psxrecomp-game.exe>]
"""
import argparse
import os
import struct
import subprocess
import sys
import tempfile
LOAD = 0x80010000
def make_exe():
header = bytearray(2048)
header[0:8] = b"PS-X EXE"
struct.pack_into("<I", header, 0x10, LOAD)
struct.pack_into("<I", header, 0x18, LOAD)
body = struct.pack("<II", 0x03E00008, 0x00000000) # jr ra; nop
struct.pack_into("<I", header, 0x1C, len(body))
return bytes(header) + body
def generate_dispatch(recompiler):
with tempfile.TemporaryDirectory() as tmp:
exe = os.path.join(tmp, "tiny.psx")
seeds = os.path.join(tmp, "seeds.txt")
out = os.path.join(tmp, "out")
with open(exe, "wb") as stream:
stream.write(make_exe())
with open(seeds, "w", encoding="utf-8") as stream:
stream.write(f"0x{LOAD:08X}\n")
result = subprocess.run(
[recompiler, exe, "--seeds", seeds, "--out-dir", out],
capture_output=True,
text=True,
)
if result.returncode:
raise RuntimeError(result.stderr or result.stdout)
dispatches = [
os.path.join(out, name)
for name in os.listdir(out)
if name.endswith("_dispatch.c")
]
if len(dispatches) != 1:
raise RuntimeError(
"expected one generated dispatch, found "
f"{[os.path.basename(path) for path in dispatches]}; "
f"generator output:\n{result.stdout}"
)
with open(dispatches[0], encoding="utf-8") as stream:
return stream.read()
def main():
here = os.path.dirname(os.path.abspath(__file__))
default = os.path.normpath(
os.path.join(here, "..", "build", "psxrecomp-game.exe")
)
parser = argparse.ArgumentParser()
parser.add_argument("--recompiler", default=default)
args = parser.parse_args()
if not os.path.isfile(args.recompiler):
print(f"FAIL: recompiler not found: {args.recompiler}", file=sys.stderr)
return 2
try:
generated = generate_dispatch(args.recompiler)
except (OSError, RuntimeError) as exc:
print(f"FAIL: generation failed: {exc}", file=sys.stderr)
return 1
required = (
"static void psx_cps_mark_game(void)",
"#if defined(_MSC_VER)",
'#pragma section(".CRT$XCU", read)',
'__declspec(allocate(".CRT$XCU")) static void '
"(*psx_cps_mark_game_ctor)(void) = psx_cps_mark_game;",
"__attribute__((constructor)) static void psx_cps_mark_game_ctor(void) "
"{ psx_cps_mark_game(); }",
)
missing = [text for text in required if text not in generated]
if missing:
for text in missing:
print(f"FAIL: generated dispatch is missing: {text}", file=sys.stderr)
return 1
print("PASS: generated CPS startup hooks cover MSVC and GCC/Clang.")
return 0
if __name__ == "__main__":
sys.exit(main())
+9
View File
@@ -28,6 +28,9 @@
#define PSX_CYC_H
#include <stdint.h>
#if defined(_MSC_VER)
#include <intrin.h> /* MSVC intrinsics: _BitScanForward (no __builtin_ctz) */
#endif
#include "cpu_state.h" /* CPUState (guard-safe: cpu_state.h includes us last) */
#ifdef __cplusplus
@@ -49,7 +52,13 @@ static inline void psx_cyc_base(CPUState* cpu) {
static inline void psx_cyc_deps(CPUState* cpu, uint32_t reg_mask) {
reg_mask &= 0xFFFFFFFEu; /* never touch ReadAbsorb[0] */
while (reg_mask) {
#if defined(_MSC_VER)
unsigned long _psx_ctz_idx;
_BitScanForward(&_psx_ctz_idx, reg_mask);
unsigned n = (unsigned)_psx_ctz_idx;
#else
unsigned n = (unsigned)__builtin_ctz(reg_mask);
#endif
cpu->read_absorb[n] = 0u;
reg_mask &= reg_mask - 1u;
}
+15
View File
@@ -93,6 +93,21 @@
extern "C" uint64_t gte_get_exec_count(void);
/* Cross-language globals defined in C translation units. Declared extern "C" at
* file scope so MSVC gives them C linkage (matching the C definitions); without
* this MSVC name-mangles the C++ references and they fail to link. GCC/Clang do
* not mangle namespace-scope variables, so this is a no-op there. The existing
* block-scope `extern` redeclarations inside functions inherit this C linkage. */
extern "C" {
extern uint64_t psx_cycle_count;
extern uint64_t s_frame_count;
extern uint32_t g_overlay_region_floor;
extern int g_psx_cps_mode;
extern uint64_t g_slice_fired, g_slice_irq_taken, g_dirty_ram_insns_run;
extern uint32_t g_slice_exit_pc, g_slice_exit_reason, g_slice_exit_iter;
extern uint32_t g_slice_exit_dispatchable, g_slice_exit_dirty, g_slice_exit_in_text, g_slice_exit_want;
}
/* memory.c */
extern "C" void memory_init(const char* bios_path);
extern "C" void memory_set_sr_ptr(const uint32_t *p);
+9
View File
@@ -3,6 +3,9 @@
#include "psx_cycles.h"
#include "cpu_state.h"
#include <stdlib.h>
#if defined(_MSC_VER)
#include <intrin.h> /* MSVC intrinsics: _BitScanReverse (no __builtin_clz) */
#endif
#include "cdrom.h"
#include "dma.h"
#include "interrupts.h"
@@ -451,7 +454,13 @@ static const uint8_t PSX_MULT_TAB24[24] = {
static inline uint32_t psx_clz32(uint32_t v) {
/* v is never 0 here (callers OR in 0x400). */
#if defined(_MSC_VER)
unsigned long _idx;
_BitScanReverse(&_idx, v); /* index of highest set bit */
return (uint32_t)(31u - _idx);
#else
return (uint32_t)__builtin_clz(v);
#endif
}
uint32_t psx_mult_latency_s(uint32_t rs) { /* MULT (signed): sign-fold magnitude */