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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3xib6jjMV8o2iE4JfixAo
This commit is contained in:
James DansereauandClaude Sonnet 5 committed 2026-08-27 21:02:12 -04:00
1 parent 2acb7f6a42
commit 81d5614fbd
5 files changed
+315 -6

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
#ifndef PORT_PCCG_H
#define PORT_PCCG_H
#include <stdint.h>
#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-<region>/pccg.bin single concatenated image
* data/pccg-<region>/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 */
+10 -1
View File
@@ -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
+232
View File
@@ -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-<region>/pccg.bin single concatenated image
* data/pccg-<region>/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 <stdlib.h>
#include <string.h>
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: <ultra64.h> must finish before bondtypes.h (see
* pcmodels.c's comment -- same PR/ucode.h anchor requirement). */
#include <ultra64.h>
#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;
}
+12 -5
View File
@@ -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_<r>.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)
+5
View File
@@ -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)