refactor(streaming): own landblock publication transactions
This commit is contained in:
parent
4001251064
commit
4f965d0699
5 changed files with 1301 additions and 122 deletions
299
src/AcDream.App/Streaming/LandblockPresentationPipeline.cs
Normal file
299
src/AcDream.App/Streaming/LandblockPresentationPipeline.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue