mcpx: Implement APU multipass voice processing

Voice Processor (VP) multipass feature allows configuring lists of voices
that are first mixed (in order) into a designated mixbin which is then used
as a sample source when processing voices with multipass flag set to true
in NV_PAVS_VOICE_CFG_FMT. Setting correct voice order in lists is the
responsibility of the game/application and in practice is handled by the
DirectSound library. The multipass mixbin is hardcoded to 31 in
DirectSound, but hardware would allow other bins.

This implementation also adds additional info to audio debug UI to see what
the source and destination voices involved are. The info is only shown
when DSP processing is off, i.e. "VP Only" (MON_VP) is selected. This is
because storing the voice numbers requires additional digging which is
required for MON_VP anyway and therefore is free. The multipass feature
itself works fine with DSP (i.e. GP and EP) enabled, only the additional
debug info is not shown.
This commit is contained in:
coldhex 2025-02-15 12:23:36 +02:00 committed by mborgerson
parent ac781ea8d1
commit 0427ae8cfc
3 changed files with 193 additions and 23 deletions

View File

@ -218,7 +218,7 @@ static int voice_resample(MCPXAPUState *d, uint16_t v, float samples[][2],
static void voice_reset_filters(MCPXAPUState *d, uint16_t v);
static void voice_process(MCPXAPUState *d,
float mixbins[NUM_MIXBINS][NUM_SAMPLES_PER_FRAME],
uint16_t v);
uint16_t v, int voice_list);
static int voice_get_samples(MCPXAPUState *d, uint32_t v, float samples[][2],
int num_samples_requested);
static void se_frame(MCPXAPUState *d);
@ -245,6 +245,7 @@ static void mcpx_debug_begin_frame(void)
{
for (int i = 0; i < MCPX_HW_MAX_VOICES; i++) {
g_dbg.vp.v[i].active = false;
g_dbg.vp.v[i].multipass_dst_voice = 0xFFFF;
}
}
@ -286,6 +287,19 @@ void mcpx_apu_debug_clear_isolations(void)
static bool voice_should_mute(uint16_t v)
{
bool m = (g_dbg_voice_monitor >= 0) && (v != g_dbg_voice_monitor);
if (m && g_dbg_cache.vp.v[g_dbg_voice_monitor].multipass) {
uint8_t mp_bin = g_dbg_cache.vp.v[g_dbg_voice_monitor].multipass_bin;
struct McpxApuDebugVoice *d = &g_dbg_cache.vp.v[v];
for (int i = 0; i < sizeof(d->bin) / sizeof(d->bin[0]); i++) {
if (d->bin[i] == mp_bin) {
m = false;
break;
}
}
}
return m || mcpx_apu_debug_is_muted(v);
}
@ -1590,9 +1604,101 @@ static void voice_reset_filters(MCPXAPUState *d, uint16_t v)
}
}
static int peek_ahead_multipass_bin(MCPXAPUState *d, uint16_t v,
uint16_t *dst_voice)
{
bool first = true;
while (v != 0xFFFF) {
bool multipass = voice_get_mask(d, v, NV_PAVS_VOICE_CFG_FMT,
NV_PAVS_VOICE_CFG_FMT_MULTIPASS);
if (multipass) {
if (first) {
break;
}
*dst_voice = v;
int mp_bin = voice_get_mask(d, v, NV_PAVS_VOICE_CFG_FMT,
NV_PAVS_VOICE_CFG_FMT_MULTIPASS_BIN);
return mp_bin;
}
v = voice_get_mask(d, v, NV_PAVS_VOICE_TAR_PITCH_LINK,
NV_PAVS_VOICE_TAR_PITCH_LINK_NEXT_VOICE_HANDLE);
first = false;
}
*dst_voice = 0xFFFF;
return -1;
}
static void dump_multipass_unused_debug_info(MCPXAPUState *d, uint16_t v)
{
unsigned int sample_size = voice_get_mask(
d, v, NV_PAVS_VOICE_CFG_FMT, NV_PAVS_VOICE_CFG_FMT_SAMPLE_SIZE);
unsigned int container_size_index = voice_get_mask(
d, v, NV_PAVS_VOICE_CFG_FMT, NV_PAVS_VOICE_CFG_FMT_CONTAINER_SIZE);
bool stream = voice_get_mask(d, v, NV_PAVS_VOICE_CFG_FMT,
NV_PAVS_VOICE_CFG_FMT_DATA_TYPE);
bool loop =
voice_get_mask(d, v, NV_PAVS_VOICE_CFG_FMT, NV_PAVS_VOICE_CFG_FMT_LOOP);
uint32_t ebo = voice_get_mask(d, v, NV_PAVS_VOICE_PAR_NEXT,
NV_PAVS_VOICE_PAR_NEXT_EBO);
uint32_t cbo = voice_get_mask(d, v, NV_PAVS_VOICE_PAR_OFFSET,
NV_PAVS_VOICE_PAR_OFFSET_CBO);
uint32_t lbo = voice_get_mask(d, v, NV_PAVS_VOICE_CUR_PSH_SAMPLE,
NV_PAVS_VOICE_CUR_PSH_SAMPLE_LBO);
uint32_t ba = voice_get_mask(d, v, NV_PAVS_VOICE_CUR_PSL_START,
NV_PAVS_VOICE_CUR_PSL_START_BA);
bool persist = voice_get_mask(d, v, NV_PAVS_VOICE_CFG_FMT,
NV_PAVS_VOICE_CFG_FMT_PERSIST);
bool linked = voice_get_mask(d, v, NV_PAVS_VOICE_CFG_FMT,
NV_PAVS_VOICE_CFG_FMT_LINKED);
struct McpxApuDebugVoice *dbg = &g_dbg.vp.v[v];
dbg->container_size = container_size_index;
dbg->sample_size = sample_size;
dbg->stream = stream;
dbg->loop = loop;
dbg->ebo = ebo;
dbg->cbo = cbo;
dbg->lbo = lbo;
dbg->ba = ba;
dbg->samples_per_block = 0; // Value overloaded with multipass bin
dbg->persist = persist;
dbg->linked = linked;
}
static void get_multipass_samples(MCPXAPUState *d,
float mixbins[][NUM_SAMPLES_PER_FRAME],
uint16_t v, float samples[][2])
{
struct McpxApuDebugVoice *dbg = &g_dbg.vp.v[v];
// DirectSound sets bin to 31, but hardware would allow other bins
int mp_bin = voice_get_mask(d, v, NV_PAVS_VOICE_CFG_FMT,
NV_PAVS_VOICE_CFG_FMT_MULTIPASS_BIN);
dbg->multipass_bin = mp_bin;
for (int i = 0; i < NUM_SAMPLES_PER_FRAME; i++) {
samples[i][0] = mixbins[mp_bin][i];
samples[i][1] = mixbins[mp_bin][i];
}
// DirectSound sets clear mix to true
bool clear_mix = voice_get_mask(d, v, NV_PAVS_VOICE_CFG_FMT,
NV_PAVS_VOICE_CFG_FMT_CLEAR_MIX);
if (clear_mix) {
memset(&mixbins[mp_bin][0], 0, sizeof(mixbins[0]));
}
// Dump irrelevant data for audio debug UI to avoid showing stale info
dump_multipass_unused_debug_info(d, v);
}
static void voice_process(MCPXAPUState *d,
float mixbins[NUM_MIXBINS][NUM_SAMPLES_PER_FRAME],
uint16_t v)
uint16_t v, int voice_list)
{
assert(v < MCPX_HW_MAX_VOICES);
bool stereo = voice_get_mask(d, v, NV_PAVS_VOICE_CFG_FMT,
@ -1633,18 +1739,28 @@ static void voice_process(MCPXAPUState *d,
assert(ea_value <= 1.0f);
float samples[NUM_SAMPLES_PER_FRAME][2] = { 0 };
for (int sample_count = 0; sample_count < NUM_SAMPLES_PER_FRAME;) {
int active = voice_get_mask(d, v, NV_PAVS_VOICE_PAR_STATE,
NV_PAVS_VOICE_PAR_STATE_ACTIVE_VOICE);
if (!active) {
return;
bool multipass = voice_get_mask(d, v, NV_PAVS_VOICE_CFG_FMT,
NV_PAVS_VOICE_CFG_FMT_MULTIPASS);
dbg->multipass = multipass;
if (multipass) {
get_multipass_samples(d, mixbins, v, samples);
} else {
for (int sample_count = 0; sample_count < NUM_SAMPLES_PER_FRAME;) {
int active = voice_get_mask(d, v, NV_PAVS_VOICE_PAR_STATE,
NV_PAVS_VOICE_PAR_STATE_ACTIVE_VOICE);
if (!active) {
return;
}
int count =
voice_resample(d, v, &samples[sample_count],
NUM_SAMPLES_PER_FRAME - sample_count, rate);
if (count < 0) {
break;
}
sample_count += count;
}
int count = voice_resample(d, v, &samples[sample_count],
NUM_SAMPLES_PER_FRAME - sample_count, rate);
if (count < 0) {
break;
}
sample_count += count;
}
int active = voice_get_mask(d, v, NV_PAVS_VOICE_PAR_STATE,
@ -1771,9 +1887,32 @@ static void voice_process(MCPXAPUState *d,
/* For VP mon, simply mix all voices together here, selecting the
* maximal volume used for any given mixbin as the overall volume for
* this voice.
*
* If the current voice belongs to a multipass sub-voice group we must
* skip it here to avoid mixing it in twice because the sub-voices are
* mixed into the multipass bin and that sub-mix will be mixed in here
* later when the destination (i.e. second pass) voice is processed.
* TODO: Are the 2D, 3D and MP voice lists merely a DirectSound
* convention? Perhaps hardware doesn't care if e.g. a multipass
* voice is in the 2D or 3D list. On the other hand, MON_VP is
* not how the hardware works anyway so not much point worrying
* about precise emulation here. DirectSound compatibility is
* enough.
*/
int mp_bin = -1;
uint16_t mp_dst_voice = 0xFFFF;
if (voice_list == NV1BA0_PIO_SET_ANTECEDENT_VOICE_LIST_MP_TOP - 1) {
mp_bin = peek_ahead_multipass_bin(d, v, &mp_dst_voice);
}
dbg->multipass_dst_voice = mp_dst_voice;
bool debug_isolation =
g_dbg_voice_monitor >= 0 && g_dbg_voice_monitor == v;
float g = 0.0f;
for (int b = 0; b < 8; b++) {
if (bin[b] == mp_bin && !debug_isolation) {
continue;
}
float hr = 1 << d->vp.submix_headroom[bin[b]];
g = fmax(g, attenuate(vol[b]) / hr);
}
@ -1822,6 +1961,8 @@ static int voice_get_samples(MCPXAPUState *d, uint32_t v, float samples[][2],
bool linked = voice_get_mask(d, v, NV_PAVS_VOICE_CFG_FMT,
NV_PAVS_VOICE_CFG_FMT_LINKED); /* FIXME? */
assert(!multipass); // Multipass is handled before this
int ssl_index = 0;
int ssl_seg = 0;
int page = 0;
@ -1954,11 +2095,6 @@ static int voice_get_samples(MCPXAPUState *d, uint32_t v, float samples[][2],
DPRINTF("CBO=%d EBO=%d\n", cbo, ebo);
if (multipass) {
// FIXME
samples_per_block = 1;
}
block_size *= samples_per_block;
// FIXME: Restructure this loop
@ -2147,7 +2283,7 @@ static void se_frame(MCPXAPUState *d)
qemu_cond_wait(&d->cond, &d->lock);
qemu_spin_lock(&d->vp.voice_spinlocks[v]);
}
voice_process(d, mixbins, v);
voice_process(d, mixbins, v, list);
qemu_spin_unlock(&d->vp.voice_spinlocks[v]);
}
d->regs[current] = d->regs[next];

View File

@ -44,6 +44,8 @@ struct McpxApuDebugVoice
bool persist;
bool multipass;
bool linked;
uint8_t multipass_bin;
uint16_t multipass_dst_voice;
int container_size, sample_size;
unsigned int samples_per_block;
uint32_t ebo, cbo, lbo, ba;

View File

@ -22,6 +22,8 @@
#include "font-manager.hh"
#include "viewport-manager.hh"
#define MAX_VOICES 256
DebugApuWindow::DebugApuWindow() : m_is_open(false)
{
}
@ -60,8 +62,7 @@ void DebugApuWindow::Draw()
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2*g_viewport_mgr.m_scale, 2*g_viewport_mgr.m_scale));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4*g_viewport_mgr.m_scale, 4*g_viewport_mgr.m_scale));
for (int i = 0; i < 256; i++)
{
for (int i = 0; i < MAX_VOICES; i++) {
if (i % 16) {
ImGui::SameLine();
}
@ -132,8 +133,15 @@ void DebugApuWindow::Draw()
assert(voice->container_size < 4);
assert(voice->sample_size < 4);
ImGui::Text("Container Size: %s, Sample Size: %s, Samples per Block: %d",
cs[voice->container_size], ss[voice->sample_size], voice->samples_per_block);
const char *spb_or_bin_label = "Samples per Block";
unsigned int spb_or_bin = voice->samples_per_block;
if (voice->multipass) {
spb_or_bin_label = "Multipass Bin";
spb_or_bin = voice->multipass_bin;
}
ImGui::Text("Container Size: %s, Sample Size: %s, %s: %d",
cs[voice->container_size], ss[voice->sample_size],
spb_or_bin_label, spb_or_bin);
ImGui::Text("Rate: %f (%d Hz)", voice->rate, (int)(48000.0/voice->rate));
ImGui::Text("EBO=%d CBO=%d LBO=%d BA=%x",
voice->ebo, voice->cbo, voice->lbo, voice->ba);
@ -153,6 +161,30 @@ void DebugApuWindow::Draw()
}
ImGui::Text("%-17s", buf);
}
int mon = mcpx_apu_debug_get_monitor();
if (mon == MCPX_APU_DEBUG_MON_VP) {
if (voice->multipass_dst_voice != 0xFFFF) {
ImGui::Text("Multipass Dest Voice: 0x%02x",
voice->multipass_dst_voice);
}
if (voice->multipass) {
ImGui::Text("Multipass Src Voices:");
int n = 0;
for (int i = 0; i < MAX_VOICES; i++) {
if (dbg->vp.v[i].multipass_dst_voice == voice_info) {
if (n > 0 && ((n & 7) == 0)) {
ImGui::Text(" ");
}
ImGui::SameLine();
ImGui::Text("0x%02x", i);
n++;
}
}
}
}
ImGui::PopFont();
ImGui::EndTooltip();
}