Normalize line endings to LF and enforce them with .gitattributes

CRLF line endings broke scripts/prepare_ps5_deps.sh in CI. Only line endings change; the build output is identical.

Signed-off-by: erickdavestech <54048831+erickdavestech@users.noreply.github.com>
This commit is contained in:
erickdavestech committed 2026-10-03 20:52:58 -04:00
1 parent a94b0ff451
commit ffcfa5d29b
17 files changed
+17915 -17904

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
* text=auto eol=lf
*.png binary
*.webp binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.zip binary
*.elf binary
*.plugin binary
+23 -23
View File
@@ -1,23 +1,23 @@
{
"project": "ps5-dualsense-overlay",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"ps5_payload_sdk": {
"repository": "https://github.com/ps5-payload-dev/sdk",
"version": "v0.43",
"commit": "d9c9519",
"purpose": "PS5 payload toolchain and system libraries"
},
"etahen": {
"repository": "https://github.com/etaHEN/etaHEN",
"version": "2.4B",
"commit": "d47f99bd37f349ae59b3c4b66e09e93ba69f56cd",
"purpose": "Pinned GPL source baseline used by the process/module sampling adapter. Runtime hardware test used etaHEN 2.6."
},
"ps5_payload_shsrv": {
"repository": "https://github.com/ps5-payload-dev/shsrv",
"version": "v0.20",
"commit": "6f320637d56d344a0e7797753099e33238bbf146",
"purpose": "Pinned GPL ptrace and ELF-loader primitives used by the source-built ShellUI bootstrap."
}
}
{
"project": "ps5-dualsense-overlay",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"ps5_payload_sdk": {
"repository": "https://github.com/ps5-payload-dev/sdk",
"version": "v0.43",
"commit": "d9c9519",
"purpose": "PS5 payload toolchain and system libraries"
},
"etahen": {
"repository": "https://github.com/etaHEN/etaHEN",
"version": "2.4B",
"commit": "d47f99bd37f349ae59b3c4b66e09e93ba69f56cd",
"purpose": "Pinned GPL source baseline used by the process/module sampling adapter. Runtime hardware test used etaHEN 2.6."
},
"ps5_payload_shsrv": {
"repository": "https://github.com/ps5-payload-dev/shsrv",
"version": "v0.20",
"commit": "6f320637d56d344a0e7797753099e33238bbf146",
"purpose": "Pinned GPL ptrace and ELF-loader primitives used by the source-built ShellUI bootstrap."
}
}
+1 -1
View File
@@ -1 +1 @@
window.LAYOUT = {"canvas": [640, 480], "sizes": {"1280": 2.0, "640": 1.0, "320": 0.5}, "units": "Todas las coordenadas están en unidades de 640x480; multiplica por sizes[carpeta].", "base": "base.png", "base_full": "base_full.png", "buttons": {"triangle": {"sprite": "pressed/triangle.png", "rect": [482, 106, 64, 64], "bit": "0x1000"}, "circle": {"sprite": "pressed/circle.png", "rect": [528, 152, 64, 64], "bit": "0x2000"}, "cross": {"sprite": "pressed/cross.png", "rect": [482, 198, 64, 64], "bit": "0x4000"}, "square": {"sprite": "pressed/square.png", "rect": [436, 152, 64, 64], "bit": "0x8000"}, "dpad_up": {"sprite": "pressed/dpad_up.png", "rect": [96, 122, 58, 66], "bit": "0x10"}, "dpad_down": {"sprite": "pressed/dpad_down.png", "rect": [96, 180, 58, 68], "bit": "0x40"}, "dpad_left": {"sprite": "pressed/dpad_left.png", "rect": [62, 154, 66, 60], "bit": "0x80"}, "dpad_right": {"sprite": "pressed/dpad_right.png", "rect": [122, 154, 66, 60], "bit": "0x20"}, "l1": {"sprite": "pressed/l1.png", "rect": [76, 56, 106, 52], "bit": "0x400"}, "r1": {"sprite": "pressed/r1.png", "rect": [458, 56, 106, 52], "bit": "0x800"}, "create": {"sprite": "pressed/create.png", "rect": [152, 88, 44, 56], "bit": "0x1"}, "options": {"sprite": "pressed/options.png", "rect": [444, 88, 42, 56], "bit": "0x8"}, "mute": {"sprite": "pressed/mute.png", "rect": [292, 288, 56, 30], "bit": null}, "ps": {"sprite": "pressed/ps.png", "rect": [286, 238, 64, 54], "bit": null}, "touchpad": {"sprite": "pressed/touchpad.png", "rect": [182, 60, 276, 160], "bit": "0x100000"}}, "sticks": {"left": {"center": [218.58, 270.67], "travel": 12, "size": [96, 96], "sprite": "stick_cap.png", "sprite_pressed": "stick_cap_l3.png", "click": "l3", "bit": "0x2"}, "right": {"center": [420.17, 270.36], "travel": 12, "size": [96, 96], "sprite": "stick_cap.png", "sprite_pressed": "stick_cap_l3.png", "click": "r3", "bit": "0x4"}}, "triggers": {"l2": {"sprite": "l2_fill.png", "rect": [100, 10, 56, 58], "fill": "bottom_up", "bit": "0x100"}, "r2": {"sprite": "r2_fill.png", "rect": [482, 10, 56, 58], "fill": "bottom_up", "bit": "0x200"}}, "touchpad": {"rect": [195.21, 72.77, 248.96, 133.28], "dot": "touch_dot.png", "dot_size": [44, 44], "note": "Mapear ScePadTouch.x/y con la resolución de scePadGetControllerInformation()."}, "credits": "Button Icons and Controls by Zacksly (CC BY 3.0 | zacksly.itch.io) — modificado"};
window.LAYOUT = {"canvas": [640, 480], "sizes": {"1280": 2.0, "640": 1.0, "320": 0.5}, "units": "Todas las coordenadas están en unidades de 640x480; multiplica por sizes[carpeta].", "base": "base.png", "base_full": "base_full.png", "buttons": {"triangle": {"sprite": "pressed/triangle.png", "rect": [482, 106, 64, 64], "bit": "0x1000"}, "circle": {"sprite": "pressed/circle.png", "rect": [528, 152, 64, 64], "bit": "0x2000"}, "cross": {"sprite": "pressed/cross.png", "rect": [482, 198, 64, 64], "bit": "0x4000"}, "square": {"sprite": "pressed/square.png", "rect": [436, 152, 64, 64], "bit": "0x8000"}, "dpad_up": {"sprite": "pressed/dpad_up.png", "rect": [96, 122, 58, 66], "bit": "0x10"}, "dpad_down": {"sprite": "pressed/dpad_down.png", "rect": [96, 180, 58, 68], "bit": "0x40"}, "dpad_left": {"sprite": "pressed/dpad_left.png", "rect": [62, 154, 66, 60], "bit": "0x80"}, "dpad_right": {"sprite": "pressed/dpad_right.png", "rect": [122, 154, 66, 60], "bit": "0x20"}, "l1": {"sprite": "pressed/l1.png", "rect": [76, 56, 106, 52], "bit": "0x400"}, "r1": {"sprite": "pressed/r1.png", "rect": [458, 56, 106, 52], "bit": "0x800"}, "create": {"sprite": "pressed/create.png", "rect": [152, 88, 44, 56], "bit": "0x1"}, "options": {"sprite": "pressed/options.png", "rect": [444, 88, 42, 56], "bit": "0x8"}, "mute": {"sprite": "pressed/mute.png", "rect": [292, 288, 56, 30], "bit": null}, "ps": {"sprite": "pressed/ps.png", "rect": [286, 238, 64, 54], "bit": null}, "touchpad": {"sprite": "pressed/touchpad.png", "rect": [182, 60, 276, 160], "bit": "0x100000"}}, "sticks": {"left": {"center": [218.58, 270.67], "travel": 12, "size": [96, 96], "sprite": "stick_cap.png", "sprite_pressed": "stick_cap_l3.png", "click": "l3", "bit": "0x2"}, "right": {"center": [420.17, 270.36], "travel": 12, "size": [96, 96], "sprite": "stick_cap.png", "sprite_pressed": "stick_cap_l3.png", "click": "r3", "bit": "0x4"}}, "triggers": {"l2": {"sprite": "l2_fill.png", "rect": [100, 10, 56, 58], "fill": "bottom_up", "bit": "0x100"}, "r2": {"sprite": "r2_fill.png", "rect": [482, 10, 56, 58], "fill": "bottom_up", "bit": "0x200"}}, "touchpad": {"rect": [195.21, 72.77, 248.96, 133.28], "dot": "touch_dot.png", "dot_size": [44, 44], "note": "Mapear ScePadTouch.x/y con la resolución de scePadGetControllerInformation()."}, "credits": "Button Icons and Controls by Zacksly (CC BY 3.0 | zacksly.itch.io) — modificado"};
+236 -236
View File
@@ -1,237 +1,237 @@
{
"canvas": [
640,
480
],
"sizes": {
"1280": 2.0,
"640": 1.0,
"320": 0.5
},
"units": "Todas las coordenadas están en unidades de 640x480; multiplica por sizes[carpeta].",
"base": "base.png",
"base_full": "base_full.png",
"buttons": {
"triangle": {
"sprite": "pressed/triangle.png",
"rect": [
482,
106,
64,
64
],
"bit": "0x1000"
},
"circle": {
"sprite": "pressed/circle.png",
"rect": [
528,
152,
64,
64
],
"bit": "0x2000"
},
"cross": {
"sprite": "pressed/cross.png",
"rect": [
482,
198,
64,
64
],
"bit": "0x4000"
},
"square": {
"sprite": "pressed/square.png",
"rect": [
436,
152,
64,
64
],
"bit": "0x8000"
},
"dpad_up": {
"sprite": "pressed/dpad_up.png",
"rect": [
96,
122,
58,
66
],
"bit": "0x10"
},
"dpad_down": {
"sprite": "pressed/dpad_down.png",
"rect": [
96,
180,
58,
68
],
"bit": "0x40"
},
"dpad_left": {
"sprite": "pressed/dpad_left.png",
"rect": [
62,
154,
66,
60
],
"bit": "0x80"
},
"dpad_right": {
"sprite": "pressed/dpad_right.png",
"rect": [
122,
154,
66,
60
],
"bit": "0x20"
},
"l1": {
"sprite": "pressed/l1.png",
"rect": [
76,
56,
106,
52
],
"bit": "0x400"
},
"r1": {
"sprite": "pressed/r1.png",
"rect": [
458,
56,
106,
52
],
"bit": "0x800"
},
"create": {
"sprite": "pressed/create.png",
"rect": [
152,
88,
44,
56
],
"bit": "0x1"
},
"options": {
"sprite": "pressed/options.png",
"rect": [
444,
88,
42,
56
],
"bit": "0x8"
},
"mute": {
"sprite": "pressed/mute.png",
"rect": [
292,
288,
56,
30
],
"bit": null
},
"ps": {
"sprite": "pressed/ps.png",
"rect": [
286,
238,
64,
54
],
"bit": null
},
"touchpad": {
"sprite": "pressed/touchpad.png",
"rect": [
182,
60,
276,
160
],
"bit": "0x100000"
}
},
"sticks": {
"left": {
"center": [
218.58,
270.67
],
"travel": 12,
"size": [
96,
96
],
"sprite": "stick_cap.png",
"sprite_pressed": "stick_cap_l3.png",
"click": "l3",
"bit": "0x2"
},
"right": {
"center": [
420.17,
270.36
],
"travel": 12,
"size": [
96,
96
],
"sprite": "stick_cap.png",
"sprite_pressed": "stick_cap_l3.png",
"click": "r3",
"bit": "0x4"
}
},
"triggers": {
"l2": {
"sprite": "l2_fill.png",
"rect": [
100,
10,
56,
58
],
"fill": "bottom_up",
"bit": "0x100"
},
"r2": {
"sprite": "r2_fill.png",
"rect": [
482,
10,
56,
58
],
"fill": "bottom_up",
"bit": "0x200"
}
},
"touchpad": {
"rect": [
195.21,
72.77,
248.96,
133.28
],
"dot": "touch_dot.png",
"dot_size": [
44,
44
],
"note": "Mapear ScePadTouch.x/y con la resolución de scePadGetControllerInformation()."
},
"credits": "Button Icons and Controls by Zacksly (CC BY 3.0 | zacksly.itch.io) — modificado"
{
"canvas": [
640,
480
],
"sizes": {
"1280": 2.0,
"640": 1.0,
"320": 0.5
},
"units": "Todas las coordenadas están en unidades de 640x480; multiplica por sizes[carpeta].",
"base": "base.png",
"base_full": "base_full.png",
"buttons": {
"triangle": {
"sprite": "pressed/triangle.png",
"rect": [
482,
106,
64,
64
],
"bit": "0x1000"
},
"circle": {
"sprite": "pressed/circle.png",
"rect": [
528,
152,
64,
64
],
"bit": "0x2000"
},
"cross": {
"sprite": "pressed/cross.png",
"rect": [
482,
198,
64,
64
],
"bit": "0x4000"
},
"square": {
"sprite": "pressed/square.png",
"rect": [
436,
152,
64,
64
],
"bit": "0x8000"
},
"dpad_up": {
"sprite": "pressed/dpad_up.png",
"rect": [
96,
122,
58,
66
],
"bit": "0x10"
},
"dpad_down": {
"sprite": "pressed/dpad_down.png",
"rect": [
96,
180,
58,
68
],
"bit": "0x40"
},
"dpad_left": {
"sprite": "pressed/dpad_left.png",
"rect": [
62,
154,
66,
60
],
"bit": "0x80"
},
"dpad_right": {
"sprite": "pressed/dpad_right.png",
"rect": [
122,
154,
66,
60
],
"bit": "0x20"
},
"l1": {
"sprite": "pressed/l1.png",
"rect": [
76,
56,
106,
52
],
"bit": "0x400"
},
"r1": {
"sprite": "pressed/r1.png",
"rect": [
458,
56,
106,
52
],
"bit": "0x800"
},
"create": {
"sprite": "pressed/create.png",
"rect": [
152,
88,
44,
56
],
"bit": "0x1"
},
"options": {
"sprite": "pressed/options.png",
"rect": [
444,
88,
42,
56
],
"bit": "0x8"
},
"mute": {
"sprite": "pressed/mute.png",
"rect": [
292,
288,
56,
30
],
"bit": null
},
"ps": {
"sprite": "pressed/ps.png",
"rect": [
286,
238,
64,
54
],
"bit": null
},
"touchpad": {
"sprite": "pressed/touchpad.png",
"rect": [
182,
60,
276,
160
],
"bit": "0x100000"
}
},
"sticks": {
"left": {
"center": [
218.58,
270.67
],
"travel": 12,
"size": [
96,
96
],
"sprite": "stick_cap.png",
"sprite_pressed": "stick_cap_l3.png",
"click": "l3",
"bit": "0x2"
},
"right": {
"center": [
420.17,
270.36
],
"travel": 12,
"size": [
96,
96
],
"sprite": "stick_cap.png",
"sprite_pressed": "stick_cap_l3.png",
"click": "r3",
"bit": "0x4"
}
},
"triggers": {
"l2": {
"sprite": "l2_fill.png",
"rect": [
100,
10,
56,
58
],
"fill": "bottom_up",
"bit": "0x100"
},
"r2": {
"sprite": "r2_fill.png",
"rect": [
482,
10,
56,
58
],
"fill": "bottom_up",
"bit": "0x200"
}
},
"touchpad": {
"rect": [
195.21,
72.77,
248.96,
133.28
],
"dot": "touch_dot.png",
"dot_size": [
44,
44
],
"note": "Mapear ScePadTouch.x/y con la resolución de scePadGetControllerInformation()."
},
"credits": "Button Icons and Controls by Zacksly (CC BY 3.0 | zacksly.itch.io) — modificado"
}
+175 -175
View File
@@ -1,175 +1,175 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pad Overlay Preview</title>
<style>
:root { --bg: #11151c; --panel: #1a2029; --text: #d8dde6; --muted: #8a93a3; --accent: #2f7bff; }
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.4 system-ui, "Segoe UI", sans-serif; }
header { display: flex; flex-wrap: wrap; gap: 12px 20px; align-items: center; padding: 12px 16px; background: var(--panel); }
header h1 { font-size: 15px; margin: 0 8px 0 0; }
label { color: var(--muted); }
select { background: #0d1117; color: var(--text); border: 1px solid #2c3442; border-radius: 6px; padding: 3px 6px; }
#src { margin-left: auto; padding: 3px 10px; border-radius: 999px; background: #0d1117; }
#src.pad { color: #2bd99f; }
#stage { display: grid; place-items: center; padding: 24px 16px; min-height: 60vh; overflow: auto; }
#stage.game { background: radial-gradient(ellipse at 30% 20%, #3b5b7a, #16222f 55%, #0b0f14); }
#stage.light { background: #c4c9be; }
#stage.checker { background: repeating-conic-gradient(#2a2f38 0 25%, #20242b 0 50%) 0 0 / 24px 24px; }
#ov { position: relative; flex: none; }
#ov img { position: absolute; display: block; user-select: none; pointer-events: none; }
#ov .off { visibility: hidden; }
.trig { position: absolute; overflow: hidden; }
.trig img { left: 0; bottom: 0; }
#help { padding: 0 16px 20px; color: var(--muted); max-width: 900px; margin: 0 auto; }
kbd { background: #0d1117; border: 1px solid #2c3442; border-radius: 4px; padding: 0 5px; color: var(--text); font-size: 12px; }
</style>
</head>
<body>
<header>
<h1>Overlay del mando — simulador</h1>
<label>Tamaño <select id="size"><option>1280</option><option selected>640</option><option>320</option></select></label>
<label>Fondo <select id="bgsel"><option value="game">juego</option><option value="checker">transparente</option><option value="light">claro</option></select></label>
<span id="src">teclado</span>
</header>
<div id="stage" class="game"><div id="ov"></div></div>
<div id="help">
Conecta un DualSense al PC (USB o Bluetooth) y pulsa un botón, o usa el teclado:
<kbd>←↑→↓</kbd> cruceta · <kbd>I</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> △ □ ✕ ○ ·
<kbd>Q</kbd>/<kbd>E</kbd> L1/R1 · <kbd>1</kbd>/<kbd>3</kbd> L2/R2 (analógico) ·
<kbd>WASD</kbd> stick izq · <kbd>TFGH</kbd> stick der · <kbd>Z</kbd>/<kbd>X</kbd> L3/R3 ·
<kbd>C</kbd> Create · <kbd>V</kbd> Options · <kbd>P</kbd> PS · <kbd>M</kbd> Mute · <kbd>Espacio</kbd> clic touchpad ·
ratón sobre el touchpad = dedo.<br>
Simula en el PC lo que dibujará el renderer en la PS5: mismos PNG, mismo layout.json.
</div>
<script src="layout.js"></script>
<script>
(() => {
const L = window.LAYOUT;
const qs = new URLSearchParams(location.search);
const ov = document.getElementById('ov');
const sizeSel = document.getElementById('size');
const bgSel = document.getElementById('bgsel');
const srcTag = document.getElementById('src');
if (L.sizes[qs.get('size')]) sizeSel.value = qs.get('size');
// Estado de entrada, idéntico al que mandará el ELF: botones + ejes en [-1,1] + gatillos [0,1] + toque.
const st = { btn: new Set(), lx: 0, ly: 0, rx: 0, ry: 0, l2: 0, r2: 0, touch: null };
let els = {};
function img(src, rect, s, extra) {
const i = document.createElement('img');
i.src = `png/${sizeSel.value}/${src}`;
i.style.left = rect[0] * s + 'px'; i.style.top = rect[1] * s + 'px';
i.style.width = rect[2] * s + 'px'; i.style.height = rect[3] * s + 'px';
if (extra) i.className = extra;
return i;
}
function build() {
const s = L.sizes[sizeSel.value];
ov.innerHTML = '';
ov.style.width = L.canvas[0] * s + 'px'; ov.style.height = L.canvas[1] * s + 'px';
els = { btn: {}, trig: {}, stick: {}, dot: null, s };
ov.append(img(L.base, [0, 0, ...L.canvas], s));
for (const [t, d] of Object.entries(L.triggers)) {
const box = document.createElement('div');
box.className = 'trig';
box.style.left = d.rect[0] * s + 'px'; box.style.width = d.rect[2] * s + 'px';
const fill = img(d.sprite, [0, 0, d.rect[2], d.rect[3]], s);
fill.style.top = ''; fill.style.bottom = '0';
box.append(fill); ov.append(box);
els.trig[t] = { box, d };
}
for (const [n, d] of Object.entries(L.buttons)) ov.append(els.btn[n] = img(d.sprite, d.rect, s, 'off'));
for (const [side, d] of Object.entries(L.sticks)) ov.append(els.stick[side] = img(d.sprite, [0, 0, ...d.size], s));
const tp = L.touchpad;
ov.append(els.dot = img(tp.dot, [0, 0, ...tp.dot_size], s, 'off'));
draw();
}
function draw() {
const s = els.s;
for (const [n, i] of Object.entries(els.btn)) i.classList.toggle('off', !st.btn.has(n));
for (const [t, { box, d }] of Object.entries(els.trig)) {
const v = Math.max(0, Math.min(1, st[t]));
box.style.top = (d.rect[1] + d.rect[3] * (1 - v)) * s + 'px';
box.style.height = d.rect[3] * v * s + 'px';
}
for (const [side, i] of Object.entries(els.stick)) {
const d = L.sticks[side];
const [x, y] = side === 'left' ? [st.lx, st.ly] : [st.rx, st.ry];
i.style.left = (d.center[0] + x * d.travel - d.size[0] / 2) * s + 'px';
i.style.top = (d.center[1] + y * d.travel - d.size[1] / 2) * s + 'px';
const want = `png/${sizeSel.value}/${st.btn.has(d.click) ? d.sprite_pressed : d.sprite}`;
if (!i.src.endsWith(want)) i.src = want;
}
const tp = L.touchpad, dot = els.dot;
dot.classList.toggle('off', !st.touch);
if (st.touch) {
dot.style.left = (tp.rect[0] + st.touch[0] * tp.rect[2] - tp.dot_size[0] / 2) * s + 'px';
dot.style.top = (tp.rect[1] + st.touch[1] * tp.rect[3] - tp.dot_size[1] / 2) * s + 'px';
}
}
// --- Teclado ---
const KEYS = {
ArrowUp: 'dpad_up', ArrowDown: 'dpad_down', ArrowLeft: 'dpad_left', ArrowRight: 'dpad_right',
KeyI: 'triangle', KeyJ: 'square', KeyK: 'cross', KeyL: 'circle', KeyQ: 'l1', KeyE: 'r1',
KeyZ: 'l3', KeyX: 'r3', KeyC: 'create', KeyV: 'options', KeyP: 'ps', KeyM: 'mute', Space: 'touchpad',
};
const held = new Set();
addEventListener('keydown', e => { if (KEYS[e.code] || /^(Key[WASDTFGH]|Digit[13])$/.test(e.code)) { held.add(e.code); e.preventDefault(); } });
addEventListener('keyup', e => held.delete(e.code));
addEventListener('blur', () => held.clear());
const axis = (neg, pos) => (held.has(pos) ? 1 : 0) - (held.has(neg) ? 1 : 0);
// --- Ratón sobre el touchpad = un dedo ---
ov.addEventListener('pointermove', e => {
const r = ov.getBoundingClientRect(), s = els.s, tp = L.touchpad.rect;
const u = ((e.clientX - r.left) / s - tp[0]) / tp[2], v = ((e.clientY - r.top) / s - tp[1]) / tp[3];
st.mouseTouch = u >= 0 && u <= 1 && v >= 0 && v <= 1 ? [u, v] : null;
});
ov.addEventListener('pointerleave', () => { st.mouseTouch = null; });
// --- Gamepad API (mapeo "standard": así ve Chrome al DualSense) ---
const GP = ['cross', 'circle', 'square', 'triangle', 'l1', 'r1', null, null, 'create', 'options',
'l3', 'r3', 'dpad_up', 'dpad_down', 'dpad_left', 'dpad_right', 'ps', 'touchpad'];
const dz = v => Math.abs(v) < 0.08 ? 0 : v;
let last = performance.now();
function tick(now) {
const dt = Math.min(0.1, (now - last) / 1000); last = now;
const pad = [...(navigator.getGamepads?.() || [])].find(p => p && p.connected);
st.btn.clear();
if (pad) {
srcTag.textContent = pad.id.replace(/\s*\(.*$/, '') || 'mando'; srcTag.className = 'pad';
GP.forEach((n, i) => { if (n && pad.buttons[i]?.pressed) st.btn.add(n); });
st.l2 = pad.buttons[6]?.value || 0; st.r2 = pad.buttons[7]?.value || 0;
[st.lx, st.ly, st.rx, st.ry] = [0, 1, 2, 3].map(i => dz(pad.axes[i] || 0));
} else {
srcTag.textContent = 'teclado'; srcTag.className = '';
for (const c of held) if (KEYS[c]) st.btn.add(KEYS[c]);
const ramp = (v, on) => on ? Math.min(1, v + dt * 4) : Math.max(0, v - dt * 6);
st.l2 = ramp(st.l2, held.has('Digit1')); st.r2 = ramp(st.r2, held.has('Digit3'));
const norm = (x, y) => { const m = Math.hypot(x, y) || 1; return [x / Math.max(1, m), y / Math.max(1, m)]; };
[st.lx, st.ly] = norm(axis('KeyA', 'KeyD'), axis('KeyW', 'KeyS'));
[st.rx, st.ry] = norm(axis('KeyF', 'KeyH'), axis('KeyT', 'KeyG'));
}
if (st.l2 > 0.05) st.btn.add('l2');
if (st.r2 > 0.05) st.btn.add('r2');
st.touch = st.mouseTouch;
draw();
requestAnimationFrame(tick);
}
sizeSel.addEventListener('change', build);
bgSel.addEventListener('change', () => { document.getElementById('stage').className = bgSel.value; });
build();
requestAnimationFrame(tick);
})();
</script>
</body>
</html>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pad Overlay Preview</title>
<style>
:root { --bg: #11151c; --panel: #1a2029; --text: #d8dde6; --muted: #8a93a3; --accent: #2f7bff; }
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.4 system-ui, "Segoe UI", sans-serif; }
header { display: flex; flex-wrap: wrap; gap: 12px 20px; align-items: center; padding: 12px 16px; background: var(--panel); }
header h1 { font-size: 15px; margin: 0 8px 0 0; }
label { color: var(--muted); }
select { background: #0d1117; color: var(--text); border: 1px solid #2c3442; border-radius: 6px; padding: 3px 6px; }
#src { margin-left: auto; padding: 3px 10px; border-radius: 999px; background: #0d1117; }
#src.pad { color: #2bd99f; }
#stage { display: grid; place-items: center; padding: 24px 16px; min-height: 60vh; overflow: auto; }
#stage.game { background: radial-gradient(ellipse at 30% 20%, #3b5b7a, #16222f 55%, #0b0f14); }
#stage.light { background: #c4c9be; }
#stage.checker { background: repeating-conic-gradient(#2a2f38 0 25%, #20242b 0 50%) 0 0 / 24px 24px; }
#ov { position: relative; flex: none; }
#ov img { position: absolute; display: block; user-select: none; pointer-events: none; }
#ov .off { visibility: hidden; }
.trig { position: absolute; overflow: hidden; }
.trig img { left: 0; bottom: 0; }
#help { padding: 0 16px 20px; color: var(--muted); max-width: 900px; margin: 0 auto; }
kbd { background: #0d1117; border: 1px solid #2c3442; border-radius: 4px; padding: 0 5px; color: var(--text); font-size: 12px; }
</style>
</head>
<body>
<header>
<h1>Overlay del mando — simulador</h1>
<label>Tamaño <select id="size"><option>1280</option><option selected>640</option><option>320</option></select></label>
<label>Fondo <select id="bgsel"><option value="game">juego</option><option value="checker">transparente</option><option value="light">claro</option></select></label>
<span id="src">teclado</span>
</header>
<div id="stage" class="game"><div id="ov"></div></div>
<div id="help">
Conecta un DualSense al PC (USB o Bluetooth) y pulsa un botón, o usa el teclado:
<kbd>←↑→↓</kbd> cruceta · <kbd>I</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> △ □ ✕ ○ ·
<kbd>Q</kbd>/<kbd>E</kbd> L1/R1 · <kbd>1</kbd>/<kbd>3</kbd> L2/R2 (analógico) ·
<kbd>WASD</kbd> stick izq · <kbd>TFGH</kbd> stick der · <kbd>Z</kbd>/<kbd>X</kbd> L3/R3 ·
<kbd>C</kbd> Create · <kbd>V</kbd> Options · <kbd>P</kbd> PS · <kbd>M</kbd> Mute · <kbd>Espacio</kbd> clic touchpad ·
ratón sobre el touchpad = dedo.<br>
Simula en el PC lo que dibujará el renderer en la PS5: mismos PNG, mismo layout.json.
</div>
<script src="layout.js"></script>
<script>
(() => {
const L = window.LAYOUT;
const qs = new URLSearchParams(location.search);
const ov = document.getElementById('ov');
const sizeSel = document.getElementById('size');
const bgSel = document.getElementById('bgsel');
const srcTag = document.getElementById('src');
if (L.sizes[qs.get('size')]) sizeSel.value = qs.get('size');
// Estado de entrada, idéntico al que mandará el ELF: botones + ejes en [-1,1] + gatillos [0,1] + toque.
const st = { btn: new Set(), lx: 0, ly: 0, rx: 0, ry: 0, l2: 0, r2: 0, touch: null };
let els = {};
function img(src, rect, s, extra) {
const i = document.createElement('img');
i.src = `png/${sizeSel.value}/${src}`;
i.style.left = rect[0] * s + 'px'; i.style.top = rect[1] * s + 'px';
i.style.width = rect[2] * s + 'px'; i.style.height = rect[3] * s + 'px';
if (extra) i.className = extra;
return i;
}
function build() {
const s = L.sizes[sizeSel.value];
ov.innerHTML = '';
ov.style.width = L.canvas[0] * s + 'px'; ov.style.height = L.canvas[1] * s + 'px';
els = { btn: {}, trig: {}, stick: {}, dot: null, s };
ov.append(img(L.base, [0, 0, ...L.canvas], s));
for (const [t, d] of Object.entries(L.triggers)) {
const box = document.createElement('div');
box.className = 'trig';
box.style.left = d.rect[0] * s + 'px'; box.style.width = d.rect[2] * s + 'px';
const fill = img(d.sprite, [0, 0, d.rect[2], d.rect[3]], s);
fill.style.top = ''; fill.style.bottom = '0';
box.append(fill); ov.append(box);
els.trig[t] = { box, d };
}
for (const [n, d] of Object.entries(L.buttons)) ov.append(els.btn[n] = img(d.sprite, d.rect, s, 'off'));
for (const [side, d] of Object.entries(L.sticks)) ov.append(els.stick[side] = img(d.sprite, [0, 0, ...d.size], s));
const tp = L.touchpad;
ov.append(els.dot = img(tp.dot, [0, 0, ...tp.dot_size], s, 'off'));
draw();
}
function draw() {
const s = els.s;
for (const [n, i] of Object.entries(els.btn)) i.classList.toggle('off', !st.btn.has(n));
for (const [t, { box, d }] of Object.entries(els.trig)) {
const v = Math.max(0, Math.min(1, st[t]));
box.style.top = (d.rect[1] + d.rect[3] * (1 - v)) * s + 'px';
box.style.height = d.rect[3] * v * s + 'px';
}
for (const [side, i] of Object.entries(els.stick)) {
const d = L.sticks[side];
const [x, y] = side === 'left' ? [st.lx, st.ly] : [st.rx, st.ry];
i.style.left = (d.center[0] + x * d.travel - d.size[0] / 2) * s + 'px';
i.style.top = (d.center[1] + y * d.travel - d.size[1] / 2) * s + 'px';
const want = `png/${sizeSel.value}/${st.btn.has(d.click) ? d.sprite_pressed : d.sprite}`;
if (!i.src.endsWith(want)) i.src = want;
}
const tp = L.touchpad, dot = els.dot;
dot.classList.toggle('off', !st.touch);
if (st.touch) {
dot.style.left = (tp.rect[0] + st.touch[0] * tp.rect[2] - tp.dot_size[0] / 2) * s + 'px';
dot.style.top = (tp.rect[1] + st.touch[1] * tp.rect[3] - tp.dot_size[1] / 2) * s + 'px';
}
}
// --- Teclado ---
const KEYS = {
ArrowUp: 'dpad_up', ArrowDown: 'dpad_down', ArrowLeft: 'dpad_left', ArrowRight: 'dpad_right',
KeyI: 'triangle', KeyJ: 'square', KeyK: 'cross', KeyL: 'circle', KeyQ: 'l1', KeyE: 'r1',
KeyZ: 'l3', KeyX: 'r3', KeyC: 'create', KeyV: 'options', KeyP: 'ps', KeyM: 'mute', Space: 'touchpad',
};
const held = new Set();
addEventListener('keydown', e => { if (KEYS[e.code] || /^(Key[WASDTFGH]|Digit[13])$/.test(e.code)) { held.add(e.code); e.preventDefault(); } });
addEventListener('keyup', e => held.delete(e.code));
addEventListener('blur', () => held.clear());
const axis = (neg, pos) => (held.has(pos) ? 1 : 0) - (held.has(neg) ? 1 : 0);
// --- Ratón sobre el touchpad = un dedo ---
ov.addEventListener('pointermove', e => {
const r = ov.getBoundingClientRect(), s = els.s, tp = L.touchpad.rect;
const u = ((e.clientX - r.left) / s - tp[0]) / tp[2], v = ((e.clientY - r.top) / s - tp[1]) / tp[3];
st.mouseTouch = u >= 0 && u <= 1 && v >= 0 && v <= 1 ? [u, v] : null;
});
ov.addEventListener('pointerleave', () => { st.mouseTouch = null; });
// --- Gamepad API (mapeo "standard": así ve Chrome al DualSense) ---
const GP = ['cross', 'circle', 'square', 'triangle', 'l1', 'r1', null, null, 'create', 'options',
'l3', 'r3', 'dpad_up', 'dpad_down', 'dpad_left', 'dpad_right', 'ps', 'touchpad'];
const dz = v => Math.abs(v) < 0.08 ? 0 : v;
let last = performance.now();
function tick(now) {
const dt = Math.min(0.1, (now - last) / 1000); last = now;
const pad = [...(navigator.getGamepads?.() || [])].find(p => p && p.connected);
st.btn.clear();
if (pad) {
srcTag.textContent = pad.id.replace(/\s*\(.*$/, '') || 'mando'; srcTag.className = 'pad';
GP.forEach((n, i) => { if (n && pad.buttons[i]?.pressed) st.btn.add(n); });
st.l2 = pad.buttons[6]?.value || 0; st.r2 = pad.buttons[7]?.value || 0;
[st.lx, st.ly, st.rx, st.ry] = [0, 1, 2, 3].map(i => dz(pad.axes[i] || 0));
} else {
srcTag.textContent = 'teclado'; srcTag.className = '';
for (const c of held) if (KEYS[c]) st.btn.add(KEYS[c]);
const ramp = (v, on) => on ? Math.min(1, v + dt * 4) : Math.max(0, v - dt * 6);
st.l2 = ramp(st.l2, held.has('Digit1')); st.r2 = ramp(st.r2, held.has('Digit3'));
const norm = (x, y) => { const m = Math.hypot(x, y) || 1; return [x / Math.max(1, m), y / Math.max(1, m)]; };
[st.lx, st.ly] = norm(axis('KeyA', 'KeyD'), axis('KeyW', 'KeyS'));
[st.rx, st.ry] = norm(axis('KeyF', 'KeyH'), axis('KeyT', 'KeyG'));
}
if (st.l2 > 0.05) st.btn.add('l2');
if (st.r2 > 0.05) st.btn.add('r2');
st.touch = st.mouseTouch;
draw();
requestAnimationFrame(tick);
}
sizeSel.addEventListener('change', build);
bgSel.addEventListener('change', () => { document.getElementById('stage').className = bgSel.value; });
build();
requestAnimationFrame(tick);
})();
</script>
</body>
</html>
+40 -40
View File
@@ -1,41 +1,41 @@
This work is licensed under the Creative Commons Attribution 3.0 Unported License.
To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/
This allow you the right to:
- Share: copy and redistribute the material in any medium or format
- Adapt: remix, transform, and build upon the material for any purpose, even commercially
Provided you follow the following terms:
- Attribution: You must give appropriate credit, provide a link to the license and indicate if
changes were made. You may do so in any reasonable manner, but not in any way that suggests
the licensor endorses you or your use.
- No additional restrictions — You may not apply legal terms or technological measures that
legally restrict others from doing anything the license permits.
How to give credit for asset use, redistributions, or derivative works:
Must include:
- Product Title: PS5 Button Icons and Controls
- Author: Zacksly
- Source: https://zacksly.itch.io
- License: “CC BY 3.0"
- If icons were edited from their original form it must be stated that it has been modified.
Example Attributions:
Credits Section:
"PS5 Button Icons and Controls - Zacksly
Licensed under CC BY 3.0 - https://zacksly.itch.io"
Statement:
"Button Icons and Controls were created by Zacksly (Licensed under CC BY 3.0 - https://zacksly.itch.io")"
Derivative Work:
"Button Icons and Controls are based on project by Zacksly (Licensed under CC BY 3.0 - https://zacksly.itch.io)"
Casual Thanks:
"Special thanks to Zacksly for the Button Icons and Controls (Licensed under CC BY 3.0 - https://zacksly.itch.io)"
Short form Credit:
"Button Icons and Controls by Zacksly (CC BY 3.0 Licensed | zacksly.itch.io)"
Notices:
- You do not have to comply with the license for elements of the material in the public domain or
where your use is permitted by an applicable exception or limitation.
- No warranties are given. The license may not give you all of the permissions necessary for your
This work is licensed under the Creative Commons Attribution 3.0 Unported License.
To view a copy of this license, visit http://creativecommons.org/licenses/by/3.0/
This allow you the right to:
- Share: copy and redistribute the material in any medium or format
- Adapt: remix, transform, and build upon the material for any purpose, even commercially
Provided you follow the following terms:
- Attribution: You must give appropriate credit, provide a link to the license and indicate if
changes were made. You may do so in any reasonable manner, but not in any way that suggests
the licensor endorses you or your use.
- No additional restrictions — You may not apply legal terms or technological measures that
legally restrict others from doing anything the license permits.
How to give credit for asset use, redistributions, or derivative works:
Must include:
- Product Title: PS5 Button Icons and Controls
- Author: Zacksly
- Source: https://zacksly.itch.io
- License: “CC BY 3.0"
- If icons were edited from their original form it must be stated that it has been modified.
Example Attributions:
Credits Section:
"PS5 Button Icons and Controls - Zacksly
Licensed under CC BY 3.0 - https://zacksly.itch.io"
Statement:
"Button Icons and Controls were created by Zacksly (Licensed under CC BY 3.0 - https://zacksly.itch.io")"
Derivative Work:
"Button Icons and Controls are based on project by Zacksly (Licensed under CC BY 3.0 - https://zacksly.itch.io)"
Casual Thanks:
"Special thanks to Zacksly for the Button Icons and Controls (Licensed under CC BY 3.0 - https://zacksly.itch.io)"
Short form Credit:
"Button Icons and Controls by Zacksly (CC BY 3.0 Licensed | zacksly.itch.io)"
Notices:
- You do not have to comply with the license for elements of the material in the public domain or
where your use is permitted by an applicable exception or limitation.
- No warranties are given. The license may not give you all of the permissions necessary for your
intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material.
+284 -284
View File
@@ -1,284 +1,284 @@
/*
* Common FPS for PS5
* Copyright (C) 2026 porhe911
* Modifications Copyright (C) 2026 khalifa007 (SimpleFPS)
* Modifications Copyright (C) 2026 erickdavestech
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace common_fps {
inline constexpr const char* kShellUiHookRequestPath =
"/system_tmp/commonfps_v121_hook_request.bin";
inline constexpr const char* kShellUiHookRequestTempPath =
"/system_tmp/commonfps_v121_hook_request.tmp";
inline constexpr const char* kShellUiHookAckPath =
"/system_tmp/commonfps_v121_hook_ack.bin";
inline constexpr const char* kShellUiHookAckTempPath =
"/system_tmp/commonfps_v121_hook_ack.tmp";
inline constexpr std::uint64_t kShellUiHookRequestMagic =
0x395145524b484643ULL; /* "CFHKREQ9" */
inline constexpr std::uint64_t kShellUiHookAckMagic =
0x394b43414b484643ULL; /* "CFHKACK9" */
inline constexpr std::uint32_t kShellUiHookProtocolVersion = 9;
inline constexpr std::size_t kShellUiHookPatchSize = 16;
inline constexpr std::uint8_t kShellUiAbsoluteJumpPrefix[6] = {
0xff, 0x25, 0x00, 0x00, 0x00, 0x00,
};
enum class ShellUiHookBackend { Unsupported, Mdbg, PtraceIo };
enum class ShellUiHookAckPhase : std::uint8_t {
Patch = 0,
Probe = 1,
MainThreadGuard = 2,
};
inline ShellUiHookBackend shellui_hook_backend(
std::uint32_t sdk, bool eta_chain) noexcept {
const std::uint32_t family = sdk & 0xffff0000U;
if ((family >= 0x03000000U && family <= 0x08200000U) ||
(family >= 0x11000000U && family <= 0x13ff0000U))
return ShellUiHookBackend::Mdbg;
if (family == 0x09600000U && eta_chain)
return ShellUiHookBackend::PtraceIo;
return ShellUiHookBackend::Unsupported;
}
/*
* The background-PUI path never replaces Application.Update. It changes only
* the first byte of Diagnostics.CheckRunningOnMainThread to RET while ShellUI
* is ptrace-stopped. FW 1.xx-8.20 uses MDBG; selected higher firmware uses
* ptrace I/O. FW 9.60 deliberately keeps the hardware-proven update-chain
* backend. Unknown future firmware fails closed.
*/
inline ShellUiHookBackend shellui_main_thread_guard_backend(
std::uint32_t sdk) noexcept {
const std::uint32_t family = sdk & 0xffff0000U;
/*
* Low/mid firmware uses the controller MDBG backend. Higher firmware
* keeps the same one-byte, expected-byte-verified guard transaction but
* uses ptrace I/O, which is already required by the ShellUI loader.
*
* FW 9.60 deliberately stays on the hardware-proven Application.Update
* chain path; returning Unsupported here selects that existing backend.
*/
if ((family >= 0x01000000U && family <= 0x08200000U) ||
family == 0x09000000U ||
(family >= 0x11000000U && family <= 0x13ff0000U))
return ShellUiHookBackend::Mdbg;
if ((family >= 0x08300000U && family < 0x09000000U) ||
(family > 0x09000000U && family < 0x09600000U) ||
(family > 0x09600000U && family <= 0x13ff0000U))
return ShellUiHookBackend::PtraceIo;
return ShellUiHookBackend::Unsupported;
}
enum class ShellUiHookStatus : std::int32_t {
Success = 0,
InvalidRequest = -1,
UnsupportedFirmware = -2,
AuthFailure = -3,
AttachFailure = -4,
ExpectedBytesMismatch = -5,
WriteFailure = -6,
VerifyFailure = -7,
DetachFailure = -8,
AuthRestoreFailure = -9,
};
#pragma pack(push, 1)
struct ShellUiHookRequest {
std::uint64_t magic = kShellUiHookRequestMagic;
std::uint32_t version = kShellUiHookProtocolVersion;
std::int32_t pid = -1;
std::uint64_t nonce = 0;
std::uint64_t method_address = 0;
std::uint64_t hook_address = 0;
std::uint64_t original_call_address = 0;
std::uint32_t patch_size = kShellUiHookPatchSize;
std::uint32_t displaced_size = 0;
std::uint8_t expected[kShellUiHookPatchSize]{};
std::uint8_t desired[kShellUiHookPatchSize]{};
std::uint32_t checksum = 0;
};
struct ShellUiHookAck {
std::uint64_t magic = kShellUiHookAckMagic;
std::uint32_t version = kShellUiHookProtocolVersion;
std::int32_t pid = -1;
std::uint64_t nonce = 0;
std::int32_t status =
static_cast<std::int32_t>(ShellUiHookStatus::InvalidRequest);
std::int32_t read_rc = -1;
std::int32_t write_rc = -1;
std::uint8_t verified = 0;
std::uint8_t restored = 0;
std::uint8_t detached = 0;
std::uint8_t auth_restored = 0;
std::uint8_t phase =
static_cast<std::uint8_t>(ShellUiHookAckPhase::Patch);
std::uint8_t observed[kShellUiHookPatchSize]{};
std::uint32_t checksum = 0;
};
#pragma pack(pop)
static_assert(sizeof(ShellUiHookRequest) == 92);
static_assert(sizeof(ShellUiHookAck) == 61);
inline std::uint32_t shellui_hook_checksum(
const void* data,
std::size_t size) noexcept {
const auto* bytes = static_cast<const std::uint8_t*>(data);
std::uint32_t hash = 2166136261U;
for (std::size_t i = 0; i < size; ++i) {
hash ^= bytes[i];
hash *= 16777619U;
}
return hash;
}
inline std::uint32_t shellui_hook_request_checksum(
const ShellUiHookRequest& request) noexcept {
return shellui_hook_checksum(
&request,
offsetof(ShellUiHookRequest, checksum));
}
inline std::uint32_t shellui_hook_ack_checksum(
const ShellUiHookAck& ack) noexcept {
return shellui_hook_checksum(
&ack,
offsetof(ShellUiHookAck, checksum));
}
inline bool shellui_hook_is_eta_chain(
const ShellUiHookRequest& request) noexcept {
return std::memcmp(request.expected, kShellUiAbsoluteJumpPrefix,
sizeof(kShellUiAbsoluteJumpPrefix)) == 0;
}
inline bool shellui_user_address(std::uint64_t address) noexcept {
return address >= 0x10000ULL && address < 0x0000800000000000ULL;
}
inline bool shellui_hook_request_common_is_valid(
const ShellUiHookRequest& request, std::int32_t pid) noexcept {
if (request.magic != kShellUiHookRequestMagic ||
request.version != kShellUiHookProtocolVersion ||
request.pid != pid || pid <= 0 || request.nonce == 0 ||
!shellui_user_address(request.method_address) ||
!shellui_user_address(request.method_address + kShellUiHookPatchSize - 1) ||
!shellui_user_address(request.hook_address) ||
request.patch_size != kShellUiHookPatchSize ||
request.checksum != shellui_hook_request_checksum(request))
return false;
return true;
}
inline bool shellui_hook_request_is_probe(
const ShellUiHookRequest& request) noexcept {
return request.displaced_size == 0;
}
inline bool shellui_hook_request_is_main_thread_guard(
const ShellUiHookRequest& request) noexcept {
return request.displaced_size == 1 &&
request.original_call_address == 0 &&
request.hook_address == request.method_address;
}
inline bool shellui_hook_probe_request_is_valid(
const ShellUiHookRequest& request, std::int32_t pid) noexcept {
if (!shellui_hook_request_common_is_valid(request, pid) ||
request.original_call_address != 0 ||
!shellui_hook_request_is_probe(request))
return false;
for (std::size_t i = 0; i < kShellUiHookPatchSize; ++i)
if (request.expected[i] != 0 || request.desired[i] != 0)
return false;
return true;
}
inline bool shellui_main_thread_guard_request_is_valid(
const ShellUiHookRequest& request, std::int32_t pid) noexcept {
if (!shellui_hook_request_common_is_valid(request, pid) ||
!shellui_hook_request_is_main_thread_guard(request) ||
request.expected[0] == 0xc3 ||
request.desired[0] != 0xc3)
return false;
for (std::size_t i = 1; i < kShellUiHookPatchSize; ++i)
if (request.desired[i] != request.expected[i])
return false;
return true;
}
inline bool shellui_hook_request_is_valid(
const ShellUiHookRequest& request, std::int32_t pid) noexcept {
if (!shellui_hook_request_common_is_valid(request, pid) ||
!shellui_user_address(request.original_call_address) ||
request.original_call_address == request.hook_address ||
request.original_call_address == request.method_address ||
request.displaced_size < 14 || request.displaced_size > 16 ||
shellui_hook_request_is_probe(request) ||
shellui_hook_request_is_main_thread_guard(request) ||
std::memcmp(request.desired, kShellUiAbsoluteJumpPrefix, 6) != 0)
return false;
std::uint64_t target = 0;
std::memcpy(&target, request.desired + 6, sizeof(target));
if (target != request.hook_address)
return false;
if (shellui_hook_is_eta_chain(request)) {
std::uint64_t previous = 0;
std::memcpy(&previous, request.expected + 6, sizeof(previous));
if (request.displaced_size != 14 ||
previous != request.original_call_address)
return false;
} else {
for (std::size_t i = 14; i < request.displaced_size; ++i)
if (request.desired[i] != 0x90)
return false;
}
for (std::size_t i = request.displaced_size;
i < kShellUiHookPatchSize;
++i)
if (request.desired[i] != request.expected[i])
return false;
return true;
}
inline ShellUiHookBackend shellui_hook_probe_backend(
std::uint32_t sdk) noexcept {
const std::uint32_t family = sdk & 0xffff0000U;
if ((family >= 0x01000000U && family <= 0x08200000U) ||
family == 0x09000000U ||
(family >= 0x11000000U && family <= 0x13ff0000U))
return ShellUiHookBackend::Mdbg;
if (family >= 0x08300000U && family <= 0x13ff0000U)
return ShellUiHookBackend::PtraceIo;
return ShellUiHookBackend::Unsupported;
}
} // namespace common_fps
/*
* Common FPS for PS5
* Copyright (C) 2026 porhe911
* Modifications Copyright (C) 2026 khalifa007 (SimpleFPS)
* Modifications Copyright (C) 2026 erickdavestech
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace common_fps {
inline constexpr const char* kShellUiHookRequestPath =
"/system_tmp/commonfps_v121_hook_request.bin";
inline constexpr const char* kShellUiHookRequestTempPath =
"/system_tmp/commonfps_v121_hook_request.tmp";
inline constexpr const char* kShellUiHookAckPath =
"/system_tmp/commonfps_v121_hook_ack.bin";
inline constexpr const char* kShellUiHookAckTempPath =
"/system_tmp/commonfps_v121_hook_ack.tmp";
inline constexpr std::uint64_t kShellUiHookRequestMagic =
0x395145524b484643ULL; /* "CFHKREQ9" */
inline constexpr std::uint64_t kShellUiHookAckMagic =
0x394b43414b484643ULL; /* "CFHKACK9" */
inline constexpr std::uint32_t kShellUiHookProtocolVersion = 9;
inline constexpr std::size_t kShellUiHookPatchSize = 16;
inline constexpr std::uint8_t kShellUiAbsoluteJumpPrefix[6] = {
0xff, 0x25, 0x00, 0x00, 0x00, 0x00,
};
enum class ShellUiHookBackend { Unsupported, Mdbg, PtraceIo };
enum class ShellUiHookAckPhase : std::uint8_t {
Patch = 0,
Probe = 1,
MainThreadGuard = 2,
};
inline ShellUiHookBackend shellui_hook_backend(
std::uint32_t sdk, bool eta_chain) noexcept {
const std::uint32_t family = sdk & 0xffff0000U;
if ((family >= 0x03000000U && family <= 0x08200000U) ||
(family >= 0x11000000U && family <= 0x13ff0000U))
return ShellUiHookBackend::Mdbg;
if (family == 0x09600000U && eta_chain)
return ShellUiHookBackend::PtraceIo;
return ShellUiHookBackend::Unsupported;
}
/*
* The background-PUI path never replaces Application.Update. It changes only
* the first byte of Diagnostics.CheckRunningOnMainThread to RET while ShellUI
* is ptrace-stopped. FW 1.xx-8.20 uses MDBG; selected higher firmware uses
* ptrace I/O. FW 9.60 deliberately keeps the hardware-proven update-chain
* backend. Unknown future firmware fails closed.
*/
inline ShellUiHookBackend shellui_main_thread_guard_backend(
std::uint32_t sdk) noexcept {
const std::uint32_t family = sdk & 0xffff0000U;
/*
* Low/mid firmware uses the controller MDBG backend. Higher firmware
* keeps the same one-byte, expected-byte-verified guard transaction but
* uses ptrace I/O, which is already required by the ShellUI loader.
*
* FW 9.60 deliberately stays on the hardware-proven Application.Update
* chain path; returning Unsupported here selects that existing backend.
*/
if ((family >= 0x01000000U && family <= 0x08200000U) ||
family == 0x09000000U ||
(family >= 0x11000000U && family <= 0x13ff0000U))
return ShellUiHookBackend::Mdbg;
if ((family >= 0x08300000U && family < 0x09000000U) ||
(family > 0x09000000U && family < 0x09600000U) ||
(family > 0x09600000U && family <= 0x13ff0000U))
return ShellUiHookBackend::PtraceIo;
return ShellUiHookBackend::Unsupported;
}
enum class ShellUiHookStatus : std::int32_t {
Success = 0,
InvalidRequest = -1,
UnsupportedFirmware = -2,
AuthFailure = -3,
AttachFailure = -4,
ExpectedBytesMismatch = -5,
WriteFailure = -6,
VerifyFailure = -7,
DetachFailure = -8,
AuthRestoreFailure = -9,
};
#pragma pack(push, 1)
struct ShellUiHookRequest {
std::uint64_t magic = kShellUiHookRequestMagic;
std::uint32_t version = kShellUiHookProtocolVersion;
std::int32_t pid = -1;
std::uint64_t nonce = 0;
std::uint64_t method_address = 0;
std::uint64_t hook_address = 0;
std::uint64_t original_call_address = 0;
std::uint32_t patch_size = kShellUiHookPatchSize;
std::uint32_t displaced_size = 0;
std::uint8_t expected[kShellUiHookPatchSize]{};
std::uint8_t desired[kShellUiHookPatchSize]{};
std::uint32_t checksum = 0;
};
struct ShellUiHookAck {
std::uint64_t magic = kShellUiHookAckMagic;
std::uint32_t version = kShellUiHookProtocolVersion;
std::int32_t pid = -1;
std::uint64_t nonce = 0;
std::int32_t status =
static_cast<std::int32_t>(ShellUiHookStatus::InvalidRequest);
std::int32_t read_rc = -1;
std::int32_t write_rc = -1;
std::uint8_t verified = 0;
std::uint8_t restored = 0;
std::uint8_t detached = 0;
std::uint8_t auth_restored = 0;
std::uint8_t phase =
static_cast<std::uint8_t>(ShellUiHookAckPhase::Patch);
std::uint8_t observed[kShellUiHookPatchSize]{};
std::uint32_t checksum = 0;
};
#pragma pack(pop)
static_assert(sizeof(ShellUiHookRequest) == 92);
static_assert(sizeof(ShellUiHookAck) == 61);
inline std::uint32_t shellui_hook_checksum(
const void* data,
std::size_t size) noexcept {
const auto* bytes = static_cast<const std::uint8_t*>(data);
std::uint32_t hash = 2166136261U;
for (std::size_t i = 0; i < size; ++i) {
hash ^= bytes[i];
hash *= 16777619U;
}
return hash;
}
inline std::uint32_t shellui_hook_request_checksum(
const ShellUiHookRequest& request) noexcept {
return shellui_hook_checksum(
&request,
offsetof(ShellUiHookRequest, checksum));
}
inline std::uint32_t shellui_hook_ack_checksum(
const ShellUiHookAck& ack) noexcept {
return shellui_hook_checksum(
&ack,
offsetof(ShellUiHookAck, checksum));
}
inline bool shellui_hook_is_eta_chain(
const ShellUiHookRequest& request) noexcept {
return std::memcmp(request.expected, kShellUiAbsoluteJumpPrefix,
sizeof(kShellUiAbsoluteJumpPrefix)) == 0;
}
inline bool shellui_user_address(std::uint64_t address) noexcept {
return address >= 0x10000ULL && address < 0x0000800000000000ULL;
}
inline bool shellui_hook_request_common_is_valid(
const ShellUiHookRequest& request, std::int32_t pid) noexcept {
if (request.magic != kShellUiHookRequestMagic ||
request.version != kShellUiHookProtocolVersion ||
request.pid != pid || pid <= 0 || request.nonce == 0 ||
!shellui_user_address(request.method_address) ||
!shellui_user_address(request.method_address + kShellUiHookPatchSize - 1) ||
!shellui_user_address(request.hook_address) ||
request.patch_size != kShellUiHookPatchSize ||
request.checksum != shellui_hook_request_checksum(request))
return false;
return true;
}
inline bool shellui_hook_request_is_probe(
const ShellUiHookRequest& request) noexcept {
return request.displaced_size == 0;
}
inline bool shellui_hook_request_is_main_thread_guard(
const ShellUiHookRequest& request) noexcept {
return request.displaced_size == 1 &&
request.original_call_address == 0 &&
request.hook_address == request.method_address;
}
inline bool shellui_hook_probe_request_is_valid(
const ShellUiHookRequest& request, std::int32_t pid) noexcept {
if (!shellui_hook_request_common_is_valid(request, pid) ||
request.original_call_address != 0 ||
!shellui_hook_request_is_probe(request))
return false;
for (std::size_t i = 0; i < kShellUiHookPatchSize; ++i)
if (request.expected[i] != 0 || request.desired[i] != 0)
return false;
return true;
}
inline bool shellui_main_thread_guard_request_is_valid(
const ShellUiHookRequest& request, std::int32_t pid) noexcept {
if (!shellui_hook_request_common_is_valid(request, pid) ||
!shellui_hook_request_is_main_thread_guard(request) ||
request.expected[0] == 0xc3 ||
request.desired[0] != 0xc3)
return false;
for (std::size_t i = 1; i < kShellUiHookPatchSize; ++i)
if (request.desired[i] != request.expected[i])
return false;
return true;
}
inline bool shellui_hook_request_is_valid(
const ShellUiHookRequest& request, std::int32_t pid) noexcept {
if (!shellui_hook_request_common_is_valid(request, pid) ||
!shellui_user_address(request.original_call_address) ||
request.original_call_address == request.hook_address ||
request.original_call_address == request.method_address ||
request.displaced_size < 14 || request.displaced_size > 16 ||
shellui_hook_request_is_probe(request) ||
shellui_hook_request_is_main_thread_guard(request) ||
std::memcmp(request.desired, kShellUiAbsoluteJumpPrefix, 6) != 0)
return false;
std::uint64_t target = 0;
std::memcpy(&target, request.desired + 6, sizeof(target));
if (target != request.hook_address)
return false;
if (shellui_hook_is_eta_chain(request)) {
std::uint64_t previous = 0;
std::memcpy(&previous, request.expected + 6, sizeof(previous));
if (request.displaced_size != 14 ||
previous != request.original_call_address)
return false;
} else {
for (std::size_t i = 14; i < request.displaced_size; ++i)
if (request.desired[i] != 0x90)
return false;
}
for (std::size_t i = request.displaced_size;
i < kShellUiHookPatchSize;
++i)
if (request.desired[i] != request.expected[i])
return false;
return true;
}
inline ShellUiHookBackend shellui_hook_probe_backend(
std::uint32_t sdk) noexcept {
const std::uint32_t family = sdk & 0xffff0000U;
if ((family >= 0x01000000U && family <= 0x08200000U) ||
family == 0x09000000U ||
(family >= 0x11000000U && family <= 0x13ff0000U))
return ShellUiHookBackend::Mdbg;
if (family >= 0x08300000U && family <= 0x13ff0000U)
return ShellUiHookBackend::PtraceIo;
return ShellUiHookBackend::Unsupported;
}
} // namespace common_fps
+44 -44
View File
@@ -1,44 +1,44 @@
/*
* Common FPS for PS5
* Copyright (C) 2026 porhe911
* Modifications Copyright (C) 2026 khalifa007 (SimpleFPS)
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <cstdint>
#include <string>
namespace common_fps {
using ProcessId = int;
enum class Corner {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
};
struct Vec2 {
float x{};
float y{};
};
struct OverlayConfig {
Corner corner = Corner::TopLeft;
int font_size = 24;
float margin_x = 10.0f;
float margin_y = 10.0f;
};
struct OverlayFrame {
bool visible = false;
bool loading = true;
int fps = 0;
OverlayConfig config{};
Vec2 anchor{};
};
} // namespace common_fps
/*
* Common FPS for PS5
* Copyright (C) 2026 porhe911
* Modifications Copyright (C) 2026 khalifa007 (SimpleFPS)
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <cstdint>
#include <string>
namespace common_fps {
using ProcessId = int;
enum class Corner {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
};
struct Vec2 {
float x{};
float y{};
};
struct OverlayConfig {
Corner corner = Corner::TopLeft;
int font_size = 24;
float margin_x = 10.0f;
float margin_y = 10.0f;
};
struct OverlayFrame {
bool visible = false;
bool loading = true;
int fps = 0;
OverlayConfig config{};
Vec2 anchor{};
};
} // namespace common_fps
+54 -54
View File
@@ -1,54 +1,54 @@
/*
* Common FPS for PS5
* Copyright (C) 2026 porhe911
* Modifications Copyright (C) 2026 khalifa007 (SimpleFPS)
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include "common_fps/types.hpp"
#include <cstdint>
#include <optional>
namespace common_fps {
inline constexpr std::uint32_t kWireMagic = 0x53465043; // "CFPS"
inline constexpr std::uint32_t kWireShutdownMagic = 0x58465043; // "CFPX"
inline constexpr std::uint16_t kWireVersion = 1;
inline constexpr std::uint16_t kDefaultIpcPort = 39028;
#pragma pack(push, 1)
struct WirePacket {
std::uint32_t magic = kWireMagic;
std::uint16_t version = kWireVersion;
std::uint16_t size = sizeof(WirePacket);
std::uint64_t sequence = 0;
std::int32_t fps = 0;
std::uint8_t loading = 1;
std::uint8_t corner = 0; // TopLeft
std::uint16_t font_size = 24;
float margin_x = 10.0f;
float margin_y = 10.0f;
};
#pragma pack(pop)
WirePacket make_wire_packet(
const OverlayFrame& frame,
std::uint64_t sequence);
/* Reserved controller-to-ShellUI quiesce packet; v1.2.0 does not send it. */
WirePacket make_shutdown_wire_packet(std::uint64_t sequence);
[[nodiscard]] bool
is_shutdown_wire_packet(const WirePacket& packet) noexcept;
std::optional<OverlayFrame>
decode_wire_packet(const WirePacket& packet);
} // namespace common_fps
/*
* Common FPS for PS5
* Copyright (C) 2026 porhe911
* Modifications Copyright (C) 2026 khalifa007 (SimpleFPS)
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include "common_fps/types.hpp"
#include <cstdint>
#include <optional>
namespace common_fps {
inline constexpr std::uint32_t kWireMagic = 0x53465043; // "CFPS"
inline constexpr std::uint32_t kWireShutdownMagic = 0x58465043; // "CFPX"
inline constexpr std::uint16_t kWireVersion = 1;
inline constexpr std::uint16_t kDefaultIpcPort = 39028;
#pragma pack(push, 1)
struct WirePacket {
std::uint32_t magic = kWireMagic;
std::uint16_t version = kWireVersion;
std::uint16_t size = sizeof(WirePacket);
std::uint64_t sequence = 0;
std::int32_t fps = 0;
std::uint8_t loading = 1;
std::uint8_t corner = 0; // TopLeft
std::uint16_t font_size = 24;
float margin_x = 10.0f;
float margin_y = 10.0f;
};
#pragma pack(pop)
WirePacket make_wire_packet(
const OverlayFrame& frame,
std::uint64_t sequence);
/* Reserved controller-to-ShellUI quiesce packet; v1.2.0 does not send it. */
WirePacket make_shutdown_wire_packet(std::uint64_t sequence);
[[nodiscard]] bool
is_shutdown_wire_packet(const WirePacket& packet) noexcept;
std::optional<OverlayFrame>
decode_wire_packet(const WirePacket& packet);
} // namespace common_fps
+49 -49
View File
@@ -1,49 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEPS="${ROOT}/.deps"
SDK_ZIP="/tmp/ps5-payload-sdk-v0.43.zip"
ETAHEN_VERSION="2.4B"
ETAHEN_COMMIT="d47f99bd37f349ae59b3c4b66e09e93ba69f56cd"
mkdir -p "${DEPS}"
echo "[1/4] PS5 Payload SDK v0.43"
wget -q \
https://github.com/ps5-payload-dev/sdk/releases/download/v0.43/ps5-payload-sdk.zip \
-O "${SDK_ZIP}"
sudo rm -rf /opt/ps5-payload-sdk
sudo unzip -q "${SDK_ZIP}" -d /opt
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
test -x "${PS5_PAYLOAD_SDK}/bin/prospero-cmake"
echo "[2/4] etaHEN ${ETAHEN_VERSION} source (${ETAHEN_COMMIT})"
rm -rf "${DEPS}/etahen"
git clone -q https://github.com/etaHEN/etaHEN.git "${DEPS}/etahen"
git -C "${DEPS}/etahen" checkout -q "${ETAHEN_COMMIT}"
RESOLVED_ETAHEN="$(git -C "${DEPS}/etahen" rev-parse HEAD)"
if [ "${RESOLVED_ETAHEN}" != "${ETAHEN_COMMIT}" ]; then
echo "ERROR: etaHEN revision mismatch"
echo "Expected: ${ETAHEN_COMMIT}"
echo "Actual: ${RESOLVED_ETAHEN}"
exit 1
fi
echo "[3/4] ps5-payload-dev/shsrv v0.20 source"
rm -rf "${DEPS}/shsrv"
git clone -q --branch v0.20 --depth 1 \
https://github.com/ps5-payload-dev/shsrv.git "${DEPS}/shsrv"
echo "[4/4] Record exact resolved revisions"
{
echo "PS5 Payload SDK: v0.43"
echo "etaHEN: ${RESOLVED_ETAHEN} (${ETAHEN_VERSION})"
echo "shsrv: $(git -C "${DEPS}/shsrv" rev-parse HEAD)"
} > "${ROOT}/RESOLVED_BUILD_DEPENDENCIES.txt"
cat "${ROOT}/RESOLVED_BUILD_DEPENDENCIES.txt"
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEPS="${ROOT}/.deps"
SDK_ZIP="/tmp/ps5-payload-sdk-v0.43.zip"
ETAHEN_VERSION="2.4B"
ETAHEN_COMMIT="d47f99bd37f349ae59b3c4b66e09e93ba69f56cd"
mkdir -p "${DEPS}"
echo "[1/4] PS5 Payload SDK v0.43"
wget -q \
https://github.com/ps5-payload-dev/sdk/releases/download/v0.43/ps5-payload-sdk.zip \
-O "${SDK_ZIP}"
sudo rm -rf /opt/ps5-payload-sdk
sudo unzip -q "${SDK_ZIP}" -d /opt
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
test -x "${PS5_PAYLOAD_SDK}/bin/prospero-cmake"
echo "[2/4] etaHEN ${ETAHEN_VERSION} source (${ETAHEN_COMMIT})"
rm -rf "${DEPS}/etahen"
git clone -q https://github.com/etaHEN/etaHEN.git "${DEPS}/etahen"
git -C "${DEPS}/etahen" checkout -q "${ETAHEN_COMMIT}"
RESOLVED_ETAHEN="$(git -C "${DEPS}/etahen" rev-parse HEAD)"
if [ "${RESOLVED_ETAHEN}" != "${ETAHEN_COMMIT}" ]; then
echo "ERROR: etaHEN revision mismatch"
echo "Expected: ${ETAHEN_COMMIT}"
echo "Actual: ${RESOLVED_ETAHEN}"
exit 1
fi
echo "[3/4] ps5-payload-dev/shsrv v0.20 source"
rm -rf "${DEPS}/shsrv"
git clone -q --branch v0.20 --depth 1 \
https://github.com/ps5-payload-dev/shsrv.git "${DEPS}/shsrv"
echo "[4/4] Record exact resolved revisions"
{
echo "PS5 Payload SDK: v0.43"
echo "etaHEN: ${RESOLVED_ETAHEN} (${ETAHEN_VERSION})"
echo "shsrv: $(git -C "${DEPS}/shsrv" rev-parse HEAD)"
} > "${ROOT}/RESOLVED_BUILD_DEPENDENCIES.txt"
cat "${ROOT}/RESOLVED_BUILD_DEPENDENCIES.txt"
+332 -332
View File
@@ -1,332 +1,332 @@
/*
* Common FPS for PS5
* Copyright (C) 2026 porhe911
* Modifications Copyright (C) 2026 khalifa007 (SimpleFPS)
* Modifications Copyright (C) 2026 erickdavestech
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "shellui_hook_controller.hpp"
#include "stable_sampler/proc_rw_v960.hpp"
#include <array>
#include <csignal>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/mman.h>
extern "C" {
#include <ps5/kernel.h>
#include <ps5/mdbg.h>
#include "pt.h"
}
namespace common_fps::ps5 {
namespace {
constexpr std::uint64_t kPtraceAuthId = 0x4800000000010003ULL;
constexpr std::uint64_t kPs5CodePageSize = 0x4000ULL;
template <typename T>
bool read_exact_file(const char* path, T& value) noexcept {
FILE* fp = std::fopen(path, "rb");
if (!fp)
return false;
const std::size_t count = std::fread(&value, 1, sizeof(value), fp);
const int trailing = std::fgetc(fp);
std::fclose(fp);
return count == sizeof(value) && trailing == EOF;
}
template <typename T>
bool write_exact_file_atomic(
const char* temporary_path,
const char* final_path,
const T& value) noexcept {
FILE* fp = std::fopen(temporary_path, "wb");
if (!fp)
return false;
const bool complete =
std::fwrite(&value, 1, sizeof(value), fp) == sizeof(value) &&
std::fflush(fp) == 0;
const bool closed = std::fclose(fp) == 0;
if (!complete || !closed) {
(void)unlink(temporary_path);
return false;
}
if (std::rename(temporary_path, final_path) != 0) {
(void)unlink(temporary_path);
return false;
}
return true;
}
void write_ack(
const ShellUiHookRequest& request,
const ShellUiHookPatchReport& report) noexcept {
ShellUiHookAck ack{};
ack.pid = request.pid;
ack.nonce = request.nonce;
ack.status = static_cast<std::int32_t>(report.status);
ack.read_rc = report.read_rc;
ack.write_rc = report.write_rc;
ack.verified = report.verified ? 1U : 0U;
ack.restored = report.restored ? 1U : 0U;
ack.detached = report.detached ? 1U : 0U;
ack.auth_restored = report.auth_restored ? 1U : 0U;
ack.phase = static_cast<std::uint8_t>(
report.probe
? ShellUiHookAckPhase::Probe
: (report.main_thread_guard
? ShellUiHookAckPhase::MainThreadGuard
: ShellUiHookAckPhase::Patch));
std::memcpy(
ack.observed,
report.observed.data(),
report.observed.size());
ack.checksum = shellui_hook_ack_checksum(ack);
(void)write_exact_file_atomic(
kShellUiHookAckTempPath,
kShellUiHookAckPath,
ack);
}
} // namespace
void clear_shellui_hook_protocol_files() noexcept {
(void)unlink(kShellUiHookRequestPath);
(void)unlink(kShellUiHookRequestTempPath);
(void)unlink(kShellUiHookAckPath);
(void)unlink(kShellUiHookAckTempPath);
}
ShellUiHookPollResult poll_and_apply_shellui_hook(
pid_t shellui_pid,
ShellUiHookPatchReport& report) noexcept {
report = {};
ShellUiHookRequest request{};
if (!read_exact_file(kShellUiHookRequestPath, request))
return ShellUiHookPollResult::NoRequest;
/* Consume one complete request; short files are retried on the next poll. */
(void)unlink(kShellUiHookRequestPath);
report.sdk_version = stable_sampler::firmware_sdk_version();
report.method_address = request.method_address;
report.displaced_size = request.displaced_size;
report.probe = shellui_hook_request_is_probe(request);
report.main_thread_guard =
shellui_hook_request_is_main_thread_guard(request);
const bool valid = report.probe
? shellui_hook_probe_request_is_valid(request, shellui_pid)
: (report.main_thread_guard
? shellui_main_thread_guard_request_is_valid(
request, shellui_pid)
: shellui_hook_request_is_valid(request, shellui_pid));
if (!valid) {
report.status = ShellUiHookStatus::InvalidRequest;
write_ack(request, report);
return ShellUiHookPollResult::Failed;
}
report.eta_chain =
!report.probe &&
!report.main_thread_guard &&
shellui_hook_is_eta_chain(request);
const ShellUiHookBackend backend = report.probe
? shellui_hook_probe_backend(report.sdk_version)
: (report.main_thread_guard
? shellui_main_thread_guard_backend(report.sdk_version)
: shellui_hook_backend(report.sdk_version, report.eta_chain));
if (backend == ShellUiHookBackend::Unsupported) {
report.status = ShellUiHookStatus::UnsupportedFirmware;
write_ack(request, report);
return ShellUiHookPollResult::Failed;
}
const std::uint32_t firmware_family =
report.sdk_version & 0xffff0000U;
const bool high_family =
firmware_family >= 0x11000000U && firmware_family <= 0x13ff0000U;
const bool fw_needs_mprotect =
(firmware_family == 0x09000000U || high_family) &&
backend == ShellUiHookBackend::Mdbg;
report.backend = fw_needs_mprotect
? "mdbg-rwx"
: (backend == ShellUiHookBackend::Mdbg ? "mdbg" : "ptrace-io");
const auto read_target = [&](void* destination, std::size_t size) {
if (backend == ShellUiHookBackend::Mdbg)
return mdbg_copyout(shellui_pid,
static_cast<intptr_t>(request.method_address), destination, size);
return pt_copyout(shellui_pid,
static_cast<intptr_t>(request.method_address), destination, size);
};
const auto write_target = [&](const std::uint8_t* bytes) {
/*
* FW 9.00 and high firmware (e.g. 13.60) can read JIT code through MDBG
* but reject a write while the JIT page is RX. For these firmware families,
* temporarily make the code page RWX while ShellUI is stopped, write the bytes,
* then restore RX before verification. No game process memory is written.
*/
const std::size_t offset =
report.main_thread_guard ? 0U : (report.eta_chain ? 6U : 0U);
const std::size_t size =
report.main_thread_guard ? 1U :
(report.eta_chain ? 8U : kShellUiHookPatchSize);
const intptr_t address =
static_cast<intptr_t>(request.method_address + offset);
if (fw_needs_mprotect) {
if (high_family &&
mdbg_copyin(shellui_pid, bytes + offset, address, size) == 0)
return 0;
const std::uint64_t page_start =
request.method_address & ~(kPs5CodePageSize - 1ULL);
const std::uint64_t page_end =
(request.method_address + size + (kPs5CodePageSize - 1ULL)) &
~(kPs5CodePageSize - 1ULL);
const std::size_t prot_len =
static_cast<std::size_t>(page_end - page_start);
if (kernel_mprotect(
shellui_pid,
static_cast<intptr_t>(page_start),
prot_len,
PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
return -1000;
}
int rc = mdbg_copyin(
shellui_pid, bytes + offset, address, size);
if (rc != 0) {
rc = pt_copyin(
shellui_pid, bytes + offset, address, size);
}
const int restore_prot_rc = kernel_mprotect(
shellui_pid,
static_cast<intptr_t>(page_start),
prot_len,
PROT_READ | PROT_EXEC);
if (restore_prot_rc != 0)
return -1001;
return rc;
}
if (backend == ShellUiHookBackend::Mdbg)
return mdbg_copyin(shellui_pid, bytes + offset, address, size);
return pt_copyin(shellui_pid, bytes + offset, address, size);
};
const pid_t self = getpid();
const std::uint64_t original_auth = kernel_get_ucred_authid(self);
if (original_auth == 0 ||
kernel_set_ucred_authid(self, kPtraceAuthId) != 0) {
report.status = ShellUiHookStatus::AuthFailure;
write_ack(request, report);
return ShellUiHookPollResult::Failed;
}
bool attached = false;
bool patch_applied = false;
std::array<std::uint8_t, kShellUiHookPatchSize> observed{};
std::array<std::uint8_t, kShellUiHookPatchSize> verified{};
if (pt_attach(shellui_pid) != 0) {
report.status = ShellUiHookStatus::AttachFailure;
} else {
attached = true;
report.read_rc = read_target(observed.data(), observed.size());
if (report.probe) {
if (report.read_rc == 0) {
report.observed = observed;
report.eta_chain =
std::memcmp(
observed.data(),
kShellUiAbsoluteJumpPrefix,
sizeof(kShellUiAbsoluteJumpPrefix)) == 0;
report.expected_matched = true;
report.verified = true;
report.status = ShellUiHookStatus::Success;
} else {
report.status = ShellUiHookStatus::ExpectedBytesMismatch;
}
} else if (report.read_rc != 0 ||
std::memcmp(
observed.data(),
request.expected,
observed.size()) != 0) {
report.status = ShellUiHookStatus::ExpectedBytesMismatch;
} else {
report.expected_matched = true;
report.write_rc = write_target(request.desired);
const int verify_rc = read_target(verified.data(), verified.size());
report.verified = verify_rc == 0 &&
std::memcmp(
verified.data(),
request.desired,
verified.size()) == 0;
if (report.verified && report.write_rc == 0) {
patch_applied = true;
report.status = ShellUiHookStatus::Success;
} else {
report.status = report.write_rc == 0
? ShellUiHookStatus::VerifyFailure
: ShellUiHookStatus::WriteFailure;
const bool still_original = verify_rc == 0 &&
std::memcmp(
verified.data(),
request.expected,
verified.size()) == 0;
if (still_original) {
report.restored = true;
} else {
const int restore_rc = write_target(request.expected);
std::array<std::uint8_t, kShellUiHookPatchSize>
restore_check{};
report.restored = restore_rc == 0 &&
read_target(restore_check.data(), restore_check.size()) == 0 &&
std::memcmp(
restore_check.data(),
request.expected,
restore_check.size()) == 0;
}
}
}
}
if (attached) {
report.detached = pt_detach(shellui_pid, 0) == 0;
(void)kill(shellui_pid, SIGCONT);
if (!report.detached && patch_applied)
report.status = ShellUiHookStatus::DetachFailure;
}
if (kernel_set_ucred_authid(self, original_auth) == 0) {
report.auth_restored =
kernel_get_ucred_authid(self) == original_auth;
}
if (!report.auth_restored && patch_applied)
report.status = ShellUiHookStatus::AuthRestoreFailure;
write_ack(request, report);
return report.status == ShellUiHookStatus::Success
? ShellUiHookPollResult::Applied
: ShellUiHookPollResult::Failed;
}
} // namespace common_fps::ps5
/*
* Common FPS for PS5
* Copyright (C) 2026 porhe911
* Modifications Copyright (C) 2026 khalifa007 (SimpleFPS)
* Modifications Copyright (C) 2026 erickdavestech
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "shellui_hook_controller.hpp"
#include "stable_sampler/proc_rw_v960.hpp"
#include <array>
#include <csignal>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/mman.h>
extern "C" {
#include <ps5/kernel.h>
#include <ps5/mdbg.h>
#include "pt.h"
}
namespace common_fps::ps5 {
namespace {
constexpr std::uint64_t kPtraceAuthId = 0x4800000000010003ULL;
constexpr std::uint64_t kPs5CodePageSize = 0x4000ULL;
template <typename T>
bool read_exact_file(const char* path, T& value) noexcept {
FILE* fp = std::fopen(path, "rb");
if (!fp)
return false;
const std::size_t count = std::fread(&value, 1, sizeof(value), fp);
const int trailing = std::fgetc(fp);
std::fclose(fp);
return count == sizeof(value) && trailing == EOF;
}
template <typename T>
bool write_exact_file_atomic(
const char* temporary_path,
const char* final_path,
const T& value) noexcept {
FILE* fp = std::fopen(temporary_path, "wb");
if (!fp)
return false;
const bool complete =
std::fwrite(&value, 1, sizeof(value), fp) == sizeof(value) &&
std::fflush(fp) == 0;
const bool closed = std::fclose(fp) == 0;
if (!complete || !closed) {
(void)unlink(temporary_path);
return false;
}
if (std::rename(temporary_path, final_path) != 0) {
(void)unlink(temporary_path);
return false;
}
return true;
}
void write_ack(
const ShellUiHookRequest& request,
const ShellUiHookPatchReport& report) noexcept {
ShellUiHookAck ack{};
ack.pid = request.pid;
ack.nonce = request.nonce;
ack.status = static_cast<std::int32_t>(report.status);
ack.read_rc = report.read_rc;
ack.write_rc = report.write_rc;
ack.verified = report.verified ? 1U : 0U;
ack.restored = report.restored ? 1U : 0U;
ack.detached = report.detached ? 1U : 0U;
ack.auth_restored = report.auth_restored ? 1U : 0U;
ack.phase = static_cast<std::uint8_t>(
report.probe
? ShellUiHookAckPhase::Probe
: (report.main_thread_guard
? ShellUiHookAckPhase::MainThreadGuard
: ShellUiHookAckPhase::Patch));
std::memcpy(
ack.observed,
report.observed.data(),
report.observed.size());
ack.checksum = shellui_hook_ack_checksum(ack);
(void)write_exact_file_atomic(
kShellUiHookAckTempPath,
kShellUiHookAckPath,
ack);
}
} // namespace
void clear_shellui_hook_protocol_files() noexcept {
(void)unlink(kShellUiHookRequestPath);
(void)unlink(kShellUiHookRequestTempPath);
(void)unlink(kShellUiHookAckPath);
(void)unlink(kShellUiHookAckTempPath);
}
ShellUiHookPollResult poll_and_apply_shellui_hook(
pid_t shellui_pid,
ShellUiHookPatchReport& report) noexcept {
report = {};
ShellUiHookRequest request{};
if (!read_exact_file(kShellUiHookRequestPath, request))
return ShellUiHookPollResult::NoRequest;
/* Consume one complete request; short files are retried on the next poll. */
(void)unlink(kShellUiHookRequestPath);
report.sdk_version = stable_sampler::firmware_sdk_version();
report.method_address = request.method_address;
report.displaced_size = request.displaced_size;
report.probe = shellui_hook_request_is_probe(request);
report.main_thread_guard =
shellui_hook_request_is_main_thread_guard(request);
const bool valid = report.probe
? shellui_hook_probe_request_is_valid(request, shellui_pid)
: (report.main_thread_guard
? shellui_main_thread_guard_request_is_valid(
request, shellui_pid)
: shellui_hook_request_is_valid(request, shellui_pid));
if (!valid) {
report.status = ShellUiHookStatus::InvalidRequest;
write_ack(request, report);
return ShellUiHookPollResult::Failed;
}
report.eta_chain =
!report.probe &&
!report.main_thread_guard &&
shellui_hook_is_eta_chain(request);
const ShellUiHookBackend backend = report.probe
? shellui_hook_probe_backend(report.sdk_version)
: (report.main_thread_guard
? shellui_main_thread_guard_backend(report.sdk_version)
: shellui_hook_backend(report.sdk_version, report.eta_chain));
if (backend == ShellUiHookBackend::Unsupported) {
report.status = ShellUiHookStatus::UnsupportedFirmware;
write_ack(request, report);
return ShellUiHookPollResult::Failed;
}
const std::uint32_t firmware_family =
report.sdk_version & 0xffff0000U;
const bool high_family =
firmware_family >= 0x11000000U && firmware_family <= 0x13ff0000U;
const bool fw_needs_mprotect =
(firmware_family == 0x09000000U || high_family) &&
backend == ShellUiHookBackend::Mdbg;
report.backend = fw_needs_mprotect
? "mdbg-rwx"
: (backend == ShellUiHookBackend::Mdbg ? "mdbg" : "ptrace-io");
const auto read_target = [&](void* destination, std::size_t size) {
if (backend == ShellUiHookBackend::Mdbg)
return mdbg_copyout(shellui_pid,
static_cast<intptr_t>(request.method_address), destination, size);
return pt_copyout(shellui_pid,
static_cast<intptr_t>(request.method_address), destination, size);
};
const auto write_target = [&](const std::uint8_t* bytes) {
/*
* FW 9.00 and high firmware (e.g. 13.60) can read JIT code through MDBG
* but reject a write while the JIT page is RX. For these firmware families,
* temporarily make the code page RWX while ShellUI is stopped, write the bytes,
* then restore RX before verification. No game process memory is written.
*/
const std::size_t offset =
report.main_thread_guard ? 0U : (report.eta_chain ? 6U : 0U);
const std::size_t size =
report.main_thread_guard ? 1U :
(report.eta_chain ? 8U : kShellUiHookPatchSize);
const intptr_t address =
static_cast<intptr_t>(request.method_address + offset);
if (fw_needs_mprotect) {
if (high_family &&
mdbg_copyin(shellui_pid, bytes + offset, address, size) == 0)
return 0;
const std::uint64_t page_start =
request.method_address & ~(kPs5CodePageSize - 1ULL);
const std::uint64_t page_end =
(request.method_address + size + (kPs5CodePageSize - 1ULL)) &
~(kPs5CodePageSize - 1ULL);
const std::size_t prot_len =
static_cast<std::size_t>(page_end - page_start);
if (kernel_mprotect(
shellui_pid,
static_cast<intptr_t>(page_start),
prot_len,
PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
return -1000;
}
int rc = mdbg_copyin(
shellui_pid, bytes + offset, address, size);
if (rc != 0) {
rc = pt_copyin(
shellui_pid, bytes + offset, address, size);
}
const int restore_prot_rc = kernel_mprotect(
shellui_pid,
static_cast<intptr_t>(page_start),
prot_len,
PROT_READ | PROT_EXEC);
if (restore_prot_rc != 0)
return -1001;
return rc;
}
if (backend == ShellUiHookBackend::Mdbg)
return mdbg_copyin(shellui_pid, bytes + offset, address, size);
return pt_copyin(shellui_pid, bytes + offset, address, size);
};
const pid_t self = getpid();
const std::uint64_t original_auth = kernel_get_ucred_authid(self);
if (original_auth == 0 ||
kernel_set_ucred_authid(self, kPtraceAuthId) != 0) {
report.status = ShellUiHookStatus::AuthFailure;
write_ack(request, report);
return ShellUiHookPollResult::Failed;
}
bool attached = false;
bool patch_applied = false;
std::array<std::uint8_t, kShellUiHookPatchSize> observed{};
std::array<std::uint8_t, kShellUiHookPatchSize> verified{};
if (pt_attach(shellui_pid) != 0) {
report.status = ShellUiHookStatus::AttachFailure;
} else {
attached = true;
report.read_rc = read_target(observed.data(), observed.size());
if (report.probe) {
if (report.read_rc == 0) {
report.observed = observed;
report.eta_chain =
std::memcmp(
observed.data(),
kShellUiAbsoluteJumpPrefix,
sizeof(kShellUiAbsoluteJumpPrefix)) == 0;
report.expected_matched = true;
report.verified = true;
report.status = ShellUiHookStatus::Success;
} else {
report.status = ShellUiHookStatus::ExpectedBytesMismatch;
}
} else if (report.read_rc != 0 ||
std::memcmp(
observed.data(),
request.expected,
observed.size()) != 0) {
report.status = ShellUiHookStatus::ExpectedBytesMismatch;
} else {
report.expected_matched = true;
report.write_rc = write_target(request.desired);
const int verify_rc = read_target(verified.data(), verified.size());
report.verified = verify_rc == 0 &&
std::memcmp(
verified.data(),
request.desired,
verified.size()) == 0;
if (report.verified && report.write_rc == 0) {
patch_applied = true;
report.status = ShellUiHookStatus::Success;
} else {
report.status = report.write_rc == 0
? ShellUiHookStatus::VerifyFailure
: ShellUiHookStatus::WriteFailure;
const bool still_original = verify_rc == 0 &&
std::memcmp(
verified.data(),
request.expected,
verified.size()) == 0;
if (still_original) {
report.restored = true;
} else {
const int restore_rc = write_target(request.expected);
std::array<std::uint8_t, kShellUiHookPatchSize>
restore_check{};
report.restored = restore_rc == 0 &&
read_target(restore_check.data(), restore_check.size()) == 0 &&
std::memcmp(
restore_check.data(),
request.expected,
restore_check.size()) == 0;
}
}
}
}
if (attached) {
report.detached = pt_detach(shellui_pid, 0) == 0;
(void)kill(shellui_pid, SIGCONT);
if (!report.detached && patch_applied)
report.status = ShellUiHookStatus::DetachFailure;
}
if (kernel_set_ucred_authid(self, original_auth) == 0) {
report.auth_restored =
kernel_get_ucred_authid(self) == original_auth;
}
if (!report.auth_restored && patch_applied)
report.status = ShellUiHookStatus::AuthRestoreFailure;
write_ack(request, report);
return report.status == ShellUiHookStatus::Success
? ShellUiHookPollResult::Applied
: ShellUiHookPollResult::Failed;
}
} // namespace common_fps::ps5
File diff suppressed because it is too large. Load diff
+45 -45
View File
@@ -1,45 +1,45 @@
/*
* Generated by tools/build_assets.py - do not edit by hand.
* Embedded artwork: "PS5 Button Icons and Controls" by Zacksly
* (https://zacksly.itch.io), licensed CC BY 3.0, modified.
* SPDX-License-Identifier: GPL-3.0-or-later AND CC-BY-3.0
*/
#pragma once
#include <stdint.h>
typedef struct { const char *file; const uint8_t *data; uint32_t size; } pad_png_t;
typedef struct { const char *name; uint32_t bit; int16_t x, y, w, h; uint8_t png; } pad_sprite_t;
typedef struct { float cx, cy, travel; int16_t w, h; uint32_t click_bit; } pad_stick_t;
typedef struct { float x, y, w, h; int16_t dot_w, dot_h; } pad_touchpad_t;
enum {
PAD_PNG_BASE,
PAD_PNG_TRIANGLE,
PAD_PNG_CIRCLE,
PAD_PNG_CROSS,
PAD_PNG_SQUARE,
PAD_PNG_DPAD_UP,
PAD_PNG_DPAD_DOWN,
PAD_PNG_DPAD_LEFT,
PAD_PNG_DPAD_RIGHT,
PAD_PNG_L1,
PAD_PNG_R1,
PAD_PNG_CREATE,
PAD_PNG_OPTIONS,
PAD_PNG_MUTE,
PAD_PNG_PS,
PAD_PNG_TOUCHPAD,
PAD_PNG_STICK_CAP,
PAD_PNG_STICK_CAP_L3,
PAD_PNG_L2_FILL,
PAD_PNG_R2_FILL,
PAD_PNG_TOUCH_DOT,
PAD_PNG_COUNT
};
extern const int pad_overlay_w, pad_overlay_h;
extern const pad_png_t pad_pngs[PAD_PNG_COUNT];
extern const pad_sprite_t pad_buttons[15];
extern const pad_sprite_t pad_triggers[2];
extern const pad_stick_t pad_sticks[2];
extern const pad_touchpad_t pad_touchpad;
/*
* Generated by tools/build_assets.py - do not edit by hand.
* Embedded artwork: "PS5 Button Icons and Controls" by Zacksly
* (https://zacksly.itch.io), licensed CC BY 3.0, modified.
* SPDX-License-Identifier: GPL-3.0-or-later AND CC-BY-3.0
*/
#pragma once
#include <stdint.h>
typedef struct { const char *file; const uint8_t *data; uint32_t size; } pad_png_t;
typedef struct { const char *name; uint32_t bit; int16_t x, y, w, h; uint8_t png; } pad_sprite_t;
typedef struct { float cx, cy, travel; int16_t w, h; uint32_t click_bit; } pad_stick_t;
typedef struct { float x, y, w, h; int16_t dot_w, dot_h; } pad_touchpad_t;
enum {
PAD_PNG_BASE,
PAD_PNG_TRIANGLE,
PAD_PNG_CIRCLE,
PAD_PNG_CROSS,
PAD_PNG_SQUARE,
PAD_PNG_DPAD_UP,
PAD_PNG_DPAD_DOWN,
PAD_PNG_DPAD_LEFT,
PAD_PNG_DPAD_RIGHT,
PAD_PNG_L1,
PAD_PNG_R1,
PAD_PNG_CREATE,
PAD_PNG_OPTIONS,
PAD_PNG_MUTE,
PAD_PNG_PS,
PAD_PNG_TOUCHPAD,
PAD_PNG_STICK_CAP,
PAD_PNG_STICK_CAP_L3,
PAD_PNG_L2_FILL,
PAD_PNG_R2_FILL,
PAD_PNG_TOUCH_DOT,
PAD_PNG_COUNT
};
extern const int pad_overlay_w, pad_overlay_h;
extern const pad_png_t pad_pngs[PAD_PNG_COUNT];
extern const pad_sprite_t pad_buttons[15];
extern const pad_sprite_t pad_triggers[2];
extern const pad_stick_t pad_sticks[2];
extern const pad_touchpad_t pad_touchpad;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+569 -569
View File
File diff suppressed because it is too large. Load diff
+41 -41
View File
@@ -1,41 +1,41 @@
/*
* Common FPS for PS5
* Copyright (C) 2026 porhe911
* Modifications Copyright (C) 2026 erickdavestech
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "common_fps/config.hpp"
#include "common_fps/types.hpp"
#include <cassert>
#include <iostream>
#include <string>
#include <type_traits>
using namespace common_fps;
int main() {
const auto cfg = default_config();
// Current public UI defaults.
assert(cfg.corner == Corner::TopLeft);
assert(cfg.font_size == 24);
// Integer-only public transport.
OverlayFrame frame;
frame.loading = false;
frame.fps = 59;
static_assert(std::is_same_v<decltype(frame.fps), int>);
// Ensure our source model never needs a decimal string.
const std::string display =
std::string("FPS: ") + std::to_string(frame.fps);
assert(display == "FPS: 59");
assert(display.find('.') == std::string::npos);
std::cout << "v1.2.0 public defaults: PASS\n";
return 0;
}
/*
* Common FPS for PS5
* Copyright (C) 2026 porhe911
* Modifications Copyright (C) 2026 erickdavestech
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "common_fps/config.hpp"
#include "common_fps/types.hpp"
#include <cassert>
#include <iostream>
#include <string>
#include <type_traits>
using namespace common_fps;
int main() {
const auto cfg = default_config();
// Current public UI defaults.
assert(cfg.corner == Corner::TopLeft);
assert(cfg.font_size == 24);
// Integer-only public transport.
OverlayFrame frame;
frame.loading = false;
frame.fps = 59;
static_assert(std::is_same_v<decltype(frame.fps), int>);
// Ensure our source model never needs a decimal string.
const std::string display =
std::string("FPS: ") + std::to_string(frame.fps);
assert(display == "FPS: 59");
assert(display.find('.') == std::string::npos);
std::cout << "v1.2.0 public defaults: PASS\n";
return 0;
}