refactor(rendering): isolate dispatcher candidate values
This commit is contained in:
parent
accd402dd7
commit
58b712c6ec
5 changed files with 356 additions and 50 deletions
83
src/AcDream.App/Rendering/Scene/RenderInstanceCandidate.cs
Normal file
83
src/AcDream.App/Rendering/Scene/RenderInstanceCandidate.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The complete acdream-owned value boundary consumed by the object
|
||||||
|
/// dispatcher. It contains presentation facts only; the dispatcher must not
|
||||||
|
/// call back into <see cref="WorldEntity"/> or any gameplay owner after this
|
||||||
|
/// value has been produced.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct RenderInstanceCandidate(
|
||||||
|
RenderProjectionId ProjectionId,
|
||||||
|
uint LocalEntityId,
|
||||||
|
uint ServerGuid,
|
||||||
|
uint SourceId,
|
||||||
|
uint ParentCellId,
|
||||||
|
Matrix4x4 RootWorld,
|
||||||
|
Vector3 Position,
|
||||||
|
Quaternion Rotation,
|
||||||
|
float Scale,
|
||||||
|
RenderWorldBounds Bounds,
|
||||||
|
PaletteOverride? PaletteOverride,
|
||||||
|
bool IsBuildingShell,
|
||||||
|
bool Animated,
|
||||||
|
int MeshPartCount,
|
||||||
|
uint TupleLandblockId)
|
||||||
|
{
|
||||||
|
internal uint Id => LocalEntityId;
|
||||||
|
|
||||||
|
internal uint? ParentCell =>
|
||||||
|
ParentCellId == 0 ? null : ParentCellId;
|
||||||
|
|
||||||
|
internal uint CacheLandblockId =>
|
||||||
|
ParentCellId != 0
|
||||||
|
? (ParentCellId & 0xFFFF0000u) | 0xFFFFu
|
||||||
|
: TupleLandblockId;
|
||||||
|
|
||||||
|
internal static RenderInstanceCandidate FromWorldEntity(
|
||||||
|
WorldEntity entity,
|
||||||
|
bool animated,
|
||||||
|
int meshPartCount,
|
||||||
|
uint tupleLandblockId)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
|
if (meshPartCount < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(meshPartCount));
|
||||||
|
if (entity.AabbDirty)
|
||||||
|
entity.RefreshAabb();
|
||||||
|
|
||||||
|
return new RenderInstanceCandidate(
|
||||||
|
ProjectionId: default,
|
||||||
|
LocalEntityId: entity.Id,
|
||||||
|
ServerGuid: entity.ServerGuid,
|
||||||
|
SourceId: entity.SourceGfxObjOrSetupId,
|
||||||
|
ParentCellId: entity.ParentCellId ?? 0u,
|
||||||
|
RootWorld:
|
||||||
|
Matrix4x4.CreateFromQuaternion(entity.Rotation)
|
||||||
|
* Matrix4x4.CreateTranslation(entity.Position),
|
||||||
|
Position: entity.Position,
|
||||||
|
Rotation: entity.Rotation,
|
||||||
|
Scale: entity.Scale,
|
||||||
|
Bounds: new RenderWorldBounds(
|
||||||
|
entity.AabbMin,
|
||||||
|
entity.AabbMax),
|
||||||
|
PaletteOverride: entity.PaletteOverride,
|
||||||
|
IsBuildingShell: entity.IsBuildingShell,
|
||||||
|
Animated: animated,
|
||||||
|
MeshPartCount: meshPartCount,
|
||||||
|
TupleLandblockId: tupleLandblockId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One ordered mesh reference belonging to a dispatcher candidate. Candidate
|
||||||
|
/// values repeat across the entity's tuples only in the transitional current
|
||||||
|
/// source; the scene frame product supplies the same facts from retained arena
|
||||||
|
/// storage.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct RenderInstanceTuple(
|
||||||
|
RenderInstanceCandidate Candidate,
|
||||||
|
int MeshRefIndex,
|
||||||
|
MeshRef MeshRef);
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.World;
|
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Selection;
|
namespace AcDream.App.Rendering.Selection;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -10,7 +8,8 @@ namespace AcDream.App.Rendering.Selection;
|
||||||
internal interface IRetailSelectionRenderSink
|
internal interface IRetailSelectionRenderSink
|
||||||
{
|
{
|
||||||
void AddVisiblePart(
|
void AddVisiblePart(
|
||||||
WorldEntity entity,
|
uint serverGuid,
|
||||||
|
uint localEntityId,
|
||||||
int partIndex,
|
int partIndex,
|
||||||
uint gfxObjId,
|
uint gfxObjId,
|
||||||
Matrix4x4 partWorld);
|
Matrix4x4 partWorld);
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,22 @@ internal sealed class RetailSelectionScene :
|
||||||
int partIndex,
|
int partIndex,
|
||||||
uint gfxObjId,
|
uint gfxObjId,
|
||||||
Matrix4x4 partWorld)
|
Matrix4x4 partWorld)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
|
AddVisiblePart(
|
||||||
|
entity.ServerGuid,
|
||||||
|
entity.Id,
|
||||||
|
partIndex,
|
||||||
|
gfxObjId,
|
||||||
|
partWorld);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddVisiblePart(
|
||||||
|
uint serverGuid,
|
||||||
|
uint localEntityId,
|
||||||
|
int partIndex,
|
||||||
|
uint gfxObjId,
|
||||||
|
Matrix4x4 partWorld)
|
||||||
{
|
{
|
||||||
// WbDrawDispatcher is also shared by sealed CreatureMode-style UI
|
// WbDrawDispatcher is also shared by sealed CreatureMode-style UI
|
||||||
// viewports (paperdoll and appraisal). Those draws occur after the
|
// viewports (paperdoll and appraisal). Those draws occur after the
|
||||||
|
|
@ -110,10 +126,13 @@ internal sealed class RetailSelectionScene :
|
||||||
// enter the world picking product.
|
// enter the world picking product.
|
||||||
if (!_frameOpen)
|
if (!_frameOpen)
|
||||||
return;
|
return;
|
||||||
if (entity.ServerGuid == 0u)
|
if (serverGuid == 0u)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!_buildingKeys.Add(new PartKey(entity.Id, partIndex, gfxObjId)))
|
if (!_buildingKeys.Add(new PartKey(
|
||||||
|
localEntityId,
|
||||||
|
partIndex,
|
||||||
|
gfxObjId)))
|
||||||
return;
|
return;
|
||||||
RetailSelectionMesh? mesh = _geometry.Resolve(gfxObjId);
|
RetailSelectionMesh? mesh = _geometry.Resolve(gfxObjId);
|
||||||
if (mesh is null)
|
if (mesh is null)
|
||||||
|
|
@ -123,14 +142,14 @@ internal sealed class RetailSelectionScene :
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_building.Add(new RetailSelectionPart(
|
_building.Add(new RetailSelectionPart(
|
||||||
entity.ServerGuid,
|
serverGuid,
|
||||||
entity.Id,
|
localEntityId,
|
||||||
partIndex,
|
partIndex,
|
||||||
partWorld,
|
partWorld,
|
||||||
mesh));
|
mesh));
|
||||||
_currentRenderSceneObserver?.ObserveSelectionPart(
|
_currentRenderSceneObserver?.ObserveSelectionPart(
|
||||||
entity.ServerGuid,
|
serverGuid,
|
||||||
entity.Id,
|
localEntityId,
|
||||||
partIndex,
|
partIndex,
|
||||||
gfxObjId,
|
gfxObjId,
|
||||||
partWorld,
|
partWorld,
|
||||||
|
|
|
||||||
|
|
@ -673,6 +673,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes = ~480 KB / frame
|
// ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes = ~480 KB / frame
|
||||||
// of GC pressure on the render thread under the original T17 shape.
|
// of GC pressure on the render thread under the original T17 shape.
|
||||||
private readonly List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> _walkScratch = new();
|
private readonly List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> _walkScratch = new();
|
||||||
|
// G2: the dispatcher consumes this acdream-owned value boundary, never
|
||||||
|
// WorldEntity. The current source translates its accepted walk into this
|
||||||
|
// retained list; G4 will replace only that producer with RenderFrameView.
|
||||||
|
private readonly List<RenderInstanceTuple> _candidateTupleScratch = new();
|
||||||
|
|
||||||
// Tier 1 cache (#53) — per-entity classification collector. Reused across
|
// Tier 1 cache (#53) — per-entity classification collector. Reused across
|
||||||
// frames; cleared at flush time when the per-entity loop crosses an entity
|
// frames; cleared at flush time when the per-entity loop crosses an entity
|
||||||
|
|
@ -1295,6 +1299,49 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
internal static uint ResolveCacheLandblockHint(WorldEntity entity, uint tupleLandblockId)
|
internal static uint ResolveCacheLandblockHint(WorldEntity entity, uint tupleLandblockId)
|
||||||
=> entity.ParentCellId is uint pc ? ((pc & 0xFFFF0000u) | 0xFFFFu) : tupleLandblockId;
|
=> entity.ParentCellId is uint pc ? ((pc & 0xFFFF0000u) | 0xFFFFu) : tupleLandblockId;
|
||||||
|
|
||||||
|
internal static uint ResolveCacheLandblockHint(
|
||||||
|
in RenderInstanceCandidate entity) =>
|
||||||
|
entity.CacheLandblockId;
|
||||||
|
|
||||||
|
private static void BuildCurrentCandidateTuples(
|
||||||
|
List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> source,
|
||||||
|
HashSet<uint>? animatedEntityIds,
|
||||||
|
List<RenderInstanceTuple> destination)
|
||||||
|
{
|
||||||
|
destination.Clear();
|
||||||
|
if (destination.Capacity < source.Count)
|
||||||
|
destination.Capacity = source.Count;
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
while (index < source.Count)
|
||||||
|
{
|
||||||
|
(WorldEntity entity, _, uint tupleLandblockId) = source[index];
|
||||||
|
int end = index + 1;
|
||||||
|
while (end < source.Count
|
||||||
|
&& ReferenceEquals(source[end].Entity, entity)
|
||||||
|
&& source[end].LandblockId == tupleLandblockId)
|
||||||
|
{
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int meshPartCount = end - index;
|
||||||
|
RenderInstanceCandidate candidate =
|
||||||
|
RenderInstanceCandidate.FromWorldEntity(
|
||||||
|
entity,
|
||||||
|
animatedEntityIds?.Contains(entity.Id) == true,
|
||||||
|
meshPartCount,
|
||||||
|
tupleLandblockId);
|
||||||
|
for (; index < end; index++)
|
||||||
|
{
|
||||||
|
int meshRefIndex = source[index].MeshRefIndex;
|
||||||
|
destination.Add(new RenderInstanceTuple(
|
||||||
|
candidate,
|
||||||
|
meshRefIndex,
|
||||||
|
entity.MeshRefs[meshRefIndex]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #119 decisive probe: rate-limited <c>[dump-entity] WALK-REJECT</c> line
|
/// #119 decisive probe: rate-limited <c>[dump-entity] WALK-REJECT</c> line
|
||||||
/// for an <c>ACDREAM_DUMP_ENTITY</c>-targeted entity that the walk filtered
|
/// for an <c>ACDREAM_DUMP_ENTITY</c>-targeted entity that the walk filtered
|
||||||
|
|
@ -1325,17 +1372,31 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
/// collapsed to ~zero / missing parts) from H-B (Tier-1 cache holding a
|
/// collapsed to ~zero / missing parts) from H-B (Tier-1 cache holding a
|
||||||
/// partial or stale batch set) from H-C (both healthy ⇒ draw-side compose).
|
/// partial or stale batch set) from H-C (both healthy ⇒ draw-side compose).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void MaybeEmitEntityDump(WorldEntity entity, uint landblockId, bool culled)
|
private void MaybeEmitEntityDump(
|
||||||
|
in RenderInstanceCandidate entity,
|
||||||
|
uint landblockId,
|
||||||
|
bool culled,
|
||||||
|
IReadOnlyList<RenderInstanceTuple> tuples)
|
||||||
{
|
{
|
||||||
var targets = RenderingDiagnostics.DumpEntitySourceIds;
|
var targets = RenderingDiagnostics.DumpEntitySourceIds;
|
||||||
if (targets.Count == 0 || !targets.Contains(entity.SourceGfxObjOrSetupId)) return;
|
if (targets.Count == 0 || !targets.Contains(entity.SourceId))
|
||||||
|
return;
|
||||||
|
|
||||||
var refs = entity.MeshRefs;
|
|
||||||
int zeroT = 0;
|
int zeroT = 0;
|
||||||
|
int refsCount = 0;
|
||||||
float tzMin = float.MaxValue, tzMax = float.MinValue;
|
float tzMin = float.MaxValue, tzMax = float.MinValue;
|
||||||
for (int i = 0; i < refs.Count; i++)
|
for (int i = 0; i < tuples.Count; i++)
|
||||||
{
|
{
|
||||||
var t = refs[i].PartTransform.Translation;
|
RenderInstanceTuple tuple = tuples[i];
|
||||||
|
if (tuple.Candidate.LocalEntityId != entity.LocalEntityId
|
||||||
|
|| tuple.Candidate.TupleLandblockId
|
||||||
|
!= entity.TupleLandblockId)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
refsCount++;
|
||||||
|
Vector3 t = tuple.MeshRef.PartTransform.Translation;
|
||||||
if (t.LengthSquared() < 1e-9f) zeroT++;
|
if (t.LengthSquared() < 1e-9f) zeroT++;
|
||||||
if (t.Z < tzMin) tzMin = t.Z;
|
if (t.Z < tzMin) tzMin = t.Z;
|
||||||
if (t.Z > tzMax) tzMax = t.Z;
|
if (t.Z > tzMax) tzMax = t.Z;
|
||||||
|
|
@ -1356,7 +1417,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var sig = (refs.Count, cacheBatches, zeroT, culled);
|
var sig = (refsCount, cacheBatches, zeroT, culled);
|
||||||
bool first = !_entityDumpSig.TryGetValue(entity.Id, out var prev);
|
bool first = !_entityDumpSig.TryGetValue(entity.Id, out var prev);
|
||||||
if (!first && prev == sig) return;
|
if (!first && prev == sig) return;
|
||||||
_entityDumpSig[entity.Id] = sig;
|
_entityDumpSig[entity.Id] = sig;
|
||||||
|
|
@ -1365,20 +1426,29 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
? (_tier1CacheDisabled ? "disabled" : "miss")
|
? (_tier1CacheDisabled ? "disabled" : "miss")
|
||||||
: $"hit:{cacheBatches} restZero={restZero} restZ=[{rzMin:F2}..{rzMax:F2}]";
|
: $"hit:{cacheBatches} restZero={restZero} restZ=[{rzMin:F2}..{rzMax:F2}]";
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[dump-entity] DRAW{(first ? "" : "-CHANGED")} id=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} " +
|
$"[dump-entity] DRAW{(first ? "" : "-CHANGED")} id=0x{entity.Id:X8} src=0x{entity.SourceId:X8} " +
|
||||||
$"lb=0x{landblockId:X8} cell=0x{(entity.ParentCellId ?? 0u):X8} " +
|
$"lb=0x{landblockId:X8} cell=0x{entity.ParentCellId:X8} " +
|
||||||
$"pos=({entity.Position.X:F2},{entity.Position.Y:F2},{entity.Position.Z:F2}) scale={entity.Scale:F2} " +
|
$"pos=({entity.Position.X:F2},{entity.Position.Y:F2},{entity.Position.Z:F2}) scale={entity.Scale:F2} " +
|
||||||
$"meshRefs={refs.Count} tZero={zeroT} tZ=[{tzMin:F2}..{tzMax:F2}] cache={cacheStr} culled={culled}");
|
$"meshRefs={refsCount} tZero={zeroT} tZ=[{tzMin:F2}..{tzMax:F2}] cache={cacheStr} culled={culled}");
|
||||||
|
|
||||||
if (first)
|
if (first)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < refs.Count; i++)
|
for (int i = 0; i < tuples.Count; i++)
|
||||||
{
|
{
|
||||||
var mr = refs[i];
|
RenderInstanceTuple tuple = tuples[i];
|
||||||
|
if (tuple.Candidate.LocalEntityId
|
||||||
|
!= entity.LocalEntityId
|
||||||
|
|| tuple.Candidate.TupleLandblockId
|
||||||
|
!= entity.TupleLandblockId)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
MeshRef mr = tuple.MeshRef;
|
||||||
var t = mr.PartTransform.Translation;
|
var t = mr.PartTransform.Translation;
|
||||||
bool loaded = _meshAdapter.TryGetRenderData(mr.GfxObjId) is not null;
|
bool loaded = _meshAdapter.TryGetRenderData(mr.GfxObjId) is not null;
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[dump-entity] part[{i:D2}] gfx=0x{mr.GfxObjId:X8} t=({t.X:F3},{t.Y:F3},{t.Z:F3}) loaded={loaded}");
|
$"[dump-entity] part[{tuple.MeshRefIndex:D2}] gfx=0x{mr.GfxObjId:X8} t=({t.X:F3},{t.Y:F3},{t.Z:F3}) loaded={loaded}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1485,6 +1555,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
set,
|
set,
|
||||||
walkResult.EntitiesWalked,
|
walkResult.EntitiesWalked,
|
||||||
_walkScratch);
|
_walkScratch);
|
||||||
|
BuildCurrentCandidateTuples(
|
||||||
|
_walkScratch,
|
||||||
|
animatedEntityIds,
|
||||||
|
_candidateTupleScratch);
|
||||||
|
|
||||||
// Tier 1 cache (#53) flush-tracking locals. _walkScratch holds one tuple
|
// Tier 1 cache (#53) flush-tracking locals. _walkScratch holds one tuple
|
||||||
// per (entity, MeshRefIndex) and is in entity-order, so all MeshRefs of
|
// per (entity, MeshRefIndex) and is in entity-order, so all MeshRefs of
|
||||||
|
|
@ -1537,8 +1611,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// a successful slow-path classification.
|
// a successful slow-path classification.
|
||||||
uint? prevTupleEntityId = null;
|
uint? prevTupleEntityId = null;
|
||||||
|
|
||||||
foreach (var (entity, partIdx, landblockId) in _walkScratch)
|
foreach (RenderInstanceTuple tuple in _candidateTupleScratch)
|
||||||
{
|
{
|
||||||
|
RenderInstanceCandidate entity = tuple.Candidate;
|
||||||
|
int partIdx = tuple.MeshRefIndex;
|
||||||
|
uint landblockId = entity.TupleLandblockId;
|
||||||
if (diag) _entitiesSeen++;
|
if (diag) _entitiesSeen++;
|
||||||
|
|
||||||
// Skip subsequent tuples of an entity that already cache-hit on
|
// Skip subsequent tuples of an entity that already cache-hit on
|
||||||
|
|
@ -1580,7 +1657,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// #119 root-cause fix: cache operations key on the entity's OWNING
|
// #119 root-cause fix: cache operations key on the entity's OWNING
|
||||||
// landblock, never the Draw call's tuple landblock (which is the
|
// landblock, never the Draw call's tuple landblock (which is the
|
||||||
// PLAYER's landblock on the bucket path). See ResolveCacheLandblockHint.
|
// PLAYER's landblock on the bucket path). See ResolveCacheLandblockHint.
|
||||||
uint cacheLb = ResolveCacheLandblockHint(entity, landblockId);
|
uint cacheLb = ResolveCacheLandblockHint(in entity);
|
||||||
|
|
||||||
bool isNewEntity = !prevTupleEntityId.HasValue || prevTupleEntityId.Value != entity.Id;
|
bool isNewEntity = !prevTupleEntityId.HasValue || prevTupleEntityId.Value != entity.Id;
|
||||||
if (isNewEntity)
|
if (isNewEntity)
|
||||||
|
|
@ -1599,7 +1676,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// The whole decision (including the routing-active gate) lives in
|
// The whole decision (including the routing-active gate) lives in
|
||||||
// the pure ResolveSlotForFrame helper so it's unit-testable.
|
// the pure ResolveSlotForFrame helper so it's unit-testable.
|
||||||
(_currentEntitySlot, _currentEntityCulled) = ResolveSlotForFrame(
|
(_currentEntitySlot, _currentEntityCulled) = ResolveSlotForFrame(
|
||||||
_clipRoutingActive, entity.ServerGuid, entity.ParentCellId,
|
_clipRoutingActive, entity.ServerGuid, entity.ParentCell,
|
||||||
_cellIdToSlot, _outdoorSlot, _outdoorVisible);
|
_cellIdToSlot, _outdoorSlot, _outdoorVisible);
|
||||||
if (_currentEntityCulled)
|
if (_currentEntityCulled)
|
||||||
probeCulledEntities++;
|
probeCulledEntities++;
|
||||||
|
|
@ -1619,14 +1696,18 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// #119 decisive probe: one-shot dump (+ change re-emission) for
|
// #119 decisive probe: one-shot dump (+ change re-emission) for
|
||||||
// ACDREAM_DUMP_ENTITY-targeted entities. Before the culled-continue
|
// ACDREAM_DUMP_ENTITY-targeted entities. Before the culled-continue
|
||||||
// so a routed-out entity still reports its state.
|
// so a routed-out entity still reports its state.
|
||||||
MaybeEmitEntityDump(entity, cacheLb, _currentEntityCulled);
|
MaybeEmitEntityDump(
|
||||||
|
in entity,
|
||||||
|
cacheLb,
|
||||||
|
_currentEntityCulled,
|
||||||
|
_candidateTupleScratch);
|
||||||
|
|
||||||
// #176 seam-draw probe: any entity parented to a target cell reports
|
// #176 seam-draw probe: any entity parented to a target cell reports
|
||||||
// its position + light set (a floor-coincident static/plate would be
|
// its position + light set (a floor-coincident static/plate would be
|
||||||
// the z-fight's second draw; the player entity is the positive
|
// the z-fight's second draw; the player entity is the positive
|
||||||
// control). Before the culled-continue, like the dump above.
|
// control). Before the culled-continue, like the dump above.
|
||||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled
|
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled
|
||||||
&& entity.ParentCellId is { } seamPc
|
&& entity.ParentCell is { } seamPc
|
||||||
&& AcDream.Core.Rendering.RenderingDiagnostics.SeamDrawTargetCells.Contains(seamPc))
|
&& AcDream.Core.Rendering.RenderingDiagnostics.SeamDrawTargetCells.Contains(seamPc))
|
||||||
MaybeEmitSeamEnt(entity);
|
MaybeEmitSeamEnt(entity);
|
||||||
}
|
}
|
||||||
|
|
@ -1650,11 +1731,9 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
if (_currentEntityCulled)
|
if (_currentEntityCulled)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var entityWorld =
|
Matrix4x4 entityWorld = entity.RootWorld;
|
||||||
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
|
||||||
Matrix4x4.CreateTranslation(entity.Position);
|
|
||||||
|
|
||||||
bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true;
|
bool isAnimated = entity.Animated;
|
||||||
|
|
||||||
// Cache-hit fast path (Task 10): static entity with a populated
|
// Cache-hit fast path (Task 10): static entity with a populated
|
||||||
// cache entry skips classification entirely. Walk the cached
|
// cache entry skips classification entirely. Walk the cached
|
||||||
|
|
@ -1689,7 +1768,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// the mesh adapter — cheap dict lookup, not a re-classify.
|
// the mesh adapter — cheap dict lookup, not a re-classify.
|
||||||
if (anyVao == 0)
|
if (anyVao == 0)
|
||||||
{
|
{
|
||||||
var firstMeshRef = entity.MeshRefs[partIdx];
|
MeshRef firstMeshRef = tuple.MeshRef;
|
||||||
var firstRenderData = _meshAdapter.TryGetRenderData(firstMeshRef.GfxObjId);
|
var firstRenderData = _meshAdapter.TryGetRenderData(firstMeshRef.GfxObjId);
|
||||||
if (firstRenderData is not null) anyVao = firstRenderData.VAO;
|
if (firstRenderData is not null) anyVao = firstRenderData.VAO;
|
||||||
}
|
}
|
||||||
|
|
@ -1728,7 +1807,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// trust MeshRefs as the source of truth here. AnimatedEntityState's
|
// trust MeshRefs as the source of truth here. AnimatedEntityState's
|
||||||
// overrides become relevant only for hot-swap (0xF625
|
// overrides become relevant only for hot-swap (0xF625
|
||||||
// ObjDescEvent) which today rebuilds MeshRefs anyway.
|
// ObjDescEvent) which today rebuilds MeshRefs anyway.
|
||||||
var meshRef = entity.MeshRefs[partIdx];
|
MeshRef meshRef = tuple.MeshRef;
|
||||||
ulong gfxObjId = meshRef.GfxObjId;
|
ulong gfxObjId = meshRef.GfxObjId;
|
||||||
|
|
||||||
var renderData = _meshAdapter.TryGetRenderData(gfxObjId);
|
var renderData = _meshAdapter.TryGetRenderData(gfxObjId);
|
||||||
|
|
@ -1871,7 +1950,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
if (!ClassifyBatches(partData, model, entity, meshRef, paletteIdentity, restPose, opacityMultiplier, collector))
|
if (!ClassifyBatches(partData, model, entity, meshRef, paletteIdentity, restPose, opacityMultiplier, collector))
|
||||||
currentEntityIncomplete = true;
|
currentEntityIncomplete = true;
|
||||||
_selectionSink?.AddVisiblePart(
|
_selectionSink?.AddVisiblePart(
|
||||||
entity,
|
entity.ServerGuid,
|
||||||
|
entity.LocalEntityId,
|
||||||
unchecked((partIdx << 16) | (setupPartIndex & 0xFFFF)),
|
unchecked((partIdx << 16) | (setupPartIndex & 0xFFFF)),
|
||||||
(uint)partGfxObjId,
|
(uint)partGfxObjId,
|
||||||
model);
|
model);
|
||||||
|
|
@ -1901,7 +1981,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
if (!ClassifyBatches(renderData, model, entity, meshRef, paletteIdentity, restPose: meshRef.PartTransform, opacityMultiplier: opacityMultiplier, collector: collector))
|
if (!ClassifyBatches(renderData, model, entity, meshRef, paletteIdentity, restPose: meshRef.PartTransform, opacityMultiplier: opacityMultiplier, collector: collector))
|
||||||
currentEntityIncomplete = true;
|
currentEntityIncomplete = true;
|
||||||
_selectionSink?.AddVisiblePart(
|
_selectionSink?.AddVisiblePart(
|
||||||
entity,
|
entity.ServerGuid,
|
||||||
|
entity.LocalEntityId,
|
||||||
partIdx,
|
partIdx,
|
||||||
(uint)gfxObjId,
|
(uint)gfxObjId,
|
||||||
model);
|
model);
|
||||||
|
|
@ -2354,13 +2435,14 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
|
|
||||||
private void PublishCachedSelectionParts(
|
private void PublishCachedSelectionParts(
|
||||||
EntityCacheEntry cachedEntry,
|
EntityCacheEntry cachedEntry,
|
||||||
WorldEntity entity,
|
in RenderInstanceCandidate entity,
|
||||||
Matrix4x4 entityWorld)
|
Matrix4x4 entityWorld)
|
||||||
{
|
{
|
||||||
foreach (CachedSelectionPart part in cachedEntry.SelectionParts)
|
foreach (CachedSelectionPart part in cachedEntry.SelectionParts)
|
||||||
{
|
{
|
||||||
_selectionSink!.AddVisiblePart(
|
_selectionSink!.AddVisiblePart(
|
||||||
entity,
|
entity.ServerGuid,
|
||||||
|
entity.LocalEntityId,
|
||||||
part.PartIndex,
|
part.PartIndex,
|
||||||
part.GfxObjId,
|
part.GfxObjId,
|
||||||
part.RestPose * entityWorld);
|
part.RestPose * entityWorld);
|
||||||
|
|
@ -3379,14 +3461,14 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// ever live in the target cells.
|
// ever live in the target cells.
|
||||||
private readonly Dictionary<ulong, string> _seamEntSigs = new();
|
private readonly Dictionary<ulong, string> _seamEntSigs = new();
|
||||||
|
|
||||||
private void MaybeEmitSeamEnt(WorldEntity entity)
|
private void MaybeEmitSeamEnt(in RenderInstanceCandidate entity)
|
||||||
{
|
{
|
||||||
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
var ci = System.Globalization.CultureInfo.InvariantCulture;
|
||||||
var snap = _pointSnapshot;
|
var snap = _pointSnapshot;
|
||||||
var sb = new System.Text.StringBuilder(200);
|
var sb = new System.Text.StringBuilder(200);
|
||||||
sb.AppendFormat(ci,
|
sb.AppendFormat(ci,
|
||||||
"guid=0x{0:X8} cell=0x{1:X8} pos=({2:F2},{3:F2},{4:F3}) culled={5} slot={6} indoor={7} L=[",
|
"guid=0x{0:X8} cell=0x{1:X8} pos=({2:F2},{3:F2},{4:F3}) culled={5} slot={6} indoor={7} L=[",
|
||||||
entity.ServerGuid, entity.ParentCellId ?? 0u,
|
entity.ServerGuid, entity.ParentCellId,
|
||||||
entity.Position.X, entity.Position.Y, entity.Position.Z,
|
entity.Position.X, entity.Position.Y, entity.Position.Z,
|
||||||
_currentEntityCulled ? 1 : 0, _currentEntitySlot, _currentEntityIndoor ? 1 : 0);
|
_currentEntityCulled ? 1 : 0, _currentEntitySlot, _currentEntityIndoor ? 1 : 0);
|
||||||
bool any = false;
|
bool any = false;
|
||||||
|
|
@ -3409,12 +3491,14 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
Console.WriteLine($"[seam-ent] t={Environment.TickCount64} {sig}");
|
Console.WriteLine($"[seam-ent] t={Environment.TickCount64} {sig}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ComputeEntityLightSet(WorldEntity entity)
|
private void ComputeEntityLightSet(
|
||||||
|
in RenderInstanceCandidate entity)
|
||||||
{
|
{
|
||||||
// #142: set the indoor flag first so it's available even when the early-return
|
// #142: set the indoor flag first so it's available even when the early-return
|
||||||
// fires below. Both the torch selection and the sun gate use the same predicate,
|
// fires below. Both the torch selection and the sun gate use the same predicate,
|
||||||
// so they can't disagree — one call, one truth.
|
// so they can't disagree — one call, one truth.
|
||||||
_currentEntityIndoor = IndoorObjectReceivesTorches(entity.ParentCellId);
|
_currentEntityIndoor =
|
||||||
|
IndoorObjectReceivesTorches(entity.ParentCell);
|
||||||
|
|
||||||
_currentEntityLightSet = InstanceLightSet.Disabled;
|
_currentEntityLightSet = InstanceLightSet.Disabled;
|
||||||
var snap = _pointSnapshot;
|
var snap = _pointSnapshot;
|
||||||
|
|
@ -3423,9 +3507,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
// Retail useSunlight gate: outdoor objects receive no per-object torches.
|
// Retail useSunlight gate: outdoor objects receive no per-object torches.
|
||||||
if (!_currentEntityIndoor) return; // #142: reuse the cached flag (was: IndoorObjectReceivesTorches(...))
|
if (!_currentEntityIndoor) return; // #142: reuse the cached flag (was: IndoorObjectReceivesTorches(...))
|
||||||
|
|
||||||
if (entity.AabbDirty) entity.RefreshAabb();
|
Vector3 center =
|
||||||
Vector3 center = (entity.AabbMin + entity.AabbMax) * 0.5f;
|
(entity.Bounds.Minimum + entity.Bounds.Maximum) * 0.5f;
|
||||||
float radius = (entity.AabbMax - entity.AabbMin).Length() * 0.5f;
|
float radius =
|
||||||
|
(entity.Bounds.Maximum - entity.Bounds.Minimum).Length() * 0.5f;
|
||||||
Array.Fill(_currentEntityLightSetScratch, -1);
|
Array.Fill(_currentEntityLightSetScratch, -1);
|
||||||
LightManager.SelectForObject(snap, center, radius, _currentEntityLightSetScratch);
|
LightManager.SelectForObject(snap, center, radius, _currentEntityLightSetScratch);
|
||||||
_currentEntityLightSet = InstanceLightSet.From(_currentEntityLightSetScratch);
|
_currentEntityLightSet = InstanceLightSet.From(_currentEntityLightSetScratch);
|
||||||
|
|
@ -3461,7 +3546,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
private bool ClassifyBatches(
|
private bool ClassifyBatches(
|
||||||
ObjectRenderData renderData,
|
ObjectRenderData renderData,
|
||||||
Matrix4x4 model,
|
Matrix4x4 model,
|
||||||
WorldEntity entity,
|
in RenderInstanceCandidate entity,
|
||||||
MeshRef meshRef,
|
MeshRef meshRef,
|
||||||
PaletteCompositeIdentity paletteIdentity,
|
PaletteCompositeIdentity paletteIdentity,
|
||||||
Matrix4x4 restPose,
|
Matrix4x4 restPose,
|
||||||
|
|
@ -3483,7 +3568,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
translucency = TranslucencyKind.AlphaBlend;
|
translucency = TranslucencyKind.AlphaBlend;
|
||||||
|
|
||||||
ResolvedTexture texture = ResolveTexture(
|
ResolvedTexture texture = ResolveTexture(
|
||||||
entity,
|
in entity,
|
||||||
meshRef,
|
meshRef,
|
||||||
batch,
|
batch,
|
||||||
paletteIdentity,
|
paletteIdentity,
|
||||||
|
|
@ -3518,11 +3603,40 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
|
|
||||||
private readonly record struct ResolvedTexture(ulong Handle, uint Layer);
|
private readonly record struct ResolvedTexture(ulong Handle, uint Layer);
|
||||||
|
|
||||||
|
private ResolvedTexture ResolveTexture(
|
||||||
|
in RenderInstanceCandidate entity,
|
||||||
|
MeshRef meshRef,
|
||||||
|
ObjectRenderBatch batch,
|
||||||
|
PaletteCompositeIdentity paletteIdentity,
|
||||||
|
out bool compositePending) =>
|
||||||
|
ResolveTexture(
|
||||||
|
entity.LocalEntityId,
|
||||||
|
entity.PaletteOverride,
|
||||||
|
meshRef,
|
||||||
|
batch,
|
||||||
|
paletteIdentity,
|
||||||
|
out compositePending);
|
||||||
|
|
||||||
private ResolvedTexture ResolveTexture(
|
private ResolvedTexture ResolveTexture(
|
||||||
WorldEntity entity,
|
WorldEntity entity,
|
||||||
MeshRef meshRef,
|
MeshRef meshRef,
|
||||||
ObjectRenderBatch batch,
|
ObjectRenderBatch batch,
|
||||||
PaletteCompositeIdentity paletteIdentity,
|
PaletteCompositeIdentity paletteIdentity,
|
||||||
|
out bool compositePending) =>
|
||||||
|
ResolveTexture(
|
||||||
|
entity.Id,
|
||||||
|
entity.PaletteOverride,
|
||||||
|
meshRef,
|
||||||
|
batch,
|
||||||
|
paletteIdentity,
|
||||||
|
out compositePending);
|
||||||
|
|
||||||
|
private ResolvedTexture ResolveTexture(
|
||||||
|
uint localEntityId,
|
||||||
|
PaletteOverride? paletteOverride,
|
||||||
|
MeshRef meshRef,
|
||||||
|
ObjectRenderBatch batch,
|
||||||
|
PaletteCompositeIdentity paletteIdentity,
|
||||||
out bool compositePending)
|
out bool compositePending)
|
||||||
{
|
{
|
||||||
compositePending = false;
|
compositePending = false;
|
||||||
|
|
@ -3536,11 +3650,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
&& overrideOrigTex != 0;
|
&& overrideOrigTex != 0;
|
||||||
uint? origTexOverride = hasOrigTexOverride ? overrideOrigTex : (uint?)null;
|
uint? origTexOverride = hasOrigTexOverride ? overrideOrigTex : (uint?)null;
|
||||||
|
|
||||||
bool sourceIsPaletteIndexed = entity.PaletteOverride is not null
|
bool sourceIsPaletteIndexed = paletteOverride is not null
|
||||||
&& _textures.IsPaletteIndexed(surfaceId, origTexOverride);
|
&& _textures.IsPaletteIndexed(surfaceId, origTexOverride);
|
||||||
WbTextureResolutionKind resolution = WbTextureResolutionPolicy.Select(
|
WbTextureResolutionKind resolution = WbTextureResolutionPolicy.Select(
|
||||||
hasOrigTexOverride,
|
hasOrigTexOverride,
|
||||||
entity.PaletteOverride is not null,
|
paletteOverride is not null,
|
||||||
sourceIsPaletteIndexed);
|
sourceIsPaletteIndexed);
|
||||||
|
|
||||||
switch (resolution)
|
switch (resolution)
|
||||||
|
|
@ -3549,10 +3663,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
{
|
{
|
||||||
BindlessTextureLocation texture =
|
BindlessTextureLocation texture =
|
||||||
_textures.GetOrUploadWithPaletteOverrideBindless(
|
_textures.GetOrUploadWithPaletteOverrideBindless(
|
||||||
entity.Id,
|
localEntityId,
|
||||||
surfaceId,
|
surfaceId,
|
||||||
origTexOverride,
|
origTexOverride,
|
||||||
entity.PaletteOverride!,
|
paletteOverride!,
|
||||||
paletteIdentity);
|
paletteIdentity);
|
||||||
compositePending = texture.Handle == 0;
|
compositePending = texture.Handle == 0;
|
||||||
return new ResolvedTexture(texture.Handle, texture.Layer);
|
return new ResolvedTexture(texture.Handle, texture.Layer);
|
||||||
|
|
@ -3562,7 +3676,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
||||||
{
|
{
|
||||||
BindlessTextureLocation texture =
|
BindlessTextureLocation texture =
|
||||||
_textures.GetOrUploadWithOrigTextureOverrideBindless(
|
_textures.GetOrUploadWithOrigTextureOverrideBindless(
|
||||||
entity.Id,
|
localEntityId,
|
||||||
surfaceId,
|
surfaceId,
|
||||||
overrideOrigTex);
|
overrideOrigTex);
|
||||||
compositePending = texture.Handle == 0;
|
compositePending = texture.Handle == 0;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Reflection;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
public sealed class RenderInstanceCandidateTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void FromWorldEntity_CopiesDispatcherFactsWithoutRetainingEntity()
|
||||||
|
{
|
||||||
|
Quaternion rotation = Quaternion.CreateFromAxisAngle(
|
||||||
|
Vector3.UnitZ,
|
||||||
|
MathF.PI / 2f);
|
||||||
|
var entity = new WorldEntity
|
||||||
|
{
|
||||||
|
Id = 41,
|
||||||
|
ServerGuid = 0x8000_0041,
|
||||||
|
SourceGfxObjOrSetupId = 0x0200_0041,
|
||||||
|
Position = new Vector3(1f, 2f, 3f),
|
||||||
|
Rotation = rotation,
|
||||||
|
MeshRefs =
|
||||||
|
[
|
||||||
|
new MeshRef(0x0100_0001, Matrix4x4.Identity),
|
||||||
|
new MeshRef(
|
||||||
|
0x0100_0002,
|
||||||
|
Matrix4x4.CreateTranslation(4f, 0f, 0f)),
|
||||||
|
],
|
||||||
|
ParentCellId = 0xA9B4_0170,
|
||||||
|
Scale = 1.25f,
|
||||||
|
IsBuildingShell = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderInstanceCandidate candidate =
|
||||||
|
RenderInstanceCandidate.FromWorldEntity(
|
||||||
|
entity,
|
||||||
|
animated: true,
|
||||||
|
meshPartCount: 2,
|
||||||
|
tupleLandblockId: 0x1122_FFFF);
|
||||||
|
|
||||||
|
Assert.Equal(entity.Id, candidate.LocalEntityId);
|
||||||
|
Assert.Equal(entity.ServerGuid, candidate.ServerGuid);
|
||||||
|
Assert.Equal(entity.SourceGfxObjOrSetupId, candidate.SourceId);
|
||||||
|
Assert.Equal(entity.ParentCellId, candidate.ParentCell);
|
||||||
|
Assert.Equal(
|
||||||
|
Matrix4x4.CreateFromQuaternion(rotation)
|
||||||
|
* Matrix4x4.CreateTranslation(entity.Position),
|
||||||
|
candidate.RootWorld);
|
||||||
|
Assert.Equal(entity.AabbMin, candidate.Bounds.Minimum);
|
||||||
|
Assert.Equal(entity.AabbMax, candidate.Bounds.Maximum);
|
||||||
|
Assert.Equal(1.25f, candidate.Scale);
|
||||||
|
Assert.True(candidate.IsBuildingShell);
|
||||||
|
Assert.True(candidate.Animated);
|
||||||
|
Assert.Equal(2, candidate.MeshPartCount);
|
||||||
|
Assert.Equal(0xA9B4_FFFFu, candidate.CacheLandblockId);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
typeof(RenderInstanceCandidate).GetFields(
|
||||||
|
BindingFlags.Instance
|
||||||
|
| BindingFlags.Public
|
||||||
|
| BindingFlags.NonPublic),
|
||||||
|
field => field.FieldType == typeof(WorldEntity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OutdoorCandidate_KeepsTupleCacheHintSeparate()
|
||||||
|
{
|
||||||
|
var entity = new WorldEntity
|
||||||
|
{
|
||||||
|
Id = 42,
|
||||||
|
ServerGuid = 0,
|
||||||
|
SourceGfxObjOrSetupId = 0x0100_0042,
|
||||||
|
Position = Vector3.Zero,
|
||||||
|
Rotation = Quaternion.Identity,
|
||||||
|
MeshRefs =
|
||||||
|
[
|
||||||
|
new MeshRef(0x0100_0042, Matrix4x4.Identity),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderInstanceCandidate candidate =
|
||||||
|
RenderInstanceCandidate.FromWorldEntity(
|
||||||
|
entity,
|
||||||
|
animated: false,
|
||||||
|
meshPartCount: 1,
|
||||||
|
tupleLandblockId: 0xA9B5_FFFF);
|
||||||
|
|
||||||
|
Assert.Null(candidate.ParentCell);
|
||||||
|
Assert.Equal(0xA9B5_FFFFu, candidate.CacheLandblockId);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue