Restore Rust core at repository root
Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "yawn-core"
|
||||
version = "0.1.0"
|
||||
description = "Shared render-data arena and render-graph compiler"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1"
|
||||
wasm-bindgen = "0.2"
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@yawn/core",
|
||||
"version": "0.1.0",
|
||||
"description": "Rust/WASM render data and render graph core",
|
||||
"type": "module",
|
||||
"exports": "./src/index.js",
|
||||
"files": ["src", "pkg"]
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
const types = { f32: Float32Array, u32: Uint32Array, i32: Int32Array };
|
||||
|
||||
export class SharedRows {
|
||||
constructor(buffer, descriptor) {
|
||||
this.buffer = buffer;
|
||||
this.descriptor = Object.freeze(descriptor);
|
||||
}
|
||||
|
||||
get name() { return this.descriptor.name; }
|
||||
get rows() { return this.descriptor.rows; }
|
||||
get stride() { return this.descriptor.stride; }
|
||||
get format() { return this.descriptor.format; }
|
||||
get view() {
|
||||
return new types[this.format](this.buffer, this.descriptor.offset, this.rows * this.stride / 4);
|
||||
}
|
||||
|
||||
row(index) {
|
||||
if (!Number.isInteger(index) || index < 0 || index >= this.rows) throw new RangeError("ROW_RANGE");
|
||||
const width = this.stride / 4;
|
||||
return this.view.subarray(index * width, (index + 1) * width);
|
||||
}
|
||||
|
||||
read(index) { return Array.from(this.row(index)); }
|
||||
write(index, values) {
|
||||
const row = this.row(index);
|
||||
if (!values || values.length !== row.length) throw new RangeError("ROW_WIDTH");
|
||||
row.set(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
share() { return { buffer: this.buffer, descriptor: this.descriptor }; }
|
||||
}
|
||||
|
||||
export class YawnCore {
|
||||
#worker;
|
||||
#buffer;
|
||||
#arrays = new Map();
|
||||
#pending = new Map();
|
||||
#next = 1;
|
||||
|
||||
constructor(canvas, { arenaBytes = 64 * 1024 * 1024, workerFactory } = {}) {
|
||||
if (!canvas) throw new TypeError("canvas is required");
|
||||
this.#worker = workerFactory?.() ?? new Worker(new URL("./worker.js", import.meta.url), {
|
||||
type: "module",
|
||||
name: "yawn-core",
|
||||
});
|
||||
this.#worker.addEventListener("message", ({ data }) => this.#message(data));
|
||||
this.#worker.addEventListener("error", () => this.#fail("WORKER_ERROR"));
|
||||
this.#worker.addEventListener("messageerror", () => this.#fail("WORKER_ERROR"));
|
||||
this.#worker.start?.();
|
||||
const offscreen = canvas.transferControlToOffscreen?.() ?? canvas;
|
||||
this.ready = this.#request("init", { canvas: offscreen, arenaBytes }, [offscreen])
|
||||
.then(({ buffer }) => { this.#buffer = buffer; });
|
||||
}
|
||||
|
||||
async allocateRows({ name, rows, stride, format }) {
|
||||
await this.ready;
|
||||
const descriptor = await this.#request("allocate", { name, rows, stride, format });
|
||||
const array = new SharedRows(this.#buffer, descriptor);
|
||||
this.#arrays.set(name, array);
|
||||
return array;
|
||||
}
|
||||
|
||||
array(name) {
|
||||
const array = this.#arrays.get(name);
|
||||
if (!array) throw new Error(`UNKNOWN_ARRAY: ${name}`);
|
||||
return array;
|
||||
}
|
||||
|
||||
async loadGraph(serialized) {
|
||||
await this.ready;
|
||||
return this.#request("load-graph", { serialized });
|
||||
}
|
||||
|
||||
#request(type, payload, transfer = []) {
|
||||
const request = this.#next++;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#pending.set(request, { resolve, reject });
|
||||
this.#worker.postMessage({ type, request, ...payload }, transfer);
|
||||
});
|
||||
}
|
||||
|
||||
#message(message) {
|
||||
const pending = this.#pending.get(message?.request);
|
||||
if (!pending) return;
|
||||
this.#pending.delete(message.request);
|
||||
if (message.error) pending.reject(Object.assign(new Error(message.error), { code: message.error }));
|
||||
else pending.resolve(message.result);
|
||||
}
|
||||
|
||||
#fail(code) {
|
||||
for (const { reject } of this.#pending.values()) reject(Object.assign(new Error(code), { code }));
|
||||
this.#pending.clear();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.#fail("DISPOSED");
|
||||
this.#worker.terminate();
|
||||
}
|
||||
}
|
||||
+424
@@ -0,0 +1,424 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use serde_json::{Map, Number, Value};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
const ALIGNMENT: u32 = 64;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Core {
|
||||
_arena: Box<[u8]>,
|
||||
base: u32,
|
||||
capacity: u32,
|
||||
used: u32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Core {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(arena_bytes: u32) -> Result<Self, JsError> {
|
||||
if arena_bytes < ALIGNMENT || arena_bytes > u32::MAX - (ALIGNMENT - 1) {
|
||||
return Err(JsError::new("INIT"));
|
||||
}
|
||||
let mut arena = vec![0; (arena_bytes + ALIGNMENT - 1) as usize].into_boxed_slice();
|
||||
let pointer = arena.as_mut_ptr() as usize;
|
||||
let base = ((pointer + ALIGNMENT as usize - 1) & !(ALIGNMENT as usize - 1)) as u32;
|
||||
Ok(Self {
|
||||
_arena: arena,
|
||||
base,
|
||||
capacity: arena_bytes,
|
||||
used: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn allocate(&mut self, rows: u32, stride: u32, format: &str) -> Result<u32, JsError> {
|
||||
if rows == 0 || stride < 16 || stride % 16 != 0 || !matches!(format, "f32" | "u32" | "i32")
|
||||
{
|
||||
return Err(JsError::new("ALLOCATION"));
|
||||
}
|
||||
let offset = align(self.used, ALIGNMENT).ok_or_else(|| JsError::new("ARENA_OOM"))?;
|
||||
let bytes = rows
|
||||
.checked_mul(stride)
|
||||
.ok_or_else(|| JsError::new("ARENA_OOM"))?;
|
||||
self.used = offset
|
||||
.checked_add(bytes)
|
||||
.filter(|end| *end <= self.capacity)
|
||||
.ok_or_else(|| JsError::new("ARENA_OOM"))?;
|
||||
Ok(self.base + offset)
|
||||
}
|
||||
|
||||
pub fn compile_graph(&self, source: &str) -> Result<String, JsError> {
|
||||
compile_graph(source).map_err(JsError::new)
|
||||
}
|
||||
}
|
||||
|
||||
fn align(value: u32, alignment: u32) -> Option<u32> {
|
||||
value
|
||||
.checked_add(alignment - 1)
|
||||
.map(|value| value & !(alignment - 1))
|
||||
}
|
||||
|
||||
enum Expression {
|
||||
Atom(Value),
|
||||
List(Vec<Expression>),
|
||||
}
|
||||
|
||||
struct Parser<'a> {
|
||||
source: &'a [u8],
|
||||
at: usize,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
fn parse(source: &'a str) -> Result<Expression, &'static str> {
|
||||
let mut parser = Parser {
|
||||
source: source.as_bytes(),
|
||||
at: 0,
|
||||
};
|
||||
let expression = parser.expression()?;
|
||||
parser.whitespace();
|
||||
if parser.at != parser.source.len() {
|
||||
return Err("GRAPH_WIRE");
|
||||
}
|
||||
Ok(expression)
|
||||
}
|
||||
|
||||
fn expression(&mut self) -> Result<Expression, &'static str> {
|
||||
self.whitespace();
|
||||
match self.source.get(self.at) {
|
||||
Some(b'(') => self.list(),
|
||||
Some(b'"') => self.string(),
|
||||
Some(b')') | None => Err("GRAPH_WIRE"),
|
||||
Some(_) => self.atom(),
|
||||
}
|
||||
}
|
||||
|
||||
fn list(&mut self) -> Result<Expression, &'static str> {
|
||||
self.at += 1;
|
||||
let mut values = Vec::new();
|
||||
loop {
|
||||
self.whitespace();
|
||||
match self.source.get(self.at) {
|
||||
Some(b')') => {
|
||||
self.at += 1;
|
||||
return Ok(Expression::List(values));
|
||||
}
|
||||
None => return Err("GRAPH_WIRE"),
|
||||
_ => values.push(self.expression()?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn string(&mut self) -> Result<Expression, &'static str> {
|
||||
let start = self.at;
|
||||
self.at += 1;
|
||||
let mut escaped = false;
|
||||
while let Some(&byte) = self.source.get(self.at) {
|
||||
self.at += 1;
|
||||
if escaped {
|
||||
escaped = false;
|
||||
} else if byte == b'\\' {
|
||||
escaped = true;
|
||||
} else if byte == b'"' {
|
||||
let value = serde_json::from_slice(&self.source[start..self.at])
|
||||
.map_err(|_| "GRAPH_WIRE")?;
|
||||
return Ok(Expression::Atom(Value::String(value)));
|
||||
}
|
||||
}
|
||||
Err("GRAPH_WIRE")
|
||||
}
|
||||
|
||||
fn atom(&mut self) -> Result<Expression, &'static str> {
|
||||
let start = self.at;
|
||||
while self
|
||||
.source
|
||||
.get(self.at)
|
||||
.is_some_and(|byte| !byte.is_ascii_whitespace() && !matches!(byte, b'(' | b')'))
|
||||
{
|
||||
self.at += 1;
|
||||
}
|
||||
let token = std::str::from_utf8(&self.source[start..self.at]).map_err(|_| "GRAPH_WIRE")?;
|
||||
let value = match token {
|
||||
"true" => Value::Bool(true),
|
||||
"false" => Value::Bool(false),
|
||||
"null" => Value::Null,
|
||||
_ => serde_json::from_str::<Value>(token)
|
||||
.ok()
|
||||
.filter(Value::is_number)
|
||||
.unwrap_or_else(|| Value::String(token.into())),
|
||||
};
|
||||
Ok(Expression::Atom(value))
|
||||
}
|
||||
|
||||
fn whitespace(&mut self) {
|
||||
while self
|
||||
.source
|
||||
.get(self.at)
|
||||
.is_some_and(u8::is_ascii_whitespace)
|
||||
{
|
||||
self.at += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compile_graph(source: &str) -> Result<String, &'static str> {
|
||||
let root = Parser::parse(source)?;
|
||||
let Expression::List(mut root) = root else {
|
||||
return Err("GRAPH_WIRE");
|
||||
};
|
||||
if root.len() != 3
|
||||
|| !matches!(&root[0], Expression::Atom(Value::String(tag)) if tag == "yawn-graph")
|
||||
|| !matches!(&root[1], Expression::Atom(Value::Number(version)) if version.as_u64() == Some(1))
|
||||
{
|
||||
return Err("GRAPH_WIRE");
|
||||
}
|
||||
let mut graph = decode(root.pop().unwrap())?;
|
||||
let object = graph.as_object_mut().ok_or("GRAPH_SHAPE")?;
|
||||
if !object.get("id").is_some_and(Value::is_string) {
|
||||
return Err("GRAPH_SHAPE");
|
||||
}
|
||||
let passes = sort_passes(object.get("passes").ok_or("GRAPH_PASS")?)?;
|
||||
plan_resources(object, &passes)?;
|
||||
object.insert("passes".into(), Value::Array(passes));
|
||||
serde_json::to_string(&graph).map_err(|_| "GRAPH_WIRE")
|
||||
}
|
||||
|
||||
fn decode(expression: Expression) -> Result<Value, &'static str> {
|
||||
let Expression::List(mut values) = expression else {
|
||||
return match expression {
|
||||
Expression::Atom(value) => Ok(value),
|
||||
Expression::List(_) => unreachable!(),
|
||||
};
|
||||
};
|
||||
if values.is_empty() {
|
||||
return Err("GRAPH_WIRE");
|
||||
}
|
||||
let tag = match values.remove(0) {
|
||||
Expression::Atom(Value::String(tag)) => tag,
|
||||
_ => return Err("GRAPH_WIRE"),
|
||||
};
|
||||
if tag == "array" {
|
||||
return values.into_iter().map(decode).collect();
|
||||
}
|
||||
if tag != "object" {
|
||||
return Err("GRAPH_WIRE");
|
||||
}
|
||||
let mut object = Map::new();
|
||||
for field in values {
|
||||
let Expression::List(mut field) = field else {
|
||||
return Err("GRAPH_WIRE");
|
||||
};
|
||||
if field.len() != 3
|
||||
|| !matches!(&field[0], Expression::Atom(Value::String(tag)) if tag == "field")
|
||||
{
|
||||
return Err("GRAPH_WIRE");
|
||||
}
|
||||
let value = decode(field.pop().unwrap())?;
|
||||
let key = match field.pop().unwrap() {
|
||||
Expression::Atom(Value::String(key)) => key,
|
||||
_ => return Err("GRAPH_WIRE"),
|
||||
};
|
||||
if object.insert(key, value).is_some() {
|
||||
return Err("GRAPH_WIRE");
|
||||
}
|
||||
}
|
||||
Ok(Value::Object(object))
|
||||
}
|
||||
|
||||
fn sort_passes(value: &Value) -> Result<Vec<Value>, &'static str> {
|
||||
let passes = value.as_array().ok_or("GRAPH_PASS")?;
|
||||
let mut ids = HashMap::new();
|
||||
for (index, pass) in passes.iter().enumerate() {
|
||||
let id = pass
|
||||
.as_object()
|
||||
.and_then(|pass| pass.get("id"))
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("GRAPH_PASS")?;
|
||||
if ids.insert(id, index).is_some() {
|
||||
return Err("GRAPH_PASS");
|
||||
}
|
||||
}
|
||||
let mut dependencies = Vec::with_capacity(passes.len());
|
||||
for pass in passes {
|
||||
let after = pass.get("after").map_or(Ok(&[][..]), |after| {
|
||||
after.as_array().map(Vec::as_slice).ok_or("GRAPH_ARRAY")
|
||||
})?;
|
||||
dependencies.push(
|
||||
after
|
||||
.iter()
|
||||
.map(|dependency| {
|
||||
dependency
|
||||
.as_str()
|
||||
.and_then(|id| ids.get(id).copied())
|
||||
.ok_or("GRAPH_DEPENDENCY")
|
||||
})
|
||||
.collect::<Result<HashSet<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
let mut emitted = vec![false; passes.len()];
|
||||
let mut result = Vec::with_capacity(passes.len());
|
||||
while result.len() < passes.len() {
|
||||
let ready = (0..passes.len()).find(|&index| {
|
||||
!emitted[index]
|
||||
&& dependencies[index]
|
||||
.iter()
|
||||
.all(|dependency| emitted[*dependency])
|
||||
});
|
||||
let index = ready.ok_or("GRAPH_CYCLE")?;
|
||||
emitted[index] = true;
|
||||
result.push(passes[index].clone());
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn plan_resources(graph: &mut Map<String, Value>, passes: &[Value]) -> Result<(), &'static str> {
|
||||
let mut used = HashSet::new();
|
||||
let mut lifetimes = HashMap::new();
|
||||
for (frame, pass) in passes.iter().enumerate() {
|
||||
for id in pass_resources(pass)? {
|
||||
used.insert(id.to_owned());
|
||||
lifetimes
|
||||
.entry(id.to_owned())
|
||||
.and_modify(|lifetime: &mut (usize, usize)| lifetime.1 = frame)
|
||||
.or_insert((frame, frame));
|
||||
}
|
||||
}
|
||||
let Some(resources) = graph.get_mut("resources") else {
|
||||
return Ok(());
|
||||
};
|
||||
let resources = resources.as_object_mut().ok_or("GRAPH_RESOURCE")?;
|
||||
for kind in ["buffers", "samplers"] {
|
||||
if let Some(declarations) = resources.get_mut(kind) {
|
||||
let declarations = declarations.as_array_mut().ok_or("GRAPH_RESOURCE")?;
|
||||
let mut ids = HashSet::new();
|
||||
declarations.retain(|declaration| {
|
||||
declaration
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|id| ids.insert(id.to_owned()) && used.contains(id))
|
||||
});
|
||||
}
|
||||
}
|
||||
let Some(textures) = resources.get_mut("textures") else {
|
||||
return Ok(());
|
||||
};
|
||||
let textures = textures.as_array_mut().ok_or("GRAPH_RESOURCE")?;
|
||||
let mut ids = HashSet::new();
|
||||
let mut planned = Vec::new();
|
||||
for mut declaration in textures.drain(..) {
|
||||
let object = declaration.as_object_mut().ok_or("GRAPH_RESOURCE")?;
|
||||
let id = object
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("GRAPH_RESOURCE")?;
|
||||
if !ids.insert(id.to_owned()) {
|
||||
return Err("GRAPH_RESOURCE");
|
||||
}
|
||||
let Some(&(first, last)) = lifetimes.get(id) else {
|
||||
continue;
|
||||
};
|
||||
planned.push((declaration, first, last));
|
||||
}
|
||||
planned.sort_by_key(|value| value.1);
|
||||
let mut output = Vec::new();
|
||||
let mut slots: Vec<(String, usize)> = Vec::new();
|
||||
for (mut declaration, first, last) in planned {
|
||||
let object = declaration.as_object_mut().unwrap();
|
||||
let key = texture_key(object)?;
|
||||
let slot = if object.get("transient") == Some(&Value::Bool(false)) {
|
||||
None
|
||||
} else {
|
||||
slots
|
||||
.iter()
|
||||
.position(|value| value.0 == key && value.1 < first)
|
||||
};
|
||||
let slot = match slot {
|
||||
Some(slot) => {
|
||||
slots[slot].1 = last;
|
||||
slot
|
||||
}
|
||||
None => {
|
||||
slots.push((key, last));
|
||||
slots.len() - 1
|
||||
}
|
||||
};
|
||||
object.insert("slot".into(), Value::Number(Number::from(slot as u64)));
|
||||
output.push(declaration);
|
||||
}
|
||||
*textures = output;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pass_resources(pass: &Value) -> Result<Vec<&str>, &'static str> {
|
||||
let mut result = Vec::new();
|
||||
for field in ["bindings", "color", "vertexBuffers"] {
|
||||
if let Some(values) = pass.get(field) {
|
||||
for value in values.as_array().ok_or("GRAPH_ARRAY")? {
|
||||
result.push(
|
||||
value
|
||||
.get("resource")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("GRAPH_RESOURCE")?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for field in ["depth", "indexBuffer"] {
|
||||
if let Some(value) = pass.get(field) {
|
||||
result.push(
|
||||
value
|
||||
.get("resource")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("GRAPH_RESOURCE")?,
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn texture_key(texture: &Map<String, Value>) -> Result<String, &'static str> {
|
||||
let mut size = texture.get("size").cloned().unwrap_or_else(|| {
|
||||
Value::Array(vec![
|
||||
Value::String("canvas".into()),
|
||||
Value::String("canvas".into()),
|
||||
])
|
||||
});
|
||||
let size = size.as_array_mut().ok_or("GRAPH_TEXTURE_SIZE")?;
|
||||
if !(2..=3).contains(&size.len()) {
|
||||
return Err("GRAPH_TEXTURE_SIZE");
|
||||
}
|
||||
if size.len() == 2 {
|
||||
size.push(Value::Number(Number::from(1)));
|
||||
}
|
||||
let format = texture
|
||||
.get("format")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("GRAPH_RESOURCE")?;
|
||||
let mut usage = texture.get("usage").map_or(Ok(Vec::new()), |usage| {
|
||||
usage
|
||||
.as_array()
|
||||
.ok_or("GRAPH_TEXTURE_USAGE")?
|
||||
.iter()
|
||||
.map(|value| value.as_str().ok_or("GRAPH_TEXTURE_USAGE"))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
})?;
|
||||
usage.sort_unstable();
|
||||
usage.dedup();
|
||||
serde_json::to_string(&(
|
||||
size,
|
||||
format,
|
||||
usage,
|
||||
texture
|
||||
.get("mipLevelCount")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(1),
|
||||
texture
|
||||
.get("sampleCount")
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(1),
|
||||
texture
|
||||
.get("dimension")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("2d"),
|
||||
))
|
||||
.map_err(|_| "GRAPH_RESOURCE")
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import initWasm, { Core as WasmCore } from "../pkg/yawn_core.js";
|
||||
|
||||
let canvas, context, device, surfaceFormat, memory, core, loadout;
|
||||
const arrays = new Map();
|
||||
const align = (value, multiple) => Math.ceil(value / multiple) * multiple;
|
||||
const fail = code => { throw new Error(code); };
|
||||
const list = value => value === undefined ? [] : Array.isArray(value) ? value : fail("GRAPH_ARRAY");
|
||||
|
||||
function index(items, code) {
|
||||
const result = new Map();
|
||||
for (const item of list(items)) {
|
||||
if (!item || typeof item.id !== "string" || result.has(item.id)) fail(code);
|
||||
result.set(item.id, item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const bufferUsage = names => list(names).reduce((usage, name) => usage | ({
|
||||
uniform: GPUBufferUsage.UNIFORM, storage: GPUBufferUsage.STORAGE,
|
||||
vertex: GPUBufferUsage.VERTEX, index: GPUBufferUsage.INDEX,
|
||||
indirect: GPUBufferUsage.INDIRECT, copySrc: GPUBufferUsage.COPY_SRC,
|
||||
}[name] ?? fail("GRAPH_BUFFER_USAGE")), GPUBufferUsage.COPY_DST);
|
||||
const textureUsage = names => list(names).reduce((usage, name) => usage | ({
|
||||
render: GPUTextureUsage.RENDER_ATTACHMENT, sampled: GPUTextureUsage.TEXTURE_BINDING,
|
||||
storage: GPUTextureUsage.STORAGE_BINDING, copySrc: GPUTextureUsage.COPY_SRC,
|
||||
copyDst: GPUTextureUsage.COPY_DST,
|
||||
}[name] ?? fail("GRAPH_TEXTURE_USAGE")), 0);
|
||||
|
||||
async function compile(graph) {
|
||||
if (!graph || typeof graph.id !== "string") fail("GRAPH_SHAPE");
|
||||
const passes = list(graph.passes);
|
||||
const renderDeclarations = index(graph.pipelines?.render, "GRAPH_PIPELINE");
|
||||
const computeDeclarations = index(graph.pipelines?.compute, "GRAPH_PIPELINE");
|
||||
const resources = new Map(), owned = [];
|
||||
try {
|
||||
for (const declaration of list(graph.resources?.buffers)) {
|
||||
const source = arrays.get(declaration.array);
|
||||
if (!source) fail("GRAPH_ARRAY_UNKNOWN");
|
||||
const gpu = device.createBuffer({ size: align(source.bytes, 4), usage: bufferUsage(declaration.usage) });
|
||||
resources.set(declaration.id, { kind: "buffer", gpu, source });
|
||||
owned.push(gpu);
|
||||
}
|
||||
|
||||
const textureSlots = new Map();
|
||||
for (const declaration of list(graph.resources?.textures)) {
|
||||
if (!Number.isInteger(declaration.slot)) fail("GRAPH_RESOURCE");
|
||||
const size = declaration.size ?? ["canvas", "canvas"];
|
||||
if (!Array.isArray(size) || size.length < 2 || size.length > 3) fail("GRAPH_TEXTURE_SIZE");
|
||||
const descriptor = {
|
||||
size: [size[0] === "canvas" ? canvas.width : size[0], size[1] === "canvas" ? canvas.height : size[1], size[2] ?? 1],
|
||||
format: declaration.format,
|
||||
usage: textureUsage(declaration.usage),
|
||||
mipLevelCount: declaration.mipLevelCount ?? 1,
|
||||
sampleCount: declaration.sampleCount ?? 1,
|
||||
dimension: declaration.dimension ?? "2d",
|
||||
};
|
||||
let slot = textureSlots.get(declaration.slot);
|
||||
if (!slot) {
|
||||
const gpu = device.createTexture(descriptor);
|
||||
slot = { gpu, view: gpu.createView() };
|
||||
textureSlots.set(declaration.slot, slot);
|
||||
owned.push(gpu);
|
||||
}
|
||||
resources.set(declaration.id, { kind: "texture", gpu: slot.gpu, view: slot.view });
|
||||
}
|
||||
for (const declaration of list(graph.resources?.samplers))
|
||||
resources.set(declaration.id, {
|
||||
kind: "sampler", gpu: device.createSampler(declaration.descriptor),
|
||||
});
|
||||
|
||||
const renderPipelines = new Map(), computePipelines = new Map();
|
||||
await Promise.all([...new Set(passes.filter(x => x.type === "render").map(x => x.pipeline))].map(async id => {
|
||||
const declaration = renderDeclarations.get(id);
|
||||
if (typeof declaration?.code !== "string") fail("GRAPH_PIPELINE");
|
||||
const module = device.createShaderModule({ code: declaration.code });
|
||||
renderPipelines.set(id, await device.createRenderPipelineAsync({
|
||||
layout: "auto",
|
||||
vertex: { module, entryPoint: declaration.vertex?.entry ?? "vertex", buffers: declaration.vertex?.buffers ?? [] },
|
||||
fragment: {
|
||||
module,
|
||||
entryPoint: declaration.fragment?.entry ?? "fragment",
|
||||
targets: list(declaration.fragment?.targets).map(target => ({
|
||||
...target, format: target.format === "canvas" ? surfaceFormat : target.format,
|
||||
})),
|
||||
},
|
||||
primitive: declaration.primitive,
|
||||
depthStencil: declaration.depthStencil,
|
||||
multisample: declaration.multisample,
|
||||
}));
|
||||
}));
|
||||
await Promise.all([...new Set(passes.filter(x => x.type === "compute").map(x => x.pipeline))].map(async id => {
|
||||
const declaration = computeDeclarations.get(id);
|
||||
if (typeof declaration?.code !== "string") fail("GRAPH_PIPELINE");
|
||||
const module = device.createShaderModule({ code: declaration.code });
|
||||
computePipelines.set(id, await device.createComputePipelineAsync({
|
||||
layout: "auto", compute: { module, entryPoint: declaration.entry ?? "main" },
|
||||
}));
|
||||
}));
|
||||
|
||||
const bindGroups = (pass, pipeline) => {
|
||||
const groups = new Map();
|
||||
for (const binding of list(pass.bindings)) {
|
||||
const resource = resources.get(binding.resource);
|
||||
if (!resource) fail("GRAPH_RESOURCE_UNKNOWN");
|
||||
const value = resource.kind === "buffer"
|
||||
? { buffer: resource.gpu, offset: binding.offset ?? 0, ...(binding.size ? { size: binding.size } : {}) }
|
||||
: resource.kind === "texture" ? resource.view : resource.gpu;
|
||||
if (!groups.has(binding.group ?? 0)) groups.set(binding.group ?? 0, []);
|
||||
groups.get(binding.group ?? 0).push({ binding: binding.binding, resource: value });
|
||||
}
|
||||
return [...groups].map(([group, entries]) => [group, device.createBindGroup({
|
||||
layout: pipeline.getBindGroupLayout(group), entries,
|
||||
})]);
|
||||
};
|
||||
const compiled = passes.map(pass => {
|
||||
const pipeline = (pass.type === "render" ? renderPipelines : computePipelines).get(pass.pipeline);
|
||||
if (!pipeline) fail("GRAPH_PASS");
|
||||
return { pass, pipeline, bindGroups: bindGroups(pass, pipeline) };
|
||||
});
|
||||
return { id: graph.id, passes: compiled, resources, owned };
|
||||
} catch (error) {
|
||||
owned.forEach(resource => resource.destroy?.());
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const clearColor = (value = [0, 0, 0, 1]) => Array.isArray(value)
|
||||
? { r: value[0], g: value[1], b: value[2], a: value[3] }
|
||||
: value;
|
||||
|
||||
function render() {
|
||||
if (!loadout) return;
|
||||
for (const resource of loadout.resources.values())
|
||||
if (resource.kind === "buffer") device.queue.writeBuffer(
|
||||
resource.gpu, 0, new Uint8Array(memory, resource.source.offset, resource.source.bytes),
|
||||
);
|
||||
const encoder = device.createCommandEncoder();
|
||||
for (const { pass, pipeline, bindGroups } of loadout.passes) {
|
||||
if (pass.type === "compute") {
|
||||
const command = encoder.beginComputePass();
|
||||
command.setPipeline(pipeline);
|
||||
bindGroups.forEach(([group, value]) => command.setBindGroup(group, value));
|
||||
command.dispatchWorkgroups(...(pass.dispatch ?? [1, 1, 1]));
|
||||
command.end();
|
||||
continue;
|
||||
}
|
||||
const view = id => id === "canvas"
|
||||
? context.getCurrentTexture().createView()
|
||||
: loadout.resources.get(id)?.view ?? fail("GRAPH_ATTACHMENT");
|
||||
const command = encoder.beginRenderPass({
|
||||
colorAttachments: list(pass.color).map(attachment => ({
|
||||
view: view(attachment.resource), loadOp: attachment.load ?? "clear",
|
||||
storeOp: attachment.store ?? "store", clearValue: clearColor(attachment.clear),
|
||||
})),
|
||||
...(pass.depth ? { depthStencilAttachment: {
|
||||
view: view(pass.depth.resource), depthLoadOp: pass.depth.load ?? "clear",
|
||||
depthStoreOp: pass.depth.store ?? "store", depthClearValue: pass.depth.clear ?? 1,
|
||||
} } : {}),
|
||||
});
|
||||
command.setPipeline(pipeline);
|
||||
bindGroups.forEach(([group, value]) => command.setBindGroup(group, value));
|
||||
list(pass.vertexBuffers).forEach(binding => command.setVertexBuffer(
|
||||
binding.slot ?? 0, loadout.resources.get(binding.resource)?.gpu ?? fail("GRAPH_RESOURCE_UNKNOWN"), binding.offset ?? 0,
|
||||
));
|
||||
if (pass.indexBuffer) command.setIndexBuffer(
|
||||
loadout.resources.get(pass.indexBuffer.resource)?.gpu ?? fail("GRAPH_RESOURCE_UNKNOWN"),
|
||||
pass.indexBuffer.format ?? "uint32", pass.indexBuffer.offset ?? 0,
|
||||
);
|
||||
const draw = pass.draw ?? {};
|
||||
if (pass.indexBuffer) command.drawIndexed(draw.indices ?? 0, draw.instances ?? 1, draw.firstIndex ?? 0, draw.baseVertex ?? 0, draw.firstInstance ?? 0);
|
||||
else command.draw(draw.vertices ?? 3, draw.instances ?? 1, draw.firstVertex ?? 0, draw.firstInstance ?? 0);
|
||||
command.end();
|
||||
}
|
||||
device.queue.submit([encoder.finish()]);
|
||||
}
|
||||
|
||||
function tick() {
|
||||
try { render(); } catch (error) {
|
||||
postMessage({ type: "runtime-error", error: error?.message ?? "RENDER_ERROR" });
|
||||
loadout = undefined;
|
||||
}
|
||||
(globalThis.requestAnimationFrame ?? (callback => setTimeout(callback, 16)))(tick);
|
||||
}
|
||||
|
||||
addEventListener("message", async ({ data: message }) => {
|
||||
try {
|
||||
let result;
|
||||
if (message.type === "init") {
|
||||
if (!(message.canvas instanceof OffscreenCanvas) || !Number.isInteger(message.arenaBytes) || message.arenaBytes < 64) fail("INIT");
|
||||
canvas = message.canvas;
|
||||
const wasm = await initWasm();
|
||||
core = new WasmCore(message.arenaBytes);
|
||||
memory = wasm.memory.buffer;
|
||||
if (!(memory instanceof SharedArrayBuffer)) fail("WASM_MEMORY_NOT_SHARED");
|
||||
const adapter = await navigator.gpu?.requestAdapter();
|
||||
if (!adapter) fail("WEBGPU_UNAVAILABLE");
|
||||
device = await adapter.requestDevice();
|
||||
context = canvas.getContext("webgpu");
|
||||
surfaceFormat = navigator.gpu.getPreferredCanvasFormat();
|
||||
context.configure({ device, format: surfaceFormat, alphaMode: "opaque" });
|
||||
device.lost.then(() => { postMessage({ type: "runtime-error", error: "DEVICE_LOST" }); loadout = undefined; });
|
||||
result = { buffer: memory };
|
||||
tick();
|
||||
} else if (message.type === "allocate") {
|
||||
const { name, rows, stride, format } = message;
|
||||
if (typeof name !== "string" || !name || arrays.has(name)) fail("ALLOCATION");
|
||||
const offset = core.allocate(rows, stride, format), bytes = rows * stride;
|
||||
result = { name, rows, stride, format, offset };
|
||||
arrays.set(name, { ...result, bytes });
|
||||
} else if (message.type === "load-graph") {
|
||||
const next = await compile(JSON.parse(core.compile_graph(message.serialized)));
|
||||
const previous = loadout;
|
||||
loadout = next;
|
||||
previous?.owned.forEach(resource => resource.destroy?.());
|
||||
result = { id: next.id };
|
||||
} else fail("MESSAGE");
|
||||
postMessage({ request: message.request, result });
|
||||
} catch (error) {
|
||||
postMessage({ request: message.request, error: error?.message ?? "CORE_ERROR" });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user