mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 12:43:34 +00:00
Compute Pipeline Specialization (#3979)
# Objective - Fixes #3970 - To support Bevy's shader abstraction(shader defs, shader imports and hot shader reloading) for compute shaders, I have followed carts advice and change the `PipelinenCache` to accommodate both compute and render pipelines. ## Solution - renamed `RenderPipelineCache` to `PipelineCache` - Cached Pipelines are now represented by an enum (render, compute) - split the `SpecializedPipelines` into `SpecializedRenderPipelines` and `SpecializedComputePipelines` - updated the game of life example ## Open Questions - should `SpecializedRenderPipelines` and `SpecializedComputePipelines` be merged and how would we do that? - should the `get_render_pipeline` and `get_compute_pipeline` methods be merged? - is pipeline specialization for different entry points a good pattern Co-authored-by: Kurt Kühnert <51823519+Ku95@users.noreply.github.com> Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
parent
0a4136d266
commit
9e450f2827
21 changed files with 516 additions and 318 deletions
|
@ -27,7 +27,7 @@ use bevy_render::{
|
||||||
color::Color,
|
color::Color,
|
||||||
render_graph::{EmptyNode, RenderGraph, SlotInfo, SlotType},
|
render_graph::{EmptyNode, RenderGraph, SlotInfo, SlotType},
|
||||||
render_phase::{
|
render_phase::{
|
||||||
batch_phase_system, sort_phase_system, BatchedPhaseItem, CachedPipelinePhaseItem,
|
batch_phase_system, sort_phase_system, BatchedPhaseItem, CachedRenderPipelinePhaseItem,
|
||||||
DrawFunctionId, DrawFunctions, EntityPhaseItem, PhaseItem, RenderPhase,
|
DrawFunctionId, DrawFunctions, EntityPhaseItem, PhaseItem, RenderPhase,
|
||||||
},
|
},
|
||||||
render_resource::*,
|
render_resource::*,
|
||||||
|
@ -198,7 +198,7 @@ impl Plugin for CorePipelinePlugin {
|
||||||
pub struct Transparent2d {
|
pub struct Transparent2d {
|
||||||
pub sort_key: FloatOrd,
|
pub sort_key: FloatOrd,
|
||||||
pub entity: Entity,
|
pub entity: Entity,
|
||||||
pub pipeline: CachedPipelineId,
|
pub pipeline: CachedRenderPipelineId,
|
||||||
pub draw_function: DrawFunctionId,
|
pub draw_function: DrawFunctionId,
|
||||||
/// Range in the vertex buffer of this item
|
/// Range in the vertex buffer of this item
|
||||||
pub batch_range: Option<Range<u32>>,
|
pub batch_range: Option<Range<u32>>,
|
||||||
|
@ -225,9 +225,9 @@ impl EntityPhaseItem for Transparent2d {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CachedPipelinePhaseItem for Transparent2d {
|
impl CachedRenderPipelinePhaseItem for Transparent2d {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn cached_pipeline(&self) -> CachedPipelineId {
|
fn cached_pipeline(&self) -> CachedRenderPipelineId {
|
||||||
self.pipeline
|
self.pipeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,7 +244,7 @@ impl BatchedPhaseItem for Transparent2d {
|
||||||
|
|
||||||
pub struct Opaque3d {
|
pub struct Opaque3d {
|
||||||
pub distance: f32,
|
pub distance: f32,
|
||||||
pub pipeline: CachedPipelineId,
|
pub pipeline: CachedRenderPipelineId,
|
||||||
pub entity: Entity,
|
pub entity: Entity,
|
||||||
pub draw_function: DrawFunctionId,
|
pub draw_function: DrawFunctionId,
|
||||||
}
|
}
|
||||||
|
@ -270,16 +270,16 @@ impl EntityPhaseItem for Opaque3d {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CachedPipelinePhaseItem for Opaque3d {
|
impl CachedRenderPipelinePhaseItem for Opaque3d {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn cached_pipeline(&self) -> CachedPipelineId {
|
fn cached_pipeline(&self) -> CachedRenderPipelineId {
|
||||||
self.pipeline
|
self.pipeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AlphaMask3d {
|
pub struct AlphaMask3d {
|
||||||
pub distance: f32,
|
pub distance: f32,
|
||||||
pub pipeline: CachedPipelineId,
|
pub pipeline: CachedRenderPipelineId,
|
||||||
pub entity: Entity,
|
pub entity: Entity,
|
||||||
pub draw_function: DrawFunctionId,
|
pub draw_function: DrawFunctionId,
|
||||||
}
|
}
|
||||||
|
@ -305,16 +305,16 @@ impl EntityPhaseItem for AlphaMask3d {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CachedPipelinePhaseItem for AlphaMask3d {
|
impl CachedRenderPipelinePhaseItem for AlphaMask3d {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn cached_pipeline(&self) -> CachedPipelineId {
|
fn cached_pipeline(&self) -> CachedRenderPipelineId {
|
||||||
self.pipeline
|
self.pipeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Transparent3d {
|
pub struct Transparent3d {
|
||||||
pub distance: f32,
|
pub distance: f32,
|
||||||
pub pipeline: CachedPipelineId,
|
pub pipeline: CachedRenderPipelineId,
|
||||||
pub entity: Entity,
|
pub entity: Entity,
|
||||||
pub draw_function: DrawFunctionId,
|
pub draw_function: DrawFunctionId,
|
||||||
}
|
}
|
||||||
|
@ -340,9 +340,9 @@ impl EntityPhaseItem for Transparent3d {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CachedPipelinePhaseItem for Transparent3d {
|
impl CachedRenderPipelinePhaseItem for Transparent3d {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn cached_pipeline(&self) -> CachedPipelineId {
|
fn cached_pipeline(&self) -> CachedRenderPipelineId {
|
||||||
self.pipeline
|
self.pipeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ use bevy_render::{
|
||||||
SetItemPipeline, TrackedRenderPass,
|
SetItemPipeline, TrackedRenderPass,
|
||||||
},
|
},
|
||||||
render_resource::{
|
render_resource::{
|
||||||
BindGroup, BindGroupLayout, RenderPipelineCache, RenderPipelineDescriptor, Shader,
|
BindGroup, BindGroupLayout, PipelineCache, RenderPipelineDescriptor, Shader,
|
||||||
SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,
|
SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,
|
||||||
},
|
},
|
||||||
renderer::RenderDevice,
|
renderer::RenderDevice,
|
||||||
|
@ -307,7 +307,7 @@ pub fn queue_material_meshes<M: SpecializedMaterial>(
|
||||||
transparent_draw_functions: Res<DrawFunctions<Transparent3d>>,
|
transparent_draw_functions: Res<DrawFunctions<Transparent3d>>,
|
||||||
material_pipeline: Res<MaterialPipeline<M>>,
|
material_pipeline: Res<MaterialPipeline<M>>,
|
||||||
mut pipelines: ResMut<SpecializedMeshPipelines<MaterialPipeline<M>>>,
|
mut pipelines: ResMut<SpecializedMeshPipelines<MaterialPipeline<M>>>,
|
||||||
mut pipeline_cache: ResMut<RenderPipelineCache>,
|
mut pipeline_cache: ResMut<PipelineCache>,
|
||||||
msaa: Res<Msaa>,
|
msaa: Res<Msaa>,
|
||||||
render_meshes: Res<RenderAssets<Mesh>>,
|
render_meshes: Res<RenderAssets<Mesh>>,
|
||||||
render_materials: Res<RenderAssets<M>>,
|
render_materials: Res<RenderAssets<M>>,
|
||||||
|
|
|
@ -18,7 +18,7 @@ use bevy_render::{
|
||||||
render_asset::RenderAssets,
|
render_asset::RenderAssets,
|
||||||
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
|
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
|
||||||
render_phase::{
|
render_phase::{
|
||||||
CachedPipelinePhaseItem, DrawFunctionId, DrawFunctions, EntityPhaseItem,
|
CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, EntityPhaseItem,
|
||||||
EntityRenderCommand, PhaseItem, RenderCommandResult, RenderPhase, SetItemPipeline,
|
EntityRenderCommand, PhaseItem, RenderCommandResult, RenderPhase, SetItemPipeline,
|
||||||
TrackedRenderPass,
|
TrackedRenderPass,
|
||||||
},
|
},
|
||||||
|
@ -1055,7 +1055,7 @@ pub fn queue_shadows(
|
||||||
casting_meshes: Query<&Handle<Mesh>, Without<NotShadowCaster>>,
|
casting_meshes: Query<&Handle<Mesh>, Without<NotShadowCaster>>,
|
||||||
render_meshes: Res<RenderAssets<Mesh>>,
|
render_meshes: Res<RenderAssets<Mesh>>,
|
||||||
mut pipelines: ResMut<SpecializedMeshPipelines<ShadowPipeline>>,
|
mut pipelines: ResMut<SpecializedMeshPipelines<ShadowPipeline>>,
|
||||||
mut pipeline_cache: ResMut<RenderPipelineCache>,
|
mut pipeline_cache: ResMut<PipelineCache>,
|
||||||
view_lights: Query<&ViewLightEntities>,
|
view_lights: Query<&ViewLightEntities>,
|
||||||
mut view_light_shadow_phases: Query<(&LightEntity, &mut RenderPhase<Shadow>)>,
|
mut view_light_shadow_phases: Query<(&LightEntity, &mut RenderPhase<Shadow>)>,
|
||||||
point_light_entities: Query<&CubemapVisibleEntities, With<ExtractedPointLight>>,
|
point_light_entities: Query<&CubemapVisibleEntities, With<ExtractedPointLight>>,
|
||||||
|
@ -1119,7 +1119,7 @@ pub fn queue_shadows(
|
||||||
pub struct Shadow {
|
pub struct Shadow {
|
||||||
pub distance: f32,
|
pub distance: f32,
|
||||||
pub entity: Entity,
|
pub entity: Entity,
|
||||||
pub pipeline: CachedPipelineId,
|
pub pipeline: CachedRenderPipelineId,
|
||||||
pub draw_function: DrawFunctionId,
|
pub draw_function: DrawFunctionId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1143,9 +1143,9 @@ impl EntityPhaseItem for Shadow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CachedPipelinePhaseItem for Shadow {
|
impl CachedRenderPipelinePhaseItem for Shadow {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn cached_pipeline(&self) -> CachedPipelineId {
|
fn cached_pipeline(&self) -> CachedRenderPipelineId {
|
||||||
self.pipeline
|
self.pipeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,16 +5,14 @@ use bevy_asset::{load_internal_asset, Handle, HandleUntyped};
|
||||||
use bevy_core_pipeline::Opaque3d;
|
use bevy_core_pipeline::Opaque3d;
|
||||||
use bevy_ecs::{prelude::*, reflect::ReflectComponent};
|
use bevy_ecs::{prelude::*, reflect::ReflectComponent};
|
||||||
use bevy_reflect::{Reflect, TypeUuid};
|
use bevy_reflect::{Reflect, TypeUuid};
|
||||||
use bevy_render::mesh::MeshVertexBufferLayout;
|
|
||||||
use bevy_render::render_resource::{
|
|
||||||
PolygonMode, RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError,
|
|
||||||
SpecializedMeshPipelines,
|
|
||||||
};
|
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
mesh::Mesh,
|
mesh::{Mesh, MeshVertexBufferLayout},
|
||||||
render_asset::RenderAssets,
|
render_asset::RenderAssets,
|
||||||
render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline},
|
render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline},
|
||||||
render_resource::{RenderPipelineCache, Shader},
|
render_resource::{
|
||||||
|
PipelineCache, PolygonMode, RenderPipelineDescriptor, Shader, SpecializedMeshPipeline,
|
||||||
|
SpecializedMeshPipelineError, SpecializedMeshPipelines,
|
||||||
|
},
|
||||||
view::{ExtractedView, Msaa},
|
view::{ExtractedView, Msaa},
|
||||||
RenderApp, RenderStage,
|
RenderApp, RenderStage,
|
||||||
};
|
};
|
||||||
|
@ -109,8 +107,8 @@ fn queue_wireframes(
|
||||||
render_meshes: Res<RenderAssets<Mesh>>,
|
render_meshes: Res<RenderAssets<Mesh>>,
|
||||||
wireframe_config: Res<WireframeConfig>,
|
wireframe_config: Res<WireframeConfig>,
|
||||||
wireframe_pipeline: Res<WireframePipeline>,
|
wireframe_pipeline: Res<WireframePipeline>,
|
||||||
mut pipeline_cache: ResMut<RenderPipelineCache>,
|
mut pipelines: ResMut<SpecializedMeshPipelines<WireframePipeline>>,
|
||||||
mut specialized_pipelines: ResMut<SpecializedMeshPipelines<WireframePipeline>>,
|
mut pipeline_cache: ResMut<PipelineCache>,
|
||||||
msaa: Res<Msaa>,
|
msaa: Res<Msaa>,
|
||||||
mut material_meshes: QuerySet<(
|
mut material_meshes: QuerySet<(
|
||||||
QueryState<(Entity, &Handle<Mesh>, &MeshUniform)>,
|
QueryState<(Entity, &Handle<Mesh>, &MeshUniform)>,
|
||||||
|
@ -132,7 +130,7 @@ fn queue_wireframes(
|
||||||
if let Some(mesh) = render_meshes.get(mesh_handle) {
|
if let Some(mesh) = render_meshes.get(mesh_handle) {
|
||||||
let key = msaa_key
|
let key = msaa_key
|
||||||
| MeshPipelineKey::from_primitive_topology(mesh.primitive_topology);
|
| MeshPipelineKey::from_primitive_topology(mesh.primitive_topology);
|
||||||
let pipeline_id = specialized_pipelines.specialize(
|
let pipeline_id = pipelines.specialize(
|
||||||
&mut pipeline_cache,
|
&mut pipeline_cache,
|
||||||
&wireframe_pipeline,
|
&wireframe_pipeline,
|
||||||
key,
|
key,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
extern crate core;
|
||||||
|
|
||||||
pub mod camera;
|
pub mod camera;
|
||||||
pub mod color;
|
pub mod color;
|
||||||
pub mod mesh;
|
pub mod mesh;
|
||||||
|
@ -36,7 +38,7 @@ use crate::{
|
||||||
mesh::MeshPlugin,
|
mesh::MeshPlugin,
|
||||||
primitives::{CubemapFrusta, Frustum},
|
primitives::{CubemapFrusta, Frustum},
|
||||||
render_graph::RenderGraph,
|
render_graph::RenderGraph,
|
||||||
render_resource::{RenderPipelineCache, Shader, ShaderLoader},
|
render_resource::{PipelineCache, Shader, ShaderLoader},
|
||||||
renderer::render_system,
|
renderer::render_system,
|
||||||
texture::ImagePlugin,
|
texture::ImagePlugin,
|
||||||
view::{ViewPlugin, WindowRenderPlugin},
|
view::{ViewPlugin, WindowRenderPlugin},
|
||||||
|
@ -146,12 +148,13 @@ impl Plugin for RenderPlugin {
|
||||||
.init_resource::<ScratchRenderWorld>()
|
.init_resource::<ScratchRenderWorld>()
|
||||||
.register_type::<Frustum>()
|
.register_type::<Frustum>()
|
||||||
.register_type::<CubemapFrusta>();
|
.register_type::<CubemapFrusta>();
|
||||||
let render_pipeline_cache = RenderPipelineCache::new(device.clone());
|
|
||||||
|
let pipeline_cache = PipelineCache::new(device.clone());
|
||||||
let asset_server = app.world.resource::<AssetServer>().clone();
|
let asset_server = app.world.resource::<AssetServer>().clone();
|
||||||
|
|
||||||
let mut render_app = App::empty();
|
let mut render_app = App::empty();
|
||||||
let mut extract_stage =
|
let mut extract_stage =
|
||||||
SystemStage::parallel().with_system(RenderPipelineCache::extract_shaders);
|
SystemStage::parallel().with_system(PipelineCache::extract_shaders);
|
||||||
// don't apply buffers when the stage finishes running
|
// don't apply buffers when the stage finishes running
|
||||||
// extract stage runs on the app world, but the buffers are applied to the render world
|
// extract stage runs on the app world, but the buffers are applied to the render world
|
||||||
extract_stage.set_apply_buffers(false);
|
extract_stage.set_apply_buffers(false);
|
||||||
|
@ -163,7 +166,7 @@ impl Plugin for RenderPlugin {
|
||||||
.add_stage(
|
.add_stage(
|
||||||
RenderStage::Render,
|
RenderStage::Render,
|
||||||
SystemStage::parallel()
|
SystemStage::parallel()
|
||||||
.with_system(RenderPipelineCache::process_pipeline_queue_system)
|
.with_system(PipelineCache::process_pipeline_queue_system)
|
||||||
.with_system(render_system.exclusive_system().at_end()),
|
.with_system(render_system.exclusive_system().at_end()),
|
||||||
)
|
)
|
||||||
.add_stage(RenderStage::Cleanup, SystemStage::parallel())
|
.add_stage(RenderStage::Cleanup, SystemStage::parallel())
|
||||||
|
@ -171,7 +174,7 @@ impl Plugin for RenderPlugin {
|
||||||
.insert_resource(device)
|
.insert_resource(device)
|
||||||
.insert_resource(queue)
|
.insert_resource(queue)
|
||||||
.insert_resource(adapter_info)
|
.insert_resource(adapter_info)
|
||||||
.insert_resource(render_pipeline_cache)
|
.insert_resource(pipeline_cache)
|
||||||
.insert_resource(asset_server)
|
.insert_resource(asset_server)
|
||||||
.init_resource::<RenderGraph>();
|
.init_resource::<RenderGraph>();
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
render_phase::TrackedRenderPass,
|
render_phase::TrackedRenderPass,
|
||||||
render_resource::{CachedPipelineId, RenderPipelineCache},
|
render_resource::{CachedRenderPipelineId, PipelineCache},
|
||||||
};
|
};
|
||||||
use bevy_app::App;
|
use bevy_app::App;
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
|
@ -162,8 +162,8 @@ pub trait EntityPhaseItem: PhaseItem {
|
||||||
fn entity(&self) -> Entity;
|
fn entity(&self) -> Entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait CachedPipelinePhaseItem: PhaseItem {
|
pub trait CachedRenderPipelinePhaseItem: PhaseItem {
|
||||||
fn cached_pipeline(&self) -> CachedPipelineId;
|
fn cached_pipeline(&self) -> CachedRenderPipelineId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [`PhaseItem`] that can be batched dynamically.
|
/// A [`PhaseItem`] that can be batched dynamically.
|
||||||
|
@ -224,8 +224,8 @@ impl<P: EntityPhaseItem, E: EntityRenderCommand> RenderCommand<P> for E {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SetItemPipeline;
|
pub struct SetItemPipeline;
|
||||||
impl<P: CachedPipelinePhaseItem> RenderCommand<P> for SetItemPipeline {
|
impl<P: CachedRenderPipelinePhaseItem> RenderCommand<P> for SetItemPipeline {
|
||||||
type Param = SRes<RenderPipelineCache>;
|
type Param = SRes<PipelineCache>;
|
||||||
#[inline]
|
#[inline]
|
||||||
fn render<'w>(
|
fn render<'w>(
|
||||||
_view: Entity,
|
_view: Entity,
|
||||||
|
@ -233,7 +233,10 @@ impl<P: CachedPipelinePhaseItem> RenderCommand<P> for SetItemPipeline {
|
||||||
pipeline_cache: SystemParamItem<'w, '_, Self::Param>,
|
pipeline_cache: SystemParamItem<'w, '_, Self::Param>,
|
||||||
pass: &mut TrackedRenderPass<'w>,
|
pass: &mut TrackedRenderPass<'w>,
|
||||||
) -> RenderCommandResult {
|
) -> RenderCommandResult {
|
||||||
if let Some(pipeline) = pipeline_cache.into_inner().get(item.cached_pipeline()) {
|
if let Some(pipeline) = pipeline_cache
|
||||||
|
.into_inner()
|
||||||
|
.get_render_pipeline(item.cached_pipeline())
|
||||||
|
{
|
||||||
pass.set_render_pipeline(pipeline);
|
pass.set_render_pipeline(pipeline);
|
||||||
RenderCommandResult::Success
|
RenderCommandResult::Success
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -28,13 +28,13 @@ pub use wgpu::{
|
||||||
BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType,
|
BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType,
|
||||||
BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferBinding,
|
BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferBinding,
|
||||||
BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, ColorWrites,
|
BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, ColorWrites,
|
||||||
CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePassDescriptor,
|
CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePass, ComputePassDescriptor,
|
||||||
ComputePipelineDescriptor, DepthBiasState, DepthStencilState, Extent3d, Face,
|
ComputePipelineDescriptor as RawComputePipelineDescriptor, DepthBiasState, DepthStencilState,
|
||||||
Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState, FrontFace,
|
Extent3d, Face, Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState,
|
||||||
ImageCopyBuffer, ImageCopyBufferBase, ImageCopyTexture, ImageCopyTextureBase, ImageDataLayout,
|
FrontFace, ImageCopyBuffer, ImageCopyBufferBase, ImageCopyTexture, ImageCopyTextureBase,
|
||||||
ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, MapMode, MultisampleState,
|
ImageDataLayout, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, MapMode,
|
||||||
Operations, Origin3d, PipelineLayout, PipelineLayoutDescriptor, PolygonMode, PrimitiveState,
|
MultisampleState, Operations, Origin3d, PipelineLayout, PipelineLayoutDescriptor, PolygonMode,
|
||||||
PrimitiveTopology, RenderPassColorAttachment, RenderPassDepthStencilAttachment,
|
PrimitiveState, PrimitiveTopology, RenderPassColorAttachment, RenderPassDepthStencilAttachment,
|
||||||
RenderPassDescriptor, RenderPipelineDescriptor as RawRenderPipelineDescriptor,
|
RenderPassDescriptor, RenderPipelineDescriptor as RawRenderPipelineDescriptor,
|
||||||
SamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource,
|
SamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource,
|
||||||
ShaderStages, StencilFaceState, StencilOperation, StencilState, StorageTextureAccess,
|
ShaderStages, StencilFaceState, StencilOperation, StencilState, StorageTextureAccess,
|
||||||
|
|
|
@ -168,3 +168,16 @@ pub struct FragmentState {
|
||||||
/// The color state of the render targets.
|
/// The color state of the render targets.
|
||||||
pub targets: Vec<ColorTargetState>,
|
pub targets: Vec<ColorTargetState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Describes a compute pipeline.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ComputePipelineDescriptor {
|
||||||
|
pub label: Option<Cow<'static, str>>,
|
||||||
|
pub layout: Option<Vec<BindGroupLayout>>,
|
||||||
|
/// The compiled shader module for this stage.
|
||||||
|
pub shader: Handle<Shader>,
|
||||||
|
pub shader_defs: Vec<String>,
|
||||||
|
/// The name of the entry point in the compiled shader. There must be a
|
||||||
|
/// function with this name in the shader.
|
||||||
|
pub entry_point: Cow<'static, str>,
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
render_resource::{
|
render_resource::{
|
||||||
AsModuleDescriptorError, BindGroupLayout, BindGroupLayoutId, ProcessShaderError,
|
AsModuleDescriptorError, BindGroupLayout, BindGroupLayoutId, ComputePipeline,
|
||||||
RawFragmentState, RawRenderPipelineDescriptor, RawVertexState, RenderPipeline,
|
ComputePipelineDescriptor, ProcessShaderError, ProcessedShader,
|
||||||
RenderPipelineDescriptor, Shader, ShaderImport, ShaderProcessor, ShaderReflectError,
|
RawComputePipelineDescriptor, RawFragmentState, RawRenderPipelineDescriptor,
|
||||||
|
RawVertexState, RenderPipeline, RenderPipelineDescriptor, Shader, ShaderImport,
|
||||||
|
ShaderProcessor, ShaderReflectError,
|
||||||
},
|
},
|
||||||
renderer::RenderDevice,
|
renderer::RenderDevice,
|
||||||
RenderWorld,
|
RenderWorld,
|
||||||
|
@ -10,12 +12,61 @@ use crate::{
|
||||||
use bevy_asset::{AssetEvent, Assets, Handle};
|
use bevy_asset::{AssetEvent, Assets, Handle};
|
||||||
use bevy_ecs::event::EventReader;
|
use bevy_ecs::event::EventReader;
|
||||||
use bevy_ecs::system::{Res, ResMut};
|
use bevy_ecs::system::{Res, ResMut};
|
||||||
use bevy_utils::{tracing::error, Entry, HashMap, HashSet};
|
use bevy_utils::{default, tracing::error, Entry, HashMap, HashSet};
|
||||||
use std::{hash::Hash, ops::Deref, sync::Arc};
|
use std::{hash::Hash, mem, ops::Deref, sync::Arc};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use wgpu::{PipelineLayoutDescriptor, ShaderModule, VertexBufferLayout as RawVertexBufferLayout};
|
use wgpu::{PipelineLayoutDescriptor, ShaderModule, VertexBufferLayout as RawVertexBufferLayout};
|
||||||
|
|
||||||
use super::ProcessedShader;
|
enum PipelineDescriptor {
|
||||||
|
RenderPipelineDescriptor(Box<RenderPipelineDescriptor>),
|
||||||
|
ComputePipelineDescriptor(Box<ComputePipelineDescriptor>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Pipeline {
|
||||||
|
RenderPipeline(RenderPipeline),
|
||||||
|
ComputePipeline(ComputePipeline),
|
||||||
|
}
|
||||||
|
|
||||||
|
type CachedPipelineId = usize;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
|
pub struct CachedRenderPipelineId(CachedPipelineId);
|
||||||
|
|
||||||
|
impl CachedRenderPipelineId {
|
||||||
|
pub const INVALID: Self = CachedRenderPipelineId(usize::MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||||
|
pub struct CachedComputePipelineId(CachedPipelineId);
|
||||||
|
|
||||||
|
impl CachedComputePipelineId {
|
||||||
|
pub const INVALID: Self = CachedComputePipelineId(usize::MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CachedPipeline {
|
||||||
|
descriptor: PipelineDescriptor,
|
||||||
|
state: CachedPipelineState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CachedPipelineState {
|
||||||
|
Queued,
|
||||||
|
Ok(Pipeline),
|
||||||
|
Err(PipelineCacheError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CachedPipelineState {
|
||||||
|
pub fn unwrap(&self) -> &Pipeline {
|
||||||
|
match self {
|
||||||
|
CachedPipelineState::Ok(pipeline) => pipeline,
|
||||||
|
CachedPipelineState::Queued => {
|
||||||
|
panic!("Pipeline has not been compiled yet. It is still in the 'Queued' state.")
|
||||||
|
}
|
||||||
|
CachedPipelineState::Err(err) => panic!("{}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ShaderData {
|
pub struct ShaderData {
|
||||||
|
@ -25,13 +76,6 @@ pub struct ShaderData {
|
||||||
dependents: HashSet<Handle<Shader>>,
|
dependents: HashSet<Handle<Shader>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
|
||||||
pub struct CachedPipelineId(usize);
|
|
||||||
|
|
||||||
impl CachedPipelineId {
|
|
||||||
pub const INVALID: Self = CachedPipelineId(usize::MAX);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct ShaderCache {
|
struct ShaderCache {
|
||||||
data: HashMap<Handle<Shader>, ShaderData>,
|
data: HashMap<Handle<Shader>, ShaderData>,
|
||||||
|
@ -48,11 +92,11 @@ impl ShaderCache {
|
||||||
pipeline: CachedPipelineId,
|
pipeline: CachedPipelineId,
|
||||||
handle: &Handle<Shader>,
|
handle: &Handle<Shader>,
|
||||||
shader_defs: &[String],
|
shader_defs: &[String],
|
||||||
) -> Result<Arc<ShaderModule>, RenderPipelineError> {
|
) -> Result<Arc<ShaderModule>, PipelineCacheError> {
|
||||||
let shader = self
|
let shader = self
|
||||||
.shaders
|
.shaders
|
||||||
.get(handle)
|
.get(handle)
|
||||||
.ok_or_else(|| RenderPipelineError::ShaderNotLoaded(handle.clone_weak()))?;
|
.ok_or_else(|| PipelineCacheError::ShaderNotLoaded(handle.clone_weak()))?;
|
||||||
let data = self.data.entry(handle.clone_weak()).or_default();
|
let data = self.data.entry(handle.clone_weak()).or_default();
|
||||||
let n_asset_imports = shader
|
let n_asset_imports = shader
|
||||||
.imports()
|
.imports()
|
||||||
|
@ -64,7 +108,7 @@ impl ShaderCache {
|
||||||
.filter(|import| matches!(import, ShaderImport::AssetPath(_)))
|
.filter(|import| matches!(import, ShaderImport::AssetPath(_)))
|
||||||
.count();
|
.count();
|
||||||
if n_asset_imports != n_resolved_asset_imports {
|
if n_asset_imports != n_resolved_asset_imports {
|
||||||
return Err(RenderPipelineError::ShaderImportNotYetAvailable);
|
return Err(PipelineCacheError::ShaderImportNotYetAvailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
data.pipelines.insert(pipeline);
|
data.pipelines.insert(pipeline);
|
||||||
|
@ -82,7 +126,7 @@ impl ShaderCache {
|
||||||
let module_descriptor = match processed.get_module_descriptor() {
|
let module_descriptor = match processed.get_module_descriptor() {
|
||||||
Ok(module_descriptor) => module_descriptor,
|
Ok(module_descriptor) => module_descriptor,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(RenderPipelineError::AsModuleDescriptorError(err, processed));
|
return Err(PipelineCacheError::AsModuleDescriptorError(err, processed));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
entry.insert(Arc::new(
|
entry.insert(Arc::new(
|
||||||
|
@ -176,13 +220,13 @@ impl LayoutCache {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
||||||
bind_group_layouts: &bind_group_layouts,
|
bind_group_layouts: &bind_group_layouts,
|
||||||
..Default::default()
|
..default()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RenderPipelineCache {
|
pub struct PipelineCache {
|
||||||
layout_cache: LayoutCache,
|
layout_cache: LayoutCache,
|
||||||
shader_cache: ShaderCache,
|
shader_cache: ShaderCache,
|
||||||
device: RenderDevice,
|
device: RenderDevice,
|
||||||
|
@ -190,88 +234,101 @@ pub struct RenderPipelineCache {
|
||||||
waiting_pipelines: HashSet<CachedPipelineId>,
|
waiting_pipelines: HashSet<CachedPipelineId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CachedPipeline {
|
impl PipelineCache {
|
||||||
descriptor: RenderPipelineDescriptor,
|
|
||||||
state: CachedPipelineState,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum CachedPipelineState {
|
|
||||||
Queued,
|
|
||||||
Ok(RenderPipeline),
|
|
||||||
Err(RenderPipelineError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CachedPipelineState {
|
|
||||||
pub fn unwrap(&self) -> &RenderPipeline {
|
|
||||||
match self {
|
|
||||||
CachedPipelineState::Ok(pipeline) => pipeline,
|
|
||||||
CachedPipelineState::Queued => {
|
|
||||||
panic!("Pipeline has not been compiled yet. It is still in the 'Queued' state.")
|
|
||||||
}
|
|
||||||
CachedPipelineState::Err(err) => panic!("{}", err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum RenderPipelineError {
|
|
||||||
#[error(
|
|
||||||
"Pipeline cound not be compiled because the following shader is not loaded yet: {0:?}"
|
|
||||||
)]
|
|
||||||
ShaderNotLoaded(Handle<Shader>),
|
|
||||||
#[error(transparent)]
|
|
||||||
ProcessShaderError(#[from] ProcessShaderError),
|
|
||||||
#[error("{0}")]
|
|
||||||
AsModuleDescriptorError(AsModuleDescriptorError, ProcessedShader),
|
|
||||||
#[error("Shader import not yet available.")]
|
|
||||||
ShaderImportNotYetAvailable,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderPipelineCache {
|
|
||||||
pub fn new(device: RenderDevice) -> Self {
|
pub fn new(device: RenderDevice) -> Self {
|
||||||
Self {
|
Self {
|
||||||
device,
|
device,
|
||||||
layout_cache: Default::default(),
|
layout_cache: default(),
|
||||||
shader_cache: Default::default(),
|
shader_cache: default(),
|
||||||
waiting_pipelines: Default::default(),
|
waiting_pipelines: default(),
|
||||||
pipelines: Default::default(),
|
pipelines: default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_state(&self, id: CachedPipelineId) -> &CachedPipelineState {
|
pub fn get_render_pipeline_state(&self, id: CachedRenderPipelineId) -> &CachedPipelineState {
|
||||||
&self.pipelines[id.0].state
|
&self.pipelines[id.0].state
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_descriptor(&self, id: CachedPipelineId) -> &RenderPipelineDescriptor {
|
pub fn get_compute_pipeline_state(&self, id: CachedComputePipelineId) -> &CachedPipelineState {
|
||||||
&self.pipelines[id.0].descriptor
|
&self.pipelines[id.0].state
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get(&self, id: CachedPipelineId) -> Option<&RenderPipeline> {
|
pub fn get_render_pipeline_descriptor(
|
||||||
if let CachedPipelineState::Ok(pipeline) = &self.pipelines[id.0].state {
|
&self,
|
||||||
|
id: CachedRenderPipelineId,
|
||||||
|
) -> &RenderPipelineDescriptor {
|
||||||
|
match &self.pipelines[id.0].descriptor {
|
||||||
|
PipelineDescriptor::RenderPipelineDescriptor(descriptor) => descriptor,
|
||||||
|
PipelineDescriptor::ComputePipelineDescriptor(_) => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get_compute_pipeline_descriptor(
|
||||||
|
&self,
|
||||||
|
id: CachedComputePipelineId,
|
||||||
|
) -> &ComputePipelineDescriptor {
|
||||||
|
match &self.pipelines[id.0].descriptor {
|
||||||
|
PipelineDescriptor::RenderPipelineDescriptor(_) => unreachable!(),
|
||||||
|
PipelineDescriptor::ComputePipelineDescriptor(descriptor) => descriptor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn get_render_pipeline(&self, id: CachedRenderPipelineId) -> Option<&RenderPipeline> {
|
||||||
|
if let CachedPipelineState::Ok(Pipeline::RenderPipeline(pipeline)) =
|
||||||
|
&self.pipelines[id.0].state
|
||||||
|
{
|
||||||
Some(pipeline)
|
Some(pipeline)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn queue(&mut self, descriptor: RenderPipelineDescriptor) -> CachedPipelineId {
|
#[inline]
|
||||||
let id = CachedPipelineId(self.pipelines.len());
|
pub fn get_compute_pipeline(&self, id: CachedComputePipelineId) -> Option<&ComputePipeline> {
|
||||||
|
if let CachedPipelineState::Ok(Pipeline::ComputePipeline(pipeline)) =
|
||||||
|
&self.pipelines[id.0].state
|
||||||
|
{
|
||||||
|
Some(pipeline)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_render_pipeline(
|
||||||
|
&mut self,
|
||||||
|
descriptor: RenderPipelineDescriptor,
|
||||||
|
) -> CachedRenderPipelineId {
|
||||||
|
let id = CachedRenderPipelineId(self.pipelines.len());
|
||||||
self.pipelines.push(CachedPipeline {
|
self.pipelines.push(CachedPipeline {
|
||||||
descriptor,
|
descriptor: PipelineDescriptor::RenderPipelineDescriptor(Box::new(descriptor)),
|
||||||
state: CachedPipelineState::Queued,
|
state: CachedPipelineState::Queued,
|
||||||
});
|
});
|
||||||
self.waiting_pipelines.insert(id);
|
self.waiting_pipelines.insert(id.0);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn queue_compute_pipeline(
|
||||||
|
&mut self,
|
||||||
|
descriptor: ComputePipelineDescriptor,
|
||||||
|
) -> CachedComputePipelineId {
|
||||||
|
let id = CachedComputePipelineId(self.pipelines.len());
|
||||||
|
self.pipelines.push(CachedPipeline {
|
||||||
|
descriptor: PipelineDescriptor::ComputePipelineDescriptor(Box::new(descriptor)),
|
||||||
|
state: CachedPipelineState::Queued,
|
||||||
|
});
|
||||||
|
self.waiting_pipelines.insert(id.0);
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_shader(&mut self, handle: &Handle<Shader>, shader: &Shader) {
|
fn set_shader(&mut self, handle: &Handle<Shader>, shader: &Shader) {
|
||||||
let pipelines_to_queue = self.shader_cache.set_shader(handle, shader.clone());
|
let pipelines_to_queue = self.shader_cache.set_shader(handle, shader.clone());
|
||||||
for cached_pipeline in pipelines_to_queue {
|
for cached_pipeline in pipelines_to_queue {
|
||||||
self.pipelines[cached_pipeline.0].state = CachedPipelineState::Queued;
|
self.pipelines[cached_pipeline].state = CachedPipelineState::Queued;
|
||||||
self.waiting_pipelines.insert(cached_pipeline);
|
self.waiting_pipelines.insert(cached_pipeline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -279,28 +336,146 @@ impl RenderPipelineCache {
|
||||||
fn remove_shader(&mut self, shader: &Handle<Shader>) {
|
fn remove_shader(&mut self, shader: &Handle<Shader>) {
|
||||||
let pipelines_to_queue = self.shader_cache.remove(shader);
|
let pipelines_to_queue = self.shader_cache.remove(shader);
|
||||||
for cached_pipeline in pipelines_to_queue {
|
for cached_pipeline in pipelines_to_queue {
|
||||||
self.pipelines[cached_pipeline.0].state = CachedPipelineState::Queued;
|
self.pipelines[cached_pipeline].state = CachedPipelineState::Queued;
|
||||||
self.waiting_pipelines.insert(cached_pipeline);
|
self.waiting_pipelines.insert(cached_pipeline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn process_render_pipeline(
|
||||||
|
&mut self,
|
||||||
|
id: CachedPipelineId,
|
||||||
|
descriptor: &RenderPipelineDescriptor,
|
||||||
|
) -> CachedPipelineState {
|
||||||
|
let vertex_module = match self.shader_cache.get(
|
||||||
|
&self.device,
|
||||||
|
id,
|
||||||
|
&descriptor.vertex.shader,
|
||||||
|
&descriptor.vertex.shader_defs,
|
||||||
|
) {
|
||||||
|
Ok(module) => module,
|
||||||
|
Err(err) => {
|
||||||
|
return CachedPipelineState::Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let fragment_data = if let Some(fragment) = &descriptor.fragment {
|
||||||
|
let fragment_module = match self.shader_cache.get(
|
||||||
|
&self.device,
|
||||||
|
id,
|
||||||
|
&fragment.shader,
|
||||||
|
&fragment.shader_defs,
|
||||||
|
) {
|
||||||
|
Ok(module) => module,
|
||||||
|
Err(err) => {
|
||||||
|
return CachedPipelineState::Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some((
|
||||||
|
fragment_module,
|
||||||
|
fragment.entry_point.deref(),
|
||||||
|
&fragment.targets,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let vertex_buffer_layouts = descriptor
|
||||||
|
.vertex
|
||||||
|
.buffers
|
||||||
|
.iter()
|
||||||
|
.map(|layout| RawVertexBufferLayout {
|
||||||
|
array_stride: layout.array_stride,
|
||||||
|
attributes: &layout.attributes,
|
||||||
|
step_mode: layout.step_mode,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let layout = if let Some(layout) = &descriptor.layout {
|
||||||
|
Some(self.layout_cache.get(&self.device, layout))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let descriptor = RawRenderPipelineDescriptor {
|
||||||
|
multiview: None,
|
||||||
|
depth_stencil: descriptor.depth_stencil.clone(),
|
||||||
|
label: descriptor.label.as_deref(),
|
||||||
|
layout,
|
||||||
|
multisample: descriptor.multisample,
|
||||||
|
primitive: descriptor.primitive,
|
||||||
|
vertex: RawVertexState {
|
||||||
|
buffers: &vertex_buffer_layouts,
|
||||||
|
entry_point: descriptor.vertex.entry_point.deref(),
|
||||||
|
module: &vertex_module,
|
||||||
|
},
|
||||||
|
fragment: fragment_data
|
||||||
|
.as_ref()
|
||||||
|
.map(|(module, entry_point, targets)| RawFragmentState {
|
||||||
|
entry_point,
|
||||||
|
module,
|
||||||
|
targets,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let pipeline = self.device.create_render_pipeline(&descriptor);
|
||||||
|
|
||||||
|
CachedPipelineState::Ok(Pipeline::RenderPipeline(pipeline))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_compute_pipeline(
|
||||||
|
&mut self,
|
||||||
|
id: CachedPipelineId,
|
||||||
|
descriptor: &ComputePipelineDescriptor,
|
||||||
|
) -> CachedPipelineState {
|
||||||
|
let compute_module = match self.shader_cache.get(
|
||||||
|
&self.device,
|
||||||
|
id,
|
||||||
|
&descriptor.shader,
|
||||||
|
&descriptor.shader_defs,
|
||||||
|
) {
|
||||||
|
Ok(module) => module,
|
||||||
|
Err(err) => {
|
||||||
|
return CachedPipelineState::Err(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let layout = if let Some(layout) = &descriptor.layout {
|
||||||
|
Some(self.layout_cache.get(&self.device, layout))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let descriptor = RawComputePipelineDescriptor {
|
||||||
|
label: descriptor.label.as_deref(),
|
||||||
|
layout,
|
||||||
|
module: &compute_module,
|
||||||
|
entry_point: descriptor.entry_point.as_ref(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let pipeline = self.device.create_compute_pipeline(&descriptor);
|
||||||
|
|
||||||
|
CachedPipelineState::Ok(Pipeline::ComputePipeline(pipeline))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_queue(&mut self) {
|
pub fn process_queue(&mut self) {
|
||||||
let pipelines = std::mem::take(&mut self.waiting_pipelines);
|
let waiting_pipelines = mem::take(&mut self.waiting_pipelines);
|
||||||
for id in pipelines {
|
let mut pipelines = mem::take(&mut self.pipelines);
|
||||||
let state = &mut self.pipelines[id.0];
|
|
||||||
match &state.state {
|
for id in waiting_pipelines {
|
||||||
|
let pipeline = &mut pipelines[id];
|
||||||
|
match &pipeline.state {
|
||||||
CachedPipelineState::Ok(_) => continue,
|
CachedPipelineState::Ok(_) => continue,
|
||||||
CachedPipelineState::Queued => {}
|
CachedPipelineState::Queued => {}
|
||||||
CachedPipelineState::Err(err) => {
|
CachedPipelineState::Err(err) => {
|
||||||
match err {
|
match err {
|
||||||
RenderPipelineError::ShaderNotLoaded(_)
|
PipelineCacheError::ShaderNotLoaded(_)
|
||||||
| RenderPipelineError::ShaderImportNotYetAvailable => { /* retry */ }
|
| PipelineCacheError::ShaderImportNotYetAvailable => { /* retry */ }
|
||||||
// shader could not be processed ... retrying won't help
|
// shader could not be processed ... retrying won't help
|
||||||
RenderPipelineError::ProcessShaderError(err) => {
|
PipelineCacheError::ProcessShaderError(err) => {
|
||||||
error!("failed to process shader: {}", err);
|
error!("failed to process shader: {}", err);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
RenderPipelineError::AsModuleDescriptorError(err, source) => {
|
PipelineCacheError::AsModuleDescriptorError(err, source) => {
|
||||||
log_shader_error(source, err);
|
log_shader_error(source, err);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -308,85 +483,21 @@ impl RenderPipelineCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let descriptor = &state.descriptor;
|
pipeline.state = match &pipeline.descriptor {
|
||||||
let vertex_module = match self.shader_cache.get(
|
PipelineDescriptor::RenderPipelineDescriptor(descriptor) => {
|
||||||
&self.device,
|
self.process_render_pipeline(id, descriptor)
|
||||||
id,
|
}
|
||||||
&descriptor.vertex.shader,
|
PipelineDescriptor::ComputePipelineDescriptor(descriptor) => {
|
||||||
&descriptor.vertex.shader_defs,
|
self.process_compute_pipeline(id, descriptor)
|
||||||
) {
|
|
||||||
Ok(module) => module,
|
|
||||||
Err(err) => {
|
|
||||||
state.state = CachedPipelineState::Err(err);
|
|
||||||
self.waiting_pipelines.insert(id);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let fragment_data = if let Some(fragment) = &descriptor.fragment {
|
if let CachedPipelineState::Err(_) = pipeline.state {
|
||||||
let fragment_module = match self.shader_cache.get(
|
self.waiting_pipelines.insert(id);
|
||||||
&self.device,
|
}
|
||||||
id,
|
|
||||||
&fragment.shader,
|
|
||||||
&fragment.shader_defs,
|
|
||||||
) {
|
|
||||||
Ok(module) => module,
|
|
||||||
Err(err) => {
|
|
||||||
state.state = CachedPipelineState::Err(err);
|
|
||||||
self.waiting_pipelines.insert(id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Some((
|
|
||||||
fragment_module,
|
|
||||||
fragment.entry_point.deref(),
|
|
||||||
&fragment.targets,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let vertex_buffer_layouts = descriptor
|
|
||||||
.vertex
|
|
||||||
.buffers
|
|
||||||
.iter()
|
|
||||||
.map(|layout| RawVertexBufferLayout {
|
|
||||||
array_stride: layout.array_stride,
|
|
||||||
attributes: &layout.attributes,
|
|
||||||
step_mode: layout.step_mode,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let layout = if let Some(layout) = &descriptor.layout {
|
|
||||||
Some(self.layout_cache.get(&self.device, layout))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let descriptor = RawRenderPipelineDescriptor {
|
|
||||||
multiview: None,
|
|
||||||
depth_stencil: descriptor.depth_stencil.clone(),
|
|
||||||
label: descriptor.label.as_deref(),
|
|
||||||
layout,
|
|
||||||
multisample: descriptor.multisample,
|
|
||||||
primitive: descriptor.primitive,
|
|
||||||
vertex: RawVertexState {
|
|
||||||
buffers: &vertex_buffer_layouts,
|
|
||||||
entry_point: descriptor.vertex.entry_point.deref(),
|
|
||||||
module: &vertex_module,
|
|
||||||
},
|
|
||||||
fragment: fragment_data
|
|
||||||
.as_ref()
|
|
||||||
.map(|(module, entry_point, targets)| RawFragmentState {
|
|
||||||
entry_point,
|
|
||||||
module,
|
|
||||||
targets,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
let pipeline = self.device.create_render_pipeline(&descriptor);
|
|
||||||
state.state = CachedPipelineState::Ok(pipeline);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.pipelines = pipelines;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn process_pipeline_queue_system(mut cache: ResMut<Self>) {
|
pub(crate) fn process_pipeline_queue_system(mut cache: ResMut<Self>) {
|
||||||
|
@ -503,6 +614,20 @@ fn log_shader_error(source: &ProcessedShader, error: &AsModuleDescriptorError) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum PipelineCacheError {
|
||||||
|
#[error(
|
||||||
|
"Pipeline cound not be compiled because the following shader is not loaded yet: {0:?}"
|
||||||
|
)]
|
||||||
|
ShaderNotLoaded(Handle<Shader>),
|
||||||
|
#[error(transparent)]
|
||||||
|
ProcessShaderError(#[from] ProcessShaderError),
|
||||||
|
#[error("{0}")]
|
||||||
|
AsModuleDescriptorError(AsModuleDescriptorError, ProcessedShader),
|
||||||
|
#[error("Shader import not yet available.")]
|
||||||
|
ShaderImportNotYetAvailable,
|
||||||
|
}
|
||||||
|
|
||||||
struct ErrorSources<'a> {
|
struct ErrorSources<'a> {
|
||||||
current: Option<&'a (dyn std::error::Error + 'static)>,
|
current: Option<&'a (dyn std::error::Error + 'static)>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +1,74 @@
|
||||||
|
use crate::render_resource::CachedComputePipelineId;
|
||||||
use crate::{
|
use crate::{
|
||||||
mesh::{InnerMeshVertexBufferLayout, MeshVertexBufferLayout, MissingVertexAttributeError},
|
mesh::{InnerMeshVertexBufferLayout, MeshVertexBufferLayout, MissingVertexAttributeError},
|
||||||
render_resource::{
|
render_resource::{
|
||||||
CachedPipelineId, RenderPipelineCache, RenderPipelineDescriptor, VertexBufferLayout,
|
CachedRenderPipelineId, ComputePipelineDescriptor, PipelineCache, RenderPipelineDescriptor,
|
||||||
|
VertexBufferLayout,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use bevy_utils::{
|
use bevy_utils::{
|
||||||
hashbrown::hash_map::RawEntryMut, tracing::error, Entry, HashMap, PreHashMap, PreHashMapExt,
|
default, hashbrown::hash_map::RawEntryMut, tracing::error, Entry, HashMap, PreHashMap,
|
||||||
|
PreHashMapExt,
|
||||||
};
|
};
|
||||||
use std::fmt::Debug;
|
use std::{fmt::Debug, hash::Hash};
|
||||||
use std::hash::Hash;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub trait SpecializedPipeline {
|
pub trait SpecializedRenderPipeline {
|
||||||
type Key: Clone + Hash + PartialEq + Eq;
|
type Key: Clone + Hash + PartialEq + Eq;
|
||||||
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor;
|
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SpecializedPipelines<S: SpecializedPipeline> {
|
pub struct SpecializedRenderPipelines<S: SpecializedRenderPipeline> {
|
||||||
cache: HashMap<S::Key, CachedPipelineId>,
|
cache: HashMap<S::Key, CachedRenderPipelineId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: SpecializedPipeline> Default for SpecializedPipelines<S> {
|
impl<S: SpecializedRenderPipeline> Default for SpecializedRenderPipelines<S> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self { cache: default() }
|
||||||
cache: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: SpecializedPipeline> SpecializedPipelines<S> {
|
impl<S: SpecializedRenderPipeline> SpecializedRenderPipelines<S> {
|
||||||
pub fn specialize(
|
pub fn specialize(
|
||||||
&mut self,
|
&mut self,
|
||||||
cache: &mut RenderPipelineCache,
|
cache: &mut PipelineCache,
|
||||||
specialize_pipeline: &S,
|
specialize_pipeline: &S,
|
||||||
key: S::Key,
|
key: S::Key,
|
||||||
) -> CachedPipelineId {
|
) -> CachedRenderPipelineId {
|
||||||
*self.cache.entry(key.clone()).or_insert_with(|| {
|
*self.cache.entry(key.clone()).or_insert_with(|| {
|
||||||
let descriptor = specialize_pipeline.specialize(key);
|
let descriptor = specialize_pipeline.specialize(key);
|
||||||
cache.queue(descriptor)
|
cache.queue_render_pipeline(descriptor)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
pub trait SpecializedComputePipeline {
|
||||||
pub enum SpecializedMeshPipelineError {
|
type Key: Clone + Hash + PartialEq + Eq;
|
||||||
#[error(transparent)]
|
fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor;
|
||||||
MissingVertexAttribute(#[from] MissingVertexAttributeError),
|
}
|
||||||
|
|
||||||
|
pub struct SpecializedComputePipelines<S: SpecializedComputePipeline> {
|
||||||
|
cache: HashMap<S::Key, CachedComputePipelineId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: SpecializedComputePipeline> Default for SpecializedComputePipelines<S> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { cache: default() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: SpecializedComputePipeline> SpecializedComputePipelines<S> {
|
||||||
|
pub fn specialize(
|
||||||
|
&mut self,
|
||||||
|
cache: &mut PipelineCache,
|
||||||
|
specialize_pipeline: &S,
|
||||||
|
key: S::Key,
|
||||||
|
) -> CachedComputePipelineId {
|
||||||
|
*self.cache.entry(key.clone()).or_insert_with(|| {
|
||||||
|
let descriptor = specialize_pipeline.specialize(key);
|
||||||
|
cache.queue_compute_pipeline(descriptor)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SpecializedMeshPipeline {
|
pub trait SpecializedMeshPipeline {
|
||||||
|
@ -58,8 +81,9 @@ pub trait SpecializedMeshPipeline {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SpecializedMeshPipelines<S: SpecializedMeshPipeline> {
|
pub struct SpecializedMeshPipelines<S: SpecializedMeshPipeline> {
|
||||||
mesh_layout_cache: PreHashMap<InnerMeshVertexBufferLayout, HashMap<S::Key, CachedPipelineId>>,
|
mesh_layout_cache:
|
||||||
vertex_layout_cache: HashMap<VertexBufferLayout, HashMap<S::Key, CachedPipelineId>>,
|
PreHashMap<InnerMeshVertexBufferLayout, HashMap<S::Key, CachedRenderPipelineId>>,
|
||||||
|
vertex_layout_cache: HashMap<VertexBufferLayout, HashMap<S::Key, CachedRenderPipelineId>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: SpecializedMeshPipeline> Default for SpecializedMeshPipelines<S> {
|
impl<S: SpecializedMeshPipeline> Default for SpecializedMeshPipelines<S> {
|
||||||
|
@ -75,11 +99,11 @@ impl<S: SpecializedMeshPipeline> SpecializedMeshPipelines<S> {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn specialize(
|
pub fn specialize(
|
||||||
&mut self,
|
&mut self,
|
||||||
cache: &mut RenderPipelineCache,
|
cache: &mut PipelineCache,
|
||||||
specialize_pipeline: &S,
|
specialize_pipeline: &S,
|
||||||
key: S::Key,
|
key: S::Key,
|
||||||
layout: &MeshVertexBufferLayout,
|
layout: &MeshVertexBufferLayout,
|
||||||
) -> Result<CachedPipelineId, SpecializedMeshPipelineError> {
|
) -> Result<CachedRenderPipelineId, SpecializedMeshPipelineError> {
|
||||||
let map = self
|
let map = self
|
||||||
.mesh_layout_cache
|
.mesh_layout_cache
|
||||||
.get_or_insert_with(layout, Default::default);
|
.get_or_insert_with(layout, Default::default);
|
||||||
|
@ -113,7 +137,7 @@ impl<S: SpecializedMeshPipeline> SpecializedMeshPipelines<S> {
|
||||||
Ok(*entry.insert(match layout_map.entry(key) {
|
Ok(*entry.insert(match layout_map.entry(key) {
|
||||||
Entry::Occupied(entry) => {
|
Entry::Occupied(entry) => {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
let stored_descriptor = cache.get_descriptor(*entry.get());
|
let stored_descriptor = cache.get_render_pipeline_descriptor(*entry.get());
|
||||||
if stored_descriptor != &descriptor {
|
if stored_descriptor != &descriptor {
|
||||||
error!("The cached pipeline descriptor for {} is not equal to the generated descriptor for the given key. This means the SpecializePipeline implementation uses 'unused' MeshVertexBufferLayout information to specialize the pipeline. This is not allowed because it would invalidate the pipeline cache.", std::any::type_name::<S>());
|
error!("The cached pipeline descriptor for {} is not equal to the generated descriptor for the given key. This means the SpecializePipeline implementation uses 'unused' MeshVertexBufferLayout information to specialize the pipeline. This is not allowed because it would invalidate the pipeline cache.", std::any::type_name::<S>());
|
||||||
}
|
}
|
||||||
|
@ -121,10 +145,16 @@ impl<S: SpecializedMeshPipeline> SpecializedMeshPipelines<S> {
|
||||||
*entry.into_mut()
|
*entry.into_mut()
|
||||||
}
|
}
|
||||||
Entry::Vacant(entry) => {
|
Entry::Vacant(entry) => {
|
||||||
*entry.insert(cache.queue(descriptor))
|
*entry.insert(cache.queue_render_pipeline(descriptor))
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum SpecializedMeshPipelineError {
|
||||||
|
#[error(transparent)]
|
||||||
|
MissingVertexAttribute(#[from] MissingVertexAttributeError),
|
||||||
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel};
|
||||||
use bevy_reflect::TypeUuid;
|
use bevy_reflect::TypeUuid;
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
render_phase::AddRenderCommand,
|
render_phase::AddRenderCommand,
|
||||||
render_resource::{Shader, SpecializedPipelines},
|
render_resource::{Shader, SpecializedRenderPipelines},
|
||||||
RenderApp, RenderStage,
|
RenderApp, RenderStage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ impl Plugin for SpritePlugin {
|
||||||
render_app
|
render_app
|
||||||
.init_resource::<ImageBindGroups>()
|
.init_resource::<ImageBindGroups>()
|
||||||
.init_resource::<SpritePipeline>()
|
.init_resource::<SpritePipeline>()
|
||||||
.init_resource::<SpecializedPipelines<SpritePipeline>>()
|
.init_resource::<SpecializedRenderPipelines<SpritePipeline>>()
|
||||||
.init_resource::<SpriteMeta>()
|
.init_resource::<SpriteMeta>()
|
||||||
.init_resource::<ExtractedSprites>()
|
.init_resource::<ExtractedSprites>()
|
||||||
.init_resource::<SpriteAssetEvents>()
|
.init_resource::<SpriteAssetEvents>()
|
||||||
|
|
|
@ -21,7 +21,7 @@ use bevy_render::{
|
||||||
SetItemPipeline, TrackedRenderPass,
|
SetItemPipeline, TrackedRenderPass,
|
||||||
},
|
},
|
||||||
render_resource::{
|
render_resource::{
|
||||||
BindGroup, BindGroupLayout, RenderPipelineCache, RenderPipelineDescriptor, Shader,
|
BindGroup, BindGroupLayout, PipelineCache, RenderPipelineDescriptor, Shader,
|
||||||
SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,
|
SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,
|
||||||
},
|
},
|
||||||
renderer::RenderDevice,
|
renderer::RenderDevice,
|
||||||
|
@ -291,7 +291,7 @@ pub fn queue_material2d_meshes<M: SpecializedMaterial2d>(
|
||||||
transparent_draw_functions: Res<DrawFunctions<Transparent2d>>,
|
transparent_draw_functions: Res<DrawFunctions<Transparent2d>>,
|
||||||
material2d_pipeline: Res<Material2dPipeline<M>>,
|
material2d_pipeline: Res<Material2dPipeline<M>>,
|
||||||
mut pipelines: ResMut<SpecializedMeshPipelines<Material2dPipeline<M>>>,
|
mut pipelines: ResMut<SpecializedMeshPipelines<Material2dPipeline<M>>>,
|
||||||
mut pipeline_cache: ResMut<RenderPipelineCache>,
|
mut pipeline_cache: ResMut<PipelineCache>,
|
||||||
msaa: Res<Msaa>,
|
msaa: Res<Msaa>,
|
||||||
render_meshes: Res<RenderAssets<Mesh>>,
|
render_meshes: Res<RenderAssets<Mesh>>,
|
||||||
render_materials: Res<RenderAssets<M>>,
|
render_materials: Res<RenderAssets<M>>,
|
||||||
|
|
|
@ -109,7 +109,7 @@ impl SpritePipelineKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpecializedPipeline for SpritePipeline {
|
impl SpecializedRenderPipeline for SpritePipeline {
|
||||||
type Key = SpritePipelineKey;
|
type Key = SpritePipelineKey;
|
||||||
|
|
||||||
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
||||||
|
@ -338,8 +338,8 @@ pub fn queue_sprites(
|
||||||
mut sprite_meta: ResMut<SpriteMeta>,
|
mut sprite_meta: ResMut<SpriteMeta>,
|
||||||
view_uniforms: Res<ViewUniforms>,
|
view_uniforms: Res<ViewUniforms>,
|
||||||
sprite_pipeline: Res<SpritePipeline>,
|
sprite_pipeline: Res<SpritePipeline>,
|
||||||
mut pipelines: ResMut<SpecializedPipelines<SpritePipeline>>,
|
mut pipelines: ResMut<SpecializedRenderPipelines<SpritePipeline>>,
|
||||||
mut pipeline_cache: ResMut<RenderPipelineCache>,
|
mut pipeline_cache: ResMut<PipelineCache>,
|
||||||
mut image_bind_groups: ResMut<ImageBindGroups>,
|
mut image_bind_groups: ResMut<ImageBindGroups>,
|
||||||
gpu_images: Res<RenderAssets<Image>>,
|
gpu_images: Res<RenderAssets<Image>>,
|
||||||
msaa: Res<Msaa>,
|
msaa: Res<Msaa>,
|
||||||
|
|
|
@ -64,7 +64,7 @@ pub fn build_ui_render(app: &mut App) {
|
||||||
|
|
||||||
render_app
|
render_app
|
||||||
.init_resource::<UiPipeline>()
|
.init_resource::<UiPipeline>()
|
||||||
.init_resource::<SpecializedPipelines<UiPipeline>>()
|
.init_resource::<SpecializedRenderPipelines<UiPipeline>>()
|
||||||
.init_resource::<UiImageBindGroups>()
|
.init_resource::<UiImageBindGroups>()
|
||||||
.init_resource::<UiMeta>()
|
.init_resource::<UiMeta>()
|
||||||
.init_resource::<ExtractedUiNodes>()
|
.init_resource::<ExtractedUiNodes>()
|
||||||
|
@ -403,8 +403,8 @@ pub fn queue_uinodes(
|
||||||
mut ui_meta: ResMut<UiMeta>,
|
mut ui_meta: ResMut<UiMeta>,
|
||||||
view_uniforms: Res<ViewUniforms>,
|
view_uniforms: Res<ViewUniforms>,
|
||||||
ui_pipeline: Res<UiPipeline>,
|
ui_pipeline: Res<UiPipeline>,
|
||||||
mut pipelines: ResMut<SpecializedPipelines<UiPipeline>>,
|
mut pipelines: ResMut<SpecializedRenderPipelines<UiPipeline>>,
|
||||||
mut pipeline_cache: ResMut<RenderPipelineCache>,
|
mut pipeline_cache: ResMut<PipelineCache>,
|
||||||
mut image_bind_groups: ResMut<UiImageBindGroups>,
|
mut image_bind_groups: ResMut<UiImageBindGroups>,
|
||||||
gpu_images: Res<RenderAssets<Image>>,
|
gpu_images: Res<RenderAssets<Image>>,
|
||||||
ui_batches: Query<(Entity, &UiBatch)>,
|
ui_batches: Query<(Entity, &UiBatch)>,
|
||||||
|
|
|
@ -62,7 +62,7 @@ impl FromWorld for UiPipeline {
|
||||||
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
pub struct UiPipelineKey {}
|
pub struct UiPipelineKey {}
|
||||||
|
|
||||||
impl SpecializedPipeline for UiPipeline {
|
impl SpecializedRenderPipeline for UiPipeline {
|
||||||
type Key = UiPipelineKey;
|
type Key = UiPipelineKey;
|
||||||
/// FIXME: there are no specialization for now, should this be removed?
|
/// FIXME: there are no specialization for now, should this be removed?
|
||||||
fn specialize(&self, _key: Self::Key) -> RenderPipelineDescriptor {
|
fn specialize(&self, _key: Self::Key) -> RenderPipelineDescriptor {
|
||||||
|
|
|
@ -8,7 +8,7 @@ use bevy_render::{
|
||||||
render_graph::*,
|
render_graph::*,
|
||||||
render_phase::*,
|
render_phase::*,
|
||||||
render_resource::{
|
render_resource::{
|
||||||
CachedPipelineId, LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor,
|
CachedRenderPipelineId, LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor,
|
||||||
},
|
},
|
||||||
renderer::*,
|
renderer::*,
|
||||||
view::*,
|
view::*,
|
||||||
|
@ -102,7 +102,7 @@ impl Node for UiPassNode {
|
||||||
pub struct TransparentUi {
|
pub struct TransparentUi {
|
||||||
pub sort_key: FloatOrd,
|
pub sort_key: FloatOrd,
|
||||||
pub entity: Entity,
|
pub entity: Entity,
|
||||||
pub pipeline: CachedPipelineId,
|
pub pipeline: CachedRenderPipelineId,
|
||||||
pub draw_function: DrawFunctionId,
|
pub draw_function: DrawFunctionId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,9 +127,9 @@ impl EntityPhaseItem for TransparentUi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CachedPipelinePhaseItem for TransparentUi {
|
impl CachedRenderPipelinePhaseItem for TransparentUi {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn cached_pipeline(&self) -> CachedPipelineId {
|
fn cached_pipeline(&self) -> CachedRenderPipelineId {
|
||||||
self.pipeline
|
self.pipeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ use bevy::{
|
||||||
render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline},
|
render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline},
|
||||||
render_resource::{
|
render_resource::{
|
||||||
BlendState, ColorTargetState, ColorWrites, Face, FragmentState, FrontFace,
|
BlendState, ColorTargetState, ColorWrites, Face, FragmentState, FrontFace,
|
||||||
MultisampleState, PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipelineCache,
|
MultisampleState, PipelineCache, PolygonMode, PrimitiveState, PrimitiveTopology,
|
||||||
RenderPipelineDescriptor, SpecializedPipeline, SpecializedPipelines, TextureFormat,
|
RenderPipelineDescriptor, SpecializedRenderPipeline, SpecializedRenderPipelines,
|
||||||
VertexBufferLayout, VertexFormat, VertexState, VertexStepMode,
|
TextureFormat, VertexBufferLayout, VertexFormat, VertexState, VertexStepMode,
|
||||||
},
|
},
|
||||||
texture::BevyDefault,
|
texture::BevyDefault,
|
||||||
view::VisibleEntities,
|
view::VisibleEntities,
|
||||||
|
@ -125,7 +125,7 @@ impl FromWorld for ColoredMesh2dPipeline {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We implement `SpecializedPipeline` to customize the default rendering from `Mesh2dPipeline`
|
// We implement `SpecializedPipeline` to customize the default rendering from `Mesh2dPipeline`
|
||||||
impl SpecializedPipeline for ColoredMesh2dPipeline {
|
impl SpecializedRenderPipeline for ColoredMesh2dPipeline {
|
||||||
type Key = Mesh2dPipelineKey;
|
type Key = Mesh2dPipelineKey;
|
||||||
|
|
||||||
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
||||||
|
@ -269,7 +269,7 @@ impl Plugin for ColoredMesh2dPlugin {
|
||||||
render_app
|
render_app
|
||||||
.add_render_command::<Transparent2d, DrawColoredMesh2d>()
|
.add_render_command::<Transparent2d, DrawColoredMesh2d>()
|
||||||
.init_resource::<ColoredMesh2dPipeline>()
|
.init_resource::<ColoredMesh2dPipeline>()
|
||||||
.init_resource::<SpecializedPipelines<ColoredMesh2dPipeline>>()
|
.init_resource::<SpecializedRenderPipelines<ColoredMesh2dPipeline>>()
|
||||||
.add_system_to_stage(RenderStage::Extract, extract_colored_mesh2d)
|
.add_system_to_stage(RenderStage::Extract, extract_colored_mesh2d)
|
||||||
.add_system_to_stage(RenderStage::Queue, queue_colored_mesh2d);
|
.add_system_to_stage(RenderStage::Queue, queue_colored_mesh2d);
|
||||||
}
|
}
|
||||||
|
@ -297,8 +297,8 @@ pub fn extract_colored_mesh2d(
|
||||||
pub fn queue_colored_mesh2d(
|
pub fn queue_colored_mesh2d(
|
||||||
transparent_draw_functions: Res<DrawFunctions<Transparent2d>>,
|
transparent_draw_functions: Res<DrawFunctions<Transparent2d>>,
|
||||||
colored_mesh2d_pipeline: Res<ColoredMesh2dPipeline>,
|
colored_mesh2d_pipeline: Res<ColoredMesh2dPipeline>,
|
||||||
mut pipelines: ResMut<SpecializedPipelines<ColoredMesh2dPipeline>>,
|
mut pipelines: ResMut<SpecializedRenderPipelines<ColoredMesh2dPipeline>>,
|
||||||
mut pipeline_cache: ResMut<RenderPipelineCache>,
|
mut pipeline_cache: ResMut<PipelineCache>,
|
||||||
msaa: Res<Msaa>,
|
msaa: Res<Msaa>,
|
||||||
render_meshes: Res<RenderAssets<Mesh>>,
|
render_meshes: Res<RenderAssets<Mesh>>,
|
||||||
colored_mesh2d: Query<(&Mesh2dHandle, &Mesh2dUniform), With<ColoredMesh2d>>,
|
colored_mesh2d: Query<(&Mesh2dHandle, &Mesh2dUniform), With<ColoredMesh2d>>,
|
||||||
|
|
|
@ -98,7 +98,7 @@ fn queue_custom(
|
||||||
custom_pipeline: Res<CustomPipeline>,
|
custom_pipeline: Res<CustomPipeline>,
|
||||||
msaa: Res<Msaa>,
|
msaa: Res<Msaa>,
|
||||||
mut pipelines: ResMut<SpecializedMeshPipelines<CustomPipeline>>,
|
mut pipelines: ResMut<SpecializedMeshPipelines<CustomPipeline>>,
|
||||||
mut pipeline_cache: ResMut<RenderPipelineCache>,
|
mut pipeline_cache: ResMut<PipelineCache>,
|
||||||
render_meshes: Res<RenderAssets<Mesh>>,
|
render_meshes: Res<RenderAssets<Mesh>>,
|
||||||
material_meshes: Query<(Entity, &MeshUniform, &Handle<Mesh>), With<CustomMaterial>>,
|
material_meshes: Query<(Entity, &MeshUniform, &Handle<Mesh>), With<CustomMaterial>>,
|
||||||
mut views: Query<(&ExtractedView, &mut RenderPhase<Transparent3d>)>,
|
mut views: Query<(&ExtractedView, &mut RenderPhase<Transparent3d>)>,
|
||||||
|
|
|
@ -10,6 +10,7 @@ use bevy::{
|
||||||
},
|
},
|
||||||
window::WindowDescriptor,
|
window::WindowDescriptor,
|
||||||
};
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
const SIZE: (u32, u32) = (1280, 720);
|
const SIZE: (u32, u32) = (1280, 720);
|
||||||
const WORKGROUP_SIZE: u32 = 8;
|
const WORKGROUP_SIZE: u32 = 8;
|
||||||
|
@ -67,7 +68,7 @@ impl Plugin for GameOfLifeComputePlugin {
|
||||||
.add_system_to_stage(RenderStage::Queue, queue_bind_group);
|
.add_system_to_stage(RenderStage::Queue, queue_bind_group);
|
||||||
|
|
||||||
let mut render_graph = render_app.world.resource_mut::<RenderGraph>();
|
let mut render_graph = render_app.world.resource_mut::<RenderGraph>();
|
||||||
render_graph.add_node("game_of_life", DispatchGameOfLife::default());
|
render_graph.add_node("game_of_life", GameOfLifeNode::default());
|
||||||
render_graph
|
render_graph
|
||||||
.add_node_edge("game_of_life", MAIN_PASS_DEPENDENCIES)
|
.add_node_edge("game_of_life", MAIN_PASS_DEPENDENCIES)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -80,6 +81,7 @@ struct GameOfLifeImageBindGroup(BindGroup);
|
||||||
fn extract_game_of_life_image(mut commands: Commands, image: Res<GameOfLifeImage>) {
|
fn extract_game_of_life_image(mut commands: Commands, image: Res<GameOfLifeImage>) {
|
||||||
commands.insert_resource(GameOfLifeImage(image.0.clone()));
|
commands.insert_resource(GameOfLifeImage(image.0.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn queue_bind_group(
|
fn queue_bind_group(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
pipeline: Res<GameOfLifePipeline>,
|
pipeline: Res<GameOfLifePipeline>,
|
||||||
|
@ -100,84 +102,96 @@ fn queue_bind_group(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GameOfLifePipeline {
|
pub struct GameOfLifePipeline {
|
||||||
sim_pipeline: ComputePipeline,
|
|
||||||
init_pipeline: ComputePipeline,
|
|
||||||
texture_bind_group_layout: BindGroupLayout,
|
texture_bind_group_layout: BindGroupLayout,
|
||||||
|
init_pipeline: CachedComputePipelineId,
|
||||||
|
update_pipeline: CachedComputePipelineId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromWorld for GameOfLifePipeline {
|
impl FromWorld for GameOfLifePipeline {
|
||||||
fn from_world(world: &mut World) -> Self {
|
fn from_world(world: &mut World) -> Self {
|
||||||
let render_device = world.resource::<RenderDevice>();
|
|
||||||
|
|
||||||
let shader_source = include_str!("../../assets/shaders/game_of_life.wgsl");
|
|
||||||
let shader = render_device.create_shader_module(&ShaderModuleDescriptor {
|
|
||||||
label: None,
|
|
||||||
source: ShaderSource::Wgsl(shader_source.into()),
|
|
||||||
});
|
|
||||||
|
|
||||||
let texture_bind_group_layout =
|
let texture_bind_group_layout =
|
||||||
render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
world
|
||||||
label: None,
|
.resource::<RenderDevice>()
|
||||||
entries: &[BindGroupLayoutEntry {
|
.create_bind_group_layout(&BindGroupLayoutDescriptor {
|
||||||
binding: 0,
|
label: None,
|
||||||
visibility: ShaderStages::COMPUTE,
|
entries: &[BindGroupLayoutEntry {
|
||||||
ty: BindingType::StorageTexture {
|
binding: 0,
|
||||||
access: StorageTextureAccess::ReadWrite,
|
visibility: ShaderStages::COMPUTE,
|
||||||
format: TextureFormat::Rgba8Unorm,
|
ty: BindingType::StorageTexture {
|
||||||
view_dimension: TextureViewDimension::D2,
|
access: StorageTextureAccess::ReadWrite,
|
||||||
},
|
format: TextureFormat::Rgba8Unorm,
|
||||||
count: None,
|
view_dimension: TextureViewDimension::D2,
|
||||||
}],
|
},
|
||||||
});
|
count: None,
|
||||||
|
}],
|
||||||
let pipeline_layout = render_device.create_pipeline_layout(&PipelineLayoutDescriptor {
|
});
|
||||||
|
let shader = world
|
||||||
|
.resource::<AssetServer>()
|
||||||
|
.load("shaders/game_of_life.wgsl");
|
||||||
|
let mut pipeline_cache = world.resource_mut::<PipelineCache>();
|
||||||
|
let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
|
||||||
label: None,
|
label: None,
|
||||||
bind_group_layouts: &[&texture_bind_group_layout],
|
layout: Some(vec![texture_bind_group_layout.clone()]),
|
||||||
push_constant_ranges: &[],
|
shader: shader.clone(),
|
||||||
|
shader_defs: vec![],
|
||||||
|
entry_point: Cow::from("init"),
|
||||||
});
|
});
|
||||||
let init_pipeline = render_device.create_compute_pipeline(&ComputePipelineDescriptor {
|
let update_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor {
|
||||||
label: None,
|
label: None,
|
||||||
layout: Some(&pipeline_layout),
|
layout: Some(vec![texture_bind_group_layout.clone()]),
|
||||||
module: &shader,
|
shader,
|
||||||
entry_point: "init",
|
shader_defs: vec![],
|
||||||
});
|
entry_point: Cow::from("update"),
|
||||||
let sim_pipeline = render_device.create_compute_pipeline(&ComputePipelineDescriptor {
|
|
||||||
label: None,
|
|
||||||
layout: Some(&pipeline_layout),
|
|
||||||
module: &shader,
|
|
||||||
entry_point: "update",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
GameOfLifePipeline {
|
GameOfLifePipeline {
|
||||||
sim_pipeline,
|
|
||||||
init_pipeline,
|
|
||||||
texture_bind_group_layout,
|
texture_bind_group_layout,
|
||||||
|
init_pipeline,
|
||||||
|
update_pipeline,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Initialized {
|
enum GameOfLifeState {
|
||||||
Default,
|
Loading,
|
||||||
No,
|
Init,
|
||||||
Yes,
|
Update,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DispatchGameOfLife {
|
struct GameOfLifeNode {
|
||||||
initialized: Initialized,
|
state: GameOfLifeState,
|
||||||
}
|
}
|
||||||
impl Default for DispatchGameOfLife {
|
|
||||||
|
impl Default for GameOfLifeNode {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
initialized: Initialized::Default,
|
state: GameOfLifeState::Loading,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl render_graph::Node for DispatchGameOfLife {
|
|
||||||
fn update(&mut self, _world: &mut World) {
|
impl render_graph::Node for GameOfLifeNode {
|
||||||
match self.initialized {
|
fn update(&mut self, world: &mut World) {
|
||||||
Initialized::Default => self.initialized = Initialized::No,
|
let pipeline = world.resource::<GameOfLifePipeline>();
|
||||||
Initialized::No => self.initialized = Initialized::Yes,
|
let pipeline_cache = world.resource::<PipelineCache>();
|
||||||
Initialized::Yes => {}
|
|
||||||
|
// if the corresponding pipeline has loaded, transition to the next stage
|
||||||
|
match self.state {
|
||||||
|
GameOfLifeState::Loading => {
|
||||||
|
if let CachedPipelineState::Ok(_) =
|
||||||
|
pipeline_cache.get_compute_pipeline_state(pipeline.init_pipeline)
|
||||||
|
{
|
||||||
|
self.state = GameOfLifeState::Init
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GameOfLifeState::Init => {
|
||||||
|
if let CachedPipelineState::Ok(_) =
|
||||||
|
pipeline_cache.get_compute_pipeline_state(pipeline.update_pipeline)
|
||||||
|
{
|
||||||
|
self.state = GameOfLifeState::Update
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GameOfLifeState::Update => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,22 +201,34 @@ impl render_graph::Node for DispatchGameOfLife {
|
||||||
render_context: &mut RenderContext,
|
render_context: &mut RenderContext,
|
||||||
world: &World,
|
world: &World,
|
||||||
) -> Result<(), render_graph::NodeRunError> {
|
) -> Result<(), render_graph::NodeRunError> {
|
||||||
let pipeline = world.resource::<GameOfLifePipeline>();
|
|
||||||
let texture_bind_group = &world.resource::<GameOfLifeImageBindGroup>().0;
|
let texture_bind_group = &world.resource::<GameOfLifeImageBindGroup>().0;
|
||||||
|
let pipeline_cache = world.resource::<PipelineCache>();
|
||||||
|
let pipeline = world.resource::<GameOfLifePipeline>();
|
||||||
|
|
||||||
let mut pass = render_context
|
let mut pass = render_context
|
||||||
.command_encoder
|
.command_encoder
|
||||||
.begin_compute_pass(&ComputePassDescriptor::default());
|
.begin_compute_pass(&ComputePassDescriptor::default());
|
||||||
|
|
||||||
if let Initialized::No = self.initialized {
|
|
||||||
pass.set_pipeline(&pipeline.init_pipeline);
|
|
||||||
pass.set_bind_group(0, texture_bind_group, &[]);
|
|
||||||
pass.dispatch(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pass.set_pipeline(&pipeline.sim_pipeline);
|
|
||||||
pass.set_bind_group(0, texture_bind_group, &[]);
|
pass.set_bind_group(0, texture_bind_group, &[]);
|
||||||
pass.dispatch(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1);
|
|
||||||
|
// select the pipeline based on the current state
|
||||||
|
match self.state {
|
||||||
|
GameOfLifeState::Loading => {}
|
||||||
|
GameOfLifeState::Init => {
|
||||||
|
let init_pipeline = pipeline_cache
|
||||||
|
.get_compute_pipeline(pipeline.init_pipeline)
|
||||||
|
.unwrap();
|
||||||
|
pass.set_pipeline(init_pipeline);
|
||||||
|
pass.dispatch(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1);
|
||||||
|
}
|
||||||
|
GameOfLifeState::Update => {
|
||||||
|
let update_pipeline = pipeline_cache
|
||||||
|
.get_compute_pipeline(pipeline.update_pipeline)
|
||||||
|
.unwrap();
|
||||||
|
pass.set_pipeline(update_pipeline);
|
||||||
|
pass.dispatch(SIZE.0 / WORKGROUP_SIZE, SIZE.1 / WORKGROUP_SIZE, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ use bevy::{
|
||||||
render_component::{ExtractComponent, ExtractComponentPlugin},
|
render_component::{ExtractComponent, ExtractComponentPlugin},
|
||||||
render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline},
|
render_phase::{AddRenderCommand, DrawFunctions, RenderPhase, SetItemPipeline},
|
||||||
render_resource::{
|
render_resource::{
|
||||||
RenderPipelineCache, RenderPipelineDescriptor, SpecializedMeshPipeline,
|
PipelineCache, RenderPipelineDescriptor, SpecializedMeshPipeline,
|
||||||
SpecializedMeshPipelineError, SpecializedMeshPipelines,
|
SpecializedMeshPipelineError, SpecializedMeshPipelines,
|
||||||
},
|
},
|
||||||
view::ExtractedView,
|
view::ExtractedView,
|
||||||
|
@ -139,7 +139,7 @@ fn queue_custom(
|
||||||
custom_pipeline: Res<IsRedPipeline>,
|
custom_pipeline: Res<IsRedPipeline>,
|
||||||
msaa: Res<Msaa>,
|
msaa: Res<Msaa>,
|
||||||
mut pipelines: ResMut<SpecializedMeshPipelines<IsRedPipeline>>,
|
mut pipelines: ResMut<SpecializedMeshPipelines<IsRedPipeline>>,
|
||||||
mut pipeline_cache: ResMut<RenderPipelineCache>,
|
mut pipeline_cache: ResMut<PipelineCache>,
|
||||||
material_meshes: Query<(Entity, &Handle<Mesh>, &MeshUniform, &IsRed)>,
|
material_meshes: Query<(Entity, &Handle<Mesh>, &MeshUniform, &IsRed)>,
|
||||||
mut views: Query<(&ExtractedView, &mut RenderPhase<Transparent3d>)>,
|
mut views: Query<(&ExtractedView, &mut RenderPhase<Transparent3d>)>,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -101,7 +101,7 @@ fn queue_custom(
|
||||||
custom_pipeline: Res<CustomPipeline>,
|
custom_pipeline: Res<CustomPipeline>,
|
||||||
msaa: Res<Msaa>,
|
msaa: Res<Msaa>,
|
||||||
mut pipelines: ResMut<SpecializedMeshPipelines<CustomPipeline>>,
|
mut pipelines: ResMut<SpecializedMeshPipelines<CustomPipeline>>,
|
||||||
mut pipeline_cache: ResMut<RenderPipelineCache>,
|
mut pipeline_cache: ResMut<PipelineCache>,
|
||||||
meshes: Res<RenderAssets<Mesh>>,
|
meshes: Res<RenderAssets<Mesh>>,
|
||||||
material_meshes: Query<
|
material_meshes: Query<
|
||||||
(Entity, &MeshUniform, &Handle<Mesh>),
|
(Entity, &MeshUniform, &Handle<Mesh>),
|
||||||
|
|
Loading…
Reference in a new issue