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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 15:54:06 +02:00
parent 1e1a0d9318
commit 1955a0d5f1
2 changed files with 41 additions and 14 deletions

View file

@ -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 = () =>
{

View file

@ -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<uint, WalkFrameStaticRecords> _cellCache = new();
private readonly Dictionary<uint, List<RenderProjectionRecord>> _outdoorByCell = new();
@ -52,11 +54,24 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData
}
/// <summary>Rebuilds the frame's outdoor/shell buckets and clears the
/// per-cell cache. Call once per frame before the driver runs.</summary>
internal void BeginFrame(RenderSceneQuery scene, uint tupleLandblockId)
/// per-cell cache. Call once per frame before the driver runs.
/// <paramref name="renderCenterLbX"/>/<paramref name="renderCenterLbY"/>
/// are the streaming recenter origin: record positions are
/// RENDER-ORIGIN-RELATIVE (each landblock's entities carry
/// <c>(lbX CenterX)·192</c> 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.</summary>
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<RenderProjectionRecord>? bucket))
_outdoorByCell[cellId] = bucket = new List<RenderProjectionRecord>();
bucket.Add(record);
}
}
/// <summary>The landscape cell owning a world position — retail's
/// 24 m cell grid inside the 192 m landblock, the same
/// <c>(lb &amp; 0xFFFF0000) | (cellX*8 + cellY + 1)</c> encoding the walk's
/// landscape turn emits.</summary>
internal static uint LandscapeCellId(Vector3 worldPosition)
/// <summary>The landscape cell owning a RENDER-ORIGIN-RELATIVE position
/// — retail's 24 m cell grid inside the 192 m landblock, producing the
/// same TRUE <c>(lb &amp; 0xFFFF0000) | (cellX*8 + cellY + 1)</c> encoding
/// the walk's landscape turn emits: the relative block index
/// (<c>floor(p/192)</c>) plus the streaming center recovers the true
/// landblock byte, because entity positions carry
/// <c>(lbX CenterX)·192</c> world offsets
/// (<c>LandblockBuildFactory</c>'s <c>worldOffset</c>).</summary>
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);
}