Skip to content

oot support#219

Draft
briaguya0 wants to merge 161 commits intoHarbourMasters:mainfrom
briaguya0:oot-assets-torchonly
Draft

oot support#219
briaguya0 wants to merge 161 commits intoHarbourMasters:mainfrom
briaguya0:oot-assets-torchonly

Conversation

@briaguya0
Copy link
Copy Markdown
Contributor

this doesn't include all of the test scaffolding i've been using. that still exists in https://github.com/briaguya0/Torch/tree/oot-simplify-addasset

i'm very happy with almost every factory in here. the one big chunk left to figure out is DisplayListFactory. i originally had a bunch of oot specific changes littered throughout that file, now i just have OoTDListHelpers that basically just reimplements it with oot specific changes.

the other thing i'd like to do is verify this against other roms, but once the dlist stuff is sorted i see no reason to wait for every rom to work before moving this out of draft

briaguya0 and others added 30 commits March 29, 2026 04:10
Recovered from filesystem after data loss. This squashes ~58 commits
originally made between 2026-03-23 and 2026-03-28. The full original
reflog is preserved in docs/recovered-git-history.md.

New OoT-specific factories:
- OoTSceneFactory (OOT:SCENE, OOT:ROOM) — scene command parsing and binary export
- OoTSkeletonFactory — skeleton, limb, and skin vertex support
- OoTAnimationFactory — normal, curve, legacy, and player animations
- OoTCollisionFactory — collision mesh with camera data and waterboxes
- OoTArrayFactory — Shipwright-compatible VTX and Vec3s arrays

Modified upstream:
- DisplayListFactory — OoT cross-segment DList handling, VTX consolidation,
  virtual segment 0x80, G_BRANCH_Z discovery, ZAPD compatibility fixes
- Companion — OoT factory registration, BUILD_OOT cmake option
- ResourceType — OoT type codes (OSKL, OSLB, OANM, OROM, OCOL, OPTH, OTXT)

Tooling (soh/):
- zapd_to_torch.py — converts ZAPDTR/OTRExporter XML to Torch YAML
- test_assets.sh, check.sh, verify.sh, manifest.sh, lib.sh — test harness
- list_assets.py — asset manifest query tool

Status at time of loss: 20,432 assets passing, 0 failures.
14,355 scene assets in progress (scene/room factory implemented,
iterating on binary format correctness). OoTTextFactory was not
recovered and needs recreation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- identify_roms.sh: identifies OoT ROMs by SHA1, renames to
  standardized format, handles duplicates
- extract_dma.py: extracts DMA tables from all 17 ROM versions
  using Shipwright filelists, outputs JSON keyed by filename
- Pre-computed DMA tables for all 17 versions (14 unique)
- Manifests directory with gitignore for generated hash files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
config.yml moved from soh/ to soh/assets/yml/ where Torch expects it.
Generated per-version YAML dirs are gitignored via local .gitignore
rather than the top-level one.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add lib/libyaz0/ with decode support following libmio0/libyay0 pattern
- Wire YAZ0 into Decompressor::Decode and AutoDecode
- Add missing PendingVtx struct in DeferredVtx namespace
- Add missing IS_VIRTUAL_SEGMENT macro in BaseFactory.h
- Add libyaz0 to CMake C_FILES glob

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- TranslateAddr now recognizes high segments (>= 0x80) when they
  exist in the segment map, not just standard segments (0x01-0x1F)
- ASSET_PTR extracts segment offset for virtual segments too,
  preventing raw 0x80XXXXXX addresses from being used as buffer offsets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OTRExporter writes 0-byte files for LimbTable entries. BlobFactory
crashed when trying to Write() a null buffer. Guard the write with
an empty check.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Empty blobs (e.g. LimbTable) now write 0 bytes to match OTRExporter
  reference output instead of writing a header with size 0
- test_assets.sh auto-logs to soh/logs/ with timestamp
- New compare_asset.sh tool for hex-diffing individual assets between
  reference and generated O2R

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
*.o2r are generated archive files. torch.hash.yml is a Torch
build cache tracking which YAMLs have been processed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hash all extracted files in a single sha256sum call instead of
  one process per file
- Redirect torch output to a log file instead of piping through grep
- Collapse duplicate jq reduce into one pass with inline fail count

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrites the asset test script in Python to avoid per-file process
spawning. YAML collection, O2R extraction, and hashing are all done
in-process. Hashes assets directly from the zip without extracting
to disk.

107s → 1.6s for 17,516 object assets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add BUILD_OOT option (default ON) following pattern of other games,
  defines OOT_SUPPORT so OoT factories are registered
- Stub OoTTextFactory so it compiles (real impl is task HarbourMasters#5)
- Expose DeferredVtx::BeginDefer in DisplayListFactory.h so
  OoTSceneFactory can call it

Enables 16,952 additional assets: 12,377 → 29,329 passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Enable GFX auto-discovery for auto-discovered limbs (previously
  disabled, causing 573 limbs to have empty DList paths)
- Fix LOD limb DList suffix: use "FarDL" instead of "DL2" to match
  OTRExporter/ZAPDTR naming convention
- Fix Curve limb DList suffixes: "CurveDL"/"Curve2DL" to match ZAPDTR
- Resolve LOD far DList before near, so shared-address limbs use
  the Far name for both fields (matches OTRExporter behavior)
- Rewrite compare_asset.sh as compare_asset.py (takes two O2Rs,
  no torch run needed)
- test_assets.py now saves generated.o2r to soh/o2r/ by default

Objects: 17,322 passed, 1 failed (MTX), 193 not generated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OTRExporter writes a 0-byte file for each skeleton's limb array
(e.g. gKeeseSkeletonLimbs). Add this to the skeleton factory's
parse to match.

Objects: 17,515 passed, 1 failed (MTX), 0 not generated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OTRExporter/ZAPDTR reads the N64 Mtx as 16 sequential int32 BE
values and writes them back as-is. Our exporter was writing
individual uint16 int-part values, which produced byte-swapped
output within each 32-bit word.

Now reads and stores the raw int32 values in the parser and writes
them in the binary exporter, matching the reference format.

Objects: 17,516 passed, 0 failed. Code: 11 passed, 0 failed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When multiple segments map to the same physical ROM address (common
for overlays which alias segments 8-13 to their code data), the
virtual address patcher was returning a segment 0x0D address instead
of segment 0x80. This caused texture lookups to fail because textures
are registered under segment 0x80 offsets in the YAML.

Now explicitly prefers segment 0x80 when it maps to the same physical
address, matching how YAML offsets are declared.

Overlays: 325 passed, 0 failed (was 101 failures).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scene/room DLists are auto-discovered by the scene factory with
room-prefixed names matching OTRExporter output. Pre-declared DList
entries from ZAPDTR XMLs used different naming (gXxxDL_ vs
xxx_room_0DL_) causing mismatches.

Scenes: 10,729 passed, 0 failed (was 27 failures).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Room mesh DLists are auto-discovered by the scene factory with
correct room-prefixed names. Pre-declared DLists from ZAPDTR XMLs
(both room-named and scene-named) conflict with auto-discovery.

18 scene-level DLists declared in room files (e.g. gKinsutaDL_0030B0)
are now missing — these need to be handled by the scene factory or
a separate mechanism. Tracked as part of scene work.

31,156 passed, 1 failed (version), 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scene/room alternate headers (SetAlternateHeaders command) are now
recursively processed as sub-assets. Processing is deferred until
after the primary header's commands (especially SetMesh) complete,
so primary DLists are registered first and alternate headers reuse
their names for shared ROM addresses.

DeferredVtx state is saved/restored around each alternate header to
prevent VTX consolidation corruption.

Exposes SaveAndClearPending/RestorePending and PendingVtx struct in
DisplayListFactory.h for use by scene factory.

31,436 passed (+280), 128 scene failures (Sets/Cutscenes), 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Alternate headers pass parent's baseName for sub-asset naming
  (DLists, backgrounds, cutscenes, pathways) so names match
  OTRExporter which doesn't prefix with Set_
- Fix cutscene suffix: "CutsceneData" instead of "Cs" to match
  OTRExporter's GetSegmentedPtrName convention

31,501 passed (+345 from session start), 108 failed, 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Cutscenes use entryName (with Set_ prefix) matching OTRExporter
- Pathways use baseName (parent name) matching OTRExporter
- Fix cutscene suffix: CutsceneData instead of Cs

31,583 passed, 109 failed (84 Set command data, 24 cutscenes, 1 version).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use getNeighborSize to limit pathway entry scanning instead of a
hard 256 maximum. This helps some alternate headers with tight
boundaries, though pathway count inference remains imperfect
without XML metadata.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OTRExporter creates empty placeholder files for actor list data
(e.g. Bmori1_room_0ActorEntry_000054). Add these as companion
files in the scene factory.

32,151 passed (+568), 109 failed, 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OoT alternate headers reference the same DLists as primary headers
under Set_-prefixed names. OTRExporter creates both files with
identical content.

- Add RegisterAssetAlias to Companion for creating duplicate O2R
  entries with the same binary data under different names
- Scene factory uses entryName for DList symbols and
  ResolveGfxWithAlias to register aliases when an existing DList
  is found at the same offset
- Alias files are written during the export phase using the
  already-serialized binary data (zero re-parsing overhead)

34,539 passed (+2,388), 109 failed, 738 not generated.
Session total: 12,377 → 34,539 (34.9% → 97.6%).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace naive 0xFFFFFFFF scan with a command-aware parser that
correctly determines cutscene boundaries by parsing the command
structure (ID + entry count + entry size per type).

Handles camera splines (terminated by continueFlag), scene
transitions (0x2D), destinations (0x3E8), and standard commands.

Cutscene sizes are now correct, but content still differs from
reference because OTRExporter re-serializes with different byte
ordering (ROM is BE, O2R is LE with CMD_HH packing). Full
re-serialization is the next step.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document the BE→LE field re-packing needed for each command type.
Raw copy doesn't work because OTRExporter uses CMD_HH/CMD_BBH/CMD_HBB
macros to pack fields into uint32 words differently than ROM layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace raw cutscene copy with proper BE→LE re-serialization using
CMD_HH/CMD_BBH/CMD_HBB field packing to match OTRExporter output.
Handles camera splines, actor cues, misc/lighting/BGM, textbox,
rumble, settime, transition, and destination commands.

33 additional cutscenes now match. 76 failures remain (likely
a subtle issue with uint16/uint32 field reading in some entries).

34,572 passed (97.7%), 76 failed, 738 not generated.
Session total: 12,377 → 34,572 (34.9% → 97.7%).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Actor cue entries have rotY/rotZ as the 3rd word packed with CMD_HH,
not a raw uint32. Differentiate actor cues from misc/lighting/BGM
commands to apply correct packing.

34,602 passed (97.8%), 46 failed (44 cutscene, 1 pathway, 1 version).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
34,602/35,386 (97.8%) passing. Remaining: 44 cutscene format
issues, 598 audio (no factory), 135 scene sub-assets, 4 text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
briaguya0 and others added 28 commits April 6, 2026 08:19
- Replace inline tuple with DrumEntry struct
- Move SFXEntry and InstEntry structs to header
- Add FontResidueState struct for cross-font stack residue tracking
- Extract all three parsers as private methods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace FontResidueState struct with FontResidue class owning
  Reset, SeedFromDrums, ApplyToInstrument, UpdateFromInstrument
- Seed residue in ParseDrums instead of ParseInstruments
- Remove drums param from ParseInstruments
- Move DrumEntry, SFXEntry, InstEntry structs to header

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…riter

Extract's font loop is now: parse drums → parse sfx → parse instruments
→ write counts → WriteDrums → WriteInstruments → WriteSFXEntries →
register companion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add FontWriteContext struct bundling audioBank, sampleBankTable,
  sampleMap to reduce parameter counts across all font methods
- Reorder loop body: parse all data first, then write via
  WriteFontCompanion
- Rename fi/fe to fontIndex/fontEntry for clarity

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract CalculateCutsceneSize into separate method
- Add helper functions IsCameraCmd, IsSingleEntryCmd, IsSmallEntryCmd
- Replace csParseOk flag with direct return {} on corrupt data
- Replace else-if chain with early continues
- Rename variables for clarity (csMaxBytes→endOffset, csSizeCalc→reader,
  csCmdWord2→entryCount, cf→marker, totalCsBytes→csSize)
- Add comments and spacing throughout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace free SerializeCutscene function with CutsceneSerializer class
- Serialize orchestrates: CalculateSize → Write
- Rename variables for clarity (calculatedSize/csSize → size)
- Add comment explaining two-phase approach

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move command helpers (IsCameraCmd, IsSmallEntryCmd, etc.) from
  file-static to CutsceneSerializer private static members
- Extract IsHandledCmd with comments for each category
- Extract WriteCameraCmd, WriteSingleEntryCmd, WriteEntryCountCmd
- Remove dead code (0x07/0x08 branch inside unhandled block)
- Replace else-if chains with early continues throughout
- Pull shared header fields (base, startFrame, endFrame) above
  per-command branches in WriteEntryCountCmd
- Clean up variable names and add comments/spacing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract GetLimbDataSize helper for limb type → data size mapping
- Remove unused canAutoDiscoverGfx variable
- Remove unused autoDiscover parameter from ResolveGfxPointer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract ParseLimbHeader, ParseCurveLimb, ParseLegacyLimb,
  ParseStandardLimb, ParseLODLimb, ParseSkinLimb as private methods
- Replace else-if chain with early returns per type
- Use AutoDecode offset overload in ParseSkinLimb (remove throwaway
  YAML nodes)
- Remove unused autoDiscover param from ResolveGfxPointer
- Add T& vs shared_ptr cross-cutting concern to checklist

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract ParseAnimatedSkinData for SkinType_Animated handling
- Extract ParseSkinVertices and ParseSkinTransformations helpers
- Flatten nesting with early returns in ParseSkinLimb
- Move skinSegmentAddr null check into ParseAnimatedSkinData

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename variables (np→numPoints, ptsAddr→pointsAddr), add comments,
improve spacing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add #ifdef OOT_SUPPORT guard
- Split parseMessages into ParseMessagesNTSC and ParseMessagesPAL
- Extract readMessageText, readMessageMetadata, readMessageOffsetNTSC,
  readMessageOffsetPAL helpers
- Extract IsEndOfMessageCode, GetTrailingBytes with named control codes
- Simplify message text loop: no flags, inline trailing byte consumption
- Move all static functions to private methods on OoTTextFactory
- Move MessageEntry struct to header
- Pass DataChunk instead of unpacked data/size
- Improve variable names, comments, and spacing throughout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add guards to DeferredVtx.h/.cpp and OoTTextFactory.h/.cpp.
All OoT factory files now consistently guarded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reviewed all changes in DisplayListFactory.cpp diff from main.
Documented cleanup needed in docs/oot-dlist-cleanup.md:
- Extract OoT-specific handlers from shared Export/parse paths
- Replace GBIMinorVersion gates with config-driven flags
- Deduplicate alias segment detection, address resolution helpers
- Consolidate gSunDL/sShadowMaterialDL name-based hacks
- Investigate G_SETOTHERMODE_H LUT encoding, NOOP zeroing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
First step of DisplayListFactory OoT extraction — move OoT-specific
code to a separate file to reduce diff noise in shared code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restore main's SearchVtx in DisplayListFactory, OoT version (with
OOT:ARRAY and cross-segment support) dispatched from helpers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract the gSunDL ranged-match VTX hack from DListBinaryExporter::Export
into OoT::DListHelpers::HandleGSunDLVtx.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract alias segment detection, cross-file VTX check, OOT:ARRAY
lookup, virtual segment handling, and cross-segment fallback into
HandleExportVtx. Restore main's G_VTX path as the fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract segment 8-13 skip, RemapSegmentedAddr fallback, and
cross-segment DL fallback. Restore main's G_DL path as fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract sShadowMaterialDL hack, unresolved texture fallback, and
gSunDL SETTILE/LOADBLOCK format corrections. Restore main's
G_SETTIMG path as fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract OOT:MTX type chain, RemapSegmentedAddr fallback, and
cross-segment matrix fallback. Restore main's G_MTX path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract G_RDPHALF_1/G_BRANCH_Z handler, G_SETOTHERMODE_H LUT
re-encoding, G_NOOP zeroing, and unhandled opcode zeroing into
HandleExportOpcodeFixups.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract all OoT-specific parse logic:
- HandleParseDL: child DList symbol derivation + AddAsset skip
- HandleParseOpcodes: G_RDPHALF_1 DeferredVtx scanning, G_MTX logging
- HandleParseVtx: cross-segment comparison, alias skip, DeferredVtx
- FlushParseVtx: deferred VTX merge at end of parse
- ShouldSkipAutoDiscovery: gate for light AddAsset skip
- Remove dead isAutogen variable

Restore main's original paths as fallbacks for all opcodes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keep the original main code at its original indent level inside
if (!OoT::DListHelpers::Handle...) blocks to minimize the diff
against main. Add comments explaining the intentional non-indentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace per-opcode wrapper blocks in DisplayListFactory with three
method-level replacements (SearchVtx, Export, Parse) that early-return
when OoT is active. DisplayListFactory.cpp diff vs main is now just
10 insertions (1 include + 3 early-return blocks), with main's code
completely untouched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Strip all OoT work-in-progress documentation and SoH-specific
tooling, manifests, DMA tables, VTX data, and test infrastructure
to produce a clean Torch-only branch for PR review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@briaguya0 briaguya0 mentioned this pull request Apr 7, 2026
Copy link
Copy Markdown
Member

@inspectredc inspectredc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First of all this is really cool to see! I haven't gone through and tested the factories yet but left a few questions about the PR overall in the major sections (the main major blocker being the issue in the DisplayListFactory)


// Deferred VTX consolidation state (ZAPD-style MergeConnectingVertexLists).
// ZAPD merges VTX per-DList (each DList has its own vertices map and merge pass).
// We collect VTX during each DList parse call and flush at the end of that parse.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be beneficial for us to have in the main Torch factories folder?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm not sure, right now it feel very tied to oot, it's just replicating

https://github.com/HarbourMasters/ZAPDTR/blob/74a8e9d92214ce9e028aa1c51c1be7622e98d200/ZAPD/ZDisplayList.cpp#L2134-L2167

i'm not sure what it would take to make this not oot-specific, and i'm not sure what other games would benefit from it

@@ -0,0 +1,122 @@
#ifdef OOT_SUPPORT
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiousity, is there any benefit to having this #ifdef since my understanding is that the cmakelists already handles the exclusion of this file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definitely not needed, i was just following the LUS pattern from things like

https://github.com/Kenix3/libultraship/blob/ddac21e09ee449d8086392ad2ec7493512c0ac6a/src/fast/backends/gfx_dxgi.cpp#L1

#if defined(ENABLE_DX11) || defined(ENABLE_DX12)

https://github.com/Kenix3/libultraship/blob/ddac21e09ee449d8086392ad2ec7493512c0ac6a/src/fast/CMakeLists.txt#L28-L32

if (NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
    list(FILTER Source_Files__Graphic EXCLUDE REGEX "graphic/Fast3D/backends/gfx_dxgi*")
    list(FILTER Source_Files__Graphic EXCLUDE REGEX "graphic/Fast3D/backends/gfx_direct3d*")
    list(FILTER Source_Files__Graphic EXCLUDE REGEX "graphic/Fast3D/backends/dxsdk/*")
endif()

but since that isn't standard in Torch i'm happy to remove the file-level guards

Comment on lines +1632 to +1634
// Not all 0x80XXXXXX addresses are VRAM pointers — segment 0x80 addresses also have
// bit 31 set. We distinguish them by checking if the address falls within the current
// file's VRAM range.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too sure this is really necessary since the segment range doesn't go this high but it can't hurt to have

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is mostly a ZAPD-ism. "segment 0x80" means "this is something we read from VRAM, but it exists in a different file"

we could use a different number for "segment" in these scenarios and update

    // If addr is below vramBase, it's not pointing into this file's VRAM range.
    // It's a segmented address using segment 0x80+.
    if (addr < vramBase) {
        return { addr, ResolvedAddr::Segmented };
    }

to patch addr to replace the 0x80 at the top with some number we use to mean "virtual segment" - but using 0x80 is what ZAPD was doing so sticking to that seemed reasonable

i worked with claude to write up a summary of how this logic came to be

claude dive

Understanding ResolveVirtualAddr and IS_VIRTUAL_SEGMENT

Context

inspectredc commented on Companion.cpp:1634 saying the distinction "can't hurt" but
questioning whether it's necessary since "the segment range doesn't go this high."
We need to fully understand why this logic exists.

Background: Why segment 128 and why it's not arbitrary

Normal N64 objects use segments 2-6. The ROM data contains pointers like 0x06001234
where the top byte (0x06) is the segment number. Torch's YAML declares
segments: [6, <rom_base>], and PopulateAddrMap stores assets with keys like
(6 << 24) | offset. When a DList has w1 = 0x06001234, Torch extracts segment 6
from the top byte, looks up the ROM base, and the gAddrMap key matches. The round-trip
works because the ROM pointer's top byte matches the YAML segment number.

Overlays and code sections are different. They're loaded into the N64's VRAM at
0x80XXXXXX addresses (KSEG0). Pointers in the ROM between overlay assets use these
VRAM addresses — e.g. 0x808C7378. The top byte is 0x80 = 128. For the same
round-trip to work, the YAML must use segment 128. This isn't a ZAPD convention that
we copied blindly — it's forced by the ROM data. ZAPD happens to use the same value
for the same reason (ZFile.h:44).

The problem: VRAM pointers don't directly match file offsets

For normal objects (segment 6), the ROM pointer IS the segment+offset: 0x06001234
means segment 6, offset 0x1234. The offset directly matches what the YAML declares.

For overlays, the ROM pointer is a VRAM address: 0x808C7378. The bottom 24 bits
(0x8C7378) are NOT a file offset — they're the bottom 24 bits of a VRAM address.
The actual file offset is 0x808C7378 - vramBase(0x808C1190) = 0x61E8. Without this
subtraction, the gAddrMap key (128 << 24) | 0x61E8 = 0x800061E8 won't match the
raw pointer's bottom 24 bits (128 << 24) | 0x8C7378 = 0x808C7378.

This is what ResolveVirtualAddr solves: it subtracts the VRAM base to recover the
file offset, then reconstructs the gAddrMap key.

What main's PatchVirtualAddr did (and why it broke)

Main's version (from inspectredc's commit 5e334dc):

if (addr & 0x80000000) {
    addr -= vramBase;
    addr += physStart;
}

Two problems:

  1. Wrong result format: Converts VRAM → absolute ROM address. For ovl_Boss_Dodongo:

    • Input: 0x808C7378 (VRAM pointer to texture)
    • Result: 0x808C7378 - 0x808C1190 + 0x00B69530 = 0x00B6F718 (ROM absolute)
    • But gAddrMap key for this texture is (128 << 24) | 0x61E8 = 0x800061E8
    • Lookup fails: 0x00B6F718 ≠ 0x800061E8
  2. Assumes all 0x80 addresses are into the current file: An overlay DList can
    reference data in a completely different file (e.g. gMtxClear in the code segment).
    The pointer 0x800FBC20 is a VRAM address into sys_matrix's range, not this
    overlay's. Subtracting this overlay's vramBase gives garbage.

What ResolveVirtualAddr does

Fixes both problems:

  1. Correct format: Subtracts vramBase to get file offset, then reconstructs
    (128 << 24) | offset — matching gAddrMap's key format.

  2. Range check: Only subtracts vramBase if addr >= vramBase (meaning it actually
    points into this file). Cross-file references (addr < vramBase) are passed through
    for external file lookup.

0x80XXXXXX addresses are VRAM pointers from ROM data

There is no "segment 0x80" on the N64. All 0x80XXXXXX addresses that flow through
ResolveVirtualAddr are real VRAM pointers read from ROM data (DList commands,
skeleton pointers, etc.). The N64's KSEG0 starts at 0x80000000, so all runtime
memory addresses start with 0x80.

The addr < vramBase check distinguishes whether a VRAM pointer is into the
current file or into a different file with a lower VRAM range. Cross-file
pointers are passed through for external file lookup.

Note: this would break if a file referenced something with a higher VRAM address
(the pointer would look like it's in the current file and get the wrong subtraction).
In practice this doesn't happen — shared code/data is loaded at low VRAM addresses
before overlays, so cross-file references always point downward. Overlays don't
reference other overlays since they're loaded/unloaded dynamically.

The four cases in ResolveVirtualAddr

Case 1: addr < 0x80000000 (normal segmented or file-relative)

Example: 0x06001234 (object_dodongo VTX reference with segment 6)
Result: returned as-is, classified as Segmented
When: processing any file with normal segments (objects, scenes, rooms)

Case 2: addr >= 0x80000000, no virtual mapping for current file

Example: 0x800EA0C8 encountered in a file without :config: virtual:
Result: returned as Unknown
When: defensive — e.g. a code file like z_fbdemo_circle that has segment 128
in its YAML but no virtual: mapping. The address is already a synthetic key.

Case 3: addr >= 0x80000000, below vramBase (cross-file VRAM pointer)

Example: processing ovl_Boss_Dodongo (vramBase=0x808C1190), DList has G_MTX
pointing to gMtxClear at VRAM 0x800FBC20 (in sys_matrix's VRAM range, not this
overlay's).
0x800FBC20 < 0x808C1190 → this VRAM pointer is NOT into our overlay's data.
Returned as { 0x800FBC20, Segmented }. GetNodeByAddr then searches external files
and resolves it using sys_matrix's virtual mapping:
0x800FBC20 - 0x80010F00 = 0xEAD20 → key (128 << 24) | 0xEAD20 = 0x800EAD20
→ finds gMtxClear.

Case 4: addr >= vramBase (VRAM pointer into current file)

Example: processing ovl_Boss_Dodongo, DList references texture at 0x808C7378

  • 0x808C7378 >= 0x808C1190 → this IS a VRAM pointer into our overlay
  • relOffset = 0x808C7378 - 0x808C1190 = 0x61E8
  • Finds segment 128 in config → returns (128 << 24) | 0x61E8 = 0x800061E8
  • Matches gAddrMap key → lookup succeeds

Concrete files

File Segment BaseAddress/virtual VRAM pointers in ROM?
object_dodongo 6 none No — uses 0x06XXXXXX
ovl_Boss_Dodongo 128 (default) 0x808C1190 Yes — 0x808CXXXX
z_fbdemo_circle 128 (default) none (= 0) Yes — 0x800XXXXX
sys_matrix 128 (default) 0x80010F00 Yes — 0x8001XXXX

Why IS_VIRTUAL_SEGMENT exists

IS_VIRTUAL_SEGMENT identifies addresses with the top byte >= 0x80 — i.e. N64 VRAM
addresses. It's used as a fallback in DList export: when a 0x80XXXXXX pointer
couldn't be resolved by any lookup, IS_VIRTUAL_SEGMENT catches it and writes null
vtxDecl (matching OTRExporter's behavior). Without it, the cross-segment fallback
would write (w1 & 0x0FFFFFFF) + 1 instead of 0, producing wrong bytes.

Concrete example: sCircleDList in z_fbdemo_circle references VTX via VRAM
address 0x800FB168. This file has no virtual mapping, so the address can't be
resolved. IS_VIRTUAL_SEGMENT catches it → null. Without the check, it would fall
through to the cross-segment fallback and write 0x000FB169 instead of 0x00000000.

Also used in:

  • ASSET_PTR macro: extract offset from both regular segments and VRAM addresses
  • Decompressor::TranslateAddr: route VRAM addresses to the segment table

Where 0x80XXXXXX addresses appear in ROM data

1. Overlay → same overlay (resolved by ResolveVirtualAddr)
ovl_Boss_Dodongo DList → sLavaFloorLavaTex: w1 = 0x808C7378
ResolveVirtualAddr: 0x808C7378 - vramBase(0x808C1190) = 0x61E8 → lookup succeeds

2. Overlay → different file (resolved by cross-file lookup)
ovl_Boss_Dodongo DList → gMtxClear: w1 = 0x800FBC20
Below this overlay's vramBase → passed to external file lookup →
sys_matrix resolves: 0x800FBC20 - 0x80010F00 = 0xEAD20 → lookup succeeds

3. Code section without virtual mapping (caught by IS_VIRTUAL_SEGMENT)
z_fbdemo_circle DList → sTransCircleVtx: w1 = 0x800FB168
No virtual mapping → PatchVirtualAddr returns unchanged → all lookups fail →
IS_VIRTUAL_SEGMENT(0x800FB168) = true → write null (matches OTRExporter)

Why 128 for the YAML segment number

N64 VRAM (KSEG0) starts at 0x80000000 — top byte is 0x80 = 128. Torch stores
overlay assets in gAddrMap with keys (128 << 24) | file_offset. This choice
doesn't add complexity — even with a different number, we'd still need:

  • ResolveVirtualAddr to subtract vramBase from ROM VRAM pointers
  • IS_VIRTUAL_SEGMENT to catch unresolvable VRAM addresses
  • Cross-file VRAM resolution

128 follows ZAPD's convention (ZFile.h:44 segment = 0x80) and avoids colliding
with real N64 segments (0-15).

const auto symbol = GetSafeNode<std::string>(asset, "symbol", "");
const auto decl = this->GetNodeByAddr(offset);

// For OoT, all assets should be pre-declared in enriched YAML.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should just make this a general setting we add to the config as I think this would make sense based on project's own preferences

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, my original plan was to just remove this before PRing but i forgot to (i was using it when trying to improve o2r generation speed because dynamically adding things slowed it down a ton)

the config setting seems like a great idea!

reader.SetEndianness(Torch::Endianness::Big);
reader.Seek(0x10, LUS::SeekOffsetType::Start);
this->gRomCRC = BSWAP32(reader.ReadUInt32());
this->gRomCRC = reader.ReadUInt32();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May have to require some decomps/ports to check this doesn't break any checks they make

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did some investigation into this here 8f06c36

as far as ports go, it seems ghostship and spaghettikart are fine, but starship will need to update the SF64_VER constants.

i didn't check decomps, good call

#endif

std::optional<std::tuple<std::string, YAML::Node>> SearchVtx(uint32_t ptr) {
auto result = OoT::DListHelpers::SearchVtx(ptr);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this break without OOT_SUPPORT? And would using Torch's own VTX factory remove the need for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this break without OOT_SUPPORT?

yes. good catch!

would using Torch's own VTX factory remove the need for this?

it wouldn't be a drop-in replacement. SearchVtx handles both VTX and OOT:ARRAY (of type VTX), and handles oot alias segments

i think some of the logic is inherently oot-specific, but some could probably be generalized. i'm not sure what the most logical way to split it up would be

mAliases[primaryPath].push_back(aliasPath);
}

void AliasManager::WriteAliases(const std::string& primaryPath, BinaryWrapper* wrapper,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a quick question about this system, is this a way for one file's data to be written to multiple locations in the output binary, and if so what is the use case for it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a way for one file's data to be written to multiple locations in the output binary

yes

what is the use case for it?

documented here briaguya0@86964d9 in the "Why aliases exist" section

DLists embed a CRC64 of their own output path ("bhash"). Set_ DLists need to be byte-identical copies of the base DList (same bhash). Re-parsing the DList under a Set_ path produces a different bhash → binary mismatch. Aliases are copies,
not independent assets.

zapd handles this by reading the same data but exporting it to different paths (happens in SetMesh)

we can't do that because DListBinaryExporter::Export adds a bhash based on the path provided as replacement, so dlists exported because they're referenced by bdan_room_0Set_0000E0 would end up with different bhash values than the ones referenced by bdan_room_0, even though they are supposed to be referencing the exact same file (just at different paths).

it's possible we can get away with them having different bhash values and this is only needed to byte-match zapd generated o2rs, but i'd need to do some digging to be sure

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants