diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 819ec525..6bd0f660 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -399,6 +399,14 @@ if(BUILD_TESTING) target_include_directories(guest_tty_test PRIVATE ../runtime/include) add_test(NAME guest_tty_test COMMAND guest_tty_test) + # MDEC status bit 29 stays set until a decode's output is down to the last + # 32-word OutFIFO load (Beetle InCommand); libpress DecDCTin spins on it. + add_executable(mdec_busy_drain_test + ../runtime/tests/test_mdec_busy_drain.c + ../runtime/src/mdec.c) + target_include_directories(mdec_busy_drain_test PRIVATE ../runtime/include) + add_test(NAME mdec_busy_drain_test COMMAND mdec_busy_drain_test) + add_executable(debug_trace_ranges_test ../runtime/tests/test_debug_trace_ranges.c ../runtime/src/debug_trace_ranges.c diff --git a/runtime/src/mdec.c b/runtime/src/mdec.c index 72a82a5a..60e9cfed 100644 --- a/runtime/src/mdec.c +++ b/runtime/src/mdec.c @@ -27,6 +27,9 @@ extern uint64_t s_frame_count; #define MDEC_NEVER_DECODED ((uint64_t)0 - 1000u) static uint64_t mdec_last_color_decode_cycle = MDEC_NEVER_DECODED; +/* Beetle OutFIFO depth: 0x20 words (MDEC_DMACanRead's block size). */ +#define MDEC_OUT_FIFO_BYTES (0x20u * 4u) + enum { MDEC_CMD_NOP = 0, MDEC_CMD_DECODE = 1, @@ -879,7 +882,18 @@ uint32_t mdec_read(uint32_t addr) { 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; + /* Bit 29 is Beetle's InCommand (mdec.c MDEC_Read). The hardware decodes + * progressively and stalls while its 32-word OutFIFO is full, so a decode + * command stays in progress until all but the last FIFO-load of its output + * has been read. This model decodes a whole command at once, so derive that + * from the unread output instead of clearing the bit at execute time: + * libpress DecDCTin spins on this bit before issuing the next frame, and + * reporting ready early let Breath of Fire IV's player issue the next + * DecDCTin/DecDCTout while a column DMA was mid-transfer. DecDCTout's + * CHCR=0 restart then dropped the words already moved, misaligning every + * later column (vertical stripes + one-macroblock wrap in the FMV). */ + if (mdec.busy || mdec.output_size - mdec.output_pos > MDEC_OUT_FIFO_BYTES) + status |= 1u << 29; if (mdec.output_pos >= mdec.output_size) status |= 1u << 31; mdec.last_status = status; return status; diff --git a/runtime/tests/test_mdec_busy_drain.c b/runtime/tests/test_mdec_busy_drain.c new file mode 100644 index 00000000..d23665ab --- /dev/null +++ b/runtime/tests/test_mdec_busy_drain.c @@ -0,0 +1,70 @@ +/* + * MDEC status bit 29 (Command Busy) across output drain. + * + * Beetle's MDEC_Read reports InCommand as bit 29, and a decode command stays + * InCommand until its last macroblock fits in the 32-word OutFIFO: the hardware + * decodes progressively and stalls while OutFIFO is full. mdec.c decodes a whole + * command at once, so bit 29 must be derived from the unread output. + * + * Regression: reporting ready right after the instant decode let libpress + * DecDCTin (which spins on bit 29) issue the next frame while a DecDCTout column + * DMA was mid-transfer. DecDCTout's CHCR=0 restart then dropped the words already + * moved, and every later column was misaligned (Breath of Fire IV intro FMV: + * vertical stripes, one-macroblock wrap, wrong colours). + * + * Build/run: ctest -R mdec_busy_drain_test + */ +#include "mdec.h" + +#include +#include + +uint64_t s_frame_count = 0; +uint64_t psx_cycle_count = 0; +int debug_server_fmv_quiet(void) { return 0; } + +#define MDEC_DATA 0x1F801820u +#define MDEC_STATUS 0x1F801824u +#define BUSY (1u << 29) +#define OUT_EMPTY (1u << 31) + +static int failures = 0; +#define CHECK(cond, ...) do { if (!(cond)) { \ + fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); fputc('\n', stderr); failures++; } } while (0) + +int main(void) { + mdec_init(); + mdec_write(MDEC_STATUS, 0x80000000u); /* reset */ + mdec_write(MDEC_STATUS, 0x60000000u); /* enable DMA in + out */ + + /* Decode, 24bpp (depth 2), 6 input words: one colour macroblock made of + * six DC-only blocks (Cr, Cb, Y0..Y3), each a 0x0000 DC word + 0xFE00 end. */ + mdec_write(MDEC_DATA, 0x30000006u); + CHECK(mdec_read(MDEC_STATUS) & BUSY, "busy while receiving parameters"); + for (int i = 0; i < 6; i++) + mdec_dma_write_word(0xFE000000u); + + /* 16x16 pixels * 3 bytes = 768 bytes = 192 words of output. */ + const uint32_t total_words = 192u, fifo_words = 0x20u; + for (uint32_t read = 0; read < total_words; read++) { + uint32_t remaining = total_words - read; + uint32_t st = mdec_read(MDEC_STATUS); + if (remaining > fifo_words) + CHECK(st & BUSY, "busy with %u words unread", remaining); + else + CHECK(!(st & BUSY), "ready with %u words unread (last FIFO load)", remaining); + CHECK(!(st & OUT_EMPTY), "output empty with %u words unread", remaining); + (void)mdec_dma_read_word(); + } + uint32_t st = mdec_read(MDEC_STATUS); + CHECK(!(st & BUSY), "ready after drain"); + CHECK(st & OUT_EMPTY, "output empty after drain"); + + if (failures) { + fprintf(stderr, "mdec_busy_drain_test: %d failure(s)\n", failures); + return 1; + } + printf("mdec_busy_drain_test: ok\n"); + return 0; +}