fix(render): skip empty groups in instanced draw to prevent crash on Tab

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-13 18:55:29 +02:00
parent 6a55838a10
commit 787e0f0aff
2 changed files with 98 additions and 0 deletions

View file

@ -0,0 +1,96 @@
# Rendering Rebuild from ACME
Port ACME's rendering pipeline to acdream. Each step produces a
visually testable result. The animation system stays unchanged (ACME
has none — ours is ported from the decompiled client).
## Step 1: Port StaticObject shader + instanced rendering
The biggest performance win. Replace per-entity DrawElements with
instanced rendering using a shared instance VBO.
**From ACME:**
- StaticObject.vert: aInstanceMatrix (mat4 at locations 3-6, divisor=1)
- StaticObject.frag: sampler2DArray + alpha cutout
- Instance buffer pattern: single float[] upload per frame
**Changes:**
- New shader: mesh_instanced.vert/frag (from ACME StaticObject.vert/frag)
- Rewrite StaticMeshRenderer to use instance buffer pattern
- Group entities by (GfxObjId, textureAtlas) → one DrawElementsInstanced per group
- Per-GfxObj TextureAtlasManager (grow-on-demand, starts at 32 slots)
- ushort indices (not uint32) for objects
**Test:** Same visual output, fewer draw calls (check perf overlay).
## Step 2: Port Landscape shader + terrain chunks
Replace per-landblock terrain draws with chunk batching.
**From ACME:**
- Landscape.vert/frag: 8 packed uvec4 attributes, sampler2DArray terrain + alpha
- TerrainChunk: N×N landblocks baked into one VBO/IBO
- TerrainGPUResourceManager: buffer creation + partial updates
**Changes:**
- New shader: terrain_acme.vert/frag (from ACME Landscape.vert/frag)
- New TerrainChunkRenderer (replaces TerrainRenderer)
- LandblockMesh.Build outputs VertexLandscape-compatible structs
- Single DrawElements per chunk (multiple landblocks)
**Test:** Same terrain appearance, one draw call per chunk.
## Step 3: Port AdjustPlanes lighting
Replace guessed sun direction with decompiled retail values.
**From decompiled:**
- FUN_00532440 (AdjustPlanes): face-normal accumulation + per-vertex lighting
- DAT constants: sun direction, ambient, diffuse
**Changes:**
- LandblockMesh: face-normal accumulation (replaces central differences)
- Shader uniforms: xLightDirection, xAmbient from decompiled constants
- Static objects: same lighting model
**Test:** Side-by-side with retail client shows matching lighting.
## Step 4: Port EnvCell portal visibility
Render only visible interior cells.
**From ACME:**
- EnvCellManager: portal visibility BFS from camera cell
- Portal occluder pass: depth-only draw of portal polygons
- Conditional depth clear when camera is inside a cell
**Changes:**
- New CellVisibility system (BFS through CellPortals)
- Portal occluder depth pass before EnvCell geometry
- Conditional depth clear in render order
**Test:** Enter a building — only visible rooms render.
## Step 5: Wire animation into instanced pipeline
The AnimationSequencer outputs per-part transforms. These need to flow
into the instance buffer alongside static transforms.
**Changes:**
- Animated entities write their per-part instance matrices into the
shared instance buffer every frame (same buffer as static objects)
- AnimationSequencer.Advance(dt) is called BEFORE the instance buffer
upload so the latest frame's transforms are included
**Test:** NPCs breathe, player walks — all through the instanced pipeline.
## Render Order (target)
```
1. Terrain (one DrawElements per chunk, PolygonOffset on)
2. Conditional depth clear (if camera inside EnvCell)
3. EnvCell geometry (DrawElementsInstanced, portal visibility culled)
4. Static objects opaque (DrawElementsInstanced, alpha cutout)
5. Static objects translucent (DrawElementsInstanced, blend on)
6. Particles (additive blend — future)
```

View file

@ -227,6 +227,7 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
// group share the same GfxObj so they have compatible overrides
// only in the degenerate case of mixed-palette entities using the
// same GfxObj — rare enough to accept the approximation here).
if (grp.Count == 0) continue;
var firstEntry = grp.Entries[0];
uint tex = ResolveTex(firstEntry.Entity, firstEntry.MeshRef, sub);
_gl.ActiveTexture(TextureUnit.Texture0);
@ -295,6 +296,7 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable
false, 64, (void*)(byteOffset + row * 16));
}
if (grp.Count == 0) continue;
var firstEntry = grp.Entries[0];
uint tex = ResolveTex(firstEntry.Entity, firstEntry.MeshRef, sub);
_gl.ActiveTexture(TextureUnit.Texture0);