Skip to content

Replace the port's weak symbols with PC_OVERRIDE and a pre-processing step - #58

Merged
Adubbz merged 3 commits into
TheMoonPeople:portfrom
carrdylan52:pr/pc-override-20261005
Oct 5, 2026
Merged

Adubbz merged 3 commits into
TheMoonPeople:portfrom
carrdylan52:pr/pc-override-20261005

Conversation

@carrdylan52

Copy link
Copy Markdown
Contributor

This is the override mechanism you described: a PC_OVERRIDE macro on the port's side, and a pre-processing stage that takes the PS2 code out for what the port overrides, the same on every platform. It replaces the merged weak object, llvm-objcopy --weaken, -fsemantic-interposition, the interposition check and tools/weaken. Three commits on current port.

Nothing under ps2/ changes. PAL and NTSC builds of this branch and of port are byte-identical, as they have to be.

What it is

  • The tag. PC_OVERRIDE goes in front of the 323 definitions in port/src that replace one of ps2/src's: 312 functions, main and ten globals of the title sequence. port/include/port.h defines it as nothing. The list is what a weak-symbol Linux build defined in both halves, so nothing is replaced that was not replaced before.
  • The stage. scripts/port/pc_override.py (about 500 lines, no dependencies) has three steps. list reads the tags in port/src and writes the name each tagged definition links under: a function's qualified name and parameter types, or the name alone for a variable and for a function with C linkage. strip writes a copy of each ps2/src unit into the build directory without its definitions of those names, and CMake compiles the copies. A function is left as its declaration, a member function is removed (an explicit specialization of one keeps its declaration), a variable becomes an extern declaration. Line numbers are kept and a #line directive names the original, so diagnostics and debug information still point into ps2/src. check stops the build if a tag names nothing ps2/src defines.
  • The link. Each name the two halves share has one definition. A replacement that is not tagged leaves the PS2 one in and the link fails on the duplicate symbol (unless the PS2 definition is inline, or Apple's linker drops it as dead code first). A tag whose signature ps2/src does not have (after a rename or a changed parameter there, say) fails the check; without it both would link and the game would quietly go on calling the PS2 code. The port's CMake no longer looks up llvm-objcopy, llvm-nm, llvm-objdump, llvm-readobj, llvm-lipo or a relocatable linker.
  • Rebuilds. The list and each copy are rewritten only when they change, so editing a file in port/src rebuilds that file and nothing of ps2/src, and adding a tag recompiles only the unit it reaches.
  • The truncation audit. scripts/port/truncations.py reads each unit as ps2/src has it, so the replaced bodies stay in docs/port/truncations.md. It now refuses a build that is not Debug: without semantic interposition an optimised build inlines functions and the link drops their bodies, which the audit would count as dead.
  • Docs. docs/PC.md ("How port/src takes precedence" and the places that mentioned weak symbols) and the rule lines of the three plan documents.

Behaviour that changes

Under weak symbols the title units' static constructors also ran over the port's ten replaced globals, as docs/PC.md noted. They no longer do. CFireOmni's constructor calls rand(), so the random sequence of a run shifts once: a recorded input replay lands on a different random dungeon floor than before. That is the only difference in the replays I have: making the build construct those ten a second time reproduces the weak-symbol build's screenshots byte for byte.

Optimised builds now inline and fold calls between ps2/src functions the port does not replace, which the interposition flags used to prevent. Debug builds are unaffected.

Testing

Linux x86-64, clang 23:

  • Clean build; ctest 94 of 94 on RADV and on lavapipe. port has 96; the two that go are tools/weaken's own.
  • git clang-format clean; truncations.py --check passes; Doxygen gives no new warning.
  • The first commit alone (tags only, still weak symbols) builds and passes 96 of 96.
  • Two recorded replays (a debug item grant, a dungeon floor) run to the end.
  • The PC port workflow's Linux job as written, in an ubuntu:26.04 container with clang 22: 94 of 94 and the truncation audit passes.
  • The Flatpak workflow as written, in Flathub's image: builds, and darkcloud starts and stops at the data check.
  • A Release build: 94 of 94 on both drivers.
  • What the script takes out of the 136 units was compared, unit by unit, with a build of the same mechanism that had the tags on the PS2 definitions: the same PAL definitions go. This one also takes out the NTSC InitCDFile under #else, which the PAL build never compiles.

PS2: PAL and NTSC, as above.

Windows: the native build from #51, rebased onto this, links with no multiple-definition flag and passes its suite in Debug and in an optimised build. That is the follow-up PR.

macOS is not built here; CI will be the first run. The macOS-only pieces that go are the Mach-O weakening, -Xclang -fsemantic-interposition, -fno-inline-functions, and -fno-zero-initialized-in-bss on the four title units whose globals are replaced.

Things you may want different

  1. How a PS2 definition is found. By name and parameter types as written, with parameter names and default arguments dropped. A replacement has to spell its signature the way ps2/src does (u_char, not unsigned char); a mismatch stops the build, at the check or at the link. A name a stub header renames (MainDraw to DunMainDraw) is tagged under the new name.
  2. It is a text pass, not a parser. It reads file scope of the .cpp units and handles a function with its body or a variable without constructor arguments. Where it cannot be sure, it stops the build rather than guess: a definition that holds part of an #if block or has a directive before its body, an inline definition it cannot read (the link would not catch that one), a line continued by a backslash outside a directive, and a definition taken out of a branch the port does not compile. For that last one it reads #ifdef/#ifndef of PORT and PAL, which CMake passes, and assumes nothing in ps2/src redefines them. A function defined in a class body, a namespace or a header cannot be replaced; the check says so. Nothing in ps2/src today trips any of this.
  3. port/src/linknames.cpp still has weak definitions. They are the port's own fallbacks for link names and have nothing to do with ps2/src; I left them alone.
  4. No test for the script. A wrong tag stops the build itself. Say if you want one.

PC_OVERRIDE goes in front of each of the 323 definitions in port/src that
replace one of ps2/src's: 312 functions, main and ten globals of the title
sequence. port.h defines the macro as nothing, so nothing is compiled
differently yet.
scripts/port/pc_override.py lists the names port/src's PC_OVERRIDE
definitions link under and writes a copy of each ps2/src unit without its
definitions of those names; the port compiles the copies. A replaced
function is left as its declaration, a member function is removed and a
variable becomes an extern declaration. ps2/src itself is not touched.

Each name then has one definition on every platform, so the merged object,
llvm-objcopy --weaken, -fsemantic-interposition, the interposition check
and tools/weaken go.

scripts/port/truncations.py reads each unit as ps2/src has it, so the
replaced bodies stay in docs/port/truncations.md, and it refuses a build
that is not a Debug one: without semantic interposition an optimised build
inlines functions whose bodies the link then drops.
@Adubbz
Adubbz merged commit f9ad958 into TheMoonPeople:port Oct 5, 2026
8 checks passed
Adubbz pushed a commit that referenced this pull request Oct 6, 2026
… step (#58)

* Tag port/src's replacements with PC_OVERRIDE

PC_OVERRIDE goes in front of each of the 323 definitions in port/src that
replace one of ps2/src's: 312 functions, main and ten globals of the title
sequence. port.h defines the macro as nothing, so nothing is compiled
differently yet.

* Build the port from ps2/src without the definitions port/src overrides

scripts/port/pc_override.py lists the names port/src's PC_OVERRIDE
definitions link under and writes a copy of each ps2/src unit without its
definitions of those names; the port compiles the copies. A replaced
function is left as its declaration, a member function is removed and a
variable becomes an extern declaration. ps2/src itself is not touched.

Each name then has one definition on every platform, so the merged object,
llvm-objcopy --weaken, -fsemantic-interposition, the interposition check
and tools/weaken go.

scripts/port/truncations.py reads each unit as ps2/src has it, so the
replaced bodies stay in docs/port/truncations.md, and it refuses a build
that is not a Debug one: without semantic interposition an optimised build
inlines functions whose bodies the link then drops.

* Describe PC_OVERRIDE in the port's documentation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants