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
207
208
209
210
211
212
213
214
215
216
217
218
|
/// 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<Void Function(), void Function()>('vf_init');
final int Function(int, int) _vfAdd =
_lib.lookupFunction<Int32 Function(Int32, Int32), int Function(int, int)>(
'vf_add');
final Pointer<Utf8> Function(Pointer<Utf8>) _vfGreet = _lib.lookupFunction<
Pointer<Utf8> Function(Pointer<Utf8>),
Pointer<Utf8> Function(Pointer<Utf8>)>('vf_greet');
final void Function(Pointer<Void>) _vfFree =
_lib.lookupFunction<Void Function(Pointer<Void>), void Function(Pointer<Void>)>(
'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<Utf8> 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<String> greetAsync(String name) =>
Isolate.run(() => greet(name));
// ---------------------------------------------------------------------------
// Mandelbrot
// ---------------------------------------------------------------------------
final void Function(Pointer<Uint8>, int, int, double, double, double, int, int,
int) _vfMandelbrot = _lib.lookupFunction<
Void Function(Pointer<Uint8>, Int32, Int32, Double, Double, Double,
Int32, Int32, Int32),
void Function(Pointer<Uint8>, 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<Uint8>(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<Uint8List> renderParallel(FractalView view, {int tiles = 4}) async {
ensureInitialized();
final count = tiles.clamp(1, view.height);
final rowsPer = (view.height / count).ceil();
final futures = <Future<Uint8List>>[];
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;
}
|