@aethervtt/shard-render 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets.d.ts +3 -0
- package/dist/assets.js +43 -2
- package/dist/atmosphere-nodes.d.ts +9 -0
- package/dist/atmosphere-nodes.js +57 -18
- package/dist/atmosphere-plugin.js +8 -2
- package/dist/baseline/atmosphere.d.ts +16 -0
- package/dist/baseline/atmosphere.js +301 -0
- package/dist/baseline/data-texture.d.ts +36 -0
- package/dist/baseline/data-texture.js +181 -0
- package/dist/baseline/environment.d.ts +14 -0
- package/dist/baseline/environment.js +368 -0
- package/dist/baseline/exposure.d.ts +25 -0
- package/dist/baseline/exposure.js +120 -0
- package/dist/baseline/lights.d.ts +33 -0
- package/dist/baseline/lights.js +133 -0
- package/dist/baseline/textures.d.ts +97 -0
- package/dist/baseline/textures.js +203 -0
- package/dist/camera.d.ts +2 -0
- package/dist/camera.js +4 -0
- package/dist/culling-shaders.js +21 -2
- package/dist/culling.d.ts +11 -2
- package/dist/culling.js +44 -18
- package/dist/cutaway-plugin.d.ts +48 -0
- package/dist/cutaway-plugin.js +364 -0
- package/dist/cutaway.d.ts +27 -0
- package/dist/cutaway.js +49 -0
- package/dist/data-store.d.ts +69 -0
- package/dist/data-store.js +105 -0
- package/dist/debug-shaders.js +17 -3
- package/dist/debug-views.js +4 -2
- package/dist/deferred-shaders.js +2 -0
- package/dist/deferred.js +24 -3
- package/dist/deform.d.ts +5 -4
- package/dist/deform.js +4 -9
- package/dist/describe-plugin.d.ts +4 -2
- package/dist/describe-plugin.js +75 -3
- package/dist/display-nodes.js +15 -4
- package/dist/dynamic-resolution.d.ts +2 -0
- package/dist/dynamic-resolution.js +6 -1
- package/dist/environment-plugin.js +17 -2
- package/dist/environment-state.d.ts +7 -3
- package/dist/environment-state.js +20 -9
- package/dist/environment.js +7 -1
- package/dist/features.d.ts +49 -0
- package/dist/features.js +106 -0
- package/dist/forward.d.ts +38 -2
- package/dist/forward.js +582 -184
- package/dist/fxaa.js +12 -2
- package/dist/gizmos-plugin.js +7 -0
- package/dist/gizmos.d.ts +3 -3
- package/dist/gizmos.js +11 -13
- package/dist/gpu-assets.d.ts +58 -4
- package/dist/gpu-assets.js +191 -8
- package/dist/graph.d.ts +11 -0
- package/dist/graph.js +30 -5
- package/dist/health.d.ts +58 -0
- package/dist/health.js +112 -0
- package/dist/index.d.ts +30 -11
- package/dist/index.js +286 -9
- package/dist/instances.d.ts +119 -8
- package/dist/instances.js +387 -78
- package/dist/interior-plugin.d.ts +121 -0
- package/dist/interior-plugin.js +465 -0
- package/dist/interior.d.ts +24 -0
- package/dist/interior.js +13 -0
- package/dist/layers.d.ts +35 -0
- package/dist/layers.js +49 -0
- package/dist/lens-plugin.d.ts +43 -0
- package/dist/lens-plugin.js +304 -0
- package/dist/lens.d.ts +41 -0
- package/dist/lens.js +99 -0
- package/dist/lights.d.ts +50 -5
- package/dist/lights.js +101 -22
- package/dist/material-pipelines.d.ts +37 -4
- package/dist/material-pipelines.js +102 -5
- package/dist/materials.d.ts +23 -0
- package/dist/materials.js +44 -1
- package/dist/outline-plugin.d.ts +48 -0
- package/dist/outline-plugin.js +529 -0
- package/dist/outline.d.ts +15 -0
- package/dist/outline.js +42 -0
- package/dist/overlays.js +1 -0
- package/dist/picking.d.ts +17 -0
- package/dist/picking.js +40 -11
- package/dist/pixel-perfect-plugin.js +10 -2
- package/dist/plugin.d.ts +58 -6
- package/dist/plugin.js +191 -24
- package/dist/post-common.d.ts +2 -0
- package/dist/post-common.js +3 -0
- package/dist/post-nodes.d.ts +2 -1
- package/dist/post-nodes.js +111 -19
- package/dist/post-plugin.js +5 -3
- package/dist/post-shaders.js +51 -6
- package/dist/projection.d.ts +24 -0
- package/dist/projection.js +87 -0
- package/dist/readback.d.ts +1 -0
- package/dist/readback.js +1 -0
- package/dist/render-scale.js +1 -0
- package/dist/screen-effects.d.ts +97 -0
- package/dist/screen-effects.js +192 -0
- package/dist/shaders.d.ts +13 -0
- package/dist/shaders.js +131 -29
- package/dist/shadow-catcher.d.ts +9 -0
- package/dist/shadow-catcher.js +133 -0
- package/dist/shadows.d.ts +25 -1
- package/dist/shadows.js +97 -19
- package/dist/standard-fields.d.ts +1 -0
- package/dist/standard-fields.js +5 -0
- package/dist/standard.js +12 -0
- package/dist/stats.d.ts +45 -1
- package/dist/stats.js +91 -3
- package/dist/surface-model.d.ts +57 -0
- package/dist/surface-model.js +289 -0
- package/dist/surface-shaders.d.ts +3 -0
- package/dist/surface-shaders.js +160 -0
- package/dist/surface.d.ts +40 -0
- package/dist/surface.js +86 -0
- package/dist/target.d.ts +1 -13
- package/dist/target.js +1 -26
- package/dist/testing.d.ts +10 -0
- package/dist/testing.js +33 -1
- package/dist/tier.d.ts +13 -0
- package/dist/tier.js +14 -0
- package/dist/timer.d.ts +2 -0
- package/dist/timer.js +7 -1
- package/dist/variants.d.ts +37 -0
- package/dist/variants.js +104 -0
- package/dist/view-visibility-plugin.d.ts +61 -0
- package/dist/view-visibility-plugin.js +267 -0
- package/dist/view-visibility.d.ts +28 -0
- package/dist/view-visibility.js +46 -0
- package/dist/view.d.ts +29 -1
- package/dist/view.js +57 -34
- package/package.json +15 -11
package/dist/outline.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineComponent, defineResource, onAdd, t } from "@aethervtt/shard-core";
|
|
2
|
+
import { warnFeatureMissing } from "./features.js";
|
|
3
|
+
const OUTLINE_OCCLUSION = ["hide", "show", "dim"];
|
|
4
|
+
const Outline = defineComponent(
|
|
5
|
+
"render/Outline",
|
|
6
|
+
{
|
|
7
|
+
color: t.color({
|
|
8
|
+
default: [1, 0.78, 0.2, 1],
|
|
9
|
+
description: "Outline color (linear), with alpha."
|
|
10
|
+
}),
|
|
11
|
+
width: t.f32({
|
|
12
|
+
default: 2,
|
|
13
|
+
min: 0,
|
|
14
|
+
max: 32,
|
|
15
|
+
unit: "px",
|
|
16
|
+
description: "Width in CSS pixels, outside the silhouette, at any zoom and display density."
|
|
17
|
+
}),
|
|
18
|
+
occluded: t.enum(OUTLINE_OCCLUSION, {
|
|
19
|
+
description: "Where something hides the outlined object: hide the outline there, show it anyway, or dim it."
|
|
20
|
+
})
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
description: "Outlines a renderable, or every renderable under this entity. Selection and hover are two Outlines with different colors; the host sets and clears them."
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
const OutlinePath = defineResource("render/OutlinePath", {
|
|
27
|
+
description: "Present when the outline pass is installed."
|
|
28
|
+
});
|
|
29
|
+
const MAX_OUTLINE_STYLES = 16;
|
|
30
|
+
function observeOutlinesWithoutPass(world) {
|
|
31
|
+
world.observe(onAdd(Outline), ({ world: w }) => {
|
|
32
|
+
if (!w.hasResource(OutlinePath))
|
|
33
|
+
warnFeatureMissing(w, "An entity has Outline", "outlinePlugin", "it draws no outline");
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
MAX_OUTLINE_STYLES,
|
|
38
|
+
OUTLINE_OCCLUSION,
|
|
39
|
+
Outline,
|
|
40
|
+
OutlinePath,
|
|
41
|
+
observeOutlinesWithoutPass
|
|
42
|
+
};
|
package/dist/overlays.js
CHANGED
|
@@ -34,6 +34,7 @@ function scenePath(world, entity) {
|
|
|
34
34
|
return v?.path || void 0;
|
|
35
35
|
}
|
|
36
36
|
const DebugOverlays = defineResource("render/DebugOverlays", {
|
|
37
|
+
hostWritable: true,
|
|
37
38
|
description: "Built-in debug drawings, each drawn through Gizmos: bounds, light volumes, camera frustums, shadow cascades, normals, transform axes, and entity labels.",
|
|
38
39
|
init: () => ({
|
|
39
40
|
bounds: false,
|
package/dist/picking.d.ts
CHANGED
|
@@ -12,7 +12,22 @@ export interface PickHit {
|
|
|
12
12
|
normal: [number, number, number];
|
|
13
13
|
/** Meters from the camera (from the ray origin, for raycasts). */
|
|
14
14
|
distance: number;
|
|
15
|
+
/**
|
|
16
|
+
* What within the entity was hit, when something knows (0059): a tilemap gives
|
|
17
|
+
* `{ kind: 'tile', layer, x, y, tile }`, the tile named as its palette names it.
|
|
18
|
+
*/
|
|
19
|
+
detail?: PickDetail;
|
|
15
20
|
}
|
|
21
|
+
/** Detail of a pick within an entity: `kind` says what the rest means. */
|
|
22
|
+
export interface PickDetail {
|
|
23
|
+
kind: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Turns a hit into detail about what within the entity it hit (a tile of a tilemap), or
|
|
28
|
+
* undefined when the hit isn't its kind. Runs for GPU picks and raycasts alike.
|
|
29
|
+
*/
|
|
30
|
+
export type PickDetailer = (world: World, hit: PickHit) => PickDetail | undefined;
|
|
16
31
|
interface PickRequest {
|
|
17
32
|
x: number;
|
|
18
33
|
y: number;
|
|
@@ -35,6 +50,8 @@ export interface PickingState {
|
|
|
35
50
|
pending: Map<string, PickRequest[]>;
|
|
36
51
|
drawers: Map<string, PickDrawer>;
|
|
37
52
|
blockers: Map<string, PickBlocker>;
|
|
53
|
+
/** Detail for hits, by who adds it (tilemaps). */
|
|
54
|
+
detailers: Map<string, PickDetailer>;
|
|
38
55
|
/** Views whose pick pass skipped a draw (a pipeline still compiling): their picks wait a frame. */
|
|
39
56
|
incomplete: Set<string>;
|
|
40
57
|
}
|
package/dist/picking.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { defineResource, mat4, ShardError } from "@aethervtt/shard-core";
|
|
2
2
|
import { GlobalTransform } from "@aethervtt/shard-transform";
|
|
3
|
-
import { Meshes } from "./assets.js";
|
|
4
|
-
import {
|
|
3
|
+
import { Materials, Meshes } from "./assets.js";
|
|
4
|
+
import { addRenderFeatures } from "./features.js";
|
|
5
|
+
import { drawMaterials, ForwardStateResource, PASS_PICK, PASS_PICK_GROUND } from "./forward.js";
|
|
5
6
|
import { RenderPhase } from "./graph.js";
|
|
6
|
-
import { Lod, Mesh3d } from "./instances.js";
|
|
7
|
+
import { Lod, Mesh3d, MeshMaterial } from "./instances.js";
|
|
7
8
|
import { DebugOverlays } from "./overlays.js";
|
|
8
9
|
import { Graph, Views } from "./plugin.js";
|
|
9
10
|
import { cameraOf } from "./view.js";
|
|
@@ -14,6 +15,7 @@ const Picking = defineResource("render/Picking", {
|
|
|
14
15
|
pending: /* @__PURE__ */ new Map(),
|
|
15
16
|
drawers: /* @__PURE__ */ new Map(),
|
|
16
17
|
blockers: /* @__PURE__ */ new Map(),
|
|
18
|
+
detailers: /* @__PURE__ */ new Map(),
|
|
17
19
|
incomplete: /* @__PURE__ */ new Set()
|
|
18
20
|
})
|
|
19
21
|
});
|
|
@@ -69,6 +71,8 @@ function pickNode(world) {
|
|
|
69
71
|
drawMaterials(ctx, state, pv, cam, cam.draws, PASS_PICK);
|
|
70
72
|
if (cam.deferred) drawMaterials(ctx, state, pv, cam, cam.forwardOnly, PASS_PICK);
|
|
71
73
|
drawMaterials(ctx, state, pv, cam, cam.transparent, PASS_PICK);
|
|
74
|
+
drawMaterials(ctx, state, pv, cam, cam.ground, PASS_PICK_GROUND);
|
|
75
|
+
drawMaterials(ctx, state, pv, cam, cam.overlay, PASS_PICK_GROUND);
|
|
72
76
|
for (const draw of picking.drawers.values()) draw(ctx, cam);
|
|
73
77
|
if (ctx.gpu.pipelines.skipped > skipped) picking.incomplete.add(ctx.view.name);
|
|
74
78
|
}
|
|
@@ -147,13 +151,15 @@ function pickReadbackNode(world) {
|
|
|
147
151
|
const dx = position[0] - eye[0];
|
|
148
152
|
const dy = position[1] - eye[1];
|
|
149
153
|
const dz = position[2] - eye[2];
|
|
150
|
-
r.resolve(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
r.resolve(
|
|
155
|
+
withDetail(w, {
|
|
156
|
+
entity,
|
|
157
|
+
path: w.isAlive(entity) ? name(w, entity) : void 0,
|
|
158
|
+
position,
|
|
159
|
+
normal: [n[0], n[1], n[2]],
|
|
160
|
+
distance: Math.sqrt(dx * dx + dy * dy + dz * dz)
|
|
161
|
+
})
|
|
162
|
+
);
|
|
157
163
|
});
|
|
158
164
|
buffer.unmap();
|
|
159
165
|
buffer.destroy();
|
|
@@ -173,6 +179,12 @@ function addPickNodes(world) {
|
|
|
173
179
|
graph.declare({ name: "pick-id", format: "r32uint", usage });
|
|
174
180
|
graph.declare({ name: "pick-normal", format: "rgba32float", usage });
|
|
175
181
|
graph.declare({ name: "pick-depth", format: "depth32float" });
|
|
182
|
+
addRenderFeatures(world, {
|
|
183
|
+
name: "render/picking",
|
|
184
|
+
description: "Entity picking from id and normal targets, read back per request.",
|
|
185
|
+
nodes: ["picking", "picking/readback"],
|
|
186
|
+
baseline: { strategy: "The same targets, read back with copies" }
|
|
187
|
+
});
|
|
176
188
|
graph.addNode("picking", pickNode(world));
|
|
177
189
|
graph.addNode("picking/readback", pickReadbackNode(world));
|
|
178
190
|
}
|
|
@@ -235,11 +247,14 @@ function worldBox(bb, m, out) {
|
|
|
235
247
|
}
|
|
236
248
|
function buildBvh(world, tick, tables) {
|
|
237
249
|
const items = [];
|
|
250
|
+
const materials = world.tryResource(Materials);
|
|
238
251
|
for (const table of world.query({ with: [Mesh3d, GlobalTransform, ComputedVisibility] }).tables) {
|
|
239
252
|
const g = table.column(GlobalTransform, "matrix");
|
|
240
253
|
const vis = table.column(ComputedVisibility, "visible");
|
|
254
|
+
const refs = table.has(MeshMaterial) ? table.column(MeshMaterial, "material") : void 0;
|
|
241
255
|
for (let i = 0; i < table.count; i++) {
|
|
242
256
|
if (!vis[i]) continue;
|
|
257
|
+
if (refs && materials?.get(refs[i])?.type.pickable === false) continue;
|
|
243
258
|
const entity = table.entities[i];
|
|
244
259
|
const mesh = meshOf(world, entity);
|
|
245
260
|
if (!mesh) continue;
|
|
@@ -442,7 +457,21 @@ function raycast(world, origin, direction, options = {}) {
|
|
|
442
457
|
}
|
|
443
458
|
}
|
|
444
459
|
hits.sort((a, b) => a.distance - b.distance);
|
|
445
|
-
|
|
460
|
+
const out = options.all ? hits : hits.slice(0, 1);
|
|
461
|
+
for (const hit of out) withDetail(world, hit);
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
function withDetail(world, hit) {
|
|
465
|
+
const detailers = world.tryResource(Picking)?.detailers;
|
|
466
|
+
if (!detailers || detailers.size === 0 || !world.isAlive(hit.entity)) return hit;
|
|
467
|
+
for (const detailer of detailers.values()) {
|
|
468
|
+
const detail = detailer(world, hit);
|
|
469
|
+
if (detail) {
|
|
470
|
+
hit.detail = detail;
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return hit;
|
|
446
475
|
}
|
|
447
476
|
export {
|
|
448
477
|
BvhResource,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { definePlugin } from "@aethervtt/shard-runtime";
|
|
2
|
+
import { addRenderFeatures } from "./features.js";
|
|
2
3
|
import { RenderPhase } from "./graph.js";
|
|
3
4
|
import { PixelPerfectPath } from "./pixel-perfect.js";
|
|
4
5
|
import { Graph, Shaders } from "./plugin.js";
|
|
@@ -6,7 +7,7 @@ import { registerShaders } from "./shaders.js";
|
|
|
6
7
|
const PIXEL_PERFECT_SHADERS = {
|
|
7
8
|
"shard::post::pixel_upscale": `
|
|
8
9
|
struct Upscale {
|
|
9
|
-
/** offset (xy), scale, 0. */
|
|
10
|
+
/** offset (xy), scale, the bars' alpha (0 for a camera clearing to alpha, 0052). */
|
|
10
11
|
params: vec4f,
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -17,7 +18,7 @@ struct Upscale {
|
|
|
17
18
|
@fragment fn fs(@builtin(position) frag: vec4f) -> @location(0) vec4f {
|
|
18
19
|
let p = (frag.xy - upscale.params.xy) / upscale.params.z;
|
|
19
20
|
let size = vec2f(textureDimensions(source));
|
|
20
|
-
if (any(p < vec2f(0.0)) || any(p >= size)) { return vec4f(0.0, 0.0, 0.0,
|
|
21
|
+
if (any(p < vec2f(0.0)) || any(p >= size)) { return vec4f(0.0, 0.0, 0.0, upscale.params.w); }
|
|
21
22
|
return textureLoad(source, vec2i(floor(p)), 0);
|
|
22
23
|
}`
|
|
23
24
|
};
|
|
@@ -92,6 +93,7 @@ function pixelUpscaleNode() {
|
|
|
92
93
|
params[0] = layout.offsetX;
|
|
93
94
|
params[1] = layout.offsetY;
|
|
94
95
|
params[2] = layout.scale;
|
|
96
|
+
params[3] = ctx.view.data.alphaOutput === true ? 0 : 1;
|
|
95
97
|
gpu.device.queue.writeBuffer(buffer, 0, params);
|
|
96
98
|
const source = layout.target.texture();
|
|
97
99
|
let cached = groups.get(ctx.view.name);
|
|
@@ -125,6 +127,12 @@ const pixelPerfectPlugin = definePlugin({
|
|
|
125
127
|
},
|
|
126
128
|
ready(app) {
|
|
127
129
|
registerShaders(app.world.resource(Shaders), PIXEL_PERFECT_SHADERS);
|
|
130
|
+
addRenderFeatures(app.world, {
|
|
131
|
+
name: "render/pixel-perfect",
|
|
132
|
+
description: "Pixel-art cameras: low-resolution render, integer upscale.",
|
|
133
|
+
nodes: ["pixel-upscale"],
|
|
134
|
+
baseline: { strategy: "The same fragment pass" }
|
|
135
|
+
});
|
|
128
136
|
app.world.resource(Graph).addNode("pixel-upscale", pixelUpscaleNode());
|
|
129
137
|
}
|
|
130
138
|
});
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type World } from '@aethervtt/shard-core';
|
|
2
|
-
import { type GpuContext } from '@aethervtt/shard-gpu';
|
|
2
|
+
import { type GpuBackendChoice, type GpuContext, type Surface, type SurfaceAlpha, type Webgl2ContextOptions } from '@aethervtt/shard-gpu';
|
|
3
3
|
import { type Plugin } from '@aethervtt/shard-runtime';
|
|
4
4
|
import { type ShaderBake, ShaderLibrary } from '@aethervtt/shard-shader';
|
|
5
5
|
import { type CapturedBuffer, type CapturedImage, RenderGraph, type RenderView } from './graph.js';
|
|
6
|
-
import {
|
|
6
|
+
import type { RenderTarget } from './target.js';
|
|
7
7
|
export declare const Gpu: import("@aethervtt/shard-core").ResourceDef<GpuContext>;
|
|
8
8
|
export declare const Graph: import("@aethervtt/shard-core").ResourceDef<RenderGraph>;
|
|
9
9
|
export declare const Shaders: import("@aethervtt/shard-core").ResourceDef<ShaderLibrary>;
|
|
@@ -29,16 +29,43 @@ export declare const RenderSet: {
|
|
|
29
29
|
readonly Graph: import("@aethervtt/shard-core").SystemSetDef;
|
|
30
30
|
};
|
|
31
31
|
export interface RenderPluginOptions {
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Render into this canvas: the plugin adds it as a surface of `gpu` (or of a device it makes).
|
|
34
|
+
* Omit for headless (views use offscreen targets).
|
|
35
|
+
*/
|
|
33
36
|
canvas?: HTMLCanvasElement | OffscreenCanvas;
|
|
34
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* With `canvas`: 'premultiplied' composites the canvas over the page, where cameras clearing to
|
|
39
|
+
* alpha 0 show what's under it (0052). Default 'opaque'.
|
|
40
|
+
*/
|
|
41
|
+
alpha?: SurfaceAlpha;
|
|
42
|
+
/** Render into a surface already on `gpu` (`gpu.addSurface`). Disposing the app removes it. */
|
|
43
|
+
surface?: Surface;
|
|
44
|
+
/**
|
|
45
|
+
* An existing GPU context (e.g. Dawn in Node, or one shared with other apps). Otherwise one is
|
|
46
|
+
* created in `ready` and destroyed with the app.
|
|
47
|
+
*/
|
|
35
48
|
gpu?: GpuContext;
|
|
49
|
+
/** The name this app's GPU objects count under in `gpu.stats(owner)`. Default `render:<n>`. */
|
|
50
|
+
owner?: string;
|
|
36
51
|
/**
|
|
37
52
|
* Headless stand-in for the window: cameras without a target render here. Used by the CLI for
|
|
38
|
-
* screenshots. Ignored when there's a canvas.
|
|
53
|
+
* screenshots. Ignored when there's a canvas or surface.
|
|
39
54
|
*/
|
|
40
55
|
target?: RenderTarget;
|
|
41
56
|
features?: GPUFeatureName[];
|
|
57
|
+
/**
|
|
58
|
+
* The graphics API, when the plugin makes the device (0064). 'auto' (default): WebGPU where it
|
|
59
|
+
* works, else WebGL2 on the baseline tier. 'webgpu' or 'webgl2' insist on one.
|
|
60
|
+
*/
|
|
61
|
+
backend?: GpuBackendChoice;
|
|
62
|
+
/** WebGL2 settings, for tests and tools (the 'minimum' profile, GL error checks). */
|
|
63
|
+
webgl2?: Webgl2ContextOptions;
|
|
64
|
+
/**
|
|
65
|
+
* 'baseline' runs the baseline tier even where the full one would (on WebGPU, a
|
|
66
|
+
* compatibility-mode device), when the plugin makes the device.
|
|
67
|
+
*/
|
|
68
|
+
tier?: 'baseline';
|
|
42
69
|
/** An existing shader library to share (e.g. a preview rendering the game's own shaders). */
|
|
43
70
|
shaders?: ShaderLibrary;
|
|
44
71
|
/**
|
|
@@ -57,8 +84,15 @@ export interface RenderPluginOptions {
|
|
|
57
84
|
* so the renderer's description grows with what's installed.
|
|
58
85
|
*/
|
|
59
86
|
export declare const RenderDescribers: import("@aethervtt/shard-core").ResourceDef<Map<string, (world: World) => unknown>>;
|
|
60
|
-
export
|
|
87
|
+
export interface RenderOptionsValue {
|
|
88
|
+
windowView: boolean;
|
|
89
|
+
/** What this app's GPU objects count under in `gpu.stats` (0052). */
|
|
90
|
+
owner: string;
|
|
91
|
+
}
|
|
92
|
+
export declare const RenderOptions: import("@aethervtt/shard-core").ResourceDef<RenderOptionsValue>;
|
|
61
93
|
export declare function renderPlugin(options?: RenderPluginOptions): Plugin;
|
|
94
|
+
/** The name this app's GPU objects count under in `gpu.stats` (0052). */
|
|
95
|
+
export declare function renderOwner(world: World): string;
|
|
62
96
|
/** A screenshot of a view (default: the window) after the next frame renders it. */
|
|
63
97
|
export declare function captureView(world: World, view?: string): Promise<CapturedImage>;
|
|
64
98
|
/**
|
|
@@ -108,4 +142,22 @@ export declare function describeRender(world: World): {
|
|
|
108
142
|
path?: string;
|
|
109
143
|
details?: unknown[];
|
|
110
144
|
}[];
|
|
145
|
+
features: {
|
|
146
|
+
name: string;
|
|
147
|
+
nodes: string[];
|
|
148
|
+
baseline: string;
|
|
149
|
+
}[];
|
|
150
|
+
health: import("./health.js").RenderHealthValue;
|
|
151
|
+
backend: import("@aethervtt/shard-gpu").GpuBackendName;
|
|
152
|
+
tier: import("@aethervtt/shard-gpu").GpuTier;
|
|
153
|
+
capabilities: import("@aethervtt/shard-gpu").GpuCapabilities;
|
|
154
|
+
shaderCache: import("@aethervtt/shard-gpu").ShaderCacheStats | undefined;
|
|
155
|
+
limits: {
|
|
156
|
+
maxTextureDimension2D: number;
|
|
157
|
+
maxColorAttachments: number;
|
|
158
|
+
maxSampledTexturesPerShaderStage: number;
|
|
159
|
+
maxInterStageShaderVariables: number;
|
|
160
|
+
maxUniformBufferBindingSize: number;
|
|
161
|
+
};
|
|
162
|
+
reasons: readonly import("@aethervtt/shard-gpu").GraphicsReason[];
|
|
111
163
|
} & Record<string, unknown>;
|
package/dist/plugin.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AssetServerResource } from "@aethervtt/shard-assets";
|
|
1
2
|
import {
|
|
2
3
|
defineEvent,
|
|
3
4
|
defineResource,
|
|
@@ -6,15 +7,30 @@ import {
|
|
|
6
7
|
Last,
|
|
7
8
|
ShardError
|
|
8
9
|
} from "@aethervtt/shard-core";
|
|
9
|
-
import {
|
|
10
|
-
|
|
10
|
+
import {
|
|
11
|
+
createGpuContext
|
|
12
|
+
} from "@aethervtt/shard-gpu";
|
|
13
|
+
import {
|
|
14
|
+
definePlugin,
|
|
15
|
+
FrameDemand,
|
|
16
|
+
LOADING_DEMAND,
|
|
17
|
+
LogResource
|
|
18
|
+
} from "@aethervtt/shard-runtime";
|
|
11
19
|
import { ShaderLibrary } from "@aethervtt/shard-shader";
|
|
20
|
+
import { loadDataTextures, releaseDataStores } from "./data-store.js";
|
|
21
|
+
import { checkUnsupportedFeatures, describeFeatures, RenderFeatures } from "./features.js";
|
|
12
22
|
import { RenderGraph } from "./graph.js";
|
|
23
|
+
import {
|
|
24
|
+
healthSystem,
|
|
25
|
+
RenderHealth,
|
|
26
|
+
RenderHealthChanged,
|
|
27
|
+
RenderHealthReports,
|
|
28
|
+
raiseHealthIssue
|
|
29
|
+
} from "./health.js";
|
|
13
30
|
import { registerEngineShaders } from "./shaders.js";
|
|
14
31
|
import { GpuMemory, RenderCounters, RenderStats } from "./stats.js";
|
|
15
|
-
import { WindowTarget } from "./target.js";
|
|
16
32
|
const Gpu = defineResource("render/Gpu", {
|
|
17
|
-
description: "The GPU device,
|
|
33
|
+
description: "The GPU device, its surfaces (canvases), and caches. Apps can share one."
|
|
18
34
|
});
|
|
19
35
|
const Graph = defineResource("render/Graph", {
|
|
20
36
|
description: "Render graph nodes, run for every view each frame."
|
|
@@ -26,7 +42,7 @@ const Views = defineResource("render/Views", {
|
|
|
26
42
|
description: "Views to render this frame. Rebuilt every frame by extract systems."
|
|
27
43
|
});
|
|
28
44
|
const Window = defineResource("render/Window", {
|
|
29
|
-
description: "Where cameras without a target render: the canvas
|
|
45
|
+
description: "Where cameras without a target render: the app's surface (canvas), or an offscreen target when headless."
|
|
30
46
|
});
|
|
31
47
|
const GpuDeviceLost = defineEvent(
|
|
32
48
|
"render/GpuDeviceLost",
|
|
@@ -51,12 +67,37 @@ const begin = defineSystem({
|
|
|
51
67
|
world.resource(Gpu).pipelines.skipped = 0;
|
|
52
68
|
}
|
|
53
69
|
});
|
|
70
|
+
const markMissing = defineSystem({
|
|
71
|
+
name: "render/mark-missing",
|
|
72
|
+
description: "Puts MissingAsset on entities that reference an asset showing a fallback.",
|
|
73
|
+
run: (_, world) => world.tryResource(AssetServerResource)?.markMissing()
|
|
74
|
+
});
|
|
75
|
+
const checkUnsupported = defineSystem({
|
|
76
|
+
name: "render/check-unsupported",
|
|
77
|
+
description: "On the baseline tier: reports render features the scene uses that it can't run (render/feature-unsupported), and WebGL2 shaders translated at load (render/shader-cache-miss).",
|
|
78
|
+
setup: () => ({ misses: 0 }),
|
|
79
|
+
run: (state, world) => {
|
|
80
|
+
const gpu = world.tryResource(Gpu);
|
|
81
|
+
if (gpu?.tier !== "baseline") return;
|
|
82
|
+
checkUnsupportedFeatures(world);
|
|
83
|
+
const cache = gpu.shaderCache();
|
|
84
|
+
if (!cache || cache.misses.length === state.misses) return;
|
|
85
|
+
state.misses = cache.misses.length;
|
|
86
|
+
let ms = 0;
|
|
87
|
+
for (const m of cache.misses) ms += m.ms;
|
|
88
|
+
const load = cache.nagaLoadMs === void 0 ? "" : `, ${cache.nagaLoadMs.toFixed(0)} ms to load it`;
|
|
89
|
+
raiseHealthIssue(world, {
|
|
90
|
+
code: "render/shader-cache-miss",
|
|
91
|
+
severity: "info",
|
|
92
|
+
message: `${state.misses} shader stage${state.misses === 1 ? "" : "s"} translated to GLSL at load (${ms.toFixed(0)} ms in naga${load}): \`shard shaders bake\` ships them translated`
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
54
96
|
const execute = defineSystem({
|
|
55
97
|
name: "render/execute-graph",
|
|
56
98
|
description: "Runs the render graph for every view and submits.",
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (recovering.value) return;
|
|
99
|
+
run: (_, world) => {
|
|
100
|
+
if (world.resource(Gpu).status !== "ok") return;
|
|
60
101
|
const views = world.resource(Views).list;
|
|
61
102
|
const window = world.tryResource(Window);
|
|
62
103
|
const options = world.resource(RenderOptions);
|
|
@@ -65,13 +106,35 @@ const execute = defineSystem({
|
|
|
65
106
|
}
|
|
66
107
|
if (views.length === 0) return;
|
|
67
108
|
world.resource(Graph).execute(world, views);
|
|
109
|
+
world.tryResource(RenderStats)?.endFrame(world.resource(Gpu).uploads(options.owner));
|
|
110
|
+
const gpu = world.resource(Gpu);
|
|
111
|
+
anyPending = gpu.pipelines.skipped > 0 || gpu.pipelines.pending > 0;
|
|
112
|
+
if (!anyPending) world.tryResource(RenderStats)?.forEach(notePending);
|
|
113
|
+
world.tryResource(FrameDemand)?.set(LOADING_DEMAND, anyPending);
|
|
68
114
|
}
|
|
69
115
|
});
|
|
116
|
+
let anyPending = false;
|
|
117
|
+
function notePending(stats) {
|
|
118
|
+
if (stats.pending > 0) anyPending = true;
|
|
119
|
+
}
|
|
70
120
|
const RenderDescribers = defineResource(
|
|
71
121
|
"render/Describers",
|
|
72
122
|
{ description: "Sections plugins add to render.describe.", init: () => /* @__PURE__ */ new Map() }
|
|
73
123
|
);
|
|
74
|
-
const RenderOptions = defineResource("render/Options"
|
|
124
|
+
const RenderOptions = defineResource("render/Options", {
|
|
125
|
+
description: "The render plugin's settings: the fallback window view, and the app's GPU owner."
|
|
126
|
+
});
|
|
127
|
+
const renderApps = /* @__PURE__ */ new WeakMap();
|
|
128
|
+
let nextOwner = 1;
|
|
129
|
+
function nextRefresh() {
|
|
130
|
+
return new Promise((resolve) => requestAnimationFrame(resolve));
|
|
131
|
+
}
|
|
132
|
+
function presentOn(state) {
|
|
133
|
+
return () => {
|
|
134
|
+
const done = state.gpu.device.queue.onSubmittedWorkDone();
|
|
135
|
+
return typeof requestAnimationFrame === "function" ? done.then(nextRefresh) : done.then(() => void 0);
|
|
136
|
+
};
|
|
137
|
+
}
|
|
75
138
|
function renderPlugin(options = {}) {
|
|
76
139
|
return definePlugin({
|
|
77
140
|
name: "render",
|
|
@@ -81,6 +144,10 @@ function renderPlugin(options = {}) {
|
|
|
81
144
|
GpuDeviceLost,
|
|
82
145
|
Graph,
|
|
83
146
|
RenderDescribers,
|
|
147
|
+
RenderFeatures,
|
|
148
|
+
RenderHealth,
|
|
149
|
+
RenderHealthChanged,
|
|
150
|
+
RenderHealthReports,
|
|
84
151
|
RenderOptions,
|
|
85
152
|
Shaders,
|
|
86
153
|
Views,
|
|
@@ -88,40 +155,123 @@ function renderPlugin(options = {}) {
|
|
|
88
155
|
],
|
|
89
156
|
dependencies: ["core/time"],
|
|
90
157
|
build(app) {
|
|
91
|
-
|
|
158
|
+
const state = {
|
|
159
|
+
owner: options.owner ?? `render:${nextOwner++}`,
|
|
160
|
+
gpu: void 0,
|
|
161
|
+
ownsDevice: false,
|
|
162
|
+
surface: void 0,
|
|
163
|
+
saved: [],
|
|
164
|
+
unsubscribe: []
|
|
165
|
+
};
|
|
166
|
+
renderApps.set(app, state);
|
|
167
|
+
app.addScope({
|
|
168
|
+
enter() {
|
|
169
|
+
const gpu = state.gpu;
|
|
170
|
+
state.saved.push(gpu?.owner);
|
|
171
|
+
if (gpu) gpu.owner = state.owner;
|
|
172
|
+
},
|
|
173
|
+
exit() {
|
|
174
|
+
const previous = state.saved.pop();
|
|
175
|
+
if (state.gpu && previous !== void 0) state.gpu.owner = previous;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
app.insertResource(Views, { list: [] }).insertResource(RenderOptions, {
|
|
179
|
+
windowView: options.windowView ?? true,
|
|
180
|
+
owner: state.owner
|
|
181
|
+
}).configureSets(
|
|
92
182
|
Last,
|
|
93
183
|
RenderSet.Extract.after(RenderSet.Begin),
|
|
94
184
|
RenderSet.Prepare.after(RenderSet.Extract),
|
|
95
185
|
RenderSet.Queue.after(RenderSet.Prepare),
|
|
96
186
|
RenderSet.Upload.after(RenderSet.Queue),
|
|
97
187
|
RenderSet.Graph.after(RenderSet.Upload)
|
|
98
|
-
).addSystems(
|
|
188
|
+
).addSystems(
|
|
189
|
+
Last,
|
|
190
|
+
begin.inSet(RenderSet.Begin),
|
|
191
|
+
markMissing.inSet(RenderSet.Begin).after(begin),
|
|
192
|
+
checkUnsupported.inSet(RenderSet.Begin).after(begin),
|
|
193
|
+
execute.inSet(RenderSet.Graph),
|
|
194
|
+
healthSystem({ status: () => state.gpu?.status ?? "ok" }).inSet(RenderSet.Graph).after(execute)
|
|
195
|
+
);
|
|
99
196
|
},
|
|
100
197
|
async ready(app) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
198
|
+
const state = renderApps.get(app);
|
|
199
|
+
const given = options.gpu ?? options.surface?.gpu;
|
|
200
|
+
if (options.surface && options.gpu && options.surface.gpu !== options.gpu) {
|
|
201
|
+
throw new ShardError("render/surface-device", "The surface belongs to another GpuContext", {
|
|
202
|
+
hint: "Pass the GpuContext the surface was added to (surface.gpu), or omit gpu."
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const gpu = given ?? await createGpuContext({
|
|
206
|
+
features: options.features,
|
|
207
|
+
backend: options.backend,
|
|
208
|
+
webgl2: options.webgl2,
|
|
209
|
+
tier: options.tier,
|
|
210
|
+
canvas: options.canvas,
|
|
211
|
+
alpha: options.alpha
|
|
212
|
+
});
|
|
213
|
+
state.gpu = gpu;
|
|
214
|
+
state.ownsDevice = !given;
|
|
215
|
+
if (gpu.tier === "baseline") await loadDataTextures();
|
|
216
|
+
gpu.withOwner(state.owner, () => {
|
|
217
|
+
app.insertResource(Gpu, gpu);
|
|
218
|
+
app.insertResource(Graph, new RenderGraph(gpu));
|
|
219
|
+
});
|
|
220
|
+
app.setPresenter(presentOn(state));
|
|
104
221
|
const shaders = options.shaders ?? new ShaderLibrary();
|
|
105
222
|
if (!options.shaders) registerEngineShaders(shaders);
|
|
106
223
|
if (options.shaderBake) shaders.preload(options.shaderBake);
|
|
107
224
|
app.insertResource(Shaders, shaders);
|
|
108
|
-
|
|
109
|
-
|
|
225
|
+
const surface = options.surface ?? (options.canvas ? gpu.surfaces.find((s) => s.canvas === options.canvas) ?? gpu.addSurface(options.canvas, { alpha: options.alpha }) : void 0);
|
|
226
|
+
state.surface = surface;
|
|
227
|
+
if (surface) {
|
|
228
|
+
app.insertResource(Window, surface);
|
|
229
|
+
state.unsubscribe.push(surface.onResize(() => app.disposed || app.requestFrame()));
|
|
230
|
+
} else if (options.target) app.insertResource(Window, options.target);
|
|
110
231
|
const log = app.world.tryResource(LogResource);
|
|
111
|
-
if (log) gpu.onError((error) => log.error(error));
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
232
|
+
if (log) state.unsubscribe.push(gpu.onError((error) => log.error(error)));
|
|
233
|
+
state.unsubscribe.push(
|
|
234
|
+
gpu.onDeviceLost((info) => {
|
|
235
|
+
app.world.send(GpuDeviceLost, info);
|
|
236
|
+
gpu.recreate().then(
|
|
237
|
+
() => app.disposed || app.requestFrame(),
|
|
238
|
+
() => app.disposed || app.requestFrame()
|
|
239
|
+
);
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
beforeDispose(app) {
|
|
244
|
+
const gpu = renderApps.get(app)?.gpu;
|
|
245
|
+
if (gpu?.status === "ok") return gpu.device.queue.onSubmittedWorkDone();
|
|
246
|
+
},
|
|
247
|
+
dispose(app) {
|
|
248
|
+
const state = renderApps.get(app);
|
|
249
|
+
if (!state) return;
|
|
250
|
+
renderApps.delete(app);
|
|
251
|
+
for (const off of state.unsubscribe.splice(0)) off();
|
|
252
|
+
app.world.tryResource(Graph)?.dispose();
|
|
253
|
+
state.surface?.remove();
|
|
254
|
+
const gpu = state.gpu;
|
|
255
|
+
if (!gpu) return;
|
|
256
|
+
releaseDataStores(gpu, state.owner);
|
|
257
|
+
gpu.release(state.owner);
|
|
258
|
+
if (state.ownsDevice) gpu.destroy();
|
|
116
259
|
}
|
|
117
260
|
});
|
|
118
261
|
}
|
|
262
|
+
function renderOwner(world) {
|
|
263
|
+
return world.resource(RenderOptions).owner;
|
|
264
|
+
}
|
|
119
265
|
function captureView(world, view = WINDOW_VIEW) {
|
|
120
|
-
|
|
266
|
+
const shot = world.resource(Graph).capture(view);
|
|
267
|
+
world.wake();
|
|
268
|
+
return shot;
|
|
121
269
|
}
|
|
122
270
|
async function captureBuffer(world, view, buffer) {
|
|
123
271
|
const graph = world.resource(Graph);
|
|
124
|
-
const
|
|
272
|
+
const pending = graph.capture(view, buffer);
|
|
273
|
+
world.wake();
|
|
274
|
+
const result = await pending;
|
|
125
275
|
if (!("format" in result)) {
|
|
126
276
|
const data = new Float32Array(result.data.length);
|
|
127
277
|
for (let i = 0; i < data.length; i++) data[i] = result.data[i] / 255;
|
|
@@ -159,7 +309,23 @@ function describeRender(world) {
|
|
|
159
309
|
stats: Object.fromEntries(world.tryResource(RenderStats) ?? []),
|
|
160
310
|
counters: { ...world.tryResource(RenderCounters) ?? { taaResets: 0, originShifts: 0 } },
|
|
161
311
|
memory: world.tryResource(GpuMemory) ?? { textures: 0, textureBytes: 0 },
|
|
162
|
-
recentErrors: gpu.errors.slice(-5).map((e) => e.toJSON())
|
|
312
|
+
recentErrors: gpu.errors.slice(-5).map((e) => e.toJSON()),
|
|
313
|
+
features: describeFeatures(world),
|
|
314
|
+
health: world.tryResource(RenderHealth) ?? { state: "ok", issues: [] },
|
|
315
|
+
// The API and tier the device runs (0064), what it can do, and why a better option was skipped.
|
|
316
|
+
backend: gpu.backend,
|
|
317
|
+
tier: gpu.tier,
|
|
318
|
+
capabilities: gpu.capabilities,
|
|
319
|
+
// WebGL2: where shader translations came from, and what naga translated this session.
|
|
320
|
+
shaderCache: gpu.shaderCache(),
|
|
321
|
+
limits: {
|
|
322
|
+
maxTextureDimension2D: gpu.device.limits.maxTextureDimension2D,
|
|
323
|
+
maxColorAttachments: gpu.device.limits.maxColorAttachments,
|
|
324
|
+
maxSampledTexturesPerShaderStage: gpu.device.limits.maxSampledTexturesPerShaderStage,
|
|
325
|
+
maxInterStageShaderVariables: gpu.device.limits.maxInterStageShaderVariables,
|
|
326
|
+
maxUniformBufferBindingSize: gpu.device.limits.maxUniformBufferBindingSize
|
|
327
|
+
},
|
|
328
|
+
reasons: gpu.reasons
|
|
163
329
|
};
|
|
164
330
|
const sections = {};
|
|
165
331
|
for (const [name, fn] of world.tryResource(RenderDescribers) ?? []) sections[name] = fn(world);
|
|
@@ -178,5 +344,6 @@ export {
|
|
|
178
344
|
captureBuffer,
|
|
179
345
|
captureView,
|
|
180
346
|
describeRender,
|
|
347
|
+
renderOwner,
|
|
181
348
|
renderPlugin
|
|
182
349
|
};
|
package/dist/post-common.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare const tex: (binding: number, sampleType?: GPUTextureSampleType) =
|
|
|
11
11
|
sampleType: GPUTextureSampleType;
|
|
12
12
|
};
|
|
13
13
|
};
|
|
14
|
+
/** Depth read without comparison, in the fragment stage (a float texture on baseline, 0064). */
|
|
15
|
+
export declare const depthTex: (gpu: GpuContext, binding: number) => GPUBindGroupLayoutEntry;
|
|
14
16
|
export declare const uniform: (binding: number, visibility?: number) => {
|
|
15
17
|
binding: number;
|
|
16
18
|
visibility: number;
|
package/dist/post-common.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GpuBuffer } from "@aethervtt/shard-gpu";
|
|
2
2
|
import { ForwardStateResource } from "./forward.js";
|
|
3
3
|
import { Shaders } from "./plugin.js";
|
|
4
|
+
import { depthReadEntry } from "./tier.js";
|
|
4
5
|
import { cameraOf } from "./view.js";
|
|
5
6
|
const textureIds = /* @__PURE__ */ new WeakMap();
|
|
6
7
|
let nextId = 1;
|
|
@@ -18,6 +19,7 @@ const tex = (binding, sampleType = "float") => ({
|
|
|
18
19
|
visibility: F(),
|
|
19
20
|
texture: { sampleType }
|
|
20
21
|
});
|
|
22
|
+
const depthTex = (gpu, binding) => depthReadEntry(gpu, binding, F());
|
|
21
23
|
const uniform = (binding, visibility = F()) => ({
|
|
22
24
|
binding,
|
|
23
25
|
visibility,
|
|
@@ -139,6 +141,7 @@ export {
|
|
|
139
141
|
HDR,
|
|
140
142
|
PostCache,
|
|
141
143
|
beginPass,
|
|
144
|
+
depthTex,
|
|
142
145
|
forwardView,
|
|
143
146
|
idOf,
|
|
144
147
|
sampler,
|