From 6a7894ac354f0f745327095fdd0051b26b9e7e17 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 27 May 2026 11:22:15 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20Phase=20A8=20RR6=20=E2=80=94=20?= =?UTF-8?q?IndoorCellStencilPipeline=203-bit=20+=20occlusion-query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the dormant single-bit stencil pipeline with WB Step 5 primitives: MarkBuildingBit2 — mark stencil bit 2 where bit 1 set PunchDepthAtStencil3 — depth=1.0 at intersection (stencil==3) EnableOtherBuildingPass — render state for stencil==3 EnvCell pass ResetBit2 — clear bit 2 between iterations UploadBuildingPortalMesh — upload a Building.ExitPortalPolygons (vs cell-based UploadPortalMesh) Plus occlusion-query helpers: EnsureOcclusionQueryId — lazy GenQuery TryReadOcclusionResult — asynchronous read-back (no CPU stall) BeginOcclusionQuery — BeginQuery wrapper EndOcclusionQuery — EndQuery wrapper All GL state sequences mirror WB VisibilityManager.cs:73-239 line-by-line. Comments reference the corresponding WB line numbers for verification. Consumed by RR7's Steps 1-4 + RR9's Step 5. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Rendering/IndoorCellStencilPipeline.cs | 243 ++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs b/src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs index 605309d..98ff91e 100644 --- a/src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs +++ b/src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs @@ -221,6 +221,249 @@ public sealed unsafe class IndoorCellStencilPipeline : IDisposable _gl.StencilMask(0xFFu); // restore default write mask } + // ------------------------------------------------------------------------- + // Phase A8 RR6 (2026-05-26): Step 5 — 3-bit stencil mode + occlusion-query + // helpers. These methods are called by RR7 (Steps 1-4 wire-in) and RR9 + // (Step 5 cross-building pass). They assume the VBO was already uploaded + // by UploadBuildingPortalMesh for the specific building being processed. + // ------------------------------------------------------------------------- + + /// + /// Phase A8 RR6: Step 5a — mark stencil bit 2 at portal silhouettes WHERE + /// bit 1 is already set. After this call, pixels at the intersection of our + /// building's portals (bit 1) and the other building's portals (bit 2) will + /// have stencil == 3. Subsequent renders + /// into those pixels only. + /// + /// GL state on entry: stencil test enabled (left by prior MarkAndPunch + /// or EnableOutdoorPass); depth test on. DepthFunc.Lequal set here. + /// + /// Mirrors WB VisibilityManager.cs:186-189. + /// + public void MarkBuildingBit2(Matrix4x4 viewProjection, int buildingPortalVertexCount) + { + // WB:186 StencilFunc.Equal(3, 0x01) — match where bit 1 is set + // WB:187 StencilOp(Keep, Keep, Replace) + // WB:188 StencilMask 0x02 — only write to bit 2 + // WB:189 ColorMask off; DepthMask off; DepthFunc.Lequal; Disable CullFace + _gl.StencilFunc(StencilFunction.Equal, 3, 0x01u); + _gl.StencilOp(StencilOp.Keep, StencilOp.Keep, StencilOp.Replace); + _gl.StencilMask(0x02u); + _gl.ColorMask(false, false, false, false); + _gl.DepthMask(false); + _gl.Enable(EnableCap.DepthTest); + _gl.DepthFunc(DepthFunction.Lequal); + _gl.Disable(EnableCap.CullFace); + + _shader.Use(); + var vp = viewProjection; + _gl.UniformMatrix4(_uViewProjectionLoc, 1, false, (float*)&vp); + _gl.Uniform1(_uWriteFarDepthLoc, 0); // color/depth writes off anyway + + _gl.BindVertexArray(_vao); + _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)buildingPortalVertexCount); + _gl.BindVertexArray(0); + } + + /// + /// Phase A8 RR6: Step 5b — punch depth=1.0 where stencil == 3 (intersection + /// of our portal silhouette and the other building's portal silhouette). Clears + /// interior-wall depth written during Step 3 so the other building's cells win + /// depth when rendered in . + /// + /// GL state on entry: stencil enabled; StencilMask 0x02 from MarkBuildingBit2. + /// This method sets StencilMask 0x00 (read-only), DepthMask on, DepthFunc.Always. + /// + /// Mirrors WB VisibilityManager.cs:201-205. + /// + public void PunchDepthAtStencil3(Matrix4x4 viewProjection, int buildingPortalVertexCount) + { + // WB:201 StencilFunc.Equal(3, 0x03) — match intersection pixels + // WB:202 StencilMask 0x00 — read-only stencil + // WB:203 DepthMask on; DepthFunc.Always + // WB:204-205 draw portal triangles with uWriteFarDepth=1 + _gl.StencilFunc(StencilFunction.Equal, 3, 0x03u); + _gl.StencilMask(0x00u); + _gl.DepthMask(true); + _gl.DepthFunc(DepthFunction.Always); + + _shader.Use(); + var vp = viewProjection; + _gl.UniformMatrix4(_uViewProjectionLoc, 1, false, (float*)&vp); + _gl.Uniform1(_uWriteFarDepthLoc, 1); // write gl_FragDepth = 1.0 + + _gl.BindVertexArray(_vao); + _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)buildingPortalVertexCount); + _gl.BindVertexArray(0); + } + + /// + /// Phase A8 RR6: Step 5c — set render state to draw the other-building's + /// EnvCells where stencil == 3. Does not issue draw calls; caller renders + /// the cells immediately after. + /// + /// GL state on entry: stencil func still Equal(3, 0x03) from + /// PunchDepthAtStencil3. This method re-enables color and CullFace, sets + /// DepthFunc.Less for the geometry pass. + /// + /// Mirrors WB VisibilityManager.cs:210-212. + /// + public void EnableOtherBuildingPass() + { + // WB:210 ColorMask true; WB:211 DepthFunc.Less; WB:212 Enable CullFace + // Stencil func stays Equal(3, 0x03) from PunchDepthAtStencil3. + _gl.ColorMask(true, true, true, false); + _gl.DepthFunc(DepthFunction.Less); + _gl.Enable(EnableCap.CullFace); + } + + /// + /// Phase A8 RR6: Step 5d — reset bit 2 to zero so the next other-building + /// iteration starts fresh. Re-draws this building's portal triangles with a + /// ref value of 1 (bit 2 = 0) to overwrite bit 2 back to 0 in stencil. + /// + /// GL state on entry: color/depth writes may still be on from + /// EnableOtherBuildingPass. This method disables them and rewrites bit 2. + /// + /// Mirrors WB VisibilityManager.cs:222-228. + /// + public void ResetBit2(Matrix4x4 viewProjection, int buildingPortalVertexCount) + { + // WB:222 ColorMask off; DepthMask off + // WB:223 StencilMask 0x02 + // WB:224 StencilFunc.Always(1, 0x02) → ref=1 AND mask=0x02 → writes 0 to bit 2 + // because ref & writeMask = 1 & 0x02 = 0 (bit 2 of 1 is 0) + // WB:225-226 StencilOp Replace + // WB:227-228 draw portal triangles + _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); + + _shader.Use(); + var vp = viewProjection; + _gl.UniformMatrix4(_uViewProjectionLoc, 1, false, (float*)&vp); + _gl.Uniform1(_uWriteFarDepthLoc, 0); + + _gl.BindVertexArray(_vao); + _gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)buildingPortalVertexCount); + _gl.BindVertexArray(0); + } + + // ------------------------------------------------------------------------- + // Occlusion-query helpers (Phase A8 RR6). Wrap raw GL query calls with + // the same asynchronous non-stalling pattern used by WB VisibilityManager + // (lines 173-184 and 267-287). All three Building.QueryId/QueryStarted/ + // WasVisible fields are written by the caller (RR9) around these helpers. + // ------------------------------------------------------------------------- + + /// + /// Phase A8 RR6: lazily allocate a GL query object handle. The + /// is written on first call and reused thereafter. Callers pass + /// ref building.QueryId. Mirrors WB VisibilityManager.cs:173 + /// (if (building.QueryId != 0) guard pattern — we create the id before + /// that guard is first needed). + /// + public uint EnsureOcclusionQueryId(ref uint slot) + { + if (slot == 0) slot = _gl.GenQuery(); + return slot; + } + + /// + /// Phase A8 RR6: non-blocking read of the previous frame's occlusion query + /// result. Returns false immediately if the result is not yet available (no + /// CPU stall). Sets to true if at least one + /// sample passed. + /// + /// Mirrors WB VisibilityManager.cs:174-180 and 272-278. + /// + public bool TryReadOcclusionResult(uint queryId, out bool anyPassed) + { + anyPassed = false; + if (queryId == 0) return false; + _gl.GetQueryObject(queryId, QueryObjectParameterName.ResultAvailable, out int available); + if (available == 0) return false; + _gl.GetQueryObject(queryId, QueryObjectParameterName.Result, out int samplesPassed); + anyPassed = samplesPassed > 0; + return true; + } + + /// + /// Phase A8 RR6: begin a samples-passed occlusion query. Caller is responsible + /// for setting building.QueryStarted = true afterward. + /// Mirrors WB VisibilityManager.cs:182 and 279. + /// + public void BeginOcclusionQuery(uint queryId) => + _gl.BeginQuery(QueryTarget.SamplesPassed, queryId); + + /// + /// Phase A8 RR6: end a samples-passed occlusion query. + /// Mirrors WB VisibilityManager.cs:195 and 286. + /// + public void EndOcclusionQuery() => + _gl.EndQuery(QueryTarget.SamplesPassed); + + // ------------------------------------------------------------------------- + // Per-building portal mesh upload (Phase A8 RR6 — S3). + // ------------------------------------------------------------------------- + + /// + /// Phase A8 RR6: upload a Building's pre-computed world-space exit portal + /// polygons as a triangle fan. Mirrors but + /// operates on + /// instead of per-cell portal lists. Called once per other-building per frame + /// (Step 5 loop in RR9) before the MarkBuildingBit2 / PunchDepthAtStencil3 / + /// EnableOtherBuildingPass / ResetBit2 sequence. + /// + /// Vertex count uploaded (always a multiple of 3; 0 if no polygons). + public int UploadBuildingPortalMesh(AcDream.App.Rendering.Wb.Building building) + { + // Pre-count vertices so we allocate exactly. + int triVertCount = 0; + foreach (var poly in building.ExitPortalPolygons) + { + if (poly.Length < 3) continue; + triVertCount += (poly.Length - 2) * 3; + } + + if (triVertCount == 0) + { + _lastVertexCount = 0; + return 0; + } + + if (triVertCount > _vboCapacityVerts) + AllocateVbo(Math.Max(triVertCount * 2, 1024)); + + // Triangle-fan triangulation — matches UploadPortalMesh / WB PortalRenderManager.cs:537-543. + var verts = new Vector3[triVertCount]; + int idx = 0; + foreach (var poly in building.ExitPortalPolygons) + { + if (poly.Length < 3) continue; + var v0 = poly[0]; + for (int i = 1; i < poly.Length - 1; i++) + { + verts[idx++] = v0; + verts[idx++] = poly[i]; + verts[idx++] = poly[i + 1]; + } + } + + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo); + fixed (Vector3* p = verts) + { + _gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, + (nuint)(idx * sizeof(Vector3)), p); + } + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); + + _lastVertexCount = idx; + return idx; + } + public void Dispose() { _shader.Dispose();