乐趣区

关于gpu:WebGPU-规范篇-06-资源及其打组绑定

系列博客总目录:https://segmentfault.com/a/1190000040716735


资源绑定,即 JavaScript 中(即 CPU 端)如何看待传递到 GPU 的数据进行组织、调配的一种设计。

为了实现这个过程(CPU 到 GPU),WebGPU 设计了几个对象用于治理这些数据,这些数据包含 某些 GPUBuffer(例如 UBO,然而不包含 VBO)、GPUTexture、GPUSampler、存储型纹理、内部纹理五种,这几个对象是:

  • 资源绑定组(当前均简称绑定组)- GPUBindGroup
  • 资源绑定组布局(当前称为绑定组的布局对象)- GPUBindGroupLayout
  • 管线布局 – GPUPipelineLayout

和标准中程序略有不同,我按我下面这个来。

1 关系繁难图解

     GPUBindGroup <--- 1 ---┓                动区
                    GPUBindGroupLayout   -----------
GPUPipelineLayout <--- N ---┛                静区
    |
    1
    ↓
GPUPipelineBase

简略的说就是绑定组有其布局,管线也有其布局;

绑定组的布局负责通知资源长什么样子,管线的布局通知管线着色器外头有多少类和别离有多少个资源坑位要进来;

两个布局负责沟通,那绑定组、管线就各司其职(一个负责组织管线所需的资源,一个负责某个渲染过程)。

2 资源绑定组 GPUBindGroup

资源绑定组,GPUBindGroup,个别会简略称之为“绑定组”,它组织了一些 资源 ,这些资源在某个具体的着色阶段一起起作用。

所谓的资源,就是 GPUBuffer、纹理、采样器等数据类型的对象。

如何创立

通过设施对象的 createBindGroup 办法创立,它须要一个必选的对象参数,这个对象参数要满足 GPUBindGroupDescriptor 类型。

dictionary GPUBindGroupDescriptor : GPUObjectDescriptorBase {
  required GPUBindGroupLayout layout;
  required sequence<GPUBindGroupEntry> entries;
};

看得出 GPUBindGroupDescriptor 类型须要两个必选参数:

  • 参数 layoutGPUBindGroupLayout 类型,通常称这个参数为资源绑定组的布局对象,有时候简略点就说对应的布局对象,上面会具体介绍这个类型;
  • 参数 entries,一个数组,每个元素皆为 GPUBindGroupEntry 类型,具体解释见下文。

GPUBindGroupEntry 类型

GPUBindGroupEntry 类型的对象,指代一种在着色阶段要用到的资源,包含一些简略的元数据。

这里的资源,在 WebGPU 中应用 GPUBindingResource 来定义。

dictionary GPUBindGroupEntry {
  required GPUIndex32 binding;
  required GPUBindingResource resource;
};

GPUBindGroupEntry 对象的字段 binding 是一个 unsigned long 类型的数字,示意这个资源在着色器代码中的 location(在 WGSL 会介绍),即通知着色器代码要从几号坑位取数据,一对一的。

而另一个字段 resource 天然指的就是这个上面要介绍的 GPUBindingResource 了。

GPUBindingResource

有四种 资源 :采样器、纹理(视图)、缓存、内部纹理。

typedef (GPUSampler or GPUTextureView or GPUBufferBinding or GPUExternalTexture) GPUBindingResource;

常见的是前三种,第四种通常用于视频类型的纹理,有趣味理解的读者可自行查阅纹理局部的章节。

第一种和第二种还比拟好了解,在采样器和纹理章节能理解到如何创立,第三种并不是 GPUBuffer 的间接传递,而是嵌套了个对象,是 GPUBufferBinding 类型的,见下文:

GPUBufferBinding

dictionary GPUBufferBinding {
  required GPUBuffer buffer;
  GPUSize64 offset = 0;
  GPUSize64 size;
};

到这里就有相熟的类型了,即 GPUBuffer

除此之外还有另外两个字段,offsetsize,前者默认值是 0,示意要从 GPUBuffer 的第几个字节开始,后者则为要从 GPUBuffer 的 offset 处向后取多少个字节。

译者注

通过 GPUBindGroup,明确地把一堆资源挨个(每个 entry)合并打包在一个组里,并且是按程序(参数 binding)组织在一起。这就好比做菜时的食材,按程序列举在灶台边,做这道菜的是一组(绑定组),做另一道菜的是另一组(绑定组)。

举例

const bindGroup = device.createBindGroup({
  layout: myBindGroupLayout, // 这个上面会介绍,这里伪装曾经有了
  entries: [/* ... */]
})

① 采样器资源

entries: [
  {
    binding: 0,
    resource: device.createSampler({/* ... */})
  },
  /* ... */
]

② 纹理(视图)资源

entries: [
  /* ... */,
  {
    binding: 1,
    resource: myTexture.createView()},
  /* ... */
]

③ GPUBuffer 资源

entries: [
  /* ... */
  {
    binding: 2,
    resource: {
      buffer: uniformBuffer, // 通常通过绑定组传递的是 UBO,不相对,
      size: 16
    }
  }
]

创立要恪守的规定

  • descriptor.layout 字段必须是一个无效的布局对象
  • descriptor.layout 对象的 entries 数量必须与 descriptor.entries 的数量统一

对于每个 descriptor.entries 中的 GPUBindGroupEntry(此处以 bindEntry 指代)来说:

  • 此 bindEntry 的 binding 数字必须与 descriptor.layout 中的某一个统一,也即一一对应关系
  • 如果 descriptor.layout.entries 中的某个 GPUBindGroupLayoutEntry 对象:

    • 有 sampler 字段,那么 bindEntry.resource 就必须是一个无效的 GPUSampler,而且此 GPUBindGroupLayoutEntry 的 sampler 字段的 type 属性

      • "filtering",那么 bindEntry.resource 这个 GPUSampler 就不能是比拟型采样器(即 isComparison 这个外部变量是 false);
      • "non-filering",那么 bindEntry.resource 这个 GPUSampler 既不能是比拟型采样器,其 isFiltering 外部属性也不能是 true;
      • "comparison",那么 bindEntry.resource 这个 GPUSampler 就必须是比拟型采样器(即 isComparison 外部属性是 true)
    • 有 texture 字段,那么 bindEntry.resource 必须是一个无效的 GPUTextureView,且此 GPUBindGroupLayoutEntry 的

      • texture 字段的 viewDimension 属性必须与 bindEntry.resource 这个 GPUTextureView 的 dimension 统一;
      • texture 字段的 sampleType 属性与 bindEntry.resource 这个 GPUTextureView 的 format 兼容;令 bindEntry.resource 这个 GPUTextureView 本来的纹理对象为 T,此时 T 的 usage 必须包含 "TEXTURE_BINDING"
      • texture 字段的 multisampled 属性是 true,那么上一条设定的纹理对象 T 的 sampleCount 必须大于 1,否则 T.sampleCount 必须是 1.
    • 有 storageTexture 字段,那么 bindEntry.resource 必须是一个无效的 GPUTextureView 对象,此时令 T 为 bindEntry.resource 这个 GPUTextureView 对象本来的纹理对象:

      • 此 storageTexture 字段的 viewDimension 必须与 bindEntry.resource 这个 GPUTextureView 的 dimension 统一;
      • 此 storageTexture 字段的 format 必须与 bindEntry.resource 这个 GPUTextureView 的创立参数(GPUTextureViewDescriptor,创立实现后被内置,JavaScript 看不到)的 format 参数统一;
      • T 对象的 usage 必须包含 "STORAGE_BINDING"
    • 有 buffer 字段,那么 bindEntry.resource 必须是一个 GPUBufferBinding 对象,且 bindEntry.resource.buffer 必须是一个无效的 GPUBuffer 对象,无妨令这个 GPUBuffer 对象为 B 对象,且此 buffer 字段的 type 是:

      • "uniform",那么 B.usage 必须包含 "UNIFORM",且 bindEntry.resource.size 必须小于等于设施限度列表中的 maxUniformBufferBindingSize 值,bindEntry.resource.offset 必须是设施限度列表中 minUniformBufferOffsetAlignment 的倍数
      • "storage""read-only-storage",B.usage 必须包含 "STORAGE",且 bindEntry.resource.size 小于等于设施限度列表中的 maxStorageBufferBindingSize,bindEntry.resource.offset 必须是设施限度列表中的 minStorageBufferOffsetAlignment 的倍数

满足以上要求后,基本上绑定组对象就能创立胜利,仍有一些可能不罕用的细节须要读者自行查阅文档。

3 资源绑定组的布局 GPUBindGroupLayout

一个资源绑定组的布局对象,是 GPUBindGroupLayout 类型,简称绑定组的布局(对象),它的作用是分割与它配套的绑定组和对应阶段的着色器,通知管线在什么着色阶段传入的数据长什么样子,是何品种。

[Exposed=(Window, DedicatedWorker), SecureContext]
interface GPUBindGroupLayout {
};
GPUBindGroupLayout includes GPUObjectBase;

如何创立

调用设施对象的 createBindGroupLayout 办法,即可创立一个绑定组布局对象。

此办法须要一个参数对象,需合乎 GPUBindGroupLayoutDescriptor 类型:

dictionary GPUBindGroupLayoutDescriptor : GPUObjectDescriptorBase {required sequence<GPUBindGroupLayoutEntry> entries;};

它只有一个必选成员 entries,是一个数组,数组的每个元素是 GPUBindGroupLayoutEntry 类型的对象。

GPUBindGroupLayoutEntry 类型

GPUBindGroupLayoutEntry 形容一个在着色器中能被拜访到的资源长什么样子,且如何找到它。

typedef [EnforceRange] unsigned long GPUShaderStageFlags;
[Exposed=(Window, DedicatedWorker)]
namespace GPUShaderStage {
  const GPUFlagsConstant VERTEX   = 0x1;
  const GPUFlagsConstant FRAGMENT = 0x2;
  const GPUFlagsConstant COMPUTE  = 0x4;
};

dictionary GPUBindGroupLayoutEntry {
  required GPUIndex32 binding;
  required GPUShaderStageFlags visibility;

  GPUBufferBindingLayout buffer;
  GPUSamplerBindingLayout sampler;
  GPUTextureBindingLayout texture;
  GPUStorageTextureBindingLayout storageTexture;
  GPUExternalTextureBindingLayout externalTexture;
};

可见,GPUBindGroupLayoutEntry 承受两个必选参数:

  • 参数 binding,unsigned long 类型的数字,指定该 entry(也即资源)绑定号,绑定号在一个绑定组布局对象的 entries 数组所有元素中是惟一的,且必须和绑定组中的 entry 有对应,以便于在 WGSL 代码中拜访对应资源;
  • 参数 visibilityGPUShaderStageFlags 类型,需从 GPUShaderStage 中拜访枚举值,示意该 entry 在什么着色阶段可见,例如指定 visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,就意味着这个 entry 所形容的资源在顶点着色阶段和偏僻着色阶段均可见。最初一个 COMPUTE 是计算管线的计算阶段。

还必须承受一个键值对作为此 entry 形容的资源的信息对象,需从 buffersamplertexturestorageTextureexternalTexture 当选一个。

形容缓存对象:GPUBufferBindingLayout

enum GPUBufferBindingType {
  "uniform",
  "storage",
  "read-only-storage",
};

dictionary GPUBufferBindingLayout {
  GPUBufferBindingType type = "uniform";
  boolean hasDynamicOffset = false;
  GPUSize64 minBindingSize = 0;
};

绑定组传递的 GPUBuffer 只有两种,UBO(即 "uniform" 类型的 GPUBuffer)和存储型 GPUBuffer,其中后者又分存储型("storage")和只读存储型("read-only-storage")两小种,通过 type 字段标识,默认值是 “uniform”,type 字段只能是 GPUBufferBindingType 枚举中的一个。

hasDynamicOffset 示意缓存对象是否有动静偏移值,默认是 false;

minBindingSize 示意的是被绑定的缓存对象的最小绑定大小,默认是 0(byte);

因为 type 的默认值是 “uniform”,所以如果你筹备创立一个绑定组,并思考到有 ubo 的传递,那么其实能够对某个 entry 简写成:

const layout = device.createBindGroupLayout({
  entries: [
    {
      binding: 0,
      visibility: GPUShaderStage.VERTEX,
      buffer: {} // <- UBO 的简写}
  ]
})

形容采样器对象:GPUSamplerBindingLayout

enum GPUSamplerBindingType {
  "filtering",
  "non-filtering",
  "comparison",
};

dictionary GPUSamplerBindingLayout {GPUSamplerBindingType type = "filtering";};

绑定组布局对象中,用于形容对应绑定组中的采样器资源时用的是 GPUSamplerBindingLayout 对象,它只有一个 type 字段,其类型是枚举类型 GPUSamplerBindingType,默认值是 "filtering"

它用来批示采样器的类型:过滤型、非过滤型和比拟型。

形容纹理对象:GPUTextureBindingLayout

enum GPUTextureSampleType {
  "float",
  "unfilterable-float",
  "depth",
  "sint",
  "uint",
};

dictionary GPUTextureBindingLayout {
  GPUTextureSampleType sampleType = "float";
  GPUTextureViewDimension viewDimension = "2d";
  boolean multisampled = false;
};

纹理对象(或者说其纹理视图对象,因为绑定组结构所需的是纹理视图对象)在绑定组的布局对象中以 GPUTextureBindingLayout 类型的 entry 示意,有三个参数:

  • samplerType,是枚举类型 GPUTextureSampleType 的字段,默认值是 "float",示意采样类型,有浮点、有符号 / 无符号整数、非过滤浮点、深度等几个可选项;
  • viewDimension,是枚举类型 GPUTextureViewDimension 的字段,默认值是 "2d",即默认绑定组中对应 binding 号的纹理视图 entry 所绑定的纹理视图资源是二维的;
  • multisampled,布尔值,默认值是 false,示意绑定组中对应 entry 所绑定的纹理视图资源是否须要多重采样。

形容存储型纹理对象:GPUStorageTextureBindingLayout

enum GPUStorageTextureAccess {"write-only",};

dictionary GPUStorageTextureBindingLayout {
  GPUStorageTextureAccess access = "write-only";
  required GPUTextureFormat format;
  GPUTextureViewDimension viewDimension = "2d";
};

存储型纹理对象在绑定组的布局对象中以 GPUStorageTextureBindingLayout 类型的 entry 示意,有一个必选参数:

  • format,类型是 GPUTextureFormat 枚举,不开展,如有须要请查阅纹理章节;

还有两个其余的参数:

  • accessGPUStorageTextureAccess 枚举类型,默认值是 "write-only",目前只能是只写类型(笔者注:不分明为何这样设计),示意存储型纹理的可拜访性;
  • viewDimensionGPUTextureViewDimension 枚举类型,默认值是 "2d",用于形容纹理视图对象(对应的纹理对象)的维度,默认是二维纹理

形容内部纹理对象:GPUExternalTextureBindingLayout

dictionary GPUExternalTextureBindingLayout {};

若须要传递内部纹理对象(视频类的纹理),则须要用到此对象。

它不须要参数,如果绑定组中存在 GPUExternalTexture,只需在此绑定组布局中写一个 entry,其 externalTexture 键指定一个空对象即可。

GPUBindGroupLayoutDescriptor 的正确用法

  • entries 数组中每个 entry 的 binding 是惟一的;
  • 每个 entry 必须合乎设施对象的性能限度列表中无关的参数项;

对于每个 entry,无妨设 buffer 型的 entry.buffer 对象名称为 bufferLayout,同理 sampler、texture、storage 的 entry 也有 samplerLayouttextureLayoutstorageTextureLayout

  • bufferLayoutsamplerLayouttextureLayoutstorageTextureLayout 不是 undefined
  • 若 entry.visibility 是 VERTEX

    • bufferLayout.type 不能是 “storage”
    • storageTextureLayout.access 不能是 “write-only”(仿佛此处有问题,待官网更新规范文档)
  • 若 entry 是 texture 类型的,即 textureLayout 不是 undefined,且 textureLayout.multisampled 是 true:

    • textureLayout.viewDimension 必须是 “2d”
    • textureLayout.sampleType 不能是 “float” 和 “depth”
  • 若 entry 是 storageTexture 类型的:

    • storageTextureLayout.viewDimension 不能是 “cube” 和 “cube-array”
    • storageTextureLayout.format 必须是存储型的格局

绑定组布局对象之间的比拟:绑定组布局的等价

当下列条件满足时,两个 GPUBindGroupLayout 对象可视作等价:

  • 外部属性 exclusivePipeline 统一(这个在 js 中看不到)
  • 对于任意 entry 的 binding 值,下列二者满足一个:

    • 布局对象的外部属性 "entryMap" 中都没有此 binding 值
    • 布局对象的外部属性 "entryMap" 中两个 binding 值相等

如果两个绑定组等价,那么它们能调配的资源其实是一样的。

举例

① 一般的绑定组布局

这个有一个采样器,一个纹理,都只在片元着色阶段可见。

const bindGroupLayout = device.createBindGroupLayout({
  entries: [
    {
      /* use for sampler */
      binding: 0,
      visibility: GPUShaderStage.FRAGMENT,
      sampler: {type: 'filtering',},
    },
    {
      /* use for texture view */
      binding: 1,
      visibility: GPUShaderStage.FRAGMENT,
      texture: {sampleType: 'float'}
    }
  ]
})

② 计算管线的绑定组布局

三个都是计算着色阶段可见,且都是 buffer 类型的 GPUBindGroupLayoutEntry。

const bindGroupLayout = device.createBindGroupLayout({
  entries: [
    {
      binding: 0,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {type: "read-only-storage"}
    },
    {
      binding: 1,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {type: "read-only-storage"}
    },
    {
      binding: 2,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {type: "storage"}
    }
  ]
});

4 管线布局 GPUPipelineLayout

管线布局定义了一种在指令编码时通过 setBindGroup 办法来设置具体的某个绑定组与某种通道编码器(GPURenderPassEncoder 或 GPUComputePassEncoder)通过 setPipeline 办法设置的管线中的着色器之间的映射关系。

这句话咋看很长很长,说白了就是通道编码器通过其 setBindGroupsetPipeline 办法别离设置绑定组和管线后,那么绑定组所指的资源就能映射到管线中的着色器里,进而能在着色器里用传入的资源了。

一种资源(GPUBuffer、纹理、采样器等)在着色器中通过三个个性独特定位:

  • 着色器的阶段(顶点、片元或计算,阶段来指定资源在哪个阶段的可见性)
  • 绑定组 id(即 setBindGroup 办法的参数,着色器中个性名是 group)
  • 绑定 id(即 entry 的 binding 属性,着色器中个性名是 bind)

三者独特形成一种资源在着色器中的地址,这个地址又能够称作管线的 绑定空间(binding space)

这个地址,也即绑定空间,必须比着色器中用到的要多,否则就会呈现在着色器中有,然而在绑定组却找不到对应的资源的状况。

管线布局对象的接口很简略:

[Exposed=(Window, DedicatedWorker), SecureContext]
interface GPUPipelineLayout {
};
GPUPipelineLayout includes GPUObjectBase;

对少数渲染管线(GPURenderPipeline)或计算管线(GPUComputePipeline)应用同一个管线布局,就能保障在切换管线时,不须要对资源进行从新资源绑定。来看上面的例子:

设有计算管线 X 和 Y,它们别离用 XPL 和 YPL 管线布局来创立;而 XPL 和 YPL 又别离应用 A、B、C 和 A、D、C 绑定组布局对象来创立。

无妨设指令编码队列有两个调度(略超此局部的内容,要看计算管线局部内容)过程:

setBindGroup(0, ...)
setBindGroup(1, ...)
setBindGroup(2, ...)
setPipeline(X)
dispatch(...)
// 留神绑定组和管线的设置循序
setBindGroup(1, ...)
setPipeline(Y)
dispatch()

在这种状况下,开发者无需批改管线布局中的绑定组布局,而只须要批改管线与绑定组即可实现不同的调度计算。

乍一看很形象,然而这正好合乎 WebGPU 的设计哲学(嗯,这句话很装杯 …)

理论也是如此,能在逻辑端好好组织好高度可复用的数据组织形式,让 GPU 能好好地按程序横扫千军,是很难受的事件。

绑定组代表的是资源,是数据实体;而管线、管线布局、绑定组布局则是形容信息和行为。为了实现两件不同的工作(管线的作用),或者在某一部分数据要求上略有不同(譬如 XPL 和 YPL 这俩管线布局中的第二个绑定组布局的不同,别离是 B 和 D),然而大部分是可复用的,譬如 A 和 C 绑定组布局。

所以上述代码中,实现了 X 的工作后,紧接着先切换 1 号槽位的绑定组,而后再切换 Y 管线,当初 Y 管线就复用了 A 和 C 绑定组布局,此时并没有批改 0 和 2 号槽位的绑定组,仅仅再次批改了 1 号槽位的绑定组,以对应 Y 管线不同的绑定组布局,大大减少了 CPU 端组织数据的开销。

留神,通常管线中最频繁切换的绑定组置于布局的顶部,换句话说,最不频繁改变的绑定组应放在底部(索引为 0 那端)。如果在绘图调用中某个绑定组变动得很多,那么最好把它拉到布局的顶部(栈顶),以减小 CPU 开销。

如何创立

管线布局通过调用设施对象的 createPipelineLayout 办法即可创立。

此办法须要一个 GPUPipelineLayoutDescriptor 类型的对象参数:

dictionary GPUPipelineLayoutDescriptor : GPUObjectDescriptorBase {required sequence<GPUBindGroupLayout> bindGroupLayouts;};

它有一个必选字段 bindGroupLayouts,是一个 GPUBindGroupLayout 数组。

创立要恪守的规定

  • descriptor.bindGroupLayouts 的个数必须不大于设施限度列表中的 maxBindGroups 值
  • 所有绑定组及其布局对象不得超过设施限度列表中无关绑定限度

留神,如果俩管线布局的绑定组布局对象数组是 等效组 ,那么它俩能够视作等效。

等效组的概念在绑定组大节有介绍。

举例

const pipelineLayout = device.createPipelineLayout({bindGroupLayouts: [bindGroupLayout],
})

计算管线的差不多。

译者废话

一个管线布局对象蕴含 N 个绑定组布局对象。

5 联动

创立绑定组,须要绑定组布局

创立管线布局,也须要绑定组布局

     GPUBindGroup <--- 1 ---┓
                    GPUBindGroupLayout
GPUPipelineLayout <--- N ---┛

创立管线的时候,只须要一个管线布局:

     GPUBindGroup <--- 1 ---┓                动区
                    GPUBindGroupLayout   -----------
GPUPipelineLayout <--- N ---┛                静区
    |
    1
    ↓
GPUPipelineBase

即是说,管线、管线布局、绑定组布局能够当时设计、安顿好,创立、组合结束就尽可能别动它们了,你只需认真地负责组织起绑定组,并应用通道编码器的 setBindGroup 设置绑定组即可。

偷懒

不指定 pipeline 创立时的参数对象的 layout 值(即管线布局),随后在创立 pipeline 后调用其 getBindGroupLayout 办法,从着色器中推断出绑定组布局对象,而后间接拿它来创立绑定组也是能够的,上面以计算管线为例:

const computePipeline = device.createComputePipeline({
  compute: {
    module: shaderModule,
    entryPoint: "main"
  }
});

const bindGroup = device.createBindGroup({layout: computePipeline.getBindGroupLayout(0),
  /* ... */
})
退出移动版