diff --git a/runtime/src/hle/audio/ax_effects.cpp b/runtime/src/hle/audio/ax_effects.cpp index a30a4ac..ae0c80f 100644 --- a/runtime/src/hle/audio/ax_effects.cpp +++ b/runtime/src/hle/audio/ax_effects.cpp @@ -3,14 +3,19 @@ #include "hle_stubs.h" #include "memory.h" #include "ppc_runtime.h" +#include "runtime_log.h" #include #include #include +#include #include +#include +#include extern "C" void func_8012B830(CpuContext* ctx); +extern "C" void func_801284B4(CpuContext* ctx); #if defined(__clang__) // PowerPC uses discrete fmuls/fadds; a fused multiply-add would change sample rounding. @@ -18,6 +23,57 @@ extern "C" void func_8012B830(CpuContext* ctx); #endif namespace { +inline float LoadFloat(const uint8_t* host) { + return BigEndian::ReadFloat32(host); +} + +inline void StoreFloat(uint8_t* host, float value) { + BigEndian::WriteFloat32(host, value); +} + +inline int32_t LoadS32(const uint8_t* host) { + return static_cast(BigEndian::Read32(host)); +} + +inline void StoreS32(uint8_t* host, int32_t value) { + BigEndian::Write32(host, static_cast(value)); +} + +// PowerPC fctiwz: round toward zero, saturating out-of-range and NaN exactly the +// way runtime/src/fpu_helpers.cpp does for the translated form. +inline int32_t ConvertToIntegerWord(float value) { + const double wide = static_cast(value); + if (std::isnan(wide)) { + return static_cast(0x80000000u); + } + if (wide >= 2147483647.0) { + return 2147483647; + } + if (wide <= -2147483648.0) { + return static_cast(0x80000000u); + } + return static_cast(wide); +} + +// Guest-thread-only range resolver. Deliberately NOT the mix's MixResolveRange: this +// callback must materialize deferred GX reads through the page table, which the +// worker-safe resolver refuses to do by design. +uint8_t* ResolveGuestThreadRange(uint32_t addr, size_t bytes) { + if (addr == 0 || bytes == 0) { + return nullptr; + } + if (uint8_t* fast = MemoryInline::GetPointerFast(addr, bytes)) { + return fast; + } + // A ring buffer may straddle the inline page granularity; the region lookup + // still returns one contiguous host mapping for the whole range. + try { + return Memory::GetPointer(addr, bytes); + } catch (const Memory::AccessViolation&) { + return nullptr; + } +} + namespace ReverbStd { constexpr uint32_t kSamplesPerFrame = 96; @@ -93,57 +149,6 @@ struct Frame { bool hasAuxOut = false; }; -inline float LoadFloat(const uint8_t* host) { - return BigEndian::ReadFloat32(host); -} - -inline void StoreFloat(uint8_t* host, float value) { - BigEndian::WriteFloat32(host, value); -} - -inline int32_t LoadS32(const uint8_t* host) { - return static_cast(BigEndian::Read32(host)); -} - -inline void StoreS32(uint8_t* host, int32_t value) { - BigEndian::Write32(host, static_cast(value)); -} - -// PowerPC fctiwz: round toward zero, saturating out-of-range and NaN exactly the -// way runtime/src/fpu_helpers.cpp does for the translated form. -inline int32_t ConvertToIntegerWord(float value) { - const double wide = static_cast(value); - if (std::isnan(wide)) { - return static_cast(0x80000000u); - } - if (wide >= 2147483647.0) { - return 2147483647; - } - if (wide <= -2147483648.0) { - return static_cast(0x80000000u); - } - return static_cast(wide); -} - -// Guest-thread-only range resolver. Deliberately NOT the mix's MixResolveRange: this -// callback must materialize deferred GX reads through the page table, which the -// worker-safe resolver refuses to do by design. -uint8_t* ResolveGuestThreadRange(uint32_t addr, size_t bytes) { - if (addr == 0 || bytes == 0) { - return nullptr; - } - if (uint8_t* fast = MemoryInline::GetPointerFast(addr, bytes)) { - return fast; - } - // A ring buffer may straddle the inline page granularity; the region lookup - // still returns one contiguous host mapping for the whole range. - try { - return Memory::GetPointer(addr, bytes); - } catch (const Memory::AccessViolation&) { - return nullptr; - } -} - // Collects everything the render loop needs. Returns false when the layout is // not one this port can serve bit-exactly, in which case the caller must run the // translated function instead. @@ -332,6 +337,486 @@ void Render(uint32_t stateAddr, Frame& frame) { } } // namespace ReverbStd + +namespace ReverbHi { + +constexpr uint32_t kSamplesPerFrame = 96; +constexpr uint32_t kChannels = 3; +constexpr uint32_t kEarlyTaps = 3; +constexpr uint32_t kCombs = 3; +constexpr uint32_t kAllpasses = 2; + +// .sdata2 constants the guest function loads through r2 (_SDA2_BASE_ = 0x8038EFA0). +constexpr uint32_t kZeroConstantAddr = 0x803884D4u; // 0.0f, the comb accumulator seed +constexpr uint32_t kOneConstantAddr = 0x803884D8u; // 1.0f +constexpr uint32_t kWetConstantAddr = 0x803884DCu; // 0.6f wet pre-scale +constexpr uint32_t kMixConstantAddr = 0x803884E0u; // 0.5f channel cross-mix + +// AXFX_REVERBHI_EXP field offsets (byte offsets into the struct in r4). Every one +// is read off the translated body at 0x801284B4 rather than guessed: the arrays +// sit back to back, which is what makes the strides below self-checking. +constexpr uint32_t kFieldEarlyLine = 0x00; // + channel * 4 +constexpr uint32_t kFieldEarlyPos = 0x0C; // + tap * 4, one set shared by all channels +constexpr uint32_t kFieldEarlyLength = 0x18; +constexpr uint32_t kFieldEarlyCoef = 0x20; // + tap * 4 +constexpr uint32_t kFieldPreDelayLine = 0x2C; // + channel * 4 +constexpr uint32_t kFieldPreDelayPos = 0x38; +constexpr uint32_t kFieldPreDelayLength = 0x3C; // 0 bypasses the pre-delay +constexpr uint32_t kFieldCombLine = 0x44; // + channel * 12 + comb * 4 +constexpr uint32_t kFieldCombPos = 0x68; // + comb * 4 +constexpr uint32_t kFieldCombLength = 0x74; // + comb * 4 +constexpr uint32_t kFieldCombCoef = 0x8C; // + comb * 4 +constexpr uint32_t kFieldAllpassLine = 0x98; // + channel * 8 + allpass * 4 +constexpr uint32_t kFieldAllpassPos = 0xB0; // + allpass * 4 +constexpr uint32_t kFieldAllpassLength = 0xB8; // + allpass * 4 +constexpr uint32_t kFieldLastApLine = 0xC8; // + channel * 4 +constexpr uint32_t kFieldLastApPos = 0xD4; // + channel * 4 +constexpr uint32_t kFieldLastApLength = 0xE0; // + channel * 4 +constexpr uint32_t kFieldAllpassCoef = 0xF8; +constexpr uint32_t kFieldLastLpfOut = 0xFC; // + channel * 4 +constexpr uint32_t kFieldDamping = 0x108; +constexpr uint32_t kFieldFlags = 0x10C; +constexpr uint32_t kFieldMixPreScale = 0x12C; +constexpr uint32_t kFieldWetPreScale = 0x134; +constexpr uint32_t kFieldAuxInputBuffers = 0x138; +constexpr uint32_t kFieldAuxOutputBuffers = 0x13C; +constexpr uint32_t kFieldMainOutGain = 0x140; +constexpr uint32_t kFieldAuxOutGain = 0x144; +constexpr uint32_t kStateStructBytes = 0x148; + +struct Frame { + uint8_t* early[kChannels]{}; + uint8_t* preDelay[kChannels]{}; + uint8_t* comb[kChannels][kCombs]{}; + uint8_t* allpass[kChannels][kAllpasses]{}; + uint8_t* lastAp[kChannels]{}; + uint8_t* main[kChannels]{}; + const uint8_t* auxIn[kChannels]{}; + uint8_t* auxOut[kChannels]{}; + + uint32_t earlyPos[kEarlyTaps]{}; + uint32_t earlyLength = 0; + float earlyCoef[kEarlyTaps]{}; + + uint32_t preDelayPos = 0; + uint32_t preDelayLength = 0; + + uint32_t combPos[kCombs]{}; + uint32_t combLength[kCombs]{}; + float combCoef[kCombs]{}; + + uint32_t allpassPos[kAllpasses]{}; + uint32_t allpassLength[kAllpasses]{}; + + uint32_t lastApPos[kChannels]{}; + uint32_t lastApLength[kChannels]{}; + float lastLpfOut[kChannels]{}; + + float zero = 0.0f; + float allpassCoef = 0.0f; + float damping = 0.0f; + float oneMinusDamping = 0.0f; + float wetScale = 0.0f; + float mixScale = 0.0f; + float mainGain = 0.0f; + float auxGain = 0.0f; + bool hasAuxIn = false; + bool hasAuxOut = false; +}; + +// Same contract as ReverbStd::BuildFrame: false means this port cannot serve the +// layout bit-exactly, and the caller runs the translated function instead. +bool BuildFrame(uint32_t buffersAddr, uint32_t stateAddr, Frame& frame) { + if (buffersAddr == 0 || stateAddr == 0) { + return false; + } + if (!Memory::Contains(stateAddr, kStateStructBytes)) { + return false; + } + + const uint32_t auxInputBuffers = Memory::Read32(stateAddr + kFieldAuxInputBuffers); + const uint32_t auxOutputBuffers = Memory::Read32(stateAddr + kFieldAuxOutputBuffers); + frame.hasAuxIn = auxInputBuffers != 0; + frame.hasAuxOut = auxOutputBuffers != 0; + + constexpr size_t kFrameBytes = kSamplesPerFrame * sizeof(int32_t); + for (uint32_t channel = 0; channel < kChannels; ++channel) { + frame.main[channel] = + ResolveGuestThreadRange(Memory::Read32(buffersAddr + channel * 4), kFrameBytes); + if (!frame.main[channel]) { + return false; + } + if (frame.hasAuxIn) { + frame.auxIn[channel] = + ResolveGuestThreadRange(Memory::Read32(auxInputBuffers + channel * 4), kFrameBytes); + if (!frame.auxIn[channel]) { + return false; + } + } + if (frame.hasAuxOut) { + frame.auxOut[channel] = + ResolveGuestThreadRange(Memory::Read32(auxOutputBuffers + channel * 4), kFrameBytes); + if (!frame.auxOut[channel]) { + return false; + } + } + } + + // Early reflections: three read taps into one per-channel line, all wrapping + // against a single shared length. + frame.earlyLength = Memory::Read32(stateAddr + kFieldEarlyLength); + if (frame.earlyLength == 0) { + return false; + } + for (uint32_t tap = 0; tap < kEarlyTaps; ++tap) { + frame.earlyPos[tap] = Memory::Read32(stateAddr + kFieldEarlyPos + tap * 4); + if (frame.earlyPos[tap] >= frame.earlyLength) { + return false; + } + frame.earlyCoef[tap] = Memory::ReadFloat32(stateAddr + kFieldEarlyCoef + tap * 4); + } + const size_t earlyBytes = static_cast(frame.earlyLength) * sizeof(float); + for (uint32_t channel = 0; channel < kChannels; ++channel) { + frame.early[channel] = ResolveGuestThreadRange( + Memory::Read32(stateAddr + kFieldEarlyLine + channel * 4), earlyBytes); + if (!frame.early[channel]) { + return false; + } + } + + // A zero pre-delay length is the guest's own bypass, not a broken layout, so + // the lines are only required when it is armed. + frame.preDelayLength = Memory::Read32(stateAddr + kFieldPreDelayLength); + frame.preDelayPos = Memory::Read32(stateAddr + kFieldPreDelayPos); + if (frame.preDelayLength != 0) { + if (frame.preDelayPos >= frame.preDelayLength) { + return false; + } + const size_t preDelayBytes = static_cast(frame.preDelayLength) * sizeof(float); + for (uint32_t channel = 0; channel < kChannels; ++channel) { + frame.preDelay[channel] = ResolveGuestThreadRange( + Memory::Read32(stateAddr + kFieldPreDelayLine + channel * 4), preDelayBytes); + if (!frame.preDelay[channel]) { + return false; + } + } + } + + for (uint32_t comb = 0; comb < kCombs; ++comb) { + frame.combLength[comb] = Memory::Read32(stateAddr + kFieldCombLength + comb * 4); + frame.combPos[comb] = Memory::Read32(stateAddr + kFieldCombPos + comb * 4); + frame.combCoef[comb] = Memory::ReadFloat32(stateAddr + kFieldCombCoef + comb * 4); + if (frame.combLength[comb] == 0 || frame.combPos[comb] >= frame.combLength[comb]) { + return false; + } + const size_t combBytes = static_cast(frame.combLength[comb]) * sizeof(float); + for (uint32_t channel = 0; channel < kChannels; ++channel) { + frame.comb[channel][comb] = ResolveGuestThreadRange( + Memory::Read32(stateAddr + kFieldCombLine + channel * 12 + comb * 4), combBytes); + if (!frame.comb[channel][comb]) { + return false; + } + } + } + + for (uint32_t allpass = 0; allpass < kAllpasses; ++allpass) { + frame.allpassLength[allpass] = Memory::Read32(stateAddr + kFieldAllpassLength + allpass * 4); + frame.allpassPos[allpass] = Memory::Read32(stateAddr + kFieldAllpassPos + allpass * 4); + if (frame.allpassLength[allpass] == 0 || + frame.allpassPos[allpass] >= frame.allpassLength[allpass]) { + return false; + } + const size_t allpassBytes = + static_cast(frame.allpassLength[allpass]) * sizeof(float); + for (uint32_t channel = 0; channel < kChannels; ++channel) { + frame.allpass[channel][allpass] = ResolveGuestThreadRange( + Memory::Read32(stateAddr + kFieldAllpassLine + channel * 8 + allpass * 4), + allpassBytes); + if (!frame.allpass[channel][allpass]) { + return false; + } + } + } + + // The trailing allpass is the one stage whose index and length are per channel; + // the guest advances it inside the channel loop rather than once per sample. + for (uint32_t channel = 0; channel < kChannels; ++channel) { + frame.lastApLength[channel] = Memory::Read32(stateAddr + kFieldLastApLength + channel * 4); + frame.lastApPos[channel] = Memory::Read32(stateAddr + kFieldLastApPos + channel * 4); + if (frame.lastApLength[channel] == 0 || + frame.lastApPos[channel] >= frame.lastApLength[channel]) { + return false; + } + frame.lastAp[channel] = ResolveGuestThreadRange( + Memory::Read32(stateAddr + kFieldLastApLine + channel * 4), + static_cast(frame.lastApLength[channel]) * sizeof(float)); + if (!frame.lastAp[channel]) { + return false; + } + frame.lastLpfOut[channel] = Memory::ReadFloat32(stateAddr + kFieldLastLpfOut + channel * 4); + } + + const float one = Memory::ReadFloat32(kOneConstantAddr); + const float wetConstant = Memory::ReadFloat32(kWetConstantAddr); + const float mixConstant = Memory::ReadFloat32(kMixConstantAddr); + frame.zero = Memory::ReadFloat32(kZeroConstantAddr); + frame.damping = Memory::ReadFloat32(stateAddr + kFieldDamping); + frame.oneMinusDamping = one - frame.damping; + frame.wetScale = wetConstant * Memory::ReadFloat32(stateAddr + kFieldWetPreScale); + frame.mixScale = mixConstant * Memory::ReadFloat32(stateAddr + kFieldMixPreScale); + frame.allpassCoef = Memory::ReadFloat32(stateAddr + kFieldAllpassCoef); + frame.mainGain = Memory::ReadFloat32(stateAddr + kFieldMainOutGain); + frame.auxGain = Memory::ReadFloat32(stateAddr + kFieldAuxOutGain); + return true; +} + +void Render(uint32_t stateAddr, Frame& frame) { + uint32_t earlyPos[kEarlyTaps]; + for (uint32_t tap = 0; tap < kEarlyTaps; ++tap) { + earlyPos[tap] = frame.earlyPos[tap]; + } + uint32_t preDelayPos = frame.preDelayPos; + uint32_t combPos[kCombs]; + for (uint32_t comb = 0; comb < kCombs; ++comb) { + combPos[comb] = frame.combPos[comb]; + } + uint32_t allpassPos[kAllpasses]; + for (uint32_t allpass = 0; allpass < kAllpasses; ++allpass) { + allpassPos[allpass] = frame.allpassPos[allpass]; + } + + for (uint32_t sample = 0; sample < kSamplesPerFrame; ++sample) { + const uint32_t frameOffset = sample * 4u; + float mixed[kChannels]; + + for (uint32_t channel = 0; channel < kChannels; ++channel) { + uint8_t* const mainSlot = frame.main[channel] + frameOffset; + int32_t rawInput = LoadS32(mainSlot); + if (frame.hasAuxIn) { + rawInput = static_cast( + static_cast(rawInput) + + static_cast(LoadS32(frame.auxIn[channel] + frameOffset))); + } + const float input = static_cast(rawInput); + + // All three early taps read before the newest sample overwrites the + // third tap's slot. Every product is its own statement so the host + // compiler cannot fuse a multiply into the following add. + uint8_t* const earlyLine = frame.early[channel]; + const float earlyTap0 = LoadFloat(earlyLine + earlyPos[0] * 4u); + const float earlyTap1 = LoadFloat(earlyLine + earlyPos[1] * 4u); + const float earlyTap2 = LoadFloat(earlyLine + earlyPos[2] * 4u); + StoreFloat(earlyLine + earlyPos[2] * 4u, input); + const float early0 = frame.earlyCoef[0] * earlyTap0; + const float early1 = frame.earlyCoef[1] * earlyTap1; + const float early2 = frame.earlyCoef[2] * earlyTap2; + const float earlySum = early0 + early1; + const float early = early2 + earlySum; + + // Pure delay, no feedback coefficient. + float excite = input; + if (frame.preDelayLength != 0) { + uint8_t* const preDelaySlot = frame.preDelay[channel] + preDelayPos * 4u; + excite = LoadFloat(preDelaySlot); + StoreFloat(preDelaySlot, input); + } + + // Three combs, all fed the same pre-delay output; their taps sum into + // the allpass chain. + float combSum = frame.zero; + for (uint32_t comb = 0; comb < kCombs; ++comb) { + uint8_t* const combSlot = frame.comb[channel][comb] + combPos[comb] * 4u; + const float combTap = LoadFloat(combSlot); + const float combFeedback = combTap * frame.combCoef[comb]; + combSum = combSum + combTap; + StoreFloat(combSlot, excite + combFeedback); + } + + float allpassOut = combSum; + for (uint32_t allpass = 0; allpass < kAllpasses; ++allpass) { + uint8_t* const allpassSlot = + frame.allpass[channel][allpass] + allpassPos[allpass] * 4u; + const float allpassTap = LoadFloat(allpassSlot); + const float allpassFeedback = allpassTap * frame.allpassCoef; + const float allpassStore = allpassOut + allpassFeedback; + StoreFloat(allpassSlot, allpassStore); + const float allpassFeedforward = allpassStore * frame.allpassCoef; + allpassOut = allpassTap - allpassFeedforward; + } + + const float dampedOld = frame.damping * frame.lastLpfOut[channel]; + const float dampedNew = frame.oneMinusDamping * allpassOut; + const float damped = dampedNew + dampedOld; + frame.lastLpfOut[channel] = damped; + + uint8_t* const lastSlot = frame.lastAp[channel] + frame.lastApPos[channel] * 4u; + const float lastTap = LoadFloat(lastSlot); + const float lastFeedback = lastTap * frame.allpassCoef; + const float lastStore = damped + lastFeedback; + StoreFloat(lastSlot, lastStore); + const float lastFeedforward = lastStore * frame.allpassCoef; + const float lastOut = lastTap - lastFeedforward; + + const uint32_t nextLastPos = frame.lastApPos[channel] + 1u; + frame.lastApPos[channel] = + nextLastPos < frame.lastApLength[channel] ? nextLastPos : 0u; + + const float wet = lastOut * frame.wetScale; + mixed[channel] = wet + early; + } + + // Each output channel takes the other two through the shared cross-mix. + const float sum12 = mixed[1] + mixed[2]; + const float sum02 = mixed[0] + mixed[2]; + const float sum01 = mixed[0] + mixed[1]; + const float cross0 = sum12 * frame.mixScale; + const float cross1 = sum02 * frame.mixScale; + const float cross2 = sum01 * frame.mixScale; + const float out[kChannels] = { + mixed[0] + cross0, + mixed[1] + cross1, + mixed[2] + cross2, + }; + + for (uint32_t channel = 0; channel < kChannels; ++channel) { + const float mainSample = out[channel] * frame.mainGain; + StoreS32(frame.main[channel] + frameOffset, ConvertToIntegerWord(mainSample)); + if (frame.hasAuxOut) { + const float auxSample = out[channel] * frame.auxGain; + StoreS32(frame.auxOut[channel] + frameOffset, ConvertToIntegerWord(auxSample)); + } + } + + for (uint32_t tap = 0; tap < kEarlyTaps; ++tap) { + const uint32_t next = earlyPos[tap] + 1u; + earlyPos[tap] = next < frame.earlyLength ? next : 0u; + } + if (frame.preDelayLength != 0) { + const uint32_t next = preDelayPos + 1u; + preDelayPos = next < frame.preDelayLength ? next : 0u; + } + for (uint32_t comb = 0; comb < kCombs; ++comb) { + const uint32_t next = combPos[comb] + 1u; + combPos[comb] = next < frame.combLength[comb] ? next : 0u; + } + for (uint32_t allpass = 0; allpass < kAllpasses; ++allpass) { + const uint32_t next = allpassPos[allpass] + 1u; + allpassPos[allpass] = next < frame.allpassLength[allpass] ? next : 0u; + } + } + + // The guest rewrites these every sample; nothing can observe the intermediate + // values, so one store per field at the end is equivalent. + for (uint32_t tap = 0; tap < kEarlyTaps; ++tap) { + Memory::Write32(stateAddr + kFieldEarlyPos + tap * 4, earlyPos[tap]); + } + if (frame.preDelayLength != 0) { + Memory::Write32(stateAddr + kFieldPreDelayPos, preDelayPos); + } + for (uint32_t comb = 0; comb < kCombs; ++comb) { + Memory::Write32(stateAddr + kFieldCombPos + comb * 4, combPos[comb]); + } + for (uint32_t allpass = 0; allpass < kAllpasses; ++allpass) { + Memory::Write32(stateAddr + kFieldAllpassPos + allpass * 4, allpassPos[allpass]); + } + for (uint32_t channel = 0; channel < kChannels; ++channel) { + Memory::Write32(stateAddr + kFieldLastApPos + channel * 4, frame.lastApPos[channel]); + Memory::WriteFloat32(stateAddr + kFieldLastLpfOut + channel * 4, + static_cast(frame.lastLpfOut[channel])); + } +} + +// Every byte this callback may write, so a differential run can snapshot, replay +// and compare it. Ring lines are listed once per channel because the guest gives +// each channel its own buffer. +void CollectWritableRegions(const Frame& frame, + std::vector>& regions) { + constexpr size_t kFrameBytes = kSamplesPerFrame * sizeof(int32_t); + for (uint32_t channel = 0; channel < kChannels; ++channel) { + regions.emplace_back(frame.main[channel], kFrameBytes); + if (frame.hasAuxOut) { + regions.emplace_back(frame.auxOut[channel], kFrameBytes); + } + regions.emplace_back(frame.early[channel], + static_cast(frame.earlyLength) * sizeof(float)); + if (frame.preDelayLength != 0) { + regions.emplace_back(frame.preDelay[channel], + static_cast(frame.preDelayLength) * sizeof(float)); + } + for (uint32_t comb = 0; comb < kCombs; ++comb) { + regions.emplace_back(frame.comb[channel][comb], + static_cast(frame.combLength[comb]) * sizeof(float)); + } + for (uint32_t allpass = 0; allpass < kAllpasses; ++allpass) { + regions.emplace_back(frame.allpass[channel][allpass], + static_cast(frame.allpassLength[allpass]) * sizeof(float)); + } + regions.emplace_back(frame.lastAp[channel], + static_cast(frame.lastApLength[channel]) * sizeof(float)); + } +} + +bool VerificationEnabled() { + static const bool enabled = [] { + const char* value = std::getenv("MKW_VERIFY_AXFX_REVERB"); + return value != nullptr && value[0] == '1'; + }(); + return enabled; +} + +// Runs the translated body and this port over identical state and compares every +// byte either can write, so a wrong field offset surfaces as a loud mismatch +// instead of subtly wrong audio. Validation only; off unless the env var is set. +void RenderVerified(CpuContext* ctx, uint32_t stateAddr, Frame& frame) { + std::vector> regions; + CollectWritableRegions(frame, regions); + + uint8_t* const stateHost = ResolveGuestThreadRange(stateAddr, kStateStructBytes); + if (!stateHost) { + Render(stateAddr, frame); + return; + } + regions.emplace_back(stateHost, kStateStructBytes); + + std::vector> before(regions.size()); + for (size_t i = 0; i < regions.size(); ++i) { + before[i].assign(regions[i].first, regions[i].first + regions[i].second); + } + + const CpuContext savedContext = *ctx; + func_801284B4(ctx); + *ctx = savedContext; + + std::vector> expected(regions.size()); + for (size_t i = 0; i < regions.size(); ++i) { + expected[i].assign(regions[i].first, regions[i].first + regions[i].second); + std::memcpy(regions[i].first, before[i].data(), before[i].size()); + } + + Render(stateAddr, frame); + + static bool reported = false; + if (reported) { + return; + } + for (size_t i = 0; i < regions.size(); ++i) { + if (std::memcmp(regions[i].first, expected[i].data(), expected[i].size()) == 0) { + continue; + } + size_t offset = 0; + while (offset < expected[i].size() && regions[i].first[offset] == expected[i][offset]) { + ++offset; + } + reported = true; + RT_LOGF(RT_TAG_AUDIO, + "AXFXReverbHiExp native output diverges from the translated body: " + "region %zu of %zu, first differing byte %zu of %zu\n", + i, regions.size(), offset, expected[i].size()); + break; + } +} + +} // namespace ReverbHi } // namespace extern "C" void AXFXReverbStdExpCallback_8012b830(CpuContext* ctx) { @@ -373,3 +858,47 @@ extern "C" void AXFXReverbStdExpCallback_8012b830(CpuContext* ctx) { REGISTER_NATIVE_FUNCTION_AS(0x8012B830, AXFXReverbStdExpCallback_8012b830, "AXFXReverbStdExpCallback_8012b830"); + +extern "C" void AXFXReverbHiExpCallback_801284b4(CpuContext* ctx) { + if (!ctx) { + return; + } + + const uint32_t buffersAddr = ctx->gpr[3]; + const uint32_t stateAddr = ctx->gpr[4]; + + uint32_t flags = 0; + try { + flags = Memory::Read32(stateAddr + ReverbHi::kFieldFlags); + } catch (const Memory::AccessViolation&) { + func_801284B4(ctx); + return; + } + if (flags != 0) { + // Reset request: the guest clears the "in progress" bit and skips the + // frame entirely. + Memory::Write32(stateAddr + ReverbHi::kFieldFlags, flags & ~2u); + return; + } + + ReverbHi::Frame frame; + bool built = false; + try { + built = ReverbHi::BuildFrame(buffersAddr, stateAddr, frame); + } catch (const Memory::AccessViolation&) { + built = false; + } + if (!built) { + func_801284B4(ctx); + return; + } + + if (ReverbHi::VerificationEnabled()) { + ReverbHi::RenderVerified(ctx, stateAddr, frame); + } else { + ReverbHi::Render(stateAddr, frame); + } +} + +REGISTER_NATIVE_FUNCTION_AS(0x801284B4, AXFXReverbHiExpCallback_801284b4, + "AXFXReverbHiExpCallback_801284b4");