fix(rendering): isolate world selection transactions

Keep shared private entity viewports outside the normal-world picking product and make the injected spatial-pin retry test independent of the production frame clock.

Co-Authored-By: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-24 22:31:15 +02:00
parent 0eb6648589
commit 056bbd4efd
3 changed files with 54 additions and 1 deletions

View file

@ -31,6 +31,7 @@ internal sealed class RetailSelectionScene :
private readonly HashSet<PartKey> _buildingKeys = new(); private readonly HashSet<PartKey> _buildingKeys = new();
private FrustumPlanes? _viewFrustum; private FrustumPlanes? _viewFrustum;
private ICurrentRenderSelectionObserver? _currentRenderSceneObserver; private ICurrentRenderSelectionObserver? _currentRenderSceneObserver;
private bool _frameOpen;
private readonly record struct PartKey(uint LocalEntityId, int PartIndex, uint GfxObjId); private readonly record struct PartKey(uint LocalEntityId, int PartIndex, uint GfxObjId);
@ -66,6 +67,7 @@ internal sealed class RetailSelectionScene :
public void Reset() public void Reset()
{ {
_currentRenderSceneObserver?.AbortSelectionFrame(); _currentRenderSceneObserver?.AbortSelectionFrame();
_frameOpen = false;
_building.Clear(); _building.Clear();
_published.Clear(); _published.Clear();
_buildingKeys.Clear(); _buildingKeys.Clear();
@ -75,6 +77,13 @@ internal sealed class RetailSelectionScene :
public void BeginFrame() public void BeginFrame()
{ {
if (_frameOpen)
{
throw new InvalidOperationException(
"The retail selection scene cannot begin a second frame before completing or aborting the first.");
}
_frameOpen = true;
_building.Clear(); _building.Clear();
_buildingKeys.Clear(); _buildingKeys.Clear();
_viewFrustum = null; _viewFrustum = null;
@ -95,6 +104,12 @@ internal sealed class RetailSelectionScene :
uint gfxObjId, uint gfxObjId,
Matrix4x4 partWorld) Matrix4x4 partWorld)
{ {
// WbDrawDispatcher is also shared by sealed CreatureMode-style UI
// viewports (paperdoll and appraisal). Those draws occur after the
// normal-world selection transaction has published and must never
// enter the world picking product.
if (!_frameOpen)
return;
if (entity.ServerGuid == 0u) if (entity.ServerGuid == 0u)
return; return;
@ -124,6 +139,13 @@ internal sealed class RetailSelectionScene :
public void CompleteFrame() public void CompleteFrame()
{ {
if (!_frameOpen)
{
throw new InvalidOperationException(
"The retail selection scene cannot complete without an open frame.");
}
_frameOpen = false;
(_published, _building) = (_building, _published); (_published, _building) = (_building, _published);
_currentRenderSceneObserver?.CompleteSelectionFrame(); _currentRenderSceneObserver?.CompleteSelectionFrame();
} }
@ -132,6 +154,10 @@ internal sealed class RetailSelectionScene :
/// completely published selection product.</summary> /// completely published selection product.</summary>
public void AbortFrame() public void AbortFrame()
{ {
if (!_frameOpen)
return;
_frameOpen = false;
_currentRenderSceneObserver?.AbortSelectionFrame(); _currentRenderSceneObserver?.AbortSelectionFrame();
_building.Clear(); _building.Clear();
_buildingKeys.Clear(); _buildingKeys.Clear();

View file

@ -94,6 +94,32 @@ public sealed class RetailSelectionSceneTests
Assert.Empty(oracle.SelectionParts); Assert.Empty(oracle.SelectionParts);
} }
[Fact]
public void PrivateViewportPartsOutsideWorldFrame_DoNotEnterPickingOrReferee()
{
var oracle = new CurrentRenderSceneOracle();
var scene = CreateScene();
scene.SetCurrentRenderSceneObserver(oracle);
var world = Entity(localId: 49u, serverGuid: 0x5000_0049u);
var privateViewport =
Entity(localId: 50u, serverGuid: 0x5000_0050u);
Publish(scene, world);
scene.AddVisiblePart(
privateViewport,
partIndex: 0,
gfxObjId: 0x0100_0001u,
Matrix4x4.CreateTranslation(0f, 0f, -5f));
RetailSelectionHit? hit = PickCenter(scene);
Assert.NotNull(hit);
Assert.Equal(world.Id, hit.Value.LocalEntityId);
CurrentRenderSelectionFingerprint fingerprint =
Assert.Single(oracle.SelectionParts);
Assert.Equal(world.Id, fingerprint.LocalEntityId);
Assert.Equal(1, oracle.Snapshot.SelectionPartCount);
}
private static RetailSelectionScene CreateScene( private static RetailSelectionScene CreateScene(
RetailSelectionLightingPulse? pulse = null) RetailSelectionLightingPulse? pulse = null)
{ {

View file

@ -900,7 +900,8 @@ public sealed class LandblockPresentationPipelineTests
nearRadius: 0, nearRadius: 0,
farRadius: 0, farRadius: 0,
onLandblockLoaded: _ => liveRecoveryCalls++, onLandblockLoaded: _ => liveRecoveryCalls++,
ensureEnvCellMeshes: _ => replayCalls++); ensureEnvCellMeshes: _ => replayCalls++,
workBudgetOptions: GenerousWorkBudget());
Assert.Throws<InvalidOperationException>(() => controller.Tick(0x60, 0x60)); Assert.Throws<InvalidOperationException>(() => controller.Tick(0x60, 0x60));
Assert.True(state.IsNearTier(landblockId)); Assert.True(state.IsNearTier(landblockId));