| Replace V with Nim 472eb49 nandithebull 8h ago | 1 | /// Web backend: the same Nim code compiled to WebAssembly by emscripten and |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 2 | /// driven over `dart:js_interop`. |
| 3 | /// |
| Replace V with Nim 472eb49 nandithebull 8h ago | 4 | /// Selected by the conditional import in `../nimflutter_ffi.dart` when |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 5 | /// `dart.library.js_interop` is available. The native twin is |
| 6 | /// `backend_native.dart`; the two files must keep the same top-level |
| 7 | /// signatures. |
| 8 | /// |
| 9 | /// The glue is emitted by tool/build_wasm.sh into example/web/ and loaded by |
| 10 | /// a <script> tag in example/web/index.html, which defines the global |
| Replace V with Nim 472eb49 nandithebull 8h ago | 11 | /// `createNimFlutterModule`. |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 12 | library; |
| 13 | |
| 14 | import 'dart:async'; |
| 15 | import 'dart:js_interop'; |
| 16 | import 'dart:typed_data'; |
| 17 | |
| 18 | import 'fractal_view.dart'; |
| 19 | |
| 20 | const String backendName = 'wasm (emscripten)'; |
| 21 | |
| Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 7h ago | 22 | /// True: bands render concurrently in Web Workers. |
| 23 | /// |
| 24 | /// The wasm module itself is single-threaded, so the parallelism comes from |
| 25 | /// running several *instances* of it — one per worker, each with its own |
| 26 | /// linear memory. Nothing is shared, which is why this needs no |
| 27 | /// SharedArrayBuffer and no COOP/COEP headers; the cost is one ~34 KB module |
| 28 | /// per worker. |
| 29 | /// |
| 30 | /// Falls back to sequential rendering if nimflutter_pool.js is not loaded. |
| 31 | bool get supportsIsolateParallelism => _pool != null; |
| 32 | |
| 33 | /// What one unit of parallelism is called here. Purely for display. |
| 34 | const String parallelUnitName = 'worker'; |
| 35 | |
| 36 | @JS('nimflutterRenderParallel') |
| 37 | external JSFunction? get _pool; |
| 38 | |
| 39 | @JS('nimflutterRenderParallel') |
| 40 | external JSPromise<JSUint8Array> _renderParallelInWorkers(int w, int h, |
| 41 | double cx, double cy, double scale, int iter, int tiles); |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 42 | |
| Replace V with Nim 472eb49 nandithebull 8h ago | 43 | @JS('createNimFlutterModule') |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 44 | external JSFunction? get _factory; |
| 45 | |
| 46 | /// The emscripten module object. Only the pieces this package uses are |
| 47 | /// declared; `_`-prefixed members are the C exports. |
| 48 | extension type _Module._(JSObject _) implements JSObject { |
| 49 | @JS('_malloc') |
| 50 | external int malloc(int bytes); |
| 51 | |
| 52 | @JS('_free') |
| 53 | external void free(int ptr); |
| 54 | |
| Replace V with Nim 472eb49 nandithebull 8h ago | 55 | @JS('_nf_init') |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 56 | external void vfInit(); |
| 57 | |
| Replace V with Nim 472eb49 nandithebull 8h ago | 58 | @JS('_nf_add') |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 59 | external int vfAdd(int a, int b); |
| 60 | |
| Replace V with Nim 472eb49 nandithebull 8h ago | 61 | @JS('_nf_greet') |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 62 | external int vfGreet(int namePtr); |
| 63 | |
| Replace V with Nim 472eb49 nandithebull 8h ago | 64 | @JS('_nf_free') |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 65 | external void vfFree(int ptr); |
| 66 | |
| Replace V with Nim 472eb49 nandithebull 8h ago | 67 | @JS('_nf_mandelbrot') |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 68 | external void vfMandelbrot(int buf, int bufLen, int w, int h, double cx, |
| 69 | double cy, double scale, int maxIter, int y0, int y1); |
| 70 | |
| 71 | /// Re-read on every use: emscripten replaces the view when the heap grows. |
| 72 | external JSUint8Array get HEAPU8; |
| 73 | |
| 74 | external String UTF8ToString(int ptr); |
| 75 | external void stringToUTF8(String s, int ptr, int maxBytesToWrite); |
| 76 | external int lengthBytesUTF8(String s); |
| 77 | } |
| 78 | |
| 79 | extension type _U8Array._(JSObject _) implements JSObject { |
| 80 | external JSUint8Array subarray(int begin, int end); |
| 81 | } |
| 82 | |
| 83 | _Module? _module; |
| 84 | |
| 85 | bool get isInitialized => _module != null; |
| 86 | |
| 87 | Future<void>? _pending; |
| 88 | |
| 89 | /// Instantiates the wasm module. Safe to call repeatedly and from several |
| 90 | /// places at once; the work happens once. |
| 91 | Future<void> initialize() { |
| 92 | if (_module != null) return Future<void>.value(); |
| 93 | return _pending ??= _load(); |
| 94 | } |
| 95 | |
| 96 | Future<void> _load() async { |
| 97 | final factory = _factory; |
| 98 | if (factory == null) { |
| 99 | throw StateError( |
| Replace V with Nim 472eb49 nandithebull 8h ago | 100 | 'createNimFlutterModule is not defined. The wasm glue is missing: run ' |
| 101 | '`just wasm` and make sure web/index.html loads nimflutter.js before ' |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 102 | 'flutter_bootstrap.js.', |
| 103 | ); |
| 104 | } |
| 105 | final module = await (factory.callAsFunction() as JSPromise<JSObject>).toDart; |
| 106 | final m = _Module._(module); |
| 107 | m.vfInit(); |
| 108 | _module = m; |
| 109 | _pending = null; |
| 110 | } |
| 111 | |
| 112 | _Module get _m { |
| 113 | final m = _module; |
| 114 | if (m == null) { |
| 115 | throw StateError( |
| Replace V with Nim 472eb49 nandithebull 8h ago | 116 | 'nimflutter_ffi is not initialised on web. Await initialize() before ' |
| 117 | 'calling into Nim — loading a wasm module cannot be synchronous.', |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 118 | ); |
| 119 | } |
| 120 | return m; |
| 121 | } |
| 122 | |
| 123 | /// Present so callers written against the native backend keep compiling. On |
| 124 | /// web it only asserts; it cannot do the loading, because that is async. |
| 125 | void ensureInitialized() => _m; |
| 126 | |
| 127 | int add(int a, int b) => _m.vfAdd(a, b); |
| 128 | |
| 129 | String greet(String name) { |
| 130 | final m = _m; |
| 131 | // stringToUTF8 needs room for the trailing NUL. |
| 132 | final size = m.lengthBytesUTF8(name) + 1; |
| 133 | final arg = m.malloc(size); |
| 134 | var res = 0; |
| 135 | try { |
| 136 | m.stringToUTF8(name, arg, size); |
| 137 | res = m.vfGreet(arg); |
| 138 | if (res == 0) { |
| Replace V with Nim 472eb49 nandithebull 8h ago | 139 | throw StateError('nf_greet returned null'); |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 140 | } |
| 141 | return m.UTF8ToString(res); |
| 142 | } finally { |
| 143 | m.free(arg); |
| Replace V with Nim 472eb49 nandithebull 8h ago | 144 | // Nim allocated the result, so Nim releases it — same rule as on native. |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 145 | if (res != 0) m.vfFree(res); |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | /// No isolates on web, so this is [greet] behind a `Future`. It does not move |
| 150 | /// the work off the UI thread. |
| 151 | Future<String> greetAsync(String name) async => greet(name); |
| 152 | |
| 153 | Uint8List renderBand(FractalView view, int y0, int y1) { |
| 154 | final m = _m; |
| 155 | final rows = y1 - y0; |
| 156 | if (rows <= 0) return Uint8List(0); |
| 157 | |
| 158 | final bytes = rows * view.width * 4; |
| 159 | final buf = m.malloc(bytes); |
| 160 | try { |
| 161 | m.vfMandelbrot(buf, bytes, view.width, view.height, view.centerX, |
| 162 | view.centerY, view.scale, view.maxIter, y0, y1); |
| 163 | // Take a view of just this band and copy it into Dart before the wasm |
| 164 | // allocation is released. Reading m.HEAPU8 here rather than earlier |
| 165 | // matters: malloc may have grown the heap and replaced the view. |
| 166 | final band = _U8Array._(m.HEAPU8).subarray(buf, buf + bytes).toDart; |
| 167 | return Uint8List.fromList(band); |
| 168 | } finally { |
| 169 | m.free(buf); |
| 170 | } |
| 171 | } |
| 172 | |
| Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 7h ago | 173 | /// Renders the frame across the worker pool, falling back to a sequential |
| 174 | /// split if the pool script is absent. |
| 175 | /// |
| 176 | /// The split is identical to the native backend's, and so is the output: the |
| 177 | /// bands are the same bands, just computed in other threads. |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 178 | Future<Uint8List> renderParallel(FractalView view, int tiles) async { |
| 179 | final count = tiles.clamp(1, view.height); |
| 180 | |
| Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 7h ago | 181 | if (_pool != null && count > 1) { |
| 182 | final result = await _renderParallelInWorkers( |
| 183 | view.width, |
| 184 | view.height, |
| 185 | view.centerX, |
| 186 | view.centerY, |
| 187 | view.scale, |
| 188 | view.maxIter, |
| 189 | count, |
| 190 | ).toDart; |
| 191 | return result.toDart; |
| 192 | } |
| 193 | |
| 194 | final rowsPer = (view.height / count).ceil(); |
| Add a WebAssembly backend for the example 728acaa nandithebull 8h ago | 195 | final out = Uint8List(view.width * view.height * 4); |
| 196 | var offset = 0; |
| 197 | for (var t = 0; t < count; t++) { |
| 198 | final y0 = t * rowsPer; |
| 199 | final y1 = ((t + 1) * rowsPer).clamp(0, view.height); |
| 200 | if (y0 >= y1) break; |
| 201 | final band = renderBand(view, y0, y1); |
| 202 | out.setRange(offset, offset + band.length, band); |
| 203 | offset += band.length; |
| 204 | } |
| 205 | return out; |
| 206 | } |