feat: FPS Limiter (#1446)

* Add interpolation frame rate cap

* wip: reworked framelimiter

Based on my testing this is a bit more stable in frametimes.

* wip: efficiency improvement + windows build fix

Significantly improve efficiency by using a hybrid approach.

* wip: UI changes

* wip: end frame AFTER limiting

* wip: remove unused include

* wip: minor ui code change

Makes it easier to remove/add presets

* Simplify Limiter UI

- Change enableFrameInterpolation to an enum with off/capped/unlimited values
- Simplify the UI to use 2 settings (unlock framerate + a max value entry)

* wip: slight limiter simplification

* wip: implement review suggestions

* wip: fix syntax error

* wip: revert enum order + replace old checks

* Fix compile error

---------

Co-authored-by: SailorSnoW <sailorsnow@pm.me>
Co-authored-by: Loïs <49660929+SailorSnoW@users.noreply.github.com>
Co-authored-by: SuperDude88 <82904174+SuperDude88@users.noreply.github.com>
Co-authored-by: Luke Street <luke@street.dev>
This commit is contained in:
Ash
2026-05-18 04:11:32 +02:00
committed by GitHub
parent 66c5cb1dae
commit 2da6590657
32 changed files with 207 additions and 59 deletions
+6 -6
View File
@@ -2410,7 +2410,7 @@ void mDoExt_3DlineMat0_c::draw() {
}
#if TARGET_PC
if (!dusk::getSettings().game.enableFrameInterpolation)
if (!dusk::frame_interp::is_enabled())
#endif
{
field_0x16 ^= (u8)1;
@@ -2740,7 +2740,7 @@ void mDoExt_3DlineMat1_c::draw() {
}
GXSetTexCoordScaleManually(GX_TEXCOORD0, 0, 0, 0);
#if TARGET_PC
if (!dusk::getSettings().game.enableFrameInterpolation)
if (!dusk::frame_interp::is_enabled())
#endif
{
mIsDrawn ^= (u8)1;
@@ -2822,7 +2822,7 @@ void mDoExt_3DlineMat1_c::update(int param_0, f32 param_1, GXColor& param_2, u16
}
#if TARGET_PC
const cXyz& lineEye = (presentationEye != nullptr && dusk::getSettings().game.enableFrameInterpolation) ? *presentationEye : sp_3c->lookat.eye;
const cXyz& lineEye = (presentationEye != nullptr && dusk::frame_interp::is_enabled()) ? *presentationEye : sp_3c->lookat.eye;
sp_13c = *local_r27 - lineEye;
#else
sp_13c = *local_r27 - sp_3c->lookat.eye;
@@ -2982,7 +2982,7 @@ void mDoExt_3DlineMat1_c::update(int param_0, GXColor& param_2, dKy_tevstr_c* pa
local_r27 = sp_38[0].field_0x0;
size_p = sp_38->field_0x4;
#if TARGET_PC
if (presentationEye != nullptr && dusk::getSettings().game.enableFrameInterpolation && size_p == NULL) {
if (presentationEye != nullptr && dusk::frame_interp::is_enabled() && size_p == NULL) {
sp_38 += 1;
continue;
}
@@ -3001,7 +3001,7 @@ void mDoExt_3DlineMat1_c::update(int param_0, GXColor& param_2, dKy_tevstr_c* pa
local_f30 = sp_130.abs();
local_f31 += local_f30 * 0.1f;
#if TARGET_PC
const cXyz& lineEye = (presentationEye != nullptr && dusk::getSettings().game.enableFrameInterpolation) ? *presentationEye : stack_3c->lookat.eye;
const cXyz& lineEye = (presentationEye != nullptr && dusk::frame_interp::is_enabled()) ? *presentationEye : stack_3c->lookat.eye;
sp_13c = local_r27[0] - lineEye;
#else
sp_13c = local_r27[0] - stack_3c->lookat.eye;
@@ -3077,7 +3077,7 @@ void mDoExt_3DlineMat1_c::update(int param_0, GXColor& param_2, dKy_tevstr_c* pa
#if TARGET_PC
void mDoExt_3DlineMat1_c::refreshGeometryForPresentationEye(const cXyz& eye) {
if (!dusk::getSettings().game.enableFrameInterpolation) {
if (!dusk::frame_interp::is_enabled()) {
return;
}
if (mInterpLineKind == 1) {
+3 -3
View File
@@ -2063,7 +2063,7 @@ static void captureScreenPerspDrawInfo(JPADrawInfo& info) {
static void drawItem3D() {
ZoneScoped;
#ifdef TARGET_PC
if (dusk::getSettings().game.enableFrameInterpolation) {
if (dusk::frame_interp::is_enabled()) {
// FRAME INTERP NOTE: Title screen needs 0.0f while everything else that runs through this is -100.0f.
if (fopAcM_SearchByName(fpcNm_TITLE_e) != nullptr) {
dMenu_Collect3D_c::setViewPortOffsetY(0.0f);
@@ -2241,7 +2241,7 @@ int mDoGph_Painter() {
#endif
dKy_setLight();
#if TARGET_PC
if (dusk::getSettings().game.enableFrameInterpolation) {
if (dusk::frame_interp::is_enabled()) {
dKy_setLight_again();
}
#endif
@@ -2296,7 +2296,7 @@ int mDoGph_Painter() {
}
#if TARGET_PC
if (dusk::getSettings().game.enableFrameInterpolation) {
if (dusk::frame_interp::is_enabled()) {
// FRAME INTERP NOTE: Currently only recalculating points for Epona's reins. Need a more global solution.
if (daHorse_c* horse = dComIfGp_getHorseActor()) {
horse->lerpControlPoints(dusk::frame_interp::get_interpolation_step());
+23 -3
View File
@@ -28,6 +28,7 @@
#include "d/d_s_logo.h"
#include "d/d_s_menu.h"
#include "d/d_s_play.h"
#include "dusk/time.h"
#include "f_ap/f_ap_game.h"
#include "f_op/f_op_msg.h"
#include "m_Do/m_Do_MemCard.h"
@@ -279,8 +280,9 @@ void main01(void) {
const auto pacing = dusk::game_clock::advance_main_loop();
if (pacing.is_interpolating) {
if (pacing.sim_ticks_to_run > 0) {
dusk::frame_interp::begin_frame(true, true, 0.0f);
dusk::frame_interp::begin_frame(dusk::getSettings().game.enableFrameInterpolation, true, 0.0f);
dusk::frame_interp::set_ui_tick_pending(true);
for (int sim_tick = 0; sim_tick < pacing.sim_ticks_to_run; ++sim_tick) {
dusk::frame_interp::begin_sim_tick();
mDoCPd_c::read();
@@ -291,7 +293,7 @@ void main01(void) {
}
}
dusk::frame_interp::begin_frame(true, false,
dusk::frame_interp::begin_frame(dusk::getSettings().game.enableFrameInterpolation, false,
dusk::game_clock::sample_interpolation_step());
dusk::frame_interp::interpolate();
dusk::frame_interp::begin_presentation_camera();
@@ -301,7 +303,7 @@ void main01(void) {
dusk::frame_interp::end_presentation_camera();
dusk::frame_interp::set_ui_tick_pending(false);
} else {
dusk::frame_interp::begin_frame(false, true, 0.0f);
dusk::frame_interp::begin_frame(dusk::FrameInterpMode::Off, true, 0.0f);
dusk::frame_interp::set_ui_tick_pending(true);
// Game Inputs
@@ -315,8 +317,26 @@ void main01(void) {
mDoAud_Execute();
}
static Limiter main_loop_limiter;
static double last_fps_setting = 0.0;
static Limiter::duration_t target_ns = 0;
if (dusk::getSettings().game.enableFrameInterpolation.getValue() == dusk::FrameInterpMode::Capped && !dusk::getTransientSettings().skipFrameRateLimit) {
double current_fps = dusk::getSettings().video.maxFrameRate.getValue();
if (current_fps != last_fps_setting) {
last_fps_setting = current_fps;
target_ns = static_cast<Limiter::duration_t>(1'000'000'000.0 / current_fps);
}
Limiter::duration_t sleepTime = main_loop_limiter.Sleep(target_ns);
dusk::frameUsagePct = 100.0f * (1.0f - static_cast<float>(sleepTime) / static_cast<float>(target_ns));
} else {
main_loop_limiter.Reset();
}
aurora_end_frame();
FrameMark;
#ifdef DUSK_DISCORD