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:
10
README.md
10
README.md
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user