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:
parent
1d51e35c14
commit
60a1698ce7
12 changed files with 1901 additions and 152 deletions
|
|
@ -15,7 +15,8 @@ namespace AcDream.App.Rendering;
|
|||
public sealed class GameWindow :
|
||||
IDisposable,
|
||||
IGameWindowPlatformPublication<GL, IInputContext>,
|
||||
IGameWindowHostInputCameraPublication
|
||||
IGameWindowHostInputCameraPublication,
|
||||
IGameWindowContentEffectsAudioPublication
|
||||
{
|
||||
private static double ClientTimerNow() =>
|
||||
System.Diagnostics.Stopwatch.GetTimestamp()
|
||||
|
|
@ -251,6 +252,10 @@ public sealed class GameWindow :
|
|||
// register sinks at startup. The router is always non-null so the
|
||||
// per-entity tick loop can just call it unconditionally.
|
||||
private readonly AcDream.Core.Physics.AnimationHookRouter _hookRouter = new();
|
||||
private AcDream.App.Composition.AnimationHookRegistrationSet?
|
||||
_hookRegistrations;
|
||||
private readonly AcDream.App.Rendering.Vfx.DeferredEntityEffectAdvanceSource
|
||||
_entityEffectAdvance = new();
|
||||
|
||||
// Phase E.2 audio. Null when ACDREAM_NO_AUDIO=1 or the OpenAL driver
|
||||
// failed to init; all three are set together.
|
||||
|
|
@ -693,6 +698,92 @@ public sealed class GameWindow :
|
|||
AcDream.App.Input.CameraPointerInputController value) =>
|
||||
PublishCompositionOwner(ref _cameraPointerInput, value, "camera pointer input");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishDatCollection(
|
||||
IDatReaderWriter value) =>
|
||||
PublishCompositionOwner(ref _dats, value, "DAT collection");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishMagicCatalog(
|
||||
AcDream.App.Spells.MagicCatalog value) =>
|
||||
PublishCompositionOwner(ref _magicCatalog, value, "magic catalog");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishAnimationLoader(
|
||||
AcDream.Core.Physics.IAnimationLoader value) =>
|
||||
PublishCompositionOwner(ref _animLoader, value, "animation loader");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishLiveEntityCollisionBuilder(
|
||||
AcDream.App.Physics.LiveEntityCollisionBuilder value) =>
|
||||
PublishCompositionOwner(
|
||||
ref _liveEntityCollisionBuilder,
|
||||
value,
|
||||
"live-entity collision builder");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishEmitterRegistry(
|
||||
AcDream.Core.Vfx.EmitterDescRegistry value) =>
|
||||
PublishCompositionOwner(ref _emitterRegistry, value, "emitter registry");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishParticleSystem(
|
||||
AcDream.Core.Vfx.ParticleSystem value) =>
|
||||
PublishCompositionOwner(ref _particleSystem, value, "particle system");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishParticleSink(
|
||||
AcDream.Core.Vfx.ParticleHookSink value) =>
|
||||
PublishCompositionOwner(ref _particleSink, value, "particle hook sink");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishAnimationHookFrames(
|
||||
AcDream.App.Rendering.Vfx.AnimationHookFrameQueue value) =>
|
||||
PublishCompositionOwner(
|
||||
ref _animationHookFrames,
|
||||
value,
|
||||
"animation-hook frame queue");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishPhysicsScriptLoader(
|
||||
AcDream.Content.Vfx.RetailPhysicsScriptLoader value) =>
|
||||
PublishCompositionOwner(
|
||||
ref _physicsScriptLoader,
|
||||
value,
|
||||
"physics-script loader");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishPhysicsScriptRunner(
|
||||
AcDream.Core.Vfx.PhysicsScriptRunner value) =>
|
||||
PublishCompositionOwner(ref _scriptRunner, value, "physics-script runner");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishLightingSink(
|
||||
AcDream.Core.Lighting.LightingHookSink value) =>
|
||||
PublishCompositionOwner(ref _lightingSink, value, "lighting hook sink");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishTranslucencySink(
|
||||
AcDream.Core.Rendering.TranslucencyHookSink value) =>
|
||||
PublishCompositionOwner(
|
||||
ref _translucencySink,
|
||||
value,
|
||||
"translucency hook sink");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishHookRegistrations(
|
||||
AcDream.App.Composition.AnimationHookRegistrationSet value) =>
|
||||
PublishCompositionOwner(
|
||||
ref _hookRegistrations,
|
||||
value,
|
||||
"animation-hook registrations");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishAudio(
|
||||
ContentAudioGraph value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
if (_soundCache is not null
|
||||
|| _audioEngine is not null
|
||||
|| _entitySoundTables is not null
|
||||
|| _audioSink is not null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The GameWindow composition shell already owns audio state.");
|
||||
}
|
||||
|
||||
_soundCache = value.SoundCache;
|
||||
_audioEngine = value.Engine;
|
||||
_entitySoundTables = value.EntitySoundTables;
|
||||
_audioSink = value.HookSink;
|
||||
}
|
||||
|
||||
private static void PublishCompositionOwner<T>(
|
||||
ref T? destination,
|
||||
T value,
|
||||
|
|
@ -748,6 +839,23 @@ public sealed class GameWindow :
|
|||
WorldRenderDiagnostics worldRenderDiagnostics =
|
||||
hostInputCamera.WorldRenderDiagnostics;
|
||||
|
||||
ContentEffectsAudioResult contentEffectsAudio =
|
||||
new ContentEffectsAudioCompositionPhase(
|
||||
new ContentEffectsAudioDependencies(
|
||||
_datDir,
|
||||
_physicsDataCache,
|
||||
_animationDiagnostics.DumpMotionEnabled,
|
||||
SpellBook,
|
||||
_hookRouter,
|
||||
_effectPoses,
|
||||
_entityEffectAdvance,
|
||||
Lighting,
|
||||
_translucencyFades,
|
||||
_options.NoAudio,
|
||||
Console.WriteLine,
|
||||
Console.Error.WriteLine),
|
||||
this).Compose(platform, hostInputCamera);
|
||||
|
||||
_gl.ClearColor(0.05f, 0.10f, 0.18f, 1.0f);
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
|
||||
|
|
@ -798,80 +906,6 @@ public sealed class GameWindow :
|
|||
Console.WriteLine("world-hud font: no system monospace font found");
|
||||
}
|
||||
|
||||
_dats = RuntimeDatCollectionFactory.OpenReadOnly(_datDir);
|
||||
_magicCatalog = AcDream.App.Spells.MagicCatalog.Load(_dats);
|
||||
SpellBook.InstallMetadata(_magicCatalog.SpellTable);
|
||||
Console.WriteLine($"spells: loaded {SpellTable.Count} entries from portal.dat");
|
||||
_animLoader = new AcDream.Content.Vfx.RetailAnimationLoader(_dats);
|
||||
_liveEntityCollisionBuilder = new AcDream.App.Physics.LiveEntityCollisionBuilder(
|
||||
_physicsDataCache,
|
||||
new AcDream.App.Physics.LiveEntityDefaultPoseResolver(
|
||||
id => _dats.Get<DatReaderWriter.DBObjs.MotionTable>(id),
|
||||
_animLoader,
|
||||
_animationDiagnostics.DumpMotionEnabled));
|
||||
_emitterRegistry = new AcDream.Core.Vfx.EmitterDescRegistry(_dats);
|
||||
|
||||
// Phase E.3 particles: always-on, no driver dependency. Registered
|
||||
// with the hook router so CreateParticle / DestroyParticle /
|
||||
// StopParticle hooks fired from motion tables produce visible
|
||||
// spawns. The Tick call is driven from OnRender.
|
||||
_particleSystem = new AcDream.Core.Vfx.ParticleSystem(_emitterRegistry!);
|
||||
_particleSink = new AcDream.Core.Vfx.ParticleHookSink(_particleSystem, _effectPoses);
|
||||
_particleSink.DiagnosticSink = message =>
|
||||
Console.Error.WriteLine($"vfx: {message}");
|
||||
_animationHookFrames = new AcDream.App.Rendering.Vfx.AnimationHookFrameQueue(
|
||||
_hookRouter,
|
||||
_effectPoses);
|
||||
_hookRouter.Register(_particleSink);
|
||||
|
||||
// Phase 6c — PhysicsScript runner. Uses the DatCollection to
|
||||
// resolve PlayScript ids, and the same ParticleHookSink the
|
||||
// animation system uses, so CreateParticleHook fired from a
|
||||
// script spawns through the normal particle pipeline.
|
||||
_physicsScriptLoader = new AcDream.Content.Vfx.RetailPhysicsScriptLoader(_dats);
|
||||
_scriptRunner = new AcDream.Core.Vfx.PhysicsScriptRunner(
|
||||
_physicsScriptLoader.LoadPhysicsScript,
|
||||
_hookRouter,
|
||||
canAdvanceOwner: ownerId => _entityEffects?.CanAdvanceOwner(ownerId) ?? true);
|
||||
|
||||
// Phase G.2 lighting hooks: SetLightHook flips IsLit on
|
||||
// owner-tagged lights so ignite-torch animations light up,
|
||||
// extinguish-torch animations go dark.
|
||||
_lightingSink = new AcDream.Core.Lighting.LightingHookSink(Lighting, _effectPoses);
|
||||
_hookRouter.Register(_lightingSink);
|
||||
|
||||
// #188 — TransparentPartHook (per-part translucency fade, e.g.
|
||||
// the "fading wall" secret-passage doors) routes here.
|
||||
_translucencySink = new AcDream.Core.Rendering.TranslucencyHookSink(_translucencyFades);
|
||||
_hookRouter.Register(_translucencySink);
|
||||
|
||||
// Phase E.2 audio: init OpenAL + hook sink. Suppressible via
|
||||
// ACDREAM_NO_AUDIO=1 for headless tests / broken audio drivers.
|
||||
if (!_options.NoAudio)
|
||||
{
|
||||
try
|
||||
{
|
||||
_soundCache = new AcDream.Core.Audio.DatSoundCache(_dats);
|
||||
_audioEngine = new AcDream.App.Audio.OpenAlAudioEngine();
|
||||
_entitySoundTables = new AcDream.App.Audio.DictionaryEntitySoundTable();
|
||||
if (_audioEngine.IsAvailable)
|
||||
{
|
||||
_audioSink = new AcDream.App.Audio.AudioHookSink(
|
||||
_audioEngine, _soundCache, _entitySoundTables);
|
||||
_hookRouter.Register(_audioSink);
|
||||
Console.WriteLine("audio: OpenAL engine ready (16 voices, 3D positional)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("audio: OpenAL unavailable (driver missing / headless) — audio disabled");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"audio: init failed: {ex.Message} — audio disabled");
|
||||
}
|
||||
}
|
||||
|
||||
// Settings are runtime state, not devtools state. Apply the immutable
|
||||
// pre-window snapshot once after camera/audio exist and before any
|
||||
// optional frontend or world/render factory consumes it.
|
||||
|
|
@ -1091,7 +1125,8 @@ public sealed class GameWindow :
|
|||
uint centerLandblockId = 0xA9B4FFFFu;
|
||||
Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}");
|
||||
|
||||
var region = _dats.Get<DatReaderWriter.DBObjs.Region>(0x13000000u);
|
||||
var region = contentEffectsAudio.Dats.Get<DatReaderWriter.DBObjs.Region>(
|
||||
0x13000000u);
|
||||
var heightTable = region?.LandDefs.LandHeightTable;
|
||||
if (heightTable is null || heightTable.Length < 256)
|
||||
throw new InvalidOperationException("Region.LandDefs.LandHeightTable missing or truncated");
|
||||
|
|
@ -1138,7 +1173,7 @@ public sealed class GameWindow :
|
|||
var terrainAtlas = _renderResourceLifetime.AcquireTerrainAtlas(
|
||||
() => AcDream.App.Rendering.TerrainAtlas.Build(
|
||||
_gl,
|
||||
_dats,
|
||||
contentEffectsAudio.Dats,
|
||||
_bindlessSupport));
|
||||
// A.5 T22.5: apply anisotropic level from quality preset. Build()
|
||||
// hard-codes 16x; override here to match the resolved quality so Low
|
||||
|
|
@ -1188,7 +1223,11 @@ public sealed class GameWindow :
|
|||
Path.Combine(shadersDir, "mesh_modern.frag"));
|
||||
Console.WriteLine("[N.5] mesh_modern shader loaded");
|
||||
|
||||
_textureCache = new TextureCache(_gl, _dats, _bindlessSupport, _gpuFrameFlights!);
|
||||
_textureCache = new TextureCache(
|
||||
_gl,
|
||||
contentEffectsAudio.Dats,
|
||||
_bindlessSupport,
|
||||
_gpuFrameFlights!);
|
||||
// Two persistent GL sampler objects (Repeat + ClampToEdge) so
|
||||
// the sky pass can pick wrap mode per submesh without mutating
|
||||
// shared per-texture wrap state. See SamplerCache + the
|
||||
|
|
@ -1567,7 +1606,7 @@ public sealed class GameWindow :
|
|||
var wbLogger = Microsoft.Extensions.Logging.Abstractions.NullLogger<AcDream.App.Rendering.Wb.WbMeshAdapter>.Instance;
|
||||
_wbMeshAdapter = new AcDream.App.Rendering.Wb.WbMeshAdapter(
|
||||
_gl,
|
||||
_dats,
|
||||
contentEffectsAudio.Dats,
|
||||
wbLogger,
|
||||
_gpuFrameFlights!);
|
||||
Console.WriteLine("[N.4+N.5] WB foundation + modern path active — routing all content through ObjectMeshManager.");
|
||||
|
|
@ -1825,6 +1864,7 @@ public sealed class GameWindow :
|
|||
_entitySoundTables?.Set(ownerId, did);
|
||||
});
|
||||
_entityEffects = entityEffects;
|
||||
_entityEffectAdvance.Bind(entityEffects);
|
||||
entityEffects.DiagnosticSink = message =>
|
||||
Console.Error.WriteLine($"vfx: {message}");
|
||||
var partArrayLifecycle =
|
||||
|
|
@ -1855,7 +1895,7 @@ public sealed class GameWindow :
|
|||
remoteTeleportPresentation.Begin);
|
||||
_equippedChildRenderer.ProjectionPoseReady += guid =>
|
||||
_liveEntityLights.OnAttachedPoseReady(guid);
|
||||
_hookRouter.Register(entityEffects);
|
||||
_hookRegistrations!.Register(entityEffects);
|
||||
|
||||
_wbDrawDispatcher = new AcDream.App.Rendering.Wb.WbDrawDispatcher(
|
||||
_gl, _meshShader!, _textureCache!, _wbMeshAdapter!, _wbEntitySpawnAdapter, _bindlessSupport!,
|
||||
|
|
@ -2032,7 +2072,11 @@ public sealed class GameWindow :
|
|||
Path.Combine(shadersDir, "sky.vert"),
|
||||
Path.Combine(shadersDir, "sky.frag")));
|
||||
_skyRenderer = new AcDream.App.Rendering.Sky.SkyRenderer(
|
||||
_gl, _dats, skyShader, _textureCache!, _samplerCache);
|
||||
_gl,
|
||||
contentEffectsAudio.Dats,
|
||||
skyShader,
|
||||
_textureCache!,
|
||||
_samplerCache);
|
||||
|
||||
// Phase G.1 particle renderer — renders rain / snow / spell auras
|
||||
// spawned into the shared ParticleSystem. Mode-1/no-degrade GfxObjs
|
||||
|
|
@ -2041,9 +2085,9 @@ public sealed class GameWindow :
|
|||
_particleRenderer = new ParticleRenderer(
|
||||
_gl,
|
||||
shadersDir,
|
||||
_particleSystem,
|
||||
contentEffectsAudio.ParticleSystem,
|
||||
_textureCache,
|
||||
_dats,
|
||||
contentEffectsAudio.Dats,
|
||||
_wbMeshAdapter,
|
||||
_retailAlphaQueue);
|
||||
|
||||
|
|
@ -3540,6 +3584,28 @@ public sealed class GameWindow :
|
|||
[
|
||||
new("live entity runtime", () => _liveEntities?.Clear()),
|
||||
]),
|
||||
new ResourceShutdownStage("effect dispatch edges",
|
||||
[
|
||||
new("entity-effect advance source", () =>
|
||||
{
|
||||
if (_entityEffects is { } effects)
|
||||
_entityEffectAdvance.Unbind(effects);
|
||||
_entityEffectAdvance.Deactivate();
|
||||
}),
|
||||
new("animation-hook registrations", () =>
|
||||
{
|
||||
AnimationHookRegistrationSet? registrations = _hookRegistrations;
|
||||
if (registrations is null)
|
||||
return;
|
||||
registrations.Dispose();
|
||||
if (!registrations.IsCleanupComplete)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Animation-hook registration cleanup remains pending.");
|
||||
}
|
||||
_hookRegistrations = null;
|
||||
}),
|
||||
]),
|
||||
new ResourceShutdownStage("live entity dependents",
|
||||
[
|
||||
new("live lights", () =>
|
||||
|
|
@ -3559,7 +3625,22 @@ public sealed class GameWindow :
|
|||
_animationHookFrames?.Clear();
|
||||
_effectPoses.Clear();
|
||||
}),
|
||||
new("audio", () => _audioEngine?.Dispose()),
|
||||
new("audio", () =>
|
||||
{
|
||||
AcDream.App.Audio.OpenAlAudioEngine? engine = _audioEngine;
|
||||
if (engine is null)
|
||||
return;
|
||||
engine.Dispose();
|
||||
if (!engine.IsDisposalComplete)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"OpenAL native-resource cleanup remains pending.");
|
||||
}
|
||||
_audioEngine = null;
|
||||
_audioSink = null;
|
||||
_soundCache = null;
|
||||
_entitySoundTables = null;
|
||||
}),
|
||||
]),
|
||||
new ResourceShutdownStage("submitted GPU work",
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue