From ec4306fbb32dcfd738ab05ddc84a34ef0002d329 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Thu, 4 Jun 2026 12:06:02 -0700 Subject: [PATCH] mmx6 framework: CD/DMA/MDEC + codegen fixes (Tomba builds+plays, MMX6 boots to intro) Framework work on codex/mmx6-support that makes Tomba build and play (title screen, 0 dispatch misses) and MMX6 boot through BIOS to its intro. Touches recompiler codegen (code_generator, function_analysis) and runtime CD-ROM/DMA/MDEC/GPU/SIO timing (cdrom, dma, mdec, gpu, psx_cycles, traps, fntrace, debug_server) + headers + runtime.cmake. Milestone snapshot for collapsing onto master. Build dir excluded. Co-Authored-By: Claude Opus 4.8 (1M context) --- TCP_COMMANDS.md | 11 +- recompiler/include/function_analysis.h | 1 + recompiler/src/code_generator.cpp | 140 +++++----- recompiler/src/function_analysis.cpp | 71 +++++ runtime/include/cdrom.h | 44 +++ runtime/include/debug_server.h | 4 + runtime/include/dma.h | 17 ++ runtime/include/fntrace.h | 1 + runtime/include/gpu.h | 4 + runtime/include/timers.h | 6 + runtime/runtime.cmake | 5 + runtime/src/cdrom.c | 372 +++++++++++++++++++++++-- runtime/src/debug_server.c | 333 ++++++++++++++++++++-- runtime/src/dma.c | 324 ++++++++++++++++++--- runtime/src/fntrace.c | 38 +++ runtime/src/gpu.c | 34 +++ runtime/src/main.cpp | 31 ++- runtime/src/mdec.c | 45 ++- runtime/src/psx_cycles.c | 2 + runtime/src/traps.c | 6 +- 20 files changed, 1289 insertions(+), 200 deletions(-) diff --git a/TCP_COMMANDS.md b/TCP_COMMANDS.md index 70db4693..1a4504c4 100644 --- a/TCP_COMMANDS.md +++ b/TCP_COMMANDS.md @@ -36,16 +36,21 @@ Columns: **N** = native, **D** = DuckStation oracle. | `dump_ram` | ✓ | ✓ | `addr`, `len` | Chunked RAM read (up to 64 KB) | | `read_scratch` | | ✓ | `addr`, `len` | Read PS1 scratchpad (0x1F800000 region) | | `read_vram` / `vram_peek` | ✓¹ | ✓ | `x`, `y`, `w`, `h` | Read 16-bit VRAM pixels (max 128×128) | -| `gpu_state` | ✓ | ✓ | — | Display area, draw offset, GPUSTAT, clip rect, xfer state | +| `gpu_state` | ✓ | ✓ | — | Display area, display depth, draw offset, GPUSTAT, clip rect, xfer state | | `sio_state` | ✓ | ✓ | — | SIO registers + (native only) pad/memcard protocol + TX/RX history | | `irq_state` | ✓ | ✓ | — | `I_STAT`, `I_MASK` (both), plus chain state on native | -| `dma_state` | | ✓ | — | DPCR, DICR, all 7 channel states (madr/bcr/chcr) | +| `dma_state` | ✓ | ✓ | — | DPCR, DICR, all 7 channel states (madr/bcr/chcr) | | `event_state` | | ✓ | — | EvCB table summary (stub on DS — events are BIOS-level) | | `overlay_state` | | ✓ | — | Current overlay info | +| `cdrom_sector_dump` | ✓ | | `offset`, `len` | Dump bytes from the last CD-ROM sector observed by the controller, including LBA/mode metadata | +| `cdrom_sector_history` | ✓ | | `count`, optional `lba` | Dump newest CD-ROM sector history entries, including raw XA subheader fields, CPU/audio delivery flags, and the first 128 bytes | +| `cdrom_sector_history_clear` | ✓ | | — | Reset the CD-ROM sector history ring | | `watch` | ✓ | ✓ | `addr` | Set byte-level memory watchpoint (fires per-frame on change) | | `unwatch` | ✓ | ✓ | `addr` | Remove memory watchpoint | | `set_input` | ✓ | ✓ | `buttons` | Override pad1 buttons (PS1 inverted bitmask, 0 = pressed) | | `clear_input` | ✓ | ✓ | — | Remove input override | +| `turbo` | ✓ | | `enabled` | Enable/disable TCP-controlled frontend turbo for fast-forward validation | +| `turbo_state` | ✓ | | — | Query TCP-controlled turbo state | | `pause` | ✓ | ✓ | — | Pause emulation | | `continue` (`c`) | ✓ | ✓ | — | Resume emulation | | `step` | ✓ | ✓ | `[count]` | Step N frames (default 1) | @@ -56,7 +61,7 @@ Columns: **N** = native, **D** = DuckStation oracle. | `frame_timeseries` | ✓ | ✓ | `start`, `end` | Compact timeseries, max 200 frames | | `set_snapshot` | ✓ | ✓ | `slot`, `addr`, `size` | Configure per-frame RAM snapshot region (slots 0-3) | | `get_snapshots` | ✓ | ✓ | — | Show snapshot config | -| `screenshot` | ✓ | ✓ | `path` | Save display to PNG | +| `screenshot` | ✓ | ✓ | `path` | Save display to PNG/file; native row dumps report `rgb555` or `rgb888` to match display depth | | `first_failure` | ✓ | | — | Find first divergence point between runs (native-side tracking) | | `read_frame_ram` | ✓ | | `addr`, `len`, `frame` | Read RAM **as of a specific frame** (from ring buffer) | | `wtrace_range` | ✓ | | `lo`, `hi` | Set RAM-write trace range (ring of 1024 writes with RA) | diff --git a/recompiler/include/function_analysis.h b/recompiler/include/function_analysis.h index 6751874a..c6f7fd49 100644 --- a/recompiler/include/function_analysis.h +++ b/recompiler/include/function_analysis.h @@ -24,6 +24,7 @@ struct FunctionAnalysisResult { int jr_ra_count; int prologue_count; int call_discovered_count = 0; // Functions found via JAL call-target following + int strong_prologue_count = 0; // Functions found from prologues with saved $ra int state_continuation_count = 0; // Split entries after calls to SaveState-style helpers }; diff --git a/recompiler/src/code_generator.cpp b/recompiler/src/code_generator.cpp index 075d9155..96edab42 100644 --- a/recompiler/src/code_generator.cpp +++ b/recompiler/src/code_generator.cpp @@ -1481,82 +1481,70 @@ std::string CodeGenerator::generate_file( // in CPUState, but these standalone functions provide correct reference behavior. ss << "/* --- Unaligned memory access helper implementations --- */\n"; ss << "\n"; - ss << "/* LWL: Load Word Left - merges high bytes from aligned word into rt.\n"; - ss << " * addr is the effective (possibly unaligned) address.\n"; - ss << " * rt_value is the current value of the destination register.\n"; - ss << " * MIPS big-endian convention: byte 0 is the most-significant byte.\n"; - ss << " * The PSX (little-endian) maps addr&3==0 -> load entire word,\n"; - ss << " * addr&3==1 -> load 3 bytes into high 24 bits, etc.\n"; - ss << " * Formula:\n"; - ss << " * aligned_addr = addr & ~3;\n"; - ss << " * shift = (addr & 3) * 8;\n"; - ss << " * mask = 0xFFFFFFFF << shift;\n"; - ss << " * result = (rt_value & ~mask) | (word << shift);\n"; - ss << " * where word = read_word(aligned_addr).\n"; - ss << " */\n"; - ss << "static uint32_t psx_lwl(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n"; - ss << " uint32_t aligned_addr = addr & ~3u;\n"; - ss << " uint32_t word = cpu->read_word(aligned_addr);\n"; - ss << " uint32_t shift = (addr & 3u) * 8u;\n"; - ss << " uint32_t mask = 0xFFFFFFFFu << shift;\n"; - ss << " return (rt_value & ~mask) | (word << shift);\n"; - ss << "}\n"; - ss << "\n"; - ss << "/* LWR: Load Word Right - merges low bytes from aligned word into rt.\n"; - ss << " * addr is the effective (possibly unaligned) address.\n"; - ss << " * rt_value is the current value of the destination register.\n"; - ss << " * Formula:\n"; - ss << " * aligned_addr = addr & ~3;\n"; - ss << " * shift = (3 - (addr & 3)) * 8;\n"; - ss << " * mask = 0xFFFFFFFF >> shift;\n"; - ss << " * result = (rt_value & ~mask) | (word >> shift);\n"; - ss << " * where word = read_word(aligned_addr).\n"; - ss << " */\n"; - ss << "static uint32_t psx_lwr(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n"; - ss << " uint32_t aligned_addr = addr & ~3u;\n"; - ss << " uint32_t word = cpu->read_word(aligned_addr);\n"; - ss << " uint32_t shift = (3u - (addr & 3u)) * 8u;\n"; - ss << " uint32_t mask = 0xFFFFFFFFu >> shift;\n"; - ss << " return (rt_value & ~mask) | (word >> shift);\n"; - ss << "}\n"; - ss << "\n"; - ss << "/* SWL: Store Word Left - stores the high bytes of rt_value into memory.\n"; - ss << " * addr is the effective (possibly unaligned) address.\n"; - ss << " * rt_value is the value of the source register.\n"; - ss << " * Formula:\n"; - ss << " * aligned_addr = addr & ~3;\n"; - ss << " * shift = (addr & 3) * 8;\n"; - ss << " * mem_mask = 0xFFFFFFFF >> (32 - shift) [i.e. low 'shift' bits preserved]\n"; - ss << " * Actually:\n"; - ss << " * mask = 0xFFFFFFFF >> shift; [bits to take from rt]\n"; - ss << " * write_word(aligned_addr,\n"; - ss << " * (read_word(aligned_addr) & ~mask) | (rt_value >> shift));\n"; - ss << " */\n"; - ss << "static void psx_swl(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n"; - ss << " uint32_t aligned_addr = addr & ~3u;\n"; - ss << " uint32_t existing = cpu->read_word(aligned_addr);\n"; - ss << " uint32_t shift = (addr & 3u) * 8u;\n"; - ss << " uint32_t mask = 0xFFFFFFFFu >> shift;\n"; - ss << " cpu->write_word(aligned_addr, (existing & ~mask) | (rt_value >> shift));\n"; - ss << "}\n"; - ss << "\n"; - ss << "/* SWR: Store Word Right - stores the low bytes of rt_value into memory.\n"; - ss << " * addr is the effective (possibly unaligned) address.\n"; - ss << " * rt_value is the value of the source register.\n"; - ss << " * Formula:\n"; - ss << " * aligned_addr = addr & ~3;\n"; - ss << " * shift = (3 - (addr & 3)) * 8;\n"; - ss << " * mask = 0xFFFFFFFF << shift;\n"; - ss << " * write_word(aligned_addr,\n"; - ss << " * (read_word(aligned_addr) & ~mask) | (rt_value << shift));\n"; - ss << " */\n"; - ss << "static void psx_swr(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n"; - ss << " uint32_t aligned_addr = addr & ~3u;\n"; - ss << " uint32_t existing = cpu->read_word(aligned_addr);\n"; - ss << " uint32_t shift = (3u - (addr & 3u)) * 8u;\n"; - ss << " uint32_t mask = 0xFFFFFFFFu << shift;\n"; - ss << " cpu->write_word(aligned_addr, (existing & ~mask) | (rt_value << shift));\n"; - ss << "}\n"; + ss << "/* LWL/LWR/SWL/SWR implement little-endian R3000A unaligned word\n"; + ss << " * semantics. Keep these switch tables in sync with psx_interpreter.c\n"; + ss << " * and dirty_ram_interp.c.\n"; + ss << " */\n"; + ss << "\n"; + ss << "/* LWL: Load Word Left - merges high bytes from aligned word into rt.\n"; + ss << " * addr is the effective (possibly unaligned) address.\n"; + ss << " * rt_value is the current value of the destination register.\n"; + ss << " */\n"; + ss << "static uint32_t psx_lwl(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n"; + ss << " uint32_t word = cpu->read_word(addr & ~3u);\n"; + ss << " switch (addr & 3u) {\n"; + ss << " case 0: return (rt_value & 0x00FFFFFFu) | (word << 24);\n"; + ss << " case 1: return (rt_value & 0x0000FFFFu) | (word << 16);\n"; + ss << " case 2: return (rt_value & 0x000000FFu) | (word << 8);\n"; + ss << " default: return word;\n"; + ss << " }\n"; + ss << "}\n"; + ss << "\n"; + ss << "/* LWR: Load Word Right - merges low bytes from aligned word into rt.\n"; + ss << " * addr is the effective (possibly unaligned) address.\n"; + ss << " * rt_value is the current value of the destination register.\n"; + ss << " */\n"; + ss << "static uint32_t psx_lwr(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n"; + ss << " uint32_t word = cpu->read_word(addr & ~3u);\n"; + ss << " switch (addr & 3u) {\n"; + ss << " case 0: return word;\n"; + ss << " case 1: return (rt_value & 0xFF000000u) | (word >> 8);\n"; + ss << " case 2: return (rt_value & 0xFFFF0000u) | (word >> 16);\n"; + ss << " default: return (rt_value & 0xFFFFFF00u) | (word >> 24);\n"; + ss << " }\n"; + ss << "}\n"; + ss << "\n"; + ss << "/* SWL: Store Word Left - stores the high bytes of rt_value into memory.\n"; + ss << " * addr is the effective (possibly unaligned) address.\n"; + ss << " * rt_value is the value of the source register.\n"; + ss << " */\n"; + ss << "static void psx_swl(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n"; + ss << " uint32_t aligned_addr = addr & ~3u;\n"; + ss << " uint32_t word = cpu->read_word(aligned_addr);\n"; + ss << " switch (addr & 3u) {\n"; + ss << " case 0: word = (word & 0xFFFFFF00u) | (rt_value >> 24); break;\n"; + ss << " case 1: word = (word & 0xFFFF0000u) | (rt_value >> 16); break;\n"; + ss << " case 2: word = (word & 0xFF000000u) | (rt_value >> 8); break;\n"; + ss << " default: word = rt_value; break;\n"; + ss << " }\n"; + ss << " cpu->write_word(aligned_addr, word);\n"; + ss << "}\n"; + ss << "\n"; + ss << "/* SWR: Store Word Right - stores the low bytes of rt_value into memory.\n"; + ss << " * addr is the effective (possibly unaligned) address.\n"; + ss << " * rt_value is the value of the source register.\n"; + ss << " */\n"; + ss << "static void psx_swr(CPUState* cpu, uint32_t addr, uint32_t rt_value) {\n"; + ss << " uint32_t aligned_addr = addr & ~3u;\n"; + ss << " uint32_t word = cpu->read_word(aligned_addr);\n"; + ss << " switch (addr & 3u) {\n"; + ss << " case 0: word = rt_value; break;\n"; + ss << " case 1: word = (word & 0x000000FFu) | (rt_value << 8); break;\n"; + ss << " case 2: word = (word & 0x0000FFFFu) | (rt_value << 16); break;\n"; + ss << " default: word = (word & 0x00FFFFFFu) | (rt_value << 24); break;\n"; + ss << " }\n"; + ss << " cpu->write_word(aligned_addr, word);\n"; + ss << "}\n"; ss << "\n"; // Make mutable copies for potential function splitting diff --git a/recompiler/src/function_analysis.cpp b/recompiler/src/function_analysis.cpp index 0b54332e..838c4022 100644 --- a/recompiler/src/function_analysis.cpp +++ b/recompiler/src/function_analysis.cpp @@ -77,6 +77,10 @@ static bool is_sw_reg_base(uint32_t instr, uint32_t base_reg, uint32_t value_reg return opcode == 0x2B && rs == base_reg && rt == value_reg; } +static bool is_sw_reg_sp(uint32_t instr, uint32_t value_reg) { + return is_sw_reg_base(instr, 29, value_reg); +} + static bool is_lui(uint32_t instr, uint32_t& rt_out) { uint32_t opcode = (instr >> 26) & 0x3F; if (opcode != 0x0F) return false; @@ -219,6 +223,7 @@ FunctionAnalysisResult FunctionAnalyzer::analyze() { result.jr_ra_count = 0; result.prologue_count = 0; result.call_discovered_count = 0; + result.strong_prologue_count = 0; result.state_continuation_count = 0; fmt::print("\n=== Function Boundary Detection ===\n\n"); @@ -304,6 +309,72 @@ FunctionAnalysisResult FunctionAnalyzer::analyze() { fmt::print("✓ Identified {} unique functions ({} call-discovered)\n\n", function_starts.size(), result.call_discovered_count); + // Pass 2.55: promote strong standalone prologues. + // + // Return-scanning alone misses scheduler/task entry points that never + // return normally, and direct JAL following misses callback entries whose + // addresses are derived at runtime. A standard stack-frame prologue that + // quickly saves $ra is a strong function-entry signal, provided the ADDIU + // is not itself a branch delay slot. + fmt::print("Finding strong standalone prologue entries...\n"); + int strong_prologues = 0; + for (uint32_t addr = exe_start; addr < exe_end; addr += 4) { + auto word_opt = exe_.read_word(addr); + if (!word_opt.has_value()) break; + + int32_t stack_size = 0; + if (!is_prologue(*word_opt, stack_size)) continue; + + if (addr >= exe_start + 4) { + auto prev_opt = exe_.read_word(addr - 4); + if (prev_opt.has_value() && is_branch_or_jump(*prev_opt)) { + continue; + } + } + + if (addr >= exe_start + 8) { + auto prev2_opt = exe_.read_word(addr - 8); + auto prev1_opt = exe_.read_word(addr - 4); + if (prev2_opt.has_value() && prev1_opt.has_value()) { + uint32_t lui_rt = 0; + if (is_lui(*prev2_opt, lui_rt) && + is_lw_same_reg_base(*prev1_opt, lui_rt)) { + continue; + } + } + } + + auto existing_it = function_starts.upper_bound(addr); + if (existing_it != function_starts.begin()) { + --existing_it; + auto return_it = function_last_return.find(*existing_it); + if (return_it != function_last_return.end() && + return_it->second + 8u > addr) { + continue; + } + } + + bool saves_ra = false; + for (uint32_t look = addr + 4; look < addr + 64 && look < exe_end; look += 4) { + auto next_opt = exe_.read_word(look); + if (!next_opt.has_value()) break; + if (is_sw_reg_sp(*next_opt, 31)) { + saves_ra = true; + break; + } + if (is_branch_or_jump(*next_opt)) { + break; + } + } + + if (saves_ra && function_starts.insert(addr).second) { + strong_prologues++; + } + } + result.strong_prologue_count = strong_prologues; + fmt::print("Identified {} strong prologue entries\n\n", + result.strong_prologue_count); + // Pass 2.6: discover continuations saved by setjmp/SaveState-style helpers. // // Some games save the caller's $ra into an application context object and diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index 59a40d1a..f35aa955 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -35,6 +35,7 @@ typedef struct CDROMDebugState { uint64_t seq; uint8_t index_reg; uint8_t stat_reg; + uint8_t request_reg; uint8_t irq_enable; uint8_t irq_flag; uint8_t mode_reg; @@ -62,8 +63,24 @@ typedef struct CDROMDebugState { int pending_delay; int pending_phase; uint32_t i_stat; + int last_sector_lba; + int last_sector_size; + uint32_t last_sector_frame; + uint8_t last_sector_mode; + uint8_t last_sector_have_raw; } CDROMDebugState; +typedef struct CDROMSectorDebugState { + int current_available; + int current_read_pos; + int current_size; + int last_lba; + int last_size; + uint32_t last_frame; + uint8_t last_mode; + uint8_t last_have_raw; +} CDROMSectorDebugState; + typedef struct CDROMTraceEntry { uint64_t seq; uint32_t addr; @@ -82,6 +99,7 @@ typedef struct CDROMTraceEntry { uint8_t response_read; uint8_t response_count; uint8_t sector_available; + uint8_t request_reg; uint8_t mode_reg; uint8_t pending_cmd; uint8_t pending_pending; @@ -94,10 +112,36 @@ typedef struct CDROMTraceEntry { } CDROMTraceEntry; #define CDROM_TRACE_CAP (1 << 16) +#define CDROM_SECTOR_HISTORY_CAP (1 << 10) +#define CDROM_SECTOR_HISTORY_BYTES 128 + +typedef struct CDROMSectorHistoryEntry { + uint64_t seq; + int lba; + int size; + uint32_t frame; + uint8_t mode; + uint8_t have_raw; + uint8_t raw_mode; + uint8_t xa_file; + uint8_t xa_channel; + uint8_t xa_submode; + uint8_t xa_coding; + uint8_t data_delivered; + uint8_t xa_audio_delivered; + uint8_t skip_reason; + uint16_t bytes_len; + uint8_t bytes[CDROM_SECTOR_HISTORY_BYTES]; +} CDROMSectorHistoryEntry; void cdrom_debug_snapshot(CDROMDebugState* out); uint64_t cdrom_debug_get_trace(const CDROMTraceEntry** out_entries); void cdrom_debug_clear_trace(void); +uint32_t cdrom_debug_copy_last_sector(uint32_t offset, uint32_t len, + uint8_t* out, + CDROMSectorDebugState* state); +uint64_t cdrom_debug_get_sector_history(const CDROMSectorHistoryEntry** out_entries); +void cdrom_debug_clear_sector_history(void); #ifdef __cplusplus } diff --git a/runtime/include/debug_server.h b/runtime/include/debug_server.h index c50d4588..cdd61ccf 100644 --- a/runtime/include/debug_server.h +++ b/runtime/include/debug_server.h @@ -166,6 +166,10 @@ void debug_server_check_watchpoints(void); * -1 if no override is active. Value is PS1 16-bit button mask. */ int debug_server_get_input_override(void); +/* TCP-controlled turbo mode. When enabled the frontend skips presentation and + * wall-clock pacing at vblank, matching the keyboard TAB turbo path. */ +int debug_server_turbo_enabled(void); + /* ---- Send helpers (for extensions) ---- */ /* Set the CPU state pointer for register queries. Call after CPU init. */ diff --git a/runtime/include/dma.h b/runtime/include/dma.h index 10b4059a..94927b88 100644 --- a/runtime/include/dma.h +++ b/runtime/include/dma.h @@ -27,9 +27,25 @@ void dma_init(void); uint32_t dma_read(uint32_t addr); void dma_write(uint32_t addr, uint32_t val); void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask); +void dma_advance(uint32_t cycles); uint32_t dma_get_dicr(void); uint32_t dma_get_dpcr(void); +typedef struct DMAChannelDebugState { + uint32_t madr; + uint32_t bcr; + uint32_t chcr; + uint32_t active; + uint32_t remaining_words; + uint32_t cycles_accum; +} DMAChannelDebugState; + +typedef struct DMADebugState { + uint32_t dpcr; + uint32_t dicr; + DMAChannelDebugState channels[7]; +} DMADebugState; + typedef struct DMATraceEntry { uint64_t seq; uint32_t frame; @@ -52,6 +68,7 @@ typedef struct DMATraceEntry { uint64_t dma_debug_get_trace(const DMATraceEntry** out_entries); void dma_debug_clear_trace(void); +void dma_debug_get_state(DMADebugState* out); #ifdef __cplusplus } diff --git a/runtime/include/fntrace.h b/runtime/include/fntrace.h index 13400f50..194328fe 100644 --- a/runtime/include/fntrace.h +++ b/runtime/include/fntrace.h @@ -61,6 +61,7 @@ void fntrace_arm(uint32_t target); /* add to arm list, or 0 to clear */ void fntrace_arm_clear(void); uint32_t fntrace_arm_count(void); uint32_t fntrace_arm_get(uint32_t i); +void fntrace_arm_from_env(const char *env_name); /* Reset ring (clears seq, leaves storage in place). */ void fntrace_clear(void); diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index eef98794..f0b78f47 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -27,10 +27,14 @@ const uint16_t* gpu_get_vram(void); /* Pointer to 1024x512 16-bit VRAM */ typedef struct { uint32_t display_x, display_y; /* VRAM start of display area (GP1(05h)) */ uint32_t width, height; /* Derived from display mode + ranges */ + int depth24; /* GP1(08h) display depth flag: RGB888 scanout */ int disabled; /* GP1(03h) display disable flag */ } GpuDisplayInfo; void gpu_get_display_info(GpuDisplayInfo* out); +void gpu_display_pixel_rgb(const GpuDisplayInfo* di, uint32_t x, uint32_t y, + uint8_t* r, uint8_t* g, uint8_t* b); +uint32_t gpu_display_pixel_argb(const GpuDisplayInfo* di, uint32_t x, uint32_t y); uint64_t gpu_get_gp0_count(void); /* Total GP0 writes since init */ void gpu_get_gp0_stats(uint64_t* nop, uint64_t* fill, uint64_t* draw, uint64_t* env, uint64_t* copy); diff --git a/runtime/include/timers.h b/runtime/include/timers.h index 53f0816d..591e12ed 100644 --- a/runtime/include/timers.h +++ b/runtime/include/timers.h @@ -10,6 +10,12 @@ extern "C" { #define TIMER_BASE 0x1F801100 void timers_init(void); +void timers_get_snapshot(uint16_t counter[3], uint32_t mode[3], + uint16_t target[3], int32_t irq_line[3], + uint32_t frac[3]); +void timers_set_snapshot(const uint16_t counter[3], const uint32_t mode[3], + const uint16_t target[3], const int32_t irq_line[3], + const uint32_t frac[3]); /* MMIO read/write for 0x1F801100..0x1F80112F. * Accepts 16-bit or 32-bit access (caller zero-extends). */ diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index c6d72644..32751371 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -233,3 +233,8 @@ function(psxrecomp_add_runtime_target target) $<$:/ENTRY:mainCRTStartup>) endif() endfunction() + +# Compatibility for early v4 game projects that used the longer helper name. +function(psxrecomp_v4_add_runtime_target target) + psxrecomp_add_runtime_target(${target} ${ARGN}) +endfunction() diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index abce8619..470a08c5 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -30,6 +30,7 @@ extern uint64_t s_frame_count; /* CD-ROM state */ static uint8_t index_reg; static uint8_t stat_reg; +static uint8_t request_reg; static uint8_t irq_enable; static uint8_t irq_flag; @@ -58,6 +59,19 @@ static uint8_t sector_buffer[SECTOR_BUFFER_SIZE]; static int sector_read_pos; static int sector_available; static int sector_size; +static uint8_t last_sector_buffer[SECTOR_BUFFER_SIZE]; +static int last_sector_lba; +static int last_sector_size; +static uint32_t last_sector_frame; +static uint8_t last_sector_mode; +static uint8_t last_sector_have_raw; +static uint8_t last_sector_raw_mode; +static uint8_t last_sector_xa_file; +static uint8_t last_sector_xa_channel; +static uint8_t last_sector_xa_submode; +static uint8_t last_sector_xa_coding; +static CDROMSectorHistoryEntry sector_history[CDROM_SECTOR_HISTORY_CAP]; +static uint64_t sector_history_seq; /* Seek target */ static uint8_t seek_min, seek_sec, seek_sect; @@ -75,15 +89,33 @@ static uint8_t cd_muted; #define XA_SUBHEADER_OFFSET 16 #define XA_DATA_OFFSET 24 #define XA_SOUND_GROUPS 18 -#define XA_NATIVE_FRAMES (XA_SOUND_GROUPS * 4 * 28) -#define XA_MAX_44100_FRAMES 4704 +#define XA_NATIVE_FRAMES (XA_SOUND_GROUPS * 8 * 28) +#define XA_MAX_44100_FRAMES 9408 #define XA_SUBMODE_AUDIO 0x04 +#define XA_SUBMODE_REALTIME 0x40 +#define CDROM_SECTOR_MODE2 0x02 + +#define CDROM_SKIP_NONE 0 +#define CDROM_SKIP_XA_AUDIO_REALTIME 1 +#define CDROM_REQUEST_BFRD 0x80u + +typedef struct CDROMSectorDelivery { + uint8_t raw_mode; + uint8_t xa_file; + uint8_t xa_channel; + uint8_t xa_submode; + uint8_t xa_coding; + uint8_t data_delivered; + uint8_t xa_audio_delivered; + uint8_t skip_reason; +} CDROMSectorDelivery; static int32_t xa_hist_l[2]; static int32_t xa_hist_r[2]; static uint8_t xa_stream_file; static uint8_t xa_stream_channel; +static uint8_t xa_stream_coding; static int xa_stream_active; /* Operating divisor: 1x during BIOS boot, switches to g_game_divisor @@ -157,6 +189,7 @@ static void trace_cdrom(uint8_t kind, uint32_t addr, uint32_t val, uint8_t width e->i_stat = i_stat; e->index_reg = index_reg; e->stat_reg = stat_reg; + e->request_reg = request_reg; e->irq_enable = irq_enable; e->irq_flag = irq_flag; e->mode_reg = mode_reg; @@ -174,6 +207,58 @@ static void trace_cdrom(uint8_t kind, uint32_t addr, uint32_t val, uint8_t width e->read_delay = read_delay; } +static int xa_is_audio_realtime(const CDROMSectorDelivery *d) { + return d && + d->raw_mode == CDROM_SECTOR_MODE2 && + ((d->xa_submode & (XA_SUBMODE_AUDIO | XA_SUBMODE_REALTIME)) == + (XA_SUBMODE_AUDIO | XA_SUBMODE_REALTIME)); +} + +static CDROMSectorDelivery classify_raw_sector(const uint8_t *raw_data, int have_raw) { + CDROMSectorDelivery d; + memset(&d, 0, sizeof(d)); + if (!have_raw || !raw_data) return d; + + d.raw_mode = raw_data[15]; + if (d.raw_mode == CDROM_SECTOR_MODE2) { + d.xa_file = raw_data[XA_SUBHEADER_OFFSET + 0]; + d.xa_channel = raw_data[XA_SUBHEADER_OFFSET + 1]; + d.xa_submode = raw_data[XA_SUBHEADER_OFFSET + 2]; + d.xa_coding = raw_data[XA_SUBHEADER_OFFSET + 3]; + } + return d; +} + +static void record_sector_history(int lba, int size, uint8_t mode, int have_raw, + const uint8_t *bytes, + const CDROMSectorDelivery *delivery) { + CDROMSectorHistoryEntry *e = + §or_history[sector_history_seq % CDROM_SECTOR_HISTORY_CAP]; + e->seq = sector_history_seq++; + e->lba = lba; + e->size = size; + e->frame = (uint32_t)s_frame_count; + e->mode = mode; + e->have_raw = (uint8_t)(have_raw ? 1 : 0); + e->raw_mode = delivery ? delivery->raw_mode : 0; + e->xa_file = delivery ? delivery->xa_file : 0; + e->xa_channel = delivery ? delivery->xa_channel : 0; + e->xa_submode = delivery ? delivery->xa_submode : 0; + e->xa_coding = delivery ? delivery->xa_coding : 0; + e->data_delivered = delivery ? delivery->data_delivered : 0; + e->xa_audio_delivered = delivery ? delivery->xa_audio_delivered : 0; + e->skip_reason = delivery ? delivery->skip_reason : CDROM_SKIP_NONE; + e->bytes_len = (uint16_t)((size < CDROM_SECTOR_HISTORY_BYTES) + ? size : CDROM_SECTOR_HISTORY_BYTES); + if (e->bytes_len && bytes) { + memcpy(e->bytes, bytes, e->bytes_len); + } + if (e->bytes_len < CDROM_SECTOR_HISTORY_BYTES) { + memset(e->bytes + e->bytes_len, 0, + CDROM_SECTOR_HISTORY_BYTES - e->bytes_len); + } +} + static int has_disc(void) { return iso_handle != NULL; } @@ -233,6 +318,15 @@ static uint8_t bin_to_bcd(int val) { return (uint8_t)(((val / 10) << 4) | (val % 10)); } +static void lba_to_msf(int lba, int pregap, int* m, int* s, int* f) { + int frames = lba + pregap; + if (frames < 0) frames = 0; + *m = frames / (60 * 75); + frames %= 60 * 75; + *s = frames / 75; + *f = frames % 75; +} + static int16_t clamp16_cd(int32_t v) { if (v > 32767) return 32767; if (v < -32768) return -32768; @@ -244,6 +338,7 @@ static void xa_reset_decode(void) { xa_hist_r[0] = xa_hist_r[1] = 0; xa_stream_file = 0xFF; xa_stream_channel = 0xFF; + xa_stream_coding = 0xFF; xa_stream_active = 0; } @@ -292,6 +387,44 @@ static int xa_decode_sector_4bit_stereo(const uint8_t* data, int16_t* out) { return pair; } +static int xa_decode_sector_4bit_mono(const uint8_t* data, int16_t* out) { + static const int k0[5] = { 0, 60, 115, 98, 122 }; + static const int k1[5] = { 0, 0, -52, -55, -60 }; + int pair = 0; + + for (int g = 0; g < XA_SOUND_GROUPS; g++) { + const uint8_t* grp = data + g * 128; + for (int blk = 0; blk < 4; blk++) { + for (int nibble = 0; nibble < 2; nibble++) { + uint8_t hdr = grp[4 + blk * 2 + nibble]; + int shift = 12 - (int)(hdr & 0x0F); + int filter = (hdr >> 4) & 0x03; + if (shift < 0) shift = 0; + + for (int i = 0; i < 28; i++) { + uint8_t b = grp[16 + blk + i * 4]; + int32_t sample_nibble = nibble ? ((b >> 4) & 0x0F) : (b & 0x0F); + if (sample_nibble >= 8) sample_nibble -= 16; + + int32_t sample = (sample_nibble << shift) + + ((k0[filter] * xa_hist_l[0] + k1[filter] * xa_hist_l[1] + 32) >> 6); + sample = clamp16_cd(sample); + xa_hist_l[1] = xa_hist_l[0]; + xa_hist_l[0] = sample; + + out[pair * 2 + 0] = (int16_t)sample; + out[pair * 2 + 1] = (int16_t)sample; + pair++; + } + } + } + } + + xa_hist_r[0] = xa_hist_l[0]; + xa_hist_r[1] = xa_hist_l[1]; + return pair; +} + static int xa_resample_to_44100(const int16_t* in, int in_frames, int sample_rate, int16_t* out, int max_frames) { if (!in || !out || in_frames <= 0 || sample_rate <= 0 || max_frames <= 0) return 0; @@ -319,40 +452,46 @@ static int xa_resample_to_44100(const int16_t* in, int in_frames, return out_frames; } -static void maybe_deliver_xa_audio(const uint8_t* raw_data) { - if (!(mode_reg & 0x40u) || !raw_data || cd_muted) return; +static int maybe_deliver_xa_audio(const uint8_t* raw_data, + const CDROMSectorDelivery *delivery) { + if (!(mode_reg & 0x40u) || !raw_data || !delivery || cd_muted) return 0; + if (!xa_is_audio_realtime(delivery)) return 0; - uint8_t file = raw_data[XA_SUBHEADER_OFFSET + 0]; - uint8_t channel = raw_data[XA_SUBHEADER_OFFSET + 1]; - uint8_t submode = raw_data[XA_SUBHEADER_OFFSET + 2]; - uint8_t coding = raw_data[XA_SUBHEADER_OFFSET + 3]; - if (!(submode & XA_SUBMODE_AUDIO)) return; + uint8_t file = delivery->xa_file; + uint8_t channel = delivery->xa_channel; + uint8_t coding = delivery->xa_coding; if ((mode_reg & 0x08u) && (file != filter_file || channel != filter_channel)) { trace_cdrom('a', 0, ((uint32_t)file << 16) | ((uint32_t)channel << 8) | coding, 0); - return; + return 0; } int stereo = (coding & 0x01u) != 0; int rate_code = (coding >> 2) & 0x03; int depth_code = (coding >> 4) & 0x03; int sample_rate = (rate_code == 0) ? 37800 : ((rate_code == 1) ? 18900 : 0); - if (!stereo || depth_code != 0 || sample_rate == 0) { + if (depth_code != 0 || sample_rate == 0) { trace_cdrom('X', 0, ((uint32_t)file << 16) | ((uint32_t)channel << 8) | coding, 0); - return; + return 0; } - if (!xa_stream_active || xa_stream_file != file || xa_stream_channel != channel) { + if (!xa_stream_active || + xa_stream_file != file || + xa_stream_channel != channel || + xa_stream_coding != coding) { xa_reset_decode(); xa_stream_file = file; xa_stream_channel = channel; + xa_stream_coding = coding; xa_stream_active = 1; } int16_t native[XA_NATIVE_FRAMES * 2]; int16_t pcm_44100[XA_MAX_44100_FRAMES * 2]; - int native_frames = xa_decode_sector_4bit_stereo(raw_data + XA_DATA_OFFSET, native); + int native_frames = stereo + ? xa_decode_sector_4bit_stereo(raw_data + XA_DATA_OFFSET, native) + : xa_decode_sector_4bit_mono(raw_data + XA_DATA_OFFSET, native); int out_frames = xa_resample_to_44100(native, native_frames, sample_rate, pcm_44100, XA_MAX_44100_FRAMES); spu_cd_audio_push(pcm_44100, out_frames); @@ -360,18 +499,21 @@ static void maybe_deliver_xa_audio(const uint8_t* raw_data) { ((uint32_t)file << 24) | ((uint32_t)channel << 16) | ((uint32_t)coding << 8) | ((uint32_t)(out_frames / 32) & 0xFFu), 0); + return 1; } -static void read_sector_at(int min, int sec, int sect) { +static int read_sector_at(int min, int sec, int sect) { int lba = msf_to_lba(min, sec, sect); uint8_t user_data[SECTOR_SIZE]; uint8_t raw_data[RAW_SECTOR_SIZE]; int have_raw = 0; + CDROMSectorDelivery delivery; + const uint8_t *history_bytes = user_data; + int history_size = SECTOR_SIZE; if (iso_handle) { have_raw = iso_read_raw_sector(iso_handle, lba, raw_data, RAW_SECTOR_SIZE); if (have_raw) { - maybe_deliver_xa_audio(raw_data); memcpy(user_data, raw_data + RAW_USER_DATA_OFFSET, SECTOR_SIZE); } else if (!iso_read_sector(iso_handle, lba, user_data, SECTOR_SIZE)) { memset(user_data, 0, sizeof(user_data)); @@ -380,11 +522,23 @@ static void read_sector_at(int min, int sec, int sect) { memset(user_data, 0, sizeof(user_data)); } + delivery = classify_raw_sector(raw_data, have_raw); + delivery.xa_audio_delivered = + (uint8_t)maybe_deliver_xa_audio(raw_data, &delivery); + delivery.data_delivered = 1; + if (delivery.xa_audio_delivered || + ((mode_reg & 0x08u) && xa_is_audio_realtime(&delivery))) { + delivery.data_delivered = 0; + delivery.skip_reason = CDROM_SKIP_XA_AUDIO_REALTIME; + } + memset(sector_buffer, 0, sizeof(sector_buffer)); - if (mode_reg & 0x20) { + if (delivery.data_delivered && (mode_reg & 0x20)) { if (have_raw) { memcpy(sector_buffer, raw_data + WHOLE_SECTOR_OFFSET, WHOLE_SECTOR_SIZE); sector_size = WHOLE_SECTOR_SIZE; + history_bytes = sector_buffer; + history_size = sector_size; } else { sector_buffer[0] = bin_to_bcd(min); sector_buffer[1] = bin_to_bcd(sec); @@ -392,15 +546,52 @@ static void read_sector_at(int min, int sec, int sect) { sector_buffer[3] = 0x02; /* Mode 2 sector. */ memcpy(sector_buffer + FALLBACK_SECTOR_HEADER_SIZE, user_data, SECTOR_SIZE); sector_size = FALLBACK_WHOLE_SECTOR_SIZE; + history_bytes = sector_buffer; + history_size = sector_size; } - } else { + } else if (delivery.data_delivered) { memcpy(sector_buffer, user_data, SECTOR_SIZE); sector_size = SECTOR_SIZE; + history_bytes = sector_buffer; + history_size = sector_size; + } else { + sector_size = 0; } sector_read_pos = 0; - sector_available = 1; + sector_available = delivery.data_delivered ? 1 : 0; + if (delivery.data_delivered) { + memcpy(last_sector_buffer, sector_buffer, (size_t)sector_size); + } else { + uint32_t copy_size = history_size < SECTOR_BUFFER_SIZE ? (uint32_t)history_size + : SECTOR_BUFFER_SIZE; + memcpy(last_sector_buffer, history_bytes, copy_size); + if (copy_size < SECTOR_BUFFER_SIZE) { + memset(last_sector_buffer + copy_size, 0, SECTOR_BUFFER_SIZE - copy_size); + } + } + last_sector_lba = lba; + last_sector_size = delivery.data_delivered ? sector_size : history_size; + last_sector_frame = (uint32_t)s_frame_count; + last_sector_mode = mode_reg; + last_sector_have_raw = (uint8_t)(have_raw ? 1 : 0); + last_sector_raw_mode = delivery.raw_mode; + last_sector_xa_file = delivery.xa_file; + last_sector_xa_channel = delivery.xa_channel; + last_sector_xa_submode = delivery.xa_submode; + last_sector_xa_coding = delivery.xa_coding; + record_sector_history(lba, history_size, mode_reg, have_raw, + history_bytes, &delivery); trace_cdrom('S', 0, (uint32_t)lba, 0); + if (!delivery.data_delivered) { + trace_cdrom('s', 0, + ((uint32_t)delivery.xa_file << 24) | + ((uint32_t)delivery.xa_channel << 16) | + ((uint32_t)delivery.xa_submode << 8) | + delivery.xa_coding, + 0); + } + return delivery.data_delivered ? 1 : 0; } static void advance_msf(int* m, int* s, int* f) { @@ -413,6 +604,7 @@ static void clear_sector_buffer(void) { sector_read_pos = 0; sector_size = 0; sector_available = 0; + request_reg &= (uint8_t)~CDROM_REQUEST_BFRD; } static void start_read_stream(uint8_t cmd) { @@ -436,13 +628,15 @@ static void stop_read_stream(void) { read_delay = 0; } -static void deliver_read_sector(void) { - read_sector_at(read_min, read_sec, read_sect); +static int deliver_read_sector(void) { + int delivered = read_sector_at(read_min, read_sec, read_sect); advance_msf(&read_min, &read_sec, &read_sect); + if (!delivered) return 0; response_clear(); response_push(stat_reg); set_irq(CDIRQ_DATA_READY); fire_cdrom_irq(); + return 1; } static void exec_command(uint8_t cmd) { @@ -541,6 +735,60 @@ static void exec_command(uint8_t cmd) { set_irq(CDIRQ_ACK); break; + case 0x0F: /* Getparam */ + response_push(stat_reg); + response_push(mode_reg); + response_push(0x00); + response_push(filter_file); + response_push(filter_channel); + set_irq(CDIRQ_ACK); + break; + + case 0x10: { /* GetlocL */ + int lba = (last_sector_lba >= 0) + ? last_sector_lba + : msf_to_lba(read_min, read_sec, read_sect); + int m, s, f; + lba_to_msf(lba, 150, &m, &s, &f); + response_push(bin_to_bcd(m)); + response_push(bin_to_bcd(s)); + response_push(bin_to_bcd(f)); + response_push(last_sector_raw_mode ? last_sector_raw_mode : CDROM_SECTOR_MODE2); + response_push(last_sector_xa_file); + response_push(last_sector_xa_channel); + response_push(last_sector_xa_submode); + response_push(last_sector_xa_coding); + set_irq(CDIRQ_ACK); + break; + } + + case 0x11: { /* GetlocP */ + int lba = (last_sector_lba >= 0) + ? last_sector_lba + : msf_to_lba(read_min, read_sec, read_sect); + int rm, rs, rf; + int am, as, af; + lba_to_msf(lba, 0, &rm, &rs, &rf); + lba_to_msf(lba, 150, &am, &as, &af); + response_push(0x01); /* single data track */ + response_push(0x01); + response_push(bin_to_bcd(rm)); + response_push(bin_to_bcd(rs)); + response_push(bin_to_bcd(rf)); + response_push(bin_to_bcd(am)); + response_push(bin_to_bcd(as)); + response_push(bin_to_bcd(af)); + set_irq(CDIRQ_ACK); + break; + } + + case 0x13: /* GetTN */ + response_push(stat_reg); + response_push(0x01); + response_push(0x01); + set_irq(CDIRQ_ACK); + break; + case 0x15: /* SeekL */ if (!has_disc()) { response_push(stat_reg | CDSTAT_ERROR | CDSTAT_SEEKERR); @@ -630,11 +878,13 @@ static void process_pending(uint32_t cycles) { case 0x06: /* ReadN data ready */ case 0x1B: /* ReadS data ready */ if (reading) { - read_sector_at(read_min, read_sec, read_sect); + int delivered = read_sector_at(read_min, read_sec, read_sect); advance_msf(&read_min, &read_sec, &read_sect); - response_push(stat_reg); - set_irq(CDIRQ_DATA_READY); - fire_cdrom_irq(); + if (delivered) { + response_push(stat_reg); + set_irq(CDIRQ_DATA_READY); + fire_cdrom_irq(); + } } break; @@ -713,8 +963,10 @@ void cdrom_init(const char* cue_path) { memset(param_fifo, 0, sizeof(param_fifo)); memset(response_fifo, 0, sizeof(response_fifo)); memset(sector_buffer, 0, sizeof(sector_buffer)); + memset(last_sector_buffer, 0, sizeof(last_sector_buffer)); index_reg = 0; + request_reg = 0; irq_enable = 0x1F; irq_flag = 0; param_count = 0; @@ -723,6 +975,16 @@ void cdrom_init(const char* cue_path) { sector_read_pos = 0; sector_size = 0; sector_available = 0; + last_sector_lba = -1; + last_sector_size = 0; + last_sector_frame = 0; + last_sector_mode = 0; + last_sector_have_raw = 0; + last_sector_raw_mode = 0; + last_sector_xa_file = 0; + last_sector_xa_channel = 0; + last_sector_xa_submode = 0; + last_sector_xa_coding = 0; stop_read_stream(); mode_reg = 0; filter_file = 0; @@ -732,6 +994,7 @@ void cdrom_init(const char* cue_path) { spu_cd_audio_reset(); pending.pending = 0; seek_min = seek_sec = seek_sect = 0; + cdrom_debug_clear_sector_history(); if (cue_path) { iso_handle = iso_open(cue_path); @@ -751,7 +1014,7 @@ uint32_t cdrom_read(uint32_t addr) { if (param_count == 0) s |= (1 << 3); if (param_count < PARAM_FIFO_SIZE) s |= (1 << 4); if (response_read < response_count) s |= (1 << 5); - if (sector_available) s |= (1 << 6); + if (request_reg & CDROM_REQUEST_BFRD) s |= (1 << 6); ret = s; break; } @@ -763,10 +1026,11 @@ uint32_t cdrom_read(uint32_t addr) { break; case 0x1F801802: - if (sector_available && sector_read_pos < sector_size) { + if ((request_reg & CDROM_REQUEST_BFRD) && sector_available && sector_read_pos < sector_size) { ret = sector_buffer[sector_read_pos++]; if (sector_read_pos >= sector_size) { sector_available = 0; + request_reg &= (uint8_t)~CDROM_REQUEST_BFRD; } } break; @@ -814,7 +1078,12 @@ void cdrom_write(uint32_t addr, uint32_t value) { break; case 0x1F801803: - if (index_reg == 1) { + if (index_reg == 0) { + request_reg = val; + if (!(request_reg & CDROM_REQUEST_BFRD)) { + sector_read_pos = 0; + } + } else if (index_reg == 1) { irq_flag &= ~(val & 0x1F); if (val & 0x40) { param_count = 0; @@ -838,11 +1107,12 @@ void cdrom_tick(void) { uint32_t cdrom_dma_read(void) { uint32_t val = 0; - if (sector_available && sector_read_pos + 4 <= sector_size) { + if ((request_reg & CDROM_REQUEST_BFRD) && sector_available && sector_read_pos + 4 <= sector_size) { memcpy(&val, sector_buffer + sector_read_pos, 4); sector_read_pos += 4; if (sector_read_pos >= sector_size) { sector_available = 0; + request_reg &= (uint8_t)~CDROM_REQUEST_BFRD; } } trace_cdrom('D', 0, val, 4); @@ -850,7 +1120,9 @@ uint32_t cdrom_dma_read(void) { } int cdrom_dma_ready(void) { - return sector_available && (sector_read_pos < sector_size); + return (request_reg & CDROM_REQUEST_BFRD) && + sector_available && + (sector_read_pos + 4 <= sector_size); } void cdrom_debug_snapshot(CDROMDebugState* out) { @@ -859,6 +1131,7 @@ void cdrom_debug_snapshot(CDROMDebugState* out) { out->seq = cdrom_trace_seq; out->index_reg = index_reg; out->stat_reg = stat_reg; + out->request_reg = request_reg; out->irq_enable = irq_enable; out->irq_flag = irq_flag; out->mode_reg = mode_reg; @@ -886,6 +1159,11 @@ void cdrom_debug_snapshot(CDROMDebugState* out) { out->pending_delay = pending.delay; out->pending_phase = pending.phase; out->i_stat = i_stat; + out->last_sector_lba = last_sector_lba; + out->last_sector_size = last_sector_size; + out->last_sector_frame = last_sector_frame; + out->last_sector_mode = last_sector_mode; + out->last_sector_have_raw = last_sector_have_raw; } uint64_t cdrom_debug_get_trace(const CDROMTraceEntry** out_entries) { @@ -897,3 +1175,37 @@ void cdrom_debug_clear_trace(void) { memset(cdrom_trace, 0, sizeof(cdrom_trace)); cdrom_trace_seq = 0; } + +uint64_t cdrom_debug_get_sector_history(const CDROMSectorHistoryEntry** out_entries) { + if (out_entries) *out_entries = sector_history; + return sector_history_seq; +} + +void cdrom_debug_clear_sector_history(void) { + memset(sector_history, 0, sizeof(sector_history)); + sector_history_seq = 0; +} + +uint32_t cdrom_debug_copy_last_sector(uint32_t offset, uint32_t len, + uint8_t* out, + CDROMSectorDebugState* state) { + if (state) { + memset(state, 0, sizeof(*state)); + state->current_available = sector_available; + state->current_read_pos = sector_read_pos; + state->current_size = sector_size; + state->last_lba = last_sector_lba; + state->last_size = last_sector_size; + state->last_frame = last_sector_frame; + state->last_mode = last_sector_mode; + state->last_have_raw = last_sector_have_raw; + } + + if (!out || last_sector_size <= 0 || offset >= (uint32_t)last_sector_size) { + return 0; + } + uint32_t avail = (uint32_t)last_sector_size - offset; + if (len > avail) len = avail; + memcpy(out, last_sector_buffer + offset, len); + return len; +} diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 80eef75d..31c0e3ea 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -18,6 +18,7 @@ #include "mdec.h" #include "interrupts.h" #include "psx_cycles.h" +#include "timers.h" #include "dirty_ram_interp.h" #include "card_read_summary.h" #include "card_data_writes.h" @@ -105,6 +106,9 @@ static uint64_t s_dirty_break_hits = 0; static int s_input_override = -1; static int s_input_frames = 0; +/* ---- Frontend turbo override ---- */ +static volatile int s_turbo_enabled = 0; + /* ---- Ring buffer (heap-allocated) ---- */ static PSXFrameRecord *s_frame_history = NULL; static uint64_t s_history_count = 0; @@ -1158,11 +1162,37 @@ static void evcb_snapshot_capture(EvCBTag tag, uint64_t fn_entry_seq) { static int s_fn_trace_active = 0; static uint32_t s_fn_trace_filter_lo = 0u; static uint32_t s_fn_trace_filter_hi = 0xFFFFFFFFu; +static uint64_t s_fn_direct_seen = 0; +static uint64_t s_fn_direct_no_cpu = 0; +static uint64_t s_fn_direct_filtered = 0; static int fn_trace_in_filter(uint32_t phys) { + if (!s_fn_trace_active) return 0; return phys >= s_fn_trace_filter_lo && phys < s_fn_trace_filter_hi; } +static void fn_trace_filter_from_env(const char *env_name) { + const char *spec = getenv(env_name); + if (!spec || !*spec) return; + + char *end = NULL; + uint32_t lo = (uint32_t)strtoul(spec, &end, 0); + if (end == spec) return; + + while (*end == ' ' || *end == '\t') end++; + if (*end != ':' && *end != '-' && *end != ',' && *end != ';') return; + end++; + + while (*end == ' ' || *end == '\t') end++; + char *end2 = NULL; + uint32_t hi = (uint32_t)strtoul(end, &end2, 0); + if (end2 == end || hi <= lo) return; + + s_fn_trace_filter_lo = lo; + s_fn_trace_filter_hi = hi; + s_fn_trace_active = 1; +} + static int card_mgr_trace_target(uint32_t phys) { phys &= 0x1FFFFFFFu; if (phys >= 0x1FC0B600u && phys < 0x1FC0C240u) return 1; @@ -1571,12 +1601,17 @@ void debug_server_log_call_entry(uint32_t func_addr) { (void)func_addr; return; #endif - if (!debug_cpu_ptr) return; + s_fn_direct_seen++; + if (!debug_cpu_ptr) { + s_fn_direct_no_cpu++; + return; + } card_mgr_trace_record(func_addr, 0); sreg_trace_record(func_addr); call_focus_record(func_addr); if (!s_fn_entry) return; if (!fn_trace_in_filter(func_addr)) return; + s_fn_direct_filtered++; FnEntryEntry *e = &s_fn_entry[s_fn_entry_seq % FN_TRACE_CAP]; e->seq = s_fn_entry_seq; e->paired_exit_seq = 0; @@ -3293,6 +3328,7 @@ static void handle_gpu_state(int id, const char *json) send_fmt("{\"id\":%d,\"ok\":true," "\"display_x\":%d,\"display_y\":%d," "\"width\":%d,\"height\":%d," + "\"depth\":%d,\"depth24\":%d," "\"disabled\":%d," "\"gpustat\":\"0x%08X\"," "\"gp0_writes\":%llu," @@ -3301,6 +3337,7 @@ static void handle_gpu_state(int id, const char *json) "\"draw_offset\":[%d,%d]}", id, di.display_x, di.display_y, di.width, di.height, + di.depth24 ? 24 : 15, di.depth24, di.disabled, gpustat, (unsigned long long)gpu_get_gp0_count(), @@ -3360,6 +3397,26 @@ static void handle_irq_state(int id, const char *json) dma_get_dpcr(), dma_get_dicr()); } +static void handle_timers_state(int id, const char *json) +{ + (void)json; + uint16_t counter[3], target[3]; + uint32_t mode[3], frac[3]; + int32_t irq_line[3]; + timers_get_snapshot(counter, mode, target, irq_line, frac); + send_fmt("{\"id\":%d,\"ok\":true,\"timers\":[" + "{\"ch\":0,\"counter\":%u,\"mode\":\"0x%04X\",\"target\":%u," + "\"irq_line\":%d,\"frac\":%u}," + "{\"ch\":1,\"counter\":%u,\"mode\":\"0x%04X\",\"target\":%u," + "\"irq_line\":%d,\"frac\":%u}," + "{\"ch\":2,\"counter\":%u,\"mode\":\"0x%04X\",\"target\":%u," + "\"irq_line\":%d,\"frac\":%u}]}", + id, + counter[0], mode[0], target[0], irq_line[0], frac[0], + counter[1], mode[1], target[1], irq_line[1], frac[1], + counter[2], mode[2], target[2], irq_line[2], frac[2]); +} + /* GPU opcode counter — defined in gpu.c */ static const char *cdrom_trace_kind_name(uint8_t kind) { @@ -3370,6 +3427,7 @@ static const char *cdrom_trace_kind_name(uint8_t kind) case 'F': return "fire_irq"; case 'f': return "irq_masked"; case 'S': return "sector"; + case 's': return "sector_skip"; case 'A': return "xa_audio"; case 'a': return "xa_skip"; case 'X': return "xa_unsupported"; @@ -3389,6 +3447,7 @@ static void handle_cdrom_state(int id, const char *json) send_fmt("{\"id\":%d,\"ok\":true," "\"seq\":%llu,\"has_disc\":%d," "\"index\":%u,\"stat\":\"0x%02X\"," + "\"request\":\"0x%02X\"," "\"irq_enable\":\"0x%02X\",\"irq_flag\":\"0x%02X\"," "\"mode\":\"0x%02X\"," "\"param_count\":%d,\"response_read\":%d,\"response_count\":%d," @@ -3398,9 +3457,11 @@ static void handle_cdrom_state(int id, const char *json) "\"filter_file\":%u,\"filter_channel\":%u,\"muted\":%u," "\"seek_msf\":[%u,%u,%u]," "\"pending\":{\"cmd\":\"0x%02X\",\"active\":%d,\"delay\":%d,\"phase\":%d}," + "\"last_sector\":{\"lba\":%d,\"size\":%d,\"frame\":%u," + "\"mode\":\"0x%02X\",\"have_raw\":%u}," "\"i_stat\":\"0x%08X\"}", id, (unsigned long long)s.seq, s.has_disc, - s.index_reg, s.stat_reg, s.irq_enable, s.irq_flag, + s.index_reg, s.stat_reg, s.request_reg, s.irq_enable, s.irq_flag, s.mode_reg, s.param_count, s.response_read, s.response_count, s.sector_available, s.sector_read_pos, s.sector_size, @@ -3409,7 +3470,131 @@ static void handle_cdrom_state(int id, const char *json) s.filter_file, s.filter_channel, s.muted, s.seek_min, s.seek_sec, s.seek_sect, s.pending_cmd, s.pending_pending, s.pending_delay, - s.pending_phase, s.i_stat); + s.pending_phase, + s.last_sector_lba, s.last_sector_size, s.last_sector_frame, + s.last_sector_mode, s.last_sector_have_raw, + s.i_stat); +} + +static void handle_cdrom_sector_dump(int id, const char *json) +{ + int offset = json_get_int(json, "offset", 0); + int len = json_get_int(json, "len", 128); + if (offset < 0) offset = 0; + if (len < 1) len = 1; + if (len > 2340) len = 2340; + + uint8_t *bytes = (uint8_t *)malloc((size_t)len); + if (!bytes) { send_err(id, "oom"); return; } + + CDROMSectorDebugState s; + uint32_t got = cdrom_debug_copy_last_sector((uint32_t)offset, + (uint32_t)len, + bytes, &s); + + size_t bufsz = 512u + (size_t)got * 2u; + char *buf = (char *)malloc(bufsz); + if (!buf) { + free(bytes); + send_err(id, "oom"); + return; + } + + size_t pos = 0; + pos += snprintf(buf + pos, bufsz - pos, + "{\"id\":%d,\"ok\":true," + "\"current\":{\"available\":%d,\"read_pos\":%d,\"size\":%d}," + "\"last\":{\"lba\":%d,\"size\":%d,\"frame\":%u," + "\"mode\":\"0x%02X\",\"have_raw\":%u}," + "\"offset\":%d,\"len\":%u,\"hex\":\"", + id, + s.current_available, s.current_read_pos, s.current_size, + s.last_lba, s.last_size, s.last_frame, + s.last_mode, s.last_have_raw, + offset, got); + for (uint32_t i = 0; i < got && pos + 3 < bufsz; i++) { + pos += snprintf(buf + pos, bufsz - pos, "%02x", bytes[i]); + } + snprintf(buf + pos, bufsz - pos, "\"}"); + debug_server_send_line(buf); + free(buf); + free(bytes); +} + +static void append_hex_bytes(char *buf, size_t bufsz, size_t *pos, + const uint8_t *bytes, uint32_t len) +{ + for (uint32_t i = 0; i < len && *pos + 3 < bufsz; i++) { + *pos += snprintf(buf + *pos, bufsz - *pos, "%02x", bytes[i]); + } +} + +static void handle_cdrom_sector_history(int id, const char *json) +{ + int count = json_get_int(json, "count", 64); + if (count < 1) count = 1; + if (count > CDROM_SECTOR_HISTORY_CAP) count = CDROM_SECTOR_HISTORY_CAP; + + int filter_lba = -1; + char lba_str[32]; + if (json_get_str(json, "lba", lba_str, sizeof(lba_str))) { + filter_lba = (int)hex_to_u32(lba_str); + } + + const CDROMSectorHistoryEntry *entries = NULL; + uint64_t total = cdrom_debug_get_sector_history(&entries); + uint64_t oldest = (total > CDROM_SECTOR_HISTORY_CAP) + ? total - CDROM_SECTOR_HISTORY_CAP : 0; + + size_t bufsz = 256u + (size_t)count * 760u; + char *buf = (char *)malloc(bufsz); + if (!buf) { send_err(id, "oom"); return; } + + size_t pos = 0; + int emitted = 0; + pos += snprintf(buf + pos, bufsz - pos, + "{\"id\":%d,\"ok\":true,\"total\":%llu," + "\"oldest\":%llu,\"entries\":[", + id, (unsigned long long)total, + (unsigned long long)oldest); + + uint64_t seq = total; + while (seq > oldest && emitted < count && pos < bufsz - 760) { + seq--; + const CDROMSectorHistoryEntry *e = + &entries[seq % CDROM_SECTOR_HISTORY_CAP]; + if (e->seq != seq) continue; + if (filter_lba >= 0 && e->lba != filter_lba) continue; + + pos += snprintf(buf + pos, bufsz - pos, + "%s{\"seq\":%llu,\"lba\":%d,\"size\":%d," + "\"frame\":%u,\"mode\":\"0x%02X\"," + "\"have_raw\":%u,\"raw_mode\":\"0x%02X\"," + "\"xa_file\":%u,\"xa_channel\":%u," + "\"xa_submode\":\"0x%02X\",\"xa_coding\":\"0x%02X\"," + "\"data_delivered\":%u,\"xa_audio_delivered\":%u," + "\"skip_reason\":%u,\"bytes_len\":%u,\"hex\":\"", + emitted ? "," : "", + (unsigned long long)e->seq, e->lba, e->size, + e->frame, e->mode, e->have_raw, e->raw_mode, + e->xa_file, e->xa_channel, e->xa_submode, e->xa_coding, + e->data_delivered, e->xa_audio_delivered, + e->skip_reason, e->bytes_len); + append_hex_bytes(buf, bufsz, &pos, e->bytes, e->bytes_len); + pos += snprintf(buf + pos, bufsz - pos, "\"}"); + emitted++; + } + + pos += snprintf(buf + pos, bufsz - pos, "],\"emitted\":%d}", emitted); + debug_server_send_line(buf); + free(buf); +} + +static void handle_cdrom_sector_history_clear(int id, const char *json) +{ + (void)json; + cdrom_debug_clear_sector_history(); + send_ok(id); } static void handle_cdrom_trace_clear(int id, const char *json) @@ -3448,6 +3633,7 @@ static void handle_cdrom_trace_dump(int id, const char *json) "\"val\":\"0x%08X\",\"w\":%u,\"func\":\"0x%08X\"," "\"pc\":\"0x%08X\",\"frame\":%u,\"i_stat\":\"0x%08X\"," "\"index\":%u,\"stat\":\"0x%02X\"," + "\"request\":\"0x%02X\"," "\"irq_enable\":\"0x%02X\",\"irq_flag\":\"0x%02X\"," "\"mode\":\"0x%02X\"," "\"param\":%u,\"resp_read\":%u,\"resp_count\":%u," @@ -3459,7 +3645,7 @@ static void handle_cdrom_trace_dump(int id, const char *json) (unsigned long long)e->seq, cdrom_trace_kind_name(e->kind), e->addr, e->val, (unsigned)e->width, e->func, e->pc, e->frame, e->i_stat, - e->index_reg, e->stat_reg, e->irq_enable, e->irq_flag, + e->index_reg, e->stat_reg, e->request_reg, e->irq_enable, e->irq_flag, e->mode_reg, e->param_count, e->response_read, e->response_count, e->sector_available, e->sector_read_pos, e->sector_size, @@ -3482,6 +3668,34 @@ static const char *dma_trace_kind_name(uint32_t kind) } } +static void handle_dma_state(int id, const char *json) +{ + (void)json; + DMADebugState s; + dma_debug_get_state(&s); + + char buf[2048]; + size_t pos = 0; + pos += snprintf(buf + pos, sizeof(buf) - pos, + "{\"id\":%d,\"ok\":true,\"dpcr\":\"0x%08X\"," + "\"dicr\":\"0x%08X\",\"channels\":[", + id, s.dpcr, s.dicr); + for (int i = 0; i < 7 && pos < sizeof(buf) - 192; i++) { + pos += snprintf(buf + pos, sizeof(buf) - pos, + "%s{\"ch\":%d,\"madr\":\"0x%08X\"," + "\"bcr\":\"0x%08X\",\"chcr\":\"0x%08X\"," + "\"active\":%u,\"remaining_words\":%u," + "\"cycles_accum\":%u}", + i ? "," : "", + i, s.channels[i].madr, s.channels[i].bcr, + s.channels[i].chcr, s.channels[i].active, + s.channels[i].remaining_words, + s.channels[i].cycles_accum); + } + snprintf(buf + pos, sizeof(buf) - pos, "]}"); + debug_server_send_line(buf); +} + static void handle_dma_trace_clear(int id, const char *json) { (void)json; @@ -4725,6 +4939,26 @@ static void handle_clear_input(int id, const char *json) send_ok(id); } +static void handle_turbo(int id, const char *json) +{ + int enabled = json_get_int(json, "enabled", -1); + if (enabled < 0) { + enabled = json_get_int(json, "on", -1); + } + if (enabled < 0) { + send_err(id, "missing enabled"); + return; + } + s_turbo_enabled = enabled ? 1 : 0; + send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d}", id, s_turbo_enabled); +} + +static void handle_turbo_state(int id, const char *json) +{ + (void)json; + send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d}", id, s_turbo_enabled); +} + /* pause / continue / step / run_to_frame: REMOVED. * * Per CLAUDE.md global rule #2 ("Never time/attach for observability — @@ -5089,7 +5323,6 @@ static void handle_screenshot_file(int id, const char *json) send_err(id, "display disabled"); return; } - const uint16_t *vram = gpu_get_vram(); uint32_t w = di.width; if (w > 640) w = 640; uint32_t h = di.height; if (h > 512) h = 512; @@ -5125,13 +5358,9 @@ static void handle_screenshot_file(int id, const char *json) uint8_t *row = (uint8_t *)malloc(row_stride); for (uint32_t y = 0; y < h; y++) { memset(row, 0, row_stride); - uint32_t vy = (di.display_y + y) & 511; for (uint32_t x = 0; x < w; x++) { - uint32_t vx = (di.display_x + x) & 1023; - uint16_t c = vram[vy * 1024 + vx]; - uint8_t r = (c & 0x1F) << 3; - uint8_t g = ((c >> 5) & 0x1F) << 3; - uint8_t b = ((c >> 10) & 0x1F) << 3; + uint8_t r, g, b; + gpu_display_pixel_rgb(&di, x, y, &r, &g, &b); row[x * 3 + 0] = b; row[x * 3 + 1] = g; row[x * 3 + 2] = r; @@ -5148,7 +5377,7 @@ static void handle_screenshot_file(int id, const char *json) static void handle_screenshot(int id, const char *json) { (void)json; - /* Read display area from GPU and encode as hex RGB555 */ + /* Read display area from GPU and encode as display-format hex rows. */ GpuDisplayInfo di; gpu_get_display_info(&di); @@ -5156,26 +5385,30 @@ static void handle_screenshot(int id, const char *json) send_err(id, "display disabled"); return; } - const uint16_t *vram = gpu_get_vram(); uint32_t w = di.width; uint32_t h = di.height; if (w > 640) w = 640; if (h > 512) h = 512; /* Send metadata first */ - send_fmt("{\"id\":%d,\"ok\":true,\"width\":%u,\"height\":%u,\"format\":\"rgb555\"}", - id, w, h); + send_fmt("{\"id\":%d,\"ok\":true,\"width\":%u,\"height\":%u,\"format\":\"%s\"}", + id, w, h, di.depth24 ? "rgb888" : "rgb555"); /* Send rows as hex lines */ - char *hex = (char *)malloc(w * 4 + 32); + uint32_t chars_per_pixel = di.depth24 ? 6u : 4u; + char *hex = (char *)malloc((size_t)w * chars_per_pixel + 32u); if (!hex) return; for (uint32_t y = 0; y < h; y++) { - uint32_t vy = (di.display_y + y) & 511; for (uint32_t x = 0; x < w; x++) { - uint32_t vx = (di.display_x + x) & 1023; - uint16_t pixel = vram[vy * 1024 + vx]; - snprintf(hex + x * 4, 5, "%04x", pixel); + if (di.depth24) { + uint8_t r, g, b; + gpu_display_pixel_rgb(&di, x, y, &r, &g, &b); + snprintf(hex + x * 6, 7, "%02x%02x%02x", r, g, b); + } else { + uint16_t pixel = gpu_vram_peek((int)(di.display_x + x), (int)(di.display_y + y)); + snprintf(hex + x * 4, 5, "%04x", pixel); + } } send_fmt("{\"row\":%u,\"hex\":\"%s\"}", y, hex); } @@ -6626,6 +6859,9 @@ static void handle_fn_clear(int id, const char *json) { s_fn_unmatched_returns = 0; s_fn_stack_overflows = 0; s_fn_tail_calls = 0; + s_fn_direct_seen = 0; + s_fn_direct_no_cpu = 0; + s_fn_direct_filtered = 0; if (s_fn_entry) memset(s_fn_entry, 0, (size_t)FN_TRACE_CAP * sizeof(FnEntryEntry)); if (s_fn_exit) memset(s_fn_exit, 0, (size_t)FN_EXIT_TRACE_CAP * sizeof(FnExitEntry)); send_ok(id); @@ -6638,7 +6874,9 @@ static void handle_fn_stats(int id, const char *json) { "\"stack_top\":%d,\"unmatched_returns\":%llu," "\"stack_overflows\":%llu,\"tail_calls\":%llu," "\"entry_capacity\":%llu,\"exit_capacity\":%llu," - "\"filter_lo\":\"0x%08X\",\"filter_hi\":\"0x%08X\"}\n", + "\"filter_lo\":\"0x%08X\",\"filter_hi\":\"0x%08X\"," + "\"active\":%d,\"direct_seen\":%llu," + "\"direct_no_cpu\":%llu,\"direct_filtered\":%llu}\n", id, (unsigned long long)s_fn_entry_seq, (unsigned long long)s_fn_exit_seq, @@ -6648,7 +6886,11 @@ static void handle_fn_stats(int id, const char *json) { (unsigned long long)s_fn_tail_calls, (unsigned long long)FN_TRACE_CAP, (unsigned long long)FN_EXIT_TRACE_CAP, - s_fn_trace_filter_lo, s_fn_trace_filter_hi); + s_fn_trace_filter_lo, s_fn_trace_filter_hi, + s_fn_trace_active, + (unsigned long long)s_fn_direct_seen, + (unsigned long long)s_fn_direct_no_cpu, + (unsigned long long)s_fn_direct_filtered); } /* Helper: parse filter / range / count for fn_*_dump. @@ -7190,9 +7432,14 @@ static const CmdEntry s_commands[] = { { "mem_words", handle_mem_words }, { "vram_peek", handle_vram_peek }, { "irq_state", handle_irq_state }, + { "timers_state", handle_timers_state }, { "cdrom_state", handle_cdrom_state }, + { "cdrom_sector_dump", handle_cdrom_sector_dump }, + { "cdrom_sector_history", handle_cdrom_sector_history }, + { "cdrom_sector_history_clear", handle_cdrom_sector_history_clear }, { "cdrom_trace_dump", handle_cdrom_trace_dump }, { "cdrom_trace_clear", handle_cdrom_trace_clear }, + { "dma_state", handle_dma_state }, { "dma_trace_dump", handle_dma_trace_dump }, { "dma_trace_clear", handle_dma_trace_clear }, { "sio_state", handle_sio_state }, @@ -7290,6 +7537,8 @@ static const CmdEntry s_commands[] = { { "press", handle_press }, { "pad_status", handle_pad_status }, { "clear_input", handle_clear_input }, + { "turbo", handle_turbo }, + { "turbo_state", handle_turbo_state }, { "pause", handle_pause }, { "continue", handle_continue }, { "step", handle_step }, @@ -7440,6 +7689,8 @@ void debug_server_init(int port) s_wtrace_trans_head = 0; s_wtrace_trans_range_count = 0; + fntrace_arm_from_env("PSX_FNTRACE_ARM"); + /* Function entry/exit ring buffers (32 MB each, 64 MB total). */ if (!s_fn_entry) s_fn_entry = (FnEntryEntry *)calloc(FN_TRACE_CAP, sizeof(FnEntryEntry)); if (!s_fn_exit) s_fn_exit = (FnExitEntry *)calloc(FN_EXIT_TRACE_CAP, sizeof(FnExitEntry)); @@ -7448,6 +7699,15 @@ void debug_server_init(int port) s_fn_stack_top = 0; s_fn_unmatched_returns = 0; s_fn_stack_overflows = 0; + s_fn_tail_calls = 0; + s_fn_prev_ra = 0; + s_fn_direct_seen = 0; + s_fn_direct_no_cpu = 0; + s_fn_direct_filtered = 0; + s_fn_trace_active = 0; + s_fn_trace_filter_lo = 0u; + s_fn_trace_filter_hi = 0xFFFFFFFFu; + fn_trace_filter_from_env("PSX_FN_FILTER"); if (!s_call_focus) { s_call_focus = (CallFocusEntry *)calloc(CALL_FOCUS_CAP, @@ -7526,7 +7786,17 @@ void debug_server_init(int port) * writer to identify the BIOS function driving this sub-state. */ s_wtrace_ranges[14].lo = 0x00066BC0u; s_wtrace_ranges[14].hi = 0x00066BD0u; - s_wtrace_range_count = 15; + s_wtrace_ranges[15].lo = 0x00097420u; /* movie/frame handoff state */ + s_wtrace_ranges[15].hi = 0x00097430u; + s_wtrace_range_count = 16; + + s_wtrace_boot_ranges[0].lo = 0x00097420u; /* movie/frame handoff state */ + s_wtrace_boot_ranges[0].hi = 0x00097430u; + s_wtrace_boot_range_count = 1; + + s_wtrace_trans_ranges[0].lo = 0x00097420u; /* movie/frame handoff state */ + s_wtrace_trans_ranges[0].hi = 0x00097430u; + s_wtrace_trans_range_count = 1; #if DEFAULT_DEBUG_PORT == 4470 /* Tomba STR/FMVs: movie state, CD sector descriptor ring, and CD globals. @@ -7591,7 +7861,9 @@ void debug_server_init(int port) s_wtrace_ranges[29].hi = 0x000B3800u; s_wtrace_ranges[30].lo = 0x000EA000u; /* BIOS licensed-screen GPU packet buffer */ s_wtrace_ranges[30].hi = 0x000ED000u; - s_wtrace_range_count = 31; + s_wtrace_ranges[31].lo = 0x00097420u; /* movie/frame handoff state */ + s_wtrace_ranges[31].hi = 0x00097430u; + s_wtrace_range_count = 32; s_wtrace_boot_ranges[0].lo = 0x0009B3B0u; /* slot0 callbacks */ s_wtrace_boot_ranges[0].hi = 0x0009B3C4u; @@ -7615,7 +7887,9 @@ void debug_server_init(int port) s_wtrace_boot_ranges[9].hi = 0x000B3800u; s_wtrace_boot_ranges[10].lo = 0x000EA000u; /* BIOS licensed-screen GPU packet buffer */ s_wtrace_boot_ranges[10].hi = 0x000ED000u; - s_wtrace_boot_range_count = 11; + s_wtrace_boot_ranges[11].lo = 0x00097420u; /* movie/frame handoff state */ + s_wtrace_boot_ranges[11].hi = 0x00097430u; + s_wtrace_boot_range_count = 12; s_wtrace_trans_ranges[0].lo = 0x0000E1F4u; /* BIOS TCB save areas */ s_wtrace_trans_ranges[0].hi = 0x0000E400u; @@ -7635,7 +7909,9 @@ void debug_server_init(int port) s_wtrace_trans_ranges[7].hi = 0x0009C9E0u; s_wtrace_trans_ranges[8].lo = 0x0009C970u; /* title/menu state variables */ s_wtrace_trans_ranges[8].hi = 0x0009C9A0u; - s_wtrace_trans_range_count = 9; + s_wtrace_trans_ranges[9].lo = 0x00097420u; /* movie/frame handoff state */ + s_wtrace_trans_ranges[9].hi = 0x00097430u; + s_wtrace_trans_range_count = 10; #endif /* Tier 1: heap-allocate MMIO trace ring buffer (2 MB). */ @@ -7842,3 +8118,8 @@ int debug_server_get_input_override(void) } return s_input_override; } + +int debug_server_turbo_enabled(void) +{ + return s_turbo_enabled != 0; +} diff --git a/runtime/src/dma.c b/runtime/src/dma.c index e6dc3f96..3bb574ed 100644 --- a/runtime/src/dma.c +++ b/runtime/src/dma.c @@ -5,7 +5,10 @@ * - Ch2 (GPU): block mode and linked-list mode * - Ch6 (OTC): ordering table clear * - * Transfer is executed synchronously when CHCR start bit is written. + * Most channels execute synchronously when CHCR start bit is written. MDEC + * request-mode transfers are advanced from the guest cycle clock so games + * which synchronize video decode through DMA busy/request state see realistic + * backpressure. * Reference: nocash PSX specs, DuckStation src/core/dma.cpp */ @@ -40,6 +43,30 @@ typedef struct { static DMAChannel channels[7]; +typedef struct { + uint8_t active; + uint8_t debug_started; + uint32_t total_words; + uint32_t remaining_words; + uint32_t cycles_accum; +} DMAAsyncChannel; + +static DMAAsyncChannel mdec_async[2]; +static DMAAsyncChannel cdrom_async; + +#define DMA_MDEC_IN_CYCLES_PER_WORD 1u +#define DMA_MDEC_OUT_CYCLES_PER_WORD 14u +#define DMA_GPU_CYCLES_PER_WORD 1u +#define DMA_CDROM_CYCLES_PER_WORD 1u + +typedef struct { + uint8_t active; + uint32_t total_words; + uint32_t cycles_remaining; +} DMADelayedComplete; + +static DMADelayedComplete delayed_complete[7]; + /* ---- Global registers ---- */ static uint32_t dpcr; /* 0x1F8010F0: DMA control (enable bits) */ @@ -132,6 +159,167 @@ static void complete_transfer(int ch) { /* ---- Transfer execution ---- */ +static void cancel_async_transfer(int ch) { + if (ch >= 0 && ch < 2) { + mdec_async[ch].active = 0; + mdec_async[ch].debug_started = 0; + mdec_async[ch].total_words = 0; + mdec_async[ch].remaining_words = 0; + mdec_async[ch].cycles_accum = 0; + } + if (ch == 3) { + cdrom_async.active = 0; + cdrom_async.debug_started = 0; + cdrom_async.total_words = 0; + cdrom_async.remaining_words = 0; + cdrom_async.cycles_accum = 0; + } + if (ch >= 0 && ch < 7) { + delayed_complete[ch].active = 0; + delayed_complete[ch].total_words = 0; + delayed_complete[ch].cycles_remaining = 0; + } +} + +static void schedule_delayed_complete(int ch, uint32_t total_words, + uint32_t cycles_per_word) { + if (total_words == 0 || cycles_per_word == 0) { + complete_transfer(ch); + return; + } + + uint64_t cycles = (uint64_t)total_words * (uint64_t)cycles_per_word; + if (cycles > UINT32_MAX) cycles = UINT32_MAX; + + delayed_complete[ch].active = 1; + delayed_complete[ch].total_words = total_words; + delayed_complete[ch].cycles_remaining = (uint32_t)cycles; +} + +static void advance_delayed_complete(int ch, uint32_t cycles) { + DMADelayedComplete *d = &delayed_complete[ch]; + if (!d->active) return; + if (cycles < d->cycles_remaining) { + d->cycles_remaining -= cycles; + return; + } + + d->active = 0; + d->total_words = 0; + d->cycles_remaining = 0; + complete_transfer(ch); +} + +static void start_async_mdec_transfer(int ch) { + DMAAsyncChannel *a = &mdec_async[ch]; + if (a->active) return; + + a->active = 1; + a->debug_started = 0; + a->total_words = transfer_word_count(ch); + a->remaining_words = a->total_words; + a->cycles_accum = 0; + + if (ch == 0) { + mdec_debug_dma_in_start(channels[0].madr & 0x1FFFFCu, a->remaining_words); + } else { + mdec_debug_dma_out_start(channels[1].madr & 0x1FFFFCu, a->remaining_words); + } + a->debug_started = 1; +} + +static void finish_async_mdec_transfer(int ch, uint32_t final_addr, uint32_t total_words) { + if (ch == 0) { + mdec_debug_dma_in_end(final_addr, total_words); + } else { + mdec_debug_dma_out_end(final_addr, total_words); + } + cancel_async_transfer(ch); + complete_transfer(ch); +} + +static void start_async_cdrom_transfer(void) { + DMAAsyncChannel *a = &cdrom_async; + if (a->active) return; + + a->active = 1; + a->debug_started = 0; + a->total_words = transfer_word_count(3); + a->remaining_words = a->total_words; + a->cycles_accum = 0; + + if (a->total_words == 0) { + cancel_async_transfer(3); + complete_transfer(3); + } +} + +static void finish_async_cdrom_transfer(uint32_t final_addr) { + channels[3].madr = final_addr; + cancel_async_transfer(3); + complete_transfer(3); +} + +static void advance_mdec_channel(int ch, uint32_t cycles) { + DMAAsyncChannel *a = &mdec_async[ch]; + if (!a->active) return; + if (!((channels[ch].chcr >> 24) & 1u) || !channel_enabled(ch)) return; + + uint32_t chcr = channels[ch].chcr; + uint32_t direction = chcr & 1u; + uint32_t step = (chcr >> 1) & 1u; + int32_t addr_step = step ? -4 : 4; + uint32_t cycles_per_word = (ch == 0) ? DMA_MDEC_IN_CYCLES_PER_WORD : DMA_MDEC_OUT_CYCLES_PER_WORD; + + if ((ch == 0 && direction == 0) || (ch == 1 && direction != 0)) { + uint32_t words = a->total_words; + uint32_t addr = channels[ch].madr & 0x1FFFFCu; + finish_async_mdec_transfer(ch, addr, words); + return; + } + + if (ch == 0) { + if (!mdec_dma_write_ready()) return; + } else { + if (!mdec_dma_read_ready()) return; + } + + if (cycles > UINT32_MAX - a->cycles_accum) { + a->cycles_accum = UINT32_MAX; + } else { + a->cycles_accum += cycles; + } + + uint32_t words_budget = a->cycles_accum / cycles_per_word; + if (words_budget == 0) return; + + uint32_t addr = channels[ch].madr & 0x1FFFFCu; + uint32_t moved = 0; + 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; + psx_write_word(addr, mdec_dma_read_word()); + } + + addr = (addr + addr_step) & 0x1FFFFCu; + a->remaining_words--; + words_budget--; + moved++; + } + + if (moved == 0) return; + + a->cycles_accum -= moved * cycles_per_word; + channels[ch].madr = addr; + + if (a->remaining_words == 0) { + finish_async_mdec_transfer(ch, addr, a->total_words); + } +} + static void execute_ch0_mdec_in(void) { uint32_t chcr = channels[0].chcr; uint32_t direction = chcr & 1; /* 1=from RAM to MDEC */ @@ -174,11 +362,12 @@ static void execute_ch1_mdec_out(void) { complete_transfer(1); } -static void execute_ch2_gpu(void) { +static uint32_t execute_ch2_gpu(void) { uint32_t chcr = channels[2].chcr; uint32_t direction = chcr & 1; /* 0=to RAM, 1=from RAM (to device) */ uint32_t step = (chcr >> 1) & 1; /* 0=forward(+4), 1=backward(-4) */ uint32_t sync_mode = (chcr >> 9) & 3; /* 0=burst, 1=block, 2=linked-list */ + uint32_t actual_words = 0; if (direction == 0) { /* GPU → RAM (VRAM read): read pixel data via GPUREAD. @@ -194,9 +383,10 @@ static void execute_ch2_gpu(void) { psx_write_word(addr, pixel_data); addr = (addr + addr_step) & 0x1FFFFCu; } + channels[2].madr = addr; + actual_words = total_words; } - complete_transfer(2); - return; + return actual_words; } /* direction == 1: RAM → GPU */ @@ -215,6 +405,7 @@ static void execute_ch2_gpu(void) { addr = (addr + addr_step) & 0x1FFFFCu; } channels[2].madr = addr; + actual_words = total_words; } else if (sync_mode == 2) { /* Linked-list mode: follow ordering table in RAM. * Each node: bits 24-31 = number of words following header, @@ -234,6 +425,7 @@ static void execute_ch2_gpu(void) { uint32_t header = psx_read_word(addr); uint32_t num_words = (header >> 24) & 0xFF; uint32_t word_addr = (addr + 4) & 0x1FFFFCu; + actual_words += 1u; for (uint32_t i = 0; i < num_words; i++) { uint32_t word = psx_read_word(word_addr); @@ -241,12 +433,19 @@ static void execute_ch2_gpu(void) { gpu_write_gp0(word); word_addr = (word_addr + 4) & 0x1FFFFCu; } + actual_words += num_words; /* Next node */ uint32_t next = header & 0xFFFFFFu; - if (next == 0xFFFFFFu) break; /* end of list */ + if (next == 0xFFFFFFu) { + channels[2].madr = 0x00FFFFFFu; + break; /* end of list */ + } addr = next & 0x1FFFFCu; } + if (safety > MAX_NODES) { + channels[2].madr = addr; + } } else { /* Burst mode (sync_mode == 0) */ uint32_t word_count = channels[2].bcr & 0xFFFF; @@ -261,45 +460,22 @@ static void execute_ch2_gpu(void) { addr = (addr + addr_step) & 0x1FFFFCu; } channels[2].madr = addr; + actual_words = word_count; } - complete_transfer(2); + return actual_words; } static void execute_ch3_cdrom(void) { uint32_t chcr = channels[3].chcr; uint32_t direction = chcr & 1; /* 0=to RAM, 1=from RAM */ - uint32_t step = (chcr >> 1) & 1; /* 0=forward(+4), 1=backward(-4) */ - uint32_t sync_mode = (chcr >> 9) & 3; if (direction != 0) { channels[3].chcr &= ~((1u << 24) | (1u << 28)); return; } - uint32_t block_size = channels[3].bcr & 0xFFFF; - uint32_t block_count = (channels[3].bcr >> 16) & 0xFFFF; - uint32_t total_words; - if (sync_mode == 1) { - if (block_size == 0) block_size = 0x10000; - if (block_count == 0) block_count = 1; - total_words = block_size * block_count; - } else { - total_words = block_size; - if (total_words == 0) total_words = 0x10000; - } - - uint32_t addr = channels[3].madr & 0x1FFFFCu; - int32_t addr_step = step ? -4 : 4; - for (uint32_t i = 0; i < total_words; i++) { - uint32_t word = cdrom_dma_read(); - psx_write_word(addr, word); - dirty_ram_mark_executable_range(addr, 4); - addr = (addr + addr_step) & 0x1FFFFCu; - } - - channels[3].madr = addr; - complete_transfer(3); + start_async_cdrom_transfer(); } static void execute_ch4_spu(void) { @@ -358,17 +534,19 @@ static void try_execute(int ch) { if (!((chcr >> 24) & 1)) return; if (!channel_enabled(ch)) return; + channels[ch].chcr &= ~(1u << 28); trace_dma('S', ch, transfer_word_count(ch), dicr, i_stat); switch (ch) { case 0: - execute_ch0_mdec_in(); + start_async_mdec_transfer(0); break; case 1: - execute_ch1_mdec_out(); + start_async_mdec_transfer(1); break; case 2: - execute_ch2_gpu(); + schedule_delayed_complete(2, execute_ch2_gpu(), + DMA_GPU_CYCLES_PER_WORD); break; case 3: execute_ch3_cdrom(); @@ -392,8 +570,57 @@ static void try_execute(int ch) { uint32_t dma_get_dicr(void) { update_master_irq(); return dicr; } uint32_t dma_get_dpcr(void) { return dpcr; } +void dma_advance(uint32_t cycles) { + if (cycles == 0) return; + advance_mdec_channel(0, cycles); + advance_mdec_channel(1, cycles); + DMAAsyncChannel *a = &cdrom_async; + if (a->active && ((channels[3].chcr >> 24) & 1u) && channel_enabled(3)) { + uint32_t chcr = channels[3].chcr; + uint32_t direction = chcr & 1u; + uint32_t step = (chcr >> 1) & 1u; + int32_t addr_step = step ? -4 : 4; + + if (direction != 0) { + cancel_async_transfer(3); + channels[3].chcr &= ~((1u << 24) | (1u << 28)); + } else if (cdrom_dma_ready()) { + if (cycles > UINT32_MAX - a->cycles_accum) { + a->cycles_accum = UINT32_MAX; + } else { + a->cycles_accum += cycles; + } + + uint32_t words_budget = a->cycles_accum / DMA_CDROM_CYCLES_PER_WORD; + uint32_t addr = channels[3].madr & 0x1FFFFCu; + uint32_t moved = 0; + while (a->remaining_words > 0 && words_budget > 0 && cdrom_dma_ready()) { + uint32_t word = cdrom_dma_read(); + psx_write_word(addr, word); + dirty_ram_mark_executable_range(addr, 4); + addr = (addr + addr_step) & 0x1FFFFCu; + a->remaining_words--; + words_budget--; + moved++; + } + + if (moved > 0) { + a->cycles_accum -= moved * DMA_CDROM_CYCLES_PER_WORD; + channels[3].madr = addr; + if (a->remaining_words == 0) { + finish_async_cdrom_transfer(addr); + } + } + } + } + advance_delayed_complete(2, cycles); +} + void dma_init(void) { memset(channels, 0, sizeof(channels)); + memset(mdec_async, 0, sizeof(mdec_async)); + memset(&cdrom_async, 0, sizeof(cdrom_async)); + memset(delayed_complete, 0, sizeof(delayed_complete)); dpcr = 0x07654321u; /* default: priorities set, no channels enabled */ dicr = 0; dma_debug_clear_trace(); @@ -419,6 +646,7 @@ uint32_t dma_read(uint32_t addr) { case 0x00: return channels[ch].madr; case 0x04: return channels[ch].bcr; case 0x08: return channels[ch].chcr; + case 0x0C: return 0; default: goto bad; } } @@ -469,11 +697,16 @@ void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask) { return; case 0x08: channels[ch].chcr = (channels[ch].chcr & ~mask) | (val & mask); + if ((mask & (1u << 24)) && !((channels[ch].chcr >> 24) & 1u)) { + cancel_async_transfer(ch); + } /* Writing CHCR's start bit set triggers transfer. */ if ((mask & (1u << 24)) && ((channels[ch].chcr >> 24) & 1)) { try_execute(ch); } return; + case 0x0C: + return; default: goto bad; } @@ -498,3 +731,26 @@ void dma_debug_clear_trace(void) { memset(dma_trace, 0, sizeof(dma_trace)); dma_trace_seq = 0; } + +void dma_debug_get_state(DMADebugState* out) { + if (!out) return; + out->dpcr = dpcr; + out->dicr = dma_get_dicr(); + for (int i = 0; i < 7; i++) { + out->channels[i].madr = channels[i].madr; + out->channels[i].bcr = channels[i].bcr; + out->channels[i].chcr = channels[i].chcr; + out->channels[i].active = + ((i < 2) ? mdec_async[i].active : 0) || + ((i == 3) ? cdrom_async.active : 0) || + delayed_complete[i].active; + out->channels[i].remaining_words = + (i < 2 && mdec_async[i].active) ? mdec_async[i].remaining_words : + (i == 3 && cdrom_async.active) ? cdrom_async.remaining_words : + delayed_complete[i].total_words; + out->channels[i].cycles_accum = + (i < 2 && mdec_async[i].active) ? mdec_async[i].cycles_accum : + (i == 3 && cdrom_async.active) ? cdrom_async.cycles_accum : + delayed_complete[i].cycles_remaining; + } +} diff --git a/runtime/src/fntrace.c b/runtime/src/fntrace.c index 15a7b730..dce7238b 100644 --- a/runtime/src/fntrace.c +++ b/runtime/src/fntrace.c @@ -2,6 +2,7 @@ #include "fntrace.h" #include +#include FntraceEntry g_fntrace_ring[FNTRACE_RING_CAP]; uint64_t g_fntrace_seq = 0; @@ -87,6 +88,43 @@ uint32_t fntrace_arm_get(uint32_t i) { return (i < s_arm_count) ? s_arm_targets[i] : 0; } +void fntrace_arm_from_env(const char *env_name) { + const char *spec = getenv(env_name); + if (!spec || !*spec) return; + + const char *p = spec; + while (*p) { + while (*p == ',' || *p == ';' || *p == ' ' || *p == '\t' || + *p == '\r' || *p == '\n') { + p++; + } + if (!*p) break; + + if ((p[0] == 'a' || p[0] == 'A') && + (p[1] == 'l' || p[1] == 'L') && + (p[2] == 'l' || p[2] == 'L') && + (p[3] == '\0' || p[3] == ',' || p[3] == ';' || + p[3] == ' ' || p[3] == '\t' || p[3] == '\r' || p[3] == '\n')) { + fntrace_arm(0xFFFFFFFFu); + p += 3; + continue; + } + + char *end = NULL; + unsigned long value = strtoul(p, &end, 0); + if (end == p) { + while (*p && *p != ',' && *p != ';' && *p != ' ' && *p != '\t' && + *p != '\r' && *p != '\n') { + p++; + } + continue; + } + + fntrace_arm((uint32_t)value); + p = end; + } +} + void fntrace_clear(void) { g_fntrace_seq = 0; /* Storage left in place; consumer dumps relative to seq. */ diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index c2748f0a..7f92bb02 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -351,9 +351,43 @@ const uint16_t* gpu_get_vram(void) { return vram; } +static uint8_t gpu_vram_byte(uint32_t byte_x, uint32_t y) { + uint16_t hw = vram[((y & 511u) * 1024u) + ((byte_x >> 1) & 1023u)]; + return (byte_x & 1u) ? (uint8_t)(hw >> 8) : (uint8_t)hw; +} + +static void gpu_rgb555_to_rgb888(uint16_t c, uint8_t* r, uint8_t* g, uint8_t* b) { + *r = (uint8_t)((c & 0x1Fu) << 3); + *g = (uint8_t)(((c >> 5) & 0x1Fu) << 3); + *b = (uint8_t)(((c >> 10) & 0x1Fu) << 3); +} + +void gpu_display_pixel_rgb(const GpuDisplayInfo* di, uint32_t x, uint32_t y, + uint8_t* r, uint8_t* g, uint8_t* b) { + if (di->depth24) { + uint32_t byte_x = ((di->display_x & 1023u) * 2u) + x * 3u; + uint32_t vy = (di->display_y + y) & 511u; + *r = gpu_vram_byte(byte_x + 0u, vy); + *g = gpu_vram_byte(byte_x + 1u, vy); + *b = gpu_vram_byte(byte_x + 2u, vy); + return; + } + + uint32_t vx = (di->display_x + x) & 1023u; + uint32_t vy = (di->display_y + y) & 511u; + gpu_rgb555_to_rgb888(vram[vy * 1024u + vx], r, g, b); +} + +uint32_t gpu_display_pixel_argb(const GpuDisplayInfo* di, uint32_t x, uint32_t y) { + uint8_t r, g, b; + gpu_display_pixel_rgb(di, x, y, &r, &g, &b); + return 0xFF000000u | ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b; +} + void gpu_get_display_info(GpuDisplayInfo* out) { out->display_x = display_area_x; out->display_y = display_area_y; + out->depth24 = (int)(display_depth & 1u); out->disabled = (int)display_disabled; /* Derive pixel width from horizontal display range and resolution mode. diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index bd4fafd5..38620603 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -496,14 +496,6 @@ static void sdl_audio_pump(void) { (uint32_t)frames * bytes_per_frame); } -/* Convert PS1 16-bit color (1-5-5-5) to ARGB8888 */ -static inline uint32_t psx16_to_argb(uint16_t c) { - uint32_t r = (c & 0x1F) << 3; - uint32_t g = ((c >> 5) & 0x1F) << 3; - uint32_t b = ((c >> 10) & 0x1F) << 3; - return 0xFF000000u | (r << 16) | (g << 8) | b; -} - /* PS1 digital pad button bits (active-low: 0=pressed, 1=released). * Bit 0 = SELECT, Bit 3 = START, Bit 4 = UP, Bit 5 = RIGHT, * Bit 6 = DOWN, Bit 7 = LEFT, Bit 8 = L2, Bit 9 = R2, @@ -948,6 +940,13 @@ static void sdl_vblank_present(void) { sdl_audio_pump(); #endif + /* TCP turbo is for automated validation and trace capture. It keeps the + * simulation advancing and the debug server polling, but removes frontend + * presentation and wall-clock pacing. */ +#ifndef PSX_NO_DEBUG_TOOLS + if (debug_server_turbo_enabled()) return; +#endif + /* Turbo mode: while TAB is held, skip both VRAM->ARGB conversion and * SDL_RenderPresent. The recompiled BIOS still advances simulated * cycles every vblank, so the BIOS proceeds at whatever rate the host @@ -1004,23 +1003,25 @@ static void sdl_vblank_present(void) { /* ---- Display from our VRAM ---- */ uint32_t w = 0, h = 0; { + static bool disabled_frame_presented = false; GpuDisplayInfo di; gpu_get_display_info(&di); if (di.disabled || di.width == 0 || di.height == 0) { #ifndef PSX_SDL_NO_RENDER - SDL_SetRenderDrawColor(sdl_renderer, 0, 0, 0, 255); - SDL_RenderClear(sdl_renderer); - SDL_RenderPresent(sdl_renderer); + if (!disabled_frame_presented) { + disabled_frame_presented = true; + SDL_SetRenderDrawColor(sdl_renderer, 0, 0, 0, 255); + SDL_RenderClear(sdl_renderer); + SDL_RenderPresent(sdl_renderer); + } #endif return; } - const uint16_t* vram = gpu_get_vram(); + disabled_frame_presented = false; w = di.width; h = di.height; for (uint32_t y = 0; y < h; y++) { - uint32_t vy = (di.display_y + y) & 511; for (uint32_t x = 0; x < w; x++) { - uint32_t vx = (di.display_x + x) & 1023; - sdl_pixel_buf[y * w + x] = psx16_to_argb(vram[vy * 1024 + vx]); + sdl_pixel_buf[y * w + x] = gpu_display_pixel_argb(&di, x, y); } } } diff --git a/runtime/src/mdec.c b/runtime/src/mdec.c index b698c05a..34134543 100644 --- a/runtime/src/mdec.c +++ b/runtime/src/mdec.c @@ -161,10 +161,35 @@ static void finish_command(void) { mdec.expected_halfwords = 0; mdec.input_count = 0; mdec.busy = 0; - mdec.input_full = 1; + mdec.input_full = 0; trace_event(MDEC_EVT_CMD_DONE, mdec.command); } +static void soft_reset(void) { + uint16_t *input = mdec.input; + uint32_t input_cap = mdec.input_cap; + uint8_t *output = mdec.output; + uint32_t output_cap = mdec.output_cap; + uint8_t y_quant[64]; + uint8_t uv_quant[64]; + int16_t scale[64]; + + memcpy(y_quant, mdec.y_quant, sizeof(y_quant)); + memcpy(uv_quant, mdec.uv_quant, sizeof(uv_quant)); + memcpy(scale, mdec.scale, sizeof(scale)); + + memset(&mdec, 0, sizeof(mdec)); + mdec.input = input; + mdec.input_cap = input_cap; + mdec.output = output; + mdec.output_cap = output_cap; + memcpy(mdec.y_quant, y_quant, sizeof(mdec.y_quant)); + memcpy(mdec.uv_quant, uv_quant, sizeof(mdec.uv_quant)); + memcpy(mdec.scale, scale, sizeof(mdec.scale)); + mdec.output_depth = 3; + mdec.current_block = 4; +} + static void idct_block(int16_t *block) { int16_t tmp[64]; int16_t *src = block; @@ -414,10 +439,11 @@ uint32_t mdec_read(uint32_t addr) { status |= ((uint32_t)mdec.output_bit15 & 1u) << 23; status |= ((uint32_t)mdec.output_signed & 1u) << 24; status |= ((uint32_t)mdec.output_depth & 3u) << 25; - if (mdec.enable_dma_out && mdec.output_pos < mdec.output_size) status |= 1u << 27; - if (mdec.enable_dma_in && mdec.busy && mdec.input_count < mdec.expected_halfwords) status |= 1u << 28; + int write_ready = mdec_dma_write_ready(); + if (!write_ready) status |= 1u << 30; + if (mdec.enable_dma_out && mdec_dma_read_ready()) status |= 1u << 27; + if (mdec.enable_dma_in && write_ready) status |= 1u << 28; if (mdec.busy) status |= 1u << 29; - if (mdec.input_full) status |= 1u << 30; if (mdec.output_pos >= mdec.output_size) status |= 1u << 31; mdec.last_status = status; return status; @@ -431,15 +457,7 @@ void mdec_write(uint32_t addr, uint32_t value) { } if (value & 0x80000000u) { - free(mdec.input); - free(mdec.output); - memset(&mdec, 0, sizeof(mdec)); - mdec.output_depth = 3; - mdec.current_block = 4; - for (int i = 0; i < 64; i++) { - mdec.y_quant[i] = 1; - mdec.uv_quant[i] = 1; - } + soft_reset(); trace_event(MDEC_EVT_RESET, value); } mdec.enable_dma_in = (uint8_t)((value >> 30) & 1u); @@ -474,6 +492,7 @@ uint32_t mdec_dma_read_word(void) { } int mdec_dma_write_ready(void) { + if (mdec.output_pos < mdec.output_size) return 0; return !mdec.busy || mdec.input_count < mdec.expected_halfwords; } diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c index da6d2238..ead2d69e 100644 --- a/runtime/src/psx_cycles.c +++ b/runtime/src/psx_cycles.c @@ -2,6 +2,7 @@ #include "psx_cycles.h" #include "cdrom.h" +#include "dma.h" #include "interrupts.h" #include "sio.h" #include "starvation_ring.h" @@ -20,6 +21,7 @@ void psx_advance_cycles(uint32_t cycles) { psx_cycle_count += (uint64_t)cycles; sio_advance(cycles); cdrom_advance(cycles); + dma_advance(cycles); timers_advance(cycles); interrupts_advance_cycles(cycles); s_watchdog_throttle += cycles; diff --git a/runtime/src/traps.c b/runtime/src/traps.c index 4b56f8a9..f6072d35 100644 --- a/runtime/src/traps.c +++ b/runtime/src/traps.c @@ -208,9 +208,9 @@ static void psx_thread_fiber_entry(void* param) debug_server_log_thread_event(11, cpu, psx_current_tcb_ptr(cpu), slot->tcb, target_pc); slot->closed = 1; - if (psx_tcb_state(cpu, slot->tcb) == 0x4000u) { - cpu->write_word(slot->tcb, 0x1000u); - } + /* Guest BIOS code owns the TCB state. A host fiber can finish because a + * thread returned through the BIOS scheduler path, but changing the BIOS + * TCB here races with games that keep that thread runnable. */ if (slot->return_fiber) { if (psx_is_valid_tcb(cpu, slot->return_tcb)) {