- Published on
PixiJS 源码揭秘 - 5. 探索批处理渲染系统
- Authors
- Name
- 青雲
在现代网页游戏和图形应用中,性能是一个至关重要的因素。PixiJS 作为一个强大的 2D 渲染引擎,引入了批处理 (Batching) 系统,以提高渲染效率。今天,我们将深入探讨PixiJS v8的批处理系统,了解其工作原理及其如何提升渲染性能。
什么是批处理系统?
通俗来说,批处理是把多个渲染任务合并成一个任务,这样能减少与图形硬件(GPU)的交流次数。例如,如果你要在网页上绘制多个相同的图片,而每张图片的渲染都是单独进行的,那么渲染性能会非常低——你需要每次都和GPU交互。然而,通过批处理技术,我们可以把多个渲染任务合并成一次性任务,从而大大提升性能。
基本概念
在PixiJS中,批处理系统通过以下几个主要类来运作:
- Batch: 批次的基本单元,包含有渲染所需的数据,如纹理、混合模式等。
- Batcher: 批处理器,负责管理批次和决定何时开始或终止一个批次。
- BatchTextureArray: 用来管理单个批次中的纹理数组。
- BatcherPipe: 批处理管道,协调批处理器和渲染系统之间的工作。
- BatcherAdaptor: 适配不同渲染环境(如WebGL或WebGPU)的插件类。
目录结构
shared
目录包含了与批处理系统相关的一些基础类和通用组件。它们主要负责定义批处理的核心逻辑和抽象概念。gl
目录下的文件是与WebGL环境相关的组件和适配器。gpu
目录下的文件是与WebGPU环境相关的组件和适配器。
batcher
├─ gl
│ ├─ utils
│ │ ├─ checkMaxIfStatementsInShader.ts // 用于检查在给定的 WebGL 渲染上下文中,片段着色器中可以包含的最大 if 语句数量。
│ │ └─ maxRecommendedTextures.ts // 获取推荐的最大纹理数量。
│ └─ GlBatchAdaptor.ts // WebGL环境下使用的批处理适配器,负责具体的WebGL调度渲染。
├─ gpu
│ ├─ GpuBatchAdaptor.ts // WebGPU环境下使用的批处理适配器,负责具体的WebGPU调度渲染。
│ ├─ generateGPULayout.ts // 在WebGPU渲染中动态地生成绑定组布局。
│ ├─ generateLayout.ts // 生成布局映射。
│ └─ getTextureBatchBindGroup.ts // 获取纹理批次绑定组。
└─ shared
├─ BatchGeometry.ts // 定义了批处理中的几何结构。
├─ BatchTextureArray.ts // 管理批次中的纹理数组。
├─ Batcher.ts // 批处理的核心类,管理批次、处理纹理、记录批次数据等。
├─ BatcherPipe.ts // 批处理管道,用于管理和执行渲染批次处理。
├─ DefaultBatcher.ts // 默认的批处理器实现,处理基本的批处理任务。
└─ DefaultShader.ts // 默认的批处理器使用的着色器。
Batcher
类是批处理的核心模块,它引用了 BatchGeometry
和 BatchTextureArray
来管理几何数据和纹理数组。BatcherPipe
调度和管理多个 Batcher
,并通过 DefaultBatcher
实现具体的批处理逻辑。DefaultShader
提供了与 DefaultBatcher
配套使用的着色器。GlBatchAdaptor
和 GpuBatchAdaptor
分别负责在 WebGL 和 WebGPU 环境中的渲染操作。
批处理系统的运作流程
BatcherPipe
添加AbstractRenderer
是渲染器的抽象基类,它负责管理渲染管道和指令集。在初始化渲染器时,调用 _addPipes
方法时,会添加 BatcherPipe 的实例到渲染管道中。
BatcherPipe
实例化
在 BatcherPipe
实例化时,会调用传入的适配器的 init
方法,将其注册到渲染器的 contextChange
事件监听器中,以便在渲染器的上下文发生变化时能够及时响应并进行相应的处理。
constructor(renderer: Renderer, adaptor: BatcherAdaptor) {
this.renderer = renderer;
this._adaptor = adaptor;
this._adaptor.init?.(this);
}
public init(batcherPipe: BatcherPipe): void
{
batcherPipe.renderer.runners.contextChange.add(this);
}
渲染对象的批处理
通过 buildInstructions
函数启动整个批处理流程,buildInstructions
函数负责构建渲染指令,它通过收集所有需要渲染的元素,并将它们添加到指令集中,然后调用渲染管道的方法来初始化和结束批处理、混合模式和颜色掩码。
buildStart
在构建渲染指令之前,buildInstructions
函数会调用渲染管道的 buildStart
方法,用于初始化批处理、混合模式和颜色掩码等。
BatcherPipe
的 buildStart
方法会设置默认的批处理器为当前活动的批处理器。它还会调用每个批处理器的 begin
方法,以准备开始批处理。
public buildStart(instructionSet: InstructionSet) {
let batchers = this._batchersByInstructionSet[instructionSet.uid];
if (!batchers) {
batchers = this._batchersByInstructionSet[instructionSet.uid] = Object.create(null);
batchers.default ||= new DefaultBatcher();
}
this._activeBatches = batchers;
this._activeBatch = this._activeBatches.default;
for (const i in this._activeBatches) {
this._activeBatches[i].begin();
}
}
addToBatch
收集所有需要渲染的元素,并将它们添加到指令集中。对于可批处理的元素,BatcherPipe
的 addToBatch
方法会被调用,将这些元素添加到批处理中。
BatcherPipe
的 addToBatch
方法中,如果当前活动的批处理器与元素的批处理器名称不同,它会调用 break
结束当前的批处理,然后获取或创建新的批处理器,并将元素添加到新的批处理器中。
public addToBatch(batchableObject: BatchableElement, instructionSet: InstructionSet) {
if (this._activeBatch.name !== batchableObject.batcherName) {
this._activeBatch.break(instructionSet);
let batch = this._activeBatches[batchableObject.batcherName];
if (!batch) {
batch = this._activeBatches[batchableObject.batcherName] = BatcherPipe.getBatcher(batchableObject.batcherName);
batch.begin();
}
this._activeBatch = batch;
}
this._activeBatch.add(batchableObject);
}
buildEnd
在收集完所有渲染指令后,buildInstructions
函数会调用渲染管道的 buildEnd
方法,用于结束批处理的构建。
首先调用当前活动批次(_activeBatch)的 break 方法,通知批处理器当前的批处理已经结束。然后遍历所有批次,更新几何缓冲区和索引缓冲区,确保所有批次的数据都被正确地提交到渲染器中。
public buildEnd(instructionSet: InstructionSet)
{
this._activeBatch.break(instructionSet);
const batches = this._activeBatches;
for (const i in batches)
{
const batch = batches[i as keyof typeof batches];
const geometry = batch.geometry;
geometry.indexBuffer.setDataWithSize(batch.indexBuffer, batch.indexSize, true);
geometry.buffers[0].setDataWithSize(batch.attributeBuffer.float32View, batch.attributeSize, false);
}
}
break
addToBatch
和 buildEnd
方法中调用的break
方法用于批处理过程中的中断情况,例如当需要切换到不同的渲染模式或纹理时。其实现有几个关键步骤:
- 初始化与早期返回: 确保当前批次中存在渲染元素,否则直接返回。
const elements = this._elements
if (!elements[this.elementStart]) return
- 获取批次对象并清理: 从池中获取一个批次对象,并清除其纹理数组。
let batch = getBatchFromPool()
let textureBatch = batch.textures
textureBatch.clear()
- 调整混合模式和缓冲区大小: 根据第一个元素的混合模式调整当前批次的混合模式。确保属性缓冲区和索引缓冲区有足够的大小。
const firstElement = elements[this.elementStart]
let blendMode = getAdjustedBlendModeBlend(firstElement.blendMode, firstElement.texture._source)
if (this.attributeSize * 4 > this.attributeBuffer.size) {
this._resizeAttributeBuffer(this.attributeSize * 4)
}
if (this.indexSize > this.indexBuffer.length) {
this._resizeIndexBuffer(this.indexSize)
}
- 循环处理每个元素: 对于每个元素,检查是否需要分割批次;如果是,则调用 _finishBatch 方法提交当前批次,并创建新批次。
for (let i = this.elementStart; i < this.elementSize; ++i) {
const element = elements[i]
elements[i] = null
//... 处理元素的逻辑...
}
- 提交最后的批次: 在处理完所有渲染元素后,提交最后的批次。
if (textureBatch.count > 0) {
this._finishBatch(batch, start, size - start, textureBatch, blendMode, instructionSet, action)
start = size
++BATCH_TICK
}
更新 GPU 数据
在渲染之前,RenderGroupSystem 会调用 upload 方法,将指令集中的数据上传到 GPU。
循环遍历所有的批次处理器,对每个批次处理器进行处理。如果批次处理器的 dirty
属性为真,表示几何数据需要更新。此时,将 dirty
属性设置为 false,并更新几何缓冲区的大小。这个方法确保了在渲染之前,所有的几何数据都是最新的,并且已经被正确地上传到 GPU。
public upload(instructionSet: InstructionSet)
{
const batchers = this._batchersByInstructionSet[instructionSet.uid];
for (const i in batchers)
{
const batcher = batchers[i as keyof typeof batchers];
const geometry = batcher.geometry;
if (batcher.dirty)
{
batcher.dirty = false;
geometry.buffers[0].update(batcher.attributeSize * 4);
}
}
}
执行批处理渲染
一旦准备好所有资源,RenderGroupSystem
会调用 executeInstructions
来执行所有渲染指令。对于批处理指令,BatcherPipe
的 execute
方法会被调用,执行批处理渲染。
export function executeInstructions(renderGroup: RenderGroup, renderer: RenderPipes)
{
const instructionSet = renderGroup.instructionSet;
const instructions = instructionSet.instructions;
for (let i = 0; i < instructionSet.instructionSize; i++)
{
const instruction = instructions[i];
(renderer[instruction.renderPipeId as keyof RenderPipes] as InstructionPipe<any>).execute(instruction);
}
}
BatcherPipe
的 execute
方法会根据具体渲染环境调用适配器的方法。
public execute(batch: Batch)
{
if (batch.action === 'startBatch')
{
const batcher = batch.batcher;
const geometry = batcher.geometry;
const shader = batcher.shader;
this._adaptor.start(this, geometry, shader);
}
this._adaptor.execute(this, batch);
}
在WebGL环境中,GlBatchAdaptor 会执行如下代码:
public start(batchPipe: BatcherPipe, geometry: Geometry, shader: Shader): void {
const renderer = batchPipe.renderer as WebGLRenderer;
renderer.shader.bind(shader, this._didUpload);
renderer.shader.updateUniformGroup(renderer.globalUniforms.uniformGroup);
renderer.geometry.bind(geometry, shader.glProgram);
}
public execute(batchPipe: BatcherPipe, batch: Batch): void {
const renderer = batchPipe.renderer as WebGLRenderer;
const textures = batch.textures.textures;
for (let i = 0; i < batch.textures.count; i++) {
renderer.texture.bind(textures[i], i);
}
renderer.geometry.draw('triangle-list', batch.size, batch.start);
}
start
:绑定着色器和几何体,并更新全局着色器组。execute
:设置渲染状态,绑定纹理并执行绘制。
WebGPU部分逻辑相似,但采用不同的API和调用方式:
public start(batchPipe: BatcherPipe, geometry: Geometry, shader: Shader): void {
const renderer = batchPipe.renderer as WebGPURenderer;
const encoder = renderer.encoder as GpuEncoderSystem;
encoder.setGeometry(geometry, shader.gpuProgram);
tempState.blendMode = 'normal';
renderer.pipeline.getPipeline(geometry, shader.gpuProgram, tempState);
encoder.resetBindGroup(1);
encoder.setBindGroup(0, renderer.globalUniforms.bindGroup, shader.gpuProgram);
}
public execute(batchPipe: BatcherPipe, batch: Batch): void {
const renderer = batchPipe.renderer as WebGPURenderer;
const encoder = renderer.encoder as GpuEncoderSystem;
if (!batch.gpuBindGroup) {
batch.gpuBindGroup = getTextureBatchBindGroup(batch.textures.textures, batch.textures.count);
}
tempState.blendMode = batch.blendMode;
const pipeline = renderer.pipeline.getPipeline(this._geometry, this._shader.gpuProgram, tempState);
encoder.setPipeline(pipeline);
encoder.renderPassEncoder.setBindGroup(1, batch.gpuBindGroup);
encoder.renderPassEncoder.drawIndexed(batch.size, 1, batch.start);
}
start
:设置几何体和着色器程序,初始化混合模式,获取并设置渲染管线。execute
:获取纹理绑定组,设置渲染管线,并通过GPU执行绘制。
流程图
初始化:
- AbstractRenderer 初始化时调用 _addPipes 方法,将 BatcherPipe 添加到渲染管道中。
- BatcherPipe 的实例化会调用 GlBatchAdaptor 的 init 方法进行初始化。
渲染循环开始:
- 应用调用 AbstractRenderer 的 render 方法。
- render 方法调用 buildInstructions 函数。
构建渲染指令:
- buildInstructions 函数调用 BatcherPipe 的 buildStart 方法,启动批处理。
- buildInstructions 函数调用 collectAllRenderablesAdvanced,收集所有需要渲染的对象,并调用 BatcherPipe 的 addToBatch 方法将对象添加到当前批处理中。
- buildInstructions 函数调用 BatcherPipe 的 buildEnd 方法,结束批处理并提交数据。
上传阶段:
- 应用调用 RenderGroupSystem 的 upload 方法。
- RenderGroupSystem 调用 BatcherPipe 的 upload 方法,更新几何缓冲区。
执行阶段:
- 应用调用 RenderGroupSystem 的 executeInstructions 方法。
- RenderGroupSystem 调用 BatcherPipe 的 execute 方法。
- BatcherPipe 调用 GlBatchAdaptor 的 start 方法设置渲染状态。
- BatcherPipe 调用 GlBatchAdaptor 的 execute 方法进行实际渲染。
总结
通过上述分析我们可以看到,PixiJS的批处理系统通过整合分配及管理资源、批次的分割与提交、以及对于不同图形API的适配,真正实现了高效的2D图形渲染。面对复杂的图形渲染任务时,批处理系统显著地减少了渲染所需的GPU交互次数,从而提升了渲染效率,也为开发者提供了强大而灵活的工具,帮助他们创建流畅、性能优异的2D图形应用。