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
|
# 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`
|
**Program:** `docs/plans/2026-07-24-modern-runtime-architecture.md`
|
||||||
**Baseline:** Slice C closeout commit `a564c4b7`
|
**Baseline:** Slice C closeout commit `a564c4b7`
|
||||||
**Behavior contract:** no visual-quality or retail-behavior change
|
**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,
|
- Test stale completions, duplicate acquire, owner reuse, asset-slot reuse,
|
||||||
cancellation, corrupt/failure states, and illegal transitions.
|
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
|
### D2 — Existing-owner adapters
|
||||||
|
|
||||||
- Parameterize current mesh, staging, composite, and standalone budgets.
|
- 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
|
- Cross-check manager GPU totals with `GpuMemoryTracker`, allowing explicitly
|
||||||
enumerated non-cache resources.
|
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
|
### D3 — Missing policies
|
||||||
|
|
||||||
- Bound `RetailAnimationLoader` by count and estimated retained bytes.
|
- 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
|
6. The architecture, roadmap, issues, inventory, and durable memory agree with
|
||||||
the implementation.
|
the implementation.
|
||||||
7. Connected screenshots retain Slice C's visual output.
|
7. Connected screenshots retain Slice C's visual output.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -408,7 +408,8 @@ internal sealed class FrameRootCompositionPhase
|
||||||
foundation.TextureCache,
|
foundation.TextureCache,
|
||||||
live.DrawDispatcher,
|
live.DrawDispatcher,
|
||||||
d.FrameProfiler,
|
d.FrameProfiler,
|
||||||
content.Dats);
|
content.Dats,
|
||||||
|
foundation.Residency);
|
||||||
lifecycleAutomation =
|
lifecycleAutomation =
|
||||||
new WorldLifecycleAutomationController(
|
new WorldLifecycleAutomationController(
|
||||||
() => session.WorldReveal.Snapshot,
|
() => session.WorldReveal.Snapshot,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Rendering.Residency;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using AcDream.Core.Terrain;
|
using AcDream.Core.Terrain;
|
||||||
|
|
@ -37,7 +38,8 @@ internal sealed record WorldRenderFoundation(
|
||||||
Shader MeshShader,
|
Shader MeshShader,
|
||||||
WbMeshAdapter MeshAdapter,
|
WbMeshAdapter MeshAdapter,
|
||||||
TextureCache TextureCache,
|
TextureCache TextureCache,
|
||||||
SamplerCache Samplers);
|
SamplerCache Samplers,
|
||||||
|
ResidencyManager Residency);
|
||||||
|
|
||||||
internal sealed record WorldRenderResult(
|
internal sealed record WorldRenderResult(
|
||||||
WorldTerrainBuildContext TerrainBuild,
|
WorldTerrainBuildContext TerrainBuild,
|
||||||
|
|
@ -47,6 +49,7 @@ internal sealed record WorldRenderDependencies(
|
||||||
WorldEnvironmentController Environment,
|
WorldEnvironmentController Environment,
|
||||||
IGameRenderResourceLifetime RenderResources,
|
IGameRenderResourceLifetime RenderResources,
|
||||||
IGpuResourceRetirementQueue ResourceRetirement,
|
IGpuResourceRetirementQueue ResourceRetirement,
|
||||||
|
ResidencyBudgetOptions ResidencyBudgets,
|
||||||
uint InitialCenterLandblockId,
|
uint InitialCenterLandblockId,
|
||||||
Action<string> Log);
|
Action<string> Log);
|
||||||
|
|
||||||
|
|
@ -101,12 +104,19 @@ internal interface IWorldRenderCompositionFactory
|
||||||
GL gl,
|
GL gl,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
IPreparedAssetSource preparedAssets,
|
IPreparedAssetSource preparedAssets,
|
||||||
IGpuResourceRetirementQueue retirement);
|
IGpuResourceRetirementQueue retirement,
|
||||||
|
ResidencyBudgetOptions budgets);
|
||||||
TextureCache CreateTextureCache(
|
TextureCache CreateTextureCache(
|
||||||
GL gl,
|
GL gl,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
BindlessSupport bindless,
|
BindlessSupport bindless,
|
||||||
IGpuResourceRetirementQueue retirement);
|
IGpuResourceRetirementQueue retirement,
|
||||||
|
ResidencyBudgetOptions budgets);
|
||||||
|
void RegisterResidencySources(
|
||||||
|
ResidencyManager manager,
|
||||||
|
WbMeshAdapter meshes,
|
||||||
|
TextureCache textures,
|
||||||
|
IPreparedAssetSource preparedAssets);
|
||||||
SamplerCache CreateSamplerCache(GL gl);
|
SamplerCache CreateSamplerCache(GL gl);
|
||||||
void Release(IDisposable resource);
|
void Release(IDisposable resource);
|
||||||
}
|
}
|
||||||
|
|
@ -263,20 +273,42 @@ internal sealed class RetailWorldRenderCompositionFactory
|
||||||
GL gl,
|
GL gl,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
IPreparedAssetSource preparedAssets,
|
IPreparedAssetSource preparedAssets,
|
||||||
IGpuResourceRetirementQueue retirement) =>
|
IGpuResourceRetirementQueue retirement,
|
||||||
|
ResidencyBudgetOptions budgets) =>
|
||||||
new(
|
new(
|
||||||
gl,
|
gl,
|
||||||
dats,
|
dats,
|
||||||
preparedAssets,
|
preparedAssets,
|
||||||
NullLogger<WbMeshAdapter>.Instance,
|
NullLogger<WbMeshAdapter>.Instance,
|
||||||
retirement);
|
retirement,
|
||||||
|
budgets);
|
||||||
|
|
||||||
public TextureCache CreateTextureCache(
|
public TextureCache CreateTextureCache(
|
||||||
GL gl,
|
GL gl,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
BindlessSupport bindless,
|
BindlessSupport bindless,
|
||||||
IGpuResourceRetirementQueue retirement) =>
|
IGpuResourceRetirementQueue retirement,
|
||||||
new(gl, dats, bindless, 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);
|
public SamplerCache CreateSamplerCache(GL gl) => new(gl);
|
||||||
|
|
||||||
|
|
@ -349,6 +381,8 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
GL gl = platform.Graphics;
|
GL gl = platform.Graphics;
|
||||||
|
var residency = new ResidencyManager(
|
||||||
|
_dependencies.ResidencyBudgets);
|
||||||
_factory.InitializeGlState(gl);
|
_factory.InitializeGlState(gl);
|
||||||
Fault(WorldRenderCompositionPoint.GlStateInitialized);
|
Fault(WorldRenderCompositionPoint.GlStateInitialized);
|
||||||
|
|
||||||
|
|
@ -434,7 +468,8 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
gl,
|
gl,
|
||||||
content.Dats,
|
content.Dats,
|
||||||
content.PreparedAssets,
|
content.PreparedAssets,
|
||||||
_dependencies.ResourceRetirement),
|
_dependencies.ResourceRetirement,
|
||||||
|
residency.Budgets),
|
||||||
_publication.PublishWbMeshAdapter,
|
_publication.PublishWbMeshAdapter,
|
||||||
WorldRenderCompositionPoint.MeshAdapterPublished);
|
WorldRenderCompositionPoint.MeshAdapterPublished);
|
||||||
TextureCache textureCache = AcquireAndPublish(
|
TextureCache textureCache = AcquireAndPublish(
|
||||||
|
|
@ -444,7 +479,8 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
gl,
|
gl,
|
||||||
content.Dats,
|
content.Dats,
|
||||||
bindless,
|
bindless,
|
||||||
_dependencies.ResourceRetirement),
|
_dependencies.ResourceRetirement,
|
||||||
|
residency.Budgets),
|
||||||
_publication.PublishTextureCache,
|
_publication.PublishTextureCache,
|
||||||
WorldRenderCompositionPoint.TextureCachePublished);
|
WorldRenderCompositionPoint.TextureCachePublished);
|
||||||
SamplerCache samplers = AcquireAndPublish(
|
SamplerCache samplers = AcquireAndPublish(
|
||||||
|
|
@ -453,6 +489,11 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
() => _factory.CreateSamplerCache(gl),
|
() => _factory.CreateSamplerCache(gl),
|
||||||
_publication.PublishSamplerCache,
|
_publication.PublishSamplerCache,
|
||||||
WorldRenderCompositionPoint.SamplerCachePublished);
|
WorldRenderCompositionPoint.SamplerCachePublished);
|
||||||
|
_factory.RegisterResidencySources(
|
||||||
|
residency,
|
||||||
|
meshAdapter,
|
||||||
|
textureCache,
|
||||||
|
content.PreparedAssets);
|
||||||
|
|
||||||
scope.Complete();
|
scope.Complete();
|
||||||
_dependencies.Log(
|
_dependencies.Log(
|
||||||
|
|
@ -473,7 +514,8 @@ internal sealed class WorldRenderCompositionPhase
|
||||||
meshShader,
|
meshShader,
|
||||||
meshAdapter,
|
meshAdapter,
|
||||||
textureCache,
|
textureCache,
|
||||||
samplers));
|
samplers,
|
||||||
|
residency));
|
||||||
}
|
}
|
||||||
catch (Exception failure)
|
catch (Exception failure)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Residency;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.UI.Testing;
|
using AcDream.App.UI.Testing;
|
||||||
|
|
||||||
|
|
@ -53,6 +54,8 @@ internal sealed record WorldLifecycleResourceSnapshot(
|
||||||
int StagedMeshUploads,
|
int StagedMeshUploads,
|
||||||
long StagedMeshBytes,
|
long StagedMeshBytes,
|
||||||
long TrackedGpuBytes,
|
long TrackedGpuBytes,
|
||||||
|
long ResidencyGpuBytes,
|
||||||
|
long GpuTrackerMinusResidencyBytes,
|
||||||
int TrackedGpuBuffers,
|
int TrackedGpuBuffers,
|
||||||
int TrackedGpuTextures,
|
int TrackedGpuTextures,
|
||||||
int OwnedCompositeTextures,
|
int OwnedCompositeTextures,
|
||||||
|
|
@ -74,6 +77,7 @@ internal sealed record WorldLifecycleResourceSnapshot(
|
||||||
long DatObjectCacheHits,
|
long DatObjectCacheHits,
|
||||||
long DatObjectCacheMisses,
|
long DatObjectCacheMisses,
|
||||||
long DatObjectCacheEvictions,
|
long DatObjectCacheEvictions,
|
||||||
|
ResidencySnapshot Residency,
|
||||||
double Fps,
|
double Fps,
|
||||||
double FrameMilliseconds,
|
double FrameMilliseconds,
|
||||||
string? LastFrameProfile);
|
string? LastFrameProfile);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Rendering.Residency;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
|
@ -36,6 +37,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
private readonly WbDrawDispatcher _dispatcher;
|
private readonly WbDrawDispatcher _dispatcher;
|
||||||
private readonly FrameProfiler _frameProfiler;
|
private readonly FrameProfiler _frameProfiler;
|
||||||
private readonly IDatReaderWriter _dats;
|
private readonly IDatReaderWriter _dats;
|
||||||
|
private readonly ResidencyManager _residency;
|
||||||
|
|
||||||
public WorldLifecycleResourceSnapshotSource(
|
public WorldLifecycleResourceSnapshotSource(
|
||||||
GpuWorldState world,
|
GpuWorldState world,
|
||||||
|
|
@ -52,7 +54,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
TextureCache textures,
|
TextureCache textures,
|
||||||
WbDrawDispatcher dispatcher,
|
WbDrawDispatcher dispatcher,
|
||||||
FrameProfiler frameProfiler,
|
FrameProfiler frameProfiler,
|
||||||
IDatReaderWriter dats)
|
IDatReaderWriter dats,
|
||||||
|
ResidencyManager residency)
|
||||||
{
|
{
|
||||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||||
_animations = animations
|
_animations = animations
|
||||||
|
|
@ -77,6 +80,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
_frameProfiler = frameProfiler
|
_frameProfiler = frameProfiler
|
||||||
?? throw new ArgumentNullException(nameof(frameProfiler));
|
?? throw new ArgumentNullException(nameof(frameProfiler));
|
||||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||||
|
_residency = residency
|
||||||
|
?? throw new ArgumentNullException(nameof(residency));
|
||||||
}
|
}
|
||||||
|
|
||||||
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
|
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
|
||||||
|
|
@ -105,6 +110,9 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
: default;
|
: default;
|
||||||
CacheStats cpuMeshCacheStats = meshManager.CpuMeshCacheStats;
|
CacheStats cpuMeshCacheStats = meshManager.CpuMeshCacheStats;
|
||||||
CacheStats decodedTextureCacheStats = meshManager.DecodedTextureCacheStats;
|
CacheStats decodedTextureCacheStats = meshManager.DecodedTextureCacheStats;
|
||||||
|
ResidencySnapshot residency = _residency.CaptureSnapshot();
|
||||||
|
long trackedGpuBytes = GpuMemoryTracker.AllocatedBytes;
|
||||||
|
long residencyGpuBytes = residency.TotalCharges.PhysicalGpuBytes;
|
||||||
|
|
||||||
return new WorldLifecycleResourceSnapshot(
|
return new WorldLifecycleResourceSnapshot(
|
||||||
LoadedLandblocks: _world.LoadedLandblockIds.Count,
|
LoadedLandblocks: _world.LoadedLandblockIds.Count,
|
||||||
|
|
@ -129,7 +137,10 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
MeshEstimatedBytes: mesh.EstimatedBytes,
|
MeshEstimatedBytes: mesh.EstimatedBytes,
|
||||||
StagedMeshUploads: _meshes.StagedUploadBacklog,
|
StagedMeshUploads: _meshes.StagedUploadBacklog,
|
||||||
StagedMeshBytes: _meshes.StagedUploadBytes,
|
StagedMeshBytes: _meshes.StagedUploadBytes,
|
||||||
TrackedGpuBytes: GpuMemoryTracker.AllocatedBytes,
|
TrackedGpuBytes: trackedGpuBytes,
|
||||||
|
ResidencyGpuBytes: residencyGpuBytes,
|
||||||
|
GpuTrackerMinusResidencyBytes: checked(
|
||||||
|
trackedGpuBytes - residencyGpuBytes),
|
||||||
TrackedGpuBuffers: GpuMemoryTracker.BufferCount,
|
TrackedGpuBuffers: GpuMemoryTracker.BufferCount,
|
||||||
TrackedGpuTextures: GpuMemoryTracker.TextureCount,
|
TrackedGpuTextures: GpuMemoryTracker.TextureCount,
|
||||||
OwnedCompositeTextures: _textures.OwnedBindlessTextureCount,
|
OwnedCompositeTextures: _textures.OwnedBindlessTextureCount,
|
||||||
|
|
@ -152,6 +163,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
DatObjectCacheHits: datObjectCacheStats.Hits,
|
DatObjectCacheHits: datObjectCacheStats.Hits,
|
||||||
DatObjectCacheMisses: datObjectCacheStats.Misses,
|
DatObjectCacheMisses: datObjectCacheStats.Misses,
|
||||||
DatObjectCacheEvictions: datObjectCacheStats.Evictions,
|
DatObjectCacheEvictions: datObjectCacheStats.Evictions,
|
||||||
|
Residency: residency,
|
||||||
Fps: render.Fps,
|
Fps: render.Fps,
|
||||||
FrameMilliseconds: render.FrameMilliseconds,
|
FrameMilliseconds: render.FrameMilliseconds,
|
||||||
LastFrameProfile: _frameProfiler.LastReport);
|
LastFrameProfile: _frameProfiler.LastReport);
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,8 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
||||||
internal long UnownedBytes => _unowned.ResidentBytes;
|
internal long UnownedBytes => _unowned.ResidentBytes;
|
||||||
internal int AtlasCount => _atlases.Count;
|
internal int AtlasCount => _atlases.Count;
|
||||||
internal long AllocatedBytes => _allocatedBytes;
|
internal long AllocatedBytes => _allocatedBytes;
|
||||||
|
internal long PhysicalBudgetBytes => _physicalBudgetBytes;
|
||||||
|
internal long UnownedBudgetBytes => _unowned.BudgetBytes;
|
||||||
internal int FrameUploadCount => _frameUploadCount;
|
internal int FrameUploadCount => _frameUploadCount;
|
||||||
internal long FrameUploadBytes => _frameUploadBytes;
|
internal long FrameUploadBytes => _frameUploadBytes;
|
||||||
internal bool CanStartUpload =>
|
internal bool CanStartUpload =>
|
||||||
|
|
@ -391,6 +393,49 @@ internal sealed class CompositeTextureArrayCache : IDisposable
|
||||||
&& _frameUploadCount < _maximumUploadsPerFrame
|
&& _frameUploadCount < _maximumUploadsPerFrame
|
||||||
&& (_frameUploadCount == 0 || _frameUploadBytes < _maximumUploadBytesPerFrame);
|
&& (_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)
|
internal bool CanUpload(long bytes)
|
||||||
{
|
{
|
||||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bytes);
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bytes);
|
||||||
|
|
|
||||||
|
|
@ -1221,6 +1221,7 @@ public sealed class GameWindow :
|
||||||
_worldEnvironment,
|
_worldEnvironment,
|
||||||
_renderResourceLifetime,
|
_renderResourceLifetime,
|
||||||
_gpuFrameFlights!,
|
_gpuFrameFlights!,
|
||||||
|
_options.ResidencyBudgets,
|
||||||
initialCenterLandblockId,
|
initialCenterLandblockId,
|
||||||
Console.WriteLine),
|
Console.WriteLine),
|
||||||
this).Compose(platformResult, contentEffectsAudio, settingsDevTools);
|
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<ResidencyObservation> _observationScratch = [];
|
||||||
private readonly List<ResidencyDomainSnapshot> _snapshotScratch = [];
|
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 ActiveAssetCount => _assetByKey.Count;
|
||||||
|
|
||||||
public int ActiveOwnerCount => _owners.Count(owner => owner is not null);
|
public int ActiveOwnerCount => _owners.Count(owner => owner is not null);
|
||||||
|
|
@ -355,19 +360,46 @@ internal sealed class ResidencyManager
|
||||||
public ResidencySnapshot CaptureSnapshot()
|
public ResidencySnapshot CaptureSnapshot()
|
||||||
{
|
{
|
||||||
_snapshotScratch.Clear();
|
_snapshotScratch.Clear();
|
||||||
for (int i = 0; i < _domainSources.Count; i++)
|
Span<bool> sourced = stackalloc bool[
|
||||||
_snapshotScratch.Add(_domainSources[i].CaptureResidency());
|
Enum.GetValues<ResidencyDomain>().Length];
|
||||||
|
|
||||||
ResidencyCharges totals = default;
|
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++)
|
for (uint index = 1; index < _assets.Count; index++)
|
||||||
{
|
{
|
||||||
AssetSlot? slot = _assets[checked((int)index)];
|
AssetSlot? slot = _assets[checked((int)index)];
|
||||||
if (slot is not null)
|
if (slot is null || sourced[(int)slot.Key.Domain])
|
||||||
totals += slot.Charges;
|
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++)
|
for (int domain = 0; domain < logicalEntries.Length; domain++)
|
||||||
totals += _snapshotScratch[i].Charges;
|
{
|
||||||
|
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);
|
return new ResidencySnapshot(_snapshotScratch.ToArray(), totals);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ internal sealed class StandaloneBindlessTextureCache : IDisposable
|
||||||
private readonly Dictionary<uint, StandaloneBindlessTextureResource> _entries = new();
|
private readonly Dictionary<uint, StandaloneBindlessTextureResource> _entries = new();
|
||||||
private readonly HashSet<uint> _disposeResidencyReleased = [];
|
private readonly HashSet<uint> _disposeResidencyReleased = [];
|
||||||
private readonly HashSet<uint> _disposeDeleted = [];
|
private readonly HashSet<uint> _disposeDeleted = [];
|
||||||
|
private long _allocatedBytes;
|
||||||
|
private long _retiringBytes;
|
||||||
private bool _disposeRequested;
|
private bool _disposeRequested;
|
||||||
private bool _disposing;
|
private bool _disposing;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
@ -62,6 +64,9 @@ internal sealed class StandaloneBindlessTextureCache : IDisposable
|
||||||
internal int OwnerCount => _owners.OwnerCount;
|
internal int OwnerCount => _owners.OwnerCount;
|
||||||
internal int UnownedEntryCount => _unowned.Count;
|
internal int UnownedEntryCount => _unowned.Count;
|
||||||
internal long UnownedBytes => _unowned.ResidentBytes;
|
internal long UnownedBytes => _unowned.ResidentBytes;
|
||||||
|
internal long AllocatedBytes => _allocatedBytes;
|
||||||
|
internal long RetiringBytes => _retiringBytes;
|
||||||
|
internal long BudgetBytes => _unowned.BudgetBytes;
|
||||||
internal int AwaitingRetirementPublicationCount =>
|
internal int AwaitingRetirementPublicationCount =>
|
||||||
_retirementLedger.AwaitingPublicationCount;
|
_retirementLedger.AwaitingPublicationCount;
|
||||||
|
|
||||||
|
|
@ -96,6 +101,7 @@ internal sealed class StandaloneBindlessTextureCache : IDisposable
|
||||||
$"Standalone particle surface 0x{resource.SurfaceId:X8} is already cached.");
|
$"Standalone particle surface 0x{resource.SurfaceId:X8} is already cached.");
|
||||||
|
|
||||||
_owners.Acquire(ownerId, resource.SurfaceId);
|
_owners.Acquire(ownerId, resource.SurfaceId);
|
||||||
|
_allocatedBytes = checked(_allocatedBytes + resource.Bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReleaseOwner(uint ownerId)
|
public void ReleaseOwner(uint ownerId)
|
||||||
|
|
@ -143,9 +149,14 @@ internal sealed class StandaloneBindlessTextureCache : IDisposable
|
||||||
// The publication ledger owns this release from this point even
|
// The publication ledger owns this release from this point even
|
||||||
// when queue admission or an immediate callback throws. Never
|
// when queue admission or an immediate callback throws. Never
|
||||||
// republish a texture after residency release has started.
|
// republish a texture after residency release has started.
|
||||||
|
_retiringBytes = checked(_retiringBytes + resource.Bytes);
|
||||||
_retirementLedger.Retire(new RetryableGpuResourceRelease(
|
_retirementLedger.Retire(new RetryableGpuResourceRelease(
|
||||||
() => _backend.MakeNonResident(resource),
|
() => _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);
|
_backend.Delete(resource);
|
||||||
_disposeDeleted.Add(resource.SurfaceId);
|
_disposeDeleted.Add(resource.SurfaceId);
|
||||||
|
_allocatedBytes = checked(
|
||||||
|
_allocatedBytes - resource.Bytes);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using Silk.NET.OpenGL;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using PixelFormatId = DatReaderWriter.Enums.PixelFormat;
|
using PixelFormatId = DatReaderWriter.Enums.PixelFormat;
|
||||||
using SurfaceType = DatReaderWriter.Enums.SurfaceType;
|
using SurfaceType = DatReaderWriter.Enums.SurfaceType;
|
||||||
|
using AcDream.App.Rendering.Residency;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
|
|
@ -87,8 +88,10 @@ public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable
|
||||||
GL gl,
|
GL gl,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
Wb.BindlessSupport? bindless,
|
Wb.BindlessSupport? bindless,
|
||||||
IGpuResourceRetirementQueue retirementQueue)
|
IGpuResourceRetirementQueue retirementQueue,
|
||||||
|
ResidencyBudgetOptions? budgets = null)
|
||||||
{
|
{
|
||||||
|
budgets ??= ResidencyBudgetOptions.Default;
|
||||||
_gl = gl;
|
_gl = gl;
|
||||||
_dats = dats;
|
_dats = dats;
|
||||||
_bindless = bindless;
|
_bindless = bindless;
|
||||||
|
|
@ -103,11 +106,15 @@ public sealed unsafe class TextureCache : Wb.IEntityTextureLifetime, IDisposable
|
||||||
composite = new CompositeTextureArrayCache(
|
composite = new CompositeTextureArrayCache(
|
||||||
gl,
|
gl,
|
||||||
bindless,
|
bindless,
|
||||||
retirementQueue);
|
retirementQueue,
|
||||||
|
budgets.CompositeUnownedBytes,
|
||||||
|
budgets.CompositePhysicalBytes);
|
||||||
resources.Add("composite texture cache", composite.Dispose);
|
resources.Add("composite texture cache", composite.Dispose);
|
||||||
particles = new StandaloneBindlessTextureCache(
|
particles = new StandaloneBindlessTextureCache(
|
||||||
new ParticleTextureBackend(this),
|
new ParticleTextureBackend(this),
|
||||||
retirementQueue);
|
retirementQueue,
|
||||||
|
budgets.StandaloneUnownedBytes,
|
||||||
|
budgets.StandaloneUnownedEntries);
|
||||||
resources.Add("particle texture cache", particles.Dispose);
|
resources.Add("particle texture cache", particles.Dispose);
|
||||||
resources.TransferAll();
|
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>
|
/// <summary>
|
||||||
/// Get or upload the GL texture handle for a Surface id. Returns a
|
/// Get or upload the GL texture handle for a Surface id. Returns a
|
||||||
/// 1x1 magenta fallback if the Surface or its RenderSurface chain is
|
/// 1x1 magenta fallback if the Surface or its RenderSurface chain is
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,19 @@ public sealed class GlobalMeshBuffer : IDisposable
|
||||||
|| _retiredCapacityBytes != 0;
|
|| _retiredCapacityBytes != 0;
|
||||||
internal int VertexHighWaterMark => _vertices.HighWaterMark;
|
internal int VertexHighWaterMark => _vertices.HighWaterMark;
|
||||||
internal int IndexHighWaterMark => _indices.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)
|
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() {
|
public void Unbind() {
|
||||||
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
GL.BindTexture(GLEnum.Texture2DArray, 0);
|
||||||
GLHelpers.CheckErrors(GL);
|
GLHelpers.CheckErrors(GL);
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,8 @@ internal sealed class MeshUploadStagingQueue
|
||||||
public int ClaimCount { get { lock (_gate) return _generationById.Count; } }
|
public int ClaimCount { get { lock (_gate) return _generationById.Count; } }
|
||||||
public long QueuedBytes { get { lock (_gate) return _queuedBytes; } }
|
public long QueuedBytes { get { lock (_gate) return _queuedBytes; } }
|
||||||
public long ClaimedBytes { get { lock (_gate) return _claimedBytes; } }
|
public long ClaimedBytes { get { lock (_gate) return _claimedBytes; } }
|
||||||
|
public int MaximumCount => _maximumCount;
|
||||||
|
public long MaximumBytes => _maximumBytes;
|
||||||
public bool IsAtHighWater
|
public bool IsAtHighWater
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -249,6 +251,11 @@ internal sealed class MeshOwnershipCounter
|
||||||
|
|
||||||
public bool IsOwned(ulong id) => Count(id) > 0;
|
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>
|
/// <summary>
|
||||||
/// Reports whether freshly uploaded data has a live owner. This is a query,
|
/// Reports whether freshly uploaded data has a live owner. This is a query,
|
||||||
/// not an acquire: upload existence and logical ownership are independent.
|
/// 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 int Count { get { lock (_data) return _data.Count; } }
|
||||||
internal long ResidentBytes { get { lock (_data) return _residentBytes; } }
|
internal long ResidentBytes { get { lock (_data) return _residentBytes; } }
|
||||||
|
internal int Capacity => _capacity;
|
||||||
|
internal long ByteCapacity => _byteCapacity;
|
||||||
|
|
||||||
public bool TryGetAndStage(
|
public bool TryGetAndStage(
|
||||||
ulong id,
|
ulong id,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
using AcDream.App.Rendering.Residency;
|
||||||
using AcDream.Core.Rendering.Wb;
|
using AcDream.Core.Rendering.Wb;
|
||||||
using PixelFormat = Silk.NET.OpenGL.PixelFormat;
|
using PixelFormat = Silk.NET.OpenGL.PixelFormat;
|
||||||
using BoundingBox = Chorizite.Core.Lib.BoundingBox;
|
using BoundingBox = Chorizite.Core.Lib.BoundingBox;
|
||||||
|
|
@ -143,8 +144,8 @@ namespace AcDream.App.Rendering.Wb
|
||||||
|
|
||||||
// LRU Cache for Unused objects
|
// LRU Cache for Unused objects
|
||||||
private readonly LinkedList<ulong> _lruList = new();
|
private readonly LinkedList<ulong> _lruList = new();
|
||||||
private readonly long _maxGpuMemory = 1024 * 1024 * 1024; // 1GB
|
private readonly long _maxGpuMemory;
|
||||||
private readonly int _maxCachedObjects = 50; // Max number of cached objects (count-based limit)
|
private readonly int _maxCachedObjects;
|
||||||
private long _currentNonArenaGpuMemory;
|
private long _currentNonArenaGpuMemory;
|
||||||
|
|
||||||
// Shared atlases grouped by (Width, Height, Format)
|
// Shared atlases grouped by (Width, Height, Format)
|
||||||
|
|
@ -158,12 +159,15 @@ namespace AcDream.App.Rendering.Wb
|
||||||
private const int RetainedEmptyAtlasCountLimit = 32;
|
private const int RetainedEmptyAtlasCountLimit = 32;
|
||||||
private readonly AcDream.App.Rendering.BoundedUnownedResourceCache<TextureAtlasManager>
|
private readonly AcDream.App.Rendering.BoundedUnownedResourceCache<TextureAtlasManager>
|
||||||
_safeEmptyAtlases = new(RetainedEmptyAtlasBudgetBytes, RetainedEmptyAtlasCountLimit);
|
_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)
|
// 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 CpuMeshUploadCache _cpuMeshCache;
|
||||||
|
|
||||||
private readonly MeshUploadStagingQueue _stagedMeshData = new();
|
private readonly MeshUploadStagingQueue _stagedMeshData;
|
||||||
private volatile bool _arenaBackpressured;
|
private volatile bool _arenaBackpressured;
|
||||||
|
|
||||||
/// <summary>#125: how many times a failed GL upload is re-staged before
|
/// <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;
|
int atlasArrays = 0;
|
||||||
foreach (List<TextureAtlasManager> atlases in _globalAtlases.Values)
|
foreach (List<TextureAtlasManager> atlases in _globalAtlases.Values)
|
||||||
atlasArrays += atlases.Count;
|
atlasArrays += atlases.Count;
|
||||||
|
long atlasBytes = CalculateAtlasBytes(_globalAtlases.Values);
|
||||||
|
long retiringAtlasBytes = CalculateAtlasBytes(_retiringAtlases);
|
||||||
long physicalBytes = CalculateTrackedGpuBytes(
|
long physicalBytes = CalculateTrackedGpuBytes(
|
||||||
_currentNonArenaGpuMemory,
|
checked(
|
||||||
|
_currentNonArenaGpuMemory
|
||||||
|
+ atlasBytes
|
||||||
|
+ retiringAtlasBytes),
|
||||||
GlobalBuffer?.PhysicalCapacityBytes ?? 0);
|
GlobalBuffer?.PhysicalCapacityBytes ?? 0);
|
||||||
return (_renderData.Count, atlasArrays, _lruList.Count, physicalBytes);
|
return (_renderData.Count, atlasArrays, _lruList.Count, physicalBytes);
|
||||||
}
|
}
|
||||||
|
|
@ -400,15 +409,24 @@ namespace AcDream.App.Rendering.Wb
|
||||||
public ObjectMeshManager(
|
public ObjectMeshManager(
|
||||||
OpenGLGraphicsDevice graphicsDevice,
|
OpenGLGraphicsDevice graphicsDevice,
|
||||||
IPreparedAssetSource preparedAssets,
|
IPreparedAssetSource preparedAssets,
|
||||||
ILogger<ObjectMeshManager> logger)
|
ILogger<ObjectMeshManager> logger,
|
||||||
|
ResidencyBudgetOptions? budgets = null)
|
||||||
{
|
{
|
||||||
|
budgets ??= ResidencyBudgetOptions.Default;
|
||||||
_graphicsDevice = graphicsDevice
|
_graphicsDevice = graphicsDevice
|
||||||
?? throw new ArgumentNullException(nameof(graphicsDevice));
|
?? throw new ArgumentNullException(nameof(graphicsDevice));
|
||||||
_preparedAssets = preparedAssets
|
_preparedAssets = preparedAssets
|
||||||
?? throw new ArgumentNullException(nameof(preparedAssets));
|
?? throw new ArgumentNullException(nameof(preparedAssets));
|
||||||
_logger = logger
|
_logger = logger
|
||||||
?? throw new ArgumentNullException(nameof(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;
|
_useModernRendering = _graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless;
|
||||||
if (_useModernRendering)
|
if (_useModernRendering)
|
||||||
{
|
{
|
||||||
|
|
@ -506,6 +524,7 @@ namespace AcDream.App.Rendering.Wb
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal bool EvictOneEmptyAtlas()
|
internal bool EvictOneEmptyAtlas()
|
||||||
{
|
{
|
||||||
|
RetryRetiringAtlasDisposals();
|
||||||
if (!_safeEmptyAtlases.TryTakeOldestOverBudget(out TextureAtlasManager victim))
|
if (!_safeEmptyAtlases.TryTakeOldestOverBudget(out TextureAtlasManager victim))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
@ -517,10 +536,28 @@ namespace AcDream.App.Rendering.Wb
|
||||||
_globalAtlases.Remove(key);
|
_globalAtlases.Remove(key);
|
||||||
}
|
}
|
||||||
_dirtyAtlases.Remove(victim);
|
_dirtyAtlases.Remove(victim);
|
||||||
|
_retiringAtlases.Add(victim);
|
||||||
victim.Dispose();
|
victim.Dispose();
|
||||||
|
RemoveCompletedAtlasRetirements();
|
||||||
return true;
|
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)
|
private void OnAtlasGpuSafeEmpty(TextureAtlasManager atlas)
|
||||||
{
|
{
|
||||||
if (IsDisposed || !atlas.IsGpuSafeEmpty || _safeEmptyAtlases.Contains(atlas))
|
if (IsDisposed || !atlas.IsGpuSafeEmpty || _safeEmptyAtlases.Contains(atlas))
|
||||||
|
|
@ -637,6 +674,7 @@ namespace AcDream.App.Rendering.Wb
|
||||||
{
|
{
|
||||||
ArgumentOutOfRangeException.ThrowIfLessThan(maximumCount, 1);
|
ArgumentOutOfRangeException.ThrowIfLessThan(maximumCount, 1);
|
||||||
ArgumentOutOfRangeException.ThrowIfLessThan(maximumBytes, 1);
|
ArgumentOutOfRangeException.ThrowIfLessThan(maximumBytes, 1);
|
||||||
|
RetryRetiringAtlasDisposals();
|
||||||
RetryPendingAtlasRetirements();
|
RetryPendingAtlasRetirements();
|
||||||
// Rollbacks own unpublished resources and therefore have no LRU
|
// Rollbacks own unpublished resources and therefore have no LRU
|
||||||
// node to wake them. Advance a bounded snapshot every render tick
|
// node to wake them. Advance a bounded snapshot every render tick
|
||||||
|
|
@ -649,6 +687,9 @@ namespace AcDream.App.Rendering.Wb
|
||||||
lock (_lruList)
|
lock (_lruList)
|
||||||
candidateBudget = _lruList.Count;
|
candidateBudget = _lruList.Count;
|
||||||
int attemptedCandidates = 0;
|
int attemptedCandidates = 0;
|
||||||
|
long trackedAtlasBytes = checked(
|
||||||
|
CalculateAtlasBytes(_globalAtlases.Values)
|
||||||
|
+ CalculateAtlasBytes(_retiringAtlases));
|
||||||
|
|
||||||
while (reclaimedCount < maximumCount
|
while (reclaimedCount < maximumCount
|
||||||
&& attemptedCandidates < candidateBudget)
|
&& attemptedCandidates < candidateBudget)
|
||||||
|
|
@ -657,9 +698,12 @@ namespace AcDream.App.Rendering.Wb
|
||||||
lock (_lruList)
|
lock (_lruList)
|
||||||
{
|
{
|
||||||
long physicalArenaBytes = GlobalBuffer?.PhysicalCapacityBytes ?? 0;
|
long physicalArenaBytes = GlobalBuffer?.PhysicalCapacityBytes ?? 0;
|
||||||
|
long nonArenaAndAtlasBytes = checked(
|
||||||
|
_currentNonArenaGpuMemory
|
||||||
|
+ trackedAtlasBytes);
|
||||||
if (!forceArenaReclamation
|
if (!forceArenaReclamation
|
||||||
&& IsWithinGpuCacheBudget(
|
&& IsWithinGpuCacheBudget(
|
||||||
_currentNonArenaGpuMemory,
|
nonArenaAndAtlasBytes,
|
||||||
physicalArenaBytes,
|
physicalArenaBytes,
|
||||||
_maxGpuMemory)
|
_maxGpuMemory)
|
||||||
&& _lruList.Count <= _maxCachedObjects)
|
&& _lruList.Count <= _maxCachedObjects)
|
||||||
|
|
@ -1207,6 +1251,105 @@ namespace AcDream.App.Rendering.Wb
|
||||||
return _preparedAssets.Read(request, ct).Data;
|
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>
|
/// <summary>
|
||||||
/// Cancel preparation tasks for IDs that are no longer needed.
|
/// Cancel preparation tasks for IDs that are no longer needed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -2545,6 +2688,7 @@ namespace AcDream.App.Rendering.Wb
|
||||||
// Every layer release has now committed logically. Publish any
|
// Every layer release has now committed logically. Publish any
|
||||||
// callback which previously failed before queue acceptance.
|
// callback which previously failed before queue acceptance.
|
||||||
Capture(ref failures, RetryPendingAtlasRetirements);
|
Capture(ref failures, RetryPendingAtlasRetirements);
|
||||||
|
Capture(ref failures, RetryRetiringAtlasDisposals);
|
||||||
|
|
||||||
bool atlasFailure = false;
|
bool atlasFailure = false;
|
||||||
foreach (List<TextureAtlasManager> atlasList in _globalAtlases.Values)
|
foreach (List<TextureAtlasManager> atlasList in _globalAtlases.Values)
|
||||||
|
|
@ -2577,6 +2721,7 @@ namespace AcDream.App.Rendering.Wb
|
||||||
_uploadRollbacks.Clear();
|
_uploadRollbacks.Clear();
|
||||||
_uploadRollbackQueue.Clear();
|
_uploadRollbackQueue.Clear();
|
||||||
_globalAtlases.Clear();
|
_globalAtlases.Clear();
|
||||||
|
_retiringAtlases.Clear();
|
||||||
_dirtyAtlases.Clear();
|
_dirtyAtlases.Clear();
|
||||||
_safeEmptyAtlases.Clear();
|
_safeEmptyAtlases.Clear();
|
||||||
_currentNonArenaGpuMemory = 0;
|
_currentNonArenaGpuMemory = 0;
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ namespace AcDream.App.Rendering.Wb {
|
||||||
return TextureArray.TotalSizeInBytes;
|
return TextureArray.TotalSizeInBytes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
internal bool IsPhysicalRetirementComplete =>
|
||||||
|
TextureArray.IsPhysicalRetirementComplete;
|
||||||
internal long LastUseSequence { get; set; }
|
internal long LastUseSequence { get; set; }
|
||||||
internal int Width => _textureWidth;
|
internal int Width => _textureWidth;
|
||||||
internal int Height => _textureHeight;
|
internal int Height => _textureHeight;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ internal sealed class TextureAtlasSlotAllocator
|
||||||
}
|
}
|
||||||
|
|
||||||
public int AvailableCount => _returned.Count + (_rented.Length - _next);
|
public int AvailableCount => _returned.Count + (_rented.Length - _next);
|
||||||
|
public int Capacity => _rented.Length;
|
||||||
|
|
||||||
public int Rent()
|
public int Rent()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
using AcDream.App.Rendering.Residency;
|
||||||
using AcDream.Core.Rendering;
|
using AcDream.Core.Rendering;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -108,7 +109,8 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
preparedAssets: null,
|
preparedAssets: null,
|
||||||
logger,
|
logger,
|
||||||
AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance,
|
AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance,
|
||||||
ownsPreparedAssets: true)
|
ownsPreparedAssets: true,
|
||||||
|
ResidencyBudgetOptions.Default)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,21 +125,24 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
preparedAssets: null,
|
preparedAssets: null,
|
||||||
logger,
|
logger,
|
||||||
resourceRetirement,
|
resourceRetirement,
|
||||||
ownsPreparedAssets: true);
|
ownsPreparedAssets: true,
|
||||||
|
ResidencyBudgetOptions.Default);
|
||||||
|
|
||||||
internal WbMeshAdapter(
|
internal WbMeshAdapter(
|
||||||
GL gl,
|
GL gl,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
IPreparedAssetSource preparedAssets,
|
IPreparedAssetSource preparedAssets,
|
||||||
ILogger<WbMeshAdapter> logger,
|
ILogger<WbMeshAdapter> logger,
|
||||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement)
|
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement,
|
||||||
|
ResidencyBudgetOptions? budgets = null)
|
||||||
: this(
|
: this(
|
||||||
gl,
|
gl,
|
||||||
dats,
|
dats,
|
||||||
preparedAssets,
|
preparedAssets,
|
||||||
logger,
|
logger,
|
||||||
resourceRetirement,
|
resourceRetirement,
|
||||||
ownsPreparedAssets: false)
|
ownsPreparedAssets: false,
|
||||||
|
budgets ?? ResidencyBudgetOptions.Default)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,11 +152,13 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
IPreparedAssetSource? preparedAssets,
|
IPreparedAssetSource? preparedAssets,
|
||||||
ILogger<WbMeshAdapter> logger,
|
ILogger<WbMeshAdapter> logger,
|
||||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement,
|
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement,
|
||||||
bool ownsPreparedAssets)
|
bool ownsPreparedAssets,
|
||||||
|
ResidencyBudgetOptions budgets)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(gl);
|
ArgumentNullException.ThrowIfNull(gl);
|
||||||
ArgumentNullException.ThrowIfNull(dats);
|
ArgumentNullException.ThrowIfNull(dats);
|
||||||
ArgumentNullException.ThrowIfNull(logger);
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
ArgumentNullException.ThrowIfNull(budgets);
|
||||||
|
|
||||||
_resourceRetirement = resourceRetirement;
|
_resourceRetirement = resourceRetirement;
|
||||||
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
|
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
|
||||||
|
|
@ -192,7 +199,8 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
meshManager = new ObjectMeshManager(
|
meshManager = new ObjectMeshManager(
|
||||||
graphicsDevice,
|
graphicsDevice,
|
||||||
resolvedPreparedAssets,
|
resolvedPreparedAssets,
|
||||||
new ConsoleErrorLogger<ObjectMeshManager>());
|
new ConsoleErrorLogger<ObjectMeshManager>(),
|
||||||
|
budgets);
|
||||||
resources.Add("WB object mesh manager", meshManager.Dispose);
|
resources.Add("WB object mesh manager", meshManager.Dispose);
|
||||||
resources.TransferAll();
|
resources.TransferAll();
|
||||||
}
|
}
|
||||||
|
|
@ -210,6 +218,26 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
: null;
|
: 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>
|
/// <summary>
|
||||||
/// Minimal Console-backed logger that fires only on
|
/// Minimal Console-backed logger that fires only on
|
||||||
/// <see cref="LogLevel.Error"/> and above. Format:
|
/// <see cref="LogLevel.Error"/> and above. Format:
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,12 @@ public interface IPreparedAssetSource : IDisposable
|
||||||
PreparedAssetSourceStats Stats { get; }
|
PreparedAssetSourceStats Stats { get; }
|
||||||
|
|
||||||
CacheStats DecodedTextureCacheStats { 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
|
internal static class PreparedAssetRequestContract
|
||||||
|
|
@ -211,6 +217,8 @@ public sealed class PakPreparedAssetSource : IPreparedAssetSource
|
||||||
|
|
||||||
public CacheStats DecodedTextureCacheStats => default;
|
public CacheStats DecodedTextureCacheStats => default;
|
||||||
|
|
||||||
|
public long MappedVirtualBytes => _reader.FileLength;
|
||||||
|
|
||||||
public PreparedAssetPresence Probe(
|
public PreparedAssetPresence Probe(
|
||||||
PakAssetType type,
|
PakAssetType type,
|
||||||
uint sourceFileId)
|
uint sourceFileId)
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ public sealed class PakReader : IDisposable {
|
||||||
private readonly ConcurrentDictionary<int, bool> _loggedCorruption = new();
|
private readonly ConcurrentDictionary<int, bool> _loggedCorruption = new();
|
||||||
|
|
||||||
public PakHeader Header { get; }
|
public PakHeader Header { get; }
|
||||||
|
public long FileLength => _fileLength;
|
||||||
|
|
||||||
public PakReader(string path) {
|
public PakReader(string path) {
|
||||||
_fileLength = new FileInfo(path).Length;
|
_fileLength = new FileInfo(path).Length;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Runtime.CompilerServices;
|
||||||
using AcDream.App.Composition;
|
using AcDream.App.Composition;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Rendering.Residency;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using AcDream.Core.Terrain;
|
using AcDream.Core.Terrain;
|
||||||
|
|
@ -19,7 +20,13 @@ public sealed class WorldRenderCompositionTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SuccessPublishesExactModernFoundationInFrozenOrder()
|
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();
|
WorldRenderResult result = fixture.Compose();
|
||||||
|
|
||||||
|
|
@ -30,6 +37,10 @@ public sealed class WorldRenderCompositionTests
|
||||||
Assert.Same(fixture.Lifetime.Atlas, result.Foundation.TerrainAtlas);
|
Assert.Same(fixture.Lifetime.Atlas, result.Foundation.TerrainAtlas);
|
||||||
Assert.Equal(QualitySettings.From(QualityPreset.High).AnisotropicLevel,
|
Assert.Equal(QualitySettings.From(QualityPreset.High).AnisotropicLevel,
|
||||||
fixture.Factory.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.Equal(1, fixture.Lifetime.AcquireCalls);
|
||||||
Assert.Empty(fixture.Factory.Releases);
|
Assert.Empty(fixture.Factory.Releases);
|
||||||
}
|
}
|
||||||
|
|
@ -134,13 +145,16 @@ public sealed class WorldRenderCompositionTests
|
||||||
private sealed class Fixture
|
private sealed class Fixture
|
||||||
{
|
{
|
||||||
private readonly WorldRenderCompositionPoint? _failurePoint;
|
private readonly WorldRenderCompositionPoint? _failurePoint;
|
||||||
|
private readonly ResidencyBudgetOptions _budgets;
|
||||||
|
|
||||||
public Fixture(
|
public Fixture(
|
||||||
bool hasFont = true,
|
bool hasFont = true,
|
||||||
WorldRenderCompositionPoint? failurePoint = null,
|
WorldRenderCompositionPoint? failurePoint = null,
|
||||||
string? publicationFailure = null)
|
string? publicationFailure = null,
|
||||||
|
ResidencyBudgetOptions? budgets = null)
|
||||||
{
|
{
|
||||||
_failurePoint = failurePoint;
|
_failurePoint = failurePoint;
|
||||||
|
_budgets = budgets ?? ResidencyBudgetOptions.Default;
|
||||||
Factory = new Factory(hasFont);
|
Factory = new Factory(hasFont);
|
||||||
Publication = new Publication(publicationFailure);
|
Publication = new Publication(publicationFailure);
|
||||||
Lifetime = new RenderLifetime(Factory.Atlas);
|
Lifetime = new RenderLifetime(Factory.Atlas);
|
||||||
|
|
@ -161,6 +175,7 @@ public sealed class WorldRenderCompositionTests
|
||||||
new WorldEnvironmentController(),
|
new WorldEnvironmentController(),
|
||||||
Lifetime,
|
Lifetime,
|
||||||
ImmediateGpuResourceRetirementQueue.Instance,
|
ImmediateGpuResourceRetirementQueue.Instance,
|
||||||
|
_budgets,
|
||||||
0xA9B4FFFFu,
|
0xA9B4FFFFu,
|
||||||
_ => { }),
|
_ => { }),
|
||||||
Publication,
|
Publication,
|
||||||
|
|
@ -199,6 +214,9 @@ public sealed class WorldRenderCompositionTests
|
||||||
public TerrainAtlas Atlas { get; } = Stub<TerrainAtlas>();
|
public TerrainAtlas Atlas { get; } = Stub<TerrainAtlas>();
|
||||||
public int AnisotropicLevel { get; private set; }
|
public int AnisotropicLevel { get; private set; }
|
||||||
public List<string> Releases { get; } = [];
|
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) { }
|
public void InitializeGlState(GL gl) { }
|
||||||
|
|
||||||
|
|
@ -266,15 +284,32 @@ public sealed class WorldRenderCompositionTests
|
||||||
GL gl,
|
GL gl,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
IPreparedAssetSource preparedAssets,
|
IPreparedAssetSource preparedAssets,
|
||||||
IGpuResourceRetirementQueue retirement) =>
|
IGpuResourceRetirementQueue retirement,
|
||||||
Resource<WbMeshAdapter>("WB mesh adapter");
|
ResidencyBudgetOptions budgets)
|
||||||
|
{
|
||||||
|
MeshBudgets = budgets;
|
||||||
|
return Resource<WbMeshAdapter>("WB mesh adapter");
|
||||||
|
}
|
||||||
|
|
||||||
public TextureCache CreateTextureCache(
|
public TextureCache CreateTextureCache(
|
||||||
GL gl,
|
GL gl,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
BindlessSupport bindless,
|
BindlessSupport bindless,
|
||||||
IGpuResourceRetirementQueue retirement) =>
|
IGpuResourceRetirementQueue retirement,
|
||||||
Resource<TextureCache>("texture cache");
|
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) =>
|
public SamplerCache CreateSamplerCache(GL gl) =>
|
||||||
Resource<SamplerCache>("sampler cache");
|
Resource<SamplerCache>("sampler cache");
|
||||||
|
|
|
||||||
|
|
@ -278,9 +278,9 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
|
|
||||||
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
|
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
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,
|
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
|
||||||
0d, 0d, null);
|
default, 0d, 0d, null);
|
||||||
|
|
||||||
private static string NewDirectory()
|
private static string NewDirectory()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,40 @@ namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
public sealed class CompositeTextureArrayCachePolicyTests
|
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]
|
[Theory]
|
||||||
[InlineData(16, 16, 64)]
|
[InlineData(16, 16, 64)]
|
||||||
[InlineData(128, 128, 64)]
|
[InlineData(128, 128, 64)]
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,60 @@ public sealed class ResidencyManagerTests
|
||||||
Assert.Equal(1, journal.Count);
|
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(
|
private static AssetHandle<MeshAsset> Resident(
|
||||||
ResidencyManager manager,
|
ResidencyManager manager,
|
||||||
ulong id,
|
ulong id,
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,8 @@ public sealed class StandaloneBindlessTextureCacheTests
|
||||||
cache.Tick();
|
cache.Tick();
|
||||||
|
|
||||||
Assert.Equal(2, cache.EntryCount);
|
Assert.Equal(2, cache.EntryCount);
|
||||||
|
Assert.Equal(12, cache.AllocatedBytes);
|
||||||
|
Assert.Equal(4, cache.RetiringBytes);
|
||||||
Assert.Equal(2, cache.UnownedEntryCount);
|
Assert.Equal(2, cache.UnownedEntryCount);
|
||||||
Assert.Single(retirements.Actions);
|
Assert.Single(retirements.Actions);
|
||||||
Assert.Empty(backend.Events);
|
Assert.Empty(backend.Events);
|
||||||
|
|
@ -60,6 +62,8 @@ public sealed class StandaloneBindlessTextureCacheTests
|
||||||
|
|
||||||
retirements.Drain();
|
retirements.Drain();
|
||||||
Assert.Equal(["nonresident:1", "delete:1"], backend.Events);
|
Assert.Equal(["nonresident:1", "delete:1"], backend.Events);
|
||||||
|
Assert.Equal(8, cache.AllocatedBytes);
|
||||||
|
Assert.Equal(0, cache.RetiringBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -196,6 +200,32 @@ public sealed class StandaloneBindlessTextureCacheTests
|
||||||
backend.Events);
|
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]
|
[Fact]
|
||||||
public void FailedResidencyReleaseNeverDeletesThatBackingTexture()
|
public void FailedResidencyReleaseNeverDeletesThatBackingTexture()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue