Launcher backgrounds drawn by tools/make-title-art.py

pic0.dds and pic1.dds had been converted from a background image whose
origin was not recorded. They are now a gradient drawn from arithmetic alone
by tools/make-title-art.py (BC7 mode 6 blocks, each one colour, so no texture
compressor is needed and the files are the same on every machine), and the
notices name that script as their origin.
This commit is contained in:
Mihawk committed 2026-09-30 19:03:08 -04:00
1 parent adc3801b95
commit 1fb369fa63
4 files changed
+121 -3

No files matched your search

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+3 -3
View File
@@ -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",
+118
View File
@@ -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="<u8")
out[0::2], out[1::2] = lo, hi
return out.tobytes()
def dds_header() -> 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("<II4sIIIII", 32, 0x4, b"DX10", 0, 0, 0, 0, 0)
header += struct.pack("<IIIII", 0x1000, 0, 0, 0, 0) # caps: texture
header += struct.pack("<IIIII", 98, 3, 0, 1, 0) # BC7_UNORM, TEXTURE2D, array 1
assert len(header) == 148
return header
def main() -> 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())