Fix AC6 >1x mosaic and cloud/effects de-swizzle streaks

Upscale mosaic. At draw-resolution scale > 1, AC6's deferred
EDRAM restore passes do integer pixel-address math on the PsParamGen
position. Its leftover sub-guest-pixel fraction scrambles into an 8x8
mosaic. Floor the param-gen position (param_gen_integer_guest_position)
and re-add the host sub-pixel at the fetch (param_gen_host_subpixel_restore).
Inert at 1x.

De-swizzle scramble. Some AC6 restore/resample passes
manually de-swizzle the Xenos EDRAM sub-tile layout of their source, which
is only correct on tiled data. The emulator detiles everything to linear,
so the de-swizzle always scrambles, streaking the cloud/effects pipeline.
For allowlisted shader hashes (ac6_neutralize_deswizzle_hashes) the sample
coordinate is replaced with the identity host-texel UV (a 1:1 copy).
This commit is contained in:
Dipshet
2026-07-08 01:28:57 +03:00
parent 06868b3780
commit 313fd236ee
5 changed files with 257 additions and 0 deletions
+45
View File
@@ -17,6 +17,9 @@ REXCVAR_DECLARE(bool, direct_host_resolve);
REXCVAR_DECLARE(int32_t, resolution_scale);
REXCVAR_DECLARE(int32_t, draw_resolution_scale_x);
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, log_file);
REXCVAR_DECLARE(std::string, log_level);
REXCVAR_DECLARE(bool, ac6_d3d_trace);
@@ -54,6 +57,19 @@ REXCVAR_DEFINE_STRING(ac6_graphics_backend, AC6_DEFAULT_GRAPHICS_BACKEND,
REXCVAR_DEFINE_BOOL(ac6_performance_mode, true, "AC6/Performance",
"Disable all diagnostics, logging, and development overlays for maximum runtime performance");
// Correctness-fix switches. Each expands
// to its detailed cvars + AC6-specific shader hashes in ApplyAc6FixDefaults()
// unless the user overrode them.
REXCVAR_DEFINE_BOOL(ac6_fix_scaling, true, "AC6",
"Fix the upscaling mosaic on AC6's deferred EDRAM restore passes by "
"flooring the PsParamGen pixel position (+ host sub-pixel restore). "
"Auto-applies only at draw resolution scale > 1; inert at 1x. On by default.");
REXCVAR_DEFINE_BOOL(ac6_fix_deswizzle, true, "AC6",
"Fix the de-swizzle mosaic/streaks: identity-override AC6's manual EDRAM "
"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).");
#include "generated/ac6recomp_config.h"
#include "generated/ac6recomp_init.h"
@@ -82,6 +98,8 @@ void ApplyAc6HybridStartupSafetyOverrides() {
REXCVAR_SET(draw_resolution_scale_x, 1);
REXCVAR_SET(draw_resolution_scale_y, 1);
}
// (The >1x param-gen mosaic fix is applied by ApplyAc6FixDefaults via
// ac6_fix_scaling.)
if (REXCVAR_GET(ac6_force_safe_direct_host_resolve)) {
REXCVAR_SET(direct_host_resolve, false);
@@ -139,6 +157,32 @@ void ApplyAc6PerformanceModeOverrides() {
REXCVAR_SET(ac6_texture_swaps_dump_enabled, false);
}
// Each fix switch -> its detailed cvars + AC6-specific shader hashes. The
// game-specific constants live here in code, not in the user's toml; each
// value is filled in only if the user did NOT set it explicitly, so any toml
// override still wins (the dev/debug path).
#define AC6_SET_IF_UNSET(cv, value) \
do { if (!rex::cvar::HasNonDefaultValue(#cv)) REXCVAR_SET(cv, value); } while (0)
void ApplyAc6FixDefaults() {
const bool scaled = REXCVAR_GET(draw_resolution_scale_x) > 1 ||
REXCVAR_GET(draw_resolution_scale_y) > 1;
// param_gen floor + host sub-pixel restore only bites at >1x --
// inert at 1x, so gate it on the draw scale.
if (REXCVAR_GET(ac6_fix_scaling) && scaled) {
AC6_SET_IF_UNSET(param_gen_integer_guest_position, true);
AC6_SET_IF_UNSET(param_gen_host_subpixel_restore, true);
}
// de-swizzle identity override is a texture-layout mismatch present
// at ALL draw scales, so it is NOT gated on scale.
if (REXCVAR_GET(ac6_fix_deswizzle)) {
AC6_SET_IF_UNSET(ac6_neutralize_deswizzle_hashes,
"7d22894002d16018, 17e5e4ac3e713245:4");
}
}
#undef AC6_SET_IF_UNSET
} // namespace
void ApplyAc6PerformanceModeOverridesPublic() {
@@ -169,6 +213,7 @@ std::unique_ptr<rex::ui::WindowedApp> Ac6recompAppCreate(rex::ui::WindowedAppCon
REXCVAR_SET(ac6_unlock_fps, false);
ApplyAc6DefaultSettings();
ApplyAc6HybridStartupSafetyOverrides();
ApplyAc6FixDefaults();
ApplyAc6PerformanceModeOverrides();
REXLOG_INFO("Ac6recompAppCreate: graphics mode={} capture={}",
+3
View File
@@ -29,6 +29,9 @@ REXCVAR_DECLARE(int32_t, draw_resolution_scale_x);
REXCVAR_DECLARE(int32_t, draw_resolution_scale_y);
REXCVAR_DECLARE(bool, resolve_resolution_scale_fill_half_pixel_offset);
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, readback_resolve);
REXCVAR_DECLARE(bool, readback_resolve_half_pixel_offset);
REXCVAR_DECLARE(bool, readback_memexport);
+32
View File
@@ -33,6 +33,38 @@ REXCVAR_DEFINE_BOOL(vfetch_index_rounding_bias, false, "GPU/Shader",
"flooring to fix black triangles caused by RCP precision");
REXCVAR_DEFINE_BOOL(draw_resolution_scaled_texture_offsets, true, "GPU/Shader",
"Scale texture offsets with draw resolution");
REXCVAR_DEFINE_BOOL(param_gen_integer_guest_position, false, "GPU/Shader",
"At >1x draw resolution scale, floor the PsParamGen pixel "
"position to the integer guest-pixel index instead of keeping "
"the sub-guest-pixel fraction. Fixes the mosaic in games that "
"feed the position into their own integer pixel-address math "
"(e.g. AC6's deferred EDRAM restore/de-swizzle passes), whose "
"frac()-based bit extraction otherwise sees a doubled period and "
"scrambles the sample coordinate. Those passes then sample at "
"guest resolution. Off by default; harmless for shaders that pass "
"the position straight to tfetch only at 1x.");
REXCVAR_DEFINE_BOOL(param_gen_host_subpixel_restore, false, "GPU/Shader",
"Builds on param_gen_integer_guest_position (and implies it): for "
"resolution-scaled, position-derived 2D samples in pixel shaders "
"that use PsParamGen, re-adds the host sub-pixel offset to the "
"sample coordinate so the de-swizzle restore passes sample at full "
"host resolution instead of the guest-texel center. Turns the "
"mosaic fix from clean-but-soft into true resolution-scaled detail. "
"Off by default; may need per-shader scoping if it disturbs other "
"passes that read scaled render targets via interpolated coords.");
REXCVAR_DEFINE_STRING(ac6_neutralize_deswizzle_hashes, "", "GPU/Shader",
"AC6: comma/space-separated tokens \"<hash>[:<slot>[+<slot>...]]\" "
"naming guest pixel-shader ucode hashes (hex) whose param_gen 2D "
"texture samples manually de-swizzle the raw EDRAM sub-tile order of "
"their source. The emulator's texture cache detiles everything to "
"linear, so that de-swizzle is always a wrong texel permutation here "
"(the mosaic/streak class). For a matching fetch the sample "
"coordinate is replaced unconditionally with the identity host-texel "
"UV (SV_Position.xy / (guest_size * scale)) -- a 1:1 copy. A bare "
"hash overrides ALL of that shader's fetches; \":4\" limits it to "
"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_BOOL(gpu_debug_markers, false, "GPU",
"Insert debug markers into GPU command streams for tools "
"like PIX and RenderDoc. Automatically enabled when "
@@ -18,6 +18,7 @@
#include <rex/assert.h>
#include <rex/cvar.h>
#include <rex/logging.h>
#include <rex/graphics/pipeline/shader/dxbc.h>
#include <rex/graphics/pipeline/shader/dxbc_translator.h>
#include <rex/graphics/xenos.h>
@@ -28,6 +29,10 @@ REXCVAR_DEFINE_BOOL(dxbc_switch, true, "GPU/Shader", "Use switch statements in D
REXCVAR_DEFINE_BOOL(dxbc_source_map, false, "GPU/Shader", "Generate source maps for DXBC");
// Defined in graphics/flags.cpp; read by param_gen below.
REXCVAR_DECLARE(bool, param_gen_integer_guest_position);
REXCVAR_DECLARE(bool, param_gen_host_subpixel_restore);
namespace rex::graphics {
using namespace ucode;
@@ -668,6 +673,22 @@ void DxbcShaderTranslator::StartPixelShader() {
a_.OpMul(dxbc::Dest::R(param_gen_temp, resolution_scaled_axes), dxbc::Src::R(param_gen_temp),
dxbc::Src::LF(1.0f / draw_resolution_scale_x_, 1.0f / draw_resolution_scale_y_, 1.0f,
1.0f));
if (REXCVAR_GET(param_gen_integer_guest_position) ||
REXCVAR_GET(param_gen_host_subpixel_restore)) {
// Snap the reverted position to the integer guest-pixel index. The
// multiply above leaves a sub-guest-pixel fraction (e.g. .5 at 2x) which
// is correct only for shaders that feed PsParamGen straight to tfetch.
// (param_gen_host_subpixel_restore re-adds that sub-pixel later, at the
// texture fetch, so the restore passes regain full host resolution.)
// Games that instead do integer pixel-address math on the position (AC6's
// deferred EDRAM restore/de-swizzle passes) break, because their
// frac()-based bit extraction sees a doubled period at >1x and scrambles
// the sample coordinate into an 8x8 mosaic. Flooring here makes that math
// operate on integer guest pixels again, at the cost of those passes
// sampling at guest resolution.
a_.OpRoundNI(dxbc::Dest::R(param_gen_temp, resolution_scaled_axes),
dxbc::Src::R(param_gen_temp));
}
}
if (shader_modification.pixel.param_gen_point) {
// A point - always front-facing (the upper bit of X is 0), not a line
@@ -11,6 +11,7 @@
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <memory>
#include <sstream>
@@ -27,6 +28,57 @@
namespace rex::graphics {
using namespace ucode;
namespace {
// Returns true if `hash` + `tfetch_index` is allowlisted by `list`, a string of
// tokens separated by commas/spaces/semicolons. Each token is
// "<hex-hash>[:<slot>[+<slot>...]]" (optional "0x" prefix on the hash):
// a bare hash matches ALL of that shader's fetch slots; with ":4" or ":1+2"
// only the listed Xenos tfetch slots match. Used to allowlist specific guest
// pixel shaders (and optionally specific fetches within them, e.g. only the
// de-swizzled fetch of a compositor whose other fetches use plain UVs) at
// runtime, so re-targeting needs no rebuild.
bool UcodeHashSlotInList(uint64_t hash, uint32_t tfetch_index, const std::string& list) {
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(':');
std::string hash_part = colon == std::string::npos ? token : token.substr(0, colon);
if (std::strtoull(hash_part.c_str(), nullptr, 16) == hash) {
if (colon == std::string::npos) {
return true; // No slot list - all slots.
}
size_t p = colon + 1;
while (p < token.size()) {
size_t q = token.find('+', p);
if (q == std::string::npos) {
q = token.size();
}
if (q > p &&
std::strtoul(token.substr(p, q - p).c_str(), nullptr, 10) == tfetch_index) {
return true;
}
p = q + 1;
}
// Hash matched but this slot isn't listed - keep scanning (the same
// hash may appear again with other slots).
}
}
}
return false;
}
} // namespace
void DxbcShaderTranslator::ProcessVertexFetchInstruction(
const ParsedVertexFetchInstruction& instr) {
if (emit_source_map_) {
@@ -772,6 +824,44 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction(
// 2D, cube: X - width, Y - height (cube maps probably can be only square, but
// for simplicity).
// 3D: X - width, Y - height, Z - depth, W - 0 if stacked 2D, 1 if 3D.
//
// For resolution-scaled, position-derived 2D samples in a
// pixel shader that uses PsParamGen (AC6's deferred restore passes), the guest
// de-swizzle now runs on integer guest pixels (see the param_gen floor) and so
// samples the guest-texel center, losing the host sub-pixel. We re-add it after
// the coordinate is normalized so the sample lands on the exact host texel =
// true resolution-scaled detail. Needs the guest texture size loaded below.
bool apply_host_subpixel_correction =
instr.opcode == FetchOpcode::kTextureFetch &&
!instr.attributes.unnormalized_coordinates &&
instr.dimension == xenos::FetchOpDimension::k2D && is_pixel_shader() &&
GetDxbcShaderModification().pixel.param_gen_enable &&
(draw_resolution_scale_x_ > 1 || draw_resolution_scale_y_ > 1) &&
REXCVAR_GET(param_gen_host_subpixel_restore);
// Surgical de-swizzle neutralize: some AC6 full-screen
// "restore/resample" passes manually de-swizzle the Xenos sub-tile layout of
// their source. That is correct only when the emulator keeps the source
// already-swizzled (scaled-resolve). When the source was detiled to LINEAR,
// the same de-swizzle scrambles it -> the streak that feeds the cloud/effects
// pipeline. For an allowlisted shader (by
// guest ucode hash; runtime string cvar) we replace the scrambled coordinate
// with the identity host-texel UV = SV_Position.xy / (guest_size * scale), a
// 1:1 copy. The layout mismatch is scale-independent -- the source is linear
// at 1x too, so this applies at ALL draw
// resolutions. (Only the separate param_gen sub-pixel restore above stays
// >1x-gated, since host sub-pixel detail exists only when upscaling.)
bool apply_deswizzle_identity = false;
if (instr.opcode == FetchOpcode::kTextureFetch &&
!instr.attributes.unnormalized_coordinates &&
instr.dimension == xenos::FetchOpDimension::k2D && is_pixel_shader() &&
GetDxbcShaderModification().pixel.param_gen_enable) {
const std::string& neutralize_hashes =
REXCVAR_GET(ac6_neutralize_deswizzle_hashes);
apply_deswizzle_identity =
!neutralize_hashes.empty() &&
UcodeHashSlotInList(current_shader().ucode_data_hash(), tfetch_index,
neutralize_hashes);
}
uint32_t size_needed_components = 0b0000;
if (instr.opcode == FetchOpcode::kGetTextureWeights) {
// Size needed for denormalization for coordinate lerp factor.
@@ -836,6 +926,12 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction(
// the texture is 3D unconditionally.
size_needed_components |= 0b1000;
}
if (apply_host_subpixel_correction || apply_deswizzle_identity) {
// 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).
size_needed_components |= 0b0011;
}
uint32_t size_and_is_3d_temp = size_needed_components ? PushSystemTemp() : UINT32_MAX;
if (size_needed_components) {
switch (instr.dimension) {
@@ -1147,6 +1243,66 @@ void DxbcShaderTranslator::ProcessTextureFetchInstruction(
}
}
}
if (apply_host_subpixel_correction) {
// coord_and_sampler_temp.xy is the final normalized coordinate, sampling
// the guest-texel center (host texel scale*swizzle(g) + scale/2). Shift it
// to the current host sub-pixel so it samples host texel scale*swizzle(g)+s
// (= the host-resolution swizzle), landing on an exact texel center.
uint32_t host_subpixel_temp = PushSystemTemp();
// host_subpixel.xy = floor(SV_Position.xy) mod draw_resolution_scale.
in_position_used_ |= 0b0011;
a_.OpFToU(dxbc::Dest::R(host_subpixel_temp, 0b0011),
dxbc::Src::V1D(in_reg_ps_position_));
a_.OpUDiv(dxbc::Dest::Null(), dxbc::Dest::R(host_subpixel_temp, 0b0011),
dxbc::Src::R(host_subpixel_temp),
dxbc::Src::LU(uint32_t(draw_resolution_scale_x_),
uint32_t(draw_resolution_scale_y_), 1, 1));
a_.OpUToF(dxbc::Dest::R(host_subpixel_temp, 0b0011), dxbc::Src::R(host_subpixel_temp));
// delta.xy = (host_subpixel - (scale-1)/2) / (guest_size * scale).
a_.OpAdd(dxbc::Dest::R(host_subpixel_temp, 0b0011), dxbc::Src::R(host_subpixel_temp),
dxbc::Src::LF(-(float(draw_resolution_scale_x_) - 1.0f) * 0.5f,
-(float(draw_resolution_scale_y_) - 1.0f) * 0.5f, 0.0f, 0.0f));
a_.OpDiv(dxbc::Dest::R(host_subpixel_temp, 0b0011), dxbc::Src::R(host_subpixel_temp),
dxbc::Src::R(size_and_is_3d_temp));
a_.OpMul(dxbc::Dest::R(host_subpixel_temp, 0b0011), dxbc::Src::R(host_subpixel_temp),
dxbc::Src::LF(1.0f / float(draw_resolution_scale_x_),
1.0f / float(draw_resolution_scale_y_), 0.0f, 0.0f));
// Only resolution-scaled textures are stored at host resolution.
a_.OpAnd(dxbc::Dest::R(host_subpixel_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(host_subpixel_temp, dxbc::Src::kZZZZ));
a_.OpAdd(dxbc::Dest::R(coord_and_sampler_temp, 0b0011),
dxbc::Src::R(coord_and_sampler_temp), dxbc::Src::R(host_subpixel_temp));
a_.OpEndIf();
// Release host_subpixel_temp.
PopSystemTemp();
}
if (apply_deswizzle_identity) {
// coord_and_sampler_temp.xy is the guest de-swizzled coordinate. The guest
// wrote these restore/resample passes for raw EDRAM-ordered resolve data,
// but the emulator's texture cache detiles EVERYTHING to linear,
// so the de-swizzle is always a wrong texel
// permutation here.
// Replace it with the identity host-texel UV =
// SV_Position.xy / (guest_size * scale) so the pass copies its source 1:1.
// Unconditional within allowlisted shaders: there is no swizzled-content
// texture this could break.
uint32_t deswizzle_temp = PushSystemTemp();
in_position_used_ |= 0b0011;
// deswizzle_temp.xy = guest_size * draw_resolution_scale = host size.
a_.OpMul(dxbc::Dest::R(deswizzle_temp, 0b0011),
dxbc::Src::R(size_and_is_3d_temp),
dxbc::Src::LF(float(draw_resolution_scale_x_),
float(draw_resolution_scale_y_), 1.0f, 1.0f));
// coord.xy = SV_Position.xy / host size = identity texel-center UV.
a_.OpDiv(dxbc::Dest::R(coord_and_sampler_temp, 0b0011),
dxbc::Src::V1D(in_reg_ps_position_), dxbc::Src::R(deswizzle_temp));
// Release deswizzle_temp.
PopSystemTemp();
}
switch (instr.dimension) {
case xenos::FetchOpDimension::k1D:
// Pad to 2D array coordinates.