#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable --allow-ffi // Copyright 2023 Fahmi Akbar Wildana // SPDX-License-Identifier: BSD-2-Clause import MemViz from "./memviz.ts"; import Canvas from "./canvas.ts"; import { footer, header } from "./string.ts"; import * as random from "./random.ts"; import { cpus } from "https://deno.land/std/node/os.ts"; import { loop, malloc, WorkerThread } from "./utils.ts"; /////////////////////////////////////////////////////////////////////////////👆 0 //👇 tweakable const capacity = { min: 100, max: 200 }, instance = 10, worker = cpus().length - 1; class Position { static TypedArray = Uint16Array; static each = 2 * Position.TypedArray.BYTES_PER_ELEMENT; //👈 x: InstanceType; y: InstanceType; constructor(public cap: number) { this.x = make(Position.TypedArray, this.cap); this.y = make(Position.TypedArray, this.cap); } color = random.color(); avatar?: Awaited> | true; } let make: ReturnType; let buffer: ArrayBufferLike; /////////////////////////////////////////////////////////////////////////////👆 1 const width = 800, height = 600, size = { particle: 2, avatar: 32 }; //👈 tweakable const radius = 100; const printMemoryLayout = true; let zoo: Position[], capacities: number[] | Uint8Array = [], border: number, inc = 0, mouse = { x: width / 2, y: height / 2 }, isFollowCursor: true | undefined, actor: WorkerThread | undefined; const script = import.meta.url; let actorID: number | undefined; if ("window" in globalThis) { const byteLength = Position.each * capacity.max * instance; //👈 console.group(...header("Main thread")); console.time("full setup"); if (worker) { actor = new WorkerThread(script, new SharedArrayBuffer(byteLength)); } make = malloc(buffer = worker ? actor!.buffer : new ArrayBuffer(byteLength)); setup(); // create zoo if (worker) { // tell worker about the memory layout of zoo actor!.run(new Uint8Array(capacities = zoo!.map((it) => it.cap)).buffer); } console.timeEnd("full setup"); // setTimeout(() => actor!.relayMessage(["no shake"]), 1e2); // debug: freeze particle console.time("prepare to draw"); const memviz = await MemViz.new(buffer); const canvas = await Canvas.new({ width, height }); console.timeEnd("prepare to draw"); console.time("fetch avatar"); Promise.all( zoo!.map((it, i) => canvas.loadImage(random.avatar.elonmusk()).then((img) => { it.avatar = img; incRect += 2; actor.relayMessage(["avatar ready", i]); }) ), ).then(() => actor.relayMessage(["follow cursor"])); console.timeEnd("fetch avatar"); console.groupEnd(); console.info(...footer("Main")); loop(() => { mouse = canvas.mouse; if (worker) actor!.relayMessage(["mousemove", mouse]); canvas.draw((ctx) => { const m = mouse; for (const { cap, x, y, color, avatar } of zoo) { ctx.fillStyle = color; for (let i = cap; i--;) { border = avatar ? size.avatar : size.particle + inc; const p = { x: Atomics.load(x, i), y: Atomics.load(y, i) }; ctx.translate(p.x, p.y); if (avatar && !is(p).inside(radius, m)) { ctx.rotate( Math.atan2(m.y - (p.y + border / 2), m.x - (p.x + border / 2)), ); ctx.drawImage(avatar, 0, 0, border, border); } else { ctx.fillRect(0, 0, border, border); } ctx.resetTransform(); } } }); memviz.draw(); }); } else { console.group(...header("Worker thread")); console.time("full setup"); const shared = await WorkerThread.selfSpawn(script, worker - 1); actorID = shared.id; make = malloc(buffer = shared.memory); capacities = new Uint8Array(shared.layout); setup(); // create zoo console.timeEnd("full setup"); console.groupEnd(); console.info(...footer(`Worker[${actorID}]`)); shared.onrelay = (data) => { if (!Array.isArray(data)) return; switch (data[0]) { case "mousemove": mouse = data[1]; break; case "no shake": isFollowCursor = true; break; case "avatar ready": border = size.avatar; zoo[data[1]].avatar = true; break; } }; loop(update); } /////////////////////////////////////////////////////////////////////////////👆 2 // WARNING: declaring global variables from here then capture it in setup() or update() will not works 😏 function setup() { zoo = Array.from( { length: instance }, (_, i) => new Position( "window" in globalThis ? random.int(capacity) //👈 set random capacity in the main thread : capacities.at(i)!, //👈 set capactiy in the worker thread from data passed by the parent thread ), ); // NOTE: Position is like DataView for buffer but based on memory layout when in worker thread // center all entities when setup in main thread if ("window" in globalThis) { for (const { x, y } of zoo) { x.fill(width / 2); y.fill(height / 2); } } if (printMemoryLayout) printMemTable(); } function update() { const clamp = (...$: number[]) => Math.min(Math.max($[1], $[0]), $[2]); const pad = border ??= size.particle * 2, vw = width - pad, vh = height - pad; if (isFollowCursor) return; // avatar looking at mouse pointer while wiggle for (const { cap, x, y, avatar } of zoo) { // for (let i= cap - 1; i--;) { let len = Math.ceil(cap / (worker || 1)) - 1; for (const offset = len * (actorID ?? 0); len--;) { // eratic particles const i = offset + len; const p = { x: Atomics.load(x, i), y: Atomics.load(y, i) }; const inside = is(p).inside(radius, mouse); if (avatar && !inside) continue; // freeze // eratic particles const rx = random.int(-inside, +inside); const ry = random.int(-inside, +inside); Atomics.store(x, i, clamp(pad, p.x + rx, vw)); Atomics.store(y, i, clamp(pad, p.y + ry, vh)); } } } /////////////////////////////////////////////////////////////////////////////👆 3 function is(p: { x: number; y: number }) { return { inside: (r: number, m: { x: number; y: number }) => (p.x - m.x + r / 2) ** 2 + (p.y - m.y - r / 2) ** 2 < r ** 2, }; } function printMemTable() { console.time("print memory layout"); const table = zoo.flatMap(( { x, y, cap }, ) => [{ malloc: cap * Position.each, }, { "|addr|👉": "|x|", start: x.byteOffset, end: x.byteOffset + x.byteLength, }, { "|addr|👉": "|y|", start: y.byteOffset, end: y.byteOffset + x.byteLength, }] ), //@ts-ignore: typescript can't infer `group.malloc` allocated = table.reduce((total, group) => total += group.malloc ?? 0, 0), available = buffer.byteLength - allocated; console.info("> every number are in bytes"); console.table(table); console.table({ "memory buffer": buffer.byteLength, allocated, available }); console.timeEnd("print memory layout"); }