mirror of
https://github.com/hedge-dev/UnleashedRecomp.git
synced 2025-04-28 13:27:58 +03:00
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:
parent
314a092747
commit
f1416c85ba
13 changed files with 211 additions and 31 deletions
5
.gitmodules
vendored
5
.gitmodules
vendored
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,3 +15,4 @@ public:
|
|||
static void Restart(std::vector<std::string> restartArgs = {});
|
||||
static void Exit();
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
1
thirdparty/implot
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 77674d270e851d3f3718aad00234201af2b76ac9
|
Loading…
Add table
Add a link
Reference in a new issue