mdec: keep status bit 29 set until decode output is down to one OutFIFO load

Bit 29 is Beetle's InCommand. 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. mdec.c decodes a whole
command at once and cleared the bit at execute time.

libpress DecDCTin spins on bit 29 before issuing the next frame. Reporting
ready early let Breath of Fire IV's FMV player issue the next DecDCTin and
DecDCTout while a column DMA was mid-transfer; DecDCTout's CHCR=0 restart
dropped the 205 words already moved, so every later column read started 820
bytes late (vertical stripes at macroblock edges, a one-macroblock vertical
wrap and shifted colour channels in the intro FMV).

Derive the bit from the unread output instead. No new state, so snapshots
are unchanged. Adds mdec_busy_drain_test (fails on the old behaviour).
This commit is contained in:
maliquem
2026-10-19 10:38:08 -03:00
parent bce31affb4
commit 613112996f
3 changed files with 93 additions and 1 deletions
+8
View File
@@ -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
+15 -1
View File
@@ -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;
+70
View File
@@ -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 <stdint.h>
#include <stdio.h>
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;
}