@aethervtt/shard-render 0.0.1
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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/assets.d.ts +104 -0
- package/dist/assets.js +213 -0
- package/dist/atmosphere-model.d.ts +96 -0
- package/dist/atmosphere-model.js +370 -0
- package/dist/atmosphere-nodes.d.ts +55 -0
- package/dist/atmosphere-nodes.js +780 -0
- package/dist/atmosphere-plugin.d.ts +5 -0
- package/dist/atmosphere-plugin.js +42 -0
- package/dist/atmosphere-shaders.d.ts +7 -0
- package/dist/atmosphere-shaders.js +595 -0
- package/dist/atmosphere-state.d.ts +72 -0
- package/dist/atmosphere-state.js +18 -0
- package/dist/atmosphere.d.ts +191 -0
- package/dist/atmosphere.js +950 -0
- package/dist/camera.d.ts +83 -0
- package/dist/camera.js +163 -0
- package/dist/clusters.d.ts +68 -0
- package/dist/clusters.js +288 -0
- package/dist/culling-shaders.d.ts +2 -0
- package/dist/culling-shaders.js +203 -0
- package/dist/culling.d.ts +84 -0
- package/dist/culling.js +498 -0
- package/dist/debug-shaders.d.ts +3 -0
- package/dist/debug-shaders.js +207 -0
- package/dist/debug-views.d.ts +60 -0
- package/dist/debug-views.js +197 -0
- package/dist/deferred-plugin.d.ts +5 -0
- package/dist/deferred-plugin.js +20 -0
- package/dist/deferred-shaders.d.ts +2 -0
- package/dist/deferred-shaders.js +151 -0
- package/dist/deferred.d.ts +22 -0
- package/dist/deferred.js +296 -0
- package/dist/deform.d.ts +80 -0
- package/dist/deform.js +213 -0
- package/dist/describe-plugin.d.ts +5 -0
- package/dist/describe-plugin.js +19 -0
- package/dist/display-nodes.d.ts +5 -0
- package/dist/display-nodes.js +189 -0
- package/dist/dynamic-resolution.d.ts +47 -0
- package/dist/dynamic-resolution.js +191 -0
- package/dist/environment-plugin.d.ts +6 -0
- package/dist/environment-plugin.js +44 -0
- package/dist/environment-shaders.d.ts +2 -0
- package/dist/environment-shaders.js +260 -0
- package/dist/environment-state.d.ts +71 -0
- package/dist/environment-state.js +144 -0
- package/dist/environment.d.ts +70 -0
- package/dist/environment.js +529 -0
- package/dist/forward.d.ts +123 -0
- package/dist/forward.js +1216 -0
- package/dist/fxaa.d.ts +2 -0
- package/dist/fxaa.js +65 -0
- package/dist/gizmo-font.d.ts +21 -0
- package/dist/gizmo-font.js +132 -0
- package/dist/gizmos-plugin.d.ts +5 -0
- package/dist/gizmos-plugin.js +34 -0
- package/dist/gizmos.d.ts +156 -0
- package/dist/gizmos.js +772 -0
- package/dist/gpu-assets.d.ts +114 -0
- package/dist/gpu-assets.js +494 -0
- package/dist/graph.d.ts +204 -0
- package/dist/graph.js +417 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +540 -0
- package/dist/instances.d.ts +349 -0
- package/dist/instances.js +1335 -0
- package/dist/large-world.d.ts +13 -0
- package/dist/large-world.js +20 -0
- package/dist/lights.d.ts +160 -0
- package/dist/lights.js +468 -0
- package/dist/material-noise.d.ts +19 -0
- package/dist/material-noise.js +7 -0
- package/dist/material-pipelines.d.ts +46 -0
- package/dist/material-pipelines.js +166 -0
- package/dist/materials.d.ts +108 -0
- package/dist/materials.js +219 -0
- package/dist/noise.d.ts +76 -0
- package/dist/noise.js +286 -0
- package/dist/overlay-registry.d.ts +18 -0
- package/dist/overlay-registry.js +30 -0
- package/dist/overlays.d.ts +35 -0
- package/dist/overlays.js +344 -0
- package/dist/paths.d.ts +20 -0
- package/dist/paths.js +46 -0
- package/dist/picking-plugin.d.ts +2 -0
- package/dist/picking-plugin.js +20 -0
- package/dist/picking.d.ts +94 -0
- package/dist/picking.js +456 -0
- package/dist/pixel-perfect-plugin.d.ts +9 -0
- package/dist/pixel-perfect-plugin.js +135 -0
- package/dist/pixel-perfect.d.ts +27 -0
- package/dist/pixel-perfect.js +70 -0
- package/dist/plugin.d.ts +111 -0
- package/dist/plugin.js +182 -0
- package/dist/pool.d.ts +19 -0
- package/dist/pool.js +43 -0
- package/dist/post-common.d.ts +51 -0
- package/dist/post-common.js +148 -0
- package/dist/post-nodes.d.ts +39 -0
- package/dist/post-nodes.js +849 -0
- package/dist/post-plugin.d.ts +5 -0
- package/dist/post-plugin.js +26 -0
- package/dist/post-shaders.d.ts +8 -0
- package/dist/post-shaders.js +698 -0
- package/dist/post.d.ts +175 -0
- package/dist/post.js +453 -0
- package/dist/readback.d.ts +4 -0
- package/dist/readback.js +95 -0
- package/dist/render-scale.d.ts +53 -0
- package/dist/render-scale.js +63 -0
- package/dist/shaders.d.ts +122 -0
- package/dist/shaders.js +1168 -0
- package/dist/shadows.d.ts +119 -0
- package/dist/shadows.js +562 -0
- package/dist/skin-asset.d.ts +26 -0
- package/dist/skin-asset.js +25 -0
- package/dist/skinning-plugin.d.ts +5 -0
- package/dist/skinning-plugin.js +16 -0
- package/dist/skinning.d.ts +19 -0
- package/dist/skinning.js +343 -0
- package/dist/standard-fields.d.ts +67 -0
- package/dist/standard-fields.js +73 -0
- package/dist/standard.d.ts +7 -0
- package/dist/standard.js +37 -0
- package/dist/stats.d.ts +25 -0
- package/dist/stats.js +18 -0
- package/dist/target.d.ts +55 -0
- package/dist/target.js +70 -0
- package/dist/testing.d.ts +34 -0
- package/dist/testing.js +126 -0
- package/dist/timer.d.ts +28 -0
- package/dist/timer.js +136 -0
- package/dist/view.d.ts +117 -0
- package/dist/view.js +389 -0
- package/dist/visibility.d.ts +10 -0
- package/dist/visibility.js +84 -0
- package/package.json +49 -0
package/dist/stats.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineResource } from "@aethervtt/shard-core";
|
|
2
|
+
const RenderStats = defineResource("render/Stats", {
|
|
3
|
+
description: "Per-view counts from the last frame: visible, culled, hidden, pending, draw calls.",
|
|
4
|
+
init: () => /* @__PURE__ */ new Map()
|
|
5
|
+
});
|
|
6
|
+
const GpuMemory = defineResource("render/GpuMemory", {
|
|
7
|
+
description: "GPU memory used by uploaded textures.",
|
|
8
|
+
init: () => ({ textures: 0, textureBytes: 0 })
|
|
9
|
+
});
|
|
10
|
+
const RenderCounters = defineResource("render/Counters", {
|
|
11
|
+
description: "Running totals since start: TAA history resets and floating-origin shifts followed.",
|
|
12
|
+
init: () => ({ taaResets: 0, originShifts: 0 })
|
|
13
|
+
});
|
|
14
|
+
export {
|
|
15
|
+
GpuMemory,
|
|
16
|
+
RenderCounters,
|
|
17
|
+
RenderStats
|
|
18
|
+
};
|
package/dist/target.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { GpuContext } from '@aethervtt/shard-gpu';
|
|
2
|
+
/** Where a view renders: the window's swapchain or an offscreen texture. */
|
|
3
|
+
export interface RenderTarget {
|
|
4
|
+
readonly label: string;
|
|
5
|
+
readonly format: GPUTextureFormat;
|
|
6
|
+
readonly width: number;
|
|
7
|
+
readonly height: number;
|
|
8
|
+
/** Cameras showing on it render at the RenderScale and are upscaled (0051). The window does. */
|
|
9
|
+
readonly renderScale?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Its pixels per CSS pixel (the display's devicePixelRatio for the window; default 1). Screen
|
|
12
|
+
* densities in "pixels" (terrain LOD, the MSAA default) are measured in CSS pixels (0051).
|
|
13
|
+
*/
|
|
14
|
+
readonly pixelRatio?: number;
|
|
15
|
+
/** The texture to render into this frame. */
|
|
16
|
+
texture(): GPUTexture;
|
|
17
|
+
}
|
|
18
|
+
/** The canvas swapchain. Resizes with the canvas. */
|
|
19
|
+
export declare class WindowTarget implements RenderTarget {
|
|
20
|
+
readonly label = "window";
|
|
21
|
+
readonly renderScale = true;
|
|
22
|
+
private readonly gpu;
|
|
23
|
+
constructor(gpu: GpuContext);
|
|
24
|
+
get format(): GPUTextureFormat;
|
|
25
|
+
get width(): number;
|
|
26
|
+
get pixelRatio(): number;
|
|
27
|
+
get height(): number;
|
|
28
|
+
texture(): GPUTexture;
|
|
29
|
+
}
|
|
30
|
+
export interface OffscreenTargetOptions {
|
|
31
|
+
label: string;
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
format?: GPUTextureFormat;
|
|
35
|
+
/** Follow the RenderScale like the window (0051). Default false. */
|
|
36
|
+
renderScale?: boolean;
|
|
37
|
+
/** Pixels per CSS pixel, for a target standing in for a high-density display. Default 1. */
|
|
38
|
+
pixelRatio?: number;
|
|
39
|
+
}
|
|
40
|
+
/** A texture render target: headless runs, screenshots, render-to-texture, second cameras. */
|
|
41
|
+
export declare class OffscreenTarget implements RenderTarget {
|
|
42
|
+
readonly label: string;
|
|
43
|
+
readonly format: GPUTextureFormat;
|
|
44
|
+
readonly renderScale: boolean;
|
|
45
|
+
readonly pixelRatio: number;
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
private current;
|
|
49
|
+
private generation;
|
|
50
|
+
private readonly gpu;
|
|
51
|
+
constructor(gpu: GpuContext, options: OffscreenTargetOptions);
|
|
52
|
+
resize(width: number, height: number): void;
|
|
53
|
+
texture(): GPUTexture;
|
|
54
|
+
destroy(): void;
|
|
55
|
+
}
|
package/dist/target.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
class OffscreenTarget {
|
|
26
|
+
label;
|
|
27
|
+
format;
|
|
28
|
+
renderScale;
|
|
29
|
+
pixelRatio;
|
|
30
|
+
width;
|
|
31
|
+
height;
|
|
32
|
+
current;
|
|
33
|
+
generation = -1;
|
|
34
|
+
gpu;
|
|
35
|
+
constructor(gpu, options) {
|
|
36
|
+
this.gpu = gpu;
|
|
37
|
+
this.label = options.label;
|
|
38
|
+
this.width = options.width;
|
|
39
|
+
this.height = options.height;
|
|
40
|
+
this.format = options.format ?? "rgba8unorm";
|
|
41
|
+
this.renderScale = options.renderScale ?? false;
|
|
42
|
+
this.pixelRatio = options.pixelRatio ?? 1;
|
|
43
|
+
}
|
|
44
|
+
resize(width, height) {
|
|
45
|
+
if (width === this.width && height === this.height) return;
|
|
46
|
+
this.width = width;
|
|
47
|
+
this.height = height;
|
|
48
|
+
this.current?.destroy();
|
|
49
|
+
this.current = void 0;
|
|
50
|
+
}
|
|
51
|
+
texture() {
|
|
52
|
+
if (!this.current || this.generation !== this.gpu.generation) {
|
|
53
|
+
this.generation = this.gpu.generation;
|
|
54
|
+
this.current = this.gpu.device.createTexture({
|
|
55
|
+
label: this.label,
|
|
56
|
+
size: [this.width, this.height],
|
|
57
|
+
format: this.format,
|
|
58
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return this.current;
|
|
62
|
+
}
|
|
63
|
+
destroy() {
|
|
64
|
+
this.current?.destroy();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
OffscreenTarget,
|
|
69
|
+
WindowTarget
|
|
70
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { App } from '@aethervtt/shard-runtime';
|
|
2
|
+
import type { CapturedImage } from './graph.js';
|
|
3
|
+
/** Renders frames until shaders and pipelines are compiled and nothing is skipped. */
|
|
4
|
+
export declare function settle(app: App, frames?: number): Promise<void>;
|
|
5
|
+
/** Settles, then captures a view. */
|
|
6
|
+
export declare function renderView(app: App, view: string): Promise<CapturedImage>;
|
|
7
|
+
export declare function pixel(image: {
|
|
8
|
+
width: number;
|
|
9
|
+
data: ArrayLike<number>;
|
|
10
|
+
}, x: number, y: number): number[];
|
|
11
|
+
/** RGBA8 → PNG bytes (sync, zlib), for debugging output. */
|
|
12
|
+
export declare function pngBytes(data: Uint8Array, width: number, height: number): Uint8Array;
|
|
13
|
+
/** Expected | actual | difference × 4, scaled up by `scale` (nearest). */
|
|
14
|
+
export declare function comparePng(expected: Uint8Array, image: {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
data: Uint8Array;
|
|
18
|
+
}, scale: number): Uint8Array;
|
|
19
|
+
export interface GoldenResult {
|
|
20
|
+
/** Mean absolute difference per channel, in 8-bit levels. */
|
|
21
|
+
mean: number;
|
|
22
|
+
max: number;
|
|
23
|
+
/** Whether the golden was just written (first run or SHARD_UPDATE_GOLDEN). */
|
|
24
|
+
written: boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compares an image with `<dir>/__golden__/<name>.rgba`. Writes it when missing (or when
|
|
28
|
+
* SHARD_UPDATE_GOLDEN is set), so a new golden appears on the first run.
|
|
29
|
+
*/
|
|
30
|
+
export declare function compareGolden(dir: string, name: string, image: {
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
data: Uint8Array;
|
|
34
|
+
}): GoldenResult;
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { deflateSync } from "node:zlib";
|
|
4
|
+
import { Culler } from "./culling.js";
|
|
5
|
+
import { captureView, Gpu, Shaders } from "./plugin.js";
|
|
6
|
+
async function settle(app, frames = 40) {
|
|
7
|
+
const gpu = app.world.resource(Gpu);
|
|
8
|
+
for (let i = 0; i < frames; i++) {
|
|
9
|
+
app.update(1 / 60);
|
|
10
|
+
await app.world.resource(Shaders).whenIdle();
|
|
11
|
+
await gpu.pipelines.whenIdle();
|
|
12
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
13
|
+
if (i >= 3 && gpu.pipelines.skipped === 0 && gpu.pipelines.pending === 0) {
|
|
14
|
+
await app.world.tryResource(Culler)?.whenIdle();
|
|
15
|
+
app.update(1 / 60);
|
|
16
|
+
await gpu.pipelines.whenIdle();
|
|
17
|
+
if (gpu.pipelines.skipped === 0) return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function renderView(app, view) {
|
|
22
|
+
await settle(app);
|
|
23
|
+
const shot = captureView(app.world, view);
|
|
24
|
+
app.update(1 / 60);
|
|
25
|
+
return shot;
|
|
26
|
+
}
|
|
27
|
+
function pixel(image, x, y) {
|
|
28
|
+
const o = (y * image.width + x) * 4;
|
|
29
|
+
return [image.data[o], image.data[o + 1], image.data[o + 2], image.data[o + 3]];
|
|
30
|
+
}
|
|
31
|
+
const CRC = new Uint32Array(256).map((_, n) => {
|
|
32
|
+
let c = n;
|
|
33
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
34
|
+
return c >>> 0;
|
|
35
|
+
});
|
|
36
|
+
function pngBytes(data, width, height) {
|
|
37
|
+
const raw = Buffer.alloc((width * 4 + 1) * height);
|
|
38
|
+
for (let y = 0; y < height; y++) {
|
|
39
|
+
raw[y * (width * 4 + 1)] = 0;
|
|
40
|
+
Buffer.from(data.buffer, data.byteOffset + y * width * 4, width * 4).copy(
|
|
41
|
+
raw,
|
|
42
|
+
y * (width * 4 + 1) + 1
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const crc = (b) => {
|
|
46
|
+
let c = 4294967295;
|
|
47
|
+
for (const x of b) c = CRC[(c ^ x) & 255] ^ c >>> 8;
|
|
48
|
+
return (c ^ 4294967295) >>> 0;
|
|
49
|
+
};
|
|
50
|
+
const chunk = (type, body) => {
|
|
51
|
+
const len = Buffer.alloc(4);
|
|
52
|
+
len.writeUInt32BE(body.length);
|
|
53
|
+
const td = Buffer.concat([Buffer.from(type), body]);
|
|
54
|
+
const c = Buffer.alloc(4);
|
|
55
|
+
c.writeUInt32BE(crc(td));
|
|
56
|
+
return Buffer.concat([len, td, c]);
|
|
57
|
+
};
|
|
58
|
+
const ihdr = Buffer.alloc(13);
|
|
59
|
+
ihdr.writeUInt32BE(width, 0);
|
|
60
|
+
ihdr.writeUInt32BE(height, 4);
|
|
61
|
+
ihdr[8] = 8;
|
|
62
|
+
ihdr[9] = 6;
|
|
63
|
+
return Buffer.concat([
|
|
64
|
+
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
65
|
+
chunk("IHDR", ihdr),
|
|
66
|
+
chunk("IDAT", deflateSync(raw)),
|
|
67
|
+
chunk("IEND", Buffer.alloc(0))
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
function comparePng(expected, image, scale) {
|
|
71
|
+
const { width, height } = image;
|
|
72
|
+
const gap = 2;
|
|
73
|
+
const W = (width * 3 + gap * 2) * scale;
|
|
74
|
+
const H = height * scale;
|
|
75
|
+
const out = new Uint8Array(W * H * 4);
|
|
76
|
+
for (let y = 0; y < H; y++) {
|
|
77
|
+
for (let x = 0; x < W; x++) {
|
|
78
|
+
const panel = Math.floor(x / scale / (width + gap));
|
|
79
|
+
const px = Math.floor(x / scale) - panel * (width + gap);
|
|
80
|
+
const o = (y * W + x) * 4;
|
|
81
|
+
out[o + 3] = 255;
|
|
82
|
+
if (px >= width) continue;
|
|
83
|
+
const s = (Math.floor(y / scale) * width + px) * 4;
|
|
84
|
+
for (let c = 0; c < 3; c++) {
|
|
85
|
+
const e = expected[s + c];
|
|
86
|
+
const a = image.data[s + c];
|
|
87
|
+
out[o + c] = panel === 0 ? e : panel === 1 ? a : Math.min(255, Math.abs(e - a) * 4);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return pngBytes(out, W, H);
|
|
92
|
+
}
|
|
93
|
+
function compareGolden(dir, name, image) {
|
|
94
|
+
const file = join(dir, "__golden__", `${name}.rgba`);
|
|
95
|
+
const out = process.env.SHARD_GOLDEN_OUT;
|
|
96
|
+
if (out) {
|
|
97
|
+
mkdirSync(out, { recursive: true });
|
|
98
|
+
writeFileSync(join(out, `${name}.png`), pngBytes(image.data, image.width, image.height));
|
|
99
|
+
}
|
|
100
|
+
if (!existsSync(file) || process.env.SHARD_UPDATE_GOLDEN) {
|
|
101
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
102
|
+
writeFileSync(file, image.data);
|
|
103
|
+
return { mean: 0, max: 0, written: true };
|
|
104
|
+
}
|
|
105
|
+
const expected = new Uint8Array(readFileSync(file));
|
|
106
|
+
if (out && expected.length === image.data.length) {
|
|
107
|
+
writeFileSync(join(out, `${name}.compare.png`), comparePng(expected, image, 4));
|
|
108
|
+
}
|
|
109
|
+
if (expected.length !== image.data.length) return { mean: 255, max: 255, written: false };
|
|
110
|
+
let sum = 0;
|
|
111
|
+
let max = 0;
|
|
112
|
+
for (let i = 0; i < expected.length; i++) {
|
|
113
|
+
const d = Math.abs(expected[i] - image.data[i]);
|
|
114
|
+
sum += d;
|
|
115
|
+
if (d > max) max = d;
|
|
116
|
+
}
|
|
117
|
+
return { mean: sum / expected.length, max, written: false };
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
compareGolden,
|
|
121
|
+
comparePng,
|
|
122
|
+
pixel,
|
|
123
|
+
pngBytes,
|
|
124
|
+
renderView,
|
|
125
|
+
settle
|
|
126
|
+
};
|
package/dist/timer.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type World } from '@aethervtt/shard-core';
|
|
2
|
+
import type { GpuContext } from '@aethervtt/shard-gpu';
|
|
3
|
+
/**
|
|
4
|
+
* Per-pass GPU timings with `timestamp-query`. Each render/compute pass writes begin and end
|
|
5
|
+
* timestamps; results are copied to a small ring of readback buffers and recorded into the profiler
|
|
6
|
+
* as `gpu:<node>` a frame or two later. Without the feature, everything here is a no-op.
|
|
7
|
+
*/
|
|
8
|
+
export declare class GpuTimer {
|
|
9
|
+
readonly enabled: boolean;
|
|
10
|
+
/** The latest frame's GPU span in ms (first pass start to last pass end), once one landed. */
|
|
11
|
+
frameMs: number;
|
|
12
|
+
/** Frames measured so far: changes when `frameMs` does. */
|
|
13
|
+
frameSamples: number;
|
|
14
|
+
private querySet;
|
|
15
|
+
private resolveBuffer;
|
|
16
|
+
private readonly readbacks;
|
|
17
|
+
private names;
|
|
18
|
+
private generation;
|
|
19
|
+
private readonly gpu;
|
|
20
|
+
constructor(gpu: GpuContext);
|
|
21
|
+
beginFrame(): void;
|
|
22
|
+
/** Timestamp writes for the next pass, or undefined when timing is off or full. */
|
|
23
|
+
allocate(name: string): GPURenderPassTimestampWrites | undefined;
|
|
24
|
+
private pendingCopy;
|
|
25
|
+
resolve(encoder: GPUCommandEncoder): void;
|
|
26
|
+
/** Call after submit. Records timings when the copy lands. */
|
|
27
|
+
readback(world: World): void;
|
|
28
|
+
}
|
package/dist/timer.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { ProfilerResource } from "@aethervtt/shard-core";
|
|
2
|
+
const MAX_PASSES = 96;
|
|
3
|
+
const STALE_NS = 1000000000n;
|
|
4
|
+
const SPANS = [
|
|
5
|
+
["gpu:span/post", (n) => n.startsWith("post/") || n === "tonemap"],
|
|
6
|
+
["gpu:span/ssao", (n) => n === "ssao" || n.startsWith("ssao/")]
|
|
7
|
+
];
|
|
8
|
+
const READBACK_BUFFERS = 3;
|
|
9
|
+
class GpuTimer {
|
|
10
|
+
enabled;
|
|
11
|
+
/** The latest frame's GPU span in ms (first pass start to last pass end), once one landed. */
|
|
12
|
+
frameMs = 0;
|
|
13
|
+
/** Frames measured so far: changes when `frameMs` does. */
|
|
14
|
+
frameSamples = 0;
|
|
15
|
+
querySet;
|
|
16
|
+
resolveBuffer;
|
|
17
|
+
readbacks = [];
|
|
18
|
+
names = [];
|
|
19
|
+
generation = -1;
|
|
20
|
+
gpu;
|
|
21
|
+
constructor(gpu) {
|
|
22
|
+
this.gpu = gpu;
|
|
23
|
+
this.enabled = gpu.features.has("timestamp-query");
|
|
24
|
+
}
|
|
25
|
+
beginFrame() {
|
|
26
|
+
this.names = [];
|
|
27
|
+
if (!this.enabled || this.generation === this.gpu.generation) return;
|
|
28
|
+
this.generation = this.gpu.generation;
|
|
29
|
+
const device = this.gpu.device;
|
|
30
|
+
this.querySet = device.createQuerySet({
|
|
31
|
+
label: "gpu-timer",
|
|
32
|
+
type: "timestamp",
|
|
33
|
+
count: MAX_PASSES * 2
|
|
34
|
+
});
|
|
35
|
+
this.resolveBuffer = device.createBuffer({
|
|
36
|
+
label: "gpu-timer/resolve",
|
|
37
|
+
size: MAX_PASSES * 2 * 8,
|
|
38
|
+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC
|
|
39
|
+
});
|
|
40
|
+
this.readbacks.length = 0;
|
|
41
|
+
for (let i = 0; i < READBACK_BUFFERS; i++) {
|
|
42
|
+
this.readbacks.push({
|
|
43
|
+
buffer: device.createBuffer({
|
|
44
|
+
label: `gpu-timer/readback-${i}`,
|
|
45
|
+
size: MAX_PASSES * 2 * 8,
|
|
46
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
47
|
+
}),
|
|
48
|
+
busy: false,
|
|
49
|
+
names: []
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Timestamp writes for the next pass, or undefined when timing is off or full. */
|
|
54
|
+
allocate(name) {
|
|
55
|
+
if (!this.enabled || !this.querySet || this.names.length >= MAX_PASSES) return void 0;
|
|
56
|
+
const i = this.names.length;
|
|
57
|
+
this.names.push(name);
|
|
58
|
+
return {
|
|
59
|
+
querySet: this.querySet,
|
|
60
|
+
beginningOfPassWriteIndex: i * 2,
|
|
61
|
+
endOfPassWriteIndex: i * 2 + 1
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
pendingCopy;
|
|
65
|
+
resolve(encoder) {
|
|
66
|
+
this.pendingCopy = void 0;
|
|
67
|
+
if (!this.enabled || this.names.length === 0) return;
|
|
68
|
+
const target = this.readbacks.find((r) => !r.busy);
|
|
69
|
+
if (!target) return;
|
|
70
|
+
const count = this.names.length * 2;
|
|
71
|
+
encoder.resolveQuerySet(this.querySet, 0, count, this.resolveBuffer, 0);
|
|
72
|
+
encoder.copyBufferToBuffer(this.resolveBuffer, 0, target.buffer, 0, count * 8);
|
|
73
|
+
target.busy = true;
|
|
74
|
+
target.names = this.names;
|
|
75
|
+
this.pendingCopy = target;
|
|
76
|
+
}
|
|
77
|
+
/** Call after submit. Records timings when the copy lands. */
|
|
78
|
+
readback(world) {
|
|
79
|
+
const target = this.pendingCopy;
|
|
80
|
+
if (!target) return;
|
|
81
|
+
this.pendingCopy = void 0;
|
|
82
|
+
const generation = this.generation;
|
|
83
|
+
target.buffer.mapAsync(GPUMapMode.READ).then(
|
|
84
|
+
() => {
|
|
85
|
+
if (generation !== this.gpu.generation) return;
|
|
86
|
+
const times = new BigUint64Array(target.buffer.getMappedRange());
|
|
87
|
+
const profiler = world.tryResource(ProfilerResource);
|
|
88
|
+
const totals = /* @__PURE__ */ new Map();
|
|
89
|
+
let latest = 0n;
|
|
90
|
+
for (let i = 0; i < target.names.length; i++) {
|
|
91
|
+
const end = times[i * 2 + 1];
|
|
92
|
+
if (end > latest) latest = end;
|
|
93
|
+
}
|
|
94
|
+
const stale = latest - STALE_NS;
|
|
95
|
+
let first = 0n;
|
|
96
|
+
let last = 0n;
|
|
97
|
+
for (let i = 0; i < target.names.length; i++) {
|
|
98
|
+
const begin = times[i * 2];
|
|
99
|
+
const end = times[i * 2 + 1];
|
|
100
|
+
if (begin === 0n || end < begin || begin < stale) continue;
|
|
101
|
+
const name = target.names[i];
|
|
102
|
+
totals.set(name, (totals.get(name) ?? 0) + Number(end - begin) / 1e6);
|
|
103
|
+
if (first === 0n || begin < first) first = begin;
|
|
104
|
+
if (end > last) last = end;
|
|
105
|
+
}
|
|
106
|
+
for (const [name, ms] of totals) profiler?.record(`gpu:${name}`, ms);
|
|
107
|
+
for (let g = 0; g < SPANS.length; g++) {
|
|
108
|
+
const [label, match] = SPANS[g];
|
|
109
|
+
let a = 0n;
|
|
110
|
+
let b = 0n;
|
|
111
|
+
for (let i = 0; i < target.names.length; i++) {
|
|
112
|
+
const begin = times[i * 2];
|
|
113
|
+
const end = times[i * 2 + 1];
|
|
114
|
+
if (begin === 0n || end < begin || begin < stale || !match(target.names[i])) continue;
|
|
115
|
+
if (a === 0n || begin < a) a = begin;
|
|
116
|
+
if (end > b) b = end;
|
|
117
|
+
}
|
|
118
|
+
if (b > a) profiler?.record(label, Number(b - a) / 1e6);
|
|
119
|
+
}
|
|
120
|
+
if (last > first) {
|
|
121
|
+
this.frameMs = Number(last - first) / 1e6;
|
|
122
|
+
this.frameSamples++;
|
|
123
|
+
profiler?.record("gpu:frame", this.frameMs);
|
|
124
|
+
}
|
|
125
|
+
target.buffer.unmap();
|
|
126
|
+
target.busy = false;
|
|
127
|
+
},
|
|
128
|
+
() => {
|
|
129
|
+
target.busy = false;
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
GpuTimer
|
|
136
|
+
};
|
package/dist/view.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { type Entity } from '@aethervtt/shard-core';
|
|
2
|
+
import type { RenderView } from './graph.js';
|
|
3
|
+
import { type DrawList } from './instances.js';
|
|
4
|
+
import { type PixelPerfectLayout } from './pixel-perfect.js';
|
|
5
|
+
import { type PostSettings } from './post.js';
|
|
6
|
+
export declare const TONEMAP_CURVES: readonly ['aces', 'agx', 'pbr-neutral', 'reinhard', 'none'];
|
|
7
|
+
export type TonemapCurve = (typeof TONEMAP_CURVES)[number];
|
|
8
|
+
/** The curve for cameras without a Tonemapping component. */
|
|
9
|
+
export declare const DEFAULT_CURVE: TonemapCurve;
|
|
10
|
+
export declare const Tonemapping: import("@aethervtt/shard-core").ComponentDef<{
|
|
11
|
+
readonly curve: import("@aethervtt/shard-core").FieldType<"aces" | "agx" | "none" | "pbr-neutral" | "reinhard", "u8">;
|
|
12
|
+
readonly dither: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
|
|
13
|
+
}>;
|
|
14
|
+
export declare const RenderPath: import("@aethervtt/shard-core").ComponentDef<{
|
|
15
|
+
readonly mode: import("@aethervtt/shard-core").FieldType<"deferred" | "forward", "u8">;
|
|
16
|
+
}>;
|
|
17
|
+
/** Present when deferredPlugin is installed: cameras with RenderPath deferred use the G-buffer. */
|
|
18
|
+
export declare const DeferredPath: import("@aethervtt/shard-core").ResourceDef<{
|
|
19
|
+
installed: true;
|
|
20
|
+
}>;
|
|
21
|
+
/** Camera data a view carries for the passes that draw it. */
|
|
22
|
+
export interface CameraData {
|
|
23
|
+
entity: Entity;
|
|
24
|
+
/** World → view. */
|
|
25
|
+
view: Float32Array;
|
|
26
|
+
proj: Float32Array;
|
|
27
|
+
viewProj: Float32Array;
|
|
28
|
+
invViewProj: Float32Array;
|
|
29
|
+
/** viewProj without the TAA jitter (motion vectors). Same as viewProj without TAA. */
|
|
30
|
+
viewProjNoJitter: Float32Array;
|
|
31
|
+
/** Last frame's unjittered viewProj (motion vectors, TAA). */
|
|
32
|
+
prevViewProj: Float32Array;
|
|
33
|
+
position: Float32Array;
|
|
34
|
+
/** Unit vector the camera looks along (-Z of its transform). */
|
|
35
|
+
forward: Float32Array;
|
|
36
|
+
frustum: Float32Array;
|
|
37
|
+
orthographic: boolean;
|
|
38
|
+
near: number;
|
|
39
|
+
/** Far plane for orthographic; 0 for perspective (infinite). */
|
|
40
|
+
far: number;
|
|
41
|
+
fovY: number;
|
|
42
|
+
orthoHeight: number;
|
|
43
|
+
aspect: number;
|
|
44
|
+
/** Render resolution (0051): the size of the view's scene textures. */
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
/** Display resolution: the target's size, which screen overlays (UI, screen text) draw at. */
|
|
48
|
+
displayWidth: number;
|
|
49
|
+
displayHeight: number;
|
|
50
|
+
/**
|
|
51
|
+
* The target's pixels per CSS pixel (devicePixelRatio on the window, else 1). Pixel thresholds
|
|
52
|
+
* meant for the eye (terrain LOD) use `displayHeight / pixelRatio`: the same on every display
|
|
53
|
+
* and at every render scale.
|
|
54
|
+
*/
|
|
55
|
+
pixelRatio: number;
|
|
56
|
+
/** Scene luminance → pre-exposed HDR. */
|
|
57
|
+
exposure: number;
|
|
58
|
+
ev100: number;
|
|
59
|
+
/** Linear clear color, as displayed before the tonemap curve. */
|
|
60
|
+
clear: GPUColor;
|
|
61
|
+
msaa: number;
|
|
62
|
+
curve: number;
|
|
63
|
+
dither: boolean;
|
|
64
|
+
/** Culled draws of this camera, rebuilt each frame. */
|
|
65
|
+
draws: DrawList;
|
|
66
|
+
/** Blended draws, back to front. */
|
|
67
|
+
transparent: DrawList;
|
|
68
|
+
/** Deferred views: opaque draws the G-buffer can't take (custom lighting), drawn forward. */
|
|
69
|
+
forwardOnly: DrawList;
|
|
70
|
+
/** Renders through the G-buffer (RenderPath deferred). */
|
|
71
|
+
deferred: boolean;
|
|
72
|
+
/** G-buffer channel to show for a capture (-1: none), also forcing a G-buffer in forward views. */
|
|
73
|
+
gbufferDebug: number;
|
|
74
|
+
/** LOD level per instance slot for this camera (CPU culling), and the slot capacity it needs. */
|
|
75
|
+
lodState: Uint8Array;
|
|
76
|
+
lodCapacity: number;
|
|
77
|
+
/** A frozen culling frustum and eye (debug view 'culling'): culling stays put as you fly. */
|
|
78
|
+
frozenFrustum: Float32Array | undefined;
|
|
79
|
+
frozenPosition: Float32Array | undefined;
|
|
80
|
+
/** Frames this camera has rendered. */
|
|
81
|
+
frames: number;
|
|
82
|
+
/** Debug view: 0 off, 1 light-cluster heat map, 2 cascades tinted by index, 3 LOD levels. */
|
|
83
|
+
debug: number;
|
|
84
|
+
/** Post-processing effects and their settings, from the camera's components. */
|
|
85
|
+
post: PostSettings;
|
|
86
|
+
/** Set for PixelPerfect cameras: the low-resolution target and how it scales up. */
|
|
87
|
+
pixelPerfect: PixelPerfectLayout | undefined;
|
|
88
|
+
}
|
|
89
|
+
export declare const Cameras: import("@aethervtt/shard-core").ResourceDef<Map<number, CameraData>>;
|
|
90
|
+
/** Global render settings the camera extraction applies to every view. */
|
|
91
|
+
export interface ViewSettingsValue {
|
|
92
|
+
/** MSAA samples for forward cameras without an Antialiasing component. */
|
|
93
|
+
msaa: number;
|
|
94
|
+
/**
|
|
95
|
+
* Displays this dense or denser (pixels per CSS pixel) skip that default MSAA: their pixels are
|
|
96
|
+
* too small for stair-stepping to show, and 4 samples of a Retina frame cost about a third of
|
|
97
|
+
* it. 0: always use `msaa`. An Antialiasing component still wins.
|
|
98
|
+
*/
|
|
99
|
+
msaaMaxPixelRatio: number;
|
|
100
|
+
}
|
|
101
|
+
export declare const ViewSettings: import("@aethervtt/shard-core").ResourceDef<ViewSettingsValue>;
|
|
102
|
+
/** Whether a camera renders below (or above) its target's resolution and needs the upscale. */
|
|
103
|
+
export declare function isScaled(cam: CameraData): boolean;
|
|
104
|
+
/** Picks the aliases for a view from its camera data. Other plugins (FXAA) replace this. */
|
|
105
|
+
export declare function viewAliases(cam: CameraData): Readonly<Record<string, string>>;
|
|
106
|
+
export declare const extractCameras: import("@aethervtt/shard-core").SystemDef<{
|
|
107
|
+
q: import("@aethervtt/shard-core").Query;
|
|
108
|
+
}>;
|
|
109
|
+
/**
|
|
110
|
+
* The floating origin moved by `offset` (spec 0040): last frame's camera now lives in the new frame,
|
|
111
|
+
* so reprojection (motion vectors, TAA) lines up instead of seeing a cell-sized jump. A point `p` in
|
|
112
|
+
* the new frame was `p − offset` in the old one, so the old view-projection gains `translate(−offset)`.
|
|
113
|
+
* `viewProjNoJitter` becomes next extraction's `prevViewProj`; a frozen culling frustum moves too.
|
|
114
|
+
*/
|
|
115
|
+
export declare function shiftCameraHistory(cam: CameraData, x: number, y: number, z: number): void;
|
|
116
|
+
/** The camera data of a view, if it's a camera view. */
|
|
117
|
+
export declare function cameraOf(view: RenderView): CameraData | undefined;
|