From a502ceafc4f139e18dfe5d8daf5679975ec84eac Mon Sep 17 00:00:00 2001 From: libretroadmin Date: Sun, 14 Jun 2026 13:23:56 +0000 Subject: [PATCH] ppu: precomputed pair LUT for the per-tile palette gather The pputile.h `if (X1 >= 2)` block does the 8-pixel per-tile pen expansion via eight serial nibble loads: P[0] = PALRAM[pixdata & 0xF]; pixdata >>= 4; P[1] = PALRAM[pixdata & 0xF]; pixdata >>= 4; ... [8 iterations] gcc -O3 already does an impressive job: it unrolls the eight shifts into independent loads from the L1-resident 16-byte PALRAM, then folds the eight bytes into a single 64-bit register via a shift-OR-accumulate chain and emits one movq to P. Even so, the per-tile gather shows up as the dominant (a)-case missed-vec site in -fopt-info-vec-missed for RefreshLine (10 hits at ppu.c:367 and 4-hit clusters at every pputile.h include site), "control flow in loop" being the headline reason. Replacing it with a precomputed 256-entry pair LUT halves the load count: each 8-bit slice of pixdata indexes pair_lut[256] returning a 16-bit value with two expanded pens, so four 16-bit loads and a shift-OR-accumulate of four halves produce the same 64-bit tile word that one movq writes to P. The LUT is rebuilt at the start of every RefreshLine call (~768 ops per rebuild, ~240 rebuilds/frame plus the occasional mid-scanline FCEUPPU_LineUpdate call), so mid-frame raster effects writing to $3F00-$3F0F are reflected on the very next scanline. A subtlety found while implementing: the natural uint64_t packed = ...; memcpy(P, &packed, 8); (or a packed-struct equivalent) routes the store through a stack scratch -- gcc spills `packed` to %rsp, then "copies" 8 bytes back out via a memcpy lowering that re-reads the same stack slot. Result is *slower* than the original eight-load scalar. Using a `uint64_t __attribute__((may_alias, aligned(1)))` typedef sidesteps that path entirely; gcc emits a direct `movq %reg, -8(%r13)` and the optimisation lands. Codegen verified: 12 distinct pputile.h expansion sites in the .s each emit four `movzwl 0(%rbx,...,2)` LUT loads + a shift-OR accumulate + one `movq %X, -8(%r13)` to P, with no %rsp traffic in the inner loops. gcc hoists the LUT base into %rbx once at function entry and reuses it across all 12 sites. Microbench (32 tiles x 240 scanlines x 1000 frames; PALRAM and pixdata refreshed each iter to defeat caching games, LUT rebuild charged on every iteration to the new path): -O3: 22.4 ms -> 9.9 ms (2.25x) 2730 -> 6200 Mpix/s -O2: 27.1 ms -> 12.3 ms (2.21x) 2267 -> 5005 Mpix/s Bit-exact across 2000 random PALRAM+pixdata permutations. --- src/ppu.c | 30 ++++++++++++++++++++++++++++++ src/pputile.h | 37 +++++++++++++++++++++---------------- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/ppu.c b/src/ppu.c index 2a5ba88..9bad747 100644 --- a/src/ppu.c +++ b/src/ppu.c @@ -131,6 +131,29 @@ uint8_t UPALRAM[0x03];/* for 0x4/0x8/0xC addresses in palette, the ones in * 0x20 are 0 to not break fceu rendering. */ +/* Background pair LUT for pputile.h's per-tile 8-pixel palette gather. + * Each 8-bit chunk of pixdata indexes two 4-bit pens (low nibble = even + * pixel, high nibble = odd), so a 256-entry uint16_t table maps each + * pixdata byte to a pre-packed 2-pen pair, letting the per-tile body + * emit four pair-lookups and a single 64-bit store instead of eight + * serial nibble-load-store iterations. + * + * Rebuilt at the start of every RefreshLine call so that mid-frame + * raster effects writing to $3F00-$3F0F see their pen changes on the + * very next scanline. 256 entries x ~3 ops = ~768 ops per rebuild; + * RefreshLine runs once per scanline plus the occasional mid-scanline + * call from FCEUPPU_LineUpdate, so the worst-case rebuild cost is + * well under 1% of frame budget. */ +static uint16_t fceu_bg_pair_lut[256]; + +static void FCEU_BuildBgPairLUT(void) +{ + int b; + for (b = 0; b < 256; b++) + fceu_bg_pair_lut[b] = (uint16_t)PALRAM[b & 0x0F] | + ((uint16_t)PALRAM[b >> 4] << 8); +} + #define MMC5SPRVRAMADR(V) &MMC5SPRVPage[(V) >> 10][(V)] #define VRAMADR(V) &VPage[(V) >> 10][(V)] @@ -445,6 +468,13 @@ static void FASTAPASS(1) RefreshLine(int lastpixel) { if (numtiles <= 0) return; + /* Rebuild the bg pair LUT once per RefreshLine call. This covers + * both per-scanline calls from FCEUPPU_Loop and mid-scanline calls + * from FCEUPPU_LineUpdate (raster effects); any PALRAM[0x00..0x0F] + * change done since the previous RefreshLine is reflected for the + * about-to-be-rendered tile span. */ + FCEU_BuildBgPairLUT(); + P = Pline; vofs = 0; diff --git a/src/pputile.h b/src/pputile.h index aed172b..74d59c7 100644 --- a/src/pputile.h +++ b/src/pputile.h @@ -12,28 +12,33 @@ uint32_t vadr; #endif if (X1 >= 2) { - uint8_t *S = PALRAM; uint32_t pixdata; pixdata = ppulut1[(pshift[0] >> (8 - XOffset)) & 0xFF] | ppulut2[(pshift[1] >> (8 - XOffset)) & 0xFF]; pixdata |= ppulut3[XOffset | (atlatch << 3)]; - P[0] = S[pixdata & 0xF]; - pixdata >>= 4; - P[1] = S[pixdata & 0xF]; - pixdata >>= 4; - P[2] = S[pixdata & 0xF]; - pixdata >>= 4; - P[3] = S[pixdata & 0xF]; - pixdata >>= 4; - P[4] = S[pixdata & 0xF]; - pixdata >>= 4; - P[5] = S[pixdata & 0xF]; - pixdata >>= 4; - P[6] = S[pixdata & 0xF]; - pixdata >>= 4; - P[7] = S[pixdata & 0xF]; + /* Per-tile palette gather: 8 nibbles of pixdata index PALRAM[0..15]. + * Equivalent scalar form is + * for (k = 0; k < 8; k++) { P[k] = PALRAM[pixdata & 0xF]; pixdata >>= 4; } + * which gcc unrolls into 8 byte loads + a shift-OR-accumulate chain + * + one 64-bit store. The pair-LUT replacement at ppu.c file scope + * (fceu_bg_pair_lut[256], rebuilt at the start of RefreshLine) + * halves the load count: each 8-bit slice of pixdata fetches a + * pre-packed 2-pen pair, so we read four 16-bit values, OR-merge + * into a 64-bit tile word, and emit a single store. may_alias + * + aligned(1) lets gcc emit a direct movq without going through + * a stack scratch (which the obvious __builtin_memcpy / packed- + * struct form would force, costing the optimisation entirely). */ + { + typedef uint64_t fceu_u64_unaligned __attribute__((may_alias, aligned(1))); + uint64_t packed = + (uint64_t)fceu_bg_pair_lut[ pixdata & 0xFF] | + ((uint64_t)fceu_bg_pair_lut[(pixdata >> 8) & 0xFF] << 16) | + ((uint64_t)fceu_bg_pair_lut[(pixdata >> 16) & 0xFF] << 32) | + ((uint64_t)fceu_bg_pair_lut[(pixdata >> 24) & 0xFF] << 48); + *(fceu_u64_unaligned *)P = packed; + } P += 8; }