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**
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
broke). It then injects with audio + video, checks both stream, cycles the audio
subsystem off/on (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.
broke). It launches the game at several **audio formats** (44100/48000/96000, PCM +
float) and asserts the hook measures each one's rate through the full inject path, then
injects with audio + video, checks both stream, cycles the audio subsystem off/on
(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

View File

@@ -295,6 +295,93 @@ void test_video_capture(const char* backend, ID3D11Device* device)
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
// few times (hook/unhook stress) and confirm the game stays alive (heartbeat advances).
void test_av_and_hook_cycles(ID3D11Device* device)
@@ -432,6 +519,12 @@ int main()
test_video_capture("dx11", 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);
device->Release();