From 59d2014fb99198e87f0d731e7b6317b8a3f5331d Mon Sep 17 00:00:00 2001 From: Irastris Date: Fri, 17 Apr 2026 21:21:44 -0400 Subject: [PATCH 01/68] Frame Interp: UI Pacing Simplification --- include/dusk/frame_interpolation.h | 7 +- .../include/JSystem/JFramework/JFWDisplay.h | 4 - libs/JSystem/src/JFramework/JFWDisplay.cpp | 17 +-- src/d/actor/d_a_midna.cpp | 2 +- src/d/d_menu_fmap2D.cpp | 5 +- src/d/d_meter2_draw.cpp | 18 +-- src/d/d_msg_scrn_light.cpp | 12 +- src/d/d_select_cursor.cpp | 24 ++-- src/d/d_select_icon.cpp | 8 +- src/d/d_timer.cpp | 129 +++++++++--------- src/dusk/frame_interpolation.cpp | 42 ++---- src/m_Do/m_Do_graphic.cpp | 114 ++++------------ src/m_Do/m_Do_main.cpp | 4 +- 13 files changed, 125 insertions(+), 261 deletions(-) diff --git a/include/dusk/frame_interpolation.h b/include/dusk/frame_interpolation.h index 383d16d288..7cd98eebe4 100644 --- a/include/dusk/frame_interpolation.h +++ b/include/dusk/frame_interpolation.h @@ -19,14 +19,11 @@ void end_record(); void interpolate(float step); float get_interpolation_step(); -void notify_presentation_frame(); void request_presentation_sync(); bool presentation_sync_active(); -void notify_sim_tick_complete(); -uint32_t begin_presentation_ui_pass(); -uint32_t get_presentation_ui_advance_ticks(); -void end_presentation_ui_pass(); +void set_ui_tick_pending(bool value); +bool get_ui_tick_pending(); void open_child(const void* key, int32_t id); void close_child(); diff --git a/libs/JSystem/include/JSystem/JFramework/JFWDisplay.h b/libs/JSystem/include/JSystem/JFramework/JFWDisplay.h index fab39a296b..28967cc0a2 100644 --- a/libs/JSystem/include/JSystem/JFramework/JFWDisplay.h +++ b/libs/JSystem/include/JSystem/JFramework/JFWDisplay.h @@ -101,10 +101,6 @@ public: void setDrawDoneMethod(EDrawDone drawDone) { mDrawDoneMethod = drawDone; } void setFader(JUTFader* fader) { mFader = fader; } -#ifdef TARGET_PC - // For frame interpolation - void setFaderSimSteps(u32 steps); -#endif void resetFader() { setFader(NULL); } JUTFader* getFader() const { return mFader; } void setClearColor(JUtility::TColor color) { mClearColor = color; } diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index 38dbc7eab1..c35b283419 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -205,14 +205,6 @@ void JFWDisplay::preGX() { } } -#ifdef TARGET_PC -static s32 s_faderSimSteps = -1; - -void JFWDisplay::setFaderSimSteps(u32 steps) { - s_faderSimSteps = static_cast(steps); -} -#endif - void JFWDisplay::endGX() { s32 bufferNum = JUTXfb::getManager()->getBufferNum(); u16 width = JUTVideo::getManager()->getFbWidth(); @@ -224,14 +216,7 @@ void JFWDisplay::endGX() { if (mFader != NULL) { ortho.setPort(); #ifdef TARGET_PC - u32 advance_count = 1; - if (dusk::getSettings().game.enableFrameInterpolation && s_faderSimSteps >= 0) { - advance_count = static_cast(s_faderSimSteps); - s_faderSimSteps = -1; - } else { - s_faderSimSteps = -1; - } - for (u32 i = 0; i < advance_count; i++) { + if (dusk::frame_interp::get_ui_tick_pending()) { mFader->advance(); } if (mFader->getStatus() != 1) { diff --git a/src/d/actor/d_a_midna.cpp b/src/d/actor/d_a_midna.cpp index 01d472e075..6df9d2cc34 100644 --- a/src/d/actor/d_a_midna.cpp +++ b/src/d/actor/d_a_midna.cpp @@ -1054,7 +1054,7 @@ void daMidna_c::setBodyPartMatrix() { } mpModel->calcWeightEnvelopeMtx(); #ifdef TARGET_PC - // Frame interpolation: Record weight envelopes for Midna here, as they are otherwise missed causing distortion + // FRAME INTERP NOTE: Record weight envelopes for Midna here, as they are otherwise missed causing distortion for (u16 i = 0; i < mpModel->getModelData()->getWEvlpMtxNum(); i++) { dusk::frame_interp::record_final_mtx_raw(reinterpret_cast(mpModel->getWeightAnmMtx(i)), mpModel->getWeightAnmMtx(i)); } diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index 558ad05d6d..c496fed38d 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -392,16 +392,15 @@ void dMenu_Fmap2DBack_c::draw() { &mArrowPos2DY); #ifdef TARGET_PC - for (u32 i = 0; i < dusk::frame_interp::get_presentation_ui_advance_ticks(); ++i) { + if (dusk::frame_interp::get_ui_tick_pending()) #endif + { field_0x11e0 -= g_fmapHIO.mCursorSpeed; if (field_0x11e0 < 0.0f) { field_0x11e0 += 360.0f; } -#ifdef TARGET_PC } -#endif mpPointParent->getPanePtr()->rotate(mpPointParent->getSizeX() / 2.0f, mpPointParent->getSizeY() / 2.0f, ROTATE_Z, diff --git a/src/d/d_meter2_draw.cpp b/src/d/d_meter2_draw.cpp index 417dcad319..8e824f583b 100644 --- a/src/d/d_meter2_draw.cpp +++ b/src/d/d_meter2_draw.cpp @@ -638,12 +638,11 @@ void dMeter2Draw_c::draw() { var_f29 = g_drawHIO.mLightDrop.mDropPikariAnimSpeed_Completed; int temp_r5_2 = g_drawHIO.mLightDrop.mPikariInterval * 15; #ifdef TARGET_PC - // Set even if not advancing + // FRAME INTERP NOTE: Set even if not advancing var_f28 = g_drawHIO.mLightDrop.mPikariScaleComplete; - - const u32 ui_advance_ticks = dusk::frame_interp::get_presentation_ui_advance_ticks(); - for (u32 tick = 0; tick < ui_advance_ticks; ++tick) { + if (dusk::frame_interp::get_ui_tick_pending()) #endif + { if (field_0x756 <= temp_r5_2) { int temp_r4 = (field_0x756 % g_drawHIO.mLightDrop.mPikariInterval); int temp_r3_5 = field_0x756 / g_drawHIO.mLightDrop.mPikariInterval; @@ -669,17 +668,12 @@ void dMeter2Draw_c::draw() { } field_0x756 = -1; -#ifdef TARGET_PC - break; -#endif } else { field_0x756++; } } } -#ifdef TARGET_PC } -#endif for (int i = 0; i < 16; i++) { if (field_0x66c[i] > 0.0f) { @@ -1349,9 +1343,9 @@ void dMeter2Draw_c::drawPikari(f32 i_posX, f32 i_posY, f32* i_framep, f32 i_scal *i_framep = 0.0f; } else { #ifdef TARGET_PC - const u32 ui_advance_ticks = dusk::frame_interp::get_presentation_ui_advance_ticks(); - for (u32 i = 0; i < ui_advance_ticks; ++i) { + if (dusk::frame_interp::get_ui_tick_pending()) #endif + { *i_framep += param_8; if (*i_framep > var_f31) { if (param_9 == 1 || param_9 == 2 || param_9 == 3) { @@ -1366,9 +1360,7 @@ void dMeter2Draw_c::drawPikari(f32 i_posX, f32 i_posY, f32* i_framep, f32 i_scal } else if (*i_framep == 18.0f && param_9 == 2) { mDoAud_seStart(Z2SE_SY_ITEM_COMBINE_ICON, NULL, 0, 0); } -#ifdef TARGET_PC } -#endif playPikariBckAnimation(*i_framep); playPikariBpkAnimation(*i_framep); diff --git a/src/d/d_msg_scrn_light.cpp b/src/d/d_msg_scrn_light.cpp index c34397aad2..19671b5d57 100644 --- a/src/d/d_msg_scrn_light.cpp +++ b/src/d/d_msg_scrn_light.cpp @@ -204,16 +204,14 @@ void dMsgScrnLight_c::draw(f32* i_anmFrame, f32 i_posX, f32 i_posY, f32 i_scaleX if (mPlayAnim) { #ifdef TARGET_PC - const u32 ui_advance_ticks = dusk::frame_interp::get_presentation_ui_advance_ticks(); - for (u32 i = 0; i < ui_advance_ticks; ++i) { + if (dusk::frame_interp::get_ui_tick_pending()) #endif + { *i_anmFrame += 1.0f; if (*i_anmFrame >= mpBck->getFrameMax()) { *i_anmFrame = 0.0f; } -#ifdef TARGET_PC } -#endif mBckFrame = *i_anmFrame; mBpkFrame = *i_anmFrame; @@ -229,17 +227,15 @@ void dMsgScrnLight_c::draw(f32* i_anmFrame, f32 i_posX, f32 i_posY, f32 i_scaleX if (mPlayAnim) { #ifdef TARGET_PC - const u32 ui_advance_ticks = dusk::frame_interp::get_presentation_ui_advance_ticks(); - for (u32 i = 0; i < ui_advance_ticks; ++i) { + if (dusk::frame_interp::get_ui_tick_pending()) #endif + { *i_anmFrame += i_anmRate; if (*i_anmFrame >= mpBck->getFrameMax()) { *i_anmFrame = 0.0f; } -#ifdef TARGET_PC } -#endif mBckFrame = *i_anmFrame; mBpkFrame = *i_anmFrame; diff --git a/src/d/d_select_cursor.cpp b/src/d/d_select_cursor.cpp index 2cdd454737..545b54c9a3 100644 --- a/src/d/d_select_cursor.cpp +++ b/src/d/d_select_cursor.cpp @@ -272,9 +272,9 @@ void dSelect_cursor_c::update() { if (field_0x30) { if (chkPlayAnime(0)) { #ifdef TARGET_PC - const u32 ui_advance_ticks = dusk::frame_interp::get_presentation_ui_advance_ticks(); - for (u32 tick = 0; tick < ui_advance_ticks; ++tick) { + if (dusk::frame_interp::get_ui_tick_pending()) #endif + { if (mNameIdx == 1) { field_0x44 += mpCursorHIO->field_0x8 * fVar1; } else { @@ -284,9 +284,7 @@ void dSelect_cursor_c::update() { if (field_0x44 >= field_0x30->getFrameMax()) { field_0x44 -= field_0x30->getFrameMax(); } -#ifdef TARGET_PC } -#endif field_0x30->setFrame(field_0x44); setBpkAnimation(field_0x30); @@ -303,9 +301,9 @@ void dSelect_cursor_c::update() { if (field_0x34[i]) { if ((i == 0 && chkPlayAnime(2)) || (i == 1 && chkPlayAnime(3))) { #ifdef TARGET_PC - const u32 ui_advance_ticks = dusk::frame_interp::get_presentation_ui_advance_ticks(); - for (u32 tick = 0; tick < ui_advance_ticks; ++tick) { + if (dusk::frame_interp::get_ui_tick_pending()) #endif + { if (mNameIdx == 1) { field_0x48[i] += mpCursorHIO->field_0x8 * fVar1; } else { @@ -314,9 +312,7 @@ void dSelect_cursor_c::update() { if (field_0x48[i] >= field_0x34[i]->getFrameMax()) { field_0x48[i] -= field_0x34[i]->getFrameMax(); } -#ifdef TARGET_PC } -#endif field_0x34[i]->setFrame(field_0x48[i]); } @@ -326,9 +322,9 @@ void dSelect_cursor_c::update() { if (field_0x2C && chkPlayAnime(1)) { #ifdef TARGET_PC - const u32 ui_advance_ticks = dusk::frame_interp::get_presentation_ui_advance_ticks(); - for (u32 tick = 0; tick < ui_advance_ticks; ++tick) { + if (dusk::frame_interp::get_ui_tick_pending()) #endif + { if (mNameIdx == 1) { field_0x40 += mpCursorHIO->field_0x8 * fVar1; } else { @@ -337,9 +333,7 @@ void dSelect_cursor_c::update() { if (field_0x40 >= field_0x2C->getFrameMax()) { field_0x40 -= field_0x2C->getFrameMax(); } -#ifdef TARGET_PC } -#endif field_0x2C->setFrame(field_0x40); setBckAnimation(field_0x2C); @@ -348,13 +342,11 @@ void dSelect_cursor_c::update() { if (chkPlayAnime(1) && mNameIdx == 0) { #ifdef TARGET_PC - const u32 ui_advance_ticks = dusk::frame_interp::get_presentation_ui_advance_ticks(); - for (u32 tick = 0; tick < ui_advance_ticks; ++tick) { + if (dusk::frame_interp::get_ui_tick_pending()) #endif + { setCursorAnimation(); -#ifdef TARGET_PC } -#endif } mpScreen->animation(); diff --git a/src/d/d_select_icon.cpp b/src/d/d_select_icon.cpp index 3a4671fefa..32be9c15e2 100644 --- a/src/d/d_select_icon.cpp +++ b/src/d/d_select_icon.cpp @@ -9,9 +9,9 @@ dSi_HIO_c::dSi_HIO_c() {} void dSelect_icon_c::animation() { if (field_0x10->getAlpha() != 0) { #ifdef TARGET_PC - const u32 ui_advance_ticks = dusk::frame_interp::get_presentation_ui_advance_ticks(); - for (u32 i = 0; i < ui_advance_ticks; ++i) { + if (dusk::frame_interp::get_ui_tick_pending()) #endif + { field_0x20 += field_0x2c; if (field_0x20 >= field_0x1c->getFrameMax()) { field_0x20 = 0.0f; @@ -22,9 +22,9 @@ void dSelect_icon_c::animation() { if (field_0x28 >= field_0x24->getFrameMax()) { field_0x28 = 0.0f; } -#ifdef TARGET_PC } - // Set even if not advancing +#ifdef TARGET_PC + // FRAME INTERP NOTE: Set even if not advancing field_0x1c->setFrame(field_0x20); #endif diff --git a/src/d/d_timer.cpp b/src/d/d_timer.cpp index 552874b552..2e89ccbbef 100644 --- a/src/d/d_timer.cpp +++ b/src/d/d_timer.cpp @@ -1341,75 +1341,70 @@ void dDlst_TimerScrnDraw_c::draw() { ((f32)g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f); #if TARGET_PC - const u32 pending_ui_ticks = dusk::frame_interp::get_presentation_ui_advance_ticks(); -#else - const u32 pending_ui_ticks = 1u; + if (dusk::frame_interp::get_ui_tick_pending()) #endif - - for (int i = 0; i < 51; i++) { - for (u32 tick = 0; tick < pending_ui_ticks; tick++) { - if (!(m_getin_info[i].bck_frame > 0.0f && m_getin_info[i].bck_frame < temp)) { - break; - } - - if (m_getin_info[i].bck_frame < 60.0f) { - m_getin_info[i].bck_frame += g_drawHIO.mMiniGame.mGetInTextAnimSpeed; - if (m_getin_info[i].bck_frame > 60.0f) { - m_getin_info[i].bck_frame = 60.0f; - } - } else if (m_getin_info[i].bck_frame < g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f) { - m_getin_info[i].bck_frame++; - } else if (m_getin_info[i].bck_frame < temp) { - m_getin_info[i].bck_frame++; - } - } - - if (m_getin_info[i].bck_frame > 0.0f && m_getin_info[i].bck_frame < temp) { - f32 var_f29 = 1.0f; - if (m_getin_info[i].bck_frame >= g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f && - m_getin_info[i].bck_frame < temp) { - var_f29 = acc(g_drawHIO.mMiniGame.mGetInTextAlphaFrames, - temp - m_getin_info[i].bck_frame, 0); - } - - if (m_getin_info[i].bck_frame < 60.0f) { - playBckAnimation(m_getin_info[i].bck_frame); - } else { - playBckAnimation(60.0f); - } - - mpGetInParent->setAlphaRate(var_f29); - - if (g_drawHIO.mMiniGame.mGetInTextLocation == 1) { - mpGetInRoot->translate(m_getin_info[i].pos_x + g_drawHIO.mMiniGame.mGetInTextPosX, - m_getin_info[i].pos_y + g_drawHIO.mMiniGame.mGetInTextPosY); - } else { - f32 temp_f2 = m_getin_info[i].bck_frame - 40.0f; - f32 var_f3 = ((temp_f2 * 0.5f) * temp_f2) * 0.15f; - if (i == 0) { - var_f3 = 0.0f; - } - - mpGetInRoot->paneTrans( - m_getin_info[i].pos_x + g_drawHIO.mMiniGame.mGetInTextPosX, - (m_getin_info[i].pos_y + g_drawHIO.mMiniGame.mGetInTextPosY) - var_f3); - } - - mpGetInRoot->scale(g_drawHIO.mMiniGame.mGetInTextSizeX, - g_drawHIO.mMiniGame.mGetInTextSizeY); - mpGetInScreen->draw(0.0f, 0.0f, graf_ctx); - - if (m_getin_info[i].pikari_frame > 0.0f) { - drawPikari(i); - } else if (m_getin_info[i].pikari_frame == -1.0f) { - if (m_getin_info[i].field_0xc == 0) { - if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mGetInPikariAppearFrames) { - m_getin_info[i].pikari_frame = - 18.0f - g_drawHIO.mMiniGame.mGetInPikariAnimSpeed; + { + for (int i = 0; i < 51; i++) { + if (m_getin_info[i].bck_frame > 0.0f && m_getin_info[i].bck_frame < temp) { + if (m_getin_info[i].bck_frame < 60.0f) { + m_getin_info[i].bck_frame += g_drawHIO.mMiniGame.mGetInTextAnimSpeed; + if (m_getin_info[i].bck_frame > 60.0f) { + m_getin_info[i].bck_frame = 60.0f; + } + } else if (m_getin_info[i].bck_frame < g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f) { + m_getin_info[i].bck_frame++; + } else if (m_getin_info[i].bck_frame < temp) { + m_getin_info[i].bck_frame++; + } + } + + if (m_getin_info[i].bck_frame > 0.0f && m_getin_info[i].bck_frame < temp) { + f32 var_f29 = 1.0f; + if (m_getin_info[i].bck_frame >= g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f && + m_getin_info[i].bck_frame < temp) { + var_f29 = acc(g_drawHIO.mMiniGame.mGetInTextAlphaFrames, + temp - m_getin_info[i].bck_frame, 0); + } + + if (m_getin_info[i].bck_frame < 60.0f) { + playBckAnimation(m_getin_info[i].bck_frame); + } else { + playBckAnimation(60.0f); + } + + mpGetInParent->setAlphaRate(var_f29); + + if (g_drawHIO.mMiniGame.mGetInTextLocation == 1) { + mpGetInRoot->translate(m_getin_info[i].pos_x + g_drawHIO.mMiniGame.mGetInTextPosX, + m_getin_info[i].pos_y + g_drawHIO.mMiniGame.mGetInTextPosY); + } else { + f32 temp_f2 = m_getin_info[i].bck_frame - 40.0f; + f32 var_f3 = ((temp_f2 * 0.5f) * temp_f2) * 0.15f; + if (i == 0) { + var_f3 = 0.0f; + } + + mpGetInRoot->paneTrans( + m_getin_info[i].pos_x + g_drawHIO.mMiniGame.mGetInTextPosX, + (m_getin_info[i].pos_y + g_drawHIO.mMiniGame.mGetInTextPosY) - var_f3); + } + + mpGetInRoot->scale(g_drawHIO.mMiniGame.mGetInTextSizeX, + g_drawHIO.mMiniGame.mGetInTextSizeY); + mpGetInScreen->draw(0.0f, 0.0f, graf_ctx); + + if (m_getin_info[i].pikari_frame > 0.0f) { + drawPikari(i); + } else if (m_getin_info[i].pikari_frame == -1.0f) { + if (m_getin_info[i].field_0xc == 0) { + if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mGetInPikariAppearFrames) { + m_getin_info[i].pikari_frame = + 18.0f - g_drawHIO.mMiniGame.mGetInPikariAnimSpeed; + } + } else if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mStartPikariAppearFrames) { + m_getin_info[i].pikari_frame = + 18.0f - g_drawHIO.mMiniGame.mStartPikariAnimSpeed; } - } else if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mStartPikariAppearFrames) { - m_getin_info[i].pikari_frame = - 18.0f - g_drawHIO.mMiniGame.mStartPikariAnimSpeed; } } } diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index aa62d6a98a..24454de99f 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -64,11 +64,9 @@ bool g_enabled = false; bool g_recording = false; bool g_interpolating = false; bool g_sync_presentation = false; -uint32_t g_presentation_counter = 0; float g_step = 0.0f; -uint32_t g_pending_presentation_ui_ticks = 0; -uint32_t g_current_presentation_ui_ticks = 0; +bool g_ui_tick_pending = false; Recording g_current_recording; Recording g_previous_recording; @@ -371,11 +369,6 @@ void interpolate(float step) { interpolate_branch(old_root, g_current_recording.root, g_step); } -void notify_presentation_frame() { - ensure_initialized(); - ++g_presentation_counter; -} - void request_presentation_sync() { ensure_initialized(); if (!g_enabled) { @@ -396,33 +389,14 @@ float get_interpolation_step() { return presentation_sync_active() ? 1.0f : g_step; } -void notify_sim_tick_complete() { +void set_ui_tick_pending(bool value) { + if (g_ui_tick_pending == value) { return; } + g_ui_tick_pending = value; +} + +bool get_ui_tick_pending() { ensure_initialized(); - g_pending_presentation_ui_ticks++; -} - -uint32_t begin_presentation_ui_pass() { - ensure_initialized(); - g_current_presentation_ui_ticks = g_pending_presentation_ui_ticks; - g_pending_presentation_ui_ticks = 0; - return g_current_presentation_ui_ticks; -} - -uint32_t get_presentation_ui_advance_ticks() { - if (!s_initialized) { - return 0; - } - if (!g_enabled) { - return 1; - } - return g_current_presentation_ui_ticks; -} - -void end_presentation_ui_pass() { - if (!s_initialized) { - return; - } - g_current_presentation_ui_ticks = 0; + return g_enabled ? g_ui_tick_pending : true; } void open_child(const void* key, int32_t id) { diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index a56e448645..8d9766e12d 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -471,72 +471,32 @@ void darwFilter(GXColor matColor) { GXEnd(); } -#ifdef TARGET_PC -static void mDoGph_AdvanceFadeState() { - if (mDoGph_gInf_c::isFade() != 0) { - f32 fade_rate = mDoGph_gInf_c::getFadeRate() + mDoGph_gInf_c::getFadeSpeed(); - - if (fade_rate < 0.0f) { - fade_rate = 0.0f; - mDoGph_gInf_c::offFade(); - } else if (fade_rate > 1.0f) { - fade_rate = 1.0f; - } - - mDoGph_gInf_c::setFadeRate(fade_rate); - mDoGph_gInf_c::getFadeColor().a = 255.0f * fade_rate; - } else { - GXColor& fade_color = mDoGph_gInf_c::getFadeColor(); - if (dComIfG_getBrightness() != 255) { - fade_color.r = 0; - fade_color.g = 0; - fade_color.b = 0; - fade_color.a = 255 - dComIfG_getBrightness(); - } else { - fade_color.a = 0; - } - } -} - -static void mDoGph_AdvanceFadeState(u32 tick_count) { - for (u32 i = 0; i < tick_count; ++i) { - mDoGph_AdvanceFadeState(); - } -} - -static void mDoGph_DrawStoredFade() { - GXColor& fade_color = mDoGph_gInf_c::getFadeColor(); - if (fade_color.a != 0) { - darwFilter(fade_color); - } -} - void mDoGph_gInf_c::calcFade() { - mDoGph_AdvanceFadeState(); - mDoGph_DrawStoredFade(); -} -#else -void mDoGph_gInf_c::calcFade() { - if (mDoGph_gInf_c::mFade != 0) { - mFadeRate += mFadeSpeed; +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + if (mFade != 0) { + mFadeRate += mFadeSpeed; - if (mFadeRate < 0.0f) { - mFadeRate = 0.0f; - mDoGph_gInf_c::mFade = 0; - } else { - if (mFadeRate > 1.0f) { - mFadeRate = 1.0f; + if (mFadeRate < 0.0f) { + mFadeRate = 0.0f; + mFade = 0; + } else { + if (mFadeRate > 1.0f) { + mFadeRate = 1.0f; + } } - } - mFadeColor.a = 255.0f * mFadeRate; - } else { - if (dComIfG_getBrightness() != 255) { - mFadeColor.r = 0; - mFadeColor.g = 0; - mFadeColor.b = 0; - mFadeColor.a = 255 - dComIfG_getBrightness(); + mFadeColor.a = 255.0f * mFadeRate; } else { - mFadeColor.a = 0; + if (dComIfG_getBrightness() != 255) { + mFadeColor.r = 0; + mFadeColor.g = 0; + mFadeColor.b = 0; + mFadeColor.a = 255 - dComIfG_getBrightness(); + } else { + mFadeColor.a = 0; + } } } @@ -544,7 +504,6 @@ void mDoGph_gInf_c::calcFade() { darwFilter(mFadeColor); } } -#endif #if PLATFORM_WII || PLATFORM_SHIELD u32 mDoGph_gInf_c::csr_c::m_blurID; @@ -954,9 +913,6 @@ int mDoGph_AfterOfDraw() { JUTVideo::getManager()->setRenderMode(mDoMch_render_c::getRenderModeObj()); mDoGph_gInf_c::endFrame(); -#ifdef TARGET_PC - dusk::frame_interp::notify_sim_tick_complete(); -#endif return 1; } @@ -2114,8 +2070,6 @@ int mDoGph_Painter() { #if TARGET_PC dusk::g_imguiConsole.PreDraw(); - - const u32 pending_ui_ticks = dusk::frame_interp::begin_presentation_ui_pass(); #endif #if DEBUG @@ -2123,7 +2077,7 @@ int mDoGph_Painter() { #endif #ifdef TARGET_PC - for (u32 i = 0; i < pending_ui_ticks; ++i) + if (dusk::frame_interp::get_ui_tick_pending()) #endif { dComIfGp_particle_calcMenu(); @@ -2214,7 +2168,7 @@ int mDoGph_Painter() { view_port->height); #ifdef TARGET_PC - // Frame interpolation: Call setViewMtx earlier so that it's interpolated in time for draw_info to use it + // FRAME INTERP NOTE: Call setViewMtx earlier so that it's interpolated in time for draw_info to use it j3dSys.setViewMtx(camera_p->view.viewMtx); JPADrawInfo draw_info(j3dSys.getViewMtx(), camera_p->view.fovy, camera_p->view.aspect); #else @@ -2615,12 +2569,7 @@ int mDoGph_Painter() { if (strcmp(dComIfGp_getStartStageName(), "F_SP127") != 0 && (mDoGph_gInf_c::isFade() & 0x80) == 0) { -#ifdef TARGET_PC - mDoGph_AdvanceFadeState(pending_ui_ticks); - mDoGph_DrawStoredFade(); -#else mDoGph_gInf_c::calcFade(); -#endif } #if DEBUG @@ -2686,7 +2635,7 @@ int mDoGph_Painter() { GXSetClipMode(GX_CLIP_ENABLE); #if TARGET_PC - for (u32 i = 0; i < pending_ui_ticks; ++i) + if (dusk::frame_interp::get_ui_tick_pending()) #endif { dDlst_list_c::calcWipe(); @@ -2740,12 +2689,7 @@ int mDoGph_Painter() { if (strcmp(dComIfGp_getStartStageName(), "F_SP127") == 0 || (mDoGph_gInf_c::isFade() & 0x80) != 0) { -#ifdef TARGET_PC - mDoGph_AdvanceFadeState(pending_ui_ticks); - mDoGph_DrawStoredFade(); -#else mDoGph_gInf_c::calcFade(); -#endif } GX_DEBUG_GROUP(dComIfGp_particle_draw2DmenuFore, &draw_info3); @@ -2778,18 +2722,10 @@ int mDoGph_Painter() { #if TARGET_PC dusk::g_imguiConsole.PostDraw(); - - if (dusk::getSettings().game.enableFrameInterpolation) { - JFWDisplay::getManager()->setFaderSimSteps(pending_ui_ticks); - } #endif mDoGph_gInf_c::endRender(); -#ifdef TARGET_PC - dusk::frame_interp::end_presentation_ui_pass(); -#endif - #if WIDESCREEN_SUPPORT mDoGph_gInf_c::offWideZoom(); #endif diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 784aea3f26..7f2cf5ccdd 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -248,8 +248,8 @@ void main01(void) { } if (dusk::getSettings().game.enableFrameInterpolation && !dusk::getTransientSettings().skipFrameRateLimit) { - dusk::frame_interp::notify_presentation_frame(); if (accumulator >= kSimStepSeconds) { + dusk::frame_interp::set_ui_tick_pending(true); mDoCPd_c::read(); dusk::gyro::read(kSimStepSeconds); fapGm_Execute(); @@ -261,7 +261,9 @@ void main01(void) { dusk::frame_interp::PresentationCameraScope presentation_camera; cAPIGph_Painter(); } + dusk::frame_interp::set_ui_tick_pending(false); } else { + dusk::frame_interp::set_ui_tick_pending(true); accumulator = 0.0f; // Game Inputs From 3a538d45cf659b2332d16e65db3c5269af3e9d44 Mon Sep 17 00:00:00 2001 From: Irastris Date: Fri, 17 Apr 2026 23:11:54 -0400 Subject: [PATCH 02/68] Frame Interp: Game Clock Refactor & Ring Item Selection --- files.cmake | 1 + include/d/d_menu_ring.h | 3 ++ include/dusk/frame_interpolation.h | 1 + include/dusk/game_clock.h | 32 +++++++++++++ src/d/d_menu_ring.cpp | 33 +++++++++++++ src/dusk/game_clock.cpp | 76 ++++++++++++++++++++++++++++++ src/m_Do/m_Do_main.cpp | 26 ++++------ 7 files changed, 156 insertions(+), 16 deletions(-) create mode 100644 include/dusk/game_clock.h create mode 100644 src/dusk/game_clock.cpp diff --git a/files.cmake b/files.cmake index ec7d097e21..d43e98bf42 100644 --- a/files.cmake +++ b/files.cmake @@ -1346,6 +1346,7 @@ set(DUSK_FILES src/dusk/file_select.cpp src/dusk/file_select.hpp src/dusk/frame_interpolation.cpp + src/dusk/game_clock.cpp src/dusk/globals.cpp src/dusk/gyro.cpp src/dusk/io.cpp diff --git a/include/d/d_menu_ring.h b/include/d/d_menu_ring.h index f28c20aac2..39d29a35e8 100644 --- a/include/d/d_menu_ring.h +++ b/include/d/d_menu_ring.h @@ -204,6 +204,9 @@ private: /* 0x6D1 */ u8 field_0x6d1; /* 0x6D2 */ u8 field_0x6d2; /* 0x6D3 */ u8 field_0x6d3; +#if TARGET_PC + f32 mSelectItemSlideElapsed[4]; +#endif }; #endif /* D_MENU_D_MENU_RING_H */ diff --git a/include/dusk/frame_interpolation.h b/include/dusk/frame_interpolation.h index 7cd98eebe4..9594529feb 100644 --- a/include/dusk/frame_interpolation.h +++ b/include/dusk/frame_interpolation.h @@ -22,6 +22,7 @@ float get_interpolation_step(); void request_presentation_sync(); bool presentation_sync_active(); +// TODO: These should be phased out as UI is progressively updated to use game_clock void set_ui_tick_pending(bool value); bool get_ui_tick_pending(); diff --git a/include/dusk/game_clock.h b/include/dusk/game_clock.h new file mode 100644 index 0000000000..6e7ea500eb --- /dev/null +++ b/include/dusk/game_clock.h @@ -0,0 +1,32 @@ +#ifndef DUSK_GAME_CLOCK_H +#define DUSK_GAME_CLOCK_H + +#include + +namespace dusk { +namespace game_clock { + +void ensure_initialized(); +void reset_accumulator(); + +constexpr float sim_pace() { return 1.0f / 30.0f; } +constexpr float period_for_original_frames(float frame_count) { return frame_count * sim_pace(); } +constexpr float ui_maximum_dt() { return 0.05f; } +constexpr float ui_initial_dt() { return 1.0f / 60.0f; } + +struct MainLoopPacer { + float presentation_dt_seconds; + bool is_interpolating; + bool do_sim_tick; + float interpolation_step; + float sim_pace; +}; + +MainLoopPacer advance_main_loop(); + +float consume_interval(const void* consumer); + +} // namespace game_clock +} // namespace dusk + +#endif // DUSK_GAME_CLOCK_H diff --git a/src/d/d_menu_ring.cpp b/src/d/d_menu_ring.cpp index 8e5b3cacd2..529597248a 100644 --- a/src/d/d_menu_ring.cpp +++ b/src/d/d_menu_ring.cpp @@ -29,6 +29,10 @@ #include +#if TARGET_PC +#include "dusk/game_clock.h" +#endif + typedef void (dMenu_Ring_c::*initFunc)(); static initFunc stick_init[] = { /* STATUS_WAIT */ &dMenu_Ring_c::stick_wait_init, @@ -183,6 +187,9 @@ dMenu_Ring_c::dMenu_Ring_c(JKRExpHeap* i_heap, STControl* i_stick, CSTControl* i } for (int i = 0; i < 4; i++) { field_0x674[i] = 0; +#if TARGET_PC + mSelectItemSlideElapsed[i] = 0.0f; +#endif field_0x518[i] = 0.0f; field_0x528[i] = 0.0f; field_0x538[i] = 0.0f; @@ -1022,6 +1029,9 @@ void dMenu_Ring_c::setJumpItem(bool i_useVibrationM) { field_0x6b8[0] != dComIfGs_getMixItemIndex(0)) { field_0x674[0] = 1; +#if TARGET_PC + mSelectItemSlideElapsed[0] = 0.0f; +#endif } } else if (field_0x6b3 == 1) { field_0x538[0] = g_ringHIO.mUnselectItemScale; @@ -1030,6 +1040,9 @@ void dMenu_Ring_c::setJumpItem(bool i_useVibrationM) { field_0x6b8[1] != dComIfGs_getMixItemIndex(1)) { field_0x674[1] = 1; +#if TARGET_PC + mSelectItemSlideElapsed[1] = 0.0f; +#endif } } if (field_0x674[0] == 1) { @@ -1520,7 +1533,15 @@ void dMenu_Ring_c::setSelectItem(int i_idx, u8 i_itemNo) { void dMenu_Ring_c::drawSelectItem() { for (int i = 0; i < 4; i++) { if (field_0x674[i] != 0) { +#if TARGET_PC + mSelectItemSlideElapsed[i] += dusk::game_clock::consume_interval(this); + const f32 u = std::min(mSelectItemSlideElapsed[i] / dusk::game_clock::period_for_original_frames(10.0f), 1.0f); + if (u >= 1.0f) { + setSelectItemForce(i); + } else { +#else if (field_0x674[i] < 10) { +#endif f32 initSizeX = dMeter2Info_getMeterItemPanePtr(i)->getInitSizeX() * 1.7f; f32 initSizeY = dMeter2Info_getMeterItemPanePtr(i)->getInitSizeY() * 1.7f; f32 initScaleX = dMeter2Info_getMeterItemPanePtr(i)->getInitScaleX(); @@ -1528,7 +1549,11 @@ void dMenu_Ring_c::drawSelectItem() { Vec pos = dMeter2Info_getMeterItemPanePtr(i)->getGlobalVtxCenter( dMeter2Info_getMeterItemPanePtr(i)->mPane, true, 0); +#if TARGET_PC + f32 fVar14 = 0.1f + 0.8f * u; +#else f32 fVar14 = field_0x674[i] / 10.0f; +#endif if (field_0x6cd != 0xff) { fVar14 = 1.0f - fVar14; } @@ -1549,9 +1574,11 @@ void dMenu_Ring_c::drawSelectItem() { 0); } } +#if !TARGET_PC field_0x674[i]++; } else { setSelectItemForce(i); +#endif } } } @@ -1562,6 +1589,9 @@ void dMenu_Ring_c::setSelectItemForce(int i_idx) { if (field_0x674[i_idx] != 0) { dComIfGs_setSelectItemIndex(i_idx, field_0x6b4[i_idx]); field_0x674[i_idx] = 0; +#if TARGET_PC + mSelectItemSlideElapsed[i_idx] = 0.0f; +#endif } } else if (field_0x674[i_idx] != 0) { for (int i = 0; i < 2; i++) { @@ -1569,6 +1599,9 @@ void dMenu_Ring_c::setSelectItemForce(int i_idx) { dComIfGs_setSelectItemIndex(i, field_0x6b4[i]); } field_0x674[i_idx] = 0; +#if TARGET_PC + mSelectItemSlideElapsed[i_idx] = 0.0f; +#endif } } diff --git a/src/dusk/game_clock.cpp b/src/dusk/game_clock.cpp new file mode 100644 index 0000000000..176c43c3b7 --- /dev/null +++ b/src/dusk/game_clock.cpp @@ -0,0 +1,76 @@ +#include "dusk/game_clock.h" + +#include +#include +#include + +namespace dusk { +namespace game_clock { + +using clock = std::chrono::steady_clock; + +bool s_initialized = false; +clock::time_point s_previous_sample{}; +float s_sim_accumulator = 0.0f; + +std::unordered_map s_interval_last_sample; + +void ensure_initialized() { + if (s_initialized) { + return; + } + s_previous_sample = clock::now(); + s_sim_accumulator = sim_pace(); + s_initialized = true; +} + +void reset_accumulator() { + ensure_initialized(); + s_sim_accumulator = 0.0f; +} + +MainLoopPacer advance_main_loop() { + ensure_initialized(); + + const clock::time_point now = clock::now(); + const float presentation_dt = std::chrono::duration(now - s_previous_sample).count(); + s_previous_sample = now; + + s_sim_accumulator += presentation_dt; + + MainLoopPacer out{}; + out.presentation_dt_seconds = presentation_dt; + + const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation && !dusk::getTransientSettings().skipFrameRateLimit; + out.is_interpolating = should_interpolate; + out.sim_pace = sim_pace(); + + if (!should_interpolate) { + s_sim_accumulator = 0.0f; + out.do_sim_tick = true; + out.interpolation_step = 0.0f; + return out; + } else { + out.do_sim_tick = s_sim_accumulator >= sim_pace(); + out.interpolation_step = out.do_sim_tick ? 0.0f : s_sim_accumulator / sim_pace(); + return out; + } +} + +float consume_interval(const void* consumer) { + ensure_initialized(); + const uintptr_t key = reinterpret_cast(consumer); + const clock::time_point now = clock::now(); + + float dt = ui_initial_dt(); + const auto it = s_interval_last_sample.find(key); + if (it != s_interval_last_sample.end()) { + dt = std::chrono::duration(now - it->second).count(); + dt = std::min(dt, ui_maximum_dt()); + } + s_interval_last_sample[key] = now; + return dt; +} + +} // namespace game_clock +} // namespace dusk diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 7f2cf5ccdd..e15bc01784 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -42,7 +42,6 @@ #include "SSystem/SComponent/c_counter.h" #include -#include #include #include #include @@ -51,6 +50,7 @@ #include "dusk/crash_reporting.h" #include "dusk/dusk.h" #include "dusk/frame_interpolation.h" +#include "dusk/game_clock.h" #include "dusk/gyro.h" #include "dusk/imgui/ImGuiEngine.hpp" #include "dusk/logging.h" @@ -205,9 +205,7 @@ void main01(void) { if (preLaunchUIWindowSize.width != 0) mDoGph_gInf_c::setWindowSize(preLaunchUIWindowSize); - constexpr float kSimStepSeconds = 1.0 / 30.0; - auto previous_time = std::chrono::steady_clock::now(); - float accumulator = kSimStepSeconds; + dusk::game_clock::ensure_initialized(); do { // 1. Update Window Events @@ -234,10 +232,7 @@ void main01(void) { eventsDone:; - auto current_time = std::chrono::steady_clock::now(); - float frame_seconds = std::chrono::duration(current_time - previous_time).count(); - previous_time = current_time; - accumulator += frame_seconds; + const dusk::game_clock::MainLoopPacer pacing = dusk::game_clock::advance_main_loop(); VIWaitForRetrace(); @@ -247,16 +242,16 @@ void main01(void) { continue; } - if (dusk::getSettings().game.enableFrameInterpolation && !dusk::getTransientSettings().skipFrameRateLimit) { - if (accumulator >= kSimStepSeconds) { + if (pacing.is_interpolating) { + if (pacing.do_sim_tick) { dusk::frame_interp::set_ui_tick_pending(true); mDoCPd_c::read(); - dusk::gyro::read(kSimStepSeconds); + dusk::gyro::read(pacing.sim_pace); fapGm_Execute(); mDoAud_Execute(); - accumulator = 0.0f; + dusk::game_clock::reset_accumulator(); } - dusk::frame_interp::interpolate(static_cast(accumulator / kSimStepSeconds)); + dusk::frame_interp::interpolate(pacing.interpolation_step); { dusk::frame_interp::PresentationCameraScope presentation_camera; cAPIGph_Painter(); @@ -264,11 +259,10 @@ void main01(void) { dusk::frame_interp::set_ui_tick_pending(false); } else { dusk::frame_interp::set_ui_tick_pending(true); - accumulator = 0.0f; - + // Game Inputs mDoCPd_c::read(); - dusk::gyro::read(frame_seconds); + dusk::gyro::read(pacing.presentation_dt_seconds); // EXECUTE GAME LOGIC & RENDER // This calls mDoGph_Painter -> JFWDisplay -> GX Functions From 3d9dfbd447d49d6faf92f4a7cae9bd98c97e37f9 Mon Sep 17 00:00:00 2001 From: Irastris Date: Sat, 18 Apr 2026 00:46:00 -0400 Subject: [PATCH 03/68] Add "Open Data Folder" to Game menu --- src/dusk/imgui/ImGuiMenuGame.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index b0c20b4d46..aa96ac421f 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -17,10 +17,34 @@ #include "m_Do/m_Do_graphic.h" #include +#include #include +#include #include "dusk/main.h" +#if defined(__APPLE__) +#include +#endif + +#if defined(_WIN32) || (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || (defined(__linux__) && !defined(__ANDROID__)) +#define DUSK_CAN_OPEN_DATA_FOLDER 1 + +namespace fs = std::filesystem; + +static void OpenDataFolder() { + const std::string path = fs::absolute(fs::path(aurora::g_config.configPath)).generic_string(); +#if defined(_WIN32) + const std::string url = std::string("file:///") + path; +#else + const std::string url = std::string("file://") + path; +#endif + (void)SDL_OpenURL(url.c_str()); +} +#else +#define DUSK_CAN_OPEN_DATA_FOLDER 0 +#endif + namespace dusk { void ImGuiMenuGame::ToggleFullscreen() { getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen); @@ -146,6 +170,14 @@ namespace dusk { ImGui::Separator(); +#if DUSK_CAN_OPEN_DATA_FOLDER + if (ImGui::MenuItem("Open Data Folder")) { + OpenDataFolder(); + } +#endif + + ImGui::Separator(); + if (ImGui::MenuItem("Reset", hotkeys::DO_RESET)) { JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; } From 5c20f527ac016bb9b4168dba78d8fad3053189cd Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 17 Apr 2026 23:11:43 -0600 Subject: [PATCH 04/68] Widescreen rework & IR scaling --- extern/aurora | 2 +- include/dusk/settings.h | 1 + include/m_Do/m_Do_graphic.h | 7 +- .../include/JSystem/JUtility/JUTVideo.h | 25 ++-- libs/JSystem/src/J2DGraph/J2DGrafContext.cpp | 5 - libs/JSystem/src/JUtility/JUTVideo.cpp | 5 +- src/d/actor/d_a_movie_player.cpp | 5 - src/d/d_file_select.cpp | 6 - src/d/d_gameover.cpp | 7 +- src/d/d_map_path.cpp | 23 +--- src/d/d_menu_collect.cpp | 5 - src/d/d_menu_dmap.cpp | 30 +---- src/d/d_menu_fmap2D.cpp | 21 +-- src/d/d_menu_letter.cpp | 5 - src/d/d_menu_option.cpp | 8 -- src/d/d_menu_window.cpp | 22 ++-- src/d/d_msg_scrn_howl.cpp | 21 --- src/d/d_ovlp_fade2.cpp | 5 - src/d/d_ovlp_fade3.cpp | 26 +--- src/d/d_pane_class.cpp | 6 +- src/d/d_s_play.cpp | 5 - src/dusk/imgui/ImGuiFirstRunPreset.cpp | 2 +- src/dusk/imgui/ImGuiMenuGame.cpp | 34 ++++- src/dusk/settings.cpp | 2 + src/dusk/stubs.cpp | 8 -- src/m_Do/m_Do_graphic.cpp | 123 +++++++----------- src/m_Do/m_Do_main.cpp | 18 +-- 27 files changed, 129 insertions(+), 298 deletions(-) diff --git a/extern/aurora b/extern/aurora index aa83f6d915..62c7ef68e8 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit aa83f6d91545c304b8f62a2965c7dcd1ec8b511b +Subproject commit 62c7ef68e895ea91c5389f381bd78e163f480d58 diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 2676c461cd..cf0ac0a3ef 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -73,6 +73,7 @@ struct UserSettings { ConfigVar bloomMultiplier; ConfigVar enableWaterRefraction; ConfigVar enableFrameInterpolation; + ConfigVar internalResolutionScale; ConfigVar shadowResolutionMultiplier; // Audio diff --git a/include/m_Do/m_Do_graphic.h b/include/m_Do/m_Do_graphic.h index f921912656..5d29049520 100644 --- a/include/m_Do/m_Do_graphic.h +++ b/include/m_Do/m_Do_graphic.h @@ -286,12 +286,7 @@ public: #if WIDESCREEN_SUPPORT static void setTvSize(); - #if TARGET_PC - static void onWide(f32 width, f32 height); - #else static void onWide(); - #endif - static void offWide(); static u8 isWide(); @@ -304,7 +299,7 @@ public: #endif #if TARGET_PC - static void setWindowSize(AuroraWindowSize const& size); + static void updateRenderSize(); #endif static TGXTexObj mFrameBufferTexObj; diff --git a/libs/JSystem/include/JSystem/JUtility/JUTVideo.h b/libs/JSystem/include/JSystem/JUtility/JUTVideo.h index 9a23a4dc7e..dfc6fd0b6c 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTVideo.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTVideo.h @@ -33,24 +33,16 @@ public: static void postRetraceProc(u32); static void drawDoneCallback(); - u16 getFbWidth() const { - #if TARGET_PC - return m_WindowSize.fb_width; - #else - return mRenderObj->fbWidth; - #endif - } - u16 getEfbHeight() const { - #if TARGET_PC - return m_WindowSize.fb_height; - #else - return mRenderObj->efbHeight; - #endif - } + u16 getFbWidth() const { return mRenderObj->fbWidth; } + u16 getEfbHeight() const { return mRenderObj->efbHeight; } void getBounds(u16& width, u16& height) const { width = (u16)getFbWidth(); height = (u16)getEfbHeight(); } +#ifdef TARGET_PC + u32 getRenderWidth() const { return mRenderWidth; } + u32 getRenderHeight() const { return mRenderHeight; } +#endif u16 getXfbHeight() const { return u16(mRenderObj->xfbHeight); } u8 isAntiAliasing() const { return u8(mRenderObj->aa); } Pattern getSamplePattern() const { return mRenderObj->sample_pattern; } @@ -63,7 +55,7 @@ public: GXRenderModeObj* getRenderMode() const { return mRenderObj; } #if TARGET_PC - void setWindowSize(AuroraWindowSize const& size); + void setRenderSize(u32 width, u32 height); #endif private: @@ -89,7 +81,8 @@ private: #if TARGET_PC public: - AuroraWindowSize m_WindowSize; + u32 mRenderWidth; + u32 mRenderHeight; #endif }; diff --git a/libs/JSystem/src/J2DGraph/J2DGrafContext.cpp b/libs/JSystem/src/J2DGraph/J2DGrafContext.cpp index 36f54a23c6..3ac3e5d197 100644 --- a/libs/JSystem/src/J2DGraph/J2DGrafContext.cpp +++ b/libs/JSystem/src/J2DGraph/J2DGrafContext.cpp @@ -64,10 +64,6 @@ void J2DGrafContext::setup2D() { } void J2DGrafContext::setScissor() { -#if TARGET_PC - GXSetScissor(mScissorBounds.i.x, mScissorBounds.i.y, mScissorBounds.getWidth(), - mScissorBounds.getHeight()); -#else JGeometry::TBox2 bounds(0, 0, 1024, 1024); JGeometry::TBox2 curBounds(mScissorBounds); mScissorBounds.intersect(bounds); @@ -81,7 +77,6 @@ void J2DGrafContext::setScissor() { } else { GXSetScissor(0, 0, 0, 0); } -#endif } void J2DGrafContext::scissor(JGeometry::TBox2 const& bounds) { diff --git a/libs/JSystem/src/JUtility/JUTVideo.cpp b/libs/JSystem/src/JUtility/JUTVideo.cpp index f718fcac23..5586342f0e 100644 --- a/libs/JSystem/src/JUtility/JUTVideo.cpp +++ b/libs/JSystem/src/JUtility/JUTVideo.cpp @@ -205,7 +205,8 @@ void JUTVideo::setRenderMode(GXRenderModeObj const* pObj) { void JUTVideo::waitRetraceIfNeed() {} #if TARGET_PC -void JUTVideo::setWindowSize(AuroraWindowSize const& size) { - m_WindowSize = size; +void JUTVideo::setRenderSize(u32 width, u32 height) { + mRenderWidth = width; + mRenderHeight = height; } #endif diff --git a/src/d/actor/d_a_movie_player.cpp b/src/d/actor/d_a_movie_player.cpp index c3074ac877..4cc7313267 100644 --- a/src/d/actor/d_a_movie_player.cpp +++ b/src/d/actor/d_a_movie_player.cpp @@ -3342,13 +3342,8 @@ static void daMP_THPGXYuv2RgbSetup(const GXRenderModeObj* rmode) { Mtx44 m; Mtx e_m; -#if TARGET_PC - w = JUTVideo::getManager()->getFbWidth(); - h = JUTVideo::getManager()->getEfbHeight(); -#else w = rmode->fbWidth; h = rmode->efbHeight; -#endif var_f31 = 0.0f; #if WIDESCREEN_SUPPORT diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 550c5b3e52..99a777929c 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -3870,16 +3870,10 @@ void dFile_select_c::_draw() { dComIfGd_set2DOpa(mSelIcon2); #if PLATFORM_GCN - #if TARGET_PC - mpFadePict->draw(0, 0, - mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight(), false, false, - false); - #else mpFadePict->draw(mDoGph_gInf_c::getMinXF(), mDoGph_gInf_c::getMinYF(), mDoGph_gInf_c::getWidthF(), mDoGph_gInf_c::getHeightF(), false, false, false); #endif - #endif } } diff --git a/src/d/d_gameover.cpp b/src/d/d_gameover.cpp index baec970a32..870fb00f14 100644 --- a/src/d/d_gameover.cpp +++ b/src/d/d_gameover.cpp @@ -37,11 +37,10 @@ void dDlst_Gameover_CAPTURE_c::draw() { TGXTexObj tex_obj; Mtx44 m; -#if TARGET_PC - GXSetTexCopySrc(0, 0, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight()); - GXSetTexCopyDst(mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight(), GX_TF_RGB565, GX_TRUE); -#else GXSetTexCopySrc(0, 0, FB_WIDTH, FB_HEIGHT); +#if TARGET_PC + GXSetTexCopyDst(FB_WIDTH, FB_HEIGHT, GX_TF_RGB565, GX_FALSE); +#else GXSetTexCopyDst(FB_WIDTH / 2, FB_HEIGHT / 2, GX_TF_RGB565, GX_TRUE); #endif GXCopyTex(mDoGph_gInf_c::mZbufferTex, 0); diff --git a/src/d/d_map_path.cpp b/src/d/d_map_path.cpp index a31ccdedfd..916bd2c223 100644 --- a/src/d/d_map_path.cpp +++ b/src/d/d_map_path.cpp @@ -16,10 +16,6 @@ #ifdef TARGET_PC constexpr u16 kMapResolutionMultiplier = 4; -// Line widths are relative to the framebuffer size. Since we're rendering to a separate -// framebuffer, we have to scale them accordingly. The original game used about half of the -// EFB for the map rendering, so this is a reasonable approximation. -constexpr u8 kMapLineWidthMultiplier = 2; #endif void dMpath_n::dTexObjAggregate_c::create() { @@ -242,11 +238,7 @@ void dDrawPath_c::rendering(dDrawPath_c::line_class const* p_line) { int width = getLineWidth(p_line->field_0x1); if (width > 0 && p_line->mDataNum >= 2) { -#ifdef TARGET_PC - GXSetLineWidth(width * kMapLineWidthMultiplier, GX_TO_ZERO); -#else - GXSetLineWidth(width * 2, GX_TO_ZERO); -#endif + GXSetLineWidth(width, GX_TO_ZERO); GXSetTevColor(GX_TEVREG0, *getLineColor(p_line->field_0x0 & 0x3F, p_line->field_0x1)); GXBegin(GX_LINESTRIP, GX_VTXFMT0, p_line->mDataNum); @@ -435,8 +427,12 @@ void dRenderingFDAmap_c::preRenderingMap() { const u16 w = mTexWidth * kMapResolutionMultiplier; const u16 h = mTexHeight * kMapResolutionMultiplier; GXCreateFrameBuffer(w, h); - GXSetViewport(0.0f, 0.0f, w, h, 0.0f, 1.0f); - GXSetScissor(0, 0, w, h); + // Set logical viewport dimensions + GXSetViewport(0.0f, 0.0f, mTexWidth, mTexHeight, 0.0f, 1.0f); + GXSetScissor(0, 0, mTexWidth, mTexHeight); + // Set render viewport dimensions + GXSetViewportRender(0.0f, 0.0f, w, h, 0.0f, 1.0f); + GXSetScissorRender(0, 0, w, h); #else GXSetViewport(0.0f, 0.0f, mTexWidth, mTexHeight, 0.0f, 1.0f); GXSetScissor(0, 0, mTexWidth, mTexHeight); @@ -517,13 +513,8 @@ void dRenderingFDAmap_c::renderingDecoration(dDrawPath_c::line_class const* p_li BE(u16)* data_p = p_line->mpData; s32 data_num = p_line->mDataNum; -#ifdef TARGET_PC - GXSetLineWidth(width * kMapLineWidthMultiplier, GX_TO_ZERO); - GXSetPointSize(width * kMapLineWidthMultiplier, GX_TO_ONE); -#else GXSetLineWidth(width, GX_TO_ONE); GXSetPointSize(width, GX_TO_ONE); -#endif GXColor lineColor = *getDecoLineColor(p_line->field_0x0 & 0x3f, p_line->field_0x1); GXSetTevColor(GX_TEVREG0, lineColor); lineColor.r = lineColor.r - 4; diff --git a/src/d/d_menu_collect.cpp b/src/d/d_menu_collect.cpp index 7ae416c4be..bb015f01b2 100644 --- a/src/d/d_menu_collect.cpp +++ b/src/d/d_menu_collect.cpp @@ -2688,12 +2688,7 @@ u8 dMenu_Collect3D_c::getMaskMdlVisible() { f32 dMenu_Collect3D_c::mViewOffsetY = -100.0f; void dMenu_Collect3D_c::setupItem3D(Mtx param_0) { -#if TARGET_PC - f32 scaleFactor = mDoGph_gInf_c::getHeight() / FB_HEIGHT; - GXSetViewport(0.0f, mViewOffsetY * scaleFactor, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight(), 0.0f, 1.0f); -#else GXSetViewport(0.0f, mViewOffsetY, FB_WIDTH, FB_HEIGHT, 0.0f, 1.0f); -#endif mViewOffsetY = -100.0f; Mtx44 projection; C_MTXPerspective(projection, 45.0f, mDoGph_gInf_c::getAspect(), 1.0f, 100000.0f); diff --git a/src/d/d_menu_dmap.cpp b/src/d/d_menu_dmap.cpp index 739dfc7843..66533dd12c 100644 --- a/src/d/d_menu_dmap.cpp +++ b/src/d/d_menu_dmap.cpp @@ -864,33 +864,15 @@ void dMenu_DmapBg_c::draw() { J2DOrthoGraph* grafContext = (J2DOrthoGraph*)dComIfGp_getCurrentGrafPort(); grafContext->setup2D(); -#if TARGET_PC - // GXGetScissor uses 11-bit GC register fields (max 2047) which overflow - // at window widths > ~1705px, producing garbage values on restore. - scissor_left = 0; - scissor_top = 0; - scissor_width = (u32)mDoGph_gInf_c::getWidth(); - scissor_height = (u32)mDoGph_gInf_c::getHeight(); -#else GXGetScissor(&scissor_left, &scissor_top, &scissor_width, &scissor_height); -#endif -#if TARGET_PC - grafContext->scissor(field_0xd94, 0.0f, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight()); -#else - grafContext->scissor(field_0xd94, 0.0f, FB_WIDTH, FB_HEIGHT); -#endif + grafContext->scissor(field_0xd94, 0.0f, FB_WIDTH, FB_HEIGHT); grafContext->setScissor(); mBaseScreen->draw(field_0xd94, field_0xd98, grafContext); dMenu_Dmap_c::myclass->drawFloorScreenBack(mFloorScreen, field_0xd94, field_0xd98, grafContext); -#if TARGET_PC - f32 dVar21 = mDoGph_gInf_c::getWidthF() / mDoGph_gInf_c::getWidth(); - f32 dVar16 = mDoGph_gInf_c::getHeightF() / mDoGph_gInf_c::getHeight(); -#else f32 dVar21 = mDoGph_gInf_c::getWidthF() / FB_WIDTH; f32 dVar16 = mDoGph_gInf_c::getHeightF() / FB_HEIGHT; -#endif mMapScreen[0]->draw(field_0xd94, field_0xd98, grafContext); if (mpBackTexture != NULL) { @@ -922,15 +904,9 @@ void dMenu_DmapBg_c::draw() { mpBackTexture->draw(local_28c, field_0xd94 + mpBackTexture->getBounds().i.y, mpBackTexture->getWidth(), mpBackTexture->getHeight(), false, false, false); -#if TARGET_PC - grafContext->scissor(field_0xd94, - 0, mDoGph_gInf_c::getWidth(), - scissor_height); -#else grafContext->scissor(field_0xd94 + mDoGph_gInf_c::getMinXF(), scissor_top, mDoGph_gInf_c::getWidthF(), scissor_height); -#endif grafContext->setScissor(); } @@ -957,11 +933,7 @@ void dMenu_DmapBg_c::draw() { Vec local_26c = pane.getGlobalVtx(mMapPane, &local_110, 0, false, 0); drawIcon(local_26c.x + field_0xd94, local_26c.y, field_0xda8, 1.0f); -#if TARGET_PC - grafContext->scissor(field_0xd94, scissor_top, mDoGph_gInf_c::getWidth(), scissor_height); -#else grafContext->scissor(field_0xd94 + mDoGph_gInf_c::getMinXF(), scissor_top, mDoGph_gInf_c::getWidthF(), scissor_height); -#endif grafContext->setScissor(); grafContext->scissor(scissor_left, scissor_top, scissor_width, scissor_height); grafContext->setScissor(); diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index 558ad05d6d..eacd2d04a8 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -276,18 +276,14 @@ void dMenu_Fmap2DBack_c::draw() { u32 scissorLeft, scissorTop, scissorWidth, scissorHeight; GXGetScissor(&scissorLeft, &scissorTop, &scissorWidth, &scissorHeight); -#if TARGET_PC - grafPort->scissor(mTransX, 0.0f, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight()); -#else grafPort->scissor(mTransX, 0.0f, FB_WIDTH, FB_HEIGHT); -#endif grafPort->setScissor(); mpBackTex->setBlackWhite(field_0x1208, field_0x120c); mpBackTex->setAlpha(mAlphaRate * 255.0f * g_fmapHIO.mBackgroundAlpha); mpBackTex->draw(mTransX + mDoGph_gInf_c::getMinXF(), - mTransZ + mDoGph_gInf_c::getMinYF(), mDoGph_gInf_c::getWidth(), - mDoGph_gInf_c::getHeight(), false, false, false); + mTransZ + mDoGph_gInf_c::getMinYF(), mDoGph_gInf_c::getWidthF(), + mDoGph_gInf_c::getHeightF(), false, false, false); mpBackScreen->draw(mTransX, mTransZ, grafPort); mpBaseScreen->draw(mTransX, mTransZ, grafPort); @@ -297,13 +293,8 @@ void dMenu_Fmap2DBack_c::draw() { Vec vec2 = mpMapArea->getGlobalVtx(&mtx, 3, false, 0); -#if TARGET_PC - f32 width = mDoGph_gInf_c::getWidthF() / mDoGph_gInf_c::getWidth(); - f32 height = mDoGph_gInf_c::getHeightF() / mDoGph_gInf_c::getHeight(); -#else f32 width = mDoGph_gInf_c::getWidthF() / FB_WIDTH; f32 height = mDoGph_gInf_c::getHeightF() / FB_HEIGHT; -#endif grafPort->scissor(mTransX + ((vec1.x - mDoGph_gInf_c::getMinXF()) / width), mTransZ + (vec1.y / height), (vec2.x - vec1.x) / width, @@ -360,11 +351,7 @@ void dMenu_Fmap2DBack_c::draw() { drawDebugRegionArea(); } -#if TARGET_PC - grafPort->scissor(scissorLeft, scissorTop, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight()); -#else grafPort->scissor(scissorLeft, scissorTop, scissorWidth, scissorHeight); -#endif grafPort->setScissor(); if (isArrowDrawFlag()) { @@ -2580,11 +2567,7 @@ void dMenu_Fmap2DTop_c::draw() { J2DOrthoGraph* ctx = static_cast(dComIfGp_getCurrentGrafPort()); ctx->setup2D(); GXGetScissor(&scissor_left, &scissor_top, &scissor_width, &scissor_height); -#if TARGET_PC - ctx->scissor(mTransX, 0.0f, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight()); -#else ctx->scissor(mTransX, 0.0f, FB_WIDTH, FB_HEIGHT); -#endif ctx->setScissor(); mpTitleScreen->draw(mTransX, mTransY, ctx); ctx->scissor(scissor_left, scissor_top, scissor_width, scissor_height); diff --git a/src/d/d_menu_letter.cpp b/src/d/d_menu_letter.cpp index cef018372f..a0155ef6b4 100644 --- a/src/d/d_menu_letter.cpp +++ b/src/d/d_menu_letter.cpp @@ -223,13 +223,8 @@ void dMenu_Letter_c::_draw() { f32 y1 = local_178.y; Vec local_184; local_184 = afStack_138.getGlobalVtx(field_0x1ec, &mtx, 3, false, 0); -#if TARGET_PC - f32 dVar17 = mDoGph_gInf_c::getWidthF() / mDoGph_gInf_c::getWidth(); - f32 dVar16 = mDoGph_gInf_c::getHeightF() / mDoGph_gInf_c::getHeight(); -#else f32 dVar17 = mDoGph_gInf_c::getWidthF() / FB_WIDTH; f32 dVar16 = mDoGph_gInf_c::getHeightF() / FB_HEIGHT; -#endif f32 fVar1 = (x1 - mDoGph_gInf_c::getMinXF()) / dVar17; f32 fVar2 = y1 / dVar16; grafContext->scissor(fVar1, fVar2, diff --git a/src/d/d_menu_option.cpp b/src/d/d_menu_option.cpp index 9384bc06b9..0898ff29af 100644 --- a/src/d/d_menu_option.cpp +++ b/src/d/d_menu_option.cpp @@ -555,19 +555,11 @@ void dMenu_Option_c::_draw() { #endif mpBlackTex->setAlpha(0xff); -#if TARGET_PC - mpBlackTex->draw(mDoGph_gInf_c::getMinXF(), mDoGph_gInf_c::getMinYF(), mDoGph_gInf_c::getWidthF(), mDoGph_gInf_c::getHeightF(), 0, 0, 0); -#else mpBlackTex->draw(0.0f, 0.0f, FB_WIDTH, FB_HEIGHT, 0, 0, 0); -#endif mpBackScreen->draw(0.0f, 0.0f, ctx); f32 alpha = (f32)g_drawHIO.mOptionScreen.mBackgroundAlpha * (f32)field_0x374; mpBlackTex->setAlpha(alpha); -#if TARGET_PC - mpBlackTex->draw(mDoGph_gInf_c::getMinXF(), mDoGph_gInf_c::getMinYF(), mDoGph_gInf_c::getWidthF(), mDoGph_gInf_c::getHeightF(), 0, 0, 0); -#else mpBlackTex->draw(0.0f, 0.0f, FB_WIDTH, FB_HEIGHT, 0, 0, 0); -#endif mpScreen->draw(0.0f, 0.0f, ctx); mpClipScreen->draw(0.0f, 0.0f, ctx); #if TARGET_PC diff --git a/src/d/d_menu_window.cpp b/src/d/d_menu_window.cpp index af8a734fe7..6d424466e5 100644 --- a/src/d/d_menu_window.cpp +++ b/src/d/d_menu_window.cpp @@ -32,15 +32,9 @@ public: if (getDrawFlag() == 1) { setDrawFlag(); dComIfGp_onPauseFlag(); - - #if TARGET_PC - GXSetTexCopySrc(0, 0, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight()); - #else GXSetTexCopySrc(0, 0, FB_WIDTH, FB_HEIGHT); - #endif - #if TARGET_PC - GXSetTexCopyDst(mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight(), (GXTexFmt)mDoGph_gInf_c::getFrameBufferTimg()->format, GX_ENABLE); + GXSetTexCopyDst(FB_WIDTH, FB_HEIGHT, (GXTexFmt)mDoGph_gInf_c::getFrameBufferTimg()->format, GX_DISABLE); #else GXSetTexCopyDst(FB_WIDTH / 2, FB_HEIGHT / 2, (GXTexFmt)mDoGph_gInf_c::getFrameBufferTimg()->format, GX_ENABLE); #endif @@ -48,17 +42,17 @@ public: GXPixModeSync(); #if TARGET_PC // init mTexObj at capture time so the gpu ref survives window resizes - mCaptureWidth = mDoGph_gInf_c::getWidth(); - mCaptureHeight = mDoGph_gInf_c::getHeight(); - GXInitTexObj(&mTexObj, mDoGph_gInf_c::getFrameBufferTex(), mCaptureWidth, mCaptureHeight, + mCaptureWidth = JUTVideo::getManager()->getRenderWidth(); + mCaptureHeight = JUTVideo::getManager()->getRenderHeight(); + GXInitTexObj(&mTexObj, mDoGph_gInf_c::getFrameBufferTex(), FB_WIDTH / 2, FB_HEIGHT / 2, (GXTexFmt)mDoGph_gInf_c::getFrameBufferTimg()->format, GX_CLAMP, GX_CLAMP, GX_FALSE); GXInitTexObjLOD(&mTexObj, GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); #endif } else { #if TARGET_PC // If the window was resized since capture, force a re-capture at the new size - if (mCaptureWidth != (u16)mDoGph_gInf_c::getWidth() || - mCaptureHeight != (u16)mDoGph_gInf_c::getHeight()) { + if (mCaptureWidth != JUTVideo::getManager()->getRenderWidth() || + mCaptureHeight != JUTVideo::getManager()->getRenderHeight()) { mFlag = 1; return; } @@ -137,8 +131,8 @@ private: /* 0x5 */ u8 mAlpha; /* 0x6 */ u8 mTopFlag; #if TARGET_PC - u16 mCaptureWidth; - u16 mCaptureHeight; + u32 mCaptureWidth; + u32 mCaptureHeight; TGXTexObj mTexObj; #endif }; diff --git a/src/d/d_msg_scrn_howl.cpp b/src/d/d_msg_scrn_howl.cpp index 03729377a5..1d39cba1a1 100644 --- a/src/d/d_msg_scrn_howl.cpp +++ b/src/d/d_msg_scrn_howl.cpp @@ -607,17 +607,10 @@ void dMsgScrnHowl_c::drawGuide() { J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort(); Vec local_b0 = field_0x128; Vec local_bc = field_0x140; -#if TARGET_PC - grafContext->scissor( - (local_b0.x - mDoGph_gInf_c::getMinXF()) / (mDoGph_gInf_c::getWidthF() / mDoGph_gInf_c::getWidth()), - field_0x2118, (local_bc.x - local_b0.x) / (mDoGph_gInf_c::getWidthF() / mDoGph_gInf_c::getWidth()), - field_0x2120); -#else grafContext->scissor( (local_b0.x - mDoGph_gInf_c::getMinXF()) / (mDoGph_gInf_c::getWidthF() / FB_WIDTH), field_0x2118, (local_bc.x - local_b0.x) / (mDoGph_gInf_c::getWidthF() / FB_WIDTH), field_0x2120); -#endif grafContext->setScissor(); f32 local_cc = mpLineH[0]->getGlobalPosX(); s16 sVar12 = 0; @@ -745,19 +738,11 @@ void dMsgScrnHowl_c::drawGuide2() { } Vec local_58 = field_0x128; Vec local_64 = field_0x140; -#if TARGET_PC - f32 local_70 = mDoGph_gInf_c::getHeightF() / mDoGph_gInf_c::getHeight(); - grafContext->scissor( - (local_58.x - mDoGph_gInf_c::getMinXF()) / (mDoGph_gInf_c::getWidthF() / mDoGph_gInf_c::getWidth()), - field_0x2118, (local_64.x - local_58.x) / (mDoGph_gInf_c::getWidthF() / mDoGph_gInf_c::getWidth()), - field_0x2120); -#else f32 local_70 = mDoGph_gInf_c::getHeightF() / mDoGph_gInf_c::getHeight(); grafContext->scissor( (local_58.x - mDoGph_gInf_c::getMinXF()) / (mDoGph_gInf_c::getWidthF() / FB_WIDTH), field_0x2118, (local_64.x - local_58.x) / (mDoGph_gInf_c::getWidthF() / FB_WIDTH), field_0x2120); -#endif grafContext->setScissor(); f32 local_74 = mpLineH[0]->getGlobalPosX(); s16 local_134 = 0; @@ -859,15 +844,9 @@ void dMsgScrnHowl_c::drawEffect() { Vec vec1 = field_0x128; Vec vec2 = field_0x140; mDoGph_gInf_c::getHeightF(); -#if TARGET_PC - grafContext->scissor( - (vec1.x - mDoGph_gInf_c::getMinXF()) / (mDoGph_gInf_c::getWidthF() / mDoGph_gInf_c::getWidth()), field_0x2118, - 12.0f + ((vec2.x - vec1.x) / (mDoGph_gInf_c::getWidthF() / mDoGph_gInf_c::getWidth())), field_0x2120); -#else grafContext->scissor( (vec1.x - mDoGph_gInf_c::getMinXF()) / (mDoGph_gInf_c::getWidthF() / FB_WIDTH), field_0x2118, 12.0f + ((vec2.x - vec1.x) / (mDoGph_gInf_c::getWidthF() / FB_WIDTH)), field_0x2120); -#endif grafContext->setScissor(); u8 timer = daAlink_getAlinkActorClass()->getWolfHowlMgrP()->getReleaseTimer(); u8 screenAlpha = mpScreen->search(MULTI_CHAR('line00'))->getAlpha(); diff --git a/src/d/d_ovlp_fade2.cpp b/src/d/d_ovlp_fade2.cpp index 37dbeedba2..c6c6cc735e 100644 --- a/src/d/d_ovlp_fade2.cpp +++ b/src/d/d_ovlp_fade2.cpp @@ -12,13 +12,8 @@ #include "m_Do/m_Do_graphic.h" void dOvlpFd2_dlst_c::draw() { -#if TARGET_PC - GXSetViewport(0.0f, 0.0f, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight(), 0.0f, 1.0f); - GXSetScissor(0, 0, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight()); -#else GXSetViewport(0.0f, 0.0f, FB_WIDTH, FB_HEIGHT, 0.0f, 1.0f); GXSetScissor(0, 0, FB_WIDTH, FB_HEIGHT); -#endif GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_CLR_RGB, GX_RGBA4, 0); GXClearVtxDesc(); GXSetVtxDesc(GX_VA_POS, GX_DIRECT); diff --git a/src/d/d_ovlp_fade3.cpp b/src/d/d_ovlp_fade3.cpp index d6f432f3ee..0b3c03db2b 100644 --- a/src/d/d_ovlp_fade3.cpp +++ b/src/d/d_ovlp_fade3.cpp @@ -13,11 +13,10 @@ #include "m_Do/m_Do_graphic.h" void dDlst_snapShot_c::draw() { -#if TARGET_PC - GXSetTexCopySrc(0, 0, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight()); - GXSetTexCopyDst(mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight(), GX_TF_RGBA8, GX_TRUE); -#else GXSetTexCopySrc(0, 0, FB_WIDTH, FB_HEIGHT); +#if TARGET_PC + GXSetTexCopyDst(FB_WIDTH, FB_HEIGHT, GX_TF_RGBA8, GX_FALSE); +#else GXSetTexCopyDst(FB_WIDTH / 2, FB_HEIGHT / 2, GX_TF_RGBA8, GX_TRUE); #endif GXCopyTex(mDoGph_gInf_c::getFrameBufferTex(), GX_FALSE); @@ -25,13 +24,8 @@ void dDlst_snapShot_c::draw() { } void dOvlpFd3_dlst_c::draw() { -#if TARGET_PC - GXSetViewport(0.0f, 0.0f, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight(), 0.0f, 1.0f); - GXSetScissor(0, 0, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight()); -#else GXSetViewport(0.0f, 0.0f, FB_WIDTH, FB_HEIGHT, 0.0f, 1.0f); GXSetScissor(0, 0, FB_WIDTH, FB_HEIGHT); -#endif GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_CLR_RGB, GX_RGBA4, 0); GXClearVtxDesc(); GXSetVtxDesc(GX_VA_POS, GX_DIRECT); @@ -112,19 +106,6 @@ void dOvlpFd3_dlst_c::draw() { GXBegin(GX_QUADS, GX_VTXFMT0, 4); - #if TARGET_PC - GXPosition2s16(-FB_WIDTH / 2, FB_HEIGHT / 2); - GXTexCoord2s8(0, 0); - - GXPosition2s16(FB_WIDTH / 2, FB_HEIGHT / 2); - GXTexCoord2s8(1, 0); - - GXPosition2s16(FB_WIDTH / 2, -FB_HEIGHT / 2); - GXTexCoord2s8(1, 1); - - GXPosition2s16(-FB_WIDTH / 2, -FB_HEIGHT / 2); - GXTexCoord2s8(0, 1); - #else GXPosition2s16(-mDoGph_gInf_c::getWidth() / 2, mDoGph_gInf_c::getHeight() / 2); GXTexCoord2s8(0, 0); @@ -136,7 +117,6 @@ void dOvlpFd3_dlst_c::draw() { GXPosition2s16(-mDoGph_gInf_c::getWidth() / 2, -mDoGph_gInf_c::getHeight() / 2); GXTexCoord2s8(0, 1); - #endif GXEnd(); diff --git a/src/d/d_pane_class.cpp b/src/d/d_pane_class.cpp index 78855b2d1c..4d5104ffcf 100644 --- a/src/d/d_pane_class.cpp +++ b/src/d/d_pane_class.cpp @@ -356,12 +356,8 @@ Vec CPaneMgr::getGlobalVtx(J2DPane* p_pane, Mtx* param_1, u8 param_2, bool param Mtx m; MtxP mp = (MtxP)param_1; J2DPane* parent = p_pane->getParentPane(); - -#if TARGET_PC - J2DOrthoGraph ortho(0.0f, 0.0f, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight(), -1.0f, 1.0f); -#else + J2DOrthoGraph ortho(0.0f, 0.0f, FB_WIDTH, FB_HEIGHT, -1.0f, 1.0f); -#endif ortho.setOrtho(mDoGph_gInf_c::getMinXF(), mDoGph_gInf_c::getMinYF(), mDoGph_gInf_c::getWidthF(), mDoGph_gInf_c::getHeightF(), -1.0f, 1.0f); if (parent != NULL) { diff --git a/src/d/d_s_play.cpp b/src/d/d_s_play.cpp index 3f3cd073b9..38308d0668 100644 --- a/src/d/d_s_play.cpp +++ b/src/d/d_s_play.cpp @@ -1418,12 +1418,7 @@ static int phase_4(dScnPly_c* i_this) { dComIfGp_setPlayerPtr(i, NULL); } -#if TARGET_PC - dComIfGp_setWindow(0, 0.0f, 0.0f, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight(), 0.0f, - 1.0f, 0, 2); -#else dComIfGp_setWindow(0, 0.0f, 0.0f, FB_WIDTH, FB_HEIGHT, 0.0f, 1.0f, 0, 2); -#endif dComIfGp_setCameraInfo(0, NULL, 0, 0, -1); dComIfGd_setWindow(NULL); dComIfGd_setViewport(NULL); diff --git a/src/dusk/imgui/ImGuiFirstRunPreset.cpp b/src/dusk/imgui/ImGuiFirstRunPreset.cpp index c218185c92..fb2d181562 100644 --- a/src/dusk/imgui/ImGuiFirstRunPreset.cpp +++ b/src/dusk/imgui/ImGuiFirstRunPreset.cpp @@ -13,7 +13,7 @@ static void ApplyPresetClassic() { auto& s = getSettings(); s.video.lockAspectRatio.setValue(true); s.game.bloomMode.setValue(BloomMode::Classic); - VILockAspectRatio(defaultAspectRatioW, defaultAspectRatioH); + AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); } static void ApplyPresetHD() { diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index b0c20b4d46..430592a536 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -21,6 +21,14 @@ #include "dusk/main.h" +namespace { +constexpr int kInternalResolutionScaleMax = 12; +} // namespace + +namespace aurora::gx { +extern bool enableLodBias; +} + namespace dusk { void ImGuiMenuGame::ToggleFullscreen() { getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen); @@ -58,14 +66,34 @@ namespace dusk { getSettings().video.lockAspectRatio.setValue(lockAspect); if (lockAspect) { - VILockAspectRatio(defaultAspectRatioW, defaultAspectRatioH); + AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); } else { - VIUnlockAspectRatio(); + AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); } config::Save(); } + u32 internalResolutionWidth = 0; + u32 internalResolutionHeight = 0; + AuroraGetRenderSize(&internalResolutionWidth, &internalResolutionHeight); + ImGui::TextDisabled("Current internal resolution: %ux%u", internalResolutionWidth, + internalResolutionHeight); + + int scale = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0, + kInternalResolutionScaleMax); + if (ImGui::SliderInt("Internal Resolution", &scale, 0, kInternalResolutionScaleMax, + scale == 0 ? "Auto" : "%dx")) + { + getSettings().game.internalResolutionScale.setValue(scale); + VISetFrameBufferScale(static_cast(scale)); + config::Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Auto renders at the native window resolution.\n" + "Higher values scale the game's internal framebuffer."); + } + constexpr const char* bloomModeNames[] = {"Off", "Classic", "Dusk"}; int bloomMode = static_cast(getSettings().game.bloomMode.getValue()); if (ImGui::BeginCombo("Bloom", bloomModeNames[bloomMode])) { @@ -93,6 +121,8 @@ namespace dusk { config::ImGuiCheckbox("Enable Water Refraction", getSettings().game.enableWaterRefraction); + ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); + ImGui::EndMenu(); } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index e4cace9ff0..0236f30afa 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -47,6 +47,7 @@ UserSettings g_userSettings = { .bloomMultiplier {"game.bloomMultiplier", 1.0f}, .enableWaterRefraction {"game.enableWaterRefraction", true}, .enableFrameInterpolation = {"game.enableFrameInterpolation", false}, + .internalResolutionScale {"game.internalResolutionScale", 0}, .shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1}, // Audio @@ -127,6 +128,7 @@ void registerSettings() { Register(g_userSettings.game.bloomMode); Register(g_userSettings.game.bloomMultiplier); Register(g_userSettings.game.enableWaterRefraction); + Register(g_userSettings.game.internalResolutionScale); Register(g_userSettings.game.shadowResolutionMultiplier); Register(g_userSettings.game.enableFastIronBoots); Register(g_userSettings.game.canTransformAnywhere); diff --git a/src/dusk/stubs.cpp b/src/dusk/stubs.cpp index 668541f49c..07e420445c 100644 --- a/src/dusk/stubs.cpp +++ b/src/dusk/stubs.cpp @@ -332,14 +332,6 @@ static VIRetraceCallback sVIPostRetraceCallback = NULL; extern "C" { -void VIConfigure(const GXRenderModeObj* rm) { - STUB_LOG(); -} - -void VIConfigurePan(u16 xOrg, u16 yOrg, u16 width, u16 height) { - STUB_LOG(); -} - u32 VIGetRetraceCount() { return sRetraceCount; } diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index a56e448645..a4a31e3228 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -141,7 +141,7 @@ void dDlst_heapMap_c::draw() { f32 var_f29 = field_0x10 - field_0x8; f32 sp4C = field_0x14 - field_0xc; f32 sp48 = var_f29 * sp4C; - + uintptr_t start_addr = (uintptr_t)m_heap->getStartAddr(); uintptr_t end_addr = (uintptr_t)m_heap->getEndAddr(); u32 sp40 = end_addr - start_addr; @@ -156,7 +156,7 @@ void dDlst_heapMap_c::draw() { f32 var_f30 = (f32)sp38 * sp3C; uintptr_t sp34 = (uintptr_t)block - start_addr; f32 sp30 = (f32)sp34 * sp3C; - + f32 var_f28 = std::floor(sp30 / var_f29); f32 sp2C = sp30 - (var_f29 * var_f28); @@ -603,21 +603,16 @@ struct tvSize { u16 width; u16 height; }; -const tvSize l_tvSize[2] = { +#ifndef TARGET_PC +const +#endif +tvSize l_tvSize[2] = { {FB_WIDTH_BASE, FB_HEIGHT_BASE}, {808, FB_HEIGHT_BASE}, }; -#if TARGET_PC -tvSize pc_tvSize = {608, 448}; -#endif - void mDoGph_gInf_c::setTvSize() { -#if TARGET_PC - const tvSize* tvsize = &pc_tvSize; -#else const tvSize* tvsize = &l_tvSize[mWide]; -#endif m_width = tvsize->width; m_height = tvsize->height; @@ -644,21 +639,11 @@ void mDoGph_gInf_c::setTvSize() { #endif } -#if TARGET_PC -void mDoGph_gInf_c::onWide(f32 width, f32 height) { - mWide = TRUE; - pc_tvSize.width = width; - pc_tvSize.height = height; - setTvSize(); - dMeter2Info_onWide2D(); -} -#else void mDoGph_gInf_c::onWide() { mWide = TRUE; setTvSize(); dMeter2Info_onWide2D(); } -#endif void mDoGph_gInf_c::offWide() { mWide = FALSE; @@ -800,8 +785,11 @@ void mDoGph_gInf_c::updateSafeAreaBounds() { return; } + u32 renderWidth = 0; + u32 renderHeight = 0; + AuroraGetRenderSize(&renderWidth, &renderHeight); if (windowSize.native_fb_width == 0 || windowSize.native_fb_height == 0 || - windowSize.fb_width == 0 || windowSize.fb_height == 0) + renderWidth == 0 || renderHeight == 0) { return; } @@ -814,23 +802,25 @@ void mDoGph_gInf_c::updateSafeAreaBounds() { const f32 safeRight = static_cast(safeRect.x + safeRect.w) * nativeScaleX; const f32 safeBottom = static_cast(safeRect.y + safeRect.h) * nativeScaleY; - const f32 viewportLeft = - (static_cast(windowSize.native_fb_width) - static_cast(windowSize.fb_width)) * - 0.5f; - const f32 viewportTop = - (static_cast(windowSize.native_fb_height) - static_cast(windowSize.fb_height)) * - 0.5f; - const f32 viewportRight = viewportLeft + static_cast(windowSize.fb_width); - const f32 viewportBottom = viewportTop + static_cast(windowSize.fb_height); + f32 viewportWidth = static_cast(windowSize.native_fb_width); + f32 viewportHeight = static_cast(windowSize.native_fb_height); + const f32 targetAspect = viewportWidth / viewportHeight; + const f32 contentAspect = static_cast(renderWidth) / static_cast(renderHeight); + if (targetAspect > contentAspect) { + viewportWidth = std::max(1.0f, std::round(viewportHeight * contentAspect)); + } else if (targetAspect < contentAspect) { + viewportHeight = std::max(1.0f, std::round(viewportWidth / contentAspect)); + } - const f32 leftInset = std::max(0.0f, safeLeft - viewportLeft) * - (m_widthF / static_cast(windowSize.fb_width)); - const f32 topInset = std::max(0.0f, safeTop - viewportTop) * - (m_heightF / static_cast(windowSize.fb_height)); - const f32 rightInset = std::max(0.0f, viewportRight - safeRight) * - (m_widthF / static_cast(windowSize.fb_width)); - const f32 bottomInset = std::max(0.0f, viewportBottom - safeBottom) * - (m_heightF / static_cast(windowSize.fb_height)); + const f32 viewportLeft = (static_cast(windowSize.native_fb_width) - viewportWidth) * 0.5f; + const f32 viewportTop = (static_cast(windowSize.native_fb_height) - viewportHeight) * 0.5f; + const f32 viewportRight = viewportLeft + viewportWidth; + const f32 viewportBottom = viewportTop + viewportHeight; + + const f32 leftInset = std::max(0.0f, safeLeft - viewportLeft) * (m_widthF / viewportWidth); + const f32 topInset = std::max(0.0f, safeTop - viewportTop) * (m_heightF / viewportHeight); + const f32 rightInset = std::max(0.0f, viewportRight - safeRight) * (m_widthF / viewportWidth); + const f32 bottomInset = std::max(0.0f, viewportBottom - safeBottom) * (m_heightF / viewportHeight); const f32 safeMinXF = m_minXF + leftInset; const f32 safeMinYF = m_minYF + topInset; @@ -851,13 +841,13 @@ void mDoGph_gInf_c::updateSafeAreaBounds() { m_safeHeightF = safeHeightF; } -void mDoGph_gInf_c::setWindowSize(AuroraWindowSize const& size) { - JUTVideo::getManager()->setWindowSize(size); - dComIfGp_setWindow(0, 0.0f, 0.0f, getWidth(), getHeight(), 0.0f, 1.0f, 0, 2); - mFader->mBox.set(0, 0, getWidth(), getHeight()); - - f32 newWidth = (getWidth() / getHeight()) * 448.0f; - onWide(newWidth, 448.0f); +void mDoGph_gInf_c::updateRenderSize() { + u32 width, height; + AuroraGetRenderSize(&width, &height); + JUTVideo::getManager()->setRenderSize(width, height); + l_tvSize[1].width = static_cast(static_cast(width) / static_cast(height) * + static_cast(l_tvSize[1].height)); + onWide(); } #endif @@ -996,7 +986,7 @@ static void drawDepth2(view_class* param_0, view_port_class* param_1, int param_ GXProject(param_0->lookat.center.x, param_0->lookat.center.y, param_0->lookat.center.z, param_0->viewMtx, sp4C, sp34, &sp1C, &sp18, &sp14); - + param_2 = (0xFF0000 - (int)(16777215.0f * sp14)) >> 8; param_2 = cLib_minMaxLimit(param_2, -0x400, 0); } @@ -1361,8 +1351,8 @@ void mDoGph_gInf_c::bloom_c::draw2() { if (mMonoColor.a == 0 && !enabled) return; - f32 width = mDoGph_gInf_c::getWidth(); - f32 height = mDoGph_gInf_c::getHeight(); + f32 width = JUTVideo::getManager()->getRenderWidth(); + f32 height = JUTVideo::getManager()->getRenderHeight(); GXLoadTexObj(getFrameBufferTexObj(), GX_TEXMAP0); GXSetNumChans(0); @@ -1446,7 +1436,7 @@ void mDoGph_gInf_c::bloom_c::draw2() { CopyToTexObj(&tmpTex[texNo], texNo, rect.w, rect.h); return &tmpTex[texNo]; }; - + auto divQuad = [&](int divNo) { auto const& rect = divRects[divNo]; f32 x0 = rect.x / width; @@ -1479,8 +1469,8 @@ void mDoGph_gInf_c::bloom_c::draw2() { } if (enabled) { - GXCreateFrameBuffer(width * 0.75f, height * 0.5f); - GXSetViewport(0.0f, 0.0f, width, height, 0.0f, 1.0f); // use oversized viewport to make the math easier + GXCreateFrameBuffer(divRects[2].x + divRects[2].w, divRects[1].y + divRects[1].h); + GXSetViewportRender(0.0f, 0.0f, width, height, 0.0f, 1.0f); // use oversized viewport to make the math easier GXSetNumTevStages(3); GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD0, GX_TEXMAP0, GX_COLOR_NULL); @@ -1551,7 +1541,7 @@ void mDoGph_gInf_c::bloom_c::draw2() { // This is applied over two passes, the second one with an alpha of 25%; however, the clipping that this introduces is a bit integral to the look, // so we do the same thing, letting it clip. float brightnessF32 = (mBlureRatio * 16 / 255.0f); - + // Distribute the brightness through the total number of passes. f32 totalNumPasses = (divNum - divStart + 1); float brightnessPerPass = 255.0f * powf(brightnessF32, 1.0f / totalNumPasses); @@ -1623,13 +1613,8 @@ void mDoGph_gInf_c::bloom_c::draw() { bool enabled = mEnable && m_buffer != NULL; if (mMonoColor.a != 0 || enabled) { -#if TARGET_PC - f32 width = mDoGph_gInf_c::getWidth(); - f32 height = mDoGph_gInf_c::getHeight(); -#else f32 width = FB_WIDTH; f32 height = FB_HEIGHT; -#endif GXSetViewport(0.0f, 0.0f, width, height, 0.0f, 1.0f); GXSetScissor(0, 0, width, height); @@ -1744,7 +1729,7 @@ void mDoGph_gInf_c::bloom_c::draw() { GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY); for (int texCoord = (int)GX_TEXCOORD1; texCoord < (int)GX_MAX_TEXCOORD; texCoord++) { GXSetTexCoordGen((GXTexCoordID)texCoord, GX_TG_MTX2x4, GX_TG_TEX0, texMtxID); - + #if TARGET_PC f32 dVar15 = mBlureSize * ((448.0f / height) / 6400.0f); #else @@ -1885,7 +1870,7 @@ static void retry_captue_frame(view_class* param_0, view_port_class* param_1, in var_r23 = height >> 1; GXSetTexCopySrc(x_orig, y_orig_pos, width, height); #ifdef TARGET_PC - GXSetTexCopyDst(width, height, (GXTexFmt)mDoGph_gInf_c::getFrameBufferTimg()->format, GX_TRUE); + GXSetTexCopyDst(width, height, (GXTexFmt)mDoGph_gInf_c::getFrameBufferTimg()->format, GX_FALSE); #else GXSetTexCopyDst(var_r24, var_r23, (GXTexFmt)mDoGph_gInf_c::getFrameBufferTimg()->format, GX_TRUE); #endif @@ -2143,8 +2128,7 @@ int mDoGph_Painter() { j3dSys.drawInit(); GXSetDither(GX_ENABLE); - J2DOrthoGraph ortho(0.0f, 0.0f, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight(), -1.0f, - 1.0f); + J2DOrthoGraph ortho(0.0f, 0.0f, FB_WIDTH, FB_HEIGHT, -1.0f, 1.0f); ortho.setOrtho(mDoGph_gInf_c::getMinXF(), mDoGph_gInf_c::getMinYF(), mDoGph_gInf_c::getWidthF(), mDoGph_gInf_c::getHeightF(), -1.0f, 1.0f); @@ -2190,13 +2174,8 @@ int mDoGph_Painter() { view_port_class new_port; new_port.x_orig = 0.0f; new_port.y_orig = 0.0f; - #if TARGET_PC - new_port.width = mDoGph_gInf_c::getWidth(); - new_port.height = mDoGph_gInf_c::getHeight(); - #else new_port.width = FB_WIDTH; new_port.height = FB_HEIGHT; - #endif new_port.near_z = view_port->near_z; new_port.far_z = view_port->far_z; new_port.scissor = view_port->scissor; @@ -2250,7 +2229,7 @@ int mDoGph_Painter() { dComIfGp_setCurrentView(&camera_p->view); dComIfGp_setCurrentViewport(view_port); GXSetProjection(camera_p->view.projMtx, GX_PERSPECTIVE); - + #if DEBUG captureScreenSetProjection(camera_p->view.projMtx); #endif @@ -2327,7 +2306,7 @@ int mDoGph_Painter() { #endif GX_DEBUG_GROUP(dComIfGd_drawOpaListPacket); - + #if DEBUG // "drawing up to special-use drawing (Opaque) except J3D (Rendering)" fapGm_HIO_c::stopCpuTimer("J3D以外などの特殊用(不透明)描画まで(レンダリング)"); @@ -2409,7 +2388,7 @@ int mDoGph_Painter() { GX_DEBUG_GROUP(dComIfGd_drawXluListInvisible); } } - + #if DEBUG // "drawing up to projection (Translucent)" @@ -2513,12 +2492,8 @@ int mDoGph_Painter() { retry_captue_frame(&camera_p->view, view_port, dComIfGp_getCameraZoomForcus(camera_id)); } - - #if TARGET_PC - GXSetViewport(0.0f, 0.0f, mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight(), 0.0f, 1.0f); - #else + GXSetViewport(0.0f, 0.0f, FB_WIDTH, FB_HEIGHT, 0.0f, 1.0f); - #endif Mtx m2; Mtx44 m; diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 784aea3f26..71981e40f3 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -126,8 +126,6 @@ AuroraStats dusk::lastFrameAuroraStats; float dusk::frameUsagePct = 0.0f; const char* configPath; -AuroraWindowSize preLaunchUIWindowSize; - bool launchUILoop() { while (dusk::IsRunning && !dusk::IsGameLaunched) { const AuroraEvent* event = aurora_update(); @@ -136,9 +134,6 @@ bool launchUILoop() { case AURORA_SDL_EVENT: dusk::g_imguiConsole.HandleSDLEvent(event->sdl); break; - case AURORA_WINDOW_RESIZED: - preLaunchUIWindowSize = event->windowSize; - break; case AURORA_DISPLAY_SCALE_CHANGED: dusk::ImGuiEngine_Initialize(event->windowSize.scale); break; @@ -202,9 +197,6 @@ void main01(void) { OSReport("Entering Main Loop (main01)...\n"); - if (preLaunchUIWindowSize.width != 0) - mDoGph_gInf_c::setWindowSize(preLaunchUIWindowSize); - constexpr float kSimStepSeconds = 1.0 / 30.0; auto previous_time = std::chrono::steady_clock::now(); float accumulator = kSimStepSeconds; @@ -219,9 +211,6 @@ void main01(void) { case AURORA_SDL_EVENT: dusk::g_imguiConsole.HandleSDLEvent(event->sdl); break; - case AURORA_WINDOW_RESIZED: - mDoGph_gInf_c::setWindowSize(event->windowSize); - break; case AURORA_DISPLAY_SCALE_CHANGED: dusk::ImGuiEngine_Initialize(event->windowSize.scale); break; @@ -247,6 +236,8 @@ void main01(void) { continue; } + mDoGph_gInf_c::updateRenderSize(); + if (dusk::getSettings().game.enableFrameInterpolation && !dusk::getTransientSettings().skipFrameRateLimit) { dusk::frame_interp::notify_presentation_frame(); if (accumulator >= kSimStepSeconds) { @@ -525,10 +516,11 @@ int game_main(int argc, char* argv[]) { .c_str()); if (dusk::getSettings().video.lockAspectRatio) { - VILockAspectRatio(defaultAspectRatioW, defaultAspectRatioH); + AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); } else { - VIUnlockAspectRatio(); + AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); } + VISetFrameBufferScale(dusk::getSettings().game.internalResolutionScale.getValue()); dusk::audio::SetMasterVolume(dusk::getSettings().audio.masterVolume / 100.0f); dusk::audio::SetEnableReverb(dusk::getSettings().audio.enableReverb); From 8010e16cab6737c263eb969292b4f67c9d06e2bd Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 18 Apr 2026 01:14:14 -0700 Subject: [PATCH 05/68] add some cheats --- include/dusk/settings.h | 9 +++ src/d/actor/d_a_alink_cut.inc | 6 ++ src/d/actor/d_a_alink_hook.inc | 32 ++++++++++ src/dusk/imgui/ImGuiMenuEnhancements.cpp | 10 +++ src/dusk/settings.cpp | 18 ++++++ src/f_ap/f_ap_game.cpp | 78 +++++++++++++++++------- 6 files changed, 132 insertions(+), 21 deletions(-) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 3b3c33bb2f..78ac311e52 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -92,6 +92,15 @@ struct UserSettings { ConfigVar gyroInvertYaw; // Cheats + ConfigVar infiniteHearts; + ConfigVar infiniteArrows; + ConfigVar infiniteBombs; + ConfigVar infiniteOil; + ConfigVar infiniteOxygen; + ConfigVar infiniteRupees; + ConfigVar moonJump; + ConfigVar superClawshot; + ConfigVar alwaysGreatspin; ConfigVar enableFastIronBoots; ConfigVar canTransformAnywhere; ConfigVar fastSpinner; diff --git a/src/d/actor/d_a_alink_cut.inc b/src/d/actor/d_a_alink_cut.inc index 8e61a262cb..f33d2935c5 100644 --- a/src/d/actor/d_a_alink_cut.inc +++ b/src/d/actor/d_a_alink_cut.inc @@ -817,6 +817,12 @@ BOOL daAlink_c::checkDownAttackState() { } BOOL daAlink_c::checkCutLargeTurnState() const { + #if TARGET_PC + if (dusk::getSettings().game.alwaysGreatspin) { + return TRUE; + } + #endif + return ((dComIfGs_isEventBit(dSv_event_flag_c::F_0344) || checkNoResetFlg3(FLG3_TRANING_CUT_LARGE_TURN)) && dComIfGs_getLife() == dComIfGs_getMaxLifeGauge() ) diff --git a/src/d/actor/d_a_alink_hook.inc b/src/d/actor/d_a_alink_hook.inc index 3641015993..d152190161 100644 --- a/src/d/actor/d_a_alink_hook.inc +++ b/src/d/actor/d_a_alink_hook.inc @@ -290,6 +290,12 @@ BOOL daAlink_c::checkHookshotStickBG(cBgS_PolyInfo& i_polyinfo) { } #endif + #if TARGET_PC + if (dusk::getSettings().game.superClawshot) { + return TRUE; + } + #endif + if (dComIfG_Bgsp().ChkPolyHSStick(i_polyinfo)) { dBgW_Base* bgw_p = dComIfG_Bgsp().GetBgWBasePointer(i_polyinfo); if (bgw_p != NULL && bgw_p->ChkPushPullOk()) { @@ -448,6 +454,12 @@ void daAlink_c::setHookshotSight() { max_length = mpHIO->mItem.mHookshot.m.mMaxLength; } + #if TARGET_PC + if (dusk::getSettings().game.superClawshot) { + max_length = 69420.0f; + } + #endif + BOOL line_cross = checkSightLine(max_length, &sight_pos); if (mHookTargetAcKeep.getActor() != NULL) { @@ -890,6 +902,14 @@ void daAlink_c::setHookshotPos() { max_length = mpHIO->mItem.mHookshot.m.mMaxLength; } + #if TARGET_PC + if (dusk::getSettings().game.superClawshot) { + return_speed = 2870.0f; + shoot_speed = 2870.0f; + max_length = 69420.0f; + } + #endif + if (mItemMode == HS_MODE_RETURN_e) { if (targetAc_p != NULL) { if (checkLv7BossRoom()) { @@ -899,6 +919,12 @@ void daAlink_c::setHookshotPos() { } } + #if TARGET_PC + if (dusk::getSettings().game.superClawshot) { + return_speed = 500.0f; + } + #endif + if (checkModeFlg(0x400)) { return_speed += current.pos.abs(field_0x3798); } @@ -1548,6 +1574,12 @@ int daAlink_c::procHookshotFly() { f32 temp_f31 = field_0x37d4.abs(); f32 temp_f30 = mpHIO->mItem.mHookshot.m.mStickReturnSpeed + spAC.abs(mHookshotTopPos); + #if TARGET_PC + if (dusk::getSettings().game.superClawshot) { + temp_f30 = 500.0f + spAC.abs(mHookshotTopPos); + } + #endif + if (temp_f31 < temp_f30 || mProcVar1.field_0x300a == 0) { setHookshotReturnEnd(); } else { diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.cpp b/src/dusk/imgui/ImGuiMenuEnhancements.cpp index a494f81771..7d179aa578 100644 --- a/src/dusk/imgui/ImGuiMenuEnhancements.cpp +++ b/src/dusk/imgui/ImGuiMenuEnhancements.cpp @@ -202,6 +202,16 @@ namespace dusk { ImGui::Separator(); if (ImGui::BeginMenu("Cheats")) { + config::ImGuiCheckbox("Infinite Hearts", getSettings().game.infiniteHearts); + config::ImGuiCheckbox("Infinite Arrows", getSettings().game.infiniteArrows); + config::ImGuiCheckbox("Infinite Bombs", getSettings().game.infiniteBombs); + config::ImGuiCheckbox("Infinite Oil", getSettings().game.infiniteOil); + config::ImGuiCheckbox("Infinite Oxygen", getSettings().game.infiniteOxygen); + config::ImGuiCheckbox("Infinite Rupees", getSettings().game.infiniteRupees); + config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump); + config::ImGuiCheckbox("Super Clawshot", getSettings().game.superClawshot); + config::ImGuiCheckbox("Always Greatspin", getSettings().game.alwaysGreatspin); + config::ImGuiCheckbox("Fast Iron Boots", getSettings().game.enableFastIronBoots); config::ImGuiCheckbox("Can Transform Anywhere", getSettings().game.canTransformAnywhere); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index cc33fc12a3..ff494f5b5d 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -66,6 +66,15 @@ UserSettings g_userSettings = { .gyroInvertYaw {"game.gyroInvertYaw", false}, // Cheats + .infiniteHearts {"game.infiniteHearts", false}, + .infiniteArrows{"game.infiniteArrows", false}, + .infiniteBombs{"game.infiniteBombs", false}, + .infiniteOil{"game.infiniteOil", false}, + .infiniteOxygen{"game.infiniteOxygen", false}, + .infiniteRupees{"game.infiniteRupees", false}, + .moonJump{"game.moonJump", false}, + .superClawshot{"game.superClawshot", false}, + .alwaysGreatspin{"game.alwaysGreatspin", false}, .enableFastIronBoots {"game.enableFastIronBoots", false}, .canTransformAnywhere {"game.canTransformAnywhere", false}, .fastSpinner {"game.fastSpinner", false}, @@ -139,6 +148,15 @@ void registerSettings() { Register(g_userSettings.game.midnasLamentNonStop); Register(g_userSettings.game.enableTurboKeybind); Register(g_userSettings.game.fastSpinner); + Register(g_userSettings.game.infiniteHearts); + Register(g_userSettings.game.infiniteArrows); + Register(g_userSettings.game.infiniteBombs); + Register(g_userSettings.game.infiniteOil); + Register(g_userSettings.game.infiniteOxygen); + Register(g_userSettings.game.infiniteRupees); + Register(g_userSettings.game.moonJump); + Register(g_userSettings.game.superClawshot); + Register(g_userSettings.game.alwaysGreatspin); Register(g_userSettings.game.enableFrameInterpolation); Register(g_userSettings.game.enableGyroAim); Register(g_userSettings.game.enableGyroRollgoal); diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index 93a9148957..69fb17d9cc 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -732,6 +732,62 @@ static void fapGm_AfterRecord() { dusk::frame_interp::end_record(); fapGm_After(); } + +static void duskExecute() { + if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigX(PAD_1)) { + if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) { + dynamic_cast(link)->handleWolfHowl(); + } + } + + if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigY(PAD_1)) { + if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) { + dynamic_cast(link)->handleQuickTransform(); + } + } + + if (dusk::getSettings().game.moonJump && (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1))) { + if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) { + link->speed.y = 56.0f; + } + } + + if (dusk::getSettings().game.fastSpinner && mDoCPd_c::getHoldR(PAD_1)) { + if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) { + auto spinnerActor = (fopAc_ac_c*)dynamic_cast(link)->getSpinnerActor(); + if (spinnerActor) { + if (spinnerActor->speedF < 60.f) + spinnerActor->speedF += 2.f; + } + } + } + + if (dusk::getSettings().game.infiniteHearts) { + dComIfGs_setLife((dComIfGs_getMaxLife() / 5) * 4); + } + + if (dusk::getSettings().game.infiniteArrows) { + dComIfGs_setArrowNum(dComIfGs_getArrowMax()); + } + + if (dusk::getSettings().game.infiniteBombs) { + dComIfGs_setBombNum(0, 99); + dComIfGs_setBombNum(1, 99); + dComIfGs_setBombNum(2, 99); + } + + if (dusk::getSettings().game.infiniteOil) { + dComIfGs_setOil(dComIfGs_getMaxOil()); + } + + if (dusk::getSettings().game.infiniteRupees) { + dComIfGs_setRupee(9999); + } + + if (dusk::getSettings().game.infiniteOxygen) { + dComIfGp_setOxygen(dComIfGp_getMaxOxygen()); + } +} #endif void fapGm_Execute() { @@ -747,27 +803,7 @@ void fapGm_Execute() { #endif #if TARGET_PC - if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigX(PAD_1)) { - if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) { - dynamic_cast(link)->handleWolfHowl(); - } - } - - if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigY(PAD_1)) { - if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) { - dynamic_cast(link)->handleQuickTransform(); - } - } - - if (dusk::getSettings().game.fastSpinner && mDoCPd_c::getHoldR(PAD_1)) { - if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) { - auto spinnerActor = (fopAc_ac_c*)dynamic_cast(link)->getSpinnerActor(); - if (spinnerActor) { - if (spinnerActor->speedF < 60.f) - spinnerActor->speedF += 2.f; - } - } - } + duskExecute(); #endif #ifdef TARGET_PC From 6ea3fef8c69465cdb283195556b9c49d36605dff Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 18 Apr 2026 03:38:16 -0700 Subject: [PATCH 06/68] fix mirrored shops / projection --- src/d/d_shop_system.cpp | 12 ++++++++++++ src/m_Do/m_Do_lib.cpp | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/d/d_shop_system.cpp b/src/d/d_shop_system.cpp index f1636bb093..e1827b76c1 100644 --- a/src/d/d_shop_system.cpp +++ b/src/d/d_shop_system.cpp @@ -1572,10 +1572,22 @@ BOOL dShopSystem_c::checkShopOpen() { } bool dShopSystem_c::checkLeftTrigger(STControl* i_stick) { +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + return i_stick->checkRightTrigger(); + } +#endif + return i_stick->checkLeftTrigger(); } bool dShopSystem_c::checkRightTrigger(STControl* i_stick) { +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + return i_stick->checkLeftTrigger(); + } +#endif + return i_stick->checkRightTrigger(); } diff --git a/src/m_Do/m_Do_lib.cpp b/src/m_Do/m_Do_lib.cpp index b541490353..bddf9c7b94 100644 --- a/src/m_Do/m_Do_lib.cpp +++ b/src/m_Do/m_Do_lib.cpp @@ -108,6 +108,12 @@ void mDoLib_project(Vec* src, Vec* dst) { } dst->x = ((0.5f + (multVec.x * calcFloat)) * xSize) + xOffset; + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + dst->x = ((0.5f + (-multVec.x * calcFloat)) * xSize) + xOffset; + } + #endif + dst->y = ((0.5f + (multVec.y * (-calcFloat))) * ySize) + yOffset; } @@ -171,6 +177,12 @@ void mDoLib_project(Vec* src, Vec* dst, JGeometry::TBox2 viewport) { } dst->x = ((0.5f + (multVec.x * calcFloat)) * xSize) + xOffset; + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + dst->x = ((0.5f + (-multVec.x * calcFloat)) * xSize) + xOffset; + } + #endif + dst->y = ((0.5f + (multVec.y * (-calcFloat))) * ySize) + yOffset; } #endif From c00fc756e3a2be535572b421a5b5bfe89f0c709a Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 18 Apr 2026 04:57:22 -0700 Subject: [PATCH 07/68] wip readme --- README.md | 136 +++++++++++++++---------------------------- docs/building.md | 98 +++++++++++++++++++++++++++++++ res/logo-mascot.webp | Bin 0 -> 192532 bytes 3 files changed, 144 insertions(+), 90 deletions(-) create mode 100644 docs/building.md create mode 100644 res/logo-mascot.webp diff --git a/README.md b/README.md index c41985d394..7322441b74 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,59 @@ -## Dusk +![DuskLogo](res/logo-mascot.webp) -### Building -#### Prerequisites -* [CMake 3.25+](https://cmake.org) - * Windows: Install `CMake Tools` in Visual Studio - * macOS: `brew install cmake` -* [Python 3+](https://python.org) - * Windows: [Microsoft Store](https://go.microsoft.com/fwlink?linkID=2082640) - * Verify it's added to `%PATH%` by typing `python` in `cmd`. - * macOS: `brew install python@3` -* **[Windows]** [Visual Studio 2026 Community](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx) - * Select `C++ Development` and verify the following packages are included: - * `Windows 11 SDK` - * `CMake Tools` - * `C++ Clang Compiler` - * `C++ Clang-cl` -* **[macOS]** [Xcode 16.4+](https://developer.apple.com/xcode/download/) -* **[Linux]** Actively tested on Ubuntu 24.04, Arch Linux & derivatives. - * Ubuntu 24.04+ packages - ``` - build-essential curl git ninja-build clang lld zlib1g-dev libcurl4-openssl-dev \ - libglu1-mesa-dev libdbus-1-dev libvulkan-dev libxi-dev libxrandr-dev libasound2-dev libpulse-dev \ - libudev-dev libpng-dev libncurses5-dev cmake libx11-xcb-dev python3 python-is-python3 \ - libclang-dev libfreetype-dev libxinerama-dev libxcursor-dev python3-markupsafe libgtk-3-dev \ - libxss-dev libxtst-dev - ``` - * Arch Linux packages - ``` - base-devel cmake ninja llvm vulkan-headers python python-markupsafe clang lld alsa-lib libpulse libxrandr freetype2 - ``` - * Fedora packages - ``` - cmake vulkan-headers ninja-build clang-devel llvm-devel libpng-devel - ``` - * It's also important that you install the developer tools and libraries - ``` - sudo dnf groupinstall "Development Tools" "Development Libraries" - ``` -#### Setup -Clone and initialize the Dusk repository -```sh -git clone --recursive https://github.com/TwilitRealm/dusk.git -cd dusk -git pull -git submodule update --init --recursive -``` +- ### **[Official Website](https://twilitrealm.dev)** +- ### **[Discord](https://discord.gg/QACynxeyna)** -#### Building +# Setup +**⚠️Dusk does NOT provide any copyrighted assets. You must provide your own copy of the game.** -**CLion (Windows / macOS / Linux)** +### 1. Verify your ROM dump +First make sure your dump of the game is clean and supported by Dusk. You can do this by checking the sha1 hash of your dump against this list of supported versions. -Open the project directory in CLion. Enable the appropriate presets for your platform: +| Version | sha1 hash | +| ------------ | ---------------------------------------- | +| GameCube USA | 75edd3ddff41f125d1b4ce1a40378f1b565519e7 | -![CLion](assets/clion.png) +### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases) -**Visual Studio (Windows)** +### 3. Setup the game +#### Windows +- Extract the zip folder +- Place your dump of the game into the same folder where you extracted to +- Launch `dusk.exe` -Open the project directory in Visual Studio. The CMake configuration will be loaded automatically. +#### macOS +- TODO -**ninja (macOS)** +#### Linux +- TODO -```sh -cmake --preset macos-default-relwithdebinfo -cmake --build --preset macos-default-relwithdebinfo -``` +#### iOS +- TODO -Alternate presets available: -- `macos-default-debug`: Clang, Debug +#### android +- TODO -**ninja (Linux)** +# Building +If you'd like to build Dusk from source, please read the [build instructions](docs/building.md). -```sh -cmake --preset linux-default-relwithdebinfo -cmake --build --preset linux-default-relwithdebinfo -``` +# Credits +- Taka +- encounter +- Antidote +- caseif +- CraftyBoss +- crowell +- dooplecks +- gymnast86 +- Irastris +- kipcode66 +- Lars +- LunarSoap +- Maddie +- MelonSpeedruns +- Pheenoh +- PJB +- Roeming +- YunataSavior -Alternate presets available: -- `linux-default-debug`: GCC, Debug -- `linux-clang-relwithdebinfo`: Clang, RelWithDebInfo -- `linux-clang-debug`: Clang, Debug - -**ninja (Windows)** - -```sh -cmake --preset windows-msvc-relwithdebinfo -cmake --build --preset windows-msvc-relwithdebinfo -``` - -Alternate presets available: -- `windows-msvc-debug`: MSVC, Debug -- `windows-clang-relwithdebinfo`: Clang-cl, RelWithDebInfo -- `windows-clang-debug`: Clang-cl, Debug - -#### Running -Pass the disc image as a positional argument. Supported formats: ISO (GCM), RVZ, WIA, WBFS, CISO, GCZ -```sh -build/{preset}/dusk /path/to/game.rvz -``` -If no path is specified, Dusk defaults to `game.iso` in the current working directory. - -#### 30 FPS on Debug -When compiled fully in a Debug the game runs too slowly to hit playable 30 FPS. To avoid this, you can set a CMake cache variable to optimize specific critical files without hampering debuggability in the rest of the program: `-DDUSK_SELECTED_OPT=ON`. When building for MSVC (Windows) you must also modify `CMAKE_CXX_FLAGS_DEBUG` and `CMAKE_C_FLAGS_DEBUG` to remove `/RTC1` from the flags, like so: `-DCMAKE_CXX_FLAGS_DEBUG="/MDd /Zi /Ob0 /Od" -DCMAKE_C_FLAGS_DEBUG="/MDd /Zi /Ob0 /Od"` +Special thanks to the TP Decomp team, the GC/Wii Decomp community, the Aurora developers, and the TP speedrunning community. diff --git a/docs/building.md b/docs/building.md new file mode 100644 index 0000000000..0b1befa49f --- /dev/null +++ b/docs/building.md @@ -0,0 +1,98 @@ +### Building +#### Prerequisites +* [CMake 3.25+](https://cmake.org) + * Windows: Install `CMake Tools` in Visual Studio + * macOS: `brew install cmake` +* [Python 3+](https://python.org) + * Windows: [Microsoft Store](https://go.microsoft.com/fwlink?linkID=2082640) + * Verify it's added to `%PATH%` by typing `python` in `cmd`. + * macOS: `brew install python@3` +* **[Windows]** [Visual Studio 2026 Community](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx) + * Select `C++ Development` and verify the following packages are included: + * `Windows 11 SDK` + * `CMake Tools` + * `C++ Clang Compiler` + * `C++ Clang-cl` +* **[macOS]** [Xcode 16.4+](https://developer.apple.com/xcode/download/) +* **[Linux]** Actively tested on Ubuntu 24.04, Arch Linux & derivatives. + * Ubuntu 24.04+ packages + ``` + build-essential curl git ninja-build clang lld zlib1g-dev libcurl4-openssl-dev \ + libglu1-mesa-dev libdbus-1-dev libvulkan-dev libxi-dev libxrandr-dev libasound2-dev libpulse-dev \ + libudev-dev libpng-dev libncurses5-dev cmake libx11-xcb-dev python3 python-is-python3 \ + libclang-dev libfreetype-dev libxinerama-dev libxcursor-dev python3-markupsafe libgtk-3-dev \ + libxss-dev libxtst-dev + ``` + * Arch Linux packages + ``` + base-devel cmake ninja llvm vulkan-headers python python-markupsafe clang lld alsa-lib libpulse libxrandr freetype2 + ``` + * Fedora packages + ``` + cmake vulkan-headers ninja-build clang-devel llvm-devel libpng-devel + ``` + * It's also important that you install the developer tools and libraries + ``` + sudo dnf groupinstall "Development Tools" "Development Libraries" + ``` +#### Setup +Clone and initialize the Dusk repository +```sh +git clone --recursive https://github.com/TwilitRealm/dusk.git +cd dusk +git pull +git submodule update --init --recursive +``` + +#### Building + +**CLion (Windows / macOS / Linux)** + +Open the project directory in CLion. Enable the appropriate presets for your platform: + +![CLion](../assets/clion.png) + +**Visual Studio (Windows)** + +Open the project directory in Visual Studio. The CMake configuration will be loaded automatically. + +**ninja (macOS)** + +```sh +cmake --preset macos-default-relwithdebinfo +cmake --build --preset macos-default-relwithdebinfo +``` + +Alternate presets available: +- `macos-default-debug`: Clang, Debug + +**ninja (Linux)** + +```sh +cmake --preset linux-default-relwithdebinfo +cmake --build --preset linux-default-relwithdebinfo +``` + +Alternate presets available: +- `linux-default-debug`: GCC, Debug +- `linux-clang-relwithdebinfo`: Clang, RelWithDebInfo +- `linux-clang-debug`: Clang, Debug + +**ninja (Windows)** + +```sh +cmake --preset windows-msvc-relwithdebinfo +cmake --build --preset windows-msvc-relwithdebinfo +``` + +Alternate presets available: +- `windows-msvc-debug`: MSVC, Debug +- `windows-clang-relwithdebinfo`: Clang-cl, RelWithDebInfo +- `windows-clang-debug`: Clang-cl, Debug + +#### Running +Pass the disc image as a positional argument. Supported formats: ISO (GCM), RVZ, WIA, WBFS, CISO, GCZ +```sh +build/{preset}/dusk /path/to/game.rvz +``` +If no path is specified, Dusk defaults to `game.iso` in the current working directory. diff --git a/res/logo-mascot.webp b/res/logo-mascot.webp new file mode 100644 index 0000000000000000000000000000000000000000..c22f3bc90fb0801143a7477d61805efc488e43b5 GIT binary patch literal 192532 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⪼PdiUJ>3+OS^>Alaf{G)(} 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Μ8R{Dy$lRjHx!!8giiG}J))w?#Jh*0 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 literal 0 HcmV?d00001 From d34be5143af50a6b24c19240f77d5db89673f106 Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Sat, 18 Apr 2026 10:16:40 -0600 Subject: [PATCH 08/68] fix quick transform conflicting with debug move combo --- src/dusk/imgui/ImGuiConsole.cpp | 2 +- src/f_ap/f_ap_game.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index f83b77b274..2779b9cd9f 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -281,7 +281,7 @@ namespace dusk { void ImGuiConsole::UpdateSettings() { getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind && ImGui::IsKeyDown(ImGuiKey_Tab); - if (mDoMain::developmentMode == 1 && mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigY(PAD_1)) { + if (mDoMain::developmentMode == 1 && (mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == (PAD_TRIGGER_R | PAD_TRIGGER_L) && mDoCPd_c::getTrigY(PAD_1)) { getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive; } if (mDoMain::developmentMode != 1) { diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index 69fb17d9cc..46b277f421 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -740,7 +740,7 @@ static void duskExecute() { } } - if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigY(PAD_1)) { + if ((mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == PAD_TRIGGER_R && mDoCPd_c::getTrigY(PAD_1)) { if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) { dynamic_cast(link)->handleQuickTransform(); } From 78d1596f662417cf2254dba565feffd234445817 Mon Sep 17 00:00:00 2001 From: Howard Luck Date: Sat, 18 Apr 2026 12:06:32 -0600 Subject: [PATCH 09/68] Frame Interp: d_meter_haihai, d_msg_out_font, d_meter_button * fix menu option haihai cursor and description icon animation speeds under frame interpolation * also fix d_meter_button --- src/d/d_meter_button.cpp | 20 +++++++++----- src/d/d_meter_haihai.cpp | 58 +++++++++++++++++++++++++--------------- src/d/d_msg_out_font.cpp | 21 ++++++++++++--- 3 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/d/d_meter_button.cpp b/src/d/d_meter_button.cpp index 88f750d6ca..d36a124d17 100644 --- a/src/d/d_meter_button.cpp +++ b/src/d/d_meter_button.cpp @@ -16,6 +16,7 @@ #include "d/d_msg_out_font.h" #include "d/d_msg_string.h" #include "d/d_pane_class.h" +#include "dusk/frame_interpolation.h" #include #if VERSION == VERSION_GCN_JPN @@ -280,15 +281,20 @@ void dMeterButton_c::draw() { s16 temp_r6 = g_drawHIO.mEmpButton.mRepeatHitFrameNum; s16 temp_r6_2 = g_drawHIO.mEmpButton.mRepeatHitFrameNum / 2; - field_0x4b8[i]++; +#ifdef TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x4b8[i]++; - if (field_0x4b8[i] >= temp_r6) { - field_0x4b8[i] = 0; + if (field_0x4b8[i] >= temp_r6) { + field_0x4b8[i] = 0; - if (field_0x4bc[i] == 0) { - field_0x4bc[i] = 1; - } else { - field_0x4bc[i] = 0; + if (field_0x4bc[i] == 0) { + field_0x4bc[i] = 1; + } else { + field_0x4bc[i] = 0; + } } } diff --git a/src/d/d_meter_haihai.cpp b/src/d/d_meter_haihai.cpp index daf30b9aae..5ea5dad5b7 100644 --- a/src/d/d_meter_haihai.cpp +++ b/src/d/d_meter_haihai.cpp @@ -11,6 +11,7 @@ #include "d/d_com_inf_game.h" #include "d/d_meter_HIO.h" #include "d/d_pane_class.h" +#include "dusk/frame_interpolation.h" dMeterHaihai_c::dMeterHaihai_c(u8 i_type) { mType = i_type; @@ -286,14 +287,19 @@ void dMeterHaihai_c::updateHaihai() { void dMeterHaihai_c::playBckAnime(J2DAnmTransformKey* i_bck) { if (checkPlayAnime(1)) { if (i_bck != NULL) { - if (mType == 4) { - mBckFrame += g_drawHIO.mWiiLockArrowBCKAnimSpeed; - } else { - mBckFrame += g_drawHIO.mScrollArrowBCKAnimSpeed; - } +#ifdef TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + if (mType == 4) { + mBckFrame += g_drawHIO.mWiiLockArrowBCKAnimSpeed; + } else { + mBckFrame += g_drawHIO.mScrollArrowBCKAnimSpeed; + } - if (mBckFrame >= i_bck->getFrameMax()) { - mBckFrame -= i_bck->getFrameMax(); + if (mBckFrame >= i_bck->getFrameMax()) { + mBckFrame -= i_bck->getFrameMax(); + } } } else { mBtkFrame = 1.0f; @@ -309,14 +315,19 @@ void dMeterHaihai_c::playBckAnime(J2DAnmTransformKey* i_bck) { void dMeterHaihai_c::playBtkAnime(J2DAnmTextureSRTKey* i_btk) { if (checkPlayAnime(2)) { if (i_btk != NULL) { - if (mType == 4) { - mBtkFrame += g_drawHIO.mWiiLockArrowBTKAnimSpeed; - } else { - mBtkFrame += g_drawHIO.mScrollArrowBTKAnimSpeed; - } +#ifdef TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + if (mType == 4) { + mBtkFrame += g_drawHIO.mWiiLockArrowBTKAnimSpeed; + } else { + mBtkFrame += g_drawHIO.mScrollArrowBTKAnimSpeed; + } - if (mBtkFrame >= i_btk->getFrameMax()) { - mBtkFrame -= i_btk->getFrameMax(); + if (mBtkFrame >= i_btk->getFrameMax()) { + mBtkFrame -= i_btk->getFrameMax(); + } } } else { mBtkFrame = 1.0f; @@ -331,14 +342,19 @@ void dMeterHaihai_c::playBtkAnime(J2DAnmTextureSRTKey* i_btk) { void dMeterHaihai_c::playBpkAnime(J2DAnmColor* i_bpk) { if (checkPlayAnime(0)) { if (i_bpk != NULL) { - if (mType == 4) { - mBpkFrame += g_drawHIO.mWiiLockArrowBPKAnimSpeed; - } else { - mBpkFrame += g_drawHIO.mScrollArrowBPKAnimSpeed; - } +#ifdef TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + if (mType == 4) { + mBpkFrame += g_drawHIO.mWiiLockArrowBPKAnimSpeed; + } else { + mBpkFrame += g_drawHIO.mScrollArrowBPKAnimSpeed; + } - if (mBpkFrame >= i_bpk->getFrameMax()) { - mBpkFrame -= i_bpk->getFrameMax(); + if (mBpkFrame >= i_bpk->getFrameMax()) { + mBpkFrame -= i_bpk->getFrameMax(); + } } } else { mBpkFrame = 1.0f; diff --git a/src/d/d_msg_out_font.cpp b/src/d/d_msg_out_font.cpp index 4d15b069d3..efc655d529 100644 --- a/src/d/d_msg_out_font.cpp +++ b/src/d/d_msg_out_font.cpp @@ -5,6 +5,7 @@ #include "JSystem/JUtility/JUTTexture.h" #include "d/d_meter2_info.h" #include "d/d_msg_object.h" +#include "dusk/frame_interpolation.h" #include "f_op/f_op_msg_mng.h" COutFontSet_c::COutFontSet_c() { @@ -311,6 +312,15 @@ void COutFont_c::draw(J2DTextBox* i_textbox, f32 param_1, f32 param_2, f32 param sp256[i] = field_0x1b4[i]; } +#ifdef TARGET_PC + bool uiTickPending = dusk::frame_interp::get_ui_tick_pending(); + if (!uiTickPending) { + for (int i = 0; i < 70; i++) { + sp256[i] = -1; + } + } +#endif + for (int i = 0; i < 35; i++) { u8 type = mpOfs[i]->getType(); J2DTextBox* tbox = mpOfs[i]->getTextBoxPtr(); @@ -505,9 +515,14 @@ void COutFont_c::draw(J2DTextBox* i_textbox, f32 param_1, f32 param_2, f32 param case 20: case 21: case 22: - field_0x1b4[type]++; - if (field_0x1b4[type] >= 28) { - field_0x1b4[type] = 0; +#ifdef TARGET_PC + if (uiTickPending) +#endif + { + field_0x1b4[type]++; + if (field_0x1b4[type] >= 28) { + field_0x1b4[type] = 0; + } } mpPane[type]->rotate(0.5f * sizeX, 0.5f * sizeY, ROTATE_Z, From 1c8bb1206ee0dcedadd1f6a4a1ae3402820762cb Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 18 Apr 2026 12:46:44 -0600 Subject: [PATCH 10/68] Unhackify mDoLib_project --- include/m_Do/m_Do_lib.h | 4 -- src/d/actor/d_a_alink_kandelaar.inc | 5 --- src/d/actor/d_a_balloon_2D.cpp | 4 -- src/d/actor/d_a_boomerang.cpp | 6 --- src/d/actor/d_a_demo00.cpp | 5 --- src/d/actor/d_a_e_fk.cpp | 6 --- src/d/actor/d_a_e_fs.cpp | 6 --- src/d/actor/d_a_e_sm.cpp | 6 --- src/d/actor/d_a_npc.cpp | 5 --- src/d/actor/d_a_obj_ari.cpp | 6 --- src/d/actor/d_a_obj_bhashi.cpp | 6 --- src/d/actor/d_a_obj_cho.cpp | 6 --- src/d/actor/d_a_obj_crvfence.cpp | 6 --- src/d/actor/d_a_obj_crvhahen.cpp | 6 --- src/d/actor/d_a_obj_dan.cpp | 6 --- src/d/actor/d_a_obj_gomikabe.cpp | 6 --- src/d/actor/d_a_obj_hhashi.cpp | 6 --- src/d/actor/d_a_obj_kamakiri.cpp | 6 --- src/d/actor/d_a_obj_katatsumuri.cpp | 6 --- src/d/actor/d_a_obj_kuwagata.cpp | 6 --- src/d/actor/d_a_obj_ten.cpp | 6 --- src/d/actor/d_a_obj_tombo.cpp | 6 --- src/d/actor/d_a_obj_zra_freeze.cpp | 5 --- src/d/actor/d_a_player.cpp | 6 --- src/d/d_insect.cpp | 4 -- src/d/d_kankyo.cpp | 4 -- src/d/d_kankyo_debug.cpp | 5 --- src/d/d_kankyo_rain.cpp | 43 ------------------- src/d/d_msg_object.cpp | 12 ------ src/d/d_msg_scrn_item.cpp | 11 ----- src/d/d_msg_scrn_talk.cpp | 11 ----- src/d/d_shop_system.cpp | 4 -- src/m_Do/m_Do_lib.cpp | 64 ----------------------------- 33 files changed, 294 deletions(-) diff --git a/include/m_Do/m_Do_lib.h b/include/m_Do/m_Do_lib.h index 8130cab8da..14e57ddf5a 100644 --- a/include/m_Do/m_Do_lib.h +++ b/include/m_Do/m_Do_lib.h @@ -43,10 +43,6 @@ struct mDoLib_clipper { }; void mDoLib_project(Vec* src, Vec* dst); -#if TARGET_PC -void mDoLib_project(Vec* src, Vec* dst, JGeometry::TBox2 viewport); -#endif - u32 mDoLib_setResTimgObj(ResTIMG const* res, TGXTexObj* o_texObj, u32 tlut_name, GXTlutObj* o_tlutObj); void mDoLib_pos2camera(Vec* src, Vec* dst); diff --git a/src/d/actor/d_a_alink_kandelaar.inc b/src/d/actor/d_a_alink_kandelaar.inc index 88c2152e03..339ff90ce6 100644 --- a/src/d/actor/d_a_alink_kandelaar.inc +++ b/src/d/actor/d_a_alink_kandelaar.inc @@ -180,12 +180,7 @@ void daAlink_c::preKandelaarDraw() { mat_p->setTevColor(2, &color); cXyz proj; - - #if TARGET_PC - mDoLib_project(&mKandelaarFlamePos, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&mKandelaarFlamePos, &proj); - #endif camera_process_class* camera_p = dComIfGp_getCamera(0); f32 trimHeight; diff --git a/src/d/actor/d_a_balloon_2D.cpp b/src/d/actor/d_a_balloon_2D.cpp index 6d9c66b464..6b5b5a2bd2 100644 --- a/src/d/actor/d_a_balloon_2D.cpp +++ b/src/d/actor/d_a_balloon_2D.cpp @@ -318,11 +318,7 @@ void daBalloon2D_c::addScoreCount(cXyz* param_1, u32 param_2, u8 param_3) { field_0x5f8[current].field_0xf = field_0x5f8[prev].field_0xf; } cXyz acStack_2c; - #if TARGET_PC - mDoLib_project(param_1, &acStack_2c, { 0, 0, FB_WIDTH, FB_HEIGHT }); - #else mDoLib_project(param_1, &acStack_2c); - #endif field_0x5f8[0].field_0x0.set(acStack_2c); field_0x5f8[0].field_0xc = param_2; field_0x5f8[0].field_0xe = 60; diff --git a/src/d/actor/d_a_boomerang.cpp b/src/d/actor/d_a_boomerang.cpp index 64dd71f985..590897640e 100644 --- a/src/d/actor/d_a_boomerang.cpp +++ b/src/d/actor/d_a_boomerang.cpp @@ -337,13 +337,7 @@ void daBoomerang_sight_c::setSight(const cXyz* i_pos, int i_no) { } Vec proj; - - #if TARGET_PC - mDoLib_project(&m_pos[i_no], &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&m_pos[i_no], &proj); - #endif - m_proj_posX[i_no] = proj.x; m_proj_posY[i_no] = proj.y; } diff --git a/src/d/actor/d_a_demo00.cpp b/src/d/actor/d_a_demo00.cpp index 871e9ebca6..99ad6c1587 100644 --- a/src/d/actor/d_a_demo00.cpp +++ b/src/d/actor/d_a_demo00.cpp @@ -1658,12 +1658,7 @@ int daDemo00_c::draw() { MTXCopy(mModel.field_0x5d4->getAnmMtx(0), mDoMtx_stack_c::get()); spb0.set(0.0f, 0.0f, 0.0f); mDoMtx_stack_c::multVec(&spb0, &sp98); - - #if TARGET_PC - mDoLib_project(&sp98, &spa4, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&sp98, &spa4); - #endif if (spa4.x >= -700.0f && spa4.x < 1600.0f && spa4.y >= -200.0f && spa4.y < 600.0f) { if (mModel.mID.field_0x18 == 0 || mModel.mID.field_0x18 == 1) { diff --git a/src/d/actor/d_a_e_fk.cpp b/src/d/actor/d_a_e_fk.cpp index 87734da262..5840df0495 100644 --- a/src/d/actor/d_a_e_fk.cpp +++ b/src/d/actor/d_a_e_fk.cpp @@ -429,13 +429,7 @@ void daE_FK_c::DamageAction() { bool daE_FK_c::checkViewArea() { Vec proj; - - #if TARGET_PC - mDoLib_project(¤t.pos, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(¤t.pos, &proj); - #endif - return (proj.x >= 0.0f && proj.x <= FB_WIDTH) && (proj.y >= 0.0f && proj.y <= FB_HEIGHT); } diff --git a/src/d/actor/d_a_e_fs.cpp b/src/d/actor/d_a_e_fs.cpp index 54dd10ad81..3ed859c686 100644 --- a/src/d/actor/d_a_e_fs.cpp +++ b/src/d/actor/d_a_e_fs.cpp @@ -463,13 +463,7 @@ static void damage_check(e_fs_class* i_this) { static bool checkViewArea(cXyz* i_pos) { Vec proj; - - #if TARGET_PC - mDoLib_project(i_pos, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(i_pos, &proj); - #endif - bool ret = false; if (proj.x >= 0.0f && proj.x <= FB_WIDTH && proj.y >= 0.0f && proj.y <= FB_HEIGHT) { ret = true; diff --git a/src/d/actor/d_a_e_sm.cpp b/src/d/actor/d_a_e_sm.cpp index 8894399cec..b85e22e640 100644 --- a/src/d/actor/d_a_e_sm.cpp +++ b/src/d/actor/d_a_e_sm.cpp @@ -1362,13 +1362,7 @@ void daE_SM_c::E_SM_C_Hook() { bool daE_SM_c::CheckViewArea() { Vec vec; - - #if TARGET_PC - mDoLib_project(¤t.pos, &vec, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(¤t.pos, &vec); - #endif - bool rv = false; if (vec.x >= 0.0f && vec.x <= FB_WIDTH && vec.y >= 0.0f && vec.y <= FB_HEIGHT) { diff --git a/src/d/actor/d_a_npc.cpp b/src/d/actor/d_a_npc.cpp index 6ec2fbe7a9..123bf66800 100644 --- a/src/d/actor/d_a_npc.cpp +++ b/src/d/actor/d_a_npc.cpp @@ -2694,12 +2694,7 @@ BOOL daNpcT_chkActorInScreen(fopAc_ac_c* i_ActorP, f32 param_1, f32 param_2, f32 } for (int i = 0; i < 8; i++) { - #if TARGET_PC - mDoLib_project(&pos_array[i], &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&pos_array[i], &proj); - #endif - if (0.0f < proj.x && proj.x < FB_WIDTH && 0.0f < proj.y && proj.y < FB_HEIGHT) { continue; } diff --git a/src/d/actor/d_a_obj_ari.cpp b/src/d/actor/d_a_obj_ari.cpp index 3e2727e2f2..267b0003af 100644 --- a/src/d/actor/d_a_obj_ari.cpp +++ b/src/d/actor/d_a_obj_ari.cpp @@ -499,13 +499,7 @@ void daObjARI_c::Z_BufferChk() { cXyz vec2, vec1; vec1 = current.pos; vec1.y += 20.0f; - - #if TARGET_PC - mDoLib_project(&vec1, &vec2, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&vec1, &vec2); - #endif - f32 trim_height; camera_process_class* camera = dComIfGp_getCamera(0); if (camera != NULL) { diff --git a/src/d/actor/d_a_obj_bhashi.cpp b/src/d/actor/d_a_obj_bhashi.cpp index 33595c7a77..00a35bc158 100644 --- a/src/d/actor/d_a_obj_bhashi.cpp +++ b/src/d/actor/d_a_obj_bhashi.cpp @@ -285,13 +285,7 @@ bool Hahen_c::CheckCull() { bool Hahen_c::checkViewArea() { Vec proj; - - #if TARGET_PC - mDoLib_project(&pos, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&pos, &proj); - #endif - return (proj.x >= -50.0f && proj.x <= 658.0f) && (proj.y >= -50.0f && proj.y <= 498.0f); } diff --git a/src/d/actor/d_a_obj_cho.cpp b/src/d/actor/d_a_obj_cho.cpp index 27b0b8ce0e..8fbc2dbde8 100644 --- a/src/d/actor/d_a_obj_cho.cpp +++ b/src/d/actor/d_a_obj_cho.cpp @@ -289,13 +289,7 @@ void daObjCHO_c::Z_BufferChk() { cXyz vec2, vec1; vec1 = current.pos; vec1.y += 20.0f; - - #if TARGET_PC - mDoLib_project(&vec1, &vec2, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&vec1, &vec2); - #endif - f32 trim_height; camera_process_class* camera = dComIfGp_getCamera(0); if (camera != NULL) { diff --git a/src/d/actor/d_a_obj_crvfence.cpp b/src/d/actor/d_a_obj_crvfence.cpp index 03ff2b1d6a..9712205ea3 100644 --- a/src/d/actor/d_a_obj_crvfence.cpp +++ b/src/d/actor/d_a_obj_crvfence.cpp @@ -224,13 +224,7 @@ void daObjCRVFENCE_c::NormalAction() { bool daObjCRVFENCE_c::checkViewArea(cXyz* param_1) { Vec sp24; - - #if TARGET_PC - mDoLib_project(param_1, &sp24, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(param_1, &sp24); - #endif - bool rv = false; bool bVar1 = false; diff --git a/src/d/actor/d_a_obj_crvhahen.cpp b/src/d/actor/d_a_obj_crvhahen.cpp index 0b44a57ec8..2dc91c6e6b 100644 --- a/src/d/actor/d_a_obj_crvhahen.cpp +++ b/src/d/actor/d_a_obj_crvhahen.cpp @@ -137,13 +137,7 @@ void daObjCRVHAHEN_c::CheckCull() { bool daObjCRVHAHEN_c::checkViewArea(cXyz* i_this) { Vec proj; - - #if TARGET_PC - mDoLib_project(i_this, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(i_this, &proj); - #endif - bool ret = false; if (proj.x >= 0.0f && proj.x <= FB_WIDTH && proj.y >= 0.0f && proj.y <= FB_HEIGHT) { diff --git a/src/d/actor/d_a_obj_dan.cpp b/src/d/actor/d_a_obj_dan.cpp index 53dbf4e1e4..cc176b9a20 100644 --- a/src/d/actor/d_a_obj_dan.cpp +++ b/src/d/actor/d_a_obj_dan.cpp @@ -267,13 +267,7 @@ void daObjDAN_c::Z_BufferChk() { cXyz vec2, vec1; vec1 = current.pos; vec1.y += 20.0f; - - #if TARGET_PC - mDoLib_project(&vec1, &vec2, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&vec1, &vec2); - #endif - f32 trim_height; camera_process_class* camera = dComIfGp_getCamera(0); if (camera != NULL) { diff --git a/src/d/actor/d_a_obj_gomikabe.cpp b/src/d/actor/d_a_obj_gomikabe.cpp index a5b58a44a5..66963f0efd 100644 --- a/src/d/actor/d_a_obj_gomikabe.cpp +++ b/src/d/actor/d_a_obj_gomikabe.cpp @@ -201,13 +201,7 @@ void daObjGOMIKABE_c::CheckCull() { bool daObjGOMIKABE_c::checkViewArea(cXyz param_1) { Vec local_24; - - #if TARGET_PC - mDoLib_project(¶m_1, &local_24, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(¶m_1, &local_24); - #endif - bool rv = false; if (local_24.x >= 0.0f && local_24.x <= FB_WIDTH && local_24.y >= 0.0f && local_24.y <= FB_HEIGHT) { rv = true; diff --git a/src/d/actor/d_a_obj_hhashi.cpp b/src/d/actor/d_a_obj_hhashi.cpp index ea45fa5ea1..d5ab5558b0 100644 --- a/src/d/actor/d_a_obj_hhashi.cpp +++ b/src/d/actor/d_a_obj_hhashi.cpp @@ -214,13 +214,7 @@ void daObjHHASHI_c::CheckCull() { bool daObjHHASHI_c::checkViewArea(int param_1) { Vec local_20; - - #if TARGET_PC - mDoLib_project(&field_0x5b0[param_1], &local_20, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&field_0x5b0[param_1], &local_20); - #endif - bool rv = false; if (local_20.x >= 0.0f && local_20.x <= FB_WIDTH && local_20.y >= 0.0f && local_20.y <= FB_HEIGHT) { rv = true; diff --git a/src/d/actor/d_a_obj_kamakiri.cpp b/src/d/actor/d_a_obj_kamakiri.cpp index daa6560e6f..2b641d7769 100644 --- a/src/d/actor/d_a_obj_kamakiri.cpp +++ b/src/d/actor/d_a_obj_kamakiri.cpp @@ -517,13 +517,7 @@ void daObjKAM_c::Z_BufferChk() { cXyz currentOffset; currentOffset = current.pos; currentOffset.y += 20.0f; - - #if TARGET_PC - mDoLib_project(¤tOffset, ¤tProj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(¤tOffset, ¤tProj); - #endif - camera_process_class* camera = dComIfGp_getCamera(0); f32 cameraHeight; if (camera != NULL) { diff --git a/src/d/actor/d_a_obj_katatsumuri.cpp b/src/d/actor/d_a_obj_katatsumuri.cpp index 03d40a1066..805aa37b44 100644 --- a/src/d/actor/d_a_obj_katatsumuri.cpp +++ b/src/d/actor/d_a_obj_katatsumuri.cpp @@ -611,13 +611,7 @@ void daObjKAT_c::Z_BufferChk() { cXyz curWithOff; curWithOff = current.pos; curWithOff.y += 20.0f; - - #if TARGET_PC - mDoLib_project(&curWithOff, &projected, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&curWithOff, &projected); - #endif - camera_process_class* camera = dComIfGp_getCamera(0); f32 unkFloat1; if (camera != NULL) { diff --git a/src/d/actor/d_a_obj_kuwagata.cpp b/src/d/actor/d_a_obj_kuwagata.cpp index 914e25dfb1..80020208d3 100644 --- a/src/d/actor/d_a_obj_kuwagata.cpp +++ b/src/d/actor/d_a_obj_kuwagata.cpp @@ -528,13 +528,7 @@ void daObjKUW_c::Z_BufferChk() { cStack_68 = current.pos; cStack_68.y += 20.0f; - - #if TARGET_PC - mDoLib_project(&cStack_68, &local_5c, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&cStack_68, &local_5c); - #endif - camera_process_class* cc = dComIfGp_getCamera(0); f32 trimHeight; if (cc != NULL) { diff --git a/src/d/actor/d_a_obj_ten.cpp b/src/d/actor/d_a_obj_ten.cpp index 1efe358607..fbedd6a8c9 100644 --- a/src/d/actor/d_a_obj_ten.cpp +++ b/src/d/actor/d_a_obj_ten.cpp @@ -593,13 +593,7 @@ void daObjTEN_c::Z_BufferChk() { cXyz cStack_68; cStack_68 = current.pos; cStack_68.y += 20.0f; - - #if TARGET_PC - mDoLib_project(&cStack_68, &local_5c, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&cStack_68, &local_5c); - #endif - camera_process_class* camera = dComIfGp_getCamera(0); f32 trimHeight; if (camera != NULL) { diff --git a/src/d/actor/d_a_obj_tombo.cpp b/src/d/actor/d_a_obj_tombo.cpp index 6fcbc938d9..a518e3cf25 100644 --- a/src/d/actor/d_a_obj_tombo.cpp +++ b/src/d/actor/d_a_obj_tombo.cpp @@ -504,13 +504,7 @@ void daObjTOMBO_c::Z_BufferChk() { cXyz cStack_68; cStack_68 = current.pos; cStack_68.y += 20.0f; - - #if TARGET_PC - mDoLib_project(&cStack_68, &local_5c, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&cStack_68, &local_5c); - #endif - camera_process_class* pCamera = dComIfGp_getCamera(0); f32 trimHeight; if (pCamera != NULL) { diff --git a/src/d/actor/d_a_obj_zra_freeze.cpp b/src/d/actor/d_a_obj_zra_freeze.cpp index 08932c439d..1525bdb572 100644 --- a/src/d/actor/d_a_obj_zra_freeze.cpp +++ b/src/d/actor/d_a_obj_zra_freeze.cpp @@ -38,12 +38,7 @@ BOOL daZraFreeze_c::chkActorInScreen() { mDoMtx_stack_c::transM(0.0f, 0.0f, 0.0f); PSMTXMultVecArray(mDoMtx_stack_c::get(), vec, vec, 8); for (int i = 0; i < 8; i++) { - #if TARGET_PC - mDoLib_project(&vec[i], &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&vec[i], &proj); - #endif - if (0.0f < proj.x && proj.x < FB_WIDTH && 0.0f < proj.y && proj.y < FB_HEIGHT) { continue; } diff --git a/src/d/actor/d_a_player.cpp b/src/d/actor/d_a_player.cpp index 4c8b31c4c6..8ceb0ef237 100644 --- a/src/d/actor/d_a_player.cpp +++ b/src/d/actor/d_a_player.cpp @@ -421,13 +421,7 @@ void daPy_sightPacket_c::draw() { void daPy_sightPacket_c::setSight() { Vec proj; - - #if TARGET_PC - mDoLib_project(&mPos, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&mPos, &proj); - #endif - mDoMtx_stack_c::transS(proj.x, proj.y, proj.z); mDoMtx_stack_c::scaleM(32.0f, 32.0f, 32.0f); mDoMtx_copy(mDoMtx_stack_c::get(), mProjMtx); diff --git a/src/d/d_insect.cpp b/src/d/d_insect.cpp index 380f545bd3..3f1d2fae11 100644 --- a/src/d/d_insect.cpp +++ b/src/d/d_insect.cpp @@ -82,11 +82,7 @@ void dInsect_c::CalcZBuffer(f32 param_0) { pos = current.pos; pos.y += 20.0f; - #if TARGET_PC - mDoLib_project(&pos, &pos_projected, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&pos, &pos_projected); - #endif if (dComIfGp_getCamera(0)) { camera_trim_height = dComIfGp_getCamera(0)->mCamera.mTrimHeight; diff --git a/src/d/d_kankyo.cpp b/src/d/d_kankyo.cpp index 9bb96efc82..c6739ee925 100644 --- a/src/d/d_kankyo.cpp +++ b/src/d/d_kankyo.cpp @@ -11004,11 +11004,7 @@ void dKy_depth_dist_set(void* process_p) { f32 var_f31 = sp24.abs(camera_p->view.lookat.eye); if (var_f31 < 2000.0f && var_f31 < kankyo->field_0x1268) { - #if TARGET_PC - mDoLib_project(&actor_p->eyePos, &sp30, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&actor_p->eyePos, &sp30); - #endif if ((sp30.x >= 0.0f && sp30.x < FB_WIDTH) && (sp30.y >= 0.0f && #if DEBUG diff --git a/src/d/d_kankyo_debug.cpp b/src/d/d_kankyo_debug.cpp index 03bfeac196..7fdad26ed2 100644 --- a/src/d/d_kankyo_debug.cpp +++ b/src/d/d_kankyo_debug.cpp @@ -915,12 +915,7 @@ void dKydb_dungeonlight_draw() { rot.y = 0; dDbVw_drawCubeXlu(player->current.pos, size, rot, color); - #if TARGET_PC - mDoLib_project(&g_env_light.dungeonlight[i].mPosition, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&g_env_light.dungeonlight[i].mPosition, &proj); - #endif - if (proj.x > 30.0f) { proj.x -= 30.0f; } diff --git a/src/d/d_kankyo_rain.cpp b/src/d/d_kankyo_rain.cpp index 31ba59e33e..64cf2247e0 100644 --- a/src/d/d_kankyo_rain.cpp +++ b/src/d/d_kankyo_rain.cpp @@ -116,12 +116,7 @@ void dKyr_lenzflare_move() { cXyz vect; cXyz proj; cXyz center; - - #if TARGET_PC - mDoLib_project(lenz_packet->mPositions, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(lenz_packet->mPositions, &proj); - #endif center.x = FB_WIDTH / 2; center.y = FB_HEIGHT / 2; @@ -221,12 +216,7 @@ void dKyr_sun_move() { } cXyz proj; - - #if TARGET_PC - mDoLib_project(sun_packet->mPos, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(sun_packet->mPos, &proj); - #endif for (int i = 0; i < 5; i++) { cXyz chkpnt = proj; @@ -4127,11 +4117,7 @@ void dKyr_drawStar(Mtx drawMtx, u8** tex) { } } -#if TARGET_PC - mDoLib_project(&moon_pos, &moon_proj, {0, 0, FB_WIDTH, FB_HEIGHT}); -#else mDoLib_project(&moon_pos, &moon_proj); -#endif // Dusk optimization: we use vertex color rather than GX_TEVREG0 to set star color. // This allows us to merge all the stars into a single draw. @@ -4280,11 +4266,7 @@ void dKyr_drawStar(Mtx drawMtx, u8** tex) { sp68.y = spBC.y + star_pos.y; sp68.z = spBC.z + star_pos.z; - #if TARGET_PC - mDoLib_project(&sp68, &star_proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&sp68, &star_proj); - #endif moon_proj.z = 0.0f; star_proj.z = 0.0f; @@ -4699,11 +4681,7 @@ void drawVrkumo(Mtx drawMtx, GXColor& color, u8** tex) { } if (g_env_light.daytime > 105.0f && g_env_light.daytime < 240.0f && !dComIfGp_event_runCheck() && sun_packet != NULL && sun_packet->mSunAlpha > 0.0f) { - #if TARGET_PC - mDoLib_project(&sun_packet->mPos[0], &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&sun_packet->mPos[0], &proj); - #endif if (proj.x > 0.0f && proj.x < FB_WIDTH && proj.y > spC4 && proj.y < (458.0f - spC4)) { pass = 0; } @@ -4968,12 +4946,7 @@ void drawVrkumo(Mtx drawMtx, GXColor& color, u8** tex) { x = 100.0f; y = 100.0f; z = 100.0f; - - #if TARGET_PC - mDoLib_project(&spF0, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&spF0, &proj); - #endif if (proj.x > -x && proj.x < (FB_WIDTH + x) && proj.y > -y && proj.y < (458.0f + z)) { break; @@ -5023,12 +4996,7 @@ void drawVrkumo(Mtx drawMtx, GXColor& color, u8** tex) { x = 100.0f; y = 100.0f; z = 100.0f; - - #if TARGET_PC - mDoLib_project(&spE4, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&spE4, &proj); - #endif if (proj.x > -x && proj.x < (FB_WIDTH + x) && proj.y > -y && proj.y < (458.0f + z)) { break; @@ -6068,13 +6036,7 @@ static void dKyr_evil_draw2(Mtx drawMtx, u8** tex) { sp34.x = 80.0f; sp34.y = 80.0f; sp34.z = 80.0f; - - #if TARGET_PC - mDoLib_project(&sp7C, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&sp7C, &proj); - #endif - if (!(proj.x > -sp34.x) || !(proj.x < (FB_WIDTH + sp34.x)) || !(proj.y > -sp34.y) || !(proj.y < (458.0f + sp34.z))) { @@ -6299,12 +6261,7 @@ void dKyr_evil_draw(Mtx drawMtx, u8** tex) { sp44.y = 80.0f; sp44.z = 80.0f; - #if TARGET_PC - mDoLib_project(&spA4, &proj, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&spA4, &proj); - #endif - if (!(proj.x > -sp44.x) || !(proj.x < (FB_WIDTH + sp44.x)) || !(proj.y > -sp44.y) || !(proj.y < (458.0f + sp44.z))) { diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp index 488af685db..9b7b64ebda 100644 --- a/src/d/d_msg_object.cpp +++ b/src/d/d_msg_object.cpp @@ -1468,24 +1468,12 @@ void dMsgObject_c::fukiPosCalc(bool param_1) { fopAc_ac_c* player = dComIfGp_getPlayer(0); cXyz local_3c; cXyz cStack_48; - - #if TARGET_PC - mDoLib_project(&player->eyePos, &cStack_48, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&player->eyePos, &cStack_48); - #endif - f32 temp; if ((field_0x100->pos == cXyz(0.0f, 0.0f, 0.0f))) { temp = cStack_48.y; } else { - - #if TARGET_PC - mDoLib_project(&field_0x100->pos, &local_3c, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&field_0x100->pos, &local_3c); - #endif - if (local_3c.x >= 0.0f && local_3c.x <= FB_WIDTH && local_3c.y >= 0.0f && local_3c.y <= FB_HEIGHT) { diff --git a/src/d/d_msg_scrn_item.cpp b/src/d/d_msg_scrn_item.cpp index 53ef97607c..672700682d 100644 --- a/src/d/d_msg_scrn_item.cpp +++ b/src/d/d_msg_scrn_item.cpp @@ -557,22 +557,11 @@ void dMsgScrnItem_c::fukiPosCalc(u8 param_1) { cXyz local_70; cXyz cStack_7c; f32 f3; - - #if TARGET_PC - mDoLib_project(&player->eyePos, &cStack_7c, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&player->eyePos, &cStack_7c); - #endif - if (iVar6->pos == cXyz(0.0f, 0.0f, 0.0f)) { f3 = cStack_7c.y; } else { - #if TARGET_PC - mDoLib_project(&iVar6->pos, &local_70, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&iVar6->pos, &local_70); - #endif - if (local_70.x >= 0.0f && local_70.x <= FB_WIDTH && local_70.y >= 0.0f && local_70.y <= FB_HEIGHT) { diff --git a/src/d/d_msg_scrn_talk.cpp b/src/d/d_msg_scrn_talk.cpp index 68b471a14d..c9f925e784 100644 --- a/src/d/d_msg_scrn_talk.cpp +++ b/src/d/d_msg_scrn_talk.cpp @@ -441,22 +441,11 @@ void dMsgScrnTalk_c::fukiPosCalc(u8 param_1) { cXyz local_70; cXyz cStack_7c; f32 f3y; - - #if TARGET_PC - mDoLib_project(&player->eyePos, &cStack_7c, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&player->eyePos, &cStack_7c); - #endif - if (msgActor->pos == cXyz(0.0f, 0.0f, 0.0f)) { f3y = cStack_7c.y; } else { - #if TARGET_PC - mDoLib_project(&msgActor->pos, &local_70, {0, 0, FB_WIDTH, FB_HEIGHT}); - #else mDoLib_project(&msgActor->pos, &local_70); - #endif - if (local_70.x >= 0.0f && local_70.x <= FB_WIDTH_BASE && local_70.y >= 0.0f && local_70.y <= FB_HEIGHT_BASE) { diff --git a/src/d/d_shop_system.cpp b/src/d/d_shop_system.cpp index f1636bb093..00be7e6df3 100644 --- a/src/d/d_shop_system.cpp +++ b/src/d/d_shop_system.cpp @@ -855,11 +855,7 @@ int dShopSystem_c::seq_wait(fopAc_ac_c* param_0, dMsgFlow_c* param_1) { } inline void pos3Dto2D(Vec* a, Vec* b) { -#if TARGET_PC - mDoLib_project(a, b, {0, 0, FB_WIDTH, FB_HEIGHT}); -#else mDoLib_project(a, b); -#endif } int dShopSystem_c::seq_start(fopAc_ac_c* actor, dMsgFlow_c* i_flow) { diff --git a/src/m_Do/m_Do_lib.cpp b/src/m_Do/m_Do_lib.cpp index b541490353..e67cc6abf1 100644 --- a/src/m_Do/m_Do_lib.cpp +++ b/src/m_Do/m_Do_lib.cpp @@ -111,70 +111,6 @@ void mDoLib_project(Vec* src, Vec* dst) { dst->y = ((0.5f + (multVec.y * (-calcFloat))) * ySize) + yOffset; } - -#if TARGET_PC -void mDoLib_project(Vec* src, Vec* dst, JGeometry::TBox2 viewport) { - if (dComIfGd_getView() == NULL) { - dst->x = 0.0f; - dst->y = 0.0f; - dst->z = 0.0f; - return; - } - - { int unused; } - - Vec multVec; - cMtx_multVec(*dComIfGd_getProjViewMtx(), src, &multVec); - - f32 calcFloat = (src->x * (*dComIfGd_getProjViewMtx())[3][0]) + - (src->y * (*dComIfGd_getProjViewMtx())[3][1]) + - (src->z * (*dComIfGd_getProjViewMtx())[3][2]) + - (*dComIfGd_getProjViewMtx())[3][3]; - if (multVec.z >= 0.0f) { - multVec.z = 0.0f; - } - if (calcFloat <= 0.0f) { - if (calcFloat == 0.0f) { - dst->z = multVec.z * 500000.0f; - } else { - dst->z = multVec.z * (0.5f / calcFloat); - } - calcFloat = 500000.0f; - } else { - calcFloat = 0.5f / calcFloat; - dst->z = multVec.z * calcFloat; - } - - f32 xOffset; - f32 yOffset; - f32 xSize; - f32 ySize; - if (viewport.i.x != 0.0f) { - xOffset = (0.5f * ((2.0f * viewport.i.x) + viewport.f.x)) - (int)(FB_WIDTH / 2); - xSize = FB_WIDTH; - } else { - #if TARGET_PC - xOffset = mDoGph_gInf_c::getSafeMinXF(); - xSize = viewport.f.x * mDoGph_gInf_c::hudAspectScaleUp; - #else - xOffset = viewport.i.x; - xSize = viewport.f.x; - #endif - } - - if (viewport.i.y != 0.0f) { - yOffset = (0.5f * ((2.0f * viewport.i.y) + viewport.f.y)) - (int)(FB_HEIGHT / 2); - ySize = FB_HEIGHT; - } else { - yOffset = viewport.i.y; - ySize = viewport.f.y; - } - - dst->x = ((0.5f + (multVec.x * calcFloat)) * xSize) + xOffset; - dst->y = ((0.5f + (multVec.y * (-calcFloat))) * ySize) + yOffset; -} -#endif - void mDoLib_pos2camera(Vec* src, Vec* dst) { if (dComIfGd_getView() == NULL) { dst->x = 0.0f; From 7b8f9c6f46eb7fc0303267324dc786f7c6123c8f Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 18 Apr 2026 13:08:14 -0600 Subject: [PATCH 11/68] Fix blur scale in mDoGph_gInf_c::bloom_c::draw2 --- src/m_Do/m_Do_graphic.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index 95c9a76d1b..d949d10ff0 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -1463,7 +1463,7 @@ void mDoGph_gInf_c::bloom_c::draw2() { GXTexObj* texPass0 = divCopyTex(Pass0, 2); GXLoadTexObj(texPass0, GX_TEXMAP0); - f32 blurScale = mBlureSize * ((448.0f / getHeight()) / 6400.0f); + f32 blurScale = mBlureSize * ((448.0f / height) / 6400.0f); // Setup blur filter TEV. GXSetNumTexGens(8); From b77ef63d5ecda891d3b4904744e494ed1542f230 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 18 Apr 2026 13:34:22 -0600 Subject: [PATCH 12/68] Reintroduce x offset change for mDoLib_project --- src/m_Do/m_Do_lib.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/m_Do/m_Do_lib.cpp b/src/m_Do/m_Do_lib.cpp index e67cc6abf1..d5fa16c7db 100644 --- a/src/m_Do/m_Do_lib.cpp +++ b/src/m_Do/m_Do_lib.cpp @@ -95,8 +95,13 @@ void mDoLib_project(Vec* src, Vec* dst) { xOffset = (0.5f * ((2.0f * viewPort->x_orig) + viewPort->width)) - (int)(FB_WIDTH / 2); xSize = FB_WIDTH; } else { +#if TARGET_PC + xOffset = mDoGph_gInf_c::getSafeMinXF(); + xSize = viewPort->width * mDoGph_gInf_c::hudAspectScaleUp; +#else xOffset = viewPort->x_orig; xSize = viewPort->width; +#endif } if (viewPort->y_orig != 0.0f) { From 85120648b12a0a3f09ed75a38972df7b7c5d5fc2 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 18 Apr 2026 14:04:28 -0600 Subject: [PATCH 13/68] Fix shadow draw viewport/scissor overflow --- src/d/d_drawlist.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/d/d_drawlist.cpp b/src/d/d_drawlist.cpp index e903e39ce4..2ab23b0bc9 100644 --- a/src/d/d_drawlist.cpp +++ b/src/d/d_drawlist.cpp @@ -1544,9 +1544,10 @@ void dDlst_shadowControl_c::imageDraw(Mtx param_0) { #ifdef TARGET_PC GXCreateFrameBuffer(r26, r26); needsRestore = true; -#endif +#else GXSetViewport(0.0f, 0.0f, r26, r26, 0.0f, 1.0f); GXSetScissor(0, 0, r26, r26); +#endif } GXSetTevColor(GX_TEVREG0, l_imageDrawColor[chan]); if (chan == 3) { From 56239e77ffa8cd277be7c2886657c187a9986da3 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 18 Apr 2026 14:16:24 -0600 Subject: [PATCH 14/68] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 62c7ef68e8..c05e7aace1 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 62c7ef68e895ea91c5389f381bd78e163f480d58 +Subproject commit c05e7aace17ab4380a90ddb899d89dd9565e6f78 From a9f8595901e70fda366e8c3017babc20d58c52c1 Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Sat, 18 Apr 2026 14:27:01 -0600 Subject: [PATCH 15/68] fix interpolation for cat cs, pop up 2d text --- src/d/actor/d_a_npc_ne.cpp | 19 +++++++++ src/d/d_meter_string.cpp | 34 ++++++++++++---- src/d/d_timer.cpp | 81 ++++++++++++++++++++------------------ 3 files changed, 88 insertions(+), 46 deletions(-) diff --git a/src/d/actor/d_a_npc_ne.cpp b/src/d/actor/d_a_npc_ne.cpp index ae43981593..1324c56cf5 100644 --- a/src/d/actor/d_a_npc_ne.cpp +++ b/src/d/actor/d_a_npc_ne.cpp @@ -18,6 +18,7 @@ #include "f_op/f_op_kankyo_mng.h" #include "c/c_damagereaction.h" #include "Z2AudioLib/Z2Instances.h" +#include "dusk/frame_interpolation.h" #include static home_path_pnt home_path[38] = { @@ -2655,6 +2656,9 @@ static void demo_camera(npc_ne_class* i_this) { i_this->mCameraFovY = 55.0f; camera->mCamera.SetTrimSize(3); daPy_getPlayerActorClass()->changeOriginalDemo(); +#ifdef TARGET_PC + dusk::frame_interp::request_presentation_sync(); +#endif // fallthrough case 2: @@ -2683,6 +2687,9 @@ static void demo_camera(npc_ne_class* i_this) { if (i_this->mDemoCounter == 0) { i_this->mCameraCenter1.set(387.0f, 133.0f, -866.0f); i_this->mCameraEye1.set(284.0f, 208.0f, -585.0f); +#ifdef TARGET_PC + dusk::frame_interp::request_presentation_sync(); +#endif } if (i_this->mDemoCounter == 12) { @@ -2719,6 +2726,9 @@ static void demo_camera(npc_ne_class* i_this) { i_this->mCameraFovY = 45.0f; camera->mCamera.SetTrimSize(3); daPy_getPlayerActorClass()->changeOriginalDemo(); +#ifdef TARGET_PC + dusk::frame_interp::request_presentation_sync(); +#endif // fallthrough case 11: @@ -2799,8 +2809,14 @@ static void demo_camera(npc_ne_class* i_this) { MtxPosition(&vec, &i_this->mCameraEye2); i_this->mCameraEye2 += player->current.pos; player->changeDemoParam2(2); +#ifdef TARGET_PC + dusk::frame_interp::request_presentation_sync(); +#endif } else if (i_this->mDemoCounter == 120) { player->changeDemoParam2(0); +#ifdef TARGET_PC + dusk::frame_interp::request_presentation_sync(); +#endif } } } @@ -2853,6 +2869,9 @@ static void demo_camera(npc_ne_class* i_this) { i_this->mCameraCenter1 = _this->current.pos; i_this->mCameraCenter1.y += 20.0f; i_this->mCameraFovY = 55.0f; +#ifdef TARGET_PC + dusk::frame_interp::request_presentation_sync(); +#endif } camera->mCamera.Set(i_this->mCameraCenter1, i_this->mCameraEye1, diff --git a/src/d/d_meter_string.cpp b/src/d/d_meter_string.cpp index ad95c53edd..c28d6d5481 100644 --- a/src/d/d_meter_string.cpp +++ b/src/d/d_meter_string.cpp @@ -16,6 +16,7 @@ #include "d/d_meter2_info.h" #include "d/d_meter_HIO.h" #include "d/d_pane_class.h" +#include "dusk/frame_interpolation.h" #include dMeterString_c::dMeterString_c(int i_stringID) { @@ -105,16 +106,27 @@ void dMeterString_c::draw() { f32 var_f30 = 1.0f; if (mAnimFrame < 60.0f) { - mAnimFrame += g_drawHIO.mMiniGame.mReadyFightTextAnimSpeed; - if (mAnimFrame > 60.0f) { - mAnimFrame = 60.0f; +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + mAnimFrame += g_drawHIO.mMiniGame.mReadyFightTextAnimSpeed; + if (mAnimFrame > 60.0f) { + mAnimFrame = 60.0f; + } } playBckAnimation(mAnimFrame); } else if (mAnimFrame < (f32)g_drawHIO.mMiniGame.mReadyFightTextWaitFrames + 60.0f) { - mAnimFrame += var_f30; +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + mAnimFrame += var_f30; } else if (mAnimFrame < var_f31) { - mAnimFrame += var_f30; +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + mAnimFrame += var_f30; var_f30 = acc(g_drawHIO.mMiniGame.field_0x172, var_f31 - mAnimFrame, 0); } @@ -128,13 +140,21 @@ void dMeterString_c::draw() { if (mPikariAnimFrame > 0.0f) { drawPikari(); } else if (mPikariAnimFrame == -1.0f && +#if TARGET_PC + dusk::frame_interp::get_ui_tick_pending() && +#endif mAnimFrame > g_drawHIO.mMiniGame.mReadyFightPikariAppearFrames) { mPikariAnimFrame = 18.0f - g_drawHIO.mMiniGame.mReadyFightPikariAnimSpeed; } - if (mAnimFrame >= var_f31) { - dMeter2Info_resetMeterString(); +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + if (mAnimFrame >= var_f31) { + dMeter2Info_resetMeterString(); + } } } } diff --git a/src/d/d_timer.cpp b/src/d/d_timer.cpp index 2e89ccbbef..cb40e3444a 100644 --- a/src/d/d_timer.cpp +++ b/src/d/d_timer.cpp @@ -23,9 +23,7 @@ #include "m_Do/m_Do_lib.h" #include -#if TARGET_PC #include "dusk/frame_interpolation.h" -#endif static int dTimer_createStart2D(s32 param_0, u16 param_1); @@ -1340,11 +1338,11 @@ void dDlst_TimerScrnDraw_c::draw() { f32 temp = (f32)g_drawHIO.mMiniGame.mGetInTextAlphaFrames + ((f32)g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f); + for (int i = 0; i < 51; i++) { #if TARGET_PC - if (dusk::frame_interp::get_ui_tick_pending()) + if (dusk::frame_interp::get_ui_tick_pending()) #endif - { - for (int i = 0; i < 51; i++) { + { if (m_getin_info[i].bck_frame > 0.0f && m_getin_info[i].bck_frame < temp) { if (m_getin_info[i].bck_frame < 60.0f) { m_getin_info[i].bck_frame += g_drawHIO.mMiniGame.mGetInTextAnimSpeed; @@ -1357,45 +1355,50 @@ void dDlst_TimerScrnDraw_c::draw() { m_getin_info[i].bck_frame++; } } + } - if (m_getin_info[i].bck_frame > 0.0f && m_getin_info[i].bck_frame < temp) { - f32 var_f29 = 1.0f; - if (m_getin_info[i].bck_frame >= g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f && - m_getin_info[i].bck_frame < temp) { - var_f29 = acc(g_drawHIO.mMiniGame.mGetInTextAlphaFrames, - temp - m_getin_info[i].bck_frame, 0); + if (m_getin_info[i].bck_frame > 0.0f && m_getin_info[i].bck_frame < temp) { + f32 var_f29 = 1.0f; + if (m_getin_info[i].bck_frame >= g_drawHIO.mMiniGame.mGetInTextWaitFrames + 60.0f && + m_getin_info[i].bck_frame < temp) { + var_f29 = acc(g_drawHIO.mMiniGame.mGetInTextAlphaFrames, + temp - m_getin_info[i].bck_frame, 0); + } + + if (m_getin_info[i].bck_frame < 60.0f) { + playBckAnimation(m_getin_info[i].bck_frame); + } else { + playBckAnimation(60.0f); + } + + mpGetInParent->setAlphaRate(var_f29); + + if (g_drawHIO.mMiniGame.mGetInTextLocation == 1) { + mpGetInRoot->translate(m_getin_info[i].pos_x + g_drawHIO.mMiniGame.mGetInTextPosX, + m_getin_info[i].pos_y + g_drawHIO.mMiniGame.mGetInTextPosY); + } else { + f32 temp_f2 = m_getin_info[i].bck_frame - 40.0f; + f32 var_f3 = ((temp_f2 * 0.5f) * temp_f2) * 0.15f; + if (i == 0) { + var_f3 = 0.0f; } - if (m_getin_info[i].bck_frame < 60.0f) { - playBckAnimation(m_getin_info[i].bck_frame); - } else { - playBckAnimation(60.0f); - } + mpGetInRoot->paneTrans( + m_getin_info[i].pos_x + g_drawHIO.mMiniGame.mGetInTextPosX, + (m_getin_info[i].pos_y + g_drawHIO.mMiniGame.mGetInTextPosY) - var_f3); + } - mpGetInParent->setAlphaRate(var_f29); + mpGetInRoot->scale(g_drawHIO.mMiniGame.mGetInTextSizeX, + g_drawHIO.mMiniGame.mGetInTextSizeY); + mpGetInScreen->draw(0.0f, 0.0f, graf_ctx); - if (g_drawHIO.mMiniGame.mGetInTextLocation == 1) { - mpGetInRoot->translate(m_getin_info[i].pos_x + g_drawHIO.mMiniGame.mGetInTextPosX, - m_getin_info[i].pos_y + g_drawHIO.mMiniGame.mGetInTextPosY); - } else { - f32 temp_f2 = m_getin_info[i].bck_frame - 40.0f; - f32 var_f3 = ((temp_f2 * 0.5f) * temp_f2) * 0.15f; - if (i == 0) { - var_f3 = 0.0f; - } - - mpGetInRoot->paneTrans( - m_getin_info[i].pos_x + g_drawHIO.mMiniGame.mGetInTextPosX, - (m_getin_info[i].pos_y + g_drawHIO.mMiniGame.mGetInTextPosY) - var_f3); - } - - mpGetInRoot->scale(g_drawHIO.mMiniGame.mGetInTextSizeX, - g_drawHIO.mMiniGame.mGetInTextSizeY); - mpGetInScreen->draw(0.0f, 0.0f, graf_ctx); - - if (m_getin_info[i].pikari_frame > 0.0f) { - drawPikari(i); - } else if (m_getin_info[i].pikari_frame == -1.0f) { + if (m_getin_info[i].pikari_frame > 0.0f) { + drawPikari(i); + } else if (m_getin_info[i].pikari_frame == -1.0f) { +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { if (m_getin_info[i].field_0xc == 0) { if (m_getin_info[i].bck_frame > g_drawHIO.mMiniGame.mGetInPikariAppearFrames) { m_getin_info[i].pikari_frame = From f02a39d921f5f7ee5fe20fb8a614fb4f1cb7eaa9 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 18 Apr 2026 16:19:59 -0600 Subject: [PATCH 16/68] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index c05e7aace1..672f1e8e5e 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit c05e7aace17ab4380a90ddb899d89dd9565e6f78 +Subproject commit 672f1e8e5ee0c47c9a829bc618883b1520d8cbec From 09e3e17aa6089d7cf7077483e7a138ad80a5c54f Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 18 Apr 2026 16:23:15 -0600 Subject: [PATCH 17/68] Disable vendored dawn for x-macos-ci --- CMakePresets.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CMakePresets.json b/CMakePresets.json index 90307aa061..89c78818f9 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -148,8 +148,7 @@ "cacheVariables": { "CMAKE_C_COMPILER": "cl", "CMAKE_CXX_COMPILER": "cl", - "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install", - "AURORA_DAWN_PROVIDER": "vendor" + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install" }, "vendor": { "microsoft.com/VisualStudioSettings/CMake/1.0": { @@ -382,7 +381,6 @@ "ci" ], "cacheVariables": { - "AURORA_DAWN_PROVIDER": "vendor", "AURORA_NOD_PROVIDER": "vendor", "CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": { "type": "BOOL", From 5bcc96977877c4fc1718b360679fe34a2595cf1c Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Sat, 18 Apr 2026 16:53:48 -0600 Subject: [PATCH 18/68] fix file select fade not rendering with frame interpolation --- include/d/d_file_select.h | 16 ++++++++++++++++ src/d/d_file_select.cpp | 14 ++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/include/d/d_file_select.h b/include/d/d_file_select.h index 72f7cb7f42..cf081a3b10 100644 --- a/include/d/d_file_select.h +++ b/include/d/d_file_select.h @@ -10,6 +10,7 @@ #include "JSystem/J3DGraphLoader/J3DAnmLoader.h" class dFile_info_c; +class J2DPicture; class dDlst_FileSel_c : public dDlst_base_c { public: @@ -113,6 +114,14 @@ public: /* 0x04 */ J2DScreen* Scr3m; }; +class dDlst_FileSelFade_c : public dDlst_base_c { +public: + void draw(); + virtual ~dDlst_FileSelFade_c() {} + + /* 0x04 */ J2DPicture* mpPict; +}; + class dFs_HIO_c : public JORReflexible { public: dFs_HIO_c(); @@ -676,6 +685,9 @@ public: #if PLATFORM_GCN /* 0x2378 */ J2DPicture* mpFadePict; #endif +#ifdef TARGET_PC + dDlst_FileSelFade_c mFadeDlst; +#endif #if PLATFORM_WII || PLATFORM_SHIELD /* 0x2376 */ u8 field_0x2376[SAVEFILE_SIZE]; @@ -684,6 +696,10 @@ public: #endif }; +#ifdef TARGET_PC +STATIC_ASSERT(sizeof(dFile_select_c) == 0x237C + sizeof(dDlst_FileSelFade_c)); +#else STATIC_ASSERT(sizeof(dFile_select_c) == 0x237C); +#endif #endif /* D_FILE_D_FILE_SELECT_H */ diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 99a777929c..805c37a418 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -3204,6 +3204,9 @@ void dFile_select_c::screenSet() { timg, NULL); mpFadePict->setBlackWhite(black, white); mpFadePict->setAlpha(0); +#ifdef TARGET_PC + mFadeDlst.mpPict = mpFadePict; +#endif #endif } @@ -3870,10 +3873,14 @@ void dFile_select_c::_draw() { dComIfGd_set2DOpa(mSelIcon2); #if PLATFORM_GCN + #if TARGET_PC + dComIfGd_set2DOpaTop(&mFadeDlst); + #else mpFadePict->draw(mDoGph_gInf_c::getMinXF(), mDoGph_gInf_c::getMinYF(), mDoGph_gInf_c::getWidthF(), mDoGph_gInf_c::getHeightF(), false, false, false); #endif + #endif } } @@ -3917,6 +3924,13 @@ void dDlst_FileSel3m_c::draw() { Scr3m->draw(0.0f, 0.0f, graf); } +#ifdef TARGET_PC +void dDlst_FileSelFade_c::draw() { + mpPict->draw(mDoGph_gInf_c::getMinXF(), mDoGph_gInf_c::getMinYF(), + mDoGph_gInf_c::getWidthF(), mDoGph_gInf_c::getHeightF(), false, false, false); +} +#endif + void dFile_select_c::errorMoveAnmInitSet(int param_1, int param_2) { mErrorMsgPane->setAnimation(field_0x0090); field_0x0130 = param_1; From d17c629ce0cacfc6b1cbe35877986f8e4f75e9ae Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 18 Apr 2026 15:54:42 -0700 Subject: [PATCH 19/68] fix bulblin eye glow --- src/d/actor/d_a_e_rd.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/d/actor/d_a_e_rd.cpp b/src/d/actor/d_a_e_rd.cpp index 3067cc456a..e540e7dccd 100644 --- a/src/d/actor/d_a_e_rd.cpp +++ b/src/d/actor/d_a_e_rd.cpp @@ -6601,13 +6601,14 @@ static int daE_RD_Execute(e_rd_class* i_this) { 1.2f, }; + #if AVOID_UB + s16 x = 0; + s16 y = 0; + #endif for (int i = 0; i < 2; i++) { MtxPush(); + #if !AVOID_UB s16 x, y; - - #if AVOID_UB - x = 0; - y = 0; #endif if (i == 0) { From 623f29eb24e84ff3a0cf53f3644e34d975c0745a Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sat, 18 Apr 2026 18:08:33 -0700 Subject: [PATCH 20/68] volume setting effects movie now --- src/d/actor/d_a_movie_player.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/d/actor/d_a_movie_player.cpp b/src/d/actor/d_a_movie_player.cpp index 4cc7313267..d972bddd4d 100644 --- a/src/d/actor/d_a_movie_player.cpp +++ b/src/d/actor/d_a_movie_player.cpp @@ -4378,6 +4378,8 @@ static void daMP_ActivePlayer_Draw() { daMP_DrawPosX = static_cast(rect.PosX); daMP_DrawPosY = static_cast(rect.PosY); + + daMP_THPPlayerSetVolume((dusk::getSettings().audio.masterVolume / 100.0f) * 127.0f, 0); #endif int frame = daMP_THPPlayerDrawCurrentFrame( From 33101d805021401c8fcb1118661c98e9157b5c00 Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Sat, 18 Apr 2026 19:34:38 -0600 Subject: [PATCH 21/68] fix minimap tear of light flash not respecting interpolation --- src/d/d_menu_fmap2D.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index 3d3b96c57f..a3afaa1641 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -342,8 +342,13 @@ void dMenu_Fmap2DBack_c::draw() { scrollAreaDraw(); } - blinkMove(30); - moveLightDropAnime(); +#ifdef TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + blinkMove(30); + moveLightDropAnime(); + } setCenterPosX(field_0x11dc, 1); drawIcon(mTransX, mTransZ, mAlphaRate, field_0xfa8 * mSpotTextureFadeAlpha); From fcfcb35929f90ff70520a72e36df91e4b7e97e14 Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Sat, 18 Apr 2026 19:50:31 -0600 Subject: [PATCH 22/68] fix JAISeqMgr beginStartSeq_ to check free memory before allocation --- libs/JSystem/src/JAudio2/JAISeqMgr.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/libs/JSystem/src/JAudio2/JAISeqMgr.cpp b/libs/JSystem/src/JAudio2/JAISeqMgr.cpp index a1f3554815..1d0e42d47e 100644 --- a/libs/JSystem/src/JAudio2/JAISeqMgr.cpp +++ b/libs/JSystem/src/JAudio2/JAISeqMgr.cpp @@ -120,11 +120,19 @@ void JAISeqMgr::mixOut() { } JAISeq* JAISeqMgr::beginStartSeq_() { - JAISeq* seq = JKR_NEW JAISeq(this, field_0x10); +#ifdef TARGET_PC + if (JAISeq::getFreeMemCount() == 0) { + JUT_WARN(273, "%s", "JASPoolAllocObject::::operator new failed .\n"); + return NULL; + } + return JKR_NEW JAISeq(this, field_0x10); +#else + JAISeq* seq = new JAISeq(this, field_0x10); if (seq == NULL) { JUT_WARN(273, "%s", "JASPoolAllocObject::::operator new failed .\n"); } return seq; +#endif } bool JAISeqMgr::endStartSeq_(JAISeq* seq, JAISoundHandle* handle) { From cd7e429a66cbf8087d20c238609706d7a7ad91f4 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Sat, 18 Apr 2026 21:33:02 -0700 Subject: [PATCH 23/68] Fix memory card not properly attaching on init. (#433) * Fix memory card not properly attaching on init. Previously the card status was forced to ready which caused quite a bit of default state getting set properly, reverting that and setting mCardCommand to `COMM_ATTACH_e` allows the memory card system to properly probe and detect the card status. A companion fix in aurora addresses the "Memory Card corrupted" error message. * Update aurora --- extern/aurora | 2 +- src/m_Do/m_Do_MemCard.cpp | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/extern/aurora b/extern/aurora index 672f1e8e5e..20a23c7d22 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 672f1e8e5ee0c47c9a829bc618883b1520d8cbec +Subproject commit 20a23c7d22aa965607260b30f89b7bbd2bf36f40 diff --git a/src/m_Do/m_Do_MemCard.cpp b/src/m_Do/m_Do_MemCard.cpp index 36de5480e5..a0c0302b09 100644 --- a/src/m_Do/m_Do_MemCard.cpp +++ b/src/m_Do/m_Do_MemCard.cpp @@ -84,11 +84,12 @@ void mDoMemCd_Ctrl_c::ThdInit() { mProbeStat = 2; mCardState = CARD_STATE_NO_CARD_e; - #if TARGET_PC - mCardState = CARD_STATE_READY_e; - #endif - +#if TARGET_PC + mCardCommand = COMM_ATTACH_e; +#else mCardCommand = COMM_NONE_e; +#endif + mChannel = SLOT_A; OSInitMutex(&mMutex); From 6dcf4942f5c143b4b559626ba17a2f218b80e39e Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Sat, 18 Apr 2026 21:51:28 -0700 Subject: [PATCH 24/68] Update aurora for CARDFormat speedup --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 20a23c7d22..b1957f10cf 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 20a23c7d22aa965607260b30f89b7bbd2bf36f40 +Subproject commit b1957f10cf9e7ea1e0e012c1968014bd11299297 From b3f8fecfe4d8b04f176a2acb1f7bffd1f606c333 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sat, 18 Apr 2026 22:17:19 -0700 Subject: [PATCH 25/68] water interpolation fix --- include/d/d_com_inf_game.h | 6 +-- include/dusk/frame_interpolation.h | 7 +++ .../JSystem/J3DGraphAnimator/J3DModel.h | 4 ++ .../JSystem/J3DGraphAnimator/J3DModelData.h | 4 ++ .../JSystem/J3DGraphBase/J3DMaterial.h | 4 +- .../JSystem/src/J3DGraphAnimator/J3DModel.cpp | 13 ++++++ .../src/J3DGraphAnimator/J3DModelData.cpp | 9 ++++ libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp | 22 +++++++++ src/dusk/frame_interpolation.cpp | 45 ++++++++++++++++++- src/m_Do/m_Do_main.cpp | 1 + 10 files changed, 108 insertions(+), 7 deletions(-) diff --git a/include/d/d_com_inf_game.h b/include/d/d_com_inf_game.h index d22d08cd10..2978eeef1a 100644 --- a/include/d/d_com_inf_game.h +++ b/include/d/d_com_inf_game.h @@ -4834,8 +4834,7 @@ inline void dComIfGd_drawXluListDark() { inline void dComIfGd_drawXluListInvisible() { ZoneScoped; #ifdef TARGET_PC - if (dusk::getSettings().game.enableWaterRefraction && - !dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::getSettings().game.enableWaterRefraction) { #endif g_dComIfG_gameInfo.drawlist.drawXluListInvisible(); #ifdef TARGET_PC @@ -4846,8 +4845,7 @@ inline void dComIfGd_drawXluListInvisible() { inline void dComIfGd_drawOpaListInvisible() { ZoneScoped; #ifdef TARGET_PC - if (dusk::getSettings().game.enableWaterRefraction && - !dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::getSettings().game.enableWaterRefraction) { #endif g_dComIfG_gameInfo.drawlist.drawOpaListInvisible(); #ifdef TARGET_PC diff --git a/include/dusk/frame_interpolation.h b/include/dusk/frame_interpolation.h index 9594529feb..e3f9cb3b45 100644 --- a/include/dusk/frame_interpolation.h +++ b/include/dusk/frame_interpolation.h @@ -22,6 +22,8 @@ float get_interpolation_step(); void request_presentation_sync(); bool presentation_sync_active(); +bool is_enabled(); + // TODO: These should be phased out as UI is progressively updated to use game_clock void set_ui_tick_pending(bool value); bool get_ui_tick_pending(); @@ -35,6 +37,11 @@ void record_final_mtx_raw_tagged(const Mtx* dest, const Mtx src, uint64_t stable bool lookup_replacement(const void* source, Mtx out); bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out); +typedef void (*InterpolationCallBack)(void* pUserWork); +void reset_interpolation_callbacks(); +// call on a sim tick, will get called during presentation +void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork); + void begin_presentation_camera(); void end_presentation_camera(); diff --git a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h index a27db4fa00..f08b2868dc 100644 --- a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h +++ b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h @@ -79,6 +79,10 @@ public: virtual void viewCalc(); virtual ~J3DModel() {} +#if TARGET_PC + static void interp_callback(void* pUserWork); +#endif + J3DModelData* getModelData() { return mModelData; } void onFlag(u32 flag) { mFlags |= flag; } diff --git a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModelData.h b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModelData.h index 6e3d54ff45..52e5018c1b 100644 --- a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModelData.h +++ b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModelData.h @@ -23,6 +23,10 @@ public: void syncJ3DSysPointers() const; void syncJ3DSysFlags() const; +#if TARGET_PC + bool needsInterpCallBack() const; +#endif + virtual ~J3DModelData() {} void simpleCalcMaterial(Mtx mtx) { simpleCalcMaterial(0, mtx); } diff --git a/libs/JSystem/include/JSystem/J3DGraphBase/J3DMaterial.h b/libs/JSystem/include/JSystem/J3DGraphBase/J3DMaterial.h index 829156ad1b..70e368eec4 100644 --- a/libs/JSystem/include/JSystem/J3DGraphBase/J3DMaterial.h +++ b/libs/JSystem/include/JSystem/J3DGraphBase/J3DMaterial.h @@ -33,6 +33,9 @@ public: void copy(J3DMaterial*); s32 newSharedDisplayList(u32); s32 newSingleSharedDisplayList(u32); +#if TARGET_PC + bool needsInterpCallBack() const; +#endif virtual void calc(f32 const (*)[4]); virtual void calcDiffTexMtx(f32 const (*)[4]); @@ -46,7 +49,6 @@ public: virtual void change(); J3DMaterial() { initialize(); } - ~J3DMaterial() {} J3DMaterial* getNext() { return mNext; } J3DShape* getShape() { return mShape; } J3DTevBlock* getTevBlock() { return mTevBlock; } diff --git a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp index f2bd737b4f..620e9deb00 100644 --- a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp +++ b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp @@ -97,6 +97,14 @@ s32 J3DModel::entryModelData(J3DModelData* pModelData, u32 mdlFlags, u32 mtxNum) return kJ3DError_Success; } +#if TARGET_PC +void J3DModel::interp_callback(void* pUserWork) { + J3DModel* i_this = static_cast(pUserWork); + i_this->calcMaterial(); + i_this->diff(); +} +#endif + s32 J3DModel::createShapePacket(J3DModelData* pModelData) { J3D_ASSERTMSG(173, pModelData != NULL, "Error : null pointer."); @@ -283,6 +291,11 @@ void J3DModel::calcMaterial() { material->calc(getAnmMtx(material->getJoint()->getJntNo())); } + +#if TARGET_PC + if (mModelData->needsInterpCallBack()) + dusk::frame_interp::add_interpolation_callback(&J3DModel::interp_callback, this); +#endif } void J3DModel::calcDiffTexMtx() { diff --git a/libs/JSystem/src/J3DGraphAnimator/J3DModelData.cpp b/libs/JSystem/src/J3DGraphAnimator/J3DModelData.cpp index e296676187..3eed051b6c 100644 --- a/libs/JSystem/src/J3DGraphAnimator/J3DModelData.cpp +++ b/libs/JSystem/src/J3DGraphAnimator/J3DModelData.cpp @@ -84,6 +84,15 @@ void J3DModelData::simpleCalcMaterial(u16 idx, Mtx param_1) { } } +#if TARGET_PC +bool J3DModelData::needsInterpCallBack() const { + for (u16 i = 0, n = getMaterialNum(); i < n; i++) + if (getMaterialNodePointer(i)->needsInterpCallBack()) + return true; + return false; +} +#endif + void J3DModelData::syncJ3DSysPointers() const { j3dSys.setTexture(getTexture()); j3dSys.setVtxPos(getVtxPosArray(), getVtxNum()); diff --git a/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp b/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp index 0c772305b2..6c109b3f8b 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp @@ -371,6 +371,28 @@ s32 J3DMaterial::newSingleSharedDisplayList(u32 dlSize) { return kJ3DError_Success; } +#if TARGET_PC +bool J3DMaterial::needsInterpCallBack() const { + for (int i = 0, n = getTexGenNum(); i < n; i++) { + J3DTexMtx* pTexMtx = mTexGenBlock->getTexMtx(i); + if (pTexMtx != NULL) { + u32 texMtxMode = pTexMtx->getTexMtxInfo().mInfo & 0x3f; + + // uses j3dSys.getViewMtx() + switch (texMtxMode) { + case J3DTexMtxMode_EnvmapBasic: + case J3DTexMtxMode_EnvmapOld: + case J3DTexMtxMode_Envmap: + case J3DTexMtxMode_ViewProjmap: + case J3DTexMtxMode_ViewProjmapBasic: + return true; + } + } + } + return false; +} +#endif + void J3DPatchedMaterial::initialize() { J3DMaterial::initialize(); } diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index 24454de99f..a79c2becdb 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -93,6 +93,23 @@ CameraSnapshot s_cam_curr{}; view_class s_presentation_view_backup{}; int s_presentation_depth = 0; +struct InterpolationCallBackWork { + dusk::frame_interp::InterpolationCallBack pCallBack; + void* pUserWork; +}; + +std::vector s_interpolationCallBackWork; + +void set_enabled(bool enabled) { + if (g_enabled == enabled) + return; + + g_enabled = enabled; + + if (!g_enabled) + s_interpolationCallBackWork.clear(); +} + void copy_view_to_snap(CameraSnapshot* dst, const view_class& v) { dst->eye = v.lookat.eye; dst->center = v.lookat.center; @@ -312,10 +329,14 @@ void clear_replacements() { namespace dusk::frame_interp { void ensure_initialized() { - g_enabled = getSettings().game.enableFrameInterpolation; + set_enabled(getSettings().game.enableFrameInterpolation); s_initialized = true; } +bool is_enabled() { + return g_enabled; +} + void begin_record() { ensure_initialized(); @@ -486,6 +507,24 @@ void record_camera(::camera_process_class* cam, int camera_id) { #endif } +static void run_interpolation_callbacks() { + for (size_t i = 0; i < s_interpolationCallBackWork.size(); i++) { + auto const& work = s_interpolationCallBackWork[i]; + work.pCallBack(work.pUserWork); + } +} + +void reset_interpolation_callbacks() { + s_interpolationCallBackWork.clear(); +} + +void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork) { + if (!is_enabled() || s_presentation_depth > 0) + return; + + s_interpolationCallBackWork.emplace_back(pCallBack, pUserWork); +} + void begin_presentation_camera() { ensure_initialized(); if (!g_enabled) { @@ -589,12 +628,14 @@ void begin_presentation_camera() { } mDoLib_clipper::setup(view->fovy, view->aspect, view->near_, far_); - + #if WIDESCREEN_SUPPORT mDoGph_gInf_c::offWideZoom(); #endif s_presentation_depth = 1; + + run_interpolation_callbacks(); } void end_presentation_camera() { diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index fe94891b28..0249269f32 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -235,6 +235,7 @@ void main01(void) { if (pacing.is_interpolating) { if (pacing.do_sim_tick) { + dusk::frame_interp::reset_interpolation_callbacks(); dusk::frame_interp::set_ui_tick_pending(true); mDoCPd_c::read(); dusk::gyro::read(pacing.sim_pace); From c239a5a226c65a89e2e2ba9fa0f2eeacc87e7950 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sat, 18 Apr 2026 22:45:54 -0700 Subject: [PATCH 26/68] freelook fix --- src/dusk/imgui/ImGuiCameraOverlay.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/dusk/imgui/ImGuiCameraOverlay.cpp b/src/dusk/imgui/ImGuiCameraOverlay.cpp index daaa2d6180..aa3d1ac093 100644 --- a/src/dusk/imgui/ImGuiCameraOverlay.cpp +++ b/src/dusk/imgui/ImGuiCameraOverlay.cpp @@ -49,9 +49,10 @@ namespace dusk { ImGui::SeparatorText("Free-look Data"); static float eyeYawDeg = 0.0f; - static float moveSpeed = 10000.0f; + static float moveSpeed = 5000.0f; static float rotSpeed = 5.0f; static cXyz freeLookPos = cXyz::Zero; + static bool freeLookActive = false; bool changed = false; @@ -91,7 +92,17 @@ namespace dusk { changed = true; } - if (changed) { + if (!freeLookActive && changed) { + freeLookPos += dCam->Center(); + freeLookActive = true; + } + + if (ImGui::IsKeyDown(ImGuiKey_R)) { + freeLookPos = cXyz::Zero; + freeLookActive = false; + } + + if (freeLookActive) { dCam->Reset(freeLookPos, freeLookPos + (frontDir * 100.0f)); } From 8d3cb51157935651027f0dadaf56e7a7e925fd6b Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sat, 18 Apr 2026 23:26:28 -0700 Subject: [PATCH 27/68] frame interp simplify --- include/dusk/frame_interpolation.h | 9 +- .../JSystem/J3DGraphAnimator/J3DModel.h | 4 +- .../JSystem/src/J3DGraphAnimator/J3DModel.cpp | 10 +- src/d/actor/d_a_midna.cpp | 2 +- src/d/actor/d_flower.inc | 2 +- src/d/actor/d_grass.inc | 2 +- src/d/d_drawlist.cpp | 27 +- src/dusk/frame_interpolation.cpp | 291 ++---------------- src/f_pc/f_pc_draw.cpp | 6 - 9 files changed, 40 insertions(+), 313 deletions(-) diff --git a/include/dusk/frame_interpolation.h b/include/dusk/frame_interpolation.h index e3f9cb3b45..b05bbe786c 100644 --- a/include/dusk/frame_interpolation.h +++ b/include/dusk/frame_interpolation.h @@ -28,13 +28,11 @@ bool is_enabled(); void set_ui_tick_pending(bool value); bool get_ui_tick_pending(); -void open_child(const void* key, int32_t id); -void close_child(); void record_camera(::camera_process_class* cam, int camera_id); -void record_final_mtx_raw(const Mtx* dest, const Mtx src); -void record_final_mtx_raw_tagged(const Mtx* dest, const Mtx src, uint64_t stable_tag); +void record_final_mtx(Mtx m, const void *key); +void record_final_mtx(Mtx m); -bool lookup_replacement(const void* source, Mtx out); +bool lookup_replacement(const void* key, Mtx out); bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out); typedef void (*InterpolationCallBack)(void* pUserWork); @@ -54,7 +52,6 @@ struct PresentationCameraScope { PresentationCameraScope& operator=(PresentationCameraScope&&) = delete; }; -uint64_t alloc_simple_shadow_pair_base(); } // namespace frame_interp } // namespace dusk #endif diff --git a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h index f08b2868dc..edf8c543b2 100644 --- a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h +++ b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h @@ -109,9 +109,7 @@ public: void setAnmMtx(int jointNo, Mtx m) { mMtxBuffer->setAnmMtx(jointNo, m); #ifdef TARGET_PC - dusk::frame_interp::record_final_mtx_raw( - reinterpret_cast(mMtxBuffer->getAnmMtx(jointNo)), - mMtxBuffer->getAnmMtx(jointNo)); + dusk::frame_interp::record_final_mtx(mMtxBuffer->getAnmMtx(jointNo)); #endif } MtxP getAnmMtx(int jointNo) { return mMtxBuffer->getAnmMtx(jointNo); } diff --git a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp index 620e9deb00..b666caa457 100644 --- a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp +++ b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp @@ -465,11 +465,11 @@ void J3DModel::calc() { #ifdef TARGET_PC for (u16 i = 0; i < mModelData->getJointNum(); ++i) { - dusk::frame_interp::record_final_mtx_raw(reinterpret_cast(getAnmMtx(i)), getAnmMtx(i)); + dusk::frame_interp::record_final_mtx(getAnmMtx(i)); } for (u16 i = 0; i < mModelData->getWEvlpMtxNum(); ++i) { - dusk::frame_interp::record_final_mtx_raw(reinterpret_cast(getWeightAnmMtx(i)), getWeightAnmMtx(i)); + dusk::frame_interp::record_final_mtx(getWeightAnmMtx(i)); } #endif } @@ -509,7 +509,7 @@ void J3DModel::viewCalc() { J3DCalcViewBaseMtx(j3dSys.getViewMtx(), mBaseScale, mBaseTransformMtx, (MtxP)&mInternalView); #ifdef TARGET_PC - dusk::frame_interp::record_final_mtx_raw(&mInternalView, mInternalView); + dusk::frame_interp::record_final_mtx(mInternalView); #endif } } else if (isCpuSkinningOn()) { @@ -517,7 +517,7 @@ void J3DModel::viewCalc() { J3DCalcViewBaseMtx(j3dSys.getViewMtx(), mBaseScale, mBaseTransformMtx, (MtxP)&mInternalView); #ifdef TARGET_PC - dusk::frame_interp::record_final_mtx_raw(&mInternalView, mInternalView); + dusk::frame_interp::record_final_mtx(mInternalView); #endif } } else if (checkFlag(J3DMdlFlag_SkinPosCpu)) { @@ -541,7 +541,7 @@ void J3DModel::viewCalc() { #ifdef TARGET_PC for (u16 i = 0; i < mModelData->getDrawMtxNum(); ++i) { - dusk::frame_interp::record_final_mtx_raw(&getDrawMtxPtr()[i], getDrawMtxPtr()[i]); + dusk::frame_interp::record_final_mtx(getDrawMtxPtr()[i]); } #endif diff --git a/src/d/actor/d_a_midna.cpp b/src/d/actor/d_a_midna.cpp index 6df9d2cc34..7bb844932e 100644 --- a/src/d/actor/d_a_midna.cpp +++ b/src/d/actor/d_a_midna.cpp @@ -1056,7 +1056,7 @@ void daMidna_c::setBodyPartMatrix() { #ifdef TARGET_PC // FRAME INTERP NOTE: Record weight envelopes for Midna here, as they are otherwise missed causing distortion for (u16 i = 0; i < mpModel->getModelData()->getWEvlpMtxNum(); i++) { - dusk::frame_interp::record_final_mtx_raw(reinterpret_cast(mpModel->getWeightAnmMtx(i)), mpModel->getWeightAnmMtx(i)); + dusk::frame_interp::record_final_mtx(mpModel->getWeightAnmMtx(i)); } #endif } diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index 034d35961e..4f28d8fac3 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -994,7 +994,7 @@ void dFlower_packet_c::update() { mDoMtx_stack_c::scaleM(temp_f31, temp_f31, temp_f31); cMtx_concat(j3dSys.getViewMtx(), temp_r28, data_p->m_modelMtx); #ifdef TARGET_PC - dusk::frame_interp::record_final_mtx_raw(reinterpret_cast(&data_p->m_modelMtx), data_p->m_modelMtx); + dusk::frame_interp::record_final_mtx(data_p->m_modelMtx); #endif } } diff --git a/src/d/actor/d_grass.inc b/src/d/actor/d_grass.inc index 0f651a3203..91537fc1c2 100644 --- a/src/d/actor/d_grass.inc +++ b/src/d/actor/d_grass.inc @@ -1018,7 +1018,7 @@ void dGrass_packet_c::update() { cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), data_p->m_modelMtx); } #ifdef TARGET_PC - dusk::frame_interp::record_final_mtx_raw(reinterpret_cast(&data_p->m_modelMtx), data_p->m_modelMtx); + dusk::frame_interp::record_final_mtx(data_p->m_modelMtx); #endif } } diff --git a/src/d/d_drawlist.cpp b/src/d/d_drawlist.cpp index 2ab23b0bc9..b9e9699fa8 100644 --- a/src/d/d_drawlist.cpp +++ b/src/d/d_drawlist.cpp @@ -1096,16 +1096,7 @@ void dDlst_shadowReal_c::draw() { GXSetVtxDesc(GX_VA_POS, GX_DIRECT); GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); GXSetCurrentMtx(GX_PNMTX0); -#ifdef TARGET_PC - Mtx receiver_proj_mtx; - if (dusk::frame_interp::lookup_replacement(&mReceiverProjMtx, receiver_proj_mtx)) { - GXLoadTexMtxImm(receiver_proj_mtx, GX_TEXMTX0, GX_MTX3x4); - } else { -#endif - GXLoadTexMtxImm(mReceiverProjMtx, GX_TEXMTX0, GX_MTX3x4); -#ifdef TARGET_PC - } -#endif + GXLoadTexMtxImm(mReceiverProjMtx, GX_TEXMTX0, GX_MTX3x4); mShadowRealPoly.draw(); } @@ -1263,14 +1254,9 @@ u8 dDlst_shadowReal_c::setShadowRealMtx(cXyz* param_0, cXyz* param_1, f32 param_ C_MTXOrtho(mRenderProjMtx, param_2, -param_2, -param_2, param_2, 1.0f, 10000.0f); C_MTXLightOrtho(mReceiverProjMtx, param_2, -param_2, -param_2, param_2, 0.5f, -0.5f, 0.5f, 0.5f); cMtx_concat(mReceiverProjMtx, mViewMtx, mReceiverProjMtx); -#ifdef TARGET_PC - dusk::frame_interp::record_final_mtx_raw(&mViewMtx, mViewMtx); - dusk::frame_interp::record_final_mtx_raw(&mReceiverProjMtx, mReceiverProjMtx); -#endif return r29; } - u32 dDlst_shadowReal_c::set(u32 i_key, J3DModel* i_model, cXyz* param_2, f32 param_3, f32 param_4, dKy_tevstr_c* param_5, f32 i_cameraZ, f32 param_7) { dScnKy_env_light_c* env_light = dKy_getEnvlight(); @@ -1292,6 +1278,7 @@ u32 dDlst_shadowReal_c::set(u32 i_key, J3DModel* i_model, cXyz* param_2, f32 par } field_0x1 = setShadowRealMtx(&sp60, param_2, param_3, param_4, param_7, param_5); + if (!field_0x1) { return 0; } @@ -1433,14 +1420,8 @@ void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* pa mDoMtx_stack_c::scaleM(param_2, 1.0f, param_2 * param_5); cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), mMtx); #ifdef TARGET_PC - const uint64_t shadow_tag_base = dusk::frame_interp::alloc_simple_shadow_pair_base(); - if (shadow_tag_base != 0) { - dusk::frame_interp::record_final_mtx_raw_tagged(&mVolumeMtx, mVolumeMtx, shadow_tag_base); - dusk::frame_interp::record_final_mtx_raw_tagged(&mMtx, mMtx, shadow_tag_base + 1u); - } else { - dusk::frame_interp::record_final_mtx_raw(&mVolumeMtx, mVolumeMtx); - dusk::frame_interp::record_final_mtx_raw(&mMtx, mMtx); - } + dusk::frame_interp::record_final_mtx(mVolumeMtx); + dusk::frame_interp::record_final_mtx(mMtx); #endif mpTexObj = param_6; } diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index a79c2becdb..1826bbf171 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -1,63 +1,16 @@ #include "dusk/frame_interpolation.h" #include +#include "mtx.h" #include "f_op/f_op_camera_mng.h" #include "m_Do/m_Do_graphic.h" namespace { -enum class Op : uint8_t { - OpenChild, - FinalMtx, -}; - -struct Label { - const void* key = nullptr; - int32_t id = 0; - - bool operator==(const Label& other) const { - return key == other.key && id == other.id; - } -}; - -struct Data { - Label child_label{}; - size_t child_index = 0; - Mtx matrix{}; - const Mtx* dest = nullptr; - uint64_t stable_tag = 0; -}; - -struct Path; - -struct ChildBucket { - Label label{}; - std::vector> nodes; -}; - -struct OpBucket { - Op op = Op::OpenChild; - std::vector values; -}; - -struct Path { - std::vector children; - std::vector ops; - std::vector> items; - Label draw_scope{}; - uint32_t simple_shadow_pair_seq = 0; -}; struct Recording { - Path root; + std::unordered_map matrix_values; }; -struct MatrixValue { - Mtx value; -}; - -using FinalMtxLookup = std::unordered_map; -using FinalMtxLookupTagged = std::unordered_map; - bool s_initialized = false; bool g_enabled = false; @@ -70,9 +23,8 @@ bool g_ui_tick_pending = false; Recording g_current_recording; Recording g_previous_recording; -std::vector g_current_path; -std::unordered_map g_replacements; +std::unordered_map g_replacements; struct CameraSnapshot { cXyz eye{}; @@ -122,14 +74,6 @@ void copy_view_to_snap(CameraSnapshot* dst, const view_class& v) { dst->valid = true; } -inline void copy_matrix(const Mtx src, Mtx dst) { - MTXCopy(src, dst); -} - -inline void concat_matrix(const Mtx lhs, const Mtx rhs, Mtx out) { - MTXConcat(lhs, rhs, out); -} - inline void lerp_matrix(Mtx out, const Mtx lhs, const Mtx rhs, float step) { const float old_weight = 1.0f - step; for (size_t row = 0; row < 3; ++row) { @@ -163,162 +107,22 @@ inline bool matrix_differs(const Mtx lhs, const Mtx rhs, float epsilon = 0.0001f return false; } -Data& append_op(Op op) { - auto& items = g_current_path.back()->items; - auto& buckets = g_current_path.back()->ops; - auto it = std::find_if(buckets.begin(), buckets.end(), - [op](const OpBucket& bucket) { return bucket.op == op; }); - if (it == buckets.end()) { - buckets.push_back({op, {}}); - it = buckets.end() - 1; - } - items.emplace_back(op, it->values.size()); - return it->values.emplace_back(); -} - -const Data* find_matching_data(const Path& path, Op op, size_t index) { - auto it = std::find_if(path.ops.begin(), path.ops.end(), - [op](const OpBucket& bucket) { return bucket.op == op; }); - if (it == path.ops.end() || index >= it->values.size()) { - return nullptr; - } - return &it->values[index]; -} - -const OpBucket* find_op_bucket(const Path& path, Op op) { - auto it = std::find_if(path.ops.begin(), path.ops.end(), - [op](const OpBucket& bucket) { return bucket.op == op; }); - if (it == path.ops.end()) { - return nullptr; - } - return &*it; -} - -void build_final_mtx_lookups(const Path& path, FinalMtxLookup& dest_lookup, FinalMtxLookupTagged& tag_lookup) { - dest_lookup.clear(); - tag_lookup.clear(); - - const OpBucket* bucket = find_op_bucket(path, Op::FinalMtx); - if (bucket == nullptr) { - return; - } - - for (const Data& data : bucket->values) { - if (data.dest != nullptr) { - dest_lookup[data.dest] = &data; - } - if (data.stable_tag != 0) { - tag_lookup[data.stable_tag] = &data; - } - } -} - -const Data* find_matching_final_mtx(const FinalMtxLookup& lookup, const Data& new_data) { - if (new_data.dest == nullptr) { - return nullptr; - } - - auto it = lookup.find(new_data.dest); - if (it == lookup.end()) { - return nullptr; - } - return it->second; -} - -ChildBucket& get_child_bucket(Path& path, const Label& label) { - auto it = std::find_if(path.children.begin(), path.children.end(), - [&label](const ChildBucket& bucket) { return bucket.label == label; }); - if (it == path.children.end()) { - path.children.push_back({}); - it = path.children.end() - 1; - it->label = label; - } - return *it; -} - -const ChildBucket* find_child_bucket(const Path& path, const Label& label) { - auto it = std::find_if(path.children.begin(), path.children.end(), - [&label](const ChildBucket& bucket) { return bucket.label == label; }); - if (it == path.children.end()) { - return nullptr; - } - return &*it; -} - -void store_replacement(const Data& old_data, const Data& new_data, float step) { - if (new_data.dest == nullptr) { - return; - } - - auto& replacement = g_replacements[new_data.dest]; - lerp_matrix(replacement.value, old_data.matrix, new_data.matrix, step); -} - -void interpolate_branch(const Path& old_path, const Path& new_path, float step) { - FinalMtxLookup old_final_mtx_lookup; - FinalMtxLookupTagged old_final_mtx_lookup_tagged; - build_final_mtx_lookups(old_path, old_final_mtx_lookup, old_final_mtx_lookup_tagged); - - for (const auto& item : new_path.items) { - const Op op = item.first; - const size_t index = item.second; - const Data* new_data = find_matching_data(new_path, op, index); - if (new_data == nullptr) { - continue; - } - - if (op == Op::OpenChild) { - const ChildBucket* new_children = find_child_bucket(new_path, new_data->child_label); - if (new_children == nullptr || new_data->child_index >= new_children->nodes.size()) - { - continue; - } - - const Path& new_child = *new_children->nodes[new_data->child_index]; - const ChildBucket* old_children = find_child_bucket(old_path, new_data->child_label); - if (old_children != nullptr && new_data->child_index < old_children->nodes.size()) - { - interpolate_branch(*old_children->nodes[new_data->child_index], new_child, step); - } else { - interpolate_branch(new_child, new_child, step); - } - continue; - } - - const Data* indexed_old_data = find_matching_data(old_path, op, index); - const Data* old_data = nullptr; - if (op == Op::FinalMtx) { - if (new_data->stable_tag != 0) { - const auto it = old_final_mtx_lookup_tagged.find(new_data->stable_tag); - old_data = it != old_final_mtx_lookup_tagged.end() ? it->second : nullptr; - } else { - old_data = find_matching_final_mtx(old_final_mtx_lookup, *new_data); - } - } else { - old_data = indexed_old_data; - } - if (op == Op::FinalMtx) { - store_replacement(old_data != nullptr ? *old_data : *new_data, *new_data, step); - } - } -} - const Mtx* resolve_replacement(const Mtx* source, Mtx* scratch) { if (!g_interpolating || source == nullptr || dusk::frame_interp::presentation_sync_active()) { return source; } - auto it = g_replacements.find(source); + auto it = g_replacements.find(reinterpret_cast(source)); if (it == g_replacements.end()) { return source; } - copy_matrix(it->second.value, *scratch); + MTXCopy(it->second, *scratch); return scratch; } bool has_recording_data(const Recording& recording) { - return !recording.root.items.empty() || !recording.root.children.empty(); + return !recording.matrix_values.empty(); } void clear_replacements() { @@ -345,7 +149,6 @@ void begin_record() { g_sync_presentation = false; g_previous_recording = {}; g_current_recording = {}; - g_current_path.clear(); clear_replacements(); s_cam_prev.valid = false; s_cam_curr.valid = false; @@ -355,8 +158,6 @@ void begin_record() { g_sync_presentation = false; g_previous_recording = std::move(g_current_recording); g_current_recording = {}; - g_current_path.clear(); - g_current_path.push_back(&g_current_recording.root); g_recording = true; g_interpolating = false; clear_replacements(); @@ -386,8 +187,13 @@ void interpolate(float step) { if (!g_interpolating) { return; } - const Path& old_root = has_recording_data(g_previous_recording) ? g_previous_recording.root : g_current_recording.root; - interpolate_branch(old_root, g_current_recording.root, g_step); + for (auto const& old : g_previous_recording.matrix_values) { + if (auto it = g_current_recording.matrix_values.find(old.first); + it != g_current_recording.matrix_values.end()) + { + lerp_matrix(g_replacements[old.first], old.second, it->second, g_step); + } + } } void request_presentation_sync() { @@ -420,63 +226,30 @@ bool get_ui_tick_pending() { return g_enabled ? g_ui_tick_pending : true; } -void open_child(const void* key, int32_t id) { - if (!s_initialized || !g_recording) { +void record_final_mtx(Mtx m, const void* key) { + if (!s_initialized || !g_recording || m == nullptr) { return; } - Label label{key, id}; - auto& siblings = get_child_bucket(*g_current_path.back(), label).nodes; - Data& data = append_op(Op::OpenChild); - data.child_label = label; - data.child_index = siblings.size(); - siblings.emplace_back(std::make_unique()); - Path* const child = siblings.back().get(); - child->draw_scope = label; - g_current_path.push_back(child); + auto& it = g_current_recording.matrix_values[reinterpret_cast(key)]; + MTXCopy(m, it); } -void close_child() { - if (!s_initialized || !g_recording || g_current_path.size() <= 1) { - return; - } - - g_current_path.pop_back(); +void record_final_mtx(Mtx m) { + record_final_mtx(m, m); } -void record_final_mtx_raw(const Mtx* dest, const Mtx src) { - if (!s_initialized || !g_recording || dest == nullptr) { - return; - } - - Data& data = append_op(Op::FinalMtx); - data.dest = dest; - data.stable_tag = 0; - copy_matrix(src, data.matrix); -} - -void record_final_mtx_raw_tagged(const Mtx* dest, const Mtx src, uint64_t stable_tag) { - if (!s_initialized || !g_recording || dest == nullptr) { - return; - } - - Data& data = append_op(Op::FinalMtx); - data.dest = dest; - data.stable_tag = stable_tag; - copy_matrix(src, data.matrix); -} - -bool lookup_replacement(const void* source, Mtx out) { - if (presentation_sync_active() || !g_interpolating || source == nullptr) { +bool lookup_replacement(const void* key, Mtx out) { + if (presentation_sync_active() || !g_interpolating || key == nullptr) { return false; } - auto it = g_replacements.find(reinterpret_cast(source)); + auto it = g_replacements.find(reinterpret_cast(key)); if (it == g_replacements.end()) { return false; } - copy_matrix(it->second.value, out); + MTXCopy(it->second, out); return true; } @@ -493,7 +266,7 @@ bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out) { return false; } - concat_matrix(*resolved_lhs, *resolved_rhs, out); + MTXConcat(*resolved_lhs, *resolved_rhs, out); return true; } @@ -652,20 +425,4 @@ void end_presentation_camera() { std::memcpy(view, &s_presentation_view_backup, sizeof(view_class)); } } - -uint64_t alloc_simple_shadow_pair_base() { - if (!s_initialized || !g_recording || g_current_path.size() <= 1) { - return 0; - } - - Path* const scope = g_current_path.back(); - const uint64_t h = static_cast(reinterpret_cast(scope->draw_scope.key)); - const uint32_t lo = scope->simple_shadow_pair_seq; - scope->simple_shadow_pair_seq += 2; - uint64_t tag0 = (h << 17) ^ (static_cast(lo) << 1u); - if (tag0 == 0) { - tag0 = 2; - } - return tag0; -} } // namespace dusk::frame_interp diff --git a/src/f_pc/f_pc_draw.cpp b/src/f_pc/f_pc_draw.cpp index 697d19cf29..8b775257fc 100644 --- a/src/f_pc/f_pc_draw.cpp +++ b/src/f_pc/f_pc_draw.cpp @@ -26,13 +26,7 @@ int fpcDw_Execute(base_process_class* i_proc) { } fpcLy_SetCurrentLayer(i_proc->layer_tag.layer); -#ifdef TARGET_PC - dusk::frame_interp::open_child(i_proc, 0); -#endif ret = draw_func(i_proc); -#ifdef TARGET_PC - dusk::frame_interp::close_child(); -#endif fpcLy_SetCurrentLayer(save_layer); return ret; } From 341e97ba82897bbc1e2f7aee4f8c8fcb1956c4ca Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 01:14:58 -0700 Subject: [PATCH 28/68] simple shadow interp fix --- include/d/d_drawlist.h | 4 ++++ src/d/d_drawlist.cpp | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/include/d/d_drawlist.h b/include/d/d_drawlist.h index 4126c715f3..8368c9e92e 100644 --- a/include/d/d_drawlist.h +++ b/include/d/d_drawlist.h @@ -209,6 +209,10 @@ public: /* 0x04 */ TGXTexObj* mpTexObj; /* 0x08 */ Mtx mVolumeMtx; /* 0x38 */ Mtx mMtx; +#if TARGET_PC + const void* mVolumeMtxKey; + const void* mMtxKey; +#endif }; // Size: 0x68 struct cBgD_Vtx_t; diff --git a/src/d/d_drawlist.cpp b/src/d/d_drawlist.cpp index b9e9699fa8..d4b477b1f4 100644 --- a/src/d/d_drawlist.cpp +++ b/src/d/d_drawlist.cpp @@ -1318,7 +1318,7 @@ void dDlst_shadowSimple_c::draw() { GXSetVtxDesc(GX_VA_POS, GX_INDEX8); #ifdef TARGET_PC Mtx volume_mtx; - if (dusk::frame_interp::lookup_replacement(&mVolumeMtx, volume_mtx)) { + if (dusk::frame_interp::lookup_replacement(mVolumeMtxKey, volume_mtx)) { GXLoadPosMtxImm(volume_mtx, GX_PNMTX0); } else { #endif @@ -1333,7 +1333,7 @@ void dDlst_shadowSimple_c::draw() { GXCallDisplayList(l_shadowVolumeDL, 0x40); #ifdef TARGET_PC Mtx shadow_mtx; - if (dusk::frame_interp::lookup_replacement(&mMtx, shadow_mtx)) { + if (dusk::frame_interp::lookup_replacement(mMtxKey, shadow_mtx)) { GXLoadPosMtxImm(shadow_mtx, GX_PNMTX1); } else { #endif @@ -1370,6 +1370,12 @@ void dDlst_shadowSimple_c::draw() { GXCallDisplayList(l_shadowVolumeDL, 0x40); } +#if TARGET_PC +static const void* getInterpKey(const void* base, int idx) { + return reinterpret_cast(reinterpret_cast(base) ^ idx); +} +#endif + void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* param_3, s16 param_4, f32 param_5, TGXTexObj* param_6) { if (param_5 < 0.0f) { @@ -1420,8 +1426,10 @@ void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* pa mDoMtx_stack_c::scaleM(param_2, 1.0f, param_2 * param_5); cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), mMtx); #ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(mVolumeMtx); - dusk::frame_interp::record_final_mtx(mMtx); + mVolumeMtxKey = getInterpKey(param_0, 0x1); + mMtxKey = getInterpKey(param_0, 0x2); + dusk::frame_interp::record_final_mtx(mVolumeMtx, mVolumeMtxKey); + dusk::frame_interp::record_final_mtx(mMtx, mMtxKey); #endif mpTexObj = param_6; } From bb9a88d7dcae02623c6d07bd182abb210189ade2 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 02:25:00 -0700 Subject: [PATCH 29/68] frame interp camera cleanups --- include/dusk/frame_interpolation.h | 13 ++- .../JSystem/J3DGraphAnimator/J3DModel.h | 2 +- .../JSystem/src/J3DGraphAnimator/J3DModel.cpp | 8 +- src/d/d_camera.cpp | 12 ++- src/d/d_kankyo.cpp | 13 +++ src/dusk/frame_interpolation.cpp | 89 ++++++++++--------- src/m_Do/m_Do_main.cpp | 4 +- 7 files changed, 84 insertions(+), 57 deletions(-) diff --git a/include/dusk/frame_interpolation.h b/include/dusk/frame_interpolation.h index b05bbe786c..3ba1af4799 100644 --- a/include/dusk/frame_interpolation.h +++ b/include/dusk/frame_interpolation.h @@ -1,5 +1,4 @@ -#ifndef DUSK_FRAME_INTERP_H -#define DUSK_FRAME_INTERP_H +#pragma once #include #include @@ -7,6 +6,7 @@ #include class camera_process_class; +class view_class; #ifdef __cplusplus namespace dusk { @@ -16,7 +16,8 @@ void ensure_initialized(); void begin_record(); void end_record(); -void interpolate(float step); +void begin_frame(bool is_sim_frame, float step); +void interpolate(); float get_interpolation_step(); void request_presentation_sync(); @@ -29,14 +30,14 @@ void set_ui_tick_pending(bool value); bool get_ui_tick_pending(); void record_camera(::camera_process_class* cam, int camera_id); +void interp_view(::view_class* view); void record_final_mtx(Mtx m, const void *key); void record_final_mtx(Mtx m); bool lookup_replacement(const void* key, Mtx out); bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out); -typedef void (*InterpolationCallBack)(void* pUserWork); -void reset_interpolation_callbacks(); +typedef void (*InterpolationCallBack)(bool isSimFrame, void* pUserWork); // call on a sim tick, will get called during presentation void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork); @@ -55,5 +56,3 @@ struct PresentationCameraScope { } // namespace frame_interp } // namespace dusk #endif - -#endif diff --git a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h index edf8c543b2..2e5b3bd38d 100644 --- a/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h +++ b/libs/JSystem/include/JSystem/J3DGraphAnimator/J3DModel.h @@ -80,7 +80,7 @@ public: virtual ~J3DModel() {} #if TARGET_PC - static void interp_callback(void* pUserWork); + static void interp_callback(bool isSimFrame, void* pUserWork); #endif J3DModelData* getModelData() { return mModelData; } diff --git a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp index b666caa457..139bba5dd6 100644 --- a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp +++ b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp @@ -98,10 +98,12 @@ s32 J3DModel::entryModelData(J3DModelData* pModelData, u32 mdlFlags, u32 mtxNum) } #if TARGET_PC -void J3DModel::interp_callback(void* pUserWork) { +void J3DModel::interp_callback(bool isSimFrame, void* pUserWork) { J3DModel* i_this = static_cast(pUserWork); - i_this->calcMaterial(); - i_this->diff(); + if (!isSimFrame) { + i_this->calcMaterial(); + i_this->diff(); + } } #endif diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index b93d569003..3e82c339ed 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -11009,6 +11009,15 @@ static int camera_execute(camera_process_class* i_this) { i_this->mCamera.CalcTrimSize(); store(i_this); + +#ifdef TARGET_PC + // record new camera for our sim frame + dusk::frame_interp::record_camera(i_this, get_camera_id(i_this)); + // interpolate the view now so that this sim frame's view matrix matches what + // we'll be rendering with later + dusk::frame_interp::interp_view(&i_this->view); +#endif + view_setup(i_this); return 1; } @@ -11077,9 +11086,6 @@ static int camera_draw(camera_process_class* i_this) { C_MTXPerspective(process->view.projMtx, process->view.fovy, process->view.aspect, process->view.near_, process->view.far_); mDoMtx_lookAt(process->view.viewMtx, &process->view.lookat.eye, &process->view.lookat.center, &process->view.lookat.up, process->view.bank); -#ifdef TARGET_PC - dusk::frame_interp::record_camera(process, camera_id); -#endif #if WIDESCREEN_SUPPORT mDoGph_gInf_c::setWideZoomProjection(process->view.projMtx); diff --git a/src/d/d_kankyo.cpp b/src/d/d_kankyo.cpp index c6739ee925..05a0f70f5a 100644 --- a/src/d/d_kankyo.cpp +++ b/src/d/d_kankyo.cpp @@ -35,6 +35,7 @@ #if TARGET_PC #include "dusk/imgui/ImGuiBloomWindow.hpp" #include "dusk/settings.h" +#include "dusk/frame_interpolation.h" #endif static void GxXFog_set(); @@ -8105,11 +8106,23 @@ void dKankyo_HIO_c::genMessage(JORMContext* mctx) { #endif +#if TARGET_PC +static void interp_callback(bool isSimFrame, void* pUserWork) { + if (!isSimFrame) { + g_env_light.drawKankyo(); + } +} +#endif + void dScnKy_env_light_c::drawKankyo() { setSunpos(); SetBaseLight(); setLight(); dKy_setLight_nowroom(g_env_light.PrevCol); + +#if TARGET_PC + dusk::frame_interp::add_interpolation_callback(interp_callback, nullptr); +#endif } void dKy_undwater_filter_draw() { diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index 1826bbf171..01604ede4f 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -19,6 +19,7 @@ bool g_interpolating = false; bool g_sync_presentation = false; float g_step = 0.0f; +bool g_is_sim_frame = false; bool g_ui_tick_pending = false; Recording g_current_recording; @@ -141,6 +142,14 @@ bool is_enabled() { return g_enabled; } +void begin_frame(bool is_sim_frame, float step) { + g_is_sim_frame = is_sim_frame; + g_step = std::clamp(step, 0.0f, 1.0f); + if (is_sim_frame) { + s_interpolationCallBackWork.clear(); + } +} + void begin_record() { ensure_initialized(); @@ -167,11 +176,6 @@ void begin_record() { s_cam_prev.valid = false; s_cam_curr.valid = false; return; - } else { - copy_view_to_snap(&s_cam_prev, cam->view); -#if WIDESCREEN_SUPPORT - s_cam_prev.wideZoom = s_cam_curr.valid ? s_cam_curr.wideZoom : false; -#endif } } @@ -179,10 +183,9 @@ void end_record() { g_recording = false; } -void interpolate(float step) { +void interpolate() { ensure_initialized(); clear_replacements(); - g_step = std::clamp(step, 0.0f, 1.0f); g_interpolating = g_enabled && !g_recording && !g_sync_presentation && has_recording_data(g_current_recording); if (!g_interpolating) { return; @@ -274,25 +277,56 @@ void record_camera(::camera_process_class* cam, int camera_id) { if (!g_enabled || camera_id != 0 || cam == nullptr) { return; } + s_cam_prev = std::move(s_cam_curr); copy_view_to_snap(&s_cam_curr, cam->view); #if WIDESCREEN_SUPPORT s_cam_curr.wideZoom = mDoGph_gInf_c::isWideZoom(); #endif } +void interp_view(::view_class* view) { + const f32 step = get_interpolation_step(); + cXyz eye; + cXyz center; + cXyz up; + lerp_xyz(&eye, s_cam_prev.eye, s_cam_curr.eye, step); + lerp_xyz(¢er, s_cam_prev.center, s_cam_curr.center, step); + lerp_xyz(&up, s_cam_prev.up, s_cam_curr.up, step); + if (!up.normalizeRS()) { + up = s_cam_curr.up; + up.normalizeRS(); + } + + view->lookat.eye = eye; + view->lookat.center = center; + view->lookat.up = up; + view->bank = lerp_bank(s_cam_prev.bank, s_cam_curr.bank, step); + view->fovy = s_cam_prev.fovy + (s_cam_curr.fovy - s_cam_prev.fovy) * step; + view->aspect = s_cam_prev.aspect + (s_cam_curr.aspect - s_cam_prev.aspect) * step; + view->near_ = s_cam_prev.near_ + (s_cam_curr.near_ - s_cam_prev.near_) * step; + view->far_ = s_cam_prev.far_ + (s_cam_curr.far_ - s_cam_prev.far_) * step; + + // FRAME INTERP TODO: It might be better if I rewired the game to not clear this flag until the + // next sim frame, but I don't care enough to right now +#if WIDESCREEN_SUPPORT + if (mDoGph_gInf_c::isWide() && !mDoGph_gInf_c::isWideZoom() && step >= 0.5f ? + s_cam_curr.wideZoom : + s_cam_prev.wideZoom) + { + mDoGph_gInf_c::onWideZoom(); + } +#endif +} + static void run_interpolation_callbacks() { for (size_t i = 0; i < s_interpolationCallBackWork.size(); i++) { auto const& work = s_interpolationCallBackWork[i]; - work.pCallBack(work.pUserWork); + work.pCallBack(g_is_sim_frame, work.pUserWork); } } -void reset_interpolation_callbacks() { - s_interpolationCallBackWork.clear(); -} - void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork) { - if (!is_enabled() || s_presentation_depth > 0) + if (!is_enabled() || s_presentation_depth > 0 || !g_is_sim_frame) return; s_interpolationCallBackWork.emplace_back(pCallBack, pUserWork); @@ -317,34 +351,7 @@ void begin_presentation_camera() { } std::memcpy(&s_presentation_view_backup, view, sizeof(view_class)); - - const f32 step = get_interpolation_step(); - cXyz eye; - cXyz center; - cXyz up; - lerp_xyz(&eye, s_cam_prev.eye, s_cam_curr.eye, step); - lerp_xyz(¢er, s_cam_prev.center, s_cam_curr.center, step); - lerp_xyz(&up, s_cam_prev.up, s_cam_curr.up, step); - if (!up.normalizeRS()) { - up = s_cam_curr.up; - up.normalizeRS(); - } - - view->lookat.eye = eye; - view->lookat.center = center; - view->lookat.up = up; - view->bank = lerp_bank(s_cam_prev.bank, s_cam_curr.bank, step); - view->fovy = s_cam_prev.fovy + (s_cam_curr.fovy - s_cam_prev.fovy) * step; - view->aspect = s_cam_prev.aspect + (s_cam_curr.aspect - s_cam_prev.aspect) * step; - view->near_ = s_cam_prev.near_ + (s_cam_curr.near_ - s_cam_prev.near_) * step; - view->far_ = s_cam_prev.far_ + (s_cam_curr.far_ - s_cam_prev.far_) * step; - - // FRAME INTERP TODO: It might be better if I rewired the game to not clear this flag until the next sim frame, but I don't care enough to right now -#if WIDESCREEN_SUPPORT - if (mDoGph_gInf_c::isWide() && !mDoGph_gInf_c::isWideZoom() && step >= 0.5f ? s_cam_curr.wideZoom : s_cam_prev.wideZoom) { - mDoGph_gInf_c::onWideZoom(); - } -#endif + interp_view(view); // FRAME INTERP TODO: Largely copied from d_camera's camera_draw function from this point, got any better ideas? C_MTXPerspective(view->projMtx, view->fovy, view->aspect, view->near_, view->far_); diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 0249269f32..7ef445fbe6 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -234,8 +234,8 @@ void main01(void) { mDoGph_gInf_c::updateRenderSize(); if (pacing.is_interpolating) { + dusk::frame_interp::begin_frame(pacing.do_sim_tick, pacing.interpolation_step); if (pacing.do_sim_tick) { - dusk::frame_interp::reset_interpolation_callbacks(); dusk::frame_interp::set_ui_tick_pending(true); mDoCPd_c::read(); dusk::gyro::read(pacing.sim_pace); @@ -243,7 +243,7 @@ void main01(void) { mDoAud_Execute(); dusk::game_clock::reset_accumulator(); } - dusk::frame_interp::interpolate(pacing.interpolation_step); + dusk::frame_interp::interpolate(); { dusk::frame_interp::PresentationCameraScope presentation_camera; cAPIGph_Painter(); From aa377cd5c1a837740d62cbff30c68db1ef6680a7 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 02:43:25 -0700 Subject: [PATCH 30/68] add alternate to interp callbacks, flags on the leafdraw --- include/dusk/frame_interpolation.h | 4 +++- include/f_pc/f_pc_leaf.h | 2 +- src/d/d_kankyo.cpp | 16 ++++---------- src/dusk/frame_interpolation.cpp | 34 +++++++++++++++--------------- src/f_pc/f_pc_leaf.cpp | 12 +++++++++++ src/m_Do/m_Do_main.cpp | 6 +++++- 6 files changed, 42 insertions(+), 32 deletions(-) diff --git a/include/dusk/frame_interpolation.h b/include/dusk/frame_interpolation.h index 3ba1af4799..7894eda9c2 100644 --- a/include/dusk/frame_interpolation.h +++ b/include/dusk/frame_interpolation.h @@ -16,7 +16,7 @@ void ensure_initialized(); void begin_record(); void end_record(); -void begin_frame(bool is_sim_frame, float step); +void begin_frame(bool enabled, bool is_sim_frame, float step); void interpolate(); float get_interpolation_step(); @@ -29,6 +29,8 @@ bool is_enabled(); void set_ui_tick_pending(bool value); bool get_ui_tick_pending(); +bool is_sim_frame(); + void record_camera(::camera_process_class* cam, int camera_id); void interp_view(::view_class* view); void record_final_mtx(Mtx m, const void *key); diff --git a/include/f_pc/f_pc_leaf.h b/include/f_pc/f_pc_leaf.h index 33d7e85d29..414d706b6a 100644 --- a/include/f_pc/f_pc_leaf.h +++ b/include/f_pc/f_pc_leaf.h @@ -25,7 +25,7 @@ typedef struct leafdraw_class : base_process_class { #endif /* 0xB8 */ leafdraw_method_class* leaf_methods; /* 0xBC */ s8 unk_0xBC; - /* 0xBD */ u8 unk_0xBD; + /* 0xBD */ u8 draw_interp_frame; /* 0xBE */ draw_priority_class draw_priority; } leafdraw_class; diff --git a/src/d/d_kankyo.cpp b/src/d/d_kankyo.cpp index 05a0f70f5a..6b84fbae43 100644 --- a/src/d/d_kankyo.cpp +++ b/src/d/d_kankyo.cpp @@ -8106,23 +8106,11 @@ void dKankyo_HIO_c::genMessage(JORMContext* mctx) { #endif -#if TARGET_PC -static void interp_callback(bool isSimFrame, void* pUserWork) { - if (!isSimFrame) { - g_env_light.drawKankyo(); - } -} -#endif - void dScnKy_env_light_c::drawKankyo() { setSunpos(); SetBaseLight(); setLight(); dKy_setLight_nowroom(g_env_light.PrevCol); - -#if TARGET_PC - dusk::frame_interp::add_interpolation_callback(interp_callback, nullptr); -#endif } void dKy_undwater_filter_draw() { @@ -8264,6 +8252,10 @@ static int dKy_Create(void* i_this) { kankyo_class* kankyo = (kankyo_class*)i_this; BOOL next_time_set = false; +#if TARGET_PC + kankyo->base.draw_interp_frame = true; +#endif + stage_envr_info_class* stage_envr_p = dComIfGp_getStageEnvrInfo(); if (stage_envr_p != NULL && dComIfGp_getStartStageRoomNo() != -1) { stage_envr_p += dComIfGp_getStartStageRoomNo(); diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index 01604ede4f..c07e1f5cee 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -53,16 +53,6 @@ struct InterpolationCallBackWork { std::vector s_interpolationCallBackWork; -void set_enabled(bool enabled) { - if (g_enabled == enabled) - return; - - g_enabled = enabled; - - if (!g_enabled) - s_interpolationCallBackWork.clear(); -} - void copy_view_to_snap(CameraSnapshot* dst, const view_class& v) { dst->eye = v.lookat.eye; dst->center = v.lookat.center; @@ -134,20 +124,24 @@ void clear_replacements() { namespace dusk::frame_interp { void ensure_initialized() { - set_enabled(getSettings().game.enableFrameInterpolation); s_initialized = true; } +void begin_frame(bool enabled, bool is_sim_frame, float step) { + g_enabled = enabled; + g_is_sim_frame = is_sim_frame; + g_step = std::clamp(step, 0.0f, 1.0f); + if (is_sim_frame) { + s_interpolationCallBackWork.clear(); + } +} + bool is_enabled() { return g_enabled; } -void begin_frame(bool is_sim_frame, float step) { - g_is_sim_frame = is_sim_frame; - g_step = std::clamp(step, 0.0f, 1.0f); - if (is_sim_frame) { - s_interpolationCallBackWork.clear(); - } +bool is_sim_frame() { + return g_is_sim_frame; } void begin_record() { @@ -285,6 +279,12 @@ void record_camera(::camera_process_class* cam, int camera_id) { } void interp_view(::view_class* view) { + if (!g_enabled) + return; + + if (!s_cam_prev.valid || !s_cam_curr.valid) + return; + const f32 step = get_interpolation_step(); cXyz eye; cXyz center; diff --git a/src/f_pc/f_pc_leaf.cpp b/src/f_pc/f_pc_leaf.cpp index eab6532a92..7f6d6df802 100644 --- a/src/f_pc/f_pc_leaf.cpp +++ b/src/f_pc/f_pc_leaf.cpp @@ -6,6 +6,10 @@ #include "f_pc/f_pc_leaf.h" #include "f_pc/f_pc_debug_sv.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + s16 fpcLf_GetPriority(const leafdraw_class* i_leaf) { return fpcDwPi_Get(&i_leaf->draw_priority); } @@ -16,6 +20,11 @@ int fpcLf_DrawMethod(leafdraw_method_class* i_methods, void* i_process) { int fpcLf_Draw(leafdraw_class* i_leaf) { int ret = 0; +#if TARGET_PC + if (!i_leaf->draw_interp_frame && !dusk::frame_interp::is_sim_frame()) { + return ret; + } +#endif if (i_leaf->unk_0xBC == 0) { ret = fpcLf_DrawMethod(i_leaf->leaf_methods, i_leaf); } @@ -56,6 +65,9 @@ int fpcLf_Create(leafdraw_class* i_leaf) { LEAFDRAW_BASE(i_leaf).subtype = fpcBs_MakeOfType(&g_fpcLf_type); fpcDwPi_Init(&i_leaf->draw_priority, pprofile->priority); i_leaf->unk_0xBC = 0; +#if TARGET_PC + i_leaf->draw_interp_frame = false; +#endif } int ret = fpcMtd_Create(&i_leaf->leaf_methods->base, i_leaf); diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 7ef445fbe6..77bccadede 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -70,6 +70,7 @@ #include "dusk/config.hpp" #include "dusk/imgui/ImGuiConsole.hpp" #include "tracy/Tracy.hpp" +#include "f_pc/f_pc_draw.h" // --- GLOBALS --- s8 mDoMain::developmentMode = -1; @@ -233,8 +234,8 @@ void main01(void) { mDoGph_gInf_c::updateRenderSize(); + dusk::frame_interp::begin_frame(pacing.is_interpolating, pacing.do_sim_tick, pacing.interpolation_step); if (pacing.is_interpolating) { - dusk::frame_interp::begin_frame(pacing.do_sim_tick, pacing.interpolation_step); if (pacing.do_sim_tick) { dusk::frame_interp::set_ui_tick_pending(true); mDoCPd_c::read(); @@ -242,6 +243,9 @@ void main01(void) { fapGm_Execute(); mDoAud_Execute(); dusk::game_clock::reset_accumulator(); + } else { + // run draw functions for anything specially marked to handle interp on non-sim ticks + fpcM_DrawIterater((fpcM_DrawIteraterFunc)fpcM_Draw); } dusk::frame_interp::interpolate(); { From e5bf7606ec6f479c486a1c4320eb11a3343c4608 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 03:19:44 -0700 Subject: [PATCH 31/68] run d_a_bg::draw on interp frames to set up lights. this is a somewhat hacky workaround for lighting setup, but it might be good enough? --- include/dusk/frame_interpolation.h | 9 --------- include/f_pc/f_pc_node.h | 1 + src/d/actor/d_a_bg.cpp | 5 +++++ src/f_pc/f_pc_node.cpp | 19 ++++++++++++++++++- src/m_Do/m_Do_ext.cpp | 6 ++++++ src/m_Do/m_Do_main.cpp | 13 +++++++------ 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/include/dusk/frame_interpolation.h b/include/dusk/frame_interpolation.h index 7894eda9c2..bec9b600cf 100644 --- a/include/dusk/frame_interpolation.h +++ b/include/dusk/frame_interpolation.h @@ -46,15 +46,6 @@ void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork void begin_presentation_camera(); void end_presentation_camera(); -struct PresentationCameraScope { - PresentationCameraScope() { begin_presentation_camera(); } - ~PresentationCameraScope() { end_presentation_camera(); } - PresentationCameraScope(const PresentationCameraScope&) = delete; - PresentationCameraScope& operator=(const PresentationCameraScope&) = delete; - PresentationCameraScope(PresentationCameraScope&&) = delete; - PresentationCameraScope& operator=(PresentationCameraScope&&) = delete; -}; - } // namespace frame_interp } // namespace dusk #endif diff --git a/include/f_pc/f_pc_node.h b/include/f_pc/f_pc_node.h index 3be7fe6c8f..0d96fd2de5 100644 --- a/include/f_pc/f_pc_node.h +++ b/include/f_pc/f_pc_node.h @@ -18,6 +18,7 @@ typedef struct process_node_class { /* 0x0BC */ layer_class layer; /* 0x0E8 */ node_list_class layer_nodelist[16]; /* 0x1A8 */ s8 unk_0x1A8; + /* 0x1A9 */ s8 draw_interp_frame; } process_node_class; typedef struct node_process_profile_definition { diff --git a/src/d/actor/d_a_bg.cpp b/src/d/actor/d_a_bg.cpp index e73723d1f9..9c453b6553 100644 --- a/src/d/actor/d_a_bg.cpp +++ b/src/d/actor/d_a_bg.cpp @@ -623,6 +623,11 @@ int daBg_c::create() { dComIfGp_roomControl_onStatusFlag(roomNo, 0x10); OS_REPORT(" room%d\n", roomNo); + +#if TARGET_PC + draw_interp_frame = true; +#endif + return cPhs_COMPLEATE_e; } diff --git a/src/f_pc/f_pc_node.cpp b/src/f_pc/f_pc_node.cpp index 205d392ff1..ca7bcc044b 100644 --- a/src/f_pc/f_pc_node.cpp +++ b/src/f_pc/f_pc_node.cpp @@ -7,6 +7,13 @@ #include "f_pc/f_pc_layer_iter.h" #include "f_pc/f_pc_debug_sv.h" +#if TARGET_PC +#include "f_op/f_op_draw_iter.h" +#include "f_pc/f_pc_manager.h" + +#include "dusk/frame_interpolation.h" +#endif + int fpcNd_DrawMethod(nodedraw_method_class* i_method_class, void* i_data) { return fpcMtd_Method(i_method_class->draw_method, i_data); } @@ -18,7 +25,17 @@ int fpcNd_Draw(process_node_class* i_procNode) { if (i_procNode->unk_0x1A8 == 0) { layer_class* save_layer = fpcLy_CurrentLayer(); fpcLy_SetCurrentLayer(&var_r28->layer); - ret = fpcNd_DrawMethod(i_procNode->nodedraw_method, i_procNode); +#if TARGET_PC + if (!i_procNode->draw_interp_frame && !dusk::frame_interp::is_sim_frame()) { + for (create_tag_class* i = fopDwIt_Begin(); i != NULL; i = fopDwIt_Next(i)) { + void* process = i->mpTagData; + fpcM_Draw(process); + } + } else +#endif + { + ret = fpcNd_DrawMethod(i_procNode->nodedraw_method, i_procNode); + } fpcLy_SetCurrentLayer(save_layer); } diff --git a/src/m_Do/m_Do_ext.cpp b/src/m_Do/m_Do_ext.cpp index 024dd596a8..84bf51ecf5 100644 --- a/src/m_Do/m_Do_ext.cpp +++ b/src/m_Do/m_Do_ext.cpp @@ -25,6 +25,7 @@ #include #include #include "dusk/logging.h" +#include "dusk/frame_interpolation.h" u8 mDoExt::CurrentHeapAdjustVerbose; u8 mDoExt::HeapAdjustVerbose; @@ -349,6 +350,11 @@ void mDoExt_modelUpdateDL(J3DModel* i_model) { } void mDoExt_modelEntryDL(J3DModel* i_model) { +#if TARGET_PC + if (!dusk::frame_interp::is_sim_frame()) + return; +#endif + modelMtxErrorCheck(i_model); J3DModelData* model_data = i_model->getModelData(); diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 77bccadede..9fa980c0ab 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -243,15 +243,16 @@ void main01(void) { fapGm_Execute(); mDoAud_Execute(); dusk::game_clock::reset_accumulator(); - } else { - // run draw functions for anything specially marked to handle interp on non-sim ticks - fpcM_DrawIterater((fpcM_DrawIteraterFunc)fpcM_Draw); } dusk::frame_interp::interpolate(); - { - dusk::frame_interp::PresentationCameraScope presentation_camera; - cAPIGph_Painter(); + dusk::frame_interp::begin_presentation_camera(); + if (!pacing.do_sim_tick) { + // run draw functions for anything specially marked to handle interp on non-sim + // ticks + fpcM_DrawIterater((fpcM_DrawIteraterFunc)fpcM_Draw); } + cAPIGph_Painter(); + dusk::frame_interp::end_presentation_camera(); dusk::frame_interp::set_ui_tick_pending(false); } else { dusk::frame_interp::set_ui_tick_pending(true); From 4e7711725a561d1eb1fd29398e9c2c48e5d8f880 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Sun, 19 Apr 2026 03:58:22 -0700 Subject: [PATCH 32/68] attempt to fix clawshot aim model position --- src/d/d_camera.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index b93d569003..9d77e5f875 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -7063,6 +7063,15 @@ bool dCamera_c::subjectCamera(s32 param_0) { } cXyz sp1E0(val0, val2, val1); + +#if TARGET_PC + f32 aspect = mDoGph_gInf_c::getAspect(); + f32 baseAspect = FB_WIDTH / FB_HEIGHT; + if (aspect > baseAspect) { + sp1E0.z += (aspect - baseAspect) * 4; + } +#endif + sp1D4 = dCamMath::xyzRotateX(sp1E0, angle_x); sp1E0 = dCamMath::xyzRotateY(sp1D4, angle_y); f32 sp6C = sp12 ? 40.0f : 0.0f; From 783230b65406361c7ea5156004368660e0e63312 Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Sun, 19 Apr 2026 10:02:55 -0600 Subject: [PATCH 33/68] Add pause on unfocus feature --- include/dusk/audio/DuskAudioSystem.h | 2 ++ include/dusk/game_clock.h | 1 + include/dusk/main.h | 1 + include/dusk/settings.h | 1 + src/dusk/audio/DuskAudioSystem.cpp | 8 ++++++++ src/dusk/game_clock.cpp | 5 +++++ src/dusk/imgui/ImGuiMenuGame.cpp | 3 +++ src/dusk/settings.cpp | 2 ++ src/m_Do/m_Do_main.cpp | 17 +++++++++++++++++ 9 files changed, 40 insertions(+) diff --git a/include/dusk/audio/DuskAudioSystem.h b/include/dusk/audio/DuskAudioSystem.h index 724776f5f1..8cdd757c82 100644 --- a/include/dusk/audio/DuskAudioSystem.h +++ b/include/dusk/audio/DuskAudioSystem.h @@ -12,6 +12,8 @@ namespace dusk::audio { void SetMasterVolume(f32 value); + void SetPaused(bool paused); + u32 GetResetCount(int channelIdx); f32 VolumeFromU16(u16 value); diff --git a/include/dusk/game_clock.h b/include/dusk/game_clock.h index 6e7ea500eb..4a394b5c2e 100644 --- a/include/dusk/game_clock.h +++ b/include/dusk/game_clock.h @@ -8,6 +8,7 @@ namespace game_clock { void ensure_initialized(); void reset_accumulator(); +void reset_frame_timer(); constexpr float sim_pace() { return 1.0f / 30.0f; } constexpr float period_for_original_frames(float frame_count) { return frame_count * sim_pace(); } diff --git a/include/dusk/main.h b/include/dusk/main.h index a9194e2aba..2152a6d564 100644 --- a/include/dusk/main.h +++ b/include/dusk/main.h @@ -5,6 +5,7 @@ namespace dusk { extern bool IsRunning; extern bool IsShuttingDown; extern bool IsGameLaunched; + extern bool IsFocusPaused; } #endif // DUSK_MAIN_H diff --git a/include/dusk/settings.h b/include/dusk/settings.h index bcec0afcd2..753649df2e 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -68,6 +68,7 @@ struct UserSettings { ConfigVar enableMirrorMode; ConfigVar invertCameraXAxis; ConfigVar disableMainHUD; + ConfigVar pauseOnFocusLost; // Graphics ConfigVar bloomMode; diff --git a/src/dusk/audio/DuskAudioSystem.cpp b/src/dusk/audio/DuskAudioSystem.cpp index c818e46738..3577d3c9c7 100644 --- a/src/dusk/audio/DuskAudioSystem.cpp +++ b/src/dusk/audio/DuskAudioSystem.cpp @@ -77,6 +77,14 @@ void dusk::audio::SetMasterVolume(const f32 value) { MasterVolume = value; } +void dusk::audio::SetPaused(const bool paused) { + if (paused) { + SDL_PauseAudioStreamDevice(PlaybackStream); + } else { + SDL_ResumeAudioStreamDevice(PlaybackStream); + } +} + void dusk::audio::SetEnableReverb(const bool value) { JASCriticalSection section; diff --git a/src/dusk/game_clock.cpp b/src/dusk/game_clock.cpp index 176c43c3b7..8eedf44da3 100644 --- a/src/dusk/game_clock.cpp +++ b/src/dusk/game_clock.cpp @@ -29,6 +29,11 @@ void reset_accumulator() { s_sim_accumulator = 0.0f; } +void reset_frame_timer() { + s_previous_sample = clock::now(); + s_sim_accumulator = 0.0f; +} + MainLoopPacer advance_main_loop() { ensure_initialized(); diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index f9c7634a5c..d1ff216b3c 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -194,6 +194,9 @@ namespace dusk { #if DUSK_ENABLE_SENTRY_NATIVE config::ImGuiCheckbox("Enable Crash Reporting", getSettings().backend.enableCrashReporting); #endif + if (!IsMobile) { + config::ImGuiCheckbox("Pause on Focus Lost", getSettings().game.pauseOnFocusLost); + } ImGui::EndMenu(); } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index fa87846f68..441ae4a1c3 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -42,6 +42,7 @@ UserSettings g_userSettings = { .enableMirrorMode {"game.enableMirrorMode", false}, .invertCameraXAxis {"game.invertCameraXAxis", false}, .disableMainHUD {"game.disableMainHUD", false}, + .pauseOnFocusLost {"game.pauseOnFocusLost", false}, // Graphics .bloomMode {"game.bloomMode", BloomMode::Classic}, @@ -136,6 +137,7 @@ void registerSettings() { Register(g_userSettings.game.enableMirrorMode); Register(g_userSettings.game.invertCameraXAxis); Register(g_userSettings.game.disableMainHUD); + Register(g_userSettings.game.pauseOnFocusLost); Register(g_userSettings.game.bloomMode); Register(g_userSettings.game.bloomMultiplier); Register(g_userSettings.game.enableWaterRefraction); diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 0249269f32..ab1990b178 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -68,6 +68,7 @@ #include "cxxopts.hpp" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" +#include "dusk/settings.h" #include "dusk/imgui/ImGuiConsole.hpp" #include "tracy/Tracy.hpp" @@ -92,6 +93,7 @@ const int audioHeapSize = 0x14D800; bool dusk::IsRunning = true; bool dusk::IsShuttingDown = false; bool dusk::IsGameLaunched = false; +bool dusk::IsFocusPaused = false; #endif s32 LOAD_COPYDATE(void*) { @@ -208,6 +210,16 @@ void main01(void) { goto eventsDone; case AURORA_SDL_EVENT: dusk::g_imguiConsole.HandleSDLEvent(event->sdl); + if (event->sdl.type == SDL_EVENT_WINDOW_FOCUS_LOST && + dusk::getSettings().game.pauseOnFocusLost) { + dusk::IsFocusPaused = true; + dusk::audio::SetPaused(true); + } else if (event->sdl.type == SDL_EVENT_WINDOW_FOCUS_GAINED && + dusk::IsFocusPaused) { + dusk::IsFocusPaused = false; + dusk::audio::SetPaused(false); + dusk::game_clock::reset_frame_timer(); + } break; case AURORA_DISPLAY_SCALE_CHANGED: dusk::ImGuiEngine_Initialize(event->windowSize.scale); @@ -221,6 +233,11 @@ void main01(void) { eventsDone:; + if (dusk::IsFocusPaused) { + std::this_thread::sleep_for(std::chrono::milliseconds(16)); + continue; + } + const dusk::game_clock::MainLoopPacer pacing = dusk::game_clock::advance_main_loop(); VIWaitForRetrace(); From e47027871701f865733ea6f2a432d494a4304706 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 10:27:37 -0700 Subject: [PATCH 34/68] grass/flower/shadow interp fixes --- src/d/actor/d_flower.inc | 9 +++++---- src/d/actor/d_grass.inc | 8 ++++---- src/d/d_drawlist.cpp | 22 ++++++++++++---------- src/m_Do/m_Do_main.cpp | 1 + 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/d/actor/d_flower.inc b/src/d/actor/d_flower.inc index 4f28d8fac3..702337d9e5 100644 --- a/src/d/actor/d_flower.inc +++ b/src/d/actor/d_flower.inc @@ -700,13 +700,13 @@ void dFlower_packet_c::draw() { #ifdef TARGET_PC Mtx flower_mtx; if (dusk::frame_interp::lookup_replacement(reinterpret_cast(&sp44->m_modelMtx), flower_mtx)) { + GXLoadPosMtxImm(flower_mtx, 0); - } else { + } else #endif + { GXLoadPosMtxImm(sp44->m_modelMtx, 0); -#ifdef TARGET_PC } -#endif GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); #if TARGET_PC @@ -855,6 +855,7 @@ void dFlower_packet_c::draw() { #ifdef TARGET_PC Mtx flower_mtx; if (dusk::frame_interp::lookup_replacement(reinterpret_cast(&sp34->m_modelMtx), flower_mtx)) { + cMtx_concat(j3dSys.getViewMtx(), flower_mtx, flower_mtx); GXLoadPosMtxImm(flower_mtx, 0); } else { #endif @@ -994,7 +995,7 @@ void dFlower_packet_c::update() { mDoMtx_stack_c::scaleM(temp_f31, temp_f31, temp_f31); cMtx_concat(j3dSys.getViewMtx(), temp_r28, data_p->m_modelMtx); #ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(data_p->m_modelMtx); + dusk::frame_interp::record_final_mtx(mDoMtx_stack_c::get(), data_p->m_modelMtx); #endif } } diff --git a/src/d/actor/d_grass.inc b/src/d/actor/d_grass.inc index 91537fc1c2..ab2724060f 100644 --- a/src/d/actor/d_grass.inc +++ b/src/d/actor/d_grass.inc @@ -756,13 +756,13 @@ void dGrass_packet_c::draw() { #ifdef TARGET_PC Mtx grass_mtx; if (dusk::frame_interp::lookup_replacement(reinterpret_cast(&var_r29->m_modelMtx), grass_mtx)) { + cMtx_concat(j3dSys.getViewMtx(), grass_mtx, grass_mtx); GXLoadPosMtxImm(grass_mtx, 0); - } else { + } else #endif + { GXLoadPosMtxImm(var_r29->m_modelMtx, 0); -#ifdef TARGET_PC } -#endif GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0); if (var_r29->field_0x05 <= 3 || var_r29->field_0x05 >= 10) { if (var_r29->field_0x02 < -1) { @@ -1018,7 +1018,7 @@ void dGrass_packet_c::update() { cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), data_p->m_modelMtx); } #ifdef TARGET_PC - dusk::frame_interp::record_final_mtx(data_p->m_modelMtx); + dusk::frame_interp::record_final_mtx(mDoMtx_stack_c::get(), data_p->m_modelMtx); #endif } } diff --git a/src/d/d_drawlist.cpp b/src/d/d_drawlist.cpp index d4b477b1f4..373791f994 100644 --- a/src/d/d_drawlist.cpp +++ b/src/d/d_drawlist.cpp @@ -1319,13 +1319,13 @@ void dDlst_shadowSimple_c::draw() { #ifdef TARGET_PC Mtx volume_mtx; if (dusk::frame_interp::lookup_replacement(mVolumeMtxKey, volume_mtx)) { + cMtx_concat(j3dSys.getViewMtx(), volume_mtx, volume_mtx); GXLoadPosMtxImm(volume_mtx, GX_PNMTX0); - } else { + } else #endif + { GXLoadPosMtxImm(mVolumeMtx, GX_PNMTX0); -#ifdef TARGET_PC } -#endif GXSetCurrentMtx(GX_PNMTX0); GXCallDisplayList(l_frontMat, 0x40); GXCallDisplayList(l_shadowVolumeDL, 0x40); @@ -1334,13 +1334,13 @@ void dDlst_shadowSimple_c::draw() { #ifdef TARGET_PC Mtx shadow_mtx; if (dusk::frame_interp::lookup_replacement(mMtxKey, shadow_mtx)) { + cMtx_concat(j3dSys.getViewMtx(), shadow_mtx, shadow_mtx); GXLoadPosMtxImm(shadow_mtx, GX_PNMTX1); - } else { + } else #endif + { GXLoadPosMtxImm(mMtx, GX_PNMTX1); -#ifdef TARGET_PC } -#endif GXSetCurrentMtx(GX_PNMTX1); if (mpTexObj != NULL) { @@ -1399,6 +1399,10 @@ void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* pa mDoMtx_stack_c::transS(param_0->x, param_1 + f30, param_0->z); mDoMtx_stack_c::YrotM(param_4); mDoMtx_stack_c::scaleM(param_2, f30 + f30 + 16.0f, param_2 * param_5); +#if TARGET_PC + mVolumeMtxKey = getInterpKey(param_0, 0x1); + dusk::frame_interp::record_final_mtx(mDoMtx_stack_c::get(), mVolumeMtxKey); +#endif cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), mVolumeMtx); f32 f31 = JMAFastSqrt(1.0f - param_3->x * param_3->x); f32 f29; @@ -1424,13 +1428,11 @@ void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* pa mDoMtx_stack_c::get()[2][3] = param_0->z; mDoMtx_stack_c::YrotM(param_4); mDoMtx_stack_c::scaleM(param_2, 1.0f, param_2 * param_5); - cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), mMtx); #ifdef TARGET_PC - mVolumeMtxKey = getInterpKey(param_0, 0x1); mMtxKey = getInterpKey(param_0, 0x2); - dusk::frame_interp::record_final_mtx(mVolumeMtx, mVolumeMtxKey); - dusk::frame_interp::record_final_mtx(mMtx, mMtxKey); + dusk::frame_interp::record_final_mtx(mDoMtx_stack_c::get(), mMtxKey); #endif + cMtx_concat(j3dSys.getViewMtx(), mDoMtx_stack_c::get(), mMtx); mpTexObj = param_6; } diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 9fa980c0ab..2e206dc49a 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -239,6 +239,7 @@ void main01(void) { if (pacing.do_sim_tick) { dusk::frame_interp::set_ui_tick_pending(true); mDoCPd_c::read(); + DuskDebugPad(); dusk::gyro::read(pacing.sim_pace); fapGm_Execute(); mDoAud_Execute(); From c6beeba14f14c820c9324bb4c8a4fddbc96fc0d3 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 11:56:48 -0700 Subject: [PATCH 35/68] proct --- src/dusk/imgui/ImGuiProcessOverlay.cpp | 126 +++++++++++++++++-------- 1 file changed, 89 insertions(+), 37 deletions(-) diff --git a/src/dusk/imgui/ImGuiProcessOverlay.cpp b/src/dusk/imgui/ImGuiProcessOverlay.cpp index 04a87f00b6..ed71e6c485 100644 --- a/src/dusk/imgui/ImGuiProcessOverlay.cpp +++ b/src/dusk/imgui/ImGuiProcessOverlay.cpp @@ -9,53 +9,109 @@ #include "f_pc/f_pc_layer_iter.h" #include "f_pc/f_pc_leaf.h" #include "f_pc/f_pc_node.h" +#include "d/d_debug_viewer.h" #include "imgui.h" #include "ImGuiConsole.hpp" #include "ImGuiMenuTools.hpp" #include "imgui_internal.h" namespace dusk { - bool showTreeRecursive; + static bool BeginProcTable() { + static ImGuiTableFlags table_flags = + ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_Resizable | + ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody; + + if (ImGui::BeginTable("proc_table", 7)) { + ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 128); + ImGui::TableSetupColumn("En", ImGuiTableColumnFlags_WidthFixed, 32); + ImGui::TableSetupColumn("Vs", ImGuiTableColumnFlags_WidthFixed, 32); + ImGui::TableSetupColumn("ProcName"); + ImGui::TableSetupColumn("Param", ImGuiTableColumnFlags_WidthFixed, 128); + ImGui::TableSetupColumn("Pi", ImGuiTableColumnFlags_WidthFixed, 64); + ImGui::TableSetupColumn("DwPi", ImGuiTableColumnFlags_WidthFixed, 64); + ImGui::TableHeadersRow(); + return true; + } + return false; + } static int ShowProcess(void* p, void*) { auto proc = static_cast(p); - char buf[64]; - snprintf(buf, sizeof(buf), "%d", proc->id); + ImGui::TableNextRow(); + ImGui::PushID(proc); + bool pending = proc->create_req != nullptr; + if (pending) + ImGui::PushStyleColor(ImGuiCol_Text, {255, 200, 0, 255}); + ImGui::TableNextColumn(); - ImVec2 avail = ImGui::GetContentRegionAvail(); + char id_buf[32]; + sprintf(id_buf, "%d", proc->id); - ImVec2 vec = { avail.x, 0 }; - if (ImGui::BeginChild(buf, vec, ImGuiChildFlags_Border | ImGuiChildFlags_AutoResizeY)) { - ImGui::Text("[%d] %s", proc->id, GetProcName(proc->profname)); - ImGui::Text("init_state: %d, create_phase: %d", proc->state.init_state, proc->state.create_phase); + int flags = ImGuiTreeNodeFlags_SpanAllColumns; + bool isLayer = fpcBs_Is_JustOfType(g_fpcNd_type, proc->subtype); + if (isLayer) { + flags |= ImGuiTreeNodeFlags_DefaultOpen; + } else { + flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; + } - const char* ofTypeName = "unknown"; - if (proc->subtype == g_fpcNd_type) { - ofTypeName = "Node"; - } - else if (proc->subtype == g_fpcLf_type) { - ofTypeName = "Leaf"; - } + bool open = ImGui::TreeNodeEx(id_buf, flags); + fopAc_ac_c* ac = fopAcM_IsActor(proc) ? (fopAc_ac_c*)proc : nullptr; + if (ac != nullptr && ImGui::IsItemHovered()) { + fopAcM_DrawCullingBox(ac, {0, 255, 255, 255}); + } - ImGui::Text("OfType: %d (%s), layer: %d", proc->subtype, ofTypeName, proc->layer_tag.layer->layer_id); + ImGui::TableNextColumn(); + bool enable = !fpcM_IsPause(proc, 1); + if (ImGui::Checkbox("##enable", &enable)) { + if (enable) + fpcM_PauseDisable(proc, 1); + else + fpcM_PauseEnable(proc, 1); + } - if (proc->create_req != nullptr) { - ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "Pending create request"); - } - - if (showTreeRecursive) { - if (fpcBs_Is_JustOfType(g_fpcNd_type, proc->subtype)) { - auto procNode = static_cast(p); - - ImGui::Text("Owns layer %d", procNode->layer.layer_id); - - fpcLyIt_OnlyHere(&procNode->layer, ShowProcess, nullptr); - } + ImGui::TableNextColumn(); + if (fpcBs_Is_JustOfType(g_fpcLf_type, proc->subtype)) { + leafdraw_class* lf = (leafdraw_class*)proc; + bool vis = lf->unk_0xBC == 0; + if (ImGui::Checkbox("##visible", &vis)) { + if (vis) + lf->unk_0xBC = 0; + else + lf->unk_0xBC = 1; } } - ImGui::EndChild(); + ImGui::TableNextColumn(); + ImGui::Text("%s", ac != nullptr ? fopAcM_getProcNameString(ac) : GetProcName(proc->profname)); + + ImGui::TableNextColumn(); + if (proc->profname == fpcNm_ROOM_SCENE_e) { + ImGui::Text("Room %d", proc->parameters); + } else { + ImGui::Text("%08x", proc->parameters); + } + + ImGui::TableNextColumn(); + ImGui::Text("%d", proc->priority.current_info.list_id); + + ImGui::TableNextColumn(); + if (fpcBs_Is_JustOfType(g_fpcLf_type, proc->subtype)) { + ImGui::Text("%d", fpcM_DrawPriority(proc)); + } else { + ImGui::Text("--"); + } + + if (isLayer && open) { + auto procNode = static_cast(p); + fpcLyIt_OnlyHere(&procNode->layer, ShowProcess, nullptr); + ImGui::TreePop(); + } + + if (pending) + ImGui::PopStyleColor(1); + ImGui::PopID(); return 1; } @@ -76,15 +132,11 @@ namespace dusk { if (ImGui::Begin("Processes", &m_showProcessManagement)) { if (ImGui::BeginTabBar("Tabs")) { - showTreeRecursive = true; if (ImGui::BeginTabItem("Tree")) { - fpcLyIt_OnlyHere(fpcLy_RootLayer(), ShowProcess, nullptr); - ImGui::EndTabItem(); - } - - showTreeRecursive = false; - if (ImGui::BeginTabItem("All layers")) { - fpcLyIt_All(ShowProcess, nullptr); + if (BeginProcTable()) { + fpcLyIt_OnlyHere(fpcLy_RootLayer(), ShowProcess, nullptr); + ImGui::EndTable(); + } ImGui::EndTabItem(); } From a4ff97564c18fb2922c82a5d976f2545411f8820 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 12:39:08 -0700 Subject: [PATCH 36/68] minor cleanup --- src/f_pc/f_pc_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/f_pc/f_pc_manager.cpp b/src/f_pc/f_pc_manager.cpp index 4d424ad75e..ad226b4d1d 100644 --- a/src/f_pc/f_pc_manager.cpp +++ b/src/f_pc/f_pc_manager.cpp @@ -65,7 +65,7 @@ void fpcM_Management(fpcM_ManagementFunc i_preExecuteFn, fpcM_ManagementFunc i_p #ifdef TARGET_PC // FRAME INTERP NOTE: Called in m_Do_main when interp is enabled - if (!dusk::getSettings().game.enableFrameInterpolation || dusk::getTransientSettings().skipFrameRateLimit) + if (!dusk::frame_interp::is_enabled()) #endif { cAPIGph_Painter(); From 53c005c4f1f99a0438c191fb8a5295361b08c1fd Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 12:48:50 -0700 Subject: [PATCH 37/68] jutfader doc --- .../include/JSystem/JUtility/JUTFader.h | 14 +-- libs/JSystem/src/JFramework/JFWDisplay.cpp | 2 +- libs/JSystem/src/JUtility/JUTFader.cpp | 88 +++++++++---------- src/d/d_menu_window.cpp | 14 +-- src/d/d_ovlp_fade.cpp | 4 +- 5 files changed, 62 insertions(+), 60 deletions(-) diff --git a/libs/JSystem/include/JSystem/JUtility/JUTFader.h b/libs/JSystem/include/JSystem/JUtility/JUTFader.h index 6c98a6840f..b85e4a6cd4 100644 --- a/libs/JSystem/include/JSystem/JUtility/JUTFader.h +++ b/libs/JSystem/include/JSystem/JUtility/JUTFader.h @@ -11,8 +11,10 @@ class JUTFader { public: enum EStatus { - UNKSTATUS_M1 = -1, - UNKSTATUS_0 = 0, + None, + Wait, + FadeIn, + FadeOut, }; JUTFader(int, int, int, int, JUtility::TColor); @@ -29,12 +31,12 @@ public: void setColor(JUtility::TColor color) { mColor.set(color); } /* 0x04 */ s32 mStatus; - /* 0x08 */ u16 field_0x8; - /* 0x0A */ u16 field_0xa; + /* 0x08 */ u16 mDuration; + /* 0x0A */ u16 mTimer; /* 0x0C */ JUtility::TColor mColor; /* 0x10 */ JGeometry::TBox2 mBox; - /* 0x20 */ int mEStatus; - /* 0x24 */ u32 field_0x24; + /* 0x20 */ int mStatusTimer; + /* 0x24 */ u32 mNextStatus; }; #endif /* JUTFADER_H */ diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index c35b283419..64b0fedcf2 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -219,7 +219,7 @@ void JFWDisplay::endGX() { if (dusk::frame_interp::get_ui_tick_pending()) { mFader->advance(); } - if (mFader->getStatus() != 1) { + if (mFader->getStatus() != JUTFader::Wait) { mFader->draw(); } #else diff --git a/libs/JSystem/src/JUtility/JUTFader.cpp b/libs/JSystem/src/JUtility/JUTFader.cpp index f7fa963e63..599ae22720 100644 --- a/libs/JSystem/src/JUtility/JUTFader.cpp +++ b/libs/JSystem/src/JUtility/JUTFader.cpp @@ -10,51 +10,51 @@ JUTFader::JUTFader(int x, int y, int width, int height, JUtility::TColor pColor) : mColor(pColor), mBox(x, y, x + width, y + height) { - mStatus = 0; - field_0x8 = 0; - field_0xa = 0; - field_0x24 = 0; - mEStatus = UNKSTATUS_M1; + mStatus = None; + mDuration = 0; + mTimer = 0; + mNextStatus = 0; + mStatusTimer = -1; } void JUTFader::advance() { - if (0 <= mEStatus && mEStatus-- == 0) { - mStatus = field_0x24; + if (0 <= mStatusTimer && mStatusTimer-- == 0) { + mStatus = mNextStatus; } - if (mStatus == 1) { + if (mStatus == Wait) { return; } switch (mStatus) { - case 0: + case None: mColor.a = 0xFF; break; - case 2: + case FadeIn: #if AVOID_UB - if (field_0x8 == 0) { - mStatus = 1; + if (mDuration == 0) { + mStatus = Wait; break; } #endif - mColor.a = 0xFF - ((++field_0xa * 0xFF) / field_0x8); + mColor.a = 0xFF - ((++mTimer * 0xFF) / mDuration); - if (field_0xa >= field_0x8) { - mStatus = 1; + if (mTimer >= mDuration) { + mStatus = Wait; } break; - case 3: + case FadeOut: #if AVOID_UB - if (field_0x8 == 0) { - mStatus = 0; + if (mDuration == 0) { + mStatus = None; break; } #endif - mColor.a = ((++field_0xa * 0xFF) / field_0x8); + mColor.a = ((++mTimer * 0xFF) / mDuration); - if (field_0xa >= field_0x8) { - mStatus = 0; + if (mTimer >= mDuration) { + mStatus = None; } break; @@ -77,53 +77,53 @@ void JUTFader::draw() { } } -bool JUTFader::startFadeIn(int param_0) { +bool JUTFader::startFadeIn(int duration) { bool statusCheck = mStatus == 0; if (statusCheck) { - mStatus = 2; - field_0xa = 0; - field_0x8 = param_0; + mStatus = FadeIn; + mTimer = 0; + mDuration = duration; } return statusCheck; } -bool JUTFader::startFadeOut(int param_0) { +bool JUTFader::startFadeOut(int duration) { bool statusCheck = mStatus == 1; if (statusCheck) { - mStatus = 3; - field_0xa = 0; - field_0x8 = param_0; + mStatus = FadeOut; + mTimer = 0; + mDuration = duration; } return statusCheck; } -void JUTFader::setStatus(JUTFader::EStatus i_status, int param_1) { +void JUTFader::setStatus(JUTFader::EStatus i_status, int timer) { switch (i_status) { - case 0: - if (param_1 != 0) { - field_0x24 = 0; - mEStatus = param_1; + case None: + if (timer != 0) { + mNextStatus = None; + mStatusTimer = timer; break; } - mStatus = 0; - field_0x24 = 0; - mEStatus = 0; + mStatus = None; + mNextStatus = None; + mStatusTimer = 0; break; - case 1: - if (param_1 != 0) { - field_0x24 = 1; - mEStatus = param_1; + case Wait: + if (timer != 0) { + mNextStatus = Wait; + mStatusTimer = timer; break; } - mStatus = 1; - field_0x24 = 1; - mEStatus = 0; + mStatus = Wait; + mNextStatus = Wait; + mStatusTimer = 0; break; } } diff --git a/src/d/d_menu_window.cpp b/src/d/d_menu_window.cpp index 6d424466e5..7f76f807df 100644 --- a/src/d/d_menu_window.cpp +++ b/src/d/d_menu_window.cpp @@ -699,7 +699,7 @@ void dMw_c::collect_open_proc() { dMeter2Info_set2DVibrationM(); } - if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::UNKSTATUS_0) { + if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::None) { mMenuProc = COLLECT_MOVE; } } @@ -914,7 +914,7 @@ void dMw_c::collect_letter_move_proc() { } void dMw_c::collect_letter_close_proc() { - if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::UNKSTATUS_0) { + if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::None) { mMenuProc = COLLECT_MOVE; } } @@ -946,7 +946,7 @@ void dMw_c::collect_fishing_move_proc() { } void dMw_c::collect_fishing_close_proc() { - if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::UNKSTATUS_0) { + if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::None) { mMenuProc = COLLECT_MOVE; } } @@ -977,7 +977,7 @@ void dMw_c::collect_skill_move_proc() { } void dMw_c::collect_skill_close_proc() { - if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::UNKSTATUS_0) { + if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::None) { mMenuProc = COLLECT_MOVE; } } @@ -1008,13 +1008,13 @@ void dMw_c::collect_insect_move_proc() { } void dMw_c::collect_insect_close_proc() { - if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::UNKSTATUS_0) { + if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::None) { mMenuProc = COLLECT_MOVE; } } void dMw_c::insect_open_proc() { - if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::UNKSTATUS_0) { + if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::None) { field_0x152 = 0; dComIfGp_setHeapLockFlag(1); dMw_insect_create(1); @@ -1050,7 +1050,7 @@ void dMw_c::insect_move_proc() { } void dMw_c::insect_close_proc() { - if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::UNKSTATUS_0) { + if (mDoGph_gInf_c::getFader()->getStatus() == JUTFader::None) { mMenuProc = NO_MENU; } } diff --git a/src/d/d_ovlp_fade.cpp b/src/d/d_ovlp_fade.cpp index 1ae49ab74a..43732cf4be 100644 --- a/src/d/d_ovlp_fade.cpp +++ b/src/d/d_ovlp_fade.cpp @@ -26,8 +26,8 @@ static void dOvlpFd_startFadeIn(int param_0) { JUTFader* fader = JFWDisplay::getManager()->getFader(); JUT_ASSERT(0, fader != NULL); - fader->setStatus(JUTFader::UNKSTATUS_0, 0); - fader->setStatus(JUTFader::UNKSTATUS_0, -1); + fader->setStatus(JUTFader::None, 0); + fader->setStatus(JUTFader::None, -1); fader->startFadeIn(param_0); } From eace147652aa0646e7993b92452010512930e2c9 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 12:57:30 -0700 Subject: [PATCH 38/68] mirror interp fix --- src/d/actor/d_a_mirror.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/d/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp index 5468d70f87..e9d56df9fb 100644 --- a/src/d/actor/d_a_mirror.cpp +++ b/src/d/actor/d_a_mirror.cpp @@ -13,6 +13,9 @@ #include #include #include "m_Do/m_Do_lib.h" +#if TARGET_PC +#include "dusk/frame_interpolation.h" +#endif #ifndef __MWERKS__ #include "dusk/math.h" @@ -441,7 +444,12 @@ void dMirror_packet_c::draw() { } mDoLib_clipper::resetFar(); - reset(); +#if TARGET_PC + if (!dusk::frame_interp::is_sim_frame()) +#endif + { + reset(); + } } daMirror_c::daMirror_c() { From 3ed7ef1c884979ccd381daa6b561397ca8ec5089 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 13:22:00 -0700 Subject: [PATCH 39/68] fix stone shadow flicker --- src/d/actor/d_a_obj_stone.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/d/actor/d_a_obj_stone.cpp b/src/d/actor/d_a_obj_stone.cpp index f74adf12a7..080069e4f0 100644 --- a/src/d/actor/d_a_obj_stone.cpp +++ b/src/d/actor/d_a_obj_stone.cpp @@ -981,9 +981,7 @@ int daObjStone_c::draw() { if (!model) { f32 shadow_size = l_shadow_size[mStoneType]; TGXTexObj* pTex = dDlst_shadowControl_c::getSimpleTex(); - cXyz pos = current.pos; - - dComIfGd_setSimpleShadow(&pos, mChkObj.GetGroundH(), shadow_size, mChkObj.m_gnd, 0, + dComIfGd_setSimpleShadow(¤t.pos, mChkObj.GetGroundH(), shadow_size, mChkObj.m_gnd, 0, 1.0f, pTex); } return 1; From 2f84f0eaa4ae78cd00a79c525fdb63a2ab923b15 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 14:16:38 -0700 Subject: [PATCH 40/68] j3d small cleanup --- libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp | 6 +++--- libs/JSystem/src/J3DGraphBase/J3DTevs.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp b/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp index 6c109b3f8b..df6c977560 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp @@ -265,7 +265,7 @@ void J3DMaterial::diff(u32 diffFlags) { } void J3DMaterial::calc(f32 const (*param_0)[4]) { - if (j3dSys.checkFlag(0x40000000)) { + if (j3dSys.checkFlag(J3DSysFlag_PostTexMtx)) { mTexGenBlock->calcPostTexMtx(param_0); } else { mTexGenBlock->calc(param_0); @@ -276,7 +276,7 @@ void J3DMaterial::calc(f32 const (*param_0)[4]) { } void J3DMaterial::calcDiffTexMtx(f32 const (*param_0)[4]) { - if (j3dSys.checkFlag(0x40000000)) { + if (j3dSys.checkFlag(J3DSysFlag_PostTexMtx)) { mTexGenBlock->calcPostTexMtxWithoutViewMtx(param_0); } else { mTexGenBlock->calcWithoutViewMtx(param_0); @@ -288,7 +288,7 @@ void J3DMaterial::setCurrentMtx() { } void J3DMaterial::calcCurrentMtx() { - if (!j3dSys.checkFlag(0x40000000)) { + if (!j3dSys.checkFlag(J3DSysFlag_PostTexMtx)) { mCurrentMtx.setCurrentTexMtx( getTexCoord(0)->getTexGenMtx(), getTexCoord(1)->getTexGenMtx(), diff --git a/libs/JSystem/src/J3DGraphBase/J3DTevs.cpp b/libs/JSystem/src/J3DGraphBase/J3DTevs.cpp index 09ddc0b405..719976770b 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DTevs.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DTevs.cpp @@ -37,9 +37,9 @@ void loadTexCoordGens(u32 texGenNum, J3DTexCoord* texCoords) { var_r28 = 61; J3DGDWriteXFCmdHdr(GX_XF_REG_DUALTEX0, texGenNum); - if (j3dSys.checkFlag(0x40000000)) { + if (j3dSys.checkFlag(J3DSysFlag_PostTexMtx)) { for (int i = 0; i < texGenNum; i++) { - if (texCoords[i].getTexGenMtx() != 60) { + if (texCoords[i].getTexGenMtx() != GX_IDENTITY) { var_r28 = i * 3; } else { var_r28 = 61; From 55eec3b0de9fef146a06b2d8307c3afa4ae4c792 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 16:01:51 -0700 Subject: [PATCH 41/68] hitstun camera interp fix --- src/dusk/frame_interpolation.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index c07e1f5cee..c72923da7d 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -133,6 +133,7 @@ void begin_frame(bool enabled, bool is_sim_frame, float step) { g_step = std::clamp(step, 0.0f, 1.0f); if (is_sim_frame) { s_interpolationCallBackWork.clear(); + s_cam_prev = std::move(s_cam_curr); } } @@ -271,7 +272,6 @@ void record_camera(::camera_process_class* cam, int camera_id) { if (!g_enabled || camera_id != 0 || cam == nullptr) { return; } - s_cam_prev = std::move(s_cam_curr); copy_view_to_snap(&s_cam_curr, cam->view); #if WIDESCREEN_SUPPORT s_cam_curr.wideZoom = mDoGph_gInf_c::isWideZoom(); From 32cddc725b38d062b56a421f8db9c11d5ce397e7 Mon Sep 17 00:00:00 2001 From: "Jasper St. Pierre" Date: Sun, 19 Apr 2026 16:21:30 -0700 Subject: [PATCH 42/68] mirror flicker fix 2 --- include/d/actor/d_a_mirror.h | 3 +++ src/d/actor/d_a_mirror.cpp | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/include/d/actor/d_a_mirror.h b/include/d/actor/d_a_mirror.h index fd69c8682e..06c5603899 100644 --- a/include/d/actor/d_a_mirror.h +++ b/include/d/actor/d_a_mirror.h @@ -25,6 +25,9 @@ public: /* 0x164 */ cXyz mMinVal; /* 0x170 */ cXyz mMaxVal; /* 0x17C */ cXyz mViewScale; +#if TARGET_PC + bool mbReset = false; +#endif }; /** diff --git a/src/d/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp index e9d56df9fb..4711dee681 100644 --- a/src/d/actor/d_a_mirror.cpp +++ b/src/d/actor/d_a_mirror.cpp @@ -34,7 +34,11 @@ dMirror_packet_c::dMirror_packet_c() { } void dMirror_packet_c::reset() { +#if TARGET_PC + mbReset = true; +#else mModelCount = 0; +#endif } void dMirror_packet_c::calcMinMax() { @@ -76,6 +80,13 @@ void dMirror_packet_c::calcMinMax() { } int dMirror_packet_c::entryModel(J3DModel* i_model) { +#if TARGET_PC + if (mbReset) { + mModelCount = 0; + mbReset = false; + } +#endif + if (mModelCount >= 0x40) { return 0; } @@ -444,12 +455,7 @@ void dMirror_packet_c::draw() { } mDoLib_clipper::resetFar(); -#if TARGET_PC - if (!dusk::frame_interp::is_sim_frame()) -#endif - { - reset(); - } + reset(); } daMirror_c::daMirror_c() { From 8b9f09bda59d73b507234f27e489adf4f87a4e53 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 19 Apr 2026 21:31:34 -0600 Subject: [PATCH 43/68] Frame interp: Fix cloud shadow flickering (#446) --- libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp | 10 +++++----- libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp | 2 ++ src/d/d_resorce.cpp | 6 +++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp index 139bba5dd6..8c895d5077 100644 --- a/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp +++ b/libs/JSystem/src/J3DGraphAnimator/J3DModel.cpp @@ -293,11 +293,6 @@ void J3DModel::calcMaterial() { material->calc(getAnmMtx(material->getJoint()->getJntNo())); } - -#if TARGET_PC - if (mModelData->needsInterpCallBack()) - dusk::frame_interp::add_interpolation_callback(&J3DModel::interp_callback, this); -#endif } void J3DModel::calcDiffTexMtx() { @@ -500,6 +495,11 @@ void J3DModel::entry() { joint->entryIn(); } } + +#if TARGET_PC + if (mModelData->needsInterpCallBack()) + dusk::frame_interp::add_interpolation_callback(&J3DModel::interp_callback, this); +#endif } void J3DModel::viewCalc() { diff --git a/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp b/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp index df6c977560..48d4731ed9 100644 --- a/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp +++ b/libs/JSystem/src/J3DGraphBase/J3DMaterial.cpp @@ -383,6 +383,8 @@ bool J3DMaterial::needsInterpCallBack() const { case J3DTexMtxMode_EnvmapBasic: case J3DTexMtxMode_EnvmapOld: case J3DTexMtxMode_Envmap: + case J3DTexMtxMode_ProjmapBasic: + case J3DTexMtxMode_Projmap: case J3DTexMtxMode_ViewProjmap: case J3DTexMtxMode_ViewProjmapBasic: return true; diff --git a/src/d/d_resorce.cpp b/src/d/d_resorce.cpp index 0bf537bfd7..73f031beae 100644 --- a/src/d/d_resorce.cpp +++ b/src/d/d_resorce.cpp @@ -296,7 +296,11 @@ J3DModelData* dRes_info_c::loaderBasicBmd(u32 i_tag, void* i_data) { addWarpMaterial(modelData); } - if (i_tag == 'BMDR' || i_tag == 'BMWR') { + // FRAME INTERP NOTE: Always create shared DL buffers so we can use J3DMaterial::diff() +#ifndef TARGET_PC + if (i_tag == 'BMDR' || i_tag == 'BMWR') +#endif + { s32 result = modelData->newSharedDisplayList(J3DMdlFlag_UseSingleDL); if (result != kJ3DError_Success) { return NULL; From 35a69e7ff185e7bc0391f8e65ea5c6d4412ecae9 Mon Sep 17 00:00:00 2001 From: Howard Luck Date: Sun, 19 Apr 2026 22:34:13 -0600 Subject: [PATCH 44/68] Frame interp pacing/camera cuts, King Bulblin reins, and crash fixes * misc fixes round 1 * kb1 fixes * fixes for encounter/ira --- include/d/actor/d_a_e_wb.h | 9 +++++ include/d/actor/d_a_obj_sw.h | 6 ++-- src/d/actor/d_a_dshutter.cpp | 15 ++++---- src/d/actor/d_a_e_wb.cpp | 62 +++++++++++++++++++++++++++++++++ src/dusk/game_clock.cpp | 3 +- src/dusk/imgui/ImGuiConsole.cpp | 3 +- 6 files changed, 84 insertions(+), 14 deletions(-) diff --git a/include/d/actor/d_a_e_wb.h b/include/d/actor/d_a_e_wb.h index dd36474025..ba680366fb 100644 --- a/include/d/actor/d_a_e_wb.h +++ b/include/d/actor/d_a_e_wb.h @@ -220,6 +220,15 @@ public: /* 0x17E2 */ s16 wait_roll_angle; ///< @brief Roll angle during wait state. /* 0x17E4 */ u8 field_0x17e4[0x17e8 - 0x17e4]; /* 0x17E8 */ f32 ride_speed_max; ///< @brief Speed rate for riding calculations. +#if TARGET_PC + cXyz himo_mat_interp_prev[2][16]; + cXyz himo_mat_interp_curr[2][16]; + cXyz himo_tex_interp_prev[2]; + cXyz himo_tex_interp_curr[2]; + bool himo_interp_prev_valid; + bool himo_interp_curr_valid; + s8 demo_cam_sync_ticks; +#endif }; STATIC_ASSERT(sizeof(e_wb_class) == 0x17EC); diff --git a/include/d/actor/d_a_obj_sw.h b/include/d/actor/d_a_obj_sw.h index 64674b0b9c..9e2793755a 100644 --- a/include/d/actor/d_a_obj_sw.h +++ b/include/d/actor/d_a_obj_sw.h @@ -68,10 +68,8 @@ public: /* 0x904 */ cXyz field_0x904[2]; /* 0x91C */ int field_0x91c; /* 0x920 */ cXyz field_0x920[63]; - /* 0xC14 */ f32 field_0xc14[4]; - /* 0xC24 */ u8 field_0xc24[0xd10 - 0xc24]; - /* 0xD10 */ s8 field_0xd10[4]; - /* 0xD14 */ u8 field_0xd14[0xd50 - 0xd14]; + /* 0xC14 */ f32 field_0xc14[63]; + /* 0xD10 */ s8 field_0xd10[64]; /* 0xD50 */ mDoExt_3DlineMat1_c field_0xd50; /* 0xD8C */ int field_0xd8c; }; diff --git a/src/d/actor/d_a_dshutter.cpp b/src/d/actor/d_a_dshutter.cpp index fc122cd4e3..264f559e07 100644 --- a/src/d/actor/d_a_dshutter.cpp +++ b/src/d/actor/d_a_dshutter.cpp @@ -215,15 +215,14 @@ int daDsh_c::create() { mType = getType(); -#ifdef TARGET_PC - const char* l_resName[] = {l_arcName[mType], ""}; -#else - // !@bug By making this static, it is only initialized the first time it runs - // If gate types that use other arcs are loaded later (without reloading the code) - // this array never gets updated and will load the incorrect arc - // On GC/Wii, REL loading causes this to reset/reinitialize so the bug is avoided - // but TPHD is all statically linked so daDsh_c::CreateHeap fails to get model data and the gate unloads + // !@bug Static-init only runs once, so slot 0 keeps the first mType's arc name forever. + // GC/Wii dodges this via REL reload; TPHD is statically linked so later gates of a + // different type load the wrong arc and CreateHeap fails. The storage must stay static + // because mResLoader.load holds the pointer past create(), so we just overwrite slot 0 + // each call instead. static const char* l_resName[] = {l_arcName[mType], ""}; +#ifdef TARGET_PC + l_resName[0] = l_arcName[mType]; #endif int phase = mResLoader.load(l_resName, NULL); diff --git a/src/d/actor/d_a_e_wb.cpp b/src/d/actor/d_a_e_wb.cpp index 819439879e..75031c42bb 100644 --- a/src/d/actor/d_a_e_wb.cpp +++ b/src/d/actor/d_a_e_wb.cpp @@ -18,6 +18,8 @@ #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include "res/Object/Always.h" +#include "dusk/dusk.h" +#include "dusk/frame_interpolation.h" #include @@ -184,6 +186,30 @@ static bool hio_set; static daE_WB_HIO_c l_HIO; +#if TARGET_PC +static void e_wb_rein_interp_callback(bool isSimFrame, void* pUserWork) { + e_wb_class* i_this = (e_wb_class*)pUserWork; + if (!i_this->himo_interp_prev_valid || !i_this->himo_interp_curr_valid) { + return; + } + const f32 alpha = dusk::frame_interp::get_interpolation_step(); + for (int r = 0; r < 2; r++) { + cXyz* dst = i_this->himo_mat[r].getPos(0); + for (int i = 0; i < 16; i++) { + const cXyz& p0 = i_this->himo_mat_interp_prev[r][i]; + const cXyz& p1 = i_this->himo_mat_interp_curr[r][i]; + dst[i] = p0 + (p1 - p0) * alpha; + } + } + cXyz* dst = i_this->himo_tex.getPos(0); + for (int i = 0; i < 2; i++) { + const cXyz& p0 = i_this->himo_tex_interp_prev[i]; + const cXyz& p1 = i_this->himo_tex_interp_curr[i]; + dst[i] = p0 + (p1 - p0) * alpha; + } +} +#endif + static void himo_control1(e_wb_class* i_this, cXyz* i_pos, int i_no, s8 param_3) { fopEn_enemy_c* enemy = &i_this->enemy; cXyz mae, ato; @@ -508,6 +534,21 @@ static int daE_WB_Draw(e_wb_class* i_this) { dComIfGd_set3DlineMat(&i_this->himo_mat[1]); 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 (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)); + i_this->himo_interp_prev_valid = true; + } + for (int r = 0; r < 2; r++) { + memcpy(i_this->himo_mat_interp_curr[r], i_this->himo_mat[r].getPos(0), 16 * sizeof(cXyz)); + } + memcpy(i_this->himo_tex_interp_curr, i_this->himo_tex.getPos(0), 2 * sizeof(cXyz)); + i_this->himo_interp_curr_valid = true; + dusk::frame_interp::add_interpolation_callback(&e_wb_rein_interp_callback, i_this); + } +#endif } return 1; @@ -3726,6 +3767,9 @@ static void demo_camera(e_wb_class* i_this) { boss = (e_rdb_class*)fopAcM_SearchByName(fpcNm_E_RDB_e); } cXyz mae, ato, eye, center; +#if TARGET_PC + const s16 entry_demo_mode = i_this->demo_mode; +#endif switch (i_this->demo_mode) { case 1: { @@ -4255,6 +4299,9 @@ static void demo_camera(e_wb_class* i_this) { if (i_this->demo_timer == 325) { fpcM_Search(s_wbZrevise_sub, i_this); +#if TARGET_PC + i_this->demo_cam_sync_ticks = 2; +#endif } if (i_this->demo_timer == 335) { @@ -4495,6 +4542,9 @@ static void demo_camera(e_wb_class* i_this) { i_this->demo_cam_way_spd.z = fabsf(i_this->demo_cam_way.z - i_this->demo_cam_ctr.z); i_this->demo_cam_morf = 0; pla->setPlayerPosAndAngle(&pla->current.pos, pla->shape_angle.y - 4000, 0); +#if TARGET_PC + dusk::frame_interp::request_presentation_sync(); +#endif } if (i_this->demo_timer == 345) { daPy_getPlayerActorClass()->setThrowDamage(boss->enemy.shape_angle.y - 8000 + TREG_S(8), @@ -4741,6 +4791,9 @@ static void demo_camera(e_wb_class* i_this) { i_this->demo_cam_eye.x += 300.0f + VREG_F(8); i_this->demo_cam_eye.y += 150.0f + VREG_F(9); i_this->demo_cam_eye.z -= 1400.0f + VREG_F(10); +#if TARGET_PC + dusk::frame_interp::request_presentation_sync(); +#endif } } else { i_this->demo_cam_eye = enemy->current.pos; @@ -4996,6 +5049,15 @@ static void demo_camera(e_wb_class* i_this) { } } } +#if TARGET_PC + if (entry_demo_mode != i_this->demo_mode) { + i_this->demo_cam_sync_ticks = 2; + } + if (i_this->demo_cam_sync_ticks > 0) { + dusk::frame_interp::request_presentation_sync(); + i_this->demo_cam_sync_ticks--; + } +#endif } static void anm_se_eff_set(e_wb_class* i_this) { diff --git a/src/dusk/game_clock.cpp b/src/dusk/game_clock.cpp index 8eedf44da3..a262d0283c 100644 --- a/src/dusk/game_clock.cpp +++ b/src/dusk/game_clock.cpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace dusk { @@ -26,7 +27,7 @@ void ensure_initialized() { void reset_accumulator() { ensure_initialized(); - s_sim_accumulator = 0.0f; + s_sim_accumulator = fmodf(s_sim_accumulator, sim_pace()); } void reset_frame_timer() { diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 2779b9cd9f..f5e25277c7 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -17,6 +17,7 @@ #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" #include "dusk/dusk.h" +#include "dusk/frame_interpolation.h" #include "dusk/main.h" #include "dusk/settings.h" #include "m_Do/m_Do_controller_pad.h" @@ -281,7 +282,7 @@ namespace dusk { void ImGuiConsole::UpdateSettings() { getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind && ImGui::IsKeyDown(ImGuiKey_Tab); - if (mDoMain::developmentMode == 1 && (mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == (PAD_TRIGGER_R | PAD_TRIGGER_L) && mDoCPd_c::getTrigY(PAD_1)) { + if (dusk::frame_interp::get_ui_tick_pending() && mDoMain::developmentMode == 1 && (mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == (PAD_TRIGGER_R | PAD_TRIGGER_L) && mDoCPd_c::getTrigY(PAD_1)) { getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive; } if (mDoMain::developmentMode != 1) { From e63897d1f7281f478f7fda35c21698413e3f335b Mon Sep 17 00:00:00 2001 From: madeline Date: Sun, 19 Apr 2026 21:43:29 -0700 Subject: [PATCH 45/68] add --console to launch.json --- .vscode/launch.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 4cc435870a..7090699dd9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "cppvsdbg", "request": "launch", "program": "${command:cmake.launchTargetPath}", - "args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso"], + "args": ["-l", "1", "--dvd", "${workspaceRoot}/orig/GZ2E01/GZ2E01.iso", "--console"], "MIMode": "gdb", "miDebuggerPath": "gdb", "symbolSearchPath": "${command:cmake.launchTargetPath}", From d85556a063b8af88424420e2364d9460c931ae40 Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Sun, 19 Apr 2026 22:27:29 -0700 Subject: [PATCH 46/68] Use vendored SDL on ubuntu for libusb (#448) --- CMakePresets.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index 89c78818f9..92799249a9 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -358,7 +358,10 @@ "inherits": [ "relwithdebinfo", "ci" - ] + ], + "cacheVariables": { + "AURORA_SDL3_PROVIDER": "vendor" + } }, { "name": "x-linux-ci-gcc", From e3761784af86f918c94a074bcef6e47124a1b6de Mon Sep 17 00:00:00 2001 From: madeline Date: Mon, 20 Apr 2026 00:21:09 -0700 Subject: [PATCH 47/68] rich presence --- CMakeLists.txt | 42 ++++++++++ files.cmake | 1 + include/dusk/discord_presence.hpp | 18 +++++ src/dusk/discord_presence.cpp | 122 ++++++++++++++++++++++++++++++ src/m_Do/m_Do_main.cpp | 16 ++++ 5 files changed, 199 insertions(+) create mode 100644 include/dusk/discord_presence.hpp create mode 100644 src/dusk/discord_presence.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c99133738b..37340576cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -316,6 +316,48 @@ if (DUSK_MOVIE_SUPPORT) list(APPEND GAME_COMPILE_DEFS MOVIE_SUPPORT=1) endif () +option(DUSK_ENABLE_DISCORD_RPC "Enable Discord Rich Presence support" ON) +if (DUSK_ENABLE_DISCORD_RPC AND NOT ANDROID AND NOT IOS AND NOT TVOS) + + FetchContent_Populate(discord_rpc + URL https://github.com/discord/discord-rpc/archive/refs/tags/v3.4.0.tar.gz + URL_HASH SHA256=e13427019027acd187352dacba6c65953af66fdf3c35fcf38fc40b454a9d7855 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + ) + # RapidJSON is a git submodule absent from the discord-rpc tarball; fetch separately. + FetchContent_Populate(rapidjson + URL https://github.com/Tencent/rapidjson/archive/refs/tags/v1.1.0.tar.gz + URL_HASH SHA256=bf7ced29704a1e696fbccf2a2b4ea068e7774fa37f6d7dd4039d0787f8bed98e + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + ) + + if (NOT TARGET discord-rpc) + set(_drpc ${discord_rpc_SOURCE_DIR}/src) + set(_drpc_src + ${_drpc}/discord_rpc.cpp + ${_drpc}/rpc_connection.cpp + ${_drpc}/serialization.cpp + ) + if (WIN32) + list(APPEND _drpc_src ${_drpc}/connection_win.cpp ${_drpc}/discord_register_win.cpp) + elseif (APPLE) + list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_osx.cpp) + else () + list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_linux.cpp) + endif () + add_library(discord-rpc STATIC ${_drpc_src}) + target_include_directories(discord-rpc PUBLIC + ${discord_rpc_SOURCE_DIR}/include + ${rapidjson_SOURCE_DIR}/include + ) + if (UNIX) + target_link_libraries(discord-rpc PUBLIC pthread) + endif () + endif () + list(APPEND GAME_LIBS discord-rpc) + list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD_RPC=1) +endif () + # Edit & Continue if (MSVC) if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug") diff --git a/files.cmake b/files.cmake index d43e98bf42..e929ec85b0 100644 --- a/files.cmake +++ b/files.cmake @@ -1387,4 +1387,5 @@ set(DUSK_FILES src/dusk/OSContext.cpp src/dusk/OSThread.cpp src/dusk/OSMutex.cpp + src/dusk/discord_presence.cpp ) diff --git a/include/dusk/discord_presence.hpp b/include/dusk/discord_presence.hpp new file mode 100644 index 0000000000..899b2ad5f9 --- /dev/null +++ b/include/dusk/discord_presence.hpp @@ -0,0 +1,18 @@ +#pragma once + +#ifdef DUSK_DISCORD_RPC + +namespace dusk { +namespace discord { + +void Initialize(); + +void RunCallbacks(); + +void UpdatePresence(); + +void Shutdown(); +} +} + +#endif // DUSK_DISCORD_RPC diff --git a/src/dusk/discord_presence.cpp b/src/dusk/discord_presence.cpp new file mode 100644 index 0000000000..3b12075c8b --- /dev/null +++ b/src/dusk/discord_presence.cpp @@ -0,0 +1,122 @@ +#ifdef DUSK_DISCORD_RPC + +#include "dusk/discord_presence.hpp" +#include "dusk/logging.h" +#include "dusk/main.h" +#include "dusk/map_loader_definitions.h" +#include "d/d_com_inf_game.h" +#include "discord_rpc.h" +#include "fmt/format.h" + +#include +#include +#include + +namespace dusk { +namespace discord { + +static int64_t g_startTime = 0; +static bool g_initialized = false; +static const char* APPLICATION_ID = "1495632471994405035"; + +static void OnReady(const DiscordUser* user) { + DuskLog.info("Discord: Connected as {}", user->username); +} + +static void OnDisconnected(int errorCode, const char* message) { + DuskLog.warn("Discord: Disconnected ({}: {})", errorCode, message); +} + +static void OnError(int errorCode, const char* message) { + DuskLog.warn("Discord: Error ({}: {})", errorCode, message); +} + +static const char* LookupMapName(const char* mapFile) { + if (!mapFile || mapFile[0] == '\0') return nullptr; + for (const auto& region : gameRegions) { + for (const auto& map : region.maps) { + if (map.mapFile && strcmp(mapFile, map.mapFile) == 0) { + return map.mapName; + } + } + } + return nullptr; +} + +void Initialize() { + g_startTime = static_cast( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + ).count() + ); + + DiscordEventHandlers handlers{}; + handlers.ready = OnReady; + handlers.disconnected = OnDisconnected; + handlers.errored = OnError; + Discord_Initialize(APPLICATION_ID, &handlers, 0, nullptr); + g_initialized = true; + + DuskLog.info("Discord Rich Presence initialized"); +} + +void RunCallbacks() { + if (!g_initialized) return; + Discord_RunCallbacks(); +} + +void UpdatePresence() { + if (!g_initialized) return; + + static auto lastUpdate = std::chrono::steady_clock::time_point{}; + const auto now = std::chrono::steady_clock::now(); + if (now - lastUpdate < std::chrono::seconds(15)) return; + lastUpdate = now; + + static std::string detailsBuf; + static std::string stateBuf; + + DiscordRichPresence presence{}; + presence.startTimestamp = g_startTime; + presence.largeImageKey = "icon"; + presence.largeImageText = "Dusk"; + + if (dusk::IsGameLaunched) { + const char* stageName = dComIfGp_getLastPlayStageName(); + + // stageName is empty until a room is actually entered + if (stageName[0] != '\0') { + const char* locationName = LookupMapName(stageName); + + if (locationName) { + detailsBuf = locationName; + } + else { + detailsBuf = "Twilight Princess"; + } + + presence.details = detailsBuf.c_str(); + + stateBuf = fmt::format(FMT_STRING("{}/{} \u2665 | {} Rupees"), + dComIfGs_getLife() / 4, dComIfGs_getMaxLife() / 5, dComIfGs_getRupee()); + + presence.state = stateBuf.c_str(); + } + } + + Discord_UpdatePresence(&presence); + DuskLog.debug("Discord Rich Presence sent"); +} + +void Shutdown() { + if (!g_initialized) return; + Discord_ClearPresence(); + Discord_Shutdown(); + g_initialized = false; + DuskLog.info("Discord Rich Presence shut down"); +} + +} // namespace discord +} // namespace dusk + +#endif // DUSK_DISCORD_RPC diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index cde7014411..c597ad2804 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -70,6 +70,7 @@ #include "dusk/config.hpp" #include "dusk/settings.h" #include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/discord_presence.hpp" #include "tracy/Tracy.hpp" #include "f_pc/f_pc_draw.h" @@ -289,6 +290,11 @@ void main01(void) { aurora_end_frame(); FrameMark; + +#ifdef DUSK_DISCORD_RPC + dusk::discord::RunCallbacks(); + dusk::discord::UpdatePresence(); +#endif } while (dusk::IsRunning); exit:; @@ -531,6 +537,10 @@ int game_main(int argc, char* argv[]) { auroraInfo = aurora_initialize(argc, argv, &config); +#ifdef DUSK_DISCORD_RPC + dusk::discord::Initialize(); +#endif + VISetWindowTitle( fmt::format("Dusk {} [{}]", DUSK_WC_DESCRIBE, dusk::backend_name(auroraInfo.backend)) .c_str()); @@ -564,6 +574,9 @@ int game_main(int argc, char* argv[]) { // pre game launch ui main loop if (!launchUILoop()) { dusk::ShutdownCrashReporting(); +#ifdef DUSK_DISCORD_RPC + dusk::discord::Shutdown(); +#endif aurora_shutdown(); return 0; } @@ -611,6 +624,9 @@ int game_main(int argc, char* argv[]) { // Notifies all CVs and causes threads to exit OSResetSystem(OS_RESET_SHUTDOWN, 0, 0); +#ifdef DUSK_DISCORD_RPC + dusk::discord::Shutdown(); +#endif aurora_shutdown(); return 0; From 10d9a818c2f54b12ae384874041beb46a389366e Mon Sep 17 00:00:00 2001 From: madeline Date: Mon, 20 Apr 2026 00:31:41 -0700 Subject: [PATCH 48/68] fix apple ci? --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 37340576cb..e8132a98fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -341,7 +341,7 @@ if (DUSK_ENABLE_DISCORD_RPC AND NOT ANDROID AND NOT IOS AND NOT TVOS) if (WIN32) list(APPEND _drpc_src ${_drpc}/connection_win.cpp ${_drpc}/discord_register_win.cpp) elseif (APPLE) - list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_osx.cpp) + list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_osx.m) else () list(APPEND _drpc_src ${_drpc}/connection_unix.cpp ${_drpc}/discord_register_linux.cpp) endif () From 6ca6ea806534d46248854ba83bde82bd88f66607 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Mon, 20 Apr 2026 04:16:35 -0700 Subject: [PATCH 49/68] fix crawl arrow direction with mirror mode --- src/d/actor/d_a_alink_crawl.inc | 38 ++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/d/actor/d_a_alink_crawl.inc b/src/d/actor/d_a_alink_crawl.inc index 4a657c1de8..2dbf88d3e1 100644 --- a/src/d/actor/d_a_alink_crawl.inc +++ b/src/d/actor/d_a_alink_crawl.inc @@ -37,18 +37,40 @@ void daAlink_c::setCrawlMoveDirectionArrow() { } if (field_0x3198 & 4) { - if (!bvar) { - direction |= data_80452F38; - } else { - direction |= data_80452F39; + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + if (!bvar) { + direction |= data_80452F39; + } else { + direction |= data_80452F38; + } + } else + #endif + { + if (!bvar) { + direction |= data_80452F38; + } else { + direction |= data_80452F39; + } } } if (field_0x3198 & 8) { - if (!bvar) { - direction |= data_80452F39; - } else { - direction |= data_80452F38; + #if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + if (!bvar) { + direction |= data_80452F38; + } else { + direction |= data_80452F39; + } + } else + #endif + { + if (!bvar) { + direction |= data_80452F39; + } else { + direction |= data_80452F38; + } } } From c7b609945b3f094544e487744a5feb6dc97664c4 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Mon, 20 Apr 2026 04:48:58 -0700 Subject: [PATCH 50/68] fix option menu widescreen regression --- src/d/d_menu_option.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/d/d_menu_option.cpp b/src/d/d_menu_option.cpp index 0898ff29af..af322a1c01 100644 --- a/src/d/d_menu_option.cpp +++ b/src/d/d_menu_option.cpp @@ -555,11 +555,23 @@ void dMenu_Option_c::_draw() { #endif mpBlackTex->setAlpha(0xff); + +#if TARGET_PC + mpBlackTex->draw(mDoGph_gInf_c::getMinXF(), mDoGph_gInf_c::getMinYF(), mDoGph_gInf_c::getWidthF(), mDoGph_gInf_c::getHeightF(), 0, 0, 0); +#else mpBlackTex->draw(0.0f, 0.0f, FB_WIDTH, FB_HEIGHT, 0, 0, 0); +#endif + mpBackScreen->draw(0.0f, 0.0f, ctx); f32 alpha = (f32)g_drawHIO.mOptionScreen.mBackgroundAlpha * (f32)field_0x374; mpBlackTex->setAlpha(alpha); + +#if TARGET_PC + mpBlackTex->draw(mDoGph_gInf_c::getMinXF(), mDoGph_gInf_c::getMinYF(), mDoGph_gInf_c::getWidthF(), mDoGph_gInf_c::getHeightF(), 0, 0, 0); +#else mpBlackTex->draw(0.0f, 0.0f, FB_WIDTH, FB_HEIGHT, 0, 0, 0); +#endif + mpScreen->draw(0.0f, 0.0f, ctx); mpClipScreen->draw(0.0f, 0.0f, ctx); #if TARGET_PC From f76a4d70876d67721c1e231561f4ef2074c4cf7c Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Mon, 20 Apr 2026 05:15:02 -0700 Subject: [PATCH 51/68] add dMsgUnit_c::setTag bug fix --- src/d/d_msg_unit.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/d/d_msg_unit.cpp b/src/d/d_msg_unit.cpp index 98dd33b4a8..f773abd373 100644 --- a/src/d/d_msg_unit.cpp +++ b/src/d/d_msg_unit.cpp @@ -271,6 +271,30 @@ void dMsgUnit_c::setTag(int i_type, int i_value, char* o_buffer, bool param_4) { u32 filesize = pHeader->size; u8* pSection = ((u8*)pHeader) + filepos; + #if TARGET_PC + // patch bug in the original game where filepos would be incremented by the next section's size rather than the current section size. + // in certain scenarios this would read past the end of the file, incrementing filepos by 0 in an infinite loop + while (filepos < filesize) { + switch (((bmg_section_t*)pSection)->magic) { + case 'FLW1': + break; + case 'FLI1': + break; + case 'INF1': + pInfoBlock = (bmg_section_t*)pSection; + break; + case 'DAT1': + pMsgDataBlock = pSection; + break; + case 'STR1': + pStrAttributeBlock = (str1_section_t*)pSection; + break; + } + + filepos += ((bmg_section_t*)pSection)->size; + pSection += ((bmg_section_t*)pSection)->size; + } + #else for (; filepos < filesize; filepos += ((bmg_section_t*)pSection)->size) { switch (((bmg_section_t*)pSection)->magic) { case 'FLW1': @@ -289,6 +313,7 @@ void dMsgUnit_c::setTag(int i_type, int i_value, char* o_buffer, bool param_4) { } pSection += ((bmg_section_t*)pSection)->size; } + #endif // This section is weird. The debug seems like entriesStr is outside the condition // but the normal build doesn't really work with that. Same for pInfoBlock->entries. From 91248d10dbf1ce0e73545a4df8ae9c0518e3d6ab Mon Sep 17 00:00:00 2001 From: madeline Date: Mon, 20 Apr 2026 05:36:31 -0700 Subject: [PATCH 52/68] disable water refraction in debug, fixes #422 --- include/d/d_com_inf_game.h | 4 ++-- include/dusk/settings.h | 2 +- src/dusk/imgui/ImGuiMenuGame.cpp | 2 -- src/dusk/imgui/ImGuiMenuTools.cpp | 10 ++++++++++ src/dusk/settings.cpp | 4 ++-- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/include/d/d_com_inf_game.h b/include/d/d_com_inf_game.h index 2978eeef1a..964cc34c0f 100644 --- a/include/d/d_com_inf_game.h +++ b/include/d/d_com_inf_game.h @@ -4834,7 +4834,7 @@ inline void dComIfGd_drawXluListDark() { inline void dComIfGd_drawXluListInvisible() { ZoneScoped; #ifdef TARGET_PC - if (dusk::getSettings().game.enableWaterRefraction) { + if (!dusk::getSettings().game.disableWaterRefraction) { #endif g_dComIfG_gameInfo.drawlist.drawXluListInvisible(); #ifdef TARGET_PC @@ -4845,7 +4845,7 @@ inline void dComIfGd_drawXluListInvisible() { inline void dComIfGd_drawOpaListInvisible() { ZoneScoped; #ifdef TARGET_PC - if (dusk::getSettings().game.enableWaterRefraction) { + if (!dusk::getSettings().game.disableWaterRefraction) { #endif g_dComIfG_gameInfo.drawlist.drawOpaListInvisible(); #ifdef TARGET_PC diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 753649df2e..03749967c9 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -73,7 +73,7 @@ struct UserSettings { // Graphics ConfigVar bloomMode; ConfigVar bloomMultiplier; - ConfigVar enableWaterRefraction; + ConfigVar disableWaterRefraction; ConfigVar enableFrameInterpolation; ConfigVar internalResolutionScale; ConfigVar shadowResolutionMultiplier; diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index d1ff216b3c..c6676df774 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -143,8 +143,6 @@ namespace dusk { } if (bloomOff) ImGui::EndDisabled(); - config::ImGuiCheckbox("Enable Water Refraction", getSettings().game.enableWaterRefraction); - ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); ImGui::EndMenu(); diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 9bb7670e88..1f9d42fbc9 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -2,6 +2,7 @@ #include "imgui.h" #include "aurora/gfx.h" +#include "ImGuiConfig.hpp" #include "dusk/hotkeys.h" #include "dusk/settings.h" #include "ImGuiConsole.hpp" @@ -28,6 +29,15 @@ namespace dusk { ImGui::Separator(); auto& collisionView = getTransientSettings().collisionView; + if (ImGui::BeginMenu("Graphics Settings")) { + bool disableWaterRefraction = getSettings().game.disableWaterRefraction; + if (ImGui::Checkbox("Disable Water Refraction", &disableWaterRefraction)) { + getSettings().game.disableWaterRefraction.setValue(disableWaterRefraction); + config::Save(); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Collision View")) { ImGui::Checkbox("Enable Terrain view", &collisionView.enableTerrainView); ImGui::Checkbox("Enable wireframe view", &collisionView.enableWireframe); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 441ae4a1c3..2bcdbe2185 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -47,7 +47,7 @@ UserSettings g_userSettings = { // Graphics .bloomMode {"game.bloomMode", BloomMode::Classic}, .bloomMultiplier {"game.bloomMultiplier", 1.0f}, - .enableWaterRefraction {"game.enableWaterRefraction", true}, + .disableWaterRefraction {"game.disableWaterRefraction", false}, .enableFrameInterpolation = {"game.enableFrameInterpolation", false}, .internalResolutionScale {"game.internalResolutionScale", 0}, .shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1}, @@ -140,7 +140,7 @@ void registerSettings() { Register(g_userSettings.game.pauseOnFocusLost); Register(g_userSettings.game.bloomMode); Register(g_userSettings.game.bloomMultiplier); - Register(g_userSettings.game.enableWaterRefraction); + Register(g_userSettings.game.disableWaterRefraction); Register(g_userSettings.game.internalResolutionScale); Register(g_userSettings.game.shadowResolutionMultiplier); Register(g_userSettings.game.enableFastIronBoots); From 65b0ec3f90b6a669b2d650b5da3ff38a207f2cfd Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Mon, 20 Apr 2026 06:02:03 -0700 Subject: [PATCH 53/68] add failsafes to lv5key to avoid reverse unlock softlock --- src/d/actor/d_a_obj_Lv5Key.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/d/actor/d_a_obj_Lv5Key.cpp b/src/d/actor/d_a_obj_Lv5Key.cpp index 9c3b5c0e4c..d213517d11 100644 --- a/src/d/actor/d_a_obj_Lv5Key.cpp +++ b/src/d/actor/d_a_obj_Lv5Key.cpp @@ -170,7 +170,7 @@ void daObjLv5Key_c::Fall(int param_0) { OS_REPORT("FALL SPD = %f\n", speed.y); - if (mAcch.ChkGroundHit()) { + if (mAcch.ChkGroundHit() IF_DUSK(|| current.pos.y < 1000.0f)) { fopAcM_GetSpeed(this); fopAcM_SetSpeedF(this, 4.0f); fopAcM_SetSpeed(this, 0.0f, 22.0f, 0.0f); @@ -192,7 +192,7 @@ void daObjLv5Key_c::Fall(int param_0) { mAcch.CrrPos(dComIfG_Bgsp()); current.pos.y = prev_y; - if (mAcch.ChkGroundHit()) { + if (mAcch.ChkGroundHit() IF_DUSK(|| current.pos.y < 1000.0f)) { setAction(&daObjLv5Key_c::Land, 1); } } From b43a9e2cccc6f2f9d336b9f08c3329a3971f3fcb Mon Sep 17 00:00:00 2001 From: madeline Date: Mon, 20 Apr 2026 06:10:18 -0700 Subject: [PATCH 54/68] menu cursor resizing fixes #313 closes #314 --- src/d/d_file_select.cpp | 24 ------------------------ src/d/d_menu_collect.cpp | 12 ------------ src/d/d_menu_save.cpp | 12 ------------ src/d/d_name.cpp | 6 +----- src/d/d_select_cursor.cpp | 9 ++++++++- 5 files changed, 9 insertions(+), 54 deletions(-) diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 805c37a418..1a61fca564 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -2102,11 +2102,7 @@ void dFile_select_c::yesnoCursorShow() { mSelIcon->setPos(pos.x, pos.y, mYnSelPane[field_0x0268]->getPanePtr(), true); mSelIcon->setAlphaRate(1.0f); - #if TARGET_PC - mSelIcon->setParam(0.96f * mDoGph_gInf_c::hudAspectScaleUp, 0.84f, 0.06f, 0.5f, 0.5f); - #else mSelIcon->setParam(0.96f, 0.84f, 0.06f, 0.5f, 0.5f); - #endif } } @@ -2259,11 +2255,7 @@ void dFile_select_c::YesNoCancelMove() { m3mSelPane[mSelectMenuNum]->getPanePtr(), true); mSelIcon->setAlphaRate(1.0f); - #if TARGET_PC - mSelIcon->setParam(0.96f * mDoGph_gInf_c::hudAspectScaleUp, 0.84f, 0.06f, 0.5f, 0.5f); - #else mSelIcon->setParam(0.96f, 0.84f, 0.06f, 0.5f, 0.5f); - #endif #if PLATFORM_WII || PLATFORM_SHIELD field_0x4333 = mSelectMenuNum; @@ -3147,11 +3139,7 @@ void dFile_select_c::screenSet() { mSelIcon = JKR_NEW dSelect_cursor_c(0, 1.0f, NULL); JUT_ASSERT(5209, mSelIcon != NULL); - #if TARGET_PC - mSelIcon->setParam(0.96f * mDoGph_gInf_c::hudAspectScaleUp, 0.94f, 0.03f, 0.7f, 0.7f); - #else mSelIcon->setParam(0.96f, 0.94f, 0.03f, 0.7f, 0.7f); - #endif Vec vtxCenter; vtxCenter = mSelFilePanes[mSelectNum]->getGlobalVtxCenter(false, 0); @@ -3287,11 +3275,7 @@ void dFile_select_c::screenSetCopySel() { mSelIcon2 = JKR_NEW dSelect_cursor_c(0, 1.0f, NULL); JUT_ASSERT(5406, mSelIcon2 != NULL); - #if TARGET_PC - mSelIcon2->setParam(0.96f * mDoGph_gInf_c::hudAspectScaleUp, 0.94f, 0.03f, 0.7f, 0.7f); - #else mSelIcon2->setParam(0.96f, 0.94f, 0.03f, 0.7f, 0.7f); - #endif Vec center = mCpSelPane[0]->getGlobalVtxCenter(false, 0); mSelIcon2->setPos(center.x, center.y, mCpSelPane[0]->getPanePtr(), true); @@ -3683,11 +3667,7 @@ void dFile_select_c::selFileCursorShow() { mSelIcon->setPos(local_1c.x, local_1c.y, mSelFilePanes[mSelectNum]->getPanePtr(), true); mSelIcon->setAlphaRate(1.0f); - #if TARGET_PC - mSelIcon->setParam(0.96f * mDoGph_gInf_c::hudAspectScaleUp, 0.94f, 0.03f, 0.7f, 0.7f); - #else mSelIcon->setParam(0.96f, 0.94f, 0.03f, 0.7f, 0.7f); - #endif } void dFile_select_c::menuWakuAlpahAnmInit(u8 i_idx, u8 param_1, u8 param_2, u8 param_3) { @@ -3730,11 +3710,7 @@ void dFile_select_c::menuCursorShow() { mSelIcon->setPos(local_24.x, local_24.y, m3mSelPane[mSelectMenuNum]->getPanePtr(), true); mSelIcon->setAlphaRate(1.0f); - #if TARGET_PC - mSelIcon->setParam(0.96f * mDoGph_gInf_c::hudAspectScaleUp, 0.84f, 0.06f, 0.5f, 0.5f); - #else mSelIcon->setParam(0.96f, 0.84f, 0.06f, 0.5f, 0.5f); - #endif } } diff --git a/src/d/d_menu_collect.cpp b/src/d/d_menu_collect.cpp index bb015f01b2..8faf5d5eb6 100644 --- a/src/d/d_menu_collect.cpp +++ b/src/d/d_menu_collect.cpp @@ -1100,23 +1100,11 @@ void dMenu_Collect2D_c::cursorPosSet() { Vec pos = mpSelPm[mCursorX][mCursorY]->getGlobalVtxCenter(false, 0); mpDrawCursor->setPos(pos.x, pos.y, mpSelPm[mCursorX][mCursorY]->getPanePtr(), false); if (mCursorY == 5) { - #if TARGET_PC - mpDrawCursor->setParam(1.1f * mDoGph_gInf_c::hudAspectScaleUp, 0.85f, 0.05f, 0.5f, 0.5f); - #else mpDrawCursor->setParam(1.1f, 0.85f, 0.05f, 0.5f, 0.5f); - #endif } else if (mCursorX == 6 && mCursorY == 0) { - #if TARGET_PC - mpDrawCursor->setParam(0.6f * mDoGph_gInf_c::hudAspectScaleUp, 0.85f, 0.03f, 0.6f, 0.6f); - #else mpDrawCursor->setParam(0.6f, 0.85f, 0.03f, 0.6f, 0.6f); - #endif } else { - #if TARGET_PC - mpDrawCursor->setParam(1.0f * mDoGph_gInf_c::hudAspectScaleUp, 1.0f, 0.1f, 0.7f, 0.7f); - #else mpDrawCursor->setParam(1.0f, 1.0f, 0.1f, 0.7f, 0.7f); - #endif } } diff --git a/src/d/d_menu_save.cpp b/src/d/d_menu_save.cpp index 0935eb4e52..61c28d6f91 100644 --- a/src/d/d_menu_save.cpp +++ b/src/d/d_menu_save.cpp @@ -386,11 +386,7 @@ void dMenu_save_c::screenSet() { mSelectedFile = dComIfGs_getDataNum(); mSelIcon = JKR_NEW dSelect_cursor_c(0, 1.0f, NULL); - #if TARGET_PC - mSelIcon->setParam(0.96f * mDoGph_gInf_c::hudAspectScaleUp, 0.94f, 0.03f, 0.7f, 0.7f); - #else mSelIcon->setParam(0.96f, 0.94f, 0.03f, 0.7f, 0.7f); - #endif Vec pos; pos = mpSelData[mSelectedFile]->getGlobalVtxCenter(false, 0); @@ -2523,11 +2519,7 @@ void dMenu_save_c::yesnoCursorShow() { mSelIcon->setPos(pos.x, pos.y, mpNoYes[mYesNoCursor]->getPanePtr(), true); mSelIcon->setAlphaRate(1.0f); - #if TARGET_PC - mSelIcon->setParam(0.96f * mDoGph_gInf_c::hudAspectScaleUp, 0.84f, 0.06f, 0.5f, 0.5f); - #else mSelIcon->setParam(0.96f, 0.84f, 0.06f, 0.5f, 0.5f); - #endif } } @@ -2676,11 +2668,7 @@ void dMenu_save_c::selFileCursorShow() { mSelIcon->setPos(pos.x, pos.y, mpSelData[mSelectedFile]->getPanePtr(), true); mSelIcon->setAlphaRate(1.0f); - #if TARGET_PC - mSelIcon->setParam(0.96f * mDoGph_gInf_c::hudAspectScaleUp, 0.94f, 0.03f, 0.7f, 0.7f); - #else mSelIcon->setParam(0.96f, 0.94f, 0.03f, 0.7f, 0.7f); - #endif } void dMenu_save_c::yesnoWakuAlpahAnmInit(u8 yesnoIdx, u8 startAlpha, u8 endAlpha, u8 anmTimer) { diff --git a/src/d/d_name.cpp b/src/d/d_name.cpp index 120bcff752..ea8624de57 100644 --- a/src/d/d_name.cpp +++ b/src/d/d_name.cpp @@ -1288,7 +1288,7 @@ void dName_c::selectCursorPosSet(int row) { #if TARGET_PC void dName_c::nameWide() { //Resize Select Icon - mSelIcon->setParam(0.82f * mDoGph_gInf_c::hudAspectScaleUp, 0.77f, 0.05f, 0.4f, 0.4f); + mSelIcon->setParam(0.82f, 0.77f, 0.05f, 0.4f, 0.4f); // List of Characters Box static u64 l_tagName[65] = { @@ -1540,11 +1540,7 @@ void dName_c::screenSet() { mSelIcon = JKR_NEW dSelect_cursor_c(0, 1.0f, NULL); JUT_ASSERT(0, mSelIcon != NULL); - #if TARGET_PC - mSelIcon->setParam(0.82f * mDoGph_gInf_c::hudAspectScaleUp, 0.77f, 0.05f, 0.4f, 0.4f); - #else mSelIcon->setParam(0.82f, 0.77f, 0.05f, 0.4f, 0.4f); - #endif Vec pos = mMojiIcon[mCharRow + mCharColumn * 5]->getGlobalVtxCenter(false, 0); mSelIcon->setPos(pos.x, pos.y, mMojiIcon[mCharRow + mCharColumn * 5]->getPanePtr(), true); diff --git a/src/d/d_select_cursor.cpp b/src/d/d_select_cursor.cpp index 545b54c9a3..6cd2c5993a 100644 --- a/src/d/d_select_cursor.cpp +++ b/src/d/d_select_cursor.cpp @@ -259,6 +259,13 @@ void dSelect_cursor_c::update() { if (field_0xb6 == 3) { fVar1 = 0.5f; } +#ifdef TARGET_PC + if (mpPane) { + Vec pos = mpPaneMgr->getGlobalVtxCenter(mpPane, false, 0); + mPositionX = pos.x; + mPositionY = pos.y; + } +#endif mpPaneMgr->translate(mPositionX, mPositionY); if (mpCursorHIO->mDebugON) { mParam1 = mpCursorHIO->mXAxisExpansion; @@ -544,7 +551,7 @@ void dSelect_cursor_c::setCursorAnimation() { } for (int i = 0; i < 4; i++) { - field_0x74[i] = mParam1 * (field_0x94[i] * ((1.0f - param3) + fVar2 * param3)); + field_0x74[i] = mParam1 * (field_0x94[i] * ((1.0f - param3) + fVar2 * param3)) IF_DUSK(* mDoGph_gInf_c::hudAspectScaleUp); field_0x84[i] = mParam2 * (field_0xa4[i] * ((1.0f - param3) + fVar2 * param3)); } From 9904720e5abbad09db0ff29fb75ec20f9635a039 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Mon, 20 Apr 2026 06:14:21 -0700 Subject: [PATCH 55/68] better lv5key fix --- src/d/actor/d_a_obj_Lv5Key.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/d/actor/d_a_obj_Lv5Key.cpp b/src/d/actor/d_a_obj_Lv5Key.cpp index d213517d11..a71ad25e33 100644 --- a/src/d/actor/d_a_obj_Lv5Key.cpp +++ b/src/d/actor/d_a_obj_Lv5Key.cpp @@ -170,7 +170,7 @@ void daObjLv5Key_c::Fall(int param_0) { OS_REPORT("FALL SPD = %f\n", speed.y); - if (mAcch.ChkGroundHit() IF_DUSK(|| current.pos.y < 1000.0f)) { + if (mAcch.ChkGroundHit() IF_DUSK(|| current.pos.abs(home.pos) > 200.0f)) { fopAcM_GetSpeed(this); fopAcM_SetSpeedF(this, 4.0f); fopAcM_SetSpeed(this, 0.0f, 22.0f, 0.0f); @@ -192,7 +192,7 @@ void daObjLv5Key_c::Fall(int param_0) { mAcch.CrrPos(dComIfG_Bgsp()); current.pos.y = prev_y; - if (mAcch.ChkGroundHit() IF_DUSK(|| current.pos.y < 1000.0f)) { + if (mAcch.ChkGroundHit() IF_DUSK(|| current.pos.abs(home.pos) > 200.0f)) { setAction(&daObjLv5Key_c::Land, 1); } } From 2ce272d5861bbd4ad88ff07662140f453154e194 Mon Sep 17 00:00:00 2001 From: madeline Date: Mon, 20 Apr 2026 06:45:05 -0700 Subject: [PATCH 56/68] fix some menus scaling wrong --- include/d/d_select_cursor.h | 7 +++++++ src/d/d_file_select.cpp | 10 ++++++++++ src/d/d_menu_collect.cpp | 11 +++++++++++ src/d/d_menu_save.cpp | 6 ++++++ src/d/d_name.cpp | 6 +++++- src/d/d_select_cursor.cpp | 14 +++++++++++++- 6 files changed, 52 insertions(+), 2 deletions(-) diff --git a/include/d/d_select_cursor.h b/include/d/d_select_cursor.h index c77da550f8..1bd5991d16 100644 --- a/include/d/d_select_cursor.h +++ b/include/d/d_select_cursor.h @@ -47,6 +47,10 @@ public: mPositionY = y; } +#ifdef TARGET_PC + void refreshAspectScale(); +#endif + void onUpdateFlag() { mUpdateFlag = true; } void resetUpdateFlag() { mUpdateFlag = false; } @@ -79,6 +83,9 @@ private: /* 0x58 */ f32 mPositionX; /* 0x5C */ f32 mPositionY; /* 0x60 */ f32 mParam1; +#ifdef TARGET_PC + f32 mBaseParam1; +#endif /* 0x64 */ f32 mParam2; /* 0x68 */ f32 mParam3; /* 0x6C */ f32 mParam4; diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 1a61fca564..23ceb96f00 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -3812,6 +3812,16 @@ void dFile_select_c::fileSelectWide() { fileSel.Scr->search(MULTI_CHAR('w_uzu07'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); fileSel.Scr->search(MULTI_CHAR('w_uzu08'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); fileSel.Scr->search(MULTI_CHAR('w_uzu09'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + + #if TARGET_PC + if (mSelIcon) { + mSelIcon->refreshAspectScale(); + } + + if (mSelIcon2) { + mSelIcon2->refreshAspectScale(); + } + #endif } #endif diff --git a/src/d/d_menu_collect.cpp b/src/d/d_menu_collect.cpp index 8faf5d5eb6..46c9c1a9d9 100644 --- a/src/d/d_menu_collect.cpp +++ b/src/d/d_menu_collect.cpp @@ -164,11 +164,22 @@ void dMenu_Collect2D_c::menuCollectWide() { // Item Description Text mpScreen->search(MULTI_CHAR('infotxtn'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + + #if TARGET_PC + if (mpDrawCursor) { + mpDrawCursor->refreshAspectScale(); + } + #endif } #endif void dMenu_Collect2D_c::_create() { mpHeap->getTotalFreeSize(); + + #if TARGET_PC + mpDrawCursor = NULL; + #endif + mpScreen = JKR_NEW J2DScreen(); mpScreen->setPriority("zelda_collect_soubi_screen.blo", 0x1020000, dComIfGp_getCollectResArchive()); diff --git a/src/d/d_menu_save.cpp b/src/d/d_menu_save.cpp index 61c28d6f91..8dd6283dfd 100644 --- a/src/d/d_menu_save.cpp +++ b/src/d/d_menu_save.cpp @@ -2801,6 +2801,12 @@ void dMenu_save_c::menuSaveWide() { mSaveSel.Scr->search(MULTI_CHAR('w_uzu07'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); mSaveSel.Scr->search(MULTI_CHAR('w_uzu08'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); mSaveSel.Scr->search(MULTI_CHAR('w_uzu09'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); + + #if TARGET_PC + if (mSelIcon) { + mSelIcon->refreshAspectScale(); + } + #endif } #endif diff --git a/src/d/d_name.cpp b/src/d/d_name.cpp index ea8624de57..e4232eff34 100644 --- a/src/d/d_name.cpp +++ b/src/d/d_name.cpp @@ -1288,7 +1288,11 @@ void dName_c::selectCursorPosSet(int row) { #if TARGET_PC void dName_c::nameWide() { //Resize Select Icon - mSelIcon->setParam(0.82f, 0.77f, 0.05f, 0.4f, 0.4f); + #if TARGET_PC + if (mSelIcon) { + mSelIcon->refreshAspectScale(); + } + #endif // List of Characters Box static u64 l_tagName[65] = { diff --git a/src/d/d_select_cursor.cpp b/src/d/d_select_cursor.cpp index 6cd2c5993a..0773433b4d 100644 --- a/src/d/d_select_cursor.cpp +++ b/src/d/d_select_cursor.cpp @@ -69,6 +69,9 @@ dSelect_cursor_c::dSelect_cursor_c(u8 param_0, f32 param_1, JKRArchive* param_2) field_0x84[i] = 0.0f; } mParam1 = mpCursorHIO->mXAxisExpansion; +#ifdef TARGET_PC + mBaseParam1 = mParam1; +#endif mParam2 = mpCursorHIO->mYAxisExpansion; mParam3 = mpCursorHIO->mOscillation; mParam4 = mpCursorHIO->mRatioX; @@ -411,6 +414,9 @@ void dSelect_cursor_c::setPos(f32 i_posX, f32 i_posY, J2DPane* i_pane, bool i_sc void dSelect_cursor_c::setParam(f32 i_param1, f32 i_param2, f32 i_param3, f32 i_param4, f32 i_param5) { mParam1 = i_param1; +#ifdef TARGET_PC + mBaseParam1 = i_param1; +#endif mParam2 = i_param2; mParam3 = i_param3; mParam4 = i_param4; @@ -551,7 +557,7 @@ void dSelect_cursor_c::setCursorAnimation() { } for (int i = 0; i < 4; i++) { - field_0x74[i] = mParam1 * (field_0x94[i] * ((1.0f - param3) + fVar2 * param3)) IF_DUSK(* mDoGph_gInf_c::hudAspectScaleUp); + field_0x74[i] = mParam1 * (field_0x94[i] * ((1.0f - param3) + fVar2 * param3)); field_0x84[i] = mParam2 * (field_0xa4[i] * ((1.0f - param3) + fVar2 * param3)); } @@ -569,3 +575,9 @@ void dSelect_cursor_c::setBckAnimation(J2DAnmTransformKey* param_0) { void dSelect_cursor_c::moveCenter(J2DPane* i_pane, f32 i_x, f32 i_y) { i_pane->translate(i_x,i_y); } + +#ifdef TARGET_PC +void dSelect_cursor_c::refreshAspectScale() { + mParam1 = mBaseParam1 * mDoGph_gInf_c::hudAspectScaleUp; +} +#endif From c42a33154c0e5225b6bc61f421b7d18e9cf38265 Mon Sep 17 00:00:00 2001 From: Irastris Date: Mon, 20 Apr 2026 18:11:27 -0400 Subject: [PATCH 57/68] Frame Interp: Fix flickering refraction in cutscenes --- src/dusk/frame_interpolation.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index c72923da7d..e0057790df 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -408,10 +408,8 @@ void begin_presentation_camera() { } mDoLib_clipper::setup(view->fovy, view->aspect, view->near_, far_); - -#if WIDESCREEN_SUPPORT - mDoGph_gInf_c::offWideZoom(); -#endif + + // FRAME INTERP NOTE: Removed the call to offWideZoom that was here, it causes problems with presentation during cutscenes. s_presentation_depth = 1; From 89acf923e0fc6b929bd09af6c657140df187a86f Mon Sep 17 00:00:00 2001 From: PJB3005 Date: Tue, 21 Apr 2026 01:11:49 +0200 Subject: [PATCH 58/68] Fix THP shutdown crash Cleanly shut down movie player threads on exit. I'm not 100% fond of having to manually insert a call like this into the shutdown logic, but the other solution is shutting down all processes and that's likely to result in causing more crashes. --- include/d/actor/d_a_movie_player.h | 6 ++++++ src/d/actor/d_a_movie_player.cpp | 9 +++++++++ src/m_Do/m_Do_main.cpp | 6 +++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/include/d/actor/d_a_movie_player.h b/include/d/actor/d_a_movie_player.h index 0ddc675c24..0e666ae470 100644 --- a/include/d/actor/d_a_movie_player.h +++ b/include/d/actor/d_a_movie_player.h @@ -94,6 +94,12 @@ static void __THPAudioInitialize(THPAudioDecodeInfo* info, u8* ptr); #define THP_TEXTURE_SET_COUNT 3 #endif +#if TARGET_PC +namespace dusk { + void MoviePlayerShutdown(); +} +#endif + struct daMP_THPPlayer { /* 0x000 */ DVDFileInfo fileInfo; /* 0x03C */ THPHeader header; diff --git a/src/d/actor/d_a_movie_player.cpp b/src/d/actor/d_a_movie_player.cpp index d972bddd4d..079584f714 100644 --- a/src/d/actor/d_a_movie_player.cpp +++ b/src/d/actor/d_a_movie_player.cpp @@ -4580,3 +4580,12 @@ actor_process_profile_definition g_profile_MOVIE_PLAYER = { }; AUDIO_INSTANCES; + +#if TARGET_PC +void dusk::MoviePlayerShutdown() { + // We need to cleanly shut down the threads to avoid crashes on shutdown. + if (daMP_c::m_myObj) { + daMP_c::m_myObj->daMP_c_Finish(); + } +} +#endif \ No newline at end of file diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index c597ad2804..e41bd1f24c 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -66,13 +66,15 @@ #include "SDL3/SDL_filesystem.h" #include "cxxopts.hpp" +#include "d/actor/d_a_movie_player.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" -#include "dusk/settings.h" #include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/settings.h" #include "dusk/discord_presence.hpp" #include "tracy/Tracy.hpp" #include "f_pc/f_pc_draw.h" +#include "tracy/Tracy.hpp" // --- GLOBALS --- s8 mDoMain::developmentMode = -1; @@ -614,6 +616,8 @@ int game_main(int argc, char* argv[]) { main01(); + dusk::MoviePlayerShutdown(); + dusk::ShutdownCrashReporting(); dusk::ShutdownFileLogging(); fflush(stdout); From faa86181247951a5a9560066a8f6e84a4371ba24 Mon Sep 17 00:00:00 2001 From: Pheenoh Date: Mon, 20 Apr 2026 19:37:38 -0600 Subject: [PATCH 59/68] Fix frame interpolation on save and world map menus --- src/d/d_menu_fmap2D.cpp | 19 +++++++++----- src/d/d_menu_save.cpp | 58 ++++++++++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index a3afaa1641..8692156daf 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -1769,14 +1769,19 @@ void dMenu_Fmap2DBack_c::calcBlink() { t * (g_fmapHIO.mMapBlink[i + 1].mUnselectedRegion.mBlinkSpeed - g_fmapHIO.mMapBlink[i].mUnselectedRegion.mBlinkSpeed); - field_0x1218++; - if (field_0x1218 >= selected_blink_speed) { - field_0x1218 = 0; - } +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x1218++; + if (field_0x1218 >= selected_blink_speed) { + field_0x1218 = 0; + } - field_0x121a++; - if (field_0x121a >= unselected_blink_speed) { - field_0x121a = 0; + field_0x121a++; + if (field_0x121a >= unselected_blink_speed) { + field_0x121a = 0; + } } f32 t_selected = 0.0f; diff --git a/src/d/d_menu_save.cpp b/src/d/d_menu_save.cpp index 8dd6283dfd..53dbe37ca4 100644 --- a/src/d/d_menu_save.cpp +++ b/src/d/d_menu_save.cpp @@ -18,6 +18,7 @@ #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include "d/d_msg_scrn_explain.h" +#include "dusk/frame_interpolation.h" #include "dusk/settings.h" #include "JSystem/J2DGraph/J2DAnmLoader.h" #include "f_op/f_op_msg_mng.h" @@ -715,7 +716,9 @@ void dMenu_save_c::_move() { } (this->*MenuSaveProc[mMenuProc])(); +#if !TARGET_PC saveSelAnm(); +#endif if (mWarning != NULL) { mWarning->_move(); @@ -732,36 +735,46 @@ void dMenu_save_c::saveSelAnm() { } void dMenu_save_c::selFileWakuAnm() { - mFileWakuAnmFrame += 2; - if (mFileWakuAnmFrame >= mpFileWakuAnm->getFrameMax()) { - mFileWakuAnmFrame -= mpFileWakuAnm->getFrameMax(); +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + mFileWakuAnmFrame += 2; + if (mFileWakuAnmFrame >= mpFileWakuAnm->getFrameMax()) { + mFileWakuAnmFrame -= mpFileWakuAnm->getFrameMax(); + } + + mFileWakuRotAnmFrame += 2; + if (mFileWakuRotAnmFrame >= mpFileWakuRotAnm->getFrameMax()) { + mFileWakuRotAnmFrame -= mpFileWakuRotAnm->getFrameMax(); + } } mpFileWakuAnm->setFrame(mFileWakuAnmFrame); - - mFileWakuRotAnmFrame += 2; - if (mFileWakuRotAnmFrame >= mpFileWakuRotAnm->getFrameMax()) { - mFileWakuRotAnmFrame -= mpFileWakuRotAnm->getFrameMax(); - } mpFileWakuRotAnm->setFrame(mFileWakuRotAnmFrame); } void dMenu_save_c::bookIconAnm() { - field_0x154 += 2; - if (field_0x154 >= field_0x150->getFrameMax()) { - field_0x154 -= field_0x150->getFrameMax(); +#if TARGET_PC + if (dusk::frame_interp::get_ui_tick_pending()) +#endif + { + field_0x154 += 2; + if (field_0x154 >= field_0x150->getFrameMax()) { + field_0x154 -= field_0x150->getFrameMax(); + } + + field_0x15c += 2; + if (field_0x15c >= field_0x158->getFrameMax()) { + field_0x15c -= field_0x158->getFrameMax(); + } + + field_0x164 += 2; + if (field_0x164 >= field_0x160->getFrameMax()) { + field_0x164 -= field_0x160->getFrameMax(); + } } field_0x150->setFrame(field_0x154); - - field_0x15c += 2; - if (field_0x15c >= field_0x158->getFrameMax()) { - field_0x15c -= field_0x158->getFrameMax(); - } field_0x158->setFrame(field_0x15c); - - field_0x164 += 2; - if (field_0x164 >= field_0x160->getFrameMax()) { - field_0x164 -= field_0x160->getFrameMax(); - } field_0x160->setFrame(field_0x164); } @@ -2812,6 +2825,9 @@ void dMenu_save_c::menuSaveWide() { void dMenu_save_c::_draw2() { if (field_0x21a1 == 0) { +#if TARGET_PC + saveSelAnm(); +#endif if (mpScrnExplain != NULL) { dComIfGd_set2DOpa(&mMenuSaveExplain); } From 30a99c22f1daac3e19a95198af0d68e5545b9edc Mon Sep 17 00:00:00 2001 From: Luke Street Date: Mon, 20 Apr 2026 20:45:16 -0600 Subject: [PATCH 60/68] Reorganize ImGui menus (#456) * Reorganize ImGui menus * Fix crash_reporting.cpp * Update aurora --- extern/aurora | 2 +- files.cmake | 2 - include/dusk/dusk.h | 1 - include/dusk/logging.h | 4 +- include/dusk/main.h | 3 + libs/JSystem/src/JFramework/JFWDisplay.cpp | 1 + src/dusk/config.cpp | 4 +- src/dusk/crash_reporting.cpp | 3 +- src/dusk/imgui/ImGuiConsole.cpp | 5 +- src/dusk/imgui/ImGuiConsole.hpp | 2 - src/dusk/imgui/ImGuiEngine.cpp | 4 +- src/dusk/imgui/ImGuiMenuEnhancements.cpp | 248 --------- src/dusk/imgui/ImGuiMenuEnhancements.hpp | 18 - src/dusk/imgui/ImGuiMenuGame.cpp | 556 ++++++++++++++------- src/dusk/imgui/ImGuiMenuGame.hpp | 7 + src/dusk/imgui/ImGuiMenuTools.cpp | 55 +- src/dusk/logging.cpp | 6 +- src/m_Do/m_Do_main.cpp | 64 +-- 18 files changed, 496 insertions(+), 489 deletions(-) delete mode 100644 src/dusk/imgui/ImGuiMenuEnhancements.cpp delete mode 100644 src/dusk/imgui/ImGuiMenuEnhancements.hpp diff --git a/extern/aurora b/extern/aurora index b1957f10cf..5d420c9f73 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit b1957f10cf9e7ea1e0e012c1968014bd11299297 +Subproject commit 5d420c9f73c93ab9a5dcd052ac92d47362764e80 diff --git a/files.cmake b/files.cmake index e929ec85b0..c8ecb9be7a 100644 --- a/files.cmake +++ b/files.cmake @@ -1366,8 +1366,6 @@ set(DUSK_FILES src/dusk/imgui/ImGuiBloomWindow.hpp src/dusk/imgui/ImGuiMenuTools.cpp src/dusk/imgui/ImGuiMenuTools.hpp - src/dusk/imgui/ImGuiMenuEnhancements.cpp - src/dusk/imgui/ImGuiMenuEnhancements.hpp src/dusk/imgui/ImGuiPreLaunchWindow.cpp src/dusk/imgui/ImGuiPreLaunchWindow.hpp src/dusk/imgui/ImGuiFirstRunPreset.hpp diff --git a/include/dusk/dusk.h b/include/dusk/dusk.h index b751990d9c..911ddbb535 100644 --- a/include/dusk/dusk.h +++ b/include/dusk/dusk.h @@ -6,7 +6,6 @@ #include "aurora/gfx.h" extern AuroraInfo auroraInfo; -extern const char* configPath; namespace dusk { extern AuroraStats lastFrameAuroraStats; diff --git a/include/dusk/logging.h b/include/dusk/logging.h index 0a9cbf238d..9b31b96bf2 100644 --- a/include/dusk/logging.h +++ b/include/dusk/logging.h @@ -4,10 +4,12 @@ #include #include +#include + void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message, unsigned int len); namespace dusk { - void InitializeFileLogging(const char* configDir, AuroraLogLevel logLevel); + void InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel); void ShutdownFileLogging(); const char* GetLogFilePath(); void SendToStubLog(AuroraLogLevel level, const char* module, const char* message); diff --git a/include/dusk/main.h b/include/dusk/main.h index 2152a6d564..065f507d36 100644 --- a/include/dusk/main.h +++ b/include/dusk/main.h @@ -1,11 +1,14 @@ #ifndef DUSK_MAIN_H #define DUSK_MAIN_H +#include + namespace dusk { extern bool IsRunning; extern bool IsShuttingDown; extern bool IsGameLaunched; extern bool IsFocusPaused; + extern std::filesystem::path ConfigPath; } #endif // DUSK_MAIN_H diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index 64b0fedcf2..9ea826feee 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -379,6 +379,7 @@ static void waitPrecise(Limiter& limiter, Uint64 targetNs) { static void waitForTick(u32 p1, u16 p2) { #if TARGET_PC if (dusk::getSettings().game.enableFrameInterpolation && !dusk::getTransientSettings().skipFrameRateLimit) { + dusk::frameUsagePct = 0.f; return; } if (dusk::getTransientSettings().skipFrameRateLimit) { diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index c377d1ba54..176967359d 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -10,7 +10,7 @@ #include #include -#include "dusk/dusk.h" +#include "dusk/main.h" using namespace dusk::config; @@ -24,7 +24,7 @@ static absl::flat_hash_map RegisteredConfigVar static bool RegistrationDone = false; static std::string GetConfigJsonPath() { - return fmt::format("{}{}", configPath, ConfigFileName); + return (dusk::ConfigPath / ConfigFileName).string(); } ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) { diff --git a/src/dusk/crash_reporting.cpp b/src/dusk/crash_reporting.cpp index 73f432e418..0499f0d6a1 100644 --- a/src/dusk/crash_reporting.cpp +++ b/src/dusk/crash_reporting.cpp @@ -3,6 +3,7 @@ #include "dusk/app_info.hpp" #include "dusk/dusk.h" #include "dusk/logging.h" +#include "dusk/main.h" #include "dusk/settings.h" #include "version.h" @@ -66,7 +67,7 @@ std::string GetReleaseName() { } std::filesystem::path GetSentryDatabasePath() { - return std::filesystem::path(configPath) / "sentry"; + return dusk::ConfigPath / "sentry"; } std::filesystem::path GetLogAttachmentPath() { diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index f5e25277c7..0b32fd5078 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -319,9 +319,11 @@ namespace dusk { } } + // The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg, + // so make the window bg fully transparent temporarily + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); if (showMenu && ImGui::BeginMainMenuBar()) { m_menuGame.draw(); - m_menuEnhancements.draw(); m_menuTools.draw(); const auto fpsLabel = @@ -336,6 +338,7 @@ namespace dusk { ImGui::EndMainMenuBar(); } + ImGui::PopStyleColor(); if (!getSettings().backend.wasPresetChosen) { m_firstRunPreset.draw(); diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp index 70c5184d0d..de4e66c7d6 100644 --- a/src/dusk/imgui/ImGuiConsole.hpp +++ b/src/dusk/imgui/ImGuiConsole.hpp @@ -9,7 +9,6 @@ #include #include "ImGuiFirstRunPreset.hpp" -#include "ImGuiMenuEnhancements.hpp" #include "ImGuiMenuGame.hpp" #include "ImGuiMenuTools.hpp" #include "ImGuiPreLaunchWindow.hpp" @@ -52,7 +51,6 @@ private: ImGuiFirstRunPreset m_firstRunPreset; ImGuiMenuGame m_menuGame; - ImGuiMenuEnhancements m_menuEnhancements; ImGuiPreLaunchWindow m_preLaunchWindow; // Keep always last diff --git a/src/dusk/imgui/ImGuiEngine.cpp b/src/dusk/imgui/ImGuiEngine.cpp index 46bfa8f3ab..4b6a7fb531 100644 --- a/src/dusk/imgui/ImGuiEngine.cpp +++ b/src/dusk/imgui/ImGuiEngine.cpp @@ -126,7 +126,7 @@ void ImGuiEngine_Initialize(float scale) { auto* colors = style.Colors; colors[ImGuiCol_Text] = ImVec4(0.95f, 0.96f, 0.98f, 1.00f); colors[ImGuiCol_TextDisabled] = ImVec4(0.36f, 0.42f, 0.47f, 1.00f); - colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f); + colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.15f, 0.17f, 0.98f); colors[ImGuiCol_ChildBg] = ImVec4(0.15f, 0.18f, 0.22f, 1.00f); colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.94f); colors[ImGuiCol_Border] = ImVec4(0.08f, 0.10f, 0.12f, 1.00f); @@ -137,7 +137,7 @@ void ImGuiEngine_Initialize(float scale) { colors[ImGuiCol_TitleBg] = ImVec4(0.09f, 0.12f, 0.14f, 0.65f); colors[ImGuiCol_TitleBgActive] = ImVec4(0.08f, 0.10f, 0.12f, 1.00f); colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); - colors[ImGuiCol_MenuBarBg] = ImVec4(0.15f, 0.18f, 0.22f, 1.00f); + colors[ImGuiCol_MenuBarBg] = ImVec4(0.15f, 0.18f, 0.22f, 0.80f); colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.39f); colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.20f, 0.25f, 0.29f, 1.00f); colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.18f, 0.22f, 0.25f, 1.00f); diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.cpp b/src/dusk/imgui/ImGuiMenuEnhancements.cpp deleted file mode 100644 index 7d179aa578..0000000000 --- a/src/dusk/imgui/ImGuiMenuEnhancements.cpp +++ /dev/null @@ -1,248 +0,0 @@ -#include "imgui.h" - -#include "ImGuiMenuEnhancements.hpp" -#include "ImGuiConfig.hpp" -#include "dusk/settings.h" - -namespace dusk { - ImGuiMenuEnhancements::ImGuiMenuEnhancements() {} - - void ImGuiMenuEnhancements::draw() { - if (ImGui::BeginMenu("Enhancements")) { - if (ImGui::BeginMenu("Gameplay")) { - ImGui::SeparatorText("Preferences"); - - config::ImGuiCheckbox("Mirror Mode", getSettings().game.enableMirrorMode); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Mirrors the world horizontally, matching the Wii version of the game."); - } - - config::ImGuiCheckbox("Disable Main HUD", getSettings().game.disableMainHUD); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Disables the main HUD of the game.\n" - "Useful for recording or a more immersive experience!"); - } - - ImGui::SeparatorText("Difficulty"); - - config::ImGuiSliderInt("Damage Multiplier", getSettings().game.damageMultiplier, 1, 8, "x%d"); - - config::ImGuiCheckbox("Instant Death", getSettings().game.instantDeath); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Any hit will instantly kill you."); - } - - config::ImGuiCheckbox("No Heart Drops", getSettings().game.noHeartDrops); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Hearts will never drop from enemies,\n" - "pots and various other places."); - } - - ImGui::SeparatorText("Quality of Life"); - - config::ImGuiCheckbox("Bigger Wallets", getSettings().game.biggerWallets); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Wallet sizes are like in the HD version. (500, 1000, 2000)"); - } - - config::ImGuiCheckbox("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Rupees won't play cutscenes after you've collected them the first time."); - } - - config::ImGuiCheckbox("Faster Climbing", getSettings().game.fastClimbing); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Quicker climbing on ladders and vines like the HD version."); - } - - config::ImGuiCheckbox("Faster Tears of Light", getSettings().game.fastTears); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Tears of Light dropped by Shadow Insects pop out faster like the HD version."); - } - - config::ImGuiCheckbox("Instant Saves", getSettings().game.instantSaves); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the delay when writing to the Memory Card."); - } - - config::ImGuiCheckbox("Hold B for Instant Text", getSettings().game.instantText); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Make text scroll immediately by holding B."); - } - - config::ImGuiCheckbox("No Climbing Miss Animation", getSettings().game.noMissClimbing); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Prevents Link from playing a struggle animation\n" - "when grabbing ledges or climbing on vines."); - } - - config::ImGuiCheckbox("No Rupee Returns", getSettings().game.noReturnRupees); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Always collect Rupees even if your Wallet is too full."); - } - - config::ImGuiCheckbox("No Sword Recoil", getSettings().game.noSwordRecoil); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Link won't recoil when his sword hits walls."); - } - - config::ImGuiCheckbox("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the TV calibration screen shown when loading a save."); - } - - config::ImGuiCheckbox("Skip Warning Screen", getSettings().game.skipWarningScreen); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Skip the warning screen shown when starting the game."); - } - - config::ImGuiCheckbox("Sun's Song (R+X)", getSettings().game.sunsSong); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Allows Wolf Link to howl and change the time of day."); - } - - config::ImGuiCheckbox("Quick Transform (R+Y)", getSettings().game.enableQuickTransform); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Transform instantly by pressing R and Y simultaneously."); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Graphics")) { - config::ImGuiSliderInt("Shadow Resolution", getSettings().game.shadowResolutionMultiplier, 1, 8, "x%d"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Improves the shadow resolution, making them higher quality."); - } - - config::ImGuiCheckbox("Unlock Framerate", getSettings().game.enableFrameInterpolation); - const bool frameInterpolationHovered = ImGui::IsItemHovered(); - ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.72f, 0.2f, 1.0f)); - ImGui::TextUnformatted("[EXPERIMENTAL]"); - ImGui::PopStyleColor(); - if (frameInterpolationHovered || ImGui::IsItemHovered()) { - ImGui::SetTooltip("Uses inter-frame interpolation to enable higher frame rates.\nVisual artifacts, animation glitches, or instability may occur."); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Audio")) { - config::ImGuiCheckbox("No Low HP Sound", getSettings().game.noLowHpSound); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Disable the beeping sound when having low health."); - } - - config::ImGuiCheckbox("Non-Stop Midna's Lament", getSettings().game.midnasLamentNonStop); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Prevents enemy music while Midna's Lament is playing."); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Input")) { - config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); - - ImGui::SeparatorText("Gyro"); - - config::ImGuiCheckbox("Gyro Aim", getSettings().game.enableGyroAim); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables the gyroscope on supported controllers\n" - "while in look mode (C-Up) and while aiming the\n" - "Slingshot, Gale Boomerang, Hero's Bow, Clawshot(s),\n" - "Ball and Chain, and Dominion Rod."); - } - - config::ImGuiCheckbox("Gyro Rollgoal", getSettings().game.enableGyroRollgoal); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Enables the gyroscope on supported controllers to\n" - "tilt the Rollgoal table in Hena's Cabin."); - } - - if (getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal) { - config::ImGuiSliderFloat("Gyro Pitch Sensitivity", getSettings().game.gyroSensitivityY, 0.25f, 4.0f, "%.2f"); - config::ImGuiSliderFloat("Gyro Yaw Sensitivity", getSettings().game.gyroSensitivityX, 0.25f, 4.0f, "%.2f"); - - if (getSettings().game.enableGyroRollgoal) { - config::ImGuiSliderFloat("Rollgoal Sensitivity", getSettings().game.gyroSensitivityRollgoal, 0.25f, 4.0f, "%.2f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Additional multiplier for scaling how strongly\n" - "the gyroscope affects the Rollgoal table."); - } - } - - config::ImGuiSliderFloat("Gyro Deadband", getSettings().game.gyroDeadband, 0.0f, 0.5f, "%.3f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Angular rates below this magnitude are treated as zero,\n" - "reducing drift and jitter when the controller is still."); - } - - config::ImGuiSliderFloat("Gyro Smoothing", getSettings().game.gyroSmoothing, 0.0f, 1.0f, "%.2f"); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Low values track raw gyro input more closely,\n" - "while higher values smooth out input over time."); - } - - config::ImGuiCheckbox("Invert Gyro Pitch", getSettings().game.gyroInvertPitch); - config::ImGuiCheckbox("Invert Gyro Yaw", getSettings().game.gyroInvertYaw); - } - - ImGui::SeparatorText("Tools"); - - config::ImGuiCheckbox("Turbo Key", getSettings().game.enableTurboKeybind); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Hold TAB to increase game speed by up to 4x."); - } - - ImGui::EndMenu(); - } - - ImGui::Separator(); - - if (ImGui::BeginMenu("Cheats")) { - config::ImGuiCheckbox("Infinite Hearts", getSettings().game.infiniteHearts); - config::ImGuiCheckbox("Infinite Arrows", getSettings().game.infiniteArrows); - config::ImGuiCheckbox("Infinite Bombs", getSettings().game.infiniteBombs); - config::ImGuiCheckbox("Infinite Oil", getSettings().game.infiniteOil); - config::ImGuiCheckbox("Infinite Oxygen", getSettings().game.infiniteOxygen); - config::ImGuiCheckbox("Infinite Rupees", getSettings().game.infiniteRupees); - config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump); - config::ImGuiCheckbox("Super Clawshot", getSettings().game.superClawshot); - config::ImGuiCheckbox("Always Greatspin", getSettings().game.alwaysGreatspin); - - config::ImGuiCheckbox("Fast Iron Boots", getSettings().game.enableFastIronBoots); - - config::ImGuiCheckbox("Can Transform Anywhere", getSettings().game.canTransformAnywhere); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Allows you to transform even if NPCs are looking."); - } - - config::ImGuiCheckbox("Fast Spinner", getSettings().game.fastSpinner); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Speeds up Spinner movement when holding R."); - } - - config::ImGuiCheckbox("Free Magic Armor", getSettings().game.freeMagicArmor); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Makes the magic armor work without rupees."); - } - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Technical")) { - config::ImGuiCheckbox("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches); - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Restores patched glitches from Wii USA 1.0,\n" - "the first released version."); - } - - ImGui::EndMenu(); - } - - ImGui::EndMenu(); - } - } -} diff --git a/src/dusk/imgui/ImGuiMenuEnhancements.hpp b/src/dusk/imgui/ImGuiMenuEnhancements.hpp deleted file mode 100644 index f40baaad65..0000000000 --- a/src/dusk/imgui/ImGuiMenuEnhancements.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef DUSK_IMGUI_MENUENHANCEMENTS_HPP -#define DUSK_IMGUI_MENUENHANCEMENTS_HPP - -#include -#include -#include - -#include "imgui.h" - -namespace dusk { - class ImGuiMenuEnhancements { - public: - ImGuiMenuEnhancements(); - void draw(); - }; -} - -#endif // DUSK_IMGUI_MENUENHANCEMENTS_HPP diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index c6676df774..691434427c 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -5,45 +5,18 @@ #include "ImGuiConsole.hpp" #include "ImGuiMenuGame.hpp" #include "ImGuiConfig.hpp" -#include #include "JSystem/JUtility/JUTGamePad.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/audio/DuskDsp.hpp" -#include "dusk/dusk.h" +#include "dusk/main.h" #include "dusk/hotkeys.h" #include "dusk/settings.h" #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" #include -#include #include -#include - -#include "dusk/main.h" - -#if defined(__APPLE__) -#include -#endif - -#if defined(_WIN32) || (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || (defined(__linux__) && !defined(__ANDROID__)) -#define DUSK_CAN_OPEN_DATA_FOLDER 1 - -namespace fs = std::filesystem; - -static void OpenDataFolder() { - const std::string path = fs::absolute(fs::path(aurora::g_config.configPath)).generic_string(); -#if defined(_WIN32) - const std::string url = std::string("file:///") + path; -#else - const std::string url = std::string("file://") + path; -#endif - (void)SDL_OpenURL(url.c_str()); -} -#else -#define DUSK_CAN_OPEN_DATA_FOLDER 0 -#endif namespace { constexpr int kInternalResolutionScaleMax = 12; @@ -63,149 +36,13 @@ namespace dusk { ImGuiMenuGame::ImGuiMenuGame() {} void ImGuiMenuGame::draw() { - if (ImGui::BeginMenu("Game")) { - if (ImGui::BeginMenu("Graphics")) { - if (!IsMobile) { - if (ImGui::MenuItem("Toggle Fullscreen", hotkeys::TOGGLE_FULLSCREEN)) { - ToggleFullscreen(); - } - - if (ImGui::MenuItem("Default Window Size")) { - getSettings().video.enableFullscreen.setValue(false); - VISetWindowFullscreen(false); - VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); - VICenterWindow(); - } - } - - bool vsync = getSettings().video.enableVsync; - if (ImGui::Checkbox("Enable Vsync", &vsync)) { - getSettings().video.enableVsync.setValue(vsync); - aurora_enable_vsync(vsync); - config::Save(); - } - - bool lockAspect = getSettings().video.lockAspectRatio; - if (ImGui::Checkbox("Force 4:3 Aspect Ratio", &lockAspect)) { - getSettings().video.lockAspectRatio.setValue(lockAspect); - - if (lockAspect) { - AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); - } else { - AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); - } - - config::Save(); - } - - u32 internalResolutionWidth = 0; - u32 internalResolutionHeight = 0; - AuroraGetRenderSize(&internalResolutionWidth, &internalResolutionHeight); - ImGui::TextDisabled("Current internal resolution: %ux%u", internalResolutionWidth, - internalResolutionHeight); - - int scale = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0, - kInternalResolutionScaleMax); - if (ImGui::SliderInt("Internal Resolution", &scale, 0, kInternalResolutionScaleMax, - scale == 0 ? "Auto" : "%dx")) - { - getSettings().game.internalResolutionScale.setValue(scale); - VISetFrameBufferScale(static_cast(scale)); - config::Save(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Auto renders at the native window resolution.\n" - "Higher values scale the game's internal framebuffer."); - } - - constexpr const char* bloomModeNames[] = {"Off", "Classic", "Dusk"}; - int bloomMode = static_cast(getSettings().game.bloomMode.getValue()); - if (ImGui::BeginCombo("Bloom", bloomModeNames[bloomMode])) { - for (int i = 0; i < IM_ARRAYSIZE(bloomModeNames); i++) { - const bool selected = bloomMode == i; - if (ImGui::Selectable(bloomModeNames[i], selected)) { - getSettings().game.bloomMode.setValue(static_cast(i)); - config::Save(); - } - if (selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - bool bloomOff = bloomMode == static_cast(BloomMode::Off); - if (bloomOff) ImGui::BeginDisabled(); - float mult = getSettings().game.bloomMultiplier.getValue(); - if (ImGui::SliderFloat("Bloom Brightness", &mult, 0.0f, 1.0f, "%.2f")) { - getSettings().game.bloomMultiplier.setValue(mult); - config::Save(); - } - if (bloomOff) ImGui::EndDisabled(); - - ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Audio")) { - ImGui::Text("Master Volume"); - if (config::ImGuiSliderInt("##masterVolume", getSettings().audio.masterVolume, 0, 100)) { - dusk::audio::SetMasterVolume(getSettings().audio.masterVolume / 100.0f); - } - - if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) { - dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); - } - /* - // TODO: Implement additional settings - ImGui::Text("Main Music Volume"); - ImGui::SliderFloat("##mainMusicVolume", &getSettings().audio.mainMusicVolume, 0, 100); - - ImGui::Text("Sub Music Volume"); - ImGui::SliderFloat("##subMusicVolume", &getSettings().audio.subMusicVolume, 0, 100); - - ImGui::Text("Sound Effects Volume"); - ImGui::SliderFloat("##soundEffectsVolume", &getSettings().audio.soundEffectsVolume, 0, 100); - - ImGui::Text("Fanfare Volume"); - ImGui::SliderFloat("##fanfareVolume", &getSettings().audio.fanfareVolume, 0, 100); - - Z2AudioMgr* audioMgr = Z2AudioMgr::getInterface(); - if (audioMgr != nullptr) { - } - */ - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Controller")) { - ImGui::MenuItem("Configure Controller", nullptr, &m_showControllerConfig); - ImGui::Checkbox("Show Input Viewer", &m_showInputViewer); - - ImGui::EndMenu(); - } - - if (ImGui::BeginMenu("Interface")) { - config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI); - config::ImGuiCheckbox("Show Pipeline Compilation", getSettings().backend.showPipelineCompilation); -#if DUSK_ENABLE_SENTRY_NATIVE - config::ImGuiCheckbox("Enable Crash Reporting", getSettings().backend.enableCrashReporting); -#endif - if (!IsMobile) { - config::ImGuiCheckbox("Pause on Focus Lost", getSettings().game.pauseOnFocusLost); - } - - ImGui::EndMenu(); - } - - ImGui::Separator(); - -#if DUSK_CAN_OPEN_DATA_FOLDER - if (ImGui::MenuItem("Open Data Folder")) { - OpenDataFolder(); - } -#endif + if (ImGui::BeginMenu("Settings")) { + drawAudioMenu(); + drawCheatsMenu(); + drawGameplayMenu(); + drawGraphicsMenu(); + drawInputMenu(); + drawInterfaceMenu(); ImGui::Separator(); @@ -221,6 +58,383 @@ namespace dusk { } } + void ImGuiMenuGame::drawGraphicsMenu() { + if (ImGui::BeginMenu("Graphics")) { + ImGui::SeparatorText("Display"); + + if (!IsMobile) { + if (ImGui::MenuItem("Toggle Fullscreen", hotkeys::TOGGLE_FULLSCREEN)) { + ToggleFullscreen(); + } + + if (ImGui::MenuItem("Restore Default Window Size")) { + getSettings().video.enableFullscreen.setValue(false); + VISetWindowFullscreen(false); + VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); + VICenterWindow(); + } + } + + bool vsync = getSettings().video.enableVsync; + if (ImGui::Checkbox("Enable VSync", &vsync)) { + getSettings().video.enableVsync.setValue(vsync); + aurora_enable_vsync(vsync); + config::Save(); + } + + bool lockAspect = getSettings().video.lockAspectRatio; + if (ImGui::Checkbox("Force 4:3 Aspect Ratio", &lockAspect)) { + getSettings().video.lockAspectRatio.setValue(lockAspect); + + if (lockAspect) { + AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT); + } else { + AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); + } + + config::Save(); + } + + ImGui::SeparatorText("Resolution"); + + u32 internalResolutionWidth = 0; + u32 internalResolutionHeight = 0; + AuroraGetRenderSize(&internalResolutionWidth, &internalResolutionHeight); + ImGui::TextDisabled("Current internal resolution: %ux%u", internalResolutionWidth, + internalResolutionHeight); + + int scale = std::clamp(getSettings().game.internalResolutionScale.getValue(), 0, + kInternalResolutionScaleMax); + if (ImGui::SliderInt("Internal Resolution", &scale, 0, kInternalResolutionScaleMax, + scale == 0 ? "Auto" : "%dx")) + { + getSettings().game.internalResolutionScale.setValue(scale); + VISetFrameBufferScale(static_cast(scale)); + config::Save(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Auto renders at the native window resolution.\n" + "Higher values scale the game's internal framebuffer."); + } + + config::ImGuiSliderInt("Shadow Resolution", getSettings().game.shadowResolutionMultiplier, 1, 8, "x%d"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Improves the shadow resolution, making them higher quality."); + } + + ImGui::SeparatorText("Post-Processing"); + + constexpr const char* bloomModeNames[] = {"Off", "Classic", "Dusk"}; + int bloomMode = static_cast(getSettings().game.bloomMode.getValue()); + if (ImGui::BeginCombo("Bloom", bloomModeNames[bloomMode])) { + for (int i = 0; i < IM_ARRAYSIZE(bloomModeNames); i++) { + const bool selected = bloomMode == i; + if (ImGui::Selectable(bloomModeNames[i], selected)) { + getSettings().game.bloomMode.setValue(static_cast(i)); + config::Save(); + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + bool bloomOff = bloomMode == static_cast(BloomMode::Off); + if (bloomOff) ImGui::BeginDisabled(); + float mult = getSettings().game.bloomMultiplier.getValue(); + if (ImGui::SliderFloat("Bloom Brightness", &mult, 0.0f, 1.0f, "%.2f")) { + getSettings().game.bloomMultiplier.setValue(mult); + config::Save(); + } + if (bloomOff) ImGui::EndDisabled(); + + ImGui::SeparatorText("Rendering"); + + config::ImGuiCheckbox("Unlock Framerate", getSettings().game.enableFrameInterpolation); + const bool frameInterpolationHovered = ImGui::IsItemHovered(); + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.72f, 0.2f, 1.0f)); + ImGui::TextUnformatted("[EXPERIMENTAL]"); + ImGui::PopStyleColor(); + if (frameInterpolationHovered || ImGui::IsItemHovered()) { + ImGui::SetTooltip("Uses inter-frame interpolation to enable higher frame rates.\nVisual artifacts, animation glitches, or instability may occur."); + } + + ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawGameplayMenu() { + if (ImGui::BeginMenu("Gameplay")) { + ImGui::SeparatorText("General"); + + config::ImGuiCheckbox("Mirror Mode", getSettings().game.enableMirrorMode); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Mirrors the world horizontally, matching the Wii version of the game."); + } + + config::ImGuiCheckbox("Disable Main HUD", getSettings().game.disableMainHUD); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Disables the main HUD of the game.\n" + "Useful for recording or a more immersive experience!"); + } + + config::ImGuiCheckbox("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Restores patched glitches from Wii USA 1.0,\n" + "the first released version."); + } + + ImGui::SeparatorText("Difficulty"); + + config::ImGuiSliderInt("Damage Multiplier", getSettings().game.damageMultiplier, 1, 8, "x%d"); + + config::ImGuiCheckbox("Instant Death", getSettings().game.instantDeath); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Any hit will instantly kill you."); + } + + config::ImGuiCheckbox("No Heart Drops", getSettings().game.noHeartDrops); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Hearts will never drop from enemies,\n" + "pots and various other places."); + } + + ImGui::SeparatorText("Quality of Life"); + + config::ImGuiCheckbox("Bigger Wallets", getSettings().game.biggerWallets); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Wallet sizes are like in the HD version. (500, 1000, 2000)"); + } + + config::ImGuiCheckbox("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Rupees won't play cutscenes after you've collected them the first time."); + } + + config::ImGuiCheckbox("Faster Climbing", getSettings().game.fastClimbing); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Quicker climbing on ladders and vines like the HD version."); + } + + config::ImGuiCheckbox("Faster Tears of Light", getSettings().game.fastTears); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Tears of Light dropped by Shadow Insects pop out faster like the HD version."); + } + + config::ImGuiCheckbox("Instant Saves", getSettings().game.instantSaves); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Skip the delay when writing to the Memory Card."); + } + + config::ImGuiCheckbox("Hold B for Instant Text", getSettings().game.instantText); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Make text scroll immediately by holding B."); + } + + config::ImGuiCheckbox("No Climbing Miss Animation", getSettings().game.noMissClimbing); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Prevents Link from playing a struggle animation\n" + "when grabbing ledges or climbing on vines."); + } + + config::ImGuiCheckbox("No Rupee Returns", getSettings().game.noReturnRupees); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Always collect Rupees even if your Wallet is too full."); + } + + config::ImGuiCheckbox("No Sword Recoil", getSettings().game.noSwordRecoil); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Link won't recoil when his sword hits walls."); + } + + config::ImGuiCheckbox("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Skip the TV calibration screen shown when loading a save."); + } + + config::ImGuiCheckbox("Skip Warning Screen", getSettings().game.skipWarningScreen); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Skip the warning screen shown when starting the game."); + } + + config::ImGuiCheckbox("Sun's Song (R+X)", getSettings().game.sunsSong); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Allows Wolf Link to howl and change the time of day."); + } + + config::ImGuiCheckbox("Quick Transform (R+Y)", getSettings().game.enableQuickTransform); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Transform instantly by pressing R and Y simultaneously."); + } + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawCheatsMenu() { + if (ImGui::BeginMenu("Cheats")) { + ImGui::SeparatorText("Resources"); + config::ImGuiCheckbox("Infinite Hearts", getSettings().game.infiniteHearts); + config::ImGuiCheckbox("Infinite Arrows", getSettings().game.infiniteArrows); + config::ImGuiCheckbox("Infinite Bombs", getSettings().game.infiniteBombs); + config::ImGuiCheckbox("Infinite Oil", getSettings().game.infiniteOil); + config::ImGuiCheckbox("Infinite Oxygen", getSettings().game.infiniteOxygen); + config::ImGuiCheckbox("Infinite Rupees", getSettings().game.infiniteRupees); + + ImGui::SeparatorText("Abilities"); + config::ImGuiCheckbox("Moon Jump (R+A)", getSettings().game.moonJump); + config::ImGuiCheckbox("Super Clawshot", getSettings().game.superClawshot); + config::ImGuiCheckbox("Always Greatspin", getSettings().game.alwaysGreatspin); + config::ImGuiCheckbox("Fast Iron Boots", getSettings().game.enableFastIronBoots); + + config::ImGuiCheckbox("Can Transform Anywhere", getSettings().game.canTransformAnywhere); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Allows you to transform even if NPCs are looking."); + } + + config::ImGuiCheckbox("Fast Spinner", getSettings().game.fastSpinner); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Speeds up Spinner movement when holding R."); + } + + config::ImGuiCheckbox("Free Magic Armor", getSettings().game.freeMagicArmor); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Makes the magic armor work without rupees."); + } + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawAudioMenu() { + if (ImGui::BeginMenu("Audio")) { + ImGui::Text("Master Volume"); + if (config::ImGuiSliderInt("##masterVolume", getSettings().audio.masterVolume, 0, 100)) { + dusk::audio::SetMasterVolume(getSettings().audio.masterVolume / 100.0f); + } + + if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) { + dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); + } + /* + // TODO: Implement additional settings + ImGui::Text("Main Music Volume"); + ImGui::SliderFloat("##mainMusicVolume", &getSettings().audio.mainMusicVolume, 0, 100); + + ImGui::Text("Sub Music Volume"); + ImGui::SliderFloat("##subMusicVolume", &getSettings().audio.subMusicVolume, 0, 100); + + ImGui::Text("Sound Effects Volume"); + ImGui::SliderFloat("##soundEffectsVolume", &getSettings().audio.soundEffectsVolume, 0, 100); + + ImGui::Text("Fanfare Volume"); + ImGui::SliderFloat("##fanfareVolume", &getSettings().audio.fanfareVolume, 0, 100); + + Z2AudioMgr* audioMgr = Z2AudioMgr::getInterface(); + if (audioMgr != nullptr) { + } + */ + + ImGui::SeparatorText("Tweaks"); + + config::ImGuiCheckbox("No Low HP Sound", getSettings().game.noLowHpSound); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Disable the beeping sound when having low health."); + } + + config::ImGuiCheckbox("Non-Stop Midna's Lament", getSettings().game.midnasLamentNonStop); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Prevents enemy music while Midna's Lament is playing."); + } + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawInputMenu() { + if (ImGui::BeginMenu("Input")) { + ImGui::SeparatorText("Controller"); + + ImGui::MenuItem("Configure Controller", nullptr, &m_showControllerConfig); + + config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); + + ImGui::SeparatorText("Gyro"); + + config::ImGuiCheckbox("Gyro Aim", getSettings().game.enableGyroAim); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Enables the gyroscope on supported controllers\n" + "while in look mode (C-Up) and while aiming the\n" + "Slingshot, Gale Boomerang, Hero's Bow, Clawshot(s),\n" + "Ball and Chain, and Dominion Rod."); + } + + config::ImGuiCheckbox("Gyro Rollgoal", getSettings().game.enableGyroRollgoal); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Enables the gyroscope on supported controllers to\n" + "tilt the Rollgoal table in Hena's Cabin."); + } + + if (getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal) { + config::ImGuiSliderFloat("Gyro Pitch Sensitivity", getSettings().game.gyroSensitivityY, 0.25f, 4.0f, "%.2f"); + config::ImGuiSliderFloat("Gyro Yaw Sensitivity", getSettings().game.gyroSensitivityX, 0.25f, 4.0f, "%.2f"); + + if (getSettings().game.enableGyroRollgoal) { + config::ImGuiSliderFloat("Rollgoal Sensitivity", getSettings().game.gyroSensitivityRollgoal, 0.25f, 4.0f, "%.2f"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Additional multiplier for scaling how strongly\n" + "the gyroscope affects the Rollgoal table."); + } + } + + config::ImGuiSliderFloat("Gyro Deadband", getSettings().game.gyroDeadband, 0.0f, 0.5f, "%.3f"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Angular rates below this magnitude are treated as zero,\n" + "reducing drift and jitter when the controller is still."); + } + + config::ImGuiSliderFloat("Gyro Smoothing", getSettings().game.gyroSmoothing, 0.0f, 1.0f, "%.2f"); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Low values track raw gyro input more closely,\n" + "while higher values smooth out input over time."); + } + + config::ImGuiCheckbox("Invert Gyro Pitch", getSettings().game.gyroInvertPitch); + config::ImGuiCheckbox("Invert Gyro Yaw", getSettings().game.gyroInvertYaw); + } + + ImGui::SeparatorText("Tools"); + + config::ImGuiCheckbox("Turbo Key", getSettings().game.enableTurboKeybind); + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Hold TAB to increase game speed by up to 4x."); + } + + ImGui::Checkbox("Show Input Viewer", &m_showInputViewer); + + ImGui::EndMenu(); + } + } + + void ImGuiMenuGame::drawInterfaceMenu() { + if (ImGui::BeginMenu("Interface")) { + config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI); + config::ImGuiCheckbox("Show Pipeline Compilation", getSettings().backend.showPipelineCompilation); +#if DUSK_ENABLE_SENTRY_NATIVE + config::ImGuiCheckbox("Enable Crash Reporting", getSettings().backend.enableCrashReporting); +#endif + if (!IsMobile) { + config::ImGuiCheckbox("Pause on Focus Lost", getSettings().game.pauseOnFocusLost); + } + + ImGui::EndMenu(); + } + } + static void drawVirtualStick(const char* id, const ImVec2& stick) { float scale = ImGuiScale(); ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + 45 * scale, ImGui::GetCursorPos().y + 10)); diff --git a/src/dusk/imgui/ImGuiMenuGame.hpp b/src/dusk/imgui/ImGuiMenuGame.hpp index 4d51cbc865..e21374c8f4 100644 --- a/src/dusk/imgui/ImGuiMenuGame.hpp +++ b/src/dusk/imgui/ImGuiMenuGame.hpp @@ -19,6 +19,13 @@ namespace dusk { static void ToggleFullscreen(); private: + void drawAudioMenu(); + void drawInputMenu(); + void drawGraphicsMenu(); + void drawGameplayMenu(); + void drawCheatsMenu(); + void drawInterfaceMenu(); + struct { int m_selectedPort = 0; bool m_isReading = false; diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 1f9d42fbc9..97f5b5a8d3 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -16,10 +16,58 @@ #include "dusk/main.h" #include "m_Do/m_Do_main.h" +#include +#include + +#if defined(__APPLE__) +#include +#endif + +#if defined(_WIN32) || (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || (defined(__linux__) && !defined(__ANDROID__)) +#define DUSK_CAN_OPEN_DATA_FOLDER 1 + +namespace fs = std::filesystem; + +static void OpenDataFolder() { + const std::string path = fs::absolute(dusk::ConfigPath).generic_string(); +#if defined(_WIN32) + const std::string url = std::string("file:///") + path; +#else + const std::string url = std::string("file://") + path; +#endif + (void)SDL_OpenURL(url.c_str()); +} +#else +#define DUSK_CAN_OPEN_DATA_FOLDER 0 +#endif + namespace dusk { ImGuiMenuTools::ImGuiMenuTools() {} void ImGuiMenuTools::draw() { + if (ImGui::BeginMenu("Tools")) { + if (!dusk::IsGameLaunched) { + ImGui::BeginDisabled(); + } + + ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor); + ImGui::MenuItem("Map Loader", hotkeys::SHOW_MAP_LOADER, &m_showMapLoader); + ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare); + + if (!dusk::IsGameLaunched) { + ImGui::EndDisabled(); + } + +#if DUSK_CAN_OPEN_DATA_FOLDER + ImGui::Separator(); + if (ImGui::MenuItem("Open Data Folder")) { + OpenDataFolder(); + } +#endif + + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Debug")) { bool developmentMode = mDoMain::developmentMode == 1; if (ImGui::Checkbox("Development Mode", &developmentMode)) { @@ -59,9 +107,6 @@ namespace dusk { ImGui::MenuItem("Debug Overlay", hotkeys::SHOW_DEBUG_OVERLAY, &m_showDebugOverlay); ImGui::MenuItem("Heap Viewer", hotkeys::SHOW_HEAP_VIEWER, &m_showHeapOverlay); ImGui::MenuItem("Player Info", hotkeys::SHOW_PLAYER_INFO, &m_showPlayerInfo); - ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor); - ImGui::MenuItem("Map Loader", hotkeys::SHOW_MAP_LOADER, &m_showMapLoader); - ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare); ImGui::MenuItem("Debug Camera", hotkeys::SHOW_DEBUG_CAMERA, &m_showCameraOverlay); ImGui::MenuItem("Audio Debug", hotkeys::SHOW_AUDIO_DEBUG, &m_showAudioDebug); ImGui::MenuItem("Bloom", nullptr, &m_showBloomWindow); @@ -96,7 +141,9 @@ namespace dusk { ImGui::SetNextWindowBgAlpha(0.65f); if (ImGui::Begin("Debug Overlay", nullptr, windowFlags)) { ImGuiStringViewText(fmt::format(FMT_STRING("FPS: {:.2f}\n"), io.Framerate)); - ImGuiStringViewText(fmt::format(FMT_STRING("Frame usage: {:.1f}%\n"), frameUsagePct)); + if (frameUsagePct > 0.f) { + ImGuiStringViewText(fmt::format(FMT_STRING("Frame usage: {:.1f}%\n"), frameUsagePct)); + } ImGui::Separator(); diff --git a/src/dusk/logging.cpp b/src/dusk/logging.cpp index 3fdd6cdf45..172059aea4 100644 --- a/src/dusk/logging.cpp +++ b/src/dusk/logging.cpp @@ -168,14 +168,14 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m aurora::Module DuskLog("dusk"); -void dusk::InitializeFileLogging(const char* configDir, AuroraLogLevel logLevel) { +void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel) { std::lock_guard lock(g_logMutex); - if (g_logFile != nullptr || configDir == nullptr) { + if (g_logFile != nullptr || configDir.empty()) { return; } std::error_code ec; - const std::filesystem::path logsDir = std::filesystem::path(configDir) / "logs"; + const std::filesystem::path logsDir = configDir / "logs"; std::filesystem::create_directories(logsDir, ec); if (ec) { std::fprintf(stderr, "[WARNING | dusk] Failed to create log directory '%s': %s\n", diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index e41bd1f24c..2164089e9f 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -98,6 +98,7 @@ bool dusk::IsRunning = true; bool dusk::IsShuttingDown = false; bool dusk::IsGameLaunched = false; bool dusk::IsFocusPaused = false; +std::filesystem::path dusk::ConfigPath; #endif s32 LOAD_COPYDATE(void*) { @@ -130,7 +131,6 @@ s32 LOAD_COPYDATE(void*) { AuroraInfo auroraInfo; AuroraStats dusk::lastFrameAuroraStats; float dusk::frameUsagePct = 0.0f; -const char* configPath; bool launchUILoop() { while (dusk::IsRunning && !dusk::IsGameLaunched) { @@ -379,7 +379,7 @@ static void ApplyCVarOverrides(const cxxopts::OptionValue& option) { } } -static const char* CalculateConfigPath() { +static std::filesystem::path CalculateConfigPath() { const auto result = SDL_GetPrefPath(dusk::OrgName, dusk::AppName); if (!result) { DuskLog.fatal("Unable to get PrefPath: {}", SDL_GetError()); @@ -388,13 +388,12 @@ static const char* CalculateConfigPath() { return result; } -static void EnsureInitialPipelineCache(const char* configDir) { - if (configDir == nullptr) { +static void EnsureInitialPipelineCache(const std::filesystem::path& configDir) { + if (configDir.empty()) { return; } - const std::filesystem::path configPathFs(configDir); - const std::filesystem::path pipelineCachePath = configPathFs / "pipeline_cache.db"; + const std::filesystem::path pipelineCachePath = configDir / "pipeline_cache.db"; if (std::filesystem::exists(pipelineCachePath)) { return; } @@ -413,10 +412,10 @@ static void EnsureInitialPipelineCache(const char* configDir) { } std::error_code ec; - std::filesystem::create_directories(configPathFs, ec); + std::filesystem::create_directories(configDir, ec); if (ec) { DuskLog.warn("Failed to create config directory '{}' for pipeline cache: {}", - configPathFs.string(), ec.message()); + configDir.string(), ec.message()); return; } @@ -507,37 +506,38 @@ int game_main(int argc, char* argv[]) { exit(1); } - configPath = CalculateConfigPath(); + dusk::ConfigPath = CalculateConfigPath(); const auto startupLogLevel = static_cast(parsed_arg_options["log-level"].as()); - dusk::InitializeFileLogging(configPath, startupLogLevel); + dusk::InitializeFileLogging(dusk::ConfigPath, startupLogLevel); dusk::config::LoadFromUserPreferences(); ApplyCVarOverrides(parsed_arg_options["cvar"]); dusk::InitializeCrashReporting(); - EnsureInitialPipelineCache(configPath); - - AuroraConfig config{}; - config.appName = dusk::AppName; - config.configPath = configPath; - config.vsync = dusk::getSettings().video.enableVsync; - config.startFullscreen = dusk::getSettings().video.enableFullscreen; - config.windowPosX = -1; - config.windowPosY = -1; - config.windowWidth = defaultWindowWidth * 2; - config.windowHeight = defaultWindowHeight * 2; - config.desiredBackend = ResolveDesiredBackend(parsed_arg_options); - config.logCallback = &aurora_log_callback; - config.logLevel = startupLogLevel; - config.mem1Size = 256 * 1024 * 1024; - config.mem2Size = 24 * 1024 * 1024; - config.allowJoystickBackgroundEvents = true; - config.imGuiInitCallback = &aurora_imgui_init_callback; - config.allowTextureReplacements = true; - config.allowTextureDumps = false; - + EnsureInitialPipelineCache(dusk::ConfigPath); PADSetDefaultMapping(&defaultPadMapping); - auroraInfo = aurora_initialize(argc, argv, &config); + { + const auto configPathString = dusk::ConfigPath.string(); + AuroraConfig config{}; + config.appName = dusk::AppName; + config.configPath = configPathString.c_str(); + config.vsync = dusk::getSettings().video.enableVsync; + config.startFullscreen = dusk::getSettings().video.enableFullscreen; + config.windowPosX = -1; + config.windowPosY = -1; + config.windowWidth = defaultWindowWidth * 2; + config.windowHeight = defaultWindowHeight * 2; + config.desiredBackend = ResolveDesiredBackend(parsed_arg_options); + config.logCallback = &aurora_log_callback; + config.logLevel = startupLogLevel; + config.mem1Size = 256 * 1024 * 1024; + config.mem2Size = 24 * 1024 * 1024; + config.allowJoystickBackgroundEvents = true; + config.imGuiInitCallback = &aurora_imgui_init_callback; + config.allowTextureReplacements = true; + config.allowTextureDumps = false; + auroraInfo = aurora_initialize(argc, argv, &config); + } #ifdef DUSK_DISCORD_RPC dusk::discord::Initialize(); From 4b3c559df30bb59a073d7bab7d9b522e14655dc1 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Mon, 20 Apr 2026 20:47:03 -0600 Subject: [PATCH 61/68] CMake: Don't build a game static lib --- CMakeLists.txt | 8 +------- files.cmake | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e8132a98fd..77cd0b9694 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -399,12 +399,6 @@ target_include_directories(game_base PRIVATE ${GAME_INCLUDE_DIRS}) target_link_libraries(game_debug PRIVATE ${GAME_LIBS}) target_link_libraries(game_base PRIVATE ${GAME_LIBS}) -# Combined game library -add_library(game STATIC - $ - $) -target_link_libraries(game PUBLIC ${GAME_LIBS}) - if(ANDROID) add_library(dusk SHARED src/dusk/main.cpp) set_target_properties(dusk PROPERTIES OUTPUT_NAME main) @@ -414,7 +408,7 @@ endif () target_compile_definitions(dusk PRIVATE TARGET_PC AVOID_UB=1 VERSION=0) target_include_directories(dusk PRIVATE include) -target_link_libraries(dusk PRIVATE game aurora::main) +target_link_libraries(dusk PRIVATE game_base game_debug aurora::main) if (TARGET crashpad_handler) add_dependencies(dusk crashpad_handler) endif () diff --git a/files.cmake b/files.cmake index c8ecb9be7a..c50cac2d8c 100644 --- a/files.cmake +++ b/files.cmake @@ -15,7 +15,6 @@ set(DOLZEL_FILES src/m_Do/m_Do_DVDError.cpp src/m_Do/m_Do_MemCard.cpp src/m_Do/m_Do_MemCardRWmng.cpp - src/m_Do/m_Do_machine_exception.cpp src/m_Do/m_Do_hostIO.cpp src/c/c_damagereaction.cpp src/c/c_dylink.cpp From 62f3d09076c117685b73c2f0bec18f6b7aa5b9ad Mon Sep 17 00:00:00 2001 From: madeline Date: Mon, 20 Apr 2026 20:22:06 -0700 Subject: [PATCH 62/68] fix incorrect eldin field name on map --- include/d/d_menu_fmap.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/d/d_menu_fmap.h b/include/d/d_menu_fmap.h index 026e98e056..48db37fef3 100644 --- a/include/d/d_menu_fmap.h +++ b/include/d/d_menu_fmap.h @@ -75,7 +75,9 @@ public: /* 0x8 */ BE(u16) mAreaName; /* 0xA */ u8 mCount; #ifdef _MSVC_LANG - u8* __get_mRoomNos() const { return (u8*)(this + 1); } + // Room numbers start at offset 0xB (right after mCount), NOT at sizeof(data)=12. + // (u8*)(this+1) would give offset 12 because MSVC sizeof=12; use &mCount+1 instead. + u8* __get_mRoomNos() const { return (u8*)&mCount + 1; } __declspec(property(get = __get_mRoomNos)) u8* mRoomNos; #else /* 0xB */ u8 mRoomNos[0]; From 46f6dc67c1a47f38731602cbef04e906e34c67b4 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Tue, 21 Apr 2026 00:46:22 -0700 Subject: [PATCH 63/68] make file select card wait times obey instantSaves setting --- src/d/d_file_select.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index 23ceb96f00..17d5f2c4e4 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -70,11 +70,7 @@ dFs_HIO_c::dFs_HIO_c() { select_icon_appear_frames = 5; appear_display_wait_frames = 15; field_0x000d = 15; - #if TARGET_PC - card_wait_frames = 0; - #else card_wait_frames = 90; - #endif test_frame_counts[0] = 1.11f; test_frame_counts[1] = 1.11f; test_frame_counts[2] = 1.11f; @@ -2367,7 +2363,7 @@ void dFile_select_c::CommandExec() { break; } - mWaitTimer = g_fsHIO.card_wait_frames; + mWaitTimer = IF_DUSK(dusk::getSettings().game.instantSaves ? 0 :) g_fsHIO.card_wait_frames; } void dFile_select_c::DataEraseWait() { @@ -4759,7 +4755,7 @@ void dFile_select_c::MemCardFormatYesSel2Disp() { bool isErrorTxtChange = errorTxtChangeAnm(); bool isYnMenuMove = yesnoMenuMoveAnm(); if (isErrorTxtChange == true && isYnMenuMove == true) { - mWaitTimer = g_fsHIO.card_wait_frames; + mWaitTimer = IF_DUSK(dusk::getSettings().game.instantSaves ? 0 :) g_fsHIO.card_wait_frames; mDoMemCd_Format(); mCardCheckProc = MEMCARDCHECKPROC_FORMAT; } @@ -4830,7 +4826,7 @@ void dFile_select_c::MemCardMakeGameFileSelDisp() { if (isErrorTxtChange == true && isYnMenuMove == true && isKetteiTxtDisp == true) { if (field_0x0268 != 0) { - mWaitTimer = g_fsHIO.card_wait_frames; + mWaitTimer = IF_DUSK(dusk::getSettings().game.instantSaves ? 0 :) g_fsHIO.card_wait_frames; setInitSaveData(); dataSave(); mCardCheckProc = MEMCARDCHECKPROC_MAKE_GAMEFILE; From 18d70df188c451d0845514424a9aedafba66fed8 Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Tue, 21 Apr 2026 12:17:56 -0400 Subject: [PATCH 64/68] Small Imgui changes for better visibility by end user (#473) Co-authored-by: MelonSpeedruns --- src/dusk/imgui/ImGuiMenuGame.cpp | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index 691434427c..f41da74b7f 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -67,7 +67,7 @@ namespace dusk { ToggleFullscreen(); } - if (ImGui::MenuItem("Restore Default Window Size")) { + if (ImGui::Button("Restore Default Window Size")) { getSettings().video.enableFullscreen.setValue(false); VISetWindowFullscreen(false); VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2); @@ -75,6 +75,8 @@ namespace dusk { } } + ImGui::Separator(); + bool vsync = getSettings().video.enableVsync; if (ImGui::Checkbox("Enable VSync", &vsync)) { getSettings().video.enableVsync.setValue(vsync); @@ -312,14 +314,14 @@ namespace dusk { void ImGuiMenuGame::drawAudioMenu() { if (ImGui::BeginMenu("Audio")) { + + ImGui::SeparatorText("Volume"); + ImGui::Text("Master Volume"); if (config::ImGuiSliderInt("##masterVolume", getSettings().audio.masterVolume, 0, 100)) { dusk::audio::SetMasterVolume(getSettings().audio.masterVolume / 100.0f); } - if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) { - dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); - } /* // TODO: Implement additional settings ImGui::Text("Main Music Volume"); @@ -339,6 +341,13 @@ namespace dusk { } */ + ImGui::SeparatorText("Effects"); + + if (config::ImGuiCheckbox("Enable Reverb", getSettings().audio.enableReverb)) { + dusk::audio::SetEnableReverb(getSettings().audio.enableReverb); + } + + ImGui::SeparatorText("Tweaks"); config::ImGuiCheckbox("No Low HP Sound", getSettings().game.noLowHpSound); @@ -359,7 +368,11 @@ namespace dusk { if (ImGui::BeginMenu("Input")) { ImGui::SeparatorText("Controller"); - ImGui::MenuItem("Configure Controller", nullptr, &m_showControllerConfig); + if (ImGui::Button("Configure Controller")){ + m_showControllerConfig = !m_showControllerConfig; + } + + ImGui::SeparatorText("Camera"); config::ImGuiCheckbox("Invert Camera X Axis", getSettings().game.invertCameraXAxis); From 595a6f1c9efc6fd7a92a7c2da0dd7e1743a60d9a Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 21 Apr 2026 14:52:16 -0600 Subject: [PATCH 65/68] Enable DoF (+ setting) & fix texture cache leak --- extern/aurora | 2 +- include/dusk/settings.h | 1 + src/dusk/imgui/ImGuiMenuGame.cpp | 2 ++ src/dusk/settings.cpp | 2 ++ src/m_Do/m_Do_graphic.cpp | 3 +++ 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 5d420c9f73..ccb9dc1ad7 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 5d420c9f73c93ab9a5dcd052ac92d47362764e80 +Subproject commit ccb9dc1ad78f7e278af19d308a4f3e870ca04889 diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 03749967c9..2cec252006 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -77,6 +77,7 @@ struct UserSettings { ConfigVar enableFrameInterpolation; ConfigVar internalResolutionScale; ConfigVar shadowResolutionMultiplier; + ConfigVar enableDepthOfField; // Audio ConfigVar noLowHpSound; diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp index f41da74b7f..448bd531ff 100644 --- a/src/dusk/imgui/ImGuiMenuGame.cpp +++ b/src/dusk/imgui/ImGuiMenuGame.cpp @@ -165,6 +165,8 @@ namespace dusk { ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias); + config::ImGuiCheckbox("Enable Depth of Field", getSettings().game.enableDepthOfField); + ImGui::EndMenu(); } } diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 2bcdbe2185..aacca0dbc9 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -51,6 +51,7 @@ UserSettings g_userSettings = { .enableFrameInterpolation = {"game.enableFrameInterpolation", false}, .internalResolutionScale {"game.internalResolutionScale", 0}, .shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1}, + .enableDepthOfField {"game.enableDepthOfField", true}, // Audio .noLowHpSound {"game.noLowHpSound", false}, @@ -143,6 +144,7 @@ void registerSettings() { Register(g_userSettings.game.disableWaterRefraction); Register(g_userSettings.game.internalResolutionScale); Register(g_userSettings.game.shadowResolutionMultiplier); + Register(g_userSettings.game.enableDepthOfField); Register(g_userSettings.game.enableFastIronBoots); Register(g_userSettings.game.canTransformAnywhere); Register(g_userSettings.game.freeMagicArmor); diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index d949d10ff0..4dc505ac03 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -1155,6 +1155,9 @@ static void drawDepth2(view_class* param_0, view_port_class* param_1, int param_ GXSetProjection(ortho, GX_ORTHOGRAPHIC); GXSetCurrentMtx(0); +#ifdef TARGET_PC + if (dusk::getSettings().game.enableDepthOfField) +#endif if (l_tevColor0.a > -255 && sp8 == 1) { GXBegin(GX_QUADS, GX_VTXFMT0, 4); GXPosition3s16(x_orig, y_orig_pos, -5); From dd3a61d84cb3e80c0424b21278299b87eefc4797 Mon Sep 17 00:00:00 2001 From: TakaRikka Date: Tue, 21 Apr 2026 15:13:55 -0700 Subject: [PATCH 66/68] fix dmap background --- src/d/d_menu_dmap.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/d/d_menu_dmap.cpp b/src/d/d_menu_dmap.cpp index 66533dd12c..17d9193241 100644 --- a/src/d/d_menu_dmap.cpp +++ b/src/d/d_menu_dmap.cpp @@ -984,7 +984,36 @@ void dMenu_DmapBg_c::update() { JUT_ASSERT(2323, mpBackTexture != NULL); void* spec = mpArchive->getResource("spec/spec.dat"); + #if TARGET_PC + struct dmap_spec { + /* 0x00 */ BE(f32) field_0x0; + /* 0x04 */ BE(f32) field_0x4; + /* 0x08 */ BE(f32) field_0x8; + /* 0x0C */ u8 field_0xc; + /* 0x0D */ u8 field_0xd; + /* 0x0E */ u8 field_0xe; + /* 0x0F */ u8 field_0xf; + /* 0x10 */ u8 field_0x10; + /* 0x11 */ u8 field_0x11; + /* 0x12 */ u8 field_0x12; + /* 0x13 */ u8 field_0x13; + }; + dmap_spec* dspec = (dmap_spec*)spec; + + field_0xd80 = dspec->field_0x0; + field_0xd84 = dspec->field_0x4; + field_0xd88 = dspec->field_0x8; + field_0xd8c = dspec->field_0xc; + field_0xd8d = dspec->field_0xd; + field_0xd8e = dspec->field_0xe; + field_0xd8f = dspec->field_0xf; + field_0xd90 = dspec->field_0x10; + field_0xd91 = dspec->field_0x11; + field_0xd92 = dspec->field_0x12; + field_0xd93 = dspec->field_0x13; + #else memcpy(&field_0xd80, spec, 20); + #endif } } From d99205ecc616ff855f250b497eb42ce6e326b250 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 21 Apr 2026 17:12:21 -0600 Subject: [PATCH 67/68] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index ccb9dc1ad7..b524038d75 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit ccb9dc1ad78f7e278af19d308a4f3e870ca04889 +Subproject commit b524038d75444519f5ab685ef37da12300eab4ed From a15d0af139d7dd8ce5fdd110d28c2f4637245ae3 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 21 Apr 2026 17:39:53 -0600 Subject: [PATCH 68/68] Better fix for mirror crashes --- extern/aurora | 2 +- src/d/actor/d_a_mirror.cpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index b524038d75..26da4c6bb0 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit b524038d75444519f5ab685ef37da12300eab4ed +Subproject commit 26da4c6bb08937ab07b5bd8bbf779b143c672a88 diff --git a/src/d/actor/d_a_mirror.cpp b/src/d/actor/d_a_mirror.cpp index 4711dee681..fb4142c0bb 100644 --- a/src/d/actor/d_a_mirror.cpp +++ b/src/d/actor/d_a_mirror.cpp @@ -30,6 +30,10 @@ static char* l_arcName = "Mirror"; static char* l_arcName2 = "MR-Table"; dMirror_packet_c::dMirror_packet_c() { +#ifdef TARGET_PC + GXInitTexObj(&mTexObj, nullptr, 0, 0, static_cast(-1), GX_MAX_TEXWRAPMODE, + GX_MAX_TEXWRAPMODE, GX_FALSE); +#endif reset(); }