PC port: phase 2 — Plan B executed (D50): offline model sidecars + port plumbing; texCopyGdls w1 fix

- tools_pc/d43_emit.py: full emit pass, 512/512 round-trip validated;
  data/pcmodels-<region>/pcmodels.bin + manifest.csv (gitignored, regenerate)
- port/src/pcmodels.c(+h): cart-extension reservation, sidecar copy, one-shot
  file_resource_table patch; romdata.c/h extended
- objecthandler_2.c: #ifdef PORT hook — pcmodelsPatchTable() + poolRemaining=0
  reset (closes D48.3/R6 reload hazard)
- language.c/textrelated.c: runtime C fixups for lang-bank BE offset tables
  (romdataFixupLangBank) and font re-layout (romdataFontPcSize/FixupFont)
- front.c: legal-screen uninitialized-pointer UB seed (N64 survived by luck)
- tex.c (D50.6): texCopyGdls copied only w0 of each 16B Gfx slot on PC —
  compaction scratch kept stale mempool w1s -> garbage G_SETTIMG -> OOB in
  import_texture_rgba16. Full-slot copy under #ifdef PORT; byte-proven vs
  PlegalpageZ sidecar (w0s match, w1s stale). Model GDLs now execute.
This commit is contained in:
James Dansereau committed 2026-08-23 10:22:03 -04:00
1 parent 8f7ab8ec69
commit 330b2bae46
11 files changed
+1588 -3

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
#ifndef PORT_PCMODELS_H
#define PORT_PCMODELS_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* D50 (docs/PCPortResearch.md): PC-layout model sidecars — Plan B (D48/D49).
*
* N64 ROM model files are 32-bit-pointer big-endian images the PC build
* cannot read in place (D37/D43). tools_pc/d43_emit.py converts all of them
* offline to PC-layout RZ sidecars:
* data/pcmodels-<region>/pcmodels.bin single concatenated image
* data/pcmodels-<region>/manifest.csv name,offset,size (decimal)
*
* romdataInit reserves romSize + pcmodelsReserveSize() at CART_BASE and
* pcmodelsLoadSidecars() copies the sidecar image to [CART_BASE+romSize, ...).
* pcmodelsPatchTable() — called once from load_object_fill_header, after
* obInit() has run — redirects every model entry's hw_address into the
* extension region and sets rom_size to the PC compressed size, so the
* existing romCopy/decompressdata load path serves PC bytes unmodified.
*/
/* Bytes to reserve immediately after the ROM (0 = no sidecar image found;
* the game boots but model loads will fail). romImg is the validated ROM
* image; the country byte at +0x3E selects the region directory. */
uint32_t pcmodelsReserveSize(const uint8_t *romImg);
/* Copy the sidecar image to cartBase+romSize and parse the manifest.
* Returns 1 on success. */
int pcmodelsLoadSidecars(uintptr_t cartBase, uint32_t romSize);
/* One-shot patch of file_resource_table + resource_lookup_data_array for
* every manifest row. Must run after obInit() (obInit derives rom_size from
* adjacent hw_address deltas). No-op if the sidecar image is absent. */
void pcmodelsPatchTable(void);
uint32_t pcmodelsTotalSize(void);
#ifdef __cplusplus
}
#endif
#endif /* PORT_PCMODELS_H */
+19
View File
@@ -61,6 +61,25 @@ void romdataFixupAnimationData(u8 *blob, u32 blobSize,
*/
void romdataFixupMusicSeqTable(u8 *blob, u32 blobSize);
/*
* D50 (docs/PCPortResearch.md): decode a language bank's big-endian offset
* table in place ([u32 offsets x N][text]; see romdata.c). blobSize is the
* decompressed bank size (resource_lookup_data_array[].poolRemaining).
*/
void romdataFixupLangBank(u8 *blob, u32 blobSize);
/*
* D50 (docs/PCPortResearch.md): re-lay out a font segment (struct font:
* kerning[169] + chars[94] + glyph pixel data) from the N64 ROM layout to
* the PC C layout, shifting the pixel block below the expanded char array.
* src/n64Size locate the N64 image (ROM segment); the caller allocates
* romdataFontPcSize() bytes, romCopies n64Size of them, then calls
* romdataFixupFont(). pixeldata fields are left as relative offsets —
* load_font_tables()'s `pixeldata += base` loop promotes them.
*/
u32 romdataFontPcSize(const u8 *src, u32 n64Size);
void romdataFixupFont(u8 *blob, u32 n64Size);
/*
* D37 (docs/PCPortResearch.md): the libaudio SFX/instrument bank segments
* (ALBankFile trees: ALBank -> ALInstrument -> ALSound -> ALWaveTable /
+236
View File
@@ -0,0 +1,236 @@
/*
* PC-layout model sidecars (D50 / Plan B, D48/D49).
*
* Serves the offline-converted model files produced by tools_pc/d43_emit.py:
* data/pcmodels-<region>/pcmodels.bin single concatenated RZ image
* data/pcmodels-<region>/manifest.csv name,offset,size (decimal)
*
* The ROM is mapped at CART_BASE (romdata.c); the sidecar image is placed in
* a reservation extension at [CART_BASE+romSize, ...). romCopy() on PC is a
* host memcpy gated only by romdataCartAddrValid(), so once every model
* entry's hw_address points into the extension region and rom_size holds the
* PC compressed size, the existing load_resource()/decompressdata() path
* serves PC-layout bytes unmodified (and sets poolRemaining = D_PC).
*
* The table patch must run AFTER obInit(): obInit derives rom_size from
* adjacent-entry hw_address deltas (ob.c), so patching earlier would be
* overwritten. load_object_fill_header calls pcmodelsPatchTable() on every
* model load; it is a one-shot no-op after the first.
*/
#include <stdlib.h>
#include <string.h>
/* D38: <stdlib.h>/<string.h> resolve to the decomp's N64 stubs via the
* include path; declare the host functions this file uses. */
extern void *malloc(unsigned long long size);
extern int snprintf(char *str, unsigned long long maxsize, const char *format, ...);
#include "platform.h"
#include "system.h"
#include "fs.h"
#include "pcmodels.h"
/* Order matters: <ultra64.h> must finish first — its PR/ucode.h anchor pulls
* in pc_protos.h, which itself includes game/ob.h. Including ob.h before
* ultra64.h leaves FILELOADMETHOD undefined mid-parse (circular guard). */
#include <ultra64.h> /* u8/u32/s32 + full PR chain */
#include "game/ob.h" /* fileentry, resource_lookup_data_entry */
/* Defined in src/game/ob.c (the table is included there from
* assets/obseg/file_resource_table.inc.c). Declared here rather than in ob.h
* so the game header stays untouched. */
extern struct fileentry file_resource_table[];
extern resource_lookup_data_entry resource_lookup_data_array[];
extern s32 file_entry_max;
#define PCMODES_CART_BASE 0x10000000u
#define PCMODES_MAX_FILES 1024
#define PCMODES_NAME_LEN 64
struct pcmodelsRow {
char name[PCMODES_NAME_LEN];
uint32_t offset;
uint32_t size;
};
static uint32_t s_total = 0;
static int s_loaded = 0;
static int s_patched = 0;
static uintptr_t s_base = 0; /* cart address of the sidecar image */
static struct pcmodelsRow s_rows[PCMODES_MAX_FILES];
static int s_rowCount = 0;
static char s_binPath[1024] = "";
static char s_manPath[1024] = "";
static const char *pcmodelsRegionForCountry(u8 country)
{
switch (country) {
case 'E': return "ntsc-final";
case 'P': return "pal-final";
case 'J': return "jpn-final";
default: return NULL;
}
}
/* Try $S/, $E/, ./ for <rel>; on success copy the resolved path into out. */
static int pcmodelsFindDataFile(const char *rel, char *out, size_t outsz)
{
static const char *locs[3] = { "$S/", "$E/", "./" };
int i;
for (i = 0; i < 3; i++) {
char pathbuf[1024];
snprintf(pathbuf, sizeof(pathbuf), "%s%s", locs[i], rel);
if (fsExists(sysResolvePath(pathbuf))) {
snprintf(out, outsz, "%s", sysResolvePath(pathbuf));
return 1;
}
}
return 0;
}
uint32_t pcmodelsReserveSize(const uint8_t *romImg)
{
const char *region = pcmodelsRegionForCountry(romImg[0x3E]);
char rel[256];
if (!region)
return 0;
snprintf(rel, sizeof(rel), "pcmodels-%s/pcmodels.bin", region);
if (!pcmodelsFindDataFile(rel, s_binPath, sizeof(s_binPath))) {
sysLogPrintf(LOG_WARNING,
"pcmodels: %s not found — model loads will fail; run "
"\"python tools_pc/d43_emit.py\" (see docs/HANDOFF.md)",
rel);
return 0;
}
snprintf(rel, sizeof(rel), "pcmodels-%s/manifest.csv", region);
if (!pcmodelsFindDataFile(rel, s_manPath, sizeof(s_manPath))) {
sysLogPrintf(LOG_ERROR, "pcmodels: manifest.csv missing next to %s",
s_binPath);
return 0;
}
FSFile *f = fsOpen(s_binPath, "rb");
if (!f) {
sysLogPrintf(LOG_ERROR, "pcmodels: cannot open %s", s_binPath);
return 0;
}
uint32_t size = (uint32_t)fsSize(f);
fsClose(f);
s_total = size;
return size;
}
int pcmodelsLoadSidecars(uintptr_t cartBase, uint32_t romSize)
{
FSFile *f;
int ok;
if (!s_total)
return 0;
f = fsOpen(s_binPath, "rb");
if (!f)
return 0;
s_base = cartBase + romSize;
ok = fsRead(f, (void *)s_base, (int32_t)s_total) == (int32_t)s_total;
fsClose(f);
if (!ok) {
sysLogPrintf(LOG_ERROR, "pcmodels: short read of %s", s_binPath);
return 0;
}
/* Parse the manifest (one-shot data for pcmodelsPatchTable). */
f = fsOpen(s_manPath, "rb");
if (!f) {
sysLogPrintf(LOG_ERROR, "pcmodels: cannot open %s", s_manPath);
return 0;
}
int32_t msize = fsSize(f);
char *buf = (char *)malloc((unsigned long long)(msize > 0 ? msize + 1 : 1));
if (!buf) {
fsClose(f);
return 0;
}
int32_t nread = fsRead(f, buf, msize);
fsClose(f);
if (nread <= 0) {
free(buf);
sysLogPrintf(LOG_ERROR, "pcmodels: empty manifest %s", s_manPath);
return 0;
}
buf[nread] = 0;
for (char *line = strtok(buf, "\r\n"); line;
line = strtok(NULL, "\r\n")) {
char *c1 = strchr(line, ',');
char *c2;
if (!c1 || !strcmp(line, "name,offset,size"))
continue;
*c1 = 0;
c2 = strchr(c1 + 1, ',');
if (!c2)
continue;
*c2 = 0;
if (s_rowCount >= PCMODES_MAX_FILES) {
sysLogPrintf(LOG_ERROR, "pcmodels: manifest exceeds %d rows",
PCMODES_MAX_FILES);
break;
}
struct pcmodelsRow *r = &s_rows[s_rowCount];
snprintf(r->name, sizeof(r->name), "%s", line);
r->offset = (uint32_t)strtol(c1 + 1, NULL, 10);
r->size = (uint32_t)strtol(c2 + 1, NULL, 10);
s_rowCount++;
}
free(buf);
if (!s_rowCount) {
sysLogPrintf(LOG_ERROR, "pcmodels: no rows parsed from %s", s_manPath);
return 0;
}
s_loaded = 1;
sysLogPrintf(LOG_INFO, "pcmodels: %d sidecars (%u bytes) at 0x%llX",
s_rowCount, s_total, (unsigned long long)s_base);
return 1;
}
void pcmodelsPatchTable(void)
{
int i, t, hit;
if (s_patched || !s_loaded)
return;
s_patched = 1;
hit = 0;
for (i = 0; i < s_rowCount; i++) {
const struct pcmodelsRow *r = &s_rows[i];
for (t = 1; t < file_entry_max; t++) {
if (file_resource_table[t].filename &&
strcmp(file_resource_table[t].filename, r->name) == 0) {
file_resource_table[t].hw_address =
(u8 *)(s_base + r->offset);
resource_lookup_data_array[t].rom_size = r->size;
hit++;
break;
}
}
}
if (hit != s_rowCount)
sysLogPrintf(LOG_WARNING,
"pcmodels: patched %d/%d manifest rows (table mismatch?)",
hit, s_rowCount);
else
sysLogPrintf(LOG_INFO, "pcmodels: table patched (%d model entries)",
hit);
}
uint32_t pcmodelsTotalSize(void)
{
return s_loaded ? s_total : 0;
}
+119 -3
View File
@@ -37,6 +37,12 @@ extern int snprintf(char *str, unsigned long long maxsize, const char *format, .
#include "system.h"
#include "fs.h"
#include "romdata.h"
#include "pcmodels.h"
/* D50: struct font/fontchar layouts for the font-segment re-layout. Order
* matters (see pcmodels.c): ultra64.h must finish before bondtypes.h. */
#include <ultra64.h>
#include "bondtypes.h"
#define CART_BASE 0x10000000u
@@ -151,16 +157,24 @@ int romdataInit(void)
continue;
}
/* D50: reserve room for the PC model sidecar image right after the
* ROM ([CART_BASE+romSize, ...)); it is loaded below and patched into
* file_resource_table after obInit. */
u32 sideTotal = pcmodelsReserveSize(img);
/* Map at the cart address so absolute asset symbols are live. */
#if defined(PLATFORM_WINDOWS)
void *at = VirtualAlloc((LPVOID)CART_BASE, romSize,
void *at = VirtualAlloc((LPVOID)CART_BASE, romSize + sideTotal,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (at == (void *)(uintptr_t)CART_BASE) {
memcpy(at, img, romSize);
free(img);
mappedAtCartBase = 1;
sysLogPrintf(LOG_INFO, "romdataInit: %s (%u bytes) mapped at "
"0x%08X (cart base)", tok, romSize, CART_BASE);
"0x%08X (cart base)%s",
tok, romSize, CART_BASE,
sideTotal ? ", + model sidecars" : "");
pcmodelsLoadSidecars(CART_BASE, romSize);
return 0;
}
if (at)
@@ -216,7 +230,8 @@ int romdataCartAddrValid(u32 addr, u32 size)
if (addr < CART_BASE)
return 0;
u32 off = addr - CART_BASE;
return off + size <= romSize;
/* D50: the model sidecar image extends the valid cart region. */
return off + size <= romSize + pcmodelsTotalSize();
}
const void *romdataMapVa(u32 va)
@@ -259,6 +274,10 @@ static u32 romdataBswap32(u32 v)
((v & 0x00FF0000u) >> 8) | ((v & 0xFF000000u) >> 24);
}
/* The port/shim <stddef.h> is the N64 stub for C TUs (no offsetof); this is
* the equivalent null-cast idiom. */
#define ROMDATA_OFFSEXP(T, M) ((u32)&(((T *)0)->M))
static u16 romdataBswap16(u16 v)
{
return (u16)((v << 8) | (v >> 8));
@@ -353,6 +372,103 @@ void romdataFixupMusicSeqTable(u8 *blob, u32 blobSize)
}
}
/*
* D50 (docs/PCPortResearch.md): a language bank is [u32 offsets x N][text].
* Offsets are big-endian and point into the text region; langGet() reads them
* little-endian on PC and would build garbage pointers. Scan from the front
* and bswap word i while bswap32(word_i) is a plausible offset (strictly past
* its own position, before the end of the blob). Zero entries stay zero
* ("string not loaded" — langGet returns NULL). The scan stops at the first
* text word: printable ASCII byte-swaps to >= 0x20000000, always >= blobSize
* (banks are < 64 KB). All-zero placeholder banks (e.g. LameE) bswap nothing.
*/
void romdataFixupLangBank(u8 *blob, u32 blobSize)
{
u32 i = 0;
while (i * 4 + 4 <= blobSize) {
u32 v = romdataBswap32(*(const u32 *)(blob + i * 4));
if (v != 0 && (v <= i * 4 || v >= blobSize))
break;
*(u32 *)(blob + i * 4) = v;
i++;
}
}
/*
* D50 (docs/PCPortResearch.md): font segments (Zurich Bold, Bank Gothic)
* serialize `struct font` in the N64 layout — big-endian scalars, 4-byte
* pointers:
* [s32 kerning x 169][fontchar x 94 @ stride 24][glyph pixel data]
* where fontchar = {s32 index, baseline, height, width, kerningindex;
* u32 pixeldata-offset-from-font-base}. The PC C layout is
* {5 s32; u64 pixeldata} @ stride sizeof(fontchar) with chars at
* ROMDATA_OFFSEXP(struct font, chars) — and that region overlaps the start of the
* pixel data in ROM (chars end 0xB74, pixels start 0xB80, PC chars end
* 0xE78), so the re-layout must also shift the pixel data down.
*
* Transform: bswap kerning in place; memmove the pixel block from
* [pixStart, n64Size) to [pcCharsEnd, ...); convert each char entry
* (backward — PC stride 32 > N64 stride 24, dst always past src) with
* pixeldata stored as a zero-extended RELATIVE offset into the new image,
* so load_font_tables()'s existing `pixeldata += base` loop promotes it
* (N64 pattern, D37-style). The caller must allocate romdataFontPcSize()
* bytes and romCopy only n64Size of them.
*/
/* src is the N64-layout image (the ROM segment); n64Size its size. */
u32 romdataFontPcSize(const u8 *src, u32 n64Size)
{
enum { N64_COFF = 169 * 4, N64_CSTRIDE = 24, NCHARS = 94 };
u32 i, n, pixStart = 0;
if (n64Size <= N64_COFF)
return n64Size;
n = (n64Size - N64_COFF) / N64_CSTRIDE;
if (n > NCHARS)
n = NCHARS;
for (i = 0; i < n; i++) {
u32 o = romdataBswap32(*(const u32 *)(src + N64_COFF + N64_CSTRIDE * i + 20));
if (o != 0 && (pixStart == 0 || o < pixStart))
pixStart = o;
}
return (u32)(ROMDATA_OFFSEXP(struct font, chars) + NCHARS * sizeof(struct fontchar))
+ n64Size - pixStart;
}
void romdataFixupFont(u8 *blob, u32 n64Size)
{
enum { N64_KERN = 169, N64_COFF = 169 * 4, N64_CSTRIDE = 24, NCHARS = 94 };
u32 i, n, pixStart = 0, pcPixOff;
for (i = 0; i < N64_KERN; i++)
*(u32 *)(blob + 4 * i) = romdataBswap32(*(const u32 *)(blob + 4 * i));
n = (n64Size - N64_COFF) / N64_CSTRIDE;
if (n > NCHARS)
n = NCHARS;
for (i = 0; i < n; i++) {
u32 o = romdataBswap32(*(const u32 *)(blob + N64_COFF + N64_CSTRIDE * i + 20));
if (o != 0 && (pixStart == 0 || o < pixStart))
pixStart = o;
}
pcPixOff = (u32)(ROMDATA_OFFSEXP(struct font, chars) + NCHARS * sizeof(struct fontchar));
if (pixStart != 0 && pixStart < n64Size)
memmove(blob + pcPixOff, blob + pixStart, n64Size - pixStart);
for (i = n; i-- > 0;) {
const u8 *s = blob + N64_COFF + N64_CSTRIDE * i;
u8 *d = blob + ROMDATA_OFFSEXP(struct font, chars) + sizeof(struct fontchar) * i;
u32 k, o;
for (k = 0; k < 5; k++)
*(u32 *)(d + 4 * k) = romdataBswap32(*(const u32 *)(s + 4 * k));
o = romdataBswap32(*(const u32 *)(s + 20));
if (o != 0 && pixStart != 0 && o >= pixStart)
o = pcPixOff + (o - pixStart);
*(u32 *)(d + 20) = o; /* zero-extended relative offset */
*(u32 *)(d + 24) = 0;
}
}
/*
* D37 (docs/PCPortResearch.md): convert a ROM-serialized libaudio bank tree
* (ALBankFile -> ALBank -> ALInstrument -> ALSound -> ALWaveTable /
+11
View File
@@ -1506,7 +1506,18 @@ Gfx *constructor_menu00_legalscreen(Gfx *DL)
s32 i;
u8 *txt;
Mtxf tmpmtx;
#ifdef PORT
/* PC port (D50): on N64 this pointer is used uninitialized at the lookat
* call below — the R4300 register happens to hold a readable address and
* the result is zeroed by `* 0.0f` anyway. The x86-64 compiler folds the
* UB read to NULL (cvtsi2ssl 0x4) and faults. Seeding it with the array
* the function assigns a few lines later keeps the value deterministic;
* v_pos is s32 so `v_pos * 0.0f` is ±0.0f either way — identical lookat.
* N64 build untouched (no -DPORT). */
struct legal_screen_text *legal_text_ptr = legalpage_text_array;
#else
struct legal_screen_text *legal_text_ptr;
#endif
renderdata = legalscreen_MRD;
+29
View File
@@ -4,6 +4,20 @@
#include "language.h"
#include "ob.h"
#ifdef PORT
/* D50: language banks carry a big-endian offset table; decode it in place
* after each load (see romdataFixupLangBank). */
#include "romdata.h"
extern resource_lookup_data_entry resource_lookup_data_array[]; /* ob.c */
static void langFixupLoadedBank(char *name, void *p)
{
s32 idx = fileGetIndex(name);
if (idx >= 0 && resource_lookup_data_array[idx].poolRemaining != 0)
romdataFixupLangBank((u8 *)p, resource_lookup_data_array[idx].poolRemaining);
}
#endif
// bss
//CODE.bss:8008C640
s32 g_LangBanks[45];
@@ -249,6 +263,15 @@ void langInit(void) {
g_LangBanks[LMPWEAPONS] = _fileNameLoadToBank(LnameX_lookuptable[LMPWEAPONS][j_text_trigger], FILELOADMETHOD_DEFAULT, 0x100, MEMPOOL_PERMANENT);
g_LangBanks[LOPTIONS] = _fileNameLoadToBank(LnameX_lookuptable[LOPTIONS][j_text_trigger], FILELOADMETHOD_DEFAULT, 0x100, MEMPOOL_PERMANENT);
g_LangBanks[LMISC] = _fileNameLoadToBank(LnameX_lookuptable[LMISC][j_text_trigger], FILELOADMETHOD_DEFAULT, 0x100, MEMPOOL_PERMANENT);
#ifdef PORT
langFixupLoadedBank(LnameX_lookuptable[LGUN][j_text_trigger], (void *)g_LangBanks[LGUN]);
langFixupLoadedBank(LnameX_lookuptable[LTITLE][j_text_trigger], (void *)g_LangBanks[LTITLE]);
langFixupLoadedBank(LnameX_lookuptable[LMPMENU][j_text_trigger], (void *)g_LangBanks[LMPMENU]);
langFixupLoadedBank(LnameX_lookuptable[LPROPOBJ][j_text_trigger], (void *)g_LangBanks[LPROPOBJ]);
langFixupLoadedBank(LnameX_lookuptable[LMPWEAPONS][j_text_trigger], (void *)g_LangBanks[LMPWEAPONS]);
langFixupLoadedBank(LnameX_lookuptable[LOPTIONS][j_text_trigger], (void *)g_LangBanks[LOPTIONS]);
langFixupLoadedBank(LnameX_lookuptable[LMISC][j_text_trigger], (void *)g_LangBanks[LMISC]);
#endif
}
@@ -354,12 +377,18 @@ struct jpncharpixels *langGetJpnCharPixels(s32 codepoint)
void langLoadToAddr(u32 id)
{
g_LangBanks[id] = _fileNameLoadToBank(LnameX_lookuptable[id][j_text_trigger],1,0x100,MEMPOOL_STAGE);
#ifdef PORT
langFixupLoadedBank(LnameX_lookuptable[id][j_text_trigger], (void *)g_LangBanks[id]);
#endif
}
void langLoadToBank(int id,u8 *target,int size)
{
g_LangBanks[id] = _fileNameLoadToAddr(LnameX_lookuptable[id][j_text_trigger],1,target,size);
#ifdef PORT
langFixupLoadedBank(LnameX_lookuptable[id][j_text_trigger], (void *)g_LangBanks[id]);
#endif
}
+21
View File
@@ -11,6 +11,11 @@
#include "quaternion.h"
#include "tex.h"
#ifdef PORT
#include "pcmodels.h" /* D50: PC-layout model sidecars (Plan B, D48/D49) */
extern resource_lookup_data_entry resource_lookup_data_array[]; /* ob.c */
#endif
/***
* Perfect Dark:
@@ -90,6 +95,22 @@ void load_object_fill_header(struct ModelFileHeader *objheader, u8 *name, u8* ds
{
void *filedata;
#ifdef PORT
/* PC port (D50, Plan B D48/D49): serve PC-layout model sidecars from the
* cart extension region. One-shot table patch (safe to call repeatedly);
* by first model load obInit() has definitely run (boss.c:179). Must run
* before _fileNameLoadTo* below reads hw_address/rom_size. */
pcmodelsPatchTable();
if (dst == 0) {
/* D48.3: a stale poolRemaining from an earlier load of this file
* would under-allocate the fresh bank chunk (P_old < round8(C_pc)+8)
* and load_resource would fail with poolRemaining=0. Resetting to 0
* forces the full-bank allocation; the bump allocator ends at the
* same cursor either way (mempAddEntryOfSizeToBank shrinks it back). */
resource_lookup_data_array[fileGetIndex((char *)name)].poolRemaining = 0;
}
#endif
if (dst != 0)
{
filedata = _fileNameLoadToAddr(name, 0, dst, size);
+9
View File
@@ -1072,7 +1072,16 @@ void texCopyGdls(Gfx *arg0, Gfx *arg1, s32 arg2)
while (arg2--)
{
#ifdef PORT
/* PC port (D50): on x86-64 the trailing `long long` union member spans
* only the first 8 of 16 slot bytes; assigning it leaves w1 stale.
* texLoadFromGdl() then reads the half-copied scratch copy and
* propagates garbage w1s into the final GDLs (seg_addr -> OOB read in
* import_texture_rgba16). Copy the whole 16-byte slot. */
*arg1 = *arg0;
#else
arg1->force_structure_alignment = arg0->force_structure_alignment;
#endif
arg1--;
arg0--;
}
+30
View File
@@ -4,6 +4,12 @@
#include "bondtypes.h"
#include "game/language.h"
#ifdef PORT
/* D50: font segments are re-laid out from the N64 ROM layout to the PC C
* layout (see romdataFixupFont). */
#include "romdata.h"
#endif
#define SPACE_WIDTH 5
#define M_COLOR_R(x) (u8)(x >> 0x18)
@@ -108,22 +114,46 @@ void load_font_tables(void)
text_s = 0;
text_t = 0;
len = (romptr_t)&_fontbankgothicSegmentEnd - (romptr_t)&_fontbankgothicSegmentStart;
#ifdef PORT
{
u32 n64len = len;
ptrFontBankGothic = (struct font *)mempAllocBytesInBank(romdataFontPcSize((const u8 *)&_fontbankgothicSegmentRomStart, n64len), MEMPOOL_STAGE);
ptrFontBankGothicChars = ptrFontBankGothic->chars;
romCopy(ptrFontBankGothic, (void *) &_fontbankgothicSegmentRomStart, n64len);
romdataFixupFont((u8 *)ptrFontBankGothic, n64len);
}
#else
len = (romptr_t)&_fontbankgothicSegmentEnd - (romptr_t)&_fontbankgothicSegmentStart;
ptrFontBankGothic = (struct font *)mempAllocBytesInBank(len, MEMPOOL_STAGE);
ptrFontBankGothicChars = ptrFontBankGothic->chars;
romCopy(ptrFontBankGothic, (void *) &_fontbankgothicSegmentRomStart, len);
#endif
// Convert pointers
for (i = 0; i < 94; i++) {
ptrFontBankGothicChars[i].pixeldata += (uintptr_t)ptrFontBankGothic;
}
len = (romptr_t)&_fontzurichboldSegmentEnd - (romptr_t)&_fontzurichboldSegmentStart;
#ifdef PORT
{
u32 n64len = len;
ptrFontZurichBold = (struct font *)mempAllocBytesInBank(romdataFontPcSize((const u8 *)&_fontzurichboldSegmentRomStart, n64len), MEMPOOL_STAGE);
ptrFontZurichBoldChars = ptrFontZurichBold->chars;
romCopy(ptrFontZurichBold, (void *) &_fontzurichboldSegmentRomStart, n64len);
romdataFixupFont((u8 *)ptrFontZurichBold, n64len);
}
#else
len = (romptr_t)&_fontzurichboldSegmentEnd - (romptr_t)&_fontzurichboldSegmentStart;
ptrFontZurichBold = (struct font *)mempAllocBytesInBank(len, MEMPOOL_STAGE);
ptrFontZurichBoldChars = ptrFontZurichBold->chars;
romCopy(ptrFontZurichBold, (void *) &_fontzurichboldSegmentRomStart, len);
#endif
// Convert pointers
for (i = 0; i < 94; i++) {
+956
View File
@@ -0,0 +1,956 @@
#!/usr/bin/env python3
"""D50 / Plan B (D48/D49): offline emit pass — convert all N64 model files to
PC layout and write RZ-compressed sidecars + manifest.csv.
Per file:
1. decompress the N64 image from the ROM (filelist.u.csv row)
2. build mutable node map; run the EXACT modelIterateDisplayLists visit
simulation (d43_gdlorder logic: LOD/SWITCH rewire, BSP splice) -> gdl_seq
3. layout pass (d43_convert placement): [switches NS*8][texconfigs NT*12]
[DFS nodes 48B + records PC_REC + vtx arrays][zero-vtx][GDLs packed in
gdl_seq order, 16B per N64 slot] -> D_PC, region map
4. emit pass: byte-exact field tables (bswap32/16, promoted ptrs -> u64
zero-ext 0x05xxxxxx VMA, GDL Primary/Secondary raw VMAs, BaseAddr=0)
5. round-trip validation: re-parse the emitted image and check every field
against the N64 source (inverse remap), plus region tiling (no gaps)
6. compress: 0x11 0x72 + raw deflate (level 6) -> C_PC
Cross-checks (all must pass):
- per-file dst!=0 buffers: round8(C)+8 <= buf AND D+round8(C) <= buf
(inflate overlap safety); N64 sanity version of the same checks
- G* hand weapons worst-case vs 0xF000
- chains (cast / title / bondview): cumulative P_final bound
B_pc + 2*(R_share_N64 - B_n64) per slot, worst over candidate sets
- totals: sum round8(C), max C, max D
Output: data/pcmodels-<region>/pcmodels.bin (single concatenated image,
sidecars at 16-aligned offsets) + manifest.csv (name,offset,size decimal;
file_resource_table.inc.c order). The port layer (port/src/pcmodels.c)
copies pcmodels.bin to [CART_BASE+romSize, ...) and patches
file_resource_table[i].hw_address / rom_size per manifest row.
"""
import csv, struct, zlib, os, re, sys, bisect
from collections import Counter
# ---------------------------------------------------------------- config ---
REGION = "ntsc-final"
CHECK_ONLY = False
if len(sys.argv) > 1:
if sys.argv[1] == "--check-only":
CHECK_ONLY = True
elif sys.argv[1] in ("ntsc-final", "pal-final", "jpn-final"):
REGION = sys.argv[1]
ROM_PATH = f"data/ge007.{REGION}.z64"
OUT_DIR = f"data/pcmodels-{REGION}"
TABLE = "assets/obseg/file_resource_table.inc.c"
FILELIST = "scripts/filelist.u.csv"
rom = open(ROM_PATH, "rb").read()
rows = list(csv.reader(open(FILELIST)))
# nsnt from assets/**/modelFileHeader.inc.c (case-insensitive match)
nsnt = {}
for root, dirs, files in os.walk("assets"):
for fn in files:
if fn.lower().endswith("modelfileheader.inc.c"):
for line in open(os.path.join(root, fn)):
m = re.search(r"MODELFILEHEADER\((.*)\)\s*;?\s*$", line)
if not m:
continue
a = [x.strip() for x in m.group(1).split(",")]
if len(a) < 9:
continue
try:
nsnt[a[0]] = (int(a[4], 0), int(a[8], 0))
except ValueError:
pass
# file_resource_table order (manifest order) + which entries are model files
table_names = []
for line in open(TABLE):
m = re.search(r'\{\s*\w+\s*,\s*"([^"]+)"', line)
if m:
table_names.append(m.group(1))
def is_model_name(n):
return n and n[0] in "CGP" and n.endswith("Z") and "_stan" not in n
# filelist lookup by basename (with .bin stripped)
fl_by_base = {}
for r in rows:
if len(r) < 3 or not r[2]:
continue
base = r[2].rsplit("/", 1)[-1]
if base.endswith(".bin"):
base = base[:-4]
fl_by_base[base] = (int(r[0]), int(r[1]))
def find_row(name):
for cand in (name, name[1:], name[:-1], name[1:-1]):
if cand in fl_by_base:
return cand, fl_by_base[cand]
return None, None
# ---------------------------------------------------------------- helpers ---
MASK = 0xFFFFFF
def be16(b, o): return struct.unpack_from(">h", b, o)[0]
def bu16(b, o): return struct.unpack_from(">H", b, o)[0]
def be32o(b, o): return struct.unpack_from(">I", b, o)[0] & MASK
def be32r(b, o): return struct.unpack_from(">I", b, o)[0]
N64_REC = {1: 0x1C, 2: 0x1C, 3: 0x1C, 4: 0x14, 8: 0x10, 9: 0x24, 10: 0x1C,
12: 0x28, 13: 0x20, 15: 0x1C, 17: 0x20, 18: 0x08, 21: 0x14,
22: 0x10, 23: 0x02, 24: 0x20}
PC_REC = {1: 24, 2: 40, 3: 40, 4: 40, 8: 24, 9: 48, 10: 28, 12: 48,
13: 48, 15: 28, 18: 16, 21: 20, 22: 32, 23: 2, 24: 64}
PC_NODE = 48
# GDL w1 remap set: only these opcodes carry file-relative seg-5 addresses in
# w1. Verified against the ROM (all 512 files): G_VTX w1 is 0x04xxxxxx (seg 4 =
# runtime vtx buffer, game-set — leave) or 0x05xxxxxx (file-relative — remap);
# G_SETTIMG w1 is 0x05xxxxxx (embedded image blob — remap). G_LOADBLOCK (0xF3)
# w1=0x07xxxxxx is ignored by port/fast3d (fixed dpLoadBlock) — leave. All
# other opcodes carry index/mode data in w1 (GE redefined the immediate range:
# 0xB1 is a triangle command with 8-bit indices, NOT G_DL) — leave.
ADDR_OPS = {0x04, 0xFD}
def round8(x): return (x + 7) & ~7
def round16(x): return (x + 15) & ~15
def round64(x): return (x + 63) & ~63
# ------------------------------------------------- node map + visit sim ----
class N:
__slots__ = ("op", "data", "parent", "next", "prev", "child")
def build_nodes(src, D, NS, NT):
"""Mutable node map. Closure MUST match d43_convert.walk() exactly:
op8/op18 follow their Affects/Controls pointer INSTEAD of child (their
Child/Next fields are not tree links and lead into garbage)."""
R0 = 4 * NS + 12 * NT
nodes = {}
stack = [R0]
while stack:
o = stack.pop()
if o in nodes or o >= D:
continue
n = N()
n.op = bu16(src, o) & 0xff
n.data = be32o(src, o + 4)
n.parent = be32o(src, o + 8)
n.next = be32o(src, o + 0xC)
n.prev = be32o(src, o + 0x10)
n.child = be32o(src, o + 0x14)
nodes[o] = n
if n.op == 8:
q = be32o(src, n.data + 8)
if q:
stack.append(q)
elif n.op == 18:
q = be32o(src, n.data + 0)
if q:
stack.append(q)
else:
if n.child:
stack.append(n.child)
if n.next:
stack.append(n.next)
return nodes, R0
def gdl_of(src, n):
if n.op in (4, 24):
return be32o(src, n.data + 0), be32o(src, n.data + 4)
if n.op == 22:
return be32o(src, n.data + 8), 0
return 0, 0
def visit_seq(nodes, src, R0):
"""EXACT modelIterateDisplayLists GDL visit order (mutable walk)."""
seq = []
node = R0
prev_node = None
prev_gdl = 0
guard = 0
while node and guard < 200000:
guard += 1
n = nodes[node]
gdl = 0
if n.op in (4, 22, 24):
p, s = gdl_of(src, n)
if node != prev_node:
gdl = p
elif s and s != prev_gdl:
gdl = s
elif n.op == 8: # LOD rewire
aff = be32o(src, n.data + 8)
n.child = aff
elif n.op == 18: # SWITCH rewire
ctl = be32o(src, n.data + 0)
n.child = ctl
elif n.op == 9: # BSP splice (visible=TRUE)
lc, rc = be32o(src, n.data + 0x18), be32o(src, n.data + 0x1C)
node1, node2 = lc, rc
if node1:
n.child = node1
nodes[node1].prev = 0
loop = node1
while nodes[loop].next and nodes[loop].next != node2:
loop = nodes[loop].next
nodes[loop].next = node2
if node2:
nodes[node2].prev = loop
loop = node2
while nodes[loop].next and nodes[loop].next != node1:
loop = nodes[loop].next
nodes[loop].next = 0
else:
n.child = node2
if node2:
nodes[node2].prev = 0
if gdl:
seq.append(gdl)
prev_node = node
prev_gdl = gdl
continue
prev_node = None
if n.child:
node = n.child
else:
while node:
nn = nodes[node].next
if nn:
node = nn
break
node = nodes[node].parent
return seq
def placement_order(nodes, src, R0):
"""d43_convert walk(): next-first DFS stack (node placement order only;
GDL packing uses visit_seq). Returns [(n64_off, op)] in placement order."""
seen = set()
stack = [R0]
out = []
while stack:
o = stack.pop()
if o in seen or o >= len(src):
continue
seen.add(o)
n = nodes[o]
out.append((o, n.op))
child, nxt = n.child, n.next
if n.op == 8:
a = be32o(src, n.data + 8)
if a:
stack.append(a)
elif n.op == 18:
c = be32o(src, n.data + 0)
if c:
stack.append(c)
else:
if child:
stack.append(child)
if nxt:
stack.append(nxt)
return out
def gdl_end(src, D, g):
o = g
while o + 8 <= D:
if (be32r(src, o) >> 24) == 0xB8:
return o + 8
o += 8
return D
# ------------------------------------------------------------- per file ----
errors = []
stats = Counter()
results = {} # name -> dict
def process(name):
key, (addr, size) = find_row(name)
if not key:
errors.append(f"{name}: no filelist row")
return
src = zlib.decompress(rom[addr:addr + size][2:], -15)
D = len(src)
# nsnt is keyed by header symbol (e.g. "armourguard"), not file base
hkey = None
for cand in (key, key[1:], key[:-1], key[1:-1]):
if cand in nsnt:
hkey = cand
break
if not hkey:
errors.append(f"{name}: no header data")
return
NS, NT = nsnt[hkey]
nodes, R0 = build_nodes(src, D, NS, NT)
if not nodes:
errors.append(f"{name}: empty tree")
return
seq = visit_seq(nodes, src, R0) # GDL packing order
placed = placement_order(nodes, src, R0) # object placement order
# ---- layout pass (d43_convert verbatim) ----
regions = [] # (old_off, old_size, new_off)
def add_region(old, osz, cur):
regions.append((old, osz, cur))
return cur + osz
dstpos = 0
# switch table: u64 node VMAs on PC (R0_pc = 8*NS + 12*NT)
for i in range(NS):
dstpos = add_region(4 * i, 8, dstpos)
for i in range(NT):
dstpos = add_region(4 * NS + 12 * i, 12, dstpos)
node_newoff = {}
rec_newoff = {}
vtx_regions = []
zero_vtx = []
for (no, op) in placed:
data = be32o(src, no + 4)
# PC ModelNode is 48 bytes (5 x u64 pointers), not the N64 24
dstpos = add_region(no, 48, dstpos)
node_newoff[no] = dstpos - 48
if op not in N64_REC:
errors.append(f"{name}: unknown opcode {op} at {no:#x}")
continue
nsz, psz = N64_REC[op], PC_REC[op]
rec_newoff[data] = dstpos
# allocate the PC footprint (emit writes psz bytes; nsz would let the
# next region start inside this record's struct)
dstpos = add_region(data, psz, dstpos)
if op == 4:
nv = bu16(src, data + 0x10); vo = be32o(src, data + 0xC)
if vo:
sz = 16 * nv if nv else None
if sz is None:
zero_vtx.append((vo,))
else:
vtx_regions.append((vo, nv))
if nv:
dstpos = add_region(vo, sz, dstpos)
elif op == 24:
nv = be16(src, data + 0xC); ncv = be16(src, data + 0xE)
vo = be32o(src, data + 8); cvo = be32o(src, data + 0x10)
puo = be32o(src, data + 0x14)
if nv and vo:
vtx_regions.append((vo, nv)); dstpos = add_region(vo, 16 * nv, dstpos)
if ncv and cvo:
op24_is_collision[cvo] = True
vtx_regions.append((cvo, ncv)); dstpos = add_region(cvo, 16 * ncv, dstpos)
if nv and puo:
dstpos = add_region(puo, 2 * nv, dstpos)
elif op == 22:
nv = struct.unpack_from(">i", src, data)[0]; vo = be32o(src, data + 4)
if nv and vo:
vtx_regions.append((vo, nv)); dstpos = add_region(vo, 16 * nv, dstpos)
# All record-referenced GDLs (needed before zero-vtx/blob layout so the
# "next object" boundaries include them).
ref_gdls = []
for (no, op) in placed:
data = be32o(src, no + 4)
if op == 4 or op == 24:
for off in (0, 4):
q = be32o(src, data + off)
if q and q not in ref_gdls:
ref_gdls.append(q)
elif op == 22:
q = be32o(src, data + 8)
if q and q not in ref_gdls:
ref_gdls.append(q)
visited_set = set(seq)
unvisited = sorted(g for g in ref_gdls if g not in visited_set)
all_gdls = unvisited + list(seq) # pack order
obj_offs_now = sorted(set(no for no, _ in placed) | set(rec_newoff.keys()) |
set(v[0] for v in vtx_regions) | set(all_gdls))
for (vo,) in zero_vtx:
nxt = min((o for o in obj_offs_now if o > vo), default=D)
sz = nxt - vo
if sz <= 0 or sz % 16:
errors.append(f"{name}: zero-vtx array {vo:#x} bad size {sz:#x}")
else:
vtx_regions.append((vo, sz // 16))
dstpos = add_region(vo, sz, dstpos)
# Embedded image blobs: G_SETTIMG seg-5 targets (P* logo/legal files only).
# texLoadFromGdl copies G_SETTIMG slots verbatim, so the remapped w1 must
# point at the relocated pixels. Each span runs from its target to the next
# placed object; overlapping spans merge (PlegalpageZ's 5 images share one
# unclaimed span). Bytes are copied raw — the RDP reads them as-is.
blob_targets = []
for g in all_gdls:
o = g
while o + 8 <= D:
w0 = be32r(src, o); c = w0 >> 24
if c == 0xB8:
break
if c == 0xFD and (be32r(src, o + 4) >> 24) == 5:
blob_targets.append(be32r(src, o + 4) & MASK)
o += 8
placed_offs = set(o for o, _, _ in regions)
intervals = []
for t in sorted(set(blob_targets)):
end = min((o for o in placed_offs if o > t), default=D)
intervals.append([t, end])
merged = []
for iv in intervals:
if merged and iv[0] <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], iv[1])
else:
merged.append(iv)
for (t, e) in merged:
dstpos = add_region(t, e - t, dstpos)
# GDLs last, tight-packed 16B slots. Visited GDLs go in VISIT order (the
# compaction span arithmetic requires it). Referenced-but-unvisited GDLs
# (e.g. PexplosionbitZ: compaction skips the file entirely when iterate
# visits zero GDLs) are packed just before the visited block so they stay
# outside [off_first, D_PC) and are never span-read; if a runtime BSP
# visible=FALSE state renders them, they are still valid 16B slots.
gdl_newoff = {}
total_slots = 0
def pack_gdl(g):
nonlocal dstpos, total_slots
end = gdl_end(src, D, g)
nslots = (end - g) // 8
if (end - g) % 8:
errors.append(f"{name}: GDL {g:#x} not 8B aligned end")
gdl_newoff[g] = dstpos
dstpos += 16 * nslots
total_slots += nslots
# region size = NEW footprint (16B slots); the old 8B bytes are not a
# 1:1 byte map, so inv_remap must never be called inside a GDL region
# (GDL pointers go through gdl_newoff, not put_ptr).
regions.append((g, 16 * nslots, gdl_newoff[g]))
for g in unvisited:
pack_gdl(g)
for g in seq:
pack_gdl(g)
D_PC = dstpos
G_PC = min(gdl_newoff.values()) if gdl_newoff else D_PC
g1_n64 = min(seq) if seq else D
# ---- P_final estimate (D45/D46 marker-expansion worst case) ----
# Runtime footprint after compaction + texLoadFromGdl expansion:
# P_off = g1_visited_new + 16*S_visited + 16*sum(K_t-1 over visited markers)
# <= D_PC + 16*sum(K_t-1 over ALL referenced-GDL markers)
# K_BOUND: exact worst-case output slots per marker type (tex.c analysis,
# verified over all files by d43_lutscan.py; unknown types default to 44).
# Markers are counted over N64 spans [g, next_gdl_or_EOF) of every
# referenced GDL (superset of what the PC runtime processes: unvisited
# GDLs and inter-GDL junk are never span-read on PC).
K_BOUND = {0: 38, 1: 46, 2: 36, 3: 18, 4: 15}
kslack = 0
gsorted = sorted(set(all_gdls))
for i, gg in enumerate(gsorted):
e = gsorted[i + 1] if i + 1 < len(gsorted) else D
j = gg
while j + 8 <= e:
w0 = be32r(src, j)
if (w0 >> 24) == 0xC0:
kslack += K_BOUND.get(w0 & 7, 44) - 1
j += 8
P_est = D_PC + 16 * kslack
# ---- remap machinery ----
regions.sort()
rold = [x[0] for x in regions]
def remap(off):
if off == 0:
return 0
i = bisect.bisect_right(rold, off) - 1
if i < 0:
return None
o, osz, n = regions[i]
if o <= off < o + osz:
return n + (off - o)
return None
# NOTE: `regions` is sorted by OLD offset above; new offsets follow DFS
# placement order and are NOT sorted, so inv_remap needs its own index.
rsorted_new = sorted(regions, key=lambda x: x[2])
rnew_offs = [x[2] for x in rsorted_new]
def inv_remap(off):
"""PC offset -> N64 offset (for round-trip validation)."""
i = bisect.bisect_right(rnew_offs, off) - 1
if i < 0:
return None
o, osz, n = rsorted_new[i]
if n <= off < n + osz:
return o + (off - n)
return None
# ---- emit pass ----
buf = bytearray(D_PC)
def put_u16(o, v): buf[o:o + 2] = struct.pack("<H", v & 0xFFFF)
def put_s16(o, v): buf[o:o + 2] = struct.pack("<H", v & 0xFFFF) # bytes identical to <h
def put_u32(o, v): buf[o:o + 4] = struct.pack("<I", v & 0xFFFFFFFF)
def put_f32(o, doff): buf[o:o + 4] = src[doff + 4:doff:-1] # bswap32 (4 bytes!)
def put_ptr(o, old_off):
if not old_off:
return
n = remap(old_off)
if n is None:
errors.append(f"{name}: ptr {old_off:#x} not in region map")
n = 0
buf[o:o + 4] = struct.pack("<I", 0x05000000 | (n & MASK))
buf[o + 4:o + 8] = b"\x00" * 4
# switches: NS x u64 (VMA of node)
for i in range(NS):
put_ptr(8 * i, be32o(src, 4 * i))
# texconfigs: NT x 12B
for i in range(NT):
doff = 4 * NS + 12 * i
npos = 8 * NS + 12 * i
tid = be32r(src, doff)
put_u32(npos, tid) # bswap32 -> LE (seg-5 VMA included:
buf[npos + 4:npos + 12] = src[doff + 4:doff + 12] # game skips >= MAX_TEXTURES on both platforms)
if (tid >> 24) == 5 and remap(tid & MASK) is None:
errors.append(f"{name}: texconfig seg-5 target {tid & MASK:#x} not in a blob region")
# nodes + records
for (no, op) in placed:
npos = node_newoff[no]
put_u16(npos, bu16(src, no)) # Opcode
put_ptr(npos + 8, be32o(src, no + 4)) # Data
put_ptr(npos + 16, be32o(src, no + 8)) # Parent
put_ptr(npos + 24, be32o(src, no + 0xC)) # Next
put_ptr(npos + 32, be32o(src, no + 0x10)) # Prev
put_ptr(npos + 40, be32o(src, no + 0x14)) # Child
d = be32o(src, no + 4)
r = rec_newoff[d]
if op == 1: # HeaderRecord
put_u16(r, bu16(src, d)); put_s16(r + 2, be16(src, d + 2))
put_ptr(r + 8, be32o(src, d + 4))
put_u16(r + 16, bu16(src, d + 8)); put_u16(r + 18, bu16(src, d + 0xA))
put_u16(r + 20, bu16(src, d + 0xC))
elif op == 2 or op == 3: # GroupRecord / OP03
for i in range(3): put_f32(r + 4 * i, d + 4 * i)
put_u16(r + 12, bu16(src, d + 0xC))
for i in range(3): put_s16(r + 14 + 2 * i, be16(src, d + 0xE + 2 * i))
put_ptr(r + 24, be32o(src, d + 0x14))
put_f32(r + 32, d + 0x18)
elif op == 4: # DisplayListRecord
p = be32o(src, d); s = be32o(src, d + 4)
if p: buf[r:r + 4] = struct.pack("<I", 0x05000000 | (gdl_newoff[p] & MASK))
if s: buf[r + 8:r + 12] = struct.pack("<I", 0x05000000 | (gdl_newoff[s] & MASK))
put_ptr(r + 24, be32o(src, d + 0xC))
put_u16(r + 32, bu16(src, d + 0x10))
buf[r + 34] = src[d + 0x12] # ModelType s8 raw
elif op == 8: # LODRecord
put_f32(r, d); put_f32(r + 4, d + 4)
put_ptr(r + 8, be32o(src, d + 8))
put_u16(r + 16, bu16(src, d + 0xC))
elif op == 9: # BSPRecord
for i in range(3): put_f32(r + 4 * i, d + 4 * i)
for i in range(3): put_f32(r + 12 + 4 * i, d + 0xC + 4 * i)
put_ptr(r + 24, be32o(src, d + 0x18))
put_ptr(r + 32, be32o(src, d + 0x1C))
put_u16(r + 42, bu16(src, d + 0x22))
elif op == 10: # BoundingBoxRecord
put_u32(r, be32r(src, d))
for i in range(6): put_f32(r + 4 + 4 * i, d + 4 + 4 * i)
elif op == 12: # GunfireRecord
for i in range(3): put_f32(r + 4 * i, d + 4 * i)
for i in range(3): put_f32(r + 12 + 4 * i, d + 0xC + 4 * i)
put_ptr(r + 24, be32o(src, d + 0x18))
put_f32(r + 32, d + 0x1C)
put_u16(r + 36, bu16(src, d + 0x20))
elif op == 13: # ShadowRecord
for i in range(2): put_f32(r + 4 * i, d + 4 * i)
for i in range(2): put_f32(r + 8 + 4 * i, d + 8 + 4 * i)
put_ptr(r + 16, be32o(src, d + 0x10))
put_ptr(r + 24, be32o(src, d + 0x14))
put_f32(r + 32, d + 0x18)
elif op == 15: # InterlinkageRecord
for i in range(3): put_f32(r + 4 * i, d + 4 * i)
for i in range(3): put_f32(r + 12 + 4 * i, d + 0xC + 4 * i)
put_f32(r + 24, d + 0x18)
elif op == 18: # SwitchRecord
put_ptr(r, be32o(src, d))
put_u16(r + 8, bu16(src, d + 4))
elif op == 21: # GroupSimpleRecord
for i in range(3): put_f32(r + 4 * i, d + 4 * i)
put_s16(r + 12, be16(src, d + 0xC)); put_u16(r + 14, bu16(src, d + 0xE))
put_f32(r + 16, d + 0x10)
elif op == 22: # DisplayListPrimaryRecord
put_u32(r, be32r(src, d)) # numVertices s32 bswap
put_ptr(r + 8, be32o(src, d + 4))
p = be32o(src, d + 8)
if p: buf[r + 16:r + 20] = struct.pack("<I", 0x05000000 | (gdl_newoff[p] & MASK))
elif op == 23: # HeadPlaceholderRecord
put_u16(r, bu16(src, d))
elif op == 24: # DisplayList_CollisionRecord
p = be32o(src, d); s = be32o(src, d + 4)
if p: buf[r:r + 4] = struct.pack("<I", 0x05000000 | (gdl_newoff[p] & MASK))
if s: buf[r + 8:r + 12] = struct.pack("<I", 0x05000000 | (gdl_newoff[s] & MASK))
put_ptr(r + 16, be32o(src, d + 8))
put_s16(r + 24, be16(src, d + 0xC)); put_s16(r + 26, be16(src, d + 0xE))
put_ptr(r + 32, be32o(src, d + 0x10))
put_ptr(r + 40, be32o(src, d + 0x14))
put_s16(r + 48, be16(src, d + 0x18)); put_u16(r + 50, bu16(src, d + 0x1A))
# vertex arrays
def emit_main_vtx(vo, nv, npos):
for i in range(nv):
o = vo + 16 * i; q = npos + 16 * i
put_s16(q, be16(src, o)); put_s16(q + 2, be16(src, o + 2))
put_s16(q + 4, be16(src, o + 4)); put_s16(q + 6, be16(src, o + 6))
put_s16(q + 8, be16(src, o + 8)); put_s16(q + 10, be16(src, o + 0xA))
buf[q + 12:q + 16] = src[o + 0xC:o + 0x10]
for (vo, nv) in vtx_regions:
npos = remap(vo)
if op24_is_collision.get(vo):
cvo_npos = npos
for i in range(nv):
o = vo + 16 * i; q = cvo_npos + 16 * i
put_s16(q, be16(src, o)); put_s16(q + 2, be16(src, o + 2))
put_s16(q + 4, be16(src, o + 4)); put_s16(q + 6, be16(src, o + 6))
v = be32r(src, o + 8)
if v == 0:
pass
elif (v >> 24) == 5:
n = remap(v & MASK)
if n is None:
errors.append(f"{name}: LinkedTo {v:#x} not in map")
n = 0
put_u32(q + 8, 0x05000000 | (n & MASK))
else:
errors.append(f"{name}: LinkedTo {v:#x} neither null nor seg-5")
put_s16(q + 12, be16(src, o + 0xC)); put_s16(q + 14, be16(src, o + 0xE))
else:
emit_main_vtx(vo, nv, npos)
# GDLs (pack order: unvisited first, then visit order), 16B LE slots
for g in all_gdls:
end = gdl_end(src, D, g)
npos = gdl_newoff[g]
j = 0
o = g
while o + 8 <= end:
w0 = be32r(src, o); w1 = be32r(src, o + 4)
q = npos + 16 * j
# PC Gfx slot = {u64 words.w0; u64 words.w1} (Gwords uses uintptr_t).
# Both the game (tex.c: (u8)(words.w0>>24), words.w1 & 0xfff) and
# fast3d (opcode = words.w0>>24 unmasked, seg_addr(words.w1)) read
# N64 w0 from the LOW dword of bytes[0..8) and N64 w1 from the LOW
# dword of bytes[8..16). High dwords must be zero (gSP* macros
# zero-extend their 32-bit patterns into the u64 fields).
c = w0 >> 24
if c in ADDR_OPS and (w1 >> 24) == 5:
n = remap(w1 & MASK)
if n is None:
errors.append(f"{name}: GDL {g:#x} op{c:#x} w1 {w1:#x} not in map")
n = 0
w1 = 0x05000000 | (n & MASK)
buf[q:q + 8] = struct.pack("<II", w0, 0) # words.w0 = w0
buf[q + 8:q + 16] = struct.pack("<II", w1, 0) # words.w1 = w1
if c == 0xB8:
break
j += 1
o += 8
# ---- round-trip validation ----
validate(name, src, D, NS, NT, nodes, R0, placed, all_gdls, buf, D_PC,
regions, node_newoff, rec_newoff, gdl_newoff, remap, inv_remap)
# ---- compress ----
co = zlib.compressobj(6, zlib.DEFLATED, -15)
comp = b"\x11\x72" + co.compress(bytes(buf)) + co.flush()
C_PC = len(comp)
results[name] = dict(C=C_PC, D=D_PC, Dn64=D, S=total_slots,
Bpc=D_PC - 16 * total_slots, Bn64=g1_n64,
Pest=P_est,
comp=comp if not CHECK_ONLY else None,
NS=NS, NT=NT)
# collision-vertex set (filled during layout; emit pass needs it)
op24_is_collision = {}
def validate(name, src, D, NS, NT, nodes, R0, placed, all_gdls, buf, D_PC,
regions, node_newoff, rec_newoff, gdl_newoff, remap, inv_remap):
def err(m): errors.append(f"{name}: RT {m}")
# tiling: regions cover [0, D_PC) exactly
rs = sorted(regions, key=lambda x: x[2])
pos = 0
for o, osz, n in rs:
if n != pos or n + osz > D_PC:
err(f"tiling gap/overlap at {pos:#x} (region {o:#x}->{n:#x})")
return
pos += osz
if pos != D_PC:
err(f"tiling ends at {pos:#x} != D_PC {D_PC:#x}")
return
# node fields
for (no, op) in placed:
npos = node_newoff[no]
if bu16(src, no) != struct.unpack_from("<H", buf, npos)[0]:
err(f"node {no:#x} opcode mismatch")
for f, pf in ((4, 8), (8, 16), (0xC, 24), (0x10, 32), (0x14, 40)):
old = be32o(src, no + f)
v = struct.unpack_from("<Q", buf, npos + pf)[0]
if old == 0:
if v != 0: err(f"node {no:#x}+{f:#x}: expected null")
else:
want = remap(old)
if (v >> 24) != 5 or (v & MASK) != want:
err(f"node {no:#x}+{f:#x}: {v:#x} != 0x05|{want and hex(want)}")
# record fields (spot-check every pointer + scalar via inverse remap)
ptrfields = {1: [4], 2: [0x14], 3: [0x14], 8: [8], 9: [0x18, 0x1C],
12: [0x18], 13: [0x10, 0x14], 18: [0], 24: [8, 0x10, 0x14],
4: [0xC], 22: [4]}
for (no, op) in placed:
d = be32o(src, no + 4)
r = rec_newoff[d]
for f in ptrfields.get(op, []):
old = be32o(src, d + f)
v = struct.unpack_from("<Q", buf, r + PC_PTR_OFF[op][f])[0]
if old == 0:
if v != 0: err(f"rec{op} {d:#x}+{f:#x}: expected null")
else:
want = remap(old)
if (v >> 24) != 5 or (v & MASK) != want:
err(f"rec{op} {d:#x}+{f:#x}: {v:#x}")
# GDL slots (all packed GDLs)
for g in all_gdls:
end = gdl_end(src, D, g)
npos = gdl_newoff[g]
j = 0; o = g
while o + 8 <= end:
w0 = be32r(src, o); w1 = be32r(src, o + 4)
q = npos + 16 * j
if struct.unpack_from("<I", buf, q)[0] != w0:
err(f"gdl {g:#x} slot {j}: w0 mismatch")
c = w0 >> 24
if struct.unpack_from("<I", buf, q + 4)[0] != 0:
err(f"gdl {g:#x} slot {j}: pad[4..8) nonzero")
w1p = struct.unpack_from("<I", buf, q + 8)[0]
if c in ADDR_OPS and (w1 >> 24) == 5:
want = remap(w1 & MASK)
if w1p != (0x05000000 | want):
err(f"gdl {g:#x} slot {j}: w1 {w1p:#x} != 0x05|{hex(want)}")
elif w1p != w1:
err(f"gdl {g:#x} slot {j}: w1 passthrough mismatch")
if struct.unpack_from("<I", buf, q + 12)[0] != 0:
err(f"gdl {g:#x} slot {j}: pad[12..16) nonzero")
if c == 0xB8:
break
j += 1; o += 8
# PC pointer offsets per (op, n64 field) — for round-trip validation
PC_PTR_OFF = {
1: {4: 8}, 2: {0x14: 24}, 3: {0x14: 24}, 8: {8: 8},
9: {0x18: 24, 0x1C: 32}, 12: {0x18: 24}, 13: {0x10: 16, 0x14: 24},
18: {0: 0}, 24: {8: 16, 0x10: 32, 0x14: 40}, 4: {0xC: 24}, 22: {4: 8},
}
# ------------------------------------------------------------------- main ---
model_names = [n for n in table_names if is_model_name(n)]
stats["table_models"] = len(model_names)
for name in model_names:
# reset per-file collision set before layout (populated below)
op24_is_collision.clear()
process(name)
if errors:
print(f"\n{len(errors)} ERRORS:")
for e in errors[:40]:
print(" ", e)
sys.exit(1)
print(f"files converted: {len(results)} / {stats['table_models']}")
tot = sum(round8(r["C"]) for r in results.values())
print(f"sum round8(C_pc) = {tot} max C = {hex(max(r['C'] for r in results.values()))}"
f" max D = {hex(max(r['D'] for r in results.values()))}")
# ------------------------------------------------------- cross-checks ------
DIRECT = {
"Csuit_lf_handZ": 0x18000,
"GtriggerZ": 0x17000, "GwatchlaserZ": 0x17000,
"PwalletbondZ": 0x17000,
"PlegalpageZ": 0x3C000, "PnintendologoZ": 0x3C000, "PgoldeneyelogoZ": 0x3C000,
}
WEAPON_BUF = 0xF000
fail = 0
def chk(cond, msg):
global fail
tag = "OK " if cond else "FAIL"
if not cond:
fail += 1
print(f"{tag} {msg}")
print("\n== single-file dst!=0 buffers ==")
for name, b in DIRECT.items():
r = results.get(name)
if not r:
chk(False, f"{name}: missing"); continue
ok = (round8(r["C"]) + 8 <= b and r["D"] + round8(r["C"]) <= b
and r["Pest"] <= b)
chk(ok, f"{name:20s} C={r['C']:>6} D={r['D']:>6} round8(C)+8={round8(r['C'])+8:#7x} "
f"D+r8C={r['D']+round8(r['C']):#7x} P_est={r['Pest']:>#7x} vs {b:#x}")
# gun.c used_to_load_1st_person_model_on_demand: every item model except
# ITEM_SUIT_LF_HAND (Csuit_lf_handZ, 0x18000) and ITEM_TRIGGER/ITEM_WATCHLASER
# (GtriggerZ/GwatchlaserZ, 0x17000 — both in DIRECT above) loads into the
# D_80032464 weapon buffer. G* files are exactly the item models.
GUN_BUF_FILES = [n for n in results
if n.startswith("G") and n.endswith("Z")
and n not in ("GtriggerZ", "GwatchlaserZ")]
worst_w = None
for n in GUN_BUF_FILES:
r = results[n]
if worst_w is None or r["Pest"] > worst_w[0]:
worst_w = (r["Pest"], n, r)
chk(worst_w[0] <= WEAPON_BUF and worst_w[2]["D"] + round8(worst_w[2]["C"]) <= WEAPON_BUF
and round8(worst_w[2]["C"]) + 8 <= WEAPON_BUF,
f"worst gun-buffer item {worst_w[1]}: P_est={worst_w[0]:#x}, D+r8C={worst_w[2]['D']+round8(worst_w[2]['C']):#x} "
f"r8C+8={round8(worst_w[2]['C'])+8:#x} vs {WEAPON_BUF:#x}")
def chain_check(label, slots, buf_pc):
"""Chain fit: the game advances bufferPtr by each file's post-load
poolRemaining (>= its actual footprint P_off), so the chain works iff
sum(P_off_i) <= R_pc (prefix argument: A_j >= P_off_j)
with P_off_i bounded by P_est_i (D45/D46 marker-expansion worst case).
Slots are independent (any body+head+weapon can be drawn), so the worst
combination is the sum of per-slot maxima. Each slot's DMA/decompress
constraints are checked against a lower bound on its remaining buffer.
"""
per_slot = []
for sl, cands in slots:
best = None
for n in cands:
r = results.get(n)
if not r:
continue
if best is None or r["Pest"] > best[0]:
best = (r["Pest"], n, r)
if best is None:
chk(False, f"{label}/{sl}: no candidates"); continue
per_slot.append((sl, best[1], best[2]))
total = sum(r["Pest"] for _, _, r in per_slot)
chk(total <= buf_pc,
f"{label}: worst combination {total:#x} vs buffer {buf_pc:#x} "
+ " ".join(f"{sl}:{n}(P_est={r['Pest']:#x})" for sl, n, r in per_slot))
rem = buf_pc
for sl, n, r in per_slot:
ok = round8(r["C"]) + 8 <= rem and r["D"] + round8(r["C"]) <= rem
chk(ok, f"{label}/{sl}: worst {n} C={r['C']:#x} D={r['D']:#x} "
f"round8(C)+8={round8(r['C'])+8:#x} D+r8C={r['D']+round8(r['C']):#x} "
f"vs rem-lower={rem:#x}")
rem -= round64(r["Pest"])
# candidate sets
heads = [n for n in results if n.startswith("Chead")]
bodies = [n for n in results if n.startswith("C") and n.endswith("Z")
and not n.startswith("Chead") and n != "Csuit_lf_handZ"]
# cast screen (front.c random_rifles_in_intro / random_pistols_in_intro):
CAST_WEAPONS = ["PchrkalashZ", "Pchrm16Z", "Pchrfnp90Z", "PchrautoshotZ",
"PchrgrenadelaunchZ", "PchrsniperrifleZ",
"PchrwppkZ", "PchrwppksilZ", "PchrskorpionZ", "PchruziZ",
"Pchrtt33Z", "PchrrugerZ", "PchrlaserZ", "PchrgoldenZ"]
# bondview2 held prop (player.c getPropForHeldItem):
BONDVIEW_PROPS = ["PchrknifeZ", "PchrwppkZ", "PchrskorpionZ", "PchrkalashZ",
"PchruziZ", "Pchrmp5kZ", "PchrspectreZ", "Pchrm16Z",
"Pchrfnp90Z", "PchrshotgunZ", "PchrgrenadelaunchZ",
"PchrgrenadeZ", "PchrremotemineZ", "PchrproximitymineZ",
"PchrtimedmineZ", "PchrrugerZ", "PchrlaserZ",
"PchrsniperrifleZ", "Pchrmp5ksilZ", "Pchrtt33Z",
"PchrwppksilZ", "PchrthrowknifeZ", "PchrautoshotZ",
"PchrrocketlaunchZ", "PchrgoldenZ", "PflagZ"]
for n in CAST_WEAPONS + BONDVIEW_PROPS:
chk(n in results, f"candidate present: {n}")
print("\n== cast screen chain (front.c:7811/7819/7869; PC 0x25000, N64 0x18160) ==")
chain_check("cast", [("body", bodies), ("head", heads), ("weapon", CAST_WEAPONS)],
0x25000)
print("\n== title gunbarrel chain (title.c:489/495/517; avail 0x24D00 both sides) ==")
# fixed files (c_item_entries indexed by BODIES enum; BODY_Male_Karl=HEAD_START
# is an alias so the effective count is 80, matching the 80 record includes):
# BODY_Brosnan_Tuxedo=5 -> CdjbondZ
# BODY_Male_Pierce_Bond_Tuxedo=78 -> CheadbrosnanZ (used as the head slot)
# PitemZ_entries[PROP_CHRWPPK] -> PchrwppkZ
t1 = ["CdjbondZ"]
t2 = ["CheadbrosnanZ"]
t3 = ["PchrwppkZ"]
chain_check("title", [("brosnan", t1), ("bond", t2), ("wppk", t3)],
0x24D00)
print("\n== bondview2 chain (bondview2.c:490/502/571; PC 0x23000, N64 0x14820) ==")
chain_check("bondview", [("body", bodies), ("head", heads), ("prop", BONDVIEW_PROPS)],
0x23000)
print("\n== stage bank (dst==0 fresh loads) ==")
maxr = max(round8(r["C"]) + 8 for r in results.values())
chk(maxr <= 0x24C400, f"max round8(C)+8 = {maxr:#x} vs STAGE bank 0x24C400")
# N64 sanity: same overlap check with N64 sizes (the game worked on N64)
print("\n== N64 sanity (overlap model) ==")
n64_fail = 0
for name, r in results.items():
# recompute C_n64 cheaply? we only stored Dn64; approximate with stored comp? no.
pass
# (N64 compressed sizes: recomputed below from ROM for the DIRECT set only)
for name, b in DIRECT.items():
key, (addr, size) = find_row(name)
Cn = size - 2
Dn = results[name]["Dn64"]
ok = round8(Cn) + 8 <= b and Dn + round8(Cn) <= b
if not ok:
n64_fail += 1
print(f" N64 {name}: round8(C)+8={round8(Cn)+8:#x} D+r8C={Dn+round8(Cn):#x} vs {b:#x}")
print(f"N64 overlap-model violations: {n64_fail} (0 expected; if >0 the overlap "
f"constraint is looser than modeled — review before trusting PC checks)")
# ---------------------------------------------------------------- output ---
if CHECK_ONLY:
print("\ncheck-only: no sidecars written")
else:
os.makedirs(OUT_DIR, exist_ok=True)
manifest = [] # (name, offset, size) in table order
off = 0
chunks = []
for name in model_names:
r = results[name]
start = (off + 15) & ~15
if start > off:
chunks.append(b"\x00" * (start - off))
off = start
manifest.append((name, off, r["C"]))
chunks.append(r["comp"])
off += r["C"]
total = (off + 15) & ~15
if off % 16:
chunks.append(b"\x00" * (16 - (off % 16)))
with open(os.path.join(OUT_DIR, "pcmodels.bin"), "wb") as f:
for c in chunks:
f.write(c)
# decimal fields: the C parser uses strtol(..., 10)
with open(os.path.join(OUT_DIR, "manifest.csv"), "w", newline="") as f:
w = csv.writer(f)
w.writerow(["name", "offset", "size"])
for name, o, s in manifest:
w.writerow([name, o, s])
print(f"\nwrote {len(manifest)} sidecars to {OUT_DIR}/pcmodels.bin")
print(f"total reservation needed: {total:#x} bytes "
f"(romSize + this must fit the extended VirtualAlloc)")
if fail:
print(f"\n{fail} CHECK FAILURES")
sys.exit(1)
print("\nALL CHECKS PASSED")
+112
View File
@@ -0,0 +1,112 @@
/* D43 Plan B: compiler-verified PC layout probe for model-file records.
*
* Prints sizeof/offsetof for every struct the offline emit pass (d43_emit.py)
* must reproduce byte-for-byte, compiled with the SAME toolchain + includes as
* the game build (x86_64-w64-mingw32-gcc, port/shim first). Run:
* x86_64-w64-mingw32-gcc -DPORT -Iport/shim -I. -Iinclude -Iinclude/PR \
* -Isrc -Isrc/game -Isrc/libultra -Isrc/libultra/audio -Iport/include \
* tools_pc/d43_layoutprobe.c -o build-pc/d43_layoutprobe.exe
*/
#include <stdio.h>
#include <ultra64.h>
#include <bondtypes.h>
/* The decomp's include/stddef.h is an empty guard (no offsetof); use the GCC
* builtin so this probe works in a C TU with the project include chain. */
#ifndef offsetof
#define offsetof(type, member) __builtin_offsetof(type, member)
#endif
#define P_SZ(t) printf("SZ %-40s %lu\n", #t, (unsigned long)sizeof(t))
#define P_OFF(t, f) printf("OFF %-32s %-12s %lu\n", #t, #f, (unsigned long)offsetof(t, f))
int main(void)
{
P_SZ(ModelNode);
P_OFF(ModelNode, Opcode); P_OFF(ModelNode, Data); P_OFF(ModelNode, Parent);
P_OFF(ModelNode, Next); P_OFF(ModelNode, Prev); P_OFF(ModelNode, Child);
P_SZ(Vertex);
P_OFF(Vertex, coord); P_OFF(Vertex, index); P_OFF(Vertex, s);
P_OFF(Vertex, CollisionRelatedIndex); P_OFF(Vertex, CollisionReserved);
P_SZ(ModelFileHeader);
P_OFF(ModelFileHeader, RootNode); P_OFF(ModelFileHeader, Switches);
P_OFF(ModelFileHeader, numSwitches); P_OFF(ModelFileHeader, numtextures);
P_OFF(ModelFileHeader, Textures);
P_SZ(ModelFileTextures);
P_OFF(ModelFileTextures, TextureID); P_OFF(ModelFileTextures, Width);
P_OFF(ModelFileTextures, RenderDepth); P_OFF(ModelFileTextures, sflags);
P_SZ(ModelRoData_HeaderRecord);
P_OFF(ModelRoData_HeaderRecord, AnimPart); P_OFF(ModelRoData_HeaderRecord, MatrixIndex);
P_OFF(ModelRoData_HeaderRecord, FirstGroup); P_OFF(ModelRoData_HeaderRecord, Group1);
P_OFF(ModelRoData_HeaderRecord, RwDataIndex);
P_SZ(ModelRoData_GroupRecord);
P_OFF(ModelRoData_GroupRecord, Origin); P_OFF(ModelRoData_GroupRecord, JointID);
P_OFF(ModelRoData_GroupRecord, MatrixID0); P_OFF(ModelRoData_GroupRecord, ChildGroup);
P_OFF(ModelRoData_GroupRecord, BoundingVolumeRadius);
P_SZ(ModelRoData_DisplayListRecord);
P_OFF(ModelRoData_DisplayListRecord, Primary); P_OFF(ModelRoData_DisplayListRecord, Secondary);
P_OFF(ModelRoData_DisplayListRecord, BaseAddr); P_OFF(ModelRoData_DisplayListRecord, Vertices);
P_OFF(ModelRoData_DisplayListRecord, numVertices); P_OFF(ModelRoData_DisplayListRecord, ModelType);
P_SZ(ModelRoData_LODRecord);
P_OFF(ModelRoData_LODRecord, MinDistance); P_OFF(ModelRoData_LODRecord, MaxDistance);
P_OFF(ModelRoData_LODRecord, Affects); P_OFF(ModelRoData_LODRecord, RwDataIndex);
P_SZ(ModelRoData_BSPRecord);
P_OFF(ModelRoData_BSPRecord, Point); P_OFF(ModelRoData_BSPRecord, Vector);
P_OFF(ModelRoData_BSPRecord, leftChild); P_OFF(ModelRoData_BSPRecord, rightChild);
P_OFF(ModelRoData_BSPRecord, RwDataIndex);
P_SZ(ModelRoData_BoundingBoxRecord);
P_OFF(ModelRoData_BoundingBoxRecord, ModelNumber); P_OFF(ModelRoData_BoundingBoxRecord, Bounds);
P_SZ(ModelRoData_GunfireRecord);
P_OFF(ModelRoData_GunfireRecord, Offset); P_OFF(ModelRoData_GunfireRecord, Size);
P_OFF(ModelRoData_GunfireRecord, Image); P_OFF(ModelRoData_GunfireRecord, Scale);
P_OFF(ModelRoData_GunfireRecord, RwDataIndex);
P_SZ(ModelRoData_ShadowRecord);
P_OFF(ModelRoData_ShadowRecord, pos); P_OFF(ModelRoData_ShadowRecord, size);
P_OFF(ModelRoData_ShadowRecord, image); P_OFF(ModelRoData_ShadowRecord, Header);
P_OFF(ModelRoData_ShadowRecord, Scale);
P_SZ(ModelRoData_InterlinkageRecord);
P_OFF(ModelRoData_InterlinkageRecord, pos); P_OFF(ModelRoData_InterlinkageRecord, pos2);
P_OFF(ModelRoData_InterlinkageRecord, Scale);
P_SZ(ModelRoData_SwitchRecord);
P_OFF(ModelRoData_SwitchRecord, Controls); P_OFF(ModelRoData_SwitchRecord, RwDataIndex);
P_SZ(ModelRoData_GroupSimpleRecord);
P_OFF(ModelRoData_GroupSimpleRecord, Origin); P_OFF(ModelRoData_GroupSimpleRecord, Group1);
P_OFF(ModelRoData_GroupSimpleRecord, Group2); P_OFF(ModelRoData_GroupSimpleRecord, BoundingVolumeRadius);
P_SZ(ModelRoData_DisplayListPrimaryRecord);
P_OFF(ModelRoData_DisplayListPrimaryRecord, numVertices);
P_OFF(ModelRoData_DisplayListPrimaryRecord, Vertices);
P_OFF(ModelRoData_DisplayListPrimaryRecord, Primary);
P_OFF(ModelRoData_DisplayListPrimaryRecord, BaseAddr);
P_SZ(ModelRoData_HeadPlaceholderRecord);
P_OFF(ModelRoData_HeadPlaceholderRecord, RwDataIndex);
P_SZ(ModelRoData_DisplayList_CollisionRecord);
P_OFF(ModelRoData_DisplayList_CollisionRecord, Primary);
P_OFF(ModelRoData_DisplayList_CollisionRecord, Secondary);
P_OFF(ModelRoData_DisplayList_CollisionRecord, Vertices);
P_OFF(ModelRoData_DisplayList_CollisionRecord, numVertices);
P_OFF(ModelRoData_DisplayList_CollisionRecord, numCollisionVertices);
P_OFF(ModelRoData_DisplayList_CollisionRecord, CollisionVertices);
P_OFF(ModelRoData_DisplayList_CollisionRecord, PointUsage);
P_OFF(ModelRoData_DisplayList_CollisionRecord, ModelType);
P_OFF(ModelRoData_DisplayList_CollisionRecord, RwDataIndex);
P_OFF(ModelRoData_DisplayList_CollisionRecord, BaseAddr);
return 0;
}