refactor(render): extract world scene frame owner

Move fallback and PView world drawing, shared alpha, particles, visibility, selection, and diagnostics behind focused frame owners. Make exceptional frames failure-atomic by restoring the shared GL baseline and preserving primary alpha failures through cleanup.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 07:22:09 +02:00
parent 85239fb373
commit 28e1cf8029
25 changed files with 2679 additions and 844 deletions

File diff suppressed because it is too large Load diff

View file

@ -377,7 +377,9 @@ void main() { } // depth-only: color writes are masked off by the caller state
_gl.ColorMask(true, true, true, true);
_gl.DepthMask(true);
_gl.DepthFunc(DepthFunction.Less);
_gl.Enable(EnableCap.CullFace);
// Renderers opt into culling locally; the shared frame convention is
// cull-off. Keep this success exit identical to frame-abort recovery.
_gl.Disable(EnableCap.CullFace);
_gl.CullFace(TriangleFace.Back);
_gl.FrontFace(FrontFaceDirection.CW);
_gl.UseProgram(0);

View file

@ -0,0 +1,113 @@
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
internal interface IRenderFrameGlState
{
void RestoreFrameDefaults();
}
internal interface IRenderFrameGlStateApi
{
void SetCapability(EnableCap capability, bool enabled);
void ColorMask(bool red, bool green, bool blue, bool alpha);
void StencilMask(uint mask);
void DepthMask(bool enabled);
void DepthFunc(DepthFunction function);
void CullFace(TriangleFace face);
void FrontFace(FrontFaceDirection direction);
void UseProgram(uint program);
void BindVertexArray(uint vertexArray);
void BindBuffer(BufferTargetARB target, uint buffer);
}
internal sealed class SilkRenderFrameGlStateApi : IRenderFrameGlStateApi
{
private readonly GL _gl;
public SilkRenderFrameGlStateApi(GL gl)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
}
public void SetCapability(EnableCap capability, bool enabled)
{
if (enabled)
_gl.Enable(capability);
else
_gl.Disable(capability);
}
public void ColorMask(bool red, bool green, bool blue, bool alpha) =>
_gl.ColorMask(red, green, blue, alpha);
public void StencilMask(uint mask) => _gl.StencilMask(mask);
public void DepthMask(bool enabled) => _gl.DepthMask(enabled);
public void DepthFunc(DepthFunction function) => _gl.DepthFunc(function);
public void CullFace(TriangleFace face) => _gl.CullFace(face);
public void FrontFace(FrontFaceDirection direction) => _gl.FrontFace(direction);
public void UseProgram(uint program) => _gl.UseProgram(program);
public void BindVertexArray(uint vertexArray) => _gl.BindVertexArray(vertexArray);
public void BindBuffer(BufferTargetARB target, uint buffer) =>
_gl.BindBuffer(target, buffer);
}
/// <summary>
/// Restores the frame-global OpenGL convention shared by frame clear and
/// exceptional world-pass rollback. This prevents a failed doorway scissor or
/// portal depth mask from clipping/color-masking the next frame's clear.
/// </summary>
internal sealed class RenderFrameGlStateController : IRenderFrameGlState
{
private readonly IRenderFrameGlStateApi _gl;
public RenderFrameGlStateController(IRenderFrameGlStateApi gl)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
}
public void RestoreFrameDefaults()
{
_gl.SetCapability(EnableCap.ScissorTest, enabled: false);
_gl.SetCapability(EnableCap.StencilTest, enabled: false);
_gl.SetCapability(EnableCap.Blend, enabled: false);
_gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: false);
for (int index = 0; index < ClipFrame.MaxPlanes; index++)
{
_gl.SetCapability(
EnableCap.ClipDistance0 + index,
enabled: false);
}
_gl.ColorMask(true, true, true, true);
_gl.StencilMask(0xFF);
_gl.DepthMask(true);
_gl.DepthFunc(DepthFunction.Less);
_gl.SetCapability(EnableCap.DepthTest, enabled: true);
// Renderers that need face culling establish it locally. The shared
// frame convention is culling off; SkyRenderer restores the state it
// observed, so enabling it here would leak culling into later passes.
_gl.SetCapability(EnableCap.CullFace, enabled: false);
_gl.CullFace(TriangleFace.Back);
_gl.FrontFace(FrontFaceDirection.CW);
_gl.UseProgram(0);
_gl.BindVertexArray(0);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
}
}

View file

@ -33,6 +33,11 @@ internal interface IRenderFrameClearPhase
RenderFrameFoundation Clear();
}
internal interface IRenderFrameFoundationSource
{
RenderFrameFoundation Foundation { get; }
}
internal interface IRenderFrameLivePreparation
{
void Prepare(int gpuSlot);
@ -43,7 +48,9 @@ internal interface IRenderFrameLivePreparation
/// typed phases preserve resource begin, clear/state, then live publication
/// order without exposing a renderer service bag.
/// </summary>
internal sealed class RenderFrameResourceController : IRenderFrameResourcePhase
internal sealed class RenderFrameResourceController :
IRenderFrameResourcePhase,
IRenderFrameFoundationSource
{
private readonly IRenderFrameSlotSource _frameSlots;
private readonly IRenderFrameBeginResources _resources;
@ -162,6 +169,7 @@ internal sealed class RuntimeRenderFrameClearPhase : IRenderFrameClearPhase
private readonly IRenderFramePortalStateSource _portal;
private readonly ParticleVisibilityController _particleVisibility;
private readonly WorldRenderDiagnostics _diagnostics;
private readonly IRenderFrameGlState _frameGlState;
public RuntimeRenderFrameClearPhase(
GL gl,
@ -169,7 +177,8 @@ internal sealed class RuntimeRenderFrameClearPhase : IRenderFrameClearPhase
WeatherSystem weather,
IRenderFramePortalStateSource portal,
ParticleVisibilityController particleVisibility,
WorldRenderDiagnostics diagnostics)
WorldRenderDiagnostics diagnostics,
IRenderFrameGlState frameGlState)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
_worldTime = worldTime ?? throw new ArgumentNullException(nameof(worldTime));
@ -178,6 +187,8 @@ internal sealed class RuntimeRenderFrameClearPhase : IRenderFrameClearPhase
_particleVisibility = particleVisibility
?? throw new ArgumentNullException(nameof(particleVisibility));
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_frameGlState = frameGlState
?? throw new ArgumentNullException(nameof(frameGlState));
}
public RenderFrameFoundation Clear()
@ -203,13 +214,11 @@ internal sealed class RuntimeRenderFrameClearPhase : IRenderFrameClearPhase
1f);
}
_gl.DepthMask(true);
_frameGlState.RestoreFrameDefaults();
_gl.Clear(
ClearBufferMask.ColorBufferBit
| ClearBufferMask.DepthBufferBit
| ClearBufferMask.StencilBufferBit);
_gl.CullFace(TriangleFace.Back);
_gl.FrontFace(FrontFaceDirection.CW);
_diagnostics.EmitGlStateTripwireIfChanged(
AcDream.Core.Rendering.RenderingDiagnostics.ProbeGlStateEnabled);

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
namespace AcDream.App.Rendering;
@ -56,7 +57,16 @@ internal readonly record struct RetailAlphaSubmission(
/// material/renderer sort.</item>
/// </list>
/// </summary>
internal sealed class RetailAlphaQueue
internal interface IWorldSceneAlphaFrame
{
void BeginFrame();
void EndFrame();
void AbortFrame();
}
internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame
{
private readonly List<RetailAlphaSubmission> _submissions = new(256);
private readonly List<IRetailAlphaDrawSource> _sources = new(4);
@ -108,58 +118,93 @@ internal sealed class RetailAlphaQueue
if (!IsCollecting)
throw new InvalidOperationException("Retail alpha flush requires an active frame.");
Exception? drawFailure = null;
List<Exception>? resetFailures = null;
try
{
if (_submissions.Count == 0)
return;
SortRetailOrder();
EnsureTokenCapacity(_submissions.Count);
EnsureSourceCapacity(_sources.Count);
Array.Clear(_sourceDrawOffsets, 0, _sources.Count);
// Prepare each renderer once for this alpha scope. The filtered
// token sequence preserves final retail order for that source, so
// every later adjacent source-run maps to one contiguous prepared
// range without another GPU upload.
for (int sourceIndex = 0; sourceIndex < _sources.Count; sourceIndex++)
if (_submissions.Count > 0)
{
IRetailAlphaDrawSource source = _sources[sourceIndex];
int sourceCount = 0;
for (int i = 0; i < _submissions.Count; i++)
SortRetailOrder();
EnsureTokenCapacity(_submissions.Count);
EnsureSourceCapacity(_sources.Count);
Array.Clear(_sourceDrawOffsets, 0, _sources.Count);
// Prepare each renderer once for this alpha scope. The filtered
// token sequence preserves final retail order for that source, so
// every later adjacent source-run maps to one contiguous prepared
// range without another GPU upload.
for (int sourceIndex = 0; sourceIndex < _sources.Count; sourceIndex++)
{
RetailAlphaSubmission submission = _submissions[i];
if (ReferenceEquals(submission.Source, source))
_tokenScratch[sourceCount++] = submission.Token;
IRetailAlphaDrawSource source = _sources[sourceIndex];
int sourceCount = 0;
for (int i = 0; i < _submissions.Count; i++)
{
RetailAlphaSubmission submission = _submissions[i];
if (ReferenceEquals(submission.Source, source))
_tokenScratch[sourceCount++] = submission.Token;
}
source.PrepareAlphaDraws(_tokenScratch.AsSpan(0, sourceCount));
}
source.PrepareAlphaDraws(_tokenScratch.AsSpan(0, sourceCount));
}
int start = 0;
while (start < _submissions.Count)
{
IRetailAlphaDrawSource source = _submissions[start].Source;
int end = start + 1;
while (end < _submissions.Count
&& ReferenceEquals(_submissions[end].Source, source))
end++;
int start = 0;
while (start < _submissions.Count)
{
IRetailAlphaDrawSource source = _submissions[start].Source;
int end = start + 1;
while (end < _submissions.Count
&& ReferenceEquals(_submissions[end].Source, source))
end++;
int count = end - start;
int sourceIndex = FindSourceIndex(source);
int firstPreparedDraw = _sourceDrawOffsets[sourceIndex];
source.DrawPreparedAlphaBatch(firstPreparedDraw, count);
_sourceDrawOffsets[sourceIndex] += count;
start = end;
int count = end - start;
int sourceIndex = FindSourceIndex(source);
int firstPreparedDraw = _sourceDrawOffsets[sourceIndex];
source.DrawPreparedAlphaBatch(firstPreparedDraw, count);
_sourceDrawOffsets[sourceIndex] += count;
start = end;
}
}
}
catch (Exception error)
{
drawFailure = error;
}
finally
{
for (int i = 0; i < _sources.Count; i++)
_sources[i].ResetAlphaSubmissions();
{
try
{
_sources[i].ResetAlphaSubmissions();
}
catch (Exception error)
{
(resetFailures ??= []).Add(error);
}
}
_sources.Clear();
_submissions.Clear();
}
if (drawFailure is not null)
{
if (resetFailures is { Count: > 0 })
{
resetFailures.Insert(0, drawFailure);
throw new AggregateException(
"Retail alpha drawing failed and its submissions could not be fully reset.",
resetFailures);
}
ExceptionDispatchInfo.Capture(drawFailure).Throw();
}
if (resetFailures is { Count: > 0 })
{
throw new AggregateException(
"Retail alpha submissions could not be fully reset.",
resetFailures);
}
}
public void EndFrame()
@ -177,6 +222,39 @@ internal sealed class RetailAlphaQueue
}
}
/// <summary>
/// Discards an incomplete frame without drawing it. Every source is still
/// told to release its retained submission payload so the next frame begins
/// from the same empty invariant as a successful flush.
/// </summary>
public void AbortFrame()
{
List<Exception>? failures = null;
try
{
for (int i = 0; i < _sources.Count; i++)
{
try
{
_sources[i].ResetAlphaSubmissions();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
}
}
finally
{
_sources.Clear();
_submissions.Clear();
IsCollecting = false;
}
if (failures is { Count: > 0 })
throw new AggregateException("Retail alpha frame abort failed.", failures);
}
private void SortRetailOrder()
{
int count = _submissions.Count;

View file

@ -74,9 +74,17 @@ internal sealed class RetailPViewParticleClassifications
/// cell shells/objects, then surviving dynamics. Landscape sky/terrain/weather
/// placement follows <c>LScape::draw @ 0x00506330</c>.
/// </summary>
internal sealed class RetailPViewPassExecutor : IRetailPViewPassExecutor
internal interface IOutdoorSceneParticleOwnerSource
{
IReadOnlySet<uint> OutdoorSceneParticleEntityIds { get; }
}
internal sealed class RetailPViewPassExecutor :
IRetailPViewPassExecutor,
IOutdoorSceneParticleOwnerSource
{
private readonly GL _gl;
private readonly IRenderFrameGlState _frameGlState;
private readonly IRetailPViewFramebufferSource _framebuffer;
private readonly ClipFrame _clipFrame;
private readonly TerrainModernRenderer? _terrain;
@ -101,6 +109,7 @@ internal sealed class RetailPViewPassExecutor : IRetailPViewPassExecutor
public RetailPViewPassExecutor(
GL gl,
IRenderFrameGlState frameGlState,
IRetailPViewFramebufferSource framebuffer,
ClipFrame clipFrame,
TerrainModernRenderer? terrain,
@ -115,6 +124,8 @@ internal sealed class RetailPViewPassExecutor : IRetailPViewPassExecutor
TerrainDrawDiagnosticsController terrainDiagnostics)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
_frameGlState = frameGlState
?? throw new ArgumentNullException(nameof(frameGlState));
_framebuffer = framebuffer
?? throw new ArgumentNullException(nameof(framebuffer));
_clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame));
@ -136,6 +147,28 @@ internal sealed class RetailPViewPassExecutor : IRetailPViewPassExecutor
_particleClassifications.BeginFrame();
}
public void AbortFrame()
{
List<Exception>? failures = null;
TryAbort(_frameGlState.RestoreFrameDefaults);
TryAbort(_particleClassifications.BeginFrame);
TryAbort(_noSceneParticleEntityIds.Clear);
if (failures is { Count: > 0 })
throw new AggregateException("Retail PView pass abort failed.", failures);
void TryAbort(Action operation)
{
try
{
operation();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
}
}
public ClipFrameAssembly AssembleClipFrame(
PortalVisibilityFrame portalFrame,
ClipFrameAssembly reuseAssembly) =>

View file

@ -889,6 +889,7 @@ public interface IRetailPViewCellSource
/// </summary>
public interface IRetailPViewPassExecutor
{
void AbortFrame();
void BeginFrame();
ClipFrameAssembly AssembleClipFrame(
PortalVisibilityFrame portalFrame,
@ -1118,7 +1119,8 @@ public sealed class RetailPViewFrameInput
public required int RenderRadius { get; init; }
public required IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries { get; init; }
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
{ get; init; }
// Pass-presentation and diagnostic values consumed synchronously by the
// typed executor. This input is data-only and is never retained.

View file

@ -9,7 +9,19 @@ namespace AcDream.App.Rendering.Selection;
/// The renderer builds one frame while input queries the previously completed
/// frame, avoiding partial visibility state during multi-slice portal drawing.
/// </summary>
internal sealed class RetailSelectionScene : IRetailSelectionRenderSink, IRetailSelectionLightingSource
internal interface IWorldSceneSelectionFrame
{
void BeginFrame();
void CompleteFrame();
void AbortFrame();
}
internal sealed class RetailSelectionScene :
IRetailSelectionRenderSink,
IRetailSelectionLightingSource,
IWorldSceneSelectionFrame
{
private readonly IRetailSelectionGeometrySource _geometry;
private readonly RetailSelectionLightingPulse _lightingPulse;
@ -100,6 +112,15 @@ internal sealed class RetailSelectionScene : IRetailSelectionRenderSink, IRetail
(_published, _building) = (_building, _published);
}
/// <summary>Discards the incomplete building frame and preserves the last
/// completely published selection product.</summary>
public void AbortFrame()
{
_building.Clear();
_buildingKeys.Clear();
_viewFrustum = null;
}
public RetailSelectionHit? Pick(
float mouseX,
float mouseY,

View file

@ -3,13 +3,22 @@ using AcDream.Core.Vfx;
namespace AcDream.App.Rendering.Vfx;
internal interface IWorldSceneParticleVisibility
{
void MarkVisibleCells(HashSet<uint> cellIds);
void CompleteFrame();
void AbortFrame();
}
/// <summary>
/// Bridges the retained retail PView result into the next physics update's
/// <c>CObjCell::IsInView</c> particle gate. The controller owns only immutable
/// frame meaning: one completed viewer position plus the AC cells admitted by
/// that completed view. It neither creates emitters nor performs rendering.
/// </summary>
public sealed class ParticleVisibilityController
public sealed class ParticleVisibilityController : IWorldSceneParticleVisibility
{
public const float ExtendedRangeMultiplier = 2f;
@ -70,6 +79,15 @@ public sealed class ParticleVisibilityController
_hasCompletedWorldView = true;
}
/// <summary>Discards the in-progress visibility product while preserving
/// the last completed view consumed by the update thread.</summary>
public void AbortFrame()
{
_buildingCellIds.Clear();
_frameUsesWorldView = false;
_frameOpen = false;
}
public void Apply(ParticleSystem particles, float rangeMultiplier)
{
ArgumentNullException.ThrowIfNull(particles);

View file

@ -61,6 +61,18 @@ internal readonly record struct WorldRenderFrame(
public LoadedCell? ClipRoot => Roots.ViewerRoot ?? Buildings.OutdoorNode;
}
internal interface IWorldRenderFrameBuilder
{
WorldRenderFrame Build(
in RenderFrameFoundation foundation,
bool waitingForLogin,
DayGroupData? activeDayGroup);
void ObserveDrawableCells(IReadOnlySet<uint> drawableCells);
void ClearDrawableCells();
}
internal interface IWorldFrameCameraSource
{
WorldCameraFrame Resolve();
@ -113,7 +125,7 @@ internal interface IWorldFrameBuildingSource
/// Orders typed world-frame fact sources without owning their borrowed render
/// resources. It makes no draw decision and retains no result from a prior call.
/// </summary>
internal sealed class WorldRenderFrameBuilder
internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
{
private readonly IWorldFrameCameraSource _camera;
private readonly IWorldFrameVisibilityPreparation _visibility;

View file

@ -0,0 +1,274 @@
using System.Numerics;
using AcDream.App.Input;
using AcDream.Core.Physics;
using AcDream.Core.Rendering;
namespace AcDream.App.Rendering;
internal readonly record struct WorldSceneDiagnosticOutcome(
int VisibleLandblocks,
int TotalLandblocks);
internal interface IWorldSceneDiagnostics
{
CameraCellResolution CameraCellResolution { get; }
void EmitPViewInput(
PortalVisibilityFrame portalFrame,
Matrix4x4 viewProjection,
LoadedCell clipRoot,
Vector3 cameraPosition,
Vector3 playerPosition);
void EmitRenderSignature(
string branch,
LoadedCell? clipRoot,
LoadedCell? viewerRoot,
LoadedCell? playerRoot,
uint viewerCellId,
uint playerCellId,
bool playerIndoorGate,
bool cameraInsideCell,
bool renderSky,
bool drawSkyThisFrame,
bool terrainDrawn,
TerrainClipMode terrainClipMode,
bool skyDrawn,
bool depthClear,
bool outdoorSceneryDrawn,
int liveDynamicDrawnCount,
string sceneParticles,
RetailPViewFrameResult? pviewResult,
Vector3 cameraPosition,
Vector3 playerPosition);
WorldSceneDiagnosticOutcome DrawAndPublish(
in WorldCameraFrame camera,
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds);
}
/// <summary>
/// Owns world-pass probes, collision wireframes, and the DebugVM facts derived
/// from one completed normal-world draw. None of these diagnostics influence
/// visibility or pass ordering.
/// </summary>
internal sealed class WorldSceneDiagnosticsController : IWorldSceneDiagnostics
{
private readonly WorldRenderDiagnostics _diagnostics;
private readonly IWorldScenePViewDiagnosticSource _pview;
private readonly IWorldSceneDebugStateSource _state;
private readonly DebugLineRenderer? _lines;
private readonly PhysicsEngine _physics;
private readonly ILocalPlayerModeSource _mode;
private readonly ILocalPlayerControllerSource _player;
private readonly DebugVmRenderFactsPublisher _debugVm;
private readonly bool _debugVmConsumerActive;
private int _debugDrawLogCount;
public WorldSceneDiagnosticsController(
WorldRenderDiagnostics diagnostics,
IWorldScenePViewDiagnosticSource pview,
IWorldSceneDebugStateSource state,
DebugLineRenderer? lines,
PhysicsEngine physics,
ILocalPlayerModeSource mode,
ILocalPlayerControllerSource player,
DebugVmRenderFactsPublisher debugVm,
bool debugVmConsumerActive)
{
_diagnostics = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics));
_pview = pview ?? throw new ArgumentNullException(nameof(pview));
_state = state ?? throw new ArgumentNullException(nameof(state));
_lines = lines;
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
_player = player ?? throw new ArgumentNullException(nameof(player));
_debugVm = debugVm ?? throw new ArgumentNullException(nameof(debugVm));
_debugVmConsumerActive = debugVmConsumerActive;
}
public CameraCellResolution CameraCellResolution => _pview.CameraCellResolution;
public void EmitPViewInput(
PortalVisibilityFrame portalFrame,
Matrix4x4 viewProjection,
LoadedCell clipRoot,
Vector3 cameraPosition,
Vector3 playerPosition)
{
if (!RenderingDiagnostics.ProbePvInputEnabled)
return;
_diagnostics.EmitPViewInput(
enabled: true,
portalFrame,
viewProjection,
clipRoot.IsOutdoorNode,
cameraPosition,
playerPosition,
_pview.RawPlayerPositionOr(playerPosition),
_pview.PlayerYaw,
_pview.SampleTerrainZ(cameraPosition.X, cameraPosition.Y));
}
public void EmitRenderSignature(
string branch,
LoadedCell? clipRoot,
LoadedCell? viewerRoot,
LoadedCell? playerRoot,
uint viewerCellId,
uint playerCellId,
bool playerIndoorGate,
bool cameraInsideCell,
bool renderSky,
bool drawSkyThisFrame,
bool terrainDrawn,
TerrainClipMode terrainClipMode,
bool skyDrawn,
bool depthClear,
bool outdoorSceneryDrawn,
int liveDynamicDrawnCount,
string sceneParticles,
RetailPViewFrameResult? pviewResult,
Vector3 cameraPosition,
Vector3 playerPosition)
{
_diagnostics.EmitRenderSignatureIfChanged(
RenderingDiagnostics.ProbeFlapEnabled,
branch,
clipRoot,
viewerRoot,
playerRoot,
viewerCellId,
playerCellId,
playerIndoorGate,
cameraInsideCell,
renderSky,
drawSkyThisFrame,
terrainDrawn,
terrainClipMode,
skyDrawn,
depthClear,
outdoorSceneryDrawn,
outdoorPortalDrawn: false,
outdoorRootObjectCount: 0,
liveDynamicDrawnCount,
sceneParticles,
pviewResult?.PortalFrame,
pviewResult?.ClipAssembly,
pviewResult?.DrawableCells,
pviewResult?.Partition,
exteriorPortalFrame: null,
exteriorClipAssembly: null,
exteriorDrawableCells: null,
exteriorPartition: null,
cameraPosition,
playerPosition);
}
public WorldSceneDiagnosticOutcome DrawAndPublish(
in WorldCameraFrame camera,
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)
{
ArgumentNullException.ThrowIfNull(bounds);
DrawCollisionWireframes(in camera);
int visible = 0;
int total = 0;
foreach (var entry in bounds)
{
total++;
if (FrustumCuller.IsAabbVisible(
camera.Frustum,
entry.AabbMin,
entry.AabbMax))
{
visible++;
}
}
Vector3 nearestOrigin =
_mode.IsPlayerMode && _player.Controller is { } controller
? controller.Position
: camera.Position;
_debugVm.PublishDebugVmFacts(
_debugVmConsumerActive,
visible,
total,
nearestOrigin,
_physics.ShadowObjects.AllEntriesForDebug());
return new WorldSceneDiagnosticOutcome(visible, total);
}
private void DrawCollisionWireframes(in WorldCameraFrame camera)
{
if (!_state.CollisionWireframesVisible || _lines is null)
return;
_lines.Begin();
int drawn = 0;
foreach (ShadowEntry shadow in _physics.ShadowObjects.AllEntriesForDebug())
{
if (shadow.CollisionType == ShadowCollisionType.Cylinder)
{
float height = shadow.CylHeight > 0f
? shadow.CylHeight
: shadow.Radius * 2f;
_lines.AddCylinder(
shadow.Position,
shadow.Radius,
height,
new Vector3(0f, 1f, 0f));
}
else
{
_lines.AddCylinder(
shadow.Position - new Vector3(0f, 0f, shadow.Radius),
shadow.Radius,
shadow.Radius * 2f,
new Vector3(1f, 0.5f, 0f));
}
drawn++;
}
if (_mode.IsPlayerMode && _player.Controller is { } localPlayer)
{
Vector3 position = localPlayer.Position;
_lines.AddCylinder(
new Vector3(position.X, position.Y, position.Z),
DebugVmRenderFactsPublisher.PlayerCollisionRadius,
1.8f,
new Vector3(1f, 0f, 0f));
LogNearbyCollisionObjects(position, drawn);
}
_lines.Flush(camera.Camera.View, camera.Projection);
}
private void LogNearbyCollisionObjects(Vector3 playerPosition, int drawn)
{
if (_debugDrawLogCount >= 5)
return;
Console.WriteLine(
$"debug frame {_debugDrawLogCount}: player=({playerPosition.X:F1},{playerPosition.Y:F1},{playerPosition.Z:F1}) drew={drawn} "
+ $"totalReg={_physics.ShadowObjects.TotalRegistered}");
int logged = 0;
foreach (ShadowEntry shadow in _physics.ShadowObjects.AllEntriesForDebug())
{
float dx = shadow.Position.X - playerPosition.X;
float dy = shadow.Position.Y - playerPosition.Y;
float horizontalDistance = MathF.Sqrt(dx * dx + dy * dy);
if (horizontalDistance >= 10f)
continue;
Console.WriteLine(
$" near id=0x{shadow.EntityId:X8} type={shadow.CollisionType} "
+ $"pos=({shadow.Position.X:F1},{shadow.Position.Y:F1},{shadow.Position.Z:F1}) "
+ $"r={shadow.Radius:F2} h={shadow.CylHeight:F2} dh={horizontalDistance:F2}");
if (++logged >= 5)
break;
}
_debugDrawLogCount++;
}
}

View file

@ -0,0 +1,328 @@
using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Wb;
using AcDream.Core.Rendering;
using AcDream.Core.Vfx;
using AcDream.Core.World;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
internal interface IWorldScenePassExecutor
{
HashSet<uint>? TerrainVisibleCellIds { get; }
void BeginFrame();
void PrepareFlatWorldClip();
void DrawFlatSky(
in WorldCameraFrame camera,
in RenderFrameFoundation foundation,
DayGroupData? activeDayGroup,
float dayFraction);
void DrawFlatTerrain(in WorldCameraFrame camera, uint? playerLandblockId);
void DrawFlatEntities(
in WorldCameraFrame camera,
IEnumerable<(uint LandblockId, System.Numerics.Vector3 AabbMin,
System.Numerics.Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> entries,
uint? playerLandblockId,
HashSet<uint> animatedEntityIds);
string DrawPostWorldParticles(
LoadedCell? clipRoot,
ClipFrameAssembly? clipAssembly,
in WorldCameraFrame camera,
IReadOnlySet<uint> outdoorOwnerIds,
string currentSignature);
void DrawFlatWeather(
in WorldCameraFrame camera,
in RenderFrameFoundation foundation,
DayGroupData? activeDayGroup,
float dayFraction);
void DisableClipDistances();
void AbortFrame();
}
/// <summary>
/// Concrete GL leaf for the flat-world safety path and the post-world particle
/// and weather passes. Retail PView frames remain owned by
/// <see cref="RetailPViewRenderer"/> and <see cref="RetailPViewPassExecutor"/>.
/// </summary>
internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
{
private readonly GL _gl;
private readonly IRenderFrameGlState _frameGlState;
private readonly ClipFrame _clipFrame;
private readonly WbDrawDispatcher _entities;
private readonly EnvCellRenderer _environmentCells;
private readonly TerrainModernRenderer? _terrain;
private readonly TerrainDrawDiagnosticsController _terrainDiagnostics;
private readonly SkyRenderer? _sky;
private readonly ParticleSystem? _particles;
private readonly ParticleRenderer? _particleRenderer;
private readonly HashSet<uint> _visibleParticleOwners = [];
private readonly HashSet<uint> _noExcludedParticleOwners = [];
public WorldScenePassExecutor(
GL gl,
IRenderFrameGlState frameGlState,
ClipFrame clipFrame,
WbDrawDispatcher entities,
EnvCellRenderer environmentCells,
TerrainModernRenderer? terrain,
TerrainDrawDiagnosticsController terrainDiagnostics,
SkyRenderer? sky,
ParticleSystem? particles,
ParticleRenderer? particleRenderer)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
_frameGlState = frameGlState
?? throw new ArgumentNullException(nameof(frameGlState));
_clipFrame = clipFrame ?? throw new ArgumentNullException(nameof(clipFrame));
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
_environmentCells = environmentCells
?? throw new ArgumentNullException(nameof(environmentCells));
_terrain = terrain;
_terrainDiagnostics = terrainDiagnostics
?? throw new ArgumentNullException(nameof(terrainDiagnostics));
_sky = sky;
_particles = particles;
_particleRenderer = particleRenderer;
}
public HashSet<uint>? TerrainVisibleCellIds => _terrain?.VisibleCellIds;
public void BeginFrame()
{
_visibleParticleOwners.Clear();
_clipFrame.Reset();
_entities.ClearClipRouting();
_environmentCells.SetClipRouting(null);
}
public void PrepareFlatWorldClip()
{
_clipFrame.ReserveTerrainUploads(_gl, 1);
_clipFrame.UploadRegions(_gl);
TerrainClipBufferBinding terrainBinding = _clipFrame.UploadTerrainClip(_gl);
_entities.SetClipRegionSsbo(_clipFrame.RegionSsbo);
_environmentCells.SetClipRegionSsbo(_clipFrame.RegionSsbo);
_terrain?.SetClipUbo(terrainBinding);
}
public void DrawFlatSky(
in WorldCameraFrame camera,
in RenderFrameFoundation foundation,
DayGroupData? activeDayGroup,
float dayFraction)
{
_clipFrame.BindTerrainClip(_gl);
EnableClipDistances();
Exception? drawFailure = null;
try
{
_sky?.RenderSky(
camera.Camera,
camera.Position,
dayFraction,
activeDayGroup,
foundation.Sky,
foundation.EnvironOverrideActive);
}
catch (Exception error)
{
drawFailure = error;
throw;
}
finally
{
try
{
DisableClipDistances();
}
catch (Exception closeFailure) when (drawFailure is not null)
{
throw new AggregateException(
"Sky drawing failed and its clip-distance bracket could not be closed.",
drawFailure,
closeFailure);
}
}
if (_particles is not null && _particleRenderer is not null)
{
_particleRenderer.Draw(
camera.Camera,
camera.Position,
ParticleRenderPass.SkyPreScene);
}
}
public void DrawFlatTerrain(
in WorldCameraFrame camera,
uint? playerLandblockId)
{
EnableClipDistances();
_terrainDiagnostics.Begin();
_terrain?.Draw(
camera.Camera,
camera.Frustum,
neverCullLandblockId: playerLandblockId);
_terrainDiagnostics.Complete();
}
public void DrawFlatEntities(
in WorldCameraFrame camera,
IEnumerable<(uint LandblockId, System.Numerics.Vector3 AabbMin,
System.Numerics.Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> entries,
uint? playerLandblockId,
HashSet<uint> animatedEntityIds) =>
_entities.Draw(
camera.Camera,
entries,
camera.Frustum,
neverCullLandblockId: playerLandblockId,
visibleCellIds: null,
animatedEntityIds: animatedEntityIds);
public string DrawPostWorldParticles(
LoadedCell? clipRoot,
ClipFrameAssembly? clipAssembly,
in WorldCameraFrame camera,
IReadOnlySet<uint> outdoorOwnerIds,
string currentSignature)
{
if (_particles is null || _particleRenderer is null)
return currentSignature;
if (clipRoot is null)
{
if (clipAssembly is not null)
{
_particleRenderer.DrawForOwners(
camera.Camera,
camera.Position,
ParticleRenderPass.Scene,
_visibleParticleOwners,
includeUnattached: true,
excludedAttachedOwnerIds: _noExcludedParticleOwners);
return AppendSignature(currentSignature, "filtered");
}
_particleRenderer.Draw(
camera.Camera,
camera.Position,
ParticleRenderPass.Scene);
return AppendSignature(currentSignature, "global");
}
if (clipRoot.IsOutdoorNode)
{
_particleRenderer.DrawForOwners(
camera.Camera,
camera.Position,
ParticleRenderPass.Scene,
outdoorOwnerIds,
includeUnattached: true);
return AppendSignature(currentSignature, "unattached");
}
return currentSignature;
}
public void DrawFlatWeather(
in WorldCameraFrame camera,
in RenderFrameFoundation foundation,
DayGroupData? activeDayGroup,
float dayFraction)
{
_clipFrame.BindTerrainClip(_gl);
EnableClipDistances();
Exception? drawFailure = null;
try
{
_sky?.RenderWeather(
camera.Camera,
camera.Position,
dayFraction,
activeDayGroup,
foundation.Sky,
foundation.EnvironOverrideActive);
}
catch (Exception error)
{
drawFailure = error;
throw;
}
finally
{
try
{
DisableClipDistances();
}
catch (Exception closeFailure) when (drawFailure is not null)
{
throw new AggregateException(
"Weather drawing failed and its clip-distance bracket could not be closed.",
drawFailure,
closeFailure);
}
}
if (_particles is not null && _particleRenderer is not null)
{
_particleRenderer.Draw(
camera.Camera,
camera.Position,
ParticleRenderPass.SkyPostScene);
}
}
public void DisableClipDistances()
{
for (int index = 0; index < ClipFrame.MaxPlanes; index++)
_gl.Disable(EnableCap.ClipDistance0 + index);
}
public void AbortFrame()
{
List<Exception>? failures = null;
TryAbort(_frameGlState.RestoreFrameDefaults);
TryAbort(_clipFrame.Reset);
TryAbort(_entities.ClearClipRouting);
TryAbort(() => _environmentCells.SetClipRouting(null));
_visibleParticleOwners.Clear();
if (failures is { Count: > 0 })
throw new AggregateException("World scene pass abort failed.", failures);
void TryAbort(Action operation)
{
try
{
operation();
}
catch (Exception error)
{
(failures ??= []).Add(error);
}
}
}
private void EnableClipDistances()
{
for (int index = 0; index < ClipFrame.MaxPlanes; index++)
_gl.Enable(EnableCap.ClipDistance0 + index);
}
private static string AppendSignature(string current, string value) =>
current == "none" ? value : current + "+" + value;
}

View file

@ -0,0 +1,345 @@
using AcDream.App.Rendering.Selection;
using AcDream.App.Rendering.Vfx;
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;
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)
{
_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));
}
public WorldRenderFrameOutcome Render(RenderFrameInput input)
{
_ = input;
bool selectionFrameStarted = _selection is not null;
bool worldFrameStarted = false;
bool pviewFrameStarted = false;
try
{
_selection?.BeginFrame();
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);
}
// Retail keeps the live sky during EnterWorld while suppressing
// terrain and object geometry until chase mode has engaged.
if (_login.IsWaitingForLogin)
return CompleteSkippedWorld();
_passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId);
terrainDrawn = true;
}
else
{
terrainClipMode = TerrainClipMode.Skip;
}
if (clipRoot is not null)
{
pviewFrameStarted = true;
pviewResult = _pview.DrawInside(
new RetailPViewFrameInput
{
RootCell = clipRoot,
NearbyBuildingCells = world.Buildings.NearbyBuildingCells,
ViewerEyePos = roots.ViewerEyePosition,
ViewProjection = camera.ViewProjection,
Cells = _pviewCells,
Camera = camera.Camera,
CameraWorldPosition = camera.Position,
Frustum = camera.Frustum,
PlayerLandblockId = roots.PlayerLandblockId,
AnimatedEntityIds = world.AnimatedEntityIds,
RenderCenterLbX = roots.RenderCenterLandblockX,
RenderCenterLbY = roots.RenderCenterLandblockY,
RenderRadius = _renderRange.NearRadius,
LandblockEntries = _entities.LandblockEntries,
RenderSky = renderSky,
RenderWeather = roots.PlayerSeenOutside,
DayFraction = _sky.DayFraction,
ActiveDayGroup = _sky.ActiveDayGroup,
SkyKeyframe = foundation.Sky,
EnvironOverrideActive = foundation.EnvironOverrideActive,
ViewerCellId = roots.ViewerCellId,
PlayerCellId = roots.PlayerCellId,
PlayerViewPosition = roots.PlayerViewPosition,
CameraView = camera.Camera.View,
CameraCellResolution = _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;
if (pviewResult.Partition.ByCell.Count > 0 || hasOutsideSlice)
sceneParticles = "pviewScoped";
outdoorSceneryDrawn = pviewResult.Partition.OutdoorStatic.Count > 0
&& hasOutsideSlice;
liveDynamicDrawnCount = pviewResult.Partition.Dynamics.Count;
}
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);
WorldRenderFrameOutcome CompleteSkippedWorld()
{
CompleteWorldFrame();
worldFrameStarted = false;
pviewFrameStarted = false;
_selection?.CompleteFrame();
selectionFrameStarted = false;
return default;
}
}
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);
}
}
}
}

View file

@ -0,0 +1,117 @@
using System.Numerics;
using AcDream.App.Input;
using AcDream.App.Streaming;
using AcDream.Core.Physics;
using AcDream.Core.World;
namespace AcDream.App.Rendering;
internal interface IWorldSceneSkyStateSource
{
DayGroupData? ActiveDayGroup { get; }
float DayFraction { get; }
}
/// <summary>
/// Holds the selected retail day group beside the world clock that provides
/// its current interpolation point. Day-group selection remains an update-time
/// concern; render owners receive this read-only seam.
/// </summary>
internal sealed class WorldSceneSkyState : IWorldSceneSkyStateSource
{
private readonly WorldTimeService _worldTime;
public WorldSceneSkyState(WorldTimeService worldTime)
{
_worldTime = worldTime ?? throw new ArgumentNullException(nameof(worldTime));
}
public DayGroupData? ActiveDayGroup { get; set; }
public float DayFraction => (float)_worldTime.DayFraction;
}
internal interface IWorldSceneEntitySource
{
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries
{ get; }
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds { get; }
}
internal sealed class RuntimeWorldSceneEntitySource : IWorldSceneEntitySource
{
private readonly GpuWorldState _world;
public RuntimeWorldSceneEntitySource(GpuWorldState world)
{
_world = world ?? throw new ArgumentNullException(nameof(world));
}
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> LandblockEntries =>
_world.LandblockEntries;
public IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> LandblockBounds =>
_world.LandblockBounds;
}
internal interface IWorldScenePViewDiagnosticSource
{
Vector3 RawPlayerPositionOr(Vector3 fallback);
float PlayerYaw { get; }
float? SampleTerrainZ(float worldX, float worldY);
CameraCellResolution CameraCellResolution { get; }
}
internal sealed class RuntimeWorldScenePViewDiagnosticSource :
IWorldScenePViewDiagnosticSource
{
private readonly ILocalPlayerControllerSource _player;
private readonly PhysicsEngine _physics;
private readonly CellVisibility _visibility;
public RuntimeWorldScenePViewDiagnosticSource(
ILocalPlayerControllerSource player,
PhysicsEngine physics,
CellVisibility visibility)
{
_player = player ?? throw new ArgumentNullException(nameof(player));
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
_visibility = visibility ?? throw new ArgumentNullException(nameof(visibility));
}
public Vector3 RawPlayerPositionOr(Vector3 fallback) =>
_player.Controller?.Position ?? fallback;
public float PlayerYaw => _player.Controller?.Yaw ?? 0f;
public float? SampleTerrainZ(float worldX, float worldY) =>
_physics.SampleTerrainZ(worldX, worldY);
public CameraCellResolution CameraCellResolution =>
_visibility.LastCameraCellResolution;
}
internal interface IWorldSceneDebugStateSource
{
bool CollisionWireframesVisible { get; }
}
internal sealed class WorldSceneDebugState : IWorldSceneDebugStateSource
{
public bool CollisionWireframesVisible { get; private set; }
public bool ToggleCollisionWireframes()
{
CollisionWireframesVisible = !CollisionWireframesVisible;
return CollisionWireframesVisible;
}
}