Load fonts asynchronously on a bg task

This saves a second or two from bootup; AFAICT this *mostly* reclaims
the dynamic fonts boot time regression.
custom
jacqueline 10 months ago
parent cbcf1bea61
commit 88ac96242f
  1. 44
      lib/luavgl/src/font.c
  2. 11
      lib/luavgl/src/fs.c
  3. 2
      lib/luavgl/src/luavgl.h
  4. 36
      lua/font.lua
  5. 115
      lua/main.lua
  6. 62
      src/tangara/lua/bridge.cpp
  7. 115
      src/tangara/lua/lua_font.cpp
  8. 15
      src/tangara/lua/lua_font.hpp

@ -1,49 +1,23 @@
#include "luavgl.h" #include "luavgl.h"
#include "private.h" #include "private.h"
static char *to_lower(char *str)
{
for (char *s = str; *s; ++s)
*s = *s >= 'A' && *s <= 'Z' ? *s | 0x60 : *s;
return str;
}
static char *luavgl_strchr(const char *s, char c)
{
while (*s) {
if (c == *s) {
return (char *)s;
}
s++;
}
return NULL;
}
/**
* Dynamic font family fallback is not supported.
* The fallback only happen when font creation fails and continue to try next
* one. Fallback logic in lvgl is supposed to be system wide.
*
* lvgl.Font("MiSansW medium, montserrat", 24, "normal")
*/
static int luavgl_font_create(lua_State *L) static int luavgl_font_create(lua_State *L)
{ {
if (!lua_isstring(L, 1)) { if (!lua_isstring(L, 1)) {
return luaL_argerror(L, 1, "expect string"); return luaL_argerror(L, 1, "expect string");
} }
const char *name = lua_tostring(L, 1); if (!lua_isfunction(L, 2)) {
const lv_font_t *font = NULL; return luaL_argerror(L, 1, "expect function");
}
luavgl_ctx_t *ctx = luavgl_context(L); luavgl_ctx_t *ctx = luavgl_context(L);
if (ctx->make_font) { if (!ctx->make_font) {
font = ctx->make_font(name); return luaL_error(L, "cannot create font");
} }
if (font) { const char *name = lua_tostring(L, 1);
lua_pushlightuserdata(L, (void *)font); int cb_ref = luaL_ref(L, LUA_REGISTRYINDEX);
return 1; ctx->make_font(L, name, cb_ref);
}
return luaL_error(L, "cannot create font"); return 0;
} }

@ -1,6 +1,17 @@
#include "luavgl.h" #include "luavgl.h"
#include "private.h" #include "private.h"
static char *luavgl_strchr(const char *s, char c)
{
while (*s) {
if (c == *s) {
return (char *)s;
}
s++;
}
return NULL;
}
typedef struct luavgl_fs_s { typedef struct luavgl_fs_s {
lv_fs_file_t file; lv_fs_file_t file;
bool closed; /* userdata exists but lv_fs has been closed */ bool closed; /* userdata exists but lv_fs has been closed */

@ -12,7 +12,7 @@
extern "C" { extern "C" {
#endif #endif
typedef const lv_font_t *(*make_font_cb)(const char *); typedef void (*make_font_cb)(lua_State *L, const char *, int cb);
typedef void (*delete_font_cb)(const lv_font_t *); typedef void (*delete_font_cb)(const lv_font_t *);
typedef int (*luavgl_pcall_t)(lua_State *L, int nargs, int nresults); typedef int (*luavgl_pcall_t)(lua_State *L, int nargs, int nresults);

@ -1,6 +1,36 @@
local lvgl = require("lvgl") local lvgl = require("lvgl")
return { local fonts = {}
fusion_12 = lvgl.Font("//lua/fonts/fusion12"), local fonts_priv = {
fusion_10 = lvgl.Font("//lua/fonts/fusion10"), has_invoked_cb = false,
cb = nil,
} }
function fonts_priv.has_loaded_all()
return fonts.fusion_12 and fonts.fusion_10
end
function fonts_priv.invoke_cb()
if fonts_priv.has_invoked_cb or not fonts_priv.cb then return end
if not fonts_priv.has_loaded_all() then return end
fonts_priv.has_invoked_cb = true
fonts_priv.cb()
end
lvgl.Font("//lua/fonts/fusion12", function(font)
fonts.fusion_12 = font
fonts_priv.invoke_cb()
end)
lvgl.Font("//lua/fonts/fusion10", function(font)
fonts.fusion_10 = font
fonts_priv.invoke_cb()
end)
function fonts.on_loaded(cb)
fonts_priv.cb = cb
fonts_priv.has_invoked_cb = false
fonts_priv.invoke_cb()
end
return fonts

@ -1,4 +1,10 @@
-- Load fonts first, since they're parsed asynchronously and we can do much of
-- the other UI setup in parallel.
local font = require("font") local font = require("font")
-- require() everything else needed for the main menu + global bindings. Do
-- this now instead of in init_ui because loading and parsing the scripts can
-- take a while.
local vol = require("volume") local vol = require("volume")
local theme = require("theme") local theme = require("theme")
local controls = require("controls") local controls = require("controls")
@ -7,55 +13,66 @@ local sd_card = require("sd_card")
local backstack = require("backstack") local backstack = require("backstack")
local main_menu = require("main_menu") local main_menu = require("main_menu")
local theme_dark = require("theme_dark") local function init_ui()
theme.set(theme_dark) -- Load the theme within init_ui because the theme needs fonts to be ready.
local theme_dark = require("theme_dark")
theme.set(theme_dark)
local lock_time = time.ticks() local lock_time = time.ticks()
-- Set up property bindings that are used across every screen. -- Set up property bindings that are used across every screen.
GLOBAL_BINDINGS = { GLOBAL_BINDINGS = {
-- Show an alert with the current volume whenever the volume changes -- Show an alert with the current volume whenever the volume changes
vol.current_pct:bind(function(pct) vol.current_pct:bind(function(pct)
require("alerts").show(function() require("alerts").show(function()
local container = lvgl.Object(nil, { local container = lvgl.Object(nil, {
w = lvgl.PCT(80), w = lvgl.PCT(80),
h = lvgl.SIZE_CONTENT, h = lvgl.SIZE_CONTENT,
flex = { flex = {
flex_direction = "column", flex_direction = "column",
justify_content = "center", justify_content = "center",
align_items = "center", align_items = "center",
align_content = "center", align_content = "center",
}, },
radius = 8, radius = 8,
pad_all = 2, pad_all = 2,
}) })
theme.set_style(container, "pop_up") theme.set_style(container, "pop_up")
container:Label { container:Label {
text = string.format("Volume %i%%", pct), text = string.format("Volume %i%%", pct),
text_font = font.fusion_10 text_font = font.fusion_10
} }
container:Bar { container:Bar {
w = lvgl.PCT(100), w = lvgl.PCT(100),
h = 8, h = 8,
range = { min = 0, max = 100 }, range = { min = 0, max = 100 },
value = pct, value = pct,
} }
container:center() container:center()
end) end)
end), end),
-- When the device has been locked for a while, default to showing the now -- When the device has been locked for a while, default to showing the now
-- playing screen after unlocking. -- playing screen after unlocking.
controls.lock_switch:bind(function(locked) controls.lock_switch:bind(function(locked)
if locked then if locked then
lock_time = time.ticks() lock_time = time.ticks()
elseif time.ticks() - lock_time > 8000 then elseif time.ticks() - lock_time > 8000 then
local queue = require("queue") local queue = require("queue")
if queue.size:get() > 0 then if queue.size:get() > 0 then
require("playing"):pushIfNotShown() require("playing"):pushIfNotShown()
end
end end
end end),
end), sd_card.mounted:bind(function(mounted)
sd_card.mounted:bind(function(mounted) backstack.reset(main_menu:new())
backstack.reset(main_menu:new()) end),
end), }
} end
-- Wait for fonts to finish, then show the main menu.
-- We could show an intermediary Lua-controlled splash/loading UI whilst we
-- wait, but in practice loading the fonts takes only a few hundred ms longer
-- than all the other UI init.
font.on_loaded(function()
init_ui()
end)

@ -24,6 +24,7 @@
#include "lua/lua_controls.hpp" #include "lua/lua_controls.hpp"
#include "lua/lua_database.hpp" #include "lua/lua_database.hpp"
#include "lua/lua_filesystem.hpp" #include "lua/lua_filesystem.hpp"
#include "lua/lua_font.hpp"
#include "lua/lua_queue.hpp" #include "lua/lua_queue.hpp"
#include "lua/lua_screen.hpp" #include "lua/lua_screen.hpp"
#include "lua/lua_testing.hpp" #include "lua/lua_testing.hpp"
@ -50,65 +51,6 @@ namespace lua {
static constexpr char kBridgeKey[] = "bridge"; static constexpr char kBridgeKey[] = "bridge";
static auto make_font_cb(const char* name) -> const lv_font_t* {
// Most Lua file paths start with "//" in order to deal with LVGL's Windows-y
// approach to paths. Try to handle such paths correctly so that paths in Lua
// code look a bit more consistent.
{
std::string name_str = name;
if (name_str.starts_with("//")) {
name++;
}
}
// This following is a bit C-brained. Sorry.
ESP_LOGI(kTag, "load font '%s'", name);
FILE* f = fopen(name, "r");
if (!f) {
return NULL;
}
uint8_t* data = NULL;
long len = 0;
lv_font_t* font = NULL;
if (fseek(f, 0, SEEK_END)) {
goto fail_with_file;
}
len = ftell(f);
if (len <= 0) {
goto fail_with_file;
}
if (fseek(f, 0, SEEK_SET)) {
goto fail_with_file;
}
data = reinterpret_cast<uint8_t*>(heap_caps_malloc(len, MALLOC_CAP_SPIRAM));
if (!data) {
goto fail_with_buffer;
}
if (fread(data, 1, len, f) < len) {
goto fail_with_buffer;
}
// We can finally start parsing the font!
font = lv_binfont_create_from_buffer(data, len);
fail_with_buffer:
// LVGL copies the font data out of the buffer, so we don't need to big raw
// data alloc after this function returns.
heap_caps_free(data);
fail_with_file:
fclose(f);
return font;
}
static auto delete_font_cb(const lv_font_t* font) -> void { static auto delete_font_cb(const lv_font_t* font) -> void {
// FIXME: luavgl never actually calls this? // FIXME: luavgl never actually calls this?
} }
@ -146,7 +88,7 @@ auto Bridge::installBaseModules(lua_State* L) -> void {
auto Bridge::installLvgl(lua_State* L) -> void { auto Bridge::installLvgl(lua_State* L) -> void {
luavgl_set_pcall(L, CallProtected); luavgl_set_pcall(L, CallProtected);
luavgl_set_font_extension(L, make_font_cb, delete_font_cb); luavgl_set_font_extension(L, loadFont, delete_font_cb);
luaL_requiref(L, "lvgl", luaopen_lvgl, true); luaL_requiref(L, "lvgl", luaopen_lvgl, true);
lua_pop(L, 1); lua_pop(L, 1);
} }

@ -0,0 +1,115 @@
/*
* Copyright 2024 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "lua_font.hpp"
#include <cstdint>
#include <string>
#include "lauxlib.h"
#include "lua.h"
#include "lvgl.h"
#include "events/event_queue.hpp"
#include "lua/bridge.hpp"
#include "lua/lua_registry.hpp"
#include "lua/lua_thread.hpp"
namespace lua {
[[maybe_unused]] static constexpr char kTag[] = "lua_font";
/* Reads the given file completely into PSRAM. */
static auto readFont(std::string path) -> std::span<uint8_t> {
// This following is a bit C-brained. Sorry.
FILE* f = fopen(path.c_str(), "r");
if (!f) {
return {};
}
uint8_t* data = NULL;
long len = 0;
if (fseek(f, 0, SEEK_END)) {
goto fail;
}
len = ftell(f);
if (len <= 0) {
goto fail;
}
if (fseek(f, 0, SEEK_SET)) {
len = 0;
goto fail;
}
data = reinterpret_cast<uint8_t*>(heap_caps_malloc(len, MALLOC_CAP_SPIRAM));
if (!data) {
len = 0;
goto fail;
}
if (fread(data, 1, len, f) < len) {
heap_caps_free(data);
len = 0;
}
fail:
fclose(f);
return {data, static_cast<size_t>(len)};
}
static auto parseFont(std::span<uint8_t> data) -> lv_font_t* {
if (data.empty()) {
return nullptr;
}
lv_font_t* font = lv_binfont_create_from_buffer(data.data(), data.size());
heap_caps_free(data.data());
return font;
}
auto loadFont(lua_State* L, const char* path, int cb_ref) -> void {
// Most Lua file paths start with "//" in order to deal with LVGL's Windows-y
// approach to paths. Try to handle such paths correctly so that paths in Lua
// code look a bit more consistent.
std::string path_str = path;
if (path_str.starts_with("//")) {
path++;
path_str = path;
}
// Do the file read from the current thread, since the path might be for a
// file in flash, and we can't read from flash in a background task.
auto font_data = readFont(path_str);
Bridge* bridge = Bridge::Get(L);
bridge->services().bg_worker().Dispatch<void>([=]() {
// Do the parsing now that we're in the background.
lv_font_t* font = parseFont(font_data);
// Hop back to the UI task to invoke the Lua callback.
events::Ui().RunOnTask([=] {
// Retrieve the callback by ref, and release the ref.
lua_rawgeti(L, LUA_REGISTRYINDEX, cb_ref);
luaL_unref(L, LUA_REGISTRYINDEX, cb_ref);
// We always invoke the callback, but we don't always have a result.
if (font) {
lua_pushlightuserdata(L, (void*)font);
} else {
lua_pushnil(L);
}
CallProtected(L, 1, 0);
});
});
}
} // namespace lua

@ -0,0 +1,15 @@
/*
* Copyright 2024 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include "lua.hpp"
namespace lua {
auto loadFont(lua_State* L, const char* name, int cb_ref) -> void;
}
Loading…
Cancel
Save