Merge branch 'claude/quirky-jepsen-fd60f1' — N.4 plan + CLAUDE.md pointer

This commit is contained in:
Erik 2026-05-08 13:04:21 +02:00
commit 076a324eca
3 changed files with 2891 additions and 0 deletions

View file

@ -437,6 +437,16 @@ acdream's plan lives in two files committed to the repo:
acceptance criteria. Do not drift from the spec without explicit user acceptance criteria. Do not drift from the spec without explicit user
approval. approval.
**Currently in flight: Phase N.4 — Rendering Pipeline Foundation.** Plan
at [`docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md`](docs/superpowers/plans/2026-05-08-phase-n4-rendering-foundation.md).
Spec at [`docs/superpowers/specs/2026-05-08-phase-n4-rendering-foundation-design.md`](docs/superpowers/specs/2026-05-08-phase-n4-rendering-foundation-design.md).
This is a 3-4 week phase adopting WB's `ObjectMeshManager` + `TextureAtlasManager`
as our shared rendering infrastructure. The plan is a **living document**
task checkboxes get marked as commits land, adjustments are appended in-place,
weeks 2-4 may be revised based on week 1 discoveries. Read the plan's "Plan
Living-Document Convention" section before contributing. After N.4 ships
this pointer is removed and the plan's status flips to "Final."
**Rules:** **Rules:**
1. Before starting a new phase or sub-piece, re-read the roadmap and the 1. Before starting a new phase or sub-piece, re-read the roadmap and the

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,414 @@
# Phase N.4 — Rendering Pipeline Foundation: Design
**Date:** 2026-05-08
**Status:** Design complete, awaiting plan generation.
**Parent design:** [2026-05-08-phase-n-worldbuilder-migration-design.md](2026-05-08-phase-n-worldbuilder-migration-design.md)
**Roadmap entry:** [docs/plans/2026-04-11-roadmap.md](../../plans/2026-04-11-roadmap.md) — Phase N.4
**Inventory reference:** [docs/architecture/worldbuilder-inventory.md](../../architecture/worldbuilder-inventory.md)
**Related:** [ISSUE #51](../../ISSUES.md) — terrain split formula divergence (handled in N.5).
## Goal
Adopt WB's `Chorizite.OpenGLSDLBackend.Lib.ObjectMeshManager` and
`TextureAtlasManager` as acdream's rendering pipeline foundation. This
is the integration that unblocks Phases N.5 (terrain), N.6 (static
objects), N.7 (env cells), N.8 (sky/particles), and absorbs N.10
(GL infrastructure consolidation). N.4 ships no visible change — the
world should look identical to today; what changes is the infrastructure
behind the scenes.
## Why
**The roadmap's original "drop-in helper" framing was wrong for N.4.**
Discovery during brainstorm 2026-05-08: WB's `ObjectMeshManager` is not
a stateless helper class like `SceneryHelpers` (N.1) or `TextureHelpers`
(N.3). It is a 2070-line stateful asset pipeline that owns:
- GPU resources per object (VAO/VBO/IBO via `ObjectRenderData`)
- Reference counting (`IncrementRefCount`/`DecrementRefCount`)
- LRU cache + memory budget (default 1 GB)
- Background-thread CPU mesh preparation, main-thread GPU upload
- Shared texture atlases keyed by `(Width, Height, Format)`
- Particle emitter staging
- Modern bindless rendering path on capable hardware
**There is no clean "just the mesh extraction" entry point.** WB's
`BuildPolygonIndices` (the algorithm we already faithfully ported into
[GfxObjMesh.cs](../../../src/AcDream.Core/Meshing/GfxObjMesh.cs)) is a
private method tightly coupled to atlas batching. To use WB's tested
infrastructure at all means adopting the whole pipeline.
**N.5 + N.6 + N.7 build on this foundation.** WB's
`TerrainRenderManager`, `StaticObjectRenderManager`, and
`EnvCellRenderManager` all consume `ObjectMeshManager` (or its atlas)
as substrate. Without N.4, each later phase would need to either fork
those render managers or duplicate the infrastructure. Doing N.4 now
means N.5/N.6/N.7 become integration phases on top of shared plumbing,
not parallel infrastructure builds.
**Real benefits beyond infrastructure consolidation:**
1. **Memory budget with LRU eviction** (we don't have this; bigger
stream radii currently risk OOM).
2. **Texture atlasing → ~4-8× fewer draw calls** for static scenery
(~1100 entities at Holtburg today).
3. **Background-thread mesh preparation** — addresses the
render-thread-stall problem from
[feedback_phase_a1_hotfix_saga.md](../../../memory/feedback_phase_a1_hotfix_saga.md)
that forced us to revert async streaming.
4. **Bindless textures** on capable hardware (free perf when
GL 4.3 + `GL_ARB_bindless_texture` are available).
## Architecture
### Two-tier rendering split
acdream's content cleanly partitions into two categories that map onto
two rendering paths:
| Tier | Content | Why this category | Path |
|---|---|---|---|
| **Atlas (shared)** | Terrain props, scenery (procedural — trees / rocks / bushes / fences from ~50 templates), buildings, slabs, dungeon static geometry | Client-side procedural; no per-instance variation; many instances of few unique meshes | WB's `ObjectMeshManager` + `TextureAtlasManager`. Big sharing wins (1100 entities ↦ ~50 atlas slots). |
| **Per-instance (customized)** | Server-spawned entities (`CreateObject`): characters, creatures, equipped items. Anything carrying `SubPalettes` / `TextureChanges` / `AnimPartChange` / `HiddenParts` / `GfxObjRemapping` | Always uniquely customized; few visible at a time (~10-50) | Existing [TextureCache.GetOrUploadWithPaletteOverride](../../../src/AcDream.App/Rendering/TextureCache.cs:122). Already hash-keys overrides for caching; already tested. |
**Routing rule**:
- Objects spawned by `LandblockStreamLoader` (procedural, no
customization) → atlas tier.
- Objects spawned by `CreateObject` (network, always customized) →
per-instance tier.
The boundary mirrors a distinction that already exists in our
networking model. We are not inventing a new conceptual line; we are
matching one that's already there.
### Animation handling
**Core insight:** in AC, animation is per-part TRANSFORM changes, not
mesh changes. A creature's Setup is a list of rigid GfxObj parts (head,
body, hands, etc.). Each part is its own static mesh; vertices inside
each part never change. Animation moves the parts as rigid bodies.
This means **mesh data is static even for animated entities** — the
cache works fine. Only the per-part transforms change per frame, and
those don't live in the mesh cache.
**Composition at draw time:**
```
final_part_world_matrix
= entity_world_transform
× animation_override (from AnimationSequencer, this frame)
× rest_pose_transform (cached in ObjectMeshData.SetupParts)
```
- WB's `ObjectMeshData.SetupParts: List<(ulong GfxObjId, Matrix4x4 Transform)>`
stores the rest-pose transforms (cached, shared).
- Our existing [AnimationSequencer](../../../src/AcDream.Core/Animation/AnimationSequencer.cs)
is **untouched**. It continues to produce per-part override matrices
per frame, driven by motion table + current motion command + tick.
- The renderer composes the three matrices per part per draw and pushes
the result as a uniform/instance attribute.
**`AnimPartChange`** (server swaps a part's GfxObj — e.g., wielding a
sword): per-entity override map `Dictionary<int partIndex, ulong gfxObjId>`.
At draw time, look up override; fall back to cached Setup part. WB's
mesh manager caches the override GfxObj's mesh data the same way as
any other part — first time seen, then shared.
**`HiddenParts`** (bitmask hiding parts): per-entity `ulong` bitmask.
Draw loop: `if (hiddenMask & (1 << partIndex)) continue;`.
**Per-frame CPU cost:** ~50 visible animated entities × ~20 parts =
~1000 matrix multiplies per frame. Sub-millisecond on any CPU.
**GPU-side per-draw transform push:** start with uniform-per-draw
(simple, ~1000 draws/frame for animated entities — fine). Promote to
per-instance vertex attribute (instanced draw, ~50 draws/frame) only
if measured perf demands it.
### Streaming loader integration
Adapter shim, ~200 LOC, sits between `LandblockStreamLoader` /
`WorldSession` and `ObjectMeshManager`:
| Source event | Adapter call | What `ObjectMeshManager` does |
|---|---|---|
| Landblock loaded by streaming | `IncrementRefCount(id)` per unique GfxObj/Setup id in `Setups[]` + `Statics[]` | Begins CPU prep on background worker if not cached; queues GPU upload on main thread |
| Landblock unloaded by streaming (radius hysteresis) | `DecrementRefCount(id)` per object | Drops to LRU when count reaches 0; LRU + 1 GB memory budget handles eviction |
| Network `CreateObject` | Per-instance path: build `PaletteOverride` from `SubPalettes`, decode through `TextureCache.GetOrUploadWithPaletteOverride`, register entity-local mesh data | Bypasses WB atlas; stays in our existing per-instance path |
| Network `RemoveObject` | Release per-instance state for entity | (no WB call) |
**Pending-spawn list preservation:** the streaming loader's existing
[pending-spawn list](../../../memory/feedback_phase_a1_hotfix_saga.md)
mechanism stays in place. `CreateObject` arriving before its landblock
streams in still parks until the landblock arrives, then drains. The
adapter is invoked when the spawn drains, not when it parks.
**Thread safety:** WB's `ObjectMeshManager` uses `ConcurrentDictionary`
for its internal state and is designed to take `IncrementRefCount` calls
from any thread. Our streaming worker can call it directly without
marshaling onto the render thread. (This is part of why WB's design
addresses the render-thread-stall problem.)
### Surface metadata strategy
**Side-table, not fork patch.**
WB's `MeshBatchData` carries `IsTransparent` + `IsAdditive`. We need to
preserve these acdream-specific surface properties already present in
our [GfxObjMesh.cs](../../../src/AcDream.Core/Meshing/GfxObjMesh.cs):
- `Translucency` (`TranslucencyKind` enum: Opaque / AlphaBlend / Additive)
- `Luminosity` (float, self-illumination coefficient — sky pass critical)
- `Diffuse` (float)
- `SurfOpacity` (float, derived from `Surface.Translucency`)
- `NeedsUvRepeat` (bool, derived from authored UV range — sky-pass wrap-mode selection)
- `DisableFog` (bool, derived from emissive surface flags — sky-pass fog skip)
Our renderer integration maintains a side-table:
`Dictionary<(ulong gfxObjId, int surfaceIdx), AcSurfaceMetadata>`. The
key matches the shape of today's `GfxObjSubMesh` — a (GfxObj, surface
index) pair uniquely identifies a per-surface render batch. Stable
across `IncrementRefCount` cycles. The metadata is computed once at
mesh-extraction time (matching today's `GfxObjMesh.Build`) and looked
up at draw time.
**Why side-table not fork patch:**
- Keeps WB's types pristine; upstream merges stay clean.
- Lookup cost is negligible (one hash lookup per batch per frame).
- Easy to roll back if WB's design evolves to incorporate similar fields.
- Preserves the careful sky-pass work done in C.1 with no risk to sky
rendering during this migration.
### Fork hygiene
**Target: zero fork patches for N.4.** WB's `acdream` branch stays at
upstream `master` plus the editor-only file deletions inherited from
N.0/N.1. If a fork patch becomes genuinely necessary mid-implementation
(e.g., a public hook is missing for our customization layer), it lands
as a single named patch with a comment explaining the rationale. Each
patch is candidate to upstream back to Chorizite/WorldBuilder.
## Components
### New code (acdream-side)
| File | Responsibility |
|---|---|
| `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs` | Bridges acdream's lifecycle events to `ObjectMeshManager`. Holds the `ObjectMeshManager` instance, exposes `IncrementRefCount` / `DecrementRefCount` / `GetRenderData` to the rest of the renderer. |
| `src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs` | Streaming-loader hook. Walks `LandblockEntry.Setups[]` + `Statics[]`, calls `WbMeshAdapter` with unique ids. Companion `LandblockUnloadAdapter` for unload events. |
| `src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs` | Network-spawn hook. Routes `CreateObject` to per-instance path, `RemoveObject` to release. |
| `src/AcDream.App/Rendering/Wb/AcSurfaceMetadata.cs` | Side-table type holding `Translucency` / `Luminosity` / `Diffuse` / `SurfOpacity` / `NeedsUvRepeat` / `DisableFog`. |
| `src/AcDream.App/Rendering/Wb/AcSurfaceMetadataTable.cs` | The `Dictionary<batchKey, AcSurfaceMetadata>` side-table, populated at mesh-extraction time, queried at draw time. |
| `src/AcDream.App/Rendering/Wb/AnimatedEntityState.cs` | Per-entity render state for animated entities: `partGfxObjOverrides` map (AnimPartChange), `hiddenMask` (HiddenParts), reference to `AnimationSequencer` for per-frame override matrices. |
| `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` | Per-frame draw loop. For each visible entity, looks up `ObjectRenderData`, composes per-part matrices (entity × animation × rest-pose), reads side-table metadata, issues GL draw. |
### Modified code (acdream-side)
| File | Change |
|---|---|
| `src/AcDream.App/Rendering/StaticMeshRenderer.cs` | Replace internal mesh-data + GL-resource handling with calls into `WbMeshAdapter`. Public surface preserved for the rest of the renderer's call sites. **N.6 will fully replace this file**; N.4 leaves it in place as a thin adapter. |
| `src/AcDream.App/Rendering/InstancedMeshRenderer.cs` | Same pattern — internal swap, public surface preserved. **N.6 fully replaces this file.** |
| `src/AcDream.App/Rendering/TextureCache.cs` | Per-instance path stays. Atlas-tier callers (anything using `GetOrUpload(surfaceId)` for static content) route through `WbMeshAdapter` instead. The override paths (`GetOrUploadWithOrigTextureOverride`, `GetOrUploadWithPaletteOverride`) keep their current behavior. |
| `src/AcDream.App/Rendering/GpuWorldState.cs` | Spawn/despawn callbacks route through `WbMeshAdapter`. Pending-spawn list mechanism preserved verbatim. |
| `src/AcDream.App/Rendering/GameWindow.cs` | Construct `WbMeshAdapter` on init; dispose on shutdown. |
| `src/AcDream.Core/Meshing/SetupMesh.cs` | Kept for tests + as the conformance-test reference implementation. Production callers route through `WbMeshAdapter`. |
| `src/AcDream.Core/Meshing/GfxObjMesh.cs` | Kept for tests + conformance reference. Production callers route through `WbMeshAdapter`. |
## Data flow
### Spawn — landblock-streamed (atlas tier)
```
LandblockStreamLoader.Load(landblockId)
→ LandblockEntry { Setups, Statics, ... }
→ LandblockSpawnAdapter.OnLoaded(entry)
for each unique gfxObjId in (entry.Setups entry.Statics):
WbMeshAdapter.IncrementRefCount(gfxObjId)
→ ObjectMeshManager.IncrementRefCount(gfxObjId)
→ if not cached: queue background prep
→ on prep complete: queue main-thread upload
→ on upload: GL VAO/VBO/IBO ready
```
### Spawn — network-customized (per-instance tier)
```
WorldSession.OnCreateObject(msg)
→ EntitySpawnAdapter.OnCreate(entity)
→ build PaletteOverride from msg.SubPalettes
→ for each surface needing per-instance decode:
TextureCache.GetOrUploadWithPaletteOverride(...)
→ register AnimatedEntityState (override map, hidden mask,
animation sequencer reference)
```
### Per-frame draw (atlas tier)
```
WbDrawDispatcher.Draw()
for each visible atlas-tier entity:
var renderData = WbMeshAdapter.GetRenderData(entity.GfxObjId)
foreach (batch in renderData.Batches):
bind atlas, bind shader, push uniforms
foreach (part in renderData.SetupParts):
push final_part_world_matrix uniform
glDrawElements(part.indices)
```
### Per-frame draw (per-instance tier, animated)
```
WbDrawDispatcher.DrawAnimated()
for each visible animated entity:
var state = entity.AnimatedEntityState
var sequencer = entity.AnimationSequencer
sequencer.AdvanceTo(currentTime) // existing
var animOverrides = sequencer.GetCurrentPartTransforms() // existing
foreach (partIdx in 0..parts.Count):
if (state.hiddenMask & (1 << partIdx)) continue;
var gfxObjId = state.partGfxObjOverrides.GetValueOrDefault(partIdx) ?? defaultParts[partIdx]
var renderData = WbMeshAdapter.GetRenderData(gfxObjId)
var meta = AcSurfaceMetadataTable.Lookup(renderData.BatchKey)
var worldMatrix = entityWorld × animOverrides[partIdx] × renderData.RestPose
bind per-instance texture (TextureCache lookup)
push uniforms (worldMatrix, meta.Luminosity, meta.Diffuse, ...)
glDrawElements(...)
```
## Testing
### Algorithmic conformance (before substitution)
Per the N.1 / N.3 pattern, conformance tests run BEFORE the substitution
to prove equivalence:
| Test | Compares |
|---|---|
| `MeshExtraction_OurBuildVsWbBuildPolygonIndices` | Battery of fixture GfxObjs (varying polygon counts, stippling flags, NegUVIndices, double-sided polys). For each: our `GfxObjMesh.Build` output vs WB's `ObjectMeshManager` output (extracted via test harness). Assert: identical vertex arrays, identical index arrays, identical per-bucket surface mapping. |
| `SetupFlattening_OurFlattenVsWbSetupParts` | Battery of representative Setups (flat / hierarchical / Resting-frame / Default-frame / no-frame). For each: our `SetupMesh.Flatten` output vs WB's Setup-parts walk. Assert: identical (GfxObjId, Matrix4x4) sequences. |
| `PerInstanceDecode_OldVsNewPath` | Synthetic palette + texture overrides (mirroring real `CreateObject` data). Decoded through new integrated path vs current `TextureCache.GetOrUploadWithPaletteOverride`. Assert: identical RGBA8. |
If any test fails it's a real divergence — investigate, do not "fix"
the test (per N.3 watchout).
### Component micro-tests
| Test | Covers |
|---|---|
| `LandblockSpawnAdapter_RegistersAndUnregisters` | Mock `ObjectMeshManager`; verify ref-count increments/decrements pair correctly across landblock load/unload events. |
| `LandblockSpawnAdapter_DedupesSharedIds` | Same GfxObj id appearing in multiple landblocks: verify single ref-count per landblock, not per occurrence. |
| `EntitySpawnAdapter_RoutesToPerInstance` | `CreateObject` with `SubPalettes` set: verify per-instance path taken, atlas tier not invoked. |
| `AnimPartChange_OverridesAtDraw` | Per-instance override map: verify draw loop resolves correct part GfxObj id when override present, falls back to Setup default when absent. |
| `HiddenParts_SuppressesDraw` | Bitmask: verify draw loop skips hidden parts. |
| `MatrixComposition_EntityAnimRest` | Known entity transform + animation matrix + rest pose: verify final world matrix matches expected composition order (column-major: rest applied first, then animation, then entity world). |
| `SurfaceMetadata_SideTableLookup` | Populate side-table during mesh extraction; query at draw time; verify Luminosity / Diffuse / DisableFog round-trip correctly. |
### Visual verification (per phase, before flipping `Live ✓`)
Walk the following with the user, comparing against pre-N.4 screenshots
or video:
1. **Holtburg outdoor** — terrain props, scenery, buildings, NPCs,
characters. Verify: no missing entities, no magenta squares, no
alpha bleeding, no shading regressions, no animation hitches.
2. **Drudge Hideout** (or comparable starter dungeon) — EnvCell
geometry, interior lighting, animated creatures.
3. **Foundry** — heavy NPC traffic, customized appearances (the
server's first-time test bed for per-instance customization
correctness).
4. **A character with extreme palette overrides** — char-creation
variant if available, otherwise a known-customized server-side
test character.
5. **Long roam** — walk for ~5 minutes across multiple landblocks,
monitor GPU memory in title bar (memory budget enforcement working
means it stabilizes; memory growing unboundedly means LRU eviction
isn't firing).
## Phasing
Single shippable phase — no internal sub-phases. Within the phase, work
ordered to minimize the duration of "broken in middle" state:
| Week | Focus | "Done when" |
|---|---|---|
| 1 | WB integration plumbing + atlas bring-up for static scenery only (smallest tier, highest sharing factor) + algorithmic conformance tests pass | Conformance tests green; static scenery renders through `ObjectMeshManager` while everything else uses old path |
| 2 | Streaming-loader adapter; LRU + memory budget verified under streaming pressure (long roam + radius 7×7) | Long roam holds steady GPU memory; landblock unload reclaims memory |
| 3 | Per-instance customization path; animated creatures with palette overrides; AnimPartChange + HiddenParts | Drudge / chicken / banderling render with correct customizations; animation matches today |
| 4 | Surface metadata side-table integration; sky-pass preservation; visual verification at named locations; polish | Visual verification at all 5 locations passes; sky pass renders identically; ready for `Live ✓` |
## Risks
1. **Per-instance customization scope creep.** If we discover a
customization path we don't already handle in `TextureCache` (e.g.,
a rare `GfxObjRemapping` case), the per-instance path may need
extension. Mitigation: enumerate all customization paths during
week 3, add tests for each before integrating.
2. **WB threading model interaction with our streaming worker.**
`ObjectMeshManager` uses `ConcurrentDictionary` and is designed for
concurrent `IncrementRefCount` calls, but its `_pendingRequests` queue
is guarded by a `lock`. Heavy concurrent landblock loads could serialize
on this lock. Mitigation: profile during week 2; if contention is
visible, batch landblock loads to amortize the lock.
3. **Sky pass regression.** The sky pass's `NeedsUvRepeat` /
`DisableFog` / `Luminosity` flow is fragile and load-bearing. The
side-table preserves the data, but the integration point with
`SkyRenderer` needs careful review. Mitigation: sky-pass-specific
visual verification before flipping `Live ✓`.
4. **Bindless rendering path mismatch.** WB enables bindless when
`GL 4.3 + GL_ARB_bindless_texture` are present. If we ship through
the bindless path and a player has older hardware, fallback path
must work. Mitigation: dev/test with `_useModernRendering = false`
forced during week 1 to ensure the non-bindless path is also exercised.
5. **Performance regression** during integration of week 1's "atlas for
static scenery, old path for everything else" mixed state. Mitigation:
keep the feature gate `ACDREAM_USE_WB_FOUNDATION=1` during weeks 1-3;
default-off until week 4 visual verification.
## Out of scope
- Replacing `StaticMeshRenderer` / `InstancedMeshRenderer` — those become
thin adapters in N.4 and are fully replaced in **N.6**.
- Replacing `TerrainAtlas` / `TerrainBlending` — that's **N.5**.
- Replacing EnvCell rendering — that's **N.7**.
- Replacing sky / particle rendering — that's **N.8**.
- Replacing visibility / culling — that's **N.9**.
- Per-instance customization beyond what's in today's `TextureCache`
(e.g., novel customization opcodes from future Phase F work) — out of
scope; future opcodes route through the same per-instance path.
## Documentation impact
- [x] [Roadmap](../../plans/2026-04-11-roadmap.md) — N.4 entry rebranded
and N.5/N.6/N.7/N.8/N.9/N.10 estimates revised (committed `6d42744`
and merged to main).
- [ ] This spec — written 2026-05-08, committing alongside.
- [ ] [worldbuilder-inventory.md](../../architecture/worldbuilder-inventory.md)
— minor update at end of N.4 to mark `ObjectMeshManager` /
`TextureAtlasManager` as "now wired up" rather than just "should
use." Not blocking N.4 start.
- [ ] [acdream-architecture.md](../../architecture/acdream-architecture.md)
— needs an acknowledging note after N.4 lands that the rendering
pipeline is WB-backed. Can follow in a later commit.
## Reference materials
- WB `ObjectMeshManager`: `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ObjectMeshManager.cs`
- WB `TextureAtlasManager`: `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/TextureAtlasManager.cs`
- WB `BaseObjectRenderManager`: `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/BaseObjectRenderManager.cs`
- ACME secondary oracle for character appearance (CreaturePalette
/ GfxObjRemapping / HiddenParts behavior):
`references/WorldBuilder-ACME-Edition/WorldBuilder/Editors/Landscape/StaticObjectManager.cs`
- Existing acdream code:
- [SetupMesh.cs](../../../src/AcDream.Core/Meshing/SetupMesh.cs)
- [GfxObjMesh.cs](../../../src/AcDream.Core/Meshing/GfxObjMesh.cs)
- [TextureCache.cs](../../../src/AcDream.App/Rendering/TextureCache.cs)
- [PaletteOverride.cs](../../../src/AcDream.Core/World/PaletteOverride.cs)
- [AnimationSequencer.cs](../../../src/AcDream.Core/Animation/AnimationSequencer.cs)