diff --git a/include/dusk/frame_interpolation.h b/include/dusk/frame_interpolation.h index 0fa8c208bd..5f2f3d134b 100644 --- a/include/dusk/frame_interpolation.h +++ b/include/dusk/frame_interpolation.h @@ -4,6 +4,7 @@ #include #include #include +#include "settings.h" class camera_process_class; class view_class; @@ -18,7 +19,7 @@ void begin_record(); void end_record(); void begin_sim_tick(); uint64_t sim_tick_seq(); -void begin_frame(bool enabled, bool is_sim_frame, float step); +void begin_frame(FrameInterpMode mode, bool is_sim_frame, float step); void interpolate(); float get_interpolation_step(); diff --git a/include/dusk/settings.h b/include/dusk/settings.h index a6511f3ea6..f89aeeb875 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -34,6 +34,12 @@ enum class GyroMode : u8 { Mouse = 1, }; +enum class FrameInterpMode : u8 { + Off = 0, + Capped = 1, + Unlimited = 2, +}; + namespace config { template <> struct ConfigEnumRange { @@ -58,7 +64,13 @@ struct ConfigEnumRange { static constexpr auto min = GyroMode::Sensor; static constexpr auto max = GyroMode::Mouse; }; -} + +template <> +struct ConfigEnumRange { + static constexpr auto min = FrameInterpMode::Off; + static constexpr auto max = FrameInterpMode::Unlimited; +}; +} // namespace config // Persistent user settings @@ -72,6 +84,7 @@ struct UserSettings { ConfigVar lockAspectRatio; ConfigVar enableFpsOverlay; ConfigVar fpsOverlayCorner; + ConfigVar maxFrameRate; } video; struct { @@ -124,7 +137,7 @@ struct UserSettings { ConfigVar bloomMultiplier; ConfigVar disableWaterRefraction; ConfigVar enableTextureReplacements; - ConfigVar enableFrameInterpolation; + ConfigVar enableFrameInterpolation; ConfigVar internalResolutionScale; ConfigVar shadowResolutionMultiplier; ConfigVar enableDepthOfField; diff --git a/include/dusk/time.h b/include/dusk/time.h index c43437f639..48dbe4c9ed 100644 --- a/include/dusk/time.h +++ b/include/dusk/time.h @@ -17,16 +17,21 @@ #include #include #endif +#ifdef __APPLE__ +#include +#endif class Limiter { public: using duration_t = Uint64; - void Reset() { m_oldTime = SDL_GetTicksNS(); } + void Reset() { + m_oldTime = SDL_GetTicksNS(); + } - void Sleep(duration_t targetFrameTime) { + duration_t Sleep(duration_t targetFrameTime) { if (targetFrameTime == 0) { - return; + return 0; } const Uint64 start = SDL_GetTicksNS(); @@ -41,6 +46,8 @@ public: } } Reset(); + + return adjustedSleepTime; } duration_t SleepTime(duration_t targetFrameTime) { @@ -74,7 +81,6 @@ private: if (!initialized || numSleeps++ % 1000 == 0) { LARGE_INTEGER freq; if (QueryPerformanceFrequency(&freq) == 0) { - DuskLog.warn("QueryPerformanceFrequency failed: {}", GetLastError()); return; } countPerNs = static_cast(freq.QuadPart) / 1e9; @@ -98,6 +104,33 @@ private: #endif } while (current.QuadPart - start.QuadPart < ticksToWait); } +#elif defined (__APPLE__) + void NanoSleep(const duration_t duration) { + // Hybrid approach using Apple Mach + uint64_t start_mach = mach_absolute_time(); + + mach_timebase_info_data_t timebase_info; + mach_timebase_info(&timebase_info); + + uint64_t total_mach_ticks = (duration * timebase_info.denom) / timebase_info.numer; + uint64_t target_mach = start_mach + total_mach_ticks; + + uint64_t buffer_ns = 2'000'000ULL; + uint64_t buffer_mach_ticks = (buffer_ns * timebase_info.denom) / timebase_info.numer; + + if (total_mach_ticks > buffer_mach_ticks) { + uint64_t sleep_until_mach = target_mach - buffer_mach_ticks; + mach_wait_until(sleep_until_mach); + } + + while (mach_absolute_time() < target_mach) { +#if defined(__aarch64__) || defined(__arm__) + asm volatile("yield" ::: "memory"); // Hardware hint, not a scheduler hint. +#else + _mm_pause(); +#endif + } + } #else void NanoSleep(const duration_t duration) { SDL_DelayPrecise(duration); } #endif diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index b8054e2130..2d4c9c7126 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -370,28 +370,28 @@ constexpr auto FRAME_PERIOD = std::chrono::duration_cast(sleepTime) / static_cast(targetNs)); - limiter.Sleep(targetNs); } #endif static void waitForTick(u32 p1, u16 p2) { #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation && !dusk::getTransientSettings().skipFrameRateLimit) { - dusk::frameUsagePct = 0.f; - return; + static Limiter limiter; + + if (dusk::frame_interp::is_enabled() && !dusk::getTransientSettings().skipFrameRateLimit) { + dusk::frameUsagePct = 0.f; + return; } + if (dusk::getTransientSettings().skipFrameRateLimit) { p1 = OS_TIMER_CLOCK / 120; } - #if TARGET_PC if (fopOvlpM_IsPeek() && dusk::getTransientSettings().stateShareLoadActive) { return; } - #endif ZoneScopedC(tracy::Color::DimGray); #endif diff --git a/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp b/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp index ee99429ee2..864dec39fc 100644 --- a/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp +++ b/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp @@ -655,7 +655,7 @@ value_or_fun: value: #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation && u <= 5 && + if (dusk::frame_interp::is_enabled() && u <= 5 && (operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12)) { dusk::frame_interp::request_presentation_sync(); @@ -666,7 +666,7 @@ value: value_n: #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation && + if (dusk::frame_interp::is_enabled() && (pN == TAdaptor_camera::sauVariableValue_3_POSITION_XYZ || pN == TAdaptor_camera::sauVariableValue_3_TARGET_POSITION_XYZ) && (operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12)) { diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index 8c82b5dc6c..e2fe7a890c 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -5990,7 +5990,7 @@ void daAlink_c::setItemMatrix(int param_0) { mDoMtx_stack_c::XrotS(-0x8000); #ifdef TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { Mtx boot_mtx; mDoMtx_concat(mpLinkModel->getAnmMtx(0x18), mDoMtx_stack_c::get(), boot_mtx); mpLinkBootModels[1]->setAnmMtx(1, boot_mtx); @@ -19767,7 +19767,7 @@ int daAlink_c::draw() { dComIfGd_getOpaListDark()->entryImm(mpHookChain, 0); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation && + if (dusk::frame_interp::is_enabled() && mEquipItem == dItemNo_IRONBALL_e && mIronBallChainPos != NULL && mIronBallChainAngle != NULL) { diff --git a/src/d/actor/d_a_b_gnd.cpp b/src/d/actor/d_a_b_gnd.cpp index f5b03be1b1..d96e6314af 100644 --- a/src/d/actor/d_a_b_gnd.cpp +++ b/src/d/actor/d_a_b_gnd.cpp @@ -397,7 +397,7 @@ static int daB_GND_Draw(b_gnd_class* i_this) { i_this->field_0x21e8.update(2, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->field_0x21e8); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mReinsInterpCurrValid) { memcpy(i_this->mReinsInterpPrev, i_this->mReinsInterpCurr, sizeof(i_this->mReinsInterpCurr)); memcpy(i_this->mReinsTexInterpPrev, i_this->mReinsTexInterpCurr, sizeof(i_this->mReinsTexInterpCurr)); diff --git a/src/d/actor/d_a_e_db.cpp b/src/d/actor/d_a_e_db.cpp index 4d60fbd6f6..6a4ed52aba 100644 --- a/src/d/actor/d_a_e_db.cpp +++ b/src/d/actor/d_a_e_db.cpp @@ -116,7 +116,7 @@ static int daE_DB_Draw(e_db_class* i_this) { i_this->stalkLine.update(12, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->stalkLine); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mStalkLineInterpCurrValid) { memcpy(i_this->mStalkLineInterpPrev, i_this->mStalkLineInterpCurr, sizeof(i_this->mStalkLineInterpCurr)); i_this->mStalkLineInterpPrevValid = true; diff --git a/src/d/actor/d_a_e_hb.cpp b/src/d/actor/d_a_e_hb.cpp index 8ed9058c6f..2b13b8a153 100644 --- a/src/d/actor/d_a_e_hb.cpp +++ b/src/d/actor/d_a_e_hb.cpp @@ -103,7 +103,7 @@ static int daE_HB_Draw(e_hb_class* i_this) { i_this->stalkLine.update(12, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->stalkLine); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mStalkLineInterpCurrValid) { memcpy(i_this->mStalkLineInterpPrev, i_this->mStalkLineInterpCurr, sizeof(i_this->mStalkLineInterpCurr)); i_this->mStalkLineInterpPrevValid = true; diff --git a/src/d/actor/d_a_e_mb.cpp b/src/d/actor/d_a_e_mb.cpp index aa77a99612..31352f92cf 100644 --- a/src/d/actor/d_a_e_mb.cpp +++ b/src/d/actor/d_a_e_mb.cpp @@ -105,7 +105,7 @@ static int daE_MB_Draw(e_mb_class* i_this) { i_this->mRopeMat.update(16, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->mRopeMat); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mRopeInterpCurrValid) { memcpy(i_this->mRopeInterpPrev, i_this->mRopeInterpCurr, sizeof(i_this->mRopeInterpCurr)); i_this->mRopeInterpPrevValid = true; diff --git a/src/d/actor/d_a_e_s1.cpp b/src/d/actor/d_a_e_s1.cpp index 31973fc317..c4455f02d1 100644 --- a/src/d/actor/d_a_e_s1.cpp +++ b/src/d/actor/d_a_e_s1.cpp @@ -161,7 +161,7 @@ static int daE_S1_Draw(e_s1_class* i_this) { dComIfGd_set3DlineMatDark(&i_this->mLineMat); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mHairInterpCurrValid) { memcpy(i_this->mHairInterpPrev, i_this->mHairInterpCurr, sizeof(i_this->mHairInterpCurr)); i_this->mHairInterpPrevValid = true; diff --git a/src/d/actor/d_a_e_wb.cpp b/src/d/actor/d_a_e_wb.cpp index 47004737cb..a353ea3e87 100644 --- a/src/d/actor/d_a_e_wb.cpp +++ b/src/d/actor/d_a_e_wb.cpp @@ -535,7 +535,7 @@ static int daE_WB_Draw(e_wb_class* i_this) { i_this->himo_tex.update(2, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->himo_tex); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->himo_interp_curr_valid) { memcpy(i_this->himo_mat_interp_prev, i_this->himo_mat_interp_curr, sizeof(i_this->himo_mat_interp_curr)); memcpy(i_this->himo_tex_interp_prev, i_this->himo_tex_interp_curr, sizeof(i_this->himo_tex_interp_curr)); diff --git a/src/d/actor/d_a_e_yd.cpp b/src/d/actor/d_a_e_yd.cpp index 18809e05ea..a10f47240c 100644 --- a/src/d/actor/d_a_e_yd.cpp +++ b/src/d/actor/d_a_e_yd.cpp @@ -107,7 +107,7 @@ static s32 daE_YD_Draw(e_yd_class* i_this) { i_this->mLineMat.update(12, l_color, &i_this->actor.tevStr); dComIfGd_set3DlineMat(&i_this->mLineMat); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mLineMatInterpCurrValid) { memcpy(i_this->mLineMatInterpPrev, i_this->mLineMatInterpCurr, sizeof(i_this->mLineMatInterpCurr)); i_this->mLineMatInterpPrevValid = true; diff --git a/src/d/actor/d_a_e_yg.cpp b/src/d/actor/d_a_e_yg.cpp index 7fa35bdf08..9eae74b1af 100644 --- a/src/d/actor/d_a_e_yg.cpp +++ b/src/d/actor/d_a_e_yg.cpp @@ -191,7 +191,7 @@ static int daE_YG_Draw(e_yg_class* i_this) { dComIfGd_set3DlineMatDark(&i_this->mLineMat); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mTentacleInterpCurrValid) { memcpy(i_this->mTentacleInterpPrev, i_this->mTentacleInterpCurr, sizeof(i_this->mTentacleInterpCurr)); i_this->mTentacleInterpPrevValid = true; diff --git a/src/d/actor/d_a_e_yh.cpp b/src/d/actor/d_a_e_yh.cpp index cf9955a7db..bf71e18818 100644 --- a/src/d/actor/d_a_e_yh.cpp +++ b/src/d/actor/d_a_e_yh.cpp @@ -135,7 +135,7 @@ static int daE_YH_Draw(e_yh_class* i_this) { i_this->mLine.update(12, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->mLine); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mLineInterpCurrValid) { memcpy(i_this->mLineInterpPrev, i_this->mLineInterpCurr, sizeof(i_this->mLineInterpCurr)); i_this->mLineInterpPrevValid = true; diff --git a/src/d/actor/d_a_horse.cpp b/src/d/actor/d_a_horse.cpp index 16790d42ff..42f2c9b8ce 100644 --- a/src/d/actor/d_a_horse.cpp +++ b/src/d/actor/d_a_horse.cpp @@ -3165,7 +3165,7 @@ void daHorse_c::setReinPosNormalSubstance() { #if TARGET_PC void daHorse_c::lerpControlPoints(f32 alpha) { // FRAME INTERP NOTE: Currently only lerping points for Epona's reins. Need a more global solution. - if (!dusk::getSettings().game.enableFrameInterpolation || !s_horseReinSimPrevValid || !s_horseReinSimCurrValid) { + if (!dusk::frame_interp::is_enabled() || !s_horseReinSimPrevValid || !s_horseReinSimCurrValid) { return; } const int nCurr = s_horseReinSimNumCurr; diff --git a/src/d/actor/d_a_obj_fchain.cpp b/src/d/actor/d_a_obj_fchain.cpp index 024b4ecf8b..acd3610b6e 100644 --- a/src/d/actor/d_a_obj_fchain.cpp +++ b/src/d/actor/d_a_obj_fchain.cpp @@ -325,7 +325,7 @@ int daObjFchain_c::draw() { dComIfGd_getOpaListDark()->entryImm(&mShape, 0); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (mChainInterpCurrValid) { memcpy(mChainInterpPrev, mChainInterpCurr, sizeof(mChainInterpCurr)); mChainInterpPrevValid = true; diff --git a/src/d/actor/d_a_obj_klift00.cpp b/src/d/actor/d_a_obj_klift00.cpp index e44cce7467..c7cede3b0e 100644 --- a/src/d/actor/d_a_obj_klift00.cpp +++ b/src/d/actor/d_a_obj_klift00.cpp @@ -493,7 +493,7 @@ int daObjKLift00_c::Draw() { dComIfGd_setList(); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (mChainInterpCurrValid) { memcpy(mChainInterpPrev, mChainInterpCurr, mNumChains * sizeof(cXyz)); mChainInterpPrevValid = true; diff --git a/src/d/actor/d_a_title.cpp b/src/d/actor/d_a_title.cpp index a75a96cb2e..5c9e503ea3 100644 --- a/src/d/actor/d_a_title.cpp +++ b/src/d/actor/d_a_title.cpp @@ -170,7 +170,7 @@ int daTitle_c::Execute() { } #ifdef TARGET_PC - if (!dusk::getSettings().game.enableFrameInterpolation) { + if (!dusk::frame_interp::is_enabled()) { #endif dMenu_Collect3D_c::setViewPortOffsetY(0.0f); #ifdef TARGET_PC @@ -354,7 +354,7 @@ void daTitle_c::fastLogoDispInit() { mProcID = 5; #ifdef TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { dusk::frame_interp::request_presentation_sync(); } #endif diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index bfc4c809c9..0de75747f2 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -10431,7 +10431,7 @@ bool dCamera_c::eventCamera(s32 param_0) { #endif #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { switch (var_r29) { case 3: case 4: @@ -11322,7 +11322,7 @@ static int camera_execute(camera_process_class* i_this) { #ifdef TARGET_PC widezoom_correction(i_this, i_this->mCamera.TrimHeight()); - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { dusk::frame_interp::add_interpolation_callback([](bool _, void* pUserWork) { const auto i_this = static_cast(pUserWork); const auto camera = &i_this->mCamera; diff --git a/src/d/d_menu_dmap.cpp b/src/d/d_menu_dmap.cpp index b39c9bb287..51cb11cdd1 100644 --- a/src/d/d_menu_dmap.cpp +++ b/src/d/d_menu_dmap.cpp @@ -991,7 +991,7 @@ void dMenu_DmapBg_c::draw() { -35.0f + (local_224.x - local_218.x), -35.0f + (local_224.y - local_218.y)); #if TARGET_PC - if (!dusk::getSettings().game.enableFrameInterpolation) { + if (!dusk::frame_interp::is_enabled()) { field_0xdda = 0; } #else @@ -2624,7 +2624,7 @@ void dMenu_Dmap_c::zoomIn_proc() { void dMenu_Dmap_c::zoomOut_init_proc() { #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { mpDrawBg->resetScrollArrowMask(); } #endif diff --git a/src/d/d_menu_fmap.cpp b/src/d/d_menu_fmap.cpp index d2b06e962c..d463b2328d 100644 --- a/src/d/d_menu_fmap.cpp +++ b/src/d/d_menu_fmap.cpp @@ -1146,7 +1146,7 @@ void dMenu_Fmap_c::zoom_spot_to_region_init() { field_0x1ec = 1.0f; #if TARGET_PC // Frame interp note: field_0x122d used to be set every draw, causing flickering. Do it here instead. - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { mpDraw2DBack->resetScrollArrowMask(); } #endif diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index d62f7d1037..a63f6ec282 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -437,7 +437,7 @@ void dMenu_Fmap2DBack_c::draw() { if (field_0x122d) { mpMeterHaihai->drawHaihai(field_0x122d); #if TARGET_PC - if (!dusk::getSettings().game.enableFrameInterpolation) { + if (!dusk::frame_interp::is_enabled()) { field_0x122d = 0; } #else diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index f0a4b986bd..3d15f1fded 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -56,6 +56,23 @@ static T sanitizeEnumValue(const ConfigVar& cVar, T value) { template void ConfigImpl::loadFromJson(ConfigVar& cVar, const json& jsonValue) { + if constexpr (std::is_enum_v) { + if (jsonValue.is_boolean()) { + using Underlying = std::underlying_type_t; + const bool b = jsonValue.get(); + + Underlying raw; + if constexpr (std::is_same_v) { + raw = b ? static_cast(2) : static_cast(0); + } else { + raw = b ? static_cast(1) : static_cast(0); + } + + cVar.setValue(sanitizeEnumValue(cVar, static_cast(raw)), false); + return; + } + } + cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get()), false); } @@ -158,6 +175,7 @@ namespace dusk::config { template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; + template class ConfigImpl; } void dusk::config::Register(ConfigVarBase& configVar) { diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index 06cc70c330..b2ef0309e3 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -142,8 +142,8 @@ uint64_t sim_tick_seq() { return g_sim_tick_seq; } -void begin_frame(bool enabled, bool is_sim_frame, float step) { - g_enabled = enabled; +void begin_frame(FrameInterpMode mode, bool is_sim_frame, float step) { + g_enabled = mode != FrameInterpMode::Off; g_is_sim_frame = is_sim_frame; g_step = std::clamp(step, 0.0f, 1.0f); } diff --git a/src/dusk/game_clock.cpp b/src/dusk/game_clock.cpp index 8b887f610c..1dc7fbf322 100644 --- a/src/dusk/game_clock.cpp +++ b/src/dusk/game_clock.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace dusk::game_clock { @@ -45,7 +46,8 @@ MainLoopPacer advance_main_loop() { MainLoopPacer out{}; out.presentation_dt_seconds = presentation_dt; - const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation && + const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation.getValue() != + dusk::FrameInterpMode::Off && !dusk::getTransientSettings().skipFrameRateLimit; out.is_interpolating = should_interpolate; out.sim_pace = sim_pace(); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 07bc5574e0..e3eae37aad 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -10,6 +10,7 @@ UserSettings g_userSettings = { .lockAspectRatio {"video.lockAspectRatio", false}, .enableFpsOverlay {"game.enableFpsOverlay", false}, .fpsOverlayCorner {"game.fpsOverlayCorner", 0}, + .maxFrameRate {"video.maxFrameRate", 240}, }, .audio = { @@ -59,7 +60,7 @@ UserSettings g_userSettings = { .bloomMultiplier {"game.bloomMultiplier", 1.0f}, .disableWaterRefraction {"game.disableWaterRefraction", false}, .enableTextureReplacements {"game.enableTextureReplacements", true}, - .enableFrameInterpolation {"game.enableFrameInterpolation", false}, + .enableFrameInterpolation {"game.enableFrameInterpolation", FrameInterpMode::Off}, .internalResolutionScale {"game.internalResolutionScale", 0}, .shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1}, .enableDepthOfField {"game.enableDepthOfField", true}, @@ -178,6 +179,7 @@ void registerSettings() { Register(g_userSettings.video.lockAspectRatio); Register(g_userSettings.video.enableFpsOverlay); Register(g_userSettings.video.fpsOverlayCorner); + Register(g_userSettings.video.maxFrameRate); // Audio Register(g_userSettings.audio.masterVolume); diff --git a/src/dusk/ui/preset.cpp b/src/dusk/ui/preset.cpp index 7bea7f8bf6..e5af8f2d50 100644 --- a/src/dusk/ui/preset.cpp +++ b/src/dusk/ui/preset.cpp @@ -40,7 +40,7 @@ void applyPresetDusk() { s.game.enableQuickTransform.setValue(true); s.game.instantSaves.setValue(true); s.game.midnasLamentNonStop.setValue(true); - s.game.enableFrameInterpolation.setValue(true); + s.game.enableFrameInterpolation.setValue(FrameInterpMode::Unlimited); s.game.sunsSong.setValue(true); s.game.bloomMode.setValue(BloomMode::Dusk); s.game.internalResolutionScale.setValue(0); diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index c8ab0a3710..780622e13a 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -59,6 +59,12 @@ constexpr std::array kFpsOverlayCornerNames = { "Bottom Right", }; +constexpr std::array kInterpolationModes = { + "Off", + "Capped", + "Unlimited", +}; + constexpr std::array kGyroInputModeLabels = { "Sensor", "Mouse", @@ -357,7 +363,7 @@ const Rml::String kBloomHelpText = const Rml::String kBloomBrightnessHelpText = "Configure bloom intensity. Higher values make bright areas glow more strongly."; const Rml::String kUnlockFramerateHelpText = - "Uses inter-frame interpolation to enable higher frame rates.

May introduce minor " + "
Uses inter-frame interpolation to enable higher frame rates.

May introduce minor " "visual artifacts or animation glitches."; int float_setting_percent(ConfigVar& var) { @@ -440,6 +446,31 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar& var, + Rml::String key, Rml::String helpText, int min, int max, int step = 5, + std::function isDisabled = {}, std::string suffix = "") { + auto& button = leftPane.add_child(NumberButton::Props{ + .key = std::move(key), + .getValue = [&var] { return var; }, + .setValue = + [&var, min, max](int value) { + var.setValue(std::clamp(value, min, max)); + config::Save(); + }, + .isDisabled = std::move(isDisabled), + .isModified = [&var] { return var.getValue() != var.getDefaultValue(); }, + .min = min, + .max = max, + .step = step, + .suffix = suffix, + }); + leftPane.register_control(button, rightPane, [helpText = std::move(helpText)](Pane& pane) { + pane.clear(); + pane.add_text(helpText); + }); + return button; +} + template void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, ConfigVar& var, const GraphicsTunerProps& props, bool prelaunch) { @@ -803,11 +834,39 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .helpText = "Enable installed texture replacements.", .onChange = [](bool value) { aurora_set_texture_replacements_enabled(value); }, }); - config_bool_select(leftPane, rightPane, getSettings().game.enableFrameInterpolation, - { + leftPane.register_control( + leftPane.add_select_button({ .key = "Unlock Framerate", - .helpText = kUnlockFramerateHelpText, + .getValue = + [] { + return kInterpolationModes[static_cast(getSettings().game.enableFrameInterpolation.getValue())]; + }, + .isModified = + [] { + return getSettings().game.enableFrameInterpolation.getValue() != + getSettings().game.enableFrameInterpolation.getDefaultValue(); + }, + }), + rightPane, [](Pane& pane) { + for (int i = 0; i < kInterpolationModes.size(); i++) { + pane.add_button({ + .text = kInterpolationModes[i], + .isSelected = + [i] { + return getSettings().game.enableFrameInterpolation.getValue() == static_cast(i); + }, + }) + .on_pressed([i] { + mDoAud_seStartMenu(kSoundItemChange); + getSettings().game.enableFrameInterpolation.setValue(static_cast(i)); + config::Save(); + }); + } + pane.add_rml(kUnlockFramerateHelpText); }); + config_int_select(leftPane, rightPane, getSettings().video.maxFrameRate, + "Framerate Cap", "Limit the framerate to the specified value.", 30, 540, 1, + [] { return getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Capped; }); config_bool_select(leftPane, rightPane, getSettings().game.enableDepthOfField, { .key = "Enable Depth of Field", diff --git a/src/m_Do/m_Do_ext.cpp b/src/m_Do/m_Do_ext.cpp index fc8952e91d..16158815cd 100644 --- a/src/m_Do/m_Do_ext.cpp +++ b/src/m_Do/m_Do_ext.cpp @@ -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) { diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index c400b22266..65a2b5b95a 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -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()); diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 360bf8314d..6593d78162 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -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(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(sleepTime) / static_cast(target_ns)); + } else { + main_loop_limiter.Reset(); + } + aurora_end_frame(); + FrameMark; #ifdef DUSK_DISCORD