feat(rendering): journal static scene projections
Append exact static and EnvCell-shell projection deltas only at committed spatial activation and detach receipts. Same-landblock rehydrate now reconciles retained, new, and omitted presentation identities without constructing or drawing the shadow scene in production. Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
This commit is contained in:
parent
dbd8318417
commit
5d19c56d15
10 changed files with 997 additions and 10 deletions
|
|
@ -0,0 +1,280 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Scene.Arch;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class StaticRenderProjectionJournalTests
|
||||
{
|
||||
private const uint LandblockId = 0x1234FFFFu;
|
||||
|
||||
[Fact]
|
||||
public void Reconcile_RegistersOnlyStaticEntitiesAndEnvCellShells()
|
||||
{
|
||||
RenderSceneGeneration generation = Generation(1);
|
||||
var journal = new RenderProjectionJournal(generation);
|
||||
var statics = new StaticRenderProjectionJournal(journal);
|
||||
WorldEntity staticEntity = Entity(7, position: new Vector3(1, 2, 3));
|
||||
WorldEntity liveEntity = Entity(8, serverGuid: 0x80000008u);
|
||||
LandblockBuild build = Build(
|
||||
LandblockId,
|
||||
[staticEntity, liveEntity],
|
||||
includeShell: true);
|
||||
var publication = Publication(build);
|
||||
|
||||
statics.Reconcile(build, publication);
|
||||
|
||||
Assert.Equal(2, statics.ProjectionCount);
|
||||
Assert.Equal(2, journal.Count);
|
||||
Assert.All(
|
||||
journal.Pending.ToArray(),
|
||||
delta => Assert.Equal(
|
||||
RenderProjectionDeltaKind.Register,
|
||||
delta.Kind));
|
||||
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
RenderDeltaApplyResult applied = journal.DrainTo(scene);
|
||||
Assert.Equal(2, applied.Registered);
|
||||
Assert.Equal(0, journal.Count);
|
||||
Assert.Equal(2, scene.Counts.Total);
|
||||
Assert.Equal(1, scene.Counts.OutdoorStatic);
|
||||
Assert.Equal(1, scene.Counts.IndoorCellStatic);
|
||||
|
||||
RenderProjectionId staticId =
|
||||
StaticRenderProjectionJournal.StaticEntityId(
|
||||
LandblockId,
|
||||
staticEntity.Id);
|
||||
Assert.True(scene.OpenQuery().TryGet(staticId, out var projected));
|
||||
CurrentRenderProjectionFingerprint expected =
|
||||
CurrentRenderSceneOracle.CreateProjectionFingerprint(
|
||||
LandblockId,
|
||||
staticEntity);
|
||||
Assert.Equal(expected.Transform, projected.Source.TransformFingerprint);
|
||||
Assert.Equal(expected.Geometry, projected.Source.GeometryFingerprint);
|
||||
Assert.Equal(expected.Appearance, projected.Source.AppearanceFingerprint);
|
||||
Assert.Equal(LandblockId, projected.Residency.OwnerLandblockId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reconcile_SameLandblockRetainsUpdatesOmitsAndAddsDeterministically()
|
||||
{
|
||||
RenderSceneGeneration generation = Generation(2);
|
||||
var journal = new RenderProjectionJournal(generation);
|
||||
var statics = new StaticRenderProjectionJournal(journal);
|
||||
WorldEntity retained = Entity(1);
|
||||
WorldEntity omitted = Entity(2);
|
||||
LandblockBuild first = Build(
|
||||
LandblockId,
|
||||
[retained, omitted],
|
||||
includeShell: true);
|
||||
statics.Reconcile(first, Publication(first));
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
journal.DrainTo(scene);
|
||||
RenderProjectionId retainedId =
|
||||
StaticRenderProjectionJournal.StaticEntityId(LandblockId, 1);
|
||||
Assert.True(scene.OpenQuery().TryGet(retainedId, out var before));
|
||||
|
||||
WorldEntity moved = Entity(1, position: new Vector3(10, 0, 0));
|
||||
WorldEntity added = Entity(3);
|
||||
LandblockBuild replacement = Build(
|
||||
LandblockId,
|
||||
[moved, added],
|
||||
includeShell: false);
|
||||
statics.Reconcile(replacement, Publication(replacement));
|
||||
|
||||
Assert.Equal(4, journal.Count);
|
||||
Assert.Equal(
|
||||
[
|
||||
RenderProjectionDeltaKind.UpdateTransform,
|
||||
RenderProjectionDeltaKind.Register,
|
||||
RenderProjectionDeltaKind.Unregister,
|
||||
RenderProjectionDeltaKind.Unregister,
|
||||
],
|
||||
journal.Pending.ToArray().Select(delta => delta.Kind).ToArray());
|
||||
|
||||
journal.DrainTo(scene);
|
||||
Assert.Equal(2, scene.Counts.Total);
|
||||
Assert.True(scene.OpenQuery().TryGet(retainedId, out var after));
|
||||
Assert.Equal(before.OwnerIncarnation, after.OwnerIncarnation);
|
||||
Assert.Equal(moved.Position, after.Transform.Position);
|
||||
Assert.False(scene.OpenQuery().TryGet(
|
||||
StaticRenderProjectionJournal.StaticEntityId(LandblockId, 2),
|
||||
out _));
|
||||
Assert.False(scene.OpenQuery().TryGet(
|
||||
StaticRenderProjectionJournal.EnvCellShellId(
|
||||
LandblockId,
|
||||
(LandblockId & 0xFFFF0000u) | 0x0100u),
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProjectionIdentity_DistinguishesDuplicateLocalIdsAcrossLandblocks()
|
||||
{
|
||||
const uint otherLandblock = 0x5678FFFFu;
|
||||
var journal = new RenderProjectionJournal(Generation(3));
|
||||
var statics = new StaticRenderProjectionJournal(journal);
|
||||
LandblockBuild first = Build(
|
||||
LandblockId,
|
||||
[Entity(42)],
|
||||
includeShell: false);
|
||||
LandblockBuild second = Build(
|
||||
otherLandblock,
|
||||
[Entity(42)],
|
||||
includeShell: false);
|
||||
|
||||
statics.Reconcile(first, Publication(first));
|
||||
statics.Reconcile(second, Publication(second));
|
||||
|
||||
Assert.Equal(2, statics.ProjectionCount);
|
||||
Assert.NotEqual(
|
||||
StaticRenderProjectionJournal.StaticEntityId(LandblockId, 42),
|
||||
StaticRenderProjectionJournal.StaticEntityId(otherLandblock, 42));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaleFarPublication_DoesNotMutateProjectionJournal()
|
||||
{
|
||||
var journal = new RenderProjectionJournal(Generation(4));
|
||||
var statics = new StaticRenderProjectionJournal(journal);
|
||||
LandblockBuild build = Build(
|
||||
LandblockId,
|
||||
[Entity(1)],
|
||||
includeShell: true);
|
||||
var stale = Publication(build) with { RequiresActivation = false };
|
||||
|
||||
statics.Reconcile(build, stale);
|
||||
|
||||
Assert.Equal(0, statics.ProjectionCount);
|
||||
Assert.Equal(0, journal.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Retire_IsExactAndIdempotentForFullAndNearLayerReceipts()
|
||||
{
|
||||
var journal = new RenderProjectionJournal(Generation(5));
|
||||
var statics = new StaticRenderProjectionJournal(journal);
|
||||
LandblockBuild build = Build(
|
||||
LandblockId,
|
||||
[Entity(1), Entity(2)],
|
||||
includeShell: true);
|
||||
statics.Reconcile(build, Publication(build));
|
||||
using var scene = new ArchRenderScene(Generation(5));
|
||||
journal.DrainTo(scene);
|
||||
|
||||
var retirement = new GpuLandblockRetirement(
|
||||
LandblockId,
|
||||
LandblockRetirementKind.NearLayer,
|
||||
build.Landblock.Entities);
|
||||
statics.Retire(retirement);
|
||||
statics.Retire(retirement);
|
||||
|
||||
Assert.Equal(3, journal.Count);
|
||||
Assert.Equal(0, statics.ProjectionCount);
|
||||
Assert.All(
|
||||
journal.Pending.ToArray(),
|
||||
delta => Assert.Equal(
|
||||
RenderProjectionDeltaKind.Unregister,
|
||||
delta.Kind));
|
||||
RenderDeltaApplyResult result = journal.DrainTo(scene);
|
||||
Assert.Equal(3, result.Unregistered);
|
||||
Assert.Equal(0, scene.Counts.Total);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_AdvancesExactGenerationAndDropsRetainedState()
|
||||
{
|
||||
var journal = new RenderProjectionJournal(Generation(6));
|
||||
var statics = new StaticRenderProjectionJournal(journal);
|
||||
LandblockBuild build = Build(
|
||||
LandblockId,
|
||||
[Entity(1)],
|
||||
includeShell: true);
|
||||
statics.Reconcile(build, Publication(build));
|
||||
|
||||
statics.Clear(Generation(7));
|
||||
|
||||
Assert.Equal(Generation(7), journal.Generation);
|
||||
Assert.Equal(0, journal.Count);
|
||||
Assert.Equal(0, statics.ProjectionCount);
|
||||
Assert.Equal(0, statics.LandblockCount);
|
||||
Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => statics.Clear(Generation(7)));
|
||||
}
|
||||
|
||||
private static GpuLandblockSpatialPublication Publication(
|
||||
LandblockBuild build,
|
||||
bool requiresActivation = true) =>
|
||||
new(
|
||||
build.LandblockId,
|
||||
build.Landblock,
|
||||
build.EnvCells?.Shells.Select(shell => shell.GeometryId).ToArray()
|
||||
?? [],
|
||||
build.Landblock.Entities.Where(entity => entity.ServerGuid == 0).ToArray(),
|
||||
requiresActivation);
|
||||
|
||||
private static LandblockBuild Build(
|
||||
uint landblockId,
|
||||
IReadOnlyList<WorldEntity> entities,
|
||||
bool includeShell)
|
||||
{
|
||||
EnvCellLandblockBuild? envCells = null;
|
||||
if (includeShell)
|
||||
{
|
||||
uint cellId = (landblockId & 0xFFFF0000u) | 0x0100u;
|
||||
envCells = new EnvCellLandblockBuild(
|
||||
landblockId,
|
||||
Array.Empty<LoadedCell>(),
|
||||
[
|
||||
new EnvCellShellPlacement(
|
||||
cellId,
|
||||
0x2_0000_0001UL,
|
||||
0x0D000001u,
|
||||
1,
|
||||
ImmutableArray<ushort>.Empty,
|
||||
new Vector3(4, 5, 6),
|
||||
Quaternion.Identity,
|
||||
Matrix4x4.CreateTranslation(4, 5, 6),
|
||||
new WbBoundingBox(Vector3.Zero, Vector3.One),
|
||||
new WbBoundingBox(
|
||||
new Vector3(4, 5, 6),
|
||||
new Vector3(5, 6, 7))),
|
||||
]);
|
||||
}
|
||||
|
||||
return new LandblockBuild(
|
||||
new LoadedLandblock(
|
||||
landblockId,
|
||||
new LandBlock(),
|
||||
entities,
|
||||
PhysicsDatBundle.Empty),
|
||||
envCells);
|
||||
}
|
||||
|
||||
private static WorldEntity Entity(
|
||||
uint id,
|
||||
uint serverGuid = 0,
|
||||
Vector3 position = default) =>
|
||||
new()
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = serverGuid,
|
||||
SourceGfxObjOrSetupId = 0x01000000u + id,
|
||||
Position = position,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs =
|
||||
[
|
||||
new MeshRef(
|
||||
0x01010000u + id,
|
||||
Matrix4x4.CreateTranslation(0, 0, id)),
|
||||
],
|
||||
};
|
||||
|
||||
private static RenderSceneGeneration Generation(ulong value) =>
|
||||
RenderSceneGeneration.FromRaw(value);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
|
|
@ -989,6 +990,40 @@ public sealed class LandblockPresentationPipelineTests
|
|||
Assert.Equal(2, meshes.PinAttempts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticRenderProjection_ReconcilesOnlyAfterSuccessfulSpatialActivation()
|
||||
{
|
||||
const uint landblockId = 0x6363FFFFu;
|
||||
var meshes = new FailFirstPreparedPinAdapter();
|
||||
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
||||
var journal = new RenderProjectionJournal(
|
||||
RenderSceneGeneration.FromRaw(1));
|
||||
var statics = new StaticRenderProjectionJournal(journal);
|
||||
LandblockBuild build = BuildWithShell(landblockId, Entity(32));
|
||||
var result = new LandblockStreamResult.Loaded(
|
||||
landblockId,
|
||||
LandblockStreamTier.Near,
|
||||
build,
|
||||
EmptyMesh());
|
||||
var pipeline = new LandblockPresentationPipeline(
|
||||
static (_, _) => { },
|
||||
state,
|
||||
staticProjectionSink: statics);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => pipeline.PublishLoaded(result));
|
||||
Assert.Equal(0, journal.Count);
|
||||
Assert.Equal(0, statics.ProjectionCount);
|
||||
|
||||
pipeline.PublishLoaded(result);
|
||||
|
||||
Assert.Equal(2, journal.Count);
|
||||
Assert.Equal(2, statics.ProjectionCount);
|
||||
Assert.Equal(2, meshes.PinAttempts);
|
||||
pipeline.PublishLoaded(result);
|
||||
Assert.Equal(2, journal.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PriorityFailure_PreservesFailingResultAndUntouchedDrainedSuffix()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue