From 4676707ae02be2a66941d82ec9d73cb5068c7c61 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Mon, 31 Aug 2026 03:17:48 +0300 Subject: [PATCH] Preserve interlaced PAL scanout height --- runtime/include/display_scanout.h | 11 +++++++++++ runtime/src/gpu.c | 13 ++++++++----- runtime/src/main.cpp | 23 +++++++++++++---------- runtime/tests/test_display_scanout.c | 3 +++ 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/runtime/include/display_scanout.h b/runtime/include/display_scanout.h index 8f317995..904b0160 100644 --- a/runtime/include/display_scanout.h +++ b/runtime/include/display_scanout.h @@ -5,6 +5,11 @@ #include +/* PAL active video is 288 lines per field and 576 rows when the interlaced + * display bit is set. CPU presentation buffers must preserve that full + * canvas even though PS1 VRAM itself remains 512 rows and wraps on access. */ +#define PSX_DISPLAY_PRESENT_MAX_HEIGHT 576u + typedef struct { uint32_t canvas_height; uint32_t canvas_origin_y; @@ -24,6 +29,12 @@ static inline uint32_t psx_display_present_height( return depth24 && canvas_height != 0u ? canvas_height : source_height; } +static inline uint32_t psx_display_interlaced_rows( + uint32_t rows, int interlaced) +{ + return interlaced ? rows * 2u : rows; +} + /* Preserve the legacy default for an unset/degenerate GP1(07h) range. A * programmed increasing range that has no active-video intersection owns a * zero height instead, so presentation fails closed to black. */ diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 4d80800f..50e15e67 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -3232,10 +3232,11 @@ void gpu_get_display_info(GpuDisplayInfo* out) { uint32_t screen_origin_y = vertical.valid ? vertical.canvas_origin_y : 0u; uint32_t screen_source_skip_y = vertical.valid ? vertical.source_skip_y : 0u; if (vres) { - h *= 2; /* 480i */ - screen_h *= 2; - screen_origin_y *= 2; - screen_source_skip_y *= 2; + h = psx_display_interlaced_rows(h, 1); + screen_h = psx_display_interlaced_rows(screen_h, 1); + screen_origin_y = psx_display_interlaced_rows(screen_origin_y, 1); + screen_source_skip_y = psx_display_interlaced_rows( + screen_source_skip_y, 1); } /* 24-bit scanout uses the same CRTC pixel width as 15-bit (DuckStation / @@ -3247,7 +3248,9 @@ void gpu_get_display_info(GpuDisplayInfo* out) { /* Clamp to sane maximums */ if (w > 640) w = 640; - if (screen_h > 512) screen_h = 512; + const uint32_t max_screen_h = out->depth24 + ? PSX_DISPLAY_PRESENT_MAX_HEIGHT : 512u; + if (screen_h > max_screen_h) screen_h = max_screen_h; if (screen_origin_y > screen_h) screen_origin_y = screen_h; if (h > screen_h - screen_origin_y) h = screen_h - screen_origin_y; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 6ca20e8d..0ed4124f 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -393,9 +393,8 @@ static void apply_offline_pad_count(int game_players, bool multitap_enabled) n = 2; g_offline_pad_count = n; } -/* ARGB8888 staging buffer. Sized for the active internal resolution: - * 640*scale x 512*scale. Allocated once the supersampling scale is known - * (sized for the native 640x512 when supersampling is off). */ +/* ARGB8888 staging buffer. The 576-row maximum preserves the full interlaced + * PAL active canvas. Allocated once the supersampling scale is known. */ static uint32_t* sdl_pixel_buf = nullptr; typedef void (*ModFrameHook)(void); @@ -1880,7 +1879,8 @@ static int ensure_sw_sdl_present(void) { sdl_renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, - 640 * tex_scale, 512 * tex_scale); + 640 * tex_scale, + (int)PSX_DISPLAY_PRESENT_MAX_HEIGHT * tex_scale); if (!sdl_texture) { std::fprintf(stderr, "psxrecomp: netplay SW present: SDL_CreateTexture failed: %s\n", @@ -1893,7 +1893,8 @@ static int ensure_sw_sdl_present(void) { if (!sdl_pixel_buf) { const int tex_scale = netplay_cpu_auth_gpu() ? 1 : g_video_scale; sdl_pixel_buf = (uint32_t*)std::malloc( - (size_t)640 * tex_scale * 512 * tex_scale * sizeof(uint32_t)); + (size_t)640 * tex_scale * PSX_DISPLAY_PRESENT_MAX_HEIGHT * + tex_scale * sizeof(uint32_t)); if (!sdl_pixel_buf) { std::fprintf(stderr, "psxrecomp: netplay SW present: staging alloc failed\n"); return -1; @@ -7294,7 +7295,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { * show that as a thin white strip; Wayland may not). Pad one black row * so any residual linear fringe is black, matching the letterbox. */ const int tex_scale = netplay_cpu_auth_gpu() ? 1 : g_video_scale; - const int tex_h = 512 * tex_scale; + const int tex_h = (int)PSX_DISPLAY_PRESENT_MAX_HEIGHT * tex_scale; if (src_w > 0 && src_h > 0 && src_h < tex_h) { static uint32_t s_black_pad[640 * 4]; /* covers g_video_scale <= 4 */ const int pad_cap = (int)(sizeof(s_black_pad) / sizeof(s_black_pad[0])); @@ -13706,12 +13707,13 @@ session_reboot: } } - /* Staging buffer + backing texture are sized for the internal resolution - * (640x512 native, times the supersampling factor). Netplay: 1×. */ + /* Staging buffer + backing texture preserve the 576-row interlaced PAL + * canvas, times the supersampling factor. Netplay: 1×. */ { const int tex_scale = netplay_cpu_auth_gpu() ? 1 : g_video_scale; sdl_pixel_buf = (uint32_t*)std::malloc( - (size_t)640 * tex_scale * 512 * tex_scale * sizeof(uint32_t)); + (size_t)640 * tex_scale * PSX_DISPLAY_PRESENT_MAX_HEIGHT * + tex_scale * sizeof(uint32_t)); if (!sdl_pixel_buf) { std::fprintf(stderr, "failed to allocate %dx staging buffer\n", tex_scale); return 1; @@ -13724,7 +13726,8 @@ session_reboot: sdl_renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, - 640 * tex_scale, 512 * tex_scale + 640 * tex_scale, + (int)PSX_DISPLAY_PRESENT_MAX_HEIGHT * tex_scale ); if (!sdl_texture) { std::fprintf(stderr, "SDL_CreateTexture failed: %s\n", SDL_GetError()); diff --git a/runtime/tests/test_display_scanout.c b/runtime/tests/test_display_scanout.c index 55b7bf56..8cf2b58c 100644 --- a/runtime/tests/test_display_scanout.c +++ b/runtime/tests/test_display_scanout.c @@ -17,6 +17,9 @@ static void test_present_height(void) assert(psx_display_present_height(0, 225, 288) == 225); assert(psx_display_present_height(1, 225, 288) == 288); assert(psx_display_present_height(1, 225, 0) == 225); + assert(psx_display_interlaced_rows(288, 0) == 288); + assert(psx_display_interlaced_rows(288, 1) == + PSX_DISPLAY_PRESENT_MAX_HEIGHT); PsxDisplayVerticalLayout unset = psx_display_vertical_layout(0, 100, 100); assert(!unset.range_set);