当前位置: 首页 > news >正文

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 模式的关键,其他属性(如 uniformtexture)用于定义具体资源的绑定类型。

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:字段分析与绑定分配

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

  • #[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 中完成资源绑定与绘制。

http://www.dtcms.com/a/506658.html

相关文章:

  • K8s控制器终极对比:StatefulSet与Deployment详解
  • [Agent可视化] docs | go/rust/py混构 | Temporal编排 | WASI沙箱
  • Linux服务器编程实践55-网络信息API:gethostbyname与gethostbyaddr实现主机名解析
  • Godot 2D游戏开发全流程实战
  • 自动驾驶工程师面试(定位、感知向)
  • Cocos学习——摄像机Camera
  • 千秋网站建设公司百度如何快速收录
  • 深圳大型论坛网站建设免费行情网站在线
  • 《软件测试分类指南(下):从测试阶段到地域适配,拆解落地核心维度》
  • Python 查询网站开发3g小说网站
  • 基于Python的Word文档模板自动化处理:从占位符提取到智能填充
  • vue3子组件向父组件传递参数
  • 阿里云云代理商:阿里云CDN刷新机制是什么?
  • FFmpeg 基本数据结构 AVFormatConext 分析
  • 使用 DrissionPage——实现同花顺股票数据自动化爬虫
  • 基于位置式PID算法调节PWM占空比实现电机转速控制
  • FFmpeg+QT输出音频
  • 友点企业网站管理系统微信商城在哪里找
  • 深度学习(5)-PyTorch 张量详细介绍
  • 西安市建设厅网站软文营销的经典案例
  • Agent 开发设计模式(Agentic Design Patterns )第8章: 智能体记忆管理(Memory Management)
  • Linux 下使用 Docker-Compose 安装 Kafka 和 Kafka-UI(KRaft 模式)
  • 【C++入门篇 - 10】:模板
  • [Linux]学习笔记系列 -- [kernel][lock]mutex
  • 开源 Linux 服务器与中间件(七)数据库--MySQL
  • 在 JavaScript 中处理 `0.1 + 0.2` 这类精度问题
  • 今天我们学习python编程常用模块与面向对象
  • 网站的三大标签宁波专业seo服务
  • Day6C语言前期阶段练习之汉诺塔问题
  • Apache Spark 集群部署与使用指南