From 81d5614fbd0e16e4acb6dde4fae10741240dc36c Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Thu, 27 Aug 2026 21:02:12 -0400 Subject: [PATCH] D69.3: bg/stan sidecar port-layer wiring (port/src/pccg.c) Serves the offline-converted sidecars from tools_pc/d69_emit.py, cloned from port/src/pcmodels.c (D50 Plan-B pattern): pccgReserveSize/ pccgLoadSidecars/pccgPatchTable/pccgTotalSize/pccgSidecarBase. Wiring: romdata.c extends the CART_BASE VirtualAlloc reservation to include pccgReserveSize() right after the pcmodels extension and loads the sidecar image there; romdataCartAddrValid's bounds check and libultra.c's D60 DMA-source diagnostic both extended to cover the pccg byte range. One-shot pccgPatchTable() call added next to pcmodelsPatchTable() in load_object_fill_header (objecthandler_2.c) -- reused purely as a "definitely after obInit()" hook, unrelated to model loading. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01C3xib6jjMV8o2iE4JfixAo --- port/include/pccg.h | 56 +++++++++ port/src/libultra.c | 11 +- port/src/pccg.c | 232 +++++++++++++++++++++++++++++++++++++ port/src/romdata.c | 17 ++- src/game/objecthandler_2.c | 5 + 5 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 port/include/pccg.h create mode 100644 port/src/pccg.c diff --git a/port/include/pccg.h b/port/include/pccg.h new file mode 100644 index 00000000..e90eeb1b --- /dev/null +++ b/port/include/pccg.h @@ -0,0 +1,56 @@ +#ifndef PORT_PCCG_H +#define PORT_PCCG_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* D69 (docs/PCPortResearch.md): PC-layout stage bg/*.seg + Tbg_*_stanZ + * sidecars -- same Plan-B pattern as D50/pcmodels.c. + * + * N64 bg/*.seg and Tbg_*_stanZ files are big-endian images with internal + * self-relative 0x0Fxxxxxx offsets that the PC build cannot read in place + * (D78-D81). tools_pc/d69_emit.py converts all of them offline to PC-layout + * sidecars: + * data/pccg-/pccg.bin single concatenated image + * data/pccg-/manifest.csv name,offset,size (decimal) + * + * romdataInit reserves romSize + pcmodelsReserveSize() + pccgReserveSize() + * at CART_BASE and pccgLoadSidecars() copies the sidecar image to + * [CART_BASE+romSize+pcmodelsTotalSize(), ...) (right after the pcmodels + * extension). pccgPatchTable() -- called once from the same + * load_object_fill_header hook as pcmodelsPatchTable(), after obInit() has + * run -- redirects every bg/stan entry's hw_address into the extension + * region and sets rom_size to the PC (possibly re-compressed) size, so the + * existing obLoadBGFileBytesAtOffset/_fileNameLoadToBank/decompressdata load + * paths serve PC bytes unmodified. + */ + +/* Bytes to reserve immediately after the pcmodels extension (0 = no sidecar + * image found; the game boots but stage loads will fault as before D69). + * romImg is the validated ROM image; the country byte at +0x3E selects the + * region directory. */ +uint32_t pccgReserveSize(const uint8_t *romImg); + +/* Copy the sidecar image to base+... and parse the manifest. `base` is the + * cart address immediately after the pcmodels extension (i.e. + * cartBase + romSize + pcmodelsTotalSize()). Returns 1 on success. */ +int pccgLoadSidecars(uintptr_t base); + +/* One-shot patch of file_resource_table + resource_lookup_data_array for + * every manifest row. Must run after obInit(). No-op if the sidecar image + * is absent. */ +void pccgPatchTable(void); + +uint32_t pccgTotalSize(void); + +/* Cart address where the sidecar image lives (0 if absent). */ +uintptr_t pccgSidecarBase(void); + +#ifdef __cplusplus +} +#endif + +#endif /* PORT_PCCG_H */ diff --git a/port/src/libultra.c b/port/src/libultra.c index e11e96ca..6aa7d7ec 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -815,12 +815,21 @@ void osAiSetConvert(u32 convert) { (void)convert; } * the file name. */ extern uintptr_t pcmodelsSidecarBase(void); extern uint32_t pcmodelsTotalSize(void); +/* D69: same diagnostic for the bg/stan sidecar image. */ +extern uintptr_t pccgSidecarBase(void); +extern uint32_t pccgTotalSize(void); static void d60logSidecarRead(u32 srcPA, void *dstVA, u32 size) { if (!getenv("GE_D60")) return; uintptr_t base = pcmodelsSidecarBase(); - if (base && srcPA >= base && srcPA < base + pcmodelsTotalSize()) + if (base && srcPA >= base && srcPA < base + pcmodelsTotalSize()) { sysLogPrintf(LOG_NOTE, "D60 sidecar read off=%llu size=0x%X dst=%p", (unsigned long long)(srcPA - base), size, dstVA); + return; + } + uintptr_t cgBase = pccgSidecarBase(); + if (cgBase && srcPA >= cgBase && srcPA < cgBase + pccgTotalSize()) + sysLogPrintf(LOG_NOTE, "D69 pccg sidecar read off=%llu size=0x%X dst=%p", + (unsigned long long)(srcPA - cgBase), size, dstVA); } /* TEMP D60/D61: a ROM-read DMA target must land in the game DRAM views diff --git a/port/src/pccg.c b/port/src/pccg.c new file mode 100644 index 00000000..824f1e52 --- /dev/null +++ b/port/src/pccg.c @@ -0,0 +1,232 @@ +/* + * PC-layout stage bg/*.seg + Tbg_*_stanZ sidecars (D69 / Plan B). + * + * Serves the offline-converted stage files produced by tools_pc/d69_emit.py: + * data/pccg-/pccg.bin single concatenated image + * data/pccg-/manifest.csv name,offset,size (decimal) + * + * Same shape as port/src/pcmodels.c -- see that file's header comment for + * the general mechanism. The pccg image is placed right after the pcmodels + * extension in the cart-base reservation (romdata.c), so once every bg/stan + * entry's hw_address points into this region and rom_size holds the PC + * size, the existing load path (obLoadBGFileBytesAtOffset / romCopy / + * _fileNameLoadToBank / decompressdata) serves PC-layout bytes unmodified. + * + * The table patch must run AFTER obInit() (same reasoning as pcmodels: + * obInit derives rom_size from adjacent-entry hw_address deltas). It is + * called from the same load_object_fill_header hook as + * pcmodelsPatchTable() -- a one-shot no-op after the first. + */ + +#include +#include + +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 "pccg.h" + +/* Order matters: must finish before bondtypes.h (see + * pcmodels.c's comment -- same PR/ucode.h anchor requirement). */ +#include +#include "game/ob.h" + +extern struct fileentry file_resource_table[]; +extern resource_lookup_data_entry resource_lookup_data_array[]; +extern s32 file_entry_max; + +#define PCCG_MAX_FILES 128 +#define PCCG_NAME_LEN 64 + +struct pccgRow { + char name[PCCG_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 pccgRow s_rows[PCCG_MAX_FILES]; +static int s_rowCount = 0; +static char s_binPath[1024] = ""; +static char s_manPath[1024] = ""; + +static const char *pccgRegionForCountry(u8 country) +{ + switch (country) { + case 'E': return "ntsc-final"; + case 'P': return "pal-final"; + case 'J': return "jpn-final"; + default: return NULL; + } +} + +static int pccgFindDataFile(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 pccgReserveSize(const uint8_t *romImg) +{ + const char *region = pccgRegionForCountry(romImg[0x3E]); + char rel[256]; + + if (!region) + return 0; + + snprintf(rel, sizeof(rel), "pccg-%s/pccg.bin", region); + if (!pccgFindDataFile(rel, s_binPath, sizeof(s_binPath))) { + sysLogPrintf(LOG_WARNING, + "pccg: %s not found -- stage loads will fault; run " + "\"python tools_pc/d69_emit.py\" (see docs/HANDOFF.md)", + rel); + return 0; + } + + snprintf(rel, sizeof(rel), "pccg-%s/manifest.csv", region); + if (!pccgFindDataFile(rel, s_manPath, sizeof(s_manPath))) { + sysLogPrintf(LOG_ERROR, "pccg: manifest.csv missing next to %s", + s_binPath); + return 0; + } + + FSFile *f = fsOpen(s_binPath, "rb"); + if (!f) { + sysLogPrintf(LOG_ERROR, "pccg: cannot open %s", s_binPath); + return 0; + } + uint32_t size = (uint32_t)fsSize(f); + fsClose(f); + s_total = size; + return size; +} + +int pccgLoadSidecars(uintptr_t base) +{ + FSFile *f; + int ok; + + if (!s_total) + return 0; + + f = fsOpen(s_binPath, "rb"); + if (!f) + return 0; + s_base = base; + ok = fsRead(f, (void *)s_base, (int32_t)s_total) == (int32_t)s_total; + fsClose(f); + if (!ok) { + sysLogPrintf(LOG_ERROR, "pccg: short read of %s", s_binPath); + return 0; + } + + f = fsOpen(s_manPath, "rb"); + if (!f) { + sysLogPrintf(LOG_ERROR, "pccg: 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, "pccg: 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 >= PCCG_MAX_FILES) { + sysLogPrintf(LOG_ERROR, "pccg: manifest exceeds %d rows", + PCCG_MAX_FILES); + break; + } + struct pccgRow *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, "pccg: no rows parsed from %s", s_manPath); + return 0; + } + + s_loaded = 1; + sysLogPrintf(LOG_INFO, "pccg: %d sidecars (%u bytes) at 0x%llX", + s_rowCount, s_total, (unsigned long long)s_base); + return 1; +} + +void pccgPatchTable(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 pccgRow *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, + "pccg: patched %d/%d manifest rows (table mismatch?)", + hit, s_rowCount); + else + sysLogPrintf(LOG_INFO, "pccg: table patched (%d bg/stan entries)", + hit); +} + +uint32_t pccgTotalSize(void) +{ + return s_loaded ? s_total : 0; +} + +uintptr_t pccgSidecarBase(void) +{ + return s_loaded ? (uintptr_t)s_base : 0; +} diff --git a/port/src/romdata.c b/port/src/romdata.c index 686d23b4..15aa4122 100644 --- a/port/src/romdata.c +++ b/port/src/romdata.c @@ -38,6 +38,7 @@ extern int snprintf(char *str, unsigned long long maxsize, const char *format, . #include "fs.h" #include "romdata.h" #include "pcmodels.h" +#include "pccg.h" /* D50: struct font/fontchar layouts for the font-segment re-layout. Order * matters (see pcmodels.c): ultra64.h must finish before bondtypes.h. */ @@ -163,20 +164,25 @@ int romdataInit(void) * ROM ([CART_BASE+romSize, ...)); it is loaded below and patched into * file_resource_table after obInit. */ u32 sideTotal = pcmodelsReserveSize(img); + /* D69: the bg/stan sidecar image is appended right after the model + * sidecar extension. */ + u32 cgTotal = pccgReserveSize(img); /* Map at the cart address so absolute asset symbols are live. */ #if defined(PLATFORM_WINDOWS) - void *at = VirtualAlloc((LPVOID)CART_BASE, romSize + sideTotal, + void *at = VirtualAlloc((LPVOID)CART_BASE, romSize + sideTotal + cgTotal, 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)%s", + "0x%08X (cart base)%s%s", tok, romSize, CART_BASE, - sideTotal ? ", + model sidecars" : ""); + sideTotal ? ", + model sidecars" : "", + cgTotal ? ", + bg/stan sidecars" : ""); pcmodelsLoadSidecars(CART_BASE, romSize); + pccgLoadSidecars(CART_BASE + romSize + pcmodelsTotalSize()); /* D55 (docs/PCPortResearch.md): the RLE folder-menu background at * `unknown2` (romassets_.s) is stored in the ROM with a @@ -249,8 +255,9 @@ int romdataCartAddrValid(u32 addr, u32 size) if (addr < CART_BASE) return 0; u32 off = addr - CART_BASE; - /* D50: the model sidecar image extends the valid cart region. */ - return off + size <= romSize + pcmodelsTotalSize(); + /* D50/D69: the model + bg/stan sidecar images extend the valid cart + * region (concatenated: ROM, then pcmodels, then pccg). */ + return off + size <= romSize + pcmodelsTotalSize() + pccgTotalSize(); } const void *romdataMapVa(u32 va) diff --git a/src/game/objecthandler_2.c b/src/game/objecthandler_2.c index 1d7a1d81..cea7deb0 100644 --- a/src/game/objecthandler_2.c +++ b/src/game/objecthandler_2.c @@ -13,6 +13,7 @@ #ifdef PORT #include "pcmodels.h" /* D50: PC-layout model sidecars (Plan B, D48/D49) */ +#include "pccg.h" /* D69: PC-layout bg/stan sidecars */ extern resource_lookup_data_entry resource_lookup_data_array[]; /* ob.c */ #endif @@ -101,6 +102,10 @@ void load_object_fill_header(struct ModelFileHeader *objheader, u8 *name, u8* ds * by first model load obInit() has definitely run (boss.c:179). Must run * before _fileNameLoadTo* below reads hw_address/rom_size. */ pcmodelsPatchTable(); + /* D69: same one-shot pattern for the bg/stan sidecars -- piggybacks on + * this call site purely for a "definitely after obInit()" hook; it has + * nothing to do with model loading. */ + pccgPatchTable(); 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)