5 Commits
Author SHA1 Message Date
MoHadiShibli 5a8f9e3d0e Prepare Control4Free 1.2.1 release 2026-10-11 12:21:56 +03:00
MoHadiShibli 75743ef24b Render diagnostic previews with isolated page fixtures 2026-10-10 14:04:11 +03:00
MoHadiShibli 0b3b448ca5 Decode Sony remote Enter keys in the launcher 2026-10-10 13:58:09 +03:00
MoHadiShibli ed5e828701 Build with official mirrors and the verified OpenOrbis archive
Avoid Docker Hub pull limits using pinned Docker Official Images on ECR. Fetch the same OpenOrbis release directly and verify its SHA256; retain the existing toolchain and runtime dependencies.
2026-10-10 00:39:29 +03:00
MoHadiShibli 1242d7346e Add HDMI-CEC navigation to the PS4 launcher
Read system TV-remote input independently, expose launcher actions through arrows and OK, and preserve DS4 shortcuts. Add reconnect handling, explicit Stop confirmation, diagnostics, and reader/navigation regressions.

Prepare the next patch version as a test candidate; PS4 HDMI-CEC reception still needs hardware confirmation before release.
2026-10-10 00:19:43 +03:00
26 changed files with 1431 additions and 104 deletions

No files matched your search

+1 -1
View File
@@ -1 +1 @@
ko_fi: mohadishibli
liberapay: MoHadiShibli
+17
View File
@@ -3,10 +3,27 @@
Versions follow [Semantic Versioning](https://semver.org): PATCH for fixes, MINOR for features that break
nothing, MAJOR for anything people rely on that changes.
## 1.2.1 — 2026-10-11
HDMI-CEC navigation for the PS4 app.
- Use your TV remote to control the PS4 app through HDMI-CEC. Press **OK** to open **Actions**,
use the arrows to choose, and press **OK** again. **Back** returns; Stop has a separate confirmation.
The diagnostic app also supports remote navigation and retains the last received remote key.
See [HDMI-CEC navigation](docs/hdmi-cec.md).
- If the app cannot reach PayLoader, it tells you to enable **GoldHEN → Server Settings → Enable
PayLoader Server** and retry Start. The setup message now fits on screen, and the installation
guide explains when the server is needed.
Thanks to [NotDev-Kike](https://github.com/MoHadiShibli/Control4Free/issues/3#issuecomment-6103983733)
for testing and confirming OK, arrows, Back and service startup with a Samsung TV remote on firmware 11.02.
## 1.2.0 — 2026-10-09
Gamepad mapping.
![DS4 Mapping with an example gamepad](https://raw.githubusercontent.com/MoHadiShibli/Control4Free/v1.2.0/docs/images/mapping.png)
- Map a gamepad on a DualShock 4: each control shows what drives it and lights up when pressed.
Click a control, then press the gamepad button for it. Map browser-exposed inputs from gamepads, wheels,
guitars, pedals and arcade sticks to DualShock 4 buttons, sticks and triggers. Wheel and guitar model
+9 -6
View File
@@ -6,7 +6,7 @@
<a href="https://github.com/MoHadiShibli/Control4Free/releases/latest"><img src="https://img.shields.io/github/v/release/MoHadiShibli/Control4Free?label=release" alt="Latest release"></a>
<a href="https://github.com/MoHadiShibli/Control4Free/actions/workflows/build.yml"><img src="https://github.com/MoHadiShibli/Control4Free/actions/workflows/build.yml/badge.svg" alt="Build"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-GPL--3.0-blue" alt="License: GPL v3"></a>
<a href="https://ko-fi.com/mohadishibli"><img src="https://img.shields.io/badge/Ko--fi-support%20this%20project-FF5E5B?logo=ko-fi&logoColor=white" alt="Support on Ko-fi"></a>
<a href="https://liberapay.com/MoHadiShibli/"><img src="https://img.shields.io/badge/Liberapay-support%20this%20project-F6C915?logo=liberapay&logoColor=black" alt="Support on Liberapay"></a>
</p>
**Use your phone or PC as a PS4 controller: on the home screen, at sign-in, and in games.**
@@ -48,7 +48,7 @@ sees a virtual DualShock 4, which provides:
light-bar colour, the one the PS4 or the game sets.
- **Fast.** Input reaches the console the moment it arrives, at a DualShock 4's own rate while you play.
- **An app on the PS4** that shows the address and a QR code, how many controllers are connected, and starts
or stops Control4Free.
or stops Control4Free. Use a controller or your TV remote through [HDMI-CEC](docs/hdmi-cec.md).
- **Starts by itself.** One button in the app sets up GoldHEN's AutoRun, so Control4Free is ready after
every restart.
- **On your phone's home screen**, like an app.
@@ -60,8 +60,8 @@ sees a virtual DualShock 4, which provides:
1. Download `Control4Free-<version>.pkg` from the [latest release](https://github.com/MoHadiShibli/Control4Free/releases/latest).
2. Install it with GoldHEN's Package Installer: **Settings → Debug Settings → Game → Package Installer**, from a
USB drive or `/data/pkg/`.
3. Open **Control4Free** from **Library → Applications** and press **Cross**. It sets up auto-start and
starts Control4Free.
3. Enable **GoldHEN → Server Settings → Enable PayLoader Server**, then open **Control4Free** from
**Library → Applications** and press **Cross**. It sets up auto-start and starts Control4Free.
4. On your phone or PC, scan the QR code or open the address shown, for example `http://192.168.1.20:4264`.
Pick a controller, then choose your user on the PS4.
@@ -78,6 +78,7 @@ New to GoldHEN packages? The **[installation guide](docs/installation.md)** walk
- **[Installation](docs/installation.md)**: requirements, installing, auto-start, updating, uninstalling.
- **[Playing](docs/playing.md)**: signing in, touch, keyboard and gamepads, the layout editor, settings.
- **[Gamepad mapping](docs/gamepad-mapping.md)**: profiles, calibration and using several input devices together.
- **[HDMI-CEC navigation](docs/hdmi-cec.md)**: using your TV remote in the PS4 app.
- **[Troubleshooting](docs/troubleshooting.md)**: the messages you might see and what to do about each.
- **[How it works](docs/how-it-works.md)**: the virtual-device API, sign-in, and what runs where.
- **[Protocol](docs/protocol.md)**: the WebSocket protocol between the page and the console.
@@ -87,10 +88,12 @@ New to GoldHEN packages? The **[installation guide](docs/installation.md)** walk
## Requirements and compatibility
- A PS4 with GoldHEN v2.4b18.10 or later. Tested on firmware 10.01, 13.02, 13.04 and 13.52; the
- A PS4 with GoldHEN v2.4b18.10 or later. Tested on firmware 10.01, 11.02, 13.02, 13.04 and 13.52; the
[compatibility table](docs/installation.md#compatibility) lists every firmware and the GoldHEN version it
needs.
- A phone, tablet or computer with a browser, on the same network as the PS4.
- To start the service directly from the app, enable **GoldHEN → Server Settings → Enable PayLoader
Server**. AutoRun can start it without this server.
- Turn off other GoldHEN controller plugins in games you play with Control4Free. A plugin that takes over a
signed-in user's controller can stop games from reading the virtual ones.
- A Remote Play session (chiaki-ng, for example) works alongside Control4Free.
@@ -121,7 +124,7 @@ Found a bug? Install the diagnostic package from the latest release first, then
[open an issue](https://github.com/MoHadiShibli/Control4Free/issues/new/choose) with its screenshots (see
[Reporting a problem](docs/troubleshooting.md#reporting-a-problem)). I maintain this project and read every
report. If Control4Free is useful to you, you can help keep it going on
[Ko-fi](https://ko-fi.com/mohadishibli).
[Liberapay](https://liberapay.com/MoHadiShibli/).
## Credits
+1 -1
View File
@@ -1 +1 @@
1.2.0
1.2.1
+2 -1
View File
@@ -2,7 +2,8 @@
#
# Holds clang/lld plus a pinned ps4-payload-sdk release, so a build needs nothing
# installed on the host but Docker. Matches the SDK's own CI (clang-18 / lld-18).
FROM ubuntu:24.04
# Docker's official Ubuntu mirror avoids Docker Hub's shared pull quota.
FROM public.ecr.aws/docker/library/ubuntu:24.04@sha256:534baea6a22c03a63003dbc8dbe78fe34bc0d7e595d9a9dc9834884ff530eb55
ARG SDK_VERSION=v0.9
+16 -4
View File
@@ -1,7 +1,19 @@
# Reuse the pinned OpenOrbis release from the existing plugin toolchain image.
ARG OPENORBIS_IMAGE=xfangfang/goldhen_plugin@sha256:e3d2acad69a1905472bd9d73e373d1d0fb6d3ca293d9f9a6ba957b9b2f49fffc
FROM ${OPENORBIS_IMAGE} AS openorbis
FROM ubuntu:20.04 AS pkgcompat
# The official release contains the same OpenOrbis files as the former pinned
# toolchain image. Fetch it directly so this build needs no Docker Hub pulls.
FROM control4free-build:latest AS openorbis
ARG OPENORBIS_VERSION=v0.5.3
ARG OPENORBIS_ARCHIVE=toolchain-llvm-18.2.zip
ARG OPENORBIS_SHA256=5006bb8629a40093b712fac971456fa4d5ad3fafaadb16d11c10b8a93b8b496f
RUN curl -fsSL --retry 3 -o /tmp/openorbis.zip \
"https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/releases/download/${OPENORBIS_VERSION}/${OPENORBIS_ARCHIVE}" \
&& echo "${OPENORBIS_SHA256} /tmp/openorbis.zip" | sha256sum -c - \
&& mkdir /tmp/openorbis \
&& unzip -q /tmp/openorbis.zip -d /tmp/openorbis \
&& tar -xzf /tmp/openorbis/toolchain-llvm-18.tar.gz -C /opt \
&& test -f /opt/OpenOrbis/PS4Toolchain/lib/crt1.o \
&& rm -rf /tmp/openorbis /tmp/openorbis.zip
# Docker's official Ubuntu mirror, pinned to the same image as Docker Hub.
FROM public.ecr.aws/docker/library/ubuntu:20.04@sha256:8feb4d8ca5354def3d8fce243717141ce31e2c428701f6682bd2fafe15388214 AS pkgcompat
RUN apt-get update && apt-get install -y --no-install-recommends libssl1.1 \
&& rm -rf /var/lib/apt/lists/*
FROM control4free-build:latest
+61
View File
@@ -0,0 +1,61 @@
# HDMI-CEC navigation
Use your TV remote to start and stop Control4Free, manage auto-start, and navigate the PS4 app.
The normal and diagnostic packages include an **Actions** menu for remotes.
## Set up
Enable **Settings → System → Enable HDMI Device Link** on the PS4 and HDMI-CEC on the TV.
The setting's name depends on the TV model. First check that the
remote's arrows and OK can navigate the PS4 home screen. TV and receiver/switch compatibility
varies; see [Sony's HDMI Device Link guide](https://manuals.playstation.net/document/en/ps4/settings/devicelink.html).
To start the service directly from the app, enable **GoldHEN → Server Settings → Enable
PayLoader Server**. If the app says PayLoader did not answer, enable it and retry Start.
The app cannot enable this GoldHEN setting automatically. AutoRun can start the service
after a restart without the server; see [Installation](installation.md).
## Using the remote
1. Press **OK** to open **Actions**. A Menu key can also open it if exposed by the PS4.
2. Use the arrows to choose an action, then press **OK** again. The menu includes setup/start,
auto-start, Stop and Close when those actions are available.
3. **Stop** opens a separate confirmation with **Cancel** selected. Choose **Stop**, release
OK, then press it again to confirm. Stopping disconnects every virtual controller.
4. **Back**, when the PS4 exposes it, returns from the menu or cancels confirmation. **Resume**
also returns without needing a Back button. **Close app** leaves the service running.
Existing DualShock 4 shortcuts work outside the menu. Its D-pad can also open and navigate
Actions in the normal app. Service operations finish before another operation or Close is
available. Release held buttons after opening the app, reconnecting or returning to it.
## Diagnostic app
**Left/Right** change pages and **Up/Down** scroll while the menu is
closed. **OK** opens Actions, which also includes **Capture kernel log**. Menu arrows move
the selection without changing the page underneath it. The Summary page reports DS4 and
TV-remote input status; a ready source shows its navigation button mask, and a failed source
shows the native error code. It also retains the last active remote sample's key code and raw
button mask, so a short press remains visible after you release it. These values describe
the PS4's input data; they are not HDMI-CEC wire command numbers.
If a remote fails inside the app, press and release the affected button, then photograph
Summary or save its report. See [Reporting a problem](troubleshooting.md#reporting-a-problem).
## Confirmed setup and limits
| TV | Remote | PS4 firmware | GoldHEN | Confirmed behavior |
|---|---|---|---|---|
| Samsung UN55NU7090P | AA59-00818A (TM1240) | 11.02 | v2.4b18.12 | OK, arrows, RETURN/Back and service startup with PayLoader Server enabled |
This setup was [confirmed by NotDev-Kike](https://github.com/MoHadiShibli/Control4Free/issues/3#issuecomment-6103983733)
with no physical DS4 connected. Other TV/remote models, simultaneous DS4 use, HDMI input
switching and rest/wake behavior still need hardware reports. Held-button handling, recovery
and Stop confirmation have automated tests; their hardware behavior was not separately reported.
The app reads the PS4's remote-control stream independently of DS4 input. TV keys control
the app only. Media/color keys and TV power/volume control are outside this implementation.
For another setup, report the TV and remote models, PS4 firmware, GoldHEN version, any
receiver/switch between TV and console, and which buttons work. Diagnostic screenshots help
identify inputs the PS4 exposes.
+14 -4
View File
@@ -14,6 +14,9 @@ Control4Free comes in two pieces:
run ELF payloads like Control4Free reliably, and have no auto-start. See [Compatibility](#compatibility) for
each firmware.
- **Debug Settings** turned on in GoldHEN, for the Package Installer.
- **PayLoader Server** enabled for starting the service directly from the app:
**GoldHEN → Server Settings → Enable PayLoader Server**. AutoRun and Payloader LaunchPad can start the
service without this server.
- A phone, tablet or computer with a modern browser, on the same network as the PS4.
## Compatibility
@@ -25,12 +28,13 @@ Control4Free's start-up code, but nobody has tried them yet.
| Firmware | GoldHEN | Status |
|---|---|---|
| 10.01 | v2.4b18.10 or later | **Tested** (v2.4b18.10) |
| 11.02 | v2.4b18.10 or later | **Tested** (v2.4b18.12) |
| 13.02, 13.04 | v2.4b18.12 or later | **Tested** (v2.4b18.12) |
| 13.52 | v2.4b18.11 or later | **Tested** (v2.4b18.12), with Control4Free 1.1.1 or later |
| 13.50 | v2.4b18.12 or later | Not tested |
| 13.00 | v2.4b18.10 or later | Not tested |
| 12.00, 12.02, 12.50, 12.52 | v2.4b18.10 or later | Not tested |
| 5.05, 6.71, 6.72, 7.0x, 7.5x, 8.0x, 8.5x, 9.0x, 9.5x, 9.60, 10.00, 10.50, 10.70, 10.71, 11.0x, 11.5x | v2.4b18.10 or later | Not tested |
| 5.05, 6.71, 6.72, 7.0x, 7.5x, 8.0x, 8.5x, 9.0x, 9.5x, 9.60, 10.00, 10.50, 10.70, 10.71, other 11.0x, 11.5x | v2.4b18.10 or later | Not tested |
The older a firmware, the less certain it is that the PS4's virtual-controller API behaves as on the tested
ones. If you try an untested firmware, an issue saying whether it worked helps others, with a
@@ -50,13 +54,15 @@ The app appears in **Library → Applications**.
## First start
Open **Control4Free** and press **Cross**. The app:
In GoldHEN, open **Server Settings** and check **Enable PayLoader Server**. Then open **Control4Free** and
press **Cross**. The app:
1. copies the service to `/data/payloads/control4free.elf`;
2. adds it to GoldHEN's AutoRun list, `/data/GoldHEN/payloads.ini`, so GoldHEN starts it every time it
loads;
3. starts it straight away if GoldHEN's **PayLoader** is on. If it isn't, turn PayLoader on in GoldHEN's
settings and press **Cross** again. AutoRun also starts Control4Free each time GoldHEN loads.
3. starts it straight away through GoldHEN's PayLoader Server. If the server is unavailable, auto-start
stays enabled; enable PayLoader Server and retry, or let AutoRun start Control4Free the next time
GoldHEN loads.
Once it's running, the app shows the address to open, for example `http://192.168.1.20:4264`, and a QR code
for your phone's camera. The PS4 also shows the address in a notification when the service starts.
@@ -78,6 +84,10 @@ screen, that's fine too: the app doesn't get in the way of Control4Free.
| **Square**, then **Cross** | Stops Control4Free and disconnects every controller. |
| **Circle** | Closes the app. Control4Free keeps running without it. |
With a TV remote over HDMI-CEC, press **OK** to open **Actions**, use the arrows to choose,
and press **OK** again. **Back** returns. See [HDMI-CEC navigation](hdmi-cec.md) for setup
and compatibility.
## Updating
1. Install the new package over the old one. Delete the old app first if the installer refuses.
+3 -2
View File
@@ -129,8 +129,9 @@ has loaded, and check it has a controller under **Gamepads on this device**.
## In the app on the PS4
**"PayLoader did not answer."** Turn on GoldHEN's PayLoader and press **Cross** again, or restart the PS4 and
run the jailbreak: with auto-start on, Control4Free starts by itself.
**"PayLoader did not answer."** Open **GoldHEN → Server Settings → Enable PayLoader Server**, check the
option and retry **Start** in the app, or restart the PS4 and run the jailbreak: with auto-start on,
Control4Free starts by itself.
**"Control4Free is not responding."** Wait a few seconds after the PS4 wakes from rest mode, then press
**Cross** again. If it stays stuck, restart the PS4 and run the jailbreak.
+1 -1
View File
@@ -20,7 +20,7 @@ CC := clang-18
CXX := clang++-18
LD := ld.lld-18
CFLAGS := --target=x86_64-pc-freebsd12-elf -fPIC -funwind-tables -O2 -Wall -Wextra -Werror -MMD -MP -D_DEFAULT_SOURCE -isysroot $(SDK) -isystem $(SDK)/include -I../vendor/jsmn -I../vendor/qrcodegen -I../vendor/stb -DC4F_LAUNCHER_VERSION='"$(VERSION)"' $(DIAG_FLAGS)
OBJECTS += $(OUT)/main.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/logo.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o
OBJECTS += $(OUT)/main.o $(OUT)/input.o $(OUT)/navigation.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.o $(OUT)/autorun.o $(OUT)/sandbox.o $(OUT)/logo.o $(OUT)/qrcodegen.o $(OUT)/stb_truetype.o $(OUT)/fonts.o
LIBS := -lc -lkernel -lSceVideoOut -lScePad -lSceUserService -lSceSystemService -lSceNetCtl
FONTS := ../vendor/roboto/Roboto-Light.ttf ../vendor/roboto/Roboto-Regular.ttf
.PHONY: all package
+1 -1
View File
@@ -184,7 +184,7 @@ int c4fAutorunCheck(char *problem, size_t size)
state = installedSize == c4fBundledSize && !memcmp(installed, c4fBundled, installedSize) ? C4F_AUTORUN_ON : C4F_AUTORUN_OUTDATED;
else if (errno == ENOENT) {
state = C4F_AUTORUN_OUTDATED;
snprintf(problem, size, "Enabled, but the payload is missing; Triangle repairs it");
snprintf(problem, size, "Enabled, but the payload is missing; choose Update auto-start in Actions or press Triangle");
} else {
state = C4F_AUTORUN_UNKNOWN;
snprintf(problem, size, "Enabled, but the payload cannot be inspected (errno %d)", errno);
+12 -6
View File
@@ -371,8 +371,8 @@ static char *c4fSummary(const C4fDiagState *s)
">> here and take screenshots of pages 1 and 2: page 2 lists every connection it saw.\n",
s->version, s->address[0] ? s->address : "the PS4's address with :4264");
else if (c4fStarted && c4fContains(c4fStartMessage, "PayLoader did not answer"))
c4fAdd(&t, ">> GoldHEN's PayLoader (port 9090) did not take the payload. Turn PayLoader on in GoldHEN's\n"
">> settings, then press Cross again.\n");
c4fAdd(&t, ">> GoldHEN's PayLoader Server (port 9090) did not take the payload. Open GoldHEN > Server Settings,\n"
">> check Enable PayLoader Server, then retry Start.\n");
else if (c4fStarted && c4fStartResult == -2 && !c4fLogChanged)
c4fAdd(&t, ">> The payload was handed to PayLoader, but Control4Free's own code never ran (it wrote no log).\n"
">> PayLoader did not run it, or it stopped while starting up on this firmware. The kernel log\n"
@@ -396,6 +396,7 @@ static char *c4fSummary(const C4fDiagState *s)
c4fAdd(&t, "\nThis app\n");
c4fAdd(&t, " version %s, bundled payload %s, open for %llu s\n", C4F_LAUNCHER_VERSION, c4fBundled,
(unsigned long long)((c4fLauncherTimeMs() - c4fBootMs) / 1000));
c4fAdd(&t, " input: %s\n", s->inputNote && s->inputNote[0] ? s->inputNote : "waiting");
c4fAdd(&t, "\nService (port 4264)\n");
if (s->running == 1) c4fAdd(&t, " running, version %s, %d controller(s)\n", s->version, s->controllers);
@@ -579,11 +580,12 @@ void c4fDrawDiag(uint32_t *pixels, const C4fLauncherScreen *s)
__DATE__, __TIME__, page + 1, C4F_DIAG_PAGES, c4fPageNames[page]);
c4fText(&c, C4F_FONT_REGULAR, C4F_D_TITLE, C4F_D_LEFT, 48, C4F_D_ACCENT, line);
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, 84, C4F_D_FAINT,
"L1 / R1: change page Up / Down: scroll Options: read the kernel log again "
"Cross: start Triangle: auto-start on/off Square: stop Circle: close");
"Left / Right: page Up / Down: scroll TV remote OK: actions "
"Cross: start Triangle: auto-start Square: stop Circle: close");
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, 114, C4F_D_WARN,
"Please take a screenshot of EVERY page (hold SHARE, or press SHARE then Triangle) and send them all.");
if (s->confirmStop) snprintf(line, sizeof(line), "Press Cross to stop Control4Free, or Circle to cancel.");
if (s->confirmStop) snprintf(line, sizeof(line), "%s", s->confirmMenu ?
"Select Cancel or Stop with the arrows, then press OK." : "Press Cross to stop Control4Free, or Circle to cancel.");
else snprintf(line, sizeof(line), "%s%s", s->busy ? "WORKING: " : "Now: ", s->message);
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, 146, C4F_D_TEXT, line);
c4fFillRect(&c, C4F_D_LEFT, 160, (int)width, 1, C4F_D_LINE);
@@ -591,7 +593,7 @@ void c4fDrawDiag(uint32_t *pixels, const C4fLauncherScreen *s)
pthread_mutex_lock(&c4fLock);
char *text = strdup(c4fPages[page] ? c4fPages[page] : "(not read yet)");
pthread_mutex_unlock(&c4fLock);
if (!text) return;
if (!text) { c4fDrawActionOverlay(pixels, s); return; }
/* Wrap every line, then show the window the scroll position picks. */
int count = 0, capacity = 256;
@@ -626,8 +628,12 @@ void c4fDrawDiag(uint32_t *pixels, const C4fLauncherScreen *s)
}
snprintf(line, sizeof(line), "lines %d-%d of %d", count ? first + 1 : 0, first + visible < count ? first + visible : count, count);
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_D_LEFT, 1072, C4F_D_FAINT, line);
const char *remoteHint = "TV remote: OK actions / Back returns L1 / R1: page Options: kernel log";
c4fText(&c, C4F_FONT_REGULAR, C4F_D_SIZE, C4F_SCREEN_WIDTH - C4F_D_LEFT -
c4fTextWidth(C4F_FONT_REGULAR, C4F_D_SIZE, remoteHint), 1072, C4F_D_FAINT, remoteHint);
free(rows);
free(text);
c4fDrawActionOverlay(pixels, s);
}
int c4fDiagMaxScroll(int page)
+1 -1
View File
@@ -15,7 +15,7 @@ extern "C" {
typedef struct {
int running, controllers, autorun, locked, busy, confirmStop;
const char *version, *problem, *message, *address, *autorunNote;
const char *version, *problem, *message, *address, *autorunNote, *inputNote;
} C4fDiagState;
/* Records what is known at start-up and reads the kernel log's backlog. */
+98
View File
@@ -0,0 +1,98 @@
#include "input.h"
#include <string.h>
uint32_t c4fInputRemoteButtons(uint32_t rawButtons, uint8_t keyCode)
{
/* PS5 SDL reads this Sony key table alongside ordinary pad buttons:
* https://github.com/ps5-payload-dev/SDL/blob/ee4c47dc0d617b3bc8f35108f9956baf228a1322/src/video/ps5/SDL_ps5remote.c
* Applying these navigation codes to PS4 is a hardware-test candidate;
* they are not the HDMI-CEC wire command numbers. */
if (rawButtons & C4F_INPUT_BUTTON_INTERCEPTED) return rawButtons;
switch (keyCode) {
case 13: return rawButtons | C4F_INPUT_BUTTON_CROSS;
case 15: return rawButtons | C4F_INPUT_BUTTON_CIRCLE;
case 18: return rawButtons | C4F_INPUT_BUTTON_OPTIONS;
default: return rawButtons;
}
}
static void c4fInputRelease(C4fInput *input, C4fInputSource *source, uint64_t retryAt)
{
if (source->handle >= 0 && source->owned && input->ops.close)
input->ops.close(input->ops.context, source->handle);
source->handle = -1;
source->owned = 0;
source->ready = 0;
source->user = -1;
source->previous = 0;
source->retryAt = retryAt;
}
void c4fInputInit(C4fInput *input, const C4fInputOps *ops)
{
memset(input, 0, sizeof(*input));
if (ops) input->ops = *ops;
for (int i = 0; i < C4F_INPUT_SOURCE_COUNT; i++) {
input->sources[i].handle = -1;
input->sources[i].user = -1;
}
}
static void c4fInputRead(C4fInput *input, int index, uint64_t now, C4fInputSample *sample)
{
C4fInputSource *source = &input->sources[index];
if (!input->ops.open || !input->ops.read) return;
if (source->handle < 0) {
if (now < source->retryAt) return;
int32_t user = C4F_INPUT_SYSTEM_USER;
if (index == C4F_INPUT_STANDARD &&
(!input->ops.initialUser || input->ops.initialUser(input->ops.context, &user) != 0 || user < 0)) {
source->retryAt = now + C4F_INPUT_RETRY_MS;
return;
}
int owned = 0;
int port = index == C4F_INPUT_STANDARD ? C4F_INPUT_PORT_STANDARD : C4F_INPUT_PORT_REMOTE;
int handle = input->ops.open(input->ops.context, user, port, &owned);
if (handle < 0) {
source->retryAt = now + C4F_INPUT_RETRY_MS;
return;
}
source->handle = handle;
source->owned = !!owned;
source->user = user;
source->ready = 0;
source->previous = 0;
}
uint32_t buttons = 0;
int connected = 0;
if (input->ops.read(input->ops.context, source->handle, &buttons, &connected) != 0 || !connected) {
c4fInputRelease(input, source, now + C4F_INPUT_RETRY_MS);
return;
}
sample->available = 1;
if (!source->ready) {
if (!buttons) source->ready = 1;
return;
}
sample->buttons = buttons;
sample->pressed = buttons & ~source->previous;
source->previous = buttons;
}
void c4fInputPoll(C4fInput *input, uint64_t now, C4fInputFrame *frame)
{
memset(frame, 0, sizeof(*frame));
c4fInputRead(input, C4F_INPUT_STANDARD, now, &frame->standard);
c4fInputRead(input, C4F_INPUT_REMOTE, now, &frame->remote);
}
void c4fInputReset(C4fInput *input, uint64_t now)
{
for (int i = 0; i < C4F_INPUT_SOURCE_COUNT; i++)
c4fInputRelease(input, &input->sources[i], now);
}
void c4fInputClose(C4fInput *input)
{
c4fInputReset(input, 0);
}
+75
View File
@@ -0,0 +1,75 @@
#ifndef C4F_LAUNCHER_INPUT_H
#define C4F_LAUNCHER_INPUT_H
#include <stdint.h>
/* These sources use the native pad API independently. Remote-control input
* belongs to the system user, rather than the launcher's signed-in user. */
enum { C4F_INPUT_STANDARD, C4F_INPUT_REMOTE, C4F_INPUT_SOURCE_COUNT };
enum { C4F_INPUT_PORT_STANDARD = 0, C4F_INPUT_PORT_REMOTE = 16 };
#define C4F_INPUT_SYSTEM_USER 0xff
#define C4F_INPUT_RETRY_MS 1000
/* Native read-side buttons used by launcher navigation. */
#define C4F_INPUT_BUTTON_OPTIONS UINT32_C(0x000008)
#define C4F_INPUT_BUTTON_UP UINT32_C(0x000010)
#define C4F_INPUT_BUTTON_RIGHT UINT32_C(0x000020)
#define C4F_INPUT_BUTTON_DOWN UINT32_C(0x000040)
#define C4F_INPUT_BUTTON_LEFT UINT32_C(0x000080)
#define C4F_INPUT_BUTTON_L1 UINT32_C(0x000400)
#define C4F_INPUT_BUTTON_R1 UINT32_C(0x000800)
#define C4F_INPUT_BUTTON_TRIANGLE UINT32_C(0x001000)
#define C4F_INPUT_BUTTON_CIRCLE UINT32_C(0x002000)
#define C4F_INPUT_BUTTON_CROSS UINT32_C(0x004000)
#define C4F_INPUT_BUTTON_SQUARE UINT32_C(0x008000)
/* PS4 reverse definitions identify bit31 as system-intercepted input. This is
* a read-side flag, not an additional virtual-controller button. */
#define C4F_INPUT_BUTTON_INTERCEPTED UINT32_C(0x80000000)
typedef struct {
void *context;
int (*initialUser)(void *context, int32_t *user);
/* Returns a handle, or a negative error. owned is true only for handles
* opened here; an already-open handle may be borrowed and must not close. */
int (*open)(void *context, int32_t user, int port, int *owned);
int (*read)(void *context, int handle, uint32_t *buttons, int *connected);
void (*close)(void *context, int handle);
} C4fInputOps;
typedef struct {
int handle, owned, ready;
int32_t user;
uint32_t previous;
uint64_t retryAt;
} C4fInputSource;
typedef struct {
C4fInputOps ops;
C4fInputSource sources[C4F_INPUT_SOURCE_COUNT];
} C4fInput;
typedef struct {
uint32_t buttons, pressed;
int available;
} C4fInputSample;
typedef struct {
C4fInputSample standard, remote;
} C4fInputFrame;
#ifdef __cplusplus
extern "C" {
#endif
void c4fInputInit(C4fInput *input, const C4fInputOps *ops);
/* Adds navigation keys reported separately by the system remote-control
* stream, preserving the ordinary button mask and unknown key codes. */
uint32_t c4fInputRemoteButtons(uint32_t rawButtons, uint8_t keyCode);
/* Each recovered source requires a neutral sample before generating edges.
* Failure or disconnect produces neutral input and a bounded retry. */
void c4fInputPoll(C4fInput *input, uint64_t now, C4fInputFrame *frame);
/* Focus changes may reset sources; durable launcher state is unaffected. */
void c4fInputReset(C4fInput *input, uint64_t now);
void c4fInputClose(C4fInput *input);
#ifdef __cplusplus
}
#endif
#endif
+160 -36
View File
@@ -1,6 +1,7 @@
/* Native PS4 launcher. The controller service runs outside this application. */
#include <errno.h>
#include <signal.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -13,6 +14,8 @@
#include <orbis/UserService.h>
#include <orbis/VideoOut.h>
#include "autorun.h"
#include "input.h"
#include "navigation.h"
#include "sandbox.h"
#include "screen.h"
#include "service.h"
@@ -40,7 +43,7 @@ static int c4fSetUp(char *message, size_t size)
result = c4fLauncherStart(payload, payloadSize, started, sizeof(started));
if (result == 0) snprintf(message, size, "Set up and running. GoldHEN will also start it after each restart.");
else if (result == -2) snprintf(message, size, "%s", started);
else snprintf(message, size, "Auto-start is on: Control4Free starts the next time GoldHEN loads. %s", started);
else snprintf(message, size, "Auto-start is on. %s", started);
return result == -2 ? -2 : 0;
}
@@ -111,7 +114,7 @@ static void *c4fWorker(void *)
C4fLauncherScreen copy = c4fScreen;
pthread_mutex_unlock(&c4fMutex);
C4fDiagState diag = { running, status.controllers, autorun, copy.locked, copy.busy, copy.confirmStop,
status.version, problem, copy.message, copy.address, copy.autorunNote };
status.version, problem, copy.message, copy.address, copy.autorunNote, copy.inputNote };
unsigned generation = c4fDiagRefresh(&diag);
pthread_mutex_lock(&c4fMutex);
c4fScreen.diagGeneration = generation;
@@ -166,6 +169,115 @@ static void c4fCloseVideo(void)
if (c4fVideoSize) sceKernelReleaseDirectMemory(c4fVideoOffset, c4fVideoSize);
}
/* The pinned SDK omits this error from Pad.h. A handle already opened by the
* application can be borrowed, but must not be closed by our reader. */
#define C4F_PAD_ERROR_ALREADY_OPENED 0x80920004u
static_assert(C4F_INPUT_SYSTEM_USER == ORBIS_USER_SERVICE_USER_ID_SYSTEM, "system input user");
static_assert(C4F_INPUT_PORT_STANDARD == ORBIS_PAD_PORT_TYPE_STANDARD, "standard input port");
static_assert(C4F_INPUT_BUTTON_UP == ORBIS_PAD_BUTTON_UP, "Up button");
static_assert(C4F_INPUT_BUTTON_RIGHT == ORBIS_PAD_BUTTON_RIGHT, "Right button");
static_assert(C4F_INPUT_BUTTON_DOWN == ORBIS_PAD_BUTTON_DOWN, "Down button");
static_assert(C4F_INPUT_BUTTON_LEFT == ORBIS_PAD_BUTTON_LEFT, "Left button");
static_assert(C4F_INPUT_BUTTON_CROSS == ORBIS_PAD_BUTTON_CROSS, "OK button");
static_assert(C4F_INPUT_BUTTON_CIRCLE == ORBIS_PAD_BUTTON_CIRCLE, "Back button");
static_assert(C4F_INPUT_BUTTON_OPTIONS == ORBIS_PAD_BUTTON_OPTIONS, "Menu button");
static_assert(offsetof(OrbisPadData, unknown) + 3 == 108, "remote-control key byte");
enum { C4F_INPUT_WAITING, C4F_INPUT_READY, C4F_INPUT_DISCONNECTED,
C4F_INPUT_OPEN_FAILED, C4F_INPUT_READ_FAILED, C4F_INPUT_USER_FAILED };
typedef struct {
int initialized, handles[2], state[2];
uint32_t error[2], buttons[2];
uint32_t remoteLastRawButtons;
uint8_t remoteLastKey;
} C4fNativeInput;
static const char *const c4fInputNames[] = { "DS4", "TV remote" };
static const char *const c4fInputStates[] = {
"waiting", "ready", "disconnected", "open failed", "read failed", "user unavailable"
};
static void c4fNativeInputState(C4fNativeInput *input, int source, int state, int result)
{
if (input->state[source] != state || input->error[source] != (uint32_t)result)
printf("[c4f-launcher] %s input: %s (0x%08x)\n",
c4fInputNames[source], c4fInputStates[state], (unsigned)result);
input->state[source] = state;
input->error[source] = (uint32_t)result;
if (state != C4F_INPUT_READY) input->buttons[source] = 0;
}
static int c4fNativeInitialUser(void *context, int32_t *user)
{
int result = sceUserServiceGetInitialUser(user);
if (result) c4fNativeInputState((C4fNativeInput *)context, 0, C4F_INPUT_USER_FAILED, result);
return result;
}
static int c4fNativeOpen(void *context, int32_t user, int port, int *owned)
{
C4fNativeInput *input = (C4fNativeInput *)context;
int source = port == ORBIS_PAD_PORT_TYPE_STANDARD ? 0 : 1;
*owned = 0;
if (!input->initialized) {
int result = scePadInit();
if (result) {
c4fNativeInputState(input, source, C4F_INPUT_OPEN_FAILED, result);
return -1;
}
input->initialized = 1;
}
int handle = scePadOpen(user, port, 0, NULL);
if ((uint32_t)handle == C4F_PAD_ERROR_ALREADY_OPENED)
handle = scePadGetHandle(user, port, 0);
else if (handle >= 0)
*owned = 1;
input->handles[source] = handle;
if (handle < 0) c4fNativeInputState(input, source, C4F_INPUT_OPEN_FAILED, handle);
return handle;
}
static int c4fNativeRead(void *context, int handle, uint32_t *buttons, int *connected)
{
C4fNativeInput *input = (C4fNativeInput *)context;
int source = handle == input->handles[0] ? 0 : 1;
OrbisPadData sample;
memset(&sample, 0, sizeof(sample));
int result = scePadReadState(handle, &sample);
uint32_t mappedButtons = sample.buttons;
if (source == C4F_INPUT_REMOTE && result == 0) {
/* The SDK's byte108 is deviceUniqueData[0] in the reverse PS4 layout.
* PS5 implementations use it for remote Enter/Back/Menu even when the
* device-data length is zero. PS4 key decoding still needs hardware
* confirmation. */
uint8_t keyCode = sample.unknown[3];
if (keyCode || sample.buttons) {
input->remoteLastKey = keyCode;
input->remoteLastRawButtons = sample.buttons;
}
mappedButtons = c4fInputRemoteButtons(sample.buttons, keyCode);
}
*buttons = result == 0 && sample.connected ? mappedButtons : 0;
*connected = result == 0 && sample.connected;
c4fNativeInputState(input, source, result ? C4F_INPUT_READ_FAILED :
sample.connected ? C4F_INPUT_READY : C4F_INPUT_DISCONNECTED, result);
input->buttons[source] = *buttons;
return result;
}
static void c4fNativeClose(void *, int handle) { scePadClose(handle); }
static void c4fNativeInputNote(const C4fNativeInput *input, char *note, size_t size)
{
snprintf(note, size, "%s: %s (0x%08x); %s: %s (0x%08x), last key 0x%02x / raw 0x%08x",
c4fInputNames[0], c4fInputStates[input->state[0]],
input->error[0] ? input->error[0] : input->buttons[0],
c4fInputNames[1], c4fInputStates[input->state[1]],
input->error[1] ? input->error[1] : input->buttons[1],
(unsigned)input->remoteLastKey, input->remoteLastRawButtons);
}
int main(void)
{
const size_t frameBytes = (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(uint32_t);
@@ -184,9 +296,11 @@ int main(void)
OrbisUserServiceInitializeParams params = {};
params.priority = ORBIS_KERNEL_PRIO_FIFO_LOWEST;
sceUserServiceInitialize(&params);
int32_t user = -1, pad = -1;
if (scePadInit() == 0 && sceUserServiceGetInitialUser(&user) == 0)
pad = scePadOpen(user, ORBIS_PAD_PORT_TYPE_STANDARD, 0, NULL);
C4fNativeInput nativeInput = {};
nativeInput.handles[0] = nativeInput.handles[1] = -1;
C4fInputOps inputOps = { &nativeInput, c4fNativeInitialUser, c4fNativeOpen, c4fNativeRead, c4fNativeClose };
C4fInput inputs;
c4fInputInit(&inputs, &inputOps);
c4fScreen.running = -1; c4fScreen.busy = 1; c4fScreen.autorun = C4F_AUTORUN_UNKNOWN;
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Checking Control4Free...");
/* Read before leaving the sandbox: /app0 is only visible from inside it. */
@@ -201,50 +315,60 @@ int main(void)
#endif
pthread_t worker;
int workerStarted = pthread_create(&worker, NULL, c4fWorker, NULL) == 0;
if (!workerStarted || pad < 0) {
if (!workerStarted) {
pthread_mutex_lock(&c4fMutex);
c4fScreen.locked = 1;
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Could not initialize launcher. Close with PS and retry.");
pthread_mutex_unlock(&c4fMutex);
}
sceSystemServiceHideSplashScreen();
uint32_t previous = 0;
uint64_t lastInputAt = c4fLauncherTimeMs();
int buffer = 0, done = 0, drawn = 0, shown[2] = { 0, 0 };
int64_t frame = 1;
C4fLauncherScreen drawnState;
memset(&drawnState, 0, sizeof(drawnState));
while (!done) {
OrbisPadData input;
memset(&input, 0, sizeof(input));
uint32_t buttons = pad >= 0 && scePadReadState(pad, &input) == 0 && input.connected ? input.buttons : 0;
uint32_t pressed = buttons & ~previous;
previous = buttons;
uint64_t now = c4fLauncherTimeMs();
/* A suspended app must not resume a pending confirmation or reuse a
* held navigation button. Do not guess unreversed focus API signatures. */
int resumed = now > lastInputAt && now - lastInputAt > 1000;
if (resumed) c4fInputReset(&inputs, now);
lastInputAt = now;
C4fInputFrame input;
c4fInputPoll(&inputs, now, &input);
pthread_mutex_lock(&c4fMutex);
if (!c4fScreen.busy) {
if (pressed & ORBIS_PAD_BUTTON_CIRCLE) {
if (c4fScreen.confirmStop) c4fScreen.confirmStop = 0;
else done = 1;
} else if (pressed & ORBIS_PAD_BUTTON_CROSS) {
if (c4fScreen.confirmStop) {
c4fCommand = C4F_DO_STOP; c4fScreen.confirmStop = 0; c4fScreen.busy = 1;
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Stopping and releasing controllers...");
} else if (!c4fScreen.locked && c4fScreen.running != 1) {
/* Until auto-start is on, Cross sets it up and starts it. */
int setUp = c4fScreen.autorun != C4F_AUTORUN_ON;
c4fCommand = setUp ? C4F_DO_SET_UP : C4F_DO_START; c4fScreen.busy = 1;
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
setUp ? "Setting up Control4Free. Please wait..." : "Starting Control4Free. Please wait...");
}
} else if ((pressed & ORBIS_PAD_BUTTON_TRIANGLE) && !c4fScreen.confirmStop &&
c4fScreen.autorun != C4F_AUTORUN_UNKNOWN) {
int off = c4fScreen.autorun == C4F_AUTORUN_ON;
c4fCommand = off ? C4F_DO_AUTORUN_OFF : C4F_DO_AUTORUN_ON; c4fScreen.busy = 1;
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
off ? "Turning auto-start off..." : "Turning auto-start on...");
} else if ((pressed & ORBIS_PAD_BUTTON_SQUARE) && c4fScreen.running == 1) c4fScreen.confirmStop = 1;
c4fNativeInputNote(&nativeInput, c4fScreen.inputNote, sizeof(c4fScreen.inputNote));
if (resumed) {
c4fScreen.actionMenu = c4fScreen.confirmStop = c4fScreen.confirmMenu = 0;
c4fScreen.actionFocus = C4F_ACTION_NONE;
}
uint32_t standardUnhandled = 0, remoteUnhandled = 0;
#ifdef C4F_DIAG
const int diagnostic = 1;
#else
const int diagnostic = 0;
#endif
C4fLauncherAction action = c4fNavigate(&c4fScreen, input.standard.pressed, input.remote.pressed,
diagnostic, &standardUnhandled, &remoteUnhandled);
if (action == C4F_ACTION_CLOSE) done = 1;
else if (action == C4F_ACTION_START) {
/* Until auto-start is on, Start sets it up and starts it. */
int setUp = c4fScreen.autorun != C4F_AUTORUN_ON;
c4fCommand = setUp ? C4F_DO_SET_UP : C4F_DO_START; c4fScreen.busy = 1;
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
setUp ? "Setting up Control4Free. Please wait..." : "Starting Control4Free. Please wait...");
} else if (action == C4F_ACTION_STOP) {
c4fCommand = C4F_DO_STOP; c4fScreen.busy = 1;
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Stopping and releasing controllers...");
} else if (action == C4F_ACTION_AUTORUN) {
int off = c4fScreen.autorun == C4F_AUTORUN_ON;
c4fCommand = off ? C4F_DO_AUTORUN_OFF : C4F_DO_AUTORUN_ON; c4fScreen.busy = 1;
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
off ? "Turning auto-start off..." : "Turning auto-start on...");
}
#ifdef C4F_DIAG
/* Pages and scrolling work while busy too. */
uint32_t pressed = standardUnhandled | remoteUnhandled;
int page = c4fScreen.diagPage;
if (pressed & (ORBIS_PAD_BUTTON_R1 | ORBIS_PAD_BUTTON_RIGHT)) c4fScreen.diagPage = (page + 1) % C4F_DIAG_PAGES;
if (pressed & (ORBIS_PAD_BUTTON_L1 | ORBIS_PAD_BUTTON_LEFT)) c4fScreen.diagPage = (page + C4F_DIAG_PAGES - 1) % C4F_DIAG_PAGES;
@@ -254,7 +378,7 @@ int main(void)
at += pressed & ORBIS_PAD_BUTTON_UP ? -10 : 10;
c4fScreen.diagScroll[page] = at >= max ? C4F_DIAG_END : at < 0 ? 0 : at;
}
if (pressed & ORBIS_PAD_BUTTON_OPTIONS) c4fDiagCapture(4000);
if (action == C4F_ACTION_CAPTURE || (pressed & ORBIS_PAD_BUTTON_OPTIONS)) c4fDiagCapture(4000);
#endif
C4fLauncherScreen snapshot = c4fScreen;
pthread_mutex_unlock(&c4fMutex);
@@ -286,7 +410,7 @@ int main(void)
#ifdef C4F_DIAG
c4fDiagStop();
#endif
if (pad >= 0) scePadClose(pad);
c4fInputClose(&inputs);
sceNetCtlTerm();
c4fCloseVideo();
free(canvas);
+178
View File
@@ -0,0 +1,178 @@
#include "navigation.h"
#include "autorun.h"
#include "input.h"
#define C4F_NAV_BACK C4F_INPUT_BUTTON_CIRCLE
#define C4F_NAV_OK C4F_INPUT_BUTTON_CROSS
#define C4F_NAV_PREVIOUS (C4F_INPUT_BUTTON_UP | C4F_INPUT_BUTTON_LEFT)
#define C4F_NAV_NEXT (C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_RIGHT)
#define C4F_NAV_DIRECTIONS (C4F_NAV_PREVIOUS | C4F_NAV_NEXT)
int c4fActionAvailable(const C4fLauncherScreen *screen, C4fLauncherAction action, int diagnostic)
{
switch (action) {
case C4F_ACTION_RESUME: return 1;
case C4F_ACTION_START: return !screen->busy && !screen->locked && screen->running != 1;
case C4F_ACTION_AUTORUN:
return !screen->busy && !screen->locked && screen->autorun != C4F_AUTORUN_UNKNOWN;
case C4F_ACTION_STOP: return !screen->busy && !screen->locked && screen->running == 1;
case C4F_ACTION_CAPTURE: return !!diagnostic;
case C4F_ACTION_CLOSE: return !screen->busy;
default: return 0;
}
}
int c4fActionList(const C4fLauncherScreen *screen, int diagnostic,
C4fLauncherAction *actions, int capacity)
{
const C4fLauncherAction order[] = { C4F_ACTION_RESUME, C4F_ACTION_START,
C4F_ACTION_AUTORUN, C4F_ACTION_STOP, C4F_ACTION_CAPTURE, C4F_ACTION_CLOSE };
int count = 0;
for (unsigned i = 0; i < sizeof(order) / sizeof(order[0]); i++) {
if (!c4fActionAvailable(screen, order[i], diagnostic)) continue;
if (actions && count < capacity) actions[count] = order[i];
count++;
}
return count;
}
const char *c4fActionLabel(const C4fLauncherScreen *screen, C4fLauncherAction action)
{
switch (action) {
case C4F_ACTION_RESUME: return "Resume";
case C4F_ACTION_START: return screen->autorun == C4F_AUTORUN_ON ? "Start Control4Free" : "Set up and start";
case C4F_ACTION_AUTORUN:
if (screen->autorun == C4F_AUTORUN_ON) return "Turn auto-start off";
if (screen->autorun == C4F_AUTORUN_OUTDATED) return "Update auto-start";
return "Turn auto-start on";
case C4F_ACTION_STOP: return "Stop Control4Free";
case C4F_ACTION_CLOSE: return "Close app";
case C4F_ACTION_CAPTURE: return "Capture kernel log";
default: return "";
}
}
static C4fLauncherAction c4fDefaultAction(const C4fLauncherScreen *screen, int diagnostic)
{
return c4fActionAvailable(screen, C4F_ACTION_START, diagnostic) ? C4F_ACTION_START : C4F_ACTION_RESUME;
}
static int c4fDirection(uint32_t buttons)
{
int previous = !!(buttons & C4F_NAV_PREVIOUS), next = !!(buttons & C4F_NAV_NEXT);
return previous == next ? 0 : next ? 1 : -1;
}
static void c4fConsume(uint32_t *standardUnhandled, uint32_t *remoteUnhandled)
{
if (standardUnhandled) *standardUnhandled = 0;
if (remoteUnhandled) *remoteUnhandled = 0;
}
static void c4fCancelConfirmation(C4fLauncherScreen *screen)
{
screen->confirmStop = 0;
screen->confirmChoice = 0;
screen->confirmMenu = 0;
}
C4fLauncherAction c4fNavigate(C4fLauncherScreen *screen, uint32_t standardPressed,
uint32_t remotePressed, int diagnostic,
uint32_t *standardUnhandled, uint32_t *remoteUnhandled)
{
if (standardUnhandled) *standardUnhandled = standardPressed;
if (remoteUnhandled) *remoteUnhandled = remotePressed;
uint32_t pressed = standardPressed | remotePressed;
if (screen->confirmStop) {
c4fConsume(standardUnhandled, remoteUnhandled);
if (!c4fActionAvailable(screen, C4F_ACTION_STOP, diagnostic)) {
c4fCancelConfirmation(screen);
return C4F_ACTION_NONE;
}
if (pressed & C4F_NAV_BACK) {
c4fCancelConfirmation(screen);
return C4F_ACTION_RESUME;
}
/* The DS4 shortcut displays Cross-to-stop. A remote's first event
* instead reveals an explicit, Cancel-focused selection and cannot
* accept the confirmation that another source opened. */
if (!screen->confirmMenu && remotePressed) {
screen->confirmMenu = 1;
screen->confirmChoice = 0;
return C4F_ACTION_NONE;
}
if (pressed & C4F_NAV_DIRECTIONS) {
if (!screen->confirmMenu) {
screen->confirmMenu = 1;
screen->confirmChoice = 0;
}
int direction = c4fDirection(pressed);
if (direction) screen->confirmChoice = direction > 0;
return C4F_ACTION_NONE;
}
if (pressed & C4F_NAV_OK) {
int stop = screen->confirmChoice;
c4fCancelConfirmation(screen);
return stop ? C4F_ACTION_STOP : C4F_ACTION_RESUME;
}
return C4F_ACTION_NONE;
}
if (screen->actionMenu) {
c4fConsume(standardUnhandled, remoteUnhandled);
if (!c4fActionAvailable(screen, (C4fLauncherAction)screen->actionFocus, diagnostic)) {
screen->actionFocus = c4fDefaultAction(screen, diagnostic);
return C4F_ACTION_NONE;
}
if (pressed & C4F_NAV_BACK) {
screen->actionMenu = 0;
return C4F_ACTION_RESUME;
}
if (pressed & C4F_NAV_DIRECTIONS) {
int direction = c4fDirection(pressed);
if (direction) {
C4fLauncherAction actions[6];
int count = c4fActionList(screen, diagnostic, actions, 6), at = 0;
while (at < count && (int)actions[at] != screen->actionFocus) at++;
screen->actionFocus = actions[(at + count + direction) % count];
}
return C4F_ACTION_NONE;
}
if (pressed & C4F_NAV_OK) {
C4fLauncherAction action = (C4fLauncherAction)screen->actionFocus;
screen->actionMenu = 0;
if (action == C4F_ACTION_STOP) {
screen->confirmStop = 1;
screen->confirmChoice = 0;
screen->confirmMenu = 1;
return C4F_ACTION_NONE;
}
return action;
}
return C4F_ACTION_NONE;
}
/* A TV remote needs an action menu because it has no Triangle or Square.
* In diagnostic mode its arrows keep the existing paging and scrolling. */
if ((remotePressed & (C4F_NAV_OK | C4F_INPUT_BUTTON_OPTIONS)) ||
(!diagnostic && (pressed & C4F_NAV_DIRECTIONS))) {
c4fConsume(standardUnhandled, remoteUnhandled);
screen->actionMenu = 1;
screen->actionFocus = c4fDefaultAction(screen, diagnostic);
return C4F_ACTION_NONE;
}
if (screen->busy) return C4F_ACTION_NONE;
if (pressed & C4F_NAV_BACK) {
c4fConsume(standardUnhandled, remoteUnhandled);
return C4F_ACTION_CLOSE;
}
if (standardPressed & C4F_NAV_OK)
return c4fActionAvailable(screen, C4F_ACTION_START, diagnostic) ? C4F_ACTION_START : C4F_ACTION_NONE;
if ((standardPressed & C4F_INPUT_BUTTON_TRIANGLE) &&
c4fActionAvailable(screen, C4F_ACTION_AUTORUN, diagnostic)) return C4F_ACTION_AUTORUN;
if ((standardPressed & C4F_INPUT_BUTTON_SQUARE) && c4fActionAvailable(screen, C4F_ACTION_STOP, diagnostic)) {
screen->confirmStop = 1;
screen->confirmChoice = 1;
screen->confirmMenu = 0;
c4fConsume(standardUnhandled, remoteUnhandled);
}
return C4F_ACTION_NONE;
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef C4F_LAUNCHER_NAVIGATION_H
#define C4F_LAUNCHER_NAVIGATION_H
#include "screen.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Returns the number of available actions, writing up to capacity entries. */
int c4fActionList(const C4fLauncherScreen *screen, int diagnostic,
C4fLauncherAction *actions, int capacity);
int c4fActionAvailable(const C4fLauncherScreen *screen, C4fLauncherAction action, int diagnostic);
const char *c4fActionLabel(const C4fLauncherScreen *screen, C4fLauncherAction action);
/* Only rising edges are accepted. The remaining bits preserve diagnostic
* paging/scrolling outside the menu, without moving both a page and focus. */
C4fLauncherAction c4fNavigate(C4fLauncherScreen *screen, uint32_t standardPressed,
uint32_t remotePressed, int diagnostic,
uint32_t *standardUnhandled, uint32_t *remoteUnhandled);
#ifdef __cplusplus
}
#endif
#endif
+83 -20
View File
@@ -9,6 +9,7 @@
#include "autorun.h"
#include "draw.h"
#include "logo.h"
#include "navigation.h"
#include "qrcodegen.h"
/* Page CSS pixels to TV pixels. */
@@ -443,18 +444,90 @@ static C4fLook c4fLookFor(const C4fLauncherScreen *s, char *meta, size_t metaSiz
meta, C4F_OK, C4F_ACCENT, 1 };
} else if (s->running == 0 && s->autorun == C4F_AUTORUN_ON) {
look = (C4fLook){ "Stopped", "Start Control4Free",
"It starts with GoldHEN after each restart. To start it now, turn on GoldHEN's PayLoader "
"and press Cross.",
"It starts with GoldHEN after each restart. To start now, enable GoldHEN's PayLoader "
"Server and press Cross.",
"Not running", C4F_FAINT, C4F_FAINT, 0 };
} else if (s->running == 0) {
look = (C4fLook){ "Stopped", "Set up Control4Free",
"Press Cross once. GoldHEN will start Control4Free each time it loads, and it starts "
"right away if GoldHEN's PayLoader is on.",
"Press Cross to set up auto-start. To start Control4Free now, enable GoldHEN's "
"PayLoader Server.",
"Not running", C4F_FAINT, C4F_FAINT, 0 };
}
return look;
}
/* The TV remote and controller share a visible action menu. Keep its rendering
* here so the normal and diagnostic screens use the same focus and confirmation. */
void c4fDrawActionOverlay(uint32_t *pixels, const C4fLauncherScreen *s)
{
if (!s->confirmStop && !s->actionMenu) return;
C4fCanvas c = { pixels, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
const float dw = 980;
const float dx = (c.w - dw) / 2;
int diagnostic = 0;
#ifdef C4F_DIAG
diagnostic = 1;
#endif
C4fLauncherAction actions[7];
int count = s->confirmStop ? 0 : c4fActionList(s, diagnostic, actions, 7);
const float dh = s->confirmStop ? (s->confirmMenu ? 410 : 330) : 220 + count * 72;
const float dy = (c.h - dh) / 2;
c4fFillRect(&c, 0, 0, c.w, c.h, C4F_RGBA(2, 11, 31, .65f));
c4fShadowRoundRect(&c, dx, dy, dw, dh, 6 * C4F_S, 0, 16 * C4F_S,
34 * C4F_S, -18 * C4F_S, C4F_RGBA(0, 0, 0, .9f));
c4fPanel(&c, dx, dy, dw, dh, C4F_RGBA(2, 11, 31, .97f), C4F_LINE);
c4fBandRoundRect(&c, dx, dy, dw, dh, 6 * C4F_S, 3 * C4F_S, C4F_ACCENT);
if (s->confirmStop) {
c4fText(&c, C4F_FONT_LIGHT, 48, dx + 48, dy + 92, C4F_TEXT, "Stop Control4Free?");
c4fTextWrapped(&c, C4F_FONT_LIGHT, 26, dx + 48, dy + 148, dw - 96, 39, 2, C4F_MUTED,
"Every controller disconnects. You can start Control4Free again from this screen.");
if (!s->confirmMenu) {
static const C4fHint hints[2] = { { C4F_KEY_CROSS, "Stop" }, { C4F_KEY_CIRCLE, "Cancel" } };
c4fHints(&c, dx + dw - 48, dy + dh - 58, hints, 2);
return;
}
const float bw = 240, gap = 24, by = dy + 235;
for (int i = 0; i < 2; i++) {
float bx = dx + dw - 48 - 2 * bw - gap + i * (bw + gap);
int focused = s->confirmChoice == i;
c4fFillRoundRect(&c, bx, by, bw, 64, 6 * C4F_S,
focused ? C4F_RGBA(40, 103, 205, .85f) : C4F_TILE);
c4fInsetRoundRect(&c, bx, by, bw, 64, 6 * C4F_S,
focused ? 3 : 1 * C4F_S, focused ? C4F_ACCENT : C4F_LINE);
c4fTextCentered(&c, C4F_FONT_REGULAR, 30, bx + bw / 2, by + 42,
focused ? C4F_TEXT : C4F_MUTED, i ? "Stop" : "Cancel");
}
c4fText(&c, C4F_FONT_LIGHT, 23, dx + 48, dy + dh - 48, C4F_MUTED,
"Arrows / D-pad: select OK / Cross: confirm Back / Circle: cancel");
return;
}
c4fText(&c, C4F_FONT_LIGHT, 48, dx + 48, dy + 84, C4F_TEXT, "Actions");
c4fText(&c, C4F_FONT_LIGHT, 24, dx + 48, dy + 125, C4F_MUTED,
s->busy ? "Working... You can return to the screen." : "Use arrows or the D-pad to choose an action.");
int focus = 0;
for (int i = 0; i < count; i++) if ((int)actions[i] == s->actionFocus) focus = i;
for (int i = 0; i < count; i++) {
const float rx = dx + 32, ry = dy + 153 + i * 72, rw = dw - 64;
int focused = i == focus;
c4fFillRoundRect(&c, rx, ry, rw, 64, 6 * C4F_S,
focused ? C4F_RGBA(40, 103, 205, .85f) : C4F_RGBA(255, 255, 255, .04f));
c4fInsetRoundRect(&c, rx, ry, rw, 64, 6 * C4F_S,
focused ? 3 : 1 * C4F_S, focused ? C4F_ACCENT : C4F_LINE_SOFT);
c4fText(&c, C4F_FONT_REGULAR, 30, rx + 24, ry + 42,
focused ? C4F_TEXT : C4F_MUTED, c4fActionLabel(s, actions[i]));
if (focused) {
float cx = rx + rw - 30, cy = ry + 32;
c4fSegment(&c, cx - 5, cy - 9, cx + 4, cy, 3, C4F_TEXT);
c4fSegment(&c, cx + 4, cy, cx - 5, cy + 9, 3, C4F_TEXT);
}
}
static const C4fHint hints[2] = { { C4F_KEY_CROSS, "Select" }, { C4F_KEY_CIRCLE, "Back" } };
c4fText(&c, C4F_FONT_LIGHT, 23, dx + 48, dy + dh - 32, C4F_MUTED, "TV remote: OK select / Back return");
c4fHints(&c, dx + dw - 48, dy + dh - 40, hints, 2);
}
void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
{
C4fCanvas c = { pixels, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
@@ -529,7 +602,7 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
if (mismatch) {
snprintf(versions, sizeof(versions), "Installed %s; running %s", C4F_LAUNCHER_VERSION, s->runningVersion);
panelTitle = versions;
panelText = "To apply this app's version now, press Square, confirm the stop with Cross, then press Cross to start. This disconnects all players.";
panelText = "Press Square, then Cross to stop, and Cross again to start this app's version. All players disconnect.";
panelDot = C4F_WARN;
} else if (s->autorun == C4F_AUTORUN_ON) {
panelTitle = "Starts by itself with GoldHEN";
@@ -538,12 +611,11 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
panelDot = C4F_OK;
} else if (s->autorun == C4F_AUTORUN_OUTDATED) {
panelTitle = "Auto-start uses an older Control4Free";
panelText = "Press Triangle to switch GoldHEN's auto-start to this version. It takes effect after the "
"next restart.";
panelText = "Press Triangle to update auto-start. GoldHEN uses this version after the next restart.";
panelDot = C4F_WARN;
} else if (s->autorun == C4F_AUTORUN_OFF) {
panelTitle = "Auto-start is off";
panelText = "Press Triangle and GoldHEN starts Control4Free each time it loads, with no PC needed.";
panelText = "Press Triangle to turn auto-start on. GoldHEN then starts Control4Free each time it loads.";
} else {
panelTitle = "Auto-start status unknown";
panelText = s->autorunNote[0] ? s->autorunNote : "Checking with GoldHEN...";
@@ -557,6 +629,8 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
panelText);
/* .home-foot and the button guide. */
if (!s->confirmStop && !s->actionMenu)
c4fText(&c, C4F_FONT_LIGHT, 23, left, 973, C4F_MUTED, "TV remote: OK opens actions / arrows select / Back returns");
c4fText(&c, C4F_FONT_LIGHT, 20, left, 1030, C4F_FAINT, "Control4Free " C4F_LAUNCHER_VERSION);
if (!s->confirmStop && !s->busy) {
C4fHint hints[4];
@@ -579,18 +653,7 @@ void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
c4fHints(&c, right, 1022, hints, n);
}
if (s->confirmStop) {
const float dw = 980, dh = 330, dx = (c.w - dw) / 2, dy = (c.h - dh) / 2;
static const C4fHint hints[2] = { { C4F_KEY_CROSS, "Stop" }, { C4F_KEY_CIRCLE, "Cancel" } };
c4fFillRect(&c, 0, 0, c.w, c.h, C4F_RGBA(2, 11, 31, .6f));
c4fShadowRoundRect(&c, dx, dy, dw, dh, 6 * C4F_S, 0, 16 * C4F_S, 34 * C4F_S, -18 * C4F_S, C4F_RGBA(0, 0, 0, .9f));
/* A little more opaque than --panel-strong, so the tiles do not ghost through. */
c4fPanel(&c, dx, dy, dw, dh, C4F_RGBA(2, 11, 31, .94f), C4F_LINE);
c4fText(&c, C4F_FONT_LIGHT, 48, dx + 48, dy + 96, C4F_TEXT, "Stop Control4Free?");
c4fTextWrapped(&c, C4F_FONT_LIGHT, 26, dx + 48, dy + 152, dw - 96, 39, 2, C4F_MUTED,
"Every controller disconnects. You can start Control4Free again from this screen.");
c4fHints(&c, dx + dw - 48, dy + dh - 58, hints, 2);
}
c4fDrawActionOverlay(pixels, s);
}
void c4fDrawIcon(uint32_t *pixels, int size)
+14 -1
View File
@@ -7,10 +7,21 @@
#endif
#define C4F_SCREEN_WIDTH 1920
#define C4F_SCREEN_HEIGHT 1080
typedef enum {
C4F_ACTION_NONE,
C4F_ACTION_RESUME,
C4F_ACTION_START,
C4F_ACTION_AUTORUN,
C4F_ACTION_STOP,
C4F_ACTION_CLOSE,
C4F_ACTION_CAPTURE
} C4fLauncherAction;
typedef struct {
int running, busy, controllers, confirmStop, locked;
int actionMenu, actionFocus;
int confirmChoice, confirmMenu; /* 0 Cancel / 1 Stop; menu-origin confirmation. */
int autorun; /* C4F_AUTORUN_* from autorun.h */
char address[64], message[160], autorunNote[96], runningVersion[32];
char address[64], message[160], autorunNote[96], runningVersion[32], inputNote[160];
#ifdef C4F_DIAG
unsigned diagGeneration; /* bumped when a diagnostic page changes */
int diagPage, diagScroll[8];
@@ -24,6 +35,8 @@ int c4fScreenInit(void);
/* Draws a whole frame. Blending reads pixels back, so pass cached memory,
* never the write-combined framebuffer. */
void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *state);
/* Shared actions and stop-confirmation overlay for both package variants. */
void c4fDrawActionOverlay(uint32_t *pixels, const C4fLauncherScreen *state);
/* The package icon (size x size), in the same style. */
void c4fDrawIcon(uint32_t *pixels, int size);
#ifdef __cplusplus
+5 -5
View File
@@ -239,10 +239,10 @@ int c4fLauncherStart(const unsigned char *payload, size_t payloadSize, char *mes
const char *targets[2];
int count = c4fTargets(targets), fd = -1;
for (int i = 0; i < count && fd < 0; i++) fd = c4fConnect(targets[i], C4F_PAYLOADER_PORT);
if (fd < 0) { snprintf(message, size, "PayLoader did not answer (errno %d). Turn it on in GoldHEN, then press Cross again.", errno); return -1; }
if (fd < 0) { snprintf(message, size, "PayLoader did not answer (errno %d). Enable PayLoader Server in GoldHEN > Server Settings, then retry Start.", errno); return -1; }
int failed = c4fSendAll(fd, payload, payloadSize, c4fLauncherTimeMs() + 10000);
shutdown(fd, SHUT_WR); close(fd);
if (failed) { snprintf(message, size, "The transfer to PayLoader broke off. Press Cross to try again."); return -2; }
if (failed) { snprintf(message, size, "The transfer to PayLoader broke off. Retry Start."); return -2; }
uint64_t deadline;
deadline = c4fLauncherTimeMs() + 20000;
while (c4fLauncherTimeMs() < deadline) {
@@ -251,7 +251,7 @@ int c4fLauncherStart(const unsigned char *payload, size_t payloadSize, char *mes
}
usleep(250000);
}
snprintf(message, size, "Sent, but Control4Free did not start (%s). Press Cross to try again.", c4fProblem);
snprintf(message, size, "Sent, but Control4Free did not start (%s). Retry Start.", c4fProblem);
/* Sending again is safe: a copy that did start holds the payload's
* instance lock, so a second one quits at once. */
return -2;
@@ -261,7 +261,7 @@ int c4fLauncherStop(char *message, size_t size)
{
C4fServiceStatus status;
int probe = c4fLauncherProbe(&status);
if (probe == 0) { snprintf(message, size, "Stopped. Press Cross to start it again."); return 0; }
if (probe == 0) { snprintf(message, size, "Stopped. Ready to start again."); return 0; }
if (probe != 1 && !c4fReached) { snprintf(message, size, "The app cannot reach Control4Free (%s). Stop it on its controller page.", c4fProblem); return -1; }
if (probe != 1) { snprintf(message, size, "This copy does not answer the app (%s). Stop it on its controller page.", c4fProblem); return -1; }
char reply[256];
@@ -269,7 +269,7 @@ int c4fLauncherStop(char *message, size_t size)
if (ret != 1) { snprintf(message, size, "Stop was not confirmed (%s). Check the controller page.", c4fProblem); return -1; }
uint64_t deadline = c4fLauncherTimeMs() + 5000;
while (c4fLauncherTimeMs() < deadline) {
if (c4fLauncherProbe(&status) == 0) { snprintf(message, size, "Stopped. Press Cross to start it again."); return 0; }
if (c4fLauncherProbe(&status) == 0) { snprintf(message, size, "Stopped. Ready to start again."); return 0; }
usleep(100000);
}
snprintf(message, size, "Still stopping. Wait a moment, then try again."); return -1;
+16
View File
@@ -0,0 +1,16 @@
/* Host-only page fixtures use the real diagnostic renderer, without native
* startup, filesystem access or report writes. Unused paths are discarded by
* the art tool's section garbage collection. */
#include "../launcher/diag.c"
void c4fDiagPreviewPage(int page, const char *text)
{
if (page < 0 || page >= C4F_DIAG_PAGES) return;
char *copy = strdup(text ? text : "");
if (!copy) return;
pthread_mutex_lock(&c4fLock);
free(c4fPages[page]);
c4fPages[page] = copy;
c4fGeneration++;
pthread_mutex_unlock(&c4fLock);
}
+563
View File
@@ -0,0 +1,563 @@
/* Launcher input and navigation using the real modules and simulated native
* readers. No controller service, network or console is contacted. */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "autorun.h"
#include "input.h"
#include "navigation.h"
#include "screen.h"
typedef struct {
int handle, owned, openError, readError, connected;
int opens, reads, closes;
uint32_t buttons;
} FakeSource;
typedef struct {
int initialResult, initialCalls;
int32_t user;
FakeSource source[2];
} FakeInput;
static int fakeInitial(void *context, int32_t *user)
{
FakeInput *fake = context;
fake->initialCalls++;
*user = fake->user;
return fake->initialResult;
}
static int fakeOpen(void *context, int32_t user, int port, int *owned)
{
FakeInput *fake = context;
int index;
if (port == C4F_INPUT_PORT_STANDARD) {
assert(user == fake->user && user >= 0);
index = C4F_INPUT_STANDARD;
} else {
assert(port == C4F_INPUT_PORT_REMOTE && user == C4F_INPUT_SYSTEM_USER);
index = C4F_INPUT_REMOTE;
}
FakeSource *source = &fake->source[index];
source->opens++;
*owned = source->owned;
return source->openError ? -1 : source->handle;
}
static FakeSource *fakeHandle(FakeInput *fake, int handle)
{
for (int i = 0; i < 2; i++) if (fake->source[i].handle == handle) return &fake->source[i];
assert(!"unknown handle");
return NULL;
}
static int fakeRead(void *context, int handle, uint32_t *buttons, int *connected)
{
FakeSource *source = fakeHandle(context, handle);
source->reads++;
/* Poison even a failed sample: a failed native call must never leak it. */
*buttons = source->buttons;
*connected = source->connected;
return source->readError ? -1 : 0;
}
static void fakeClose(void *context, int handle)
{
FakeSource *source = fakeHandle(context, handle);
assert(source->owned);
source->closes++;
assert(source->closes <= source->opens);
}
static void reader(FakeInput *fake, C4fInput *input)
{
memset(fake, 0, sizeof(*fake));
fake->user = 42;
fake->source[C4F_INPUT_STANDARD] = (FakeSource){ .handle = 11, .owned = 1, .connected = 1 };
fake->source[C4F_INPUT_REMOTE] = (FakeSource){ .handle = 22, .owned = 1, .connected = 1 };
C4fInputOps ops = { fake, fakeInitial, fakeOpen, fakeRead, fakeClose };
c4fInputInit(input, &ops);
}
static C4fInputFrame poll(C4fInput *input, uint64_t now)
{
C4fInputFrame frame;
/* Poll must fill both sources even if one cannot be opened. */
memset(&frame, 0xff, sizeof(frame));
c4fInputPoll(input, now, &frame);
return frame;
}
static void remoteKeyNormalization(void)
{
/* These are Sony remote packet key codes, not HDMI-CEC wire opcodes.
* The native wrapper's remote-port gate is reviewed separately in main;
* this host test checks only the helper and its downstream reader contract. */
const uint32_t raw = UINT32_C(0x25a500f0);
assert(c4fInputRemoteButtons(raw, 13) == (raw | C4F_INPUT_BUTTON_CROSS));
assert(c4fInputRemoteButtons(raw, 15) == (raw | C4F_INPUT_BUTTON_CIRCLE));
assert(c4fInputRemoteButtons(raw, 18) == (raw | C4F_INPUT_BUTTON_OPTIONS));
assert(c4fInputRemoteButtons(C4F_INPUT_BUTTON_CROSS, 13) == C4F_INPUT_BUTTON_CROSS);
for (unsigned code = 0; code <= UINT8_MAX; code++) {
if (code == 13 || code == 15 || code == 18) continue;
assert(c4fInputRemoteButtons(raw, (uint8_t)code) == raw);
assert(c4fInputRemoteButtons(0, (uint8_t)code) == 0);
}
assert(c4fInputRemoteButtons(C4F_INPUT_BUTTON_INTERCEPTED, 13) == C4F_INPUT_BUTTON_INTERCEPTED);
assert(c4fInputRemoteButtons(C4F_INPUT_BUTTON_INTERCEPTED, 15) == C4F_INPUT_BUTTON_INTERCEPTED);
assert(c4fInputRemoteButtons(C4F_INPUT_BUTTON_INTERCEPTED, 18) == C4F_INPUT_BUTTON_INTERCEPTED);
puts("PASS Sony remote key normalization preserves raw buttons, ignores unknown codes and respects interception");
}
static void remoteOnly(void)
{
FakeInput fake;
C4fInput input;
reader(&fake, &input);
fake.source[C4F_INPUT_STANDARD].openError = 1;
fake.source[C4F_INPUT_REMOTE].buttons = C4F_INPUT_BUTTON_CROSS;
C4fInputFrame frame = poll(&input, 0);
assert(!frame.standard.available && !frame.standard.buttons && !frame.standard.pressed);
assert(frame.remote.available && !frame.remote.pressed);
fake.source[C4F_INPUT_REMOTE].buttons = 0;
poll(&input, 10);
fake.source[C4F_INPUT_REMOTE].buttons = C4F_INPUT_BUTTON_CROSS;
frame = poll(&input, 20);
assert(frame.remote.pressed == C4F_INPUT_BUTTON_CROSS);
assert(!poll(&input, 30).remote.pressed);
assert(fake.source[C4F_INPUT_STANDARD].opens == 1);
c4fInputClose(&input);
assert(!fake.source[C4F_INPUT_STANDARD].closes && fake.source[C4F_INPUT_REMOTE].closes == 1);
c4fInputClose(&input);
assert(fake.source[C4F_INPUT_REMOTE].closes == 1);
puts("PASS remote-only launch, held OK baseline, no repeated activation and owned-handle cleanup");
}
static void sourceEdges(void)
{
FakeInput fake;
C4fInput input;
reader(&fake, &input);
C4fInputFrame frame = poll(&input, 0);
assert(frame.standard.available && frame.remote.available);
fake.source[C4F_INPUT_STANDARD].buttons = C4F_INPUT_BUTTON_CROSS;
frame = poll(&input, 10);
assert(frame.standard.pressed == C4F_INPUT_BUTTON_CROSS && !frame.remote.pressed);
fake.source[C4F_INPUT_REMOTE].buttons = C4F_INPUT_BUTTON_CROSS;
frame = poll(&input, 20);
assert(!frame.standard.pressed && frame.remote.pressed == C4F_INPUT_BUTTON_CROSS);
fake.source[C4F_INPUT_STANDARD].buttons = 0;
frame = poll(&input, 30);
assert(!frame.standard.buttons && frame.remote.buttons == C4F_INPUT_BUTTON_CROSS);
assert(!frame.standard.pressed && !frame.remote.pressed);
fake.source[C4F_INPUT_STANDARD].buttons = C4F_INPUT_BUTTON_CROSS;
frame = poll(&input, 40);
assert(frame.standard.pressed == C4F_INPUT_BUTTON_CROSS && !frame.remote.pressed);
fake.source[C4F_INPUT_REMOTE].buttons = 0;
poll(&input, 50);
fake.source[C4F_INPUT_REMOTE].buttons = C4F_INPUT_BUTTON_CROSS;
frame = poll(&input, 60);
assert(!frame.standard.pressed && frame.remote.pressed == C4F_INPUT_BUTTON_CROSS);
c4fInputClose(&input);
assert(fake.source[0].closes == 1 && fake.source[1].closes == 1);
puts("PASS independent controller/remote edges while either source holds or releases the same button");
}
static void remoteOpenRetry(void)
{
FakeInput fake;
C4fInput input;
reader(&fake, &input);
fake.source[1].openError = 1;
C4fInputFrame frame = poll(&input, 0);
assert(frame.standard.available && !frame.remote.available);
fake.source[0].buttons = C4F_INPUT_BUTTON_CROSS;
frame = poll(&input, 10);
assert(frame.standard.pressed == C4F_INPUT_BUTTON_CROSS && !frame.remote.buttons);
fake.source[1].openError = 0;
fake.source[1].buttons = C4F_INPUT_BUTTON_CROSS;
poll(&input, 999);
assert(fake.source[1].opens == 1);
frame = poll(&input, 1000);
assert(frame.remote.available && !frame.remote.buttons && !frame.remote.pressed);
assert(fake.source[1].opens == 2 && !frame.standard.pressed);
fake.source[1].buttons = 0;
poll(&input, 1010);
fake.source[1].buttons = C4F_INPUT_BUTTON_CROSS;
assert(poll(&input, 1020).remote.pressed == C4F_INPUT_BUTTON_CROSS);
c4fInputClose(&input);
assert(fake.source[0].closes == 1 && fake.source[1].closes == 1);
puts("PASS unavailable remote leaves DS4 operational and retry waits for a new neutral baseline");
}
static void readerRecovery(void)
{
FakeInput fake;
C4fInput input;
reader(&fake, &input);
poll(&input, 0);
fake.source[0].buttons = C4F_INPUT_BUTTON_CROSS;
fake.source[1].buttons = C4F_INPUT_BUTTON_RIGHT;
poll(&input, 10);
fake.source[1].readError = 1;
C4fInputFrame frame = poll(&input, 20);
assert(frame.standard.available && frame.standard.buttons == C4F_INPUT_BUTTON_CROSS);
assert(!frame.remote.available && !frame.remote.buttons && !frame.remote.pressed);
assert(fake.source[1].closes == 1);
fake.source[1].readError = 0;
poll(&input, 1019);
assert(fake.source[1].opens == 1);
frame = poll(&input, 1020);
assert(fake.source[1].opens == 2 && frame.remote.available && !frame.remote.pressed);
fake.source[1].buttons = 0;
poll(&input, 1030);
fake.source[1].buttons = C4F_INPUT_BUTTON_RIGHT;
assert(poll(&input, 1040).remote.pressed == C4F_INPUT_BUTTON_RIGHT);
fake.source[0].connected = 0;
frame = poll(&input, 1050);
assert(!frame.standard.available && !frame.standard.buttons && !frame.standard.pressed);
assert(frame.remote.available && frame.remote.buttons == C4F_INPUT_BUTTON_RIGHT);
assert(fake.source[0].closes == 1);
fake.source[0].connected = 1;
poll(&input, 2049);
assert(fake.source[0].opens == 1);
frame = poll(&input, 2050);
assert(frame.standard.available && !frame.standard.pressed && fake.source[0].opens == 2);
fake.source[0].buttons = 0;
poll(&input, 2060);
fake.source[0].buttons = C4F_INPUT_BUTTON_CROSS;
assert(poll(&input, 2070).standard.pressed == C4F_INPUT_BUTTON_CROSS);
c4fInputReset(&input, 2080);
frame = poll(&input, 2080);
assert(!frame.standard.pressed && !frame.remote.pressed);
fake.source[0].buttons = fake.source[1].buttons = 0;
poll(&input, 4000);
fake.source[0].buttons = C4F_INPUT_BUTTON_CROSS;
assert(poll(&input, 4010).standard.pressed == C4F_INPUT_BUTTON_CROSS);
c4fInputClose(&input);
assert(fake.source[0].closes == fake.source[0].opens);
assert(fake.source[1].closes == fake.source[1].opens);
puts("PASS read failures/disconnects clear stale activity, bounded retry and neutral-gated recovery/reset");
}
static void unavailableUserAndBorrowedHandle(void)
{
FakeInput fake;
C4fInput input;
reader(&fake, &input);
fake.initialResult = -1;
C4fInputFrame frame = poll(&input, 0);
assert(!frame.standard.available && frame.remote.available);
assert(fake.initialCalls == 1 && !fake.source[0].opens);
fake.initialResult = 0;
fake.user = -1;
poll(&input, 1000);
assert(fake.initialCalls == 2 && !fake.source[0].opens);
fake.user = 42;
fake.source[0].owned = 0; /* get-handle result owned by another subsystem */
frame = poll(&input, 2000);
assert(frame.standard.available && fake.source[0].opens == 1);
fake.source[0].readError = 1;
frame = poll(&input, 2010);
assert(!frame.standard.available && !frame.standard.buttons);
assert(!fake.source[0].closes && frame.remote.available);
fake.source[0].readError = 0;
frame = poll(&input, 3010);
assert(frame.standard.available && fake.source[0].opens == 2);
c4fInputClose(&input);
assert(!fake.source[0].closes && fake.source[1].closes == 1);
puts("PASS unavailable/invalid starting user does not block remote, borrowed handles remain open");
}
static C4fLauncherScreen stopped(void)
{
C4fLauncherScreen screen;
memset(&screen, 0, sizeof(screen));
screen.autorun = C4F_AUTORUN_ON;
return screen;
}
static int navigate(C4fLauncherScreen *screen, uint32_t standard, uint32_t remote, int diagnostic)
{
uint32_t standardUnhandled = 0, remoteUnhandled = 0;
return c4fNavigate(screen, standard, remote, diagnostic, &standardUnhandled, &remoteUnhandled);
}
static int remoteFrame(FakeInput *fake, C4fInput *input, C4fLauncherScreen *screen,
uint64_t now, uint32_t buttons)
{
fake->source[1].buttons = buttons;
C4fInputFrame frame = poll(input, now);
return navigate(screen, frame.standard.pressed, frame.remote.pressed, 0);
}
static int remoteKeyFrame(FakeInput *fake, C4fInput *input, C4fLauncherScreen *screen,
uint64_t now, uint8_t keyCode, int diagnostic)
{
/* The native reader supplies this normalized snapshot only for the remote
* port. Keep the fixture's standard controller snapshots unchanged. */
fake->source[C4F_INPUT_REMOTE].buttons = c4fInputRemoteButtons(0, keyCode);
C4fInputFrame frame = poll(input, now);
return navigate(screen, frame.standard.pressed, frame.remote.pressed, diagnostic);
}
static void extendedRemoteNavigation(int diagnostic)
{
FakeInput fake;
C4fInput input;
reader(&fake, &input);
fake.source[C4F_INPUT_STANDARD].openError = 1;
C4fLauncherScreen screen = stopped();
assert(remoteKeyFrame(&fake, &input, &screen, 0, 13, diagnostic) == C4F_ACTION_NONE);
assert(!screen.actionMenu);
assert(remoteKeyFrame(&fake, &input, &screen, 10, 13, diagnostic) == C4F_ACTION_NONE);
assert(!screen.actionMenu);
remoteKeyFrame(&fake, &input, &screen, 20, 0, diagnostic);
assert(remoteKeyFrame(&fake, &input, &screen, 30, 13, diagnostic) == C4F_ACTION_NONE);
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_START);
assert(remoteKeyFrame(&fake, &input, &screen, 40, 13, diagnostic) == C4F_ACTION_NONE);
assert(screen.actionMenu);
remoteKeyFrame(&fake, &input, &screen, 50, 0, diagnostic);
assert(remoteKeyFrame(&fake, &input, &screen, 60, 13, diagnostic) == C4F_ACTION_START);
assert(!screen.actionMenu);
assert(remoteKeyFrame(&fake, &input, &screen, 70, 13, diagnostic) == C4F_ACTION_NONE);
remoteKeyFrame(&fake, &input, &screen, 80, 0, diagnostic);
remoteKeyFrame(&fake, &input, &screen, 90, 13, diagnostic);
remoteKeyFrame(&fake, &input, &screen, 100, 0, diagnostic);
assert(remoteKeyFrame(&fake, &input, &screen, 110, 15, diagnostic) == C4F_ACTION_RESUME);
assert(!screen.actionMenu);
assert(remoteKeyFrame(&fake, &input, &screen, 120, 15, diagnostic) == C4F_ACTION_NONE);
remoteKeyFrame(&fake, &input, &screen, 130, 0, diagnostic);
assert(remoteKeyFrame(&fake, &input, &screen, 140, 18, diagnostic) == C4F_ACTION_NONE);
assert(screen.actionMenu);
assert(remoteKeyFrame(&fake, &input, &screen, 150, 18, diagnostic) == C4F_ACTION_NONE);
assert(screen.actionMenu);
remoteKeyFrame(&fake, &input, &screen, 160, 0, diagnostic);
assert(remoteKeyFrame(&fake, &input, &screen, 170, 15, diagnostic) == C4F_ACTION_RESUME);
fake.source[C4F_INPUT_REMOTE].connected = 0;
assert(remoteKeyFrame(&fake, &input, &screen, 180, 13, diagnostic) == C4F_ACTION_NONE);
assert(!screen.actionMenu && fake.source[C4F_INPUT_REMOTE].closes == 1);
fake.source[C4F_INPUT_REMOTE].connected = 1;
assert(remoteKeyFrame(&fake, &input, &screen, 1179, 13, diagnostic) == C4F_ACTION_NONE);
assert(fake.source[C4F_INPUT_REMOTE].opens == 1);
assert(remoteKeyFrame(&fake, &input, &screen, 1180, 13, diagnostic) == C4F_ACTION_NONE);
assert(fake.source[C4F_INPUT_REMOTE].opens == 2 && !screen.actionMenu);
assert(remoteKeyFrame(&fake, &input, &screen, 1181, 13, diagnostic) == C4F_ACTION_NONE);
remoteKeyFrame(&fake, &input, &screen, 1190, 0, diagnostic);
assert(remoteKeyFrame(&fake, &input, &screen, 1200, 13, diagnostic) == C4F_ACTION_NONE);
assert(screen.actionMenu);
remoteKeyFrame(&fake, &input, &screen, 1210, 0, diagnostic);
assert(remoteKeyFrame(&fake, &input, &screen, 1220, 15, diagnostic) == C4F_ACTION_RESUME);
assert(!screen.actionMenu);
c4fInputClose(&input);
assert(fake.source[C4F_INPUT_REMOTE].closes == 2);
puts(diagnostic ? "PASS extended remote OK/Back/Options, held-key safety and reconnect in diagnostics"
: "PASS extended remote OK/Back/Options, held-key safety and reconnect in the launcher");
}
static void heldRemoteConfirmation(void)
{
FakeInput fake;
C4fInput input;
reader(&fake, &input);
fake.source[0].openError = 1;
C4fLauncherScreen screen = stopped();
screen.running = 1;
assert(remoteFrame(&fake, &input, &screen, 0, 0) == C4F_ACTION_NONE);
assert(remoteFrame(&fake, &input, &screen, 10, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_NONE);
assert(screen.actionMenu);
assert(remoteFrame(&fake, &input, &screen, 20, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_NONE);
assert(screen.actionMenu);
remoteFrame(&fake, &input, &screen, 30, 0);
remoteFrame(&fake, &input, &screen, 40, C4F_INPUT_BUTTON_UP);
remoteFrame(&fake, &input, &screen, 50, 0);
remoteFrame(&fake, &input, &screen, 60, C4F_INPUT_BUTTON_UP);
assert(screen.actionFocus == C4F_ACTION_STOP);
remoteFrame(&fake, &input, &screen, 70, 0);
assert(remoteFrame(&fake, &input, &screen, 80, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_NONE);
assert(screen.confirmStop && !screen.confirmChoice);
assert(remoteFrame(&fake, &input, &screen, 90, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_NONE);
assert(screen.confirmStop && !screen.confirmChoice);
remoteFrame(&fake, &input, &screen, 100, 0);
remoteFrame(&fake, &input, &screen, 110, C4F_INPUT_BUTTON_RIGHT);
assert(screen.confirmChoice);
remoteFrame(&fake, &input, &screen, 120, 0);
assert(remoteFrame(&fake, &input, &screen, 130, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_STOP);
assert(remoteFrame(&fake, &input, &screen, 140, C4F_INPUT_BUTTON_CROSS) == C4F_ACTION_NONE);
c4fInputClose(&input);
puts("PASS combined native-reader/navigation flow never selects or confirms Stop from held OK");
}
static void menuActions(void)
{
C4fLauncherScreen screen = stopped();
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_START);
assert(navigate(&screen, 0, 0, 0) == C4F_ACTION_NONE && screen.actionMenu);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_START);
assert(!screen.actionMenu);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_DOWN, 0) == C4F_ACTION_NONE);
assert(screen.actionFocus == C4F_ACTION_AUTORUN);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_AUTORUN);
assert(!screen.actionMenu);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0) == C4F_ACTION_NONE);
assert(screen.actionFocus == C4F_ACTION_RESUME);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_RESUME);
assert(!screen.actionMenu);
screen.running = 1;
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_RESUME);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0) == C4F_ACTION_NONE);
assert(screen.actionFocus == C4F_ACTION_CLOSE);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_CLOSE);
assert(!screen.actionMenu);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CIRCLE, 0) == C4F_ACTION_CLOSE);
screen = stopped();
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CIRCLE, 0) == C4F_ACTION_RESUME);
assert(!screen.actionMenu);
puts("PASS remote arrows/OK reach Start, Auto-start, Resume and Close; opening OK is consumed");
}
static void stopConfirmation(void)
{
C4fLauncherScreen screen = stopped();
screen.running = 1;
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
assert(screen.actionFocus == C4F_ACTION_STOP);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
assert(screen.confirmStop && screen.confirmMenu && !screen.confirmChoice && !screen.actionMenu);
assert(navigate(&screen, 0, 0, 0) == C4F_ACTION_NONE && screen.confirmStop);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_RESUME);
assert(!screen.confirmStop);
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
navigate(&screen, 0, C4F_INPUT_BUTTON_UP, 0);
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_RIGHT, 0) == C4F_ACTION_NONE);
assert(screen.confirmChoice);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_STOP);
assert(!screen.confirmStop);
/* Existing DS4 shortcuts keep their separate confirmation flow. */
assert(navigate(&screen, C4F_INPUT_BUTTON_SQUARE, 0, 0) == C4F_ACTION_NONE);
assert(screen.confirmStop && !screen.confirmMenu);
assert(navigate(&screen, C4F_INPUT_BUTTON_CROSS, 0, 0) == C4F_ACTION_STOP);
assert(!screen.confirmStop);
navigate(&screen, C4F_INPUT_BUTTON_SQUARE, 0, 0);
assert(navigate(&screen, C4F_INPUT_BUTTON_CIRCLE, 0, 0) == C4F_ACTION_RESUME);
assert(!screen.confirmStop);
navigate(&screen, C4F_INPUT_BUTTON_SQUARE, 0, 0);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
assert(screen.confirmStop && screen.confirmMenu && !screen.confirmChoice);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_RESUME);
assert(!screen.confirmStop);
navigate(&screen, C4F_INPUT_BUTTON_SQUARE, 0, 0);
assert(navigate(&screen, C4F_INPUT_BUTTON_CROSS, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
assert(screen.confirmStop && screen.confirmMenu && !screen.confirmChoice);
assert(navigate(&screen, C4F_INPUT_BUTTON_CROSS, 0, 0) == C4F_ACTION_RESUME);
navigate(&screen, C4F_INPUT_BUTTON_SQUARE, 0, 0);
assert(navigate(&screen, C4F_INPUT_BUTTON_LEFT, 0, 0) == C4F_ACTION_NONE);
assert(screen.confirmStop && screen.confirmMenu && !screen.confirmChoice);
assert(navigate(&screen, C4F_INPUT_BUTTON_CROSS, 0, 0) == C4F_ACTION_RESUME);
puts("PASS remote Stop requires a separate fresh confirmation, defaults Cancel and preserves DS4 shortcuts");
}
static void unavailableActions(void)
{
C4fLauncherScreen screen = stopped();
screen.autorun = C4F_AUTORUN_UNKNOWN;
assert(!c4fActionAvailable(&screen, C4F_ACTION_AUTORUN, 0));
assert(navigate(&screen, C4F_INPUT_BUTTON_TRIANGLE, 0, 0) == C4F_ACTION_NONE);
screen.locked = 1;
assert(!c4fActionAvailable(&screen, C4F_ACTION_START, 0));
assert(navigate(&screen, C4F_INPUT_BUTTON_CROSS, 0, 0) == C4F_ACTION_NONE);
assert(navigate(&screen, C4F_INPUT_BUTTON_CIRCLE, 0, 0) == C4F_ACTION_CLOSE);
screen.busy = 1;
assert(navigate(&screen, C4F_INPUT_BUTTON_CROSS | C4F_INPUT_BUTTON_TRIANGLE | C4F_INPUT_BUTTON_SQUARE,
C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_RESUME);
assert(navigate(&screen, C4F_INPUT_BUTTON_CIRCLE, C4F_INPUT_BUTTON_CIRCLE, 0) == C4F_ACTION_RESUME);
assert(navigate(&screen, C4F_INPUT_BUTTON_CIRCLE, C4F_INPUT_BUTTON_CIRCLE, 0) == C4F_ACTION_NONE);
screen = stopped();
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
assert(screen.actionFocus == C4F_ACTION_START);
screen.running = 1; /* a status refresh while the user is choosing */
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
assert(screen.actionMenu && screen.actionFocus != C4F_ACTION_START);
/* A direction and OK arriving together must only move the focus. */
screen = stopped();
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 0);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_CROSS, 0) == C4F_ACTION_NONE);
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_AUTORUN);
assert(navigate(&screen, C4F_INPUT_BUTTON_UP, C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_CROSS, 0)
== C4F_ACTION_NONE);
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_AUTORUN);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_UP | C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_CROSS, 0)
== C4F_ACTION_NONE);
assert(screen.actionMenu && screen.actionFocus == C4F_ACTION_AUTORUN);
screen.running = 1;
screen.actionMenu = 0;
screen.confirmStop = screen.confirmMenu = screen.confirmChoice = 1;
assert(navigate(&screen, C4F_INPUT_BUTTON_UP, C4F_INPUT_BUTTON_DOWN | C4F_INPUT_BUTTON_CROSS, 0)
== C4F_ACTION_NONE);
assert(screen.confirmStop && screen.confirmChoice);
puts("PASS unavailable/busy actions, status-change focus repair and simultaneous direction/OK safety");
}
static void diagnosticNavigation(void)
{
C4fLauncherScreen screen = stopped();
uint32_t standardUnhandled, remoteUnhandled;
uint32_t directions = C4F_INPUT_BUTTON_UP | C4F_INPUT_BUTTON_RIGHT;
assert(c4fNavigate(&screen, directions, directions, 1, &standardUnhandled, &remoteUnhandled) == C4F_ACTION_NONE);
assert(standardUnhandled == directions && remoteUnhandled == directions);
assert(!screen.actionMenu);
assert(c4fNavigate(&screen, C4F_INPUT_BUTTON_OPTIONS, 0, 1,
&standardUnhandled, &remoteUnhandled) == C4F_ACTION_NONE);
assert(standardUnhandled == C4F_INPUT_BUTTON_OPTIONS && !remoteUnhandled);
assert(!c4fActionAvailable(&screen, C4F_ACTION_CAPTURE, 0));
assert(c4fActionAvailable(&screen, C4F_ACTION_CAPTURE, 1));
navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 1);
/* The action list defines display order; navigating through every row
* ensures capture is reachable using only the TV remote. */
C4fLauncherAction actions[8];
int count = c4fActionList(&screen, 1, actions, 8), reached = 0;
assert(count > 0 && count <= 8);
for (int i = 0; i <= count; i++) {
if (screen.actionFocus == C4F_ACTION_CAPTURE) { reached = 1; break; }
assert(c4fNavigate(&screen, 0, C4F_INPUT_BUTTON_DOWN, 1,
&standardUnhandled, &remoteUnhandled) == C4F_ACTION_NONE);
assert(!remoteUnhandled);
}
assert(reached);
assert(navigate(&screen, 0, C4F_INPUT_BUTTON_CROSS, 1) == C4F_ACTION_CAPTURE);
assert(!screen.actionMenu);
puts("PASS diagnostic arrows remain page/scroll inputs outside Actions; remote can choose Capture");
}
int main(void)
{
remoteKeyNormalization();
remoteOnly();
sourceEdges();
remoteOpenRetry();
readerRecovery();
unavailableUserAndBorrowedHandle();
menuActions();
extendedRemoteNavigation(0);
extendedRemoteNavigation(1);
heldRemoteConfirmation();
stopConfirmation();
unavailableActions();
diagnosticNavigation();
return 0;
}
+16 -1
View File
@@ -47,6 +47,14 @@ def one_reply(address, reply, check=None):
def main():
web.build()
# The launcher's own input reader and action navigation, with only the
# native PS4 pad calls replaced. This also exercises a remote-only launch.
for diagnostic in (False, True):
binary = 'build/launcher-input-diag-host-test' if diagnostic else 'build/launcher-input-host-test'
subprocess.run(['clang-18', '-std=gnu11', '-D_DEFAULT_SOURCE', '-Wall', '-Wextra', '-Werror',
*(['-DC4F_DIAG'] if diagnostic else []), '-Ilauncher', 'launcher/input.c',
'launcher/navigation.c', 'tests/launcher_input_test.c', '-o', binary], check=True)
subprocess.run([binary], check=True)
subprocess.run(['clang-18', '-std=gnu11', '-D_DEFAULT_SOURCE', '-Wall', '-Wextra', '-Werror',
'-ffunction-sections', '-fdata-sections', '-Ilauncher', '-Itests/include',
'tests/diag_test.c', '-Wl,--gc-sections,--wrap=write,--wrap=fsync', '-pthread',
@@ -177,10 +185,17 @@ def main():
sys.path.insert(0, str(ROOT / 'tools'))
import package_launcher as package
package.build_art_tool()
for state in ('setup', 'running', 'stopped', 'outdated', 'mismatch', 'confirm', 'busy', 'locked', 'unknown'):
for state in ('setup', 'running', 'stopped', 'payloader-unavailable', 'outdated', 'mismatch', 'confirm', 'busy', 'locked', 'unknown',
'remote-actions-stopped', 'remote-actions-running', 'remote-confirm-cancel',
'remote-confirm-stop', 'remote-actions-busy', 'remote-actions-unavailable'):
png = ROOT / f'build/launcher-preview-{state}.png'
package.render(['screen', state], png, 1920, 1080)
assert png.stat().st_size > 50000, png
package.build_art_tool(diagnostic=True)
for state in ('remote-actions-diag', 'remote-confirm-diag', 'remote-input-diag', 'payloader-unavailable-diag'):
png = ROOT / f'build/launcher-preview-{state}.png'
package.render(['screen', state], png, 1920, 1080, diagnostic=True)
assert png.stat().st_size > 50000, png
package.render(['icon', '512'], ROOT / 'build/launcher-icon.png', 512, 512)
assert subprocess.run([str(package.ART), 'screen', 'nonsense', '/dev/null']).returncode != 0
# The icon the payload serves is committed, so the payload build needs no
+42 -6
View File
@@ -1,13 +1,19 @@
/* Renders launcher artwork on the build machine with the launcher's own drawing
* code: the package icon, and screen states for review. Writes raw RGBA.
* launcher_art icon <size> <out.rgba>
* launcher_art screen <setup|running|stopped|outdated|confirm|busy|locked|unknown> <out.rgba> */
* launcher_art screen <state> <out.rgba>
* Includes TV-remote action and confirmation states. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "autorun.h"
#include "logo.h"
#include "screen.h"
#ifdef C4F_DIAG
#include "diag.h"
/* Host preview fixture; the native application does not link it. */
void c4fDiagPreviewPage(int page, const char *text);
#endif
static int c4fWrite(const char *path, const uint32_t *pixels, size_t count)
{
@@ -54,18 +60,23 @@ int main(int argc, char **argv)
snprintf(s.address, sizeof(s.address), "http://192.168.1.20:4264");
const char *state = argv[2], *message = "";
s.autorun = C4F_AUTORUN_ON;
if (!strcmp(state, "running") || !strcmp(state, "confirm") || !strcmp(state, "outdated") || !strcmp(state, "mismatch")) {
if (!strcmp(state, "running") || !strcmp(state, "confirm") || !strcmp(state, "outdated") || !strcmp(state, "mismatch") ||
!strcmp(state, "remote-actions-running") || !strcmp(state, "remote-confirm-cancel") ||
!strcmp(state, "remote-confirm-stop") || !strcmp(state, "remote-confirm-diag")) {
s.running = 1; s.controllers = 2; s.confirmStop = !strcmp(state, "confirm");
if (!strcmp(state, "outdated")) s.autorun = C4F_AUTORUN_OUTDATED;
if (!strcmp(state, "mismatch")) snprintf(s.runningVersion, sizeof(s.runningVersion), "older build");
message = "Ready. Open the address on your phone or PC.";
} else if (!strcmp(state, "setup")) {
s.autorun = C4F_AUTORUN_OFF; message = "Not running.";
} else if (!strcmp(state, "stopped")) {
message = "Stopped. Press Cross to start it again.";
} else if (!strcmp(state, "busy")) {
} else if (!strcmp(state, "stopped") || !strcmp(state, "remote-actions-stopped") ||
!strcmp(state, "remote-actions-diag") || !strcmp(state, "remote-input-diag")) {
message = "Stopped. Ready to start again.";
} else if (!strcmp(state, "payloader-unavailable") || !strcmp(state, "payloader-unavailable-diag")) {
message = "Auto-start is on. PayLoader did not answer (errno 61). Enable PayLoader Server in GoldHEN > Server Settings, then retry Start.";
} else if (!strcmp(state, "busy") || !strcmp(state, "remote-actions-busy")) {
s.running = -1; s.busy = 1; s.autorun = C4F_AUTORUN_UNKNOWN; message = "Setting up Control4Free. Please wait...";
} else if (!strcmp(state, "locked")) {
} else if (!strcmp(state, "locked") || !strcmp(state, "remote-actions-unavailable")) {
s.running = -1; s.locked = 1;
message = "Could not initialize launcher. Close with PS and retry.";
} else if (!strcmp(state, "unknown")) {
@@ -75,9 +86,34 @@ int main(int argc, char **argv)
} else {
return 2;
}
if (!strncmp(state, "remote-actions-", 15)) {
s.actionMenu = 1;
s.actionFocus = !strcmp(state, "remote-actions-running") ? C4F_ACTION_STOP :
!strcmp(state, "remote-actions-stopped") ? C4F_ACTION_START :
!strcmp(state, "remote-actions-diag") ? C4F_ACTION_CAPTURE : C4F_ACTION_RESUME;
} else if (!strncmp(state, "remote-confirm-", 15)) {
s.confirmStop = s.confirmMenu = 1;
s.confirmChoice = !strcmp(state, "remote-confirm-stop");
}
snprintf(s.message, sizeof(s.message), "%s", message);
uint32_t *pixels = calloc((size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT, sizeof(uint32_t));
if (!pixels) return 2;
#ifdef C4F_DIAG
if (!strcmp(state, "remote-input-diag") || !strcmp(state, "payloader-unavailable-diag")) {
snprintf(s.inputNote, sizeof(s.inputNote),
"DS4: disconnected (0x00000000); TV remote: ready (0x00000000), last key 0x0d / raw 0x00000000");
char summary[768];
snprintf(summary, sizeof(summary),
">> Example input data for layout review.\n\nConsole\n"
" system software: example\n\nThis app\n version %s\n"
" input: %s\n\nService (port 4264)\n not running\n\n"
"Auto-start\n state: on\n\nLast start from this app\n message: %s\n",
C4F_LAUNCHER_VERSION, s.inputNote, s.message);
c4fDiagPreviewPage(0, summary);
}
c4fDrawDiag(pixels, &s);
#else
c4fDrawLauncher(pixels, &s);
#endif
return c4fWrite(argv[3], pixels, (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT);
}
+21 -7
View File
@@ -31,17 +31,30 @@ def embed_fonts():
f'c4fFontLight={sources[0]}', f'c4fFontRegular={sources[1]}'], check=True)
def build_art_tool():
def build_art_tool(diagnostic=False):
"""Host build of the launcher's drawing code, for the icon and previews."""
embed_fonts()
art_version = VERSION + ('-diag' if diagnostic else '')
flags = ['clang-18', '-std=gnu11', '-O2', '-D_DEFAULT_SOURCE', f'-I{ROOT}/launcher',
f'-I{ROOT}/vendor/stb', f'-I{ROOT}/vendor/qrcodegen',
f'-DC4F_LAUNCHER_VERSION="{VERSION}"']
f'-DC4F_LAUNCHER_VERSION="{art_version}"']
extra_sources = []
extra_flags = []
art = ART
if diagnostic:
# Preview only: discard native startup/storage paths and retain the real
# diagnostic drawing function. Existing host declarations provide types;
# no console APIs or diagnostic file operations are called by this tool.
flags += ['-DC4F_DIAG', '-ffunction-sections', '-fdata-sections', f'-I{ROOT}/tests/include']
extra_sources = [str(ROOT / 'tests/diag_preview.c')]
extra_flags = ['-Wl,--gc-sections', '-pthread']
art = ART.with_name(ART.name + '-diag')
stb = OUT / 'stb_truetype-host.o'
subprocess.run([*flags, '-w', '-c', str(ROOT / 'launcher/stb_truetype.c'), '-o', str(stb)], check=True)
subprocess.run([*flags, '-Wall', '-Wextra', '-Werror', str(ROOT / 'tools/launcher_art.c'),
str(ROOT / 'launcher/screen.c'), str(ROOT / 'launcher/draw.c'), str(ROOT / 'launcher/logo.c'),
str(ROOT / 'vendor/qrcodegen/qrcodegen.c'), str(FONTS), str(stb), '-lm', '-o', str(ART)],
str(ROOT / 'launcher/screen.c'), str(ROOT / 'launcher/navigation.c'),
str(ROOT / 'launcher/draw.c'), str(ROOT / 'launcher/logo.c'), *extra_sources,
str(ROOT / 'vendor/qrcodegen/qrcodegen.c'), str(FONTS), str(stb), '-lm', *extra_flags, '-o', str(art)],
check=True)
@@ -86,10 +99,11 @@ def write_png(path, width, height, rgba, alpha=False):
chunk(b'IDAT', zlib.compress(b''.join(rows), 9)) + chunk(b'IEND', b''))
def render(args, path, width, height):
def render(args, path, width, height, diagnostic=False):
"""Draw with the art tool (build_art_tool first) and save a PNG."""
raw = OUT / 'art.rgba'
subprocess.run([str(ART), *args, str(raw)], check=True)
raw = OUT / ('art-diag.rgba' if diagnostic else 'art.rgba')
art = ART.with_name(ART.name + '-diag') if diagnostic else ART
subprocess.run([str(art), *args, str(raw)], check=True)
write_png(path, width, height, raw.read_bytes())
raw.unlink()