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 AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering.Selection;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -10,7 +8,8 @@ namespace AcDream.App.Rendering.Selection;
|
|||
internal interface IRetailSelectionRenderSink
|
||||
{
|
||||
void AddVisiblePart(
|
||||
WorldEntity entity,
|
||||
uint serverGuid,
|
||||
uint localEntityId,
|
||||
int partIndex,
|
||||
uint gfxObjId,
|
||||
Matrix4x4 partWorld);
|
||||
|
|
|
|||
|
|
@ -103,6 +103,22 @@ internal sealed class RetailSelectionScene :
|
|||
int partIndex,
|
||||
uint gfxObjId,
|
||||
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
|
||||
// viewports (paperdoll and appraisal). Those draws occur after the
|
||||
|
|
@ -110,10 +126,13 @@ internal sealed class RetailSelectionScene :
|
|||
// enter the world picking product.
|
||||
if (!_frameOpen)
|
||||
return;
|
||||
if (entity.ServerGuid == 0u)
|
||||
if (serverGuid == 0u)
|
||||
return;
|
||||
|
||||
if (!_buildingKeys.Add(new PartKey(entity.Id, partIndex, gfxObjId)))
|
||||
if (!_buildingKeys.Add(new PartKey(
|
||||
localEntityId,
|
||||
partIndex,
|
||||
gfxObjId)))
|
||||
return;
|
||||
RetailSelectionMesh? mesh = _geometry.Resolve(gfxObjId);
|
||||
if (mesh is null)
|
||||
|
|
@ -123,14 +142,14 @@ internal sealed class RetailSelectionScene :
|
|||
return;
|
||||
|
||||
_building.Add(new RetailSelectionPart(
|
||||
entity.ServerGuid,
|
||||
entity.Id,
|
||||
serverGuid,
|
||||
localEntityId,
|
||||
partIndex,
|
||||
partWorld,
|
||||
mesh));
|
||||
_currentRenderSceneObserver?.ObserveSelectionPart(
|
||||
entity.ServerGuid,
|
||||
entity.Id,
|
||||
serverGuid,
|
||||
localEntityId,
|
||||
partIndex,
|
||||
gfxObjId,
|
||||
partWorld,
|
||||
|
|
|
|||
|
|
@ -673,6 +673,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes = ~480 KB / frame
|
||||
// of GC pressure on the render thread under the original T17 shape.
|
||||
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
|
||||
// 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)
|
||||
=> 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>
|
||||
/// #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
|
||||
|
|
@ -1325,17 +1372,31 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
/// 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).
|
||||
/// </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;
|
||||
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 refsCount = 0;
|
||||
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.Z < tzMin) tzMin = 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);
|
||||
if (!first && prev == sig) return;
|
||||
_entityDumpSig[entity.Id] = sig;
|
||||
|
|
@ -1365,20 +1426,29 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
? (_tier1CacheDisabled ? "disabled" : "miss")
|
||||
: $"hit:{cacheBatches} restZero={restZero} restZ=[{rzMin:F2}..{rzMax:F2}]";
|
||||
Console.WriteLine(
|
||||
$"[dump-entity] DRAW{(first ? "" : "-CHANGED")} id=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} " +
|
||||
$"lb=0x{landblockId:X8} cell=0x{(entity.ParentCellId ?? 0u):X8} " +
|
||||
$"[dump-entity] DRAW{(first ? "" : "-CHANGED")} id=0x{entity.Id:X8} src=0x{entity.SourceId: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} " +
|
||||
$"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)
|
||||
{
|
||||
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;
|
||||
bool loaded = _meshAdapter.TryGetRenderData(mr.GfxObjId) is not null;
|
||||
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,
|
||||
walkResult.EntitiesWalked,
|
||||
_walkScratch);
|
||||
BuildCurrentCandidateTuples(
|
||||
_walkScratch,
|
||||
animatedEntityIds,
|
||||
_candidateTupleScratch);
|
||||
|
||||
// Tier 1 cache (#53) flush-tracking locals. _walkScratch holds one tuple
|
||||
// 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.
|
||||
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++;
|
||||
|
||||
// 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
|
||||
// landblock, never the Draw call's tuple landblock (which is the
|
||||
// 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;
|
||||
if (isNewEntity)
|
||||
|
|
@ -1599,7 +1676,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// The whole decision (including the routing-active gate) lives in
|
||||
// the pure ResolveSlotForFrame helper so it's unit-testable.
|
||||
(_currentEntitySlot, _currentEntityCulled) = ResolveSlotForFrame(
|
||||
_clipRoutingActive, entity.ServerGuid, entity.ParentCellId,
|
||||
_clipRoutingActive, entity.ServerGuid, entity.ParentCell,
|
||||
_cellIdToSlot, _outdoorSlot, _outdoorVisible);
|
||||
if (_currentEntityCulled)
|
||||
probeCulledEntities++;
|
||||
|
|
@ -1619,14 +1696,18 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// #119 decisive probe: one-shot dump (+ change re-emission) for
|
||||
// ACDREAM_DUMP_ENTITY-targeted entities. Before the culled-continue
|
||||
// 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
|
||||
// its position + light set (a floor-coincident static/plate would be
|
||||
// the z-fight's second draw; the player entity is the positive
|
||||
// control). Before the culled-continue, like the dump above.
|
||||
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeSeamDrawEnabled
|
||||
&& entity.ParentCellId is { } seamPc
|
||||
&& entity.ParentCell is { } seamPc
|
||||
&& AcDream.Core.Rendering.RenderingDiagnostics.SeamDrawTargetCells.Contains(seamPc))
|
||||
MaybeEmitSeamEnt(entity);
|
||||
}
|
||||
|
|
@ -1650,11 +1731,9 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (_currentEntityCulled)
|
||||
continue;
|
||||
|
||||
var entityWorld =
|
||||
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
||||
Matrix4x4.CreateTranslation(entity.Position);
|
||||
Matrix4x4 entityWorld = entity.RootWorld;
|
||||
|
||||
bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true;
|
||||
bool isAnimated = entity.Animated;
|
||||
|
||||
// Cache-hit fast path (Task 10): static entity with a populated
|
||||
// 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.
|
||||
if (anyVao == 0)
|
||||
{
|
||||
var firstMeshRef = entity.MeshRefs[partIdx];
|
||||
MeshRef firstMeshRef = tuple.MeshRef;
|
||||
var firstRenderData = _meshAdapter.TryGetRenderData(firstMeshRef.GfxObjId);
|
||||
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
|
||||
// overrides become relevant only for hot-swap (0xF625
|
||||
// ObjDescEvent) which today rebuilds MeshRefs anyway.
|
||||
var meshRef = entity.MeshRefs[partIdx];
|
||||
MeshRef meshRef = tuple.MeshRef;
|
||||
ulong gfxObjId = meshRef.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))
|
||||
currentEntityIncomplete = true;
|
||||
_selectionSink?.AddVisiblePart(
|
||||
entity,
|
||||
entity.ServerGuid,
|
||||
entity.LocalEntityId,
|
||||
unchecked((partIdx << 16) | (setupPartIndex & 0xFFFF)),
|
||||
(uint)partGfxObjId,
|
||||
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))
|
||||
currentEntityIncomplete = true;
|
||||
_selectionSink?.AddVisiblePart(
|
||||
entity,
|
||||
entity.ServerGuid,
|
||||
entity.LocalEntityId,
|
||||
partIdx,
|
||||
(uint)gfxObjId,
|
||||
model);
|
||||
|
|
@ -2354,13 +2435,14 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
|
||||
private void PublishCachedSelectionParts(
|
||||
EntityCacheEntry cachedEntry,
|
||||
WorldEntity entity,
|
||||
in RenderInstanceCandidate entity,
|
||||
Matrix4x4 entityWorld)
|
||||
{
|
||||
foreach (CachedSelectionPart part in cachedEntry.SelectionParts)
|
||||
{
|
||||
_selectionSink!.AddVisiblePart(
|
||||
entity,
|
||||
entity.ServerGuid,
|
||||
entity.LocalEntityId,
|
||||
part.PartIndex,
|
||||
part.GfxObjId,
|
||||
part.RestPose * entityWorld);
|
||||
|
|
@ -3379,14 +3461,14 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// ever live in the target cells.
|
||||
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 snap = _pointSnapshot;
|
||||
var sb = new System.Text.StringBuilder(200);
|
||||
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=[",
|
||||
entity.ServerGuid, entity.ParentCellId ?? 0u,
|
||||
entity.ServerGuid, entity.ParentCellId,
|
||||
entity.Position.X, entity.Position.Y, entity.Position.Z,
|
||||
_currentEntityCulled ? 1 : 0, _currentEntitySlot, _currentEntityIndoor ? 1 : 0);
|
||||
bool any = false;
|
||||
|
|
@ -3409,12 +3491,14 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
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
|
||||
// fires below. Both the torch selection and the sun gate use the same predicate,
|
||||
// so they can't disagree — one call, one truth.
|
||||
_currentEntityIndoor = IndoorObjectReceivesTorches(entity.ParentCellId);
|
||||
_currentEntityIndoor =
|
||||
IndoorObjectReceivesTorches(entity.ParentCell);
|
||||
|
||||
_currentEntityLightSet = InstanceLightSet.Disabled;
|
||||
var snap = _pointSnapshot;
|
||||
|
|
@ -3423,9 +3507,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// Retail useSunlight gate: outdoor objects receive no per-object torches.
|
||||
if (!_currentEntityIndoor) return; // #142: reuse the cached flag (was: IndoorObjectReceivesTorches(...))
|
||||
|
||||
if (entity.AabbDirty) entity.RefreshAabb();
|
||||
Vector3 center = (entity.AabbMin + entity.AabbMax) * 0.5f;
|
||||
float radius = (entity.AabbMax - entity.AabbMin).Length() * 0.5f;
|
||||
Vector3 center =
|
||||
(entity.Bounds.Minimum + entity.Bounds.Maximum) * 0.5f;
|
||||
float radius =
|
||||
(entity.Bounds.Maximum - entity.Bounds.Minimum).Length() * 0.5f;
|
||||
Array.Fill(_currentEntityLightSetScratch, -1);
|
||||
LightManager.SelectForObject(snap, center, radius, _currentEntityLightSetScratch);
|
||||
_currentEntityLightSet = InstanceLightSet.From(_currentEntityLightSetScratch);
|
||||
|
|
@ -3461,7 +3546,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
private bool ClassifyBatches(
|
||||
ObjectRenderData renderData,
|
||||
Matrix4x4 model,
|
||||
WorldEntity entity,
|
||||
in RenderInstanceCandidate entity,
|
||||
MeshRef meshRef,
|
||||
PaletteCompositeIdentity paletteIdentity,
|
||||
Matrix4x4 restPose,
|
||||
|
|
@ -3483,7 +3568,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
translucency = TranslucencyKind.AlphaBlend;
|
||||
|
||||
ResolvedTexture texture = ResolveTexture(
|
||||
entity,
|
||||
in entity,
|
||||
meshRef,
|
||||
batch,
|
||||
paletteIdentity,
|
||||
|
|
@ -3518,11 +3603,40 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
|
||||
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(
|
||||
WorldEntity entity,
|
||||
MeshRef meshRef,
|
||||
ObjectRenderBatch batch,
|
||||
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)
|
||||
{
|
||||
compositePending = false;
|
||||
|
|
@ -3536,11 +3650,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
&& overrideOrigTex != 0;
|
||||
uint? origTexOverride = hasOrigTexOverride ? overrideOrigTex : (uint?)null;
|
||||
|
||||
bool sourceIsPaletteIndexed = entity.PaletteOverride is not null
|
||||
bool sourceIsPaletteIndexed = paletteOverride is not null
|
||||
&& _textures.IsPaletteIndexed(surfaceId, origTexOverride);
|
||||
WbTextureResolutionKind resolution = WbTextureResolutionPolicy.Select(
|
||||
hasOrigTexOverride,
|
||||
entity.PaletteOverride is not null,
|
||||
paletteOverride is not null,
|
||||
sourceIsPaletteIndexed);
|
||||
|
||||
switch (resolution)
|
||||
|
|
@ -3549,10 +3663,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
BindlessTextureLocation texture =
|
||||
_textures.GetOrUploadWithPaletteOverrideBindless(
|
||||
entity.Id,
|
||||
localEntityId,
|
||||
surfaceId,
|
||||
origTexOverride,
|
||||
entity.PaletteOverride!,
|
||||
paletteOverride!,
|
||||
paletteIdentity);
|
||||
compositePending = texture.Handle == 0;
|
||||
return new ResolvedTexture(texture.Handle, texture.Layer);
|
||||
|
|
@ -3562,7 +3676,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
BindlessTextureLocation texture =
|
||||
_textures.GetOrUploadWithOrigTextureOverrideBindless(
|
||||
entity.Id,
|
||||
localEntityId,
|
||||
surfaceId,
|
||||
overrideOrigTex);
|
||||
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