[video_core, maxwell] fixes for homebrew games that use MESA compiler (#4012)

two main issues:

shader_environment.cpp

Support Mesa nv50_ir-compiled homebrew fragment shaders
Mesa nv50_ir (used by NZP-Switch via libdrm_nouveau) emits Maxwell SASS that differs from NVN in two ways our shader pipeline didn't account for, causing "infinite" loops during shader scanning.

How?  TryFindSize() previously detected end-of-shader only via NVN's "BRA $-1" self-branch trailer (SELF_BRANCH_A/B). Mesa-compiled shaders end with an unconditional @PT EXIT and emit no trailer, so scanning ran past the shader and looped to MAXIMUM_SIZE.

Fix: Added a secondary match against @PT EXIT T (opcode 0xE30, predicate PT, flow T) as a fallback terminator. The mask is estrictive enough to reject predicated EXITs, conditional-flow EXITs, and sched control words, so well-formed NVN shaders see no ehavior change (their single @PT EXIT immediately precedes BRA $-1, and both detections return the same size).

load_store_attribute.cpp + attribute.h

IPA's is_perspective check only applied the ×position_w correction for IR::IsGeneric() attributes, and used a per-component SPH lookup. Two failure modes followed:

1. Mesa fragment shaders read varyings via legacy attribute slots
(ColorFrontDiffuse, FixedFncTexture, FogCoordinate) which are
remapped to generic varyings later by ConvertLegacyToGeneric. At
IPA-translation time they're still legacy, so IsGeneric() was
false and the inject was skipped — but the resulting GLSL/SPIR-V
varying was Smooth (perspective-correct), and the SASS still
issued its manual perspective dance via MUFU.RCP(ATTR_W)+FMUL,
compounding an extra ×clip_w factor. NZP fog (eye_z distance
varying) ended up as eye_z⁴×density² instead of eye_z²×density²,
saturating fog to white everywhere.

2. Even for legacy attrs that should inject, Mesa stores them in two formats selected by the IPA's interpolation_mode field:
Pass/Multiply/Constant → attr/w (perspective); inject ×position_w
Sc (ScreenLinear)      → raw attr; do NOT inject
Unconditionally injecting for Sc dims vertex colors by 1/clip_w because there's no SASS multiplier to round-trip it back.

Fix: extend the IPA correction path to also fire on IsLegacyAttribute(), gated on interpolation_mode != Sc. While here, the generic-path check is widened from per-component to vector-level (first non-Unused PixelImap, with all-Unused fallthrough treated as Perspective) so it matches the GLSL interpolation qualifier picked by CollectInterpolationInfo — this fixes the same all-zeroed-imap case Mesa exposes for explicit generics. IsLegacyAttribute moved from translate_program.cpp's anonymous namespace into attribute.h next to IsGeneric so IPA can reach it.

Tested on NaziZombies:Portable (Switch homebrew): fog now matches real
hardware, vertex-colored geometry (hand, gun, decals) renders correctly.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4012
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
This commit is contained in:
xbzk 2026-06-15 03:05:11 +02:00 committed by crueter
parent ef4113aeaa
commit 73918d23d5
No known key found for this signature in database
GPG key ID: 425ACD2D4830EBC6
5 changed files with 38 additions and 15 deletions

View file

@ -151,7 +151,7 @@ Result IFileSystem::GetTotalSpaceSize(
Result IFileSystem::GetFileTimeStampRaw(
Out<FileSys::FileTimeStampRaw> out_timestamp,
const InLargeData<FileSys::Sf::Path, BufferAttr_HipcPointer> path) {
LOG_WARNING(Service_FS, "(Partial Implementation) called. file={}", path->str);
LOG_DEBUG(Service_FS, "(Partial Implementation) called. file={}", path->str);
FileSys::FileTimeStampRaw vfs_timestamp{};
R_TRY(backend->GetFileTimeStampRaw(&vfs_timestamp, FileSys::Path(path->str)));

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@ -240,6 +240,14 @@ constexpr size_t NUM_FIXEDFNCTEXTURE = 10;
return attribute >= Attribute::Generic0X && attribute <= Attribute::Generic31X;
}
[[nodiscard]] inline bool IsLegacyAttribute(Attribute attribute) noexcept {
return (attribute >= Attribute::ColorFrontDiffuseR &&
attribute <= Attribute::ColorBackSpecularA) ||
attribute == Attribute::FogCoordinate ||
(attribute >= Attribute::FixedFncTexture0S &&
attribute <= Attribute::FixedFncTexture9Q);
}
[[nodiscard]] inline u32 GenericAttributeIndex(Attribute attribute) {
if (!IsGeneric(attribute))
throw InvalidArgument("Attribute is not generic {}", attribute);

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@ -163,12 +166,24 @@ void TranslatorVisitor::IPA(u64 insn) {
const IR::Attribute attribute{ipa.attribute};
IR::F32 value{is_indexed ? ir.GetAttributeIndexed(X(ipa.index_reg))
: ir.GetAttribute(attribute)};
if (IR::IsGeneric(attribute)) {
const ProgramHeader& sph{env.SPH()};
const u32 attr_index{IR::GenericAttributeIndex(attribute)};
const u32 element{static_cast<u32>(attribute) % 4};
const std::array input_map{sph.ps.GenericInputMap(attr_index)};
const bool is_perspective{input_map[element] == Shader::PixelImap::Perspective};
const bool is_legacy{IR::IsLegacyAttribute(attribute)};
if (IR::IsGeneric(attribute) || is_legacy) {
bool is_perspective{is_legacy &&
ipa.interpolation_mode != InterpolationMode::Sc};
if (!is_legacy) {
const ProgramHeader& sph{env.SPH()};
const u32 attr_index{IR::GenericAttributeIndex(attribute)};
const std::array input_map{sph.ps.GenericInputMap(attr_index)};
Shader::PixelImap effective_imap{Shader::PixelImap::Unused};
for (const Shader::PixelImap component : input_map) {
if (component != Shader::PixelImap::Unused) {
effective_imap = component;
break;
}
}
is_perspective = effective_imap == Shader::PixelImap::Perspective ||
effective_imap == Shader::PixelImap::Unused;
}
if (is_perspective) {
const IR::F32 position_w{ir.GetAttribute(IR::Attribute::PositionW)};
value = ir.FPMul(value, position_w);

View file

@ -132,13 +132,7 @@ void AddNVNStorageBuffers(IR::Program& program) {
}
}
bool IsLegacyAttribute(IR::Attribute attribute) {
return (attribute >= IR::Attribute::ColorFrontDiffuseR &&
attribute <= IR::Attribute::ColorBackSpecularA) ||
attribute == IR::Attribute::FogCoordinate ||
(attribute >= IR::Attribute::FixedFncTexture0S &&
attribute <= IR::Attribute::FixedFncTexture9Q);
}
using IR::IsLegacyAttribute; //rescoped to attribute.h to make it visible in load_store_attribute.cpp IPA
std::map<IR::Attribute, IR::Attribute> GenerateLegacyToGenericMappings(
const VaryingState& state, std::queue<IR::Attribute> unused_generics,

View file

@ -254,6 +254,9 @@ std::optional<u64> GenericEnvironment::TryFindSize() {
static constexpr u64 SELF_BRANCH_A = 0xE2400FFFFF87000FULL;
static constexpr u64 SELF_BRANCH_B = 0xE2400FFFFF07000FULL;
static constexpr u64 MESA_EXIT_MASK = 0xFFF00000000F001FULL;
static constexpr u64 MESA_EXIT_VALUE = (0xE30ULL << 52) | (0x7ULL << 16) | 0xFULL;
code.resize(MAXIMUM_SIZE / INST_SIZE);
GPUVAddr guest_addr{program_base + start_address};
@ -267,6 +270,9 @@ std::optional<u64> GenericEnvironment::TryFindSize() {
if (inst == SELF_BRANCH_A || inst == SELF_BRANCH_B) {
return offset + index;
}
if ((inst & MESA_EXIT_MASK) == MESA_EXIT_VALUE) {
return offset + index + INST_SIZE;
}
}
guest_addr += BLOCK_SIZE;
size += BLOCK_SIZE;