| Replace V with Nim 472eb49 nandithebull 9h ago | 1 | // A Mandelbrot explorer whose pixels are computed in Nim. |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 2 | // |
| 3 | // What this is actually demonstrating: |
| 4 | // * bulk data across the FFI boundary — a full RGBA frame per render, |
| Replace V with Nim 472eb49 nandithebull 9h ago | 5 | // into a buffer Dart allocates and Nim fills in place (nothing to nf_free) |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 6 | // * band parallelism — the image is split into horizontal strips rendered |
| 7 | // on separate isolates, which is safe because the library is -gc none |
| Replace V with Nim 472eb49 nandithebull 9h ago | 8 | // * the same algorithm written twice, in Nim and in Dart, so the cost of the |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 9 | // bridge is a number on screen rather than a claim |
| 10 | import 'dart:async'; |
| 11 | import 'dart:math' as math; |
| 12 | import 'dart:typed_data'; |
| 13 | import 'dart:ui' as ui; |
| 14 | |
| 15 | import 'package:flutter/gestures.dart'; |
| 16 | import 'package:flutter/material.dart'; |
| Replace V with Nim 472eb49 nandithebull 9h ago | 17 | import 'package:nimflutter_ffi/nimflutter_ffi.dart' as v; |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 18 | |
| 19 | import 'mandelbrot_dart.dart'; |
| 20 | |
| 21 | void main() => runApp(const ExampleApp()); |
| 22 | |
| 23 | class ExampleApp extends StatelessWidget { |
| 24 | const ExampleApp({super.key}); |
| 25 | |
| 26 | @override |
| 27 | Widget build(BuildContext context) => MaterialApp( |
| Replace V with Nim 472eb49 nandithebull 9h ago | 28 | title: 'nimflutter_ffi', |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 29 | debugShowCheckedModeBanner: false, |
| 30 | theme: ThemeData.dark(useMaterial3: true).copyWith( |
| 31 | scaffoldBackgroundColor: const Color(0xFF12141A), |
| 32 | ), |
| 33 | home: const ExplorerPage(), |
| 34 | ); |
| 35 | } |
| 36 | |
| 37 | class ExplorerPage extends StatefulWidget { |
| 38 | const ExplorerPage({super.key}); |
| 39 | |
| 40 | @override |
| 41 | State<ExplorerPage> createState() => _ExplorerPageState(); |
| 42 | } |
| 43 | |
| Add drag-to-pan 0ed90f3 nandithebull 11h ago | 44 | /// Identifies the interactive canvas, so tests can drive gestures on it |
| 45 | /// without depending on whether a frame has finished rendering yet. |
| 46 | const Key canvasKey = Key('fractal-canvas'); |
| 47 | |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 48 | class _ExplorerPageState extends State<ExplorerPage> { |
| 49 | static const int _renderWidth = 720; |
| 50 | static const int _renderHeight = 540; |
| 51 | |
| 52 | v.FractalView _view = const v.FractalView( |
| 53 | width: _renderWidth, |
| 54 | height: _renderHeight, |
| 55 | maxIter: 400, |
| 56 | ); |
| 57 | |
| 58 | ui.Image? _image; |
| 59 | bool _rendering = false; |
| 60 | bool _renderQueued = false; |
| 61 | int _lastMs = 0; |
| Raise the isolate ceiling to 32, and add the sweep that justifies it 59a731a nandithebull 11h ago | 62 | int _tiles = 16; |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 63 | _Benchmark? _benchmark; |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 64 | String? _bootError; |
| 65 | |
| 66 | /// Bands run on isolates natively and sequentially on web, so the word the |
| 67 | /// UI uses for them has to follow the backend. |
| Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 8h ago | 68 | String get _unit => |
| 69 | v.supportsIsolateParallelism ? v.parallelUnitName : 'band'; |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 70 | |
| 71 | @override |
| 72 | void initState() { |
| 73 | super.initState(); |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 74 | _boot(); |
| 75 | } |
| 76 | |
| 77 | /// Brings the bridge up and draws the first frame. |
| 78 | Future<void> _boot() => _render(); |
| 79 | |
| 80 | /// Waits for the bridge, returning false if it could not be brought up. |
| 81 | /// |
| 82 | /// On native this resolves immediately. On web it is where the wasm module |
| 83 | /// is fetched and instantiated, which cannot be synchronous. |
| 84 | /// |
| 85 | /// Every path that asks for a frame goes through here, not just startup: |
| 86 | /// the first layout pass calls _fitToCanvas -> _render before initState's |
| 87 | /// future has resolved, so gating only the boot path would still let the |
| 88 | /// very first render race the module load. initialize() is idempotent and |
| 89 | /// hands back the in-flight future, so the extra calls just wait. |
| 90 | Future<bool> _ensureReady() async { |
| 91 | if (v.isInitialized) return true; |
| 92 | try { |
| 93 | await v.initialize(); |
| 94 | return true; |
| 95 | } catch (e) { |
| 96 | if (mounted) setState(() => _bootError = '$e'); |
| 97 | return false; |
| 98 | } |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 99 | } |
| 100 | |
| 101 | @override |
| 102 | void dispose() { |
| 103 | _image?.dispose(); |
| 104 | super.dispose(); |
| 105 | } |
| 106 | |
| 107 | /// Renders the current view, coalescing requests that arrive mid-frame. |
| 108 | /// |
| 109 | /// A wheel gesture emits far more events than we can render. Dropping them |
| 110 | /// outright would leave the view mutated but never drawn, so instead the |
| 111 | /// latest request is remembered and serviced once the in-flight frame lands. |
| 112 | Future<void> _render() async { |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 113 | if (!await _ensureReady()) return; |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 114 | if (_rendering) { |
| 115 | _renderQueued = true; |
| 116 | return; |
| 117 | } |
| 118 | setState(() => _rendering = true); |
| 119 | |
| 120 | do { |
| 121 | _renderQueued = false; |
| 122 | final view = _view; |
| 123 | |
| 124 | final watch = Stopwatch()..start(); |
| 125 | final pixels = _tiles > 1 |
| 126 | ? await v.renderParallel(view, tiles: _tiles) |
| 127 | : v.render(view); |
| 128 | watch.stop(); |
| 129 | |
| 130 | final image = await _decode(pixels, view.width, view.height); |
| 131 | if (!mounted) { |
| 132 | image.dispose(); |
| 133 | return; |
| 134 | } |
| 135 | setState(() { |
| 136 | _image?.dispose(); |
| 137 | _image = image; |
| 138 | _lastMs = watch.elapsedMilliseconds; |
| 139 | }); |
| 140 | } while (_renderQueued); |
| 141 | |
| 142 | if (!mounted) return; |
| 143 | setState(() => _rendering = false); |
| 144 | } |
| 145 | |
| 146 | static Future<ui.Image> _decode(Uint8List rgba, int w, int h) { |
| 147 | final completer = Completer<ui.Image>(); |
| 148 | ui.decodeImageFromPixels(rgba, w, h, ui.PixelFormat.rgba8888, |
| 149 | completer.complete); |
| 150 | return completer.future; |
| 151 | } |
| 152 | |
| Add drag-to-pan 0ed90f3 nandithebull 11h ago | 153 | /// Drags the view by a pixel delta, so the image follows the cursor. |
| 154 | void _panBy(Offset delta, Size widgetSize) { |
| 155 | if (widgetSize.isEmpty || delta == Offset.zero) return; |
| 156 | setState(() { |
| 157 | _view = _view.pannedBy( |
| 158 | delta.dx / widgetSize.width, |
| 159 | delta.dy / widgetSize.height, |
| 160 | ); |
| 161 | }); |
| 162 | _render(); |
| 163 | } |
| 164 | |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 165 | /// Zooms about a point, keeping that point under the cursor. |
| 166 | void _zoomAt(Offset local, Size widgetSize, double factor) { |
| 167 | if (widgetSize.isEmpty) return; |
| 168 | setState(() { |
| 169 | _view = _view.zoomedAt( |
| 170 | local.dx / widgetSize.width, |
| 171 | local.dy / widgetSize.height, |
| 172 | factor, |
| 173 | ); |
| 174 | }); |
| 175 | _render(); |
| 176 | } |
| 177 | |
| 178 | /// Restores the starting framing. |
| 179 | /// |
| 180 | /// Only the pan/zoom is reset. Width and height belong to the canvas (see |
| 181 | /// _fitToCanvas) and maxIter belongs to its slider — clobbering those made |
| 182 | /// "Reset view" silently undo settings the button does not own, and briefly |
| 183 | /// render at the wrong aspect ratio before the canvas corrected it. |
| 184 | void _reset() { |
| 185 | const home = v.FractalView(width: 1, height: 1); |
| 186 | setState(() { |
| 187 | _view = _view.copyWith( |
| 188 | centerX: home.centerX, |
| 189 | centerY: home.centerY, |
| 190 | scale: home.scale, |
| 191 | ); |
| 192 | }); |
| 193 | _render(); |
| 194 | } |
| 195 | |
| Replace V with Nim 472eb49 nandithebull 9h ago | 196 | /// Renders the identical frame in Nim and in Dart on this isolate, so the |
| 197 | /// comparison is like-for-like rather than Nim-plus-parallelism vs Dart. |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 198 | Future<void> _runBenchmark() async { |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 199 | if (!await _ensureReady()) return; |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 200 | setState(() { |
| 201 | _rendering = true; |
| 202 | _benchmark = null; |
| 203 | }); |
| 204 | |
| 205 | final swV = Stopwatch()..start(); |
| 206 | v.render(_view); |
| 207 | swV.stop(); |
| 208 | |
| 209 | final swP = Stopwatch()..start(); |
| 210 | await v.renderParallel(_view, tiles: _tiles); |
| 211 | swP.stop(); |
| 212 | |
| 213 | final swD = Stopwatch()..start(); |
| 214 | renderDart(_view); |
| 215 | swD.stop(); |
| 216 | |
| 217 | if (!mounted) return; |
| 218 | setState(() { |
| 219 | _rendering = false; |
| 220 | _benchmark = _Benchmark( |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 221 | unit: _unit, |
| 222 | parallel: v.supportsIsolateParallelism, |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 223 | vSerialMs: swV.elapsedMilliseconds, |
| 224 | vParallelMs: swP.elapsedMilliseconds, |
| 225 | dartMs: swD.elapsedMilliseconds, |
| 226 | tiles: _tiles, |
| 227 | ); |
| 228 | }); |
| 229 | } |
| 230 | |
| 231 | @override |
| 232 | Widget build(BuildContext context) { |
| 233 | return Scaffold( |
| 234 | body: SafeArea( |
| 235 | child: LayoutBuilder( |
| 236 | builder: (context, constraints) { |
| 237 | final wide = constraints.maxWidth > 900; |
| 238 | final canvas = _buildCanvas(); |
| 239 | final controls = _buildControls(); |
| 240 | return wide |
| 241 | ? Row( |
| 242 | crossAxisAlignment: CrossAxisAlignment.stretch, |
| 243 | children: [ |
| 244 | Expanded(child: canvas), |
| 245 | SizedBox( |
| 246 | width: 340, |
| 247 | child: SingleChildScrollView(child: controls), |
| 248 | ), |
| 249 | ], |
| 250 | ) |
| 251 | : ListView( |
| 252 | children: [ |
| 253 | AspectRatio( |
| 254 | aspectRatio: _renderWidth / _renderHeight, |
| 255 | child: canvas, |
| 256 | ), |
| 257 | controls, |
| 258 | ], |
| 259 | ); |
| 260 | }, |
| 261 | ), |
| 262 | ), |
| 263 | ); |
| 264 | } |
| 265 | |
| 266 | /// Keeps the rendered buffer at the canvas's aspect ratio. |
| 267 | /// |
| 268 | /// Without this the image would be letterboxed or cropped, and the |
| 269 | /// click-to-zoom maths — which maps widget coordinates straight into the |
| 270 | /// complex plane — would point at the wrong place. |
| 271 | void _fitToCanvas(Size size) { |
| 272 | if (size.isEmpty) return; |
| 273 | final height = (_renderWidth * size.height / size.width).round(); |
| 274 | if ((height - _view.height).abs() <= 2) return; |
| 275 | WidgetsBinding.instance.addPostFrameCallback((_) { |
| 276 | if (!mounted) return; |
| 277 | setState(() => _view = _view.copyWith(height: height)); |
| 278 | _render(); |
| 279 | }); |
| 280 | } |
| 281 | |
| 282 | Widget _buildCanvas() { |
| 283 | return LayoutBuilder( |
| 284 | builder: (context, constraints) { |
| 285 | final size = Size(constraints.maxWidth, constraints.maxHeight); |
| 286 | _fitToCanvas(size); |
| 287 | return Listener( |
| Add drag-to-pan 0ed90f3 nandithebull 11h ago | 288 | key: canvasKey, |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 289 | // Wheel zoom, anchored at the cursor like the tap zoom. One notch is |
| 290 | // a small step so a scroll feels continuous rather than octave-wise; |
| 291 | // the render loop coalesces whatever it cannot keep up with. |
| 292 | onPointerSignal: (event) { |
| 293 | if (event is! PointerScrollEvent) return; |
| 294 | final dy = event.scrollDelta.dy; |
| 295 | if (dy == 0) return; |
| 296 | _zoomAt(event.localPosition, size, math.pow(1.0015, dy).toDouble()); |
| 297 | }, |
| 298 | child: GestureDetector( |
| Add drag-to-pan 0ed90f3 nandithebull 11h ago | 299 | // Zoom on tap up rather than tap down. The gesture arena already |
| 300 | // keeps onTapDown from firing once a pan claims the pointer, so |
| 301 | // this is not what makes drag-to-pan work — it just means the |
| 302 | // zoom commits on a completed tap. |
| 303 | onTapUp: (d) => _zoomAt(d.localPosition, size, 0.5), |
| 304 | onSecondaryTapUp: (d) => _zoomAt(d.localPosition, size, 2.0), |
| 305 | onPanUpdate: (d) => _panBy(d.delta, size), |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 306 | child: Stack( |
| 307 | fit: StackFit.expand, |
| 308 | children: [ |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 309 | if (_bootError != null) |
| 310 | // Most likely on web: the wasm glue did not load, so there is |
| Replace V with Nim 472eb49 nandithebull 9h ago | 311 | // no Nim to call. Say so rather than spinning forever. |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 312 | Center( |
| 313 | child: Padding( |
| 314 | padding: const EdgeInsets.all(24), |
| 315 | child: Text( |
| Replace V with Nim 472eb49 nandithebull 9h ago | 316 | 'Could not start the Nim bridge.\n\n$_bootError', |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 317 | textAlign: TextAlign.center, |
| 318 | style: Theme.of(context).textTheme.bodySmall, |
| 319 | ), |
| 320 | ), |
| 321 | ) |
| 322 | else if (_image != null) |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 323 | RawImage(image: _image, fit: BoxFit.fill) |
| 324 | else |
| 325 | const Center(child: CircularProgressIndicator()), |
| 326 | Positioned( |
| 327 | left: 12, |
| 328 | bottom: 12, |
| 329 | child: _Chip( |
| 330 | 'zoom ${(3.0 / _view.scale).toStringAsFixed(1)}x · ' |
| 331 | '$_lastMs ms · ' |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 332 | '${_tiles > 1 ? "$_tiles ${_unit}s" : "1 $_unit"}', |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 333 | ), |
| 334 | ), |
| 335 | if (_rendering) |
| 336 | const Positioned( |
| 337 | right: 12, |
| 338 | top: 12, |
| 339 | child: SizedBox( |
| 340 | width: 18, |
| 341 | height: 18, |
| 342 | child: CircularProgressIndicator(strokeWidth: 2), |
| 343 | ), |
| 344 | ), |
| 345 | ], |
| 346 | ), |
| 347 | ), |
| 348 | ); |
| 349 | }, |
| 350 | ); |
| 351 | } |
| 352 | |
| 353 | /// Returns a non-scrolling column. |
| 354 | /// |
| 355 | /// The wide layout wraps this in its own scroll view; the narrow layout |
| 356 | /// embeds it in the page's ListView. Making this a ListView itself would |
| 357 | /// nest two scrollables and give the inner one unbounded height. |
| 358 | Widget _buildControls() { |
| 359 | final theme = Theme.of(context); |
| 360 | return Padding( |
| 361 | padding: const EdgeInsets.all(20), |
| 362 | child: Column( |
| 363 | crossAxisAlignment: CrossAxisAlignment.stretch, |
| 364 | children: [ |
| Replace V with Nim 472eb49 nandithebull 9h ago | 365 | Text('Mandelbrot in Nim', style: theme.textTheme.titleLarge), |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 366 | const SizedBox(height: 4), |
| 367 | Text( |
| Add drag-to-pan 0ed90f3 nandithebull 11h ago | 368 | 'Scroll or tap to zoom in, right-click to zoom out, drag to pan. ' |
| Replace V with Nim 472eb49 nandithebull 9h ago | 369 | 'Every pixel is computed by nf_mandelbrot in src/nimflutter.v.', |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 370 | style: theme.textTheme.bodySmall, |
| 371 | ), |
| 372 | const SizedBox(height: 20), |
| 373 | _Labelled( |
| 374 | label: 'iterations', |
| 375 | value: '${_view.maxIter}', |
| 376 | child: Slider( |
| 377 | value: _view.maxIter.toDouble(), |
| 378 | min: 50, |
| 379 | max: 2000, |
| 380 | divisions: 39, |
| 381 | onChanged: (n) => |
| 382 | setState(() => _view = _view.copyWith(maxIter: n.round())), |
| 383 | onChangeEnd: (_) => _render(), |
| 384 | ), |
| 385 | ), |
| 386 | _Labelled( |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 387 | label: '${_unit}s', |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 388 | value: '$_tiles', |
| 389 | child: Slider( |
| 390 | value: _tiles.toDouble(), |
| 391 | min: 1, |
| Raise the isolate ceiling to 32, and add the sweep that justifies it 59a731a nandithebull 11h ago | 392 | // Deliberately well past the core count. Bands cost wildly |
| 393 | // different amounts — rows through the set's interior run the full |
| 394 | // iteration cap, rows in open space escape immediately — so the |
| 395 | // frame waits on its slowest band. Over-decomposing lets a free |
| 396 | // core pick up the next small band instead of idling. Measured on |
| 397 | // an 8-thread machine: 8 tiles 44 ms, 24 tiles 29 ms. |
| 398 | max: 32, |
| 399 | divisions: 31, |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 400 | onChanged: (n) => setState(() => _tiles = n.round()), |
| 401 | onChangeEnd: (_) => _render(), |
| 402 | ), |
| 403 | ), |
| 404 | const SizedBox(height: 8), |
| 405 | Row( |
| 406 | children: [ |
| 407 | Expanded( |
| 408 | child: FilledButton.tonal( |
| 409 | onPressed: _rendering ? null : _reset, |
| 410 | child: const Text('Reset view'), |
| 411 | ), |
| 412 | ), |
| 413 | const SizedBox(width: 12), |
| 414 | Expanded( |
| 415 | child: FilledButton( |
| 416 | onPressed: _rendering ? null : _runBenchmark, |
| 417 | child: const Text('Benchmark'), |
| 418 | ), |
| 419 | ), |
| 420 | ], |
| 421 | ), |
| 422 | const SizedBox(height: 20), |
| 423 | if (_benchmark != null) _BenchmarkCard(result: _benchmark!), |
| 424 | const SizedBox(height: 12), |
| 425 | Card( |
| 426 | color: theme.colorScheme.surfaceContainerHighest, |
| 427 | child: Padding( |
| 428 | padding: const EdgeInsets.all(14), |
| 429 | child: Text( |
| Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 8h ago | 430 | v.backendName == 'dart:ffi' |
| Replace V with Nim 472eb49 nandithebull 9h ago | 431 | ? 'The Nim kernel fills a buffer Dart allocates, so no Nim ' |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 432 | 'memory crosses the boundary and there is nothing to ' |
| 433 | 'free. Bands are independent, which is what makes the ' |
| Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 8h ago | 434 | 'isolate slider safe: ARC has no stop-the-world phase ' |
| 435 | 'to serialise them.' |
| Replace V with Nim 472eb49 nandithebull 9h ago | 436 | : 'The same Nim kernel, compiled to WebAssembly and reached ' |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 437 | 'over dart:js_interop instead of dart:ffi — it fills a ' |
| 438 | 'buffer in the wasm heap, so nothing needs freeing on ' |
| Render in parallel on web, via a pool of Web Workers 4506a58 nandithebull 8h ago | 439 | 'the Dart side. Each Web Worker holds its own instance ' |
| 440 | 'of the module, so nothing is shared and no ' |
| 441 | 'SharedArrayBuffer or COOP/COEP headers are needed. The ' |
| 442 | 'frame is identical to the native build, byte for byte.', |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 443 | style: theme.textTheme.bodySmall, |
| 444 | ), |
| 445 | ), |
| 446 | ), |
| 447 | ], |
| 448 | ), |
| 449 | ); |
| 450 | } |
| 451 | } |
| 452 | |
| 453 | class _Benchmark { |
| 454 | const _Benchmark({ |
| 455 | required this.vSerialMs, |
| 456 | required this.vParallelMs, |
| 457 | required this.dartMs, |
| 458 | required this.tiles, |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 459 | required this.unit, |
| 460 | required this.parallel, |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 461 | }); |
| 462 | |
| 463 | final int vSerialMs; |
| 464 | final int vParallelMs; |
| 465 | final int dartMs; |
| 466 | final int tiles; |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 467 | |
| 468 | /// 'isolate' natively, 'band' on web. |
| 469 | final String unit; |
| 470 | |
| 471 | /// Whether the split actually ran concurrently. |
| 472 | final bool parallel; |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 473 | } |
| 474 | |
| 475 | class _BenchmarkCard extends StatelessWidget { |
| 476 | const _BenchmarkCard({required this.result}); |
| 477 | |
| 478 | final _Benchmark result; |
| 479 | |
| 480 | @override |
| 481 | Widget build(BuildContext context) { |
| 482 | final theme = Theme.of(context); |
| 483 | final ratio = result.dartMs / result.vSerialMs; |
| 484 | return Card( |
| 485 | child: Padding( |
| 486 | padding: const EdgeInsets.all(14), |
| 487 | child: Column( |
| 488 | crossAxisAlignment: CrossAxisAlignment.start, |
| 489 | children: [ |
| 490 | Text('same frame, three ways', |
| 491 | style: theme.textTheme.titleSmall), |
| 492 | const SizedBox(height: 10), |
| Replace V with Nim 472eb49 nandithebull 9h ago | 493 | _Row(label: 'Nim, 1 ${result.unit}', value: '${result.vSerialMs} ms'), |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 494 | _Row( |
| Replace V with Nim 472eb49 nandithebull 9h ago | 495 | label: 'Nim, ${result.tiles} ${result.unit}s', |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 496 | value: '${result.vParallelMs} ms'), |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 497 | _Row(label: 'Dart, 1 ${result.unit}', value: '${result.dartMs} ms'), |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 498 | const Divider(height: 20), |
| 499 | Text( |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 500 | result.parallel |
| Replace V with Nim 472eb49 nandithebull 9h ago | 501 | ? 'Nim is ${ratio.toStringAsFixed(2)}x Dart on one isolate. ' |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 502 | 'For a scalar FP loop the two are close — the real win ' |
| 503 | 'here is the parallel split, which Dart could also do. ' |
| 504 | 'The bridge is what this demonstrates, not a speed claim.' |
| Replace V with Nim 472eb49 nandithebull 9h ago | 505 | : 'Nim is ${ratio.toStringAsFixed(2)}x Dart on one thread. ' |
| Add a WebAssembly backend for the example 728acaa nandithebull 9h ago | 506 | 'The wasm module is single-threaded, so the ' |
| 507 | '${result.tiles}-band row above is the same work done ' |
| 508 | 'in sequence, not in parallel — it is here to show the ' |
| 509 | 'split produces identical pixels either way.', |
| Add a Mandelbrot example app, and a V kernel worth demonstrating 81540bb nandithebull 11h ago | 510 | style: theme.textTheme.bodySmall, |
| 511 | ), |
| 512 | ], |
| 513 | ), |
| 514 | ), |
| 515 | ); |
| 516 | } |
| 517 | } |
| 518 | |
| 519 | class _Row extends StatelessWidget { |
| 520 | const _Row({required this.label, required this.value}); |
| 521 | |
| 522 | final String label; |
| 523 | final String value; |
| 524 | |
| 525 | @override |
| 526 | Widget build(BuildContext context) => Padding( |
| 527 | padding: const EdgeInsets.symmetric(vertical: 2), |
| 528 | child: Row( |
| 529 | mainAxisAlignment: MainAxisAlignment.spaceBetween, |
| 530 | children: [ |
| 531 | Text(label, style: Theme.of(context).textTheme.bodyMedium), |
| 532 | Text(value, |
| 533 | style: Theme.of(context) |
| 534 | .textTheme |
| 535 | .bodyMedium |
| 536 | ?.copyWith(fontFeatures: const [ui.FontFeature.tabularFigures()])), |
| 537 | ], |
| 538 | ), |
| 539 | ); |
| 540 | } |
| 541 | |
| 542 | class _Labelled extends StatelessWidget { |
| 543 | const _Labelled({ |
| 544 | required this.label, |
| 545 | required this.value, |
| 546 | required this.child, |
| 547 | }); |
| 548 | |
| 549 | final String label; |
| 550 | final String value; |
| 551 | final Widget child; |
| 552 | |
| 553 | @override |
| 554 | Widget build(BuildContext context) { |
| 555 | final theme = Theme.of(context); |
| 556 | return Column( |
| 557 | crossAxisAlignment: CrossAxisAlignment.start, |
| 558 | children: [ |
| 559 | Row( |
| 560 | mainAxisAlignment: MainAxisAlignment.spaceBetween, |
| 561 | children: [ |
| 562 | Text(label, style: theme.textTheme.labelLarge), |
| 563 | Text(value, style: theme.textTheme.labelLarge), |
| 564 | ], |
| 565 | ), |
| 566 | child, |
| 567 | ], |
| 568 | ); |
| 569 | } |
| 570 | } |
| 571 | |
| 572 | class _Chip extends StatelessWidget { |
| 573 | const _Chip(this.text); |
| 574 | |
| 575 | final String text; |
| 576 | |
| 577 | @override |
| 578 | Widget build(BuildContext context) => Container( |
| 579 | padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), |
| 580 | decoration: BoxDecoration( |
| 581 | color: Colors.black.withValues(alpha: 0.55), |
| 582 | borderRadius: BorderRadius.circular(8), |
| 583 | ), |
| 584 | child: Text(text, |
| 585 | style: const TextStyle(color: Colors.white, fontSize: 12)), |
| 586 | ); |
| 587 | } |