Mirai's Miscellaneous Misadventures

M33 / main.js

// copyright 2022 zamfofex
// license: AGPLv3 or later

let global = v =>
{
	if (typeof v === "number") return v
	return v.value
}

let createCanvas = (document, width, height) =>
{
	let canvas = document.createElement("canvas")
	canvas.width = width
	canvas.height = height
	canvas.tabIndex = 0
	return canvas
}

export let run = async (
{
	imports: extraImports = {},
	window = globalThis.window,
	document = window.document,
	canvas = createCanvas(document, 512, 256),
	url = "mimimi.wasm",
	body,
	module,
	chapter: startChapter = "mimimi_test",
} = {}) =>
{
	let memcpy = (a, b, n) =>
	{
		new Uint8Array(memory.buffer, a, n).set(new Uint8Array(memory.buffer, b, n))
		return a
	}
	
	let memmove = (a, b, n) =>
	{
		new Uint8Array(memory.buffer, a, n).set(new Uint8Array(memory.buffer, b, n).slice())
		return a
	}
	
	let memset = (a, b, n) =>
	{
		new Uint8Array(memory.buffer, a, n).set(Array(n).fill(b))
		return a
	}
	
	let memcmp = (a, b, n) =>
	{
		a = new Uint8Array(memory.buffer, a, n)
		b = new Uint8Array(memory.buffer, b, n)
		for (let i = 0 ; i < n ; i++)
			if (a[i] !== b[i]) return a[i] - b[i]
		return 0
	}
	
	let coroutineYield = coroutineBuffer =>
	{
		let struct = getView().getUint32(coroutineBuffer + 16, true)
		
		if (asyncify_get_state() === 2)
			asyncify_stop_rewind()
		else
			asyncify_start_unwind(struct)
	}
	
	let coroutineContinue = coroutineBuffer =>
	{
		let start = getView().getUint32(coroutineBuffer + 0, true)
		let data = getView().getUint32(coroutineBuffer + 12, true)
		let struct = getView().getUint32(coroutineBuffer + 16, true)
		let behavior = getView().getUint32(coroutineBuffer + 20, true)
		let state = getView().getUint32(coroutineBuffer + 24, true)
		
		if (state !== 0)
			asyncify_start_rewind(struct),
			table.get(start)()
		else
			getView().setUint32(coroutineBuffer + 24, 1, true),
			table.get(start)(behavior, data)
		
		if (asyncify_get_state() === 1)
			asyncify_stop_unwind()
		else
			finishCoroutine(coroutineBuffer)
	}
	
	let finishCoroutine = coroutineBuffer =>
	{
		let finish = getView().getUint32(coroutineBuffer + 4, true)
		let buffer = getView().getUint32(coroutineBuffer + 8, true)
		let data = getView().getUint32(coroutineBuffer + 12, true)
		let struct = getView().getUint32(coroutineBuffer + 16, true)
		let behavior = getView().getUint32(coroutineBuffer + 20, true)
		
		getView().setUint32(coroutineBuffer + 24, 2, true)
		
		table.get(finish)(data)
		mimimi_wasm_deallocate(0, buffer)
		mimimi_wasm_deallocate(0, struct)
		mimimi_wasm_deallocate(0, behavior)
	}
	
	let finishCoroutine2 = coroutineBuffer =>
	{
		let state = getView().getUint32(coroutineBuffer + 24, true)
		if (state !== 2) finishCoroutine(coroutineBuffer)
		
		let coroutine = getView().getUint32(coroutineBuffer + 28, true)
		mimimi_wasm_deallocate(0, coroutine)
		mimimi_wasm_deallocate(0, coroutineBuffer)
	}
	
	let createCoroutine = (start, finish, data) =>
	{
		let buffer = mimimi_wasm_allocate(0, 8192)
		
		let struct = mimimi_wasm_allocate(0, 8)
		getView().setUint32(struct + 0, buffer, true)
		getView().setUint32(struct + 4, buffer + 8192, true)
		
		let coroutineBuffer = mimimi_wasm_allocate(0, 32)
		getView().setUint32(coroutineBuffer + 0, start, true)
		getView().setUint32(coroutineBuffer + 4, finish, true)
		getView().setUint32(coroutineBuffer + 8, buffer, true)
		getView().setUint32(coroutineBuffer + 12, data, true)
		getView().setUint32(coroutineBuffer + 16, struct, true)
		getView().setUint32(coroutineBuffer + 24, 0, true)
		
		let behavior = mimimi_wasm_allocate(0, 12)
		getView().setUint32(behavior + 0, yieldIndex, true)
		getView().setUint32(behavior + 4, finishIndex, true)
		getView().setUint32(behavior + 8, coroutineBuffer, true)
		getView().setUint32(coroutineBuffer + 20, behavior, true)
		
		let coroutine = mimimi_wasm_allocate(0, 12)
		getView().setUint32(coroutine + 0, continueIndex, true)
		getView().setUint32(coroutine + 4, finishIndex2, true)
		getView().setUint32(coroutine + 8, coroutineBuffer, true)
		getView().setUint32(coroutineBuffer + 28, coroutine, true)
		
		return coroutine
	}
	
	let cache = new Map()
	let pending = []
	
	let schedule = f =>
	{
		if (pending.length === 0)
			f()
		else
			pending.push(f)
	}
	
	let rch = [0x11, 0x44, 0x77, 0x99, 0xCC, 0xFF]
	let gch = [0x11, 0x33, 0x55, 0x77, 0x99, 0xBB, 0xDD, 0xFF]
	let bch = [0x22, 0x55, 0x88, 0xBB, 0xEE]
	let texture = (data0, asset) =>
	{
		let view = getView()
		
		let width = view.getUint32(asset, true)
		let height = view.getUint32(asset + 4, true)
		let colors = view.getUint32(asset + 8, true)
		
		if (width === 0 || height === 0)
		{
			cache.set(asset, null)
			return null
		}
		
		let image = new ImageData(width, height)
		let {data} = image
		
		for (let x = 0 ; x < width ; x++)
		for (let y = 0 ; y < height ; y++)
		{
			let o = x + y * width
			let color = view.getUint8(colors + o)
			
			if (color === 0) continue
			
			if (color < 0x10)
			{
				let ch = color * 0x11
				data[o * 4 + 0] = ch
				data[o * 4 + 1] = ch
				data[o * 4 + 2] = ch
				data[o * 4 + 3] = 0xFF
			}
			else
			{
				color -= 0x10
				let r = rch[Math.floor(color / 40) % 6]
				let g = gch[Math.floor(color / 5) % 8]
				let b = bch[Math.floor(color / 1) % 5]
				
				data[o * 4 + 0] = r
				data[o * 4 + 1] = g
				data[o * 4 + 2] = b
				data[o * 4 + 3] = 0xFF
			}
		}
		
		pending.push(async () => cache.set(asset, await createImageBitmap(image)))
		return asset
	}
	
	let stamp = (data, x, y, asset) => schedule(() => context.drawImage(cache.get(asset), x, y))
	
	let invalidate = (data, asset) => schedule(() => cache.delete(asset))
	
	let imports =
	{
		env:
		{
		 	memcpy, memmove, memset, memcmp,
			mimimi_wasm_javascript_texture: texture,
			mimimi_wasm_javascript_invalidate: invalidate,
			mimimi_wasm_javascript_stamp: stamp,
			mimimi_wasm_javascript_create_coroutine: createCoroutine,
			mimimi_wasm_javascript_yield: coroutineYield,
			mimimi_wasm_javascript_continue: coroutineContinue,
			mimimi_wasm_javascript_finish_coroutine: finishCoroutine,
			mimimi_wasm_javascript_finish_coroutine2: finishCoroutine2,
			...extraImports,
		}
	}
	
	if (!module && !body) body = await fetch(url)
	
	let exports
	if (module)
	{
		let {instance} = await WebAssembly.instantiate(module, imports)
		exports = instance.exports
	}
	else if (body)
	{
		try { exports = (await WebAssembly.instantiateStreaming(body, imports)).instance.exports }
		catch (e) { exports = (await WebAssembly.instantiate(await body.arrayBuffer(), imports)).instance.exports }
	}
	
	let {memory, __indirect_function_table: table} = exports
	let {mimimi_wasm_allocator, mimimi_wasm_allocate, mimimi_wasm_deallocate} = exports
	let {mimimi_wasm_texture, mimimi_wasm_invalidate, mimimi_wasm_stamp} = exports
	let {mimimi_wasm_yield, mimimi_wasm_continue} = exports
	let {mimimi_wasm_finish_coroutine, mimimi_wasm_finish_coroutine2} = exports
	let {mimimi_wasm_coroutines, asyncify_get_state} = exports
	let {asyncify_start_rewind, asyncify_stop_rewind} = exports
	let {asyncify_start_unwind, asyncify_stop_unwind} = exports
	let {[startChapter]: start} = exports
	
	let getView = () => new DataView(memory.buffer)
	
	let f = table.grow(7)
	table.set(f + 0, mimimi_wasm_texture)
	table.set(f + 1, mimimi_wasm_invalidate)
	table.set(f + 2, mimimi_wasm_stamp)
	if (mimimi_wasm_yield)
	{
		table.set(f + 3, mimimi_wasm_yield)
		table.set(f + 4, mimimi_wasm_continue)
		table.set(f + 5, mimimi_wasm_finish_coroutine)
		table.set(f + 6, mimimi_wasm_finish_coroutine2)
	}
	let yieldIndex = f + 3
	let continueIndex = f + 4
	let finishIndex = f + 5
	let finishIndex2 = f + 6
	
	let context = canvas.getContext("2d")
	
	let size = mimimi_wasm_allocate(0, 8)
	getView().setUint32(size + 0, canvas.width, true)
	getView().setUint32(size + 4, canvas.height, true)
	
	let allocator = getView().getUint32(global(mimimi_wasm_allocator), true)
	
	let engine = mimimi_wasm_allocate(0, 16)
	getView().setUint32(engine + 4, f + 0, true)
	getView().setUint32(engine + 8, f + 1, true)
	getView().setUint32(engine + 12, f + 2, true)
	getView().setUint32(engine + 16, size, true)
	getView().setUint32(engine + 20, allocator, true)
	
	if (mimimi_wasm_coroutines)
	{
		let coroutines = getView().getUint32(global(mimimi_wasm_coroutines), true)
		getView().setUint32(engine + 24, coroutines, true)
	}
	
	let chapter = start(engine)
	let behavior = getView().getUint32(chapter, true)
	let keys = chapter + 4
	
	let inputs = {ArrowLeft: 1, ArrowRight: 2, KeyA: 1, KeyD: 2}
	let down = 0
	
	canvas.addEventListener("keydown", ({code}) =>
	{
		let v = inputs[code]
		if (v) down = down | v
	})
	
	canvas.addEventListener("keyup", ({code}) =>
	{
		let v = inputs[code]
		if (v) down = down & ~v
		down %= 0x100
	})
	
	let tap = 0
	canvas.addEventListener("pointerdown", ({target, offsetX, button}) =>
	{
		if (button !== 0) return
		
		if (offsetX < target.offsetWidth / 2)
			tap = tap | 1
		else
			tap = tap | 2
	})
	canvas.addEventListener("pointerup", () => tap = 0)
	
	let past = performance.now()
	let step = async () =>
	{
		if (pending.length !== 0)
		{
			for (let f of pending) await f()
			pending = []
		}
		
		getView().setUint8(keys, down|tap)
		let f = table.get(getView().getUint32(behavior, true))(getView().getUint32(behavior + 8, true))
		
		let now = performance.now()
		setTimeout(step, (100/3) + past - now)
		past = now
	}
	step()
	
	return canvas
}