Optimize viewer shaders and camera controls; sample GPU profiles asynchronously

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b382-1e79-712b-abd1-d29201c9bb5e
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-09-18 11:52:09 +00:00
co-authored by heaust
parent bc957cf40d
commit eb8aa0e9dc
21 changed files with 762 additions and 174 deletions
+62 -2
View File
@@ -292,7 +292,31 @@ test("Node, ArcRotateCamera, and light handles update their shared rows", async
assert.equal(scene.array("pointLights").row(light.id)[6], 3);
});
test("ArcRotateCamera pans one projected world pixel per pointer pixel", async () => {
test("ArcRotateCamera damps pan, orbit and zoom with reversed vertical orbit", async (t) => {
let now = 0;
let next = 1;
const frames = new Map<number, FrameRequestCallback>();
const originalRequest = globalThis.requestAnimationFrame;
const originalCancel = globalThis.cancelAnimationFrame;
t.mock.method(performance, "now", () => now);
globalThis.requestAnimationFrame = (callback) => {
const id = next++;
frames.set(id, callback);
return id;
};
globalThis.cancelAnimationFrame = (id) => {
frames.delete(id);
};
t.after(() => {
globalThis.requestAnimationFrame = originalRequest;
globalThis.cancelAnimationFrame = originalCancel;
});
const advance = (ms: number) => {
now += ms;
const callbacks = [...frames.values()];
frames.clear();
for (const callback of callbacks) callback(now);
};
const listeners = new Map<string, Set<(event: any) => void>>();
const element = {
clientWidth: 400,
@@ -329,7 +353,43 @@ test("ArcRotateCamera pans one projected world pixel per pointer pixel", async (
pointerId: 1,
});
dispatch("pointermove", { clientX: 120, clientY: 110 });
dispatch("pointerup", {});
advance(80);
close(camera.position, [-2 * (1 - Math.exp(-1)), 1 - Math.exp(-1), 10]);
advance(1600);
close(camera.position, [-2, 1, 10]);
advance(16);
assert.equal(frames.size, 0);
const orbit = () => {
dispatch("pointerdown", {
button: 0,
clientX: 100,
clientY: 100,
pointerId: 1,
});
dispatch("pointermove", { clientX: 140, clientY: 120 });
dispatch("pointerup", {});
};
orbit();
advance(40);
const released = camera.alpha;
advance(40);
assert.ok(camera.alpha < released);
const pose = [camera.alpha, camera.beta];
close(pose, [
-0.2 * (1 - Math.exp(-1)),
Math.PI / 2 - 0.1 * (1 - Math.exp(-1)),
]);
camera.detachControls();
camera.alpha = 0;
camera.beta = Math.PI / 2;
camera.attachControls({ element });
orbit();
advance(80);
close([camera.alpha, camera.beta], pose);
dispatch("wheel", { deltaY: 100 });
advance(80);
close([camera.radius], [10 * Math.exp(0.1 * (1 - Math.exp(-1)))]);
camera.detachControls();
assert.equal(frames.size, 0);
});
+158
View File
@@ -4,6 +4,11 @@ import { Mesh } from "../src/Mesh";
import { FXAA } from "../src/PostProcesses/FXAA";
import { PBRMaterial } from "../src/materials/PBRMaterial";
import { Texture } from "../src/materials/Texture";
import { ShaderMaterial } from "../src/materials/ShaderMaterial";
import { PointLight } from "../src/lights/PointLight";
import { SpotLight } from "../src/lights/SpotLight";
import { DirectionalLight } from "../src/lights/DirectionalLight";
import { ComputePass } from "../src/ComputePass";
import {
bucketDraws,
createScene,
@@ -34,6 +39,11 @@ test("empty scenes use neutral authored defaults without placeholder geometry",
test("opaque PBR graph has no implicit environment and compacts sparse lights", async () => {
const scene = await createScene();
await Promise.all([
new PointLight(scene).ready,
new SpotLight(scene).ready,
new DirectionalLight(scene).ready,
]);
const material = new PBRMaterial(scene);
await material.ready;
const mesh = detailedMesh(scene, material);
@@ -451,3 +461,151 @@ test("serialized render graphs compile into explicitly selectable loadouts", asy
assert.equal(loadoutId, `loadout-${before + 1}`);
assert.equal(core.loadouts.at(-1), loadoutId);
});
test("packed ORM reads are shared only for the same texture and UV transform", async () => {
const scene = await createScene();
const orm = new Texture(scene, { size: [1, 1, 1] });
const other = new Texture(scene, { size: [1, 1, 1] });
await Promise.all([orm.ready, other.ready]);
for (const [occlusion, info, shared] of [
[orm, {}, true],
[other, {}, false],
[orm, { texCoord: 1 }, false],
[orm, { offset: [0.25, -0.1] }, false],
[orm, { scale: [2, 1] }, false],
[orm, { rotation: 0.4 }, false],
] as const) {
const material = new PBRMaterial(scene, {
metallicRoughnessTexture: orm,
occlusionTexture: occlusion,
textureInfos: [{}, {}, {}, {}, info],
});
await material.ready;
const mesh = await detailedMesh(scene, material).ready;
const graph = latestGraph(scene);
const pass = forwardPasses(graph).at(-1);
const code = renderPipeline(graph, pass.pipeline).code;
assert.equal(code.includes("mix(1.0, packedMaterial.r"), shared);
assert.equal(code.includes("textureSample(occlusionTexture"), !shared);
assert.equal(
pass.bindings.some((b: any) => b.group === 1 && b.binding === 5),
!shared,
);
await mesh.dispose();
}
});
test("light variants follow first/last light lifetimes and keep hot edits rebuild-free", async () => {
const scene = await createScene();
const material = await new PBRMaterial(scene).ready;
await detailedMesh(scene, material).ready;
const code = () => pbrPipelines(latestGraph(scene))[0].code;
assert.doesNotMatch(code(), /fn (point|spot|directional)Contribution/);
const first = await new PointLight(scene, { intensity: 0 }).ready;
assert.match(code(), /fn pointContribution/);
assert.doesNotMatch(code(), /fn (spot|directional)Contribution/);
assert.equal(code().match(/let nDotV =/g)?.length, 1);
assert.match(code(), /let diffuseColor = base.rgb \* \(1.0 - metallic\)/);
const before = testCore(scene).graphs.length;
first.intensity = 3;
first.radius = 0.4;
const second = await new PointLight(scene).ready;
await first.dispose();
assert.equal(testCore(scene).graphs.length, before);
assert.match(code(), /fn pointContribution/);
const spot = await new SpotLight(scene).ready;
const directional = await new DirectionalLight(scene).ready;
assert.match(code(), /fn spotContribution/);
assert.match(code(), /fn directionalContribution/);
await second.dispose();
assert.doesNotMatch(code(), /fn pointContribution/);
await spot.dispose();
await directional.dispose();
assert.doesNotMatch(code(), /fn (point|spot|directional)Contribution/);
});
test("direct canvas loadout preserves clear/depth and falls back for linear-color consumers", async () => {
const scene = await createScene({
directToCanvas: true,
clearColor: [0.02, 0.09, 0.3, 0.5],
});
const material = await new PBRMaterial(scene).ready;
const mesh = await detailedMesh(scene, material).ready;
const assertDirect = () => {
const graph = latestGraph(scene);
assert.equal(
graph.resources.textures.some((t: any) => t.id === "hdr"),
false,
);
assert.equal(
graph.passes.some((p: any) => p.id === "present"),
false,
);
const background = graph.passes.find(
(p: any) => p.id === "canvas-background",
);
assert.deepEqual(background.depth, { resource: "depth", clear: 1 });
assert.match(
renderPipeline(graph, "canvas-background").code,
/canvasOutput\(vec4<f32>\(0.02, 0.09, 0.3, 0.5\)\)/,
);
const forward = forwardPasses(graph)[0];
assert.deepEqual(forward.color, [{ resource: "canvas", load: "load" }]);
assert.deepEqual(forward.depth, { resource: "depth", load: "load" });
assert.equal(pbrPipelines(graph)[0].fragment.targets[0].format, "canvas");
assert.match(pbrPipelines(graph)[0].code, /unpack2x16float\(pack2x16float/);
};
const assertHdr = () =>
assert.ok(renderPipeline(latestGraph(scene), "present"));
assertDirect();
const effect = new FXAA(scene);
await effect.ready;
assertHdr();
await effect.dispose();
assertDirect();
for (const options of [{ flags: 2 }, { transmission: 0.5 }]) {
const special = await new PBRMaterial(scene, options).ready;
const extra = await detailedMesh(scene, special).ready;
assertHdr();
await extra.dispose();
assertDirect();
}
const compute = new ComputePass({
code: "@compute @workgroup_size(1) fn main() {}",
});
await scene.addComputePass(compute);
assertHdr();
assert.match(
pbrPipelines(latestGraph(scene))[0].code,
/fn directionalContribution/,
);
await scene.removeComputePass(compute.id);
assertDirect();
const custom = new ShaderMaterial(scene, { code: "custom WGSL" });
await custom.ready;
assertHdr();
await custom.dispose();
assertDirect();
const basic = await new Mesh(scene, {
vertexData: { positions: triangle.positions, indices: triangle.indices },
}).ready;
assertHdr();
await basic.dispose();
assertDirect();
const mask = await new PBRMaterial(scene, { flags: 1 }).ready;
const masked = await detailedMesh(scene, mask).ready;
assertDirect();
assert.match(
pbrPipelines(latestGraph(scene)).find((p: any) =>
p.code.includes("discard"),
).code,
/canvasOutput/,
);
await masked.dispose();
await mesh.dispose();
assertHdr();
const ldr = await createScene({ directToCanvas: true, hdr: false });
const ldrMaterial = await new PBRMaterial(ldr).ready;
await detailedMesh(ldr, ldrMaterial).ready;
assert.ok(renderPipeline(latestGraph(ldr), "present"));
});