# Size an RGBA8 Staging Buffer for WebGPU in Node.js

A gallery may load the same crop at several sizes. If it uploads decoded pixels through a staging buffer, multiplying width by height and four bytes can underestimate the required buffer span. The copy may place each row on an aligned boundary.

This tutorial builds a Node.js preflight for a single two-dimensional rgba8unorm upload at buffer offset zero. The tool takes dimensions and a byte budget, then reports the layout for a WebGPU copyBufferToTexture operation.

## Separate pixel bytes from row stride

Each RGBA8 pixel occupies four bytes. A 65-pixel row therefore contains 260 bytes of pixel data. WebGPU requires the supplied bytesPerRow for a buffer-to-texture copy to be a multiple of 256. Round the row size up to the next multiple, which gives 512 bytes here.

A row starts at its index times the stride. For three rows, the start offsets are 0, 512, and 1,024. The final row needs its 260 pixel bytes, so the minimum occupied span is 1,284 bytes. Allocating a whole stride for the last row is a separate choice if your allocator needs it.

## State what the budget measures

The budget in this example covers one staging buffer's required span. The destination texture, decoded source pixels and any simultaneously queued uploads each need their own accounting.

The program assumes an uncompressed RGBA8 format, one layer and offset zero. It always returns an explicit stride, including for a single row. Compressed formats and texture arrays have different block or layer calculations. Check the device's texture and buffer limits before creating resources.

## Implement the calculation

Save this as plan-copy.mjs. BigInt keeps intermediate arithmetic exact. The stride fits the layout's 32-bit field, and the total uses a decimal string so JSON can represent it even when it exceeds the safe Number range.

```javascript
const MAX_U32 = 0xffffffff;
const ROW_ALIGNMENT = 256n;

export function planRgba8Copy({ width, height, budgetBytes }) {
  for (const [name, value] of Object.entries({ width, height })) {
    if (!Number.isInteger(value) || value < 1 || value > MAX_U32) {
      throw new RangeError(`${name} must be a positive 32-bit unsigned integer`);
    }
  }
  if (!Number.isSafeInteger(budgetBytes) || budgetBytes < 0) {
    throw new RangeError("budgetBytes must be a nonnegative safe integer");
  }

  const rowBytes = BigInt(width) * 4n;
  const stride = ((rowBytes + ROW_ALIGNMENT - 1n) / ROW_ALIGNMENT) *
    ROW_ALIGNMENT;
  if (stride > BigInt(MAX_U32)) {
    throw new RangeError("bytesPerRow exceeds the layout field");
  }

  const required = BigInt(height - 1) * stride + rowBytes;
  return {
    rowBytes: Number(rowBytes),
    bytesPerRow: Number(stride),
    requiredBytes: required.toString(),
    fitsBudget: required <= BigInt(budgetBytes)
  };
}
```

## Check a copy from the command line

Save this entry point as check-copy.mjs. It prints a plan for a valid manifest. Exit status 0 means the span fits the budget, 1 means it exceeds the budget, and 2 means the input could not be used.

```javascript
import { readFile } from "node:fs/promises";
import { planRgba8Copy } from "./plan-copy.mjs";

try {
  const args = process.argv.slice(2);
  if (args.length !== 1) throw new Error("Usage: node check-copy.mjs copy.json");
  const manifest = JSON.parse(await readFile(args[0], "utf8"));
  const plan = planRgba8Copy(manifest);
  console.log(JSON.stringify(plan));
  if (!plan.fitsBudget) process.exitCode = 1;
} catch (error) {
  console.error(error.message);
  process.exitCode = 2;
}
```

Save the following example as copy.json.

```json
{"width":65,"height":3,"budgetBytes":1284}
```

Run the command from the folder containing the three files.

```bash
node check-copy.mjs copy.json
# {"rowBytes":260,"bytesPerRow":512,"requiredBytes":"1284","fitsBudget":true}
```

## Apply the layout to the upload

When filling the staging buffer, copy exactly rowBytes from each packed input row to the corresponding stride offset. Use bytesPerRow in the buffer copy layout. If the buffer has an existing allocation offset, add that offset when checking how much space is available.

Test a width of 64 as well as 65. At 64 pixels the row is already 256 bytes, so alignment adds no gap. At 65 the stride jumps to 512. A one-row upload of 65 pixels still occupies only 260 bytes under this layout.

## Sources and disclosure

The alignment and data-span rules are documented in the [W3C WebGPU texel copies section](https://www.w3.org/TR/webgpu/#texel-copies).

The workflow background is [PixMind's comparison of image sizes for iteration and review](https://www.pixmind.io/posts/midjourney-v8-1-hd-vs-sd). The dimensions here are synthetic and independent of any provider output size.

AI disclosure. AI assisted with drafting this article. The calculation and CLI were tested with synthetic manifests. This example performs byte arithmetic without decoding an image or issuing a GPU command.
