The login tunnel now covers from the first world-facing frame (the sky-void backdrop can never present pre-tunnel) and holds through an atomic tunnel-to-world swap at reveal completion — the void is structurally unreachable on both edges, pinned by frame-sequence tests across WorldSceneRenderer/WorldRevealCoordinator/LocalPlayerTeleport- Controller/RuntimeWorldTransitState. Vitals detail icons draw at their authored centered offsets in both stacked and side-by-side layouts. Implemented and live-probed by the fix agent; finalized by the lead after the agent parked post-verification (gates re-run green: App 5512/3, Runtime 1747/0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
352 lines
14 KiB
C#
352 lines
14 KiB
C#
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Rendering.Selection;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.Core.Rendering;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
internal interface IWorldScenePViewRenderer
|
|
{
|
|
IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; }
|
|
|
|
RetailPViewFrameResult DrawInside(RetailPViewFrameInput input);
|
|
|
|
void AbortFrame();
|
|
}
|
|
|
|
internal sealed class WorldScenePViewRenderer : IWorldScenePViewRenderer
|
|
{
|
|
private readonly RetailPViewRenderer _renderer;
|
|
private readonly IRetailPViewPassExecutor _passes;
|
|
private readonly IOutdoorSceneParticleOwnerSource _particles;
|
|
|
|
public WorldScenePViewRenderer(
|
|
RetailPViewRenderer renderer,
|
|
IRetailPViewPassExecutor passes,
|
|
IOutdoorSceneParticleOwnerSource particles)
|
|
{
|
|
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
|
_passes = passes ?? throw new ArgumentNullException(nameof(passes));
|
|
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
|
|
}
|
|
|
|
public IReadOnlySet<uint> OutdoorSceneParticleEntityIds =>
|
|
_particles.OutdoorSceneParticleEntityIds;
|
|
|
|
public RetailPViewFrameResult DrawInside(RetailPViewFrameInput input) =>
|
|
_renderer.DrawInside(input, _passes);
|
|
|
|
public void AbortFrame() => _passes.AbortFrame();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Owns the normal-world render transaction between shared frame-resource
|
|
/// preparation and private presentation. It preserves retail's one PView
|
|
/// decision: a resolved viewer root uses DrawInside; only an unresolved root
|
|
/// uses the flat safety path.
|
|
/// </summary>
|
|
internal sealed class WorldSceneRenderer : IWorldSceneFramePhase
|
|
{
|
|
private readonly IRenderFrameFoundationSource _foundation;
|
|
private readonly IRenderLoginStateSource _login;
|
|
private readonly IWorldSceneSkyStateSource _sky;
|
|
private readonly IWorldRenderFrameBuilder _frames;
|
|
private readonly IWorldSceneEntitySource _entities;
|
|
private readonly IWorldSceneSelectionFrame? _selection;
|
|
private readonly IWorldSceneAlphaFrame _alpha;
|
|
private readonly IWorldSceneParticleVisibility _particleVisibility;
|
|
private readonly IWorldScenePViewRenderer _pview;
|
|
private readonly IRetailPViewCellSource _pviewCells;
|
|
private readonly IWorldScenePassExecutor _passes;
|
|
private readonly IWorldRenderRangeSource _renderRange;
|
|
private readonly IWorldSceneDiagnostics _diagnostics;
|
|
private readonly IWorldGenerationAvailability _availability;
|
|
private readonly RetailPViewFrameInput _pviewFrameInput = new();
|
|
|
|
public WorldSceneRenderer(
|
|
IRenderFrameFoundationSource foundation,
|
|
IRenderLoginStateSource login,
|
|
IWorldSceneSkyStateSource sky,
|
|
IWorldRenderFrameBuilder frames,
|
|
IWorldSceneEntitySource entities,
|
|
IWorldSceneSelectionFrame? selection,
|
|
IWorldSceneAlphaFrame alpha,
|
|
IWorldSceneParticleVisibility particleVisibility,
|
|
IWorldScenePViewRenderer pview,
|
|
IRetailPViewCellSource pviewCells,
|
|
IWorldScenePassExecutor passes,
|
|
IWorldRenderRangeSource renderRange,
|
|
IWorldSceneDiagnostics diagnostics,
|
|
IWorldGenerationAvailability? availability = null)
|
|
{
|
|
_foundation = foundation ?? throw new ArgumentNullException(nameof(foundation));
|
|
_login = login ?? throw new ArgumentNullException(nameof(login));
|
|
_sky = sky ?? throw new ArgumentNullException(nameof(sky));
|
|
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
|
|
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
|
_selection = selection;
|
|
_alpha = alpha ?? throw new ArgumentNullException(nameof(alpha));
|
|
_particleVisibility = particleVisibility
|
|
?? throw new ArgumentNullException(nameof(particleVisibility));
|
|
_pview = pview ?? throw new ArgumentNullException(nameof(pview));
|
|
_pviewCells = pviewCells ?? throw new ArgumentNullException(nameof(pviewCells));
|
|
_passes = passes ?? throw new ArgumentNullException(nameof(passes));
|
|
_renderRange = renderRange ?? throw new ArgumentNullException(nameof(renderRange));
|
|
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
|
|
_availability = availability ?? AlwaysAvailableWorldGeneration.Instance;
|
|
}
|
|
|
|
public WorldRenderFrameOutcome Render(RenderFrameInput input)
|
|
{
|
|
_ = input;
|
|
bool selectionFrameStarted = _selection is not null;
|
|
bool worldFrameStarted = false;
|
|
bool pviewFrameStarted = false;
|
|
try
|
|
{
|
|
_selection?.BeginFrame();
|
|
if (!_availability.IsWorldAvailable)
|
|
{
|
|
_selection?.CompleteFrame();
|
|
selectionFrameStarted = false;
|
|
return default;
|
|
}
|
|
|
|
RenderFrameFoundation foundation = _foundation.Foundation;
|
|
if (foundation.PortalViewportVisible)
|
|
{
|
|
_selection?.CompleteFrame();
|
|
selectionFrameStarted = false;
|
|
return default;
|
|
}
|
|
|
|
// Set before BeginFrame so an already-poisoned queue is recovered by
|
|
// the same abort path instead of making every later frame fail.
|
|
worldFrameStarted = true;
|
|
_alpha.BeginFrame();
|
|
WorldRenderFrame world = _frames.Build(
|
|
in foundation,
|
|
_login.IsWaitingForLogin,
|
|
_sky.ActiveDayGroup);
|
|
_passes.BeginFrame();
|
|
|
|
WorldCameraFrame camera = world.Camera;
|
|
WorldRootFrame roots = world.Roots;
|
|
LoadedCell? clipRoot = world.ClipRoot;
|
|
bool renderSky = roots.RenderSky;
|
|
bool drawSkyThisFrame = false;
|
|
bool terrainDrawn = false;
|
|
bool skyDrawn = false;
|
|
bool depthClear = false;
|
|
bool outdoorSceneryDrawn = false;
|
|
int liveDynamicDrawnCount = 0;
|
|
string sceneParticles = "none";
|
|
TerrainClipMode terrainClipMode = TerrainClipMode.Planes;
|
|
RetailPViewFrameResult? pviewResult = null;
|
|
|
|
if (clipRoot is null)
|
|
{
|
|
_passes.PrepareFlatWorldClip();
|
|
drawSkyThisFrame = renderSky;
|
|
skyDrawn = drawSkyThisFrame;
|
|
if (drawSkyThisFrame)
|
|
{
|
|
_passes.DrawFlatSky(
|
|
in camera,
|
|
in foundation,
|
|
_sky.ActiveDayGroup,
|
|
_sky.DayFraction);
|
|
}
|
|
|
|
// The former sky-only "waiting for login" skip lived here.
|
|
// It is unreachable now: LocalPlayerTeleportRenderStateSource
|
|
// folds IsWaitingForLogin into PortalViewportVisible (retail's
|
|
// pre-player gameplay screen draws NO world — black, not sky),
|
|
// so the portal-visible return above already covers every
|
|
// waiting frame. One gate computes the frame's visibility;
|
|
// this phase only enforces it.
|
|
_passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId);
|
|
terrainDrawn = true;
|
|
}
|
|
else
|
|
{
|
|
terrainClipMode = TerrainClipMode.Skip;
|
|
}
|
|
|
|
if (clipRoot is not null)
|
|
{
|
|
pviewFrameStarted = true;
|
|
pviewResult = _pview.DrawInside(
|
|
_pviewFrameInput.Reset(
|
|
clipRoot,
|
|
world.Buildings.NearbyBuildingCells,
|
|
roots.ViewerEyePosition,
|
|
camera.ViewProjection,
|
|
_pviewCells,
|
|
camera.Camera,
|
|
camera.Position,
|
|
camera.Frustum,
|
|
roots.PlayerLandblockId,
|
|
world.AnimatedEntityIds,
|
|
roots.RenderCenterLandblockX,
|
|
roots.RenderCenterLandblockY,
|
|
_renderRange.NearRadius,
|
|
_entities.LandblockEntries,
|
|
renderSky,
|
|
roots.PlayerSeenOutside,
|
|
_sky.DayFraction,
|
|
_sky.ActiveDayGroup,
|
|
foundation.Sky,
|
|
foundation.EnvironOverrideActive,
|
|
roots.ViewerCellId,
|
|
roots.PlayerCellId,
|
|
roots.PlayerViewPosition,
|
|
camera.Camera.View,
|
|
_diagnostics.CameraCellResolution));
|
|
|
|
_particleVisibility.MarkVisibleCells(pviewResult.DrawableCells);
|
|
_frames.ObserveDrawableCells(pviewResult.DrawableCells);
|
|
_diagnostics.EmitPViewInput(
|
|
pviewResult.PortalFrame,
|
|
camera.ViewProjection,
|
|
clipRoot,
|
|
camera.Position,
|
|
roots.PlayerViewPosition);
|
|
|
|
bool hasOutsideSlice = pviewResult.ClipAssembly.OutsideViewSlices.Length > 0;
|
|
terrainDrawn = hasOutsideSlice;
|
|
skyDrawn = renderSky && hasOutsideSlice;
|
|
// This mirrors the established diagnostic meaning, including
|
|
// outdoor roots whose PView executor does not issue an interior
|
|
// depth clear.
|
|
depthClear = hasOutsideSlice;
|
|
RenderProjectionCounts sourceCounts =
|
|
pviewResult.SourceCounts;
|
|
if (sourceCounts.IndoorCellStatic > 0 || hasOutsideSlice)
|
|
sceneParticles = "pviewScoped";
|
|
outdoorSceneryDrawn =
|
|
sourceCounts.OutdoorStatic > 0
|
|
&& hasOutsideSlice;
|
|
liveDynamicDrawnCount =
|
|
sourceCounts.LiveDynamicRoot;
|
|
}
|
|
else
|
|
{
|
|
_passes.DrawFlatEntities(
|
|
in camera,
|
|
_entities.LandblockEntries,
|
|
roots.PlayerLandblockId,
|
|
world.AnimatedEntityIds);
|
|
_frames.ClearDrawableCells();
|
|
}
|
|
|
|
_passes.DisableClipDistances();
|
|
sceneParticles = _passes.DrawPostWorldParticles(
|
|
clipRoot,
|
|
pviewResult?.ClipAssembly,
|
|
in camera,
|
|
_pview.OutdoorSceneParticleEntityIds,
|
|
sceneParticles);
|
|
|
|
if (clipRoot is null && drawSkyThisFrame)
|
|
{
|
|
skyDrawn = true;
|
|
_passes.DrawFlatWeather(
|
|
in camera,
|
|
in foundation,
|
|
_sky.ActiveDayGroup,
|
|
_sky.DayFraction);
|
|
}
|
|
|
|
_diagnostics.EmitRenderSignature(
|
|
clipRoot is null ? "OutdoorRoot" : "RetailPViewInside",
|
|
clipRoot,
|
|
roots.ViewerRoot,
|
|
roots.PlayerRoot,
|
|
roots.ViewerCellId,
|
|
roots.PlayerCellId,
|
|
roots.PlayerIndoorGate,
|
|
roots.CameraInsideCell,
|
|
renderSky,
|
|
drawSkyThisFrame,
|
|
terrainDrawn,
|
|
terrainClipMode,
|
|
skyDrawn,
|
|
depthClear,
|
|
outdoorSceneryDrawn,
|
|
liveDynamicDrawnCount,
|
|
sceneParticles,
|
|
pviewResult,
|
|
camera.Position,
|
|
roots.PlayerViewPosition);
|
|
|
|
WorldSceneDiagnosticOutcome diagnostic = _diagnostics.DrawAndPublish(
|
|
in camera,
|
|
_entities.LandblockBounds);
|
|
CompleteWorldFrame();
|
|
worldFrameStarted = false;
|
|
pviewFrameStarted = false;
|
|
_selection?.CompleteFrame();
|
|
selectionFrameStarted = false;
|
|
return new WorldRenderFrameOutcome(
|
|
diagnostic.VisibleLandblocks,
|
|
diagnostic.TotalLandblocks,
|
|
NormalWorldDrawn: true);
|
|
}
|
|
catch (Exception renderFailure)
|
|
{
|
|
List<Exception>? abortFailures = AbortFrame(
|
|
worldFrameStarted,
|
|
pviewFrameStarted,
|
|
selectionFrameStarted);
|
|
if (abortFailures is { Count: > 0 })
|
|
{
|
|
abortFailures.Insert(0, renderFailure);
|
|
throw new AggregateException(
|
|
"World rendering failed and the incomplete frame could not be fully aborted.",
|
|
abortFailures);
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private void CompleteWorldFrame()
|
|
{
|
|
_alpha.EndFrame();
|
|
if (_passes.TerrainVisibleCellIds is { } visibleTerrainCells)
|
|
_particleVisibility.MarkVisibleCells(visibleTerrainCells);
|
|
_particleVisibility.CompleteFrame();
|
|
}
|
|
|
|
private List<Exception>? AbortFrame(
|
|
bool worldFrameStarted,
|
|
bool pviewFrameStarted,
|
|
bool selectionFrameStarted)
|
|
{
|
|
List<Exception>? failures = null;
|
|
if (worldFrameStarted)
|
|
{
|
|
if (pviewFrameStarted)
|
|
TryAbort(_pview.AbortFrame);
|
|
TryAbort(_passes.AbortFrame);
|
|
TryAbort(_particleVisibility.AbortFrame);
|
|
TryAbort(_alpha.AbortFrame);
|
|
}
|
|
if (selectionFrameStarted && _selection is not null)
|
|
TryAbort(_selection.AbortFrame);
|
|
return failures;
|
|
|
|
void TryAbort(Action abort)
|
|
{
|
|
try
|
|
{
|
|
abort();
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
(failures ??= []).Add(error);
|
|
}
|
|
}
|
|
}
|
|
}
|