mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 20:20:19 +02:00
Drop stb_image from Switch OTA launcher with a pre-baked logo asset.
This commit is contained in:
@@ -44,6 +44,7 @@ native/switch-ota-launcher/*.nro
|
|||||||
native/switch-ota-launcher/*.nacp
|
native/switch-ota-launcher/*.nacp
|
||||||
native/switch-ota-launcher/*.elf
|
native/switch-ota-launcher/*.elf
|
||||||
native/switch-ota-launcher/*.map
|
native/switch-ota-launcher/*.map
|
||||||
|
native/switch-ota-launcher/romfs/logo.rgba
|
||||||
native/switch-ota-launcher/romfs/logo.png
|
native/switch-ota-launcher/romfs/logo.png
|
||||||
native/switch-ota-launcher/romfs/cacert.pem
|
native/switch-ota-launcher/romfs/cacert.pem
|
||||||
native/switch-ota-launcher/romfs/ota-bootstrap.nro
|
native/switch-ota-launcher/romfs/ota-bootstrap.nro
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ APP_VERSION ?= 0.0.0
|
|||||||
|
|
||||||
# Prefer project Switch icon if present
|
# Prefer project Switch icon if present
|
||||||
ICON := $(TOPDIR)/../../assets/switch/icon.jpg
|
ICON := $(TOPDIR)/../../assets/switch/icon.jpg
|
||||||
LOGO_SRC := $(TOPDIR)/../../assets/logo/logo.png
|
LOGO_RGBA_SRC := $(TOPDIR)/assets/logo.rgba
|
||||||
|
LOGO_ROMFS := $(CURDIR)/$(ROMFS)/logo.rgba
|
||||||
|
|
||||||
#---------------------------------------------------------------------------------
|
#---------------------------------------------------------------------------------
|
||||||
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
|
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
|
||||||
@@ -148,7 +149,8 @@ bootstrap-romfs:
|
|||||||
|
|
||||||
sync-romfs: bootstrap-romfs
|
sync-romfs: bootstrap-romfs
|
||||||
@mkdir -p $(CURDIR)/$(ROMFS)
|
@mkdir -p $(CURDIR)/$(ROMFS)
|
||||||
@cp -f $(LOGO_SRC) $(CURDIR)/$(ROMFS)/logo.png
|
@[ -f "$(LOGO_RGBA_SRC)" ] || (echo "missing $(LOGO_RGBA_SRC) — run: python3 scripts/switch/bake_ota_logo.py" && exit 1)
|
||||||
|
@cp -f "$(LOGO_RGBA_SRC)" "$(LOGO_ROMFS)"
|
||||||
@if ! curl -sfL --time-cond $(CACERT_ROMFS) -o $(CACERT_ROMFS) $(CACERT_URL); then \
|
@if ! curl -sfL --time-cond $(CACERT_ROMFS) -o $(CACERT_ROMFS) $(CACERT_URL); then \
|
||||||
[ -f $(CACERT_ROMFS) ] || (echo "sync-romfs: failed to fetch cacert.pem" && exit 1); \
|
[ -f $(CACERT_ROMFS) ] || (echo "sync-romfs: failed to fetch cacert.pem" && exit 1); \
|
||||||
fi
|
fi
|
||||||
@@ -161,7 +163,7 @@ $(BUILD): sync-romfs
|
|||||||
clean:
|
clean:
|
||||||
@echo clean ...
|
@echo clean ...
|
||||||
@rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf $(TARGET).lst build-host
|
@rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf $(TARGET).lst build-host
|
||||||
@rm -f $(CURDIR)/$(ROMFS)/logo.png
|
@rm -f $(CURDIR)/$(ROMFS)/logo.rgba $(CURDIR)/$(ROMFS)/logo.png
|
||||||
|
|
||||||
host-test:
|
host-test:
|
||||||
@mkdir -p build-host
|
@mkdir -p build-host
|
||||||
|
|||||||
@@ -59,6 +59,18 @@ scripts/switch/build_ota_launcher.sh
|
|||||||
|
|
||||||
Docker fallback uses the same pin as fused builds (`scripts/switch/dkp-docker.image`).
|
Docker fallback uses the same pin as fused builds (`scripts/switch/dkp-docker.image`).
|
||||||
|
|
||||||
|
## OTA logo asset
|
||||||
|
|
||||||
|
The launcher draws a pre-scaled logo from `romfs:/logo.rgba` (no PNG decoder in
|
||||||
|
the NRO). The baked blob lives at `assets/logo.rgba` and is copied into romfs at
|
||||||
|
build time. After changing `assets/logo/logo.png`, regenerate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/switch/bake_ota_logo.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires Pillow, or on macOS uses `sips` when Pillow is not installed.
|
||||||
|
|
||||||
## Packaging
|
## Packaging
|
||||||
|
|
||||||
`scripts/switch/pack_sd_zip.sh GAME_NRO VERSION OUT_ZIP LAUNCHER_NRO` writes both
|
`scripts/switch/pack_sd_zip.sh GAME_NRO VERSION OUT_ZIP LAUNCHER_NRO` writes both
|
||||||
|
|||||||
Binary file not shown.
@@ -12,12 +12,10 @@
|
|||||||
#if defined(__SWITCH__)
|
#if defined(__SWITCH__)
|
||||||
#include <switch.h>
|
#include <switch.h>
|
||||||
|
|
||||||
#define STB_IMAGE_IMPLEMENTATION
|
|
||||||
#define STBI_ONLY_PNG
|
|
||||||
#define STBI_NO_THREAD_LOCALS
|
|
||||||
#include "../third_party/stb_image.h"
|
|
||||||
#include "../third_party/font8x8_basic.h"
|
#include "../third_party/font8x8_basic.h"
|
||||||
|
|
||||||
|
#define OTA_LOGO_ROMFS "romfs:/logo.rgba"
|
||||||
|
|
||||||
#define FB_W 1280
|
#define FB_W 1280
|
||||||
#define FB_H 720
|
#define FB_H 720
|
||||||
|
|
||||||
@@ -261,25 +259,47 @@ static void blit_logo(u32 *fb, u32 stride_px, int dst_x, int dst_y, int max_w) {
|
|||||||
|
|
||||||
static void load_logo(void) {
|
static void load_logo(void) {
|
||||||
if (g_logo) return;
|
if (g_logo) return;
|
||||||
/* romfs is mounted in ota_net_init() before the release check runs. */
|
FILE *fp = fopen(OTA_LOGO_ROMFS, "rb");
|
||||||
int w = 0, h = 0, n = 0;
|
if (!fp) return;
|
||||||
unsigned char *data = stbi_load("romfs:/logo.png", &w, &h, &n, 4);
|
|
||||||
if (!data || w <= 0 || h <= 0) {
|
unsigned char header[8];
|
||||||
if (data) stbi_image_free(data);
|
if (fread(header, 1, sizeof(header), fp) != sizeof(header)) {
|
||||||
|
fclose(fp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int w = (int)(header[0] | (header[1] << 8) | (header[2] << 16) | (header[3] << 24));
|
||||||
|
int h = (int)(header[4] | (header[5] << 8) | (header[6] << 16) | (header[7] << 24));
|
||||||
|
if (w <= 0 || h <= 0 || w > 2048 || h > 2048) {
|
||||||
|
fclose(fp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t nbytes = (size_t)w * (size_t)h * 4u;
|
||||||
|
unsigned char *data = (unsigned char *)malloc(nbytes);
|
||||||
|
if (!data) {
|
||||||
|
fclose(fp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fread(data, 1, nbytes, fp) != nbytes) {
|
||||||
|
free(data);
|
||||||
|
fclose(fp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
g_logo = (u32 *)malloc((size_t)w * (size_t)h * sizeof(u32));
|
g_logo = (u32 *)malloc((size_t)w * (size_t)h * sizeof(u32));
|
||||||
if (!g_logo) {
|
if (!g_logo) {
|
||||||
stbi_image_free(data);
|
free(data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < w * h; i++) {
|
for (int i = 0; i < w * h; i++) {
|
||||||
unsigned char *p = data + i * 4;
|
unsigned char *p = data + (size_t)i * 4u;
|
||||||
g_logo[i] = RGBA8(p[0], p[1], p[2], p[3]);
|
g_logo[i] = RGBA8(p[0], p[1], p[2], p[3]);
|
||||||
}
|
}
|
||||||
|
free(data);
|
||||||
g_logo_w = w;
|
g_logo_w = w;
|
||||||
g_logo_h = h;
|
g_logo_h = h;
|
||||||
stbi_image_free(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int ui_ensure(void) {
|
static int ui_ensure(void) {
|
||||||
|
|||||||
-7988
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Bake assets/logo/logo.png into a pre-scaled RGBA blob for the Switch OTA UI.
|
||||||
|
|
||||||
|
Output format (little-endian):
|
||||||
|
uint32 width, uint32 height, then width*height RGBA8888 pixels.
|
||||||
|
|
||||||
|
Regenerate after editing the source PNG:
|
||||||
|
python3 scripts/switch/bake_ota_logo.py
|
||||||
|
|
||||||
|
Uses Pillow when available (see scripts/setup.sh). On macOS without Pillow,
|
||||||
|
falls back to sips + BMP export.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
import platform
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
DEFAULT_SRC = ROOT / "assets/logo/logo.png"
|
||||||
|
DEFAULT_OUT = ROOT / "native/switch-ota-launcher/assets/logo.rgba"
|
||||||
|
DEFAULT_MAX_W = 320
|
||||||
|
DEFAULT_MAX_H = 90
|
||||||
|
|
||||||
|
|
||||||
|
def fit_size(src_w: int, src_h: int, max_w: int, max_h: int) -> tuple[int, int]:
|
||||||
|
scale = min(max_w / src_w, max_h / src_h, 1.0)
|
||||||
|
w = max(1, int(math.floor(src_w * scale + 0.5)))
|
||||||
|
h = max(1, int(math.floor(src_h * scale + 0.5)))
|
||||||
|
return w, h
|
||||||
|
|
||||||
|
|
||||||
|
def read_png_size(path: Path) -> tuple[int, int]:
|
||||||
|
with path.open("rb") as fp:
|
||||||
|
sig = fp.read(8)
|
||||||
|
if sig != b"\x89PNG\r\n\x1a\n":
|
||||||
|
raise SystemExit(f"not a PNG: {path}")
|
||||||
|
while True:
|
||||||
|
raw = fp.read(8)
|
||||||
|
if len(raw) < 8:
|
||||||
|
raise SystemExit(f"truncated PNG: {path}")
|
||||||
|
length, ctype = struct.unpack(">I4s", raw)
|
||||||
|
data = fp.read(length)
|
||||||
|
fp.read(4)
|
||||||
|
if ctype == b"IHDR":
|
||||||
|
return struct.unpack(">II", data[:8])
|
||||||
|
if ctype == b"IEND":
|
||||||
|
break
|
||||||
|
raise SystemExit(f"missing IHDR in PNG: {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def read_bmp_rgba(path: Path) -> tuple[int, int, bytes]:
|
||||||
|
data = path.read_bytes()
|
||||||
|
if len(data) < 54 or data[:2] != b"BM":
|
||||||
|
raise SystemExit(f"not a BMP: {path}")
|
||||||
|
pixel_offset = struct.unpack_from("<I", data, 10)[0]
|
||||||
|
width = struct.unpack_from("<i", data, 18)[0]
|
||||||
|
height_raw = struct.unpack_from("<i", data, 22)[0]
|
||||||
|
bpp = struct.unpack_from("<H", data, 28)[0]
|
||||||
|
height = abs(height_raw)
|
||||||
|
top_down = height_raw < 0
|
||||||
|
if bpp != 32 or width <= 0 or height <= 0:
|
||||||
|
raise SystemExit(f"unsupported BMP {width}x{height_raw} @{bpp}bpp: {path}")
|
||||||
|
|
||||||
|
row_bytes = ((width * 4 + 3) // 4) * 4
|
||||||
|
pixels = bytearray(width * height * 4)
|
||||||
|
for row in range(height):
|
||||||
|
src_row = row if top_down else height - 1 - row
|
||||||
|
row_off = pixel_offset + src_row * row_bytes
|
||||||
|
for col in range(width):
|
||||||
|
b, g, r, a = data[row_off + col * 4 : row_off + col * 4 + 4]
|
||||||
|
dst = (row * width + col) * 4
|
||||||
|
pixels[dst : dst + 4] = bytes((r, g, b, a))
|
||||||
|
return width, height, bytes(pixels)
|
||||||
|
|
||||||
|
|
||||||
|
def bake_with_sips(src: Path, out_w: int, out_h: int) -> tuple[int, int, bytes]:
|
||||||
|
if platform.system() != "Darwin":
|
||||||
|
raise SystemExit(
|
||||||
|
"Pillow is required to bake the OTA logo on this platform. "
|
||||||
|
"Run scripts/setup.sh or: pip install pillow"
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
tmp_dir = Path(tmp)
|
||||||
|
scaled_png = tmp_dir / "scaled.png"
|
||||||
|
scaled_bmp = tmp_dir / "scaled.bmp"
|
||||||
|
subprocess.run(
|
||||||
|
["sips", "-z", str(out_h), str(out_w), str(src), "--out", str(scaled_png)],
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
["sips", "-s", "format", "bmp", str(scaled_png), "--out", str(scaled_bmp)],
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
return read_bmp_rgba(scaled_bmp)
|
||||||
|
|
||||||
|
|
||||||
|
def bake_with_pillow(src: Path, out_w: int, out_h: int) -> tuple[int, int, bytes]:
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
with Image.open(src) as image:
|
||||||
|
image = image.convert("RGBA")
|
||||||
|
if image.size != (out_w, out_h):
|
||||||
|
image = image.resize((out_w, out_h), Image.LANCZOS)
|
||||||
|
w, h = image.size
|
||||||
|
return w, h, image.tobytes()
|
||||||
|
|
||||||
|
|
||||||
|
def bake(src: Path, out: Path, max_w: int, max_h: int) -> tuple[int, int]:
|
||||||
|
if not src.is_file():
|
||||||
|
raise SystemExit(f"missing source PNG: {src}")
|
||||||
|
|
||||||
|
src_w, src_h = read_png_size(src)
|
||||||
|
out_w, out_h = fit_size(src_w, src_h, max_w, max_h)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image # noqa: F401
|
||||||
|
|
||||||
|
w, h, pixels = bake_with_pillow(src, out_w, out_h)
|
||||||
|
except ImportError:
|
||||||
|
w, h, pixels = bake_with_sips(src, out_w, out_h)
|
||||||
|
|
||||||
|
if len(pixels) != w * h * 4:
|
||||||
|
raise SystemExit(f"unexpected pixel buffer size for {w}x{h}")
|
||||||
|
|
||||||
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with out.open("wb") as fp:
|
||||||
|
fp.write(struct.pack("<II", w, h))
|
||||||
|
fp.write(pixels)
|
||||||
|
|
||||||
|
return w, h
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
src = Path(argv[1]) if len(argv) > 1 else DEFAULT_SRC
|
||||||
|
out = Path(argv[2]) if len(argv) > 2 else DEFAULT_OUT
|
||||||
|
max_w = int(argv[3]) if len(argv) > 3 else DEFAULT_MAX_W
|
||||||
|
max_h = int(argv[4]) if len(argv) > 4 else DEFAULT_MAX_H
|
||||||
|
|
||||||
|
w, h = bake(src, out, max_w, max_h)
|
||||||
|
print(f"wrote {out} ({w}x{h}, {8 + w * h * 4} bytes)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv))
|
||||||
@@ -508,7 +508,16 @@ test('AC-010: Fonte do launcher e Makefile DEVKITPRO existem @spec:AC-010', () =
|
|||||||
assert.ok(fs.existsSync(path.join(root, 'native/switch-ota-launcher/src/ota_ui.c')));
|
assert.ok(fs.existsSync(path.join(root, 'native/switch-ota-launcher/src/ota_ui.c')));
|
||||||
const otaUi = read('native/switch-ota-launcher/src/ota_ui.c');
|
const otaUi = read('native/switch-ota-launcher/src/ota_ui.c');
|
||||||
assert.match(otaUi, /COL_RAIL_R|rail|FFD600|255,\s*214/);
|
assert.match(otaUi, /COL_RAIL_R|rail|FFD600|255,\s*214/);
|
||||||
assert.match(otaUi, /logo\.png|stb_image/);
|
assert.match(otaUi, /logo\.rgba/);
|
||||||
|
assert.doesNotMatch(otaUi, /stb_image/);
|
||||||
|
assert.ok(
|
||||||
|
fs.existsSync(path.join(root, 'native/switch-ota-launcher/assets/logo.rgba')),
|
||||||
|
'pre-baked OTA logo asset'
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
fs.existsSync(path.join(root, 'scripts/switch/bake_ota_logo.py')),
|
||||||
|
'logo bake script for regenerating logo.rgba'
|
||||||
|
);
|
||||||
assert.match(otaUi, /ota_ui_sanitize_ascii/);
|
assert.match(otaUi, /ota_ui_sanitize_ascii/);
|
||||||
assert.match(otaUi, /draw_text_wrapped_centered/);
|
assert.match(otaUi, /draw_text_wrapped_centered/);
|
||||||
assert.match(otaUi, /ota_ui_alert_error/);
|
assert.match(otaUi, /ota_ui_alert_error/);
|
||||||
@@ -519,6 +528,7 @@ test('AC-010: Fonte do launcher e Makefile DEVKITPRO existem @spec:AC-010', () =
|
|||||||
assert.doesNotMatch(read('native/switch-ota-launcher/src/ota_net.c'), /CURLOPT_SSL_VERIFYPEER,\s*0L/);
|
assert.doesNotMatch(read('native/switch-ota-launcher/src/ota_net.c'), /CURLOPT_SSL_VERIFYPEER,\s*0L/);
|
||||||
assert.match(read('native/switch-ota-launcher/Makefile'), /^ROMFS\s*:=/m);
|
assert.match(read('native/switch-ota-launcher/Makefile'), /^ROMFS\s*:=/m);
|
||||||
assert.match(read('native/switch-ota-launcher/Makefile'), /cacert\.pem/);
|
assert.match(read('native/switch-ota-launcher/Makefile'), /cacert\.pem/);
|
||||||
|
assert.match(read('native/switch-ota-launcher/Makefile'), /logo\.rgba/);
|
||||||
|
|
||||||
const mk = read('native/switch-ota-launcher/Makefile');
|
const mk = read('native/switch-ota-launcher/Makefile');
|
||||||
assert.match(mk, /libnx\/switch_rules|DEVKITPRO/);
|
assert.match(mk, /libnx\/switch_rules|DEVKITPRO/);
|
||||||
@@ -530,8 +540,9 @@ test('AC-010: Fonte do launcher e Makefile DEVKITPRO existem @spec:AC-010', () =
|
|||||||
assert.match(read('native/switch-ota-launcher/src/ota_unzip.c'), /zzip\/zzip\.h/);
|
assert.match(read('native/switch-ota-launcher/src/ota_unzip.c'), /zzip\/zzip\.h/);
|
||||||
assert.match(read('native/switch-ota-launcher/src/main.c'), /ota_unzip_extract_file/);
|
assert.match(read('native/switch-ota-launcher/src/main.c'), /ota_unzip_extract_file/);
|
||||||
assert.match(read('native/switch-ota-launcher/src/main.c'), /GAME_MEMBER_IN_ZIP|switch\/gen1recomp\//);
|
assert.match(read('native/switch-ota-launcher/src/main.c'), /GAME_MEMBER_IN_ZIP|switch\/gen1recomp\//);
|
||||||
assert.match(read('native/switch-ota-launcher/src/main.c'), /LAUNCHER_MEMBER_IN_ZIP|ota_fs_atomic_replace_nro/);
|
assert.match(read('native/switch-ota-launcher/src/main.c'), /LAUNCHER_MEMBER_IN_ZIP|ota_fs_stage_launcher_bootstrap/);
|
||||||
assert.match(read('native/switch-ota-launcher/src/main.c'), /ota_fs_atomic_replace_nro\([\s\S]*\) != 0/);
|
assert.match(read('native/switch-ota-launcher/src/main.c'), /ota_fs_stage_launcher_bootstrap\([\s\S]*\) != 0/);
|
||||||
|
assert.match(read('native/switch-ota-launcher/src/main.c'), /return 2.*bootstrap|bootstrap.*return 2/i);
|
||||||
assert.match(read('native/switch-ota-launcher/src/ota_fs.c'), /ota_fs_atomic_replace_nro/);
|
assert.match(read('native/switch-ota-launcher/src/ota_fs.c'), /ota_fs_atomic_replace_nro/);
|
||||||
assert.match(read('native/switch-ota-launcher/src/ota_fs.c'), /remove\(dest\)/);
|
assert.match(read('native/switch-ota-launcher/src/ota_fs.c'), /remove\(dest\)/);
|
||||||
assert.doesNotMatch(read('native/switch-ota-launcher/src/ota_fs.c'), /#ifdef _WIN32[\s\S]*remove\(dest\)/);
|
assert.doesNotMatch(read('native/switch-ota-launcher/src/ota_fs.c'), /#ifdef _WIN32[\s\S]*remove\(dest\)/);
|
||||||
|
|||||||
Reference in New Issue
Block a user