@aethervtt/shard-gpu 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/caches.d.ts CHANGED
@@ -10,17 +10,29 @@ export declare class PipelineCache {
10
10
  private readonly render_;
11
11
  private readonly compute_;
12
12
  private readonly gpu;
13
+ /** Compiles in flight, and when the current run of them started (0062's `coldStart.pipelines`). */
14
+ private compiling;
15
+ private busySince;
16
+ private busyTotal;
13
17
  constructor(gpu: GpuContext);
18
+ /**
19
+ * Wall time, in ms, during which at least one pipeline was compiling, counted since the cache was
20
+ * made. Compiles that overlap count once.
21
+ */
22
+ busyMs(now?: number): number;
14
23
  render(descriptor: GPURenderPipelineDescriptor): GPURenderPipeline | undefined;
15
24
  compute(descriptor: GPUComputePipelineDescriptor): GPUComputePipeline | undefined;
16
25
  /** Pipelines still compiling. */
17
26
  get pending(): number;
18
27
  /** Resolves when every pipeline requested so far has finished compiling (or failed). */
19
28
  whenIdle(): Promise<void>;
29
+ /** Why the pipeline for `descriptor` failed, if it did (0061): callers draw a fallback instead. */
30
+ failure(descriptor: GPURenderPipelineDescriptor | GPUComputePipelineDescriptor): ShardError | undefined;
20
31
  /** Errors from pipelines that failed to compile, by label. */
21
32
  failures(): ShardError[];
22
33
  clear(): void;
23
34
  private get;
35
+ private compiled;
24
36
  }
25
37
  /** Bind group layouts, pipeline layouts, and samplers: equal descriptors give the same object. */
26
38
  export declare class LayoutCache {
package/dist/caches.js CHANGED
@@ -6,9 +6,20 @@ class PipelineCache {
6
6
  render_ = /* @__PURE__ */ new Map();
7
7
  compute_ = /* @__PURE__ */ new Map();
8
8
  gpu;
9
+ /** Compiles in flight, and when the current run of them started (0062's `coldStart.pipelines`). */
10
+ compiling = 0;
11
+ busySince = 0;
12
+ busyTotal = 0;
9
13
  constructor(gpu) {
10
14
  this.gpu = gpu;
11
15
  }
16
+ /**
17
+ * Wall time, in ms, during which at least one pipeline was compiling, counted since the cache was
18
+ * made. Compiles that overlap count once.
19
+ */
20
+ busyMs(now = performance.now()) {
21
+ return this.busyTotal + (this.compiling > 0 ? now - this.busySince : 0);
22
+ }
12
23
  render(descriptor) {
13
24
  return this.get(this.render_, descriptor, (d) => this.gpu.device.createRenderPipelineAsync(d));
14
25
  }
@@ -30,6 +41,12 @@ class PipelineCache {
30
41
  }
31
42
  await Promise.all(pending);
32
43
  }
44
+ /** Why the pipeline for `descriptor` failed, if it did (0061): callers draw a fallback instead. */
45
+ failure(descriptor) {
46
+ const key = descriptorKey(descriptor);
47
+ const state = this.render_.get(key) ?? this.compute_.get(key);
48
+ return state?.status === "failed" ? state.error : void 0;
49
+ }
33
50
  /** Errors from pipelines that failed to compile, by label. */
34
51
  failures() {
35
52
  const out = [];
@@ -46,15 +63,19 @@ class PipelineCache {
46
63
  const key = descriptorKey(descriptor);
47
64
  const state = map.get(key);
48
65
  if (state?.status === "ready") return state.pipeline;
66
+ if (state?.status === "failed") return void 0;
49
67
  if (state) {
50
68
  this.skipped++;
51
69
  return void 0;
52
70
  }
71
+ if (this.compiling++ === 0) this.busySince = performance.now();
53
72
  const promise = create(descriptor).then(
54
73
  (pipeline) => {
74
+ this.compiled();
55
75
  map.set(key, { status: "ready", pipeline });
56
76
  },
57
77
  (err) => {
78
+ this.compiled();
58
79
  const error = toShardError(err, descriptor.label);
59
80
  map.set(key, { status: "failed", error });
60
81
  this.gpu.reportError(error);
@@ -64,6 +85,9 @@ class PipelineCache {
64
85
  this.skipped++;
65
86
  return void 0;
66
87
  }
88
+ compiled() {
89
+ if (--this.compiling === 0) this.busyTotal += performance.now() - this.busySince;
90
+ }
67
91
  }
68
92
  class LayoutCache {
69
93
  bindGroupLayouts = /* @__PURE__ */ new Map();
package/dist/context.d.ts CHANGED
@@ -1,9 +1,56 @@
1
1
  import { ShardError } from '@aethervtt/shard-core';
2
+ import type { BakedTranslations } from '@aethervtt/shard-gpu-webgl2';
2
3
  import { LayoutCache, PipelineCache } from './caches.js';
3
4
  import { type GpuErrorListener } from './errors.js';
5
+ import { type GpuMemory, type GpuStats, type GpuUploads } from './ledger.js';
6
+ import { Surface, type SurfaceAlpha, type SurfaceOptions } from './surface.js';
7
+ import { type GpuBackendName, type GpuCapabilities, type GpuTier, type GraphicsReason } from './tier.js';
8
+ /** The graphics API to run on (0064): 'auto' tries WebGPU, then WebGL2. */
9
+ export type GpuBackendChoice = 'auto' | GpuBackendName;
10
+ /** WebGL2 settings, for tests and tools. */
11
+ export interface Webgl2ContextOptions {
12
+ /** 'minimum' holds the device to WebGL2's floor (tests of the least capable devices). */
13
+ profile?: 'native' | 'minimum';
14
+ /** Checks GL errors after every submit: a round trip to the GPU process, so for dev and tests. */
15
+ checkErrors?: boolean;
16
+ /** A context to use as it is: tests pass a fake one. */
17
+ context?: WebGL2RenderingContext;
18
+ /**
19
+ * A baked translation set (`shard shaders bake`), or where it's served: with every shader in it,
20
+ * naga, the WGSL translator, never loads.
21
+ */
22
+ shaders?: string | URL | BakedTranslations;
23
+ /** Keeps translations in IndexedDB across sessions. Default true where there is IndexedDB. */
24
+ persist?: boolean;
25
+ }
26
+ /** Where WebGL2's translations came from this session (0064's shader cache). */
27
+ export interface ShaderCacheStats {
28
+ hits: {
29
+ memory: number;
30
+ baked: number;
31
+ stored: number;
32
+ };
33
+ misses: {
34
+ entry: string;
35
+ stage: 'vertex' | 'fragment';
36
+ ms: number;
37
+ }[];
38
+ nagaLoadMs: number | undefined;
39
+ baked: number;
40
+ }
4
41
  export interface CreateGpuContextOptions {
5
- /** Omit for offscreen/headless rendering. */
42
+ /**
43
+ * The graphics API (0064). 'auto' (default): WebGPU when it gives a device, through exactly the
44
+ * request Shard has always made; else WebGL2, running the baseline tier. 'webgpu' never falls
45
+ * back; 'webgl2' doesn't try WebGPU.
46
+ */
47
+ backend?: GpuBackendChoice;
48
+ /** WebGL2 settings, when it's the backend. */
49
+ webgl2?: Webgl2ContextOptions;
50
+ /** A canvas to add as the first surface. Omit for a device only (headless, or surfaces later). */
6
51
  canvas?: HTMLCanvasElement | OffscreenCanvas;
52
+ /** The canvas surface's alpha mode. Default 'opaque'. */
53
+ alpha?: SurfaceAlpha;
7
54
  /** The WebGPU entry point. Defaults to `navigator.gpu`; in Node pass the `webgpu` package's. */
8
55
  gpu?: GPU;
9
56
  /** Requested if the adapter supports them. */
@@ -16,51 +63,172 @@ export interface CreateGpuContextOptions {
16
63
  * true; Basis textures transcode to whichever the device ends up with.
17
64
  */
18
65
  compressedTextures?: boolean;
66
+ /**
67
+ * How a lost device is replaced (0061): up to `attempts` tries, `intervalMs` apart, before the
68
+ * context gives up (`status: 'failed'`). Default 3 tries, 1 s apart.
69
+ */
70
+ recovery?: {
71
+ attempts?: number;
72
+ intervalMs?: number;
73
+ };
74
+ /**
75
+ * `'baseline'` runs the baseline tier (0064) even where the full one would: on WebGPU, a
76
+ * compatibility-mode device. Omitted, the device decides: `full` on a core WebGPU device,
77
+ * `baseline` on a compatibility-mode one (asked for only when no core adapter is found).
78
+ */
79
+ tier?: 'baseline';
19
80
  }
81
+ /** Whether the device works: `lost` between a loss and its replacement, `failed` if that gave up. */
82
+ export type GpuStatus = 'ok' | 'lost' | 'failed';
20
83
  export interface DeviceLostInfo {
21
84
  reason: string;
22
85
  message: string;
23
86
  }
24
87
  /**
25
- * The adapter, device, and optional canvas, plus the caches every renderer needs. After a device
26
- * loss, `recreate()` gets a new device and bumps `generation`; `GpuBuffer`s and caches rebuild
27
- * against it.
88
+ * The adapter and device, the canvases it draws into (surfaces), and the caches every renderer
89
+ * needs. Several apps can share one (0052): each counts its buffers and textures under its own
90
+ * owner. After a device loss, `recreate()` gets a new device and bumps `generation`; `GpuBuffer`s,
91
+ * caches, and surfaces rebuild against it.
28
92
  */
29
93
  export declare class GpuContext {
30
94
  adapter: GPUAdapter;
31
95
  device: GPUDevice;
32
96
  readonly format: GPUTextureFormat;
33
- readonly canvas: HTMLCanvasElement | OffscreenCanvas | undefined;
34
- readonly context: GPUCanvasContext | undefined;
35
97
  readonly features: ReadonlySet<string>;
98
+ /** The graphics API the device runs on (0064). */
99
+ readonly backend: GpuBackendName;
100
+ /**
101
+ * The tier the engine runs at (0064): `full` on a core WebGPU device, `baseline` on anything
102
+ * else that runs the baseline subset. Kept across device replacement.
103
+ */
104
+ readonly tier: GpuTier;
105
+ /** What the device can do, as far as the tiers care. */
106
+ capabilities: GpuCapabilities;
107
+ /** Why better options were skipped while opening the device (no core adapter, say). */
108
+ readonly reasons: readonly GraphicsReason[];
36
109
  readonly pipelines: PipelineCache;
37
110
  readonly layouts: LayoutCache;
38
111
  /** Increments when the device is replaced. */
39
112
  generation: number;
40
- /** Canvas pixels per CSS pixel (devicePixelRatio) at the last resize; 1 without a canvas. */
41
- pixelRatio: number;
113
+ /**
114
+ * How long the first adapter and device request took, in ms (0062's `coldStart.device`). Set by
115
+ * `createGpuContext`; 0 for a context built directly.
116
+ */
117
+ deviceMs: number;
118
+ /**
119
+ * Who new buffers and textures count against. An app sets it to its owner while it runs (the
120
+ * render plugin's scope); otherwise it's `'gpu'`, the device's own.
121
+ */
122
+ owner: string;
42
123
  /** Most recent GPU errors, newest last. */
43
124
  readonly errors: ShardError[];
125
+ /** Whether the device works now (0061). */
126
+ status: GpuStatus;
44
127
  private readonly options;
45
128
  private readonly errorListeners;
46
129
  private readonly lostListeners;
47
- constructor(options: CreateGpuContextOptions, adapter: GPUAdapter, device: GPUDevice, format: GPUTextureFormat, context: GPUCanvasContext | undefined);
48
- /** Match the canvas backing size to its CSS size. Returns true if it changed. */
49
- resize(): boolean;
130
+ private readonly surfaces_;
131
+ private readonly ledger;
132
+ private readonly shared_;
133
+ private recreating;
134
+ private destroyed;
135
+ /** The WebGL2 shim's canvas contexts, when that's the backend. */
136
+ private readonly canvasContext;
137
+ constructor(options: CreateGpuContextOptions, adapter: GPUAdapter, device: GPUDevice, format: GPUTextureFormat, opened?: {
138
+ backend?: GpuBackendName;
139
+ tier?: GpuTier;
140
+ reasons?: GraphicsReason[];
141
+ hdrSampleCount?: 1 | 4;
142
+ canvasContext?: (canvas: HTMLCanvasElement | OffscreenCanvas) => unknown;
143
+ });
144
+ /** The canvases this device draws into, in the order they were added. */
145
+ get surfaces(): readonly Surface[];
146
+ /** Configures a canvas as a surface of this device. `surface.remove()` gives it back. */
147
+ addSurface(canvas: HTMLCanvasElement | OffscreenCanvas, options?: SurfaceOptions): Surface;
148
+ /** @internal Called by `Surface.remove()`. */
149
+ forgetSurface(surface: Surface): void;
150
+ /** WebGL2: where shader translations came from, and what naga translated (0064). */
151
+ shaderCache(): ShaderCacheStats | undefined;
152
+ /**
153
+ * WebGL2: every shader translation this session used, as a baked set to ship as
154
+ * `webgl2.shaders` (0064). Undefined on WebGPU.
155
+ */
156
+ exportShaderCache(): BakedTranslations | undefined;
157
+ /** Runs `fn` with new buffers and textures counted against `owner`. */
158
+ withOwner<T>(owner: string, fn: () => T): T;
159
+ /** Live buffers and textures this device made for `owner`, or for everyone (0052). */
160
+ stats(owner?: string): GpuStats;
161
+ /** Live bytes for `owner` (or everyone) by what they're for: targets, textures, geometry... (0062). */
162
+ memory(owner?: string): GpuMemory;
163
+ /**
164
+ * Bytes `owner` has written to the GPU since the device was made, by upload category, and the
165
+ * buffers and textures it created (0055). Cumulative: diff two reads for a frame's worth.
166
+ */
167
+ uploads(owner: string): GpuUploads;
168
+ /** Owners with live objects, sorted. */
169
+ owners(): string[];
170
+ /** Destroys every buffer and texture `owner` still has. Returns how many. */
171
+ release(owner: string): number;
172
+ /**
173
+ * A per-device object every app on this device shares, made once by `create` and counted against
174
+ * the device, not whichever app asked first. Made again after a device loss.
175
+ */
176
+ shared<T>(key: string, create: (device: GPUDevice) => T): T;
50
177
  onError(listener: GpuErrorListener): () => void;
51
178
  onDeviceLost(listener: (info: DeviceLostInfo) => void): () => void;
179
+ /** Error and device-loss listeners, for teardown tests. */
180
+ get listenerCount(): {
181
+ error: number;
182
+ lost: number;
183
+ };
52
184
  reportError(error: ShardError): void;
53
185
  /**
54
186
  * Runs `fn` inside a validation error scope. Errors are reported asynchronously (WebGPU
55
187
  * resolves error scopes later) with `label` naming the object.
56
188
  */
57
189
  validate<T>(label: string, fn: () => T): T;
58
- /** Gets a fresh adapter and device after a loss, and reconfigures the canvas. */
190
+ /**
191
+ * Gets a fresh adapter and device after a loss and reconfigures every surface with its alpha
192
+ * mode. Apps sharing the device all call it; they share one recreation.
193
+ */
59
194
  recreate(): Promise<void>;
195
+ /** Replaces the device, retrying as `options.recovery` says; `failed` once every try failed. */
196
+ private recover;
60
197
  /** For tests and debugging: destroys the device and reports it as an unexpected loss. */
61
198
  simulateDeviceLoss(): void;
199
+ /** Removes every surface and destroys the device. */
62
200
  destroy(): void;
201
+ private replaceDevice;
63
202
  private attach;
203
+ /**
204
+ * Counts every buffer and texture the device makes against the current owner, and every byte
205
+ * written into one against its owner and upload category.
206
+ */
207
+ private instrument;
64
208
  private handleLoss;
65
209
  }
210
+ export interface OpenedDevice {
211
+ adapter: GPUAdapter;
212
+ device: GPUDevice;
213
+ backend: GpuBackendName;
214
+ tier: GpuTier;
215
+ reasons: GraphicsReason[];
216
+ /** See `GpuCapabilities.hdrSampleCount`. */
217
+ hdrSampleCount: 1 | 4;
218
+ /** The entry point the device came from (its preferred canvas format). */
219
+ gpu: Pick<GPU, 'getPreferredCanvasFormat'>;
220
+ /** WebGL2: the shim's canvas contexts. */
221
+ canvasContext?: (canvas: HTMLCanvasElement | OffscreenCanvas) => unknown;
222
+ }
223
+ /**
224
+ * The device for `options` (0064): on WebGPU, a core device through exactly the request Shard has
225
+ * always made, else a compatibility-mode one; failing both (or asked to), a WebGL2 one. Both of
226
+ * the latter run the baseline tier. `keep` and `keepBackend` replace a lost device as it was: a
227
+ * core device isn't swapped for a compatibility one mid-session, or WebGPU for WebGL2.
228
+ */
229
+ export declare function openDevice(options: CreateGpuContextOptions, keep?: GpuTier, keepBackend?: GpuBackendName): Promise<OpenedDevice>;
230
+ /**
231
+ * A device, plus a first surface when `options.canvas` is given. On WebGPU without a core adapter
232
+ * it opens a compatibility-mode device and runs the baseline tier (`gpu.tier`, 0064).
233
+ */
66
234
  export declare function createGpuContext(options?: CreateGpuContextOptions): Promise<GpuContext>;