mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
2b2aebd8cb
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.
(cherry picked from commit 8fee64f178)