feat(render): Phase A8 Wave 4 — RenderInsideOutAcdream byte-for-byte WB port
Six surgical edits in GameWindow.cs (+275 LOC):
1. _indoorStencilPipeline field + ctor init (line 172 + 1788). Uses
the portal_stencil.{vert,frag} shaders. Disposed at line 10595.
2. Strict cameraInsideBuilding gate (line 7079-7097): visibility.CameraCell
PointInCell + BuildingId != null. camBuildings + otherBuildings lists
populated from _buildingRegistries.GetBuildingsContainingCell / .All().
3. envCellViewProj compute + _envCellFrustum.Update + _envCellRenderer
.PrepareRenderBatches (line 7192) — once per frame, before sky.
4. Frame clear now includes StencilBufferBit (line 6947) so stencil starts
at 0 each frame. RR7 missed this.
5. Old "depth clear when inside" workaround (was lines 7210-7215) DELETED.
Replaced with one-line marker pointing at RenderInsideOutAcdream.
6. Indoor-vs-outdoor branch (line 7284-7298): on cameraInsideBuilding,
call RenderInsideOutAcdream. Otherwise, existing Dispatcher.Draw(set: All).
The outdoor path retains pre-A8 behavior exactly.
7. RenderInsideOutAcdream method (line 10587-10761): byte-for-byte port of
WB VisibilityManager.RenderInsideOut at
references/WorldBuilder/.../VisibilityManager.cs:73-239. Substitutions:
portalManager.RenderBuildingStencilMask -> _indoorStencilPipeline.RenderBuildingStencilMask
envCellManager.Render(pass, filter) -> _envCellRenderer.Render(pass, filter)
terrainManager.Render(...) -> _terrain?.Draw(camera, frustum, neverCullLb)
sceneryManager + staticObjectManager -> _wbDrawDispatcher.Draw(set: OutdoorScenery)
sceneryShader.Bind() -> _meshShader.Use()
Step 1 + 2 (camera-building portals stencil mark + far-depth punch).
Step 3 (cells of camera-buildings, opaque + transparent).
Step 4 (stencil-gated terrain + scenery).
Step 5 (cross-building visibility via 3-bit stencil + occlusion query).
8. Four EmitXxxProbe stub methods (Task 9 fills them with real output).
LiveDynamic (player + NPCs + dropped items) is NOT YET drawn separately;
Task 9 follow-up may add the LiveDynamic dispatch call after stencil
disable. Pre-A8 behavior had no separate LiveDynamic pass either —
dynamic entities flow through Dispatcher.Draw(All) on the outdoor path.
Subagent deviation from spec: `camera` parameter typed as
AcDream.App.Rendering.ICamera (the actual type GameWindow uses) rather
than AcDream.Core.Rendering.Camera (which doesn't exist).
Build green. 82/82 App.Tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4b4f687070
commit
f9a644a366
1 changed files with 275 additions and 12 deletions
|
|
@ -166,6 +166,11 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.Rendering.Wb.EnvCellRenderer? _envCellRenderer;
|
||||
private AcDream.App.Rendering.Wb.WbFrustum? _envCellFrustum;
|
||||
|
||||
// Phase A8 (2026-05-28): portal-stencil pipeline — Steps 1+2+5a-d of WB's
|
||||
// RenderInsideOut algorithm. Lazily-created portal VBO; caller sets GL state
|
||||
// around each RenderBuildingStencilMask call.
|
||||
private AcDream.App.Rendering.IndoorCellStencilPipeline? _indoorStencilPipeline;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 6.4: per-entity animation playback state for entities whose
|
||||
/// MotionTable resolved to a real cycle. The render loop ticks each
|
||||
|
|
@ -1776,6 +1781,14 @@ public sealed class GameWindow : IDisposable
|
|||
_envCellRenderer = new AcDream.App.Rendering.Wb.EnvCellRenderer(
|
||||
_gl, _wbMeshAdapter!.MeshManager!, _envCellFrustum);
|
||||
_envCellRenderer.Initialize(_meshShader!);
|
||||
|
||||
// Phase A8 (2026-05-28): portal-stencil pipeline. Uses its own dedicated
|
||||
// shader (portal_stencil.vert / portal_stencil.frag) that only writes
|
||||
// stencil and optionally gl_FragDepth = 1.0 (far-depth punch semantic).
|
||||
_indoorStencilPipeline = new AcDream.App.Rendering.IndoorCellStencilPipeline(
|
||||
_gl,
|
||||
Path.Combine(shadersDir, "portal_stencil.vert"),
|
||||
Path.Combine(shadersDir, "portal_stencil.frag"));
|
||||
}
|
||||
|
||||
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
||||
|
|
@ -6944,7 +6957,7 @@ public sealed class GameWindow : IDisposable
|
|||
System.Math.Clamp(fogColor.Z, 0f, 1f),
|
||||
1f);
|
||||
|
||||
_gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
||||
_gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit);
|
||||
|
||||
// Phase N.6 slice 1: one-shot surface-format histogram dump under
|
||||
// ACDREAM_DUMP_SURFACES=1. Zero cost when off.
|
||||
|
|
@ -7058,6 +7071,35 @@ public sealed class GameWindow : IDisposable
|
|||
var visibility = _cellVisibility.ComputeVisibility(camPos);
|
||||
bool cameraInsideCell = visibility?.CameraCell is not null;
|
||||
|
||||
// Phase A8 (2026-05-28): strict cameraInsideBuilding gate. Requires the
|
||||
// camera cell to actually be a Building (BuildingId != null per RR4). The
|
||||
// existing cameraInsideCell flag stays — it's still used by the sky /
|
||||
// weather suppression code paths which gate on "any indoor cell" (including
|
||||
// dungeon cells without BuildingId).
|
||||
bool cameraInsideBuilding =
|
||||
visibility?.CameraCell is not null
|
||||
&& AcDream.App.Rendering.CellVisibility.PointInCell(camPos, visibility.CameraCell)
|
||||
&& visibility.CameraCell.BuildingId is not null;
|
||||
|
||||
var camBuildings = new System.Collections.Generic.List<AcDream.App.Rendering.Wb.Building>();
|
||||
var otherBuildings = new System.Collections.Generic.List<AcDream.App.Rendering.Wb.Building>();
|
||||
|
||||
if (cameraInsideBuilding)
|
||||
{
|
||||
uint lbId = visibility!.CameraCell!.CellId & 0xFFFF0000u;
|
||||
if (_buildingRegistries.TryGetValue(lbId, out var reg))
|
||||
{
|
||||
foreach (var b in reg.GetBuildingsContainingCell(visibility.CameraCell.CellId))
|
||||
camBuildings.Add(b);
|
||||
}
|
||||
|
||||
var camCellId = visibility!.CameraCell!.CellId;
|
||||
foreach (var rr in _buildingRegistries.Values)
|
||||
foreach (var b in rr.All())
|
||||
if (!b.EnvCellIds.Contains(camCellId))
|
||||
otherBuildings.Add(b);
|
||||
}
|
||||
|
||||
// SPIKE 2026-05-26: A8 transition investigation. Lights up the
|
||||
// dormant RenderingDiagnostics.ProbeVisibilityEnabled flag (added
|
||||
// by Task 6 of the original A8 plan). Per-frame state captures:
|
||||
|
|
@ -7160,6 +7202,12 @@ public sealed class GameWindow : IDisposable
|
|||
playerLb = (uint)((plx << 24) | (ply << 16) | 0xFFFF);
|
||||
}
|
||||
|
||||
// Phase A8: prepare EnvCellRenderer's per-frame visibility snapshot.
|
||||
// Always called — cheap when no cells loaded, cheap when frustum culls all.
|
||||
var envCellViewProj = camera.View * camera.Projection;
|
||||
_envCellFrustum?.Update(envCellViewProj);
|
||||
_envCellRenderer?.PrepareRenderBatches(envCellViewProj, camPos);
|
||||
|
||||
// Phase G.1: sky renderer — draws the far-plane-infinity
|
||||
// celestial meshes FIRST so the rest of the scene z-tests
|
||||
// on top of them (depth mask off, no depth writes). Skipped
|
||||
|
|
@ -7207,12 +7255,9 @@ public sealed class GameWindow : IDisposable
|
|||
_terrainCpuSampleCursor = (_terrainCpuSampleCursor + 1) % _terrainCpuSamples.Length;
|
||||
MaybeFlushTerrainDiag();
|
||||
|
||||
// Conditional depth clear: when camera is inside a building, clear
|
||||
// depth (not color) so interior geometry writes fresh Z values on top
|
||||
// of the terrain color buffer. Exit portals show outdoor terrain color
|
||||
// because we kept the color buffer. Matching ACME GameScene.cs pattern.
|
||||
if (cameraInsideCell)
|
||||
_gl!.Clear(ClearBufferMask.DepthBufferBit);
|
||||
// Phase A8 (2026-05-28): the pre-A8 "depth clear when inside" workaround
|
||||
// is deleted. RenderInsideOutAcdream below handles indoor visibility via
|
||||
// stencil-gated portals instead.
|
||||
|
||||
// L-fix1 (2026-04-28): pass the set of animated-entity ids so
|
||||
// the renderer keeps remote players / NPCs / monsters
|
||||
|
|
@ -7230,11 +7275,27 @@ public sealed class GameWindow : IDisposable
|
|||
animatedIds.Add(k);
|
||||
}
|
||||
|
||||
// N.5: WbDrawDispatcher is always non-null (modern path mandatory).
|
||||
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||
neverCullLandblockId: playerLb,
|
||||
visibleCellIds: visibility?.VisibleCellIds,
|
||||
animatedEntityIds: animatedIds);
|
||||
// Phase A8 (2026-05-28): indoor-vs-outdoor render branch. The outdoor
|
||||
// branch keeps pre-A8 behavior (single Draw(set: All)). The indoor branch
|
||||
// runs WB's RenderInsideOut algorithm byte-for-byte via RenderInsideOutAcdream.
|
||||
// LiveDynamic (player + NPCs + dropped items) is drawn last in BOTH branches
|
||||
// after stencil is disabled, so dynamic entities depth-test against everything
|
||||
// but aren't stencil-clipped.
|
||||
if (cameraInsideBuilding)
|
||||
{
|
||||
RenderInsideOutAcdream(envCellViewProj, camPos, visibility!.CameraCell!,
|
||||
camBuildings, otherBuildings,
|
||||
camera, frustum, playerLb, animatedIds,
|
||||
visibility?.VisibleCellIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
// N.5: WbDrawDispatcher is always non-null (modern path mandatory).
|
||||
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||
neverCullLandblockId: playerLb,
|
||||
visibleCellIds: visibility?.VisibleCellIds,
|
||||
animatedEntityIds: animatedIds);
|
||||
}
|
||||
|
||||
// Phase G.1 / E.3: draw all live particles after opaque
|
||||
// scene geometry so alpha blending composites correctly.
|
||||
|
|
@ -10509,6 +10570,207 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// ── Phase A8 (2026-05-28): RenderInsideOutAcdream + probe stubs ────────────
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 (2026-05-28): port of WB's <c>VisibilityManager.RenderInsideOut</c>
|
||||
/// at <c>references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs:73-239</c>.
|
||||
/// Byte-for-byte port with these substitutions:
|
||||
/// portalManager.RenderBuildingStencilMask → _indoorStencilPipeline.RenderBuildingStencilMask
|
||||
/// envCellManager.Render(pass, filter) → _envCellRenderer.Render(pass, filter)
|
||||
/// terrainManager.Render(...) → _terrain?.Draw(camera, frustum, neverCullLandblockId)
|
||||
/// sceneryManager + staticObjectManager → folded into _wbDrawDispatcher.Draw(set: OutdoorScenery)
|
||||
/// sceneryShader.Bind() → _meshShader.Use()
|
||||
/// state.ShowScenery / ShowStaticObjects → always true (game client; no editor toggles)
|
||||
/// state.EnableTransparencyPass → true (we want translucent geometry)
|
||||
/// </summary>
|
||||
private void RenderInsideOutAcdream(
|
||||
System.Numerics.Matrix4x4 viewProj,
|
||||
System.Numerics.Vector3 camPos,
|
||||
AcDream.App.Rendering.LoadedCell cameraCell,
|
||||
System.Collections.Generic.List<AcDream.App.Rendering.Wb.Building> camBuildings,
|
||||
System.Collections.Generic.List<AcDream.App.Rendering.Wb.Building> otherBuildings,
|
||||
AcDream.App.Rendering.ICamera camera,
|
||||
AcDream.App.Rendering.FrustumPlanes? frustum,
|
||||
uint? playerLb,
|
||||
System.Collections.Generic.HashSet<uint>? animatedIds,
|
||||
System.Collections.Generic.HashSet<uint>? visibleCellIds)
|
||||
{
|
||||
var gl = _gl!;
|
||||
bool didInsideStencil = false;
|
||||
|
||||
EmitBuildingsProbe(visibilityCellId: cameraCell.CellId, camBuildings, otherBuildings);
|
||||
|
||||
// Steps 1+2: stencil bit 1 + far-depth punch at camera-buildings' portals.
|
||||
if (camBuildings.Count > 0)
|
||||
{
|
||||
didInsideStencil = true;
|
||||
gl.Enable(EnableCap.StencilTest);
|
||||
gl.ClearStencil(0);
|
||||
gl.Clear(ClearBufferMask.StencilBufferBit);
|
||||
|
||||
// Step 1: stencil bit 1 at our buildings' portals.
|
||||
// WB VisibilityManager.cs:86-94
|
||||
gl.Disable(EnableCap.CullFace);
|
||||
gl.StencilFunc(StencilFunction.Always, 1, 0xFFu);
|
||||
gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace);
|
||||
gl.StencilMask(0x01u);
|
||||
gl.ColorMask(false, false, false, false);
|
||||
gl.DepthMask(false);
|
||||
gl.Enable(EnableCap.DepthTest);
|
||||
gl.DepthFunc(DepthFunction.Always);
|
||||
|
||||
EmitDrawOrderProbe(step: 1, sub: ' ');
|
||||
foreach (var b in camBuildings)
|
||||
{
|
||||
_indoorStencilPipeline!.RenderBuildingStencilMask(b, viewProj, writeFarDepth: false);
|
||||
EmitStencilProbe(op: "mark");
|
||||
}
|
||||
|
||||
// Step 2: punch depth at portals.
|
||||
// WB VisibilityManager.cs:99-104
|
||||
gl.DepthMask(true);
|
||||
gl.DepthFunc(DepthFunction.Always);
|
||||
|
||||
EmitDrawOrderProbe(step: 2, sub: ' ');
|
||||
foreach (var b in camBuildings)
|
||||
{
|
||||
_indoorStencilPipeline!.RenderBuildingStencilMask(b, viewProj, writeFarDepth: true);
|
||||
EmitStencilProbe(op: "punch");
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: render camera-buildings' cells (stencil off, DepthFunc.Less).
|
||||
// WB VisibilityManager.cs:107-127
|
||||
gl.ColorMask(true, true, true, false);
|
||||
gl.DepthMask(true);
|
||||
gl.Disable(EnableCap.StencilTest);
|
||||
gl.DepthFunc(DepthFunction.Less);
|
||||
_meshShader!.Use();
|
||||
|
||||
EmitDrawOrderProbe(step: 3, sub: ' ');
|
||||
var currentEnvCellIds = new System.Collections.Generic.HashSet<uint>();
|
||||
if (camBuildings.Count > 0)
|
||||
{
|
||||
foreach (var b in camBuildings)
|
||||
foreach (var id in b.EnvCellIds) currentEnvCellIds.Add(id);
|
||||
_envCellRenderer!.Render(AcDream.App.Rendering.Wb.WbRenderPass.Opaque, currentEnvCellIds);
|
||||
|
||||
// Transparency pass.
|
||||
gl.DepthMask(false);
|
||||
_envCellRenderer!.Render(AcDream.App.Rendering.Wb.WbRenderPass.Transparent, currentEnvCellIds);
|
||||
gl.DepthMask(true);
|
||||
}
|
||||
|
||||
EmitEnvCellProbe(camBuildings.Count, otherBuildings.Count, currentEnvCellIds.Count);
|
||||
|
||||
// Step 4: stencil-gated outdoor (terrain + scenery + static objects).
|
||||
// WB VisibilityManager.cs:130-154
|
||||
if (didInsideStencil)
|
||||
{
|
||||
gl.Enable(EnableCap.StencilTest);
|
||||
gl.StencilFunc(StencilFunction.Equal, 1, 0x01u);
|
||||
gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Keep);
|
||||
gl.StencilMask(0x00u);
|
||||
gl.ColorMask(true, true, true, false);
|
||||
gl.DepthMask(true);
|
||||
gl.Enable(EnableCap.CullFace);
|
||||
gl.DepthFunc(DepthFunction.Less);
|
||||
}
|
||||
|
||||
EmitDrawOrderProbe(step: 4, sub: ' ');
|
||||
// Terrain (WB line 143).
|
||||
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
||||
|
||||
_meshShader!.Use();
|
||||
// Scenery + static objects via dispatcher (WB lines 148-154).
|
||||
_wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||
neverCullLandblockId: playerLb,
|
||||
visibleCellIds: visibleCellIds, // OK — outdoor cells outside the building
|
||||
animatedEntityIds: animatedIds,
|
||||
set: AcDream.App.Rendering.Wb.WbDrawDispatcher.EntitySet.OutdoorScenery);
|
||||
|
||||
// Step 5: per-other-building 3-bit stencil pipeline.
|
||||
// WB VisibilityManager.cs:157-232
|
||||
if (didInsideStencil && otherBuildings.Count > 0)
|
||||
{
|
||||
gl.Enable(EnableCap.StencilTest);
|
||||
gl.ColorMask(false, false, false, false);
|
||||
gl.DepthMask(false);
|
||||
gl.DepthFunc(DepthFunction.Lequal);
|
||||
|
||||
foreach (var b in otherBuildings)
|
||||
{
|
||||
// Occlusion-query read-back (prev-frame async).
|
||||
_indoorStencilPipeline!.EnsureOcclusionQueryId(ref b.QueryId);
|
||||
if (b.QueryStarted &&
|
||||
_indoorStencilPipeline.TryReadOcclusionResult(b.QueryId, out bool anyPassed))
|
||||
{
|
||||
b.WasVisible = anyPassed;
|
||||
}
|
||||
_indoorStencilPipeline.BeginOcclusionQuery(b.QueryId);
|
||||
b.QueryStarted = true;
|
||||
|
||||
// Step 5a: mark bit 2 (Ref=3, Mask=0x02). WB VisibilityManager.cs:186-192
|
||||
EmitDrawOrderProbe(step: 5, sub: 'a');
|
||||
gl.StencilFunc(StencilFunction.Equal, 3, 0x01u);
|
||||
gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace);
|
||||
gl.StencilMask(0x02u);
|
||||
gl.Disable(EnableCap.CullFace);
|
||||
_indoorStencilPipeline.RenderBuildingStencilMask(b, viewProj, writeFarDepth: false);
|
||||
_indoorStencilPipeline.EndOcclusionQuery();
|
||||
|
||||
// Step 5b: clear depth at stencil==3. WB VisibilityManager.cs:201-205
|
||||
EmitDrawOrderProbe(step: 5, sub: 'b');
|
||||
gl.StencilFunc(StencilFunction.Equal, 3, 0x03u);
|
||||
gl.StencilMask(0x00u);
|
||||
gl.DepthMask(true);
|
||||
gl.DepthFunc(DepthFunction.Always);
|
||||
_indoorStencilPipeline.RenderBuildingStencilMask(b, viewProj, writeFarDepth: true);
|
||||
|
||||
// Step 5c: render this building's cells where stencil==3. WB VisibilityManager.cs:210-220
|
||||
EmitDrawOrderProbe(step: 5, sub: 'c');
|
||||
gl.ColorMask(true, true, true, false);
|
||||
gl.DepthFunc(DepthFunction.Less);
|
||||
gl.Enable(EnableCap.CullFace);
|
||||
_meshShader!.Use();
|
||||
_envCellRenderer!.Render(AcDream.App.Rendering.Wb.WbRenderPass.Opaque, b.EnvCellIds);
|
||||
gl.DepthMask(false);
|
||||
_envCellRenderer!.Render(AcDream.App.Rendering.Wb.WbRenderPass.Transparent, b.EnvCellIds);
|
||||
gl.DepthMask(true);
|
||||
|
||||
// Step 5d: reset bit 2 (Ref=1, Mask=0x02) for next iteration. WB VisibilityManager.cs:222-228
|
||||
EmitDrawOrderProbe(step: 5, sub: 'd');
|
||||
gl.ColorMask(false, false, false, false);
|
||||
gl.DepthMask(false);
|
||||
gl.StencilMask(0x02u);
|
||||
gl.StencilFunc(StencilFunction.Always, 1, 0x02u);
|
||||
gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace);
|
||||
_indoorStencilPipeline.RenderBuildingStencilMask(b, viewProj, writeFarDepth: false);
|
||||
}
|
||||
gl.DepthFunc(DepthFunction.Less);
|
||||
}
|
||||
|
||||
// Cleanup (WB VisibilityManager.cs:234-238).
|
||||
if (didInsideStencil)
|
||||
{
|
||||
gl.Disable(EnableCap.StencilTest);
|
||||
gl.StencilMask(0xFFu);
|
||||
gl.ColorMask(true, true, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase A8 probe stubs (Task 9 wires these up with real output) ────────
|
||||
|
||||
private void EmitDrawOrderProbe(int step, char sub) { /* Task 9 */ }
|
||||
private void EmitEnvCellProbe(int ourBldgs, int otherBldgs, int filterCnt) { /* Task 9 */ }
|
||||
private void EmitStencilProbe(string op) { /* Task 9 */ }
|
||||
private void EmitBuildingsProbe(uint visibilityCellId,
|
||||
System.Collections.Generic.List<AcDream.App.Rendering.Wb.Building> camBldgs,
|
||||
System.Collections.Generic.List<AcDream.App.Rendering.Wb.Building> otherBldgs) { /* Task 9 */ }
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Phase N.5b: emits [TERRAIN-DIAG] once per ~5s under
|
||||
/// ACDREAM_WB_DIAG=1. Mirrors <c>WbDrawDispatcher.MaybeFlushDiag</c>:
|
||||
/// rolling 256-sample buffer of microseconds, median + p95 reported.
|
||||
|
|
@ -10593,6 +10855,7 @@ public sealed class GameWindow : IDisposable
|
|||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||
_wbDrawDispatcher?.Dispose();
|
||||
_envCellRenderer?.Dispose(); // Phase A8
|
||||
_indoorStencilPipeline?.Dispose(); // Phase A8 (2026-05-28)
|
||||
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
||||
_samplerCache?.Dispose();
|
||||
_textureCache?.Dispose();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue