diff --git a/src/main.cpp b/src/main.cpp index 5fc78164..dcb6bff9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,6 +19,9 @@ REXCVAR_DECLARE(int32_t, draw_resolution_scale_y); REXCVAR_DECLARE(bool, param_gen_integer_guest_position); REXCVAR_DECLARE(bool, param_gen_host_subpixel_restore); 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, log_file); REXCVAR_DECLARE(std::string, log_level); REXCVAR_DECLARE(bool, ac6_d3d_trace); @@ -70,6 +73,13 @@ REXCVAR_DEFINE_BOOL(ac6_fix_deswizzle, true, "AC6", "sub-tile de-swizzle, which is always a wrong texel permutation once the " "emulator detiles to linear. On by default; effective at all draw scales " "(the swizzle is present at 1x too)."); +REXCVAR_DEFINE_BOOL(ac6_fix_dof, true, "AC6", + "Fix the in-engine cutscene depth-of-field striping/ghosting at draw " + "resolution scale > 1: the 6-pass DoF chain is authored for the 640x360 " + "grid, so its guest-texel kernels alias against the extra detail of " + "scaled sources (combed CoC weights, ghost copies at the sparse gather " + "taps). Snaps the CoC pre-blurs to the guest grid and densifies the " + "gather taps along each pass's axis. On by default; inert at 1x."); #include "generated/ac6recomp_config.h" #include "generated/ac6recomp_init.h" @@ -187,6 +197,15 @@ void ApplyAc6FixDefaults() { AC6_SET_IF_UNSET(ac6_neutralize_deswizzle_hashes, "7d22894002d16018, 17e5e4ac3e713245:4"); } + // Cutscene DoF chain (CoC pre-blur pair snapped to the guest grid; the + // two 13-tap gathers densified along their pass axes). The translator + // only emits these at draw scale > 1, so no scale gate is needed here. + if (REXCVAR_GET(ac6_fix_dof)) { + AC6_SET_IF_UNSET(ac6_snap_guest_texel_hashes, + "85d733927e3bba9c, d050dfa6f58567f6"); + AC6_SET_IF_UNSET(ac6_densify_x_fetch_hashes, "6328f9c40913c82c"); + AC6_SET_IF_UNSET(ac6_densify_y_fetch_hashes, "5bd20f9d0d911687"); + } } #undef AC6_SET_IF_UNSET diff --git a/thirdparty/rexglue-sdk/include/rex/graphics/flags.h b/thirdparty/rexglue-sdk/include/rex/graphics/flags.h index 58d9bb87..1fc336b8 100644 --- a/thirdparty/rexglue-sdk/include/rex/graphics/flags.h +++ b/thirdparty/rexglue-sdk/include/rex/graphics/flags.h @@ -32,6 +32,9 @@ REXCVAR_DECLARE(bool, draw_resolution_scaled_texture_offsets); REXCVAR_DECLARE(bool, param_gen_integer_guest_position); REXCVAR_DECLARE(bool, param_gen_host_subpixel_restore); 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, readback_resolve); REXCVAR_DECLARE(bool, readback_resolve_half_pixel_offset); REXCVAR_DECLARE(bool, readback_memexport); diff --git a/thirdparty/rexglue-sdk/src/graphics/flags.cpp b/thirdparty/rexglue-sdk/src/graphics/flags.cpp index da18cc0e..7d259ecd 100644 --- a/thirdparty/rexglue-sdk/src/graphics/flags.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/flags.cpp @@ -65,6 +65,35 @@ REXCVAR_DEFINE_STRING(ac6_neutralize_deswizzle_hashes, "", "GPU/Shader", "Xenos tfetch slot 4 (needed when only some fetches de-swizzle, e.g. " "the AC6 cloud compositor: scene fetch de-swizzles, mask/cloud " "fetches use plain UVs and must be left alone). Runtime, no rebuild."); +REXCVAR_DEFINE_STRING(ac6_snap_guest_texel_hashes, "", "GPU/Shader", + "AC6: same \"[:[+...]]\" token list as " + "ac6_neutralize_deswizzle_hashes, naming guest pixel-shader ucode " + "hashes whose texture taps encode fractional guest-texel bilinear " + "positions (e.g. the cutscene depth-of-field gather pair). Sampling " + "a resolution-scaled texture with such a kernel reinterprets every " + "blend ratio against the finer host grid and alternates the kernel " + "phase per output pixel, aliasing into striping at >1x. For matching " + "fetches the normalized coordinate is snapped to the guest texel " + "center (where host bilinear of a scaled texture returns exactly the " + "guest texel's box average), restoring the guest sampling grid at any " + "draw resolution scale. Applied only at >1x and only to " + "resolution-scaled textures. Runtime, no rebuild."); +REXCVAR_DEFINE_STRING(ac6_densify_x_fetch_hashes, "", "GPU/Shader", + "AC6: \"[:[+...]]\" list for HORIZONTAL-axis " + "separable filter passes (e.g. the cutscene DoF H gather " + "6328f9c40913c82c). Each allowlisted fetch becomes the average of " + "2*draw_resolution_scale_x samples spread along X across one " + "guest-texel half-gap on each side of the original tap - the union " + "of all taps' cells covers the kernel span continuously, so ghost " + "copies of arbitrarily thin features merge at any resolution scale. " + "Convolving the authored kernel with the 2-texel cell box widens the " + "blur by ~2% (visually shape-faithful). Per-axis scale aware; no-op " + "when draw_resolution_scale_x is 1. Read at shader translation."); +REXCVAR_DEFINE_STRING(ac6_densify_y_fetch_hashes, "", "GPU/Shader", + "AC6: as ac6_densify_x_fetch_hashes, for VERTICAL-axis separable " + "filter passes (e.g. the cutscene DoF V gather 5bd20f9d0d911687): " + "2*draw_resolution_scale_y samples along Y per fetch. No-op when " + "draw_resolution_scale_y is 1."); REXCVAR_DEFINE_BOOL(ac6_flare_drop_quad2, true, "GPU/Shader", "AC6: cull the sun lens-flare's spurious second billboard " "(vertices 4-7) to remove the faint rectangle in the sky"); 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 277c1305..c3b05e26 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 @@ -862,6 +862,63 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction( UcodeHashSlotInList(current_shader().ucode_data_hash(), tfetch_index, neutralize_hashes); } + // Guest-texel snap for scale-broken filter kernels: some game passes (AC6's + // cutscene depth-of-field gather pair) build their kernel out of fractional + // guest-texel tap offsets whose bilinear blend ratios are tuned for the + // guest-resolution texel grid. Against a resolution-scaled texture the same + // UVs land on the finer host grid: every ratio is reinterpreted and the + // kernel phase alternates per output pixel, aliasing into striping. For an + // allowlisted shader (same syntax as the de-swizzle neutralize) snap the + // final coordinate to the guest texel center, where host bilinear of a + // scaled texture returns exactly the guest texel's box average - the guest + // sampling grid at any scale. Unlike the passes above these shaders sample + // via interpolators, so no param_gen requirement. + bool apply_guest_texel_snap = false; + if (instr.opcode == FetchOpcode::kTextureFetch && + !instr.attributes.unnormalized_coordinates && + instr.dimension == xenos::FetchOpDimension::k2D && is_pixel_shader() && + (draw_resolution_scale_x_ > 1 || draw_resolution_scale_y_ > 1)) { + const std::string& snap_hashes = REXCVAR_GET(ac6_snap_guest_texel_hashes); + apply_guest_texel_snap = + !snap_hashes.empty() && + UcodeHashSlotInList(current_shader().ucode_data_hash(), tfetch_index, + snap_hashes); + } + // Tap-footprint box filter for sparse-kernel shaders (AC6 DoF gathers): + // taps several guest texels apart alias against the extra detail of a + // resolution-scaled source. Each allowlisted fetch becomes the average of a + // 4-sample cluster at (+/-0.25, +/-0.25) guest texels around the original + // position - a guest-Nyquist low-pass at the exact tap location, preserving + // the kernel's fractional placement (no output quantization, unlike the + // texel-center snap above). + // Axis densification for separable sparse-kernel passes (AC6 DoF gathers): + // each allowlisted fetch becomes the average of 2*scale_axis samples spread + // along the pass axis across one guest-texel half-gap on each side of the + // tap. The union of adjacent taps' cells covers the kernel span + // continuously, so ghost copies of features thinner than the tap spacing + // merge at any resolution scale; the effective kernel is the authored one + // convolved with a 2-guest-texel box (~2% wider - shape-faithful). Per-axis + // resolution scale aware (Xenia allows asymmetric X/Y scales); no-op when + // the relevant axis scale is 1. + uint32_t densify_axis_samples[2] = {0, 0}; + if (instr.opcode == FetchOpcode::kTextureFetch && + !instr.attributes.unnormalized_coordinates && + instr.dimension == xenos::FetchOpDimension::k2D && is_pixel_shader()) { + const std::string& densify_x_hashes = REXCVAR_GET(ac6_densify_x_fetch_hashes); + if (draw_resolution_scale_x_ > 1 && !densify_x_hashes.empty() && + UcodeHashSlotInList(current_shader().ucode_data_hash(), tfetch_index, + densify_x_hashes)) { + densify_axis_samples[0] = 2 * uint32_t(draw_resolution_scale_x_); + } + const std::string& densify_y_hashes = REXCVAR_GET(ac6_densify_y_fetch_hashes); + if (draw_resolution_scale_y_ > 1 && !densify_y_hashes.empty() && + UcodeHashSlotInList(current_shader().ucode_data_hash(), tfetch_index, + densify_y_hashes)) { + densify_axis_samples[1] = 2 * uint32_t(draw_resolution_scale_y_); + } + } + const bool apply_cluster_filter = + densify_axis_samples[0] != 0 || densify_axis_samples[1] != 0; uint32_t size_needed_components = 0b0000; if (instr.opcode == FetchOpcode::kGetTextureWeights) { // Size needed for denormalization for coordinate lerp factor. @@ -926,10 +983,12 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction( // the texture is 3D unconditionally. size_needed_components |= 0b1000; } - if (apply_host_subpixel_correction || apply_deswizzle_identity) { + if (apply_host_subpixel_correction || apply_deswizzle_identity || + apply_guest_texel_snap || apply_cluster_filter) { // Need the guest texture width/height (XY) to convert the host sub-pixel // offset (Tier B) or the SV_Position into normalized texture space - // (de-swizzle identity). + // (de-swizzle identity), or to locate the guest texel grid (texel snap / + // cluster filters). size_needed_components |= 0b0011; } uint32_t size_and_is_3d_temp = size_needed_components ? PushSystemTemp() : UINT32_MAX; @@ -1303,6 +1362,32 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction( // Release deswizzle_temp. PopSystemTemp(); } + if (apply_guest_texel_snap) { + // coord_and_sampler_temp.xy is the final normalized coordinate. Snap it + // to the guest texel center so the allowlisted kernel samples the guest + // grid regardless of the host storage resolution (see the gate above). + uint32_t snap_temp = PushSystemTemp(); + // snap_temp.xy = (floor(uv * guest_size) + 0.5) / guest_size. + a_.OpMul(dxbc::Dest::R(snap_temp, 0b0011), + dxbc::Src::R(coord_and_sampler_temp), dxbc::Src::R(size_and_is_3d_temp)); + a_.OpRoundNI(dxbc::Dest::R(snap_temp, 0b0011), dxbc::Src::R(snap_temp)); + a_.OpAdd(dxbc::Dest::R(snap_temp, 0b0011), dxbc::Src::R(snap_temp), + dxbc::Src::LF(0.5f, 0.5f, 0.0f, 0.0f)); + a_.OpDiv(dxbc::Dest::R(snap_temp, 0b0011), dxbc::Src::R(snap_temp), + dxbc::Src::R(size_and_is_3d_temp)); + // Only textures stored at host resolution present a finer grid than the + // kernel was tuned for; guest-resolution sources stay stock. + a_.OpAnd(dxbc::Dest::R(snap_temp, 0b0100), + LoadSystemConstant(SystemConstants::Index::kTexturesResolutionScaled, + offsetof(SystemConstants, textures_resolution_scaled), + dxbc::Src::kXXXX), + dxbc::Src::LU(uint32_t(1) << tfetch_index)); + a_.OpIf(true, dxbc::Src::R(snap_temp, dxbc::Src::kZZZZ)); + a_.OpMov(dxbc::Dest::R(coord_and_sampler_temp, 0b0011), dxbc::Src::R(snap_temp)); + a_.OpEndIf(); + // Release snap_temp. + PopSystemTemp(); + } switch (instr.dimension) { case xenos::FetchOpDimension::k1D: // Pad to 2D array coordinates. @@ -1957,6 +2042,65 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction( texture_binding_signed.bindful_srv_index, uint32_t(SRVMainRegister::kBindfulTexturesStart) + texture_binding_index_signed); } + // Tap densification (see the gate above): re-emit the whole sample + // block once per cluster offset around the original coordinate and + // average the RAW results (before format decode - consistent with how + // Xenos bilinear itself filters in storage space). Restricted to + // plain 2D fetches. Cluster geometry: 2*scale_axis samples per listed + // axis, spanning one guest texel each side of the tap (a grid if both + // axes are listed). + const bool cluster_this_srv = apply_cluster_filter && + srv_dimension == xenos::FetchOpDimension::k2D && + !layer_lerp_needed; + float cluster_dx[64]; + float cluster_dy[64]; + uint32_t cluster_count = 1; + cluster_dx[0] = 0.0f; + cluster_dy[0] = 0.0f; + if (cluster_this_srv) { + float xs[8], ys[8]; + uint32_t nx = 1, ny = 1; + xs[0] = 0.0f; + ys[0] = 0.0f; + if (densify_axis_samples[0]) { + nx = std::min(densify_axis_samples[0], 8u); + for (uint32_t j = 0; j < nx; ++j) { + xs[j] = (float(j) + 0.5f) * 2.0f / float(nx) - 1.0f; + } + } + if (densify_axis_samples[1]) { + ny = std::min(densify_axis_samples[1], 8u); + for (uint32_t j = 0; j < ny; ++j) { + ys[j] = (float(j) + 0.5f) * 2.0f / float(ny) - 1.0f; + } + } + cluster_count = 0; + for (uint32_t jy = 0; jy < ny; ++jy) { + for (uint32_t jx = 0; jx < nx; ++jx) { + cluster_dx[cluster_count] = xs[jx]; + cluster_dy[cluster_count] = ys[jy]; + ++cluster_count; + } + } + } + uint32_t cluster_accum_temp = UINT32_MAX; + uint32_t cluster_coord_temp = UINT32_MAX; + const uint32_t cluster_passes = cluster_this_srv ? cluster_count : 1; + if (cluster_this_srv) { + cluster_accum_temp = PushSystemTemp(); + cluster_coord_temp = PushSystemTemp(); + // The original tap coordinate (and the sampler index in W). + a_.OpMov(dxbc::Dest::R(cluster_coord_temp), dxbc::Src::R(coord_and_sampler_temp)); + } + for (uint32_t cluster_pass = 0; cluster_pass < cluster_passes; ++cluster_pass) { + if (cluster_this_srv) { + // coord.xy = original + cluster offset (guest texels) / guest_size. + a_.OpDiv(dxbc::Dest::R(coord_and_sampler_temp, 0b0011), + dxbc::Src::LF(cluster_dx[cluster_pass], cluster_dy[cluster_pass], 0.0f, 0.0f), + dxbc::Src::R(size_and_is_3d_temp)); + a_.OpAdd(dxbc::Dest::R(coord_and_sampler_temp, 0b0011), + dxbc::Src::R(coord_and_sampler_temp), dxbc::Src::R(cluster_coord_temp)); + } for (uint32_t layer = 0; layer < (layer_lerp_needed ? 2u : 1u); ++layer) { uint32_t layer_value_temp = system_temp_result_; if (layer) { @@ -2050,6 +2194,26 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction( PopSystemTemp(); } } + if (cluster_this_srv) { + if (cluster_pass == 0) { + a_.OpMov(dxbc::Dest::R(cluster_accum_temp, used_result_nonzero_components), + dxbc::Src::R(system_temp_result_)); + } else { + a_.OpAdd(dxbc::Dest::R(cluster_accum_temp, used_result_nonzero_components), + dxbc::Src::R(cluster_accum_temp), dxbc::Src::R(system_temp_result_)); + } + } + } // cluster_pass + if (cluster_this_srv) { + // The tap's value = the average of the cluster samples. + a_.OpMul(dxbc::Dest::R(system_temp_result_, used_result_nonzero_components), + dxbc::Src::R(cluster_accum_temp), + dxbc::Src::LF(1.0f / float(cluster_passes))); + // Restore the unoffset coordinate for any downstream use. + a_.OpMov(dxbc::Dest::R(coord_and_sampler_temp), dxbc::Src::R(cluster_coord_temp)); + // Release cluster_coord_temp and cluster_accum_temp. + PopSystemTemp(2); + } } if (instr.dimension == xenos::FetchOpDimension::k3DOrStacked) { // Close the stacked/3D check.