Implement frame limiter. (#60)

* Implement audio timing with integer math.

* Add busy loop to audio thread.

* Implement a frame limiter.

* Implement implot.

* Add more stuff to the profiler window.

* Redo frame limiter logic to fix drifting.

* Move frame limiter, add limiters for SFD & loading screen.

* Update waiting logic for audio thread.

* Correct small delta time errors.

* Decrease delta time error threshold.

* Set busy wait threshold to 2ms.

* Change spin wait in D3D12 present to infinite wait.

* Replace FPS literals with constants.
This commit is contained in:
Skyth (Asilkan) 2024-12-22 19:58:06 +03:00 committed by GitHub
parent 314a092747
commit f1416c85ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 211 additions and 31 deletions

5
.gitmodules vendored
View file

@ -57,4 +57,7 @@
url = https://github.com/martinus/unordered_dense.git
[submodule "thirdparty/SDL_mixer"]
path = thirdparty/SDL_mixer
url = https://github.com/libsdl-org/SDL_mixer
url = https://github.com/libsdl-org/SDL_mixer
[submodule "thirdparty/implot"]
path = thirdparty/implot
url = https://github.com/epezent/implot.git

View file

@ -196,11 +196,14 @@ set(SWA_USER_CXX_SOURCES
set(SWA_THIRDPARTY_SOURCES
"${SWA_THIRDPARTY_ROOT}/imgui/backends/imgui_impl_sdl2.cpp"
"${SWA_THIRDPARTY_ROOT}/imgui/imgui.cpp"
"${SWA_THIRDPARTY_ROOT}/imgui/imgui.cpp"
"${SWA_THIRDPARTY_ROOT}/imgui/imgui_demo.cpp"
"${SWA_THIRDPARTY_ROOT}/imgui/imgui_draw.cpp"
"${SWA_THIRDPARTY_ROOT}/imgui/imgui_tables.cpp"
"${SWA_THIRDPARTY_ROOT}/imgui/imgui_widgets.cpp"
"${SWA_THIRDPARTY_ROOT}/implot/implot.cpp"
"${SWA_THIRDPARTY_ROOT}/implot/implot_demo.cpp"
"${SWA_THIRDPARTY_ROOT}/implot/implot_items.cpp"
"${SWA_THIRDPARTY_ROOT}/libmspack/libmspack/mspack/lzxd.c"
"${SWA_THIRDPARTY_ROOT}/tiny-AES-c/aes.c"
"${SWA_TOOLS_ROOT}/ShaderRecomp/thirdparty/smol-v/source/smolv.cpp"
@ -209,7 +212,8 @@ set(SWA_THIRDPARTY_SOURCES
set(SWA_THIRDPARTY_INCLUDES
"${SWA_THIRDPARTY_ROOT}/concurrentqueue"
"${SWA_THIRDPARTY_ROOT}/ddspp"
"${SWA_THIRDPARTY_ROOT}/imgui"
"${SWA_THIRDPARTY_ROOT}/imgui"
"${SWA_THIRDPARTY_ROOT}/implot"
"${SWA_THIRDPARTY_ROOT}/libmspack/libmspack/mspack"
"${SWA_THIRDPARTY_ROOT}/magic_enum/include"
"${SWA_THIRDPARTY_ROOT}/stb"

View file

@ -38,6 +38,14 @@ PPC_FUNC(sub_824EB490)
PPC_FUNC_IMPL(__imp__sub_822C1130);
PPC_FUNC(sub_822C1130)
{
// Correct small delta time errors.
if (Config::FPS >= FPS_MIN && Config::FPS < FPS_MAX)
{
double targetDeltaTime = 1.0 / Config::FPS;
if (abs(ctx.f1.f64 - targetDeltaTime) < 0.00001)
ctx.f1.f64 = targetDeltaTime;
}
App::s_deltaTime = ctx.f1.f64;
SDL_PumpEvents();
@ -48,3 +56,4 @@ PPC_FUNC(sub_822C1130)
__imp__sub_822C1130(ctx, base);
}

View file

@ -15,3 +15,4 @@ public:
static void Restart(std::vector<std::string> restartArgs = {});
static void Exit();
};

View file

@ -41,10 +41,6 @@ static void AudioThread()
size_t channels = g_downMixToStereo ? 2 : XAUDIO_NUM_CHANNELS;
constexpr double INTERVAL = double(XAUDIO_NUM_SAMPLES) / double(XAUDIO_SAMPLES_HZ);
auto start = std::chrono::steady_clock::now();
size_t iteration = 1;
while (true)
{
uint32_t queuedAudioSize = SDL_GetQueuedAudioSize(g_audioDevice);
@ -57,15 +53,14 @@ static void AudioThread()
g_clientCallback(ctx.ppcContext, reinterpret_cast<uint8_t*>(g_memory.base));
}
auto next = start + std::chrono::duration<double>(iteration * INTERVAL);
auto now = std::chrono::steady_clock::now();
constexpr auto INTERVAL = 1000000000ns * XAUDIO_NUM_SAMPLES / XAUDIO_SAMPLES_HZ;
auto next = now + (INTERVAL - now.time_since_epoch() % INTERVAL);
if ((next - now) > 1s)
next = now;
std::this_thread::sleep_for(std::chrono::floor<std::chrono::milliseconds>(next - now));
std::this_thread::sleep_until(next);
iteration = std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count() / INTERVAL + 1;
while (std::chrono::steady_clock::now() < next)
std::this_thread::yield();
}
}

View file

@ -1265,9 +1265,7 @@ namespace plume {
bool D3D12SwapChain::present(uint32_t textureIndex, RenderCommandSemaphore **waitSemaphores, uint32_t waitSemaphoreCount) {
if (waitableObject != NULL) {
while (WaitForSingleObjectEx(waitableObject, 0, FALSE)) {
std::this_thread::yield();
}
WaitForSingleObject(waitableObject, INFINITE);
}
UINT syncInterval = vsyncEnabled ? 1 : 0;

View file

@ -4,6 +4,7 @@
#include "imgui/imgui_snapshot.h"
#include "imgui/imgui_font_builder.h"
#include <app.h>
#include <bc_diff.h>
#include <cpu/code_cache.h>
#include <cpu/guest_code.h>
@ -1317,6 +1318,7 @@ void Video::CreateHostDevice(bool sdlVideoDefault)
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImPlot::CreateContext();
GameWindow::Init(sdlVideoDefault);
@ -1885,6 +1887,100 @@ static uint32_t HashVertexDeclaration(uint32_t vertexDeclaration)
return vertexDeclaration;
}
static constexpr size_t PROFILER_VALUE_COUNT = 1024;
static size_t g_profilerValueIndex;
struct Profiler
{
std::atomic<double> value;
double values[PROFILER_VALUE_COUNT];
std::chrono::steady_clock::time_point start;
void Begin()
{
start = std::chrono::steady_clock::now();
}
void End()
{
value = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
}
double UpdateAndReturnAverage()
{
values[g_profilerValueIndex] = value;
return std::accumulate(values, values + PROFILER_VALUE_COUNT, 0.0) / PROFILER_VALUE_COUNT;
}
};
static double g_applicationValues[PROFILER_VALUE_COUNT];
static Profiler g_renderDirectorProfiler;
static bool g_profilerVisible;
static bool g_profilerWasToggled;
static void DrawProfiler()
{
bool toggleProfiler = SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_F1] != 0;
if (!g_profilerWasToggled && toggleProfiler)
g_profilerVisible = !g_profilerVisible;
g_profilerWasToggled = toggleProfiler;
if (!g_profilerVisible)
return;
ImFont* font = ImFontAtlasSnapshot::GetFont("FOT-SeuratPro-M.otf");
float defaultScale = font->Scale;
font->Scale = ImGui::GetDefaultFont()->FontSize / font->FontSize;
ImGui::PushFont(font);
if (ImGui::Begin("Profiler", &g_profilerVisible))
{
g_applicationValues[g_profilerValueIndex] = App::s_deltaTime * 1000.0;
double renderDirectorAvg = g_renderDirectorProfiler.UpdateAndReturnAverage();
if (ImPlot::BeginPlot("Frame Time"))
{
ImPlot::SetupAxisLimits(ImAxis_Y1, 0.0, 20.0);
ImPlot::SetupAxis(ImAxis_Y1, "ms", ImPlotAxisFlags_None);
ImPlot::PlotLine<double>("Application", g_applicationValues, PROFILER_VALUE_COUNT, 1.0, 0.0, ImPlotLineFlags_None, g_profilerValueIndex);
ImPlot::PlotLine<double>("Render Director", g_renderDirectorProfiler.values, PROFILER_VALUE_COUNT, 1.0, 0.0, ImPlotLineFlags_None, g_profilerValueIndex);
ImPlot::EndPlot();
}
g_profilerValueIndex = (g_profilerValueIndex + 1) % PROFILER_VALUE_COUNT;
const double applicationAvg = std::accumulate(g_applicationValues, g_applicationValues + PROFILER_VALUE_COUNT, 0.0) / PROFILER_VALUE_COUNT;
ImGui::Text("Average Application: %g ms (%g FPS)", applicationAvg, 1000.0 / applicationAvg);
ImGui::Text("Average Render Director: %g ms (%g FPS)", renderDirectorAvg, 1000.0 / renderDirectorAvg);
O1HeapDiagnostics diagnostics, physicalDiagnostics;
{
std::lock_guard lock(g_userHeap.mutex);
diagnostics = o1heapGetDiagnostics(g_userHeap.heap);
}
{
std::lock_guard lock(g_userHeap.physicalMutex);
physicalDiagnostics = o1heapGetDiagnostics(g_userHeap.physicalHeap);
}
ImGui::Text("Heap Allocated: %d MB", int32_t(diagnostics.allocated / (1024 * 1024)));
ImGui::Text("Physical Heap Allocated: %d MB", int32_t(physicalDiagnostics.allocated / (1024 * 1024)));
auto capabilities = g_device->getCapabilities();
ImGui::Text("Present Wait: %s", capabilities.presentWait ? "Supported" : "Unsupported");
ImGui::Text("Triangle Fan: %s", capabilities.triangleFan ? "Supported" : "Unsupported");
}
ImGui::End();
ImGui::PopFont();
font->Scale = defaultScale;
}
static void DrawImGui()
{
ImGui_ImplSDL2_NewFrame();
@ -1916,6 +2012,8 @@ static void DrawImGui()
MessageWindow::Draw();
ButtonGuide::Draw();
DrawProfiler();
ImGui::Render();
auto drawData = ImGui::GetDrawData();
@ -4077,6 +4175,7 @@ static std::thread g_renderThread([]
while (true)
{
size_t count = g_renderQueue.wait_dequeue_bulk(commands, std::size(commands));
for (size_t i = 0; i < count; i++)
{
auto& cmd = commands[i];
@ -4750,6 +4849,8 @@ PPC_FUNC(sub_8258C8A0)
PPC_FUNC_IMPL(__imp__sub_8258CAE0);
PPC_FUNC(sub_8258CAE0)
{
g_renderDirectorProfiler.Begin();
if (g_needsResize)
{
auto r3 = ctx.r3;
@ -4762,6 +4863,8 @@ PPC_FUNC(sub_8258CAE0)
}
__imp__sub_8258CAE0(ctx, base);
g_renderDirectorProfiler.End();
}
void PostProcessResolutionFix(PPCRegister& r4, PPCRegister& f1, PPCRegister& f2)

View file

@ -1,12 +1,10 @@
#include <cpu/code_cache.h>
#include <cpu/guest_code.h>
#include <api/SWA.h>
#include <ui/game_window.h>
#include <user/config.h>
#include <app.h>
float m_lastLoadingFrameDelta = 0.0f;
std::chrono::high_resolution_clock::time_point m_lastLoadingFrameTime;
void DownForceDeltaTimeFixMidAsmHook(PPCRegister& f0)
{
f0.f64 = 30.0;
@ -79,14 +77,71 @@ void Camera2DSlopeLerpFixMidAsmHook(PPCRegister& t, PPCRegister& deltaTime)
t.f64 = ComputeLerpFactor(t.f64, deltaTime.f64 / 60.0);
}
void LoadingScreenSpeedFixMidAsmHook(PPCRegister& r4)
using namespace std::chrono_literals;
static std::chrono::steady_clock::time_point g_next;
void ApplicationUpdateMidAsmHook()
{
auto now = std::chrono::high_resolution_clock::now();
if (Config::FPS >= FPS_MIN && Config::FPS < FPS_MAX)
{
auto now = std::chrono::steady_clock::now();
m_lastLoadingFrameDelta = std::min(std::chrono::duration<float>(now - m_lastLoadingFrameTime).count(), 1.0f / 15.0f);
m_lastLoadingFrameTime = now;
if (now < g_next)
{
std::this_thread::sleep_for(std::chrono::floor<std::chrono::milliseconds>(g_next - now - 2ms));
auto pDeltaTime = (be<float>*)g_memory.Translate(r4.u32);
while ((now = std::chrono::steady_clock::now()) < g_next)
std::this_thread::yield();
}
else
{
g_next = now;
}
*pDeltaTime = m_lastLoadingFrameDelta;
g_next += 1000000000ns / Config::FPS;
}
}
static std::chrono::steady_clock::time_point g_prev;
bool LoadingUpdateMidAsmHook(PPCRegister& r31)
{
auto now = std::chrono::steady_clock::now();
double deltaTime = std::min(std::chrono::duration<double>(now - g_prev).count(), 1.0 / 15.0);
g_prev = now;
uint8_t* base = reinterpret_cast<uint8_t*>(g_memory.base);
uint32_t application = PPC_LOAD_U32(PPC_LOAD_U32(r31.u32 + 4));
uint32_t update = PPC_LOAD_U32(PPC_LOAD_U32(application) + 20);
g_ppcContext->r3.u32 = application;
g_ppcContext->f1.f64 = deltaTime;
reinterpret_cast<PPCFunc*>(g_codeCache.Find(update))(*g_ppcContext, base);
bool loading = PPC_LOAD_U8(0x83367A4C);
if (loading)
{
now = std::chrono::steady_clock::now();
constexpr auto INTERVAL = 1000000000ns / 30;
auto next = now + (INTERVAL - now.time_since_epoch() % INTERVAL);
std::this_thread::sleep_until(next);
}
return loading;
}
// ADXM_WaitVsync
PPC_FUNC_IMPL(__imp__sub_8312DBF8);
PPC_FUNC(sub_8312DBF8)
{
auto now = std::chrono::steady_clock::now();
constexpr auto INTERVAL = 1000000000ns / 60;
auto next = now + (INTERVAL - now.time_since_epoch() % INTERVAL);
std::this_thread::sleep_for(std::chrono::floor<std::chrono::milliseconds>(next - now - 1ms));
while (std::chrono::steady_clock::now() < next)
std::this_thread::yield();
}

View file

@ -39,6 +39,7 @@ using Microsoft::WRL::ComPtr;
#include <SDL_mixer.h>
#include <imgui.h>
#include <imgui_internal.h>
#include <implot.h>
#include <backends/imgui_impl_sdl2.h>
#include <o1heap.h>
#include <cstddef>
@ -47,6 +48,7 @@ using Microsoft::WRL::ComPtr;
#include <fmt/core.h>
#include <list>
#include <semaphore>
#include <numeric>
#include "framework.h"
#include "mutex.h"

View file

@ -850,7 +850,7 @@ static void DrawConfigOptions()
DrawConfigOption(rowCount++, yOffset, &Config::ResolutionScale, true, nullptr, 0.25f, 1.0f, 2.0f);
DrawConfigOption(rowCount++, yOffset, &Config::Fullscreen, true);
DrawConfigOption(rowCount++, yOffset, &Config::VSync, true);
DrawConfigOption(rowCount++, yOffset, &Config::FPS, true, nullptr, 15, 120, 240);
DrawConfigOption(rowCount++, yOffset, &Config::FPS, true, nullptr, FPS_MIN, 120, FPS_MAX);
DrawConfigOption(rowCount++, yOffset, &Config::Brightness, true);
DrawConfigOption(rowCount++, yOffset, &Config::AntiAliasing, true);
DrawConfigOption(rowCount++, yOffset, &Config::TransparencyAntiAliasing, Config::AntiAliasing != EAntiAliasing::None, &Localise("Options_Desc_NotAvailableMSAA"));

View file

@ -169,6 +169,9 @@ CONFIG_DEFINE_ENUM_TEMPLATE(ETripleBuffering)
{ "Off", ETripleBuffering::Off }
};
static constexpr int32_t FPS_MIN = 15;
static constexpr int32_t FPS_MAX = 240;
enum class EAntiAliasing : uint32_t
{
None = 0,

View file

@ -425,11 +425,6 @@ name = "ParticleTestDrawIndexedPrimitiveMidAsmHook"
address = 0x827D25AC
registers = ["r7"]
[[midasm_hook]]
name = "LoadingScreenSpeedFixMidAsmHook"
address = 0x824DAB60
registers = ["r4"]
[[midasm_hook]]
name = "MotionBlurPrevInvViewProjectionMidAsmHook"
address = 0x82BA9E7C
@ -588,3 +583,14 @@ registers = ["r3"]
name = "PostureDPadSupportMidAsmHook"
address = 0x823CDA2C
registers = ["r3"]
[[midasm_hook]]
name = "ApplicationUpdateMidAsmHook"
address = 0x822C0EC8
[[midasm_hook]]
name = "LoadingUpdateMidAsmHook"
address = 0x825360C8
registers = ["r31"]
jump_address_on_true = 0x825360C8
jump_address_on_false = 0x82536140

1
thirdparty/implot vendored Submodule

@ -0,0 +1 @@
Subproject commit 77674d270e851d3f3718aad00234201af2b76ac9