diff --git a/src/main.cpp b/src/main.cpp index 71afad9a..06f11f35 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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)); } diff --git a/thirdparty/rexglue-sdk/include/rex/graphics/flags.h b/thirdparty/rexglue-sdk/include/rex/graphics/flags.h index 7e7040e9..c2dffa5a 100644 --- a/thirdparty/rexglue-sdk/include/rex/graphics/flags.h +++ b/thirdparty/rexglue-sdk/include/rex/graphics/flags.h @@ -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); diff --git a/thirdparty/rexglue-sdk/include/rex/graphics/pipeline/shader/dxbc_translator.h b/thirdparty/rexglue-sdk/include/rex/graphics/pipeline/shader/dxbc_translator.h index ba26097a..9a880d18 100644 --- a/thirdparty/rexglue-sdk/include/rex/graphics/pipeline/shader/dxbc_translator.h +++ b/thirdparty/rexglue-sdk/include/rex/graphics/pipeline/shader/dxbc_translator.h @@ -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). diff --git a/thirdparty/rexglue-sdk/src/graphics/flags.cpp b/thirdparty/rexglue-sdk/src/graphics/flags.cpp index 01010a6d..9a2b73bd 100644 --- a/thirdparty/rexglue-sdk/src/graphics/flags.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/flags.cpp @@ -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 \":\": 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.") diff --git a/thirdparty/rexglue-sdk/src/graphics/pipeline/shader/dxbc_translator.cpp b/thirdparty/rexglue-sdk/src/graphics/pipeline/shader/dxbc_translator.cpp index ff7c9e7c..a5e95d2e 100644 --- a/thirdparty/rexglue-sdk/src/graphics/pipeline/shader/dxbc_translator.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/pipeline/shader/dxbc_translator.cpp @@ -13,7 +13,9 @@ #include #include +#include #include +#include #include #include @@ -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 +// ":[+...]" (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) { diff --git a/thirdparty/rexglue-sdk/src/graphics/pipeline/shader/dxbc_translator_fetch.cpp b/thirdparty/rexglue-sdk/src/graphics/pipeline/shader/dxbc_translator_fetch.cpp index c3b05e26..c555b437 100644 --- a/thirdparty/rexglue-sdk/src/graphics/pipeline/shader/dxbc_translator_fetch.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/pipeline/shader/dxbc_translator_fetch.cpp @@ -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),