Guard float colour outputs against NaN, start mesh counts at zero, and read XInput pads SDL misses.

Patch 0027: a float colour output turns NaN into 0 and clamps the rest to +-1e30 before it reaches its target, so one
bad texel cannot spread through bloom, blur and downsample passes as black or white blocks; integer targets keep their
bits. Task and mesh shaders start their mesh and primitive counts at 0 behind a barrier, and a task emits at most one
mesh group per thread, so a task that culls everything no longer launches what the shared memory last held. Without a
gamepad SDL can open, the first connected XInput controller (slots 0-3, empty slots asked once a second) is read
directly. Kokiri Forest: 60 fps, GPU 5.5 ms a frame, unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-07 01:33:08 -05:00
parent 18b9ef3cd0
commit 29cab01da7
4 changed files with 87 additions and 4 deletions

View File

@@ -638,9 +638,67 @@ static int pad_physical(uint8_t pd[PAD_COUNT]) {
// Under lock, state 1: the open gamepad's buttons (Switch bits after NX2NAVI_PAD_LAYOUT and the NX2NAVI_BIND_* lines) and
// sticks; 0 without a connected gamepad.
#ifdef _WIN32
// Without a gamepad SDL can open, Xbox controllers are read through XInput directly (xinput1_4, else xinput9_1_0):
// SDL may miss a pad another program or driver presents only as XInput. Slots 0-3, the first connected one; a slot
// found empty is asked again once a second, as XInputGetState on an empty slot takes milliseconds.
typedef struct { uint16_t buttons; uint8_t lt, rt; int16_t lx, ly, rx, ry; } XiPad;
typedef struct { uint32_t packet; XiPad pad; } XiState;
typedef uint32_t(__stdcall *XiGetState)(uint32_t, XiState *);
__declspec(dllimport) void *__stdcall LoadLibraryA(const char *);
__declspec(dllimport) void *__stdcall GetProcAddress(void *, const char *);
// Under lock: the first connected XInput pad's inputs by position (input_map.h PAD_*) and sticks (y up); 0 without one.
static int xinput_read(uint8_t pd[PAD_COUNT], int32_t *raw) {
static XiGetState get;
static int tried, slot = -1;
static uint64_t next_try;
if (!tried) {
tried = 1;
void *m = LoadLibraryA("xinput1_4.dll");
if (!m) m = LoadLibraryA("xinput9_1_0.dll");
if (m) get = (XiGetState)GetProcAddress(m, "XInputGetState");
}
if (!get) return 0;
XiState st;
if (slot >= 0 && get((uint32_t)slot, &st) != 0) {
rt_log("input: XInput controller %d disconnected", slot);
slot = -1;
}
if (slot < 0) {
uint64_t now = plat_now_ns();
if (now < next_try) return 0;
next_try = now + 1000000000ull;
for (int i = 0; i < 4 && slot < 0; i++)
if (get((uint32_t)i, &st) == 0) slot = i, rt_log("input: XInput controller %d (SDL has no gamepad)", i);
if (slot < 0) return 0;
}
uint16_t w = st.pad.buttons;
static const struct { uint16_t bit; uint8_t pad; } map[] = {
{0x1000, PAD_SOUTH}, {0x2000, PAD_EAST}, {0x4000, PAD_WEST}, {0x8000, PAD_NORTH}, {0x0040, PAD_LSTICK},
{0x0080, PAD_RSTICK}, {0x0100, PAD_L}, {0x0200, PAD_R}, {0x0010, PAD_START}, {0x0020, PAD_BACK},
{0x0004, PAD_DPAD_LEFT}, {0x0001, PAD_DPAD_UP}, {0x0008, PAD_DPAD_RIGHT}, {0x0002, PAD_DPAD_DOWN}};
for (size_t i = 0; i < sizeof map / sizeof *map; i++) pd[map[i].pad] = (w & map[i].bit) != 0;
pd[PAD_ZL] = st.pad.lt > 128, pd[PAD_ZR] = st.pad.rt > 128;
raw[0] = st.pad.lx, raw[1] = st.pad.ly, raw[2] = st.pad.rx, raw[3] = st.pad.ry;
return 1;
}
#endif
static int pad_read(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
uint8_t pd[PAD_COUNT] = {0};
#ifdef _WIN32
if (!pad_physical(pd)) {
int32_t raw[4];
if (!xinput_read(pd, raw)) return 0;
*b |= input_map_pad_buttons(pd, pad_layout_get());
input_pad_stick(raw[0], raw[1], stick_dead_get(0), sl);
input_pad_stick(raw[2], raw[3], stick_dead_get(1), sr);
return 1;
}
#else
if (!pad_physical(pd)) return 0;
#endif
*b |= input_map_pad_buttons(pd, pad_layout_get());
stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, stick_dead_get(0), sl);
stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, stick_dead_get(1), sr);

View File

@@ -99,8 +99,8 @@ def prepare_sources() -> Path:
for junk in list(src.glob("src/*/obj")) + list(src.glob("src/*/bin")):
shutil.rmtree(junk)
patches = sorted((XLATE / "patches").glob("*.patch"))
if len(patches) != 25:
sys.exit(f"expected 25 patches, found {len(patches)}")
if len(patches) != 26:
sys.exit(f"expected 26 patches, found {len(patches)}")
for patch in patches:
# git apply works outside a repository when given a directory; --check first so a failure names the patch.
for extra in (["--check"], []):

View File

@@ -252,13 +252,20 @@ static class MeshSpirv
body.Add(new Insn(OpFunction, new[] { tVoid, entryFn, 0u, tFnVoid }));
Label();
uint tid = Emit(OpLoad, tUint, lii);
if (m.Stage == 7 && m.HasTask) { CopyPayload(tid, true); Barrier(); }
// The counts start at 0: a task that culls everything never writes its mesh count, and the word then held what
// the workgroup's shared memory last had, up to tens of thousands of mesh groups and a lost device; a mesh
// shader that emits nothing leaves its primitive count the same way.
uint countWord = C((uint)(m.Stage == 6 ? (m.Pay + 4) / 4 : m.Map / 4));
If(tid, C(1), () => Do(OpStore, Emit(OpAccessChain, pSharedUint, sharedVar, countWord), C(0)));
if (m.Stage == 7 && m.HasTask) CopyPayload(tid, true);
Barrier();
Emit(OpFunctionCall, tVoid, mainFn);
Barrier();
var modes = new List<Insn>();
if (m.Stage == 6)
{
uint count = SharedWord(C((uint)(m.Pay + 4) / 4));
// one mesh group per task thread at most: a garbage count cannot launch more than the task could fill
uint count = UMin(SharedWord(C((uint)(m.Pay + 4) / 4)), C((uint)Math.Max(1, m.Threads)));
CopyPayload(tid, false);
Do(OpEmitMeshTasksEXT, count, C(1), C(1), payloadVar);
}

View File

@@ -0,0 +1,18 @@
--- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
@@ -495,6 +495,15 @@
Operand src = Register(regIndexBase + component, RegisterType.Gpr);
+ // NX2NAVI: a float colour output carries no NaN or infinity on to its target: one NaN texel spreads
+ // through the averages of bloom, blur and downsample passes into blocks of black or white. NaN
+ // becomes 0 and the rest is clamped to +-1e30; integer targets keep their bits.
+ if ((TranslatorContext.Definitions.GetFragmentOutputColorType(rtIndex) & AggregateType.ElementTypeMask) == AggregateType.FP32)
+ {
+ Operand clamped = this.FPMinimum(this.FPMaximum(src, ConstF(-1e30f)), ConstF(1e30f));
+ src = this.ConditionalSelect(this.IsNan(src), ConstF(0f), clamped);
+ }
+
// Perform B <-> R swap if needed, for BGRA formats (not supported on OpenGL).
if (!supportsBgra && (component == 0 || component == 2))
{