From 1955a0d5f166908b43bb26f71a3d904f7987bc9a Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 30 Aug 2026 15:54:06 +0200 Subject: [PATCH] fix(render) Campaign FW3.2b-2: scenery buckets need the streaming recenter The owner watched the first passing lifecycle run and reported most outdoor scenery not rendering (terrain/buildings/interiors fine, no seams). Root cause: retained-scene record positions are RENDER-ORIGIN- RELATIVE (each landblock carries (lbX - CenterX)*192 offsets), but the walk world data bucketed outdoor statics by treating them as absolute - garbage landblock bytes, so the walk's landscape-cell turns read empty buckets and trees/rocks/fences never drew. LandscapeCellId now adds the frame's RenderCenterLbX/Y back to the relative block index, producing the TRUE cell ids the walk's OnLandscapeCellTurn emits. Buildings and cell statics were unaffected (id-keyed, not position-bucketed). The same run confirmed the campaign thesis live: NO SEAMS at the doorway class under walk order + retail depth, with the lift hack still present (its retirement is FW3.3). Suites: hermetic 6,750/0. Co-Authored-By: Claude Fable 5 --- .../Rendering/RetailPViewRenderer.cs | 6 ++- .../Rendering/Walk/WalkProductionWorldData.cs | 49 ++++++++++++++----- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index fbfb9050..ec49de82 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -1167,7 +1167,11 @@ public sealed class RetailPViewRenderer Walk.WalkCell? cameraCell, Walk.WalkLandscape landscape) { - _walkWorldData!.BeginFrame(_sceneFrameProduct!.SceneQuery, ctx.PlayerLandblockId ?? 0u); + _walkWorldData!.BeginFrame( + _sceneFrameProduct!.SceneQuery, + ctx.PlayerLandblockId ?? 0u, + ctx.RenderCenterLbX, + ctx.RenderCenterLbY); Action clearInteriorDepth = () => { diff --git a/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs b/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs index a9ef6195..cf0eb9cf 100644 --- a/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs +++ b/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs @@ -37,6 +37,8 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData private readonly WalkBuildingRegistry _buildings; private RenderSceneQuery _scene; private uint _tupleLandblockId; + private int _renderCenterLbX; + private int _renderCenterLbY; private readonly Dictionary _cellCache = new(); private readonly Dictionary> _outdoorByCell = new(); @@ -52,11 +54,24 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData } /// Rebuilds the frame's outdoor/shell buckets and clears the - /// per-cell cache. Call once per frame before the driver runs. - internal void BeginFrame(RenderSceneQuery scene, uint tupleLandblockId) + /// per-cell cache. Call once per frame before the driver runs. + /// / + /// are the streaming recenter origin: record positions are + /// RENDER-ORIGIN-RELATIVE (each landblock's entities carry + /// (lbX − CenterX)·192 offsets), so mapping a position back to + /// its TRUE landblock byte needs the center added back — the first + /// connected gate of the FW3.2b-2 cutover shipped without this and most + /// outdoor scenery landed in garbage buckets no walk turn ever reads. + internal void BeginFrame( + RenderSceneQuery scene, + uint tupleLandblockId, + int renderCenterLbX, + int renderCenterLbY) { _scene = scene; _tupleLandblockId = tupleLandblockId; + _renderCenterLbX = renderCenterLbX; + _renderCenterLbY = renderCenterLbY; _cellCache.Clear(); _outdoorMaterialized.Clear(); _shellMaterialized.Clear(); @@ -87,26 +102,34 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData shells.Add(record); continue; } - uint cellId = LandscapeCellId(record.Transform.Position); + uint cellId = LandscapeCellId( + record.Transform.Position, _renderCenterLbX, _renderCenterLbY); if (!_outdoorByCell.TryGetValue(cellId, out List? bucket)) _outdoorByCell[cellId] = bucket = new List(); bucket.Add(record); } } - /// The landscape cell owning a world position — retail's - /// 24 m cell grid inside the 192 m landblock, the same - /// (lb & 0xFFFF0000) | (cellX*8 + cellY + 1) encoding the walk's - /// landscape turn emits. - internal static uint LandscapeCellId(Vector3 worldPosition) + /// The landscape cell owning a RENDER-ORIGIN-RELATIVE position + /// — retail's 24 m cell grid inside the 192 m landblock, producing the + /// same TRUE (lb & 0xFFFF0000) | (cellX*8 + cellY + 1) encoding + /// the walk's landscape turn emits: the relative block index + /// (floor(p/192)) plus the streaming center recovers the true + /// landblock byte, because entity positions carry + /// (lbX − CenterX)·192 world offsets + /// (LandblockBuildFactory's worldOffset). + internal static uint LandscapeCellId( + Vector3 relativePosition, int renderCenterLbX, int renderCenterLbY) { - int lbX = (int)MathF.Floor(worldPosition.X / 192f); - int lbY = (int)MathF.Floor(worldPosition.Y / 192f); - float localX = worldPosition.X - lbX * 192f; - float localY = worldPosition.Y - lbY * 192f; + int relBlockX = (int)MathF.Floor(relativePosition.X / 192f); + int relBlockY = (int)MathF.Floor(relativePosition.Y / 192f); + float localX = relativePosition.X - relBlockX * 192f; + float localY = relativePosition.Y - relBlockY * 192f; int cellX = Math.Clamp((int)(localX / 24f), 0, 7); int cellY = Math.Clamp((int)(localY / 24f), 0, 7); - uint landblock = ((uint)(byte)lbX << 24) | ((uint)(byte)lbY << 16); + uint landblock = + ((uint)(byte)(renderCenterLbX + relBlockX) << 24) + | ((uint)(byte)(renderCenterLbY + relBlockY) << 16); return landblock | (uint)(cellX * 8 + cellY + 1); }