Buffers & ownership

SDK signatures

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export interface InitOptions {
  readonly adapter?: VGPUAdapter;
  /** Never set: adoption lives in `initFromDevice(device)`. */
  readonly device?: never;
  readonly powerPreference?: GPUPowerPreference;
  readonly requiredFeatures?: readonly GPUFeatureName[];
  readonly requiredLimits?: RequiredDeviceLimits;
  readonly label?: string;
}
 
/** vgpu creates the device and destroys it on dispose. */
export declare function init(options?: InitOptions): Promise<Gpu>;
 
/** vgpu adopts a device it did not create and never destroys it. */
export declare function initFromDevice(device: GPUDevice): Promise<Gpu>;
TypeScript
1
2
3
4
declare class Device {
  /** Wraps a caller-owned GPUBuffer without taking ownership of its native lifetime. */
  wrapBuffer(buffer: GPUBuffer): Buffer;
}
Error codeCondition
VGPU-INIT-DEVICE-INVALIDThe device passed to initFromDevice fails structural validation.
VGPU-EXTERNAL-BUFFER-INVALIDwrapBuffer receives a value without finite size and usage.

To adopt a device owned by another library, call initFromDevice(device) instead of init(). The two entry points are separate on purpose: init() stays byte-minimal for apps that let vgpu create its own device, and bundlers drop initFromDevice entirely when you do not import it.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
import * as ort from "onnxruntime-web/webgpu";
import { init, initFromDevice } from "vgpu";
 
// Requested device — vgpu creates it and destroys it on dispose.
const owned = await init({ powerPreference: "high-performance" });
 
// Adopted device — vgpu borrows it and never destroys it.
const gpu = await initFromDevice(await ort.env.webgpu.device);
 
// A device is not an init() option.
// @ts-expect-error adoption is initFromDevice(device)
await init({ device: await ort.env.webgpu.device });

Device.wrapBuffer wraps a caller-owned GPUBuffer in a vgpu Buffer without taking ownership. wrapper.gpu is the exact object you passed in. Disposing the wrapper detaches it from vgpu but never destroys the underlying buffer, and dispose is idempotent — a double dispose is a no-op.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
import type { Gpu } from "vgpu";
 
declare const gpu: Gpu;
declare const raw: GPUBuffer;
 
const wrapper = gpu.device.wrapBuffer(raw); // raw: a GPUBuffer you own
 
wrapper.gpu === raw; // true — same object, no copy
wrapper.dispose();   // detaches from vgpu; raw is NOT destroyed
wrapper.dispose();   // no-op — dispose is idempotent
raw.size;            // still valid — its lifetime never left your hands

Platform and ownership matrix

PlatformModePublic routeRequired lifetime
BrowserSnapshotinitFromDevice(device); one raw encoder copy into a vgpu-owned destinationretain Tensor through submit and await gpu.device.queue.flush()
BrowserReferencegpu.device.wrapBuffer(tensor.gpuBuffer)retain → wrap → submit → flush → wrapper dispose → Tensor dispose
NodeSnapshotsame route after user-side pinned Dawn and ORT initializationretain Tensor through submit and await gpu.device.queue.flush()
NodeReferencesame route after user-side pinned Dawn and ORT initializationretain → wrap → submit → flush → wrapper dispose → Tensor dispose

Choose one of two consumption modes. Snapshot copies the model output once, GPU-to-GPU, into a buffer vgpu owns; after the copy you are decoupled from the runtime and may free its tensor at any time. Reference wraps the runtime's buffer directly with zero copies; the runtime keeps ownership and you must respect its lifetime.

Reference mode

In reference mode, always await gpu.device.queue.flush() before disposing the source tensor. Do not dispose the tensor while vgpu work that reads it is still in flight — skipping the flush is an experimental fast path, not a supported contract.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import * as ort from "onnxruntime-web/webgpu";
import type { Buffer, Compute, Gpu } from "vgpu";
 
declare const session: ort.InferenceSession;
declare const input: ort.Tensor;
declare const gpu: Gpu;
declare const compute: Compute;
declare const destination: Buffer;
declare const workgroups: number;
 
const output = (await session.run({ input })).output;       // 1. retain the tensor
const source = gpu.device.wrapBuffer(output.gpuBuffer);     // 2. wrap — zero copies
compute.set({ source, destination }).dispatch(workgroups);  // 3. submit vgpu work
await gpu.device.queue.flush();                             // 4. flush — required
source.dispose();                                           // 5. drop the wrapper
output.dispose();                                           // 6. runtime may free its buffer now

Snapshot mode

Snapshot needs no new API: copy once with the raw escape hatch (gpu.gpu is the shared GPUDevice, buffer.gpu the raw GPUBuffer), then treat the destination as any other vgpu buffer.

TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import * as ort from "onnxruntime-web/webgpu";
import type { Gpu } from "vgpu";
 
declare const session: ort.InferenceSession;
declare const input: ort.Tensor;
declare const gpu: Gpu;
 
const output = (await session.run({ input })).output;
const destination = gpu.device.createBuffer({
  size: output.gpuBuffer.size,
  usage: ["storage", "copy_dst"],
});
 
const encoder = gpu.gpu.createCommandEncoder();
encoder.copyBufferToBuffer(output.gpuBuffer, 0, destination.gpu, 0, output.gpuBuffer.size);
gpu.gpu.queue.submit([encoder.finish()]);
await gpu.device.queue.flush();
 
output.dispose(); // decoupled — destination is yours, independent of the runtime

Scope

The integration is deliberately narrow: vgpu adopts a device and wraps buffers — everything else stays in your hands.

vgpu does not import ORT, resolve ORT WASM assets, mutate Node globals, validate GPUBuffer provenance, interpret Tensor dtype/shape/layout, recover a lost device, or transfer ownership of the borrowed device or buffer.