fix #451: stabilize portal seam rendering
All checks were successful
CI / linux-portable (push) Successful in 3m32s
CI / windows-gate (push) Successful in 6m55s
CI / release (push) Successful in 2m12s

This commit is contained in:
Erik 2026-08-27 14:30:21 +02:00
parent f6fe0f2a4f
commit 1d2f2f738f
29 changed files with 1650 additions and 239 deletions

View file

@ -134,6 +134,8 @@ internal sealed class RetailPViewPassExecutor :
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
private readonly RetailPViewParticleClassifications _particleClassifications = new();
private readonly HashSet<uint> _noSceneParticleEntityIds = [];
private readonly Dictionary<uint, int> _singleCellClipRouting = new(1);
private readonly Dictionary<uint, int> _noCellClipRouting = new(0);
/// <summary>
/// Borrowed until the next late landscape pass. The outdoor-root post-world
@ -207,6 +209,8 @@ internal sealed class RetailPViewPassExecutor :
{
List<Exception>? failures = null;
TryAbort(_frameGlState.RestoreFrameDefaults);
TryAbort(() => _envCells.SetClipRouting(null));
TryAbort(_entities.ClearClipRouting);
TryAbort(_particleClassifications.BeginFrame);
TryAbort(_noSceneParticleEntityIds.Clear);
if (failures is { Count: > 0 })
@ -230,6 +234,11 @@ internal sealed class RetailPViewPassExecutor :
ClipFrameAssembly reuseAssembly) =>
ClipFrameAssembler.Assemble(_clipFrame, portalFrame, reuseAssembly);
public void AppendLookInClipFrames(
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
ClipFrameAssembly assembly) =>
ClipFrameAssembler.AppendLookInFrames(_clipFrame, lookInFrames, assembly);
public void PrepareClipFrame(int terrainUploadCount) =>
_surface.PrepareClipFrame(terrainUploadCount);
@ -246,6 +255,24 @@ internal sealed class RetailPViewPassExecutor :
_entities.ClearClipRouting();
}
public void UseCellPortalViewRouting(uint cellId, ClipViewSlice slice)
{
_singleCellClipRouting.Clear();
_singleCellClipRouting.Add(cellId, slice.Slot);
_envCells.SetClipRouting(_singleCellClipRouting);
// Retail DrawMesh only viewcone-checks an object's sphere under the
// installed PortalList and then draws the mesh whole. Hard clipping the
// object here slices a stationary player when the chase camera crosses
// into the opposite cathedral cell while the player remains behind.
_entities.ClearClipRouting();
}
private void UseOutdoorPortalViewRouting(ClipViewSlice slice) =>
_entities.SetClipRouting(
_noCellClipRouting,
outdoorSlot: slice.Slot,
outdoorVisible: true);
public void PrepareCellBatches(
RetailPViewFrameInput frame,
HashSet<uint> visibleCellIds) =>
@ -376,6 +403,7 @@ internal sealed class RetailPViewPassExecutor :
if (scissor)
_surface.EndScissor();
_entities.ClearClipRouting();
DisableClipDistances();
}
@ -418,8 +446,7 @@ internal sealed class RetailPViewPassExecutor :
_particleClassifications.ReplaceOutdoor(context.ParticleOwnerIds);
if (!frame.RootCell.IsOutdoorNode
&& _particleClassifications.Outdoor.Count > 0
if (_particleClassifications.Outdoor.Count > 0
&& _particles is not null
&& _particleRenderer is not null)
{
@ -427,7 +454,8 @@ internal sealed class RetailPViewPassExecutor :
frame.Camera,
frame.CameraWorldPosition,
ParticleRenderPass.Scene,
_particleClassifications.Outdoor);
_particleClassifications.Outdoor,
clipSlot: (uint)context.Slice.Slot);
}
EnableClipDistances();
@ -456,6 +484,77 @@ internal sealed class RetailPViewPassExecutor :
if (scissor)
_surface.EndScissor();
_entities.ClearClipRouting();
DisableClipDistances();
}
public void DrawLandscapeStaticParticles(
RetailPViewFrameInput frame,
RetailPViewLandscapeStaticParticleContext context)
{
bool scissor = BeginDoorwayScissor(context.Slice.NdcAabb);
_surface.BindTerrainClip();
DisableClipDistances();
_particleClassifications.ReplaceOutdoor(context.ParticleOwnerIds);
if (_particleClassifications.Outdoor.Count > 0
&& _particles is not null
&& _particleRenderer is not null)
{
_particleRenderer.DrawForOwners(
frame.Camera,
frame.CameraWorldPosition,
ParticleRenderPass.Scene,
_particleClassifications.Outdoor,
clipSlot: (uint)context.Slice.Slot);
}
if (scissor)
_surface.EndScissor();
_entities.ClearClipRouting();
DisableClipDistances();
}
public void DrawLandscapeBuildingShellSlice(
RetailPViewFrameInput frame,
RetailPViewLandscapeBuildingShellSliceContext context)
{
UseOutdoorPortalViewRouting(context.Slice);
bool scissor = BeginDoorwayScissor(context.Slice.NdcAabb);
_surface.BindTerrainClip();
DisableClipDistances();
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
{
RenderFrameView drawView = request.View;
_entities.DrawPackedProductionRoute(
frame.Camera,
in drawView,
request.Route,
request.RouteIndex,
request.CellId,
request.TupleLandblockId);
}
else if (context.BuildingShells.Count > 0)
{
var buildingEntry = (
frame.PlayerLandblockId ?? 0u,
Vector3.Zero,
Vector3.Zero,
context.BuildingShells,
(IReadOnlyDictionary<uint, WorldEntity>?)null);
_entities.Draw(
frame.Camera,
new[] { buildingEntry },
frame.Frustum,
neverCullLandblockId: frame.PlayerLandblockId,
visibleCellIds: null,
animatedEntityIds: frame.AnimatedEntityIds);
}
if (scissor)
_surface.EndScissor();
_entities.ClearClipRouting();
DisableClipDistances();
}
@ -468,10 +567,13 @@ internal sealed class RetailPViewPassExecutor :
public void DrawLookInPortalPunch(
RetailPViewFrameInput frame,
RetailPViewCellSliceContext context) =>
DrawPortalDepthWrite(context, frame, forceFarZ: true);
RetailPViewCellSliceContext context,
int portalIndex) =>
DrawPortalDepthWrite(context, frame, forceFarZ: true, portalIndex);
public void DrawUnattachedSceneParticles(RetailPViewFrameInput frame)
public void DrawUnattachedSceneParticles(
RetailPViewFrameInput frame,
ClipViewSlice slice)
{
if (_particles is null || _particleRenderer is null)
return;
@ -482,7 +584,8 @@ internal sealed class RetailPViewPassExecutor :
frame.CameraWorldPosition,
ParticleRenderPass.Scene,
_noSceneParticleEntityIds,
includeUnattached: true);
includeUnattached: true,
clipSlot: (uint)slice.Slot);
}
public void FlushLandscapeAlpha() => _alpha.Flush();
@ -509,7 +612,8 @@ internal sealed class RetailPViewPassExecutor :
frame.Camera,
frame.CameraWorldPosition,
ParticleRenderPass.Scene,
visible);
visible,
clipSlot: (uint)context.Slice.Slot);
DisableClipDistances();
}
@ -552,7 +656,8 @@ internal sealed class RetailPViewPassExecutor :
private void DrawPortalDepthWrite(
RetailPViewCellSliceContext context,
RetailPViewFrameInput frame,
bool forceFarZ)
bool forceFarZ,
int? onlyPortalIndex = null)
{
// Retail D3DPolyRender::DrawPortalPolyInternal @ 0x0059BC90.
// Main interior roots stamp true depth (seal); outdoor and look-in
@ -566,6 +671,8 @@ internal sealed class RetailPViewPassExecutor :
Span<Vector3> world = stackalloc Vector3[32];
for (int index = 0; index < cell.Portals.Count; index++)
{
if (onlyPortalIndex.HasValue && index != onlyPortalIndex.Value)
continue;
if (cell.Portals[index].OtherCellId != 0xFFFF)
continue;
if (index >= cell.PortalPolygons.Count)