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
|
## C-ABI surface consumed by Dart FFI.
##
## Rules for everything below:
## * only C-compatible types cross the boundary (cint, cdouble, cstring,
## pointer) — never a Nim string, seq, ref or object
## * anything Nim allocates and hands out is released by nf_free
## * allocations that cross the boundary use the *shared* heap, because Dart
## calls in from many isolates and each one is its own OS thread
import std/math
var initialized {.global.} = false
proc NimMain() {.importc.}
proc nf_init() {.exportc: "nf_init", dynlib, cdecl.} =
## Initialises the Nim runtime. Idempotent and cheap.
##
## Shared builds run NimMain from a library constructor on ELF/Mach-O, but
## static archives (iOS) have no such hook, so the caller needs a guaranteed
## entry point.
if not initialized:
NimMain()
initialized = true
proc nf_add(a, b: cint): cint {.exportc: "nf_add", dynlib, cdecl.} =
a + b
proc nf_greet(name: cstring): cstring {.exportc: "nf_greet", dynlib, cdecl.} =
## Returns a shared-heap C string. Caller releases it with nf_free.
let greeting = "Hello, " & $name & ", from Nim!"
# allocShared0, not alloc0: the Dart side may free this from a different
# isolate than the one that allocated it, and Nim's default heap is
# thread-local.
let buf = cast[cstring](allocShared0(greeting.len + 1))
copyMem(buf, greeting.cstring, greeting.len)
buf
proc nf_free(p: pointer) {.exportc: "nf_free", dynlib, cdecl.} =
if p != nil:
deallocShared(p)
proc nf_mandelbrot(buf: ptr UncheckedArray[uint8], bufLen: csize_t,
w, h: cint, cx, cy, scale: cdouble, maxIter, y0, y1: cint)
{.exportc: "nf_mandelbrot", dynlib, cdecl.} =
## Fills rows [y0, y1) of a w x h image as RGBA8888, packed from the start
## of `buf`.
##
## The buffer belongs to the *caller*: nothing is allocated here, so there is
## nothing to nf_free, and each call touches only its own rows — which is
## what makes concurrent calls from several isolates safe.
##
## `bufLen` is the caller's own statement of how many bytes `buf` holds, and
## every rejection below is all-or-nothing: a short buffer writes *nothing*
## rather than filling what fits. Partial output would be indistinguishable
## from a rendered frame.
if buf == nil or w <= 0 or h <= 0 or maxIter <= 0 or y0 < 0 or y1 <= y0:
return
# Widen before multiplying. The required size is computed in 64-bit because
# (y1 - y0) * w * 4 overflows int32 for perfectly ordinary-looking geometry
# — w = 2^30 wraps to exactly 0, which a length check alone would accept.
let needed = (y1 - y0).int64 * w.int64 * 4'i64
if needed <= 0 or needed > bufLen.int64:
return
let
aspect = w.float64 / h.float64
invW = 1.0 / w.float64
invH = 1.0 / h.float64
for y in y0 ..< y1:
let
im = cy + (y.float64 * invH - 0.5) * scale
row = (y - y0) * w * 4
for x in 0 ..< w:
let re = cx + (x.float64 * invW - 0.5) * scale * aspect
var
zr = 0.0
zi = 0.0
zr2 = 0.0
zi2 = 0.0
i: cint = 0
while i < maxIter:
zr2 = zr * zr
zi2 = zi * zi
if zr2 + zi2 > 4.0:
break
zi = 2.0 * zr * zi + im
zr = zr2 - zi2 + re
inc i
let idx = row + x * 4
if i >= maxIter:
# Inside the set.
buf[idx] = 0'u8
buf[idx + 1] = 0'u8
buf[idx + 2] = 0'u8
buf[idx + 3] = 255'u8
continue
# Smooth (fractional) escape count, so bands don't posterise.
let mag = sqrt(zr2 + zi2)
var nu = i.float64
if mag > 1.0:
nu = i.float64 + 1.0 - ln(ln(mag) / ln(2.0)) / ln(2.0)
let t = nu / maxIter.float64
buf[idx] = uint8(255.0 * (0.5 + 0.5 * sin(3.0 + t * 18.0)))
buf[idx + 1] = uint8(255.0 * (0.5 + 0.5 * sin(3.6 + t * 18.0)))
buf[idx + 2] = uint8(255.0 * (0.5 + 0.5 * sin(4.2 + t * 18.0)))
buf[idx + 3] = 255'u8
|