handles: one interleaved vertices/indices arena
Geometry becomes a span in a single 80-byte interleaved `vertices` array plus a span in a single `indices` array, allocated through the core's span allocator instead of one arena array per mesh attribute. Draws are always indexed with baseVertex; face-material overrides emit one draw per run of same-material faces rather than one per face. Vertex data is validated as a whole before any span is touched, same-geometry mutations are serialised, and render signal 4 is held across a span change until the graph reflecting it is published. Sibling arrays grow to the primary array's capacity in one batch; reserve() takes vertex/index totals. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b2dd-db32-712d-99ce-12ce4a86d979 Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
+27
-36
@@ -12,18 +12,17 @@ export const VertexKinds = Object.freeze([
|
|||||||
"indices",
|
"indices",
|
||||||
] as const);
|
] as const);
|
||||||
export type VertexKind = (typeof VertexKinds)[number];
|
export type VertexKind = (typeof VertexKinds)[number];
|
||||||
|
export type VertexStreams = Partial<Record<VertexKind, ArrayLike<number>>>;
|
||||||
export type MeshOptions = NodeOptions & {
|
export type MeshOptions = NodeOptions & {
|
||||||
geometryId?: number;
|
geometryId?: number;
|
||||||
material?: PBRMaterial;
|
material?: PBRMaterial;
|
||||||
vertexData?: Partial<Record<VertexKind, ArrayLike<number>>>;
|
vertexData?: VertexStreams;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A renderable Node; clones share geometry until either clone mutates vertex data. */
|
/** A renderable Node; clones share geometry until either clone mutates vertex data. */
|
||||||
export class Mesh extends Node {
|
export class Mesh extends Node {
|
||||||
geometryId: number;
|
geometryId: number;
|
||||||
vertexCount = 0;
|
|
||||||
indexCount = 0;
|
|
||||||
instanceOf = -1;
|
instanceOf = -1;
|
||||||
readonly faceMaterials = new Map<number, number>();
|
readonly faceMaterials = new Map<number, number>();
|
||||||
#registered = false;
|
#registered = false;
|
||||||
@@ -34,13 +33,6 @@ export class Mesh extends Node {
|
|||||||
const nodeReady = this.ready;
|
const nodeReady = this.ready;
|
||||||
this.ready = nodeReady.then(async () => {
|
this.ready = nodeReady.then(async () => {
|
||||||
if (options.material) await options.material.ready;
|
if (options.material) await options.material.ready;
|
||||||
await scene.ensureRows(`mesh.${this.id}.faceMaterials`, 1, 16, "u32");
|
|
||||||
this.vertexCount =
|
|
||||||
Number(scene.geometryData(this.geometryId, "positions")?.length ?? 0) /
|
|
||||||
3;
|
|
||||||
this.indexCount = Number(
|
|
||||||
scene.geometryData(this.geometryId, "indices")?.length ?? 0,
|
|
||||||
);
|
|
||||||
this.instanceOf =
|
this.instanceOf =
|
||||||
options.geometryId === undefined ? this.id : this.geometryId;
|
options.geometryId === undefined ? this.id : this.geometryId;
|
||||||
scene
|
scene
|
||||||
@@ -51,8 +43,8 @@ export class Mesh extends Node {
|
|||||||
options.visible === false ? 0 : 1,
|
options.visible === false ? 0 : 1,
|
||||||
this.instanceOf,
|
this.instanceOf,
|
||||||
]);
|
]);
|
||||||
for (const [kind, data] of Object.entries(options.vertexData ?? {}))
|
if (options.vertexData)
|
||||||
await this.#setVertexData(kind as VertexKind, data!, false);
|
await this.#setVertexData(options.vertexData, false);
|
||||||
this.#writeBounds();
|
this.#writeBounds();
|
||||||
this.#registered = true;
|
this.#registered = true;
|
||||||
await scene.registerMesh(this);
|
await scene.registerMesh(this);
|
||||||
@@ -60,6 +52,14 @@ export class Mesh extends Node {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get vertexCount() {
|
||||||
|
return this.scene.geometry(this.geometryId).vertexCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get indexCount() {
|
||||||
|
return this.scene.geometry(this.geometryId).indexCount;
|
||||||
|
}
|
||||||
|
|
||||||
get isVisible() {
|
get isVisible() {
|
||||||
if (this.id < 0) throw new Error("Await mesh.ready before reading it");
|
if (this.id < 0) throw new Error("Await mesh.ready before reading it");
|
||||||
return this.scene.array("meshInfo").row(this.id)[2] !== 0;
|
return this.scene.array("meshInfo").row(this.id)[2] !== 0;
|
||||||
@@ -87,20 +87,22 @@ export class Mesh extends Node {
|
|||||||
return new Mesh(this.scene, { ...options, geometryId: this.geometryId });
|
return new Mesh(this.scene, { ...options, geometryId: this.geometryId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async setVertexData(kind: VertexKind, data: ArrayLike<number>) {
|
/** Replaces one stream, or several at once, repacking the geometry a single time. */
|
||||||
if (!VertexKinds.includes(kind))
|
async setVertexData(
|
||||||
throw new TypeError(`Unknown vertex kind: ${kind}`);
|
kind: VertexKind | VertexStreams,
|
||||||
|
data?: ArrayLike<number>,
|
||||||
|
) {
|
||||||
|
const streams = typeof kind === "string" ? { [kind]: data } : kind;
|
||||||
|
for (const name of Object.keys(streams))
|
||||||
|
if (!VertexKinds.includes(name as VertexKind))
|
||||||
|
throw new TypeError(`Unknown vertex kind: ${name}`);
|
||||||
await this.ready;
|
await this.ready;
|
||||||
await this.#setVertexData(kind, data, true);
|
await this.#setVertexData(streams, true);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
async #setVertexData(
|
async #setVertexData(streams: VertexStreams, makeUnique: boolean) {
|
||||||
kind: VertexKind,
|
if (makeUnique && this.scene.geometry(this.geometryId).references > 1) {
|
||||||
data: ArrayLike<number>,
|
|
||||||
makeUnique: boolean,
|
|
||||||
) {
|
|
||||||
if (makeUnique && this.scene.geometryReferences(this.geometryId) > 1) {
|
|
||||||
const original = this.geometryId;
|
const original = this.geometryId;
|
||||||
this.geometryId = await this.scene.cloneGeometry(original);
|
this.geometryId = await this.scene.cloneGeometry(original);
|
||||||
this.scene.releaseGeometry(original);
|
this.scene.releaseGeometry(original);
|
||||||
@@ -117,10 +119,8 @@ export class Mesh extends Node {
|
|||||||
this.id,
|
this.id,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (kind === "positions") this.vertexCount = data.length / 3;
|
await this.scene.setVertexData(this.geometryId, streams, makeUnique);
|
||||||
if (kind === "indices") this.indexCount = data.length;
|
if (streams.positions) this.#writeBounds();
|
||||||
await this.scene.setVertexData(this.geometryId, kind, data, makeUnique);
|
|
||||||
if (kind === "positions") this.#writeBounds();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMaterialForFaces(material: PBRMaterial, faces: number | number[]) {
|
async setMaterialForFaces(material: PBRMaterial, faces: number | number[]) {
|
||||||
@@ -128,19 +128,11 @@ export class Mesh extends Node {
|
|||||||
if (material.scene !== this.scene)
|
if (material.scene !== this.scene)
|
||||||
throw new Error("Material must belong to the same Scene");
|
throw new Error("Material must belong to the same Scene");
|
||||||
const list = Array.isArray(faces) ? faces : [faces];
|
const list = Array.isArray(faces) ? faces : [faces];
|
||||||
const maximum = Math.max(...list);
|
|
||||||
const array = await this.scene.ensureRows(
|
|
||||||
`mesh.${this.id}.faceMaterials`,
|
|
||||||
maximum + 1,
|
|
||||||
16,
|
|
||||||
"u32",
|
|
||||||
);
|
|
||||||
this.scene.markDirty(true);
|
|
||||||
for (const face of list) {
|
for (const face of list) {
|
||||||
if (!Number.isInteger(face) || face < 0) throw new RangeError("face");
|
if (!Number.isInteger(face) || face < 0) throw new RangeError("face");
|
||||||
array.row(face)[0] = material.id + 1;
|
|
||||||
this.faceMaterials.set(face, material.id);
|
this.faceMaterials.set(face, material.id);
|
||||||
}
|
}
|
||||||
|
this.scene.markDirty(true);
|
||||||
await this.scene.updateRenderGraph();
|
await this.scene.updateRenderGraph();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -168,7 +160,6 @@ export class Mesh extends Node {
|
|||||||
this.#registered = false;
|
this.#registered = false;
|
||||||
await this.scene.unregisterMesh(this);
|
await this.scene.unregisterMesh(this);
|
||||||
}
|
}
|
||||||
await this.scene.core.deleteRows(`mesh.${this.id}.faceMaterials`);
|
|
||||||
await super.dispose();
|
await super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+555
-546
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,20 @@ export async function importGltf(scene: Scene, url: string | URL) {
|
|||||||
await scene.reserve({
|
await scene.reserve({
|
||||||
nodes: result.primitives.length,
|
nodes: result.primitives.length,
|
||||||
materials: result.materials.length,
|
materials: result.materials.length,
|
||||||
|
vertices: result.primitives.reduce(
|
||||||
|
(total: number, primitive: any) => total + primitive.positions.length / 3,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
// Each geometry's index span is padded to whole rows of four.
|
||||||
|
indices: result.primitives.reduce(
|
||||||
|
(total: number, primitive: any) =>
|
||||||
|
total +
|
||||||
|
Math.ceil(
|
||||||
|
(primitive.indices?.length ?? primitive.positions.length / 3) / 4,
|
||||||
|
) *
|
||||||
|
4,
|
||||||
|
0,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
let textures: Texture[] = [];
|
let textures: Texture[] = [];
|
||||||
const imported = await scene.batchGraphUpdates(async () => {
|
const imported = await scene.batchGraphUpdates(async () => {
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ import { ArcRotateCamera } from "../src/camera/ArcRotateCamera";
|
|||||||
import { PointLight } from "../src/lights/PointLight";
|
import { PointLight } from "../src/lights/PointLight";
|
||||||
import { PBRMaterial } from "../src/materials/PBRMaterial";
|
import { PBRMaterial } from "../src/materials/PBRMaterial";
|
||||||
import type { Texture } from "../src/materials/Texture";
|
import type { Texture } from "../src/materials/Texture";
|
||||||
import { createScene, testCore, triangle } from "./scene-helpers";
|
import {
|
||||||
|
createScene,
|
||||||
|
forwardPasses,
|
||||||
|
latestGraph,
|
||||||
|
testCore,
|
||||||
|
triangle,
|
||||||
|
} from "./scene-helpers";
|
||||||
|
|
||||||
function close(actual: ArrayLike<number>, expected: ArrayLike<number>) {
|
function close(actual: ArrayLike<number>, expected: ArrayLike<number>) {
|
||||||
assert.equal(actual.length, expected.length);
|
assert.equal(actual.length, expected.length);
|
||||||
@@ -139,6 +145,13 @@ test("Mesh packs geometry and bounds, then clones with copy-on-write", async ()
|
|||||||
assert.equal(mesh.indexCount, 3);
|
assert.equal(mesh.indexCount, 3);
|
||||||
close(scene.geometryData(mesh.geometryId, "positions")!, triangle.positions);
|
close(scene.geometryData(mesh.geometryId, "positions")!, triangle.positions);
|
||||||
close(scene.array("bounds").row(mesh.id), [-1, -1, 0, 0, 1, 1, 0, 0]);
|
close(scene.array("bounds").row(mesh.id), [-1, -1, 0, 0, 1, 1, 0, 0]);
|
||||||
|
// Vertex 1 lives in the shared interleaved array: position, normal, uv, uv2, tangent, colour.
|
||||||
|
const geometry = scene.geometry(mesh.geometryId);
|
||||||
|
close(
|
||||||
|
scene.array("vertices").row(geometry.vertexRow + 1),
|
||||||
|
[1, -1, 0, 0, 0, 0, 1, 0, 1, 0, 0.9, 0.1, 1, 0, 0, 1, 0, 1, 0, 1],
|
||||||
|
);
|
||||||
|
close(scene.array("indices").row(geometry.indexRow), [0, 1, 2, 0]);
|
||||||
|
|
||||||
const clone = mesh.clone({ position: [2, 0, 0] });
|
const clone = mesh.clone({ position: [2, 0, 0] });
|
||||||
await clone.ready;
|
await clone.ready;
|
||||||
@@ -155,9 +168,81 @@ test("Mesh packs geometry and bounds, then clones with copy-on-write", async ()
|
|||||||
await clone.setMaterialForFaces(firstMaterial, 0);
|
await clone.setMaterialForFaces(firstMaterial, 0);
|
||||||
assert.equal(clone.materialId, secondMaterial.id);
|
assert.equal(clone.materialId, secondMaterial.id);
|
||||||
assert.equal(clone.faceMaterials.get(0), firstMaterial.id);
|
assert.equal(clone.faceMaterials.get(0), firstMaterial.id);
|
||||||
assert.equal(
|
});
|
||||||
scene.array(`mesh.${clone.id}.faceMaterials`).row(0)[0],
|
|
||||||
firstMaterial.id + 1,
|
test("geometry spans validate before mutating, move intact, and inhibit rendering until published", async () => {
|
||||||
|
const scene = await createScene();
|
||||||
|
const material = new PBRMaterial(scene);
|
||||||
|
await material.ready;
|
||||||
|
const first = new Mesh(scene, { material, vertexData: triangle });
|
||||||
|
const second = new Mesh(scene, { material, vertexData: triangle });
|
||||||
|
await Promise.all([first.ready, second.ready]);
|
||||||
|
const core = testCore(scene);
|
||||||
|
const before = {
|
||||||
|
graphs: core.graphs.length,
|
||||||
|
first: { ...scene.geometry(first.geometryId) },
|
||||||
|
second: { ...scene.geometry(second.geometryId) },
|
||||||
|
};
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
first.setVertexData("indices", [0, 1, 3]),
|
||||||
|
/VERTEX_DATA: indices\[2\]/u,
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
first.setVertexData("indices", [0, 1, 1.5]),
|
||||||
|
/VERTEX_DATA: indices\[2\]/u,
|
||||||
|
);
|
||||||
|
await assert.rejects(
|
||||||
|
first.setVertexData("normals", [0, 0, 1]),
|
||||||
|
/VERTEX_DATA: normals/u,
|
||||||
|
);
|
||||||
|
assert.deepEqual({ ...scene.geometry(first.geometryId) }, before.first);
|
||||||
|
close(scene.geometryData(first.geometryId, "indices")!, [0, 1, 2]);
|
||||||
|
assert.equal(core.graphs.length, before.graphs);
|
||||||
|
|
||||||
|
// Hold the older loadout switch open while the first mesh grows past its span.
|
||||||
|
let releaseSwitch!: () => void;
|
||||||
|
const switching = new Promise<void>((resolve) => (releaseSwitch = resolve));
|
||||||
|
const switchLoadout = core.switchLoadout.bind(core);
|
||||||
|
core.switchLoadout = (id) => switching.then(() => switchLoadout(id));
|
||||||
|
const older = scene.updateRenderGraph();
|
||||||
|
const grown = first.setVertexData({
|
||||||
|
positions: [0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0],
|
||||||
|
normals: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1],
|
||||||
|
tangents: Array(16).fill(0),
|
||||||
|
uvs: [0, 0, 1, 0, 0, 1, 1, 1],
|
||||||
|
uvs2: [0, 0, 1, 0, 0, 1, 1, 1],
|
||||||
|
colors: Array(16).fill(1),
|
||||||
|
indices: [0, 1, 2, 2, 1, 3],
|
||||||
|
});
|
||||||
|
const signals = core.array("signals").row(0);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve));
|
||||||
|
assert.equal(signals[4], 1);
|
||||||
|
assert.equal(core.loadouts.length, before.graphs);
|
||||||
|
releaseSwitch();
|
||||||
|
await older;
|
||||||
|
assert.equal(signals[4], 1);
|
||||||
|
core.switchLoadout = switchLoadout;
|
||||||
|
await grown;
|
||||||
|
assert.equal(signals[4], 0);
|
||||||
|
assert.equal(signals[5], 1);
|
||||||
|
|
||||||
|
const moved = scene.geometry(first.geometryId);
|
||||||
|
assert.equal(moved.vertexCount, 4);
|
||||||
|
assert.notEqual(moved.vertexRow, before.first.vertexRow);
|
||||||
|
assert.deepEqual({ ...scene.geometry(second.geometryId) }, before.second);
|
||||||
|
close(
|
||||||
|
scene.array("vertices").row(before.second.vertexRow + 1),
|
||||||
|
[1, -1, 0, 0, 0, 0, 1, 0, 1, 0, 0.9, 0.1, 1, 0, 0, 1, 0, 1, 0, 1],
|
||||||
|
);
|
||||||
|
close(scene.array("indices").row(moved.indexRow), [0, 1, 2, 2]);
|
||||||
|
const draws = forwardPasses(latestGraph(scene)).map((pass: any) => pass.draw);
|
||||||
|
assert.deepEqual(
|
||||||
|
draws.map((draw: any) => [draw.baseVertex, draw.firstIndex, draw.indices]),
|
||||||
|
[
|
||||||
|
[moved.vertexRow, moved.indexRow * 4, 6],
|
||||||
|
[before.second.vertexRow, before.second.indexRow * 4, 3],
|
||||||
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Vendored
+14
-5
@@ -87,7 +87,12 @@ declare module "@yawn/handles" {
|
|||||||
stride: number,
|
stride: number,
|
||||||
format: "f32" | "u32" | "i32",
|
format: "f32" | "u32" | "i32",
|
||||||
): Promise<SharedRows>;
|
): Promise<SharedRows>;
|
||||||
reserve(additional: { nodes?: number; materials?: number }): Promise<void>;
|
reserve(additional: {
|
||||||
|
nodes?: number;
|
||||||
|
materials?: number;
|
||||||
|
vertices?: number;
|
||||||
|
indices?: number;
|
||||||
|
}): Promise<void>;
|
||||||
batchWrites<T>(operation: () => T): T;
|
batchWrites<T>(operation: () => T): T;
|
||||||
batchGraphUpdates<T>(operation: () => T | Promise<T>): Promise<T>;
|
batchGraphUpdates<T>(operation: () => T | Promise<T>): Promise<T>;
|
||||||
compileRenderGraph(serialized: string): Promise<string>;
|
compileRenderGraph(serialized: string): Promise<string>;
|
||||||
@@ -149,18 +154,22 @@ declare module "@yawn/handles" {
|
|||||||
export interface MeshOptions extends NodeOptions {
|
export interface MeshOptions extends NodeOptions {
|
||||||
geometryId?: number;
|
geometryId?: number;
|
||||||
material?: PBRMaterial;
|
material?: PBRMaterial;
|
||||||
vertexData?: Partial<Record<VertexKind, ArrayLike<number>>>;
|
vertexData?: VertexStreams;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
}
|
}
|
||||||
|
export type VertexStreams = Partial<Record<VertexKind, ArrayLike<number>>>;
|
||||||
export class Mesh extends Node {
|
export class Mesh extends Node {
|
||||||
constructor(scene: Scene, options?: MeshOptions);
|
constructor(scene: Scene, options?: MeshOptions);
|
||||||
geometryId: number;
|
geometryId: number;
|
||||||
vertexCount: number;
|
readonly vertexCount: number;
|
||||||
indexCount: number;
|
readonly indexCount: number;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
material: PBRMaterial;
|
material: PBRMaterial;
|
||||||
clone(options?: Omit<MeshOptions, "geometryId" | "vertexData">): Mesh;
|
clone(options?: Omit<MeshOptions, "geometryId" | "vertexData">): Mesh;
|
||||||
setVertexData(kind: VertexKind, data: ArrayLike<number>): Promise<this>;
|
setVertexData(
|
||||||
|
kind: VertexKind | VertexStreams,
|
||||||
|
data?: ArrayLike<number>,
|
||||||
|
): Promise<this>;
|
||||||
setMaterialForFaces(
|
setMaterialForFaces(
|
||||||
material: PBRMaterial,
|
material: PBRMaterial,
|
||||||
faces: number | number[],
|
faces: number | number[],
|
||||||
|
|||||||
Reference in New Issue
Block a user