Compare commits

..

14 Commits

Author SHA1 Message Date
Luke Street 319efbe662 Reset game clock with over 250ms frame gap 2026-04-22 00:30:11 -06:00
Luke Street 6f34bb050a Call J3DModel::diff in mDoExt_modelEntryDL when interpolating
Fixes #355
2026-04-22 00:17:52 -06:00
Irastris a2a56122e2 Gyro: Hawk Aiming 2026-04-22 01:46:10 -04:00
Irastris 58f2679def Rollgoal: Gyro & Mirror Mode Fixes 2026-04-22 01:41:35 -04:00
Luke Street 396ea02fe5 Fix flowers with interpolation on (#486) 2026-04-21 23:33:03 -06:00
TakaRikka 8100ddb990 Merge pull request #485 from TwilitRealm/frame-pacing-2
Rework interpolation pacing; interpolate every frame
2026-04-21 22:05:33 -07:00
Luke Street d78c46a628 Rework interpolation pacing; interpolate every frame 2026-04-21 22:45:18 -06:00
Phillip Stephens 1e93657ab5 Update aurora for fix 2026-04-21 19:25:28 -07:00
madeline 366e47245e make sun song play the correct notes 2026-04-21 19:23:53 -07:00
madeline 4c53ba91be Merge branch 'main' of https://github.com/TakaRikka/dusk 2026-04-21 18:59:23 -07:00
madeline cf080523cb dsp oscillator channels fixes #471 fixes #131 2026-04-21 18:59:11 -07:00
Luke Street a15d0af139 Better fix for mirror crashes 2026-04-21 17:39:53 -06:00
Luke Street d99205ecc6 Update aurora 2026-04-21 17:12:28 -06:00
TakaRikka dd3a61d84c fix dmap background 2026-04-21 15:13:55 -07:00
16 changed files with 277 additions and 92 deletions
+1 -1
+1
View File
@@ -16,6 +16,7 @@ void ensure_initialized();
void begin_record();
void end_record();
void begin_sim_tick();
void begin_frame(bool enabled, bool is_sim_frame, float step);
void interpolate();
float get_interpolation_step();
+6 -13
View File
@@ -1,13 +1,8 @@
#ifndef DUSK_GAME_CLOCK_H
#define DUSK_GAME_CLOCK_H
#pragma once
#include <stddef.h>
namespace dusk {
namespace game_clock {
namespace dusk::game_clock {
void ensure_initialized();
void reset_accumulator();
void reset_frame_timer();
constexpr float sim_pace() { return 1.0f / 30.0f; }
@@ -18,16 +13,14 @@ 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;
int sim_ticks_to_run;
float sim_pace;
};
MainLoopPacer advance_main_loop();
void commit_sim_tick();
float sample_interpolation_step();
float consume_interval(const void* consumer);
} // namespace game_clock
} // namespace dusk
#endif // DUSK_GAME_CLOCK_H
} // namespace dusk::game_clock
+5 -5
View File
@@ -117,8 +117,8 @@ static Z2WolfHowlLine sNewSong3[9] = {
#if TARGET_PC
static Z2WolfHowlLine sHowlTimeSong[6] = {
{HOWL_LINE_MID, 20}, {HOWL_LINE_LOW, 20}, {HOWL_LINE_HIGH, 40},
{HOWL_LINE_MID, 20}, {HOWL_LINE_LOW, 20}, {HOWL_LINE_HIGH, 40},
{HOWL_LINE_MID, 15}, {HOWL_LINE_LOW, 15}, {HOWL_LINE_HIGH, 30},
{HOWL_LINE_MID, 15}, {HOWL_LINE_LOW, 15}, {HOWL_LINE_HIGH, 30},
};
#endif
@@ -368,9 +368,9 @@ void Z2WolfHowlMgr::setCorrectData(s8 curveID, Z2WolfHowlData* data) {
break;
#if TARGET_PC
case Z2WOLFHOWL_TIMESONG:
cPitchUp = 1.259906f;
cPitchCenter = 0.94387f;
cPitchDown = 0.840885f;
cPitchUp = 1.3348f;
cPitchCenter = 1.0f;
cPitchDown = 0.7937f;
break;
#endif
default:
+1
View File
@@ -154,6 +154,7 @@ bool daAlink_c::checkGyroAimContext() {
case PROC_BOW_SUBJECT:
case PROC_BOOMERANG_SUBJECT:
case PROC_COPY_ROD_SUBJECT:
case PROC_HAWK_SUBJECT:
case PROC_HOOKSHOT_SUBJECT:
case PROC_SWIM_HOOKSHOT_SUBJECT:
case PROC_HORSE_BOW_SUBJECT:
+10 -10
View File
@@ -761,6 +761,11 @@ static void koro2_game(fshop_class* i_this) {
sp5C.x = mDoCPd_c::getStickX3D(PAD_1);
sp5C.y = 0.0f;
sp5C.z = mDoCPd_c::getStickY(PAD_1);
#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
sp5C.x = -sp5C.x;
}
#endif
MtxPosition(&sp5C, &sp68);
f32 reg_f31 = sp68.x;
@@ -782,20 +787,15 @@ static void koro2_game(fshop_class* i_this) {
reg_f30 = 0.0f;
}
s16 gyro_ax = 0;
s16 gyro_az = 0;
#if TARGET_PC
if (dusk::getSettings().game.enableGyroRollgoal) {
s16 rg_add_x;
s16 rg_add_z;
dusk::gyro::rollgoalTableOffset(rg_add_x, rg_add_z);
s16 tgt_x = static_cast<s16>(reg_f30 * (-6000.0f + JREG_F(7))) + rg_add_x;
s16 tgt_z = static_cast<s16>(reg_f31 * (-6000.0f + JREG_F(8))) + rg_add_z;
cLib_addCalcAngleS2(&i_this->field_0x4020.x, tgt_x, 4, 0x200);
cLib_addCalcAngleS2(&i_this->field_0x4020.z, tgt_z, 4, 0x200);
dusk::gyro::rollgoalTableOffset(gyro_ax, gyro_az);
}
#else
cLib_addCalcAngleS2(&i_this->field_0x4020.x, reg_f30 * (-6000.0f + JREG_F(7)), 4, 0x200);
cLib_addCalcAngleS2(&i_this->field_0x4020.z, reg_f31 * (-6000.0f + JREG_F(8)), 4, 0x200);
#endif
cLib_addCalcAngleS2(&i_this->field_0x4020.x, reg_f30 * (-6000.0f + JREG_F(7)) + gyro_ax, 4, 0x200);
cLib_addCalcAngleS2(&i_this->field_0x4020.z, reg_f31 * (-6000.0f + JREG_F(8)) + gyro_az, 4, 0x200);
}
#if TARGET_PC
if (i_this->field_0x4010 != 2) {
+4
View File
@@ -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<GXTexFmt>(-1), GX_MAX_TEXWRAPMODE,
GX_MAX_TEXWRAPMODE, GX_FALSE);
#endif
reset();
}
+6 -9
View File
@@ -699,8 +699,8 @@ void dFlower_packet_c::draw() {
if (!cLib_checkBit<u8>(sp44->m_state, 4) && !cLib_checkBit<u8>(sp44->m_state, 0x40)) {
#ifdef TARGET_PC
Mtx flower_mtx;
if (dusk::frame_interp::lookup_replacement(reinterpret_cast<const void*>(&sp44->m_modelMtx), flower_mtx)) {
if (dusk::frame_interp::lookup_replacement(&sp44->m_modelMtx, flower_mtx)) {
cMtx_concat(j3dSys.getViewMtx(), flower_mtx, flower_mtx);
GXLoadPosMtxImm(flower_mtx, 0);
} else
#endif
@@ -854,21 +854,18 @@ void dFlower_packet_c::draw() {
if (!cLib_checkBit<u8>(sp34->m_state, 4) && cLib_checkBit<u8>(sp34->m_state, 0x40)) {
#ifdef TARGET_PC
Mtx flower_mtx;
if (dusk::frame_interp::lookup_replacement(reinterpret_cast<const void*>(&sp34->m_modelMtx), flower_mtx)) {
if (dusk::frame_interp::lookup_replacement(&sp34->m_modelMtx, flower_mtx)) {
cMtx_concat(j3dSys.getViewMtx(), flower_mtx, flower_mtx);
GXLoadPosMtxImm(flower_mtx, 0);
} else {
} else
#endif
{
GXLoadPosMtxImm(sp34->m_modelMtx, 0);
#ifdef TARGET_PC
}
#endif
GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0);
#if TARGET_PC
GXLoadTexObj(&mTexObj_l_J_Ohana01_64128_0419TEX, GX_TEXMAP0);
#endif
if (!cLib_checkBit<u8>(sp34->m_state, 8)) {
if (!cLib_checkBit<u8>(sp34->m_state, 0x10)) {
GXCallDisplayList(mp_Jhana01DL, m_Jhana01DL_size);
@@ -995,7 +992,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(mDoMtx_stack_c::get(), data_p->m_modelMtx);
dusk::frame_interp::record_final_mtx(temp_r28, data_p->m_modelMtx);
#endif
}
}
+29
View File
@@ -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
}
}
+132 -10
View File
@@ -5,14 +5,15 @@
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdio>
#include <span>
#include "Adpcm.hpp"
#include "freeverb/revmodel.hpp"
#include "JSystem/JAudio2/JASDriverIF.h"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/endian.h"
#include "dusk/logging.h"
#include "global.h"
#include "tracy/Tracy.hpp"
@@ -95,6 +96,13 @@ static void RenderChannel(
ChannelAuxData& channelAux,
OutputSubframe& subframe);
static void RenderOutputChannel(
const JASDsp::TChannel& sourceChannel,
ChannelAuxData& aux,
OutputChannel outputChannel,
const std::span<f32> inputSamples,
OutputSubframe& fullOutputSubframe);
/**
* Converts a pitch value on a DSP channel to a sample rate.
*/
@@ -117,6 +125,8 @@ static void ResetChannel(JASDsp::TChannel& channel, ChannelAuxData& aux) {
aux.resamplePos = 0.0;
aux.resamplePrev = 0;
aux.oscPhase = 0;
aux.prev_lp_out = 0.0f;
aux.prev_lp_in = 0.0f;
@@ -141,6 +151,119 @@ static void MixSubframe(DspSubframe& dst, const DspSubframe& src) {
}
}
enum class OscType : u16 {
SQUARE_WAVE_PW_50 = 0,
SAW_WAVE = 1,
SQUARE_WAVE_PW_25 = 3,
TRIANGLE_WAVE = 4,
// idk what 5 and 6 are
SINE_WAVE = 7,
// idk what 8 and 9 are
SINE_WAVE_VAR_STEP = 10,
EVOLVING_HARMONIC = 11,
EVOLVING_RAMP = 12,
};
static s16 gEvolvingHarmonic[64];
static void GenerateEvolvingHarmonic() {
static bool initialized = false;
if (!initialized) {
gEvolvingHarmonic[62] = 8191;
gEvolvingHarmonic[63] = 16383;
initialized = true;
}
u32 prev2 = (u32)gEvolvingHarmonic[62];
u32 prev1 = (u32)gEvolvingHarmonic[63];
for (int i = 0; i < 64; i += 2) {
u32 cur = (u32)gEvolvingHarmonic[i];
gEvolvingHarmonic[i] = (s16)((s32)(prev2 * prev1 - (cur << 16)) >> 16);
prev2 = prev1;
prev1 = cur;
cur = (u32)gEvolvingHarmonic[i + 1];
gEvolvingHarmonic[i + 1] = (s16)((s32)(2u * (prev2 * prev1 + (cur << 16))) >> 16);
prev2 = prev1;
prev1 = cur;
}
}
static void RenderOscChannel(
JASDsp::TChannel& channel,
ChannelAuxData& channelAux,
OutputSubframe& subframe) {
if (channel.mResetFlag)
ResetChannel(channel, channelAux);
const u32 pitch = channel.mPitch;
DspSubframe buf = {};
const auto oscType = static_cast<OscType>(channel.mBytesPerBlock);
switch (oscType) {
case OscType::SQUARE_WAVE_PW_50: {
std::generate(buf.begin(), buf.end(), [&] {
f32 s = channelAux.oscPhase < 0x8000u ? 0.5f : -0.5f;
channelAux.oscPhase += pitch >> 1;
return s;
});
break;
}
case OscType::SQUARE_WAVE_PW_25: {
std::generate(buf.begin(), buf.end(), [&] {
f32 s = channelAux.oscPhase < 0x4000u ? 0.5f : -0.5f;
channelAux.oscPhase += pitch >> 1;
return s;
});
break;
}
case OscType::SAW_WAVE:
case OscType::EVOLVING_RAMP: {
std::generate(buf.begin(), buf.end(), [&] {
f32 s = (f32)(s16)channelAux.oscPhase / 32768.0f;
channelAux.oscPhase += pitch >> 1;
return s;
});
break;
}
case OscType::SINE_WAVE:
case OscType::SINE_WAVE_VAR_STEP: {
std::generate(buf.begin(), buf.end(), [&] {
f32 s = sinf((f32)channelAux.oscPhase * (2.0f * M_PI / 65536.0f)) * 0.5f;
channelAux.oscPhase += pitch >> 1;
return s;
});
break;
}
case OscType::TRIANGLE_WAVE: {
std::generate(buf.begin(), buf.end(), [&] {
f32 s = 0.5f - fabsf((f32)(s16)channelAux.oscPhase / 32768.0f);
channelAux.oscPhase += pitch >> 1;
return s;
});
break;
}
case OscType::EVOLVING_HARMONIC: {
std::generate(buf.begin(), buf.end(), [&] {
f32 s = gEvolvingHarmonic[channelAux.oscPhase >> 10] / 32768.0f;
channelAux.oscPhase += pitch >> 1;
return s;
});
break;
}
default:
DuskLog.error("RenderOscChannel: unimplemented oscillator type {}", channel.mBytesPerBlock);
break;
}
auto samples = std::span(buf).subspan(0, DSP_SUBFRAME_SIZE);
RenderOutputChannel(channel, channelAux, OutputChannel::LEFT, samples, subframe);
RenderOutputChannel(channel, channelAux, OutputChannel::RIGHT, samples, subframe);
}
void dusk::audio::DspRender(OutputSubframe& subframe) {
ZoneScoped;
if (DumpAudio != sDumpWasActive) {
@@ -152,6 +275,8 @@ void dusk::audio::DspRender(OutputSubframe& subframe) {
}
}
GenerateEvolvingHarmonic();
std::span channels(JASDsp::CH_BUF, DSP_CHANNELS);
DspSubframe reverbInputL = {};
@@ -174,17 +299,14 @@ void dusk::audio::DspRender(OutputSubframe& subframe) {
channel.mIsFinished = true;
continue;
}
else if (channel.mWaveAramAddress == 0) {
// I think these are oscillator channels? Not backed by audio.
// No idea how to implement these yet, so skip them.
channel.mIsFinished = true;
continue;
}
ValidateChannel(channel);
OutputSubframe channelSubframe = {};
RenderChannel(channel, channelAux, channelSubframe);
if (channel.mWaveAramAddress == 0) {
RenderOscChannel(channel, channelAux, channelSubframe);
} else {
ValidateChannel(channel);
RenderChannel(channel, channelAux, channelSubframe);
}
if (EnableReverb) {
// scale the input to the reverb rather than using wet/dry on the output.
+3
View File
@@ -53,6 +53,9 @@ namespace dusk::audio {
// last consumed sample from decodeBuf
s16 resamplePrev;
// phase of oscillator channels
u16 oscPhase;
// low pass previous state
f32 prev_lp_out; // out[n-1]
f32 prev_lp_in; // in[n-1]
+10 -4
View File
@@ -127,14 +127,20 @@ void ensure_initialized() {
s_initialized = true;
}
void begin_sim_tick() {
ensure_initialized();
if (!g_enabled) {
return;
}
s_interpolationCallBackWork.clear();
s_cam_prev = std::move(s_cam_curr);
}
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();
s_cam_prev = std::move(s_cam_curr);
}
}
bool is_enabled() {
+44 -23
View File
@@ -5,62 +5,84 @@
#include <cmath>
#include <unordered_map>
namespace dusk {
namespace game_clock {
namespace dusk::game_clock {
using clock = std::chrono::steady_clock;
bool s_initialized = false;
clock::time_point s_previous_sample{};
float s_sim_accumulator = 0.0f;
clock::time_point s_current_snapshot_time{};
std::unordered_map<uintptr_t, clock::time_point> s_interval_last_sample;
constexpr clock::duration kSimPeriodDuration =
std::chrono::duration_cast<clock::duration>(std::chrono::duration<float>(sim_pace()));
constexpr clock::duration kAbnormalGapResetThreshold = std::chrono::milliseconds(250);
constexpr int kMaxSimTicksPerFrame = 2;
void ensure_initialized() {
if (s_initialized) {
return;
}
s_previous_sample = clock::now();
s_sim_accumulator = sim_pace();
s_current_snapshot_time = s_previous_sample;
s_initialized = true;
}
void reset_accumulator() {
ensure_initialized();
s_sim_accumulator = fmodf(s_sim_accumulator, sim_pace());
}
void reset_frame_timer() {
s_previous_sample = clock::now();
s_sim_accumulator = 0.0f;
s_current_snapshot_time = s_previous_sample - kSimPeriodDuration;
}
MainLoopPacer advance_main_loop() {
ensure_initialized();
const clock::time_point now = clock::now();
const float presentation_dt = std::chrono::duration<float>(now - s_previous_sample).count();
const clock::duration frame_gap = now - s_previous_sample;
const float presentation_dt = std::chrono::duration<float>(frame_gap).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;
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();
s_current_snapshot_time = now;
out.sim_ticks_to_run = 1;
return out;
}
if (frame_gap > kAbnormalGapResetThreshold) {
s_current_snapshot_time = now - kSimPeriodDuration;
out.sim_ticks_to_run = 0;
return out;
}
int sim_ticks_to_run = 0;
clock::time_point projected_snapshot_time = s_current_snapshot_time;
const clock::time_point render_time = now - kSimPeriodDuration;
while (sim_ticks_to_run < kMaxSimTicksPerFrame && projected_snapshot_time < render_time) {
projected_snapshot_time += kSimPeriodDuration;
sim_ticks_to_run++;
}
out.sim_ticks_to_run = sim_ticks_to_run;
return out;
}
void commit_sim_tick() {
ensure_initialized();
s_current_snapshot_time += kSimPeriodDuration;
}
float sample_interpolation_step() {
ensure_initialized();
const float step =
std::chrono::duration<float>(clock::now() - s_current_snapshot_time).count() / sim_pace();
return std::clamp(step, 0.0f, 1.0f);
}
float consume_interval(const void* consumer) {
@@ -78,5 +100,4 @@ float consume_interval(const void* consumer) {
return dt;
}
} // namespace game_clock
} // namespace dusk
} // namespace dusk::game_clock
+1 -1
View File
@@ -3,7 +3,7 @@
namespace dusk::gyro {
namespace {
constexpr s32 kRollgoalTableMaxOffset = 12000;
constexpr s32 kRollgoalTableMaxOffset = 6500;
constexpr float kGyroEmaAlphaMin = 0.05f;
constexpr float kGyroEmaAlphaMax = 1.0f;
+6 -1
View File
@@ -351,8 +351,13 @@ void mDoExt_modelUpdateDL(J3DModel* i_model) {
void mDoExt_modelEntryDL(J3DModel* i_model) {
#if TARGET_PC
if (!dusk::frame_interp::is_sim_frame())
if (!dusk::frame_interp::is_sim_frame()) {
// FRAME INTERP NOTE: This fixes issue #355 where some lights would flicker.
// This is likely better solved by updating J3DMaterial::needsInterpCallBack,
// but it's unclear what exactly needs to be added.
i_model->diff();
return;
}
#endif
modelMtxErrorCheck(i_model);
+18 -15
View File
@@ -242,8 +242,6 @@ void main01(void) {
continue;
}
const dusk::game_clock::MainLoopPacer pacing = dusk::game_clock::advance_main_loop();
VIWaitForRetrace();
dusk::lastFrameAuroraStats = *aurora_get_stats();
@@ -254,28 +252,33 @@ void main01(void) {
mDoGph_gInf_c::updateRenderSize();
dusk::frame_interp::begin_frame(pacing.is_interpolating, pacing.do_sim_tick, pacing.interpolation_step);
const auto pacing = dusk::game_clock::advance_main_loop();
if (pacing.is_interpolating) {
if (pacing.do_sim_tick) {
if (pacing.sim_ticks_to_run > 0) {
dusk::frame_interp::begin_frame(true, true, 0.0f);
dusk::frame_interp::set_ui_tick_pending(true);
mDoCPd_c::read();
DuskDebugPad();
dusk::gyro::read(pacing.sim_pace);
fapGm_Execute();
mDoAud_Execute();
dusk::game_clock::reset_accumulator();
for (int sim_tick = 0; sim_tick < pacing.sim_ticks_to_run; ++sim_tick) {
dusk::frame_interp::begin_sim_tick();
mDoCPd_c::read();
DuskDebugPad();
dusk::gyro::read(pacing.sim_pace);
fapGm_Execute();
mDoAud_Execute();
dusk::game_clock::commit_sim_tick();
}
}
dusk::frame_interp::begin_frame(true, false,
dusk::game_clock::sample_interpolation_step());
dusk::frame_interp::interpolate();
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);
}
// run draw functions for anything specially marked to handle interp
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::begin_frame(false, true, 0.0f);
dusk::frame_interp::set_ui_tick_pending(true);
// Game Inputs