From bb4f4855ac243ee21422bb25f4442024e7c6bfe0 Mon Sep 17 00:00:00 2001 From: Dipshet <264011288+Dipshet@users.noreply.github.com> Date: Wed, 8 Jul 2026 02:14:02 +0300 Subject: [PATCH] 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). --- .../rex/graphics/d3d12/command_processor.h | 8 +++++ .../rexglue-sdk/include/rex/graphics/flags.h | 3 ++ .../src/graphics/d3d12/command_processor.cpp | 32 +++++++++++++++++++ thirdparty/rexglue-sdk/src/graphics/flags.cpp | 4 +++ thirdparty/rexglue-sdk/src/system/xmemory.cpp | 19 +++++++++++ 5 files changed, 66 insertions(+) diff --git a/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/command_processor.h b/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/command_processor.h index bc1f550c..4ec9dbf9 100644 --- a/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/command_processor.h +++ b/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/command_processor.h @@ -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 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 vertex_buffer_states_{}; uint64_t vertex_buffers_in_sync_[2] = {}; + std::atomic vertex_buffer_memory_invalidated_{false}; + void* vertex_buffer_memory_invalidation_callback_handle_ = nullptr; std::atomic pix_capture_requested_ = false; bool pix_capturing_; diff --git a/thirdparty/rexglue-sdk/include/rex/graphics/flags.h b/thirdparty/rexglue-sdk/include/rex/graphics/flags.h index 02319267..cca4bfee 100644 --- a/thirdparty/rexglue-sdk/include/rex/graphics/flags.h +++ b/thirdparty/rexglue-sdk/include/rex/graphics/flags.h @@ -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 diff --git a/thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp b/thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp index 2e44d371..59108668 100644 --- a/thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp @@ -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 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(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]; diff --git a/thirdparty/rexglue-sdk/src/graphics/flags.cpp b/thirdparty/rexglue-sdk/src/graphics/flags.cpp index da11d6e4..1d1e3e10 100644 --- a/thirdparty/rexglue-sdk/src/graphics/flags.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/flags.cpp @@ -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; diff --git a/thirdparty/rexglue-sdk/src/system/xmemory.cpp b/thirdparty/rexglue-sdk/src/system/xmemory.cpp index ca7f4e86..3c0c5f9c 100644 --- a/thirdparty/rexglue-sdk/src/system/xmemory.cpp +++ b/thirdparty/rexglue-sdk/src/system/xmemory.cpp @@ -29,6 +29,7 @@ // TODO(benvanik): move xbox.h out #include +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 glob reinterpret_cast(host_address) & ~(uintptr_t(host_page_size - 1)); if (rex::memory::Protect(reinterpret_cast(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})",