@vgpu/render 0.0.6 → 0.0.7
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/README.md +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/perf/frame-time-measure.d.ts +22 -0
- package/dist/perf/frame-time-measure.d.ts.map +1 -0
- package/dist/perf/frame-time-measure.js +91 -0
- package/dist/perf/frame-time-measure.js.map +1 -0
- package/dist/perf/gpu-frame-time.d.ts +49 -0
- package/dist/perf/gpu-frame-time.d.ts.map +1 -0
- package/dist/perf/gpu-frame-time.js +50 -0
- package/dist/perf/gpu-frame-time.js.map +1 -0
- package/dist/perf/index.d.ts +5 -0
- package/dist/perf/index.d.ts.map +1 -0
- package/dist/perf/index.js +3 -0
- package/dist/perf/index.js.map +1 -0
- package/dist/perf/pixel-diff.d.ts +27 -0
- package/dist/perf/pixel-diff.d.ts.map +1 -0
- package/dist/perf/pixel-diff.js +40 -0
- package/dist/perf/pixel-diff.js.map +1 -0
- package/dist/pipeline-descriptor.d.ts +35 -0
- package/dist/pipeline-descriptor.d.ts.map +1 -0
- package/dist/pipeline-descriptor.js +44 -0
- package/dist/pipeline-descriptor.js.map +1 -0
- package/dist/pipeline.d.ts +22 -33
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +30 -46
- package/dist/pipeline.js.map +1 -1
- package/dist/render-pass.d.ts.map +1 -1
- package/dist/render-pass.js +4 -0
- package/dist/render-pass.js.map +1 -1
- package/dist/storage-buffer.d.ts +81 -0
- package/dist/storage-buffer.d.ts.map +1 -0
- package/dist/storage-buffer.js +91 -0
- package/dist/storage-buffer.js.map +1 -0
- package/dist/uniform.d.ts +55 -0
- package/dist/uniform.d.ts.map +1 -0
- package/dist/uniform.js +64 -0
- package/dist/uniform.js.map +1 -0
- package/package.json +8 -3
- package/src/createRenderPipeline.docs.md +34 -0
- package/src/perf/perf.docs.md +45 -0
- package/src/storage-buffer.docs.md +102 -0
- package/src/uniform.docs.md +70 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @vgpu/render
|
|
2
2
|
|
|
3
|
-
> 0.0.
|
|
3
|
+
> 0.0.7 — early preview
|
|
4
4
|
|
|
5
5
|
`@vgpu/render` is the small rendering layer on top of `@vgpu/core`. It focuses on explicit WebGPU-style control: create pipelines, encode standalone render passes, or build one frame command encoder with multiple user-ordered passes.
|
|
6
6
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
export { beginFrame, Frame } from "./frame.ts";
|
|
2
2
|
export { createRenderBundle, RenderBundleRecorder } from "./render-bundle.ts";
|
|
3
|
-
export { createRenderPipeline, createRenderPipelineAsync } from "./pipeline.ts";
|
|
3
|
+
export { createRenderPipeline, createRenderPipelineAsync, createRenderPipelineFromDescriptor, createRenderPipelineFromDescriptorAsync } from "./pipeline.ts";
|
|
4
4
|
export { RenderPass } from "./render-pass.ts";
|
|
5
5
|
export { RapidRenderer } from "./rapid-renderer.ts";
|
|
6
|
+
export { StorageBuffer } from "./storage-buffer.ts";
|
|
7
|
+
export { Uniform } from "./uniform.ts";
|
|
6
8
|
export { UniformPool } from "./uniform-pool.ts";
|
|
7
9
|
export type { FrameOptions, FrameRenderPassCallback } from "./frame.ts";
|
|
8
10
|
export type { RenderBundleOptions } from "./render-bundle.ts";
|
|
9
11
|
export type { RenderPipelineAsyncFallback, RenderPipelineFragmentOptions, RenderPipelineOptions, RenderPipelineShaderInput, RenderPipelineStageOptions, RenderPipelineVertexOptions, } from "./pipeline.ts";
|
|
10
12
|
export type { DrawSpec } from "./rapid-renderer.ts";
|
|
13
|
+
export type { StorageBufferOptions } from "./storage-buffer.ts";
|
|
14
|
+
export type { UniformOptions } from "./uniform.ts";
|
|
11
15
|
export type { UniformLayout, UniformPoolOptions, UniformSlot } from "./uniform-pool-types.ts";
|
|
12
16
|
export type { ColorAttachment, DepthStencilAttachment, RenderPassDrawOptions, RenderPassDynamicOffsets, RenderPassOptions, } from "./render-pass.ts";
|
|
13
17
|
export { box, capsule, cone, cylinder, degToRad, disk, dodecahedron, fullscreenQuad, getMaterialDeclarations, icosahedron, icosphere, material, Mesh, octahedron, orthographicCamera, perspectiveCamera, plane, ring, sampler, sphere, srgb, tetrahedron, torus, wgslDeclarations } from "./domain/index.ts";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,kCAAkC,EAAE,uCAAuC,EAAE,MAAM,eAAe,CAAC;AAC7J,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AACxE,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,YAAY,EACV,2BAA2B,EAC3B,6BAA6B,EAC7B,qBAAqB,EACrB,yBAAyB,EACzB,0BAA0B,EAC1B,2BAA2B,GAC5B,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC9F,YAAY,EACV,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,cAAc,EAAE,uBAAuB,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC7S,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,kBAAkB,EAAE,aAAa,EAAE,QAAQ,EAAE,mBAAmB,EAAE,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,IAAI,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export { beginFrame, Frame } from "./frame.js";
|
|
2
2
|
export { createRenderBundle, RenderBundleRecorder } from "./render-bundle.js";
|
|
3
|
-
export { createRenderPipeline, createRenderPipelineAsync } from "./pipeline.js";
|
|
3
|
+
export { createRenderPipeline, createRenderPipelineAsync, createRenderPipelineFromDescriptor, createRenderPipelineFromDescriptorAsync } from "./pipeline.js";
|
|
4
4
|
export { RenderPass } from "./render-pass.js";
|
|
5
5
|
export { RapidRenderer } from "./rapid-renderer.js";
|
|
6
|
+
export { StorageBuffer } from "./storage-buffer.js";
|
|
7
|
+
export { Uniform } from "./uniform.js";
|
|
6
8
|
export { UniformPool } from "./uniform-pool.js";
|
|
7
9
|
export { box, capsule, cone, cylinder, degToRad, disk, dodecahedron, fullscreenQuad, getMaterialDeclarations, icosahedron, icosphere, material, Mesh, octahedron, orthographicCamera, perspectiveCamera, plane, ring, sampler, sphere, srgb, tetrahedron, torus, wgslDeclarations } from "./domain/index.js";
|
|
8
10
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,kCAAkC,EAAE,uCAAuC,EAAE,MAAM,eAAe,CAAC;AAC7J,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAsBhD,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,cAAc,EAAE,uBAAuB,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Device } from "@vgpu/core";
|
|
2
|
+
import { type Frame } from "../frame.ts";
|
|
3
|
+
export type GpuFrameEncoder = (frame: Frame, index: number) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Wall-clock fallback: record one sample per frame while batching only the
|
|
6
|
+
* queue flush cadence. This keeps `samples` aligned with requested frames,
|
|
7
|
+
* matching the timestamp-query path and public docs.
|
|
8
|
+
*/
|
|
9
|
+
export declare function measureWallClock(device: Device, encode: GpuFrameEncoder, frames: number, batch: number, label: string): Promise<number[]>;
|
|
10
|
+
/**
|
|
11
|
+
* GPU-only timing via timestamp queries. Writes a begin/end timestamp around
|
|
12
|
+
* each frame's passes, then resolves all pairs in one read at the end. Throws
|
|
13
|
+
* if timestamp writes are unavailable so the caller can fall back to wall-clock.
|
|
14
|
+
*/
|
|
15
|
+
export declare function measureTimestamp(device: Device, encode: GpuFrameEncoder, frames: number, label: string): Promise<number[]>;
|
|
16
|
+
export declare function summarize(ms: number[]): {
|
|
17
|
+
medianMs: number;
|
|
18
|
+
meanMs: number;
|
|
19
|
+
minMs: number;
|
|
20
|
+
p95Ms: number;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=frame-time-measure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-time-measure.d.ts","sourceRoot":"","sources":["../../src/perf/frame-time-measure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAc,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAKrD,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;AAEpE;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,EAAE,CAAC,CAenB;AAED;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,eAAe,EACvB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,EAAE,CAAC,CAgDnB;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAaA"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { beginFrame } from "../frame.js";
|
|
2
|
+
const MS_PER_NS = 1 / 1_000_000;
|
|
3
|
+
const MAP_READ = 1; // GPUMapMode.READ
|
|
4
|
+
/**
|
|
5
|
+
* Wall-clock fallback: record one sample per frame while batching only the
|
|
6
|
+
* queue flush cadence. This keeps `samples` aligned with requested frames,
|
|
7
|
+
* matching the timestamp-query path and public docs.
|
|
8
|
+
*/
|
|
9
|
+
export async function measureWallClock(device, encode, frames, batch, label) {
|
|
10
|
+
const samples = [];
|
|
11
|
+
for (let i = 0; i < frames;) {
|
|
12
|
+
const n = Math.min(batch, frames - i);
|
|
13
|
+
for (let k = 0; k < n; k++) {
|
|
14
|
+
const start = performance.now();
|
|
15
|
+
const frame = beginFrame(device, { label });
|
|
16
|
+
encode(frame, i + k);
|
|
17
|
+
frame.submit();
|
|
18
|
+
samples.push(performance.now() - start);
|
|
19
|
+
}
|
|
20
|
+
await device.queue.flush();
|
|
21
|
+
i += n;
|
|
22
|
+
}
|
|
23
|
+
return samples;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* GPU-only timing via timestamp queries. Writes a begin/end timestamp around
|
|
27
|
+
* each frame's passes, then resolves all pairs in one read at the end. Throws
|
|
28
|
+
* if timestamp writes are unavailable so the caller can fall back to wall-clock.
|
|
29
|
+
*/
|
|
30
|
+
export async function measureTimestamp(device, encode, frames, label) {
|
|
31
|
+
const gpu = device.gpu;
|
|
32
|
+
const querySet = gpu.createQuerySet({ type: "timestamp", count: frames * 2 });
|
|
33
|
+
const resolve = device.createBuffer({
|
|
34
|
+
size: frames * 2 * 8,
|
|
35
|
+
usage: ["query_resolve", "copy_src"],
|
|
36
|
+
label: `${label}-ts-resolve`,
|
|
37
|
+
});
|
|
38
|
+
const read = device.createBuffer({
|
|
39
|
+
size: frames * 2 * 8,
|
|
40
|
+
usage: ["map_read", "copy_dst"],
|
|
41
|
+
label: `${label}-ts-read`,
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
for (let i = 0; i < frames; i++) {
|
|
45
|
+
const frame = beginFrame(device, { label });
|
|
46
|
+
const enc = frame.gpu;
|
|
47
|
+
if (typeof enc.writeTimestamp !== "function") {
|
|
48
|
+
throw new Error("GPUCommandEncoder.writeTimestamp unavailable");
|
|
49
|
+
}
|
|
50
|
+
enc.writeTimestamp(querySet, i * 2);
|
|
51
|
+
encode(frame, i);
|
|
52
|
+
enc.writeTimestamp(querySet, i * 2 + 1);
|
|
53
|
+
frame.submit();
|
|
54
|
+
}
|
|
55
|
+
const enc = gpu.createCommandEncoder({ label: `${label}-ts-resolve` });
|
|
56
|
+
enc.resolveQuerySet(querySet, 0, frames * 2, resolve.gpu, 0);
|
|
57
|
+
enc.copyBufferToBuffer(resolve.gpu, 0, read.gpu, 0, frames * 2 * 8);
|
|
58
|
+
gpu.queue.submit([enc.finish()]);
|
|
59
|
+
await read.gpu.mapAsync(MAP_READ);
|
|
60
|
+
const ticks = new BigUint64Array(read.gpu.getMappedRange().slice(0));
|
|
61
|
+
read.gpu.unmap();
|
|
62
|
+
const samples = [];
|
|
63
|
+
for (let i = 0; i < frames; i++) {
|
|
64
|
+
const dt = Number(ticks[i * 2 + 1] - ticks[i * 2]) * MS_PER_NS;
|
|
65
|
+
if (Number.isFinite(dt) && dt > 0)
|
|
66
|
+
samples.push(dt);
|
|
67
|
+
}
|
|
68
|
+
if (samples.length === 0)
|
|
69
|
+
throw new Error("no valid timestamp samples");
|
|
70
|
+
return samples;
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
querySet.destroy();
|
|
74
|
+
resolve.dispose();
|
|
75
|
+
read.dispose();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function summarize(ms) {
|
|
79
|
+
if (ms.length === 0) {
|
|
80
|
+
return { medianMs: 0, meanMs: 0, minMs: 0, p95Ms: 0 };
|
|
81
|
+
}
|
|
82
|
+
const sorted = [...ms].sort((a, b) => a - b);
|
|
83
|
+
const at = (q) => sorted[Math.min(sorted.length - 1, Math.max(0, Math.floor(q * sorted.length)))];
|
|
84
|
+
return {
|
|
85
|
+
medianMs: at(0.5),
|
|
86
|
+
meanMs: sorted.reduce((s, v) => s + v, 0) / sorted.length,
|
|
87
|
+
minMs: sorted[0],
|
|
88
|
+
p95Ms: at(0.95),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=frame-time-measure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame-time-measure.js","sourceRoot":"","sources":["../../src/perf/frame-time-measure.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAc,MAAM,aAAa,CAAC;AAErD,MAAM,SAAS,GAAG,CAAC,GAAG,SAAS,CAAC;AAChC,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,kBAAkB;AAItC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAc,EACd,MAAuB,EACvB,MAAc,EACd,KAAa,EACb,KAAa;IAEb,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,GAAI,CAAC;QAC7B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC;QACtC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;YAChC,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;YAC5C,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YACrB,KAAK,CAAC,MAAM,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC;QAC1C,CAAC;QACD,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QAC3B,CAAC,IAAI,CAAC,CAAC;IACT,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,MAAc,EACd,MAAuB,EACvB,MAAc,EACd,KAAa;IAEb,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;IACvB,MAAM,QAAQ,GAAG,GAAG,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9E,MAAM,OAAO,GAAG,MAAM,CAAC,YAAY,CAAC;QAClC,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,CAAC;QACpB,KAAK,EAAE,CAAC,eAAe,EAAE,UAAU,CAAC;QACpC,KAAK,EAAE,GAAG,KAAK,aAAa;KAC7B,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAC;QAC/B,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,CAAC;QACpB,KAAK,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC;QAC/B,KAAK,EAAE,GAAG,KAAK,UAAU;KAC1B,CAAC,CAAC;IACH,IAAI,CAAC;QACH,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAChC,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;YAC5C,MAAM,GAAG,GAAG,KAAK,CAAC,GAEjB,CAAC;YACF,IAAI,OAAO,GAAG,CAAC,cAAc,KAAK,UAAU,EAAE,CAAC;gBAC7C,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;YAClE,CAAC;YACD,GAAG,CAAC,cAAc,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;YACpC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YACjB,GAAG,CAAC,cAAc,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;YACxC,KAAK,CAAC,MAAM,EAAE,CAAC;QACjB,CAAC;QACD,MAAM,GAAG,GAAG,GAAG,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,GAAG,KAAK,aAAa,EAAE,CAAC,CAAC;QACvE,GAAG,CAAC,eAAe,CAAC,QAAQ,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QAC7D,GAAG,CAAC,kBAAkB,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QACpE,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAEjC,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,KAAK,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACrE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QAEjB,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAChC,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAE,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,GAAG,SAAS,CAAC;YACjE,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC;gBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtD,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;QACxE,OAAO,OAAO,CAAC;IACjB,CAAC;YAAS,CAAC;QACT,QAAQ,CAAC,OAAO,EAAE,CAAC;QACnB,OAAO,CAAC,OAAO,EAAE,CAAC;QAClB,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,EAAY;IAMpC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IACxD,CAAC;IACD,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7C,MAAM,EAAE,GAAG,CAAC,CAAS,EAAE,EAAE,CACvB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAE,CAAC;IACnF,OAAO;QACL,QAAQ,EAAE,EAAE,CAAC,GAAG,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM;QACzD,KAAK,EAAE,MAAM,CAAC,CAAC,CAAE;QACjB,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC;KAChB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Device } from "@vgpu/core";
|
|
2
|
+
import { type Frame } from "../frame.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Options for {@link gpuFrameTime}. All have sensible defaults; the common call passes only the
|
|
5
|
+
* device and an encode callback.
|
|
6
|
+
*/
|
|
7
|
+
export interface GpuFrameTimeOptions {
|
|
8
|
+
/** Measured frames (after warmup). Default 120. */
|
|
9
|
+
readonly frames?: number;
|
|
10
|
+
/** Warmup frames discarded before measuring (shader compile + lazy allocs settle). Default 30. */
|
|
11
|
+
readonly warmup?: number;
|
|
12
|
+
/** Frames per wall-clock batch (amortizes submit/drain latency). Ignored for timestamp-query. Default 8. */
|
|
13
|
+
readonly batch?: number;
|
|
14
|
+
/** Force the wall-clock path even when timestamp-query is available. Default false. */
|
|
15
|
+
readonly forceWallClock?: boolean;
|
|
16
|
+
readonly label?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface GpuFrameTimeResult {
|
|
19
|
+
/** Median per-frame time in milliseconds — the headline number for before/after comparisons. */
|
|
20
|
+
readonly medianMs: number;
|
|
21
|
+
readonly meanMs: number;
|
|
22
|
+
readonly minMs: number;
|
|
23
|
+
readonly p95Ms: number;
|
|
24
|
+
/** Per-frame samples behind the stats. */
|
|
25
|
+
readonly samples: number;
|
|
26
|
+
/**
|
|
27
|
+
* How the time was obtained. `timestamp-query` is GPU-only (excludes submit/drain) and needs the
|
|
28
|
+
* device feature; `wall-clock` (queue.flush) is the robust fallback and works everywhere.
|
|
29
|
+
*/
|
|
30
|
+
readonly method: "timestamp-query" | "wall-clock";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Measures GPU time per frame for a render routine, for before/after optimization comparisons.
|
|
34
|
+
*
|
|
35
|
+
* The `encode` callback records the frame's passes onto a vgpu {@link Frame} — the SAME body you
|
|
36
|
+
* run in production. The harness owns warmup, the loop, submit, and timing, so you don't hand-roll
|
|
37
|
+
* a bench. It prefers GPU timestamp queries when the device supports them (`timestamp-query`) and
|
|
38
|
+
* otherwise falls back to wall-clock timing via `device.queue.flush()`.
|
|
39
|
+
*
|
|
40
|
+
* Tooling only — never call this on a live animation-frame path.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const { medianMs, method } = await gpuFrameTime(device, (frame, i) => {
|
|
44
|
+
* frame.renderPass(scenePass, (pass) => drawScene(pass, i));
|
|
45
|
+
* frame.renderPass(floorPass, (pass) => drawFloor(pass, i));
|
|
46
|
+
* });
|
|
47
|
+
*/
|
|
48
|
+
export declare function gpuFrameTime(device: Device, encode: (frame: Frame, index: number) => void, options?: GpuFrameTimeOptions): Promise<GpuFrameTimeResult>;
|
|
49
|
+
//# sourceMappingURL=gpu-frame-time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gpu-frame-time.d.ts","sourceRoot":"","sources":["../../src/perf/gpu-frame-time.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAc,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAGrD;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,mDAAmD;IACnD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,kGAAkG;IAClG,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,4GAA4G;IAC5G,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,uFAAuF;IACvF,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,gGAAgG;IAChG,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,0CAA0C;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,YAAY,CAAC;CACnD;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,EAC7C,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CAwB7B"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { beginFrame } from "../frame.js";
|
|
2
|
+
import { measureTimestamp, measureWallClock, summarize } from "./frame-time-measure.js";
|
|
3
|
+
/**
|
|
4
|
+
* Measures GPU time per frame for a render routine, for before/after optimization comparisons.
|
|
5
|
+
*
|
|
6
|
+
* The `encode` callback records the frame's passes onto a vgpu {@link Frame} — the SAME body you
|
|
7
|
+
* run in production. The harness owns warmup, the loop, submit, and timing, so you don't hand-roll
|
|
8
|
+
* a bench. It prefers GPU timestamp queries when the device supports them (`timestamp-query`) and
|
|
9
|
+
* otherwise falls back to wall-clock timing via `device.queue.flush()`.
|
|
10
|
+
*
|
|
11
|
+
* Tooling only — never call this on a live animation-frame path.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const { medianMs, method } = await gpuFrameTime(device, (frame, i) => {
|
|
15
|
+
* frame.renderPass(scenePass, (pass) => drawScene(pass, i));
|
|
16
|
+
* frame.renderPass(floorPass, (pass) => drawFloor(pass, i));
|
|
17
|
+
* });
|
|
18
|
+
*/
|
|
19
|
+
export async function gpuFrameTime(device, encode, options = {}) {
|
|
20
|
+
const frames = Math.max(1, Math.floor(options.frames ?? 120));
|
|
21
|
+
const warmup = Math.max(0, Math.floor(options.warmup ?? 30));
|
|
22
|
+
const batch = Math.max(1, Math.floor(options.batch ?? 8));
|
|
23
|
+
const label = options.label ?? "vgpu-gpuFrameTime";
|
|
24
|
+
await warmupFrames(device, encode, warmup, label);
|
|
25
|
+
let perFrameMs = null;
|
|
26
|
+
let method = "wall-clock";
|
|
27
|
+
if (!options.forceWallClock && device.features.has("timestamp-query")) {
|
|
28
|
+
try {
|
|
29
|
+
perFrameMs = await measureTimestamp(device, encode, frames, label);
|
|
30
|
+
method = "timestamp-query";
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
perFrameMs = null; // any timestamp hiccup → fall back to the robust path
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!perFrameMs || perFrameMs.length === 0) {
|
|
37
|
+
perFrameMs = await measureWallClock(device, encode, frames, batch, label);
|
|
38
|
+
method = "wall-clock";
|
|
39
|
+
}
|
|
40
|
+
return { ...summarize(perFrameMs), samples: perFrameMs.length, method };
|
|
41
|
+
}
|
|
42
|
+
async function warmupFrames(device, encode, warmup, label) {
|
|
43
|
+
for (let i = 0; i < warmup; i++) {
|
|
44
|
+
const frame = beginFrame(device, { label });
|
|
45
|
+
encode(frame, i);
|
|
46
|
+
frame.submit();
|
|
47
|
+
}
|
|
48
|
+
await device.queue.flush();
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=gpu-frame-time.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gpu-frame-time.js","sourceRoot":"","sources":["../../src/perf/gpu-frame-time.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAc,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAiCxF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAc,EACd,MAA6C,EAC7C,UAA+B,EAAE;IAEjC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC;IAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC;IAC1D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,mBAAmB,CAAC;IAEnD,MAAM,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IAElD,IAAI,UAAU,GAAoB,IAAI,CAAC;IACvC,IAAI,MAAM,GAAiC,YAAY,CAAC;IACxD,IAAI,CAAC,OAAO,CAAC,cAAc,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,CAAC;QACtE,IAAI,CAAC;YACH,UAAU,GAAG,MAAM,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;YACnE,MAAM,GAAG,iBAAiB,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,UAAU,GAAG,IAAI,CAAC,CAAC,sDAAsD;QAC3E,CAAC;IACH,CAAC;IACD,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3C,UAAU,GAAG,MAAM,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QAC1E,MAAM,GAAG,YAAY,CAAC;IACxB,CAAC;IAED,OAAO,EAAE,GAAG,SAAS,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;AAC1E,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,MAAc,EACd,MAA6C,EAC7C,MAAc,EACd,KAAa;IAEb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5C,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACjB,KAAK,CAAC,MAAM,EAAE,CAAC;IACjB,CAAC;IACD,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { gpuFrameTime } from "./gpu-frame-time.ts";
|
|
2
|
+
export type { GpuFrameTimeOptions, GpuFrameTimeResult } from "./gpu-frame-time.ts";
|
|
3
|
+
export { pixelDiff } from "./pixel-diff.ts";
|
|
4
|
+
export type { PixelDiffResult } from "./pixel-diff.ts";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/perf/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/perf/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Texture } from "@vgpu/core";
|
|
2
|
+
export interface PixelDiffResult {
|
|
3
|
+
/** Largest absolute per-byte difference (0–255). The headline: ≤1–2 is driver-rounding noise. */
|
|
4
|
+
readonly maxByte: number;
|
|
5
|
+
/** Mean absolute per-byte difference. */
|
|
6
|
+
readonly meanByte: number;
|
|
7
|
+
/** Bytes that differ at all. */
|
|
8
|
+
readonly changedBytes: number;
|
|
9
|
+
readonly totalBytes: number;
|
|
10
|
+
/** changedBytes / totalBytes. A tiny fraction with maxByte ≤ ~2 means "imperceptible". */
|
|
11
|
+
readonly changedFraction: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Compares two renders byte-for-byte — the verify half of measure-before-keeping. Pass two
|
|
15
|
+
* `Texture`s (read back via {@link Texture.read}) or two already-read `Uint8Array`s. Use it to
|
|
16
|
+
* confirm an optimization is bit-exact (maxByte 0) or imperceptible (maxByte ≤ ~2 on a small
|
|
17
|
+
* fraction) before keeping it.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const before = await renderInto(target); // baseline texture
|
|
21
|
+
* applyOptimization();
|
|
22
|
+
* const after = await renderInto(target);
|
|
23
|
+
* const { maxByte } = await pixelDiff(before, after);
|
|
24
|
+
* expect(maxByte).toBeLessThanOrEqual(2);
|
|
25
|
+
*/
|
|
26
|
+
export declare function pixelDiff(a: Texture | Uint8Array, b: Texture | Uint8Array): Promise<PixelDiffResult>;
|
|
27
|
+
//# sourceMappingURL=pixel-diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pixel-diff.d.ts","sourceRoot":"","sources":["../../src/perf/pixel-diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,WAAW,eAAe;IAC9B,iGAAiG;IACjG,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yCAAyC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,gCAAgC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,0FAA0F;IAC1F,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,SAAS,CAC7B,CAAC,EAAE,OAAO,GAAG,UAAU,EACvB,CAAC,EAAE,OAAO,GAAG,UAAU,GACtB,OAAO,CAAC,eAAe,CAAC,CAwB1B"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compares two renders byte-for-byte — the verify half of measure-before-keeping. Pass two
|
|
3
|
+
* `Texture`s (read back via {@link Texture.read}) or two already-read `Uint8Array`s. Use it to
|
|
4
|
+
* confirm an optimization is bit-exact (maxByte 0) or imperceptible (maxByte ≤ ~2 on a small
|
|
5
|
+
* fraction) before keeping it.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const before = await renderInto(target); // baseline texture
|
|
9
|
+
* applyOptimization();
|
|
10
|
+
* const after = await renderInto(target);
|
|
11
|
+
* const { maxByte } = await pixelDiff(before, after);
|
|
12
|
+
* expect(maxByte).toBeLessThanOrEqual(2);
|
|
13
|
+
*/
|
|
14
|
+
export async function pixelDiff(a, b) {
|
|
15
|
+
const da = a instanceof Uint8Array ? a : await a.read();
|
|
16
|
+
const db = b instanceof Uint8Array ? b : await b.read();
|
|
17
|
+
const total = Math.min(da.length, db.length);
|
|
18
|
+
let maxByte = 0;
|
|
19
|
+
let sum = 0;
|
|
20
|
+
let changed = 0;
|
|
21
|
+
for (let i = 0; i < total; i++) {
|
|
22
|
+
const d = Math.abs((da[i] ?? 0) - (db[i] ?? 0));
|
|
23
|
+
if (d > 0)
|
|
24
|
+
changed++;
|
|
25
|
+
if (d > maxByte)
|
|
26
|
+
maxByte = d;
|
|
27
|
+
sum += d;
|
|
28
|
+
}
|
|
29
|
+
// A length mismatch is itself a difference; surface it rather than silently comparing a prefix.
|
|
30
|
+
if (da.length !== db.length)
|
|
31
|
+
maxByte = Math.max(maxByte, 255);
|
|
32
|
+
return {
|
|
33
|
+
maxByte,
|
|
34
|
+
meanByte: total === 0 ? 0 : sum / total,
|
|
35
|
+
changedBytes: changed,
|
|
36
|
+
totalBytes: total,
|
|
37
|
+
changedFraction: total === 0 ? 0 : changed / total,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=pixel-diff.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pixel-diff.js","sourceRoot":"","sources":["../../src/perf/pixel-diff.ts"],"names":[],"mappings":"AAcA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,CAAuB,EACvB,CAAuB;IAEvB,MAAM,EAAE,GAAG,CAAC,YAAY,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,MAAM,EAAE,GAAG,CAAC,YAAY,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;IAExD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC;IAC7C,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChD,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,CAAC;QACrB,IAAI,CAAC,GAAG,OAAO;YAAE,OAAO,GAAG,CAAC,CAAC;QAC7B,GAAG,IAAI,CAAC,CAAC;IACX,CAAC;IACD,gGAAgG;IAChG,IAAI,EAAE,CAAC,MAAM,KAAK,EAAE,CAAC,MAAM;QAAE,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAE9D,OAAO;QACL,OAAO;QACP,QAAQ,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,KAAK;QACvC,YAAY,EAAE,OAAO;QACrB,UAAU,EAAE,KAAK;QACjB,eAAe,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,KAAK;KACnD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Shader } from "@vgpu/core";
|
|
2
|
+
export type RenderPipelineShaderInput = Shader | GPUShaderModule;
|
|
3
|
+
export interface RenderPipelineStageOptions {
|
|
4
|
+
/** VGPU Shader or raw GPUShaderModule for this stage. Defaults to RenderPipelineOptions.shader. */
|
|
5
|
+
readonly shader?: RenderPipelineShaderInput;
|
|
6
|
+
/** Descriptor-like alias for shader. */
|
|
7
|
+
readonly module?: RenderPipelineShaderInput;
|
|
8
|
+
/** Entry-point name. Kept for backwards compatibility with the first render helper API. */
|
|
9
|
+
readonly entry?: string;
|
|
10
|
+
/** Descriptor-like entry-point name. */
|
|
11
|
+
readonly entryPoint?: string;
|
|
12
|
+
readonly constants?: Record<string, GPUPipelineConstantValue>;
|
|
13
|
+
}
|
|
14
|
+
export interface RenderPipelineVertexOptions extends RenderPipelineStageOptions {
|
|
15
|
+
readonly buffers?: readonly (GPUVertexBufferLayout | null)[];
|
|
16
|
+
}
|
|
17
|
+
export interface RenderPipelineFragmentOptions extends RenderPipelineStageOptions {
|
|
18
|
+
readonly targets: readonly (GPUColorTargetState | null)[];
|
|
19
|
+
}
|
|
20
|
+
export interface RenderPipelineOptions {
|
|
21
|
+
/** Optional shared shader module used by vertex/fragment stages that do not provide their own module. */
|
|
22
|
+
readonly shader?: RenderPipelineShaderInput;
|
|
23
|
+
readonly vertex: RenderPipelineVertexOptions;
|
|
24
|
+
readonly fragment?: RenderPipelineFragmentOptions;
|
|
25
|
+
readonly primitive?: GPUPrimitiveState;
|
|
26
|
+
readonly depthStencil?: GPUDepthStencilState;
|
|
27
|
+
readonly multisample?: GPUMultisampleState;
|
|
28
|
+
readonly layout?: GPUPipelineLayout | "auto";
|
|
29
|
+
readonly label?: string;
|
|
30
|
+
/** createRenderPipelineAsync fallback when GPUDevice.createRenderPipelineAsync is unavailable. Defaults to "sync". */
|
|
31
|
+
readonly fallback?: RenderPipelineAsyncFallback;
|
|
32
|
+
}
|
|
33
|
+
export type RenderPipelineAsyncFallback = "sync" | "throw";
|
|
34
|
+
export declare function toRenderPipelineDescriptor(opts: RenderPipelineOptions): GPURenderPipelineDescriptor;
|
|
35
|
+
//# sourceMappingURL=pipeline-descriptor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline-descriptor.d.ts","sourceRoot":"","sources":["../src/pipeline-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAa,MAAM,YAAY,CAAC;AAE/C,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe,CAAC;AAEjE,MAAM,WAAW,0BAA0B;IACzC,mGAAmG;IACnG,QAAQ,CAAC,MAAM,CAAC,EAAE,yBAAyB,CAAC;IAC5C,wCAAwC;IACxC,QAAQ,CAAC,MAAM,CAAC,EAAE,yBAAyB,CAAC;IAC5C,2FAA2F;IAC3F,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,wCAAwC;IACxC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,2BAA4B,SAAQ,0BAA0B;IAC7E,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,qBAAqB,GAAG,IAAI,CAAC,EAAE,CAAC;CAC9D;AAED,MAAM,WAAW,6BAA8B,SAAQ,0BAA0B;IAC/E,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,mBAAmB,GAAG,IAAI,CAAC,EAAE,CAAC;CAC3D;AAED,MAAM,WAAW,qBAAqB;IACpC,yGAAyG;IACzG,QAAQ,CAAC,MAAM,CAAC,EAAE,yBAAyB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,2BAA2B,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,6BAA6B,CAAC;IAClD,QAAQ,CAAC,SAAS,CAAC,EAAE,iBAAiB,CAAC;IACvC,QAAQ,CAAC,YAAY,CAAC,EAAE,oBAAoB,CAAC;IAC7C,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAAC;IAC7C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,sHAAsH;IACtH,QAAQ,CAAC,QAAQ,CAAC,EAAE,2BAA2B,CAAC;CACjD;AAED,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,qBAAqB,GAAG,2BAA2B,CAyBnG"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Shader, VGPUError } from "@vgpu/core";
|
|
2
|
+
export function toRenderPipelineDescriptor(opts) {
|
|
3
|
+
const descriptor = {
|
|
4
|
+
label: opts.label,
|
|
5
|
+
layout: opts.layout ?? "auto",
|
|
6
|
+
vertex: {
|
|
7
|
+
module: stageModule(opts.vertex, opts.shader, "vertex"),
|
|
8
|
+
entryPoint: stageEntryPoint(opts.vertex),
|
|
9
|
+
constants: opts.vertex.constants,
|
|
10
|
+
buffers: opts.vertex.buffers ? [...opts.vertex.buffers] : undefined,
|
|
11
|
+
},
|
|
12
|
+
primitive: opts.primitive,
|
|
13
|
+
depthStencil: opts.depthStencil,
|
|
14
|
+
multisample: opts.multisample,
|
|
15
|
+
};
|
|
16
|
+
if (opts.fragment) {
|
|
17
|
+
descriptor.fragment = {
|
|
18
|
+
module: stageModule(opts.fragment, opts.shader, "fragment"),
|
|
19
|
+
entryPoint: stageEntryPoint(opts.fragment),
|
|
20
|
+
constants: opts.fragment.constants,
|
|
21
|
+
targets: [...opts.fragment.targets],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return descriptor;
|
|
25
|
+
}
|
|
26
|
+
function stageModule(stage, fallback, stageName) {
|
|
27
|
+
const module = stage.module ?? stage.shader ?? fallback;
|
|
28
|
+
if (!module) {
|
|
29
|
+
throw new VGPUError({
|
|
30
|
+
code: "VGPU-RENDER-PIPELINE-MISSING-SHADER",
|
|
31
|
+
message: `Missing shader module for ${stageName} stage.`,
|
|
32
|
+
fix: "Pass options.shader for a shared module or pass vertex/fragment module or shader.",
|
|
33
|
+
where: `createRenderPipeline.${stageName}`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return isShader(module) ? module.gpu : module;
|
|
37
|
+
}
|
|
38
|
+
function stageEntryPoint(stage) {
|
|
39
|
+
return stage.entryPoint ?? stage.entry;
|
|
40
|
+
}
|
|
41
|
+
function isShader(input) {
|
|
42
|
+
return input instanceof Shader;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=pipeline-descriptor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline-descriptor.js","sourceRoot":"","sources":["../src/pipeline-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAwC/C,MAAM,UAAU,0BAA0B,CAAC,IAA2B;IACpE,MAAM,UAAU,GAAgC;QAC9C,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,MAAM;QAC7B,MAAM,EAAE;YACN,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC;YACvD,UAAU,EAAE,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC;YACxC,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;YAChC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS;SACpE;QACD,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,WAAW,EAAE,IAAI,CAAC,WAAW;KAC9B,CAAC;IAEF,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,UAAU,CAAC,QAAQ,GAAG;YACpB,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC;YAC3D,UAAU,EAAE,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC;YAC1C,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,SAAS;YAClC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;SACpC,CAAC;IACJ,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,WAAW,CAClB,KAAiC,EACjC,QAA+C,EAC/C,SAAgC;IAEhC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,QAAQ,CAAC;IACxD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,SAAS,CAAC;YAClB,IAAI,EAAE,qCAAqC;YAC3C,OAAO,EAAE,6BAA6B,SAAS,SAAS;YACxD,GAAG,EAAE,mFAAmF;YACxF,KAAK,EAAE,wBAAwB,SAAS,EAAE;SAC3C,CAAC,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;AAChD,CAAC;AAED,SAAS,eAAe,CAAC,KAAiC;IACxD,OAAO,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,KAAK,CAAC;AACzC,CAAC;AAED,SAAS,QAAQ,CAAC,KAAgC;IAChD,OAAO,KAAK,YAAY,MAAM,CAAC;AACjC,CAAC"}
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -1,37 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export type RenderPipelineAsyncFallback
|
|
4
|
-
export interface RenderPipelineStageOptions {
|
|
5
|
-
/** VGPU Shader or raw GPUShaderModule for this stage. Defaults to RenderPipelineOptions.shader. */
|
|
6
|
-
readonly shader?: RenderPipelineShaderInput;
|
|
7
|
-
/** Descriptor-like alias for shader. */
|
|
8
|
-
readonly module?: RenderPipelineShaderInput;
|
|
9
|
-
/** Entry-point name. Kept for backwards compatibility with the first render helper API. */
|
|
10
|
-
readonly entry?: string;
|
|
11
|
-
/** Descriptor-like entry-point name. */
|
|
12
|
-
readonly entryPoint?: string;
|
|
13
|
-
readonly constants?: Record<string, GPUPipelineConstantValue>;
|
|
14
|
-
}
|
|
15
|
-
export interface RenderPipelineVertexOptions extends RenderPipelineStageOptions {
|
|
16
|
-
readonly buffers?: readonly (GPUVertexBufferLayout | null)[];
|
|
17
|
-
}
|
|
18
|
-
export interface RenderPipelineFragmentOptions extends RenderPipelineStageOptions {
|
|
19
|
-
readonly targets: readonly (GPUColorTargetState | null)[];
|
|
20
|
-
}
|
|
21
|
-
export interface RenderPipelineOptions {
|
|
22
|
-
/** Optional shared shader module used by vertex/fragment stages that do not provide their own module. */
|
|
23
|
-
readonly shader?: RenderPipelineShaderInput;
|
|
24
|
-
readonly vertex: RenderPipelineVertexOptions;
|
|
25
|
-
readonly fragment?: RenderPipelineFragmentOptions;
|
|
26
|
-
readonly primitive?: GPUPrimitiveState;
|
|
27
|
-
readonly depthStencil?: GPUDepthStencilState;
|
|
28
|
-
readonly multisample?: GPUMultisampleState;
|
|
29
|
-
readonly layout?: GPUPipelineLayout | "auto";
|
|
30
|
-
readonly label?: string;
|
|
31
|
-
/** createRenderPipelineAsync fallback when GPUDevice.createRenderPipelineAsync is unavailable. Defaults to "sync". */
|
|
32
|
-
readonly fallback?: RenderPipelineAsyncFallback;
|
|
33
|
-
}
|
|
1
|
+
import { type Device } from "@vgpu/core";
|
|
2
|
+
import type { RenderPipelineAsyncFallback, RenderPipelineOptions } from "./pipeline-descriptor.ts";
|
|
3
|
+
export type { RenderPipelineAsyncFallback, RenderPipelineFragmentOptions, RenderPipelineOptions, RenderPipelineShaderInput, RenderPipelineStageOptions, RenderPipelineVertexOptions, } from "./pipeline-descriptor.ts";
|
|
34
4
|
export declare function createRenderPipeline(device: Device, opts: RenderPipelineOptions): GPURenderPipeline;
|
|
35
5
|
export declare function createRenderPipelineAsync(device: Device, opts: RenderPipelineOptions): Promise<GPURenderPipeline>;
|
|
6
|
+
/**
|
|
7
|
+
* Create a render pipeline from a raw, hand-built `GPURenderPipelineDescriptor`.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Use this when you already own a native descriptor and only want VGPU's
|
|
11
|
+
* `Device` wrapper to forward it — no `RenderPipelineOptions` reshape. The
|
|
12
|
+
* descriptor is passed through unchanged.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createRenderPipelineFromDescriptor(device: Device, descriptor: GPURenderPipelineDescriptor): GPURenderPipeline;
|
|
15
|
+
/**
|
|
16
|
+
* Async variant of {@link createRenderPipelineFromDescriptor} with the same
|
|
17
|
+
* async→sync compatibility fallback as {@link createRenderPipelineAsync}.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* The descriptor is forwarded unchanged. When `GPUDevice.createRenderPipelineAsync`
|
|
21
|
+
* is unavailable, the default `fallback: "sync"` emits a once-only diagnostic and
|
|
22
|
+
* calls the synchronous path; pass `fallback: "throw"` for a structured `VGPUError`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createRenderPipelineFromDescriptorAsync(device: Device, descriptor: GPURenderPipelineDescriptor, fallback?: RenderPipelineAsyncFallback): Promise<GPURenderPipeline>;
|
|
36
25
|
export declare function __resetCreateRenderPipelineAsyncFallbackWarningForTests(): void;
|
|
37
26
|
//# sourceMappingURL=pipeline.d.ts.map
|
package/dist/pipeline.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,KAAK,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,KAAK,EAAE,2BAA2B,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAEnG,YAAY,EACV,2BAA2B,EAC3B,6BAA6B,EAC7B,qBAAqB,EACrB,yBAAyB,EACzB,0BAA0B,EAC1B,2BAA2B,GAC5B,MAAM,0BAA0B,CAAC;AASlC,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB,GAAG,iBAAiB,CAEnG;AAED,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAO5B;AAED;;;;;;;GAOG;AACH,wBAAgB,kCAAkC,CAChD,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,2BAA2B,GACtC,iBAAiB,CAEnB;AAED;;;;;;;;GAQG;AACH,wBAAsB,uCAAuC,CAC3D,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,2BAA2B,EACvC,QAAQ,CAAC,EAAE,2BAA2B,GACrC,OAAO,CAAC,iBAAiB,CAAC,CAE5B;AA0BD,wBAAgB,uDAAuD,IAAI,IAAI,CAE9E"}
|