diff --git a/nix/shell.nix b/nix/shell.nix index d352c909e762c8621bcc56d1773b7c1eb9a94730..031f9748d5d4d34cd599e36aec62e0f77fda4be5 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -20,6 +20,9 @@ in with nixpkgs; pkgs.mkShell { cmake ninja ncurses5 + + (python3.withPackages (ps: with ps; [ pygame wasmer wasmer-compiler-cranelift ])) + emscripten ]; shellHook = '' # For esp.py openocd integration. diff --git a/sim/README.md b/sim/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e87280050401f6611de831c8276106f21770fd1f --- /dev/null +++ b/sim/README.md @@ -0,0 +1,44 @@ +badg23 simulator +=== + +This is a little simulator that allows quicker development iteration on Python code. + +It's a (C)Python application which sets up its environment so that it appears similar enough to the Badge's micropython environment, loading scripts from `python_payload` and `python_modules` in the main project directory. + +All C-implemented functions are implemented (or maybe just stubbed out) by 'fakes' in the fakes directory. Please try to keep this in sync with the real usermodule implementation. + +Of particular interest is how we provide a `ctx`-compatible API: we compile it using emscripten to a WebAssembly bundle, which we then execute using wasmer. + +Setting up +--- + +If not using nix-shell, you'll need Python3 with the following libraries: + + - pygame + - wasmer + - wasmer-compiler-cranelift + +All of these should be available in PyPI. + +Running +--- + +From the main badge23-firmware checkout: + +``` +python3 sim/run.py +``` + +Known Issues +--- + +No support for input of any kind yet (captouch, three-way buttons). + +No support for audio yet. + +No support for LEDs yet. + +Hacking +--- + +A precompiled WASM bundle for ctx is checked into git. If you wish to rebuild it, run `build.sh` in `sim/wasm`. diff --git a/sim/background.png b/sim/background.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f775ad7473755db962478b3f2b8ab4a854a0f3 Binary files /dev/null and b/sim/background.png differ diff --git a/sim/fakes/ctx.py b/sim/fakes/ctx.py new file mode 100644 index 0000000000000000000000000000000000000000..7e5ac6c8aeed9c19a8fb36144c1d362d32d50674 --- /dev/null +++ b/sim/fakes/ctx.py @@ -0,0 +1,113 @@ +""" +ctx.py implements a subset of uctx that is backed by a WebAssembly-compiled +ctx. The interface between our uctx fake and the underlying ctx is the +serialized ctx protocol as described in [1]. + +[1] - https://ctx.graphics/protocol/ +""" +import os + +import wasmer +import wasmer_compiler_cranelift + + +class Wasm: + """ + Wasm wraps access to WebAssembly functions, converting to/from Python types + as needed. It's intended to be used as a singleton. + """ + def __init__(self): + store = wasmer.Store(wasmer.engine.JIT(wasmer_compiler_cranelift.Compiler)) + simpath = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + wasmpath = os.path.join(simpath, 'wasm', 'ctx.wasm') + module = wasmer.Module(store, open(wasmpath, 'rb').read()) + wasi_version = wasmer.wasi.get_version(module, strict=False) + wasi_env = wasmer.wasi.StateBuilder('badge23sim').finalize() + import_object = wasi_env.generate_import_object(store, wasi_version) + instance = wasmer.Instance(module, import_object) + self._i = instance + + def malloc(self, n): + return self._i.exports.malloc(n) + + def free(self, p): + self._i.exports.free(p) + + def ctx_parse(self, ctx, s): + s = s.encode('utf-8') + slen = len(s) + 1 + p = self.malloc(slen) + mem = self._i.exports.memory.uint8_view(p) + mem[0:slen] = s + mem[slen-1] = 0 + self._i.exports.ctx_parse(ctx, p) + self.free(p) + + def ctx_new_for_framebuffer(self, width, height): + """ + Call ctx_new_for_framebuffer, but also first allocate the underlying + framebuffer and return it alongside the Ctx*. + """ + fb = self.malloc(width * height * 2) + print('fb', hex(fb)) + return fb, self._i.exports.ctx_new_for_framebuffer(fb, width, height, width * 2, 7) + + def ctx_apply_transform(self, ctx, *args): + args = [float(a) for a in args] + return self._i.exports.ctx_apply_transform(ctx, *args) + +_wasm = Wasm() + +class Ctx: + """ + Ctx implements a subset of uctx [1]. It should be extended as needed as we + make use of more and more uctx features in the badge code. + + [1] - https://ctx.graphics/uctx/ + """ + LEFT = 'left' + RIGHT = 'right' + CENTER = 'center' + END = 'end' + MIDDLE = 'middle' + + def __init__(self): + self._fb, self._ctx = _wasm.ctx_new_for_framebuffer(240, 240) + # Place (0, 0) in the center of the screen, mathing what the real badge + # software does. + _wasm.ctx_apply_transform(self._ctx, 1, 0, 120, 0, 1, 120, 0, 0, 1) + + self.text_align = 'start' + self.text_baseline = 'alphabetic' + + def _get_fb(self): + return _wasm._i.exports.memory.uint8_view(self._fb) + + def _emit(self, text): + _wasm.ctx_parse(self._ctx, text) + + def move_to(self, x, y): + self._emit(f"moveTo {x} {x}") + return self + + def rgb(self, r, g, b): + self._emit(f"rgb {r/255} {g/255} {b/255}") + return self + + def text(self, s): + self._emit(f"textAlign {self.text_align}") + self._emit(f"textBaseline {self.text_baseline}") + self._emit(f"text \"{s}\"") + return self + + def round_rectangle(self, x, y, width, height, radius): + self._emit(f"roundRectangle {x} {y} {width} {height} {radius}") + return self + + def rectangle(self, x, y, width, height): + self._emit(f"rectangle {x} {y} {width} {height}") + return self + + def fill(self): + self._emit(f"fill") + return self diff --git a/sim/fakes/hardware.py b/sim/fakes/hardware.py new file mode 100644 index 0000000000000000000000000000000000000000..398a3dfc8a7a0ab0305abc4c3725dd8a7026faf8 --- /dev/null +++ b/sim/fakes/hardware.py @@ -0,0 +1,104 @@ +import pygame +import math +import os + +pygame.init() +screen_w = 814 +screen_h = 854 +screen = pygame.display.set_mode(size=(screen_w, screen_h)) +simpath = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +bgpath = os.path.join(simpath, 'background.png') +background = pygame.image.load(bgpath) + +_deprecated_notified = set() +def _deprecated(f): + def wrapper(*args, **kwargs): + if f not in _deprecated_notified: + print(f'{f.__name__} is deprecated!') + _deprecated_notified.add(f) + return f(*args, **kwargs) + return wrapper + +def init_done(): + return True + +def captouch_autocalib(): + pass + +def captouch_calibration_active(): + return False + +_global_ctx = None +def get_ctx(): + global _global_ctx + import ctx + + if _global_ctx is None: + _global_ctx = ctx.Ctx() + return _global_ctx + +@_deprecated +def display_fill(color): + """ + display_fill is deprecated as it doesn't work well with ctx's framebuffer + ownership / state diffing. Instead, callers should use plain ctx functions + to fill the screen. + """ + r = (color >> 11) & 0b11111 + g = (color >> 5 ) & 0b111111 + b = color & 0b11111 + get_ctx().rgb(r << 2, g << 3, b <<2).rectangle(-120, -120, 240, 240).fill() + + +def display_update(): + fb = get_ctx()._get_fb() + + full = pygame.Surface((screen_w, screen_h), flags=pygame.SRCALPHA) + full.blit(background, (0, 0)) + + center_x = 408 + center_y = 426 + off_x = center_x - (240 // 2) + off_y = center_y - (240 // 2) + + oled = pygame.Surface((240, 240), flags=pygame.SRCALPHA) + oled_buf = oled.get_buffer() + for y in range(240): + rgba = bytearray() + for x in range(240): + dx = (x - 120) + dy = (y - 120) + dist = math.sqrt(dx**2 + dy**2) + + fbh = fb[y * 240 * 2 + x * 2] + fbl = fb[y * 240 * 2 + x * 2 + 1] + fbv = (fbh << 8) | fbl + r = (fbv >> 11) & 0b11111 + g = (fbv >> 5) & 0b111111 + b = (fbv >> 0) & 0b11111 + if dist > 120: + rgba += bytes([255, 255, 255, 0]) + else: + rgba += bytes([b << 3, g << 2, r << 3, 0xff]) + oled_buf.write(bytes(rgba), y * 240 * 4) + + del oled_buf + full.blit(oled, (off_x, off_y)) + + screen.blit(full, (0,0)) + pygame.display.flip() + +def set_led_rgb(a, b, c, d): + pass + +def update_leds(): + pass + +def set_global_volume_dB(a): + pass + +def get_button(a): + return 0 + +def get_captouch(a): + return 0 diff --git a/sim/fakes/synth.py b/sim/fakes/synth.py new file mode 100644 index 0000000000000000000000000000000000000000..32f7291e217ed9366c63808a6e7792b37e1c8a50 --- /dev/null +++ b/sim/fakes/synth.py @@ -0,0 +1,9 @@ +class tinysynth: + def __init__(self, a, b): + pass + + def decay(self, a): + pass + + def waveform(self, a): + pass diff --git a/sim/fakes/time.py b/sim/fakes/time.py new file mode 100644 index 0000000000000000000000000000000000000000..a14f83f4796760c17e061af0a2a60409c08f050e --- /dev/null +++ b/sim/fakes/time.py @@ -0,0 +1,4 @@ + +def sleep_ms(ms): + import _time + _time.sleep(ms * 0.001) diff --git a/sim/run.py b/sim/run.py new file mode 100644 index 0000000000000000000000000000000000000000..28a5d14da97a0e59da18b2f8ea11275ea7c84b3e --- /dev/null +++ b/sim/run.py @@ -0,0 +1,44 @@ +import importlib +import importlib.machinery +from importlib.machinery import PathFinder, BuiltinImporter +import importlib.util +import os +import sys + + +projectpath = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + +class Hook: + """ + Hook implements a importlib.abc.Finder which overwrites resolution order to + more closely match the resolution order on the badge's Micropython + environment. + """ + def find_spec(self, fullname, path, target=None): + # Attempt to load from python_payload, then python_modules then + # sim/fakes. Afterwards, the normal import resolution order kicks in. + paths = [ + os.path.join(projectpath, 'python_payload', fullname+'.py'), + os.path.join(projectpath, 'python_payload', fullname), + os.path.join(projectpath, 'python_modules', fullname+'.py'), + os.path.join(projectpath, 'python_modules', fullname), + os.path.join(projectpath, 'sim', 'fakes', fullname+'.py'), + ] + for p in paths: + if os.path.exists(p): + root = os.path.split(p)[:-1] + return PathFinder.find_spec(fullname, root) + # As we provide our own micropython-compatible time library, allow + # resolving the original CPython time through _time + if fullname == '_time': + return BuiltinImporter.find_spec('time') + return None + +sys.meta_path.insert(0, Hook()) +sys.path_importer_cache.clear() + +# Clean up whatever might have already been imported as `time`. +import time +importlib.reload(time) + +import main diff --git a/sim/wasm/.gitattributes b/sim/wasm/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..077f4304c7aa8682656d2d26202d93f1d235c5e2 --- /dev/null +++ b/sim/wasm/.gitattributes @@ -0,0 +1 @@ +ctx.wasm binary diff --git a/sim/wasm/build.sh b/sim/wasm/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..4981d3a244d316a8de628f5185055010e5a41d80 --- /dev/null +++ b/sim/wasm/build.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -e -x + +# Work around Nix badness. +# See: https://github.com/NixOS/nixpkgs/issues/139943 +emscriptenpath="$(dirname $(dirname $(which emcc)))" +if [[ "${emscriptenpath}" = /nix/store/* ]]; then + if [ ! -d ~/.emscripten_cache ]; then + cp -rv "$emscriptenpath/share/emscripten/cache" ~/.emscripten_cache + chmod u+rwX -R ~/.emscripten_cache + fi + export EM_CACHE=~/.emscripten_cache +fi + + +emcc ctx.c \ + -I ../../usermodule/uctx/uctx/ \ + -I ../../usermodule/uctx/fonts/ \ + -s EXPORTED_FUNCTIONS=_ctx_new_for_framebuffer,_ctx_parse,_ctx_apply_transform,_malloc,_free \ + --no-entry -flto -O3 \ + -o ctx.wasm + diff --git a/sim/wasm/ctx.c b/sim/wasm/ctx.c new file mode 100644 index 0000000000000000000000000000000000000000..9efafaf0abf52bbdd229f67d132dd4ac39a27c5f --- /dev/null +++ b/sim/wasm/ctx.c @@ -0,0 +1,4 @@ +#include <stdint.h> +#include "ctx_config.h" +#define CTX_IMPLEMENTATION +#include "ctx.h" diff --git a/sim/wasm/ctx.wasm b/sim/wasm/ctx.wasm new file mode 100755 index 0000000000000000000000000000000000000000..6e5300ff9f43b2f99051d670f5be14adbf4e08a5 Binary files /dev/null and b/sim/wasm/ctx.wasm differ diff --git a/sim/wasm/ctx_config.h b/sim/wasm/ctx_config.h new file mode 100644 index 0000000000000000000000000000000000000000..9e70abc56c2cc71934a96d344754212ed0205b33 --- /dev/null +++ b/sim/wasm/ctx_config.h @@ -0,0 +1,104 @@ +#pragma once + +// Keep in sync with defines in usermodule/uctx/uctx.c. + +#define CTX_TINYVG 1 +#define CTX_TVG_STDIO 0 +#define CTX_DITHER 1 + +#define CTX_PDF 0 +#define CTX_PROTOCOL_U8_COLOR 1 +#define CTX_AVOID_CLIPPED_SUBDIVISION 0 +#define CTX_LIMIT_FORMATS 1 +#define CTX_ENABLE_FLOAT 0 +#define CTX_32BIT_SEGMENTS 0 +#define CTX_ENABLE_RGBA8 1 +#define CTX_ENABLE_RGB332 1 +#define CTX_ENABLE_GRAY1 1 +#define CTX_ENABLE_GRAY2 1 +#define CTX_ENABLE_GRAY4 1 +#define CTX_ENABLE_RGB565 1 +#define CTX_ENABLE_RGB565_BYTESWAPPED 1 +#define CTX_ENABLE_CBRLE 0 +#define CTX_BITPACK_PACKER 0 +#define CTX_COMPOSITING_GROUPS 0 +#define CTX_RENDERSTREAM_STATIC 0 +#define CTX_GRADIENT_CACHE 1 +#define CTX_ENABLE_CLIP 1 +#define CTX_MIN_JOURNAL_SIZE 512 // grows dynamically +#define CTX_MIN_EDGE_LIST_SIZE 512 // is also max and limits complexity + // of paths that can be filled +#define CTX_STATIC_OPAQUE 1 +#define CTX_MAX_SCANLINE_LENGTH 512 +#define CTX_1BIT_CLIP 1 + +#define CTX_MAX_DASHES 10 +#define CTX_MAX_GRADIENT_STOPS 10 +#define CTX_CM 0 +#define CTX_SHAPE_CACHE 0 +#define CTX_SHAPE_CACHE_DEFAULT 0 +#define CTX_RASTERIZER_MAX_CIRCLE_SEGMENTS 128 +#define CTX_NATIVE_GRAYA8 0 +#define CTX_ENABLE_SHADOW_BLUR 0 +#define CTX_FONTS_FROM_FILE 0 +#define CTX_MAX_KEYDB 16 +#define CTX_FRAGMENT_SPECIALIZE 1 +#define CTX_FAST_FILL_RECT 1 +#define CTX_MAX_TEXTURES 1 +#define CTX_PARSER_MAXLEN 512 +#define CTX_PARSER_FIXED_TEMP 1 +#define CTX_CURRENT_PATH 1 +#define CTX_BLENDING_AND_COMPOSITING 1 +#define CTX_STRINGPOOL_SIZE 256 +#define CTX_AUDIO 0 +#define CTX_CLIENTS 0 + +#define CTX_RAW_KB_EVENTS 0 +#define CTX_MATH 0 +#define CTX_TERMINAL_EVENTS 0 // gets rid of posix bits and bobs +#define CTX_THREADS 0 +#define CTX_TILED 0 +#define CTX_FORMATTER 0 // we want these eventually +#define CTX_PARSER 0 // enabled +#define CTX_BRAILLE_TEXT 0 + +#define CTX_BAREMETAL 1 + +#define CTX_EVENTS 1 +#define CTX_MAX_DEVICES 1 +#define CTX_MAX_KEYBINDINGS 16 +#define CTX_RASTERIZER 1 +#define CTX_MAX_STATES 5 +#define CTX_MAX_EDGES 127 +#define CTX_MAX_PENDING 64 +#define CTX_MAX_CBS 8 +#define CTX_MAX_LISTEN_FDS 1 + +#define CTX_ONE_FONT_ENGINE 1 + +#define CTX_STATIC_FONT(font) \ + ctx_load_font_ctx(ctx_font_##font##_name, \ + ctx_font_##font, \ + sizeof (ctx_font_##font)) + + + +#define CTX_MAX_FONTS 10 + +#include "Arimo-Regular.h" +#include "Arimo-Bold.h" +#include "Arimo-Italic.h" +#include "Arimo-BoldItalic.h" +#define CTX_FONT_0 CTX_STATIC_FONT(Arimo_Regular) +#define CTX_FONT_1 CTX_STATIC_FONT(Arimo_Bold) +#define CTX_FONT_2 CTX_STATIC_FONT(Arimo_Italic) +#define CTX_FONT_3 CTX_STATIC_FONT(Arimo_BoldItalic) + +#include "Tinos-Regular.h" +#define CTX_FONT_13 CTX_STATIC_FONT(Tinos_Regular) + +#include "Cousine-Regular.h" +#include "Cousine-Bold.h" +#define CTX_FONT_21 CTX_STATIC_FONT(Cousine_Regular) +#define CTX_FONT_22 CTX_STATIC_FONT(Cousine_Bold) + diff --git a/usermodule/uctx/uctx/ctx.h b/usermodule/uctx/uctx/ctx.h index aeafa2f93fda6238725f341e56c817b4a9c430ab..aff487f9c3c26915ac98f0fa92771d5c381c1ae8 100644 --- a/usermodule/uctx/uctx/ctx.h +++ b/usermodule/uctx/uctx/ctx.h @@ -27717,7 +27717,7 @@ static int is_in_ctx (void) } #endif -#if EMSCRIPTEN +#if CTX_WASM_ORIG // Renamed from upstream, as we don't want this code to be compiled in, even though we're using Emscripten. CTX_EXPORT Ctx * ctx_wasm_get_context (int flags); @@ -40644,7 +40644,7 @@ Ctx *ctx_new_tft (TFT_eSPI *tft, } #endif -#ifdef EMSCRIPTEN +#ifdef CTX_WASM_ORIG // Renamed from upstream, as we don't want this code to be compiled in, even though we're using Emscripten. #include "emscripten.h" #include <unistd.h>