From 05edbec8279f232c5cb38107f328dc2e39444a79 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Mon, 18 May 2026 02:06:38 +0200 Subject: [PATCH 01/10] Tag operator new overloads with noexcept According to the C++ standard, regular operator new must always return a valid pointer, and allocation failure should throw an exception. The original game did not use exceptions, and instead had its operator new return null on alloc failure. Clang and GCC seem to be enforcing the standard here, and this is causing crashes on non-Windows platforms like https://sentry.twilitrealm.dev/organizations/twilitrealm/issues/952/, where a JAISe allocation failure (custom pooled operator new overload) crashes the game when it should be handled gracefully. MSVC seems to not make use of this opportunity, meaning the code works as intended. Tagging the operators with noexcept seems to satisfy GCC, but I admit cppreference is very light on details here. --- .../include/JSystem/JAudio2/JASHeapCtrl.h | 10 +++--- .../JSystem/include/JSystem/JKernel/JKRHeap.h | 34 +++++++++++-------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h b/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h index 7a3ae12470..0378dc36b8 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 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; } From 831a34ac8f2163315ee363c40546c4fc3e6bc1c9 Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Mon, 18 May 2026 02:15:12 +0200 Subject: [PATCH 02/10] Right, the definition needs the specifier too --- libs/JSystem/src/JKernel/JKRHeap.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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); From 629cd2f9d3897a41fdb86eba9c53bda7c121e33c Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Mon, 18 May 2026 02:16:43 +0200 Subject: [PATCH 03/10] Missed these in JASHeapCtrl.h --- libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h b/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h index 0378dc36b8..996fef6fef 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h @@ -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 From 2be2615e616c59d5a5acfe12eb9065cda18fb1df Mon Sep 17 00:00:00 2001 From: doop <56421834+dooplecks@users.noreply.github.com> Date: Mon, 18 May 2026 00:46:45 +0000 Subject: [PATCH 04/10] Endian-swap indirect texture matrix in J2D data Fixes copy/delete effects on the file screen. --- include/dusk/endian.h | 22 +++++++++++++++++++ .../include/JSystem/J2DGraph/J2DTevs.h | 2 +- libs/JSystem/src/J2DGraph/J2DTevs.cpp | 6 +++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/include/dusk/endian.h b/include/dusk/endian.h index 0fec59411f..e73eb8da19 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 < 3; i++) { + for (int j = 0; j < 4; j++) { + mtx[i][j] = contents[i][j]; + } + } + } +}; + template void be_swap(T& val) { val = BE::swap(val); 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/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) { From 1745e110fb4669e526281b16c3aa136c9c994a5e Mon Sep 17 00:00:00 2001 From: doop <56421834+dooplecks@users.noreply.github.com> Date: Mon, 18 May 2026 00:56:14 +0000 Subject: [PATCH 05/10] Fix typo --- include/dusk/endian.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/dusk/endian.h b/include/dusk/endian.h index e73eb8da19..d16d0aa80c 100644 --- a/include/dusk/endian.h +++ b/include/dusk/endian.h @@ -241,8 +241,8 @@ struct BE { } void to_host(Mtx23& mtx) const { - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 4; j++) { + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 3; j++) { mtx[i][j] = contents[i][j]; } } From 28d0017d4c36d38c50a8defa0778022a70a80b4d Mon Sep 17 00:00:00 2001 From: Olivia!! Date: Mon, 18 May 2026 04:04:34 +0200 Subject: [PATCH 06/10] Transform level is 0xF after lakebed temple, verified on dolphin (#1571) --- src/dusk/imgui/ImGuiSaveEditor.cpp | 2 +- src/dusk/ui/editor.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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( From 66c5cb1daebd39db7a09a1ac5d89f1b1496cc2f4 Mon Sep 17 00:00:00 2001 From: Irastris Date: Sun, 17 May 2026 22:09:04 -0400 Subject: [PATCH 07/10] Fix interpolation for Shadow Beast & Twilight Vermin hair (#1575) * Fix interpolation for Shadow Beast hair * Fix interpolation for Twilight Vermin hair --- src/d/actor/d_a_e_s1.cpp | 7 +++++++ src/d/actor/d_a_e_yg.cpp | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/src/d/actor/d_a_e_s1.cpp b/src/d/actor/d_a_e_s1.cpp index a5a05fce20..31973fc317 100644 --- a/src/d/actor/d_a_e_s1.cpp +++ b/src/d/actor/d_a_e_s1.cpp @@ -117,6 +117,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 diff --git a/src/d/actor/d_a_e_yg.cpp b/src/d/actor/d_a_e_yg.cpp index 44a8aee249..7fa35bdf08 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 From 2da6590657159f4ae4e7a49bfd0fe4d2f73d2ec2 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 18 May 2026 04:11:32 +0200 Subject: [PATCH 08/10] feat: FPS Limiter (#1446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 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 --- include/dusk/frame_interpolation.h | 3 +- include/dusk/settings.h | 17 ++++- include/dusk/time.h | 41 ++++++++++-- libs/JSystem/src/JFramework/JFWDisplay.cpp | 14 ++-- .../src/JStudio/JStudio/jstudio-object.cpp | 4 +- src/d/actor/d_a_alink.cpp | 4 +- src/d/actor/d_a_b_gnd.cpp | 2 +- src/d/actor/d_a_e_db.cpp | 2 +- src/d/actor/d_a_e_hb.cpp | 2 +- src/d/actor/d_a_e_mb.cpp | 2 +- src/d/actor/d_a_e_s1.cpp | 2 +- src/d/actor/d_a_e_wb.cpp | 2 +- src/d/actor/d_a_e_yd.cpp | 2 +- src/d/actor/d_a_e_yg.cpp | 2 +- src/d/actor/d_a_e_yh.cpp | 2 +- src/d/actor/d_a_horse.cpp | 2 +- src/d/actor/d_a_obj_fchain.cpp | 2 +- src/d/actor/d_a_obj_klift00.cpp | 2 +- src/d/actor/d_a_title.cpp | 4 +- src/d/d_camera.cpp | 4 +- src/d/d_menu_dmap.cpp | 4 +- src/d/d_menu_fmap.cpp | 2 +- src/d/d_menu_fmap2D.cpp | 2 +- src/dusk/config.cpp | 18 +++++ src/dusk/frame_interpolation.cpp | 4 +- src/dusk/game_clock.cpp | 4 +- src/dusk/settings.cpp | 4 +- src/dusk/ui/preset.cpp | 2 +- src/dusk/ui/settings.cpp | 67 +++++++++++++++++-- src/m_Do/m_Do_ext.cpp | 12 ++-- src/m_Do/m_Do_graphic.cpp | 6 +- src/m_Do/m_Do_main.cpp | 26 ++++++- 32 files changed, 207 insertions(+), 59 deletions(-) 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 From 7919c6110ddec7b73b868584eb588cdab366b7db Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 17 May 2026 22:25:12 -0400 Subject: [PATCH 09/10] time.h: Include immintrin.h on x86_64 macOS --- include/dusk/time.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/dusk/time.h b/include/dusk/time.h index 48dbe4c9ed..ead946188b 100644 --- a/include/dusk/time.h +++ b/include/dusk/time.h @@ -19,6 +19,9 @@ #endif #ifdef __APPLE__ #include +#if defined(__x86_64__) || defined(__i386__) +#include +#endif #endif class Limiter { From c516b91476cbdcfde16fe0b57d7bfc6fcb37fab6 Mon Sep 17 00:00:00 2001 From: Irastris Date: Sun, 17 May 2026 22:26:10 -0400 Subject: [PATCH 10/10] Add "Output Resampling" option (#1541) Adds an Output Resampling option to the Video tab to allow choosing between the old Bilinear sampler and a new Area sampler. Area sampling produces a much cleaner, softer image when downscaling, and a significantly sharper image when upscaling. This can also serve as a halfway decent anti-aliasing substitute until we have a more proper implementation. --- extern/aurora | 2 +- include/dusk/settings.h | 12 ++++++++++++ src/dusk/config.cpp | 1 + src/dusk/settings.cpp | 2 ++ src/dusk/ui/graphics_tuner.cpp | 27 +++++++++++++++++++++++++++ src/dusk/ui/graphics_tuner.hpp | 1 + src/dusk/ui/settings.cpp | 11 +++++++++++ src/m_Do/m_Do_main.cpp | 9 +++++++++ 8 files changed, 64 insertions(+), 1 deletion(-) 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/settings.h b/include/dusk/settings.h index f89aeeb875..bd6f30683b 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, @@ -47,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; @@ -140,6 +151,7 @@ struct UserSettings { ConfigVar enableFrameInterpolation; ConfigVar internalResolutionScale; ConfigVar shadowResolutionMultiplier; + ConfigVar resampler; ConfigVar enableDepthOfField; ConfigVar enableMapBackground; ConfigVar disableCutscenePillarboxing; diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index 3d15f1fded..1f9e7c54a6 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -176,6 +176,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/settings.cpp b/src/dusk/settings.cpp index e3eae37aad..5861241fe3 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -63,6 +63,7 @@ UserSettings g_userSettings = { .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}, @@ -223,6 +224,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/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/settings.cpp b/src/dusk/ui/settings.cpp index 780622e13a..9cf8a2f5d2 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -357,6 +357,8 @@ 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."; @@ -805,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, diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 6593d78162..490c0b8980 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -604,6 +604,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);