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