God of War Ragnarök

Adaptive triggers never reach the pad: told it runs on a Steam Deck, the game never loads Sony's library and drops every trigger effect.

GPL-3.0-or-laterThis fix in MacGameFix

What you see

No adaptive trigger effect on a DualSense, while the game plays its rumble and, with Steam Input on, shows PlayStation buttons.

Why it happens

Steam answers that the game runs on a Steam Deck under Wine, so the game never loads Sony's library, and its scePadSetTriggerEffect wrapper drops every effect it builds.

What the fix changes

A jump at that wrapper sends each effect to the pad as the DualSense's own trigger effect, in an output report that carries only the trigger fields.

The code

The game's scePadSetTriggerEffect wrapper is found by the name it hands on, not by an address.

fixes/gowr-triggers.c · find_wrapper, lines 293–350
c
static uint8_t *find_wrapper(char *why, size_t cap)
{
    static const char name[] = "scePadSetTriggerEffect";
    IMAGE_NT_HEADERS64 *nt = (IMAGE_NT_HEADERS64 *)(g_exe + ((IMAGE_DOS_HEADER *)g_exe)->e_lfanew);
    IMAGE_SECTION_HEADER *sec = IMAGE_FIRST_SECTION(nt);
    IMAGE_DATA_DIRECTORY *exc = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
    const IMAGE_RUNTIME_FUNCTION_ENTRY *fn = (const IMAGE_RUNTIME_FUNCTION_ENTRY *)(g_exe + exc->VirtualAddress);
    size_t nfn = exc->Size / sizeof(*fn), lo, hi;
    uint8_t *text = NULL, *found = NULL, *s;
    uint32_t text_size = 0, str_rva = 0, i, k;
    int strings = 0, hits = 0;

    for (k = 0; k < nt->FileHeader.NumberOfSections; k++)
    {
        uint8_t *b = g_exe + sec[k].VirtualAddress;
        uint32_t n = sec[k].Misc.VirtualSize;
        if (!memcmp(sec[k].Name, ".text", 6)) { text = b; text_size = n; continue; }
        if (sec[k].Characteristics & IMAGE_SCN_MEM_EXECUTE || !(sec[k].Characteristics & IMAGE_SCN_MEM_READ)) continue;
        for (s = b + 1; s + sizeof(name) <= b + n; s++)
            if (*s == name[0] && !s[-1] && !memcmp(s, name, sizeof(name)))
            {
                strings++;
                str_rva = (uint32_t)(s - g_exe);
            }
    }
    if (strings != 1 || !text || !nfn)
    {
        snprintf(why, cap, "%d copies of the name, %s, %u functions", strings, text ? ".text" : "no .text", (unsigned)nfn);
        return NULL;
    }
    for (i = 0; i + 7 <= text_size; i++)
    {
        uint8_t *at = text + i, *start;
        uint32_t rva;
        int32_t disp;
        if ((at[0] != 0x48 && at[0] != 0x4c) || at[1] != 0x8d || (at[2] & 0xc7) != 0x05) continue;
        memcpy(&disp, at + 3, 4);
        rva = (uint32_t)(at + 7 - g_exe) + (uint32_t)disp;
        if (rva != str_rva) continue;
        rva = (uint32_t)(at - g_exe);
        for (lo = 0, hi = nfn; lo < hi; )
        {
            size_t mid = (lo + hi) / 2;
            if (fn[mid].BeginAddress <= rva) lo = mid + 1; else hi = mid;
        }
        if (!lo || rva >= fn[lo - 1].EndAddress) continue;
        start = g_exe + fn[lo - 1].BeginAddress;
        if (memcmp(start, wrapper_start, sizeof(wrapper_start))) continue;
        hits++;
        found = start;
    }
    if (hits != 1)
    {
        snprintf(why, cap, "%d functions name it and start as the wrapper does", hits);
        return NULL;
    }
    return found;
}

Sony's trigger effect modes become the DualSense's own trigger effect bytes.

fixes/gowr-triggers.c · convert, lines 114–152
c
static void convert(uint32_t mode, const uint8_t *p, uint8_t out[11])
{
    uint8_t s[10] = {0};
    int i;
    switch (mode)
    {
    case MODE_FEEDBACK:      /* position 0-9, strength 1-8 */
        for (i = p[0]; i < 10; i++) s[i] = p[1];
        zones(s, 0x21, 0, out);
        return;
    case MODE_WEAPON:        /* start 2-7, end start+1-8, strength 1-8 */
        memset(out, 0, 11);
        if (p[0] > 9 || p[1] > 9 || p[1] <= p[0] || !p[2] || p[2] > 8) { out[0] = 0x05; return; }
        out[0] = 0x25;
        out[1] = ((1u << p[0]) | (1u << p[1])) & 0xff;
        out[2] = ((1u << p[0]) | (1u << p[1])) >> 8;
        out[3] = p[2] - 1;
        return;
    case MODE_VIBRATION:     /* position 0-9, amplitude 1-8, frequency in Hz */
        for (i = p[0]; i < 10; i++) s[i] = p[1];
        zones(s, 0x26, p[2], out);
        return;
    case MODE_MULTI_FEEDBACK:  /* strength[10] */
        zones(p, 0x21, 0, out);
        return;
    case MODE_SLOPE:         /* start, end, start strength, end strength */
        if (p[1] > p[0] && p[1] <= 9)
            for (i = p[0]; i < 10; i++)
                s[i] = i >= p[1] ? p[3] : (uint8_t)(p[2] + (int)(p[3] - p[2]) * (i - p[0]) / (p[1] - p[0]));
        zones(s, 0x21, 0, out);
        return;
    case MODE_MULTI_VIBRATION: /* frequency, amplitude[10] */
        zones(p + 1, 0x26, p[0], out);
        return;
    default:
        memset(out, 0, 11);
        out[0] = 0x05;
    }
}

Each effect the game builds goes to the pad, only when it changes, and the game gets the answer it always got.

fixes/gowr-triggers.c · triggers_SetTriggerEffect, lines 256–290
c
__attribute__((noinline)) int32_t triggers_SetTriggerEffect(int32_t handle, const uint8_t *param)
{
    uint8_t want[2][11];
    int sides = 0, i;
    (void)handle;

    if (!param) return (int32_t)RETURN_NOT_LOADED;
    InterlockedIncrement(&g_effects);
    EnterCriticalSection(&g_lock);
    if (!g_pad_looked) find_pad();
    if (g_pad != INVALID_HANDLE_VALUE)
    {
        /* Sony's command[0] is L2 and command[1] R2; the DualSense's report has R2 first. */
        for (i = 0; i < 2; i++)
        {
            int ds = i ? 0 : 1;
            const uint8_t *cmd = param + CMD_FIRST + CMD_SIZE * i;
            if (!(param[0] & (1 << i))) continue;
            convert(*(const uint32_t *)cmd, cmd + CMD_PARAM, want[ds]);
            if (memcmp(want[ds], g_last[ds], 11) || !g_set[ds])
            {
                if (!g_set[ds] || want[ds][0] != g_last[ds][0])
                    logline("%s: mode %u -> DualSense %02x %02x%02x %02x%02x%02x%02x .. %02x", ds ? "L2" : "R2",
                            *(const uint32_t *)cmd, want[ds][0], want[ds][1], want[ds][2], want[ds][3], want[ds][4],
                            want[ds][5], want[ds][6], want[ds][9]);
                memcpy(g_last[ds], want[ds], 11);
                g_set[ds] = 1;
                sides |= 1 << ds;
            }
        }
        if (sides) write_sides(want, sides);
    }
    LeaveCriticalSection(&g_lock);
    return (int32_t)RETURN_NOT_LOADED;   /* what the original answers: the game behaves as before */
}

Details

Needs
D3DMetal, through NotProton; the fix does not depend on the backend.
Installer
install-gowr-triggers.sh
Executable
GoWR.exe
Built from
gowr-triggers.c