Compare commits
10 commits
17caf72bdd
...
7eb0faf778
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eb0faf778 | ||
|
|
8ea87c6d4b | ||
|
|
3b0328da62 | ||
|
|
5aa8c4c727 | ||
|
|
593f8774f6 | ||
|
|
9100b278b7 | ||
|
|
ae6ea8e925 | ||
|
|
79761cbdd5 | ||
|
|
134a1c8f5e | ||
|
|
41dd2d5cb4 |
16 changed files with 202 additions and 57 deletions
|
|
@ -25,6 +25,7 @@
|
|||
// write-combined: never read it back; readback memory must be read through
|
||||
// InvalidateForRead.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace rex::graphics::nrhi {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ size_t page_size();
|
|||
// This is likely 64KiB.
|
||||
size_t allocation_granularity();
|
||||
|
||||
// Statically recompiled guest code executes from the host image, never from
|
||||
// guest pages, so the execute variants only describe guest-side intent: the
|
||||
// platform allocators serve them without the host execute bit (W^X).
|
||||
enum class PageAccess {
|
||||
kNoAccess = 0,
|
||||
kReadOnly = 1 << 0,
|
||||
|
|
@ -82,16 +85,6 @@ enum class DeallocationType {
|
|||
kDecommit = 1 << 1,
|
||||
};
|
||||
|
||||
// Whether the host allows the pages to be allocated or mapped with
|
||||
// PageAccess::kExecuteReadWrite - if not, separate mappings backed by the same
|
||||
// memory-mapped file must be used to write to executable pages.
|
||||
bool IsWritableExecutableMemorySupported();
|
||||
|
||||
// Whether PageAccess::kExecuteReadWrite is a supported and preferred way of
|
||||
// writing executable memory, useful for simulating how Xenia would work without
|
||||
// writable executable memory on a system with it.
|
||||
bool IsWritableExecutableMemoryPreferred();
|
||||
|
||||
// Allocates a block of memory at the given page-aligned base address.
|
||||
// Fails if the memory is not available.
|
||||
// Specify nullptr for base_address to leave it up to the system.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ REXCVAR_DECLARE(bool, kernel_debug_monitor);
|
|||
REXCVAR_DECLARE(bool, kernel_cert_monitor);
|
||||
REXCVAR_DECLARE(bool, ignore_thread_priorities);
|
||||
REXCVAR_DECLARE(bool, ignore_thread_affinities);
|
||||
REXCVAR_DECLARE(bool, writable_executable_memory);
|
||||
REXCVAR_DECLARE(bool, protect_zero);
|
||||
REXCVAR_DECLARE(bool, protect_on_release);
|
||||
REXCVAR_DECLARE(bool, scribble_heap);
|
||||
|
|
|
|||
|
|
@ -222,6 +222,15 @@ class Window {
|
|||
// readable from any thread (frame pacers, settings UI).
|
||||
static float CachedDisplayRefreshHz();
|
||||
|
||||
// Refresh-derived automatic frame cap for the given display refresh rate,
|
||||
// 0 while the refresh is unknown. Presents run with tearing allowed, so on
|
||||
// a VRR display any present arriving faster than the panel's minimum
|
||||
// refresh period tears; the cap must sit far enough below the refresh rate
|
||||
// that swap-to-present jitter cannot cross it. A fixed margin that is
|
||||
// ample at 60 Hz is too thin at 144 Hz, so the margin scales with the
|
||||
// refresh rate. Single source of the policy for pacers and settings UI.
|
||||
static float AutoFrameCapHz(float refresh_hz);
|
||||
|
||||
// Round trips are not guaranteed to return the same results.
|
||||
static constexpr uint32_t ConvertSizeDpi(uint32_t size, uint32_t new_dpi, uint32_t old_dpi) {
|
||||
// Always rounding up to prevent zero sizes (unless the input is zero) as
|
||||
|
|
|
|||
|
|
@ -13,10 +13,6 @@
|
|||
#include <rex/memory/utils.h>
|
||||
#include <rex/platform.h>
|
||||
|
||||
REXCVAR_DEFINE_BOOL(writable_executable_memory, true, "Memory",
|
||||
"Allow executable memory to be writable")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
|
||||
|
||||
#if REX_ARCH_ARM64
|
||||
#include <arm_neon.h>
|
||||
#endif
|
||||
|
|
@ -26,10 +22,6 @@ REXCVAR_DEFINE_BOOL(writable_executable_memory, true, "Memory",
|
|||
namespace rex {
|
||||
namespace memory {
|
||||
|
||||
bool IsWritableExecutableMemoryPreferred() {
|
||||
return REXCVAR_GET(writable_executable_memory);
|
||||
}
|
||||
|
||||
// TODO(benvanik): fancy AVX versions.
|
||||
// https://github.com/gnuradio/volk/blob/master/kernels/volk/volk_16u_byteswap.h
|
||||
// https://github.com/gnuradio/volk/blob/master/kernels/volk/volk_32u_byteswap.h
|
||||
|
|
|
|||
|
|
@ -118,19 +118,19 @@ uint32_t ToPosixProtectFlags(PageAccess access) {
|
|||
case PageAccess::kReadWrite:
|
||||
return PROT_READ | PROT_WRITE;
|
||||
case PageAccess::kExecuteReadOnly:
|
||||
return PROT_READ | PROT_EXEC;
|
||||
// Statically recompiled guest code executes from the host image, never
|
||||
// from guest pages, so execute requests are served without PROT_EXEC.
|
||||
// This keeps the process W^X: no path through this layer can create
|
||||
// executable memory.
|
||||
return PROT_READ;
|
||||
case PageAccess::kExecuteReadWrite:
|
||||
return PROT_READ | PROT_WRITE | PROT_EXEC;
|
||||
return PROT_READ | PROT_WRITE;
|
||||
default:
|
||||
assert_unhandled_case(access);
|
||||
return PROT_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsWritableExecutableMemorySupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO(tomc): this needs to go somewhere else. we should utilize the platform namespace more.
|
||||
#if REX_PLATFORM_LINUX
|
||||
namespace {
|
||||
|
|
|
|||
|
|
@ -50,9 +50,13 @@ DWORD ToWin32ProtectFlags(PageAccess access) {
|
|||
case PageAccess::kReadWrite:
|
||||
return PAGE_READWRITE;
|
||||
case PageAccess::kExecuteReadOnly:
|
||||
return PAGE_EXECUTE_READ;
|
||||
// Statically recompiled guest code executes from the host image, never
|
||||
// from guest pages, so execute requests are served without the execute
|
||||
// bit. This keeps the process W^X: no path through this layer can
|
||||
// create executable memory.
|
||||
return PAGE_READONLY;
|
||||
case PageAccess::kExecuteReadWrite:
|
||||
return PAGE_EXECUTE_READWRITE;
|
||||
return PAGE_READWRITE;
|
||||
default:
|
||||
assert_unhandled_case(access);
|
||||
return PAGE_NOACCESS;
|
||||
|
|
@ -81,16 +85,6 @@ PageAccess ToXeniaProtectFlags(DWORD access) {
|
|||
}
|
||||
}
|
||||
|
||||
bool IsWritableExecutableMemorySupported() {
|
||||
#ifdef REX_BASE_MEMORY_WIN_USE_DESKTOP_FUNCTIONS
|
||||
return true;
|
||||
#else
|
||||
// To test FromApp functions on desktop, undefine
|
||||
// REX_BASE_MEMORY_WIN_USE_DESKTOP_FUNCTIONS and link to WindowsApp.lib.
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void* AllocFixed(void* base_address, size_t length, AllocationType allocation_type,
|
||||
PageAccess access) {
|
||||
DWORD alloc_type = 0;
|
||||
|
|
@ -206,10 +200,11 @@ void* MapFileView(FileMappingHandle handle, void* base_address, size_t length, P
|
|||
file_access = FILE_MAP_ALL_ACCESS;
|
||||
break;
|
||||
case PageAccess::kExecuteReadOnly:
|
||||
file_access = FILE_MAP_READ | FILE_MAP_EXECUTE;
|
||||
// See ToWin32ProtectFlags: guest memory is never host-executable.
|
||||
file_access = FILE_MAP_READ;
|
||||
break;
|
||||
case PageAccess::kExecuteReadWrite:
|
||||
file_access = FILE_MAP_ALL_ACCESS | FILE_MAP_EXECUTE;
|
||||
file_access = FILE_MAP_ALL_ACCESS;
|
||||
break;
|
||||
case PageAccess::kNoAccess:
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -2497,7 +2497,14 @@ void D3D12CommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, uint32_t frontbu
|
|||
}
|
||||
|
||||
void D3D12CommandProcessor::OnPrimaryBufferEnd() {
|
||||
if (REXCVAR_GET(d3d12_submit_on_primary_buffer_end) && submission_open_ &&
|
||||
// While the native guest-output renderer is suppressing emulated draws,
|
||||
// batch everything into the end-of-frame submission: the mid-frame
|
||||
// primary-buffer submits carry only the small exempt composition passes,
|
||||
// and the GPU then idles until the native pass arrives (a measured
|
||||
// ~0.3 ms/frame starvation bubble). The emulated path keeps the
|
||||
// per-buffer submits for its own pacing.
|
||||
if (REXCVAR_GET(d3d12_submit_on_primary_buffer_end) &&
|
||||
!ShouldSuppressEmulatedDraws() && submission_open_ &&
|
||||
CanEndSubmissionImmediately()) {
|
||||
EndSubmission(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,13 @@ REXCVAR_DEFINE_BOOL(vulkan_deferred_resolve_clears, false, "GPU/Vulkan",
|
|||
"with an attachment clear per resolve")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(vulkan_allow_inexact_rt_transfers, false, "GPU/Vulkan",
|
||||
"Allow the host render target path on devices lacking integer sampled-image "
|
||||
"MSAA or UINT transfer format support, degrading render target ownership "
|
||||
"transfers from bit-exact to approximate (for non-conformant drivers such as "
|
||||
"Vulkan-on-D3D12, where the fragment shader interlock path is unavailable)")
|
||||
.lifecycle(rex::cvar::Lifecycle::kInitOnly);
|
||||
|
||||
// DEFINE_string(
|
||||
// render_target_path_vulkan, "",
|
||||
// "Render target emulation path to use on Vulkan.\n"
|
||||
|
|
@ -429,6 +436,11 @@ bool VulkanRenderTargetCache::Initialize(uint32_t shared_memory_binding_count) {
|
|||
"can't be bit-exact on this device; switching to fragment shader "
|
||||
"interlock path for D3D12 parity");
|
||||
path_ = Path::kPixelShaderInterlock;
|
||||
} else if (REXCVAR_GET(vulkan_allow_inexact_rt_transfers)) {
|
||||
REXGPU_WARN(
|
||||
"VulkanRenderTargetCache: Proceeding with non-bit-exact host render "
|
||||
"target ownership transfers (vulkan_allow_inexact_rt_transfers); "
|
||||
"emulated rendering may be inaccurate on this device");
|
||||
} else {
|
||||
REXGPU_ERROR(
|
||||
"VulkanRenderTargetCache: Bit-exact host render target ownership "
|
||||
|
|
|
|||
|
|
@ -84,6 +84,36 @@ endif()
|
|||
|
||||
if(WIN32)
|
||||
target_link_libraries(rexruntime PRIVATE ws2_32)
|
||||
|
||||
# Version metadata for the runtime DLL. Unsigned binaries without any
|
||||
# version resource are prone to antivirus false positives; embedding
|
||||
# identity here also lets crash dumps and support logs name the build.
|
||||
# The embedding application may override these before including the SDK.
|
||||
enable_language(RC)
|
||||
if(NOT REXGLUE_RUNTIME_RC_VERSION)
|
||||
set(REXGLUE_RUNTIME_RC_VERSION "0,0,0,0")
|
||||
endif()
|
||||
if(NOT REXGLUE_RUNTIME_RC_VERSION_STRING)
|
||||
set(REXGLUE_RUNTIME_RC_VERSION_STRING "0.0.0")
|
||||
endif()
|
||||
if(NOT REXGLUE_RUNTIME_RC_PRODUCT_NAME)
|
||||
set(REXGLUE_RUNTIME_RC_PRODUCT_NAME "rexglue")
|
||||
endif()
|
||||
if(NOT REXGLUE_RUNTIME_RC_COMPANY)
|
||||
set(REXGLUE_RUNTIME_RC_COMPANY "rexglue project")
|
||||
endif()
|
||||
if(NOT REXGLUE_RUNTIME_RC_DESCRIPTION)
|
||||
set(REXGLUE_RUNTIME_RC_DESCRIPTION "rexglue guest runtime library")
|
||||
endif()
|
||||
if(NOT REXGLUE_RUNTIME_RC_COPYRIGHT)
|
||||
set(REXGLUE_RUNTIME_RC_COPYRIGHT "(c) rexglue contributors")
|
||||
endif()
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/rexruntime_version.rc.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/rexruntime_version.rc
|
||||
@ONLY)
|
||||
target_sources(rexruntime PRIVATE
|
||||
${CMAKE_CURRENT_BINARY_DIR}/rexruntime_version.rc)
|
||||
endif()
|
||||
|
||||
if(REXGLUE_USE_D3D12)
|
||||
|
|
|
|||
28
src/system/rexruntime_version.rc.in
Normal file
28
src/system/rexruntime_version.rc.in
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
1 VERSIONINFO
|
||||
FILEVERSION @REXGLUE_RUNTIME_RC_VERSION@
|
||||
PRODUCTVERSION @REXGLUE_RUNTIME_RC_VERSION@
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS 0x40004L
|
||||
FILETYPE 0x2L
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "@REXGLUE_RUNTIME_RC_COMPANY@"
|
||||
VALUE "FileDescription", "@REXGLUE_RUNTIME_RC_DESCRIPTION@"
|
||||
VALUE "FileVersion", "@REXGLUE_RUNTIME_RC_VERSION_STRING@"
|
||||
VALUE "InternalName", "rexruntime"
|
||||
VALUE "LegalCopyright", "@REXGLUE_RUNTIME_RC_COPYRIGHT@"
|
||||
VALUE "OriginalFilename", "rexruntime.dll"
|
||||
VALUE "ProductName", "@REXGLUE_RUNTIME_RC_PRODUCT_NAME@"
|
||||
VALUE "ProductVersion", "@REXGLUE_RUNTIME_RC_VERSION_STRING@"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
|
|
@ -1351,10 +1351,12 @@ void SimpleSettingsDialog::BuildRows(std::vector<RowSpec>& rows, int category) {
|
|||
"Auto tracks the current display and does exactly that. Frames above the "
|
||||
"refresh rate cannot be shown and only make steady motion judder.";
|
||||
if (FrameCapHasAuto()) {
|
||||
const float hz = rex::ui::Window::CachedDisplayRefreshHz();
|
||||
// Same policy the guest pacer applies (see Window::AutoFrameCapHz).
|
||||
const float auto_cap =
|
||||
rex::ui::Window::AutoFrameCapHz(rex::ui::Window::CachedDisplayRefreshHz());
|
||||
row.options.push_back(
|
||||
hz >= 30.0f
|
||||
? "Auto (" + std::to_string(int(hz) - 4) + " FPS)"
|
||||
auto_cap > 0.0f
|
||||
? "Auto (" + std::to_string(int(auto_cap)) + " FPS)"
|
||||
: std::string("Auto"));
|
||||
}
|
||||
for (size_t i = 1; i < kFrameCapLabels.size(); ++i) {
|
||||
|
|
|
|||
|
|
@ -424,13 +424,19 @@ int DrawWizardScreen(ImGuiDrawer* drawer, ImGuiIO& io, const WizardScreenSpec& s
|
|||
const float cy = y + row_h * 0.5f;
|
||||
AddTextVCentered(dl, bold_ol, label_size, col_x + 18.0f * s, cy, kColRowText, row.label);
|
||||
// Long paths shrink toward 15*s, then ellipsize from the FRONT - the
|
||||
// filename tail is the informative part.
|
||||
// filename tail is the informative part. The step walks the unquantized
|
||||
// size so it always makes progress: font_px() snaps to buckets a little
|
||||
// over a pixel wide, so at small viewport scales a step is narrower than
|
||||
// one bucket and re-quantizing the previous result returns it unchanged.
|
||||
const float label_w = bold_ol->CalcTextSizeA(label_size, FLT_MAX, 0.0f, row.label).x;
|
||||
const float max_w = col_w - 36.0f * s - label_w - 24.0f * s;
|
||||
float vsize = font_px(18.0f * s);
|
||||
while (vsize > 15.0f * s &&
|
||||
const float min_vsize = 15.0f * s;
|
||||
float raw_vsize = 18.0f * s;
|
||||
float vsize = font_px(raw_vsize);
|
||||
while (raw_vsize > min_vsize &&
|
||||
font->CalcTextSizeA(vsize, FLT_MAX, 0.0f, row.value.c_str()).x > max_w) {
|
||||
vsize = font_px(vsize - 0.5f * s);
|
||||
raw_vsize = std::max(min_vsize, raw_vsize - 0.5f * s);
|
||||
vsize = font_px(raw_vsize);
|
||||
}
|
||||
std::string shown = row.value;
|
||||
while (shown.size() > 1 &&
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
#include <rex/assert.h>
|
||||
|
|
@ -45,6 +46,13 @@ REXCVAR_DEFINE_INT32(presenter_strict_guest_output_backpressure_timeout_ms, 100,
|
|||
.range(0, 1000)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(presenter_present_cadence_log, false, "UI/Presenter",
|
||||
"Log guest present cadence statistics every 600 swaps: average/max "
|
||||
"swap-to-swap interval, standard deviation, and frame-to-frame jitter "
|
||||
"(mean absolute successive difference). Attributes perceived judder to "
|
||||
"irregular present timing versus low frame rate.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(present_letterbox, true, "UI/Presenter",
|
||||
"Enable letterboxing for non-native aspect ratios");
|
||||
|
||||
|
|
@ -585,6 +593,40 @@ bool Presenter::RefreshGuestOutput(
|
|||
return false;
|
||||
}
|
||||
guest_output_active_last_refresh_ = true;
|
||||
// Present-cadence telemetry: the interval between guest output refreshes
|
||||
// is the cadence the host presents at (painting is triggered per
|
||||
// refresh), which VRR displays turn directly into perceived smoothness.
|
||||
// jitter = mean absolute successive frame-to-frame difference, the
|
||||
// judder-relevant metric; sd and max catch slow drift and hitches.
|
||||
if (REXCVAR_GET(presenter_present_cadence_log)) {
|
||||
static std::chrono::steady_clock::time_point s_cad_prev{};
|
||||
static double s_cad_sum = 0, s_cad_sumsq = 0, s_cad_max = 0;
|
||||
static double s_cad_jitter_sum = 0, s_cad_last = 0;
|
||||
static uint32_t s_cad_n = 0;
|
||||
const auto cad_now = std::chrono::steady_clock::now();
|
||||
if (s_cad_prev.time_since_epoch().count() != 0) {
|
||||
const double ms =
|
||||
std::chrono::duration<double, std::milli>(cad_now - s_cad_prev).count();
|
||||
s_cad_sum += ms;
|
||||
s_cad_sumsq += ms * ms;
|
||||
s_cad_max = std::max(s_cad_max, ms);
|
||||
if (s_cad_n != 0) {
|
||||
s_cad_jitter_sum += std::abs(ms - s_cad_last);
|
||||
}
|
||||
s_cad_last = ms;
|
||||
if (++s_cad_n >= 600) {
|
||||
const double avg = s_cad_sum / s_cad_n;
|
||||
const double var = std::max(0.0, s_cad_sumsq / s_cad_n - avg * avg);
|
||||
REXLOG_INFO(
|
||||
"present-cadence: avg={:.2f}ms sd={:.2f} jitter={:.3f} max={:.2f} "
|
||||
"(600 swaps)",
|
||||
avg, std::sqrt(var), s_cad_jitter_sum / (s_cad_n - 1), s_cad_max);
|
||||
s_cad_sum = s_cad_sumsq = s_cad_max = s_cad_jitter_sum = s_cad_last = 0;
|
||||
s_cad_n = 0;
|
||||
}
|
||||
}
|
||||
s_cad_prev = cad_now;
|
||||
}
|
||||
if (GuestFrameStatsEnabled()) {
|
||||
std::lock_guard<std::mutex> stats_lock(guest_frame_stats_mutex_);
|
||||
guest_frame_timestamps_[guest_frame_timestamp_next_] = std::chrono::steady_clock::now();
|
||||
|
|
@ -766,19 +808,31 @@ Presenter::GuestFrameStats Presenter::GetGuestFrameStats() const {
|
|||
}
|
||||
const double span_to_now_seconds =
|
||||
std::chrono::duration_cast<std::chrono::duration<double>>(now - oldest_in_window).count();
|
||||
if (span_to_now_seconds > 0.0) {
|
||||
stats.fps = double(frames_in_window) / span_to_now_seconds;
|
||||
}
|
||||
double span_between_frames_seconds = 0.0;
|
||||
if (frames_in_window >= 2) {
|
||||
const double span_between_frames_seconds =
|
||||
span_between_frames_seconds =
|
||||
std::chrono::duration_cast<std::chrono::duration<double>>(newest_in_window -
|
||||
oldest_in_window)
|
||||
.count();
|
||||
if (span_between_frames_seconds > 0.0) {
|
||||
stats.frame_time_ms =
|
||||
span_between_frames_seconds * 1000.0 / double(frames_in_window - 1);
|
||||
}
|
||||
} else if (stats.fps > 0.0) {
|
||||
}
|
||||
if (span_between_frames_seconds > 0.0 && span_to_now_seconds > 0.0) {
|
||||
const double mean_interval_seconds =
|
||||
span_between_frames_seconds / double(frames_in_window - 1);
|
||||
stats.frame_time_ms = mean_interval_seconds * 1000.0;
|
||||
// Rate = frame intervals per second, not frames per second: N frames span
|
||||
// only N - 1 intervals, so dividing the frame count by the span reads up
|
||||
// to one FPS high (oscillating with sampling phase, e.g. 140-141 at a
|
||||
// steady 140). The partial interval elapsed since the newest frame is
|
||||
// included so a steady source reads exactly its rate regardless of
|
||||
// phase, but clamped to one interval so the estimate still decays toward
|
||||
// zero when the guest stops producing frames.
|
||||
const double partial_interval = std::min(
|
||||
(span_to_now_seconds - span_between_frames_seconds) / mean_interval_seconds, 1.0);
|
||||
stats.fps = (double(frames_in_window - 1) + partial_interval) / span_to_now_seconds;
|
||||
} else if (span_to_now_seconds > 0.0) {
|
||||
// A single frame in the window (or coincident timestamps): no interval to
|
||||
// measure, keep the frame-count estimate.
|
||||
stats.fps = double(frames_in_window) / span_to_now_seconds;
|
||||
stats.frame_time_ms = 1000.0 / stats.fps;
|
||||
}
|
||||
return stats;
|
||||
|
|
|
|||
|
|
@ -431,7 +431,12 @@ bool ReXApp::SetupEnvironment() {
|
|||
if (std::filesystem::exists(config_path_))
|
||||
REXLOG_INFO("Loaded config: {}", config_path_.filename().string());
|
||||
|
||||
REXLOG_INFO("{} starting", GetName());
|
||||
// Include the build title so support logs identify the exact build.
|
||||
if (auto build_title = GetBuildTitle(); !build_title.empty()) {
|
||||
REXLOG_INFO("{} starting {}", GetName(), build_title);
|
||||
} else {
|
||||
REXLOG_INFO("{} starting", GetName());
|
||||
}
|
||||
if (!game_data_root_.empty()) {
|
||||
REXLOG_INFO(" Game directory: {}", game_data_root_.string());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <iterator>
|
||||
|
||||
#include <rex/assert.h>
|
||||
|
|
@ -78,6 +79,17 @@ void Window::UpdateCachedDisplayRefresh() {
|
|||
}
|
||||
}
|
||||
|
||||
float Window::AutoFrameCapHz(float refresh_hz) {
|
||||
if (refresh_hz < 30.0f) {
|
||||
return 0.0f;
|
||||
}
|
||||
// 4 FPS of margin or 5% of the refresh rate, whichever is larger (see the
|
||||
// declaration): 4 FPS is 6.7% of headroom at 60 Hz but only 0.2 ms of
|
||||
// present slack at 144 Hz, within routine thread-scheduling jitter.
|
||||
const float margin = std::max(4.0f, refresh_hz * 0.05f);
|
||||
return std::floor(refresh_hz - margin);
|
||||
}
|
||||
|
||||
Window::Window(WindowedAppContext& app_context, const std::string_view title,
|
||||
uint32_t desired_logical_width, uint32_t desired_logical_height)
|
||||
: app_context_(app_context),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue