| Replace V with Nim 472eb49 nandithebull 8h ago | 1 | ## C-ABI surface consumed by Dart FFI. |
| 2 | ## |
| 3 | ## Rules for everything below: |
| 4 | ## * only C-compatible types cross the boundary (cint, cdouble, cstring, |
| 5 | ## pointer) — never a Nim string, seq, ref or object |
| 6 | ## * anything Nim allocates and hands out is released by nf_free |
| 7 | ## * allocations that cross the boundary use the *shared* heap, because Dart |
| 8 | ## calls in from many isolates and each one is its own OS thread |
| 9 | |
| 10 | import std/math |
| 11 | |
| 12 | var initialized {.global.} = false |
| 13 | |
| 14 | proc NimMain() {.importc.} |
| 15 | |
| 16 | proc nf_init() {.exportc: "nf_init", dynlib, cdecl.} = |
| 17 | ## Initialises the Nim runtime. Idempotent and cheap. |
| 18 | ## |
| 19 | ## Shared builds run NimMain from a library constructor on ELF/Mach-O, but |
| 20 | ## static archives (iOS) have no such hook, so the caller needs a guaranteed |
| 21 | ## entry point. |
| 22 | if not initialized: |
| 23 | NimMain() |
| 24 | initialized = true |
| 25 | |
| 26 | proc nf_add(a, b: cint): cint {.exportc: "nf_add", dynlib, cdecl.} = |
| 27 | a + b |
| 28 | |
| 29 | proc nf_greet(name: cstring): cstring {.exportc: "nf_greet", dynlib, cdecl.} = |
| 30 | ## Returns a shared-heap C string. Caller releases it with nf_free. |
| 31 | let greeting = "Hello, " & $name & ", from Nim!" |
| 32 | # allocShared0, not alloc0: the Dart side may free this from a different |
| 33 | # isolate than the one that allocated it, and Nim's default heap is |
| 34 | # thread-local. |
| 35 | let buf = cast[cstring](allocShared0(greeting.len + 1)) |
| 36 | copyMem(buf, greeting.cstring, greeting.len) |
| 37 | buf |
| 38 | |
| 39 | proc nf_free(p: pointer) {.exportc: "nf_free", dynlib, cdecl.} = |
| 40 | if p != nil: |
| 41 | deallocShared(p) |
| 42 | |
| 43 | proc nf_mandelbrot(buf: ptr UncheckedArray[uint8], bufLen: csize_t, |
| 44 | w, h: cint, cx, cy, scale: cdouble, maxIter, y0, y1: cint) |
| 45 | {.exportc: "nf_mandelbrot", dynlib, cdecl.} = |
| 46 | ## Fills rows [y0, y1) of a w x h image as RGBA8888, packed from the start |
| 47 | ## of `buf`. |
| 48 | ## |
| 49 | ## The buffer belongs to the *caller*: nothing is allocated here, so there is |
| 50 | ## nothing to nf_free, and each call touches only its own rows — which is |
| 51 | ## what makes concurrent calls from several isolates safe. |
| 52 | ## |
| 53 | ## `bufLen` is the caller's own statement of how many bytes `buf` holds, and |
| 54 | ## every rejection below is all-or-nothing: a short buffer writes *nothing* |
| 55 | ## rather than filling what fits. Partial output would be indistinguishable |
| 56 | ## from a rendered frame. |
| 57 | if buf == nil or w <= 0 or h <= 0 or maxIter <= 0 or y0 < 0 or y1 <= y0: |
| 58 | return |
| 59 | |
| 60 | # Widen before multiplying. The required size is computed in 64-bit because |
| 61 | # (y1 - y0) * w * 4 overflows int32 for perfectly ordinary-looking geometry |
| 62 | # — w = 2^30 wraps to exactly 0, which a length check alone would accept. |
| 63 | let needed = (y1 - y0).int64 * w.int64 * 4'i64 |
| 64 | if needed <= 0 or needed > bufLen.int64: |
| 65 | return |
| 66 | |
| 67 | let |
| 68 | aspect = w.float64 / h.float64 |
| 69 | invW = 1.0 / w.float64 |
| 70 | invH = 1.0 / h.float64 |
| 71 | |
| 72 | for y in y0 ..< y1: |
| 73 | let |
| 74 | im = cy + (y.float64 * invH - 0.5) * scale |
| 75 | row = (y - y0) * w * 4 |
| 76 | for x in 0 ..< w: |
| 77 | let re = cx + (x.float64 * invW - 0.5) * scale * aspect |
| 78 | |
| 79 | var |
| 80 | zr = 0.0 |
| 81 | zi = 0.0 |
| 82 | zr2 = 0.0 |
| 83 | zi2 = 0.0 |
| 84 | i: cint = 0 |
| 85 | while i < maxIter: |
| 86 | zr2 = zr * zr |
| 87 | zi2 = zi * zi |
| 88 | if zr2 + zi2 > 4.0: |
| 89 | break |
| 90 | zi = 2.0 * zr * zi + im |
| 91 | zr = zr2 - zi2 + re |
| 92 | inc i |
| 93 | |
| 94 | let idx = row + x * 4 |
| 95 | if i >= maxIter: |
| 96 | # Inside the set. |
| 97 | buf[idx] = 0'u8 |
| 98 | buf[idx + 1] = 0'u8 |
| 99 | buf[idx + 2] = 0'u8 |
| 100 | buf[idx + 3] = 255'u8 |
| 101 | continue |
| 102 | |
| 103 | # Smooth (fractional) escape count, so bands don't posterise. |
| 104 | let mag = sqrt(zr2 + zi2) |
| 105 | var nu = i.float64 |
| 106 | if mag > 1.0: |
| 107 | nu = i.float64 + 1.0 - ln(ln(mag) / ln(2.0)) / ln(2.0) |
| 108 | let t = nu / maxIter.float64 |
| 109 | |
| 110 | buf[idx] = uint8(255.0 * (0.5 + 0.5 * sin(3.0 + t * 18.0))) |
| 111 | buf[idx + 1] = uint8(255.0 * (0.5 + 0.5 * sin(3.6 + t * 18.0))) |
| 112 | buf[idx + 2] = uint8(255.0 * (0.5 + 0.5 * sin(4.2 + t * 18.0))) |
| 113 | buf[idx + 3] = 255'u8 |