Add PC port scaffolding (CMake build + port layer)

Initial scaffolding for a PC port of GoldenEye 007, modeled on the
Perfect Dark PC port (same Rare "Indy" engine). The N64 build
(Makefile + IDA) is untouched; the PC build is a parallel CMake build.

Build:
- CMakeLists.txt: SDL2 + zlib + OpenGL. Compiles src/game/*.c + curated
  engine files + libultra audio/gu + port/. Per-region macro sets
  (ROMID US/EU/JP) mirror the N64 Makefile's LCDEFS exactly.
- build-pc.sh, cmake/: configure+build helper and target/SDL2 modules.

Port layer (port/):
- video/audio/input/fs/romdata/config/system/mixer: stubs with
  TODO(Phase N) markers and correct interfaces.
- libultra.c: libultra OS shims (message queues, VI, PI, AI, EEPROM,
  controller, PFS/motor no-ops, region-gated osTvType).
- gesched.c: software-RSP scheduler replacing src/sched.c task submission.
- n64stubs.c: inert stubs for N64-boot + rmon symbols.
- random.c: PRNG ported verbatim from src/random.s (bit-exact).
- ucode.c: RSP/ASP/GSP microcode segment markers (dummies; Phase 3 TODO).

Docs:
- docs/PCPortResearch.md: architecture, GE-specific deltas, phased plan,
  and the scaffolding review (findings A1-A4/B1-B4/C1-C2 + resolutions).
- PORTING.md: top-level entry point.

See docs/PCPortResearch.md for the full plan.
This commit is contained in:
James Dansereau committed 2026-08-17 19:17:06 -04:00
1 parent ef209c113c
commit cd6cc158b6
37 files changed
+3091

No files matched your search

+5
View File
@@ -67,6 +67,11 @@ bin/*
build/*
build-*/*
notes/*
# PC port (see CMakeLists.txt / port/)
build-pc/
data/
ge007.ini
!build/test.c
!notes/StyleGuide.md
#tools
+298
View File
@@ -0,0 +1,298 @@
# ============================================================================
# GoldenEye 007 — PC port build
#
# This is a PARALLEL build system to the N64 `Makefile`. It compiles the same
# game C sources (src/game, src) for a modern host (Windows/Linux/macOS)
# against the PC shims in `port/`. The N64 build is untouched.
#
# See docs/PCPortResearch.md for the architecture and plan.
# Modelled on the Perfect Dark PC port (perfect-dark-pc-port/perfect_dark).
#
# Usage:
# cmake -S . -B build-pc -DROMID=ntsc-final
# cmake --build build-pc -j
# ============================================================================
cmake_minimum_required(VERSION 3.16)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
if(APPLE)
if(CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "" FORCE)
else()
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "" FORCE)
endif()
endif()
project(ge007 C CXX)
include(TargetArch)
include(FindPkgConfig)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Default build type
if(NOT CMAKE_BUILD_TYPE AND NOT EXISTS ${CMAKE_BINARY_DIR}/CMakeCache.txt)
set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "" FORCE)
endif()
# ----------------------------------------------------------------------------
# Options
# ----------------------------------------------------------------------------
# Which ROM the executable expects in the data/ dir.
set(ROMID "ntsc-final" CACHE STRING "ROM id: ntsc-final (US), pal-final (EU), jpn-final (JP)")
# ----------------------------------------------------------------------------
# Target arch / platform
# ----------------------------------------------------------------------------
target_architecture(TARGET_ARCH)
if(TARGET_ARCH MATCHES "64")
set(TARGET_IS_64BIT TRUE)
else()
set(TARGET_IS_64BIT FALSE)
endif()
message(STATUS "Target arch: ${TARGET_ARCH} (64bit=${TARGET_IS_64BIT})")
if(WIN32)
set(TARGET_OS "windows")
elseif(APPLE)
set(TARGET_OS "osx")
elseif(UNIX)
set(TARGET_OS "linux")
else()
message(FATAL_ERROR "Unknown target OS")
endif()
set(TARGET_PLATFORM "${TARGET_ARCH}-${TARGET_OS}")
message(STATUS "Target platform: ${TARGET_PLATFORM}")
# Region -> compile definitions.
#
# The game code #ifdefs a full per-region macro set (see the N64 Makefile,
# the VERSION blocks). e.g. VERSION_EU is used in 60+ files, LEFTOVERDEBUG in
# 26, BUGFIX_R1 in 21. We MUST emit the same set the N64 build uses for the
# matching region, or the PC build takes different conditional branches and
# silently diverges (and fails to link). These mirror Makefile LCDEFS exactly.
if(ROMID STREQUAL "ntsc-final")
set(VERSION_IS_PAL 0)
set(BIN_REGION "")
set(REGION_DEFS
-DVERSION_US -DLANG_US -DREFRESH_NTSC
-DLEFTOVERDEBUG -DLEFTOVERSPECTRUM -DBUGFIX_R0 -DBYTEMATCH)
elseif(ROMID STREQUAL "pal-final")
set(VERSION_IS_PAL 1)
set(BIN_REGION ".pal")
set(REGION_DEFS
-DVERSION_EU -DLANG_EU -DREFRESH_PAL
-DBUGFIX_R1 -DBUGFIX_R2 -DBYTEMATCH)
elseif(ROMID STREQUAL "jpn-final")
set(VERSION_IS_PAL 0)
set(BIN_REGION ".jpn")
set(REGION_DEFS
-DVERSION_JP -DLANG_JP -DREFRESH_NTSC
-DBUGFIX_R1 -DLEFTOVERDEBUG -DLEFTOVERSPECTRUM -DBYTEMATCH)
else()
message(FATAL_ERROR "Unknown ROMID: ${ROMID}")
endif()
# git hash for version info
execute_process(COMMAND git rev-parse --short HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE VERSION_HASH OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(NOT VERSION_HASH)
set(VERSION_HASH "unknown")
endif()
configure_file(
"${CMAKE_SOURCE_DIR}/port/include/versioninfo.h.in"
"${CMAKE_BINARY_DIR}/port/include/versioninfo.h" @ONLY)
# ----------------------------------------------------------------------------
# Compiler definitions & flags
# ----------------------------------------------------------------------------
add_definitions(
-DPORT=1
-D_LANGUAGE_C=1
-DAVOID_UB=1
-DPAL=${VERSION_IS_PAL}
${REGION_DEFS}
)
if(TARGET_IS_64BIT)
add_definitions(-DPLATFORM_64BIT=1)
endif()
# The decomp is big-endian N64 code with unaligned accesses; these flags are
# required to compile it cleanly on a little-endian host (same as the PD port).
add_compile_options(
-fno-strict-aliasing
-funsigned-char
-fwrapv
-include "versioninfo.h"
)
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
add_compile_options(-Og)
endif()
add_compile_options(
-Wall
-Wno-address
-Wno-misleading-indentation
-Wno-missing-braces
-Wno-multichar
-Wno-tautological-compare
-Wno-unused-but-set-variable
-Wno-unused-value
-Wno-unused-variable
)
add_compile_options($<$<COMPILE_LANGUAGE:C>:-Wno-pointer-sign>)
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-Wno-unused-function>)
include_directories(
"${CMAKE_SOURCE_DIR}"
"${CMAKE_SOURCE_DIR}/include"
"${CMAKE_SOURCE_DIR}/include/PR"
"${CMAKE_SOURCE_DIR}/src"
"${CMAKE_SOURCE_DIR}/src/game"
"${CMAKE_SOURCE_DIR}/src/libultra"
"${CMAKE_SOURCE_DIR}/src/libultra/audio"
"${CMAKE_SOURCE_DIR}/port/include"
"${CMAKE_BINARY_DIR}/port/include"
)
# ----------------------------------------------------------------------------
# Dependencies: SDL2, zlib, OpenGL
# ----------------------------------------------------------------------------
set(LIBS "")
find_package(SDL2 REQUIRED)
include_directories(SYSTEM "${SDL2_INCLUDE_DIR}")
list(APPEND LIBS "${SDL2_LIBRARY}")
find_package(ZLIB REQUIRED)
if(ZLIB_INCLUDE_DIR)
include_directories(SYSTEM "${ZLIB_INCLUDE_DIR}")
endif()
list(APPEND LIBS "${ZLIB_LIBRARY}")
if(NOT GL_LIBRARY)
if(WIN32)
set(GL_LIBRARY opengl32)
elseif(APPLE)
set(GL_LIBRARY "-framework OpenGL")
else()
set(GL_LIBRARY GL)
endif()
endif()
list(APPEND LIBS "${GL_LIBRARY}")
if(NOT EXTRA_LIBRARIES)
if(WIN32)
set(EXTRA_LIBRARIES dbghelp -static-libstdc++)
else()
set(EXTRA_LIBRARIES stdc++ m dl pthread)
endif()
endif()
list(APPEND LIBS "${EXTRA_LIBRARIES}")
# ----------------------------------------------------------------------------
# Sources
# ----------------------------------------------------------------------------
# Port layer (shims + software RSP).
file(GLOB_RECURSE SRC_PORT "${CMAKE_SOURCE_DIR}/port/src/*.c")
file(GLOB_RECURSE SRC_PORT_CXX "${CMAKE_SOURCE_DIR}/port/src/*.cpp")
file(GLOB_RECURSE SRC_FAST3D "${CMAKE_SOURCE_DIR}/port/fast3d/*.cpp")
# Game code — compiled unmodified.
file(GLOB_RECURSE SRC_GAME "${CMAKE_SOURCE_DIR}/src/game/*.c")
# libaudio (AL) — runs on the CPU, platform independent.
file(GLOB SRC_LIBAUDIO "${CMAKE_SOURCE_DIR}/src/libultra/audio/*.c")
# Rare's audio "New" driver (drvrNew/env/reverb) — synthesizer.c calls
# alSaveNew/alAuxBusNew/alMainBusNew/alLoadNew/alResampleNew/alEnvmixerNew/
# alEnvmixerParam/alFxParam, all defined here. Runs on the CPU; must be linked.
file(GLOB SRC_LIBULTRARE_AUDIO "${CMAKE_SOURCE_DIR}/src/libultrare/audio/*.c")
# gu matrix helpers.
file(GLOB SRC_GU "${CMAKE_SOURCE_DIR}/src/libultra/gu/*.c")
# VI mode table (DATA, not a driver): fr.c references osViModeTable ~15x.
# This is the only libultra/io file we compile — the rest are hardware
# drivers that get shimmed in port/src/libultra.c.
set(SRC_LIBULTRARE_DATA
src/libultrare/io/vitbl.c
)
list(PREPEND SRC_LIBULTRARE_DATA "${CMAKE_SOURCE_DIR}")
# Engine files in src/ that are platform independent.
# NOTE: this list is a starting point and must be curated during Phase 1.
#
# INCLUDED here (platform-independent or CPU-side):
# init.c — provides mainproc() (the game's real main entry) + the thread
# setup helpers. Its N64-only init() is compiled but NOT called;
# the N64 symbols init() references are stubbed in
# port/src/n64stubs.c (see A3 in docs/PCPortResearch.md).
# cfb.c — defines cfb_16 (the framebuffer), referenced by fr.c and game
# code (bondview2.c, front.c). Pure data.
# stacks.c — thread-stack arrays (sp_boot/sp_rmon/sp_idle/sp_shed/sp_main/
# sp_audi + sp_debug under LEFTOVERDEBUG). Pure data; referenced
# by init.c (thread setup) and audi.c (sp_audi). The SP_*_SZ
# sizes in bondconstants.h match these exactly, so sizeof() is
# correct. No duplicate-definition risk (all other refs are
# extern decls in bondgame.h / audi.c).
#
# EXCLUDED (hardware drivers, replaced by port/ shims):
# sched.c crash.c rmon.c usb.c tlb_*.c pi.c vi.c motor.c
# (there is no src/spectrum.c in this decomp)
# All of src/libultra/io/*.c and src/libultrare/io/*.c EXCEPT vitbl.c.
# motor.c / pfsinit.c / pfsisplug.c are NOT compiled: joy.c calls
# osMotorInit/Start/Stop and osPfsInit/osPfsIsPlug only for accessory
# (Rumble Pak / Memory Pak) detection, so they are shimmed as no-ops in
# port/src/libultra.c (see B2 — saves are EEPROM, not PFS).
set(SRC_ENGINE
src/init.c
src/audi.c
src/snd.c
src/music.c
src/joy.c
src/fr.c
src/cfb.c
src/stacks.c
src/boss.c
src/mema.c
src/memp.c
src/deb.c
src/debugmenu.c
src/speed_graph.c
src/token.c
src/str.c
src/sprintf.c
src/ramrom.c
)
list(PREPEND SRC_ENGINE "${CMAKE_SOURCE_DIR}")
set(SRC
${SRC_GAME}
${SRC_PORT}
${SRC_PORT_CXX}
${SRC_FAST3D}
${SRC_LIBAUDIO}
${SRC_LIBULTRARE_AUDIO}
${SRC_LIBULTRARE_DATA}
${SRC_GU}
${SRC_ENGINE}
)
# ----------------------------------------------------------------------------
# Target
# ----------------------------------------------------------------------------
set(BIN_NAME "ge007${BIN_REGION}.${TARGET_ARCH}")
add_executable(ge007 ${SRC})
target_link_libraries(ge007 ${LIBS})
set_target_properties(ge007 PROPERTIES OUTPUT_NAME "${BIN_NAME}")
if(WIN32)
set_target_properties(ge007 PROPERTIES WIN32_EXECUTABLE TRUE)
endif()
message(STATUS "Output binary: ${BIN_NAME}")
+67
View File
@@ -0,0 +1,67 @@
# GoldenEye 007 — PC Port
A work-in-progress port of [GoldenEye 007](https://en.wikipedia.org/wiki/GoldenEye_007)
to modern platforms (Windows / Linux / macOS), built on the decompilation in
this repository.
It follows the approach of the
[Perfect Dark PC port](https://github.com/perfect-dark-pc-port/perfect_dark),
which ports the same Rare "Indy" engine (a later revision).
> **Status: scaffolding.** The directory structure, build system, and port-layer
> interfaces are in place. The actual implementation is being brought in phase
> by phase. See below.
## Read this first
* **[docs/PCPortResearch.md](docs/PCPortResearch.md)** — the preliminary
research: how the PD port works, what GE-specific work remains (the `G_SETTEX`
RSP command, custom color-combiner/render modes), the hardware surfaces that
must be shimmed, and the phased plan. **Start here.**
* **[port/README.md](port/README.md)** — the port layer layout and how it fits
together.
## How it works (one paragraph)
The N64 game code (`src/game`, `src`) is compiled **unmodified** for the host.
The **RSP (the N64's 3D coprocessor) is emulated in software**
(`port/fast3d/`): it interprets the display list the game builds and emits
OpenGL calls, bypassing the RDP. All other N64 hardware (VI, AI, PI, SI, OS
threads/timers) is shimmed in `port/src/libultra.c` and friends. Saves
(Memory Pak / EEPROM) are backed by files.
## Layout
```
CMakeLists.txt # PC build (parallel to the N64 Makefile)
cmake/ # CMake helper modules
build-pc.sh # configure + build helper
port/
├── fast3d/ # software RSP (adapted from the PD port)
├── include/ # port-facing headers
└── src/ # port layer (main, libultra shims, video, audio, input, ...)
docs/PCPortResearch.md
```
The N64 build (`Makefile`, `tools/`, `rsp/`, etc.) is **untouched**.
## Building (once implementation lands)
```sh
./build-pc.sh ntsc-final # or pal-final / jpn-final
```
Then put your ROM in `./data/` and run the binary. (The ROM is not
distributed; you must own the game.)
## Phased plan (summary)
| Phase | Goal |
|-------|------|
| 0 | Scaffolding (this change) |
| 1 | Boot to a window: system/fs/rom/config/video + OS shims |
| 2 | Rendering: bring in fast3d, add `G_SETTEX`, port scheduler |
| 3 | Audio + input |
| 4 | Saves + polish (widescreen, mouse-look, FPS, config) |
Details and risks: [docs/PCPortResearch.md §8–9](docs/PCPortResearch.md).
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
#
# Build the GoldenEye 007 PC port.
#
# Usage:
# ./build-pc.sh [ntsc-final|pal-final|jpn-final]
#
# Dependencies (see docs/PCPortResearch.md):
# - CMake >= 3.16
# - SDL2 dev
# - zlib dev
# - OpenGL dev (opengl32 on Windows, GL on Linux, OpenGL.framework on macOS)
#
# Example (Linux):
# sudo apt install cmake libsdl2-dev zlib1g-dev libgl4-mesa-dev
# Example (macOS):
# brew install cmake sdl2 zlib
# Example (Windows/MSYS2):
# pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-SDL2 \
# mingw-w64-x86_64-zlib mingw-w64-x86_64-cmake
#
set -euo pipefail
ROMID="${1:-ntsc-final}"
BUILD_DIR="build-pc"
echo "==> Configuring PC port (ROMID=${ROMID})"
cmake -S . -B "${BUILD_DIR}" -DROMID="${ROMID}"
echo "==> Building"
cmake --build "${BUILD_DIR}" -j
echo "==> Done."
echo " Binary: ${BUILD_DIR}/ge007.*"
echo " Put your ROM in ./data/ (see README) and run the binary."
+172
View File
@@ -0,0 +1,172 @@
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.
# Locate SDL2 library
# This module defines
# SDL2_LIBRARY, the name of the library to link against
# SDL2_FOUND, if false, do not try to link to SDL2
# SDL2_INCLUDE_DIR, where to find SDL.h
#
# This module responds to the the flag:
# SDL2_BUILDING_LIBRARY
# If this is defined, then no SDL2_main will be linked in because
# only applications need main().
# Otherwise, it is assumed you are building an application and this
# module will attempt to locate and set the the proper link flags
# as part of the returned SDL2_LIBRARY variable.
#
# Don't forget to include SDL2main.h and SDL2main.m your project for the
# OS X framework based version. (Other versions link to -lSDL2main which
# this module will try to find on your behalf.) Also for OS X, this
# module will automatically add the -framework Cocoa on your behalf.
#
#
# Additional Note: If you see an empty SDL2_LIBRARY_TEMP in your configuration
# and no SDL2_LIBRARY, it means CMake did not find your SDL2 library
# (SDL2.dll, libsdl2.so, SDL2.framework, etc).
# Set SDL2_LIBRARY_TEMP to point to your SDL2 library, and configure again.
# Similarly, if you see an empty SDL2MAIN_LIBRARY, you should set this value
# as appropriate. These values are used to generate the final SDL2_LIBRARY
# variable, but when these values are unset, SDL2_LIBRARY does not get created.
#
#
# $SDL2DIR is an environment variable that would
# correspond to the ./configure --prefix=$SDL2DIR
# used in building SDL2.
# l.e.galup 9-20-02
#
# Modified by Eric Wing.
# Added code to assist with automated building by using environmental variables
# and providing a more controlled/consistent search behavior.
# Added new modifications to recognize OS X frameworks and
# additional Unix paths (FreeBSD, etc).
# Also corrected the header search path to follow "proper" SDL2 guidelines.
# Added a search for SDL2main which is needed by some platforms.
# Added a search for threads which is needed by some platforms.
# Added needed compile switches for MinGW.
#
# On OSX, this will prefer the Framework version (if found) over others.
# People will have to manually change the cache values of
# SDL2_LIBRARY to override this selection or set the CMake environment
# CMAKE_INCLUDE_PATH to modify the search paths.
#
# Note that the header path has changed from SDL2/SDL.h to just SDL.h
# This needed to change because "proper" SDL2 convention
# is #include "SDL.h", not <SDL2/SDL.h>. This is done for portability
# reasons because not all systems place things in SDL2/ (see FreeBSD).
#
# Ported by Johnny Patterson. This is a literal port for SDL2 of the FindSDL.cmake
# module with the minor edit of changing "SDL" to "SDL2" where necessary. This
# was not created for redistribution, and exists temporarily pending official
# SDL2 CMake modules.
FIND_PATH(SDL2_INCLUDE_DIR SDL.h
HINTS
$ENV{SDL2DIR}
PATH_SUFFIXES include/SDL2 include
PATHS
~/Library/Frameworks
/Library/Frameworks
/usr/local/include/SDL2
/usr/include/SDL2
/sw # Fink
/opt/local # DarwinPorts
/opt/csw # Blastwave
/opt
/boot/system/develop/headers/SDL2 #Hiaku OS
)
#MESSAGE("SDL2_INCLUDE_DIR is ${SDL2_INCLUDE_DIR}")
FIND_LIBRARY(SDL2_LIBRARY_TEMP
NAMES SDL2
HINTS
$ENV{SDL2DIR}
PATH_SUFFIXES lib64 lib
PATHS
/sw
/opt/local
/opt/csw
/opt
/system/lib #Hiaku OS
)
#MESSAGE("SDL2_LIBRARY_TEMP is ${SDL2_LIBRARY_TEMP}")
IF(NOT SDL2_BUILDING_LIBRARY)
IF(NOT ${SDL2_INCLUDE_DIR} MATCHES ".framework")
# Non-OS X framework versions expect you to also dynamically link to
# SDL2main. This is mainly for Windows and OS X. Other (Unix) platforms
# seem to provide SDL2main for compatibility even though they don't
# necessarily need it.
FIND_LIBRARY(SDL2MAIN_LIBRARY
NAMES SDL2main
HINTS
$ENV{SDL2DIR}
PATH_SUFFIXES lib64 lib
PATHS
/sw
/opt/local
/opt/csw
/opt
)
ENDIF(NOT ${SDL2_INCLUDE_DIR} MATCHES ".framework")
ENDIF(NOT SDL2_BUILDING_LIBRARY)
# SDL2 may require threads on your system.
# The Apple build may not need an explicit flag because one of the
# frameworks may already provide it.
# But for non-OSX systems, I will use the CMake Threads package.
IF(NOT APPLE)
FIND_PACKAGE(Threads)
ENDIF(NOT APPLE)
# MinGW needs an additional library, mwindows
# It's total link flags should look like -lmingw32 -lSDL2main -lSDL2 -lmwindows
# (Actually on second look, I think it only needs one of the m* libraries.)
IF(MINGW)
SET(MINGW32_LIBRARY mingw32 CACHE STRING "mwindows for MinGW")
ENDIF(MINGW)
SET(SDL2_FOUND "NO")
IF(SDL2_LIBRARY_TEMP)
# For SDL2main
IF(NOT SDL2_BUILDING_LIBRARY)
IF(SDL2MAIN_LIBRARY)
SET(SDL2_LIBRARY_TEMP ${SDL2MAIN_LIBRARY} ${SDL2_LIBRARY_TEMP})
ENDIF(SDL2MAIN_LIBRARY)
ENDIF(NOT SDL2_BUILDING_LIBRARY)
# For OS X, SDL2 uses Cocoa as a backend so it must link to Cocoa.
# CMake doesn't display the -framework Cocoa string in the UI even
# though it actually is there if I modify a pre-used variable.
# I think it has something to do with the CACHE STRING.
# So I use a temporary variable until the end so I can set the
# "real" variable in one-shot.
IF(APPLE)
SET(SDL2_LIBRARY_TEMP ${SDL2_LIBRARY_TEMP} "-framework Cocoa")
ENDIF(APPLE)
# For threads, as mentioned Apple doesn't need this.
# In fact, there seems to be a problem if I used the Threads package
# and try using this line, so I'm just skipping it entirely for OS X.
IF(NOT APPLE)
SET(SDL2_LIBRARY_TEMP ${SDL2_LIBRARY_TEMP} ${CMAKE_THREAD_LIBS_INIT})
ENDIF(NOT APPLE)
# For MinGW library
IF(MINGW)
SET(SDL2_LIBRARY_TEMP ${MINGW32_LIBRARY} ${SDL2_LIBRARY_TEMP})
ENDIF(MINGW)
# Set the final string here so the GUI reflects the final state.
SET(SDL2_LIBRARY ${SDL2_LIBRARY_TEMP} CACHE STRING "Where the SDL2 Library can be found")
# Set the temp variable to INTERNAL so it is not seen in the CMake GUI
SET(SDL2_LIBRARY_TEMP "${SDL2_LIBRARY_TEMP}" CACHE INTERNAL "")
SET(SDL2_FOUND "YES")
ENDIF(SDL2_LIBRARY_TEMP)
INCLUDE(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2
REQUIRED_VARS SDL2_LIBRARY SDL2_INCLUDE_DIR)
+28
View File
@@ -0,0 +1,28 @@
# Determine the target architecture and set TARGET_ARCH / TARGET_IS_64BIT.
# Modelled on the Perfect Dark PC port's cmake/TargetArch.cmake.
function(target_architecture OUT_VAR)
if(CMAKE_CROSSCOMPILING)
# Derive from the cross-compiler target, e.g. x86_64-w64-mingw32-gcc
string(REGEX MATCH "^(i[3-6]86|x86_64|aarch64|arm|powerpc|wasm32)" _arch
"${CMAKE_C_COMPILER_TARGET}")
else()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
set(_arch "x86_64")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(i[3-6]86|x86)$")
set(_arch "i686")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$")
set(_arch "aarch64")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm)$")
set(_arch "arm")
else()
set(_arch "${CMAKE_SYSTEM_PROCESSOR}")
endif()
endif()
if(_arch STREQUAL "i386")
set(_arch "i686")
endif()
set(${OUT_VAR} "${_arch}" PARENT_SCOPE)
endfunction()
+485
View File
@@ -0,0 +1,485 @@
# GoldenEye 007 — PC Port: Preliminary Research
This document captures the preliminary research done to plan a PC port of
GoldenEye 007, based on the decompilation in this repository
(`n64decomp/007`). It is modelled on the approach used by the
[Perfect Dark PC port](https://github.com/perfect-dark-pc-port/perfect_dark),
which ports the same Rare "Indy" engine (a later revision) to modern
platforms.
---
## 1. Executive summary
* **Feasibility: HIGH.** GoldenEye and Perfect Dark share the same Rare
engine family. The PD port proves the whole approach works end-to-end
(singleplayer + split-screen multiplayer, mouse-look, widescreen, 60 FPS,
mods). GE is, if anything, a *simpler* graphics pipeline than PD in some
respects, though it has a few custom RSP extensions (see §5).
* **Recommended strategy: fork this decomp and add a `port/` layer**, exactly
mirroring the PD port's layout. The N64 build (Makefile + IDO) stays
untouched; the PC build is a parallel CMake build that compiles the same
game C sources against a set of PC shims.
* **The single hardest component is the RSP (Reality Signal Processor)
emulator.** The PD port ships a complete, working "fast3d" software RSP
(`port/fast3d/`, ~7k lines of C++) that interprets GBI display lists and
emits OpenGL calls. It **already handles `G_TRI4`**, which is the main
custom triangle command GE uses. GE's other custom command, `G_SETTEX`,
appears **unused** (its emitter `gsSPUseTexture` is never called), so the
remaining GE-specific work is mostly **verifying the custom
render/color-combiner modes** against the RSP ucode.
* **Audio is straightforward.** GE uses the standard libaudio (AL) engine on
the R4300, the same as PD. The PD port's SDL audio + mixer approach carries
over directly.
* **Saves** are **EEPROM-based** (controller EEPROM via `osEeprom*`), not
Memory Pak. The PFS/motor code in `joy.c` is only for accessory (Rumble Pak
/ Memory Pak) detection and can be stubbed. Phase 4 is therefore a
file-backed EEPROM plus no-op PFS/motor shims — simpler than a full Memory
Pak emulation.
---
## 2. What the PD port actually does (the reference architecture)
The PD port is a **fork of the PD decomp** with one added top-level
directory, `port/`. The original game code (`src/game/*.c`, `src/lib/*.c`)
is compiled **unmodified** for the PC. All N64-hardware dependencies are
satisfied by the `port/` layer:
```
port/
├── fast3d/ # Software RSP: interprets GBI display lists -> OpenGL
│ ├── gfx_pc.cpp # the RSP interpreter (matrices, lights, tris, fog)
│ ├── gfx_opengl.cpp # OpenGL rendering backend (shaders, textures, zbuf)
│ ├── gfx_sdl2.cpp # SDL2 window-manager backend
│ ├── gfx_cc.cpp # color-combiner -> GLSL shader compiler
│ └── ...
├── include/ # port-facing headers
│ ├── audio.h config.h fs.h input.h mixer.h mod.h
│ ├── romdata.h system.h video.h utils.h platform.h
└── src/
├── main.c # PC entry point (replaces boot.s / _start.s)
├── libultra.c # shims for the libultra OS API (threads, time, VI, PI, SI, SP, AI)
├── video.c # SDL2 window + GL context + frame pacing
├── audio.c # SDL audio device, buffer queueing
├── mixer.c # audio mixing / RSP-audio-buffer processing
├── mod.c # MOD music playback
├── input.c # SDL2 keyboard/mouse/gamepad -> N64 controller structs
├── fs.c # filesystem abstraction over the ROM + data dir
├── romdata.c # loads the .z64 ROM from disk, sets up segments
├── config.c # INI config (pd.ini)
├── system.c # platform primitives (time, logging, paths)
├── crash.c # crash handler / stack traces
├── pdsched.c # replacement RCP scheduler that drives the software RSP
└── utils.c
```
### 2.1 The key architectural insight: emulate the RSP, bypass the RDP
On real hardware the render path is:
```
R4300 builds a Gfx display list
|
v
RSP (coproc 0) runs "fast3d" ucode, turns GBI cmds into RDP cmds
|
v
RDP (coproc 1) rasterizes into the framebuffer
```
The PD port **emulates the RSP in software** (`fast3d/gfx_pc.cpp`) and
**bypasses the RDP entirely**: the software RSP translates GBI commands
directly into OpenGL calls. The R4300 game code is run natively and is
unaware of the difference — it just builds display lists and "starts the
RSP" as usual.
The frame flow (from `port/src/pdsched.c` + `port/src/video.c`):
```
videoStartFrame() -> gfx_start_frame()
game builds display list (Gfx*)
videoSubmitCommands(Gfx* cmds) -> gfx_run(cmds) # <- the software RSP runs here
videoEndFrame() -> gfx_end_frame() # swap buffers, FPS accounting
```
### 2.2 How the RSP is invoked
The game's RCP scheduler (`src/lib/sched.c` in PD, `src/sched.c` in GE) calls
`osSpTaskLoad()` + `osSpTaskStartGo()` to hand a task (gfx or audio) to the
RSP. The port:
* **Excludes** the original `libultra/io/sptask.c` (and the other `io/*.c`
hardware drivers) from the PC build.
* Provides its own scheduler (`pdsched.c`) that, instead of writing to RSP
registers, calls `gfx_run()` on the display list for gfx tasks and feeds the
audio buffer for audio tasks.
* Shims the rest of the libultra OS API in `libultra.c` (threads become
no-ops / single-threaded, `osGetTime` maps to a host clock, VI/PI/SI/AI
functions map to the port's video/audio/fs/input layers).
### 2.3 Build system
The PC build is **CMake** (the N64 build stays Make + IDO). Dependencies:
**SDL2, zlib, OpenGL** (plus `stdc++`, `m`, `dl`). It compiles:
* `src/game/*.c` (all of it)
* a curated list of `src/lib/*.c` (the engine helpers that are
platform-independent: mema, memp, model, music, snd, vi, rdp, etc.)
* `src/lib/ultra/audio/*.c` (libaudio — runs fine on the CPU)
* `src/lib/ultra/gu/*.c` (gu matrix helpers)
* `port/src/*.c` + `port/src/*.cpp` + `port/fast3d/*.cpp`
It deliberately **excludes** the N64 I/O drivers (`src/lib/ultra/io/*.c`),
the boot/TLB assembly, and the RSP microcode (`.s` under `rsp/`).
---
## 3. GoldenEye decomp: what we're porting
* **Repo:** `n64decomp/007` (this repo). WIP decomp that byte-matches the
US/EU/JP ROMs. Game code lives in `src/game/` (~242 files) plus engine
files in `src/`.
* **Engine layout** (same family as PD):
* `src/sched.c` — RCP scheduler (`osCreateScheduler`, `__scExec`,
`__scYield`). Structurally near-identical to PD's `src/lib/sched.c`.
* `src/game/rsp.c` — builds the Gfx display list, wraps it in an `OSScTask`,
and starts it via the scheduler (`rspGfxTaskStart`).
* `src/fr.c` — frame/VI management, video modes (`osViSetMode`).
* `src/audi.c`, `src/snd.c`, `src/music.c` — libaudio-based sound + music.
* `src/joy.c` — controllers, **EEPROM** save handling (via `joyGamePak*`
-> `osEeprom*`), plus PFS/motor code for accessory (Rumble Pak / Memory
Pak) detection only.
* `src/init.c` — `init()` -> `mainproc()` -> `bossEntry()` (game entry).
**Compiled for the PC** (provides `mainproc()`); its N64-only `init()` is
stubbed (see A3).
* **Custom RSP microcode:** `rsp/graphics/gmain.s` (1545 lines) — a modified
fast3d. This is the RSP-side code that consumes the display list. For the PC
port we do **not** run this; we replace it with the software RSP. But it is
the authoritative reference for which GBI commands GE actually emits.
### 3.1 Entry / main loop
```
boot.s -> init() [src/init.c]
-> decompress data segment
-> osInitialize(), TLB setup
-> osCreateThread(mainThread, mainproc)
mainproc()
-> idleCreateThread(), piCreateManager(), rmonCreateThread()
-> schedulerInitThread() # osCreateScheduler + osScAddClient(gfxClient)
-> bossEntry() # [src/boss.c] real game start
```
The port's `main.c` replaces `boot.s`/`init()` and drives `mainproc()` (or
`bossEntry()`) directly after setting up video/audio/input/ROM.
### 3.2 The two libultra trees: `src/libultra/` and `src/libultrare/`
GE ships **two** libultra trees. `src/libultra/` is the standard Nintendo
libultra; `src/libultrare/` holds Rare's modified/replacement files. The N64
build selects which files to compile via `src/libultrare/Makefile.libultrare`,
which explicitly lists each file as *original* (from `libultra/`) or *Rare*
(from `libultrare/`). The Rare files **override** the originals where they
overlap.
What the PC build must do with each:
* **`src/libultra/audio/*.c`** — standard libaudio (AL). Runs on the CPU;
**compile** (the game's `audi.c`/`snd.c`/`music.c` depend on it).
* **`src/libultrare/audio/*.c`** (`drvrNew.c`, `env.c`, `reverb.c`) — Rare's
audio "New" driver. `synthesizer.c` calls `alSaveNew`/`alAuxBusNew`/
`alMainBusNew`/`alLoadNew`/`alResampleNew`/`alEnvmixerNew`/`alEnvmixerParam`/
`alFxParam`, all defined here. Runs on the CPU; **compile**.
* **`src/libultra/gu/*.c`** — matrix helpers (`guMtxF`, `guOrtho`, ...).
**Compile**.
* **`src/libultrare/io/vitbl.c`** — defines `osViModeTable` (a data table),
referenced ~15× by `fr.c`. **Compile** (it's data, not a driver). This is the
*only* `io/` file we compile.
* **All other `src/libultra/io/*.c` and `src/libultrare/io/*.c`** — hardware
drivers (PI, SI, SP, DP, VI, controller, PFS, motor, EEPROM, ...). **Exclude
and shim** in `port/src/libultra.c`. Notably `pfsinit.c`/`pfsisplug.c`
(`osPfsInit`/`osPfsIsPlug`) and `motor.c` (`osMotor*`) are called by
`joy.c` only for accessory detection, so they are shimmed as no-ops (see §7).
* **`src/libultra/os/*.c` + `src/libultrare/os/*.c`** — OS core (threads,
message queues, timers, cache, TLB). **Exclude and shim** in
`port/src/libultra.c` (+ `n64stubs.c` for the boot/TLB/FPU symbols).
> The exact original-vs-Rare file lists are in
> `src/libultrare/Makefile.libultrare` (`LIBULTRA_*_C_FILES` vs
> `LIBULTRARE_*_C_FILES`). Use it as the ground truth when curating the PC
> build's source list.
---
## 4. N64 hardware surfaces that must be shimmed
| Subsystem | GE API used | Port strategy (from PD) |
|-----------|-------------|--------------------------|
| RSP (coproc 0) | `osSpTaskLoad/StartGo/Yield`, display lists | Software RSP (`fast3d`) — **main work** |
| RDP (coproc 1) | `osDpSetStatus`, `osDpSetNextBuffer`, render modes | Bypassed; OpenGL backend |
| VI (video) | `osViSetMode`, `osViVSyncCallback`, `osViWaitVSync` | SDL2 window + GL context + frame pacing |
| AI (audio) | `osAiSetFrequency/SetNextBuffer/GetLength` | SDL audio device + queue |
| PI (peripheral) | `osPiRawStartDma`, `osPiCreateManager`, cart reads | Read from ROM file on disk |
| SI (controller) | `osContInit/StartReadData`, `osEeprom*`, `osPfs*`/`osMotor*` | SDL input + file-backed EEPROM; PFS/motor stubbed (accessory detection only) |
| OS core | threads, msg queues, timers, `osGetTime`, cache | Single-threaded shims + host clock |
| R4300 specifics | TLB, cache, FPU csr, K0/K1 segments | No-ops / identity (native x86-64) |
---
## 5. GE-specific graphics differences vs PD (the RSP work)
GE's `include/gbi_extension.h` defines the custom GBI surface. Compared to the
PD port's `fast3d`, the deltas are:
1. **`G_TRI4`** — draws up to 4 triangles in one command using 4-bit vertex
indices (0–15). A memory-saving optimization (Rare packed 4 tris into the
slot normally used for `G_TRI2`).
* **Status: already supported** by the PD `fast3d` (`gfx_sp_tri4`, and the
PD game itself uses it). ✅
2. **`G_SETTEX`** (`0xc0`) — "use texture from bank": selects a texture by
bank/tile with detail/mipmap type, min-level, detail-id and texture-id.
Emitted only by `gsSPUseTexture` (`include/gbi_extension.h:193`).
* **Status: appears UNUSED.** `gsSPUseTexture` is defined but never called
anywhere in the game code or the RSP ucode (`rsp/graphics/gmain.s`), and
`G_SETTEX` appears nowhere outside its `#define`. The game uses the
standard texture commands (`gSPTexture`, `gSPTextureL`,
`gSPTextureRectangle`, `gSPTextureRectangleFlip`) — all already handled
by the PD `fast3d`. **If this holds (verify the decomp is complete),
Phase 2 shrinks to verifying the custom CC/RM modes and the PD fast3d
may work largely as-is.** Keep a `G_SETTEX` decode path as a safety net,
but it is no longer the headline task.
3. **Custom color-combiner modes** — `G_CC_MODULATEIFADE`, `G_CC_FADE`,
`G_CC_DECALFADE`, `G_CC_BLENDRGBFADEA`, etc. (fade/blend variants).
* **Status: handled generically** by the fast3d color-combiner -> GLSL
shader compiler (`gfx_cc.cpp`), which compiles arbitrary CC equations.
Needs verification that each GE mode produces correct GLSL.
4. **Custom render mode** — `G_RM_CUSTOM_AA_ZB_XLU_SURF` (AA_ZB_XLU with
`Z_UPD`).
* **Status: handled** by the render-mode -> GL state mapping; verify.
5. **`gDPLoadTLUT06/07`** — custom TLUT load variants.
* **Status: minor**, map onto the existing `G_LOADTLUT` path.
**Conclusion:** the PD `fast3d` is a strong starting point. `G_SETTEX` appears
unused (see item 2), so the concrete new RSP work is **verification of the
custom CC/RM modes** against `gmain.s`, with `G_SETTEX` as a low-priority
safety net. Everything else (matrices, lights, fog, tri1/tri4, texture load,
sync/flush) is shared.
> Note: GE's on-hardware RSP ucode is `rsp/graphics/gmain.s`. We do not run it
> on PC, but it is the ground truth for the exact command encodings GE emits —
> use it to validate the software RSP's command decoding.
---
## 6. Audio
* GE uses **libaudio (AL)** on the R4300 (`src/audi.c`, `src/snd.c`,
`src/music.c`) — identical architecture to PD. Sound synthesis (ADPCM
decode, mixing, FX) runs on the CPU; the RSP audio ucode is a thin
pass-through to the AI DMA.
* The PD port runs libaudio natively and routes the mixed output through
`audio.c` (SDL device) + `mixer.c`. **Carries over directly.**
* Music: GE uses the same MOD-style music system; PD's `mod.c` is the model.
* Output rate: PD uses 22020 Hz stereo s16. GE's `OUTPUT_RATE` in `audi.c`
should be matched.
---
## 7. Input & saves
* **Controllers:** `src/joy.c` uses `osContInit` / `osContStartReadData`.
The port maps SDL2 keyboard/mouse/gamepad into the N64 `OSError`/controller
structs. PD implements 1964GEPD-style and Xbox-style bindings — a good
default for GE too (fire=Z, aim=R, etc.).
* **Saves:** GE saves are **EEPROM-based**, not Memory Pak. The actual save
I/O (`src/game/file2.c`) uses `joyGamePakProbe/LongRead/LongWrite`, which
map to `osEepromProbe/LongRead/LongWrite` (controller EEPROM). The PFS/motor
code in `joy.c` (`osPfsInit`, `osMotorInit/Start/Stop`) is only for
accessory (Rumble Pak / Memory Pak) detection. The port therefore:
* backs the EEPROM with a file in the data dir (e.g. `eeprom.bin`), and
* stubs PFS/motor to report "no accessory" so the game proceeds without
rumble/mempak.
This is simpler than a full Memory Pak/PFS emulation.
---
## 8. Recommended plan
### Phase 0 — Scaffolding (this change)
* Add `port/` directory structure + headers + stub sources.
* Add a top-level `CMakeLists.txt` (+ `cmake/` modules) for the PC build,
parallel to the N64 `Makefile`.
* This document + `port/README.md`.
### Phase 1 — Boot to a window
* Port `system.c`, `fs.c`, `romdata.c`, `config.c`, `main.c`.
* Port `video.c` + a minimal `fast3d` (clear color, present).
* Goal: load `ge007.u.z64`, open a window, draw a clear color.
### Phase 2 — RSP / rendering
* Bring in the PD `fast3d` (gfx_pc / gfx_opengl / gfx_cc / gfx_sdl2).
* Verify the custom CC/RM modes against `gmain.s`; add a `G_SETTEX` decode
path only if it turns out to be used (see §5 — it appears unused).
* Port `pdsched.c` (GE variant) to drive the software RSP.
* Goal: render the title screen / first level.
### Phase 3 — Audio + input
* Port `audio.c`, `mixer.c`, `mod.c`; wire libaudio output to SDL.
* **Decide the ASP strategy** (see §11 C2): if audio runs the `aspMain`
microcode, wire `aspMainTextStart`/`aspMainDataStart` to the real microcode
bytes in the ROM; if audio is CPU-only, the `port/src/ucode.c` dummies stand.
* Port `input.c` (keyboard/mouse/gamepad).
* Goal: playable with sound.
### Phase 4 — Saves + polish
* File-backed EEPROM (saves); PFS/motor already stubbed as no-ops.
* Widescreen, mouse-look, FPS options, config file, crash handler.
* Goal: feature-parity with the original + QoL extras.
---
## 9. Open questions / risks
1. **`G_SETTEX` (low priority)** — appears unused (`gsSPUseTexture` is never
called). If a future decomp revision or a missed call site shows it is used,
reverse the texture-bank selection (bank -> image, tile, detail/mipmap type)
from `gmain.s` + `tex.c` / `initmttex.c`. Not currently the main unknown.
2. **Custom CC correctness** — the GLSL compiler must handle every fade/blend
mode GE uses; validate per-mode against the original. This is now the main
rendering unknown.
3. **Scheduler fidelity** — GE's `sched.c` has speed-graph / profiling hooks
(`speedgraphMarkerHandler`) and a slightly different task model than PD;
the port scheduler must reproduce the gfx/audio interleaving and VSync
pacing.
4. **Decomp completeness** — the decomp is WIP; some functions may still be
stubs/`unk_`. The PC port inherits whatever state the decomp is in. Track
the upstream `n64decomp/007` and rebase.
5. **Endianness / alignment** — GE, like PD, is big-endian N64 code with
unaligned accesses; the PD port handles this with careful casts and
`-fno-strict-aliasing`. Reuse those flags.
---
## 10. References
* PD port: https://github.com/perfect-dark-pc-port/perfect_dark
* PD decomp: https://github.com/n64decomp/perfect_dark
* GE decomp (this repo): https://github.com/n64decomp/007
* GE docs: https://github.com/kholdfuzion/goldeneye_docs
* N64 devkit / libultra: `include/PR/*.h` in this repo
* GE custom GBI: `include/gbi_extension.h`
* GE RSP ucode: `rsp/graphics/gmain.s`
---
## 11. Scaffolding review: findings & resolutions
The initial scaffolding was reviewed against the sources. Each finding below
was verified and resolved in the same change. Code comments in `CMakeLists.txt`
and `port/src/` reference these labels (A1–A4, B1–B4).
### A. Build-blocking issues
* **A1 — Region/version macros.** The N64 `Makefile` defines a full per-region
macro set (`VERSION_*`, `LANG_*`, `REFRESH_*`, `LEFTOVERDEBUG`,
`LEFTOVERSPECTRUM`, `BUGFIX_R*`, `BYTEMATCH`) that the game code `#ifdef`s
heavily (e.g. `VERSION_EU` in 60+ files). **Resolved:** `CMakeLists.txt`
now emits the matching `REGION_DEFS` per `ROMID`, mirroring the Makefile
exactly.
* **A2 — Missing source files.** The initial `SRC_ENGINE` list + "exclude all
`io/*.c`" strategy missed files the included code depends on. **Resolved:**
added `src/cfb.c` (defines `cfb_16`, the framebuffer),
`src/libultrare/audio/*.c` (`drvrNew`/`env`/`reverb` — the audio "New"
driver `synthesizer.c` calls), and `src/libultrare/io/vitbl.c` (defines
`osViModeTable`, referenced ~15× by `fr.c`). `motor.c` / `pfsinit.c` /
`pfsisplug.c` are **not** compiled — `joy.c` calls `osMotor*`/`osPfs*` only
for accessory detection, so they are shimmed as no-ops (see B2).
* **A3 — `mainproc()` / `init.c` + the excluded engine files' symbols.**
`mainproc()` is defined only in `src/init.c`, which the initial build
excluded, contradicting `main.c` calling it. **Resolved:** `init.c` is now
**compiled** (it provides `mainproc()` + the thread-setup helpers). Its
N64-only `init()` is compiled but not called.
Compiling `init.c` (and the other included engine files) pulls in a set of
symbols that live in the EXCLUDED files (`sched.c`, `rmon.c`, `vi.c`,
`stacks.c`). Each is now provided:
* **`src/stacks.c`** (added to `SRC_ENGINE`) — thread-stack arrays
`sp_boot`/`sp_rmon`/`sp_idle`/`sp_shed`/`sp_main`/`sp_audi` (+ `sp_debug`
under `LEFTOVERDEBUG`). Pure data; the `SP_*_SZ` sizes in
`bondconstants.h` match exactly, so `sizeof()` is correct.
* **`port/src/gesched.c`** (replaces `sched.c`) — scheduler globals
`os_scheduler`, `gfxClient[3]`, `g_schedViCurrentFrameBuffer`,
`g_ViChangeVideoModes`, `g_ViModes`, `g_ViModePtrs`, plus
`get_counters()` and `permit_stderr()`.
* **`port/src/n64stubs.c`** — `init()`'s N64 symbols (segment starts,
segment boundary pointers, `jump_decompressfile`, TLB, FPU CSR) and the
rmon host-I/O functions (`rmonMain`, `osReadHost`, `osWriteHost`,
`rmonGetToken`, `rmonStatus`, `osSyncPrintf`).
* **`port/src/libultra.c`** — the libultra OS calls `init.c` makes, plus
`osTvType` (region-based), `viInit`, `vi_c_debug_MQ` (from the excluded
`vi.c`), and `osPiReadIo` (cartridge token read, called by `token.c`).
Every external symbol referenced by the compiled set was cross-checked
against its definition; no duplicates (all other `sp_*`/`rmon*` references
are `extern` decls).
* **A4 — `libultra.c` missing the high-level message API + others.** The
initial stub had low-level `osEnqueueMesg`/`osDequeueMesg` but not
`osSendMesg`/`osRecvMesg` (used in 13/26 files) or several others. **
Resolved:** `port/src/libultra.c` now provides `osSendMesg`/`osRecvMesg`/
`osSetEventMesg`, the controller query/read API, `osPfs*`/`osMotor*`,
`osSetTimer`/`osStopTimer`/`osSetIntMask`, `osViBlack`/`osViSetYScale`/
`osViSetXScale`/`osViSetEvent`, `osUnmapTLB`, `__osGetFpcCsr`/
`__osSetFpcCsr`, `piCreateManager`, and `viDebugRemoved`. The scheduler API
(`osCreateScheduler`/`osScAddClient`/`osScGetCmdQ`) lives in
`port/src/gesched.c` (it's game scheduler API, not libultra).
### B. Research-doc corrections
* **B1 — `G_SETTEX` appears unused.** `gsSPUseTexture` (the only emitter) is
never called in the game code or `gmain.s`; the game uses standard texture
commands the PD `fast3d` already handles. **Resolved:** §5, §8 (Phase 2),
and §9 updated — Phase 2 shrinks to verifying the custom CC/RM modes; a
`G_SETTEX` decode path is kept only as a safety net.
* **B2 — Saves are EEPROM-based, not Memory Pak/PFS.** The save I/O
(`src/game/file2.c`) uses `joyGamePak*` -> `osEeprom*`; the PFS/motor code
in `joy.c` is accessory detection only. **Resolved:** §7 and Phase 4
updated — Phase 4 is a file-backed EEPROM + no-op PFS/motor shims.
* **B3 — `src/libultrare/` was unmentioned.** **Resolved:** new §3.2 documents
the two libultra trees and exactly which files the PC build compiles vs.
shims, citing `Makefile.libultrare` as ground truth.
* **B4 — `src/spectrum.c` does not exist.** **Resolved:** removed from the
`CMakeLists.txt` excluded-files comment.
### C. Assembly (`.s`) file symbols
The A/B sweeps covered excluded **`.c`** files. A second symbol source is the
MIPS assembly (`src/*.s`), also not compiled for the PC. Sweeping every global
label in the `.s` files against the compiled C set found **10 more** undefined
symbols, in two distinct classes:
* **C1 — PRNG (`src/random.s`) — ported to C, not stubbed.** `randomGetNext`,
`randomGetNextFrom`, `randomSetSeed`, and `g_randomSeed` are used for real
gameplay logic: `randomGetNextFrom()` feeds the CRC (`src/game/crc.c`),
`g_randomSeed` is persisted in replay state (`src/game/ramromreplay.c`), and
`randomGetNext()` drives `RANDOMFRAC()`/`RANDOMGETNEXT_F32()`. A no-op would
silently corrupt CRCs and replays. **Resolved:** `port/src/random.c` ports
the three functions **verbatim** (each MIPS instruction mirrored with explicit
`dsll32`/`dsrl32` masks for bit-exactness) and defines
`u64 g_randomSeed = 0xAB8D9F7781280783ULL` (the two `.words` in `random.s`).
Signatures match `src/random.h`. Verified bit-exact against a Python
simulation of the assembly (the PRNG is effectively 32-bit stored in a u64;
the initial seed's high 32 bits are exercised only on the first call).
* **C2 — RSP/ASP/GSP microcode segment markers — dummy definitions.**
`rspbootTextStart`/`rspbootTextEnd` (`rspboot.s`), `gsp3DTextStart`/
`gsp3DDataStart` (`gspboot.s`), `aspMainTextStart`/`aspMainDataStart`
(`aspboot.s`) are ROM addresses of the RSP/ASP microcode, referenced by
`src/audi.c` and `src/game/rsp.c` (declared `extern long long int <name>[]`).
**Resolved:** `port/src/ucode.c` defines all six as `long long int <name>[1] = {0}`.
The graphics path is safe permanently (fast3d interprets the GBI list directly
and never runs the gsp3D microcode, so the size differences are never read).
**Carried into Phase 3:** the audio path is the one place it could matter — if
the port emulates the ASP by *running* the `aspMain` microcode, those two
markers must point at the real microcode bytes in the ROM (PD `pd.ld`
`RSP_TEXT_SEGMENT` model); if audio is CPU-only, the dummies are fine.
A `TODO(Phase 3)` on the `aspMain*` dummies records this decision.
+70
View File
@@ -0,0 +1,70 @@
# GoldenEye 007 — PC port layer
This directory contains the PC port layer: the code that lets the (unmodified)
GoldenEye game sources run on a modern host instead of N64 hardware.
It mirrors the layout of the [Perfect Dark PC
port](https://github.com/perfect-dark-pc-port/perfect_dark), which ports the
same Rare engine family. See `docs/PCPortResearch.md` (repo root) for the full
research write-up and phased plan.
## Layout
```
port/
├── fast3d/ # Software RSP: interprets GBI display lists -> OpenGL
│ ├── gfx_pc.cpp # RSP interpreter (matrices, lights, tris, fog)
│ ├── gfx_opengl.cpp # OpenGL rendering backend (shaders, textures, zbuf)
│ ├── gfx_sdl2.cpp # SDL2 window-manager backend
│ ├── gfx_cc.cpp # color-combiner -> GLSL shader compiler
│ └── ...
├── include/ # port-facing headers
│ ├── audio.h config.h fs.h input.h mixer.h
│ ├── platform.h romdata.h system.h video.h utils.h
│ └── versioninfo.h.in
└── src/
├── main.c # PC entry point (replaces boot.s / init())
├── libultra.c # shims for the libultra OS API
├── video.c # SDL2 window + GL context + frame pacing
├── audio.c # SDL audio device, buffer queueing
├── mixer.c # audio mixing
├── input.c # SDL2 keyboard/mouse/gamepad -> N64 controller structs
├── fs.c # filesystem abstraction over the ROM + data dir
├── romdata.c # loads the .z64 ROM from disk, sets up segments
├── config.c # INI config (ge007.ini)
├── system.c # platform primitives (time, logging, paths)
├── crash.c # crash handler / stack traces
├── gesched.c # replacement RCP scheduler that drives the software RSP
└── utils.c
```
## How it works (short version)
* The R4300 game code (`src/game`, `src`) is compiled **unmodified**.
* The **RSP is emulated in software** (`fast3d/`): it interprets the Gfx
display list the game builds and emits OpenGL calls, bypassing the RDP.
* The **RCP scheduler** (`gesched.c`) replaces `src/sched.c`'s hardware task
submission: instead of writing RSP registers it calls `gfx_run()`.
* **libultra OS** functions are shimmed in `libultra.c` (single-threaded,
host clock, VI/PI/SI/AI mapped onto the video/audio/fs/input layers).
* **Saves** (Memory Pak / EEPROM) are backed by files in the data dir.
## GE-specific RSP work
The PD `fast3d` already handles `G_TRI4` (GE's 4-triangle command). The new
work for GE is:
* **`G_SETTEX`** — GE's custom texture-bank command (`include/gbi_extension.h`,
`gsSPUseTexture`). Must be added to the RSP interpreter + GL backend.
* Verify GE's custom color-combiner modes (`G_CC_*FADE*`) and render mode
(`G_RM_CUSTOM_AA_ZB_XLU_SURF`) produce correct GLSL / GL state.
Use `rsp/graphics/gmain.s` (the real GE RSP ucode) as the ground truth for the
exact command encodings.
## Status
**Scaffolding only.** The files in `src/` and `fast3d/` are stubs that define
the interfaces and mark the work to be done. The actual implementation is
brought in phase by phase (see `docs/PCPortResearch.md` §8), largely by
adapting the PD port's `port/` layer.
+51
View File
@@ -0,0 +1,51 @@
# fast3d — software RSP for the GE PC port
This directory will hold the **software RSP**: a C++ interpreter that consumes
the Gfx display list the game builds and emits OpenGL calls, bypassing the N64
RDP entirely.
## Source: adapt from the Perfect Dark PC port
The PD port ships a complete, working fast3d at
`perfect-dark-pc-port/perfect_dark/port/fast3d/`:
| File | Role | ~Lines |
|--------------------|-------------------------------------------------------------|--------|
| `gfx_pc.cpp` | The RSP interpreter: matrices, lights, fog, tri1/tri4, vtx | 2800 |
| `gfx_opengl.cpp` | OpenGL backend: shaders, textures, zbuf, framebuffers | 1300 |
| `gfx_cc.cpp` | Color-combiner equation -> GLSL shader compiler | 60 |
| `gfx_sdl2.cpp` | SDL2 window-manager backend | 460 |
| `gfx_api.h` | Public API (`gfx_init/run/start_frame/end_frame/...`) | 58 |
| `gfx_pc.h` | Internal state (RSP struct, texture cache, combiner pool) | 60 |
| `gfx_rendering_api.h` | Rendering-API vtable (GL backend implements this) | 61 |
| `gfx_window_manager_api.h` | Window-manager vtable (SDL2 implements this) | 54 |
| `glad/` | GL loader | — |
**The PD fast3d already handles `G_TRI4`** (GE's 4-triangle command) — the PD
game uses it too. So the bulk of the RSP work is done; we adapt, not rewrite.
## GE-specific work to add
1. **`G_SETTEX`** (opcode `0xc0`) — GE's custom texture-bank command
(`include/gbi_extension.h`, `gsSPUseTexture`). Add a case in the `gfx_pc.cpp`
dispatch and a corresponding texture-bank selection in the GL backend.
Reverse the exact semantics from `rsp/graphics/gmain.s` + `src/game/tex.c` /
`initmttex.c`.
2. **Verify custom color-combiner modes** — `G_CC_MODULATEIFADE`, `G_CC_FADE`,
`G_CC_DECALFADE`, `G_CC_BLENDRGBFADEA`, etc. The `gfx_cc.cpp` compiler
handles arbitrary CC equations; confirm each GE mode compiles to correct
GLSL.
3. **Verify the custom render mode** — `G_RM_CUSTOM_AA_ZB_XLU_SURF`
(AA_ZB_XLU with `Z_UPD`).
4. **`gDPLoadTLUT06/07`** — map onto the existing `G_LOADTLUT` path.
## Ground truth
`rsp/graphics/gmain.s` (repo root) is the real GE RSP ucode. We do **not** run
it on PC, but it is the authoritative reference for the exact command
encodings GE emits — use it to validate the software RSP's decoding.
## Status
**Empty (scaffolding).** Populate during Phase 2 by adapting the PD fast3d and
adding the GE-specific commands above.
+66
View File
@@ -0,0 +1,66 @@
#ifndef GFX_API_H
#define GFX_API_H
/*
* Public API for the software RSP (fast3d).
*
* This is the interface the rest of the port (video.c / gesched.c) uses to
* drive the RSP. It mirrors the PD port's port/fast3d/gfx_api.h.
*
* STATUS: scaffolding — the implementation (gfx_pc.cpp etc.) is brought in
* during Phase 2 by adapting the PD fast3d. See port/fast3d/README.md.
*/
#ifndef __cplusplus
#include <stdint.h>
#include <stdbool.h>
#endif
#include <PR/gbi.h>
#ifdef __cplusplus
extern "C" {
#endif
struct GfxRenderingAPI;
struct GfxWindowManagerAPI;
struct GfxDimensions {
float internal_mul;
uint32_t width, height;
float aspect_ratio;
};
struct GfxInitSettings {
struct GfxWindowManagerAPI *wapi;
struct GfxRenderingAPI *rapi;
/* window title / size / fullscreen / etc. — see PD gfx_window_manager_api.h */
const char *title;
uint32_t width, height;
int fullscreen;
int vsync;
};
/* Current draw-area dimensions (before any scaling). */
extern struct GfxDimensions gfx_current_dimensions;
void gfx_init(const struct GfxInitSettings *settings);
void gfx_destroy(void);
/* Frame boundary. */
void gfx_start_frame(void);
void gfx_end_frame(void);
/* Run the RSP over a display list (the core of the emulation). */
void gfx_run(Gfx *commands);
/* Options. */
void gfx_set_target_fps(int fps);
void gfx_set_texture_filter(int mode);
void gfx_set_mipmap_filter(int mode);
#ifdef __cplusplus
}
#endif
#endif /* GFX_API_H */
+36
View File
@@ -0,0 +1,36 @@
#ifndef PORT_AUDIO_H
#define PORT_AUDIO_H
/*
* Audio: SDL audio device + buffer queueing.
* Modelled on the PD port's port/include/audio.h.
*
* libaudio (AL) runs on the CPU and produces mixed s16 stereo output. The AI
* (osAiSetNextBuffer / osAiGetLength / osAiSetFrequency) is mapped onto this
* layer, which queues the mixed samples to the SDL audio device.
*/
#include <PR/ultratypes.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Initialize the SDL audio device. Returns 0 on success. */
int audioInit(void);
void audioDestroy(void);
/* Number of samples (stereo s16 frames) currently queued. */
s32 audioGetSamplesBuffered(void);
/* Queue the next block of mixed samples (len is in bytes). */
void audioSetNextBuffer(const s16 *buf, u32 len);
/* Called at the end of each frame to flush the queued buffer to the device. */
void audioEndFrame(void);
#ifdef __cplusplus
}
#endif
#endif /* PORT_AUDIO_H */
+32
View File
@@ -0,0 +1,32 @@
#ifndef PORT_CONFIG_H
#define PORT_CONFIG_H
/*
* INI configuration (ge007.ini).
* Modelled on the PD port's port/include/config.h.
*
* A tiny key=value store. Options register themselves (usually via a
* PD_CONSTRUCTOR) and are loaded from / saved to the ini in the data dir.
*/
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Load the config file (no-op if it doesn't exist yet). */
void configLoad(void);
/* Save the current values back to the config file. */
void configSave(void);
/* Register an integer option. min/max are clamping bounds (0 = unbounded). */
void configRegisterInt(const char *key, int *value, int min, int max);
/* Register a string option (buf must live for the program's lifetime). */
void configRegisterString(const char *key, char *value, int bufSize);
#ifdef __cplusplus
}
#endif
#endif /* PORT_CONFIG_H */
+46
View File
@@ -0,0 +1,46 @@
#ifndef PORT_FS_H
#define PORT_FS_H
/*
* Filesystem abstraction over the ROM + data dir.
* Modelled on the PD port's port/include/fs.h.
*
* Provides a small POSIX-ish file API so the port layer (and any game code
* that touches files) has a uniform interface across Windows/Linux/macOS.
*/
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define FS_MAXPATH 512
typedef struct FSFile FSFile;
/* Open a file. mode is a fopen-style string ("rb", "wb", "r+b", ...). */
FSFile *fsOpen(const char *path, const char *mode);
void fsClose(FSFile *f);
/* Read up to size bytes into buf; returns bytes read (-1 on error/EOF). */
int32_t fsRead(FSFile *f, void *buf, int32_t size);
/* Write size bytes from buf; returns bytes written (-1 on error). */
int32_t fsWrite(FSFile *f, const void *buf, int32_t size);
/* Seek to offset (whence: SEEK_SET/CUR/END). Returns 0 on success. */
int fsSeek(FSFile *f, int32_t offset, int whence);
/* Current file position. */
int32_t fsTell(FSFile *f);
/* File size in bytes. */
int32_t fsSize(FSFile *f);
/* Does the file exist? */
int fsExists(const char *path);
#ifdef __cplusplus
}
#endif
#endif /* PORT_FS_H */
+35
View File
@@ -0,0 +1,35 @@
#ifndef PORT_INPUT_H
#define PORT_INPUT_H
/*
* Input: SDL2 keyboard/mouse/gamepad -> N64 controller structs.
* Modelled on the PD port's port/include/input.h.
*
* The game reads controllers via osContInit / osContStartReadData (src/joy.c).
* This layer populates the N64 controller state (buttons, stick) from SDL
* events. Default bindings follow the 1964GEPD / Xbox schemes; see the PD
* port README for the reference table.
*/
#include <PR/ultratypes.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Initialize SDL input. Returns 0 on success. */
int inputInit(void);
void inputDestroy(void);
/* Poll once per frame; updates the connected-controller state that
* osContStartReadData / osContRead will return. */
void inputUpdate(void);
/* Number of controllers currently "connected" (1..4). */
int inputGetNumControllers(void);
#ifdef __cplusplus
}
#endif
#endif /* PORT_INPUT_H */
+34
View File
@@ -0,0 +1,34 @@
#ifndef PORT_MIXER_H
#define PORT_MIXER_H
/*
* Audio mixing.
* Modelled on the PD port's port/include/mixer.h.
*
* On the N64 the RSP audio ucode does the final mix / sample-rate handling
* before the AI DMA. Since the RSP is emulated in software here, this module
* performs that final processing on the CPU and hands the result to audio.c.
*
* NOTE: the exact responsibilities depend on how GE's libaudio output is
* wired (see src/audi.c). This is a placeholder interface to be refined in
* Phase 3 (audio).
*/
#include <PR/ultratypes.h>
#ifdef __cplusplus
extern "C" {
#endif
void mixerInit(void);
void mixerDestroy(void);
/* Process one frame of audio. `in` is the libaudio-mixed buffer, `frames`
* the number of stereo frames. Output is queued via audioSetNextBuffer. */
void mixerProcess(const s16 *in, u32 frames);
#ifdef __cplusplus
}
#endif
#endif /* PORT_MIXER_H */
+51
View File
@@ -0,0 +1,51 @@
#ifndef PORT_PLATFORM_H
#define PORT_PLATFORM_H
/*
* Platform abstraction for the GE PC port.
*
* Provides the small set of host primitives the port layer needs, with
* per-OS implementations selected at compile time. Modelled on the PD port's
* port/include/platform.h.
*/
#include <stdint.h>
#include <stddef.h>
#if defined(_WIN32) || defined(_WIN64)
#define PLATFORM_WINDOWS 1
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#if TARGET_OS_IPHONE
#define PLATFORM_IOS 1
#else
#define PLATFORM_MACOS 1
#endif
#elif defined(__linux__)
#define PLATFORM_LINUX 1
#else
#error "Unsupported platform"
#endif
#if defined(_M_X64) || defined(__x86_64__) || defined(__aarch64__)
#define PLATFORM_64BIT 1
#endif
/* Constructor attribute: run before main(). */
#if defined(_MSC_VER)
#define PD_CONSTRUCTOR
#define PD_EXPORT
#else
#define PD_CONSTRUCTOR __attribute__((constructor))
#define PD_EXPORT __attribute__((visibility("default")))
#endif
/* Log levels (see system.h). */
enum LogLevel {
LOG_ERROR = 0,
LOG_WARNING,
LOG_INFO,
LOG_DEBUG,
};
#endif /* PORT_PLATFORM_H */
+42
View File
@@ -0,0 +1,42 @@
#ifndef PORT_ROMDATA_H
#define PORT_ROMDATA_H
/*
* ROM loading: reads the .z64 ROM from the data dir and exposes its segments.
* Modelled on the PD port's port/include/romdata.h.
*
* The N64 build links assets in from the ROM at fixed addresses. The PC port
* instead loads the ROM file and maps the game's data segments (code, data,
* assets) into a heap-allocated image, then patches the segment table so the
* game's IMAGESEG / asset references resolve into that image.
*/
#include <PR/ultratypes.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* Load the ROM for the configured ROMID from the data dir.
* Returns 0 on success, non-zero on failure.
*/
int romdataInit(void);
void romdataDestroy(void);
/* Pointer to the loaded ROM image (start of the .z64). */
const u8 *romdataGetRom(void);
/* Size of the loaded ROM in bytes. */
u32 romdataGetRomSize(void);
/*
* Map an N64 virtual address (0xA0000000 RDRAM space) to the corresponding
* pointer in the loaded image. Used to resolve asset/segment references.
*/
const void *romdataMapVa(u32 va);
#ifdef __cplusplus
}
#endif
#endif /* PORT_ROMDATA_H */
+49
View File
@@ -0,0 +1,49 @@
#ifndef PORT_SYSTEM_H
#define PORT_SYSTEM_H
/*
* Platform primitives: time, logging, paths, exit.
* Modelled on the PD port's port/include/system.h.
*/
#include <stdint.h>
#include "platform.h"
#ifdef __cplusplus
extern "C" {
#endif
/* --- Time --------------------------------------------------------------- */
/* Monotonic microseconds since an arbitrary origin. */
uint64_t sysGetMicroseconds(void);
/* Wall-clock seconds since the epoch (for timestamps). */
int64_t sysGetTime(void);
/* Sleep for the given number of microseconds. */
void sysSleep(uint32_t micros);
/* --- Logging ------------------------------------------------------------ */
void sysLogPrintf(enum LogLevel level, const char *fmt, ...)
__attribute__((format(printf, 2, 3)));
/* --- Paths -------------------------------------------------------------- */
/*
* Resolve a path relative to the data dir. "$S/" prefixes the data dir,
* "$E/" prefixes the executable dir. Returns a pointer to a static buffer.
*/
const char *sysResolvePath(const char *path);
/* Return the directory containing the executable. */
const char *sysGetExeDir(void);
/* --- Lifecycle ---------------------------------------------------------- */
void sysExit(int code);
#ifdef __cplusplus
}
#endif
#endif /* PORT_SYSTEM_H */
+32
View File
@@ -0,0 +1,32 @@
#ifndef PORT_UTILS_H
#define PORT_UTILS_H
/*
* Small shared helpers for the port layer.
* Modelled on the PD port's port/include/utils.h.
*/
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define ARRAYCOUNT(a) (sizeof(a) / sizeof((a)[0]))
/* Read a big-endian 16/32-bit value from an (possibly unaligned) buffer. */
uint16_t readU16BE(const void *p);
uint32_t readU32BE(const void *p);
void writeU16BE(void *p, uint16_t v);
void writeU32BE(void *p, uint32_t v);
/* Clamp helpers. */
int clampi(int v, int lo, int hi);
float clampf(float v, float lo, float hi);
#ifdef __cplusplus
}
#endif
#endif /* PORT_UTILS_H */
+11
View File
@@ -0,0 +1,11 @@
#ifndef PORT_VERSIONINFO_H
#define PORT_VERSIONINFO_H
/* Generated by CMake from versioninfo.h.in — do not edit. */
#define GE007_VERSION_HASH "@VERSION_HASH@"
#define GE007_TARGET_PLATFORM "@TARGET_PLATFORM@"
#define GE007_ROMID "@ROMID@"
#define GE007_IS_PAL @VERSION_IS_PAL@
#endif /* PORT_VERSIONINFO_H */
+46
View File
@@ -0,0 +1,46 @@
#ifndef PORT_VIDEO_H
#define PORT_VIDEO_H
/*
* Video: SDL2 window + OpenGL context + frame pacing.
* Modelled on the PD port's port/include/video.h.
*
* The game's VI (osViSetMode / osViVSyncCallback / osViWaitVSync) is mapped
* onto this layer. The software RSP (fast3d) renders into the GL context that
* this layer owns.
*/
#include <PR/ultratypes.h>
#include <PR/gbi.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Initialize the window + GL context. Returns 0 on success. */
int videoInit(void);
void videoDestroy(void);
/* Frame boundary hooks, driven by the port scheduler (gesched.c). */
void videoStartFrame(void);
void videoSubmitCommands(Gfx *cmds); /* runs the software RSP on the list */
void videoEndFrame(void);
/* VSync / framerate control. */
void videoSetVsync(int enabled);
void videoSetFramerateLimit(int fps); /* 0 = unlimited */
/* Window state queries (kept in sync with the options menu). */
int videoGetFullscreen(void);
void videoSetFullscreen(int enabled);
int videoGetMaximize(void);
void videoSetMaximize(int enabled);
/* Current FPS (measured). */
float videoGetFPS(void);
#ifdef __cplusplus
}
#endif
#endif /* PORT_VIDEO_H */
+70
View File
@@ -0,0 +1,70 @@
/*
* Audio: SDL audio device + buffer queueing.
*
* libaudio (AL) produces mixed s16 stereo output on the CPU. This module owns
* the SDL audio device and queues the mixed samples. osAiSetNextBuffer /
* osAiGetLength (libultra.c) route through here.
*
* Modelled on the PD port's port/src/audio.c (~75 lines).
*
* STATUS: scaffolding stub — implement during Phase 3.
*/
#include <SDL.h>
#include "platform.h"
#include "system.h"
#include "config.h"
#include "audio.h"
static SDL_AudioDeviceID dev = 0;
static const s16 *nextBuf = NULL;
static u32 nextSize = 0;
static int bufferSize = 512;
static int queueLimit = 8192;
int audioInit(void)
{
/* TODO(Phase 3):
* - SDL_InitSubSystem(AUDIO)
* - open device: 22020 Hz (match GE's OUTPUT_RATE in src/audi.c),
* AUDIO_S16SYS, 2 channels
* - unpause
*/
sysLogPrintf(LOG_INFO, "audioInit: TODO (Phase 3)");
return 0;
}
void audioDestroy(void)
{
if (dev) { SDL_CloseAudioDevice(dev); dev = 0; }
}
s32 audioGetSamplesBuffered(void)
{
return dev ? (SDL_GetQueuedAudioSize(dev) / 4) : 0;
}
void audioSetNextBuffer(const s16 *buf, u32 len)
{
nextBuf = buf;
nextSize = len;
}
void audioEndFrame(void)
{
if (nextBuf && nextSize && dev) {
if (audioGetSamplesBuffered() < queueLimit) {
SDL_QueueAudio(dev, nextBuf, nextSize);
}
nextBuf = NULL;
nextSize = 0;
}
}
PD_CONSTRUCTOR static void audioConfigInit(void)
{
configRegisterInt("Audio.BufferSize", &bufferSize, 0, 1 * 1024 * 1024);
configRegisterInt("Audio.QueueLimit", &queueLimit, 0, 1 * 1024 * 1024);
}
+58
View File
@@ -0,0 +1,58 @@
/*
* INI configuration (ge007.ini).
*
* A tiny key=value store. Options register themselves (usually via a
* PD_CONSTRUCTOR) and are loaded from / saved to the ini in the data dir.
*
* Modelled on the PD port's port/src/config.c (~300 lines).
*
* STATUS: scaffolding stub — implement during Phase 1.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "platform.h"
#include "system.h"
#include "config.h"
#define MAX_OPTIONS 256
struct IntOption { char key[64]; int *value; int min, max; };
struct StrOption { char key[64]; char *value; int bufSize; };
static struct IntOption intOpts[MAX_OPTIONS];
static int numIntOpts = 0;
static struct StrOption strOpts[MAX_OPTIONS];
static int numStrOpts = 0;
void configRegisterInt(const char *key, int *value, int min, int max)
{
if (numIntOpts >= MAX_OPTIONS) return;
struct IntOption *o = &intOpts[numIntOpts++];
strncpy(o->key, key, sizeof(o->key) - 1);
o->value = value;
o->min = min;
o->max = max;
}
void configRegisterString(const char *key, char *value, int bufSize)
{
if (numStrOpts >= MAX_OPTIONS) return;
struct StrOption *o = &strOpts[numStrOpts++];
strncpy(o->key, key, sizeof(o->key) - 1);
o->value = value;
o->bufSize = bufSize;
}
void configLoad(void)
{
/* TODO(Phase 1): parse "$S/ge007.ini", apply to registered options. */
sysLogPrintf(LOG_INFO, "configLoad: TODO (Phase 1)");
}
void configSave(void)
{
/* TODO(Phase 1): write all registered options to "$S/ge007.ini". */
}
+26
View File
@@ -0,0 +1,26 @@
/*
* Crash handler / stack traces.
*
* Catches signals (SIGSEGV etc.) and prints a backtrace + the game's state,
* to make the inevitable porting crashes debuggable.
*
* Modelled on the PD port's port/src/crash.c (~360 lines).
*
* STATUS: scaffolding stub — implement during Phase 1.
*/
#include <stdio.h>
#include <stdlib.h>
#include "platform.h"
#include "system.h"
void crashInit(void)
{
/* TODO(Phase 1):
* - install signal handlers (SIGSEGV, SIGFPE, SIGILL, SIGBUS)
* - on crash: print backtrace (dbghelp on Windows, backtrace() on POSIX)
* - dump relevant game state (current level, frame, etc.)
*/
sysLogPrintf(LOG_INFO, "crashInit: TODO (Phase 1)");
}
+76
View File
@@ -0,0 +1,76 @@
/*
* Filesystem abstraction over the ROM + data dir.
*
* A small POSIX-ish file API so the port layer has a uniform interface across
* Windows/Linux/macOS.
*
* Modelled on the PD port's port/src/fs.c (~295 lines).
*
* STATUS: scaffolding stub — implement during Phase 1.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "platform.h"
#include "fs.h"
struct FSFile {
FILE *f;
};
FSFile *fsOpen(const char *path, const char *mode)
{
FSFile *f = (FSFile *)calloc(1, sizeof(*f));
if (!f) return NULL;
f->f = fopen(path, mode);
if (!f->f) { free(f); return NULL; }
return f;
}
void fsClose(FSFile *f)
{
if (f) { if (f->f) fclose(f->f); free(f); }
}
int32_t fsRead(FSFile *f, void *buf, int32_t size)
{
if (!f || !f->f) return -1;
return (int32_t)fread(buf, 1, (size_t)size, f->f);
}
int32_t fsWrite(FSFile *f, const void *buf, int32_t size)
{
if (!f || !f->f) return -1;
return (int32_t)fwrite(buf, 1, (size_t)size, f->f);
}
int fsSeek(FSFile *f, int32_t offset, int whence)
{
if (!f || !f->f) return -1;
return fseek(f->f, (long)offset, whence);
}
int32_t fsTell(FSFile *f)
{
if (!f || !f->f) return -1;
return (int32_t)ftell(f->f);
}
int32_t fsSize(FSFile *f)
{
if (!f || !f->f) return -1;
int32_t cur = ftell(f->f);
fseek(f->f, 0, SEEK_END);
int32_t sz = ftell(f->f);
fseek(f->f, cur, SEEK_SET);
return sz;
}
int fsExists(const char *path)
{
FILE *f = fopen(path, "rb");
if (f) { fclose(f); return 1; }
return 0;
}
+110
View File
@@ -0,0 +1,110 @@
/*
* Port RCP scheduler.
*
* Replaces the hardware task submission in src/sched.c. On the N64,
* __scExec() hands a gfx/audio task to the RSP via osSpTaskLoad/StartGo.
* Here, instead of writing RSP registers, we:
* - gfx task: call videoSubmitCommands(display_list) -> the software RSP
* - audio task: feed the mixed buffer to the audio layer
* and reproduce the gfx/audio interleaving + VSync pacing the game expects.
*
* Modelled on the PD port's port/src/pdsched.c (~405 lines).
*
* STATUS: scaffolding stub — implement during Phase 2.
*
* NOTE: GE's src/sched.c has speed-graph / profiling hooks
* (speedgraphMarkerHandler) and a slightly different task model than PD.
* Reproduce the exact gfx/audio/VSync behaviour; see
* docs/PCPortResearch.md §9.3.
*/
#include <PR/ultratypes.h>
#include <PR/os.h>
#include <PR/gbi.h>
#include "sched.h"
#include "fr.h" /* NUM_VIDEO_FRAME_BUFFERS */
#include "platform.h"
#include "video.h"
#include "audio.h"
#include "system.h"
/*
* Scheduler globals (normally defined in src/sched.c, which is EXCLUDED from
* the PC build). init.c's schedulerInitThread() references os_scheduler and
* gfxClient, so the port provides them here (matching src/sched.c:70-71).
*/
OSSched os_scheduler;
OSScClient gfxClient[3];
/*
* Scheduler/VI globals (normally defined in src/sched.c, EXCLUDED). fr.c
* references these; the port provides them so it links. Values are the
* sched.c initializers.
*/
s32 g_schedViCurrentFrameBuffer = 0;
s32 g_ViChangeVideoModes[NUM_VIDEO_FRAME_BUFFERS] = {0, 0};
OSViMode g_ViModes[NUM_VIDEO_FRAME_BUFFERS];
OSViMode *g_ViModePtrs[NUM_VIDEO_FRAME_BUFFERS];
/*
* Performance counters (normally in src/sched.c). speed_graph.c reads them
* via get_counters().
*/
static u32 g_DisplayPerformanceCounters[4]; // clock, cmc, pipe, tmem
u32 *get_counters(void) { return g_DisplayPerformanceCounters; }
/* Debug stderr control (normally in src/sched.c). boss.c calls it. */
void permit_stderr(u32 flag) { (void)flag; }
/*
* Scheduler API (declared in sched.h, normally defined in src/sched.c which
* is EXCLUDED from the PC build). The port provides these so engine code that
* calls osCreateScheduler/osScAddClient/osScGetCmdQ links. The real work —
* driving the software RSP per frame — happens in geschedRunFrame().
*/
void osCreateScheduler(OSSched *s, void *stack, u8 mode, u32 numFields)
{
/* TODO(Phase 2): initialise the port scheduler state in *s. */
(void)s; (void)stack; (void)mode; (void)numFields;
sysLogPrintf(LOG_INFO, "osCreateScheduler: TODO (Phase 2)");
}
void osScAddClient(OSSched *s, OSScClient *c, OSMesgQueue *msgQ, OSScClient *next)
{
/* TODO(Phase 2): register a gfx/audio client. */
(void)s; (void)c; (void)msgQ; (void)next;
}
void osScRemoveClient(OSSched *s, OSScClient *c)
{
(void)s; (void)c;
}
OSMesgQueue *osScGetCmdQ(OSSched *s)
{
/* TODO(Phase 2): return the scheduler's command queue. */
(void)s;
return NULL;
}
void geschedInit(void)
{
/* TODO(Phase 2): set up the scheduler + gfx client, mirroring
* schedulerInitThread() in src/init.c but with the port's frame hooks. */
sysLogPrintf(LOG_INFO, "geschedInit: TODO (Phase 2)");
}
/*
* Per-frame driver. Called by the main loop (or the VI vsync callback shim).
* Sequence (mirrors PD's pdsched.c):
* videoStartFrame()
* ... game builds the display list ...
* videoSubmitCommands(cmds)
* videoEndFrame()
*/
void geschedRunFrame(void *displayList)
{
videoStartFrame();
videoSubmitCommands((Gfx *)displayList);
audioEndFrame();
videoEndFrame();
}
+48
View File
@@ -0,0 +1,48 @@
/*
* Input: SDL2 keyboard/mouse/gamepad -> N64 controller structs.
*
* The game reads controllers via osContInit / osContStartReadData (src/joy.c).
* This module populates the N64 controller state from SDL events.
*
* Modelled on the PD port's port/src/input.c (~1550 lines). Default bindings
* follow the 1964GEPD / Xbox schemes (see the PD port README table).
*
* STATUS: scaffolding stub — implement during Phase 3.
*/
#include <SDL.h>
#include "platform.h"
#include "system.h"
#include "config.h"
#include "input.h"
static int numControllers = 1;
int inputInit(void)
{
/* TODO(Phase 3):
* - SDL_InitSubSystem(INPUT)
* - open gamepad(s)
* - set up relative mouse mode for mouse-look
* - load bindings from config
*/
sysLogPrintf(LOG_INFO, "inputInit: TODO (Phase 3)");
return 0;
}
void inputDestroy(void)
{
/* TODO(Phase 3) */
}
void inputUpdate(void)
{
/* TODO(Phase 3): poll SDL events, write into the controller state that
* osContStartReadData / osContRead will return. */
}
int inputGetNumControllers(void)
{
return numControllers;
}
+375
View File
@@ -0,0 +1,375 @@
/*
* libultra OS shims for the PC port.
*
* The game code calls the libultra OS API (threads, message queues, timers,
* VI, PI, SI, SP, AI, cache, memory). On the PC none of that hardware exists,
* so this file provides single-threaded / host-backed implementations.
*
* Modelled on the PD port's port/src/libultra.c (~490 lines). That file is the
* reference implementation to adapt; this stub marks the surface area.
*
* STATUS: scaffolding stub — implement during Phase 1 (OS core) and Phase 2
* (SP/VI), Phase 3 (AI/SI).
*/
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <PR/os.h>
#include <PR/os_internal.h>
#include <PR/rcp.h>
#include <PR/rdb.h>
#include "platform.h"
#include "system.h"
#include "video.h"
#include "audio.h"
#include "input.h"
#include "fs.h"
#include "romdata.h"
/* ------------------------------------------------------------------------ */
/* Globals the game expects to exist (normally set by osInitialize). */
/* ------------------------------------------------------------------------ */
u64 osClockRate = 6250000; /* RSP counter rate (Hz) — see PD port */
u32 osMemSize = 16 * 1024 * 1024; /* pretend 16MB RDRAM (+expansion) */
/* TV type (normally set from console hardware in os/initialize.c, excluded).
* The game reads it to pick PAL/NTSC behaviour (e.g. schedulerInitThread
* checks osTvType == OS_TV_MPAL). Set to match the emulated region: PAL
* consoles report MPAL, NTSC consoles report NTSC. */
#ifdef REFRESH_PAL
u32 osTvType = OS_TV_MPAL; /* EU: PAL (0=PAL 1=NTSC 2=MPAL) */
#else
u32 osTvType = OS_TV_NTSC; /* US/JP: NTSC */
#endif
s32 osResetType = 0;
s32 osViClock = 0;
/* ------------------------------------------------------------------------ */
/* Time */
/* ------------------------------------------------------------------------ */
/*
* The R4300 "SP timer" is a free-running counter. Map it to the host clock.
* The PD port scales host microseconds into the N64 counter domain so that
* osGetTime()-based timing in the game behaves correctly.
*/
OSTime osGetTime(void)
{
/* TODO(Phase 1): scale sysGetMicroseconds() into the N64 counter domain. */
return (OSTime)sysGetMicroseconds();
}
u32 osGetCount(void)
{
return (u32)osGetTime();
}
/* ------------------------------------------------------------------------ */
/* Threads — single-threaded: create/start are no-ops, the "main" thread is */
/* the host thread. The game's scheduler drives everything cooperatively. */
/* ------------------------------------------------------------------------ */
void osCreateThread(OSThread *t, OSId id, void (*entry)(void *), void *arg,
void *sp, OSPri prio)
{
(void)entry; (void)arg; (void)sp; (void)prio;
if (t) { t->id = id; t->state = OS_STATE_STOPPED; }
}
void osStartThread(OSThread *t) { if (t) t->state = OS_STATE_RUNNING; }
void osStopThread(OSThread *t) { if (t) t->state = OS_STATE_STOPPED; }
void osDestroyThread(OSThread *t) { if (t) t->id = 0; }
void osYieldThread(void) { /* cooperative: nothing to do */ }
OSPri osGetThreadPri(OSThread *t) { (void)t; return 0; }
void osSetThreadPri(OSId id, OSPri prio) { (void)id; (void)prio; }
/* ------------------------------------------------------------------------ */
/* Message queues — used by the scheduler / VI / controller callbacks. */
/* Implement a simple ring buffer; see PD port for the exact semantics. */
/* ------------------------------------------------------------------------ */
void osCreateMesgQueue(OSMesgQueue *mq, OSMesg first, int count)
{
/* TODO(Phase 1): allocate + init ring buffer. */
(void)mq; (void)first; (void)count;
}
OSMesg osDequeueMesg(OSMesgQueue *mq)
{
/* TODO(Phase 1) */
(void)mq;
return NULL;
}
OSMesg osMesgQueueLast(OSMesgQueue *mq)
{
/* TODO(Phase 1) */
(void)mq;
return NULL;
}
void osEnqueueMesg(OSMesgQueue *mq, OSMesg msg)
{
/* TODO(Phase 1) */
(void)mq; (void)msg;
}
/*
* High-level message API — the one the game actually uses (osSendMesg in 13
* files, osRecvMesg in 26). The PD port's libultra.c defines these; in the
* single-threaded port they are largely no-ops / immediate, but the scheduler
* and audio code rely on the semantics, so implement them properly in Phase 1.
*/
s32 osSendMesg(OSMesgQueue *mq, OSMesg msg, s32 flag)
{
/* TODO(Phase 1): enqueue msg; return 0 on success. */
(void)mq; (void)msg; (void)flag;
return 0;
}
s32 osRecvMesg(OSMesgQueue *mq, OSMesg *msg, s32 flag)
{
/* TODO(Phase 1): dequeue into *msg; return 0 on success, -1 if empty. */
(void)mq; (void)msg; (void)flag;
return 0;
}
void osSetEventMesg(OSEvent e, OSMesgQueue *mq, OSMesg msg)
{
/* TODO(Phase 1) */
(void)e; (void)mq; (void)msg;
}
/* ------------------------------------------------------------------------ */
/* VI (video) — map onto the port video layer. */
/* ------------------------------------------------------------------------ */
void osViSetMode(OSViMode *vm)
{
/* TODO(Phase 2): translate the requested mode into a window/GL viewport. */
(void)vm;
}
void osViSetEffect(OSViEffect ve) { (void)ve; }
void osViSetSpecialFeatures(u32 f) { (void)f; }
void osViVSyncCallback(OSMesgQueue *mq, OSMesg msg)
{
/* TODO(Phase 2): the port scheduler calls this each frame. */
(void)mq; (void)msg;
}
void osViWaitVSync(void)
{
/* TODO(Phase 2): block until the next vsync (frame pacing). */
}
void osViSetSync(OSMesgQueue *mq, OSMesg msg) { (void)mq; (void)msg; }
void osViBlack(u8 active)
{
/* TODO(Phase 2): if active, clear the screen (see PD port). */
(void)active;
}
void osViSetYScale(f32 value) { (void)value; }
void osViSetXScale(f32 value) { (void)value; }
void osViSetEvent(OSMesgQueue *mq, OSMesg msg, u32 retraceCount)
{ (void)mq; (void)msg; (void)retraceCount; }
/* ------------------------------------------------------------------------ */
/* AI (audio) — map onto the port audio layer. */
/* ------------------------------------------------------------------------ */
void osAiSetFrequency(u32 hz)
{
/* TODO(Phase 3): store the output rate; audio.c uses it. */
(void)hz;
}
void osAiSetNextBuffer(void *buf, u32 size)
{
/* TODO(Phase 3): hand the mixed buffer to audioSetNextBuffer(). */
(void)buf; (void)size;
}
u32 osAiGetLength(void)
{
/* TODO(Phase 3): return audioGetSamplesBuffered() in the right units. */
return 0;
}
void osAiSetConvert(u32 convert) { (void)convert; }
/* ------------------------------------------------------------------------ */
/* PI (peripheral) — ROM/cart reads come from the loaded ROM image. */
/* ------------------------------------------------------------------------ */
void osPiCreateManager(OSMesgQueue *mq, int prio) { (void)mq; (void)prio; }
int osPiStartDma(OSPiChannel ch, OSPiMsg *msg, int sync)
{
/* TODO(Phase 1): service a read from romdata (cart -> host memory). */
(void)ch; (void)msg; (void)sync;
return 1;
}
int osPiRawStartDma(int direction, void *src, void *dst, u32 size)
{
/* TODO(Phase 1): copy between ROM image and host memory. */
if (src && dst && size) memcpy(dst, src, size);
(void)direction;
return 1;
}
u32 osPiGetStatus(void) { return 0; } /* not busy */
/*
* PI device register read (normally src/libultra/io, EXCLUDED). token.c uses
* it to read the cartridge token string. On the PC there is no cartridge
* token register; return 0 (no token) so the game proceeds with an empty
* token. TODO(Phase 1): optionally back with a value from the ROM/config.
*/
s32 osPiReadIo(u32 devAddr, u32 *data)
{
(void)devAddr;
if (data) *data = 0;
return 0;
}
/* ------------------------------------------------------------------------ */
/* SI (controller / eeprom / mempak) — map onto input + file-backed saves. */
/* ------------------------------------------------------------------------ */
s32 osContInit(OSMesgQueue *mesgq, u8 *bitpattern, OSContStatus *data)
{
/* TODO(Phase 3): report inputGetNumControllers() as connected. */
(void)mesgq; (void)bitpattern; (void)data;
return 0;
}
s32 osContStartReadData(OSMesgQueue *mesgq)
{
/* TODO(Phase 3): snapshot the current SDL input into the controller buf. */
(void)mesgq;
return 0;
}
s32 osContStartQuery(OSMesgQueue *mq)
{
/* TODO(Phase 3) */
(void)mq;
return 0;
}
void osContGetQuery(OSContStatus *status)
{
/* TODO(Phase 3): fill status for each connected controller. */
(void)status;
}
void osContGetReadData(OSContPad *pad)
{
/* TODO(Phase 3): fill pad from the current SDL input snapshot. */
(void)pad;
}
/* EEPROM: back with a file in the data dir (see B2 — saves are EEPROM-based).
* TODO(Phase 4): see docs/PCPortResearch.md §7. */
int osEepromProbe(OSMesgQueue *mq) { (void)mq; return 0; }
int osEepromRead(OSMesgQueue *mq, u16 addr, void *buf, u16 size)
{ (void)mq; (void)addr; (void)buf; (void)size; return 0; }
int osEepromWrite(OSMesgQueue *mq, u16 addr, void *buf, u16 size)
{ (void)mq; (void)addr; (void)buf; (void)size; return 0; }
int osEepromLongRead(OSMesgQueue *mq, u16 addr, void *buf, u16 size)
{ (void)mq; (void)addr; (void)buf; (void)size; return 0; }
int osEepromLongWrite(OSMesgQueue *mq, u16 addr, void *buf, u16 size)
{ (void)mq; (void)addr; (void)buf; (void)size; return 0; }
/* Memory Pak (PFS) + Rumble Pak (motor): accessory detection only (see B2).
* The PC has no Memory Pak; report "not plugged in" so the game proceeds
* without rumble/mempak. TODO(Phase 4): optionally back PFS with a file. */
s32 osPfsInit(OSMesgQueue *queue, OSPfs *pfs, int channel)
{ (void)queue; (void)pfs; (void)channel; return PFS_ERR_NOPACK; }
s32 osPfsIsPlug(OSMesgQueue *queue, u8 *pattern)
{ (void)queue; if (pattern) *pattern = 0; return 0; }
s32 osMotorInit(OSMesgQueue *mq, OSPfs *pfs, int channel)
{ (void)mq; (void)pfs; (void)channel; return -1; }
s32 osMotorStart(OSPfs *pfs) { (void)pfs; return -1; }
s32 osMotorStop(OSPfs *pfs) { (void)pfs; return -1; }
/* ------------------------------------------------------------------------ */
/* SP (RSP) — the port scheduler (gesched.c) drives the software RSP, so */
/* these are mostly no-ops / bookkeeping. See gesched.c. */
/* ------------------------------------------------------------------------ */
void osSpTaskLoad(OSTask *t) { (void)t; /* handled by gesched.c */ }
void osSpTaskStartGo(OSTask *t) { (void)t; /* handled by gesched.c */ }
void osSpTaskYield(void) { /* handled by gesched.c */ }
void osSpFlush(void) { /* no-op */ }
void osSpSetFifo(void *fifo, int size, int flags) { (void)fifo; (void)size; (void)flags; }
void *osSpGetFifo(void) { return NULL; }
int osSpTaskDone(void) { return 1; }
/* ------------------------------------------------------------------------ */
/* RDP — bypassed (the software RSP emits GL directly). No-ops. */
/* ------------------------------------------------------------------------ */
void osDpSetStatus(u32 set, u32 clear) { (void)set; (void)clear; }
u32 osDpGetStatus(void) { return 0; }
int osDpSetNextBuffer(void *buf, int size) { (void)buf; (void)size; return 1; }
/* ------------------------------------------------------------------------ */
/* Cache — no-op on the host (no split I/D cache to manage). */
/* ------------------------------------------------------------------------ */
void osWritebackDCache(void *addr, int size) { (void)addr; (void)size; }
void osWritebackDCacheAll(void) { }
void osInvalICache(void *addr, int size) { (void)addr; (void)size; }
void osInvalDCache(void *addr, int size) { (void)addr; (void)size; }
void *osVirtualToPhysical(void *va) { return va; }
void *osPhysicalToVirtual(unsigned int pa) { return (void *)pa; }
/* ------------------------------------------------------------------------ */
/* Misc */
/* ------------------------------------------------------------------------ */
void osInitialize(void)
{
/* The game calls this early. On the PC there is little to do; the port
* has already set up video/audio/input/rom in main(). */
}
void osExit(int code) { sysExit(code); }
u32 osGetFpcCsr(void) { return 0; }
void osSetFpcCsr(u32 csr) { (void)csr; }
/*
* FPU CSR accessors used by init.c (the __os* variants are the raw libultra
* names; init() saves/restores the FPU control/status register around the
* decompress step). Unused on the PC but must link.
*/
u32 __osGetFpcCsr(void) { return 0; }
void __osSetFpcCsr(u32 csr) { (void)csr; }
/* TLB unmap (libultra/os/unmaptlb.s). The PC has its own MMU; no-op. */
void osUnmapTLB(int index) { (void)index; }
/* ------------------------------------------------------------------------ */
/* Timers / interrupts */
/* ------------------------------------------------------------------------ */
/* Timers: the port runs a single-threaded loop; timers are serviced by the
* scheduler. TODO(Phase 1): wire to the port's frame clock. */
int osSetTimer(OSTimer *timer, OSTime start, OSTime period,
OSMesgQueue *mq, OSMesg msg)
{ (void)timer; (void)start; (void)period; (void)mq; (void)msg; return 0; }
int osStopTimer(OSTimer *timer) { (void)timer; return 0; }
/* Interrupt mask: no hardware interrupts on the PC. */
OSIntMask osSetIntMask(OSIntMask mask) { (void)mask; return 0; }
/* ------------------------------------------------------------------------ */
/* PI / VI managers referenced by init.c (pi.c / vi.c are not compiled) */
/* ------------------------------------------------------------------------ */
void piCreateManager(OSMesgQueue *mq, int prio)
{
/* ROM access is handled by romdata.c; no PI manager needed on the PC. */
(void)mq; (void)prio;
}
void viDebugRemoved(void)
{
/* N64 debug VI hook; no-op on the PC. */
}
void viInit(void)
{
/* VI init (normally src/vi.c, EXCLUDED). boss.c calls it during startup.
* TODO(Phase 2): initialise the port video layer here. */
}
/* VI debug message queue (normally src/vi.c, EXCLUDED). fr.c references it. */
OSMesgQueue vi_c_debug_MQ;
+81
View File
@@ -0,0 +1,81 @@
/*
* PC entry point for the GoldenEye 007 port.
*
* Replaces the N64 boot path (boot.s -> init() -> mainproc() -> bossEntry()).
* On the PC we:
* 1. set up system / config / fs / rom
* 2. load the ROM and map its segments
* 3. init video (SDL2 + GL), audio, input
* 4. hand control to the game's mainproc() / bossEntry()
*
* Modelled on the PD port's port/src/main.c.
*
* STATUS: scaffolding stub — fill in during Phase 1.
*/
#include <stdlib.h>
#include <stdio.h>
#include <PR/ultratypes.h>
#include "platform.h"
#include "system.h"
#include "config.h"
#include "fs.h"
#include "romdata.h"
#include "video.h"
#include "audio.h"
#include "input.h"
#include "mixer.h"
/* Defined in the game (src/init.c). The port calls into the real game entry. */
extern void mainproc(void *args);
int main(int argc, char **argv)
{
(void)argc;
(void)argv;
sysLogPrintf(LOG_INFO, "GoldenEye 007 PC port starting "
"(%s, %s)", GE007_ROMID, GE007_VERSION_HASH);
/* 1. Platform + config + filesystem. */
configLoad();
/* 2. Load the ROM and map segments. */
if (romdataInit() != 0) {
sysLogPrintf(LOG_ERROR, "Failed to load ROM (expected a .z64 in the "
"data/ dir, see README)");
return 1;
}
/* 3. Video / audio / input. */
if (videoInit() != 0) {
sysLogPrintf(LOG_ERROR, "videoInit failed");
return 1;
}
audioInit();
mixerInit();
inputInit();
/* 4. Hand control to the game.
*
* mainproc() is defined in src/init.c, which IS compiled for the PC (see
* A3 in docs/PCPortResearch.md). init.c's N64-only init() is compiled but
* NOT called — its hardware deps (segment starts, decompress, TLB, FPU
* CSR) are stubbed in port/src/n64stubs.c, and the libultra OS calls are
* shimmed in port/src/libultra.c. mainproc() itself just sets up the
* threads and calls schedulerInitThread() + bossEntry().
*/
mainproc(NULL);
/* mainproc() normally does not return; clean up if it does. */
inputDestroy();
mixerDestroy();
audioDestroy();
videoDestroy();
romdataDestroy();
configSave();
return 0;
}
+31
View File
@@ -0,0 +1,31 @@
/*
* Audio mixing.
*
* On the N64 the RSP audio ucode does final mix / sample handling before the
* AI DMA. With the RSP emulated in software, this runs on the CPU.
*
* Modelled on the PD port's port/src/mixer.c (~720 lines).
*
* STATUS: scaffolding stub — implement during Phase 3. The exact interface
* depends on how GE's libaudio output is wired (see src/audi.c).
*/
#include "platform.h"
#include "mixer.h"
#include "audio.h"
void mixerInit(void)
{
/* TODO(Phase 3) */
}
void mixerDestroy(void)
{
/* TODO(Phase 3) */
}
void mixerProcess(const s16 *in, u32 frames)
{
/* TODO(Phase 3): process + queue via audioSetNextBuffer(). */
(void)in; (void)frames;
}
+83
View File
@@ -0,0 +1,83 @@
/*
* N64-boot-specific symbol stubs.
*
* We compile src/init.c (for mainproc() — see A3 in docs/PCPortResearch.md),
* but its N64-only init() is never called on the PC. The compiler still needs
* the symbols init() references to resolve at link time, so this file provides
* inert stubs for the ones that are NOT part of the libultra OS API (those
* live in libultra.c).
*
* These are the boot/segment/decompress/TLB symbols, normally provided by the
* linker script, boot.s, the inflate code, and the tlb_*.s assembly, plus the
* rmon (remote monitor) host-communication functions from src/rmon.c.
*
* STATUS: scaffolding — the stubs let the compiled set link. The init() ones
* are never executed on the PC (init() is not called; mainproc() is); the
* rmon ones are inert (rmon is the N64 host debugger).
*/
#include <stdio.h>
#include <stdarg.h>
#include <PR/ultratypes.h>
/* --- Segment start/end getters (normally linker-script symbols) --------- */
/* On the PC the ROM is loaded by romdata.c; these are unused. Return 0. */
u32 get_csegmentSegmentStart(void) { return 0; }
u32 get_cdataSegmentRomStart(void) { return 0; }
u32 get_cdataSegmentRomEnd(void) { return 0; }
u32 get_inflateSegmentRomStart(void) { return 0; }
u32 get_inflateSegmentRomEnd(void) { return 0; }
/*
* Linker segment boundary pointers (normally linker-script symbols, declared
* `extern u32 *` in bondgame.h). init() takes their ADDRESSES for a size calc
* and a PI DMA; it is compiled but never called on the PC. Dummy pointers so
* the symbols exist and link.
*/
u32 *_codeSegmentRomStart = NULL;
u32 *_inflateSegmentRomStart = NULL;
u32 *_alt_startSegmentRomStart = NULL;
u32 *_alt_startSegmentStart = NULL;
/* --- Decompressor (src/inflate, not built for PC) ----------------------- */
/* init() calls this to unpack the data segment. Unused on the PC. */
u32 jump_decompressfile(u32 source, u32 target, u32 buffer)
{
(void)source; (void)target; (void)buffer;
return 0;
}
/* --- TLB (src/tlb_*.s, not built for PC) -------------------------------- */
/* The PC has its own MMU; the N64 TLB miss handler is irrelevant. */
void initTLBPrepareContext(void) { /* no-op */ }
/* Address of the N64 TLB-miss handler. init() copies it to K0BASE; unused. */
void resolve_TLBaddress_for_InvalidHit(void) { /* no-op */ }
/* --- Remote monitor (src/rmon.c, not built for PC) ---------------------- */
/* rmon is the N64 remote monitor (host debugger); it has no PC equivalent. */
/* init.c's rmonCreateThread() starts a thread running rmonMain; stub it as a */
/* no-op so the thread (if ever started) does nothing. */
void rmonMain(void) { /* no-op: rmon (remote monitor) is N64-only */ }
/* rmon host I/O + token/status, referenced by game code (indy_commands.c,
* indy_comms.c, boss.c, token.c). All inert on the PC. */
void osReadHost(void *buffer, u32 size) { (void)buffer; (void)size; }
void osWriteHost(void *buffer, u32 size) { (void)buffer; (void)size; }
s32 rmonGetToken(void) { return 0; }
s32 rmonStatus(void) { return 0; }
/*
* osSyncPrintf is the game's debug/printf channel (also used by the assert()
* macros). Route it to stderr so assert failures and debug output are visible
* during porting. TODO(Phase 1): integrate with the port's sysLogPrintf.
*/
void osSyncPrintf(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
+84
View File
@@ -0,0 +1,84 @@
/*
* PRNG — ported verbatim from src/random.s (MIPS assembly, not compiled for
* the PC). This is a REAL implementation, not a stub: the game uses it for
* genuine gameplay logic, so a no-op would silently corrupt behaviour.
*
* - randomGetNextFrom() feeds the CRC in src/game/crc.c
* - g_randomSeed is persisted in replay state (src/game/ramromreplay.c)
* - randomGetNext() drives RANDOMFRAC()/RANDOMGETNEXT_F32() (bondconstants.h)
*
* The assembly operates on the low 32 bits of the seed (the high 32 bits are
* never read), but the seed is stored as a full u64. We mirror each
* instruction with explicit masks to guarantee bit-exactness. The initial
* seed is the two .words in random.s: 0xAB8D9F77 (hi) / 0x81280783 (lo).
*/
#include <ultra64.h>
#include <random.h>
u64 g_randomSeed = 0xAB8D9F7781280783ULL;
/*
* MIPS64 shift helpers, matching the .s instructions exactly:
* dsll32 rd,rs,n -> (rs << n) & 0xFFFFFFFF (low 32 bits kept, hi zeroed)
* dsrl32 rd,rs,n -> (rs >> n) & 0xFFFFFFFF (low 32 bits kept, hi zeroed)
* Plain `<<`/`>>` on u64 match dsll/dsrl (full 64-bit shifts).
*/
static inline u64 dsll32(u64 x, int n) { return (x << n) & 0xFFFFFFFFULL; }
static inline u64 dsrl32(u64 x, int n) { return (x >> n) & 0xFFFFFFFFULL; }
/*
* Advance the global seed and return the new low 32 bits.
* Mirrors randomGetNext in random.s line-by-line.
*/
u32 randomGetNext(void)
{
u64 a0 = g_randomSeed;
u64 a1, a2;
a2 = dsll32(a0, 0x1f); /* dsll32 $a2, $a0, 0x1f */
a1 = a0 << 0x1f; /* dsll $a1, $a0, 0x1f */
a2 = a2 >> 0x1f; /* dsrl $a2, $a2, 0x1f */
a1 = dsrl32(a1, 0); /* dsrl32 $a1, $a1, 0 */
a0 = dsll32(a0, 0xc); /* dsll32 $a0, $a0, 0xc */
a2 = a2 | a1; /* or $a2, $a2, $a1 */
a0 = dsrl32(a0, 0); /* dsrl32 $a0, $a0, 0 */
a2 = a2 ^ a0; /* xor $a2, $a2, $a0 */
a0 = a2 >> 0x14; /* dsrl $a0, $a2, 0x14 */
a0 = a0 & 0xfff; /* andi $a0, $a0, 0xfff*/
a0 = a0 ^ a2; /* xor $a0, $a0, $a2 */
g_randomSeed = a0; /* sd $a0, g_randomSeed */
return (u32)a0; /* v0 = a0 & 0xFFFFFFFF */
}
/* Set the seed (the .s adds 1 before storing). */
void randomSetSeed(u32 seed)
{
g_randomSeed = (u64)seed + 1; /* daddiu $a0,$a0,1; sd $a0, g_randomSeed */
}
/*
* Same transform as randomGetNext, but applied to *seed (a caller-owned u64)
* instead of the global. Mirrors randomGetNextFrom in random.s.
*/
u32 randomGetNextFrom(u64 *seed)
{
u64 a3 = *seed; /* ld $a3, ($a0) */
u64 a1, a2;
a2 = dsll32(a3, 0x1f); /* dsll32 $a2, $a3, 0x1f */
a1 = a3 << 0x1f; /* dsll $a1, $a3, 0x1f */
a2 = a2 >> 0x1f; /* dsrl $a2, $a2, 0x1f */
a1 = dsrl32(a1, 0); /* dsrl32 $a1, $a1, 0 */
a3 = dsll32(a3, 0xc); /* dsll32 $a3, $a3, 0xc */
a2 = a2 | a1; /* or $a2, $a2, $a1 */
a3 = dsrl32(a3, 0); /* dsrl32 $a3, $a3, 0 */
a2 = a2 ^ a3; /* xor $a2, $a2, $a3 */
a3 = a2 >> 0x14; /* dsrl $a3, $a2, 0x14 */
a3 = a3 & 0xfff; /* andi $a3, $a3, 0xfff*/
a3 = a3 ^ a2; /* xor $a3, $a3, $a2 */
*seed = a3; /* sd $a3, ($a0) */
return (u32)a3; /* v0 = a3 & 0xFFFFFFFF */
}
+87
View File
@@ -0,0 +1,87 @@
/*
* ROM loading: reads the .z64 ROM from the data dir and exposes its segments.
*
* The N64 build links assets in from the ROM at fixed addresses. The PC port
* loads the ROM file into a heap image and maps the game's data segments into
* it, then patches the segment table so asset references resolve.
*
* Modelled on the PD port's port/src/romdata.c (~605 lines).
*
* STATUS: scaffolding stub — implement during Phase 1.
*
* NOTE: GE's ROM layout (segments, asset offsets) differs from PD's. The
* segment table and asset map must be derived from this decomp's
* ge007.ld / imagelist.u.csv / assets/ layout. See docs/PCPortResearch.md.
*/
#include <stdlib.h>
#include <string.h>
#include "platform.h"
#include "system.h"
#include "fs.h"
#include "romdata.h"
static u8 *rom = NULL;
static u32 romSize = 0;
/* Expected ROM file names per region (place one in the data/ dir). */
static const char *romFileName(void)
{
#if GE007_IS_PAL
return "ge007.pal-final.z64";
#else
return "ge007.ntsc-final.z64";
#endif
}
int romdataInit(void)
{
const char *path = sysResolvePath("$S/" romFileName());
/* TODO(Phase 1):
* - open + read the whole ROM into `rom`
* - validate the N64 magic ("Nintendo" @ 0x100) + region
* - build the segment map (code/data/assets) from the decomp's layout
* - patch the game's segment table so IMAGESEG / asset refs resolve
*/
sysLogPrintf(LOG_INFO, "romdataInit: loading %s (TODO Phase 1)", path);
FSFile *f = fsOpen(path, "rb");
if (!f) {
sysLogPrintf(LOG_ERROR, "romdataInit: cannot open %s", path);
return -1;
}
romSize = (u32)fsSize(f);
rom = (u8 *)malloc(romSize);
if (!rom || fsRead(f, rom, romSize) != (int32_t)romSize) {
sysLogPrintf(LOG_ERROR, "romdataInit: failed to read ROM");
fsClose(f);
return -1;
}
fsClose(f);
if (romSize < 0x104 || memcmp(rom + 0x100, "Nintendo", 8) != 0) {
sysLogPrintf(LOG_ERROR, "romdataInit: bad N64 ROM magic");
return -1;
}
return 0;
}
void romdataDestroy(void)
{
free(rom);
rom = NULL;
romSize = 0;
}
const u8 *romdataGetRom(void) { return rom; }
u32 romdataGetRomSize(void) { return romSize; }
const void *romdataMapVa(u32 va)
{
/* TODO(Phase 1): map an N64 RDRAM VA (0xA0000000+) into the image. */
(void)va;
return NULL;
}
+81
View File
@@ -0,0 +1,81 @@
/*
* Platform primitives: time, logging, paths, exit.
*
* Modelled on the PD port's port/src/system.c (~315 lines).
*
* STATUS: scaffolding stub — implement during Phase 1.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include "platform.h"
#include "system.h"
/* --- Time --------------------------------------------------------------- */
uint64_t sysGetMicroseconds(void)
{
/* TODO(Phase 1): use a monotonic clock.
* - Windows: QueryPerformanceCounter
* - POSIX: clock_gettime(CLOCK_MONOTONIC)
*/
return 0;
}
int64_t sysGetTime(void)
{
/* TODO(Phase 1): wall clock (time(NULL)). */
return 0;
}
void sysSleep(uint32_t micros)
{
/* TODO(Phase 1): Sleep() / usleep(). */
(void)micros;
}
/* --- Logging ------------------------------------------------------------ */
void sysLogPrintf(enum LogLevel level, const char *fmt, ...)
{
static const char *tags[] = { "ERROR", "WARN ", "INFO ", "DEBUG" };
va_list ap;
fprintf(stderr, "[%s] ", tags[level]);
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
}
/* --- Paths -------------------------------------------------------------- */
static char exeDir[512] = ".";
const char *sysGetExeDir(void)
{
/* TODO(Phase 1): resolve the executable's directory.
* - Windows: GetModuleFileName
* - Linux: readlink /proc/self/exe
* - macOS: _NSGetExecutablePath
*/
return exeDir;
}
const char *sysResolvePath(const char *path)
{
static char out[1024];
/* TODO(Phase 1): expand "$S/" (data dir) and "$E/" (exe dir) prefixes. */
strncpy(out, path, sizeof(out) - 1);
out[sizeof(out) - 1] = 0;
return out;
}
/* --- Lifecycle ---------------------------------------------------------- */
void sysExit(int code)
{
exit(code);
}
+48
View File
@@ -0,0 +1,48 @@
/*
* RSP/ASP/GSP microcode segment markers.
*
* On the N64 these are linker-defined addresses of the RSP/ASP/GSP microcode
* embedded in the ROM (see src/rspboot.s, src/gspboot.s, src/aspboot.s — the
* .s files are not compiled for the PC). The game references them to build
* SP tasks:
*
* src/audi.c: ucode_boot = (u64*)rspbootTextStart
* ucode = (u64*)aspMainTextStart
* ucode_data = (u64*)aspMainDataStart
* src/game/rsp.c: ucode_boot_size = (s32)gsp3DTextStart - (s32)rspbootTextStart
*
* The game declares them as `extern long long int <name>[];`.
*
* STATUS: dummy definitions so the link succeeds.
*
* - GRAPHICS (Phase 2): fast3d interprets the GBI display list directly and
* never executes the gsp3D microcode, so gsp3DTextStart/gsp3DDataStart are
* unused metadata. The ucode_boot_size differences compute to garbage but
* are never read on the graphics path. Dummies are fine permanently here.
*
* - AUDIO (Phase 3): this is the one place it could matter. If the port
* emulates the ASP by actually RUNNING the aspMain microcode, then
* aspMainTextStart/aspMainDataStart must point at the real microcode bytes
* in the loaded ROM (follow the PD port's ld/pd.ld RSP_TEXT_SEGMENT model).
* If audio is handled entirely on the CPU (bypassing the ASP), these
* dummies are fine permanently. DECISION NEEDED in Phase 3.
*
* TODO(Phase 2/3): wire these to the real microcode in the ROM image if the
* corresponding path executes microcode.
*/
/* RSP boot microcode (src/rspboot.s). */
long long int rspbootTextStart[1] = {0};
long long int rspbootTextEnd[1] = {0};
/* GSP 3D microcode (src/gspboot.s). Unused by fast3d (see note above). */
long long int gsp3DTextStart[1] = {0};
long long int gsp3DDataStart[1] = {0};
/*
* ASP audio microcode (src/aspboot.s).
* TODO(Phase 3): if the port runs the ASP microcode, point these at the real
* microcode bytes in the ROM (see file header note).
*/
long long int aspMainTextStart[1] = {0};
long long int aspMainDataStart[1] = {0};
+55
View File
@@ -0,0 +1,55 @@
/*
* Small shared helpers for the port layer.
*
* Modelled on the PD port's port/src/utils.c (~195 lines).
*
* STATUS: scaffolding stub — implement during Phase 1.
*/
#include <stdint.h>
#include <string.h>
#include "utils.h"
uint16_t readU16BE(const void *p)
{
const uint8_t *b = (const uint8_t *)p;
return (uint16_t)((b[0] << 8) | b[1]);
}
uint32_t readU32BE(const void *p)
{
const uint8_t *b = (const uint8_t *)p;
return ((uint32_t)b[0] << 24) | ((uint32_t)b[1] << 16) |
((uint32_t)b[2] << 8) | (uint32_t)b[3];
}
void writeU16BE(void *p, uint16_t v)
{
uint8_t *b = (uint8_t *)p;
b[0] = (uint8_t)(v >> 8);
b[1] = (uint8_t)(v & 0xff);
}
void writeU32BE(void *p, uint32_t v)
{
uint8_t *b = (uint8_t *)p;
b[0] = (uint8_t)(v >> 24);
b[1] = (uint8_t)(v >> 16);
b[2] = (uint8_t)(v >> 8);
b[3] = (uint8_t)(v & 0xff);
}
int clampi(int v, int lo, int hi)
{
if (v < lo) return lo;
if (v > hi) return hi;
return v;
}
float clampf(float v, float lo, float hi)
{
if (v < lo) return lo;
if (v > hi) return hi;
return v;
}
+87
View File
@@ -0,0 +1,87 @@
/*
* Video: SDL2 window + OpenGL context + frame pacing.
*
* Owns the GL context that the software RSP (fast3d) renders into. Drives the
* frame boundary hooks called by the port scheduler (gesched.c).
*
* Modelled on the PD port's port/src/video.c (~590 lines).
*
* STATUS: scaffolding stub — implement during Phase 1 (window + clear) and
* Phase 2 (frame pacing, options).
*/
#include <SDL.h>
#include "platform.h"
#include "system.h"
#include "config.h"
#include "video.h"
/* fast3d entry points (see port/fast3d/gfx_api.h). */
extern void gfx_init(const void *settings);
extern void gfx_destroy(void);
extern void gfx_start_frame(void);
extern void gfx_run(void *commands);
extern void gfx_end_frame(void);
static int initDone = 0;
static int vidVsync = 1;
static int vidFpsLimit = 0;
static int vidFullscreen = 0;
static int vidMaximize = 0;
int videoInit(void)
{
/* TODO(Phase 1):
* - SDL_Init(VIDEO)
* - create window (title "GoldenEye 007", default 640x480 or config)
* - create GL 3.0+ context
* - build the GfxInitSettings (window-manager API = SDL2, rendering API
* = OpenGL) and call gfx_init()
* - apply config: vsync, fps limit, fullscreen, maximize
*/
sysLogPrintf(LOG_INFO, "videoInit: TODO (Phase 1)");
initDone = 1;
return 0;
}
void videoDestroy(void)
{
if (initDone) {
gfx_destroy();
initDone = 0;
}
}
void videoStartFrame(void)
{
if (initDone) gfx_start_frame();
}
void videoSubmitCommands(Gfx *cmds)
{
if (initDone) gfx_run(cmds);
}
void videoEndFrame(void)
{
if (!initDone) return;
gfx_end_frame();
/* TODO(Phase 2): FPS accounting + framerate limiting. */
}
void videoSetVsync(int enabled) { vidVsync = enabled; /* TODO: SDL_GL_SetSwapInterval */ }
void videoSetFramerateLimit(int fps) { vidFpsLimit = fps; /* TODO */ }
int videoGetFullscreen(void) { return vidFullscreen; }
void videoSetFullscreen(int enabled) { vidFullscreen = enabled; /* TODO */ }
int videoGetMaximize(void) { return vidMaximize; }
void videoSetMaximize(int enabled) { vidMaximize = enabled; /* TODO */ }
float videoGetFPS(void) { return 0.0f; /* TODO */ }
PD_CONSTRUCTOR static void videoConfigInit(void)
{
configRegisterInt("Video.Vsync", &vidVsync, 0, 1);
configRegisterInt("Video.FramerateLimit", &vidFpsLimit, 0, 240);
configRegisterInt("Video.Fullscreen", &vidFullscreen, 0, 1);
configRegisterInt("Video.Maximize", &vidMaximize, 0, 1);
}