mirror of
https://github.com/niakw/Prospero.Eden-Encore.git
synced 2026-10-17 13:01:16 +02:00
R433: bypass texture cache read mutex on never-registered pages (bounded PS5 Vulkan fast path) [core-ready]
This commit is contained in:
1 parent
f302ebbb8c
commit
9742ccd704
7 files changed
+170
-3
No files matched your search
@@ -44,6 +44,7 @@ jobs:
|
||||
python3 -B tools/check-ps5-pipeline-build-wait.py
|
||||
python3 -B tools/check-ps5-pipeline-fairness.py
|
||||
python3 -B tools/check-upstream-texture-memory-r428.py
|
||||
python3 -B tools/check-ps5-image-pages-r433.py
|
||||
python3 -B tools/check-ps5-present-phase-timing.py
|
||||
python3 -B tools/check-ps5-first-use-draw-preservation.py
|
||||
python3 -B tools/check-ps5-gpu-worker-draw-guard.py
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# R433 — PS5 Vulkan texture-cache read lock avoidance
|
||||
|
||||
Ported selectively from [ProsperoEden d7f2764](https://github.com/blackbearreloaded/ProsperoEden/commit/d7f2764f8ecf8bdbd979eece2b402621edceee2f) without merging its launcher, CPU affinity changes, frame-clock scaling, or asynchronous ASTC image policy.
|
||||
|
||||
## Problem
|
||||
|
||||
A guest CPU memory read asks RasterizerVulkan::GetFlushArea whether a GPU-modified texture requires download. Prior Encore always acquires the texture cache mutex—even when no texture has EVER been registered at that guest physical memory range. The GPU often holds this mutex across draws, creating contention at steady 30 FPS.
|
||||
|
||||
## Bounded approach
|
||||
|
||||
- A 512 KiB, initially zeroed, process-global atomic bitmap describes the full 16 GiB device-address range at 4 KiB granularity.
|
||||
- TextureCache::RegisterImage marks a dense image's CPU range or EACH sparse segment before adding the slot-map entry; it NEVER clears marks. Registered or formerly registered pages always use the original mutex path.
|
||||
- GetFlushArea fast-returns the unchanged preemptive download-area fallback only for valid ranges with no marked pages.
|
||||
- Out-of-space, overflow, or queries larger than 256 KiB use the original lock. Long-range queries cannot scan unbounded bitmap words on a guest critical path.
|
||||
- Producer writes release and consumer reads acquire; only the bitmap is lock free. The original cache registration, invalidation and readback behavior and lock ordering are unchanged.
|
||||
- Pinned Eden archives remain untouched. The active Vulkan fork and tools/prepare-vulkan-port.py are modified together, with a source-replay oracle in the core preflight.
|
||||
- R433 host C++ tests cover page/word boundaries, large spans, sparse ranges and parallel marking under ASan/UBSan.
|
||||
|
||||
## Caveats and verification
|
||||
|
||||
This is a **lock contention hypothesis**, not a measured FPS gain. The bitset is deliberately monotonic and may become less selective after many game sessions, but never becomes optimistic because of eviction. In concurrent registration, marking precedes slot-map insertion, but a CPU read racing a registration can still linearize before the mark, as with an unsynchronized guest read; rendering fidelity and HUD correctness must be verified on hardware. Until R433 is built, the R432 all-on package is the last native-qualified image.
|
||||
|
||||
No changes to DualSense/gameplay mapping, PS5 firmware affinity, image resolution, shader compilation, texture conversion, or guest frame timing.
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#pragma once
|
||||
// R433: monotonic page occupancy for PS5 Vulkan texture-cache lock elision.
|
||||
// A positive bit only means an image has EVER occupied this CPU address; no
|
||||
// unregister clears it. This is conservative, including between game launches.
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
namespace Eden::ImagePages {
|
||||
inline constexpr unsigned kPageBits = 12; // Core::DEVICE_PAGEBITS
|
||||
inline constexpr std::uint64_t kSpace = std::uint64_t{1} << 34;
|
||||
inline constexpr std::size_t kWords = static_cast<std::size_t>(kSpace >> kPageBits) / 64;
|
||||
inline std::array<std::atomic<std::uint64_t>, kWords> held{};
|
||||
constexpr std::uint64_t Bits(unsigned first, unsigned last) noexcept {
|
||||
return (~std::uint64_t{0} >> (63 - last)) & (~std::uint64_t{0} << first);
|
||||
}
|
||||
// Publish the page BEFORE it is inserted into the registered image slot map.
|
||||
inline void Mark(std::uint64_t address, std::uint64_t size) noexcept {
|
||||
if (size == 0 || address >= kSpace) return;
|
||||
const auto first = address >> kPageBits;
|
||||
const auto last = ((std::min)(size, kSpace - address) + address - 1) >> kPageBits;
|
||||
for (auto word = first / 64; word <= last / 64; ++word) {
|
||||
const auto bits = Bits(word == first / 64 ? first % 64 : 0,
|
||||
word == last / 64 ? last % 64 : 63);
|
||||
if ((held[word].load(std::memory_order_relaxed) & bits) != bits)
|
||||
held[word].fetch_or(bits, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
// Return true ONLY when it is safe to skip the cache lock. Large and invalid
|
||||
// queries use the unchanged blocking path; cap bitmap scanning on guest calls.
|
||||
inline bool NeverHeld(std::uint64_t address, std::uint64_t size) noexcept {
|
||||
if (size == 0) size = 1;
|
||||
if (address >= kSpace || size > kSpace - address) return false;
|
||||
if (size > 256 * 1024) return false;
|
||||
const auto first = address >> kPageBits;
|
||||
const auto last = (address + size - 1) >> kPageBits;
|
||||
for (auto word = first / 64; word <= last / 64; ++word) {
|
||||
const auto bits = Bits(word == first / 64 ? first % 64 : 0,
|
||||
word == last / 64 ? last % 64 : 63);
|
||||
if (held[word].load(std::memory_order_acquire) & bits) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace Eden::ImagePages
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""R433: host-sanitized proof of monotonic texture-page lock-elision boundaries."""
|
||||
import ast
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
header = (root / "headless/image_pages.h").read_text()
|
||||
raster = (root / "vendor/encore-active/eden-vulkan/vulkan_rasterizer.cpp").read_text()
|
||||
cache = (root / "vendor/encore-active/eden-vulkan/vulkan-cache/video_core/texture_cache/texture_cache.h").read_text()
|
||||
generator = (root / "tools/prepare-vulkan-port.py").read_text()
|
||||
ast.parse(generator)
|
||||
assert raster.count("::Eden::ImagePages::NeverHeld(addr, size)") == 1
|
||||
assert raster.index("::Eden::ImagePages::NeverHeld(addr, size)") < raster.index(
|
||||
"::Eden::Performance::GuestCacheLock(texture_cache.mutex, true)",
|
||||
raster.index("RasterizerVulkan::GetFlushArea"))
|
||||
assert cache.count("::Eden::ImagePages::Mark(image.cpu_addr, image.guest_size_bytes);") == 1
|
||||
assert cache.count("::Eden::ImagePages::Mark(cpu_addr, size);") == 1
|
||||
assert cache.index("::Eden::ImagePages::Mark(image.cpu_addr") < cache.index(
|
||||
"slot_map_views.insert(image.gpu_addr")
|
||||
assert cache.index("::Eden::ImagePages::Mark(cpu_addr, size);") < cache.index(
|
||||
"slot_map_views.insert(gpu_addr, cpu_addr")
|
||||
assert "ImagePages::NeverHeld" in generator and generator.count("ImagePages::Mark(") == 2
|
||||
assert "memory_order_acquire" in header and "memory_order_release" in header
|
||||
assert "size > 256 * 1024" in header
|
||||
cxx = shutil.which("c++") or shutil.which("clang++") or shutil.which("g++")
|
||||
assert cxx
|
||||
fixture = r"""
|
||||
#include "image_pages.h"
|
||||
#include <cassert>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
using namespace Eden::ImagePages;
|
||||
int main() {
|
||||
constexpr std::uint64_t p=4096;
|
||||
assert(NeverHeld(0,1) && NeverHeld(kSpace-1,1) && NeverHeld(123456789,64*p));
|
||||
assert(!NeverHeld(kSpace,1) && !NeverHeld(kSpace-1,2));
|
||||
assert(!NeverHeld(0,256*1024+1));
|
||||
Mark(p,1);
|
||||
assert(NeverHeld(p-1,1) && !NeverHeld(p,1) && !NeverHeld(2*p-1,1));
|
||||
assert(NeverHeld(2*p,1) && !NeverHeld(p-1,2));
|
||||
Mark(63*p+5,2*p);
|
||||
assert(!NeverHeld(63*p,1) && !NeverHeld(64*p,1) && !NeverHeld(65*p,1));
|
||||
assert(NeverHeld(66*p,p));
|
||||
Mark(1000*p,9000*p);
|
||||
assert(!NeverHeld(1000*p,1) && !NeverHeld(5000*p,1));
|
||||
assert(NeverHeld(10000*p,p));
|
||||
Mark(kSpace-1,1<<20);
|
||||
assert(!NeverHeld(kSpace-1,1) && !NeverHeld(kSpace-1,2));
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<unsigned> misses{0};
|
||||
for(unsigned i=0;i<8;++i) threads.emplace_back([i,&misses] {
|
||||
auto base=(1000000u+i*1000u)*4096ull;
|
||||
for(unsigned n=0;n<800;++n) {
|
||||
Mark(base+n*4096,p);
|
||||
if(NeverHeld(base+n*4096,1)) ++misses;
|
||||
}
|
||||
});
|
||||
for(auto& thread:threads) thread.join();
|
||||
assert(misses.load()==0);
|
||||
for(unsigned i=0;i<8;++i) {
|
||||
auto base=(1000000u+i*1000u)*4096ull;
|
||||
assert(!NeverHeld(base+799*p,1) && NeverHeld(base+800*p,1));
|
||||
}
|
||||
}
|
||||
"""
|
||||
with tempfile.TemporaryDirectory(prefix="eden-r433-image-pages-") as tmp:
|
||||
source = Path(tmp) / "test.cpp"
|
||||
exe = Path(tmp) / "test"
|
||||
source.write_text(fixture)
|
||||
subprocess.run([cxx, "-std=c++20", "-O1", "-pthread",
|
||||
"-fsanitize=address,undefined", "-fno-omit-frame-pointer",
|
||||
"-Wall", "-Wextra", "-Werror", "-I" + str(root / "headless"),
|
||||
str(source), "-o", str(exe)], check=True, timeout=90)
|
||||
subprocess.run([str(exe)], check=True, timeout=90)
|
||||
print("PASS R433: registered pages never fast-skipped; bounded scanning, concurrent marking, source replay guards")
|
||||
@@ -75,12 +75,12 @@ void RasterizerVulkan::DispatchCompute() {"""),
|
||||
# when the guest enables occlusion counting.
|
||||
('const bool can_defer_clear = ENABLE_DEFERRED_CLEAR &&',
|
||||
'const bool can_defer_clear = ENABLE_DEFERRED_CLEAR && !regs.zpass_pixel_count_enable &&'),
|
||||
('#include <algorithm>', '#include <algorithm>\n#include "performance.h"'),
|
||||
('#include <algorithm>', '#include <algorithm>\n#include "performance.h"\n#include "image_pages.h"'),
|
||||
# Guest threads take the cache locks here (tracked-page writes, flush-area reads).
|
||||
('bool RasterizerVulkan::OnCPUWrite(DAddr addr, u64 size) {',
|
||||
'bool RasterizerVulkan::OnCPUWrite(DAddr addr, u64 size) {\n ::Eden::Performance::BatchedGuestWriteTimer guest_write_timer;\n const bool sample_guest_write = ::Eden::Performance::ShouldSampleGuestWrite();'),
|
||||
('VideoCore::RasterizerDownloadArea RasterizerVulkan::GetFlushArea(DAddr addr, u64 size) {',
|
||||
'VideoCore::RasterizerDownloadArea RasterizerVulkan::GetFlushArea(DAddr addr, u64 size) {\n ::Eden::Performance::DiagnosticTimer guest_read_timer(::Eden::Performance::guest_cpu_read);'),
|
||||
"VideoCore::RasterizerDownloadArea RasterizerVulkan::GetFlushArea(DAddr addr, u64 size) {\n ::Eden::Performance::DiagnosticTimer guest_read_timer(::Eden::Performance::guest_cpu_read);\n // A guest read cannot overlap a registered image when no page in this\n // range has ever appeared in the image slot map. Skip the GPU-held mutex.\n // Unknown/out-of-range/large ranges retain the original blocking path.\n if (::Eden::ImagePages::NeverHeld(addr, size)) {\n return VideoCore::RasterizerDownloadArea{\n .start_address = Common::AlignDown(addr, Core::DEVICE_PAGESIZE),\n .end_address = Common::AlignUp(addr + size, Core::DEVICE_PAGESIZE),\n .preemtive = true,\n };\n }"),
|
||||
# ... and wait for those locks spinning briefly before sleeping (performance.h GuestCacheLock).
|
||||
(' std::scoped_lock lock{texture_cache.mutex};\n auto area = texture_cache.GetFlushArea(addr, size);',
|
||||
' ::Eden::Performance::GuestCacheLock(texture_cache.mutex, true);\n'
|
||||
@@ -1277,7 +1277,10 @@ texture_costs.extend([
|
||||
])
|
||||
adapt('src/video_core/texture_cache/texture_cache.h',
|
||||
'vulkan-cache/video_core/texture_cache/texture_cache.h', [
|
||||
('namespace VideoCommon {', '#include "performance.h"\n#include "vulkan_gc_budget.h"\n#include "common/scope_exit.h"\nnamespace Vulkan { class TextureCacheRuntime; }\n\nnamespace VideoCommon {'),
|
||||
('namespace VideoCommon {', '#include "performance.h"\n#include "image_pages.h"\n#include "vulkan_gc_budget.h"\n#include "common/scope_exit.h"\nnamespace Vulkan { class TextureCacheRuntime; }\n\nnamespace VideoCommon {'),
|
||||
# R433: publish monotonic page occupancy before a slot-map entry exists.
|
||||
(" auto map_id =\n slot_map_views.insert(image.gpu_addr, image.cpu_addr, image.guest_size_bytes, image_id);", " ::Eden::ImagePages::Mark(image.cpu_addr, image.guest_size_bytes);\n auto map_id =\n slot_map_views.insert(image.gpu_addr, image.cpu_addr, image.guest_size_bytes, image_id);"),
|
||||
(" auto map_id = slot_map_views.insert(gpu_addr, cpu_addr, size, image_id);", " ::Eden::ImagePages::Mark(cpu_addr, size);\n auto map_id = slot_map_views.insert(gpu_addr, cpu_addr, size, image_id);"),
|
||||
(download_prefix, download_prefix + (port / 'vulkan_download_batch.inc').read_text() + '\n'),
|
||||
(gc_original, gc),
|
||||
# Vulkan's total_used_memory may be a driver-reported snapshot rather than
|
||||
|
||||
+3
@@ -27,6 +27,7 @@
|
||||
#include "video_core/textures/decoders.h"
|
||||
|
||||
#include "performance.h"
|
||||
#include "image_pages.h"
|
||||
#include "vulkan_gc_budget.h"
|
||||
#include "common/scope_exit.h"
|
||||
namespace Vulkan { class TextureCacheRuntime; }
|
||||
@@ -2464,6 +2465,7 @@ void TextureCache<P>::RegisterImage(ImageId image_id) {
|
||||
(*channel_state->gpu_page_table)[page].push_back(image_id);
|
||||
});
|
||||
if (False(image.flags & ImageFlagBits::Sparse)) {
|
||||
::Eden::ImagePages::Mark(image.cpu_addr, image.guest_size_bytes);
|
||||
auto map_id =
|
||||
slot_map_views.insert(image.gpu_addr, image.cpu_addr, image.guest_size_bytes, image_id);
|
||||
ForEachCPUPage(image.cpu_addr, image.guest_size_bytes,
|
||||
@@ -2474,6 +2476,7 @@ void TextureCache<P>::RegisterImage(ImageId image_id) {
|
||||
boost::container::small_vector<ImageViewId, 16> sparse_maps;
|
||||
ForEachSparseSegment(
|
||||
image, [this, image_id, &sparse_maps](GPUVAddr gpu_addr, DAddr cpu_addr, size_t size) {
|
||||
::Eden::ImagePages::Mark(cpu_addr, size);
|
||||
auto map_id = slot_map_views.insert(gpu_addr, cpu_addr, size, image_id);
|
||||
ForEachCPUPage(cpu_addr, size,
|
||||
[this, map_id](u64 page) { page_table[page].push_back(map_id); });
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include "performance.h"
|
||||
#include "image_pages.h"
|
||||
#include "dev_vulkan.h"
|
||||
#include "diagnostics.h"
|
||||
#include <filesystem>
|
||||
@@ -893,6 +894,17 @@ bool RasterizerVulkan::MustFlushRegion(DAddr addr, u64 size, VideoCommon::CacheT
|
||||
|
||||
VideoCore::RasterizerDownloadArea RasterizerVulkan::GetFlushArea(DAddr addr, u64 size) {
|
||||
::Eden::Performance::DiagnosticTimer guest_read_timer(::Eden::Performance::guest_cpu_read);
|
||||
// A guest read cannot overlap a registered image when no page in this
|
||||
// range has ever appeared in the image slot map. Skip the GPU-held mutex.
|
||||
// Unknown/out-of-range/large ranges retain the original blocking path.
|
||||
if (::Eden::ImagePages::NeverHeld(addr, size)) {
|
||||
return VideoCore::RasterizerDownloadArea{
|
||||
.start_address = Common::AlignDown(addr, Core::DEVICE_PAGESIZE),
|
||||
.end_address = Common::AlignUp(addr + size, Core::DEVICE_PAGESIZE),
|
||||
.preemtive = true,
|
||||
};
|
||||
}
|
||||
|
||||
{
|
||||
::Eden::Performance::GuestCacheLock(texture_cache.mutex, true);
|
||||
std::lock_guard lock{texture_cache.mutex, std::adopt_lock};
|
||||
|
||||
Reference in new issue
Block a user