Bevy 渲染系统 Bindless 实现与交互逻辑

在现代游戏引擎与图形渲染中,Bindless 技术是提升大量资源管理效率、减少渲染状态切换的关键。Bevy 作为一款基于 Rust 的现代化游戏引擎,其 Bindless 实现既有对跨平台兼容性的考量,也包含独特的资源管理逻辑。本文将完全基于 Bevy 相关源码文档,从执行流程、Bindless 实现、宏机制到 Shader 交互,一步步拆解 Bevy 渲染系统的核心逻辑。

一、Bevy 渲染系统的核心执行流程

要理解 Bindless 技术在 Bevy 中的作用,首先需要明确渲染代码的核心执行路径 ------ 从 executor 启动到具体系统任务执行,每一步都为后续资源绑定奠定基础。

1.1 执行入口:MultiThreadedExecutor 的 run 方法

Bevy 渲染代码的执行起点是 MultiThreadedExecutor(继承自 SystemExecutor)的 run 方法。该方法会触发上下文的 tick 循环,是所有渲染任务的 "总开关"。

1.2 循环触发:context.tick_executor 与锁竞争

在 run 方法内部,会调用 context.tick_executor 方法,该方法的核心是重复执行 guard.tick(self, conditions) 。由于是多线程上下文,每次执行前必须通过 try_lock 尝试获取锁 ------ 只有成功获取锁,才能进入下一轮 tick 逻辑,避免多线程资源竞争。

1.3 Tick 核心逻辑:三大关键步骤

tick 方法内部包含三个核心操作,共同完成渲染前的资源准备与任务调度:

  1. finish_system_and_handle_dependents :处理已完成系统的依赖关系,确保后续任务的执行顺序正确;
  2. rebuild_active_access :重建当前活跃的资源访问关系,更新资源的读写状态;
  3. spawn_system_tasks :启动系统任务,这是 Bindless 资源绑定的关键触发点 ------ 该方法会调用 spawn_system_task,异步执行 bevy_pbr::material::specialize_material_meshes。

1.4 资源收集与 Pipeline 准备

在 specialize_material_meshes 异步任务中,会首先获取可见实体的关键资源:

  • material_instance(材质实例)
  • mesh_instance(网格实例)
  • lightmap(光照图)
  • render_visibility_ranges(渲染可见范围)

通过这些资源,进一步提取出 mesh(网格)、material(材质)、material_bind_group(材质绑定组)。随后,根据 mesh 的布局获取 pipeline_id------ 而获取 pipeline_id 必须执行 specialize 方法:

  • specialize 会加载顶点着色器、片元着色器与描述符;
  • 若启用 Bindless 模式,会在 Shader 宏中自动添加 BINDLESS 标识,为后续 Bindless 资源访问铺路。

二、Bevy 中 Bindless 渲染的实现机制

Bevy 的 Bindless 支持并非 "完全动态",而是基于 AsBindGroup 派生宏的 "批量绑定" 方案,核心围绕属性解析、双重代码生成、资源跟踪 三大模块展开。

2.1 核心入口:AsBindGroup 派生宏

Bevy 的 Bindless 实现集中在 as_bind_group.rs 文件中,通过 #[derive(AsBindGroup)] 宏为结构体(如材质)自动生成绑定逻辑。该宏支持多个辅助属性,用于定义资源绑定规则:

#[proc_macro_derive(

AsBindGroup,

attributes(

uniform, storage_texture, texture, sampler,

bind_group_data, storage, bindless, data

)

)]

其中 bindless 属性是开启 Bindless 模式的关键,其他属性(如 uniform、texture)用于定义具体资源的绑定类型。

2.2 Bindless 实现的四大核心环节

1. Bindless 属性解析

宏会优先解析结构体级别的 #[bindless(limit(N))] 属性,确定 Bindless 资源的数量限制:

  • 支持显式限制(如 limit(4),指定每组最大资源数);
  • 支持自动限制(使用 AUTO_BINDLESS_SLAB_RESOURCE_LIMIT,由引擎动态分配)。

解析逻辑核心代码如下:

} else if attr_ident == BINDLESS_ATTRIBUTE_NAME {

attr_bindless_count = Some(BindlessSlabResourceLimitAttr::Auto);

if let Meta::List(_) = attr.meta {

attr.parse_nested_meta(|submeta| {

if submeta.path.is_ident(&LIMIT_MODIFIER_NAME) {

let content;

parenthesized!(content in submeta.input);

let lit: LitInt = content.parse()?;

attr_bindless_count = Some(BindlessSlabResourceLimitAttr::Limit(lit));

return Ok(());

}// ...

2. 双重代码生成:Bindless 与传统布局兼容

为确保跨平台兼容性,宏会生成两套绑定布局:

  • Bindless 布局 :当硬件支持且启用 Bindless 时使用,基于资源数组与索引表;
  • 传统布局 :当 Bindless 不支持或被禁用时回退,使用独立的绑定组。

引擎会在运行时根据设备功能(如是否支持 Descriptor Indexing)自动选择布局。

3. 资源类型跟踪

通过 BindlessResourceType 枚举跟踪所有使用的资源类型(纹理、采样器、缓冲区等),同时维护:

  • BindlessIndexTableDescriptor:描述索引表的结构,用于查找资源在数组中的位置;
  • 绑定数组描述符:为每种资源类型创建大型数组(如纹理数组、采样器数组)。
4. 条件缓冲区处理

由于 WebGPU 暂不支持真正的 "Bindless Uniform",Bevy 在 Bindless 模式下会将 Uniform 缓冲区提升为存储缓冲区 ,并根据模式选择绑定类型:

let uniform_binding_type_declarations = match attr_bindless_count {

Some(_) => {

quote! {

let (#uniform_binding_type, #uniform_buffer_usages) =

if Self::bindless_supported(render_device) && !force_no_bindless {

(

BufferBindingType::Storage { read_only: true },

BufferUsages::STORAGE,

)

} else {

(

BufferBindingType::Uniform,

BufferUsages::UNIFORM,

)

};

}

}// ...

2.3 Bindless 核心机制:资源数组 + 索引表

Bevy Bindless 的本质是 "用大型资源数组替代独立绑定组,用索引表实现动态访问",具体逻辑如下:

1. 资源数组:集中管理同类型资源

不再为每个材质创建独立绑定,而是将所有同类型资源(如 2D 纹理、采样器)存储在大型数组中,示例 Shader 代码:

// Bindless 模式:大型资源数组

@group(2) @binding(5) var bindless_textures_2d: binding_array<texture_2d<f32>>;

@group(2) @binding(1) var bindless_samplers_filtering: binding_array<sampler>;

2. 索引表:定位资源位置

通过索引表(bindless_index_table)存储资源在数组中的索引,Shader 中通过索引动态访问资源:

// 索引表:存储资源在数组中的位置@group(2) @binding(0) var<storage> bindless_index_table: array<MaterialIndexEntry>;

// 动态访问:通过索引获取资源

let material_index = bindless_index_table[slot].material_index;

let texture = bindless_textures_2d[material_index];

3. Rust 代码示例:Bindless 材质定义

以下是文档中典型的 Bindless 材质定义,清晰展示了 bindless 属性与资源绑定的关联:

#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]

#[uniform(0, BindlessMaterialUniform, binding_array(10))]

// Uniform 存储在绑定数组10中#[bindless(limit(4))]

// 每组最多4个材质实例共享绑定组

struct BindlessMaterial {

color: LinearRgba,

#[texture(1)] // 纹理绑定到索引1

#[sampler(2)] // 采样器绑定到索引2

color_texture: Option<Handle<Image>>,

}

三、澄清 Bevy Bindless 的认知:并非完全动态的 Bindless

很多开发者会误以为 Bevy 的 Bindless 是 "完全动态、可随意增删资源" 的实现,但实际上它是一种 "准 Bindless" 方案,需明确以下关键区别。

3.1 为什么不是 "完全 Bindless"?

真正的 Bindless 应具备四大特性:

  1. 支持通过句柄直接访问任意 GPU 资源;
  2. 无需预先将资源分组到绑定组;
  3. 运行时可动态添加 / 删除资源;
  4. 基于 Descriptor Heaps(DirectX)或类似机制管理资源。

而 Bevy 的实现存在三个限制:

  1. 资源仍需绑定 :资源必须预先加入大型数组,无法完全脱离绑定;
  2. 动态性有限 :数组大小预定义,无法随意增删内部数据;
  3. 依赖批次分组 :limit(N) 限制了每组资源数量,并非全局动态管理。

3.2 关键误解: limit(4) 的真实含义

文档中明确指出,#[bindless(limit(4))] 并非 "每组最多 4 个材质实例",而是每个绑定组(bind group)中最多包含 4 个材质实例

  • 当材质实例数≤4 时,共享同一个绑定组;
  • 当第 5 个实例加入时,自动创建新的绑定组;
  • 目的是平衡性能(减少绑定组数量)与兼容性(适配硬件数组大小限制)。

3.3 Bevy 选择 "准 Bindless" 的原因

  1. WebGPU 兼容性 :WebGPU 目前的 API 不支持完全动态的 Descriptor Heaps;
  2. 跨平台支持 :需适配不同硬件(PC、移动端、Web)的资源管理限制;
  3. 性能平衡 :在 "灵活性" 与 "渲染效率" 间找到折中,避免过度动态导致的性能损耗;
  4. 渐进式改进 :为未来 WebGPU 支持完整 Bindless 打下基础。

四、实现真正 Bindless 渲染的方案(修改 Bevy 与 WGPU)

若要在 Bevy 中实现 "完全动态" 的 Bindless,需从底层 API 支持、资源管理、渲染系统重构三方面入手,甚至修改 WGPU 核心逻辑。

底层 API 支持:依赖现代图形接口特性

真正的 Bindless 需依赖以下 API 特性:

  • DirectX 12 :使用 Descriptor Heaps 管理资源句柄;
  • Vulkan :启用 Descriptor Indexing 扩展;
  • Metal :通过 Argument Buffers 实现动态资源访问。

五、derive_as_bind_group 宏的完整工作流程

derive_as_bind_group 是 Bevy 资源绑定的 "核心引擎",它通过过程宏自动生成 AsBindGroup 实现,将 Rust 结构体转换为 GPU 可识别的绑定资源。以下是其完整流程。

5.1 步骤 1:初始设置与模块加载

宏首先从 Bevy 清单中加载依赖模块,确保生成代码时能正确引用渲染、图像、资产相关接口:

let manifest = BevyManifest::shared();

let render_path = manifest.get_path("bevy_render");

let image_path = manifest.get_path("bevy_image");

let asset_path = manifest.get_path("bevy_asset");

let ecs_path = manifest.get_path("bevy_ecs");

5.2 步骤 2:结构体属性处理

解析结构体级别的属性,确定绑定组的全局配置:

  • #[bind_group_data(Type)]:指定绑定组所需的配置数据类型(如材质密钥);
  • #[bindless(limit(N))]:启用 Bindless 模式并设置资源数量限制;
  • 其他属性:如 #[uniform(0, Type)] 定义全局 Uniform 缓冲区。

处理逻辑核心代码:

for attr in &ast.attrs {

if let Some(attr_ident) = attr.path().get_ident() {

if attr_ident == BIND_GROUP_DATA_ATTRIBUTE_NAME {

// 处理 bind_group_data 属性

} else if attr_ident == BINDLESS_ATTRIBUTE_NAME {

// 处理 bindless 属性

}

}}

5.3 步骤 3:字段分析与绑定分配

遍历结构体的每个字段,根据字段属性(uniform、texture、sampler 等)确定资源类型与绑定索引:

  • #[uniform(N)]:字段作为 Uniform 缓冲区,绑定到索引 N;
  • #[texture(N)]:字段作为纹理,绑定到索引 N;
  • #[sampler(N)]:字段作为采样器,绑定到索引 N;
  • #[storage(N)]:字段作为存储缓冲区,绑定到索引 N。

同时维护 binding_states 向量,跟踪绑定索引的占用状态,防止冲突:

enum BindingState<'a> {

Free, // 未占用

Occupied { binding_type: BindingType, ident: &'a Ident }, // 已占用

OccupiedConvertedUniform, // 转换后的 Uniform

OccupiedMergeableUniform { uniform_fields: Vec<&'a syn::Field> }, // 可合并的 Uniform}

5.4 步骤 4:生成不同类型的绑定代码

根据资源类型,生成对应的 GPU 资源绑定代码,以下是三种核心类型的生成逻辑:

1. Uniform 缓冲区

为 #[uniform] 属性生成缓冲区创建代码,将 Rust 数据序列化为 GPU 可读取的格式:

binding_impls.push(quote! {{

let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new());

buffer.write(&self.#field_name).unwrap(); // 序列化字段数据

(

#binding_index, // 绑定索引

#render_path::render_resource::OwnedBindingResource::Buffer(

render_device.create_buffer_with_data(

&#render_path::render_resource::BufferInitDescriptor {

label: None,

usage: #uniform_buffer_usages, // 缓冲区用途

contents: buffer.as_ref(), // 序列化后的数据

},

)

)

)}});

2. 纹理资源

为 #[texture] 属性生成纹理视图获取代码,处理资源句柄与 fallback 逻辑:

binding_impls.insert(0, quote! {

(

#binding_index,

#render_path::render_resource::OwnedBindingResource::TextureView(

#render_path::render_resource::#dimension, // 纹理维度(如 D2)

{

let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into();

if let Some(handle) = handle {

// 从资源管理器获取纹理视图

images.get(handle)

.ok_or_else(|| AsBindGroupError::RetryNextUpdate)?

.texture_view.clone()

} else {

// 使用 fallback 纹理

#fallback_image.texture_view.clone()

}

}

)

)});

3. 采样器资源

为 #[sampler] 属性生成采样器获取代码,验证采样器类型并处理 fallback:

binding_impls.insert(0, quote! {

(

#binding_index,

#render_path::render_resource::OwnedBindingResource::Sampler(

{

let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into();

if let Some(handle) = handle {

let image = images.get(handle)

.ok_or_else(|| AsBindGroupError::RetryNextUpdate)?;

image.sampler.clone() // 获取图像关联的采样器

} else {

#fallback_image.sampler.clone() // fallback 采样器

}

}

)

)});

5.5 步骤 5:生成绑定组布局条目

绑定组布局条目(BindGroupLayoutEntry)是 WGPU 识别资源类型的关键,宏会根据资源类型生成对应的布局描述:

non_bindless_binding_layouts.push(quote!{

#bind_group_layout_entries.push(

#render_path::render_resource::BindGroupLayoutEntry {

binding: #binding_index, // 绑定索引

visibility: #visibility, // 可见的 Shader 阶段(如 FRAGMENT)

ty: #render_path::render_resource::BindingType::Buffer {

ty: #uniform_binding_type, // 缓冲区类型(Uniform/Storage)

has_dynamic_offset: false, // 是否支持动态偏移

min_binding_size: Some(<#field_ty as ShaderType>::min_size()), // 最小缓冲区大小

},

count: #actual_bindless_slot_count, // Bindless 模式下的数组大小

}

);});

5.6 步骤 6:Bindless 模式的特殊处理

若启用 #[bindless],宏会额外生成以下逻辑:

  1. 跟踪 Bindless 资源类型(纹理、采样器等);
  2. 生成绑定数组的布局条目(而非单个绑定);
  3. 创建索引表描述符,用于资源定位:

match #actual_bindless_slot_count {

Some(bindless_slot_count) => {

let bindless_index_table_range = #bindless_index_table_range;

#bind_group_layout_entries.extend(

#render_path::render_resource::create_bindless_bind_group_layout_entries(

bindless_index_table_range.end.0 - bindless_index_table_range.start.0,

bindless_slot_count.into(),

#bindless_index_table_binding_number,

).into_iter()

);

#(#bindless_binding_layouts)*;

}

None => {

#(#non_bindless_binding_layouts)*;

}};

六、AsBindGroup 与 Shader、Draw Call 的交互细节

AsBindGroup 生成的代码并非孤立存在,而是与 Shader 定义、渲染管线、Draw Call 紧密协作,共同完成 "Rust 资源 → GPU 渲染" 的闭环。

6.1 第一步:Shader 中的绑定组定义

Shader(以 WGSL 为例)需预先定义与 Rust 材质对应的绑定组结构,确保索引与类型匹配。以下是传统模式与 Bindless 模式的对比:

传统绑定模式

// 材质 Uniform 缓冲区(group 1, binding 0)@group(1) @binding(0) var<uniform> material: MaterialUniform;// 纹理(group 1, binding 1)@group(1) @binding(1) var color_texture: texture_2d<f32>;// 采样器(group 1, binding 2)@group(1) @binding(2) var color_sampler: sampler;

Bindless 绑定模式

// 索引表(存储资源在数组中的位置)@group(1) @binding(0) var<storage> material_indices: array<MaterialIndex>;// 纹理数组(group 1, binding 1)@group(1) @binding(1) var bindless_textures: binding_array<texture_2d<f32>>;// 采样器数组(group 1, binding 2)@group(1) @binding(2) var bindless_samplers: binding_array<sampler>;

// 动态访问资源@fragmentfn fragment(in: VertexOutput) -> @location(0) vec4<f32> {

let index = material_indices[instance_index]; // 从实例索引获取资源位置

let texture = bindless_textures[index.texture_index];

let sampler = bindless_samplers[index.sampler_index];

return textureSample(texture, sampler, in.uv);}

6.2 第二步:Rust 中的材质定义与宏生成代码

以传统模式为例,Rust 材质定义需与 Shader 绑定组一一对应:

#[derive(Asset, AsBindGroup, TypePath, Debug, Clone)]

#[bind_group_data(MyMaterialKey)] // 绑定组配置数据

#[uniform(0, MyMaterialUniform)] // 全局 Uniform(group 1, binding 0)

struct MyMaterial {

#[uniform(1)] // 字段级 Uniform(group 1, binding 1)

color: Color,

#[texture(2)] // 纹理(group 1, binding 2)

#[sampler(3)] // 采样器(group 1, binding 3)

base_texture: Handle<Image>,

}

宏会为该结构体生成 AsBindGroup 实现,核心包含两个方法:

  1. unprepared_bind_group :收集 GPU 资源(缓冲区、纹理视图、采样器),返回未准备好的绑定组;
  2. bind_group_layout_entries :返回绑定组布局条目,定义资源类型与索引。

6.3 第三步:渲染管线中的绑定组关联

渲染管线(RenderPipeline)需关联绑定组布局,确保管线与 Shader、材质绑定组兼容:

// 创建绑定组布局(从材质的 AsBindGroup 实现获取)

let material_bind_group_layout = MyMaterial::bind_group_layout(&render_device, false);

// 创建渲染管线布局,关联绑定组布局

let pipeline_layout = render_device.create_pipeline_layout(

&PipelineLayoutDescriptor {

label: Some("my_material_pipeline_layout"),

bind_group_layouts: &[

&mesh_bind_group_layout, // group 0:网格数据

&material_bind_group_layout, // group 1:材质数据(与 Shader 对应)

],

push_constant_ranges: &[],

});

6.4 第四步:Draw Call 中的绑定与绘制

在渲染阶段,每个 Draw Call 都会执行以下步骤,完成资源绑定与绘制:

1. 缓存或创建绑定组

为避免重复创建绑定组,引擎会使用缓存(material_bind_group_cache)存储已创建的绑定组:

let material_bind_group = match material_bind_group_cache.get(&material_key) {

Some(bind_group) => bind_group.clone(), // 从缓存获取

None => {

// 从材质生成未准备的绑定组

let unprepared_bind_group = material.unprepared_bind_group(

&material_bind_group_layout,

&render_device,

&mut system_params,

false,

)?;

// 创建 WGPU 绑定组

let bind_group = render_device.create_bind_group(&BindGroupDescriptor {

label: Some("my_material_bind_group"),

layout: &material_bind_group_layout,

entries: &[

// Uniform 缓冲区条目(binding 0)

BindGroupEntry {

binding: 0,

resource: BindingResource::Buffer(BufferBinding {

buffer: &uniform_buffer,

offset: 0,

size: None,

}),

},

// 纹理条目(binding 2)

BindGroupEntry {

binding: 2,

resource: BindingResource::TextureView(&texture_view),

},

// 采样器条目(binding 3)

BindGroupEntry {

binding: 3,

resource: BindingResource::Sampler(&sampler),

},

],

});

material_bind_group_cache.insert(material_key, bind_group.clone());

bind_group

}};

2. 绑定资源并执行绘制

在 RenderPass 中,将绑定组绑定到对应的 Shader 组,然后执行绘制命令:

// 绑定 group 0(网格数据)

render_pass.set_bind_group(0, &mesh_bind_group, &[]);// 绑定 group 1(材质数据,与 Shader 的 @group(1) 对应)

render_pass.set_bind_group(1, &material_bind_group, &[]);// 设置渲染管线

render_pass.set_pipeline(&render_pipeline);// 执行绘制(索引绘制)

render_pass.draw_indexed(0..index_count, 0, 0..1);

七、总结

Bevy 的渲染系统围绕 "兼容性" 与 "性能" 设计,其 Bindless 实现虽非完全动态,却通过 "资源数组 + 索引表" 的方案,在 WebGPU 限制下实现了高效的资源管理。核心逻辑可概括为:

  1. 执行流程 :从 MultiThreadedExecutor 的 run 方法启动,通过 tick 循环调度任务,最终触发 specialize_material_meshes 收集资源;
  2. Bindless 实现 :基于 AsBindGroup 宏,通过属性解析、双重代码生成、资源跟踪,实现 "批量绑定";
  3. 宏机制 :derive_as_bind_group 自动生成资源绑定代码,处理 Uniform、纹理、采样器等不同资源类型;
  4. 交互逻辑 :与 Shader 绑定组定义一一对应,通过渲染管线关联,在 Draw Call 中完成资源绑定与绘制。
相关推荐
新石器程序员23 天前
借鉴bevy实现适用于Godot-rust的状态管理
rust·游戏引擎·godot·bevy
VT LI2 个月前
Bevy渲染引擎核心技术深度解析:架构、体积雾与Meshlet渲染
bevy·ecs·meshlet·gpudriven·体积雾
摇光655352 年前
Bevy的一些窗口设置
rust·游戏引擎·bevy·ecs