Skip to content

Commit e8f7f60

Browse files
Aspect-correct logo + flicker-free single-pass compose.
make_logo.py now preserves the silhouette's natural aspect ratio. Source logo is 397x337 (≈1.18:1), so output dimensions auto-fit: big logo is 160x135, small corner logo is 64x54. Width is always rounded to a multiple of 8 (1-bit packing) which is also a multiple of 2 (XFB pair packing). Screensaver replaces the erase-then-draw two-step with xfb_compose_logo: walks the union bbox of (new, prev) positions once, writing each pixel pair's final value (logo color or black) exactly once. Eliminates the brief just-erased / not-yet-drawn window where scanout could read pure black where the sprite should be — that was the residual flicker source.
1 parent e80641e commit e8f7f60

2 files changed

Lines changed: 78 additions & 34 deletions

File tree

gamecube/buildtools/make_logo.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,21 @@ def find_source() -> Path:
5454
sys.exit('No source PNG found; tried: ' + ', '.join(str(p) for p in CANDIDATES))
5555

5656

57-
def pack_mask(flat: Image.Image, w: int, h: int):
57+
def pack_mask(flat: Image.Image, max_w: int, max_h: int):
5858
"""Crop `flat` (grayscale, dark = silhouette) to its silhouette bbox,
59-
resize to w×h, and pack into a 1-bit alpha mask (MSB-first per byte).
60-
Cropping first eliminates the empty padding around the silhouette so
61-
placement in the framebuffer can align cleanly with the console grid."""
59+
resize to fit inside `max_w` × `max_h` while preserving the silhouette's
60+
natural aspect ratio, and pack into a 1-bit alpha mask. Output width is
61+
rounded down to an even number (XFB packs 2 px / word). Returns
62+
(packed_bytes, bytes_per_row, lit_count, actual_w, actual_h)."""
6263
bw = flat.point(lambda v: 255 if v < 128 else 0, 'L')
6364
bbox = bw.getbbox()
6465
src = flat.crop(bbox) if bbox else flat
66+
sw, sh = src.size
67+
scale = min(max_w / sw, max_h / sh)
68+
# Width rounded to a multiple of 8 (1-bit packing) which is also a
69+
# multiple of 2 (XFB pair packing). Height has no such constraint.
70+
w = max(8, (int(sw * scale) // 8) * 8)
71+
h = max(1, int(sh * scale))
6572
scaled = src.resize((w, h), Image.LANCZOS)
6673
bytes_per_row = w // 8
6774
packed = bytearray(bytes_per_row * h)
@@ -71,7 +78,7 @@ def pack_mask(flat: Image.Image, w: int, h: int):
7178
if scaled.getpixel((x, y)) < 128:
7279
packed[y * bytes_per_row + x // 8] |= 0x80 >> (x % 8)
7380
lit += 1
74-
return packed, bytes_per_row, lit
81+
return packed, bytes_per_row, lit, w, h
7582

7683

7784
def emit_mask(f, name: str, w: int, h: int, packed: bytes,
@@ -133,22 +140,21 @@ def main() -> None:
133140
bg = Image.new('RGBA', img.size, (255, 255, 255, 255))
134141
flat = Image.alpha_composite(bg, img).convert('L')
135142

136-
big = pack_mask(flat, LOGO_W, LOGO_H)
137-
small = pack_mask(flat, LOGO_SMALL_W, LOGO_SMALL_H)
143+
big_packed, big_bpr, big_lit, big_w, big_h = pack_mask(flat, LOGO_W, LOGO_H)
144+
s_packed, s_bpr, s_lit, s_w, s_h = pack_mask(flat, LOGO_SMALL_W, LOGO_SMALL_H)
138145
title_packed, title_bpr, title_lit, title_w, title_h = render_title_bitmap()
139146

140147
OUT.parent.mkdir(parents=True, exist_ok=True)
141148
with OUT.open('w') as f:
142149
f.write('// Auto-generated by buildtools/make_logo.py — DO NOT EDIT.\n')
143150
f.write(f'// Source: {src}\n\n')
144151
f.write('#ifndef GEN_LOGO_H\n#define GEN_LOGO_H\n\n')
145-
emit_mask(f, 'logo', LOGO_W, LOGO_H, big[0], big[1], big[2])
146-
emit_mask(f, 'logo_small', LOGO_SMALL_W, LOGO_SMALL_H,
147-
small[0], small[1], small[2])
152+
emit_mask(f, 'logo', big_w, big_h, big_packed, big_bpr, big_lit)
153+
emit_mask(f, 'logo_small', s_w, s_h, s_packed, s_bpr, s_lit)
148154
emit_mask(f, 'title', title_w, title_h, title_packed, title_bpr, title_lit)
149155
f.write('#endif // GEN_LOGO_H\n')
150-
print(f'wrote {OUT} (big {big[2]} lit, small {small[2]} lit, '
151-
f'title {title_w}x{title_h})')
156+
print(f'wrote {OUT} (big {big_w}x{big_h} {big_lit} lit, '
157+
f'small {s_w}x{s_h} {s_lit} lit, title {title_w}x{title_h})')
152158

153159

154160
if __name__ == '__main__':

gamecube/ppc/main.c

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ static inline u32 yuv_pair(u8 y0, u8 y1, u8 cb, u8 cr) {
3333
return ((u32)y0 << 24) | ((u32)cb << 16) | ((u32)y1 << 8) | cr;
3434
}
3535

36+
__attribute__((unused))
3637
static void xfb_clear_box(u32 *fb_words, int fb_pitch_words, int x_px,
3738
int y_px, int w, int h) {
3839
u32 black = yuv_pair(BLACK_Y, BLACK_Y, NEUTRAL_C, NEUTRAL_C);
@@ -71,6 +72,51 @@ static void xfb_draw_logo(u32 *fb_words, int fb_pitch_words, int x_px,
7172
LOGO_H, LOGO_BYTES_PER_ROW, col);
7273
}
7374

75+
// Single-pass screensaver composer: walks the union rect of (new logo,
76+
// old logo), writing each pixel pair exactly once. Pixels inside the new
77+
// logo bbox use mask + color, pixels outside that bbox (including where
78+
// the old logo was) get plain black. Eliminates the brief
79+
// just-erased/not-yet-drawn window that the old erase+draw two-step
80+
// created — that window was the source of the screensaver flicker on
81+
// scanouts that landed inside it.
82+
static void xfb_compose_logo(u32 *fb_words, int fb_pitch_words,
83+
int new_x, int new_y, int prev_x, int prev_y,
84+
const yuv_t *col) {
85+
// Even-aligned union bbox (XFB packs 2 px / word).
86+
int ux1 = (new_x < prev_x ? new_x : prev_x) & ~1;
87+
int uy1 = new_y < prev_y ? new_y : prev_y;
88+
int rmax = (new_x + LOGO_W > prev_x + LOGO_W ? new_x : prev_x) + LOGO_W;
89+
int ux2 = (rmax + 1) & ~1; // round up to even
90+
int uy2 = (new_y + LOGO_H > prev_y + LOGO_H ? new_y : prev_y) + LOGO_H;
91+
if (ux1 < 0) ux1 = 0;
92+
if (uy1 < 0) uy1 = 0;
93+
const u8 black_y = BLACK_Y, neutral = NEUTRAL_C;
94+
95+
for (int y = uy1; y < uy2; y++) {
96+
int rel_y = y - new_y;
97+
bool y_in_logo = rel_y >= 0 && rel_y < LOGO_H;
98+
const u8 *mask_row = y_in_logo ? &logo_mask[rel_y * LOGO_BYTES_PER_ROW]
99+
: NULL;
100+
u32 *line = fb_words + y * fb_pitch_words;
101+
for (int x = ux1; x < ux2; x += 2) {
102+
bool lit_a = false, lit_b = false;
103+
if (mask_row) {
104+
int rel_a = x - new_x;
105+
int rel_b = rel_a + 1;
106+
if (rel_a >= 0 && rel_a < LOGO_W)
107+
lit_a = mask_row[rel_a / 8] & (0x80 >> (rel_a % 8));
108+
if (rel_b >= 0 && rel_b < LOGO_W)
109+
lit_b = mask_row[rel_b / 8] & (0x80 >> (rel_b % 8));
110+
}
111+
u8 ya = lit_a ? col->y : black_y;
112+
u8 yb = lit_b ? col->y : black_y;
113+
bool any = lit_a || lit_b;
114+
line[x / 2] = yuv_pair(ya, yb, any ? col->cb : neutral,
115+
any ? col->cr : neutral);
116+
}
117+
}
118+
}
119+
74120
// Console column 0 sits at pixel x=20 (CONSOLE_START_POS, defined below).
75121
// With cropped masks (no internal padding) the bounding box left edge IS
76122
// the silhouette's visible edge — so placing the logo at x=20 makes its
@@ -424,36 +470,28 @@ int main(int argc, char **argv) {
424470
ss_x = 80; // pixel coords now
425471
ss_y = 80;
426472
}
427-
// Update every frame with small increments → smooth glide.
428-
if (ss_prev_x >= 0) {
429-
xfb_clear_box(fb_words, FB_PITCH, ss_prev_x, ss_prev_y, LOGO_W, LOGO_H);
430-
}
431-
// 4px / 3px per frame at 30Hz → ~120/90 px/sec (interlaced display
432-
// shows two fields per frame; updating once per pair keeps both
433-
// fields displaying the SAME logo position so we don't get the
434-
// even/odd-line flicker that line-doubled sprites create when
435-
// their content shifts mid-frame).
473+
// Step + bounce.
436474
ss_x += ss_dx * 4;
437475
ss_y += ss_dy * 3;
438-
// Bounce against the actual framebuffer edges. Whatever the TV
439-
// overscan eats happens at the same outer ring regardless, so
440-
// letting the sprite touch x=0 / x=FB_W-LOGO_W maximizes visible
441-
// travel on CRTs with conservative overscan.
442476
const int max_x = FB_W - LOGO_W;
443477
const int max_y = FB_H - LOGO_H;
444-
const int min_x = 0;
445-
const int min_y = 0;
446-
if (ss_x <= min_x) { ss_x = min_x; ss_dx = -ss_dx; ss_color = (ss_color + 1) % CYCLE_LEN; }
478+
if (ss_x <= 0) { ss_x = 0; ss_dx = -ss_dx; ss_color = (ss_color + 1) % CYCLE_LEN; }
447479
if (ss_x >= max_x) { ss_x = max_x; ss_dx = -ss_dx; ss_color = (ss_color + 1) % CYCLE_LEN; }
448-
if (ss_y <= min_y) { ss_y = min_y; ss_dy = -ss_dy; ss_color = (ss_color + 1) % CYCLE_LEN; }
480+
if (ss_y <= 0) { ss_y = 0; ss_dy = -ss_dy; ss_color = (ss_color + 1) % CYCLE_LEN; }
449481
if (ss_y >= max_y) { ss_y = max_y; ss_dy = -ss_dy; ss_color = (ss_color + 1) % CYCLE_LEN; }
450-
// Pixel positions must be even (XFB packs 2 px/word).
451-
int draw_x = ss_x & ~1;
452-
xfb_draw_logo(fb_words, FB_PITCH, draw_x, ss_y, &cycle_yuv[ss_color]);
482+
int draw_x = ss_x & ~1; // even alignment for XFB pair packing
483+
int prev_x = ss_prev_x >= 0 ? ss_prev_x : draw_x;
484+
int prev_y = ss_prev_x >= 0 ? ss_prev_y : ss_y;
485+
// One pass over the union of (new, old) bbox: each pixel-pair gets
486+
// its final value (logo or black) written exactly once. No
487+
// intermediate just-cleared-not-yet-drawn frames for scanout to
488+
// catch → no flicker.
489+
xfb_compose_logo(fb_words, FB_PITCH, draw_x, ss_y, prev_x, prev_y,
490+
&cycle_yuv[ss_color]);
453491
ss_prev_x = draw_x;
454492
ss_prev_y = ss_y;
455-
LongWait(2); // 30 Hz update — pairs an even+odd field on the
456-
// same logo position, eliminating interlace flicker.
493+
LongWait(2); // 30 Hz update — both interlaced fields show the
494+
// same logo position, so even/odd lines agree.
457495
continue;
458496
}
459497

0 commit comments

Comments
 (0)