Fix lines on open waters seen at an acute angle (undefined shader derivatives)

The water shader's wave-normal lookup sits inside control flow this translator
generates from the guest's predication and jumps, where DXBC derivatives are
undefined: the gradient collapses to zero along the ocean mesh's seams, the LOD
clamps to the finest mip, and grazing Fresnel amplifies that into a bright line.
Computing the gradients in the prologue, where the quad is uniform, restores them.
This commit is contained in:
Dipshet
2026-08-08 20:12:28 +02:00
parent 07d09128e6
commit 98020a02b2
6 changed files with 182 additions and 8 deletions
+20 -4
View File
@@ -18,6 +18,7 @@ REXCVAR_DECLARE(std::string, ac6_neutralize_deswizzle_hashes);
REXCVAR_DECLARE(std::string, ac6_snap_guest_texel_hashes);
REXCVAR_DECLARE(std::string, ac6_densify_x_fetch_hashes);
REXCVAR_DECLARE(std::string, ac6_densify_y_fetch_hashes);
REXCVAR_DECLARE(std::string, ac6_fix_hoisted_fetch_gradients_hashes);
REXCVAR_DECLARE(bool, param_gen_integer_guest_position);
REXCVAR_DECLARE(bool, param_gen_host_subpixel_restore);
REXCVAR_DECLARE(std::string, log_level);
@@ -59,6 +60,9 @@ REXCVAR_DEFINE_BOOL(ac6_fix_dof, true, "AC6/Fixes",
"Fix cutscene depth-of-field striping and ghosting at resolution "
"scale above 1x.")
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_BOOL(ac6_fix_water_line, true, "AC6/Fixes",
"Fix the thin bright lines across open water.")
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
#include "generated/ac6recomp_config.h"
#include "generated/ac6recomp_init.h"
@@ -192,6 +196,16 @@ void ApplyAc6FixDefaults() {
rex::cvar::SetSessionDefault("ac6_densify_y_fetch_hashes", "5bd20f9d0d911687",
"ac6_fix_dof");
}
// Water pixel shader, interpolator 4 - the wave-normal UVs. Its fetches sit
// inside translated guest control flow, where DXBC derivatives are undefined,
// so the gradient collapses at the ocean mesh's seams and the lookup clamps
// to the finest mip. Hoisting the gradients to the prologue restores what the
// console computes. Not gated on draw scale: the seams are in the game's own
// vertex data, so this bites at 1x too.
if (REXCVAR_GET(ac6_fix_water_line)) {
rex::cvar::SetSessionDefault("ac6_fix_hoisted_fetch_gradients_hashes",
"35a80cb48a481624:4", "ac6_fix_water_line");
}
}
} // namespace
@@ -227,16 +241,18 @@ void ApplyAc6PerformanceModeOverridesPublic() {
void LogAc6ConfigPresetSummary() {
REXLOG_ERROR("AC6 config: graphics mode={} capture={}", REXCVAR_GET(ac6_graphics_mode),
REXCVAR_GET(ac6_render_capture) ? "true" : "false");
REXLOG_ERROR("AC6 fixes: scaling={} deswizzle={} dof={} perf_mode={} unlock_fps={}",
REXLOG_ERROR("AC6 fixes: scaling={} deswizzle={} dof={} water_line={} perf_mode={} "
"unlock_fps={}",
REXCVAR_GET(ac6_fix_scaling), REXCVAR_GET(ac6_fix_deswizzle),
REXCVAR_GET(ac6_fix_dof), REXCVAR_GET(ac6_performance_mode),
REXCVAR_GET(ac6_unlock_fps));
REXCVAR_GET(ac6_fix_dof), REXCVAR_GET(ac6_fix_water_line),
REXCVAR_GET(ac6_performance_mode), REXCVAR_GET(ac6_unlock_fps));
REXLOG_ERROR("AC6 fix payloads: neutralize='{}' snap='{}' densify_x='{}' densify_y='{}' "
"param_gen={}/{}",
"hoisted_gradients='{}' param_gen={}/{}",
REXCVAR_GET(ac6_neutralize_deswizzle_hashes),
REXCVAR_GET(ac6_snap_guest_texel_hashes),
REXCVAR_GET(ac6_densify_x_fetch_hashes),
REXCVAR_GET(ac6_densify_y_fetch_hashes),
REXCVAR_GET(ac6_fix_hoisted_fetch_gradients_hashes),
REXCVAR_GET(param_gen_integer_guest_position),
REXCVAR_GET(param_gen_host_subpixel_restore));
}
+1
View File
@@ -35,6 +35,7 @@ REXCVAR_DECLARE(std::string, ac6_neutralize_deswizzle_hashes);
REXCVAR_DECLARE(std::string, ac6_snap_guest_texel_hashes);
REXCVAR_DECLARE(std::string, ac6_densify_x_fetch_hashes);
REXCVAR_DECLARE(std::string, ac6_densify_y_fetch_hashes);
REXCVAR_DECLARE(std::string, ac6_fix_hoisted_fetch_gradients_hashes);
REXCVAR_DECLARE(std::string, readback_resolve);
REXCVAR_DECLARE(bool, readback_resolve_half_pixel_offset);
REXCVAR_DECLARE(bool, readback_memexport);
@@ -1081,6 +1081,15 @@ class DxbcShaderTranslator : public ShaderTranslator {
// alpha test, alpha to coverage, exponent bias, gamma, and also for ROV
// writing).
uint32_t system_temps_color_[4];
// Screen-space gradients of one interpolator, computed in the prologue where
// control flow is quad-uniform, so that a texture fetch buried inside
// translated guest control flow can use well-defined derivatives instead of
// recomputing undefined ones at the fetch site. UINT32_MAX when unused;
// hoisted_gradient_interpolator_ is the interpolator (and guest register)
// index they belong to.
uint32_t system_temp_hoisted_grad_h_;
uint32_t system_temp_hoisted_grad_v_;
uint32_t hoisted_gradient_interpolator_;
// Memory export temporary registers are allocated if the shader writes any
// eM# (current_shader().memexport_eM_written() != 0).
+24
View File
@@ -75,6 +75,30 @@ REXCVAR_DEFINE_STRING(ac6_densify_y_fetch_hashes, "", "AC6/Fixes",
"Normally driven by ac6_fix_dof.")
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart)
.debug_only();
REXCVAR_DEFINE_STRING(ac6_fix_hoisted_fetch_gradients_hashes, "", "AC6/Fixes",
"Token list \"<hash>:<interpolator index>\": compute that "
"interpolator's screen-space gradients once in the pixel "
"shader prologue, where the quad is uniform, and use them for "
"any computed-LOD texture fetch in that shader whose "
"coordinates come from the matching guest register.\n"
"The guest predicates with \"(p0) exec\" and branches with "
"\"jmp\", which on Xenos still let every pixel of the quad "
"run - results are masked - so derivatives stay well defined. "
"This translator turns both into real control flow, and DXBC "
"derivatives inside non-uniform control flow are UNDEFINED: a "
"quad pixel that did not enter the block holds a stale "
"coordinate, so the gradient collapses, the LOD goes to "
"negative infinity and the fetch clamps to the finest mip. "
"Measured directly in AC6's ocean - the gradient the fetch "
"receives reads zero along a mesh seam while the same "
"quantity measured in the prologue is smooth.\n"
"This restores hardware behaviour, so it is a fix rather than "
"an enhancement. Which shaders it applies to stays a per-shader "
"list rather than a blanket default, because the hazard is "
"general rather than AC6-specific and widening it wants testing "
"far beyond one ocean. Normally driven by ac6_fix_water_line.")
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart)
.debug_only();
REXCVAR_DEFINE_BOOL(ac6_flare_drop_quad2, true, "AC6/Fixes",
"Fix the faint rectangle around the sun by culling the lens "
"flare's spurious second billboard.")
@@ -13,7 +13,9 @@
#include <algorithm>
#include <atomic>
#include <cstdlib>
#include <cstring>
#include <string>
#include <memory>
#include <rex/assert.h>
@@ -37,6 +39,51 @@ REXCVAR_DECLARE(bool, param_gen_host_subpixel_restore);
namespace rex::graphics {
using namespace ucode;
namespace {
// Whether `hash` appears in a comma/space separated token list whose tokens are
// "<hex-hash>:<index>[+<index>...]" (an optional "0x" prefix is accepted). Used
// to opt individual guest shaders into a translation change at runtime, so
// re-targeting needs no rebuild.
bool UcodeHashInList(uint64_t hash, const std::string& list, uint32_t index) {
auto is_sep = [](char c) {
return c == ',' || c == ';' || c == ' ' || c == '\t' || c == '\n' || c == '\r';
};
size_t i = 0, n = list.size();
while (i < n) {
while (i < n && is_sep(list[i])) {
++i;
}
size_t start = i;
while (i < n && !is_sep(list[i])) {
++i;
}
if (i > start) {
std::string token = list.substr(start, i - start);
size_t colon = token.find(':');
if (colon == std::string::npos) {
continue;
}
if (std::strtoull(token.substr(0, colon).c_str(), nullptr, 16) != hash) {
continue;
}
size_t p = colon + 1;
while (p < token.size()) {
size_t e = token.find('+', p);
if (e == std::string::npos) {
e = token.size();
}
if (e > p && std::strtoul(token.substr(p, e - p).c_str(), nullptr, 10) == index) {
return true;
}
p = e + 1;
}
// The same hash may appear again with other indices - keep scanning.
}
}
return false;
}
} // namespace
// Notes about operands:
//
// Reading and writing:
@@ -640,11 +687,24 @@ void DxbcShaderTranslator::StartPixelShader() {
if (i == param_gen_interpolator) {
continue;
}
bool interpolator_present =
i < xenos::kMaxInterpolators && (interpolator_mask & (UINT32_C(1) << i));
dxbc::Src interpolator_src(
interpolator_present
? dxbc::Src::V1D(in_reg_ps_interpolators_ +
rex::bit_count(interpolator_mask & ((UINT32_C(1) << i) - 1)))
: dxbc::Src::LF(0.0f));
a_.OpMov(uses_register_dynamic_addressing ? dxbc::Dest::X(0, i) : dxbc::Dest::R(i),
(i < xenos::kMaxInterpolators && (interpolator_mask & (UINT32_C(1) << i)))
? dxbc::Src::V1D(in_reg_ps_interpolators_ +
rex::bit_count(interpolator_mask & ((UINT32_C(1) << i) - 1)))
: dxbc::Src::LF(0.0f));
interpolator_src);
// Hoisted gradients (ac6_fix_hoisted_fetch_gradients_hashes): take this
// interpolator's derivatives HERE, in the prologue, where every pixel of the
// quad is running, so a fetch inside translated guest control flow can use a
// well-defined value instead of an undefined one computed at the fetch site.
if (interpolator_present && i == hoisted_gradient_interpolator_ &&
system_temp_hoisted_grad_h_ != UINT32_MAX) {
a_.OpDerivRTXCoarse(dxbc::Dest::R(system_temp_hoisted_grad_h_), interpolator_src);
a_.OpDerivRTYCoarse(dxbc::Dest::R(system_temp_hoisted_grad_v_), interpolator_src);
}
}
// Write the pixel parameters to the specified interpolator register
@@ -842,6 +902,9 @@ void DxbcShaderTranslator::StartTranslation() {
// Allocate global system temporary registers that may also be used in the
// epilogue.
system_temp_hoisted_grad_h_ = UINT32_MAX;
system_temp_hoisted_grad_v_ = UINT32_MAX;
hoisted_gradient_interpolator_ = UINT32_MAX;
if (is_vertex_shader()) {
system_temp_position_ = PushSystemTemp(0b1111);
system_temp_point_size_edge_flag_kill_vertex_ = PushSystemTemp(0b0100);
@@ -881,6 +944,25 @@ void DxbcShaderTranslator::StartTranslation() {
system_temps_color_[i] = PushSystemTemp(0b1111);
}
}
// Registers for the hoisted gradients of one interpolator - see
// ac6_fix_hoisted_fetch_gradients_hashes. They have to live across the whole
// guest program, because the fetch that consumes them is buried inside its
// control flow.
if (!is_depth_only_pixel_shader_) {
const std::string& hoisted_grad_hashes =
REXCVAR_GET(ac6_fix_hoisted_fetch_gradients_hashes);
if (!hoisted_grad_hashes.empty()) {
uint64_t ucode_hash = current_shader().ucode_data_hash();
for (uint32_t i = 0; i < xenos::kMaxInterpolators; ++i) {
if (UcodeHashInList(ucode_hash, hoisted_grad_hashes, i)) {
hoisted_gradient_interpolator_ = i;
system_temp_hoisted_grad_h_ = PushSystemTemp();
system_temp_hoisted_grad_v_ = PushSystemTemp();
break;
}
}
}
}
}
// Allocate temporary registers for memexport.
@@ -1127,6 +1209,14 @@ void DxbcShaderTranslator::CompleteShaderCode() {
// system_temp_point_size_edge_flag_kill_vertex_.
PopSystemTemp(2);
} else if (is_pixel_shader()) {
if (system_temp_hoisted_grad_h_ != UINT32_MAX) {
// Release system_temp_hoisted_grad_v_ and system_temp_hoisted_grad_h_
// (pushed after system_temps_color_).
PopSystemTemp(2);
system_temp_hoisted_grad_h_ = UINT32_MAX;
system_temp_hoisted_grad_v_ = UINT32_MAX;
hoisted_gradient_interpolator_ = UINT32_MAX;
}
// Release system_temps_color_.
uint32_t shader_writes_color_targets = current_shader().writes_color_targets();
for (int32_t i = 3; i >= 0; --i) {
@@ -726,6 +726,18 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction(
uint32_t tfetch_index = instr.operands[1].storage_index;
// See ac6_fix_hoisted_fetch_gradients_hashes: use the prologue-computed
// gradients for this fetch if its coordinates are read straight from the guest
// register that the hoisted interpolator was loaded into. Requiring a plain,
// absolutely-addressed register read is what keeps the substitution honest -
// anything else and the gradients would not correspond to the coordinates.
bool use_hoisted_gradients =
is_pixel_shader() && system_temp_hoisted_grad_h_ != UINT32_MAX &&
hoisted_gradient_interpolator_ != UINT32_MAX &&
instr.operands[0].storage_source == InstructionStorageSource::kRegister &&
instr.operands[0].storage_addressing_mode == InstructionStorageAddressingMode::kAbsolute &&
instr.operands[0].storage_index == hoisted_gradient_interpolator_;
// Whether to use gradients (implicit or explicit) for LOD calculation.
bool use_computed_lod = instr.attributes.use_computed_lod &&
(is_pixel_shader() || instr.attributes.use_register_gradients);
@@ -1742,6 +1754,28 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction(
dxbc::Src::R(size_and_is_3d_temp, dxbc::Src::kZZZZ));
a_.OpEndIf();
}
} else if (use_hoisted_gradients) {
// This fetch sits inside translated guest control flow, where DXBC
// derivatives are undefined - a quad pixel that did not enter the
// block holds a stale coordinate, so the finite difference
// collapses, the LOD runs off to negative infinity, and the fetch
// clamps to the finest mip. On Xenos the guest's predication and
// jumps still let every pixel of the quad execute, so its
// derivatives are well defined; this restores that by using the
// gradients taken in the prologue, where the quad is uniform.
// Swizzled the way the coordinate operand is, so the components line
// up with coord_and_sampler_temp.
for (uint32_t i = 0; i < grad_component_count; ++i) {
uint32_t component =
uint32_t(instr.operands[0].GetComponent(i)) - uint32_t(SwizzleSource::kX);
a_.OpMov(dxbc::Dest::R(grad_h_lod_temp, 1 << i),
dxbc::Src::R(system_temp_hoisted_grad_h_).Select(component));
a_.OpMov(dxbc::Dest::R(grad_v_temp, 1 << i),
dxbc::Src::R(system_temp_hoisted_grad_v_).Select(component));
}
a_.OpMul(dxbc::Dest::R(grad_h_lod_temp, grad_mask), dxbc::Src::R(grad_h_lod_temp),
lod_src);
a_.OpMul(dxbc::Dest::R(grad_v_temp, grad_mask), dxbc::Src::R(grad_v_temp), lod_src);
} else {
// Coarse is according to the Direct3D 11.3 specification.
a_.OpDerivRTXCoarse(dxbc::Dest::R(grad_h_lod_temp, grad_mask),