---
title: Buffers & ownership
description: The integration is deliberately narrow: vgpu adopts a device and wraps buffers — everything else stays in your hands.
---

# Buffers & ownership



## SDK signatures

```ts
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>;
```

```ts
declare class Device {
  /** Wraps a caller-owned GPUBuffer without taking ownership of its native lifetime. */
  wrapBuffer(buffer: GPUBuffer): Buffer;
}
```

| Error code                     | Condition                                                          |
| ------------------------------ | ------------------------------------------------------------------ |
| `VGPU-INIT-DEVICE-INVALID`     | The device passed to `initFromDevice` fails structural validation. |
| `VGPU-EXTERNAL-BUFFER-INVALID` | `wrapBuffer` 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.

```ts
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.

```ts
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

| Platform | Mode      | Public route                                                                 | Required lifetime                                                 |
| -------- | --------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Browser  | Snapshot  | `initFromDevice(device)`; one raw encoder copy into a vgpu-owned destination | retain Tensor through submit and `await gpu.device.queue.flush()` |
| Browser  | Reference | `gpu.device.wrapBuffer(tensor.gpuBuffer)`                                    | retain → wrap → submit → flush → wrapper dispose → Tensor dispose |
| Node     | Snapshot  | same route after user-side pinned Dawn and ORT initialization                | retain Tensor through submit and `await gpu.device.queue.flush()` |
| Node     | Reference | same route after user-side pinned Dawn and ORT initialization                | retain → 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.

```ts
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.

```ts
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.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)