feat(rendering): complete current-path render referee
Extend the non-drawing oracle through ordered PView routes, dispatcher visibility and final instance payloads, and accepted retail selection parts. Lifecycle artifacts can now referee the later shadow scene without influencing production visibility or draw decisions.
This commit is contained in:
parent
dbaea19269
commit
f9b68f8f2a
11 changed files with 964 additions and 41 deletions
|
|
@ -16,12 +16,13 @@ retail-faithful gameplay
|
|||
|
||||
| Unit | State | Evidence / rollback |
|
||||
|---|---|---|
|
||||
| F0a — partition-input referee | implemented; Release gate green | Diagnostic-only `CurrentRenderSceneOracle`; 3,685 App tests / 3 skips and 8,169 complete-solution tests / 5 skips. No production draw decision changed. |
|
||||
| F0b — survivor/dispatcher referee | active | Extends the same current-path oracle through PView survivor and dispatcher classification boundaries. |
|
||||
| F1–F5 | pending | No render-scene storage or shadow projection exists yet. |
|
||||
| F0a — partition-input referee | complete | Diagnostic-only `CurrentRenderSceneOracle`; no production draw decision changed. |
|
||||
| F0b — survivor/dispatcher/selection referee | complete | Exact PView routes, dispatcher candidates and final group payloads, material/alpha/clip/light/selection fields, and accepted picking parts. Release gate: 3,690 App tests / 3 skips and 8,174 complete-solution tests / 5 skips. |
|
||||
| F1 — scene types and contained adapter | active | No render-scene storage or shadow projection exists yet. |
|
||||
| F2–F5 | pending | Delta publication and continuous shadow comparison have not started. |
|
||||
| G0–G5 | pending | No production consumer has switched. |
|
||||
|
||||
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0a is
|
||||
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
|
||||
non-drawing diagnostic infrastructure and therefore is not a visual-cutover
|
||||
rollback unit. Each later commit that changes the production draw source is
|
||||
listed in section 10 before it is offered for a connected gate.
|
||||
|
|
|
|||
|
|
@ -374,6 +374,10 @@ internal sealed class FrameRootCompositionPhase
|
|||
&& d.Options.AutomationArtifactDirectory is not null
|
||||
? new CurrentRenderSceneOracle()
|
||||
: null;
|
||||
live.DrawDispatcher.SetCurrentRenderSceneObserver(
|
||||
currentRenderSceneOracle);
|
||||
live.SelectionScene.SetCurrentRenderSceneObserver(
|
||||
currentRenderSceneOracle);
|
||||
var worldSceneRenderer = new WorldSceneRenderer(
|
||||
renderFrameResources,
|
||||
renderLoginState,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
|
@ -13,6 +14,7 @@ namespace AcDream.App.Rendering;
|
|||
public sealed class RetailPViewRenderer
|
||||
{
|
||||
private readonly InteriorEntityPartition.IObserver? _partitionObserver;
|
||||
private readonly ICurrentRenderPViewObserver? _candidateObserver;
|
||||
private readonly PortalVisibilityFrame _mainPortalFrameScratch = new();
|
||||
private readonly ClipFrameAssembly _clipAssemblyScratch = new();
|
||||
private readonly ViewconeCuller _viewconeScratch = new();
|
||||
|
|
@ -72,6 +74,7 @@ public sealed class RetailPViewRenderer
|
|||
InteriorEntityPartition.IObserver? partitionObserver)
|
||||
{
|
||||
_partitionObserver = partitionObserver;
|
||||
_candidateObserver = partitionObserver as ICurrentRenderPViewObserver;
|
||||
}
|
||||
|
||||
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
||||
|
|
@ -198,41 +201,51 @@ public sealed class RetailPViewRenderer
|
|||
ctx.ViewProjection,
|
||||
_viewconeScratch);
|
||||
|
||||
// #118: stage assignment for dynamics under an INTERIOR root. Retail
|
||||
// draws the OUTSIDE world's objects inside the landscape stage —
|
||||
// PView::DrawCells runs LScape::draw FIRST (pc:432719), then the gated
|
||||
// full depth clear (pc:432731-432732) and the exit-portal SEALS
|
||||
// (pc:432785-432786); DrawBlock draws every landcell's objects via
|
||||
// DrawSortCell (0x005a17c0, pc:430124). A dynamic deferred to our
|
||||
// single last pass instead z-fails against the seal's true-depth stamp
|
||||
// the moment it stands beyond the door plane — the house-exit
|
||||
// clip+vanish (pinned by HouseExitWalkReplayTests). So under an
|
||||
// interior root: outdoor-classified dynamics draw in the outside
|
||||
// stage; an indoor dynamic whose sphere STRADDLES an exit portal
|
||||
// draws in BOTH stages (retail's per-overlapped-cell shadow-part
|
||||
// draw, DrawBlock pc:430056-430064) so neither body half clips at the
|
||||
// plane. Outdoor roots keep ALL dynamics in the last pass — our
|
||||
// z-buffered equivalent of retail's painter-ordered outdoor pass (the
|
||||
// BR-2 punch-after-dynamics lesson, reverted 88be519).
|
||||
_outsideStageDynamics.Clear();
|
||||
if (!ctx.RootCell.IsOutdoorNode)
|
||||
_candidateObserver?.BeginPViewFrame();
|
||||
try
|
||||
{
|
||||
foreach (var e in partition.Dynamics)
|
||||
// #118: stage assignment for dynamics under an INTERIOR root. Retail
|
||||
// draws the OUTSIDE world's objects inside the landscape stage —
|
||||
// PView::DrawCells runs LScape::draw FIRST (pc:432719), then the gated
|
||||
// full depth clear (pc:432731-432732) and the exit-portal SEALS
|
||||
// (pc:432785-432786); DrawBlock draws every landcell's objects via
|
||||
// DrawSortCell (0x005a17c0, pc:430124). A dynamic deferred to our
|
||||
// single last pass instead z-fails against the seal's true-depth stamp
|
||||
// the moment it stands beyond the door plane — the house-exit
|
||||
// clip+vanish (pinned by HouseExitWalkReplayTests). So under an
|
||||
// interior root: outdoor-classified dynamics draw in the outside
|
||||
// stage; an indoor dynamic whose sphere STRADDLES an exit portal
|
||||
// draws in BOTH stages (retail's per-overlapped-cell shadow-part
|
||||
// draw, DrawBlock pc:430056-430064) so neither body half clips at the
|
||||
// plane. Outdoor roots keep ALL dynamics in the last pass — our
|
||||
// z-buffered equivalent of retail's painter-ordered outdoor pass (the
|
||||
// BR-2 punch-after-dynamics lesson, reverted 88be519).
|
||||
_outsideStageDynamics.Clear();
|
||||
if (!ctx.RootCell.IsOutdoorNode)
|
||||
{
|
||||
EntitySphere(e, out var c, out float r);
|
||||
if (DynamicDrawsInOutsideStage(e.ParentCellId, c, r, drawableCells, ctx.Cells))
|
||||
_outsideStageDynamics.Add(e);
|
||||
foreach (var e in partition.Dynamics)
|
||||
{
|
||||
EntitySphere(e, out var c, out float r);
|
||||
if (DynamicDrawsInOutsideStage(e.ParentCellId, c, r, drawableCells, ctx.Cells))
|
||||
_outsideStageDynamics.Add(e);
|
||||
}
|
||||
}
|
||||
|
||||
DrawLandscapeThroughOutsideView(ctx, passes, clipAssembly, partition, viewcone);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells);
|
||||
DrawEnvCellShells(passes, pvFrame);
|
||||
DrawCellObjectLists(ctx, passes, pvFrame, clipAssembly, drawableCells, partition, viewcone);
|
||||
DrawDynamicsLast(ctx, passes, partition, viewcone, ctx.RootCell.IsOutdoorNode);
|
||||
|
||||
_candidateObserver?.CompletePViewFrame();
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_candidateObserver?.AbortPViewFrame();
|
||||
throw;
|
||||
}
|
||||
|
||||
DrawLandscapeThroughOutsideView(ctx, passes, clipAssembly, partition, viewcone);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells);
|
||||
DrawEnvCellShells(passes, pvFrame);
|
||||
DrawCellObjectLists(ctx, passes, pvFrame, clipAssembly, drawableCells, partition, viewcone);
|
||||
DrawDynamicsLast(ctx, passes, partition, viewcone, ctx.RootCell.IsOutdoorNode);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// R-A2: group the nearby building cells by BuildingId and run one per-building flood per group
|
||||
|
|
@ -435,6 +448,11 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
if (_cellStaticScratch.Count > 0)
|
||||
{
|
||||
_candidateObserver?.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.LookInObject,
|
||||
i,
|
||||
cellId,
|
||||
_cellStaticScratch);
|
||||
passes.DrawEntityBucket(ctx, _cellStaticScratch, _oneCell);
|
||||
|
||||
// The cell-particles pass for look-in cells — retail's
|
||||
|
|
@ -489,6 +507,11 @@ public sealed class RetailPViewRenderer
|
|||
if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r))
|
||||
_outdoorStaticScratch.Add(e);
|
||||
}
|
||||
_candidateObserver?.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.LandscapeOutdoorStatic,
|
||||
probeSliceIndex,
|
||||
0,
|
||||
_outdoorStaticScratch);
|
||||
probeSliceIndex++;
|
||||
passes.DrawLandscapeSlice(ctx, new RetailPViewLandscapeSliceContext(slice, _outdoorStaticScratch));
|
||||
}
|
||||
|
|
@ -541,6 +564,11 @@ public sealed class RetailPViewRenderer
|
|||
probeSliceIndex,
|
||||
_outsideStageDynamics,
|
||||
viewcone);
|
||||
_candidateObserver?.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.LandscapeOutsideDynamic,
|
||||
probeSliceIndex,
|
||||
0,
|
||||
_outdoorStaticScratch);
|
||||
probeSliceIndex++;
|
||||
passes.DrawLandscapeSliceLate(ctx, new RetailPViewLandscapeLateSliceContext(
|
||||
slice, _outdoorStaticScratch, _lateParticleOwnerScratch));
|
||||
|
|
@ -693,6 +721,11 @@ public sealed class RetailPViewRenderer
|
|||
if (_dynamicsScratch.Count == 0)
|
||||
return;
|
||||
|
||||
_candidateObserver?.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.DynamicLast,
|
||||
0,
|
||||
0,
|
||||
_dynamicsScratch);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
passes.DrawEntityBucket(ctx, _dynamicsScratch, visibleCellIds: null);
|
||||
|
||||
|
|
@ -779,6 +812,11 @@ public sealed class RetailPViewRenderer
|
|||
// cull; only the draw is batched).
|
||||
if (_allCellStatics.Count > 0)
|
||||
{
|
||||
_candidateObserver?.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.CellStatic,
|
||||
0,
|
||||
0,
|
||||
_allCellStatics);
|
||||
passes.UseIndoorMembershipOnlyRouting();
|
||||
passes.DrawEntityBucket(ctx, _allCellStatics, _cellObjCells);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene;
|
||||
|
|
@ -25,6 +27,48 @@ internal readonly record struct CurrentRenderProjectionFingerprint(
|
|||
RenderSceneHash128 Geometry,
|
||||
RenderSceneHash128 Appearance);
|
||||
|
||||
internal enum CurrentRenderPViewRoute : byte
|
||||
{
|
||||
LandscapeOutdoorStatic,
|
||||
LandscapeOutsideDynamic,
|
||||
LookInObject,
|
||||
CellStatic,
|
||||
DynamicLast,
|
||||
}
|
||||
|
||||
internal readonly record struct CurrentRenderPViewCandidateFingerprint(
|
||||
int Sequence,
|
||||
CurrentRenderPViewRoute Route,
|
||||
int RouteIndex,
|
||||
uint CellId,
|
||||
CurrentRenderProjectionFingerprint Projection);
|
||||
|
||||
internal readonly record struct CurrentRenderDispatcherFingerprint(
|
||||
int Sequence,
|
||||
int DrawSequence,
|
||||
WbDrawDispatcher.EntitySet Set,
|
||||
int MeshRefIndex,
|
||||
uint TupleLandblockId,
|
||||
uint CacheLandblockId,
|
||||
CurrentRenderProjectionFingerprint Projection);
|
||||
|
||||
internal readonly record struct CurrentRenderDispatcherSubmission(
|
||||
int VisibleInstanceCount,
|
||||
int ImmediateInstanceCount,
|
||||
int OpaqueGroupCount,
|
||||
int TransparentGroupCount,
|
||||
bool TransparentDeferred,
|
||||
RenderSceneHash128 Digest);
|
||||
|
||||
internal readonly record struct CurrentRenderSelectionFingerprint(
|
||||
int Sequence,
|
||||
uint ServerGuid,
|
||||
uint LocalEntityId,
|
||||
int PartIndex,
|
||||
uint GfxObjId,
|
||||
Matrix4x4 LocalToWorld,
|
||||
RenderSceneHash128 Geometry);
|
||||
|
||||
internal readonly record struct CurrentRenderSceneOracleSnapshot(
|
||||
bool Enabled,
|
||||
ulong CompletedFrameSequence,
|
||||
|
|
@ -34,7 +78,24 @@ internal readonly record struct CurrentRenderSceneOracleSnapshot(
|
|||
int CellStaticCount,
|
||||
int DynamicCount,
|
||||
int CellBucketCount,
|
||||
RenderSceneHash128 Digest)
|
||||
RenderSceneHash128 Digest,
|
||||
ulong CompletedPViewFrameSequence,
|
||||
int AbortedPViewFrames,
|
||||
int PViewCandidateCount,
|
||||
RenderSceneHash128 PViewDigest,
|
||||
ulong DispatcherFrameSequence,
|
||||
int AbortedDispatcherFrames,
|
||||
int DispatcherDrawCount,
|
||||
int DispatcherEntityCount,
|
||||
int DispatcherMeshRefCount,
|
||||
int DispatcherInstanceCount,
|
||||
int DispatcherOpaqueGroupCount,
|
||||
int DispatcherTransparentGroupCount,
|
||||
RenderSceneHash128 DispatcherDigest,
|
||||
ulong CompletedSelectionFrameSequence,
|
||||
int AbortedSelectionFrames,
|
||||
int SelectionPartCount,
|
||||
RenderSceneHash128 SelectionDigest)
|
||||
{
|
||||
public static CurrentRenderSceneOracleSnapshot Disabled { get; } = new(
|
||||
Enabled: false,
|
||||
|
|
@ -45,7 +106,24 @@ internal readonly record struct CurrentRenderSceneOracleSnapshot(
|
|||
CellStaticCount: 0,
|
||||
DynamicCount: 0,
|
||||
CellBucketCount: 0,
|
||||
Digest: RenderSceneHash128.Empty);
|
||||
Digest: RenderSceneHash128.Empty,
|
||||
CompletedPViewFrameSequence: 0,
|
||||
AbortedPViewFrames: 0,
|
||||
PViewCandidateCount: 0,
|
||||
PViewDigest: RenderSceneHash128.Empty,
|
||||
DispatcherFrameSequence: 0,
|
||||
AbortedDispatcherFrames: 0,
|
||||
DispatcherDrawCount: 0,
|
||||
DispatcherEntityCount: 0,
|
||||
DispatcherMeshRefCount: 0,
|
||||
DispatcherInstanceCount: 0,
|
||||
DispatcherOpaqueGroupCount: 0,
|
||||
DispatcherTransparentGroupCount: 0,
|
||||
DispatcherDigest: RenderSceneHash128.Empty,
|
||||
CompletedSelectionFrameSequence: 0,
|
||||
AbortedSelectionFrames: 0,
|
||||
SelectionPartCount: 0,
|
||||
SelectionDigest: RenderSceneHash128.Empty);
|
||||
}
|
||||
|
||||
internal interface ICurrentRenderSceneOracleSnapshotSource
|
||||
|
|
@ -53,6 +131,56 @@ internal interface ICurrentRenderSceneOracleSnapshotSource
|
|||
CurrentRenderSceneOracleSnapshot Snapshot { get; }
|
||||
}
|
||||
|
||||
internal interface ICurrentRenderPViewObserver
|
||||
{
|
||||
void BeginPViewFrame();
|
||||
|
||||
void ObservePViewBucket(
|
||||
CurrentRenderPViewRoute route,
|
||||
int routeIndex,
|
||||
uint cellId,
|
||||
IReadOnlyList<WorldEntity> entities);
|
||||
|
||||
void CompletePViewFrame();
|
||||
|
||||
void AbortPViewFrame();
|
||||
}
|
||||
|
||||
internal interface ICurrentRenderDispatcherObserver
|
||||
{
|
||||
void BeginDispatcherFrame();
|
||||
|
||||
void ObserveDispatcherDraw(
|
||||
WbDrawDispatcher.EntitySet set,
|
||||
int entitiesWalked,
|
||||
IReadOnlyList<(
|
||||
WorldEntity Entity,
|
||||
int MeshRefIndex,
|
||||
uint LandblockId)> accepted);
|
||||
|
||||
void ObserveDispatcherSubmission(
|
||||
in CurrentRenderDispatcherSubmission submission);
|
||||
|
||||
void AbortDispatcherFrame();
|
||||
}
|
||||
|
||||
internal interface ICurrentRenderSelectionObserver
|
||||
{
|
||||
void BeginSelectionFrame();
|
||||
|
||||
void ObserveSelectionPart(
|
||||
uint serverGuid,
|
||||
uint localEntityId,
|
||||
int partIndex,
|
||||
uint gfxObjId,
|
||||
Matrix4x4 localToWorld,
|
||||
RetailSelectionMesh mesh);
|
||||
|
||||
void CompleteSelectionFrame();
|
||||
|
||||
void AbortSelectionFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic fingerprint of the already-accepted
|
||||
/// <see cref="InteriorEntityPartition"/> output. This is the Slice-F referee:
|
||||
|
|
@ -65,21 +193,59 @@ internal interface ICurrentRenderSceneOracleSnapshotSource
|
|||
/// </summary>
|
||||
internal sealed class CurrentRenderSceneOracle :
|
||||
InteriorEntityPartition.IObserver,
|
||||
ICurrentRenderPViewObserver,
|
||||
ICurrentRenderDispatcherObserver,
|
||||
ICurrentRenderSelectionObserver,
|
||||
ICurrentRenderSceneOracleSnapshotSource
|
||||
{
|
||||
private readonly List<CurrentRenderProjectionFingerprint> _projections = [];
|
||||
private readonly Dictionary<WorldEntity, CurrentRenderProjectionFingerprint>
|
||||
_projectionByEntity =
|
||||
new(ReferenceEqualityComparer.Instance);
|
||||
private readonly List<CurrentRenderPViewCandidateFingerprint>
|
||||
_pviewCandidates = [];
|
||||
private readonly List<CurrentRenderDispatcherFingerprint>
|
||||
_dispatcherCandidates = [];
|
||||
private readonly Dictionary<WorldEntity, CurrentRenderProjectionFingerprint>
|
||||
_dispatcherProjectionByEntity =
|
||||
new(ReferenceEqualityComparer.Instance);
|
||||
private readonly List<CurrentRenderSelectionFingerprint>
|
||||
_selectionParts = [];
|
||||
private ulong _frameSequence;
|
||||
private int _abortedFrames;
|
||||
private int _outdoorStaticCount;
|
||||
private int _cellStaticCount;
|
||||
private int _dynamicCount;
|
||||
private bool _frameOpen;
|
||||
private ulong _pviewFrameSequence;
|
||||
private int _abortedPViewFrames;
|
||||
private bool _pviewFrameOpen;
|
||||
private StableRenderHash128 _pviewHash;
|
||||
private ulong _dispatcherFrameSequence;
|
||||
private int _abortedDispatcherFrames;
|
||||
private int _dispatcherDrawCount;
|
||||
private int _dispatcherEntityCount;
|
||||
private int _dispatcherInstanceCount;
|
||||
private int _dispatcherOpaqueGroupCount;
|
||||
private int _dispatcherTransparentGroupCount;
|
||||
private bool _dispatcherFrameOpen;
|
||||
private StableRenderHash128 _dispatcherHash;
|
||||
private ulong _selectionFrameSequence;
|
||||
private int _abortedSelectionFrames;
|
||||
private bool _selectionFrameOpen;
|
||||
private StableRenderHash128 _selectionHash;
|
||||
|
||||
public CurrentRenderSceneOracleSnapshot Snapshot { get; private set; } =
|
||||
CurrentRenderSceneOracleSnapshot.Disabled with { Enabled = true };
|
||||
|
||||
internal IReadOnlyList<CurrentRenderProjectionFingerprint> Projections =>
|
||||
_projections;
|
||||
internal IReadOnlyList<CurrentRenderPViewCandidateFingerprint>
|
||||
PViewCandidates => _pviewCandidates;
|
||||
internal IReadOnlyList<CurrentRenderDispatcherFingerprint>
|
||||
DispatcherCandidates => _dispatcherCandidates;
|
||||
internal IReadOnlyList<CurrentRenderSelectionFingerprint>
|
||||
SelectionParts => _selectionParts;
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
|
|
@ -91,6 +257,7 @@ internal sealed class CurrentRenderSceneOracle :
|
|||
|
||||
_frameOpen = true;
|
||||
_projections.Clear();
|
||||
_projectionByEntity.Clear();
|
||||
_outdoorStaticCount = 0;
|
||||
_cellStaticCount = 0;
|
||||
_dynamicCount = 0;
|
||||
|
|
@ -126,10 +293,16 @@ internal sealed class CurrentRenderSceneOracle :
|
|||
"Unknown current render projection class.");
|
||||
}
|
||||
|
||||
_projections.Add(CreateFingerprint(
|
||||
CurrentRenderProjectionFingerprint fingerprint = CreateFingerprint(
|
||||
landblockId,
|
||||
entity,
|
||||
projectionClass));
|
||||
projectionClass);
|
||||
_projections.Add(fingerprint);
|
||||
if (!_projectionByEntity.TryAdd(entity, fingerprint))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The current render partition observed entity 0x{entity.Id:X8} more than once.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Complete(InteriorEntityPartition.Result result)
|
||||
|
|
@ -170,7 +343,26 @@ internal sealed class CurrentRenderSceneOracle :
|
|||
CellStaticCount: _cellStaticCount,
|
||||
DynamicCount: _dynamicCount,
|
||||
CellBucketCount: result.ByCell.Count,
|
||||
Digest: aggregate.Finish());
|
||||
Digest: aggregate.Finish(),
|
||||
CompletedPViewFrameSequence: Snapshot.CompletedPViewFrameSequence,
|
||||
AbortedPViewFrames: _abortedPViewFrames,
|
||||
PViewCandidateCount: Snapshot.PViewCandidateCount,
|
||||
PViewDigest: Snapshot.PViewDigest,
|
||||
DispatcherFrameSequence: Snapshot.DispatcherFrameSequence,
|
||||
AbortedDispatcherFrames: _abortedDispatcherFrames,
|
||||
DispatcherDrawCount: Snapshot.DispatcherDrawCount,
|
||||
DispatcherEntityCount: Snapshot.DispatcherEntityCount,
|
||||
DispatcherMeshRefCount: Snapshot.DispatcherMeshRefCount,
|
||||
DispatcherInstanceCount: Snapshot.DispatcherInstanceCount,
|
||||
DispatcherOpaqueGroupCount: Snapshot.DispatcherOpaqueGroupCount,
|
||||
DispatcherTransparentGroupCount:
|
||||
Snapshot.DispatcherTransparentGroupCount,
|
||||
DispatcherDigest: Snapshot.DispatcherDigest,
|
||||
CompletedSelectionFrameSequence:
|
||||
Snapshot.CompletedSelectionFrameSequence,
|
||||
AbortedSelectionFrames: _abortedSelectionFrames,
|
||||
SelectionPartCount: Snapshot.SelectionPartCount,
|
||||
SelectionDigest: Snapshot.SelectionDigest);
|
||||
}
|
||||
|
||||
public void AbortFrame()
|
||||
|
|
@ -187,6 +379,328 @@ internal sealed class CurrentRenderSceneOracle :
|
|||
Snapshot = Snapshot with { AbortedFrames = _abortedFrames };
|
||||
}
|
||||
|
||||
public void BeginPViewFrame()
|
||||
{
|
||||
if (_pviewFrameOpen)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The current render-scene oracle cannot begin a second PView frame before completing or aborting the first.");
|
||||
}
|
||||
|
||||
_pviewFrameOpen = true;
|
||||
_pviewCandidates.Clear();
|
||||
_pviewHash = StableRenderHash128.Create();
|
||||
}
|
||||
|
||||
public void ObservePViewBucket(
|
||||
CurrentRenderPViewRoute route,
|
||||
int routeIndex,
|
||||
uint cellId,
|
||||
IReadOnlyList<WorldEntity> entities)
|
||||
{
|
||||
if (!_pviewFrameOpen)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The current render-scene oracle received a PView bucket outside an open frame.");
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(entities);
|
||||
|
||||
_pviewHash.Add((byte)0xB1);
|
||||
_pviewHash.Add((byte)route);
|
||||
_pviewHash.Add(routeIndex);
|
||||
_pviewHash.Add(cellId);
|
||||
_pviewHash.Add(entities.Count);
|
||||
foreach (WorldEntity entity in entities)
|
||||
{
|
||||
if (!_projectionByEntity.TryGetValue(entity, out var projection))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"PView routed entity 0x{entity.Id:X8} that was absent from the current partition.");
|
||||
}
|
||||
|
||||
var candidate = new CurrentRenderPViewCandidateFingerprint(
|
||||
Sequence: _pviewCandidates.Count,
|
||||
Route: route,
|
||||
RouteIndex: routeIndex,
|
||||
CellId: cellId,
|
||||
Projection: projection);
|
||||
_pviewCandidates.Add(candidate);
|
||||
AddPViewCandidate(ref _pviewHash, in candidate);
|
||||
}
|
||||
}
|
||||
|
||||
public void CompletePViewFrame()
|
||||
{
|
||||
if (!_pviewFrameOpen)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The current render-scene oracle cannot complete PView without an open frame.");
|
||||
}
|
||||
|
||||
_pviewFrameOpen = false;
|
||||
Snapshot = Snapshot with
|
||||
{
|
||||
CompletedPViewFrameSequence = checked(++_pviewFrameSequence),
|
||||
AbortedPViewFrames = _abortedPViewFrames,
|
||||
PViewCandidateCount = _pviewCandidates.Count,
|
||||
PViewDigest = _pviewHash.Finish(),
|
||||
};
|
||||
}
|
||||
|
||||
public void AbortPViewFrame()
|
||||
{
|
||||
if (!_pviewFrameOpen)
|
||||
return;
|
||||
|
||||
_pviewFrameOpen = false;
|
||||
_pviewCandidates.Clear();
|
||||
_abortedPViewFrames = checked(_abortedPViewFrames + 1);
|
||||
Snapshot = Snapshot with
|
||||
{
|
||||
AbortedPViewFrames = _abortedPViewFrames,
|
||||
};
|
||||
}
|
||||
|
||||
public void BeginDispatcherFrame()
|
||||
{
|
||||
_dispatcherFrameOpen = true;
|
||||
_dispatcherCandidates.Clear();
|
||||
_dispatcherProjectionByEntity.Clear();
|
||||
_dispatcherDrawCount = 0;
|
||||
_dispatcherEntityCount = 0;
|
||||
_dispatcherInstanceCount = 0;
|
||||
_dispatcherOpaqueGroupCount = 0;
|
||||
_dispatcherTransparentGroupCount = 0;
|
||||
_dispatcherHash = StableRenderHash128.Create();
|
||||
Snapshot = Snapshot with
|
||||
{
|
||||
DispatcherFrameSequence = checked(++_dispatcherFrameSequence),
|
||||
AbortedDispatcherFrames = _abortedDispatcherFrames,
|
||||
DispatcherDrawCount = 0,
|
||||
DispatcherEntityCount = 0,
|
||||
DispatcherMeshRefCount = 0,
|
||||
DispatcherInstanceCount = 0,
|
||||
DispatcherOpaqueGroupCount = 0,
|
||||
DispatcherTransparentGroupCount = 0,
|
||||
DispatcherDigest = _dispatcherHash.Finish(),
|
||||
};
|
||||
}
|
||||
|
||||
public void ObserveDispatcherDraw(
|
||||
WbDrawDispatcher.EntitySet set,
|
||||
int entitiesWalked,
|
||||
IReadOnlyList<(
|
||||
WorldEntity Entity,
|
||||
int MeshRefIndex,
|
||||
uint LandblockId)> accepted)
|
||||
{
|
||||
if (!_dispatcherFrameOpen)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The current render-scene oracle received dispatcher input outside an open frame.");
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(accepted);
|
||||
|
||||
int drawSequence = _dispatcherDrawCount++;
|
||||
_dispatcherEntityCount = checked(_dispatcherEntityCount + entitiesWalked);
|
||||
_dispatcherHash.Add((byte)0xD1);
|
||||
_dispatcherHash.Add(drawSequence);
|
||||
_dispatcherHash.Add((int)set);
|
||||
_dispatcherHash.Add(entitiesWalked);
|
||||
_dispatcherHash.Add(accepted.Count);
|
||||
|
||||
foreach (var tuple in accepted)
|
||||
{
|
||||
uint cacheLandblockId =
|
||||
WbDrawDispatcher.ResolveCacheLandblockHint(
|
||||
tuple.Entity,
|
||||
tuple.LandblockId);
|
||||
if (!_dispatcherProjectionByEntity.TryGetValue(
|
||||
tuple.Entity,
|
||||
out CurrentRenderProjectionFingerprint projection)
|
||||
|| projection.LandblockId != cacheLandblockId)
|
||||
{
|
||||
projection = CreateFingerprint(
|
||||
cacheLandblockId,
|
||||
tuple.Entity,
|
||||
ProjectionClassOf(tuple.Entity));
|
||||
_dispatcherProjectionByEntity[tuple.Entity] = projection;
|
||||
}
|
||||
var candidate = new CurrentRenderDispatcherFingerprint(
|
||||
Sequence: _dispatcherCandidates.Count,
|
||||
DrawSequence: drawSequence,
|
||||
Set: set,
|
||||
MeshRefIndex: tuple.MeshRefIndex,
|
||||
TupleLandblockId: tuple.LandblockId,
|
||||
CacheLandblockId: cacheLandblockId,
|
||||
Projection: projection);
|
||||
_dispatcherCandidates.Add(candidate);
|
||||
AddDispatcherCandidate(ref _dispatcherHash, in candidate);
|
||||
}
|
||||
|
||||
Snapshot = Snapshot with
|
||||
{
|
||||
DispatcherFrameSequence = _dispatcherFrameSequence,
|
||||
AbortedDispatcherFrames = _abortedDispatcherFrames,
|
||||
DispatcherDrawCount = _dispatcherDrawCount,
|
||||
DispatcherEntityCount = _dispatcherEntityCount,
|
||||
DispatcherMeshRefCount = _dispatcherCandidates.Count,
|
||||
DispatcherInstanceCount = _dispatcherInstanceCount,
|
||||
DispatcherOpaqueGroupCount = _dispatcherOpaqueGroupCount,
|
||||
DispatcherTransparentGroupCount =
|
||||
_dispatcherTransparentGroupCount,
|
||||
DispatcherDigest = _dispatcherHash.Finish(),
|
||||
};
|
||||
}
|
||||
|
||||
public void ObserveDispatcherSubmission(
|
||||
in CurrentRenderDispatcherSubmission submission)
|
||||
{
|
||||
if (!_dispatcherFrameOpen)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The current render-scene oracle received dispatcher submission outside an open frame.");
|
||||
}
|
||||
if (_dispatcherDrawCount == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A dispatcher submission cannot precede its accepted candidate set.");
|
||||
}
|
||||
|
||||
_dispatcherInstanceCount = checked(
|
||||
_dispatcherInstanceCount + submission.VisibleInstanceCount);
|
||||
_dispatcherOpaqueGroupCount = checked(
|
||||
_dispatcherOpaqueGroupCount + submission.OpaqueGroupCount);
|
||||
_dispatcherTransparentGroupCount = checked(
|
||||
_dispatcherTransparentGroupCount
|
||||
+ submission.TransparentGroupCount);
|
||||
_dispatcherHash.Add((byte)0xD2);
|
||||
_dispatcherHash.Add(_dispatcherDrawCount - 1);
|
||||
_dispatcherHash.Add(submission.VisibleInstanceCount);
|
||||
_dispatcherHash.Add(submission.ImmediateInstanceCount);
|
||||
_dispatcherHash.Add(submission.OpaqueGroupCount);
|
||||
_dispatcherHash.Add(submission.TransparentGroupCount);
|
||||
_dispatcherHash.Add(submission.TransparentDeferred);
|
||||
_dispatcherHash.Add(submission.Digest.Low);
|
||||
_dispatcherHash.Add(submission.Digest.High);
|
||||
|
||||
Snapshot = Snapshot with
|
||||
{
|
||||
DispatcherInstanceCount = _dispatcherInstanceCount,
|
||||
DispatcherOpaqueGroupCount = _dispatcherOpaqueGroupCount,
|
||||
DispatcherTransparentGroupCount =
|
||||
_dispatcherTransparentGroupCount,
|
||||
DispatcherDigest = _dispatcherHash.Finish(),
|
||||
};
|
||||
}
|
||||
|
||||
public void AbortDispatcherFrame()
|
||||
{
|
||||
if (!_dispatcherFrameOpen)
|
||||
return;
|
||||
|
||||
_dispatcherFrameOpen = false;
|
||||
_dispatcherCandidates.Clear();
|
||||
_dispatcherProjectionByEntity.Clear();
|
||||
_abortedDispatcherFrames = checked(_abortedDispatcherFrames + 1);
|
||||
Snapshot = Snapshot with
|
||||
{
|
||||
AbortedDispatcherFrames = _abortedDispatcherFrames,
|
||||
};
|
||||
}
|
||||
|
||||
public void BeginSelectionFrame()
|
||||
{
|
||||
if (_selectionFrameOpen)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The current render-scene oracle cannot begin a second selection frame before completing or aborting the first.");
|
||||
}
|
||||
|
||||
_selectionFrameOpen = true;
|
||||
_selectionParts.Clear();
|
||||
_selectionHash = StableRenderHash128.Create();
|
||||
}
|
||||
|
||||
public void ObserveSelectionPart(
|
||||
uint serverGuid,
|
||||
uint localEntityId,
|
||||
int partIndex,
|
||||
uint gfxObjId,
|
||||
Matrix4x4 localToWorld,
|
||||
RetailSelectionMesh mesh)
|
||||
{
|
||||
if (!_selectionFrameOpen)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The current render-scene oracle received a selection part outside an open frame.");
|
||||
}
|
||||
ArgumentNullException.ThrowIfNull(mesh);
|
||||
|
||||
StableRenderHash128 geometry = StableRenderHash128.Create();
|
||||
geometry.Add(mesh.SphereCenter);
|
||||
geometry.Add(mesh.SphereRadius);
|
||||
geometry.Add(mesh.Polygons.Count);
|
||||
foreach (RetailSelectionPolygon polygon in mesh.Polygons)
|
||||
{
|
||||
geometry.Add(polygon.SingleSided);
|
||||
geometry.Add(polygon.Vertices.Count);
|
||||
foreach (Vector3 vertex in polygon.Vertices)
|
||||
geometry.Add(vertex);
|
||||
}
|
||||
|
||||
var fingerprint = new CurrentRenderSelectionFingerprint(
|
||||
Sequence: _selectionParts.Count,
|
||||
ServerGuid: serverGuid,
|
||||
LocalEntityId: localEntityId,
|
||||
PartIndex: partIndex,
|
||||
GfxObjId: gfxObjId,
|
||||
LocalToWorld: localToWorld,
|
||||
Geometry: geometry.Finish());
|
||||
_selectionParts.Add(fingerprint);
|
||||
_selectionHash.Add(fingerprint.Sequence);
|
||||
_selectionHash.Add(fingerprint.ServerGuid);
|
||||
_selectionHash.Add(fingerprint.LocalEntityId);
|
||||
_selectionHash.Add(fingerprint.PartIndex);
|
||||
_selectionHash.Add(fingerprint.GfxObjId);
|
||||
_selectionHash.Add(fingerprint.LocalToWorld);
|
||||
_selectionHash.Add(fingerprint.Geometry.Low);
|
||||
_selectionHash.Add(fingerprint.Geometry.High);
|
||||
}
|
||||
|
||||
public void CompleteSelectionFrame()
|
||||
{
|
||||
if (!_selectionFrameOpen)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The current render-scene oracle cannot complete selection without an open frame.");
|
||||
}
|
||||
|
||||
_selectionFrameOpen = false;
|
||||
Snapshot = Snapshot with
|
||||
{
|
||||
CompletedSelectionFrameSequence =
|
||||
checked(++_selectionFrameSequence),
|
||||
AbortedSelectionFrames = _abortedSelectionFrames,
|
||||
SelectionPartCount = _selectionParts.Count,
|
||||
SelectionDigest = _selectionHash.Finish(),
|
||||
};
|
||||
}
|
||||
|
||||
public void AbortSelectionFrame()
|
||||
{
|
||||
if (!_selectionFrameOpen)
|
||||
return;
|
||||
|
||||
_selectionFrameOpen = false;
|
||||
_selectionParts.Clear();
|
||||
_abortedSelectionFrames = checked(_abortedSelectionFrames + 1);
|
||||
Snapshot = Snapshot with
|
||||
{
|
||||
AbortedSelectionFrames = _abortedSelectionFrames,
|
||||
};
|
||||
}
|
||||
|
||||
private static CurrentRenderProjectionFingerprint CreateFingerprint(
|
||||
uint landblockId,
|
||||
WorldEntity entity,
|
||||
|
|
@ -261,6 +775,40 @@ internal sealed class CurrentRenderSceneOracle :
|
|||
Appearance: appearance.Finish());
|
||||
}
|
||||
|
||||
private static InteriorEntityPartition.ProjectionClass ProjectionClassOf(
|
||||
WorldEntity entity) =>
|
||||
entity.ServerGuid != 0
|
||||
? InteriorEntityPartition.ProjectionClass.Dynamic
|
||||
: InteriorEntityPartition.IsIndoorCellId(entity.ParentCellId)
|
||||
? InteriorEntityPartition.ProjectionClass.CellStatic
|
||||
: InteriorEntityPartition.ProjectionClass.OutdoorStatic;
|
||||
|
||||
private static void AddPViewCandidate(
|
||||
ref StableRenderHash128 hash,
|
||||
in CurrentRenderPViewCandidateFingerprint candidate)
|
||||
{
|
||||
hash.Add(candidate.Sequence);
|
||||
hash.Add((byte)candidate.Route);
|
||||
hash.Add(candidate.RouteIndex);
|
||||
hash.Add(candidate.CellId);
|
||||
CurrentRenderProjectionFingerprint projection = candidate.Projection;
|
||||
AddProjection(ref hash, in projection);
|
||||
}
|
||||
|
||||
private static void AddDispatcherCandidate(
|
||||
ref StableRenderHash128 hash,
|
||||
in CurrentRenderDispatcherFingerprint candidate)
|
||||
{
|
||||
hash.Add(candidate.Sequence);
|
||||
hash.Add(candidate.DrawSequence);
|
||||
hash.Add((int)candidate.Set);
|
||||
hash.Add(candidate.MeshRefIndex);
|
||||
hash.Add(candidate.TupleLandblockId);
|
||||
hash.Add(candidate.CacheLandblockId);
|
||||
CurrentRenderProjectionFingerprint projection = candidate.Projection;
|
||||
AddProjection(ref hash, in projection);
|
||||
}
|
||||
|
||||
private static void AddProjection(
|
||||
ref StableRenderHash128 hash,
|
||||
in CurrentRenderProjectionFingerprint projection)
|
||||
|
|
@ -371,6 +919,12 @@ internal struct StableRenderHash128
|
|||
Add(value.Z);
|
||||
}
|
||||
|
||||
public void Add(Vector2 value)
|
||||
{
|
||||
Add(value.X);
|
||||
Add(value.Y);
|
||||
}
|
||||
|
||||
public void Add(Quaternion value)
|
||||
{
|
||||
Add(value.X);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ internal sealed class RetailSelectionScene :
|
|||
private List<RetailSelectionPart> _published = new();
|
||||
private readonly HashSet<PartKey> _buildingKeys = new();
|
||||
private FrustumPlanes? _viewFrustum;
|
||||
private ICurrentRenderSelectionObserver? _currentRenderSceneObserver;
|
||||
|
||||
private readonly record struct PartKey(uint LocalEntityId, int PartIndex, uint GfxObjId);
|
||||
|
||||
|
|
@ -56,9 +58,14 @@ internal sealed class RetailSelectionScene :
|
|||
out RetailSelectionLighting lighting)
|
||||
=> _lightingPulse.TryGet(serverGuid, localEntityId, out lighting);
|
||||
|
||||
internal void SetCurrentRenderSceneObserver(
|
||||
ICurrentRenderSelectionObserver? observer) =>
|
||||
_currentRenderSceneObserver = observer;
|
||||
|
||||
/// <summary>Clears every session-owned frame and material pulse.</summary>
|
||||
public void Reset()
|
||||
{
|
||||
_currentRenderSceneObserver?.AbortSelectionFrame();
|
||||
_building.Clear();
|
||||
_published.Clear();
|
||||
_buildingKeys.Clear();
|
||||
|
|
@ -71,6 +78,7 @@ internal sealed class RetailSelectionScene :
|
|||
_building.Clear();
|
||||
_buildingKeys.Clear();
|
||||
_viewFrustum = null;
|
||||
_currentRenderSceneObserver?.BeginSelectionFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -105,17 +113,26 @@ internal sealed class RetailSelectionScene :
|
|||
partIndex,
|
||||
partWorld,
|
||||
mesh));
|
||||
_currentRenderSceneObserver?.ObserveSelectionPart(
|
||||
entity.ServerGuid,
|
||||
entity.Id,
|
||||
partIndex,
|
||||
gfxObjId,
|
||||
partWorld,
|
||||
mesh);
|
||||
}
|
||||
|
||||
public void CompleteFrame()
|
||||
{
|
||||
(_published, _building) = (_building, _published);
|
||||
_currentRenderSceneObserver?.CompleteSelectionFrame();
|
||||
}
|
||||
|
||||
/// <summary>Discards the incomplete building frame and preserves the last
|
||||
/// completely published selection product.</summary>
|
||||
public void AbortFrame()
|
||||
{
|
||||
_currentRenderSceneObserver?.AbortSelectionFrame();
|
||||
_building.Clear();
|
||||
_buildingKeys.Clear();
|
||||
_viewFrustum = null;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Numerics;
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.Core.Lighting;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Rendering;
|
||||
|
|
@ -97,6 +98,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
private int _scratchPeakUnits;
|
||||
|
||||
private readonly BindlessSupport _bindless;
|
||||
private ICurrentRenderDispatcherObserver? _currentRenderSceneObserver;
|
||||
|
||||
public readonly record struct DrawStats(
|
||||
EntitySet Set,
|
||||
|
|
@ -843,8 +845,16 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_dynamicBufferSetCursor = 0;
|
||||
_dynamicFrameStarted = true;
|
||||
_activeDynamicBufferSet = null;
|
||||
_currentRenderSceneObserver?.BeginDispatcherFrame();
|
||||
}
|
||||
|
||||
internal void SetCurrentRenderSceneObserver(
|
||||
ICurrentRenderDispatcherObserver? observer) =>
|
||||
_currentRenderSceneObserver = observer;
|
||||
|
||||
internal void AbortCurrentRenderSceneObserverFrame() =>
|
||||
_currentRenderSceneObserver?.AbortDispatcherFrame();
|
||||
|
||||
/// <summary>
|
||||
/// Fix B (A7 #3): hand the dispatcher this frame's GLOBAL point-light snapshot
|
||||
/// (<see cref="LightManager.PointSnapshot"/>). Call once per frame BEFORE
|
||||
|
|
@ -1471,6 +1481,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
ref walkResult,
|
||||
probeState,
|
||||
set);
|
||||
_currentRenderSceneObserver?.ObserveDispatcherDraw(
|
||||
set,
|
||||
walkResult.EntitiesWalked,
|
||||
_walkScratch);
|
||||
|
||||
// Tier 1 cache (#53) flush-tracking locals. _walkScratch holds one tuple
|
||||
// per (entity, MeshRefIndex) and is in entity-order, so all MeshRefs of
|
||||
|
|
@ -1947,6 +1961,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (anyVao == 0)
|
||||
{
|
||||
LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0);
|
||||
ObserveCurrentDispatcherSubmission(
|
||||
visibleInstanceCount: 0,
|
||||
immediateInstanceCount: 0,
|
||||
deferTransparent: false);
|
||||
_cpuStopwatch.Stop();
|
||||
if (diag) MaybeFlushDiag();
|
||||
return;
|
||||
|
|
@ -1965,6 +1983,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (totalInstances == 0)
|
||||
{
|
||||
LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0);
|
||||
ObserveCurrentDispatcherSubmission(
|
||||
visibleInstanceCount: 0,
|
||||
immediateInstanceCount: 0,
|
||||
deferTransparent);
|
||||
_cpuStopwatch.Stop();
|
||||
if (diag) MaybeFlushDiag();
|
||||
return;
|
||||
|
|
@ -2085,6 +2107,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_opaqueDrawCount,
|
||||
_transparentDrawCount,
|
||||
totalTriangles);
|
||||
ObserveCurrentDispatcherSubmission(
|
||||
totalInstances,
|
||||
immediateInstances,
|
||||
deferTransparent);
|
||||
|
||||
// ── Phase 5: upload four buffers ────────────────────────────────────
|
||||
ActivateNextDynamicBufferSet();
|
||||
|
|
@ -2422,6 +2448,75 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
g.Translucency,
|
||||
g.CullMode);
|
||||
|
||||
private void ObserveCurrentDispatcherSubmission(
|
||||
int visibleInstanceCount,
|
||||
int immediateInstanceCount,
|
||||
bool deferTransparent)
|
||||
{
|
||||
ICurrentRenderDispatcherObserver? observer =
|
||||
_currentRenderSceneObserver;
|
||||
if (observer is null)
|
||||
return;
|
||||
|
||||
int opaqueGroupCount =
|
||||
visibleInstanceCount == 0 ? 0 : _opaqueDraws.Count;
|
||||
int transparentGroupCount =
|
||||
visibleInstanceCount == 0 ? 0 : _translucentDraws.Count;
|
||||
StableRenderHash128 hash = StableRenderHash128.Create();
|
||||
hash.Add(visibleInstanceCount);
|
||||
hash.Add(immediateInstanceCount);
|
||||
hash.Add(opaqueGroupCount);
|
||||
hash.Add(transparentGroupCount);
|
||||
hash.Add(deferTransparent);
|
||||
if (visibleInstanceCount != 0)
|
||||
{
|
||||
foreach (InstanceGroup group in _opaqueDraws)
|
||||
AddSubmissionGroup(ref hash, group);
|
||||
foreach (InstanceGroup group in _translucentDraws)
|
||||
AddSubmissionGroup(ref hash, group);
|
||||
}
|
||||
|
||||
var submission = new CurrentRenderDispatcherSubmission(
|
||||
VisibleInstanceCount: visibleInstanceCount,
|
||||
ImmediateInstanceCount: immediateInstanceCount,
|
||||
OpaqueGroupCount: opaqueGroupCount,
|
||||
TransparentGroupCount: transparentGroupCount,
|
||||
TransparentDeferred: deferTransparent,
|
||||
Digest: hash.Finish());
|
||||
observer.ObserveDispatcherSubmission(in submission);
|
||||
}
|
||||
|
||||
private static void AddSubmissionGroup(
|
||||
ref StableRenderHash128 hash,
|
||||
InstanceGroup group)
|
||||
{
|
||||
GroupKey key = ToKey(group);
|
||||
hash.Add(key.FirstIndex);
|
||||
hash.Add(key.BaseVertex);
|
||||
hash.Add(key.IndexCount);
|
||||
hash.Add(key.BindlessTextureHandle);
|
||||
hash.Add(key.TextureLayer);
|
||||
hash.Add((int)key.Translucency);
|
||||
hash.Add((int)key.CullMode);
|
||||
hash.Add(group.Matrices.Count);
|
||||
for (int index = 0; index < group.Matrices.Count; index++)
|
||||
{
|
||||
hash.Add(group.Matrices[index]);
|
||||
hash.Add(group.LocalSortCenters[index]);
|
||||
hash.Add(group.Slots[index]);
|
||||
InstanceLightSet lights = group.LightSets[index];
|
||||
for (int lightIndex = 0;
|
||||
lightIndex < LightManager.MaxLightsPerObject;
|
||||
lightIndex++)
|
||||
{
|
||||
hash.Add(lights[lightIndex]);
|
||||
}
|
||||
hash.Add(group.IndoorFlags[index]);
|
||||
hash.Add(group.Opacities[index]);
|
||||
hash.Add(group.SelectionLighting[index]);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeferTransparentGroups(Vector3 cameraWorldPosition, Matrix4x4 viewProjection)
|
||||
{
|
||||
RetailAlphaQueue queue = _alphaQueue!;
|
||||
|
|
|
|||
|
|
@ -299,6 +299,7 @@ internal sealed class WorldScenePassExecutor : IWorldScenePassExecutor
|
|||
TryAbort(_frameGlState.RestoreFrameDefaults);
|
||||
TryAbort(_clipFrame.Reset);
|
||||
TryAbort(_entities.ClearClipRouting);
|
||||
TryAbort(_entities.AbortCurrentRenderSceneObserverFrame);
|
||||
TryAbort(() => _environmentCells.SetClipRouting(null));
|
||||
_visibleParticleOwners.Clear();
|
||||
if (failures is { Count: > 0 })
|
||||
|
|
|
|||
|
|
@ -126,7 +126,24 @@ public sealed class WorldLifecycleAutomationControllerTests
|
|||
CellBucketCount: 4,
|
||||
Digest: new RenderSceneHash128(
|
||||
Low: 0x0123456789ABCDEF,
|
||||
High: 0xFEDCBA9876543210)),
|
||||
High: 0xFEDCBA9876543210),
|
||||
CompletedPViewFrameSequence: 19,
|
||||
AbortedPViewFrames: 0,
|
||||
PViewCandidateCount: 61,
|
||||
PViewDigest: new RenderSceneHash128(11, 12),
|
||||
DispatcherFrameSequence: 19,
|
||||
AbortedDispatcherFrames: 0,
|
||||
DispatcherDrawCount: 3,
|
||||
DispatcherEntityCount: 52,
|
||||
DispatcherMeshRefCount: 80,
|
||||
DispatcherInstanceCount: 96,
|
||||
DispatcherOpaqueGroupCount: 11,
|
||||
DispatcherTransparentGroupCount: 4,
|
||||
DispatcherDigest: new RenderSceneHash128(13, 14),
|
||||
CompletedSelectionFrameSequence: 19,
|
||||
AbortedSelectionFrames: 0,
|
||||
SelectionPartCount: 7,
|
||||
SelectionDigest: new RenderSceneHash128(15, 16)),
|
||||
TrackedGpuBytes = 1234,
|
||||
Residency = new ResidencySnapshot(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
|
@ -225,6 +226,143 @@ public sealed class CurrentRenderSceneOracleTests
|
|||
Assert.Empty(oracle.Projections);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PViewDigestNamesRouteOrderAndRejectsObjectsOutsidePartition()
|
||||
{
|
||||
WorldEntity cellStatic = Entity(
|
||||
id: 50,
|
||||
serverGuid: 0,
|
||||
parentCell: CellA);
|
||||
WorldEntity dynamic = Entity(
|
||||
id: 1_000_050,
|
||||
serverGuid: 0x8000_0050,
|
||||
parentCell: CellA);
|
||||
var oracle = new CurrentRenderSceneOracle();
|
||||
var result = new InteriorEntityPartition.Result();
|
||||
InteriorEntityPartition.Partition(
|
||||
result,
|
||||
new HashSet<uint> { CellA },
|
||||
new[] { Entry(LandblockA, cellStatic, dynamic) },
|
||||
oracle);
|
||||
|
||||
oracle.BeginPViewFrame();
|
||||
oracle.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.CellStatic,
|
||||
routeIndex: 0,
|
||||
CellA,
|
||||
[cellStatic]);
|
||||
oracle.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.DynamicLast,
|
||||
routeIndex: 0,
|
||||
cellId: 0,
|
||||
[dynamic]);
|
||||
oracle.CompletePViewFrame();
|
||||
CurrentRenderSceneOracleSnapshot first = oracle.Snapshot;
|
||||
|
||||
oracle.BeginPViewFrame();
|
||||
oracle.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.DynamicLast,
|
||||
routeIndex: 0,
|
||||
cellId: 0,
|
||||
[dynamic]);
|
||||
oracle.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.CellStatic,
|
||||
routeIndex: 0,
|
||||
CellA,
|
||||
[cellStatic]);
|
||||
oracle.CompletePViewFrame();
|
||||
CurrentRenderSceneOracleSnapshot second = oracle.Snapshot;
|
||||
|
||||
Assert.Equal(2, first.PViewCandidateCount);
|
||||
Assert.NotEqual(first.PViewDigest, second.PViewDigest);
|
||||
Assert.Equal(
|
||||
first.CompletedPViewFrameSequence + 1,
|
||||
second.CompletedPViewFrameSequence);
|
||||
|
||||
oracle.BeginPViewFrame();
|
||||
InvalidOperationException error = Assert.Throws<InvalidOperationException>(
|
||||
() => oracle.ObservePViewBucket(
|
||||
CurrentRenderPViewRoute.CellStatic,
|
||||
routeIndex: 0,
|
||||
CellA,
|
||||
[Entity(51, 0, CellA)]));
|
||||
Assert.Contains("absent from the current partition", error.Message);
|
||||
oracle.AbortPViewFrame();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DispatcherDigestResetsPerFrameAndRecordsAcceptedMeshRefs()
|
||||
{
|
||||
WorldEntity entity = Entity(
|
||||
id: 60,
|
||||
serverGuid: 0,
|
||||
parentCell: CellA);
|
||||
var accepted = new List<(
|
||||
WorldEntity Entity,
|
||||
int MeshRefIndex,
|
||||
uint LandblockId)>
|
||||
{
|
||||
(entity, 0, LandblockB),
|
||||
};
|
||||
var oracle = new CurrentRenderSceneOracle();
|
||||
|
||||
oracle.BeginDispatcherFrame();
|
||||
oracle.ObserveDispatcherDraw(
|
||||
WbDrawDispatcher.EntitySet.All,
|
||||
entitiesWalked: 1,
|
||||
accepted);
|
||||
var submission = new CurrentRenderDispatcherSubmission(
|
||||
VisibleInstanceCount: 3,
|
||||
ImmediateInstanceCount: 2,
|
||||
OpaqueGroupCount: 1,
|
||||
TransparentGroupCount: 1,
|
||||
TransparentDeferred: true,
|
||||
Digest: new RenderSceneHash128(101, 202));
|
||||
oracle.ObserveDispatcherSubmission(in submission);
|
||||
CurrentRenderSceneOracleSnapshot first = oracle.Snapshot;
|
||||
|
||||
Assert.Equal(1uL, first.DispatcherFrameSequence);
|
||||
Assert.Equal(1, first.DispatcherDrawCount);
|
||||
Assert.Equal(1, first.DispatcherEntityCount);
|
||||
Assert.Equal(1, first.DispatcherMeshRefCount);
|
||||
Assert.Equal(3, first.DispatcherInstanceCount);
|
||||
Assert.Equal(1, first.DispatcherOpaqueGroupCount);
|
||||
Assert.Equal(1, first.DispatcherTransparentGroupCount);
|
||||
CurrentRenderDispatcherFingerprint fingerprint =
|
||||
Assert.Single(oracle.DispatcherCandidates);
|
||||
Assert.Equal(LandblockB, fingerprint.TupleLandblockId);
|
||||
Assert.Equal(LandblockA, fingerprint.CacheLandblockId);
|
||||
|
||||
oracle.BeginDispatcherFrame();
|
||||
CurrentRenderSceneOracleSnapshot reset = oracle.Snapshot;
|
||||
|
||||
Assert.Equal(2uL, reset.DispatcherFrameSequence);
|
||||
Assert.Equal(0, reset.DispatcherDrawCount);
|
||||
Assert.Equal(0, reset.DispatcherMeshRefCount);
|
||||
Assert.Empty(oracle.DispatcherCandidates);
|
||||
Assert.NotEqual(first.DispatcherDigest, reset.DispatcherDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DispatcherAbortDiscardsIncompleteCandidates()
|
||||
{
|
||||
WorldEntity entity = Entity(
|
||||
id: 70,
|
||||
serverGuid: 0x8000_0070,
|
||||
parentCell: CellA);
|
||||
var oracle = new CurrentRenderSceneOracle();
|
||||
|
||||
oracle.BeginDispatcherFrame();
|
||||
oracle.ObserveDispatcherDraw(
|
||||
WbDrawDispatcher.EntitySet.All,
|
||||
entitiesWalked: 1,
|
||||
[(entity, 0, LandblockA)]);
|
||||
oracle.AbortDispatcherFrame();
|
||||
|
||||
Assert.Equal(1, oracle.Snapshot.AbortedDispatcherFrames);
|
||||
Assert.Empty(oracle.DispatcherCandidates);
|
||||
}
|
||||
|
||||
private static CurrentRenderProjectionFingerprint CaptureSingle(
|
||||
WorldEntity entity,
|
||||
uint landblockId,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
|
@ -126,6 +127,37 @@ public sealed class RetailPViewPassExecutorTests
|
|||
Assert.Contains("phantom-objects", executor.Operations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DrawInside_referee_records_the_exact_routed_entity_buckets()
|
||||
{
|
||||
var oracle = new CurrentRenderSceneOracle();
|
||||
var renderer = new RetailPViewRenderer(oracle);
|
||||
using var executor = new RecordingExecutor();
|
||||
LoadedCell interior = InteriorWithExit(0xA9B40100u);
|
||||
WorldEntity cellStatic = Entity(20u, parentCellId: interior.CellId);
|
||||
WorldEntity dynamic = Entity(
|
||||
21u,
|
||||
serverGuid: 0x80000021u,
|
||||
parentCellId: interior.CellId);
|
||||
|
||||
renderer.DrawInside(Frame(interior, [cellStatic, dynamic]), executor);
|
||||
|
||||
Assert.Equal(1uL, oracle.Snapshot.CompletedPViewFrameSequence);
|
||||
Assert.Equal(2, oracle.Snapshot.PViewCandidateCount);
|
||||
Assert.Collection(
|
||||
oracle.PViewCandidates,
|
||||
candidate =>
|
||||
{
|
||||
Assert.Equal(CurrentRenderPViewRoute.CellStatic, candidate.Route);
|
||||
Assert.Equal(cellStatic.Id, candidate.Projection.EntityId);
|
||||
},
|
||||
candidate =>
|
||||
{
|
||||
Assert.Equal(CurrentRenderPViewRoute.DynamicLast, candidate.Route);
|
||||
Assert.Equal(dynamic.Id, candidate.Projection.EntityId);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DrawInside_interior_root_executes_the_nearby_building_look_in_punch()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Selection;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
|
|
@ -68,6 +69,31 @@ public sealed class RetailSelectionSceneTests
|
|||
Assert.Equal(published.Id, hit.Value.LocalEntityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentPathRefereeRecordsOnlyAcceptedSelectionParts()
|
||||
{
|
||||
var oracle = new CurrentRenderSceneOracle();
|
||||
var scene = CreateScene();
|
||||
scene.SetCurrentRenderSceneObserver(oracle);
|
||||
var entity = Entity(localId: 48u, serverGuid: 0x5000_0048u);
|
||||
|
||||
Publish(scene, entity);
|
||||
|
||||
Assert.Equal(1uL, oracle.Snapshot.CompletedSelectionFrameSequence);
|
||||
Assert.Equal(1, oracle.Snapshot.SelectionPartCount);
|
||||
CurrentRenderSelectionFingerprint part =
|
||||
Assert.Single(oracle.SelectionParts);
|
||||
Assert.Equal(entity.ServerGuid, part.ServerGuid);
|
||||
Assert.Equal(entity.Id, part.LocalEntityId);
|
||||
Assert.Equal(0, part.PartIndex);
|
||||
Assert.Equal(0x0100_0001u, part.GfxObjId);
|
||||
|
||||
scene.BeginFrame();
|
||||
scene.AbortFrame();
|
||||
Assert.Equal(1, oracle.Snapshot.AbortedSelectionFrames);
|
||||
Assert.Empty(oracle.SelectionParts);
|
||||
}
|
||||
|
||||
private static RetailSelectionScene CreateScene(
|
||||
RetailSelectionLightingPulse? pulse = null)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue