PSX Research

This commit is contained in:
Marco 2026-04-13 16:50:28 +02:00
commit 8d34c85c22
13 changed files with 1720 additions and 8 deletions

View file

@ -119,6 +119,132 @@ function chooseBundleForType(bundles, typeWord) {
return null;
}
async function loadJsonIfExists(filePath) {
try {
return JSON.parse(await fs.readFile(filePath, 'utf8'));
} catch {
return null;
}
}
function buildRuntimeMap0MaskedBindings(correlation, bundles, tolerance = 0x200) {
const byType = new Map();
const diagnostics = [];
const provisionalBindings = [];
if (!correlation || correlation.currentMapId !== 0 || !Array.isArray(correlation.combined)) {
return { byType, diagnostics };
}
for (const row of correlation.combined) {
if (!row || !Number.isInteger(row.typeId) || !Number.isInteger(row.rawRecordCount) || row.rawRecordCount <= 0) {
continue;
}
if ((row.headerKind !== 4 && row.headerKind !== 5) || (row.headerWord3 !== 1 && row.headerWord3 !== 2)) {
continue;
}
if (!Number.isInteger(row.headerWord11) || row.headerWord11 === 0) {
continue;
}
const maskedAbsoluteOffset = row.headerWord11 & 0x0fffff;
let bestMatch = null;
for (const bundle of bundles) {
if (bundle.kind !== row.headerKind || bundle.mode !== row.headerWord3) {
continue;
}
const absoluteOffsetDelta = Math.abs(bundle.absoluteOffset - maskedAbsoluteOffset);
if (!bestMatch || absoluteOffsetDelta < bestMatch.absoluteOffsetDelta) {
bestMatch = {
bundle,
absoluteOffsetDelta,
};
}
}
if (!bestMatch || bestMatch.absoluteOffsetDelta > tolerance) {
continue;
}
const binding = {
typeId: row.typeId,
bundle: bestMatch.bundle,
mappingSource: 'runtime-map0-masked-header-offset-proxy',
runtimeBinding: {
maskedAbsoluteOffset,
absoluteOffsetDelta: bestMatch.absoluteOffsetDelta,
headerKind: row.headerKind,
headerMode: row.headerWord3,
headerWord4: row.headerWord4,
headerWord8: row.headerWord8,
headerWord10: row.headerWord10,
visibleCount: row.visibleCount ?? 0,
rawRecordCount: row.rawRecordCount,
},
};
provisionalBindings.push(binding);
}
const bundleBuckets = new Map();
for (const binding of provisionalBindings) {
const bucket = bundleBuckets.get(binding.bundle.absoluteOffset) ?? [];
bucket.push(binding);
bundleBuckets.set(binding.bundle.absoluteOffset, bucket);
}
for (const binding of provisionalBindings) {
const bucket = bundleBuckets.get(binding.bundle.absoluteOffset) ?? [];
const isCrowdedLargeSingleFrameBundle =
bucket.length >= 4 &&
binding.bundle.frameCount === 1 &&
binding.bundle.width >= 96 &&
binding.bundle.height >= 48;
if (isCrowdedLargeSingleFrameBundle) {
continue;
}
byType.set(binding.typeId, binding);
diagnostics.push({
typeId: binding.typeId,
bundleSlot: binding.bundle.slot,
bundleAbsoluteOffset: binding.bundle.absoluteOffset,
maskedAbsoluteOffset: binding.runtimeBinding.maskedAbsoluteOffset,
absoluteOffsetDelta: binding.runtimeBinding.absoluteOffsetDelta,
headerKind: binding.runtimeBinding.headerKind,
headerMode: binding.runtimeBinding.headerMode,
visibleCount: binding.runtimeBinding.visibleCount,
rawRecordCount: binding.runtimeBinding.rawRecordCount,
crowdedBundleTypeCount: bucket.length,
});
}
diagnostics.sort((left, right) => left.typeId - right.typeId);
return { byType, diagnostics };
}
function chooseBundleBinding(record, bundles, options = {}) {
if (options.bindingMode === 'runtime-map0-masked-proxy') {
const runtimeBinding = options.runtimeMap0Bindings?.get(record.typeWord) ?? null;
if (runtimeBinding?.bundle) {
return runtimeBinding;
}
}
const rawTypeBundle = chooseBundleForType(bundles, record.typeWord);
if (!rawTypeBundle) {
return null;
}
return {
bundle: rawTypeBundle,
mappingSource: 'raw-typeword-bundle-slot-diagnostic',
runtimeBinding: null,
};
}
function describeMapScope(recordSet) {
if (recordSet.source === 'combined') {
return 'layered object-projection probe from both loader-sized section-0 constructor-placement and root-dispatch records in post_audio_section_00';
@ -738,9 +864,9 @@ async function buildSceneItems(region04, records, bundles, options = {}) {
continue;
}
const rawTypeBundle = chooseBundleForType(bundles, record.typeWord);
const bundle = rawTypeBundle;
if (!bundle) {
const binding = chooseBundleBinding(record, bundles, options);
const bundle = binding?.bundle ?? null;
if (!bundle || !binding) {
continue;
}
if (nonMapFacingBundleOffsets.has(bundle.absoluteOffset)) {
@ -788,9 +914,13 @@ async function buildSceneItems(region04, records, bundles, options = {}) {
defaultPaletteIndex: bundle.defaultPaletteIndex ?? null,
resolvedPaletteIndex: bundle.resolvedPaletteIndex ?? null,
paletteFormula: bundle.paletteFormula ?? null,
mappingSource: 'raw-typeword-bundle-slot-diagnostic',
mappingSource: binding.mappingSource,
templateTypeId: null,
donorTypeId: null,
runtimeBindingMaskedAbsoluteOffset: binding.runtimeBinding?.maskedAbsoluteOffset ?? null,
runtimeBindingOffsetDelta: binding.runtimeBinding?.absoluteOffsetDelta ?? null,
runtimeBindingVisibleCount: binding.runtimeBinding?.visibleCount ?? null,
runtimeBindingRawRecordCount: binding.runtimeBinding?.rawRecordCount ?? null,
rawWords: record.rawWords ?? record.words,
flipped: (record.laneWord & 0x0002) !== 0,
width: sprite.width,
@ -903,10 +1033,17 @@ export async function exportMap(options) {
{ mode1RuntimePalette }
);
const runtimeMap0Correlation = options.bindingMode === 'runtime-map0-masked-proxy'
? await loadJsonIfExists(path.join(cacheBaseRoot, 'runtime-map0-correlation.json'))
: null;
const runtimeMap0BindingResult = buildRuntimeMap0MaskedBindings(runtimeMap0Correlation, bundles);
const { items: sceneItems, skippedRecords } = await buildSceneItems(region04, recordSet.records, bundles, {
paletteSets,
bindingMode: options.bindingMode,
mode1RuntimePalette,
mode1PaletteBank,
runtimeMap0Bindings: runtimeMap0BindingResult.byType,
});
const bindingDiversity = summarizeBindingDiversity(sceneItems);
const authoredLayerSummary = summarizeAuthoredLayers(recordSet.records);
@ -949,7 +1086,11 @@ export async function exportMap(options) {
bundleCount: bundles.length,
bundleSource: bundles[0]?.bundleSource ?? 'none',
gpuRamDumpPath: options.gpuRamDumpPath ?? null,
artBindingSource: 'raw-typeword-bundle-slot-diagnostic',
artBindingSource: options.bindingMode === 'runtime-map0-masked-proxy'
? 'runtime-map0-masked-header-offset-proxy-with-raw-fallback'
: 'raw-typeword-bundle-slot-diagnostic',
runtimeMap0BindingTypeCount: runtimeMap0BindingResult.diagnostics.length,
runtimeMap0BindingTypes: runtimeMap0BindingResult.diagnostics,
activeHeaderOverrideCandidateCount: activeHeaderOverrideCandidates.length,
bestActiveHeaderOverrideCandidate: activeHeaderOverrideCandidates[0]
? {
@ -975,7 +1116,9 @@ export async function exportMap(options) {
'The root-dispatch lane is now rendered as a second authored layer, but runtime-driven control mutations and dynamic effect spawns are still out of scope.',
'Known non-map-facing portrait/talk root types `0x0042` and `0x0049`, plus the known portrait bundle `0x000D84F4`, are excluded from probe rendering.',
'Viewer-derived sidecars, donor mappings, and cached scene references are intentionally disabled in this standalone exporter.',
'Current bundle selection is still diagnostic-only until the late DAT_800758d8 art bank is parsed directly.',
options.bindingMode === 'runtime-map0-masked-proxy'
? 'Current bundle selection uses an experimental map-0 runtime masked-offset proxy where live headerWord11 low bits land near a scanned raw bundle; all non-matching types still fall back to the raw slot diagnostic.'
: 'Current bundle selection is still diagnostic-only until the late DAT_800758d8 art bank is parsed directly.',
'No floor or tile layer is decoded directly yet; post_audio_region_02 and the decompressed level-state lane remain unresolved.',
]),
'Palette routing remains partly heuristic when authored token and default bank evidence are both absent.',
@ -1023,6 +1166,10 @@ export async function exportMap(options) {
mappingSource: item.mappingSource,
templateTypeId: item.templateTypeId,
donorTypeId: item.donorTypeId,
runtimeBindingMaskedAbsoluteOffset: item.runtimeBindingMaskedAbsoluteOffset,
runtimeBindingOffsetDelta: item.runtimeBindingOffsetDelta,
runtimeBindingVisibleCount: item.runtimeBindingVisibleCount,
runtimeBindingRawRecordCount: item.runtimeBindingRawRecordCount,
token06HighByte: item.paletteDiagnostics?.token06HighByte ?? null,
token0cHighByte: item.paletteDiagnostics?.token0cHighByte ?? null,
expectedPaletteToken: item.paletteDiagnostics?.expectedPaletteToken ?? null,