目次

webgpufundamentals.org

Fix, Fork, Contribute

WebGPU データのコピー

この記事はGemini Code Assistによって自動翻訳されました。翻訳に問題がある場合は、お手数ですがこちらからPull Requestを送信してください。

これまでのほとんどの記事では、バッファにデータを入れるためにwriteBuffer関数を使用し、テクスチャにデータを入れるためにwriteTexture関数を使用してきました。バッファやテクスチャにデータを入れる方法はいくつかあります。

writeBuffer

writeBufferは、JavaScriptのTypedArrayまたはArrayBufferからバッファにデータをコピーします。これは、バッファにデータを取り込む最も簡単な方法と言えるでしょう。

writeBufferは次の形式に従います。

device.queue.writeBuffer(
  destBuffer,  // 書き込み先のバッファ
  destOffset,  // 書き込みを開始する宛先バッファ内の場所
  srcData,     // TypedArrayまたはArrayBuffer
  srcOffset?,  // コピーを開始するsrcData内の**要素**のオフセット
  size?,       // コピーするsrcDataの**要素**のサイズ
)

srcOffsetが渡されない場合は0です。sizeが渡されない場合はsrcDataのサイズです。

重要:srcOffsetsizesrcDataの要素単位です。

言い換えると、

device.queue.writeBuffer(
  someBuffer,
  someOffset,
  someFloat32Array,
  6,
  7,
)

上記のコードは、float32 #6から7つのfloat32のデータをコピーします。別の言い方をすれば、someFloat32ArrayがビューであるArrayBufferの部分から、バイト24から始まる28バイトをコピーします。

writeTexture

writeTextureは、JavaScriptのTypedArrayまたはArrayBufferからテクスチャにデータをコピーします。

writeTextureには次のシグネチャがあります。

device.writeTexture(
  // 宛先の詳細
  { texture, mipLevel: 0, origin: [0, 0, 0], aspect: "all" },

  // ソースデータ
  srcData,

  // ソースデータの詳細
  { offset: 0, bytesPerRow, rowsPerImage },

  // サイズ:
  [ width, height, depthOrArrayLayers ] または { width, height, depthOrArrayLayers }
)

注意点:

  • textureにはGPUTextureUsage.COPY_DSTの使用法が必要です。

  • mipLeveloriginaspectはすべてデフォルト値を持つため、指定する必要がない場合が多いです。

  • bytesPerRow:これは、データの次のブロック行に進むために進むバイト数です。

    これは、1つ以上のブロック行をコピーする場合に必要です。ほとんどの場合、1つ以上のブロック行をコピーしているため、ほとんどの場合に必要です。

  • rowsPerImage:これは、ある画像の開始から次の画像に進むために進むブロック行の数です。

    これは、1つ以上のレイヤーをコピーする場合に必要です。つまり、サイズ引数のdepthOrArrayLayersが1より大きい場合は、この値を指定する必要があります。

コピーは次のように機能すると考えることができます。

   // 擬似コード
   const [x, y, z] = origin ?? [0, 0, 0];
   const [blockWidth, blockHeight, bytesPerBlock] = 
      getBlockInfoForTextureFormat(texture.format);

   const blocksAcross = width / blockWidth;
   const blocksDown = height / blockHeight;
   const bytesPerBlockRow = blocksAcross * bytesPerBlock;

   for (layer = 0; layer < depthOrArrayLayers; layer) {
      for (row = 0; row < blocksDown; ++row) {
        const start = offset + (layer * rowsPerImage + row) * bytesPerRow;
        copyRowToTexture(
            texture,               // コピー先のテクスチャ
            x, y + row, z + layer, // コピー先のテクスチャ内の場所
            srcDataAsBytes + start,
            bytesPerBlockRow);
      }
   }

ブロック行

テクスチャはブロックに編成されています。ほとんどの通常のテクスチャでは、ブロックの幅と高さは両方とも1です。圧縮テクスチャでは、これが変わります。たとえば、フォーマットbc1-rgba-unormのブロック幅は4、ブロック高さは4です。つまり、幅を8、高さを12に設定すると、6つのブロックしかコピーされません。最初の行に2ブロック、2番目の行に2ブロック、3番目の行に2ブロックです。

圧縮テクスチャの場合、サイズと原点はブロックサイズに合わせる必要があります。

重要:WebGPUでサイズ(GPUExtent3Dとして定義)を受け取る場所はどこでも、1〜3個の数値の配列、または1〜3個のプロパティを持つオブジェクトのいずれかになります。heightdepthOrArrayLayersはデフォルトで1なので、

  • [2] 幅=2、高さ=1、depthOrArrayLayers=1のサイズ
  • [2, 3] 幅=2、高さ=3、depthOrArrayLayers=1のサイズ
  • [2, 3, 4] 幅=2、高さ=3、depthOrArrayLayers=4のサイズ
  • { width: 2 } 幅=2、高さ=1、depthOrArrayLayers=1のサイズ
  • { width: 2, height: 3 } 幅=2、高さ=3、depthOrArrayLayers=1のサイズ
  • { width: 2, height: 3, depthOrArrayLayers: 4 } 幅=2、高さ=3、depthOrArrayLayers=4のサイズ

同様に、原点が表示される場所(デフォルトはGPUOrigin3D)はどこでも、3つの数値の配列、またはxyzプロパティを持つオブジェクトのいずれかを持つことができます。それらはすべてデフォルトで0なので、

  • [5] x=5、y=0、z=0の原点
  • [5, 6] x=5、y=6、z=0の原点
  • [5, 6, 7] x=5、y=6、z=7の原点
  • { x: 5 } x=5、y=0、z=0の原点
  • { x: 5, y: 6 } x=5、y=6、z=0の原点
  • { x: 5, y: 6, z: 7 } x=5、y=6、z=7の原点
  • aspectは、深度ステンシル形式にデータをコピーする場合にのみ実際に機能します。一度に1つのアスペクト(depth-onlyまたはstencil-onlyのいずれか)にのみコピーできます。

豆知識:テクスチャにはwidthheightdepthOrArrayLayerプロパティがあり、これは有効なGPUExtent3Dであることを意味します。つまり、このテクスチャが与えられた場合、

const texture = device.createTexture({
  format: 'r8unorm',
  size: [2, 4],
  usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_ATTACHMENT,
});

これらはすべて機能します。

// 2x4ピクセルのデータをテクスチャにコピーします
const bytesPerRow = 2;
device.queue.writeTexture({ texture }, data, { bytesPerRow }, [2, 4]);
device.queue.writeTexture({ texture }, data, { bytesPerRow }, [texture.width, texture.height]);
device.queue.writeTexture({ texture }, data, { bytesPerRow }, {width: 2, height: 4});
device.queue.writeTexture({ texture }, data, { bytesPerRow }, {width: texture.width, height: texture.height});
device.queue.writeTexture({ texture }, data, { bytesPerRow }, texture); // !!!

最後のものは、テクスチャにwidthheightdepthOrArrayLayersがあるため機能します。あまり明確ではないため、そのスタイルは使用していませんが、有効です。

copyBufferToBuffer

copyBufferToBufferは、名前が示すように、あるバッファから別のバッファにデータをコピーします。

シグネチャ:

encoder.copyBufferToBuffer(
  source,       // コピー元のバッファ
  sourceOffset, // コピーを開始する場所
  dest,         // コピー先のバッファ
  destOffset,   // コピーを開始する場所
  size,         // コピーするバイト数
)
  • sourceにはGPUBufferUsage.COPY_SRCの使用法が必要です。
  • destにはGPUBufferUsage.COPY_DSTの使用法が必要です。
  • sizeは4の倍数でなければなりません。

copyBufferToTexture

copyBufferToTextureは、名前が示すように、バッファからテクスチャにデータをコピーします。

シグネチャ:

encoder.copyBufferToTexture(
  // ソースバッファの詳細
  { buffer, offset: 0, bytesPerRow, rowsPerImage },

  // 宛先テクスチャの詳細
  { texture, mipLevel: 0, origin: [0, 0, 0], aspect: "all" },

  // サイズ:
  [ width, height, depthOrArrayLayers ] または { width, height, depthOrArrayLayers }
)

これはwriteTextureとほぼ同じパラメータを持っています。最大の違いは、bytesPerRow256の倍数でなければならないことです。

  • textureにはGPUTextureUsage.COPY_DSTの使用法が必要です。
  • bufferにはGPUBufferUsage.COPY_SRCの使用法が必要です。

copyTextureToBuffer

copyTextureToBufferは、名前が示すように、テクスチャからバッファにデータをコピーします。

シグネチャ:

encoder.copyTextureToBuffer(
  // ソーステクスチャの詳細
  { texture, mipLevel: 0, origin: [0, 0, 0], aspect: "all" },

  // 宛先バッファの詳細
  { buffer, offset: 0, bytesPerRow, rowsPerImage },

  // サイズ:
  [ width, height, depthOrArrayLayers ]
)

これはcopyBufferToTextureと同様のパラメータを持っていますが、テクスチャ(現在はソース)とバッファ(現在は宛先)が入れ替わっています。copyBufferToTextureと同様に、bytesPerRow256の倍数でなければなりません

  • textureにはGPUTextureUsage.COPY_SRCの使用法が必要です。
  • bufferにはGPUBufferUsage.COPY_DSTの使用法が必要です。

copyTextureToTexture

copyTextureToTextureは、あるテクスチャの一部を別のテクスチャにコピーします。

2つのテクスチャは、同じフォーマットであるか、接尾辞'-srgb'のみが異なる必要があります。

シグネチャ:

encoder.copyTextureToTexture(
  // ソーステクスチャの詳細
  src: { texture, mipLevel: 0, origin: [0, 0, 0], aspect: "all" },

  // 宛先テクスチャの詳細
  dst: { texture, mipLevel: 0, origin: [0, 0, 0], aspect: "all" },

  // サイズ:
  [ width, height, depthOrArrayLayers ] または { width, height, depthOrArrayLayers }
);
  • src.textureにはGPUTextureUsage.COPY_SRCの使用法が必要です。
  • dst.textureにはGPUTextureUsage.COPY_DSTの使用法が必要です。
  • widthはブロック幅の倍数でなければなりません。
  • heightはブロック高さの倍数でなければなりません。
  • src.origin[0]または.xはブロック幅の倍数でなければなりません。
  • src.origin[1]または.yはブロック高さの倍数でなければなりません。
  • dst.origin[0]または.xはブロック幅の倍数でなければなりません。
  • dst.origin[1]または.yはブロック高さの倍数でなければなりません。

シェーダー

シェーダーは、ストレージバッファ、ストレージテクスチャに読み書きでき、間接的にテクスチャにレンダリングできます。これらはすべて、バッファやテクスチャにデータを取り込む方法です。つまり、データを生成したり、コピーしたり、転送したりするシェーダーを作成できます。

バッファのマッピング

バッファをマップできます。バッファをマップするということは、JavaScriptから読み書きできるようにすることです。少なくともWebGPUのバージョン1では、マップ可能なバッファには厳しい制限があります。つまり、マップ可能なバッファは、コピー元またはコピー先の一時的な場所としてのみ使用できます。マップ可能なバッファは、他の種類のバッファ(ユニフォームバッファ、頂点バッファ、インデックスバッファ、ストレージバッファなど)として使用することはできません[1]

2つの使用法フラグの組み合わせでマップ可能なバッファを作成できます。

  • GPUBufferUsage.MAP_READ | GPU_BufferUsage.COPY_DST

    これは、上記のコピーコマンドを使用して別のバッファまたはテクスチャからデータをコピーし、それをマップしてJavaScriptで値を読み取ることができるバッファです。

  • GPUBufferUsage.MAP_WRITE | GPU_BufferUsage.COPY_SRC

    これは、JavaScriptでマップできるバッファです。JavaScriptからデータを入れ、最後にマップを解除して、上記のコピーコマンドを使用してその内容を別のバッファまたはテクスチャにコピーできます。

バッファのマッピングプロセスは非同期です。buffer.mapAsync(mode, offset = 0, size?)を呼び出します。ここで、offsetsizeはバイト単位です。sizeが指定されていない場合は、バッファ全体のサイズです。modeGPUMapMode.READまたはGPUMapMode.WRITEのいずれかでなければならず、もちろん、バッファを作成したときに渡したMAP_使用法フラグと一致する必要があります。

mapAsyncPromiseを返します。プロミスが解決されると、バッファはマップ可能になります。次に、buffer.getMappedRange(offset = 0, size?)を呼び出して、バッファの一部またはすべてを表示できます。ここで、offsetはマップしたバッファの部分へのバイトオフセットです。getMappedRangeArrayBufferを返すため、一般的に、何らかの用途に使用するには、それを使用してTypedArrayを構築します。

バッファをマッピングする例を次に示します。

const buffer = device.createBuffer({
  size: 1024,
  usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});

// バッファ全体をマップします
await buffer.mapAsync(GPUMapMode.READ);

// バッファ全体を32ビット浮動小数点数の配列として取得します
const f32 = new Float32Array(buffer.getMappedRange())

...

buffer.unmap();

注:一度マップされると、unmapを呼び出すまでバッファはWebGPUで使用できません。unmapが呼び出された瞬間に、バッファはJavaScriptから消えます。つまり、上記の例を考えてみましょう。

const f32 = new Float32Array(buffer.getMappedRange())

f32[0] = 123;
console.log(f32[0]); // 123を出力します

buffer.unmap();

console.log(f32[0]); // undefinedを出力します

最初の記事で、ストレージバッファ内の数値を2倍にし、その結果をマップ可能なバッファにコピーして、結果を読み取るためにマップした例をすでに見てきました。

もう1つの例は、コンピュートシェーダーの基本に関する記事です。ここでは、さまざまな@builtinコンピュートシェーダーの値をストレージバッファに出力しました。次に、それらの結果をマップ可能なバッファにコピーし、結果を読み取るためにマップしました。

mappedAtCreation

mappedAtCreation: trueは、バッファを作成するときに追加できるフラグです。この場合、バッファにはGPUBufferUsage.COPY_DSTGPUBufferUsage.MAP_WRITEも使用法フラグは必要ありません。

これは、作成時にバッファにデータを入れるための特別なフラグです。バッファを作成するときにmappedAtCreation: trueフラグを追加します。バッファは、書き込み用にすでにマップされた状態で作成されます。例:

 const buffer = device.createBuffer({
   size: 16,
   usage: GPUBufferUsage.UNIFORM,
   mappedAtCreation: true,
 });
 const arrayBuffer = buffer.getMappedRange(0, buffer.size);
 const f32 = new Float32Array(arrayBuffer);
 f32.set([1, 2, 3, 4]);
 buffer.unmap();

または、より簡潔に

 const buffer = device.createBuffer({
   size: 16,
   usage: GPUBufferUsage.UNIFORM,
   mappedAtCreation: true,
 });
 new Float32Array(buffer.getMappedRange(0, buffer.size)).set([1, 2, 3, 4]);
 buffer.unmap();

mappedAtCreation: trueで作成されたバッファには、自動的にフラグが設定されないことに注意してください。これは、最初に作成するときにバッファにデータを入れるための便宜上のものです。作成時にマップされ、一度マップを解除すると、他のバッファと同様に動作し、指定した使用法に対してのみ機能します。つまり、後でコピーしたい場合はGPUBufferUsage.COPY_DSTが必要であり、後でマップしたい場合はGPUBufferData.MAP_READまたはGPUBufferData.MAP_WRITEが必要です。

マップ可能なバッファを効率的に使用する

上記で、バッファのマッピングが非同期であることがわかりました。つまり、mapAsyncを呼び出してバッファのマッピングを要求してから、マップされてgetMappedRangeを呼び出すことができるようになるまで、不確定な時間がかかります。

これを回避する一般的な方法は、常にマップされたバッファのセットを保持することです。すでにマップされているため、すぐに使用できます。1つ使用してマップを解除し、バッファを使用するコマンドを送信したらすぐに、再度マップするように要求します。プロミスが解決されると、すでにマップされたバッファのプールに戻します。マップされたバッファが必要で、利用可能なものがない場合は、新しいものを作成してプールに追加します。


  1. 例外は、mappedAtCreation: trueを設定した場合です。mappedAtCreationを参照してください。 ↩︎

問題点/バグ? githubでissueを作成.
comments powered by Disqus