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;