diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 56a208d9..157fd8d2 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -38,6 +38,9 @@ if(BUILD_TESTING) add_executable(ws_scene_hold_test tests/test_ws_scene_hold.c) target_include_directories(ws_scene_hold_test PRIVATE include) add_test(NAME ws_scene_hold_test COMMAND ws_scene_hold_test) + add_executable(mdec_snapshot_activity_test tests/test_mdec_snapshot_activity.c) + target_include_directories(mdec_snapshot_activity_test PRIVATE include src) + add_test(NAME mdec_snapshot_activity_test COMMAND mdec_snapshot_activity_test) add_executable(mod_texture_banks_test tests/test_mod_texture_banks.c src/mod_texture_banks.c) target_include_directories(mod_texture_banks_test PRIVATE include) add_test(NAME mod_texture_banks_test COMMAND mod_texture_banks_test) diff --git a/runtime/src/mdec.c b/runtime/src/mdec.c index 0c4653b6..72a82a5a 100644 --- a/runtime/src/mdec.c +++ b/runtime/src/mdec.c @@ -20,13 +20,12 @@ extern uint64_t s_frame_count; -/* FMV-activity detector: host display-frame stamp of the newest colour - * (15/24-bit) MDEC decode. Used only by mdec_recently_active() for local - * frontend / rewind policy — never folded into netplay digests. */ -static uint64_t mdec_last_color_decode_frame = (uint64_t)0 - 1000u; -/* Guest-cycle stamp of the same event — snap age is relative to this so - * peers with identical FIFO/tables but different present rates hash equal. */ -static uint64_t mdec_last_color_decode_cycle = (uint64_t)0 - 1000u; +/* FMV-activity detector: guest-cycle stamp of the newest colour (15/24-bit) + * MDEC decode, read by mdec_recently_active() / mdec_color_age_cycles(). The + * snapshot carries its age, so peers with identical FIFO/tables but different + * present rates hash equal. MDEC_NEVER_DECODED = no colour decode yet. */ +#define MDEC_NEVER_DECODED ((uint64_t)0 - 1000u) +static uint64_t mdec_last_color_decode_cycle = MDEC_NEVER_DECODED; enum { MDEC_CMD_NOP = 0, @@ -691,7 +690,6 @@ static void execute_decode(void) { mdec.decode_input_pos = pos; /* FMV detector: stamp colour (15/24-bit) decodes only — streamed video. * The 4/8-bit luma path above is texture decompression, not video. */ - mdec_last_color_decode_frame = s_frame_count; mdec_last_color_decode_cycle = psx_cycle_count; trace_event(MDEC_EVT_DECODE_DONE, mdec.output_size); } @@ -784,7 +782,7 @@ int mdec_recently_active(uint32_t within_frames) { } uint64_t mdec_color_age_cycles(void) { - if (mdec_last_color_decode_cycle == (uint64_t)0 - 1000u) + if (mdec_last_color_decode_cycle == MDEC_NEVER_DECODED) return (uint64_t)0 - 1u; if (psx_cycle_count < mdec_last_color_decode_cycle) return (uint64_t)0 - 1u; @@ -796,10 +794,9 @@ void mdec_init(void) { memset(mdec_trace, 0, sizeof(mdec_trace)); mdec_trace_seq = 0; mdec_trace_head = 0; - /* Rematch resets s_frame_count; a stale stamp makes mdec_recently_active - * wrap and lie for the whole next session. */ - mdec_last_color_decode_frame = (uint64_t)0 - 1000u; - mdec_last_color_decode_cycle = (uint64_t)0 - 1000u; + /* A rematch resets the guest clock; a stale stamp would make + * mdec_recently_active lie for the whole next session. */ + mdec_last_color_decode_cycle = MDEC_NEVER_DECODED; for (int i = 0; i < 64; i++) { mdec.y_quant[i] = 1; mdec.uv_quant[i] = 1; @@ -1057,7 +1054,14 @@ void mdec_debug_dma_out_end(uint32_t addr, uint32_t words) { } /* ---- boot_state snapshot (variable-length input/output FIFOs) ------------ */ -#define MDEC_SNAP_VER 1u +/* v2: the colour-decode age is MDEC_SNAP_AGE_NEVER when nothing was decoded. + * v1 wrote age 1000 for that case, which a load turned into "decoded 1000 + * cycles ago" -- a false FMV for the next ~8 frames after every snapshot load + * made before the first video (issue #475). v1 payloads still load. */ +#define MDEC_SNAP_VER 2u +#define MDEC_SNAP_VER_V1 1u +#define MDEC_SNAP_AGE_NEVER UINT64_MAX +#define MDEC_SNAP_V1_AGE_NEVER 1000ull #define MDEC_SNAP_INPUT_MAX (4u * 1024u * 1024u) /* halfwords */ #define MDEC_SNAP_OUTPUT_MAX (8u * 1024u * 1024u) /* bytes */ @@ -1116,10 +1120,11 @@ void mdec_snapshot_write(uint8_t *p) { (void)pst_w_u32(&w, mdec.input_count); (void)pst_w_u32(&w, mdec.output_size); /* Guest-cycle age (not host s_frame_count) — netplay aux digests this blob. */ - if (psx_cycle_count >= mdec_last_color_decode_cycle) - age = psx_cycle_count - mdec_last_color_decode_cycle; + if (mdec_last_color_decode_cycle == MDEC_NEVER_DECODED || + psx_cycle_count < mdec_last_color_decode_cycle) + age = MDEC_SNAP_AGE_NEVER; else - age = 1000ull; + age = psx_cycle_count - mdec_last_color_decode_cycle; (void)pst_w_u64(&w, age); for (uint32_t i = 0; i < mdec.input_count; i++) (void)pst_w_u16(&w, mdec.input ? mdec.input[i] : 0u); @@ -1144,7 +1149,8 @@ static uint32_t mdec_wire_u32(const uint8_t *p, uint32_t off) { int mdec_snapshot_validate(const uint8_t *p, uint32_t len) { uint32_t input_count, output_size; if (!p || len < mdec_snap_fixed_bytes()) return 0; - if (mdec_wire_u32(p, 0u) != MDEC_SNAP_VER) return 0; + if (mdec_wire_u32(p, 0u) != MDEC_SNAP_VER && + mdec_wire_u32(p, 0u) != MDEC_SNAP_VER_V1) return 0; input_count = mdec_wire_u32(p, MDEC_SNAP_OFF_COUNTS); output_size = mdec_wire_u32(p, MDEC_SNAP_OFF_COUNTS + 4u); if (input_count > MDEC_SNAP_INPUT_MAX || output_size > MDEC_SNAP_OUTPUT_MAX) @@ -1157,11 +1163,12 @@ int mdec_snapshot_validate(const uint8_t *p, uint32_t len) { int mdec_snapshot_read(const uint8_t *p, uint32_t len) { PstR r; uint32_t ver = 0, input_count = 0, output_size = 0, reserved; - uint64_t age = 1000ull; + uint64_t age = MDEC_SNAP_AGE_NEVER; int16_t s16; if (!mdec_snapshot_validate(p, len)) return 0; pst_r_init(&r, p, len); - if (!pst_r_u32(&r, &ver) || ver != MDEC_SNAP_VER) return 0; + if (!pst_r_u32(&r, &ver) || + (ver != MDEC_SNAP_VER && ver != MDEC_SNAP_VER_V1)) return 0; if (!pst_r_u32(&r, &mdec.command) || !pst_r_u32(&r, &mdec.expected_halfwords) || !pst_r_u32(&r, &mdec.last_status) || @@ -1213,24 +1220,14 @@ int mdec_snapshot_read(const uint8_t *p, uint32_t len) { } if (output_size && !pst_r_bytes(&r, mdec.output, output_size)) return 0; - /* Age is guest cycles since last colour decode (SNAP_VER=1 payload). */ - if (age > (1ull << 40)) - age = (1ull << 40); - if (age >= psx_cycle_count) - mdec_last_color_decode_cycle = 0; + /* Age is guest cycles since the last colour decode. "Never" stays never: + * restoring it as a recent decode reports FMV that is not playing. A decode + * older than the restored clock's origin cannot be recent either. */ + if (ver == MDEC_SNAP_VER_V1 && age == MDEC_SNAP_V1_AGE_NEVER) + age = MDEC_SNAP_AGE_NEVER; + if (age == MDEC_SNAP_AGE_NEVER || age > psx_cycle_count) + mdec_last_color_decode_cycle = MDEC_NEVER_DECODED; else mdec_last_color_decode_cycle = psx_cycle_count - age; - /* Refresh host-frame hysteresis for local FMV policy only (~1 frame ≈ - * 338688 cycles @ NTSC). Cap so recently_active stays meaningful. */ - { - const uint64_t cycles_per_frame = 338688ull; - uint64_t frames_ago = age / cycles_per_frame; - if (frames_ago > 100000ull) - frames_ago = 100000ull; - if (frames_ago >= s_frame_count) - mdec_last_color_decode_frame = 0; - else - mdec_last_color_decode_frame = s_frame_count - frames_ago; - } return 1; } diff --git a/runtime/tests/test_mdec_snapshot_activity.c b/runtime/tests/test_mdec_snapshot_activity.c new file mode 100644 index 00000000..27a31eb5 --- /dev/null +++ b/runtime/tests/test_mdec_snapshot_activity.c @@ -0,0 +1,98 @@ +#undef NDEBUG /* the Release test target must execute its assertions */ +/* The MDEC FMV-activity stamp survives a snapshot round trip as itself: + * "never decoded" stays never (issue #475: it used to load as "decoded 1000 + * cycles ago", a false FMV for ~8 frames after every early snapshot load), + * a real recent decode stays recent, and v1 payloads still load. */ +#include "../src/mdec.c" + +#include +#include +#include + +uint64_t psx_cycle_count; +uint64_t s_frame_count; +int debug_server_fmv_quiet(void) { return 1; } + +#define FRAME 338688ull + +static uint8_t *snap(uint32_t *len) { + *len = mdec_snapshot_bytes(); + uint8_t *p = (uint8_t *)malloc(*len); + assert(p); + mdec_snapshot_write(p); + return p; +} + +static void put_u32(uint8_t *p, uint32_t off, uint32_t v) { + for (int i = 0; i < 4; i++) p[off + i] = (uint8_t)(v >> (8 * i)); +} +static void put_u64(uint8_t *p, uint32_t off, uint64_t v) { + for (int i = 0; i < 8; i++) p[off + i] = (uint8_t)(v >> (8 * i)); +} + +int main(void) { + uint32_t len; + uint8_t *p; + const uint32_t age_off = MDEC_SNAP_OFF_COUNTS + 8u; + + /* 1. Nothing decoded: save, load later in the session -> still idle. */ + mdec_init(); + psx_cycle_count = 100ull * FRAME; + assert(!mdec_recently_active(8)); + p = snap(&len); + psx_cycle_count = 200ull * FRAME; + assert(mdec_snapshot_read(p, len)); + assert(!mdec_recently_active(8)); + assert(mdec_color_age_cycles() == (uint64_t)0 - 1u); + free(p); + + /* 2. A real decode 3 frames before the save stays 3 frames old. */ + mdec_init(); + psx_cycle_count = 500ull * FRAME; + mdec_last_color_decode_cycle = psx_cycle_count - 3ull * FRAME; + p = snap(&len); + mdec_init(); + assert(!mdec_recently_active(8)); + psx_cycle_count = 500ull * FRAME; + assert(mdec_snapshot_read(p, len)); + assert(mdec_recently_active(8)); + assert(mdec_color_age_cycles() == 3ull * FRAME); + free(p); + + /* 3. A v1 payload's "never" (age 1000) loads as never... */ + mdec_init(); + psx_cycle_count = 50ull * FRAME; + p = snap(&len); + put_u32(p, 0u, MDEC_SNAP_VER_V1); + put_u64(p, age_off, MDEC_SNAP_V1_AGE_NEVER); + assert(mdec_snapshot_validate(p, len)); + assert(mdec_snapshot_read(p, len)); + assert(!mdec_recently_active(8)); + /* ...and any other v1 age is a real decode. */ + put_u64(p, age_off, 2ull * FRAME); + assert(mdec_snapshot_read(p, len)); + assert(mdec_recently_active(8)); + assert(mdec_color_age_cycles() == 2ull * FRAME); + free(p); + + /* 4. A decode older than the restored clock's origin is not recent. */ + mdec_init(); + psx_cycle_count = 900ull * FRAME; + mdec_last_color_decode_cycle = 100ull * FRAME; + p = snap(&len); + psx_cycle_count = 10ull * FRAME; + assert(mdec_snapshot_read(p, len)); + assert(!mdec_recently_active(8)); + free(p); + + /* 5. Unknown versions are still refused. */ + mdec_init(); + p = snap(&len); + put_u32(p, 0u, 3u); + assert(!mdec_snapshot_validate(p, len)); + assert(!mdec_snapshot_read(p, len)); + free(p); + + puts("mdec_snapshot_activity_test: PASS"); + return 0; +}