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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
|
||||
@@ -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
@@ -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 playground’s **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" />
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user