mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-22 07:04:31 -04:00
Fix cutscene depth-of-field striping and ghosting at render scale > 1
The cutscene DoF chain is authored for the 640×360 guest grid; at render scale > 1 its guest-granular blur kernels alias against host-granular content, visible as streaks and ghost outlines. Fix: snap the two CoC pre-blur passes to guest texel centers and densify the two 13-tap gather passes per axis. Hash-allowlisted, behind ac6_fix_dof (on), inert at 1x.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+29
@@ -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 \"<hash>[:<slot>[+<slot>...]]\" 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: \"<hash>[:<slot>[+<slot>...]]\" 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");
|
||||
|
||||
+166
-2
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user