目录

webgpufundamentals.org

Fix, Fork, Contribute

WebGPU 数据内存布局

在 WebGPU 中,您提供的几乎所有数据都需要在内存中进行布局,以便与着色器中的定义相匹配。这与 JavaScript 和 TypeScript 形成了巨大反差,在 JavaScript 和 TypeScript 中,内存布局问题很少出现。

在 WGSL 中,当您编写着色器时,通常会定义 struct结构体。结构体有点像 JavaScript 中的对象。结构体的成员,类似于 JavaScript 对象的属性。但是除了给每个属性命名外,您还必须给它定义一个类型。此外,在提供数据时,您还需要计算结构体中的特定成员将出现在缓冲区的哪个位置。

WGSL v1 中, 有 4 中基本类型

  • f32 (32 位浮点数)
  • i32 (32 位整数)
  • u32 (32 位无符号整数)
  • f16 (16 位浮点数) [1]

1 个字节为 8 位,所以一个 32 位的值需要 4 个字节,一个 16 位的值则需要 2 个字节。

如果我们定义个类似下面这样的结构体

struct OurStruct {
  velocity: f32,
  acceleration: f32,
  frameCount: u32,
};

这种结构体的可视化表示可能是这样的

每个方形块为一个字节。正如图中所示,一共需要 12 个字节,velocity占前 4 个字节,acceleration占后 4 个字节,frameCount占最后 4 个字节。

要将数据传递给着色器,我们需要准备数据以匹配内存布局 OurStruct。为此,我们需要创建一个 12 字节的 ArrayBuffer 然后设置正确类型的 TypedArray 视图,这样我们就可以将其填满。

const kOurStructSizeBytes =
    4 + // velocity
    4 + // acceleration
    4; // frameCount
const ourStructData = new ArrayBuffer(kOurStructSizeBytes);
const ourStructValuesAsF32 = new Float32Array(ourStructData);
const ourStructValuesAsU32 = new Uint32Array(ourStructData);

在上面,ourStructData 是一个 ArrayBuffer,它是一块内存空间。,ourStructValuesAsF32ourStructValuesAsU32的内存空间是同一个,但是显示的方式不同,一个是以 32 位浮点数显示值的内存视图,另一个则是以 32 位无符号整数显示值得内存视图。

现在我们有了一个缓冲区和两个视图,可以在结构体上设置数据了。

const kVelocityOffset = 0;
const kAccelerationOffset = 1;
const kFrameCountOffset = 2;

ourStructValuesAsF32[kVelocityOffset] = 1.2;
ourStructValuesAsF32[kAccelerationOffset] = 3.4;
ourStructValuesAsU32[kFrameCountOffset] = 56; // an integer value

TypedArrays

请注意,就像编程中的许多事情一样,我们有多种方法可以做到这一点。TypedArray 的构造函数有多种形式。例如

  • new Float32Array(12) 该版本创建了一个新的 ArrayBuffer,在本例中为 12 * 4 字节。然后创建 Float32Array类型的内存视图 来查看它。

  • new Float32Array([4, 5, 6]) 该版本创建了一个新的 ArrayBuffer,在本例中为 3 * 4 字节。然后创建 Float32Array类型的内存视图 来查看它。并且设置其初始值为 4, 5, 6。

    请注意,您也可以传递另一个 TypedArray作为参数。例如

    new Float32Array(someUint8ArrayOf6Values) 将新建一个大小为 6 * 4 的 ArrayBuffer,然后创建一个 Float32Array 来查看它,再将现有视图中的值复制到新的 Float32Array 中。数值是按数字而不是二进制复制的。换句话说,它们是这样被复制的

    srcArray.forEach((v, i) => (dstArray[i] = v));
    
  • new Float32Array(someArrayBuffer) 这就是我们之前使用的例子。在现有缓冲区上新建一个 Float32Array 视图。

  • new Float32Array(someArrayBuffer, byteOffset)

    这将在现有缓冲区上创建一个新的 Float32Array,但会从byteOffset偏移处开始创建视图

  • new Float32Array(someArrayBuffer, byteOffset, length) 这会在现有缓冲区上新建一个 Float32Array。视图从 byteOffset偏移处开始,且具有length个单位长度。因此,如果我们以 3 为长度,该视图将是 3 个 float32 值(12 字节)的 someArrayBuffer

使用最后一种形式,我们可以将上面的代码改为

const kOurStructSizeBytes =
    4 + // velocity
    4 + // acceleration
    4; // frameCount
const ourStructData = new ArrayBuffer(kOurStructSizeBytes);
const velocityView = new Float32Array(ourStructData, 0, 1);
const accelerationView = new Float32Array(ourStructData, 4, 1);
const frameCountView = new Uint32Array(ourStructData, 8, 1);

velocityView[0] = 1.2;
accelerationView[0] = 3.4;
frameCountView[0] = 56;

此外,每个类型数组都具有以下属性

TypedArray 有多种方法,其中许多方法与 Array 相似,但有一种方法与 Array 不同,那就是 subarray。它可以创建一个新的相同类型的 TypedArray 视图。它的参数是 subarray(begin,end), end 索引的元素不在其中。因此,someTypedArray.subarray(5, 10) 将创建一个新的TypedArray,其中的 ArrayBuffer 与原来的TypedArray中的ArrayBuffer是同一个。其中包含 someTypedArray 的第 5 至 9 个元素。

所以我们能够将上面的代码改下如下

const kOurStructSizeFloat32Units =
    1 + // velocity
    1 + // acceleration
    1 ; // frameCount
const ourStructDataAsF32 = new Float32Array(kOurStructSizeFloat32Units);
const ourStructDataAsU32 = new Uint32Array(ourStructDataAsF32.buffer);
const velocityView = ourStructDataAsF32.subarray(0, 1);
const accelerationView = ourStructDataAsF32.subarray(1, 2);
const frameCountView = ourStructDataAsU32.subarray(2, 3);

velocityView[0] = 1.2;
accelerationView[0] = 3.4;
frameCountView[0] = 56;

WGSL 有 4 种基本类型。它们是:

typedescriptionshort name
vec2<f32>a type with 2 f32svec2f
vec2<u32>a type with 2 u32svec2u
vec2<i32>a type with 2 i32svec2i
vec2<f16>a type with 2 f16svec2h
vec3<f32>a type with 3 f32svec3f
vec3<u32>a type with 3 u32svec3u
vec3<i32>a type with 3 i32svec3i
vec3<f16>a type with 3 f16svec3h
vec4<f32>a type with 4 f32svec4f
vec4<u32>a type with 4 u32svec4u
vec4<i32>a type with 4 i32svec4i
vec4<f16>a type with 4 f16svec4h
mat2x2<f32>a matrix of 2 vec2<f32>smat2x2f
mat2x2<u32>a matrix of 2 vec2<u32>smat2x2u
mat2x2<i32>a matrix of 2 vec2<i32>smat2x2i
mat2x2<f16>a matrix of 2 vec2<f16>smat2x2h
mat2x3<f32>a matrix of 2 vec3<f32>smat2x3f
mat2x3<u32>a matrix of 2 vec3<u32>smat2x3u
mat2x3<i32>a matrix of 2 vec3<i32>smat2x3i
mat2x3<f16>a matrix of 2 vec3<f16>smat2x3h
mat2x4<f32>a matrix of 2 vec4<f32>smat2x4f
mat2x4<u32>a matrix of 2 vec4<u32>smat2x4u
mat2x4<i32>a matrix of 2 vec4<i32>smat2x4i
mat2x4<f16>a matrix of 2 vec4<f16>smat2x4h
mat3x2<f32>a matrix of 3 vec2<f32>smat3x2f
mat3x2<u32>a matrix of 3 vec2<u32>smat3x2u
mat3x2<i32>a matrix of 3 vec2<i32>smat3x2i
mat3x2<f16>a matrix of 3 vec2<f16>smat3x2h
mat3x3<f32>a matrix of 3 vec3<f32>smat3x3f
mat3x3<u32>a matrix of 3 vec3<u32>smat3x3u
mat3x3<i32>a matrix of 3 vec3<i32>smat3x3i
mat3x3<f16>a matrix of 3 vec3<f16>smat3x3h
mat3x4<f32>a matrix of 3 vec4<f32>smat3x4f
mat3x4<u32>a matrix of 3 vec4<u32>smat3x4u
mat3x4<i32>a matrix of 3 vec4<i32>smat3x4i
mat3x4<f16>a matrix of 3 vec4<f16>smat3x4h
mat4x2<f32>a matrix of 4 vec2<f32>smat4x2f
mat4x2<u32>a matrix of 4 vec2<u32>smat4x2u
mat4x2<i32>a matrix of 4 vec2<i32>smat4x2i
mat4x2<f16>a matrix of 4 vec2<f16>smat4x2h
mat4x3<f32>a matrix of 4 vec3<f32>smat4x3f
mat4x3<u32>a matrix of 4 vec3<u32>smat4x3u
mat4x3<i32>a matrix of 4 vec3<i32>smat4x3i
mat4x3<f16>a matrix of 4 vec3<f16>smat4x3h
mat4x4<f32>a matrix of 4 vec4<f32>smat4x4f
mat4x4<u32>a matrix of 4 vec4<u32>smat4x4u
mat4x4<i32>a matrix of 4 vec4<i32>smat4x4i
mat4x4<f16>a matrix of 4 vec4<f16>smat4x4h

已知 vec3f 是一个有 3 个 f32 的类型,而 mat4x4f 是一个由 f32 组成的 4x4 矩阵,因此它有 16 个 f32,那么你认为下面的结构在内存中是什么样子的?

struct Ex2 {
  scale: f32,
  offset: vec3f,
  projection: mat4x4f,
};

想好了吗?

这是怎么回事?原来,每种类型都有对齐要求。对于给定的类型,它必须对齐到一定字节数的倍数。

以下是各种类型的尺寸和对齐方式。

但等等,还有一个问题!

你认为这个结构体的布局会是怎样的?

struct Ex3 {
  transform: mat3x3f,
  directions: array<vec3f, 4>,
};

array<type, count> 语法定义了一个具有count元素的数组类型。

请看…

如果查看对齐表,你会发现 vec3<f32> 的对齐方式为 16 字节。这意味着,无论是矩阵还是数组中的每个 vec3<f32> 最终都有一个额外的空格。

这是另一个结构体,看看它的内存布局

struct Ex4a {
  velocity: vec3f,
};

struct Ex4 {
  orientation: vec3f,
  size: f32,
  direction: array<vec3f, 1>,
  scale: f32,
  info: Ex4a,
  friction: f32,
};

为什么size最终就在orientation之后,位于字节偏移量 12。而scalefriction却被移到了偏移量 32 和 64?

这是因为数组和结构体有自己特殊的对齐规则,所以即使数组是单个 vec3fEx4a 结构体也是单个 vec3f,它们也会按照不同的规则对齐。

计算偏移和大小很麻烦!!

计算 WGSL 中数据的大小和偏移可能是 WebGPU 最大的痛点。您需要自己计算这些偏移量并保持更新。如果您在着色器中的结构体中间添加了一个成员,您就需要返回 JavaScript 更新所有偏移量。如果弄错了一个字节或长度,你传给着色器的数据就会出错。虽然不会出错,但着色器很可能会做错事情,因为它看到的是错误的数据。你的模型将无法绘制,或者你的计算将产生糟糕的结果。

幸运的是,有现成的库可以帮助我们。

这里有一个:webgpu-utils

你只需将 WGSL 代码交给它,它就会提供一个 API 来帮你完成所有这些工作。通过这种方式您就可以更改结构,而且通常情况下,一切都能正常工作。

例如,在最后一个例子中,我们可以像这样将值传递给 webgpu-utils

import {
  makeShaderDataDefinitions,
  makeStructuredView,
} from 'https://greggman.github.io/webgpu-utils/dist/0.x/webgpu-utils-1.x.module.js';

const code = `
struct Ex4a {
  velocity: vec3f,
};

struct Ex4 {
  orientation: vec3f,
  size: f32,
  direction: array<vec3f, 1>,
  scale: f32,
  info: Ex4a,
  friction: f32,
};
@group(0) @binding(0) var<uniform> myUniforms: Ex4;

...
`;

const defs = makeShaderDataDefinitions(code);
const myUniformValues = makeStructuredView(defs.uniforms.myUniforms);

// Set some values via set
myUniformValues.set({
  orientation: [1, 0, -1],
  size: 2,
  direction: [0, 1, 0],
  scale: 1.5,
  info: {
    velocity: [2, 3, 4],
  },
  friction: 0.1,
});

// now pass myUniformValues.arrayBuffer to WebGPU when needed.

至于你是使用这个特定的库,还是使用其他库,或者根本不使用,这取决于你自己。对我来说,我经常要花上二三十到六十分钟的时间来弄清为什么有些东西不能工作,最后才发现是我手动计算的偏移或尺寸出错了,所以对于我自己的工作来说,我宁愿使用一个库来避免这种痛苦。

如果您想手动计算,这里有一个页面可以为您计算偏移量


  1. f16 类型支持是 可选的特性 ↩︎

有疑问? 在stackoverflow上提问.
Issue/Bug? 在GitHub上提issue.
comments powered by Disqus