Optimize forward rendering and add benchmark

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-20 14:36:51 +00:00
co-authored by heaust
parent 000bfd63bf
commit abfa428464
18 changed files with 390 additions and 97 deletions
+29
View File
@@ -28,6 +28,7 @@ const fps = ref(0);
const profilerOpen = ref(false);
const profilerSupported = ref(null);
const profile = ref(null);
const adapterInfo = ref("");
let generation = 0;
let current;
let stopProfile;
@@ -88,6 +89,20 @@ async function attachProfiler(runId = generation) {
profile.value = null;
profilerSupported.value = null;
if (!profilerOpen.value) return;
if (!adapterInfo.value) {
const adapter = await navigator.gpu?.requestAdapter({
powerPreference: "high-performance",
});
const info = adapter?.info;
adapterInfo.value = [
info?.vendor,
info?.architecture,
info?.device,
info?.description,
]
.filter(Boolean)
.join(" · ");
}
const core = current?.scene?.core ?? current?.core;
if (!core?.onProfile || !core?.setProfiler) {
profilerSupported.value = false;
@@ -273,6 +288,13 @@ onUnmounted(() => {
<strong>GPU passes</strong>
<span v-if="profile">{{ profile.milliseconds.toFixed(2) }} ms</span>
</div>
<div v-if="profile" class="profiler-meta">
<span>{{ adapterInfo || profile.adapter }}</span>
<span>
{{ profile.canvas.width }}×{{ profile.canvas.height }} ·
{{ profile.wallMilliseconds.toFixed(2) }} ms wall
</span>
</div>
<p v-if="profilerSupported === false">
Timestamp queries are unavailable on this GPU.
</p>
@@ -522,6 +544,13 @@ canvas {
color: #60a5fa;
font-weight: 700;
}
.profiler-meta {
display: grid;
gap: 3px;
margin: -5px 0 12px;
color: #64748b;
font-size: 10px;
}
.profiler p {
color: #94a3b8;
}
+94
View File
@@ -365,6 +365,99 @@ return {
},
};`;
const benchmark = `import { ArcRotateCamera, Mesh, PBRMaterial, Scene, Texture } from "@yawn/handles";
const parameters = new URL(location.href).searchParams;
const draws = Math.max(1, Math.min(512, Number(parameters.get("draws")) || 138));
const grid = Math.max(1, Math.min(256, Number(parameters.get("grid")) || 128));
const overdraw = parameters.has("overdraw");
const columns = 12;
const positions = [];
const normals = [];
const uvs = [];
const indices = [];
for (let y = 0; y <= grid; y++) {
for (let x = 0; x <= grid; x++) {
positions.push(x / grid * 2 - 1, y / grid * 2 - 1, 0);
normals.push(0, 0, 1);
uvs.push(x / grid * 32, y / grid * 32);
}
}
for (let y = 0; y < grid; y++) {
for (let x = 0; x < grid; x++) {
const first = y * (grid + 1) + x;
indices.push(
first, first + 1, first + grid + 2,
first, first + grid + 2, first + grid + 1,
);
}
}
const pixels = new OffscreenCanvas(1024, 1024);
const context = pixels.getContext("2d");
for (let y = 0; y < 64; y++) {
for (let x = 0; x < 64; x++) {
context.fillStyle = (x + y) % 2 ? "#e2e8f0" : "#172554";
context.fillRect(x * 16, y * 16, 16, 16);
}
}
const scene = new Scene(canvas, { fps: 1000, hdr: true });
await scene.ready;
log("Benchmark core ready; preparing geometry…");
const camera = new ArcRotateCamera(scene, {
targetPosition: [0, 0, 0],
alpha: 0,
beta: Math.PI / 2,
radius: 3,
aspect: canvas.width / canvas.height,
});
await camera.ready;
log("Benchmark camera ready; compiling graph…");
let material;
let source;
const meshes = [];
await scene.batchGraphUpdates(async () => {
const texture = new Texture(scene, {
source: pixels.transferToImageBitmap(),
format: "rgba8unorm-srgb",
});
await texture.ready;
material = new PBRMaterial(scene, {
baseColorTexture: texture,
roughness: 0.45,
});
await material.ready;
source = new Mesh(scene, {
material,
vertexData: { positions, normals, uvs, indices },
});
await source.ready;
meshes.push(source);
for (let index = 1; index < draws; index++) meshes.push(source.clone());
await Promise.all(meshes.slice(1).map((mesh) => mesh.ready));
for (let index = 0; index < meshes.length; index++) {
const column = index % columns;
const row = Math.floor(index / columns);
meshes[index].position = overdraw
? [0, 0, index * 0.01]
: [
-1 + (column + 0.5) * 2 / columns,
1 - (row + 0.5) * 2 / columns,
0,
];
meshes[index].scale = overdraw
? [0.9, 0.9, 1]
: [0.9 / columns, 0.9 / columns, 1];
}
});
const triangles = draws * grid * grid * 2;
log(\`Deterministic \${overdraw ? "overdraw" : "geometry"} benchmark: \${draws} draws, \${triangles.toLocaleString()} triangles, 1024² minified texture.\`);
log("Open Profile and compare Forward GPU time, not page load time.");
return { scene, meshes, material, camera, dispose: () => scene.dispose() };`;
const core = `import { YawnCore } from "@yawn/core";
const encode = (value) => {
@@ -403,5 +496,6 @@ export const playgrounds = {
compute: { title: "Compute pass", code: compute },
post: { title: "HDR post processing", code: post },
importing: { title: "glTF import and BVH picking", code: importing },
benchmark: { title: "Forward benchmark", code: benchmark },
core: { title: "Direct core graph", code: core },
};
+2 -2
View File
@@ -1,6 +1,6 @@
# Cameras and controls
Every camera allocates a generic `cameras` slot and a transform node. Projection, lens, controller state, and transforms are direct SAB rows after construction.
Every camera allocates generic `cameras`, `cameraMatrices`, and transform rows. Camera handles precompute the view-projection matrix when lens or transform values change, so the forward vertex shader performs four dot products instead of rebuilding the camera projection for every vertex.
## Shared lens and projection controls
@@ -72,7 +72,7 @@ follow.stop();
follow.start();
```
The input and follow loops never post camera updates to core: they read and mutate the same camera, position, and quaternion rows that any other worker can use.
The input and follow loops never post camera updates to core: they mutate the position, quaternion, camera, and derived matrix rows directly in shared memory.
<Playground example="cameras" />
+3 -1
View File
@@ -56,7 +56,9 @@ await core.setProfiler(false);
stop();
```
`new YawnCore(canvas, { debug: true })` enables the same timestamp-query mode at startup. Timings describe the physical compute and render passes produced by graph compilation, so 138 compatible draws appear as one bundled forward pass. The fullscreen playgrounds **Profile** button shows the stream in a toggleable sidebar.
`new YawnCore(canvas, { debug: true })` enables the same timestamp-query mode at startup. Timings describe physical GPU passes and actual compiled draw counts; compatible indexed draws over consecutive instances collapse into one command. The sidebar also reports canvas size and wall-clock completion time.
The saved **Forward benchmark** playground renders 138 logical objects and 4.5 million triangles. Add `&grid=32` to lower geometry density or `&overdraw=1` to stack the objects while diagnosing depth and fragment cost.
<Playground example="core" />
+1 -1
View File
@@ -30,7 +30,7 @@ await albedo.ready;
const textured = new PBRMaterial(scene, { baseColorTexture: albedo });
```
Creating or removing a `Texture` rebuilds the single graph loadout so the GPU resource is allocated up front. The addon decodes URLs outside core, then transfers an `ImageBitmap` to the render worker; compatible loadout rebuilds reuse the allocated GPU texture instead of uploading it again.
Creating or removing a `Texture` rebuilds the single graph loadout so the GPU resource is allocated up front. The addon generates and transfers a complete mip chain by default; pass `mipmaps: false` only for data that must remain single-level. Compatible loadout rebuilds reuse the allocated GPU texture without uploading it again.
## Custom WGSL