From 36a559c27c4c496d16868bedee52c851526addcf Mon Sep 17 00:00:00 2001 From: pogoflow <6523921+pogoflow@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:40:51 +0100 Subject: [PATCH] Draw BG modes 4-6 with the compatibility renderer under NewRenderer PpuDrawWholeLine implements modes 0-3 and treats every other mode as mode 7. A mode 4, 5 or 6 screen therefore sampled BG1's tilemap as a mode-7 pixel map: a mode 5 menu with hi-res text over a 4bpp background came out as vertical colour stripes, while VRAM, CGRAM and OAM matched a reference emulator. Found on a HiROM title. ppu_runLine now routes modes 4-6 to ppu_draw_whole_line_legacy, which already handles 16-pixel-wide tiles, the hi-res column split and offset-per-tile, until the optimized renderer grows them. Modes 0-3 and 7 are unchanged. tests/ppu/ppu_hires_mode_test.c: for modes 4, 5 and 6 the optimized renderer must produce the compatibility renderer's scanline, mode 5 must show the 16-pixel tile split, and mode 1 stays on the optimized path. Registered in tests/ppu/run.ps1 and tests/run_c_tests.sh. --- runner/src/snes/ppu.c | 15 ++- tests/ppu/ppu_hires_mode_test.c | 163 ++++++++++++++++++++++++++++++++ tests/ppu/run.ps1 | 2 +- tests/run_c_tests.sh | 4 + 4 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 tests/ppu/ppu_hires_mode_test.c diff --git a/runner/src/snes/ppu.c b/runner/src/snes/ppu.c index c5728bd9..13a8220d 100644 --- a/runner/src/snes/ppu.c +++ b/runner/src/snes/ppu.c @@ -770,6 +770,18 @@ int ppu_wlog_collect(uint32_t frame, PpuWlogEntry *out, int cap, int *lost) { return count; } +/* BG modes the optimized line renderer implements. PpuDrawWholeLine knows + * modes 0-3 and treats everything else as mode 7, so a mode 4/5/6 screen + * (8bpp with offset-per-tile, and the two 512-pixel hi-res modes) came out + * as mode-7 garbage (a mode 5 menu with hi-res text over a 4bpp background + * showed up as vertical colour stripes). Those modes take the + * compatibility renderer's scanline until the optimized one grows them; + * it samples the main screen from the odd hi-res column, as it always did + * with NewRenderer=0. Regression: tests/ppu/ppu_hires_mode_test.c. */ +static bool PpuOptimizedRendererHandlesMode(uint mode) { + return mode <= 3 || mode == 7; +} + void ppu_runLine(Ppu* ppu, int line) { PPU_T0_DECL /* Writes from here to the next ppu_runLine land on line + 1; no register @@ -844,7 +856,8 @@ void ppu_runLine(Ppu* ppu, int line) { sizeof(ppu->overlayBuffers[kPpuOverlaySource_Obj])); ppu->lineHasSprites = !PPU_forcedBlank(ppu) && ppu_evaluateSprites(ppu, line - 1); - if (ppu->renderFlags & kPpuRenderFlags_NewRenderer) { + if ((ppu->renderFlags & kPpuRenderFlags_NewRenderer) && + PpuOptimizedRendererHandlesMode(PPU_mode(ppu))) { debug_server_on_ppu_line_drawn(line, 1, ppu->bgmode); PPU_T0; PpuDrawWholeLine(ppu, line); PPU_ACC(g_ppu_sec_line_ms); #ifdef SNESRECOMP_INTERP_PROFILE diff --git a/tests/ppu/ppu_hires_mode_test.c b/tests/ppu/ppu_hires_mode_test.c new file mode 100644 index 00000000..4dca02ec --- /dev/null +++ b/tests/ppu/ppu_hires_mode_test.c @@ -0,0 +1,163 @@ +/* Regression: BG modes the optimized renderer does not implement (4, 5, 6) + * must come out of the same scanline as the compatibility renderer instead + * of falling through to the mode-7 path. + * + * Seen on a mode 5 menu (hi-res text over a 4bpp background): with + * NewRenderer=1 the screen was vertical colour stripes, because the mode + * dispatch in PpuDrawWholeLine knew modes 0-3 and treated everything else + * as mode 7, so BG1's tilemap was sampled as a mode-7 pixel map. + * + * No game ROM, generated data, or platform frontend is required. */ +#include +#include +#include + +#include "snes/ppu.h" +#include "snes/snes.h" + +Snes *g_snes; + +/* ppu.c references these runtime globals, but this harness links ppu.c alone. */ +int snes_frame_counter; +unsigned char g_snesrecomp_last_hdmaen; + +uint16_t WsShadowTile(int layer, int screen_x, uint32_t wrapped_y, + uint16_t real_tile) { + (void)layer; + (void)screen_x; + (void)wrapped_y; + return real_tile; +} + +bool WsShadowLayerActive(int layer) { + (void)layer; + return false; +} + +uint32_t WsShadowWorldX(int layer) { + (void)layer; + return 0; +} + +uint32_t WsShadowPresentWorldY(int layer, int screen_x) { + (void)layer; + (void)screen_x; + return 0; +} + +uint32_t WsShadowScrollY(int layer) { + (void)layer; + return 0; +} + +void WsShadowOnVramWrite(uint16_t word_adr, uint16_t value) { + (void)word_adr; + (void)value; +} + +static int check(bool condition, const char *message) { + if (!condition) fprintf(stderr, "FAIL: %s\n", message); + return condition ? 0 : 1; +} + +/* One solid 4bpp tile (16 words) at word address tile_base. */ +static void fill_solid_4bpp_tile(Ppu *ppu, uint16_t tile_base, uint8_t color) { + uint16_t lo = 0, hi = 0; + for (int bit = 0; bit < 8; bit++) { + if (color & 1) lo |= (uint16_t)(1u << bit); + if (color & 2) lo |= (uint16_t)(1u << (8 + bit)); + if (color & 4) hi |= (uint16_t)(1u << bit); + if (color & 8) hi |= (uint16_t)(1u << (8 + bit)); + } + for (int row = 0; row < 8; row++) { + ppu->vram[tile_base + row] = lo; + ppu->vram[tile_base + 8 + row] = hi; + } +} + +/* BG1 only, tilemap at word 0, characters at word 0x1000. Every tilemap + * entry names tile 1; in the 16-pixel-wide tile modes the right half is + * tile 2. Tile 1 is colour 1, tile 2 is colour 2, so a hi-res scanline is + * 8 pixels of colour 1 followed by 8 of colour 2, repeating. */ +static void setup_two_tone_bg1(Ppu *ppu, uint8_t mode) { + ppu_reset(ppu); + ppu->inidisp = 0x0f; + ppu->bgmode = mode; + ppu->screenEnabled[0] = 1; + ppu->bgXsc[0] = 0x00; + ppu->bgTileAdr = 0x0001; + memset(ppu->vram, 0, sizeof ppu->vram); + for (int i = 0; i < 32 * 32; i++) ppu->vram[i] = 1; + fill_solid_4bpp_tile(ppu, 0x1000 + 1 * 16, 1); + fill_solid_4bpp_tile(ppu, 0x1000 + 2 * 16, 2); + for (size_t i = 0; i < sizeof ppu->cgram / sizeof ppu->cgram[0]; i++) + ppu->cgram[i] = 0; + ppu->cgram[1] = 0x001f; /* red */ + ppu->cgram[2] = 0x7c00; /* blue */ +} + +static void render_line(Ppu *ppu, uint8_t *pixels, size_t pitch, uint32_t flags) { + memset(pixels, 0, pitch); + PpuBeginDrawing(ppu, pixels, pitch, flags); + ppu_runLine(ppu, 0); + ppu_runLine(ppu, 1); +} + +static bool pixel_is(const uint8_t *pixels, int x, uint8_t r, uint8_t g, uint8_t b) { + const uint8_t *p = pixels + (size_t)x * 4; /* BGRX */ + return p[0] == b && p[1] == g && p[2] == r; +} + +int main(void) { + enum { kPitch = kPpuXPixels * 4 }; + uint8_t legacy[kPitch], optimized[kPitch]; + Ppu *ppu = ppu_init(); + int failures = 0; + if (!ppu) return 2; + + static const uint8_t kModes[] = { 4, 5, 6 }; + for (size_t m = 0; m < sizeof kModes; m++) { + char msg[96]; + uint8_t mode = kModes[m]; + + setup_two_tone_bg1(ppu, mode); + render_line(ppu, legacy, kPitch, 0); + setup_two_tone_bg1(ppu, mode); + render_line(ppu, optimized, kPitch, kPpuRenderFlags_NewRenderer); + + snprintf(msg, sizeof msg, "mode %u: optimized renderer matches the compatibility renderer", mode); + failures += check(!memcmp(legacy, optimized, kPitch), msg); + + snprintf(msg, sizeof msg, "mode %u: compatibility renderer draws the layer (not backdrop)", mode); + failures += check(!pixel_is(legacy, 0, 0, 0, 0), msg); + } + + /* Mode 5 samples the main screen from the odd hi-res column, so the + * 256-wide output shows 4 pixels of tile 1 then 4 of tile 2. */ + setup_two_tone_bg1(ppu, 5); + render_line(ppu, optimized, kPitch, kPpuRenderFlags_NewRenderer); + failures += check(pixel_is(optimized, 0, 0xff, 0, 0) && pixel_is(optimized, 3, 0xff, 0, 0), + "mode 5: pixels 0-3 come from the left 8 hi-res columns (tile 1, red)"); + failures += check(pixel_is(optimized, 4, 0, 0, 0xff) && pixel_is(optimized, 7, 0, 0, 0xff), + "mode 5: pixels 4-7 come from the right 8 hi-res columns (tile 2, blue)"); + failures += check(pixel_is(optimized, 8, 0xff, 0, 0), + "mode 5: the 16-pixel tile repeats at pixel 8"); + + /* Modes 0-3 and 7 stay on the optimized path: a mode-1 scene must not + * change because of the fallback. */ + setup_two_tone_bg1(ppu, 1); + render_line(ppu, legacy, kPitch, 0); + setup_two_tone_bg1(ppu, 1); + render_line(ppu, optimized, kPitch, kPpuRenderFlags_NewRenderer); + failures += check(pixel_is(optimized, 0, 0xff, 0, 0) && pixel_is(optimized, 7, 0xff, 0, 0) && + pixel_is(optimized, 8, 0xff, 0, 0), + "mode 1: 8-pixel tiles, tile 1 fills pixels 0-15"); + + ppu_free(ppu); + if (failures) { + fprintf(stderr, "%d check(s) failed\n", failures); + return 1; + } + printf("ppu_hires_mode_test: ok\n"); + return 0; +} diff --git a/tests/ppu/run.ps1 b/tests/ppu/run.ps1 index 09f8d9ec..b01565a9 100644 --- a/tests/ppu/run.ps1 +++ b/tests/ppu/run.ps1 @@ -1,7 +1,7 @@ $ErrorActionPreference = "Stop" $root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path $gcc = (Get-Command gcc).Source -$tests = @("ppu_sprite_limit_test", "ppu_world_mirror_test", "ppu_elastic_band_test", "ppu_mode7_hd_test", "ppu_raster_journal_test") +$tests = @("ppu_sprite_limit_test", "ppu_world_mirror_test", "ppu_elastic_band_test", "ppu_mode7_hd_test", "ppu_raster_journal_test", "ppu_hires_mode_test") $failed = @() foreach ($test in $tests) { diff --git a/tests/run_c_tests.sh b/tests/run_c_tests.sh index 59d44c6d..0b3a3b10 100644 --- a/tests/run_c_tests.sh +++ b/tests/run_c_tests.sh @@ -76,6 +76,10 @@ echo "=== PPU sprite limits ===" -lm -o "$OUT/ppu_sprite_limit_test" "$OUT/ppu_sprite_limit_test" +echo "=== PPU modes 4-6 fall back to the compatibility renderer ===" +"$CC" -std=c11 -Wall -Wextra -O1 -DSNESRECOMP_REVERSE_DEBUG=0 -I "$ROOT/runner/src" -I "$ROOT/runner/src/snes" "$ROOT/tests/ppu/ppu_hires_mode_test.c" "$ROOT/runner/src/snes/ppu.c" "$ROOT/runner/src/snes/ppu_legacy.c" -lm -o "$OUT/ppu_hires_mode_test" +"$OUT/ppu_hires_mode_test" + echo "=== PPU widescreen world-mirror band ===" "$CC" -std=c11 -Wall -Wextra -O1 -DSNESRECOMP_REVERSE_DEBUG=0 -I "$ROOT/runner/src" -I "$ROOT/runner/src/snes" "$ROOT/tests/ppu/ppu_world_mirror_test.c" "$ROOT/runner/src/snes/ppu.c" "$ROOT/runner/src/snes/ppu_legacy.c" -lm -o "$OUT/ppu_world_mirror_test" "$OUT/ppu_world_mirror_test"