mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
build: add opt-in runtime IPO experiment with validation gates
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
include_guard(GLOBAL)
|
||||
|
||||
option(PSX_RUNTIME_IPO
|
||||
"Experimental IPO/LTO for optimized runtime targets (requires supported C/C++ toolchain)"
|
||||
OFF)
|
||||
|
||||
function(psxrecomp_apply_runtime_ipo target)
|
||||
# OFF must not alter a parent project's explicit IPO policy or add flags.
|
||||
if(NOT PSX_RUNTIME_IPO)
|
||||
return()
|
||||
endif()
|
||||
include(CheckIPOSupported)
|
||||
check_ipo_supported(RESULT _psx_ipo_ok OUTPUT _psx_ipo_error LANGUAGES C CXX)
|
||||
if(NOT _psx_ipo_ok)
|
||||
message(FATAL_ERROR
|
||||
"PSX_RUNTIME_IPO=ON was requested, but C/C++ IPO is unsupported.\n"
|
||||
"Use a compatible compiler/linker or configure PSX_RUNTIME_IPO=OFF.\n"
|
||||
"${_psx_ipo_error}")
|
||||
endif()
|
||||
# Target/configuration-scoped: do not silently enable IPO in third-party
|
||||
# libraries, build helpers, Debug, or custom configurations.
|
||||
set_property(TARGET ${target} PROPERTY INTERPROCEDURAL_OPTIMIZATION FALSE)
|
||||
set_property(TARGET ${target} PROPERTY INTERPROCEDURAL_OPTIMIZATION_DEBUG FALSE)
|
||||
foreach(_psx_ipo_config RELEASE RELWITHDEBINFO MINSIZEREL)
|
||||
set_property(TARGET ${target}
|
||||
PROPERTY INTERPROCEDURAL_OPTIMIZATION_${_psx_ipo_config} TRUE)
|
||||
endforeach()
|
||||
message(STATUS "psxrecomp: experimental runtime IPO enabled for ${target} (optimized configurations)")
|
||||
endfunction()
|
||||
@@ -0,0 +1,73 @@
|
||||
# Draft: opt-in runtime IPO/LTO validation
|
||||
|
||||
Status: experimental, OFF by default. Tracking: `beads-eio.3.149`.
|
||||
The PE report is supplemental evidence, not a locally reproduced benchmark.
|
||||
No IRQ, device deadline, I-cache, guest codegen or overlay ABI behavior changes.
|
||||
|
||||
## Build contract
|
||||
|
||||
`-DPSX_RUNTIME_IPO=ON` checks C and C++ IPO support and enables it only on runtime
|
||||
targets, for Release, RelWithDebInfo and MinSizeRel. Debug and custom configurations
|
||||
are not enabled by this option. Unsupported toolchains fail with an explicit
|
||||
diagnostic rather than silently ignoring a requested experiment.
|
||||
|
||||
The default OFF path changes no target property, including an explicit parent
|
||||
project IPO policy. This is not a project-wide
|
||||
`CMAKE_INTERPROCEDURAL_OPTIMIZATION=ON`: third-party libraries, emitters and other
|
||||
targets are not opted in. This narrower scope is deliberate for validation and
|
||||
may not reproduce the contributor's project-wide result.
|
||||
|
||||
CMake provides the target/configuration properties and support probe used here:
|
||||
[IPO target property](https://cmake.org/cmake/help/latest/prop_tgt/INTERPROCEDURAL_OPTIMIZATION.html),
|
||||
[CheckIPOSupported](https://cmake.org/cmake/help/latest/module/CheckIPOSupported.html).
|
||||
|
||||
## PGO is a separate variable
|
||||
|
||||
The existing `pgo-train` command configures generate/debug-ON, builds, trains,
|
||||
then configures use/debug-OFF and builds again. It reports failures. The supplied
|
||||
early-stop report does not match that successful control path; do not change
|
||||
the pipeline without the failing revision, command and exit/error evidence.
|
||||
|
||||
Use the existing `--cmake-extra` interface to pass the IPO option to both PGO
|
||||
build phases if testing the combination. Inspect actual compile/link commands;
|
||||
an ON cache entry alone does not prove every target was built with IPO.
|
||||
|
||||
Runtime CMake PGO and this IPO option affect linked runtime/generated C/C++.
|
||||
`compile_overlays.py` launches a separate `-shared -O2` compiler command with
|
||||
neither profile flags nor IPO. They do not directly profile/optimize PE's dynamic
|
||||
decoder DLL. Gains may come from linked code or callbacks, which must be measured.
|
||||
|
||||
Do not replace a user's working profiles or cache when running experiments.
|
||||
Use isolated project/build/save directories and record profile identities.
|
||||
|
||||
## Automated checks
|
||||
|
||||
- Default OFF leaves target properties unset; parent IPO choice preserved.
|
||||
- Injected unsupported toolchain fails only when IPO is requested.
|
||||
- Real supported-toolchain two-file C/C++ IPO build/link/execution.
|
||||
- Debug compilation emits no IPO flags; unrelated target remains unchanged.
|
||||
- Mocked PGO success/failure orchestration and direct overlay compiler command.
|
||||
These mocks do not validate real profile collection or performance.
|
||||
|
||||
The real IPO smoke check explicitly reports a skip when the tested compiler
|
||||
cannot support IPO; that does not count as a positive link/run validation.
|
||||
|
||||
## Evidence needed before promotion
|
||||
|
||||
1. Repeated content-aligned LTO off/on trials with the same PGO profile, compiler,
|
||||
debug flags, assets, input and cache state. LTO-only/full-factorial trials can
|
||||
answer broader attribution questions, but are not required merely to measure
|
||||
LTO's incremental effect with one fixed profile.
|
||||
2. Per-frame tails/maxima and audio underruns, not only periodic average FPS.
|
||||
The reported small incremental gain overlaps the earlier run-to-run spread.
|
||||
3. Regenerated title regression: Tomba, MMX6 and Ape, including LLE boot, FMV,
|
||||
menus and gameplay. Do not infer unchanged behavior from frame counts alone.
|
||||
4. Shipping compiler/linker/platform and launcher-enabled coverage, with profile
|
||||
mismatch/missing-function warnings visible during profile validation. Record
|
||||
binary size, build/link time and peak memory as well as host performance.
|
||||
5. Exact PE configuration, profile/capture identities and raw measurements from
|
||||
the contributor. Their project-wide IPO result is not a guarantee for this
|
||||
narrower runtime-only option or other titles.
|
||||
|
||||
Leave this PR in draft pending that feedback. Do not claim a performance win or
|
||||
enable IPO by default merely because the isolated compiler test passes.
|
||||
@@ -502,11 +502,21 @@ if(BUILD_TESTING)
|
||||
add_test(NAME reachable_discovery_test COMMAND reachable_discovery_test)
|
||||
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||
add_test(NAME runtime_ipo_configuration
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_runtime_ipo.py
|
||||
--cmake ${CMAKE_COMMAND} --cc ${CMAKE_C_COMPILER}
|
||||
--cxx ${CMAKE_CXX_COMPILER} --ninja ${CMAKE_MAKE_PROGRAM})
|
||||
endif()
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME optional_netplay_auth
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_optional_netplay_auth.py
|
||||
--cxx ${CMAKE_CXX_COMPILER})
|
||||
add_test(NAME pgo_rebuild_flow
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_pgo_rebuild_flow.py)
|
||||
add_test(
|
||||
NAME cli_project_packaging
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""No real build, game launch, cache cleanup, or profile mutation."""
|
||||
import argparse
|
||||
import contextlib
|
||||
import io
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from unittest import mock
|
||||
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
sys.path[:0] = [str(root), str(root / "tools")]
|
||||
import psxrecomp_cli as cli
|
||||
import compile_overlays as overlays
|
||||
|
||||
# Existing source paths satisfy the file guards; ALL consumers of their
|
||||
# contents and all mutating/build/process operations are mocked below.
|
||||
def check_flow(fail_training=False):
|
||||
args = argparse.Namespace(config=str(root / "CLAUDE.md"), project_root=str(root),
|
||||
build_dir="review-not-created", target="psx-runtime", exe_basename="psx-runtime",
|
||||
no_pgo=True, force_pgo=False, disc=str(root / "CLAUDE.md"), cmake_extra=[],
|
||||
train_secs=100, train_runs=1)
|
||||
progress = mock.Mock()
|
||||
events = []
|
||||
def configure(*a, **kw):
|
||||
events.append(("configure", kw["pgo"], kw["extra"][-1]))
|
||||
def train(*a, **kw):
|
||||
events.append(("train", kw["train_secs"], kw["train_runs"]))
|
||||
if fail_training:
|
||||
raise RuntimeError("synthetic training failure")
|
||||
with contextlib.ExitStack() as stack:
|
||||
replacements = dict(load_sections=lambda _: {},
|
||||
activate_embedded_toolchain=lambda *a: True,
|
||||
clamp_future_mtimes=lambda *a, **kw: 0,
|
||||
_cmake_configure=configure,
|
||||
_cmake_build=lambda *a: events.append(("build",)),
|
||||
run_pgo_train=train,
|
||||
_resolve_runtime_exe=lambda *a: (root / "fake-runtime.exe", None))
|
||||
for name, replacement in replacements.items():
|
||||
stack.enter_context(mock.patch.object(cli, name, replacement))
|
||||
result = cli.cmd_pgo_train(args, progress)
|
||||
expected = [("configure", "generate", "-DPSX_DEBUG_TOOLS=ON"), ("build",), ("train", 100, 1)]
|
||||
if not fail_training:
|
||||
expected += [("configure", "use", "-DPSX_DEBUG_TOOLS=OFF"), ("build",)]
|
||||
assert result == cli.EXIT_OK
|
||||
assert progress.result.call_args.kwargs["ok"] is True
|
||||
else:
|
||||
assert result == cli.EXIT_ERROR
|
||||
progress.result.assert_not_called()
|
||||
progress.error.assert_called_once()
|
||||
assert events == expected, events
|
||||
print("PGO flow", "failure" if fail_training else "success", events)
|
||||
|
||||
check_flow()
|
||||
check_flow(True)
|
||||
with mock.patch.object(overlays, "_toolchain_env", return_value=({}, "fake-toolchain")), \
|
||||
mock.patch.object(overlays.subprocess, "run", return_value=mock.Mock(returncode=0)) as run, \
|
||||
contextlib.redirect_stdout(io.StringIO()):
|
||||
assert overlays._compile_dll_direct("fake.c", "fake.dll", [], gcc="fake-gcc")
|
||||
command = run.call_args.args[0]
|
||||
assert "-O2" in command and "-shared" in command
|
||||
assert not any("profile" in arg.lower() for arg in command)
|
||||
print("Overlay DLL command (mocked subprocess):", command)
|
||||
print("PASS: successful PGO finishes use/debug-OFF; failures report failure; overlay DLL command has no PGO flags")
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Isolated configure/build checks: no BIOS, game or network required."""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
def run(command, success=True):
|
||||
p = subprocess.run(list(map(str, command)), capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace", timeout=180)
|
||||
if success is not None and (p.returncode == 0) != success:
|
||||
raise AssertionError(f"unexpected exit {p.returncode}: {command}\n{p.stdout}\n{p.stderr}")
|
||||
return p
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--cmake", required=True)
|
||||
ap.add_argument("--cc", required=True)
|
||||
ap.add_argument("--cxx", required=True)
|
||||
ap.add_argument("--ninja", required=True)
|
||||
args = ap.parse_args()
|
||||
with tempfile.TemporaryDirectory(prefix="psx-ipo-") as temp:
|
||||
root = Path(temp)
|
||||
(root / "first.c").write_text("int other(void); int main(void) { return other() != 42; }\n")
|
||||
(root / "second.cpp").write_text('extern "C" int other(void) { return 42; }\n')
|
||||
(root / "fake_modules").mkdir()
|
||||
(root / "fake_modules/CheckIPOSupported.cmake").write_text('''
|
||||
function(check_ipo_supported)
|
||||
set(_psx_ipo_ok FALSE PARENT_SCOPE)
|
||||
set(_psx_ipo_error "synthetic unsupported toolchain" PARENT_SCOPE)
|
||||
endfunction()
|
||||
''')
|
||||
(root / "CMakeLists.txt").write_text('''
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(IPOSmoke C CXX)
|
||||
if(SIMULATE_UNSUPPORTED)
|
||||
list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/fake_modules")
|
||||
endif()
|
||||
include("''' + (ROOT / "cmake/psx_runtime_ipo.cmake").as_posix() + '''")
|
||||
add_executable(subject first.c second.cpp)
|
||||
add_library(unrelated OBJECT first.c)
|
||||
if(PARENT_IPO)
|
||||
set_property(TARGET subject PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
|
||||
endif()
|
||||
psxrecomp_apply_runtime_ipo(subject)
|
||||
get_target_property(base subject INTERPROCEDURAL_OPTIMIZATION)
|
||||
get_target_property(release subject INTERPROCEDURAL_OPTIMIZATION_RELEASE)
|
||||
get_target_property(debug subject INTERPROCEDURAL_OPTIMIZATION_DEBUG)
|
||||
get_target_property(others unrelated INTERPROCEDURAL_OPTIMIZATION_RELEASE)
|
||||
file(WRITE "${CMAKE_BINARY_DIR}/properties.txt" "${base}|${release}|${debug}|${others}")
|
||||
''', encoding="utf-8")
|
||||
def configure(name, *options, ok=True):
|
||||
dest = root / name
|
||||
result = run([args.cmake, "-S", root, "-B", dest, "-G", "Ninja",
|
||||
f"-DCMAKE_MAKE_PROGRAM={args.ninja}", f"-DCMAKE_C_COMPILER={args.cc}",
|
||||
f"-DCMAKE_CXX_COMPILER={args.cxx}", "-DCMAKE_BUILD_TYPE=Release", *options], ok)
|
||||
return dest, result
|
||||
off, _ = configure("off", "-DSIMULATE_UNSUPPORTED=ON")
|
||||
assert (off / "properties.txt").read_text() == "base-NOTFOUND|release-NOTFOUND|debug-NOTFOUND|others-NOTFOUND"
|
||||
parent, _ = configure("parent", "-DPARENT_IPO=ON", "-DSIMULATE_UNSUPPORTED=ON")
|
||||
assert (parent / "properties.txt").read_text().startswith("TRUE|release-NOTFOUND|")
|
||||
_, failure = configure("unsupported", "-DPSX_RUNTIME_IPO=ON", "-DSIMULATE_UNSUPPORTED=ON", ok=False)
|
||||
assert "PSX_RUNTIME_IPO=ON was requested" in failure.stderr
|
||||
on, result = configure("on", "-DPSX_RUNTIME_IPO=ON", ok=None)
|
||||
if result.returncode != 0:
|
||||
assert "PSX_RUNTIME_IPO=ON was requested" in result.stderr, result.stderr
|
||||
print("PASS: default/parent/unsupported guards; SKIP actual IPO link: toolchain reports unsupported")
|
||||
return
|
||||
assert (on / "properties.txt").read_text() == "FALSE|TRUE|FALSE|others-NOTFOUND"
|
||||
build = run([args.cmake, "--build", on, "--verbose"])
|
||||
# MSVC and GCC/Clang spell IPO flags differently; this test's explicit
|
||||
# Ninja/compiler arguments cover either native toolchain.
|
||||
assert any(flag in build.stdout for flag in ("-flto", "/GL", "-GL")), build.stdout
|
||||
run([on / ("subject.exe" if os.name == "nt" else "subject")])
|
||||
debug, _ = configure("debug", "-DPSX_RUNTIME_IPO=ON", "-DCMAKE_BUILD_TYPE=Debug")
|
||||
dbg_build = run([args.cmake, "--build", debug, "--verbose"])
|
||||
assert not any(flag in dbg_build.stdout for flag in ("-flto", "/GL", "-GL"))
|
||||
print("PASS: default OFF, parent policy, unsupported failure, scoped ON, actual IPO link/run, Debug OFF")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -14,6 +14,7 @@ endif()
|
||||
# costly PGO rebuild/train cycle a silent no-op.
|
||||
set(PSX_PGO "" CACHE STRING "PGO mode: empty, generate, or use")
|
||||
set_property(CACHE PSX_PGO PROPERTY STRINGS "" generate use)
|
||||
include("${PSXRECOMP_ROOT}/cmake/psx_runtime_ipo.cmake")
|
||||
|
||||
include("${PSXRECOMP_ROOT}/cmake/psx_dependency_archive.cmake")
|
||||
include("${PSXRECOMP_ROOT}/runtime/chd_dependency.cmake")
|
||||
@@ -1389,6 +1390,7 @@ function(psxrecomp_add_runtime_target target)
|
||||
# CMAKE_C_STANDARD setting. cxx_std_17 likewise — game CMakeLists may omit
|
||||
# CMAKE_CXX_STANDARD; mod_packages.cpp must not compile as a pre-17 dialect.
|
||||
target_compile_features(${target} PRIVATE c_std_11 cxx_std_17)
|
||||
psxrecomp_apply_runtime_ipo(${target})
|
||||
|
||||
if(NOT PSX_PGO STREQUAL "")
|
||||
if(NOT PSX_PGO STREQUAL "generate" AND NOT PSX_PGO STREQUAL "use")
|
||||
|
||||
Reference in New Issue
Block a user