fix(streaming): preserve portal destination ownership
Detach old-world spatial ownership atomically, prioritize destination retirement dependencies, and reveal the viewport at the retail transition edge. Give private paperdoll views independent mesh ownership and retain dormant ACE entities so portal revisits preserve server objects without extending active GPU lifetimes.
This commit is contained in:
parent
2c848d4167
commit
823936ec31
57 changed files with 2551 additions and 324 deletions
|
|
@ -277,6 +277,7 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
internal const long TargetArrayBytes = 4L * 1024 * 1024;
|
||||
internal const int MaximumLayersPerArray = 64;
|
||||
internal const int DefaultMaximumUploadsPerFrame = 16;
|
||||
internal const int DestinationRevealMaximumUploadsPerFrame = 64;
|
||||
internal const long DefaultMaximumUploadBytesPerFrame = 8L * 1024 * 1024;
|
||||
internal const int MaximumLogicalEvictionsPerFrame = 16;
|
||||
internal const int MaximumAtlasCreationsPerFrame = 1;
|
||||
|
|
@ -298,6 +299,7 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
private long _frameUploadBytes;
|
||||
private int _frameAtlasCreationCount;
|
||||
private bool _uploadBudgetBlocked;
|
||||
private bool _destinationRevealUploadPriority;
|
||||
private int _pendingAtlasWidth;
|
||||
private int _pendingAtlasHeight;
|
||||
private long _pendingAtlasAllocationBytes;
|
||||
|
|
@ -390,9 +392,16 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
internal long FrameUploadBytes => _frameUploadBytes;
|
||||
internal bool CanStartUpload =>
|
||||
!_uploadBudgetBlocked
|
||||
&& _frameUploadCount < _maximumUploadsPerFrame
|
||||
&& _frameUploadCount < CurrentMaximumUploadsPerFrame
|
||||
&& (_frameUploadCount == 0 || _frameUploadBytes < _maximumUploadBytesPerFrame);
|
||||
|
||||
private int CurrentMaximumUploadsPerFrame =>
|
||||
_destinationRevealUploadPriority
|
||||
? Math.Max(
|
||||
_maximumUploadsPerFrame,
|
||||
DestinationRevealMaximumUploadsPerFrame)
|
||||
: _maximumUploadsPerFrame;
|
||||
|
||||
internal Residency.ResidencyDomainSnapshot CaptureResidency()
|
||||
{
|
||||
long retiringBytes = 0;
|
||||
|
|
@ -439,7 +448,7 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
internal bool CanUpload(long bytes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bytes);
|
||||
if (_frameUploadCount >= _maximumUploadsPerFrame)
|
||||
if (_frameUploadCount >= CurrentMaximumUploadsPerFrame)
|
||||
return false;
|
||||
|
||||
// Always allow one item so a texture larger than the normal frame
|
||||
|
|
@ -479,10 +488,12 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
return false;
|
||||
}
|
||||
|
||||
public void BeginFrame()
|
||||
public void BeginFrame(bool destinationRevealUploadPriority = false)
|
||||
{
|
||||
ThrowIfUnavailable();
|
||||
_retirementLedger.RetryPendingPublications();
|
||||
_destinationRevealUploadPriority =
|
||||
destinationRevealUploadPriority;
|
||||
_frameUploadCount = 0;
|
||||
_frameUploadBytes = 0;
|
||||
_frameAtlasCreationCount = 0;
|
||||
|
|
|
|||
|
|
@ -378,13 +378,15 @@ internal sealed class CreatureAppraisalViewportRenderer :
|
|||
GL gl,
|
||||
WbDrawDispatcher dispatcher,
|
||||
SceneLightingUboBinding lightUbo,
|
||||
IEntityTextureLifetime textureLifetime)
|
||||
IEntityTextureLifetime textureLifetime,
|
||||
IWbMeshAdapter meshAdapter)
|
||||
{
|
||||
_renderer = new PrivateEntityViewportRenderer(
|
||||
gl,
|
||||
dispatcher,
|
||||
lightUbo,
|
||||
textureLifetime,
|
||||
meshAdapter,
|
||||
CreatureAppraisalEntityBuilder.RenderId,
|
||||
_camera,
|
||||
"creature examination");
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
|
|||
private readonly IPaperdollDollRenderer _renderer;
|
||||
private readonly IPaperdollFrameView _view;
|
||||
private readonly IPaperdollDollFactory _factory;
|
||||
private WorldEntity? _doll;
|
||||
private bool _dirty = true;
|
||||
|
||||
public PaperdollFramePresenter(
|
||||
|
|
@ -78,20 +79,111 @@ internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
|
|||
{
|
||||
if (_factory.TryBuild(out WorldEntity? doll))
|
||||
{
|
||||
_renderer.SetDoll(doll);
|
||||
// Same-generation CreateObject refreshes can repeat the exact
|
||||
// player ObjDesc at a portal boundary. Retail redresses its
|
||||
// private inventory object in place; releasing and reacquiring
|
||||
// an identical synthetic owner briefly blanks the viewport and
|
||||
// churns its texture composites.
|
||||
if (!HasEquivalentAppearance(_doll, doll))
|
||||
{
|
||||
_renderer.SetDoll(doll);
|
||||
_doll = doll;
|
||||
}
|
||||
_dirty = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Retail's private viewport remains present while the live
|
||||
// player projection is still materializing. Keep the dirty
|
||||
// edge armed and clear any doll from the prior session.
|
||||
_renderer.SetDoll(null);
|
||||
// gmPaperDollUI::RedressCreature @ 0x004A3BC0 leaves its
|
||||
// private m_pInventoryObject intact when the SmartBox player
|
||||
// is temporarily unavailable. Keep the successful doll and
|
||||
// retry this dirty redress on the next visible frame.
|
||||
}
|
||||
}
|
||||
|
||||
_view.SetTextureHandle(_renderer.Render(width, height));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the private object only at the owning character-session
|
||||
/// boundary, matching gmPaperDollUI's private-object lifetime.
|
||||
/// </summary>
|
||||
public void ResetSession()
|
||||
{
|
||||
_renderer.SetDoll(null);
|
||||
_doll = null;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
private static bool HasEquivalentAppearance(
|
||||
WorldEntity? current,
|
||||
WorldEntity? candidate)
|
||||
{
|
||||
if (current is null || candidate is null)
|
||||
return ReferenceEquals(current, candidate);
|
||||
if (current.SourceGfxObjOrSetupId != candidate.SourceGfxObjOrSetupId
|
||||
|| current.Scale != candidate.Scale
|
||||
|| current.HiddenPartsMask != candidate.HiddenPartsMask
|
||||
|| current.MeshRefs.Count != candidate.MeshRefs.Count
|
||||
|| current.PartOverrides.Count != candidate.PartOverrides.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < current.MeshRefs.Count; i++)
|
||||
{
|
||||
MeshRef left = current.MeshRefs[i];
|
||||
MeshRef right = candidate.MeshRefs[i];
|
||||
if (left.GfxObjId != right.GfxObjId
|
||||
|| left.PartTransform != right.PartTransform
|
||||
|| !DictionaryEquals(
|
||||
left.SurfaceOverrides,
|
||||
right.SurfaceOverrides))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < current.PartOverrides.Count; i++)
|
||||
{
|
||||
if (current.PartOverrides[i] != candidate.PartOverrides[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
PaletteOverride? leftPalette = current.PaletteOverride;
|
||||
PaletteOverride? rightPalette = candidate.PaletteOverride;
|
||||
if (leftPalette is null || rightPalette is null)
|
||||
return leftPalette is null && rightPalette is null;
|
||||
if (leftPalette.BasePaletteId != rightPalette.BasePaletteId
|
||||
|| leftPalette.SubPalettes.Count != rightPalette.SubPalettes.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < leftPalette.SubPalettes.Count; i++)
|
||||
{
|
||||
if (leftPalette.SubPalettes[i] != rightPalette.SubPalettes[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DictionaryEquals(
|
||||
IReadOnlyDictionary<uint, uint>? left,
|
||||
IReadOnlyDictionary<uint, uint>? right)
|
||||
{
|
||||
if (left is null || right is null)
|
||||
return left is null && right is null;
|
||||
if (left.Count != right.Count)
|
||||
return false;
|
||||
foreach ((uint key, uint value) in left)
|
||||
{
|
||||
if (!right.TryGetValue(key, out uint rightValue)
|
||||
|| rightValue != value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Retained-UI visibility and texture publication for the doll view.</summary>
|
||||
|
|
|
|||
|
|
@ -21,13 +21,15 @@ public sealed class PaperdollViewportRenderer :
|
|||
GL gl,
|
||||
WbDrawDispatcher dispatcher,
|
||||
SceneLightingUboBinding lightUbo,
|
||||
IEntityTextureLifetime textureLifetime)
|
||||
IEntityTextureLifetime textureLifetime,
|
||||
IWbMeshAdapter meshAdapter)
|
||||
{
|
||||
_renderer = new PrivateEntityViewportRenderer(
|
||||
gl,
|
||||
dispatcher,
|
||||
lightUbo,
|
||||
textureLifetime,
|
||||
meshAdapter,
|
||||
DollEntityBuilder.DollRenderId,
|
||||
new DollViewportCamera(),
|
||||
"paperdoll");
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
private readonly Random _random;
|
||||
private readonly Action<string?>? _displayNotice;
|
||||
private IDisposable? _displayNoticeLifetime;
|
||||
private readonly PortalMeshReferenceOwner _meshReferences;
|
||||
private readonly SyntheticEntityMeshReferenceOwner _meshReferences;
|
||||
|
||||
private bool _visible;
|
||||
private double _rotationElapsed;
|
||||
|
|
@ -104,7 +104,7 @@ public sealed class PortalTunnelPresentation : IDisposable
|
|||
MeshRefs = SetupMesh.Flatten(setup),
|
||||
};
|
||||
|
||||
_meshReferences = new PortalMeshReferenceOwner(
|
||||
_meshReferences = new SyntheticEntityMeshReferenceOwner(
|
||||
meshAdapter,
|
||||
setup.Parts.Select(part => (ulong)(uint)part));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,9 +32,12 @@ internal sealed unsafe class PrivateEntityViewportRenderer :
|
|||
private readonly WbDrawDispatcher _dispatcher;
|
||||
private readonly SceneLightingUboBinding _lightUbo;
|
||||
private readonly FixedEntityTextureOwnerLease _textureOwnerLease;
|
||||
private readonly IWbMeshAdapter _meshAdapter;
|
||||
private readonly IPrivateEntityViewportCamera _camera;
|
||||
private readonly HashSet<uint> _animatedIds;
|
||||
private readonly string _diagnosticName;
|
||||
private readonly List<SyntheticEntityMeshReferenceOwner>
|
||||
_retiringMeshReferences = [];
|
||||
|
||||
private uint _fbo;
|
||||
private uint _colorTex;
|
||||
|
|
@ -42,12 +45,14 @@ internal sealed unsafe class PrivateEntityViewportRenderer :
|
|||
private int _fbW;
|
||||
private int _fbH;
|
||||
private WorldEntity? _entity;
|
||||
private SyntheticEntityMeshReferenceOwner? _meshReferences;
|
||||
|
||||
public PrivateEntityViewportRenderer(
|
||||
GL gl,
|
||||
WbDrawDispatcher dispatcher,
|
||||
SceneLightingUboBinding lightUbo,
|
||||
IEntityTextureLifetime textureLifetime,
|
||||
IWbMeshAdapter meshAdapter,
|
||||
uint renderId,
|
||||
IPrivateEntityViewportCamera camera,
|
||||
string diagnosticName)
|
||||
|
|
@ -57,6 +62,8 @@ internal sealed unsafe class PrivateEntityViewportRenderer :
|
|||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
_lightUbo = lightUbo ?? throw new ArgumentNullException(nameof(lightUbo));
|
||||
_meshAdapter = meshAdapter
|
||||
?? throw new ArgumentNullException(nameof(meshAdapter));
|
||||
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
||||
_diagnosticName = string.IsNullOrWhiteSpace(diagnosticName)
|
||||
? "creature viewport"
|
||||
|
|
@ -69,10 +76,63 @@ internal sealed unsafe class PrivateEntityViewportRenderer :
|
|||
|
||||
public void SetEntity(WorldEntity? entity)
|
||||
{
|
||||
ReleaseRetiringMeshReferences();
|
||||
|
||||
if (ReferenceEquals(_entity, entity))
|
||||
return;
|
||||
_textureOwnerLease.Replace(entity is not null);
|
||||
|
||||
SyntheticEntityMeshReferenceOwner? replacement = null;
|
||||
if (entity is not null)
|
||||
{
|
||||
replacement = new SyntheticEntityMeshReferenceOwner(
|
||||
_meshAdapter,
|
||||
CollectMeshIds(entity));
|
||||
replacement.Acquire();
|
||||
}
|
||||
|
||||
SyntheticEntityMeshReferenceOwner? previous = _meshReferences;
|
||||
try
|
||||
{
|
||||
_textureOwnerLease.Replace(entity is not null);
|
||||
}
|
||||
catch (Exception textureFailure)
|
||||
{
|
||||
if (replacement is null)
|
||||
throw;
|
||||
|
||||
try
|
||||
{
|
||||
replacement.Dispose();
|
||||
}
|
||||
catch (Exception rollbackFailure)
|
||||
{
|
||||
throw new AggregateException(
|
||||
$"The {_diagnosticName} texture-owner replacement failed "
|
||||
+ "and the replacement mesh-owner rollback did not converge.",
|
||||
textureFailure,
|
||||
rollbackFailure);
|
||||
}
|
||||
|
||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo
|
||||
.Capture(textureFailure)
|
||||
.Throw();
|
||||
}
|
||||
|
||||
_meshReferences = replacement;
|
||||
_entity = entity;
|
||||
|
||||
if (previous is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
previous.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_retiringMeshReferences.Add(previous);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public uint Render(int width, int height)
|
||||
|
|
@ -246,7 +306,78 @@ internal sealed unsafe class PrivateEntityViewportRenderer :
|
|||
public void Dispose()
|
||||
{
|
||||
_entity = null;
|
||||
_textureOwnerLease.Dispose();
|
||||
DeleteFramebuffer();
|
||||
if (_meshReferences is { } current)
|
||||
{
|
||||
_meshReferences = null;
|
||||
_retiringMeshReferences.Add(current);
|
||||
}
|
||||
|
||||
List<Exception>? failures = null;
|
||||
try
|
||||
{
|
||||
_textureOwnerLease.Dispose();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= []).Add(error);
|
||||
}
|
||||
try
|
||||
{
|
||||
ReleaseRetiringMeshReferences();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= []).Add(error);
|
||||
}
|
||||
try
|
||||
{
|
||||
DeleteFramebuffer();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= []).Add(error);
|
||||
}
|
||||
|
||||
if (failures is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
$"The {_diagnosticName} resources did not fully release.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ulong> CollectMeshIds(WorldEntity entity)
|
||||
{
|
||||
for (int i = 0; i < entity.MeshRefs.Count; i++)
|
||||
yield return entity.MeshRefs[i].GfxObjId;
|
||||
for (int i = 0; i < entity.PartOverrides.Count; i++)
|
||||
yield return entity.PartOverrides[i].GfxObjId;
|
||||
}
|
||||
|
||||
private void ReleaseRetiringMeshReferences()
|
||||
{
|
||||
List<Exception>? failures = null;
|
||||
for (int i = _retiringMeshReferences.Count - 1; i >= 0; i--)
|
||||
{
|
||||
SyntheticEntityMeshReferenceOwner owner =
|
||||
_retiringMeshReferences[i];
|
||||
try
|
||||
{
|
||||
owner.Dispose();
|
||||
if (owner.IsDisposed)
|
||||
_retiringMeshReferences.RemoveAt(i);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= []).Add(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
$"One or more {_diagnosticName} mesh owners remain pending.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ using AcDream.App.Rendering.Wb;
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Owns the synthetic portal creature's mesh references. Acquisition happens
|
||||
/// only after the presentation has been published to its runtime owner, so a
|
||||
/// partial backend failure can never strand an unreachable constructor-local
|
||||
/// reference. Per-reference physical state makes both acquisition and teardown
|
||||
/// resumable without replaying successful mutations.
|
||||
/// Owns one synthetic/private entity's mesh references independently of world
|
||||
/// cell residence. Acquisition happens only after the presentation has been
|
||||
/// published to its runtime owner, so a partial backend failure can never
|
||||
/// strand an unreachable constructor-local reference. Per-reference physical
|
||||
/// state makes both acquisition and teardown resumable without replaying
|
||||
/// successful mutations.
|
||||
/// </summary>
|
||||
internal sealed class PortalMeshReferenceOwner : IDisposable
|
||||
internal sealed class SyntheticEntityMeshReferenceOwner : IDisposable
|
||||
{
|
||||
private sealed class ReferenceState(ulong gfxObjId)
|
||||
{
|
||||
|
|
@ -25,7 +26,7 @@ internal sealed class PortalMeshReferenceOwner : IDisposable
|
|||
private bool _reconciling;
|
||||
private bool _reconcileAgain;
|
||||
|
||||
public PortalMeshReferenceOwner(
|
||||
public SyntheticEntityMeshReferenceOwner(
|
||||
IWbMeshAdapter meshAdapter,
|
||||
IEnumerable<ulong> gfxObjIds)
|
||||
{
|
||||
|
|
@ -62,7 +63,7 @@ internal sealed class PortalMeshReferenceOwner : IDisposable
|
|||
catch (Exception rollbackFailure)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Portal mesh acquisition failed and its rollback did not fully converge.",
|
||||
"Synthetic entity mesh acquisition failed and its rollback did not fully converge.",
|
||||
acquisitionFailure,
|
||||
rollbackFailure);
|
||||
}
|
||||
|
|
@ -132,7 +133,7 @@ internal sealed class PortalMeshReferenceOwner : IDisposable
|
|||
|
||||
string operation = targetHeld ? "acquisition" : "release";
|
||||
(failures ??= []).Add(new InvalidOperationException(
|
||||
$"Portal mesh 0x{reference.GfxObjId:X10} reference {operation} failed.",
|
||||
$"Synthetic entity mesh 0x{reference.GfxObjId:X10} reference {operation} failed.",
|
||||
error));
|
||||
}
|
||||
}
|
||||
|
|
@ -148,7 +149,7 @@ internal sealed class PortalMeshReferenceOwner : IDisposable
|
|||
|
||||
if (failures is not null)
|
||||
throw new AggregateException(
|
||||
"One or more portal mesh references failed to reconcile.",
|
||||
"One or more synthetic entity mesh references failed to reconcile.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,9 @@ using AcDream.App.Rendering.Residency;
|
|||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable
|
||||
public sealed unsafe class TextureCache
|
||||
: Wb.IEntityTextureLifetime,
|
||||
IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
|
|
@ -39,6 +41,7 @@ public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable
|
|||
|
||||
private readonly Wb.BindlessSupport? _bindless;
|
||||
private readonly CompositeTextureArrayCache? _compositeTextures;
|
||||
private bool _destinationRevealUploadPriority;
|
||||
|
||||
// Standalone Texture2DArray caches. Shared world surfaces use WB's atlas;
|
||||
// this base cache remains for consumers such as particle rendering.
|
||||
|
|
@ -65,6 +68,9 @@ public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable
|
|||
internal int CachedUnownedParticleTextureCount => _particleTextures?.UnownedEntryCount ?? 0;
|
||||
internal long CachedUnownedParticleTextureBytes => _particleTextures?.UnownedBytes ?? 0;
|
||||
|
||||
internal void SetDestinationRevealUploadPriority(bool enabled) =>
|
||||
_destinationRevealUploadPriority = enabled;
|
||||
|
||||
// Phase N.6 slice 1 (2026-05-11): per-upload metadata for the
|
||||
// ACDREAM_DUMP_SURFACES=1 histogram dump path. Populated at upload
|
||||
// time so the dump method doesn't have to query GL state. Keyed by
|
||||
|
|
@ -557,7 +563,8 @@ public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable
|
|||
/// </summary>
|
||||
public void TickParticleTextureCache() => _particleTextures?.Tick();
|
||||
|
||||
public void BeginCompositeTextureFrame() => _compositeTextures?.BeginFrame();
|
||||
public void BeginCompositeTextureFrame() =>
|
||||
_compositeTextures?.BeginFrame(_destinationRevealUploadPriority);
|
||||
|
||||
/// <summary>
|
||||
/// Cheap 64-bit hash over a palette override's identity so two
|
||||
|
|
|
|||
|
|
@ -21,9 +21,12 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// <c>DefaultDatReaderWriter</c> file-handle set has been removed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||
public sealed class WbMeshAdapter
|
||||
: IDisposable,
|
||||
IWbMeshAdapter
|
||||
{
|
||||
internal const int MaximumUploadsPerFrame = 8;
|
||||
internal const int DestinationRevealMaximumUploadsPerFrame = 64;
|
||||
internal const long MaximumUploadBytesPerFrame = 8L * 1024 * 1024;
|
||||
internal const long MaximumArrayAllocationBytesPerFrame = 8L * 1024 * 1024;
|
||||
internal const long MaximumMipmapBytesPerFrame = 8L * 1024 * 1024;
|
||||
|
|
@ -48,22 +51,12 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
private readonly ObjectMeshManager? _meshManager;
|
||||
private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue? _resourceRetirement;
|
||||
private readonly IPreparedAssetSource? _ownedPreparedAssets;
|
||||
private readonly MeshUploadFrameBudget _uploadBudget = new(new MeshUploadBudgetLimits(
|
||||
MaximumUploadsPerFrame,
|
||||
MaximumUploadBytesPerFrame,
|
||||
MaximumArrayAllocationBytesPerFrame,
|
||||
MaximumMipmapBytesPerFrame,
|
||||
MaximumNewArraysPerFrame,
|
||||
MaximumBufferUploadBytesPerFrame,
|
||||
MaximumBufferAllocationBytesPerFrame,
|
||||
MaximumBufferCopyBytesPerFrame,
|
||||
MaximumNewBuffersPerFrame,
|
||||
MaximumSingleUploadBytes,
|
||||
MaximumSingleArrayAllocationBytes,
|
||||
MaximumSingleMipmapBytes,
|
||||
MaximumSingleNewArrays,
|
||||
MaximumSingleBufferUploadBytes));
|
||||
private readonly MeshUploadFrameBudget _ordinaryUploadBudget =
|
||||
CreateUploadBudget(MaximumUploadsPerFrame);
|
||||
private readonly MeshUploadFrameBudget _destinationRevealUploadBudget =
|
||||
CreateUploadBudget(DestinationRevealMaximumUploadsPerFrame);
|
||||
private readonly HashSet<TextureAtlasManager> _mipmapsBudgeted = new();
|
||||
private bool _destinationRevealUploadPriority;
|
||||
|
||||
/// <summary>
|
||||
/// True when this instance was created via <see cref="CreateUninitialized"/>;
|
||||
|
|
@ -91,6 +84,27 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
internal (int Count, long Bytes) CpuMeshCacheDiagnostics =>
|
||||
_meshManager?.CpuCacheDiagnostics ?? default;
|
||||
|
||||
internal void SetDestinationRevealUploadPriority(bool enabled) =>
|
||||
_destinationRevealUploadPriority = enabled;
|
||||
|
||||
private static MeshUploadFrameBudget CreateUploadBudget(
|
||||
int maximumObjects) =>
|
||||
new(new MeshUploadBudgetLimits(
|
||||
maximumObjects,
|
||||
MaximumUploadBytesPerFrame,
|
||||
MaximumArrayAllocationBytesPerFrame,
|
||||
MaximumMipmapBytesPerFrame,
|
||||
MaximumNewArraysPerFrame,
|
||||
MaximumBufferUploadBytesPerFrame,
|
||||
MaximumBufferAllocationBytesPerFrame,
|
||||
MaximumBufferCopyBytesPerFrame,
|
||||
MaximumNewBuffersPerFrame,
|
||||
MaximumSingleUploadBytes,
|
||||
MaximumSingleArrayAllocationBytes,
|
||||
MaximumSingleMipmapBytes,
|
||||
MaximumSingleNewArrays,
|
||||
MaximumSingleBufferUploadBytes));
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the UI-Studio/tooling WB pipeline. Production composition
|
||||
/// supplies the validated pak source through the internal overload below;
|
||||
|
|
@ -457,7 +471,15 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
// single frame. UploadOrRequeue bounds the retries (MaxUploadRetries) so
|
||||
// a genuine defect surfaces loudly instead of the old silent sticky drop.
|
||||
List<MeshUploadQueueItem>? requeue = null;
|
||||
_uploadBudget.Reset();
|
||||
MeshUploadFrameBudget uploadBudget =
|
||||
_destinationRevealUploadPriority
|
||||
? _destinationRevealUploadBudget
|
||||
: _ordinaryUploadBudget;
|
||||
int maximumUploads =
|
||||
_destinationRevealUploadPriority
|
||||
? DestinationRevealMaximumUploadsPerFrame
|
||||
: MaximumUploadsPerFrame;
|
||||
uploadBudget.Reset();
|
||||
_mipmapsBudgeted.Clear();
|
||||
int staleDiscardCount = 0;
|
||||
bool arenaBackpressured = false;
|
||||
|
|
@ -470,7 +492,7 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
{
|
||||
GlobalMeshMaintenanceStep maintenance =
|
||||
globalBuffer.AdvanceMigration(MaximumBufferCopyBytesPerFrame);
|
||||
_uploadBudget.RecordBufferMaintenance(
|
||||
uploadBudget.RecordBufferMaintenance(
|
||||
maintenance.AllocationBytes,
|
||||
maintenance.CopyBytes,
|
||||
maintenance.NewBufferCount);
|
||||
|
|
@ -478,8 +500,8 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
}
|
||||
|
||||
while (globalBuffer?.IsMigrationInProgress != true
|
||||
&& _uploadBudget.BufferCopyBytes == 0
|
||||
&& _uploadBudget.ObjectCount < MaximumUploadsPerFrame)
|
||||
&& uploadBudget.BufferCopyBytes == 0
|
||||
&& uploadBudget.ObjectCount < maximumUploads)
|
||||
{
|
||||
staleDiscardCount += meshManager.DiscardUnownedStagedPrefix(
|
||||
MaximumStaleDiscardsPerFrame - staleDiscardCount);
|
||||
|
|
@ -501,7 +523,7 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
}
|
||||
if (capacity == GlobalMeshCapacityResult.MigrationStarted)
|
||||
{
|
||||
_uploadBudget.RecordBufferMaintenance(
|
||||
uploadBudget.RecordBufferMaintenance(
|
||||
maintenance.AllocationBytes,
|
||||
maintenance.CopyBytes,
|
||||
maintenance.NewBufferCount);
|
||||
|
|
@ -544,7 +566,7 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
next.Data,
|
||||
_mipmapsBudgeted,
|
||||
next.Generation);
|
||||
if (!_uploadBudget.TryAdmit(cost))
|
||||
if (!uploadBudget.TryAdmit(cost))
|
||||
break;
|
||||
}
|
||||
catch (NotSupportedException error)
|
||||
|
|
@ -597,29 +619,29 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
// frames. It never competes with destination materialization, and the
|
||||
// GlobalMeshBuffer policy retains enough hysteresis that portal-route
|
||||
// revisits do not alternate shrink/grow every frame.
|
||||
if (_uploadBudget.ObjectCount == 0
|
||||
if (uploadBudget.ObjectCount == 0
|
||||
&& LastMipmapArrayCount == 0
|
||||
&& meshManager.StagedMeshCount == 0
|
||||
&& globalBuffer?.IsMigrationInProgress == false
|
||||
&& globalBuffer.TryTrimUnusedTail(out GlobalMeshMaintenanceStep trim))
|
||||
{
|
||||
_uploadBudget.RecordBufferMaintenance(
|
||||
uploadBudget.RecordBufferMaintenance(
|
||||
trim.AllocationBytes,
|
||||
trim.CopyBytes,
|
||||
trim.NewBufferCount);
|
||||
meshManager.SetArenaBackpressure(true);
|
||||
}
|
||||
|
||||
LastUploadCount = _uploadBudget.ObjectCount;
|
||||
LastUploadBytes = _uploadBudget.SourceBytes;
|
||||
LastUploadCount = uploadBudget.ObjectCount;
|
||||
LastUploadBytes = uploadBudget.SourceBytes;
|
||||
LastStaleDiscardCount = staleDiscardCount;
|
||||
LastArrayAllocationBytes = _uploadBudget.ArrayAllocationBytes;
|
||||
LastPlannedMipmapBytes = _uploadBudget.MipmapBytes;
|
||||
LastNewArrayCount = _uploadBudget.NewArrayCount;
|
||||
LastBufferUploadBytes = _uploadBudget.BufferUploadBytes;
|
||||
LastBufferAllocationBytes = _uploadBudget.BufferAllocationBytes;
|
||||
LastBufferCopyBytes = _uploadBudget.BufferCopyBytes;
|
||||
LastNewBufferCount = _uploadBudget.NewBufferCount;
|
||||
LastArrayAllocationBytes = uploadBudget.ArrayAllocationBytes;
|
||||
LastPlannedMipmapBytes = uploadBudget.MipmapBytes;
|
||||
LastNewArrayCount = uploadBudget.NewArrayCount;
|
||||
LastBufferUploadBytes = uploadBudget.BufferUploadBytes;
|
||||
LastBufferAllocationBytes = uploadBudget.BufferAllocationBytes;
|
||||
LastBufferCopyBytes = uploadBudget.BufferCopyBytes;
|
||||
LastNewBufferCount = uploadBudget.NewBufferCount;
|
||||
|
||||
if (texProbe)
|
||||
EmitTexFlushProbe(pendingBefore);
|
||||
|
|
|
|||
|
|
@ -339,7 +339,6 @@ internal sealed class RuntimeWorldFrameVisibilityPreparation
|
|||
|
||||
_particles.UseWorldView();
|
||||
_reveal?.ObserveWorldViewportVisible();
|
||||
_reveal?.Complete();
|
||||
}
|
||||
|
||||
public void PublishViewProjection(in WorldCameraFrame camera) =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
using AcDream.App.Streaming;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Applies one reveal generation to every render-resource participant and
|
||||
/// restores their ordinary profile only when that exact generation ends.
|
||||
/// Replacing an active generation keeps priority continuously enabled.
|
||||
/// </summary>
|
||||
internal sealed class WorldRevealRenderResourceScheduler
|
||||
: IWorldRevealRenderResourceScheduler
|
||||
{
|
||||
private readonly Action<bool>[] _participants;
|
||||
private long _activeGeneration;
|
||||
|
||||
public WorldRevealRenderResourceScheduler(
|
||||
params Action<bool>[] participants)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(participants);
|
||||
if (participants.Length == 0)
|
||||
throw new ArgumentException(
|
||||
"At least one reveal upload participant is required.",
|
||||
nameof(participants));
|
||||
if (participants.Any(static participant => participant is null))
|
||||
throw new ArgumentException(
|
||||
"Reveal upload participants cannot contain null.",
|
||||
nameof(participants));
|
||||
|
||||
_participants = [.. participants];
|
||||
}
|
||||
|
||||
public void BeginDestinationReveal(long revealGeneration)
|
||||
{
|
||||
if (revealGeneration <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(revealGeneration));
|
||||
|
||||
_activeGeneration = revealGeneration;
|
||||
for (int i = 0; i < _participants.Length; i++)
|
||||
_participants[i](true);
|
||||
}
|
||||
|
||||
public void EndDestinationReveal(long revealGeneration)
|
||||
{
|
||||
if (revealGeneration <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(revealGeneration));
|
||||
if (_activeGeneration != revealGeneration)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < _participants.Length; i++)
|
||||
_participants[i](false);
|
||||
_activeGeneration = 0;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue