/// Idiomatic Dart surface over the V library. /// /// Callers never see a Pointer, and never own V memory: every string that /// crosses the boundary is copied into Dart and the V allocation is released /// before the call returns. library vflutter_ffi; import 'dart:ffi'; import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; import 'package:ffi/ffi.dart'; const String _libName = 'vflutter'; DynamicLibrary _open() { if (Platform.isMacOS || Platform.isIOS) { // Static archive linked into the app binary. return DynamicLibrary.process(); } if (Platform.isAndroid || Platform.isLinux) { return DynamicLibrary.open('lib$_libName.so'); } if (Platform.isWindows) { return DynamicLibrary.open('$_libName.dll'); } throw UnsupportedError('vflutter_ffi: unsupported platform'); } final DynamicLibrary _lib = _open(); final void Function() _vfInit = _lib.lookupFunction('vf_init'); final int Function(int, int) _vfAdd = _lib.lookupFunction( 'vf_add'); final Pointer Function(Pointer) _vfGreet = _lib.lookupFunction< Pointer Function(Pointer), Pointer Function(Pointer)>('vf_greet'); final void Function(Pointer) _vfFree = _lib.lookupFunction), void Function(Pointer)>( 'vf_free'); bool _ready = false; /// Initialises the V runtime. Idempotent and cheap; called automatically by /// every API below, so you rarely need it directly. void ensureInitialized() { if (_ready) return; _vfInit(); _ready = true; } /// Adds two integers in V. The trivial case, useful as a liveness check. int add(int a, int b) { ensureInitialized(); return _vfAdd(a, b); } /// Round-trips a string through V. /// /// V allocates the result; this function copies it into a Dart [String] and /// frees the V allocation before returning, so there is nothing to release. String greet(String name) { ensureInitialized(); final arg = name.toNativeUtf8(); Pointer res = nullptr; try { res = _vfGreet(arg); if (res == nullptr) { throw StateError('vf_greet returned null'); } return res.toDartString(); } finally { calloc.free(arg); if (res != nullptr) _vfFree(res.cast()); } } /// Runs [greet] on a helper isolate. /// /// V compiled with `-gc none` has no stop-the-world phase and no thread-local /// runtime state, so calls are safe from any isolate. Use this for work long /// enough to jank a frame. Future greetAsync(String name) => Isolate.run(() => greet(name)); // --------------------------------------------------------------------------- // Mandelbrot // --------------------------------------------------------------------------- final void Function(Pointer, int, int, double, double, double, int, int, int) _vfMandelbrot = _lib.lookupFunction< Void Function(Pointer, Int32, Int32, Double, Double, Double, Int32, Int32, Int32), void Function(Pointer, int, int, double, double, double, int, int, int)>('vf_mandelbrot'); /// Where to look in the complex plane, and how hard to look. class FractalView { const FractalView({ required this.width, required this.height, this.centerX = -0.5, this.centerY = 0.0, this.scale = 3.0, this.maxIter = 500, }); final int width; final int height; /// Centre of the viewport in the complex plane. final double centerX; final double centerY; /// Width of the viewport in the complex plane. Smaller = deeper zoom. final double scale; /// Escape-time iteration cap. The cost of a frame scales with this. final int maxIter; /// Returns this view zoomed by [factor] about a point given in *fractional* /// viewport coordinates — (0,0) top-left, (1,1) bottom-right. /// /// The point under the cursor stays under the cursor: that is the whole /// contract, and it is what makes click-to-zoom feel anchored rather than /// drifting. `factor < 1` zooms in. FractalView zoomedAt(double fx, double fy, double factor) { final aspect = width / height; final ox = fx - 0.5; final oy = fy - 0.5; // The complex-plane point currently under (fx, fy). final targetX = centerX + ox * scale * aspect; final targetY = centerY + oy * scale; final newScale = scale * factor; return copyWith( scale: newScale, centerX: targetX - ox * newScale * aspect, centerY: targetY - oy * newScale, ); } FractalView copyWith({ int? width, int? height, double? centerX, double? centerY, double? scale, int? maxIter, }) => FractalView( width: width ?? this.width, height: height ?? this.height, centerX: centerX ?? this.centerX, centerY: centerY ?? this.centerY, scale: scale ?? this.scale, maxIter: maxIter ?? this.maxIter, ); } /// Renders rows [y0, y1) of [view] into a fresh RGBA byte buffer. /// /// The buffer is allocated on the Dart side and filled in place by V, so no V /// memory is created and nothing needs freeing on the V side. The native /// allocation is released before returning; callers get a plain [Uint8List]. Uint8List renderBand(FractalView view, int y0, int y1) { ensureInitialized(); final rows = y1 - y0; if (rows <= 0) return Uint8List(0); final bytes = rows * view.width * 4; final buf = calloc(bytes); try { _vfMandelbrot(buf, view.width, view.height, view.centerX, view.centerY, view.scale, view.maxIter, y0, y1); // Copy out of native memory before it is freed. return Uint8List.fromList(buf.asTypedList(bytes)); } finally { calloc.free(buf); } } /// Renders the whole of [view] on the calling isolate. Uint8List render(FractalView view) => renderBand(view, 0, view.height); /// Renders [view] as [tiles] horizontal bands computed in parallel. /// /// Bands are independent by construction — each call writes only its own rows /// — so this is real parallelism across isolates, not interleaving. `-gc none` /// means there is no stop-the-world phase to serialise them. Future renderParallel(FractalView view, {int tiles = 4}) async { ensureInitialized(); final count = tiles.clamp(1, view.height); final rowsPer = (view.height / count).ceil(); final futures = >[]; for (var t = 0; t < count; t++) { final y0 = t * rowsPer; final y1 = ((t + 1) * rowsPer).clamp(0, view.height); if (y0 >= y1) break; futures.add(Isolate.run(() => renderBand(view, y0, y1))); } final bands = await Future.wait(futures); final out = Uint8List(view.width * view.height * 4); var offset = 0; for (final band in bands) { out.setRange(offset, offset + band.length, band); offset += band.length; } return out; }