ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
bake_library.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Bake compiled .rabook files into a C header of MRAM-resident byte arrays.
5
6Each blob is embedded as the chunked RBKC container tools/epub_compile emits --
7still compressed. The firmware inflates one into SDRAM only when
8`book_open()` is called, which is what lets several full books (covers and
9inline images included) sit alongside the firmware in MRAM at all.
10
11Cover thumbnails are the reason this tool does more than embed bytes. Each
12book's cover is pre-decoded here into a gray8 thumbnail, matching the firmware's
13`sh_image_decode_gray8` exactly, so boot can draw the whole shelf without
14inflating a single book. If the two decoders ever diverge, the shelf renders
15differently before and after a book is opened.
16
17The emitted thumbnail bytes are architecture-dependent to regenerate, so
18re-baking moves the framebuffer golden -- see scripts/builders/books.sh, and re-pin
19the golden in the same change.
20
21Usage:
22 bake_library.py <out.h> <rabook>|<title>|<author> [<rabook>|<title>|<author> ...]
23"""
24
25from __future__ import annotations
26
27import struct
28import sys
29import zlib
30from pathlib import Path
31
32# Shelf thumbnail box -- MUST match k_sh_thumb_w / k_sh_thumb_h in sh_app.h.
33THUMB_W = 130
34THUMB_H = 195
35
36# Sentinel value in the .rabook header meaning "no cover image present".
37COVER_ABSENT = 0xFFFFFFFF
38# Maximum line length (characters) for emitted C array initializer rows.
39ARRAY_LINE_LIMIT = 96
40# Minimum number of argv entries: script, out.h, at least one spec.
41MIN_ARGV_COUNT = 3
42
43
44def unwrap_container(data: bytes) -> bytes:
45 """Inflate a chunked RBKC .rabook container back to its flat blob.
46
47 Keep in sync with book_container_t in apps/shared_libs/book/inc/book.h:
48 "RBKC" + <I chunk_bytes + <Q total + <I count + <I reserved(0), a
49 (count + 1)-entry <Q offset table, then count concatenated zlib streams.
50 """
51 if data[:4] != b"RBKC":
52 msg = "not an RBKC container"
53 raise ValueError(msg)
54 chunk_bytes, total, count, reserved = struct.unpack_from("<IQII", data, 4)
55 if reserved != 0 or chunk_bytes == 0 or count != (total + chunk_bytes - 1) // chunk_bytes:
56 msg = "malformed RBKC header"
57 raise ValueError(msg)
58 offsets = struct.unpack_from(f"<{count + 1}Q", data, 24)
59 payload = 24 + 8 * (count + 1)
60 blob = b"".join(
61 zlib.decompress(data[payload + offsets[i] : payload + offsets[i + 1]]) for i in range(count)
62 )
63 if len(blob) != total:
64 msg = "RBKC inflated size mismatch"
65 raise ValueError(msg)
66 return blob
67
68
69def decode_cover_thumb(blob: bytes) -> tuple[bytes, int, int] | None:
70 """Decode the book cover into a (gray8 bytes, w, h) thumbnail, or None.
71
72 Mirrors sh_image_decode_gray8 / sh_fit_box / sh_gray4_at byte-for-byte so the
73 baked thumbnail is identical to a runtime decode (keeps the render hash stable).
74 """
75 try:
76 inflated = unwrap_container(blob)
77 except ValueError:
78 return None
79
80 def u32(o: int) -> int:
81 return struct.unpack_from("<I", inflated, o)[0]
82
83 def u16(o: int) -> int:
84 return struct.unpack_from("<H", inflated, o)[0]
85
86 cover = u32(36)
87 if cover == COVER_ABSENT:
88 return None
89 img = u32(76) + (cover * 24)
90 src_w, src_h, fmt, data_off = u16(img + 4), u16(img + 6), inflated[img + 8], u32(img + 12)
91 if fmt != 0 or src_w == 0 or src_h == 0:
92 return None
93 data = inflated[u32(88) + data_off :]
94 fit_w = THUMB_W
95 fit_h = (THUMB_W * src_h) // src_w
96 if fit_h > THUMB_H:
97 fit_h = THUMB_H
98 fit_w = (THUMB_H * src_w) // src_h
99 fit_w = max(1, fit_w)
100 fit_h = max(1, fit_h)
101 out = bytearray(fit_w * fit_h)
102 for dy in range(fit_h):
103 sy = (dy * src_h) // fit_h
104 row = sy * src_w
105 for dx in range(fit_w):
106 flat = row + ((dx * src_w) // fit_w)
107 byte = data[flat >> 1]
108 nib = (byte & 0x0F) if (flat & 1) else (byte >> 4)
109 out[(dy * fit_w) + dx] = (nib << 4) | nib
110 return bytes(out), fit_w, fit_h
111
112
113def emit_array(name: str, data: bytes) -> str:
114 """Render bytes as a 4-byte-aligned `static const uint8_t` C array.
115
116 The `alignas(4)` is not cosmetic: the firmware casts into these blobs to
117 read the container's 32-bit header fields, and an unaligned load faults on
118 the target. Every byte is emitted with a `U` suffix and lines are wrapped
119 near ARRAY_LINE_LIMIT characters -- a soft limit, checked after appending,
120 so a line may overrun by one element's width.
121
122 Output is deterministic for identical input, which is what lets the
123 generated header be committed and diffed.
124
125 Args:
126 name: C identifier for the array; used unquoted and unvalidated.
127 data: Bytes to emit. Empty input still yields a well-formed (if
128 zero-length, and therefore not valid C) array.
129
130 Returns:
131 The declaration as a newline-joined string, no trailing newline.
132 """
133 out = [f"alignas(4) static const uint8_t {name}[{len(data)}U] = {{"]
134 line = " "
135 for b in data:
136 line += f"{b}U,"
137 if len(line) >= ARRAY_LINE_LIMIT:
138 out.append(line)
139 line = " "
140 if line.strip():
141 out.append(line)
142 out.append("};")
143 return "\n".join(out)
144
145
146def _load_books(specs: list[str]) -> list[tuple[bytes, str, str, tuple | None]]:
147 """Read every `<path>|<title>|<author>` spec into a blob + metadata tuple.
148
149 The `|` separator is unescaped, so a title containing a pipe splits into
150 the wrong fields and raises rather than silently baking a mangled shelf.
151 """
152 books = []
153 for spec in specs:
154 path, title, author = spec.split("|")
155 with Path(path).open("rb") as f:
156 blob = f.read()
157 books.append((blob, title, author, decode_cover_thumb(blob)))
158 return books
159
160
161def _header_preamble() -> list[str]:
162 """The generated-file banner, include guard and includes."""
163 return [
164 "/**",
165 " * @file library.h",
166 " * @generated by tools/epub_compile/src/bake_library.py -- do not edit by hand.",
167 " * @brief Baked full .rabook blobs + pre-decoded cover thumbnails (generated).",
168 " * @details Each entry is the chunked RBKC container (book_open inflates it",
169 " * on demand) plus a gray8 cover thumbnail the shelf blits without any",
170 " * boot-time inflation. Regenerate with tools/epub_compile/src/",
171 " * bake_library.py (the",
172 " * thumbnail bytes are architecture-dependent to regenerate; see",
173 " * scripts/builders/books.sh -- re-pin the fb golden when re-baking).",
174 " *",
175 " * @copyright Copyright (c) 2026 Brighton Sikarskie",
176 " * SPDX-License-Identifier: MIT",
177 " * @since Version 0.1.0",
178 " */",
179 "#pragma once",
180 "",
181 "#include <stdint.h>",
182 "",
183 "// NOLINTBEGIN(readability-magic-numbers)",
184 ]
185
186
187def _emit_book_arrays(
188 books: list[tuple[bytes, str, str, tuple | None]],
189) -> tuple[list[str], list[tuple]]:
190 """Emit one byte array per blob (and per thumbnail), in shelf order.
191
192 Returns ``(lines, names)``; ``names`` feeds the table below and carries the
193 symbol names just emitted, so the two cannot disagree about what exists.
194 """
195 parts: list[str] = []
196 names: list[tuple] = []
197 for i, (data, title, author, thumb) in enumerate(books):
198 blob_name = f"k_lib_blob{i:02d}"
199 parts.append(f'/** @brief "{title}" by {author} -- {len(data)} bytes. */')
200 parts.append(emit_array(blob_name, data))
201 thumb_name, tw, th = "nullptr", 0, 0
202 if thumb is not None:
203 tbytes, tw, th = thumb
204 thumb_name = f"k_lib_thumb{i:02d}"
205 parts.append(f"/** @brief Cover thumbnail for {blob_name} ({tw}x{th} gray8). */")
206 parts.append(emit_array(thumb_name, tbytes))
207 names.append((blob_name, len(data), title, author, thumb_name, tw, th))
208 parts.append("")
209 return parts, names
210
211
212def _emit_library_table(count: int, names: list[tuple]) -> list[str]:
213 """Emit the library_book_t struct, the count enum and the shelf table.
214
215 Command-line order is shelf order, and this preserves it.
216 """
217 parts = [
218 "/** @brief One openable baked book: compressed blob + cover thumbnail + metadata. */",
219 "typedef struct {",
220 " const uint8_t* blob; /**< RBKC container start. */",
221 " uint32_t len; /**< Container length in bytes. */",
222 " const uint8_t* thumb; /**< gray8 cover thumbnail, or NULL. */",
223 " uint16_t thumb_w; /**< Thumbnail width in pixels. */",
224 " uint16_t thumb_h; /**< Thumbnail height in pixels. */",
225 " const char* title; /**< Display title. */",
226 " const char* author; /**< Display author. */",
227 "} library_book_t;",
228 "",
229 "typedef enum : uint16_t {",
230 f" k_library_count = {count}U, /**< Number of entries in k_library. */",
231 "} library_count_t;",
232 "",
233 "static const library_book_t k_library[k_library_count] = {",
234 ]
235 for blob_name, n, title, author, thumb_name, tw, th in names:
236 t = title.replace('"', '\\"')
237 a = author.replace('"', '\\"')
238 parts.append(f' {{ {blob_name}, {n}U, {thumb_name}, {tw}U, {th}U, "{t}", "{a}" }},')
239 parts.extend(["};", "// NOLINTEND(readability-magic-numbers)", ""])
240 return parts
241
242
243def main(argv: list[str]) -> int:
244 """Bake every `<path>|<title>|<author>` spec into the output header.
245
246 Title and author are passed on the command line rather than read from the
247 blob because the baked shelf shows them before any book is inflated -- the
248 metadata inside the container is not reachable without inflating it.
249
250 The `|` separator is unescaped, so a title containing a pipe splits into the
251 wrong fields and raises. Books appear in the header in command-line order,
252 and that order is the shelf order.
253
254 Args:
255 argv: Full argument vector INCLUDING the program name at index 0.
256
257 Returns:
258 0 on success, 2 on a usage error.
259
260 Raises:
261 ValueError: A spec did not split into exactly three fields.
262 OSError: A .rabook path could not be read, or the header not written.
263 """
264 if len(argv) < MIN_ARGV_COUNT:
265 sys.stderr.write("usage: bake_library.py <out.h> <rabook>|<title>|<author> ...\n")
266 return 2
267 out_path = argv[1]
268 books = _load_books(argv[2:])
269 arrays, names = _emit_book_arrays(books)
270 parts = [*_header_preamble(), *arrays, *_emit_library_table(len(books), names)]
271 with Path(out_path).open("w") as f:
272 f.write("\n".join(parts))
273 sys.stderr.write(
274 f"bake_library: wrote {out_path} ({len(books)} books, "
275 f"{sum(len(d) for d, _, _, _ in books)} blob bytes + thumbnails)\n"
276 )
277 return 0
278
279
280if __name__ == "__main__":
281 sys.exit(main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298