Compare commits

...

10 commits

16 changed files with 202 additions and 57 deletions

View file

@ -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 {

View file

@ -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.

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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:

View file

@ -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);
}

View file

@ -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 "

View file

@ -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)

View 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

View file

@ -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) {

View file

@ -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 &&

View file

@ -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;

View file

@ -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());
}

View file

@ -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),