@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/target.js CHANGED
@@ -1,27 +1,3 @@
1
- class WindowTarget {
2
- label = "window";
3
- renderScale = true;
4
- gpu;
5
- constructor(gpu) {
6
- this.gpu = gpu;
7
- }
8
- get format() {
9
- return this.gpu.format;
10
- }
11
- get width() {
12
- return this.gpu.canvas?.width ?? 1;
13
- }
14
- get pixelRatio() {
15
- return this.gpu.pixelRatio;
16
- }
17
- get height() {
18
- return this.gpu.canvas?.height ?? 1;
19
- }
20
- texture() {
21
- this.gpu.resize();
22
- return this.gpu.context.getCurrentTexture();
23
- }
24
- }
25
1
  class OffscreenTarget {
26
2
  label;
27
3
  format;
@@ -65,6 +41,5 @@ class OffscreenTarget {
65
41
  }
66
42
  }
67
43
  export {
68
- OffscreenTarget,
69
- WindowTarget
44
+ OffscreenTarget
70
45
  };
package/dist/testing.d.ts CHANGED
@@ -32,3 +32,13 @@ export declare function compareGolden(dir: string, name: string, image: {
32
32
  height: number;
33
33
  data: Uint8Array;
34
34
  }): GoldenResult;
35
+ /**
36
+ * Watches a device for what the baseline tier (0064) must never ask for: compute passes, and
37
+ * storage buffers visible to the vertex or fragment stage. Counts from now on.
38
+ */
39
+ export declare function watchBaseline(gpu: import('@aethervtt/shard-gpu').GpuContext): {
40
+ computePasses: number;
41
+ renderStorage: string[];
42
+ };
43
+ /** Mean absolute difference of two images' bytes. */
44
+ export declare function meanDifference(a: ArrayLike<number>, b: ArrayLike<number>): number;
package/dist/testing.js CHANGED
@@ -116,11 +116,43 @@ function compareGolden(dir, name, image) {
116
116
  }
117
117
  return { mean: sum / expected.length, max, written: false };
118
118
  }
119
+ function watchBaseline(gpu) {
120
+ const found = { computePasses: 0, renderStorage: [] };
121
+ const device = gpu.device;
122
+ const createEncoder = device.createCommandEncoder.bind(device);
123
+ device.createCommandEncoder = (descriptor) => {
124
+ const encoder = createEncoder(descriptor);
125
+ const begin = encoder.beginComputePass.bind(encoder);
126
+ encoder.beginComputePass = (d) => {
127
+ found.computePasses++;
128
+ return begin(d);
129
+ };
130
+ return encoder;
131
+ };
132
+ const createLayout = device.createBindGroupLayout.bind(device);
133
+ device.createBindGroupLayout = (descriptor) => {
134
+ for (const e of descriptor.entries) {
135
+ const render = e.visibility & (GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT);
136
+ const t = e.buffer?.type;
137
+ if (render && (t === "storage" || t === "read-only-storage"))
138
+ found.renderStorage.push(`${descriptor.label ?? "?"}#${e.binding}`);
139
+ }
140
+ return createLayout(descriptor);
141
+ };
142
+ return found;
143
+ }
144
+ function meanDifference(a, b) {
145
+ let sum = 0;
146
+ for (let i = 0; i < a.length; i++) sum += Math.abs(a[i] - b[i]);
147
+ return sum / a.length;
148
+ }
119
149
  export {
120
150
  compareGolden,
121
151
  comparePng,
152
+ meanDifference,
122
153
  pixel,
123
154
  pngBytes,
124
155
  renderView,
125
- settle
156
+ settle,
157
+ watchBaseline
126
158
  };
package/dist/tier.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { GpuContext } from '@aethervtt/shard-gpu';
2
+ /**
3
+ * A texture's binding dimension, fixed at creation on baseline (compatibility mode and WebGL2):
4
+ * spread into the texture descriptor. Nothing on the full tier.
5
+ */
6
+ export declare function bindingDimension(gpu: GpuContext, dimension: GPUTextureViewDimension): {
7
+ textureBindingViewDimension?: GPUTextureViewDimension;
8
+ };
9
+ /**
10
+ * The layout entry for a depth texture read without comparison. On baseline the shader rewrite
11
+ * reads it as a float texture (naga can't load from depth), so it binds as unfilterable float.
12
+ */
13
+ export declare function depthReadEntry(gpu: GpuContext, binding: number, visibility: GPUShaderStageFlags, viewDimension?: GPUTextureViewDimension): GPUBindGroupLayoutEntry;
package/dist/tier.js ADDED
@@ -0,0 +1,14 @@
1
+ function bindingDimension(gpu, dimension) {
2
+ return gpu.tier === "baseline" ? { textureBindingViewDimension: dimension } : {};
3
+ }
4
+ function depthReadEntry(gpu, binding, visibility, viewDimension = "2d") {
5
+ return {
6
+ binding,
7
+ visibility,
8
+ texture: gpu.tier === "baseline" ? { sampleType: "unfilterable-float", viewDimension } : viewDimension === "2d" ? { sampleType: "depth" } : { sampleType: "depth", viewDimension }
9
+ };
10
+ }
11
+ export {
12
+ bindingDimension,
13
+ depthReadEntry
14
+ };
package/dist/timer.d.ts CHANGED
@@ -18,6 +18,8 @@ export declare class GpuTimer {
18
18
  private generation;
19
19
  private readonly gpu;
20
20
  constructor(gpu: GpuContext);
21
+ /** Destroys the query set; buffers are released with the rest of the app's (0052). */
22
+ destroy(): void;
21
23
  beginFrame(): void;
22
24
  /** Timestamp writes for the next pass, or undefined when timing is off or full. */
23
25
  allocate(name: string): GPURenderPassTimestampWrites | undefined;
package/dist/timer.js CHANGED
@@ -22,6 +22,12 @@ class GpuTimer {
22
22
  this.gpu = gpu;
23
23
  this.enabled = gpu.features.has("timestamp-query");
24
24
  }
25
+ /** Destroys the query set; buffers are released with the rest of the app's (0052). */
26
+ destroy() {
27
+ this.querySet?.destroy();
28
+ this.querySet = void 0;
29
+ this.generation = this.gpu.generation;
30
+ }
25
31
  beginFrame() {
26
32
  this.names = [];
27
33
  if (!this.enabled || this.generation === this.gpu.generation) return;
@@ -117,7 +123,7 @@ class GpuTimer {
117
123
  }
118
124
  if (b > a) profiler?.record(label, Number(b - a) / 1e6);
119
125
  }
120
- if (last > first) {
126
+ if (first !== 0n) {
121
127
  this.frameMs = Number(last - first) / 1e6;
122
128
  this.frameSamples++;
123
129
  profiler?.record("gpu:frame", this.frameMs);
@@ -0,0 +1,37 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import type { App } from '@aethervtt/shard-runtime';
3
+ /** A kind of manifest entry, and how to show the variants it names. */
4
+ export interface ShaderVariantSource {
5
+ /** The entry's key: `{ "dice": … }` is run by the source named `dice`. */
6
+ readonly name: string;
7
+ readonly description: string;
8
+ /**
9
+ * Shows every variant `entry` names for at least a frame, stepping the app until their pipelines
10
+ * are made (`app.update`, then `Shaders.whenIdle` and `gpu.pipelines.whenIdle`). Returns how many
11
+ * variants it showed.
12
+ */
13
+ show(app: App, entry: unknown): Promise<number>;
14
+ }
15
+ export declare const ShaderVariantSources: import("@aethervtt/shard-core").ResourceDef<Map<string, ShaderVariantSource>>;
16
+ /** Registers a manifest entry kind. Plugins call it in `build`. */
17
+ export declare function addShaderVariantSource(world: World, source: ShaderVariantSource): void;
18
+ /** The manifest's shape. */
19
+ export interface ShaderVariantManifest {
20
+ variants: Record<string, unknown>[];
21
+ }
22
+ /** Runs one manifest entry through its source. Throws `render/unknown-variant-source` for a key no plugin registered. */
23
+ export declare function showVariants(app: App, entry: Record<string, unknown>): Promise<number>;
24
+ /** A material asset's path (or guid) and parameter overrides. */
25
+ export interface MaterialVariantEntry {
26
+ path?: string;
27
+ guid?: string;
28
+ params?: Record<string, unknown>;
29
+ }
30
+ /**
31
+ * Shows what `spawn` makes to a camera of its own (an offscreen target, a shadowed sun), stepping
32
+ * the app until every pipeline it asked for is made, then takes it all away again. For variant
33
+ * sources: what's drawn is what a bake records.
34
+ */
35
+ export declare function showToCamera(app: App, spawn: (at: [number, number, number]) => Entity[]): Promise<void>;
36
+ /** `{ "material": { "path": …, "params": … } }`: a material asset, with its parameters set, on a cube. */
37
+ export declare const materialVariants: ShaderVariantSource;
@@ -0,0 +1,104 @@
1
+ import { assetServer } from "@aethervtt/shard-assets";
2
+ import {
3
+ defineResource,
4
+ ShardError
5
+ } from "@aethervtt/shard-core";
6
+ import { cube } from "@aethervtt/shard-mesh";
7
+ import { lookAt, Transform } from "@aethervtt/shard-transform";
8
+ import { Materials, Meshes, RenderTargets } from "./assets.js";
9
+ import { Camera3d } from "./camera.js";
10
+ import { Mesh3d, MeshMaterial } from "./instances.js";
11
+ import { DirectionalLight } from "./lights.js";
12
+ import { Gpu, Shaders } from "./plugin.js";
13
+ import { OffscreenTarget } from "./target.js";
14
+ const ShaderVariantSources = defineResource(
15
+ "render/ShaderVariantSources",
16
+ {
17
+ description: "Kinds of shaders.variants.json entries (0064) and how to show their variants, for `shard shaders bake`.",
18
+ init: () => /* @__PURE__ */ new Map()
19
+ }
20
+ );
21
+ function addShaderVariantSource(world, source) {
22
+ world.initResource(ShaderVariantSources).set(source.name, source);
23
+ }
24
+ function showVariants(app, entry) {
25
+ const keys = Object.keys(entry);
26
+ if (keys.length !== 1) {
27
+ throw new ShardError(
28
+ "render/variant-entry",
29
+ `A shaders.variants.json entry has one key, its source: got ${keys.length ? keys.join(", ") : "none"}`
30
+ );
31
+ }
32
+ const name = keys[0];
33
+ const source = app.world.tryResource(ShaderVariantSources)?.get(name);
34
+ if (!source) {
35
+ const known = [...app.world.tryResource(ShaderVariantSources)?.keys() ?? []];
36
+ throw new ShardError("render/unknown-variant-source", `No shader variant source "${name}"`, {
37
+ hint: `Known: ${known.join(", ") || "none"}. Plugins register theirs with addShaderVariantSource.`
38
+ });
39
+ }
40
+ return source.show(app, entry[name]);
41
+ }
42
+ const STAGE = [5e4, 0, 5e4];
43
+ async function showToCamera(app, spawn) {
44
+ const world = app.world;
45
+ const gpu = world.resource(Gpu);
46
+ const target = new OffscreenTarget(gpu, { label: "shader-variants", width: 64, height: 64 });
47
+ const ref = world.resource(RenderTargets).add(target, `render:variants/${STAGE.join(",")}`);
48
+ const eye = [STAGE[0], STAGE[1] + 2, STAGE[2] + 4];
49
+ const made = [
50
+ world.spawn(
51
+ [Camera3d, { target: ref, order: 90 }],
52
+ [Transform, { translation: eye, rotation: lookAt(eye, STAGE) }]
53
+ ),
54
+ world.spawn(
55
+ [DirectionalLight, { shadows: true }],
56
+ [Transform, { translation: STAGE, rotation: lookAt([0, 0, 0], [-0.4, -1, -0.3]) }]
57
+ ),
58
+ ...spawn(STAGE)
59
+ ];
60
+ try {
61
+ for (let i = 0; i < 8; i++) {
62
+ app.update(1 / 60);
63
+ await world.resource(Shaders).whenIdle();
64
+ await gpu.pipelines.whenIdle();
65
+ }
66
+ } finally {
67
+ for (const e of made) if (world.isAlive(e)) world.despawn(e);
68
+ app.update(1 / 60);
69
+ target.destroy?.();
70
+ }
71
+ }
72
+ const materialVariants = {
73
+ name: "material",
74
+ description: "A material asset by path or guid, with parameter overrides: { path, params? }.",
75
+ async show(app, entry) {
76
+ const list = Array.isArray(entry) ? entry : [entry];
77
+ const world = app.world;
78
+ for (const m of list) {
79
+ const ref = m.guid ? { guid: m.guid } : { path: m.path };
80
+ await assetServer(world).load(ref);
81
+ const material = world.resource(Materials).get(ref);
82
+ if (!material) {
83
+ throw new ShardError("render/variant-material", `No material ${m.guid ?? m.path}`);
84
+ }
85
+ if (m.params) material.set(m.params);
86
+ const mesh = world.resource(Meshes).add(cube({ size: 1 }));
87
+ await showToCamera(app, (at) => [
88
+ world.spawn(
89
+ [Mesh3d, { mesh }],
90
+ [MeshMaterial, { material: ref }],
91
+ [Transform, { translation: at }]
92
+ )
93
+ ]);
94
+ }
95
+ return list.length;
96
+ }
97
+ };
98
+ export {
99
+ ShaderVariantSources,
100
+ addShaderVariantSource,
101
+ materialVariants,
102
+ showToCamera,
103
+ showVariants
104
+ };
@@ -0,0 +1,61 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import type { GpuBuffer } from '@aethervtt/shard-gpu';
3
+ import { type HiddenSet } from './view-visibility.js';
4
+ /**
5
+ * Every camera's hidden set, one range of words each. The GPU culler reads them from a region of
6
+ * its LOD state buffer; only words that changed upload there.
7
+ */
8
+ export declare class HiddenSets {
9
+ /** Words per set: one bit per instance slot of the store's capacity. */
10
+ words: number;
11
+ data: Uint32Array<ArrayBuffer>;
12
+ readonly sets: Map<number, HiddenSet>;
13
+ /** Free set indices, reused before growing. */
14
+ private readonly free;
15
+ private count;
16
+ private dirty;
17
+ private dirtyLo;
18
+ private dirtyHi;
19
+ /** Every word uploads next time (a new layout, buffer, region or device). */
20
+ private all;
21
+ /** Where the last upload went: a buffer version (it bumps when replaced) and byte offset. */
22
+ private target;
23
+ private targetVersion;
24
+ private targetOffset;
25
+ /** Bytes uploaded by the last `upload`. */
26
+ uploadedBytes: number;
27
+ /** Resolves since start (tests: a still frame resolves nothing). */
28
+ resolves: number;
29
+ /** The set of `camera`, made on first use. */
30
+ setOf(camera: Entity): HiddenSet;
31
+ /** Frees a set: its words clear (and upload, so a reused index starts empty). */
32
+ drop(set: HiddenSet): void;
33
+ /** Makes room for `capacity` slots per set; a new width re-lays every set and uploads all. */
34
+ ensureSlots(capacity: number): void;
35
+ private layout;
36
+ private write;
37
+ /**
38
+ * Writes a set's new bits (from `next`, `words` long), marking only words that differ. Returns
39
+ * whether any did.
40
+ */
41
+ assign(set: HiddenSet, next: Uint32Array): boolean;
42
+ /**
43
+ * Uploads changed words in runs to `buffer` at `byteOffset`; everything when that's not where
44
+ * the last upload went, or the buffer was replaced (grown, or a new device).
45
+ */
46
+ upload(buffer: GpuBuffer, byteOffset: number): number;
47
+ }
48
+ /**
49
+ * Resolves each camera's ViewVisibility into its hidden set, after the instance slots are current.
50
+ * Lists resolve again only when they change, a slot is allocated or freed, or a ChildOf changes.
51
+ */
52
+ export declare const resolveViewVisibility: import("@aethervtt/shard-core").SystemDef<{
53
+ q: import("@aethervtt/shard-core").Query;
54
+ hierarchy: import("@aethervtt/shard-core").Query;
55
+ epoch: number;
56
+ frame: number;
57
+ }>;
58
+ /** render.describe's viewVisibility section: per camera, what it hides and the slots it resolves to. */
59
+ export declare function describeViewVisibility(world: World): Record<string, unknown>;
60
+ /** Per-view hiding (0070): cameras' ViewVisibility lists, resolved for their culls. */
61
+ export declare const viewVisibilityPlugin: import("@aethervtt/shard-runtime").Plugin;
@@ -0,0 +1,267 @@
1
+ import {
2
+ ChildOf,
3
+ Children,
4
+ defineSystem,
5
+ Last
6
+ } from "@aethervtt/shard-core";
7
+ import { definePlugin } from "@aethervtt/shard-runtime";
8
+ import { Camera3d } from "./camera.js";
9
+ import { addRenderFeatures } from "./features.js";
10
+ import { InstanceSlot, Instances, prepareInstances } from "./instances.js";
11
+ import { RenderDescribers, RenderSet } from "./plugin.js";
12
+ import { Cameras } from "./view.js";
13
+ import { HiddenSetsResource, ViewVisibility } from "./view-visibility.js";
14
+ class HiddenSets {
15
+ /** Words per set: one bit per instance slot of the store's capacity. */
16
+ words = 0;
17
+ data = new Uint32Array(0);
18
+ sets = /* @__PURE__ */ new Map();
19
+ /** Free set indices, reused before growing. */
20
+ free = [];
21
+ count = 0;
22
+ dirty = new Uint8Array(0);
23
+ dirtyLo = Number.POSITIVE_INFINITY;
24
+ dirtyHi = -1;
25
+ /** Every word uploads next time (a new layout, buffer, region or device). */
26
+ all = true;
27
+ /** Where the last upload went: a buffer version (it bumps when replaced) and byte offset. */
28
+ target;
29
+ targetVersion = -1;
30
+ targetOffset = -1;
31
+ /** Bytes uploaded by the last `upload`. */
32
+ uploadedBytes = 0;
33
+ /** Resolves since start (tests: a still frame resolves nothing). */
34
+ resolves = 0;
35
+ /** The set of `camera`, made on first use. */
36
+ setOf(camera) {
37
+ let set = this.sets.get(camera);
38
+ if (set) return set;
39
+ const index = this.free.pop() ?? this.count++;
40
+ if (index * this.words + this.words > this.data.length) this.layout(this.words, index + 1);
41
+ const base = index * this.words;
42
+ set = {
43
+ camera,
44
+ base,
45
+ bits: this.data.subarray(base, base + this.words),
46
+ slots: 0,
47
+ hide: [],
48
+ shadows: false,
49
+ frame: -1
50
+ };
51
+ this.sets.set(camera, set);
52
+ return set;
53
+ }
54
+ /** Frees a set: its words clear (and upload, so a reused index starts empty). */
55
+ drop(set) {
56
+ for (let w = 0; w < set.bits.length; w++) if (set.bits[w] !== 0) this.write(set.base + w, 0);
57
+ this.sets.delete(set.camera);
58
+ this.free.push(set.base / Math.max(1, this.words));
59
+ }
60
+ /** Makes room for `capacity` slots per set; a new width re-lays every set and uploads all. */
61
+ ensureSlots(capacity) {
62
+ const words = Math.ceil(capacity / 32);
63
+ if (words > this.words) this.layout(words, this.count);
64
+ }
65
+ layout(words, sets) {
66
+ const data = new Uint32Array(words * Math.max(1, sets * 2));
67
+ for (const set of this.sets.values()) {
68
+ const index = set.base / Math.max(1, this.words);
69
+ data.set(set.bits, index * words);
70
+ set.base = index * words;
71
+ }
72
+ this.data = data;
73
+ for (const set of this.sets.values()) set.bits = data.subarray(set.base, set.base + words);
74
+ this.words = words;
75
+ this.dirty = new Uint8Array(data.length);
76
+ this.all = true;
77
+ }
78
+ write(word, value) {
79
+ if (this.data[word] === value) return;
80
+ this.data[word] = value;
81
+ if (this.dirty[word]) return;
82
+ this.dirty[word] = 1;
83
+ if (word < this.dirtyLo) this.dirtyLo = word;
84
+ if (word > this.dirtyHi) this.dirtyHi = word;
85
+ }
86
+ /**
87
+ * Writes a set's new bits (from `next`, `words` long), marking only words that differ. Returns
88
+ * whether any did.
89
+ */
90
+ assign(set, next2) {
91
+ let changed = false;
92
+ for (let w = 0; w < set.bits.length; w++) {
93
+ const value = next2[w];
94
+ if (set.bits[w] !== value) {
95
+ this.write(set.base + w, value);
96
+ changed = true;
97
+ }
98
+ }
99
+ return changed;
100
+ }
101
+ /**
102
+ * Uploads changed words in runs to `buffer` at `byteOffset`; everything when that's not where
103
+ * the last upload went, or the buffer was replaced (grown, or a new device).
104
+ */
105
+ upload(buffer, byteOffset) {
106
+ buffer.ensureCapacity(byteOffset + this.data.byteLength);
107
+ void buffer.buffer;
108
+ if (buffer !== this.target || buffer.version !== this.targetVersion || byteOffset !== this.targetOffset) {
109
+ this.target = buffer;
110
+ this.targetVersion = buffer.version;
111
+ this.targetOffset = byteOffset;
112
+ this.all = true;
113
+ }
114
+ let bytes = 0;
115
+ if (this.all) {
116
+ this.all = false;
117
+ this.dirty.fill(0);
118
+ this.dirtyLo = Number.POSITIVE_INFINITY;
119
+ this.dirtyHi = -1;
120
+ if (this.data.length === 0) return 0;
121
+ buffer.write(this.data, byteOffset);
122
+ this.uploadedBytes = this.data.byteLength;
123
+ return this.uploadedBytes;
124
+ }
125
+ let start = -1;
126
+ for (let w = this.dirtyLo; w <= this.dirtyHi + 1; w++) {
127
+ if (w <= this.dirtyHi && this.dirty[w] === 1) {
128
+ this.dirty[w] = 0;
129
+ if (start < 0) start = w;
130
+ } else if (start >= 0) {
131
+ buffer.write(this.data, byteOffset + start * 4, start, w - start);
132
+ bytes += (w - start) * 4;
133
+ start = -1;
134
+ }
135
+ }
136
+ this.dirtyLo = Number.POSITIVE_INFINITY;
137
+ this.dirtyHi = -1;
138
+ this.uploadedBytes = bytes;
139
+ return bytes;
140
+ }
141
+ }
142
+ let next = new Uint32Array(0);
143
+ const stack = [];
144
+ function resolve(world, roots, out) {
145
+ out.fill(0);
146
+ let slots = 0;
147
+ stack.length = 0;
148
+ for (const e of roots) if (e !== null && e !== void 0) stack.push(e);
149
+ while (stack.length > 0) {
150
+ const e = stack.pop();
151
+ if (!world.isAlive(e)) continue;
152
+ const table = world.entityTable(e);
153
+ const row = world.entityRow(e);
154
+ if (table.has(InstanceSlot)) {
155
+ const slot = table.column(InstanceSlot, "slot")[row] - 1;
156
+ if (slot >= 0 && (out[slot >>> 5] & 1 << (slot & 31)) === 0) {
157
+ out[slot >>> 5] = out[slot >>> 5] | 1 << (slot & 31);
158
+ slots++;
159
+ }
160
+ }
161
+ if (table.has(Children)) {
162
+ const children = table.column(Children, "entities")[row];
163
+ if (children) {
164
+ for (const c of children) if (c !== null) stack.push(c);
165
+ }
166
+ }
167
+ }
168
+ return slots;
169
+ }
170
+ const resolveViewVisibility = defineSystem({
171
+ name: "render/resolve-view-visibility",
172
+ description: "Resolves cameras' ViewVisibility lists to hidden slot bitsets (0070).",
173
+ setup: (world) => ({
174
+ q: world.query({ with: [Camera3d, ViewVisibility] }),
175
+ hierarchy: world.query({ with: [ChildOf] }),
176
+ epoch: -1,
177
+ frame: 0
178
+ }),
179
+ run: (state, world, ctx) => {
180
+ const sets = world.tryResource(HiddenSetsResource);
181
+ if (!sets) return;
182
+ const cameras = world.resource(Cameras);
183
+ const store = world.resource(Instances);
184
+ const frame = ++state.frame;
185
+ let listed = 0;
186
+ for (const table of state.q.tables) listed += table.count;
187
+ if (listed === 0 && sets.sets.size === 0) return;
188
+ sets.ensureSlots(store.capacity);
189
+ if (next.length < sets.words) next = new Uint32Array(sets.words);
190
+ const since = ctx.lastRunTick;
191
+ let moved = store.slotEpoch !== state.epoch;
192
+ state.epoch = store.slotEpoch;
193
+ if (!moved) {
194
+ for (const table of state.hierarchy.tables) {
195
+ if (table.lastStructural > since || table.lastChanged(ChildOf) > since) {
196
+ moved = true;
197
+ break;
198
+ }
199
+ }
200
+ }
201
+ for (const table of state.q.tables) {
202
+ const hide = table.column(ViewVisibility, "hide");
203
+ const shadows = table.column(ViewVisibility, "shadows");
204
+ const changed = table.changedTicks(ViewVisibility);
205
+ for (let i = 0; i < table.count; i++) {
206
+ const camera = table.entities[i];
207
+ const fresh = !sets.sets.has(camera);
208
+ const set = sets.setOf(camera);
209
+ set.frame = frame;
210
+ const cam = cameras.get(camera);
211
+ if (cam) cam.hidden = set;
212
+ const hideShadows = shadows[i] === 1;
213
+ if (!fresh && !moved && changed[i] <= since && set.shadows === hideShadows) continue;
214
+ const list = hide[i] ?? [];
215
+ const words = next.subarray(0, sets.words);
216
+ set.slots = resolve(world, list, words);
217
+ set.hide = list.filter((e) => e !== null);
218
+ sets.resolves++;
219
+ if (sets.assign(set, words) && hideShadows || set.shadows !== hideShadows)
220
+ store.shadowEpoch++;
221
+ set.shadows = hideShadows;
222
+ }
223
+ }
224
+ if (sets.sets.size > listed) {
225
+ for (const set of sets.sets.values()) {
226
+ if (set.frame === frame) continue;
227
+ const cam = cameras.get(set.camera);
228
+ if (cam?.hidden === set) cam.hidden = void 0;
229
+ if (set.shadows) store.shadowEpoch++;
230
+ sets.drop(set);
231
+ }
232
+ }
233
+ }
234
+ });
235
+ function describeViewVisibility(world) {
236
+ const out = {};
237
+ for (const set of world.tryResource(HiddenSetsResource)?.sets.values() ?? [])
238
+ out[`camera:${set.camera}`] = {
239
+ hide: [...set.hide],
240
+ slots: set.slots,
241
+ shadows: set.shadows ? "hide" : "keep"
242
+ };
243
+ return out;
244
+ }
245
+ const viewVisibilityPlugin = definePlugin({
246
+ name: "render/view-visibility",
247
+ dependencies: ["render/forward"],
248
+ build(app) {
249
+ app.insertResource(HiddenSetsResource, new HiddenSets());
250
+ app.addSystems(Last, resolveViewVisibility.inSet(RenderSet.Prepare).after(prepareInstances));
251
+ },
252
+ ready(app) {
253
+ app.world.initResource(RenderDescribers).set("viewVisibility", describeViewVisibility);
254
+ addRenderFeatures(app.world, {
255
+ name: "render/view-visibility",
256
+ description: "Cameras' hidden lists (ViewVisibility).",
257
+ nodes: [],
258
+ baseline: { strategy: "CPU culls read the same bits" }
259
+ });
260
+ }
261
+ });
262
+ export {
263
+ HiddenSets,
264
+ describeViewVisibility,
265
+ resolveViewVisibility,
266
+ viewVisibilityPlugin
267
+ };
@@ -0,0 +1,28 @@
1
+ import { type Entity, type World } from '@aethervtt/shard-core';
2
+ import type { HiddenSets } from './view-visibility-plugin.js';
3
+ export declare const VIEW_SHADOWS: readonly ['keep', 'hide'];
4
+ export declare const ViewVisibility: import("@aethervtt/shard-core").ComponentDef<{
5
+ readonly hide: import("@aethervtt/shard-core").FieldType<(number | null)[], "object">;
6
+ readonly shadows: import("@aethervtt/shard-core").FieldType<"hide" | "keep", "u8">;
7
+ }>;
8
+ /** `hiddenBase` of a view with no hidden set. */
9
+ export declare const NO_HIDDEN = 4294967295;
10
+ /** One camera's hidden set: a bit per instance slot, in its range of `HiddenSets.data`. */
11
+ export interface HiddenSet {
12
+ camera: Entity;
13
+ /** Its first word in `HiddenSets.data` (and the GPU buffer). */
14
+ base: number;
15
+ /** Its words: bit `s & 31` of word `s >> 5` is slot `s`. */
16
+ bits: Uint32Array;
17
+ /** Slots it resolves to. */
18
+ slots: number;
19
+ /** The entities listed, as last resolved. */
20
+ hide: Entity[];
21
+ /** Its camera's cascades leave the slots out too. */
22
+ shadows: boolean;
23
+ /** The frame it was last seen on (sets of cameras that lose the component are freed). */
24
+ frame: number;
25
+ }
26
+ export declare const HiddenSetsResource: import("@aethervtt/shard-core").ResourceDef<HiddenSets>;
27
+ /** Warns once per world about a ViewVisibility without its plugin (render/feature-missing). */
28
+ export declare function observeViewVisibilityWithoutPlugin(world: World): void;