checkpoint: preserve cathedral look-in investigation state

This commit is contained in:
Erik 2026-08-30 23:29:28 +02:00
parent c287db8651
commit 572de1ec30
11 changed files with 757 additions and 197 deletions

View file

@ -6,6 +6,7 @@ using AcDream.Core.Lighting;
using AcDream.Core.Meshing;
using AcDream.Core.Selection;
using AcDream.Core.World;
using AcDream.App.Rendering.Walk;
using DatReaderWriter.Enums;
namespace AcDream.App.Rendering.Wb;
@ -40,6 +41,8 @@ public sealed unsafe partial class WbDrawDispatcher
private RenderSceneGeneration _packedProductionGeneration;
private ulong _packedProductionFrameSequence;
private int _packedProductionNextRange;
private readonly Dictionary<(int RouteIndex, uint LocalEntityId, int PartIndex, uint GfxObjId), string>
_probeLookInPartStates = [];
internal IReadOnlyList<CurrentRenderDispatcherSubmission>
PackedDispatcherSubmissions => _packedSubmissions;
@ -295,7 +298,12 @@ public sealed unsafe partial class WbDrawDispatcher
source.MeshPartOffset,
source.MeshPartCount),
ref anyVao,
publishSelection);
publishSelection,
range.Route is RenderFrameCandidateRoute.LookInObject
? view.WalkLookInViews
: null,
range.RouteIndex,
range.CellId);
}
return new PackedRangeClassification(
@ -334,7 +342,10 @@ public sealed unsafe partial class WbDrawDispatcher
in RenderInstanceCandidate entity,
ReadOnlySpan<RenderFrameMeshPart> meshParts,
ref uint anyVao,
bool publishSelection)
bool publishSelection,
IWalkLookInViewSource? lookInViews,
int lookInRouteIndex,
uint lookInCellId)
{
(uint slot, bool culled) = ResolveSlotForFrame(
_clipRoutingActive,
@ -361,7 +372,8 @@ public sealed unsafe partial class WbDrawDispatcher
: new Vector2(0f, 1f);
PackedProjectionClassificationEntry? cacheEntry = null;
if (!entity.Animated)
bool lookInConeActive = lookInViews is not null;
if (!entity.Animated && !lookInConeActive)
{
PackedClassificationIdentity identity =
PackedClassificationIdentity.From(in projection);
@ -394,7 +406,7 @@ public sealed unsafe partial class WbDrawDispatcher
entity.ProjectionId,
in identity);
}
else
else if (entity.Animated)
{
_packedClassificationCache.RecordAnimatedClassification();
}
@ -485,6 +497,21 @@ public sealed unsafe partial class WbDrawDispatcher
partTransform * meshRef.PartTransform;
Matrix4x4 model =
restPose * entity.RootWorld;
int selectionPartIndex = unchecked(
(partIndex << 16)
| (setupPartIndex & 0xFFFF));
if (!PartVisibleInLookInTurn(
lookInViews,
lookInRouteIndex,
lookInCellId,
in entity,
selectionPartIndex,
(uint)gfxObjId,
partData,
model))
{
continue;
}
if (!ClassifyPackedBatches(
partData,
restPose,
@ -502,9 +529,6 @@ public sealed unsafe partial class WbDrawDispatcher
{
reusableAcrossFrames = false;
}
int selectionPartIndex = unchecked(
(partIndex << 16)
| (setupPartIndex & 0xFFFF));
cacheEntry?.SelectionParts.Add(
new PackedClassifiedSelectionPart(
selectionPartIndex,
@ -538,6 +562,18 @@ public sealed unsafe partial class WbDrawDispatcher
Matrix4x4 restPose = meshRef.PartTransform;
Matrix4x4 model = restPose * entity.RootWorld;
if (!PartVisibleInLookInTurn(
lookInViews,
lookInRouteIndex,
lookInCellId,
in entity,
partIndex,
meshRef.GfxObjId,
renderData,
model))
{
continue;
}
if (!ClassifyPackedBatches(
renderData,
restPose,
@ -576,6 +612,97 @@ public sealed unsafe partial class WbDrawDispatcher
}
}
/// <summary>
/// Retail RenderDeviceD3D::DrawMesh @0x005A0860 admits each CGfxObj
/// independently by transforming its drawing sphere into the active
/// portal view. A whole-entity sphere is not equivalent for multipart
/// creatures: the union can be roughly ten metres wide and intersect an
/// aperture while every actual body/equipment part is behind its wall.
/// </summary>
private bool PartVisibleInLookInTurn(
IWalkLookInViewSource? lookInViews,
int routeIndex,
uint cellId,
in RenderInstanceCandidate entity,
int partIndex,
uint gfxObjId,
ObjectRenderData renderData,
Matrix4x4 localToWorld)
{
if (lookInViews is null)
return true;
// ObjectRenderData.SelectionSphere is retained per GfxObj by the
// prepared mesh payload. It is never the entity-union sphere and is
// therefore the correct granularity for DrawMesh admission. The
// current package stores a conservative vertex-derived sphere here;
// this preserves whole-mesh drawing while avoiding the invalid
// aggregate-character admission that caused the cathedral bleed.
if (renderData.SelectionSphere is not { Radius: > 0f } sphere)
return true;
bool visible = LookInDrawingSphereVisible(
lookInViews,
routeIndex,
sphere,
localToWorld,
out Vector3 center,
out float radius);
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled
&& cellId == 0xF4180112u)
{
var key = (routeIndex, entity.LocalEntityId, partIndex, gfxObjId);
string state =
$"visible={(visible ? 1 : 0)} "
+ $"center=({center.X:F2},{center.Y:F2},{center.Z:F2}) "
+ $"r={radius:F2}";
if (!_probeLookInPartStates.TryGetValue(key, out string? previous)
|| previous != state)
{
_probeLookInPartStates[key] = state;
Console.WriteLine(
$"[lookin-part] route={routeIndex} cell=0x{cellId:X8} "
+ $"id={entity.LocalEntityId:x} part={partIndex} "
+ $"gfx=0x{gfxObjId:X8} {state}");
}
}
return visible;
}
internal static bool LookInDrawingSphereVisible(
IWalkLookInViewSource lookInViews,
int routeIndex,
DatReaderWriter.Types.Sphere sphere,
Matrix4x4 localToWorld,
out Vector3 center,
out float radius)
{
ArgumentNullException.ThrowIfNull(lookInViews);
ArgumentNullException.ThrowIfNull(sphere);
center = Vector3.Transform(sphere.Origin, localToWorld);
float scaleX = new Vector3(
localToWorld.M11,
localToWorld.M12,
localToWorld.M13).Length();
float scaleY = new Vector3(
localToWorld.M21,
localToWorld.M22,
localToWorld.M23).Length();
float scaleZ = new Vector3(
localToWorld.M31,
localToWorld.M32,
localToWorld.M33).Length();
radius = sphere.Radius
* MathF.Max(scaleX, MathF.Max(scaleY, scaleZ));
return lookInViews.SphereVisibleInLookInTurn(
routeIndex,
in center,
radius);
}
/// <summary>
/// Mirrors the production dispatcher's no-VAO early return. That return
/// records an empty submission with transparent deferral disabled even