@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
@@ -66,6 +66,7 @@ import shard::mesh::mesh_vertex_at;
66
66
  import shard::pbr::material::pbr_input;
67
67
  import shard::pbr::standard::material;
68
68
  import shard::pbr::gbuffer::{ GBufferOutput, pack_gbuffer };
69
+ @if(CUTAWAY) import shard::cutaway::cutaway_clip;
69
70
 
70
71
  const FLAG_RECEIVER: u32 = 4u;
71
72
 
@@ -85,6 +86,7 @@ const FLAG_RECEIVER: u32 = 4u;
85
86
 
86
87
  /** The surface stage only: what the material makes of the surface, packed for lighting. */
87
88
  @fragment fn fs(in: VertexOutput) -> GBufferOutput {
89
+ @if(CUTAWAY) cutaway_clip(in.world_position, in.clip.xy, in.flags);
88
90
  let p = pbr_input(in);
89
91
  @if(MASK) if (p.alpha < material.alphaCutoff) { discard; }
90
92
  return pack_gbuffer(p, (in.flags & FLAG_RECEIVER) != 0u, view.exposure);
package/dist/deferred.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { ShardError } from "@aethervtt/shard-core";
2
2
  import { GpuBuffer } from "@aethervtt/shard-gpu";
3
3
  import { Culler } from "./culling.js";
4
+ import { addRenderFeatures } from "./features.js";
4
5
  import {
5
6
  drawMaterials,
6
7
  ForwardStateResource,
7
8
  gbufferEmissiveFormat,
9
+ INTERIOR_DEFINES,
8
10
  PASS_GBUFFER,
9
11
  PASS_OPAQUE,
10
12
  sceneColor,
@@ -12,6 +14,7 @@ import {
12
14
  } from "./forward.js";
13
15
  import { RenderPhase } from "./graph.js";
14
16
  import { Gpu, Graph, RenderDescribers, Shaders, Views } from "./plugin.js";
17
+ import { depthReadEntry } from "./tier.js";
15
18
  import { Cameras, cameraOf } from "./view.js";
16
19
  const GBUFFER_CHANNELS = ["albedo", "normal", "roughness", "metallic", "emissive"];
17
20
  const deferredView = (view) => cameraOf(view)?.deferred === true;
@@ -80,7 +83,11 @@ function lightingNode() {
80
83
  const cam = cameraOf(ctx.view);
81
84
  const pv = state.views.get(ctx.view.name);
82
85
  if (!pv) return;
83
- const module = ctx.world.resource(Shaders).module(gpu, { root: "shard::pbr::deferred_lighting" });
86
+ const mode = state.interiorMode;
87
+ const module = ctx.world.resource(Shaders).module(
88
+ gpu,
89
+ mode !== 0 ? { root: "shard::pbr::deferred_lighting", defines: INTERIOR_DEFINES[mode] } : { root: "shard::pbr::deferred_lighting" }
90
+ );
84
91
  if (!module) {
85
92
  gpu.pipelines.skipped++;
86
93
  return;
@@ -93,7 +100,7 @@ function lightingNode() {
93
100
  { binding: 0, visibility: F, texture: { sampleType: "float" } },
94
101
  { binding: 1, visibility: F, texture: { sampleType: "unfilterable-float" } },
95
102
  { binding: 2, visibility: F, texture: { sampleType: "float" } },
96
- { binding: 3, visibility: F, texture: { sampleType: "depth" } }
103
+ depthReadEntry(gpu, 3, F)
97
104
  ]
98
105
  });
99
106
  layoutGeneration = gpu.generation;
@@ -271,7 +278,7 @@ function describeDeferred(world) {
271
278
  deferredMeshes: cam.draws.visible,
272
279
  forwardMeshes: cam.forwardOnly.visible + cam.transparent.visible,
273
280
  forwardReasons: forwardReason(cam, world)
274
- } : { path: "forward", msaa: cam.msaa };
281
+ } : cam.msaaCapped ? { path: "forward", msaa: cam.msaa, msaaLimit: "the device can't multisample rgba16float" } : { path: "forward", msaa: cam.msaa };
275
282
  }
276
283
  return { views };
277
284
  }
@@ -281,6 +288,20 @@ function addDeferredNodes(app) {
281
288
  graph.declare({ name: "gbuffer0", format: "rgba8unorm-srgb" });
282
289
  graph.declare({ name: "gbuffer1", format: "rgba16float" });
283
290
  graph.declare({ name: "gbuffer2", format: gbufferEmissiveFormat(gpu) });
291
+ addRenderFeatures(app.world, {
292
+ name: "render/deferred",
293
+ description: "The deferred path: G-buffer, lighting, and forward-only materials.",
294
+ nodes: [
295
+ "deferred-gbuffer",
296
+ "gbuffer-fill",
297
+ "deferred-lighting",
298
+ "deferred-forward",
299
+ "gbuffer-debug"
300
+ ],
301
+ baseline: {
302
+ strategy: "The same passes, reading lights and clusters through the baseline accessors"
303
+ }
304
+ });
284
305
  graph.addNode("deferred-gbuffer", gbufferNode);
285
306
  graph.addNode("gbuffer-fill", gbufferFillNode);
286
307
  graph.addNode("deferred-lighting", lightingNode());
package/dist/deform.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
1
+ import type { GpuContext } from '@aethervtt/shard-gpu';
2
2
  import type { Mesh } from '@aethervtt/shard-mesh';
3
+ import { DataStore } from './data-store.js';
3
4
  /**
4
5
  * Per-slot deform record, 12 words (mirrors `Deform` in `shard::mesh` and `shard::cull`):
5
6
  *
@@ -47,9 +48,9 @@ export declare class DeformStore {
47
48
  private vertexWords;
48
49
  private wasted;
49
50
  readonly meshes: Map<Mesh, MeshDeform>;
50
- readonly recordBuffer: GpuBuffer;
51
- readonly poseBuffer: GpuBuffer;
52
- readonly vertexBuffer: GpuBuffer;
51
+ readonly recordBuffer: DataStore;
52
+ readonly poseBuffer: DataStore;
53
+ readonly vertexBuffer: DataStore;
53
54
  private recordLo;
54
55
  private recordHi;
55
56
  private vertexDirty;
package/dist/deform.js CHANGED
@@ -1,4 +1,4 @@
1
- import { GpuBuffer } from "@aethervtt/shard-gpu";
1
+ import { DataStore } from "./data-store.js";
2
2
  const DEFORM_WORDS = 12;
3
3
  const DEFORM_BYTES = DEFORM_WORDS * 4;
4
4
  const SKIN_WORDS = 6;
@@ -26,14 +26,9 @@ class DeformStore {
26
26
  constructor(gpu) {
27
27
  this.gpu = gpu;
28
28
  this.generation = gpu.generation;
29
- const usage = GPUBufferUsage.STORAGE;
30
- this.recordBuffer = new GpuBuffer(gpu, {
31
- label: "deform/records",
32
- usage,
33
- size: DEFORM_BYTES * 256
34
- });
35
- this.poseBuffer = new GpuBuffer(gpu, { label: "deform/poses", usage, size: 16 * 1024 });
36
- this.vertexBuffer = new GpuBuffer(gpu, { label: "deform/vertices", usage, size: 4096 });
29
+ this.recordBuffer = new DataStore(gpu, { label: "deform/records", size: DEFORM_BYTES * 256 });
30
+ this.poseBuffer = new DataStore(gpu, { label: "deform/poses", size: 16 * 1024 });
31
+ this.vertexBuffer = new DataStore(gpu, { label: "deform/vertices", size: 4096 });
37
32
  }
38
33
  /** Slots the record array holds. */
39
34
  get capacity() {
@@ -1,5 +1,7 @@
1
1
  /**
2
- * The lighting, culling, and renderScale sections of `render.describe`, for agents and the editor.
3
- * Only introspection: an app that ships without it renders the same.
2
+ * The lighting, culling, renderScale, surfaces, uploads, and frames sections of `render.describe`,
3
+ * and the
4
+ * `gpu.stats` method, for agents and the editor. Only introspection: an app that ships without it
5
+ * renders the same.
4
6
  */
5
7
  export declare const renderDescribePlugin: import("@aethervtt/shard-runtime").Plugin;
@@ -1,17 +1,89 @@
1
- import { definePlugin } from "@aethervtt/shard-runtime";
1
+ import { defineSchema, t } from "@aethervtt/shard-core";
2
+ import { definePlugin, FrameDemand } from "@aethervtt/shard-runtime";
2
3
  import { describeCulling, describeLighting } from "./debug-views.js";
3
- import { RenderDescribers } from "./plugin.js";
4
+ import { GroundLayer } from "./layers.js";
5
+ import { Gpu, RenderDescribers, RenderOptions, Window } from "./plugin.js";
4
6
  import { describeRenderScale } from "./render-scale.js";
7
+ import { RenderStats } from "./stats.js";
8
+ import { Cameras } from "./view.js";
9
+ function describeSurfaces(world) {
10
+ const window = world.tryResource(Window);
11
+ return world.resource(Gpu).surfaces.map((s) => ({
12
+ label: s.label,
13
+ size: [s.width, s.height],
14
+ alpha: s.alpha,
15
+ pixelRatio: s.pixelRatio,
16
+ thisApp: s === window
17
+ }));
18
+ }
19
+ function describeUploads(world) {
20
+ const stats = world.resource(RenderStats);
21
+ const bytes = (r) => ({
22
+ bytes: { ...r.bytes },
23
+ sceneBytes: r.sceneBytes,
24
+ created: r.created
25
+ });
26
+ const rebuilds = (r) => ({
27
+ chunksRebuilt: r.chunksRebuilt,
28
+ meshesRebuilt: r.meshesRebuilt,
29
+ shadowMapsRendered: r.shadowMapsRendered
30
+ });
31
+ return {
32
+ frames: stats.frames,
33
+ lastFrame: bytes(stats.lastFrame),
34
+ recent: bytes(stats.recent),
35
+ rebuilds: { lastFrame: rebuilds(stats.lastFrame), recent: rebuilds(stats.recent) }
36
+ };
37
+ }
38
+ function describeGround(world) {
39
+ const bands = {};
40
+ world.query({ with: [GroundLayer] }).each((_e, row, table) => {
41
+ const band = String(table.column(GroundLayer, "band")[row]);
42
+ bands[band] = (bands[band] ?? 0) + 1;
43
+ });
44
+ const views = {};
45
+ for (const cam of world.resource(Cameras).values())
46
+ views[`camera:${cam.entity}`] = {
47
+ instances: cam.ground.visible + cam.overlay.visible,
48
+ draws: cam.ground.length + cam.overlay.length
49
+ };
50
+ return { bands, views };
51
+ }
52
+ const gpuStats = {
53
+ name: "gpu.stats",
54
+ description: "Live GPU buffers and textures, by owner: each app's render plugin is one, and 'gpu' holds what apps on the device share.",
55
+ params: defineSchema("render/GpuStatsParams", {
56
+ owner: t.string({ description: "One owner. Empty: this app, every owner, and the total." })
57
+ }),
58
+ handler: ({ world }, p) => {
59
+ const gpu = world.resource(Gpu);
60
+ if (p.owner) return { owner: p.owner, ...gpu.stats(p.owner) };
61
+ return {
62
+ owner: world.resource(RenderOptions).owner,
63
+ total: gpu.stats(),
64
+ owners: Object.fromEntries(gpu.owners().map((o) => [o, gpu.stats(o)]))
65
+ };
66
+ }
67
+ };
5
68
  const renderDescribePlugin = definePlugin({
6
69
  name: "render/describe",
7
70
  dependencies: ["render/forward"],
8
- build() {
71
+ build(app) {
72
+ app.addMethod(gpuStats);
9
73
  },
10
74
  ready(app) {
11
75
  const describers = app.world.initResource(RenderDescribers);
12
76
  describers.set("lighting", (world) => describeLighting(world));
13
77
  describers.set("culling", (world) => describeCulling(world));
14
78
  describers.set("renderScale", (world) => describeRenderScale(world));
79
+ describers.set("surfaces", describeSurfaces);
80
+ describers.set("uploads", describeUploads);
81
+ describers.set("ground", describeGround);
82
+ describers.set("frames", (world) => world.resource(FrameDemand).describe());
83
+ describers.set(
84
+ "gpuObjects",
85
+ (world) => world.resource(Gpu).stats(world.resource(RenderOptions).owner)
86
+ );
15
87
  }
16
88
  });
17
89
  export {
@@ -1,3 +1,4 @@
1
+ import { addRenderFeatures } from "./features.js";
1
2
  import { Textures } from "@aethervtt/shard-texture";
2
3
  import { GpuAssetsResource } from "./gpu-assets.js";
3
4
  import { RenderPhase } from "./graph.js";
@@ -53,15 +54,17 @@ function tonemapNode() {
53
54
  }
54
55
  const format = ctx.texture("ldr").format;
55
56
  const srgb = format.endsWith("-srgb");
57
+ const alpha = cam.alphaOutput;
56
58
  const pipeline = cache.render(
57
59
  ctx,
58
- `tonemap/${format}`,
60
+ `tonemap/${format}${alpha ? "/alpha" : ""}`,
59
61
  "shard::post::tonemap",
60
62
  "fs",
61
63
  [layout],
62
64
  [{ format }],
63
65
  {
64
- SRGB_TARGET: srgb
66
+ SRGB_TARGET: srgb,
67
+ TRANSPARENT: alpha
65
68
  }
66
69
  );
67
70
  if (!pipeline) return;
@@ -144,13 +147,15 @@ function upscaleNode() {
144
147
  const input = ctx.texture("display");
145
148
  const target = ctx.texture("view-target");
146
149
  const format = target.format;
150
+ const alpha = cameraOf(ctx.view).alphaOutput;
147
151
  const pipeline = cache.render(
148
152
  ctx,
149
- `upscale/${format}`,
153
+ `upscale/${format}${alpha ? "/alpha" : ""}`,
150
154
  "shard::post::upscale",
151
155
  "fs",
152
156
  [layout],
153
- [{ format }]
157
+ [{ format }],
158
+ { TRANSPARENT: alpha }
154
159
  );
155
160
  if (!pipeline) return;
156
161
  const settings = ctx.world.tryResource(RenderScale);
@@ -180,6 +185,12 @@ function upscaleNode() {
180
185
  function addDisplayNodes(world) {
181
186
  const graph = world.resource(Graph);
182
187
  graph.declare({ name: "display", format: "view" });
188
+ addRenderFeatures(world, {
189
+ name: "render/display",
190
+ description: "Tonemapping and the upscale to the display.",
191
+ nodes: ["tonemap", "post/upscale"],
192
+ baseline: { strategy: "The same fragment passes" }
193
+ });
183
194
  graph.addNode("tonemap", tonemapNode());
184
195
  graph.addNode("post/upscale", upscaleNode());
185
196
  }
@@ -27,6 +27,8 @@ export declare class RenderScaleController {
27
27
  private probeAge;
28
28
  private probeWait;
29
29
  private hold;
30
+ /** Verifying an upward probe: it needs frames to measure, so on-demand apps keep rendering. */
31
+ get probing(): boolean;
30
32
  /**
31
33
  * One frame. `skip`: pipelines are compiling, so the frame says nothing about steady cost.
32
34
  * `displayMs`: one refresh of the display, the budget when `targetMs` is 0.
@@ -1,5 +1,5 @@
1
1
  import { defineSystem, Last } from "@aethervtt/shard-core";
2
- import { DisplayRate, definePlugin, Time } from "@aethervtt/shard-runtime";
2
+ import { DisplayRate, definePlugin, FrameDemand, Time } from "@aethervtt/shard-runtime";
3
3
  import { Gpu, Graph, RenderSet } from "./plugin.js";
4
4
  import {
5
5
  HIGHEST,
@@ -30,6 +30,10 @@ class RenderScaleController {
30
30
  probeAge = 0;
31
31
  probeWait = 3;
32
32
  hold = 0;
33
+ /** Verifying an upward probe: it needs frames to measure, so on-demand apps keep rendering. */
34
+ get probing() {
35
+ return this.probeFrom > 0;
36
+ }
33
37
  /**
34
38
  * One frame. `skip`: pipelines are compiling, so the frame says nothing about steady cost.
35
39
  * `displayMs`: one refresh of the display, the budget when `targetMs` is 0.
@@ -175,6 +179,7 @@ const updateRenderScale = defineSystem({
175
179
  gpu.pipelines.pending > 0,
176
180
  display
177
181
  );
182
+ world.tryResource(FrameDemand)?.set("render/render-scale", controller.probing);
178
183
  }
179
184
  });
180
185
  const dynamicResolutionPlugin = definePlugin({
@@ -4,15 +4,17 @@ import {
4
4
  DefaultEnvironment,
5
5
  describeEnvironment,
6
6
  EnvironmentMap,
7
+ Environments,
7
8
  ProceduralSky,
8
9
  prepareEnvironments,
9
10
  runEnvironmentWork,
10
11
  Skybox
11
12
  } from "./environment.js";
12
13
  import { ENVIRONMENT_SHADERS } from "./environment-shaders.js";
14
+ import { addRenderFeatures } from "./features.js";
13
15
  import { ForwardStateResource, isCamera, skyNode } from "./forward.js";
14
16
  import { RenderPhase } from "./graph.js";
15
- import { Graph, RenderDescribers, RenderSet, Shaders } from "./plugin.js";
17
+ import { Gpu, Graph, RenderDescribers, RenderSet, Shaders } from "./plugin.js";
16
18
  import { registerShaders } from "./shaders.js";
17
19
  const environmentPlugin = definePlugin({
18
20
  name: "render/environment",
@@ -22,11 +24,24 @@ const environmentPlugin = definePlugin({
22
24
  app.world.initResource(DefaultEnvironment);
23
25
  app.addSystems(Last, prepareEnvironments.inSet(RenderSet.Prepare));
24
26
  },
25
- ready(app) {
27
+ async ready(app) {
26
28
  const world = app.world;
29
+ const baseline = world.resource(Gpu).tier === "baseline" ? await import("./baseline/environment.js") : void 0;
27
30
  registerShaders(world.resource(Shaders), ENVIRONMENT_SHADERS);
31
+ if (baseline) {
32
+ registerShaders(world.resource(Shaders), baseline.BASELINE_ENVIRONMENT_SHADERS);
33
+ world.resource(Environments).baseline = baseline;
34
+ }
28
35
  world.initResource(RenderDescribers).set("environment", (w) => describeEnvironment(w));
29
36
  const graph = world.resource(Graph);
37
+ addRenderFeatures(world, {
38
+ name: "render/environment",
39
+ description: "Image-based lighting: cube conversion, specular prefilter, SH irradiance, BRDF LUT; and the sky.",
40
+ nodes: ["environment", "sky"],
41
+ baseline: {
42
+ strategy: "Prefilter, SH and BRDF LUT as fragment passes into the cube faces and mips"
43
+ }
44
+ });
30
45
  graph.addNode("environment", {
31
46
  kind: "raw",
32
47
  phase: RenderPhase.Setup,
@@ -1,6 +1,7 @@
1
- import { GpuBuffer, type GpuContext } from '@aethervtt/shard-gpu';
1
+ import type { GpuContext } from '@aethervtt/shard-gpu';
2
2
  import type { Texture } from '@aethervtt/shard-texture';
3
3
  import type { CameraAtmosphere } from './atmosphere-state.js';
4
+ import { DataStore } from './data-store.js';
4
5
  import type { CameraData } from './view.js';
5
6
  /** Frames between rebakes of a moving camera's atmosphere environment. */
6
7
  export declare const REBAKE_FRAMES = 8;
@@ -19,7 +20,8 @@ export declare class Environment {
19
20
  specular: GPUTexture;
20
21
  specularView: GPUTextureView;
21
22
  sourceView: GPUTextureView;
22
- readonly sh: GpuBuffer;
23
+ /** SH9 of the diffuse irradiance: `@data(uniform)` in the forward shader. */
24
+ readonly sh: DataStore;
23
25
  /** 'pending' until the GPU has prefiltered the current key. */
24
26
  state: 'pending' | 'ready';
25
27
  /** How many times it's been (re)baked or prefiltered. */
@@ -59,12 +61,14 @@ export declare class EnvironmentStore {
59
61
  lutReady: boolean;
60
62
  emptyCube: GPUTexture | undefined;
61
63
  emptyView: GPUTextureView | undefined;
62
- emptySh: GpuBuffer | undefined;
64
+ emptySh: DataStore | undefined;
63
65
  sampler: GPUSampler | undefined;
64
66
  generation: number;
65
67
  frame: number;
66
68
  /** The frame the prefilter work last ran (it runs once per frame, for all views). */
67
69
  ranFrame: number;
70
+ /** The baseline tier's prefilter (0064): fragment passes, loaded only on a baseline device. */
71
+ baseline: typeof import('./baseline/environment.js') | undefined;
68
72
  ensure(gpu: GpuContext): void;
69
73
  }
70
74
  export declare const Environments: import("@aethervtt/shard-core").ResourceDef<EnvironmentStore>;
@@ -1,5 +1,6 @@
1
1
  import { defineResource } from "@aethervtt/shard-core";
2
- import { GpuBuffer } from "@aethervtt/shard-gpu";
2
+ import { DataStore } from "./data-store.js";
3
+ import { bindingDimension } from "./tier.js";
3
4
  const REBAKE_FRAMES = 8;
4
5
  const SOURCE_SIZE = 512;
5
6
  const SKY_SIZE = 256;
@@ -15,6 +16,7 @@ class Environment {
15
16
  specular;
16
17
  specularView;
17
18
  sourceView;
19
+ /** SH9 of the diffuse irradiance: `@data(uniform)` in the forward shader. */
18
20
  sh;
19
21
  /** 'pending' until the GPU has prefiltered the current key. */
20
22
  state = "pending";
@@ -37,20 +39,25 @@ class Environment {
37
39
  size: [this.size, this.size, 6],
38
40
  format: "rgba16float",
39
41
  mipLevelCount: mips,
40
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_SRC
42
+ // Written by compute on the full tier; rendered face by face on baseline (0064).
43
+ usage: GPUTextureUsage.TEXTURE_BINDING | (gpu.tier === "full" ? GPUTextureUsage.STORAGE_BINDING : GPUTextureUsage.RENDER_ATTACHMENT) | GPUTextureUsage.COPY_SRC,
44
+ ...bindingDimension(gpu, "cube")
41
45
  });
42
46
  this.specular = gpu.device.createTexture({
43
47
  label: `environment/${kind}/specular`,
44
48
  size: [SPECULAR_SIZE, SPECULAR_SIZE, 6],
45
49
  format: "rgba16float",
46
50
  mipLevelCount: SPECULAR_MIPS,
47
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_SRC
51
+ // Written by compute on the full tier; rendered face by face on baseline (0064).
52
+ usage: GPUTextureUsage.TEXTURE_BINDING | (gpu.tier === "full" ? GPUTextureUsage.STORAGE_BINDING : GPUTextureUsage.RENDER_ATTACHMENT) | GPUTextureUsage.COPY_SRC,
53
+ ...bindingDimension(gpu, "cube")
48
54
  });
49
55
  this.sourceView = this.source.createView({ dimension: "cube" });
50
56
  this.specularView = this.specular.createView({ dimension: "cube" });
51
- this.sh = new GpuBuffer(gpu, {
57
+ this.sh = new DataStore(gpu, {
52
58
  label: `environment/${kind}/sh`,
53
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
59
+ kind: "uniform",
60
+ usage: GPUBufferUsage.COPY_SRC,
54
61
  size: 9 * 16
55
62
  });
56
63
  }
@@ -74,6 +81,8 @@ class EnvironmentStore {
74
81
  frame = 0;
75
82
  /** The frame the prefilter work last ran (it runs once per frame, for all views). */
76
83
  ranFrame = -1;
84
+ /** The baseline tier's prefilter (0064): fragment passes, loaded only on a baseline device. */
85
+ baseline;
77
86
  ensure(gpu) {
78
87
  if (this.generation === gpu.generation && this.lut) return;
79
88
  this.generation = gpu.generation;
@@ -84,19 +93,21 @@ class EnvironmentStore {
84
93
  label: "environment/brdf-lut",
85
94
  size: [LUT_SIZE, LUT_SIZE],
86
95
  format: "rgba16float",
87
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.STORAGE_BINDING
96
+ // Computed on the full tier; drawn by a fragment pass on baseline (0064).
97
+ usage: GPUTextureUsage.TEXTURE_BINDING | (gpu.tier === "full" ? GPUTextureUsage.STORAGE_BINDING : GPUTextureUsage.RENDER_ATTACHMENT)
88
98
  });
89
99
  this.lutReady = false;
90
100
  this.emptyCube = gpu.device.createTexture({
91
101
  label: "environment/empty",
92
102
  size: [1, 1, 6],
93
103
  format: "rgba16float",
94
- usage: GPUTextureUsage.TEXTURE_BINDING
104
+ usage: GPUTextureUsage.TEXTURE_BINDING,
105
+ ...bindingDimension(gpu, "cube")
95
106
  });
96
107
  this.emptyView = this.emptyCube.createView({ dimension: "cube" });
97
- this.emptySh = new GpuBuffer(gpu, {
108
+ this.emptySh = new DataStore(gpu, {
98
109
  label: "environment/empty-sh",
99
- usage: GPUBufferUsage.STORAGE,
110
+ kind: "uniform",
100
111
  size: 9 * 16
101
112
  });
102
113
  this.sampler = gpu.device.createSampler({
@@ -90,6 +90,7 @@ const ProceduralSky = defineComponent(
90
90
  const DefaultEnvironment = defineResource(
91
91
  "render/DefaultEnvironment",
92
92
  {
93
+ hostWritable: true,
93
94
  description: "The environment (map or procedural sky) for cameras that have none. AmbientLight is the fallback when neither is set.",
94
95
  init: () => ({ map: null, sky: null, background: true })
95
96
  }
@@ -194,6 +195,7 @@ const prepareEnvironments = defineSystem({
194
195
  } else if (map?.texture) useMap(map, skybox);
195
196
  else if (defaults.map?.texture)
196
197
  useMap(defaults.map, defaults.background ? Math.max(skybox, 1) : skybox);
198
+ if (cam.alphaOutput) entry.background = -1;
197
199
  store.cameras.set(e, entry);
198
200
  view.data.environment = entry;
199
201
  }
@@ -312,6 +314,10 @@ function runEnvironmentWork(ctx) {
312
314
  const gpu = ctx.gpu;
313
315
  const store = world.resource(Environments);
314
316
  if (store.ranFrame === store.frame) return;
317
+ if (gpu.tier === "baseline") {
318
+ store.baseline?.runBaselineEnvironment(ctx, store);
319
+ return;
320
+ }
315
321
  if (store.pending.length === 0 && store.lutReady) {
316
322
  store.ranFrame = store.frame;
317
323
  return;
@@ -422,7 +428,7 @@ function runEnvironmentWork(ctx) {
422
428
  mipLevelCount: 1
423
429
  })
424
430
  },
425
- { binding: 1, resource: { buffer: env.sh.buffer } }
431
+ { binding: 1, resource: { buffer: env.sh.gpuBuffer } }
426
432
  ]
427
433
  })
428
434
  );
@@ -0,0 +1,49 @@
1
+ import { type ComponentDef, type World } from '@aethervtt/shard-core';
2
+ /**
3
+ * Logs `render/feature-missing` once per world and plugin: `what` asked for a feature whose
4
+ * `plugin` (one forwardPlugin includes) isn't installed, and `outcome` is what happens instead.
5
+ */
6
+ export declare function warnFeatureMissing(world: World, what: string, plugin: string, outcome: string): void;
7
+ /** How a feature runs on the baseline tier: a short account of its strategy there. */
8
+ export interface BaselineStrategy {
9
+ strategy: string;
10
+ }
11
+ export interface RenderFeature {
12
+ /** `package/feature`, e.g. `render/bloom`. */
13
+ readonly name: string;
14
+ readonly description: string;
15
+ /** The graph nodes it adds. */
16
+ readonly nodes: readonly string[];
17
+ /** What it does on the baseline tier, or `'unsupported'`. */
18
+ readonly baseline: BaselineStrategy | 'unsupported';
19
+ /** Components that mean a scene uses it (for `render/feature-unsupported` on baseline). */
20
+ readonly components?: readonly ComponentDef[];
21
+ }
22
+ export declare const RenderFeatures: import("@aethervtt/shard-core").ResourceDef<Map<string, RenderFeature>>;
23
+ /** Registers features a plugin installs. Plugins call it where they add their graph nodes. */
24
+ export declare function addRenderFeatures(world: World, ...features: RenderFeature[]): void;
25
+ /** The feature a graph node belongs to, if one registered it. */
26
+ export declare function featureOfNode(world: World, node: string): RenderFeature | undefined;
27
+ /** `render.describe → features`: each installed feature, its nodes, and its baseline strategy. */
28
+ export declare function describeFeatures(world: World): {
29
+ name: string;
30
+ nodes: string[];
31
+ baseline: string;
32
+ }[];
33
+ /**
34
+ * The graph nodes of features the baseline tier doesn't run (`baseline: 'unsupported'`): the
35
+ * graph skips them on a baseline device, so they never ask for what it lacks.
36
+ */
37
+ export declare function unsupportedNodes(world: World): ReadonlySet<string>;
38
+ /**
39
+ * Raises `render/feature-unsupported` for `feature` (a feature, or a part of one) that a scene uses
40
+ * on the baseline tier: RenderHealth turns degraded until `clearUnsupported`.
41
+ */
42
+ export declare function reportUnsupported(world: World, feature: string, message: string): void;
43
+ export declare function clearUnsupported(world: World, feature: string): void;
44
+ /**
45
+ * On the baseline tier: reports each unsupported feature whose components are in the world, and
46
+ * clears the report once they're gone. A query per component, made once; issues change only when
47
+ * a feature's use does.
48
+ */
49
+ export declare function checkUnsupportedFeatures(world: World): void;