From e4e27f02b53592eb0b45f744ed88ea91531eb3af Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Tue, 21 Jul 2026 14:29:38 -0700 Subject: [PATCH] runtime: optimize MDEC FMV hot paths Reconstruct the shareable pieces from PR #58 onto current master: add contiguous MDEC DMA input bursts, reduce per-pixel/per-byte MDEC output overhead, add sparse IDCT fast paths, and keep the MDEC activity stamp sane across rematches. Also quiet the heavy debug trace recorders while MDEC video is active so debug builds can query state without starving FMV playback. Regression testing was done with non-debug OpenGL builds across multiple FMV-heavy titles. Debug-tool FMV playback was also exercised with the quiet gate enabled. Co-authored-by: Alex Vanderveen Co-authored-by: Cursor --- runtime/include/debug_server.h | 6 ++ runtime/include/mdec.h | 3 + runtime/src/debug_server.c | 32 ++++++- runtime/src/dma.c | 64 ++++++++++---- runtime/src/main.cpp | 1 + runtime/src/mdec.c | 148 ++++++++++++++++++++++++++++----- 6 files changed, 218 insertions(+), 36 deletions(-) diff --git a/runtime/include/debug_server.h b/runtime/include/debug_server.h index 796e8dc4..b8b5db41 100644 --- a/runtime/include/debug_server.h +++ b/runtime/include/debug_server.h @@ -91,6 +91,12 @@ void debug_server_get_status(int *listening, int *port, int *error); * Call once per vblank. */ void debug_server_poll(void); +/* FMV quiet mode suppresses high-frequency trace rings while MDEC video is + * active. The TCP command pump stays live; expensive per-dispatch/per-frame + * recording backs off so debug builds can still play FMVs at speed. */ +void debug_server_set_fmv_quiet(int quiet); +int debug_server_fmv_quiet(void); + /* TCP serve-stall telemetry: cumulative main-thread ms spent inside * bounded TCP sends, and clients dropped for exceeding the send budget. * Surfaced in the freeze heartbeat / wedge dumps so a TCP-throttled run diff --git a/runtime/include/mdec.h b/runtime/include/mdec.h index 2f91caab..0db6e52f 100644 --- a/runtime/include/mdec.h +++ b/runtime/include/mdec.h @@ -13,6 +13,9 @@ void mdec_write(uint32_t addr, uint32_t value); void mdec_dma_write_word(uint32_t value); uint32_t mdec_dma_read_word(void); +/* Contiguous LE bursts — bit-identical to N× word helpers; MotK FMV DMA. */ +uint32_t mdec_dma_write_words(const uint32_t *src, uint32_t max_words); +uint32_t mdec_dma_read_words(uint32_t *dst, uint32_t max_words); int mdec_dma_write_ready(void); int mdec_dma_read_ready(void); diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 3d3e3975..aea4c5b1 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -123,6 +123,22 @@ static size_t s_resp_len = 0, s_resp_cap = 0; static int s_resp_overflow = 0; static int s_in_command = 0; /* 1 while emu runs process_command */ static int io_thread_main(void *arg); /* defined near debug_server_poll */ +static volatile int s_fmv_quiet = 0; + +void debug_server_set_fmv_quiet(int quiet) +{ + static int enabled = -1; + if (enabled < 0) { + const char *env = getenv("PSX_DEBUG_FMV_QUIET"); + enabled = (!env || env[0] != '0') ? 1 : 0; + } + s_fmv_quiet = enabled && quiet; +} + +int debug_server_fmv_quiet(void) +{ + return s_fmv_quiet ? 1 : 0; +} /* ---- Frame counter (set by record_frame caller) ---- */ /* Non-static so other instrumentation (e.g. dirty_ram_interp.c) can stamp @@ -2019,6 +2035,7 @@ void debug_server_log_call_entry(uint32_t func_addr) { psx_native_stack_guard(func_addr); /* runs in debug AND release (before the early-return) */ #endif #ifndef PSX_NO_DEBUG_TOOLS + if (s_fmv_quiet) return; ls_suppress_begin(); if (s_synth_recurse_armed) { s_synth_recurse_armed = 0; psx_synth_recurse(0); } /* cyc_watch: universal compiled-function-entry hook (game AND BIOS, incl. @@ -2232,6 +2249,7 @@ void debug_server_cyc_observe(uint32_t block_leader_phys) { (void)block_leader_phys; return; #else + if (s_fmv_quiet) return; cyc_watch_observe(block_leader_phys & 0x1FFFFFFFu); /* #2 lockstep comparator: per-basic-block compiled-vs-interp check. Self-gates * on the armed frame window; ~free (one branch) when disarmed. */ @@ -2245,6 +2263,7 @@ void debug_server_trace_dispatch(uint32_t func_addr) { (void)func_addr; return; #endif + if (s_fmv_quiet) return; ls_suppress_begin(); /* cyc_watch: compiled-dispatch path. func_addr is already the physical * (normalized) block leader. Sampled before the block runs. */ @@ -8399,6 +8418,7 @@ void debug_server_trace_write_check(uint32_t phys, uint32_t old_val, (void)phys; (void)old_val; (void)new_val; (void)width; return; #endif + if (s_fmv_quiet) return; if (is_card_critical_addr(phys)) card_trace_record(phys, old_val, new_val, width); fp_record_write(phys, new_val, g_debug_last_store_pc); { @@ -8427,6 +8447,7 @@ void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width) (void)addr; (void)val; (void)width; return; #endif + if (s_fmv_quiet) return; /* First-divergence fingerprint + frame recorder also see device writes. */ fp_record_mmio(addr, val, g_debug_last_store_pc); rec_event(REC_KIND_MMIO_W, addr, val, g_debug_last_store_pc, @@ -8470,12 +8491,13 @@ void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width) * livelock that polls them. `val` is the value the CPU actually loaded. */ void debug_server_trace_mmio_read(uint32_t addr, uint32_t val, uint8_t width) { - rec_event(REC_KIND_MMIO_R, addr, val, g_debug_last_store_pc, - debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0); #ifdef PSX_NO_DEBUG_TOOLS (void)addr; (void)val; (void)width; return; #endif + if (s_fmv_quiet) return; + rec_event(REC_KIND_MMIO_R, addr, val, g_debug_last_store_pc, + debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0); if (!s_mmio_rtrace || s_mmio_rtrace_range_count == 0) return; if (capture_frozen()) return; uint32_t phys = addr & 0x1FFFFFFFu; @@ -12949,6 +12971,11 @@ void debug_server_poll(void) void debug_server_record_frame(void) { + if (s_fmv_quiet) { + s_history_count = s_frame_count + 1; + s_frame_count++; + return; + } if (!s_frame_history) return; if (!s_cpu) return; @@ -13041,6 +13068,7 @@ void debug_server_wait_if_paused(void) void debug_server_check_watchpoints(void) { + if (s_fmv_quiet) return; if (s_client == SOCK_INVALID) return; for (int i = 0; i < MAX_WATCHPOINTS; i++) { diff --git a/runtime/src/dma.c b/runtime/src/dma.c index a465cc47..2b856256 100644 --- a/runtime/src/dma.c +++ b/runtime/src/dma.c @@ -547,20 +547,43 @@ static void advance_mdec_channel(int ch, uint32_t cycles) { g_dma_cur_ch = 1; g_dma_cur_bcr = channels[1].bcr; g_dma_initiator_pc = s_dma_ch_initiator_pc[1]; } - while (a->remaining_words > 0 && words_budget > 0) { - if (ch == 0) { + /* Contiguous +4 ch0 (RAM→MDEC): feed via LE burst helper. Guest halfwords + * + decode triggers match the per-word loop; wraps / decrementing MADR + * and ch1 (needs psx_write_word watchers) stay on the word path. */ + if (ch == 0 && addr_step == 4) { + extern uint8_t *memory_get_ram_ptr(void); + uint8_t *ram = memory_get_ram_ptr(); + while (a->remaining_words > 0 && words_budget > 0) { if (!mdec_dma_write_ready()) break; - mdec_dma_write_word(psx_read_word(addr)); - } else { - if (!mdec_dma_read_ready()) break; - g_dma_cur_madr = addr; - psx_write_word(addr, mdec_dma_read_word()); + uint32_t n = a->remaining_words < words_budget + ? a->remaining_words : words_budget; + uint32_t max_by_ram = (0x200000u - addr) / 4u; + if (max_by_ram == 0) break; + if (n > max_by_ram) n = max_by_ram; + uint32_t got = + mdec_dma_write_words((const uint32_t *)(ram + addr), n); + if (got == 0) break; + addr = (addr + got * 4u) & 0x1FFFFCu; + a->remaining_words -= got; + words_budget -= got; + moved += got; } + } else { + while (a->remaining_words > 0 && words_budget > 0) { + if (ch == 0) { + if (!mdec_dma_write_ready()) break; + mdec_dma_write_word(psx_read_word(addr)); + } else { + if (!mdec_dma_read_ready()) break; + g_dma_cur_madr = addr; + psx_write_word(addr, mdec_dma_read_word()); + } - addr = (addr + addr_step) & 0x1FFFFCu; - a->remaining_words--; - words_budget--; - moved++; + addr = (addr + addr_step) & 0x1FFFFCu; + a->remaining_words--; + words_budget--; + moved++; + } } if (mdec_writes_ram) { g_dma_cur_ch = -1; g_dma_exec_depth--; } @@ -584,9 +607,22 @@ static void execute_ch0_mdec_in(void) { if (direction != 0) { mdec_debug_dma_in_start(addr, total_words); - for (uint32_t i = 0; i < total_words; i++) { - mdec_dma_write_word(psx_read_word(addr)); - addr = (addr + addr_step) & 0x1FFFFCu; + if (addr_step == 4 && total_words > 0 && + addr + total_words * 4u <= 0x200000u) { + extern uint8_t *memory_get_ram_ptr(void); + uint32_t got = mdec_dma_write_words( + (const uint32_t *)(memory_get_ram_ptr() + addr), total_words); + addr = (addr + got * 4u) & 0x1FFFFCu; + /* If the FIFO stalled mid-burst, finish any remainder word-wise. */ + for (uint32_t i = got; i < total_words; i++) { + mdec_dma_write_word(psx_read_word(addr)); + addr = (addr + 4u) & 0x1FFFFCu; + } + } else { + for (uint32_t i = 0; i < total_words; i++) { + mdec_dma_write_word(psx_read_word(addr)); + addr = (addr + addr_step) & 0x1FFFFCu; + } } mdec_debug_dma_in_end(addr, total_words); channels[0].madr = addr; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 394ba4d9..dd7265d0 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -2609,6 +2609,7 @@ static void load_transition_note(int read_active, int load_active, /* Called from gpu_vblank_tick() at each simulated vblank. */ static void sdl_vblank_present(void) { #ifndef PSX_NO_DEBUG_TOOLS + debug_server_set_fmv_quiet(mdec_recently_active(2)); /* Debug server: pause gate, poll commands, record frame, check watchpoints. */ debug_server_wait_if_paused(); debug_server_poll(); diff --git a/runtime/src/mdec.c b/runtime/src/mdec.c index b9446483..aa234182 100644 --- a/runtime/src/mdec.c +++ b/runtime/src/mdec.c @@ -162,6 +162,13 @@ static void append_byte(uint8_t value) { mdec.output[mdec.output_size++] = value; } +/* Reserve `bytes` of output room once (MotK FMV macroblocks write 512/768 + * bytes each). Avoids ensure_output_capacity per channel byte. */ +static uint8_t *output_reserve(uint32_t bytes) { + if (!ensure_output_capacity(mdec.output_size + bytes)) return NULL; + return mdec.output + mdec.output_size; +} + static uint8_t input_byte(uint32_t byte_index) { uint16_t hw = mdec.input[byte_index >> 1]; return (byte_index & 1u) ? (uint8_t)(hw >> 8) : (uint8_t)hw; @@ -223,22 +230,61 @@ static int mask9_clamp_s8(int v) { * bias differed from hardware → the washed/wrong-contrast FMV). The block buffer * holds int8-range samples on return. */ static void idct_block(int16_t *block) { + /* DC-only fast path: bit-identical to the loops below when AC is zero + * (common in dark/flat FMV macroblocks). Pass1 only fills tmp[*][0]; + * pass2 only multiplies by scale[*][0]. */ + int ac = 0; + for (int i = 1; i < 64; i++) ac |= block[i]; + if (!ac) { + int16_t tmp0[8]; + int dc = block[0]; + for (int x = 0; x < 8; x++) { + int sum = dc * (int)mdec.scale[x * 8]; + tmp0[x] = (int16_t)((sum + 0x4000) >> 15); + } + for (int col = 0; col < 8; col++) { + for (int x = 0; x < 8; x++) { + int sum = (int)tmp0[col] * (int)mdec.scale[x * 8]; + block[col * 8 + x] = + (int16_t)mask9_clamp_s8((sum + 0x4000) >> 15); + } + } + return; + } + int16_t tmp[64]; - /* pass 1 — int16 out, transposed */ + /* pass 1 — int16 out, transposed. Skip all-zero source columns + * (sparse AC after zig-zag); zero column ⇒ zero tmp[*][col]. */ for (int col = 0; col < 8; col++) { + const int16_t *src = block + col * 8; + int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3] + | (int)src[4] | (int)src[5] | (int)src[6] | (int)src[7]; + if (!col_or) { + for (int x = 0; x < 8; x++) tmp[x * 8 + col] = 0; + continue; + } for (int x = 0; x < 8; x++) { int sum = 0; + const int16_t *sc = mdec.scale + x * 8; for (int u = 0; u < 8; u++) - sum += (int)block[col * 8 + u] * (int)mdec.scale[x * 8 + u]; + sum += (int)src[u] * (int)sc[u]; tmp[x * 8 + col] = (int16_t)((sum + 0x4000) >> 15); } } - /* pass 2 — int8 out (Mask9ClampS8), no transpose */ + /* pass 2 — int8 out (Mask9ClampS8), no transpose. Same zero-row skip. */ for (int col = 0; col < 8; col++) { + const int16_t *src = tmp + col * 8; + int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3] + | (int)src[4] | (int)src[5] | (int)src[6] | (int)src[7]; + if (!col_or) { + for (int x = 0; x < 8; x++) block[col * 8 + x] = 0; + continue; + } for (int x = 0; x < 8; x++) { int sum = 0; + const int16_t *sc = mdec.scale + x * 8; for (int u = 0; u < 8; u++) - sum += (int)tmp[col * 8 + u] * (int)mdec.scale[x * 8 + u]; + sum += (int)src[u] * (int)sc[u]; block[col * 8 + x] = (int16_t)mask9_clamp_s8((sum + 0x4000) >> 15); } } @@ -300,7 +346,9 @@ static int rgb_to_555_chan(uint8_t c) { return v; } -static void append_rgb_pixel(int y, int cr, int cb) { +/* Emit one YCbCr pixel into a pre-reserved output cursor (bit-identical to + * the former append_byte path). Returns advanced cursor. */ +static uint8_t *emit_rgb_pixel(uint8_t *out, int y, int cr, int cb) { /* Beetle YCbCr_to_RGB (mdec.cpp:293-304): /256 coeffs (359,-88/-183,454), * +0x80 rounding, the reduced-precision GREEN mask (-88*cb &~0x1F, -183*cr * &~0x07) — the hardware quirk our old /1024 path lacked, the main green-hue @@ -319,34 +367,43 @@ static void append_rgb_pixel(int y, int cr, int cb) { uint16_t pixel_xor = (uint16_t)((mdec.output_bit15 ? 0x8000u : 0u) | (mdec.output_signed ? 0x4210u : 0u)); packed ^= pixel_xor; - append_byte((uint8_t)packed); - append_byte((uint8_t)(packed >> 8)); - } else { - /* 24bpp (mdec.cpp:370-393): rgb_xor = signed ? 0x80 : 0x00. */ - uint8_t rgb_xor = mdec.output_signed ? 0x80u : 0x00u; - append_byte((uint8_t)(ru ^ rgb_xor)); - append_byte((uint8_t)(gu ^ rgb_xor)); - append_byte((uint8_t)(bu ^ rgb_xor)); + out[0] = (uint8_t)packed; + out[1] = (uint8_t)(packed >> 8); + return out + 2; } + /* 24bpp (mdec.cpp:370-393): rgb_xor = signed ? 0x80 : 0x00. */ + uint8_t rgb_xor = mdec.output_signed ? 0x80u : 0x00u; + out[0] = (uint8_t)(ru ^ rgb_xor); + out[1] = (uint8_t)(gu ^ rgb_xor); + out[2] = (uint8_t)(bu ^ rgb_xor); + return out + 3; } static void append_luma_block(const int16_t *yblk) { - for (int i = 0; i < 64; i++) { - append_byte(to_output_u8(yblk[i])); - } + uint8_t *out = output_reserve(64u); + if (!out) return; + for (int i = 0; i < 64; i++) out[i] = to_output_u8(yblk[i]); + mdec.output_size += 64u; } static void append_color_macroblock(const int16_t *crblk, const int16_t *cbblk, const int16_t yblk[4][64]) { + /* 16×16: 512 bytes @15bpp, 768 @24bpp — reserve once per MB. */ + uint32_t need = (mdec.output_depth == 3) ? 512u : 768u; + uint8_t *out = output_reserve(need); + if (!out) return; + uint8_t *p = out; for (int py = 0; py < 16; py++) { for (int px = 0; px < 16; px++) { int y_index = (py >= 8 ? 2 : 0) + (px >= 8 ? 1 : 0); int lx = px & 7; int ly = py & 7; int chroma = (px >> 1) + (py >> 1) * 8; - append_rgb_pixel(yblk[y_index][lx + ly * 8], crblk[chroma], cbblk[chroma]); + p = emit_rgb_pixel(p, yblk[y_index][lx + ly * 8], + crblk[chroma], cbblk[chroma]); } } + mdec.output_size += need; } /* Monotonic count of decode invocations — the frontend FMV detector samples @@ -487,6 +544,9 @@ void mdec_init(void) { memset(mdec_trace, 0, sizeof(mdec_trace)); mdec_trace_seq = 0; mdec_trace_head = 0; + /* Rematch resets s_frame_count; a stale stamp makes mdec_recently_active + * wrap and lie for the whole next session. */ + mdec_last_color_decode_frame = (uint64_t)0 - 1000u; for (int i = 0; i < 64; i++) { mdec.y_quant[i] = 1; mdec.uv_quant[i] = 1; @@ -541,12 +601,50 @@ void mdec_dma_write_word(uint32_t value) { write_data(value); } +/* Feed up to `max_words` from a contiguous LE word source (DMA ch0). Stops + * early if the FIFO becomes not-ready after a decode completes mid-burst. + * Guest bytes + decode triggers match N× mdec_dma_write_word. */ +uint32_t mdec_dma_write_words(const uint32_t *src, uint32_t max_words) { + uint32_t moved = 0; + while (moved < max_words) { + if (!mdec_dma_write_ready()) break; + /* Fast fill while a DECODE/QUANT/SCALE command is collecting input — + * same halfword packing + single execute_command as write_data. */ + if (mdec.busy && mdec.input_count < mdec.expected_halfwords) { + uint32_t need_hw = mdec.expected_halfwords - mdec.input_count; + uint32_t need_words = (need_hw + 1u) / 2u; + uint32_t n = max_words - moved; + if (n > need_words) n = need_words; + for (uint32_t i = 0; i < n; i++) { + uint32_t value = src[moved++]; + mdec.input[mdec.input_count++] = (uint16_t)value; + if (mdec.input_count < mdec.expected_halfwords) { + mdec.input[mdec.input_count++] = (uint16_t)(value >> 16); + } + } + if (mdec.input_count >= mdec.expected_halfwords) { + execute_command(); + } + continue; + } + write_data(src[moved++]); + } + return moved; +} + uint32_t mdec_dma_read_word(void) { uint32_t value = 0; uint32_t start_pos = mdec.output_pos; - for (uint32_t i = 0; i < 4u; i++) { - if (mdec.output_pos < mdec.output_size) { - value |= (uint32_t)mdec.output[mdec.output_pos++] << (i * 8u); + uint32_t avail = (start_pos < mdec.output_size) + ? (mdec.output_size - start_pos) : 0u; + if (avail >= 4u) { + memcpy(&value, mdec.output + start_pos, sizeof(value)); + mdec.output_pos = start_pos + 4u; + } else { + for (uint32_t i = 0; i < 4u; i++) { + if (mdec.output_pos < mdec.output_size) { + value |= (uint32_t)mdec.output[mdec.output_pos++] << (i * 8u); + } } } mdec.dma_out_words++; @@ -563,6 +661,16 @@ uint32_t mdec_dma_read_word(void) { return value; } +/* Drain up to `max_words` into a contiguous LE destination (DMA ch1). */ +uint32_t mdec_dma_read_words(uint32_t *dst, uint32_t max_words) { + uint32_t moved = 0; + while (moved < max_words && mdec.output_pos < mdec.output_size) { + dst[moved] = mdec_dma_read_word(); + moved++; + } + return moved; +} + int mdec_dma_write_ready(void) { if (mdec.output_pos < mdec.output_size) return 0; return !mdec.busy || mdec.input_count < mdec.expected_halfwords;