diff --git a/sce_sys/pic0.dds b/sce_sys/pic0.dds index 9bf5336..e085028 100644 Binary files a/sce_sys/pic0.dds and b/sce_sys/pic0.dds differ diff --git a/sce_sys/pic1.dds b/sce_sys/pic1.dds index 9bf5336..e5959da 100644 Binary files a/sce_sys/pic1.dds and b/sce_sys/pic1.dds differ diff --git a/tooling/notices/components.json b/tooling/notices/components.json index 64cfe40..97c8057 100644 --- a/tooling/notices/components.json +++ b/tooling/notices/components.json @@ -16,7 +16,7 @@ "name": "PS5 RetroArch (this port: title code, platform integration, build scripts and patches)", "kind": "code", "always": true, - "artifacts": ["eboot.bin", "retroarch.cfg", "sce_sys/param.json"], + "artifacts": ["eboot.bin", "retroarch.cfg", "LEGAL.txt", "sce_sys/param.json"], "licence": "GPL-3.0-or-later", "copyright": [ "Copyright (C) 2026 Mihawk", @@ -180,10 +180,10 @@ "kind": "asset", "artifacts": ["sce_sys/icon0.png", "sce_sys/pic0.dds", "sce_sys/pic1.dds", "sce_sys/snd0.at9"], "licence": "see modifications", - "copyright": ["snd0.at9: Copyright (C) 2026 BlackBearReloaded, GPL-3.0-or-later according to ps5-native-app-boilerplate's NOTICE.md"], + "copyright": ["pic0.dds, pic1.dds: Copyright (C) 2026 Mihawk, GPL-3.0-or-later (drawn by tools/make-title-art.py)", "snd0.at9: Copyright (C) 2026 BlackBearReloaded, GPL-3.0-or-later according to ps5-native-app-boilerplate's NOTICE.md"], "texts": [], "source": {"kind": "git", "path": ".", "remote": "https://github.com/mihawk-99/PS5_RetroArch"}, - "modifications": "icon0.png is the RetroArch logo (libretro's branding); pic0.dds and pic1.dds were converted from a background image whose origin is not recorded (docs/releases/SOURCE_CORRESPONDENCE.md); snd0.at9 is ps5-native-app-boilerplate's default selection track." + "modifications": "icon0.png is the RetroArch logo (libretro's branding); pic0.dds and pic1.dds are a gradient background drawn from nothing but arithmetic by tools/make-title-art.py in this repository (they replaced, in v0.5.6-alpha.5, a background whose origin was not recorded); snd0.at9 is ps5-native-app-boilerplate's default selection track." }, { "id": "fceumm", diff --git a/tools/make-title-art.py b/tools/make-title-art.py new file mode 100644 index 0000000..53fcaf9 --- /dev/null +++ b/tools/make-title-art.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# PS5 RetroArch - make the launcher backgrounds (sce_sys/pic0.dds, pic1.dds). +# Copyright (C) 2026 Mihawk-99 +# SPDX-License-Identifier: GPL-3.0-or-later +# +# The backgrounds are drawn here, from nothing but arithmetic, so their origin +# is this script: a dark gradient with a soft glow, 3840x2160, in the format the +# launcher reads (DDS, DX10 header, BC7_UNORM, one level). +# +# Each 4x4 block is one colour, which BC7's mode 6 holds exactly (both +# endpoints that colour, every index 0), so no texture compressor is needed +# and the output is the same on every machine. At 4K a 4x4 step is invisible +# in a gradient this gentle. +# +# python3 tools/make-title-art.py write sce_sys/pic0.dds and pic1.dds +# python3 tools/make-title-art.py --png DIR also write PNG previews to DIR +import struct +import sys +from pathlib import Path + +import numpy as np + +WIDTH, HEIGHT = 3840, 2160 +ROOT = Path(__file__).resolve().parent.parent + + +def draw(variant: int) -> np.ndarray: + """The picture, one RGB colour per 4x4 block (HEIGHT/4 x WIDTH/4 x 3, 0..1).""" + by, bx = HEIGHT // 4, WIDTH // 4 + y, x = np.mgrid[0:by, 0:bx].astype(np.float64) + u, v = x / (bx - 1), y / (by - 1) + # A diagonal from deep navy to a dark violet + top = np.array([0.035, 0.045, 0.110]) + bottom = np.array([0.095, 0.040, 0.140]) + t = np.clip(0.55 * u + 0.45 * v, 0.0, 1.0)[..., None] + rgb = top * (1 - t) + bottom * t + # A soft glow, placed differently in the two pictures + cx, cy = (0.30, 0.38) if variant == 0 else (0.70, 0.62) + d2 = ((u - cx) * 16 / 9) ** 2 + (v - cy) ** 2 + glow = np.exp(-d2 / 0.09)[..., None] + rgb = rgb + glow * np.array([0.10, 0.14, 0.26]) + # A second, fainter glow in a warmer tone + d2b = ((u - (1 - cx)) * 16 / 9) ** 2 + (v - (1 - cy)) ** 2 + rgb = rgb + np.exp(-d2b / 0.05)[..., None] * np.array([0.09, 0.04, 0.10]) + # A gentle vignette + r2 = ((u - 0.5) * 16 / 9) ** 2 + (v - 0.5) ** 2 + rgb = rgb * (1.0 - 0.35 * np.clip(r2 / 1.1, 0, 1))[..., None] + return np.clip(rgb, 0.0, 1.0) + + +def bc7_mode6_solid(blocks: np.ndarray) -> bytes: + """BC7 blocks, each one solid colour (8-bit values, odd, alpha 255).""" + c = np.round(blocks * 255).astype(np.uint32) + c = c | 1 # the p-bit (1, for alpha 255) is each value's lowest bit + c7 = c >> 1 # the 7 stored bits + r, g, b = c7[..., 0].ravel(), c7[..., 1].ravel(), c7[..., 2].ravel() + a = np.full_like(r, 127) + # Mode 6: bits 0-6 the mode (bit 6 set), then R0 R1 G0 G1 B0 B1 A0 A1 (7 each), + # P0 P1, then the indices (all 0). Bits count from the low end of the block. + lo = np.full(r.shape, 1 << 6, dtype=np.uint64) + hi = np.zeros(r.shape, dtype=np.uint64) + fields = [r, r, g, g, b, b, a, a] + pos = 7 + for field in fields: + for bit in range(7): + value = ((field >> bit) & 1).astype(np.uint64) + if pos < 64: + lo |= value << np.uint64(pos) + else: + hi |= value << np.uint64(pos - 64) + pos += 1 + for _ in range(2): # P0, P1 + if pos < 64: + lo |= np.uint64(1) << np.uint64(pos) + else: + hi |= np.uint64(1) << np.uint64(pos - 64) + pos += 1 + out = np.empty(r.shape[0] * 2, dtype=" bytes: + """DDS with a DX10 header: BC7_UNORM (98), 2D, one level.""" + DDSD_CAPS, DDSD_HEIGHT, DDSD_WIDTH, DDSD_PIXELFORMAT, DDSD_LINEARSIZE = 0x1, 0x2, 0x4, 0x1000, 0x80000 + flags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE + linear = WIDTH * HEIGHT # BC7: 16 bytes a 4x4 block, one byte a pixel + header = struct.pack("<4sIIIIIII", b"DDS ", 124, flags, HEIGHT, WIDTH, linear, 0, 1) + header += b"\0" * 44 # reserved + header += struct.pack(" int: + png_dir = None + if len(sys.argv) == 3 and sys.argv[1] == "--png": + png_dir = Path(sys.argv[2]) + elif len(sys.argv) != 1: + print(__doc__ or "usage: make-title-art.py [--png DIR]", file=sys.stderr) + return 2 + for variant, name in ((0, "pic0.dds"), (1, "pic1.dds")): + blocks = draw(variant) + data = dds_header() + bc7_mode6_solid(blocks) + (ROOT / "sce_sys" / name).write_bytes(data) + print(f"sce_sys/{name}: {len(data)} bytes") + if png_dir: + from PIL import Image + png_dir.mkdir(parents=True, exist_ok=True) + pixels = ((np.round(blocks * 255).astype(np.uint8)) | 1).repeat(4, axis=0).repeat(4, axis=1) + Image.fromarray(pixels, "RGB").save(png_dir / name.replace(".dds", ".png")) + return 0 + + +if __name__ == "__main__": + sys.exit(main())