Fix invisible missile/jet trails (stale vertex-buffer residency)

Missile/jet trail position history is a fixed guest-address ring, so its
vertex fetch constants never change and the command processor's
vertex_buffer_states_ address cache skipped RequestRange forever, the GPU
copy of the trail history froze at zero (trails invisible) while CPU memory was
correct.

Register a physical-memory invalidation callback that flags when the CPU writes
watched GPU-visible memory, and drop the cached vertex-buffer states at the next
draw so the ring's pages get re-uploaded. Also notify those callbacks from the
access-violation page-recovery path (xmemory), since a recovered page becomes
read-write and would otherwise never fault again to report the write. Gated by
ac6_fix_trails (default ON).
This commit is contained in:
Dipshet
2026-07-08 02:14:02 +03:00
parent 06868b3780
commit bb4f4855ac
5 changed files with 66 additions and 0 deletions
@@ -437,6 +437,12 @@ class D3D12CommandProcessor : public CommandProcessor {
void InvalidateAllVertexBufferResidency();
void InvalidateVertexBufferResidency(uint32_t vfetch_index);
void InvalidateVertexBufferResidencyRange(uint32_t first_vfetch, uint32_t last_vfetch);
// Registered with the memory system: flags that a CPU write invalidated
// watched GPU-visible memory, so the cached vertex buffer states must be
// dropped before the next draw (otherwise fixed-address buffers never
// re-request their ranges and their GPU copies go permanently stale).
static std::pair<uint32_t, uint32_t> VertexBufferMemoryInvalidationCallbackThunk(
void* context_ptr, uint32_t physical_address_start, uint32_t length, bool exact_range);
void WriteGammaRampSRV(bool is_pwl, D3D12_CPU_DESCRIPTOR_HANDLE handle) const;
@@ -687,6 +693,8 @@ class D3D12CommandProcessor : public CommandProcessor {
};
std::array<VertexBufferState, 96> vertex_buffer_states_{};
uint64_t vertex_buffers_in_sync_[2] = {};
std::atomic<bool> vertex_buffer_memory_invalidated_{false};
void* vertex_buffer_memory_invalidation_callback_handle_ = nullptr;
std::atomic<bool> pix_capture_requested_ = false;
bool pix_capturing_;
+3
View File
@@ -114,4 +114,7 @@ REXCVAR_DECLARE(std::string, render_target_path_d3d12);
REXCVAR_DECLARE(bool, d3d12_readback_memexport);
REXCVAR_DECLARE(bool, d3d12_readback_resolve);
// AC6 game-specific fixes
REXCVAR_DECLARE(bool, ac6_fix_trails);
#define XE_GPU_FINE_GRAINED_DRAW_SCOPES 1
@@ -225,6 +225,15 @@ void D3D12CommandProcessor::InvalidateVertexBufferResidency(uint32_t vfetch_inde
vertex_buffers_in_sync_[vfetch_index >> 6] &= ~(uint64_t(1) << (vfetch_index & 63));
}
std::pair<uint32_t, uint32_t> D3D12CommandProcessor::VertexBufferMemoryInvalidationCallbackThunk(
void* context_ptr, uint32_t physical_address_start, uint32_t length, bool exact_range) {
// Runs on the writing (guest) thread - just flag; the GPU thread drops its
// cached vertex buffer states at the next draw.
auto command_processor = static_cast<D3D12CommandProcessor*>(context_ptr);
command_processor->vertex_buffer_memory_invalidated_.store(true, std::memory_order_release);
return std::make_pair(uint32_t(0), UINT32_MAX);
}
void D3D12CommandProcessor::InvalidateVertexBufferResidencyRange(uint32_t first_vfetch,
uint32_t last_vfetch) {
if (first_vfetch > last_vfetch) {
@@ -1098,6 +1107,16 @@ bool D3D12CommandProcessor::SetupContext() {
REXGPU_ERROR("Failed to initialize shared memory");
return false;
}
// CPU writes to watched GPU-visible memory must drop the cached vertex
// buffer states: the vertex_buffer_states_ address cache otherwise skips
// RequestRanges forever for buffers whose fetch constants never change
// (e.g. AC6's fixed-address trail history ring), so pages invalidated by
// the CPU would never be re-uploaded to the GPU copy.
if (REXCVAR_GET(ac6_fix_trails)) {
vertex_buffer_memory_invalidation_callback_handle_ =
memory_->RegisterPhysicalMemoryInvalidationCallback(
VertexBufferMemoryInvalidationCallbackThunk, this);
}
// Initialize the render target cache before configuring binding - need to
// know if using rasterizer-ordered views for the bindless root signature.
@@ -1905,6 +1924,11 @@ void D3D12CommandProcessor::ShutdownContext() {
render_target_cache_.reset();
if (vertex_buffer_memory_invalidation_callback_handle_) {
memory_->UnregisterPhysicalMemoryInvalidationCallback(
vertex_buffer_memory_invalidation_callback_handle_);
vertex_buffer_memory_invalidation_callback_handle_ = nullptr;
}
shared_memory_.reset();
deferred_command_list_.Reset();
@@ -2932,6 +2956,14 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3
// Must not call anything that can change the descriptor heap from now on!
// Ensure vertex buffers are resident.
if (vertex_buffer_memory_invalidated_.exchange(false, std::memory_order_acquire)) {
// The CPU wrote to watched GPU-visible memory since the last draw: cached
// vertex buffer states may cover invalidated pages, and the address-match
// shortcut below would skip the RequestRanges re-upload forever. Drop the
// cache; the shared-memory validity fast path keeps still-valid buffers
// cheap to re-request.
InvalidateAllVertexBufferResidency();
}
const Shader::ConstantRegisterMap& constant_map_vertex = vertex_shader->constant_register_map();
for (uint32_t i = 0; i < rex::countof(constant_map_vertex.vertex_fetch_bitmap); ++i) {
uint32_t vfetch_bits_remaining = constant_map_vertex.vertex_fetch_bitmap[i];
+4
View File
@@ -37,6 +37,10 @@ REXCVAR_DEFINE_BOOL(gpu_debug_markers, false, "GPU",
"Insert debug markers into GPU command streams for tools "
"like PIX and RenderDoc. Automatically enabled when "
"RenderDoc is detected.");
REXCVAR_DEFINE_BOOL(ac6_fix_trails, true, "AC6",
"Fix invisible missile/jet trails: drop stale cached vertex-buffer "
"residency so the GPU copy of AC6's fixed-address trail history ring "
"refreshes when the CPU writes it. On by default.");
bool IsGpuDebugMarkersEnabled() {
static bool cached = false;
+19
View File
@@ -29,6 +29,7 @@
// TODO(benvanik): move xbox.h out
#include <rex/system/xtypes.h>
REXCVAR_DECLARE(bool, ac6_fix_trails); // defined in graphics/flags.cpp
REXCVAR_DEFINE_BOOL(protect_zero, true, "Memory", "Protect the zero page from reads and writes")
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
@@ -527,6 +528,24 @@ bool Memory::AccessViolationCallback(std::unique_lock<std::recursive_mutex> glob
reinterpret_cast<uintptr_t>(host_address) & ~(uintptr_t(host_page_size - 1));
if (rex::memory::Protect(reinterpret_cast<void*>(page_base), host_page_size,
rex::memory::PageAccess::kReadWrite, nullptr)) {
// The write is about to proceed on a page that cached consumers (e.g.
// the GPU shared-memory copy of guest RAM) may still consider valid.
// Since the page is read-write from here on, no further faults will
// report writes to it, so without an explicit invalidation the cached
// copies go permanently stale (this froze AC6's missile/jet trail
// position history at zero on the GPU while CPU memory was fine).
// Notify the physical-memory invalidation callbacks about the whole
// page so caches refetch it and re-arm their own watches.
uint32_t recovered_physical_address = GetPhysicalAddress(virtual_address);
if (REXCVAR_GET(ac6_fix_trails) && recovered_physical_address != UINT32_MAX) {
uint32_t recovered_page_start =
recovered_physical_address & ~uint32_t(host_page_size - 1);
auto lock = global_critical_region_.Acquire();
for (auto invalidation_callback : physical_memory_invalidation_callbacks_) {
invalidation_callback->first(invalidation_callback->second, recovered_page_start,
uint32_t(host_page_size), true);
}
}
REXSYS_WARN(
"Recovered stale physical page protection for guest {:08X} (host {:016X}, "
"guest_protect {:08X})",