Files
wiicompiled/runtime/src/hle/audio/ax_mix.cpp
T
patchzyy 1d9cae387a fix ntsc-u crash on courses with torches (#346)
* fix ntsc-u map for vtable-only functions

* add --race and --race-cpu boot options

* add MKW_PERF_STATS frame busy timing and --race-cpu

* fix race seeds for --race-cpu benchmarks

* bind interrupt natives at compile time without register guards

* inline fast path for unguarded indirect calls

* batch gx worker head publishes

* split cold paths out of translated functions

* cache observed gx worker tail on the producer

* print game id in fatal log

* fix ntsc-u map verdict counts
2026-10-24 13:48:29 +02:00

1582 lines
62 KiB
C++

#include "ax_dsp.h"
#include "ax_internal.h"
#include "abi_bridge.h"
#include "perf_stats.h"
#include "audio_backend.h"
#include "ax_mix_kernels.h"
#include "isa/big_endian.h"
#include "memory.h"
#include "ppc_runtime.h"
#include "runtime_config.h"
#include "runtime_log.h"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <string>
#include <thread>
namespace AxDspHle {
namespace {
std::filesystem::path FindDspCoefficientRom() {
if (const auto executableDirectory = RuntimeConfigFile::ExecutableDirectory()) {
const auto adjacent = *executableDirectory / "dsp_coef.bin";
if (std::filesystem::is_regular_file(adjacent)) {
return adjacent;
}
}
for (auto base = std::filesystem::current_path(); !base.empty();) {
const auto sourceTreeAsset = base / "runtime" / "assets" / "dsp" / "dsp_coef.bin";
if (std::filesystem::is_regular_file(sourceTreeAsset)) {
return sourceTreeAsset;
}
const auto parent = base.parent_path();
if (parent == base) {
break;
}
base = parent;
}
throw std::runtime_error("Missing bundled Wii DSP coefficient ROM (dsp_coef.bin)");
}
class AXWii {
public:
AXWii() {
m_lastMainVolume = 0x8000;
m_lastAuxVolumes.fill(0x8000);
}
~AXWii() { ShutdownMixWorker(); }
void Initialize() {
// Every entry point that resets or reconfigures mix state joins the
// worker first: the mix owns those members outright while it runs.
JoinMix();
RefreshMixMemoryMap();
std::lock_guard<std::mutex> lock(m_mutex);
m_mixWorkerEnabled = RuntimeConfigFile::AudioMixWorkerEnabled(true);
MarkDspInitialized();
ResetDspTaskGlobals();
m_mailState = MailState::WaitingForCmdListSize;
m_cmdListSize = 0;
m_toDspBusy = false;
m_cpuMails.clear();
m_compressorPos = 0;
m_deferredResumeCallbacks = 0;
// Both layouts are re-derived from the ucode CRC by ConfigureFromTask;
// until one arrives, the PB layout is detected per parameter block and
// the command layout is probed per command list.
m_pbLayout = PBLayout::Unknown;
m_commandLayout = AXCommandLayout::Auto;
m_ucodeCrc = 0;
m_auxOutCount = 0;
m_auxInCount = 0;
m_deferredAux = false;
LoadResamplingCoefficients();
PushMail(kDspInit);
}
void Stop() {
JoinMix();
std::lock_guard<std::mutex> lock(m_mutex);
m_mailState = MailState::WaitingForCmdListSize;
m_cmdListSize = 0;
m_toDspBusy = false;
m_cpuMails.clear();
m_lastCpuMail = 0;
m_compressorPos = 0;
m_deferredResumeCallbacks = 0;
m_ucodeCrc = 0;
}
void ConfigureFromTask(uint32_t taskPtr) {
// ConfigureFromTaskLocked rewrites the ucode CRC and the PB/command
// layouts, all of which the mix reads.
JoinMix();
std::lock_guard<std::mutex> lock(m_mutex);
ConfigureFromTaskLocked(taskPtr);
}
bool MixBusy() {
if (!m_mixThread.joinable()) {
return false;
}
std::lock_guard<std::mutex> lock(m_mixMutex);
return m_mixPending || m_mixBusy;
}
void JoinMix() {
if (m_mixThread.joinable()) {
PerfStats::WaitScope wait;
std::unique_lock<std::mutex> lock(m_mixMutex);
m_mixIdle.wait(lock, [this] { return !m_mixPending && !m_mixBusy; });
}
PublishAuxOutputs();
}
void ShutdownMixWorker() {
if (!m_mixThread.joinable()) {
return;
}
{
std::lock_guard<std::mutex> lock(m_mixMutex);
m_mixStop = true;
}
m_mixWake.notify_all();
m_mixThread.join();
}
void SetMixWorkerEnabled(bool enabled) {
std::lock_guard<std::mutex> lock(m_mutex);
m_mixWorkerEnabled = enabled;
}
void SendMail(uint32_t mail) {
std::lock_guard<std::mutex> lock(m_mutex);
HandleMail(mail);
m_toDspBusy = false;
}
void QueueResumeCallback() {
std::lock_guard<std::mutex> lock(m_mutex);
++m_deferredResumeCallbacks;
}
void ServiceDeferredCallbacks() {
while (true) {
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_deferredResumeCallbacks == 0) {
return;
}
--m_deferredResumeCallbacks;
}
InvokeAxTaskCallback(0x2cu, false);
}
}
uint32_t CheckMailToDSP() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_toDspBusy ? 1u : 0u;
}
uint32_t CheckMailFromDSP() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_cpuMails.empty() ? 0u : 1u;
}
uint32_t ReadMailFromDSP() {
std::lock_guard<std::mutex> lock(m_mutex);
if (m_cpuMails.empty()) {
return m_lastCpuMail & 0x7fffffffu;
}
m_lastCpuMail = m_cpuMails.front();
m_cpuMails.pop_front();
return m_lastCpuMail;
}
private:
// What DispatchCommandList has to know about a command list before it is
// allowed to leave the guest thread.
struct CommandListScan {
uint32_t outputCommands = 0;
uint32_t auxInputCount = 0;
// Three blocks per MIX_AUX* (x3) plus four per UPL_AUX* (x2) = 17.
std::array<uint32_t, 24> auxInputAddrs{};
};
struct AuxBlock {
uint32_t addr = 0;
std::array<int32_t, kAxSamplesPerFrame> data{};
};
enum Cmd : uint16_t {
CMD_SETUP = 0x00,
CMD_ADD_TO_LR = 0x01,
CMD_SUB_TO_LR = 0x02,
CMD_ADD_SUB_TO_LR = 0x03,
CMD_PROCESS = 0x04,
CMD_MIX_AUXA = 0x05,
CMD_MIX_AUXB = 0x06,
CMD_MIX_AUXC = 0x07,
CMD_UPL_AUXA_MIX_LRSC = 0x08,
CMD_UPL_AUXB_MIX_LRSC = 0x09,
CMD_COMPRESSOR = 0x0a,
CMD_OUTPUT = 0x0b,
CMD_OUTPUT_DPL2 = 0x0c,
CMD_WM_OUTPUT = 0x0d,
CMD_END = 0x0e,
};
static constexpr uint16_t kCmdSetPbAddressOld = 0x04;
struct DecodedCommand {
Cmd cmd = CMD_SETUP;
bool setsPbAddress = false;
};
static DecodedCommand DecodeCommand(uint16_t raw, AXCommandLayout layout) {
if (layout != AXCommandLayout::Old) {
return {static_cast<Cmd>(raw), false};
}
if (raw == kCmdSetPbAddressOld) {
return {CMD_SETUP, true};
}
return {static_cast<Cmd>(raw > kCmdSetPbAddressOld ? raw - 1u : raw), false};
}
// The old layout runs the PB chain the previous SET_PB_ADDRESS stashed, and
// ProcessPBList applies the old per-millisecond PB update walk for it.
static bool UsesStashedPbAddress(AXCommandLayout layout) {
return layout == AXCommandLayout::Old;
}
// Only the plain New encoding puts a volume halfword in front of OUTPUT's
// two addresses; Old and NewNoOutputVolume both imply 0x8000.
static bool OutputCarriesVolume(AXCommandLayout layout) {
return layout != AXCommandLayout::Old && layout != AXCommandLayout::NewNoOutputVolume;
}
void PushMail(uint32_t mail) {
m_cpuMails.push_back(mail);
}
void HandleMail(uint32_t mail) {
switch (m_mailState) {
case MailState::WaitingForCmdListSize:
if ((mail & kMailCmdListMask) == kMailCmdList) {
m_cmdListSize = mail & 0xffffu;
m_mailState = MailState::WaitingForCmdListAddress;
} else if ((mail & 0xffff0000u) == kTaskMailToDsp) {
HandleTaskMail(mail);
}
break;
case MailState::WaitingForCmdListAddress:
// The previous mix owns m_cmdList, the mix buses and the PB chain
// until it finishes. In practice this join is free
JoinMix();
CopyCmdList(mail, m_cmdListSize);
DispatchCommandList();
m_cmdListSize = 0;
// yield mail and the deferred resume callback are still raised synchronously at mail time,
PushMail(kDspYield);
++m_deferredResumeCallbacks;
m_mailState = MailState::WaitingForNextTask;
break;
case MailState::WaitingForNextTask:
if ((mail & kMailCmdListMask) == kMailCmdList) {
m_cmdListSize = mail & 0xffffu;
m_mailState = MailState::WaitingForCmdListAddress;
} else {
HandleTaskMail(mail);
}
break;
}
}
void HandleTaskMail(uint32_t mail) {
switch (mail) {
case kMailResume:
PushMail(kDspResume);
m_mailState = MailState::WaitingForCmdListSize;
break;
case kMailNewUCode:
case kMailContinue:
m_mailState = MailState::WaitingForCmdListSize;
break;
case kMailReset:
PushMail(kDspDone);
m_mailState = MailState::WaitingForCmdListSize;
break;
default:
m_mailState = MailState::WaitingForCmdListSize;
break;
}
}
void CopyCmdList(uint32_t addr, uint32_t sizeWords) {
if (sizeWords > m_cmdList.size()) {
// Truncating a command list drops whole commands
if (!m_loggedCmdListTruncated.exchange(true, std::memory_order_relaxed)) {
RT_LOGF(RT_TAG_AUDIO,
"command list of %u words exceeds the %zu-word buffer; truncating\n",
sizeWords, m_cmdList.size());
std::fflush(stderr);
}
sizeWords = static_cast<uint32_t>(m_cmdList.size());
}
for (uint32_t i = 0; i < sizeWords; ++i) {
m_cmdList[i] = Memory::Read16(addr + i * 2);
}
}
// Everything that must be observed at mail time happens here, before the mix leaves the
// guest thread: the DSP sync mails OUTPUT commands raise (so the mail sequence doesn't
// depend on where the mix runs)
void DispatchCommandList() {
AXCommandLayout layout = m_commandLayout;
if (layout == AXCommandLayout::Auto) {
const bool validNew = ScanCommandList(AXCommandLayout::New, nullptr);
const bool validNewNoOutputVolume = ScanCommandList(AXCommandLayout::NewNoOutputVolume, nullptr);
const bool validOld = ScanCommandList(AXCommandLayout::Old, nullptr);
layout = validNew ? AXCommandLayout::New :
(validNewNoOutputVolume ? AXCommandLayout::NewNoOutputVolume :
(validOld ? AXCommandLayout::Old : AXCommandLayout::New));
}
CommandListScan scan{};
ScanCommandList(layout, &scan);
for (uint32_t i = 0; i < scan.outputCommands; ++i) {
PushMail(kDspSync);
}
const bool deferred = m_mixWorkerEnabled && EnsureMixWorker();
m_deferredAux = deferred;
m_auxInCount = 0;
if (deferred) {
for (uint32_t i = 0; i < scan.auxInputCount && m_auxInCount < m_auxIn.size(); ++i) {
AuxBlock& block = m_auxIn[m_auxInCount++];
block.addr = scan.auxInputAddrs[i];
ReadGuestS32Buffer(block.addr, block.data.data(), kAxSamplesPerFrame);
}
}
m_mixLayout = layout;
m_mixCmdListSize = m_cmdListSize;
if (deferred) {
{
std::lock_guard<std::mutex> lock(m_mixMutex);
m_mixPending = true;
}
m_mixWake.notify_one();
return;
}
ExecuteCommandList(m_mixLayout, m_mixCmdListSize);
}
bool CanReadWords(uint32_t idx, uint32_t count) const {
return idx <= m_cmdListSize && count <= m_cmdListSize - idx;
}
// Walks the command list for a candidate layout
bool ScanCommandList(AXCommandLayout layout, CommandListScan* scan) const {
uint32_t idx = 0;
bool end = false;
auto addAuxInput = [&](uint32_t addr, uint32_t blocks) {
if (!scan) {
return;
}
for (uint32_t block = 0; block < blocks; ++block) {
if (scan->auxInputCount >= scan->auxInputAddrs.size()) {
return;
}
scan->auxInputAddrs[scan->auxInputCount++] =
addr + block * kAxSamplesPerFrame * static_cast<uint32_t>(sizeof(int32_t));
}
};
auto wordAddress = [&](uint32_t base) {
return Hilo(m_cmdList[base], m_cmdList[base + 1]);
};
while (!end) {
if (!CanReadWords(idx, 1)) {
return false;
}
const uint16_t cmd = m_cmdList[idx++];
const uint32_t operandBase = idx;
auto skip = [&](uint32_t words) {
if (!CanReadWords(idx, words)) {
return false;
}
idx += words;
return true;
};
const DecodedCommand decoded = DecodeCommand(cmd, layout);
if (decoded.setsPbAddress) {
if (!skip(2)) return false;
continue;
}
switch (decoded.cmd) {
case CMD_SETUP:
case CMD_ADD_TO_LR:
case CMD_SUB_TO_LR:
case CMD_ADD_SUB_TO_LR:
if (!skip(2)) return false;
break;
case CMD_PROCESS:
if (!UsesStashedPbAddress(layout) && !skip(2)) return false;
break;
case CMD_MIX_AUXA:
case CMD_MIX_AUXB:
case CMD_MIX_AUXC:
if (!skip(5)) return false;
// volume, write address, read address; the mix reads three
// consecutive 96-word blocks from the read address.
addAuxInput(wordAddress(operandBase + 3), 3);
break;
case CMD_UPL_AUXA_MIX_LRSC:
case CMD_UPL_AUXB_MIX_LRSC:
if (!skip(13)) return false;
// volume then six addresses; [2..5] are read back.
for (uint32_t slot = 2; slot < 6; ++slot) {
addAuxInput(wordAddress(operandBase + 1 + slot * 2), 1);
}
break;
case CMD_COMPRESSOR:
if (!skip(4)) return false;
break;
case CMD_OUTPUT:
case CMD_OUTPUT_DPL2:
if (!skip(OutputCarriesVolume(layout) ? 5 : 4)) return false;
if (scan) ++scan->outputCommands;
break;
case CMD_WM_OUTPUT:
if (!skip(8)) return false;
break;
case CMD_END:
end = true;
break;
default:
return false;
}
}
return idx <= m_cmdListSize;
}
void ExecuteCommandList(AXCommandLayout layout, uint32_t cmdListSize) {
uint32_t idx = 0;
uint32_t oldPbAddr = 0;
const bool newFilter = m_ucodeCrc != 0 ? UCodeUsesNewFilter(m_ucodeCrc) : layout != AXCommandLayout::Old;
bool end = false;
m_auxOutCount = 0;
while (!end && idx < cmdListSize) {
const uint16_t cmd = m_cmdList[idx++];
uint16_t volume = 0;
uint16_t frames = 0;
uint32_t addr = 0;
uint32_t addr2 = 0;
const DecodedCommand decoded = DecodeCommand(cmd, layout);
if (decoded.setsPbAddress) {
oldPbAddr = ReadAddress(idx);
continue;
}
switch (decoded.cmd) {
case CMD_SETUP:
addr = ReadAddress(idx);
SetupProcessing(addr);
break;
case CMD_ADD_TO_LR:
case CMD_SUB_TO_LR:
addr = ReadAddress(idx);
AddToLR(addr, decoded.cmd == CMD_SUB_TO_LR);
break;
case CMD_ADD_SUB_TO_LR:
addr = ReadAddress(idx);
AddSubToLR(addr);
break;
case CMD_PROCESS:
if (UsesStashedPbAddress(layout)) {
ProcessPBList(oldPbAddr, newFilter, true);
} else {
addr = ReadAddress(idx);
ProcessPBList(addr, newFilter, false);
}
break;
case CMD_MIX_AUXA:
case CMD_MIX_AUXB:
case CMD_MIX_AUXC:
volume = m_cmdList[idx++];
addr = ReadAddress(idx);
addr2 = ReadAddress(idx);
MixAUXSamples(decoded.cmd - CMD_MIX_AUXA, addr, addr2, volume);
break;
case CMD_UPL_AUXA_MIX_LRSC:
case CMD_UPL_AUXB_MIX_LRSC: {
volume = m_cmdList[idx++];
std::array<uint32_t, 6> addresses{};
for (uint32_t& out : addresses) {
out = ReadAddress(idx);
}
UploadAUXMixLRSC(decoded.cmd == CMD_UPL_AUXB_MIX_LRSC ? 1 : 0, addresses.data(), volume);
break;
}
case CMD_COMPRESSOR:
volume = m_cmdList[idx++];
frames = m_cmdList[idx++];
addr = ReadAddress(idx);
RunCompressor(volume, frames, addr);
break;
case CMD_OUTPUT:
case CMD_OUTPUT_DPL2:
volume = OutputCarriesVolume(layout) ? m_cmdList[idx++] : 0x8000u;
addr = ReadAddress(idx);
addr2 = ReadAddress(idx);
OutputSamples(addr2, addr, volume, decoded.cmd == CMD_OUTPUT_DPL2);
break;
case CMD_WM_OUTPUT: {
std::array<uint32_t, 4> addresses{};
for (uint32_t& out : addresses) {
out = ReadAddress(idx);
}
OutputWMSamples(addresses.data());
break;
}
case CMD_END:
end = true;
break;
default:
ReportUnknownCommand(cmd, idx - 1);
end = true;
break;
}
}
}
void ReportUnknownCommand(uint16_t cmd, uint32_t word) {
if (m_loggedUnknownCommand.exchange(true, std::memory_order_relaxed)) {
return;
}
RT_LOGF(RT_TAG_AUDIO,
"unknown AX command 0x%04X at word %u; abandoning the command list\n",
cmd, word);
std::fflush(stderr);
}
uint32_t ReadAddress(uint32_t& idx) const {
const uint16_t hi = m_cmdList[idx++];
const uint16_t lo = m_cmdList[idx++];
return Hilo(hi, lo);
}
// Head-of-frame pointer for every bus, in AxBus order. This is the only
// place the 20 buses are enumerated.
AXBuffers MixBuffers() {
AXBuffers buffers{};
for (size_t i = 0; i < kAxRegularBusCount; ++i) {
buffers[i] = m_bus[i].data();
}
for (size_t i = 0; i < kAxWiimoteBusCount; ++i) {
buffers[kAxRegularBusCount + i] = m_wmBus[i].data();
}
return buffers;
}
void SetupProcessing(uint32_t initAddr) {
const AXBuffers buffers = MixBuffers();
for (size_t i = 0; i < buffers.size(); ++i) {
const uint32_t value = (MixRead16(initAddr + static_cast<uint32_t>(i * 6)) << 16) |
MixRead16(initAddr + static_cast<uint32_t>(i * 6 + 2));
const int16_t delta = static_cast<int16_t>(MixRead16(initAddr + static_cast<uint32_t>(i * 6 + 4)));
const int count = (i < kAxRegularBusCount ? 32 : 6) * 3;
if (value == 0) {
std::fill(buffers[i], buffers[i] + count, 0);
} else {
const int32_t start = static_cast<int32_t>(value);
for (int j = 0; j < count; ++j) {
buffers[i][j] = start + j * delta;
}
}
}
}
void AddToLR(uint32_t addr, bool neg) {
for (uint32_t i = 0; i < kAxSamplesPerFrame; ++i) {
int32_t value = static_cast<int32_t>(MixRead32(addr + i * 4));
if (neg) {
value = -value;
}
m_bus[kBusMainL][i] += value;
m_bus[kBusMainR][i] += value;
}
}
void AddSubToLR(uint32_t addr) {
for (uint32_t i = 0; i < kAxSamplesPerFrame; ++i) {
m_bus[kBusMainL][i] += static_cast<int32_t>(MixRead32(addr + i * 4));
}
for (uint32_t i = 0; i < kAxSamplesPerFrame; ++i) {
m_bus[kBusMainR][i] -= static_cast<int32_t>(MixRead32(addr + (kAxSamplesPerFrame + i) * 4));
}
}
static AXMixControl ConvertMixerControl(uint32_t mixerControl) {
uint32_t ret = 0;
if (mixerControl & 0x00000001) ret |= MIX_MAIN_L;
if (mixerControl & 0x00000002) ret |= MIX_MAIN_R;
if (mixerControl & 0x00000004) ret |= MIX_MAIN_L | MIX_MAIN_R | MIX_MAIN_L_RAMP | MIX_MAIN_R_RAMP;
if (mixerControl & 0x00000008) ret |= MIX_MAIN_S;
if (mixerControl & 0x00000010) ret |= MIX_MAIN_S | MIX_MAIN_S_RAMP;
if (mixerControl & 0x00010000) ret |= MIX_AUXA_L;
if (mixerControl & 0x00020000) ret |= MIX_AUXA_R;
if (mixerControl & 0x00040000) ret |= MIX_AUXA_L | MIX_AUXA_R | MIX_AUXA_L_RAMP | MIX_AUXA_R_RAMP;
if (mixerControl & 0x00080000) ret |= MIX_AUXA_S;
if (mixerControl & 0x00100000) ret |= MIX_AUXA_S | MIX_AUXA_S_RAMP;
if (mixerControl & 0x00200000) ret |= MIX_AUXB_L;
if (mixerControl & 0x00400000) ret |= MIX_AUXB_R;
if (mixerControl & 0x00800000) ret |= MIX_AUXB_L | MIX_AUXB_R | MIX_AUXB_L_RAMP | MIX_AUXB_R_RAMP;
if (mixerControl & 0x01000000) ret |= MIX_AUXB_S;
if (mixerControl & 0x02000000) ret |= MIX_AUXB_S | MIX_AUXB_S_RAMP;
if (mixerControl & 0x04000000) ret |= MIX_AUXC_L;
if (mixerControl & 0x08000000) ret |= MIX_AUXC_R;
if (mixerControl & 0x10000000) ret |= MIX_AUXC_L | MIX_AUXC_R | MIX_AUXC_L_RAMP | MIX_AUXC_R_RAMP;
if (mixerControl & 0x20000000) ret |= MIX_AUXC_S;
if (mixerControl & 0x40000000) ret |= MIX_AUXC_S | MIX_AUXC_S_RAMP;
return static_cast<AXMixControl>(ret);
}
static void GenerateVolumeRamp(uint16_t* output, uint16_t from, uint16_t to, size_t count) {
float current = static_cast<float>(from);
const float delta = (static_cast<float>(to) - static_cast<float>(from)) / static_cast<float>(count);
for (size_t i = 0; i < count; ++i) {
current += delta;
output[i] = static_cast<uint16_t>(current);
}
}
void ProcessPBList(uint32_t pbAddr, bool newFilter, bool oldAxLayout) {
uint32_t guard = 0;
while (pbAddr && guard++ < 256) {
AXPBWii pb{};
ReadPB(pbAddr, pb);
AXBuffers buffers = MixBuffers();
const AXMixControl control = ConvertMixerControl(Hilo(pb.mixer_control_hi, pb.mixer_control_lo));
if (oldAxLayout && (pb.updates.num_updates[0] | pb.updates.num_updates[1] | pb.updates.num_updates[2]) != 0) {
const auto updates = LoadPBUpdates(pb);
for (uint32_t ms = 0; ms < 3; ++ms) {
ApplyPBUpdatesForMs(pb, updates, ms);
ProcessVoice(pb, buffers, 32, control, BaseCoefficients(), newFilter);
AdvanceBuffers(buffers, 32, 6);
}
} else {
ProcessVoice(pb, buffers, kAxSamplesPerFrame, control, BaseCoefficients(), newFilter);
}
WritePB(pbAddr, pb);
pbAddr = Hilo(pb.next_pb_hi, pb.next_pb_lo);
}
}
// Four consecutive 0x200-halfword polyphase banks; only the voice resample selects a
// bank (VoiceCoefficients). Wiimote downsample always uses bank 0 at its fixed ratio.
const int16_t* BaseCoefficients() const {
return m_hasCoeffs ? m_coeffs.data() : nullptr;
}
// Mirrors GetInputSamples' coeffs += pb.coef_select * 0x200, but bounds-checked since
// coef_select is a guest halfword; a corrupt PB degrades to linear interpolation instead
// of an out-of-bounds read.
static const int16_t* VoiceCoefficients(const int16_t* base, uint16_t coefSelect) {
if (base == nullptr) {
return nullptr;
}
const uint32_t offset = static_cast<uint32_t>(coefSelect) * 0x200u;
if (offset + 0x200u > kResamplingCoefficientCount) {
return nullptr;
}
return base + offset;
}
void LoadResamplingCoefficients() {
if (m_triedLoadCoeffs) {
return;
}
m_triedLoadCoeffs = true;
const auto path = FindDspCoefficientRom();
std::ifstream stream(path, std::ios::binary | std::ios::ate);
if (!stream || stream.tellg() != static_cast<std::streamoff>(m_coeffs.size() * 2)) {
throw std::runtime_error("Bundled Wii DSP coefficient ROM has an invalid size: " +
RuntimeConfigFile::PathToUtf8(path));
}
stream.seekg(0);
std::array<uint8_t, kResamplingCoefficientCount * 2> bytes{};
if (!stream.read(reinterpret_cast<char*>(bytes.data()), bytes.size())) {
throw std::runtime_error("Failed to read bundled Wii DSP coefficient ROM: " +
RuntimeConfigFile::PathToUtf8(path));
}
for (size_t i = 0; i < m_coeffs.size(); ++i) {
const uint16_t word = static_cast<uint16_t>(bytes[i * 2]) << 8 |
static_cast<uint16_t>(bytes[i * 2 + 1]);
m_coeffs[i] = static_cast<int16_t>(word);
}
m_hasCoeffs = true;
}
static std::array<PBUpdate, 32> LoadPBUpdates(const AXPBWii& pb) {
std::array<PBUpdate, 32> updates{};
const uint32_t addr = Hilo(pb.updates.data_hi, pb.updates.data_lo);
if (addr == 0) {
return updates;
}
for (size_t i = 0; i < updates.size(); ++i) {
updates[i].pb_offset = MixRead16(addr + static_cast<uint32_t>(i * sizeof(PBUpdate)));
updates[i].new_value = MixRead16(addr + static_cast<uint32_t>(i * sizeof(PBUpdate) + sizeof(uint16_t)));
}
return updates;
}
void ApplyPBUpdatesForMs(AXPBWii& pb, const std::array<PBUpdate, 32>& updates, uint32_t ms) {
uint32_t start = 0;
for (uint32_t i = 0; i < ms; ++i) {
start += pb.updates.num_updates[i];
}
const uint32_t count = pb.updates.num_updates[ms];
uint16_t* words = reinterpret_cast<uint16_t*>(&pb);
constexpr uint32_t wordCount = sizeof(AXPBWii) / sizeof(uint16_t);
for (uint32_t i = start; i < start + count && i < updates.size(); ++i) {
if (updates[i].pb_offset >= wordCount) {
continue;
}
words[updates[i].pb_offset] = updates[i].new_value;
}
}
static void AdvanceBuffers(AXBuffers& buffers, uint32_t regular, uint32_t wiimote) {
for (size_t i = 0; i < kAxRegularBusCount; ++i) {
buffers[i] += regular;
}
for (size_t i = kAxRegularBusCount; i < buffers.size(); ++i) {
buffers[i] += wiimote;
}
}
// Both directions take their offsets in the same order - guest side first,
// then host side - so a mirrored read/write pair reads the same way round.
static void CopyGuestToStruct(uint32_t addr, size_t guestOffset, char* dst, size_t structOffset,
size_t byteCount) {
if (const uint8_t* guest =
MixResolveRange(addr + static_cast<uint32_t>(guestOffset), byteCount)) {
for (size_t i = 0; i < byteCount; i += sizeof(uint16_t)) {
uint16_t value = 0;
std::memcpy(&value, guest + i, sizeof(value));
*reinterpret_cast<uint16_t*>(dst + structOffset + i) = MemoryInline::ByteSwap16(value);
}
return;
}
for (size_t i = 0; i < byteCount; i += sizeof(uint16_t)) {
*reinterpret_cast<uint16_t*>(dst + structOffset + i) =
MixRead16(addr + static_cast<uint32_t>(guestOffset + i));
}
}
static void CopyStructToGuest(uint32_t addr, size_t guestOffset, const char* src, size_t structOffset,
size_t byteCount) {
if (uint8_t* guest =
MixResolveRange(addr + static_cast<uint32_t>(guestOffset), byteCount)) {
for (size_t i = 0; i < byteCount; i += sizeof(uint16_t)) {
BigEndian::Write16(
guest + i, *reinterpret_cast<const uint16_t*>(src + structOffset + i));
}
return;
}
for (size_t i = 0; i < byteCount; i += sizeof(uint16_t)) {
MixWrite16(addr + static_cast<uint32_t>(guestOffset + i),
*reinterpret_cast<const uint16_t*>(src + structOffset + i));
}
}
// One run of halfwords that is present in both the guest parameter block and
// AXPBWii, possibly at different offsets because the guest layout omits a
// field this struct carries.
struct PBSegment {
size_t guestOffset;
size_t structOffset;
size_t byteCount;
};
// The segmentation is a property of the ucode's PB layout, not of the
// direction of travel, so reads and writes share this one table. Returns the
// number of segments written into `out`.
static size_t PBSegmentsForLayout(PBLayout layout, std::array<PBSegment, 3>& out) {
constexpr size_t pbBytes = sizeof(AXPBWii);
constexpr size_t updatesBegin = offsetof(AXPBWii, updates);
constexpr size_t updatesEnd = offsetof(AXPBWii, updates) + sizeof(PBUpdatesWii);
constexpr size_t gapBegin = offsetof(AXPBWii, hpf) + sizeof(PBHighPassFilter);
constexpr size_t gapEnd = offsetof(AXPBWii, biquad) + sizeof(PBBiquadFilter);
switch (layout) {
case PBLayout::SkipBiquadGapOnly:
out[0] = {0, 0, gapBegin};
out[1] = {gapBegin, gapEnd, pbBytes - gapEnd};
return 2;
case PBLayout::SkipUpdatesAndBiquadGap:
out[0] = {0, 0, updatesBegin};
out[1] = {updatesBegin, updatesEnd, gapBegin - updatesEnd};
out[2] = {gapBegin, gapEnd, pbBytes - gapEnd};
return 3;
case PBLayout::SkipUpdatesOnly:
case PBLayout::Unknown:
default:
// Unknown means detection has not settled yet. ReadPB deliberately
// falls back to the SkipUpdatesOnly shape so it has something to
// score candidates against; WritePBWithLayout refuses instead,
// because guessing a segmentation on the way back out would corrupt
// the guest's parameter block.
out[0] = {0, 0, updatesBegin};
out[1] = {updatesBegin, updatesEnd, pbBytes - updatesEnd};
return 2;
}
}
static void ReadPBWithLayout(uint32_t addr, AXPBWii& pb, PBLayout layout) {
std::memset(&pb, 0, sizeof(pb));
char* dst = reinterpret_cast<char*>(&pb);
std::array<PBSegment, 3> segments{};
const size_t count = PBSegmentsForLayout(layout, segments);
for (size_t i = 0; i < count; ++i) {
CopyGuestToStruct(addr, segments[i].guestOffset, dst, segments[i].structOffset,
segments[i].byteCount);
}
}
static void WritePBWithLayout(uint32_t addr, const AXPBWii& pb, PBLayout layout) {
if (layout == PBLayout::Unknown) {
// See PBSegmentsForLayout: no segmentation is known to be correct,
// so nothing is written back. WritePB refuses earlier for the same
// reason; this is the guard that makes it safe on its own.
return;
}
const char* src = reinterpret_cast<const char*>(&pb);
std::array<PBSegment, 3> segments{};
const size_t count = PBSegmentsForLayout(layout, segments);
for (size_t i = 0; i < count; ++i) {
CopyStructToGuest(addr, segments[i].guestOffset, src, segments[i].structOffset,
segments[i].byteCount);
}
}
static bool LooksLikeMainRamPointer(uint32_t addr) {
return addr == 0 || MixContains(addr, sizeof(uint16_t) * 8);
}
static bool LooksLikeAxSampleFormat(uint16_t format) {
return format == 0 || format == 0x19 || format == 0x0a;
}
static int ScorePBLayout(uint32_t addr, const AXPBWii& pb) {
int score = 0;
const uint32_t next = Hilo(pb.next_pb_hi, pb.next_pb_lo);
const uint32_t self = Hilo(pb.this_pb_hi, pb.this_pb_lo);
const uint32_t ratio = Hilo(pb.src.ratio_hi, pb.src.ratio_lo);
const uint32_t loop = Hilo(pb.audio_addr.loop_addr_hi, pb.audio_addr.loop_addr_lo);
const uint32_t end = Hilo(pb.audio_addr.end_addr_hi, pb.audio_addr.end_addr_lo);
const uint32_t current = Hilo(pb.audio_addr.cur_addr_hi, pb.audio_addr.cur_addr_lo);
score += LooksLikeMainRamPointer(next) ? 8 : -16;
score += self == addr ? 12 : (LooksLikeMainRamPointer(self) ? 2 : -8);
score += pb.running <= 1 ? 8 : -16;
score += pb.running == 1 ? 16 : 0;
score += pb.is_stream <= 1 ? 3 : -4;
score += pb.src_type <= 2 ? 8 : -10;
score += LooksLikeAxSampleFormat(pb.audio_addr.sample_format) ? 14 : -12;
score += ratio > 0 && ratio <= 0x00040000u ? 12 : (pb.running == 1 ? -12 : 0);
score += (loop & 0xfc000000u) == 0 ? 5 : -5;
score += (end & 0xfc000000u) == 0 ? 5 : -5;
score += (current & 0xc0000000u) == 0 || (current & 0xc0000000u) == 0x80000000u ? 5 : -5;
score += end >= loop ? 4 : -4;
return score;
}
PBLayout DetectPBLayout(uint32_t addr) {
struct Candidate {
PBLayout layout;
AXPBWii pb;
int score;
};
std::array<Candidate, 3> candidates{{
{PBLayout::SkipUpdatesOnly, {}, 0},
{PBLayout::SkipUpdatesAndBiquadGap, {}, 0},
{PBLayout::SkipBiquadGapOnly, {}, 0},
}};
Candidate* best = nullptr;
for (Candidate& candidate : candidates) {
ReadPBWithLayout(addr, candidate.pb, candidate.layout);
candidate.score = ScorePBLayout(addr, candidate.pb);
if (candidate.pb.running == 1 && (!best || candidate.score > best->score)) {
best = &candidate;
}
}
if (!best || best->score < 40) {
return PBLayout::Unknown;
}
return best->layout;
}
void ReadPB(uint32_t addr, AXPBWii& pb) {
PBLayout layout = m_pbLayout;
if (layout == PBLayout::Unknown) {
layout = DetectPBLayout(addr);
if (layout != PBLayout::Unknown) {
m_pbLayout = layout;
} else {
layout = PBLayout::SkipUpdatesOnly;
}
}
ReadPBWithLayout(addr, pb, layout);
}
void ConfigureFromTaskLocked(uint32_t taskPtr) {
if (taskPtr == 0) {
return;
}
const uint32_t iramAddr = ReadGuestU32OrZero(taskPtr + 0x0cu);
const uint32_t iramLength = ReadGuestU32OrZero(taskPtr + 0x10u);
const uint32_t crc = HashEctorGuest(iramAddr, iramLength);
if (crc == 0) {
return;
}
m_ucodeCrc = crc;
const PBLayout derivedPBLayout = PBLayoutForUCode(crc);
if (derivedPBLayout != PBLayout::Unknown) {
m_pbLayout = derivedPBLayout;
}
m_commandLayout = CommandLayoutForUCode(crc);
}
// PB write-back goes straight into guest memory even from the worker, deliberately:
// the CPU's only PB chain access is __AXServiceVPB, reached from __AXSyncPBs before
// the mail that starts a mix (and after the previous mix was joined), so guest and mix
// never touch a PB concurrently. Shadowing it would only risk a stale-snapshot clobber.
void WritePB(uint32_t addr, const AXPBWii& pb) const {
if (m_pbLayout == PBLayout::Unknown) {
return;
}
WritePBWithLayout(addr, pb, m_pbLayout);
}
void RunCompressor(uint16_t threshold, uint16_t releaseFrames, uint32_t tableAddr) {
bool triggered = false;
for (uint32_t i = 0; i < kAxSamplesPerFrame; ++i) {
const int64_t left = m_bus[kBusMainL][i];
const int64_t right = m_bus[kBusMainR][i];
if ((left < 0 ? -left : left) > threshold || (right < 0 ? -right : right) > threshold) {
triggered = true;
break;
}
}
constexpr uint32_t kAttackEntryCount = 11;
const uint32_t frameByteSize = kAxSamplesPerFrame * sizeof(uint16_t);
uint32_t tableOffset = 0;
if (triggered) {
tableOffset = m_compressorPos * frameByteSize;
m_compressorPos = releaseFrames;
} else if (m_compressorPos != 0) {
--m_compressorPos;
tableOffset = (kAttackEntryCount + m_compressorPos) * frameByteSize;
} else {
return;
}
for (uint32_t i = 0; i < kAxSamplesPerFrame; ++i) {
const uint16_t coef = MixRead16(tableAddr + tableOffset + i * sizeof(uint16_t));
m_bus[kBusMainL][i] =
static_cast<int>((static_cast<int64_t>(m_bus[kBusMainL][i]) * coef) >> 15);
m_bus[kBusMainR][i] =
static_cast<int>((static_cast<int64_t>(m_bus[kBusMainR][i]) * coef) >> 15);
}
}
void ProcessVoice(AXPBWii& pb, const AXBuffers& buffers, uint32_t count, AXMixControl control,
const int16_t* coeffs, bool newFilter) {
if (pb.running != 1) {
return;
}
Accelerator accel;
accel.Setup(&pb);
std::array<int16_t, kAxSamplesPerFrame> samples{};
Resample(accel, pb, samples.data(), count, coeffs);
// Same wrapping 16-bit ramp as MixAdd, minus the accumulate. The two's
// complement wrap of the int16 envelope is bit-identical to the uint16
// wrap the kernel performs, so the value stored back is unchanged.
pb.vol_env.cur_volume = static_cast<int16_t>(AxMixKernels::ScaleRamp(
samples.data(), count, static_cast<uint16_t>(pb.vol_env.cur_volume),
static_cast<uint16_t>(pb.vol_env.cur_volume_delta)));
if (pb.lpf.on) {
LowPassFilter(samples.data(), count, pb.lpf);
}
if (newFilter && pb.biquad.on) {
BiquadFilter(samples.data(), count, pb.biquad);
}
auto mix = [&](uint32_t bit, uint32_t rampBit, int* out, VolumeData& vd, int16_t& dpop) {
if ((control & bit) == 0) {
return;
}
MixAdd(out, samples.data(), count, vd, dpop, (control & rampBit) != 0);
};
mix(MIX_MAIN_L, MIX_MAIN_L_RAMP, buffers[kBusMainL], pb.mixer.main_left, pb.dpop.main_left);
mix(MIX_MAIN_R, MIX_MAIN_R_RAMP, buffers[kBusMainR], pb.mixer.main_right, pb.dpop.main_right);
mix(MIX_MAIN_S, MIX_MAIN_S_RAMP, buffers[kBusMainS], pb.mixer.main_surround, pb.dpop.main_surround);
mix(MIX_AUXA_L, MIX_AUXA_L_RAMP, buffers[kBusAuxAL], pb.mixer.auxA_left, pb.dpop.auxA_left);
mix(MIX_AUXA_R, MIX_AUXA_R_RAMP, buffers[kBusAuxAR], pb.mixer.auxA_right, pb.dpop.auxA_right);
mix(MIX_AUXA_S, MIX_AUXA_S_RAMP, buffers[kBusAuxAS], pb.mixer.auxA_surround, pb.dpop.auxA_surround);
mix(MIX_AUXB_L, MIX_AUXB_L_RAMP, buffers[kBusAuxBL], pb.mixer.auxB_left, pb.dpop.auxB_left);
mix(MIX_AUXB_R, MIX_AUXB_R_RAMP, buffers[kBusAuxBR], pb.mixer.auxB_right, pb.dpop.auxB_right);
mix(MIX_AUXB_S, MIX_AUXB_S_RAMP, buffers[kBusAuxBS], pb.mixer.auxB_surround, pb.dpop.auxB_surround);
mix(MIX_AUXC_L, MIX_AUXC_L_RAMP, buffers[kBusAuxCL], pb.mixer.auxC_left, pb.dpop.auxC_left);
mix(MIX_AUXC_R, MIX_AUXC_R_RAMP, buffers[kBusAuxCR], pb.mixer.auxC_right, pb.dpop.auxC_right);
mix(MIX_AUXC_S, MIX_AUXC_S_RAMP, buffers[kBusAuxCS], pb.mixer.auxC_surround, pb.dpop.auxC_surround);
if (pb.remote) {
if (newFilter && pb.remote_iir.on) {
if (pb.remote_iir.on == 2) {
BiquadFilter(samples.data(), count, pb.remote_iir.biquad);
} else {
LowPassFilter(samples.data(), count, pb.remote_iir.lpf);
}
}
const uint32_t wmCount = count == kAxSamplesPerFrame ? 18u : 6u;
std::array<int16_t, 18> wmSamples{};
pb.remote_src.cur_addr_frac = static_cast<uint16_t>(ResampleAudio(
[&samples, count](uint32_t index) {
return samples[std::min<uint32_t>(index, count - 1)];
},
wmSamples.data(), wmCount, pb.remote_src.last_samples, pb.remote_src.cur_addr_frac,
0x00055555u, 0, coeffs));
auto mixRemote = [&](uint32_t channel, int* out, VolumeData& vd, int16_t& dpop) {
const uint16_t mode = static_cast<uint16_t>((pb.remote_mixer_control >> (channel * 2)) & 3u);
if (mode == 0) {
return;
}
MixAdd(out, wmSamples.data(), wmCount, vd, dpop, (mode & 2u) != 0);
};
mixRemote(0, buffers[kBusWm0Main], pb.remote_mixer.main0, pb.remote_dpop.main0);
mixRemote(1, buffers[kBusWm0Aux], pb.remote_mixer.aux0, pb.remote_dpop.aux0);
mixRemote(2, buffers[kBusWm1Main], pb.remote_mixer.main1, pb.remote_dpop.main1);
mixRemote(3, buffers[kBusWm1Aux], pb.remote_mixer.aux1, pb.remote_dpop.aux1);
mixRemote(4, buffers[kBusWm2Main], pb.remote_mixer.main2, pb.remote_dpop.main2);
mixRemote(5, buffers[kBusWm2Aux], pb.remote_mixer.aux2, pb.remote_dpop.aux2);
mixRemote(6, buffers[kBusWm3Main], pb.remote_mixer.main3, pb.remote_dpop.main3);
mixRemote(7, buffers[kBusWm3Aux], pb.remote_mixer.aux3, pb.remote_dpop.aux3);
}
}
template <typename Reader>
static uint32_t ResampleAudio(Reader&& readSample, int16_t* out, uint32_t count, int16_t* lastSamples,
uint32_t pos, uint32_t ratio, uint16_t srcType,
const int16_t* coeffs = nullptr) {
if (coeffs && srcType == 0) {
std::array<int16_t, 4> temp{};
std::memcpy(temp.data(), lastSamples, sizeof(int16_t) * 4);
uint32_t idx = 4;
uint32_t readSamples = 0;
for (uint32_t i = 0; i < count; ++i) {
pos += ratio;
while (pos >= 0x10000u) {
temp[idx++ & 3] = readSample(readSamples++);
pos -= 0x10000u;
}
const uint16_t fracIndex = static_cast<uint16_t>(((pos & 0xffffu) >> 9) << 2);
const int16_t* c = coeffs + fracIndex;
const int64_t t0 = temp[idx++ & 3];
const int64_t t1 = temp[idx++ & 3];
const int64_t t2 = temp[idx++ & 3];
const int64_t t3 = temp[idx++ & 3];
out[i] = ClampS16((t0 * c[0] + t1 * c[1] + t2 * c[2] + t3 * c[3]) >> 15);
}
lastSamples[3] = temp[--idx & 3];
lastSamples[2] = temp[--idx & 3];
lastSamples[1] = temp[--idx & 3];
lastSamples[0] = temp[--idx & 3];
return pos & 0xffffu;
}
// SRCTYPE_POLYPHASE (0) without a table and SRCTYPE_LINEAR (1) both degrade to
// LINEAR; anything else (SRCTYPE_NEAREST or undefined) reads the accelerator 1:1
// and ignores the ratio, matching DSPHLE's ResampleAudio branch order.
if (srcType != 0 && srcType != 1) {
for (uint32_t i = 0; i < count; ++i) {
out[i] = readSample(i);
}
if (count >= 4) {
std::memcpy(lastSamples, out + count - 4, sizeof(int16_t) * 4);
}
return pos;
}
std::array<int16_t, 4> temp{};
std::memcpy(temp.data(), lastSamples, sizeof(int16_t) * 4);
uint32_t idx = 4;
uint32_t readSamples = 0;
for (uint32_t i = 0; i < count; ++i) {
pos += ratio;
while (pos >= 0x10000u) {
temp[idx++ & 3] = readSample(readSamples++);
pos -= 0x10000u;
}
const uint16_t frac = pos & 0xffffu;
if (frac == 0) {
out[i] = temp[idx++ & 3];
idx += 3;
} else {
const int32_t s0 = temp[idx++ & 3];
const int32_t s1 = temp[idx++ & 3];
idx += 2;
out[i] = static_cast<int16_t>((s0 * static_cast<uint16_t>(-frac) + s1 * frac) >> 16);
}
}
lastSamples[3] = temp[--idx & 3];
lastSamples[2] = temp[--idx & 3];
lastSamples[1] = temp[--idx & 3];
lastSamples[0] = temp[--idx & 3];
return pos & 0xffffu;
}
// `coeffs` is the DROM base; the bank is selected here, per voice, exactly
// where GetInputSamples selects it.
static void Resample(Accelerator& accel, AXPBWii& pb, int16_t* out, uint32_t count,
const int16_t* coeffs) {
pb.src.cur_addr_frac = static_cast<uint16_t>(ResampleAudio(
[&accel](uint32_t) {
return accel.ReadSample();
},
out, count, pb.src.last_samples, pb.src.cur_addr_frac,
Hilo(pb.src.ratio_hi, pb.src.ratio_lo), pb.src_type,
VoiceCoefficients(coeffs, pb.coef_select)));
}
static void MixAdd(int* out, const int16_t* input, uint32_t count, VolumeData& vd, int16_t& dpop, bool ramp) {
// int16 x uint16 always fits in int32, so the 64-bit clamp the generic
// helper performs is unnecessary here.
const uint16_t delta = ramp ? vd.volume_delta : 0;
vd.volume = AxMixKernels::MixAddRamp(out, input, count, vd.volume, delta, dpop);
}
static void LowPassFilter(int16_t* samples, uint32_t count, PBLowPassFilter& filter) {
for (uint32_t i = 0; i < count; ++i) {
filter.yn1 = samples[i] = ClampS16((filter.a0 * static_cast<int32_t>(samples[i]) +
filter.b0 * static_cast<int32_t>(filter.yn1)) >> 15);
}
}
static void BiquadFilter(int16_t* samples, uint32_t count, PBBiquadFilter& filter) {
for (uint32_t i = 0; i < count; ++i) {
const int16_t xn0 = samples[i];
int64_t value = 0;
value += filter.b0 * static_cast<int32_t>(xn0);
value += filter.b1 * static_cast<int32_t>(filter.xn1);
value += filter.b2 * static_cast<int32_t>(filter.xn2);
value += filter.a1 * static_cast<int32_t>(filter.yn1);
value += filter.a2 * static_cast<int32_t>(filter.yn2);
value <<= 2;
value += (value & 0x10000) ? 0x8000 : 0x7fff;
const int16_t yn0 = ClampS16(value >> 16);
filter.xn2 = filter.xn1;
filter.yn2 = filter.yn1;
filter.xn1 = xn0;
filter.yn1 = yn0;
samples[i] = yn0;
}
}
void MixAUXSamples(uint32_t auxId, uint32_t writeAddr, uint32_t readAddr, uint16_t volume) {
std::array<uint16_t, kAxSamplesPerFrame> ramp{};
GenerateVolumeRamp(ramp.data(), m_lastAuxVolumes[auxId], volume, ramp.size());
m_lastAuxVolumes[auxId] = volume;
std::array<int*, 3> src{};
if (auxId == 0) src = {m_bus[kBusAuxAL].data(), m_bus[kBusAuxAR].data(), m_bus[kBusAuxAS].data()};
if (auxId == 1) src = {m_bus[kBusAuxBL].data(), m_bus[kBusAuxBR].data(), m_bus[kBusAuxBS].data()};
if (auxId == 2) src = {m_bus[kBusAuxCL].data(), m_bus[kBusAuxCR].data(), m_bus[kBusAuxCS].data()};
if (writeAddr) {
for (int* buffer : src) {
WriteAuxOutBuffer(writeAddr, buffer, kAxSamplesPerFrame);
writeAddr += kAxSamplesPerFrame * sizeof(int32_t);
}
}
std::array<int*, 3> dst = {m_bus[kBusMainL].data(), m_bus[kBusMainR].data(), m_bus[kBusMainS].data()};
std::array<int, kAxSamplesPerFrame> aux{};
for (int* buffer : dst) {
ReadAuxInBuffer(readAddr, aux.data(), kAxSamplesPerFrame);
readAddr += kAxSamplesPerFrame * sizeof(int32_t);
AxMixKernels::MixAccumRamp32(buffer, aux.data(), ramp.data(), kAxSamplesPerFrame);
}
}
void UploadAUXMixLRSC(int auxId, uint32_t* addresses, uint16_t volume) {
int* auxLeft = auxId ? m_bus[kBusAuxBL].data() : m_bus[kBusAuxAL].data();
int* auxRight = auxId ? m_bus[kBusAuxBR].data() : m_bus[kBusAuxAR].data();
int* auxSurround = auxId ? m_bus[kBusAuxBS].data() : m_bus[kBusAuxAS].data();
int* auxCBuffer = auxId ? m_bus[kBusAuxCS].data() : m_bus[kBusAuxCR].data();
WriteAuxOutBuffer(addresses[0], auxLeft, kAxSamplesPerFrame);
WriteAuxOutBuffer(addresses[0] + kAxSamplesPerFrame * sizeof(int32_t), auxRight, kAxSamplesPerFrame);
WriteAuxOutBuffer(addresses[0] + 2 * kAxSamplesPerFrame * sizeof(int32_t), auxSurround, kAxSamplesPerFrame);
WriteAuxOutBuffer(addresses[1], auxCBuffer, kAxSamplesPerFrame);
std::array<uint16_t, kAxSamplesPerFrame> ramp{};
GenerateVolumeRamp(ramp.data(), m_lastAuxVolumes[auxId], volume, ramp.size());
m_lastAuxVolumes[auxId] = volume;
std::array<int*, 4> mixDest = {m_bus[kBusMainL].data(), m_bus[kBusMainR].data(),
m_bus[kBusMainS].data(), m_bus[kBusAuxCL].data()};
std::array<int, kAxSamplesPerFrame> temp{};
for (uint32_t i = 0; i < mixDest.size(); ++i) {
ReadAuxInBuffer(addresses[2 + i], temp.data(), kAxSamplesPerFrame);
AxMixKernels::MixAccumRamp32(mixDest[i], temp.data(), ramp.data(),
kAxSamplesPerFrame);
}
}
void OutputSamples(uint32_t lrAddr, uint32_t surroundAddr, uint16_t volume, bool uploadAuxC) {
std::array<uint16_t, kAxSamplesPerFrame> ramp{};
GenerateVolumeRamp(ramp.data(), m_lastMainVolume, volume, ramp.size());
m_lastMainVolume = volume;
WriteGuestS32Buffer(surroundAddr, m_bus[kBusMainS].data(), kAxSamplesPerFrame);
if (uploadAuxC) {
WriteGuestS32Buffer(surroundAddr + kAxSamplesPerFrame * sizeof(int32_t),
m_bus[kBusAuxCL].data(), kAxSamplesPerFrame);
}
std::array<int16_t, kAxSamplesPerFrame * 2> pcm{};
for (uint32_t i = 0; i < kAxSamplesPerFrame; ++i) {
const int16_t left = ClampS16((static_cast<int64_t>(m_bus[kBusMainL][i]) * ramp[i]) >> 15);
const int16_t right = ClampS16((static_cast<int64_t>(m_bus[kBusMainR][i]) * ramp[i]) >> 15);
// OUTPUT is destructive on the DSP: the main buses are left holding the ramped,
// clamped result (not the wide accumulator) so any mixing after it sees that value.
m_bus[kBusMainL][i] = left;
m_bus[kBusMainR][i] = right;
pcm[i * 2] = right;
pcm[i * 2 + 1] = left;
}
// The AI frame (contiguous right/left interleaved) lands in guest memory directly
// even from the worker: its only reader, PushAudioBlock, runs after Audio_HLE_Tick
// has joined the worker, so the guest never observes a half-written frame.
if (uint8_t* host = MixResolveRange(lrAddr, pcm.size() * sizeof(int16_t))) {
for (size_t i = 0; i < pcm.size(); ++i) {
BigEndian::Write16(host + i * sizeof(uint16_t), static_cast<uint16_t>(pcm[i]));
}
} else {
for (uint32_t i = 0; i < kAxSamplesPerFrame; ++i) {
MixWrite16(lrAddr + i * 4, static_cast<uint16_t>(pcm[i * 2]));
MixWrite16(lrAddr + i * 4 + 2, static_cast<uint16_t>(pcm[i * 2 + 1]));
}
}
}
void OutputWMSamples(uint32_t* addresses) {
const AXBuffers all = MixBuffers();
const std::array<int*, 4> buffers = {all[kBusWm0Main], all[kBusWm1Main], all[kBusWm2Main],
all[kBusWm3Main]};
for (uint32_t i = 0; i < buffers.size(); ++i) {
for (uint32_t j = 0; j < kAxWiimoteSamplesPerFrame; ++j) {
MixWrite16(addresses[i] + j * 2, static_cast<uint16_t>(ClampS16(buffers[i][j])));
}
}
}
// Aux OUT/IN stay off guest memory during a deferred mix, since __AXProcessAux reads a mix
// result inside the same __AXOutNewFrame that produced it: OUT shadows to a host buffer
// JoinMix publishes one AX frame later (a 3 ms older reverb tap, not a dry-signal change),
// and IN snapshots at mail time to match a synchronous mix. Reads check this frame's shadow
// first, so a same-buffer write/read still behaves like a synchronous read-back.
void WriteAuxOutBuffer(uint32_t addr, const int* src, uint32_t count) {
if (!m_deferredAux) {
WriteGuestS32Buffer(addr, src, count);
return;
}
if (addr == 0) {
return;
}
if (m_auxOutCount >= m_auxOut.size()) {
ReportAuxShadowOverflow();
return;
}
AuxBlock& block = m_auxOut[m_auxOutCount++];
block.addr = addr;
std::memcpy(block.data.data(), src, count * sizeof(int32_t));
}
void ReadAuxInBuffer(uint32_t addr, int* dst, uint32_t count) {
if (m_deferredAux) {
for (uint32_t i = m_auxOutCount; i-- > 0;) {
if (m_auxOut[i].addr == addr) {
std::memcpy(dst, m_auxOut[i].data.data(), count * sizeof(int32_t));
return;
}
}
for (uint32_t i = 0; i < m_auxInCount; ++i) {
if (m_auxIn[i].addr == addr) {
std::memcpy(dst, m_auxIn[i].data.data(), count * sizeof(int32_t));
return;
}
}
}
ReadGuestS32Buffer(addr, dst, count);
}
// Guest thread only, with the worker joined.
void PublishAuxOutputs() {
for (uint32_t i = 0; i < m_auxOutCount; ++i) {
WriteGuestS32Buffer(m_auxOut[i].addr, m_auxOut[i].data.data(), kAxSamplesPerFrame);
}
// Publish exactly once: __AXProcessAux may process the aux buffer in
// place, so replaying the shadow at the next join would overwrite the
// reverb result the guest just computed.
m_auxOutCount = 0;
}
void ReportAuxShadowOverflow() {
if (m_loggedAuxShadowOverflow.exchange(true, std::memory_order_relaxed)) {
return;
}
RT_LOGF(RT_TAG_AUDIO, "aux-out shadow overflow; dropped an aux upload\n");
std::fflush(stderr);
}
bool EnsureMixWorker() {
if (m_mixThread.joinable()) {
return true;
}
if (g_mixMem1.load(std::memory_order_relaxed) == nullptr) {
return false;
}
RT_LOGF(RT_TAG_AUDIO, "starting the AX/DSP mix worker thread\n");
std::fflush(stderr);
m_mixStop = false;
m_mixPending = false;
m_mixBusy = false;
try {
m_mixThread = std::thread([this] { MixWorkerMain(); });
} catch (const std::system_error&) {
RT_LOGF(RT_TAG_AUDIO, "failed to start the mix worker; mixing inline\n");
std::fflush(stderr);
return false;
}
return true;
}
void MixWorkerMain() {
t_onMixWorker = true;
for (;;) {
{
std::unique_lock<std::mutex> lock(m_mixMutex);
m_mixWake.wait(lock, [this] { return m_mixStop || m_mixPending; });
if (!m_mixPending) {
return;
}
m_mixPending = false;
m_mixBusy = true;
}
try {
ExecuteCommandList(m_mixLayout, m_mixCmdListSize);
} catch (const std::exception& error) {
ReportMixWorkerFailure(error.what());
} catch (...) {
ReportMixWorkerFailure("unknown exception");
}
{
std::lock_guard<std::mutex> lock(m_mixMutex);
m_mixBusy = false;
}
m_mixIdle.notify_all();
}
}
void ReportMixWorkerFailure(const char* what) {
if (m_loggedMixWorkerFailure.exchange(true, std::memory_order_relaxed)) {
return;
}
RT_LOGF(RT_TAG_AUDIO, "worker aborted a command list: %s\n", what);
std::fflush(stderr);
}
mutable std::mutex m_mutex;
MailState m_mailState = MailState::WaitingForCmdListSize;
bool m_toDspBusy = false;
uint32_t m_cmdListSize = 0;
std::array<uint16_t, 512> m_cmdList{};
std::deque<uint32_t> m_cpuMails;
uint32_t m_lastCpuMail = 0;
uint32_t m_compressorPos = 0;
uint32_t m_deferredResumeCallbacks = 0;
uint32_t m_ucodeCrc = 0;
PBLayout m_pbLayout = PBLayout::Unknown;
AXCommandLayout m_commandLayout = AXCommandLayout::Auto;
bool m_triedLoadCoeffs = false;
bool m_hasCoeffs = false;
std::array<int16_t, 0x800> m_coeffs{};
// Indexed by AxBus. The wiimote buses live in their own array because they
// are 18 samples per frame rather than 96.
std::array<std::array<int, kAxSamplesPerFrame>, kAxRegularBusCount> m_bus{};
std::array<std::array<int, kAxWiimoteSamplesPerFrame>, kAxWiimoteBusCount> m_wmBus{};
uint16_t m_lastMainVolume = 0x8000;
std::array<uint16_t, 3> m_lastAuxVolumes{};
// Mix worker handshake.
std::thread m_mixThread;
mutable std::mutex m_mixMutex;
std::condition_variable m_mixWake; // guest -> worker
std::condition_variable m_mixIdle; // worker -> guest
bool m_mixStop = false;
bool m_mixPending = false;
bool m_mixBusy = false;
bool m_mixWorkerEnabled = false;
AXCommandLayout m_mixLayout = AXCommandLayout::New;
uint32_t m_mixCmdListSize = 0;
std::atomic<bool> m_loggedMixWorkerFailure{false};
std::atomic<bool> m_loggedAuxShadowOverflow{false};
std::atomic<bool> m_loggedUnknownCommand{false};
std::atomic<bool> m_loggedCmdListTruncated{false};
// Aux marshalling. m_deferredAux is written by the guest thread before the
// worker is signalled and read by whichever thread runs the mix.
bool m_deferredAux = false;
uint32_t m_auxOutCount = 0;
uint32_t m_auxInCount = 0;
std::array<AuxBlock, 24> m_auxOut{};
std::array<AuxBlock, 24> m_auxIn{};
};
AXWii& Instance() {
static AXWii ax;
return ax;
}
} // namespace
void Init() {
InitAram();
Instance().Initialize();
}
void InitForAXOut(CpuContext* ctx) {
Init();
g_axTaskPtr = kAxDspTaskAddr;
Memory::Write32(g_axTaskPtr + 0x00u, 1);
Memory::Write32(g_axTaskPtr + 0x04u, 0);
Memory::Write32(g_axTaskPtr + 0x0cu, kAxIramMmemAddr);
Memory::Write32(g_axTaskPtr + 0x14u, 0);
Memory::Write32(g_axTaskPtr + 0x18u, kAxDramMmemAddr);
Memory::Write32(g_axTaskPtr + 0x1cu, kAxDramLength);
Memory::Write32(g_axTaskPtr + 0x20u, kAxDramDspAddr);
Memory::Write32(g_axTaskPtr + 0x28u, kAxInitCallback);
Memory::Write32(g_axTaskPtr + 0x2cu, kAxResumeCallback);
Memory::Write32(g_axTaskPtr + 0x30u, kAxDoneCallback);
Memory::Write32(g_axTaskPtr + 0x34u, kAxRequestCallback);
// __AXOutInitDSP's ucode length and DSP init/resume vectors, then its two flags.
(void)ctx;
Memory::Write32(g_axTaskPtr + 0x10u, Memory::Read16(MKW_GADDR(80385804)));
Memory::Write16(g_axTaskPtr + 0x24u, Memory::Read16(MKW_GADDR(80385800)));
Memory::Write16(g_axTaskPtr + 0x26u, Memory::Read16(MKW_GADDR(80385802)));
Instance().ConfigureFromTask(g_axTaskPtr);
LinkSingleDspTask(g_axTaskPtr);
Memory::Write32(MKW_GADDR(80386528), 1);
Memory::Write32(MKW_GADDR(80386524), 0);
}
void Stop() {
Instance().Stop();
}
uint32_t CheckInit() {
return ReadGuestU32OrZero(kDspInitializedAddr);
}
uint32_t AddTask(uint32_t taskPtr) {
if (taskPtr == 0) {
return 0;
}
MarkDspInitialized();
Instance().ConfigureFromTask(taskPtr);
const uint32_t firstTask = ReadGuestU32OrZero(kDspFirstTaskAddr);
if (firstTask == 0) {
LinkSingleDspTask(taskPtr);
} else {
const uint32_t oldCurrent = ReadGuestU32OrZero(kDspCurrentTaskAddr);
Memory::TryWrite32(oldCurrent + 0x38u, taskPtr);
Memory::TryWrite32(taskPtr + 0x38u, 0);
Memory::TryWrite32(taskPtr + 0x3cu, oldCurrent);
Memory::TryWrite32(kDspCurrentTaskAddr, taskPtr);
}
Memory::TryWrite32(taskPtr + 0x00u, 0);
Memory::TryWrite32(taskPtr + 0x08u, 1);
if (taskPtr == ReadGuestU32OrZero(kDspFirstTaskAddr)) {
AssertTask(taskPtr);
}
return taskPtr;
}
void SendMailToDSP(uint32_t mail) {
Instance().SendMail(mail);
}
uint32_t CheckMailToDSP() {
return Instance().CheckMailToDSP();
}
uint32_t CheckMailFromDSP() {
return Instance().CheckMailFromDSP();
}
uint32_t ReadMailFromDSP() {
return Instance().ReadMailFromDSP();
}
uint32_t AssertTask(uint32_t taskPtr) {
if (taskPtr != 0) {
g_axTaskPtr = taskPtr;
Memory::TryWrite32(kDspAssertPendingAddr, 1);
Memory::TryWrite32(kDspAssertTaskAddr, taskPtr);
Memory::TryWrite32(kDspRunningTaskAddr, taskPtr);
Memory::Write32(taskPtr, 1);
Instance().QueueResumeCallback();
}
return taskPtr;
}
void ServiceDeferredCallbacks() {
Instance().ServiceDeferredCallbacks();
}
void InitAram() {
// Retire cached host sample windows: the guest memory map may have been rebuilt since
// the last AX session, so the worker (which caches a window thread_local) must be idle
// across the generation bump and region-base refresh.
Instance().JoinMix();
RefreshMixMemoryMap();
g_aramWindowGeneration.fetch_add(1, std::memory_order_relaxed);
}
void JoinMixWorker() {
Instance().JoinMix();
}
bool MixWorkerBusy() {
return Instance().MixBusy();
}
void ShutdownMixWorker() {
Instance().ShutdownMixWorker();
}
void SetMixWorkerEnabled(bool enabled) {
Instance().SetMixWorkerEnabled(enabled);
}
} // namespace AxDspHle