Three.js TSL 中的 BarrierNode:WebGPU Compute Shader 的三种 GPU 屏障机制(workgroup / storage / texture)
在 Three.js 的节点系统(TSL)中,BarrierNode 是 WebGPU 后端独有的 GPU 控制屏障节点,用于在 compute shader 内同步不同地址空间的计算与访存操作。本文以官方 API 文档为骨架,结合 src/nodes/gpgpu/BarrierNode.js 的完整源码、渲染器代码生成链路以及 examples 中的真实 compute 示例,讲清屏障的构造参数、三种语义(workgroup/storage/texture)、代码生成行为,以及它在并行排序等实际场景中的插入位置。读完本文,你将能够在 three/tsl 编写的 compute 函数中正确使用 workgroupBarrier()、storageBarrier()、textureBarrier(),并理解 WebGL 回退后端下屏障的实际表现。
核心概念:什么是 GPU 控制屏障
官方 API 文档(docs/pages/BarrierNode.html.md)对 BarrierNode 的定义是:
Represents a GPU control barrier that synchronizes compute operations within a given scope. This node can only be used with a WebGPU backend.
翻译过来就是两点关键约束:
- 控制屏障(control barrier):它不是渲染屏障或同步对象,而是 compute shader 内部的“汇合点”——让某类操作(执行流或访存)完成之前,后续指令必须等待;
- 仅限 WebGPU 后端:只有当渲染器运行在 WebGPU 后端时,屏障才真正生效;WebGL 后端下它会退化为一条注释(下文“代码生成链路”一节给出源码证据)。
BarrierNode 的继承关系为 EventDispatcher → Node,构造函数签名为:
new BarrierNode( scope )
其中 scope 是唯一的构造参数,官方文档描述为 “The scope defines the behavior of the node”(scope 决定节点的行为)。scope 的取值直接映射到 WGSL 的三类屏障函数:'workgroup'、'storage'、'texture'。
在 TSL 中,开发者几乎不会直接 new BarrierNode(scope),而是使用文档源文件 src/nodes/gpgpu/BarrierNode.js 底部导出的三个 TSL 便捷函数。
源码结构:BarrierNode 的完整实现
BarrierNode.js 全文约百行,核心结构如下:
import Node from '../core/Node.js';
import { nodeProxy } from '../tsl/TSLCore.js';
/**
* Represents a GPU control barrier that synchronizes compute operations within a given scope.
*
* This node can only be used with a WebGPU backend.
*
* @augments Node
*/
class BarrierNode extends Node {
constructor( scope ) {
super();
this.scope = scope;
this.isBarrierNode = true;
}
setup( builder ) {
builder.allowEarlyReturns = false;
builder.allowGlobalVariables = false;
}
generate( builder ) {
const { scope } = this;
const { renderer } = builder;
if ( renderer.backend.isWebGLBackend === true ) {
builder.addFlowCode( `\t// ${scope}Barrier \n` );
} else {
builder.addLineFlowCode( `${scope}Barrier()`, this );
}
}
}
export default BarrierNode;
三个关键成员与方法
scope:保存构造时传入的字符串('workgroup'/'storage'/'texture'),在generate()中被拼进最终输出的代码片段${scope}Barrier()。isBarrierNode = true:节点类型标记。在整库源码中,isBarrierNode仅在此文件内被赋值,没有其他模块读取该标记——从源码结构看,它主要用于调试与类型辨识,屏障的实际语义由generate()的输出代码承担。setup( builder ):屏障插入节点图后,会把当前构建器(NodeBuilder)的allowEarlyReturns与allowGlobalVariables都置为false。这意味着包含屏障的 compute 程序不允许提前 return,也不允许声明全局变量——这符合 compute shader 中屏障对控制流一致性的要求(屏障必须被同一工作组内所有调用以一致的方式执行)。generate( builder ):真正的代码生成逻辑,按后端分叉,见下一节。
TSL 入口:三个便捷函数
文件末尾通过 nodeProxy 生成通用的 barrier 工厂函数,并导出三个带默认 scope 的 TSL 函数:
const barrier = /*@__PURE__*/ nodeProxy( BarrierNode );
export const workgroupBarrier = () => barrier( 'workgroup' ).toStack();
export const storageBarrier = () => barrier( 'storage' ).toStack();
export const textureBarrier = () => barrier( 'texture' ).toStack();
源码注释给出了三个函数各自的确切语义:
| TSL 函数 | 生成的 WGSL 调用 | 语义(源码注释原文翻译) |
|---|---|---|
workgroupBarrier() |
workgroupBarrier() |
工作组内同步:同一工作组内的所有 compute 调用必须互相等待,直到各自执行到屏障点,才能继续越过屏障。 |
storageBarrier() |
storageBarrier() |
存储屏障:所有对 storage 地址空间变量的访问必须全部完成后,调用才能通过屏障。 |
textureBarrier() |
textureBarrier() |
纹理屏障:所有对 texture 地址空间变量的访问必须全部完成后,调用才能通过屏障。 |
注意三个函数都调用了 .toStack(),即把屏障作为栈式节点压入节点图——在 TSL 的流程控制(Fn 函数体)中,它是一条独立的“语句”,而不是可参与表达式计算的返回值。
代码生成链路:WebGPU 与 WebGL 的分叉
generate( builder ) 是理解“仅限 WebGPU 后端”这一约束的关键。它读取 builder.renderer.backend 并分两种情况:
- WebGL 后端(
renderer.backend.isWebGLBackend === true,该标记在 WebGLBackend.js 中设置): 调用builder.addFlowCode( '\t// ' + scope + 'Barrier \n' ),即只输出一行注释(例如\t// workgroupBarrier)。也就是说,在 WebGL 后端编译同一份 TSL 代码时,屏障会被静默“抹掉”,不参与执行。这对需要屏障保证正确性的算法(如并行排序)意味着 WebGL 回退下结果可能是错的,只能作为“可运行但降级”的演示。 - WebGPU 后端:调用
builder.addLineFlowCode(${scope}Barrier(), this ),把屏障作为一条流程语句(flow line)写入生成的 WGSL 函数体,并关联当前节点实例(用于节点图追踪)。
addLineFlowCode 的实现在 NodeBuilder.js(addLineFlowCode( code, node = null ),内部委托给 addLineFlowCodeBlock),它负责把这条独立语句以正确的缩进与上下文插入到 compute 函数的代码流中。
这也解释了为什么 setup() 要关闭 early returns:屏障所在函数的流程必须是完整的顺序块,NodeBuilder 才能安全地在其中插入单行流程语句。
导出路径:从哪里 import
三个屏障函数经由模块聚合链路对外导出:
- src/nodes/gpgpu/BarrierNode.js 定义并导出
workgroupBarrier、storageBarrier、textureBarrier; - src/nodes/TSL.js 第 136 行
export * from './gpgpu/BarrierNode.js',因此three/tsl入口可直接使用:
import { workgroupBarrier, storageBarrier, textureBarrier } from 'three/tsl';
- 完整的
three包(three/webgpu等构建)在 src/Three.TSL.js 中显式再导出:第 553 行export const storageBarrier = TSL.storageBarrier;、第 594 行export const textureBarrier = TSL.textureBarrier;、第 672 行export const workgroupBarrier = TSL.workgroupBarrier;。
仓库的 TSL 参考手册(docs/TSL.md)也列出了这组函数:
| 函数 | 说明 |
|---|---|
workgroupBarrier() |
Creates a workgroup barrier. |
storageBarrier() |
Creates a storage barrier. |
textureBarrier() |
Creates a storage barrier.(文档原文如此) |
barrier() |
Creates a memory barrier. |
即文档侧同时暴露了通用 barrier(scope) 工厂与三个具名函数;源码中 barrier 工厂由 nodeProxy( BarrierNode ) 生成,具名函数是它的快捷封装。
实战用法一:storage 缓冲区 + workgroupBarrier 的倒序演示
examples/webgpu_storage_buffer.html 是仓库中最直接展示屏障用法的官方示例(该页面同时用 WebGPU 与 WebGL 两个后端各渲染一份结果,恰好可以对照观察“WebGL 下屏障被注释化”的差异)。其核心代码:
import { storage, If, vec3, uv, uint, float, Fn, instanceIndex, workgroupBarrier } from 'three/tsl';
// 创建 float/vec2/vec3/vec4 四种类型的存储缓冲区,尺寸 32
const size = 32;
const type = [ 'float', 'vec2', 'vec3', 'vec4' ];
const arrayBufferNodes = [];
for ( let i = 0; i < type.length; i ++ ) {
const typeSize = i + 1;
const array = new Array( size * typeSize ).fill( 0 );
const arrayBuffer = new THREE.StorageInstancedBufferAttribute( new Float32Array( array ), typeSize );
arrayBufferNodes.push( storage( arrayBuffer, type[ i ], size ).setPBO( true ) );
}
const computeInitOrder = Fn( () => {
for ( let i = 0; i < type.length; i ++ ) {
arrayBufferNodes[ i ].element( instanceIndex ).assign( instanceIndex );
}
} );
const computeInvertOrder = Fn( () => {
for ( let i = 0; i < type.length; i ++ ) {
const invertIndex = arrayBufferNodes[ i ].element( uint( size - 1 ).sub( instanceIndex ) ).toVar();
workgroupBarrier(); // ← 所有线程的读取都完成之后,才允许写入
arrayBufferNodes[ i ].element( instanceIndex ).assign( invertIndex );
}
} );
// 派发到 GPU 执行
const computeInit = computeInitOrder().compute( size );
const compute = computeInvertOrder().compute( size );
这里屏障的角色非常典型:先读后写的就地(in-place)倒序。每个线程先把自己目标位置的值读到局部变量 invertIndex(.toVar() 保证只读一次),随后 workgroupBarrier() 确保工作组内所有线程的读操作都已完成,之后才开始写入 arrayBufferNodes[i].element(instanceIndex)——否则后写的线程会覆盖还没被别的线程读走的值,导致数据错乱。后续渲染阶段用 MeshBasicNodeMaterial 的 colorNode 把倒序结果画成彩色柱状条,用于肉眼验证排序是否正确。
实战用法二:并行排序算法中的密集屏障
更完整的用法在 GPU 位排序(bitonic sort)工具 examples/jsm/gpgpu/BitonicSort.js(被 examples/webgpu_compute_sort_bitonic.html 使用)。它对 workgroupBarrier() 的使用呈现出并行算法中屏障的三种标准插入点:
1. 填充本地内存之后——把全局数据搬入 workgroup 共享内存(workgroupArray 创建的 localStorage)后,必须等所有线程都写入完毕,才能开始本地排序:
localStorage.element( localID1 ).assign( dataBuffer.element( localOffset.add( localID1 ) ) );
localStorage.element( localID2 ).assign( dataBuffer.element( localOffset.add( localID2 ) ) );
// Ensure that all local data has been populated
workgroupBarrier();
2. 排序循环的每一轮迭代之间——bitonic 的 flip / disperse 循环里,每个比较交换阶段开始前都要屏障,确保上一轮所有线程的交换写回已完成:
Loop( { start: uint( 2 ), end: uint( workgroupSize * 2 ), type: 'uint', condition: '<=', update: '<<= 1' }, () => {
// Ensure that last dispatch block executed
workgroupBarrier();
const flipIdx = getBitonicFlipIndices( invocationLocalIndex, flipBlockHeight );
this._localCompareAndSwapTSL( flipIdx.x, flipIdx.y );
const localBlockHeight = flipBlockHeight.div( 2 );
Loop( { start: localBlockHeight, end: uint( 1 ), type: 'uint', condition: '>', update: '>>= 1' }, () => {
// Ensure that last dispatch op executed
workgroupBarrier();
const disperseIdx = getBitonicDisperseIndices( invocationLocalIndex, localBlockHeight );
this._localCompareAndSwapTSL( disperseIdx.x, disperseIdx.y );
localBlockHeight.divAssign( 2 );
} );
flipBlockHeight.shiftLeftAssign( 1 );
} );
3. 把结果写回全局缓冲区之前——所有本地交换完成后,等全部线程收敛,再统一写回 dataBuffer:
// Ensure that all invocations have swapped their own regions of data
workgroupBarrier();
dataBuffer.element( localOffset.add( localID1 ) ).assign( localStorage.element( localID1 ) );
dataBuffer.element( localOffset.add( localID2 ) ).assign( localStorage.element( localID2 ) );
该算法的整体流程由 computeStep( renderer ) / compute( renderer ) 驱动:按 stepCount(由 _getStepCount() 计算)逐次 renderer.compute(...) 派发 flip / disperse / local 等 compute shader,每一步之间 GPU 侧通过 dispatch 边界天然同步,而 shader 内部的同步完全依赖这些 workgroupBarrier() 调用。这构成一个完整的对照:跨 dispatch 的同步由驱动保证,intra-dispatch(工作组内)的同步则必须手写屏障。
使用注意事项
- 屏障只在 compute 上下文中有效:
workgroupBarrier()/storageBarrier()/textureBarrier()应写在Fn( () => { ... } )().compute( ... )构建的 compute 函数体内(参见上述两个示例)。BarrierNode.generate()走的是 flow code 通道,即它作为语句参与函数体代码流,而非作为取值表达式。 - WebGL 回退下屏障不生效:源码中
renderer.backend.isWebGLBackend === true分支只生成注释。若算法正确性依赖屏障(例如BitonicSort),WebGL 后端结果没有保证;需要正确性的场景应限制在支持 WebGPU compute 的环境。 - scope 决定同步范围:workgroup 是执行流屏障(最常用,排序、归约等算法基本都用它);storage / texture 是访存屏障,用于“所有对某地址空间的读写可见性确认”这一更细粒度的语义。源码中
generate()直接把 scope 拼进函数名,因此 scope 必须是三者之一,否则生成的 WGSL 不合法。 - 包含屏障的函数受流程限制:
setup()会把allowEarlyReturns与allowGlobalVariables置为false,写含屏障的 compute 函数时避免在其内依赖提前返回或全局变量。
相关资源
- API 文档:docs/pages/BarrierNode.html.md
- 源码实现:src/nodes/gpgpu/BarrierNode.js
- 模块聚合导出:src/nodes/TSL.js、src/Three.TSL.js
- 代码生成入口:src/nodes/core/NodeBuilder.js
- TSL 函数手册:docs/TSL.md
- 官方示例:examples/webgpu_storage_buffer.html、examples/webgpu_compute_sort_bitonic.html
- 排序算法实现:examples/jsm/gpgpu/BitonicSort.js
小结
BarrierNode 是 Three.js TSL 把 WGSL 三种 GPU 控制屏障(workgroup / storage / texture)封装成节点的方式:构造参数 scope 决定屏障语义,generate() 按后端分叉生成 ${scope}Barrier() 语句(WebGL 后端则退化为注释),workgroupBarrier()、storageBarrier()、textureBarrier() 三个 .toStack() 封装的 TSL 函数是实际使用入口。从 webgpu_storage_buffer.html 的“先读后写”倒序到 BitonicSort.js 中“填充—循环交换—写回”三段式屏障布局,可以看出它正是 compute shader 内多线程数据竞争的同步基石;而在跨 dispatch 的算法编排中,它与 renderer.compute() 的逐次派发共同构成完整的同步体系。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00