mirror of
https://github.com/mitch030504/AstroQuest-Frame.git
synced 2026-10-17 17:01:09 +02:00
Port pipeline cache repairs and precise late-frame recovery from elliotttate
This commit is contained in:
1 parent
4b9479a086
commit
d4e4e00192
10 files changed
+356
-17
No files matched your search
@@ -78,6 +78,12 @@ or SteamVR motion smoothing.
|
||||
- Upstream now provides the SteamVR address-space reservation and runtime-specific idle
|
||||
recovery behavior; those implementations are retained instead of duplicate fork patches.
|
||||
|
||||
### Selected elliotttate performance port (candidate)
|
||||
|
||||
This candidate adds pipeline-cache preload repairs and bounded late-frame recovery. See
|
||||
[the port notes](docs/elliotttate-performance-port.md) for scope, attribution and limitations.
|
||||
Set `env=SHADPS4_XR_LATE_WAIT_MS=0` in settings.txt to disable late-frame waiting.
|
||||
|
||||
### Precise VR presentation pacing (experimental)
|
||||
|
||||
VR early-flip presentation now uses a reusable Windows high-resolution waitable timer
|
||||
@@ -838,3 +844,4 @@ bash tools/make-pc-vr.sh # cop
|
||||
|
||||
The OpenXR loader (Khronos OpenXR-SDK 1.1.63, `externals/openxr-sdk`) is built with the
|
||||
emulator; `ENABLE_OPENXR` (on for Windows) switches the whole of it.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Selected elliotttate performance port
|
||||
|
||||
Reviewed on 2026-10-07: [elliotttate/AstroQuest at b72db1b](https://github.com/elliotttate/AstroQuest/tree/b72db1b).
|
||||
[Their measurements and limitations](https://github.com/elliotttate/AstroQuest/blob/b72db1b/docs/pc-performance.md).
|
||||
|
||||
Adapted pipeline-cache repairs and late-frame recovery from `6af3210`, preserving the original license headers. This candidate retains upstream 0.20, the soccer-enemy fix, Full framerate selector, expanded resolutions, separate headset queue and precise presentation timer.
|
||||
|
||||
## Included
|
||||
|
||||
Cached image/sampler lists now contain their storage, so a new process does not load pointers from the process that wrote the cache. Ordinary descriptor layouts avoid reading guest descriptors during preloading; dynamic-mip layouts requiring guest memory are deferred. Vertex-fetch data survives later shader stages, and shader permutations point at the persistent cached program's information.
|
||||
|
||||
Metadata and pipeline-key versions are incremented to reject the former layout. Upstream 0.20's shader-binary version 3 (geometry-input ABI fix) is preserved. Separate cold/warm processes test cache reuse before enabling it in the installed configuration.
|
||||
|
||||
When a recent stream's next frame is missing, the OpenXR thread waits briefly before repeating the last picture. The budget starts at xrWaitFrame wakeup, defaults to 3 ms, and is configurable with `SHADPS4_XR_LATE_WAIT_MS` (0 disables, maximum 6). Idle streams and pictures the runtime does not want are skipped. No Vulkan queue lock is held. The log counts recovered pictures and timeouts.
|
||||
|
||||
The initial condition-variable port's nominal 3-ms timeout took 15-16 ms on this PC. The Windows adaptation uses a producer event and a high-resolution timeout timer; measured timeout was about 3.5 ms. Arriving frames can wake it immediately. Unsupported precise timers disable waiting instead of using a coarse timeout. This is a scheduling budget, not a hard real-time guarantee.
|
||||
|
||||
## Kept separate
|
||||
|
||||
- Their native 90/120 engine mode is promising, but their 120 measurements use a simulator and game-1.00 offsets. The 90 mode is unmeasured and 1.04 offsets are unverified. It is not enabled here.
|
||||
- Their guest HR-timer scheduler addresses the game's 3-ms tick, with broader cancellation/lifetime implications than the presentation timer already in this fork.
|
||||
- Parallel texture copies, stream/shader memoization and lock-skipping are not imported wholesale. Their notes document unresolved memory corruption near the parallel-copy/shader-memo work; no causal fix is established.
|
||||
- Their 32 presentation polls and high-resolution sleeps overlap with this fork. Their new delivery phase was measured at simulated 120 Hz; this fork retains its phase until a real-runtime comparison.
|
||||
|
||||
Startup checks do not establish improved headset gameplay. The existing regular release remains available for comparison.
|
||||
@@ -13,6 +13,7 @@ public:
|
||||
timer = CreateWaitableTimerExW(nullptr, nullptr,
|
||||
high_resolution ? CREATE_WAITABLE_TIMER_HIGH_RESOLUTION : 0,
|
||||
TIMER_MODIFY_STATE | SYNCHRONIZE);
|
||||
precise = timer && high_resolution;
|
||||
if (!timer && high_resolution) {
|
||||
timer = CreateWaitableTimerExW(nullptr, nullptr, 0, TIMER_MODIFY_STATE | SYNCHRONIZE);
|
||||
}
|
||||
@@ -26,16 +27,27 @@ public:
|
||||
DWORD Wait(std::chrono::nanoseconds duration, bool interruptible) {
|
||||
if (duration.count() <= 0)
|
||||
return WAIT_OBJECT_0;
|
||||
if (!timer)
|
||||
return WAIT_FAILED;
|
||||
LARGE_INTEGER interval;
|
||||
interval.QuadPart = -std::max<long long>(1, duration.count() / 100);
|
||||
if (!SetWaitableTimer(timer, &interval, 0, nullptr, nullptr, FALSE))
|
||||
if (!Arm(duration))
|
||||
return WAIT_FAILED;
|
||||
return WaitForSingleObjectEx(timer, INFINITE, interruptible);
|
||||
}
|
||||
|
||||
bool Arm(std::chrono::nanoseconds duration) {
|
||||
if (!timer)
|
||||
return false;
|
||||
LARGE_INTEGER interval;
|
||||
interval.QuadPart = -std::max<long long>(1, duration.count() / 100);
|
||||
return SetWaitableTimer(timer, &interval, 0, nullptr, nullptr, FALSE) != FALSE;
|
||||
}
|
||||
HANDLE Handle() const {
|
||||
return timer;
|
||||
}
|
||||
bool IsHighResolution() const {
|
||||
return precise;
|
||||
}
|
||||
|
||||
private:
|
||||
HANDLE timer{};
|
||||
bool precise{};
|
||||
};
|
||||
} // namespace Common
|
||||
@@ -0,0 +1,98 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#pragma once
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#ifdef _WIN32
|
||||
#include "common/windows_sleep_timer.h"
|
||||
#endif
|
||||
|
||||
namespace Core::Vr {
|
||||
// Producer notification wakes immediately; the Windows timeout uses its own high-
|
||||
// resolution timer instead of the coarse timeout of SleepConditionVariableSRW.
|
||||
class LateFrameSignal {
|
||||
public:
|
||||
#ifdef _WIN32
|
||||
LateFrameSignal() : event{CreateEventW(nullptr, FALSE, FALSE, nullptr)} {}
|
||||
~LateFrameSignal() {
|
||||
if (event)
|
||||
CloseHandle(event);
|
||||
}
|
||||
void Notify() {
|
||||
if (event)
|
||||
SetEvent(event);
|
||||
}
|
||||
#else
|
||||
void Notify() {
|
||||
delivered.notify_one();
|
||||
}
|
||||
#endif
|
||||
LateFrameSignal(const LateFrameSignal&) = delete;
|
||||
LateFrameSignal& operator=(const LateFrameSignal&) = delete;
|
||||
#ifndef _WIN32
|
||||
LateFrameSignal() = default;
|
||||
#endif
|
||||
template <typename Ready>
|
||||
bool WaitUntil(std::unique_lock<std::mutex>& lock, std::chrono::steady_clock::time_point until,
|
||||
Ready ready) {
|
||||
#ifdef _WIN32
|
||||
while (!ready()) {
|
||||
const auto remaining = until - std::chrono::steady_clock::now();
|
||||
// Unsupported timers or allocation failure disable waiting, rather than use
|
||||
// a coarse timeout that can consume a whole headset refresh.
|
||||
if (remaining.count() <= 0 || !event || !timer.IsHighResolution())
|
||||
return false;
|
||||
// Producer updates readiness and notifies under the same mutex. Resetting
|
||||
// before unlocking cannot lose a delivery between the predicate and wait.
|
||||
ResetEvent(event);
|
||||
if (!timer.Arm(std::chrono::duration_cast<std::chrono::nanoseconds>(remaining)))
|
||||
return false;
|
||||
const HANDLE handles[]{event, timer.Handle()};
|
||||
lock.unlock();
|
||||
const DWORD result = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
|
||||
lock.lock();
|
||||
if (ready())
|
||||
return true;
|
||||
if (result != WAIT_OBJECT_0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return delivered.wait_until(lock, until, ready);
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
#ifdef _WIN32
|
||||
HANDLE event{};
|
||||
Common::WindowsSleepTimer timer;
|
||||
#else
|
||||
std::condition_variable delivered;
|
||||
#endif
|
||||
};
|
||||
|
||||
enum class LateFrameResult { Skipped, Ready, TimedOut };
|
||||
|
||||
// Budget begins at the runtime wakeup. The predicate is always checked first so a
|
||||
// delivery between the initial TakeFrame and taking the mutex is not lost.
|
||||
template <typename Ready>
|
||||
LateFrameResult WaitForLateFrame(LateFrameSignal& delivered, std::unique_lock<std::mutex>& lock,
|
||||
std::chrono::steady_clock::time_point woke,
|
||||
std::chrono::steady_clock::time_point last_delivery, float rate,
|
||||
float wait_ms, Ready ready) {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
if (ready())
|
||||
return LateFrameResult::Ready;
|
||||
if (!(wait_ms > 0.0f && rate > 30.0f) || last_delivery == Clock::time_point{} ||
|
||||
woke - last_delivery >= std::chrono::duration<double>(2.0 / rate)) {
|
||||
return LateFrameResult::Skipped;
|
||||
}
|
||||
const auto until = woke + std::chrono::duration_cast<Clock::duration>(
|
||||
std::chrono::duration<double, std::milli>(wait_ms));
|
||||
if (Clock::now() >= until)
|
||||
return LateFrameResult::TimedOut;
|
||||
return delivered.WaitUntil(lock, until, ready) ? LateFrameResult::Ready
|
||||
: LateFrameResult::TimedOut;
|
||||
}
|
||||
} // namespace Core::Vr
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -22,6 +23,7 @@
|
||||
#include <SDL3/SDL_events.h>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "core/vr/late_frame_wait.h"
|
||||
#include "common/polyfill_thread.h"
|
||||
#include "common/singleton.h"
|
||||
#include "common/thread.h"
|
||||
@@ -391,6 +393,7 @@ struct OpenXrHost::Impl {
|
||||
|
||||
// The title's frames on their way here.
|
||||
std::mutex slot_mutex;
|
||||
LateFrameSignal delivered_cv;
|
||||
std::array<Slot, NumSlots> slots;
|
||||
s32 latest{-1};
|
||||
u32 next_slot{};
|
||||
@@ -478,6 +481,8 @@ struct OpenXrHost::Impl {
|
||||
u32 display_frames{};
|
||||
u32 reported_delivered{};
|
||||
u32 copied_frames{};
|
||||
u32 late_frames{};
|
||||
u32 late_wait_timeouts{};
|
||||
u32 head_tracked_frames{};
|
||||
u32 palm_samples{};
|
||||
float palm_distance{};
|
||||
@@ -1753,7 +1758,25 @@ struct OpenXrHost::Impl {
|
||||
AskAudioDevicesIfDue();
|
||||
UpdatePad(pose_time);
|
||||
|
||||
if (const s32 index = TakeFrame(); index >= 0) {
|
||||
s32 taken = TakeFrame();
|
||||
// From elliotttate/AstroQuest: a frame a little late need not become a repeat.
|
||||
// Budget is measured from xrWaitFrame wakeup, not from the start of this wait,
|
||||
// and no Vulkan queue lock is held while the producer is allowed to finish.
|
||||
static const float late_wait_ms =
|
||||
std::clamp(EnvFloat("SHADPS4_XR_LATE_WAIT_MS", 3.0f), 0.0f, 6.0f);
|
||||
if (taken < 0 && late_wait_ms > 0.0f && have_frame &&
|
||||
frame_state.shouldRender == XR_TRUE && refresh_rate > 30.0f) {
|
||||
std::unique_lock lock{slot_mutex};
|
||||
const auto result = WaitForLateFrame(delivered_cv, lock, woke, last_delivery,
|
||||
refresh_rate, late_wait_ms, [this] {
|
||||
return latest >= 0 && slots[latest].state == Slot::State::Ready;
|
||||
});
|
||||
lock.unlock();
|
||||
taken = TakeFrame();
|
||||
if (taken >= 0) ++late_frames;
|
||||
else if (result == LateFrameResult::TimedOut) ++late_wait_timeouts;
|
||||
}
|
||||
if (const s32 index = taken; index >= 0) {
|
||||
const auto started = Clock::now();
|
||||
bool blank = false;
|
||||
const bool copied = CopyFrame(static_cast<u32>(index), blank);
|
||||
@@ -2466,6 +2489,10 @@ struct OpenXrHost::Impl {
|
||||
"({:.2f} ms at worst)",
|
||||
graphics.queue_index, end_lock_time / frames * 1e3,
|
||||
end_call_time / frames * 1e3, end_call_worst * 1e3);
|
||||
LOG_INFO(Core_Vr, "Late-frame recovery: {} pictures recovered, {} waits timed out",
|
||||
late_frames, late_wait_timeouts);
|
||||
late_frames = 0;
|
||||
late_wait_timeouts = 0;
|
||||
if ((has_hand_tracking || (use_grips && actions_ready)) && !controllers_used) {
|
||||
const float held = pad_samples != 0 ? 1.0f / static_cast<float>(pad_samples) : 0.0f;
|
||||
LOG_INFO(Core_Vr,
|
||||
@@ -2841,6 +2868,7 @@ void OpenXrHost::EndFrame(u32 index, const PresentedFrame& info, vk::Semaphore r
|
||||
}
|
||||
}
|
||||
impl->last_delivery = now;
|
||||
impl->delivered_cv.Notify();
|
||||
}
|
||||
|
||||
void OpenXrHost::DropFrame(u32 index) {
|
||||
|
||||
@@ -125,13 +125,20 @@ struct ImageResource {
|
||||
}
|
||||
|
||||
u32 NumBindings(const auto& info) const {
|
||||
// Only an image whose mips are indexed in the shader needs its sharp for this: a
|
||||
// pipeline made from the cache as the emulator starts has no sharps to read (the
|
||||
// title's memory holds nothing yet).
|
||||
if (mip_fallback_mode != MipStorageFallbackMode::DynamicIndex) {
|
||||
return 1;
|
||||
}
|
||||
const AmdGpu::Image tsharp = GetSharp(info);
|
||||
return (mip_fallback_mode == MipStorageFallbackMode::DynamicIndex)
|
||||
? (tsharp.last_level - tsharp.base_level + 1)
|
||||
: 1;
|
||||
return tsharp.last_level - tsharp.base_level + 1;
|
||||
}
|
||||
};
|
||||
using ImageResourceList = boost::container::small_vector<ImageResource, NUM_IMAGES>;
|
||||
// (Fixed capacity, like the other lists: the pipeline cache stores Shader::InfoPersistent as its
|
||||
// bytes, and a small_vector's bytes hold a pointer to its elements, which pointed into the
|
||||
// process that wrote the cache.)
|
||||
using ImageResourceList = boost::container::static_vector<ImageResource, NUM_IMAGES>;
|
||||
|
||||
struct SamplerResource {
|
||||
u32 sharp_idx;
|
||||
@@ -145,7 +152,7 @@ struct SamplerResource {
|
||||
: info.template ReadUdSharp<AmdGpu::Sampler>(sharp_idx);
|
||||
}
|
||||
};
|
||||
using SamplerResourceList = boost::container::small_vector<SamplerResource, NUM_SAMPLERS>;
|
||||
using SamplerResourceList = boost::container::static_vector<SamplerResource, NUM_SAMPLERS>;
|
||||
|
||||
struct FMaskResource {
|
||||
u32 sharp_idx;
|
||||
|
||||
@@ -136,6 +136,8 @@ private:
|
||||
std::array<const Shader::Info*, MaxShaderStages> infos{};
|
||||
std::array<vk::ShaderModule, MaxShaderStages> modules{};
|
||||
std::optional<Shader::Gcn::FetchShaderData> fetch_shader{};
|
||||
// Preloading must retain vertex fetch data across later shader stages.
|
||||
std::optional<Shader::Gcn::FetchShaderData> preload_fetch_shader{};
|
||||
GraphicsPipelineKey graphics_key{};
|
||||
u8 targets_left_out{};
|
||||
ComputePipelineKey compute_key{};
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
|
||||
namespace Serialization {
|
||||
/* You should increment versions below once corresponding serialization scheme is changed. */
|
||||
// Recompile cached shaders with the corrected geometry input ABI and invocation ID.
|
||||
// Preserve upstream 0.20 geometry-input ABI and invocation-ID correction.
|
||||
static constexpr u32 ShaderBinaryVersion = 3u;
|
||||
static constexpr u32 ShaderMetaVersion = 2u;
|
||||
static constexpr u32 PipelineKeyVersion = 2u;
|
||||
// Inline image/sampler lists replace pointer-containing small vectors.
|
||||
static constexpr u32 ShaderMetaVersion = 3u;
|
||||
static constexpr u32 PipelineKeyVersion = 3u;
|
||||
} // namespace Serialization
|
||||
|
||||
namespace Vulkan {
|
||||
@@ -143,6 +144,16 @@ bool ComputePipeline::SerializationSupport::Deserialize(Serialization::Archive&
|
||||
return true;
|
||||
}
|
||||
|
||||
// A pipeline whose descriptor layout depends on the title's image sharps (an image whose mips
|
||||
// the shader indexes: a binding for each) cannot be made before the title runs: it is not
|
||||
// preloaded but compiled when the title first uses it. (Reading the sharps as the emulator
|
||||
// starts read the title's memory before there was any.)
|
||||
static bool UsesSharpsToLayOut(const Shader::Info& info) {
|
||||
return std::ranges::any_of(info.images, [](const Shader::ImageResource& image) {
|
||||
return image.mip_fallback_mode == Shader::MipStorageFallbackMode::DynamicIndex;
|
||||
});
|
||||
}
|
||||
|
||||
bool PipelineCache::LoadComputePipeline(Serialization::Archive& ar) {
|
||||
compute_key.Deserialize(ar);
|
||||
|
||||
@@ -161,6 +172,11 @@ bool PipelineCache::LoadComputePipeline(Serialization::Archive& ar) {
|
||||
if (!LoadPipelineStage(meta_ar, 0)) {
|
||||
return false;
|
||||
}
|
||||
if (UsesSharpsToLayOut(*infos[0])) {
|
||||
infos.fill(nullptr);
|
||||
modules.fill(nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto [it, is_new] = compute_pipelines.try_emplace(compute_key);
|
||||
ASSERT(is_new);
|
||||
@@ -212,6 +228,7 @@ bool GraphicsPipeline::SerializationSupport::Deserialize(Serialization::Archive&
|
||||
}
|
||||
|
||||
bool PipelineCache::LoadGraphicsPipeline(Serialization::Archive& ar) {
|
||||
preload_fetch_shader.reset();
|
||||
graphics_key.Deserialize(ar);
|
||||
|
||||
GraphicsPipeline::SerializationSupport sdata{};
|
||||
@@ -233,20 +250,44 @@ bool PipelineCache::LoadGraphicsPipeline(Serialization::Archive& ar) {
|
||||
Serialization::Archive meta_ar{std::move(meta_blob)};
|
||||
|
||||
if (!LoadPipelineStage(meta_ar, stage_idx)) {
|
||||
infos.fill(nullptr);
|
||||
modules.fill(nullptr);
|
||||
preload_fetch_shader.reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (const auto* vs_info = infos[u32(Shader::LogicalStage::Vertex)];
|
||||
vs_info && vs_info->has_fetch_shader && !preload_fetch_shader) {
|
||||
// A pipeline that would draw without its vertex input is not preloaded: it is compiled
|
||||
// when the title first draws with it, from the fetch shader it uses then.
|
||||
LOG_WARNING(Render_Vulkan,
|
||||
"Not preloading a pipeline: vertex shader {:#x} has a fetch shader, but the "
|
||||
"cache has no fetch data for it",
|
||||
vs_info->pgm_hash);
|
||||
infos.fill(nullptr);
|
||||
modules.fill(nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (std::ranges::any_of(infos, [](const Shader::Info* info) {
|
||||
return info != nullptr && UsesSharpsToLayOut(*info);
|
||||
})) {
|
||||
infos.fill(nullptr);
|
||||
modules.fill(nullptr);
|
||||
preload_fetch_shader.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto [it, is_new] = graphics_pipelines.try_emplace(graphics_key);
|
||||
ASSERT(is_new);
|
||||
|
||||
it.value() = std::make_unique<GraphicsPipeline>(
|
||||
instance, scheduler, desc_heap, profile, graphics_key, *pipeline_cache, infos,
|
||||
runtime_infos, fetch_shader, modules, sdata, true);
|
||||
runtime_infos, preload_fetch_shader, modules, sdata, true);
|
||||
|
||||
infos.fill(nullptr);
|
||||
modules.fill(nullptr);
|
||||
fetch_shader.reset();
|
||||
preload_fetch_shader.reset();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -256,9 +297,15 @@ bool PipelineCache::LoadPipelineStage(Serialization::Archive& ar, size_t stage)
|
||||
Shader::StageSpecialization spec{};
|
||||
spec.info = &program->info;
|
||||
size_t perm_idx{};
|
||||
if (!LoadShaderMeta(ar, program->info, fetch_shader, spec, perm_idx)) {
|
||||
std::optional<Shader::Gcn::FetchShaderData> stage_fetch{};
|
||||
if (!LoadShaderMeta(ar, program->info, stage_fetch, spec, perm_idx)) {
|
||||
return false;
|
||||
}
|
||||
// Only the stage that has a fetch shader (the vertex stage) sets the pipeline's fetch data,
|
||||
// as at runtime. A later stage, a geometry shader above all, has none and must not clear it.
|
||||
if (stage_fetch) {
|
||||
preload_fetch_shader = std::move(stage_fetch);
|
||||
}
|
||||
|
||||
std::vector<u32> spv{};
|
||||
Storage::DataBase::Instance().Load(Storage::BlobType::ShaderBinary,
|
||||
@@ -278,6 +325,9 @@ bool PipelineCache::LoadPipelineStage(Serialization::Archive& ar, size_t stage)
|
||||
module = CompileSPV(spv, instance.GetDevice());
|
||||
it_pgm.value() = std::move(program);
|
||||
} else {
|
||||
// The permutation joins the program already cached: its specialization must point at
|
||||
// that program's info, not at the one this load made, which is gone when it returns.
|
||||
spec.info = &it_pgm.value()->info;
|
||||
const auto& it = std::ranges::find(it_pgm.value()->modules, spec, &Program::Module::spec);
|
||||
if (it != it_pgm.value()->modules.end()) {
|
||||
// If the permutation is already preloaded, make sure it has the same permutation index
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "core/vr/late_frame_wait.h"
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
using namespace std::chrono_literals;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Core::Vr::LateFrameResult;
|
||||
int main() {
|
||||
int failures = 0;
|
||||
std::mutex mutex;
|
||||
Core::Vr::LateFrameSignal delivered;
|
||||
bool ready = false;
|
||||
auto run = [&](auto woke, auto last, float ms) {
|
||||
std::unique_lock lock{mutex};
|
||||
return Core::Vr::WaitForLateFrame(delivered, lock, woke, last, 90, ms,
|
||||
[&] { return ready; });
|
||||
};
|
||||
auto now = Clock::now();
|
||||
if (run(now, now, 0) != LateFrameResult::Skipped)
|
||||
++failures;
|
||||
if (run(now, now - 1s, 3) != LateFrameResult::Skipped)
|
||||
++failures;
|
||||
if (run(now, Clock::time_point{}, 3) != LateFrameResult::Skipped)
|
||||
++failures;
|
||||
ready = true;
|
||||
if (run(now - 1s, now, 3) != LateFrameResult::Ready)
|
||||
++failures;
|
||||
ready = false;
|
||||
now = Clock::now();
|
||||
if (run(now - 10ms, now - 11ms, 3) != LateFrameResult::TimedOut)
|
||||
++failures;
|
||||
std::jthread producer{[&] {
|
||||
std::this_thread::sleep_for(1ms);
|
||||
std::scoped_lock lock{mutex};
|
||||
ready = true;
|
||||
delivered.Notify();
|
||||
}};
|
||||
now = Clock::now();
|
||||
if (run(now, now, 30) != LateFrameResult::Ready)
|
||||
++failures;
|
||||
producer.join();
|
||||
ready = false;
|
||||
std::jthread spurious{[&] {
|
||||
std::this_thread::sleep_for(1ms);
|
||||
delivered.Notify();
|
||||
}};
|
||||
now = Clock::now();
|
||||
if (run(now, now, 3) != LateFrameResult::TimedOut)
|
||||
++failures;
|
||||
spurious.join();
|
||||
std::vector<double> waits;
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
now = Clock::now();
|
||||
if (run(now, now, 3) != LateFrameResult::TimedOut)
|
||||
++failures;
|
||||
waits.push_back(
|
||||
std::chrono::duration<double, std::milli>(Clock::now() - now).count());
|
||||
}
|
||||
std::sort(waits.begin(), waits.end());
|
||||
printf("3-ms timeout: median %.3f p95 %.3f max %.3f ms\n", waits[25],
|
||||
waits[47], waits.back());
|
||||
printf("late-frame disabled/stale/race/deadline/arrival/spurious checks: %d "
|
||||
"failures\n",
|
||||
failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "shader_recompiler/info.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
int main() {
|
||||
auto source = std::make_unique<Shader::InfoPersistent>();
|
||||
for (unsigned i = 0; i < Shader::NUM_IMAGES; ++i)
|
||||
source->images.push_back(Shader::ImageResource{.sharp_idx = i + 17});
|
||||
for (unsigned i = 0; i < Shader::NUM_SAMPLERS; ++i)
|
||||
source->samplers.push_back(Shader::SamplerResource{.sharp_idx = i + 91});
|
||||
std::vector<unsigned char> bytes(sizeof(*source));
|
||||
std::memcpy(bytes.data(), source.get(), bytes.size());
|
||||
auto restored = std::make_unique<Shader::InfoPersistent>();
|
||||
std::memcpy(restored.get(), bytes.data(), bytes.size());
|
||||
source.reset(); // Original process/object storage is gone, as on a warm cache
|
||||
// launch.
|
||||
int failures = 0;
|
||||
auto inside = [&](const void *p) {
|
||||
auto at = reinterpret_cast<uintptr_t>(p),
|
||||
begin = reinterpret_cast<uintptr_t>(restored.get());
|
||||
return at >= begin && at < begin + sizeof(*restored);
|
||||
};
|
||||
if (!inside(restored->images.data()) || !inside(restored->samplers.data()))
|
||||
++failures;
|
||||
if (restored->images.size() != Shader::NUM_IMAGES ||
|
||||
restored->samplers.size() != Shader::NUM_SAMPLERS)
|
||||
++failures;
|
||||
for (unsigned i = 0; i < Shader::NUM_IMAGES; ++i)
|
||||
if (restored->images[i].sharp_idx != i + 17)
|
||||
++failures;
|
||||
for (unsigned i = 0; i < Shader::NUM_SAMPLERS; ++i)
|
||||
if (restored->samplers[i].sharp_idx != i + 91)
|
||||
++failures;
|
||||
printf("persistent shader image/sampler lists survive byte relocation and "
|
||||
"original destruction at full capacity: %d failures\n",
|
||||
failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user