Test: cover audio format variants in the mock-game stress suite

mock_game_test now launches coop_mock_game at 44100/48000/96000 (PCM + float)
and asserts the hook measures each variant's rate through the full inject path
(+ non-silent capture) -- the "all audio variants work" part of the suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 05:34:50 +02:00
parent 5796d5af39
commit 2d65961bf2
2 changed files with 99 additions and 4 deletions

View File

@@ -214,10 +214,12 @@ ctest --test-dir build -C Debug --output-on-failure
hook's shared video texture, and **decodes the frame number out of the captured pixels** hook's shared video texture, and **decodes the frame number out of the captured pixels**
to assert the mirror sees a *monotonic, advancing* sequence for both backends (the bar to assert the mirror sees a *monotonic, advancing* sequence for both backends (the bar
for no dropped / stale / out-of-order frames — what the DX12 rotating-backbuffer bug for no dropped / stale / out-of-order frames — what the DX12 rotating-backbuffer bug
broke). It then injects with audio + video, checks both stream, cycles the audio broke). It launches the game at several **audio formats** (44100/48000/96000, PCM +
subsystem off/on (hook/unhook stress), and confirms the game never crashes and capture float) and asserts the hook measures each one's rate through the full inject path, then
resumes. This suite drove out five real audio races (see Lessons learned). Skips cleanly injects with audio + video, checks both stream, cycles the audio subsystem off/on
without a D3D11 device. (hook/unhook stress), and confirms the game never crashes and capture resumes. This
suite drove out five real audio races (see Lessons learned). Skips cleanly without a
D3D11 device.
### Debugging the hooks against a real game ### Debugging the hooks against a real game

View File

@@ -295,6 +295,93 @@ void test_video_capture(const char* backend, ID3D11Device* device)
game.kill(); game.kill();
} }
// Launch the mock game rendering audio at `rate`/`channels`/`bits`/`fmt`, inject the audio
// hook (late, so it's the guessed path), and verify the hook MEASURES the right sample rate
// for this variant and captures non-silent audio. (Channels/bit-depth aren't recoverable for
// a guessed stream -- that's the documented limitation -- so the rate is the variant check.)
void test_audio_variant(unsigned rate, unsigned channels, unsigned bits, const wchar_t* fmt)
{
wchar_t args[64];
swprintf(args, static_cast<int>(std::size(args)), L"dx11 30 %u %u %u %ls", rate, channels, bits, fmt);
std::printf("== audio variant: %u Hz %u ch %u-bit %ls ==\n", rate, channels, bits, fmt);
MockGame game = MockGame::launch(args);
if (!game.ok)
{
check(false, "launch coop_mock_game (audio variant)");
return;
}
Sleep(800);
// Audio subsystem only.
SharedMemory shm;
const std::uint32_t disabled = (1u << HookSubsys_Input) | (1u << HookSubsys_Focus) |
(1u << HookSubsys_Video) | (1u << HookSubsys_Mkb);
SharedBlock* block = make_ipc(shm, game.pid(), disabled);
SharedMemory ring_shm;
AudioRingHeader* ring = nullptr;
if (ring_shm.create(audio_ring_name(game.pid()), audio_ring_total_size(kAudioRingCapacity)))
{
ring = ring_shm.as<AudioRingHeader>();
audio_ring_init(*ring, kAudioRingCapacity);
ring->capture_enabled.store(1, std::memory_order_release);
}
if (block == nullptr || ring == nullptr || !inject_retry(game.pid()))
{
check(false, "inject into mock game (audio variant)");
game.kill();
return;
}
// Wait for the rate to be measured + published (Measured / LowConfidence / Exact), then
// drain to prove non-silent capture.
std::vector<std::uint8_t> drain(kAudioRingCapacity);
std::uint32_t measured = 0;
std::uint32_t state = 0;
double peak = 0.0;
for (int i = 0; i < 120 && game.alive(); ++i) // up to ~6 s
{
Sleep(50);
std::uint32_t got = 0;
while ((got = audio_ring_pop(*ring, drain.data(), static_cast<std::uint32_t>(drain.size()))) > 0)
{
if (ring->bits == 32 && ring->format_tag == 3)
{
const auto* f = reinterpret_cast<const float*>(drain.data());
for (std::uint32_t k = 0; k < got / 4; ++k)
{
peak = std::max(peak, static_cast<double>(std::fabs(f[k])));
}
}
else if (ring->bits == 16)
{
const auto* s = reinterpret_cast<const std::int16_t*>(drain.data());
for (std::uint32_t k = 0; k < got / 2; ++k)
{
peak = std::max(peak, std::abs(s[k]) / 32768.0);
}
}
if (got < drain.size())
{
break;
}
}
state = block->status.audio_streams[0].format_state;
if (state == AudioFormat_Measured || state == AudioFormat_LowConfidence || state == AudioFormat_Exact)
{
measured = block->status.audio_streams[0].sample_rate;
if (measured != 0 && peak > 0.01)
{
break;
}
}
}
std::printf(" measured rate=%u Hz (state=%u) peak=%.4f\n", measured, state, peak);
check(measured == rate, "hook measured this variant's sample rate");
check(peak > 0.01, "captured non-silent audio for this variant");
game.kill();
}
// Inject with audio + video, verify both stream, then cycle the audio subsystem off/on a // Inject with audio + video, verify both stream, then cycle the audio subsystem off/on a
// few times (hook/unhook stress) and confirm the game stays alive (heartbeat advances). // few times (hook/unhook stress) and confirm the game stays alive (heartbeat advances).
void test_av_and_hook_cycles(ID3D11Device* device) void test_av_and_hook_cycles(ID3D11Device* device)
@@ -432,6 +519,12 @@ int main()
test_video_capture("dx11", device); test_video_capture("dx11", device);
test_video_capture("dx12", device); test_video_capture("dx12", device);
// Audio variants: the hook must measure each variant's rate through the full inject path.
test_audio_variant(44100, 2, 16, L"pcm");
test_audio_variant(48000, 2, 32, L"float");
test_audio_variant(96000, 2, 32, L"float");
test_av_and_hook_cycles(device); test_av_and_hook_cycles(device);
device->Release(); device->Release();