fix(streaming): preserve portal destination ownership
Detach old-world spatial ownership atomically, prioritize destination retirement dependencies, and reveal the viewport at the retail transition edge. Give private paperdoll views independent mesh ownership and retain dormant ACE entities so portal revisits preserve server objects without extending active GPU lifetimes.
This commit is contained in:
parent
2c848d4167
commit
823936ec31
57 changed files with 2551 additions and 324 deletions
|
|
@ -463,6 +463,146 @@ public sealed class LandblockRetirementCoordinatorTests
|
|||
Assert.Equal(0, coordinator.PendingCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OriginRecenterAdoption_DuplicateReceiptFailsBeforeLedgerMutation()
|
||||
{
|
||||
const uint landblockId = 0x4647FFFFu;
|
||||
var state = new GpuWorldState();
|
||||
int detachedCallbacks = 0;
|
||||
LandblockRetirementCoordinator coordinator =
|
||||
LandblockRetirementCoordinator.CreateBudgeted(
|
||||
state,
|
||||
ticket => AdvancePresentationStep(ticket),
|
||||
ticket => CompletePresentation(ticket),
|
||||
_ => detachedCallbacks++);
|
||||
GpuLandblockRetirement[] receipts =
|
||||
[
|
||||
new(
|
||||
landblockId,
|
||||
LandblockRetirementKind.Full,
|
||||
Array.Empty<WorldEntity>()),
|
||||
new(
|
||||
landblockId & 0xFFFF0000u,
|
||||
LandblockRetirementKind.Full,
|
||||
Array.Empty<WorldEntity>()),
|
||||
];
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
coordinator.AdoptDetachedFull(receipts));
|
||||
|
||||
Assert.Equal(0, coordinator.PendingCount);
|
||||
Assert.False(coordinator.IsPending(landblockId));
|
||||
Assert.Equal(0, detachedCallbacks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OriginRecenterAdoption_ObserverFailureRetainsEveryCleanupReceipt()
|
||||
{
|
||||
uint[] landblockIds = [0x4648FFFFu, 0x4649FFFFu];
|
||||
var state = new GpuWorldState();
|
||||
int detachedCallbacks = 0;
|
||||
LandblockRetirementCoordinator coordinator =
|
||||
LandblockRetirementCoordinator.CreateBudgeted(
|
||||
state,
|
||||
ticket => AdvancePresentationStep(ticket),
|
||||
ticket => CompletePresentation(ticket),
|
||||
retirement =>
|
||||
{
|
||||
detachedCallbacks++;
|
||||
if (retirement.LandblockId == landblockIds[0])
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"injected detached observer failure");
|
||||
}
|
||||
});
|
||||
GpuLandblockRetirement[] receipts = landblockIds
|
||||
.Select(id => new GpuLandblockRetirement(
|
||||
id,
|
||||
LandblockRetirementKind.Full,
|
||||
Array.Empty<WorldEntity>()))
|
||||
.ToArray();
|
||||
|
||||
Exception? failure = coordinator.AdoptDetachedFull(receipts);
|
||||
|
||||
Assert.IsType<InvalidOperationException>(failure);
|
||||
Assert.Equal(2, detachedCallbacks);
|
||||
Assert.Equal(2, coordinator.PendingCount);
|
||||
Assert.All(landblockIds, id => Assert.True(coordinator.IsPending(id)));
|
||||
|
||||
int frames = 0;
|
||||
while (coordinator.PendingCount != 0 && frames++ < 64)
|
||||
{
|
||||
var meter = new StreamingWorkMeter(Budget(maxEntityOperations: 64));
|
||||
coordinator.Advance(meter);
|
||||
meter.FinishFrame();
|
||||
}
|
||||
|
||||
Assert.Equal(0, coordinator.PendingCount);
|
||||
Assert.All(landblockIds, id => Assert.False(coordinator.IsPending(id)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DestinationDependency_CompletesOutOfOrder_WithoutReorderingCleanupFifo()
|
||||
{
|
||||
uint[] landblockIds =
|
||||
[
|
||||
0x5151FFFFu,
|
||||
0x5252FFFFu,
|
||||
0x5353FFFFu,
|
||||
];
|
||||
uint destinationId = landblockIds[2];
|
||||
var terrainOrder = new List<uint>();
|
||||
var state = new GpuWorldState();
|
||||
LandblockRetirementCoordinator coordinator =
|
||||
LandblockRetirementCoordinator.CreateBudgeted(
|
||||
state,
|
||||
ticket =>
|
||||
{
|
||||
if (ticket.NextIncompleteStage
|
||||
== LandblockRetirementStage.Terrain)
|
||||
{
|
||||
return ticket.RunOnceStep(
|
||||
LandblockRetirementStage.Terrain,
|
||||
() => terrainOrder.Add(ticket.LandblockId));
|
||||
}
|
||||
|
||||
return AdvancePresentationStep(ticket);
|
||||
},
|
||||
ticket => CompletePresentation(
|
||||
ticket,
|
||||
terrain: () => terrainOrder.Add(ticket.LandblockId)));
|
||||
GpuLandblockRetirement[] receipts = landblockIds
|
||||
.Select(id => new GpuLandblockRetirement(
|
||||
id,
|
||||
LandblockRetirementKind.Full,
|
||||
Array.Empty<WorldEntity>()))
|
||||
.ToArray();
|
||||
Assert.Null(coordinator.AdoptDetachedFull(receipts));
|
||||
|
||||
var destinationMeter = new StreamingWorkMeter(
|
||||
Budget(maxEntityOperations: 64),
|
||||
destinationReservationActive: true);
|
||||
using (destinationMeter.EnterLane(StreamingWorkLane.Destination))
|
||||
coordinator.AdvancePriority(destinationId, destinationMeter);
|
||||
destinationMeter.FinishFrame();
|
||||
|
||||
Assert.False(coordinator.IsPending(destinationId));
|
||||
Assert.True(coordinator.IsPending(landblockIds[0]));
|
||||
Assert.True(coordinator.IsPending(landblockIds[1]));
|
||||
Assert.Equal(2, coordinator.PendingCount);
|
||||
Assert.Equal([destinationId], terrainOrder);
|
||||
|
||||
var cleanupMeter = new StreamingWorkMeter(
|
||||
Budget(maxEntityOperations: 64));
|
||||
coordinator.Advance(cleanupMeter);
|
||||
cleanupMeter.FinishFrame();
|
||||
|
||||
Assert.Equal(0, coordinator.PendingCount);
|
||||
Assert.Equal(
|
||||
[destinationId, landblockIds[0], landblockIds[1]],
|
||||
terrainOrder);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObserverFailure_DrainsReentrantDifferentIdBeginBeforeRethrow()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue