refactor(streaming): own landblock publication transactions

This commit is contained in:
Erik 2026-07-21 19:55:24 +02:00
parent 4001251064
commit 4f965d0699
5 changed files with 1301 additions and 122 deletions

View file

@ -0,0 +1,299 @@
using AcDream.App.Rendering.Wb;
using AcDream.Core.Terrain;
using AcDream.Core.World;
namespace AcDream.App.Streaming;
/// <summary>
/// Render-thread transaction coordinator for one accepted landblock streaming
/// result. Streaming policy (desired tier, generation, priority, and apply
/// budget) remains in <see cref="StreamingController"/>; spatial buckets remain
/// in <see cref="GpuWorldState"/>. This owner fixes the presentation ordering
/// independently of those policies so the concrete render, physics, and static
/// publishers can be extracted behind it without returning callbacks to the
/// game window.
/// </summary>
/// <remarks>
/// Retail establishes cells/buildings before static objects in
/// <c>LScape::grab_visible_cells @ 0x00504EC0</c>. Its release path separates
/// <c>CLandBlock::release_all @ 0x0052FCF0</c> (objects and visible cells) from
/// later destruction in <c>CLandBlock::Destroy @ 0x0052FAA0</c> (static objects
/// and buildings). Acdream's detach-first retry ledger is the existing
/// asynchronous lifetime adaptation. The injected publication operation
/// currently contains the already-shipped production transaction; later
/// Slice-5 checkpoints replace it with focused publishers.
/// </remarks>
public sealed class LandblockPresentationPipeline
{
private enum PublicationKind : byte
{
Loaded,
PromoteExisting,
PromoteSelfContained,
Far,
}
private sealed class PublicationTransaction
{
public required PublicationKind Kind;
public required LandblockBuild Build;
public required LandblockMeshData MeshData;
public required uint LandblockId;
public LandblockStreamTier Tier;
public bool PresentationCommitted;
public bool SpatialCommitted;
public GpuLandblockSpatialPublication? SpatialPublication;
public bool SpatialPresentationCommitted;
public bool EnvCellReplayCommitted;
public bool LiveRecoveryCommitted;
}
private readonly Action<LandblockBuild, LandblockMeshData> _publishBeforeSpatialCommit;
private readonly GpuWorldState _state;
private readonly Action<uint>? _onLandblockLoaded;
private readonly Action<EnvCellLandblockBuild>? _ensureEnvCellMeshes;
private readonly LandblockRetirementCoordinator _retirements;
private readonly Dictionary<LandblockStreamResult, PublicationTransaction> _publications =
new(ReferenceEqualityComparer.Instance);
public LandblockPresentationPipeline(
Action<LandblockBuild, LandblockMeshData> publishBeforeSpatialCommit,
GpuWorldState state,
Action<uint>? onLandblockLoaded = null,
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null,
LandblockRetirementCoordinator? retirementCoordinator = null,
Action<uint>? removeTerrain = null,
Action<uint>? demoteNearLayer = null)
{
ArgumentNullException.ThrowIfNull(publishBeforeSpatialCommit);
ArgumentNullException.ThrowIfNull(state);
_publishBeforeSpatialCommit = publishBeforeSpatialCommit;
_state = state;
_onLandblockLoaded = onLandblockLoaded;
_ensureEnvCellMeshes = ensureEnvCellMeshes;
_retirements = retirementCoordinator
?? LandblockRetirementCoordinator.CreateLegacy(
state,
removeTerrain,
demoteNearLayer);
}
public int PendingRetirementCount => _retirements.PendingCount;
public int PendingPublicationCount => _publications.Count;
public bool IsRetirementPending(uint landblockId) =>
_retirements.IsPending(landblockId);
public bool HasPendingPublication(LandblockStreamResult result)
{
ArgumentNullException.ThrowIfNull(result);
return _publications.ContainsKey(result);
}
public IReadOnlyList<LandblockStreamResult> GetPendingPublicationResults() =>
_publications.Keys.ToArray();
public void ResumePublication(LandblockStreamResult result)
{
ArgumentNullException.ThrowIfNull(result);
if (!_publications.TryGetValue(result, out PublicationTransaction? transaction))
return;
Advance(result, transaction);
}
public void AdvanceRetirements() => _retirements.Advance();
public void BeginFullRetirement(uint landblockId) =>
_retirements.BeginFull(landblockId);
public void BeginNearLayerRetirement(uint landblockId) =>
_retirements.BeginNearLayer(landblockId);
public void PublishLoaded(LandblockStreamResult.Loaded loaded)
{
ArgumentNullException.ThrowIfNull(loaded);
PublicationTransaction transaction = GetOrCreate(
loaded,
PublicationKind.Loaded,
loaded.Build,
loaded.MeshData,
loaded.LandblockId,
loaded.Tier);
Advance(loaded, transaction);
}
public void PublishPromoted(
LandblockStreamResult.Promoted promoted,
bool mergeIntoExistingLandblock)
{
ArgumentNullException.ThrowIfNull(promoted);
PublicationKind kind = mergeIntoExistingLandblock
? PublicationKind.PromoteExisting
: PublicationKind.PromoteSelfContained;
PublicationTransaction transaction = GetOrCreate(
promoted,
kind,
promoted.Build,
promoted.MeshData,
promoted.LandblockId,
LandblockStreamTier.Near);
Advance(promoted, transaction);
}
public void PublishAsFar(
LandblockStreamResult acceptedResult,
LandblockBuild completedBuild,
LandblockMeshData meshData)
{
ArgumentNullException.ThrowIfNull(acceptedResult);
ArgumentNullException.ThrowIfNull(completedBuild);
ArgumentNullException.ThrowIfNull(meshData);
if (!_publications.TryGetValue(acceptedResult, out PublicationTransaction? transaction))
{
LoadedLandblock completed = completedBuild.Landblock;
var farLandblock = new LoadedLandblock(
completed.LandblockId,
completed.Heightmap,
Array.Empty<WorldEntity>(),
PhysicsDatBundle.Empty);
transaction = new PublicationTransaction
{
Kind = PublicationKind.Far,
Build = new LandblockBuild(farLandblock),
MeshData = meshData,
LandblockId = farLandblock.LandblockId,
Tier = LandblockStreamTier.Far,
};
_publications.Add(acceptedResult, transaction);
}
else if (transaction.Kind != PublicationKind.Far)
{
throw new InvalidOperationException(
$"Landblock publication 0x{transaction.LandblockId:X8} changed kind while pending.");
}
Advance(acceptedResult, transaction);
}
private PublicationTransaction GetOrCreate(
LandblockStreamResult result,
PublicationKind kind,
LandblockBuild build,
LandblockMeshData meshData,
uint landblockId,
LandblockStreamTier tier)
{
if (_publications.TryGetValue(result, out PublicationTransaction? existing))
{
if (existing.Kind != kind
&& !(existing.Kind is PublicationKind.PromoteExisting
or PublicationKind.PromoteSelfContained
&& kind is PublicationKind.PromoteExisting
or PublicationKind.PromoteSelfContained))
{
throw new InvalidOperationException(
$"Landblock publication 0x{existing.LandblockId:X8} changed kind while pending.");
}
return existing;
}
var created = new PublicationTransaction
{
Kind = kind,
Build = build,
MeshData = meshData,
LandblockId = landblockId,
Tier = tier,
};
_publications.Add(result, created);
return created;
}
private void Advance(
LandblockStreamResult result,
PublicationTransaction transaction)
{
if (!transaction.PresentationCommitted)
{
try
{
_publishBeforeSpatialCommit(transaction.Build, transaction.MeshData);
}
catch (Exception error)
{
// The legacy GameWindow callback still spans several concrete
// owners and cannot report a committed prefix. Until Slice-5
// checkpoints C-E replace it with individually ledgered
// publishers, its production wrapper conservatively reports a
// committed mutation. Test/compatibility callbacks retain the
// established strong-exception contract unless they explicitly
// report otherwise.
if (error is StreamingMutationException { MutationCommitted: true })
_publications.Remove(result);
throw;
}
transaction.PresentationCommitted = true;
}
if (!transaction.SpatialPresentationCommitted)
{
// Keep live-projection visibility publication at the shipped
// boundary: bucket mutation, pin/script reconciliation, then the
// outer spatial observer commit. If pinning throws, the receipt is
// retained and only that suffix retries.
using GpuWorldState.MutationBatch mutation = _state.BeginMutationBatch();
if (!transaction.SpatialCommitted)
{
IEnumerable<ulong>? renderIds =
transaction.Build.EnvCells?.Shells.Select(static shell => shell.GeometryId);
transaction.SpatialPublication = transaction.Kind switch
{
PublicationKind.Loaded
or PublicationKind.PromoteSelfContained
or PublicationKind.Far =>
_state.CommitLandblockSpatial(
transaction.Build.Landblock,
renderIds,
transaction.Tier),
PublicationKind.PromoteExisting =>
_state.CommitEntitiesToExistingLandblockSpatial(
transaction.LandblockId,
transaction.Build.Landblock.Entities,
renderIds),
_ => throw new InvalidOperationException(
$"Unknown landblock publication kind {transaction.Kind}."),
};
transaction.SpatialCommitted = true;
}
_state.ActivateLandblockPresentation(
transaction.SpatialPublication
?? throw new InvalidOperationException(
"A committed spatial publication has no activation receipt."));
transaction.SpatialPresentationCommitted = true;
}
if (!transaction.EnvCellReplayCommitted)
{
if (transaction.Kind != PublicationKind.Far
&& transaction.Build.EnvCells is { Shells.Length: > 0 } envCells)
{
_ensureEnvCellMeshes?.Invoke(envCells);
}
transaction.EnvCellReplayCommitted = true;
}
if (!transaction.LiveRecoveryCommitted)
{
// Retained live projections can materialize only after the
// canonical bucket exists. Landblock load never reconstructs their
// identity or replays create-time resources.
_onLandblockLoaded?.Invoke(transaction.LandblockId);
transaction.LiveRecoveryCommitted = true;
}
_publications.Remove(result);
}
}