[font] Keep the glyph table in flash (#20079)

This commit is contained in:
J. Nick Koston
2026-10-03 06:29:06 -05:00
committed by GitHub
parent f5c79eeec0
commit 8691a43879
5 changed files with 49 additions and 12 deletions
+2 -1
View File
@@ -726,7 +726,8 @@ async def to_code(config):
)
]
glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
# constexpr Glyph table in flash; on ESP8266 static_const_array would place it in RAM.
glyphs = cg.progmem_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
font_height = pt_to_px(base_font.size.height)
ascender = pt_to_px(base_font.size.ascender)
+2 -1
View File
@@ -309,7 +309,8 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
// Unknown char, skip
ESP_LOGW(TAG, "Codepoint 0x%08" PRIx32 " not found in font", code_point);
if (!this->glyphs_.empty()) {
uint8_t glyph_width = this->glyphs_[0].advance;
// Full-width read: a narrowing byte load would fault on a PROGMEM table on ESP8266.
int glyph_width = this->glyphs_[0].advance;
display->rectangle(x_at, y_start, glyph_width, this->height_, color);
x_at += glyph_width;
}
+14
View File
@@ -1,5 +1,7 @@
#pragma once
#include <type_traits>
#include "esphome/core/color.h"
#include "esphome/core/datatypes.h"
#include "esphome/core/defines.h"
@@ -36,6 +38,18 @@ class Glyph final {
int width;
int height;
};
// The glyph table lives in flash, read with plain loads, which ESP8266 only allows for whole words.
static_assert(std::is_same_v<decltype(Glyph::code_point), const uint32_t>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::data), const uint8_t *>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::advance), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::offset_x), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::offset_y), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::width), int>, "Glyph fields must stay word sized");
static_assert(std::is_same_v<decltype(Glyph::height), int>, "Glyph fields must stay word sized");
#ifdef USE_ESP8266
static_assert(alignof(Glyph) == sizeof(uint32_t), "Glyph fields must stay word sized");
static_assert(sizeof(Glyph) == 7 * sizeof(uint32_t), "Glyph is read from flash with word loads");
#endif
class Font final
#ifdef USE_DISPLAY
@@ -0,0 +1,11 @@
esphome:
name: test
esp8266:
board: esp01_1m
font:
- file: ../NotoSans-Regular.ttf
id: noto
size: 12
glyphs: "AB"
+20 -10
View File
@@ -4,8 +4,10 @@ Focuses on verifying that long multi-byte (Chinese/CJK) glyph strings
are correctly processed through the font configuration pipeline.
"""
from collections.abc import Callable
import functools
from pathlib import Path
import re
from unittest.mock import MagicMock, patch
import pytest
@@ -260,15 +262,12 @@ def mock_cg():
with (
patch("esphome.components.font.cg.add_define") as mock_define,
patch("esphome.components.font.cg.progmem_array") as mock_progmem,
patch("esphome.components.font.cg.static_const_array") as mock_static,
patch("esphome.components.font.cg.new_Pvariable") as mock_new_pvar,
):
mock_progmem.return_value = MagicMock()
mock_static.return_value = MagicMock()
yield {
"add_define": mock_define,
"progmem_array": mock_progmem,
"static_const_array": mock_static,
"new_Pvariable": mock_new_pvar,
}
@@ -288,13 +287,13 @@ async def test_to_code_long_latin_generates_all_glyphs(mock_cg):
mock_cg["add_define"].assert_any_call("USE_FONT")
# progmem_array receives the combined bitmap data (non-empty)
mock_cg["progmem_array"].assert_called_once()
bitmap_data = mock_cg["progmem_array"].call_args.args[1]
# bitmap data first, then the glyph table
assert mock_cg["progmem_array"].call_count == 2
bitmap_data = mock_cg["progmem_array"].call_args_list[0].args[1]
assert len(bitmap_data) > 0
# static_const_array receives one entry per unique glyph
mock_cg["static_const_array"].assert_called_once()
glyph_initializer = mock_cg["static_const_array"].call_args.args[1]
# the glyph table receives one entry per unique glyph
glyph_initializer = mock_cg["progmem_array"].call_args_list[1].args[1]
assert len(glyph_initializer) == glyph_count
# new_Pvariable is called with the correct glyph count
@@ -314,7 +313,7 @@ async def test_to_code_glyph_entries_contain_expected_fields(mock_cg):
await to_code(config)
glyph_initializer = mock_cg["static_const_array"].call_args.args[1]
glyph_initializer = mock_cg["progmem_array"].call_args_list[1].args[1]
for entry in glyph_initializer:
assert len(entry) == 7, f"Glyph entry should have 7 fields, got {len(entry)}"
codepoint = entry[0]
@@ -332,6 +331,17 @@ async def test_to_code_glyphs_sorted_by_utf8(mock_cg):
await to_code(config)
glyph_initializer = mock_cg["static_const_array"].call_args.args[1]
glyph_initializer = mock_cg["progmem_array"].call_args_list[1].args[1]
codepoints = [entry[0] for entry in glyph_initializer]
assert codepoints == sorted(codepoints)
def test_glyph_table_is_progmem(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""The constexpr glyph table goes to flash, not a RAM static const array."""
main_cpp = generate_main(component_config_path("glyph_table.yaml"))
assert re.search(r"static constexpr font::Glyph \w+\[\] PROGMEM = ", main_cpp)
assert "static const font::Glyph" not in main_cpp