From 6ffed08948ad422d1f99c55ea9d43641d1d158fd Mon Sep 17 00:00:00 2001 From: "U-DESKTOP-SPFP6AQ\\twistedtechre" Date: Mon, 4 May 2026 07:16:54 +0200 Subject: [PATCH] core: video pipeline - SW framebuffer, hoist invariants, drop NTSC memcpy Rework retro_run_blit to: * Try GET_CURRENT_SOFTWARE_FRAMEBUFFER for zero-copy. If the frontend hands us its own scanout buffer with a compatible pixel format, the blit writes the palette-LUT conversion directly into it instead of routing through fceu_video_out. Falls back to the existing buffer when the frontend declines or returns a mismatched format. The pixel format negotiated at retro_load_game is now cached in a file- scope active_pixformat for this validation. * Drop the per-frame memcpy in the NTSC path. nes_ntsc_blit writes into ntsc_video_out with a wider stride (NES_NTSC_WIDTH includes right-margin padding); the previous code copied row-by-row into a tightly packed fceu_video_out before video_cb. The pitch parameter to video_cb already lets the frontend skip past padding per scanline, so the copy was redundant. Saves ~580 KB / frame at 32 bpp - around 35 MB/s of memory traffic at 60 fps. * Hoist loop-invariant decisions out of the per-pixel inner loop. GameInfo->type != GIT_NSF and use_raw_palette do not change mid- frame; emphasis (XDBuf) is uniform per scanline because the PPU writes one colour_emphasis byte to all 256 entries of dtarget at ppu.c:683-685. The deemphasis branch is now once per row instead of once per pixel. * Replace fceu_video_out[y * width + x] indexing with a running out_row pointer (strength reduction). * Remove the unused incr local. Bit-exact verified against the baseline across 4 test ROMs (silent, idle, active gameplay, all-channel max-volume stress) for non-NTSC, SW-framebuffer-granted, and NTSC composite paths - 1200+ frames total. Audio output also bit-identical. Determinism audit pass over the video pipeline turned up no issues: no rand/srand/time/clock/gettimeofday in any core path; PPU writes to XBuf are scanline-deterministic; nes_ntsc_blit is stateless; burst_phase initializes to zero and toggles deterministically; and the existing FCEU_MemoryRand and rt-01 weakbits state machines from earlier passes remain in place. --- src/drivers/libretro/libretro.c | 93 ++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 24 deletions(-) diff --git a/src/drivers/libretro/libretro.c b/src/drivers/libretro/libretro.c index 04a7d89..afd0db5 100644 --- a/src/drivers/libretro/libretro.c +++ b/src/drivers/libretro/libretro.c @@ -153,6 +153,12 @@ static int crop_overscan_v_bottom; static bool use_raw_palette; static int aspect_ratio_par; +/* Pixel format negotiated with the frontend at retro_load_game. Cached + * here for retro_run_blit so it can validate that + * GET_CURRENT_SOFTWARE_FRAMEBUFFER returned a format we can write into + * directly. */ +static enum retro_pixel_format active_pixformat = RETRO_PIXEL_FORMAT_UNKNOWN; + /* * Flags to keep track of whether turbo * buttons toggled on or off. @@ -2925,7 +2931,6 @@ static void retro_run_blit(uint8_t *gfx) static unsigned int __attribute__((aligned(16))) d_list[32]; void* texture_vram_p = NULL; #endif - unsigned incr = 0; unsigned width = 256; unsigned height = 240; unsigned pitch = width * sizeof(bpp_t); @@ -3008,52 +3013,86 @@ static void retro_run_blit(uint8_t *gfx) width = NES_WIDTH - crop_overscan_h_left - crop_overscan_h_right; width = NES_NTSC_OUT_WIDTH(width); height = NES_HEIGHT - crop_overscan_v_top - crop_overscan_v_bottom; - pitch = width * sizeof(bpp_t); + pitch = NES_NTSC_WIDTH * sizeof(bpp_t); + /* Pass ntsc_video_out directly to the frontend with the wider + * source pitch instead of memcpy'ing into a tightly-packed + * fceu_video_out. video_cb's pitch parameter already lets the + * frontend skip past the unused right margin per scanline; the + * memcpy was redundant. Saves ~580 KB per frame at 32 bpp full + * NES_NTSC_OUT_WIDTH * NES_HEIGHT - i.e. ~35 MB/s of bandwidth + * at 60 fps. */ { - int32_t h_offset = (crop_overscan_h_left ? NES_NTSC_OUT_WIDTH(crop_overscan_h_left) : 0); - int32_t v_offset = crop_overscan_v_top; + int32_t h_offset = (crop_overscan_h_left ? NES_NTSC_OUT_WIDTH(crop_overscan_h_left) : 0); + int32_t v_offset = crop_overscan_v_top; const bpp_t *in = ntsc_video_out + h_offset + NES_NTSC_WIDTH * v_offset; - bpp_t *out = fceu_video_out; - - for (y = 0; y < height; y++) - { - memcpy(out, in, pitch); - in += NES_NTSC_WIDTH; - out += width; - } + video_cb(in, width, height, pitch); } - video_cb(fceu_video_out, width, height, pitch); } else #endif /* HAVE_NTSC_FILTER */ { - incr += (crop_overscan_h_left + crop_overscan_h_right); width -= (crop_overscan_h_left + crop_overscan_h_right); height -= (crop_overscan_v_top + crop_overscan_v_bottom); - pitch -= (crop_overscan_h_left + crop_overscan_h_right) * sizeof(bpp_t); + pitch = width * sizeof(bpp_t); gfx += (crop_overscan_v_top * 256) + crop_overscan_h_left; { - uint8_t *deemp = XDBuf + (gfx - XBuf); - for (y = 0; y < height; y++, gfx += incr, deemp += incr) + /* Try GET_CURRENT_SOFTWARE_FRAMEBUFFER for zero-copy: if the + * frontend can hand us its own scanout buffer, we write the + * pixel-format conversion directly into it and skip a round + * trip through fceu_video_out. */ + struct retro_framebuffer fb = {0}; + bpp_t *target = fceu_video_out; + size_t target_stride = (size_t)width; /* in pixels */ + + fb.width = width; + fb.height = height; + fb.access_flags = RETRO_MEMORY_ACCESS_WRITE; + if (environ_cb(RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER, &fb) + && fb.format == active_pixformat + && fb.data + && (fb.pitch % sizeof(bpp_t)) == 0) { - for (x = 0; x < width; x++, gfx++, deemp++) + target = (bpp_t *)fb.data; + target_stride = fb.pitch / sizeof(bpp_t); + pitch = fb.pitch; + } + + /* Hoist loop-invariant decisions out of the hot per-pixel + * loop. The PPU writes a uniform colour_emphasis byte to + * every entry of XDBuf for a given scanline (ppu.c:683-685), + * so emphasis is constant per row - branch once per row, not + * per pixel. NSF and use_raw_palette flags don't change mid- + * frame either. */ + { + const uint8_t *deemp_row = XDBuf + (gfx - XBuf); + const bool is_nsf = (GameInfo->type == GIT_NSF); + const uint8_t pixel_mask = use_raw_palette ? 0x3F : 0xFF; + + for (y = 0; y < height; y++) { - if (*deemp != 0 && GameInfo->type != GIT_NSF) + bpp_t *out_row = target + (size_t)y * target_stride; + uint8_t deemp = is_nsf ? 0 : deemp_row[0]; + + if (deemp != 0) { - fceu_video_out[y * width + x] = retro_palette[256 + (*gfx & 0x3F) + ((*deemp & 0x07) << 6)]; + unsigned base = 256u + ((unsigned)(deemp & 0x07) << 6); + for (x = 0; x < width; x++) + out_row[x] = retro_palette[base + (gfx[x] & 0x3F)]; } else { - uint8_t pixel_mask = use_raw_palette ? 0x3F : 0xFF; - fceu_video_out[y * width + x] = retro_palette[*gfx & pixel_mask]; + for (x = 0; x < width; x++) + out_row[x] = retro_palette[gfx[x] & pixel_mask]; } + gfx += 256; + deemp_row += 256; } } - } - video_cb(fceu_video_out, width, height, pitch); + video_cb(target, width, height, pitch); + } } #endif } @@ -3655,12 +3694,18 @@ bool retro_load_game(const struct retro_game_info *info) #ifdef FRONTEND_SUPPORTS_RGB888 pixformat = RETRO_PIXEL_FORMAT_XRGB8888; if(environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &pixformat)) + { log_cb.log(RETRO_LOG_INFO, "Frontend supports RGBX888 - will use that instead of XRGB1555.\n"); + active_pixformat = pixformat; + } #else #if FRONTEND_SUPPORTS_RGB565 pixformat = RETRO_PIXEL_FORMAT_RGB565; if(environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &pixformat)) + { log_cb.log(RETRO_LOG_INFO, "Frontend supports RGB565 - will use that instead of XRGB1555.\n"); + active_pixformat = pixformat; + } #endif #endif