首页
/ Three.js TSL 中的 BarrierNode:WebGPU Compute Shader 的三种 GPU 屏障机制(workgroup / storage / texture)

Three.js TSL 中的 BarrierNode:WebGPU Compute Shader 的三种 GPU 屏障机制(workgroup / storage / texture)

2026-09-04 18:19:38作者:裘晴惠Vivianne

在 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.

翻译过来就是两点关键约束:

  1. 控制屏障(control barrier):它不是渲染屏障或同步对象,而是 compute shader 内部的“汇合点”——让某类操作(执行流或访存)完成之前,后续指令必须等待;
  2. 仅限 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)的 allowEarlyReturnsallowGlobalVariables 都置为 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 并分两种情况:

  1. WebGL 后端renderer.backend.isWebGLBackend === true,该标记在 WebGLBackend.js 中设置): 调用 builder.addFlowCode( '\t// ' + scope + 'Barrier \n' ),即只输出一行注释(例如 \t// workgroupBarrier)。也就是说,在 WebGL 后端编译同一份 TSL 代码时,屏障会被静默“抹掉”,不参与执行。这对需要屏障保证正确性的算法(如并行排序)意味着 WebGL 回退下结果可能是错的,只能作为“可运行但降级”的演示。
  2. WebGPU 后端:调用 builder.addLineFlowCode( ${scope}Barrier(), this ),把屏障作为一条流程语句(flow line)写入生成的 WGSL 函数体,并关联当前节点实例(用于节点图追踪)。

addLineFlowCode 的实现在 NodeBuilder.jsaddLineFlowCode( code, node = null ),内部委托给 addLineFlowCodeBlock),它负责把这条独立语句以正确的缩进与上下文插入到 compute 函数的代码流中。

这也解释了为什么 setup() 要关闭 early returns:屏障所在函数的流程必须是完整的顺序块,NodeBuilder 才能安全地在其中插入单行流程语句。

导出路径:从哪里 import

三个屏障函数经由模块聚合链路对外导出:

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)——否则后写的线程会覆盖还没被别的线程读走的值,导致数据错乱。后续渲染阶段用 MeshBasicNodeMaterialcolorNode 把倒序结果画成彩色柱状条,用于肉眼验证排序是否正确。

实战用法二:并行排序算法中的密集屏障

更完整的用法在 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(工作组内)的同步则必须手写屏障。

使用注意事项

  1. 屏障只在 compute 上下文中有效workgroupBarrier() / storageBarrier() / textureBarrier() 应写在 Fn( () => { ... } )().compute( ... ) 构建的 compute 函数体内(参见上述两个示例)。BarrierNode.generate() 走的是 flow code 通道,即它作为语句参与函数体代码流,而非作为取值表达式。
  2. WebGL 回退下屏障不生效:源码中 renderer.backend.isWebGLBackend === true 分支只生成注释。若算法正确性依赖屏障(例如 BitonicSort),WebGL 后端结果没有保证;需要正确性的场景应限制在支持 WebGPU compute 的环境。
  3. scope 决定同步范围:workgroup 是执行流屏障(最常用,排序、归约等算法基本都用它);storage / texture 是访存屏障,用于“所有对某地址空间的读写可见性确认”这一更细粒度的语义。源码中 generate() 直接把 scope 拼进函数名,因此 scope 必须是三者之一,否则生成的 WGSL 不合法。
  4. 包含屏障的函数受流程限制setup() 会把 allowEarlyReturnsallowGlobalVariables 置为 false,写含屏障的 compute 函数时避免在其内依赖提前返回或全局变量。

相关资源

小结

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() 的逐次派发共同构成完整的同步体系。

登录后查看全文
热门项目推荐
相关项目推荐