Skip to content

Commit b688eec

Browse files
Screensaver: real bitmap logo via direct XFB rendering.
Replaces the ASCII silhouette with a 128x128 1-bit alpha-mask of branding/logo.png, baked into ppc/gen_logo.h by buildtools/make_logo.py at build time. Each screensaver step writes packed YUY2 32-bit words straight to the XFB: - Lit pixels get the current cycle color (BT.601 limited-range YUV table for the 7 ANSI cycle colors). - Unlit pixels in the bounding box get plain black. - Cb/Cr are pair-shared (4:2:2); pair gets logo color when at least one pixel is lit, neutral otherwise. - Bounding box of the previous frame is cleared each step before the next draw. CI workflows generate gen_logo.h before the make step. Header is gitignored.
1 parent 16a9555 commit b688eec

5 files changed

Lines changed: 185 additions & 38 deletions

File tree

.github/workflows/release.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ jobs:
7272
working-directory: ${{ needs.parse.outputs.console }}
7373
run: python3 buildtools/make_banner.py opening.bnr
7474

75+
- name: Generate screensaver logo header
76+
if: needs.parse.outputs.console == 'gamecube'
77+
working-directory: ${{ needs.parse.outputs.console }}
78+
run: python3 buildtools/make_logo.py
79+
7580
- name: Build (GameCube)
7681
if: needs.parse.outputs.console == 'gamecube'
7782
run: |

.github/workflows/verify-build.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ jobs:
4343
working-directory: ${{ matrix.console }}
4444
run: python3 buildtools/make_banner.py opening.bnr
4545

46+
- name: Generate screensaver logo header
47+
if: matrix.console == 'gamecube'
48+
working-directory: ${{ matrix.console }}
49+
run: python3 buildtools/make_logo.py
50+
4651
- name: Build (gamecube target)
4752
if: matrix.console == 'gamecube'
4853
run: |

gamecube/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ opening.bnr
1414
.idea/
1515
__pycache__/
1616
*.pyc
17+
18+
# Generated header — produced by buildtools/make_logo.py
19+
ppc/gen_logo.h

gamecube/buildtools/make_logo.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env python3
2+
"""Generate ppc/gen_logo.h: a 1-bit alpha mask of the joypad logo for the
3+
screensaver.
4+
5+
Source: branding/screensaver-logo.png (preferred) or
6+
branding/logo.png (fallback) or
7+
../../branding/logo_solid.png (joypad shared)
8+
9+
The mask gets rendered each frame in the cycle color so the screensaver
10+
can color-shift on every wall bounce while the silhouette stays crisp.
11+
"""
12+
13+
import sys
14+
from pathlib import Path
15+
16+
from PIL import Image
17+
18+
LOGO_W = 128
19+
LOGO_H = 128
20+
21+
HERE = Path(__file__).parent
22+
PROJECT = HERE.parent
23+
OUT = PROJECT / 'ppc' / 'gen_logo.h'
24+
25+
CANDIDATES = [
26+
PROJECT / 'branding' / 'screensaver-logo.png',
27+
PROJECT / 'branding' / 'logo.png',
28+
]
29+
30+
31+
def find_source() -> Path:
32+
for p in CANDIDATES:
33+
if p.exists():
34+
return p
35+
sys.exit('No source PNG found; tried: ' + ', '.join(str(p) for p in CANDIDATES))
36+
37+
38+
def main() -> None:
39+
src = find_source()
40+
img = Image.open(src).convert('RGBA')
41+
42+
# Composite alpha onto white. Source logos are typically dark icon on
43+
# transparent → after composite we get dark icon on white. Threshold the
44+
# grayscale: dark = "lit" (the silhouette), light = "unlit" (background).
45+
bg = Image.new('RGBA', img.size, (255, 255, 255, 255))
46+
flat = Image.alpha_composite(bg, img).convert('L')
47+
scaled = flat.resize((LOGO_W, LOGO_H), Image.LANCZOS)
48+
49+
# Pack into 1-bit mask, MSB-first per byte.
50+
bytes_per_row = LOGO_W // 8
51+
packed = bytearray(bytes_per_row * LOGO_H)
52+
lit_count = 0
53+
for y in range(LOGO_H):
54+
for x in range(LOGO_W):
55+
lit = scaled.getpixel((x, y)) < 128
56+
if lit:
57+
packed[y * bytes_per_row + x // 8] |= 0x80 >> (x % 8)
58+
lit_count += 1
59+
60+
OUT.parent.mkdir(parents=True, exist_ok=True)
61+
with OUT.open('w') as f:
62+
f.write(f'// Auto-generated by buildtools/make_logo.py — DO NOT EDIT.\n')
63+
f.write(f'// Source: {src}\n')
64+
f.write(f'// {LOGO_W}x{LOGO_H} 1-bit alpha mask, MSB-first.\n')
65+
f.write(f'// {lit_count} lit pixels of {LOGO_W * LOGO_H}.\n\n')
66+
f.write('#ifndef GEN_LOGO_H\n#define GEN_LOGO_H\n\n')
67+
f.write(f'#define LOGO_W {LOGO_W}\n')
68+
f.write(f'#define LOGO_H {LOGO_H}\n')
69+
f.write(f'#define LOGO_BYTES_PER_ROW {bytes_per_row}\n\n')
70+
f.write(f'static const unsigned char logo_mask[{len(packed)}] = {{\n')
71+
line = []
72+
for i, b in enumerate(packed):
73+
line.append(f'0x{b:02x}')
74+
if (i + 1) % 16 == 0:
75+
f.write(' ' + ', '.join(line) + ',\n')
76+
line = []
77+
if line:
78+
f.write(' ' + ', '.join(line) + ',\n')
79+
f.write('};\n\n')
80+
f.write('#endif // GEN_LOGO_H\n')
81+
print(f'wrote {OUT} ({len(packed)} bytes mask, {lit_count} lit px)')
82+
83+
84+
if __name__ == '__main__':
85+
main()

gamecube/ppc/main.c

Lines changed: 87 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,67 @@
55
#include <ogc/lwp_watchdog.h>
66

77
#include "../common/common_utils.h"
8+
#include "gen_logo.h"
89
#include "n64.h"
910
#include "ppc_utils.h"
1011

12+
// XFB is YUY2-packed: each 32-bit word holds [Y0 Cb Y1 Cr] for a 2-pixel
13+
// horizontal pair. RGB→YUV (BT.601 limited range) per ANSI cycle color.
14+
typedef struct {
15+
unsigned char y, cb, cr;
16+
} yuv_t;
17+
18+
static const yuv_t cycle_yuv[] = {
19+
{ 81, 90, 240}, // red
20+
{145, 54, 34}, // green
21+
{210, 16, 146}, // yellow
22+
{ 41, 240, 110}, // blue
23+
{106, 202, 222}, // magenta
24+
{170, 166, 16}, // cyan
25+
{235, 128, 128}, // white
26+
};
27+
#define CYCLE_LEN ((int)(sizeof(cycle_yuv) / sizeof(cycle_yuv[0])))
28+
29+
#define BLACK_Y 16
30+
#define NEUTRAL_C 128
31+
32+
static inline u32 yuv_pair(u8 y0, u8 y1, u8 cb, u8 cr) {
33+
return ((u32)y0 << 24) | ((u32)cb << 16) | ((u32)y1 << 8) | cr;
34+
}
35+
36+
static void xfb_clear_box(u32 *fb_words, int fb_pitch_words, int x_px,
37+
int y_px, int w, int h) {
38+
u32 black = yuv_pair(BLACK_Y, BLACK_Y, NEUTRAL_C, NEUTRAL_C);
39+
int x_pair = x_px / 2;
40+
int w_pair = w / 2;
41+
for (int row = 0; row < h; row++) {
42+
u32 *line = fb_words + (y_px + row) * fb_pitch_words + x_pair;
43+
for (int col = 0; col < w_pair; col++) line[col] = black;
44+
}
45+
}
46+
47+
static void xfb_draw_logo(u32 *fb_words, int fb_pitch_words, int x_px,
48+
int y_px, const yuv_t *col) {
49+
int x_pair = x_px / 2;
50+
for (int row = 0; row < LOGO_H; row++) {
51+
u32 *line = fb_words + (y_px + row) * fb_pitch_words + x_pair;
52+
const u8 *mask_row = &logo_mask[row * LOGO_BYTES_PER_ROW];
53+
for (int col_pair = 0; col_pair < LOGO_W / 2; col_pair++) {
54+
int idx_a = col_pair * 2;
55+
int idx_b = idx_a + 1;
56+
bool lit_a = mask_row[idx_a / 8] & (0x80 >> (idx_a % 8));
57+
bool lit_b = mask_row[idx_b / 8] & (0x80 >> (idx_b % 8));
58+
u8 y0 = lit_a ? col->y : BLACK_Y;
59+
u8 y1 = lit_b ? col->y : BLACK_Y;
60+
// Cb/Cr are shared between the pair (4:2:2). Use logo color when at
61+
// least one of the pair is lit, neutral otherwise.
62+
bool any_lit = lit_a || lit_b;
63+
line[col_pair] = yuv_pair(y0, y1, any_lit ? col->cb : NEUTRAL_C,
64+
any_lit ? col->cr : NEUTRAL_C);
65+
}
66+
}
67+
}
68+
1169
#define CONSOLE_START_POS 20
1270

1371
static void *xfb = NULL;
@@ -324,53 +382,44 @@ int main(int argc, char **argv) {
324382
diff_msec(last_activity, gettime()) >= IDLE_THRESHOLD_MS;
325383

326384
if (idle) {
327-
// === Screensaver: clear once on entry, then bounce a color-cycling
328-
// ASCII rendition of the joypad logo (logo_solid.svg in branding/)
329-
// around the screen, erasing the prior frame each step.
330-
static const char *logo[] = {
331-
" _________ ",
332-
" / + o o\\ ",
333-
" ( +++ ^_^ )",
334-
" \\ + o o / ",
335-
" '---------' ",
336-
};
337-
const int LOGO_W = 15;
338-
const int LOGO_H = 5;
339-
const char *blank_row = " "; // LOGO_W + 1 spaces
385+
// === Screensaver: bitmap-rendered joypad logo bounces around the XFB
386+
// directly. Color cycles through cycle_yuv[] on each wall hit.
387+
const int FB_W = rMode->fbWidth;
388+
const int FB_H = rMode->xfbHeight;
389+
const int FB_PITCH = FB_W / 2; // 32-bit words per scanline
390+
u32 *fb_words = (u32 *)xfb;
340391

341392
if (!screensaver_on) {
342-
printf("\x1b[2J");
393+
// Clear the entire framebuffer to black.
394+
u32 black = yuv_pair(BLACK_Y, BLACK_Y, NEUTRAL_C, NEUTRAL_C);
395+
for (int i = 0; i < FB_PITCH * FB_H; i++) fb_words[i] = black;
343396
screensaver_on = true;
344397
ss_prev_x = -1;
398+
ss_x = 80; // pixel coords now
399+
ss_y = 80;
345400
}
346401
ss_frame++;
347-
// Move every 4th frame for a slower glide. Two LongWait(2) per loop
348-
// iteration ≈ 33ms/frame, so this updates ~7-8 times per second.
349402
if ((ss_frame & 3) == 0) {
403+
// Erase prior bounding box.
350404
if (ss_prev_x >= 0) {
351-
for (int row = 0; row < LOGO_H; row++) {
352-
SetPosition(ss_prev_x, ss_prev_y + row);
353-
printf("%s", blank_row);
354-
}
355-
}
356-
ss_x += ss_dx;
357-
ss_y += ss_dy;
358-
// Bounds for 480p NTSC console: ~77 chars wide × ~28 tall.
359-
// Logo's left edge can travel up to (width - LOGO_W) so its right
360-
// edge reaches the screen's right side.
361-
const int max_x = 77 - LOGO_W;
362-
const int max_y = 23;
363-
if (ss_x <= 0) { ss_x = 0; ss_dx = -ss_dx; ss_color = (ss_color % 7) + 1; }
364-
if (ss_x >= max_x) { ss_x = max_x; ss_dx = -ss_dx; ss_color = (ss_color % 7) + 1; }
365-
if (ss_y <= 1) { ss_y = 1; ss_dy = -ss_dy; ss_color = (ss_color % 7) + 1; }
366-
if (ss_y >= max_y) { ss_y = max_y; ss_dy = -ss_dy; ss_color = (ss_color % 7) + 1; }
367-
SetFgColor(ss_color, 2);
368-
for (int row = 0; row < LOGO_H; row++) {
369-
SetPosition(ss_x, ss_y + row);
370-
printf("%s", logo[row]);
405+
xfb_clear_box(fb_words, FB_PITCH, ss_prev_x, ss_prev_y, LOGO_W, LOGO_H);
371406
}
372-
fflush(stdout);
373-
ss_prev_x = ss_x;
407+
// Step + bounce. Move 4 px/update so the glide is visible without
408+
// racing across the screen.
409+
ss_x += ss_dx * 4;
410+
ss_y += ss_dy * 2;
411+
const int max_x = FB_W - LOGO_W - 24; // leave overscan margin
412+
const int max_y = FB_H - LOGO_H - 24;
413+
const int min_x = 24;
414+
const int min_y = 24;
415+
if (ss_x <= min_x) { ss_x = min_x; ss_dx = -ss_dx; ss_color = (ss_color + 1) % CYCLE_LEN; }
416+
if (ss_x >= max_x) { ss_x = max_x; ss_dx = -ss_dx; ss_color = (ss_color + 1) % CYCLE_LEN; }
417+
if (ss_y <= min_y) { ss_y = min_y; ss_dy = -ss_dy; ss_color = (ss_color + 1) % CYCLE_LEN; }
418+
if (ss_y >= max_y) { ss_y = max_y; ss_dy = -ss_dy; ss_color = (ss_color + 1) % CYCLE_LEN; }
419+
// Pixel positions must be even (XFB packs 2 px/word).
420+
int draw_x = ss_x & ~1;
421+
xfb_draw_logo(fb_words, FB_PITCH, draw_x, ss_y, &cycle_yuv[ss_color]);
422+
ss_prev_x = draw_x;
374423
ss_prev_y = ss_y;
375424
}
376425
LongWait(2);

0 commit comments

Comments
 (0)