Part of #114.
Part of the "dogfood API" umbrella. Package: @vgpu/core. Purely additive.
Context
Teardown in real vgpu consumers is a long flat list of .destroy() calls. The Eve hero shader's
dispose() (render.ts:702-729 in vercel/eve) is ~25 consecutive lines of
x.destroy() / bloomTargets?.scene.destroy(), and the same six bloom-target destroys are
duplicated a second time inside the resize guard (render.ts:396-401). Adding a new resource
means remembering to add a line in two places, or leaking. There is no batch utility and no
integration with the TC39 explicit-resource-management standard (using, DisposableStack).
The groundwork already exists in core: Texture.destroy() and Buffer.destroy() are idempotent
(texture.ts:37-41, buffer.ts:24-28), Device.destroy() cascades to its readback pool
(device.ts:59-64), and all three classes already carry a dispose() alias
(texture.ts:43-45, buffer.ts:30-32, device.ts:66-68). This proposal adds the two missing
pieces: a batch call, and the standard dispose symbol.
Proposed API
// @vgpu/core
/** Anything with a destroy() or dispose() method — covers Texture, Buffer, Device,
* Shader, PingPong, raw GPUTexture/GPUBuffer, and native DisposableStack. */
export type Destroyable = { destroy(): void } | { dispose(): void };
/** Destroy every given resource. Variadic and flat (spread arrays at the callsite).
* null/undefined entries are skipped, so optional resources compose directly.
* Calls destroy() when present, else dispose(). No ordering guarantee.
* Safe to combine with `using`/double-destroy: vgpu destroy() is idempotent. */
export function destroyAll(
...resources: ReadonlyArray<Destroyable | null | undefined>
): void;// Symbol.dispose alias on Texture, Buffer, Device (and PingPong / future resources).
// Runtime-safe symbol resolution — the same fallback TypeScript's `using` emit uses:
const disposeSymbol: typeof Symbol.dispose =
Symbol.dispose ?? (Symbol.for("Symbol.dispose") as typeof Symbol.dispose);
class Texture {
destroy(): void; // existing, idempotent
dispose(): void; // existing alias
[disposeSymbol](): void { this.destroy(); } // NEW — enables `using` / DisposableStack
}Implementation (whole utility, ~7 lines):
export function destroyAll(
...resources: ReadonlyArray<Destroyable | null | undefined>
): void {
for (const r of resources) {
if (!r) continue;
if ("destroy" in r) r.destroy();
else r.dispose();
}
}Support story. vgpu is on TS 5.7 (Symbol.dispose typing landed in 5.2); the only repo change
needed is adding "ESNext.Disposable" to lib in tsconfig.base.json (currently
["ES2022","DOM"]). At runtime, Symbol.dispose is native in Node ≥ 18.18 and current evergreen
browsers; the Symbol.for("Symbol.dispose") fallback above matches TypeScript's downlevel using
emit, so using works on every runtime consumers target — vgpu ships no polyfill. Note that
DisposableStack itself is native only in newer runtimes (Node 24+, 2025-era browsers), which is
exactly why the zero-dependency destroyAll carries its weight today.
Example
// Eve teardown (render.ts:702-729) — the resource list stays visible at the dispose site:
dispose() {
destroyAll(
gpuMesh.vertexBuffer, gpuMesh.indexBuffer, gpuMesh.lineIndexBuffer,
insideParams.buffer, backDepthParams.buffer,
outsideParams.buffer, outsideParams.fallbackBackMaterial, outsideParams.fallbackBackDepth,
opaqueOutsideParams.buffer, opaqueOutsideParams.fallbackBackMaterial, opaqueOutsideParams.fallbackBackDepth,
wireParams.buffer, wireParams.fallbackBackMaterial, wireParams.fallbackBackDepth,
envBgParams.buffer, blurParamsBuffer, compositeParamsBuffer, previewMode,
bloomTargets?.scene, bloomTargets?.backMaterial, bloomTargets?.backDepth, // nullish-
bloomTargets?.backSurfaceDepth, bloomTargets?.horizontal, bloomTargets?.vertical, // tolerant
);
}
// Lexically scoped code (tests, offline render harness) — the standard does the rest:
using device = await createDevice();
using staging = device.createBuffer({ size, usage: ["map_read", "copy_dst"] });
// ...automatic cleanup at scope exit, even on throw
// Non-lexical lifetimes (React unmount) — native DisposableStack, no vgpu group class needed:
const stack = new DisposableStack();
const tex = stack.use(device.createTexture({ ... }));
return () => stack.dispose();Same example without the util
// Today (render.ts:702-729, abridged — 25 lines of this):
dispose() {
gpuMesh.vertexBuffer.destroy();
gpuMesh.indexBuffer.destroy();
gpuMesh.lineIndexBuffer.destroy();
insideParams.buffer.destroy();
backDepthParams.buffer.destroy();
outsideParams.buffer.destroy();
outsideParams.fallbackBackMaterial.destroy();
outsideParams.fallbackBackDepth.destroy();
// ...11 more lines...
bloomTargets?.scene.destroy();
bloomTargets?.backMaterial.destroy();
bloomTargets?.backDepth.destroy();
bloomTargets?.backSurfaceDepth.destroy();
bloomTargets?.horizontal.destroy();
bloomTargets?.vertical.destroy();
}
// And no `using` support at all: try/finally by hand in every test and harness.Alternatives
Symbol.disposeonly, nodestroyAll— makes batch teardown depend onDisposableStack, which is not yet native everywhere and forces.use()registration at creation time, scattering the ownership list. Eve's dispose is a flat list read in one place;destroyAllpreserves that shape at ~7 lines of implementation cost. Rejected.destroyAllaccepting nested arrays — spread already flattens at the callsite (destroyAll(...bloomTargets, extra)); recursion adds type and behavior surface for zero expressiveness. Rejected (explicit > magic).DisposableGroup/ auto-registration ondevice.create*— a competing group abstraction the standard already provides, plus hidden lifetime coupling that breaks intentional early destroys (e.g.Texture.resize). Rejected — the standard is the group abstraction.
Activity
0No activity yet.