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