fix(gui): survive font rendering under low heap
Cache font heights, release FreeType glyph state between apps, and catch renderer allocation failures before they cross Lua and abort the device. Individual low-memory draws degrade to allocation-free Font 1. Show disconnected WiFi as three faded zero-signal bars without expanding the icon footprint.
This commit is contained in:
@@ -50,8 +50,10 @@ within an app.
|
||||
|
||||
Lua text always uses one OpenFontRender TTF face: embedded Meslo by default, or one SD
|
||||
font selected by `gui.setFont(path)`. Sizes are per draw/measurement call and UI nodes use
|
||||
`fontSize`; never load a face per node. A new app restores the embedded face. Font 1 is
|
||||
reserved for C++ failure screens.
|
||||
`fontSize`; never load a face per node. A new app restores the embedded face and drops the
|
||||
previous glyph cache. OpenFontRender allocates C++ queues even for measurement: cache font
|
||||
heights, and keep the `std::bad_alloc` boundary in `gui.cpp` so low heap falls back to Font
|
||||
1 instead of aborting across Lua. Font 1 also serves C++ failure screens.
|
||||
|
||||
## Status Bar
|
||||
|
||||
|
||||
@@ -87,8 +87,9 @@ local height = gui.getFontHeight(20)
|
||||
`gui.setFont("/fonts/MyFont.ttf")` replaces the selected face with a TTF from SD;
|
||||
`gui.setFont(nil)` restores Meslo. One face is retained at a time, SD fonts are capped at
|
||||
4MB, and every app starts on Meslo. UI text accepts `fontSize = 20` and defaults to 14.
|
||||
The embedded font contains printable ASCII and Latin-1; Font 1 remains compiled in only
|
||||
for C++ failure screens that must work when the renderer does not.
|
||||
The embedded font contains printable ASCII and Latin-1. Font 1 remains compiled as the
|
||||
allocation-free fallback for C++ failure screens and for individual draws when FreeType
|
||||
cannot allocate under severe heap pressure.
|
||||
|
||||
## Settings
|
||||
|
||||
|
||||
+26
-21
@@ -13,7 +13,7 @@ local PAD = 6
|
||||
-- often it calls draw().
|
||||
local M = {height = 44, interval = 1000}
|
||||
local BAR_H = M.height
|
||||
local WIFI_W = 17
|
||||
local WIFI_W = 11
|
||||
local GAP = 8
|
||||
local TITLE_FONT_SIZE = 14
|
||||
local INFO_FONT_SIZE = 10
|
||||
@@ -21,6 +21,7 @@ local INFO_FONT_SIZE = 10
|
||||
-- What is currently on the panel. Every field repaints only when its value changes, so
|
||||
-- the once-a-second tick costs one clock rectangle instead of a whole bar.
|
||||
local shown = {}
|
||||
local geometry = {}
|
||||
|
||||
local function clock()
|
||||
return sys.isClockSynced() and os.date("%H:%M:%S") or "--:--:--"
|
||||
@@ -43,10 +44,7 @@ local function drawWifi(x, y, bars, color, muted)
|
||||
local h = i * 3
|
||||
gui.fillRect(x + (i - 1) * 4, y + 9 - h, 3, h, i <= bars and color or muted)
|
||||
end
|
||||
if bars == 0 then
|
||||
gui.drawLine(x + 12, y + 3, x + 16, y + 7, color)
|
||||
gui.drawLine(x + 16, y + 3, x + 12, y + 7, color)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- Fills the leading square of the bar, which is the rect the firmware treats as back.
|
||||
@@ -73,25 +71,33 @@ end
|
||||
|
||||
function M.draw(hasBack)
|
||||
local theme = ui.theme
|
||||
local w = gui.getWidth()
|
||||
local titleH = gui.getFontHeight(TITLE_FONT_SIZE)
|
||||
local infoH = gui.getFontHeight(INFO_FONT_SIZE)
|
||||
local titleY = (BAR_H - 1 - titleH) // 2
|
||||
local topY, bottomY = PAD, BAR_H - 1 - PAD - infoH
|
||||
|
||||
-- Fixed slot widths keep the title and neighbouring fields still as values change.
|
||||
local clockW = gui.getTextWidth("00:00:00", INFO_FONT_SIZE)
|
||||
local memW = gui.getTextWidth("000kB", INFO_FONT_SIZE)
|
||||
local clockX = w - PAD - clockW
|
||||
local signalX = w - PAD - WIFI_W
|
||||
local memX = signalX - GAP - memW
|
||||
local rightX = math.min(clockX, memX)
|
||||
local rotation, font = gui.getRotation(), gui.getFont()
|
||||
local frameKey = rotation .. "|" .. font
|
||||
if geometry.key ~= frameKey then
|
||||
local w = gui.getWidth()
|
||||
local titleH = gui.getFontHeight(TITLE_FONT_SIZE)
|
||||
local infoH = gui.getFontHeight(INFO_FONT_SIZE)
|
||||
local clockW = gui.getTextWidth("00:00:00", INFO_FONT_SIZE)
|
||||
local memW = gui.getTextWidth("000kB", INFO_FONT_SIZE)
|
||||
local clockX = w - PAD - clockW
|
||||
local signalX = w - PAD - WIFI_W
|
||||
local memX = signalX - GAP - memW
|
||||
geometry = {
|
||||
key = frameKey, w = w, titleY = (BAR_H - 1 - titleH) // 2,
|
||||
topY = PAD, bottomY = BAR_H - 1 - PAD - infoH, infoH = infoH,
|
||||
clockW = clockW, memW = memW, clockX = clockX, signalX = signalX,
|
||||
memX = memX, rightX = math.min(clockX, memX),
|
||||
}
|
||||
end
|
||||
local w, titleY, topY, bottomY = geometry.w, geometry.titleY, geometry.topY, geometry.bottomY
|
||||
local infoH, clockW, memW = geometry.infoH, geometry.clockW, geometry.memW
|
||||
local clockX, signalX, memX, rightX = geometry.clockX, geometry.signalX, geometry.memX, geometry.rightX
|
||||
|
||||
-- Rotation moves every slot, the theme recolors them, and an app may rename itself at
|
||||
-- any time. A new app gets a fresh Lua state, so an empty cache already means "repaint
|
||||
-- everything".
|
||||
local name = sys.getAppName()
|
||||
local key = gui.getRotation() .. "|" .. ui.themeName .. "|" .. gui.getFont() .. "|" .. name
|
||||
local key = frameKey .. "|" .. ui.themeName .. "|" .. name
|
||||
if key ~= shown.key then
|
||||
shown = {key = key}
|
||||
gui.fillRect(0, 0, w, BAR_H, theme.bg)
|
||||
@@ -112,8 +118,7 @@ function M.draw(hasBack)
|
||||
if mem ~= shown.mem then
|
||||
shown.mem = mem
|
||||
gui.fillRect(memX, bottomY, memW, infoH, theme.bg)
|
||||
gui.drawText(mem, memX + memW - gui.getTextWidth(mem, INFO_FONT_SIZE), bottomY,
|
||||
theme.muted, theme.bg, INFO_FONT_SIZE)
|
||||
gui.drawText(mem, memX, bottomY, theme.muted, theme.bg, INFO_FONT_SIZE)
|
||||
end
|
||||
|
||||
local bars = signalBars()
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <SD.h>
|
||||
#include <TFT_eSPI.h>
|
||||
|
||||
#include <new>
|
||||
|
||||
#include "../../gfx/round_rect.h"
|
||||
#include "../bindings.h"
|
||||
#include "../lua_app.h"
|
||||
@@ -104,20 +106,35 @@ static unsigned fontSize(lua_State* L, int index) {
|
||||
}
|
||||
|
||||
static int l_gui_fontHeight(lua_State* L) {
|
||||
lua_pushinteger(L, app(L)->fontHeight(fontSize(L, 1)));
|
||||
return 1;
|
||||
try {
|
||||
lua_pushinteger(L, app(L)->fontHeight(fontSize(L, 1)));
|
||||
return 1;
|
||||
} catch (const std::bad_alloc&) {
|
||||
lua_pushinteger(L, fontSize(L, 1));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int l_gui_textWidth(lua_State* L) {
|
||||
const char* text = luaL_checkstring(L, 1);
|
||||
lua_pushinteger(L, app(L)->textWidth(text, fontSize(L, 2)));
|
||||
return 1;
|
||||
unsigned size = fontSize(L, 2);
|
||||
try {
|
||||
lua_pushinteger(L, app(L)->textWidth(text, size));
|
||||
return 1;
|
||||
} catch (const std::bad_alloc&) {
|
||||
lua_pushinteger(L, strlen(text) * size * 4 / 7);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int l_gui_setFont(lua_State* L) {
|
||||
if (lua_isnoneornil(L, 1)) {
|
||||
if (!app(L)->setFont(nullptr)) return luaL_error(L, "default font unavailable");
|
||||
return 0;
|
||||
try {
|
||||
if (!app(L)->setFont(nullptr)) return luaL_error(L, "default font unavailable");
|
||||
return 0;
|
||||
} catch (const std::bad_alloc&) {
|
||||
return luaL_error(L, "out of memory loading font");
|
||||
}
|
||||
}
|
||||
|
||||
const char* path = luaL_checkstring(L, 1);
|
||||
@@ -126,8 +143,12 @@ static int l_gui_setFont(lua_State* L) {
|
||||
size_t size = file.size();
|
||||
file.close();
|
||||
if (!size || size > MAX_FONT_FILE) return luaL_error(L, "font must be 1 byte to 4MB");
|
||||
if (!app(L)->setFont(path)) return luaL_error(L, "invalid font: %s", path);
|
||||
return 0;
|
||||
try {
|
||||
if (!app(L)->setFont(path)) return luaL_error(L, "invalid font: %s", path);
|
||||
return 0;
|
||||
} catch (const std::bad_alloc&) {
|
||||
return luaL_error(L, "out of memory loading font");
|
||||
}
|
||||
}
|
||||
|
||||
static int l_gui_getFont(lua_State* L) {
|
||||
@@ -141,7 +162,17 @@ static int l_gui_drawText(lua_State* L) {
|
||||
int y = luaL_checkinteger(L, 3);
|
||||
uint16_t fg = luaL_optinteger(L, 4, TFT_BLACK);
|
||||
uint16_t bg = luaL_optinteger(L, 5, TFT_WHITE);
|
||||
app(L)->drawText(text, x, y, fg, bg, fontSize(L, 6));
|
||||
try {
|
||||
app(L)->drawText(text, x, y, fg, bg, fontSize(L, 6));
|
||||
} catch (const std::bad_alloc&) {
|
||||
// Text remains legible when FreeType cannot allocate its short-lived glyph queues.
|
||||
// Font 1 is already in flash and allocates nothing, so low memory degrades instead of
|
||||
// crossing the Lua C boundary as an uncaught C++ exception and aborting the device.
|
||||
app(L)->tft.setTextFont(1);
|
||||
app(L)->tft.setTextSize(1);
|
||||
app(L)->tft.setTextColor(fg, bg);
|
||||
app(L)->tft.drawString(text, x, y);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -84,11 +84,14 @@ void LuaApp::resetFontRenderer() {
|
||||
font->setDrawer(tft);
|
||||
fontLoaded = false;
|
||||
currentFont = "";
|
||||
std::fill(fontHeightCache, fontHeightCache + 97, 0);
|
||||
}
|
||||
|
||||
bool LuaApp::setFont(const char* path) {
|
||||
String requested = path ? path : "builtin:MesloLGS-Regular";
|
||||
if (fontLoaded && requested == currentFont) return true;
|
||||
// Reload the embedded face for every app so FreeType's glyph cache belongs to the app
|
||||
// that populated it and is released at the same lifecycle boundary as the Lua state.
|
||||
if (path && fontLoaded && requested == currentFont) return true;
|
||||
resetFontRenderer();
|
||||
|
||||
FT_Error error = path ? font->loadFont(path) : font->loadFont(mesloStart, mesloEnd - mesloStart);
|
||||
@@ -117,8 +120,9 @@ int LuaApp::textWidth(const char* text, unsigned size) {
|
||||
}
|
||||
|
||||
int LuaApp::fontHeight(unsigned size) {
|
||||
if (fontHeightCache[size]) return fontHeightCache[size];
|
||||
font->setFontSize(size);
|
||||
return font->getTextHeight("Mg");
|
||||
return fontHeightCache[size] = font->getTextHeight("Mg");
|
||||
}
|
||||
|
||||
void LuaApp::drawText(const char* text, int x, int y, uint16_t fg, uint16_t bg, unsigned size) {
|
||||
|
||||
@@ -101,6 +101,7 @@ class LuaApp {
|
||||
OpenFontRender* font = nullptr;
|
||||
String currentFont;
|
||||
bool fontLoaded = false;
|
||||
int fontHeightCache[97] = {};
|
||||
// One strike: a bar that failed once fails identically every second, and the serial
|
||||
// log is the only place anyone would see it. The strip stays reserved either way, so a
|
||||
// bar that dies mid-run never resizes the app underneath it.
|
||||
|
||||
Reference in New Issue
Block a user