@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.
Files changed (134) hide show
  1. package/dist/assets.d.ts +3 -0
  2. package/dist/assets.js +43 -2
  3. package/dist/atmosphere-nodes.d.ts +9 -0
  4. package/dist/atmosphere-nodes.js +57 -18
  5. package/dist/atmosphere-plugin.js +8 -2
  6. package/dist/baseline/atmosphere.d.ts +16 -0
  7. package/dist/baseline/atmosphere.js +301 -0
  8. package/dist/baseline/data-texture.d.ts +36 -0
  9. package/dist/baseline/data-texture.js +181 -0
  10. package/dist/baseline/environment.d.ts +14 -0
  11. package/dist/baseline/environment.js +368 -0
  12. package/dist/baseline/exposure.d.ts +25 -0
  13. package/dist/baseline/exposure.js +120 -0
  14. package/dist/baseline/lights.d.ts +33 -0
  15. package/dist/baseline/lights.js +133 -0
  16. package/dist/baseline/textures.d.ts +97 -0
  17. package/dist/baseline/textures.js +203 -0
  18. package/dist/camera.d.ts +2 -0
  19. package/dist/camera.js +4 -0
  20. package/dist/culling-shaders.js +21 -2
  21. package/dist/culling.d.ts +11 -2
  22. package/dist/culling.js +44 -18
  23. package/dist/cutaway-plugin.d.ts +48 -0
  24. package/dist/cutaway-plugin.js +364 -0
  25. package/dist/cutaway.d.ts +27 -0
  26. package/dist/cutaway.js +49 -0
  27. package/dist/data-store.d.ts +69 -0
  28. package/dist/data-store.js +105 -0
  29. package/dist/debug-shaders.js +17 -3
  30. package/dist/debug-views.js +4 -2
  31. package/dist/deferred-shaders.js +2 -0
  32. package/dist/deferred.js +24 -3
  33. package/dist/deform.d.ts +5 -4
  34. package/dist/deform.js +4 -9
  35. package/dist/describe-plugin.d.ts +4 -2
  36. package/dist/describe-plugin.js +75 -3
  37. package/dist/display-nodes.js +15 -4
  38. package/dist/dynamic-resolution.d.ts +2 -0
  39. package/dist/dynamic-resolution.js +6 -1
  40. package/dist/environment-plugin.js +17 -2
  41. package/dist/environment-state.d.ts +7 -3
  42. package/dist/environment-state.js +20 -9
  43. package/dist/environment.js +7 -1
  44. package/dist/features.d.ts +49 -0
  45. package/dist/features.js +106 -0
  46. package/dist/forward.d.ts +38 -2
  47. package/dist/forward.js +582 -184
  48. package/dist/fxaa.js +12 -2
  49. package/dist/gizmos-plugin.js +7 -0
  50. package/dist/gizmos.d.ts +3 -3
  51. package/dist/gizmos.js +11 -13
  52. package/dist/gpu-assets.d.ts +58 -4
  53. package/dist/gpu-assets.js +191 -8
  54. package/dist/graph.d.ts +11 -0
  55. package/dist/graph.js +30 -5
  56. package/dist/health.d.ts +58 -0
  57. package/dist/health.js +112 -0
  58. package/dist/index.d.ts +30 -11
  59. package/dist/index.js +286 -9
  60. package/dist/instances.d.ts +119 -8
  61. package/dist/instances.js +387 -78
  62. package/dist/interior-plugin.d.ts +121 -0
  63. package/dist/interior-plugin.js +465 -0
  64. package/dist/interior.d.ts +24 -0
  65. package/dist/interior.js +13 -0
  66. package/dist/layers.d.ts +35 -0
  67. package/dist/layers.js +49 -0
  68. package/dist/lens-plugin.d.ts +43 -0
  69. package/dist/lens-plugin.js +304 -0
  70. package/dist/lens.d.ts +41 -0
  71. package/dist/lens.js +99 -0
  72. package/dist/lights.d.ts +50 -5
  73. package/dist/lights.js +101 -22
  74. package/dist/material-pipelines.d.ts +37 -4
  75. package/dist/material-pipelines.js +102 -5
  76. package/dist/materials.d.ts +23 -0
  77. package/dist/materials.js +44 -1
  78. package/dist/outline-plugin.d.ts +48 -0
  79. package/dist/outline-plugin.js +529 -0
  80. package/dist/outline.d.ts +15 -0
  81. package/dist/outline.js +42 -0
  82. package/dist/overlays.js +1 -0
  83. package/dist/picking.d.ts +17 -0
  84. package/dist/picking.js +40 -11
  85. package/dist/pixel-perfect-plugin.js +10 -2
  86. package/dist/plugin.d.ts +58 -6
  87. package/dist/plugin.js +191 -24
  88. package/dist/post-common.d.ts +2 -0
  89. package/dist/post-common.js +3 -0
  90. package/dist/post-nodes.d.ts +2 -1
  91. package/dist/post-nodes.js +111 -19
  92. package/dist/post-plugin.js +5 -3
  93. package/dist/post-shaders.js +51 -6
  94. package/dist/projection.d.ts +24 -0
  95. package/dist/projection.js +87 -0
  96. package/dist/readback.d.ts +1 -0
  97. package/dist/readback.js +1 -0
  98. package/dist/render-scale.js +1 -0
  99. package/dist/screen-effects.d.ts +97 -0
  100. package/dist/screen-effects.js +192 -0
  101. package/dist/shaders.d.ts +13 -0
  102. package/dist/shaders.js +131 -29
  103. package/dist/shadow-catcher.d.ts +9 -0
  104. package/dist/shadow-catcher.js +133 -0
  105. package/dist/shadows.d.ts +25 -1
  106. package/dist/shadows.js +97 -19
  107. package/dist/standard-fields.d.ts +1 -0
  108. package/dist/standard-fields.js +5 -0
  109. package/dist/standard.js +12 -0
  110. package/dist/stats.d.ts +45 -1
  111. package/dist/stats.js +91 -3
  112. package/dist/surface-model.d.ts +57 -0
  113. package/dist/surface-model.js +289 -0
  114. package/dist/surface-shaders.d.ts +3 -0
  115. package/dist/surface-shaders.js +160 -0
  116. package/dist/surface.d.ts +40 -0
  117. package/dist/surface.js +86 -0
  118. package/dist/target.d.ts +1 -13
  119. package/dist/target.js +1 -26
  120. package/dist/testing.d.ts +10 -0
  121. package/dist/testing.js +33 -1
  122. package/dist/tier.d.ts +13 -0
  123. package/dist/tier.js +14 -0
  124. package/dist/timer.d.ts +2 -0
  125. package/dist/timer.js +7 -1
  126. package/dist/variants.d.ts +37 -0
  127. package/dist/variants.js +104 -0
  128. package/dist/view-visibility-plugin.d.ts +61 -0
  129. package/dist/view-visibility-plugin.js +267 -0
  130. package/dist/view-visibility.d.ts +28 -0
  131. package/dist/view-visibility.js +46 -0
  132. package/dist/view.d.ts +29 -1
  133. package/dist/view.js +57 -34
  134. package/package.json +15 -11
@@ -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 { drawMaterials, ForwardStateResource, PASS_PICK } from "./forward.js";
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
- entity,
152
- path: w.isAlive(entity) ? name(w, entity) : void 0,
153
- position,
154
- normal: [n[0], n[1], n[2]],
155
- distance: Math.sqrt(dx * dx + dy * dy + dz * dz)
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
- return options.all ? hits : hits.slice(0, 1);
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, 1.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 { type RenderTarget } from './target.js';
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
- /** Render into this canvas. Omit for headless (views use offscreen targets). */
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
- /** An existing GPU context (e.g. Dawn in Node). Otherwise one is created in `ready`. */
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 declare const RenderOptions: import("@aethervtt/shard-core").ResourceDef<Required<Pick<RenderPluginOptions, "windowView">>>;
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 { createGpuContext } from "@aethervtt/shard-gpu";
10
- import { definePlugin, LogResource } from "@aethervtt/shard-runtime";
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, caches, and canvas."
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 swapchain, or an offscreen target when headless."
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
- setup: () => ({ recovering: { value: false } }),
58
- run: ({ recovering }, world) => {
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
- app.insertResource(Views, { list: [] }).insertResource(RenderOptions, { windowView: options.windowView ?? true }).configureSets(
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(Last, begin.inSet(RenderSet.Begin), execute.inSet(RenderSet.Graph));
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 gpu = options.gpu ?? await createGpuContext({ canvas: options.canvas, features: options.features });
102
- app.insertResource(Gpu, gpu);
103
- app.insertResource(Graph, new RenderGraph(gpu));
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
- if (gpu.context) app.insertResource(Window, new WindowTarget(gpu));
109
- else if (options.target) app.insertResource(Window, options.target);
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
- gpu.onDeviceLost((info) => {
113
- app.world.send(GpuDeviceLost, info);
114
- void gpu.recreate();
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
- return world.resource(Graph).capture(view);
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 result = await graph.capture(view, buffer);
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
  };
@@ -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;
@@ -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,