fix(streaming): retire stale portal-region entities

Port retail's 25-second leave-visibility lifetime over canonical live records, retaining spatially resident and owned entities while using the conservative ACE visibility envelope for nonresident records. Route expiry through the normal generation-safe F747 teardown so animations, effects, physics, and render owners unwind symmetrically.

Replace the append-only modern mesh buffer with coalescing vertex/index ranges and upload each mesh's vertices once instead of once per material. Released zero-reference meshes can now reuse GPU ranges after portal and cache churn.

A connected five-region round trip returned animation ownership to baseline, recreated the starting region on revisit, and held normal FPS. Release build succeeds and all 5,927 tests pass with five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-18 10:02:15 +02:00
parent 2cbf34a668
commit 3971997689
13 changed files with 918 additions and 117 deletions

View file

@ -0,0 +1,91 @@
using AcDream.App.World;
using AcDream.Core.Net.Messages;
namespace AcDream.App.Tests.World;
public sealed class LiveEntityLivenessControllerTests
{
[Fact]
public void OutOfRangeWorldEntityExpiresAfterTwentyFiveSeconds()
{
var tracker = new LiveEntityLivenessTracker();
var samples = new[] { Sample(0x7000_0001u, generation: 4, visible: false) };
Assert.Empty(tracker.Tick(10.0, samples));
Assert.Empty(tracker.Tick(34.999, samples));
Assert.Equal(
new LiveEntityPruneCandidate(0x7000_0001u, 4),
Assert.Single(tracker.Tick(35.0, samples)));
Assert.Equal(0, tracker.DeadlineCount);
}
[Fact]
public void ReturningToVisibilityCancelsTheDeadline()
{
var tracker = new LiveEntityLivenessTracker();
Assert.Empty(tracker.Tick(0.0, [Sample(1, 1, visible: false)]));
Assert.Empty(tracker.Tick(20.0, [Sample(1, 1, visible: true)]));
Assert.Empty(tracker.Tick(40.0, [Sample(1, 1, visible: false)]));
Assert.Empty(tracker.Tick(64.9, [Sample(1, 1, visible: false)]));
Assert.Single(tracker.Tick(65.0, [Sample(1, 1, visible: false)]));
}
[Fact]
public void NonWorldRetentionCancelsTheDeadline()
{
var tracker = new LiveEntityLivenessTracker();
Assert.Empty(tracker.Tick(0.0, [Sample(1, 1, visible: false)]));
Assert.Empty(tracker.Tick(30.0, [Sample(1, 1, visible: false, retained: true)]));
Assert.Equal(0, tracker.DeadlineCount);
}
[Fact]
public void ReusedGuidGetsANewGenerationDeadline()
{
var tracker = new LiveEntityLivenessTracker();
Assert.Empty(tracker.Tick(0.0, [Sample(1, 1, visible: false)]));
Assert.Empty(tracker.Tick(24.0, [Sample(1, 2, visible: false)]));
Assert.Empty(tracker.Tick(25.0, [Sample(1, 2, visible: false)]));
Assert.Equal(
new LiveEntityPruneCandidate(1, 2),
Assert.Single(tracker.Tick(49.0, [Sample(1, 2, visible: false)])));
}
[Fact]
public void RemovedRecordDropsItsDeadline()
{
var tracker = new LiveEntityLivenessTracker();
Assert.Empty(tracker.Tick(0.0, [Sample(1, 1, visible: false)]));
Assert.Empty(tracker.Tick(30.0, []));
Assert.Equal(0, tracker.DeadlineCount);
}
[Fact]
public void ConservativeVisibilityUsesGlobalLandblockCoordinates()
{
CreateObject.ServerPosition player = Position(0x3032_0001u, 190f, 20f, 5f);
CreateObject.ServerPosition adjacent = Position(0x3132_0001u, 1f, 20f, 5f);
CreateObject.ServerPosition exactlyTwoBlocks = Position(0x3232_0001u, 190f, 20f, 5f);
CreateObject.ServerPosition beyond = Position(0x3332_0001u, 1f, 20f, 5f);
Assert.True(LiveEntityLivenessController.IsWithinConservativeVisibility(player, adjacent));
Assert.True(LiveEntityLivenessController.IsWithinConservativeVisibility(player, exactlyTwoBlocks));
Assert.False(LiveEntityLivenessController.IsWithinConservativeVisibility(player, beyond));
}
private static LiveEntityLivenessSample Sample(
uint guid,
ushort generation,
bool visible,
bool retained = false) =>
new(guid, generation, visible, retained);
private static CreateObject.ServerPosition Position(
uint cell,
float x,
float y,
float z) =>
new(cell, x, y, z, 1f, 0f, 0f, 0f);
}