Files
sharpemu/tests/SharpEmu.Libs.Tests/Pad/PadExportsTests.cs
T
kuba b572738547 Pad: implement scePadGetTriggerEffectState under its own NID (#712)
NID znaWI0gpuo8 was mapped to sceUserServiceGetUserName as a "title-captured
alias". It is not that symbol. Recomputing the NID of every catalogued name
(base64 of the reversed first eight sha1 bytes of name+salt) resolves
znaWI0gpuo8 to scePadGetTriggerEffectState, and sceUserServiceGetUserName
hashes to 1xxcMiGu2fo instead. Auditing all 1087 export declarations the same
way found this to be the only NID whose declared name is wrong.

The consequence was not a missing export but a wrong one: the user-service
handler rejected the pad's arguments and returned
SCE_USER_SERVICE_ERROR_INVALID_PARAMETER about eighteen thousand times per
run in PPSA10112, so every poll fell back to a cached button bitmask. It
also hid the calls from every search for pad activity, which is why this
title was believed never to touch scePad at all.

The state size is taken from the caller's own frame rather than assumed: the
guest points the out-param at rbp-0x30 and stores its stack cookie at
rbp-0x28, leaving eight bytes for the state. Writing the sixteen the frame
superficially suggests would land on the cookie and fail the guest's stack
check - the same failure this codebase has already hit three times from
oversized HLE writes - so the test pins the size by asserting the cookie
survives.

No host pad exposes DualSense adaptive-trigger feedback, so the neutral
all-zero state is reported as success, which lets the caller take its normal
path instead of the fallback.
2026-07-31 12:09:24 +03:00

77 lines
2.5 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Pad;
using Xunit;
namespace SharpEmu.Libs.Tests.Pad;
public sealed class PadExportsTests
{
private const ulong Base = 0x1_0000_0000;
private const int InvalidHandle = unchecked((int)0x80920003);
private readonly FakeCpuMemory _memory = new(Base, 0x1000);
private readonly CpuContext _ctx;
public PadExportsTests()
{
_ctx = new CpuContext(_memory, Generation.Gen5);
}
[Theory]
[InlineData(0, 0)]
[InlineData(1, 0)]
[InlineData(2, InvalidHandle)]
[InlineData(-1, InvalidHandle)]
public void SetTiltCorrectionState_ValidatesHandle(int handle, int expected)
{
_ctx[CpuRegister.Rdi] = unchecked((ulong)handle);
Assert.Equal(expected, PadExports.PadSetTiltCorrectionState(_ctx));
}
/// <summary>
/// Mirrors the calling frame observed in PPSA10112: the out-param points at
/// rbp-0x30 and the caller's stack cookie sits at rbp-0x28, so the state is
/// eight bytes. Writing more would smash the cookie and fail the guest's
/// stack check, which is the failure mode this size guards against.
/// </summary>
[Fact]
public void GetTriggerEffectState_WritesEightBytesAndLeavesTheCookieIntact()
{
const ulong stateAddress = Base + 0x100;
const ulong cookieAddress = stateAddress + 8;
const ulong cookie = 0xC0DEC0DECAFEBA00UL;
Assert.True(_memory.TryWrite(stateAddress, new byte[] { 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE }));
Assert.True(_memory.TryWrite(cookieAddress, BitConverter.GetBytes(cookie)));
_ctx[CpuRegister.Rdi] = 0;
_ctx[CpuRegister.Rsi] = stateAddress;
Assert.Equal(0, PadExports.PadGetTriggerEffectState(_ctx));
Span<byte> state = stackalloc byte[8];
Assert.True(_memory.TryRead(stateAddress, state));
foreach (var value in state)
{
Assert.Equal(0, value);
}
Span<byte> guard = stackalloc byte[8];
Assert.True(_memory.TryRead(cookieAddress, guard));
Assert.Equal(cookie, BitConverter.ToUInt64(guard));
}
[Theory]
[InlineData(2)]
[InlineData(-1)]
public void GetTriggerEffectState_RejectsForeignHandles(int handle)
{
_ctx[CpuRegister.Rdi] = unchecked((ulong)handle);
_ctx[CpuRegister.Rsi] = Base + 0x100;
Assert.Equal(InvalidHandle, PadExports.PadGetTriggerEffectState(_ctx));
}
}