Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 90 additions & 8 deletions port/build-port.cmd
Original file line number Diff line number Diff line change
@@ -1,12 +1,94 @@
@echo off
rem Build the PC port's gate-1 smoke runner: 32-bit MSVC via VS Build Tools,
rem same toolchain-location pattern as the recomp's build scripts.
rem Build the PC port's gate smoke runners: 32-bit MSVC via CMake + Ninja.
rem
rem The toolchain is LOCATED, not hardcoded. This script used to name
rem "...\2022\BuildTools\..." directly; when the machine moved to VS 18 the
rem path stopped existing and the failure surfaced as a confusing error from
rem cmake rather than "your compiler moved". vswhere is the supported way to
rem ask where the toolset actually is.
setlocal
set "PATH=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer;%PATH%"
call "%ProgramFiles(x86)%\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars32.bat" >nul

set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if not exist "%VSWHERE%" (
echo error: vswhere.exe not found at "%VSWHERE%"
echo install Visual Studio 2019+ or the VS Build Tools.
exit /b 1
)

rem -products * so Build Tools installs count, not just the IDE SKUs.
set "VSINSTALL="
for /f "usebackq delims=" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSINSTALL=%%i"
if not defined VSINSTALL (
echo error: no Visual Studio install carries the MSVC x86/x64 toolset.
echo install the "Desktop development with C++" workload.
exit /b 1
)

set "VCVARS=%VSINSTALL%\VC\Auxiliary\Build\vcvars32.bat"
if not exist "%VCVARS%" (
echo error: vcvars32.bat missing under "%VSINSTALL%"
exit /b 1
)
rem vcvars32.bat itself shells out to a BARE `vswhere` to resolve the Windows
rem SDK, so the Installer directory has to be on PATH before the call -- drop
rem this and vcvars prints "'vswhere.exe' is not recognized" and then leaves
rem the environment half-built, which surfaces much later as a missing cl.
for %%d in ("%VSWHERE%") do set "PATH=%%~dpd;%PATH%"
call "%VCVARS%" >nul
if errorlevel 1 exit /b 1
set "CMAKEBIN=%ProgramFiles(x86)%\Microsoft Visual Studio\2022\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake"
set "PATH=%CMAKEBIN%\CMake\bin;%CMAKEBIN%\Ninja;%PATH%"
cmake -S "%~dp0." -B "%~dp0..\build\port" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_MAKE_PROGRAM="%CMAKEBIN%\Ninja\ninja.exe" %*

rem CMake and Ninja: prefer the copies bundled with the "C++ CMake tools for
rem Windows" component, then anything vcvars or the user put on PATH.
set "VSCMAKE=%VSINSTALL%\Common7\IDE\CommonExtensions\Microsoft\CMake"
set "CMAKE_EXE="
set "NINJA_EXE="
if exist "%VSCMAKE%\CMake\bin\cmake.exe" set "CMAKE_EXE=%VSCMAKE%\CMake\bin\cmake.exe"
if exist "%VSCMAKE%\Ninja\ninja.exe" set "NINJA_EXE=%VSCMAKE%\Ninja\ninja.exe"
if not defined CMAKE_EXE call :locate cmake CMAKE_EXE
if not defined NINJA_EXE call :locate ninja NINJA_EXE

rem Last resort: `pip install cmake ninja` is what the error below tells you
rem to do, and it drops both into Python's Scripts directory -- which is NOT
rem on PATH in a default Windows Python install, so `where` misses them.
rem Take our own advice seriously enough to find the result.
if not defined CMAKE_EXE goto :pyprobe
if not defined NINJA_EXE goto :pyprobe
goto :haveboth
:pyprobe
set "PYSCRIPTS="
for /f "usebackq delims=" %%i in (`python -c "import sysconfig;print(sysconfig.get_path('scripts'))" 2^>nul`) do set "PYSCRIPTS=%%i"
if defined PYSCRIPTS (
if not defined CMAKE_EXE if exist "%PYSCRIPTS%\cmake.exe" set "CMAKE_EXE=%PYSCRIPTS%\cmake.exe"
if not defined NINJA_EXE if exist "%PYSCRIPTS%\ninja.exe" set "NINJA_EXE=%PYSCRIPTS%\ninja.exe"
)
:haveboth

if not defined CMAKE_EXE (
echo error: cmake.exe not found.
echo add the VS component "C++ CMake tools for Windows",
echo or: pip install cmake ninja
exit /b 1
)
if not defined NINJA_EXE (
echo error: ninja.exe not found.
echo add the VS component "C++ CMake tools for Windows",
echo or: pip install cmake ninja
exit /b 1
)

echo toolchain: %VSINSTALL%
echo cmake: %CMAKE_EXE%
echo ninja: %NINJA_EXE%

"%CMAKE_EXE%" -S "%~dp0." -B "%~dp0..\build\port" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_MAKE_PROGRAM="%NINJA_EXE%" %*
if errorlevel 1 exit /b 1
ninja -C "%~dp0..\build\port"
"%NINJA_EXE%" -C "%~dp0..\build\port"
exit /b %errorlevel%

rem :locate <exe-name> <var-to-set> -- first PATH hit, or leaves the var unset.
:locate
for /f "usebackq delims=" %%i in (`where %1 2^>nul`) do (
set "%2=%%i"
goto :eof
)
goto :eof
17 changes: 17 additions & 0 deletions port/evidence-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"comment": "Files compiled into the port with no proof they are the game's code. This is a DEBT LEDGER, not a target -- --ratchet fails when it grows. Shrink it by matching the function, not by editing this file.",
"unproven": [
"src/_ZN14ArrowSignRight16CleanupResourcesEv.cpp",
"src/_ZN18NestedHeapIterator7AddLastEP13HeapAllocator.cpp",
"src/_ZN18NestedHeapIterator8AddFirstEP13HeapAllocator.cpp",
"src/_ZN4Heap8AllocateEji.cpp",
"src/_ZN5Model17UpdateFileOffsetsER8BMD_File.cpp",
"src/_ZN5Model27LoadCompressedTextureToVramEPcjS0_.cpp",
"src/_ZN5Timer10StartTimerEv.cpp",
"src/_ZN5Timer7GetTimeEv.cpp",
"src/_ZN5Timer9StopTimerEv.cpp",
"src/_ZN8Platform21IsClsnInRangeOnScreenE5Fix12IiES1_.cpp",
"src/_ZN9ActorBase22BeforeCleanupResourcesEv.cpp",
"src/_ZN9ActorBasenwEj.cpp"
]
}
4 changes: 4 additions & 0 deletions port/hal/actor_vtables.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,11 @@ short *data_0209b460; /* spawn default position ptr */
signed char data_0209b44c_c;
int data_0209b468[4]; /* actor list head the ctor links into */
}
// Both spellings, same storage -- Actor's ctor reaches it by the plain C name
// and other TUs by the MSVC-mangled one. Aliasing only one direction forks the
// storage silently, which is the exact failure this file exists to prevent.
#pragma comment(linker, "/alternatename:?data_0209b44c@@3CA=_data_0209b44c_c")
#pragma comment(linker, "/alternatename:_data_0209b44c=_data_0209b44c_c")

extern "C" {
unsigned char data_0209f2d8_c; /* mega-char state byte: none */
Expand Down
21 changes: 20 additions & 1 deletion port/hal/cxxname_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,30 @@ void *data_020a0eac_c;
}
#pragma comment(linker, "/alternatename:?data_020a0eac@@3PAUHeap@@A=_data_020a0eac_c")
#pragma comment(linker, "/alternatename:_data_020a0eac=_data_020a0eac_c")
void func_0206e2f8(void *p, int v, unsigned n)
// ActorBase::operator new references the plain C name (decl_*.h extern "C"
// guard); nothing currently wants the mangled spelling, but the alias costs
// nothing and both forms are __cdecl free functions, so aliasing is safe
// here in a way it is NOT for the __thiscall method bridges above.
extern "C" void func_0206e2f8(void *p, int v, unsigned n)
{
unsigned char *b = (unsigned char *)p;
for (unsigned i = 0; i < n; ++i) b[i] = (unsigned char)v;
}
#pragma comment(linker, "/alternatename:?func_0206e2f8@@YAXPAXHI@Z=_func_0206e2f8")

// Platform's ROM ctor (src/_ZN8PlatformC2Ev.c) installs its vtable pointer.
// Platform.h carries no virtuals, so MSVC emits no vtable object -- storage
// only, exactly like the transient base vtables in actor_vtables.cpp. The
// gate never dispatches through it; if that changes it needs real slots.
extern "C" {
void *_ZTV8Platform[20];
}

// ArrowSignRight::CleanupResources calls Release as a real __thiscall METHOD
// (SharedFilePtr.h declares the class), while the implementation is the
// C-named free function in src/. Convert the convention rather than alias it.
#include "SharedFilePtr.h"
void SharedFilePtr::Release() { hal_fileptr_release(this); }
extern "C" void hal_m43_roty(void *m, int a);
void Matrix4x3_FromRotationY(void *m, int a) { hal_m43_roty(m, a); }

Expand Down
25 changes: 20 additions & 5 deletions port/hal/heap_globals.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ void MultiStore_Int(int val, int *dst, int len)
#pragma comment(linker, "/alternatename:_data_020a4d38=__ZN6Memory16rootHeapIteratorE")
#pragma comment(linker, "/alternatename:?_ZN6Memory16rootHeapIteratorE@@3DA=__ZN6Memory16rootHeapIteratorE")
#pragma comment(linker, "/alternatename:?_ZN6Memory25isRootHeapIterInitializedE@@3HA=__ZN6Memory25isRootHeapIterInitializedE")
// HeapAllocator's ctor reaches the same two globals by their data_-address
// names at C++ LINKAGE (?data_020a4d38@@3DA / ?data_020a4d34@@3HA), which the
// C-spelling alias above does not cover. This surfaced when those declarations
// moved into a decl_*.h. data_020a4d34 is the four bytes below the iterator --
// isRootHeapIterInitialized (symbols.txt: both bss, 0x020a4d34 / 0x020a4d38).
// Missing either spelling forks the storage exactly as this file's header warns.
#pragma comment(linker, "/alternatename:?data_020a4d38@@3DA=__ZN6Memory16rootHeapIteratorE")
#pragma comment(linker, "/alternatename:?data_020a4d34@@3HA=__ZN6Memory25isRootHeapIterInitializedE")
#pragma comment(linker, "/alternatename:_data_020a4d34=__ZN6Memory25isRootHeapIterInitializedE")
// FUNCTION alias only where the conventions MATCH: this reference and the C
// definition are both __cdecl free functions.
#pragma comment(linker, "/alternatename:?_ZN18NestedHeapIteratorC1Ej@@YAXPAXI@Z=__ZN18NestedHeapIteratorC1Ej")
Expand All @@ -53,9 +62,15 @@ void _ZN18NestedHeapIterator8AddFirstEP13HeapAllocator(void *self, HeapAllocator
int _ZN18NestedHeapIterator4NextEP13HeapAllocator(void *self, HeapAllocator *a)
{ return ((NestedHeapIterator *)self)->Next(a); }
}
// And the reverse direction: AddLast/AddFirst reference Init as a C++
// __cdecl FREE function (?_ZN..4Init..@@YAXPAD0@Z, char* args) while
// Init.cpp defines the method. C++ linkage on purpose -- extern "C" would
// decorate this wrong.
void _ZN18NestedHeapIterator4InitEP13HeapAllocator(char *self, char *a)
// And the reverse direction: AddLast/AddFirst reference Init as a __cdecl
// FREE function with char* args while Init.cpp defines the method.
//
// This forwarder was C++-linkage "on purpose" until include/decl_*.h grew its
// extern "C" guard. That guard is now the authority (see the rationale in
// decl_NestedHeapIterator.h: an unguarded C++ declaration emits _Z3Fooi,
// which exists nowhere), so every TU including the decl header emits a
// C-linkage reference and the C++-mangled definition resolves nothing. The
// convention still has to be converted by hand -- the caller passes char*,
// the method is __thiscall -- so this stays a real forwarder, not an alias.
extern "C" void _ZN18NestedHeapIterator4InitEP13HeapAllocator(char *self, char *a)
{ ((NestedHeapIterator *)self)->Init((HeapAllocator *)a); }
22 changes: 22 additions & 0 deletions port/hal/model_host.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,28 @@ extern "C" void _ZN2GX7LoadTexEPKvjj(const void *s, unsigned o, unsigned z)
GX::LoadTex(s, o, z);
}

// Same migration, one function later. src/_ZN2GX11LoadTexPlttEPKvjj.cpp moved
// to namespace-style C++ ("language-mode migration only ... nothing outside
// this file can shift"). That holds for the ROM link, where the symbol is the
// filename either way. It does not hold here: the name stopped being spelled
// by hand and started being mangled by MSVC, so gx_upload_bridge.cpp's
// extern "C" _ZN2GX11LoadTexPlttEPKvjj lost its definition and nine gate
// binaries failed to link. Bridge it exactly as LoadTex above.
namespace GX {
void LoadTexPltt(const void *src, unsigned addr, unsigned size);
}
extern "C" void _ZN2GX11LoadTexPlttEPKvjj(const void *s, unsigned a, unsigned z)
{
GX::LoadTexPltt(s, a, z);
}

// The migrated TU also reaches its destination-base global as C++-linkage
// ?data_020a60b0@@3IA, while the storage above is inside this file's
// extern "C" block. ALIAS, never a second definition -- two definitions would
// link cleanly and then leave LoadTexPltt writing to a different base than
// the one the Model budget code reads.
#pragma comment(linker, "/alternatename:?data_020a60b0@@3IA=_data_020a60b0")

// DMA fallback DMASyncWordTransfer uses; same synchronous copy semantics.
extern "C" void DMAStartTransfer(int ch, int src, int dst, int ctrl);
extern "C" void DMAStartTransferFB(unsigned char ch, u32 src, u32 dst, u32 ctrl)
Expand Down
12 changes: 8 additions & 4 deletions port/hal/os_time.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
//
// On the DS this reads the OS tick counter (timer hardware + IRQ-maintained
// high bits). Timer::Start/Stop/GetTime take differences of it, so any
// monotonic s64 with a stable rate is semantically faithful. The Timer TUs
// declare it with C++ linkage, which is why this is a .cpp -- a C definition
// does not mangle to what they reference.
// monotonic s64 with a stable rate is semantically faithful.
//
// LINKAGE: this used to be a C++-linkage definition because the Timer TUs
// referenced the mangled spelling. They no longer do -- they include
// decl_common.h, which declares `extern s64 func_02059650(void)` inside its
// extern "C" guard, so the reference is the plain C name. The file stays .cpp
// for the host-side statics; only the linkage of the entry point changed.
//
// GATE 1: the tick is a manually-advanced counter so the smoke tests are
// deterministic -- sm64ds_hal_advance_ticks() stands in for time passing.
Expand All @@ -14,7 +18,7 @@ typedef long long s64;

static s64 g_ticks;

s64 func_02059650()
extern "C" s64 func_02059650()
{
return g_ticks;
}
Expand Down
29 changes: 21 additions & 8 deletions port/hal/shims.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,24 @@ int Fader::IsAtEnd() { return 0; }
// hardware upload.
void FaderBrightness::AdvanceFade() { AdvanceInterp(); }

// The func_0203ae58 bridge that used to live here is gone, on the terms its own
// comment set out: it existed because Fader::AdvanceInterp called the 20.12
// approach helper by its historical address-shaped name, which the NDS build
// resolves by address and the host cannot. That extern has now been modernised
// to _Z14ApproachLinearRiii -- the real ROM symbol at 0x0203ae58, defined by
// src/_Z14ApproachLinearRiii.cpp -- which is exactly what a host C++ build emits
// for ApproachLinear(int&, int, int). The name now resolves on both sides
// without help, so bridging it would be a duplicate definition.
// The func_0203ae58 bridge was deleted here on the reasoning that
// _Z14ApproachLinearRiii is "exactly what a host C++ build emits for
// ApproachLinear(int&, int, int)", making a bridge a duplicate definition.
// That is true of an ITANIUM-ABI host compiler. It is not true of MSVC, which
// emits ?ApproachLinear@@YAHAAHHH@Z -- _Z... is the GCC/Clang spelling, not a
// universal one. On top of that, decl_common.h now declares the symbol inside
// its extern "C" guard, so Fader::AdvanceInterp emits a reference to the
// C-decorated __Z14ApproachLinearRiii, which nothing on this host defines.
// smoke.exe failed to link on exactly that.
//
// So the bridge returns, one layer lower than before: define the Itanium-shaped
// name at C linkage and forward to the real C++ definition in
// src/_Z14ApproachLinearRiii.cpp. Under MSVC those are two distinct symbols,
// so this is not a duplicate definition. On a GCC/Clang host it WOULD be --
// guard it by toolchain if the port ever grows a second host compiler.
int ApproachLinear(int &ref, int target, int step);

extern "C" int _Z14ApproachLinearRiii(int &ref, int target, int step)
{
return ApproachLinear(ref, target, step);
}
65 changes: 57 additions & 8 deletions port/tools/host_frontier.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
python port/tools/host_frontier.py --detail <bucket-substring>
python port/tools/host_frontier.py --jobs 8 --batch 40

Requires the VS Build Tools 32-bit environment (run from build-port.cmd's
shell, or let the script locate vcvars itself, which it does by default).
Requires an MSVC 32-bit environment. The toolchain is located via vswhere,
same as build-port.cmd; set SM64DS_VCVARS to point at a specific vcvars32.bat.
"""
import argparse
import os
Expand All @@ -34,8 +34,40 @@
INCLUDE = REPO / "include"
PORT = REPO / "port"

VCVARS = (r"C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools"
r"\VC\Auxiliary\Build\vcvars32.bat")
def find_vcvars():
"""Locate vcvars32.bat, asking vswhere rather than hardcoding a path.

This was a literal "...\\2022\\BuildTools\\..." string until the machine
moved to VS 18: the install vanished, vcvars silently did nothing, and the
failure surfaced two steps later as "cl not on the captured PATH" -- which
reads like a toolchain bug instead of a missing compiler. Set
SM64DS_VCVARS to override.
"""
override = os.environ.get("SM64DS_VCVARS")
if override:
if not Path(override).exists():
sys.exit(f"SM64DS_VCVARS points at a missing file: {override}")
return override

vswhere = (Path(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)"))
/ "Microsoft Visual Studio" / "Installer" / "vswhere.exe")
if not vswhere.exists():
sys.exit(f"vswhere.exe not found at {vswhere} -- "
"install Visual Studio 2019+ or the VS Build Tools")
# -products * so Build Tools installs count, not just the IDE SKUs.
out = subprocess.run(
[str(vswhere), "-latest", "-products", "*",
"-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"-property", "installationPath"],
capture_output=True, text=True, errors="replace")
roots = [ln.strip() for ln in out.stdout.splitlines() if ln.strip()]
if not roots:
sys.exit("no Visual Studio install carries the MSVC x86/x64 toolset -- "
'install the "Desktop development with C++" workload')
vcvars = Path(roots[0]) / "VC" / "Auxiliary" / "Build" / "vcvars32.bat"
if not vcvars.exists():
sys.exit(f"vcvars32.bat missing under {roots[0]}")
return str(vcvars)

# First-error attribution: MSVC error code -> human bucket. Anything not
# listed reports as its raw code so new classes surface instead of hiding
Expand Down Expand Up @@ -64,16 +96,33 @@ def vc_env():
reports 'cannot find the path specified' with empty output, which then
surfaces later as FileNotFoundError on 'cl' -- a confusing distance from
the actual failure)."""
vcvars = find_vcvars()
# vcvars32.bat shells out to a BARE `vswhere`; without the Installer
# directory on PATH it prints "'vswhere.exe' is not recognized" into the
# output we are about to parse for KEY=VALUE lines. It recovers and still
# builds a usable environment, but the stray error is pure noise on a
# capture whose failure mode is already hard to read. Same prepend
# build-port.cmd does, for the same reason.
parent = dict(os.environ)
installer = str(Path(vcvars).parents[3] / "Installer")
for probe in (installer,
str(Path(os.environ.get("ProgramFiles(x86)",
r"C:\Program Files (x86)"))
/ "Microsoft Visual Studio" / "Installer")):
if Path(probe, "vswhere.exe").exists():
parent["PATH"] = probe + os.pathsep + parent.get("PATH", "")
break
out = subprocess.run(
["cmd", "/c", "call", VCVARS, ">nul", "&&", "set"],
capture_output=True, text=True, errors="replace")
env = dict(os.environ)
["cmd", "/c", "call", vcvars, ">nul", "&&", "set"],
capture_output=True, text=True, errors="replace", env=parent)
env = dict(parent)
for line in out.stdout.splitlines():
if "=" in line:
k, _, v = line.partition("=")
env[k] = v
if not any("Hostx86" in p or "HostX86" in p for p in env.get("PATH", "").split(";")):
sys.exit("vcvars32 capture failed: cl not on the captured PATH")
sys.exit(f"vcvars32 capture failed (cl not on the captured PATH)\n"
f" vcvars: {vcvars}")
# CreateProcess resolves the executable against the PARENT's PATH, not
# the child env being passed -- so find cl.exe now and invoke it by
# absolute path, or every spawn dies FileNotFoundError despite a correct
Expand Down
Loading