Optimize render graph and add GPU profiling
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:
@@ -4,6 +4,7 @@ export type GraphBuffer = {
|
|||||||
id: string;
|
id: string;
|
||||||
array: string;
|
array: string;
|
||||||
usage?: string[];
|
usage?: string[];
|
||||||
|
sync?: "frame" | "loadout";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GraphTexture = {
|
export type GraphTexture = {
|
||||||
@@ -18,6 +19,8 @@ export type GraphSampler = {
|
|||||||
id: string;
|
id: string;
|
||||||
magFilter?: "nearest" | "linear";
|
magFilter?: "nearest" | "linear";
|
||||||
minFilter?: "nearest" | "linear";
|
minFilter?: "nearest" | "linear";
|
||||||
|
addressModeU?: "clamp-to-edge" | "repeat" | "mirror-repeat";
|
||||||
|
addressModeV?: "clamp-to-edge" | "repeat" | "mirror-repeat";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GraphBinding = {
|
export type GraphBinding = {
|
||||||
|
|||||||
+298
-51
@@ -91,7 +91,7 @@ fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|||||||
clusters[3] = bitcast<u32>(light.b);
|
clusters[3] = bitcast<u32>(light.b);
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const forwardShader = /* wgsl */ `
|
const basicForwardShader = /* wgsl */ `
|
||||||
struct Accent { color: vec4<f32> }
|
struct Accent { color: vec4<f32> }
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@builtin(position) position: vec4<f32>,
|
@builtin(position) position: vec4<f32>,
|
||||||
@@ -153,6 +153,103 @@ fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
return vec4<f32>(color, base.a);
|
return vec4<f32>(color, base.a);
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
function pbrShader(mask: number) {
|
||||||
|
const baseTexture = mask & 1;
|
||||||
|
const materialTexture = mask & 2;
|
||||||
|
const normalTexture = mask & 4;
|
||||||
|
return /* wgsl */ `
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) position: vec4<f32>,
|
||||||
|
@location(0) world: vec3<f32>,
|
||||||
|
@location(1) normal: vec3<f32>,
|
||||||
|
@location(2) uv: vec2<f32>,
|
||||||
|
@location(3) tangent: vec4<f32>,
|
||||||
|
@location(4) @interpolate(flat) mesh: u32,
|
||||||
|
@location(5) @interpolate(flat) material: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0) var<storage, read> clusters: array<u32>;
|
||||||
|
@group(0) @binding(1) var<uniform> accent: vec4<f32>;
|
||||||
|
@group(0) @binding(2) var<storage, read> positions: array<vec4<f32>>;
|
||||||
|
@group(0) @binding(3) var<storage, read> quaternions: 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(6) var<storage, read> materials: array<vec4<f32>>;
|
||||||
|
@group(0) @binding(7) var<storage, read> cameras: array<vec4<f32>>;
|
||||||
|
${mask ? "@group(1) @binding(0) var materialSampler: sampler;" : ""}
|
||||||
|
${baseTexture ? "@group(1) @binding(1) var baseTexture: texture_2d<f32>;" : ""}
|
||||||
|
${materialTexture ? "@group(1) @binding(2) var materialTexture: texture_2d<f32>;" : ""}
|
||||||
|
${normalTexture ? "@group(1) @binding(3) var normalTexture: texture_2d<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vertex(
|
||||||
|
@location(0) point: vec3<f32>,
|
||||||
|
@location(1) localNormal: vec3<f32>,
|
||||||
|
@location(2) uv: vec2<f32>,
|
||||||
|
${normalTexture ? "@location(3) localTangent: vec4<f32>," : ""}
|
||||||
|
@builtin(instance_index) packed: u32,
|
||||||
|
) -> VertexOutput {
|
||||||
|
let instance = packed & 65535u;
|
||||||
|
let scale = scales[instance].xyz;
|
||||||
|
let world = rotate(quaternions[instance], point * scale) + positions[instance].xyz;
|
||||||
|
var clip = vec4<f32>(world, 1.0);
|
||||||
|
if (cameras[2].w != 0.0) {
|
||||||
|
let cameraNode = u32(cameras[1].x);
|
||||||
|
let inverse = vec4<f32>(-quaternions[cameraNode].xyz, quaternions[cameraNode].w);
|
||||||
|
let view = rotate(inverse, world - positions[cameraNode].xyz);
|
||||||
|
if (cameras[1].y == 1.0) {
|
||||||
|
let size = max(cameras[1].z, 0.0001);
|
||||||
|
clip = vec4<f32>(view.x / (size * cameras[0].y * 0.5), view.y / (size * 0.5), -view.z / cameras[0].w, 1.0);
|
||||||
|
} 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;
|
||||||
|
output.position = select(vec4<f32>(2.0, 2.0, 2.0, 1.0), clip, meshInfo[instance * 4u + 2u] != 0u);
|
||||||
|
output.world = world;
|
||||||
|
output.normal = normalize(rotate(quaternions[instance], localNormal / scale));
|
||||||
|
output.uv = uv;
|
||||||
|
output.tangent = ${normalTexture ? "vec4(normalize(rotate(quaternions[instance], localTangent.xyz * scale)), localTangent.w)" : "vec4(1.0, 0.0, 0.0, 1.0)"};
|
||||||
|
output.mesh = instance;
|
||||||
|
output.material = packed >> 16u;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fragment(input: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let fallback = meshInfo[input.mesh * 4u + 1u];
|
||||||
|
let material = select(fallback, input.material - 1u, input.material != 0u);
|
||||||
|
let factor = materials[material * 3u];
|
||||||
|
let properties = materials[material * 3u + 1u];
|
||||||
|
let extra = materials[material * 3u + 2u];
|
||||||
|
let base = factor * ${baseTexture ? "textureSample(baseTexture, materialSampler, input.uv)" : "vec4(1.0)"};
|
||||||
|
let packedMaterial = ${materialTexture ? "textureSample(materialTexture, materialSampler, input.uv)" : "vec4(1.0)"};
|
||||||
|
let metallic = clamp(properties.x * ${materialTexture ? "packedMaterial.b" : "1.0"}, 0.0, 1.0);
|
||||||
|
let roughness = clamp(properties.y * ${materialTexture ? "packedMaterial.g" : "1.0"}, 0.04, 1.0);
|
||||||
|
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));" : ""}
|
||||||
|
let cameraNode = u32(cameras[1].x);
|
||||||
|
let view = normalize(positions[cameraNode].xyz - input.world);
|
||||||
|
let lightDirection = normalize(vec3<f32>(0.4, 0.7, 0.6));
|
||||||
|
let halfVector = normalize(lightDirection + view);
|
||||||
|
let nDotL = max(dot(normal, lightDirection), 0.0);
|
||||||
|
let nDotH = max(dot(normal, halfVector), 0.0);
|
||||||
|
let f0 = mix(vec3(0.04), base.rgb, metallic);
|
||||||
|
let specular = f0 * pow(nDotH, max(2.0, 2.0 / (roughness * roughness) - 2.0));
|
||||||
|
let clusterLight = vec3<f32>(bitcast<f32>(clusters[1]), bitcast<f32>(clusters[2]), bitcast<f32>(clusters[3]));
|
||||||
|
let ambient = vec3(0.08) + clusterLight;
|
||||||
|
let diffuse = base.rgb * (1.0 - metallic) * nDotL;
|
||||||
|
let emissive = vec3(properties.z, properties.w, extra.x);
|
||||||
|
return vec4((base.rgb * ambient + diffuse + specular * nDotL + emissive) * accent.a, base.a);
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
const emptyForwardShader = /* wgsl */ `
|
const emptyForwardShader = /* wgsl */ `
|
||||||
struct Accent { color: vec4<f32> }
|
struct Accent { color: vec4<f32> }
|
||||||
@group(0) @binding(0) var<uniform> accent: Accent;
|
@group(0) @binding(0) var<uniform> accent: Accent;
|
||||||
@@ -257,10 +354,13 @@ export class Scene {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
options: { arenaBytes?: number; fps?: number; hdr?: boolean } = {},
|
options: { arenaBytes?: number; debug?: boolean; fps?: number; hdr?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
this.hdr = options.hdr ?? true;
|
this.hdr = options.hdr ?? true;
|
||||||
this.core = new YawnCore(canvas, { arenaBytes: options.arenaBytes });
|
this.core = new YawnCore(canvas, {
|
||||||
|
arenaBytes: options.arenaBytes,
|
||||||
|
debug: options.debug,
|
||||||
|
});
|
||||||
this.ready = this.#initialize(options.fps ?? 60);
|
this.ready = this.#initialize(options.fps ?? 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,7 +735,23 @@ export class Scene {
|
|||||||
usage: ["render", "sampled"],
|
usage: ["render", "sampled"],
|
||||||
transient: false,
|
transient: false,
|
||||||
});
|
});
|
||||||
|
addTexture({
|
||||||
|
id: "depth",
|
||||||
|
format: "depth24plus",
|
||||||
|
size: ["canvas", "canvas", 1],
|
||||||
|
usage: ["render"],
|
||||||
|
transient: true,
|
||||||
|
});
|
||||||
addSampler({ id: "linear", magFilter: "linear", minFilter: "linear" });
|
addSampler({ id: "linear", magFilter: "linear", minFilter: "linear" });
|
||||||
|
addSampler({
|
||||||
|
id: "material-linear",
|
||||||
|
magFilter: "linear",
|
||||||
|
minFilter: "linear",
|
||||||
|
addressModeU: "repeat",
|
||||||
|
addressModeV: "repeat",
|
||||||
|
});
|
||||||
|
for (const { number: _, source: __, ...texture } of this.#textures.values())
|
||||||
|
addTexture(texture);
|
||||||
|
|
||||||
let previous = computeIds.length ? computeIds : ["cluster-lights"];
|
let previous = computeIds.length ? computeIds : ["cluster-lights"];
|
||||||
if (!renderedMeshes.length) {
|
if (!renderedMeshes.length) {
|
||||||
@@ -665,22 +781,7 @@ export class Scene {
|
|||||||
["cameras", "cameras"],
|
["cameras", "cameras"],
|
||||||
])
|
])
|
||||||
addBuffer({ id, array, usage: ["storage"] });
|
addBuffer({ id, array, usage: ["storage"] });
|
||||||
renderPipelines.push({
|
const forwardPipelines = new Set<string>();
|
||||||
id: "forward-pbr",
|
|
||||||
code: forwardShader,
|
|
||||||
vertex: {
|
|
||||||
entry: "vertex",
|
|
||||||
buffers: [
|
|
||||||
{
|
|
||||||
arrayStride: 16,
|
|
||||||
attributes: [
|
|
||||||
{ format: "float32x3", offset: 0, shaderLocation: 0 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
fragment: { entry: "fragment", targets: [{ format: hdrFormat }] },
|
|
||||||
});
|
|
||||||
let firstRender = true;
|
let firstRender = true;
|
||||||
for (const mesh of renderedMeshes) {
|
for (const mesh of renderedMeshes) {
|
||||||
const vertex = `geometry-${mesh.geometryId}-positions`;
|
const vertex = `geometry-${mesh.geometryId}-positions`;
|
||||||
@@ -688,13 +789,33 @@ export class Scene {
|
|||||||
id: vertex,
|
id: vertex,
|
||||||
array: `geometry.${mesh.geometryId}.positions`,
|
array: `geometry.${mesh.geometryId}.positions`,
|
||||||
usage: ["vertex"],
|
usage: ["vertex"],
|
||||||
|
sync: "loadout",
|
||||||
});
|
});
|
||||||
|
const normal = `geometry-${mesh.geometryId}-normals`;
|
||||||
|
const uv = `geometry-${mesh.geometryId}-uvs`;
|
||||||
|
const tangent = `geometry-${mesh.geometryId}-tangents`;
|
||||||
|
const hasNormals = !!this.geometryData(mesh.geometryId, "normals");
|
||||||
|
const hasUvs = !!this.geometryData(mesh.geometryId, "uvs");
|
||||||
|
const hasTangents = !!this.geometryData(mesh.geometryId, "tangents");
|
||||||
|
for (const [present, id, kind] of [
|
||||||
|
[hasNormals, normal, "normals"],
|
||||||
|
[hasUvs, uv, "uvs"],
|
||||||
|
[hasTangents, tangent, "tangents"],
|
||||||
|
] as const)
|
||||||
|
if (present)
|
||||||
|
addBuffer({
|
||||||
|
id,
|
||||||
|
array: `geometry.${mesh.geometryId}.${kind}`,
|
||||||
|
usage: ["vertex"],
|
||||||
|
sync: "loadout",
|
||||||
|
});
|
||||||
const indexed = mesh.indexCount > 0;
|
const indexed = mesh.indexCount > 0;
|
||||||
if (indexed)
|
if (indexed)
|
||||||
addBuffer({
|
addBuffer({
|
||||||
id: `geometry-${mesh.geometryId}-indices`,
|
id: `geometry-${mesh.geometryId}-indices`,
|
||||||
array: `geometry.${mesh.geometryId}.indices`,
|
array: `geometry.${mesh.geometryId}.indices`,
|
||||||
usage: ["index"],
|
usage: ["index"],
|
||||||
|
sync: "loadout",
|
||||||
});
|
});
|
||||||
const draws =
|
const draws =
|
||||||
indexed && mesh.faceMaterials.size
|
indexed && mesh.faceMaterials.size
|
||||||
@@ -717,8 +838,103 @@ export class Scene {
|
|||||||
];
|
];
|
||||||
for (const draw of draws) {
|
for (const draw of draws) {
|
||||||
const id = `forward-${mesh.id}-${draw.face}`;
|
const id = `forward-${mesh.id}-${draw.face}`;
|
||||||
if (mesh.id > 65535 || (draw.material ?? 0) > 65534)
|
const material =
|
||||||
|
draw.material ?? Number(this.array("meshInfo").row(mesh.id)[1]);
|
||||||
|
if (mesh.id > 65535 || material > 65534)
|
||||||
throw new RangeError("Scene handle limit");
|
throw new RangeError("Scene handle limit");
|
||||||
|
const pointers = this.array("materialTextures").row(material);
|
||||||
|
const texture = (lane: number) =>
|
||||||
|
pointers[lane]
|
||||||
|
? this.#textures.get(Number(pointers[lane]) - 1)
|
||||||
|
: undefined;
|
||||||
|
const baseTexture = texture(0);
|
||||||
|
const materialTexture = texture(1);
|
||||||
|
const normalTexture = hasTangents ? texture(2) : undefined;
|
||||||
|
const detailed = hasNormals && hasUvs;
|
||||||
|
const mask = detailed
|
||||||
|
? (baseTexture ? 1 : 0) |
|
||||||
|
(materialTexture ? 2 : 0) |
|
||||||
|
(normalTexture ? 4 : 0)
|
||||||
|
: 0;
|
||||||
|
const pipeline = detailed ? `forward-pbr-${mask}` : "forward-basic";
|
||||||
|
if (!forwardPipelines.has(pipeline)) {
|
||||||
|
forwardPipelines.add(pipeline);
|
||||||
|
renderPipelines.push({
|
||||||
|
id: pipeline,
|
||||||
|
code: detailed ? pbrShader(mask) : basicForwardShader,
|
||||||
|
vertex: {
|
||||||
|
entry: "vertex",
|
||||||
|
buffers: detailed
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
arrayStride: 16,
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
format: "float32x3",
|
||||||
|
offset: 0,
|
||||||
|
shaderLocation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arrayStride: 16,
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
format: "float32x3",
|
||||||
|
offset: 0,
|
||||||
|
shaderLocation: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arrayStride: 16,
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
format: "float32x2",
|
||||||
|
offset: 0,
|
||||||
|
shaderLocation: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...(normalTexture
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
arrayStride: 16,
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
format: "float32x4",
|
||||||
|
offset: 0,
|
||||||
|
shaderLocation: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
arrayStride: 16,
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
format: "float32x3",
|
||||||
|
offset: 0,
|
||||||
|
shaderLocation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fragment: {
|
||||||
|
entry: "fragment",
|
||||||
|
targets: [{ format: hdrFormat }],
|
||||||
|
},
|
||||||
|
depthStencil: {
|
||||||
|
format: "depth24plus",
|
||||||
|
depth_write_enabled: true,
|
||||||
|
depth_compare: "less",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
const instance =
|
const instance =
|
||||||
(mesh.id +
|
(mesh.id +
|
||||||
(draw.material === undefined
|
(draw.material === undefined
|
||||||
@@ -728,18 +944,56 @@ export class Scene {
|
|||||||
passes.push({
|
passes.push({
|
||||||
id,
|
id,
|
||||||
type: "render",
|
type: "render",
|
||||||
pipeline: "forward-pbr",
|
pipeline,
|
||||||
after: previous,
|
after: previous,
|
||||||
bindings: [
|
bindings: [
|
||||||
"clusters",
|
...[
|
||||||
"accent",
|
"clusters",
|
||||||
"node-positions",
|
"accent",
|
||||||
"node-quaternions",
|
"node-positions",
|
||||||
"node-scales",
|
"node-quaternions",
|
||||||
"mesh-info",
|
"node-scales",
|
||||||
"materials",
|
"mesh-info",
|
||||||
"cameras",
|
"materials",
|
||||||
].map((resource, binding) => ({ group: 0, binding, resource })),
|
"cameras",
|
||||||
|
].map((resource, binding) => ({
|
||||||
|
group: 0,
|
||||||
|
binding,
|
||||||
|
resource,
|
||||||
|
})),
|
||||||
|
...(detailed && mask
|
||||||
|
? [
|
||||||
|
{ group: 1, binding: 0, resource: "material-linear" },
|
||||||
|
...(baseTexture
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
group: 1,
|
||||||
|
binding: 1,
|
||||||
|
resource: baseTexture.id,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(materialTexture
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
group: 1,
|
||||||
|
binding: 2,
|
||||||
|
resource: materialTexture.id,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(normalTexture
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
group: 1,
|
||||||
|
binding: 3,
|
||||||
|
resource: normalTexture.id,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
color: [
|
color: [
|
||||||
{
|
{
|
||||||
resource: "hdr",
|
resource: "hdr",
|
||||||
@@ -748,7 +1002,20 @@ export class Scene {
|
|||||||
: { load: "load" }),
|
: { load: "load" }),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vertexBuffers: [{ slot: 0, resource: vertex }],
|
depth: {
|
||||||
|
resource: "depth",
|
||||||
|
...(firstRender ? { clear: 1 } : { load: "load" }),
|
||||||
|
},
|
||||||
|
vertexBuffers: [
|
||||||
|
{ slot: 0, resource: vertex },
|
||||||
|
...(detailed
|
||||||
|
? [
|
||||||
|
{ slot: 1, resource: normal },
|
||||||
|
{ slot: 2, resource: uv },
|
||||||
|
...(normalTexture ? [{ slot: 3, resource: tangent }] : []),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
...(indexed
|
...(indexed
|
||||||
? {
|
? {
|
||||||
indexBuffer: {
|
indexBuffer: {
|
||||||
@@ -827,26 +1094,6 @@ export class Scene {
|
|||||||
previous = [pipeline];
|
previous = [pipeline];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { number, source: _, ...texture } of this.#textures.values()) {
|
|
||||||
addTexture(texture);
|
|
||||||
const id = `retain-texture-${number}`;
|
|
||||||
computePipelines.push({
|
|
||||||
id,
|
|
||||||
code: "@group(0) @binding(0) var source: texture_2d<f32>; @group(0) @binding(1) var<storage, read_write> output: array<u32>; @compute @workgroup_size(1) fn main() { output[1] = textureDimensions(source).x; }",
|
|
||||||
entry: "main",
|
|
||||||
});
|
|
||||||
passes.push({
|
|
||||||
id,
|
|
||||||
type: "compute",
|
|
||||||
pipeline: id,
|
|
||||||
after: ["cluster-lights"],
|
|
||||||
dispatch: [1, 1, 1],
|
|
||||||
bindings: [
|
|
||||||
{ group: 0, binding: 0, resource: texture.id },
|
|
||||||
{ group: 0, binding: 1, resource: "clusters" },
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const toneMap = String(
|
const toneMap = String(
|
||||||
[...this.#effects.values()].find(
|
[...this.#effects.values()].find(
|
||||||
(effect) => effect.kind === "colorGrading",
|
(effect) => effect.kind === "colorGrading",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Mesh } from "../Mesh";
|
import { Mesh } from "../Mesh";
|
||||||
import type { Scene } from "../Scene";
|
import type { Scene } from "../Scene";
|
||||||
import { PBRMaterial } from "../materials/PBRMaterial";
|
import { PBRMaterial } from "../materials/PBRMaterial";
|
||||||
|
import { Texture } from "../materials/Texture";
|
||||||
|
|
||||||
let worker: Worker | undefined;
|
let worker: Worker | undefined;
|
||||||
let nextRequest = 1;
|
let nextRequest = 1;
|
||||||
@@ -42,9 +43,31 @@ export async function importGltf(scene: Scene, url: string | URL) {
|
|||||||
nodes: result.primitives.length,
|
nodes: result.primitives.length,
|
||||||
materials: result.materials.length,
|
materials: result.materials.length,
|
||||||
});
|
});
|
||||||
return scene.batchGraphUpdates(async () => {
|
let textures: Texture[] = [];
|
||||||
|
const imported = await scene.batchGraphUpdates(async () => {
|
||||||
|
const srgb = new Set<number>();
|
||||||
|
for (const material of result.materials) {
|
||||||
|
if (material.baseColorTexture >= 0) srgb.add(material.baseColorTexture);
|
||||||
|
if (material.emissiveTexture >= 0) srgb.add(material.emissiveTexture);
|
||||||
|
}
|
||||||
|
textures = result.textures.map(
|
||||||
|
({ image }: { image: ImageBitmap }, index: number) =>
|
||||||
|
new Texture(scene, {
|
||||||
|
source: image,
|
||||||
|
format: srgb.has(index) ? "rgba8unorm-srgb" : "rgba8unorm",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await Promise.all(textures.map((texture: Texture) => texture.ready));
|
||||||
const materials = result.materials.map(
|
const materials = result.materials.map(
|
||||||
(options: any) => new PBRMaterial(scene, options),
|
(options: any) =>
|
||||||
|
new PBRMaterial(scene, {
|
||||||
|
...options,
|
||||||
|
baseColorTexture: textures[options.baseColorTexture],
|
||||||
|
metallicRoughnessTexture:
|
||||||
|
textures[options.metallicRoughnessTexture],
|
||||||
|
normalTexture: textures[options.normalTexture],
|
||||||
|
emissiveTexture: textures[options.emissiveTexture],
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
await Promise.all(materials.map((material: PBRMaterial) => material.ready));
|
await Promise.all(materials.map((material: PBRMaterial) => material.ready));
|
||||||
const meshes: Mesh[] = result.primitives.map(
|
const meshes: Mesh[] = result.primitives.map(
|
||||||
@@ -67,4 +90,10 @@ export async function importGltf(scene: Scene, url: string | URL) {
|
|||||||
await Promise.all(meshes.map((mesh) => mesh.ready));
|
await Promise.all(meshes.map((mesh) => mesh.ready));
|
||||||
return meshes;
|
return meshes;
|
||||||
});
|
});
|
||||||
|
await Promise.all(
|
||||||
|
result.textures.map((_: unknown, index: number) =>
|
||||||
|
scene.core.deleteTexture(textures[index].resource),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return imported;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,26 @@ async function load(url: string) {
|
|||||||
return new Uint8Array(await result.arrayBuffer());
|
return new Uint8Array(await result.arrayBuffer());
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
const imageBlobs = await Promise.all(
|
||||||
|
(document.images ?? []).map(async (image: any) => {
|
||||||
|
if (image.bufferView !== undefined) {
|
||||||
|
const view = document.bufferViews[image.bufferView];
|
||||||
|
const bytes = buffers[view.buffer];
|
||||||
|
const start = view.byteOffset ?? 0;
|
||||||
|
return new Blob([bytes.slice(start, start + view.byteLength)], {
|
||||||
|
type: image.mimeType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const result = await fetch(new URL(image.uri, url));
|
||||||
|
if (!result.ok) throw new Error(`HTTP_${result.status}`);
|
||||||
|
return result.blob();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const textures = await Promise.all(
|
||||||
|
(document.textures ?? []).map(async (texture: any) => ({
|
||||||
|
image: await createImageBitmap(imageBlobs[texture.source]),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
const accessor = (id: number, integer = false) => {
|
const accessor = (id: number, integer = false) => {
|
||||||
const source = document.accessors[id];
|
const source = document.accessors[id];
|
||||||
@@ -199,6 +219,10 @@ async function load(url: string) {
|
|||||||
roughness: pbr.roughnessFactor ?? 0.7,
|
roughness: pbr.roughnessFactor ?? 0.7,
|
||||||
emissive: material.emissiveFactor ?? [0, 0, 0],
|
emissive: material.emissiveFactor ?? [0, 0, 0],
|
||||||
alphaCutoff: material.alphaCutoff ?? 0.5,
|
alphaCutoff: material.alphaCutoff ?? 0.5,
|
||||||
|
baseColorTexture: pbr.baseColorTexture?.index ?? -1,
|
||||||
|
metallicRoughnessTexture: pbr.metallicRoughnessTexture?.index ?? -1,
|
||||||
|
normalTexture: material.normalTexture?.index ?? -1,
|
||||||
|
emissiveTexture: material.emissiveTexture?.index ?? -1,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const primitives: any[] = [];
|
const primitives: any[] = [];
|
||||||
@@ -264,7 +288,7 @@ async function load(url: string) {
|
|||||||
if (!nodes.length)
|
if (!nodes.length)
|
||||||
for (let id = 0; id < (document.meshes ?? []).length; id++)
|
for (let id = 0; id < (document.meshes ?? []).length; id++)
|
||||||
emitMesh(id, {});
|
emitMesh(id, {});
|
||||||
return { materials, primitives };
|
return { materials, primitives, textures };
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener("message", async ({ data }) => {
|
addEventListener("message", async ({ data }) => {
|
||||||
@@ -275,6 +299,7 @@ addEventListener("message", async ({ data }) => {
|
|||||||
.map((name) => primitive[name]?.buffer)
|
.map((name) => primitive[name]?.buffer)
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
);
|
);
|
||||||
|
transfers.push(...result.textures.map((texture: any) => texture.image));
|
||||||
(postMessage as any)({ request: data.request, result }, transfers);
|
(postMessage as any)({ request: data.request, result }, transfers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
postMessage({
|
postMessage({
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ export class PBRMaterial {
|
|||||||
|
|
||||||
constructor(scene: Scene, options: PBRMaterialOptions = {}) {
|
constructor(scene: Scene, options: PBRMaterialOptions = {}) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
this.ready = scene.allocateMaterial().then((id) => {
|
const textures = [
|
||||||
|
options.baseColorTexture,
|
||||||
|
options.metallicRoughnessTexture,
|
||||||
|
options.normalTexture,
|
||||||
|
options.emissiveTexture,
|
||||||
|
].filter((texture): texture is Texture => texture !== undefined);
|
||||||
|
this.ready = Promise.all(textures.map((texture) => texture.ready)).then(async () => {
|
||||||
|
const id = await scene.allocateMaterial();
|
||||||
this.id = id;
|
this.id = id;
|
||||||
const color = Array.from(options.baseColor ?? [1, 1, 1, 1]);
|
const color = Array.from(options.baseColor ?? [1, 1, 1, 1]);
|
||||||
const emissive = Array.from(options.emissive ?? [0, 0, 0]);
|
const emissive = Array.from(options.emissive ?? [0, 0, 0]);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ let nextTextureName = 1;
|
|||||||
/** A graph texture resource handle; image decoding/upload policy remains outside core. */
|
/** A graph texture resource handle; image decoding/upload policy remains outside core. */
|
||||||
export class Texture {
|
export class Texture {
|
||||||
readonly scene: Scene;
|
readonly scene: Scene;
|
||||||
readonly id: number;
|
id = -1;
|
||||||
readonly resource: string;
|
readonly resource: string;
|
||||||
readonly source?: string | ImageBitmap;
|
readonly source?: string | ImageBitmap;
|
||||||
readonly ready: Promise<void>;
|
readonly ready: Promise<void>;
|
||||||
@@ -21,24 +21,44 @@ export class Texture {
|
|||||||
|
|
||||||
constructor(scene: Scene, options: TextureOptions = {}) {
|
constructor(scene: Scene, options: TextureOptions = {}) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
this.resource = `texture-${nextTextureName++}`;
|
const resource = `texture-${nextTextureName++}`;
|
||||||
|
this.resource = resource;
|
||||||
this.source = options.source;
|
this.source = options.source;
|
||||||
const registration = scene.registerTexture({
|
this.ready = (async () => {
|
||||||
id: this.resource,
|
const image =
|
||||||
source: options.source,
|
typeof options.source === "string"
|
||||||
size: options.size ?? [1, 1, 1],
|
? await createImageBitmap(await (await fetch(options.source)).blob())
|
||||||
format: options.format ?? "rgba8unorm",
|
: options.source;
|
||||||
usage: [...new Set([...(options.usage ?? ["copyDst"]), "sampled"])],
|
const registration = scene.registerTexture({
|
||||||
transient: options.transient ?? false,
|
id: resource,
|
||||||
});
|
source: image,
|
||||||
this.id = registration.number;
|
size:
|
||||||
this.ready = registration.ready;
|
options.size ??
|
||||||
|
(image instanceof ImageBitmap
|
||||||
|
? [image.width, image.height, 1]
|
||||||
|
: [1, 1, 1]),
|
||||||
|
format: options.format ?? "rgba8unorm",
|
||||||
|
usage: [
|
||||||
|
...new Set([
|
||||||
|
...(options.usage ?? ["copyDst"]),
|
||||||
|
"sampled",
|
||||||
|
...(image instanceof ImageBitmap ? ["render"] : []),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
transient: options.transient ?? false,
|
||||||
|
});
|
||||||
|
this.id = registration.number;
|
||||||
|
await registration.ready;
|
||||||
|
if (image instanceof ImageBitmap)
|
||||||
|
await scene.core.uploadTexture(resource, image);
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
async dispose() {
|
async dispose() {
|
||||||
await this.ready;
|
await this.ready;
|
||||||
if (this.#disposed) return;
|
if (this.#disposed) return;
|
||||||
this.#disposed = true;
|
this.#disposed = true;
|
||||||
|
await this.scene.core.deleteTexture(this.resource);
|
||||||
await this.scene.unregisterTexture(this.id);
|
await this.scene.unregisterTexture(this.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -15,5 +15,5 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
web-sys = { version = "0.3", features = ["OffscreenCanvas"] }
|
web-sys = { version = "0.3", features = ["ImageBitmap", "OffscreenCanvas"] }
|
||||||
wgpu = { version = "26", default-features = false, features = ["serde", "webgpu", "wgsl"] }
|
wgpu = { version = "26", default-features = false, features = ["serde", "webgpu", "wgsl"] }
|
||||||
|
|||||||
+32
-2
@@ -46,9 +46,10 @@ export class YawnCore {
|
|||||||
#buffer;
|
#buffer;
|
||||||
#arrays = new Map();
|
#arrays = new Map();
|
||||||
#pending = new Map();
|
#pending = new Map();
|
||||||
|
#profileListeners = new Set();
|
||||||
#next = 1;
|
#next = 1;
|
||||||
|
|
||||||
constructor(canvas, { arenaBytes = 64 * 1024 * 1024, workerFactory } = {}) {
|
constructor(canvas, { arenaBytes = 64 * 1024 * 1024, debug = false, workerFactory } = {}) {
|
||||||
if (!canvas) throw new TypeError("canvas is required");
|
if (!canvas) throw new TypeError("canvas is required");
|
||||||
this.#worker = workerFactory?.() ?? new Worker(new URL("./worker.js", import.meta.url), {
|
this.#worker = workerFactory?.() ?? new Worker(new URL("./worker.js", import.meta.url), {
|
||||||
type: "module",
|
type: "module",
|
||||||
@@ -59,12 +60,13 @@ export class YawnCore {
|
|||||||
this.#worker.addEventListener("messageerror", () => this.#fail("WORKER_ERROR"));
|
this.#worker.addEventListener("messageerror", () => this.#fail("WORKER_ERROR"));
|
||||||
this.#worker.start?.();
|
this.#worker.start?.();
|
||||||
const offscreen = canvas.transferControlToOffscreen?.() ?? canvas;
|
const offscreen = canvas.transferControlToOffscreen?.() ?? canvas;
|
||||||
this.ready = this.#request("init", { canvas: offscreen, arenaBytes }, [offscreen]).then(result => {
|
this.ready = this.#request("init", { canvas: offscreen, arenaBytes }, [offscreen]).then(async result => {
|
||||||
this.#buffer = result.buffer;
|
this.#buffer = result.buffer;
|
||||||
for (const descriptor of result.rows) this.#arrays.set(
|
for (const descriptor of result.rows) this.#arrays.set(
|
||||||
descriptor.name,
|
descriptor.name,
|
||||||
new SharedRows(this.#buffer, descriptor),
|
new SharedRows(this.#buffer, descriptor),
|
||||||
);
|
);
|
||||||
|
if (debug) await this.#request("set-profiler", { enabled: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +119,18 @@ export class YawnCore {
|
|||||||
return this.#request("switch-loadout", { id });
|
return this.#request("switch-loadout", { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadTexture(name, image) {
|
||||||
|
await this.ready;
|
||||||
|
if (typeof name !== "string" || !(image instanceof ImageBitmap))
|
||||||
|
throw new TypeError("TEXTURE_SOURCE");
|
||||||
|
return this.#request("upload-texture", { name, image }, [image]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTexture(name) {
|
||||||
|
await this.ready;
|
||||||
|
return this.#request("delete-texture", { name });
|
||||||
|
}
|
||||||
|
|
||||||
async play() {
|
async play() {
|
||||||
await this.ready;
|
await this.ready;
|
||||||
return this.#request("play");
|
return this.#request("play");
|
||||||
@@ -132,6 +146,17 @@ export class YawnCore {
|
|||||||
return this.#request("set-fps", { fps });
|
return this.#request("set-fps", { fps });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setProfiler(enabled) {
|
||||||
|
await this.ready;
|
||||||
|
return this.#request("set-profiler", { enabled: Boolean(enabled) });
|
||||||
|
}
|
||||||
|
|
||||||
|
onProfile(listener) {
|
||||||
|
if (typeof listener !== "function") throw new TypeError("PROFILE_LISTENER");
|
||||||
|
this.#profileListeners.add(listener);
|
||||||
|
return () => this.#profileListeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
array(name) {
|
array(name) {
|
||||||
const array = this.#arrays.get(name);
|
const array = this.#arrays.get(name);
|
||||||
if (!array) throw new Error(`UNKNOWN_ARRAY: ${name}`);
|
if (!array) throw new Error(`UNKNOWN_ARRAY: ${name}`);
|
||||||
@@ -147,6 +172,10 @@ export class YawnCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#message(message) {
|
#message(message) {
|
||||||
|
if (message?.type === "profile") {
|
||||||
|
for (const listener of this.#profileListeners) listener(message.stats);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const pending = this.#pending.get(message?.request);
|
const pending = this.#pending.get(message?.request);
|
||||||
if (!pending) return;
|
if (!pending) return;
|
||||||
this.#pending.delete(message.request);
|
this.#pending.delete(message.request);
|
||||||
@@ -161,6 +190,7 @@ export class YawnCore {
|
|||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
this.#fail("DISPOSED");
|
this.#fail("DISPOSED");
|
||||||
|
this.#profileListeners.clear();
|
||||||
this.#worker.terminate();
|
this.#worker.terminate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
@@ -174,6 +174,21 @@ 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> {
|
||||||
|
let gpu = self.gpu.borrow();
|
||||||
|
let gpu = gpu
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| JsError::new("WEBGPU_UNINITIALIZED"))?;
|
||||||
|
self.store
|
||||||
|
.borrow_mut()
|
||||||
|
.upload_texture(name, image, gpu)
|
||||||
|
.map_err(|error| JsError::new(&error))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_texture(&self, name: &str) {
|
||||||
|
self.store.borrow_mut().delete_texture(name);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn play(&self) {
|
pub fn play(&self) {
|
||||||
self.render.play();
|
self.render.play();
|
||||||
}
|
}
|
||||||
@@ -185,4 +200,18 @@ impl Core {
|
|||||||
pub fn set_fps(&self, fps: u32) -> Result<(), JsError> {
|
pub fn set_fps(&self, fps: u32) -> Result<(), JsError> {
|
||||||
self.render.set_fps(fps).map_err(JsError::new)
|
self.render.set_fps(fps).map_err(JsError::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_profiler(&self, enabled: bool) -> bool {
|
||||||
|
let supported = self
|
||||||
|
.gpu
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|gpu| gpu.timestamp_queries);
|
||||||
|
self.render.set_profiling(enabled && supported);
|
||||||
|
supported
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_profile(&self) -> Option<String> {
|
||||||
|
self.render.take_profile()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::num::NonZeroU64;
|
use std::num::NonZeroU64;
|
||||||
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::gpu::Wgpu;
|
use crate::gpu::Wgpu;
|
||||||
use crate::graph::{Binding, Extent, Pass, RenderGraph, RenderPipeline, Texture};
|
use crate::graph::{Binding, Execution, Extent, Pass, RenderGraph, RenderPipeline, Texture};
|
||||||
use crate::render_data::RenderData;
|
use crate::render_data::RenderData;
|
||||||
|
|
||||||
pub struct GpuResources {
|
pub struct GpuResources {
|
||||||
@@ -16,28 +18,59 @@ pub struct GpuResources {
|
|||||||
pub render_pipelines: HashMap<String, wgpu::RenderPipeline>,
|
pub render_pipelines: HashMap<String, wgpu::RenderPipeline>,
|
||||||
pub compute_pipelines: HashMap<String, wgpu::ComputePipeline>,
|
pub compute_pipelines: HashMap<String, wgpu::ComputePipeline>,
|
||||||
pub passes: Vec<GpuPass>,
|
pub passes: Vec<GpuPass>,
|
||||||
|
pub profiler: Option<GpuProfiler>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GpuProfiler {
|
||||||
|
query_set: wgpu::QuerySet,
|
||||||
|
resolve: wgpu::Buffer,
|
||||||
|
readback: Arc<wgpu::Buffer>,
|
||||||
|
query_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProfileMap {
|
||||||
|
readback: Arc<wgpu::Buffer>,
|
||||||
|
state: Arc<AtomicU8>,
|
||||||
|
labels: Vec<String>,
|
||||||
|
timestamp_period: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GpuBuffer {
|
pub struct GpuBuffer {
|
||||||
pub buffer: wgpu::Buffer,
|
pub buffer: wgpu::Buffer,
|
||||||
pub source: String,
|
pub source: String,
|
||||||
|
pub sync_each_frame: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct GpuTexture {
|
pub struct GpuTexture {
|
||||||
pub _texture: wgpu::Texture,
|
pub texture: wgpu::Texture,
|
||||||
pub view: wgpu::TextureView,
|
pub view: wgpu::TextureView,
|
||||||
|
key: String,
|
||||||
|
uploaded: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum GpuPass {
|
pub enum GpuPass {
|
||||||
Render(wgpu::RenderBundle),
|
Render {
|
||||||
|
label: String,
|
||||||
|
first: usize,
|
||||||
|
last: usize,
|
||||||
|
bundle: wgpu::RenderBundle,
|
||||||
|
},
|
||||||
Compute {
|
Compute {
|
||||||
|
label: String,
|
||||||
|
pass: usize,
|
||||||
pipeline: wgpu::ComputePipeline,
|
pipeline: wgpu::ComputePipeline,
|
||||||
bind_groups: Vec<(u32, wgpu::BindGroup)>,
|
bind_groups: Vec<(u32, wgpu::BindGroup)>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GpuResources {
|
impl GpuResources {
|
||||||
pub fn activate(graph: &RenderGraph, gpu: &Wgpu, data: &RenderData) -> Result<Self, String> {
|
pub fn activate(
|
||||||
|
graph: &RenderGraph,
|
||||||
|
gpu: &Wgpu,
|
||||||
|
data: &RenderData,
|
||||||
|
previous: Option<&Self>,
|
||||||
|
) -> Result<Self, String> {
|
||||||
let mut buffers = HashMap::new();
|
let mut buffers = HashMap::new();
|
||||||
for source in &graph.resources.buffers {
|
for source in &graph.resources.buffers {
|
||||||
let rows = data.rows(&source.array).ok_or("GRAPH_ARRAY_UNKNOWN")?;
|
let rows = data.rows(&source.array).ok_or("GRAPH_ARRAY_UNKNOWN")?;
|
||||||
@@ -54,6 +87,7 @@ impl GpuResources {
|
|||||||
GpuBuffer {
|
GpuBuffer {
|
||||||
buffer,
|
buffer,
|
||||||
source: source.array.clone(),
|
source: source.array.clone(),
|
||||||
|
sync_each_frame: source.sync == "frame",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -65,12 +99,32 @@ impl GpuResources {
|
|||||||
let physical = match physical_slots.get(&source.slot) {
|
let physical = match physical_slots.get(&source.slot) {
|
||||||
Some(&physical) => physical,
|
Some(&physical) => physical,
|
||||||
None => {
|
None => {
|
||||||
|
let key = source.key()?;
|
||||||
|
if !source.transient {
|
||||||
|
if let Some(texture) = previous
|
||||||
|
.and_then(|resources| {
|
||||||
|
resources
|
||||||
|
.texture_slots
|
||||||
|
.get(&source.id)
|
||||||
|
.map(|slot| &resources.textures[*slot])
|
||||||
|
})
|
||||||
|
.filter(|texture| texture.key == key)
|
||||||
|
{
|
||||||
|
let physical = textures.len();
|
||||||
|
textures.push(texture.clone());
|
||||||
|
physical_slots.insert(source.slot, physical);
|
||||||
|
texture_slots.insert(source.id.clone(), physical);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
let descriptor = texture_descriptor(source, gpu.width, gpu.height)?;
|
let descriptor = texture_descriptor(source, gpu.width, gpu.height)?;
|
||||||
let texture = gpu.device.create_texture(&descriptor);
|
let texture = gpu.device.create_texture(&descriptor);
|
||||||
let physical = textures.len();
|
let physical = textures.len();
|
||||||
textures.push(GpuTexture {
|
textures.push(GpuTexture {
|
||||||
view: texture.create_view(&wgpu::TextureViewDescriptor::default()),
|
view: texture.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||||
_texture: texture,
|
texture,
|
||||||
|
key,
|
||||||
|
uploaded: false,
|
||||||
});
|
});
|
||||||
physical_slots.insert(source.slot, physical);
|
physical_slots.insert(source.slot, physical);
|
||||||
physical
|
physical
|
||||||
@@ -121,6 +175,30 @@ impl GpuResources {
|
|||||||
})
|
})
|
||||||
.collect::<Result<HashMap<_, _>, _>>()?;
|
.collect::<Result<HashMap<_, _>, _>>()?;
|
||||||
|
|
||||||
|
let profiler = (gpu.timestamp_queries && !graph.executions.is_empty()).then(|| {
|
||||||
|
let query_count = graph.executions.len() as u32 * 2;
|
||||||
|
let bytes = u64::from(query_count) * 8;
|
||||||
|
GpuProfiler {
|
||||||
|
query_set: gpu.device.create_query_set(&wgpu::QuerySetDescriptor {
|
||||||
|
label: Some("frame-profile"),
|
||||||
|
ty: wgpu::QueryType::Timestamp,
|
||||||
|
count: query_count,
|
||||||
|
}),
|
||||||
|
resolve: gpu.device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("frame-profile-resolve"),
|
||||||
|
size: bytes,
|
||||||
|
usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
}),
|
||||||
|
readback: Arc::new(gpu.device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("frame-profile-readback"),
|
||||||
|
size: bytes,
|
||||||
|
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
})),
|
||||||
|
query_count,
|
||||||
|
}
|
||||||
|
});
|
||||||
let mut resources = Self {
|
let mut resources = Self {
|
||||||
buffers,
|
buffers,
|
||||||
textures,
|
textures,
|
||||||
@@ -129,11 +207,27 @@ impl GpuResources {
|
|||||||
render_pipelines,
|
render_pipelines,
|
||||||
compute_pipelines,
|
compute_pipelines,
|
||||||
passes: Vec::new(),
|
passes: Vec::new(),
|
||||||
|
profiler,
|
||||||
};
|
};
|
||||||
for pass in &graph.passes {
|
for execution in &graph.executions {
|
||||||
let compiled = match pass.kind.as_str() {
|
let compiled = match execution {
|
||||||
"render" => GpuPass::Render(resources.render_bundle(graph, pass, gpu)?),
|
Execution::Render(passes) => GpuPass::Render {
|
||||||
"compute" => {
|
label: if passes.len() == 1 {
|
||||||
|
graph.passes[passes[0]].id.clone()
|
||||||
|
} else if passes
|
||||||
|
.iter()
|
||||||
|
.all(|index| graph.passes[*index].id.starts_with("forward-"))
|
||||||
|
{
|
||||||
|
format!("Forward ({} draws)", passes.len())
|
||||||
|
} else {
|
||||||
|
format!("Render ({} draws)", passes.len())
|
||||||
|
},
|
||||||
|
first: passes[0],
|
||||||
|
last: *passes.last().unwrap(),
|
||||||
|
bundle: resources.render_bundle(graph, passes, gpu)?,
|
||||||
|
},
|
||||||
|
Execution::Compute(index) => {
|
||||||
|
let pass = &graph.passes[*index];
|
||||||
let pipeline = resources
|
let pipeline = resources
|
||||||
.compute_pipelines
|
.compute_pipelines
|
||||||
.get(&pass.pipeline)
|
.get(&pass.pipeline)
|
||||||
@@ -145,11 +239,12 @@ impl GpuResources {
|
|||||||
gpu,
|
gpu,
|
||||||
)?;
|
)?;
|
||||||
GpuPass::Compute {
|
GpuPass::Compute {
|
||||||
|
label: pass.id.clone(),
|
||||||
|
pass: *index,
|
||||||
pipeline,
|
pipeline,
|
||||||
bind_groups,
|
bind_groups,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => return Err("GRAPH_PASS".into()),
|
|
||||||
};
|
};
|
||||||
resources.passes.push(compiled);
|
resources.passes.push(compiled);
|
||||||
}
|
}
|
||||||
@@ -163,16 +258,146 @@ impl GpuResources {
|
|||||||
.map(|texture| &texture.view)
|
.map(|texture| &texture.view)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn upload_texture(
|
||||||
|
&mut self,
|
||||||
|
id: &str,
|
||||||
|
image: &web_sys::ImageBitmap,
|
||||||
|
gpu: &Wgpu,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let texture = self
|
||||||
|
.texture_slots
|
||||||
|
.get(id)
|
||||||
|
.and_then(|slot| self.textures.get_mut(*slot))
|
||||||
|
.ok_or("GRAPH_TEXTURE_UNKNOWN")?;
|
||||||
|
gpu.queue.copy_external_image_to_texture(
|
||||||
|
&wgpu::CopyExternalImageSourceInfo {
|
||||||
|
source: wgpu::ExternalImageSource::ImageBitmap(image.clone()),
|
||||||
|
origin: wgpu::Origin2d::ZERO,
|
||||||
|
flip_y: false,
|
||||||
|
},
|
||||||
|
wgpu::TexelCopyTextureInfo {
|
||||||
|
texture: &texture.texture,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: wgpu::Origin3d::ZERO,
|
||||||
|
aspect: wgpu::TextureAspect::All,
|
||||||
|
}
|
||||||
|
.to_tagged(wgpu::PredefinedColorSpace::Srgb, false),
|
||||||
|
wgpu::Extent3d {
|
||||||
|
width: image.width(),
|
||||||
|
height: image.height(),
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
texture.uploaded = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn needs_upload(&self, id: &str) -> bool {
|
||||||
|
self.texture_slots
|
||||||
|
.get(id)
|
||||||
|
.and_then(|slot| self.textures.get(*slot))
|
||||||
|
.is_some_and(|texture| !texture.uploaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_timestamps(&self, pass: usize) -> Option<wgpu::RenderPassTimestampWrites<'_>> {
|
||||||
|
self.profiler
|
||||||
|
.as_ref()
|
||||||
|
.map(|profiler| wgpu::RenderPassTimestampWrites {
|
||||||
|
query_set: &profiler.query_set,
|
||||||
|
beginning_of_pass_write_index: Some(pass as u32 * 2),
|
||||||
|
end_of_pass_write_index: Some(pass as u32 * 2 + 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compute_timestamps(&self, pass: usize) -> Option<wgpu::ComputePassTimestampWrites<'_>> {
|
||||||
|
self.profiler
|
||||||
|
.as_ref()
|
||||||
|
.map(|profiler| wgpu::ComputePassTimestampWrites {
|
||||||
|
query_set: &profiler.query_set,
|
||||||
|
beginning_of_pass_write_index: Some(pass as u32 * 2),
|
||||||
|
end_of_pass_write_index: Some(pass as u32 * 2 + 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_profile(&self, encoder: &mut wgpu::CommandEncoder) {
|
||||||
|
let Some(profiler) = &self.profiler else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
encoder.resolve_query_set(
|
||||||
|
&profiler.query_set,
|
||||||
|
0..profiler.query_count,
|
||||||
|
&profiler.resolve,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
encoder.copy_buffer_to_buffer(
|
||||||
|
&profiler.resolve,
|
||||||
|
0,
|
||||||
|
&profiler.readback,
|
||||||
|
0,
|
||||||
|
u64::from(profiler.query_count) * 8,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_profile(&self, timestamp_period: f32) -> Option<ProfileMap> {
|
||||||
|
let profiler = self.profiler.as_ref()?;
|
||||||
|
let state = Arc::new(AtomicU8::new(0));
|
||||||
|
let callback = state.clone();
|
||||||
|
profiler
|
||||||
|
.readback
|
||||||
|
.map_async(wgpu::MapMode::Read, .., move |result| {
|
||||||
|
callback.store(if result.is_ok() { 1 } else { 2 }, Ordering::Release);
|
||||||
|
});
|
||||||
|
Some(ProfileMap {
|
||||||
|
readback: profiler.readback.clone(),
|
||||||
|
state,
|
||||||
|
labels: self
|
||||||
|
.passes
|
||||||
|
.iter()
|
||||||
|
.map(|pass| pass.label().to_owned())
|
||||||
|
.collect(),
|
||||||
|
timestamp_period,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileMap {
|
||||||
|
pub fn state(&self) -> u8 {
|
||||||
|
self.state.load(Ordering::Acquire)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self) -> Vec<(String, f64)> {
|
||||||
|
let bytes = self.readback.get_mapped_range(..);
|
||||||
|
let values = bytes
|
||||||
|
.chunks_exact(8)
|
||||||
|
.map(|bytes| u64::from_le_bytes(bytes.try_into().unwrap()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let profile = self
|
||||||
|
.labels
|
||||||
|
.into_iter()
|
||||||
|
.zip(values.chunks_exact(2))
|
||||||
|
.map(|(label, timestamps)| {
|
||||||
|
(
|
||||||
|
label,
|
||||||
|
timestamps[1].saturating_sub(timestamps[0]) as f64
|
||||||
|
* f64::from(self.timestamp_period)
|
||||||
|
/ 1_000_000.0,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
drop(bytes);
|
||||||
|
self.readback.unmap();
|
||||||
|
profile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GpuResources {
|
||||||
fn render_bundle(
|
fn render_bundle(
|
||||||
&self,
|
&self,
|
||||||
graph: &RenderGraph,
|
graph: &RenderGraph,
|
||||||
pass: &Pass,
|
passes: &[usize],
|
||||||
gpu: &Wgpu,
|
gpu: &Wgpu,
|
||||||
) -> Result<wgpu::RenderBundle, String> {
|
) -> Result<wgpu::RenderBundle, String> {
|
||||||
let pipeline = self
|
let pass = &graph.passes[passes[0]];
|
||||||
.render_pipelines
|
|
||||||
.get(&pass.pipeline)
|
|
||||||
.ok_or("GRAPH_PIPELINE")?;
|
|
||||||
let declaration = graph
|
let declaration = graph
|
||||||
.pipelines
|
.pipelines
|
||||||
.render
|
.render
|
||||||
@@ -204,40 +429,56 @@ impl GpuResources {
|
|||||||
sample_count: multisample(&declaration.multisample)?.count,
|
sample_count: multisample(&declaration.multisample)?.count,
|
||||||
multiview: None,
|
multiview: None,
|
||||||
});
|
});
|
||||||
encoder.set_pipeline(pipeline);
|
let mut previous_pipeline = None;
|
||||||
for (group, bind_group) in
|
let mut previous_bindings: Option<&[Binding]> = None;
|
||||||
self.bind_groups(pass, |group| pipeline.get_bind_group_layout(group), gpu)?
|
for index in passes {
|
||||||
{
|
let pass = &graph.passes[*index];
|
||||||
encoder.set_bind_group(group, &bind_group, &[]);
|
let pipeline = self
|
||||||
}
|
.render_pipelines
|
||||||
for binding in &pass.vertex_buffers {
|
.get(&pass.pipeline)
|
||||||
let buffer = &self
|
.ok_or("GRAPH_PIPELINE")?;
|
||||||
.buffers
|
let pipeline_changed = previous_pipeline != Some(pass.pipeline.as_str());
|
||||||
.get(&binding.resource)
|
if pipeline_changed {
|
||||||
.ok_or("GRAPH_RESOURCE_UNKNOWN")?
|
encoder.set_pipeline(pipeline);
|
||||||
.buffer;
|
previous_pipeline = Some(pass.pipeline.as_str());
|
||||||
encoder.set_vertex_buffer(binding.slot, buffer.slice(binding.offset..));
|
}
|
||||||
}
|
if pipeline_changed || previous_bindings != Some(pass.bindings.as_slice()) {
|
||||||
if let Some(binding) = &pass.index_buffer {
|
for (group, bind_group) in
|
||||||
let buffer = &self
|
self.bind_groups(pass, |group| pipeline.get_bind_group_layout(group), gpu)?
|
||||||
.buffers
|
{
|
||||||
.get(&binding.resource)
|
encoder.set_bind_group(group, &bind_group, &[]);
|
||||||
.ok_or("GRAPH_RESOURCE_UNKNOWN")?
|
}
|
||||||
.buffer;
|
previous_bindings = Some(&pass.bindings);
|
||||||
encoder.set_index_buffer(
|
}
|
||||||
buffer.slice(binding.offset..),
|
for binding in &pass.vertex_buffers {
|
||||||
parse(&binding.format, "GRAPH_INDEX_FORMAT")?,
|
let buffer = &self
|
||||||
);
|
.buffers
|
||||||
encoder.draw_indexed(
|
.get(&binding.resource)
|
||||||
pass.draw.first_index..pass.draw.first_index + pass.draw.indices,
|
.ok_or("GRAPH_RESOURCE_UNKNOWN")?
|
||||||
pass.draw.base_vertex,
|
.buffer;
|
||||||
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
|
encoder.set_vertex_buffer(binding.slot, buffer.slice(binding.offset..));
|
||||||
);
|
}
|
||||||
} else {
|
if let Some(binding) = &pass.index_buffer {
|
||||||
encoder.draw(
|
let buffer = &self
|
||||||
pass.draw.first_vertex..pass.draw.first_vertex + pass.draw.vertices,
|
.buffers
|
||||||
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
|
.get(&binding.resource)
|
||||||
);
|
.ok_or("GRAPH_RESOURCE_UNKNOWN")?
|
||||||
|
.buffer;
|
||||||
|
encoder.set_index_buffer(
|
||||||
|
buffer.slice(binding.offset..),
|
||||||
|
parse(&binding.format, "GRAPH_INDEX_FORMAT")?,
|
||||||
|
);
|
||||||
|
encoder.draw_indexed(
|
||||||
|
pass.draw.first_index..pass.draw.first_index + pass.draw.indices,
|
||||||
|
pass.draw.base_vertex,
|
||||||
|
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
encoder.draw(
|
||||||
|
pass.draw.first_vertex..pass.draw.first_vertex + pass.draw.vertices,
|
||||||
|
pass.draw.first_instance..pass.draw.first_instance + pass.draw.instances,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(encoder.finish(&wgpu::RenderBundleDescriptor {
|
Ok(encoder.finish(&wgpu::RenderBundleDescriptor {
|
||||||
label: Some(&pass.id),
|
label: Some(&pass.id),
|
||||||
@@ -290,6 +531,14 @@ impl GpuResources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GpuPass {
|
||||||
|
fn label(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Render { label, .. } | Self::Compute { label, .. } => label,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn create_render_pipeline(
|
fn create_render_pipeline(
|
||||||
source: &RenderPipeline,
|
source: &RenderPipeline,
|
||||||
gpu: &Wgpu,
|
gpu: &Wgpu,
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ pub struct RenderGraph {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub pipelines: PipelineDeclarations,
|
pub pipelines: PipelineDeclarations,
|
||||||
pub passes: Vec<Pass>,
|
pub passes: Vec<Pass>,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub executions: Vec<Execution>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Execution {
|
||||||
|
Compute(usize),
|
||||||
|
Render(Vec<usize>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Deserialize)]
|
#[derive(Clone, Default, Deserialize)]
|
||||||
@@ -30,6 +38,8 @@ pub struct Buffer {
|
|||||||
pub array: String,
|
pub array: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub usage: Vec<String>,
|
pub usage: Vec<String>,
|
||||||
|
#[serde(default = "frame_sync")]
|
||||||
|
pub sync: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
@@ -186,7 +196,7 @@ pub struct Pass {
|
|||||||
pub dispatch: [u32; 3],
|
pub dispatch: [u32; 3],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize, PartialEq, Eq)]
|
||||||
pub struct Binding {
|
pub struct Binding {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub group: u32,
|
pub group: u32,
|
||||||
@@ -318,7 +328,9 @@ impl RenderGraph {
|
|||||||
for pass in &mut self.passes {
|
for pass in &mut self.passes {
|
||||||
pass.dependencies = pass.after.iter().map(|id| sorted_ids[id]).collect();
|
pass.dependencies = pass.after.iter().map(|id| sorted_ids[id]).collect();
|
||||||
}
|
}
|
||||||
self.plan_resources()
|
self.plan_resources()?;
|
||||||
|
self.plan_execution();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plan_resources(&mut self) -> Result<(), &'static str> {
|
fn plan_resources(&mut self) -> Result<(), &'static str> {
|
||||||
@@ -391,6 +403,80 @@ impl RenderGraph {
|
|||||||
self.validate_ids()
|
self.validate_ids()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn plan_execution(&mut self) {
|
||||||
|
let mut executions = Vec::new();
|
||||||
|
for index in 0..self.passes.len() {
|
||||||
|
let merge = executions.last().is_some_and(|execution| match execution {
|
||||||
|
Execution::Render(passes) => self.can_merge_render(*passes.last().unwrap(), index),
|
||||||
|
Execution::Compute(_) => false,
|
||||||
|
});
|
||||||
|
if merge {
|
||||||
|
let Some(Execution::Render(passes)) = executions.last_mut() else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
passes.push(index);
|
||||||
|
} else if self.passes[index].kind == "render" {
|
||||||
|
executions.push(Execution::Render(vec![index]));
|
||||||
|
} else {
|
||||||
|
executions.push(Execution::Compute(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.executions = executions;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_merge_render(&self, previous: usize, next: usize) -> bool {
|
||||||
|
let previous = &self.passes[previous];
|
||||||
|
let next = &self.passes[next];
|
||||||
|
if next.kind != "render"
|
||||||
|
|| previous.color.len() != next.color.len()
|
||||||
|
|| self.sample_count(&previous.pipeline) != self.sample_count(&next.pipeline)
|
||||||
|
|| previous
|
||||||
|
.color
|
||||||
|
.iter()
|
||||||
|
.zip(&next.color)
|
||||||
|
.any(|(previous, next)| {
|
||||||
|
previous.resource != next.resource
|
||||||
|
|| previous.store != "store"
|
||||||
|
|| next.load != "load"
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let same_depth = match (&previous.depth, &next.depth) {
|
||||||
|
(None, None) => true,
|
||||||
|
(Some(previous), Some(next)) => {
|
||||||
|
previous.resource == next.resource
|
||||||
|
&& previous.store == "store"
|
||||||
|
&& next.load == "load"
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
same_depth
|
||||||
|
&& !next.bindings.iter().any(|binding| {
|
||||||
|
next.color
|
||||||
|
.iter()
|
||||||
|
.any(|attachment| attachment.resource == binding.resource)
|
||||||
|
|| next
|
||||||
|
.depth
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|attachment| attachment.resource == binding.resource)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_count(&self, pipeline: &str) -> Option<u64> {
|
||||||
|
self.pipelines
|
||||||
|
.render
|
||||||
|
.iter()
|
||||||
|
.find(|value| value.id == pipeline)
|
||||||
|
.map(|value| {
|
||||||
|
value
|
||||||
|
.multisample
|
||||||
|
.get("count")
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.unwrap_or(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_ids(&self) -> Result<(), &'static str> {
|
fn validate_ids(&self) -> Result<(), &'static str> {
|
||||||
let mut resources = HashSet::new();
|
let mut resources = HashSet::new();
|
||||||
for id in self
|
for id in self
|
||||||
@@ -405,6 +491,14 @@ impl RenderGraph {
|
|||||||
return Err("GRAPH_RESOURCE");
|
return Err("GRAPH_RESOURCE");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if self
|
||||||
|
.resources
|
||||||
|
.buffers
|
||||||
|
.iter()
|
||||||
|
.any(|buffer| !matches!(buffer.sync.as_str(), "frame" | "loadout"))
|
||||||
|
{
|
||||||
|
return Err("GRAPH_BUFFER_SYNC");
|
||||||
|
}
|
||||||
let mut pipelines = HashSet::new();
|
let mut pipelines = HashSet::new();
|
||||||
for id in self
|
for id in self
|
||||||
.pipelines
|
.pipelines
|
||||||
@@ -443,7 +537,7 @@ impl Pass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Texture {
|
impl Texture {
|
||||||
fn key(&self) -> Result<String, &'static str> {
|
pub(crate) fn key(&self) -> Result<String, &'static str> {
|
||||||
let mut usage = self.usage.clone();
|
let mut usage = self.usage.clone();
|
||||||
usage.sort_unstable();
|
usage.sort_unstable();
|
||||||
usage.dedup();
|
usage.dedup();
|
||||||
@@ -498,3 +592,6 @@ fn index_format() -> String {
|
|||||||
fn dispatch() -> [u32; 3] {
|
fn dispatch() -> [u32; 3] {
|
||||||
[1, 1, 1]
|
[1, 1, 1]
|
||||||
}
|
}
|
||||||
|
fn frame_sync() -> String {
|
||||||
|
"frame".into()
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +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>,
|
||||||
active: Option<Loadout>,
|
active: Option<Loadout>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,11 +26,44 @@ impl Store {
|
|||||||
|
|
||||||
pub fn switch(&mut self, id: &str, gpu: &Wgpu, data: &RenderData) -> Result<(), String> {
|
pub fn switch(&mut self, id: &str, gpu: &Wgpu, data: &RenderData) -> Result<(), String> {
|
||||||
let graph = self.graphs.get(id).ok_or("GRAPH_UNKNOWN")?.clone();
|
let graph = self.graphs.get(id).ok_or("GRAPH_UNKNOWN")?.clone();
|
||||||
let resources = GpuResources::activate(&graph, gpu, data)?;
|
let mut resources = GpuResources::activate(
|
||||||
|
&graph,
|
||||||
|
gpu,
|
||||||
|
data,
|
||||||
|
self.active.as_ref().map(|active| &active.resources),
|
||||||
|
)?;
|
||||||
|
for (name, image) in &self.texture_sources {
|
||||||
|
if resources.needs_upload(name) {
|
||||||
|
resources.upload_texture(name, image, gpu)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
self.active = Some(Loadout { graph, resources });
|
self.active = Some(Loadout { graph, resources });
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn upload_texture(
|
||||||
|
&mut self,
|
||||||
|
name: String,
|
||||||
|
image: web_sys::ImageBitmap,
|
||||||
|
gpu: &Wgpu,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(active) = &mut self.active {
|
||||||
|
if active.resources.texture_slots.contains_key(&name) {
|
||||||
|
active.resources.upload_texture(&name, &image, gpu)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(previous) = self.texture_sources.insert(name, image) {
|
||||||
|
previous.close();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_texture(&mut self, name: &str) {
|
||||||
|
if let Some(image) = self.texture_sources.remove(name) {
|
||||||
|
image.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn active_mut(&mut self) -> Option<&mut Loadout> {
|
pub fn active_mut(&mut self) -> Option<&mut Loadout> {
|
||||||
self.active.as_mut()
|
self.active.as_mut()
|
||||||
}
|
}
|
||||||
@@ -62,7 +96,17 @@ impl Store {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let graph = active.graph.clone();
|
let graph = active.graph.clone();
|
||||||
let resources = GpuResources::activate(&graph, gpu, data)?;
|
let mut resources = GpuResources::activate(
|
||||||
|
&graph,
|
||||||
|
gpu,
|
||||||
|
data,
|
||||||
|
self.active.as_ref().map(|active| &active.resources),
|
||||||
|
)?;
|
||||||
|
for (name, image) in &self.texture_sources {
|
||||||
|
if resources.needs_upload(name) {
|
||||||
|
resources.upload_texture(name, image, gpu)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
self.active = Some(Loadout { graph, resources });
|
self.active = Some(Loadout { graph, resources });
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+126
-18
@@ -1,10 +1,12 @@
|
|||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use gloo_timers::future::TimeoutFuture;
|
use gloo_timers::future::TimeoutFuture;
|
||||||
|
|
||||||
use crate::gpu::Wgpu;
|
use crate::gpu::Wgpu;
|
||||||
use crate::gpu_resource::GpuPass;
|
use crate::gpu_resource::{GpuPass, ProfileMap};
|
||||||
use crate::graph::{ColorAttachment, DepthAttachment};
|
use crate::graph::{ColorAttachment, DepthAttachment};
|
||||||
use crate::render_data::RenderData;
|
use crate::render_data::RenderData;
|
||||||
use crate::store::{Loadout, Store};
|
use crate::store::{Loadout, Store};
|
||||||
@@ -16,6 +18,13 @@ pub struct RenderLoop {
|
|||||||
frame: Cell<u32>,
|
frame: Cell<u32>,
|
||||||
elapsed: Cell<f64>,
|
elapsed: Cell<f64>,
|
||||||
last: Cell<f64>,
|
last: Cell<f64>,
|
||||||
|
profiling: Cell<bool>,
|
||||||
|
profile: RefCell<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Submission {
|
||||||
|
completed: Arc<AtomicBool>,
|
||||||
|
profile: Option<ProfileMap>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderLoop {
|
impl RenderLoop {
|
||||||
@@ -27,6 +36,8 @@ impl RenderLoop {
|
|||||||
frame: Cell::new(0),
|
frame: Cell::new(0),
|
||||||
elapsed: Cell::new(0.0),
|
elapsed: Cell::new(0.0),
|
||||||
last: Cell::new(js_sys::Date::now()),
|
last: Cell::new(js_sys::Date::now()),
|
||||||
|
profiling: Cell::new(false),
|
||||||
|
profile: RefCell::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +58,17 @@ impl RenderLoop {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_profiling(&self, enabled: bool) {
|
||||||
|
self.profiling.set(enabled);
|
||||||
|
if !enabled {
|
||||||
|
self.profile.borrow_mut().take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_profile(&self) -> Option<String> {
|
||||||
|
self.profile.borrow_mut().take()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start(
|
pub fn start(
|
||||||
self: &Rc<Self>,
|
self: &Rc<Self>,
|
||||||
gpu: Rc<RefCell<Option<Wgpu>>>,
|
gpu: Rc<RefCell<Option<Wgpu>>>,
|
||||||
@@ -71,11 +93,51 @@ impl RenderLoop {
|
|||||||
data.update_info(delta as f32, frame, elapsed as f32, control.fps.get());
|
data.update_info(delta as f32, frame, elapsed as f32, control.fps.get());
|
||||||
data.skip_render()
|
data.skip_render()
|
||||||
};
|
};
|
||||||
if !skip {
|
let submission = if !skip {
|
||||||
if let (Some(gpu), Some(loadout)) =
|
if let (Some(gpu), Some(loadout)) =
|
||||||
(gpu.borrow_mut().as_mut(), store.borrow_mut().active_mut())
|
(gpu.borrow_mut().as_mut(), store.borrow_mut().active_mut())
|
||||||
{
|
{
|
||||||
let _ = gpu.render(loadout, &data.borrow());
|
gpu.render(loadout, &data.borrow(), control.profiling.get())
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(submission) = submission {
|
||||||
|
while !submission.completed.load(Ordering::Acquire) {
|
||||||
|
TimeoutFuture::new(0).await;
|
||||||
|
}
|
||||||
|
if let Some(profile) = submission.profile {
|
||||||
|
while profile.state() == 0 {
|
||||||
|
TimeoutFuture::new(0).await;
|
||||||
|
}
|
||||||
|
if profile.state() == 1 {
|
||||||
|
let passes = profile
|
||||||
|
.read()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, milliseconds)| {
|
||||||
|
serde_json::json!({
|
||||||
|
"name": name,
|
||||||
|
"milliseconds": milliseconds,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let milliseconds = passes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|pass| pass["milliseconds"].as_f64())
|
||||||
|
.sum::<f64>();
|
||||||
|
*control.profile.borrow_mut() = Some(
|
||||||
|
serde_json::json!({
|
||||||
|
"frame": frame,
|
||||||
|
"milliseconds": milliseconds,
|
||||||
|
"passes": passes,
|
||||||
|
})
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,8 +150,16 @@ impl RenderLoop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Wgpu {
|
impl Wgpu {
|
||||||
fn render(&mut self, loadout: &mut Loadout, data: &RenderData) -> Result<(), String> {
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
loadout: &mut Loadout,
|
||||||
|
data: &RenderData,
|
||||||
|
profile: bool,
|
||||||
|
) -> Result<Option<Submission>, String> {
|
||||||
for buffer in loadout.resources.buffers.values() {
|
for buffer in loadout.resources.buffers.values() {
|
||||||
|
if !buffer.sync_each_frame {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
self.queue.write_buffer(
|
self.queue.write_buffer(
|
||||||
&buffer.buffer,
|
&buffer.buffer,
|
||||||
0,
|
0,
|
||||||
@@ -102,7 +172,7 @@ impl Wgpu {
|
|||||||
self.surface.configure(&self.device, &self.config);
|
self.surface.configure(&self.device, &self.config);
|
||||||
self.surface.get_current_texture().map_err(|_| "SURFACE")?
|
self.surface.get_current_texture().map_err(|_| "SURFACE")?
|
||||||
}
|
}
|
||||||
Err(wgpu::SurfaceError::Timeout) => return Ok(()),
|
Err(wgpu::SurfaceError::Timeout) => return Ok(None),
|
||||||
Err(_) => return Err("SURFACE".into()),
|
Err(_) => return Err("SURFACE".into()),
|
||||||
};
|
};
|
||||||
let surface_view = output
|
let surface_view = output
|
||||||
@@ -113,15 +183,21 @@ impl Wgpu {
|
|||||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||||
label: Some("frame"),
|
label: Some("frame"),
|
||||||
});
|
});
|
||||||
for (pass, compiled) in loadout.graph.passes.iter().zip(&loadout.resources.passes) {
|
for (pass_index, compiled) in loadout.resources.passes.iter().enumerate() {
|
||||||
match compiled {
|
match compiled {
|
||||||
GpuPass::Compute {
|
GpuPass::Compute {
|
||||||
|
pass,
|
||||||
pipeline,
|
pipeline,
|
||||||
bind_groups,
|
bind_groups,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
|
let pass = &loadout.graph.passes[*pass];
|
||||||
|
let timestamp_writes = profile
|
||||||
|
.then(|| loadout.resources.compute_timestamps(pass_index))
|
||||||
|
.flatten();
|
||||||
let mut command = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
let mut command = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||||
label: Some(&pass.id),
|
label: Some(&pass.id),
|
||||||
timestamp_writes: None,
|
timestamp_writes,
|
||||||
});
|
});
|
||||||
command.set_pipeline(pipeline);
|
command.set_pipeline(pipeline);
|
||||||
for (group, bind_group) in bind_groups {
|
for (group, bind_group) in bind_groups {
|
||||||
@@ -133,11 +209,19 @@ impl Wgpu {
|
|||||||
pass.dispatch[2],
|
pass.dispatch[2],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
GpuPass::Render(bundle) => {
|
GpuPass::Render {
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
bundle,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let pass = &loadout.graph.passes[*first];
|
||||||
|
let last = &loadout.graph.passes[*last];
|
||||||
let colors = pass
|
let colors = pass
|
||||||
.color
|
.color
|
||||||
.iter()
|
.iter()
|
||||||
.map(|attachment| {
|
.zip(&last.color)
|
||||||
|
.map(|(attachment, final_attachment)| {
|
||||||
Ok(Some(wgpu::RenderPassColorAttachment {
|
Ok(Some(wgpu::RenderPassColorAttachment {
|
||||||
view: view(
|
view: view(
|
||||||
&loadout.resources,
|
&loadout.resources,
|
||||||
@@ -146,39 +230,57 @@ impl Wgpu {
|
|||||||
)?,
|
)?,
|
||||||
depth_slice: None,
|
depth_slice: None,
|
||||||
resolve_target: None,
|
resolve_target: None,
|
||||||
ops: color_ops(attachment)?,
|
ops: color_ops(attachment, final_attachment)?,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, String>>()?;
|
.collect::<Result<Vec<_>, String>>()?;
|
||||||
let depth = pass
|
let depth = pass
|
||||||
.depth
|
.depth
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|attachment| {
|
.zip(last.depth.as_ref())
|
||||||
|
.map(|(attachment, final_attachment)| {
|
||||||
Ok::<_, String>(wgpu::RenderPassDepthStencilAttachment {
|
Ok::<_, String>(wgpu::RenderPassDepthStencilAttachment {
|
||||||
view: view(
|
view: view(
|
||||||
&loadout.resources,
|
&loadout.resources,
|
||||||
&surface_view,
|
&surface_view,
|
||||||
&attachment.resource,
|
&attachment.resource,
|
||||||
)?,
|
)?,
|
||||||
depth_ops: Some(depth_ops(attachment)?),
|
depth_ops: Some(depth_ops(attachment, final_attachment)?),
|
||||||
stencil_ops: None,
|
stencil_ops: None,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
let timestamp_writes = profile
|
||||||
|
.then(|| loadout.resources.render_timestamps(pass_index))
|
||||||
|
.flatten();
|
||||||
let mut command = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let mut command = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some(&pass.id),
|
label: Some(&pass.id),
|
||||||
color_attachments: &colors,
|
color_attachments: &colors,
|
||||||
depth_stencil_attachment: depth,
|
depth_stencil_attachment: depth,
|
||||||
timestamp_writes: None,
|
timestamp_writes,
|
||||||
occlusion_query_set: None,
|
occlusion_query_set: None,
|
||||||
});
|
});
|
||||||
command.execute_bundles([bundle]);
|
command.execute_bundles([bundle]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if profile {
|
||||||
|
loadout.resources.resolve_profile(&mut encoder);
|
||||||
|
}
|
||||||
self.queue.submit([encoder.finish()]);
|
self.queue.submit([encoder.finish()]);
|
||||||
|
let profile = profile
|
||||||
|
.then(|| {
|
||||||
|
loadout
|
||||||
|
.resources
|
||||||
|
.map_profile(self.queue.get_timestamp_period())
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
output.present();
|
output.present();
|
||||||
Ok(())
|
let completed = Arc::new(AtomicBool::new(false));
|
||||||
|
let callback = completed.clone();
|
||||||
|
self.queue
|
||||||
|
.on_submitted_work_done(move || callback.store(true, Ordering::Release));
|
||||||
|
Ok(Some(Submission { completed, profile }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +298,10 @@ fn view<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn color_ops(attachment: &ColorAttachment) -> Result<wgpu::Operations<wgpu::Color>, String> {
|
fn color_ops(
|
||||||
|
attachment: &ColorAttachment,
|
||||||
|
final_attachment: &ColorAttachment,
|
||||||
|
) -> Result<wgpu::Operations<wgpu::Color>, String> {
|
||||||
let clear = attachment.clear.as_slice();
|
let clear = attachment.clear.as_slice();
|
||||||
Ok(wgpu::Operations {
|
Ok(wgpu::Operations {
|
||||||
load: match attachment.load.as_str() {
|
load: match attachment.load.as_str() {
|
||||||
@@ -209,18 +314,21 @@ fn color_ops(attachment: &ColorAttachment) -> Result<wgpu::Operations<wgpu::Colo
|
|||||||
}),
|
}),
|
||||||
_ => return Err("GRAPH_LOAD_OP".into()),
|
_ => return Err("GRAPH_LOAD_OP".into()),
|
||||||
},
|
},
|
||||||
store: store_op(&attachment.store)?,
|
store: store_op(&final_attachment.store)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn depth_ops(attachment: &DepthAttachment) -> Result<wgpu::Operations<f32>, String> {
|
fn depth_ops(
|
||||||
|
attachment: &DepthAttachment,
|
||||||
|
final_attachment: &DepthAttachment,
|
||||||
|
) -> Result<wgpu::Operations<f32>, String> {
|
||||||
Ok(wgpu::Operations {
|
Ok(wgpu::Operations {
|
||||||
load: match attachment.load.as_str() {
|
load: match attachment.load.as_str() {
|
||||||
"load" => wgpu::LoadOp::Load,
|
"load" => wgpu::LoadOp::Load,
|
||||||
"clear" => wgpu::LoadOp::Clear(attachment.clear),
|
"clear" => wgpu::LoadOp::Clear(attachment.clear),
|
||||||
_ => return Err("GRAPH_LOAD_OP".into()),
|
_ => return Err("GRAPH_LOAD_OP".into()),
|
||||||
},
|
},
|
||||||
store: store_op(&attachment.store)?,
|
store: store_op(&final_attachment.store)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ pub struct Wgpu {
|
|||||||
pub format: wgpu::TextureFormat,
|
pub format: wgpu::TextureFormat,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
|
pub timestamp_queries: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Wgpu {
|
impl Wgpu {
|
||||||
@@ -30,8 +31,12 @@ impl Wgpu {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| "WEBGPU_UNAVAILABLE")?;
|
.map_err(|_| "WEBGPU_UNAVAILABLE")?;
|
||||||
|
let required_features = adapter.features() & wgpu::Features::TIMESTAMP_QUERY;
|
||||||
let (device, queue) = adapter
|
let (device, queue) = adapter
|
||||||
.request_device(&wgpu::DeviceDescriptor::default())
|
.request_device(&wgpu::DeviceDescriptor {
|
||||||
|
required_features,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| "DEVICE")?;
|
.map_err(|_| "DEVICE")?;
|
||||||
let config = surface
|
let config = surface
|
||||||
@@ -48,6 +53,7 @@ impl Wgpu {
|
|||||||
format,
|
format,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
timestamp_queries: required_features.contains(wgpu::Features::TIMESTAMP_QUERY),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import initWasm, { Core } from "./pkg/yawn_core.js";
|
import initWasm, { Core } from "./pkg/yawn_core.js";
|
||||||
|
|
||||||
let core;
|
let core;
|
||||||
|
let profileTimer;
|
||||||
|
|
||||||
const fail = code => { throw new Error(code); };
|
const fail = code => { throw new Error(code); };
|
||||||
|
|
||||||
@@ -39,6 +40,12 @@ addEventListener("message", async ({ data: message }) => {
|
|||||||
case "switch-loadout":
|
case "switch-loadout":
|
||||||
core.switch_loadout(message.id);
|
core.switch_loadout(message.id);
|
||||||
break;
|
break;
|
||||||
|
case "upload-texture":
|
||||||
|
core.upload_texture(message.name, message.image);
|
||||||
|
break;
|
||||||
|
case "delete-texture":
|
||||||
|
core.delete_texture(message.name);
|
||||||
|
break;
|
||||||
case "play":
|
case "play":
|
||||||
core.play();
|
core.play();
|
||||||
break;
|
break;
|
||||||
@@ -48,6 +55,14 @@ addEventListener("message", async ({ data: message }) => {
|
|||||||
case "set-fps":
|
case "set-fps":
|
||||||
core.set_fps(message.fps);
|
core.set_fps(message.fps);
|
||||||
break;
|
break;
|
||||||
|
case "set-profiler":
|
||||||
|
result = core.set_profiler(Boolean(message.enabled));
|
||||||
|
clearInterval(profileTimer);
|
||||||
|
profileTimer = result && message.enabled ? setInterval(() => {
|
||||||
|
const stats = core.take_profile();
|
||||||
|
if (stats) postMessage({ type: "profile", stats: JSON.parse(stats) });
|
||||||
|
}, 250) : undefined;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
fail("MESSAGE");
|
fail("MESSAGE");
|
||||||
}
|
}
|
||||||
|
|||||||
+317
-45
@@ -1,36 +1,117 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
import { basicSetup } from "codemirror";
|
||||||
|
import { javascript } from "@codemirror/lang-javascript";
|
||||||
|
import { oneDark } from "@codemirror/theme-one-dark";
|
||||||
|
import { EditorView } from "@codemirror/view";
|
||||||
|
import { computed, nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||||
import * as Handles from "@yawn/handles";
|
import * as Handles from "@yawn/handles";
|
||||||
import { YawnCore } from "@yawn/core";
|
import { YawnCore } from "@yawn/core";
|
||||||
import { playgrounds } from "./playgrounds";
|
import { playgrounds } from "./playgrounds";
|
||||||
|
|
||||||
const props = defineProps({ example: { type: String, default: "triangle" } });
|
const props = defineProps({
|
||||||
const preset = playgrounds[props.example] ?? playgrounds.triangle;
|
example: { type: String, default: "triangle" },
|
||||||
|
fullscreen: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
const examples = Object.entries(playgrounds);
|
||||||
|
const selected = ref(playgrounds[props.example] ? props.example : "triangle");
|
||||||
|
const preset = computed(() => playgrounds[selected.value]);
|
||||||
const canvas = ref();
|
const canvas = ref();
|
||||||
const source = ref(preset.code);
|
const editor = ref();
|
||||||
|
const preview = ref();
|
||||||
|
const source = ref(preset.value.code);
|
||||||
const status = ref("Starting…");
|
const status = ref("Starting…");
|
||||||
const output = ref([]);
|
const output = ref([]);
|
||||||
const failed = ref(false);
|
const failed = ref(false);
|
||||||
const running = ref(false);
|
const running = ref(false);
|
||||||
const canvasKey = ref(0);
|
const canvasKey = ref(0);
|
||||||
const fps = ref(0);
|
const fps = ref(0);
|
||||||
|
const profilerOpen = ref(false);
|
||||||
|
const profilerSupported = ref(null);
|
||||||
|
const profile = ref(null);
|
||||||
let generation = 0;
|
let generation = 0;
|
||||||
let current;
|
let current;
|
||||||
|
let stopProfile;
|
||||||
let fpsFrame = 0;
|
let fpsFrame = 0;
|
||||||
let sampledFrame = 0;
|
let sampledFrame = 0;
|
||||||
let sampledAt = 0;
|
let sampledAt = 0;
|
||||||
|
let editorView;
|
||||||
|
|
||||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
||||||
const api = { ...Handles, YawnCore };
|
const api = { ...Handles, YawnCore };
|
||||||
|
|
||||||
|
function updateSource(value) {
|
||||||
|
source.value = value;
|
||||||
|
if (editorView && editorView.state.doc.toString() !== value) {
|
||||||
|
editorView.dispatch({
|
||||||
|
changes: { from: 0, to: editorView.state.doc.length, insert: value },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestedSave() {
|
||||||
|
if (!props.fullscreen) return selected.value;
|
||||||
|
const save = new URL(location.href).searchParams.get("save");
|
||||||
|
return save && playgrounds[save] ? save : "triangle";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSave(event, save, push = true) {
|
||||||
|
event?.preventDefault();
|
||||||
|
selected.value = playgrounds[save] ? save : "triangle";
|
||||||
|
updateSource(preset.value.code);
|
||||||
|
if (props.fullscreen && push) {
|
||||||
|
const url = new URL(location.href);
|
||||||
|
url.searchParams.set("save", selected.value);
|
||||||
|
history.pushState(null, "", url);
|
||||||
|
}
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreSave() {
|
||||||
|
openSave(undefined, requestedSave(), false);
|
||||||
|
}
|
||||||
|
|
||||||
async function dispose(value = current) {
|
async function dispose(value = current) {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
current = undefined;
|
if (value === current) {
|
||||||
delete window.__yawnPlayground;
|
stopProfile?.();
|
||||||
|
stopProfile = undefined;
|
||||||
|
current = undefined;
|
||||||
|
delete window.__yawnPlayground;
|
||||||
|
}
|
||||||
if (typeof value === "function") await value();
|
if (typeof value === "function") await value();
|
||||||
else if (typeof value.dispose === "function") await value.dispose();
|
else if (typeof value.dispose === "function") await value.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function attachProfiler(runId = generation) {
|
||||||
|
stopProfile?.();
|
||||||
|
stopProfile = undefined;
|
||||||
|
profile.value = null;
|
||||||
|
profilerSupported.value = null;
|
||||||
|
if (!profilerOpen.value) return;
|
||||||
|
const core = current?.scene?.core ?? current?.core;
|
||||||
|
if (!core?.onProfile || !core?.setProfiler) {
|
||||||
|
profilerSupported.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopProfile = core.onProfile((stats) => {
|
||||||
|
if (runId === generation) profile.value = stats;
|
||||||
|
});
|
||||||
|
profilerSupported.value = await core.setProfiler(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleProfiler() {
|
||||||
|
profilerOpen.value = !profilerOpen.value;
|
||||||
|
if (profilerOpen.value) {
|
||||||
|
await attachProfiler();
|
||||||
|
} else {
|
||||||
|
stopProfile?.();
|
||||||
|
stopProfile = undefined;
|
||||||
|
profile.value = null;
|
||||||
|
const core = current?.scene?.core ?? current?.core;
|
||||||
|
await core?.setProfiler?.(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const runId = ++generation;
|
const runId = ++generation;
|
||||||
running.value = true;
|
running.value = true;
|
||||||
@@ -43,8 +124,12 @@ async function run() {
|
|||||||
throw new Error("Cross-origin isolation is disabled");
|
throw new Error("Cross-origin isolation is disabled");
|
||||||
canvasKey.value++;
|
canvasKey.value++;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
canvas.value.width = 960;
|
canvas.value.width = props.fullscreen
|
||||||
canvas.value.height = 540;
|
? Math.max(1, preview.value.clientWidth)
|
||||||
|
: 960;
|
||||||
|
canvas.value.height = props.fullscreen
|
||||||
|
? Math.max(1, preview.value.clientHeight)
|
||||||
|
: 540;
|
||||||
const executable = source.value.replace(/^\s*import\s+[^;]+;\s*$/gm, "");
|
const executable = source.value.replace(/^\s*import\s+[^;]+;\s*$/gm, "");
|
||||||
const names = Object.keys(api);
|
const names = Object.keys(api);
|
||||||
const started = performance.now();
|
const started = performance.now();
|
||||||
@@ -54,7 +139,7 @@ async function run() {
|
|||||||
"log",
|
"log",
|
||||||
executable,
|
executable,
|
||||||
)(...names.map((name) => api[name]), canvas.value, (message) =>
|
)(...names.map((name) => api[name]), canvas.value, (message) =>
|
||||||
output.value.push(String(message)),
|
runId === generation && output.value.push(String(message)),
|
||||||
);
|
);
|
||||||
if (runId !== generation) {
|
if (runId !== generation) {
|
||||||
await dispose(result);
|
await dispose(result);
|
||||||
@@ -62,29 +147,23 @@ async function run() {
|
|||||||
}
|
}
|
||||||
current = result;
|
current = result;
|
||||||
window.__yawnPlayground = result;
|
window.__yawnPlayground = result;
|
||||||
|
await attachProfiler(runId);
|
||||||
status.value = `Running · ${Math.round(performance.now() - started)} ms`;
|
status.value = `Running · ${Math.round(performance.now() - started)} ms`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
failed.value = true;
|
if (runId === generation) {
|
||||||
status.value = error instanceof Error ? error.message : String(error);
|
failed.value = true;
|
||||||
|
status.value = error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (runId === generation) running.value = false;
|
if (runId === generation) running.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
source.value = preset.code;
|
updateSource(preset.value.code);
|
||||||
run();
|
run();
|
||||||
}
|
}
|
||||||
|
|
||||||
function tab(event) {
|
|
||||||
if (event.key !== "Tab") return;
|
|
||||||
event.preventDefault();
|
|
||||||
const editor = event.currentTarget;
|
|
||||||
const start = editor.selectionStart;
|
|
||||||
source.value = `${source.value.slice(0, start)} ${source.value.slice(editor.selectionEnd)}`;
|
|
||||||
requestAnimationFrame(() => editor.setSelectionRange(start + 2, start + 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
function sampleFps(time = performance.now()) {
|
function sampleFps(time = performance.now()) {
|
||||||
try {
|
try {
|
||||||
const core = current?.scene?.core ?? current?.core;
|
const core = current?.scene?.core ?? current?.core;
|
||||||
@@ -95,9 +174,8 @@ function sampleFps(time = performance.now()) {
|
|||||||
sampledFrame = frame;
|
sampledFrame = frame;
|
||||||
sampledAt = time;
|
sampledAt = time;
|
||||||
} else if (time - sampledAt >= 500) {
|
} else if (time - sampledAt >= 500) {
|
||||||
fps.value = Math.round(
|
const measured = ((frame - sampledFrame) * 1000) / (time - sampledAt);
|
||||||
((frame - sampledFrame) * 1000) / (time - sampledAt),
|
fps.value = measured < 10 ? measured.toFixed(1) : Math.round(measured);
|
||||||
);
|
|
||||||
sampledFrame = frame;
|
sampledFrame = frame;
|
||||||
sampledAt = time;
|
sampledAt = time;
|
||||||
}
|
}
|
||||||
@@ -110,19 +188,46 @@ function sampleFps(time = performance.now()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
selected.value = requestedSave();
|
||||||
|
source.value = preset.value.code;
|
||||||
|
editorView = new EditorView({
|
||||||
|
doc: source.value,
|
||||||
|
parent: editor.value,
|
||||||
|
extensions: [
|
||||||
|
basicSetup,
|
||||||
|
javascript(),
|
||||||
|
oneDark,
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
EditorView.contentAttributes.of({
|
||||||
|
"aria-label": "Editable playground code",
|
||||||
|
spellcheck: "false",
|
||||||
|
}),
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged) source.value = update.state.doc.toString();
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (props.fullscreen) addEventListener("popstate", restoreSave);
|
||||||
sampleFps();
|
sampleFps();
|
||||||
run();
|
run();
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
generation++;
|
generation++;
|
||||||
|
removeEventListener("popstate", restoreSave);
|
||||||
|
editorView?.destroy();
|
||||||
cancelAnimationFrame(fpsFrame);
|
cancelAnimationFrame(fpsFrame);
|
||||||
dispose();
|
dispose();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="playground" :aria-label="`${preset.title} playground`">
|
<section
|
||||||
|
class="playground"
|
||||||
|
:class="{ fullscreen }"
|
||||||
|
:aria-label="`${preset.title} playground`"
|
||||||
|
>
|
||||||
<header>
|
<header>
|
||||||
|
<a v-if="fullscreen" class="brand" href="/">Yawn</a>
|
||||||
<strong>{{ preset.title }}</strong>
|
<strong>{{ preset.title }}</strong>
|
||||||
<span :class="{ failed }" data-playground-status>{{ status }}</span>
|
<span :class="{ failed }" data-playground-status>{{ status }}</span>
|
||||||
<button
|
<button
|
||||||
@@ -133,21 +238,54 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="fullscreen"
|
||||||
|
type="button"
|
||||||
|
class="secondary"
|
||||||
|
:aria-pressed="profilerOpen"
|
||||||
|
data-profiler-toggle
|
||||||
|
@click="toggleProfiler"
|
||||||
|
>
|
||||||
|
Profile
|
||||||
|
</button>
|
||||||
<button type="button" :disabled="running" @click="run">
|
<button type="button" :disabled="running" @click="run">
|
||||||
{{ running ? "Running…" : "Run" }}
|
{{ running ? "Running…" : "Run" }}
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
<nav v-if="fullscreen" class="saves" aria-label="Saved playgrounds">
|
||||||
|
<span>Saved</span>
|
||||||
|
<a
|
||||||
|
v-for="([save, item]) in examples"
|
||||||
|
:key="save"
|
||||||
|
:href="`/playground?save=${save}`"
|
||||||
|
:aria-current="save === selected ? 'page' : undefined"
|
||||||
|
@click="openSave($event, save)"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
<div class="workspace">
|
<div class="workspace">
|
||||||
<textarea
|
<div ref="editor" class="editor" />
|
||||||
v-model="source"
|
<div ref="preview" class="preview">
|
||||||
aria-label="Editable playground code"
|
|
||||||
autocomplete="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
@keydown="tab"
|
|
||||||
/>
|
|
||||||
<div class="preview">
|
|
||||||
<canvas :key="canvasKey" ref="canvas" aria-label="Yawn WebGPU output" />
|
<canvas :key="canvasKey" ref="canvas" aria-label="Yawn WebGPU output" />
|
||||||
|
<aside v-if="profilerOpen" class="profiler" data-playground-profiler>
|
||||||
|
<div class="profiler-title">
|
||||||
|
<strong>GPU passes</strong>
|
||||||
|
<span v-if="profile">{{ profile.milliseconds.toFixed(2) }} ms</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="profilerSupported === false">
|
||||||
|
Timestamp queries are unavailable on this GPU.
|
||||||
|
</p>
|
||||||
|
<p v-else-if="!profile">Waiting for a completed frame…</p>
|
||||||
|
<table v-else>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(pass, index) in profile.passes" :key="`${index}-${pass.name}`">
|
||||||
|
<th>{{ pass.name }}</th>
|
||||||
|
<td>{{ pass.milliseconds.toFixed(2) }} ms</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</aside>
|
||||||
<div v-if="output.length" class="output" data-playground-log>
|
<div v-if="output.length" class="output" data-playground-log>
|
||||||
<div v-for="(line, index) in output" :key="index">{{ line }}</div>
|
<div v-for="(line, index) in output" :key="index">{{ line }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,6 +308,22 @@ onUnmounted(() => {
|
|||||||
background: #0b1020;
|
background: #0b1020;
|
||||||
box-shadow: var(--vp-shadow-3);
|
box-shadow: var(--vp-shadow-3);
|
||||||
}
|
}
|
||||||
|
.playground.fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
inset: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
margin: 0;
|
||||||
|
transform: none;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
:global(.VPContent.has-sidebar .playground) {
|
:global(.VPContent.has-sidebar .playground) {
|
||||||
left: calc(50% + var(--vp-sidebar-width) / 2);
|
left: calc(50% + var(--vp-sidebar-width) / 2);
|
||||||
width: min(1120px, calc(100vw - var(--vp-sidebar-width) - 64px));
|
width: min(1120px, calc(100vw - var(--vp-sidebar-width) - 64px));
|
||||||
@@ -193,6 +347,14 @@ header {
|
|||||||
header strong {
|
header strong {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.brand {
|
||||||
|
padding-right: 12px;
|
||||||
|
color: #60a5fa;
|
||||||
|
border-right: 1px solid #334155;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
header span {
|
header span {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -223,24 +385,64 @@ button:disabled {
|
|||||||
cursor: wait;
|
cursor: wait;
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
}
|
}
|
||||||
|
.saves {
|
||||||
|
display: flex;
|
||||||
|
min-height: 34px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 4px 10px;
|
||||||
|
color: #64748b;
|
||||||
|
border-bottom: 1px solid #263249;
|
||||||
|
background: #0d1424;
|
||||||
|
font:
|
||||||
|
11px/1 ui-monospace,
|
||||||
|
SFMono-Regular,
|
||||||
|
Menlo,
|
||||||
|
monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.saves span {
|
||||||
|
margin-right: 3px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
.saves a {
|
||||||
|
padding: 5px 7px;
|
||||||
|
color: #94a3b8;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.saves a:hover {
|
||||||
|
color: #e2e8f0;
|
||||||
|
background: #1e293b;
|
||||||
|
}
|
||||||
|
.saves a[aria-current="page"] {
|
||||||
|
color: #bfdbfe;
|
||||||
|
background: #1d4ed8;
|
||||||
|
}
|
||||||
.workspace {
|
.workspace {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
min-height: 510px;
|
min-height: 510px;
|
||||||
}
|
}
|
||||||
textarea {
|
.fullscreen .workspace {
|
||||||
box-sizing: border-box;
|
min-height: 0;
|
||||||
width: 100%;
|
flex: 1;
|
||||||
|
}
|
||||||
|
.editor {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 510px;
|
height: 510px;
|
||||||
resize: none;
|
|
||||||
padding: 18px;
|
|
||||||
color: #dbeafe;
|
|
||||||
border: 0;
|
|
||||||
border-right: 1px solid #263249;
|
border-right: 1px solid #263249;
|
||||||
outline: none;
|
|
||||||
background: #0b1020;
|
background: #0b1020;
|
||||||
tab-size: 2;
|
}
|
||||||
|
.fullscreen .editor {
|
||||||
|
height: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.editor :deep(.cm-editor) {
|
||||||
|
height: 100%;
|
||||||
|
background: #0b1020;
|
||||||
font:
|
font:
|
||||||
13px/1.55 ui-monospace,
|
13px/1.55 ui-monospace,
|
||||||
SFMono-Regular,
|
SFMono-Regular,
|
||||||
@@ -248,7 +450,17 @@ textarea {
|
|||||||
Consolas,
|
Consolas,
|
||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
textarea:focus {
|
.editor :deep(.cm-scroller) {
|
||||||
|
overflow: auto;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
.editor :deep(.cm-gutters) {
|
||||||
|
color: #526079;
|
||||||
|
border-right-color: #263249;
|
||||||
|
background: #0d1424;
|
||||||
|
}
|
||||||
|
.editor :deep(.cm-focused) {
|
||||||
|
outline: none;
|
||||||
box-shadow: inset 0 0 0 2px #2563eb;
|
box-shadow: inset 0 0 0 2px #2563eb;
|
||||||
}
|
}
|
||||||
.preview {
|
.preview {
|
||||||
@@ -281,6 +493,59 @@ canvas {
|
|||||||
Menlo,
|
Menlo,
|
||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
.profiler {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: min(320px, 72%);
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px;
|
||||||
|
color: #cbd5e1;
|
||||||
|
border-left: 1px solid #334155;
|
||||||
|
background: rgb(2 6 23 / 94%);
|
||||||
|
font:
|
||||||
|
12px/1.45 ui-monospace,
|
||||||
|
SFMono-Regular,
|
||||||
|
Menlo,
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
.profiler-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
.profiler-title span {
|
||||||
|
color: #60a5fa;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.profiler p {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.profiler table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
.profiler th,
|
||||||
|
.profiler td {
|
||||||
|
padding: 7px 0;
|
||||||
|
border-bottom: 1px solid #1e293b;
|
||||||
|
}
|
||||||
|
.profiler th {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 190px;
|
||||||
|
text-align: left;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.profiler td {
|
||||||
|
color: #93c5fd;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.fps {
|
.fps {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
@@ -309,14 +574,21 @@ canvas {
|
|||||||
.workspace {
|
.workspace {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
textarea {
|
.editor {
|
||||||
height: 360px;
|
height: 360px;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-bottom: 1px solid #263249;
|
border-bottom: 1px solid #263249;
|
||||||
}
|
}
|
||||||
|
.fullscreen .editor {
|
||||||
|
height: 45vh;
|
||||||
|
min-height: 240px;
|
||||||
|
}
|
||||||
.preview {
|
.preview {
|
||||||
min-height: 360px;
|
min-height: 360px;
|
||||||
}
|
}
|
||||||
|
.fullscreen .preview {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
header strong {
|
header strong {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,12 +137,25 @@ log(\`Two mesh handles share geometry #\${source.geometryId}.\`);
|
|||||||
|
|
||||||
return { scene, source, instance, dispose: () => scene.dispose() };`;
|
return { scene, source, instance, dispose: () => scene.dispose() };`;
|
||||||
|
|
||||||
const materials = `import { Mesh, PBRMaterial, Scene } from "@yawn/handles";
|
const materials = `import { Mesh, PBRMaterial, Scene, Texture } from "@yawn/handles";
|
||||||
|
|
||||||
const scene = new Scene(canvas, { hdr: true });
|
const scene = new Scene(canvas, { hdr: true });
|
||||||
await scene.ready;
|
await scene.ready;
|
||||||
|
const pixels = new OffscreenCanvas(64, 64);
|
||||||
|
const context = pixels.getContext("2d");
|
||||||
|
context.fillStyle = "#ff3018";
|
||||||
|
context.fillRect(0, 0, 64, 64);
|
||||||
|
context.fillStyle = "#ffd166";
|
||||||
|
context.fillRect(0, 0, 32, 32);
|
||||||
|
context.fillRect(32, 32, 32, 32);
|
||||||
|
const albedo = new Texture(scene, {
|
||||||
|
source: pixels.transferToImageBitmap(),
|
||||||
|
format: "rgba8unorm-srgb",
|
||||||
|
});
|
||||||
|
await albedo.ready;
|
||||||
const paint = new PBRMaterial(scene, {
|
const paint = new PBRMaterial(scene, {
|
||||||
baseColor: [0.85, 0.08, 0.18, 1],
|
baseColor: [1, 1, 1, 1],
|
||||||
|
baseColorTexture: albedo,
|
||||||
metallic: 0.75,
|
metallic: 0.75,
|
||||||
roughness: 0.18,
|
roughness: 0.18,
|
||||||
});
|
});
|
||||||
@@ -151,6 +164,8 @@ const mesh = new Mesh(scene, {
|
|||||||
material: paint,
|
material: paint,
|
||||||
vertexData: {
|
vertexData: {
|
||||||
positions: [-0.7, -0.6, 0, 0.7, -0.6, 0, 0, 0.72, 0],
|
positions: [-0.7, -0.6, 0, 0.7, -0.6, 0, 0, 0.72, 0],
|
||||||
|
normals: [0, 0, 1, 0, 0, 1, 0, 0, 1],
|
||||||
|
uvs: [0, 0, 2, 0, 1, 2],
|
||||||
indices: [0, 1, 2],
|
indices: [0, 1, 2],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -167,6 +182,7 @@ return {
|
|||||||
scene,
|
scene,
|
||||||
mesh,
|
mesh,
|
||||||
paint,
|
paint,
|
||||||
|
albedo,
|
||||||
dispose() {
|
dispose() {
|
||||||
canvas.removeEventListener("pointermove", move);
|
canvas.removeEventListener("pointermove", move);
|
||||||
scene.dispose();
|
scene.dispose();
|
||||||
@@ -256,7 +272,7 @@ return { scene, mesh, exposure, grade, fxaa, dispose: () => scene.dispose() };`;
|
|||||||
|
|
||||||
const importing = `import { AmbientLight, ArcRotateCamera, Picking, Scene, importGltf } from "@yawn/handles";
|
const importing = `import { AmbientLight, ArcRotateCamera, Picking, Scene, importGltf } from "@yawn/handles";
|
||||||
|
|
||||||
const scene = new Scene(canvas, { hdr: true, fps: 1, arenaBytes: 384 * 1024 * 1024 });
|
const scene = new Scene(canvas, { hdr: true, arenaBytes: 384 * 1024 * 1024 });
|
||||||
await scene.ready;
|
await scene.ready;
|
||||||
await scene.core.pause();
|
await scene.core.pause();
|
||||||
log("Importing /models/sponza.glb in the importer worker…");
|
log("Importing /models/sponza.glb in the importer worker…");
|
||||||
@@ -307,9 +323,12 @@ for (const mesh of meshes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const camera = new ArcRotateCamera(scene, {
|
const camera = new ArcRotateCamera(scene, {
|
||||||
alpha: 0.7,
|
targetPosition: [-0.3, 0, 0],
|
||||||
beta: 1.1,
|
alpha: Math.PI / 2,
|
||||||
radius: 2.7,
|
beta: Math.PI / 2,
|
||||||
|
radius: 0.3,
|
||||||
|
near: 0.01,
|
||||||
|
far: 10,
|
||||||
aspect: canvas.width / canvas.height,
|
aspect: canvas.width / canvas.height,
|
||||||
controls: { element: canvas, pointer: true },
|
controls: { element: canvas, pointer: true },
|
||||||
});
|
});
|
||||||
|
|||||||
+16
-1
@@ -39,10 +39,25 @@ const id = await core.allocateObject("application.values");
|
|||||||
values.row(id).set([1, 2, 3, 4]);
|
values.row(id).set([1, 2, 3, 4]);
|
||||||
```
|
```
|
||||||
|
|
||||||
Graph frontends serialize plain data to `(yawn-graph 1 ...)`. Named `after` edges preserve DAG fan-out; Rust sorts passes, detects cycles, culls unused declarations, plans compatible transient lifetimes, and allocates the active loadout.
|
Graph frontends serialize plain data to `(yawn-graph 1 ...)`. Named `after` edges preserve DAG fan-out; Rust sorts passes, detects cycles, culls unused declarations, aliases compatible transient lifetimes, merges compatible render passes into bundles, and allocates the active loadout. Unchanged persistent textures survive loadout rebuilds.
|
||||||
|
|
||||||
The `Scene` addon is one such frontend. It is replaceable and has no privileged core API.
|
The `Scene` addon is one such frontend. It is replaceable and has no privileged core API.
|
||||||
|
|
||||||
|
## Profile physical GPU passes
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const stop = core.onProfile((frame) => {
|
||||||
|
console.table(frame.passes); // name + GPU milliseconds
|
||||||
|
});
|
||||||
|
const supported = await core.setProfiler(true);
|
||||||
|
|
||||||
|
// Later:
|
||||||
|
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.
|
||||||
|
|
||||||
<Playground example="core" />
|
<Playground example="core" />
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|||||||
@@ -22,17 +22,15 @@ mesh.material = paint;
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
const albedo = new Texture(scene, {
|
const albedo = new Texture(scene, {
|
||||||
source: "/textures/paint.ktx2",
|
source: "/textures/paint.png",
|
||||||
size: [2048, 2048, 1],
|
|
||||||
format: "rgba8unorm-srgb",
|
format: "rgba8unorm-srgb",
|
||||||
usage: ["sampled", "copyDst"],
|
|
||||||
});
|
});
|
||||||
await albedo.ready;
|
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 source pointer stays on the handle for an importer or application uploader; core never owns image-loading policy.
|
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.
|
||||||
|
|
||||||
## Custom WGSL
|
## Custom WGSL
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
# Minimal playground
|
---
|
||||||
|
layout: false
|
||||||
|
---
|
||||||
|
|
||||||
This page creates an HDR `Scene`, one PBR material, and one indexed mesh. Move the pointer over the canvas: the handler writes the `sceneAccent` shared row directly without messaging the renderer.
|
<Playground fullscreen />
|
||||||
|
|
||||||
<Playground />
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Playground from "./.vitepress/Playground.vue";
|
import Playground from "./.vitepress/Playground.vue";
|
||||||
|
|||||||
Generated
+203
@@ -12,6 +12,9 @@
|
|||||||
"addons/*"
|
"addons/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@codemirror/lang-javascript": "^6.2.5",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
"vitepress": "^1.6.4"
|
"vitepress": "^1.6.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -334,6 +337,123 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@codemirror/autocomplete": {
|
||||||
|
"version": "6.20.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
|
||||||
|
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/commands": {
|
||||||
|
"version": "6.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.0.tgz",
|
||||||
|
"integrity": "sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.7.0",
|
||||||
|
"@codemirror/view": "^6.27.0",
|
||||||
|
"@lezer/common": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lang-javascript": {
|
||||||
|
"version": "6.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
|
||||||
|
"integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.6.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.17.0",
|
||||||
|
"@lezer/common": "^1.0.0",
|
||||||
|
"@lezer/javascript": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/language": {
|
||||||
|
"version": "6.12.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
||||||
|
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.23.0",
|
||||||
|
"@lezer/common": "^1.5.0",
|
||||||
|
"@lezer/highlight": "^1.0.0",
|
||||||
|
"@lezer/lr": "^1.0.0",
|
||||||
|
"style-mod": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/lint": {
|
||||||
|
"version": "6.9.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
|
||||||
|
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.42.0",
|
||||||
|
"crelt": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/search": {
|
||||||
|
"version": "6.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
|
||||||
|
"integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.37.0",
|
||||||
|
"crelt": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/state": {
|
||||||
|
"version": "6.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
|
||||||
|
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/theme-one-dark": {
|
||||||
|
"version": "6.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
|
||||||
|
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
|
"@lezer/highlight": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@codemirror/view": {
|
||||||
|
"version": "6.43.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.9.tgz",
|
||||||
|
"integrity": "sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/state": "^6.7.0",
|
||||||
|
"crelt": "^1.0.6",
|
||||||
|
"style-mod": "^4.1.0",
|
||||||
|
"w3c-keyname": "^2.2.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@docsearch/css": {
|
"node_modules/@docsearch/css": {
|
||||||
"version": "3.8.2",
|
"version": "3.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz",
|
||||||
@@ -783,6 +903,52 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@lezer/common": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/highlight": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/javascript": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.2.0",
|
||||||
|
"@lezer/highlight": "^1.1.3",
|
||||||
|
"@lezer/lr": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@lezer/lr": {
|
||||||
|
"version": "1.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
||||||
|
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@lezer/common": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@marijn/find-cluster-break": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.46.2",
|
"version": "4.46.2",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz",
|
||||||
@@ -1295,6 +1461,22 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codemirror": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.0.0",
|
||||||
|
"@codemirror/commands": "^6.0.0",
|
||||||
|
"@codemirror/language": "^6.0.0",
|
||||||
|
"@codemirror/lint": "^6.0.0",
|
||||||
|
"@codemirror/search": "^6.0.0",
|
||||||
|
"@codemirror/state": "^6.0.0",
|
||||||
|
"@codemirror/view": "^6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/comma-separated-tokens": {
|
"node_modules/comma-separated-tokens": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||||
@@ -1322,6 +1504,13 @@
|
|||||||
"url": "https://github.com/sponsors/mesqueeb"
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crelt": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -1870,6 +2059,13 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/style-mod": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/superjson": {
|
"node_modules/superjson": {
|
||||||
"version": "2.2.6",
|
"version": "2.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
|
||||||
@@ -2207,6 +2403,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/zwitch": {
|
"node_modules/zwitch": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
"start": "wasm-pack build core --target web --out-dir pkg --release && vitepress dev docs --host 0.0.0.0 --port ${PORT:-8080}"
|
"start": "wasm-pack build core --target web --out-dir pkg --release && vitepress dev docs --host 0.0.0.0 --port ${PORT:-8080}"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@codemirror/lang-javascript": "^6.2.5",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
"vitepress": "^1.6.4"
|
"vitepress": "^1.6.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user