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 4ec9dbf9..50544238 100644 --- a/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/command_processor.h +++ b/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/command_processor.h @@ -696,6 +696,13 @@ class D3D12CommandProcessor : public CommandProcessor { std::atomic vertex_buffer_memory_invalidated_{false}; void* vertex_buffer_memory_invalidation_callback_handle_ = nullptr; + // AC6 HD terrain: vertex fetch constant 95 of gated terrain draws is + // redirected to the synthetic full-density ring table (one game-global + // system-heap guest allocation); guest registers and memory stay untouched. + bool AC6TerrainHdEnsureRing(); + bool ac6_hd_active_ = false; + uint32_t ac6_hd_ring_phys_ = 0; // UINT32_MAX = allocation failed + 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 1fc336b8..7e7040e9 100644 --- a/thirdparty/rexglue-sdk/include/rex/graphics/flags.h +++ b/thirdparty/rexglue-sdk/include/rex/graphics/flags.h @@ -89,6 +89,7 @@ REXCVAR_DECLARE(std::string, dump_shaders); REXCVAR_DECLARE(bool, dxbc_switch); REXCVAR_DECLARE(bool, dxbc_source_map); REXCVAR_DECLARE(bool, vfetch_index_rounding_bias); +REXCVAR_DECLARE(bool, ac6_terrain_hd); // GPU Tracing REXCVAR_DECLARE(std::string, trace_gpu_prefix); diff --git a/thirdparty/rexglue-sdk/include/rex/graphics/primitive_processor.h b/thirdparty/rexglue-sdk/include/rex/graphics/primitive_processor.h index eed9e1b3..f7903115 100644 --- a/thirdparty/rexglue-sdk/include/rex/graphics/primitive_processor.h +++ b/thirdparty/rexglue-sdk/include/rex/graphics/primitive_processor.h @@ -170,6 +170,9 @@ class PrimitiveProcessor { // memory). bool Process(ProcessingResult& result_out); + // AC6: set by the command processor before Process() for terrain draws. + void SetAc6TerrainFanHd(bool enabled) { ac6_terrain_fan_hd_ = enabled; } + // Invalidates the cache within the range. std::pair MemoryInvalidationCallback(uint32_t physical_address_start, uint32_t length, bool exact_range); @@ -515,6 +518,33 @@ class PrimitiveProcessor { } } + // AC6: full-resolution ("HD") terrain fan emission. With the synthetic HD + // ring (see the D3D12 command processor), a terrain fan's 9 usable slots + // address the full 3x3 height samples of its 2x2-cell patch (row-major: + // slot k = sample (k / 3, k % 3)); this draws the patch as 2 triangles per + // cell (world-anchored diagonal, authored cw winding) - 24 output indices, + // the same count as the plain conversion. Only correct together with the + // synthetic ring - both are gated by the command processor. + template + static void TriangleFanToListHdAc6(Index* dest, const Index* source, + const IndexTransform& index_transform) { + Index s[9]; + for (uint32_t i = 0; i < 9; ++i) { + s[i] = index_transform(source[i]); + } + // Cells (r, c): tris (A, B, D) + (A, D, C) with A = 3r+c, B = A+3 (next + // row), C = A+1 (next column), D = A+4 (diagonal). + static constexpr uint8_t kHdPattern[8][3] = { + {0, 3, 4}, {0, 4, 1}, {1, 4, 5}, {1, 5, 2}, + {3, 6, 7}, {3, 7, 4}, {4, 7, 8}, {4, 8, 5}, + }; + for (const auto& tri : kHdPattern) { + *(dest++) = s[tri[0]]; + *(dest++) = s[tri[1]]; + *(dest++) = s[tri[2]]; + } + } + static constexpr uint32_t GetLineLoopStripIndexCount(uint32_t loop_index_count) { // Even if 2 vertices are supplied, two lines are still drawn between them. // https://www.khronos.org/opengl/wiki/Primitive @@ -595,14 +625,21 @@ class PrimitiveProcessor { xenos::PrimitiveType source_primitive_type, const IndexTransform& index_transform, PrimitiveRangeIterator ranges_beginning, - PrimitiveRangeIterator ranges_end) { + PrimitiveRangeIterator ranges_end, + bool ac6_hd_fans = false) { Index* dest_write_ptr = dest; switch (source_primitive_type) { case xenos::PrimitiveType::kTriangleFan: for (PrimitiveRangeIterator range_it = ranges_beginning; range_it != ranges_end; ++range_it) { - TriangleFanToList(dest_write_ptr, source + range_it->guest_offset, - range_it->guest_index_count, index_transform); + if (ac6_hd_fans && range_it->guest_index_count == 10) { + // AC6 terrain fan - emit the full-resolution pattern. + TriangleFanToListHdAc6(dest_write_ptr, source + range_it->guest_offset, + index_transform); + } else { + TriangleFanToList(dest_write_ptr, source + range_it->guest_offset, + range_it->guest_index_count, index_transform); + } dest_write_ptr += range_it->host_index_count; } break; @@ -633,6 +670,9 @@ class PrimitiveProcessor { SharedMemory& shared_memory_; bool full_32bit_vertex_indices_used_ = false; + // AC6: per-draw request from the command processor to emit the + // full-resolution terrain fan pattern (see TriangleFanToListHdAc6). + bool ac6_terrain_fan_hd_ = false; bool convert_triangle_fans_to_lists_ = false; bool convert_line_loops_to_strips_ = false; bool convert_quad_lists_to_triangle_lists_ = false; @@ -670,13 +710,15 @@ class PrimitiveProcessor { // index conversion used by non-kVertex host vertex shader types on // backends not supporting full 32-bit index fetch in this path. uint32_t non_vertex_32bit_dma_to_24bit : 1; // 60 + // Converted with the AC6 full-resolution terrain emission. + uint32_t ac6_fan_hd : 1; // 61 }; CacheKey() : key(0) { static_assert_size(*this, sizeof(key)); } CacheKey(uint32_t base, uint32_t count, xenos::IndexFormat format, xenos::Endian endian, bool is_reset_enabled, xenos::PrimitiveType conversion_guest_primitive_type = xenos::PrimitiveType::kNone, - bool non_vertex_32bit_dma_to_24bit = false) { + bool non_vertex_32bit_dma_to_24bit = false, bool ac6_fan_hd = false) { // Clear unused bits, then set each field explicitly, not via the // initializer list (which causes `uint64_t key = 0;` to be ignored, and // also can't contain initializers for aliasing union members). @@ -688,6 +730,7 @@ class PrimitiveProcessor { this->is_reset_enabled = is_reset_enabled; this->conversion_guest_primitive_type = conversion_guest_primitive_type; this->non_vertex_32bit_dma_to_24bit = non_vertex_32bit_dma_to_24bit; + this->ac6_fan_hd = ac6_fan_hd; } struct Hasher { diff --git a/thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp b/thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp index 857e686c..c335acc7 100644 --- a/thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/d3d12/command_processor.cpp @@ -211,6 +211,50 @@ void D3D12CommandProcessor::InvalidateGpuMemory() { } } +bool D3D12CommandProcessor::AC6TerrainHdEnsureRing() { + if (ac6_hd_ring_phys_ == UINT32_MAX) { + return false; + } + if (ac6_hd_ring_phys_) { + return true; + } + // Build the synthetic ring: for each of the group's 4 fans (patch bases + // (0,0), (2,0), (0,2), (2,2) in (row, col) cells), slots (10f+1+k) mod 40 + // hold sample k of the fan's full 3x3 grid (row-major; the +1 is the + // shader's ring phase). Slots 0/10/20/30 are never referenced. The ring is + // game-global - the authored one is byte-identical across maps. + static const uint16_t kPatchBase[4][2] = {{0, 0}, {2, 0}, {0, 2}, {2, 2}}; + uint16_t entries[80] = {}; + for (uint32_t f = 0; f < 4; ++f) { + for (uint32_t k = 0; k < 9; ++k) { + uint32_t slot = (10 * f + 1 + k) % 40; + entries[slot * 2 + 0] = uint16_t(kPatchBase[f][0] + k / 3); // row + entries[slot * 2 + 1] = uint16_t(kPatchBase[f][1] + k % 3); // col + } + } + memory::Memory* memory = kernel_state_->memory(); + uint32_t ring_virt = memory->SystemHeapAlloc(sizeof(entries), 256, memory::kSystemHeapPhysical); + if (!ring_virt) { + REXGPU_ERROR("AC6 HD terrain: failed to allocate the synthetic ring table"); + ac6_hd_ring_phys_ = UINT32_MAX; + return false; + } + uint32_t* ring_host = memory->TranslateVirtual(ring_virt); + const uint32_t* words = reinterpret_cast(entries); + for (uint32_t i = 0; i < sizeof(entries) / sizeof(uint32_t); ++i) { + // Guest vertex fetch tables are 8-in-32 big-endian. + ring_host[i] = rex::byte_swap(words[i]); + } + ac6_hd_ring_phys_ = memory->GetPhysicalAddress(ring_virt); + if (ac6_hd_ring_phys_ == UINT32_MAX) { + REXGPU_ERROR("AC6 HD terrain: synthetic ring allocation has no physical address"); + return false; + } + REXGPU_INFO("AC6 HD terrain: synthetic ring at guest 0x{:08X} (virtual 0x{:08X})", + ac6_hd_ring_phys_, ring_virt); + return true; +} + void D3D12CommandProcessor::InvalidateAllVertexBufferResidency() { vertex_buffers_in_sync_[0] = 0; vertex_buffers_in_sync_[1] = 0; @@ -2736,6 +2780,29 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3 return false; } + // AC6 HD terrain: the terrain vertex shader is fully data-driven, so a + // synthetic ring table (vertex fetch 95 redirect) plus a full-density fan + // emission make the unmodified shader draw every shipped height sample - + // no more T-junction cracks ("rifts"), and river beds render true. + { + bool ac6_hd = false; + if (REXCVAR_GET(ac6_terrain_hd) && + primitive_type == xenos::PrimitiveType::kTriangleFan) { + uint64_t vs_ucode_hash = vertex_shader->ucode_data_hash(); + ac6_hd = (vs_ucode_hash == UINT64_C(0x042F34FADAD3F370) || + vs_ucode_hash == UINT64_C(0xD113DCDC8F6AC408)) && + AC6TerrainHdEnsureRing(); + } + if (ac6_hd != ac6_hd_active_) { + // The effective vertex fetch constant 95 changes without a guest + // register write - drop the residency shortcut and the uploaded copy. + ac6_hd_active_ = ac6_hd; + vertex_buffers_in_sync_[95 >> 6] &= ~(uint64_t(1) << (95 & 63)); + cbuffer_binding_fetch_.up_to_date = false; + } + primitive_processor_->SetAc6TerrainFanHd(ac6_hd); + } + // Process primitives. PrimitiveProcessor::ProcessingResult primitive_processing_result; if (!primitive_processor_->Process(primitive_processing_result)) { @@ -3048,6 +3115,12 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3 continue; } xenos::xe_gpu_vertex_fetch_t vfetch_constant = regs.GetVertexFetch(vfetch_index); + if (ac6_hd_active_ && vfetch_index == 95) { + // AC6 HD terrain: request residency for the synthetic ring, not for + // the authored ring the guest fetch constant points to. + vfetch_constant.address = ac6_hd_ring_phys_ >> 2; + vfetch_constant.size = 40; + } switch (vfetch_constant.type) { case xenos::FetchConstantType::kVertex: break; @@ -4806,6 +4879,13 @@ bool D3D12CommandProcessor::UpdateBindings(const D3D12Shader* vertex_shader, return false; } std::memcpy(fetch_constants, ®s[XE_GPU_REG_SHADER_CONSTANT_FETCH_00_0], kFetchConstantsSize); + if (ac6_hd_active_) { + // AC6 HD terrain: point vertex fetch constant 95 at the synthetic + // ring. Address bits only - type, endian and size stay authentic. + uint32_t* fetch_dwords = reinterpret_cast(fetch_constants); + fetch_dwords[95 * 2] = + (ac6_hd_ring_phys_ & ~uint32_t(3)) | (fetch_dwords[95 * 2] & uint32_t(3)); + } cbuffer_binding_fetch_.up_to_date = true; current_graphics_root_up_to_date_ &= ~(1u << root_parameter_fetch_constants); } diff --git a/thirdparty/rexglue-sdk/src/graphics/flags.cpp b/thirdparty/rexglue-sdk/src/graphics/flags.cpp index 879c6b37..01010a6d 100644 --- a/thirdparty/rexglue-sdk/src/graphics/flags.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/flags.cpp @@ -31,6 +31,10 @@ REXCVAR_DEFINE_BOOL(use_fuzzy_alpha_epsilon, true, "GPU", REXCVAR_DEFINE_BOOL(vfetch_index_rounding_bias, false, "GPU/Shader", "Apply small epsilon bias to vertex fetch indices before " "flooring to fix black triangles caused by RCP precision"); +REXCVAR_DEFINE_BOOL(ac6_terrain_hd, true, "AC6/Enhancements", + "Draw the terrain at the full shipped resolution (2x what " + "the game draws), also removes the terrain cracks.") + .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); REXCVAR_DEFINE_BOOL(draw_resolution_scaled_texture_offsets, true, "GPU/Shader", "Scale texture offsets with draw resolution"); // The param_gen pair and the ac6_* hash lists below are consumed at shader diff --git a/thirdparty/rexglue-sdk/src/graphics/primitive_processor.cpp b/thirdparty/rexglue-sdk/src/graphics/primitive_processor.cpp index 19ee54ac..a500d654 100644 --- a/thirdparty/rexglue-sdk/src/graphics/primitive_processor.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/primitive_processor.cpp @@ -644,7 +644,8 @@ bool PrimitiveProcessor::Process(ProcessingResult& result_out) { trace_writer_.WriteMemoryRead(guest_index_base, guest_index_buffer_needed_bytes); CacheTransaction cache_transaction( *this, CacheKey(guest_index_base, guest_draw_vertex_count, guest_index_format, - guest_index_endian, guest_primitive_reset_enabled, guest_primitive_type)); + guest_index_endian, guest_primitive_reset_enabled, guest_primitive_type, + false, ac6_terrain_fan_hd_)); if (cache_transaction.GetFoundResult()) { cacheable = *cache_transaction.GetFoundResult(); } else { @@ -696,7 +697,7 @@ bool PrimitiveProcessor::Process(ProcessingResult& result_out) { } ConvertSinglePrimitiveRanges( host_indices, guest_indices, guest_primitive_type, PassthroughIndexTransform(), - single_primitive_ranges_.cbegin(), single_primitive_ranges_.cend()); + single_primitive_ranges_.cbegin(), single_primitive_ranges_.cend(), ac6_terrain_fan_hd_); } else { // 32-bit indices - may need to pre-swap and pre-mask also if the host // doesn't support full 32-bit vertex indices. @@ -727,32 +728,32 @@ bool PrimitiveProcessor::Process(ProcessingResult& result_out) { if (full_32bit_vertex_indices_used_) { ConvertSinglePrimitiveRanges( host_indices, guest_indices, guest_primitive_type, PassthroughIndexTransform(), - single_primitive_ranges_beginning, single_primitive_ranges_end); + single_primitive_ranges_beginning, single_primitive_ranges_end, ac6_terrain_fan_hd_); } else { switch (guest_index_endian) { case xenos::Endian::kNone: ConvertSinglePrimitiveRanges(host_indices, guest_indices, guest_primitive_type, To24NonSwappingIndexTransform(), single_primitive_ranges_beginning, - single_primitive_ranges_end); + single_primitive_ranges_end, ac6_terrain_fan_hd_); break; case xenos::Endian::k8in16: ConvertSinglePrimitiveRanges(host_indices, guest_indices, guest_primitive_type, To24Swapping8In16IndexTransform(), single_primitive_ranges_beginning, - single_primitive_ranges_end); + single_primitive_ranges_end, ac6_terrain_fan_hd_); break; case xenos::Endian::k8in32: ConvertSinglePrimitiveRanges(host_indices, guest_indices, guest_primitive_type, To24Swapping8In32IndexTransform(), single_primitive_ranges_beginning, - single_primitive_ranges_end); + single_primitive_ranges_end, ac6_terrain_fan_hd_); break; case xenos::Endian::k16in32: ConvertSinglePrimitiveRanges(host_indices, guest_indices, guest_primitive_type, To24Swapping16In32IndexTransform(), single_primitive_ranges_beginning, - single_primitive_ranges_end); + single_primitive_ranges_end, ac6_terrain_fan_hd_); break; default: assert_unhandled_case(guest_index_endian);