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:
@@ -12,6 +12,7 @@ export type GraphTexture = {
|
|||||||
format?: string;
|
format?: string;
|
||||||
size?: [number | "canvas", number | "canvas", number?];
|
size?: [number | "canvas", number | "canvas", number?];
|
||||||
usage?: string[];
|
usage?: string[];
|
||||||
|
mipLevelCount?: number;
|
||||||
transient?: boolean;
|
transient?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ export type GraphSampler = {
|
|||||||
id: string;
|
id: string;
|
||||||
magFilter?: "nearest" | "linear";
|
magFilter?: "nearest" | "linear";
|
||||||
minFilter?: "nearest" | "linear";
|
minFilter?: "nearest" | "linear";
|
||||||
|
mipmapFilter?: "nearest" | "linear";
|
||||||
addressModeU?: "clamp-to-edge" | "repeat" | "mirror-repeat";
|
addressModeU?: "clamp-to-edge" | "repeat" | "mirror-repeat";
|
||||||
addressModeV?: "clamp-to-edge" | "repeat" | "mirror-repeat";
|
addressModeV?: "clamp-to-edge" | "repeat" | "mirror-repeat";
|
||||||
};
|
};
|
||||||
|
|||||||
+28
-34
@@ -38,6 +38,7 @@ const rows = [
|
|||||||
["meshInfo", 16, "u32"],
|
["meshInfo", 16, "u32"],
|
||||||
["bounds", 32, "f32"],
|
["bounds", 32, "f32"],
|
||||||
["cameras", 80, "f32"],
|
["cameras", 80, "f32"],
|
||||||
|
["cameraMatrices", 80, "f32"],
|
||||||
["materials", 48, "f32"],
|
["materials", 48, "f32"],
|
||||||
["materialTextures", 32, "u32"],
|
["materialTextures", 32, "u32"],
|
||||||
["pointLights", 32, "f32"],
|
["pointLights", 32, "f32"],
|
||||||
@@ -94,7 +95,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|||||||
const basicForwardShader = /* wgsl */ `
|
const basicForwardShader = /* wgsl */ `
|
||||||
struct Accent { color: vec4<f32> }
|
struct Accent { color: vec4<f32> }
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@builtin(position) position: vec4<f32>,
|
@invariant @builtin(position) position: vec4<f32>,
|
||||||
@location(0) normal: vec3<f32>,
|
@location(0) normal: vec3<f32>,
|
||||||
@location(1) @interpolate(flat) mesh: u32,
|
@location(1) @interpolate(flat) mesh: u32,
|
||||||
@location(2) @interpolate(flat) material: u32,
|
@location(2) @interpolate(flat) material: u32,
|
||||||
@@ -107,7 +108,7 @@ struct VertexOutput {
|
|||||||
@group(0) @binding(4) var<storage, read> scales: array<vec4<f32>>;
|
@group(0) @binding(4) var<storage, read> scales: array<vec4<f32>>;
|
||||||
@group(0) @binding(5) var<storage, read> meshInfo: array<u32>;
|
@group(0) @binding(5) var<storage, read> meshInfo: array<u32>;
|
||||||
@group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>;
|
@group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>;
|
||||||
@group(0) @binding(7) var<storage, read> cameras: array<vec4<f32>>;
|
@group(0) @binding(7) var<storage, read> cameraMatrices: array<vec4<f32>>;
|
||||||
|
|
||||||
fn rotate(q: vec4<f32>, value: vec3<f32>) -> vec3<f32> {
|
fn rotate(q: vec4<f32>, value: vec3<f32>) -> vec3<f32> {
|
||||||
return value + 2.0 * cross(q.xyz, cross(q.xyz, value) + q.w * value);
|
return value + 2.0 * cross(q.xyz, cross(q.xyz, value) + q.w * value);
|
||||||
@@ -119,18 +120,14 @@ fn vertex(@location(0) point: vec3<f32>, @builtin(instance_index) packed: u32) -
|
|||||||
let visible = meshInfo[instance * 4u + 2u];
|
let visible = meshInfo[instance * 4u + 2u];
|
||||||
let transformed = rotate(quaternions[instance], point * scales[instance].xyz) + positions[instance].xyz;
|
let transformed = rotate(quaternions[instance], point * scales[instance].xyz) + positions[instance].xyz;
|
||||||
var clip = vec4<f32>(transformed, 1.0);
|
var clip = vec4<f32>(transformed, 1.0);
|
||||||
if (cameras[2].w != 0.0) {
|
if (cameraMatrices[4].w != 0.0) {
|
||||||
let cameraNode = u32(cameras[1].x);
|
let world = vec4(transformed, 1.0);
|
||||||
let inverse = vec4<f32>(-quaternions[cameraNode].xyz, quaternions[cameraNode].w);
|
clip = vec4(
|
||||||
let view = rotate(inverse, transformed - positions[cameraNode].xyz);
|
dot(cameraMatrices[0], world),
|
||||||
if (cameras[1].y == 1.0) {
|
dot(cameraMatrices[1], world),
|
||||||
let size = max(cameras[1].z, 0.0001);
|
dot(cameraMatrices[2], world),
|
||||||
clip = vec4<f32>(view.x / (size * cameras[0].y * 0.5), view.y / (size * 0.5), -view.z / cameras[0].w, 1.0);
|
dot(cameraMatrices[3], world),
|
||||||
} else {
|
);
|
||||||
let focal = 1.0 / tan(cameras[0].x * 0.5);
|
|
||||||
let depth = (-view.z * cameras[0].w - cameras[0].z * cameras[0].w) / (cameras[0].w - cameras[0].z);
|
|
||||||
clip = vec4<f32>(view.x * focal / cameras[0].y, view.y * focal, depth, -view.z);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var output: VertexOutput;
|
var output: VertexOutput;
|
||||||
output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), clip, visible != 0u);
|
output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), clip, visible != 0u);
|
||||||
@@ -159,7 +156,7 @@ function pbrShader(mask: number) {
|
|||||||
const normalTexture = mask & 4;
|
const normalTexture = mask & 4;
|
||||||
return /* wgsl */ `
|
return /* wgsl */ `
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@builtin(position) position: vec4<f32>,
|
@invariant @builtin(position) position: vec4<f32>,
|
||||||
@location(0) world: vec3<f32>,
|
@location(0) world: vec3<f32>,
|
||||||
@location(1) normal: vec3<f32>,
|
@location(1) normal: vec3<f32>,
|
||||||
@location(2) uv: vec2<f32>,
|
@location(2) uv: vec2<f32>,
|
||||||
@@ -175,7 +172,7 @@ struct VertexOutput {
|
|||||||
@group(0) @binding(4) var<storage, read> scales: array<vec4<f32>>;
|
@group(0) @binding(4) var<storage, read> scales: array<vec4<f32>>;
|
||||||
@group(0) @binding(5) var<storage, read> meshInfo: array<u32>;
|
@group(0) @binding(5) var<storage, read> meshInfo: array<u32>;
|
||||||
@group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>;
|
@group(0) @binding(6) var<storage, read> materials: array<vec4<f32>>;
|
||||||
@group(0) @binding(7) var<storage, read> cameras: array<vec4<f32>>;
|
@group(0) @binding(7) var<storage, read> cameraMatrices: array<vec4<f32>>;
|
||||||
${mask ? "@group(1) @binding(0) var materialSampler: sampler;" : ""}
|
${mask ? "@group(1) @binding(0) var materialSampler: sampler;" : ""}
|
||||||
${baseTexture ? "@group(1) @binding(1) var baseTexture: texture_2d<f32>;" : ""}
|
${baseTexture ? "@group(1) @binding(1) var baseTexture: texture_2d<f32>;" : ""}
|
||||||
${materialTexture ? "@group(1) @binding(2) var materialTexture: texture_2d<f32>;" : ""}
|
${materialTexture ? "@group(1) @binding(2) var materialTexture: texture_2d<f32>;" : ""}
|
||||||
@@ -197,18 +194,14 @@ fn vertex(
|
|||||||
let scale = scales[instance].xyz;
|
let scale = scales[instance].xyz;
|
||||||
let world = rotate(quaternions[instance], point * scale) + positions[instance].xyz;
|
let world = rotate(quaternions[instance], point * scale) + positions[instance].xyz;
|
||||||
var clip = vec4<f32>(world, 1.0);
|
var clip = vec4<f32>(world, 1.0);
|
||||||
if (cameras[2].w != 0.0) {
|
if (cameraMatrices[4].w != 0.0) {
|
||||||
let cameraNode = u32(cameras[1].x);
|
let homogeneous = vec4(world, 1.0);
|
||||||
let inverse = vec4<f32>(-quaternions[cameraNode].xyz, quaternions[cameraNode].w);
|
clip = vec4(
|
||||||
let view = rotate(inverse, world - positions[cameraNode].xyz);
|
dot(cameraMatrices[0], homogeneous),
|
||||||
if (cameras[1].y == 1.0) {
|
dot(cameraMatrices[1], homogeneous),
|
||||||
let size = max(cameras[1].z, 0.0001);
|
dot(cameraMatrices[2], homogeneous),
|
||||||
clip = vec4<f32>(view.x / (size * cameras[0].y * 0.5), view.y / (size * 0.5), -view.z / cameras[0].w, 1.0);
|
dot(cameraMatrices[3], homogeneous),
|
||||||
} else {
|
);
|
||||||
let focal = 1.0 / tan(cameras[0].x * 0.5);
|
|
||||||
let depth = (-view.z * cameras[0].w - cameras[0].z * cameras[0].w) / (cameras[0].w - cameras[0].z);
|
|
||||||
clip = vec4<f32>(view.x * focal / cameras[0].y, view.y * focal, depth, -view.z);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var output: VertexOutput;
|
var output: VertexOutput;
|
||||||
output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), clip, meshInfo[instance * 4u + 2u] != 0u);
|
output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), clip, meshInfo[instance * 4u + 2u] != 0u);
|
||||||
@@ -234,8 +227,7 @@ fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
let roughness = clamp(properties.y * ${materialTexture ? "packedMaterial.g" : "1.0"}, 0.04, 1.0);
|
let roughness = clamp(properties.y * ${materialTexture ? "packedMaterial.g" : "1.0"}, 0.04, 1.0);
|
||||||
var normal = normalize(input.normal);
|
var normal = normalize(input.normal);
|
||||||
${normalTexture ? "let tangent = normalize(input.tangent.xyz); let bitangent = normalize(cross(normal, tangent)) * input.tangent.w; let mapped = textureSample(normalTexture, materialSampler, input.uv).xyz * 2.0 - 1.0; normal = normalize(mat3x3<f32>(tangent, bitangent, normal) * vec3(mapped.xy * extra.y, mapped.z));" : ""}
|
${normalTexture ? "let tangent = normalize(input.tangent.xyz); let bitangent = normalize(cross(normal, tangent)) * input.tangent.w; let mapped = textureSample(normalTexture, materialSampler, input.uv).xyz * 2.0 - 1.0; normal = normalize(mat3x3<f32>(tangent, bitangent, normal) * vec3(mapped.xy * extra.y, mapped.z));" : ""}
|
||||||
let cameraNode = u32(cameras[1].x);
|
let view = normalize(cameraMatrices[4].xyz - input.world);
|
||||||
let view = normalize(positions[cameraNode].xyz - input.world);
|
|
||||||
let lightDirection = normalize(vec3<f32>(0.4, 0.7, 0.6));
|
let lightDirection = normalize(vec3<f32>(0.4, 0.7, 0.6));
|
||||||
let halfVector = normalize(lightDirection + view);
|
let halfVector = normalize(lightDirection + view);
|
||||||
let nDotL = max(dot(normal, lightDirection), 0.0);
|
let nDotL = max(dot(normal, lightDirection), 0.0);
|
||||||
@@ -683,7 +675,6 @@ export class Scene {
|
|||||||
])
|
])
|
||||||
addBuffer({ id, array, usage: ["storage"] });
|
addBuffer({ id, array, usage: ["storage"] });
|
||||||
addBuffer({ id: "accent", array: "sceneAccent", usage: ["uniform"] });
|
addBuffer({ id: "accent", array: "sceneAccent", usage: ["uniform"] });
|
||||||
|
|
||||||
computePipelines.push({
|
computePipelines.push({
|
||||||
id: "cluster-lights",
|
id: "cluster-lights",
|
||||||
code: clusterShader,
|
code: clusterShader,
|
||||||
@@ -747,6 +738,7 @@ export class Scene {
|
|||||||
id: "material-linear",
|
id: "material-linear",
|
||||||
magFilter: "linear",
|
magFilter: "linear",
|
||||||
minFilter: "linear",
|
minFilter: "linear",
|
||||||
|
mipmapFilter: "linear",
|
||||||
addressModeU: "repeat",
|
addressModeU: "repeat",
|
||||||
addressModeV: "repeat",
|
addressModeV: "repeat",
|
||||||
});
|
});
|
||||||
@@ -778,7 +770,7 @@ export class Scene {
|
|||||||
["node-scales", "nodeScales"],
|
["node-scales", "nodeScales"],
|
||||||
["mesh-info", "meshInfo"],
|
["mesh-info", "meshInfo"],
|
||||||
["materials", "materials"],
|
["materials", "materials"],
|
||||||
["cameras", "cameras"],
|
["camera-matrices", "cameraMatrices"],
|
||||||
])
|
])
|
||||||
addBuffer({ id, array, usage: ["storage"] });
|
addBuffer({ id, array, usage: ["storage"] });
|
||||||
const forwardPipelines = new Set<string>();
|
const forwardPipelines = new Set<string>();
|
||||||
@@ -841,7 +833,9 @@ export class Scene {
|
|||||||
const material =
|
const material =
|
||||||
draw.material ?? Number(this.array("meshInfo").row(mesh.id)[1]);
|
draw.material ?? Number(this.array("meshInfo").row(mesh.id)[1]);
|
||||||
if (mesh.id > 65535 || material > 65534)
|
if (mesh.id > 65535 || material > 65534)
|
||||||
throw new RangeError("Scene handle limit");
|
throw new RangeError(
|
||||||
|
`Scene handle limit: mesh ${mesh.id}, material ${material}`,
|
||||||
|
);
|
||||||
const pointers = this.array("materialTextures").row(material);
|
const pointers = this.array("materialTextures").row(material);
|
||||||
const texture = (lane: number) =>
|
const texture = (lane: number) =>
|
||||||
pointers[lane]
|
pointers[lane]
|
||||||
@@ -955,7 +949,7 @@ export class Scene {
|
|||||||
"node-scales",
|
"node-scales",
|
||||||
"mesh-info",
|
"mesh-info",
|
||||||
"materials",
|
"materials",
|
||||||
"cameras",
|
"camera-matrices",
|
||||||
].map((resource, binding) => ({
|
].map((resource, binding) => ({
|
||||||
group: 0,
|
group: 0,
|
||||||
binding,
|
binding,
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
import { Node, type NodeOptions } from "../Node";
|
import { Node, type NodeOptions } from "../Node";
|
||||||
import type { Scene } from "../Scene";
|
import type { Scene } from "../Scene";
|
||||||
|
|
||||||
|
function rotate(q: ArrayLike<number>, value: number[]) {
|
||||||
|
const [x, y, z] = value;
|
||||||
|
const tx = 2 * (q[1] * z - q[2] * y);
|
||||||
|
const ty = 2 * (q[2] * x - q[0] * z);
|
||||||
|
const tz = 2 * (q[0] * y - q[1] * x);
|
||||||
|
return [
|
||||||
|
x + q[3] * tx + q[1] * tz - q[2] * ty,
|
||||||
|
y + q[3] * ty + q[2] * tx - q[0] * tz,
|
||||||
|
z + q[3] * tz + q[0] * ty - q[1] * tx,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function dot(left: number[], right: ArrayLike<number>) {
|
||||||
|
return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
|
||||||
|
}
|
||||||
|
|
||||||
export type CameraOptions = NodeOptions & {
|
export type CameraOptions = NodeOptions & {
|
||||||
fov?: number;
|
fov?: number;
|
||||||
near?: number;
|
near?: number;
|
||||||
@@ -24,6 +40,7 @@ export class Camera extends Node {
|
|||||||
const nodeReady = this.ready;
|
const nodeReady = this.ready;
|
||||||
this.ready = nodeReady.then(async () => {
|
this.ready = nodeReady.then(async () => {
|
||||||
this.cameraId = await scene.core.allocateObject("cameras");
|
this.cameraId = await scene.core.allocateObject("cameras");
|
||||||
|
await scene.ensureRows("cameraMatrices", this.cameraId + 1, 80, "f32");
|
||||||
const row = scene.array("cameras").row(this.cameraId);
|
const row = scene.array("cameras").row(this.cameraId);
|
||||||
row.set([
|
row.set([
|
||||||
options.fov ?? Math.PI / 3,
|
options.fov ?? Math.PI / 3,
|
||||||
@@ -39,6 +56,7 @@ export class Camera extends Node {
|
|||||||
options.sensorWidth ?? 36,
|
options.sensorWidth ?? 36,
|
||||||
1,
|
1,
|
||||||
]);
|
]);
|
||||||
|
this.refreshMatrix();
|
||||||
return this;
|
return this;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -49,17 +67,20 @@ export class Camera extends Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get fov() { return this.cameraRow()[0]; }
|
get fov() { return this.cameraRow()[0]; }
|
||||||
set fov(value: number) { this.cameraRow()[0] = value; }
|
set fov(value: number) { this.cameraRow()[0] = value; this.refreshMatrix(); }
|
||||||
get aspect() { return this.cameraRow()[1]; }
|
get aspect() { return this.cameraRow()[1]; }
|
||||||
set aspect(value: number) { this.cameraRow()[1] = value; }
|
set aspect(value: number) { this.cameraRow()[1] = value; this.refreshMatrix(); }
|
||||||
get near() { return this.cameraRow()[2]; }
|
get near() { return this.cameraRow()[2]; }
|
||||||
set near(value: number) { this.cameraRow()[2] = value; }
|
set near(value: number) { this.cameraRow()[2] = value; this.refreshMatrix(); }
|
||||||
get far() { return this.cameraRow()[3]; }
|
get far() { return this.cameraRow()[3]; }
|
||||||
set far(value: number) { this.cameraRow()[3] = value; }
|
set far(value: number) { this.cameraRow()[3] = value; this.refreshMatrix(); }
|
||||||
get projection() { return this.cameraRow()[5] === 1 ? "orthographic" : "perspective"; }
|
get projection() { return this.cameraRow()[5] === 1 ? "orthographic" : "perspective"; }
|
||||||
set projection(value: "perspective" | "orthographic") { this.cameraRow()[5] = value === "orthographic" ? 1 : 0; }
|
set projection(value: "perspective" | "orthographic") {
|
||||||
|
this.cameraRow()[5] = value === "orthographic" ? 1 : 0;
|
||||||
|
this.refreshMatrix();
|
||||||
|
}
|
||||||
get orthoSize() { return this.cameraRow()[6]; }
|
get orthoSize() { return this.cameraRow()[6]; }
|
||||||
set orthoSize(value: number) { this.cameraRow()[6] = value; }
|
set orthoSize(value: number) { this.cameraRow()[6] = value; this.refreshMatrix(); }
|
||||||
get focalLength() { return this.cameraRow()[7]; }
|
get focalLength() { return this.cameraRow()[7]; }
|
||||||
set focalLength(value: number) { this.cameraRow()[7] = value; }
|
set focalLength(value: number) { this.cameraRow()[7] = value; }
|
||||||
get aperture() { return this.cameraRow()[8]; }
|
get aperture() { return this.cameraRow()[8]; }
|
||||||
@@ -69,6 +90,18 @@ export class Camera extends Node {
|
|||||||
get sensorWidth() { return this.cameraRow()[10]; }
|
get sensorWidth() { return this.cameraRow()[10]; }
|
||||||
set sensorWidth(value: number) { this.cameraRow()[10] = value; }
|
set sensorWidth(value: number) { this.cameraRow()[10] = value; }
|
||||||
|
|
||||||
|
override get position(): Float32Array { return super.position; }
|
||||||
|
override set position(value: ArrayLike<number>) {
|
||||||
|
super.position = value;
|
||||||
|
this.refreshMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
override get quaternion(): Float32Array { return super.quaternion; }
|
||||||
|
override set quaternion(value: ArrayLike<number>) {
|
||||||
|
super.quaternion = value;
|
||||||
|
this.refreshMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
lookAt(target: ArrayLike<number>) {
|
lookAt(target: ArrayLike<number>) {
|
||||||
if (target.length !== 3) throw new RangeError("camera target");
|
if (target.length !== 3) throw new RangeError("camera target");
|
||||||
const position = this.position;
|
const position = this.position;
|
||||||
@@ -86,6 +119,47 @@ export class Camera extends Node {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected refreshMatrix() {
|
||||||
|
if (this.id < 0 || this.cameraId < 0) return;
|
||||||
|
const camera = this.cameraRow();
|
||||||
|
const position = this.position;
|
||||||
|
const quaternion = this.quaternion;
|
||||||
|
const right = rotate(quaternion, [1, 0, 0]);
|
||||||
|
const up = rotate(quaternion, [0, 1, 0]);
|
||||||
|
const forward = rotate(quaternion, [0, 0, 1]);
|
||||||
|
let rows: number[][];
|
||||||
|
if (camera[5] === 1) {
|
||||||
|
const size = Math.max(camera[6], 0.0001);
|
||||||
|
const x = 2 / (size * camera[1]);
|
||||||
|
const y = 2 / size;
|
||||||
|
const z = -1 / camera[3];
|
||||||
|
rows = [
|
||||||
|
[...right.map((value) => value * x), -dot(right, position) * x],
|
||||||
|
[...up.map((value) => value * y), -dot(up, position) * y],
|
||||||
|
[...forward.map((value) => value * z), -dot(forward, position) * z],
|
||||||
|
[0, 0, 0, 1],
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
const focal = 1 / Math.tan(camera[0] * 0.5);
|
||||||
|
const x = focal / camera[1];
|
||||||
|
const z = -camera[3] / (camera[3] - camera[2]);
|
||||||
|
const translation = (-camera[2] * camera[3]) / (camera[3] - camera[2]);
|
||||||
|
rows = [
|
||||||
|
[...right.map((value) => value * x), -dot(right, position) * x],
|
||||||
|
[...up.map((value) => value * focal), -dot(up, position) * focal],
|
||||||
|
[
|
||||||
|
...forward.map((value) => value * z),
|
||||||
|
-dot(forward, position) * z + translation,
|
||||||
|
],
|
||||||
|
[...forward.map((value) => -value), dot(forward, position)],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
this.scene
|
||||||
|
.array("cameraMatrices")
|
||||||
|
.row(this.cameraId)
|
||||||
|
.set([...rows.flat(), ...position, camera[11]]);
|
||||||
|
}
|
||||||
|
|
||||||
override async dispose() {
|
override async dispose() {
|
||||||
await this.ready;
|
await this.ready;
|
||||||
if (this.#cameraDisposed) return;
|
if (this.#cameraDisposed) return;
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export class FreeCamera extends Camera {
|
|||||||
this.position[0] += (x * Math.cos(yaw) + z * Math.sin(yaw)) * speed;
|
this.position[0] += (x * Math.cos(yaw) + z * Math.sin(yaw)) * speed;
|
||||||
this.position[1] += y * speed;
|
this.position[1] += y * speed;
|
||||||
this.position[2] += (x * -Math.sin(yaw) + z * Math.cos(yaw)) * speed;
|
this.position[2] += (x * -Math.sin(yaw) + z * Math.cos(yaw)) * speed;
|
||||||
|
this.refreshMatrix();
|
||||||
this.#frame = requestAnimationFrame(this.#update);
|
this.#frame = requestAnimationFrame(this.#update);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export type TextureOptions = {
|
|||||||
size?: [number | "canvas", number | "canvas", number?];
|
size?: [number | "canvas", number | "canvas", number?];
|
||||||
format?: string;
|
format?: string;
|
||||||
usage?: string[];
|
usage?: string[];
|
||||||
|
mipmaps?: boolean;
|
||||||
transient?: boolean;
|
transient?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,6 +30,10 @@ export class Texture {
|
|||||||
typeof options.source === "string"
|
typeof options.source === "string"
|
||||||
? await createImageBitmap(await (await fetch(options.source)).blob())
|
? await createImageBitmap(await (await fetch(options.source)).blob())
|
||||||
: options.source;
|
: options.source;
|
||||||
|
const mipLevelCount =
|
||||||
|
image instanceof ImageBitmap && options.mipmaps !== false
|
||||||
|
? Math.floor(Math.log2(Math.max(image.width, image.height))) + 1
|
||||||
|
: 1;
|
||||||
const registration = scene.registerTexture({
|
const registration = scene.registerTexture({
|
||||||
id: resource,
|
id: resource,
|
||||||
source: image,
|
source: image,
|
||||||
@@ -38,6 +43,7 @@ export class Texture {
|
|||||||
? [image.width, image.height, 1]
|
? [image.width, image.height, 1]
|
||||||
: [1, 1, 1]),
|
: [1, 1, 1]),
|
||||||
format: options.format ?? "rgba8unorm",
|
format: options.format ?? "rgba8unorm",
|
||||||
|
mipLevelCount,
|
||||||
usage: [
|
usage: [
|
||||||
...new Set([
|
...new Set([
|
||||||
...(options.usage ?? ["copyDst"]),
|
...(options.usage ?? ["copyDst"]),
|
||||||
@@ -49,8 +55,22 @@ export class Texture {
|
|||||||
});
|
});
|
||||||
this.id = registration.number;
|
this.id = registration.number;
|
||||||
await registration.ready;
|
await registration.ready;
|
||||||
if (image instanceof ImageBitmap)
|
if (image instanceof ImageBitmap) {
|
||||||
await scene.core.uploadTexture(resource, image);
|
const levels = [image];
|
||||||
|
for (let level = 1; level < mipLevelCount; level++)
|
||||||
|
levels.push(
|
||||||
|
await createImageBitmap(image, {
|
||||||
|
resizeWidth: Math.max(1, image.width >> level),
|
||||||
|
resizeHeight: Math.max(1, image.height >> level),
|
||||||
|
resizeQuality: "high",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await Promise.all(
|
||||||
|
levels.map((level, index) =>
|
||||||
|
scene.core.uploadTexture(resource, level, index),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -119,11 +119,20 @@ export class YawnCore {
|
|||||||
return this.#request("switch-loadout", { id });
|
return this.#request("switch-loadout", { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadTexture(name, image) {
|
async uploadTexture(name, image, mipLevel = 0) {
|
||||||
await this.ready;
|
await this.ready;
|
||||||
if (typeof name !== "string" || !(image instanceof ImageBitmap))
|
if (
|
||||||
|
typeof name !== "string" ||
|
||||||
|
!(image instanceof ImageBitmap) ||
|
||||||
|
!Number.isInteger(mipLevel) ||
|
||||||
|
mipLevel < 0
|
||||||
|
)
|
||||||
throw new TypeError("TEXTURE_SOURCE");
|
throw new TypeError("TEXTURE_SOURCE");
|
||||||
return this.#request("upload-texture", { name, image }, [image]);
|
return this.#request(
|
||||||
|
"upload-texture",
|
||||||
|
{ name, image, mipLevel },
|
||||||
|
[image],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteTexture(name) {
|
async deleteTexture(name) {
|
||||||
|
|||||||
+7
-2
@@ -174,14 +174,19 @@ impl Core {
|
|||||||
.map_err(|error| JsError::new(&error))
|
.map_err(|error| JsError::new(&error))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upload_texture(&self, name: String, image: web_sys::ImageBitmap) -> Result<(), JsError> {
|
pub fn upload_texture(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
mip_level: u32,
|
||||||
|
image: web_sys::ImageBitmap,
|
||||||
|
) -> Result<(), JsError> {
|
||||||
let gpu = self.gpu.borrow();
|
let gpu = self.gpu.borrow();
|
||||||
let gpu = gpu
|
let gpu = gpu
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| JsError::new("WEBGPU_UNINITIALIZED"))?;
|
.ok_or_else(|| JsError::new("WEBGPU_UNINITIALIZED"))?;
|
||||||
self.store
|
self.store
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.upload_texture(name, image, gpu)
|
.upload_texture(name, mip_level, image, gpu)
|
||||||
.map_err(|error| JsError::new(&error))
|
.map_err(|error| JsError::new(&error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use std::num::NonZeroU64;
|
use std::num::NonZeroU64;
|
||||||
use std::sync::atomic::{AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -46,7 +46,7 @@ pub struct GpuTexture {
|
|||||||
pub texture: wgpu::Texture,
|
pub texture: wgpu::Texture,
|
||||||
pub view: wgpu::TextureView,
|
pub view: wgpu::TextureView,
|
||||||
key: String,
|
key: String,
|
||||||
uploaded: bool,
|
uploaded_mips: HashSet<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum GpuPass {
|
pub enum GpuPass {
|
||||||
@@ -124,7 +124,7 @@ impl GpuResources {
|
|||||||
view: texture.create_view(&wgpu::TextureViewDescriptor::default()),
|
view: texture.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||||
texture,
|
texture,
|
||||||
key,
|
key,
|
||||||
uploaded: false,
|
uploaded_mips: HashSet::new(),
|
||||||
});
|
});
|
||||||
physical_slots.insert(source.slot, physical);
|
physical_slots.insert(source.slot, physical);
|
||||||
physical
|
physical
|
||||||
@@ -211,21 +211,29 @@ impl GpuResources {
|
|||||||
};
|
};
|
||||||
for execution in &graph.executions {
|
for execution in &graph.executions {
|
||||||
let compiled = match execution {
|
let compiled = match execution {
|
||||||
Execution::Render(passes) => GpuPass::Render {
|
Execution::Render(passes) => {
|
||||||
label: if passes.len() == 1 {
|
let (bundle, draws) = resources.render_bundle(graph, passes, gpu)?;
|
||||||
graph.passes[passes[0]].id.clone()
|
GpuPass::Render {
|
||||||
} else if passes
|
label: if passes.len() == 1 {
|
||||||
.iter()
|
graph.passes[passes[0]].id.clone()
|
||||||
.all(|index| graph.passes[*index].id.starts_with("forward-"))
|
} else if passes
|
||||||
{
|
.iter()
|
||||||
format!("Forward ({} draws)", passes.len())
|
.all(|index| graph.passes[*index].id.starts_with("forward-"))
|
||||||
} else {
|
{
|
||||||
format!("Render ({} draws)", passes.len())
|
format!("Forward ({draws} draws)")
|
||||||
},
|
} else if passes
|
||||||
first: passes[0],
|
.iter()
|
||||||
last: *passes.last().unwrap(),
|
.all(|index| graph.passes[*index].id.starts_with("depth-"))
|
||||||
bundle: resources.render_bundle(graph, passes, gpu)?,
|
{
|
||||||
},
|
format!("Depth ({draws} draws)")
|
||||||
|
} else {
|
||||||
|
format!("Render ({draws} draws)")
|
||||||
|
},
|
||||||
|
first: passes[0],
|
||||||
|
last: *passes.last().unwrap(),
|
||||||
|
bundle,
|
||||||
|
}
|
||||||
|
}
|
||||||
Execution::Compute(index) => {
|
Execution::Compute(index) => {
|
||||||
let pass = &graph.passes[*index];
|
let pass = &graph.passes[*index];
|
||||||
let pipeline = resources
|
let pipeline = resources
|
||||||
@@ -261,6 +269,7 @@ impl GpuResources {
|
|||||||
pub fn upload_texture(
|
pub fn upload_texture(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: &str,
|
id: &str,
|
||||||
|
mip_level: u32,
|
||||||
image: &web_sys::ImageBitmap,
|
image: &web_sys::ImageBitmap,
|
||||||
gpu: &Wgpu,
|
gpu: &Wgpu,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -277,7 +286,7 @@ impl GpuResources {
|
|||||||
},
|
},
|
||||||
wgpu::TexelCopyTextureInfo {
|
wgpu::TexelCopyTextureInfo {
|
||||||
texture: &texture.texture,
|
texture: &texture.texture,
|
||||||
mip_level: 0,
|
mip_level,
|
||||||
origin: wgpu::Origin3d::ZERO,
|
origin: wgpu::Origin3d::ZERO,
|
||||||
aspect: wgpu::TextureAspect::All,
|
aspect: wgpu::TextureAspect::All,
|
||||||
}
|
}
|
||||||
@@ -288,15 +297,15 @@ impl GpuResources {
|
|||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
texture.uploaded = true;
|
texture.uploaded_mips.insert(mip_level);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn needs_upload(&self, id: &str) -> bool {
|
pub fn needs_upload(&self, id: &str, mip_level: u32) -> bool {
|
||||||
self.texture_slots
|
self.texture_slots
|
||||||
.get(id)
|
.get(id)
|
||||||
.and_then(|slot| self.textures.get(*slot))
|
.and_then(|slot| self.textures.get(*slot))
|
||||||
.is_some_and(|texture| !texture.uploaded)
|
.is_some_and(|texture| !texture.uploaded_mips.contains(&mip_level))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_timestamps(&self, pass: usize) -> Option<wgpu::RenderPassTimestampWrites<'_>> {
|
pub fn render_timestamps(&self, pass: usize) -> Option<wgpu::RenderPassTimestampWrites<'_>> {
|
||||||
@@ -396,7 +405,7 @@ impl GpuResources {
|
|||||||
graph: &RenderGraph,
|
graph: &RenderGraph,
|
||||||
passes: &[usize],
|
passes: &[usize],
|
||||||
gpu: &Wgpu,
|
gpu: &Wgpu,
|
||||||
) -> Result<wgpu::RenderBundle, String> {
|
) -> Result<(wgpu::RenderBundle, usize), String> {
|
||||||
let pass = &graph.passes[passes[0]];
|
let pass = &graph.passes[passes[0]];
|
||||||
let declaration = graph
|
let declaration = graph
|
||||||
.pipelines
|
.pipelines
|
||||||
@@ -431,8 +440,10 @@ impl GpuResources {
|
|||||||
});
|
});
|
||||||
let mut previous_pipeline = None;
|
let mut previous_pipeline = None;
|
||||||
let mut previous_bindings: Option<&[Binding]> = None;
|
let mut previous_bindings: Option<&[Binding]> = None;
|
||||||
for index in passes {
|
let mut draws = 0;
|
||||||
let pass = &graph.passes[*index];
|
let mut at = 0;
|
||||||
|
while at < passes.len() {
|
||||||
|
let pass = &graph.passes[passes[at]];
|
||||||
let pipeline = self
|
let pipeline = self
|
||||||
.render_pipelines
|
.render_pipelines
|
||||||
.get(&pass.pipeline)
|
.get(&pass.pipeline)
|
||||||
@@ -468,10 +479,17 @@ impl GpuResources {
|
|||||||
buffer.slice(binding.offset..),
|
buffer.slice(binding.offset..),
|
||||||
parse(&binding.format, "GRAPH_INDEX_FORMAT")?,
|
parse(&binding.format, "GRAPH_INDEX_FORMAT")?,
|
||||||
);
|
);
|
||||||
|
let mut instances = pass.draw.instances;
|
||||||
|
while at + 1 < passes.len()
|
||||||
|
&& can_instance(pass, &graph.passes[passes[at + 1]], instances)
|
||||||
|
{
|
||||||
|
at += 1;
|
||||||
|
instances += graph.passes[passes[at]].draw.instances;
|
||||||
|
}
|
||||||
encoder.draw_indexed(
|
encoder.draw_indexed(
|
||||||
pass.draw.first_index..pass.draw.first_index + pass.draw.indices,
|
pass.draw.first_index..pass.draw.first_index + pass.draw.indices,
|
||||||
pass.draw.base_vertex,
|
pass.draw.base_vertex,
|
||||||
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
|
pass.draw.first_instance..pass.draw.first_instance + instances,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
encoder.draw(
|
encoder.draw(
|
||||||
@@ -479,10 +497,15 @@ impl GpuResources {
|
|||||||
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
|
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
draws += 1;
|
||||||
|
at += 1;
|
||||||
}
|
}
|
||||||
Ok(encoder.finish(&wgpu::RenderBundleDescriptor {
|
Ok((
|
||||||
label: Some(&pass.id),
|
encoder.finish(&wgpu::RenderBundleDescriptor {
|
||||||
}))
|
label: Some(&pass.id),
|
||||||
|
}),
|
||||||
|
draws,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bind_groups(
|
fn bind_groups(
|
||||||
@@ -531,6 +554,18 @@ impl GpuResources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn can_instance(first: &Pass, next: &Pass, instances: u32) -> bool {
|
||||||
|
first.pipeline == next.pipeline
|
||||||
|
&& first.bindings == next.bindings
|
||||||
|
&& first.vertex_buffers == next.vertex_buffers
|
||||||
|
&& first.index_buffer == next.index_buffer
|
||||||
|
&& first.draw.indices != 0
|
||||||
|
&& first.draw.indices == next.draw.indices
|
||||||
|
&& first.draw.first_index == next.draw.first_index
|
||||||
|
&& first.draw.base_vertex == next.draw.base_vertex
|
||||||
|
&& first.draw.first_instance + instances == next.draw.first_instance
|
||||||
|
}
|
||||||
|
|
||||||
impl GpuPass {
|
impl GpuPass {
|
||||||
fn label(&self) -> &str {
|
fn label(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
@@ -621,7 +656,7 @@ fn create_render_pipeline(
|
|||||||
primitive: primitive(&source.primitive)?,
|
primitive: primitive(&source.primitive)?,
|
||||||
depth_stencil: depth_stencil(&source.depth_stencil)?,
|
depth_stencil: depth_stencil(&source.depth_stencil)?,
|
||||||
multisample: multisample(&source.multisample)?,
|
multisample: multisample(&source.multisample)?,
|
||||||
fragment: Some(wgpu::FragmentState {
|
fragment: (!targets.is_empty()).then_some(wgpu::FragmentState {
|
||||||
module: &module,
|
module: &module,
|
||||||
entry_point: Some(&source.fragment.entry),
|
entry_point: Some(&source.fragment.entry),
|
||||||
compilation_options: Default::default(),
|
compilation_options: Default::default(),
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ pub struct DepthAttachment {
|
|||||||
pub store: String,
|
pub store: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize, PartialEq, Eq)]
|
||||||
pub struct VertexBinding {
|
pub struct VertexBinding {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub slot: u32,
|
pub slot: u32,
|
||||||
@@ -238,7 +238,7 @@ pub struct VertexBinding {
|
|||||||
pub offset: u64,
|
pub offset: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize, PartialEq, Eq)]
|
||||||
pub struct IndexBinding {
|
pub struct IndexBinding {
|
||||||
pub resource: String,
|
pub resource: String,
|
||||||
#[serde(default = "index_format")]
|
#[serde(default = "index_format")]
|
||||||
@@ -247,7 +247,7 @@ pub struct IndexBinding {
|
|||||||
pub offset: u64,
|
pub offset: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Draw {
|
pub struct Draw {
|
||||||
#[serde(default = "three")]
|
#[serde(default = "three")]
|
||||||
|
|||||||
+21
-11
@@ -13,7 +13,7 @@ pub struct Loadout {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Store {
|
pub struct Store {
|
||||||
graphs: HashMap<String, RenderGraph>,
|
graphs: HashMap<String, RenderGraph>,
|
||||||
texture_sources: HashMap<String, web_sys::ImageBitmap>,
|
texture_sources: HashMap<String, HashMap<u32, web_sys::ImageBitmap>>,
|
||||||
active: Option<Loadout>,
|
active: Option<Loadout>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,9 +32,11 @@ impl Store {
|
|||||||
data,
|
data,
|
||||||
self.active.as_ref().map(|active| &active.resources),
|
self.active.as_ref().map(|active| &active.resources),
|
||||||
)?;
|
)?;
|
||||||
for (name, image) in &self.texture_sources {
|
for (name, levels) in &self.texture_sources {
|
||||||
if resources.needs_upload(name) {
|
for (mip_level, image) in levels {
|
||||||
resources.upload_texture(name, image, gpu)?;
|
if resources.needs_upload(name, *mip_level) {
|
||||||
|
resources.upload_texture(name, *mip_level, image, gpu)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.active = Some(Loadout { graph, resources });
|
self.active = Some(Loadout { graph, resources });
|
||||||
@@ -44,23 +46,29 @@ impl Store {
|
|||||||
pub fn upload_texture(
|
pub fn upload_texture(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: String,
|
name: String,
|
||||||
|
mip_level: u32,
|
||||||
image: web_sys::ImageBitmap,
|
image: web_sys::ImageBitmap,
|
||||||
gpu: &Wgpu,
|
gpu: &Wgpu,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if let Some(active) = &mut self.active {
|
if let Some(active) = &mut self.active {
|
||||||
if active.resources.texture_slots.contains_key(&name) {
|
if active.resources.texture_slots.contains_key(&name) {
|
||||||
active.resources.upload_texture(&name, &image, gpu)?;
|
active
|
||||||
|
.resources
|
||||||
|
.upload_texture(&name, mip_level, &image, gpu)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(previous) = self.texture_sources.insert(name, image) {
|
let levels = self.texture_sources.entry(name).or_default();
|
||||||
|
if let Some(previous) = levels.insert(mip_level, image) {
|
||||||
previous.close();
|
previous.close();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn delete_texture(&mut self, name: &str) {
|
pub fn delete_texture(&mut self, name: &str) {
|
||||||
if let Some(image) = self.texture_sources.remove(name) {
|
if let Some(levels) = self.texture_sources.remove(name) {
|
||||||
image.close();
|
for image in levels.into_values() {
|
||||||
|
image.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,9 +110,11 @@ impl Store {
|
|||||||
data,
|
data,
|
||||||
self.active.as_ref().map(|active| &active.resources),
|
self.active.as_ref().map(|active| &active.resources),
|
||||||
)?;
|
)?;
|
||||||
for (name, image) in &self.texture_sources {
|
for (name, levels) in &self.texture_sources {
|
||||||
if resources.needs_upload(name) {
|
for (mip_level, image) in levels {
|
||||||
resources.upload_texture(name, image, gpu)?;
|
if resources.needs_upload(name, *mip_level) {
|
||||||
|
resources.upload_texture(name, *mip_level, image, gpu)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.active = Some(Loadout { graph, resources });
|
self.active = Some(Loadout { graph, resources });
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ impl RenderLoop {
|
|||||||
while !submission.completed.load(Ordering::Acquire) {
|
while !submission.completed.load(Ordering::Acquire) {
|
||||||
TimeoutFuture::new(0).await;
|
TimeoutFuture::new(0).await;
|
||||||
}
|
}
|
||||||
|
let wall_milliseconds = js_sys::Date::now() - started;
|
||||||
if let Some(profile) = submission.profile {
|
if let Some(profile) = submission.profile {
|
||||||
while profile.state() == 0 {
|
while profile.state() == 0 {
|
||||||
TimeoutFuture::new(0).await;
|
TimeoutFuture::new(0).await;
|
||||||
@@ -129,10 +130,15 @@ impl RenderLoop {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter_map(|pass| pass["milliseconds"].as_f64())
|
.filter_map(|pass| pass["milliseconds"].as_f64())
|
||||||
.sum::<f64>();
|
.sum::<f64>();
|
||||||
|
let gpu = gpu.borrow();
|
||||||
|
let gpu = gpu.as_ref().unwrap();
|
||||||
*control.profile.borrow_mut() = Some(
|
*control.profile.borrow_mut() = Some(
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"frame": frame,
|
"frame": frame,
|
||||||
"milliseconds": milliseconds,
|
"milliseconds": milliseconds,
|
||||||
|
"wallMilliseconds": wall_milliseconds,
|
||||||
|
"adapter": gpu.adapter,
|
||||||
|
"canvas": { "width": gpu.width, "height": gpu.height },
|
||||||
"passes": passes,
|
"passes": passes,
|
||||||
})
|
})
|
||||||
.to_string(),
|
.to_string(),
|
||||||
|
|||||||
+13
-1
@@ -8,6 +8,7 @@ pub struct Wgpu {
|
|||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub timestamp_queries: bool,
|
pub timestamp_queries: bool,
|
||||||
|
pub adapter: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Wgpu {
|
impl Wgpu {
|
||||||
@@ -27,11 +28,21 @@ impl Wgpu {
|
|||||||
let adapter = instance
|
let adapter = instance
|
||||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||||
compatible_surface: Some(&surface),
|
compatible_surface: Some(&surface),
|
||||||
..Default::default()
|
power_preference: wgpu::PowerPreference::HighPerformance,
|
||||||
|
force_fallback_adapter: false,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| "WEBGPU_UNAVAILABLE")?;
|
.map_err(|_| "WEBGPU_UNAVAILABLE")?;
|
||||||
let required_features = adapter.features() & wgpu::Features::TIMESTAMP_QUERY;
|
let required_features = adapter.features() & wgpu::Features::TIMESTAMP_QUERY;
|
||||||
|
let info = adapter.get_info();
|
||||||
|
let adapter_name = if info.name.is_empty() {
|
||||||
|
format!("{:?} · {:?}", info.device_type, info.backend)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{} · {:?} · {:?}",
|
||||||
|
info.name, info.device_type, info.backend
|
||||||
|
)
|
||||||
|
};
|
||||||
let (device, queue) = adapter
|
let (device, queue) = adapter
|
||||||
.request_device(&wgpu::DeviceDescriptor {
|
.request_device(&wgpu::DeviceDescriptor {
|
||||||
required_features,
|
required_features,
|
||||||
@@ -54,6 +65,7 @@ impl Wgpu {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
timestamp_queries: required_features.contains(wgpu::Features::TIMESTAMP_QUERY),
|
timestamp_queries: required_features.contains(wgpu::Features::TIMESTAMP_QUERY),
|
||||||
|
adapter: adapter_name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ addEventListener("message", async ({ data: message }) => {
|
|||||||
core.switch_loadout(message.id);
|
core.switch_loadout(message.id);
|
||||||
break;
|
break;
|
||||||
case "upload-texture":
|
case "upload-texture":
|
||||||
core.upload_texture(message.name, message.image);
|
core.upload_texture(message.name, message.mipLevel, message.image);
|
||||||
break;
|
break;
|
||||||
case "delete-texture":
|
case "delete-texture":
|
||||||
core.delete_texture(message.name);
|
core.delete_texture(message.name);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const fps = ref(0);
|
|||||||
const profilerOpen = ref(false);
|
const profilerOpen = ref(false);
|
||||||
const profilerSupported = ref(null);
|
const profilerSupported = ref(null);
|
||||||
const profile = ref(null);
|
const profile = ref(null);
|
||||||
|
const adapterInfo = ref("");
|
||||||
let generation = 0;
|
let generation = 0;
|
||||||
let current;
|
let current;
|
||||||
let stopProfile;
|
let stopProfile;
|
||||||
@@ -88,6 +89,20 @@ async function attachProfiler(runId = generation) {
|
|||||||
profile.value = null;
|
profile.value = null;
|
||||||
profilerSupported.value = null;
|
profilerSupported.value = null;
|
||||||
if (!profilerOpen.value) return;
|
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;
|
const core = current?.scene?.core ?? current?.core;
|
||||||
if (!core?.onProfile || !core?.setProfiler) {
|
if (!core?.onProfile || !core?.setProfiler) {
|
||||||
profilerSupported.value = false;
|
profilerSupported.value = false;
|
||||||
@@ -273,6 +288,13 @@ onUnmounted(() => {
|
|||||||
<strong>GPU passes</strong>
|
<strong>GPU passes</strong>
|
||||||
<span v-if="profile">{{ profile.milliseconds.toFixed(2) }} ms</span>
|
<span v-if="profile">{{ profile.milliseconds.toFixed(2) }} ms</span>
|
||||||
</div>
|
</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">
|
<p v-if="profilerSupported === false">
|
||||||
Timestamp queries are unavailable on this GPU.
|
Timestamp queries are unavailable on this GPU.
|
||||||
</p>
|
</p>
|
||||||
@@ -522,6 +544,13 @@ canvas {
|
|||||||
color: #60a5fa;
|
color: #60a5fa;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
.profiler-meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
margin: -5px 0 12px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
.profiler p {
|
.profiler p {
|
||||||
color: #94a3b8;
|
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 core = `import { YawnCore } from "@yawn/core";
|
||||||
|
|
||||||
const encode = (value) => {
|
const encode = (value) => {
|
||||||
@@ -403,5 +496,6 @@ export const playgrounds = {
|
|||||||
compute: { title: "Compute pass", code: compute },
|
compute: { title: "Compute pass", code: compute },
|
||||||
post: { title: "HDR post processing", code: post },
|
post: { title: "HDR post processing", code: post },
|
||||||
importing: { title: "glTF import and BVH picking", code: importing },
|
importing: { title: "glTF import and BVH picking", code: importing },
|
||||||
|
benchmark: { title: "Forward benchmark", code: benchmark },
|
||||||
core: { title: "Direct core graph", code: core },
|
core: { title: "Direct core graph", code: core },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Cameras and controls
|
# 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
|
## Shared lens and projection controls
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ follow.stop();
|
|||||||
follow.start();
|
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" />
|
<Playground example="cameras" />
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -56,7 +56,9 @@ await core.setProfiler(false);
|
|||||||
stop();
|
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" />
|
<Playground example="core" />
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ await albedo.ready;
|
|||||||
const textured = new PBRMaterial(scene, { baseColorTexture: albedo });
|
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
|
## Custom WGSL
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user