From 313fd236ee399a73ab409c966b1e81fdc3df3f92 Mon Sep 17 00:00:00 2001 From: Dipshet <264011288+Dipshet@users.noreply.github.com> Date: Wed, 8 Jul 2026 01:28:57 +0300 Subject: [PATCH] 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). --- src/main.cpp | 45 +++++ .../rexglue-sdk/include/rex/graphics/flags.h | 3 + thirdparty/rexglue-sdk/src/graphics/flags.cpp | 32 ++++ .../pipeline/shader/dxbc_translator.cpp | 21 +++ .../pipeline/shader/dxbc_translator_fetch.cpp | 156 ++++++++++++++++++ 5 files changed, 257 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index f3a1d939..f0c63723 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 Ac6recompAppCreate(rex::ui::WindowedAppCon REXCVAR_SET(ac6_unlock_fps, false); ApplyAc6DefaultSettings(); ApplyAc6HybridStartupSafetyOverrides(); + ApplyAc6FixDefaults(); ApplyAc6PerformanceModeOverrides(); REXLOG_INFO("Ac6recompAppCreate: graphics mode={} capture={}", diff --git a/thirdparty/rexglue-sdk/include/rex/graphics/flags.h b/thirdparty/rexglue-sdk/include/rex/graphics/flags.h index 02319267..d4214c33 100644 --- a/thirdparty/rexglue-sdk/include/rex/graphics/flags.h +++ b/thirdparty/rexglue-sdk/include/rex/graphics/flags.h @@ -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); diff --git a/thirdparty/rexglue-sdk/src/graphics/flags.cpp b/thirdparty/rexglue-sdk/src/graphics/flags.cpp index da11d6e4..d815c0e4 100644 --- a/thirdparty/rexglue-sdk/src/graphics/flags.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/flags.cpp @@ -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 \"[:[+...]]\" " + "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 " 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 dd97cb1a..e3833e26 100644 --- a/thirdparty/rexglue-sdk/src/graphics/pipeline/shader/dxbc_translator.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/pipeline/shader/dxbc_translator.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -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 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 1098e0ef..277c1305 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 @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -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 +// "[:[+...]]" (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.