Skip to content

Commit b137505

Browse files
jplexercodex
andcommitted
tools: add WebAssembly firmware text renderer
Signed-off-by: Joshua Jun <lets@throw.rocks> Co-authored-by: GPT-6 <noreply@openai.com> Signed-off-by: GPT-6 <noreply@openai.com>
1 parent d516d62 commit b137505

4 files changed

Lines changed: 254 additions & 0 deletions

File tree

tools/text2wasm/CMakeLists.txt

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# SPDX-FileCopyrightText: 2026 Core Devices LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
cmake_minimum_required(VERSION 3.21)
4+
project(pebble-text2wasm LANGUAGES C)
5+
6+
if(NOT EMSCRIPTEN)
7+
message(FATAL_ERROR "Configure with emcmake cmake to select the Emscripten toolchain")
8+
endif()
9+
find_package(Git REQUIRED)
10+
get_filename_component(PEBBLEOS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
11+
set(DIST "${CMAKE_CURRENT_BINARY_DIR}/dist")
12+
set(OVERRIDES "${CMAKE_CURRENT_BINARY_DIR}/overrides")
13+
file(MAKE_DIRECTORY "${DIST}" "${OVERRIDES}/kernel" "${OVERRIDES}/resource")
14+
15+
# The browser does not use events; its time ABI differs from the watch's.
16+
set(EVENTS_HEADER "${PEBBLEOS_ROOT}/src/fw/kernel/events.h")
17+
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${EVENTS_HEADER}")
18+
file(READ "${EVENTS_HEADER}" events)
19+
string(REPLACE "#if __SIZEOF_POINTER__ == 4"
20+
"#if __SIZEOF_POINTER__ == 4 && !defined(__EMSCRIPTEN__)" events "${events}")
21+
file(CONFIGURE OUTPUT "${OVERRIDES}/kernel/events.h" CONTENT "${events}" @ONLY)
22+
file(CONFIGURE OUTPUT "${OVERRIDES}/resource/resource_ids.auto.h"
23+
CONTENT "#pragma once\n#define RESOURCE_ID_FONT_FALLBACK_INTERNAL 0xffffffff\n" @ONLY)
24+
25+
add_executable(pebble_renderer
26+
renderer.c
27+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/8_bit/framebuffer.c"
28+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/framebuffer.c"
29+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/gcolor_definitions.c"
30+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/gtypes.c"
31+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/gbitmap.c"
32+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/graphics.c"
33+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/gtransform.c"
34+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/bitblt.c"
35+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/8_bit/bitblt_private.c"
36+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/text_render.c"
37+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/utf8.c"
38+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/text_layout.c"
39+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/rtl_support.c"
40+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/arabic_shaping.c"
41+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/perimeter.c"
42+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/text_resources.c"
43+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/graphics_private.c"
44+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/graphics_private_raw.c"
45+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/graphics_circle.c"
46+
"${PEBBLEOS_ROOT}/src/fw/applib/graphics/graphics_line.c"
47+
"${PEBBLEOS_ROOT}/src/fw/applib/fonts/codepoint.c"
48+
"${PEBBLEOS_ROOT}/lib/util/keyed_circular_cache.c"
49+
"${PEBBLEOS_ROOT}/lib/util/math.c"
50+
"${PEBBLEOS_ROOT}/lib/util/trig.c"
51+
"${PEBBLEOS_ROOT}/lib/util/iterator.c"
52+
)
53+
set_target_properties(pebble_renderer PROPERTIES
54+
OUTPUT_NAME renderer
55+
SUFFIX .js
56+
RUNTIME_OUTPUT_DIRECTORY "${DIST}"
57+
)
58+
target_include_directories(pebble_renderer PRIVATE
59+
"${OVERRIDES}"
60+
"${PEBBLEOS_ROOT}/src/fw/util/time"
61+
"${PEBBLEOS_ROOT}/tests/overrides/dummy_board"
62+
"${PEBBLEOS_ROOT}/tests/overrides/default"
63+
"${PEBBLEOS_ROOT}/tests/stubs"
64+
"${PEBBLEOS_ROOT}/tests/fakes"
65+
"${PEBBLEOS_ROOT}/tests/test_includes"
66+
"${PEBBLEOS_ROOT}/tests"
67+
"${PEBBLEOS_ROOT}/include"
68+
"${PEBBLEOS_ROOT}/include/pbl"
69+
"${PEBBLEOS_ROOT}/kernel/arch/posix/include"
70+
"${PEBBLEOS_ROOT}/subsys"
71+
"${PEBBLEOS_ROOT}/src/fw"
72+
"${PEBBLEOS_ROOT}/src/fw/applib/vendor/uPNG"
73+
"${PEBBLEOS_ROOT}/src/fw/applib/vendor/tinflate"
74+
"${PEBBLEOS_ROOT}/tests/fixtures"
75+
)
76+
target_compile_definitions(pebble_renderer PRIVATE
77+
UNITTEST
78+
CONFIG_SCREEN_COLOR_DEPTH_BITS=8
79+
DISP_COLS=144
80+
DISP_ROWS=168
81+
DISPLAY_FRAMEBUFFER_BYTES=24192
82+
PBL_COLOR
83+
PBL_RECT
84+
CONFIG_BOARD_OBELIX=1
85+
CONFIG_PLATFORM_EMERY=1
86+
CONFIG_FLASH_GD25Q256E=1
87+
CONFIG_KERNEL_NUM_PRIORITIES=5
88+
CONFIG_KERNEL_THREAD_TLS_SLOTS=2
89+
CONFIG_KERNEL_STACK_ALIGN=8
90+
CONFIG_KERNEL_TICK_HZ=1024
91+
)
92+
target_compile_options(pebble_renderer PRIVATE
93+
-O2
94+
-Wno-error=incompatible-pointer-types
95+
-Wno-gnu-variable-sized-type-not-at-end
96+
)
97+
target_link_options(pebble_renderer PRIVATE
98+
-O2
99+
-Wl,--error-limit=0
100+
-sSINGLE_FILE=1
101+
-sMODULARIZE=1
102+
-sEXPORT_NAME=createPebbleRenderer
103+
-sALLOW_MEMORY_GROWTH=1
104+
"-sEXPORTED_FUNCTIONS=['_render','_malloc','_free']"
105+
"-sEXPORTED_RUNTIME_METHODS=['ccall','HEAPU8']"
106+
)
107+
108+
set(font_outputs)
109+
foreach(directory normal common)
110+
file(GLOB fonts CONFIGURE_DEPENDS "${PEBBLEOS_ROOT}/resources/${directory}/base/pbf/GOTHIC*.pbf")
111+
foreach(font IN LISTS fonts)
112+
get_filename_component(name "${font}" NAME)
113+
set(output "${DIST}/${name}")
114+
add_custom_command(OUTPUT "${output}"
115+
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${font}" "${output}"
116+
DEPENDS "${font}"
117+
VERBATIM)
118+
list(APPEND font_outputs "${output}")
119+
endforeach()
120+
endforeach()
121+
122+
add_custom_target(text2wasm ALL
123+
COMMAND ${CMAKE_COMMAND}
124+
"-DGIT_EXECUTABLE=${GIT_EXECUTABLE}"
125+
"-DPEBBLEOS_ROOT=${PEBBLEOS_ROOT}"
126+
"-DOUTPUT=${DIST}/revision.txt"
127+
-P "${CMAKE_CURRENT_SOURCE_DIR}/WriteRevision.cmake"
128+
DEPENDS pebble_renderer ${font_outputs}
129+
BYPRODUCTS "${DIST}/revision.txt"
130+
VERBATIM)

tools/text2wasm/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!-- SPDX-FileCopyrightText: 2026 Core Devices LLC -->
2+
<!-- SPDX-License-Identifier: Apache-2.0 -->
3+
4+
# Firmware text rendering in WebAssembly
5+
6+
With Emscripten on PATH:
7+
8+
```sh
9+
emcmake cmake -S tools/text2wasm -B build-text2wasm
10+
cmake --build build-text2wasm --target text2wasm
11+
```
12+
13+
`dist/renderer.js` contains WASM and its loader. The output also includes Gothic
14+
base fonts and the firmware revision. Load with `createPebbleRenderer()`, pass a
15+
UTF-8 string, base PBF pointer/length, and extension PBF pointer/length to `render`, then copy the returned 144×168
16+
RGBA framebuffer before the next call. The caller retains ownership of inputs.
17+
18+
The target compiles firmware glyph decoding, layout, RTL/shaping, and drawing
19+
sources unchanged. Resource reads are served from memory. Test adapters supply
20+
application state and heap services. A generated header disables the on-watch
21+
event-size assertion for Emscripten's different time ABI; events are not used.
22+
23+
This is a text-box demo, not a full firmware emulator. It has no emoji font,
24+
or system-screen layout support yet. Optional extension PBFs use firmware glyph fallback. Input fonts must be
25+
trusted, compiler-produced PBF files; malformed PBF handling is not hardened.
26+
The browser host should cap text length and recreate the module after a trap.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# SPDX-FileCopyrightText: 2026 Core Devices LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${PEBBLEOS_ROOT}" rev-parse HEAD
5+
OUTPUT_VARIABLE revision
6+
COMMAND_ERROR_IS_FATAL ANY)
7+
file(CONFIGURE OUTPUT "${OUTPUT}" CONTENT "${revision}" @ONLY)

tools/text2wasm/renderer.c

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/* SPDX-FileCopyrightText: 2026 Core Devices LLC */
2+
/* SPDX-License-Identifier: Apache-2.0 */
3+
#include <stdlib.h>
4+
#include <string.h>
5+
#include "applib/graphics/graphics.h"
6+
#include "applib/graphics/framebuffer.h"
7+
#include "applib/graphics/text.h"
8+
#include "applib/graphics/text_resources.h"
9+
#include "stubs_app_state.h"
10+
#include "stubs_applib_resource.h"
11+
#include "stubs_compiled_with_legacy2_sdk.h"
12+
#include "stubs_logging.h"
13+
#include "stubs_pebble_tasks.h"
14+
#include "stubs_heap.h"
15+
#include "stubs_pbl_malloc.h"
16+
#include "stubs_memory_layout.h"
17+
18+
static const uint8_t *font_bytes;
19+
static const uint8_t *extension_bytes;
20+
static size_t extension_size;
21+
static size_t font_size;
22+
static FrameBuffer framebuffer;
23+
static GContext context;
24+
static uint8_t pixels[144 * 168 * 4];
25+
size_t sys_resource_load_range(ResAppNum app, uint32_t id, uint32_t offset, uint8_t *buffer,
26+
size_t size) {
27+
const uint8_t *bytes = id == 2 ? extension_bytes : font_bytes;
28+
size_t length = id == 2 ? extension_size : font_size;
29+
if (!bytes || offset > length || size > length - offset) return 0;
30+
memcpy(buffer, bytes + offset, size);
31+
return size;
32+
}
33+
bool sys_resource_is_valid(ResAppNum app, uint32_t id) {
34+
return id == 1 || (id == 2 && extension_size);
35+
}
36+
uint32_t sys_resource_get_and_cache(ResAppNum app, uint32_t id) {
37+
return id;
38+
}
39+
ResourceCallbackHandle resource_watch(ResAppNum app, uint32_t id, ResourceChangedCallback cb,
40+
void *data) {
41+
return NULL;
42+
}
43+
FontInfo *fonts_get_system_emoji_font_for_size(unsigned int size) {
44+
return NULL;
45+
}
46+
void passert_failed(const char *file, int line, const char *message, ...) {
47+
abort();
48+
}
49+
void passert_failed_no_message(const char *file, int line) {
50+
abort();
51+
}
52+
void passert_failed_no_message_with_lr(const char *file, int line, uint32_t lr) {
53+
abort();
54+
}
55+
uint8_t *render(const char *text, const uint8_t *font, size_t length, const uint8_t *extension,
56+
size_t extension_length) {
57+
font_bytes = font;
58+
font_size = length;
59+
extension_bytes = extension;
60+
extension_size = extension_length;
61+
FontInfo info = {0};
62+
if (!text_resources_init_font(0, 1, extension_length ? 2 : 0, &info)) return NULL;
63+
framebuffer_init(&framebuffer, &(GSize){144, 168});
64+
graphics_context_init(&context, &framebuffer, GContextInitializationMode_App);
65+
s_app_state_get_graphics_context = &context;
66+
framebuffer_clear(&framebuffer);
67+
graphics_context_set_text_color(&context, GColorBlack);
68+
graphics_draw_text(&context, text, &info, GRect(4, 4, 136, 160),
69+
GTextOverflowModeTrailingEllipsis, GTextAlignmentLeft, NULL);
70+
for (int i = 0; i < 144 * 168; i++) {
71+
uint8_t c = framebuffer.buffer[i];
72+
pixels[4 * i] = ((c >> 4) & 3) * 85;
73+
pixels[4 * i + 1] = ((c >> 2) & 3) * 85;
74+
pixels[4 * i + 2] = (c & 3) * 85;
75+
pixels[4 * i + 3] = 255;
76+
}
77+
return pixels;
78+
}
79+
80+
uint8_t fonts_get_font_height(GFont font) {
81+
return ((FontInfo *)font)->max_height;
82+
}
83+
void sys_font_reload_font(FontInfo *font) {
84+
abort();
85+
}
86+
GFont sys_font_get_system_font(const char *key) {
87+
return NULL;
88+
}
89+
void util_assertion_failed(const char *file, int line) {
90+
abort();
91+
}

0 commit comments

Comments
 (0)