From 12f9ebefabe03df23bfa112f470402454f360a84 Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 6 Aug 2026 09:08:20 -0500 Subject: [PATCH] A quiet sound at the wrong distance no longer kills the game From Nathan's crash dump: an access violation reading 8093e920, fourteen minutes into a session, on 4.12.115. rpl4opt!PatchLevelOfDetail::SetupPatch+0xbb rpl4opt!Static3DPatchSource::StartImplementation+0x50 rpl4opt!AudioRenderer::ExecuteBackground+0x9e The faulting instruction is g_buffers[index] with index = 0x20000000 - 536 million - and the array base in eax at 0093e920, which is exactly the address it died on. So the index was garbage, and the dump says where the garbage came from: the stack slot holding info.bufferIndex. PRESET_getSampleInfo builds a SAMPLEINFO to return when it is asked for a zone the preset does not have. It sets chan, file, implemented and loop - and not bufferIndex. Every caller tests bufferIndex >= 0 before using it, so "no such zone" was meant to be rejected there; instead the test read whatever was on the stack, and passed whenever that happened to be positive. AL_getBuffer then indexed the array with it, unchecked. Why it asked for a zone that is not there: the loop runs to sourceSet.count, which was fixed when the audio source was built, from whichever level of detail was selected at the time. SetDistance re-picks the level of detail by distance on the line immediately before SetupPatch runs, and the zone counts across the recovered banks are nothing like uniform - of 200 presets, 46 have no zones at all, and the rest run 1 to 4. So a sound that moved far enough to drop to a quieter patch could ask that patch for a zone it never had. In the dump: count 3, died asking for zone 2. Fixed at all three levels, because any one of them alone would have held: the default carries bufferIndex = -1 so the existing guard works, AL_getBuffer returns AL_NONE rather than reading past its array, and SetupPatch asks for no more zones than the patch it is actually using has. Verified: the dump's own numbers reproduce arithmetically, and two full races run clean. The distance-dependent trigger itself was reasoned from the dump rather than reproduced here - it needs a sound to cross a level of detail boundary into a shorter patch - so the belt-and-braces. Co-Authored-By: Claude Opus 5 (1M context) --- MUNGA_L4/L4AUDLVL.cpp | 19 ++++++++++++++++++- MUNGA_L4/L4AUDRES.cpp | 10 ++++++++++ MUNGA_L4/WTPresets.cpp | 7 +++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/MUNGA_L4/L4AUDLVL.cpp b/MUNGA_L4/L4AUDLVL.cpp index d5ef095..5f3fb4d 100644 --- a/MUNGA_L4/L4AUDLVL.cpp +++ b/MUNGA_L4/L4AUDLVL.cpp @@ -128,8 +128,25 @@ void // #endif SAMPLEINFO info; + // + // Ask this patch for no more zones than it has. + // + // sourceSet.count was fixed when the audio source was built, from + // whichever level of detail was selected at the time. SetDistance + // re-picks the level of detail by distance immediately before this + // runs (see Static3DPatchSource::StartImplementation), and a + // further-away patch can have fewer zones than the one the source was + // sized for - so the count outruns this patch's zone list, and the + // zones past the end come back as "no such zone". + // + int zone_count = PRESET_getNumSamples(bankID,patchID); + if (zone_count > sourceSet.count) + { + zone_count = sourceSet.count; + } + //Attach buffers - for (int i=0; i < sourceSet.count; i++) + for (int i=0; i < zone_count; i++) { info = PRESET_getSampleInfo(bankID,patchID,i); if (info.bufferIndex >= 0) diff --git a/MUNGA_L4/L4AUDRES.cpp b/MUNGA_L4/L4AUDRES.cpp index eb79704..b6bbdf1 100644 --- a/MUNGA_L4/L4AUDRES.cpp +++ b/MUNGA_L4/L4AUDRES.cpp @@ -871,6 +871,16 @@ void ALuint AL_getBuffer(int index) { + // + // 0 is AL_NONE - "no buffer" - which alSourcei accepts and which detaches + // the source rather than crashing. An index that is out of range means a + // zone that does not exist, and the only thing an unchecked lookup here + // can do about it is read whatever lies past the array. + // + if (g_buffers == NULL || index < 0 || index >= g_numBuffers) + { + return 0; + } return g_buffers[index]; } diff --git a/MUNGA_L4/WTPresets.cpp b/MUNGA_L4/WTPresets.cpp index 047f309..a3fe3ff 100644 --- a/MUNGA_L4/WTPresets.cpp +++ b/MUNGA_L4/WTPresets.cpp @@ -32,6 +32,13 @@ SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd) default.file = ""; default.implemented = false; default.loop = SampleLoop::LoopAtWill; + // + // -1 = no buffer. Every caller tests bufferIndex >= 0 before using it as + // an index, and this one field was being left as whatever was on the + // stack - so "this zone does not exist" read as a real buffer whenever + // the garbage happened to be positive, and indexed g_buffers with it. + // + default.bufferIndex = -1; if (sampleInd < 0 || sampleInd >= allPresets[bank-1][preset].sampleNum) {