HD terrain: draw the full shipped terrain resolution (ac6_terrain_hd, on)

The game ships 2x the terrain height samples it ever draws; its terrain
vertex shader is fully data-driven, so a synthetic full-density ring table
(vertex fetch constant 95 redirect) plus a matching fan emission make the
unmodified shader express every sample - no more terrain cracks ("rifts"),
true river beds.
This commit is contained in:
Dipshet
2026-08-07 16:42:20 +02:00
parent 93c6dba3ff
commit 90c1ef45ff
6 changed files with 147 additions and 11 deletions
@@ -696,6 +696,13 @@ class D3D12CommandProcessor : public CommandProcessor {
std::atomic<bool> 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<bool> pix_capture_requested_ = false;
bool pix_capturing_;
+1
View File
@@ -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);
@@ -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<uint32_t, uint32_t> 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 <typename Index, typename IndexTransform>
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 {
@@ -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<uint32_t*>(ring_virt);
const uint32_t* words = reinterpret_cast<const uint32_t*>(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, &regs[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<uint32_t*>(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);
}
+4
View File
@@ -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
@@ -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);