nandi/vflutter_ffipublic Fork 0
4506a586d7400b00d732dcb97167be75ab41cab1
Commits
Clone
git clone https://git.rickub.com/nandi/vflutter_ffi.git
git clone ssh://git@rickub.com/nandi/vflutter_ffi.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Render in parallel on web, via a pool of Web Workers 4506a58 · on 4506a586d7400b00d732dcb97167be75ab41cab1 · nandithebull · 19h ago
backend_web.dart · 206 lines · 6.2 KBDart Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/// Web backend: the same Nim code compiled to WebAssembly by emscripten and
/// driven over `dart:js_interop`.
///
/// Selected by the conditional import in `../nimflutter_ffi.dart` when
/// `dart.library.js_interop` is available. The native twin is
/// `backend_native.dart`; the two files must keep the same top-level
/// signatures.
///
/// The glue is emitted by tool/build_wasm.sh into example/web/ and loaded by
/// a <script> tag in example/web/index.html, which defines the global
/// `createNimFlutterModule`.
library;

import 'dart:async';
import 'dart:js_interop';
import 'dart:typed_data';

import 'fractal_view.dart';

const String backendName = 'wasm (emscripten)';

/// True: bands render concurrently in Web Workers.
///
/// The wasm module itself is single-threaded, so the parallelism comes from
/// running several *instances* of it — one per worker, each with its own
/// linear memory. Nothing is shared, which is why this needs no
/// SharedArrayBuffer and no COOP/COEP headers; the cost is one ~34 KB module
/// per worker.
///
/// Falls back to sequential rendering if nimflutter_pool.js is not loaded.
bool get supportsIsolateParallelism => _pool != null;

/// What one unit of parallelism is called here. Purely for display.
const String parallelUnitName = 'worker';

@JS('nimflutterRenderParallel')
external JSFunction? get _pool;

@JS('nimflutterRenderParallel')
external JSPromise<JSUint8Array> _renderParallelInWorkers(int w, int h,
    double cx, double cy, double scale, int iter, int tiles);

@JS('createNimFlutterModule')
external JSFunction? get _factory;

/// The emscripten module object. Only the pieces this package uses are
/// declared; `_`-prefixed members are the C exports.
extension type _Module._(JSObject _) implements JSObject {
  @JS('_malloc')
  external int malloc(int bytes);

  @JS('_free')
  external void free(int ptr);

  @JS('_nf_init')
  external void vfInit();

  @JS('_nf_add')
  external int vfAdd(int a, int b);

  @JS('_nf_greet')
  external int vfGreet(int namePtr);

  @JS('_nf_free')
  external void vfFree(int ptr);

  @JS('_nf_mandelbrot')
  external void vfMandelbrot(int buf, int bufLen, int w, int h, double cx,
      double cy, double scale, int maxIter, int y0, int y1);

  /// Re-read on every use: emscripten replaces the view when the heap grows.
  external JSUint8Array get HEAPU8;

  external String UTF8ToString(int ptr);
  external void stringToUTF8(String s, int ptr, int maxBytesToWrite);
  external int lengthBytesUTF8(String s);
}

extension type _U8Array._(JSObject _) implements JSObject {
  external JSUint8Array subarray(int begin, int end);
}

_Module? _module;

bool get isInitialized => _module != null;

Future<void>? _pending;

/// Instantiates the wasm module. Safe to call repeatedly and from several
/// places at once; the work happens once.
Future<void> initialize() {
  if (_module != null) return Future<void>.value();
  return _pending ??= _load();
}

Future<void> _load() async {
  final factory = _factory;
  if (factory == null) {
    throw StateError(
      'createNimFlutterModule is not defined. The wasm glue is missing: run '
      '`just wasm` and make sure web/index.html loads nimflutter.js before '
      'flutter_bootstrap.js.',
    );
  }
  final module = await (factory.callAsFunction() as JSPromise<JSObject>).toDart;
  final m = _Module._(module);
  m.vfInit();
  _module = m;
  _pending = null;
}

_Module get _m {
  final m = _module;
  if (m == null) {
    throw StateError(
      'nimflutter_ffi is not initialised on web. Await initialize() before '
      'calling into Nim — loading a wasm module cannot be synchronous.',
    );
  }
  return m;
}

/// Present so callers written against the native backend keep compiling. On
/// web it only asserts; it cannot do the loading, because that is async.
void ensureInitialized() => _m;

int add(int a, int b) => _m.vfAdd(a, b);

String greet(String name) {
  final m = _m;
  // stringToUTF8 needs room for the trailing NUL.
  final size = m.lengthBytesUTF8(name) + 1;
  final arg = m.malloc(size);
  var res = 0;
  try {
    m.stringToUTF8(name, arg, size);
    res = m.vfGreet(arg);
    if (res == 0) {
      throw StateError('nf_greet returned null');
    }
    return m.UTF8ToString(res);
  } finally {
    m.free(arg);
    // Nim allocated the result, so Nim releases it — same rule as on native.
    if (res != 0) m.vfFree(res);
  }
}

/// No isolates on web, so this is [greet] behind a `Future`. It does not move
/// the work off the UI thread.
Future<String> greetAsync(String name) async => greet(name);

Uint8List renderBand(FractalView view, int y0, int y1) {
  final m = _m;
  final rows = y1 - y0;
  if (rows <= 0) return Uint8List(0);

  final bytes = rows * view.width * 4;
  final buf = m.malloc(bytes);
  try {
    m.vfMandelbrot(buf, bytes, view.width, view.height, view.centerX,
        view.centerY, view.scale, view.maxIter, y0, y1);
    // Take a view of just this band and copy it into Dart before the wasm
    // allocation is released. Reading m.HEAPU8 here rather than earlier
    // matters: malloc may have grown the heap and replaced the view.
    final band = _U8Array._(m.HEAPU8).subarray(buf, buf + bytes).toDart;
    return Uint8List.fromList(band);
  } finally {
    m.free(buf);
  }
}

/// Renders the frame across the worker pool, falling back to a sequential
/// split if the pool script is absent.
///
/// The split is identical to the native backend's, and so is the output: the
/// bands are the same bands, just computed in other threads.
Future<Uint8List> renderParallel(FractalView view, int tiles) async {
  final count = tiles.clamp(1, view.height);

  if (_pool != null && count > 1) {
    final result = await _renderParallelInWorkers(
      view.width,
      view.height,
      view.centerX,
      view.centerY,
      view.scale,
      view.maxIter,
      count,
    ).toDart;
    return result.toDart;
  }

  final rowsPer = (view.height / count).ceil();
  final out = Uint8List(view.width * view.height * 4);
  var offset = 0;
  for (var t = 0; t < count; t++) {
    final y0 = t * rowsPer;
    final y1 = ((t + 1) * rowsPer).clamp(0, view.height);
    if (y0 >= y1) break;
    final band = renderBand(view, y0, y1);
    out.setRange(offset, offset + band.length, band);
    offset += band.length;
  }
  return out;
}