Extract VM audio opcode handler

This commit is contained in:
gamer147
2026-08-02 22:25:06 -04:00
parent 26e1df48d3
commit 5587b47152
4 changed files with 179 additions and 127 deletions

View File

@@ -172,6 +172,11 @@ the interpolation helpers consumed by retained-scene sampling. `engine/Age.Engin
owns surface/movie transition queues, presentation activity and click-skip logic, diagnostics and dirty-reason
accounting, and visible retained-scene snapshots.
`engine/Age.Engine/Vm/VirtualMachine.cs` retains VM lifecycle, cross-domain state, and the proven top-level
opcode dispatcher. Its partial-class companion `engine/Age.Engine/Vm/VirtualMachine.Audio.cs` owns VM audio
state, BGM restart semantics, and the BGM/voice/SFX/mixer opcode handler; `Step` retains the audio labels and
routes that family into the handler.
The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map
runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports
have no repository output tree and write their automatic maps below `user://diagnostics/page-maps` instead.

View File

@@ -624,6 +624,12 @@ do not mix mechanical moves with semantic changes.
accounting, scene reset, persistence, and locking. The planned `GfxState` decomposition is complete; runtime
validation remains green.
The first bounded `VirtualMachine.Step` extraction converted the sealed VM type to a sealed partial class and
moved VM-owned audio state, BGM restart semantics, and the existing BGM/voice/SFX/mixer case bodies into
`engine/Age.Engine/Vm/VirtualMachine.Audio.cs`. The top-level dispatcher retains the exact opcode labels and
routes only that family through `StepAudio`; public VM behavior and case-body logic remain unchanged. Runtime
validation remains green.
**Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where
deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
each domain move.
@@ -995,7 +1001,8 @@ layer's rendering diverges from ADV; save layout.
## 8. Immediate next step
Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed
by the tracked launcher and layered validation driver. With the planned `Main`, `GodotAdvHost`, and `GfxState`
domains isolated, begin thinning `VirtualMachine.Step` through existing domain handler methods without replacing
the proven dispatcher or changing public types, commands, and generated output.
domains isolated and the first `VirtualMachine.Step` family routed through a domain handler, extract the movie
opcode family next without replacing the proven dispatcher or changing public types, commands, and generated
output.
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
not replace Phase B gameplay validation or the open cross-platform gates.

View File

@@ -0,0 +1,145 @@
using Age.Engine.Model;
using Age.Engine.Persistence;
namespace Age.Engine.Vm;
public sealed partial class VirtualMachine
{
private readonly AudioMixerSettings _audioMixerSettings;
private long _currentBgmTrackId;
private readonly long[] _loadedSoundEffectResourceIds =
new long[NativeNumberedSaveState.SoundEffectChannelCount];
private void RestartBgm(long requestedTrackId, int startMode)
{
if (requestedTrackId != 0)
_currentBgmTrackId = requestedTrackId;
if (_currentBgmTrackId == 0)
_host.StopBgm();
else
_host.RestartBgm(_currentBgmTrackId, startMode);
}
private int StepAudio(string label, IReadOnlyList<Operand> a, int pc)
{
switch (label)
{
case "play-bgm":
{
long requestedTrackId = Read(a[0]);
// Native scripts freely reassert the stage BGM after FIELD action/event cleanup.
// The music facade retains the current track id, so an identical request keeps the
// existing stream position instead of reopening the OGG from the beginning.
if (requestedTrackId != _currentBgmTrackId)
{
_currentBgmTrackId = requestedTrackId;
_host.PlayBgm(_currentBgmTrackId);
}
return pc + 1;
}
case "restart-bgm-loop": // 0xb7: force start, including current-track/zero alias
RestartBgm(Read(a[0]), 1);
return pc + 1;
case "stop-bgm": // 0xb8: release source and clear retained track
_currentBgmTrackId = 0;
_host.StopBgm();
return pc + 1;
case "restart-bgm-once": // 0xb9: force start without decoder rewind at EOF
RestartBgm(Read(a[0]), 0);
return pc + 1;
case "get-current-bgm-track":
Write(a[0], _currentBgmTrackId);
return pc + 1;
case "play-voice":
_autoVoicePending = true;
TextHistory.AppendVoice(Read(a[0]), 0, _advTextStyle);
_host.PlayVoice(Read(a[0]), 0); return pc + 1;
case "play-history-voice": // 0x1bd: native voice start/history argument is one
case "u0041D910":
_autoVoicePending = true;
TextHistory.AppendVoice(Read(a[0]), 1, _advTextStyle);
_host.PlayVoice(Read(a[0]), 1); return pc + 1;
case "set-voice-bgm-duck-control": // 0x1cf: bit 0 suppresses automatic voice ducking
_host.SetVoiceBgmDuckControl(Read(a[0])); return pc + 1;
case "schedule-voice-playback": // 0x2c0: replace the pending delayed combat voice request
_host.ScheduleVoicePlayback(Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1;
case "play-sound-effect": // 0xb4 / semantics: sfx-load
{
long resourceId = Read(a[0]);
int channel = (int)Read(a[1]);
if ((uint)channel < (uint)_loadedSoundEffectResourceIds.Length)
_loadedSoundEffectResourceIds[channel] = resourceId;
_host.LoadSoundEffect(resourceId, channel);
return pc + 1;
}
case "u0041D050": // 0xb5 / semantics: sfx-start-once
_host.StartSoundEffect((int)Read(a[0]), 0); return pc + 1;
case "sfx-start-loop": // 0xba / same worker, logical decoder rewind at EOF
_host.StartSoundEffect((int)Read(a[0]), 1); return pc + 1;
case "u0041D080": // 0xb6 / semantics: sfx-release
{
int channel = (int)Read(a[0]);
if ((uint)channel < (uint)_loadedSoundEffectResourceIds.Length)
_loadedSoundEffectResourceIds[channel] = 0;
_host.ReleaseSoundEffect(channel);
return pc + 1;
}
case "schedule-sfx-start": // 0x2bf / native SetDelay(channel, start mode, delay ms)
_host.ScheduleSoundEffectStart((int)Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1;
case "u0041D2B0": // 0xc2 / semantics: fade-bgm
{
int targetPercent = (int)Read(a[0]);
_host.FadeBgm(targetPercent, Read(a[1]));
if (targetPercent == 0)
{
_currentBgmTrackId = 0;
_host.StopBgm();
}
return pc + 1;
}
case "get-audio-volume": // 0xc5 (category)(out basis points)
{
int category = unchecked((int)Read(a[0]));
if (_audioMixerSettings.TryGetVolume(category, out int basisPoints))
Write(a[1], basisPoints);
else
_host.ReportWarning($"audio volume category out of range: {category}");
return pc + 1;
}
case "set-audio-volume": // 0xc6 (category)(basis points)
{
int category = unchecked((int)Read(a[0]));
long basisPoints = Read(a[1]);
if (_audioMixerSettings.TrySetVolume(category, basisPoints))
_host.ApplyAudioVolume(category, checked((int)basisPoints));
else
_host.ReportWarning($"audio volume category out of range: {category}");
return pc + 1;
}
case "get-audio-route-enabled": // 0xc7 (category)(out boolean)
{
int category = unchecked((int)Read(a[0]));
if (_audioMixerSettings.TryGetRouteEnabled(category, out bool enabled))
Write(a[1], enabled ? 1 : 0);
else
_host.ReportWarning($"audio route category out of range: {category}");
return pc + 1;
}
case "set-audio-route-enabled": // 0x1ba (category)(enabled)
{
int category = unchecked((int)Read(a[0]));
bool enabled = Read(a[1]) != 0;
if (_audioMixerSettings.TrySetRouteEnabled(category, enabled, out bool changed))
{
if (changed) _host.ApplyAudioRouteEnabled(category, enabled);
}
else
_host.ReportWarning($"audio route category out of range: {category}");
return pc + 1;
}
default:
throw new InvalidOperationException($"Non-audio opcode routed to audio handler: {label}");
}
}
}

View File

@@ -22,7 +22,7 @@ public readonly record struct SharedProfileShutdownFlushResult(
SharedProfileShutdownFlushOutcome Outcome,
string? Error = null);
public sealed class VirtualMachine
public sealed partial class VirtualMachine
{
private const long NoJump = 0xFFFFFFFF;
private const int HALT = int.MinValue;
@@ -42,7 +42,6 @@ public sealed class VirtualMachine
private readonly Encoding _nativeStringEncoding;
private readonly IScriptProvider? _provider;
private readonly SharedProfile _sharedProfile;
private readonly AudioMixerSettings _audioMixerSettings;
private readonly DiagnosticOutputState _diagnosticOutput;
private readonly INativeDatStore? _nativeDatStore;
private static readonly bool _diagSetTexture = System.Environment.GetEnvironmentVariable("AGE_DIAG_SETTEX") == "1";
@@ -58,9 +57,6 @@ public sealed class VirtualMachine
private NativeNumberedSaveState? _loadedNumberedState;
private NativeNumberedSaveState? _retainedNativeNumberedState;
private int _restoreFrameIndex = -1;
private long _currentBgmTrackId;
private readonly long[] _loadedSoundEffectResourceIds =
new long[NativeNumberedSaveState.SoundEffectChannelCount];
private uint _accumulatedPlaySeconds;
private readonly long _sessionStartTimestamp;
private ExecFrame? _debugActiveFrame;
@@ -822,16 +818,6 @@ public sealed class VirtualMachine
_intStacks[i] = new Stack<int>(0x100);
}
private void RestartBgm(long requestedTrackId, int startMode)
{
if (requestedTrackId != 0)
_currentBgmTrackId = requestedTrackId;
if (_currentBgmTrackId == 0)
_host.StopBgm();
else
_host.RestartBgm(_currentBgmTrackId, startMode);
}
private FrameOutcome RunFrame(ExecFrame frame, FrameCause cause, long callId = 0)
{
@@ -1218,7 +1204,8 @@ public sealed class VirtualMachine
{
int op = ins.Opcode;
var a = ins.Args;
switch (_t.Label(op))
string label = _t.Label(op);
switch (label)
{
case "script-entry":
Gfx.ClearSurfaceReloadPolicies(); return pc + 1;
@@ -2555,118 +2542,26 @@ public sealed class VirtualMachine
Gfx.ReleaseSurfaceRange(42, 1000 - 42);
_host.ReleaseSurfaceRange(42, 1000 - 42); return pc + 1;
case "play-bgm":
{
long requestedTrackId = Read(a[0]);
// Native scripts freely reassert the stage BGM after FIELD action/event cleanup.
// The music facade retains the current track id, so an identical request keeps the
// existing stream position instead of reopening the OGG from the beginning.
if (requestedTrackId != _currentBgmTrackId)
{
_currentBgmTrackId = requestedTrackId;
_host.PlayBgm(_currentBgmTrackId);
}
return pc + 1;
}
case "restart-bgm-loop": // 0xb7: force start, including current-track/zero alias
RestartBgm(Read(a[0]), 1);
return pc + 1;
case "stop-bgm": // 0xb8: release source and clear retained track
_currentBgmTrackId = 0;
_host.StopBgm();
return pc + 1;
case "restart-bgm-once": // 0xb9: force start without decoder rewind at EOF
RestartBgm(Read(a[0]), 0);
return pc + 1;
case "restart-bgm-loop":
case "stop-bgm":
case "restart-bgm-once":
case "get-current-bgm-track":
Write(a[0], _currentBgmTrackId);
return pc + 1;
case "play-voice":
_autoVoicePending = true;
TextHistory.AppendVoice(Read(a[0]), 0, _advTextStyle);
_host.PlayVoice(Read(a[0]), 0); return pc + 1;
case "play-history-voice": // 0x1bd: native voice start/history argument is one
case "play-history-voice":
case "u0041D910":
_autoVoicePending = true;
TextHistory.AppendVoice(Read(a[0]), 1, _advTextStyle);
_host.PlayVoice(Read(a[0]), 1); return pc + 1;
case "set-voice-bgm-duck-control": // 0x1cf: bit 0 suppresses automatic voice ducking
_host.SetVoiceBgmDuckControl(Read(a[0])); return pc + 1;
case "schedule-voice-playback": // 0x2c0: replace the pending delayed combat voice request
_host.ScheduleVoicePlayback(Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1;
case "play-sound-effect": // 0xb4 / semantics: sfx-load
{
long resourceId = Read(a[0]);
int channel = (int)Read(a[1]);
if ((uint)channel < (uint)_loadedSoundEffectResourceIds.Length)
_loadedSoundEffectResourceIds[channel] = resourceId;
_host.LoadSoundEffect(resourceId, channel);
return pc + 1;
}
case "u0041D050": // 0xb5 / semantics: sfx-start-once
_host.StartSoundEffect((int)Read(a[0]), 0); return pc + 1;
case "sfx-start-loop": // 0xba / same worker, logical decoder rewind at EOF
_host.StartSoundEffect((int)Read(a[0]), 1); return pc + 1;
case "u0041D080": // 0xb6 / semantics: sfx-release
{
int channel = (int)Read(a[0]);
if ((uint)channel < (uint)_loadedSoundEffectResourceIds.Length)
_loadedSoundEffectResourceIds[channel] = 0;
_host.ReleaseSoundEffect(channel);
return pc + 1;
}
case "schedule-sfx-start": // 0x2bf / native SetDelay(channel, start mode, delay ms)
_host.ScheduleSoundEffectStart((int)Read(a[0]), (int)Read(a[1]), Read(a[2])); return pc + 1;
case "u0041D2B0": // 0xc2 / semantics: fade-bgm
{
int targetPercent = (int)Read(a[0]);
_host.FadeBgm(targetPercent, Read(a[1]));
if (targetPercent == 0)
{
_currentBgmTrackId = 0;
_host.StopBgm();
}
return pc + 1;
}
case "get-audio-volume": // 0xc5 (category)(out basis points)
{
int category = unchecked((int)Read(a[0]));
if (_audioMixerSettings.TryGetVolume(category, out int basisPoints))
Write(a[1], basisPoints);
else
_host.ReportWarning($"audio volume category out of range: {category}");
return pc + 1;
}
case "set-audio-volume": // 0xc6 (category)(basis points)
{
int category = unchecked((int)Read(a[0]));
long basisPoints = Read(a[1]);
if (_audioMixerSettings.TrySetVolume(category, basisPoints))
_host.ApplyAudioVolume(category, checked((int)basisPoints));
else
_host.ReportWarning($"audio volume category out of range: {category}");
return pc + 1;
}
case "get-audio-route-enabled": // 0xc7 (category)(out boolean)
{
int category = unchecked((int)Read(a[0]));
if (_audioMixerSettings.TryGetRouteEnabled(category, out bool enabled))
Write(a[1], enabled ? 1 : 0);
else
_host.ReportWarning($"audio route category out of range: {category}");
return pc + 1;
}
case "set-audio-route-enabled": // 0x1ba (category)(enabled)
{
int category = unchecked((int)Read(a[0]));
bool enabled = Read(a[1]) != 0;
if (_audioMixerSettings.TrySetRouteEnabled(category, enabled, out bool changed))
{
if (changed) _host.ApplyAudioRouteEnabled(category, enabled);
}
else
_host.ReportWarning($"audio route category out of range: {category}");
return pc + 1;
}
case "set-voice-bgm-duck-control":
case "schedule-voice-playback":
case "play-sound-effect":
case "u0041D050":
case "sfx-start-loop":
case "u0041D080":
case "schedule-sfx-start":
case "u0041D2B0":
case "get-audio-volume":
case "set-audio-volume":
case "get-audio-route-enabled":
case "set-audio-route-enabled":
return StepAudio(label, a, pc);
case "u00415880": // 0xd9 / semantics: clear-run-state-0x1000
return pc + 1;
case "get-initial-root-run": // 0x130 (out)