fix(render): harden portal exit handoff
Some checks failed
CI / linux-portable (push) Successful in 3m19s
CI / windows-gate (push) Failing after 6m43s
CI / release (push) Has been skipped

This commit is contained in:
Erik 2026-08-25 17:39:44 +02:00
parent ddbd7e4096
commit 82e4b4cb6d
17 changed files with 896 additions and 119 deletions

View file

@ -468,7 +468,7 @@ public sealed class LocalPlayerTeleportControllerTests
}
[Fact]
public void ExitSound_ReleasesDestinationBeforeHidingTunnelButKeepsProtocolActive()
public void ExitSound_HidesTunnelBeforeReleasingDestinationAndKeepsProtocolActive()
{
var order = new List<string>();
var harness = new Harness(order: order);
@ -481,6 +481,8 @@ public sealed class LocalPlayerTeleportControllerTests
harness.Controller.Tick(0.016f);
Assert.True(Index(order, "presentation-exit") < Index(order, "reservation-end"));
Assert.True(Index(order, "reservation-end") < Index(order, "exit-cue"));
Assert.False(harness.Presentation.IsPortalViewportVisible);
Assert.True(harness.Controller.IsActive);
Assert.False(harness.Reveal.Snapshot.Completed);
@ -1589,10 +1591,13 @@ public sealed class LocalPlayerTeleportControllerTests
harness.Controller.Tick(0.016f);
Assert.False(harness.Placement.Called);
// Viewport swap: reservation release + exit cue + tunnel retire
// Viewport swap: tunnel retire, reservation release, then exit cue
// (gmSmartBoxUI::UseTime, Sound_UI_ExitPortal @ 0x004D7405).
order.Clear();
harness.Presentation.Enqueue(TeleportAnimEvent.PlayExitSound);
harness.Controller.Tick(0.016f);
Assert.True(Index(order, "presentation-exit") < Index(order, "reservation-end"));
Assert.True(Index(order, "reservation-end") < Index(order, "exit-cue"));
Assert.Equal(["enter", "exit"], harness.Presentation.Cues);
Assert.False(harness.Presentation.IsPortalViewportVisible);
Assert.Single(harness.Streaming.ReservationEnds);
@ -2126,9 +2131,17 @@ public sealed class LocalPlayerTeleportControllerTests
public void TickTunnel(float deltaSeconds) => _order.Add("tunnel-tick");
public readonly List<string> Cues = new();
public void PlayEnterCue() => Cues.Add("enter");
public void PlayExitCue() => Cues.Add("exit");
public void PlayExitCue()
{
Cues.Add("exit");
_order.Add("exit-cue");
}
public void EnterTunnel() => IsPortalViewportVisible = true;
public void ExitTunnel() => IsPortalViewportVisible = false;
public void ExitTunnel()
{
IsPortalViewportVisible = false;
_order.Add("presentation-exit");
}
public void SetWaitCue(bool visible) => WaitCueValues.Add(visible);
public void Reset()

View file

@ -0,0 +1,129 @@
using AcDream.App.Rendering;
using AcDream.App.Streaming;
using AcDream.Runtime;
namespace AcDream.App.Tests.Streaming;
public sealed class RevealTimingProbeTests
{
[Fact]
public void ResourceSampling_IsLowFrequencyAndCoalescesSamePollEdges()
{
var resources = new CountingResourceSource();
var probe = new RevealTimingProbe(
loadedLandblockCount: static () => 4,
renderResources: resources);
probe.Begin(
kind: "Login",
generation: 2,
destinationCell: 0x1234_0001u,
window: new StreamingRevealWindow(NearRadius: 1, FarRadius: 2));
probe.Observe(
default,
RuntimePortalSnapshot.Idle with { Generation = 2 });
probe.Observe(
new WorldRevealReadinessSnapshot(
DestinationCell: 0x1234_0001u,
IsIndoor: false,
IsUnhydratable: false,
RequiredRenderRadius: 2,
RequiredNearRadius: 1,
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true),
RuntimePortalSnapshot.Idle with
{
Generation = 2,
Materialized = true,
WorldViewportObserved = true,
});
// Begin, one coalesced readiness-edge snapshot, and summary. The
// ordinary false poll does not touch the renderer resource owners.
Assert.Equal(3, resources.CaptureCount);
}
[Fact]
public void ResourceLineCarriesColdMeshTextureAndProcessAttribution()
{
MeshStreamResourceDiagnostics mesh =
default(MeshStreamResourceDiagnostics) with
{
RenderData = 12,
AtlasArrays = 3,
EstimatedBytes = 4_000,
GlobalUploadCount = 7,
GlobalUploadedBytes = 8_000,
FrameUploadCount = 2,
FrameUploadBytes = 900,
StagedUploadBacklog = 5,
StagedUploadBytes = 6_000,
GlobalCapacityBytes = 10_000,
GlobalPhysicalCapacityBytes = 20_000,
GlobalMigrationInProgress = true,
PreparedReads = 21,
PreparedLoaded = 20,
};
TextureStreamResourceDiagnostics textures =
default(TextureStreamResourceDiagnostics) with
{
OwnedBindlessTextures = 17,
TextureOwners = 15,
CachedCompositeTextures = 9,
CachedUnownedComposites = 2,
CachedUnownedCompositeBytes = 700,
CompositeAtlases = 2,
CompositeAtlasBytes = 30_000,
CompositeWarmupPending = 4,
FrameCompositeUploadCount = 3,
FrameCompositeUploadBytes = 1_200,
};
ProcessResourceDiagnostics process = new(
ManagedBytes: 40_000,
ManagedCommittedBytes: 50_000,
TrackedGpuBytes: 60_000,
TrackedGpuBuffers: 11,
TrackedGpuTextures: 13);
var snapshot = new RenderFrameResourceDiagnosticsSnapshot(
default,
default,
mesh,
textures,
process);
string line = RevealTimingProbe.FormatResourceLine(
"render-ready",
"Login",
generation: 2,
elapsedMilliseconds: 3456,
snapshot);
Assert.StartsWith(
"[reveal-resource] checkpoint=render-ready kind=Login gen=2 elapsedMs=3456",
line);
Assert.Contains("meshData=12", line);
Assert.Contains("globalUploads=7", line);
Assert.Contains("staged=5", line);
Assert.Contains("arenaMigrating=1", line);
Assert.Contains("prepared=0/21/20/0/0", line);
Assert.Contains("ownedTextures=17", line);
Assert.Contains("unownedCompositeBytes=700", line);
Assert.Contains("compositePending=4", line);
Assert.Contains("frameCompositeUploadBytes=1200", line);
Assert.Contains("managedBytes=40000", line);
Assert.Contains("trackedGpuBytes=60000", line);
}
private sealed class CountingResourceSource :
IRenderFrameResourceDiagnosticsSource
{
public int CaptureCount { get; private set; }
public RenderFrameResourceDiagnosticsSnapshot Capture()
{
CaptureCount++;
return default;
}
}
}

View file

@ -0,0 +1,23 @@
using AcDream.App.Streaming;
namespace AcDream.App.Tests.Streaming;
public sealed class StreamingDiagnosticsTests
{
[Theory]
[InlineData(null, null)]
[InlineData("", null)]
[InlineData("0", null)]
[InlineData("garbage", null)]
[InlineData("1", StreamingDiagnostics.DefaultTunnelFreezeFrame)]
[InlineData("true", StreamingDiagnostics.DefaultTunnelFreezeFrame)]
[InlineData("TRUE", StreamingDiagnostics.DefaultTunnelFreezeFrame)]
[InlineData("2", 2)]
[InlineData("72", 72)]
[InlineData("120", 120)]
[InlineData("121", null)]
public void TunnelFreezeParser_AcceptsTheDefaultAliasOrAnAuthoredFrame(
string? raw,
int? expected) =>
Assert.Equal(expected, StreamingDiagnostics.ParseTunnelFreezeFrame(raw));
}