mirror of
https://github.com/MoHadiShibli/Control4Free.git
synced 2026-10-06 09:00:29 +02:00
Launcher app: home-screen PKG to start, check and stop Control4Free
An OpenOrbis app that sends the bundled payload to GoldHEN's PayLoader, shows its address and a QR code, and stops it through the loopback API. It draws its screen in software in the controller page's style (stb_truetype with Roboto), and its icon with the same code. The package (title ID CFRE00001) carries OpenOrbis's stub libc.prx and libSceFios2.prx in sce_module/, as the toolchain asks. The README covers GoldHEN AutoRun, the launcher, the page and the build.
This commit is contained in:
1 parent
6d931a05ce
commit
b3863fa5f1
22 files changed
+8544
No files matched your search
@@ -0,0 +1,155 @@
|
||||
# Control4Free
|
||||
|
||||
A GoldHEN payload for controlling the PS4 from a phone or PC, using touch,
|
||||
keyboard, or an Xbox/other controller exposed by the browser's Gamepad API.
|
||||
Virtual controllers use the PS4's native user-selection screen, so they work on
|
||||
the home screen, at sign-in and in games.
|
||||
|
||||
**Status: the browser controller works on the development console; the launcher
|
||||
app and GoldHEN AutoRun start are ready for their first console test.** Native
|
||||
user sign-in, menu control and gameplay have been confirmed by the tester. Four
|
||||
virtual slots are exposed; the console's device limits apply.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A PS4 running GoldHEN. Development console: firmware 10.01, GoldHEN 2.4b18.10.
|
||||
Starting Control4Free from GoldHEN itself (LaunchPad or AutoRun) needs 2.4b18.10
|
||||
or later; the launcher app and PC sending need PayLoader.
|
||||
- GoldHEN's klog server, which 2.4b18 always runs. Device creation relies on
|
||||
kernel-log events to recover the virtual device's handle.
|
||||
- A phone or PC on the same network.
|
||||
- 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 controllers.
|
||||
|
||||
## Start it with GoldHEN (AutoRun)
|
||||
|
||||
GoldHEN 2.4b18.10 can run payloads on its own, from its **Payloader LaunchPad**, and
|
||||
automatically each time GoldHEN starts (**AutoRun**). Set this up once and no PC or
|
||||
PayLoader is needed to start Control4Free.
|
||||
|
||||
1. Put `control4free.elf` in `/data/payloads/` on the PS4, for example through
|
||||
GoldHEN's FTP server (port 2121). GoldHEN also lists payloads in a `Payloads`
|
||||
folder on a USB drive, but keep the AutoRun copy on the console.
|
||||
2. In GoldHEN, open **Payloads > Payloader LaunchPad** and select
|
||||
`control4free.elf` to start it now.
|
||||
3. Press **Square** on it to add it to the AutoRun queue. GoldHEN then starts it
|
||||
every time GoldHEN loads.
|
||||
|
||||
When you update Control4Free, replace `/data/payloads/control4free.elf` as well.
|
||||
|
||||
## Install the launcher app
|
||||
|
||||
The launcher is a home-screen app that shows whether Control4Free is running, its
|
||||
address and a QR code for your phone, and can start or stop it.
|
||||
|
||||
1. Copy `build/Control4Free-0.2.0.pkg` to a USB drive and install it using the
|
||||
PS4's Package Installer with GoldHEN active.
|
||||
2. Open **Control4Free** from the home screen. If AutoRun already started it, the
|
||||
app shows **Running**. Otherwise turn on GoldHEN's PayLoader and press
|
||||
**Cross**.
|
||||
3. Scan the QR code or enter the address on your phone. Select a controller there
|
||||
and sign in through the PS4 screen.
|
||||
4. Press **Circle** to close the app. Control4Free keeps running on its own. Reopen
|
||||
the app to see the address again, or press **Square**, then **Cross**, to stop it
|
||||
and disconnect all its controllers.
|
||||
|
||||
The app does not turn on GoldHEN or PayLoader, and does not start after a reboot by
|
||||
itself; AutoRun does that.
|
||||
|
||||
**Upgrading from the original test payload:** disconnect controllers and use
|
||||
**Stop Control4Free** in the phone menu before starting from the launcher. The
|
||||
old payload has no launcher-management API, so the app refuses to start a second
|
||||
copy over it. Rebooting the console also clears that old instance.
|
||||
|
||||
## Use
|
||||
|
||||
1. Stop any older Control4Free payload before loading another. They all use port
|
||||
4264.
|
||||
2. Start Control4Free: from GoldHEN (above), from the launcher app, or by sending
|
||||
`build/control4free.elf` from a PC to GoldHEN's PayLoader on port 9090 with a
|
||||
payload sender. The payload serves its own page; no PC web server is needed.
|
||||
3. Open `http://YOUR-PS4-IP:4264` in the phone or PC browser.
|
||||
4. Select a free controller. This creates it and opens native PS4 user selection.
|
||||
Use Left/Right and Cross to select a user or follow the PS4's guest flow.
|
||||
**Do not press PS while the initial user-selection screen is open:** in the
|
||||
hardware experiment this cancelled selection. PS works after sign-in.
|
||||
5. Test the home screen and then a game before adding another controller.
|
||||
|
||||
For an Xbox or other physical controller, connect it to the phone/PC, press a
|
||||
button so the browser detects it, then choose its virtual-controller slot under
|
||||
connected controllers. Sign it in through the same PS4 screen. Add and sign in
|
||||
controllers one at a time. Touch and keyboard can also operate the selected slot.
|
||||
|
||||
The corner menu provides local layout/keyboard preferences, **Disconnect
|
||||
controller**, and **Stop Control4Free**. PS and Share send actual controller
|
||||
buttons. The PS4 assigns users; there are no pad or user settings on the page.
|
||||
|
||||
Some browsers restrict Gamepad API access on an HTTP page. If the page reports
|
||||
that restriction, try opening a saved copy of `client/index.html` locally and
|
||||
entering `YOUR-PS4-IP:4264` in its connection settings. Browser support for local
|
||||
files varies. Keep the controller page in the foreground while playing.
|
||||
|
||||
## Connection behavior
|
||||
|
||||
- Opening the page does not create a controller. Creation follows an explicit
|
||||
controller selection; the payload never presses sign-in buttons automatically.
|
||||
- Each virtual slot has one browser owner. Multiple local inputs may share that
|
||||
browser's selected slot, or use separate slots.
|
||||
- A lost connection releases held buttons. After 3 seconds without updates,
|
||||
inputs become neutral; after 15 seconds the controller is removed. A short
|
||||
disconnect leaves a 15-second window to select that controller again.
|
||||
- Explicit disconnect removes the controller immediately. Stopping the payload
|
||||
removes all controllers and restores the saved host credentials. Browser Stop
|
||||
refuses while another browser owns a controller. The launcher's confirmed Stop
|
||||
can disconnect all controllers through its loopback-only API.
|
||||
- If the kernel log is not available yet when Control4Free starts (AutoRun can
|
||||
start it early), it connects to it when the first controller is created.
|
||||
- There is no time limit. Rebooting the console ends it; AutoRun, the launcher or
|
||||
a PC starts it again.
|
||||
|
||||
## Build
|
||||
|
||||
The [ps4-payload-sdk](https://github.com/ps4-payload-dev/sdk) toolchain is pinned
|
||||
in the Docker image:
|
||||
|
||||
```sh
|
||||
docker build -t control4free-build docker/
|
||||
docker run --rm -v "$PWD:/src" -w /src control4free-build make
|
||||
```
|
||||
|
||||
Output: `build/control4free.elf`, with the compressed page embedded. `C4F_STAGE=0`
|
||||
is the default browser build. Stages 1-8 remain available as diagnostics, with
|
||||
separate `control4free-stageN.elf` outputs.
|
||||
|
||||
To build the installable launcher after the payload image is available:
|
||||
|
||||
```sh
|
||||
docker build -t control4free-launcher-build -f docker/Dockerfile.launcher docker/
|
||||
docker run --rm -v "$PWD:/src" -w /src control4free-launcher-build bash -lc 'make && make -C launcher'
|
||||
```
|
||||
|
||||
Output: `build/Control4Free-0.2.0.pkg` (title ID `CFRE00001`). On Windows,
|
||||
`tools/build-launcher.ps1` runs both image builds and the package build. It does
|
||||
not send anything to a console. Packaging uses OpenOrbis and LibOrbisPkg. The
|
||||
launcher draws its screen and its icon in software, in the controller page's style.
|
||||
|
||||
## Current limits
|
||||
|
||||
Rumble, lightbar feedback and motion input are not implemented. Rest-mode
|
||||
recovery and multiple native user sessions are unverified. User-assignment status
|
||||
comes from kernel-log events and may lag or miss an event; check the TV.
|
||||
Logs are written to `/data/control4free/spike.log` on the console.
|
||||
|
||||
## Credits and license
|
||||
|
||||
The VDA code is ported from [seregonwar/SplashDown](https://github.com/seregonwar/SplashDown).
|
||||
The build uses John Törnblom's [ps4-payload-sdk](https://github.com/ps4-payload-dev/sdk)
|
||||
and the [OpenOrbis PS4 Toolchain](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain).
|
||||
[Ghostcontrol](https://github.com/srbraboo/Ghostcontrol-PS5-USB-Controller-Patcher)
|
||||
was a research reference. JSON parsing uses [jsmn](https://github.com/zserge/jsmn);
|
||||
the launcher uses [QR Code generator](https://github.com/nayuki/QR-Code-generator),
|
||||
[stb_truetype](https://github.com/nothings/stb) and the
|
||||
[Roboto](https://github.com/googlefonts/roboto) font.
|
||||
|
||||
GPL-3.0; see [LICENSE](LICENSE) and [THIRD_PARTY.md](THIRD_PARTY.md).
|
||||
@@ -0,0 +1,33 @@
|
||||
# Third-party material
|
||||
|
||||
- The virtual-device implementation derives from seregonwar/SplashDown's
|
||||
`psbutton.c` (https://github.com/seregonwar/SplashDown), GPL-3.0.
|
||||
- `vendor/jsmn/jsmn.h` is from https://github.com/zserge/jsmn, copyright Serge
|
||||
Zaitsev, under the MIT license included in `vendor/jsmn/LICENSE`.
|
||||
Retrieved 2026-10-04; SHA-256:
|
||||
`c04533e9181e1e33baceb0f55ac449b05145bb936e8c68cc77dfe0d8277514fb`.
|
||||
- The toolchain uses https://github.com/ps4-payload-dev/sdk; its version is pinned
|
||||
in `docker/Dockerfile` and it retains its own licensing.
|
||||
- The native launcher uses the OpenOrbis PS4 Toolchain and its software-display
|
||||
initialization pattern (https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain,
|
||||
GPL-3.0). The toolchain supplies `sce_sys/about/right.sprx` and its stub
|
||||
`sce_module/libc.prx` and `sce_module/libSceFios2.prx`; LibOrbisPkg produces the
|
||||
package. `docker/Dockerfile.launcher` pins the OpenOrbis image by digest.
|
||||
- `vendor/qrcodegen/qrcodegen.c` and `.h` are Project Nayuki's QR generator v1.8.0
|
||||
(https://github.com/nayuki/QR-Code-generator/tree/v1.8.0/c), MIT. The full license
|
||||
is retained at the top of each file. SHA-256 for `.c`:
|
||||
`300eff07ee25baaa7578f20284411638154716379437391e7e689c0e6ce81403`;
|
||||
for `.h`: `e82df4bff37d18b5863b9e7486fe6bda1b6cda8c3b9ecebfec473907265cb589`.
|
||||
- `vendor/stb/stb_truetype.h` is Sean Barrett's stb_truetype v1.26
|
||||
(https://github.com/nothings/stb), public domain or MIT, as chosen at the end of
|
||||
the file. Retrieved 2026-10-04; SHA-256:
|
||||
`ecd30b05e0dd4fea3a13c26810dd9e1992dc379049482c393d5a19e6b5090aab`.
|
||||
- `vendor/roboto/` holds Roboto Light and Regular v2.138
|
||||
(https://github.com/googlefonts/roboto), Apache License 2.0
|
||||
(`vendor/roboto/LICENSE`). The launcher draws its text with them. SHA-256:
|
||||
`Roboto-Light.ttf` `a08729d794801eaa158c53b1558f4cc351c3b1791eb3d22faf7058b2b7df9c8c`,
|
||||
`Roboto-Regular.ttf` `f3edb8058e523f5612bfd99d0745e661568ad85e1b6217bc62f786fabae624c6`.
|
||||
|
||||
These license notices are also included inside the package.
|
||||
|
||||
The HTTP/WebSocket transport in `src/net.c` is a new implementation.
|
||||
@@ -0,0 +1,14 @@
|
||||
# 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
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends libssl1.1 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
FROM control4free-build:latest
|
||||
COPY --from=openorbis /opt/OpenOrbis/PS4Toolchain /opt/OpenOrbis/PS4Toolchain
|
||||
# The SDK's self-contained LibOrbisPkg runtime expects OpenSSL 1.1.
|
||||
COPY --from=pkgcompat /usr/lib/x86_64-linux-gnu/libssl.so.1.1 /usr/lib/x86_64-linux-gnu/
|
||||
COPY --from=pkgcompat /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 /usr/lib/x86_64-linux-gnu/
|
||||
ENV OO_PS4_TOOLCHAIN=/opt/OpenOrbis/PS4Toolchain
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
|
||||
WORKDIR /src
|
||||
@@ -0,0 +1,36 @@
|
||||
ifndef OO_PS4_TOOLCHAIN
|
||||
$(error OO_PS4_TOOLCHAIN is undefined; use the launcher Docker image)
|
||||
endif
|
||||
SDK := $(OO_PS4_TOOLCHAIN)
|
||||
OUT := ../build/launcher
|
||||
PKGDIR := $(OUT)/pkg
|
||||
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
|
||||
OBJECTS := $(OUT)/main.o $(OUT)/screen.o $(OUT)/draw.o $(OUT)/service.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
|
||||
.DEFAULT_GOAL := all
|
||||
-include $(OBJECTS:.o=.d)
|
||||
all: package
|
||||
$(OUT):
|
||||
mkdir -p $@
|
||||
$(OUT)/%.o: %.c | $(OUT)
|
||||
$(CC) $(CFLAGS) -std=gnu11 -c $< -o $@
|
||||
$(OUT)/main.o: main.cpp | $(OUT)
|
||||
$(CXX) $(CFLAGS) -std=c++17 -fno-exceptions -fno-rtti -c $< -o $@
|
||||
$(OUT)/qrcodegen.o: ../vendor/qrcodegen/qrcodegen.c | $(OUT)
|
||||
$(CC) $(CFLAGS) -std=gnu11 -c $< -o $@
|
||||
# Vendor code, built with its warnings off.
|
||||
$(OUT)/stb_truetype.o: stb_truetype.c | $(OUT)
|
||||
$(CC) $(CFLAGS) -std=gnu11 -w -c $< -o $@
|
||||
$(OUT)/fonts.c: $(FONTS) ../tools/embed_file.py | $(OUT)
|
||||
python3 ../tools/embed_file.py $@ c4fFontLight=../vendor/roboto/Roboto-Light.ttf c4fFontRegular=../vendor/roboto/Roboto-Regular.ttf
|
||||
$(OUT)/fonts.o: $(OUT)/fonts.c
|
||||
$(CC) $(CFLAGS) -std=gnu11 -c $< -o $@
|
||||
$(OUT)/launcher.elf: $(OBJECTS)
|
||||
$(LD) -m elf_x86_64 -pie --script $(SDK)/link.x --eh-frame-hdr -o $@ $(OBJECTS) -L$(SDK)/lib $(LIBS) $(SDK)/lib/crt1.o
|
||||
package: $(OUT)/launcher.elf ../build/control4free.elf
|
||||
python3 ../tools/package_launcher.py --sdk $(SDK)
|
||||
+348
@@ -0,0 +1,348 @@
|
||||
/* Software drawing for the native launcher. Shape coverage comes from signed
|
||||
* distances, text from stb_truetype; everything is blended in 8-bit ARGB. */
|
||||
#include "draw.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "stb_truetype.h"
|
||||
|
||||
extern const unsigned char c4fFontLight[], c4fFontRegular[];
|
||||
|
||||
typedef struct { unsigned char *bitmap; int w, h, x0, y0, ready; float advance; } C4fGlyph;
|
||||
typedef struct { int font, size; float scale; C4fGlyph glyph[95]; } C4fFace;
|
||||
|
||||
static stbtt_fontinfo c4fFonts[C4F_FONT_COUNT];
|
||||
static C4fFace c4fFaces[24];
|
||||
static int c4fFaceCount;
|
||||
static uint8_t c4fTextGamma[256];
|
||||
|
||||
int c4fDrawInit(void)
|
||||
{
|
||||
const unsigned char *data[C4F_FONT_COUNT] = { c4fFontLight, c4fFontRegular };
|
||||
for (int i = 0; i < C4F_FONT_COUNT; i++)
|
||||
if (!stbtt_InitFont(&c4fFonts[i], data[i], stbtt_GetFontOffsetForIndex(data[i], 0))) return -1;
|
||||
/* Light strokes on a dark background read thinner than a browser draws
|
||||
* them; lift partial coverage a little. */
|
||||
for (int i = 0; i < 256; i++) c4fTextGamma[i] = (uint8_t)(powf(i / 255.0f, 0.8f) * 255.0f + 0.5f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static float c4fClamp01(float v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
|
||||
static int c4fMinI(int a, int b) { return a < b ? a : b; }
|
||||
static int c4fMaxI(int a, int b) { return a > b ? a : b; }
|
||||
|
||||
void c4fBlend(C4fCanvas *c, int x, int y, C4fColor color, float coverage)
|
||||
{
|
||||
if ((unsigned)x >= (unsigned)c->w || (unsigned)y >= (unsigned)c->h) return;
|
||||
int a = (int)(coverage * color.a + 0.5f);
|
||||
if (a <= 0) return;
|
||||
uint32_t *p = &c->px[(size_t)y * c->w + x];
|
||||
if (a >= 255) { *p = 0xff000000u | (uint32_t)color.r << 16 | (uint32_t)color.g << 8 | color.b; return; }
|
||||
uint32_t d = *p;
|
||||
unsigned inv = 255u - (unsigned)a;
|
||||
unsigned r = (((d >> 16) & 255u) * inv + color.r * (unsigned)a + 127u) / 255u;
|
||||
unsigned g = (((d >> 8) & 255u) * inv + color.g * (unsigned)a + 127u) / 255u;
|
||||
unsigned b = ((d & 255u) * inv + color.b * (unsigned)a + 127u) / 255u;
|
||||
*p = 0xff000000u | r << 16 | g << 8 | b;
|
||||
}
|
||||
|
||||
void c4fFillRect(C4fCanvas *c, int x, int y, int w, int h, C4fColor color)
|
||||
{
|
||||
for (int py = c4fMaxI(y, 0); py < c4fMinI(y + h, c->h); py++)
|
||||
for (int px = c4fMaxI(x, 0); px < c4fMinI(x + w, c->w); px++) c4fBlend(c, px, py, color, 1);
|
||||
}
|
||||
|
||||
typedef struct { int x0, y0, x1, y1; } C4fBox;
|
||||
|
||||
static int c4fClip(const C4fCanvas *c, float x0, float y0, float x1, float y1, C4fBox *b)
|
||||
{
|
||||
b->x0 = c4fMaxI((int)floorf(x0), 0);
|
||||
b->y0 = c4fMaxI((int)floorf(y0), 0);
|
||||
b->x1 = c4fMinI((int)ceilf(x1), c->w);
|
||||
b->y1 = c4fMinI((int)ceilf(y1), c->h);
|
||||
return b->x0 < b->x1 && b->y0 < b->y1;
|
||||
}
|
||||
|
||||
/* Signed distance to a rounded box (centre, half size, corner radius). */
|
||||
float c4fRoundBoxDistance(float px, float py, float cx, float cy, float hx, float hy, float r)
|
||||
{
|
||||
float qx = fabsf(px - cx) - (hx - r), qy = fabsf(py - cy) - (hy - r);
|
||||
float ox = qx > 0 ? qx : 0, oy = qy > 0 ? qy : 0, inner = qx > qy ? qx : qy;
|
||||
return sqrtf(ox * ox + oy * oy) + (inner < 0 ? inner : 0) - r;
|
||||
}
|
||||
|
||||
typedef struct { float cx, cy, hx, hy, r; } C4fShape;
|
||||
|
||||
static C4fShape c4fShape(float x, float y, float w, float h, float r)
|
||||
{
|
||||
C4fShape s = { x + w / 2, y + h / 2, w / 2, h / 2, r };
|
||||
if (s.r > s.hx) s.r = s.hx;
|
||||
if (s.r > s.hy) s.r = s.hy;
|
||||
if (s.r < 0) s.r = 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
static float c4fShapeDistance(const C4fShape *s, int px, int py)
|
||||
{
|
||||
return c4fRoundBoxDistance(px + 0.5f, py + 0.5f, s->cx, s->cy, s->hx, s->hy, s->r);
|
||||
}
|
||||
|
||||
void c4fFillRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, C4fColor fill)
|
||||
{
|
||||
C4fShape s = c4fShape(x, y, w, h, r);
|
||||
C4fBox b;
|
||||
if (!c4fClip(c, x - 1, y - 1, x + w + 1, y + h + 1, &b)) return;
|
||||
for (int py = b.y0; py < b.y1; py++)
|
||||
for (int px = b.x0; px < b.x1; px++) {
|
||||
float cov = c4fClamp01(0.5f - c4fShapeDistance(&s, px, py));
|
||||
if (cov > 0) c4fBlend(c, px, py, fill, cov);
|
||||
}
|
||||
}
|
||||
|
||||
void c4fInsetRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, float width, C4fColor line)
|
||||
{
|
||||
C4fShape s = c4fShape(x, y, w, h, r);
|
||||
C4fBox b;
|
||||
if (!c4fClip(c, x - 1, y - 1, x + w + 1, y + h + 1, &b)) return;
|
||||
for (int py = b.y0; py < b.y1; py++)
|
||||
for (int px = b.x0; px < b.x1; px++) {
|
||||
float d = c4fShapeDistance(&s, px, py);
|
||||
float cov = c4fClamp01(0.5f - d) - c4fClamp01(0.5f - (d + width));
|
||||
if (cov > 0) c4fBlend(c, px, py, line, cov);
|
||||
}
|
||||
}
|
||||
|
||||
void c4fSheenRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, float angle,
|
||||
C4fColor color, float a0, float a1, float stop)
|
||||
{
|
||||
C4fShape s = c4fShape(x, y, w, h, r);
|
||||
C4fBox b;
|
||||
/* CSS gradient angles: 0deg points up, clockwise. */
|
||||
float rad = angle * 3.14159265f / 180.0f, dx = sinf(rad), dy = -cosf(rad);
|
||||
float length = fabsf(w * dx) + fabsf(h * dy);
|
||||
if (!c4fClip(c, x - 1, y - 1, x + w + 1, y + h + 1, &b)) return;
|
||||
for (int py = b.y0; py < b.y1; py++)
|
||||
for (int px = b.x0; px < b.x1; px++) {
|
||||
float cov = c4fClamp01(0.5f - c4fShapeDistance(&s, px, py));
|
||||
if (cov <= 0) continue;
|
||||
float t = ((px + 0.5f - s.cx) * dx + (py + 0.5f - s.cy) * dy) / length + 0.5f;
|
||||
float a = t >= stop ? a1 : t <= 0 ? a0 : a0 + (a1 - a0) * (t / stop);
|
||||
c4fBlend(c, px, py, color, cov * a);
|
||||
}
|
||||
}
|
||||
|
||||
void c4fBandRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, float height, C4fColor color)
|
||||
{
|
||||
C4fShape s = c4fShape(x, y, w, h, r);
|
||||
C4fBox b;
|
||||
if (!c4fClip(c, x - 1, y - 1, x + w + 1, y + height + 1, &b)) return;
|
||||
for (int py = b.y0; py < b.y1; py++) {
|
||||
float band = c4fClamp01(fminf(py + 1.0f, y + height) - fmaxf((float)py, y));
|
||||
if (band <= 0) continue;
|
||||
for (int px = b.x0; px < b.x1; px++) {
|
||||
float cov = c4fClamp01(0.5f - c4fShapeDistance(&s, px, py)) * band;
|
||||
if (cov > 0) c4fBlend(c, px, py, color, cov);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* A blurred edge: the normal CDF, approximated by a logistic curve. */
|
||||
static float c4fSoftEdge(float distance, float sigma)
|
||||
{
|
||||
return 1.0f / (1.0f + expf(1.702f * distance / sigma));
|
||||
}
|
||||
|
||||
void c4fShadowRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r,
|
||||
float dx, float dy, float blur, float spread, C4fColor color)
|
||||
{
|
||||
float sigma = blur > 1 ? blur / 2 : 0.5f, reach = 3 * sigma;
|
||||
C4fShape own = c4fShape(x, y, w, h, r);
|
||||
C4fShape shadow = c4fShape(x + dx - spread, y + dy - spread, w + 2 * spread, h + 2 * spread, r + spread);
|
||||
C4fBox b;
|
||||
if (shadow.hx <= 0 || shadow.hy <= 0) return;
|
||||
if (!c4fClip(c, fminf(x, x + dx - spread) - reach, fminf(y, y + dy - spread) - reach,
|
||||
fmaxf(x + w, x + dx + w + spread) + reach, fmaxf(y + h, y + dy + h + spread) + reach, &b)) return;
|
||||
for (int py = b.y0; py < b.y1; py++)
|
||||
for (int px = b.x0; px < b.x1; px++) {
|
||||
/* An outer box-shadow is never painted under the element itself. */
|
||||
float inside = c4fClamp01(0.5f - c4fShapeDistance(&own, px, py));
|
||||
if (inside >= 1) continue;
|
||||
float d = c4fShapeDistance(&shadow, px, py);
|
||||
if (d > reach) continue;
|
||||
c4fBlend(c, px, py, color, c4fSoftEdge(d, sigma) * (1 - inside));
|
||||
}
|
||||
}
|
||||
|
||||
void c4fFillCircle(C4fCanvas *c, float cx, float cy, float r, C4fColor fill)
|
||||
{
|
||||
C4fBox b;
|
||||
if (!c4fClip(c, cx - r - 1, cy - r - 1, cx + r + 1, cy + r + 1, &b)) return;
|
||||
for (int py = b.y0; py < b.y1; py++)
|
||||
for (int px = b.x0; px < b.x1; px++) {
|
||||
float cov = c4fClamp01(0.5f - (hypotf(px + 0.5f - cx, py + 0.5f - cy) - r));
|
||||
if (cov > 0) c4fBlend(c, px, py, fill, cov);
|
||||
}
|
||||
}
|
||||
|
||||
void c4fRing(C4fCanvas *c, float cx, float cy, float r, float width, C4fColor line)
|
||||
{
|
||||
C4fBox b;
|
||||
if (!c4fClip(c, cx - r - 1, cy - r - 1, cx + r + 1, cy + r + 1, &b)) return;
|
||||
for (int py = b.y0; py < b.y1; py++)
|
||||
for (int px = b.x0; px < b.x1; px++) {
|
||||
float d = hypotf(px + 0.5f - cx, py + 0.5f - cy) - r;
|
||||
float cov = c4fClamp01(0.5f - d) - c4fClamp01(0.5f - (d + width));
|
||||
if (cov > 0) c4fBlend(c, px, py, line, cov);
|
||||
}
|
||||
}
|
||||
|
||||
void c4fGlowCircle(C4fCanvas *c, float cx, float cy, float r, float blur, float spread, C4fColor color)
|
||||
{
|
||||
float sigma = blur > 1 ? blur / 2 : 0.5f, reach = r + spread + 3 * sigma;
|
||||
C4fBox b;
|
||||
if (!c4fClip(c, cx - reach, cy - reach, cx + reach, cy + reach, &b)) return;
|
||||
for (int py = b.y0; py < b.y1; py++)
|
||||
for (int px = b.x0; px < b.x1; px++) {
|
||||
float distance = hypotf(px + 0.5f - cx, py + 0.5f - cy);
|
||||
float inside = c4fClamp01(0.5f - (distance - r));
|
||||
if (inside >= 1) continue;
|
||||
c4fBlend(c, px, py, color, c4fSoftEdge(distance - (r + spread), sigma) * (1 - inside));
|
||||
}
|
||||
}
|
||||
|
||||
void c4fSegment(C4fCanvas *c, float x0, float y0, float x1, float y1, float width, C4fColor color)
|
||||
{
|
||||
float hw = width / 2, vx = x1 - x0, vy = y1 - y0, length2 = vx * vx + vy * vy;
|
||||
C4fBox b;
|
||||
if (!c4fClip(c, fminf(x0, x1) - hw - 1, fminf(y0, y1) - hw - 1, fmaxf(x0, x1) + hw + 1, fmaxf(y0, y1) + hw + 1, &b)) return;
|
||||
for (int py = b.y0; py < b.y1; py++)
|
||||
for (int px = b.x0; px < b.x1; px++) {
|
||||
float fx = px + 0.5f - x0, fy = py + 0.5f - y0;
|
||||
float t = length2 > 0 ? c4fClamp01((fx * vx + fy * vy) / length2) : 0;
|
||||
float cov = c4fClamp01(0.5f - (hypotf(fx - t * vx, fy - t * vy) - hw));
|
||||
if (cov > 0) c4fBlend(c, px, py, color, cov);
|
||||
}
|
||||
}
|
||||
|
||||
void c4fSquareOutline(C4fCanvas *c, float cx, float cy, float half, float width, C4fColor color)
|
||||
{
|
||||
float reach = half + width;
|
||||
C4fBox b;
|
||||
if (!c4fClip(c, cx - reach - 1, cy - reach - 1, cx + reach + 1, cy + reach + 1, &b)) return;
|
||||
for (int py = b.y0; py < b.y1; py++)
|
||||
for (int px = b.x0; px < b.x1; px++) {
|
||||
float d = c4fRoundBoxDistance(px + 0.5f, py + 0.5f, cx, cy, half, half, width * 0.4f);
|
||||
float cov = c4fClamp01(0.5f - (fabsf(d) - width / 2));
|
||||
if (cov > 0) c4fBlend(c, px, py, color, cov);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- text ---- */
|
||||
|
||||
static C4fFace *c4fFace(int font, float size)
|
||||
{
|
||||
int pixels = (int)(size + 0.5f);
|
||||
for (int i = 0; i < c4fFaceCount; i++)
|
||||
if (c4fFaces[i].font == font && c4fFaces[i].size == pixels) return &c4fFaces[i];
|
||||
if (font < 0 || font >= C4F_FONT_COUNT || c4fFaceCount == (int)(sizeof(c4fFaces) / sizeof(c4fFaces[0]))) return NULL;
|
||||
C4fFace *face = &c4fFaces[c4fFaceCount++];
|
||||
memset(face, 0, sizeof(*face));
|
||||
face->font = font;
|
||||
face->size = pixels;
|
||||
/* CSS font-size is the em size. */
|
||||
face->scale = stbtt_ScaleForMappingEmToPixels(&c4fFonts[font], (float)pixels);
|
||||
return face;
|
||||
}
|
||||
|
||||
static int c4fCodepoint(unsigned char ch) { return ch < 32 || ch > 126 ? '?' : ch; }
|
||||
|
||||
static C4fGlyph *c4fGlyph(C4fFace *face, int ch)
|
||||
{
|
||||
C4fGlyph *g = &face->glyph[ch - 32];
|
||||
if (!g->ready) {
|
||||
const stbtt_fontinfo *font = &c4fFonts[face->font];
|
||||
int advance, bearing, x0, y0, x1, y1;
|
||||
stbtt_GetCodepointHMetrics(font, ch, &advance, &bearing);
|
||||
stbtt_GetCodepointBitmapBox(font, ch, face->scale, face->scale, &x0, &y0, &x1, &y1);
|
||||
g->advance = advance * face->scale;
|
||||
g->x0 = x0;
|
||||
g->y0 = y0;
|
||||
g->w = x1 - x0;
|
||||
g->h = y1 - y0;
|
||||
if (g->w > 0 && g->h > 0 && (g->bitmap = malloc((size_t)g->w * (size_t)g->h)))
|
||||
stbtt_MakeCodepointBitmap(font, g->bitmap, g->w, g->h, g->w, face->scale, face->scale, ch);
|
||||
g->ready = 1;
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
static float c4fTextWidthN(int font, float size, const char *text, int length)
|
||||
{
|
||||
C4fFace *face = c4fFace(font, size);
|
||||
float width = 0;
|
||||
int previous = 0;
|
||||
if (!face) return 0;
|
||||
for (int i = 0; i < length && text[i]; i++) {
|
||||
int ch = c4fCodepoint((unsigned char)text[i]);
|
||||
if (previous) width += stbtt_GetCodepointKernAdvance(&c4fFonts[font], previous, ch) * face->scale;
|
||||
width += c4fGlyph(face, ch)->advance;
|
||||
previous = ch;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
float c4fTextWidth(int font, float size, const char *text)
|
||||
{
|
||||
return c4fTextWidthN(font, size, text, (int)strlen(text));
|
||||
}
|
||||
|
||||
float c4fTextN(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text, int length)
|
||||
{
|
||||
C4fFace *face = c4fFace(font, size);
|
||||
float pen = x;
|
||||
int previous = 0, baseline = (int)floorf(y + 0.5f);
|
||||
if (!face) return 0;
|
||||
for (int i = 0; i < length && text[i]; i++) {
|
||||
int ch = c4fCodepoint((unsigned char)text[i]);
|
||||
if (previous) pen += stbtt_GetCodepointKernAdvance(&c4fFonts[font], previous, ch) * face->scale;
|
||||
C4fGlyph *g = c4fGlyph(face, ch);
|
||||
if (g->bitmap) {
|
||||
int gx = (int)floorf(pen + 0.5f) + g->x0, gy = baseline + g->y0;
|
||||
for (int row = 0; row < g->h; row++)
|
||||
for (int col = 0; col < g->w; col++) {
|
||||
unsigned value = g->bitmap[row * g->w + col];
|
||||
if (value) c4fBlend(c, gx + col, gy + row, color, c4fTextGamma[value] / 255.0f);
|
||||
}
|
||||
}
|
||||
pen += g->advance;
|
||||
previous = ch;
|
||||
}
|
||||
return pen - x;
|
||||
}
|
||||
|
||||
float c4fText(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text)
|
||||
{
|
||||
return c4fTextN(c, font, size, x, y, color, text, (int)strlen(text));
|
||||
}
|
||||
|
||||
int c4fTextWrapped(C4fCanvas *c, int font, float size, float x, float y, float maxWidth,
|
||||
float lineHeight, int maxLines, C4fColor color, const char *text)
|
||||
{
|
||||
int lines = 0;
|
||||
while (*text && lines < maxLines) {
|
||||
while (*text == ' ') text++;
|
||||
if (!*text) break;
|
||||
int fit = 0, end = 0;
|
||||
for (;;) {
|
||||
while (text[end] && text[end] != ' ') end++;
|
||||
if (fit && c4fTextWidthN(font, size, text, end) > maxWidth) break;
|
||||
fit = end;
|
||||
if (!text[end]) break;
|
||||
end++;
|
||||
}
|
||||
if (c) c4fTextN(c, font, size, x, y + lines * lineHeight, color, text, fit);
|
||||
lines++;
|
||||
text += fit;
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef C4F_LAUNCHER_DRAW_H
|
||||
#define C4F_LAUNCHER_DRAW_H
|
||||
/* Software drawing into a cached ARGB buffer: anti-aliased shapes and text.
|
||||
* Blending reads the destination, so never draw straight into the
|
||||
* write-combined framebuffer. */
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct { uint32_t *px; int w, h; } C4fCanvas;
|
||||
typedef struct { uint8_t r, g, b, a; } C4fColor;
|
||||
#define C4F_RGBA(r, g, b, a) ((C4fColor){ (r), (g), (b), (uint8_t)((a) * 255.0f + 0.5f) })
|
||||
|
||||
enum { C4F_FONT_LIGHT, C4F_FONT_REGULAR, C4F_FONT_COUNT };
|
||||
|
||||
int c4fDrawInit(void);
|
||||
|
||||
/* Signed distance to a rounded box: centre, half size, corner radius. */
|
||||
float c4fRoundBoxDistance(float px, float py, float cx, float cy, float hx, float hy, float r);
|
||||
|
||||
void c4fBlend(C4fCanvas *c, int x, int y, C4fColor color, float coverage);
|
||||
void c4fFillRect(C4fCanvas *c, int x, int y, int w, int h, C4fColor color);
|
||||
|
||||
/* Rounded rectangles; x/y/w/h/r in pixels, fractions allowed. */
|
||||
void c4fFillRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, C4fColor fill);
|
||||
/* A CSS "inset 0 0 0 <width>px" line along the inside edge. */
|
||||
void c4fInsetRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, float width, C4fColor line);
|
||||
/* A linear gradient clipped to the shape: alpha a0 at the start, a1 from
|
||||
* `stop` on, along a CSS angle in degrees. */
|
||||
void c4fSheenRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, float angle,
|
||||
C4fColor color, float a0, float a1, float stop);
|
||||
/* The part of the shape between top and top+height (a tile's colour bar). */
|
||||
void c4fBandRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r, float height, C4fColor color);
|
||||
/* A CSS outer box-shadow: drawn only outside the shape itself. */
|
||||
void c4fShadowRoundRect(C4fCanvas *c, float x, float y, float w, float h, float r,
|
||||
float dx, float dy, float blur, float spread, C4fColor color);
|
||||
|
||||
void c4fFillCircle(C4fCanvas *c, float cx, float cy, float r, C4fColor fill);
|
||||
void c4fRing(C4fCanvas *c, float cx, float cy, float r, float width, C4fColor line);
|
||||
/* Soft halo around a circle, like box-shadow 0 0 <blur>px <spread>px. */
|
||||
void c4fGlowCircle(C4fCanvas *c, float cx, float cy, float r, float blur, float spread, C4fColor color);
|
||||
/* A line with round caps. */
|
||||
void c4fSegment(C4fCanvas *c, float x0, float y0, float x1, float y1, float width, C4fColor color);
|
||||
void c4fSquareOutline(C4fCanvas *c, float cx, float cy, float half, float width, C4fColor color);
|
||||
|
||||
/* Text on a baseline. Sizes are CSS font-size in pixels. ASCII only. */
|
||||
float c4fText(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text);
|
||||
float c4fTextN(C4fCanvas *c, int font, float size, float x, float y, C4fColor color, const char *text, int length);
|
||||
float c4fTextWidth(int font, float size, const char *text);
|
||||
/* Word-wrapped text; returns the number of lines (drawn only if c is set). */
|
||||
int c4fTextWrapped(C4fCanvas *c, int font, float size, float x, float y, float maxWidth,
|
||||
float lineHeight, int maxLines, C4fColor color, const char *text);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,192 @@
|
||||
/* Native PS4 launcher. The controller service runs outside this application. */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
#include <orbis/libkernel.h>
|
||||
#include <orbis/NetCtl.h>
|
||||
#include <orbis/Pad.h>
|
||||
#include <orbis/SystemService.h>
|
||||
#include <orbis/UserService.h>
|
||||
#include <orbis/VideoOut.h>
|
||||
#include "screen.h"
|
||||
#include "service.h"
|
||||
|
||||
static pthread_mutex_t c4fMutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static C4fLauncherScreen c4fScreen;
|
||||
static int c4fCommand, c4fQuit;
|
||||
|
||||
static void *c4fWorker(void *)
|
||||
{
|
||||
uint64_t nextCheck = 0;
|
||||
int first = 1;
|
||||
for (;;) {
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
int command = c4fCommand, quit = c4fQuit;
|
||||
c4fCommand = 0;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
if (quit) return NULL;
|
||||
if (command || c4fLauncherTimeMs() >= nextCheck) {
|
||||
char message[160] = {0}, address[64] = {0};
|
||||
int result = 0;
|
||||
if (command == 1) result = c4fLauncherStart("/app0/assets/control4free.elf", message, sizeof(message));
|
||||
if (command == 2) result = c4fLauncherStop(message, sizeof(message));
|
||||
C4fServiceStatus status;
|
||||
int running = c4fLauncherProbe(&status);
|
||||
OrbisNetCtlInfo info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
if (sceNetCtlGetInfo(ORBIS_NET_CTL_INFO_IP_ADDRESS, &info) == 0 &&
|
||||
info.ip_address[0] && strcmp(info.ip_address, "0.0.0.0"))
|
||||
snprintf(address, sizeof(address), "http://%.15s:4264", info.ip_address);
|
||||
pthread_mutex_lock(&c4fMutex);
|
||||
if (result == -2) c4fScreen.locked = 1;
|
||||
if (command) snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s", message);
|
||||
else if (!c4fScreen.locked && (first || running != c4fScreen.running)) {
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "%s",
|
||||
running == 1 ? "Ready. Scan the code or open the address on your phone." :
|
||||
running == 0 ? "Not running. Press Cross to start it." :
|
||||
"Something else answers on port 4264. Stop the old copy from its phone page.");
|
||||
}
|
||||
c4fScreen.running = running;
|
||||
c4fScreen.controllers = status.controllers;
|
||||
snprintf(c4fScreen.address, sizeof(c4fScreen.address), "%s", address);
|
||||
/* A queued click while a periodic probe was in flight remains busy. */
|
||||
if (!c4fCommand) c4fScreen.busy = 0;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
first = 0; nextCheck = c4fLauncherTimeMs() + 2000;
|
||||
}
|
||||
usleep(100000);
|
||||
}
|
||||
}
|
||||
|
||||
static int c4fVideo = -1;
|
||||
static OrbisKernelEqueue c4fFlipQueue;
|
||||
static int c4fHasQueue, c4fBufferRegistration = -1;
|
||||
static void *c4fVideoMemory;
|
||||
static off_t c4fVideoOffset;
|
||||
static size_t c4fVideoSize;
|
||||
static void *c4fBuffers[2];
|
||||
|
||||
static int c4fInitVideo(void)
|
||||
{
|
||||
c4fVideo = sceVideoOutOpen(ORBIS_VIDEO_USER_MAIN, ORBIS_VIDEO_OUT_BUS_MAIN, 0, NULL);
|
||||
if (c4fVideo < 0) return -1;
|
||||
if (sceKernelCreateEqueue(&c4fFlipQueue, "Control4Free screen") < 0) return -1;
|
||||
c4fHasQueue = 1;
|
||||
if (sceVideoOutAddFlipEvent(c4fFlipQueue, c4fVideo, NULL) < 0) return -1;
|
||||
const size_t frameSize = C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(uint32_t);
|
||||
const size_t alignment = 0x200000;
|
||||
size_t size = (frameSize * 2 + alignment - 1) & ~(alignment - 1);
|
||||
if (sceKernelAllocateDirectMemory(0, sceKernelGetDirectMemorySize(), size, alignment, 3, &c4fVideoOffset) < 0) return -1;
|
||||
c4fVideoSize = size;
|
||||
if (sceKernelMapDirectMemory(&c4fVideoMemory, size, 0x33, 0, c4fVideoOffset, alignment) < 0) return -1;
|
||||
c4fBuffers[0] = c4fVideoMemory;
|
||||
c4fBuffers[1] = (char *)c4fVideoMemory + frameSize;
|
||||
OrbisVideoOutBufferAttribute attr;
|
||||
memset(&attr, 0, sizeof(attr));
|
||||
/* Same linear ARGB format as the OpenOrbis software-rendering sample. */
|
||||
sceVideoOutSetBufferAttribute(&attr, 0x80000000, 1, 0, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT, C4F_SCREEN_WIDTH);
|
||||
c4fBufferRegistration = sceVideoOutRegisterBuffers(c4fVideo, 0, c4fBuffers, 2, &attr);
|
||||
if (c4fBufferRegistration < 0) return -1;
|
||||
return sceVideoOutSetFlipRate(c4fVideo, 1); /* 30 Hz is enough for a launcher. */
|
||||
}
|
||||
|
||||
static void c4fCloseVideo(void)
|
||||
{
|
||||
if (c4fVideo >= 0) {
|
||||
if (c4fBufferRegistration >= 0) sceVideoOutUnregisterBuffers(c4fVideo, c4fBufferRegistration);
|
||||
sceVideoOutClose(c4fVideo);
|
||||
}
|
||||
if (c4fHasQueue) sceKernelDeleteEqueue(c4fFlipQueue);
|
||||
if (c4fVideoMemory) sceKernelMunmap(c4fVideoMemory, c4fVideoSize);
|
||||
if (c4fVideoSize) sceKernelReleaseDirectMemory(c4fVideoOffset, c4fVideoSize);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const size_t frameBytes = (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(uint32_t);
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
printf("[c4f-launcher] start " C4F_LAUNCHER_VERSION "\n");
|
||||
/* Frames are composed in cached memory: blending reads pixels back, which
|
||||
* is very slow from the write-combined framebuffers. */
|
||||
uint32_t *canvas = (uint32_t *)malloc(frameBytes);
|
||||
if (!canvas || c4fScreenInit() < 0 || c4fInitVideo() < 0) {
|
||||
printf("[c4f-launcher] display initialization failed\n");
|
||||
c4fCloseVideo(); free(canvas); return 1;
|
||||
}
|
||||
sceNetCtlInit();
|
||||
OrbisUserServiceInitializeParams params = {};
|
||||
params.priority = ORBIS_KERNEL_PRIO_FIFO_LOWEST;
|
||||
sceUserServiceInitialize(¶ms);
|
||||
int32_t user = -1, pad = -1;
|
||||
if (scePadInit() == 0 && sceUserServiceGetInitialUser(&user) == 0)
|
||||
pad = scePadOpen(user, ORBIS_PAD_PORT_TYPE_STANDARD, 0, NULL);
|
||||
c4fScreen.running = -1; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Checking Control4Free...");
|
||||
pthread_t worker;
|
||||
int workerStarted = pthread_create(&worker, NULL, c4fWorker, NULL) == 0;
|
||||
if (!workerStarted || pad < 0) {
|
||||
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;
|
||||
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;
|
||||
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 = 2; c4fScreen.confirmStop = 0; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Stopping and releasing controllers...");
|
||||
} else if (!c4fScreen.locked && c4fScreen.running != 1) {
|
||||
c4fCommand = 1; c4fScreen.busy = 1;
|
||||
snprintf(c4fScreen.message, sizeof(c4fScreen.message), "Starting Control4Free. Please wait...");
|
||||
}
|
||||
} else if ((pressed & ORBIS_PAD_BUTTON_SQUARE) && c4fScreen.running == 1) c4fScreen.confirmStop = 1;
|
||||
}
|
||||
C4fLauncherScreen snapshot = c4fScreen;
|
||||
pthread_mutex_unlock(&c4fMutex);
|
||||
/* Redraw only when something changed, then copy it to each buffer once. */
|
||||
if (!drawn || memcmp(&snapshot, &drawnState, sizeof(snapshot))) {
|
||||
c4fDrawLauncher(canvas, &snapshot);
|
||||
drawnState = snapshot; drawn = 1; shown[0] = shown[1] = 0;
|
||||
}
|
||||
if (!shown[buffer]) { memcpy(c4fBuffers[buffer], canvas, frameBytes); shown[buffer] = 1; }
|
||||
if (sceVideoOutSubmitFlip(c4fVideo, buffer, ORBIS_VIDEO_OUT_FLIP_VSYNC, frame) < 0) break;
|
||||
for (;;) {
|
||||
OrbisVideoOutFlipStatus status;
|
||||
memset(&status, 0, sizeof(status));
|
||||
if (sceVideoOutGetFlipStatus(c4fVideo, &status) < 0) { done = 1; break; }
|
||||
if (status.flipArg >= frame) break;
|
||||
OrbisKernelEvent event;
|
||||
int count;
|
||||
OrbisKernelUseconds timeout = 100000;
|
||||
sceKernelWaitEqueue(c4fFlipQueue, &event, 1, &count, &timeout);
|
||||
}
|
||||
buffer ^= 1; frame++;
|
||||
}
|
||||
pthread_mutex_lock(&c4fMutex); c4fQuit = 1; pthread_mutex_unlock(&c4fMutex);
|
||||
if (workerStarted) pthread_join(worker, NULL);
|
||||
if (pad >= 0) scePadClose(pad);
|
||||
sceNetCtlTerm();
|
||||
c4fCloseVideo();
|
||||
free(canvas);
|
||||
/* Closing this app intentionally does not stop the external payload. */
|
||||
sceSystemServiceLoadExec("exit", NULL);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
/* The launcher screen, drawn in the controller page's style (client/index.html):
|
||||
* the same blue backdrop with light waves, tiles, panels and colours, at 1.5x
|
||||
* the page's CSS sizes for a 1080p TV. */
|
||||
#include "screen.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "draw.h"
|
||||
#include "qrcodegen.h"
|
||||
|
||||
/* Page CSS pixels to TV pixels. */
|
||||
#define C4F_S 1.5f
|
||||
|
||||
/* Layout, in TV pixels. */
|
||||
#define C4F_LEFT 150.0f
|
||||
#define C4F_RIGHT 1770.0f
|
||||
#define C4F_ROW_TOP 336.0f
|
||||
#define C4F_ROW_HEIGHT 444.0f
|
||||
#define C4F_ROW_GAP 30.0f
|
||||
#define C4F_QR_WIDTH 520.0f
|
||||
#define C4F_MAIN_WIDTH (C4F_RIGHT - C4F_LEFT - C4F_ROW_GAP - C4F_QR_WIDTH)
|
||||
#define C4F_QR_LEFT (C4F_LEFT + C4F_MAIN_WIDTH + C4F_ROW_GAP)
|
||||
#define C4F_PANEL_TOP (C4F_ROW_TOP + C4F_ROW_HEIGHT + 28)
|
||||
#define C4F_PANEL_HEIGHT 128.0f
|
||||
|
||||
/* The page's palette (:root). */
|
||||
#define C4F_TEXT C4F_RGBA(244, 247, 255, 1)
|
||||
#define C4F_MUTED C4F_RGBA(226, 236, 255, .72f)
|
||||
#define C4F_FAINT C4F_RGBA(226, 236, 255, .46f)
|
||||
#define C4F_LINE C4F_RGBA(255, 255, 255, .16f)
|
||||
#define C4F_LINE_SOFT C4F_RGBA(255, 255, 255, .09f)
|
||||
#define C4F_PANEL C4F_RGBA(3, 16, 44, .55f)
|
||||
#define C4F_PANEL_STRONG C4F_RGBA(2, 11, 31, .86f)
|
||||
#define C4F_TILE C4F_RGBA(12, 40, 98, .5f)
|
||||
#define C4F_ACCENT C4F_RGBA(58, 140, 255, 1)
|
||||
#define C4F_OK C4F_RGBA(69, 224, 138, 1)
|
||||
#define C4F_WARN C4F_RGBA(255, 208, 77, 1)
|
||||
#define C4F_BAD C4F_RGBA(255, 107, 125, 1)
|
||||
#define C4F_CIRCLE_KEY C4F_RGBA(255, 102, 128, 1)
|
||||
#define C4F_CROSS_KEY C4F_RGBA(134, 182, 255, 1)
|
||||
#define C4F_SQUARE_KEY C4F_RGBA(255, 146, 220, 1)
|
||||
#define C4F_WHITE C4F_RGBA(255, 255, 255, 1)
|
||||
|
||||
static uint32_t *c4fBackdrop;
|
||||
|
||||
static float c4fClamp(float v) { return v < 0 ? 0 : v > 1 ? 1 : v; }
|
||||
|
||||
/* ---- backdrop (.backdrop, .waves, .sparkles) ---- */
|
||||
|
||||
static float c4fNoise(int x, int y)
|
||||
{
|
||||
uint32_t h = (uint32_t)x * 374761393u + (uint32_t)y * 668265263u;
|
||||
h = (h ^ (h >> 13)) * 1274126177u;
|
||||
return (float)((h ^ (h >> 16)) & 0xffffu) / 65536.0f - 0.5f;
|
||||
}
|
||||
|
||||
static void c4fMix(float rgb[3], float r, float g, float b, float a)
|
||||
{
|
||||
rgb[0] += (r - rgb[0]) * a;
|
||||
rgb[1] += (g - rgb[1]) * a;
|
||||
rgb[2] += (b - rgb[2]) * a;
|
||||
}
|
||||
|
||||
static void c4fPaintGradient(C4fCanvas *c)
|
||||
{
|
||||
static const float stops[4][4] = {
|
||||
{ 0.00f, 0x02, 0x0f, 0x2b }, { 0.45f, 0x06, 0x27, 0x66 },
|
||||
{ 0.72f, 0x0b, 0x3f, 0x9c }, { 1.00f, 0x04, 0x19, 0x4a },
|
||||
};
|
||||
const float w = (float)c->w, h = (float)c->h;
|
||||
/* linear-gradient(165deg): CSS angles point up at 0 and turn clockwise. */
|
||||
const float dx = 0.258819f, dy = 0.965926f, length = w * dx + h * dy;
|
||||
for (int y = 0; y < c->h; y++)
|
||||
for (int x = 0; x < c->w; x++) {
|
||||
float px = x + 0.5f, py = y + 0.5f, rgb[3];
|
||||
float t = c4fClamp(((px - w / 2) * dx + (py - h / 2) * dy) / length + 0.5f);
|
||||
int i = t > stops[2][0] ? 2 : t > stops[1][0] ? 1 : 0;
|
||||
float f = (t - stops[i][0]) / (stops[i + 1][0] - stops[i][0]);
|
||||
for (int k = 0; k < 3; k++) rgb[k] = stops[i][k + 1] + (stops[i + 1][k + 1] - stops[i][k + 1]) * f;
|
||||
/* radial-gradient(80% 60% at 0% -10%, rgba(25, 90, 200, .55), transparent 60%) */
|
||||
float ex = px / (0.8f * w), ey = (py + 0.1f * h) / (0.6f * h);
|
||||
c4fMix(rgb, 25, 90, 200, 0.55f * c4fClamp(1 - sqrtf(ex * ex + ey * ey) / 0.6f));
|
||||
/* radial-gradient(120% 70% at 75% 115%, rgba(70, 150, 255, .5), transparent 62%), on top */
|
||||
ex = (px - 0.75f * w) / (1.2f * w);
|
||||
ey = (py - 1.15f * h) / (0.7f * h);
|
||||
c4fMix(rgb, 70, 150, 255, 0.5f * c4fClamp(1 - sqrtf(ex * ex + ey * ey) / 0.62f));
|
||||
/* Dither, so the dark gradient does not band on a TV. */
|
||||
float noise = c4fNoise(x, y);
|
||||
uint32_t out = 0xff000000u;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
int v = (int)floorf(rgb[k] + noise + 0.5f);
|
||||
out |= (uint32_t)(v < 0 ? 0 : v > 255 ? 255 : v) << (16 - 8 * k);
|
||||
}
|
||||
c->px[(size_t)y * c->w + x] = out;
|
||||
}
|
||||
}
|
||||
|
||||
/* The page's wave paths (viewBox 0 0 3200 400): the start y, then four cubic
|
||||
* segments (c1x c1y c2x c2y x y) with every "S" expanded. */
|
||||
static const float c4fWaveA[25] = { 230, 200, 150, 400, 310, 800, 230, 1200, 150, 1400, 150, 1600, 230,
|
||||
1800, 150, 2000, 310, 2400, 230, 2800, 150, 3000, 150, 3200, 230 };
|
||||
static const float c4fWaveB[25] = { 260, 250, 200, 550, 330, 800, 260, 1050, 190, 1350, 190, 1600, 260,
|
||||
1850, 200, 2150, 330, 2400, 260, 2650, 190, 2950, 190, 3200, 260 };
|
||||
static const float c4fWaveC[25] = { 200, 300, 110, 500, 300, 800, 200, 1100, 100, 1300, 100, 1600, 200,
|
||||
1900, 110, 2100, 300, 2400, 200, 2700, 100, 2900, 100, 3200, 200 };
|
||||
static const float c4fWaveD[25] = { 215, 300, 140, 520, 290, 800, 215, 1080, 140, 1320, 130, 1600, 215,
|
||||
1900, 140, 2120, 290, 2400, 215, 2680, 140, 2920, 130, 3200, 215 };
|
||||
static const float c4fWaveE[25] = { 240, 280, 170, 540, 330, 800, 240, 1060, 150, 1330, 160, 1600, 240,
|
||||
1880, 170, 2140, 330, 2400, 240, 2660, 150, 2930, 160, 3200, 240 };
|
||||
|
||||
/* Gradient stops: offset, r, g, b, alpha. */
|
||||
typedef float C4fWaveStops[3][5];
|
||||
static const C4fWaveStops c4fSlowStops = { { 0, 255, 255, 255, 0 }, { .5f, 188, 216, 255, .55f }, { 1, 255, 255, 255, 0 } };
|
||||
static const C4fWaveStops c4fMainStops = { { 0, 127, 178, 255, .1f }, { .5f, 255, 255, 255, .5f }, { 1, 127, 178, 255, .1f } };
|
||||
|
||||
static C4fColor c4fWaveColor(const C4fWaveStops stops, float t, float opacity)
|
||||
{
|
||||
const float *a = stops[0], *b = stops[1];
|
||||
t = c4fClamp(t);
|
||||
if (t > stops[1][0]) { a = stops[1]; b = stops[2]; }
|
||||
float f = (t - a[0]) / (b[0] - a[0]);
|
||||
/* Interpolate premultiplied, as browsers do. */
|
||||
float alpha = a[4] + (b[4] - a[4]) * f, rgb[3];
|
||||
for (int k = 0; k < 3; k++) {
|
||||
float v = a[k + 1] * a[4] + (b[k + 1] * b[4] - a[k + 1] * a[4]) * f;
|
||||
rgb[k] = alpha > 0 ? v / alpha : a[k + 1];
|
||||
}
|
||||
return (C4fColor){ (uint8_t)rgb[0], (uint8_t)rgb[1], (uint8_t)rgb[2], (uint8_t)(alpha * opacity * 255 + 0.5f) };
|
||||
}
|
||||
|
||||
static void c4fPaintWave(C4fCanvas *c, const float path[25], float offset, float top, float sx, float sy,
|
||||
float stroke, float opacity, const C4fWaveStops stops)
|
||||
{
|
||||
enum { STEPS = 48, POINTS = 4 * STEPS + 1 };
|
||||
float px[POINTS], py[POINTS], x0 = 0, y0 = path[0];
|
||||
int n = 1;
|
||||
px[0] = offset;
|
||||
py[0] = top + y0 * sy;
|
||||
for (int s = 0; s < 4; s++) {
|
||||
const float *k = path + 1 + s * 6;
|
||||
for (int i = 1; i <= STEPS; i++) {
|
||||
float t = (float)i / STEPS, u = 1 - t;
|
||||
float bx = u * u * u * x0 + 3 * u * u * t * k[0] + 3 * u * t * t * k[2] + t * t * t * k[4];
|
||||
float by = u * u * u * y0 + 3 * u * u * t * k[1] + 3 * u * t * t * k[3] + t * t * t * k[5];
|
||||
px[n] = offset + bx * sx;
|
||||
py[n] = top + by * sy;
|
||||
n++;
|
||||
}
|
||||
x0 = k[4];
|
||||
y0 = k[5];
|
||||
}
|
||||
/* The paths only ever move right, so each column crosses a path once. */
|
||||
float half = fmaxf(stroke * sy / 2, 0.35f);
|
||||
int seg = 0;
|
||||
for (int x = 0; x < c->w; x++) {
|
||||
float fx = x + 0.5f;
|
||||
if (fx < px[0] || fx > px[n - 1]) continue;
|
||||
while (seg < n - 2 && px[seg + 1] < fx) seg++;
|
||||
float span = px[seg + 1] - px[seg];
|
||||
float slope = span > 0 ? (py[seg + 1] - py[seg]) / span : 0;
|
||||
float yc = py[seg] + slope * (fx - px[seg]), norm = sqrtf(1 + slope * slope);
|
||||
C4fColor color = c4fWaveColor(stops, (fx - offset) / (sx * 3200), opacity);
|
||||
for (int y = (int)floorf(yc - (half + 1) * norm); y <= (int)ceilf(yc + (half + 1) * norm); y++) {
|
||||
float cov = c4fClamp(half + 0.5f - fabsf(y + 0.5f - yc) / norm);
|
||||
if (cov > 0) c4fBlend(c, x, y, color, cov);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void c4fPaintBackdrop(C4fCanvas *c, float scale)
|
||||
{
|
||||
const float w = (float)c->w, h = (float)c->h, sx = 2 * w / 3200;
|
||||
c4fPaintGradient(c);
|
||||
/* A still from the drifting animation, the brightest part mid-screen.
|
||||
* .waves.slow: bottom 14%, height 50%, opacity .7. */
|
||||
float top = 0.36f * h, sy = 0.5f * h / 400, offset = -0.70f * w;
|
||||
c4fPaintWave(c, c4fWaveA, offset, top, sx, sy, 1.2f, 0.7f, c4fSlowStops);
|
||||
c4fPaintWave(c, c4fWaveB, offset, top, sx, sy, 2.0f, 0.7f * 0.6f, c4fSlowStops);
|
||||
/* .waves: bottom 4%, height 62%. */
|
||||
top = 0.34f * h;
|
||||
sy = 0.62f * h / 400;
|
||||
offset = -0.38f * w;
|
||||
c4fPaintWave(c, c4fWaveC, offset, top, sx, sy, 1.5f, 1.0f, c4fMainStops);
|
||||
c4fPaintWave(c, c4fWaveD, offset, top, sx, sy, 1.0f, 0.7f, c4fMainStops);
|
||||
c4fPaintWave(c, c4fWaveE, offset, top, sx, sy, 3.0f, 0.35f, c4fMainStops);
|
||||
/* .sparkles spans -10% to 110% of the height. */
|
||||
static const float dots[8][4] = {
|
||||
{ .12f, .22f, 1.5f, .70f }, { .28f, .64f, 1.0f, .55f }, { .47f, .38f, 1.5f, .60f }, { .63f, .76f, 1.0f, .50f },
|
||||
{ .78f, .30f, 2.0f, .45f }, { .88f, .58f, 1.0f, .60f }, { .36f, .88f, 1.5f, .50f }, { .92f, .12f, 1.0f, .55f },
|
||||
};
|
||||
for (int i = 0; i < 8; i++) {
|
||||
float cx = dots[i][0] * w, cy = -0.1f * h + dots[i][1] * 1.2f * h, r = fmaxf(dots[i][2] * scale, 1.5f);
|
||||
for (int y = (int)floorf(cy - r); y <= (int)ceilf(cy + r); y++)
|
||||
for (int x = (int)floorf(cx - r); x <= (int)ceilf(cx + r); x++) {
|
||||
float d = hypotf(x + 0.5f - cx, y + 0.5f - cy);
|
||||
if (d < r) c4fBlend(c, x, y, C4F_RGBA(255, 255, 255, dots[i][3]), 1 - d / r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int c4fClampI(int v, int lo, int hi) { return v < lo ? lo : v > hi ? hi : v; }
|
||||
|
||||
/* One box-blur pass along rows or columns of an RGB buffer, edges clamped. */
|
||||
static void c4fBoxPass(const uint8_t *src, uint8_t *dst, int w, int h, int radius, int horizontal)
|
||||
{
|
||||
const int n = horizontal ? w : h, lines = horizontal ? h : w, divisor = 2 * radius + 1;
|
||||
const int step = horizontal ? 3 : w * 3, lineStep = horizontal ? w * 3 : 3;
|
||||
for (int line = 0; line < lines; line++) {
|
||||
const uint8_t *s = src + (size_t)line * lineStep;
|
||||
uint8_t *d = dst + (size_t)line * lineStep;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
int sum = 0;
|
||||
for (int i = -radius; i <= radius; i++) sum += s[c4fClampI(i, 0, n - 1) * step + k];
|
||||
for (int i = 0; i < n; i++) {
|
||||
d[i * step + k] = (uint8_t)((sum + divisor / 2) / divisor);
|
||||
sum += s[c4fClampI(i + radius + 1, 0, n - 1) * step + k] - s[c4fClampI(i - radius, 0, n - 1) * step + k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* backdrop-filter: blur(sigma) under a tile. The backdrop never changes, so
|
||||
* this is done once, clipped to the tile's rounded shape. */
|
||||
static void c4fBlurUnder(C4fCanvas *c, float x, float y, float w, float h, float r, float sigma)
|
||||
{
|
||||
/* Three box passes of this radius approximate a Gaussian of that sigma. */
|
||||
const int radius = (int)((sqrtf(4 * sigma * sigma + 1) - 1) / 2 + 0.5f), margin = 3 * radius;
|
||||
const int x0 = c4fClampI((int)floorf(x) - margin, 0, c->w), y0 = c4fClampI((int)floorf(y) - margin, 0, c->h);
|
||||
const int x1 = c4fClampI((int)ceilf(x + w) + margin, 0, c->w), y1 = c4fClampI((int)ceilf(y + h) + margin, 0, c->h);
|
||||
const int bw = x1 - x0, bh = y1 - y0;
|
||||
if (bw <= 0 || bh <= 0 || radius < 1) return;
|
||||
uint8_t *buffer = malloc((size_t)bw * bh * 3), *scratch = malloc((size_t)bw * bh * 3);
|
||||
if (buffer && scratch) {
|
||||
for (int py = 0; py < bh; py++)
|
||||
for (int px = 0; px < bw; px++) {
|
||||
uint32_t v = c->px[(size_t)(y0 + py) * c->w + x0 + px];
|
||||
uint8_t *o = buffer + ((size_t)py * bw + px) * 3;
|
||||
o[0] = (uint8_t)(v >> 16); o[1] = (uint8_t)(v >> 8); o[2] = (uint8_t)v;
|
||||
}
|
||||
for (int pass = 0; pass < 3; pass++) {
|
||||
c4fBoxPass(buffer, scratch, bw, bh, radius, 1);
|
||||
c4fBoxPass(scratch, buffer, bw, bh, radius, 0);
|
||||
}
|
||||
const float cx = x + w / 2, cy = y + h / 2;
|
||||
for (int py = 0; py < bh; py++)
|
||||
for (int px = 0; px < bw; px++) {
|
||||
int gx = x0 + px, gy = y0 + py;
|
||||
float cov = c4fClamp(0.5f - c4fRoundBoxDistance(gx + 0.5f, gy + 0.5f, cx, cy, w / 2, h / 2, r));
|
||||
if (cov <= 0) continue;
|
||||
uint32_t *p = &c->px[(size_t)gy * c->w + gx], out = 0xff000000u;
|
||||
const uint8_t *b = buffer + ((size_t)py * bw + px) * 3;
|
||||
float noise = c4fNoise(gx, gy);
|
||||
for (int k = 0; k < 3; k++) {
|
||||
float old = (float)((*p >> (16 - 8 * k)) & 255u);
|
||||
int v = (int)floorf(old + (b[k] - old) * cov + noise + 0.5f);
|
||||
out |= (uint32_t)c4fClampI(v, 0, 255) << (16 - 8 * k);
|
||||
}
|
||||
*p = out;
|
||||
}
|
||||
}
|
||||
free(buffer);
|
||||
free(scratch);
|
||||
}
|
||||
|
||||
/* ---- pieces of the page ---- */
|
||||
|
||||
static float c4fCapsuleDistance(float x, float y, float ax, float ay, float bx, float by, float r)
|
||||
{
|
||||
float vx = bx - ax, vy = by - ay, fx = x - ax, fy = y - ay;
|
||||
float t = c4fClamp((fx * vx + fy * vy) / (vx * vx + vy * vy));
|
||||
return hypotf(fx - t * vx, fy - t * vy) - r;
|
||||
}
|
||||
|
||||
/* The page's controller mark (.app-mark, viewBox 0 0 32 32) from simple shapes. */
|
||||
static float c4fMarkDistance(float x, float y)
|
||||
{
|
||||
float body = c4fRoundBoxDistance(x, y, 16, 15, 12.9f, 5, 5);
|
||||
float grips = fminf(c4fCapsuleDistance(x, y, 6.4f, 16.5f, 7.6f, 21.2f, 3.3f),
|
||||
c4fCapsuleDistance(x, y, 25.6f, 16.5f, 24.4f, 21.2f, 3.3f));
|
||||
float dpad = fminf(c4fRoundBoxDistance(x, y, 11, 17, 1, 3, 0.3f), c4fRoundBoxDistance(x, y, 11, 17, 3, 1, 0.3f));
|
||||
float buttons = fminf(hypotf(x - 21.5f, y - 15.5f), hypotf(x - 19, y - 19)) - 1.5f;
|
||||
return fmaxf(fminf(body, grips), -fminf(dpad, buttons));
|
||||
}
|
||||
|
||||
static void c4fMark(C4fCanvas *c, float cx, float cy, float unit, C4fColor color)
|
||||
{
|
||||
for (int y = (int)floorf(cy - 8 * unit); y <= (int)ceilf(cy + 8 * unit); y++)
|
||||
for (int x = (int)floorf(cx - 14 * unit); x <= (int)ceilf(cx + 14 * unit); x++) {
|
||||
float d = c4fMarkDistance(16 + (x + 0.5f - cx) / unit, 17.3f + (y + 0.5f - cy) / unit) * unit;
|
||||
float cov = c4fClamp(0.5f - d);
|
||||
if (cov > 0) c4fBlend(c, x, y, color, cov);
|
||||
}
|
||||
}
|
||||
|
||||
static void c4fTextCentered(C4fCanvas *c, int font, float size, float cx, float y, C4fColor color, const char *text)
|
||||
{
|
||||
c4fText(c, font, size, cx - c4fTextWidth(font, size, text) / 2, y, color, text);
|
||||
}
|
||||
|
||||
/* .tile: --tile with a light sheen, a 1px line, a drop shadow and the colour bar. */
|
||||
static void c4fTile(C4fCanvas *c, float x, float y, float w, float h, C4fColor bar)
|
||||
{
|
||||
const float r = 6 * C4F_S;
|
||||
c4fShadowRoundRect(c, x, y, w, h, r, 0, 16 * C4F_S, 34 * C4F_S, -18 * C4F_S, C4F_RGBA(0, 0, 0, .9f));
|
||||
c4fFillRoundRect(c, x, y, w, h, r, C4F_TILE);
|
||||
c4fSheenRoundRect(c, x, y, w, h, r, 160, C4F_WHITE, .13f, .02f, .55f);
|
||||
c4fInsetRoundRect(c, x, y, w, h, r, 1 * C4F_S, C4F_LINE);
|
||||
bar.a = (uint8_t)(bar.a * .9f);
|
||||
c4fBandRoundRect(c, x, y, w, h, r, 3 * C4F_S, bar);
|
||||
}
|
||||
|
||||
/* .panel */
|
||||
static void c4fPanel(C4fCanvas *c, float x, float y, float w, float h, C4fColor fill, C4fColor line)
|
||||
{
|
||||
c4fFillRoundRect(c, x, y, w, h, 6 * C4F_S, fill);
|
||||
c4fInsetRoundRect(c, x, y, w, h, 6 * C4F_S, 1 * C4F_S, line);
|
||||
}
|
||||
|
||||
/* .ring: the numbered circle on a controller tile. */
|
||||
static void c4fRingBadge(C4fCanvas *c, float cx, float cy, C4fColor color, const char *label)
|
||||
{
|
||||
const float r = 26 * C4F_S, hx = cx, hy = cy - 0.3f * r, far = hypotf(r, 1.3f * r);
|
||||
c4fGlowCircle(c, cx, cy, r, 18 * C4F_S, -4 * C4F_S, color);
|
||||
/* radial-gradient(circle at 50% 35%, rgba(255, 255, 255, .18), rgba(255, 255, 255, .03)) */
|
||||
for (int y = (int)floorf(cy - r - 1); y <= (int)ceilf(cy + r + 1); y++)
|
||||
for (int x = (int)floorf(cx - r - 1); x <= (int)ceilf(cx + r + 1); x++) {
|
||||
float inside = c4fClamp(0.5f - (hypotf(x + 0.5f - cx, y + 0.5f - cy) - r));
|
||||
if (inside <= 0) continue;
|
||||
float t = c4fClamp(hypotf(x + 0.5f - hx, y + 0.5f - hy) / far);
|
||||
c4fBlend(c, x, y, C4F_RGBA(255, 255, 255, .18f + (.03f - .18f) * t), inside);
|
||||
}
|
||||
c4fRing(c, cx, cy, r, 3 * C4F_S, color);
|
||||
c4fTextCentered(c, C4F_FONT_LIGHT, 24 * C4F_S, cx, cy + 24 * C4F_S * 0.36f, C4F_TEXT, label);
|
||||
}
|
||||
|
||||
/* .dot, with the glow the page gives a live connection. */
|
||||
static void c4fDot(C4fCanvas *c, float cx, float cy, C4fColor color, int glow)
|
||||
{
|
||||
if (glow) c4fGlowCircle(c, cx, cy, 4 * C4F_S, 10 * C4F_S, 0, color);
|
||||
c4fFillCircle(c, cx, cy, 4 * C4F_S, color);
|
||||
}
|
||||
|
||||
/* Button guide in the PS4's manner, with the page's face-button colours. */
|
||||
enum { C4F_KEY_CROSS, C4F_KEY_CIRCLE, C4F_KEY_SQUARE };
|
||||
typedef struct { int key; const char *label; } C4fHint;
|
||||
#define C4F_HINT_RADIUS 22.0f
|
||||
#define C4F_HINT_GAP 14.0f
|
||||
#define C4F_HINT_SPACING 40.0f
|
||||
#define C4F_HINT_SIZE 26.0f
|
||||
|
||||
static void c4fHints(C4fCanvas *c, float right, float cy, const C4fHint *hints, int count)
|
||||
{
|
||||
float x = right;
|
||||
for (int i = 0; i < count; i++)
|
||||
x -= (i ? C4F_HINT_SPACING : 0) + 2 * C4F_HINT_RADIUS + C4F_HINT_GAP +
|
||||
c4fTextWidth(C4F_FONT_REGULAR, C4F_HINT_SIZE, hints[i].label);
|
||||
for (int i = 0; i < count; i++) {
|
||||
float cx = x + C4F_HINT_RADIUS;
|
||||
c4fFillCircle(c, cx, cy, C4F_HINT_RADIUS, C4F_RGBA(255, 255, 255, .08f));
|
||||
c4fRing(c, cx, cy, C4F_HINT_RADIUS, 1 * C4F_S, C4F_LINE_SOFT);
|
||||
if (hints[i].key == C4F_KEY_CROSS) {
|
||||
c4fSegment(c, cx - 8.5f, cy - 8.5f, cx + 8.5f, cy + 8.5f, 3.6f, C4F_CROSS_KEY);
|
||||
c4fSegment(c, cx - 8.5f, cy + 8.5f, cx + 8.5f, cy - 8.5f, 3.6f, C4F_CROSS_KEY);
|
||||
} else if (hints[i].key == C4F_KEY_CIRCLE) {
|
||||
c4fRing(c, cx, cy, 10.5f, 3.6f, C4F_CIRCLE_KEY);
|
||||
} else {
|
||||
c4fSquareOutline(c, cx, cy, 8.5f, 3.4f, C4F_SQUARE_KEY);
|
||||
}
|
||||
x += 2 * C4F_HINT_RADIUS + C4F_HINT_GAP;
|
||||
x += c4fText(c, C4F_FONT_REGULAR, C4F_HINT_SIZE, x, cy + C4F_HINT_SIZE * 0.36f, C4F_TEXT, hints[i].label);
|
||||
x += C4F_HINT_SPACING;
|
||||
}
|
||||
}
|
||||
|
||||
static void c4fQrCard(C4fCanvas *c, float x, float y, float size, const char *address)
|
||||
{
|
||||
static char lastAddress[64];
|
||||
static uint8_t qr[qrcodegen_BUFFER_LEN_FOR_VERSION(5)];
|
||||
static int qrSize;
|
||||
if (strcmp(lastAddress, address)) {
|
||||
uint8_t temp[sizeof(qr)];
|
||||
qrSize = qrcodegen_encodeText(address, temp, qr, qrcodegen_Ecc_MEDIUM, 1, 5, qrcodegen_Mask_AUTO, true)
|
||||
? qrcodegen_getSize(qr) : 0;
|
||||
snprintf(lastAddress, sizeof(lastAddress), "%s", address);
|
||||
}
|
||||
c4fFillRoundRect(c, x, y, size, size, 6 * C4F_S, C4F_WHITE);
|
||||
if (!qrSize) return;
|
||||
/* Whole pixels per module and a four-module quiet zone keep it scannable. */
|
||||
int scale = (int)(size / (float)(qrSize + 8)), drawn = qrSize * scale;
|
||||
int left = (int)(x + (size - drawn) / 2), top = (int)(y + (size - drawn) / 2);
|
||||
for (int row = 0; row < qrSize; row++)
|
||||
for (int col = 0; col < qrSize; col++)
|
||||
if (qrcodegen_getModule(qr, col, row))
|
||||
c4fFillRect(c, left + col * scale, top + row * scale, scale, scale, C4F_RGBA(4, 14, 40, 1));
|
||||
}
|
||||
|
||||
/* ---- the screen ---- */
|
||||
|
||||
typedef struct {
|
||||
const char *pill, *title, *subtitle, *meta;
|
||||
C4fColor dot, bar;
|
||||
int glow;
|
||||
} C4fLook;
|
||||
|
||||
static C4fLook c4fLookFor(const C4fLauncherScreen *s, char *meta, size_t metaSize)
|
||||
{
|
||||
C4fLook look = { "Not responding", "Check Control4Free",
|
||||
"Another program answers on port 4264, or nothing replied in time. Stop the old copy from "
|
||||
"its phone page, or restart the PS4.",
|
||||
"Not responding", C4F_BAD, C4F_BAD, 0 };
|
||||
if (s->locked) {
|
||||
look.pill = look.meta = "Restart needed";
|
||||
look.title = "Restart the PS4";
|
||||
look.subtitle = "Control4Free was sent but never answered. Restart the console before you try again, so "
|
||||
"two copies never run at once.";
|
||||
} else if (s->busy) {
|
||||
look = (C4fLook){ "Working", "Please wait", s->message, "Checking", C4F_WARN, C4F_ACCENT, 0 };
|
||||
} else if (s->running == 1) {
|
||||
if (s->controllers == 1) snprintf(meta, metaSize, "1 controller connected");
|
||||
else snprintf(meta, metaSize, "%d controllers connected", s->controllers);
|
||||
look = (C4fLook){ "Running", "Control4Free is running",
|
||||
"Open the address or scan the code on your phone or PC. Pick a controller there, then "
|
||||
"choose its user on the PS4.",
|
||||
meta, C4F_OK, C4F_ACCENT, 1 };
|
||||
} else if (s->running == 0) {
|
||||
look = (C4fLook){ "Stopped", "Start Control4Free",
|
||||
"Press Cross with GoldHEN's PayLoader turned on, or start it from GoldHEN's Payloader "
|
||||
"LaunchPad. Add it to AutoRun there and it starts with GoldHEN.",
|
||||
"Not running", C4F_FAINT, C4F_FAINT, 0 };
|
||||
}
|
||||
return look;
|
||||
}
|
||||
|
||||
void c4fDrawLauncher(uint32_t *pixels, const C4fLauncherScreen *s)
|
||||
{
|
||||
C4fCanvas c = { pixels, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
|
||||
const float left = C4F_LEFT, right = C4F_RIGHT, top = C4F_ROW_TOP, height = C4F_ROW_HEIGHT;
|
||||
const float qrWidth = C4F_QR_WIDTH, mainWidth = C4F_MAIN_WIDTH, pad = 16 * C4F_S;
|
||||
char meta[48], count[8];
|
||||
C4fLook look = c4fLookFor(s, meta, sizeof(meta));
|
||||
int live = s->running == 1 && !s->locked;
|
||||
|
||||
if (c4fBackdrop) memcpy(pixels, c4fBackdrop, (size_t)c.w * (size_t)c.h * sizeof(uint32_t));
|
||||
else memset(pixels, 0, (size_t)c.w * (size_t)c.h * sizeof(uint32_t));
|
||||
|
||||
/* .topbar: the app mark, the name and the state. */
|
||||
c4fMark(&c, 64 + 13 * C4F_S, 54, 26 * C4F_S / 32, C4F_WHITE);
|
||||
float x = 64 + 26 * C4F_S + 10 * C4F_S;
|
||||
x += c4fText(&c, C4F_FONT_LIGHT, 17 * C4F_S, x, 63, C4F_TEXT, "Control4Free") + 14 * C4F_S;
|
||||
c4fDot(&c, x + 4 * C4F_S, 55, look.dot, look.glow);
|
||||
c4fText(&c, C4F_FONT_LIGHT, 13 * C4F_S, x + 16 * C4F_S, 62, C4F_MUTED, look.pill);
|
||||
|
||||
/* .screen-title and .screen-sub */
|
||||
c4fText(&c, C4F_FONT_LIGHT, 40 * C4F_S, left, 196, C4F_TEXT, look.title);
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 26, left, 250, 1500, 39, 2, C4F_MUTED, look.subtitle);
|
||||
|
||||
/* Where to connect, as a controller tile. */
|
||||
c4fTile(&c, left, top, mainWidth, height, look.bar);
|
||||
if (s->running == 1) snprintf(count, sizeof(count), "%d", s->controllers);
|
||||
else snprintf(count, sizeof(count), "-");
|
||||
c4fRingBadge(&c, left + pad + 26 * C4F_S, top + pad + 26 * C4F_S + 4, look.bar, count);
|
||||
float textX = left + pad + 52 * C4F_S + 22;
|
||||
c4fText(&c, C4F_FONT_REGULAR, 30, textX, top + 66, C4F_TEXT, "Open on your phone or PC");
|
||||
c4fText(&c, C4F_FONT_LIGHT, 23, textX, top + 102, C4F_MUTED, look.meta);
|
||||
|
||||
float fieldX = left + pad, fieldY = top + 150, fieldWidth = mainWidth - 2 * pad, fieldHeight = 88;
|
||||
c4fFillRoundRect(&c, fieldX, fieldY, fieldWidth, fieldHeight, 4 * C4F_S, C4F_RGBA(0, 0, 0, .3f));
|
||||
c4fInsetRoundRect(&c, fieldX, fieldY, fieldWidth, fieldHeight, 4 * C4F_S, 1 * C4F_S, C4F_LINE);
|
||||
if (s->address[0])
|
||||
c4fText(&c, C4F_FONT_REGULAR, 40, fieldX + 28, fieldY + fieldHeight / 2 + 14, live ? C4F_TEXT : C4F_MUTED, s->address);
|
||||
else
|
||||
c4fText(&c, C4F_FONT_LIGHT, 28, fieldX + 28, fieldY + fieldHeight / 2 + 10, C4F_MUTED,
|
||||
"Connect the PS4 to your home network");
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 24, fieldX, fieldY + fieldHeight + 44, fieldWidth, 36, 2, C4F_MUTED,
|
||||
"Use the same network as the PS4. Touch, a keyboard and controllers connected to the phone "
|
||||
"or PC all work.");
|
||||
|
||||
/* The last result along the bottom of the tile. */
|
||||
float lineY = top + height - 104;
|
||||
c4fFillRoundRect(&c, fieldX, lineY, fieldWidth, 1 * C4F_S, 0, C4F_LINE_SOFT);
|
||||
c4fDot(&c, fieldX + 4 * C4F_S, lineY + 44, look.dot, look.glow);
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 24, fieldX + 24, lineY + 52, fieldWidth - 24, 34, 2, C4F_TEXT,
|
||||
s->busy ? "Working..." : s->message);
|
||||
|
||||
/* The QR code tile. */
|
||||
float qrX = C4F_QR_LEFT, card = 340, cardX = qrX + (qrWidth - card) / 2, cardY = top + 34;
|
||||
c4fTile(&c, qrX, top, qrWidth, height, look.bar);
|
||||
if (live && s->address[0]) {
|
||||
c4fQrCard(&c, cardX, cardY, card, s->address);
|
||||
c4fTextCentered(&c, C4F_FONT_LIGHT, 24, qrX + qrWidth / 2, cardY + card + 50, C4F_MUTED,
|
||||
"Scan with your phone's camera");
|
||||
} else {
|
||||
c4fFillRoundRect(&c, cardX, cardY, card, card, 6 * C4F_S, C4F_RGBA(0, 0, 0, .3f));
|
||||
c4fInsetRoundRect(&c, cardX, cardY, card, card, 6 * C4F_S, 1 * C4F_S, C4F_LINE_SOFT);
|
||||
c4fMark(&c, cardX + card / 2, cardY + card / 2, 5, C4F_FAINT);
|
||||
c4fTextCentered(&c, C4F_FONT_LIGHT, 24, qrX + qrWidth / 2, cardY + card + 50, C4F_MUTED,
|
||||
"The code appears once it runs");
|
||||
}
|
||||
|
||||
/* .panel with the one thing worth knowing. */
|
||||
float panelY = C4F_PANEL_TOP;
|
||||
c4fPanel(&c, left, panelY, right - left, C4F_PANEL_HEIGHT, C4F_PANEL, C4F_LINE_SOFT);
|
||||
c4fText(&c, C4F_FONT_LIGHT, 19 * C4F_S, left + 20 * C4F_S, panelY + 52, C4F_TEXT,
|
||||
"Closing this app leaves your controllers running");
|
||||
c4fTextWrapped(&c, C4F_FONT_LIGHT, 23, left + 20 * C4F_S, panelY + 94, right - left - 40 * C4F_S, 34, 1, C4F_MUTED,
|
||||
"Open it again any time to see the address or to stop Control4Free. Turn off other "
|
||||
"controller plugins in your games.");
|
||||
|
||||
/* .home-foot and the button guide. */
|
||||
c4fText(&c, C4F_FONT_LIGHT, 20, left, 1030, C4F_FAINT, "Control4Free " C4F_LAUNCHER_VERSION);
|
||||
if (!s->confirmStop && !s->busy) {
|
||||
C4fHint hints[2];
|
||||
int n = 0;
|
||||
if (!s->locked) {
|
||||
hints[n].key = live ? C4F_KEY_SQUARE : C4F_KEY_CROSS;
|
||||
hints[n++].label = live ? "Stop" : "Start";
|
||||
}
|
||||
hints[n].key = C4F_KEY_CIRCLE;
|
||||
hints[n++].label = "Close";
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void c4fDrawIcon(uint32_t *pixels, int size)
|
||||
{
|
||||
C4fCanvas c = { pixels, size, size };
|
||||
c4fPaintBackdrop(&c, size / 360.0f);
|
||||
c4fMark(&c, size / 2.0f, size * 0.40f, size * 0.60f / 26, C4F_WHITE);
|
||||
c4fTextCentered(&c, C4F_FONT_LIGHT, size * 0.105f, size / 2.0f, size * 0.80f, C4F_TEXT, "Control4Free");
|
||||
}
|
||||
|
||||
int c4fScreenInit(void)
|
||||
{
|
||||
if (c4fDrawInit()) return -1;
|
||||
if (!c4fBackdrop) {
|
||||
c4fBackdrop = malloc((size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT * sizeof(uint32_t));
|
||||
if (!c4fBackdrop) return -1;
|
||||
C4fCanvas c = { c4fBackdrop, C4F_SCREEN_WIDTH, C4F_SCREEN_HEIGHT };
|
||||
c4fPaintBackdrop(&c, C4F_S);
|
||||
/* .tile has backdrop-filter: blur(6px), .panel blur(10px). */
|
||||
c4fBlurUnder(&c, C4F_LEFT, C4F_ROW_TOP, C4F_MAIN_WIDTH, C4F_ROW_HEIGHT, 6 * C4F_S, 6 * C4F_S);
|
||||
c4fBlurUnder(&c, C4F_QR_LEFT, C4F_ROW_TOP, C4F_QR_WIDTH, C4F_ROW_HEIGHT, 6 * C4F_S, 6 * C4F_S);
|
||||
c4fBlurUnder(&c, C4F_LEFT, C4F_PANEL_TOP, C4F_RIGHT - C4F_LEFT, C4F_PANEL_HEIGHT, 6 * C4F_S, 10 * C4F_S);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef C4F_LAUNCHER_SCREEN_H
|
||||
#define C4F_LAUNCHER_SCREEN_H
|
||||
#include <stdint.h>
|
||||
#define C4F_LAUNCHER_VERSION "0.2.0"
|
||||
#define C4F_SCREEN_WIDTH 1920
|
||||
#define C4F_SCREEN_HEIGHT 1080
|
||||
typedef struct {
|
||||
int running, busy, controllers, confirmStop, locked;
|
||||
char address[64], message[160];
|
||||
} C4fLauncherScreen;
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* Loads the fonts and paints the backdrop once. 0 on success. */
|
||||
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);
|
||||
/* The package icon (size x size), in the same style. */
|
||||
void c4fDrawIcon(uint32_t *pixels, int size);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,202 @@
|
||||
/* Local-only launcher transport. No controller creation or input injection. */
|
||||
#include "service.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#define JSMN_STATIC
|
||||
#define JSMN_STRICT
|
||||
#include "jsmn.h"
|
||||
|
||||
#ifndef C4F_SERVICE_PORT
|
||||
#define C4F_SERVICE_PORT 4264
|
||||
#endif
|
||||
#ifndef C4F_PAYLOADER_PORT
|
||||
#define C4F_PAYLOADER_PORT 9090
|
||||
#endif
|
||||
|
||||
uint64_t c4fLauncherTimeMs(void)
|
||||
{
|
||||
struct timespec t;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
return (uint64_t)t.tv_sec * 1000 + t.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
static int c4fWaitSocket(int fd, int writeReady, uint64_t deadline)
|
||||
{
|
||||
for (;;) {
|
||||
uint64_t now = c4fLauncherTimeMs();
|
||||
if (now >= deadline) { errno = ETIMEDOUT; return -1; }
|
||||
fd_set set; FD_ZERO(&set); FD_SET(fd, &set);
|
||||
unsigned remaining = (unsigned)(deadline - now);
|
||||
struct timeval tv = { remaining / 1000, (remaining % 1000) * 1000 };
|
||||
int ret = select(fd + 1, writeReady ? NULL : &set, writeReady ? &set : NULL, NULL, &tv);
|
||||
if (ret > 0) return 0;
|
||||
if (ret < 0 && errno == EINTR) continue;
|
||||
if (ret == 0) errno = ETIMEDOUT;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static int c4fConnect(int port)
|
||||
{
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
int savedError;
|
||||
if (fd >= (int)FD_SETSIZE || fcntl(fd, F_SETFL, O_NONBLOCK) < 0) goto fail;
|
||||
#ifdef SO_NOSIGPIPE
|
||||
int one = 1;
|
||||
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof(one));
|
||||
#endif
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET; addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0) return fd;
|
||||
if (errno != EINPROGRESS) goto fail;
|
||||
if (c4fWaitSocket(fd, 1, c4fLauncherTimeMs() + 700)) goto fail;
|
||||
int error = 0; socklen_t size = sizeof(error);
|
||||
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &size) < 0) goto fail;
|
||||
if (error) { errno = error; goto fail; }
|
||||
return fd;
|
||||
fail:
|
||||
savedError = errno; close(fd); errno = savedError; return -1;
|
||||
}
|
||||
|
||||
static int c4fSendAll(int fd, const void *data, size_t size, uint64_t deadline)
|
||||
{
|
||||
const char *p = data;
|
||||
while (size) {
|
||||
if (c4fWaitSocket(fd, 1, deadline)) return -1;
|
||||
int flags = 0;
|
||||
#ifdef MSG_NOSIGNAL
|
||||
flags = MSG_NOSIGNAL;
|
||||
#endif
|
||||
ssize_t n = send(fd, p, size, flags);
|
||||
if (n < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
|
||||
if (n <= 0) return -1;
|
||||
p += n; size -= (size_t)n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int c4fHttp(const char *method, const char *path, char *body, size_t capacity)
|
||||
{
|
||||
int fd = c4fConnect(C4F_SERVICE_PORT);
|
||||
if (fd < 0) return errno == ECONNREFUSED ? 0 : -1;
|
||||
char request[256], response[2048];
|
||||
int n = snprintf(request, sizeof(request), "%s %s HTTP/1.1\r\nHost: 127.0.0.1:%d\r\nX-Control4Free-Launcher: 1\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", method, path, C4F_SERVICE_PORT);
|
||||
uint64_t deadline = c4fLauncherTimeMs() + 1500;
|
||||
if (c4fSendAll(fd, request, (size_t)n, deadline)) { close(fd); return -1; }
|
||||
size_t used = 0;
|
||||
int complete = 0;
|
||||
while (used < sizeof(response) - 1) {
|
||||
if (c4fWaitSocket(fd, 0, deadline)) break;
|
||||
ssize_t count = recv(fd, response + used, sizeof(response) - 1 - used, 0);
|
||||
if (count < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
|
||||
if (count == 0) { complete = 1; break; }
|
||||
if (count < 0) break;
|
||||
used += (size_t)count;
|
||||
}
|
||||
close(fd); response[used] = 0;
|
||||
char *start = strstr(response, "\r\n\r\n");
|
||||
if (!complete || strncmp(response, "HTTP/1.1 200 OK\r\n", 17) || !start) return -1;
|
||||
start += 4;
|
||||
size_t length = used - (size_t)(start - response);
|
||||
if (!length || length >= capacity || memchr(start, 0, length)) return -1;
|
||||
memcpy(body, start, length); body[length] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int c4fEquals(const char *json, const jsmntok_t *token, const char *value)
|
||||
{
|
||||
return token->end - token->start == (int)strlen(value) && !memcmp(json + token->start, value, strlen(value));
|
||||
}
|
||||
|
||||
int c4fLauncherProbe(C4fServiceStatus *status)
|
||||
{
|
||||
char body[512]; jsmntok_t tokens[24]; jsmn_parser parser;
|
||||
memset(status, 0, sizeof(*status));
|
||||
int result = c4fHttp("GET", "/api/status", body, sizeof(body));
|
||||
if (result <= 0) return result;
|
||||
jsmn_init(&parser);
|
||||
int count = jsmn_parse(&parser, body, strlen(body), tokens, 24);
|
||||
if (count < 1 || tokens[0].type != JSMN_OBJECT || tokens[0].end != (int)strlen(body)) return -1;
|
||||
int app = 0, api = 0, controllers = 0, stopping = 0;
|
||||
for (int i = 1; i + 1 < count; i += 2) {
|
||||
jsmntok_t *key = &tokens[i], *value = &tokens[i+1];
|
||||
if (key->type != JSMN_STRING || (value->type != JSMN_STRING && value->type != JSMN_PRIMITIVE)) return -1;
|
||||
if (c4fEquals(body, key, "application")) app = value->type == JSMN_STRING && c4fEquals(body, value, "Control4Free");
|
||||
else if (c4fEquals(body, key, "api")) api = value->type == JSMN_PRIMITIVE && c4fEquals(body, value, "1");
|
||||
else if (c4fEquals(body, key, "controllers")) {
|
||||
if (value->type != JSMN_PRIMITIVE || value->end - value->start != 1 || body[value->start] < '0' || body[value->start] > '4') return -1;
|
||||
status->controllers = body[value->start] - '0'; controllers = 1;
|
||||
} else if (c4fEquals(body, key, "stopping")) {
|
||||
if (value->type != JSMN_PRIMITIVE || (!c4fEquals(body, value, "true") && !c4fEquals(body, value, "false"))) return -1;
|
||||
status->stopping = c4fEquals(body, value, "true"); stopping = 1;
|
||||
} else if (c4fEquals(body, key, "version")) {
|
||||
int size = value->end - value->start;
|
||||
if (value->type != JSMN_STRING || size >= (int)sizeof(status->version)) return -1;
|
||||
memcpy(status->version, body + value->start, (size_t)size);
|
||||
}
|
||||
}
|
||||
return app && api && controllers && stopping ? 1 : -1;
|
||||
}
|
||||
|
||||
int c4fLauncherStart(const char *payload, char *message, size_t size)
|
||||
{
|
||||
C4fServiceStatus status;
|
||||
int probe = c4fLauncherProbe(&status);
|
||||
if (probe == 1) { snprintf(message, size, "Already running. Scan the code to connect."); return 0; }
|
||||
if (probe != 0) { snprintf(message, size, "Existing service detected. Stop it from the phone first."); return -1; }
|
||||
FILE *file = fopen(payload, "rb");
|
||||
if (!file) { snprintf(message, size, "Payload missing. Reinstall the Control4Free package."); return -1; }
|
||||
unsigned char buffer[16384];
|
||||
size_t first = fread(buffer, 1, sizeof(buffer), file);
|
||||
if (first < 64 || memcmp(buffer, "\177ELF", 4) || buffer[4] != 2 || buffer[5] != 1) {
|
||||
fclose(file); snprintf(message, size, "Invalid bundled payload. Reinstall this package."); return -1;
|
||||
}
|
||||
int fd = c4fConnect(C4F_PAYLOADER_PORT);
|
||||
if (fd < 0) { fclose(file); snprintf(message, size, "Turn on GoldHEN's PayLoader, then press Cross again."); return -1; }
|
||||
uint64_t deadline = c4fLauncherTimeMs() + 10000;
|
||||
int failed = c4fSendAll(fd, buffer, first, deadline);
|
||||
while (!failed) {
|
||||
size_t count = fread(buffer, 1, sizeof(buffer), file);
|
||||
if (!count) { failed = ferror(file); break; }
|
||||
failed = c4fSendAll(fd, buffer, count, deadline);
|
||||
}
|
||||
fclose(file); shutdown(fd, SHUT_WR); close(fd);
|
||||
if (failed) { snprintf(message, size, "Transfer interrupted. Restart the PS4 before retrying."); return -2; }
|
||||
deadline = c4fLauncherTimeMs() + 20000;
|
||||
while (c4fLauncherTimeMs() < deadline) {
|
||||
if (c4fLauncherProbe(&status) == 1 && !status.stopping) {
|
||||
snprintf(message, size, "Ready. Scan the code or open the address on your phone."); return 0;
|
||||
}
|
||||
usleep(250000);
|
||||
}
|
||||
snprintf(message, size, "Sent, but no reply. Restart the PS4 before retrying.");
|
||||
return -2; /* Caller locks further sends until restart, avoiding duplicates. */
|
||||
}
|
||||
|
||||
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 != 1) { snprintf(message, size, "Older service detected. Stop it from the phone."); return -1; }
|
||||
char reply[256];
|
||||
int ret = c4fHttp("POST", "/api/stop", reply, sizeof(reply));
|
||||
if (ret != 1) { snprintf(message, size, "Stop was not confirmed. Check the phone page."); 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; }
|
||||
usleep(100000);
|
||||
}
|
||||
snprintf(message, size, "Still stopping. Wait before trying again."); return -1;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef C4F_LAUNCHER_SERVICE_H
|
||||
#define C4F_LAUNCHER_SERVICE_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
enum { C4F_SERVICE_UNKNOWN = -1, C4F_SERVICE_DOWN = 0, C4F_SERVICE_RUNNING = 1 };
|
||||
typedef struct { int controllers, stopping; char version[24]; } C4fServiceStatus;
|
||||
uint64_t c4fLauncherTimeMs(void);
|
||||
/* DOWN is returned only for a refused local connection. Ambiguous responses
|
||||
* must not authorize starting another copy in the shared payload host. */
|
||||
int c4fLauncherProbe(C4fServiceStatus *status);
|
||||
int c4fLauncherStart(const char *payload, char *message, size_t size);
|
||||
int c4fLauncherStop(char *message, size_t size);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,3 @@
|
||||
/* stb_truetype's implementation, built on its own with warnings off (vendor code). */
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include "stb_truetype.h"
|
||||
@@ -0,0 +1,15 @@
|
||||
<# Build the payload and installable PS4 launcher locally. Requires Docker. #>
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||
Push-Location $projectRoot
|
||||
try {
|
||||
docker build -t control4free-build -f docker/Dockerfile docker
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Payload toolchain build failed.' }
|
||||
docker build -t control4free-launcher-build -f docker/Dockerfile.launcher docker
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Launcher toolchain build failed.' }
|
||||
docker run --rm --network none -v "${projectRoot}:/src" -w /src control4free-launcher-build bash -lc 'make && make -C launcher'
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Launcher package build failed.' }
|
||||
Get-Item -LiteralPath (Join-Path $projectRoot 'build/Control4Free-0.2.0.pkg') | Select-Object FullName,Length
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Embed binary files in a C source: embed_file.py out.c symbol=path [symbol=path ...]"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
out, pairs = Path(sys.argv[1]), [arg.split('=', 1) for arg in sys.argv[2:]]
|
||||
lines = ['/* Generated by tools/embed_file.py; do not edit. */', '#include <stddef.h>']
|
||||
for symbol, path in pairs:
|
||||
data = Path(path).read_bytes()
|
||||
lines.append(f'/* {Path(path).name} */')
|
||||
lines.append(f'__attribute__((aligned(4))) const unsigned char {symbol}[] = {{')
|
||||
for i in range(0, len(data), 24):
|
||||
lines.append(''.join(f'{b},' for b in data[i:i + 24]))
|
||||
lines.append('};')
|
||||
lines.append(f'const size_t {symbol}Size = {len(data)};')
|
||||
out.write_text('\n'.join(lines) + '\n')
|
||||
@@ -0,0 +1,56 @@
|
||||
/* 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 <running|stopped|busy|confirm|locked|unknown> <out.rgba> */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "screen.h"
|
||||
|
||||
static int c4fWrite(const char *path, const uint32_t *pixels, size_t count)
|
||||
{
|
||||
FILE *out = fopen(path, "wb");
|
||||
if (!out) return 1;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
unsigned char rgba[4] = { (unsigned char)(pixels[i] >> 16), (unsigned char)(pixels[i] >> 8),
|
||||
(unsigned char)pixels[i], (unsigned char)(pixels[i] >> 24) };
|
||||
fwrite(rgba, 1, 4, out);
|
||||
}
|
||||
return fclose(out) != 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc != 4 || c4fScreenInit()) return 2;
|
||||
if (!strcmp(argv[1], "icon")) {
|
||||
int size = atoi(argv[2]);
|
||||
uint32_t *pixels = size > 0 && size <= 4096 ? calloc((size_t)size * size, sizeof(uint32_t)) : NULL;
|
||||
if (!pixels) return 2;
|
||||
c4fDrawIcon(pixels, size);
|
||||
return c4fWrite(argv[3], pixels, (size_t)size * size);
|
||||
}
|
||||
if (strcmp(argv[1], "screen")) return 2;
|
||||
C4fLauncherScreen s;
|
||||
memset(&s, 0, sizeof(s));
|
||||
snprintf(s.address, sizeof(s.address), "http://192.168.1.20:4264");
|
||||
const char *state = argv[2], *message = "";
|
||||
if (!strcmp(state, "running") || !strcmp(state, "confirm")) {
|
||||
s.running = 1; s.controllers = 2; s.confirmStop = !strcmp(state, "confirm");
|
||||
message = "Ready. Scan the code or open the address on your phone.";
|
||||
} else if (!strcmp(state, "stopped")) {
|
||||
message = "Not running. Press Cross to start it.";
|
||||
} else if (!strcmp(state, "busy")) {
|
||||
s.running = -1; s.busy = 1; message = "Starting Control4Free. Please wait...";
|
||||
} else if (!strcmp(state, "locked")) {
|
||||
s.running = -1; s.locked = 1; message = "Sent, but no reply. Restart the PS4 before retrying.";
|
||||
} else if (!strcmp(state, "unknown")) {
|
||||
s.running = -1; message = "Existing service detected. Stop it from the phone first.";
|
||||
} else {
|
||||
return 2;
|
||||
}
|
||||
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;
|
||||
c4fDrawLauncher(pixels, &s);
|
||||
return c4fWrite(argv[3], pixels, (size_t)C4F_SCREEN_WIDTH * C4F_SCREEN_HEIGHT);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
"""Build the native launcher package using OpenOrbis/LibOrbisPkg only."""
|
||||
import argparse
|
||||
import binascii
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import zlib
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT / 'build/launcher'
|
||||
STAGE = OUT / 'pkg'
|
||||
ART = OUT / 'launcher-art'
|
||||
FONTS = OUT / 'fonts.c'
|
||||
TITLE_ID = 'CFRE00001' # 4 letters + 5 digits, the usual title ID format
|
||||
CONTENT_ID = f'IV0000-{TITLE_ID}_00-CONTROL4FREE0000'
|
||||
assert len(CONTENT_ID) == 36
|
||||
|
||||
|
||||
def embed_fonts():
|
||||
sources = [ROOT / 'vendor/roboto/Roboto-Light.ttf', ROOT / 'vendor/roboto/Roboto-Regular.ttf']
|
||||
if FONTS.exists() and all(FONTS.stat().st_mtime >= s.stat().st_mtime for s in sources):
|
||||
return
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(['python3', str(ROOT / 'tools/embed_file.py'), str(FONTS),
|
||||
f'c4fFontLight={sources[0]}', f'c4fFontRegular={sources[1]}'], check=True)
|
||||
|
||||
|
||||
def build_art_tool():
|
||||
"""Host build of the launcher's drawing code, for the icon and previews."""
|
||||
embed_fonts()
|
||||
flags = ['clang-18', '-std=gnu11', '-O2', '-D_DEFAULT_SOURCE', f'-I{ROOT}/launcher',
|
||||
f'-I{ROOT}/vendor/stb', f'-I{ROOT}/vendor/qrcodegen']
|
||||
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 / 'vendor/qrcodegen/qrcodegen.c'), str(FONTS), str(stb), '-lm', '-o', str(ART)],
|
||||
check=True)
|
||||
|
||||
|
||||
def write_png(path, width, height, rgba, alpha=False):
|
||||
def chunk(kind, data):
|
||||
return struct.pack('>I', len(data)) + kind + data + struct.pack('>I', binascii.crc32(kind + data) & 0xffffffff)
|
||||
pixels = rgba if alpha else bytes(b for i, b in enumerate(rgba) if i % 4 != 3)
|
||||
stride = width * (4 if alpha else 3)
|
||||
scanlines = b''.join(b'\0' + pixels[y * stride:(y + 1) * stride] for y in range(height))
|
||||
header = struct.pack('>IIBBBBB', width, height, 8, 6 if alpha else 2, 0, 0, 0)
|
||||
Path(path).write_bytes(b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', header) +
|
||||
chunk(b'IDAT', zlib.compress(scanlines, 9)) + chunk(b'IEND', b''))
|
||||
|
||||
|
||||
def render(args, path, width, height):
|
||||
"""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)
|
||||
write_png(path, width, height, raw.read_bytes())
|
||||
raw.unlink()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--sdk', required=True, type=Path)
|
||||
sdk = parser.parse_args().sdk
|
||||
binary = sdk / 'bin/linux'
|
||||
# Start clean, so nothing left over from an older build gets packaged.
|
||||
shutil.rmtree(STAGE, ignore_errors=True)
|
||||
for directory in ('sce_sys/about', 'sce_module', 'assets', 'assets/licenses'):
|
||||
(STAGE / directory).mkdir(parents=True, exist_ok=True)
|
||||
run = lambda *args: subprocess.run([str(arg) for arg in args], cwd=STAGE, check=True)
|
||||
run(binary / 'create-fself', '-in=' + str(OUT / 'launcher.elf'), '-out=' + str(OUT / 'launcher.oelf'),
|
||||
'--eboot', 'eboot.bin', '--paid', '0x3800000000000011')
|
||||
shutil.copyfile(ROOT / 'build/control4free.elf', STAGE / 'assets/control4free.elf')
|
||||
shutil.copyfile(sdk / 'samples/graphics/sce_sys/about/right.sprx', STAGE / 'sce_sys/about/right.sprx')
|
||||
# OpenOrbis's stub modules. Its CHANGELOG (v0.5) says homebrew packages
|
||||
# should carry them in sce_module/, and every sample does.
|
||||
for name in ('libc.prx', 'libSceFios2.prx'):
|
||||
shutil.copyfile(sdk / 'samples/graphics/sce_module' / name, STAGE / 'sce_module' / name)
|
||||
licenses = STAGE / 'assets/licenses'
|
||||
shutil.copyfile(ROOT / 'LICENSE', licenses / 'Control4Free-GPL-3.0.txt')
|
||||
shutil.copyfile(ROOT / 'THIRD_PARTY.md', licenses / 'THIRD_PARTY.txt')
|
||||
shutil.copyfile(ROOT / 'vendor/jsmn/LICENSE', licenses / 'jsmn-MIT.txt')
|
||||
shutil.copyfile(ROOT / 'vendor/roboto/LICENSE', licenses / 'Roboto-Apache-2.0.txt')
|
||||
qrcodegen = (ROOT / 'vendor/qrcodegen/qrcodegen.h').read_text()
|
||||
(licenses / 'qrcodegen-MIT.txt').write_text(qrcodegen.split('*/', 1)[0] + '*/\n')
|
||||
stb = (ROOT / 'vendor/stb/stb_truetype.h').read_text()
|
||||
(licenses / 'stb_truetype.txt').write_text(stb[stb.index('This software is available under 2 licenses'):])
|
||||
# The home-screen icon comes from the launcher's own drawing code.
|
||||
build_art_tool()
|
||||
render(['icon', '512'], STAGE / 'sce_sys/icon0.png', 512, 512)
|
||||
pkgtool = binary / 'PkgTool.Core'
|
||||
sfo = 'sce_sys/param.sfo'
|
||||
run(pkgtool, 'sfo_new', sfo)
|
||||
entries = {
|
||||
'APP_TYPE': ('Integer', 4, '1'), 'APP_VER': ('Utf8', 8, '00.20'),
|
||||
'ATTRIBUTE': ('Integer', 4, '0'), 'CATEGORY': ('Utf8', 4, 'gd'),
|
||||
'CONTENT_ID': ('Utf8', 48, CONTENT_ID), 'DOWNLOAD_DATA_SIZE': ('Integer', 4, '0'),
|
||||
'SYSTEM_VER': ('Integer', 4, '0'), 'TITLE': ('Utf8', 128, 'Control4Free'),
|
||||
'TITLE_ID': ('Utf8', 12, TITLE_ID), 'VERSION': ('Utf8', 8, '00.20'),
|
||||
}
|
||||
for key, (kind, size, value) in entries.items():
|
||||
run(pkgtool, 'sfo_setentry', sfo, key, '--type', kind, '--maxsize', size, '--value', value)
|
||||
files = ['eboot.bin', 'sce_sys/param.sfo', 'sce_sys/icon0.png', 'sce_sys/about/right.sprx',
|
||||
'sce_module/libc.prx', 'sce_module/libSceFios2.prx', 'assets/control4free.elf']
|
||||
files += sorted(str(p.relative_to(STAGE)) for p in (STAGE / 'assets/licenses').iterdir())
|
||||
project = STAGE / 'launcher.gp4'
|
||||
run(binary / 'create-gp4', '-out', project, '--content-id=' + CONTENT_ID, '--files', ' '.join(files))
|
||||
# create-gp4 ships a sample directory tree. Describe the actual package,
|
||||
# including nested license files, and keep source paths relative to it.
|
||||
tree = ET.parse(project)
|
||||
rootdir = tree.getroot().find('rootdir')
|
||||
rootdir.clear()
|
||||
for filename in files:
|
||||
parent = rootdir
|
||||
for part in Path(filename).parts[:-1]:
|
||||
child = next((p for p in parent if p.get('targ_name') == part), None)
|
||||
if child is None:
|
||||
child = ET.SubElement(parent, 'dir', targ_name=part)
|
||||
parent = child
|
||||
ET.indent(tree)
|
||||
tree.write(project, encoding='utf-8', xml_declaration=True)
|
||||
shutil.copyfile(project, OUT / 'launcher.gp4')
|
||||
run(pkgtool, 'pkg_build', project, OUT)
|
||||
generated = OUT / (CONTENT_ID + '.pkg')
|
||||
target = ROOT / 'build/Control4Free-0.2.0.pkg'
|
||||
shutil.copyfile(generated, target)
|
||||
print(f'Built {target} ({target.stat().st_size:,} bytes)', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Vendored
+1022
File diff suppressed because it is too large.
Load diff
Vendored
+385
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
* QR Code generator library (C)
|
||||
*
|
||||
* Copyright (c) Project Nayuki. (MIT License)
|
||||
* https://www.nayuki.io/page/qr-code-generator-library
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
* - The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
* - The Software is provided "as is", without warranty of any kind, express or
|
||||
* implied, including but not limited to the warranties of merchantability,
|
||||
* fitness for a particular purpose and noninfringement. In no event shall the
|
||||
* authors or copyright holders be liable for any claim, damages or other
|
||||
* liability, whether in an action of contract, tort or otherwise, arising from,
|
||||
* out of or in connection with the Software or the use or other dealings in the
|
||||
* Software.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
|
||||
/*
|
||||
* This library creates QR Code symbols, which is a type of two-dimension barcode.
|
||||
* Invented by Denso Wave and described in the ISO/IEC 18004 standard.
|
||||
* A QR Code structure is an immutable square grid of dark and light cells.
|
||||
* The library provides functions to create a QR Code from text or binary data.
|
||||
* The library covers the QR Code Model 2 specification, supporting all versions (sizes)
|
||||
* from 1 to 40, all 4 error correction levels, and 4 character encoding modes.
|
||||
*
|
||||
* Ways to create a QR Code object:
|
||||
* - High level: Take the payload data and call qrcodegen_encodeText() or qrcodegen_encodeBinary().
|
||||
* - Low level: Custom-make the list of segments and call
|
||||
* qrcodegen_encodeSegments() or qrcodegen_encodeSegmentsAdvanced().
|
||||
* (Note that all ways require supplying the desired error correction level and various byte buffers.)
|
||||
*/
|
||||
|
||||
|
||||
/*---- Enum and struct types----*/
|
||||
|
||||
/*
|
||||
* The error correction level in a QR Code symbol.
|
||||
*/
|
||||
enum qrcodegen_Ecc {
|
||||
// Must be declared in ascending order of error protection
|
||||
// so that an internal qrcodegen function works properly
|
||||
qrcodegen_Ecc_LOW = 0 , // The QR Code can tolerate about 7% erroneous codewords
|
||||
qrcodegen_Ecc_MEDIUM , // The QR Code can tolerate about 15% erroneous codewords
|
||||
qrcodegen_Ecc_QUARTILE, // The QR Code can tolerate about 25% erroneous codewords
|
||||
qrcodegen_Ecc_HIGH , // The QR Code can tolerate about 30% erroneous codewords
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* The mask pattern used in a QR Code symbol.
|
||||
*/
|
||||
enum qrcodegen_Mask {
|
||||
// A special value to tell the QR Code encoder to
|
||||
// automatically select an appropriate mask pattern
|
||||
qrcodegen_Mask_AUTO = -1,
|
||||
// The eight actual mask patterns
|
||||
qrcodegen_Mask_0 = 0,
|
||||
qrcodegen_Mask_1,
|
||||
qrcodegen_Mask_2,
|
||||
qrcodegen_Mask_3,
|
||||
qrcodegen_Mask_4,
|
||||
qrcodegen_Mask_5,
|
||||
qrcodegen_Mask_6,
|
||||
qrcodegen_Mask_7,
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Describes how a segment's data bits are interpreted.
|
||||
*/
|
||||
enum qrcodegen_Mode {
|
||||
qrcodegen_Mode_NUMERIC = 0x1,
|
||||
qrcodegen_Mode_ALPHANUMERIC = 0x2,
|
||||
qrcodegen_Mode_BYTE = 0x4,
|
||||
qrcodegen_Mode_KANJI = 0x8,
|
||||
qrcodegen_Mode_ECI = 0x7,
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* A segment of character/binary/control data in a QR Code symbol.
|
||||
* The mid-level way to create a segment is to take the payload data
|
||||
* and call a factory function such as qrcodegen_makeNumeric().
|
||||
* The low-level way to create a segment is to custom-make the bit buffer
|
||||
* and initialize a qrcodegen_Segment struct with appropriate values.
|
||||
* Even in the most favorable conditions, a QR Code can only hold 7089 characters of data.
|
||||
* Any segment longer than this is meaningless for the purpose of generating QR Codes.
|
||||
* Moreover, the maximum allowed bit length is 32767 because
|
||||
* the largest QR Code (version 40) has 31329 modules.
|
||||
*/
|
||||
struct qrcodegen_Segment {
|
||||
// The mode indicator of this segment.
|
||||
enum qrcodegen_Mode mode;
|
||||
|
||||
// The length of this segment's unencoded data. Measured in characters for
|
||||
// numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode.
|
||||
// Always zero or positive. Not the same as the data's bit length.
|
||||
int numChars;
|
||||
|
||||
// The data bits of this segment, packed in bitwise big endian.
|
||||
// Can be null if the bit length is zero.
|
||||
uint8_t *data;
|
||||
|
||||
// The number of valid data bits used in the buffer. Requires
|
||||
// 0 <= bitLength <= 32767, and bitLength <= (capacity of data array) * 8.
|
||||
// The character count (numChars) must agree with the mode and the bit buffer length.
|
||||
int bitLength;
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*---- Macro constants and functions ----*/
|
||||
|
||||
#define qrcodegen_VERSION_MIN 1 // The minimum version number supported in the QR Code Model 2 standard
|
||||
#define qrcodegen_VERSION_MAX 40 // The maximum version number supported in the QR Code Model 2 standard
|
||||
|
||||
// Calculates the number of bytes needed to store any QR Code up to and including the given version number,
|
||||
// as a compile-time constant. For example, 'uint8_t buffer[qrcodegen_BUFFER_LEN_FOR_VERSION(25)];'
|
||||
// can store any single QR Code from version 1 to 25 (inclusive). The result fits in an int (or int16).
|
||||
// Requires qrcodegen_VERSION_MIN <= n <= qrcodegen_VERSION_MAX.
|
||||
#define qrcodegen_BUFFER_LEN_FOR_VERSION(n) ((((n) * 4 + 17) * ((n) * 4 + 17) + 7) / 8 + 1)
|
||||
|
||||
// The worst-case number of bytes needed to store one QR Code, up to and including
|
||||
// version 40. This value equals 3918, which is just under 4 kilobytes.
|
||||
// Use this more convenient value to avoid calculating tighter memory bounds for buffers.
|
||||
#define qrcodegen_BUFFER_LEN_MAX qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX)
|
||||
|
||||
|
||||
|
||||
/*---- Functions (high level) to generate QR Codes ----*/
|
||||
|
||||
/*
|
||||
* Encodes the given text string to a QR Code, returning true if successful.
|
||||
* If the data is too long to fit in any version in the given range
|
||||
* at the given ECC level, then false is returned.
|
||||
*
|
||||
* The input text must be encoded in UTF-8 and contain no NULs.
|
||||
* Requires 1 <= minVersion <= maxVersion <= 40.
|
||||
*
|
||||
* The smallest possible QR Code version within the given range is automatically
|
||||
* chosen for the output. Iff boostEcl is true, then the ECC level of the result
|
||||
* may be higher than the ecl argument if it can be done without increasing the
|
||||
* version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or
|
||||
* qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow).
|
||||
*
|
||||
* About the arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion):
|
||||
* - Before calling the function:
|
||||
* - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow
|
||||
* reading and writing; hence each array must have a length of at least len.
|
||||
* - The two ranges must not overlap (aliasing).
|
||||
* - The initial state of both ranges can be uninitialized
|
||||
* because the function always writes before reading.
|
||||
* - After the function returns:
|
||||
* - Both ranges have no guarantee on which elements are initialized and what values are stored.
|
||||
* - tempBuffer contains no useful data and should be treated as entirely uninitialized.
|
||||
* - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule().
|
||||
*
|
||||
* If successful, the resulting QR Code may use numeric,
|
||||
* alphanumeric, or byte mode to encode the text.
|
||||
*
|
||||
* In the most optimistic case, a QR Code at version 40 with low ECC
|
||||
* can hold any UTF-8 string up to 2953 bytes, or any alphanumeric string
|
||||
* up to 4296 characters, or any digit string up to 7089 characters.
|
||||
* These numbers represent the hard upper limit of the QR Code standard.
|
||||
*
|
||||
* Please consult the QR Code specification for information on
|
||||
* data capacities per version, ECC level, and text encoding mode.
|
||||
*/
|
||||
bool qrcodegen_encodeText(const char *text, uint8_t tempBuffer[], uint8_t qrcode[],
|
||||
enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl);
|
||||
|
||||
|
||||
/*
|
||||
* Encodes the given binary data to a QR Code, returning true if successful.
|
||||
* If the data is too long to fit in any version in the given range
|
||||
* at the given ECC level, then false is returned.
|
||||
*
|
||||
* Requires 1 <= minVersion <= maxVersion <= 40.
|
||||
*
|
||||
* The smallest possible QR Code version within the given range is automatically
|
||||
* chosen for the output. Iff boostEcl is true, then the ECC level of the result
|
||||
* may be higher than the ecl argument if it can be done without increasing the
|
||||
* version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or
|
||||
* qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow).
|
||||
*
|
||||
* About the arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(maxVersion):
|
||||
* - Before calling the function:
|
||||
* - The array ranges dataAndTemp[0 : len] and qrcode[0 : len] must allow
|
||||
* reading and writing; hence each array must have a length of at least len.
|
||||
* - The two ranges must not overlap (aliasing).
|
||||
* - The input array range dataAndTemp[0 : dataLen] should normally be
|
||||
* valid UTF-8 text, but is not required by the QR Code standard.
|
||||
* - The initial state of dataAndTemp[dataLen : len] and qrcode[0 : len]
|
||||
* can be uninitialized because the function always writes before reading.
|
||||
* - After the function returns:
|
||||
* - Both ranges have no guarantee on which elements are initialized and what values are stored.
|
||||
* - dataAndTemp contains no useful data and should be treated as entirely uninitialized.
|
||||
* - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule().
|
||||
*
|
||||
* If successful, the resulting QR Code will use byte mode to encode the data.
|
||||
*
|
||||
* In the most optimistic case, a QR Code at version 40 with low ECC can hold any byte
|
||||
* sequence up to length 2953. This is the hard upper limit of the QR Code standard.
|
||||
*
|
||||
* Please consult the QR Code specification for information on
|
||||
* data capacities per version, ECC level, and text encoding mode.
|
||||
*/
|
||||
bool qrcodegen_encodeBinary(uint8_t dataAndTemp[], size_t dataLen, uint8_t qrcode[],
|
||||
enum qrcodegen_Ecc ecl, int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl);
|
||||
|
||||
|
||||
/*---- Functions (low level) to generate QR Codes ----*/
|
||||
|
||||
/*
|
||||
* Encodes the given segments to a QR Code, returning true if successful.
|
||||
* If the data is too long to fit in any version at the given ECC level,
|
||||
* then false is returned.
|
||||
*
|
||||
* The smallest possible QR Code version is automatically chosen for
|
||||
* the output. The ECC level of the result may be higher than the
|
||||
* ecl argument if it can be done without increasing the version.
|
||||
*
|
||||
* About the byte arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX):
|
||||
* - Before calling the function:
|
||||
* - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow
|
||||
* reading and writing; hence each array must have a length of at least len.
|
||||
* - The two ranges must not overlap (aliasing).
|
||||
* - The initial state of both ranges can be uninitialized
|
||||
* because the function always writes before reading.
|
||||
* - The input array segs can contain segments whose data buffers overlap with tempBuffer.
|
||||
* - After the function returns:
|
||||
* - Both ranges have no guarantee on which elements are initialized and what values are stored.
|
||||
* - tempBuffer contains no useful data and should be treated as entirely uninitialized.
|
||||
* - Any segment whose data buffer overlaps with tempBuffer[0 : len]
|
||||
* must be treated as having invalid values in that array.
|
||||
* - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule().
|
||||
*
|
||||
* Please consult the QR Code specification for information on
|
||||
* data capacities per version, ECC level, and text encoding mode.
|
||||
*
|
||||
* This function allows the user to create a custom sequence of segments that switches
|
||||
* between modes (such as alphanumeric and byte) to encode text in less space.
|
||||
* This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary().
|
||||
*/
|
||||
bool qrcodegen_encodeSegments(const struct qrcodegen_Segment segs[], size_t len,
|
||||
enum qrcodegen_Ecc ecl, uint8_t tempBuffer[], uint8_t qrcode[]);
|
||||
|
||||
|
||||
/*
|
||||
* Encodes the given segments to a QR Code, returning true if successful.
|
||||
* If the data is too long to fit in any version in the given range
|
||||
* at the given ECC level, then false is returned.
|
||||
*
|
||||
* Requires 1 <= minVersion <= maxVersion <= 40.
|
||||
*
|
||||
* The smallest possible QR Code version within the given range is automatically
|
||||
* chosen for the output. Iff boostEcl is true, then the ECC level of the result
|
||||
* may be higher than the ecl argument if it can be done without increasing the
|
||||
* version. The mask is either between qrcodegen_Mask_0 to 7 to force that mask, or
|
||||
* qrcodegen_Mask_AUTO to automatically choose an appropriate mask (which may be slow).
|
||||
*
|
||||
* About the byte arrays, letting len = qrcodegen_BUFFER_LEN_FOR_VERSION(qrcodegen_VERSION_MAX):
|
||||
* - Before calling the function:
|
||||
* - The array ranges tempBuffer[0 : len] and qrcode[0 : len] must allow
|
||||
* reading and writing; hence each array must have a length of at least len.
|
||||
* - The two ranges must not overlap (aliasing).
|
||||
* - The initial state of both ranges can be uninitialized
|
||||
* because the function always writes before reading.
|
||||
* - The input array segs can contain segments whose data buffers overlap with tempBuffer.
|
||||
* - After the function returns:
|
||||
* - Both ranges have no guarantee on which elements are initialized and what values are stored.
|
||||
* - tempBuffer contains no useful data and should be treated as entirely uninitialized.
|
||||
* - Any segment whose data buffer overlaps with tempBuffer[0 : len]
|
||||
* must be treated as having invalid values in that array.
|
||||
* - If successful, qrcode can be passed into qrcodegen_getSize() and qrcodegen_getModule().
|
||||
*
|
||||
* Please consult the QR Code specification for information on
|
||||
* data capacities per version, ECC level, and text encoding mode.
|
||||
*
|
||||
* This function allows the user to create a custom sequence of segments that switches
|
||||
* between modes (such as alphanumeric and byte) to encode text in less space.
|
||||
* This is a low-level API; the high-level API is qrcodegen_encodeText() and qrcodegen_encodeBinary().
|
||||
*/
|
||||
bool qrcodegen_encodeSegmentsAdvanced(const struct qrcodegen_Segment segs[], size_t len, enum qrcodegen_Ecc ecl,
|
||||
int minVersion, int maxVersion, enum qrcodegen_Mask mask, bool boostEcl, uint8_t tempBuffer[], uint8_t qrcode[]);
|
||||
|
||||
|
||||
/*
|
||||
* Tests whether the given string can be encoded as a segment in numeric mode.
|
||||
* A string is encodable iff each character is in the range 0 to 9.
|
||||
*/
|
||||
bool qrcodegen_isNumeric(const char *text);
|
||||
|
||||
|
||||
/*
|
||||
* Tests whether the given string can be encoded as a segment in alphanumeric mode.
|
||||
* A string is encodable iff each character is in the following set: 0 to 9, A to Z
|
||||
* (uppercase only), space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.
|
||||
*/
|
||||
bool qrcodegen_isAlphanumeric(const char *text);
|
||||
|
||||
|
||||
/*
|
||||
* Returns the number of bytes (uint8_t) needed for the data buffer of a segment
|
||||
* containing the given number of characters using the given mode. Notes:
|
||||
* - Returns SIZE_MAX on failure, i.e. numChars > INT16_MAX or the internal
|
||||
* calculation of the number of needed bits exceeds INT16_MAX (i.e. 32767).
|
||||
* - Otherwise, all valid results are in the range [0, ceil(INT16_MAX / 8)], i.e. at most 4096.
|
||||
* - It is okay for the user to allocate more bytes for the buffer than needed.
|
||||
* - For byte mode, numChars measures the number of bytes, not Unicode code points.
|
||||
* - For ECI mode, numChars must be 0, and the worst-case number of bytes is returned.
|
||||
* An actual ECI segment can have shorter data. For non-ECI modes, the result is exact.
|
||||
*/
|
||||
size_t qrcodegen_calcSegmentBufferSize(enum qrcodegen_Mode mode, size_t numChars);
|
||||
|
||||
|
||||
/*
|
||||
* Returns a segment representing the given binary data encoded in
|
||||
* byte mode. All input byte arrays are acceptable. Any text string
|
||||
* can be converted to UTF-8 bytes and encoded as a byte mode segment.
|
||||
*/
|
||||
struct qrcodegen_Segment qrcodegen_makeBytes(const uint8_t data[], size_t len, uint8_t buf[]);
|
||||
|
||||
|
||||
/*
|
||||
* Returns a segment representing the given string of decimal digits encoded in numeric mode.
|
||||
*/
|
||||
struct qrcodegen_Segment qrcodegen_makeNumeric(const char *digits, uint8_t buf[]);
|
||||
|
||||
|
||||
/*
|
||||
* Returns a segment representing the given text string encoded in alphanumeric mode.
|
||||
* The characters allowed are: 0 to 9, A to Z (uppercase only), space,
|
||||
* dollar, percent, asterisk, plus, hyphen, period, slash, colon.
|
||||
*/
|
||||
struct qrcodegen_Segment qrcodegen_makeAlphanumeric(const char *text, uint8_t buf[]);
|
||||
|
||||
|
||||
/*
|
||||
* Returns a segment representing an Extended Channel Interpretation
|
||||
* (ECI) designator with the given assignment value.
|
||||
*/
|
||||
struct qrcodegen_Segment qrcodegen_makeEci(long assignVal, uint8_t buf[]);
|
||||
|
||||
|
||||
/*---- Functions to extract raw data from QR Codes ----*/
|
||||
|
||||
/*
|
||||
* Returns the side length of the given QR Code, assuming that encoding succeeded.
|
||||
* The result is in the range [21, 177]. Note that the length of the array buffer
|
||||
* is related to the side length - every 'uint8_t qrcode[]' must have length at least
|
||||
* qrcodegen_BUFFER_LEN_FOR_VERSION(version), which equals ceil(size^2 / 8 + 1).
|
||||
*/
|
||||
int qrcodegen_getSize(const uint8_t qrcode[]);
|
||||
|
||||
|
||||
/*
|
||||
* Returns the color of the module (pixel) at the given coordinates, which is false
|
||||
* for light or true for dark. The top left corner has the coordinates (x=0, y=0).
|
||||
* If the given coordinates are out of bounds, then false (light) is returned.
|
||||
*/
|
||||
bool qrcodegen_getModule(const uint8_t qrcode[], int x, int y);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
Vendored
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+5079
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user