diff --git a/extern/aurora b/extern/aurora index f93b9e5bc2..10006618ee 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit f93b9e5bc20850198538ccd3abc69ab2b128ecf7 +Subproject commit 10006618ee493f248b8597e4dfa1d2871d76a1d9 diff --git a/include/dusk/endian.h b/include/dusk/endian.h index 0fec59411f..d16d0aa80c 100644 --- a/include/dusk/endian.h +++ b/include/dusk/endian.h @@ -227,6 +227,28 @@ struct BE { } }; +typedef f32 Mtx23[2][3]; +template <> +struct BE { + BE contents[2][3]; + + auto& operator[](int x) { + return contents[x]; + } + + auto& operator[](int x) const { + return contents[x]; + } + + void to_host(Mtx23& mtx) const { + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 3; j++) { + mtx[i][j] = contents[i][j]; + } + } + } +}; + template void be_swap(T& val) { val = BE::swap(val); 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 80b589021e..03d87045eb 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -15,6 +15,11 @@ enum class BloomMode : int { Dusk = 2, }; +enum class Resampler : int { + Bilinear = 0, + Area = 1, +}; + enum class GameLanguage : u8 { English = OS_LANGUAGE_ENGLISH, German = OS_LANGUAGE_GERMAN, @@ -34,6 +39,12 @@ enum class GyroMode : u8 { Mouse = 1, }; +enum class FrameInterpMode : u8 { + Off = 0, + Capped = 1, + Unlimited = 2, +}; + namespace config { template <> struct ConfigEnumRange { @@ -41,6 +52,12 @@ struct ConfigEnumRange { static constexpr auto max = BloomMode::Dusk; }; +template <> +struct ConfigEnumRange { + static constexpr auto min = Resampler::Bilinear; + static constexpr auto max = Resampler::Area; +}; + template <> struct ConfigEnumRange { static constexpr auto min = GameLanguage::English; @@ -58,7 +75,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 +95,7 @@ struct UserSettings { ConfigVar lockAspectRatio; ConfigVar enableFpsOverlay; ConfigVar fpsOverlayCorner; + ConfigVar maxFrameRate; } video; struct { @@ -124,9 +148,10 @@ struct UserSettings { ConfigVar bloomMultiplier; ConfigVar disableWaterRefraction; ConfigVar enableTextureReplacements; - ConfigVar enableFrameInterpolation; + ConfigVar enableFrameInterpolation; ConfigVar internalResolutionScale; ConfigVar shadowResolutionMultiplier; + ConfigVar resampler; ConfigVar enableDepthOfField; ConfigVar enableMapBackground; ConfigVar disableCutscenePillarboxing; diff --git a/include/dusk/time.h b/include/dusk/time.h index c43437f639..ead946188b 100644 --- a/include/dusk/time.h +++ b/include/dusk/time.h @@ -17,16 +17,24 @@ #include #include #endif +#ifdef __APPLE__ +#include +#if defined(__x86_64__) || defined(__i386__) +#include +#endif +#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 +49,8 @@ public: } } Reset(); + + return adjustedSleepTime; } duration_t SleepTime(duration_t targetFrameTime) { @@ -74,7 +84,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 +107,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/include/JSystem/J2DGraph/J2DTevs.h b/libs/JSystem/include/JSystem/J2DGraph/J2DTevs.h index 2d5f3ca2d6..ea81bc869f 100644 --- a/libs/JSystem/include/JSystem/J2DGraph/J2DTevs.h +++ b/libs/JSystem/include/JSystem/J2DGraph/J2DTevs.h @@ -116,7 +116,7 @@ private: * */ struct J2DIndTexMtxInfo { - /* 0x00 */ Mtx23 mMtx; + /* 0x00 */ BE(Mtx23) mMtx; /* 0x18 */ s8 mScaleExp; J2DIndTexMtxInfo& operator=(const J2DIndTexMtxInfo& other) { diff --git a/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h b/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h index 7a3ae12470..996fef6fef 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h @@ -287,28 +287,28 @@ template class JASPoolAllocObject { public: #if TARGET_PC - static void* operator new(size_t n, JKRHeapToken) { + static void* operator new(size_t n, JKRHeapToken) IF_DUSK(noexcept) { return operator new(n); } #endif - static void* operator new(size_t n) { + static void* operator new(size_t n) IF_DUSK(noexcept) { #if PLATFORM_GCN JASMemPool& memPool_ = getMemPool_(); #endif return memPool_.alloc(n); } - static void* operator new(size_t n, void* ptr) { + static void* operator new(size_t n, void* ptr) IF_DUSK(noexcept) { return ptr; } #if TARGET_PC - static void operator delete(void* ptr, size_t n, JKRHeapToken) { + static void operator delete(void* ptr, size_t n, JKRHeapToken) IF_DUSK(noexcept) { operator delete(ptr, n); } #endif - static void operator delete(void* ptr, size_t n) { + static void operator delete(void* ptr, size_t n) IF_DUSK(noexcept) { #if PLATFORM_GCN JASMemPool& memPool_ = getMemPool_(); #endif @@ -402,28 +402,28 @@ template class JASPoolAllocObject_MultiThreaded { public: #if TARGET_PC - static void* operator new(size_t n, JKRHeapToken) { + static void* operator new(size_t n, JKRHeapToken) IF_DUSK(noexcept) { return operator new(n); } #endif - static void* operator new(size_t n) { + static void* operator new(size_t n) IF_DUSK(noexcept) { #if PLATFORM_GCN JASMemPool_MultiThreaded& memPool_ = getMemPool(); #endif return memPool_.alloc(n); } - static void* operator new(size_t n, void* ptr) { + static void* operator new(size_t n, void* ptr) IF_DUSK(noexcept) { return ptr; } #if TARGET_PC - static void operator delete(void* ptr, size_t n, JKRHeapToken) { + static void operator delete(void* ptr, size_t n, JKRHeapToken) IF_DUSK(noexcept) { return operator delete(ptr, n); } #endif - static void operator delete(void* ptr, size_t n) { + static void operator delete(void* ptr, size_t n) IF_DUSK(noexcept) { #if PLATFORM_GCN JASMemPool_MultiThreaded& memPool_ = getMemPool(); #endif diff --git a/libs/JSystem/include/JSystem/JKernel/JKRHeap.h b/libs/JSystem/include/JSystem/JKernel/JKRHeap.h index d3710bd5df..d1b27a0fd0 100644 --- a/libs/JSystem/include/JSystem/JKernel/JKRHeap.h +++ b/libs/JSystem/include/JSystem/JKernel/JKRHeap.h @@ -237,11 +237,11 @@ enum class JKRHeapToken { Dummy }; -inline void* operator new(size_t, JKRHeapToken, void* where) { +inline void* operator new(size_t, JKRHeapToken, void* where) noexcept { return where; } -inline void* operator new[](size_t, JKRHeapToken, void* where) { +inline void* operator new[](size_t, JKRHeapToken, void* where) noexcept { return where; } @@ -264,21 +264,21 @@ inline void* operator new[](size_t, JKRHeapToken, void* where) { #define JKR_HEAP_TOKEN_PARAM #endif -void* operator new(size_t size JKR_HEAP_TOKEN_PARAM); -void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, int alignment); -void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, JKRHeap* heap, int alignment); +void* operator new(size_t size JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept); +void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, int alignment) IF_DUSK(noexcept); +void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, JKRHeap* heap, int alignment) IF_DUSK(noexcept); // On PC, these new[] overloads are only used to catch usages of JKR_NEW with []. -void* operator new[](size_t size JKR_HEAP_TOKEN_PARAM); -void* operator new[](size_t size JKR_HEAP_TOKEN_PARAM, int alignment); -void* operator new[](size_t size JKR_HEAP_TOKEN_PARAM, JKRHeap* heap, int alignment); +void* operator new[](size_t size JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept); +void* operator new[](size_t size JKR_HEAP_TOKEN_PARAM, int alignment) IF_DUSK(noexcept); +void* operator new[](size_t size JKR_HEAP_TOKEN_PARAM, JKRHeap* heap, int alignment) IF_DUSK(noexcept); -void operator delete(void* ptr JKR_HEAP_TOKEN_PARAM); -void operator delete[](void* ptr JKR_HEAP_TOKEN_PARAM); +void operator delete(void* ptr JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept); +void operator delete[](void* ptr JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept); #if TARGET_PC template -void jkrDelete(T* ptr) { +void jkrDelete(T* ptr) IF_DUSK(noexcept) { if (ptr == nullptr) { return; } @@ -298,7 +298,7 @@ void jkrDelete(T* ptr) { } template<> -inline void jkrDelete(void* ptr) { +inline void jkrDelete(void* ptr) IF_DUSK(noexcept) { if (ptr == nullptr) { return; } @@ -322,7 +322,7 @@ constexpr bool newArgsHasCustomAlignment() { } template -T* jkrNewArray(size_t count, std::in_place_type_t, Args&&... args) { +T* jkrNewArray(size_t count, std::in_place_type_t, Args&&... args) IF_DUSK(noexcept) { size_t allocSize = count * sizeof(T); if constexpr (!std::is_trivially_destructible()) { static_assert( @@ -333,6 +333,10 @@ T* jkrNewArray(size_t count, std::in_place_type_t, Args&&... args) { } void* ptr = operator new(allocSize, JKRHeapToken::Dummy, args...); + if (!ptr) { + return nullptr; + } + T* dataPtr; if constexpr (!std::is_trivially_destructible()) { auto length = static_cast(ptr); @@ -352,7 +356,7 @@ T* jkrNewArray(size_t count, std::in_place_type_t, Args&&... args) { } template -void jkrDeleteArray(T* pointer) { +void jkrDeleteArray(T* pointer) IF_DUSK(noexcept) { if (pointer == nullptr) { return; } @@ -372,7 +376,7 @@ void jkrDeleteArray(T* pointer) { } template<> -inline void jkrDeleteArray(void* pointer) { +inline void jkrDeleteArray(void* pointer) IF_DUSK(noexcept) { if (pointer == nullptr) { return; } diff --git a/libs/JSystem/src/J2DGraph/J2DTevs.cpp b/libs/JSystem/src/J2DGraph/J2DTevs.cpp index 8daf82872c..33a65dd273 100644 --- a/libs/JSystem/src/J2DGraph/J2DTevs.cpp +++ b/libs/JSystem/src/J2DGraph/J2DTevs.cpp @@ -68,8 +68,14 @@ void J2DIndTevStage::load(u8 tevStage) { } void J2DIndTexMtx::load(u8 indTexMtx) { +#ifdef TARGET_PC + Mtx23 mtx; + mIndTexMtxInfo.mMtx.to_host(mtx); + GXSetIndTexMtx((GXIndTexMtxID)(GX_ITM_0 + indTexMtx), mtx, mIndTexMtxInfo.mScaleExp); +#else GXSetIndTexMtx((GXIndTexMtxID)(GX_ITM_0 + indTexMtx), mIndTexMtxInfo.mMtx, mIndTexMtxInfo.mScaleExp); +#endif } void J2DIndTexCoordScale::load(u8 indTexStage) { 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/JKernel/JKRHeap.cpp b/libs/JSystem/src/JKernel/JKRHeap.cpp index ce1899a4ba..c4849437ac 100644 --- a/libs/JSystem/src/JKernel/JKRHeap.cpp +++ b/libs/JSystem/src/JKernel/JKRHeap.cpp @@ -559,7 +559,7 @@ void* operator new(size_t size) { return JKRHeap::alloc(size, 4, NULL); } #else -void* operator new(size_t size JKR_HEAP_TOKEN_PARAM) { +void* operator new(size_t size JKR_HEAP_TOKEN_PARAM) noexcept { if (sCurrentHeap == NULL) { return fallback_alloc(size, 0, false); } @@ -576,7 +576,7 @@ void* operator new(size_t size, int alignment) { return JKRHeap::alloc(size, alignment, NULL); } #else -void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, int alignment) { +void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, int alignment) noexcept { void* mem = JKRHeap::alloc(size, alignment, nullptr); if (mem == nullptr) { return fallback_alloc(size, abs(alignment), true); @@ -585,7 +585,7 @@ void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, int alignment) { } #endif -void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, JKRHeap* heap, int alignment) { +void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, JKRHeap* heap, int alignment) IF_DUSK(noexcept) { void* mem = JKRHeap::alloc(size, alignment, heap); #if TARGET_PC if (mem == nullptr) { @@ -600,7 +600,7 @@ void* operator new[](size_t size) { return JKRHeap::alloc(size, 4, NULL); } #else -void* operator new[](size_t JKR_HEAP_TOKEN_PARAM) { +void* operator new[](size_t JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept) { OSPanic(__FILE__, __LINE__, "Allocation should go through JKR_NEW_ARRAY instead"); } #endif @@ -610,12 +610,12 @@ void* operator new[](size_t size, int alignment) { return JKRHeap::alloc(size, alignment, NULL); } #else -void* operator new[](size_t JKR_HEAP_TOKEN_PARAM, int) { +void* operator new[](size_t JKR_HEAP_TOKEN_PARAM, int) IF_DUSK(noexcept) { OSPanic(__FILE__, __LINE__, "Allocation should go through JKR_NEW_ARRAY instead"); } #endif -void* operator new[](size_t JKR_HEAP_TOKEN_PARAM, JKRHeap*, int) { +void* operator new[](size_t JKR_HEAP_TOKEN_PARAM, JKRHeap*, int) IF_DUSK(noexcept) { OSPanic(__FILE__, __LINE__, "Allocation should go through JKR_NEW_ARRAY instead"); } @@ -624,7 +624,7 @@ void operator delete(void* ptr) { JKRHeap::free(ptr, NULL); } #else -void operator delete(void* ptr JKR_HEAP_TOKEN_PARAM) { +void operator delete(void* ptr JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept) { if (ptr == NULL) return; JKRHeap* heap = JKRHeap::findFromRoot(ptr); @@ -645,7 +645,7 @@ void operator delete[](void* ptr) { JKRHeap::free(ptr, NULL); } #else -void operator delete[](void* ptr JKR_HEAP_TOKEN_PARAM) { +void operator delete[](void* ptr JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept) { if (ptr == NULL) return; JKRHeap* heap = JKRHeap::findFromRoot(ptr); 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 f2501e8f86..a2100563c3 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -5997,7 +5997,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); @@ -19797,7 +19797,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 db87e1294f..21d715189e 100644 --- a/src/d/actor/d_a_e_s1.cpp +++ b/src/d/actor/d_a_e_s1.cpp @@ -120,6 +120,13 @@ static void daE_S1_interp_callback(bool isSimFrame, void* pUserWork) { dst[i] = p0 + (p1 - p0) * alpha; } } + GXColor line_color; + line_color.r = JREG_S(0) + 5; + line_color.g = JREG_S(1) + 10; + line_color.b = JREG_S(2) + 10; + line_color.a = 0xFF; + + i_this->mLineMat.update(16, line_color, &i_this->tevStr); } #endif @@ -157,7 +164,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 44a8aee249..9eae74b1af 100644 --- a/src/d/actor/d_a_e_yg.cpp +++ b/src/d/actor/d_a_e_yg.cpp @@ -139,6 +139,7 @@ static BOOL pl_check(e_yg_class* i_this, f32 i_dist) { #if TARGET_PC static void daE_YG_interp_callback(bool isSimFrame, void* pUserWork) { e_yg_class* i_this = (e_yg_class*)pUserWork; + fopAc_ac_c* actor = (fopAc_ac_c*)&i_this->actor; if (!i_this->mTentacleInterpPrevValid || !i_this->mTentacleInterpCurrValid) { return; } @@ -152,6 +153,13 @@ static void daE_YG_interp_callback(bool isSimFrame, void* pUserWork) { dst[i] = p0 + (p1 - p0) * alpha; } } + GXColor color; + color.r = JREG_S(0) + 20; + color.g = JREG_S(1) + 20; + color.b = JREG_S(2) + 20; + color.a = 0xFF; + + i_this->mLineMat.update(10, color, &actor->tevStr); } #endif @@ -183,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..1f9e7c54a6 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,8 @@ namespace dusk::config { template class ConfigImpl; 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/imgui/ImGuiSaveEditor.cpp b/src/dusk/imgui/ImGuiSaveEditor.cpp index b08ae2860b..c09267bc27 100644 --- a/src/dusk/imgui/ImGuiSaveEditor.cpp +++ b/src/dusk/imgui/ImGuiSaveEditor.cpp @@ -713,7 +713,7 @@ namespace dusk { transformLevel++; } } - if (ImGui::SliderInt("Transform Level", &transformLevel, 0, 3)) { + if (ImGui::SliderInt("Transform Level", &transformLevel, 0, 4)) { u8 newFlags = 0; for (int i = 0; i < transformLevel; i++) { newFlags |= (1 << i); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 4eb302a057..f32d0daf32 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,9 +60,10 @@ 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}, + .resampler {"game.resampler", Resampler::Bilinear}, .enableDepthOfField {"game.enableDepthOfField", true}, .enableMapBackground {"game.enableMapBackground", true}, .disableCutscenePillarboxing {"game.disableCutscenePillarboxing", false}, @@ -179,6 +181,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); @@ -222,6 +225,7 @@ void registerSettings() { Register(g_userSettings.game.disableWaterRefraction); Register(g_userSettings.game.enableTextureReplacements); Register(g_userSettings.game.internalResolutionScale); + Register(g_userSettings.game.resampler); Register(g_userSettings.game.shadowResolutionMultiplier); Register(g_userSettings.game.enableDepthOfField); Register(g_userSettings.game.enableMapBackground); diff --git a/src/dusk/ui/editor.cpp b/src/dusk/ui/editor.cpp index bd6dc3e067..196c9bf719 100644 --- a/src/dusk/ui/editor.cpp +++ b/src/dusk/ui/editor.cpp @@ -1501,14 +1501,14 @@ EditorWindow::EditorWindow() { .getValue = [] { return std::popcount(static_cast( - get_player_status_b()->mTransformLevelFlag & 0x7)); + get_player_status_b()->mTransformLevelFlag & 0xF)); }, .setValue = [](int value) { get_player_status_b()->mTransformLevelFlag = static_cast((1u << value) - 1u); }, - .max = 3, + .max = 4, }), rightPane, {}); leftPane.register_control( diff --git a/src/dusk/ui/graphics_tuner.cpp b/src/dusk/ui/graphics_tuner.cpp index 4b70bab592..0b75b27a89 100644 --- a/src/dusk/ui/graphics_tuner.cpp +++ b/src/dusk/ui/graphics_tuner.cpp @@ -3,6 +3,7 @@ #include "Z2AudioLib/Z2SeMgr.h" #include "m_Do/m_Do_audio.h" +#include #include #include #include @@ -43,6 +44,8 @@ int get_value(GraphicsOption option) { return getSettings().game.internalResolutionScale.getValue(); case GraphicsOption::ShadowResolution: return getSettings().game.shadowResolutionMultiplier.getValue(); + case GraphicsOption::Resampler: + return static_cast(getSettings().game.resampler.getValue()); case GraphicsOption::BloomMode: return static_cast(getSettings().game.bloomMode.getValue()); case GraphicsOption::BloomMultiplier: @@ -62,6 +65,22 @@ void set_value(GraphicsOption option, int value) { case GraphicsOption::ShadowResolution: getSettings().game.shadowResolutionMultiplier.setValue(value); break; + case GraphicsOption::Resampler: { + const auto sampler = static_cast(std::clamp(value, + static_cast(Resampler::Bilinear), + static_cast(Resampler::Area))); + getSettings().game.resampler.setValue(sampler); + switch (sampler) { + case Resampler::Area: + aurora_set_resampler(SAMPLER_AREA); + break; + case Resampler::Bilinear: + default: + aurora_set_resampler(SAMPLER_BILINEAR); + break; + } + break; + } case GraphicsOption::BloomMode: getSettings().game.bloomMode.setValue(static_cast(std::clamp( value, static_cast(BloomMode::Off), static_cast(BloomMode::Dusk)))); @@ -177,6 +196,14 @@ Rml::String format_graphics_setting_value(GraphicsOption option, int value) { } case GraphicsOption::ShadowResolution: return fmt::format("{}×", value); + case GraphicsOption::Resampler: + switch (static_cast(value)) { + case Resampler::Bilinear: + return "Bilinear"; + case Resampler::Area: + return "Area"; + } + break; case GraphicsOption::BloomMode: switch (static_cast(value)) { case BloomMode::Off: diff --git a/src/dusk/ui/graphics_tuner.hpp b/src/dusk/ui/graphics_tuner.hpp index 36932bcbbb..6f8f113d13 100644 --- a/src/dusk/ui/graphics_tuner.hpp +++ b/src/dusk/ui/graphics_tuner.hpp @@ -42,6 +42,7 @@ private: enum class GraphicsOption { InternalResolution, ShadowResolution, + Resampler, BloomMode, BloomMultiplier, }; 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..9cf8a2f5d2 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", @@ -351,13 +357,15 @@ const Rml::String kInternalResolutionHelpText = const Rml::String kShadowResolutionHelpText = "Configure the shadow-map resolution. Higher values improve shadow quality but increase GPU " "and memory usage."; +const Rml::String kResamplerHelpText = + "Configure the sampling method used when scaling the internal resolution for final presentation."; const Rml::String kBloomHelpText = "Configure the post-processing bloom effect. Classic uses the original bloom pass; Dusklight uses " "a higher-quality bloom pass."; 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 +448,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) { @@ -774,6 +807,15 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .valueMax = 8, .defaultValue = 1, }, mPrelaunch); + graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.resampler, + GraphicsTunerProps{ + .option = GraphicsOption::Resampler, + .title = "Output Resampling", + .helpText = kResamplerHelpText, + .valueMin = static_cast(Resampler::Bilinear), + .valueMax = static_cast(Resampler::Area), + .defaultValue = static_cast(Resampler::Bilinear), + }, mPrelaunch); leftPane.add_section("Post-Processing"); graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMode, @@ -803,11 +845,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 0e6801ebd1..7f75bf8443 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" @@ -284,8 +285,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(); @@ -296,7 +298,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(); @@ -306,7 +308,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 @@ -320,8 +322,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 @@ -600,6 +620,15 @@ int game_main(int argc, char* argv[]) { AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); } VISetFrameBufferScale(dusk::getSettings().game.internalResolutionScale.getValue()); + switch (dusk::getSettings().game.resampler.getValue()) { + case dusk::Resampler::Area: + aurora_set_resampler(SAMPLER_AREA); + break; + case dusk::Resampler::Bilinear: + default: + aurora_set_resampler(SAMPLER_BILINEAR); + break; + } dusk::audio::SetMasterVolume(dusk::audio::MasterVolumeToLinear(dusk::getSettings().audio.masterVolume / 100.0f)); dusk::audio::SetEnableReverb(dusk::getSettings().audio.enableReverb);