psx map improvement

This commit is contained in:
MaddoScientisto 2026-04-16 23:52:41 +02:00
commit 9fe261610f
14 changed files with 859 additions and 483 deletions

View file

@ -9,8 +9,9 @@ import {
extractPaletteSets,
} from './bundles.js';
import { decodeBundleFrame, encodePng, scanSpriteBundles } from './bundles.js';
import { buildOverrideBundleBindings, parseOverrideBank } from './override-bank.js';
import { renderMap } from './render.js';
import { parseLsetWdl, parseRegion00Records, parseRegion01Records, summarizeRegion02 } from './wdl.js';
import { parseCtorPlacementsBlock, parseDispatchRootsBlock, parseLoaderLayout, parseLsetWdl, parseRegion00Records, parseRegion01Records, summarizeRegion02 } from './wdl.js';
const DEFAULT_BACKGROUND = { red: 18, green: 18, blue: 18, alpha: 255 };
@ -85,17 +86,38 @@ function chooseRecordSet(wdl, mapSource) {
? parseRegion01Records(region01Source)
: { source: 'region01', recordStartOffset: 0, records: [] };
// Loader-faithful paths: pull ctorPlacements and dispatchRoots directly from
// the packSubranges in the 14-u32 loader header. When present these override
// the heuristic region scans because they match the executable's dispatch
// iterators 1:1.
const packSubranges = wdl.loaderLayout?.packSubranges ?? [];
const ctorBlock = packSubranges.find((block) => block.name === 'ctorPlacements');
const dispatchBlock = packSubranges.find((block) => block.name === 'dispatchRoots');
const ctorPlacementsLoader = ctorBlock
? parseCtorPlacementsBlock(ctorBlock)
: { source: 'ctorPlacements', records: [] };
const dispatchRootsLoader = dispatchBlock
? parseDispatchRootsBlock(dispatchBlock)
: { source: 'dispatchRoots', records: [] };
const constructorRecordSet = ctorPlacementsLoader.records.length > 0
? ctorPlacementsLoader
: region01Records;
const rootRecordSet = dispatchRootsLoader.records.length > 0
? dispatchRootsLoader
: region00Records;
if (mapSource === 'region00' || mapSource === 'roots') {
return region00Records;
return rootRecordSet;
}
if (mapSource === 'region01' || mapSource === 'constructors') {
return region01Records;
return constructorRecordSet;
}
if (mapSource === 'combined' || mapSource === 'layered' || mapSource === 'auto') {
const records = [
...region01Records.records.map((record) => ({ ...record, authoredLayer: 'constructors' })),
...region00Records.records.map((record) => ({ ...record, authoredLayer: 'roots' })),
...constructorRecordSet.records.map((record) => ({ ...record, authoredLayer: record.authoredLayer ?? 'constructors' })),
...rootRecordSet.records.map((record) => ({ ...record, authoredLayer: record.authoredLayer ?? 'roots' })),
];
return {
@ -103,13 +125,25 @@ function chooseRecordSet(wdl, mapSource) {
recordStartOffset: 0,
records,
layers: {
constructors: region01Records.records.length,
roots: region00Records.records.length,
constructors: constructorRecordSet.records.length,
roots: rootRecordSet.records.length,
},
loaderSources: {
ctorPlacements: {
used: ctorPlacementsLoader.records.length > 0,
count: ctorPlacementsLoader.records.length,
reportedCount: ctorPlacementsLoader.reportedCount ?? null,
},
dispatchRoots: {
used: dispatchRootsLoader.records.length > 0,
count: dispatchRootsLoader.records.length,
reportedCount: dispatchRootsLoader.reportedCount ?? null,
},
},
};
}
return region01Records.records.length >= 8 ? region01Records : region00Records;
return constructorRecordSet.records.length >= 8 ? constructorRecordSet : rootRecordSet;
}
function chooseBundleForType(bundles, typeWord) {
@ -127,6 +161,146 @@ async function loadJsonIfExists(filePath) {
}
}
async function loadOverrideBankBindingsForBuffer(buffer, layout, variant, paletteSets, options = {}) {
if (!layout) {
return {
bundles: [],
byType: new Map(),
overrideBank: null,
artInstallBank: null,
failedEntries: [],
};
}
const overrideBlock = layout.blocksByName.get('override');
const artInstallBlock = layout.blocksByName.get('artInstall');
const result = {
bundles: [],
byType: new Map(),
overrideBank: null,
artInstallBank: null,
failedEntries: [],
};
// Parse art-install first: it provides the initial per-type drawable bindings
// for both kind-4 and kind-5 resources. The later override pass then
// replaces some slots with raw-header pointers.
if (artInstallBlock && artInstallBlock.buffer) {
const bank = parseOverrideBank(artInstallBlock, {
variant,
payloadBase: 0x2718,
});
result.artInstallBank = bank;
if (bank.valid) {
const { bundles: raw, failedEntries } = buildOverrideBundleBindings(bank, { buffer });
const tagged = raw.map((bundle) => ({
...bundle,
bundleSource: variant === 'spec_a' ? 'art-install-spec-a' : 'art-install-lset',
}));
const resolved = resolveBundlePalettes(tagged, paletteSets, {
mode1RuntimePalette: options.mode1RuntimePalette,
});
for (const bundle of resolved) {
if (!bundle.sourceBuffer && Number.isInteger(bundle.absoluteOffset)) {
bundle.sourceBuffer = buffer.subarray(bundle.absoluteOffset);
}
result.byType.set(bundle.typeId, bundle);
result.bundles.push(bundle);
}
result.failedEntries.push(...failedEntries);
}
}
if (overrideBlock && overrideBlock.buffer) {
const bank = parseOverrideBank(overrideBlock, { variant, payloadBase: 8 });
result.overrideBank = bank;
if (bank.valid) {
const { bundles: raw, failedEntries } = buildOverrideBundleBindings(bank, { buffer });
const tagged = raw.map((bundle) => ({
...bundle,
bundleSource: variant === 'spec_a' ? 'override-bank-spec-a' : 'override-bank-lset',
}));
const resolved = resolveBundlePalettes(tagged, paletteSets, {
mode1RuntimePalette: options.mode1RuntimePalette,
});
for (const bundle of resolved) {
if (!bundle.sourceBuffer && Number.isInteger(bundle.absoluteOffset)) {
bundle.sourceBuffer = buffer.subarray(bundle.absoluteOffset);
}
// Late override wins over earlier art-install for the same type,
// matching the loader's bank-slot overwrite behaviour.
result.byType.set(bundle.typeId, bundle);
result.bundles.push(bundle);
}
result.failedEntries.push(...failedEntries);
}
}
return result;
}
async function loadSpecAOverrideBank(wdlPath, discRoot, paletteSets, options = {}) {
const candidatePaths = [];
if (discRoot) {
candidatePaths.push(path.join(discRoot, 'SPEC_A.WDL'));
}
const wdlDir = path.dirname(wdlPath);
candidatePaths.push(path.resolve(wdlDir, '..', 'SPEC_A.WDL'));
candidatePaths.push(path.resolve(wdlDir, 'SPEC_A.WDL'));
for (const candidate of candidatePaths) {
try {
const buffer = await fs.readFile(candidate);
const layout = parseLoaderLayout(buffer, { variant: 'spec_a' });
const result = await loadOverrideBankBindingsForBuffer(
buffer,
layout,
'spec_a',
paletteSets,
options,
);
result.sourcePath = candidate;
return result;
} catch {
// try next candidate
}
}
return { bundles: [], byType: new Map(), overrideBank: null, failedEntries: [], sourcePath: null };
}
async function loadOverrideBankBindings(lsetWdl, wdlPath, discRoot, paletteSets, options = {}) {
// SPEC_A.WDL provides the base (bundle A) override table; the map-local
// LSET*.WDL provides the override B table. The loader runs SPEC_A first
// then LSET, and for matching typeIds LSET wins because it overwrites the
// bank slot. We merge in the same order here.
const specAResult = await loadSpecAOverrideBank(wdlPath, discRoot, paletteSets, options);
const lsetLayout = lsetWdl.loaderLayout;
const lsetResult = await loadOverrideBankBindingsForBuffer(
lsetWdl.buffer,
lsetLayout,
'lset',
paletteSets,
options,
);
const bundles = [...specAResult.bundles, ...lsetResult.bundles];
const byType = new Map();
for (const bundle of specAResult.bundles) {
byType.set(bundle.typeId, bundle);
}
// LSET wins over SPEC_A for identical typeIds (matches loader behaviour).
for (const bundle of lsetResult.bundles) {
byType.set(bundle.typeId, bundle);
}
return {
bundles,
byType,
specA: specAResult,
lset: lsetResult,
};
}
function buildRuntimeMap0MaskedBindings(correlation, bundles, tolerance = 0x200) {
const byType = new Map();
const diagnostics = [];
@ -226,6 +400,23 @@ function buildRuntimeMap0MaskedBindings(correlation, bundles, tolerance = 0x200)
}
function chooseBundleBinding(record, bundles, options = {}) {
// Primary path: per-type override-bank binding. The late header-only override
// block of each WDL pass (SPEC_A.WDL followed by the map-local LSET*.WDL)
// writes raw 0x58-byte active-header pointers into
// `psx_type_art_active_header_bank[type]`. Constructors and the render
// submitters read that bank at runtime, so an override-bank bundle IS the
// drawable resource bound to that type for this level.
const overrideBundle = options.overrideBundlesByType?.get(record.typeWord) ?? null;
if (overrideBundle) {
return {
bundle: overrideBundle,
mappingSource: overrideBundle.overrideVariant === 'spec_a'
? 'override-bank-spec-a'
: 'override-bank-lset',
runtimeBinding: null,
};
}
if (options.bindingMode === 'runtime-map0-masked-proxy') {
const runtimeBinding = options.runtimeMap0Bindings?.get(record.typeWord) ?? null;
if (runtimeBinding?.bundle) {
@ -233,6 +424,13 @@ function chooseBundleBinding(record, bundles, options = {}) {
}
}
if (options.bindingMode === 'override-bank') {
// Strict mode: refuse to fall back to the diagnostic slot rule. Types with
// no override binding get dropped from the scene so the render reflects
// only executable-backed art.
return null;
}
const rawTypeBundle = chooseBundleForType(bundles, record.typeWord);
if (!rawTypeBundle) {
return null;
@ -1033,6 +1231,20 @@ export async function exportMap(options) {
{ mode1RuntimePalette }
);
// Override-bank bindings: per-type drawable-header pointers installed by the
// late override pass in wdl_resource_bundle_load_by_index. This is the same
// runtime state constructors and render submitters read when resolving art,
// so it gives executable-backed typeWord -> bundle truth for the bundle A
// (SPEC_A.WDL) and bundle B (map-local LSET WDL) passes combined.
const overrideBankResult = await loadOverrideBankBindings(
wdl,
options.wdlPath,
options.discRoot ?? null,
paletteSets,
{ mode1RuntimePalette },
);
bundles = [...bundles, ...overrideBankResult.bundles];
const runtimeMap0Correlation = options.bindingMode === 'runtime-map0-masked-proxy'
? await loadJsonIfExists(path.join(cacheBaseRoot, 'runtime-map0-correlation.json'))
: null;
@ -1044,6 +1256,7 @@ export async function exportMap(options) {
mode1RuntimePalette,
mode1PaletteBank,
runtimeMap0Bindings: runtimeMap0BindingResult.byType,
overrideBundlesByType: overrideBankResult.byType,
});
const bindingDiversity = summarizeBindingDiversity(sceneItems);
const authoredLayerSummary = summarizeAuthoredLayers(recordSet.records);