@@ -19,10 +19,11 @@ export** binary (`bin/png/`). Two properties of the library make a Pebble port r
1919 Floyd–Steinberg error diffusion — its only state is a small two-row error buffer carried forward
2020 between strips; see [ the Pebble dither] ( #dither-palette ) .
2121
22- ` lib/frame.zig ` 's ` render() ` renders the ** full image as a single band**
23- (` image.band(Linear, buffer, image.height, 0) ` ), and both shells call it that way. Driving the
24- pipeline strip-by-strip on-device is therefore _ new shell code_ , not new library code — the banded
25- primitives already exist and are tested.
22+ ` lib/frame.zig ` 's ` render() ` renders the whole frame in one shot: it builds a supersampled band
23+ (` supersampled.band(Linear, linear_buffer, supersampled.height, 0) ` ), runs ` Watchface.render ` into
24+ it, box-averages it down, then constructs the target-resolution band over the front of the same
25+ buffer. Driving the pipeline strip-by-strip on-device is therefore _ new shell code_ , not new library
26+ code — the banded primitives already exist and are tested.
2627
2728## Feasibility
2829
@@ -59,7 +60,7 @@ approach has direct prior art.
59601 . Whether Zig 0.16 / LLVM emits relocations for ` thumbv7m ` /` thumbv7em ` that the Pebble app loader
6061 applies correctly for any mutable globals/statics. Build a minimal Zig-on-Pebble proof first and
6162 keep global state at zero.
62- 2 . Soft-float f32 plus scalarized ` @Vector ` SIMD (≈59 uses across ` lib/ ` ; neither Cortex-M4F nor
63+ 2 . Soft-float f32 plus scalarized ` @Vector ` SIMD (used throughout ` lib/ ` ; neither Cortex-M4F nor
6364 base Cortex-M33 has packed SIMD) — code size and render time within the budget. Fine in principle
6465 for a once-per-minute redraw, but must be measured on the emulator.
65663 . ` compiler_rt ` soft-float routines linking against newlib-nano without duplicate ` __aeabi_* `
@@ -85,13 +86,10 @@ rectangular 64-colour model is a straightforward second target.
8586
8687Notes:
8788
88- - The SoC is the ** SiFli SF32LB52J** , a big.LITTLE pair of Cortex-M33 STAR-MC1 cores (240 MHz HCPU +
89- 24 MHz LCPU, 512 KB SRAM). The 16 MB PSRAM exists on-chip but is ** not enabled in PebbleOS** , so
90- don't count on it.
91- - Round 2 is ** real and pre-orderable** (shown at CES, January 2026) but ** has not shipped to
92- customers** . Plan to validate entirely on the ** emulator** .
93- - ** FPU:** the Cortex-M33 FPU is optional and not confirmed for the SF32LB52J from primary sources —
94- but it's ** moot** , because the Pebble app ABI is soft-float regardless (see below).
89+ - The 16 MB PSRAM exists on-chip but is ** not enabled in PebbleOS** , so don't count on it. Round 2
90+ has not shipped to customers (2026-05-30); plan to validate entirely on the ** emulator** .
91+ - ** FPU:** the Cortex-M33 FPU is optional and unconfirmed for the SF32LB52J, but ** moot** — the
92+ Pebble app ABI is soft-float regardless (see below).
9593
9694### Platform names vs. board codenames
9795
@@ -153,25 +151,21 @@ The Floyd–Steinberg dither's only extra scratch is a two-row error buffer
153151(` dither.errorBufferSize(width) ` = ` width × 3 × 2 ` f32; ~ 6 KB at 260 px), carried forward between
154152strips (see below).
155153
156- Supersampling (` config.supersample_enabled ` ; factor ` N = 2 ` via ` frame.supersampleFactor ` ) renders
157- the continuous image at ` N× ` and box-averages it down in linear light before quantizing, which
158- antialiases the prism, hand, and rainbow edges. The downsample is purely local — each output pixel
159- reads only its own ` N × N ` source block — so it stays band-compatible: a ` band_height = 1 ` strip
160- needs ` N ` supersampled rows of ` N × width ` linear scratch (` N² × width × 16 B ` ; ~ 16 KB at ` N = 2 ` ,
161- 260 px wide), still far under budget. The cost is render time, which grows with ` N² ` — measure it on
162- the emulator against the once-a-minute redraw before enabling it.
154+ Supersampling (` config.supersample_enabled ` ; factor ` N = 2 ` via ` frame.supersampleFactor ` )
155+ antialiases the prism, hand, and rainbow edges. It stays band-compatible because each output pixel
156+ reads only its own ` N × N ` source block, so a ` band_height = 1 ` strip needs only ` N² × width × 16 B `
157+ of linear scratch (~ 16 KB at ` N = 2 ` , 260 px); the cost is render time, which grows with ` N² ` —
158+ measure it on the emulator.
163159
164160The ** framebuffer itself is owned by the firmware** — ` graphics_capture_frame_buffer ` hands you the
165161real 8-bit ` GColor8 ` buffer (~ 66 KB for 260×260), which the OS already allocated. The app only pays
166162for the band scratch above, comfortably within budget.
167163
168- The Floyd–Steinberg dither carries error between rows, so it is ** order-dependent** : strips must be
169- applied top-to-bottom and the caller must persist the two-row error buffer across ` dither.apply `
170- calls (it is zeroed on the first band, where ` y_offset == 0 ` ). Done that way, strip rendering
171- reproduces the single-pass output bit-for-bit (proven by the
172- ` multi-band apply matches single-band apply ` test). This is the one cross-band dependency the
173- renderer has; everything else is per-pixel. Drive a few ` band_height ` values and pick the smallest
174- that renders fast enough.
164+ The dither is the renderer's one cross-band dependency (top-to-bottom order, persisted error buffer;
165+ see [ the Pebble dither] ( #dither-palette ) ); everything else is per-pixel. Driven that way, strip
166+ rendering reproduces the single-pass output bit-for-bit (the
167+ ` multi-band apply matches single-band apply ` test). Drive a few ` band_height ` values and pick the
168+ smallest that renders fast enough.
175169
176170## Pixel format: ` GColor8 `
177171
@@ -347,16 +341,14 @@ each strip into the framebuffer via `gbitmap_get_data_row_info`.
347341
348342## C app shell
349343
350- Standard Pebble watchface lifecycle:
344+ Standard Pebble watchface lifecycle (` window_create ` → a ` Layer ` with an update proc →
345+ ` tick_timer_service_subscribe(MINUTE_UNIT, …) ` whose handler calls ` layer_mark_dirty ` →
346+ ` app_event_loop ` ); scaffold it from ` pebble new-project ` . The only port-specific part is the band
347+ loop in the update proc:
351348
352349``` c
353- #include < pebble.h>
354-
355350extern void pebbleRenderBand (uint8_t * out, uint16_t band_index, uint8_t hour, uint8_t minute);
356351
357- static Window * s_window;
358- static Layer * s_canvas;
359-
360352static void canvas_update_proc(Layer * layer, GContext * ctx) {
361353 GBitmap * fb = graphics_capture_frame_buffer(ctx);
362354 if (!fb) return;
@@ -376,30 +368,6 @@ static void canvas_update_proc(Layer *layer, GContext *ctx) {
376368
377369 graphics_release_frame_buffer(ctx, fb);
378370}
379-
380- static void tick_handler(struct tm * tick_time, TimeUnits units) {
381- layer_mark_dirty(s_canvas);
382- }
383-
384- static void window_load(Window * window) {
385- Layer * root = window_get_root_layer(window);
386- s_canvas = layer_create(layer_get_bounds(root));
387- layer_set_update_proc(s_canvas, canvas_update_proc);
388- layer_add_child(root, s_canvas);
389- }
390-
391- static void window_unload(Window * window) { layer_destroy(s_canvas); }
392-
393- int main(void) {
394- s_window = window_create();
395- window_set_window_handlers(s_window, (WindowHandlers){
396- .load = window_load, .unload = window_unload,
397- });
398- window_stack_push(s_window, true);
399- tick_timer_service_subscribe(MINUTE_UNIT, tick_handler);
400- app_event_loop();
401- window_destroy(s_window);
402- }
403371```
404372
405373## Build integration
@@ -478,30 +446,21 @@ band-by-band and `pebble screenshot` to compare against the PNG export.
478446
479447## Open questions
480448
449+ The relocation, soft-float perf/size, and ` compiler_rt ` linker risks are tracked in
450+ [ Risks to retire early] ( #feasibility ) . The remaining open questions:
451+
481452- ** Exact app build flags.** Read the installed SDK ` waftools ` for the precise ` -mcpu ` , ` -mthumb ` ,
482453 ` -fPIC ` /` -fPIE ` , ` -mfloat-abi=soft ` , ` -msingle-pic-base ` /` -mpic-register ` — don't infer the PIC
483454 model from ARM convention.
484- - ** Relocations.** Empirically confirm Zig 0.16/LLVM emits data relocations the Pebble loader
485- applies correctly for ` thumbv7m ` /` thumbv7em ` under ` relocation-model=pic ` . Build a minimal proof
486- first.
487- - ** Performance & size.** Measure soft-float render time and ` .pbw ` code/heap size on the
488- ` gabbro ` /` emery ` emulator with ` @Vector ` ops scalarized, against the ~ 128 KB budget.
489- - ** Linker hygiene.** Confirm ` compiler_rt ` soft-float routines link against newlib-nano with no
490- duplicate ` __aeabi_* ` symbols.
491455- ** Manifest.** Confirm the exact ` sdkVersion ` string and ` targetPlatforms ` list the appstore
492456 accepts for a 4.9.x watchface; re-read per-app heap numbers from PebbleOS headers.
493457- <a id =" panel-gamma " ></a >** Panel gamma — mostly resolved; not a blocker.** ` GColor8 ` is a _ nominal_
494- colour space: levels expand linearly to {0, 85, 170, 255} (no gamma), and the QEMU emulator
495- renders them linearly (` * 255 / 3 ` , no curve), so the dither — which quantizes in the sRGB domain
496- assuming the four levels are evenly spaced — is ** exactly** emulator-accurate. PebbleOS adds
497- ** no** gamma/colour-correction LUT for ` getafix ` /` obelix ` — the SiFli driver
498- (` src/fw/drivers/display/sf32lb/display_jdi.c ` ) only does a mechanical 222→332 bit-repack (its
499- LCDC layer is ` RGB332 ` ); the ` GColor8 ` /ARGB2222 model is identical to ` basalt ` /` chalk ` . The
500- ** only** residual unknown is the physical reflective JDI panel + the closed SiFli vendor HAL
501- (` bf0_hal_lcdc.c ` ), measurable only on real hardware. If its response diverges from sRGB it
502- degrades dither ** quality** (the threshold lands at slightly wrong brightnesses — most visible on
503- the rainbow gradient), never output validity; the fix applies a correction curve before
504- quantization. So it cannot gate pre-hardware work.
458+ colour space (levels expand linearly to {0, 85, 170, 255}, no gamma) and QEMU renders them
459+ linearly, so the sRGB-domain dither is ** exactly** emulator-accurate; PebbleOS adds no
460+ gamma/colour LUT for ` getafix ` /` obelix ` . The only residual unknown is the physical reflective JDI
461+ panel's response, measurable only on hardware — and a divergence degrades dither ** quality** , not
462+ validity (fixable with a correction curve before quantization), so it can't gate pre-hardware
463+ work.
505464- ** Shipping reality.** Confirm Round 2 hardware actually ships before relying on anything beyond
506465 the emulator.
507466
0 commit comments