mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-21 23:00:53 -04:00
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:
@@ -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_;
|
||||
|
||||
|
||||
@@ -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, ®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<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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user