diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index cd65887..4de48ed 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -158,6 +158,14 @@ public sealed class GameWindow : IDisposable private readonly System.Collections.Generic.Dictionary _buildingRegistries = new(); + // Phase A8 (2026-05-28): WB EnvCellRenderManager port. Cells render + // through this dedicated pipeline now, NOT through WbDrawDispatcher. + // The dispatcher's IndoorPass still walks cell-static stabs (WorldEntity + // records with real GfxObj MeshRefs); only the cell GEOMETRY (walls / + // floors / ceilings) flows through here. + private AcDream.App.Rendering.Wb.EnvCellRenderer? _envCellRenderer; + private AcDream.App.Rendering.Wb.WbFrustum? _envCellFrustum; + /// /// Phase 6.4: per-entity animation playback state for entities whose /// MotionTable resolved to a real cycle. The render loop ticks each @@ -1760,6 +1768,14 @@ public sealed class GameWindow : IDisposable _classificationCache); // A.5 T22.5: apply A2C gate from quality preset. _wbDrawDispatcher.AlphaToCoverage = _resolvedQuality.AlphaToCoverage; + + // Phase A8: EnvCellRenderer init. Shares _meshShader (mesh_modern.{vert,frag}) + // with the dispatcher — both consume the same global mesh buffer (VAO/IBO) + // from ObjectMeshManager.GlobalBuffer. + _envCellFrustum = new AcDream.App.Rendering.Wb.WbFrustum(); + _envCellRenderer = new AcDream.App.Rendering.Wb.EnvCellRenderer( + _gl, _wbMeshAdapter!.MeshManager!, _envCellFrustum); + _envCellRenderer.Initialize(_meshShader!); } // Phase G.1 sky renderer — its own shader (sky.vert / sky.frag) @@ -1842,6 +1858,7 @@ public sealed class GameWindow : IDisposable _physicsEngine.RemoveLandblock(id); _cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu); _buildingRegistries.Remove(id); // Phase A8 + _envCellRenderer?.RemoveLandblock(id); // Phase A8 }); // A.5 T22.5: apply max-completions from resolved quality. _streamingController.MaxCompletionsPerFrame = _resolvedQuality.MaxCompletionsPerFrame; @@ -5398,6 +5415,13 @@ public sealed class GameWindow : IDisposable if (environment is not null && environment.Cells.TryGetValue(envCell.CellStructure, out cellStruct)) { + // Phase A8 (2026-05-28): cells render through EnvCellRenderer, NOT as + // WorldEntities with fake MeshRefs. The CellMesh.Build call stays for + // _pendingCellMeshes (still populated for any consumer) and the physics + // data cache (CacheCellStruct uses cellStruct directly, not the sub-meshes). + // Static objects inside the cell continue to flow through the dispatcher + // as WorldEntity records below — they have real GfxObj MeshRefs that work + // fine; EnvCellRenderer.RegisterCell receives an empty staticObjects list. var cellSubMeshes = AcDream.Core.Meshing.CellMesh.Build(envCell, cellStruct, _dats); if (cellSubMeshes.Count > 0) { @@ -5414,23 +5438,23 @@ public sealed class GameWindow : IDisposable System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * System.Numerics.Matrix4x4.CreateTranslation(physicsCellOrigin); - var cellMeshRef = new AcDream.Core.World.MeshRef(envCellId, cellTransform); + // Phase A8: register the cell with EnvCellRenderer for rendering. + // staticObjects is empty — cell stabs continue as separate WorldEntity + // records via the dispatcher (see lines below for the unchanged stab path). + _envCellRenderer?.RegisterCell( + landblockId: landblockId, + envCellId: envCellId, + envCell: envCell, + cellStruct: cellStruct, + cellTransform: cellTransform, + cellWorldPosition: cellOrigin, + cellRotation: envCell.Position.Orientation, + staticObjects: System.Array.Empty<(uint, System.Numerics.Vector3, System.Numerics.Quaternion, bool, System.Numerics.Matrix4x4)>()); - var cellEntity = new AcDream.Core.World.WorldEntity - { - Id = interiorIdBase + localCounter++, - SourceGfxObjOrSetupId = envCellId, - Position = System.Numerics.Vector3.Zero, - Rotation = System.Numerics.Quaternion.Identity, - MeshRefs = new[] { cellMeshRef }, - ParentCellId = envCellId, - }; - result.Add(cellEntity); - - // Step 4: build LoadedCell for portal visibility. + // Step 4: build LoadedCell for portal visibility (UNCHANGED from pre-A8). BuildLoadedCell(envCellId, envCell, cellStruct, cellOrigin, cellTransform); - // Cache CellStruct physics BSP for indoor collision. + // Cache CellStruct physics BSP for indoor collision (UNCHANGED). _physicsDataCache.CacheCellStruct(envCellId, envCell, cellStruct, physicsCellTransform); } } @@ -5854,6 +5878,11 @@ public sealed class GameWindow : IDisposable AcDream.App.Rendering.Wb.BuildingLoader.Build( lbInfo, lb.LandblockId, drainedCells); } + + // Phase A8: finalize EnvCellRenderer's per-landblock instance store. + // Atomically swaps PendingInstances -> committed, computes TotalEnvCellBounds, + // populates StaticPartGroups/BuildingPartGroups via PopulateRecursive. + _envCellRenderer?.FinalizeLandblock(lb.LandblockId); } // N.5: WbMeshAdapter.Tick() handles GPU upload for all GfxObj meshes via @@ -8923,6 +8952,7 @@ public sealed class GameWindow : IDisposable _physicsEngine.RemoveLandblock(id); _cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu); _buildingRegistries.Remove(id); // Phase A8 + _envCellRenderer?.RemoveLandblock(id); // Phase A8 }); _streamingController.MaxCompletionsPerFrame = newResolved.MaxCompletionsPerFrame; @@ -10562,6 +10592,7 @@ public sealed class GameWindow : IDisposable _liveSession = null; _audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context _wbDrawDispatcher?.Dispose(); + _envCellRenderer?.Dispose(); // Phase A8 _skyRenderer?.Dispose(); // depends on sampler cache; dispose first _samplerCache?.Dispose(); _textureCache?.Dispose(); diff --git a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs index ea5781a..ecf2579 100644 --- a/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs @@ -120,6 +120,13 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter /// public AcSurfaceMetadataTable MetadataTable => _metadataTable; + /// + /// Phase A8 (2026-05-28): exposes the underlying + /// so EnvCellRenderer can share the same global mesh buffer (VAO/VBO/IBO). + /// Returns null when the adapter is uninitialized. + /// + public ObjectMeshManager? MeshManager => _meshManager; + /// /// Returns the WB render data for , or null if not /// yet uploaded or if this adapter is uninitialized. Increments WB's