From 9d5b3af7a39cd012b8e42ecb978f70df4d11c7bc Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Thu, 24 Sep 2026 22:32:00 +0200 Subject: [PATCH] Add read-only assets to overlay apps and improve apps --- .gitignore | 1 + App/apps/app_api.h | 25 +++- App/apps/app_art.py | 140 ++++++++++++++++++++++ App/apps/app_assets.py | 107 +++++++++++++++++ App/apps/app_overlay.c | 72 +++++++++++- App/apps/app_overlay.h | 20 +++- App/apps/beacon/beacon_app.c | 61 +++++----- App/apps/beacon/build.sh | 15 +-- App/apps/beacon/gen_assets.py | 30 +++++ App/apps/beam/beam_app.c | 23 ++-- App/apps/beam/build.sh | 15 +-- App/apps/beam/gen_assets.py | 15 +++ App/apps/breakout/breakout_app.c | 60 +++++++--- App/apps/breakout/build.sh | 15 +-- App/apps/breakout/gen_assets.py | 53 +++++++++ App/apps/cube3d/build.sh | 15 +-- App/apps/cube3d/cube3d_app.c | 154 ++++++++++--------------- App/apps/cube3d/gen_assets.py | 90 +++++++++++++++ App/apps/fm/build.sh | 15 +-- App/apps/fm/fm_app.c | 36 +++--- App/apps/fm/gen_assets.py | 22 ++++ App/apps/foxhunt/build.sh | 15 +-- App/apps/foxhunt/foxhunt_app.c | 63 +++++----- App/apps/foxhunt/gen_assets.py | 34 ++++++ App/apps/pack_app.py | 25 +++- App/apps/plasma/build.sh | 15 +-- App/apps/plasma/gen_assets.py | 39 +++++++ App/apps/plasma/plasma_app.c | 56 +++------ App/apps/rapidroll/build.sh | 15 +-- App/apps/rapidroll/gen_assets.py | 60 ++++++++++ App/apps/rapidroll/rapidroll_app.c | 94 ++++++++------- App/apps/snake/build.sh | 15 +-- App/apps/snake/gen_assets.py | 79 +++++++++++++ App/apps/snake/snake_app.c | 107 ++++++++++------- App/apps/spaceimpact/build.sh | 15 +-- App/apps/spaceimpact/gen_assets.py | 62 ++++++++++ App/apps/spaceimpact/spaceimpact_app.c | 116 ++++++++++--------- App/apps/tetris/build.sh | 15 +-- App/apps/tetris/gen_assets.py | 65 +++++++++++ App/apps/tetris/tetris_app.c | 99 ++++++++++------ App/scheduler.c | 7 ++ App/scheduler.h | 5 + compile-app.sh | 7 +- 43 files changed, 1478 insertions(+), 514 deletions(-) create mode 100644 App/apps/app_art.py create mode 100755 App/apps/app_assets.py create mode 100755 App/apps/beacon/gen_assets.py create mode 100755 App/apps/beam/gen_assets.py create mode 100755 App/apps/breakout/gen_assets.py create mode 100755 App/apps/cube3d/gen_assets.py create mode 100755 App/apps/fm/gen_assets.py create mode 100755 App/apps/foxhunt/gen_assets.py create mode 100755 App/apps/plasma/gen_assets.py create mode 100755 App/apps/rapidroll/gen_assets.py create mode 100755 App/apps/snake/gen_assets.py create mode 100755 App/apps/spaceimpact/gen_assets.py create mode 100755 App/apps/tetris/gen_assets.py diff --git a/.gitignore b/.gitignore index 865848f0..ed69d57a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ App/apps/*/*.bin App/apps/*/*.map App/apps/*/*.app App/apps/*/*_app_blob.h +App/apps/*/*_assets.h build/apps/ diff --git a/App/apps/app_api.h b/App/apps/app_api.h index 8dc22d33..73f3c4a5 100644 --- a/App/apps/app_api.h +++ b/App/apps/app_api.h @@ -38,10 +38,14 @@ #include #include -/* First unpublished/public baseline: all services currently present below are - * ABI major 1, API level 1. */ +/* API levels within ABI major 1: + * 1 baseline: every service up to and including beam_draw + * 2 ticks_ms, rand32, asset_read (+ app_header_t asset_size / asset_crc) */ #define APP_ABI_MAJOR 1u -#define APP_API_LEVEL 1u +#define APP_API_LEVEL 2u + +/* Minimum API level of an app that ships read-only assets (pack_app.py). */ +#define APP_API_ASSETS 2u /* KEY codes mirrored from driver/keyboard.h (enum KEY_Code_e). Kept in sync by * value so the app stays independent of the firmware headers. */ @@ -226,6 +230,21 @@ typedef struct app_api { void (*beam_rx)(bool start); /* arm or stop FSK reception */ uint8_t (*beam_rx_poll)(uint16_t *packet); /* APP_BEAM_RX_* */ void (*beam_draw)(const char *status); /* MAIN display with one BEAM center line */ + + /* ---- API level 2: time, randomness, read-only assets ---- */ + /* Free-running millisecond clock with 10 ms resolution (SysTick). Compare + * with unsigned subtraction: (api->ticks_ms() - start) >= period. */ + uint32_t (*ticks_ms)(void); + /* xorshift32 PRNG, never 0. The resident state persists across launches + * and is re-mixed with RSSI noise and SysTick jitter at each launch, so apps + * need no seed of their own. */ + uint32_t (*rand32)(void); + /* Copy len bytes of this app's assets, starting at offset, into buf. The + * read is clamped to the packed asset size; returns the byte count copied + * (0 past the end or when the app has no assets). The loader verified the + * assets' CRC before launch. buf may live in the overlay (.bss) or on the + * stack. */ + uint16_t (*asset_read)(uint16_t offset, void *buf, uint16_t len); } app_api_t; /* BK4819 AF modes for set_af (mirror driver/bk4819.h values). */ diff --git a/App/apps/app_art.py b/App/apps/app_art.py new file mode 100644 index 00000000..304c314f --- /dev/null +++ b/App/apps/app_art.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# Shared artwork helpers for overlay-app title screens (used by gen_assets.py). +# +# Canvas is a 128x64 1-bit picture. Canvas.pages() packs it the way the LCD +# buffers are laid out: 8 pages of 128 column bytes, LSB = top row; page 0 is +# the status line, pages 1..7 the frame buffer. An app streams the result into +# A->status_line and A->fb with two asset_read calls. +# +# The logo font draws letters as stroke skeletons: polylines on a 14-row grid +# (x right, y down), scaled, drawn with a square pen and sheared into italics. +# Corners are cut by diagonal segments (chamfered arcade look), and each word +# is outlined by a one-pixel ring drawn one blank pixel away from the letters. +W, H = 128, 64 +PROMPT_TOP = 56 # page 7 stays blank: the app blinks its prompt there + +LOGO = { + "A": (9, [[(0, 13), (0, 3), (3, 0), (6, 0), (9, 3), (9, 13)], [(0, 8), (9, 8)]]), + "B": (9, [[(0, 13), (0, 0), (7, 0), (9, 2), (9, 4), (7, 6), (0, 6)], + [(7, 6), (9, 8), (9, 11), (7, 13), (0, 13)]]), + "C": (9, [[(9, 0), (3, 0), (0, 3), (0, 10), (3, 13), (9, 13)]]), + "D": (9, [[(0, 0), (6, 0), (9, 3), (9, 10), (6, 13), (0, 13), (0, 0)]]), + "E": (9, [[(9, 0), (0, 0), (0, 13), (9, 13)], [(0, 6), (6, 6)]]), + "I": (6, [[(0, 0), (6, 0)], [(3, 0), (3, 13)], [(0, 13), (6, 13)]]), + "K": (9, [[(0, 0), (0, 13)], [(9, 0), (3, 6), (0, 6)], [(3, 6), (9, 12), (9, 13)]]), + "L": (8, [[(0, 0), (0, 13), (8, 13)]]), + "M": (12, [[(0, 13), (0, 0), (6, 6), (12, 0), (12, 13)]]), + "N": (9, [[(0, 13), (0, 0), (9, 13), (9, 0)]]), + "O": (9, [[(3, 0), (6, 0), (9, 3), (9, 10), (6, 13), (3, 13), (0, 10), (0, 3), (3, 0)]]), + "P": (9, [[(0, 13), (0, 0), (7, 0), (9, 2), (9, 5), (7, 7), (0, 7)]]), + "R": (9, [[(0, 13), (0, 0), (7, 0), (9, 2), (9, 5), (7, 7), (0, 7)], [(5, 7), (9, 11), (9, 13)]]), + "S": (9, [[(9, 0), (2, 0), (0, 2), (0, 4), (2, 6), (7, 6), (9, 8), (9, 11), (7, 13), (0, 13)]]), + "T": (10, [[(0, 0), (10, 0)], [(5, 0), (5, 13)]]), + "U": (9, [[(0, 0), (0, 10), (3, 13), (6, 13), (9, 10), (9, 0)]]), +} +LOGO_KX, LOGO_KY = 1.0, 1.1 # default skeleton scale +LOGO_BRUSH = 3 # stroke thickness in pixels +LOGO_GAP = 3 # pixels between letters +LOGO_SHEAR = 4 # one pixel of slant every LOGO_SHEAR rows +LOGO_RING = 2 # outline distance from the letters (0 = none) + +def line(pts, x0, y0, x1, y1): + """Bresenham segment into the pts set.""" + dx, dy = abs(x1 - x0), -abs(y1 - y0) + sx, sy = (1 if x0 < x1 else -1), (1 if y0 < y1 else -1) + err = dx + dy + while True: + pts.add((x0, y0)) + if x0 == x1 and y0 == y1: + return + e2 = 2 * err + if e2 >= dy: + err += dy + x0 += sx + if e2 <= dx: + err += dx + y0 += sy + +def logo_glyph(ch, kx, ky): + """(advance width, pixel set) of one logo letter, before shearing.""" + width, polylines = LOGO[ch] + skeleton = set() + for poly in polylines: + pts = [(round(x * kx), round(y * ky)) for x, y in poly] + for a, b in zip(pts, pts[1:]): + line(skeleton, *a, *b) + pixels = {(x + dx, y + dy) for x, y in skeleton + for dx in range(LOGO_BRUSH) for dy in range(LOGO_BRUSH)} + return round(width * kx) + LOGO_BRUSH, pixels + +class Canvas: + def __init__(self): + self.px = [[0] * W for _ in range(H)] + + def set(self, x, y, on=1): + if 0 <= x < W and 0 <= y < H: + self.px[y][x] = on + + def rect(self, x0, y0, x1, y1): + for y in range(y0, y1 + 1): + for x in range(x0, x1 + 1): + self.set(x, y) + + def sprite(self, x, y, cols, scale=1): + """Column-major sprite (LSB = top row), as the apps store them.""" + for i, c in enumerate(cols): + for bit in range(8): + if c >> bit & 1: + for dx in range(scale): + for dy in range(scale): + self.set(x + i * scale + dx, y + bit * scale + dy) + + def word(self, text, y, kx=LOGO_KX, ky=LOGO_KY): + """Centered, outlined italic logo word; returns its letter height.""" + height = round(13 * ky) + LOGO_BRUSH + glyphs = [logo_glyph(ch, kx, ky) for ch in text] + total = sum(w for w, _ in glyphs) + LOGO_GAP * (len(glyphs) - 1) + (height - 1) // LOGO_SHEAR + x = (W - total) // 2 + solid = set() + for width, pixels in glyphs: + for gx, gy in pixels: + solid.add((x + gx + (height - 1 - gy) // LOGO_SHEAR, y + gy)) + x += width + LOGO_GAP + grown = [solid] # solid dilated by 0, 1, ... pixels + for _ in range(LOGO_RING): + grown.append({(px + dx, py + dy) for px, py in grown[-1] + for dx in (-1, 0, 1) for dy in (-1, 0, 1)}) + ring = grown[-1] - grown[-2] if LOGO_RING else set() + for px, py in solid | ring: + self.set(px, py) + return height + + def two_words(self, first, second): + """Logo on two lines from the top of the screen; returns the bottom row.""" + top = LOGO_RING # keep the outline on screen + height = self.word(first, top) + second_top = top + height + 2 * LOGO_RING + 1 + return second_top + self.word(second, second_top) + LOGO_RING + + def pages(self): + """Title-screen payload: status page then the 7 frame-buffer pages.""" + if any(self.px[y][x] for y in range(PROMPT_TOP, H) for x in range(W)): + raise SystemExit("title: the bottom page is reserved for the prompt") + out = bytearray() + for page in range(H // 8): + for x in range(W): + b = 0 + for bit in range(8): + if self.px[page * 8 + bit][x]: + b |= 1 << bit + out.append(b) + return bytes(out) + + def preview(self, path, scale=4): + """Binary PGM, LCD-like colours (dark pixels on a pale panel).""" + with open(path, "wb") as f: + f.write(b"P5 %d %d 255\n" % (W * scale, H * scale)) + for y in range(H): + row = bytes((40 if self.px[y][x] else 200) + for x in range(W) for _ in range(scale)) + f.write(row * scale) diff --git a/App/apps/app_assets.py b/App/apps/app_assets.py new file mode 100755 index 00000000..4e4b478f --- /dev/null +++ b/App/apps/app_assets.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# Shared builder for overlay-app read-only assets (served by api->asset_read). +# +# Each app has a gen_assets.py that declares its texts and data with this +# module, then calls Assets.main(): it writes the asset blob packed into the +# .app by pack_app.py, and a C header of offsets the app includes. The header +# is generated before compiling, so code and data can never drift apart. +# +# a = Assets("SNAKE") +# a.text("T_PAUSE", "PAUSE") # NUL-terminated string +# a.table("T_STATE", ["TX", "RX"]) # fixed-stride strings (+ _STRIDE) +# a.u16("SPR_HEAD", [0x6566, 0x0FD2]) # packed little-endian data +# a.main() # argv: +# +# Texts come first, so their offsets stay small (cheap immediates in Thumb +# code). TEXT_MAX is the longest text including its NUL: the size of the +# app's shared text buffer, read in one asset_read by T(). +import os, re, struct, sys + +HERE = os.path.dirname(os.path.abspath(__file__)) + +def _cdefine(header, name): + pat = re.compile(r"^\s*#define\s+" + re.escape(name) + r"\s+(0[xX][0-9a-fA-F]+|\d+)[uUlL]*\b") + with open(os.path.join(HERE, header)) as f: + for line in f: + m = pat.match(line) + if m: + return int(m.group(1), 0) + sys.exit(f"{header}: #define {name} not found") + +ASSET_MAX = _cdefine("app_overlay.h", "APP_ASSET_MAX") + +class Assets: + def __init__(self, guard): + self.guard = guard + self.texts = [] # (name, bytes) + self.data = [] # (name, bytes) + self.extra = [] # (name, value) plain constants + self.names = set() + + def _name(self, name): + if name in self.names or not re.fullmatch(r"[A-Z][A-Z0-9_]*", name): + sys.exit(f"asset name {name!r} is invalid or duplicated") + self.names.add(name) + + def text(self, name, s): + self._name(name) + self.texts.append((name, s.encode("ascii") + b"\x00")) + + def table(self, name, strings, stride=None): + """Fixed-stride string table: entry i is at name + i * name_STRIDE.""" + self._name(name) + width = stride or max(len(s) for s in strings) + 1 + if any(len(s) + 1 > width for s in strings): + sys.exit(f"{name}: an entry does not fit the {width} B stride") + blob = b"".join(s.encode("ascii").ljust(width, b"\x00") for s in strings) + self.texts.append((name, blob)) + self.const(name + "_STRIDE", width) + + def const(self, name, value): + self._name(name) + self.extra.append((name, value)) + + def raw(self, name, payload): + self._name(name) + self.data.append((name, bytes(payload))) + + def u8(self, name, values): self.raw(name, struct.pack(f"<{len(values)}B", *values)) + def i8(self, name, values): self.raw(name, struct.pack(f"<{len(values)}b", *values)) + def u16(self, name, values): self.raw(name, struct.pack(f"<{len(values)}H", *values)) + def i16(self, name, values): self.raw(name, struct.pack(f"<{len(values)}h", *values)) + def u32(self, name, values): self.raw(name, struct.pack(f"<{len(values)}I", *values)) + + def build(self): + blob = bytearray() + lines = [] + text_max = 1 + strides = dict(self.extra) + for name, payload in self.texts: + lines.append((name, len(blob), len(payload))) + text_max = max(text_max, strides.get(name + "_STRIDE", len(payload))) + blob += payload + for name, payload in self.data: # asset_read copies bytes: no alignment needed + lines.append((name, len(blob), len(payload))) + blob += payload + if len(blob) > ASSET_MAX: + sys.exit(f"assets {len(blob)} B exceed the {ASSET_MAX} B asset area") + return bytes(blob), lines, text_max + + def main(self): + if len(sys.argv) != 3: + sys.exit("usage: gen_assets.py ") + blob, lines, text_max = self.build() + with open(sys.argv[1], "wb") as f: + f.write(blob) + guard = self.guard.upper() + "_ASSETS_H" + out = [f"/* Generated by gen_assets.py - do not edit ({len(blob)} B of assets). */", + f"#ifndef {guard}", f"#define {guard}", "", + f"#define TEXT_MAX {text_max}u /* longest text incl. NUL */"] + for name, off, size in lines: + out.append(f"#define {name} {off}u") + out.append(f"#define {name}_LEN {size}u") + for name, value in self.extra: + out.append(f"#define {name} {value}u") + out += ["", f"#endif /* {guard} */", ""] + with open(sys.argv[2], "w") as f: + f.write("\n".join(out)) diff --git a/App/apps/app_overlay.c b/App/apps/app_overlay.c index ef4b98cb..5762a95d 100644 --- a/App/apps/app_overlay.c +++ b/App/apps/app_overlay.c @@ -50,12 +50,17 @@ #include "functions.h" #include "frequencies.h" #include "radio.h" +#include "scheduler.h" #include "helper/battery.h" #include "settings.h" #include "misc.h" /* dBmCorrTable */ _Static_assert(sizeof(app_header_t) == 64, "app_header_t must be 64 bytes"); _Static_assert(sizeof(app_api_t) <= UINT16_MAX, "app_api_t size field overflow"); +_Static_assert(APP_ASSET_OFFSET + APP_ASSET_MAX == APP_CODE_OFFSET, + "assets must end where the code sector starts"); +_Static_assert(APP_ASSET_MAX <= APP_OVERLAY_MAX, + "assets are CRC-checked through the overlay buffer"); enum { APP_AVAILABLE_CAPS = 0u @@ -420,6 +425,49 @@ static void app_cfg_save(const uint8_t *buf, uint8_t len) app_cfg_len = len; /* mark dirty; the loader commits after the app returns */ } +_Static_assert(APP_CFG_OFFSET + sizeof(app_cfg_buf) <= APP_ASSET_OFFSET, + "config area overlaps the assets"); + +/* ---- API level 2: time, randomness, read-only assets ---- */ +static uint16_t app_asset_size; /* verified asset size of the running app */ +static uint32_t app_rng_state = 0x2545F491u; + +static uint32_t app_ticks_ms(void) +{ + return SCHEDULER_GetTick10ms() * 10u; +} + +static uint32_t app_rand32(void) +{ + uint32_t x = app_rng_state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + app_rng_state = x; + return x; +} + +/* Fold fresh entropy into the persistent state: RSSI noise LSBs, the SysTick + * phase of the key press that launched the app, and the 10 ms counter. */ +static void app_rng_mix(void) +{ + app_rng_state ^= ((uint32_t)BK4819_ReadRegister(BK4819_REG_67) << 16) ^ + SysTick->VAL ^ SCHEDULER_GetTick10ms(); + if (app_rng_state == 0u) + app_rng_state = 0x2545F491u; + app_rand32(); +} + +static uint16_t app_asset_read(uint16_t offset, void *buf, uint16_t len) +{ + if (offset >= app_asset_size) + return 0; + if (len > app_asset_size - offset) + len = app_asset_size - offset; + PY25Q16_ReadBuffer(APP_SLOT_BASE(app_run_slot) + APP_ASSET_OFFSET + offset, buf, len); + return len; +} + /* ---- v2 battery / backlight ---- */ static void app_draw_battery(void) { @@ -517,7 +565,8 @@ uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header) if (h.required_caps & ~APP_AVAILABLE_CAPS) return APP_ERR_CAP; if (h.code_size < 2u || h.code_size > APP_OVERLAY_MAX || (uint32_t)h.entry_off > h.code_size - 2u || /* leave room for a 2-byte Thumb insn */ - (h.entry_off & 1u) != 0u) /* entry must be Thumb-aligned (even) */ + (h.entry_off & 1u) != 0u || /* entry must be Thumb-aligned (even) */ + h.asset_size > APP_ASSET_MAX) return APP_ERR_SIZE; if (out_header) @@ -685,6 +734,9 @@ static const app_api_t app_api = { .beam_rx_poll = app_beam_rx_poll, .beam_draw = app_beam_draw, #endif + .ticks_ms = app_ticks_ms, + .rand32 = app_rand32, + .asset_read = app_asset_read, }; uint8_t APP_LaunchOverlay(uint8_t slot) @@ -711,12 +763,20 @@ uint8_t APP_LaunchOverlay(uint8_t slot) /* Repurpose the sector cache: drop any cached config sector, load the code * straight in (ReadBuffer bypasses the cache), and verify it in RAM before - * trusting it. Zeroing first leaves the app's .bss clean. */ + * trusting it. Zeroing first leaves the app's .bss clean. The assets are + * verified first through the same buffer: a slot written by a host that + * does not know the asset area (older UV Studio) is refused here instead of + * handing the app unprogrammed flash. */ PY25Q16_InvalidateCache(); + bool assets_ok = true; + if (h.asset_size) { + PY25Q16_ReadBuffer(APP_SLOT_BASE(slot) + APP_ASSET_OFFSET, ws, h.asset_size); + assets_ok = (uint16_t)MB_Crc32Bytes(ws, h.asset_size) == h.asset_crc; + } memset(ws, 0, APP_OVERLAY_MAX); PY25Q16_ReadBuffer(APP_SLOT_BASE(slot) + APP_CODE_OFFSET, ws, h.code_size); - if (MB_Crc32Bytes(ws, h.code_size) != h.code_crc32) { + if (!assets_ok || MB_Crc32Bytes(ws, h.code_size) != h.code_crc32) { PY25Q16_InvalidateCache(); #ifdef ENABLE_FEAT_F4HWN_RXTX_LOG RXTX_LOG_Resume(); @@ -728,8 +788,10 @@ uint8_t APP_LaunchOverlay(uint8_t slot) __DSB(); __ISB(); - app_run_slot = slot; /* for cfg_load / cfg_save */ - app_cfg_len = 0; + app_run_slot = slot; /* for cfg_load / cfg_save / asset_read */ + app_asset_size = h.asset_size; + app_cfg_len = 0; + app_rng_mix(); #ifdef ENABLE_FMRADIO app_fm_dirty = false; #endif diff --git a/App/apps/app_overlay.h b/App/apps/app_overlay.h index f152e282..a7b6897b 100644 --- a/App/apps/app_overlay.h +++ b/App/apps/app_overlay.h @@ -39,6 +39,13 @@ * ... ] * 0x120000 slot 15 ] * + * Header sector layout (slot-relative): + * + * 0x000 app_header_t (64 B) + * 0x040 per-app config staged by cfg_save (16 B, reserved up to 0x0FF) + * 0x100 read-only assets (API level 2, up to 3840 B, CRC-checked at launch) + * 0x1000 code (<= 4 KiB, copied into the overlay RAM) + * * Host tooling (APP_SlotErase/Write/Info) touches the external flash only and is * never brick-critical; a bad slot is simply refused at launch by the CRC check. */ @@ -63,6 +70,12 @@ * via compile-app.sh; the loader checks each blob's link_vma against it. ---- */ #define APP_OVERLAY_MAX 0x00001000u /* 4 KiB */ +/* ---- read-only assets, served by api->asset_read (API level 2) ---- + * Literal values so pack_app.py can parse them; checked against the slot + * geometry by static asserts in app_overlay.c. */ +#define APP_ASSET_OFFSET 0x00000100u /* assets start in the header sector */ +#define APP_ASSET_MAX 0x00000F00u /* up to the end of the header sector */ + /* ---- blob header (64 bytes, little-endian; see App/apps/pack_app.py) ---- */ #define APP_MAGIC 0x31504146u /* "FAP1" */ #define APP_HDR_VERSION 1u @@ -96,7 +109,8 @@ typedef struct __attribute__((packed)) { char version[APP_VERSION_LEN];/* app version string */ uint32_t link_vma; /* RAM VMA the code was linked at */ uint32_t required_caps; /* APP_CAP_* required by this app */ - uint8_t reserved[4]; /* pad to 64 bytes */ + uint16_t asset_size; /* bytes at APP_ASSET_OFFSET, 0 = none */ + uint16_t asset_crc; /* low 16 bits of the assets' CRC-32 */ } app_header_t; enum { @@ -105,8 +119,8 @@ enum { APP_ERR_MAGIC, /* no/invalid header */ APP_ERR_ABI, /* ABI family/API level mismatch */ APP_ERR_NOT_COMMITTED, /* image not marked complete */ - APP_ERR_SIZE, /* code_size out of range */ - APP_ERR_CRC, /* code CRC-32 mismatch */ + APP_ERR_SIZE, /* code_size or asset_size out of range */ + APP_ERR_CRC, /* code or asset CRC mismatch */ APP_ERR_VMA, /* overlay buffer not at the link VMA */ APP_ERR_AUTH, /* host write refused: timestamp mismatch */ APP_ERR_CAP, /* required firmware capability missing */ diff --git a/App/apps/beacon/beacon_app.c b/App/apps/beacon/beacon_app.c index 361fdcf0..f10c49c8 100644 --- a/App/apps/beacon/beacon_app.c +++ b/App/apps/beacon/beacon_app.c @@ -30,6 +30,7 @@ #include #include #include "../app_api.h" +#include "beacon_assets.h" /* generated by gen_assets.py */ #define LCD_WIDTH 128 #define TICK_MS 50 @@ -56,13 +57,8 @@ #define CFG_MAGIC 0xF4 -/* Sentinel-prefixed Morse (leading 1 sentinel, then elements MSB-first: 0 dit, 1 dah). */ -static const uint8_t M_LET[26] = {0x05,0x18,0x1A,0x0C,0x02,0x12,0x0E,0x10,0x04,0x17,0x0D,0x14,0x07,0x06,0x0F,0x16,0x1D,0x0A,0x08,0x03,0x09,0x11,0x0B,0x19,0x1B,0x1C}; -static const uint8_t M_DIG[10] = {0x3F,0x2F,0x27,0x23,0x21,0x20,0x30,0x38,0x3C,0x3E}; -static const char FOX_TAIL[5] = {'E','I','S','H','5'}; -static const uint8_t BMP_TX[16] = {0x1c,0x22,0x41,0x1c,0x22,0x00,0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c}; -static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c}; -static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e}; +/* Texts, fox identifiers, the Morse table and icons are read-only assets + * (gen_assets.py); only the displayed item is fetched from external flash. */ static const app_api_t *A; @@ -75,6 +71,10 @@ static char msg[17]; static uint8_t prevKey; static char str[16]; +/* Text from the assets in a shared buffer, valid until the next T() call. */ +static char tbuf[TEXT_MAX]; +static char *T(uint16_t off){ A->asset_read(off,tbuf,TEXT_MAX); return tbuf; } + /* ---- tiny formatting ---- */ static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; } static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; } @@ -105,26 +105,28 @@ static bool settingKey(uint8_t key,int8_t dir){ /* ---- message ---- */ static void buildMsg(void){ char *o=msg; - if(foxFox==FOX_CALL){ - if(foxCall[0]){ o=put(o,foxCall); o=put(o," MOE"); } - else o=put(o,"MOE"); - } else if(foxFox==FOX_MO){ o=put(o,"MO"); } - else { o=put(o,"MO"); *o++=FOX_TAIL[foxFox]; } + uint8_t id=foxFox; + if(foxFox==FOX_CALL){ /* " MOE", or "MOE" alone */ + id=0; + if(foxCall[0]){ o=put(o,foxCall); *o++=' '; } + } + o=put(o,T(FOX_ID+id*FOX_ID_STRIDE)); *o='\0'; } static void foxLabel(char *out){ - char *o=put(out,"FOX "); - if(foxFox==FOX_CALL) o=put(o,"CALL"); - else if(foxFox==FOX_MO) o=put(o,"MO"); - else { o=put(o,"MO"); *o++=FOX_TAIL[foxFox]; } + char *o=put(out,T(T_FOX)); + o=put(o,T(foxFox==FOX_CALL?T_CALL:FOX_ID+foxFox*FOX_ID_STRIDE)); *o='\0'; } static uint8_t morseByte(char c){ + uint8_t i, code; if(c>='a'&&c<='z') c-=32; - if(c>='A'&&c<='Z') return M_LET[c-'A']; - if(c>='0'&&c<='9') return M_DIG[c-'0']; - if(c=='/') return 0x32; - return 0; + if(c>='A'&&c<='Z') i=(uint8_t)(c-'A'); + else if(c>='0'&&c<='9') i=(uint8_t)(26+c-'0'); + else if(c=='/') i=36; + else return 0; + A->asset_read(MORSE+i,&code,1); + return code; } /* ---- drawing ---- */ @@ -133,10 +135,9 @@ static void tag(const char *s,uint8_t x,uint8_t line){ A->print_inverse(s,x,line static void chrome(void){ A->display_clear(); A->status_clear(); - A->print_inverse("BEACON",2,0,true,true,26); + A->print_inverse(T(T_BEACON),2,0,true,true,26); A->draw_battery(); - if(foxLocked) cpy(A->status_line+70,FONT_LOCK,sizeof(FONT_LOCK)); - else if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F)); + if(foxLocked||fArm) A->asset_read(foxLocked?BMP_LOCK:BMP_F,A->status_line+70,BMP_F_LEN); { char *o=putu(str,A->tx_freq()/100000u); *o++='.'; uint32_t fr=A->tx_freq()%100000u; for(int8_t d=4;d>=0;d--){ uint32_t p=1; for(int8_t k=0;kprint_normal(str,(uint8_t)(126-slen(str)*7),0,6); @@ -144,7 +145,7 @@ static void chrome(void){ static void drawTxSeconds(void){ uint8_t sec=(uint8_t)((txMsLeft+999u)/1000u); for(uint8_t x=0;xfb[0][x]=0; - A->print_normal("TX",1,0,0); + A->print_normal(T(T_TX),1,0,0); char *o=str; put2(&o,sec); *o++='s'; *o='\0'; A->print_normal(str,(uint8_t)(127-slen(str)*7),0,0); secShown=sec; @@ -178,18 +179,18 @@ static void updateProgress(uint8_t vis){ static void beaconDraw(bool txNow,uint8_t il){ chrome(); if(txNow){ - cpy(A->status_line+48,BMP_TX,16); + A->asset_read(BMP_TX,A->status_line+48,BMP_TX_LEN); drawTxSeconds(); drawMessage(charsSent); } else { - A->print_string("IDLE",0,127,1,10); + A->print_string(T(T_IDLE),0,127,1,10); char *o=str; put2(&o,il); *o++='s'; *o='\0'; A->print_string(str,0,127,3,10); } - char *o=put(str,"TX "); o=putu(o,beaconTx); o=put(o,"s"); *o='\0'; tag(str,4,5); - o=put(str,"IDLE "); o=putu(o,beaconIdle); o=put(o,"s"); *o='\0'; tag(str,4,6); + char *o=put(str,T(T_TX)); *o++=' '; o=putu(o,beaconTx); *o++='s'; *o='\0'; tag(str,4,5); + o=put(str,T(T_IDLE)); *o++=' '; o=putu(o,beaconIdle); *o++='s'; *o='\0'; tag(str,4,6); foxLabel(str); tag(str,66,5); - { const char *m=carrier?"CARR":"TONE"; tag(m,(uint8_t)(125-slen(m)*4),5); } + { const char *m=T(carrier?T_CARR:T_TONE); tag(m,(uint8_t)(125-slen(m)*4),5); } } static void blit(void){ A->blit_status(); A->blit_full(); } @@ -295,7 +296,7 @@ static void saveConfig(void){ uint8_t c[5]={CFG_MAGIC,beaconIdle,foxFox,beaconTx static void txDenied(void){ chrome(); - A->print_string("TX OFF",0,127,1,8); + A->print_string(T(T_TX_OFF),0,127,1,8); blit(); for(uint8_t i=0;i<20;i++) tickDelay(); } diff --git a/App/apps/beacon/build.sh b/App/apps/beacon/build.sh index bcbf1b2d..36605da3 100755 --- a/App/apps/beacon/build.sh +++ b/App/apps/beacon/build.sh @@ -8,8 +8,8 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="Beacon" # <-- the only per-app line -APP_VER="1.0" -APP_API_MIN=1 +APP_VER="1.1" +APP_API_MIN=2 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) @@ -24,14 +24,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous -step() { printf '\r ๐Ÿ”จ %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; } +step() { printf '\r ๐Ÿ”จ %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } trap 'printf "\r โŒ %-13s build failed \n" "$APP_NAME"' ERR -step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" -step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \ - --shortcut beacon >/dev/null + --shortcut beacon --assets "${APP}_assets.bin" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/beacon/gen_assets.py b/App/apps/beacon/gen_assets.py new file mode 100755 index 00000000..7158932e --- /dev/null +++ b/App/apps/beacon/gen_assets.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# Beacon read-only assets: texts, fox identifiers, Morse table and icons. +# +# ./gen_assets.py beacon_assets.bin beacon_assets.h +import os, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets + +a = Assets("BEACON") +a.text("T_BEACON", "BEACON") +a.text("T_TX", "TX") +a.text("T_IDLE", "IDLE") +a.text("T_FOX", "FOX ") +a.text("T_CALL", "CALL") +a.text("T_CARR", "CARR") +a.text("T_TONE", "TONE") +a.text("T_TX_OFF", "TX OFF") +# Fox identifiers by foxFox (0..FOX_MO); FOX_CALL sends " " + entry 0. +a.table("FOX_ID", ["MOE", "MOI", "MOS", "MOH", "MO5", "MO"]) +# Sentinel-prefixed Morse (leading 1, then elements MSB-first: 0 dit, 1 dah): +# A..Z, then 0..9, then '/'. +a.u8("MORSE", [0x05,0x18,0x1A,0x0C,0x02,0x12,0x0E,0x10,0x04,0x17,0x0D,0x14,0x07, + 0x06,0x0F,0x16,0x1D,0x0A,0x08,0x03,0x09,0x11,0x0B,0x19,0x1B,0x1C, + 0x3F,0x2F,0x27,0x23,0x21,0x20,0x30,0x38,0x3C,0x3E, + 0x32]) +a.u8("BMP_TX", [0x1c,0x22,0x41,0x1c,0x22,0x00,0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c]) +a.u8("BMP_LOCK", [0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c]) +a.u8("BMP_F", [0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e]) +a.main() diff --git a/App/apps/beam/beam_app.c b/App/apps/beam/beam_app.c index 33940743..90d28392 100644 --- a/App/apps/beam/beam_app.c +++ b/App/apps/beam/beam_app.c @@ -10,6 +10,7 @@ #include #include #include "../app_api.h" +#include "beam_assets.h" /* generated by gen_assets.py */ #define PACKET_MAGIC 0xBEA5u #define PACKET_VERSION 2u @@ -37,10 +38,6 @@ typedef struct { _Static_assert(sizeof(beam_payload_t) == 48u, "BEAM v2 wire payload layout changed"); -static const uint16_t OBFUSCATION[8] = { - 0x6C16, 0xE614, 0x912E, 0x400D, 0x3521, 0x40D5, 0x0313, 0x80E9 -}; - static const app_api_t *A; static uint32_t packet_store[18]; /* 72 bytes, 4-byte aligned for the payload */ static uint16_t copiedChannel; @@ -71,23 +68,19 @@ static uint16_t crc16(const void *buffer, uint16_t size) static void obfuscate(void) { + uint16_t key[8]; /* obfuscation key, from the assets */ uint16_t *p = packet(); + A->asset_read(OBFUSCATION, key, sizeof(key)); for (uint8_t i = 0; i < 32u; i++) - p[i + 1u] ^= OBFUSCATION[i & 7u]; + p[i + 1u] ^= key[i & 7u]; } static void draw(void) { - const char *state; - switch (status) { - case STATUS_TX_WAIT: state = "SENDING"; break; - case STATUS_TX_DONE: state = "SENT"; break; - case STATUS_RX_WAIT: state = "WAITING"; break; - case STATUS_RX_SAVED: state = "RECEIVED"; break; - case STATUS_RX_FULL: state = "MEM FULL"; break; - case STATUS_ERROR: state = "ERROR"; break; - default: state = mode ? "BEAM RX" : "BEAM TX"; break; - } + /* T_STATE: READY in TX / RX mode, then one entry per later status. */ + char state[T_STATE_STRIDE]; + const uint8_t index = status ? (uint8_t)(status + 1u) : mode; + A->asset_read(T_STATE + index * T_STATE_STRIDE, state, sizeof(state)); A->beam_draw(state); A->blit_status(); A->blit_full(); diff --git a/App/apps/beam/build.sh b/App/apps/beam/build.sh index f8850ed5..48433e96 100755 --- a/App/apps/beam/build.sh +++ b/App/apps/beam/build.sh @@ -3,8 +3,8 @@ set -euo pipefail APP="$(basename "$PWD")" APP_NAME="Beam" -APP_VER="1.0" -APP_API_MIN=1 +APP_VER="1.1" +APP_API_MIN=2 APP_VMA=${APP_VMA:-0x20000280} OUT="${APP_NAME// /}" @@ -19,14 +19,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ rm -f ./*.app ./*.elf ./*.bin -step() { printf '\r ๐Ÿ”จ %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; } +step() { printf '\r ๐Ÿ”จ %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } trap 'printf "\r โŒ %-13s build failed \n" "$APP_NAME"' ERR -step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" -step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \ - --shortcut beam --require beam >/dev/null + --shortcut beam --require beam --assets "${APP}_assets.bin" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/beam/gen_assets.py b/App/apps/beam/gen_assets.py new file mode 100755 index 00000000..b302b4bf --- /dev/null +++ b/App/apps/beam/gen_assets.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# Beam read-only assets: status texts and the packet obfuscation key. +# +# ./gen_assets.py beam_assets.bin beam_assets.h +import os, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets + +a = Assets("BEAM") +# Indexed by beam_app.c: 0/1 = READY in TX/RX mode, then status + 1. +a.table("T_STATE", ["BEAM TX", "BEAM RX", "SENDING", "SENT", "WAITING", + "RECEIVED", "MEM FULL", "ERROR"]) +a.u16("OBFUSCATION", [0x6C16, 0xE614, 0x912E, 0x400D, 0x3521, 0x40D5, 0x0313, 0x80E9]) +a.main() diff --git a/App/apps/breakout/breakout_app.c b/App/apps/breakout/breakout_app.c index e588acc8..042ec21d 100644 --- a/App/apps/breakout/breakout_app.c +++ b/App/apps/breakout/breakout_app.c @@ -27,6 +27,7 @@ #include #include #include "../app_api.h" +#include "breakout_assets.h" /* generated by gen_assets.py */ #define LCD_WIDTH 128 #define BRICK_NUMBER 18 @@ -70,12 +71,13 @@ static void u2str(char *out, const char *label, uint16_t v, uint8_t w) { static const app_api_t *A; /* ---- game state (lives in the overlay; re-initialised at entry) ---- */ -static uint32_t randSeed; static uint8_t blockAnim; static bool isInitialized; static bool isPaused; static bool saverPaused, saverActive; static bool gameOver, redrawRequired; +static bool title; +static uint8_t titleTick; static uint8_t levelCountBreakout; static uint8_t gameOverLevel; static uint16_t tone; @@ -89,12 +91,11 @@ static Brick brick[BRICK_NUMBER]; static Racket racket; static Ball ball; -static const uint8_t BRICK_ANIM_PATTERNS[4] = - {0b00110001, 0b00101001, 0b00100101, 0b00100011}; +/* Text from the assets in a shared buffer, valid until the next T() call. */ +static char tbuf[TEXT_MAX]; +static char *T(uint16_t off) { A->asset_read(off, tbuf, TEXT_MAX); return tbuf; } -static void srand_custom(uint32_t seed) { randSeed = seed ? seed : 1; } -static int rand_custom(void) { randSeed = randSeed * 1103515245u + 12345u; return (randSeed >> 16) & 0x7FFF; } -static int randInt(int min, int max) { return min + (rand_custom() % (max - min + 1)); } +static int randInt(int min, int max) { return min + (int)(A->rand32() % (uint32_t)(max - min + 1)); } static void reset(void) { ballCount = BALL_NUMBER; levelCountBreakout = 1; score = 0; } @@ -102,9 +103,9 @@ static void playBeep(uint16_t t) { A->play_tone(t, 100); } static void drawScoreValues(uint8_t level, int16_t balls, uint16_t value) { A->status_clear(); - u2str(str, "Level ", level, 2); A->print_tiny(str, 0, 1, true, true); - u2str(str, "Ball ", (balls < 0) ? 0 : (uint16_t)balls, 2); A->print_tiny(str, 45, 1, true, true); - u2str(str, "Score ", value, 4); A->print_tiny(str, 88, 1, true, true); + u2str(str, T(T_LEVEL), level, 2); A->print_tiny(str, 0, 1, true, true); + u2str(str, T(T_BALL), (balls < 0) ? 0 : (uint16_t)balls, 2); A->print_tiny(str, 45, 1, true, true); + u2str(str, T(T_SCORE), value, 4); A->print_tiny(str, 88, 1, true, true); } static void drawScore(void) { drawScoreValues(levelCountBreakout, ballCount, score); } @@ -154,7 +155,7 @@ static void drawBall(void) { gameOver = true; reset(); initWall(); /* drawWall drawn by main loop */ isPaused = true; - A->print_bold("GAME OVER", 0, LCD_WIDTH - 1, 4); + A->print_bold(T(T_GAME_OVER), 0, LCD_WIDTH - 1, 4); } initRacket(); initBall(); } @@ -162,6 +163,8 @@ static void drawBall(void) { } static void drawWall(bool checkCollision) { + uint8_t anim[BRICK_ANIM_LEN]; /* brick patterns, read once per frame */ + A->asset_read(BRICK_ANIM, anim, sizeof(anim)); for (uint8_t i = 0; i < BRICK_NUMBER; i++) { if (brick[i].destroy) continue; uint8_t *fb_ptr = A->fb[brick[i].y / 8] + brick[i].x; @@ -187,7 +190,7 @@ static void drawWall(bool checkCollision) { } } else { for (uint8_t k = 0; k < 13; k++) - fb_ptr[k + 1] = BRICK_ANIM_PATTERNS[(blockAnim + k) % 4]; + fb_ptr[k + 1] = anim[(blockAnim + k) % 4]; } } } @@ -219,10 +222,25 @@ static void redrawScene(void) { racket.p = racket.x; renderBall(true); if (isPaused) - A->print_bold(gameOver ? "GAME OVER" : "PAUSE", 0, LCD_WIDTH - 1, 4); + A->print_bold(T(gameOver ? T_GAME_OVER : T_PAUSE), 0, LCD_WIDTH - 1, 4); +} + +/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers, + * with a blinking prompt on the blank bottom page. */ +static void drawTitle(void) { + titleTick++; + A->asset_read(ART_TITLE, A->status_line, LCD_WIDTH); + A->asset_read(ART_TITLE + LCD_WIDTH, A->fb[0], 7u * LCD_WIDTH); + if (titleTick & 32u) + A->print_bold(T(T_PRESS), 0, LCD_WIDTH - 1, 6); } static void OnKeyDown(uint8_t key) { + if (title) { /* MENU starts the game (and pauses it later on) */ + if (key == APP_KEY_MENU) { title = false; redrawRequired = true; } + else if (key == APP_KEY_EXIT) isInitialized = false; + return; + } bool wasPaused = isPaused; switch (key) { case APP_KEY_4: @@ -231,7 +249,7 @@ static void OnKeyDown(uint8_t key) { case APP_KEY_DOWN: if (!isPaused && racket.x < 102) racket.x += 2; isPaused = false; break; case APP_KEY_MENU: isPaused = !isPaused; - if (isPaused) A->print_bold("PAUSE", 0, LCD_WIDTH - 1, 4); + if (isPaused) A->print_bold(T(T_PAUSE), 0, LCD_WIDTH - 1, 4); break; case APP_KEY_EXIT: isPaused = false; isInitialized = false; break; } @@ -282,7 +300,7 @@ void app_main(const app_api_t *api) { #ifdef APP_POC_HELLO /* Minimal bisection test: prove jump + ABI + return work, no game body. */ A->display_clear(); - A->print_bold("OVL HELLO", 0, LCD_WIDTH - 1, 3); + A->print_bold(T(T_HELLO), 0, LCD_WIDTH - 1, 3); A->blit_full(); A->delay_ms(2000); return; @@ -295,10 +313,6 @@ void app_main(const app_api_t *api) { kbdPrev = APP_KEY_INVALID; kbdCur = APP_KEY_INVALID; uint8_t swap = 0; - /* The low BK4819 counter bits vary continuously; mix them with the tuned - * frequency instead of carrying a launch-only seed field in every ABI - * table. */ - srand_custom(((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq()); A->led(false); A->backlight_on(); @@ -306,6 +320,7 @@ void app_main(const app_api_t *api) { reset(); initWall(); initRacket(); initBall(); A->status_clear(); isInitialized = true; + title = true; while (isInitialized) { handleInput(); @@ -316,6 +331,15 @@ void app_main(const app_api_t *api) { continue; } + if (title) { /* the game scene is redrawn when MENU leaves the title */ + drawTitle(); + A->blit_status(); + A->blit_full(); + A->backlight_update(); + A->delay_ms(TICK_MS); + continue; + } + if (redrawRequired) { redrawScene(); redrawRequired = false; diff --git a/App/apps/breakout/build.sh b/App/apps/breakout/build.sh index aa9b5d5f..fb32530f 100755 --- a/App/apps/breakout/build.sh +++ b/App/apps/breakout/build.sh @@ -8,8 +8,8 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="Breakout" # <-- the only per-app line -APP_VER="1.0" -APP_API_MIN=1 +APP_VER="1.1" +APP_API_MIN=2 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) @@ -24,13 +24,14 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous -step() { printf '\r ๐Ÿ”จ %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; } +step() { printf '\r ๐Ÿ”จ %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } trap 'printf "\r โŒ %-13s build failed \n" "$APP_NAME"' ERR -step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" -step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ - --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/breakout/gen_assets.py b/App/apps/breakout/gen_assets.py new file mode 100755 index 00000000..7e9e1596 --- /dev/null +++ b/App/apps/breakout/gen_assets.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# Breakout read-only assets: texts, the brick animation patterns and the +# 128x64 title screen. +# +# ./gen_assets.py breakout_assets.bin breakout_assets.h [preview.pgm] +import os, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets +from app_art import Canvas + +BRICK_ANIM = [0b00110001, 0b00101001, 0b00100101, 0b00100011] + +def brick(cv, x, y): + """A 15 px brick as drawn by the game: end caps and the animated fill.""" + for k in range(15): + col = 0b00011110 if k in (0, 14) else BRICK_ANIM[(k - 1) % 4] + for bit in range(8): + if col >> bit & 1: + cv.set(x + k, y + bit) + +def title_screen(): + """BREAK / OUT, a few bricks, the racket and the ball with its trail.""" + cv = Canvas() + cv.two_words("BREAK", "OUT") + for x in (2, 22, 86, 106): + brick(cv, x, 42) + for k in range(24): # racket, as renderRacket() + cv.set(52 + k, 54) + if 0 < k < 23: + cv.set(52 + k, 53); cv.set(52 + k, 55) + cv.rect(62, 47, 64, 49) # ball, as renderBall() + cv.set(61, 48); cv.set(65, 48) + for i in range(0, 8, 2): # dotted trail + cv.set(58 - i * 2, 47 - i // 2) + return cv + +preview_path = sys.argv.pop(3) if len(sys.argv) == 4 else None + +a = Assets("BREAKOUT") +a.text("T_LEVEL", "Level ") +a.text("T_BALL", "Ball ") +a.text("T_SCORE", "Score ") +a.text("T_GAME_OVER", "GAME OVER") +a.text("T_PAUSE", "PAUSE") +a.text("T_HELLO", "OVL HELLO") # APP_POC_HELLO bisection build only +a.text("T_PRESS", "PRESS MENU") +a.u8("BRICK_ANIM", BRICK_ANIM) +title = title_screen() +a.raw("ART_TITLE", title.pages()) +a.main() +if preview_path: + title.preview(preview_path) diff --git a/App/apps/cube3d/build.sh b/App/apps/cube3d/build.sh index cadc1df6..8918bcfc 100755 --- a/App/apps/cube3d/build.sh +++ b/App/apps/cube3d/build.sh @@ -8,8 +8,8 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="Cube3D" # <-- the only per-app line -APP_VER="1.0" -APP_API_MIN=1 +APP_VER="1.1" +APP_API_MIN=2 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) @@ -24,13 +24,14 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous -step() { printf '\r ๐Ÿ”จ %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; } +step() { printf '\r ๐Ÿ”จ %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } trap 'printf "\r โŒ %-13s build failed \n" "$APP_NAME"' ERR -step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" -step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ - --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/cube3d/cube3d_app.c b/App/apps/cube3d/cube3d_app.c index b588d102..72fe71d3 100644 --- a/App/apps/cube3d/cube3d_app.c +++ b/App/apps/cube3d/cube3d_app.c @@ -23,17 +23,21 @@ * the signed area of its projected polygon shows it facing us). * - WIRE : every edge, with the far hemisphere dotted for a depth cue. * Edges use a self-clipped Bresenham writing the full 64 rows directly, since the - * resident pixel helper does not bound-check. The eight solids' faces (with a - * uniform outward winding) are generated offline by a convex-hull extractor, so - * nothing here has to be hand-wound. Pure compute, no radio. + * resident pixel helper does not bound-check. The solids, the sine table and the + * speeds are read-only assets (gen_assets.py; faces with a uniform outward + * winding from an offline convex-hull extractor). They are copied onto the stack + * (the sine once, the displayed solid every frame), so none of them occupies the + * 4 KiB overlay and a solid can be added without touching this file. + * Pure compute, no radio. * - * Keys: UP/DOWN speed ยท 1-8 shape ยท STAR next shape ยท F solid/wire ยท + * Keys: UP/DOWN speed ยท 1-9 shape ยท STAR next shape ยท F solid/wire ยท * MENU pause ยท EXIT quit. */ #include #include #include "../app_api.h" +#include "cube3d_assets.h" /* generated by gen_assets.py */ #define W 128 #define H 64 @@ -41,20 +45,10 @@ #define CY 32 /* projection centre y */ #define DIST 150 /* camera distance along +z (keeps zc > 0) */ #define FOCAL 80 /* focal length / field-of-view scale */ -#define MAXV 12 /* largest vertex count across the solids */ +#define MAXV SHAPE_MAXV /* largest vertex count across the solids */ static const app_api_t *A; - -/* One Q14 sine quadrant. Symmetry recovers the full 256-step wave while saving - * 382 bytes for the renderer. */ -static const int16_t SIN_Q[65] = { - 0, 402, 804, 1205, 1606, 2006, 2404, 2801, 3196, 3590, 3981, - 4370, 4756, 5139, 5520, 5897, 6270, 6639, 7005, 7366, 7723, 8076, - 8423, 8765, 9102, 9434, 9760,10080,10394,10702,11003,11297,11585, - 11866,12140,12406,12665,12916,13160,13395,13623,13842,14053,14256, - 14449,14635,14811,14978,15137,15286,15426,15557,15679,15791,15893, - 15986,16069,16143,16207,16261,16305,16340,16364,16379,16384 -}; +static const int16_t *sinq; /* Q14 sine quadrant, on app_main's stack */ static int sin8(uint8_t angle) { @@ -62,70 +56,13 @@ static int sin8(uint8_t angle) uint8_t i = angle & 63u; if (quadrant & 1u) i = (uint8_t)(64u - i); - const int value = SIN_Q[i]; + const int value = sinq[i]; return quadrant >= 2u ? -value : value; } -/* A face is a polygon of up to 6 vertex indices, wound CCW as seen from outside - * (generated offline by the hull extractor, so the signed-area cull sign is the - * same for every solid). Unused slots are 0-padded and ignored (n gives length). */ -typedef struct { uint8_t n; uint8_t v[6]; } face_t; - -static const int8_t CUBE_V[8][3]={{-26,-26,-26},{26,-26,-26},{26,26,-26},{-26,26,-26},{-26,-26,26},{26,-26,26},{26,26,26},{-26,26,26}}; -static const face_t CUBE_F[6]={{4,{2,1,0,3,0,0}},{4,{4,0,1,5,0,0}},{4,{7,3,0,4,0,0}},{4,{5,1,2,6,0,0}},{4,{6,2,3,7,0,0}},{4,{7,4,5,6,0,0}}}; - -static const int8_t OCTAHEDRON_V[6][3]={{38,0,0},{-38,0,0},{0,38,0},{0,-38,0},{0,0,38},{0,0,-38}}; -static const face_t OCTAHEDRON_F[8]={{3,{4,0,2,0,0,0}},{3,{2,0,5,0,0,0}},{3,{3,0,4,0,0,0}},{3,{5,0,3,0,0,0}},{3,{2,1,4,0,0,0}},{3,{5,1,2,0,0,0}},{3,{4,1,3,0,0,0}},{3,{3,1,5,0,0,0}}}; - -static const int8_t TETRAHEDRON_V[4][3]={{28,28,28},{28,-28,-28},{-28,28,-28},{-28,-28,28}}; -static const face_t TETRAHEDRON_F[4]={{3,{2,0,1,0,0,0}},{3,{1,0,3,0,0,0}},{3,{3,0,2,0,0,0}},{3,{2,1,3,0,0,0}}}; - -static const int8_t DIAMOND_V[8][3]={{30,0,0},{14,26,0},{-14,26,0},{-30,0,0},{-14,-26,0},{14,-26,0},{0,0,40},{0,0,-40}}; -static const face_t DIAMOND_F[12]={{3,{6,0,1,0,0,0}},{3,{1,0,7,0,0,0}},{3,{5,0,6,0,0,0}},{3,{7,0,5,0,0,0}},{3,{6,1,2,0,0,0}},{3,{2,1,7,0,0,0}},{3,{6,2,3,0,0,0}},{3,{3,2,7,0,0,0}},{3,{6,3,4,0,0,0}},{3,{4,3,7,0,0,0}},{3,{6,4,5,0,0,0}},{3,{5,4,7,0,0,0}}}; - -static const int8_t ICOSAHEDRON_V[12][3]={{0,18,29},{0,18,-29},{0,-18,29},{0,-18,-29},{18,29,0},{18,-29,0},{-18,29,0},{-18,-29,0},{29,0,18},{29,0,-18},{-29,0,18},{-29,0,-18}}; -static const face_t ICOSAHEDRON_F[20]={{3,{8,0,2,0,0,0}},{3,{2,0,10,0,0,0}},{3,{6,0,4,0,0,0}},{3,{4,0,8,0,0,0}},{3,{10,0,6,0,0,0}},{3,{3,1,9,0,0,0}},{3,{11,1,3,0,0,0}},{3,{4,1,6,0,0,0}},{3,{9,1,4,0,0,0}},{3,{6,1,11,0,0,0}},{3,{5,2,7,0,0,0}},{3,{8,2,5,0,0,0}},{3,{7,2,10,0,0,0}},{3,{7,3,5,0,0,0}},{3,{5,3,9,0,0,0}},{3,{11,3,7,0,0,0}},{3,{9,4,8,0,0,0}},{3,{8,5,9,0,0,0}},{3,{10,6,11,0,0,0}},{3,{11,7,10,0,0,0}}}; - -static const int8_t CUBOCTA_V[12][3]={{-24,-24,0},{-24,24,0},{24,-24,0},{24,24,0},{-24,0,-24},{-24,0,24},{24,0,-24},{24,0,24},{0,-24,-24},{0,-24,24},{0,24,-24},{0,24,24}}; -static const face_t CUBOCTA_F[14]={{4,{4,0,5,1,0,0}},{4,{2,9,0,8,0,0}},{3,{8,0,4,0,0,0}},{3,{5,0,9,0,0,0}},{4,{10,1,11,3,0,0}},{3,{4,1,10,0,0,0}},{3,{11,1,5,0,0,0}},{4,{3,7,2,6,0,0}},{3,{6,2,8,0,0,0}},{3,{9,2,7,0,0,0}},{3,{10,3,6,0,0,0}},{3,{7,3,11,0,0,0}},{4,{8,4,10,6,0,0}},{4,{7,11,5,9,0,0}}}; - -static const int8_t HEXPRISM_V[12][3]={{26,0,24},{12,22,24},{-12,22,24},{-26,0,24},{-12,-22,24},{12,-22,24},{26,0,-24},{12,22,-24},{-12,22,-24},{-26,0,-24},{-12,-22,-24},{12,-22,-24}}; -static const face_t HEXPRISM_F[8]={{6,{4,5,0,1,2,3}},{4,{7,1,0,6,0,0}},{4,{6,0,5,11,0,0}},{4,{8,2,1,7,0,0}},{4,{9,3,2,8,0,0}},{4,{10,4,3,9,0,0}},{4,{11,5,4,10,0,0}},{6,{9,8,7,6,11,10}}}; - -static const int8_t PENTAGEM_V[7][3]={{28,0,0},{8,26,0},{-22,16,0},{-22,-16,0},{8,-26,0},{0,0,42},{0,0,-42}}; -static const face_t PENTAGEM_F[10]={{3,{5,0,1,0,0,0}},{3,{1,0,6,0,0,0}},{3,{4,0,5,0,0,0}},{3,{6,0,4,0,0,0}},{3,{5,1,2,0,0,0}},{3,{2,1,6,0,0,0}},{3,{5,2,3,0,0,0}},{3,{3,2,6,0,0,0}},{3,{5,3,4,0,0,0}},{3,{4,3,6,0,0,0}}}; - -typedef struct { - const int8_t (*v)[3]; - const face_t *f; - uint8_t nv; - uint8_t nf; - const char *name; - uint8_t namelen; -} shape_t; - -#define NSHAPE 8 -static const shape_t SHAPES[NSHAPE] = { - { CUBE_V, CUBE_F, 8, 6, "CUBE", 4 }, - { OCTAHEDRON_V, OCTAHEDRON_F, 6, 8, "OCTAHEDRON", 10 }, - { TETRAHEDRON_V, TETRAHEDRON_F, 4, 4, "TETRAHEDRON", 11 }, - { DIAMOND_V, DIAMOND_F, 8, 12, "DIAMOND", 7 }, - { ICOSAHEDRON_V, ICOSAHEDRON_F, 12, 20, "ICOSAHEDRON", 11 }, - { CUBOCTA_V, CUBOCTA_F, 12, 14, "CUBOCTA", 7 }, - { HEXPRISM_V, HEXPRISM_F, 12, 8, "HEXPRISM", 8 }, - { PENTAGEM_V, PENTAGEM_F, 7, 10, "PENTAGEM", 8 }, -}; - /* Per-frame projected screen coords + rotated depth of each vertex. */ static int16_t px[MAXV], py[MAXV], pz[MAXV]; -/* Quarter-half-units per frame. The low end has fractional angular steps; - * level 16 reaches the old 16 half-units/frame once divided by four. */ -static const uint8_t ROT_RATE[16] = { - 1, 2, 3, 4, 6, 8, 10, 12, - 16, 20, 24, 30, 36, 44, 52, 64 -}; - /* Set one pixel across the full 64 rows: 0..7 -> status line, 8..63 -> fb. */ static void set_pixel(int x, int y) { @@ -168,13 +105,15 @@ static void clear_screen(void) } } -/* Signed area of a face's projected polygon (<0 == facing us, calibrated). */ -static int face_area(const face_t *f) +/* Signed area of a packed face's projected polygon (<0 == facing us). */ +static int face_area(const uint8_t *f) { + const uint8_t n = f[0]; + const uint8_t *v = f + 1; int sa = 0; - for (uint8_t k = 0; k < f->n; k++) { - const uint8_t a = f->v[k]; - const uint8_t b = f->v[(k + 1u == f->n) ? 0u : k + 1u]; + for (uint8_t k = 0; k < n; k++) { + const uint8_t a = v[k]; + const uint8_t b = v[(k + 1u == n) ? 0u : k + 1u]; sa += (int)px[a] * py[b] - (int)px[b] * py[a]; } return sa; @@ -187,6 +126,16 @@ void app_main(const app_api_t *api) A->backlight_on(); A->status_clear(); + /* Assets copied to the stack: the sine once, the displayed solid every + * frame (nv, nf, name + NUL, vertices, packed faces; see gen_assets.py). */ + int16_t sin_q[SIN_Q_LEN / 2u]; + uint8_t rec[SHAPE_REC_MAX]; + uint8_t nshape; + A->asset_read(SIN_Q, sin_q, sizeof(sin_q)); + sinq = sin_q; + if (A->asset_read(SHAPES, &nshape, 1) != 1u || nshape == 0u) + return; + uint16_t ax = 0, ay = 0, az = 0; /* Q2 half-units: 2048 = full turn */ uint8_t shape = 0; uint8_t speed = 4; /* 1..16, shared by all three rotation axes */ @@ -222,14 +171,15 @@ void app_main(const app_api_t *api) paused = !paused; break; case APP_KEY_STAR: - shape = (uint8_t)((shape + 1u) % NSHAPE); + shape = (uint8_t)((shape + 1u) % nshape); break; case APP_KEY_F: wire = !wire; break; case APP_KEY_1: case APP_KEY_2: case APP_KEY_3: case APP_KEY_4: case APP_KEY_5: case APP_KEY_6: case APP_KEY_7: case APP_KEY_8: - if ((uint8_t)(key - APP_KEY_1) < NSHAPE) + case APP_KEY_9: + if ((uint8_t)(key - APP_KEY_1) < nshape) shape = (uint8_t)(key - APP_KEY_1); break; default: @@ -240,14 +190,23 @@ void app_main(const app_api_t *api) if (!running) break; - const shape_t *s = &SHAPES[shape]; + uint8_t entry[3]; /* directory: u16 record offset, u8 record length */ + A->asset_read(SHAPES + 1u + 3u * shape, entry, sizeof(entry)); + const uint8_t reclen = entry[2] < sizeof(rec) ? entry[2] : (uint8_t)sizeof(rec); + A->asset_read(SHAPES + (uint16_t)(entry[0] | (entry[1] << 8)), rec, reclen); + const uint8_t nv = rec[0] < MAXV ? rec[0] : MAXV; + const uint8_t nf = rec[1]; + uint8_t namelen = 0; + while (2u + namelen < reclen && rec[2u + namelen]) + namelen++; + const int8_t (*verts)[3] = (const int8_t (*)[3])&rec[3u + namelen]; const uint8_t ia = (uint8_t)(ax >> 3), ib = (uint8_t)(ay >> 3), ic = (uint8_t)(az >> 3); const int cx = sin8((uint8_t)(ia + 64u)), sxr = sin8(ia); const int cy = sin8((uint8_t)(ib + 64u)), syr = sin8(ib); const int cz = sin8((uint8_t)(ic + 64u)), szr = sin8(ic); - for (uint8_t i = 0; i < s->nv; i++) { - int x = s->v[i][0], y = s->v[i][1], z = s->v[i][2]; + for (uint8_t i = 0; i < nv; i++) { + int x = verts[i][0], y = verts[i][1], z = verts[i][2]; int ny = (y * cx - z * sxr) >> 14; /* Rx */ int nz = (y * sxr + z * cx) >> 14; y = ny; z = nz; @@ -265,29 +224,32 @@ void app_main(const app_api_t *api) } clear_screen(); - for (uint8_t i = 0; i < s->nf; i++) { - const face_t *f = &s->f[i]; - if (!wire && face_area(f) >= 0) - continue; /* hidden face culled */ - for (uint8_t k = 0; k < f->n; k++) { - const uint8_t a = f->v[k]; - const uint8_t b = f->v[(k + 1u == f->n) ? 0u : k + 1u]; - const bool dotted = wire && (pz[a] + pz[b] > 0); /* far half */ - draw_edge(px[a], py[a], px[b], py[b], dotted); + const uint8_t *f = &rec[3u + namelen + 3u * rec[0]]; /* packed faces */ + for (uint8_t i = 0; i < nf; i++) { + const uint8_t n = f[0]; + if (wire || face_area(f) < 0) { /* else hidden face culled */ + for (uint8_t k = 0; k < n; k++) { + const uint8_t a = f[1u + k]; + const uint8_t b = f[(k + 1u == n) ? 1u : 2u + k]; + const bool dotted = wire && (pz[a] + pz[b] > 0); /* far half */ + draw_edge(px[a], py[a], px[b], py[b], dotted); + } } + f += 1u + n; } /* Shape name: inverse label, top-left of the status bar (scan-list look). */ - const uint8_t end = (uint8_t)(2u + 4u * s->namelen); + const uint8_t end = (uint8_t)(2u + 4u * namelen); for (uint8_t i = 0; i <= end; i++) A->status_line[i] = 0; - A->print_inverse(s->name, 2, 0, true, true, end); + A->print_inverse((const char *)&rec[2], 2, 0, true, true, end); A->blit_status(); A->blit_full(); if (!paused) { - const uint16_t rate = ROT_RATE[speed - 1u]; + uint8_t rate; + A->asset_read(ROT_RATE + speed - 1u, &rate, 1); ax += rate; ay += (uint16_t)(rate + rate / 2u); az += (uint16_t)((rate + 1u) / 2u); diff --git a/App/apps/cube3d/gen_assets.py b/App/apps/cube3d/gen_assets.py new file mode 100755 index 00000000..4ec0acb9 --- /dev/null +++ b/App/apps/cube3d/gen_assets.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# Cube3D read-only assets: the solids, the Q14 sine quadrant and the speeds. +# +# SHAPES layout (offsets relative to SHAPES): +# 0 count N +# 1 + 3*k solid k directory entry: u16 record offset, u8 record length +# record nv, nf, name + NUL, nv x (x, y, z) int8, then nf packed faces +# (n, v0 .. v{n-1}), each wound CCW as seen from outside +# The faces were generated offline by a convex-hull extractor, so every solid +# shares the same outward winding and the signed-area cull sign. SHAPE_MAXV and +# SHAPE_REC_MAX size the app's buffers, so adding a solid needs no C change. +# +# ./gen_assets.py cube3d_assets.bin cube3d_assets.h +import os, struct, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets + +MAXN = 6 # largest face polygon + +SHAPES = [ + ("CUBE", + [(-26,-26,-26),(26,-26,-26),(26,26,-26),(-26,26,-26),(-26,-26,26),(26,-26,26),(26,26,26),(-26,26,26)], + [(2,1,0,3),(4,0,1,5),(7,3,0,4),(5,1,2,6),(6,2,3,7),(7,4,5,6)]), + ("OCTAHEDRON", + [(38,0,0),(-38,0,0),(0,38,0),(0,-38,0),(0,0,38),(0,0,-38)], + [(4,0,2),(2,0,5),(3,0,4),(5,0,3),(2,1,4),(5,1,2),(4,1,3),(3,1,5)]), + ("TETRAHEDRON", + [(28,28,28),(28,-28,-28),(-28,28,-28),(-28,-28,28)], + [(2,0,1),(1,0,3),(3,0,2),(2,1,3)]), + ("DIAMOND", + [(30,0,0),(14,26,0),(-14,26,0),(-30,0,0),(-14,-26,0),(14,-26,0),(0,0,40),(0,0,-40)], + [(6,0,1),(1,0,7),(5,0,6),(7,0,5),(6,1,2),(2,1,7),(6,2,3),(3,2,7),(6,3,4),(4,3,7),(6,4,5),(5,4,7)]), + ("ICOSAHEDRON", + [(0,18,29),(0,18,-29),(0,-18,29),(0,-18,-29),(18,29,0),(18,-29,0),(-18,29,0),(-18,-29,0),(29,0,18),(29,0,-18),(-29,0,18),(-29,0,-18)], + [(8,0,2),(2,0,10),(6,0,4),(4,0,8),(10,0,6),(3,1,9),(11,1,3),(4,1,6),(9,1,4),(6,1,11), + (5,2,7),(8,2,5),(7,2,10),(7,3,5),(5,3,9),(11,3,7),(9,4,8),(8,5,9),(10,6,11),(11,7,10)]), + ("CUBOCTA", + [(-24,-24,0),(-24,24,0),(24,-24,0),(24,24,0),(-24,0,-24),(-24,0,24),(24,0,-24),(24,0,24),(0,-24,-24),(0,-24,24),(0,24,-24),(0,24,24)], + [(4,0,5,1),(2,9,0,8),(8,0,4),(5,0,9),(10,1,11,3),(4,1,10),(11,1,5),(3,7,2,6),(6,2,8),(9,2,7),(10,3,6),(7,3,11),(8,4,10,6),(7,11,5,9)]), + ("HEXPRISM", + [(26,0,24),(12,22,24),(-12,22,24),(-26,0,24),(-12,-22,24),(12,-22,24),(26,0,-24),(12,22,-24),(-12,22,-24),(-26,0,-24),(-12,-22,-24),(12,-22,-24)], + [(4,5,0,1,2,3),(7,1,0,6),(6,0,5,11),(8,2,1,7),(9,3,2,8),(10,4,3,9),(11,5,4,10),(9,8,7,6,11,10)]), + ("PENTAGEM", + [(28,0,0),(8,26,0),(-22,16,0),(-22,-16,0),(8,-26,0),(0,0,42),(0,0,-42)], + [(5,0,1),(1,0,6),(4,0,5),(6,0,4),(5,1,2),(2,1,6),(5,2,3),(3,2,6),(5,3,4),(4,3,6)]), +] + +# One Q14 sine quadrant: symmetry recovers the full 256-step wave. +SIN_Q = [ + 0, 402, 804, 1205, 1606, 2006, 2404, 2801, 3196, 3590, 3981, + 4370, 4756, 5139, 5520, 5897, 6270, 6639, 7005, 7366, 7723, 8076, + 8423, 8765, 9102, 9434, 9760,10080,10394,10702,11003,11297,11585, + 11866,12140,12406,12665,12916,13160,13395,13623,13842,14053,14256, + 14449,14635,14811,14978,15137,15286,15426,15557,15679,15791,15893, + 15986,16069,16143,16207,16261,16305,16340,16364,16379,16384, +] + +# Quarter-half-units per frame for speed levels 1..16. The low end has +# fractional angular steps; level 16 reaches 16 half-units/frame once divided +# by four. +ROT_RATE = [1, 2, 3, 4, 6, 8, 10, 12, 16, 20, 24, 30, 36, 44, 52, 64] + +def record(name, verts, faces): + out = bytes([len(verts), len(faces)]) + name.encode("ascii") + b"\x00" + for v in verts: + out += struct.pack("<3b", *v) + for f in faces: + if not 3 <= len(f) <= MAXN or any(i >= len(verts) for i in f): + sys.exit(f"{name}: bad face {f}") + out += bytes([len(f), *f]) + if len(out) > 255: + sys.exit(f"{name}: record {len(out)} B exceeds 255 B") + return out + +records = [record(*s) for s in SHAPES] +base = 1 + 3 * len(records) +shapes = bytes([len(records)]) +body = b"" +for r in records: + shapes += struct.pack(" BroadcastFM) @@ -24,14 +24,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous -step() { printf '\r ๐Ÿ”จ %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; } +step() { printf '\r ๐Ÿ”จ %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } trap 'printf "\r โŒ %-13s build failed \n" "$APP_NAME"' ERR -step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" -step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \ - --shortcut fm --require fm --screensaver >/dev/null + --shortcut fm --require fm --screensaver --assets "${APP}_assets.bin" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/fm/fm_app.c b/App/apps/fm/fm_app.c index 5fa382b1..fb91a8cd 100644 --- a/App/apps/fm/fm_app.c +++ b/App/apps/fm/fm_app.c @@ -30,15 +30,12 @@ #include #include #include "../app_api.h" +#include "fm_assets.h" /* generated by gen_assets.py */ #define STEP 1 #define SCAN_SETTLE 100 #define CHMAX APP_FM_CH_MAX -static const char BAND_NAME[4][10] = { "87.5-108M", "76-108M", "76-90M", "64-76M" }; -static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e}; -static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; } - static const app_api_t *A; static uint16_t *ch; /* shared gFM_Channels[48] */ static app_fm_state_t st; /* freq_playing, sel_freq, band, is_mr, sel_ch */ @@ -57,6 +54,11 @@ static char str[16]; static char *putu(char *o, uint32_t v){ char t[6]; int8_t n=0; do{t[n++]=(char)('0'+v%10);v/=10;}while(v&&n<6); while(n--)*o++=t[n]; return o; } static char *put(char *o, const char *s){ while(*s)*o++=*s++; return o; } +/* Texts, band names and the F icon are read-only assets (gen_assets.py). + * T() returns a shared buffer, valid until the next T() call. */ +static char tbuf[TEXT_MAX]; +static char *T(uint16_t off){ A->asset_read(off,tbuf,TEXT_MAX); return tbuf; } + static void bandLimits(void){ lo=A->fm_lo(st.band); hi=A->fm_hi(st.band); } static uint16_t wrap(uint16_t f){ if(fhi)return lo; return f; } static void tune(void){ A->fm_set_freq(st.freq_playing, st.band); } @@ -102,29 +104,29 @@ static void draw(void) A->display_clear(); A->status_clear(); A->draw_battery(); - if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F)); /* F armed indicator */ - A->print_string("FM",2,0,0,8); - A->print_normal(BAND_NAME[st.band],1,0,6); + if(fArm) A->asset_read(BMP_F,A->status_line+70,BMP_F_LEN); /* F armed indicator */ + A->print_string(T(T_FM),2,0,0,8); + A->print_normal(T(BAND_NAME+st.band*BAND_NAME_STRIDE),1,0,6); /* ---- status (line 3) ---- */ - if(askSave) { o=put(b,"SAVE?"); *o='\0'; } - else if(askDelete) { o=put(b,"DEL?"); *o='\0'; } + if(askSave) { o=put(b,T(T_SAVE)); *o='\0'; } + else if(askDelete) { o=put(b,T(T_DEL)); *o='\0'; } else if(scanState) { - if(autoScan){ o=put(b,"A-SCAN("); o=putu(o,chPos); *o++=')'; } /* count like UI_DisplayFM */ - else o=put(b,"M-SCAN"); + if(autoScan){ o=put(b,T(T_ASCAN)); o=putu(o,chPos); *o++=')'; } /* count like UI_DisplayFM */ + else o=put(b,T(T_MSCAN)); *o='\0'; } - else if(st.is_mr) { o=put(b,"MR(CH"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o++=')'; *o='\0'; } + else if(st.is_mr) { o=put(b,T(T_MR_CH)); o=num2(o,(uint8_t)(st.sel_ch+1)); *o++=')'; *o='\0'; } else { - o=put(b,"VFO"); *o='\0'; - for(uint8_t i=0;iprint_string(b,0,127,3,10); /* ---- line 1: input in progress / channel prompt / frequency ---- */ if(inIdx>0){ if(st.is_mr||askSave){ /* channel number: "CH-XX" */ - o=put(b,"CH-"); + o=put(b,T(T_CH)); *o++=(char)('0'+inBox[0]); *o++=(inIdx>1)?(char)('0'+inBox[1]):'-'; *o='\0'; @@ -139,10 +141,10 @@ static void draw(void) A->display_freq(b,36,1,false); } } else if(askSave){ /* pick a save slot */ - o=put(b,"CH-"); o=num2(o,(uint8_t)(savePos+1)); *o='\0'; + o=put(b,T(T_CH)); o=num2(o,(uint8_t)(savePos+1)); *o='\0'; A->print_string(b,0,127,1,10); } else if(askDelete){ - o=put(b,"CH-"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o='\0'; + o=put(b,T(T_CH)); o=num2(o,(uint8_t)(st.sel_ch+1)); *o='\0'; A->print_string(b,0,127,1,10); } else { freqBig(st.freq_playing); diff --git a/App/apps/fm/gen_assets.py b/App/apps/fm/gen_assets.py new file mode 100755 index 00000000..1a319c49 --- /dev/null +++ b/App/apps/fm/gen_assets.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# Broadcast FM read-only assets: texts, band names and the F icon. +# +# ./gen_assets.py fm_assets.bin fm_assets.h +import os, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets + +a = Assets("FM") +a.text("T_FM", "FM") +a.text("T_SAVE", "SAVE?") +a.text("T_DEL", "DEL?") +a.text("T_ASCAN", "A-SCAN(") +a.text("T_MSCAN", "M-SCAN") +a.text("T_MR_CH", "MR(CH") +a.text("T_VFO", "VFO") +a.text("T_VFO_CH", "VFO(CH") +a.text("T_CH", "CH-") +a.table("BAND_NAME", ["87.5-108M", "76-108M", "76-90M", "64-76M"]) # by band 0..3 +a.u8("BMP_F", [0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e]) +a.main() diff --git a/App/apps/foxhunt/build.sh b/App/apps/foxhunt/build.sh index 2d77d1f0..a8bf154c 100755 --- a/App/apps/foxhunt/build.sh +++ b/App/apps/foxhunt/build.sh @@ -8,8 +8,8 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="FoxHunt" # <-- the only per-app line -APP_VER="1.0" -APP_API_MIN=1 +APP_VER="1.1" +APP_API_MIN=2 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) @@ -24,14 +24,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous -step() { printf '\r ๐Ÿ”จ %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; } +step() { printf '\r ๐Ÿ”จ %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } trap 'printf "\r โŒ %-13s build failed \n" "$APP_NAME"' ERR -step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" -step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \ - --shortcut foxhunt >/dev/null + --shortcut foxhunt --assets "${APP}_assets.bin" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/foxhunt/foxhunt_app.c b/App/apps/foxhunt/foxhunt_app.c index fba3aacf..9020230c 100644 --- a/App/apps/foxhunt/foxhunt_app.c +++ b/App/apps/foxhunt/foxhunt_app.c @@ -28,6 +28,7 @@ #include #include #include "../app_api.h" +#include "foxhunt_assets.h" /* generated by gen_assets.py */ #define LCD_WIDTH 128 #define BK_REG_13 0x13 @@ -70,21 +71,8 @@ #define CFG_MAGIC 0xF1 /* config validity marker */ -static const uint16_t ATT_REG13[ATT_COUNT] = { 0x03DF, 0x03DD, 0x03DB, 0x03D9, 0x0379, 0x0139 }; -static const uint8_t ATT_DB[ATT_BYP0] = { 0, 6, 15, 27 }; - -/* Status-bar / trend icons (bytes copied verbatim from App/bitmaps.c). */ -static const uint8_t BMP_BARS[11] = {0x40,0x40,0x00,0x70,0x70,0x00,0x7c,0x7c,0x00,0x7f,0x7f}; -static const uint8_t BMP_GRAPH[15] = {0x08,0x04,0x02,0x04,0x08,0x10,0x20,0x10,0x08,0x04,0x02,0x04,0x08,0x10,0x20}; -static const uint8_t BMP_SIGNAL[10] = {0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c}; -static const uint8_t BMP_SPEAKER[10] = {0x1c,0x1c,0x3e,0x7f,0x00,0x22,0x1c,0x41,0x22,0x1c}; -static const uint8_t BMP_UP[11] = {0x20,0x30,0x38,0x3c,0x3e,0x3f,0x3e,0x3c,0x38,0x30,0x20}; -static const uint8_t BMP_DOWN[11] = {0x01,0x03,0x07,0x0f,0x1f,0x3f,0x1f,0x0f,0x07,0x03,0x01}; -static const uint8_t BMP_FLAT[11] = {0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12}; -static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e}; -static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c}; - -static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; } +/* Texts, the attenuator ladder and the icons are read-only assets + * (gen_assets.py): icons are copied straight into the LCD buffers. */ static const app_api_t *A; @@ -113,12 +101,17 @@ static char *puti(char *o,int v){ } static void i2str(char *out,int v){ *puti(out,v)='\0'; } +/* Text from the assets in a shared buffer, valid until the next T() call. */ +static char tbuf[TEXT_MAX]; +static char *T(uint16_t off){ A->asset_read(off,tbuf,TEXT_MAX); return tbuf; } + /* ---- radio helpers ---- */ static int16_t iabs16(int16_t v){ return v<0?-v:v; } static void applyAtt(void){ - uint16_t reg = A->bk_read(BK_REG_13); - reg = (uint16_t)((reg & ~0x03FFu) | ATT_REG13[attStep]); + uint16_t reg = A->bk_read(BK_REG_13), gain; + A->asset_read(ATT_REG13 + attStep * 2u, &gain, sizeof(gain)); + reg = (uint16_t)((reg & ~0x03FFu) | gain); A->bk_write(BK_REG_13, reg); } @@ -155,9 +148,9 @@ static uint8_t fillCount(int16_t dbm){ return (uint8_t)n; } static void buildS(char *out,int16_t dbm){ - if(dbm>=-93){ int16_t o=dbm-(-93); if(o>40)o=40; char *p=put(out,"S9+"); if(o<10)*p++='0'; i2str(p,o); } - else if(dbm<-141) put(out,"S0")[0]='\0'; - else { char *p=put(out,"S"); i2str(p,(int)((uint16_t)(dbm+147)/6u)); } + if(dbm>=-93){ int16_t o=dbm-(-93); if(o>40)o=40; char *p=put(out,T(T_S9P)); if(o<10)*p++='0'; i2str(p,o); } + else if(dbm<-141) put(out,T(T_S0))[0]='\0'; + else { char *p=put(out,T(T_S)); i2str(p,(int)((uint16_t)(dbm+147)/6u)); } } static int16_t div_trunc_pow2(int32_t v, uint8_t shift) @@ -232,33 +225,32 @@ static void draw(void){ A->display_clear(); A->status_clear(); - A->print_inverse("FOX HUNT",2,0,true,true,34); + A->print_inverse(T(T_TITLE),2,0,true,true,34); A->draw_battery(); /* status-bar icons: graph mode @38, audio mode @55, F/lock @69 */ - if(foxGraphMode==GRAPH_HIST) cpy(A->status_line+38,BMP_GRAPH,15); - else cpy(A->status_line+38,BMP_BARS,11); - if(foxAudioMode==AUDIO_BEEP) cpy(A->status_line+55,BMP_SIGNAL,10); - else if(foxAudioMode==AUDIO_STATION) cpy(A->status_line+55,BMP_SPEAKER,10); - if(foxLocked) cpy(A->status_line+70,FONT_LOCK,9); - else if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F)); + if(foxGraphMode==GRAPH_HIST) A->asset_read(BMP_GRAPH,A->status_line+38,BMP_GRAPH_LEN); + else A->asset_read(BMP_BARS,A->status_line+38,BMP_BARS_LEN); + if(foxAudioMode!=AUDIO_OFF) + A->asset_read(foxAudioMode==AUDIO_BEEP?BMP_SIGNAL:BMP_SPEAKER,A->status_line+55,BMP_SIGNAL_LEN); + if(foxLocked||fArm) A->asset_read(foxLocked?BMP_LOCK:BMP_F,A->status_line+70,BMP_F_LEN); i2str(big,curDbm); A->display_freq(big,2,0,false); - A->print_normal("dBm",(uint8_t)(slen(big)*13+4),0,1); + A->print_normal(T(T_DBM),(uint8_t)(slen(big)*13+4),0,1); /* trend arrow (line 0, right) + signed delta (line 1) */ - cpy(A->fb[0]+115, trendDelta>0?BMP_UP:trendDelta<0?BMP_DOWN:BMP_FLAT, 11); + A->asset_read(trendDelta>0?BMP_UP:trendDelta<0?BMP_DOWN:BMP_FLAT, A->fb[0]+115, BMP_UP_LEN); if(trendDelta!=0){ char *p=str; if(trendDelta>0)*p++='+'; else{*p++='-';} int16_t a=iabs16(trendDelta); if(a<10){*p++='0';} i2str(p,a); - A->print_normal("dBm",127-3*7,0,1); + A->print_normal(T(T_DBM),127-3*7,0,1); A->print_normal(str,(uint8_t)(127-3*7-2-slen(str)*7),1,1); } - i2str(put(str,"PK "),peakDbm); + i2str(put(str,T(T_PK)),peakDbm); tag(str,4,2); - i2str(put(str,"MN "),minDbm); + i2str(put(str,T(T_MN)),minDbm); tag(str,(uint8_t)(((uint32_t)LCD_WIDTH-(uint32_t)slen(str)*4u)/2u),2); buildS(sMeter,curDbm); A->print_inverse(sMeter,(uint8_t)(126-slen(sMeter)*4),2,false,true,125); @@ -266,8 +258,11 @@ static void draw(void){ if(foxGraphMode==GRAPH_HIST) drawHist(); else drawBar(); - if(attStepasset_read(ATT_DB+attStep,&db,1); + char *o=put(str,T(T_ATT)); o=puti(o,db); put(o,T(T_DB))[0]='\0'; + } + else put(str,T(attStep==ATT_BYP0?T_BYP:T_BYP_PLUS))[0]='\0'; tag(str,4,6); { uint32_t f=A->rx_freq(); char *o=puti(str,(int)(f/100000u)); *o++='.'; diff --git a/App/apps/foxhunt/gen_assets.py b/App/apps/foxhunt/gen_assets.py new file mode 100755 index 00000000..2012731c --- /dev/null +++ b/App/apps/foxhunt/gen_assets.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# FoxHunt read-only assets: texts, attenuator ladder and icons (bytes copied +# verbatim from App/bitmaps.c). +# +# ./gen_assets.py foxhunt_assets.bin foxhunt_assets.h +import os, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets + +a = Assets("FOXHUNT") +a.text("T_TITLE", "FOX HUNT") +a.text("T_DBM", "dBm") +a.text("T_S9P", "S9+") +a.text("T_S0", "S0") +a.text("T_S", "S") +a.text("T_PK", "PK ") +a.text("T_MN", "MN ") +a.text("T_ATT", "ATT ") +a.text("T_DB", "dB") +a.text("T_BYP", "BYP") +a.text("T_BYP_PLUS", "BYP+") +a.u16("ATT_REG13", [0x03DF, 0x03DD, 0x03DB, 0x03D9, 0x0379, 0x0139]) # REG_13 gain bits by attStep +a.u8("ATT_DB", [0, 6, 15, 27]) # attenuation below the bypass steps +a.u8("BMP_BARS", [0x40,0x40,0x00,0x70,0x70,0x00,0x7c,0x7c,0x00,0x7f,0x7f]) +a.u8("BMP_GRAPH", [0x08,0x04,0x02,0x04,0x08,0x10,0x20,0x10,0x08,0x04,0x02,0x04,0x08,0x10,0x20]) +a.u8("BMP_SIGNAL", [0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c]) +a.u8("BMP_SPEAKER", [0x1c,0x1c,0x3e,0x7f,0x00,0x22,0x1c,0x41,0x22,0x1c]) +a.u8("BMP_UP", [0x20,0x30,0x38,0x3c,0x3e,0x3f,0x3e,0x3c,0x38,0x30,0x20]) +a.u8("BMP_DOWN", [0x01,0x03,0x07,0x0f,0x1f,0x3f,0x1f,0x0f,0x07,0x03,0x01]) +a.u8("BMP_FLAT", [0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12]) +a.u8("BMP_F", [0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e]) +a.u8("BMP_LOCK", [0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c]) +a.main() diff --git a/App/apps/pack_app.py b/App/apps/pack_app.py index 4d16d752..c655f428 100644 --- a/App/apps/pack_app.py +++ b/App/apps/pack_app.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 -# Pack an overlay-app raw binary into a .app blob: 64-byte header + code. +# Pack an overlay-app raw binary into a .app blob: 64-byte header + code +# [+ read-only assets]. The host installer writes the code at slot+0x1000 and +# the assets (the bytes after code_size) at slot+APP_ASSET_OFFSET. # Header layout mirrors app_overlay.h : app_header_t (little-endian, packed). # The format/version constants (magic, header + ABI/API levels, overlay budget) are # read straight from the C headers the firmware itself compiles, so there is a @@ -9,6 +11,8 @@ # # ./pack_app.py breakout/breakout.bin breakout/breakout.app --name Breakout \ # --ver 1.0 --vma 0x20000280 --api-min 1 +# ./pack_app.py game.bin Game.app --name Game --vma 0x20000280 --api-min 2 \ +# --assets levels.bin # served to the app by api->asset_read() import argparse, os, re, struct, zlib, sys HERE = os.path.dirname(os.path.abspath(__file__)) @@ -32,6 +36,8 @@ HDR_VERSION = cdefine("app_overlay.h", "APP_HDR_VERSION") ABI_MAJOR = cdefine("app_api.h", "APP_ABI_MAJOR") API_LEVEL = cdefine("app_api.h", "APP_API_LEVEL") OVERLAY_MAX = cdefine("app_overlay.h", "APP_OVERLAY_MAX") # 4 KiB overlay budget +ASSET_MAX = cdefine("app_overlay.h", "APP_ASSET_MAX") # header-sector asset area +API_ASSETS = cdefine("app_api.h", "APP_API_ASSETS") # first level serving assets FLAG_COMMITTED = cdefine("app_overlay.h", "APP_FLAG_COMMITTED") FLAG_SCREEN_SAVER = cdefine("app_overlay.h", "APP_FLAG_SCREEN_SAVER") FLAG_SHORTCUT_SHIFT = cdefine("app_overlay.h", "APP_FLAG_SHORTCUT_SHIFT") @@ -68,6 +74,8 @@ def main(): help="resident quick action advertised by this app") ap.add_argument("--require", action="append", choices=CAPABILITIES, default=[], help="resident capability required by this app (repeatable)") + ap.add_argument("--assets", + help=f"read-only asset file served by api->asset_read (<= {ASSET_MAX} B)") a = ap.parse_args() if not 1 <= a.api_min <= API_LEVEL: @@ -79,6 +87,13 @@ def main(): if len(code) > OVERLAY_MAX: sys.exit(f"code {len(code)} B exceeds overlay budget {OVERLAY_MAX} B") + assets = open(a.assets, "rb").read() if a.assets else b"" + if len(assets) > ASSET_MAX: + sys.exit(f"assets {len(assets)} B exceed the {ASSET_MAX} B asset area") + if assets and a.api_min < API_ASSETS: + sys.exit(f"--assets needs --api-min {API_ASSETS} or higher (asset_read)") + asset_crc = (zlib.crc32(assets) & 0xFFFF) if assets else 0 + crc = zlib.crc32(code) & 0xFFFFFFFF flags = (FLAG_COMMITTED | (FLAG_SCREEN_SAVER if a.screensaver else 0) | @@ -87,22 +102,24 @@ def main(): for capability in a.require: required_caps |= CAPABILITIES[capability] header = struct.pack( - "<4sHBBIIHH16s16sII4s", + "<4sHBBIIHH16s16sIIHH", MAGIC, HDR_VERSION, ABI_MAJOR, a.api_min, len(code), crc, a.entry, flags, field(a.name, 16), field(a.ver, 16), a.vma, - required_caps, b"\x00" * 4, + required_caps, len(assets), asset_crc, ) assert len(header) == 64, len(header) with open(a.outfile, "wb") as f: f.write(header) f.write(code) + f.write(assets) print(f"{a.outfile}: name={a.name!r} ver={a.ver!r} " f"abi={ABI_MAJOR} api>={a.api_min} caps=0x{required_caps:08x} " f"vma=0x{a.vma:08x} " - f"code={len(code)} B crc32=0x{crc:08x} -> blob {64 + len(code)} B") + f"code={len(code)} B crc32=0x{crc:08x} assets={len(assets)} B " + f"-> blob {64 + len(code) + len(assets)} B") if __name__ == "__main__": main() diff --git a/App/apps/plasma/build.sh b/App/apps/plasma/build.sh index 69959c3d..cc8e53a3 100755 --- a/App/apps/plasma/build.sh +++ b/App/apps/plasma/build.sh @@ -8,8 +8,8 @@ set -euo pipefail APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ... APP_NAME="Plasma" # <-- the only per-app line -APP_VER="1.0" -APP_API_MIN=1 +APP_VER="1.1" +APP_API_MIN=2 APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM) @@ -24,13 +24,14 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous -step() { printf '\r ๐Ÿ”จ %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; } +step() { printf '\r ๐Ÿ”จ %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } trap 'printf "\r โŒ %-13s build failed \n" "$APP_NAME"' ERR -step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" -step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ - --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ + --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/plasma/gen_assets.py b/App/apps/plasma/gen_assets.py new file mode 100755 index 00000000..c943e1cb --- /dev/null +++ b/App/apps/plasma/gen_assets.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# Plasma read-only assets: the sine wave, the ordered-dither matrix and the +# pattern presets. The app copies them onto its stack at launch, so the render +# loop reads them from RAM at full speed while they stay out of the overlay. +# +# ./gen_assets.py plasma_assets.bin plasma_assets.h +import os, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets + +# 32 * sin(2*pi*i/256): four of these sum to -128..128. +SIN = [ + 0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 9, 10, 11, 12, + 12, 13, 14, 14, 15, 16, 16, 17, 18, 18, 19, 20, 20, 21, 21, 22, + 23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 29, + 30, 30, 30, 30, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 30, 30, 30, + 30, 29, 29, 29, 28, 28, 27, 27, 27, 26, 26, 25, 25, 24, 24, 23, + 23, 22, 21, 21, 20, 20, 19, 18, 18, 17, 16, 16, 15, 14, 14, 13, + 12, 12, 11, 10, 9, 9, 8, 7, 6, 5, 5, 4, 3, 2, 2, 1, + 0, -1, -2, -2, -3, -4, -5, -5, -6, -7, -8, -9, -9, -10, -11, -12, + -12, -13, -14, -14, -15, -16, -16, -17, -18, -18, -19, -20, -20, -21, -21, -22, + -23, -23, -24, -24, -25, -25, -26, -26, -27, -27, -27, -28, -28, -29, -29, -29, + -30, -30, -30, -30, -31, -31, -31, -31, -31, -32, -32, -32, -32, -32, -32, -32, + -32, -32, -32, -32, -32, -32, -32, -32, -31, -31, -31, -31, -31, -30, -30, -30, + -30, -29, -29, -29, -28, -28, -27, -27, -27, -26, -26, -25, -25, -24, -24, -23, + -23, -22, -21, -21, -20, -20, -19, -18, -18, -17, -16, -16, -15, -14, -14, -13, + -12, -12, -11, -10, -9, -9, -8, -7, -6, -5, -5, -4, -3, -2, -2, -1, +] + +a = Assets("PLASMA") +a.i8("SIN", SIN) +# 4x4 ordered-dither matrix, flattened: idx = (y&3)*4 + (x&3), values 0..15. +a.u8("BAYER", [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5]) +# Pattern presets: {x scale, y scale, diagonal scale, radial ring shift}. +a.u8("VAR", [4, 4, 3, 5, 6, 3, 5, 4, 3, 7, 2, 6, 5, 5, 4, 5, 2, 8, 6, 4]) +a.const("NVAR", 5) +a.main() diff --git a/App/apps/plasma/plasma_app.c b/App/apps/plasma/plasma_app.c index 32c036c2..e278aa97 100644 --- a/App/apps/plasma/plasma_app.c +++ b/App/apps/plasma/plasma_app.c @@ -30,40 +30,15 @@ #include #include #include "../app_api.h" +#include "plasma_assets.h" /* generated by gen_assets.py */ #define W 128 #define H 64 static const app_api_t *A; -/* 32 * sin(2*pi*i/256): four of these sum to -128..128. */ -static const int8_t SIN[256] = { - 0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 9, 10, 11, 12, - 12, 13, 14, 14, 15, 16, 16, 17, 18, 18, 19, 20, 20, 21, 21, 22, - 23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 29, - 30, 30, 30, 30, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, - 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 30, 30, 30, - 30, 29, 29, 29, 28, 28, 27, 27, 27, 26, 26, 25, 25, 24, 24, 23, - 23, 22, 21, 21, 20, 20, 19, 18, 18, 17, 16, 16, 15, 14, 14, 13, - 12, 12, 11, 10, 9, 9, 8, 7, 6, 5, 5, 4, 3, 2, 2, 1, - 0, -1, -2, -2, -3, -4, -5, -5, -6, -7, -8, -9, -9,-10,-11,-12, - -12,-13,-14,-14,-15,-16,-16,-17,-18,-18,-19,-20,-20,-21,-21,-22, - -23,-23,-24,-24,-25,-25,-26,-26,-27,-27,-27,-28,-28,-29,-29,-29, - -30,-30,-30,-30,-31,-31,-31,-31,-31,-32,-32,-32,-32,-32,-32,-32, - -32,-32,-32,-32,-32,-32,-32,-32,-31,-31,-31,-31,-31,-30,-30,-30, - -30,-29,-29,-29,-28,-28,-27,-27,-27,-26,-26,-25,-25,-24,-24,-23, - -23,-22,-21,-21,-20,-20,-19,-18,-18,-17,-16,-16,-15,-14,-14,-13, - -12,-12,-11,-10, -9, -9, -8, -7, -6, -5, -5, -4, -3, -2, -2, -1, -}; - -/* 4x4 ordered-dither matrix, flattened: idx = (y&3)*4 + (x&3), values 0..15. */ -static const uint8_t BAYER[16] = { 0,8,2,10, 12,4,14,6, 3,11,1,9, 15,7,13,5 }; - -/* Pattern presets: {x scale, y scale, diagonal scale, radial ring shift}. */ -#define NVAR 5 -static const uint8_t VAR[NVAR][4] = { - {4,4,3,5}, {6,3,5,4}, {3,7,2,6}, {5,5,4,5}, {2,8,6,4}, -}; +/* The sine wave, the 4x4 ordered-dither matrix and the pattern presets are + * read-only assets (gen_assets.py), copied onto app_main's stack at launch. */ /* Per-frame separable tables. */ static int8_t colA[W]; /* horizontal sine, by column x */ @@ -81,6 +56,13 @@ void app_main(const app_api_t *api) A->backlight_on(); A->status_clear(); + int8_t sine[SIN_LEN]; /* 32 * sin(2*pi*i/256) */ + uint8_t bayer[BAYER_LEN]; /* idx = (y&3)*4 + (x&3), values 0..15 */ + uint8_t presets[NVAR][4]; /* x, y, diagonal scales, radial shift */ + A->asset_read(SIN, sine, sizeof(sine)); + A->asset_read(BAYER, bayer, sizeof(bayer)); + A->asset_read(VAR, presets, sizeof(presets)); + uint16_t t1 = 0, t2 = 0, t3 = 0, tr = 0, tc = 0, tc2 = 0; uint16_t autoCtr = 0; uint8_t speed = 3; @@ -125,22 +107,22 @@ void app_main(const app_api_t *api) if (!running) break; - const uint8_t sx = VAR[var][0], sy = VAR[var][1]; - const uint8_t sd = VAR[var][2], rsh = VAR[var][3]; + const uint8_t sx = presets[var][0], sy = presets[var][1]; + const uint8_t sd = presets[var][2], rsh = presets[var][3]; /* drifting radial centre (gentle Lissajous), then the separable squares */ - const int cx = 64 + SIN[(uint8_t)tc]; /* 32..96 */ - const int cy = 32 + (SIN[(uint8_t)tc2] >> 1);/* 16..48 */ + const int cx = 64 + sine[(uint8_t)tc]; /* 32..96 */ + const int cy = 32 + (sine[(uint8_t)tc2] >> 1);/* 16..48 */ for (uint8_t x = 0; x < W; x++) { - colA[x] = SIN[(uint8_t)(x * sx + t1)]; + colA[x] = sine[(uint8_t)(x * sx + t1)]; int dx = (int)x - cx; sqx[x] = (uint16_t)(dx * dx); } for (uint8_t y = 0; y < H; y++) { - rowA[y] = SIN[(uint8_t)(y * sy + t2)]; + rowA[y] = sine[(uint8_t)(y * sy + t2)]; int dy = (int)y - cy; sqy[y] = (uint16_t)(dy * dy); } - for (uint16_t s = 0; s < W + H; s++) diagA[s] = SIN[(uint8_t)(s * sd + t3)]; + for (uint16_t s = 0; s < W + H; s++) diagA[s] = sine[(uint8_t)(s * sd + t3)]; /* render all 8 pages: page 0 -> status line (top), pages 1..7 -> fb[0..6] */ for (uint8_t x = 0; x < W; x++) { @@ -152,13 +134,13 @@ void app_main(const app_api_t *api) for (uint8_t b = 0; b < 8u; b++) { const uint8_t y = (uint8_t)(p * 8u + b); int v = cxv + rowA[y] + diagA[x + y] - + SIN[(uint8_t)(((sxv + sqy[y]) >> rsh) + tr)]; + + sine[(uint8_t)(((sxv + sqy[y]) >> rsh) + tr)]; v += 128; /* 0..256 */ bool on; if (bands) on = ((v >> 4) & 1u) != 0u; /* sweeping stripes */ else { int lvl = v >> 4; if (lvl > 15) lvl = 15; /* 0..15 stipple */ - on = lvl > (int)BAYER[((y & 3u) << 2) | bx]; + on = lvl > (int)bayer[((y & 3u) << 2) | bx]; } if (on) byte = (uint8_t)(byte | (1u << b)); } diff --git a/App/apps/rapidroll/build.sh b/App/apps/rapidroll/build.sh index 1a029980..6598e6ad 100755 --- a/App/apps/rapidroll/build.sh +++ b/App/apps/rapidroll/build.sh @@ -3,8 +3,8 @@ set -euo pipefail APP="$(basename "$PWD")" APP_NAME="Rapid Roll" -APP_VER="1.0" -APP_API_MIN=1 +APP_VER="1.1" +APP_API_MIN=2 APP_VMA=${APP_VMA:-0x20000280} OUT="${APP_NAME// /}" @@ -22,14 +22,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ rm -f ./*.app ./*.elf ./*.bin -step() { printf '\r ๐Ÿ”จ %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; } +step() { printf '\r ๐Ÿ”จ %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } trap 'printf "\r โŒ %-13s build failed \n" "$APP_NAME"' ERR -step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" -step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" \ - --vma "${APP_VMA}" --screensaver >/dev/null + --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/rapidroll/gen_assets.py b/App/apps/rapidroll/gen_assets.py new file mode 100755 index 00000000..e0042fd3 --- /dev/null +++ b/App/apps/rapidroll/gen_assets.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# Rapid Roll read-only assets: texts, decimal places, the sprite sheet +# (column-major, LSB = top row, read by blit() at draw time) and the 128x64 +# title screen. +# +# ./gen_assets.py rapidroll_assets.bin rapidroll_assets.h [preview.pgm] +import os, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets +from app_art import Canvas + +SPRITES = [ + ("SPR_BALL", [0x3C,0x42,0x8D,0x85,0x81,0x81,0x42,0x3C, # 4 frames: a mark turning + 0x3C,0x42,0x81,0x81,0x85,0x8D,0x42,0x3C, # as the ball rolls + 0x3C,0x42,0x81,0x81,0xA1,0xB1,0x42,0x3C, + 0x3C,0x42,0xB1,0xA1,0x81,0x81,0x42,0x3C]), + ("SPR_HEART", [0x1E,0x21,0x45,0x82,0x82,0x41,0x21,0x1E]), # 8x8, the size of the ball + ("SPR_LIFE", [0x06,0x0F,0x1E,0x0F,0x06]), + ("SPR_BOOM", [0x00,0x52,0x34,0x08,0x34,0x4A,0x08,0x00]), + ("SPR_TOOTH", [0x03,0x07,0x0F,0x07,0x03,0x01,0x01]), + ("SPR_SPIKE", [0x0C,0x0E,0x0F,0x0E,0x0C,0x08,0x08]), # 3-row tooth on a 1-row floor, drawn from y - 3 + ("SPR_SQUASH", [0x0C,0x12,0x21,0x21,0x21,0x21,0x12,0x0C]), # 8x6 ball, just landed +] + +def pill(cv, x, y, w): + """A safe platform as drawn by the game: hollow 3-row pill, rounded ends.""" + for k in range(w): + v = 0x02 if k in (0, w - 1) else 0x07 if k in (1, w - 2) else 0x05 + for bit in range(3): + if v >> bit & 1: + cv.set(x + k, y + bit) + +def title_screen(sprites): + """RAPID / ROLL, the ball on a platform, spikes and a heart to grab.""" + cv = Canvas() + cv.two_words("RAPID", "ROLL") + pill(cv, 4, 53, 28) + cv.sprite(14, 45, sprites["SPR_BALL"][:8]) + spike = sprites["SPR_SPIKE"] + cv.sprite(48, 50, [spike[k % 7] for k in range(26)]) # spiked floor, from y - 3 + pill(cv, 92, 49, 28) + cv.sprite(100, 41, sprites["SPR_HEART"]) + return cv + +preview_path = sys.argv.pop(3) if len(sys.argv) == 4 else None +a = Assets("RAPIDROLL") +a.text("T_LV", "LV") +a.text("T_GAME_OVER", "GAME OVER") +a.text("T_PAUSE", "PAUSE") +a.text("T_LEVEL1", "LEVEL 1") +a.text("T_PRESS", "PRESS MENU") +a.u16("PLACE", [10000, 1000, 100, 10, 1]) +for name, cols in SPRITES: + a.u8(name, cols) +title = title_screen(dict(SPRITES)) +a.raw("ART_TITLE", title.pages()) +a.main() +if preview_path: + title.preview(preview_path) diff --git a/App/apps/rapidroll/rapidroll_app.c b/App/apps/rapidroll/rapidroll_app.c index 8b517c92..56e9cfeb 100644 --- a/App/apps/rapidroll/rapidroll_app.c +++ b/App/apps/rapidroll/rapidroll_app.c @@ -27,6 +27,7 @@ #include #include #include "../app_api.h" +#include "rapidroll_assets.h" /* generated by gen_assets.py */ #define W 128 #define TICK_MS 40u @@ -46,36 +47,17 @@ slow LCD but choppier, 1 is smooth */ #define NONE 0xFFu -enum { ST_PLAY, ST_OVER }; +enum { ST_PLAY, ST_OVER, ST_TITLE }; enum { P_FREE, P_SAFE, P_SPIKE, P_CRUMBLE }; -/* Sprite offsets in ART[] (column-major, LSB = top row). */ -#define SPR_BALL 0u /* 4 frames: a mark turning as the ball rolls */ -#define SPR_HEART 32u /* 8x8, the size of the ball */ -#define SPR_LIFE 40u -#define SPR_BOOM 45u -#define SPR_TOOTH 53u -#define SPR_SPIKE 60u /* one tooth period (7 px, as the ceiling), drawn from y - 3 */ -#define SPR_SQUASH 67u +/* Texts, decimal places and sprites (column-major, LSB = top row) are + * read-only assets (gen_assets.py), fetched from external flash when drawn. */ typedef struct { int8_t y; uint8_t x, w, kind, heart; } plat_t; /* heart: crumble timer on P_CRUMBLE */ -static const uint8_t ART[] = { - /* ball @ 0 */ 0x3C,0x42,0x8D,0x85,0x81,0x81,0x42,0x3C, /* ring: solid smears */ - 0x3C,0x42,0x81,0x81,0x85,0x8D,0x42,0x3C, - 0x3C,0x42,0x81,0x81,0xA1,0xB1,0x42,0x3C, - 0x3C,0x42,0xB1,0xA1,0x81,0x81,0x42,0x3C, - /* heart @32 */ 0x1E,0x21,0x45,0x82,0x82,0x41,0x21,0x1E, - /* life @40 */ 0x06,0x0F,0x1E,0x0F,0x06, - /* boom @45 */ 0x00,0x52,0x34,0x08,0x34,0x4A,0x08,0x00, - /* tooth @53 */ 0x03,0x07,0x0F,0x07,0x03,0x01,0x01, - /* spike @60 */ 0x0C,0x0E,0x0F,0x0E,0x0C,0x08,0x08, /* 3-row tooth on a 1-row floor */ - /* squash @67 */ 0x0C,0x12,0x21,0x21,0x21,0x21,0x12,0x0C, /* 8x6 ball, just landed */ -}; - static const app_api_t *A; static plat_t plats[N_PLAT]; -static uint32_t randomState, score; +static uint32_t score; static int16_t bx, by, boomX, boomY; /* ball and explosion, top-left */ static int8_t lastRowY; /* y of the most recently spawned row */ static uint8_t lastX, nextX; /* current and planned safe platform x */ @@ -83,6 +65,7 @@ static uint8_t on; /* platform under the ball, or NONE */ static uint8_t mode, prevKey, lives, level, rows, riseAcc; static uint8_t banner, waitCd, boom, respawnCd, invul, vy, squash; static bool running, paused, saver, alive, spikeAllowed; +static uint8_t titleTick; static char text[6]; void *memset(void *dst, int value, size_t size) @@ -92,10 +75,17 @@ void *memset(void *dst, int value, size_t size) return dst; } +/* Text from the assets in a shared buffer, valid until the next T() call. */ +static char tbuf[TEXT_MAX]; +static char *T(uint16_t off) +{ + A->asset_read(off, tbuf, TEXT_MAX); + return tbuf; +} + static uint32_t rnd(void) { - randomState = randomState * 1664525u + 1013904223u; - return randomState >> 8; + return A->rand32() >> 8; /* resident PRNG, seeded by the loader */ } static void pixel(int16_t x, int16_t y) @@ -111,15 +101,20 @@ static void column(int16_t x, int16_t y, uint8_t v) if (v & 1u) pixel(x, y); } -/* Draw a column-major sprite (up to 8 rows high, LSB = top). */ -static void blit(int16_t x, int16_t y, const uint8_t *col, uint8_t w) +/* Draw a column-major sprite (up to 8 rows high, LSB = top, w <= 8) read + * from the assets at offset art. */ +static void blit(int16_t x, int16_t y, uint16_t art, uint8_t w) { + uint8_t cols[8]; + const uint8_t *col = cols; + A->asset_read(art, cols, w); while (w--) column(x++, y, *col++); } static void number(uint32_t value, uint8_t width) { - static const uint16_t place[5] = {10000u, 1000u, 100u, 10u, 1u}; + uint16_t place[5]; + A->asset_read(PLACE, place, sizeof(place)); const uint8_t first = (uint8_t)(5u - width); for (uint8_t i = 0; i < width; i++) { uint8_t digit = 0; @@ -292,7 +287,7 @@ static void draw(void) for (int16_t y = 10; y < 64; y += 2) { pixel(0, y); pixel(W - 1, y); } /* x = 0/127 are tooth-free */ /* Ceiling: 7-px tooth period started at x = -5, so the 18 visible teeth * span x = 2..125 and leave the same 2-px margin on both sides. */ - for (int16_t x = -5; x < W; x += 7) blit(x, 8, &ART[SPR_TOOTH], 7); + for (int16_t x = -5; x < W; x += 7) blit(x, 8, SPR_TOOTH, 7); for (uint8_t i = 0; i < N_PLAT; i++) { const plat_t *p = &plats[i]; @@ -300,7 +295,7 @@ static void draw(void) for (uint8_t k = 0; k < p->w; k++) /* hollow 3-row pill with rounded ends */ column(p->x + k, p->y, (k == 0u || k == p->w - 1u) ? 0x02u : (k == 1u || k == p->w - 2u) ? 0x07u : 0x05u); - if (p->heart && p->y >= CEIL_Y + 8) blit(p->x + 10, p->y - 8, &ART[SPR_HEART], 8); + if (p->heart && p->y >= CEIL_Y + 8) blit(p->x + 10, p->y - 8, SPR_HEART, 8); } else if (p->kind == P_CRUMBLE) { if (!(p->heart & 2u)) /* cracked pill, blinking once it starts to give way */ for (uint8_t k = 0; k < p->w; k++) @@ -308,26 +303,39 @@ static void draw(void) : (k == 1u || k == p->w - 2u) ? 0x07u : ((k & 3u) == 3u) ? 0x00u : 0x05u); } else if (p->kind == P_SPIKE) { + uint8_t spike[SPR_SPIKE_LEN]; /* one tooth period, read once per row */ + A->asset_read(SPR_SPIKE, spike, sizeof(spike)); for (uint8_t k = 0, j = 0; k < SPIKE_W; k++, j = j == 6u ? 0u : j + 1u) - column(p->x + k, p->y - 3, ART[SPR_SPIKE + j]); + column(p->x + k, p->y - 3, spike[j]); } } if (alive && !(invul & 2u)) { /* blinks after a respawn; the mark turns every 4 px */ - if (squash) blit(bx, by + 2, &ART[SPR_SQUASH], 8); - else blit(bx, by, &ART[SPR_BALL + ((bx >> 2) & 3u) * 8u], 8); + if (squash) blit(bx, by + 2, SPR_SQUASH, 8); + else blit(bx, by, SPR_BALL + ((bx >> 2) & 3u) * 8u, 8); } - if (boom) blit(boomX, boomY, &ART[SPR_BOOM], 8); + if (boom) blit(boomX, boomY, SPR_BOOM, 8); - for (uint8_t i = 0; i < lives; i++) blit(1 + i * 6, 1, &ART[SPR_LIFE], 5); + for (uint8_t i = 0; i < lives; i++) blit(1 + i * 6, 1, SPR_LIFE, 5); if (!(banner & 4u)) { /* blinks for a while after a level change */ - A->print_tiny("LV", 52, 1, true, true); + A->print_tiny(T(T_LV), 52, 1, true, true); number(level, 1); A->print_tiny(text, 62, 1, true, true); } number(score > 99999u ? 99999u : score, 5); A->print_tiny(text, 107, 1, true, true); - if (mode == ST_OVER) A->print_bold("GAME OVER", 0, 127, 3); - else if (paused) A->print_bold("PAUSE", 0, 127, 3); - else if (banner > 10u && level == 1u) A->print_bold("LEVEL 1", 0, 127, 2); /* start only: it overwrites its line */ + if (mode == ST_OVER) A->print_bold(T(T_GAME_OVER), 0, 127, 3); + else if (paused) A->print_bold(T(T_PAUSE), 0, 127, 3); + else if (banner > 10u && level == 1u) A->print_bold(T(T_LEVEL1), 0, 127, 2); /* start only: it overwrites its line */ +} + +/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers, + * with a blinking prompt on the blank bottom page. */ +static void draw_title(void) +{ + titleTick++; + A->asset_read(ART_TITLE, A->status_line, W); + A->asset_read(ART_TITLE + W, A->fb[0], 7u * W); + if (titleTick & 16u) + A->print_bold(T(T_PRESS), 0, 127, 6); } static void poll_key(void) @@ -359,18 +367,18 @@ __attribute__((section(".text.entry"), used)) void app_main(const app_api_t *api) { A = api; - randomState = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq(); running = true; saver = false; prevKey = A->get_key(); A->led(false); A->backlight_on(); - new_game(); + mode = ST_TITLE; /* MENU starts a game, like after GAME OVER */ while (running) { poll_key(); if (!running) break; if (saver) { A->backlight_update(); A->delay_ms(TICK_MS); continue; } - if (!paused) tick(); - draw(); A->blit_status(); A->blit_full(); A->backlight_update(); + if (mode == ST_TITLE) draw_title(); + else { if (!paused) tick(); draw(); } + A->blit_status(); A->blit_full(); A->backlight_update(); A->delay_ms(TICK_MS); } A->led(false); diff --git a/App/apps/snake/build.sh b/App/apps/snake/build.sh index 9b1dfdda..39493df7 100644 --- a/App/apps/snake/build.sh +++ b/App/apps/snake/build.sh @@ -3,8 +3,8 @@ set -euo pipefail APP="$(basename "$PWD")" APP_NAME="Snake" -APP_VER="1.0" -APP_API_MIN=1 +APP_VER="1.1" +APP_API_MIN=2 APP_VMA=${APP_VMA:-0x20000280} OUT="${APP_NAME// /}" @@ -22,14 +22,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ rm -f ./*.app ./*.elf ./*.bin -step() { printf '\r ๐Ÿ”จ %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; } +step() { printf '\r ๐Ÿ”จ %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } trap 'printf "\r โŒ %-13s build failed \n" "$APP_NAME"' ERR -step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" -step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" \ - --vma "${APP_VMA}" --screensaver >/dev/null + --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/snake/gen_assets.py b/App/apps/snake/gen_assets.py new file mode 100755 index 00000000..0aeb866d --- /dev/null +++ b/App/apps/snake/gen_assets.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# Snake read-only assets: texts, decimal places, direction steps, the 4x4 +# sprites (16-bit masks, bit i = pixel (i & 3, i >> 2)) and the 128x64 title +# screen. The four sprite sets are contiguous: the app loads them onto its +# stack in one read per frame. +# +# ./gen_assets.py snake_assets.bin snake_assets.h [preview.pgm] +import os, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets +from app_art import Canvas + +HEAD = [0x6566, 0x0FD2, 0x66A6, 0x0FB4] # by direction: up, right, down, left +HEAD_OPEN = [0x65A9, 0xC35A, 0x95A6, 0x3CA5] # facing the food +BODY = [0, 0, 0, 0x0CA6, 0, 0x6426, 0x6AC0, 0, # by neighbour mask: + 0, 0x0356, 0x0DB0, 0, 0x6530, 0, 0, 0] # up 1, right 2, down 4, left 8 +TAIL = [0, 0x0466, 0x0CE0, 0, 0x6620, 0, 0, 0, + 0x0370, 0, 0, 0, 0, 0, 0, 0] +FOOD = 0x6996 +STEP = {"U": (0, -1), "R": (1, 0), "D": (0, 1), "L": (-1, 0)} +NEIGHBOUR = {(0, -1): 1, (1, 0): 2, (0, 1): 4, (-1, 0): 8} +HEADING = {(0, -1): 0, (1, 0): 1, (0, 1): 2, (-1, 0): 3} + +def cell(cv, mask, gx, gy): + """One 4x4 sprite on the game's board grid (as draw_sprite()).""" + for i in range(16): + if mask >> i & 1: + cv.set(2 + gx * 4 + (i & 3), 10 + gy * 4 + (i >> 2)) + +def title_screen(): + """The logo inside the board frame; a snake at game scale heads for food.""" + cv = Canvas() + for x in range(1, 127): # board frame, as render() + cv.set(x, 1); cv.set(x, 54) + for y in range(1, 55): + cv.set(1, y); cv.set(126, y) + cv.word("SNAKE", 6, kx=1.3, ky=1.1) + x, y = 2, 8 # tail cell, then the moves + cells = [(x, y)] + for move, count in (("R", 8), ("U", 2), ("R", 6), ("D", 2), ("R", 8)): + for _ in range(count): + x, y = x + STEP[move][0], y + STEP[move][1] + cells.append((x, y)) + for k, (cx, cy) in enumerate(cells): + if k == len(cells) - 1: # head, mouth open towards the food + px, py = cells[k - 1] + mask = HEAD_OPEN[HEADING[(cx - px, cy - py)]] + elif k == 0: + nx, ny = cells[1] + mask = TAIL[NEIGHBOUR[(nx - cx, ny - cy)]] + else: + (px, py), (nx, ny) = cells[k - 1], cells[k + 1] + mask = BODY[NEIGHBOUR[(nx - cx, ny - cy)] | NEIGHBOUR[(px - cx, py - cy)]] + cell(cv, mask, cx, cy) + cell(cv, FOOD, 28, 8) + return cv + +preview_path = sys.argv.pop(3) if len(sys.argv) == 4 else None +a = Assets("SNAKE") +a.text("T_SCORE", "SCORE:") +a.text("T_BEST", "BEST:") +a.text("T_NEW_SCORE", "NEW SCORE") +a.text("T_GAME_OVER", "GAME OVER") +a.text("T_PAUSE", "PAUSE") +a.text("T_PRESS", "PRESS MENU") +a.u16("PLACE", [10000, 1000, 100, 10]) +a.i8("DXY", [0, -1, 1, 0, 0, 1, -1, 0]) # (dx, dy) for up, right, down, left +# Direction bit from a neighbour's packed (dx + 2 * dy) & 7 offset. +a.u8("DIR_MASK", [0, 2, 4, 0, 0, 0, 1, 8]) +a.u16("SPR_HEAD", HEAD) +a.u16("SPR_HEAD_OPEN", HEAD_OPEN) +a.u16("SPR_BODY", BODY) +a.u16("SPR_TAIL", TAIL) +title = title_screen() +a.raw("ART_TITLE", title.pages()) +a.main() +if preview_path: + title.preview(preview_path) diff --git a/App/apps/snake/snake_app.c b/App/apps/snake/snake_app.c index fb7aa0ac..78bd85a1 100644 --- a/App/apps/snake/snake_app.c +++ b/App/apps/snake/snake_app.c @@ -28,6 +28,7 @@ #include #include #include "../app_api.h" +#include "snake_assets.h" /* generated by gen_assets.py */ #define W 128u @@ -59,32 +60,25 @@ typedef struct { static const app_api_t *A; static uint32_t board_map[ROWS]; -static uint32_t randomState, best, initial_best; +static uint32_t best, initial_best; static segment_t snake[RING_SIZE]; static uint16_t head_idx, tail_idx, repeatMs; static int16_t moveTimer; static uint8_t foodX, foodY, direction, nextDirection, previousKey; -static bool paused, gameOver, running, newBest, saverPaused, saverActive; +static bool paused, gameOver, running, newBest, saverPaused, saverActive, title; +static uint8_t titleTick; static char text[6]; -static const int8_t DX[4] = { 0, 1, 0, -1 }; -static const int8_t DY[4] = {-1, 0, 1, 0 }; +/* Texts, decimal places, direction steps and sprites are read-only assets + * (gen_assets.py). The sprite sets and the direction map are copied onto the + * stack once per frame by draw_snake(). */ -/* --- Branchless Direction Map --- */ -static const uint8_t DIR_MASK[8] = { [6]=1, [1]=2, [2]=4, [7]=8 }; +/* Sprite index in draw_snake()'s copy of the SPR_HEAD..SPR_TAIL block. */ +#define SPR_INDEX(item) (((item) - SPR_HEAD) / 2u) +#define SPR_BLOCK_LEN (SPR_TAIL + SPR_TAIL_LEN - SPR_HEAD) -/* Macro for compact direction evaluation */ -#define GET_DIR(n_idx, s_x, s_y) DIR_MASK[(((snake[n_idx].x & 0x7F) - (s_x)) + ((snake[n_idx].y - (s_y)) << 1)) & 7] - -/* --- Sprites --- */ -static const uint16_t HEAD_SPRITE[4] = { 0x6566, 0x0FD2, 0x66A6, 0x0FB4 }; -static const uint16_t HEAD_OPEN_SPRITE[4] = { 0x65A9, 0xC35A, 0x95A6, 0x3CA5 }; -static const uint16_t BODY_SPRITE[16] = { - 0, 0, 0, 0x0CA6, 0, 0x6426, 0x6AC0, 0, 0, 0x0356, 0x0DB0, 0, 0x6530, 0, 0, 0 -}; -static const uint16_t TAIL_SPRITE[16] = { - 0, 0x0466, 0x0CE0, 0, 0x6620, 0, 0, 0, 0x0370, 0, 0, 0, 0, 0, 0, 0 -}; +/* Macro for compact direction evaluation (dir_mask: draw_snake()'s stack copy) */ +#define GET_DIR(n_idx, s_x, s_y) dir_mask[(((snake[n_idx].x & 0x7F) - (s_x)) + ((snake[n_idx].y - (s_y)) << 1)) & 7] static void new_game(void); @@ -92,16 +86,24 @@ static void new_game(void); /* Utilities & RNG */ /* -------------------------------------------------------------------------- */ -static uint32_t random_next(void) +/* Text from the assets in a shared buffer, valid until the next T() call. */ +static char tbuf[TEXT_MAX]; +static char *T(uint16_t off) { - randomState = randomState * 1664525u + 1013904223u; - return randomState; + A->asset_read(off, tbuf, TEXT_MAX); + return tbuf; +} + +/* One grid step (dx, dy) for a direction: up, right, down, left. */ +static void step_of(uint32_t dir, int8_t d[2]) +{ + A->asset_read(DXY + dir * 2u, d, 2); } static void place_food(void) { for (uint32_t tries = 0; tries < 128u; tries++) { - const uint32_t r = random_next(); + const uint32_t r = A->rand32(); /* resident PRNG, seeded by the loader */ const uint32_t x = ((r >> 24) * COLS) >> 8; const uint32_t y = (((r >> 16) & 0xFFu) * ROWS) >> 8; @@ -139,6 +141,11 @@ static void draw_sprite(uint32_t bx, uint32_t by, uint32_t sprite) static void draw_snake(void) { + uint16_t spr[SPR_BLOCK_LEN / 2u]; + uint8_t dir_mask[DIR_MASK_LEN]; + A->asset_read(SPR_HEAD, spr, sizeof(spr)); + A->asset_read(DIR_MASK, dir_mask, sizeof(dir_mask)); + uint32_t idx = head_idx; uint32_t next_idx = 0; uint32_t len = (head_idx - tail_idx + 1u) & RING_MASK; @@ -149,15 +156,17 @@ static void draw_snake(void) uint32_t sprite = 0; if (i == 0) { - bool open = ((sx + DX[direction]) == foodX && (sy + DY[direction]) == foodY); - sprite = open ? HEAD_OPEN_SPRITE[direction] : HEAD_SPRITE[direction]; + int8_t d[2]; + step_of(direction, d); + bool open = ((sx + d[0]) == foodX && (sy + d[1]) == foodY); + sprite = spr[(open ? SPR_INDEX(SPR_HEAD_OPEN) : SPR_INDEX(SPR_HEAD)) + direction]; } else if (snake[idx].x & 0x80) { sprite = 0x6FF6; } else if (i == len - 1u) { - sprite = TAIL_SPRITE[GET_DIR(next_idx, sx, sy)]; + sprite = spr[SPR_INDEX(SPR_TAIL) + GET_DIR(next_idx, sx, sy)]; } else { uint32_t prev_idx = (idx - 1u) & RING_MASK; - sprite = BODY_SPRITE[GET_DIR(next_idx, sx, sy) | GET_DIR(prev_idx, sx, sy)]; + sprite = spr[SPR_INDEX(SPR_BODY) + (GET_DIR(next_idx, sx, sy) | GET_DIR(prev_idx, sx, sy))]; } draw_sprite(BOARD_X + (sx << 2), BOARD_Y + (sy << 2), sprite); @@ -168,7 +177,8 @@ static void draw_snake(void) static void number_5(uint32_t value) { - static const uint16_t place[4] = { 10000u, 1000u, 100u, 10u }; + uint16_t place[4]; + A->asset_read(PLACE, place, sizeof(place)); if (value > 99999u) value = 99999u; @@ -201,18 +211,18 @@ static void render(void) draw_snake(); /* Score computed dynamically based on current length */ - A->print_tiny("SCORE:", 2, 1, true, true); + A->print_tiny(T(T_SCORE), 2, 1, true, true); number_5((((head_idx - tail_idx + 1u) & RING_MASK) - 3u) * SCORE_PER_FOOD); A->print_tiny(text, 32, 1, true, true); - A->print_tiny("BEST:", 81, 1, true, true); + A->print_tiny(T(T_BEST), 81, 1, true, true); number_5(best); A->print_tiny(text, 107, 1, true, true); if (gameOver) - A->print_bold(newBest ? "NEW SCORE" : "GAME OVER", 0, W - 1, 3); + A->print_bold(T(newBest ? T_NEW_SCORE : T_GAME_OVER), 0, W - 1, 3); else if (paused) - A->print_bold("PAUSE", 0, W - 1, 3); + A->print_bold(T(T_PAUSE), 0, W - 1, 3); } /* -------------------------------------------------------------------------- */ @@ -222,8 +232,10 @@ static void render(void) static void move_snake(void) { /* Casting to unsigned handles both upper limit and negative underflow in one instruction */ - uint32_t nx = (snake[head_idx].x & 0x7F) + DX[nextDirection]; - uint32_t ny = snake[head_idx].y + DY[nextDirection]; + int8_t d[2]; + step_of(nextDirection, d); + uint32_t nx = (snake[head_idx].x & 0x7F) + d[0]; + uint32_t ny = snake[head_idx].y + d[1]; if (nx >= COLS || ny >= ROWS) { gameOver = true; @@ -271,6 +283,17 @@ static void move_snake(void) /* Input & Loop */ /* -------------------------------------------------------------------------- */ +/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers, + * with a blinking prompt on the blank bottom page. */ +static void draw_title(void) +{ + titleTick++; + A->asset_read(ART_TITLE, A->status_line, W); + A->asset_read(ART_TITLE + W, A->fb[0], 7u * W); + if (titleTick & 32u) + A->print_bold(T(T_PRESS), 0, W - 1, 6); +} + static void key_action(uint32_t key, bool repeat) { if (key == APP_KEY_EXIT) { @@ -278,6 +301,14 @@ static void key_action(uint32_t key, bool repeat) return; } + if (title) { /* the game-over restart keys also leave the title */ + if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0)) { + title = false; + new_game(); + } + return; + } + if (gameOver) { if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0)) new_game(); @@ -380,10 +411,6 @@ void app_main(const app_api_t *api) best = (cfg.magic == CFG_MAGIC && cfg.version == 1u) ? cfg.best : 0u; initial_best = best; - randomState = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq() ^ best ^ 0x534E414Bu; - if (!randomState) - randomState = 1u; - previousKey = APP_KEY_INVALID; repeatMs = 0; running = true; @@ -392,6 +419,7 @@ void app_main(const app_api_t *api) A->backlight_on(); new_game(); + title = true; while (running) { poll_key(); @@ -404,7 +432,7 @@ void app_main(const app_api_t *api) continue; } - if (!paused && !gameOver) { + if (!title && !paused && !gameOver) { if (moveTimer <= 0) { moveTimer = MOVE_INTERVAL; move_snake(); @@ -413,7 +441,8 @@ void app_main(const app_api_t *api) } } - render(); + if (title) draw_title(); + else render(); A->blit_status(); A->blit_full(); A->backlight_update(); diff --git a/App/apps/spaceimpact/build.sh b/App/apps/spaceimpact/build.sh index f4a8a1f9..e10c3ce6 100755 --- a/App/apps/spaceimpact/build.sh +++ b/App/apps/spaceimpact/build.sh @@ -4,8 +4,8 @@ set -euo pipefail APP="$(basename "$PWD")" APP_NAME="Space Impact" -APP_VER="1.0" -APP_API_MIN=1 +APP_VER="1.1" +APP_API_MIN=2 APP_VMA=${APP_VMA:-0x20000280} OUT="${APP_NAME// /}" @@ -20,14 +20,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ rm -f ./*.app ./*.elf ./*.bin -step() { printf '\r ๐Ÿ”จ %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; } +step() { printf '\r ๐Ÿ”จ %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } trap 'printf "\r โŒ %-13s build failed \n" "$APP_NAME"' ERR -step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" -step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" \ - --vma "${APP_VMA}" --screensaver >/dev/null + --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/spaceimpact/gen_assets.py b/App/apps/spaceimpact/gen_assets.py new file mode 100755 index 00000000..d70e894f --- /dev/null +++ b/App/apps/spaceimpact/gen_assets.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# Space Impact read-only assets: texts, tables, the sprite sheet (column-major, +# LSB = top row, read by blit() at draw time) and the 128x64 title screen as 8 +# LCD pages of 128 bytes (page 0 = status line, pages 1..7 = frame buffer; +# page 7 stays blank for the blinking prompt). +# +# ./gen_assets.py spaceimpact_assets.bin spaceimpact_assets.h [preview.pgm] +import os, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets +from app_art import Canvas + +SPRITES = [ + ("SPR_SHIP", [0x41,0x6B,0x7F,0x3E,0x3E,0x1C,0x1C,0x1C,0x08,0x08]), + ("SPR_ENEMY", [0x4C,0x32,0x15,0x21,0x21,0x15,0x32,0x4C, # invader (+ type * 8) + 0x08,0x14,0x14,0x14,0x22,0x41,0x49,0x77, # jet + 0x0C,0x56,0x3D,0x15,0x15,0x3D,0x56,0x0C, # saucer + 0x0E,0x11,0x6D,0x41,0x41,0x6D,0x11,0x0E]), # skull + ("SPR_BOOM", [0x00,0x00,0x08,0x1C,0x08,0x00,0x00,0x00, # + frame * 8 + 0x41,0x04,0x10,0x40,0x02,0x20,0x08,0x41]), + ("SPR_BOSS", [0x00,0x78,0x84,0x02,0x32,0x31,0x01,0x01,0x01,0x01,0x31,0x32,0x02,0x84,0x78,0x00, # octopus + 0x30,0x0C,0x82,0x71,0x0D,0x03,0xE0,0x1E,0x02,0x00,0x03,0x0D,0x31,0xC2,0x0C,0x30, + 0xC0,0x20,0x10,0xDC,0xD2,0x12,0x1D,0xD1,0xD1,0x1D,0x12,0xD2,0xDC,0x10,0x20,0xC0, # mothership (+32) + 0x20,0x11,0x0A,0x06,0x02,0x02,0x02,0x3E,0x3E,0x02,0x02,0x02,0x06,0x0A,0x11,0x20]), + ("SPR_HEART", [0x06,0x0F,0x1E,0x0F,0x06]), + ("SPR_MISS", [0x1B,0x0E,0x04,0x0E,0x0E,0x0E,0x0E,0x04]), # 8x5, also the HUD stock icon +] + +def title_screen(sprites): + cv = Canvas() + cv.two_words("SPACE", "IMPACT") + # Scene under the logo: the ship firing at an invader wave. + ship, enemy = sprites["SPR_SHIP"], sprites["SPR_ENEMY"] + cv.sprite(6, 46, ship) + for x in (18, 26, 34): # gun burst + for k in range(4): + cv.set(x + k, 49) + for i, ex in enumerate((52, 68, 84, 100)): + cv.sprite(ex, 45 + (i & 1) * 3, enemy[8 * (i & 3):8 * (i & 3) + 8]) + return cv + +def main(): + preview_path = sys.argv.pop(3) if len(sys.argv) == 4 else None + a = Assets("SPACEIMPACT") + a.text("T_GAME_OVER", "GAME OVER") + a.text("T_PAUSE", "PAUSE") + a.text("T_PRESS", "PRESS MENU") + a.text("T_LEVEL", "LEVEL 1") # digit patched at run time + a.u16("PLACE", [10000, 1000, 100, 10, 1]) + a.u8("POINTS", [10, 15, 40, 25]) # score per enemy type + a.u8("SPEED", [1, 2, 1, 1]) # pixels per tick per enemy type + for name, cols in SPRITES: + a.u8(name, cols) + cv = title_screen(dict(SPRITES)) + a.raw("ART_TITLE", cv.pages()) + a.main() + if preview_path: + cv.preview(preview_path) + +if __name__ == "__main__": + main() diff --git a/App/apps/spaceimpact/spaceimpact_app.c b/App/apps/spaceimpact/spaceimpact_app.c index 9708530b..046463c2 100644 --- a/App/apps/spaceimpact/spaceimpact_app.c +++ b/App/apps/spaceimpact/spaceimpact_app.c @@ -15,12 +15,18 @@ * * Controls: 2/8 or UP/DOWN move the ship, 5/MENU launches a piercing * missile, F pauses and EXIT quits. + * + * Texts, tables and artwork live in the app's read-only assets (built by + * gen_assets.py), not in the 4 KiB overlay: sprites are fetched from external + * flash as they are drawn and the title screen is streamed straight into the + * frame buffer. */ #include #include #include #include "../app_api.h" +#include "spaceimpact_assets.h" /* generated by gen_assets.py */ #define W 128 #define TICK_MS 40u @@ -33,43 +39,16 @@ #define LEVEL_LEN 700u /* ticks of waves before the boss */ #define BOOM 10u /* explosion length in ticks */ -enum { ST_PLAY, ST_OVER }; - -/* Sprite offsets in ART[] (column-major, LSB = top row). */ -#define SPR_SHIP 0u -#define SPR_ENEMY 10u /* + type * 8 */ -#define SPR_BOOM 42u /* + frame * 8 */ -#define SPR_BOSS 58u /* octopus, then mothership (+32) */ -#define SPR_HEART 122u -#define SPR_MISS 127u /* 8x5, also the HUD stock icon */ +enum { ST_PLAY, ST_OVER, ST_TITLE }; typedef struct { int16_t x; int8_t y, vx, vy; uint8_t live; } shot_t; /* state: 0 free, 1 alive, >1 exploding while the counter decreases. */ typedef struct { int16_t x; int8_t y, base; uint8_t type, hp, state, phase; } enemy_t; -static const uint8_t ART[] = { - /* ship @ 0 */ 0x41,0x6B,0x7F,0x3E,0x3E,0x1C,0x1C,0x1C,0x08,0x08, - /* invader @ 10 */ 0x4C,0x32,0x15,0x21,0x21,0x15,0x32,0x4C, - /* jet @ 18 */ 0x08,0x14,0x14,0x14,0x22,0x41,0x49,0x77, - /* saucer @ 26 */ 0x0C,0x56,0x3D,0x15,0x15,0x3D,0x56,0x0C, - /* skull @ 34 */ 0x0E,0x11,0x6D,0x41,0x41,0x6D,0x11,0x0E, - /* boom @ 42 */ 0x00,0x00,0x08,0x1C,0x08,0x00,0x00,0x00, - 0x41,0x04,0x10,0x40,0x02,0x20,0x08,0x41, - /* octopus @ 58 */ 0x00,0x78,0x84,0x02,0x32,0x31,0x01,0x01,0x01,0x01,0x31,0x32,0x02,0x84,0x78,0x00, - 0x30,0x0C,0x82,0x71,0x0D,0x03,0xE0,0x1E,0x02,0x00,0x03,0x0D,0x31,0xC2,0x0C,0x30, - /* mother @ 90 */ 0xC0,0x20,0x10,0xDC,0xD2,0x12,0x1D,0xD1,0xD1,0x1D,0x12,0xD2,0xDC,0x10,0x20,0xC0, - 0x20,0x11,0x0A,0x06,0x02,0x02,0x02,0x3E,0x3E,0x02,0x02,0x02,0x06,0x0A,0x11,0x20, - /* heart @122 */ 0x06,0x0F,0x1E,0x0F,0x06, - /* missile @127 */ 0x1B,0x0E,0x04,0x0E,0x0E,0x0E,0x0E,0x04, -}; - -static const uint8_t POINTS[4] = {10u, 15u, 40u, 25u}; -static const uint8_t SPEED[4] = {1u, 2u, 1u, 1u}; /* pixels per tick */ - static const app_api_t *A; static enemy_t enemies[N_ENEMY]; static shot_t shots[N_SHOT], hostile[N_HOSTILE]; -static uint32_t randomState, score; +static uint32_t score; static uint16_t distance; static uint16_t frame; /* 16-bit so the slow star layers wrap without a jump */ static uint8_t mode, prevKey, kills; @@ -80,7 +59,6 @@ static int8_t bossY, bossDir; static uint8_t bossHp, bossMax; static bool running, paused, boss, saver; static char text[6]; -static char levelText[] = "LEVEL 1"; void *memset(void *dst, int value, size_t size) { @@ -89,10 +67,24 @@ void *memset(void *dst, int value, size_t size) return dst; } +/* Text from the assets in a shared buffer, valid until the next T() call. */ +static char tbuf[TEXT_MAX]; +static char *T(uint16_t off) +{ + A->asset_read(off, tbuf, TEXT_MAX); + return tbuf; +} + +static uint8_t asset_u8(uint16_t off) +{ + uint8_t v; + A->asset_read(off, &v, 1); + return v; +} + static uint32_t rnd(void) { - randomState = randomState * 1664525u + 1013904223u; - return randomState >> 8; + return A->rand32() >> 8; /* resident PRNG, seeded by the loader */ } static void pixel(int16_t x, int16_t y) @@ -102,9 +94,13 @@ static void pixel(int16_t x, int16_t y) *p |= (uint8_t)(1u << (y & 7)); } -/* Draw a column-major sprite (up to 8 rows high, LSB = top). */ -static void blit(int16_t x, int16_t y, const uint8_t *col, uint8_t w) +/* Draw a column-major sprite (up to 8 rows high, LSB = top, w <= 16) read + * from the assets at offset art. */ +static void blit(int16_t x, int16_t y, uint16_t art, uint8_t w) { + uint8_t cols[16]; + const uint8_t *col = cols; + A->asset_read(art, cols, w); for (; w; w--, x++) { uint8_t v = *col++; for (int16_t yy = y; v; v >>= 1, yy++) @@ -130,7 +126,8 @@ static int8_t aim(int16_t y) static void number(uint32_t value, uint8_t width) { - static const uint16_t place[5] = {10000u, 1000u, 100u, 10u, 1u}; + uint16_t place[5]; + A->asset_read(PLACE, place, sizeof(place)); const uint8_t first = (uint8_t)(5u - width); for (uint8_t i = 0; i < width; i++) { uint8_t digit = 0; @@ -164,7 +161,6 @@ static void boom(int16_t x, int8_t y, uint8_t delay) static void start_level(void) { - levelText[6] = (char)('0' + level); distance = 0; banner = 50u; spawnCd = 0; boss = false; } @@ -192,7 +188,7 @@ static void player_hit(void) static void kill_enemy(enemy_t *e) { e->state = BOOM; - score += POINTS[e->type]; + score += asset_u8(POINTS + e->type); if ((++kills & 15u) == 0u && missiles < 9u) missiles++; } @@ -244,7 +240,7 @@ static void update_enemies(void) const uint8_t t = e->type; int16_t y = e->y; e->phase++; - if (t != 2u || (frame & 1u)) e->x -= SPEED[t]; /* saucer: half speed */ + if (t != 2u || (frame & 1u)) e->x -= asset_u8(SPEED + t); /* saucer: half speed */ if (t == 0u) { const uint8_t p = (e->phase >> 1) & 31u; y = e->base - 8 + (int16_t)(p < 16u ? p : 31u - p); /* triangle wave */ @@ -342,8 +338,8 @@ static void tick(void) static void draw_hud(void) { - for (uint8_t i = 0; i < lives; i++) blit(1 + i * 6, 1, &ART[SPR_HEART], 5); - blit(33, 1, &ART[SPR_MISS], 8); + for (uint8_t i = 0; i < lives; i++) blit(1 + i * 6, 1, SPR_HEART, 5); + blit(33, 1, SPR_MISS, 8); number(missiles, 1); A->print_tiny(text, 43, 1, true, true); number(score > 99999u ? 99999u : score, 5); A->print_tiny(text, 107, 1, true, true); for (uint8_t x = 0; x < W; x += 2u) pixel(x, 8); @@ -396,20 +392,20 @@ static void draw(void) for (uint8_t i = 0; i < N_ENEMY; i++) { const enemy_t *e = &enemies[i]; if (e->state == 1u && !(e->type == 2u && e->hp < 3u && (frame & 2u))) /* hurt saucer blinks */ - blit(e->x, e->y, &ART[SPR_ENEMY + e->type * 8u], 8); + blit(e->x, e->y, SPR_ENEMY + e->type * 8u, 8); else if (e->state > 1u && e->state <= BOOM) - blit(e->x, e->y, &ART[SPR_BOOM + (e->state > 5u ? 0u : 8u)], 8); + blit(e->x, e->y, SPR_BOOM + (e->state > 5u ? 0u : 8u), 8); } if (boss) { - const uint8_t *spr = &ART[SPR_BOSS + ((level & 1u) ? 0u : 32u)]; /* alternate bosses */ + const uint16_t spr = SPR_BOSS + ((level & 1u) ? 0u : 32u); /* alternate bosses */ blit(bossX, bossY, spr, 16); - blit(bossX, bossY + 8, spr + 16, 16); + blit(bossX, bossY + 8, spr + 16u, 16); } - if (mode == ST_PLAY && !(invul & 2u)) blit(PX, py, &ART[SPR_SHIP], 10); + if (mode == ST_PLAY && !(invul & 2u)) blit(PX, py, SPR_SHIP, 10); for (uint8_t i = 0; i < N_SHOT; i++) { const shot_t *s = &shots[i]; - if (s->live == 2u) blit(s->x, s->y - 2, &ART[SPR_MISS], 8); + if (s->live == 2u) blit(s->x, s->y - 2, SPR_MISS, 8); else if (s->live) for (uint8_t k = 0; k < 4u; k++) pixel(s->x + k, s->y); } for (uint8_t i = 0; i < N_HOSTILE; i++) { @@ -421,14 +417,28 @@ static void draw(void) draw_hud(); if (mode == ST_OVER) { - A->print_bold("GAME OVER", 0, 127, 3); + A->print_bold(T(T_GAME_OVER), 0, 127, 3); } else if (paused) { - A->print_bold("PAUSE", 0, 127, 3); + A->print_bold(T(T_PAUSE), 0, 127, 3); } else if (banner > 10u) { - A->print_bold(levelText, 0, 127, 3); + char *t = T(T_LEVEL); + t[6] = (char)('0' + level); /* "LEVEL n" */ + A->print_bold(t, 0, 127, 3); } } +/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers + * every frame, the starfield scrolls over it and the prompt blinks. */ +static void draw_title(void) +{ + frame++; + A->asset_read(ART_TITLE, A->status_line, W); + A->asset_read(ART_TITLE + W, A->fb[0], 7u * W); + draw_stars(); + if (frame & 16u) + A->print_bold(T(T_PRESS), 0, 127, 6); +} + static void move_player(int8_t dy) { int16_t ny = py + dy; @@ -470,18 +480,18 @@ __attribute__((section(".text.entry"), used)) void app_main(const app_api_t *api) { A = api; - randomState = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq(); running = true; paused = false; prevKey = A->get_key(); A->led(false); A->backlight_on(); - new_game(); + mode = ST_TITLE; /* MENU starts a game, like after GAME OVER */ while (running) { poll_key(); if (!running) break; if (saver) { A->backlight_update(); A->delay_ms(TICK_MS); continue; } - if (!paused) tick(); - draw(); A->blit_status(); A->blit_full(); A->backlight_update(); + if (mode == ST_TITLE) draw_title(); + else { if (!paused) tick(); draw(); } + A->blit_status(); A->blit_full(); A->backlight_update(); A->delay_ms(TICK_MS); } diff --git a/App/apps/tetris/build.sh b/App/apps/tetris/build.sh index d543593f..2b48bd23 100755 --- a/App/apps/tetris/build.sh +++ b/App/apps/tetris/build.sh @@ -3,8 +3,8 @@ set -euo pipefail APP="$(basename "$PWD")" APP_NAME="Tetris" -APP_VER="1.0" -APP_API_MIN=1 +APP_VER="1.1" +APP_API_MIN=2 APP_VMA=${APP_VMA:-0x20000280} OUT="${APP_NAME// /}" @@ -22,14 +22,15 @@ LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \ rm -f ./*.app ./*.elf ./*.bin -step() { printf '\r ๐Ÿ”จ %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; } +step() { printf '\r ๐Ÿ”จ %-13s [%d/4] %-8s' "$APP_NAME" "$1" "$2"; } trap 'printf "\r โŒ %-13s build failed \n" "$APP_NAME"' ERR -step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" -step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" -step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ +step 1 assets ; python3 ./gen_assets.py "${APP}_assets.bin" "${APP}_assets.h" +step 2 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf" +step 3 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin" +step 4 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \ --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" \ - --vma "${APP_VMA}" --screensaver >/dev/null + --vma "${APP_VMA}" --screensaver --assets "${APP}_assets.bin" >/dev/null trap - ERR BYTES=$(wc -c < "${APP}.bin") diff --git a/App/apps/tetris/gen_assets.py b/App/apps/tetris/gen_assets.py new file mode 100755 index 00000000..0d6fe10b --- /dev/null +++ b/App/apps/tetris/gen_assets.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# Tetris read-only assets: texts, piece masks, scoring, decimal places, the +# rotation wall kicks and the 128x64 title screen. +# +# ./gen_assets.py tetris_assets.bin tetris_assets.h [preview.pgm] +import os, sys +sys.dont_write_bytecode = True +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +from app_assets import Assets +from app_art import Canvas + +MASKS = [0x00F0, 0x4444, 0x0F00, 0x2222, + 0x0066, 0x0066, 0x0066, 0x0066, + 0x0072, 0x0262, 0x0270, 0x0232, + 0x0036, 0x0462, 0x0360, 0x0231, + 0x0063, 0x0264, 0x0630, 0x0132, + 0x0071, 0x0226, 0x0470, 0x0322, + 0x0074, 0x0622, 0x0170, 0x0223] + +def title_cell(cv, x, y): + """Bevelled 4x4 block on a 5 px grid: larger than in game, to read at a glance.""" + for k in range(4): + cv.set(x + k, y); cv.set(x + k, y + 3); cv.set(x, y + k); cv.set(x + 3, y + k) + cv.rect(x + 1, y + 1, x + 2, y + 2) + +def title_screen(): + """Wide logo over a stack with a gap, and a T piece coming down into it.""" + cv = Canvas() + cv.word("TETRIS", 2, kx=1.45, ky=1.2) + floor, x0 = 55, 4 + stack = ["111111111111101111111111", # bottom row first, 5 px cells + "111110111111100111111101", + "001100011110000011100100"] + for r, row in enumerate(stack): + for c, cell in enumerate(row): + if cell == "1": + title_cell(cv, x0 + c * 5, floor - 4 - 5 * r) + t_down = 0x0027 # T pointing down, over the gap + for i in range(16): + if t_down >> i & 1: + title_cell(cv, x0 + (12 + (i & 3)) * 5, floor - 4 - 25 + (i >> 2) * 5) + return cv + +preview_path = sys.argv.pop(3) if len(sys.argv) == 4 else None +a = Assets("TETRIS") +a.text("T_TETRIS", "TETRIS") +a.text("T_NEXT", "NEXT") +a.text("T_NEW_BEST", "NEW BEST!") +a.text("T_GAME_OVER", "GAME OVER") +a.text("T_PAUSE", "PAUSE") +a.text("T_SCORE", "SCORE") +a.text("T_LINES", "LINES") +a.text("T_LEVEL", "LEVEL") +a.text("T_BEST", "BEST") +a.text("T_PRESS", "PRESS MENU") +# Four 4x4 masks (one per rotation) for I, O, T, S, Z, J and L. +a.u16("MASK", MASKS) +a.u16("LINE_POINTS", [0, 100, 300, 500, 800]) # by lines cleared +a.u32("DECIMAL_PLACE", [100000, 10000, 1000, 100, 10, 1]) +a.i8("KICK", [0, -1, 1, -2, 2]) # rotation x offsets +title = title_screen() +a.raw("ART_TITLE", title.pages()) +a.main() +if preview_path: + title.preview(preview_path) diff --git a/App/apps/tetris/tetris_app.c b/App/apps/tetris/tetris_app.c index f195700f..0e194ee6 100644 --- a/App/apps/tetris/tetris_app.c +++ b/App/apps/tetris/tetris_app.c @@ -31,6 +31,7 @@ #include #include #include "../app_api.h" +#include "tetris_assets.h" /* generated by gen_assets.py */ #define W 128u #define H 64u @@ -67,28 +68,18 @@ void *memset(void *dst, int value, size_t size) return dst; } -/* Four 4x4 masks for I, O, T, S, Z, J and L. */ -static const uint16_t MASK[7][4] = { - {0x00F0, 0x4444, 0x0F00, 0x2222}, - {0x0066, 0x0066, 0x0066, 0x0066}, - {0x0072, 0x0262, 0x0270, 0x0232}, - {0x0036, 0x0462, 0x0360, 0x0231}, - {0x0063, 0x0264, 0x0630, 0x0132}, - {0x0071, 0x0226, 0x0470, 0x0322}, - {0x0074, 0x0622, 0x0170, 0x0223}, -}; - -static const uint16_t LINE_POINTS[5] = {0, 100, 300, 500, 800}; -static const uint32_t DECIMAL_PLACE[6] = {100000u, 10000u, 1000u, 100u, 10u, 1u}; +/* Texts, piece masks, scoring, decimal places and wall kicks are read-only + * assets (gen_assets.py), fetched from external flash when needed. */ static uint16_t board[ROWS]; static uint8_t bag[7], bagPos; static uint8_t piece, nextPiece, rotation; static int8_t pieceX, pieceY; -static uint32_t randomState, score, best; +static uint32_t score, best; static uint16_t lines, gravityMs; static uint8_t level; -static bool paused, gameOver, running, saverPaused, saverActive, bestDirty, newBest; +static bool paused, gameOver, running, saverPaused, saverActive, bestDirty, newBest, title; +static uint8_t titleTick; static uint8_t previousKey; static uint16_t repeatMs; static char text[9]; @@ -100,10 +91,20 @@ static void new_game(void); /* RNG and the shuffled 7-piece bag */ /* -------------------------------------------------------------------------- */ -static uint32_t random_next(void) +/* Text from the assets in a shared buffer, valid until the next T() call. */ +static char tbuf[TEXT_MAX]; +static char *T(uint16_t off) { - randomState = randomState * 1664525u + 1013904223u; - return randomState; + A->asset_read(off, tbuf, TEXT_MAX); + return tbuf; +} + +/* 4x4 occupancy mask of a piece in a rotation (bit i = cell (i & 3, i >> 2)). */ +static uint16_t mask_of(uint8_t type, uint8_t rot) +{ + uint16_t mask; + A->asset_read(MASK + (type * 4u + (rot & 3u)) * 2u, &mask, sizeof(mask)); + return mask; } static void refill_bag(void) @@ -113,7 +114,7 @@ static void refill_bag(void) for (uint8_t i = 6u; i > 0u; i--) { uint8_t j; do { - j = (uint8_t)(random_next() >> 29); + j = (uint8_t)(A->rand32() >> 29); /* resident PRNG, seeded by the loader */ } while (j > i); const uint8_t t = bag[i]; bag[i] = bag[j]; bag[j] = t; } @@ -190,7 +191,7 @@ static void cell(int gx, int gy, uint8_t style) static bool collision(uint8_t type, uint8_t rot, int8_t px, int8_t py) { - const uint16_t mask = MASK[type][rot & 3u]; + const uint16_t mask = mask_of(type, rot); for (uint8_t i = 0; i < 16u; i++) { if (!(mask & (uint16_t)(1u << i))) continue; @@ -206,7 +207,7 @@ static bool collision(uint8_t type, uint8_t rot, int8_t px, int8_t py) static void draw_piece(uint8_t type, uint8_t rot, int8_t px, int8_t py, uint8_t style) { - const uint16_t mask = MASK[type][rot & 3u]; + const uint16_t mask = mask_of(type, rot); for (uint8_t i = 0; i < 16u; i++) if (mask & (uint16_t)(1u << i)) cell(px + (i & 3u), py + (i >> 2), style); @@ -216,7 +217,8 @@ static void number(uint32_t value, uint8_t width) { const uint8_t first = (uint8_t)(6u - width); for (uint8_t i = 0; i < width; i++) { - const uint32_t place = DECIMAL_PLACE[first + i]; + uint32_t place; + A->asset_read(DECIMAL_PLACE + (first + i) * 4u, &place, sizeof(place)); uint8_t digit = 0; while (value >= place) { value -= place; @@ -229,7 +231,7 @@ static void number(uint32_t value, uint8_t width) static void preview(void) { - const uint16_t mask = MASK[nextPiece][0]; + const uint16_t mask = mask_of(nextPiece, 0); const int ox = nextPiece < 2u ? PREVIEW_CX - 7 : PREVIEW_CX - 5; const int oy = nextPiece == 0u ? PREVIEW_CY - 5 : PREVIEW_CY - 3; for (uint8_t i = 0; i < 16u; i++) { @@ -255,16 +257,16 @@ static void stat(const char *label, uint8_t y, uint32_t value, static void panel(void) { - A->print_tiny("NEXT", LABEL_X, 2, false, true); + A->print_tiny(T(T_NEXT), LABEL_X, 2, false, true); preview(); if (gameOver) - A->print_inverse(newBest ? "NEW BEST!" : "GAME OVER", 82, 2, false, true, 118); + A->print_inverse(T(newBest ? T_NEW_BEST : T_GAME_OVER), 82, 2, false, true, 118); else if (paused) - A->print_inverse("PAUSE", 90, 2, false, true, 110); - stat("SCORE", 24, score > 999999u ? 999999u : score, 101, 6); - stat("LINES", 32, lines > 999u ? 999u : lines, 113, 3); - stat("LEVEL", 40, level, 117, 2); - stat("BEST", 48, best > 999999u ? 999999u : best, 101, 6); + A->print_inverse(T(T_PAUSE), 90, 2, false, true, 110); + stat(T(T_SCORE), 24, score > 999999u ? 999999u : score, 101, 6); + stat(T(T_LINES), 32, lines > 999u ? 999u : lines, 113, 3); + stat(T(T_LEVEL), 40, level, 117, 2); + stat(T(T_BEST), 48, best > 999999u ? 999999u : best, 101, 6); } /* effect 0 is normal, 1..8 is the line-clear sweep, 0xFF flashes a lock. */ @@ -272,7 +274,7 @@ static void render(bool showPiece, uint32_t clearRows, uint8_t effect) { A->display_clear(); A->status_clear(); - A->print_inverse("TETRIS", 74, 0, true, true, 101); + A->print_inverse(T(T_TETRIS), 74, 0, true, true, 101); container(); @@ -357,7 +359,7 @@ static void lock_piece(void) render(true, 0, 0xFFu); A->blit_status(); A->blit_full(); A->delay_ms(45); - const uint16_t mask = MASK[piece][rotation]; + const uint16_t mask = mask_of(piece, rotation); bool above = false; for (uint8_t i = 0; i < 16u; i++) { if (!(mask & (uint16_t)(1u << i))) @@ -377,7 +379,9 @@ static void lock_piece(void) lines = (uint16_t)(lines + cleared); while (level < 15u && lines >= (uint16_t)level * 10u) level++; - score += (uint32_t)LINE_POINTS[cleared] * level; + uint16_t points; + A->asset_read(LINE_POINTS + cleared * 2u, &points, sizeof(points)); + score += (uint32_t)points * level; update_best(); spawn_piece(); } @@ -396,7 +400,8 @@ static bool move_down(bool manual) static void rotate_piece(void) { const uint8_t next = (uint8_t)((rotation + 1u) & 3u); - static const int8_t kick[5] = {0, -1, 1, -2, 2}; + int8_t kick[KICK_LEN]; + A->asset_read(KICK, kick, sizeof(kick)); for (uint8_t i = 0; i < 5u; i++) if (!collision(piece, next, pieceX + kick[i], pieceY)) { pieceX += kick[i]; rotation = next; return; @@ -421,9 +426,27 @@ static void hard_drop(void) /* Input, screensaver handling and the main loop */ /* -------------------------------------------------------------------------- */ +/* Title: the 1 KiB picture is streamed from the assets into the LCD buffers, + * with a blinking prompt on the blank bottom page. */ +static void draw_title(void) +{ + titleTick++; + A->asset_read(ART_TITLE, A->status_line, W); + A->asset_read(ART_TITLE + W, A->fb[0], 7u * W); + if (titleTick & 32u) + A->print_bold(T(T_PRESS), 0, W - 1, 6); +} + static void key_action(uint8_t key, bool repeat) { if (key == APP_KEY_EXIT) { running = false; return; } + if (title) { /* the game-over restart keys also leave the title */ + if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0)) { + title = false; + new_game(); + } + return; + } if (gameOver) { if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0)) new_game(); @@ -509,14 +532,13 @@ void app_main(const app_api_t *api) best = cfg.magic == CFG_MAGIC && cfg.version == 1u ? cfg.best : 0u; bestDirty = false; - randomState = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq() ^ best; - if (!randomState) randomState = 1; previousKey = APP_KEY_INVALID; repeatMs = 0; running = true; A->led(false); A->backlight_on(); new_game(); + title = true; while (running) { poll_key(); @@ -528,7 +550,7 @@ void app_main(const app_api_t *api) continue; } - if (!paused && !gameOver) { + if (!title && !paused && !gameOver) { const uint16_t interval = level >= 15u ? 100u : (uint16_t)(900u - (level - 1u) * 55u); gravityMs = (uint16_t)(gravityMs + TICK_MS); if (gravityMs >= interval) { @@ -537,7 +559,8 @@ void app_main(const app_api_t *api) } } - render(true, 0, 0); + if (title) draw_title(); + else render(true, 0, 0); A->blit_status(); A->blit_full(); A->backlight_update(); diff --git a/App/scheduler.c b/App/scheduler.c index 3401e5ad..fbf8c714 100644 --- a/App/scheduler.c +++ b/App/scheduler.c @@ -44,6 +44,13 @@ static volatile uint32_t gGlobalSysTickCounter; +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS +uint32_t SCHEDULER_GetTick10ms(void) +{ + return gGlobalSysTickCounter; +} +#endif + // we come here every 10ms void SysTick_Handler(void) { diff --git a/App/scheduler.h b/App/scheduler.h index aee615f6..759dd08b 100644 --- a/App/scheduler.h +++ b/App/scheduler.h @@ -28,4 +28,9 @@ static void inline SCHEDULER_Disable() NVIC_DisableIRQ(SysTick_IRQn); } +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS +/* Free-running 10 ms SysTick counter (wraps after ~497 days). */ +uint32_t SCHEDULER_GetTick10ms(void); +#endif + #endif diff --git a/compile-app.sh b/compile-app.sh index c89bcfef..79fb5eb6 100755 --- a/compile-app.sh +++ b/compile-app.sh @@ -66,7 +66,8 @@ printf ' VMA %s ยท budget %d B / %d.00 KiB ยท out %s/\n\n' \ files=(); sizes=(); vmas=(); states=() fail=0 -read_u32le() { od -An -tx1 -j"$2" -N4 "$1" | awk '{printf "0x%s%s%s%s",$4,$3,$2,$1}'; } +read_u32le() { od -An -tx1 -j"$2" -N4 "$1" | awk 'NF{printf "0x%s%s%s%s",$4,$3,$2,$1}'; } +read_u16le() { od -An -tx1 -j"$2" -N2 "$1" | awk 'NF{printf "0x%s%s",$2,$1}'; } for app in "${TARGETS[@]}"; do if docker run --rm ${TTY_ARG:+"$TTY_ARG"} -u "$(id -u):$(id -g)" \ @@ -77,9 +78,11 @@ for app in "${TARGETS[@]}"; do if [ -n "$appfile" ] && [ -f "$appfile" ]; then cp -f "$appfile" "$OUT_DIR/" base=$(basename "$appfile") - code=$(( $(wc -c < "$appfile") - 64 )) + code=$(( $(read_u32le "$appfile" 8) )) # app_header_t.code_size + assets=$(( $(read_u16le "$appfile" 60) )) # app_header_t.asset_size vma=$(read_u32le "$appfile" 52) state="โœ… OK"; [ "$code" -gt "$OVERLAY_MAX" ] && { state="๐Ÿšจ OVERFLOW"; fail=1; } + [ "$assets" -gt 0 ] && state="$state (+$assets B assets)" else base="$app.app"; code=-1; vma="--"; state="โŒ NO BLOB"; fail=1 fi