mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
8fee64f178
screenshot_hires passed the pixel width as out_pitch, but the resolves (sw_render_display / sw_render_display_hires) step rows in BYTES, as the present path does (main.cpp: sw * sizeof(uint32_t)). Each row advanced a quarter of a row, tiling the frame 4x across and leaving the bottom three quarters black. Verified on WipEout 3: content rows 65/256 -> 256/256. The same handler could also emit untouched allocation: gr_scale() reports the GL backend's internal scale, but sw_render_display_hires falls back to the native resolve when the CPU hi-res mirror does not exist, filling w*h of an ow*oh buffer while still returning non-zero. The buffer is now calloc'd and the resolve must report FULL cover or the capture redoes itself honestly at native size. Add present_shot / present_shot_seq: a capture of the COMPOSED present surface. Every existing capture resolves the display buffer BEFORE the backend fits it to the window, so on a 508x256 display in a 4:3 window they answer 508x256 while the player sees 640x480. That is correct for faithfulness work but wrong for anything aspect-shaped -- a widescreen change moves the GTE squash and the present fit, which is exactly the stage those captures skip. Fulfilled by whichever backend owns the present: SDL_RenderReadPixels on the software path, glReadPixels before SwapWindow on GL (with the bottom-up flip). Notes on the contract: - GL fulfilment can run on the frame-interpolation thread (interp_present -> gl_swap_with_osd), not just the emu thread, so the path buffer and pending flag are mutex-guarded and the counters are atomic. present_shot_take() claims the request under the lock, so two present paths cannot fulfil the same shot. - A staged shot is OVERWRITTEN by a later request rather than refused, as savestate's request_save_inner does. A present that never arrives therefore cannot wedge the command. - Vulkan presents through its own swapchain and has no readback hook, so the request is refused there instead of being accepted and never fulfilled (CLAUDE.md rule 15). - present_shot_seq advances on success AND failure so a polling client always terminates; its `wrote` field reports whether a PNG landed. - The SDL2 branch sizes its buffer from SDL_GetRendererOutputSize rather than a viewport*scale reconstruction, which could be smaller than the region SDL_RenderReadPixels fills. Also: debug_client gains positional-path screenshot_file/present_shot and turbo/turbo_state mappings (they previously fell through to the raw key=value handler); TCP_COMMANDS.md marks screenshot_hires as native (it is registered on the native server and the blank N column read as Beetle-only), documents the two new commands, and the generated index is regenerated via tools/gen_tcp_commands.py. Tested on WipEout 3 SE (PAL, OpenGL, RelWithDebInfo + PSX_DEBUG_TOOLS=ON): capture matches the on-screen window at 1280x960; seq/wrote reports 1 on success and 0 for an unwritable path; back-to-back staging is accepted. The SDL software path, the SDL2 branches and Vulkan refusal are compile-verified only.