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.
This commit is contained in:
committed by
U-DESKTOP-SPFP6AQ\twistedtechre
parent
3a84a6fd0b
commit
a502ceafc4
30
src/ppu.c
30
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user