runtime: cheap SPUCNT gate for the sample-event scheduler

psx_spu_sample_event_service() and psx_spu_sample_event_cycles_to_next()
built a full SpuGlobalState snapshot (memset + ~40 register reads + three
24-voice loops) on every device-service call and every deadline recompute,
just to test one bit (SPUCNT & 0x40, SPU IRQ9 enable). Under an
MMIO-polling guest loop — every PS1 wait loop, loudest on the BoF3 Capcom
FMV whose LOGO overlay polls per tiny basic block — the snapshot was ~40%
of emu-thread wall time (gdb stack-sampled, clean boot, live FMV).

Add spu_ctrl_read() (one array load) and gate on it. Semantics identical:
same register, same bit.

Measured (BoF3 Capcom FMV, clean boot, headless vblank raises/s):
  Debug build:           28.5 -> ~52
  RelWithDebInfo build:  97.6 uncapped (~1.6x realtime; paced holds 60)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin
2026-09-01 15:36:55 -04:00
parent 201cb13f85
commit 8eb29d255d
3 changed files with 17 additions and 7 deletions
+5
View File
@@ -81,6 +81,11 @@ typedef struct SpuGlobalState {
void spu_get_voice_state(int voice, SpuVoiceState* out);
void spu_get_global_state(SpuGlobalState* out);
/* SPUCNT (0x1F801DAA) alone. The sample-event scheduler gates on one ctrl bit
* per device-service call; building the full SpuGlobalState there (register
* sweep + three 24-voice loops) was ~40% of emu-thread time during the Capcom
* FMV's MMIO-polling loop (gdb-sampled 2026-09-01). */
uint16_t spu_ctrl_read(void);
/* Debug peek into SPU sample RAM (spu_ram TCP command). */
uint32_t spu_ram_peek(uint32_t addr, uint8_t *out, uint32_t len);
+8 -7
View File
@@ -418,7 +418,6 @@ static int spu_sample_event_mode(void) {
}
uint32_t psx_spu_sample_event_cycles_to_next(void) {
SpuGlobalState state;
g_spu_sample_deadline_queries++;
if (!spu_sample_event_mode()) {
g_spu_sample_mode_rejects++;
@@ -428,8 +427,10 @@ uint32_t psx_spu_sample_event_cycles_to_next(void) {
g_spu_sample_pump_null_rejects++;
return UINT32_MAX;
}
spu_get_global_state(&state);
if ((state.ctrl & 0x0040u) == 0) {
/* Gate on SPUCNT alone — this runs on every deadline recompute, and the
* full SpuGlobalState snapshot here dominated the emu thread under an
* MMIO-polling guest loop (Capcom FMV, gdb-sampled 2026-09-01). */
if ((spu_ctrl_read() & 0x0040u) == 0) {
g_spu_sample_ctrl_rejects++;
return UINT32_MAX;
}
@@ -449,9 +450,9 @@ void psx_spu_sample_event_service(void) {
g_spu_sample_service_checks++;
if (!spu_sample_event_mode() || !s_midframe_audio_pump)
return;
SpuGlobalState state;
spu_get_global_state(&state);
if ((state.ctrl & 0x0040u) != 0) {
/* SPUCNT alone — same hot-gate rationale as the deadline query above. */
const uint16_t ctrl = spu_ctrl_read();
if ((ctrl & 0x0040u) != 0) {
g_spu_sample_enabled_services++;
g_spu_sample_last_service_phase = (uint32_t)(psx_cycle_count % 768u);
}
@@ -460,7 +461,7 @@ void psx_spu_sample_event_service(void) {
if ((psx_get_cycle_count() % 768u) != 0)
g_spu_sample_deferred_mismatches++;
}
if ((state.ctrl & 0x0040u) != 0 &&
if ((ctrl & 0x0040u) != 0 &&
(psx_get_cycle_count() % 768u) == 0) {
g_spu_sample_service_pumps++;
s_midframe_audio_pump();
+4
View File
@@ -1601,6 +1601,10 @@ uint32_t spu_ram_peek(uint32_t addr, uint8_t *out, uint32_t len) {
return len;
}
uint16_t spu_ctrl_read(void) {
return spu_regs[reg_index(0x1F801DAAu)];
}
void spu_get_global_state(SpuGlobalState* out) {
if (!out) return;
memset(out, 0, sizeof(*out));