Frame rate: full-width PSRAM draw band (6 bands a frame instead of 14 on the T-Deck), the still aurora drawn once and copied, LVGL at -O2 with its blend and label loops in IRAM (scripts/build/perf_lvgl.py, 1 KB more loop stack), circle and gradient caches, newlib memcpy. Rolled out to the M9, Attaky, RAK, Wio, CrowPanel and V4-R8; the Pager gets the band only, the V4 the caches only. Frame benchmark tour (DOC_PERF_BENCH) for measuring it. Fixes: hop counts read from the packed path length (#621); Arabic shaping rows patched in LVGL (patch_lvgl_arabic.py), Arabic 101 shift layer, kaf, symbols, text sent as standard letters (#582); info popup colour runs that never wrap (#602); keypad nav reaches the chat header (#623); the browser opens the changelog (#622, gen-changelog.py at every site deploy); the Theme background colour picker no longer edits the accent; flasher entries for P4 units with factory ESP-AT Wi-Fi chips (#612, #619). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
212 lines
8.0 KiB
Python
212 lines
8.0 KiB
Python
# SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
||
# LVGL 8.4 shapes Arabic through the ap_chars_map[] table in src/misc/lv_txt_ap.c, and
|
||
# a few of its rows are wrong (LVGL fixed most of them only in v9; #582, reported by a
|
||
# native speaker on the T-LoRa Pager). A row is {offset from U+0622, final form, then the
|
||
# initial, medial and isolated forms as offsets from the final one, {joins the previous
|
||
# letter, joins the next}}. Lookup also matches every presentation form a row produces,
|
||
# so one bad row can swallow other characters:
|
||
#
|
||
# - آ (U+0622) had no row: it never joined the letter before it (بآ), and لآ never
|
||
# became its ligature (U+FEF5), because the lam-alef check finds the alef by its row.
|
||
# Its forms are final U+FE82 and isolated U+FE81, like the other alef rows.
|
||
# - ئ (U+0626) said it does not join the next letter, so شيئا, رئيس, هيئة broke apart
|
||
# after it (LVGL 9 #6430).
|
||
# - ة (U+0629) had the initial and medial offsets of a joining letter, which land on
|
||
# ت (U+FE95, U+FE96). It never joins the next letter, so it gets isolated and final
|
||
# forms only, as the other non-joining rows do (LVGL 9 still has the medial one wrong).
|
||
# - ۰ (U+06F0) had +1/+2/-1 forms: lookup then took ۱ and ۲ for forms of ۰, and all
|
||
# three were drawn as U+06EF (LVGL 9 #6430, #10718).
|
||
# - The alef row (U+0627) was labelled آ; only the comment changes.
|
||
#
|
||
# Idempotent and fail-closed, like patch_lvgl_anim_uaf.py, and installed the same way: a
|
||
# PlatformIO pre-script with a pre-link check, and --patch-file for the IDF builds
|
||
# (Tanmatsu, T-Display P4), which vendor LVGL through their fetch-deps.sh.
|
||
|
||
import os
|
||
import sys
|
||
|
||
MARKER = "wadamesh-lvgl-arabic-patch"
|
||
EDITS = (
|
||
(
|
||
" /*{Key Offset, End, Beginning, Middle, Isolated, {conjunction}}*/\n"
|
||
" {1, 0xFE84, -1, 0, -1, {1, 0}}, // أ\n",
|
||
" /*{Key Offset, End, Beginning, Middle, Isolated, {conjunction}}*/\n"
|
||
" {0, 0xFE82, -1, 0, -1, {1, 0}}, // آ (wadamesh-lvgl-arabic-patch: LVGL 8 had no row)\n"
|
||
" {1, 0xFE84, -1, 0, -1, {1, 0}}, // أ\n",
|
||
),
|
||
(
|
||
" {4, 0xFE8A, 1, 2, -1, {1, 0}}, // ئ\n",
|
||
" {4, 0xFE8A, 1, 2, -1, {1, 1}}, // ئ (joins the next letter)\n",
|
||
),
|
||
(
|
||
" {5, 0xFE8E, -1, 0, -1, {1, 0}}, // آ\n",
|
||
" {5, 0xFE8E, -1, 0, -1, {1, 0}}, // ا\n",
|
||
),
|
||
(
|
||
" {7, 0xFE94, 1, 2, -1, {1, 0}}, // ة\n",
|
||
" {7, 0xFE94, -1, 0, -1, {1, 0}}, // ة (never joins the next letter)\n",
|
||
),
|
||
(
|
||
" {206, 0x06F0, 1, 2, -1, {0, 0}}, // ۰\n",
|
||
" {206, 0x06F0, 0, 0, 0, {0, 0}}, // ۰ (its own form only)\n",
|
||
),
|
||
)
|
||
REQUIRED = ("const ap_chars_map_t ap_chars_map[] = {", " if(ch_code == 0x0622) {")
|
||
|
||
|
||
def patch_source(source):
|
||
if any(source.count(line) != 1 for line in REQUIRED):
|
||
raise RuntimeError("lv_txt_ap.c does not match LVGL 8.4 (LVGL version drift?)")
|
||
olds = [source.count(old) for old, _ in EDITS]
|
||
news = [source.count(new) for _, new in EDITS]
|
||
marker = source.count(MARKER)
|
||
if all(n == 1 for n in news) and all(o == 0 for o in olds) and marker == 1:
|
||
return source, False
|
||
if not (all(o == 1 for o in olds) and all(n == 0 for n in news) and marker == 0):
|
||
raise RuntimeError("lv_txt_ap.c ap_chars_map does not match LVGL 8.4 (LVGL version drift?)")
|
||
patched = source
|
||
for old, new in EDITS:
|
||
patched = patched.replace(old, new, 1)
|
||
if (any(patched.count(new) != 1 for _, new in EDITS)
|
||
or any(patched.count(old) != 0 for old, _ in EDITS)
|
||
or patched.count(MARKER) != 1):
|
||
raise RuntimeError("lv_txt_ap.c patch verification failed")
|
||
return patched, True
|
||
|
||
|
||
def patch_file(path):
|
||
with open(path, encoding="utf-8") as source_file:
|
||
source = source_file.read()
|
||
patched, changed = patch_source(source)
|
||
if changed:
|
||
with open(path, "w", encoding="utf-8") as source_file:
|
||
source_file.write(patched)
|
||
return changed
|
||
|
||
|
||
def verify_source(source):
|
||
_, changed = patch_source(source)
|
||
if changed:
|
||
raise RuntimeError("lv_txt_ap.c still has LVGL 8's Arabic shaping rows")
|
||
|
||
|
||
def verify_file(path):
|
||
with open(path, encoding="utf-8") as source_file:
|
||
verify_source(source_file.read())
|
||
|
||
|
||
def self_test():
|
||
fixture = (REQUIRED[0] + "\n" + "".join(old for old, _ in EDITS)
|
||
+ " LV_AP_END_CHARS_LIST\n};\n" + REQUIRED[1] + "\n")
|
||
patched, changed = patch_source(fixture)
|
||
assert changed
|
||
assert MARKER in patched
|
||
assert "{0, 0xFE82, -1, 0, -1, {1, 0}}" in patched
|
||
assert "{4, 0xFE8A, 1, 2, -1, {1, 1}}" in patched
|
||
assert "{7, 0xFE94, -1, 0, -1, {1, 0}}" in patched
|
||
assert "{206, 0x06F0, 0, 0, 0, {0, 0}}" in patched
|
||
|
||
same, changed = patch_source(patched)
|
||
assert not changed
|
||
assert same == patched
|
||
verify_source(patched)
|
||
|
||
try:
|
||
verify_source(fixture)
|
||
except RuntimeError:
|
||
pass
|
||
else:
|
||
raise AssertionError("verification must reject an unpatched source")
|
||
|
||
for invalid in ("static void nothing(void) {}\n",
|
||
fixture.replace(EDITS[1][0], ""),
|
||
patched + EDITS[0][0],
|
||
"/* " + MARKER + " */\n" + fixture):
|
||
try:
|
||
patch_source(invalid)
|
||
except RuntimeError:
|
||
pass
|
||
else:
|
||
raise AssertionError("drift or a half-patched source must fail closed")
|
||
|
||
print("patch_lvgl_arabic self-test passed")
|
||
|
||
|
||
def install(platformio_env):
|
||
path = os.path.join(
|
||
platformio_env.subst("$PROJECT_LIBDEPS_DIR"),
|
||
platformio_env.subst("$PIOENV"),
|
||
"lvgl",
|
||
"src",
|
||
"misc",
|
||
"lv_txt_ap.c",
|
||
)
|
||
|
||
def apply_or_error():
|
||
if not os.path.isfile(path):
|
||
return "LVGL patch target is missing: %s" % path
|
||
try:
|
||
changed = patch_file(path)
|
||
except (OSError, RuntimeError) as error:
|
||
return str(error)
|
||
print("[patch_lvgl_arabic] %s" % ("patched lv_txt_ap.c" if changed else "already patched"))
|
||
return None
|
||
|
||
if os.path.isfile(path):
|
||
error = apply_or_error()
|
||
if error is not None:
|
||
print("[patch_lvgl_arabic] ERROR: %s" % error)
|
||
platformio_env.Exit(1)
|
||
else:
|
||
print("[patch_lvgl_arabic] LVGL not fetched yet - patch deferred to pre-link check")
|
||
|
||
def verify_patched(target, source, env):
|
||
del target, source
|
||
if os.path.isfile(path):
|
||
try:
|
||
verify_file(path)
|
||
return 0
|
||
except (OSError, RuntimeError):
|
||
pass
|
||
error = apply_or_error()
|
||
if error is not None:
|
||
print("[patch_lvgl_arabic] ERROR: %s" % error)
|
||
return 1
|
||
print("[patch_lvgl_arabic] ERROR: LVGL was patched after compilation")
|
||
print("[patch_lvgl_arabic] ERROR: re-run `pio run` to compile the fix")
|
||
return 1
|
||
|
||
platformio_env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", verify_patched)
|
||
|
||
|
||
try:
|
||
Import("env") # noqa: F821 - provided by PlatformIO/SCons
|
||
except NameError:
|
||
env = None
|
||
|
||
if env is not None:
|
||
install(env)
|
||
|
||
if env is None and __name__ == "__main__":
|
||
if len(sys.argv) == 2 and sys.argv[1] == "--self-test":
|
||
self_test()
|
||
elif len(sys.argv) == 3 and sys.argv[1] == "--patch-file":
|
||
try:
|
||
changed = patch_file(sys.argv[2])
|
||
print("patch_lvgl_arabic: %s" % ("patched" if changed else "already patched"))
|
||
except (OSError, RuntimeError) as error:
|
||
print("patch_lvgl_arabic: ERROR: %s" % error, file=sys.stderr)
|
||
raise SystemExit(1)
|
||
elif len(sys.argv) == 3 and sys.argv[1] == "--verify-file":
|
||
try:
|
||
verify_file(sys.argv[2])
|
||
print("patch_lvgl_arabic: verified")
|
||
except (OSError, RuntimeError) as error:
|
||
print("patch_lvgl_arabic: ERROR: %s" % error, file=sys.stderr)
|
||
raise SystemExit(1)
|
||
else:
|
||
raise SystemExit(
|
||
"usage: patch_lvgl_arabic.py --self-test | --patch-file <lv_txt_ap.c> | --verify-file <lv_txt_ap.c>"
|
||
)
|