feat(render): unify physical residency accounting
This commit is contained in:
parent
1866ea0c6d
commit
3e18fc2730
25 changed files with 642 additions and 48 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Modern Runtime Slice D — Typed Asset Handles and Unified Residency
|
||||
|
||||
**Status:** active
|
||||
**Status:** active — D1 and D2 complete
|
||||
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
|
||||
**Baseline:** Slice C closeout commit `a564c4b7`
|
||||
**Behavior contract:** no visual-quality or retail-behavior change
|
||||
|
|
@ -196,6 +196,10 @@ defaults below their current values.
|
|||
- Test stale completions, duplicate acquire, owner reuse, asset-slot reuse,
|
||||
cancellation, corrupt/failure states, and illegal transitions.
|
||||
|
||||
**Complete 2026-07-24.** The typed ledger, generation checks, bounded
|
||||
worker-observation journal, startup budget profile, and transition conformance
|
||||
tests landed without moving physical GL ownership into the policy layer.
|
||||
|
||||
### D2 — Existing-owner adapters
|
||||
|
||||
- Parameterize current mesh, staging, composite, and standalone budgets.
|
||||
|
|
@ -205,6 +209,18 @@ defaults below their current values.
|
|||
- Cross-check manager GPU totals with `GpuMemoryTracker`, allowing explicitly
|
||||
enumerated non-cache resources.
|
||||
|
||||
**Complete 2026-07-24.** Production composition passes one immutable budget
|
||||
profile into the existing mesh and texture owners and registers exact snapshot
|
||||
sources for object geometry/atlases, prepared meshes, staging, the global mesh
|
||||
arena, composite arrays, standalone particle arrays, and the prepared package
|
||||
mapping. Removed object atlases and standalone textures retain retiring-byte
|
||||
ownership until their accepted frame-fence release actually completes.
|
||||
Lifecycle artifacts now publish both cache-attributed GPU bytes and the signed
|
||||
`GpuTrackerMinusResidencyBytes` remainder; that remainder deliberately covers
|
||||
enumerated non-cache allocations such as terrain, shaders, UI textures,
|
||||
framebuffers, and dynamic draw buffers rather than being mislabeled as cache
|
||||
residence.
|
||||
|
||||
### D3 — Missing policies
|
||||
|
||||
- Bound `RetailAnimationLoader` by count and estimated retained bytes.
|
||||
|
|
@ -242,4 +258,3 @@ Slice D closes only when:
|
|||
6. The architecture, roadmap, issues, inventory, and durable memory agree with
|
||||
the implementation.
|
||||
7. Connected screenshots retain Slice C's visual output.
|
||||
|
||||
|
|
|
|||
|
|
@ -408,7 +408,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
foundation.TextureCache,
|
||||
live.DrawDispatcher,
|
||||
d.FrameProfiler,
|
||||
content.Dats);
|
||||
content.Dats,
|
||||
foundation.Residency);
|
||||
lifecycleAutomation =
|
||||
new WorldLifecycleAutomationController(
|
||||
() => session.WorldReveal.Snapshot,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Concurrent;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Terrain;
|
||||
|
|
@ -37,7 +38,8 @@ internal sealed record WorldRenderFoundation(
|
|||
Shader MeshShader,
|
||||
WbMeshAdapter MeshAdapter,
|
||||
TextureCache TextureCache,
|
||||
SamplerCache Samplers);
|
||||
SamplerCache Samplers,
|
||||
ResidencyManager Residency);
|
||||
|
||||
internal sealed record WorldRenderResult(
|
||||
WorldTerrainBuildContext TerrainBuild,
|
||||
|
|
@ -47,6 +49,7 @@ internal sealed record WorldRenderDependencies(
|
|||
WorldEnvironmentController Environment,
|
||||
IGameRenderResourceLifetime RenderResources,
|
||||
IGpuResourceRetirementQueue ResourceRetirement,
|
||||
ResidencyBudgetOptions ResidencyBudgets,
|
||||
uint InitialCenterLandblockId,
|
||||
Action<string> Log);
|
||||
|
||||
|
|
@ -101,12 +104,19 @@ internal interface IWorldRenderCompositionFactory
|
|||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
IGpuResourceRetirementQueue retirement);
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
ResidencyBudgetOptions budgets);
|
||||
TextureCache CreateTextureCache(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
BindlessSupport bindless,
|
||||
IGpuResourceRetirementQueue retirement);
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
ResidencyBudgetOptions budgets);
|
||||
void RegisterResidencySources(
|
||||
ResidencyManager manager,
|
||||
WbMeshAdapter meshes,
|
||||
TextureCache textures,
|
||||
IPreparedAssetSource preparedAssets);
|
||||
SamplerCache CreateSamplerCache(GL gl);
|
||||
void Release(IDisposable resource);
|
||||
}
|
||||
|
|
@ -263,20 +273,42 @@ internal sealed class RetailWorldRenderCompositionFactory
|
|||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
IGpuResourceRetirementQueue retirement) =>
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
ResidencyBudgetOptions budgets) =>
|
||||
new(
|
||||
gl,
|
||||
dats,
|
||||
preparedAssets,
|
||||
NullLogger<WbMeshAdapter>.Instance,
|
||||
retirement);
|
||||
retirement,
|
||||
budgets);
|
||||
|
||||
public TextureCache CreateTextureCache(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
BindlessSupport bindless,
|
||||
IGpuResourceRetirementQueue retirement) =>
|
||||
new(gl, dats, bindless, retirement);
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
ResidencyBudgetOptions budgets) =>
|
||||
new(gl, dats, bindless, retirement, budgets);
|
||||
|
||||
public void RegisterResidencySources(
|
||||
ResidencyManager manager,
|
||||
WbMeshAdapter meshes,
|
||||
TextureCache textures,
|
||||
IPreparedAssetSource preparedAssets)
|
||||
{
|
||||
meshes.RegisterResidencySources(manager);
|
||||
textures.RegisterResidencySources(manager);
|
||||
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
|
||||
ResidencyDomain.PreparedPackage,
|
||||
() => new ResidencyDomainSnapshot(
|
||||
ResidencyDomain.PreparedPackage,
|
||||
EntryCount: 1,
|
||||
OwnerCount: 1,
|
||||
Charges: new ResidencyCharges(
|
||||
MappedVirtualBytes:
|
||||
preparedAssets.MappedVirtualBytes))));
|
||||
}
|
||||
|
||||
public SamplerCache CreateSamplerCache(GL gl) => new(gl);
|
||||
|
||||
|
|
@ -349,6 +381,8 @@ internal sealed class WorldRenderCompositionPhase
|
|||
try
|
||||
{
|
||||
GL gl = platform.Graphics;
|
||||
var residency = new ResidencyManager(
|
||||
_dependencies.ResidencyBudgets);
|
||||
_factory.InitializeGlState(gl);
|
||||
Fault(WorldRenderCompositionPoint.GlStateInitialized);
|
||||
|
||||
|
|
@ -434,7 +468,8 @@ internal sealed class WorldRenderCompositionPhase
|
|||
gl,
|
||||
content.Dats,
|
||||
content.PreparedAssets,
|
||||
_dependencies.ResourceRetirement),
|
||||
_dependencies.ResourceRetirement,
|
||||
residency.Budgets),
|
||||
_publication.PublishWbMeshAdapter,
|
||||
WorldRenderCompositionPoint.MeshAdapterPublished);
|
||||
TextureCache textureCache = AcquireAndPublish(
|
||||
|
|
@ -444,7 +479,8 @@ internal sealed class WorldRenderCompositionPhase
|
|||
gl,
|
||||
content.Dats,
|
||||
bindless,
|
||||
_dependencies.ResourceRetirement),
|
||||
_dependencies.ResourceRetirement,
|
||||
residency.Budgets),
|
||||
_publication.PublishTextureCache,
|
||||
WorldRenderCompositionPoint.TextureCachePublished);
|
||||
SamplerCache samplers = AcquireAndPublish(
|
||||
|
|
@ -453,6 +489,11 @@ internal sealed class WorldRenderCompositionPhase
|
|||
() => _factory.CreateSamplerCache(gl),
|
||||
_publication.PublishSamplerCache,
|
||||
WorldRenderCompositionPoint.SamplerCachePublished);
|
||||
_factory.RegisterResidencySources(
|
||||
residency,
|
||||
meshAdapter,
|
||||
textureCache,
|
||||
content.PreparedAssets);
|
||||
|
||||
scope.Complete();
|
||||
_dependencies.Log(
|
||||
|
|
@ -473,7 +514,8 @@ internal sealed class WorldRenderCompositionPhase
|
|||
meshShader,
|
||||
meshAdapter,
|
||||
textureCache,
|
||||
samplers));
|
||||
samplers,
|
||||
residency));
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text.Json;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.UI.Testing;
|
||||
|
||||
|
|
@ -53,6 +54,8 @@ internal sealed record WorldLifecycleResourceSnapshot(
|
|||
int StagedMeshUploads,
|
||||
long StagedMeshBytes,
|
||||
long TrackedGpuBytes,
|
||||
long ResidencyGpuBytes,
|
||||
long GpuTrackerMinusResidencyBytes,
|
||||
int TrackedGpuBuffers,
|
||||
int TrackedGpuTextures,
|
||||
int OwnedCompositeTextures,
|
||||
|
|
@ -74,6 +77,7 @@ internal sealed record WorldLifecycleResourceSnapshot(
|
|||
long DatObjectCacheHits,
|
||||
long DatObjectCacheMisses,
|
||||
long DatObjectCacheEvictions,
|
||||
ResidencySnapshot Residency,
|
||||
double Fps,
|
||||
double FrameMilliseconds,
|
||||
string? LastFrameProfile);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
|
|
@ -36,6 +37,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
private readonly WbDrawDispatcher _dispatcher;
|
||||
private readonly FrameProfiler _frameProfiler;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly ResidencyManager _residency;
|
||||
|
||||
public WorldLifecycleResourceSnapshotSource(
|
||||
GpuWorldState world,
|
||||
|
|
@ -52,7 +54,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
TextureCache textures,
|
||||
WbDrawDispatcher dispatcher,
|
||||
FrameProfiler frameProfiler,
|
||||
IDatReaderWriter dats)
|
||||
IDatReaderWriter dats,
|
||||
ResidencyManager residency)
|
||||
{
|
||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
_animations = animations
|
||||
|
|
@ -77,6 +80,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
_frameProfiler = frameProfiler
|
||||
?? throw new ArgumentNullException(nameof(frameProfiler));
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_residency = residency
|
||||
?? throw new ArgumentNullException(nameof(residency));
|
||||
}
|
||||
|
||||
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
|
||||
|
|
@ -105,6 +110,9 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
: default;
|
||||
CacheStats cpuMeshCacheStats = meshManager.CpuMeshCacheStats;
|
||||
CacheStats decodedTextureCacheStats = meshManager.DecodedTextureCacheStats;
|
||||
ResidencySnapshot residency = _residency.CaptureSnapshot();
|
||||
long trackedGpuBytes = GpuMemoryTracker.AllocatedBytes;
|
||||
long residencyGpuBytes = residency.TotalCharges.PhysicalGpuBytes;
|
||||
|
||||
return new WorldLifecycleResourceSnapshot(
|
||||
LoadedLandblocks: _world.LoadedLandblockIds.Count,
|
||||
|
|
@ -129,7 +137,10 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
MeshEstimatedBytes: mesh.EstimatedBytes,
|
||||
StagedMeshUploads: _meshes.StagedUploadBacklog,
|
||||
StagedMeshBytes: _meshes.StagedUploadBytes,
|
||||
TrackedGpuBytes: GpuMemoryTracker.AllocatedBytes,
|
||||
TrackedGpuBytes: trackedGpuBytes,
|
||||
ResidencyGpuBytes: residencyGpuBytes,
|
||||
GpuTrackerMinusResidencyBytes: checked(
|
||||
trackedGpuBytes - residencyGpuBytes),
|
||||
TrackedGpuBuffers: GpuMemoryTracker.BufferCount,
|
||||
TrackedGpuTextures: GpuMemoryTracker.TextureCount,
|
||||
OwnedCompositeTextures: _textures.OwnedBindlessTextureCount,
|
||||
|
|
@ -152,6 +163,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
DatObjectCacheHits: datObjectCacheStats.Hits,
|
||||
DatObjectCacheMisses: datObjectCacheStats.Misses,
|
||||
DatObjectCacheEvictions: datObjectCacheStats.Evictions,
|
||||
Residency: residency,
|
||||
Fps: render.Fps,
|
||||
FrameMilliseconds: render.FrameMilliseconds,
|
||||
LastFrameProfile: _frameProfiler.LastReport);
|
||||
|
|
|
|||
|
|
@ -384,6 +384,8 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
internal long UnownedBytes => _unowned.ResidentBytes;
|
||||
internal int AtlasCount => _atlases.Count;
|
||||
internal long AllocatedBytes => _allocatedBytes;
|
||||
internal long PhysicalBudgetBytes => _physicalBudgetBytes;
|
||||
internal long UnownedBudgetBytes => _unowned.BudgetBytes;
|
||||
internal int FrameUploadCount => _frameUploadCount;
|
||||
internal long FrameUploadBytes => _frameUploadBytes;
|
||||
internal bool CanStartUpload =>
|
||||
|
|
@ -391,6 +393,49 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
|||
&& _frameUploadCount < _maximumUploadsPerFrame
|
||||
&& (_frameUploadCount == 0 || _frameUploadBytes < _maximumUploadBytesPerFrame);
|
||||
|
||||
internal Residency.ResidencyDomainSnapshot CaptureResidency()
|
||||
{
|
||||
long retiringBytes = 0;
|
||||
long usedBytes = 0;
|
||||
long availableBytes = 0;
|
||||
for (int i = 0; i < _atlases.Count; i++)
|
||||
{
|
||||
Atlas atlas = _atlases[i];
|
||||
if (atlas.ReleaseRequested
|
||||
|| atlas.ReleaseStage != AtlasReleaseStage.Resident)
|
||||
{
|
||||
retiringBytes = checked(
|
||||
retiringBytes + atlas.Resource.Bytes);
|
||||
usedBytes = checked(
|
||||
usedBytes + atlas.Resource.Bytes);
|
||||
continue;
|
||||
}
|
||||
|
||||
long layerBytes = atlas.Resource.Bytes / atlas.Slots.Capacity;
|
||||
usedBytes = checked(
|
||||
usedBytes
|
||||
+ layerBytes * checked(
|
||||
atlas.EntryCount + atlas.PendingRetirements));
|
||||
availableBytes = checked(
|
||||
availableBytes
|
||||
+ layerBytes * atlas.AvailableLayers);
|
||||
}
|
||||
|
||||
return new Residency.ResidencyDomainSnapshot(
|
||||
Residency.ResidencyDomain.CompositeTextures,
|
||||
EntryCount: _entries.Count,
|
||||
OwnerCount: _owners.OwnerCount,
|
||||
Charges: new Residency.ResidencyCharges(
|
||||
GpuRequestedBytes: _pendingAtlasAllocationBytes,
|
||||
GpuResidentBytes: checked(
|
||||
_allocatedBytes - retiringBytes),
|
||||
RetiringBytes: retiringBytes),
|
||||
BudgetBytes: _physicalBudgetBytes,
|
||||
CapacityBytes: _allocatedBytes,
|
||||
UsedBytes: usedBytes,
|
||||
LargestFreeBytes: availableBytes);
|
||||
}
|
||||
|
||||
internal bool CanUpload(long bytes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bytes);
|
||||
|
|
|
|||
|
|
@ -1221,6 +1221,7 @@ public sealed class GameWindow :
|
|||
_worldEnvironment,
|
||||
_renderResourceLifetime,
|
||||
_gpuFrameFlights!,
|
||||
_options.ResidencyBudgets,
|
||||
initialCenterLandblockId,
|
||||
Console.WriteLine),
|
||||
this).Compose(platformResult, contentEffectsAudio, settingsDevTools);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
namespace AcDream.App.Rendering.Residency;
|
||||
|
||||
internal sealed class DelegateResidencyDomainSource(
|
||||
ResidencyDomain domain,
|
||||
Func<ResidencyDomainSnapshot> capture) : IResidencyDomainSource
|
||||
{
|
||||
private readonly Func<ResidencyDomainSnapshot> _capture =
|
||||
capture ?? throw new ArgumentNullException(nameof(capture));
|
||||
|
||||
public ResidencyDomain Domain { get; } = domain;
|
||||
|
||||
public ResidencyDomainSnapshot CaptureResidency()
|
||||
{
|
||||
ResidencyDomainSnapshot snapshot = _capture();
|
||||
if (snapshot.Domain != Domain)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Residency source {Domain} returned {snapshot.Domain}.");
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,11 @@ internal sealed class ResidencyManager
|
|||
private readonly List<ResidencyObservation> _observationScratch = [];
|
||||
private readonly List<ResidencyDomainSnapshot> _snapshotScratch = [];
|
||||
|
||||
public ResidencyManager(ResidencyBudgetOptions? budgets = null) =>
|
||||
Budgets = budgets ?? ResidencyBudgetOptions.Default;
|
||||
|
||||
public ResidencyBudgetOptions Budgets { get; }
|
||||
|
||||
public int ActiveAssetCount => _assetByKey.Count;
|
||||
|
||||
public int ActiveOwnerCount => _owners.Count(owner => owner is not null);
|
||||
|
|
@ -355,19 +360,46 @@ internal sealed class ResidencyManager
|
|||
public ResidencySnapshot CaptureSnapshot()
|
||||
{
|
||||
_snapshotScratch.Clear();
|
||||
for (int i = 0; i < _domainSources.Count; i++)
|
||||
_snapshotScratch.Add(_domainSources[i].CaptureResidency());
|
||||
|
||||
Span<bool> sourced = stackalloc bool[
|
||||
Enum.GetValues<ResidencyDomain>().Length];
|
||||
ResidencyCharges totals = default;
|
||||
for (int i = 0; i < _domainSources.Count; i++)
|
||||
{
|
||||
ResidencyDomainSnapshot snapshot =
|
||||
_domainSources[i].CaptureResidency();
|
||||
_snapshotScratch.Add(snapshot);
|
||||
sourced[(int)snapshot.Domain] = true;
|
||||
totals += snapshot.Charges;
|
||||
}
|
||||
|
||||
ResidencyCharges[] logicalCharges =
|
||||
new ResidencyCharges[sourced.Length];
|
||||
int[] logicalEntries = new int[sourced.Length];
|
||||
int[] logicalOwners = new int[sourced.Length];
|
||||
for (uint index = 1; index < _assets.Count; index++)
|
||||
{
|
||||
AssetSlot? slot = _assets[checked((int)index)];
|
||||
if (slot is not null)
|
||||
totals += slot.Charges;
|
||||
if (slot is null || sourced[(int)slot.Key.Domain])
|
||||
continue;
|
||||
int domain = (int)slot.Key.Domain;
|
||||
logicalCharges[domain] += slot.Charges;
|
||||
logicalEntries[domain]++;
|
||||
logicalOwners[domain] = checked(
|
||||
logicalOwners[domain] + slot.Owners.Count);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _snapshotScratch.Count; i++)
|
||||
totals += _snapshotScratch[i].Charges;
|
||||
for (int domain = 0; domain < logicalEntries.Length; domain++)
|
||||
{
|
||||
if (logicalEntries[domain] == 0)
|
||||
continue;
|
||||
ResidencyCharges charges = logicalCharges[domain];
|
||||
_snapshotScratch.Add(new ResidencyDomainSnapshot(
|
||||
(ResidencyDomain)domain,
|
||||
logicalEntries[domain],
|
||||
logicalOwners[domain],
|
||||
charges));
|
||||
totals += charges;
|
||||
}
|
||||
|
||||
return new ResidencySnapshot(_snapshotScratch.ToArray(), totals);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ internal sealed class StandaloneBindlessTextureCache : IDisposable
|
|||
private readonly Dictionary<uint, StandaloneBindlessTextureResource> _entries = new();
|
||||
private readonly HashSet<uint> _disposeResidencyReleased = [];
|
||||
private readonly HashSet<uint> _disposeDeleted = [];
|
||||
private long _allocatedBytes;
|
||||
private long _retiringBytes;
|
||||
private bool _disposeRequested;
|
||||
private bool _disposing;
|
||||
private bool _disposed;
|
||||
|
|
@ -62,6 +64,9 @@ internal sealed class StandaloneBindlessTextureCache : IDisposable
|
|||
internal int OwnerCount => _owners.OwnerCount;
|
||||
internal int UnownedEntryCount => _unowned.Count;
|
||||
internal long UnownedBytes => _unowned.ResidentBytes;
|
||||
internal long AllocatedBytes => _allocatedBytes;
|
||||
internal long RetiringBytes => _retiringBytes;
|
||||
internal long BudgetBytes => _unowned.BudgetBytes;
|
||||
internal int AwaitingRetirementPublicationCount =>
|
||||
_retirementLedger.AwaitingPublicationCount;
|
||||
|
||||
|
|
@ -96,6 +101,7 @@ internal sealed class StandaloneBindlessTextureCache : IDisposable
|
|||
$"Standalone particle surface 0x{resource.SurfaceId:X8} is already cached.");
|
||||
|
||||
_owners.Acquire(ownerId, resource.SurfaceId);
|
||||
_allocatedBytes = checked(_allocatedBytes + resource.Bytes);
|
||||
}
|
||||
|
||||
public void ReleaseOwner(uint ownerId)
|
||||
|
|
@ -143,9 +149,14 @@ internal sealed class StandaloneBindlessTextureCache : IDisposable
|
|||
// The publication ledger owns this release from this point even
|
||||
// when queue admission or an immediate callback throws. Never
|
||||
// republish a texture after residency release has started.
|
||||
_retiringBytes = checked(_retiringBytes + resource.Bytes);
|
||||
_retirementLedger.Retire(new RetryableGpuResourceRelease(
|
||||
() => _backend.MakeNonResident(resource),
|
||||
() => _backend.Delete(resource)));
|
||||
() => _backend.Delete(resource),
|
||||
() => _retiringBytes = checked(
|
||||
_retiringBytes - resource.Bytes),
|
||||
() => _allocatedBytes = checked(
|
||||
_allocatedBytes - resource.Bytes)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -204,6 +215,8 @@ internal sealed class StandaloneBindlessTextureCache : IDisposable
|
|||
{
|
||||
_backend.Delete(resource);
|
||||
_disposeDeleted.Add(resource.SurfaceId);
|
||||
_allocatedBytes = checked(
|
||||
_allocatedBytes - resource.Bytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using Silk.NET.OpenGL;
|
|||
using System.Linq;
|
||||
using PixelFormatId = DatReaderWriter.Enums.PixelFormat;
|
||||
using SurfaceType = DatReaderWriter.Enums.SurfaceType;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
|
|
@ -87,8 +88,10 @@ public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable
|
|||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
Wb.BindlessSupport? bindless,
|
||||
IGpuResourceRetirementQueue retirementQueue)
|
||||
IGpuResourceRetirementQueue retirementQueue,
|
||||
ResidencyBudgetOptions? budgets = null)
|
||||
{
|
||||
budgets ??= ResidencyBudgetOptions.Default;
|
||||
_gl = gl;
|
||||
_dats = dats;
|
||||
_bindless = bindless;
|
||||
|
|
@ -103,11 +106,15 @@ public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable
|
|||
composite = new CompositeTextureArrayCache(
|
||||
gl,
|
||||
bindless,
|
||||
retirementQueue);
|
||||
retirementQueue,
|
||||
budgets.CompositeUnownedBytes,
|
||||
budgets.CompositePhysicalBytes);
|
||||
resources.Add("composite texture cache", composite.Dispose);
|
||||
particles = new StandaloneBindlessTextureCache(
|
||||
new ParticleTextureBackend(this),
|
||||
retirementQueue);
|
||||
retirementQueue,
|
||||
budgets.StandaloneUnownedBytes,
|
||||
budgets.StandaloneUnownedEntries);
|
||||
resources.Add("particle texture cache", particles.Dispose);
|
||||
resources.TransferAll();
|
||||
}
|
||||
|
|
@ -123,6 +130,37 @@ public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
internal void RegisterResidencySources(ResidencyManager manager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manager);
|
||||
if (_compositeTextures is not null)
|
||||
{
|
||||
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
|
||||
ResidencyDomain.CompositeTextures,
|
||||
_compositeTextures.CaptureResidency));
|
||||
}
|
||||
if (_particleTextures is not null)
|
||||
{
|
||||
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
|
||||
ResidencyDomain.StandaloneTextures,
|
||||
CaptureStandaloneResidency));
|
||||
}
|
||||
}
|
||||
|
||||
private ResidencyDomainSnapshot CaptureStandaloneResidency()
|
||||
{
|
||||
StandaloneBindlessTextureCache textures = EnsureParticleTexturesAvailable();
|
||||
return new ResidencyDomainSnapshot(
|
||||
ResidencyDomain.StandaloneTextures,
|
||||
EntryCount: textures.EntryCount,
|
||||
OwnerCount: textures.OwnerCount,
|
||||
Charges: new ResidencyCharges(
|
||||
GpuResidentBytes: checked(
|
||||
textures.AllocatedBytes - textures.RetiringBytes),
|
||||
RetiringBytes: textures.RetiringBytes),
|
||||
BudgetBytes: textures.BudgetBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get or upload the GL texture handle for a Surface id. Returns a
|
||||
/// 1x1 magenta fallback if the Surface or its RenderSurface chain is
|
||||
|
|
|
|||
|
|
@ -137,6 +137,19 @@ public sealed class GlobalMeshBuffer : IDisposable
|
|||
|| _retiredCapacityBytes != 0;
|
||||
internal int VertexHighWaterMark => _vertices.HighWaterMark;
|
||||
internal int IndexHighWaterMark => _indices.HighWaterMark;
|
||||
internal long UsedBytes => checked(
|
||||
(long)_vertices.Used * VertexPositionNormalTexture.Size
|
||||
+ (long)_indices.Used * sizeof(ushort));
|
||||
internal long LargestFreeBytes => checked(
|
||||
(long)_vertices.LargestFreeRange * VertexPositionNormalTexture.Size
|
||||
+ (long)_indices.LargestFreeRange * sizeof(ushort));
|
||||
internal long PendingRangeRetirementBytes => checked(
|
||||
(long)_vertices.PendingReleaseLength * VertexPositionNormalTexture.Size
|
||||
+ (long)_indices.PendingReleaseLength * sizeof(ushort));
|
||||
internal long RequestedMigrationBytes => _migration?.NewCapacityBytes ?? 0;
|
||||
internal long RetiredBackingBytes => _retiredCapacityBytes;
|
||||
internal long ResidentCapacityBytes => checked(
|
||||
CapacityBytes - PendingRangeRetirementBytes);
|
||||
|
||||
internal GlobalMeshUploadPlan PlanUpload(int vertexCount, int indexCount)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -523,6 +523,15 @@ namespace AcDream.App.Rendering.Wb {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True only after every retained bindless-handle, GL-name, and memory
|
||||
/// accounting release stage has completed. Logical disposal can become
|
||||
/// durable earlier while the frame fence still owns the physical array.
|
||||
/// </summary>
|
||||
internal bool IsPhysicalRetirementComplete =>
|
||||
Volatile.Read(ref _disposeQueued) != 0
|
||||
&& Volatile.Read(ref _disposeRelease) is null;
|
||||
|
||||
public void Unbind() {
|
||||
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
||||
GLHelpers.CheckErrors(GL);
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@ internal sealed class MeshUploadStagingQueue
|
|||
public int ClaimCount { get { lock (_gate) return _generationById.Count; } }
|
||||
public long QueuedBytes { get { lock (_gate) return _queuedBytes; } }
|
||||
public long ClaimedBytes { get { lock (_gate) return _claimedBytes; } }
|
||||
public int MaximumCount => _maximumCount;
|
||||
public long MaximumBytes => _maximumBytes;
|
||||
public bool IsAtHighWater
|
||||
{
|
||||
get
|
||||
|
|
@ -249,6 +251,11 @@ internal sealed class MeshOwnershipCounter
|
|||
|
||||
public bool IsOwned(ulong id) => Count(id) > 0;
|
||||
|
||||
public int OwnedIdCount => _counts.Count(pair => pair.Value > 0);
|
||||
|
||||
public int TotalReferenceCount => _counts.Sum(
|
||||
pair => Math.Max(0, pair.Value));
|
||||
|
||||
/// <summary>
|
||||
/// Reports whether freshly uploaded data has a live owner. This is a query,
|
||||
/// not an acquire: upload existence and logical ownership are independent.
|
||||
|
|
@ -297,6 +304,8 @@ internal sealed class CpuMeshUploadCache
|
|||
|
||||
internal int Count { get { lock (_data) return _data.Count; } }
|
||||
internal long ResidentBytes { get { lock (_data) return _residentBytes; } }
|
||||
internal int Capacity => _capacity;
|
||||
internal long ByteCapacity => _byteCapacity;
|
||||
|
||||
public bool TryGetAndStage(
|
||||
ulong id,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ using System.Runtime.InteropServices;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AcDream.Content;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
using AcDream.Core.Rendering.Wb;
|
||||
using PixelFormat = Silk.NET.OpenGL.PixelFormat;
|
||||
using BoundingBox = Chorizite.Core.Lib.BoundingBox;
|
||||
|
|
@ -143,8 +144,8 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
// LRU Cache for Unused objects
|
||||
private readonly LinkedList<ulong> _lruList = new();
|
||||
private readonly long _maxGpuMemory = 1024 * 1024 * 1024; // 1GB
|
||||
private readonly int _maxCachedObjects = 50; // Max number of cached objects (count-based limit)
|
||||
private readonly long _maxGpuMemory;
|
||||
private readonly int _maxCachedObjects;
|
||||
private long _currentNonArenaGpuMemory;
|
||||
|
||||
// Shared atlases grouped by (Width, Height, Format)
|
||||
|
|
@ -158,12 +159,15 @@ namespace AcDream.App.Rendering.Wb
|
|||
private const int RetainedEmptyAtlasCountLimit = 32;
|
||||
private readonly AcDream.App.Rendering.BoundedUnownedResourceCache<TextureAtlasManager>
|
||||
_safeEmptyAtlases = new(RetainedEmptyAtlasBudgetBytes, RetainedEmptyAtlasCountLimit);
|
||||
// Arrays removed from the reusable atlas families remain physical GPU
|
||||
// allocations until their frame-fenced release completes. Retaining
|
||||
// the owners here makes that overlap both retryable and observable.
|
||||
private readonly List<TextureAtlasManager> _retiringAtlases = [];
|
||||
|
||||
// CPU-side cache for prepared mesh data (to avoid re-reading/decoding from DAT)
|
||||
private readonly int _maxCpuCacheSize = 100;
|
||||
private readonly CpuMeshUploadCache _cpuMeshCache;
|
||||
|
||||
private readonly MeshUploadStagingQueue _stagedMeshData = new();
|
||||
private readonly MeshUploadStagingQueue _stagedMeshData;
|
||||
private volatile bool _arenaBackpressured;
|
||||
|
||||
/// <summary>#125: how many times a failed GL upload is re-staged before
|
||||
|
|
@ -264,8 +268,13 @@ namespace AcDream.App.Rendering.Wb
|
|||
int atlasArrays = 0;
|
||||
foreach (List<TextureAtlasManager> atlases in _globalAtlases.Values)
|
||||
atlasArrays += atlases.Count;
|
||||
long atlasBytes = CalculateAtlasBytes(_globalAtlases.Values);
|
||||
long retiringAtlasBytes = CalculateAtlasBytes(_retiringAtlases);
|
||||
long physicalBytes = CalculateTrackedGpuBytes(
|
||||
_currentNonArenaGpuMemory,
|
||||
checked(
|
||||
_currentNonArenaGpuMemory
|
||||
+ atlasBytes
|
||||
+ retiringAtlasBytes),
|
||||
GlobalBuffer?.PhysicalCapacityBytes ?? 0);
|
||||
return (_renderData.Count, atlasArrays, _lruList.Count, physicalBytes);
|
||||
}
|
||||
|
|
@ -400,15 +409,24 @@ namespace AcDream.App.Rendering.Wb
|
|||
public ObjectMeshManager(
|
||||
OpenGLGraphicsDevice graphicsDevice,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
ILogger<ObjectMeshManager> logger)
|
||||
ILogger<ObjectMeshManager> logger,
|
||||
ResidencyBudgetOptions? budgets = null)
|
||||
{
|
||||
budgets ??= ResidencyBudgetOptions.Default;
|
||||
_graphicsDevice = graphicsDevice
|
||||
?? throw new ArgumentNullException(nameof(graphicsDevice));
|
||||
_preparedAssets = preparedAssets
|
||||
?? throw new ArgumentNullException(nameof(preparedAssets));
|
||||
_logger = logger
|
||||
?? throw new ArgumentNullException(nameof(logger));
|
||||
_cpuMeshCache = new CpuMeshUploadCache(_maxCpuCacheSize);
|
||||
_maxGpuMemory = budgets.ObjectMeshGpuBytes;
|
||||
_maxCachedObjects = budgets.ObjectMeshUnownedEntries;
|
||||
_cpuMeshCache = new CpuMeshUploadCache(
|
||||
budgets.PreparedMeshCpuEntries,
|
||||
budgets.PreparedMeshCpuBytes);
|
||||
_stagedMeshData = new MeshUploadStagingQueue(
|
||||
budgets.MeshStagingEntries,
|
||||
budgets.MeshStagingBytes);
|
||||
_useModernRendering = _graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless;
|
||||
if (_useModernRendering)
|
||||
{
|
||||
|
|
@ -506,6 +524,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// </summary>
|
||||
internal bool EvictOneEmptyAtlas()
|
||||
{
|
||||
RetryRetiringAtlasDisposals();
|
||||
if (!_safeEmptyAtlases.TryTakeOldestOverBudget(out TextureAtlasManager victim))
|
||||
return false;
|
||||
|
||||
|
|
@ -517,10 +536,28 @@ namespace AcDream.App.Rendering.Wb
|
|||
_globalAtlases.Remove(key);
|
||||
}
|
||||
_dirtyAtlases.Remove(victim);
|
||||
_retiringAtlases.Add(victim);
|
||||
victim.Dispose();
|
||||
RemoveCompletedAtlasRetirements();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RetryRetiringAtlasDisposals()
|
||||
{
|
||||
for (int i = 0; i < _retiringAtlases.Count; i++)
|
||||
_retiringAtlases[i].Dispose();
|
||||
RemoveCompletedAtlasRetirements();
|
||||
}
|
||||
|
||||
private void RemoveCompletedAtlasRetirements()
|
||||
{
|
||||
for (int i = _retiringAtlases.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_retiringAtlases[i].IsPhysicalRetirementComplete)
|
||||
_retiringAtlases.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAtlasGpuSafeEmpty(TextureAtlasManager atlas)
|
||||
{
|
||||
if (IsDisposed || !atlas.IsGpuSafeEmpty || _safeEmptyAtlases.Contains(atlas))
|
||||
|
|
@ -637,6 +674,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(maximumCount, 1);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(maximumBytes, 1);
|
||||
RetryRetiringAtlasDisposals();
|
||||
RetryPendingAtlasRetirements();
|
||||
// Rollbacks own unpublished resources and therefore have no LRU
|
||||
// node to wake them. Advance a bounded snapshot every render tick
|
||||
|
|
@ -649,6 +687,9 @@ namespace AcDream.App.Rendering.Wb
|
|||
lock (_lruList)
|
||||
candidateBudget = _lruList.Count;
|
||||
int attemptedCandidates = 0;
|
||||
long trackedAtlasBytes = checked(
|
||||
CalculateAtlasBytes(_globalAtlases.Values)
|
||||
+ CalculateAtlasBytes(_retiringAtlases));
|
||||
|
||||
while (reclaimedCount < maximumCount
|
||||
&& attemptedCandidates < candidateBudget)
|
||||
|
|
@ -657,9 +698,12 @@ namespace AcDream.App.Rendering.Wb
|
|||
lock (_lruList)
|
||||
{
|
||||
long physicalArenaBytes = GlobalBuffer?.PhysicalCapacityBytes ?? 0;
|
||||
long nonArenaAndAtlasBytes = checked(
|
||||
_currentNonArenaGpuMemory
|
||||
+ trackedAtlasBytes);
|
||||
if (!forceArenaReclamation
|
||||
&& IsWithinGpuCacheBudget(
|
||||
_currentNonArenaGpuMemory,
|
||||
nonArenaAndAtlasBytes,
|
||||
physicalArenaBytes,
|
||||
_maxGpuMemory)
|
||||
&& _lruList.Count <= _maxCachedObjects)
|
||||
|
|
@ -1207,6 +1251,105 @@ namespace AcDream.App.Rendering.Wb
|
|||
return _preparedAssets.Read(request, ct).Data;
|
||||
}
|
||||
|
||||
internal ResidencyDomainSnapshot CaptureObjectMeshResidency()
|
||||
{
|
||||
GlobalMeshBuffer? arena = GlobalBuffer;
|
||||
long nonArenaRetiring = 0;
|
||||
foreach (ObjectReleaseTicket ticket in _objectReleases.Values)
|
||||
{
|
||||
nonArenaRetiring = checked(
|
||||
nonArenaRetiring
|
||||
+ Math.Max(0, ticket.Data.NonArenaGpuBytes));
|
||||
}
|
||||
nonArenaRetiring = Math.Min(
|
||||
nonArenaRetiring,
|
||||
_currentNonArenaGpuMemory);
|
||||
|
||||
long arenaResident = arena?.ResidentCapacityBytes ?? 0;
|
||||
long arenaRequested = arena?.RequestedMigrationBytes ?? 0;
|
||||
long arenaRetiring = checked(
|
||||
(arena?.PendingRangeRetirementBytes ?? 0)
|
||||
+ (arena?.RetiredBackingBytes ?? 0));
|
||||
long atlasResident = CalculateAtlasBytes(_globalAtlases.Values);
|
||||
long atlasRetiring = CalculateAtlasBytes(_retiringAtlases);
|
||||
return new ResidencyDomainSnapshot(
|
||||
ResidencyDomain.ObjectMeshes,
|
||||
EntryCount: checked(
|
||||
_renderData.Count
|
||||
+ _globalAtlases.Values.Sum(static atlases => atlases.Count)
|
||||
+ _retiringAtlases.Count),
|
||||
OwnerCount: _ownership.TotalReferenceCount,
|
||||
Charges: new ResidencyCharges(
|
||||
GpuRequestedBytes: arenaRequested,
|
||||
GpuResidentBytes: checked(
|
||||
_currentNonArenaGpuMemory
|
||||
- nonArenaRetiring
|
||||
+ arenaResident
|
||||
+ atlasResident),
|
||||
RetiringBytes: checked(
|
||||
nonArenaRetiring
|
||||
+ arenaRetiring
|
||||
+ atlasRetiring)),
|
||||
BudgetBytes: _maxGpuMemory);
|
||||
}
|
||||
|
||||
internal static long CalculateAtlasBytes(
|
||||
IEnumerable<IReadOnlyCollection<TextureAtlasManager>> atlasFamilies)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(atlasFamilies);
|
||||
long bytes = 0;
|
||||
foreach (IReadOnlyCollection<TextureAtlasManager> family in atlasFamilies)
|
||||
{
|
||||
foreach (TextureAtlasManager atlas in family)
|
||||
bytes = checked(bytes + atlas.AllocatedBytes);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
internal static long CalculateAtlasBytes(
|
||||
IEnumerable<TextureAtlasManager> atlases)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(atlases);
|
||||
long bytes = 0;
|
||||
foreach (TextureAtlasManager atlas in atlases)
|
||||
bytes = checked(bytes + atlas.AllocatedBytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
internal ResidencyDomainSnapshot CapturePreparedMeshResidency() =>
|
||||
new(
|
||||
ResidencyDomain.PreparedMeshCpu,
|
||||
EntryCount: _cpuMeshCache.Count,
|
||||
OwnerCount: 0,
|
||||
Charges: new ResidencyCharges(
|
||||
CpuPreparedBytes: _cpuMeshCache.ResidentBytes),
|
||||
BudgetBytes: _cpuMeshCache.ByteCapacity);
|
||||
|
||||
internal ResidencyDomainSnapshot CaptureStagingResidency() =>
|
||||
new(
|
||||
ResidencyDomain.MeshStaging,
|
||||
EntryCount: _stagedMeshData.ClaimCount,
|
||||
OwnerCount: 0,
|
||||
Charges: new ResidencyCharges(
|
||||
StagingBytes: _stagedMeshData.ClaimedBytes),
|
||||
BudgetBytes: _stagedMeshData.MaximumBytes);
|
||||
|
||||
internal ResidencyDomainSnapshot CaptureGlobalArenaResidency()
|
||||
{
|
||||
GlobalMeshBuffer? arena = GlobalBuffer;
|
||||
return new ResidencyDomainSnapshot(
|
||||
ResidencyDomain.GlobalMeshArena,
|
||||
EntryCount: arena is null ? 0 : 2,
|
||||
OwnerCount: 0,
|
||||
// Physical arena bytes are already charged once by the
|
||||
// ObjectMeshes aggregate. This row exposes allocator facts.
|
||||
Charges: ResidencyCharges.Zero,
|
||||
BudgetBytes: GlobalMeshBuffer.MaximumPhysicalArenaBytes,
|
||||
CapacityBytes: arena?.CapacityBytes ?? 0,
|
||||
UsedBytes: arena?.UsedBytes ?? 0,
|
||||
LargestFreeBytes: arena?.LargestFreeBytes ?? 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel preparation tasks for IDs that are no longer needed.
|
||||
/// </summary>
|
||||
|
|
@ -2545,6 +2688,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
// Every layer release has now committed logically. Publish any
|
||||
// callback which previously failed before queue acceptance.
|
||||
Capture(ref failures, RetryPendingAtlasRetirements);
|
||||
Capture(ref failures, RetryRetiringAtlasDisposals);
|
||||
|
||||
bool atlasFailure = false;
|
||||
foreach (List<TextureAtlasManager> atlasList in _globalAtlases.Values)
|
||||
|
|
@ -2577,6 +2721,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
_uploadRollbacks.Clear();
|
||||
_uploadRollbackQueue.Clear();
|
||||
_globalAtlases.Clear();
|
||||
_retiringAtlases.Clear();
|
||||
_dirtyAtlases.Clear();
|
||||
_safeEmptyAtlases.Clear();
|
||||
_currentNonArenaGpuMemory = 0;
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ namespace AcDream.App.Rendering.Wb {
|
|||
return TextureArray.TotalSizeInBytes;
|
||||
}
|
||||
}
|
||||
internal bool IsPhysicalRetirementComplete =>
|
||||
TextureArray.IsPhysicalRetirementComplete;
|
||||
internal long LastUseSequence { get; set; }
|
||||
internal int Width => _textureWidth;
|
||||
internal int Height => _textureHeight;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ internal sealed class TextureAtlasSlotAllocator
|
|||
}
|
||||
|
||||
public int AvailableCount => _returned.Count + (_rented.Length - _next);
|
||||
public int Capacity => _rented.Length;
|
||||
|
||||
public int Rent()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Content;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
using AcDream.Core.Rendering;
|
||||
using DatReaderWriter;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -108,7 +109,8 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
preparedAssets: null,
|
||||
logger,
|
||||
AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance,
|
||||
ownsPreparedAssets: true)
|
||||
ownsPreparedAssets: true,
|
||||
ResidencyBudgetOptions.Default)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -123,21 +125,24 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
preparedAssets: null,
|
||||
logger,
|
||||
resourceRetirement,
|
||||
ownsPreparedAssets: true);
|
||||
ownsPreparedAssets: true,
|
||||
ResidencyBudgetOptions.Default);
|
||||
|
||||
internal WbMeshAdapter(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
ILogger<WbMeshAdapter> logger,
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement)
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement,
|
||||
ResidencyBudgetOptions? budgets = null)
|
||||
: this(
|
||||
gl,
|
||||
dats,
|
||||
preparedAssets,
|
||||
logger,
|
||||
resourceRetirement,
|
||||
ownsPreparedAssets: false)
|
||||
ownsPreparedAssets: false,
|
||||
budgets ?? ResidencyBudgetOptions.Default)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -147,11 +152,13 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
IPreparedAssetSource? preparedAssets,
|
||||
ILogger<WbMeshAdapter> logger,
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement,
|
||||
bool ownsPreparedAssets)
|
||||
bool ownsPreparedAssets,
|
||||
ResidencyBudgetOptions budgets)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
ArgumentNullException.ThrowIfNull(budgets);
|
||||
|
||||
_resourceRetirement = resourceRetirement;
|
||||
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
|
||||
|
|
@ -192,7 +199,8 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
meshManager = new ObjectMeshManager(
|
||||
graphicsDevice,
|
||||
resolvedPreparedAssets,
|
||||
new ConsoleErrorLogger<ObjectMeshManager>());
|
||||
new ConsoleErrorLogger<ObjectMeshManager>(),
|
||||
budgets);
|
||||
resources.Add("WB object mesh manager", meshManager.Dispose);
|
||||
resources.TransferAll();
|
||||
}
|
||||
|
|
@ -210,6 +218,26 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
: null;
|
||||
}
|
||||
|
||||
internal void RegisterResidencySources(ResidencyManager manager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manager);
|
||||
ObjectMeshManager meshManager = _meshManager
|
||||
?? throw new InvalidOperationException(
|
||||
"An initialized mesh adapter is required for residency diagnostics.");
|
||||
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
|
||||
ResidencyDomain.ObjectMeshes,
|
||||
meshManager.CaptureObjectMeshResidency));
|
||||
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
|
||||
ResidencyDomain.PreparedMeshCpu,
|
||||
meshManager.CapturePreparedMeshResidency));
|
||||
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
|
||||
ResidencyDomain.MeshStaging,
|
||||
meshManager.CaptureStagingResidency));
|
||||
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
|
||||
ResidencyDomain.GlobalMeshArena,
|
||||
meshManager.CaptureGlobalArenaResidency));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal Console-backed logger that fires only on
|
||||
/// <see cref="LogLevel.Error"/> and above. Format:
|
||||
|
|
|
|||
|
|
@ -119,6 +119,12 @@ public interface IPreparedAssetSource : IDisposable
|
|||
PreparedAssetSourceStats Stats { get; }
|
||||
|
||||
CacheStats DecodedTextureCacheStats { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Immutable package address-space mapped by this source. This is virtual
|
||||
/// address space, not committed managed or process-working-set bytes.
|
||||
/// </summary>
|
||||
long MappedVirtualBytes => 0;
|
||||
}
|
||||
|
||||
internal static class PreparedAssetRequestContract
|
||||
|
|
@ -211,6 +217,8 @@ public sealed class PakPreparedAssetSource : IPreparedAssetSource
|
|||
|
||||
public CacheStats DecodedTextureCacheStats => default;
|
||||
|
||||
public long MappedVirtualBytes => _reader.FileLength;
|
||||
|
||||
public PreparedAssetPresence Probe(
|
||||
PakAssetType type,
|
||||
uint sourceFileId)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ public sealed class PakReader : IDisposable {
|
|||
private readonly ConcurrentDictionary<int, bool> _loggedCorruption = new();
|
||||
|
||||
public PakHeader Header { get; }
|
||||
public long FileLength => _fileLength;
|
||||
|
||||
public PakReader(string path) {
|
||||
_fileLength = new FileInfo(path).Length;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Runtime.CompilerServices;
|
|||
using AcDream.App.Composition;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Terrain;
|
||||
|
|
@ -19,7 +20,13 @@ public sealed class WorldRenderCompositionTests
|
|||
[Fact]
|
||||
public void SuccessPublishesExactModernFoundationInFrozenOrder()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
ResidencyBudgetOptions budgets =
|
||||
ResidencyBudgetOptions.Default with
|
||||
{
|
||||
ObjectMeshGpuBytes = 321,
|
||||
CompositePhysicalBytes = 654,
|
||||
};
|
||||
var fixture = new Fixture(budgets: budgets);
|
||||
|
||||
WorldRenderResult result = fixture.Compose();
|
||||
|
||||
|
|
@ -30,6 +37,10 @@ public sealed class WorldRenderCompositionTests
|
|||
Assert.Same(fixture.Lifetime.Atlas, result.Foundation.TerrainAtlas);
|
||||
Assert.Equal(QualitySettings.From(QualityPreset.High).AnisotropicLevel,
|
||||
fixture.Factory.AnisotropicLevel);
|
||||
Assert.Same(budgets, result.Foundation.Residency.Budgets);
|
||||
Assert.Same(budgets, fixture.Factory.MeshBudgets);
|
||||
Assert.Same(budgets, fixture.Factory.TextureBudgets);
|
||||
Assert.Same(result.Foundation.Residency, fixture.Factory.RegisteredResidency);
|
||||
Assert.Equal(1, fixture.Lifetime.AcquireCalls);
|
||||
Assert.Empty(fixture.Factory.Releases);
|
||||
}
|
||||
|
|
@ -134,13 +145,16 @@ public sealed class WorldRenderCompositionTests
|
|||
private sealed class Fixture
|
||||
{
|
||||
private readonly WorldRenderCompositionPoint? _failurePoint;
|
||||
private readonly ResidencyBudgetOptions _budgets;
|
||||
|
||||
public Fixture(
|
||||
bool hasFont = true,
|
||||
WorldRenderCompositionPoint? failurePoint = null,
|
||||
string? publicationFailure = null)
|
||||
string? publicationFailure = null,
|
||||
ResidencyBudgetOptions? budgets = null)
|
||||
{
|
||||
_failurePoint = failurePoint;
|
||||
_budgets = budgets ?? ResidencyBudgetOptions.Default;
|
||||
Factory = new Factory(hasFont);
|
||||
Publication = new Publication(publicationFailure);
|
||||
Lifetime = new RenderLifetime(Factory.Atlas);
|
||||
|
|
@ -161,6 +175,7 @@ public sealed class WorldRenderCompositionTests
|
|||
new WorldEnvironmentController(),
|
||||
Lifetime,
|
||||
ImmediateGpuResourceRetirementQueue.Instance,
|
||||
_budgets,
|
||||
0xA9B4FFFFu,
|
||||
_ => { }),
|
||||
Publication,
|
||||
|
|
@ -199,6 +214,9 @@ public sealed class WorldRenderCompositionTests
|
|||
public TerrainAtlas Atlas { get; } = Stub<TerrainAtlas>();
|
||||
public int AnisotropicLevel { get; private set; }
|
||||
public List<string> Releases { get; } = [];
|
||||
public ResidencyBudgetOptions? MeshBudgets { get; private set; }
|
||||
public ResidencyBudgetOptions? TextureBudgets { get; private set; }
|
||||
public ResidencyManager? RegisteredResidency { get; private set; }
|
||||
|
||||
public void InitializeGlState(GL gl) { }
|
||||
|
||||
|
|
@ -266,15 +284,32 @@ public sealed class WorldRenderCompositionTests
|
|||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
IGpuResourceRetirementQueue retirement) =>
|
||||
Resource<WbMeshAdapter>("WB mesh adapter");
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
ResidencyBudgetOptions budgets)
|
||||
{
|
||||
MeshBudgets = budgets;
|
||||
return Resource<WbMeshAdapter>("WB mesh adapter");
|
||||
}
|
||||
|
||||
public TextureCache CreateTextureCache(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
BindlessSupport bindless,
|
||||
IGpuResourceRetirementQueue retirement) =>
|
||||
Resource<TextureCache>("texture cache");
|
||||
IGpuResourceRetirementQueue retirement,
|
||||
ResidencyBudgetOptions budgets)
|
||||
{
|
||||
TextureBudgets = budgets;
|
||||
return Resource<TextureCache>("texture cache");
|
||||
}
|
||||
|
||||
public void RegisterResidencySources(
|
||||
ResidencyManager manager,
|
||||
WbMeshAdapter meshes,
|
||||
TextureCache textures,
|
||||
IPreparedAssetSource preparedAssets)
|
||||
{
|
||||
RegisteredResidency = manager;
|
||||
}
|
||||
|
||||
public SamplerCache CreateSamplerCache(GL gl) =>
|
||||
Resource<SamplerCache>("sampler cache");
|
||||
|
|
|
|||
|
|
@ -278,9 +278,9 @@ public sealed class WorldLifecycleAutomationControllerTests
|
|||
|
||||
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0L, 0, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
|
||||
0, 0, 0L, 0, 0L, 0L, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
|
||||
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
|
||||
0d, 0d, null);
|
||||
default, 0d, 0d, null);
|
||||
|
||||
private static string NewDirectory()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,40 @@ namespace AcDream.App.Tests.Rendering;
|
|||
|
||||
public sealed class CompositeTextureArrayCachePolicyTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResidencySnapshotSeparatesResidentRequestedAndRetiringStorage()
|
||||
{
|
||||
var backend = new FakeBackend(maximumLayers: 1);
|
||||
var retirements = new DeferredRetirementQueue();
|
||||
using var cache = CreateCache(
|
||||
backend,
|
||||
retirements,
|
||||
unownedBudgetBytes: 0,
|
||||
physicalBudgetBytes: 0);
|
||||
cache.BeginFrame();
|
||||
Assert.True(cache.TryAddAndAcquire(
|
||||
1,
|
||||
Key(1),
|
||||
Texture(4, 4),
|
||||
out _));
|
||||
|
||||
var resident = cache.CaptureResidency();
|
||||
Assert.Equal(64, resident.Charges.GpuResidentBytes);
|
||||
Assert.Equal(0, resident.Charges.RetiringBytes);
|
||||
Assert.Equal(64, resident.CapacityBytes);
|
||||
Assert.Equal(64, resident.UsedBytes);
|
||||
|
||||
cache.ReleaseOwner(1);
|
||||
cache.Tick();
|
||||
retirements.DrainAll();
|
||||
cache.Tick();
|
||||
|
||||
var retired = cache.CaptureResidency();
|
||||
Assert.Equal(0, retired.Charges.GpuResidentBytes);
|
||||
Assert.Equal(0, retired.Charges.RetiringBytes);
|
||||
Assert.Equal(0, retired.CapacityBytes);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(16, 16, 64)]
|
||||
[InlineData(128, 128, 64)]
|
||||
|
|
|
|||
|
|
@ -343,6 +343,60 @@ public sealed class ResidencyManagerTests
|
|||
Assert.Equal(1, journal.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DomainSourcesAggregateWithoutCountingMappedAddressSpaceAsRam()
|
||||
{
|
||||
var manager = new ResidencyManager();
|
||||
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
|
||||
ResidencyDomain.PreparedPackage,
|
||||
() => new ResidencyDomainSnapshot(
|
||||
ResidencyDomain.PreparedPackage,
|
||||
EntryCount: 1,
|
||||
OwnerCount: 1,
|
||||
Charges: new ResidencyCharges(
|
||||
LogicalBytes: 64,
|
||||
PinnedBytes: 32,
|
||||
MappedVirtualBytes: 30_000))));
|
||||
manager.RegisterDomainSource(new DelegateResidencyDomainSource(
|
||||
ResidencyDomain.StandaloneTextures,
|
||||
() => new ResidencyDomainSnapshot(
|
||||
ResidencyDomain.StandaloneTextures,
|
||||
EntryCount: 2,
|
||||
OwnerCount: 1,
|
||||
Charges: new ResidencyCharges(
|
||||
GpuResidentBytes: 128),
|
||||
BudgetBytes: 256,
|
||||
CapacityBytes: 128,
|
||||
UsedBytes: 96,
|
||||
LargestFreeBytes: 16)));
|
||||
|
||||
ResidencySnapshot snapshot = manager.CaptureSnapshot();
|
||||
|
||||
Assert.Equal(64, snapshot.TotalCharges.CommittedCpuBytes);
|
||||
Assert.Equal(30_000, snapshot.TotalCharges.MappedVirtualBytes);
|
||||
Assert.Equal(128, snapshot.TotalCharges.PhysicalGpuBytes);
|
||||
Assert.Equal(
|
||||
16,
|
||||
snapshot.Get(ResidencyDomain.StandaloneTextures)
|
||||
.FragmentedFreeBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyOneCanonicalSourceMayOwnADomain()
|
||||
{
|
||||
var manager = new ResidencyManager();
|
||||
var first = new DelegateResidencyDomainSource(
|
||||
ResidencyDomain.ObjectMeshes,
|
||||
() => default);
|
||||
manager.RegisterDomainSource(first);
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
manager.RegisterDomainSource(
|
||||
new DelegateResidencyDomainSource(
|
||||
ResidencyDomain.ObjectMeshes,
|
||||
() => default)));
|
||||
}
|
||||
|
||||
private static AssetHandle<MeshAsset> Resident(
|
||||
ResidencyManager manager,
|
||||
ulong id,
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ public sealed class StandaloneBindlessTextureCacheTests
|
|||
cache.Tick();
|
||||
|
||||
Assert.Equal(2, cache.EntryCount);
|
||||
Assert.Equal(12, cache.AllocatedBytes);
|
||||
Assert.Equal(4, cache.RetiringBytes);
|
||||
Assert.Equal(2, cache.UnownedEntryCount);
|
||||
Assert.Single(retirements.Actions);
|
||||
Assert.Empty(backend.Events);
|
||||
|
|
@ -60,6 +62,8 @@ public sealed class StandaloneBindlessTextureCacheTests
|
|||
|
||||
retirements.Drain();
|
||||
Assert.Equal(["nonresident:1", "delete:1"], backend.Events);
|
||||
Assert.Equal(8, cache.AllocatedBytes);
|
||||
Assert.Equal(0, cache.RetiringBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -196,6 +200,32 @@ public sealed class StandaloneBindlessTextureCacheTests
|
|||
backend.Events);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DisposePreservesAcceptedFenceRetirementAccountingUntilCompletion()
|
||||
{
|
||||
var backend = new FakeBackend();
|
||||
var retirements = new DeferredRetirementQueue();
|
||||
var cache = CreateCache(
|
||||
backend,
|
||||
retirements,
|
||||
unownedBudgetBytes: 1_000,
|
||||
maximumUnownedCount: 1);
|
||||
AddAndRelease(cache, ownerId: 10, Resource(1, bytes: 4));
|
||||
AddAndRelease(cache, ownerId: 20, Resource(2, bytes: 4));
|
||||
cache.Tick();
|
||||
|
||||
Assert.Equal(8, cache.AllocatedBytes);
|
||||
Assert.Equal(4, cache.RetiringBytes);
|
||||
|
||||
cache.Dispose();
|
||||
|
||||
Assert.Equal(4, cache.AllocatedBytes);
|
||||
Assert.Equal(4, cache.RetiringBytes);
|
||||
retirements.Drain();
|
||||
Assert.Equal(0, cache.AllocatedBytes);
|
||||
Assert.Equal(0, cache.RetiringBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedResidencyReleaseNeverDeletesThatBackingTexture()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue