Skip to content

Commit 172d7bc

Browse files
committed
Preserve cached core frame across CMD_EVENT_REINIT (frontend + Metal)
command_event_reinit tears the video driver down via video_driver_free, which nulls video_st->frame_cache_data (the pointer was borrowed from the core's own framebuffer and isn't guaranteed to stay live across the driver swap). The new driver then comes up with no cached frame to replay, so reinits triggered from inside the menu (e.g. toggling video_hdr_mode between HDR10 and scRGB on Metal) leave the paused-core background missing until the user F1-cycles back through the core. Snapshot the cached frame into a static buffer before reinit, restore the pointer into video_st->frame_cache_data afterwards, and call video_driver_cached_frame() to push it through the new driver's frame callback. The static buffer is realloc'd on demand and reused across reinits so there's no leak — the core's next real frame will simply overwrite the frame_cache_data pointer at its leisure. In the Metal driver, additionally latch a _frameEverUploaded flag so that on frame==NULL calls before the first real frame lands (e.g. in the one-or-two-frame window before the cached replay completes), we can also fall back to reading video_st->frame_cache_data directly from inside renderFrame. The two paths overlap cleanly: the frontend snapshot keeps the cache pointer valid across reinit, and the driver-side latch guards the transient. Fixes paused-core background disappearing after HDR mode toggle on Metal; harmless elsewhere because other drivers either don't rebuild their internal frame texture on reinit (keeping content across the swap by accident) or already have equivalent workarounds. Files command.c : snapshot-and-replay around video_driver_reinit. gfx/drivers/metal.m : _frameEverUploaded latch + cached-frame fallback in renderFrame.
1 parent 42f3a3c commit 172d7bc

2 files changed

Lines changed: 100 additions & 1 deletion

File tree

command.c

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2440,8 +2440,70 @@ void command_event_reinit(const int flags)
24402440
const input_device_driver_t
24412441
*sec_joypad = NULL;
24422442
#endif
2443+
/* Snapshot the last cached core frame before tearing the video
2444+
* driver down. video_driver_free() nulls frame_cache_data as part
2445+
* of the reinit cycle (the pointer was borrowed from the core's
2446+
* own framebuffer and isn't guaranteed to stay live across the
2447+
* driver swap), so without a snapshot the new driver would come
2448+
* up with no core image to replay. Restored + replayed below so
2449+
* the paused-core background remains visible when reinit is
2450+
* triggered from inside the menu (e.g. HDR mode toggle).
2451+
*
2452+
* The snapshot buffer is static and reused across reinits — we
2453+
* need it to outlive command_event_reinit because we hand the
2454+
* pointer to video_st->frame_cache_data for the new driver to
2455+
* read via video_driver_cached_frame(), and the core's next real
2456+
* frame will replace the pointer at its leisure. Resizing in
2457+
* place on each call keeps it bounded at one buffer's worth. */
2458+
static void *cached_snapshot = NULL;
2459+
static size_t cached_snapshot_cap = 0;
2460+
size_t want_size = 0;
2461+
unsigned cached_snapshot_w = 0;
2462+
unsigned cached_snapshot_h = 0;
2463+
size_t cached_snapshot_p = 0;
2464+
2465+
if ( video_st
2466+
&& video_st->frame_cache_data
2467+
&& video_st->frame_cache_data != RETRO_HW_FRAME_BUFFER_VALID
2468+
&& video_st->frame_cache_height
2469+
&& video_st->frame_cache_pitch)
2470+
want_size = video_st->frame_cache_pitch
2471+
* video_st->frame_cache_height;
2472+
if (want_size > cached_snapshot_cap)
2473+
{
2474+
void *tmp = realloc(cached_snapshot, want_size);
2475+
if (tmp)
2476+
{
2477+
cached_snapshot = tmp;
2478+
cached_snapshot_cap = want_size;
2479+
}
2480+
else
2481+
want_size = 0;
2482+
}
2483+
if (want_size && cached_snapshot)
2484+
{
2485+
memcpy(cached_snapshot, video_st->frame_cache_data, want_size);
2486+
cached_snapshot_w = video_st->frame_cache_width;
2487+
cached_snapshot_h = video_st->frame_cache_height;
2488+
cached_snapshot_p = video_st->frame_cache_pitch;
2489+
}
24432490

24442491
video_driver_reinit(flags);
2492+
2493+
/* Restore the snapshot and ask the new driver to replay it so the
2494+
* paused-core background appears in the first post-reinit frame.
2495+
* The buffer stays live across subsequent frame_cb calls from the
2496+
* core (which overwrite the pointer) and is either reused on the
2497+
* next reinit or freed at shutdown. */
2498+
if (cached_snapshot_p && cached_snapshot_h)
2499+
{
2500+
video_st->frame_cache_data = cached_snapshot;
2501+
video_st->frame_cache_width = cached_snapshot_w;
2502+
video_st->frame_cache_height = cached_snapshot_h;
2503+
video_st->frame_cache_pitch = cached_snapshot_p;
2504+
video_driver_cached_frame();
2505+
}
2506+
24452507
/* Poll input to avoid possibly stale data to corrupt things. */
24462508
if (joypad && joypad->poll)
24472509
joypad->poll();

gfx/drivers/metal.m

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3235,6 +3235,18 @@ @implementation MetalDriver
32353235
* Vulkan's behaviour: HDR is effectively an init-time choice that
32363236
* takes effect on the next driver reinit. */
32373237
unsigned _initial_hdr_mode;
3238+
3239+
/* Tracks whether _frameView has received any real frame since this
3240+
* driver instance came up. When the HDR mode is toggled mid-session
3241+
* the frontend fires CMD_EVENT_REINIT, destroying and recreating the
3242+
* driver. If the user was in the menu with a core paused in the
3243+
* background, the new driver's frame texture is empty and subsequent
3244+
* menu-loop frames arrive with frame==NULL, leaving hdrComposite
3245+
* nothing to sample — the paused core background disappears until
3246+
* the user F1-cycles. On the first frame==NULL after reinit we fall
3247+
* back to the frontend's frame_cache_data to repopulate the texture
3248+
* ourselves. */
3249+
bool _frameEverUploaded;
32383250
}
32393251

32403252
- (instancetype)initWithVideo:(const video_info_t *)video
@@ -3540,8 +3552,33 @@ - (bool)renderFrame:(const void *)frame
35403552
_frameView.frameCount = frameCount;
35413553
if (frame && width && height)
35423554
{
3543-
_frameView.size = CGSizeMake(width, height);
3555+
_frameView.size = CGSizeMake(width, height);
35443556
[_frameView updateFrame:frame pitch:pitch];
3557+
_frameEverUploaded = true;
3558+
}
3559+
else if (!_frameEverUploaded)
3560+
{
3561+
/* Driver just came up (init or post-reinit). The frontend
3562+
* hasn't delivered a real frame yet and — on CMD_EVENT_REINIT
3563+
* from inside the menu — likely won't until the user leaves
3564+
* the menu. Pull the cached frame so the core image reappears
3565+
* under the menu immediately instead of waiting for an F1
3566+
* cycle. Safe to do every frame while the flag is clear; we
3567+
* latch it after the first successful upload. */
3568+
video_driver_state_t *video_st = video_state_get_ptr();
3569+
if ( video_st
3570+
&& video_st->frame_cache_data
3571+
&& video_st->frame_cache_data != RETRO_HW_FRAME_BUFFER_VALID
3572+
&& video_st->frame_cache_width
3573+
&& video_st->frame_cache_height
3574+
&& video_st->frame_cache_pitch)
3575+
{
3576+
_frameView.size = CGSizeMake(video_st->frame_cache_width,
3577+
video_st->frame_cache_height);
3578+
[_frameView updateFrame:video_st->frame_cache_data
3579+
pitch:video_st->frame_cache_pitch];
3580+
_frameEverUploaded = true;
3581+
}
35453582
}
35463583

35473584
/* Acquire the frame encoder. In SDR mode this lazily opens a pass

0 commit comments

Comments
 (0)