refactor(app): compose content effects and audio startup

Move Phase-2 startup behind typed composition/publication boundaries, replace the GameWindow-capturing PhysicsScript gate with a focused deferred source, and own animation-hook registrations reversibly. Make OpenAL construction and teardown transactional so every device, context, source, and buffer prefix is retryable without replay.
This commit is contained in:
Erik 2026-07-22 15:36:05 +02:00
parent 1d51e35c14
commit 60a1698ce7
12 changed files with 1901 additions and 152 deletions

View file

@ -52,11 +52,10 @@ namespace AcDream.App.Audio;
public sealed unsafe class OpenAlAudioEngine : IAudioEngine
{
// ── Backends ─────────────────────────────────────────────────────────────
private readonly ALContext? _alc;
private readonly AL? _al;
private readonly Device* _device;
private readonly Context* _context;
private readonly bool _available;
private AL? _al;
private OpenAlResourceLifetime? _resources;
private bool _available;
private bool _disposed;
// ── Pools ────────────────────────────────────────────────────────────────
private const int PoolSize3D = 16; // retail 16-slot voice pool
@ -91,44 +90,48 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine
public bool IsAvailable => _available;
public OpenAlAudioEngine()
: this(new SilkOpenAlResourceApiFactory())
{
}
internal OpenAlAudioEngine(IOpenAlResourceApiFactory apiFactory)
{
ArgumentNullException.ThrowIfNull(apiFactory);
IOpenAlResourceApi api;
try
{
_alc = ALContext.GetApi(soft: true);
_al = AL.GetApi(soft: true);
_device = _alc.OpenDevice(string.Empty);
if (_device == null)
api = apiFactory.Create();
}
catch
{
return;
}
_al = api.AudioApi;
_resources = new OpenAlResourceLifetime(api);
try
{
if (!_resources.TryOpenDevice())
{
_available = false;
return;
}
_context = _alc.CreateContext(_device, null);
if (_context == null)
if (!_resources.TryCreateContext())
{
_alc.CloseDevice(_device);
_device = null;
_available = false;
DisableAfterInitializationFailure(
new InvalidOperationException("OpenAL could not create a context."));
return;
}
if (!_alc.MakeContextCurrent(_context))
if (!_resources.TryMakeCurrent())
{
_alc.DestroyContext(_context);
_context = null;
_alc.CloseDevice(_device);
_device = null;
_available = false;
DisableAfterInitializationFailure(
new InvalidOperationException("OpenAL could not activate its context."));
return;
}
// Initialise 3D source pool.
for (int i = 0; i < PoolSize3D; i++)
{
uint src = _al.GenSource();
_al.SetSourceProperty(src, SourceFloat.Gain, 1f);
_al.SetSourceProperty(src, SourceFloat.MaxDistance, 1000f);
_al.SetSourceProperty(src, SourceFloat.RolloffFactor, 1f);
_al.SetSourceProperty(src, SourceFloat.ReferenceDistance, 2f);
_al.SetSourceProperty(src, SourceBoolean.Looping, false);
uint src = _resources.Create3DSource();
_pool3D[i] = new Slot3D { SourceId = src, InUse = false };
}
@ -136,61 +139,57 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine
// ignore 3D position.
for (int i = 0; i < PoolSizeUi; i++)
{
uint src = _al.GenSource();
_al.SetSourceProperty(src, SourceBoolean.SourceRelative, true);
_al.SetSourceProperty(src, SourceFloat.Gain, 1f);
_al.SetSourceProperty(src, SourceBoolean.Looping, false);
uint src = _resources.CreateUiSource();
_poolUi[i] = src;
}
// Global distance model = inverse-square clamped (classic retail feel).
_al.DistanceModel(DistanceModel.InverseDistanceClamped);
api.SelectRetailDistanceModel();
_available = true;
}
catch
catch (OpenAlInitializationException)
{
// OpenAL driver unavailable (headless CI, missing libopenal).
_available = false;
throw;
}
catch (Exception failure)
{
DisableAfterInitializationFailure(failure);
}
}
public void Dispose()
{
if (!_available || _al is null) return;
if (_disposed)
return;
_available = false;
_resources?.RetryCleanup();
_disposed = _resources is null || _resources.IsCleanupComplete;
}
internal bool IsDisposalComplete =>
_disposed || _resources is null || _resources.IsCleanupComplete;
private void DisableAfterInitializationFailure(Exception failure)
{
_available = false;
if (_resources is null)
return;
try
{
for (int i = 0; i < PoolSize3D; i++)
{
var slot = _pool3D[i];
if (slot is null) continue;
_al.SourceStop(slot.SourceId);
_al.DeleteSource(slot.SourceId);
}
for (int i = 0; i < PoolSizeUi; i++)
{
_al.SourceStop(_poolUi[i]);
_al.DeleteSource(_poolUi[i]);
}
foreach (var kv in _ambientSources)
{
_al.SourceStop(kv.Value);
_al.DeleteSource(kv.Value);
}
foreach (var buf in _bufferByWaveId.Values)
{
_al.DeleteBuffer(buf);
}
if (_context != null && _alc is not null)
{
_alc.MakeContextCurrent(null);
_alc.DestroyContext(_context);
}
if (_device != null && _alc is not null)
_alc.CloseDevice(_device);
_resources.RetryCleanup();
}
catch { /* shutdown — ignore */ }
catch (AggregateException cleanupFailure)
{
throw new OpenAlInitializationException(
failure,
_resources,
cleanupFailure);
}
_al = null;
}
// ── IAudioEngine ─────────────────────────────────────────────────────────
@ -339,10 +338,11 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine
if (_bufferByWaveId.TryGetValue(waveId, out var existing)) return existing;
uint buf = _al.GenBuffer();
_resources!.OwnBuffer(buf);
BufferFormat fmt = PickFormat(wave);
if (fmt == 0)
{
_al.DeleteBuffer(buf);
_resources.ReleaseBuffer(buf);
_bufferByWaveId[waveId] = 0;
return 0;
}