mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
PC port: Phase 2 — real threads, dual-mapped DRAM, fast3d integration
- Replace setjmp/longjmp green threads with real pthreads (8MB stacks); PortQueue side table (mutex+condvar) for OSMesgQueue; dedicated tick thread posts VI retrace + services timers (D24). - Dual-mapped N64 DRAM: one 8MB store at V1 0x70000000 (s32-safe, all game RAM symbols) and V2 0x80000000 (KSEG0 mirror for offset|0x80000000 rebuilds). Windows: CreateFileMappingW + MapViewOfFileEx; POSIX: memfd + MAP_FIXED. dram_syms.s pins cfb_16/_bssSegmentEnd/tlb block (D25). - Address-width shims: PHYS_TO_K0 identity, OS_K0_TO_PHYSICAL = offset from V1 base, OS_PHYSICAL_TO_K0 identity; fast3d seg_addr() remaps small offsets into V2 (D26). - ASLR off (-Wl,--disable-dynamicbase): absolute symbols referenced through pointer-typed externs (.refptr + BASE reloc) must not rebase; startup sanity check on image base 0x140000000 (D27). - osPiStartDma posts its completion message on every path (fixes romReceiveMesg deadlock) (D29). - SEH-free crash handler: raw fault log first, validated manual EBP-chain walk second (-fno-omit-frame-pointer), TerminateProcess; crashDumpThreads via Toolhelp32 for heartbeat hang dumps (D30). - PD fast3d integrated (port/fast3d/), replaces gfxstub.c; real src/sched.c compiled; mainproc runs as a real pthread, host thread pumps SDL events. - Docs: findings D24-D31 + Phase 2 status (section F/G). Current state: boots through bossInitMainthreadData to langInit(), then SIGSEGVs on the first file load — stack frame clobbered by a table of truncated s32 pointers (D31, open). Next: attribute the write with 'set scheduler-locking on' + hardware watchpoint.
This commit is contained in:
1 parent
2aad171344
commit
13ca03a6db
42 files changed
+12660
-537
No files matched your search
@@ -117,3 +117,9 @@ assets/obseg/em/data/under.seg
|
||||
scripts/local/
|
||||
scripts/make/local/
|
||||
scripts/__pycache__/generate_prop_model_c.cpython-310.pyc
|
||||
|
||||
# PC port debug/session artifacts
|
||||
_run.log
|
||||
ge007.crash.log
|
||||
.pi/
|
||||
run_out.log
|
||||
+55
-4
@@ -148,6 +148,23 @@ configure_file(
|
||||
"${CMAKE_SOURCE_DIR}/port/include/hoststring.h.in"
|
||||
"${CMAKE_BINARY_DIR}/port/include/hoststring.h" @ONLY)
|
||||
|
||||
# Host <stdlib.h> path for port/shim/stdlib.h. The N64 include/stdlib.h
|
||||
# typedefs lldiv_t/ldiv_t, which conflict with the host's when both land in
|
||||
# one C++ TU (fast3d): MinGW's own sec_api/stdlib_s.h does `#include
|
||||
# <stdlib.h>`, which resolves to the N64 header via -I. The shim routes C++
|
||||
# TUs to the host header by absolute path (same trick as hoststring.h).
|
||||
get_filename_component(PORT_HOST_STDLIB_H "${_PORT_GCC_DIR}/../include/stdlib.h" ABSOLUTE)
|
||||
configure_file(
|
||||
"${CMAKE_SOURCE_DIR}/port/include/hoststdlib.h.in"
|
||||
"${CMAKE_BINARY_DIR}/port/include/hoststdlib.h" @ONLY)
|
||||
|
||||
# Host <stddef.h> path for port/shim/stddef.h (same reason as stdlib: the N64
|
||||
# stub shadows the host header in C++ TUs, hiding max_align_t from cstddef).
|
||||
get_filename_component(PORT_HOST_STDDDEF_H "${_PORT_GCC_DIR}/../include/stddef.h" ABSOLUTE)
|
||||
configure_file(
|
||||
"${CMAKE_SOURCE_DIR}/port/include/hoststddef.h.in"
|
||||
"${CMAKE_BINARY_DIR}/port/include/hoststddef.h" @ONLY)
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Compiler definitions & flags
|
||||
# ----------------------------------------------------------------------------
|
||||
@@ -170,6 +187,11 @@ add_compile_options(
|
||||
-fwrapv
|
||||
-include "versioninfo.h"
|
||||
)
|
||||
# Keep frame pointers: dbghelp's StackWalk64 (crash handler + thread dumps)
|
||||
# cannot read DWARF unwind info on MinGW, so the EBP chain is the only way to
|
||||
# get a real backtrace of a hung/crashed game thread.
|
||||
add_compile_options(-fno-omit-frame-pointer)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
add_compile_options(-Og)
|
||||
endif()
|
||||
@@ -201,6 +223,18 @@ add_compile_options(
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:C>:-Wno-pointer-sign>)
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-Wno-unused-function>)
|
||||
|
||||
# ASLR must be OFF: dram_syms.s defines absolute symbols (cfb_16 @ 0x70000000,
|
||||
# _bssSegmentEnd @ 0x70050000) that the game references through pointer-typed
|
||||
# externs (src/bondgame.h: `extern u32 *_bssSegmentEnd`). On x86-64/PE, taking
|
||||
# their address emits a .refptr slot holding the absolute value PLUS a base
|
||||
# relocation; with ASLR the loader rebases it to runtime_base+0x70050000 =
|
||||
# garbage. With DYNAMICBASE disabled the image loads at its preferred base
|
||||
# (0x140000000) and the relocations are no-ops. main.c sanity-checks the load
|
||||
# base at startup.
|
||||
if(MINGW)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--disable-dynamicbase")
|
||||
endif()
|
||||
|
||||
# port/shim first: PC-only shadow headers (bondtypes.h, bondconstants.h,
|
||||
# PR/gbi.h) that fix N64-compiler assumptions without touching src/ or
|
||||
# include/. Inert in the N64 build (its Makefile never sees port/shim).
|
||||
@@ -244,7 +278,10 @@ endif()
|
||||
list(APPEND LIBS "${GL_LIBRARY}")
|
||||
|
||||
if(NOT EXTRA_LIBRARIES)
|
||||
if(WIN32)
|
||||
if(MINGW)
|
||||
# MinGW: pthreads for the real-OS-thread kernel (port/src/libultra.c).
|
||||
set(EXTRA_LIBRARIES dbghelp -static-libstdc++ pthread)
|
||||
elseif(WIN32)
|
||||
set(EXTRA_LIBRARIES dbghelp -static-libstdc++)
|
||||
else()
|
||||
set(EXTRA_LIBRARIES stdc++ m dl pthread)
|
||||
@@ -258,7 +295,8 @@ list(APPEND LIBS "${EXTRA_LIBRARIES}")
|
||||
# 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")
|
||||
file(GLOB_RECURSE SRC_FAST3D "${CMAKE_SOURCE_DIR}/port/fast3d/*.cpp"
|
||||
"${CMAKE_SOURCE_DIR}/port/fast3d/glad/*.c")
|
||||
|
||||
# Generated absolute ROM asset symbols for this region (see
|
||||
# scripts/gen_romassets.py). Defines every obseg/ramrom/music label and the
|
||||
@@ -269,6 +307,10 @@ if(NOT EXISTS ${SRC_ROMASSETS})
|
||||
message(FATAL_ERROR "Missing ${SRC_ROMASSETS} — run: python3 scripts/gen_romassets.py ${ASSET_REGION}")
|
||||
endif()
|
||||
|
||||
# Absolute symbols inside the reserved N64-DRAM region (port/src/dram.c):
|
||||
# cfb_16 (replaces src/cfb.c) and _bssSegmentEnd (mempool start marker).
|
||||
set(SRC_DRAM_SYMS "${CMAKE_SOURCE_DIR}/port/src/dram_syms.s")
|
||||
|
||||
# Game code — compiled unmodified.
|
||||
file(GLOB_RECURSE SRC_GAME "${CMAKE_SOURCE_DIR}/src/game/*.c")
|
||||
|
||||
@@ -344,21 +386,29 @@ list(PREPEND SRC_ASSETS "${CMAKE_SOURCE_DIR}")
|
||||
# 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
|
||||
# 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).
|
||||
#
|
||||
# sched.c IS compiled (Phase 2): the game's real scheduler runs on the
|
||||
# cooperative thread kernel; its hardware leaf calls (osSpTaskLoad/StartGo,
|
||||
# osDpSetNextBuffer, osVi*) are shimmed in port/src/libultra.c, which runs
|
||||
# the software RSP inline and posts the RSP_DONE/RDP_DONE messages.
|
||||
set(SRC_ENGINE
|
||||
src/init.c
|
||||
src/sched.c
|
||||
src/audi.c
|
||||
src/snd.c
|
||||
src/music.c
|
||||
src/joy.c
|
||||
src/fr.c
|
||||
src/cfb.c
|
||||
# src/cfb.c — cfb_16 now lives at 0x80000000 in the reserved DRAM region
|
||||
# (port/src/dram_syms.s) so OS_K0_TO_PHYSICAL(cfb_16[0]) is a small offset
|
||||
# that fast3d can map back; a host-BSS array would truncate to garbage.
|
||||
src/stacks.c
|
||||
src/boss.c
|
||||
src/mema.c
|
||||
@@ -377,6 +427,7 @@ set(SRC
|
||||
${SRC_GAME}
|
||||
${SRC_PORT}
|
||||
${SRC_ROMASSETS}
|
||||
${SRC_DRAM_SYMS}
|
||||
${SRC_PORT_CXX}
|
||||
${SRC_FAST3D}
|
||||
${SRC_LIBAUDIO}
|
||||
|
||||
@@ -733,3 +733,188 @@ validation, 12 MB mapped at 0x10000000, window renders for the full test
|
||||
duration. Asset symbol spot-checks against the ROM confirm correct offsets
|
||||
(e.g. music bank BE seqCount=63 @0x419790, first track offset 508 → Mno_music
|
||||
@0x41998C; sfx/instrument banks start with GE's `B1` header).
|
||||
|
||||
### F. Phase 2 runtime findings (threads, DRAM, addresses)
|
||||
|
||||
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
|
||||
threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software
|
||||
RSP (`port/fast3d/`). The boot path now runs: ROM map → DRAM reserve → video
|
||||
init → mainThread (real pthread) → bossEntry → bossInitMainthreadData through
|
||||
mempool init, VI init, rspInit, joyInit + controller-init timers, stanInit,
|
||||
gameInit — and currently dies inside `langInit()` (D31).
|
||||
|
||||
* **D24 — setjmp/longjmp green threads are unusable on MinGW x64.** The first
|
||||
kernel used setjmp/longjmp context switches; under Windows x64 + MinGW the
|
||||
longjmp path corrupts FPU/MC state when it interacts with SEH unwinding
|
||||
(observed: STATUS_DATATYPE_MISALIGNMENT-class crash inside a resumed
|
||||
"thread" with garbage register state). PD's port does not use setjmp at all.
|
||||
**Resolved:** `port/src/libultra.c` now runs every game thread as a real
|
||||
pthread (`PortThread` side table, 8 MB stacks). Message queues keep the N64
|
||||
`OSMesgQueue` layout and get a `PortQueue` side table (mutex + condvar, max
|
||||
64 queues); all osSendMesg/osRecvMesg paths lock it. A dedicated tick thread
|
||||
posts one VI retrace per frame (NTSC 60 Hz) and services the software timers
|
||||
(both OS_MESG_NOBLOCK — the tick thread can never deadlock). `idleThread` is
|
||||
intercepted by ID and parked; `mainproc` runs as a real pthread so the host
|
||||
main thread is free to pump SDL/OS events (`videoPumpEvents()` in the host
|
||||
loop — on Windows WndProc only runs on the window-creating thread). The GL
|
||||
context is made current on shedThread per frame via
|
||||
`gfx_sdl_make_context_current()`. Verified live: VI retrace flowing, all 4
|
||||
controller-init timers fire, bossmq loop completes.
|
||||
* **D25 — dual-mapped N64 DRAM region.** Game code needs two incompatible
|
||||
address forms of the same RAM: `osVirtualToPhysical`/s32 arithmetic wants
|
||||
live host pointers that fit in a positive s32, while `offset | 0x80000000`
|
||||
rebuilds (bg.c:3184/3322, propobj.c:8578/8699, title.c:476) want a KSEG0
|
||||
view at 0x80000000. **Resolved:** one 8 MB backing store mapped twice —
|
||||
V1 @ 0x70000000 (all game RAM symbols live here) and V2 @ 0x80000000
|
||||
(byte-identical mirror). `port/src/dram.c`: Windows uses
|
||||
`CreateFileMappingW(INVALID_HANDLE_VALUE,…)` + two `MapViewOfFileEx(…, base)`
|
||||
calls (the documented kernel32 API that maps a section at an exact address;
|
||||
`NtMapViewOfSection` fails with STATUS_MEMORY_NOT_ALLOCATED 0xc0000045 and
|
||||
segfaults on wrong SECTION_INHERIT values — do not retry it); POSIX uses
|
||||
memfd + two MAP_SHARED|MAP_FIXED mmaps. Both views sanity-checked to alias.
|
||||
`port/src/dram_syms.s` pins the absolute symbols: `cfb_16` @ 0x70000000
|
||||
(0x4B000), `_bssSegmentEnd` @ 0x70050000 (mempool start), tlb block end @
|
||||
0x702F4400 (= page_align_down(0x803AB400) − 93×0x2000, the exact N64 value;
|
||||
the tlbmanage stub returns it so `mempInit`'s pool size is correct — a NULL
|
||||
return here made the mempool spin in `while(1)` at src/memp.c:164).
|
||||
* **D26 — address-width shims (K0 sign-extension).** N64 K0 addresses have bit
|
||||
31 set; passing them through s32 parameters sign-extends to invalid x86-64
|
||||
pointers. With the dual map, live RAM sits below 0x80000000 so identity is
|
||||
safe: `port/shim/PR/R4300.h` → `PHYS_TO_K0(x) = (x)`; `port/shim/PR/os.h` →
|
||||
`OS_K0_TO_PHYSICAL(x) = (u32)((char *)x - 0x70000000)` (GBI w1 words then
|
||||
carry small offsets) and `OS_PHYSICAL_TO_K0(x) = (x)`. fast3d's `seg_addr()`
|
||||
(gfx_pc.cpp) resolves both forms: full host pointers pass through, values <
|
||||
0x800000 get +0x80000000 (landing in V2). Same remap applied to the
|
||||
G_MW_SEGMENT handler.
|
||||
* **D27 — ASLR must be OFF for this exe.** `src/bondgame.h:8` declares
|
||||
`extern u32 *_bssSegmentEnd;` (pointer type), so `&_bssSegmentEnd` in game
|
||||
code emits a `.refptr._bssSegmentEnd` slot holding the absolute value with a
|
||||
BASE relocation. Under ASLR the loader rebases it to runtime_base +
|
||||
0x70050000 = garbage, and `mempCheckMemflagTokens` then spins/AVs with
|
||||
poolAreaStart ≈ 0xF0xxxxxx. (On MIPS `&abs_symbol` is the symbol value
|
||||
itself; x86-64/PE cannot do that.) **Resolved:** MINGW link flag
|
||||
`-Wl,--disable-dynamicbase` (note: `--disable-dynamic-base` is NOT
|
||||
recognized by GNU ld 2.47); the image now loads at its preferred base
|
||||
0x140000000 and relocations are no-ops. `main.c` fails loudly at startup if
|
||||
`sysImageBase() != 0x140000000ul` so any future ASLR regression is a clean
|
||||
error, not silent DRAM corruption.
|
||||
* **D28 — ninja stale-object hazard with new shim headers.** If a shim header
|
||||
did not exist when a .obj was last built, it is absent from that object's
|
||||
.d dependency file and ninja will NOT rebuild the TU after the header
|
||||
appears — the old (unshimmed) code silently persists. Observed: boss.c.obj
|
||||
still contained `or $0x80000000,%eax` (original PHYS_TO_K0) days after the
|
||||
shim landed. **Rule:** whenever anything under `port/shim/` changes, delete
|
||||
all `.obj` files and full-rebuild. Verify with `gcc -E` using the exact
|
||||
ninja flags (`ninja -C build-pc -t commands <tgt>`) plus objdump of the
|
||||
rebuilt object.
|
||||
* **D29 — `osPiStartDma` must post its completion message on every path.** The
|
||||
N64 PI posts the caller's OSMesgPI to mq when a DMA completes; GE never
|
||||
inspects the message but always blocks on it (`romReceiveMesg()` →
|
||||
`osRecvMesg(OS_MESG_BLOCK)` in ramrom.c:44). The shim did the memcpy
|
||||
synchronously and posted nothing → mainThread deadlocked in the first file
|
||||
load. **Resolved:** post unconditionally after `piServiceDma()` (also for
|
||||
skipped/dropped DMAs) with OS_MESG_BLOCK, posting the caller's OSMesgPI.
|
||||
* **D30 — crash handler without SEH.** MinGW GCC 16 has no `__try`/`__except`
|
||||
and does not recognize `-fseh-exceptions`, so the unhandled-exception filter
|
||||
cannot rely on structured exceptions to protect symbolication. **Resolved:**
|
||||
`port/src/crash.c` phase 1 writes raw fault info (registers, modules) to
|
||||
`ge007.crash.log` without touching dbghelp (SymInitialize/StackWalk64 can
|
||||
allocate and re-fault inside the filter); phase 2 does a validated manual
|
||||
EBP-chain walk (`-fno-omit-frame-pointer` on all TUs; each frame is [saved
|
||||
RBP, return address], stop if saved_fp ≤ fp or outside thread stack limits).
|
||||
`TerminateProcess` instead of abort (SIGABRT can re-enter the exception
|
||||
machinery). `crashDumpThreads()` (Toolhelp32 snapshot + SuspendThread +
|
||||
GetThreadContext + StackWalk64 per thread) is called from the heartbeat
|
||||
error dump with TID→name matching; StackWalk64 cannot read DWARF unwind info
|
||||
on MinGW, so multi-frame traces depend entirely on the EBP chain. Symbols
|
||||
are recovered offline: `addr2line -e build-pc/ge007.x86_64.exe -f -C
|
||||
<base+rel>` (the log's "rel" = address − actual load base).
|
||||
* **D31 — langInit SIGSEGV: stack frame clobbered by a truncated-pointer table (OPEN).**
|
||||
After D24–D29, mainThread reaches `langInit()` and dies on the **first** file
|
||||
load (`_fileNameLoadToBank(LnameX_lookuptable[LGUN][…])` → index 670 →
|
||||
`fileIndexLoadToBank` → `mempAllocBytesInBank` → `load_resource`). Crash
|
||||
signature: RIP = 0x140330003 (inside `.bss`, in `g_Props`), RSP only ~0xD8
|
||||
below langInit's entry RSP (a *shallow* chain — the stack pointer itself is
|
||||
fine), and the frame region above load_resource's return address is filled
|
||||
with 8-byte slots `{u32, 0x00000001}` where each u32 is the **low 32 bits of
|
||||
a live .bss pointer**, alternating between `g_Props` (0x14033xxxx) and
|
||||
`resource_lookup_data_array` (0x14034xxxx). The crash is a `ret` popping one
|
||||
of these truncated pointers as the return address (observed RIP equals the
|
||||
table value exactly, high word 0x00000001). Something in the load chain is
|
||||
storing 64-bit pointers into s32/int storage (fine on 32-bit MIPS,
|
||||
truncating on x86-64) and that storage overlaps a live frame.
|
||||
|
||||
Established facts (all under gdb, `gdb -batch -ex "handle SIGSEGV stop"`):
|
||||
* All arguments at `load_resource` entry are valid: ptrdata=0x702aa400
|
||||
(mempool), srcfile->hw_address=0x108ed250, rom_size=0x720, source=
|
||||
0x702f3ce0. `romCopy` (PI shim memcpy) completes.
|
||||
* `zlib_inflate()` itself **completes normally** (`finish` → returns 0,
|
||||
rz_wp=3872; the RZ stream is plain deflate after a 2-byte header, so
|
||||
endianness is not an issue in the bitstream). At its entry rz_outbuf =
|
||||
ptrdata ✓, rz_inbuf = source+2 ✓, and the huft table base (tl) sits inside
|
||||
load_resource's 8 KB local `buffer` (rz_hlist).
|
||||
* **Not** a huft-table overflow: max observed `rz_hufts` = 797 entries ×
|
||||
4 B (`struct huft {u8 e; u8 b; union{u16 n; struct huft *t} v;}`) = 3188 B
|
||||
< 8448 B buffer.
|
||||
* Ruled out by code inspection: `mempAddEntryOfSizeToBank` (only touches
|
||||
pool pos/prevpos), `fileGetIndex` (reads only), `decompressdata` epilogue
|
||||
(returns rz_wp).
|
||||
* `load_resource` prologue: `push rbp/rdi/rsi/rbx; mov $0x2128,%eax;
|
||||
call ___chkstk_ms; sub %rax,%rsp; lea 0x80(%rsp),%rbp` — RBP is a **fake
|
||||
frame pointer** (RSP+0x80); the real return address is at [RBP+0x20E8].
|
||||
The compiler reuses pushed-register slots as locals (verified: the saved-
|
||||
rbx slot [RBP+0x20D0] legitimately receives `buffer+12`). Watchpoints on
|
||||
RBP-relative offsets are therefore easy to misplace — compute from entry
|
||||
RSP instead.
|
||||
* Hardware watchpoint on the real retaddr slot (set at the first
|
||||
instruction, where [RSP] = retaddr): first write changes it from a valid
|
||||
.text return address to **`buffer+12`** (a pointer into load_resource's
|
||||
own local array), reported RIP at the prologue boundary (`push %rdi`) —
|
||||
i.e. either a concurrent write by another thread (hardware watchpoints are
|
||||
global) or an 8-byte-attributed access. A `thread apply all bt` at that
|
||||
moment showed every other game thread parked (RtlUserThreadStart / sleep
|
||||
syscalls; gdb could not unwind their stacks further), so the writer is not
|
||||
yet identified.
|
||||
|
||||
Next steps: re-run the watchpoint with **`set scheduler-locking on`**
|
||||
(only mainThread runs → any hit is its own) to attribute the write to an
|
||||
exact instruction; if it is load_resource's own spill, that is a GCC
|
||||
miscompilation → compile ob.c at -O0/-Og or add a PORT-gated hook. Also try
|
||||
multiple watchpoints on consecutive slots to catch a sequential table fill
|
||||
mid-flight, and grep the disassembly of the whole chain for stores through
|
||||
s32-typed pointers (`movl …(%reg)` where %reg holds a truncated pointer).
|
||||
|
||||
### G. Phase 2 status (current)
|
||||
|
||||
Done: PD fast3d integrated (`port/fast3d/`, replaces the deleted gfxstub.c);
|
||||
GE's real `src/sched.c` compiled (shedThread runs `__scMain`); real pthread
|
||||
kernel (D24); dual-mapped DRAM + address shims (D25/D26); ASLR off with startup
|
||||
sanity check (D27); PI completion messages (D29); SEH-free crash handler with
|
||||
thread dumps (D30). The process now boots, maps the ROM, opens the window,
|
||||
runs `mainproc()` → `bossInitMainthreadData` through controller init / timers /
|
||||
mempool / VI / rsp / stan / game init — and SIGSEGVs inside `langInit()`'s
|
||||
first file load (D31). No frames rendered yet.
|
||||
|
||||
Reference — the file-load chain (for D31 debugging):
|
||||
`langInit` (language.c:230) loads 7 text banks via
|
||||
`_fileNameLoadToBank(LnameX_lookuptable[bank][j_text_trigger], …, 0x100,
|
||||
MEMPOOL_PERMANENT)` → `fileGetIndex` (strcmp over file_resource_table) →
|
||||
`fileIndexLoadToBank(index, …)` (ob.c:216): if poolRemaining==0 take
|
||||
`mempGetBankSizeLeft(bank)`; `ptrdata = mempAllocBytesInBank(…)`; hw_address
|
||||
nonzero → `load_resource(ptrdata, poolRemaining, &file_resource_table[index],
|
||||
&resource_lookup_data_array[index])` (ob.c:37): `source = (ptrdata + bytes) −
|
||||
((rom_size+7)&−8)`; `romCopy(source, hw_address, rom_size)`;
|
||||
`decompressdata(source, ptrdata, buffer)` with `u8 buffer[0x2100]` on the
|
||||
stack → sets rz_inbuf=source+2 / rz_outbuf=ptrdata / rz_hlist=buffer →
|
||||
`zlib_inflate()` (src/game/zlib.c:668). rom_size for index 670 = 0x720,
|
||||
decompressed size = 3872 (0xF20). `file_resource_table` is compiled
|
||||
(`assets/obseg/file_resource_table.inc.c`) with hw_address = cart addresses;
|
||||
rom_size = adjacent-marker differences computed in obInit.
|
||||
|
||||
Debugging techniques that worked: gdb **launch** mode (`gdb -batch -ex
|
||||
"handle SIGSEGV stop" -ex run …` — attach fails with error 87); breakpoint
|
||||
chains through init functions to bracket the crash; `finish` to let a suspect
|
||||
function complete and inspect its return; hardware watchpoints on exact stack
|
||||
slots (compute offsets from entry RSP, not RBP — see D31); `addr2line` for
|
||||
offline symbolication; objdump + `gcc -E` with exact ninja flags to verify
|
||||
shim expansion in compiled code.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Emill, MaikelChan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* GE adaptation header for fast3d (see docs/PCPortResearch.md).
|
||||
*
|
||||
* fast3d was written against Perfect Dark's extended GBI set
|
||||
* (pd_port/src/include/gbiex.h + PD's include/PR/gbi.h). GoldenEye shares the
|
||||
* same Rare "Indy" engine family but its extension header is
|
||||
* include/gbi_extension.h, which defines G_TRI4 (0xb1) and GE's custom
|
||||
* combine/RM modes — but NOT the PD-specific opcodes below.
|
||||
*
|
||||
* GE never emits the PD-only opcodes (its RSP ucode is rsp/graphics/gmain.s,
|
||||
* which has no handling for them), so the corresponding dispatch cases in
|
||||
* gfx_pc.cpp are dead code for this port; they only need to COMPILE. The
|
||||
* values mirror PD's gbiex.h exactly so that if a ROM ever does emit one,
|
||||
* behaviour matches the sibling port rather than hitting the fatal "unknown
|
||||
* opcode" path.
|
||||
*
|
||||
* G_COL (7) is "new in PD": GE fills vertex colours directly in Vtx.cn[]
|
||||
* and its ucode has no colour-table DMA, so that case is dead here too.
|
||||
*/
|
||||
#ifndef FAST3D_GBIEX_H
|
||||
#define FAST3D_GBIEX_H
|
||||
|
||||
#include <PR/gbi.h>
|
||||
#include "include/gbi_extension.h" /* G_TRI4 (0xb1), GE's own extensions */
|
||||
|
||||
/* PD extension opcodes (pd_port/src/include/gbiex.h). */
|
||||
#define G_COL 7 /* vertex colour DMA — PD-only */
|
||||
#define G_SETFB_EXT 0x21
|
||||
#define G_SETTIMG_FB_EXT 0x23
|
||||
#define G_INVALTEXCACHE_EXT 0x34
|
||||
#define G_TEXRECT_WIDE_EXT 0x37
|
||||
#define G_FILLRECT_WIDE_EXT 0x38
|
||||
#define G_SETGRAYSCALE_EXT 0x39
|
||||
#define G_EXTRAGEOMETRYMODE_EXT 0x3a
|
||||
#define G_SETINTENSITY_EXT 0x40
|
||||
#define G_COPYFB_EXT 0x41
|
||||
#define G_IMAGERECT_EXT 0x42
|
||||
#define G_RDPFLUSH_EXT 0x43
|
||||
#define G_CLEAR_DEPTH_EXT 0x44
|
||||
#define G_SETSUBPIXELOFFSET_EXT 0x45
|
||||
|
||||
/* Extra geometry/aspect bits used by the PD ucode (dead for GE). */
|
||||
#define G_ASPECT_LEFT_EXT 0x00000010
|
||||
#define G_ASPECT_RIGHT_EXT 0x00000020
|
||||
#define G_ASPECT_WIDE_EXT 0x00000040
|
||||
#define G_ASPECT_CENTER_EXT (G_ASPECT_LEFT_EXT | G_ASPECT_RIGHT_EXT)
|
||||
#define G_ASPECT_MODE_EXT (G_ASPECT_CENTER_EXT | G_ASPECT_WIDE_EXT)
|
||||
#define G_NO_CLIPPING_EXT 0x00000100
|
||||
#define G_MODULATE_EXT 0x00000200
|
||||
#define G_TF_BLUR_EXT (1 << G_MDSFT_TEXTFILT)
|
||||
|
||||
/* Big-endian read helpers (pd_port/src/include/platform.h). The host is
|
||||
* little-endian, so a "BE" value in ROM data must be byte-swapped. */
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#define PD_BSWAP16(x) __builtin_bswap16(x)
|
||||
#define PD_BSWAP32(x) __builtin_bswap32(x)
|
||||
#else
|
||||
#error "Implement PD_BSWAP macros for your compiler."
|
||||
#endif
|
||||
#define PD_BE16(x) PD_BSWAP16(x)
|
||||
#define PD_BE32(x) PD_BSWAP32(x)
|
||||
|
||||
#endif /* FAST3D_GBIEX_H */
|
||||
+30
-38
@@ -1,29 +1,18 @@
|
||||
#ifndef GFX_API_H
|
||||
#define GFX_API_H
|
||||
|
||||
/*
|
||||
* Public API for the software RSP (fast3d).
|
||||
*
|
||||
* This is the interface the rest of the port (video.c / gesched.c) uses to
|
||||
* drive the RSP. It mirrors the PD port's port/fast3d/gfx_api.h.
|
||||
*
|
||||
* STATUS: scaffolding — the implementation (gfx_pc.cpp etc.) is brought in
|
||||
* during Phase 2 by adapting the PD fast3d. See port/fast3d/README.md.
|
||||
*/
|
||||
|
||||
#ifndef __cplusplus
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#endif
|
||||
|
||||
#include <PR/gbi.h>
|
||||
#include "gfx_rendering_api.h"
|
||||
#include "gfx_window_manager_api.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct GfxRenderingAPI;
|
||||
struct GfxWindowManagerAPI;
|
||||
struct XYWidthHeight {
|
||||
int16_t x, y;
|
||||
uint32_t width, height;
|
||||
};
|
||||
|
||||
struct GfxDimensions {
|
||||
float internal_mul;
|
||||
@@ -34,33 +23,36 @@ struct GfxDimensions {
|
||||
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;
|
||||
struct GfxWindowInitSettings window_settings;
|
||||
};
|
||||
|
||||
/* Current draw-area dimensions (before any scaling). */
|
||||
extern struct GfxDimensions gfx_current_dimensions;
|
||||
extern struct GfxDimensions gfx_current_window_dimensions; // The dimensions of the window
|
||||
extern struct GfxDimensions
|
||||
gfx_current_dimensions; // The dimensions of the draw area the game draws to, before scaling (if applicable)
|
||||
extern struct XYWidthHeight
|
||||
gfx_current_game_window_viewport; // The area of the window the game is drawn to, (0, 0) is top-left corner
|
||||
extern uint32_t gfx_msaa_level;
|
||||
extern struct XYWidthHeight gfx_current_native_viewport; // The internal/native video mode of the game
|
||||
extern float gfx_current_native_aspect; // The aspect ratio of the above mode
|
||||
extern bool gfx_framebuffers_enabled;
|
||||
extern bool gfx_detail_textures_enabled;
|
||||
|
||||
void gfx_init(const struct GfxInitSettings *settings);
|
||||
void gfx_destroy(void);
|
||||
|
||||
/* Frame boundary. */
|
||||
struct GfxRenderingAPI* gfx_get_current_rendering_api(void);
|
||||
void gfx_start_frame(void);
|
||||
void gfx_run(Gfx* commands);
|
||||
void gfx_end_frame(void);
|
||||
void gfx_set_target_fps(int);
|
||||
void gfx_set_texture_filter(enum FilteringMode mode);
|
||||
void gfx_set_mipmap_filter(enum MipmapFilteringMode mode);
|
||||
void gfx_texture_cache_clear(void);
|
||||
void gfx_texture_cache_delete(const uint8_t *orig_addr);
|
||||
void gfx_texture_cache_delete_range(const uint8_t *start, const uint8_t *end);
|
||||
int gfx_create_framebuffer(uint32_t width, uint32_t height, int upscale, int autoresize);
|
||||
void gfx_resize_framebuffer(int fb, uint32_t width, uint32_t height, int upscale, int autoresize);
|
||||
void gfx_set_framebuffer(int fb, float noise_scale) ;
|
||||
void gfx_reset_framebuffer(void);
|
||||
void gfx_copy_framebuffer(int fb_dst, int fb_src, int left, int top, int use_back);
|
||||
|
||||
/* 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 */
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "gfx_cc.h"
|
||||
|
||||
void gfx_cc_get_features(uint64_t shader_id0, uint32_t shader_id1, struct CCFeatures* cc_features) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int k = 0; k < 4; k++) {
|
||||
cc_features->c[i][j][k] = (shader_id0 >> (i * 32 + j * 16 + k * 4)) & 0xf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cc_features->opt_alpha = (shader_id1 & SHADER_OPT_ALPHA) != 0;
|
||||
cc_features->opt_fog = (shader_id1 & SHADER_OPT_FOG) != 0;
|
||||
cc_features->opt_texture_edge = (shader_id1 & SHADER_OPT_TEXTURE_EDGE) != 0;
|
||||
cc_features->opt_noise = (shader_id1 & SHADER_OPT_NOISE) != 0;
|
||||
cc_features->opt_2cyc = (shader_id1 & SHADER_OPT_2CYC) != 0;
|
||||
cc_features->opt_alpha_threshold = (shader_id1 & SHADER_OPT_ALPHA_THRESHOLD) != 0;
|
||||
cc_features->opt_invisible = (shader_id1 & SHADER_OPT_INVISIBLE) != 0;
|
||||
cc_features->opt_grayscale = (shader_id1 & SHADER_OPT_GRAYSCALE) != 0;
|
||||
cc_features->opt_blur = (shader_id1 & SHADER_OPT_BLUR) != 0;
|
||||
|
||||
cc_features->clamp[0][0] = (shader_id1 & SHADER_OPT_TEXEL0_CLAMP_S);
|
||||
cc_features->clamp[0][1] = (shader_id1 & SHADER_OPT_TEXEL0_CLAMP_T);
|
||||
cc_features->clamp[1][0] = (shader_id1 & SHADER_OPT_TEXEL1_CLAMP_S);
|
||||
cc_features->clamp[1][1] = (shader_id1 & SHADER_OPT_TEXEL1_CLAMP_T);
|
||||
|
||||
cc_features->used_textures[0] = false;
|
||||
cc_features->used_textures[1] = false;
|
||||
cc_features->num_inputs = 0;
|
||||
|
||||
for (int c = 0; c < 2; c++) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
if (cc_features->c[c][i][j] >= SHADER_INPUT_1 && cc_features->c[c][i][j] <= SHADER_INPUT_7) {
|
||||
if (cc_features->c[c][i][j] > cc_features->num_inputs) {
|
||||
cc_features->num_inputs = cc_features->c[c][i][j];
|
||||
}
|
||||
}
|
||||
if (cc_features->c[c][i][j] == SHADER_TEXEL0 || cc_features->c[c][i][j] == SHADER_TEXEL0A) {
|
||||
cc_features->used_textures[0] = true;
|
||||
}
|
||||
if (cc_features->c[c][i][j] == SHADER_TEXEL1 || cc_features->c[c][i][j] == SHADER_TEXEL1A) {
|
||||
cc_features->used_textures[1] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int c = 0; c < 2; c++) {
|
||||
cc_features->do_single[c][0] = cc_features->c[c][0][2] == SHADER_0;
|
||||
cc_features->do_single[c][1] = cc_features->c[c][1][2] == SHADER_0;
|
||||
cc_features->do_multiply[c][0] = cc_features->c[c][0][1] == SHADER_0 && cc_features->c[c][0][3] == SHADER_0;
|
||||
cc_features->do_multiply[c][1] = cc_features->c[c][1][1] == SHADER_0 && cc_features->c[c][1][3] == SHADER_0;
|
||||
cc_features->do_mix[c][0] = cc_features->c[c][0][1] == cc_features->c[c][0][3];
|
||||
cc_features->do_mix[c][1] = cc_features->c[c][1][1] == cc_features->c[c][1][3];
|
||||
cc_features->color_alpha_same[c] =
|
||||
((shader_id0 >> c * 32) & 0xffff) == ((shader_id0 >> (c * 32 + 16)) & 0xffff);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#ifndef GFX_CC_H
|
||||
#define GFX_CC_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <compare>
|
||||
#endif
|
||||
|
||||
enum {
|
||||
SHADER_0,
|
||||
SHADER_INPUT_1,
|
||||
SHADER_INPUT_2,
|
||||
SHADER_INPUT_3,
|
||||
SHADER_INPUT_4,
|
||||
SHADER_INPUT_5,
|
||||
SHADER_INPUT_6,
|
||||
SHADER_INPUT_7,
|
||||
SHADER_TEXEL0,
|
||||
SHADER_TEXEL0A,
|
||||
SHADER_TEXEL1,
|
||||
SHADER_TEXEL1A,
|
||||
SHADER_1,
|
||||
SHADER_COMBINED,
|
||||
SHADER_NOISE,
|
||||
SHADER_LOD_FRAC,
|
||||
};
|
||||
|
||||
#define SHADER_OPT_ALPHA (1 << 0)
|
||||
#define SHADER_OPT_FOG (1 << 1)
|
||||
#define SHADER_OPT_TEXTURE_EDGE (1 << 2)
|
||||
#define SHADER_OPT_NOISE (1 << 3)
|
||||
#define SHADER_OPT_2CYC (1 << 4)
|
||||
#define SHADER_OPT_ALPHA_THRESHOLD (1 << 5)
|
||||
#define SHADER_OPT_INVISIBLE (1 << 6)
|
||||
#define SHADER_OPT_GRAYSCALE (1 << 7)
|
||||
#define SHADER_OPT_TEXEL0_CLAMP_S (1 << 8)
|
||||
#define SHADER_OPT_TEXEL0_CLAMP_T (1 << 9)
|
||||
#define SHADER_OPT_TEXEL1_CLAMP_S (1 << 10)
|
||||
#define SHADER_OPT_TEXEL1_CLAMP_T (1 << 11)
|
||||
#define SHADER_OPT_BLUR (1 << 12)
|
||||
|
||||
struct ColorCombinerKey {
|
||||
uint64_t combine_mode;
|
||||
uint64_t options;
|
||||
auto operator<=>(const ColorCombinerKey&) const = default;
|
||||
};
|
||||
|
||||
#define SHADER_MAX_TEXTURES 2
|
||||
#define SHADER_FIRST_TEXTURE 0
|
||||
|
||||
struct CCFeatures {
|
||||
uint8_t c[2][2][4];
|
||||
bool opt_alpha;
|
||||
bool opt_fog;
|
||||
bool opt_texture_edge;
|
||||
bool opt_noise;
|
||||
bool opt_2cyc;
|
||||
bool opt_alpha_threshold;
|
||||
bool opt_invisible;
|
||||
bool opt_grayscale;
|
||||
bool opt_blur;
|
||||
bool used_textures[2];
|
||||
bool clamp[2][2];
|
||||
int num_inputs;
|
||||
bool do_single[2][2];
|
||||
bool do_multiply[2][2];
|
||||
bool do_mix[2][2];
|
||||
bool color_alpha_same[2];
|
||||
};
|
||||
|
||||
void gfx_cc_get_features(uint64_t shader_id0, uint32_t shader_id1, struct CCFeatures* cc_features);
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,8 @@
|
||||
#ifndef GFX_OPENGL_H
|
||||
#define GFX_OPENGL_H
|
||||
|
||||
#include "gfx_rendering_api.h"
|
||||
|
||||
extern struct GfxRenderingAPI gfx_opengl_api;
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,60 @@
|
||||
#ifndef GFX_PC_H
|
||||
#define GFX_PC_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <unordered_map>
|
||||
#include <list>
|
||||
#include <cstddef>
|
||||
|
||||
#include <PR/gbi.h>
|
||||
|
||||
#include "system.h"
|
||||
|
||||
#define SCREEN_WIDTH ((int32_t)gfx_current_native_viewport.width)
|
||||
#define SCREEN_HEIGHT ((int32_t)gfx_current_native_viewport.height)
|
||||
|
||||
extern uintptr_t gfxFramebuffer;
|
||||
|
||||
struct GfxRenderingAPI;
|
||||
struct GfxWindowManagerAPI;
|
||||
|
||||
struct TextureCacheKey {
|
||||
const uint8_t* texture_addr;
|
||||
const uint8_t* palette_addrs[2];
|
||||
uint8_t fmt, siz;
|
||||
uint8_t palette_index;
|
||||
|
||||
bool operator==(const TextureCacheKey&) const noexcept = default;
|
||||
|
||||
struct Hasher {
|
||||
size_t operator()(const TextureCacheKey& key) const noexcept {
|
||||
uintptr_t addr = (uintptr_t)key.texture_addr;
|
||||
return (size_t)(addr ^ (addr >> 5));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
typedef std::unordered_map<TextureCacheKey, struct TextureCacheValue, TextureCacheKey::Hasher> TextureCacheMap;
|
||||
typedef std::pair<const TextureCacheKey, struct TextureCacheValue> TextureCacheNode;
|
||||
|
||||
struct TextureCacheValue {
|
||||
uint32_t texture_id;
|
||||
uint8_t cms, cmt;
|
||||
bool linear_filter;
|
||||
|
||||
std::list<struct TextureCacheMapIter>::iterator lru_location;
|
||||
};
|
||||
|
||||
struct TextureCacheMapIter {
|
||||
TextureCacheMap::iterator it;
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
|
||||
#include "gfx_api.h"
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef GFX_RENDERING_API_H
|
||||
#define GFX_RENDERING_API_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
struct ShaderProgram;
|
||||
|
||||
struct GfxClipParameters {
|
||||
bool z_is_from_0_to_1;
|
||||
bool invert_y;
|
||||
};
|
||||
|
||||
enum FilteringMode { FILTER_NONE, FILTER_LINEAR, FILTER_THREE_POINT };
|
||||
enum MipmapFilteringMode { MIPMAP_DISABLED, MIPMAP_NEAREST, MIPMAP_LINEAR };
|
||||
|
||||
struct GfxRenderingAPI {
|
||||
const char* (*get_name)(void);
|
||||
int (*get_max_texture_size)(void);
|
||||
struct GfxClipParameters (*get_clip_parameters)(void);
|
||||
void (*unload_shader)(struct ShaderProgram* old_prg);
|
||||
void (*load_shader)(struct ShaderProgram* new_prg);
|
||||
struct ShaderProgram* (*create_and_load_new_shader)(uint64_t shader_id0, uint32_t shader_id1);
|
||||
struct ShaderProgram* (*lookup_shader)(uint64_t shader_id0, uint32_t shader_id1);
|
||||
void (*shader_get_info)(struct ShaderProgram* prg, uint8_t* num_inputs, bool used_textures[2]);
|
||||
void (*clear_shaders)(void);
|
||||
uint32_t (*new_texture)(void);
|
||||
void (*select_texture)(int tile, uint32_t texture_id, bool linear_filter);
|
||||
void (*upload_texture)(const uint8_t* rgba32_buf, uint32_t width, uint32_t height, bool gen_mipmaps);
|
||||
void (*set_sampler_parameters)(int sampler, bool linear_filter, uint32_t cms, uint32_t cmt, bool mipmaps);
|
||||
void (*set_depth_mode)(bool depth_test, bool depth_update, bool depth_compare, bool depth_source_prim, uint16_t zmode);
|
||||
void (*set_depth_range)(float znear, float zfar);
|
||||
void (*set_viewport)(int x, int y, int width, int height);
|
||||
void (*set_scissor)(int x, int y, int width, int height);
|
||||
void (*set_use_alpha)(bool use_alpha, bool modulate);
|
||||
void (*draw_triangles)(float buf_vbo[], size_t buf_vbo_len, size_t buf_vbo_num_tris);
|
||||
void (*init)(void);
|
||||
void (*on_resize)(void);
|
||||
void (*start_frame)(void);
|
||||
void (*end_frame)(void);
|
||||
void (*finish_render)(void);
|
||||
int (*create_framebuffer)();
|
||||
void (*update_framebuffer_parameters)(int fb_id, uint32_t width, uint32_t height, uint32_t msaa_level,
|
||||
bool opengl_invert_y, bool render_target, bool has_depth_buffer,
|
||||
bool can_extract_depth);
|
||||
bool (*start_draw_to_framebuffer)(int fb_id, float noise_scale);
|
||||
void (*copy_framebuffer)(int fb_dst, int fb_src, int left, int top, bool flip_y, bool use_back);
|
||||
void (*clear_framebuffer)(bool clear_color, bool clear_depth);
|
||||
void (*resolve_msaa_color_buffer)(int fb_id_target, int fb_id_source);
|
||||
void* (*get_framebuffer_texture_id)(int fb_id);
|
||||
void (*select_texture_fb)(int fb_id);
|
||||
void (*delete_texture)(uint32_t texID);
|
||||
void (*set_texture_filter)(enum FilteringMode mode);
|
||||
enum FilteringMode (*get_texture_filter)(void);
|
||||
void (*set_mipmap_filter)(enum MipmapFilteringMode mode);
|
||||
void (*set_anisotropy_level)(int);
|
||||
int (*get_max_anisotropy_level)(void);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,7 @@
|
||||
#ifndef GFX_SCREEN_CONFIG_H
|
||||
#define GFX_SCREEN_CONFIG_H
|
||||
|
||||
#define DESIRED_SCREEN_WIDTH 640
|
||||
#define DESIRED_SCREEN_HEIGHT 480
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef GFX_SDL_H
|
||||
#define GFX_SDL_H
|
||||
|
||||
#include "gfx_window_manager_api.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct GfxWindowManagerAPI gfx_sdl;
|
||||
|
||||
/* Make the window's GL context current on the calling thread (rendering
|
||||
* runs on the game's scheduler thread, not the thread that created it). */
|
||||
void gfx_sdl_make_context_current(void);
|
||||
|
||||
/* Refresh the cached drawable size after a window resize (called by the
|
||||
* host thread's event pump). */
|
||||
void gfx_sdl_update_cached_size(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,488 @@
|
||||
#include <stdio.h>
|
||||
#include <SDL.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "system.h"
|
||||
|
||||
#include "gfx_window_manager_api.h"
|
||||
#include "gfx_screen_config.h"
|
||||
|
||||
static SDL_Window* wnd;
|
||||
static SDL_GLContext ctx;
|
||||
static SDL_Renderer* renderer;
|
||||
static int sdl_to_lus_table[512];
|
||||
static bool vsync_enabled = true;
|
||||
// OTRTODO: These are redundant. Info can be queried from SDL.
|
||||
static int window_width = DESIRED_SCREEN_WIDTH;
|
||||
static int window_height = DESIRED_SCREEN_HEIGHT;
|
||||
static uint32_t fullscreen_flag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
static bool fullscreen_state;
|
||||
static bool maximized_state;
|
||||
static bool is_running = true;
|
||||
static void (*on_fullscreen_changed_callback)(bool is_now_fullscreen);
|
||||
|
||||
// GE port: frame pacing is owned by the cooperative thread kernel's vsync
|
||||
// tick (libultra.c posts one VI retrace per frame at the console rate), so
|
||||
// fast3d's internal busy-wait sync is disabled to avoid double-pacing. The
|
||||
// GL swap still honors display vsync (vsync_enabled below).
|
||||
static int target_fps = 0;
|
||||
static uint64_t previous_time;
|
||||
static uint64_t qpc_freq;
|
||||
|
||||
#define FRAME_INTERVAL_US_NUMERATOR 1000000
|
||||
#define FRAME_INTERVAL_US_DENOMINATOR (target_fps)
|
||||
|
||||
static int32_t gfx_sdl_get_maximized_state(void) {
|
||||
return (int32_t)maximized_state;
|
||||
}
|
||||
|
||||
static int32_t gfx_sdl_get_fullscreen_state(void) {
|
||||
return (int32_t)fullscreen_state;
|
||||
}
|
||||
|
||||
static int32_t gfx_sdl_get_fullscreen_flag_mode(void) {
|
||||
return fullscreen_flag == SDL_WINDOW_FULLSCREEN_DESKTOP ? 0 : 1;
|
||||
}
|
||||
|
||||
static void gfx_sdl_set_fullscreen_flag(int32_t mode) {
|
||||
switch (mode) {
|
||||
case 0: {
|
||||
fullscreen_flag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
} break;
|
||||
case 1: {
|
||||
fullscreen_flag = SDL_WINDOW_FULLSCREEN;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
static void set_fullscreen(bool on, bool call_callback) {
|
||||
fullscreen_state = on;
|
||||
SDL_SetWindowFullscreen(wnd, on ? fullscreen_flag : 0);
|
||||
if (call_callback && on_fullscreen_changed_callback) {
|
||||
on_fullscreen_changed_callback(on);
|
||||
}
|
||||
}
|
||||
|
||||
static void set_maximize_window(bool on) {
|
||||
maximized_state = on;
|
||||
if (on) {
|
||||
SDL_MaximizeWindow(wnd);
|
||||
} else {
|
||||
SDL_RestoreWindow (wnd);
|
||||
}
|
||||
}
|
||||
|
||||
static void gfx_sdl_get_active_window_refresh_rate(uint32_t* refresh_rate) {
|
||||
int display_in_use = SDL_GetWindowDisplayIndex(wnd);
|
||||
|
||||
SDL_DisplayMode mode;
|
||||
SDL_GetCurrentDisplayMode(display_in_use, &mode);
|
||||
*refresh_rate = mode.refresh_rate;
|
||||
}
|
||||
|
||||
static void gfx_sdl_init(const struct GfxWindowInitSettings *set) {
|
||||
window_width = set->width;
|
||||
window_height = set->height;
|
||||
|
||||
#ifdef SDL_HINT_VIDEO_HIGHDPI_DISABLED
|
||||
if (!set->allow_hidpi) {
|
||||
// HiDPI control, if available
|
||||
SDL_SetHint(SDL_HINT_VIDEO_HIGHDPI_DISABLED, "1");
|
||||
#if defined(PLATFORM_WIN32) && defined(SDL_HINT_WINDOWS_DPI_AWARENESS)
|
||||
// if HiDPI is disabled, declare ourselves DPI aware to get 1:1 window size on Windows
|
||||
SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitor");
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
|
||||
sysFatalError("Could not init SDL:\n%s", SDL_GetError());
|
||||
}
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
||||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||
if (sysArgCheck("--debug-gl")) {
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG);
|
||||
}
|
||||
|
||||
int posX = set->x;
|
||||
int posY = set->y;
|
||||
int display_in_use = SDL_GetWindowDisplayIndex(wnd);
|
||||
if (display_in_use < 0) { // Fallback to default if out of bounds
|
||||
posX = SDL_WINDOWPOS_UNDEFINED;
|
||||
posY = SDL_WINDOWPOS_UNDEFINED;
|
||||
}
|
||||
|
||||
if (set->centered) {
|
||||
SDL_DisplayMode mode = {};
|
||||
SDL_GetCurrentDisplayMode(0, &mode);
|
||||
posX = mode.w / 2 - window_width / 2;
|
||||
posY = mode.h / 2 - window_height / 2;
|
||||
}
|
||||
|
||||
if (set->fullscreen_is_exclusive) {
|
||||
fullscreen_flag = SDL_WINDOW_FULLSCREEN;
|
||||
}
|
||||
|
||||
// we will unhide the window once the GL context is successfully created
|
||||
Uint32 flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL;
|
||||
|
||||
// if fullscreen was requested, start the window in fullscreen right away
|
||||
if (set->fullscreen) {
|
||||
flags |= fullscreen_flag;
|
||||
fullscreen_state = true;
|
||||
}
|
||||
|
||||
if (set->maximized) {
|
||||
flags |= SDL_WINDOW_MAXIMIZED;
|
||||
maximized_state = true;
|
||||
}
|
||||
|
||||
#ifdef SDL_WINDOW_ALLOW_HIGHDPI
|
||||
if (set->allow_hidpi) {
|
||||
flags |= SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
}
|
||||
#endif
|
||||
|
||||
// ideally we need 3.0 compat
|
||||
// if that doesn't work, try 3.2 core in case we're on mac, 2.1 compat as a last resort
|
||||
static u32 glver[][3] = {
|
||||
{ 0, 0, 0 }, // for command line override
|
||||
{ 3, 0, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY }, // 3.0: default, has all the features required
|
||||
{ 4, 1, SDL_GL_CONTEXT_PROFILE_CORE }, // 4.1core: macs only have core profile and this is the latest
|
||||
{ 3, 2, SDL_GL_CONTEXT_PROFILE_CORE }, // 3.2core: older macs will only have this at best
|
||||
{ 3, 0, SDL_GL_CONTEXT_PROFILE_ES }, // es3: don't really support ES properly, but we can try
|
||||
{ 2, 1, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY }, // 2.1: absolute last resort, will still require GLSL130 as an extension
|
||||
};
|
||||
|
||||
u32 verstart = 1;
|
||||
const u32 verend = sizeof(glver) / sizeof(*glver);
|
||||
const char *verstr = sysArgGetString("--gl-version");
|
||||
if (verstr && *verstr) {
|
||||
// user override
|
||||
glver[0][2] = strstr(verstr, "core") ? SDL_GL_CONTEXT_PROFILE_CORE :
|
||||
(strstr(verstr, "es") ? SDL_GL_CONTEXT_PROFILE_ES :
|
||||
SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
|
||||
sscanf(verstr, "%d.%d", &glver[0][0], &glver[0][1]);
|
||||
if (glver[0][0] >= 1 && glver[0][0] <= 4 && glver[0][1] < 9) {
|
||||
verstart = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ctx = NULL;
|
||||
u32 vmin = 0, vmaj = 0, vprof = SDL_GL_CONTEXT_PROFILE_COMPATIBILITY;
|
||||
const char *vprofstr = "";
|
||||
for (u32 i = verstart; i < verend && !ctx; ++i) {
|
||||
vmaj = glver[i][0];
|
||||
vmin = glver[i][1];
|
||||
vprof = glver[i][2];
|
||||
vprofstr = (vprof == SDL_GL_CONTEXT_PROFILE_CORE ? "core" :
|
||||
(vprof == SDL_GL_CONTEXT_PROFILE_ES ? "es" : ""));
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, vmaj);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, vmin);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, vprof);
|
||||
|
||||
wnd = SDL_CreateWindow(set->title, posX, posY, window_width, window_height, flags);
|
||||
if (!wnd) {
|
||||
sysLogPrintf(LOG_WARNING, "SDL: could not open SDL window for GL%d.%d%s:\n%s", vmaj, vmin, vprofstr, SDL_GetError());
|
||||
continue;
|
||||
}
|
||||
|
||||
ctx = SDL_GL_CreateContext(wnd);
|
||||
if (!ctx) {
|
||||
sysLogPrintf(LOG_WARNING, "SDL: could not create GL%d.%d%s context: %s", vmaj, vmin, vprofstr, SDL_GetError());
|
||||
SDL_DestroyWindow(wnd);
|
||||
wnd = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!wnd || !ctx) {
|
||||
sysFatalError("Could not open SDL window with an OpenGL context of any supported version:\n%s", SDL_GetError());
|
||||
} else {
|
||||
sysLogPrintf(LOG_NOTE, "SDL: created GL%d.%d%s context", vmaj, vmin, vprofstr);
|
||||
}
|
||||
|
||||
SDL_GL_MakeCurrent(wnd, ctx);
|
||||
SDL_GL_SetSwapInterval(1);
|
||||
|
||||
SDL_ShowWindow(wnd);
|
||||
|
||||
qpc_freq = SDL_GetPerformanceFrequency();
|
||||
}
|
||||
|
||||
static void gfx_sdl_close(void) {
|
||||
is_running = false;
|
||||
}
|
||||
|
||||
static void gfx_sdl_set_fullscreen_changed_callback(void (*on_fullscreen_changed)(bool is_now_fullscreen)) {
|
||||
on_fullscreen_changed_callback = on_fullscreen_changed;
|
||||
}
|
||||
|
||||
static void gfx_sdl_set_fullscreen(bool enable) {
|
||||
set_fullscreen(enable, true);
|
||||
}
|
||||
|
||||
static void gfx_sdl_set_fullscreen_exclusive(bool enable) {
|
||||
const uint32_t newflag = enable ? SDL_WINDOW_FULLSCREEN : SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
if (fullscreen_flag != newflag) {
|
||||
fullscreen_flag = newflag;
|
||||
// reset fullscreen to take new value into account if it already is in fullscreen
|
||||
if (fullscreen_state) {
|
||||
fullscreen_state = false;
|
||||
set_fullscreen(enable, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void gfx_sdl_set_maximize_window(bool enable) {
|
||||
set_maximize_window(enable);
|
||||
}
|
||||
|
||||
static void gfx_sdl_set_cursor_visibility(bool visible) {
|
||||
if (visible) {
|
||||
SDL_ShowCursor(SDL_ENABLE);
|
||||
} else {
|
||||
SDL_ShowCursor(SDL_DISABLE);
|
||||
}
|
||||
}
|
||||
|
||||
static void get_centered_positions_native(int32_t width, int32_t height, int32_t *posX, int32_t *posY) {
|
||||
const int disp_idx = SDL_GetWindowDisplayIndex(wnd);
|
||||
SDL_DisplayMode mode = {};
|
||||
SDL_GetDesktopDisplayMode(disp_idx, &mode);
|
||||
*posX = mode.w / 2 - width / 2;
|
||||
*posY = mode.h / 2 - height / 2;
|
||||
}
|
||||
|
||||
static void gfx_sdl_get_centered_positions(int32_t width, int32_t height, int32_t *posX, int32_t *posY) {
|
||||
const int disp_idx = SDL_GetWindowDisplayIndex(wnd);
|
||||
SDL_DisplayMode mode = {};
|
||||
SDL_GetCurrentDisplayMode(disp_idx, &mode);
|
||||
*posX = mode.w / 2 - width / 2;
|
||||
*posY = mode.h / 2 - height / 2;
|
||||
}
|
||||
|
||||
static void gfx_sdl_set_closest_resolution(int32_t width, int32_t height, bool should_center) {
|
||||
const SDL_DisplayMode mode = {.w = width, .h = height};
|
||||
const int disp_idx = SDL_GetWindowDisplayIndex(wnd);
|
||||
SDL_DisplayMode closest = {};
|
||||
if (SDL_GetClosestDisplayMode(disp_idx, &mode, &closest)) {
|
||||
SDL_SetWindowDisplayMode(wnd, &closest);
|
||||
SDL_SetWindowSize(wnd, closest.w, closest.h);
|
||||
if (should_center) {
|
||||
int32_t posX = 0;
|
||||
int32_t posY = 0;
|
||||
get_centered_positions_native(closest.w, closest.h, &posX, &posY);
|
||||
SDL_SetWindowPosition(wnd, posX, posY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void gfx_sdl_set_dimensions(uint32_t width, uint32_t height, int32_t posX, int32_t posY) {
|
||||
SDL_SetWindowSize(wnd, width, height);
|
||||
SDL_SetWindowPosition(wnd, posX, posY);
|
||||
}
|
||||
|
||||
static void gfx_sdl_get_dimensions(uint32_t* width, uint32_t* height, int32_t* posX, int32_t* posY) {
|
||||
SDL_GL_GetDrawableSize(wnd, static_cast<int*>((void*)width), static_cast<int*>((void*)height));
|
||||
SDL_GetWindowPosition(wnd, static_cast<int*>(posX), static_cast<int*>(posY));
|
||||
}
|
||||
|
||||
static void gfx_sdl_handle_events(void) {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_KEYDOWN:
|
||||
if (event.key.keysym.sym == SDLK_RETURN && (event.key.keysym.mod & KMOD_ALT)) {
|
||||
// alt-enter received, switch fullscreen state
|
||||
set_fullscreen(!fullscreen_state, true);
|
||||
} else if (event.key.keysym.sym == SDLK_ESCAPE) {
|
||||
// GE's own menus use the keyboard; ESC on an empty event
|
||||
// queue is our quit key (matches the Phase 1 demo loop).
|
||||
exit(0);
|
||||
}
|
||||
break;
|
||||
case SDL_WINDOWEVENT:
|
||||
if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||
SDL_GL_GetDrawableSize(wnd, &window_width, &window_height);
|
||||
if (!fullscreen_state) {
|
||||
maximized_state = SDL_GetWindowFlags(wnd) & SDL_WINDOW_MAXIMIZED ? true : false;
|
||||
}
|
||||
} else if (event.window.event == SDL_WINDOWEVENT_CLOSE &&
|
||||
event.window.windowID == SDL_GetWindowID(wnd)) {
|
||||
// We listen specifically for main window close because closing main window
|
||||
// on macOS does not trigger SDL_Quit.
|
||||
exit(0);
|
||||
}
|
||||
break;
|
||||
case SDL_QUIT:
|
||||
exit(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* The window/context are created on the host main thread, but the game's
|
||||
* scheduler thread (shedThread) does all rendering. Make the context current
|
||||
* there before each frame. extern "C": called from C (port/src/video.c). */
|
||||
extern "C" void gfx_sdl_make_context_current(void) {
|
||||
if (wnd && ctx) {
|
||||
SDL_GL_MakeCurrent(wnd, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/* The host thread owns SDL event pumping (see videoPumpEvents); when it sees
|
||||
* a window resize it calls this to refresh the cached drawable size that
|
||||
* fast3d uses for scaling. */
|
||||
extern "C" void gfx_sdl_update_cached_size(void) {
|
||||
if (wnd) {
|
||||
SDL_GL_GetDrawableSize(wnd, &window_width, &window_height);
|
||||
}
|
||||
}
|
||||
|
||||
static bool gfx_sdl_start_frame(void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint64_t qpc_to_100ns(uint64_t qpc) {
|
||||
return qpc / qpc_freq * 10000000 + qpc % qpc_freq * 10000000 / qpc_freq;
|
||||
}
|
||||
|
||||
static inline void sync_framerate_with_timer(void) {
|
||||
uint64_t t;
|
||||
t = qpc_to_100ns(SDL_GetPerformanceCounter());
|
||||
|
||||
const int64_t next = previous_time + 10 * FRAME_INTERVAL_US_NUMERATOR / FRAME_INTERVAL_US_DENOMINATOR;
|
||||
int64_t left = next - t;
|
||||
// We want to exit a bit early, so we can busy-wait the rest to never miss the deadline
|
||||
left -= 15000UL;
|
||||
if (left > 0) {
|
||||
sysSleep(left);
|
||||
}
|
||||
|
||||
do {
|
||||
sysCpuRelax();
|
||||
t = qpc_to_100ns(SDL_GetPerformanceCounter());
|
||||
} while ((int64_t)t < next);
|
||||
|
||||
t = qpc_to_100ns(SDL_GetPerformanceCounter());
|
||||
if (left > 0 && t - next < 10000) {
|
||||
// In case it takes some time for the application to wake up after sleep,
|
||||
// or inaccurate timer,
|
||||
// don't let that slow down the framerate.
|
||||
t = next;
|
||||
}
|
||||
previous_time = t;
|
||||
}
|
||||
|
||||
static void gfx_sdl_swap_buffers_begin(void) {
|
||||
if (target_fps) {
|
||||
sync_framerate_with_timer();
|
||||
}
|
||||
SDL_GL_SwapWindow(wnd);
|
||||
}
|
||||
|
||||
static void gfx_sdl_swap_buffers_end(void) {
|
||||
|
||||
}
|
||||
|
||||
static double gfx_sdl_get_time(void) {
|
||||
return SDL_GetPerformanceCounter() / (double)qpc_freq;
|
||||
}
|
||||
|
||||
static int32_t gfx_sdl_get_target_fps(void) {
|
||||
return target_fps;
|
||||
}
|
||||
|
||||
static void gfx_sdl_set_target_fps(int fps) {
|
||||
target_fps = fps;
|
||||
}
|
||||
|
||||
static bool gfx_sdl_can_disable_vsync(void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static void *gfx_sdl_get_window_handle(void) {
|
||||
return (void *)wnd;
|
||||
}
|
||||
|
||||
static void gfx_sdl_set_window_title(const char *title) {
|
||||
SDL_SetWindowTitle(wnd, title);
|
||||
}
|
||||
|
||||
static int gfx_sdl_get_swap_interval(void) {
|
||||
return SDL_GL_GetSwapInterval();
|
||||
}
|
||||
|
||||
static bool gfx_sdl_set_swap_interval(int interval) {
|
||||
const bool success = SDL_GL_SetSwapInterval(interval) >= 0;
|
||||
vsync_enabled = success && (interval != 0);
|
||||
if (!success) {
|
||||
sysLogPrintf(LOG_WARNING, "SDL: failed to set vsync %d: %s", interval, SDL_GetError());
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
int gfx_sdl_get_display_mode(int modenum, int *out_w, int *out_h) {
|
||||
const int display_in_use = SDL_GetWindowDisplayIndex(wnd);
|
||||
SDL_DisplayMode sdlmode;
|
||||
if (SDL_GetDisplayMode(display_in_use, modenum, &sdlmode) == 0) {
|
||||
*out_w = sdlmode.w;
|
||||
*out_h = sdlmode.h;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int gfx_sdl_get_current_display_mode(int *out_w, int *out_h) {
|
||||
const int display_in_use = SDL_GetWindowDisplayIndex(wnd);
|
||||
SDL_DisplayMode sdlmode;
|
||||
if (SDL_GetCurrentDisplayMode(display_in_use, &sdlmode) == 0) {
|
||||
*out_w = sdlmode.w;
|
||||
*out_h = sdlmode.h;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int gfx_sdl_get_num_display_modes(void) {
|
||||
const int display_in_use = SDL_GetWindowDisplayIndex(wnd);
|
||||
return SDL_GetNumDisplayModes(display_in_use);
|
||||
}
|
||||
|
||||
struct GfxWindowManagerAPI gfx_sdl = {
|
||||
gfx_sdl_init,
|
||||
gfx_sdl_close,
|
||||
gfx_sdl_get_display_mode,
|
||||
gfx_sdl_get_current_display_mode,
|
||||
gfx_sdl_get_num_display_modes,
|
||||
gfx_sdl_get_fullscreen_state,
|
||||
gfx_sdl_set_fullscreen_changed_callback,
|
||||
gfx_sdl_set_fullscreen,
|
||||
gfx_sdl_set_fullscreen_exclusive,
|
||||
gfx_sdl_set_fullscreen_flag,
|
||||
gfx_sdl_get_fullscreen_flag_mode,
|
||||
gfx_sdl_get_maximized_state,
|
||||
gfx_sdl_set_maximize_window,
|
||||
gfx_sdl_get_active_window_refresh_rate,
|
||||
gfx_sdl_set_cursor_visibility,
|
||||
gfx_sdl_set_closest_resolution,
|
||||
gfx_sdl_set_dimensions,
|
||||
gfx_sdl_get_dimensions,
|
||||
gfx_sdl_get_centered_positions,
|
||||
gfx_sdl_handle_events,
|
||||
gfx_sdl_start_frame,
|
||||
gfx_sdl_swap_buffers_begin,
|
||||
gfx_sdl_swap_buffers_end,
|
||||
gfx_sdl_get_time,
|
||||
gfx_sdl_get_target_fps,
|
||||
gfx_sdl_set_target_fps,
|
||||
gfx_sdl_can_disable_vsync,
|
||||
gfx_sdl_get_window_handle,
|
||||
gfx_sdl_set_window_title,
|
||||
gfx_sdl_get_swap_interval,
|
||||
gfx_sdl_set_swap_interval,
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef GFX_WINDOW_MANAGER_API_H
|
||||
#define GFX_WINDOW_MANAGER_API_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
struct GfxWindowInitSettings {
|
||||
const char *title;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
bool fullscreen;
|
||||
bool fullscreen_is_exclusive;
|
||||
bool maximized;
|
||||
bool centered;
|
||||
bool allow_hidpi;
|
||||
};
|
||||
|
||||
struct GfxWindowManagerAPI {
|
||||
void (*init)(const struct GfxWindowInitSettings *settings);
|
||||
void (*close)(void);
|
||||
int (*get_display_mode)(int modenum, int *out_w, int *out_h);
|
||||
int (*get_current_display_mode)(int *out_w, int *out_h);
|
||||
int (*get_num_display_modes)(void);
|
||||
int32_t (*get_fullscreen_state)(void);
|
||||
void (*set_fullscreen_changed_callback)(void (*on_fullscreen_changed)(bool is_now_fullscreen));
|
||||
void (*set_fullscreen)(bool enable);
|
||||
void (*set_fullscreen_exclusive)(bool exc);
|
||||
void (*set_fullscreen_flag)(int32_t mode);
|
||||
int32_t (*get_fullscreen_flag_mode)(void);
|
||||
int32_t (*get_maximized_state)(void);
|
||||
void (*set_maximize)(bool enable);
|
||||
void (*get_active_window_refresh_rate)(uint32_t* refresh_rate);
|
||||
void (*set_cursor_visibility)(bool visible);
|
||||
void (*set_closest_resolution)(int32_t width, int32_t height, bool should_center);
|
||||
void (*set_dimensions)(uint32_t width, uint32_t height, int32_t posX, int32_t posY);
|
||||
void (*get_dimensions)(uint32_t* width, uint32_t* height, int32_t* posX, int32_t* posY);
|
||||
void (*get_centered_positions)(int32_t width, int32_t height, int32_t *posX, int32_t *posY);
|
||||
void (*handle_events)(void);
|
||||
bool (*start_frame)(void);
|
||||
void (*swap_buffers_begin)(void);
|
||||
void (*swap_buffers_end)(void);
|
||||
double (*get_time)(void); // For debug
|
||||
int32_t (*get_target_fps)(void);
|
||||
void (*set_target_fps)(int fps);
|
||||
bool (*can_disable_vsync)(void);
|
||||
void *(*get_window_handle)(void);
|
||||
void (*set_window_title)(const char *);
|
||||
int (*get_swap_interval)(void);
|
||||
bool (*set_swap_interval)(int);
|
||||
};
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,311 @@
|
||||
#ifndef __khrplatform_h_
|
||||
#define __khrplatform_h_
|
||||
|
||||
/*
|
||||
** Copyright (c) 2008-2018 The Khronos Group Inc.
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person obtaining a
|
||||
** copy of this software and/or associated documentation files (the
|
||||
** "Materials"), to deal in the Materials without restriction, including
|
||||
** without limitation the rights to use, copy, modify, merge, publish,
|
||||
** distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
** permit persons to whom the Materials are furnished to do so, subject to
|
||||
** the following conditions:
|
||||
**
|
||||
** The above copyright notice and this permission notice shall be included
|
||||
** in all copies or substantial portions of the Materials.
|
||||
**
|
||||
** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
*/
|
||||
|
||||
/* Khronos platform-specific types and definitions.
|
||||
*
|
||||
* The master copy of khrplatform.h is maintained in the Khronos EGL
|
||||
* Registry repository at https://github.com/KhronosGroup/EGL-Registry
|
||||
* The last semantic modification to khrplatform.h was at commit ID:
|
||||
* 67a3e0864c2d75ea5287b9f3d2eb74a745936692
|
||||
*
|
||||
* Adopters may modify this file to suit their platform. Adopters are
|
||||
* encouraged to submit platform specific modifications to the Khronos
|
||||
* group so that they can be included in future versions of this file.
|
||||
* Please submit changes by filing pull requests or issues on
|
||||
* the EGL Registry repository linked above.
|
||||
*
|
||||
*
|
||||
* See the Implementer's Guidelines for information about where this file
|
||||
* should be located on your system and for more details of its use:
|
||||
* http://www.khronos.org/registry/implementers_guide.pdf
|
||||
*
|
||||
* This file should be included as
|
||||
* #include <KHR/khrplatform.h>
|
||||
* by Khronos client API header files that use its types and defines.
|
||||
*
|
||||
* The types in khrplatform.h should only be used to define API-specific types.
|
||||
*
|
||||
* Types defined in khrplatform.h:
|
||||
* khronos_int8_t signed 8 bit
|
||||
* khronos_uint8_t unsigned 8 bit
|
||||
* khronos_int16_t signed 16 bit
|
||||
* khronos_uint16_t unsigned 16 bit
|
||||
* khronos_int32_t signed 32 bit
|
||||
* khronos_uint32_t unsigned 32 bit
|
||||
* khronos_int64_t signed 64 bit
|
||||
* khronos_uint64_t unsigned 64 bit
|
||||
* khronos_intptr_t signed same number of bits as a pointer
|
||||
* khronos_uintptr_t unsigned same number of bits as a pointer
|
||||
* khronos_ssize_t signed size
|
||||
* khronos_usize_t unsigned size
|
||||
* khronos_float_t signed 32 bit floating point
|
||||
* khronos_time_ns_t unsigned 64 bit time in nanoseconds
|
||||
* khronos_utime_nanoseconds_t unsigned time interval or absolute time in
|
||||
* nanoseconds
|
||||
* khronos_stime_nanoseconds_t signed time interval in nanoseconds
|
||||
* khronos_boolean_enum_t enumerated boolean type. This should
|
||||
* only be used as a base type when a client API's boolean type is
|
||||
* an enum. Client APIs which use an integer or other type for
|
||||
* booleans cannot use this as the base type for their boolean.
|
||||
*
|
||||
* Tokens defined in khrplatform.h:
|
||||
*
|
||||
* KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values.
|
||||
*
|
||||
* KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0.
|
||||
* KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0.
|
||||
*
|
||||
* Calling convention macros defined in this file:
|
||||
* KHRONOS_APICALL
|
||||
* KHRONOS_APIENTRY
|
||||
* KHRONOS_APIATTRIBUTES
|
||||
*
|
||||
* These may be used in function prototypes as:
|
||||
*
|
||||
* KHRONOS_APICALL void KHRONOS_APIENTRY funcname(
|
||||
* int arg1,
|
||||
* int arg2) KHRONOS_APIATTRIBUTES;
|
||||
*/
|
||||
|
||||
#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)
|
||||
# define KHRONOS_STATIC 1
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
* Definition of KHRONOS_APICALL
|
||||
*-------------------------------------------------------------------------
|
||||
* This precedes the return type of the function in the function prototype.
|
||||
*/
|
||||
#if defined(KHRONOS_STATIC)
|
||||
/* If the preprocessor constant KHRONOS_STATIC is defined, make the
|
||||
* header compatible with static linking. */
|
||||
# define KHRONOS_APICALL
|
||||
#elif defined(_WIN32)
|
||||
# define KHRONOS_APICALL __declspec(dllimport)
|
||||
#elif defined (__SYMBIAN32__)
|
||||
# define KHRONOS_APICALL IMPORT_C
|
||||
#elif defined(__ANDROID__)
|
||||
# define KHRONOS_APICALL __attribute__((visibility("default")))
|
||||
#else
|
||||
# define KHRONOS_APICALL
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
* Definition of KHRONOS_APIENTRY
|
||||
*-------------------------------------------------------------------------
|
||||
* This follows the return type of the function and precedes the function
|
||||
* name in the function prototype.
|
||||
*/
|
||||
#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__SCITECH_SNAP__)
|
||||
/* Win32 but not WinCE */
|
||||
# define KHRONOS_APIENTRY __stdcall
|
||||
#else
|
||||
# define KHRONOS_APIENTRY
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
* Definition of KHRONOS_APIATTRIBUTES
|
||||
*-------------------------------------------------------------------------
|
||||
* This follows the closing parenthesis of the function prototype arguments.
|
||||
*/
|
||||
#if defined (__ARMCC_2__)
|
||||
#define KHRONOS_APIATTRIBUTES __softfp
|
||||
#else
|
||||
#define KHRONOS_APIATTRIBUTES
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
* basic type definitions
|
||||
*-----------------------------------------------------------------------*/
|
||||
#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)
|
||||
|
||||
|
||||
/*
|
||||
* Using <stdint.h>
|
||||
*/
|
||||
#include <stdint.h>
|
||||
typedef int32_t khronos_int32_t;
|
||||
typedef uint32_t khronos_uint32_t;
|
||||
typedef int64_t khronos_int64_t;
|
||||
typedef uint64_t khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
/*
|
||||
* To support platform where unsigned long cannot be used interchangeably with
|
||||
* inptr_t (e.g. CHERI-extended ISAs), we can use the stdint.h intptr_t.
|
||||
* Ideally, we could just use (u)intptr_t everywhere, but this could result in
|
||||
* ABI breakage if khronos_uintptr_t is changed from unsigned long to
|
||||
* unsigned long long or similar (this results in different C++ name mangling).
|
||||
* To avoid changes for existing platforms, we restrict usage of intptr_t to
|
||||
* platforms where the size of a pointer is larger than the size of long.
|
||||
*/
|
||||
#if defined(__SIZEOF_LONG__) && defined(__SIZEOF_POINTER__)
|
||||
#if __SIZEOF_POINTER__ > __SIZEOF_LONG__
|
||||
#define KHRONOS_USE_INTPTR_T
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#elif defined(__VMS ) || defined(__sgi)
|
||||
|
||||
/*
|
||||
* Using <inttypes.h>
|
||||
*/
|
||||
#include <inttypes.h>
|
||||
typedef int32_t khronos_int32_t;
|
||||
typedef uint32_t khronos_uint32_t;
|
||||
typedef int64_t khronos_int64_t;
|
||||
typedef uint64_t khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
|
||||
|
||||
/*
|
||||
* Win32
|
||||
*/
|
||||
typedef __int32 khronos_int32_t;
|
||||
typedef unsigned __int32 khronos_uint32_t;
|
||||
typedef __int64 khronos_int64_t;
|
||||
typedef unsigned __int64 khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif defined(__sun__) || defined(__digital__)
|
||||
|
||||
/*
|
||||
* Sun or Digital
|
||||
*/
|
||||
typedef int khronos_int32_t;
|
||||
typedef unsigned int khronos_uint32_t;
|
||||
#if defined(__arch64__) || defined(_LP64)
|
||||
typedef long int khronos_int64_t;
|
||||
typedef unsigned long int khronos_uint64_t;
|
||||
#else
|
||||
typedef long long int khronos_int64_t;
|
||||
typedef unsigned long long int khronos_uint64_t;
|
||||
#endif /* __arch64__ */
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif 0
|
||||
|
||||
/*
|
||||
* Hypothetical platform with no float or int64 support
|
||||
*/
|
||||
typedef int khronos_int32_t;
|
||||
typedef unsigned int khronos_uint32_t;
|
||||
#define KHRONOS_SUPPORT_INT64 0
|
||||
#define KHRONOS_SUPPORT_FLOAT 0
|
||||
|
||||
#else
|
||||
|
||||
/*
|
||||
* Generic fallback
|
||||
*/
|
||||
#include <stdint.h>
|
||||
typedef int32_t khronos_int32_t;
|
||||
typedef uint32_t khronos_uint32_t;
|
||||
typedef int64_t khronos_int64_t;
|
||||
typedef uint64_t khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
/*
|
||||
* Types that are (so far) the same on all platforms
|
||||
*/
|
||||
typedef signed char khronos_int8_t;
|
||||
typedef unsigned char khronos_uint8_t;
|
||||
typedef signed short int khronos_int16_t;
|
||||
typedef unsigned short int khronos_uint16_t;
|
||||
|
||||
/*
|
||||
* Types that differ between LLP64 and LP64 architectures - in LLP64,
|
||||
* pointers are 64 bits, but 'long' is still 32 bits. Win64 appears
|
||||
* to be the only LLP64 architecture in current use.
|
||||
*/
|
||||
#ifdef KHRONOS_USE_INTPTR_T
|
||||
typedef intptr_t khronos_intptr_t;
|
||||
typedef uintptr_t khronos_uintptr_t;
|
||||
#elif defined(_WIN64)
|
||||
typedef signed long long int khronos_intptr_t;
|
||||
typedef unsigned long long int khronos_uintptr_t;
|
||||
#else
|
||||
typedef signed long int khronos_intptr_t;
|
||||
typedef unsigned long int khronos_uintptr_t;
|
||||
#endif
|
||||
|
||||
#if defined(_WIN64)
|
||||
typedef signed long long int khronos_ssize_t;
|
||||
typedef unsigned long long int khronos_usize_t;
|
||||
#else
|
||||
typedef signed long int khronos_ssize_t;
|
||||
typedef unsigned long int khronos_usize_t;
|
||||
#endif
|
||||
|
||||
#if KHRONOS_SUPPORT_FLOAT
|
||||
/*
|
||||
* Float type
|
||||
*/
|
||||
typedef float khronos_float_t;
|
||||
#endif
|
||||
|
||||
#if KHRONOS_SUPPORT_INT64
|
||||
/* Time types
|
||||
*
|
||||
* These types can be used to represent a time interval in nanoseconds or
|
||||
* an absolute Unadjusted System Time. Unadjusted System Time is the number
|
||||
* of nanoseconds since some arbitrary system event (e.g. since the last
|
||||
* time the system booted). The Unadjusted System Time is an unsigned
|
||||
* 64 bit value that wraps back to 0 every 584 years. Time intervals
|
||||
* may be either signed or unsigned.
|
||||
*/
|
||||
typedef khronos_uint64_t khronos_utime_nanoseconds_t;
|
||||
typedef khronos_int64_t khronos_stime_nanoseconds_t;
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Dummy value used to pad enum types to 32 bits.
|
||||
*/
|
||||
#ifndef KHRONOS_MAX_ENUM
|
||||
#define KHRONOS_MAX_ENUM 0x7FFFFFFF
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Enumerated boolean type
|
||||
*
|
||||
* Values other than zero should be considered to be true. Therefore
|
||||
* comparisons should not be made against KHRONOS_TRUE.
|
||||
*/
|
||||
typedef enum {
|
||||
KHRONOS_FALSE = 0,
|
||||
KHRONOS_TRUE = 1,
|
||||
KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
|
||||
} khronos_boolean_enum_t;
|
||||
|
||||
#endif /* __khrplatform_h_ */
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef PORT_CRASH_H
|
||||
#define PORT_CRASH_H
|
||||
|
||||
/*
|
||||
* Crash handler: installs an unhandled-exception filter (Windows) or signal
|
||||
* handlers (POSIX) that print a symbolicated backtrace and write
|
||||
* ge007.crash.log, then abort. Call crashInit() before running game code.
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void crashInit(void);
|
||||
void crashShutdown(void);
|
||||
|
||||
/* Unwind every thread in the process and log a short symbolicated
|
||||
* backtrace for each (labelled via tids/names when known). For diagnosing
|
||||
* hangs: call from the kernel heartbeat. */
|
||||
void crashDumpThreads(const unsigned long *tids, const char **names, int count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PORT_CRASH_H */
|
||||
@@ -0,0 +1,13 @@
|
||||
#ifndef PORT_DRAM_H
|
||||
#define PORT_DRAM_H
|
||||
|
||||
/*
|
||||
* Reserve the N64-DRAM region: one 8 MB backing store mapped at BOTH
|
||||
* 0x70000000 (s32-safe "virtual" view, where game RAM symbols live) and
|
||||
* 0x80000000 (KSEG0 mirror). See port/src/dram.c. Must be called before any
|
||||
* game thread runs; the absolute symbols in port/src/dram_syms.s point into
|
||||
* the V1 view.
|
||||
*/
|
||||
void *dramReserve(void);
|
||||
|
||||
#endif /* PORT_DRAM_H */
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef PORT_HOSTSTDDDEF_H
|
||||
#define PORT_HOSTSTDDDEF_H
|
||||
/* Generated by CMake from hoststddef.h.in — do not edit.
|
||||
* Includes the HOST <stddef.h> by absolute path so that port/shim/stddef.h
|
||||
* can bypass the N64 include/stddef.h (an empty stub whose guard otherwise
|
||||
* prevents the host header's max_align_t/size_t from reaching C++ TUs). */
|
||||
#include "@PORT_HOST_STDDDEF_H@"
|
||||
#endif
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef PORT_HOSTSTDLIB_H
|
||||
#define PORT_HOSTSTDLIB_H
|
||||
/* Generated by CMake from hoststdlib.h.in — do not edit.
|
||||
* Includes the HOST <stdlib.h> by absolute path so that port/shim/stdlib.h
|
||||
* can bypass the N64 include/stdlib.h (whose lldiv_t/ldiv_t typedefs clash
|
||||
* with the host's in C++ TUs). See docs/PCPortResearch.md §11. */
|
||||
#include "@PORT_HOST_STDLIB_H@"
|
||||
#endif
|
||||
@@ -31,6 +31,14 @@
|
||||
#define PLATFORM_64BIT 1
|
||||
#endif
|
||||
|
||||
#if defined(_M_IX86) || defined(__i386__)
|
||||
#define PLATFORM_X86 1
|
||||
#elif defined(_M_X64) || defined(__x86_64__)
|
||||
#define PLATFORM_X86_64 1
|
||||
#elif defined(__aarch64__)
|
||||
#define PLATFORM_ARM 1
|
||||
#endif
|
||||
|
||||
/* Constructor attribute: run before main(). */
|
||||
#if defined(_MSC_VER)
|
||||
#define PD_CONSTRUCTOR
|
||||
@@ -44,6 +52,7 @@
|
||||
enum LogLevel {
|
||||
LOG_ERROR = 0,
|
||||
LOG_WARNING,
|
||||
LOG_NOTE,
|
||||
LOG_INFO,
|
||||
LOG_DEBUG,
|
||||
};
|
||||
|
||||
@@ -17,6 +17,12 @@ extern "C" {
|
||||
|
||||
/* Monotonic microseconds since an arbitrary origin. */
|
||||
uint64_t sysGetMicroseconds(void);
|
||||
|
||||
/*
|
||||
* Base address the executable was loaded at (for turning runtime addresses
|
||||
* into file offsets for addr2line; ASLR moves the image on Windows).
|
||||
*/
|
||||
uintptr_t sysImageBase(void);
|
||||
/* Wall-clock seconds since the epoch (for timestamps). */
|
||||
int64_t sysGetTime(void);
|
||||
/* Sleep for the given number of microseconds. */
|
||||
@@ -27,6 +33,13 @@ void sysSleep(uint32_t micros);
|
||||
void sysLogPrintf(enum LogLevel level, const char *fmt, ...)
|
||||
__attribute__((format(printf, 2, 3)));
|
||||
|
||||
/*
|
||||
* Print a fatal error message and terminate the process. Used by fast3d for
|
||||
* unrecoverable conditions (bad Gfx command, OOM in the texture cache, ...).
|
||||
*/
|
||||
void sysFatalError(const char *fmt, ...)
|
||||
__attribute__((format(printf, 1, 2))) __attribute__((noreturn));
|
||||
|
||||
/* --- Paths -------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
@@ -38,10 +51,31 @@ const char *sysResolvePath(const char *path);
|
||||
/* Return the directory containing the executable. */
|
||||
const char *sysGetExeDir(void);
|
||||
|
||||
/* --- Command line ------------------------------------------------------- */
|
||||
|
||||
/* Store the host argv (call once from main). */
|
||||
void sysSetArgs(int argc, char **argv);
|
||||
/* Nonzero if the given argument (e.g. "--debug-gl") was passed. */
|
||||
int sysArgCheck(const char *arg);
|
||||
/* Value following the given argument on the command line, or NULL. */
|
||||
const char *sysArgGetString(const char *arg);
|
||||
|
||||
/* --- CPU ---------------------------------------------------------------- */
|
||||
|
||||
/* Briefly yield/relax the CPU (busy-wait hint). Used by fast3d's frame sync. */
|
||||
void sysCpuRelax(void);
|
||||
|
||||
/* --- Lifecycle ---------------------------------------------------------- */
|
||||
|
||||
void sysExit(int code);
|
||||
|
||||
/*
|
||||
* Start the cooperative thread kernel (green threads + vsync tick). Call
|
||||
* once from main() before running any game code; after this, blocking
|
||||
* osRecvMesg() calls in the game suspend/resume threads.
|
||||
*/
|
||||
void portKernelInit(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+22
-17
@@ -2,14 +2,16 @@
|
||||
#define PORT_VIDEO_H
|
||||
|
||||
/*
|
||||
* Video: SDL2 window + OpenGL context + frame pacing.
|
||||
* Modelled on the PD port's port/include/video.h.
|
||||
* Video: SDL2 window + OpenGL context + frame pacing, on top of fast3d's
|
||||
* window-manager / rendering APIs (port/fast3d).
|
||||
*
|
||||
* 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.
|
||||
* The game's VI (osViSetMode / osViSwapBuffer / ...) is mapped onto this
|
||||
* layer by the libultra shims; the software RSP (fast3d) renders into the GL
|
||||
* context owned here.
|
||||
*/
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include <PR/ultratypes.h>
|
||||
#include <PR/gbi.h>
|
||||
|
||||
@@ -21,24 +23,27 @@ extern "C" {
|
||||
int videoInit(void);
|
||||
void videoDestroy(void);
|
||||
|
||||
/* Frame boundary hooks, driven by the port scheduler (gesched.c). */
|
||||
/* Frame boundary hooks, driven by the SP task shim (libultra.c). These run
|
||||
* on the game's scheduler thread. */
|
||||
void videoStartFrame(void);
|
||||
void videoSubmitCommands(Gfx *cmds); /* runs the software RSP on the list */
|
||||
void videoEndFrame(void);
|
||||
|
||||
/* Pump window events; returns 1 when the app should quit. Used by the
|
||||
* Phase 1 demo loop until the scheduler owns the frame boundary. */
|
||||
int videoHandleEvents(void);
|
||||
/* Host-thread SDL event pump: keeps the window responsive (Windows only
|
||||
* dispatches messages to the creating thread) and handles quit. Called in a
|
||||
* loop from main(). Exits the process on QUIT/ESC/close. */
|
||||
void videoPumpEvents(void);
|
||||
|
||||
/* VSync / framerate control. */
|
||||
void videoSetVsync(int enabled);
|
||||
void videoSetFramerateLimit(int fps); /* 0 = unlimited */
|
||||
/* The game's native video mode (NTSC 640x480, PAL 640x400). fast3d scales
|
||||
* N64 screen coordinates into window pixels using this. */
|
||||
void videoUpdateNativeResolution(s32 w, s32 h);
|
||||
s32 videoGetNativeWidth(void);
|
||||
s32 videoGetNativeHeight(void);
|
||||
|
||||
/* Window state queries (kept in sync with the options menu). */
|
||||
int videoGetFullscreen(void);
|
||||
void videoSetFullscreen(int enabled);
|
||||
int videoGetMaximize(void);
|
||||
void videoSetMaximize(int enabled);
|
||||
/* Offscreen framebuffers (fast3d GL FBOs) + texture cache control. */
|
||||
s32 videoCreateFramebuffer(u32 w, u32 h, s32 upscale, s32 autoresize);
|
||||
void videoCopyFramebuffer(s32 dst, s32 src, s32 left, s32 top);
|
||||
void videoResetTextureCache(void);
|
||||
|
||||
/* Current FPS (measured). */
|
||||
float videoGetFPS(void);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* PC port shim for PR/R4300.h (see docs/PCPortResearch.md).
|
||||
*
|
||||
* On the N64, PHYS_TO_K0 turns a physical address into a KSEG0 virtual
|
||||
* address by OR'ing in 0x80000000. On the PC port the game's working RAM
|
||||
* lives in the s32-safe DRAM view at 0x70000000 (see port/src/dram.c):
|
||||
* addresses with bit 31 set would sign-extend to invalid pointers when they
|
||||
* pass through s32 parameters (e.g. mempCheckMemflagTokens in src/memp.c).
|
||||
* The KSEG0 view at 0x80000000 still exists as a byte-identical mirror for
|
||||
* code that rebuilds pointers with `offset | 0x80000000`, and "physical"
|
||||
* addresses are offsets from the V1 base (see the PR/os.h shim).
|
||||
*
|
||||
* So PHYS_TO_K0 is the identity here: its argument is already a live host
|
||||
* pointer in the V1 view.
|
||||
*
|
||||
* Inert in the N64 build (no -DPORT): pure pass-through.
|
||||
*/
|
||||
#ifndef _PORT_SHIM_R4300_H_
|
||||
#define _PORT_SHIM_R4300_H_
|
||||
|
||||
#if defined(PORT)
|
||||
# include "include/PR/R4300.h"
|
||||
|
||||
# undef PHYS_TO_K0
|
||||
# define PHYS_TO_K0(x) ((u32)(x)) /* identity: V1 view is already virtual */
|
||||
|
||||
#else
|
||||
# include <PR/R4300.h>
|
||||
#endif
|
||||
|
||||
#endif /* _PORT_SHIM_R4300_H_ */
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* PC port shim for PR/os.h — DRAM address conversions (see docs/PCPortResearch.md).
|
||||
*
|
||||
* The game's working RAM lives in the s32-safe DRAM view at 0x70000000
|
||||
* ("V1"), with a byte-identical KSEG0 mirror at 0x80000000 ("V2"); see
|
||||
* port/src/dram.c. Addresses with bit 31 set sign-extend to invalid
|
||||
* pointers through s32 parameters (src/memp.c), so live pointers must stay
|
||||
* in V1, while `offset | 0x80000000` rebuilds (src/game/bg.c) must land in
|
||||
* V2. The two views share one backing store, so both are the same data.
|
||||
*
|
||||
* "Physical" addresses in GBI words and ROM data are therefore OFFSETS FROM
|
||||
* THE V1 BASE, and the conversions become:
|
||||
*
|
||||
* OS_K0_TO_PHYSICAL(x) = (u32)((char *)x - 0x70000000)
|
||||
* small offset P; fast3d's seg_addr() resolves it with +0x80000000,
|
||||
* landing in V2. `P | 0x80000000` (bg.c et al.) also lands in V2.
|
||||
* OS_PHYSICAL_TO_K0(x) = x (identity)
|
||||
* Callers pass EITHER live V1 pointers (bondview2.c, front.c) — which
|
||||
* then go into GBI words as full addresses that fast3d passes through
|
||||
* — OR small physical offsets, which fast3d remaps to V2. Both forms
|
||||
* resolve correctly.
|
||||
* osVirtualToPhysical / osPhysicalToVirtual stay the identity (port/src/
|
||||
* libultra.c): V1 addresses are already live host pointers < 4 GB.
|
||||
*
|
||||
* Inert in the N64 build (no -DPORT): pure pass-through.
|
||||
*/
|
||||
#ifndef _PORT_SHIM_OS_H_
|
||||
#define _PORT_SHIM_OS_H_
|
||||
|
||||
#if defined(PORT)
|
||||
# include "include/PR/os.h"
|
||||
|
||||
# undef OS_K0_TO_PHYSICAL
|
||||
# define OS_K0_TO_PHYSICAL(x) ((u32)((char *)(x) - 0x70000000))
|
||||
|
||||
# undef OS_PHYSICAL_TO_K0
|
||||
# define OS_PHYSICAL_TO_K0(x) ((void *)(x))
|
||||
|
||||
#else
|
||||
# include <PR/os.h>
|
||||
#endif
|
||||
|
||||
#endif /* _PORT_SHIM_OS_H_ */
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* PC port shim for <stdarg.h>.
|
||||
*
|
||||
* The N64 decomp's include/stdarg.h does `#include <ultra64.h>` (the console
|
||||
* dev-kit convenience: one header pulls in all of libultra). On the PC that
|
||||
* means any HOST header that internally includes <stdarg.h> (SDL_stdinc.h,
|
||||
* parts of MinGW's own headers, ...) drags PR/os.h into the middle of host
|
||||
* header parsing. If MinGW's <errno.h> has already run (it defines the
|
||||
* `errno` macro), PR/os.h's controller structs — which have fields literally
|
||||
* named `errno` — fail to parse ("field '_errno' declared as a function").
|
||||
*
|
||||
* Parse the N64 header with the errno macro suppressed; restore it after.
|
||||
* Inert in the N64 build (port/shim is not on its include path).
|
||||
*/
|
||||
|
||||
#pragma push_macro("errno")
|
||||
#undef errno
|
||||
#include "include/stdarg.h"
|
||||
#pragma pop_macro("errno")
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* PC port shim for <stddef.h>.
|
||||
*
|
||||
* The N64 decomp ships an essentially empty include/stddef.h (just its
|
||||
* `_STDDEF_H_` guard). In C game code that is harmless. In C++ TUs (fast3d)
|
||||
* it shadows the host header, so libstdc++'s <cstddef> never sees the host's
|
||||
* max_align_t/size_t and fails to compile. Route C++ TUs to the host header
|
||||
* by absolute path (generated hoststddef.h — same trick as
|
||||
* port/shim/string.h / port/shim/stdlib.h).
|
||||
*
|
||||
* Inert in the N64 build (port/shim is not on its include path).
|
||||
*/
|
||||
#if defined(__cplusplus)
|
||||
/* MinGW's <stdint.h> does:
|
||||
* #define __need_wint_t
|
||||
* #define __need_wchar_t
|
||||
* #include <stddef.h>
|
||||
* and never undefs the __need_* macros. stddef.h then refuses to run its
|
||||
* main body (which defines max_align_t) for the rest of the TU, breaking
|
||||
* libstdc++'s <cstddef>. Clear the poison before pulling in the host header.
|
||||
*/
|
||||
#undef __need_wchar_t
|
||||
#undef __need_size_t
|
||||
#undef __need_ptrdiff_t
|
||||
#undef __need_NULL
|
||||
#undef __need_wint_t
|
||||
#include "hoststddef.h"
|
||||
#else
|
||||
#include "include/stddef.h"
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* PC port shim for <stdlib.h>.
|
||||
*
|
||||
* The N64 decomp ships include/stdlib.h (a libc stub: lldiv_t/ldiv_t typedefs
|
||||
* + a couple of prototypes). C game code gets it, exactly as on the console.
|
||||
*
|
||||
* In C++ TUs (fast3d) it must NOT be used: MinGW's own headers pull in the
|
||||
* host <stdlib.h> through their include chains (sec_api/stdlib_s.h does
|
||||
* `#include <stdlib.h>`, which resolves to the N64 header via -I), and the
|
||||
* lldiv_t/ldiv_t typedefs then conflict with the host's. The shim routes C++
|
||||
* TUs to the host header by absolute path (generated hoststdlib.h, same
|
||||
* trick as port/shim/string.h — see docs/PCPortResearch.md §11 D12).
|
||||
*
|
||||
* Inert in the N64 build (port/shim is not on its include path).
|
||||
*/
|
||||
#if defined(__cplusplus)
|
||||
#include "hoststdlib.h"
|
||||
#else
|
||||
#include "include/stdlib.h"
|
||||
#endif
|
||||
+408
-14
@@ -1,26 +1,420 @@
|
||||
/*
|
||||
* Crash handler / stack traces.
|
||||
*
|
||||
* Catches signals (SIGSEGV etc.) and prints a backtrace + the game's state,
|
||||
* to make the inevitable porting crashes debuggable.
|
||||
* Catches access violations (Windows) / fatal signals (POSIX) and prints a
|
||||
* backtrace with file:line info (build with -g; MinGW emits PDBs so symbols
|
||||
* resolve), 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.
|
||||
* Adapted from the PD port's port/src/crash.c.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE 1
|
||||
#endif
|
||||
|
||||
#include "platform.h"
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <PR/ultratypes.h>
|
||||
#include "system.h"
|
||||
#include "platform.h"
|
||||
#include "crash.h"
|
||||
|
||||
#define CRASH_LOG_FNAME "ge007.crash.log"
|
||||
#define CRASH_MAX_MSG 8192
|
||||
#define CRASH_MAX_SYM 256
|
||||
#define CRASH_MAX_FRAMES 32
|
||||
#define CRASH_MSG(...) \
|
||||
if (msglen < CRASH_MAX_MSG) msglen += snprintf(msg + msglen, CRASH_MAX_MSG - msglen, __VA_ARGS__)
|
||||
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
|
||||
#include <windows.h>
|
||||
#include <dbghelp.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <inttypes.h>
|
||||
#include <excpt.h>
|
||||
|
||||
static LPTOP_LEVEL_EXCEPTION_FILTER prevExFilter;
|
||||
|
||||
static void *crashGetModuleBase(const void *addr)
|
||||
{
|
||||
HMODULE h = NULL;
|
||||
GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, addr, &h);
|
||||
return (void *)h;
|
||||
}
|
||||
|
||||
/*
|
||||
* Phase 1 — raw crash info, NO dbghelp. Every call here is a plain Win32
|
||||
* query; nothing allocates or walks memory, so this cannot re-fault even if
|
||||
* the heap or a stack is corrupted. Written to console + file BEFORE any
|
||||
* risky symbolication.
|
||||
*/
|
||||
static void crashStackTraceRaw(char *msg, PEXCEPTION_POINTERS exinfo)
|
||||
{
|
||||
CONTEXT context = *exinfo->ContextRecord;
|
||||
DWORD msglen = 0;
|
||||
|
||||
CRASH_MSG("EXCEPTION: 0x%08lx\n", exinfo->ExceptionRecord->ExceptionCode);
|
||||
#if defined(PLATFORM_X86_64)
|
||||
CRASH_MSG("PC: %p (Rip=%p Rsp=%p Rbp=%p)\n",
|
||||
exinfo->ExceptionRecord->ExceptionAddress,
|
||||
(void *)context.Rip, (void *)context.Rsp, (void *)context.Rbp);
|
||||
/* FPU state: for STATUS_FLOATING_POINT_* we want to know whether the
|
||||
* exception masks were actually cleared (MxCsr bits 7..10 / x87 CW
|
||||
* mask bits) or this is a stale status bit. */
|
||||
CRASH_MSG("MxCsr=0x%08lx x87CW=0x%04x x87SW=0x%04x\n",
|
||||
(unsigned long)context.MxCsr,
|
||||
(unsigned)(context.FloatSave.ControlWord & 0xffff),
|
||||
(unsigned)(context.FloatSave.StatusWord & 0xffff));
|
||||
PVOID low = NULL, high = NULL;
|
||||
if (GetCurrentThreadStackLimits(low, high)) {
|
||||
CRASH_MSG("thread stack: %p..%p Rsp=%p used=%llu bytes\n",
|
||||
low, high, (void *)context.Rsp,
|
||||
(unsigned long long)((uintptr_t)high - (uintptr_t)context.Rsp));
|
||||
}
|
||||
#else
|
||||
CRASH_MSG("PC: %p\n", exinfo->ExceptionRecord->ExceptionAddress);
|
||||
#endif
|
||||
CRASH_MSG("MODULE: [%p]\n", crashGetModuleBase(exinfo->ExceptionRecord->ExceptionAddress));
|
||||
CRASH_MSG("MAIN MODULE: [%p]\n", crashGetModuleBase(crashInit));
|
||||
}
|
||||
|
||||
/*
|
||||
* Phase 2 — backtrace by walking the EBP chain manually. The build keeps
|
||||
* frame pointers (-fno-omit-frame-pointer), so every frame is a pair of
|
||||
* qwords at RBP: [saved RBP, return address]. No dbghelp, no allocation:
|
||||
* each pointer is validated against the thread's stack bounds BEFORE being
|
||||
* dereferenced, and the chain must strictly ascend (x64 stacks grow down),
|
||||
* so a corrupted chain terminates instead of re-faulting. Addresses are
|
||||
* symbolicated offline with addr2line when needed.
|
||||
*/
|
||||
static void crashStackTraceSym(char *msg, PEXCEPTION_POINTERS exinfo)
|
||||
{
|
||||
CONTEXT context = *exinfo->ContextRecord;
|
||||
DWORD msglen = 0;
|
||||
|
||||
#if defined(PLATFORM_X86_64)
|
||||
PVOID low = NULL, high = NULL;
|
||||
if (!GetCurrentThreadStackLimits(low, high)) {
|
||||
snprintf(msg, CRASH_MAX_MSG, "\nBACKTRACE: (no stack limits)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
uintptr_t fp = context.Rbp;
|
||||
int i = 0;
|
||||
CRASH_MSG("\nBACKTRACE:\n#00: %p (crash PC)\n", exinfo->ExceptionRecord->ExceptionAddress);
|
||||
|
||||
while (i < CRASH_MAX_FRAMES - 1) {
|
||||
/* The frame record must live inside the committed stack. */
|
||||
if (fp < (uintptr_t)low || fp > (uintptr_t)high) break;
|
||||
|
||||
const uintptr_t *frame = (const uintptr_t *)fp;
|
||||
const uintptr_t saved_fp = frame[0];
|
||||
const uintptr_t ret_addr = frame[1];
|
||||
|
||||
CRASH_MSG("#%02d: %p", i + 1, (void *)ret_addr);
|
||||
const uintptr_t modbase = (uintptr_t)crashGetModuleBase((void *)ret_addr);
|
||||
if (modbase) {
|
||||
CRASH_MSG(" [%p]+%llx", (void *)modbase,
|
||||
(unsigned long long)(ret_addr - modbase));
|
||||
}
|
||||
CRASH_MSG("\n");
|
||||
|
||||
/* Chain must ascend; stop on the first frame that doesn't. */
|
||||
if (saved_fp <= fp || saved_fp > (uintptr_t)high) break;
|
||||
fp = saved_fp;
|
||||
++i;
|
||||
}
|
||||
if (i == CRASH_MAX_FRAMES - 1) {
|
||||
CRASH_MSG("...\n");
|
||||
}
|
||||
#else
|
||||
snprintf(msg, CRASH_MAX_MSG, "\nBACKTRACE: not implemented on this arch\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
static void crashWriteLog(const char *msg, int append)
|
||||
{
|
||||
FILE *f = fopen(CRASH_LOG_FNAME, append ? "ab" : "wb");
|
||||
if (f) {
|
||||
if (!append) fprintf(f, "Crash!\n\n");
|
||||
fprintf(f, "%s", msg);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Thread dump: unwind every thread in this process and print a short
|
||||
* backtrace for each, labelled with the game-thread name when known.
|
||||
* Called from the kernel heartbeat when no frame has rendered for a while,
|
||||
* so a hang shows WHERE every thread is stuck instead of as silence.
|
||||
*
|
||||
* tids/names: parallel arrays (may be NULL/0). Threads not in the list are
|
||||
* labelled by TID only. Uses SuspendThread + GetThreadContext + StackWalk64;
|
||||
* each thread is resumed before the next is touched, so a hang in one walk
|
||||
* cannot wedge the others.
|
||||
*/
|
||||
void crashDumpThreads(const unsigned long *tids, const char **names, int count)
|
||||
{
|
||||
HANDLE process = GetCurrentProcess();
|
||||
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
|
||||
if (snap == INVALID_HANDLE_VALUE) return;
|
||||
|
||||
/* Function symbols only — line tables are slow to load and the dump
|
||||
* prints function+offset, which is enough to find a hang. */
|
||||
SymSetOptions(SymGetOptions() | SYMOPT_DEBUG);
|
||||
SymInitialize(process, NULL, TRUE);
|
||||
|
||||
THREADENTRY32 te;
|
||||
te.dwSize = sizeof(te);
|
||||
if (!Thread32First(snap, &te)) {
|
||||
CloseHandle(snap);
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
if (te.th32OwnerProcessID != GetCurrentProcessId()) continue;
|
||||
|
||||
char label[64] = "?";
|
||||
int matched = 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (tids && tids[i] == te.th32ThreadID) {
|
||||
snprintf(label, sizeof(label), "%s", names ? names[i] : "?");
|
||||
matched = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* When a specific thread list is given, skip everything else —
|
||||
* walking ~20 system threads with symbol resolution is slow and
|
||||
* buries the interesting frames. */
|
||||
if (count > 0 && !matched) continue;
|
||||
|
||||
HANDLE hth = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE, te.th32ThreadID);
|
||||
if (!hth) continue;
|
||||
|
||||
CONTEXT ctx;
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
ctx.ContextFlags = CONTEXT_FULL;
|
||||
DWORD susps = SuspendThread(hth);
|
||||
if (susps != (DWORD)-1 && GetThreadContext(hth, &ctx)) {
|
||||
sysLogPrintf(LOG_ERROR, " [thread %lu '%s']", te.th32ThreadID, label);
|
||||
|
||||
#if defined(PLATFORM_X86_64)
|
||||
STACKFRAME64 sf;
|
||||
memset(&sf, 0, sizeof(sf));
|
||||
sf.AddrPC.Offset = ctx.Rip;
|
||||
sf.AddrPC.Mode = AddrModeFlat;
|
||||
sf.AddrFrame.Offset = ctx.Rbp;
|
||||
sf.AddrFrame.Mode = AddrModeFlat;
|
||||
sf.AddrStack.Offset = ctx.Rsp;
|
||||
sf.AddrStack.Mode = AddrModeFlat;
|
||||
|
||||
char symbuf[sizeof(SYMBOL_INFO) + CRASH_MAX_SYM * sizeof(TCHAR)];
|
||||
PSYMBOL_INFO sym = (PSYMBOL_INFO)symbuf;
|
||||
sym->SizeOfStruct = sizeof(*sym);
|
||||
sym->MaxNameLen = CRASH_MAX_SYM;
|
||||
DWORD64 disp64 = 0;
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (!StackWalk64(IMAGE_FILE_MACHINE_AMD64, process, hth, &sf, &ctx, NULL,
|
||||
SymFunctionTableAccess64, SymGetModuleBase64, NULL))
|
||||
break;
|
||||
if (sym->SizeOfStruct == 0) {
|
||||
sym->SizeOfStruct = sizeof(*sym);
|
||||
sym->MaxNameLen = CRASH_MAX_SYM;
|
||||
}
|
||||
const char *where = "?";
|
||||
if (SymFromAddr(process, sf.AddrPC.Offset, &disp64, sym)) {
|
||||
static char linebuf[CRASH_MAX_FRAMES][512];
|
||||
snprintf(linebuf[i % CRASH_MAX_FRAMES], sizeof(linebuf[0]), "%s+%llx",
|
||||
sym->Name, (unsigned long long)disp64);
|
||||
where = linebuf[i % CRASH_MAX_FRAMES];
|
||||
}
|
||||
sysLogPrintf(LOG_ERROR, " #%d %p %s", i,
|
||||
(void *)(uintptr_t)sf.AddrPC.Offset, where);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
ResumeThread(hth);
|
||||
CloseHandle(hth);
|
||||
} while (Thread32Next(snap, &te));
|
||||
|
||||
CloseHandle(snap);
|
||||
SymCleanup(process);
|
||||
}
|
||||
|
||||
static LONG __stdcall crashHandler(PEXCEPTION_POINTERS exinfo)
|
||||
{
|
||||
char msg[CRASH_MAX_MSG + 1] = { 0 };
|
||||
|
||||
if (IsDebuggerPresent()) {
|
||||
if (prevExFilter) {
|
||||
return prevExFilter(exinfo);
|
||||
}
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
|
||||
/* Phase 1: raw info, guaranteed to survive. */
|
||||
crashStackTraceRaw(msg, exinfo);
|
||||
sysLogPrintf(LOG_ERROR, "FATAL: %s", msg);
|
||||
fflush(stderr);
|
||||
fflush(stdout);
|
||||
crashWriteLog(msg, 0);
|
||||
|
||||
/* Phase 2: EBP-chain backtrace (validated reads only — see
|
||||
* crashStackTraceSym). The raw data is already on disk either way. */
|
||||
{
|
||||
char sym[CRASH_MAX_MSG + 1] = { 0 };
|
||||
crashStackTraceSym(sym, exinfo);
|
||||
if (sym[0]) {
|
||||
sysLogPrintf(LOG_ERROR, "%s", sym);
|
||||
crashWriteLog(sym, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Terminate directly — abort() raises SIGABRT which can re-enter the
|
||||
* exception machinery while we are already inside it. */
|
||||
TerminateProcess(GetCurrentProcess(), exinfo->ExceptionRecord->ExceptionCode);
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
|
||||
#elif defined(PLATFORM_LINUX)
|
||||
|
||||
#include <ucontext.h>
|
||||
#include <signal.h>
|
||||
#include <execinfo.h>
|
||||
#include <unistd.h>
|
||||
#include <ctype.h>
|
||||
#include <dlfcn.h>
|
||||
#include <sys/fcntl.h>
|
||||
|
||||
static struct sigaction prevSigAction;
|
||||
|
||||
static void *crashGetModuleBase(const void *addr)
|
||||
{
|
||||
Dl_info info;
|
||||
if (dladdr(addr, &info)) {
|
||||
return info.dli_fbase;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void crashStackTrace(char *msg, int sig, void *pc)
|
||||
{
|
||||
unsigned msglen = 0;
|
||||
void *frames[CRASH_MAX_FRAMES] = { NULL };
|
||||
|
||||
const int nframes = backtrace(frames, CRASH_MAX_FRAMES);
|
||||
if (nframes <= 0) {
|
||||
CRASH_MSG("no information\n");
|
||||
return;
|
||||
}
|
||||
|
||||
char **strings = backtrace_symbols(frames, nframes);
|
||||
|
||||
CRASH_MSG("SIGNAL: %d\n", sig);
|
||||
CRASH_MSG("PC: ");
|
||||
if (pc) {
|
||||
CRASH_MSG("%p\n", pc);
|
||||
} else if (strings) {
|
||||
CRASH_MSG("%s\n", strings[0]);
|
||||
} else {
|
||||
CRASH_MSG("%p\n", frames[0]);
|
||||
}
|
||||
|
||||
CRASH_MSG("MODULE: %p\n", crashGetModuleBase(frames[0]));
|
||||
CRASH_MSG("MAIN MODULE: %p\n", crashGetModuleBase(crashInit));
|
||||
CRASH_MSG("\nBACKTRACE:\n");
|
||||
|
||||
int i;
|
||||
for (i = 0; i < nframes; ++i) {
|
||||
CRASH_MSG("#%02d: ", i);
|
||||
if (strings && strings[i]) {
|
||||
CRASH_MSG("%s\n", strings[i]);
|
||||
} else {
|
||||
CRASH_MSG("%p\n", frames[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (i == CRASH_MAX_FRAMES) {
|
||||
CRASH_MSG("...\n");
|
||||
} else if (i <= 1) {
|
||||
CRASH_MSG("no information\n");
|
||||
}
|
||||
|
||||
free(strings);
|
||||
}
|
||||
|
||||
static void crashHandler(int sig, siginfo_t *siginfo, void *ctx)
|
||||
{
|
||||
char msg[CRASH_MAX_MSG + 1] = { 0 };
|
||||
|
||||
void *pc = NULL;
|
||||
if (ctx) {
|
||||
ucontext_t *ucontext = (ucontext_t *)ctx;
|
||||
#ifdef PLATFORM_X86
|
||||
pc = (void *)ucontext->uc_mcontext.gregs[REG_EIP];
|
||||
#elif defined(PLATFORM_X86_64)
|
||||
pc = (void *)ucontext->uc_mcontext.gregs[REG_RIP];
|
||||
#endif
|
||||
}
|
||||
|
||||
sysLogPrintf(LOG_ERROR, "FATAL: Crashed: PC=%p SIGNAL=%d", pc, sig);
|
||||
|
||||
fflush(stderr);
|
||||
fflush(stdout);
|
||||
|
||||
crashStackTrace(msg, sig, pc);
|
||||
|
||||
{
|
||||
FILE *f = fopen(CRASH_LOG_FNAME, "wb");
|
||||
if (f) {
|
||||
fprintf(f, "Crash!\n\n%s", msg);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
sysFatalError("Crash!\n\n%s", msg);
|
||||
}
|
||||
|
||||
#endif /* PLATFORM_WINDOWS / PLATFORM_LINUX */
|
||||
|
||||
int g_CrashEnabled = 0;
|
||||
|
||||
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)");
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
SetErrorMode(SEM_FAILCRITICALERRORS);
|
||||
prevExFilter = SetUnhandledExceptionFilter(crashHandler);
|
||||
g_CrashEnabled = 1;
|
||||
#elif defined(PLATFORM_LINUX)
|
||||
struct sigaction sigact = { 0 };
|
||||
sigact.sa_flags = SA_SIGINFO | SA_ONSTACK;
|
||||
sigact.sa_sigaction = crashHandler;
|
||||
sigaction(SIGSEGV, &sigact, &prevSigAction);
|
||||
sigaction(SIGABRT, &sigact, &prevSigAction);
|
||||
sigaction(SIGBUS, &sigact, &prevSigAction);
|
||||
sigaction(SIGILL, &sigact, &prevSigAction);
|
||||
g_CrashEnabled = 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
void crashShutdown(void)
|
||||
{
|
||||
if (!g_CrashEnabled) {
|
||||
return;
|
||||
}
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
if (prevExFilter) {
|
||||
SetUnhandledExceptionFilter(prevExFilter);
|
||||
}
|
||||
#elif defined(PLATFORM_LINUX)
|
||||
sigaction(SIGSEGV, &prevSigAction, NULL);
|
||||
sigaction(SIGABRT, &prevSigAction, NULL);
|
||||
sigaction(SIGBUS, &prevSigAction, NULL);
|
||||
sigaction(SIGILL, &prevSigAction, NULL);
|
||||
#endif
|
||||
g_CrashEnabled = 0;
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* dram.c — a slice of N64 "DRAM" in host memory, mapped twice.
|
||||
*
|
||||
* GE's game code mixes two address conventions that cannot both hold on an
|
||||
* x64 host with a single mapping:
|
||||
*
|
||||
* 1. Pointers that pass through s32 (e.g. mempCheckMemflagTokens(s32, s32)
|
||||
* in src/memp.c). Any address with bit 31 set sign-extends to an
|
||||
* invalid 0xffffffffXXXXXXXX pointer when stored back into a u8*.
|
||||
* => the game's working RAM must live BELOW 0x80000000.
|
||||
*
|
||||
* 2. "Physical" addresses that the game ORs with 0x80000000 to make them
|
||||
* dereferenceable (e.g. `texnum = *((u16 *)(temp.word | 0x80000000))`
|
||||
* in src/game/bg.c, where temp.word is a GBI w1 value produced by
|
||||
* OS_K0_TO_PHYSICAL). => a KSEG0 view at 0x80000000 must exist and
|
||||
* mirror the working RAM.
|
||||
*
|
||||
* The port therefore maps ONE 8 MB section at two addresses:
|
||||
*
|
||||
* V1 @ 0x70000000 "virtual" view — s32-safe; ALL game RAM symbols live
|
||||
* here (cfb_16, _bssSegmentEnd/mempool start, tlbBlock/
|
||||
* mempool end). Mempool pointers etc. are 0x70xxxxxx:
|
||||
* positive as s32, safe through every 32-bit path.
|
||||
* V2 @ 0x80000000 KSEG0 view — byte-identical mirror of V1 (same section
|
||||
* backing), for code that rebuilds pointers with
|
||||
* `offset | 0x80000000`.
|
||||
*
|
||||
* The port-side header shims (port/shim/PR/R4300.h, port/shim/PR/os.h) make
|
||||
* the conversion macros consistent with this layout:
|
||||
* PHYS_TO_K0(x) = x (identity)
|
||||
* OS_K0_TO_PHYSICAL(x) = (u32)((char*)x - V1_BASE) (small offset)
|
||||
* so GBI w1 words carry small offsets that fast3d's seg_addr() resolves by
|
||||
* adding 0x80000000 (landing in V2, same data), and osVirtualToPhysical(x)
|
||||
* stays the identity (V1 addresses are already live host pointers < 4 GB).
|
||||
*
|
||||
* Layout (NTSC US .stacks sits at 0x803AB400, MAPPING_TABLE_COUNT=93 pages,
|
||||
* so the N64 tlb block is 0x803AB400 - 93*0x2000 = 0x802F4400 — kept as the
|
||||
* mempool end for fidelity):
|
||||
*
|
||||
* +0x000000 cfb_16 (2 x 320x240x2 = 0x4B000, from src/cfb.c)
|
||||
* +0x050000 _bssSegmentEnd -> mempool start (boss.c:217)
|
||||
* ... game heap (mempools / dyn / audio buffers)
|
||||
* +0x2F4400 tlb block -> mempool end (tlbmanageGetTlbAllocatedBlock)
|
||||
* +0x800000 end of region (8 MB, like an 8-MB-RAM N64)
|
||||
*/
|
||||
|
||||
#include "platform.h"
|
||||
#include "system.h"
|
||||
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#define _GNU_SOURCE /* memfd_create — must precede all system headers */
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#define DRAM_V1_BASE 0x70000000UL /* s32-safe "virtual" view */
|
||||
#define DRAM_K0_BASE 0x80000000UL /* KSEG0 mirror view */
|
||||
#define DRAM_SIZE 0x00800000UL /* 8 MB */
|
||||
|
||||
void *dramReserve(void)
|
||||
{
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
/* One anonymous section, two views at fixed addresses. */
|
||||
SECURITY_ATTRIBUTES sa;
|
||||
sa.nLength = sizeof(sa);
|
||||
sa.lpSecurityDescriptor = NULL;
|
||||
sa.bInheritHandle = FALSE;
|
||||
|
||||
HANDLE hSec = CreateFileMappingW(INVALID_HANDLE_VALUE, &sa, PAGE_READWRITE,
|
||||
(DWORD)(DRAM_SIZE >> 32),
|
||||
(DWORD)(DRAM_SIZE & 0xFFFFFFFF), NULL);
|
||||
if (!hSec) {
|
||||
sysFatalError("dram: CreateFileMapping failed (%lu)", GetLastError());
|
||||
}
|
||||
|
||||
void *v1 = MapViewOfFileEx(hSec, FILE_MAP_ALL_ACCESS, 0, 0, DRAM_SIZE,
|
||||
(void *)DRAM_V1_BASE);
|
||||
if (v1 != (void *)DRAM_V1_BASE) {
|
||||
sysFatalError("dram: could not map DRAM at %p (err %lu)",
|
||||
(void *)DRAM_V1_BASE, GetLastError());
|
||||
}
|
||||
|
||||
void *v2 = MapViewOfFileEx(hSec, FILE_MAP_ALL_ACCESS, 0, 0, DRAM_SIZE,
|
||||
(void *)DRAM_K0_BASE);
|
||||
if (v2 != (void *)DRAM_K0_BASE) {
|
||||
sysFatalError("dram: could not map KSEG0 mirror at %p (err %lu)",
|
||||
(void *)DRAM_K0_BASE, GetLastError());
|
||||
}
|
||||
|
||||
/* Sanity: the views must alias the same backing store. */
|
||||
((volatile char *)v1)[0] = 0x5A;
|
||||
if (((volatile char *)v2)[0] != 0x5A) {
|
||||
sysFatalError("dram: V1/V2 views do not share a backing store");
|
||||
}
|
||||
((volatile char *)v1)[0] = 0;
|
||||
|
||||
CloseHandle(hSec);
|
||||
return v1;
|
||||
#else
|
||||
/* memfd + two MAP_SHARED mmaps = one backing store, two views. */
|
||||
int fd = memfd_create("ge007_dram", 0);
|
||||
if (fd < 0) {
|
||||
sysFatalError("dram: memfd_create failed");
|
||||
}
|
||||
if (ftruncate(fd, DRAM_SIZE) != 0) {
|
||||
sysFatalError("dram: ftruncate failed");
|
||||
}
|
||||
|
||||
void *v1 = mmap((void *)DRAM_V1_BASE, DRAM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED | MAP_FIXED, fd, 0);
|
||||
if (v1 == MAP_FAILED || v1 != (void *)DRAM_V1_BASE) {
|
||||
sysFatalError("dram: could not map DRAM at %p", (void *)DRAM_V1_BASE);
|
||||
}
|
||||
|
||||
void *v2 = mmap((void *)DRAM_K0_BASE, DRAM_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED | MAP_FIXED, fd, 0);
|
||||
if (v2 == MAP_FAILED || v2 != (void *)DRAM_K0_BASE) {
|
||||
sysFatalError("dram: could not map KSEG0 mirror at %p",
|
||||
(void *)DRAM_K0_BASE);
|
||||
}
|
||||
|
||||
((volatile char *)v1)[0] = 0x5A;
|
||||
if (((volatile char *)v2)[0] != 0x5A) {
|
||||
sysFatalError("dram: V1/V2 views do not share a backing store");
|
||||
}
|
||||
((volatile char *)v1)[0] = 0;
|
||||
|
||||
close(fd);
|
||||
return v1;
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* dram_syms.s — absolute symbols inside the "virtual" (s32-safe) DRAM view.
|
||||
* See port/src/dram.c: one 8 MB backing store is mapped at BOTH 0x70000000
|
||||
* (V1, where these live) and 0x80000000 (V2, the KSEG0 mirror). These are
|
||||
* committed host addresses, so they are live pointers on the PC.
|
||||
*
|
||||
* cfb_16 replaces src/cfb.c (excluded from the build): the two
|
||||
* 320x240x16-bit framebuffers, 0x4B000 bytes total.
|
||||
* _bssSegmentEnd end-of-.bss marker; boss.c:217 starts the mempools at
|
||||
* PHYS_TO_K0(osVirtualToPhysical(&_bssSegmentEnd)).
|
||||
*/
|
||||
.section .data
|
||||
|
||||
.global cfb_16
|
||||
.set cfb_16, 0x70000000
|
||||
|
||||
.global _bssSegmentEnd
|
||||
.set _bssSegmentEnd, 0x70050000
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* Port RCP scheduler.
|
||||
*
|
||||
* Replaces the hardware task submission in src/sched.c. On the N64,
|
||||
* __scExec() hands a gfx/audio task to the RSP via osSpTaskLoad/StartGo.
|
||||
* Here, instead of writing RSP registers, we:
|
||||
* - gfx task: call videoSubmitCommands(display_list) -> the software RSP
|
||||
* - audio task: feed the mixed buffer to the audio layer
|
||||
* and reproduce the gfx/audio interleaving + VSync pacing the game expects.
|
||||
*
|
||||
* Modelled on the PD port's port/src/pdsched.c (~405 lines).
|
||||
*
|
||||
* STATUS: scaffolding stub — implement during Phase 2.
|
||||
*
|
||||
* NOTE: GE's src/sched.c has speed-graph / profiling hooks
|
||||
* (speedgraphMarkerHandler) and a slightly different task model than PD.
|
||||
* Reproduce the exact gfx/audio/VSync behaviour; see
|
||||
* docs/PCPortResearch.md §9.3.
|
||||
*/
|
||||
|
||||
#include <PR/ultratypes.h>
|
||||
#include <PR/os.h>
|
||||
#include <PR/gbi.h>
|
||||
|
||||
#include "sched.h"
|
||||
#include "fr.h" /* NUM_VIDEO_FRAME_BUFFERS */
|
||||
|
||||
#include "platform.h"
|
||||
#include "video.h"
|
||||
#include "audio.h"
|
||||
#include "system.h"
|
||||
|
||||
/*
|
||||
* Scheduler globals (normally defined in src/sched.c, which is EXCLUDED from
|
||||
* the PC build). init.c's schedulerInitThread() references os_scheduler and
|
||||
* gfxClient, so the port provides them here (matching src/sched.c:70-71).
|
||||
*/
|
||||
OSSched os_scheduler;
|
||||
OSScClient gfxClient[3];
|
||||
|
||||
/*
|
||||
* Scheduler/VI globals (normally defined in src/sched.c, EXCLUDED). fr.c
|
||||
* references these; the port provides them so it links. Values are the
|
||||
* sched.c initializers.
|
||||
*/
|
||||
s32 g_schedViCurrentFrameBuffer = 0;
|
||||
s32 g_ViChangeVideoModes[NUM_VIDEO_FRAME_BUFFERS] = {0, 0};
|
||||
OSViMode g_ViModes[NUM_VIDEO_FRAME_BUFFERS];
|
||||
OSViMode *g_ViModePtrs[NUM_VIDEO_FRAME_BUFFERS];
|
||||
|
||||
/*
|
||||
* Performance counters (normally in src/sched.c). speed_graph.c reads them
|
||||
* via get_counters().
|
||||
*/
|
||||
static u32 g_DisplayPerformanceCounters[4]; // clock, cmc, pipe, tmem
|
||||
u32 *get_counters(void) { return g_DisplayPerformanceCounters; }
|
||||
|
||||
/* Debug stderr control (normally in src/sched.c). boss.c calls it. */
|
||||
void permit_stderr(u32 flag) { (void)flag; }
|
||||
|
||||
/*
|
||||
* Scheduler API (declared in sched.h, normally defined in src/sched.c which
|
||||
* is EXCLUDED from the PC build). The port provides these so engine code that
|
||||
* calls osCreateScheduler/osScAddClient/osScGetCmdQ links. The real work —
|
||||
* driving the software RSP per frame — happens in geschedRunFrame().
|
||||
*/
|
||||
void osCreateScheduler(OSSched *s, void *stack, u8 mode, u32 numFields)
|
||||
{
|
||||
/* TODO(Phase 2): initialise the port scheduler state in *s. */
|
||||
(void)s; (void)stack; (void)mode; (void)numFields;
|
||||
sysLogPrintf(LOG_INFO, "osCreateScheduler: TODO (Phase 2)");
|
||||
}
|
||||
void osScAddClient(OSSched *s, OSScClient *c, OSMesgQueue *msgQ, OSScClient *next)
|
||||
{
|
||||
/* TODO(Phase 2): register a gfx/audio client. */
|
||||
(void)s; (void)c; (void)msgQ; (void)next;
|
||||
}
|
||||
void osScRemoveClient(OSSched *s, OSScClient *c)
|
||||
{
|
||||
(void)s; (void)c;
|
||||
}
|
||||
OSMesgQueue *osScGetCmdQ(OSSched *s)
|
||||
{
|
||||
/* TODO(Phase 2): return the scheduler's command queue. */
|
||||
(void)s;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void geschedInit(void)
|
||||
{
|
||||
/* TODO(Phase 2): set up the scheduler + gfx client, mirroring
|
||||
* schedulerInitThread() in src/init.c but with the port's frame hooks. */
|
||||
sysLogPrintf(LOG_INFO, "geschedInit: TODO (Phase 2)");
|
||||
}
|
||||
|
||||
/*
|
||||
* Per-frame driver. Called by the main loop (or the VI vsync callback shim).
|
||||
* Sequence (mirrors PD's pdsched.c):
|
||||
* videoStartFrame()
|
||||
* ... game builds the display list ...
|
||||
* videoSubmitCommands(cmds)
|
||||
* videoEndFrame()
|
||||
*/
|
||||
void geschedRunFrame(void *displayList)
|
||||
{
|
||||
videoStartFrame();
|
||||
videoSubmitCommands((Gfx *)displayList);
|
||||
audioEndFrame();
|
||||
videoEndFrame();
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Software-RSP placeholder (Phase 2).
|
||||
*
|
||||
* port/fast3d/ currently contains only the API header (gfx_api.h); the real
|
||||
* implementation arrives in Phase 2 by adapting the PD port's fast3d. video.c
|
||||
* calls these entry points, so they must exist to link. Every function is a
|
||||
* no-op: frames are submitted and dropped until the RSP emulator lands.
|
||||
*
|
||||
* DELETE THIS FILE when the real fast3d sources are added (they define the
|
||||
* same symbols; keeping both would be a duplicate-definition link error).
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <PR/gbi.h>
|
||||
|
||||
/* port/fast3d is not on the include path; use the relative form. */
|
||||
#include "../fast3d/gfx_api.h"
|
||||
|
||||
struct GfxDimensions gfx_current_dimensions = {
|
||||
1.0f, /* internal_mul */
|
||||
640, 480, /* width, height */
|
||||
1.5f, /* aspect_ratio */
|
||||
};
|
||||
|
||||
void gfx_init(const struct GfxInitSettings *settings) { (void)settings; }
|
||||
void gfx_destroy(void) {}
|
||||
void gfx_start_frame(void) {}
|
||||
void gfx_end_frame(void) {}
|
||||
void gfx_run(Gfx *commands) { (void)commands; }
|
||||
|
||||
void gfx_set_target_fps(int fps) { (void)fps; }
|
||||
void gfx_set_texture_filter(int mode) { (void)mode; }
|
||||
void gfx_set_mipmap_filter(int mode) { (void)mode; }
|
||||
+729
-127
File diff suppressed because it is too large.
Load diff
+50
-28
@@ -5,43 +5,49 @@
|
||||
* On the PC we:
|
||||
* 1. set up system / config / fs / rom
|
||||
* 2. load the ROM and map it at the cart base (0x10000000)
|
||||
* 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: Phase 1 — ROM loads and maps, a window opens and clears each frame.
|
||||
* Step 4 (mainproc) is deferred until the software RSP (fast3d) and the
|
||||
* scheduler can service the game's rendering/audio threads; see
|
||||
* docs/PCPortResearch.md.
|
||||
* 3. init video (SDL2 + GL via fast3d), audio, input
|
||||
* 4. start the thread kernel and run the game's mainproc() as a real OS
|
||||
* thread (it IS the N64 mainThread) — which runs bossEntry(), the real
|
||||
* game loop. The game's own scheduler (src/sched.c) drives frames; see
|
||||
* docs/PCPortResearch.md.
|
||||
* 5. the host main thread then owns SDL event pumping for the lifetime of
|
||||
* the process (Windows only dispatches window messages to the creating
|
||||
* thread, and every game thread can be blocked on a queue).
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <PR/ultratypes.h>
|
||||
#include <PR/os.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "system.h"
|
||||
#include "config.h"
|
||||
#include "fs.h"
|
||||
#include "romdata.h"
|
||||
#include "dram.h"
|
||||
#include "video.h"
|
||||
#include "audio.h"
|
||||
#include "input.h"
|
||||
#include "mixer.h"
|
||||
#include "crash.h"
|
||||
#include "thread_config.h"
|
||||
|
||||
/* Defined in the game (src/init.c). The port calls into the real game entry. */
|
||||
extern void mainproc(void *args);
|
||||
extern OSThread mainThread; /* src/init.c:75 */
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
sysSetArgs(argc, argv);
|
||||
|
||||
sysLogPrintf(LOG_INFO, "GoldenEye 007 PC port starting "
|
||||
"(%s, %s)", GE007_ROMID, GE007_VERSION_HASH);
|
||||
|
||||
/* Crash handler first, so any failure below is debuggable. */
|
||||
crashInit();
|
||||
|
||||
/* 1. Platform + config + filesystem. */
|
||||
configLoad();
|
||||
|
||||
@@ -52,6 +58,25 @@ int main(int argc, char **argv)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 2a. Sanity: the image MUST have loaded at its preferred base.
|
||||
* dram_syms.s absolute symbols are referenced through pointer-typed
|
||||
* externs, which on x86-64/PE become .refptr slots with BASE
|
||||
* relocations. The build disables ASLR (--disable-dynamic-base) so the
|
||||
* loader loads at 0x140000000 and those relocations are no-ops; if we
|
||||
* ever got relocated, every such slot would be silently corrupted.
|
||||
* Fail loudly instead. */
|
||||
if (sysImageBase() != 0x140000000ul) {
|
||||
sysLogPrintf(LOG_ERROR,
|
||||
"image loaded at %p, expected preferred base 0x140000000; "
|
||||
"absolute DRAM symbols would be corrupted (ASLR must be off)",
|
||||
(void *)sysImageBase());
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 2b. Reserve the N64-DRAM region: s32-safe view @ 0x70000000 (cfb_16,
|
||||
* mempools) + KSEG0 mirror @ 0x80000000 (see port/src/dram.c). */
|
||||
dramReserve();
|
||||
|
||||
/* 3. Video / audio / input. */
|
||||
if (videoInit() != 0) {
|
||||
sysLogPrintf(LOG_ERROR, "videoInit failed");
|
||||
@@ -61,27 +86,24 @@ int main(int argc, char **argv)
|
||||
mixerInit();
|
||||
inputInit();
|
||||
|
||||
/* 4. Phase 1 demo loop: clear colour until the RSP/scheduler can drive
|
||||
* real frames (Phase 2). Press ESC or close the window to quit.
|
||||
*
|
||||
* When fast3d lands, replace this with mainproc(NULL) — defined in
|
||||
* src/init.c (compiled for the PC; see A3 in docs/PCPortResearch.md).
|
||||
* init.c's N64-only init() is never called; its hardware deps are
|
||||
* stubbed in port/src/n64stubs.c and the libultra OS calls are shimmed
|
||||
* in port/src/libultra.c.
|
||||
*/
|
||||
sysLogPrintf(LOG_INFO, "Phase 1: ROM mapped at 0x%08X (%u bytes); "
|
||||
"demo loop running — ESC to quit",
|
||||
/* 4. Run the game. mainproc() runs as the N64 mainThread (a real OS
|
||||
* thread with its own stack); it creates the rmon/idle/scheduler/
|
||||
* audio threads and never returns in practice. */
|
||||
sysLogPrintf(LOG_INFO, "ROM mapped at 0x%08X (%u bytes); starting game",
|
||||
(unsigned)0x10000000, romdataGetRomSize());
|
||||
portKernelInit();
|
||||
osCreateThread(&mainThread, MAIN_THREAD_ID, &mainproc, NULL, NULL,
|
||||
MAIN_THREAD_PRIORITY);
|
||||
osStartThread(&mainThread);
|
||||
|
||||
/* 5. Host thread: pump SDL events until the window is closed / ESC.
|
||||
* videoPumpEvents() exits the process on quit. */
|
||||
for (;;) {
|
||||
if (videoHandleEvents())
|
||||
break;
|
||||
videoStartFrame();
|
||||
videoSubmitCommands(NULL); /* no-op until fast3d lands */
|
||||
videoEndFrame();
|
||||
videoPumpEvents();
|
||||
sysSleep(8);
|
||||
}
|
||||
|
||||
/* Clean up. */
|
||||
/* Unreachable in practice; clean up if we ever get here. */
|
||||
inputDestroy();
|
||||
mixerDestroy();
|
||||
audioDestroy();
|
||||
|
||||
+33
-3
@@ -21,6 +21,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <PR/ultratypes.h>
|
||||
#include <PR/os.h>
|
||||
#include <tlb_manage.h>
|
||||
|
||||
/* --- Segment start/end getters (normally linker-script symbols) --------- */
|
||||
@@ -44,8 +45,11 @@ u32 get_inflateSegmentRomEnd(void) { return 0; }
|
||||
* What remains here are pure-RAM segment symbols that have no ROM presence:
|
||||
* the host's own .bss/.csegment ends and the N64 vaddr markers (only used by
|
||||
* the never-run N64 boot path on the PC).
|
||||
*
|
||||
* _bssSegmentEnd is NOT here: it is an ABSOLUTE symbol in dram_syms.s
|
||||
* (0x70050000, inside the s32-safe DRAM view — boss.c starts the mempools
|
||||
* at PHYS_TO_K0(osVirtualToPhysical(&_bssSegmentEnd)); see port/src/dram.c).
|
||||
*/
|
||||
u32 *_bssSegmentEnd = NULL;
|
||||
u32 *_csegmentSegmentStart = NULL;
|
||||
u32 *_csegmentSegmentEnd = NULL;
|
||||
u32 *_inflateSegmentVaddrStart = NULL;
|
||||
@@ -71,7 +75,15 @@ void resolve_TLBaddress_for_InvalidHit(void) { /* no-op */ }
|
||||
/*
|
||||
* tlb_manage.c is excluded (it manages the N64's 64-entry TLB for on-demand
|
||||
* ROM segment loading). boss.c still calls two of its functions from
|
||||
* bossInitMainthreadData(); inert on the PC.
|
||||
* bossInitMainthreadData().
|
||||
*
|
||||
* tlbmanageGetTlbAllocatedBlock() is NOT inert: boss.c:218 uses it as the
|
||||
* END of the mempool area (mempCheckMemflagTokens(start, block - start)).
|
||||
* On the N64 it is page_align_down(&sp_boot) - MAPPING_TABLE_COUNT*PAGE_SIZE
|
||||
* = 0x803AB400 - 93*0x2000 = 0x802F4400 (US .stacks). We keep that exact
|
||||
* OFFSET (+0x2F4400 from the DRAM base) but express it in the s32-safe V1
|
||||
* view (see dram.c): 0x70000000 + 0x2F4400 = 0x702F4400, so [start, block)
|
||||
* is live host memory and survives the s32 paths in src/memp.c.
|
||||
*/
|
||||
void tlbmanageEstablishManagementTable(void) { /* no-op */ }
|
||||
void tlbmanageResetCurrentEntriesCount(void) { /* no-op */ }
|
||||
@@ -79,7 +91,10 @@ void tlbmanageTranslateLoadRomFromTlbAddress(u32 address)
|
||||
{
|
||||
(void)address;
|
||||
}
|
||||
u8 (*tlbmanageGetTlbAllocatedBlock(void))[TLB_BLOCK_SIZE] { return NULL; }
|
||||
u8 (*tlbmanageGetTlbAllocatedBlock(void))[TLB_BLOCK_SIZE]
|
||||
{
|
||||
return (u8 (*)[TLB_BLOCK_SIZE])0x702F4400;
|
||||
}
|
||||
|
||||
/* --- K&R libc helpers (IDO provided these; the host libc does not) ------- */
|
||||
/* Signatures mirror include/PR/os.h:983 / include/bstring.h exactly. */
|
||||
@@ -121,3 +136,18 @@ void osSyncPrintf(const char *fmt, ...)
|
||||
vfprintf(stderr, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
/* --- VI manager (src/libultrare/io/vimgr.c, not built for PC) ----------- */
|
||||
/* sched.c's osCreateScheduler() calls osCreateViManager(OS_PRIORITY_VIMGR). */
|
||||
/* The N64 VI manager thread serviced video interrupts; on the port the */
|
||||
/* cooperative kernel posts the retrace message itself (port/src/libultra.c), */
|
||||
/* so this is a no-op. */
|
||||
void osCreateViManager(OSPri pri) { (void)pri; }
|
||||
|
||||
/* --- Crash screen (src/crash.c, not built for PC) ------------------------ */
|
||||
/* src/crash.c is the game's TLB-fault diagnostics + rmon-driven crash */
|
||||
/* screen renderer. It depends on rmon and the N64 exception path, neither of */
|
||||
/* which exists here; real host faults are caught by port/src/crash.c instead.*/
|
||||
/* sched.c only calls this for the debug stderr overlay (off at boot), so a */
|
||||
/* no-op is safe. */
|
||||
void crashRenderFrame(u16 *buffer) { (void)buffer; }
|
||||
+113
-17
@@ -1,9 +1,7 @@
|
||||
/*
|
||||
* 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.
|
||||
* Modelled on the PD port's port/src/system.c.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
@@ -20,40 +18,72 @@
|
||||
#include <windows.h>
|
||||
#include <io.h>
|
||||
#else
|
||||
#define _POSIX_C_SOURCE 199309L
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
#include "system.h"
|
||||
|
||||
/* --- Time --------------------------------------------------------------- */
|
||||
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
static LARGE_INTEGER g_qpcFreq;
|
||||
#endif
|
||||
|
||||
uint64_t sysGetMicroseconds(void)
|
||||
{
|
||||
/* TODO(Phase 1): use a monotonic clock.
|
||||
* - Windows: QueryPerformanceCounter
|
||||
* - POSIX: clock_gettime(CLOCK_MONOTONIC)
|
||||
*/
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
static int inited = 0;
|
||||
LARGE_INTEGER c;
|
||||
if (!inited) {
|
||||
QueryPerformanceFrequency(&g_qpcFreq);
|
||||
inited = 1;
|
||||
}
|
||||
QueryPerformanceCounter(&c);
|
||||
return (uint64_t)((double)c.QuadPart * 1000000.0 / (double)g_qpcFreq.QuadPart);
|
||||
#else
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000000ull + (uint64_t)(ts.tv_nsec / 1000);
|
||||
#endif
|
||||
}
|
||||
|
||||
uintptr_t sysImageBase(void)
|
||||
{
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
return (uintptr_t)GetModuleHandleW(NULL);
|
||||
#else
|
||||
/* TODO: parse /proc/self/maps for the first executable mapping. */
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
int64_t sysGetTime(void)
|
||||
{
|
||||
/* TODO(Phase 1): wall clock (time(NULL)). */
|
||||
return 0;
|
||||
return (int64_t)time(NULL);
|
||||
}
|
||||
|
||||
void sysSleep(uint32_t micros)
|
||||
{
|
||||
/* TODO(Phase 1): Sleep() / usleep(). */
|
||||
(void)micros;
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
Sleep((DWORD)((micros + 999) / 1000));
|
||||
#else
|
||||
struct timespec req;
|
||||
req.tv_sec = micros / 1000000u;
|
||||
req.tv_nsec = (long)(micros % 1000000u) * 1000L;
|
||||
while (nanosleep(&req, &req) != 0) { /* EINTR: keep sleeping the rest */
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/* --- Logging ------------------------------------------------------------ */
|
||||
|
||||
void sysLogPrintf(enum LogLevel level, const char *fmt, ...)
|
||||
{
|
||||
static const char *tags[] = { "ERROR", "WARN ", "INFO ", "DEBUG" };
|
||||
static const char *tags[] = { "ERROR", "WARN ", "NOTE ", "INFO ", "DEBUG" };
|
||||
va_list ap;
|
||||
if ((int)level >= 5) level = LOG_DEBUG;
|
||||
fprintf(stderr, "[%s] ", tags[level]);
|
||||
va_start(ap, fmt);
|
||||
vfprintf(stderr, fmt, ap);
|
||||
@@ -61,17 +91,83 @@ void sysLogPrintf(enum LogLevel level, const char *fmt, ...)
|
||||
fprintf(stderr, "\n");
|
||||
}
|
||||
|
||||
void sysFatalError(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
fprintf(stderr, "[FATAL] ");
|
||||
va_start(ap, fmt);
|
||||
vfprintf(stderr, fmt, ap);
|
||||
va_end(ap);
|
||||
fprintf(stderr, "\n");
|
||||
fflush(stderr);
|
||||
abort();
|
||||
}
|
||||
|
||||
/* --- Command line ------------------------------------------------------- */
|
||||
|
||||
static int g_argc = 0;
|
||||
static char **g_argv = NULL;
|
||||
|
||||
void sysSetArgs(int argc, char **argv)
|
||||
{
|
||||
g_argc = argc;
|
||||
g_argv = argv;
|
||||
}
|
||||
|
||||
int sysArgCheck(const char *arg)
|
||||
{
|
||||
for (int i = 1; i < g_argc; ++i) {
|
||||
if (g_argv[i] && strcmp(g_argv[i], arg) == 0)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *sysArgGetString(const char *arg)
|
||||
{
|
||||
for (int i = 1; i < g_argc; ++i) {
|
||||
if (g_argv[i] && strcmp(g_argv[i], arg) == 0) {
|
||||
if (i + 1 < g_argc)
|
||||
return g_argv[i + 1];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* --- CPU ---------------------------------------------------------------- */
|
||||
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
#define PORT_DO_YIELD() YieldProcessor()
|
||||
#elif defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
|
||||
#include <immintrin.h>
|
||||
#define PORT_DO_YIELD() _mm_pause()
|
||||
#else
|
||||
#define PORT_DO_YIELD() do { } while (0)
|
||||
#endif
|
||||
|
||||
void sysCpuRelax(void)
|
||||
{
|
||||
PORT_DO_YIELD();
|
||||
}
|
||||
|
||||
/* --- 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
|
||||
*/
|
||||
#if defined(PLATFORM_WINDOWS)
|
||||
if (exeDir[0] == '.' && exeDir[1] == 0) {
|
||||
DWORD n = GetModuleFileNameA(NULL, exeDir, sizeof(exeDir) - 1);
|
||||
if (n) {
|
||||
char *slash = strrchr(exeDir, '\\');
|
||||
if (slash)
|
||||
*slash = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return exeDir;
|
||||
}
|
||||
|
||||
|
||||
+157
-144
@@ -1,198 +1,211 @@
|
||||
/*
|
||||
* Video: SDL2 window + OpenGL context + frame pacing.
|
||||
* Video: SDL2 window + OpenGL context + frame pacing on top of fast3d.
|
||||
*
|
||||
* Owns the GL context that the software RSP (fast3d) renders into. Drives the
|
||||
* frame boundary hooks called by the port scheduler (gesched.c).
|
||||
* The window itself lives in port/fast3d/gfx_sdl2.cpp (the wapi backend);
|
||||
* this file wires the rendering API up, owns frame boundaries and FPS stats,
|
||||
* and exposes the small surface the libultra VI shims need.
|
||||
*
|
||||
* Phase 1: real window + GL context + clear colour, driven by the demo loop
|
||||
* in main.c. gfx_* entry points are still the no-ops in gfxstub.c until the
|
||||
* PD-derived fast3d lands (Phase 2); videoStartFrame/EndFrame do the GL work
|
||||
* directly so a visible frame exists before that.
|
||||
* Modelled on the PD port's port/src/video.c (slimmed: no options menu).
|
||||
*/
|
||||
|
||||
#include <SDL.h>
|
||||
#include <GL/gl.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <PR/ultratypes.h>
|
||||
#include <PR/gbi.h>
|
||||
|
||||
#include "platform.h"
|
||||
#include "system.h"
|
||||
#include "config.h"
|
||||
#include "video.h"
|
||||
|
||||
/* fast3d entry points (see port/fast3d/gfx_api.h). */
|
||||
extern void gfx_init(const void *settings);
|
||||
extern void gfx_destroy(void);
|
||||
extern void gfx_start_frame(void);
|
||||
extern void gfx_run(void *commands);
|
||||
extern void gfx_end_frame(void);
|
||||
#include "../fast3d/gfx_api.h"
|
||||
#include "../fast3d/gfx_sdl.h"
|
||||
#include "../fast3d/gfx_opengl.h"
|
||||
|
||||
static int initDone = 0;
|
||||
static int vidVsync = 1;
|
||||
static int vidFpsLimit = 0;
|
||||
static int vidFullscreen = 0;
|
||||
static int vidMaximize = 0;
|
||||
/* GE's internal resolution: NTSC LAN1 is 640x480; PAL LAN1 shows a
|
||||
* 640x400 area. The window opens at the native size (1:1) by default. */
|
||||
#ifdef REFRESH_PAL
|
||||
#define GE_NATIVE_W 640
|
||||
#define GE_NATIVE_H 400
|
||||
#else
|
||||
#define GE_NATIVE_W 640
|
||||
#define GE_NATIVE_H 480
|
||||
#endif
|
||||
|
||||
static SDL_Window *vidWindow = NULL;
|
||||
static SDL_GLContext vidGL = NULL;
|
||||
static struct GfxWindowManagerAPI *wmAPI;
|
||||
static struct GfxRenderingAPI *renderingAPI;
|
||||
static int initDone = 0;
|
||||
|
||||
/* FPS accounting. */
|
||||
static uint64_t fpsLastTick = 0;
|
||||
static int fpsFrames = 0;
|
||||
static float fpsValue = 0.0f;
|
||||
static u32 frames = 0;
|
||||
static double fpsWindowStart = 0.0;
|
||||
static int fpsNumFrames = 0;
|
||||
static float vidAvgFPS = 0.f;
|
||||
|
||||
int videoInit(void)
|
||||
{
|
||||
if (initDone)
|
||||
return 0;
|
||||
wmAPI = &gfx_sdl;
|
||||
renderingAPI = &gfx_opengl_api;
|
||||
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
|
||||
sysLogPrintf(LOG_ERROR, "videoInit: SDL_Init failed: %s",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
gfx_current_native_viewport.width = GE_NATIVE_W;
|
||||
gfx_current_native_viewport.height = GE_NATIVE_H;
|
||||
gfx_current_native_aspect = (float)GE_NATIVE_W / (float)GE_NATIVE_H;
|
||||
gfx_framebuffers_enabled = true;
|
||||
gfx_detail_textures_enabled = false;
|
||||
gfx_msaa_level = 1;
|
||||
|
||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
||||
/* fast3d will want a modern context once it lands; 3.0 core is the floor
|
||||
* the PD port targets. */
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||
struct GfxInitSettings set = {
|
||||
.wapi = wmAPI,
|
||||
.rapi = renderingAPI,
|
||||
.window_settings = {
|
||||
.title = "GoldenEye 007",
|
||||
.width = GE_NATIVE_W,
|
||||
.height = GE_NATIVE_H,
|
||||
.x = 100,
|
||||
.y = 100,
|
||||
.fullscreen = false,
|
||||
.fullscreen_is_exclusive = false,
|
||||
.maximized = false,
|
||||
.centered = true,
|
||||
.allow_hidpi = false,
|
||||
},
|
||||
};
|
||||
|
||||
int w = 640, h = 480; /* GE's internal resolution (NTSC) */
|
||||
Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE;
|
||||
if (vidFullscreen)
|
||||
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
gfx_init(&set);
|
||||
|
||||
vidWindow = SDL_CreateWindow("GoldenEye 007 (PC port)",
|
||||
SDL_WINDOWPOS_CENTERED,
|
||||
SDL_WINDOWPOS_CENTERED, w, h, flags);
|
||||
if (!vidWindow) {
|
||||
sysLogPrintf(LOG_ERROR, "videoInit: SDL_CreateWindow failed: %s",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
if (vidMaximize)
|
||||
SDL_MaximizeWindow(vidWindow);
|
||||
/* VSync on; fast3d paces the window itself. */
|
||||
wmAPI->set_swap_interval(1);
|
||||
|
||||
vidGL = SDL_GL_CreateContext(vidWindow);
|
||||
if (!vidGL) {
|
||||
sysLogPrintf(LOG_ERROR, "videoInit: SDL_GL_CreateContext failed: %s",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
SDL_GL_MakeCurrent(vidWindow, vidGL);
|
||||
SDL_GL_SetSwapInterval(vidVsync ? 1 : 0);
|
||||
gfx_set_texture_filter(FILTER_LINEAR);
|
||||
gfx_set_mipmap_filter(MIPMAP_LINEAR);
|
||||
|
||||
/* Phase 1 clear colour: dark blue-grey so the window is visibly alive. */
|
||||
glClearColor(0.10f, 0.12f, 0.16f, 1.0f);
|
||||
glViewport(0, 0, w, h);
|
||||
|
||||
/* fast3d hooks (no-ops until Phase 2). */
|
||||
gfx_init(NULL);
|
||||
|
||||
fpsLastTick = sysGetMicroseconds();
|
||||
initDone = 1;
|
||||
sysLogPrintf(LOG_INFO, "videoInit: %dx%d window + GL context up", w, h);
|
||||
sysLogPrintf(LOG_INFO, "video: %dx%d window (native %dx%d)",
|
||||
(int)gfx_current_dimensions.width, (int)gfx_current_dimensions.height,
|
||||
GE_NATIVE_W, GE_NATIVE_H);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void videoDestroy(void)
|
||||
{
|
||||
if (!initDone)
|
||||
return;
|
||||
gfx_destroy();
|
||||
if (vidGL) {
|
||||
SDL_GL_DeleteContext(vidGL);
|
||||
vidGL = NULL;
|
||||
if (initDone) {
|
||||
gfx_destroy();
|
||||
initDone = 0;
|
||||
}
|
||||
if (vidWindow) {
|
||||
SDL_DestroyWindow(vidWindow);
|
||||
vidWindow = NULL;
|
||||
}
|
||||
SDL_Quit();
|
||||
initDone = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Pump window events. Returns 1 when the app should quit (window close or
|
||||
* ESC). Called by the demo loop until the port scheduler owns the frame
|
||||
* boundary (Phase 2).
|
||||
*/
|
||||
int videoHandleEvents(void)
|
||||
{
|
||||
SDL_Event e;
|
||||
while (SDL_PollEvent(&e)) {
|
||||
if (e.type == SDL_QUIT)
|
||||
return 1;
|
||||
if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void videoStartFrame(void)
|
||||
{
|
||||
if (!initDone)
|
||||
if (!initDone) {
|
||||
return;
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
}
|
||||
/* Rendering runs on the game's scheduler thread; the GL context was
|
||||
* created on the host main thread. */
|
||||
gfx_sdl_make_context_current();
|
||||
gfx_start_frame();
|
||||
}
|
||||
|
||||
/*
|
||||
* Host-thread SDL event pump.
|
||||
*
|
||||
* On Windows, window messages are only dispatched when the thread that
|
||||
* CREATED the window pumps them — and every game thread can be blocked on a
|
||||
* message queue at any time. So the host main thread (which created the
|
||||
* window in videoInit) must keep pumping; otherwise the window goes
|
||||
* "Not Responding" and ESC/close never arrive. fast3d's own handle_events
|
||||
* (which runs during rendering) remains as a backstop.
|
||||
*/
|
||||
void videoPumpEvents(void)
|
||||
{
|
||||
if (!initDone) {
|
||||
return;
|
||||
}
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
switch (ev.type) {
|
||||
case SDL_QUIT:
|
||||
sysLogPrintf(LOG_INFO, "video: quit requested");
|
||||
exit(0);
|
||||
break;
|
||||
case SDL_KEYDOWN:
|
||||
if (ev.key.keysym.sym == SDLK_ESCAPE) {
|
||||
sysLogPrintf(LOG_INFO, "video: ESC -> quit");
|
||||
exit(0);
|
||||
}
|
||||
break;
|
||||
case SDL_WINDOWEVENT:
|
||||
if (ev.window.event == SDL_WINDOWEVENT_CLOSE) {
|
||||
sysLogPrintf(LOG_INFO, "video: window closed");
|
||||
exit(0);
|
||||
} else if (ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||
gfx_sdl_update_cached_size();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void videoSubmitCommands(Gfx *cmds)
|
||||
{
|
||||
if (initDone)
|
||||
gfx_run(cmds);
|
||||
if (!initDone) {
|
||||
return;
|
||||
}
|
||||
gfx_run(cmds);
|
||||
}
|
||||
|
||||
void videoEndFrame(void)
|
||||
{
|
||||
if (!initDone)
|
||||
if (!initDone) {
|
||||
return;
|
||||
}
|
||||
gfx_end_frame();
|
||||
if (vidWindow)
|
||||
SDL_GL_SwapWindow(vidWindow);
|
||||
|
||||
/* FPS accounting. */
|
||||
fpsFrames++;
|
||||
uint64_t now = sysGetMicroseconds();
|
||||
if (now - fpsLastTick >= 1000000) {
|
||||
fpsValue = (float)fpsFrames * 1000000.0f / (float)(now - fpsLastTick);
|
||||
fpsFrames = 0;
|
||||
fpsLastTick = now;
|
||||
++frames;
|
||||
++fpsNumFrames;
|
||||
|
||||
double now = wmAPI->get_time();
|
||||
if (fpsWindowStart == 0.0) {
|
||||
fpsWindowStart = now;
|
||||
}
|
||||
if (now - fpsWindowStart >= 1.0) {
|
||||
vidAvgFPS = (float)(fpsNumFrames / (now - fpsWindowStart));
|
||||
fpsNumFrames = 0;
|
||||
fpsWindowStart = now;
|
||||
}
|
||||
}
|
||||
|
||||
void videoSetVsync(int enabled)
|
||||
float videoGetFPS(void)
|
||||
{
|
||||
vidVsync = enabled;
|
||||
if (initDone && vidWindow)
|
||||
SDL_GL_SetSwapInterval(vidVsync ? 1 : 0);
|
||||
return vidAvgFPS;
|
||||
}
|
||||
void videoSetFramerateLimit(int fps) { vidFpsLimit = fps; /* TODO Phase 2 */ }
|
||||
int videoGetFullscreen(void) { return vidFullscreen; }
|
||||
void videoSetFullscreen(int enabled)
|
||||
{
|
||||
vidFullscreen = enabled;
|
||||
if (initDone && vidWindow)
|
||||
SDL_SetWindowFullscreen(vidWindow,
|
||||
enabled ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
|
||||
}
|
||||
int videoGetMaximize(void) { return vidMaximize; }
|
||||
void videoSetMaximize(int enabled)
|
||||
{
|
||||
vidMaximize = enabled;
|
||||
if (initDone && vidWindow) {
|
||||
if (enabled)
|
||||
SDL_MaximizeWindow(vidWindow);
|
||||
else
|
||||
SDL_RestoreWindow(vidWindow);
|
||||
}
|
||||
}
|
||||
float videoGetFPS(void) { return fpsValue; }
|
||||
|
||||
PD_CONSTRUCTOR static void videoConfigInit(void)
|
||||
void videoUpdateNativeResolution(s32 w, s32 h)
|
||||
{
|
||||
configRegisterInt("Video.Vsync", &vidVsync, 0, 1);
|
||||
configRegisterInt("Video.FramerateLimit", &vidFpsLimit, 0, 240);
|
||||
configRegisterInt("Video.Fullscreen", &vidFullscreen, 0, 1);
|
||||
configRegisterInt("Video.Maximize", &vidMaximize, 0, 1);
|
||||
if (w <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
gfx_current_native_viewport.width = w;
|
||||
gfx_current_native_viewport.height = h;
|
||||
gfx_current_native_aspect = (float)w / (float)h;
|
||||
}
|
||||
|
||||
s32 videoGetNativeWidth(void) { return gfx_current_native_viewport.width; }
|
||||
s32 videoGetNativeHeight(void) { return gfx_current_native_viewport.height; }
|
||||
|
||||
s32 videoCreateFramebuffer(u32 w, u32 h, s32 upscale, s32 autoresize)
|
||||
{
|
||||
return gfx_create_framebuffer(w, h, upscale, autoresize);
|
||||
}
|
||||
|
||||
void videoCopyFramebuffer(s32 dst, s32 src, s32 left, s32 top)
|
||||
{
|
||||
/* assume immediate copies always read the front buffer */
|
||||
gfx_copy_framebuffer(dst, src, left, top, false);
|
||||
}
|
||||
|
||||
void videoResetTextureCache(void)
|
||||
{
|
||||
gfx_texture_cache_clear();
|
||||
}
|
||||
Reference in new issue
Block a user