## 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