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:
parent
85239fb373
commit
28e1cf8029
25 changed files with 2679 additions and 844 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
|
|
|
|||
113
src/AcDream.App/Rendering/RenderFrameGlStateController.cs
Normal file
113
src/AcDream.App/Rendering/RenderFrameGlStateController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
274
src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs
Normal file
274
src/AcDream.App/Rendering/WorldSceneDiagnosticsController.cs
Normal 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++;
|
||||
}
|
||||
}
|
||||
328
src/AcDream.App/Rendering/WorldScenePassExecutor.cs
Normal file
328
src/AcDream.App/Rendering/WorldScenePassExecutor.cs
Normal 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;
|
||||
}
|
||||
345
src/AcDream.App/Rendering/WorldSceneRenderer.cs
Normal file
345
src/AcDream.App/Rendering/WorldSceneRenderer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/AcDream.App/Rendering/WorldSceneRuntimeSources.cs
Normal file
117
src/AcDream.App/Rendering/WorldSceneRuntimeSources.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue