diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 748f806d..79370185 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -427,8 +427,9 @@ static int s_fmv_skip_present_skip = 0; static int s_netplay_depth24_present_skip = 0; static uint32_t s_fmv_skip_last_mdec = 0; static int s_fmv_skip_hold = 0; -/* MotK FMV cutover: after an idle gap, blank the first depth24+MDEC presents - * so one-frame RGB888 junk (stale VRAM) never reaches the window. */ +/* MotK FMV cutover: after leaving depth24 (or a long MDEC idle), blank the + * first depth24+MDEC presents so one-frame RGB888 junk never reaches the + * window. Short inter-frame MDEC gaps must not re-arm this (see cutover_tick). */ static int s_d24_prev_mdec = 0; static int s_d24_saw_gap = 0; static int s_d24_cutover_blank = 0; @@ -3681,16 +3682,22 @@ static void load_transition_note(int read_active, int load_active, } /* Tick MotK-style depth24 cutover state once per present. Arm a short full- - * frame blank when MDEC returns after an idle gap (or after leaving depth24). */ + * frame blank when MDEC returns after leaving depth24, or after a *long* + * intra-depth24 idle (real inter-movie gap) — not after every short gap + * between ~15fps MDEC frames. With SIMD IDCT a movie frame often finishes in + * one vblank; treating !mdec_recently_active(3) as a cutover re-armed a + * 2-present full black blank on every decode burst (BPE FMV flicker). */ static void depth24_cutover_tick(int depth24) { const int mdec_on = depth24 && mdec_recently_active(3); + /* ~0.5s @60Hz — longer than inter-frame MDEC idle in a live movie. */ + const int mdec_long_idle = depth24 && !mdec_recently_active(30); if (!depth24) { s_d24_prev_mdec = 0; s_d24_cutover_blank = 0; s_d24_saw_gap = 1; /* next depth24+MDEC is a fresh movie cutover */ return; } - if (!mdec_on) + if (mdec_long_idle) s_d24_saw_gap = 1; if (s_d24_saw_gap && mdec_on && !s_d24_prev_mdec) { /* Hide the transitional present(s) that still show stale RGB888 junk. */ @@ -3710,8 +3717,8 @@ static void depth24_cutover_tick(int depth24) { * reset the span — hold ticks skip Swap, then the next present saw lim=0 * and wiped the whole frame every vblank. * - * Optional short cutover blank (tip): full-frame black for 1–2 presents when - * MDEC returns after an idle gap, hiding one-frame transitional junk. */ + * Optional short cutover blank: full-frame black for 1–2 presents on movie + * start / long idle resume only (see depth24_cutover_tick). */ static void depth24_fix_trailing_margin(uint32_t *buf, uint32_t w, uint32_t h, uint32_t display_x) { if (!buf || w < 8u || h == 0u) return; diff --git a/runtime/src/mdec.c b/runtime/src/mdec.c index 64526476..79a8f611 100644 --- a/runtime/src/mdec.c +++ b/runtime/src/mdec.c @@ -5,6 +5,22 @@ #include #include +/* x86-64 always has SSE2; MSVC x64 does not define __SSE2__. */ +#if defined(__SSE2__) || defined(_M_X64) || defined(_M_AMD64) || \ + (defined(_M_IX86_FP) && _M_IX86_FP >= 2) +#include +#define MDEC_HAVE_SSE2 1 +#if defined(_MSC_VER) +#define MDEC_ALIGN16 __declspec(align(16)) +#else +#define MDEC_ALIGN16 __attribute__((aligned(16))) +#endif +#endif +#if !defined(MDEC_HAVE_SSE2) && (defined(__ARM_NEON) || defined(__ARM_NEON__)) +#include +#define MDEC_HAVE_NEON 1 +#endif + extern uint64_t s_frame_count; /* FMV-activity detector: frame stamp of the newest colour (15/24-bit) MDEC @@ -232,73 +248,172 @@ static int mask9_clamp_s8(int v) { return v; } -/* Faithful R3000A MDEC IDCT (Beetle IDCT/IDCT_1D_Multi, mdec.cpp:243-291). - * Two separable 1-D passes over the >>3 scale matrix: pass 1 keeps int16 and - * transposes (out[x*8+col]); pass 2 clamps to int8 via Mask9ClampS8. Rounding - * is (sum + 0x4000) >> 15 (vs the old (sum+0xFFF)/0x2000 + per-tap /8, whose - * bias differed from hardware → the washed/wrong-contrast FMV). The block buffer - * holds int8-range samples on return. */ -static void idct_block(int16_t *block) { - /* DC-only fast path: bit-identical to the loops below when AC is zero - * (common in dark/flat FMV macroblocks). Pass1 only fills tmp[*][0]; - * pass2 only multiplies by scale[*][0]. */ +/* Faithful R3000A MDEC IDCT (Beetle IDCT/IDCT_1D_Multi). Two separable 1-D + * passes over the >>3 scale matrix: pass 1 keeps int16 and transposes + * (out[x*8+col]); pass 2 clamps via Mask9ClampS8. Rounding (sum+0x4000)>>15. + * SSE2 path matches Beetle's madd_epi16 reduce (bit-identical to scalar). */ + +static void idct_block_dc_only(int16_t *block) +{ + int16_t tmp0[8]; + int dc = block[0]; + int x, col; + for (x = 0; x < 8; x++) { + int sum = dc * (int)mdec.scale[x * 8]; + tmp0[x] = (int16_t)((sum + 0x4000) >> 15); + } + for (col = 0; col < 8; col++) { + for (x = 0; x < 8; x++) { + int sum = (int)tmp0[col] * (int)mdec.scale[x * 8]; + block[col * 8 + x] = + (int16_t)mask9_clamp_s8((sum + 0x4000) >> 15); + } + } +} + +static void idct_block_scalar(int16_t *block) +{ int ac = 0; - for (int i = 1; i < 64; i++) ac |= block[i]; + int i, col, x, u; + int16_t tmp[64]; + + for (i = 1; i < 64; i++) + ac |= block[i]; if (!ac) { - int16_t tmp0[8]; - int dc = block[0]; - for (int x = 0; x < 8; x++) { - int sum = dc * (int)mdec.scale[x * 8]; - tmp0[x] = (int16_t)((sum + 0x4000) >> 15); - } - for (int col = 0; col < 8; col++) { - for (int x = 0; x < 8; x++) { - int sum = (int)tmp0[col] * (int)mdec.scale[x * 8]; - block[col * 8 + x] = - (int16_t)mask9_clamp_s8((sum + 0x4000) >> 15); - } - } + idct_block_dc_only(block); return; } - int16_t tmp[64]; - /* pass 1 — int16 out, transposed. Skip all-zero source columns - * (sparse AC after zig-zag); zero column ⇒ zero tmp[*][col]. */ - for (int col = 0; col < 8; col++) { + for (col = 0; col < 8; col++) { const int16_t *src = block + col * 8; - int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3] - | (int)src[4] | (int)src[5] | (int)src[6] | (int)src[7]; + int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3] | + (int)src[4] | (int)src[5] | (int)src[6] | (int)src[7]; if (!col_or) { - for (int x = 0; x < 8; x++) tmp[x * 8 + col] = 0; + for (x = 0; x < 8; x++) + tmp[x * 8 + col] = 0; continue; } - for (int x = 0; x < 8; x++) { + for (x = 0; x < 8; x++) { int sum = 0; const int16_t *sc = mdec.scale + x * 8; - for (int u = 0; u < 8; u++) + for (u = 0; u < 8; u++) sum += (int)src[u] * (int)sc[u]; tmp[x * 8 + col] = (int16_t)((sum + 0x4000) >> 15); } } - /* pass 2 — int8 out (Mask9ClampS8), no transpose. Same zero-row skip. */ - for (int col = 0; col < 8; col++) { + for (col = 0; col < 8; col++) { const int16_t *src = tmp + col * 8; - int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3] - | (int)src[4] | (int)src[5] | (int)src[6] | (int)src[7]; + int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3] | + (int)src[4] | (int)src[5] | (int)src[6] | (int)src[7]; if (!col_or) { - for (int x = 0; x < 8; x++) block[col * 8 + x] = 0; + for (x = 0; x < 8; x++) + block[col * 8 + x] = 0; continue; } - for (int x = 0; x < 8; x++) { + for (x = 0; x < 8; x++) { int sum = 0; const int16_t *sc = mdec.scale + x * 8; - for (int u = 0; u < 8; u++) + for (u = 0; u < 8; u++) sum += (int)src[u] * (int)sc[u]; - block[col * 8 + x] = (int16_t)mask9_clamp_s8((sum + 0x4000) >> 15); + block[col * 8 + x] = + (int16_t)mask9_clamp_s8((sum + 0x4000) >> 15); } } } +#if defined(MDEC_HAVE_SSE2) +/* Horizontal sum of 4×i32 after madd_epi16 — same reduce as Beetle mdec.c. */ +static int idct_sse2_dot8(const int16_t *src8, const int16_t *scale8) +{ + __m128i c = _mm_loadu_si128((const __m128i *)src8); + __m128i m = _mm_loadu_si128((const __m128i *)scale8); + __m128i sum = _mm_madd_epi16(m, c); + MDEC_ALIGN16 int32_t tmp[4]; + sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, _MM_SHUFFLE(0, 1, 2, 3))); + sum = _mm_add_epi32(sum, _mm_shuffle_epi32(sum, _MM_SHUFFLE(0, 0, 0, 1))); + _mm_store_si128((__m128i *)tmp, sum); + return tmp[0]; +} + +static void idct_block_sse2(int16_t *block) +{ + int ac = 0; + int i, col, x; + MDEC_ALIGN16 int16_t tmp[64]; + + for (i = 1; i < 64; i++) + ac |= block[i]; + if (!ac) { + idct_block_dc_only(block); + return; + } + + for (col = 0; col < 8; col++) { + const int16_t *src = block + col * 8; + int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3] | + (int)src[4] | (int)src[5] | (int)src[6] | (int)src[7]; + if (!col_or) { + for (x = 0; x < 8; x++) + tmp[x * 8 + col] = 0; + continue; + } + for (x = 0; x < 8; x++) { + int sum = idct_sse2_dot8(src, mdec.scale + x * 8); + tmp[x * 8 + col] = (int16_t)((sum + 0x4000) >> 15); + } + } + for (col = 0; col < 8; col++) { + const int16_t *src = tmp + col * 8; + int col_or = (int)src[0] | (int)src[1] | (int)src[2] | (int)src[3] | + (int)src[4] | (int)src[5] | (int)src[6] | (int)src[7]; + if (!col_or) { + for (x = 0; x < 8; x++) + block[col * 8 + x] = 0; + continue; + } + for (x = 0; x < 8; x++) { + int sum = idct_sse2_dot8(src, mdec.scale + x * 8); + block[col * 8 + x] = + (int16_t)mask9_clamp_s8((sum + 0x4000) >> 15); + } + } +} + +static void idct_simd_selfcheck(void) +{ + uint32_t seed = 0xC0FFEEu; + int n; + for (n = 0; n < 64; n++) + mdec.scale[n] = (int16_t)(((n % 9) - 4) * 256); + /* Deterministic patterns + PRNG — abort if SSE2 ever drifts from scalar. */ + for (n = 0; n < 512; n++) { + int16_t a[64], b[64]; + int i; + for (i = 0; i < 64; i++) { + seed = seed * 1664525u + 1013904223u; + a[i] = (int16_t)((int)(seed >> 16) - 32768); + if (n < 8) + a[i] = (i == 0) ? (int16_t)(n * 100) : 0; /* DC-only */ + b[i] = a[i]; + } + idct_block_scalar(a); + idct_block_sse2(b); + if (memcmp(a, b, sizeof(a)) != 0) + abort(); + } +} + +static void idct_block(int16_t *block) +{ + idct_block_sse2(block); +} +#else +static void idct_block(int16_t *block) +{ + idct_block_scalar(block); +} +#endif + static int decode_rle_block(int16_t *block, const uint8_t *quant, uint32_t *pos, uint32_t end) { memset(block, 0, 64 * sizeof(int16_t)); @@ -395,6 +510,96 @@ static void append_luma_block(const int16_t *yblk) { mdec.output_size += 64u; } +#if defined(MDEC_HAVE_SSE2) +/* Beetle mdec.c EncodeRow24 — 8 luma + 4 chroma → 24 RGB bytes, bit-exact. */ +#define MDEC_MUL32(a, b) \ + _mm_or_si128( \ + _mm_and_si128(_mm_mul_epu32((a), (b)), _mm_set1_epi64x(0xFFFFFFFFull)),\ + _mm_slli_epi64( \ + _mm_mul_epu32(_mm_srli_epi64((a), 32), _mm_srli_epi64((b), 32)), \ + 32)) +#define MDEC_M9(v) \ + _mm_min_epi16(_mm_max_epi16(_mm_srai_epi16(_mm_slli_epi16((v), 7), 7), \ + _mm_set1_epi16(-128)), \ + _mm_set1_epi16(127)) + +static void mdec_encode_row24_sse2(const int16_t *by, const int16_t *cb4, + const int16_t *cr4, uint8_t rgb_xor, + uint8_t *out) +{ + int16_t cbd[8], crd[8]; + int8_t by8[8]; + int l, i; + __m128i R, G, B; + __m128i flip = _mm_set1_epi16((short)(0x80 ^ rgb_xor)); + uint8_t r8[8], g8[8], b8[8]; + __m128i y16, CB, CR, cb_lo, cb_hi, cr_lo, cr_hi, y_lo, y_hi; + const __m128i k359 = _mm_set1_epi32(359); + const __m128i k454 = _mm_set1_epi32(454); + const __m128i km88 = _mm_set1_epi32(-88); + const __m128i km183 = _mm_set1_epi32(-183); + const __m128i bias = _mm_set1_epi32(0x80); + __m128i r_lo, r_hi, b_lo, b_hi, a0, a1, c0, c1, g_lo, g_hi; + + for (l = 0; l < 8; l++) + by8[l] = (int8_t)by[l]; + for (l = 0; l < 4; l++) { + cbd[2 * l] = cbd[2 * l + 1] = (int16_t)cb4[l]; + crd[2 * l] = crd[2 * l + 1] = (int16_t)cr4[l]; + } + + y16 = _mm_srai_epi16( + _mm_slli_epi16( + _mm_unpacklo_epi8(_mm_loadl_epi64((const __m128i *)by8), + _mm_setzero_si128()), + 8), + 8); + CB = _mm_loadu_si128((const __m128i *)cbd); + CR = _mm_loadu_si128((const __m128i *)crd); + cb_lo = _mm_srai_epi32(_mm_unpacklo_epi16(CB, CB), 16); + cb_hi = _mm_srai_epi32(_mm_unpackhi_epi16(CB, CB), 16); + cr_lo = _mm_srai_epi32(_mm_unpacklo_epi16(CR, CR), 16); + cr_hi = _mm_srai_epi32(_mm_unpackhi_epi16(CR, CR), 16); + y_lo = _mm_srai_epi32(_mm_unpacklo_epi16(y16, y16), 16); + y_hi = _mm_srai_epi32(_mm_unpackhi_epi16(y16, y16), 16); + + r_lo = _mm_add_epi32( + y_lo, _mm_srai_epi32(_mm_add_epi32(MDEC_MUL32(k359, cr_lo), bias), 8)); + r_hi = _mm_add_epi32( + y_hi, _mm_srai_epi32(_mm_add_epi32(MDEC_MUL32(k359, cr_hi), bias), 8)); + b_lo = _mm_add_epi32( + y_lo, _mm_srai_epi32(_mm_add_epi32(MDEC_MUL32(k454, cb_lo), bias), 8)); + b_hi = _mm_add_epi32( + y_hi, _mm_srai_epi32(_mm_add_epi32(MDEC_MUL32(k454, cb_hi), bias), 8)); + a0 = _mm_andnot_si128(_mm_set1_epi32(0x1F), MDEC_MUL32(km88, cb_lo)); + a1 = _mm_andnot_si128(_mm_set1_epi32(0x1F), MDEC_MUL32(km88, cb_hi)); + c0 = _mm_andnot_si128(_mm_set1_epi32(0x07), MDEC_MUL32(km183, cr_lo)); + c1 = _mm_andnot_si128(_mm_set1_epi32(0x07), MDEC_MUL32(km183, cr_hi)); + g_lo = _mm_add_epi32( + y_lo, + _mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(a0, c0), bias), 8)); + g_hi = _mm_add_epi32( + y_hi, + _mm_srai_epi32(_mm_add_epi32(_mm_add_epi32(a1, c1), bias), 8)); + R = MDEC_M9(_mm_packs_epi32(r_lo, r_hi)); + G = MDEC_M9(_mm_packs_epi32(g_lo, g_hi)); + B = MDEC_M9(_mm_packs_epi32(b_lo, b_hi)); + R = _mm_xor_si128(_mm_and_si128(R, _mm_set1_epi16(0xFF)), flip); + G = _mm_xor_si128(_mm_and_si128(G, _mm_set1_epi16(0xFF)), flip); + B = _mm_xor_si128(_mm_and_si128(B, _mm_set1_epi16(0xFF)), flip); + _mm_storel_epi64((__m128i *)r8, _mm_packus_epi16(R, R)); + _mm_storel_epi64((__m128i *)g8, _mm_packus_epi16(G, G)); + _mm_storel_epi64((__m128i *)b8, _mm_packus_epi16(B, B)); + for (i = 0; i < 8; i++) { + out[i * 3 + 0] = r8[i]; + out[i * 3 + 1] = g8[i]; + out[i * 3 + 2] = b8[i]; + } +} +#undef MDEC_MUL32 +#undef MDEC_M9 +#endif /* MDEC_HAVE_SSE2 */ + static void append_color_macroblock(const int16_t *crblk, const int16_t *cbblk, const int16_t yblk[4][64]) { /* 16×16: 512 bytes @15bpp, 768 @24bpp — reserve once per MB. */ @@ -402,6 +607,27 @@ static void append_color_macroblock(const int16_t *crblk, const int16_t *cbblk, uint8_t *out = output_reserve(need); if (!out) return; uint8_t *p = out; +#if defined(MDEC_HAVE_SSE2) + /* 24bpp FMV path — row SIMD (Beetle EncodeRow24). 16bpp stays scalar. */ + if (mdec.output_depth != 3) { + uint8_t rgb_xor = mdec.output_signed ? 0x80u : 0x00u; + int py; + for (py = 0; py < 16; py++) { + int y_left = (py >= 8 ? 2 : 0); + int y_right = y_left + 1; + int ly = py & 7; + int crow = (py >> 1) * 8; + mdec_encode_row24_sse2(&yblk[y_left][ly * 8], &cbblk[crow], + &crblk[crow], rgb_xor, p); + p += 24; + mdec_encode_row24_sse2(&yblk[y_right][ly * 8], &cbblk[crow + 4], + &crblk[crow + 4], rgb_xor, p); + p += 24; + } + mdec.output_size += need; + return; + } +#endif for (int py = 0; py < 16; py++) { for (int px = 0; px < 16; px++) { int y_index = (py >= 8 ? 2 : 0) + (px >= 8 ? 1 : 0); @@ -562,6 +788,60 @@ void mdec_init(void) { } mdec.output_depth = 3; mdec.current_block = 4; +#if defined(MDEC_HAVE_SSE2) + idct_simd_selfcheck(); + memset(mdec.scale, 0, sizeof(mdec.scale)); + /* YCbCr row vs scalar emit — 24bpp FMV path (depth 2). */ + { + uint32_t seed = 0xBADC0DEEu; + int n, i; + mdec.output_depth = 2; + mdec.output_signed = 0; + for (n = 0; n < 256; n++) { + int16_t by[8], cb4[4], cr4[4]; + uint8_t sse[24], ref[24]; + uint8_t *rp = ref; + for (i = 0; i < 8; i++) { + seed = seed * 1664525u + 1013904223u; + by[i] = (int16_t)((int)(seed % 255u) - 128); + } + for (i = 0; i < 4; i++) { + seed = seed * 1664525u + 1013904223u; + cb4[i] = (int16_t)((int)(seed % 255u) - 128); + seed = seed * 1664525u + 1013904223u; + cr4[i] = (int16_t)((int)(seed % 255u) - 128); + } + mdec_encode_row24_sse2(by, cb4, cr4, 0, sse); + for (i = 0; i < 8; i++) + rp = emit_rgb_pixel(rp, by[i], cr4[i >> 1], cb4[i >> 1]); + if (memcmp(sse, ref, 24) != 0) + abort(); + } + mdec.output_signed = 1; + for (n = 0; n < 64; n++) { + int16_t by[8], cb4[4], cr4[4]; + uint8_t sse[24], ref[24]; + uint8_t *rp = ref; + for (i = 0; i < 8; i++) { + seed = seed * 1664525u + 1013904223u; + by[i] = (int16_t)((int)(seed % 255u) - 128); + } + for (i = 0; i < 4; i++) { + seed = seed * 1664525u + 1013904223u; + cb4[i] = (int16_t)((int)(seed % 255u) - 128); + seed = seed * 1664525u + 1013904223u; + cr4[i] = (int16_t)((int)(seed % 255u) - 128); + } + mdec_encode_row24_sse2(by, cb4, cr4, 0x80u, sse); + for (i = 0; i < 8; i++) + rp = emit_rgb_pixel(rp, by[i], cr4[i >> 1], cb4[i >> 1]); + if (memcmp(sse, ref, 24) != 0) + abort(); + } + mdec.output_signed = 0; + mdec.output_depth = 3; + } +#endif } uint32_t mdec_read(uint32_t addr) {