Restore BPE FMV cutover fix and SIMD MDEC paths.

Only re-arm depth24 cutover blank on long MDEC idle (not every ~15fps
decode gap), and bring back SSE2/NEON IDCT hot paths lost from the dirty tree.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alex Vanderveen
2026-07-31 17:07:58 -04:00
parent 2d0bfe5dac
commit e7f68a2ba7
2 changed files with 334 additions and 47 deletions
+13 -6
View File
@@ -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;
+321 -41
View File
@@ -5,6 +5,22 @@
#include <stdlib.h>
#include <string.h>
/* 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 <emmintrin.h>
#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 <arm_neon.h>
#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) {