Strip core to render data and render graphs

Move glTF, picking, camera controls, and conventional handles into addons. Keep camera and material mutations in SIMD-aligned shared SOA rows and synchronize material updates directly into GPU buffers.

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:
Amp
2026-08-19 09:41:41 +00:00
co-authored by heaust
parent 0e44917f9e
commit 6bbf8039e4
67 changed files with 3152 additions and 3669 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
export const gltfShader = /* wgsl */ `
struct UniformData { mouse_move: vec2<f32>, mouse_click: vec2<f32>, resolution: vec2<f32>, time: f32, _padding0: f32, camera_position: vec4<f32> }
struct UniformData { resolution: vec2<f32>, time: f32, _padding0: f32, camera_position: vec4<f32> }
struct MaterialData { base_color_factor: vec4<f32>, emissive_factor: vec4<f32>, surface_factors: vec4<f32>, alpha_optics: vec4<f32>, flags: vec4<u32>, uv_sets: vec4<u32>, debug_extras: vec4<u32> }
@group(0) @binding(0) var<uniform> uni: UniformData;
@group(1) @binding(0) var<uniform> view_proj: mat4x4<f32>;
@@ -76,8 +76,8 @@ export const noopComputeShader = /* wgsl */ `@compute @workgroup_size(1) fn main
export const defaultPipelines = Object.freeze({
render: Object.freeze([
Object.freeze({ name: "ground_plane", shader: groundShader, vertexEntry: "vs_main", fragmentEntry: "fs_main" }),
Object.freeze({ name: "gltf_standard", shader: gltfShader, vertexEntry: "vs_main", fragmentEntry: "fs_main" }),
Object.freeze({ name: "gltf_standard_double_sided", shader: gltfShader, vertexEntry: "vs_main", fragmentEntry: "fs_main", doubleSided: true }),
Object.freeze({ name: "gltf_standard", shader: gltfShader, vertexEntry: "vs_main", fragmentEntry: "fs_main", material: true }),
Object.freeze({ name: "gltf_standard_double_sided", shader: gltfShader, vertexEntry: "vs_main", fragmentEntry: "fs_main", doubleSided: true, material: true }),
Object.freeze({ name: "frame_out", shader: frameShader, vertexEntry: "vs_main", fragmentEntry: "fs_frame_out" }),
]),
compute: Object.freeze([
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@yawn/gltf-import",
"version": "0.1.0",
"description": "glTF fetch worker that uploads directly into Yawn shared SOA memory",
"description": "glTF worker that publishes generic render data directly into Yawn shared SOA memory",
"type": "module",
"exports": "./src/index.js"
}
+392
View File
@@ -0,0 +1,392 @@
const GLB_MAGIC = 0x46546c67;
const JSON_CHUNK = 0x4e4f534a;
const BIN_CHUNK = 0x004e4942;
const PACKET_MAGIC = 0x50445259;
const PACKET_VERSION = 1;
const COMPONENT_WIDTH = Object.freeze({ SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4, MAT2: 4, MAT3: 9, MAT4: 16 });
const COMPONENT_SIZE = Object.freeze({ 5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4 });
const IDENTITY = Object.freeze([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const fail = code => { throw new Error(code); };
const align4 = value => (value + 3) & ~3;
const finite = values => values.every(Number.isFinite);
function parseContainer(source) {
if (!(source instanceof Uint8Array) || !source.byteLength) fail("GLTF_EMPTY");
const view = new DataView(source.buffer, source.byteOffset, source.byteLength);
if (source.byteLength >= 12 && view.getUint32(0, true) === GLB_MAGIC) {
if (view.getUint32(4, true) !== 2 || view.getUint32(8, true) !== source.byteLength)
fail("GLTF_INVALID_CONTAINER");
let offset = 12, json, binary;
while (offset < source.byteLength) {
if (offset + 8 > source.byteLength) fail("GLTF_INVALID_CONTAINER");
const length = view.getUint32(offset, true);
const type = view.getUint32(offset + 4, true);
const end = offset + 8 + length;
if (end > source.byteLength) fail("GLTF_INVALID_CONTAINER");
const chunk = source.subarray(offset + 8, end);
if (type === JSON_CHUNK && !json) json = chunk;
if (type === BIN_CHUNK && !binary) binary = chunk;
offset = end;
}
if (!json) fail("GLTF_JSON_MISSING");
return { document: JSON.parse(decoder.decode(json).replace(/\0+$/u, "").trimEnd()), binary };
}
return { document: JSON.parse(decoder.decode(source).replace(/^\uFEFF/u, "")), binary: undefined };
}
async function fetchBytes(uri, baseUrl, fetcher) {
const response = await fetcher(new URL(uri, baseUrl));
if (!response.ok) fail(`HTTP_${response.status}`);
return new Uint8Array(await response.arrayBuffer());
}
async function loadBuffers(document, binary, baseUrl, fetcher) {
return Promise.all((document.buffers ?? []).map(async (buffer, index) => {
const bytes = buffer.uri === undefined
? (index === 0 ? binary : undefined)
: await fetchBytes(buffer.uri, baseUrl, fetcher);
if (!bytes || bytes.byteLength < buffer.byteLength) fail("GLTF_BUFFER_INVALID");
return bytes;
}));
}
function component(data, offset, type) {
switch (type) {
case 5120: return data.getInt8(offset);
case 5121: return data.getUint8(offset);
case 5122: return data.getInt16(offset, true);
case 5123: return data.getUint16(offset, true);
case 5125: return data.getUint32(offset, true);
case 5126: return data.getFloat32(offset, true);
default: fail("GLTF_ACCESSOR_COMPONENT");
}
}
function normalizeComponent(value, type) {
switch (type) {
case 5120: return Math.max(value / 127, -1);
case 5121: return value / 255;
case 5122: return Math.max(value / 32767, -1);
case 5123: return value / 65535;
case 5125: return value / 4294967295;
default: return value;
}
}
function viewBytes(document, buffers, index) {
const view = document.bufferViews?.[index];
const buffer = view && buffers[view.buffer];
if (!view || !buffer) fail("GLTF_BUFFER_VIEW_INVALID");
const start = view.byteOffset ?? 0;
const end = start + view.byteLength;
if (end > buffer.byteLength) fail("GLTF_BUFFER_VIEW_INVALID");
return { view, bytes: buffer.subarray(start, end) };
}
function readAccessor(document, buffers, index, { integer = false } = {}) {
const accessor = document.accessors?.[index];
const width = accessor && COMPONENT_WIDTH[accessor.type];
const size = accessor && COMPONENT_SIZE[accessor.componentType];
if (!accessor || !width || !size || !Number.isInteger(accessor.count) || accessor.count < 0)
fail("GLTF_ACCESSOR_INVALID");
const values = integer ? new Uint32Array(accessor.count * width) : new Float32Array(accessor.count * width);
if (accessor.bufferView !== undefined) {
const { view, bytes } = viewBytes(document, buffers, accessor.bufferView);
const stride = view.byteStride ?? width * size;
const start = accessor.byteOffset ?? 0;
if (stride < width * size || start + Math.max(0, accessor.count - 1) * stride + width * size > bytes.byteLength)
fail("GLTF_ACCESSOR_RANGE");
const data = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
for (let item = 0; item < accessor.count; item++) {
for (let lane = 0; lane < width; lane++) {
let value = component(data, start + item * stride + lane * size, accessor.componentType);
if (!integer && accessor.normalized) value = normalizeComponent(value, accessor.componentType);
values[item * width + lane] = value;
}
}
}
if (accessor.sparse) {
const sparse = accessor.sparse;
const indices = sparse.indices;
const indexSize = COMPONENT_SIZE[indices.componentType];
if (!indexSize || ![5121, 5123, 5125].includes(indices.componentType)) fail("GLTF_SPARSE_INVALID");
const indexView = viewBytes(document, buffers, indices.bufferView).bytes;
const valueView = viewBytes(document, buffers, sparse.values.bufferView).bytes;
const indexStart = indices.byteOffset ?? 0;
const valueStart = sparse.values.byteOffset ?? 0;
if (indexStart + sparse.count * indexSize > indexView.byteLength || valueStart + sparse.count * width * size > valueView.byteLength)
fail("GLTF_SPARSE_INVALID");
const indexData = new DataView(indexView.buffer, indexView.byteOffset, indexView.byteLength);
const valueData = new DataView(valueView.buffer, valueView.byteOffset, valueView.byteLength);
for (let item = 0; item < sparse.count; item++) {
const target = component(indexData, indexStart + item * indexSize, indices.componentType);
if (target >= accessor.count) fail("GLTF_SPARSE_INVALID");
for (let lane = 0; lane < width; lane++) {
let value = component(valueData, valueStart + (item * width + lane) * size, accessor.componentType);
if (!integer && accessor.normalized) value = normalizeComponent(value, accessor.componentType);
values[target * width + lane] = value;
}
}
}
if (!finite(values)) fail("GLTF_ACCESSOR_NONFINITE");
return { count: accessor.count, width, values };
}
const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
function normalize(value, fallback) {
const length = Math.hypot(...value);
return length > Number.EPSILON && Number.isFinite(length) ? value.map(item => item / length) : fallback;
}
function lanes(values, index, width) { return Array.from(values.subarray(index * width, (index + 1) * width)); }
function repairGeometry(positions, normals, tangents, uvs, indices) {
const count = positions.length / 3;
if (indices.length % 3 || Array.from(indices).some(index => index >= count)) fail("GLTF_TRIANGLES_INVALID");
const normalsValid = normals?.length === positions.length;
const tangentsValid = tangents?.length === count * 4;
if (normalsValid && tangentsValid)
return { positions, normals, tangents, uvs, indices };
const outPositions = [], outNormals = [], outTangents = [], outUvs = [];
for (let triangle = 0; triangle < indices.length; triangle += 3) {
const ids = [indices[triangle], indices[triangle + 1], indices[triangle + 2]];
const p = ids.map(index => lanes(positions, index, 3));
const uv = ids.map(index => lanes(uvs, index, 2));
const faceNormal = normalize(cross(sub(p[1], p[0]), sub(p[2], p[0])), [0, 1, 0]);
const duv1 = sub([...uv[1], 0], [...uv[0], 0]);
const duv2 = sub([...uv[2], 0], [...uv[0], 0]);
const determinant = duv1[0] * duv2[1] - duv1[1] * duv2[0];
const edge1 = sub(p[1], p[0]), edge2 = sub(p[2], p[0]);
const rawTangent = Math.abs(determinant) > Number.EPSILON
? edge1.map((value, lane) => (value * duv2[1] - edge2[lane] * duv1[1]) / determinant)
: [0, 0, 0];
const rawBitangent = Math.abs(determinant) > Number.EPSILON
? edge2.map((value, lane) => (value * duv1[0] - edge1[lane] * duv2[0]) / determinant)
: [0, 0, 0];
for (let corner = 0; corner < 3; corner++) {
const normal = normalize(normalsValid ? lanes(normals, ids[corner], 3) : faceNormal, faceNormal);
const projected = rawTangent.map((value, lane) => value - normal[lane] * dot(normal, rawTangent));
const axis = Math.abs(normal[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0];
const tangent = normalize(projected, normalize(cross(axis, normal), [0, 0, 1]));
const generated = [...tangent, dot(cross(normal, tangent), rawBitangent) < 0 ? -1 : 1];
outPositions.push(...p[corner]);
outNormals.push(...normal);
outTangents.push(...(tangentsValid ? lanes(tangents, ids[corner], 4) : generated));
outUvs.push(...uv[corner]);
}
}
const repairedIndices = Uint32Array.from({ length: outPositions.length / 3 }, (_, index) => index);
return {
positions: new Float32Array(outPositions),
normals: new Float32Array(outNormals),
tangents: new Float32Array(outTangents),
uvs: new Float32Array(outUvs),
indices: repairedIndices,
};
}
function multiply(a, b) {
const result = Array(16).fill(0);
for (let column = 0; column < 4; column++)
for (let row = 0; row < 4; row++)
for (let lane = 0; lane < 4; lane++)
result[column * 4 + row] += a[lane * 4 + row] * b[column * 4 + lane];
return result;
}
function nodeMatrix(node) {
if (node.matrix) {
if (node.matrix.length !== 16 || !finite(node.matrix)) fail("GLTF_NODE_TRANSFORM");
return Array.from(node.matrix);
}
const [x, y, z, w] = node.rotation ?? [0, 0, 0, 1];
const [sx, sy, sz] = node.scale ?? [1, 1, 1];
const [tx, ty, tz] = node.translation ?? [0, 0, 0];
const matrix = [
(1 - 2 * y * y - 2 * z * z) * sx, (2 * x * y + 2 * z * w) * sx, (2 * x * z - 2 * y * w) * sx, 0,
(2 * x * y - 2 * z * w) * sy, (1 - 2 * x * x - 2 * z * z) * sy, (2 * y * z + 2 * x * w) * sy, 0,
(2 * x * z + 2 * y * w) * sz, (2 * y * z - 2 * x * w) * sz, (1 - 2 * x * x - 2 * y * y) * sz, 0,
tx, ty, tz, 1,
];
if (!finite(matrix)) fail("GLTF_NODE_TRANSFORM");
return matrix;
}
function textureReference(reference) {
return reference ? { texture: reference.index, texCoord: reference.texCoord ?? 0 } : null;
}
function materialMetadata(material, index) {
const pbr = material.pbrMetallicRoughness ?? {};
const ior = material.extensions?.KHR_materials_ior?.ior ?? 1.5;
if (!Number.isFinite(ior) || (ior !== 0 && ior < 1)) fail("GLTF_MATERIAL_IOR");
return {
key: index + 1,
baseColorFactor: pbr.baseColorFactor ?? [1, 1, 1, 1],
metallicFactor: pbr.metallicFactor ?? 1,
roughnessFactor: pbr.roughnessFactor ?? 1,
emissiveFactor: material.emissiveFactor ?? [0, 0, 0],
ior,
alphaMode: (material.alphaMode ?? "OPAQUE").toLowerCase(),
alphaCutoff: material.alphaCutoff ?? 0.5,
doubleSided: material.doubleSided ?? false,
baseColorTexture: textureReference(pbr.baseColorTexture),
metallicRoughnessTexture: textureReference(pbr.metallicRoughnessTexture),
normalTexture: textureReference(material.normalTexture),
normalScale: material.normalTexture?.scale ?? 1,
occlusionTexture: textureReference(material.occlusionTexture),
occlusionStrength: material.occlusionTexture?.strength ?? 1,
emissiveTexture: textureReference(material.emissiveTexture),
};
}
function samplerMetadata(sampler) {
const min = sampler.minFilter;
return {
magFilter: sampler.magFilter === 9728 ? "nearest" : "linear",
minFilter: [9728, 9984, 9986].includes(min) ? "nearest" : "linear",
mipmapFilter: [9984, 9985].includes(min) ? "nearest" : "linear",
addressU: sampler.wrapS === 33071 ? "clamp_to_edge" : sampler.wrapS === 33648 ? "mirror_repeat" : "repeat",
addressV: sampler.wrapT === 33071 ? "clamp_to_edge" : sampler.wrapT === 33648 ? "mirror_repeat" : "repeat",
};
}
function inferMime(image) {
if (image.mimeType) return image.mimeType;
const uri = image.uri?.toLowerCase() ?? "";
if (uri.startsWith("data:image/png") || uri.endsWith(".png")) return "image/png";
if (uri.startsWith("data:image/jpeg") || /\.jpe?g(?:$|[?#])/u.test(uri)) return "image/jpeg";
fail("GLTF_IMAGE_MIME");
}
async function decodeScene(document, buffers, baseUrl, fetcher) {
const geometries = [], occurrences = [], geometryIds = new Map();
for (let meshIndex = 0; meshIndex < (document.meshes ?? []).length; meshIndex++) {
const mesh = document.meshes[meshIndex];
for (let primitiveIndex = 0; primitiveIndex < (mesh.primitives ?? []).length; primitiveIndex++) {
const primitive = mesh.primitives[primitiveIndex];
if ((primitive.mode ?? 4) !== 4 || primitive.attributes?.POSITION === undefined) fail("GLTF_TRIANGLES_REQUIRED");
const position = readAccessor(document, buffers, primitive.attributes.POSITION);
if (position.width !== 3 || !position.count) fail("GLTF_POSITION_INVALID");
const normal = primitive.attributes.NORMAL === undefined ? null : readAccessor(document, buffers, primitive.attributes.NORMAL);
const tangent = primitive.attributes.TANGENT === undefined ? null : readAccessor(document, buffers, primitive.attributes.TANGENT);
const texcoord = primitive.attributes.TEXCOORD_0 === undefined ? null : readAccessor(document, buffers, primitive.attributes.TEXCOORD_0);
if (normal && normal.width !== 3 || tangent && tangent.width !== 4 || texcoord && texcoord.width !== 2)
fail("GLTF_ATTRIBUTE_INVALID");
const uvs = new Float32Array(position.count * 2);
if (texcoord) uvs.set(texcoord.values.subarray(0, uvs.length));
const indexAccessor = primitive.indices === undefined
? null
: readAccessor(document, buffers, primitive.indices, { integer: true });
if (indexAccessor && indexAccessor.width !== 1) fail("GLTF_INDEX_INVALID");
const indices = indexAccessor?.values
?? Uint32Array.from({ length: position.count }, (_, index) => index);
const repaired = repairGeometry(position.values, normal?.values, tangent?.values, uvs, indices);
const id = geometries.length;
geometryIds.set(`${meshIndex}:${primitiveIndex}`, id);
const material = primitive.material === undefined ? undefined : document.materials?.[primitive.material];
geometries.push({
id,
material: primitive.material === undefined ? 0 : primitive.material + 1,
instanceType: [1 | 4 | (material?.doubleSided ? 8 : 0), ...Array(15).fill(0)],
...repaired,
});
}
}
const children = new Set((document.nodes ?? []).flatMap(node => node.children ?? []));
const scene = document.scenes?.[document.scene ?? 0];
const roots = scene?.nodes ?? (document.nodes ?? []).map((_, index) => index).filter(index => !children.has(index));
const active = new Set();
const visit = (nodeIndex, parent) => {
const node = document.nodes?.[nodeIndex];
if (!node || active.has(nodeIndex)) fail("GLTF_NODE_INVALID");
active.add(nodeIndex);
const world = multiply(parent, nodeMatrix(node));
if (node.mesh !== undefined) {
const mesh = document.meshes?.[node.mesh];
if (!mesh) fail("GLTF_MESH_INVALID");
for (let primitive = 0; primitive < mesh.primitives.length; primitive++) {
const geometry = geometryIds.get(`${node.mesh}:${primitive}`);
if (geometry !== undefined) occurrences.push({ geometry, transform: world });
}
}
for (const child of node.children ?? []) visit(child, world);
active.delete(nodeIndex);
};
for (const root of roots) visit(root, IDENTITY);
const images = await Promise.all((document.images ?? []).map(async image => {
const data = image.bufferView === undefined
? await fetchBytes(image.uri, baseUrl, fetcher)
: viewBytes(document, buffers, image.bufferView).bytes.slice();
return { mimeType: inferMime(image), bytes: data };
}));
return {
geometries,
occurrences,
materials: [materialMetadata({}, -1), ...(document.materials ?? []).map(materialMetadata)],
textures: (document.textures ?? []).map(texture => ({ image: texture.source, sampler: texture.sampler ?? null })),
samplers: (document.samplers ?? []).map(samplerMetadata),
images,
};
}
function encodePacket(scene) {
const chunks = [];
let payloadLength = 0;
const append = (source, alignment = 4) => {
const bytes = source instanceof Uint8Array
? source
: new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
const offset = alignment === 4 ? align4(payloadLength) : payloadLength;
if (offset > payloadLength) chunks.push({ offset: payloadLength, bytes: new Uint8Array(offset - payloadLength) });
chunks.push({ offset, bytes });
payloadLength = offset + bytes.byteLength;
return offset;
};
const stream = (values, width) => ({ offset: append(values), count: values.length / width });
const metadata = {
geometries: scene.geometries.map(geometry => ({
id: geometry.id,
material: geometry.material,
instanceType: geometry.instanceType,
positions: stream(geometry.positions, 3),
normals: stream(geometry.normals, 3),
tangents: stream(geometry.tangents, 4),
uvs: stream(geometry.uvs, 2),
indices: stream(geometry.indices, 1),
})),
occurrences: scene.occurrences,
materials: scene.materials,
textures: scene.textures,
samplers: scene.samplers,
images: scene.images.map(image => ({
mimeType: image.mimeType,
data: { offset: append(image.bytes), byteLength: image.bytes.byteLength },
})),
};
const metadataBytes = encoder.encode(JSON.stringify(metadata));
const payloadOffset = align4(16 + metadataBytes.byteLength);
const packet = new Uint8Array(payloadOffset + payloadLength);
const header = new DataView(packet.buffer);
header.setUint32(0, PACKET_MAGIC, true);
header.setUint32(4, PACKET_VERSION, true);
header.setUint32(8, metadataBytes.byteLength, true);
header.setUint32(12, payloadLength, true);
packet.set(metadataBytes, 16);
for (const chunk of chunks) packet.set(chunk.bytes, payloadOffset + chunk.offset);
return packet;
}
/** Convert glTF 2.0/GLB bytes into Yawn's format-neutral render-data packet. */
export async function gltfToRenderDataPacket(source, baseUrl, fetcher = fetch) {
const { document, binary } = parseContainer(source);
if (document.asset?.version !== "2.0") fail("GLTF_VERSION_UNSUPPORTED");
const buffers = await loadBuffers(document, binary, baseUrl, fetcher);
return encodePacket(await decodeScene(document, buffers, baseUrl, fetcher));
}
+30 -5
View File
@@ -6,7 +6,31 @@ export class GltfImportError extends Error {
}
}
/** Fetches glTF in a dedicated worker and commits only shared-memory upload metadata. */
function frameCamera(core, bounds, framing) {
if (!bounds || framing === false) return;
if (framing !== undefined && framing !== "exterior" && framing !== "interior")
throw new TypeError("framing must be exterior, interior, or false");
const min = bounds.min, max = bounds.max;
if (!Array.isArray(min) || !Array.isArray(max) || min.length !== 3 || max.length !== 3) return;
const center = min.map((value, axis) => (value + max[axis]) * 0.5);
const extent = max.map((value, axis) => value - min[axis]);
const radius = Math.max(1, Math.hypot(...extent) * 0.5);
const camera = core.array("camera.state");
const state = camera.read(0);
const interior = framing === "interior";
const eye = interior
? [center[0], center[1] + radius * 0.05, center[2]]
: [center[0] + radius * 1.8, center[1] + radius * 1.4, center[2] + radius * 1.8];
const target = interior ? [center[0] + radius, center[1], center[2]] : center;
state.splice(0, 3, ...eye);
state.splice(4, 3, ...target);
state.splice(8, 3, 0, 1, 0);
state[14] = Math.max(radius * 0.001, 0.1);
state[15] = Math.max(radius * 6, 1.1);
camera.write(0, state);
}
/** Parses glTF in a dedicated worker and publishes a generic render-data packet through shared memory. */
export class GltfImporter {
#core;
#worker;
@@ -16,7 +40,8 @@ export class GltfImporter {
#disposed = false;
constructor(core, { workerFactory } = {}) {
if (!core?.allocateArray || !core?.commitGlbUpload) throw new TypeError("core must implement the Yawn shared upload protocol");
if (!core?.allocateArray || !core?.commitRenderDataUpload)
throw new TypeError("core must implement the Yawn shared render-data protocol");
this.#core = core;
this.#worker = workerFactory
? workerFactory()
@@ -55,7 +80,7 @@ export class GltfImporter {
if (message.type === "allocate") {
const length = Math.ceil(message.byteLength / 16);
pending.array = await this.#core.allocateArray({
name: "upload.gltf",
name: "upload.renderData",
domain: "fixed",
scalar: "u32",
lanes: 4,
@@ -68,11 +93,11 @@ export class GltfImporter {
...pending.array.share(),
});
} else if (message.type === "ready") {
const result = await this.#core.commitGlbUpload(
const result = await this.#core.commitRenderDataUpload(
pending.array,
message.byteLength,
pending.options,
);
frameCamera(this.#core, result.bounds, pending.options.framing);
this.#pending.delete(message.request);
pending.resolve(result);
} else if (message.type === "error") {
+8 -6
View File
@@ -1,4 +1,5 @@
import { writeSharedUpload } from "./shared-upload.js";
import { gltfToRenderDataPacket } from "./gltf.js";
const downloads = new Map();
@@ -10,16 +11,17 @@ addEventListener("message", async ({ data: message }) => {
if (!response.ok) throw new Error(`HTTP_${response.status}`);
const bytes = new Uint8Array(await response.arrayBuffer());
if (!bytes.byteLength) throw new Error("GLTF_EMPTY");
downloads.set(request, bytes);
postMessage({ type: "allocate", request, byteLength: bytes.byteLength });
const packet = await gltfToRenderDataPacket(bytes, message.url);
downloads.set(request, packet);
postMessage({ type: "allocate", request, byteLength: packet.byteLength });
return;
}
if (message?.type === "storage") {
const bytes = downloads.get(request);
if (!bytes) throw new Error("GLTF_REQUEST_UNKNOWN");
writeSharedUpload(message.buffer, message.descriptor, bytes);
const packet = downloads.get(request);
if (!packet) throw new Error("GLTF_REQUEST_UNKNOWN");
writeSharedUpload(message.buffer, message.descriptor, packet);
downloads.delete(request);
postMessage({ type: "ready", request, byteLength: bytes.byteLength });
postMessage({ type: "ready", request, byteLength: packet.byteLength });
}
} catch (error) {
downloads.delete(request);
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@yawn/mesh-handles",
"version": "0.1.0",
"description": "Conventional mesh handles over the Yawn worker and shared-SOA protocol",
"description": "Conventional mesh, instance, camera, and material handles over Yawn render data",
"type": "module",
"exports": "./src/index.js",
"dependencies": {
+1 -1
View File
@@ -1,4 +1,4 @@
import { SnapshotReader } from "@yawn/core/snapshot";
import { SnapshotReader } from "./snapshot.js";
import { DerivedBvh } from "./bvh-core.js";
let reader, bvh = new DerivedBvh(), epoch = 0, updating = false, requestedEpoch = 0;
+300 -5
View File
@@ -1,7 +1,10 @@
const TOKEN = Symbol("yawn mesh handle addon");
import { RendererError } from "@yawn/core";
import { SnapshotReader } from "./snapshot.js";
/** Creates the optional snapshot/BVH worker used by core's picking protocol. */
export const createPickingWorker = () => new Worker(
const TOKEN = Symbol("yawn mesh handle addon");
const SNAPSHOT_EVENT = "yawn-render-data-snapshot";
const PUBLISHED_EVENT = "yawn-render-data-snapshot-published";
const createPickingWorker = () => new Worker(
new URL("./bvh-worker.js", import.meta.url),
{ type: "module", name: "yawn-spatial-query" },
);
@@ -9,9 +12,17 @@ export const createPickingWorker = () => new Worker(
/** Conventional mesh/instance objects layered entirely over the Yawn core protocol. */
export class MeshHandles {
#core;
#factory; #worker; #reader; #snapshot; #epoch = 0; #next = 1; #picks = new Map(); #disposed = false;
#onSnapshot; #onPublished;
constructor(core) {
constructor(core, { pickingWorkerFactory = createPickingWorker } = {}) {
this.#core = core;
this.#factory = pickingWorkerFactory;
this.#onSnapshot = event => this.#installSnapshot(event.detail);
this.#onPublished = event => this.#publish(event.detail?.epoch);
core.addEventListener?.(SNAPSHOT_EVENT, this.#onSnapshot);
core.addEventListener?.(PUBLISHED_EVENT, this.#onPublished);
if (core.renderDataSnapshot) this.#installSnapshot(core.renderDataSnapshot);
}
fromImportedScene(result) {
@@ -20,7 +31,7 @@ export class MeshHandles {
}
async pickRay(origin, direction, options) {
const result = await this.#core.pickRay(origin, direction, options);
const result = await this.#pickRay(origin, direction, options);
return {
...result,
hits: result.hits.map((hit) => ({
@@ -29,6 +40,124 @@ export class MeshHandles {
})),
};
}
#installSnapshot(snapshot) {
try {
if (snapshot?.controlVersion !== 1 || snapshot?.schemaVersion !== 2) throw new Error("version");
this.#snapshot = snapshot;
this.#reader = new SnapshotReader(snapshot.memory, snapshot.controlPtr);
this.#epoch = this.#reader.latest().epoch;
this.#worker?.postMessage({ type: "init", ...snapshot });
} catch {
this.#disable("PICK_PROTOCOL_MISMATCH");
}
}
#publish(epoch) {
if (this.#disposed || !this.#reader) return;
try {
this.#epoch = this.#reader.latest().epoch;
this.#worker?.postMessage({ type: "update", epoch: this.#epoch || (epoch >>> 0) });
} catch {
this.#disable("PICK_PROTOCOL_MISMATCH");
}
}
#ensureWorker() {
if (this.#worker) return true;
if (!this.#reader || !this.#factory) return false;
try {
this.#worker = this.#factory();
this.#worker.addEventListener("message", event => this.#workerMessage(event.data));
this.#worker.addEventListener("error", () => this.#disable("PICK_WORKER_ERROR"));
this.#worker.addEventListener("messageerror", () => this.#disable("PICK_WORKER_ERROR"));
this.#worker.start?.();
this.#worker.postMessage({ type: "init", ...this.#snapshot });
if (this.#epoch) this.#worker.postMessage({ type: "update", epoch: this.#epoch });
return true;
} catch {
this.#disable("PICK_WORKER_ERROR");
return false;
}
}
#disable(code) {
const error = new RendererError(code);
for (const pending of this.#picks.values()) pending.reject(error);
this.#picks.clear();
try { this.#worker?.terminate?.(); } catch { /* best effort */ }
this.#worker = null;
}
#workerMessage(message) {
if (message?.type === "fatal") { this.#disable(message.code || "PICK_WORKER_ERROR"); return; }
if (message?.type !== "pick") return;
const pending = this.#picks.get(message.request);
if (!pending) return;
this.#picks.delete(message.request);
let latest;
try { latest = this.#reader.latest().epoch; this.#epoch = latest; }
catch { pending.reject(new RendererError("PICK_PROTOCOL_MISMATCH")); this.#disable("PICK_PROTOCOL_MISMATCH"); return; }
if (message.stale || pending.epoch !== message.epoch || message.epoch !== latest) {
if (!pending.retried && latest) this.#sendPick({ ...pending, retried: true }, latest);
else pending.reject(new RendererError("PICK_STALE"));
return;
}
pending.resolve({
epoch: latest,
hits: (message.hits || []).map(hit => ({
instance: [hit.slot >>> 0, hit.generation >>> 0],
distance: hit.distance,
})),
});
}
#sendPick(pending, epoch) {
const request = this.#next++ >>> 0 || this.#next++;
pending.epoch = epoch;
this.#picks.set(request, pending);
try {
this.#worker.postMessage({
type: "pick", request, epoch,
origin: pending.origin, direction: pending.direction,
maxDistance: pending.maxDistance, maxHits: pending.maxHits,
});
} catch {
this.#picks.delete(request);
pending.reject(new RendererError("PICK_WORKER_ERROR"));
}
}
#pickRay(origin, direction, { maxDistance = Infinity, maxHits = 1 } = {}) {
const vector = (value, name) => {
if (!value || value.length !== 3 || [...value].some(x => typeof x !== "number" || !Number.isFinite(x)))
throw new TypeError(`${name} must contain 3 finite numbers`);
return [...value];
};
origin = vector(origin, "origin");
direction = vector(direction, "direction");
if (direction.every(x => x === 0)) throw new TypeError("direction must be nonzero");
if (typeof maxDistance !== "number" || (!(Number.isFinite(maxDistance) && maxDistance >= 0) && maxDistance !== Infinity) || !Number.isInteger(maxHits) || maxHits < 1 || maxHits > 64)
throw new TypeError("invalid pick options");
if (this.#disposed) return Promise.reject(new RendererError("DISPOSED"));
if (!this.#ensureWorker()) return Promise.reject(new RendererError("PICK_UNAVAILABLE"));
let epoch;
try { epoch = this.#reader.latest().epoch; this.#epoch = epoch; }
catch { return Promise.reject(new RendererError("PICK_PROTOCOL_MISMATCH")); }
if (!epoch) return Promise.reject(new RendererError("PICK_STALE"));
return new Promise((resolve, reject) => this.#sendPick({
resolve, reject, origin, direction, maxDistance, maxHits, retried: false,
}, epoch));
}
dispose() {
if (this.#disposed) return;
this.#disposed = true;
this.#core.removeEventListener?.(SNAPSHOT_EVENT, this.#onSnapshot);
this.#core.removeEventListener?.(PUBLISHED_EVENT, this.#onPublished);
try { this.#worker?.postMessage?.({ type: "dispose" }); } catch { /* best effort */ }
this.#disable("DISPOSED");
}
}
export class Mesh {
@@ -65,3 +194,169 @@ export class Instance {
setTransform(transform) { this.#live(); this.#core.setInstanceTransform(this.#handle, transform); }
async destroy() { this.#live(); await this.#core.destroyInstance(this.#handle); this.#dead = true; }
}
function requireArray(core, name, { domain, scalar, lanes }) {
if (!core?.array) throw new TypeError("core must implement the Yawn shared render-data protocol");
const array = core.array(name);
if (array.domain !== domain || array.scalar !== scalar || array.lanes !== lanes)
throw new RendererError("SOA_PROTOCOL_MISMATCH");
return array;
}
function vector(value, length, name) {
if (!value || value.length !== length || [...value].some(item => typeof item !== "number" || !Number.isFinite(item)))
throw new TypeError(`${name} must contain ${length} finite numbers`);
return Array.from(value);
}
function finite(value, name) {
if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${name} must be finite`);
return value;
}
/** Conventional camera properties backed by the canonical SIMD-width camera SOA row. */
export class CameraHandle {
#array;
constructor(core) {
this.#array = requireArray(core, "camera.state", { domain: "fixed", scalar: "f32", lanes: 16 });
}
get state() { return this.#array.read(0); }
set state(value) { this.#write(vector(value, 16, "state")); }
get position() { return this.state.slice(0, 3); }
set position(value) { this.update({ position: value }); }
get target() { return this.state.slice(4, 7); }
set target(value) { this.update({ target: value }); }
get up() { return this.state.slice(8, 11); }
set up(value) { this.update({ up: value }); }
get fovY() { return this.state[12]; }
set fovY(value) { this.update({ fovY: value }); }
get aspect() { return this.state[13]; }
set aspect(value) { this.update({ aspect: value }); }
get near() { return this.state[14]; }
set near(value) { this.update({ near: value }); }
get far() { return this.state[15]; }
set far(value) { this.update({ far: value }); }
update(properties = {}) {
if (!properties || typeof properties !== "object") throw new TypeError("camera properties must be an object");
const known = new Set(["position", "target", "up", "fovY", "aspect", "near", "far"]);
for (const key of Object.keys(properties)) if (!known.has(key)) throw new TypeError(`unknown camera property '${key}'`);
const state = this.state;
if (properties.position !== undefined) state.splice(0, 3, ...vector(properties.position, 3, "position"));
if (properties.target !== undefined) state.splice(4, 3, ...vector(properties.target, 3, "target"));
if (properties.up !== undefined) state.splice(8, 3, ...vector(properties.up, 3, "up"));
if (properties.fovY !== undefined) state[12] = finite(properties.fovY, "fovY");
if (properties.aspect !== undefined) state[13] = finite(properties.aspect, "aspect");
if (properties.near !== undefined) state[14] = finite(properties.near, "near");
if (properties.far !== undefined) state[15] = finite(properties.far, "far");
this.#write(state);
return this;
}
lookAt(position, target, { up = this.up } = {}) {
return this.update({ position, target, up });
}
#write(state) {
const offset = state.slice(0, 3).map((value, axis) => state[4 + axis] - value);
const up = state.slice(8, 11);
const cross = [
offset[1] * up[2] - offset[2] * up[1],
offset[2] * up[0] - offset[0] * up[2],
offset[0] * up[1] - offset[1] * up[0],
];
if (Math.hypot(...offset) < 0.1 || Math.hypot(...up) === 0 || Math.hypot(...cross) === 0)
throw new RangeError("camera position, target, and up do not define a view");
if (!(state[12] > 0 && state[12] < Math.PI) || state[13] <= 0 || state[14] <= 0 || state[15] <= state[14])
throw new RangeError("camera projection is invalid");
state[3] = state[7] = 1;
state[11] = 0;
this.#array.write(0, state);
}
}
const FLOAT_WORD = new ArrayBuffer(4);
const FLOAT_VIEW = new Float32Array(FLOAT_WORD);
const WORD_VIEW = new Uint32Array(FLOAT_WORD);
function wordToFloat(word) { WORD_VIEW[0] = word; return FLOAT_VIEW[0]; }
function floatToWord(value) { FLOAT_VIEW[0] = value; return WORD_VIEW[0]; }
/** Creates scene-scoped material objects over packed material SOA rows. */
export class MaterialHandles {
#array;
constructor(core) {
this.#array = requireArray(core, "material.state", { domain: "fixed", scalar: "u32", lanes: 28 });
}
fromImportedScene(result) {
if (!result || !Array.isArray(result.materials)) throw new TypeError("invalid imported scene");
return result.materials.map(material => this.get(material?.key));
}
get(key) {
if (!Number.isInteger(key) || key < 0 || key > 0xffffffff || key >= this.#array.length)
throw new RangeError("material key is outside the shared material rows");
return new MaterialHandle(TOKEN, this.#array, key);
}
}
export class MaterialHandle {
#array; #key;
constructor(token, array, key) {
if (token !== TOKEN) throw new TypeError("MaterialHandle cannot be constructed directly");
this.#array = array;
this.#key = key;
}
get key() { return this.#key; }
get baseColor() { return this.#floats(0, 4); }
set baseColor(value) { this.update({ baseColor: value }); }
get emissive() { return this.#floats(4, 3); }
set emissive(value) { this.update({ emissive: value }); }
get metallic() { return this.#float(8); }
set metallic(value) { this.update({ metallic: value }); }
get roughness() { return this.#float(9); }
set roughness(value) { this.update({ roughness: value }); }
get normalScale() { return this.#float(10); }
set normalScale(value) { this.update({ normalScale: value }); }
get occlusionStrength() { return this.#float(11); }
set occlusionStrength(value) { this.update({ occlusionStrength: value }); }
get alphaCutoff() { return this.#float(13); }
set alphaCutoff(value) { this.update({ alphaCutoff: value }); }
get ior() { return this.#float(14); }
set ior(value) { this.update({ ior: value }); }
update(properties = {}) {
if (!properties || typeof properties !== "object") throw new TypeError("material properties must be an object");
const known = new Set(["baseColor", "emissive", "metallic", "roughness", "normalScale", "occlusionStrength", "alphaCutoff", "ior"]);
for (const key of Object.keys(properties)) if (!known.has(key)) throw new TypeError(`unknown material property '${key}'`);
const words = this.#array.read(this.#key);
const setFloat = (lane, value, name) => { words[lane] = floatToWord(finite(value, name)); };
if (properties.baseColor !== undefined)
vector(properties.baseColor, 4, "baseColor").forEach((value, lane) => setFloat(lane, value, "baseColor"));
if (properties.emissive !== undefined)
vector(properties.emissive, 3, "emissive").forEach((value, lane) => setFloat(4 + lane, value, "emissive"));
for (const [name, lane] of [["metallic", 8], ["roughness", 9], ["normalScale", 10], ["occlusionStrength", 11], ["alphaCutoff", 13]]) {
if (properties[name] !== undefined) setFloat(lane, properties[name], name);
}
if (properties.metallic !== undefined && !(properties.metallic >= 0 && properties.metallic <= 1)) throw new RangeError("metallic must be in [0, 1]");
if (properties.roughness !== undefined && !(properties.roughness >= 0 && properties.roughness <= 1)) throw new RangeError("roughness must be in [0, 1]");
if (properties.occlusionStrength !== undefined && !(properties.occlusionStrength >= 0 && properties.occlusionStrength <= 1)) throw new RangeError("occlusionStrength must be in [0, 1]");
if (properties.alphaCutoff !== undefined && !(properties.alphaCutoff >= 0 && properties.alphaCutoff <= 1)) throw new RangeError("alphaCutoff must be in [0, 1]");
if (properties.ior !== undefined) {
const ior = finite(properties.ior, "ior");
if (ior !== 0 && ior < 1) throw new RangeError("ior must be 0 or at least 1");
setFloat(14, ior, "ior");
setFloat(15, ior === 0 ? 1 : ((ior - 1) / (ior + 1)) ** 2, "ior");
}
this.#array.write(this.#key, words);
return this;
}
#float(lane) { return wordToFloat(this.#array.read(this.#key)[lane]); }
#floats(start, length) { return this.#array.read(this.#key).slice(start, start + length).map(wordToFloat); }
}
+96
View File
@@ -0,0 +1,96 @@
/** Shared render-data snapshot protocol consumed by the mesh-handle BVH worker. */
export const SNAPSHOT = Object.freeze({
MAGIC: 0x504e5359, BLOB_MAGIC: 0x32534452, VERSION: 1, BYTES: 256,
SLOTS: 3, SLOT_BYTES: 64, SCHEMA: 2, INIT: 0, OPEN: 1, FAILED: 2,
CLOSED: 3, FREE: 0, WRITING: 1, READY: 2, READING: 3,
});
export const STREAM_NAMES = ["meshSlot", "meshGeneration", "meshLocalMin", "meshLocalMax", "instanceSlot", "instanceGeneration", "instanceMeshSlot", "instanceMeshGeneration", "instanceModel", "instanceWorldMin", "instanceWorldMax", "instanceType"];
const COMPONENTS = [1, 1, 3, 3, 1, 1, 1, 1, 16, 3, 3, 16];
const SCALARS = [1, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1];
const STRIDES = COMPONENTS.map(n => n * 4);
export class SnapshotProtocolError extends Error {
constructor(code) { super(code); this.code = code; this.name = "SnapshotProtocolError"; }
}
const bad = code => { throw new SnapshotProtocolError(code); };
const add = (a, b) => { const n = a + b; if (!Number.isSafeInteger(n) || n > 0xffffffff) bad("BAD_RANGE"); return n; };
const mul = (a, b) => { const n = a * b; if (!Number.isSafeInteger(n) || n > 0xffffffff) bad("BAD_RANGE"); return n; };
export class SnapshotReader {
constructor(memory, controlPtr) {
if (!memory || !(memory.buffer instanceof SharedArrayBuffer)) bad("BAD_MEMORY");
if (!Number.isInteger(controlPtr) || controlPtr < 0 || controlPtr % 64 || add(controlPtr, 256) > memory.buffer.byteLength) bad("BAD_CONTROL_POINTER");
this.memory = memory; this.controlPtr = controlPtr; this.buffer = null;
this.refresh(); this.validateControl();
}
refresh() {
if (this.buffer === this.memory.buffer) return;
this.buffer = this.memory.buffer;
if (add(this.controlPtr, 256) > this.buffer.byteLength) bad("BAD_CONTROL_POINTER");
this.control = new Int32Array(this.buffer, this.controlPtr, 64);
}
validateControl() {
const h = this.control;
if ((Atomics.load(h, 0) >>> 0) !== SNAPSHOT.MAGIC) bad("BAD_MAGIC");
if ((Atomics.load(h, 1) >>> 0) !== SNAPSHOT.VERSION) bad("BAD_VERSION");
if ((Atomics.load(h, 2) >>> 0) !== SNAPSHOT.BYTES || (Atomics.load(h, 3) >>> 0) !== SNAPSHOT.SLOTS || (Atomics.load(h, 4) >>> 0) !== SNAPSHOT.SLOT_BYTES || (Atomics.load(h, 5) >>> 0) !== SNAPSHOT.SCHEMA) bad("BAD_LAYOUT");
const lifecycle = Atomics.load(h, 6) >>> 0;
if (lifecycle > SNAPSHOT.CLOSED) bad("BAD_LIFECYCLE");
if ((Atomics.load(h, 15) >>> 0) !== 0) bad("BAD_RESERVED");
}
latest() {
this.refresh(); this.validateControl();
for (let tries = 0; tries < 16; tries++) {
const a = Atomics.load(this.control, 7) >>> 0;
if (a & 1) continue;
const lifecycle = Atomics.load(this.control, 6) >>> 0;
const value = { lifecycle, epoch: Atomics.load(this.control, 8) >>> 0, slot: Atomics.load(this.control, 9) >>> 0, revisionLo: Atomics.load(this.control, 10) >>> 0, revisionHi: Atomics.load(this.control, 11) >>> 0, wasmPages: Atomics.load(this.control, 12) >>> 0, layoutEpoch: Atomics.load(this.control, 13) >>> 0, error: Atomics.load(this.control, 14) >>> 0 };
const b = Atomics.load(this.control, 7) >>> 0;
if (a === b && !(b & 1)) {
if (lifecycle === SNAPSHOT.FAILED) bad("SNAPSHOT_FAILED");
if (lifecycle === SNAPSHOT.CLOSED) bad("SNAPSHOT_CLOSED");
if (lifecycle !== SNAPSHOT.INIT && lifecycle !== SNAPSHOT.OPEN) bad("BAD_LIFECYCLE");
if (value.wasmPages && value.wasmPages > this.buffer.byteLength / 65536) bad("BAD_WASM_PAGES");
return value;
}
}
bad("UNSTABLE_CONTROL");
}
transaction(fn, expectedEpoch = 0) {
this.refresh();
const latest = this.latest();
if (!latest.epoch || latest.slot >= SNAPSHOT.SLOTS || (expectedEpoch && latest.epoch !== expectedEpoch)) return null;
let control = this.control;
const base = 16 + latest.slot * 16;
if (Atomics.compareExchange(control, base, SNAPSHOT.READY, SNAPSHOT.READING) !== SNAPSHOT.READY) return null;
try {
// memory.grow replaces memory.buffer even after the slot has been pinned.
this.refresh(); control = this.control;
const slot = Array.from({length: 16}, (_, i) => Atomics.load(control, base + i) >>> 0);
if (slot[0] !== SNAPSHOT.READING || slot[1] !== latest.epoch || slot[2] !== latest.layoutEpoch || slot[5] !== latest.revisionLo || slot[6] !== latest.revisionHi || slot[9] !== SNAPSHOT.SCHEMA || slot[10] !== 64) return null;
if (slot.slice(11).some(Boolean)) bad("BAD_SLOT_RESERVED");
const ptr = slot[3], bytes = slot[4];
if (ptr % 16 || bytes < 448 || bytes % 16 || add(ptr, bytes) > this.buffer.byteLength) bad("BAD_SLOT");
const u32 = new Uint32Array(this.buffer, ptr, bytes / 4);
if (u32[0] !== SNAPSHOT.BLOB_MAGIC || u32[1] !== SNAPSHOT.SCHEMA || u32[2] !== 64 || u32[3] !== bytes || u32[4] !== slot[1] || u32[5] !== slot[5] || u32[6] !== slot[6] || u32[7] !== 12 || u32[8] !== 64 || u32[9] !== 32 || u32[10] !== slot[7] || u32[11] !== slot[8] || u32[12] !== 0x01020304 || u32[13] !== 3) bad("BAD_BLOB");
if (u32[14] || u32[15]) bad("BAD_BLOB_RESERVED");
const ranges = [], streams = {};
for (let i = 0; i < 12; i++) {
const d = 16 + i * 8, semantic = u32[d], scalar = u32[d + 1], offset = u32[d + 2], count = u32[d + 3], components = u32[d + 4], stride = u32[d + 5], width = u32[d + 6], reserved = u32[d + 7];
const want = i < 4 ? slot[7] : slot[8];
if (semantic !== i + 1 || scalar !== SCALARS[i] || count !== want || components !== COMPONENTS[i] || stride !== STRIDES[i] || width !== 4 || reserved || offset < 448 || offset % 16) bad("BAD_DESCRIPTOR");
const end = add(offset, mul(stride, count));
if (end > bytes) bad("BAD_DESCRIPTOR_RANGE");
if (count) ranges.push([offset, end]);
const Type = scalar === 2 ? Float32Array : Uint32Array;
streams[STREAM_NAMES[i]] = new Type(this.buffer, add(ptr, offset), mul(count, components));
}
ranges.sort((a, b) => a[0] - b[0]);
for (let i = 1; i < ranges.length; i++) if (ranges[i][0] < ranges[i - 1][1]) bad("OVERLAPPING_STREAMS");
return fn(Object.freeze({epoch: slot[1], revisionLo: slot[5], revisionHi: slot[6], meshCount: slot[7], instanceCount: slot[8], streams: Object.freeze(streams)}));
} finally {
Atomics.store(control, base, SNAPSHOT.FREE); Atomics.notify(control, base);
}
}
}
+3 -1
View File
@@ -51,13 +51,15 @@ function normalizePipelines(raw = {}) {
vertexEntry: pipeline.vertexEntry ?? "vs_main",
fragmentEntry: pipeline.fragmentEntry ?? "fs_main",
doubleSided: pipeline.doubleSided ?? false,
material: pipeline.material ?? false,
};
if (
!identifier(result.name) ||
!identifier(result.vertexEntry) ||
!identifier(result.fragmentEntry) ||
typeof result.shader !== "string" ||
typeof result.doubleSided !== "boolean"
typeof result.doubleSided !== "boolean" ||
typeof result.material !== "boolean"
)
fail("AST_PIPELINE", "invalid render pipeline declaration");
return result;