Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

smoother timing in frame limiter #1415

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 33 additions & 40 deletions common/util/FrameLimiter.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "FrameLimiter.h"
#include "common/common_types.h"
#include <thread>
#include <stdio.h>

double FrameLimiter::round_to_nearest_60fps(double current) {
double one_frame = 1.f / 60.f;
Expand All @@ -16,34 +18,18 @@ FrameLimiter::FrameLimiter() {}

FrameLimiter::~FrameLimiter() {}

void FrameLimiter::run(double target_fps,
bool experimental_accurate_lag,
bool do_sleeps,
double engine_time) {
double target_seconds;
if (experimental_accurate_lag) {
target_seconds = round_to_nearest_60fps(engine_time);
} else {
target_seconds = 1.f / target_fps;
}
double remaining_time = target_seconds - m_timer.getSeconds();

if (do_sleeps && remaining_time > 0.001) {
std::this_thread::sleep_for(std::chrono::microseconds(int((remaining_time - 0.001) * 1e6)));
}

while (remaining_time > 0) {
remaining_time = target_seconds - m_timer.getSeconds();
}

m_timer.start();
void sleep_us(u64 us) {
std::this_thread::sleep_for(std::chrono::microseconds(us));
}

#else

#define NOMINMAX
#include <Windows.h>

void sleep_us(u64 us) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have two different implementations of sleep_us?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One is for linux and the other is for Windows.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh, yeah. I meant why not use

void sleep_us(u64 us) {
  std::this_thread::sleep_for(std::chrono::microseconds(us))
}

for both platforms?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need sleeps that are accurate to a few ms for this to work, but the default windows timer resolution is (sometimes) ~15 ms. that's almost a whole frame long, so it becomes impossible to sleep for the right amount of time. The solution is to use the windows API to request a higher timer resolution and do the sleeps manually.

The C++ standard library sleeps are kinda weird too, they have very poorly specified behavior:
"The standard recommends that a steady clock is used to measure the duration. If an implementation uses a system clock instead, the wait time may also be sensitive to clock adjustments."

and we definitely wouldn't want the frame limiter to be affected by somebody changing their system time...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I did not realise that the std sleeps were so poorly defined. I always assumed they were a steady clock, because as you said, if they're not that's hugely problematic.

Sleep(us);
}

FrameLimiter::FrameLimiter() {
timeBeginPeriod(1);
}
Expand All @@ -52,27 +38,34 @@ FrameLimiter::~FrameLimiter() {
timeEndPeriod(0);
}

void FrameLimiter::run(double target_fps,
bool experimental_accurate_lag,
bool do_sleeps,
double engine_time) {
double target_seconds;
if (experimental_accurate_lag) {
target_seconds = round_to_nearest_60fps(engine_time);
} else {
target_seconds = 1.f / target_fps;
}
double remaining_time = target_seconds - m_timer.getSeconds();
#endif
void FrameLimiter::run(double target_fps, bool do_sleeps, double engine_time) {
// we want to exit this function when the current time reaches m_us_target.
// and update m_us_target for the next frame.
constexpr s64 max_lag_us = 20000;
constexpr s64 max_ahead_us = 50000;

s64 entry_time = m_timer.getUs();

if (do_sleeps && remaining_time > 0.001) {
Sleep((remaining_time * 1000) - 1);
// first, see if we're lagging too far behind:
if (entry_time > m_us_target + max_lag_us) {
m_us_target = entry_time - max_lag_us;
}

while (remaining_time > 0) {
remaining_time = target_seconds - m_timer.getSeconds();
// see if we're too far ahead
if (m_us_target > entry_time + max_ahead_us) {
m_us_target = entry_time + max_ahead_us;
}

m_timer.start();
}
// sleep!
s64 remaining = m_us_target - entry_time;
if (do_sleeps && remaining > 1000) {
sleep_us(remaining - 1000);
}
while (m_timer.getUs() < m_us_target) {
// wait....
}

#endif
// now update the next target...
m_us_target += 1e6 * round_to_nearest_60fps(engine_time) * 60 / target_fps;
}
5 changes: 4 additions & 1 deletion common/util/FrameLimiter.h
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
#pragma once

#include "common/common_types.h"
#include "common/util/Timer.h"

class FrameLimiter {
public:
FrameLimiter();
~FrameLimiter();

void run(double target_fps, bool experimental_accurate_lag, bool do_sleeps, double engine_time);
void run(double target_fps, bool do_sleeps, double engine_time);

private:
double round_to_nearest_60fps(double current);

Timer m_timer;

s64 m_us_target = 0;
};
1 change: 0 additions & 1 deletion game/graphics/opengl_renderer/debug_gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ void OpenGlDebugGui::draw(const DmaStats& dma_stats) {
target_fps = m_target_fps_text;
}
ImGui::Separator();
ImGui::Checkbox("Accurate Lag Mode", &experimental_accurate_lag);
ImGui::Checkbox("Sleep in Frame Limiter", &sleep_in_frame_limiter);
ImGui::EndMenu();
}
Expand Down
5 changes: 2 additions & 3 deletions game/graphics/opengl_renderer/debug_gui.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,8 @@ class OpenGlDebugGui {

bool get_vsync_flag() { return m_vsync; }

bool framelimiter = false;
bool framelimiter = true;
float target_fps = 60.f;
bool experimental_accurate_lag = false;
bool sleep_in_frame_limiter = true;
bool small_profiler = false;
bool record_events = false;
Expand All @@ -75,6 +74,6 @@ class OpenGlDebugGui {
bool m_draw_debug = false;
bool m_want_screenshot = false;
char m_screenshot_save_name[256] = "screenshot.png";
bool m_vsync = true;
bool m_vsync = false;
float m_target_fps_text = 60.0;
};
15 changes: 9 additions & 6 deletions game/graphics/pipelines/opengl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ static int gl_init(GfxSettings& settings) {
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); // 4.3
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // core profile, not compat
// debug check
glfwWindowHint(GLFW_REFRESH_RATE, 60);
if (settings.debug) {
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);
} else {
Expand Down Expand Up @@ -262,7 +262,6 @@ void render_game_frame(int width, int height, int lbox_width, int lbox_height) {
// should be fine to remove this mutex if the game actually waits for vsync to call
// send_chain again. but let's be safe for now.
std::unique_lock<std::mutex> lock(g_gfx_data->dma_mutex);
g_gfx_data->engine_timer.start();
g_gfx_data->has_data_to_render = false;
g_gfx_data->sync_cv.notify_all();
}
Expand Down Expand Up @@ -436,18 +435,23 @@ static void gl_render_display(GfxDisplay* display) {
glfwSwapInterval(req_vsync);
}

g_gfx_data->last_engine_time = g_gfx_data->engine_timer.getSeconds();

// actual vsync
g_gfx_data->debug_gui.finish_frame();
{
auto p = scoped_prof("swap-buffers");
glfwSwapBuffers(window);
}

if (g_gfx_data->debug_gui.framelimiter) {
auto p = scoped_prof("frame-limiter");
g_gfx_data->frame_limiter.run(
g_gfx_data->debug_gui.target_fps, g_gfx_data->debug_gui.experimental_accurate_lag,
g_gfx_data->debug_gui.sleep_in_frame_limiter, g_gfx_data->last_engine_time);
g_gfx_data->frame_limiter.run(g_gfx_data->debug_gui.target_fps,
g_gfx_data->debug_gui.sleep_in_frame_limiter,
g_gfx_data->last_engine_time);
}
g_gfx_data->engine_timer.start();

g_gfx_data->debug_gui.start_frame();
prof().instant_event("ROOT");
update_global_profiler();
Expand Down Expand Up @@ -498,7 +502,6 @@ u32 gl_sync_path() {
return 0;
}
std::unique_lock<std::mutex> lock(g_gfx_data->sync_mutex);
g_gfx_data->last_engine_time = g_gfx_data->engine_timer.getSeconds();
if (!g_gfx_data->has_data_to_render) {
return 0;
}
Expand Down
17 changes: 14 additions & 3 deletions goal_src/engine/draw/drawable.gc
Original file line number Diff line number Diff line change
Expand Up @@ -1276,8 +1276,8 @@
(define *screen-shot* #f)
(defun display-frame-start ((disp display) (new-frame-idx int) (odd-even int))
"Set up a new frame. Call this before drawing anything.
new-frame-idx is the display frame that will be set up.
odd-even is the odd-even of the new frame"
new-frame-idx is the display frame that will be set up.
odd-even is the odd-even of the new frame"


;; due to a HW bug in the PS2, you must set this.
Expand All @@ -1292,7 +1292,7 @@
)
)

)
)
)

(let ((float-time-ratio (/ (the float (timer-count (the-as timer-bank #x10000800))) (the float *ticks-per-frame*))))
Expand All @@ -1304,18 +1304,29 @@
;; if we actually do miss a frame, the time ratio will be around 2.

(#when PC_PORT
(if (< float-time-ratio 3.3)
(set! time-ratio 3.0)
)
(if (< float-time-ratio 2.3)
(set! time-ratio 2.0)
)
(if (< float-time-ratio 1.3)
(set! time-ratio 1.0)
)


#|
(if (> time-ratio 1.)
(format #t "LAG ~f frames~%" (- time-ratio 1.))
)
|#
)
;; (format *stdcon* "time: ~f and ~f~%" time-ratio float-time-ratio)
)




;; inform display system of our speed. This will adjust the scaling used in all physics calculations
(set-time-ratios *display* time-ratio)

Expand Down