7 Commits
Author SHA1 Message Date
erickdavestech e1de419707 Fix overlay freeze after console Rest Mode and controller reconnects (v1.0.1)
Handle sceUserServiceGetForegroundUser returning 0xffffffff with periodic non-blocking retries.

Add automatic handle reset and reconnect via scePadClose if 15 consecutive frames fail.

Track foreground user changes periodically to rebind pad upon switching accounts.

Signed-off-by: erickdavestech <54048831+erickdavestech@users.noreply.github.com>
2026-10-04 14:16:08 -04:00
erickdavestech ffcfa5d29b 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>
2026-10-03 20:52:58 -04:00
erickdavestech a94b0ff451 Add CI, Dependabot for actions and pin PS5 payload SDK v0.43
CI runs host tests, the upstream source gate, the plugin wrapper test and the PS5 build with release verification. The dependency script now installs the SDK version used to build the releases.

Signed-off-by: erickdavestech <54048831+erickdavestech@users.noreply.github.com>
2026-10-03 20:42:00 -04:00
erickdavestech f9fcff6335 Expand security policy and add code owners
Response times, coordinated disclosure and download verification in SECURITY.md; CODEOWNERS assigns reviews to the maintainer.

Signed-off-by: erickdavestech <54048831+erickdavestech@users.noreply.github.com>
2026-10-03 20:37:03 -04:00
erickdavestech 864375c939 Document USB loading and warn against running alongside Common FPS or SimpleFPS
Signed-off-by: erickdavestech <54048831+erickdavestech@users.noreply.github.com>
2026-10-03 20:32:59 -04:00
erickdavestech cf906ea645 Add overlay screenshots to the README
Game images are attributed to their owners and excluded from the project's licenses.

Signed-off-by: erickdavestech <54048831+erickdavestech@users.noreply.github.com>
2026-10-03 20:30:36 -04:00
erickdavestech 04745b9975 Simplify usage to uploading the .elf through the payload manager portal
Releases now ship the .elf only. Issue forms and contributing guide updated accordingly.

Signed-off-by: erickdavestech <54048831+erickdavestech@users.noreply.github.com>
2026-10-03 20:24:37 -04:00
32 changed files with 18129 additions and 17902 deletions

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
+1
View File
@@ -0,0 +1 @@
* @erickdavestech
+1 -10
View File
@@ -14,15 +14,6 @@ body:
placeholder: v1.0.0
validations:
required: true
- type: dropdown
id: format
attributes:
label: File used
options:
- ELF
- etaHEN plugin
validations:
required: true
- type: input
id: console
attributes:
@@ -34,7 +25,7 @@ body:
id: loader
attributes:
label: Homebrew enabler and payload loader
placeholder: kstuff-lite + ELF loader on 9021
placeholder: kstuff-lite + payload manager (web portal or USB)
validations:
required: true
- type: input
+1 -1
View File
@@ -5,4 +5,4 @@ contact_links:
about: Report security problems privately, not in a public issue.
- name: Download the latest release
url: https://github.com/erickdavestech/ps5-dualsense-overlay/releases/latest
about: Ready-to-use ELF and etaHEN plugin with checksums.
about: Ready-to-use ELF with checksums.
+8
View File
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
commit-message:
prefix: ci
+93
View File
@@ -0,0 +1,93 @@
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
host-tests:
name: Host tests
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install toolchain
run: |
sudo apt-get update
sudo apt-get install -y cmake clang-18
- name: Configure
run: cmake -S . -B build-host -DCMAKE_BUILD_TYPE=Release
env:
CC: clang-18
CXX: clang++-18
- name: Build
run: cmake --build build-host -j2
- name: Test
run: ctest --test-dir build-host --output-on-failure
- name: Upstream source gate
run: python3 tools/check_v1_source_gate.py
- name: Plugin wrapper test
run: python3 tests/test_plugin_wrapper.py
ps5-build:
name: PS5 build
runs-on: ubuntu-24.04
needs: host-tests
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install host dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
git wget unzip cmake meson pkg-config \
python3 python3-pyelftools \
clang-18 lld-18 xxd
- name: Prepare pinned dependencies
run: bash ./scripts/prepare_ps5_deps.sh
- name: Build and verify
run: bash ./scripts/ps5_source_build.sh
- name: Publish checksums
run: |
{
echo "### Build checksums"
echo '```text'
cat dist/SHA256SUMS.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload build
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ps5-dualsense-overlay-${{ github.sha }}
path: |
dist/Common_FPS_PS5_v1.2.1.elf
dist/Common_FPS_PS5_etaHEN_v1.2.1.plugin
dist/Common_FPS_ShellUI_v1.2.1.elf
dist/SHA256SUMS.txt
RESOLVED_BUILD_DEPENDENCIES.txt
if-no-files-found: error
retention-days: 30
+2
View File
@@ -22,5 +22,7 @@ __pycache__/
*.pyc
*.bak
*.bak-*
*.log
*.db
.DS_Store
Thumbs.db
+17 -6
View File
@@ -16,9 +16,10 @@ identity and the release verifier.
bash ./scripts/prepare_ps5_deps.sh
```
The script installs the PS5 payload SDK pinned by upstream (v0.41) into `/opt/ps5-payload-sdk`
(it uses `sudo`) and clones the exact etaHEN and shsrv commits listed in `DEPENDENCIES.lock.json` into
`.deps/`. If you already have an SDK installed, export `PS5_PAYLOAD_SDK` and skip the SDK step.
The script installs PS5 payload SDK v0.43 — the version used to build the releases — into
`/opt/ps5-payload-sdk` (it uses `sudo`) and clones the exact etaHEN and shsrv commits listed in
`DEPENDENCIES.lock.json` into `.deps/`. If you already have SDK v0.43 installed, export
`PS5_PAYLOAD_SDK` and skip the SDK step.
## 2. Build the payloads
@@ -64,12 +65,22 @@ Requires Pillow and Google Chrome or Chromium (headless). It regenerates `assets
`assets/layout.json` and the embedded sprite tables in `src/ps5/shellui_payload/` from the source SVGs
in `assets/source/`. `assets/preview.html` shows the result on a PC.
## Continuous integration
Every push to `main` and every pull request runs [`.github/workflows/ci.yml`](.github/workflows/ci.yml):
host tests, the upstream source gate, the plugin wrapper test and the full PS5 build with
`tools/verify_release.py`. Each run publishes the build checksums in its summary and attaches the build
as an artifact.
## Reproducing a release
Release v1.0.0 was built from its tag with ps5-payload-sdk v0.43 and the etaHEN and shsrv commits
pinned in `DEPENDENCIES.lock.json`. Building the tag with the same SDK produces a controller ELF with the
SHA-256 published in the release's `SHA256SUMS.txt`:
Release v1.0.0 was built with ps5-payload-sdk v0.43 and the etaHEN and shsrv commits pinned in
`DEPENDENCIES.lock.json`. Following the steps above from a clean checkout produces a controller ELF
identical to the published one:
```bash
sha256sum dist/Common_FPS_PS5_v1.2.1.elf
```
Compare the result with the release's `SHA256SUMS.txt`. The dependency script at the `v1.0.0` tag still
points to SDK v0.41; when building that tag, install SDK v0.43 and export `PS5_PAYLOAD_SDK` first.
+24
View File
@@ -4,6 +4,29 @@ All notable changes to this project are documented in this file. The format foll
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
[Semantic Versioning](https://semver.org/).
## [Unreleased]
## [1.0.1] - 2026-10-04
### Added
- Periodic foreground user detection (~every 2 seconds) to automatically re-bind the DualSense controller handle when switching local accounts.
- Automatic non-blocking retry (~30 frames / 0.5s) when acquiring controller handle on resume or before user login.
- Screenshots of the overlay in the README, with attribution to the games shown.
- Security policy with response times, coordinated disclosure and download verification; code owners file.
- Continuous integration: host tests, upstream source gate, plugin wrapper test and the PS5 build with release verification on every push and pull request; Dependabot updates for the pinned actions.
### Fixed
- **Overlay freeze after Rest Mode (sleep/resume):** Fixed issue where `sceUserServiceGetForegroundUser` returning `0xffffffff` (-1) during console standby or before logging into an account caused pad initialization to permanently fail, leaving the overlay visible but unresponsive.
- **Controller disconnect / standby recovery:** If `scePadReadState` fails for 15 consecutive frames (e.g. controller turned off, Bluetooth drop, or system suspend), the dead handle is now safely closed via `scePadClose` and re-acquired automatically.
### Changed
- The dependency script and `DEPENDENCIES.lock.json` pin PS5 payload SDK v0.43.
- Usage documentation simplified to loading the `.elf` with a payload manager, through its web portal or from a USB drive.
- Documented that the overlay must not run together with Common FPS or SimpleFPS.
## [1.0.0] - 2026-10-03
First public release. Based on Common FPS for PS5 v1.2.1 by porhe911 with the SimpleFPS patch by
@@ -51,4 +74,5 @@ khalifa007.
- The PS button cannot be read (intercepted by the system).
- The Mute button is disabled: the controller only reports the momentary press.
[1.0.1]: https://github.com/erickdavestech/ps5-dualsense-overlay/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/erickdavestech/ps5-dualsense-overlay/releases/tag/v1.0.0
+1 -1
View File
@@ -9,7 +9,7 @@ By participating you agree to follow the [Code of Conduct](CODE_OF_CONDUCT.md).
Open an issue with the **Bug report** template and include:
- the overlay version and whether you used the `.elf` or the `.plugin`;
- the overlay version;
- console model and exact system software version;
- homebrew enabler and payload loader;
- the game you were running;
+7
View File
@@ -28,6 +28,13 @@ each modified file names who modified it.
> "PS5 Button Icons and Controls - Zacksly
> Licensed under CC BY 3.0 - https://zacksly.itch.io"
## Screenshots
The images in `docs/screenshots/` show the overlay over third-party games and belong to their respective
owners: *Wuchang: Fallen Feathers* © 2025 505 Games, developed by Leenzee (`title-screen.webp`); the
gameplay image (`gameplay.webp`) belongs to its respective copyright holder. They are used only to
illustrate the overlay and are not covered by this project's licenses.
## PS5 homebrew projects
- **PS5 payload SDK** — John Törnblom — https://github.com/ps5-payload-dev/sdk
+4 -4
View File
@@ -1,11 +1,11 @@
{
"project": "Common FPS for PS5",
"version": "1.1.0",
"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.41",
"commit": "d2e2e58",
"version": "v0.43",
"commit": "d9c9519",
"purpose": "PS5 payload toolchain and system libraries"
},
"etahen": {
+29 -37
View File
@@ -2,6 +2,7 @@
[![License: GPL-3.0-or-later](https://img.shields.io/badge/license-GPL--3.0--or--later-blue.svg)](LICENSE)
[![Latest release](https://img.shields.io/github/v/release/erickdavestech/ps5-dualsense-overlay)](https://github.com/erickdavestech/ps5-dualsense-overlay/releases/latest)
[![CI](https://github.com/erickdavestech/ps5-dualsense-overlay/actions/workflows/ci.yml/badge.svg)](https://github.com/erickdavestech/ps5-dualsense-overlay/actions/workflows/ci.yml)
On-screen **DualSense controller overlay** for a PlayStation 5 running homebrew. A single payload
runs entirely on the console and draws the controller over the running game, lighting up buttons,
@@ -12,21 +13,21 @@ sticks, triggers and the touchpad in real time.
**[Download the latest release](https://github.com/erickdavestech/ps5-dualsense-overlay/releases/latest)**
## Screenshots
![The DualSense overlay over the title screen of Wuchang: Fallen Feathers](docs/screenshots/title-screen.webp)
![The DualSense overlay during gameplay](docs/screenshots/gameplay.webp)
<sub>Game images: <i>Wuchang: Fallen Feathers</i> © 2025 505 Games, developed by Leenzee (first image); the
second image belongs to its respective copyright holder. They are shown only to illustrate the overlay.
This project is not affiliated with or endorsed by the game publishers.</sub>
## Download
Every release contains:
| File | Use it when |
|---|---|
| `ps5-dualsense-overlay-<version>.elf` | You load payloads with an ELF loader (for example `elfldr` on port 9021). |
| `ps5-dualsense-overlay-<version>.plugin` | You use etaHEN and want the overlay loaded automatically on every boot. |
| `SHA256SUMS.txt` | Checksums to verify both files. |
Verify the download before using it:
```bash
sha256sum -c SHA256SUMS.txt
```
Get `ps5-dualsense-overlay-<version>.elf` from the
[latest release](https://github.com/erickdavestech/ps5-dualsense-overlay/releases/latest). The
release also includes `SHA256SUMS.txt` to verify the file.
## Features
@@ -48,40 +49,28 @@ sha256sum -c SHA256SUMS.txt
| PS5 | 11.xx – 13.50 | Allowed by the code, not tested |
| PS5 | 10.xx and earlier | Code paths inherited from upstream, not tested with this overlay |
Tested with kstuff-lite and the ELF loader on port 9021. Other homebrew enablers may work but have not
been tested.
Tested with kstuff-lite, loading the `.elf` through the payload manager's web portal. Other homebrew
enablers may work but have not been tested.
## Usage
1. Boot the console and start your homebrew enabler as usual.
2. Load the overlay **once**:
- **ELF** — send it to your ELF loader. From Linux, macOS or WSL:
```bash
cat ps5-dualsense-overlay-v1.0.0.elf > /dev/tcp/<PS5-IP>/9021
```
From Windows PowerShell:
```powershell
$b = [IO.File]::ReadAllBytes("ps5-dualsense-overlay-v1.0.0.elf")
$c = New-Object Net.Sockets.TcpClient("<PS5-IP>", 9021); $s = $c.GetStream()
$s.Write($b, 0, $b.Length); $s.Flush(); $c.Close()
```
- **etaHEN plugin** — copy the `.plugin` file to `/data/etaHEN/plugins/` (or to `etahen/plugins/`
on a USB drive) and restart etaHEN.
1. Jailbreak the console as usual (tested with kstuff-lite).
2. Load `ps5-dualsense-overlay-<version>.elf` with your payload manager, in either of these ways:
- **Web portal:** from a PC or phone on the same network, open the payload manager's portal using
the console's local IP and upload the `.elf`.
- **USB:** copy the `.elf` to a USB drive, connect it to the console and launch it from the payload
manager.
3. Open any game. The controller appears in the bottom-left corner.
Load only **one** copy per boot. Loading a second copy on top of a running one makes both fight over
the system UI and can end in a *System Software Error*. To update, reboot and load the new version.
Load it only **once** per boot, and do not run it together with Common FPS or SimpleFPS: they inject
into the same system process. To update, reboot and load the new version.
## Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Nothing appears | The overlay only draws while a game is in the foreground. Open a game. |
| *System Software Error* right after loading | A second copy was loaded on top of a running one. Reboot and load it once. |
| *System Software Error* right after loading | A second copy was loaded on top of a running one, or Common FPS / SimpleFPS is also running. Reboot and load only this overlay, once. |
| The PS button never lights up | Expected. The system intercepts it before any application can read it. |
| The Mute button never lights up | Expected. It is disabled, see [Known limitations](#known-limitations). |
@@ -168,7 +157,8 @@ Bug reports and pull requests are welcome. Read [CONTRIBUTING.md](CONTRIBUTING.m
- Controller artwork: **"PS5 Button Icons and Controls" by Zacksly**, licensed **CC BY 3.0**
(https://zacksly.itch.io), modified — see [`assets/source/LICENSE.txt`](assets/source/LICENSE.txt).
Every file keeps its copyright header and SPDX license identifier.
Every file keeps its copyright header and SPDX license identifier. The screenshots in
`docs/screenshots/` show third-party games and are not covered by this project's licenses.
## Legal notice
@@ -177,6 +167,8 @@ Every file keeps its copyright header and SPDX license identifier.
Interactive Entertainment.
- No Sony code, firmware, encryption keys, official SDK or game content is included. The project is
built with the open-source PS5 payload SDK.
- The screenshots show third-party games; those images remain the property of their respective owners
and are used only to illustrate this project.
- This project does not contain or distribute any exploit, jailbreak or copy-protection circumvention
tool, and it does not enable running unauthorized copies of software. It only runs on a console on
which its owner has already enabled homebrew.
+39 -7
View File
@@ -2,21 +2,53 @@
## Supported versions
Only the latest release receives fixes.
| Version | Supported |
|---|---|
| Latest release | Yes |
| Older releases | No — please update to the latest release |
## Reporting a problem
Do not open a public issue for security problems. Report them privately through GitHub:
open the repository's **Security** tab and choose **Report a vulnerability**.
Do not open a public issue, discussion or pull request for security problems. Report them privately
through GitHub: open the repository's **Security** tab and choose **Report a vulnerability**.
Please include the overlay version, console model, system software version, homebrew enabler and the
steps to reproduce. You will get an answer as soon as possible, and fixes will be credited unless you
prefer otherwise.
Please include:
- the overlay version and how it was loaded;
- console model, system software version and homebrew enabler;
- a description of the problem and its impact;
- steps to reproduce and, if possible, the renderer log (`/system_tmp/padoverlay_shellui.log`).
## Response process
- **Acknowledgement:** the maintainer aims to acknowledge every report within 7 days.
- **Assessment:** within 30 days you will be told whether the report is confirmed and, if so, the plan
to fix or mitigate it.
- **Fix and release:** confirmed problems are fixed in a new release; the advisory and the release notes
describe the impact and the affected versions.
- **Coordinated disclosure:** please keep the details private until a fix is released, or for 90 days
after the report, whichever comes first, unless a different timeline is agreed.
- **Credit:** reporters are credited in the advisory and the release notes unless they prefer to remain
anonymous.
## Scope
In scope: problems in this project's code, for example behavior that corrupts memory of a process it
should not touch, writes outside the documented paths, or crashes the system UI in a reproducible way.
Out of scope: vulnerabilities in the PS5 system software, in homebrew enablers or in payload loaders —
Out of scope: vulnerabilities in the PS5 system software, in homebrew enablers or in payload managers —
report those to their respective projects. This project does not accept or publish exploits.
## Verifying downloads
Download the overlay only from this repository's
[Releases page](https://github.com/erickdavestech/ps5-dualsense-overlay/releases). Each release
includes `SHA256SUMS.txt`; check the file before loading it:
```bash
sha256sum -c SHA256SUMS.txt
```
Release tags (`v*`) are protected against deletion and modification, commits on `main` are signed and
show as **Verified** on GitHub, and every release can be rebuilt from source to obtain the same binary
(see [BUILDING.md](BUILDING.md)).
+7
View File
@@ -117,3 +117,10 @@ and recomposed, sticks split into movable sprites, pressed states recolored, L2/
The original license text is kept at `assets/source/LICENSE.txt`. This artwork is **not** under
GPL-3.0; it remains under CC BY 3.0 and must be attributed as shown in `CREDITS.md`.
## Screenshots
The images in `docs/screenshots/` show the overlay over third-party games and remain the property of their
respective owners (*Wuchang: Fallen Feathers* © 2025 505 Games, developed by Leenzee; the gameplay image
belongs to its respective copyright holder). They are included only to illustrate this project and are
not licensed under GPL-3.0 or CC BY 3.0.
+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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

+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
+4 -4
View File
@@ -3,16 +3,16 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DEPS="${ROOT}/.deps"
SDK_ZIP="/tmp/ps5-payload-sdk-v0.41.zip"
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.41"
echo "[1/4] PS5 Payload SDK v0.43"
wget -q \
https://github.com/ps5-payload-dev/sdk/releases/download/v0.41/ps5-payload-sdk.zip \
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
@@ -41,7 +41,7 @@ git clone -q --branch v0.20 --depth 1 \
echo "[4/4] Record exact resolved revisions"
{
echo "PS5 Payload SDK: v0.41"
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"
+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;
}