mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-06 13:07:30 +00:00
326 lines
13 KiB
C
326 lines
13 KiB
C
/* Copyright 2026 Armel F4HWN
|
|
* https://github.com/armel
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/*
|
|
* Plasma — overlay app. Full-screen demoscene effects on the 1-bit 128x64 LCD,
|
|
* all drawn from one scalar field per pixel:
|
|
* - scrolled sines: horizontal (x), vertical (y) and diagonal (x+y);
|
|
* - up to three radial centres drifting on Lissajous paths, each adding
|
|
* either a sine ripple of the squared distance (plasma rings, Fresnel zone
|
|
* plates whose interference gives moire) or a 1/r^2 falloff (metaballs).
|
|
* Because (x-cx)^2 + (y-cy)^2 is separable, every term comes from per-column
|
|
* and per-row tables, so each of the 8192 pixels costs only a handful of adds.
|
|
* The field is shown as sweeping bands, an ordered 4x4 Bayer stipple or iso
|
|
* contours, optionally inverted. A title pill in the top-left corner spells
|
|
* out each change for a moment ("SCENE 8/9: BLOBS", "SPEED 3/8", ...). Pure compute + framebuffer, no radio. The tables live
|
|
* on app_main's stack (~2 KiB kept out of the 4 KiB overlay) and no division
|
|
* is linked.
|
|
*
|
|
* Keys: 1-9 scene · UP/DOWN speed · STAR bands/stipple/contour · 0 invert ·
|
|
* F auto-cycle · MENU pause · EXIT quit. Scene, look and speed persist.
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include "../app_api.h"
|
|
#include "plasma_assets.h" /* generated by gen_assets.py */
|
|
|
|
#define W 128
|
|
#define H 64
|
|
#define NCENTRE 3u
|
|
#define SPEED_MAX 8u
|
|
#define AUTO_FRAMES 400u /* frames per scene in auto-cycle */
|
|
#define HUD_MS 1500u /* HUD display time after a change */
|
|
#define CFG_MAGIC 0xA7u
|
|
|
|
enum { MODE_BANDS, MODE_DITHER, MODE_CONTOUR };
|
|
enum { FLAG_INV = 1u, FLAG_AUTO = 2u };
|
|
/* Title kinds, in T_LABEL order; INVERT/AUTO follow the FLAG_* bit order. */
|
|
enum { HUD_SCENE, HUD_MODE, HUD_SPEED, HUD_INVERT, HUD_AUTO, HUD_PAUSE };
|
|
|
|
typedef struct {
|
|
uint8_t magic, scene, mode, speed, flags;
|
|
} config_t;
|
|
|
|
/* Centre c follows phase = tc * K + O on each axis, with its own multipliers
|
|
* and offsets so the metaballs never move in lockstep. Centre 0 keeps the
|
|
* original plasma drift (x at 1 step, y at 2 steps per frame). */
|
|
static const uint8_t centre_kx[NCENTRE] = { 1, 2, 1 };
|
|
static const uint8_t centre_ky[NCENTRE] = { 2, 1, 3 };
|
|
static const uint8_t centre_ox[NCENTRE] = { 0, 85, 170 };
|
|
static const uint8_t centre_oy[NCENTRE] = { 0, 40, 200 };
|
|
|
|
/* All the state in one struct: one base address for every access. */
|
|
struct globals {
|
|
const app_api_t *api;
|
|
uint32_t hud_until; /* ticks_ms() deadline of the HUD */
|
|
uint8_t scene, mode, speed, flags;
|
|
uint8_t hud; /* HUD_* kind of the last change */
|
|
bool paused;
|
|
};
|
|
static struct globals g;
|
|
#define A (g.api)
|
|
|
|
static void show_hud(uint8_t kind)
|
|
{
|
|
g.hud = kind;
|
|
g.hud_until = A->ticks_ms() + HUD_MS;
|
|
}
|
|
|
|
/* Append one asset string (without its NUL). */
|
|
static char *put_text(char *d, uint16_t offset)
|
|
{
|
|
char w[TEXT_MAX];
|
|
A->asset_read(offset, w, sizeof(w));
|
|
for (const char *s = w; *s; s++)
|
|
*d++ = *s;
|
|
return d;
|
|
}
|
|
|
|
/* Title pill in the top-left corner of the status line, as in the other apps:
|
|
* the last change spelled out for HUD_MS, then "PAUSE" while paused.
|
|
* Longest: "SCENE 8/9: CLASSIC", 18 chars, 72 px. */
|
|
static void draw_hud(void)
|
|
{
|
|
uint8_t kind = g.hud;
|
|
if ((int32_t)(g.hud_until - A->ticks_ms()) <= 0) {
|
|
if (!g.paused)
|
|
return;
|
|
kind = HUD_PAUSE;
|
|
}
|
|
|
|
char s[24];
|
|
char *d = put_text(s, T_LABEL + kind * T_LABEL_STRIDE);
|
|
switch (kind) {
|
|
case HUD_SCENE: /* SCENE 8/9: BLOBS */
|
|
*d++ = (char)('1' + g.scene);
|
|
*d++ = '/';
|
|
*d++ = (char)('0' + NSCENE);
|
|
*d++ = ':';
|
|
*d++ = ' ';
|
|
d = put_text(d, T_SCENE + g.scene * T_SCENE_STRIDE);
|
|
break;
|
|
case HUD_MODE: /* RENDER: DITHER */
|
|
d = put_text(d, T_MODE + g.mode * T_MODE_STRIDE);
|
|
break;
|
|
case HUD_SPEED: /* SPEED 3/8 */
|
|
*d++ = (char)('0' + g.speed);
|
|
*d++ = '/';
|
|
*d++ = (char)('0' + SPEED_MAX);
|
|
break;
|
|
case HUD_INVERT: /* INVERT: ON */
|
|
case HUD_AUTO: /* AUTO CYCLE: OFF */
|
|
d = put_text(d, T_ONOFF + ((g.flags >> (kind - HUD_INVERT)) & 1u) * T_ONOFF_STRIDE);
|
|
break;
|
|
default: /* PAUSE */
|
|
break;
|
|
}
|
|
*d = '\0';
|
|
|
|
const uint8_t end = (uint8_t)(2u + (d - s) * 4); /* text starts at x = 2 */
|
|
for (uint8_t i = 0; i <= end + 1u; i++)
|
|
A->status_line[i] = 0; /* blank the pill area plus a 1 px margin on its right */
|
|
A->print_inverse(s, 2, 0, true, true, end);
|
|
}
|
|
|
|
__attribute__((section(".text.entry"), used))
|
|
void app_main(const app_api_t *api)
|
|
{
|
|
A = api;
|
|
A->backlight_on();
|
|
A->status_clear();
|
|
|
|
config_t cfg;
|
|
A->cfg_load((uint8_t *)&cfg, sizeof(cfg));
|
|
if (cfg.magic == CFG_MAGIC && cfg.scene < NSCENE && cfg.mode < NMODE &&
|
|
cfg.speed - 1u < SPEED_MAX) {
|
|
g.scene = cfg.scene;
|
|
g.mode = cfg.mode;
|
|
g.speed = cfg.speed;
|
|
g.flags = cfg.flags & (FLAG_INV | FLAG_AUTO);
|
|
} else {
|
|
g.speed = 3u; /* scene 0, bands, not inverted */
|
|
}
|
|
show_hud(HUD_SCENE);
|
|
|
|
/* Read-only tables, copied from the assets onto the stack. */
|
|
int8_t sine[SIN_LEN]; /* 32 * sin(2*pi*i/256) */
|
|
int8_t fall[FALL_LEN]; /* metaball falloff, by d^2 >> shift */
|
|
uint8_t bayer[BAYER_LEN]; /* idx = (y&3)*4 + (x&3), values 0..15 */
|
|
uint8_t scenes[NSCENE][SCENE_REC]; /* see gen_assets.py SCENES */
|
|
A->asset_read(SIN, sine, sizeof(sine));
|
|
A->asset_read(FALL, fall, sizeof(fall));
|
|
A->asset_read(BAYER, bayer, sizeof(bayer));
|
|
A->asset_read(SCENE, scenes, sizeof(scenes));
|
|
|
|
/* Per-frame separable tables. */
|
|
int8_t colA[W]; /* horizontal sine, by column x */
|
|
int8_t rowA[H]; /* vertical sine, by row y */
|
|
int8_t diagA[W + H]; /* diagonal sine, by (x + y) */
|
|
uint16_t sqx[NCENTRE][W]; /* (x - cx)^2 per radial centre */
|
|
uint16_t sqy[NCENTRE][H]; /* (y - cy)^2 */
|
|
uint8_t prevLvl[H]; /* previous column, contour mode */
|
|
|
|
uint8_t t1 = 0, t2 = 0, t3 = 0, tr = 0, tc = 0;
|
|
uint16_t autoCtr = 0;
|
|
bool running = true;
|
|
uint8_t prevKey = APP_KEY_INVALID;
|
|
|
|
while (running) {
|
|
uint8_t key = A->get_key();
|
|
if (key == APP_KEY_SAVER) {
|
|
prevKey = APP_KEY_INVALID;
|
|
A->delay_ms(10);
|
|
A->backlight_update();
|
|
continue;
|
|
}
|
|
if (key == APP_KEY_WAKE)
|
|
key = APP_KEY_INVALID;
|
|
if (key != prevKey && key != APP_KEY_INVALID) {
|
|
A->backlight_on();
|
|
switch (key) {
|
|
case APP_KEY_EXIT: running = false; break;
|
|
case APP_KEY_UP:
|
|
case APP_KEY_DOWN: {
|
|
const int8_t direction = A->nav_dir(key);
|
|
if (direction > 0 && g.speed < SPEED_MAX) g.speed++;
|
|
if (direction < 0 && g.speed > 1u) g.speed--;
|
|
show_hud(HUD_SPEED);
|
|
break;
|
|
}
|
|
case APP_KEY_MENU:
|
|
g.paused = !g.paused;
|
|
show_hud(g.paused ? HUD_PAUSE : HUD_SCENE);
|
|
break;
|
|
case APP_KEY_STAR:
|
|
if (++g.mode >= NMODE) g.mode = 0;
|
|
show_hud(HUD_MODE);
|
|
break;
|
|
case APP_KEY_0:
|
|
g.flags ^= FLAG_INV;
|
|
show_hud(HUD_INVERT);
|
|
break;
|
|
case APP_KEY_F:
|
|
g.flags ^= FLAG_AUTO;
|
|
autoCtr = 0;
|
|
show_hud(HUD_AUTO);
|
|
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: case APP_KEY_9:
|
|
g.scene = (uint8_t)(key - APP_KEY_1);
|
|
g.flags &= (uint8_t)~FLAG_AUTO;
|
|
show_hud(HUD_SCENE);
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
prevKey = key;
|
|
if (!running)
|
|
break;
|
|
|
|
const uint8_t *sc = scenes[g.scene];
|
|
const bool blob = sc[0] != 0u;
|
|
const uint8_t n = sc[1];
|
|
const uint8_t sx = sc[2], sy = sc[3], sd = sc[4];
|
|
const uint8_t rsh = sc[5];
|
|
const int gain = 1 << sc[6];
|
|
const uint8_t mode = g.mode;
|
|
const uint8_t inv = (g.flags & FLAG_INV) ? 0xFFu : 0u;
|
|
|
|
/* Drifting radial centres, then their separable squares. */
|
|
for (uint8_t c = 0; c < n; c++) {
|
|
const int cx = 64 + sine[(uint8_t)(tc * centre_kx[c] + centre_ox[c])]; /* 32..96 */
|
|
const int cy = 32 + (sine[(uint8_t)(tc * centre_ky[c] + centre_oy[c])] >> 1); /* 16..48 */
|
|
for (uint8_t x = 0; x < W; x++) {
|
|
const int dx = (int)x - cx;
|
|
sqx[c][x] = (uint16_t)(dx * dx);
|
|
}
|
|
for (uint8_t y = 0; y < H; y++) {
|
|
const int dy = (int)y - cy;
|
|
sqy[c][y] = (uint16_t)(dy * dy);
|
|
}
|
|
}
|
|
/* Linear sine terms; a zero scale drops the term. */
|
|
for (uint8_t x = 0; x < W; x++)
|
|
colA[x] = sx ? sine[(uint8_t)(x * sx + t1)] : 0;
|
|
for (uint8_t y = 0; y < H; y++)
|
|
rowA[y] = sy ? sine[(uint8_t)(y * sy + t2)] : 0;
|
|
for (uint8_t s = 0; s < W + H; s++)
|
|
diagA[s] = sd ? sine[(uint8_t)(s * sd + t3)] : 0;
|
|
|
|
/* Render all 8 pages: page 0 -> status line (top), pages 1..7 -> fb[0..6]. */
|
|
for (uint8_t x = 0; x < W; x++) {
|
|
const int8_t cxv = colA[x];
|
|
const uint8_t bx = x & 3u;
|
|
uint8_t up = 0; /* level of the pixel above */
|
|
for (uint8_t p = 0; p < 8u; p++) {
|
|
uint8_t byte = 0;
|
|
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];
|
|
for (uint8_t c = 0; c < n; c++) {
|
|
const uint16_t d = (uint16_t)((sqx[c][x] + sqy[c][y]) >> rsh);
|
|
v += blob ? fall[d > 255u ? 255u : d]
|
|
: sine[(uint8_t)(d + tr)];
|
|
}
|
|
v = v * gain + 128;
|
|
if (v < 0) v = 0;
|
|
if (v > 255) v = 255;
|
|
const uint8_t lvl = (uint8_t)v >> 4; /* 0..15 */
|
|
|
|
bool on;
|
|
if (mode == MODE_BANDS) {
|
|
on = (lvl & 1u) != 0u; /* sweeping stripes */
|
|
} else if (mode == MODE_DITHER) {
|
|
on = lvl > bayer[((y & 3u) << 2) | bx]; /* 4x4 stipple */
|
|
} else { /* iso contours */
|
|
on = (x && prevLvl[y] != lvl) || (y && up != lvl);
|
|
prevLvl[y] = lvl;
|
|
up = lvl;
|
|
}
|
|
if (on) byte = (uint8_t)(byte | (1u << b));
|
|
}
|
|
byte ^= inv;
|
|
if (p == 0) A->status_line[x] = byte;
|
|
else A->fb[p - 1][x] = byte;
|
|
}
|
|
}
|
|
draw_hud();
|
|
A->blit_status();
|
|
A->blit_full();
|
|
|
|
if (!g.paused) {
|
|
t1 += g.speed; t2 += (uint8_t)(g.speed + 1u); t3 += 1u;
|
|
tr += g.speed; tc += 1u;
|
|
if ((g.flags & FLAG_AUTO) && ++autoCtr >= AUTO_FRAMES) {
|
|
autoCtr = 0;
|
|
if (++g.scene >= NSCENE) g.scene = 0; /* wrap without a modulo */
|
|
show_hud(HUD_SCENE);
|
|
}
|
|
}
|
|
|
|
A->backlight_update();
|
|
}
|
|
|
|
cfg.magic = CFG_MAGIC;
|
|
cfg.scene = g.scene;
|
|
cfg.mode = g.mode;
|
|
cfg.speed = g.speed;
|
|
cfg.flags = g.flags;
|
|
A->cfg_save((const uint8_t *)&cfg, sizeof(cfg));
|
|
}
|