Skip to content

Commit 0298b61

Browse files
committed
SQUASHME: More refined phase locked loop approach
1 parent 65a73dc commit 0298b61

1 file changed

Lines changed: 117 additions & 21 deletions

File tree

ui/xemu.c

Lines changed: 117 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -796,17 +796,111 @@ static void report_stats(void)
796796
}
797797
#endif
798798

799+
typedef struct VBlankPhaseLockedLoop {
800+
int64_t vblank_period_ns;
801+
int64_t expected_vblank_ns;
802+
int64_t last_vblank_ns;
803+
int64_t integral_gain_divisor;
804+
int64_t proportional_gain_divisor;
805+
bool phase_locked;
806+
bool resync_required;
807+
} VBlankPLL;
808+
static VBlankPLL vblank_pll_state = { 0 };
809+
810+
static inline int64_t pll_wrap_phase_error(int64_t error, int64_t period)
811+
{
812+
const int64_t wrapped = error % period;
813+
const int64_t half_period = period / 2;
814+
815+
if (wrapped > half_period) {
816+
return wrapped - period;
817+
}
818+
819+
if (wrapped < -half_period) {
820+
return wrapped + period;
821+
}
822+
823+
return wrapped;
824+
}
825+
799826
// Time reserved for SDL_GL_SwapWindow to perform a swap. This value is chosen
800827
// empirically as the best balance between minimizing guest stalls while still
801828
// hitting the target host refresh rate.
802-
#define VSYNC_SWAP_GRACE_PERIOD_NS 300000LL
803-
static int64_t display_vsync_interval = 1000000000LL / 60;
829+
#define VSYNC_SWAP_GRACE_PERIOD_NS 1400000LL
830+
#define MISSED_FRAME_RESYNC_COUNT 8
831+
832+
#define PLL_BASE_JITTER_NS 1500000LL
833+
834+
static inline void pll_swap_window(SDL_Window *window)
835+
{
836+
if (qatomic_xchg(&vblank_pll_state.resync_required, false)) {
837+
vblank_pll_state.last_vblank_ns = 0;
838+
vblank_pll_state.phase_locked = false;
839+
}
840+
841+
if (vblank_pll_state.phase_locked) {
842+
int64_t now = (int64_t)SDL_GetTicksNS();
843+
int64_t sleep_target_ns =
844+
vblank_pll_state.expected_vblank_ns - VSYNC_SWAP_GRACE_PERIOD_NS;
845+
int64_t delay_ns = sleep_target_ns - now;
846+
847+
if (delay_ns > 0 && delay_ns < vblank_pll_state.vblank_period_ns) {
848+
SDL_DelayPrecise((uint64_t)delay_ns);
849+
}
850+
}
851+
852+
SDL_GL_SwapWindow(window);
853+
const int64_t swap_finish = (int64_t)SDL_GetTicksNS();
854+
855+
if (!vblank_pll_state.phase_locked) {
856+
if (vblank_pll_state.last_vblank_ns != 0) {
857+
int64_t delta = pll_wrap_phase_error(
858+
swap_finish - vblank_pll_state.last_vblank_ns,
859+
vblank_pll_state.vblank_period_ns);
860+
861+
const int64_t bootstrap_tol =
862+
MIN(PLL_BASE_JITTER_NS, vblank_pll_state.vblank_period_ns / 5);
863+
if (llabs(delta) < bootstrap_tol) {
864+
vblank_pll_state.expected_vblank_ns = swap_finish;
865+
vblank_pll_state.phase_locked = true;
866+
}
867+
}
868+
vblank_pll_state.last_vblank_ns = swap_finish;
869+
return;
870+
}
871+
872+
int64_t error_ns = swap_finish - vblank_pll_state.expected_vblank_ns;
873+
if (llabs(error_ns) >
874+
vblank_pll_state.vblank_period_ns * MISSED_FRAME_RESYNC_COUNT) {
875+
vblank_pll_state.expected_vblank_ns = swap_finish;
876+
} else {
877+
int64_t phase_error_ns =
878+
pll_wrap_phase_error(error_ns, vblank_pll_state.vblank_period_ns);
879+
int64_t nearest_vblank = swap_finish - phase_error_ns;
880+
881+
const int64_t tear_tol =
882+
MIN(PLL_BASE_JITTER_NS * 2, vblank_pll_state.vblank_period_ns / 4);
883+
if (llabs(phase_error_ns) > tear_tol) {
884+
// Handle adaptive vsync tearing swaps. These can occur
885+
// significantly out of phase and should be ignored.
886+
vblank_pll_state.expected_vblank_ns =
887+
nearest_vblank + vblank_pll_state.vblank_period_ns;
888+
} else {
889+
vblank_pll_state.vblank_period_ns +=
890+
phase_error_ns / vblank_pll_state.integral_gain_divisor;
891+
vblank_pll_state.expected_vblank_ns =
892+
nearest_vblank + vblank_pll_state.vblank_period_ns +
893+
(phase_error_ns / vblank_pll_state.proportional_gain_divisor);
894+
}
895+
}
896+
}
804897

805898
enum VsyncOverrideBehavior {
806899
VOB_FORCE_OFF,
807900
VOB_FORCE_ON,
808901
VOB_NO_OVERRIDE,
809902
};
903+
810904
static enum VsyncOverrideBehavior vsync_override = VOB_NO_OVERRIDE;
811905

812906
/**
@@ -874,21 +968,12 @@ static void gl_render_frame(struct xemu_console *scon)
874968

875969
nv2a_release_framebuffer_surface();
876970

877-
static int64_t last_ui_frame_time_ns = 0;
878-
if (last_ui_frame_time_ns &&
879-
(vsync_override == VOB_FORCE_ON || (vsync_override == VOB_NO_OVERRIDE &&
880-
g_config.display.window.vsync))) {
881-
int64_t next_frame = last_ui_frame_time_ns + display_vsync_interval -
882-
VSYNC_SWAP_GRACE_PERIOD_NS;
883-
884-
int64_t now = qemu_clock_get_ns(QEMU_CLOCK_REALTIME);
885-
if (now < next_frame) {
886-
SDL_DelayPrecise(next_frame - now);
887-
}
971+
if (vsync_override == VOB_FORCE_ON ||
972+
(vsync_override == VOB_NO_OVERRIDE && g_config.display.window.vsync)) {
973+
pll_swap_window(scon->real_window);
974+
} else {
975+
SDL_GL_SwapWindow(scon->real_window);
888976
}
889-
890-
SDL_GL_SwapWindow(scon->real_window);
891-
last_ui_frame_time_ns = qemu_clock_get_ns(QEMU_CLOCK_REALTIME);
892977
assert(glGetError() == GL_NO_ERROR);
893978

894979
qatomic_set(&rendering, false);
@@ -898,24 +983,31 @@ static void gl_render_frame(struct xemu_console *scon)
898983
#endif
899984
}
900985

986+
static inline void apply_vsync_interval(const int64_t interval)
987+
{
988+
vblank_pll_state.vblank_period_ns = interval;
989+
// Gain is calculated with a target of locking within ~1.6 seconds at 60Hz.
990+
vblank_pll_state.integral_gain_divisor = 1666666666LL / interval;
991+
vblank_pll_state.proportional_gain_divisor = 166666666LL / interval;
992+
}
993+
901994
static void calculate_vsync_interval_ns(void)
902995
{
903996
SDL_DisplayID display_idx = SDL_GetDisplayForWindow(m_window);
904997
const SDL_DisplayMode *mode = SDL_GetCurrentDisplayMode(display_idx);
905998
if (mode && mode->refresh_rate_numerator > 0 &&
906999
mode->refresh_rate_denominator > 0) {
907-
display_vsync_interval =
908-
(1000000000LL * mode->refresh_rate_denominator) /
909-
mode->refresh_rate_numerator;
1000+
apply_vsync_interval((1000000000LL * mode->refresh_rate_denominator) /
1001+
mode->refresh_rate_numerator);
9101002
return;
9111003
}
9121004

9131005
if (mode && mode->refresh_rate > 0) {
914-
display_vsync_interval = (int64_t)(1000000000.0 / mode->refresh_rate);
1006+
apply_vsync_interval((int64_t)(1000000000.0 / mode->refresh_rate));
9151007
return;
9161008
}
9171009

918-
display_vsync_interval = 1000000000LL / 60;
1010+
apply_vsync_interval(1000000000LL / 60);
9191011
}
9201012

9211013
static bool event_watch_callback(void *userdata, SDL_Event *event)
@@ -928,6 +1020,10 @@ static bool event_watch_callback(void *userdata, SDL_Event *event)
9281020
} else if (event->type == SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED ||
9291021
event->type == SDL_EVENT_WINDOW_DISPLAY_CHANGED) {
9301022
calculate_vsync_interval_ns();
1023+
} else if (event->type == SDL_EVENT_WINDOW_RESTORED ||
1024+
event->type == SDL_EVENT_WINDOW_MAXIMIZED ||
1025+
event->type == SDL_EVENT_WINDOW_FOCUS_GAINED) {
1026+
qatomic_set(&vblank_pll_state.resync_required, true);
9311027
}
9321028

9331029
return true; // Ignored

0 commit comments

Comments
 (0)