feat(headless): share immutable process content
This commit is contained in:
parent
fbdb58a962
commit
12b500d383
11 changed files with 617 additions and 18 deletions
227
src/AcDream.Headless/Hosting/HeadlessProcessContentOwner.cs
Normal file
227
src/AcDream.Headless/Hosting/HeadlessProcessContentOwner.cs
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
using AcDream.Content;
|
||||
using AcDream.Headless.Configuration;
|
||||
|
||||
namespace AcDream.Headless.Hosting;
|
||||
|
||||
internal readonly record struct HeadlessProcessContentSnapshot(
|
||||
int LeaseCount,
|
||||
bool IsDisposeRequested,
|
||||
bool IsDisposed,
|
||||
long MappedVirtualBytes)
|
||||
{
|
||||
internal bool IsConverged =>
|
||||
IsDisposed
|
||||
&& LeaseCount == 0
|
||||
&& MappedVirtualBytes == 0L;
|
||||
}
|
||||
|
||||
internal interface IHeadlessProcessContentFactory
|
||||
{
|
||||
(IDatReaderWriter Dats, IPreparedAssetSource Prepared) Open(
|
||||
HeadlessContentDescriptor descriptor,
|
||||
Action<string> diagnostic);
|
||||
}
|
||||
|
||||
internal sealed class ProductionHeadlessProcessContentFactory
|
||||
: IHeadlessProcessContentFactory
|
||||
{
|
||||
public (IDatReaderWriter Dats, IPreparedAssetSource Prepared) Open(
|
||||
HeadlessContentDescriptor descriptor,
|
||||
Action<string> diagnostic)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
ArgumentNullException.ThrowIfNull(diagnostic);
|
||||
|
||||
string datDirectory = Path.GetFullPath(descriptor.DatDirectory);
|
||||
string preparedAssetPath =
|
||||
Path.GetFullPath(descriptor.PreparedAssetPath);
|
||||
IDatReaderWriter? dats = null;
|
||||
try
|
||||
{
|
||||
dats = RuntimeDatCollectionFactory.OpenReadOnly(datDirectory);
|
||||
var prepared = new PakPreparedAssetSource(
|
||||
preparedAssetPath,
|
||||
dats,
|
||||
diagnostic);
|
||||
return (dats, prepared);
|
||||
}
|
||||
catch
|
||||
{
|
||||
dats?.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process lifetime root for immutable/read-only content shared by all
|
||||
/// headless sessions. A lease carries no mutable session state. Disposal is
|
||||
/// requested once and closes the package/DAT handles only after the final
|
||||
/// session releases its lease.
|
||||
/// </summary>
|
||||
internal sealed class HeadlessProcessContentOwner : IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private IDatReaderWriter? _dats;
|
||||
private IPreparedAssetSource? _prepared;
|
||||
private int _leaseCount;
|
||||
private bool _disposeRequested;
|
||||
private bool _disposed;
|
||||
|
||||
internal HeadlessProcessContentOwner(
|
||||
HeadlessContentDescriptor descriptor,
|
||||
Action<string> diagnostic,
|
||||
IHeadlessProcessContentFactory? factory = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
ArgumentNullException.ThrowIfNull(diagnostic);
|
||||
(IDatReaderWriter dats, IPreparedAssetSource prepared) =
|
||||
(factory ?? new ProductionHeadlessProcessContentFactory())
|
||||
.Open(descriptor, diagnostic);
|
||||
_dats = dats
|
||||
?? throw new InvalidOperationException(
|
||||
"The headless content factory returned no DAT owner.");
|
||||
_prepared = prepared
|
||||
?? throw new InvalidOperationException(
|
||||
"The headless content factory returned no prepared source.");
|
||||
if (prepared is not IPreparedCollisionSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
prepared.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
dats.Dispose();
|
||||
}
|
||||
_prepared = null;
|
||||
_dats = null;
|
||||
throw new NotSupportedException(
|
||||
"Headless production content must expose prepared collision.");
|
||||
}
|
||||
}
|
||||
|
||||
internal HeadlessProcessContentLease AcquireLease(string sessionId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(sessionId);
|
||||
lock (_gate)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(
|
||||
_disposeRequested || _disposed,
|
||||
this);
|
||||
checked
|
||||
{
|
||||
_leaseCount++;
|
||||
}
|
||||
return new HeadlessProcessContentLease(
|
||||
this,
|
||||
sessionId,
|
||||
_dats!,
|
||||
_prepared!);
|
||||
}
|
||||
}
|
||||
|
||||
internal HeadlessProcessContentSnapshot CaptureSnapshot()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return new HeadlessProcessContentSnapshot(
|
||||
_leaseCount,
|
||||
_disposeRequested,
|
||||
_disposed,
|
||||
_prepared?.MappedVirtualBytes ?? 0L);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposeRequested = true;
|
||||
if (_leaseCount != 0)
|
||||
return;
|
||||
DrainResources();
|
||||
}
|
||||
}
|
||||
|
||||
private void Release()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_leaseCount <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Headless content lease count underflow.");
|
||||
}
|
||||
_leaseCount--;
|
||||
if (_leaseCount == 0 && _disposeRequested)
|
||||
DrainResources();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainResources()
|
||||
{
|
||||
if (_prepared is not null)
|
||||
{
|
||||
_prepared.Dispose();
|
||||
_prepared = null;
|
||||
}
|
||||
if (_dats is not null)
|
||||
{
|
||||
_dats.Dispose();
|
||||
_dats = null;
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
internal sealed class HeadlessProcessContentLease : IDisposable
|
||||
{
|
||||
private HeadlessProcessContentOwner? _owner;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly IPreparedAssetSource _prepared;
|
||||
|
||||
internal HeadlessProcessContentLease(
|
||||
HeadlessProcessContentOwner owner,
|
||||
string sessionId,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource prepared)
|
||||
{
|
||||
_owner = owner;
|
||||
SessionId = sessionId;
|
||||
_dats = dats;
|
||||
_prepared = prepared;
|
||||
}
|
||||
|
||||
internal string SessionId { get; }
|
||||
|
||||
internal IDatReaderWriter Dats
|
||||
{
|
||||
get
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_owner is null, this);
|
||||
return _dats;
|
||||
}
|
||||
}
|
||||
|
||||
internal IPreparedAssetSource PreparedAssets
|
||||
{
|
||||
get
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_owner is null, this);
|
||||
return _prepared;
|
||||
}
|
||||
}
|
||||
|
||||
internal IPreparedCollisionSource PreparedCollision =>
|
||||
(IPreparedCollisionSource)PreparedAssets;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
HeadlessProcessContentOwner? owner =
|
||||
Interlocked.Exchange(ref _owner, null);
|
||||
owner?.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue