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:
@@ -6,7 +6,13 @@ import { ArcRotateCamera } from "../src/camera/ArcRotateCamera";
|
||||
import { PointLight } from "../src/lights/PointLight";
|
||||
import { PBRMaterial } from "../src/materials/PBRMaterial";
|
||||
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>) {
|
||||
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);
|
||||
close(scene.geometryData(mesh.geometryId, "positions")!, triangle.positions);
|
||||
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] });
|
||||
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);
|
||||
assert.equal(clone.materialId, secondMaterial.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],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user