From cd6cc158b68fd9a6c165783b653a56dfdb413a01 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Mon, 17 Aug 2026 19:17:06 -0400 Subject: [PATCH] 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. --- .gitignore | 7 +- CMakeLists.txt | 298 +++++++++++++++++++++ PORTING.md | 67 +++++ build-pc.sh | 35 +++ cmake/FindSDL2.cmake | 172 ++++++++++++ cmake/TargetArch.cmake | 28 ++ docs/PCPortResearch.md | 485 ++++++++++++++++++++++++++++++++++ port/README.md | 70 +++++ port/fast3d/README.md | 51 ++++ port/fast3d/gfx_api.h | 66 +++++ port/include/audio.h | 36 +++ port/include/config.h | 32 +++ port/include/fs.h | 46 ++++ port/include/input.h | 35 +++ port/include/mixer.h | 34 +++ port/include/platform.h | 51 ++++ port/include/romdata.h | 42 +++ port/include/system.h | 49 ++++ port/include/utils.h | 32 +++ port/include/versioninfo.h.in | 11 + port/include/video.h | 46 ++++ port/src/audio.c | 70 +++++ port/src/config.c | 58 ++++ port/src/crash.c | 26 ++ port/src/fs.c | 76 ++++++ port/src/gesched.c | 110 ++++++++ port/src/input.c | 48 ++++ port/src/libultra.c | 375 ++++++++++++++++++++++++++ port/src/main.c | 81 ++++++ port/src/mixer.c | 31 +++ port/src/n64stubs.c | 83 ++++++ port/src/random.c | 84 ++++++ port/src/romdata.c | 87 ++++++ port/src/system.c | 81 ++++++ port/src/ucode.c | 48 ++++ port/src/utils.c | 55 ++++ port/src/video.c | 87 ++++++ 37 files changed, 3092 insertions(+), 1 deletion(-) create mode 100644 CMakeLists.txt create mode 100644 PORTING.md create mode 100644 build-pc.sh create mode 100644 cmake/FindSDL2.cmake create mode 100644 cmake/TargetArch.cmake create mode 100644 docs/PCPortResearch.md create mode 100644 port/README.md create mode 100644 port/fast3d/README.md create mode 100644 port/fast3d/gfx_api.h create mode 100644 port/include/audio.h create mode 100644 port/include/config.h create mode 100644 port/include/fs.h create mode 100644 port/include/input.h create mode 100644 port/include/mixer.h create mode 100644 port/include/platform.h create mode 100644 port/include/romdata.h create mode 100644 port/include/system.h create mode 100644 port/include/utils.h create mode 100644 port/include/versioninfo.h.in create mode 100644 port/include/video.h create mode 100644 port/src/audio.c create mode 100644 port/src/config.c create mode 100644 port/src/crash.c create mode 100644 port/src/fs.c create mode 100644 port/src/gesched.c create mode 100644 port/src/input.c create mode 100644 port/src/libultra.c create mode 100644 port/src/main.c create mode 100644 port/src/mixer.c create mode 100644 port/src/n64stubs.c create mode 100644 port/src/random.c create mode 100644 port/src/romdata.c create mode 100644 port/src/system.c create mode 100644 port/src/ucode.c create mode 100644 port/src/utils.c create mode 100644 port/src/video.c diff --git a/.gitignore b/.gitignore index da3e06f3..9f28b4b0 100644 --- a/.gitignore +++ b/.gitignore @@ -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 @@ -111,4 +116,4 @@ assets/obseg/em/data/under.seg scripts/local/ scripts/make/local/ -scripts/__pycache__/generate_prop_model_c.cpython-310.pyc +scripts/__pycache__/generate_prop_model_c.cpython-310.pyc diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..cc2e70c3 --- /dev/null +++ b/CMakeLists.txt @@ -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($<$:-Wno-pointer-sign>) +add_compile_options($<$:-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}") diff --git a/PORTING.md b/PORTING.md new file mode 100644 index 00000000..f22ec01f --- /dev/null +++ b/PORTING.md @@ -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). diff --git a/build-pc.sh b/build-pc.sh new file mode 100644 index 00000000..432b0a0c --- /dev/null +++ b/build-pc.sh @@ -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." diff --git a/cmake/FindSDL2.cmake b/cmake/FindSDL2.cmake new file mode 100644 index 00000000..02c6bba0 --- /dev/null +++ b/cmake/FindSDL2.cmake @@ -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 . 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) diff --git a/cmake/TargetArch.cmake b/cmake/TargetArch.cmake new file mode 100644 index 00000000..b2558475 --- /dev/null +++ b/cmake/TargetArch.cmake @@ -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() diff --git a/docs/PCPortResearch.md b/docs/PCPortResearch.md new file mode 100644 index 00000000..567e0843 --- /dev/null +++ b/docs/PCPortResearch.md @@ -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 []`). + **Resolved:** `port/src/ucode.c` defines all six as `long long int [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. diff --git a/port/README.md b/port/README.md new file mode 100644 index 00000000..e4f3ee90 --- /dev/null +++ b/port/README.md @@ -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. diff --git a/port/fast3d/README.md b/port/fast3d/README.md new file mode 100644 index 00000000..17c31794 --- /dev/null +++ b/port/fast3d/README.md @@ -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. diff --git a/port/fast3d/gfx_api.h b/port/fast3d/gfx_api.h new file mode 100644 index 00000000..4e52a4c7 --- /dev/null +++ b/port/fast3d/gfx_api.h @@ -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 +#include +#endif + +#include + +#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 */ diff --git a/port/include/audio.h b/port/include/audio.h new file mode 100644 index 00000000..478e86d6 --- /dev/null +++ b/port/include/audio.h @@ -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 + +#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 */ diff --git a/port/include/config.h b/port/include/config.h new file mode 100644 index 00000000..ec0633e1 --- /dev/null +++ b/port/include/config.h @@ -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 + +#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 */ diff --git a/port/include/fs.h b/port/include/fs.h new file mode 100644 index 00000000..6cf3a0fa --- /dev/null +++ b/port/include/fs.h @@ -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 +#include + +#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 */ diff --git a/port/include/input.h b/port/include/input.h new file mode 100644 index 00000000..1e74180d --- /dev/null +++ b/port/include/input.h @@ -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 + +#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 */ diff --git a/port/include/mixer.h b/port/include/mixer.h new file mode 100644 index 00000000..186da2b5 --- /dev/null +++ b/port/include/mixer.h @@ -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 + +#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 */ diff --git a/port/include/platform.h b/port/include/platform.h new file mode 100644 index 00000000..3fc2e19a --- /dev/null +++ b/port/include/platform.h @@ -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 +#include + +#if defined(_WIN32) || defined(_WIN64) + #define PLATFORM_WINDOWS 1 +#elif defined(__APPLE__) + #include + #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 */ diff --git a/port/include/romdata.h b/port/include/romdata.h new file mode 100644 index 00000000..9822a8fb --- /dev/null +++ b/port/include/romdata.h @@ -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 + +#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 */ diff --git a/port/include/system.h b/port/include/system.h new file mode 100644 index 00000000..41c5ce33 --- /dev/null +++ b/port/include/system.h @@ -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 +#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 */ diff --git a/port/include/utils.h b/port/include/utils.h new file mode 100644 index 00000000..11d192cd --- /dev/null +++ b/port/include/utils.h @@ -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 +#include + +#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 */ diff --git a/port/include/versioninfo.h.in b/port/include/versioninfo.h.in new file mode 100644 index 00000000..ea7f379b --- /dev/null +++ b/port/include/versioninfo.h.in @@ -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 */ diff --git a/port/include/video.h b/port/include/video.h new file mode 100644 index 00000000..fbbca45d --- /dev/null +++ b/port/include/video.h @@ -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 +#include + +#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 */ diff --git a/port/src/audio.c b/port/src/audio.c new file mode 100644 index 00000000..75f9c325 --- /dev/null +++ b/port/src/audio.c @@ -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 + +#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); +} diff --git a/port/src/config.c b/port/src/config.c new file mode 100644 index 00000000..38192b22 --- /dev/null +++ b/port/src/config.c @@ -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 +#include +#include + +#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". */ +} diff --git a/port/src/crash.c b/port/src/crash.c new file mode 100644 index 00000000..c29377ee --- /dev/null +++ b/port/src/crash.c @@ -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 +#include + +#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)"); +} diff --git a/port/src/fs.c b/port/src/fs.c new file mode 100644 index 00000000..ddcdd5e9 --- /dev/null +++ b/port/src/fs.c @@ -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 +#include +#include + +#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; +} diff --git a/port/src/gesched.c b/port/src/gesched.c new file mode 100644 index 00000000..12bd3a73 --- /dev/null +++ b/port/src/gesched.c @@ -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 +#include +#include + +#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(); +} diff --git a/port/src/input.c b/port/src/input.c new file mode 100644 index 00000000..bd378e0f --- /dev/null +++ b/port/src/input.c @@ -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 + +#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; +} diff --git a/port/src/libultra.c b/port/src/libultra.c new file mode 100644 index 00000000..73bb7604 --- /dev/null +++ b/port/src/libultra.c @@ -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 +#include +#include + +#include +#include +#include +#include + +#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; diff --git a/port/src/main.c b/port/src/main.c new file mode 100644 index 00000000..4f2bf0c2 --- /dev/null +++ b/port/src/main.c @@ -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 +#include + +#include + +#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; +} diff --git a/port/src/mixer.c b/port/src/mixer.c new file mode 100644 index 00000000..663cfc8f --- /dev/null +++ b/port/src/mixer.c @@ -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; +} diff --git a/port/src/n64stubs.c b/port/src/n64stubs.c new file mode 100644 index 00000000..f3cf4206 --- /dev/null +++ b/port/src/n64stubs.c @@ -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 +#include + +#include + +/* --- 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); +} diff --git a/port/src/random.c b/port/src/random.c new file mode 100644 index 00000000..32c42d20 --- /dev/null +++ b/port/src/random.c @@ -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 +#include + +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 */ +} diff --git a/port/src/romdata.c b/port/src/romdata.c new file mode 100644 index 00000000..b1fc864c --- /dev/null +++ b/port/src/romdata.c @@ -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 +#include + +#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; +} diff --git a/port/src/system.c b/port/src/system.c new file mode 100644 index 00000000..32b44f89 --- /dev/null +++ b/port/src/system.c @@ -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 +#include +#include +#include + +#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); +} diff --git a/port/src/ucode.c b/port/src/ucode.c new file mode 100644 index 00000000..095354b7 --- /dev/null +++ b/port/src/ucode.c @@ -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 [];`. + * + * 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}; diff --git a/port/src/utils.c b/port/src/utils.c new file mode 100644 index 00000000..9f9213bd --- /dev/null +++ b/port/src/utils.c @@ -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 +#include + +#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; +} diff --git a/port/src/video.c b/port/src/video.c new file mode 100644 index 00000000..5490a266 --- /dev/null +++ b/port/src/video.c @@ -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 + +#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); +}