@aethervtt/shard-render 0.0.1 → 0.2.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 (170) hide show
  1. package/dist/ablation.d.ts +74 -0
  2. package/dist/ablation.js +117 -0
  3. package/dist/assets.d.ts +3 -0
  4. package/dist/assets.js +43 -2
  5. package/dist/atmosphere-nodes.d.ts +9 -0
  6. package/dist/atmosphere-nodes.js +157 -30
  7. package/dist/atmosphere-plugin.js +8 -2
  8. package/dist/atmosphere-shaders.js +17 -5
  9. package/dist/baseline/atmosphere.d.ts +16 -0
  10. package/dist/baseline/atmosphere.js +301 -0
  11. package/dist/baseline/data-texture.d.ts +36 -0
  12. package/dist/baseline/data-texture.js +181 -0
  13. package/dist/baseline/environment.d.ts +14 -0
  14. package/dist/baseline/environment.js +368 -0
  15. package/dist/baseline/exposure.d.ts +25 -0
  16. package/dist/baseline/exposure.js +133 -0
  17. package/dist/baseline/lights.d.ts +33 -0
  18. package/dist/baseline/lights.js +133 -0
  19. package/dist/baseline/textures.d.ts +97 -0
  20. package/dist/baseline/textures.js +203 -0
  21. package/dist/camera.d.ts +2 -0
  22. package/dist/camera.js +4 -0
  23. package/dist/crowd.d.ts +21 -0
  24. package/dist/crowd.js +121 -0
  25. package/dist/culling-shaders.js +21 -2
  26. package/dist/culling.d.ts +11 -2
  27. package/dist/culling.js +44 -18
  28. package/dist/cutaway-plugin.d.ts +48 -0
  29. package/dist/cutaway-plugin.js +364 -0
  30. package/dist/cutaway.d.ts +27 -0
  31. package/dist/cutaway.js +49 -0
  32. package/dist/data-store.d.ts +69 -0
  33. package/dist/data-store.js +105 -0
  34. package/dist/debug-shaders.js +17 -3
  35. package/dist/debug-views.js +4 -2
  36. package/dist/decals-plugin.d.ts +5 -0
  37. package/dist/decals-plugin.js +54 -0
  38. package/dist/decals.d.ts +69 -0
  39. package/dist/decals.js +326 -0
  40. package/dist/deferred-shaders.js +2 -0
  41. package/dist/deferred.js +28 -4
  42. package/dist/deform.d.ts +5 -4
  43. package/dist/deform.js +4 -9
  44. package/dist/describe-plugin.d.ts +4 -2
  45. package/dist/describe-plugin.js +75 -3
  46. package/dist/display-nodes.js +37 -13
  47. package/dist/dynamic-resolution.d.ts +2 -0
  48. package/dist/dynamic-resolution.js +6 -1
  49. package/dist/environment-plugin.js +17 -2
  50. package/dist/environment-state.d.ts +7 -3
  51. package/dist/environment-state.js +20 -9
  52. package/dist/environment.js +7 -1
  53. package/dist/features.d.ts +49 -0
  54. package/dist/features.js +106 -0
  55. package/dist/foliage-budget.d.ts +64 -0
  56. package/dist/foliage-budget.js +108 -0
  57. package/dist/foliage-path.d.ts +19 -0
  58. package/dist/foliage-path.js +7 -0
  59. package/dist/foliage-shaders.d.ts +11 -0
  60. package/dist/foliage-shaders.js +504 -0
  61. package/dist/foliage.d.ts +194 -0
  62. package/dist/foliage.js +1027 -0
  63. package/dist/forward.d.ts +38 -2
  64. package/dist/forward.js +608 -186
  65. package/dist/fxaa.js +12 -2
  66. package/dist/gizmos-plugin.js +7 -0
  67. package/dist/gizmos.d.ts +3 -3
  68. package/dist/gizmos.js +11 -13
  69. package/dist/gpu-assets.d.ts +58 -4
  70. package/dist/gpu-assets.js +191 -8
  71. package/dist/graph.d.ts +45 -0
  72. package/dist/graph.js +169 -19
  73. package/dist/health.d.ts +58 -0
  74. package/dist/health.js +112 -0
  75. package/dist/index.d.ts +40 -11
  76. package/dist/index.js +343 -9
  77. package/dist/instances.d.ts +119 -8
  78. package/dist/instances.js +387 -78
  79. package/dist/interior-plugin.d.ts +121 -0
  80. package/dist/interior-plugin.js +465 -0
  81. package/dist/interior.d.ts +24 -0
  82. package/dist/interior.js +13 -0
  83. package/dist/layers.d.ts +35 -0
  84. package/dist/layers.js +49 -0
  85. package/dist/lens-plugin.d.ts +43 -0
  86. package/dist/lens-plugin.js +304 -0
  87. package/dist/lens.d.ts +41 -0
  88. package/dist/lens.js +99 -0
  89. package/dist/lights.d.ts +50 -5
  90. package/dist/lights.js +101 -22
  91. package/dist/material-pipelines.d.ts +37 -4
  92. package/dist/material-pipelines.js +102 -5
  93. package/dist/materials.d.ts +23 -0
  94. package/dist/materials.js +44 -1
  95. package/dist/outline-plugin.d.ts +48 -0
  96. package/dist/outline-plugin.js +529 -0
  97. package/dist/outline.d.ts +15 -0
  98. package/dist/outline.js +42 -0
  99. package/dist/overlay-registry.d.ts +3 -1
  100. package/dist/overlay-registry.js +3 -1
  101. package/dist/overlays.js +6 -2
  102. package/dist/perf-overlay.d.ts +9 -0
  103. package/dist/perf-overlay.js +125 -0
  104. package/dist/picking.d.ts +17 -0
  105. package/dist/picking.js +40 -11
  106. package/dist/pixel-perfect-plugin.js +10 -2
  107. package/dist/plugin.d.ts +69 -7
  108. package/dist/plugin.js +223 -25
  109. package/dist/post-common.d.ts +22 -2
  110. package/dist/post-common.js +82 -2
  111. package/dist/post-nodes.d.ts +7 -1
  112. package/dist/post-nodes.js +235 -111
  113. package/dist/post-plugin.js +5 -3
  114. package/dist/post-scene.d.ts +130 -0
  115. package/dist/post-scene.js +164 -0
  116. package/dist/post-shaders.js +85 -62
  117. package/dist/post.js +11 -1
  118. package/dist/projection.d.ts +24 -0
  119. package/dist/projection.js +87 -0
  120. package/dist/readback.d.ts +1 -0
  121. package/dist/readback.js +1 -0
  122. package/dist/render-scale.js +1 -0
  123. package/dist/ribbons-plugin.d.ts +5 -0
  124. package/dist/ribbons-plugin.js +59 -0
  125. package/dist/ribbons.d.ts +44 -0
  126. package/dist/ribbons.js +288 -0
  127. package/dist/scenario.d.ts +70 -0
  128. package/dist/scenario.js +125 -0
  129. package/dist/screen-effects.d.ts +97 -0
  130. package/dist/screen-effects.js +192 -0
  131. package/dist/shaders.d.ts +13 -0
  132. package/dist/shaders.js +189 -34
  133. package/dist/shadow-catcher.d.ts +9 -0
  134. package/dist/shadow-catcher.js +133 -0
  135. package/dist/shadows.d.ts +27 -2
  136. package/dist/shadows.js +100 -20
  137. package/dist/standard-fields.d.ts +1 -0
  138. package/dist/standard-fields.js +5 -0
  139. package/dist/standard.js +18 -0
  140. package/dist/stats.d.ts +45 -1
  141. package/dist/stats.js +91 -3
  142. package/dist/surface-model.d.ts +57 -0
  143. package/dist/surface-model.js +289 -0
  144. package/dist/surface-shaders.d.ts +3 -0
  145. package/dist/surface-shaders.js +160 -0
  146. package/dist/surface.d.ts +40 -0
  147. package/dist/surface.js +86 -0
  148. package/dist/target.d.ts +1 -13
  149. package/dist/target.js +1 -26
  150. package/dist/testing.d.ts +11 -0
  151. package/dist/testing.js +39 -1
  152. package/dist/tier.d.ts +13 -0
  153. package/dist/tier.js +14 -0
  154. package/dist/timer.d.ts +18 -1
  155. package/dist/timer.js +73 -20
  156. package/dist/variants.d.ts +37 -0
  157. package/dist/variants.js +104 -0
  158. package/dist/vfx-draw.d.ts +37 -0
  159. package/dist/vfx-draw.js +291 -0
  160. package/dist/vfx-state.d.ts +42 -0
  161. package/dist/vfx-state.js +81 -0
  162. package/dist/vfx.d.ts +69 -0
  163. package/dist/vfx.js +103 -0
  164. package/dist/view-visibility-plugin.d.ts +61 -0
  165. package/dist/view-visibility-plugin.js +267 -0
  166. package/dist/view-visibility.d.ts +28 -0
  167. package/dist/view-visibility.js +46 -0
  168. package/dist/view.d.ts +29 -1
  169. package/dist/view.js +57 -34
  170. package/package.json +23 -11
@@ -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";
@@ -32,7 +33,8 @@ function tonemapNode() {
32
33
  kind: "render",
33
34
  phase: RenderPhase.Tonemap,
34
35
  enabled: (view) => cameraOf(view) !== void 0,
35
- reads: ["post-hdr"],
36
+ // Bloom's chain, when the view blooms: the glow is composited here (see the 'bloom' node).
37
+ reads: ["post-hdr", "bloom"],
36
38
  writes: ["ldr"],
37
39
  color: [{ resource: "ldr", clear: { r: 0, g: 0, b: 0, a: 1 } }],
38
40
  run: (ctx) => {
@@ -42,7 +44,7 @@ function tonemapNode() {
42
44
  layoutGen = gpu.generation;
43
45
  layout = gpu.layouts.bindGroupLayout({
44
46
  label: "tonemap",
45
- entries: [tex(0, "unfilterable-float"), uniform(1), tex(2), sampler(3)]
47
+ entries: [tex(0, "unfilterable-float"), uniform(1), tex(2), sampler(3), tex(4)]
46
48
  });
47
49
  white = gpu.device.createTexture({
48
50
  label: "tonemap/no-lut",
@@ -53,15 +55,19 @@ function tonemapNode() {
53
55
  }
54
56
  const format = ctx.texture("ldr").format;
55
57
  const srgb = format.endsWith("-srgb");
58
+ const alpha = cam.alphaOutput;
59
+ const bloom = (cam.post.effects & PostEffect.Bloom) !== 0;
56
60
  const pipeline = cache.render(
57
61
  ctx,
58
- `tonemap/${format}`,
62
+ `tonemap/${format}${alpha ? "/alpha" : ""}${bloom ? "/bloom" : ""}`,
59
63
  "shard::post::tonemap",
60
64
  "fs",
61
65
  [layout],
62
66
  [{ format }],
63
67
  {
64
- SRGB_TARGET: srgb
68
+ SRGB_TARGET: srgb,
69
+ TRANSPARENT: alpha,
70
+ BLOOM: bloom
65
71
  }
66
72
  );
67
73
  if (!pipeline) return;
@@ -79,7 +85,7 @@ function tonemapNode() {
79
85
  flags |= 4;
80
86
  }
81
87
  }
82
- scratch.fill(0, 0, 32);
88
+ scratch.fill(0, 0, 36);
83
89
  u[0] = cam.curve;
84
90
  u[1] = cam.dither ? 1 : 0;
85
91
  u[2] = flags;
@@ -97,20 +103,30 @@ function tonemapNode() {
97
103
  scratch[26] = cam.width / Math.max(1, cam.height);
98
104
  scratch[28] = 1 / cam.width;
99
105
  scratch[29] = 1 / cam.height;
100
- const params = cache.buffer(gpu, `${ctx.view.name}/tonemap`, 128);
101
- params.write(scratch, 0, 0, 32);
106
+ const chain = bloom ? ctx.texture("bloom") : void 0;
107
+ if (chain) {
108
+ const b = post.bloom;
109
+ scratch[32] = b.intensity;
110
+ scratch[33] = chain.mipLevelCount;
111
+ scratch[34] = b.threshold > 0 ? 0 : 1;
112
+ }
113
+ const params = cache.buffer(gpu, `${ctx.view.name}/tonemap`, 144);
114
+ params.write(scratch, 0, 0, 36);
102
115
  const hdr = ctx.texture("post-hdr");
103
- const lutView = lut ?? white.createView();
104
116
  const group = cache.group(
105
117
  gpu,
106
118
  `${ctx.view.name}/tonemap`,
107
- `${idOf(hdr)}/${params.version}/${lut ? idOf(lut) : 0}`,
119
+ `${idOf(hdr)}/${params.version}/${lut ? idOf(lut) : 0}/${chain ? idOf(chain) : 0}`,
108
120
  layout,
109
121
  () => [
110
122
  { binding: 0, resource: hdr.createView() },
111
123
  { binding: 1, resource: { buffer: params.buffer } },
112
- { binding: 2, resource: lutView },
113
- { binding: 3, resource: cache.sampler(gpu) }
124
+ { binding: 2, resource: lut ?? white.createView() },
125
+ { binding: 3, resource: cache.sampler(gpu) },
126
+ {
127
+ binding: 4,
128
+ resource: chain ? chain.createView({ mipLevelCount: 1 }) : white.createView()
129
+ }
114
130
  ]
115
131
  );
116
132
  const pass = ctx.renderPass;
@@ -144,13 +160,15 @@ function upscaleNode() {
144
160
  const input = ctx.texture("display");
145
161
  const target = ctx.texture("view-target");
146
162
  const format = target.format;
163
+ const alpha = cameraOf(ctx.view).alphaOutput;
147
164
  const pipeline = cache.render(
148
165
  ctx,
149
- `upscale/${format}`,
166
+ `upscale/${format}${alpha ? "/alpha" : ""}`,
150
167
  "shard::post::upscale",
151
168
  "fs",
152
169
  [layout],
153
- [{ format }]
170
+ [{ format }],
171
+ { TRANSPARENT: alpha }
154
172
  );
155
173
  if (!pipeline) return;
156
174
  const settings = ctx.world.tryResource(RenderScale);
@@ -180,6 +198,12 @@ function upscaleNode() {
180
198
  function addDisplayNodes(world) {
181
199
  const graph = world.resource(Graph);
182
200
  graph.declare({ name: "display", format: "view" });
201
+ addRenderFeatures(world, {
202
+ name: "render/display",
203
+ description: "Tonemapping and the upscale to the display.",
204
+ nodes: ["tonemap", "post/upscale"],
205
+ baseline: { strategy: "The same fragment passes" }
206
+ });
183
207
  graph.addNode("tonemap", tonemapNode());
184
208
  graph.addNode("post/upscale", upscaleNode());
185
209
  }
@@ -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;
@@ -0,0 +1,106 @@
1
+ import { defineResource } from "@aethervtt/shard-core";
2
+ import { LogResource } from "@aethervtt/shard-runtime";
3
+ import { clearHealthIssue, raiseHealthIssue } from "./health.js";
4
+ const missingPlugins = /* @__PURE__ */ new WeakMap();
5
+ function warnFeatureMissing(world, what, plugin, outcome) {
6
+ let seen = missingPlugins.get(world);
7
+ if (!seen) {
8
+ seen = /* @__PURE__ */ new Set();
9
+ missingPlugins.set(world, seen);
10
+ }
11
+ if (seen.has(plugin)) return;
12
+ seen.add(plugin);
13
+ world.tryResource(LogResource)?.log("warn", `${what}, but ${plugin} isn't installed; ${outcome}`, {
14
+ code: "render/feature-missing",
15
+ hint: `Add ${plugin} from '@aethervtt/shard-render' (forwardPlugin includes it).`
16
+ });
17
+ }
18
+ const RenderFeatures = defineResource("render/Features", {
19
+ description: "Render features installed, the graph nodes each adds, and its baseline strategy.",
20
+ init: () => /* @__PURE__ */ new Map()
21
+ });
22
+ function addRenderFeatures(world, ...features) {
23
+ const registry = world.initResource(RenderFeatures);
24
+ for (const feature of features) registry.set(feature.name, feature);
25
+ }
26
+ function featureOfNode(world, node) {
27
+ for (const feature of world.tryResource(RenderFeatures)?.values() ?? []) {
28
+ if (feature.nodes.includes(node)) return feature;
29
+ }
30
+ return void 0;
31
+ }
32
+ function describeFeatures(world) {
33
+ return [...world.tryResource(RenderFeatures)?.values() ?? []].map((f) => ({
34
+ name: f.name,
35
+ nodes: [...f.nodes],
36
+ baseline: f.baseline === "unsupported" ? "unsupported" : f.baseline.strategy
37
+ }));
38
+ }
39
+ const unsupported = /* @__PURE__ */ new WeakMap();
40
+ function unsupportedNodes(world) {
41
+ const registry = world.initResource(RenderFeatures);
42
+ let cached = unsupported.get(registry);
43
+ if (!cached || cached.size !== registry.size) {
44
+ const nodes = /* @__PURE__ */ new Set();
45
+ for (const f of registry.values())
46
+ if (f.baseline === "unsupported") for (const n of f.nodes) nodes.add(n);
47
+ cached = { size: registry.size, nodes };
48
+ unsupported.set(registry, cached);
49
+ }
50
+ return cached.nodes;
51
+ }
52
+ function reportUnsupported(world, feature, message) {
53
+ raiseHealthIssue(world, {
54
+ code: "render/feature-unsupported",
55
+ severity: "degraded",
56
+ ref: feature,
57
+ message
58
+ });
59
+ }
60
+ function clearUnsupported(world, feature) {
61
+ clearHealthIssue(world, "render/feature-unsupported", feature);
62
+ }
63
+ const usage = /* @__PURE__ */ new WeakMap();
64
+ function checkUnsupportedFeatures(world) {
65
+ let state = usage.get(world);
66
+ if (!state) {
67
+ state = { queries: /* @__PURE__ */ new Map(), reported: /* @__PURE__ */ new Set() };
68
+ usage.set(world, state);
69
+ }
70
+ const queries = state.queries;
71
+ for (const f of world.initResource(RenderFeatures).values()) {
72
+ if (f.baseline !== "unsupported" || !f.components) continue;
73
+ let used = false;
74
+ for (const c of f.components) {
75
+ let q = queries.get(c);
76
+ if (!q) {
77
+ q = world.query({ with: [c] });
78
+ queries.set(c, q);
79
+ }
80
+ for (const table of q.tables) if (table.count > 0) used = true;
81
+ }
82
+ if (used === state.reported.has(f.name)) continue;
83
+ if (used) {
84
+ state.reported.add(f.name);
85
+ reportUnsupported(
86
+ world,
87
+ f.name,
88
+ `${f.name} (${f.description}) runs on the full tier only (WebGPU with compute and storage in shaders): it draws nothing here`
89
+ );
90
+ } else {
91
+ state.reported.delete(f.name);
92
+ clearUnsupported(world, f.name);
93
+ }
94
+ }
95
+ }
96
+ export {
97
+ RenderFeatures,
98
+ addRenderFeatures,
99
+ checkUnsupportedFeatures,
100
+ clearUnsupported,
101
+ describeFeatures,
102
+ featureOfNode,
103
+ reportUnsupported,
104
+ unsupportedNodes,
105
+ warnFeatureMissing
106
+ };
@@ -0,0 +1,64 @@
1
+ import { type Profiler } from '@aethervtt/shard-core';
2
+ /** The budget key foliage's GPU time is measured by: it covers `gpu:foliage/place`, `/cull`, `/draw`. */
3
+ export declare const FOLIAGE_SPAN_KEY = "gpu:foliage";
4
+ export interface FoliageBudgetValue {
5
+ /**
6
+ * GPU milliseconds foliage may take a frame. 0 (the default): its `gpu:foliage` slice of the
7
+ * app's scenario (`App.perfScenario`) on the detected machine, and with no scenario (or an
8
+ * unknown machine) no target: foliage draws at full detail.
9
+ */
10
+ ms: number;
11
+ /** The least detail it thins to, as a share of the instances drawn at full detail. Default 0.1. */
12
+ minDetail: number;
13
+ /** Read only: the target in force (ms), 0 when none. */
14
+ target: number;
15
+ /** Read only: foliage's GPU time a frame, smoothed (ms). */
16
+ measuredMs: number;
17
+ /**
18
+ * Read only: the share of full detail drawn (minDetail to 1). Density is its square root and range
19
+ * its fourth root, so instances drawn scale with it.
20
+ */
21
+ detail: number;
22
+ }
23
+ export declare const FoliageBudget: import("@aethervtt/shard-core").ResourceDef<FoliageBudgetValue>;
24
+ /**
25
+ * The controller: one GPU time a frame in, detail out. Smoothed (an exponential average), with
26
+ * hysteresis (it moves only above the target, or under 85% of it) and asymmetric steps (down 1% a
27
+ * sample, up 0.5%), so the lag of GPU timings (frames late) doesn't make it oscillate. Like
28
+ * `TerrainBudget.triangles`' LOD bias (0043), slow on purpose: density and range change gradually.
29
+ */
30
+ export declare class FoliageController {
31
+ detail: number;
32
+ /** Smoothed GPU time (ms); 0 before the first sample. */
33
+ smoothed: number;
34
+ private primed;
35
+ /** One frame's foliage GPU time against `target` (0: none, back to full detail at once). */
36
+ sample(ms: number, target: number, minDetail: number): void;
37
+ /** No target: full detail, and the smoothing starts over. */
38
+ reset(): void;
39
+ /** The share of instances kept by thinning (the cull's density factor). */
40
+ get density(): number;
41
+ /** The share of each layer's range drawn. */
42
+ get rangeScale(): number;
43
+ }
44
+ /**
45
+ * Reads the GPU time a budget key covers, one GPU frame at a time, without allocating: when
46
+ * `gpu:frame` has a new sample, the covered spans that got one with it are summed. Span names are
47
+ * matched by `spanCovers` as they're interned.
48
+ */
49
+ export declare class CoveredGpuTime {
50
+ readonly key: string;
51
+ /** The latest frame's covered time (ms), after `read` returned true. */
52
+ ms: number;
53
+ private ids;
54
+ private seen;
55
+ private count;
56
+ private scanned;
57
+ private frameId;
58
+ private frameRuns;
59
+ constructor(key: string);
60
+ /** True when a new GPU frame's timings landed since the last call; `ms` is then its covered time. */
61
+ read(profiler: Profiler): boolean;
62
+ /** New span names since the last scan: the ones the key covers join the list. */
63
+ private scan;
64
+ }