@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
package/dist/fxaa.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { definePlugin } from "@aethervtt/shard-runtime";
2
+ import { addRenderFeatures } from "./features.js";
2
3
  import { RenderPhase } from "./graph.js";
3
4
  import { Graph, Shaders } from "./plugin.js";
4
5
  import { hasEffect, PostEffect, PostFeatures } from "./post.js";
5
6
  import { idOf, PostCache, sampler, tex } from "./post-common.js";
6
7
  import { FXAA_SHADERS } from "./post-shaders.js";
7
8
  import { registerShaders } from "./shaders.js";
9
+ import { cameraOf } from "./view.js";
8
10
  function fxaaNode() {
9
11
  const cache = new PostCache();
10
12
  let layout;
@@ -24,15 +26,17 @@ function fxaaNode() {
24
26
  }
25
27
  const input = ctx.texture("ldr");
26
28
  const format = ctx.texture("display").format;
29
+ const alpha = cameraOf(ctx.view)?.alphaOutput === true;
27
30
  const pipeline = cache.render(
28
31
  ctx,
29
- `fxaa/${format}`,
32
+ `fxaa/${format}${alpha ? "/alpha" : ""}`,
30
33
  "shard::post::fxaa",
31
34
  "fs",
32
35
  [layout],
33
36
  [{ format }],
34
37
  {
35
- SRGB_TARGET: format.endsWith("-srgb")
38
+ SRGB_TARGET: format.endsWith("-srgb"),
39
+ TRANSPARENT: alpha
36
40
  }
37
41
  );
38
42
  if (!pipeline) return;
@@ -57,6 +61,12 @@ const fxaaPlugin = definePlugin({
57
61
  registerShaders(app.world.resource(Shaders), FXAA_SHADERS);
58
62
  const graph = app.world.resource(Graph);
59
63
  graph.declare({ name: "fxaa-in", format: "view" });
64
+ addRenderFeatures(app.world, {
65
+ name: "render/fxaa",
66
+ description: "Fast approximate anti-aliasing.",
67
+ nodes: ["post/fxaa"],
68
+ baseline: { strategy: "The same fragment pass" }
69
+ });
60
70
  graph.addNode("post/fxaa", fxaaNode());
61
71
  }
62
72
  });
@@ -2,6 +2,7 @@ import { First, Last } from "@aethervtt/shard-core";
2
2
  import { definePlugin } from "@aethervtt/shard-runtime";
3
3
  import { OriginShift } from "@aethervtt/shard-transform";
4
4
  import { GIZMO_SHADERS } from "./debug-shaders.js";
5
+ import { addRenderFeatures } from "./features.js";
5
6
  import { upload } from "./forward.js";
6
7
  import { beginGizmos, GizmoGpuResource, Gizmos, gizmoNode, uploadGizmos } from "./gizmos.js";
7
8
  import { DebugOverlays, drawOverlays, gridsOverlay } from "./overlays.js";
@@ -26,6 +27,12 @@ const gizmosPlugin = definePlugin({
26
27
  },
27
28
  ready(app) {
28
29
  registerShaders(app.world.resource(Shaders), GIZMO_SHADERS);
30
+ addRenderFeatures(app.world, {
31
+ name: "render/gizmos",
32
+ description: "Debug lines, shapes and labels.",
33
+ nodes: ["gizmos"],
34
+ baseline: { strategy: "Line and glyph records in data textures" }
35
+ });
29
36
  app.world.resource(Graph).addNode("gizmos", gizmoNode(app.world));
30
37
  }
31
38
  });
package/dist/gizmos.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type World } from '@aethervtt/shard-core';
2
- import { GpuBuffer } from '@aethervtt/shard-gpu';
2
+ import { DataStore } from './data-store.js';
3
3
  import { type NodeDescriptor } from './graph.js';
4
4
  /**
5
5
  * Immediate-mode debug drawing. Any system calls `world.resource(Gizmos).line(…)` and the line
@@ -127,8 +127,8 @@ export declare const Gizmos: import("@aethervtt/shard-core").ResourceDef<GizmoSt
127
127
  export declare const beginGizmos: import("@aethervtt/shard-core").SystemDef<undefined>;
128
128
  interface GizmoGpu {
129
129
  generation: number;
130
- lines: GpuBuffer;
131
- glyphs: GpuBuffer;
130
+ lines: DataStore;
131
+ glyphs: DataStore;
132
132
  atlas: GPUTexture;
133
133
  layouts: {
134
134
  view: GPUBindGroupLayout;
package/dist/gizmos.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { defineResource, defineSystem, mat4 } from "@aethervtt/shard-core";
2
- import { GpuBuffer } from "@aethervtt/shard-gpu";
3
2
  import { Time } from "@aethervtt/shard-runtime";
3
+ import { DataStore, dataEntry } from "./data-store.js";
4
4
  import { ForwardStateResource } from "./forward.js";
5
5
  import { LABEL_FONT, labelAtlas, labelWidth } from "./gizmo-font.js";
6
6
  import { RenderPhase } from "./graph.js";
7
7
  import { Gpu, Shaders } from "./plugin.js";
8
+ import { depthReadEntry } from "./tier.js";
8
9
  import { cameraOf } from "./view.js";
9
10
  const GIZMO_LINE_FLOATS = 8;
10
11
  const GLYPH_FLOATS = 12;
@@ -537,12 +538,9 @@ function gizmoGpu(world, gpu) {
537
538
  const glyphData = new Float32Array(GLYPH_FLOATS * 256);
538
539
  r.gpu = {
539
540
  generation: gpu.generation,
540
- lines: new GpuBuffer(gpu, { label: "gizmos/lines", usage: GPUBufferUsage.STORAGE, size: 4096 }),
541
- glyphs: new GpuBuffer(gpu, {
542
- label: "gizmos/glyphs",
543
- usage: GPUBufferUsage.STORAGE,
544
- size: 4096
545
- }),
541
+ // Read by the vertex stage: data textures on baseline (0064).
542
+ lines: new DataStore(gpu, { label: "gizmos/lines", size: 4096 }),
543
+ glyphs: new DataStore(gpu, { label: "gizmos/glyphs", size: 4096 }),
546
544
  atlas,
547
545
  layouts: {
548
546
  view: gpu.layouts.bindGroupLayout({
@@ -552,9 +550,9 @@ function gizmoGpu(world, gpu) {
552
550
  data: gpu.layouts.bindGroupLayout({
553
551
  label: "gizmos/data",
554
552
  entries: [
555
- { binding: 0, visibility: V, buffer: { type: "read-only-storage" } },
556
- { binding: 1, visibility: V, buffer: { type: "read-only-storage" } },
557
- { binding: 2, visibility: F, texture: { sampleType: "depth" } },
553
+ dataEntry(gpu, 0, V),
554
+ dataEntry(gpu, 1, V),
555
+ depthReadEntry(gpu, 2, F),
558
556
  { binding: 3, visibility: F, texture: { sampleType: "float" } }
559
557
  ]
560
558
  })
@@ -733,11 +731,11 @@ function gizmoNode(world) {
733
731
  gpu,
734
732
  r,
735
733
  `data/${ctx.view.name}`,
736
- `${idOf(r.lines.buffer)}/${idOf(r.glyphs.buffer)}/${idOf(depth)}/${idOf(r.atlas)}`,
734
+ `${r.lines.version}/${r.glyphs.version}/${idOf(depth)}/${idOf(r.atlas)}`,
737
735
  r.layouts.data,
738
736
  () => [
739
- { binding: 0, resource: { buffer: r.lines.buffer } },
740
- { binding: 1, resource: { buffer: r.glyphs.buffer } },
737
+ { binding: 0, resource: r.lines.resource() },
738
+ { binding: 1, resource: r.glyphs.resource() },
741
739
  { binding: 2, resource: depth.createView() },
742
740
  { binding: 3, resource: r.atlas.createView() }
743
741
  ]
@@ -1,10 +1,11 @@
1
+ import { type AssetServer } from '@aethervtt/shard-assets';
1
2
  import { type World } from '@aethervtt/shard-core';
2
3
  import type { GpuContext } from '@aethervtt/shard-gpu';
3
- import type { Mesh } from '@aethervtt/shard-mesh';
4
- import { type Texture } from '@aethervtt/shard-texture';
5
- import { type MaterialAsset } from './assets.js';
4
+ import { Mesh } from '@aethervtt/shard-mesh';
5
+ import { Texture } from '@aethervtt/shard-texture';
6
+ import { MaterialAsset } from './assets.js';
6
7
  import type { MaterialType } from './materials.js';
7
- import type { GpuMemoryData } from './stats.js';
8
+ import type { FrameCounts, GpuMemoryData } from './stats.js';
8
9
  export interface GpuMesh {
9
10
  version: number;
10
11
  generation: number;
@@ -48,6 +49,8 @@ export interface GpuTexture {
48
49
  version: number;
49
50
  generation: number;
50
51
  bytes: number;
52
+ /** Frees the texture and anything made from it (baseline twins); else `texture.destroy()`. */
53
+ destroy?(): void;
51
54
  }
52
55
  /**
53
56
  * GPU copies of meshes, textures, and materials, shared by every pass that draws them (forward,
@@ -56,6 +59,8 @@ export interface GpuTexture {
56
59
  */
57
60
  export declare class GpuAssets {
58
61
  readonly meshes: Map<Mesh, GpuMesh>;
62
+ /** Where mesh uploads are counted (`RenderStats.current.meshesRebuilt`), when set. */
63
+ counts: FrameCounts | undefined;
59
64
  readonly materials: Map<MaterialAsset, GpuMaterial>;
60
65
  readonly textures: Map<Texture, GpuTexture>;
61
66
  private defaults;
@@ -75,10 +80,25 @@ export declare class GpuAssets {
75
80
  readonly gpu: GpuContext;
76
81
  readonly memory: GpuMemoryData;
77
82
  materialLayout: GPUBindGroupLayout;
83
+ /**
84
+ * The baseline tier's texture strategy (0064), set on a baseline device: one format and one
85
+ * binding dimension per texture, and twins for slots that read it otherwise.
86
+ */
87
+ baseline: typeof import('./baseline/textures.js') | undefined;
88
+ /** Uploaded this frame on baseline: their bytes go at the next frame (twins take them first). */
89
+ private readonly releaseLater;
90
+ /** Asset guids of textures, for reloading one a twin needs. */
91
+ private readonly guids;
92
+ private world;
78
93
  constructor(gpu: GpuContext, memory: GpuMemoryData);
79
94
  /** Call once per frame before drawing: rebuilds after device loss and resets readiness. */
80
95
  beginFrame(): void;
81
96
  mesh(mesh: Mesh): GpuMesh;
97
+ /**
98
+ * Writes a CPU mesh's new data into the buffers of its previous version, when each fits and the
99
+ * mesh is still indexed the same way. Returns false when it needs new buffers.
100
+ */
101
+ private rewrite;
82
102
  private destroyMesh;
83
103
  /** Index buffers shared by GPU meshes, per index array. */
84
104
  private readonly sharedIndexBuffers;
@@ -87,8 +107,23 @@ export declare class GpuAssets {
87
107
  * normals, uvs, uvs1, tangents; f32), and the index buffer every mesh with the same array shares.
88
108
  */
89
109
  private gpuMesh;
110
+ /**
111
+ * Frees GPU copies of assets as the asset server drops them: unloaded (an owner released, 0061),
112
+ * or a fallback replaced by the real asset. Returns the unsubscribe.
113
+ */
114
+ watch(server: AssetServer): () => void;
115
+ /** The GPU objects made for one asset object (a texture, mesh or material): owners.describe (0061). */
116
+ objectsOf(item: unknown): {
117
+ buffers: number;
118
+ textures: number;
119
+ bytes: number;
120
+ };
90
121
  /** Frees a mesh's GPU buffers now (they're made again if it's drawn later). */
91
122
  releaseMesh(mesh: Mesh): void;
123
+ /** Frees a texture's GPU copy now (it's uploaded again if it's used later). */
124
+ releaseTexture(texture: Texture): void;
125
+ /** Frees a material's uniform buffers now (they're made again if it's drawn later). */
126
+ releaseMaterial(material: MaterialAsset): void;
92
127
  /** Uploads a texture (all levels) on first use and when its version changes. */
93
128
  texture(texture: Texture): GpuTexture | undefined;
94
129
  /** 1x1 white and flat-normal textures for empty slots. */
@@ -105,6 +140,25 @@ export declare class GpuAssets {
105
140
  private resolveSlots;
106
141
  /** Whether a texture can be uploaded; if its pixels are gone (device loss), reloads it. */
107
142
  private available;
143
+ private twins;
144
+ /** What baseline textures ask of this asset layer: reloads, fallbacks, and twin accounting. */
145
+ private twinHost;
146
+ /**
147
+ * Baseline (0064), `render.describe → twins`: the twins waiting for their bytes and how long
148
+ * they've waited, and the last ones made with their waits (0 when made from kept bytes).
149
+ */
150
+ describeTwins(): {
151
+ pending: {
152
+ texture: string;
153
+ kind: string;
154
+ waitedMs: number;
155
+ }[];
156
+ recent: {
157
+ texture: string;
158
+ kind: string;
159
+ waitedMs: number;
160
+ }[];
161
+ };
108
162
  /** The group-1 layout of a material type on this device. */
109
163
  layoutOf(type: MaterialType): GPUBindGroupLayout;
110
164
  /** The material's uniforms and bind group, uploaded when its version or textures change. */
@@ -1,11 +1,14 @@
1
- import { assetServer } from "@aethervtt/shard-assets";
1
+ import { AssetServerResource, assetServer } from "@aethervtt/shard-assets";
2
2
  import { defineResource } from "@aethervtt/shard-core";
3
- import { FORMAT_INFO, Textures } from "@aethervtt/shard-texture";
4
- import { STANDARD_TYPE, TEXTURE_SLOTS } from "./assets.js";
3
+ import { Mesh } from "@aethervtt/shard-mesh";
4
+ import { FORMAT_INFO, Texture, Textures } from "@aethervtt/shard-texture";
5
+ import { MaterialAsset, STANDARD_TYPE, TEXTURE_SLOTS } from "./assets.js";
5
6
  import { materialLayout } from "./shaders.js";
6
7
  const SRGB_SLOT = [true, false, false, false, true];
7
8
  class GpuAssets {
8
9
  meshes = /* @__PURE__ */ new Map();
10
+ /** Where mesh uploads are counted (`RenderStats.current.meshesRebuilt`), when set. */
11
+ counts;
9
12
  materials = /* @__PURE__ */ new Map();
10
13
  textures = /* @__PURE__ */ new Map();
11
14
  defaults;
@@ -31,6 +34,16 @@ class GpuAssets {
31
34
  gpu;
32
35
  memory;
33
36
  materialLayout;
37
+ /**
38
+ * The baseline tier's texture strategy (0064), set on a baseline device: one format and one
39
+ * binding dimension per texture, and twins for slots that read it otherwise.
40
+ */
41
+ baseline;
42
+ /** Uploaded this frame on baseline: their bytes go at the next frame (twins take them first). */
43
+ releaseLater = [];
44
+ /** Asset guids of textures, for reloading one a twin needs. */
45
+ guids = /* @__PURE__ */ new WeakMap();
46
+ world;
34
47
  constructor(gpu, memory) {
35
48
  this.gpu = gpu;
36
49
  this.memory = memory;
@@ -40,6 +53,11 @@ class GpuAssets {
40
53
  /** Call once per frame before drawing: rebuilds after device loss and resets readiness. */
41
54
  beginFrame() {
42
55
  this.ready.clear();
56
+ for (let i = 0; i < this.releaseLater.length; i++) {
57
+ const t = this.releaseLater[i];
58
+ if (!t.keepCpu) t.levels = void 0;
59
+ }
60
+ this.releaseLater.length = 0;
43
61
  if (this.generation === this.gpu.generation) return;
44
62
  this.generation = this.gpu.generation;
45
63
  this.materialLayout = createMaterialLayout(this.gpu);
@@ -56,7 +74,11 @@ class GpuAssets {
56
74
  if (mesh.gpu) gm.count = mesh.drawCount;
57
75
  return gm;
58
76
  }
59
- if (gm && gm.generation === gpu.generation) this.destroyMesh(gm);
77
+ if (this.counts) this.counts.meshesRebuilt++;
78
+ if (gm && gm.generation === gpu.generation) {
79
+ if (!mesh.gpu && this.rewrite(mesh, gm)) return gm;
80
+ this.destroyMesh(gm);
81
+ }
60
82
  if (mesh.gpu) return this.gpuMesh(mesh);
61
83
  const device = gpu.device;
62
84
  const n = mesh.vertexCount;
@@ -97,6 +119,42 @@ class GpuAssets {
97
119
  this.meshes.set(mesh, gm);
98
120
  return gm;
99
121
  }
122
+ /**
123
+ * Writes a CPU mesh's new data into the buffers of its previous version, when each fits and the
124
+ * mesh is still indexed the same way. Returns false when it needs new buffers.
125
+ */
126
+ rewrite(mesh, gm) {
127
+ if (gm.sharedVertices || gm.sharedIndices) return false;
128
+ const indices = mesh.indices;
129
+ if (indices === void 0 !== (gm.indices === void 0)) return false;
130
+ const n = mesh.vertexCount;
131
+ const fits = (buffer, bytes) => buffer.size >= bytes;
132
+ if (!fits(gm.positions, n * 12) || !fits(gm.normals, n * 12) || !fits(gm.uvs, n * 8) || !fits(gm.uvs1, n * 8) || !fits(gm.tangents, n * 16) || indices !== void 0 && !fits(gm.indices, indices.byteLength + 3 & ~3))
133
+ return false;
134
+ const queue = this.gpu.device.queue;
135
+ const write = (buffer, data) => {
136
+ if (data.length > 0) queue.writeBuffer(buffer, 0, data, 0, data.length);
137
+ };
138
+ write(gm.positions, mesh.positions);
139
+ write(gm.normals, mesh.normals ?? new Float32Array(n * 3).map((_, i) => i % 3 === 1 ? 1 : 0));
140
+ const uvs = mesh.uvs ?? new Float32Array(n * 2);
141
+ write(gm.uvs, uvs);
142
+ write(gm.uvs1, mesh.uvs1 ?? uvs);
143
+ write(gm.tangents, mesh.tangents ?? new Float32Array(n * 4));
144
+ if (indices) {
145
+ let data = indices;
146
+ if (data instanceof Uint16Array && data.length % 2 === 1) {
147
+ const padded = new Uint16Array(data.length + 1);
148
+ padded.set(data);
149
+ data = padded;
150
+ }
151
+ write(gm.indices, data);
152
+ gm.indexFormat = indices instanceof Uint32Array ? "uint32" : "uint16";
153
+ }
154
+ gm.version = mesh.version;
155
+ gm.count = mesh.drawCount;
156
+ return true;
157
+ }
100
158
  destroyMesh(gm) {
101
159
  if (gm.sharedVertices) return;
102
160
  gm.positions.destroy();
@@ -155,6 +213,46 @@ class GpuAssets {
155
213
  this.meshes.set(mesh, gm);
156
214
  return gm;
157
215
  }
216
+ /**
217
+ * Frees GPU copies of assets as the asset server drops them: unloaded (an owner released, 0061),
218
+ * or a fallback replaced by the real asset. Returns the unsubscribe.
219
+ */
220
+ watch(server) {
221
+ return server.onUnload((_, item) => {
222
+ if (item instanceof Texture) this.releaseTexture(item);
223
+ else if (item instanceof Mesh) this.releaseMesh(item);
224
+ else if (item instanceof MaterialAsset) this.releaseMaterial(item);
225
+ });
226
+ }
227
+ /** The GPU objects made for one asset object (a texture, mesh or material): owners.describe (0061). */
228
+ objectsOf(item) {
229
+ const out = { buffers: 0, textures: 0, bytes: 0 };
230
+ const gpu = this.gpu;
231
+ if (item instanceof Texture) {
232
+ const gt = this.textures.get(item);
233
+ if (gt && gt.generation === gpu.generation) {
234
+ out.textures = 1;
235
+ out.bytes = gt.bytes;
236
+ }
237
+ } else if (item instanceof Mesh) {
238
+ const gm = this.meshes.get(item);
239
+ if (gm && gm.generation === gpu.generation && !gm.sharedVertices) {
240
+ const buffers = [gm.positions, gm.normals, gm.uvs, gm.uvs1, gm.tangents];
241
+ if (gm.indices && !gm.sharedIndices) buffers.push(gm.indices);
242
+ out.buffers = buffers.length;
243
+ for (const b of buffers) out.bytes += b.size;
244
+ }
245
+ } else if (item instanceof MaterialAsset) {
246
+ const gm = this.materials.get(item);
247
+ if (gm && gm.generation === gpu.generation) {
248
+ const buffers = [gm.buffer, gm.textureBuffer];
249
+ if (gm.ownBuffer) buffers.push(gm.ownBuffer);
250
+ out.buffers = buffers.length;
251
+ for (const b of buffers) out.bytes += b.size;
252
+ }
253
+ }
254
+ return out;
255
+ }
158
256
  /** Frees a mesh's GPU buffers now (they're made again if it's drawn later). */
159
257
  releaseMesh(mesh) {
160
258
  const gm = this.meshes.get(mesh);
@@ -162,6 +260,30 @@ class GpuAssets {
162
260
  if (gm.generation === this.gpu.generation) this.destroyMesh(gm);
163
261
  this.meshes.delete(mesh);
164
262
  }
263
+ /** Frees a texture's GPU copy now (it's uploaded again if it's used later). */
264
+ releaseTexture(texture) {
265
+ const gt = this.textures.get(texture);
266
+ if (!gt) return;
267
+ if (gt.generation === this.gpu.generation) {
268
+ if (gt.destroy) gt.destroy();
269
+ else gt.texture.destroy();
270
+ this.memory.textures--;
271
+ this.memory.textureBytes -= gt.bytes;
272
+ }
273
+ this.textures.delete(texture);
274
+ }
275
+ /** Frees a material's uniform buffers now (they're made again if it's drawn later). */
276
+ releaseMaterial(material) {
277
+ const gm = this.materials.get(material);
278
+ if (!gm) return;
279
+ if (gm.generation === this.gpu.generation) {
280
+ gm.buffer.destroy();
281
+ gm.textureBuffer.destroy();
282
+ gm.ownBuffer?.destroy();
283
+ }
284
+ this.materials.delete(material);
285
+ this.ready.delete(material);
286
+ }
165
287
  /** Uploads a texture (all levels) on first use and when its version changes. */
166
288
  texture(texture) {
167
289
  const gpu = this.gpu;
@@ -174,7 +296,16 @@ class GpuAssets {
174
296
  this.memory.textures--;
175
297
  this.memory.textureBytes -= existing.bytes;
176
298
  }
177
- existing?.texture.destroy();
299
+ if (existing?.destroy) existing.destroy();
300
+ else existing?.texture.destroy();
301
+ if (this.baseline) {
302
+ const out2 = new this.baseline.BaselineTexture(this.twinHost(), texture);
303
+ this.textures.set(texture, out2);
304
+ this.memory.textures++;
305
+ this.memory.textureBytes += out2.bytes;
306
+ if (!texture.keepCpu) this.releaseLater.push(texture);
307
+ return out2;
308
+ }
178
309
  const info = FORMAT_INFO[texture.format];
179
310
  const srgbView = texture.gpuOnly ? void 0 : info.srgbView;
180
311
  const layers = texture.faces * texture.layers;
@@ -229,6 +360,14 @@ class GpuAssets {
229
360
  defaultTextures() {
230
361
  const gpu = this.gpu;
231
362
  if (this.defaults && this.defaults.white.generation === gpu.generation) return this.defaults;
363
+ if (this.baseline) {
364
+ this.defaults = {
365
+ white: this.baseline.solidTexture(this.twinHost(), [255, 255, 255, 255]),
366
+ normal: this.baseline.solidTexture(this.twinHost(), [128, 128, 255, 255])
367
+ };
368
+ this.samplers.clear();
369
+ return this.defaults;
370
+ }
232
371
  const solid = (label, rgba) => {
233
372
  const texture = gpu.device.createTexture({
234
373
  label,
@@ -289,6 +428,7 @@ class GpuAssets {
289
428
  return ready;
290
429
  }
291
430
  resolveSlots(world, material) {
431
+ this.world = world;
292
432
  const store = world.tryResource(Textures);
293
433
  material.sync();
294
434
  const type = material.type;
@@ -300,7 +440,7 @@ class GpuAssets {
300
440
  this.ownTextures[i] = void 0;
301
441
  continue;
302
442
  }
303
- const texture = store?.get(ref);
443
+ const texture = store?.get(ref) ?? missingTexture(world, ref);
304
444
  if (!texture || !this.available(world, texture, ref)) return false;
305
445
  this.ownTextures[i] = texture;
306
446
  }
@@ -312,14 +452,22 @@ class GpuAssets {
312
452
  this.slotTextures[i] = void 0;
313
453
  continue;
314
454
  }
315
- const texture = store?.get(ref);
455
+ const texture = store?.get(ref) ?? missingTexture(world, ref);
316
456
  if (!texture || !this.available(world, texture, ref)) return false;
317
457
  this.slotTextures[i] = texture;
318
458
  }
459
+ if (this.baseline && material.value.deferUntilReady) {
460
+ for (let i = 0; i < 5; i++) {
461
+ const texture = this.slotTextures[i];
462
+ const g = texture ? this.texture(texture) : void 0;
463
+ if (g && g.waiting(SRGB_SLOT[i], false)) return false;
464
+ }
465
+ }
319
466
  return true;
320
467
  }
321
468
  /** Whether a texture can be uploaded; if its pixels are gone (device loss), reloads it. */
322
469
  available(world, texture, ref) {
470
+ if (ref.guid) this.guids.set(texture, ref.guid);
323
471
  if (texture.levels || this.textures.has(texture)) return true;
324
472
  if (ref.guid && !this.reloading.has(ref.guid)) {
325
473
  this.reloading.add(ref.guid);
@@ -328,6 +476,34 @@ class GpuAssets {
328
476
  }
329
477
  return false;
330
478
  }
479
+ twins;
480
+ /** What baseline textures ask of this asset layer: reloads, fallbacks, and twin accounting. */
481
+ twinHost() {
482
+ const assets = this;
483
+ this.twins ??= new this.baseline.Twins({
484
+ gpu: this.gpu,
485
+ get world() {
486
+ return assets.world;
487
+ },
488
+ guidOf: (texture) => this.guids.get(texture),
489
+ reload: (texture) => {
490
+ const guid = this.guids.get(texture);
491
+ const world = this.world;
492
+ if (!guid || !world || this.reloading.has(guid)) return;
493
+ this.reloading.add(guid);
494
+ void assetServer(world).reload(guid).finally(() => this.reloading.delete(guid));
495
+ },
496
+ white: () => this.defaultTextures().white
497
+ });
498
+ return this.twins;
499
+ }
500
+ /**
501
+ * Baseline (0064), `render.describe → twins`: the twins waiting for their bytes and how long
502
+ * they've waited, and the last ones made with their waits (0 when made from kept bytes).
503
+ */
504
+ describeTwins() {
505
+ return this.twins?.describe() ?? { pending: [], recent: [] };
506
+ }
331
507
  /** The group-1 layout of a material type on this device. */
332
508
  layoutOf(type) {
333
509
  return type.bindGroupLayout(this.gpu, standardEntries());
@@ -451,7 +627,7 @@ class GpuAssets {
451
627
  });
452
628
  } else {
453
629
  const g = gm.ownBound[i] ?? defaults.white;
454
- entries.push({ binding: binding++, resource: g.linear });
630
+ entries.push({ binding: binding++, resource: type.colors.has(name) ? g.srgb : g.linear });
455
631
  }
456
632
  entries.push({ binding: binding++, resource: this.sampler("repeat", "linear") });
457
633
  }
@@ -484,6 +660,13 @@ function standardEntries() {
484
660
  function createMaterialLayout(gpu) {
485
661
  return STANDARD_TYPE.bindGroupLayout(gpu, standardEntries());
486
662
  }
663
+ function missingTexture(world, ref) {
664
+ if (ref.guid === void 0) return void 0;
665
+ const server = world.tryResource(AssetServerResource);
666
+ if (!server || server.entry(ref.guid)) return void 0;
667
+ server.missing(ref, "Texture");
668
+ return world.tryResource(Textures)?.byGuid(ref.guid);
669
+ }
487
670
  const GpuAssetsResource = defineResource("render/GpuAssets", {
488
671
  description: "GPU copies of meshes, textures, and materials."
489
672
  });
package/dist/graph.d.ts CHANGED
@@ -103,8 +103,14 @@ export declare const RenderPhase: {
103
103
  readonly Opaque: 300;
104
104
  readonly Lighting: 350;
105
105
  readonly Sky: 400;
106
+ /** Coplanar ground bands (0057): after the sky, which draws where no depth was written. */
107
+ readonly Ground: 420;
106
108
  readonly Sprites: 450;
107
109
  readonly Transparent: 500;
110
+ /** Projected fog's composite (0058): after transparent objects, before the overlay band. */
111
+ readonly Fog: 520;
112
+ /** Ground bands from the overlay band up, over fog (0058). */
113
+ readonly Overlay3d: 530;
108
114
  readonly Effects3d: 550;
109
115
  readonly Resolve: 600;
110
116
  readonly Post: 700;
@@ -164,15 +170,20 @@ export declare class RenderGraph {
164
170
  private lastViews;
165
171
  private readonly viewData;
166
172
  constructor(gpu: GpuContext);
173
+ /** Rejects pending captures and destroys the timer's query set (the app is being disposed). */
174
+ dispose(): void;
167
175
  addNode(name: string, node: NodeDescriptor): void;
168
176
  removeNode(name: string): void;
169
177
  hasNode(name: string): boolean;
178
+ /** Every node's name, in the order they were added. */
179
+ nodeNames(): string[];
170
180
  /** The data of a view rendered in the last frame. */
171
181
  lastViewData(view: string): Record<string, unknown> | undefined;
172
182
  /** Declares a transient texture any node can write, read, or attach by name. */
173
183
  declare(texture: TransientTexture): void;
174
184
  /** The resolution with every node enabled and no aliases. */
175
185
  resolve(): ResolvedGraph;
186
+ /** `off`: nodes that don't run on this device (features the baseline tier doesn't support). */
176
187
  private resolveFor;
177
188
  /** Whether a node after `k` loads or reads `resource` (so its contents must be stored). */
178
189
  private loadedLater;