namespace AcDream.App.Rendering.Residency;
///
/// Single-writer logical residency owner. It has no dependency on OpenGL and
/// never stores physical resource names.
///
internal sealed class ResidencyManager
{
private sealed class AssetSlot
{
public required RuntimeTypeHandle AssetType { get; init; }
public required ushort Generation { get; init; }
public required ResidencyAssetKey Key { get; init; }
public required ulong WorldGeneration { get; init; }
public AssetResidencyState State { get; set; }
public ResidencyPriority Priority { get; set; }
public long LastUsedFrame { get; set; }
public long RebuildCost { get; set; }
public ResidencyCharges Charges { get; set; }
public string? Failure { get; set; }
public HashSet Owners { get; } = [];
}
private sealed class OwnerSlot
{
public required ushort Generation { get; init; }
public required ResidencyOwnerKind Kind { get; init; }
public required ulong LogicalId { get; init; }
public required ulong WorldGeneration { get; init; }
public HashSet Assets { get; } = [];
}
private readonly record struct TypedAssetKey(
RuntimeTypeHandle AssetType,
ResidencyAssetKey Key);
private readonly List _assets = [null];
private readonly List _assetGenerations = [0];
private readonly Stack _freeAssets = [];
private readonly Dictionary _assetByKey = [];
private readonly List _owners = [null];
private readonly List _ownerGenerations = [0];
private readonly Stack _freeOwners = [];
private readonly List _domainSources = [];
private readonly List _observationScratch = [];
private readonly List _snapshotScratch = [];
public int ActiveAssetCount => _assetByKey.Count;
public int ActiveOwnerCount => _owners.Count(owner => owner is not null);
public void RegisterDomainSource(IResidencyDomainSource source)
{
ArgumentNullException.ThrowIfNull(source);
if (_domainSources.Any(candidate => candidate.Domain == source.Domain))
{
throw new InvalidOperationException(
$"Residency domain {source.Domain} is already registered.");
}
_domainSources.Add(source);
}
public OwnerToken CreateOwner(
ResidencyOwnerKind kind,
ulong logicalId,
ulong worldGeneration)
{
uint index = AllocateIndex(_owners, _ownerGenerations, _freeOwners);
ushort generation = NextGeneration(_ownerGenerations, index);
_owners[checked((int)index)] = new OwnerSlot
{
Generation = generation,
Kind = kind,
LogicalId = logicalId,
WorldGeneration = worldGeneration,
};
return new OwnerToken(index, generation);
}
public bool RetireOwner(OwnerToken owner)
{
if (!TryGetOwner(owner, out OwnerSlot slot))
return false;
AssetReference[] assets = slot.Assets.ToArray();
for (int i = 0; i < assets.Length; i++)
{
if (TryGetAsset(assets[i], out AssetSlot asset))
asset.Owners.Remove(owner);
}
_owners[checked((int)owner.Index)] = null;
_freeOwners.Push(owner.Index);
return true;
}
public AssetHandle Request(
ResidencyAssetKey key,
ResidencyPriority priority,
ulong worldGeneration,
long frame,
long rebuildCost = 0)
{
ArgumentOutOfRangeException.ThrowIfNegative(frame);
ArgumentOutOfRangeException.ThrowIfNegative(rebuildCost);
RuntimeTypeHandle type = typeof(TAsset).TypeHandle;
var typedKey = new TypedAssetKey(type, key);
if (_assetByKey.TryGetValue(typedKey, out uint existingIndex))
{
AssetSlot existing = _assets[checked((int)existingIndex)]
?? throw new InvalidOperationException(
"Residency key index did not resolve to a live slot.");
if (IsTerminal(existing.State))
{
RecycleAsset(existingIndex, existing);
}
else
{
existing.Priority = Max(existing.Priority, priority);
existing.LastUsedFrame = Math.Max(existing.LastUsedFrame, frame);
existing.RebuildCost = Math.Max(existing.RebuildCost, rebuildCost);
return new AssetHandle(
existingIndex,
existing.Generation);
}
}
uint index = AllocateIndex(_assets, _assetGenerations, _freeAssets);
ushort generation = NextGeneration(_assetGenerations, index);
var slot = new AssetSlot
{
AssetType = type,
Generation = generation,
Key = key,
WorldGeneration = worldGeneration,
State = AssetResidencyState.Requested,
Priority = priority,
LastUsedFrame = frame,
RebuildCost = rebuildCost,
};
_assets[checked((int)index)] = slot;
_assetByKey.Add(typedKey, index);
return new AssetHandle(index, generation);
}
public AssetLease Acquire(
AssetHandle handle,
OwnerToken owner,
ResidencyPriority priority,
long frame)
{
AssetSlot asset = GetAsset(handle);
OwnerSlot ownerSlot = GetOwner(owner);
if (asset.State is AssetResidencyState.Retiring
or AssetResidencyState.Cancelled
or AssetResidencyState.Missing
or AssetResidencyState.Corrupt
or AssetResidencyState.Failed)
{
throw new InvalidOperationException(
$"Cannot acquire an asset in {asset.State} state.");
}
if (ownerSlot.WorldGeneration != asset.WorldGeneration
&& ownerSlot.Kind is not ResidencyOwnerKind.Session
and not ResidencyOwnerKind.UserInterface
and not ResidencyOwnerKind.Tooling)
{
throw new InvalidOperationException(
"A world-scoped owner cannot acquire an asset from another generation.");
}
var lease = new AssetLease(handle, owner);
if (asset.Owners.Add(owner))
ownerSlot.Assets.Add(handle.Untyped);
asset.Priority = Max(asset.Priority, priority);
asset.LastUsedFrame = Math.Max(asset.LastUsedFrame, frame);
return lease;
}
public bool Release(AssetLease lease)
{
if (!TryGetAsset(lease.Handle.Untyped, out AssetSlot asset)
|| !TryGetOwner(lease.Owner, out OwnerSlot owner))
{
return false;
}
bool removed = asset.Owners.Remove(lease.Owner);
if (removed)
{
owner.Assets.Remove(lease.Handle.Untyped);
if (asset.Owners.Count == 0)
asset.Priority = ResidencyPriority.Speculative;
}
return removed;
}
public bool Touch(
AssetHandle handle,
ResidencyPriority priority,
long frame)
{
if (!TryGetAsset(handle.Untyped, out AssetSlot asset))
return false;
asset.Priority = Max(asset.Priority, priority);
asset.LastUsedFrame = Math.Max(asset.LastUsedFrame, frame);
return true;
}
public bool Transition(
AssetHandle handle,
AssetResidencyState next,
ResidencyCharges charges,
long frame,
string? failure = null) =>
Transition(handle.Untyped, next, charges, frame, failure);
public bool CompleteRetirement(AssetHandle handle) =>
CompleteRetirement(handle.Untyped);
public bool CompleteRetirement(AssetReference asset)
{
if (!TryGetAsset(asset, out AssetSlot slot))
return false;
if (slot.State != AssetResidencyState.Retiring)
{
throw new InvalidOperationException(
$"Only a retiring asset can become absent; current={slot.State}.");
}
if (slot.Owners.Count != 0)
throw new InvalidOperationException(
"A retiring asset retained logical owners.");
RecycleAsset(asset.Index, slot);
return true;
}
public bool TryGetSnapshot(
AssetHandle handle,
out ResidencyEntrySnapshot snapshot) =>
TryGetSnapshot(handle.Untyped, out snapshot);
public int Drain(ResidencyObservationJournal journal)
{
ArgumentNullException.ThrowIfNull(journal);
_observationScratch.Clear();
journal.DrainTo(_observationScratch);
_observationScratch.Sort(static (left, right) =>
{
int byIndex = left.Asset.Index.CompareTo(right.Asset.Index);
if (byIndex != 0)
return byIndex;
return left.Kind.CompareTo(right.Kind);
});
int applied = 0;
for (int i = 0; i < _observationScratch.Count; i++)
{
ResidencyObservation observation = _observationScratch[i];
bool accepted = observation.Kind switch
{
ResidencyObservationKind.Touch =>
Touch(
observation.Asset,
observation.Priority,
observation.Frame),
ResidencyObservationKind.Transition =>
Transition(
observation.Asset,
observation.State,
observation.Charges,
observation.Frame,
observation.Failure),
_ => throw new ArgumentOutOfRangeException(
nameof(observation),
observation.Kind,
"unknown residency observation kind"),
};
if (accepted)
applied++;
}
return applied;
}
public IReadOnlyList SelectEvictions(
ResidencyDomain domain,
long bytesToRelease,
int maximumCount)
{
ArgumentOutOfRangeException.ThrowIfNegative(bytesToRelease);
ArgumentOutOfRangeException.ThrowIfNegative(maximumCount);
if (bytesToRelease == 0 || maximumCount == 0)
return Array.Empty();
List<(uint Index, AssetSlot Slot)> candidates = [];
for (uint index = 1; index < _assets.Count; index++)
{
AssetSlot? slot = _assets[checked((int)index)];
if (slot is null
|| slot.Key.Domain != domain
|| slot.Owners.Count != 0
|| slot.State is not (AssetResidencyState.Resident
or AssetResidencyState.Prepared))
{
continue;
}
candidates.Add((index, slot));
}
candidates.Sort(static (left, right) =>
{
int order = left.Slot.Priority.CompareTo(right.Slot.Priority);
if (order != 0)
return order;
order = left.Slot.LastUsedFrame.CompareTo(right.Slot.LastUsedFrame);
if (order != 0)
return order;
order = left.Slot.RebuildCost.CompareTo(right.Slot.RebuildCost);
return order != 0
? order
: left.Index.CompareTo(right.Index);
});
List requests = [];
long selectedBytes = 0;
for (int i = 0;
i < candidates.Count
&& requests.Count < maximumCount
&& selectedBytes < bytesToRelease;
i++)
{
(uint index, AssetSlot slot) = candidates[i];
ResidencyCharges retiring = ToRetiringCharges(slot.Charges);
var asset = new AssetReference(
index,
slot.Generation,
slot.AssetType);
Transition(
asset,
AssetResidencyState.Retiring,
retiring,
slot.LastUsedFrame);
requests.Add(new ResidencyTrimRequest(asset, slot.Key, retiring));
selectedBytes = checked(
selectedBytes + Math.Max(
1,
retiring.RetiringBytes
+ retiring.CommittedCpuBytes));
}
return requests;
}
public ResidencySnapshot CaptureSnapshot()
{
_snapshotScratch.Clear();
for (int i = 0; i < _domainSources.Count; i++)
_snapshotScratch.Add(_domainSources[i].CaptureResidency());
ResidencyCharges totals = default;
for (uint index = 1; index < _assets.Count; index++)
{
AssetSlot? slot = _assets[checked((int)index)];
if (slot is not null)
totals += slot.Charges;
}
for (int i = 0; i < _snapshotScratch.Count; i++)
totals += _snapshotScratch[i].Charges;
return new ResidencySnapshot(_snapshotScratch.ToArray(), totals);
}
private bool Transition(
AssetReference asset,
AssetResidencyState next,
ResidencyCharges charges,
long frame,
string? failure = null)
{
if (!TryGetAsset(asset, out AssetSlot slot))
return false;
ArgumentOutOfRangeException.ThrowIfNegative(frame);
charges.Validate();
ValidateTransition(slot.State, next);
ValidateCharges(next, charges);
if (next == AssetResidencyState.Retiring && slot.Owners.Count != 0)
{
throw new InvalidOperationException(
"An owned asset cannot enter retiring state.");
}
slot.State = next;
slot.Charges = charges;
slot.LastUsedFrame = Math.Max(slot.LastUsedFrame, frame);
slot.Failure = next is AssetResidencyState.Corrupt
or AssetResidencyState.Failed
? failure
: null;
if (IsTerminal(next))
DetachAllOwners(asset, slot);
return true;
}
private bool Touch(
AssetReference asset,
ResidencyPriority priority,
long frame)
{
if (!TryGetAsset(asset, out AssetSlot slot))
return false;
slot.Priority = Max(slot.Priority, priority);
slot.LastUsedFrame = Math.Max(slot.LastUsedFrame, frame);
return true;
}
private bool TryGetSnapshot(
AssetReference asset,
out ResidencyEntrySnapshot snapshot)
{
if (!TryGetAsset(asset, out AssetSlot slot))
{
snapshot = default;
return false;
}
snapshot = new ResidencyEntrySnapshot(
asset,
slot.Key,
slot.State,
slot.Priority,
slot.WorldGeneration,
slot.LastUsedFrame,
slot.RebuildCost,
slot.Owners.Count,
slot.Charges,
slot.Failure);
return true;
}
private AssetSlot GetAsset(AssetHandle handle) =>
TryGetAsset(handle.Untyped, out AssetSlot slot)
? slot
: throw new InvalidOperationException("Asset handle is stale or invalid.");
private OwnerSlot GetOwner(OwnerToken owner) =>
TryGetOwner(owner, out OwnerSlot slot)
? slot
: throw new InvalidOperationException("Owner token is stale or invalid.");
private bool TryGetAsset(
AssetReference asset,
out AssetSlot slot)
{
slot = null!;
if (!asset.IsValid || asset.Index >= _assets.Count)
return false;
AssetSlot? candidate = _assets[checked((int)asset.Index)];
if (candidate is null
|| candidate.Generation != asset.Generation
|| !candidate.AssetType.Equals(asset.AssetType))
{
return false;
}
slot = candidate;
return true;
}
private bool TryGetOwner(OwnerToken owner, out OwnerSlot slot)
{
slot = null!;
if (!owner.IsValid || owner.Index >= _owners.Count)
return false;
OwnerSlot? candidate = _owners[checked((int)owner.Index)];
if (candidate is null || candidate.Generation != owner.Generation)
return false;
slot = candidate;
return true;
}
private void RecycleAsset(uint index, AssetSlot slot)
{
if (slot.Owners.Count != 0)
throw new InvalidOperationException(
"An asset cannot be recycled while owners remain.");
_assetByKey.Remove(new TypedAssetKey(slot.AssetType, slot.Key));
_assets[checked((int)index)] = null;
_freeAssets.Push(index);
}
private void DetachAllOwners(AssetReference asset, AssetSlot slot)
{
if (slot.Owners.Count == 0)
return;
OwnerToken[] owners = slot.Owners.ToArray();
for (int i = 0; i < owners.Length; i++)
{
if (TryGetOwner(owners[i], out OwnerSlot owner))
owner.Assets.Remove(asset);
}
slot.Owners.Clear();
}
private static uint AllocateIndex(
List slots,
List generations,
Stack free)
where T : class
{
if (free.TryPop(out uint reused))
return reused;
uint index = checked((uint)slots.Count);
slots.Add(null);
generations.Add(0);
return index;
}
private static ushort NextGeneration(
List generations,
uint index)
{
ushort generation = unchecked(
(ushort)(generations[checked((int)index)] + 1));
if (generation == 0)
generation = 1;
generations[checked((int)index)] = generation;
return generation;
}
private static ResidencyPriority Max(
ResidencyPriority left,
ResidencyPriority right) =>
left >= right ? left : right;
private static bool IsTerminal(AssetResidencyState state) =>
state is AssetResidencyState.Cancelled
or AssetResidencyState.Missing
or AssetResidencyState.Corrupt
or AssetResidencyState.Failed;
private static void ValidateTransition(
AssetResidencyState current,
AssetResidencyState next)
{
bool legal = current switch
{
AssetResidencyState.Requested =>
next is AssetResidencyState.Prepared
or AssetResidencyState.Cancelled
or AssetResidencyState.Missing
or AssetResidencyState.Corrupt
or AssetResidencyState.Failed
or AssetResidencyState.Retiring,
AssetResidencyState.Prepared =>
next is AssetResidencyState.UploadPending
or AssetResidencyState.Retiring,
AssetResidencyState.UploadPending =>
next is AssetResidencyState.Resident
or AssetResidencyState.Prepared
or AssetResidencyState.Retiring
or AssetResidencyState.Cancelled
or AssetResidencyState.Corrupt
or AssetResidencyState.Failed,
AssetResidencyState.Resident =>
next is AssetResidencyState.Retiring,
_ => false,
};
if (!legal)
{
throw new InvalidOperationException(
$"Illegal residency transition {current} -> {next}.");
}
}
private static void ValidateCharges(
AssetResidencyState state,
ResidencyCharges charges)
{
if (state is AssetResidencyState.Cancelled
or AssetResidencyState.Missing
or AssetResidencyState.Corrupt
or AssetResidencyState.Failed)
{
if (!charges.IsZero)
{
throw new InvalidOperationException(
$"Terminal state {state} cannot retain residency charges.");
}
return;
}
if (state == AssetResidencyState.Retiring
&& (charges.GpuRequestedBytes != 0
|| charges.GpuResidentBytes != 0
|| charges.StagingBytes != 0))
{
throw new InvalidOperationException(
"Retiring charges must move transient and resident GPU bytes into RetiringBytes.");
}
}
private static ResidencyCharges ToRetiringCharges(
ResidencyCharges current) =>
new(
LogicalBytes: current.LogicalBytes,
CpuPreparedBytes: current.CpuPreparedBytes,
DecodedBytes: current.DecodedBytes,
PinnedBytes: current.PinnedBytes,
RetiringBytes: checked(
current.GpuRequestedBytes
+ current.GpuResidentBytes
+ current.RetiringBytes));
}