Skip to content

Libretro - D3D11/GL Delay Frame Swap Fix#2280

Draft
Immersion95 wants to merge 1 commit intoflyinghead:masterfrom
Immersion95:master-hope
Draft

Libretro - D3D11/GL Delay Frame Swap Fix#2280
Immersion95 wants to merge 1 commit intoflyinghead:masterfrom
Immersion95:master-hope

Conversation

@Immersion95
Copy link
Contributor

@Immersion95 Immersion95 commented Mar 24, 2026

Fixes #1615

I used AI to come up with a solution to a problem that had been bothering me a lot :p. I’m not expecting this to be merged, I just hope it helps you understand the issue I was running into and gives you some inspiration or direction for fixing it. Frankly I'm not able to help much more than than, but I really want this issue to be solved :).

Delay Frame Swap is indeed broken on Libretro - D3D11/GL - non-threaded rendering. I don't use threaded rendering as it adds 1 frame of input lag at the moment libretro/flycast#738.

Summary

This is a draft PR fixing DelayFrameSwapping in non-threaded Libretro mode for GL and DX11 backends. It was investigated and developed with the assistance of Claude (Anthropic).

Background: what DelayFrameSwapping does

On real Dreamcast hardware, a rendered frame is only displayed at the next vblank interrupt. DelayFrameSwapping emulates this behaviour by delaying the frame presentation by one vblank, adding exactly 1 frame of latency compared to having the option disabled.

This can be verified by counting frames in a capture:

Configuration Frames visible
DelayFrameSwapping OFF N
DelayFrameSwapping ON N + 1 (one frame of latency added)

This is the expected, hardware-accurate behaviour.

The problem

In non-threaded Libretro mode (GL and DX11), DelayFrameSwapping does not work correctly for all games. For some games such as Capcom vs SNK 2, it works as expected, enabling it correctly adds +1 frame of latency compared to having it disabled, matching real hardware behaviour.

However, for other games, enabling or disabling DelayFrameSwapping produces identical output, the intended +1 frame of hardware-accurate latency is never applied. The option is effectively broken for those games in this configuration.

SFIII Double Impact shows visible frame drops and stutter. Street Fighter Zero 3 has the image alternating between top and bottom of the screen every frame. Street Fighter 3rd Strike has no visible artifact, but DelayFrameSwapping has no effect, frame count is identical with it on or off.

Vulkan is not affected, it works correctly for all games and produces the expected +1 frame latency when DelayFrameSwapping is enabled.

Root cause

The fix for DelayFrameSwapping relies on rend_swap_frame() detecting when the game flips its display buffer:

void rend_swap_frame(u32 fb_r_sof)
{
    if (fb_r_sof == fb_w_cur ...)
        pvrQueue.enqueue(Present);
}

This comparison fails due to a race condition: the game writes FB_W_SOF1 (next write buffer address, updates fb_w_cur) before writing FB_R_SOF1 (flip request). By the time rend_swap_frame() runs, fb_w_cur has already moved to the next frame's buffer address, the check fails and Present() is never called.

In threaded mode, this is not fatal: rend_single_frame() loops continuously until presented = true, so a missed Present is retried automatically next cycle.

In non-threaded mode, there is no retry loop. One call to retro_run() must produce exactly one frame. If Present() is never called, retro_rend_present() is never called, is_dupe stays true, and RetroArch receives video_cb(NULL) and duplicates the previous frame.

Vulkan is unaffected because it uses a fundamentally different presentation mechanism (set_image()), RetroArch retrieves the image directly rather than waiting for a signal from Flycast.

The fix

Two new boolean flags are introduced in Renderer_if.cpp.

swap_frame_called is set to true when rend_swap_frame() is called, meaning the game has already flipped FB_R_SOF1 before the vblank. It is reset to false at each vblank.

presented already existed but was only reset in rend_single_frame() (threaded mode). It is now also reset at the start of each rend_start_render() in non-threaded Libretro mode, so the vblank fallback can reliably detect whether Present already succeeded this cycle.

At the vblank (rend_vblank()), a fallback Present is fired if all of the following conditions are met:

else if (config::DelayFrameSwapping
        && !config::ThreadedRendering
        && render_called
        && swap_frame_called
        && !presented
        && rend_is_enabled())
{
    pvrQueue.enqueue(PvrMessageQueue::Present);
}

The swap_frame_called guard is critical: it prevents the fallback from firing on games like Capcom vs SNK 2, which flip FB_R_SOF1 after the vblank scanline. Without this guard, the SH4 would be stopped too early for those games, causing micro-stutters that did not exist before the fix.

renderer->Present() returns false if frameRendered is already false (e.g. RTT frames, or Present already triggered via rend_swap_frame()), so there is no risk of double presentation.

What is not affected

Vulkan is unchanged and already works correctly. Threaded rendering is unchanged, protected by #ifdef LIBRETRO and !config::ThreadedRendering guards. Standalone is unchanged, protected by #ifdef LIBRETRO. Games that already work correctly such as CvS2 are unaffected, the !presented guard ensures the fallback never fires when the normal path already succeeded.

Files changed

core/hw/pvr/Renderer_if.cpp - only file modified.

Tested

SFIII Double Impact: stutter resolved ✓
Street Fighter Zero 3: vertical alternation resolved ✓
Street Fighter 3rd Strike: DelayFrameSwapping now correctly adds +1 frame ✓
Capcom vs SNK 2: no regression, same smoothness as before ✓
Vulkan: unaffected ✓
Threaded rendering: unaffected ✓

Investigated and developed with the assistance of Claude (Anthropic).

@Immersion95 Immersion95 deleted the master-hope branch March 24, 2026 16:57
@Immersion95 Immersion95 restored the master-hope branch March 24, 2026 16:58
@Immersion95 Immersion95 reopened this Mar 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Win/Libretro] "Delay Frame Swapping ON" + "Threaded rendering OFF" make some games stutter heavily in DX11/GL

1 participant