-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Live reloading of shaders #937
Changes from 2 commits
006e6e0
e6867e9
cc33b71
c4b7822
b33a6b3
182a39f
6b3c5fd
aa421c2
71a650c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
#version 450 | ||
|
||
layout(location = 0) out vec4 o_Target; | ||
|
||
layout(set = 1, binding = 1) uniform MyMaterial_color { | ||
vec4 color; | ||
}; | ||
|
||
void main() { | ||
o_Target = color * 0.5; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -282,4 +282,22 @@ impl PipelineCompiler { | |
}) | ||
.flatten() | ||
} | ||
|
||
/// Remove any specialized shaders or pipelines that match the | ||
/// changed shader set. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would remove "changed" here. In this local scope we don't know why the shaders in the set should be removed, just that they should. |
||
pub fn remove_shaders( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A few things:
It doesn't quite work, but this is the general idea: use super::{state_descriptors::PrimitiveTopology, IndexFormat, PipelineDescriptor};
use crate::{
pipeline::{BindType, InputStepMode, VertexBufferDescriptor},
renderer::RenderResourceContext,
shader::{Shader, ShaderSource},
};
use bevy_asset::{Assets, Handle};
use bevy_reflect::Reflect;
use bevy_utils::{HashMap, HashSet};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
#[derive(Clone, Eq, PartialEq, Debug, Reflect)]
pub struct PipelineSpecialization {
pub shader_specialization: ShaderSpecialization,
pub primitive_topology: PrimitiveTopology,
pub dynamic_bindings: Vec<String>,
pub index_format: IndexFormat,
pub vertex_buffer_descriptor: VertexBufferDescriptor,
pub sample_count: u32,
}
impl Default for PipelineSpecialization {
fn default() -> Self {
Self {
sample_count: 1,
index_format: IndexFormat::Uint32,
shader_specialization: Default::default(),
primitive_topology: Default::default(),
dynamic_bindings: Default::default(),
vertex_buffer_descriptor: Default::default(),
}
}
}
impl PipelineSpecialization {
pub fn empty() -> &'static PipelineSpecialization {
pub static EMPTY: Lazy<PipelineSpecialization> = Lazy::new(PipelineSpecialization::default);
&EMPTY
}
}
#[derive(Clone, Eq, PartialEq, Debug, Default, Reflect, Serialize, Deserialize)]
pub struct ShaderSpecialization {
pub shader_defs: HashSet<String>,
}
#[derive(Debug)]
struct SpecializedShader {
shader: Handle<Shader>,
pipelines: Vec<Handle<PipelineDescriptor>>,
specialization: ShaderSpecialization,
}
#[derive(Debug)]
struct SpecializedPipeline {
pipeline: Handle<PipelineDescriptor>,
specialization: PipelineSpecialization,
}
#[derive(Debug, Default)]
pub struct PipelineCompiler {
specialized_shaders: HashMap<Handle<Shader>, Vec<SpecializedShader>>,
specialized_shader_pipelines: HashMap<Handle<Shader>, Vec<Handle<PipelineDescriptor>>>,
specialized_pipelines: HashMap<Handle<PipelineDescriptor>, Vec<SpecializedPipeline>>,
}
impl PipelineCompiler {
fn compile_shader(
&mut self,
render_resource_context: &dyn RenderResourceContext,
shaders: &mut Assets<Shader>,
shader_handle: &Handle<Shader>,
shader_specialization: &ShaderSpecialization,
) -> Handle<Shader> {
let specialized_shaders = self
.specialized_shaders
.entry(shader_handle.clone_weak())
.or_insert_with(Vec::new);
let shader = shaders.get(shader_handle).unwrap();
// don't produce new shader if the input source is already spirv
if let ShaderSource::Spirv(_) = shader.source {
return shader_handle.clone_weak();
}
if let Some(specialized_shader) =
specialized_shaders
.iter()
.find(|current_specialized_shader| {
current_specialized_shader.specialization == *shader_specialization
})
{
// if shader has already been compiled with current configuration, use existing shader
specialized_shader.shader.clone_weak()
} else {
// if no shader exists with the current configuration, create new shader and compile
let shader_def_vec = shader_specialization
.shader_defs
.iter()
.cloned()
.collect::<Vec<String>>();
let compiled_shader =
render_resource_context.get_specialized_shader(shader, Some(&shader_def_vec));
let specialized_handle = shaders.add(compiled_shader);
let weak_specialized_handle = specialized_handle.clone_weak();
specialized_shaders.push(SpecializedShader {
shader: specialized_handle,
pipelines: Vec::new(),
specialization: shader_specialization.clone(),
});
weak_specialized_handle
}
}
pub fn get_specialized_pipeline(
&self,
pipeline: &Handle<PipelineDescriptor>,
specialization: &PipelineSpecialization,
) -> Option<Handle<PipelineDescriptor>> {
self.specialized_pipelines
.get(pipeline)
.and_then(|specialized_pipelines| {
specialized_pipelines
.iter()
.find(|current_specialized_pipeline| {
¤t_specialized_pipeline.specialization == specialization
})
})
.map(|specialized_pipeline| specialized_pipeline.pipeline.clone_weak())
}
pub fn compile_pipeline(
&mut self,
render_resource_context: &dyn RenderResourceContext,
pipelines: &mut Assets<PipelineDescriptor>,
shaders: &mut Assets<Shader>,
source_pipeline: &Handle<PipelineDescriptor>,
pipeline_specialization: &PipelineSpecialization,
) -> Handle<PipelineDescriptor> {
let source_descriptor = pipelines.get(source_pipeline).unwrap();
let mut specialized_descriptor = source_descriptor.clone();
let specialized_vertex_shader = self.compile_shader(
render_resource_context,
shaders,
&specialized_descriptor.shader_stages.vertex,
&pipeline_specialization.shader_specialization,
);
specialized_descriptor.shader_stages.vertex = specialized_vertex_shader.clone_weak();
let mut specialized_fragment_shader = None;
specialized_descriptor.shader_stages.fragment = specialized_descriptor
.shader_stages
.fragment
.as_ref()
.map(|fragment| {
let shader = self.compile_shader(
render_resource_context,
shaders,
fragment,
&pipeline_specialization.shader_specialization,
);
specialized_fragment_shader = Some(shader.clone_weak());
shader
});
let mut layout = render_resource_context.reflect_pipeline_layout(
&shaders,
&specialized_descriptor.shader_stages,
true,
);
if !pipeline_specialization.dynamic_bindings.is_empty() {
// set binding uniforms to dynamic if render resource bindings use dynamic
for bind_group in layout.bind_groups.iter_mut() {
let mut binding_changed = false;
for binding in bind_group.bindings.iter_mut() {
if pipeline_specialization
.dynamic_bindings
.iter()
.any(|b| b == &binding.name)
{
if let BindType::Uniform {
ref mut dynamic, ..
} = binding.bind_type
{
*dynamic = true;
binding_changed = true;
}
}
}
if binding_changed {
bind_group.update_id();
}
}
}
specialized_descriptor.layout = Some(layout);
// create a vertex layout that provides all attributes from either the specialized vertex buffers or a zero buffer
let mut pipeline_layout = specialized_descriptor.layout.as_mut().unwrap();
// the vertex buffer descriptor of the mesh
let mesh_vertex_buffer_descriptor = &pipeline_specialization.vertex_buffer_descriptor;
// the vertex buffer descriptor that will be used for this pipeline
let mut compiled_vertex_buffer_descriptor = VertexBufferDescriptor {
step_mode: InputStepMode::Vertex,
stride: mesh_vertex_buffer_descriptor.stride,
..Default::default()
};
for shader_vertex_attribute in pipeline_layout.vertex_buffer_descriptors.iter() {
let shader_vertex_attribute = shader_vertex_attribute
.attributes
.get(0)
.expect("Reflected layout has no attributes.");
if let Some(target_vertex_attribute) = mesh_vertex_buffer_descriptor
.attributes
.iter()
.find(|x| x.name == shader_vertex_attribute.name)
{
// copy shader location from reflected layout
let mut compiled_vertex_attribute = target_vertex_attribute.clone();
compiled_vertex_attribute.shader_location = shader_vertex_attribute.shader_location;
compiled_vertex_buffer_descriptor
.attributes
.push(compiled_vertex_attribute);
} else {
panic!(
"Attribute {} is required by shader, but not supplied by mesh. Either remove the attribute from the shader or supply the attribute ({}) to the mesh. ",
shader_vertex_attribute.name,
shader_vertex_attribute.name,
);
}
}
//TODO: add other buffers (like instancing) here
let mut vertex_buffer_descriptors = Vec::<VertexBufferDescriptor>::default();
vertex_buffer_descriptors.push(compiled_vertex_buffer_descriptor);
pipeline_layout.vertex_buffer_descriptors = vertex_buffer_descriptors;
specialized_descriptor.sample_count = pipeline_specialization.sample_count;
specialized_descriptor.primitive_topology = pipeline_specialization.primitive_topology;
specialized_descriptor.index_format = pipeline_specialization.index_format;
let specialized_pipeline_handle = pipelines.add(specialized_descriptor);
render_resource_context.create_render_pipeline(
specialized_pipeline_handle.clone_weak(),
pipelines.get(&specialized_pipeline_handle).unwrap(),
&shaders,
);
// track specialized shader pipelines
self.specialized_shader_pipelines
.entry(specialized_vertex_shader)
.or_insert_with(Default::default)
.push(specialized_pipeline_handle.clone_weak());
if let Some(specialized_fragment_shader) = specialized_fragment_shader {
self.specialized_shader_pipelines
.entry(specialized_fragment_shader)
.or_insert_with(Default::default)
.push(specialized_pipeline_handle.clone_weak());
}
let specialized_pipelines = self
.specialized_pipelines
.entry(source_pipeline.clone_weak())
.or_insert_with(Vec::new);
let weak_specialized_pipeline_handle = specialized_pipeline_handle.clone_weak();
specialized_pipelines.push(SpecializedPipeline {
pipeline: specialized_pipeline_handle,
specialization: pipeline_specialization.clone(),
});
weak_specialized_pipeline_handle
}
pub fn iter_compiled_pipelines(
&self,
pipeline_handle: Handle<PipelineDescriptor>,
) -> Option<impl Iterator<Item = &Handle<PipelineDescriptor>>> {
if let Some(compiled_pipelines) = self.specialized_pipelines.get(&pipeline_handle) {
Some(
compiled_pipelines
.iter()
.map(|specialized_pipeline| &specialized_pipeline.pipeline),
)
} else {
None
}
}
pub fn iter_all_compiled_pipelines(&self) -> impl Iterator<Item = &Handle<PipelineDescriptor>> {
self.specialized_pipelines
.values()
.map(|compiled_pipelines| {
compiled_pipelines
.iter()
.map(|specialized_pipeline| &specialized_pipeline.pipeline)
})
.flatten()
}
/// Remove any specialized shaders or pipelines that match the
/// changed shader set.
pub fn remove_shader(
&mut self,
shader: &Handle<Shader>,
pipelines: &mut Assets<PipelineDescriptor>,
shaders: &mut Assets<Shader>,
) {
if let Some(specialized_shaders) = self.specialized_shaders.remove(shader) {
for specialized_shader in specialized_shaders {
shaders.remove(&specialized_shader.shader);
if let Some(specialized_pipelines) = self
.specialized_shader_pipelines
.get(&specialized_shader.shader)
{
for specialized_pipeline in specialized_pipelines {
println!("remove pipeline");
self.specialized_pipelines.remove(specialized_pipeline);
pipelines.remove(specialized_pipeline);
}
}
}
}
}
} |
||
&mut self, | ||
shaders: HashSet<&Handle<Shader>>, | ||
pipelines: &Assets<PipelineDescriptor>, | ||
) { | ||
self.specialized_shaders.retain(|k, _| !shaders.contains(k)); | ||
self.specialized_pipelines.retain(|k, _| { | ||
pipelines | ||
.get(k) | ||
.unwrap() | ||
.shader_stages | ||
.iter() | ||
.all(|s| !shaders.contains(&s)) | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,9 +5,10 @@ use crate::{ | |
prelude::Msaa, | ||
renderer::RenderResourceBindings, | ||
}; | ||
use bevy_asset::{Assets, Handle}; | ||
use bevy_asset::{AssetEvent, Assets, Handle}; | ||
use bevy_ecs::{Query, Res, ResMut}; | ||
use bevy_reflect::Reflect; | ||
use bevy_utils::HashSet; | ||
|
||
#[derive(Debug, Default, Clone, Reflect)] | ||
pub struct RenderPipeline { | ||
|
@@ -82,6 +83,27 @@ pub fn draw_render_pipelines_system( | |
meshes: Res<Assets<Mesh>>, | ||
mut query: Query<(&mut Draw, &mut RenderPipelines, &Handle<Mesh>)>, | ||
) { | ||
// Pipelines will be rebuilt for shaders that have been modified. | ||
let mut changed_shaders = HashSet::default(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should move shader updating to its own system (like the current mesh_resource_provider_system) |
||
for event in draw_context | ||
.shader_event_reader | ||
.iter(&draw_context.shader_events) | ||
{ | ||
match event { | ||
AssetEvent::Modified { handle } => { | ||
changed_shaders.insert(handle); | ||
} | ||
// Creating shaders on the fly is unhandled since they | ||
// have to exist already when assigned to a pipeline. If a | ||
// shader is removed the pipeline keeps using its | ||
// specialized version. Maybe this should be a warning? | ||
AssetEvent::Created { .. } | AssetEvent::Removed { .. } => (), | ||
} | ||
} | ||
draw_context | ||
.pipeline_compiler | ||
.remove_shaders(changed_shaders, &draw_context.pipelines); | ||
|
||
for (mut draw, mut render_pipelines, mesh_handle) in query.iter_mut() { | ||
if !draw.is_visible { | ||
continue; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,29 +39,21 @@ void main() { | |
} | ||
"#; | ||
|
||
const FRAGMENT_SHADER: &str = r#" | ||
#version 450 | ||
layout(location = 0) out vec4 o_Target; | ||
layout(set = 1, binding = 1) uniform MyMaterial_color { | ||
vec4 color; | ||
}; | ||
void main() { | ||
o_Target = color; | ||
} | ||
"#; | ||
|
||
fn setup( | ||
commands: &mut Commands, | ||
asset_server: ResMut<AssetServer>, | ||
mut pipelines: ResMut<Assets<PipelineDescriptor>>, | ||
mut shaders: ResMut<Assets<Shader>>, | ||
mut meshes: ResMut<Assets<Mesh>>, | ||
mut materials: ResMut<Assets<MyMaterial>>, | ||
mut render_graph: ResMut<RenderGraph>, | ||
) { | ||
asset_server.watch_for_changes().unwrap(); | ||
|
||
// Create a new shader pipeline | ||
let pipeline_handle = pipelines.add(PipelineDescriptor::default_config(ShaderStages { | ||
vertex: shaders.add(Shader::from_glsl(ShaderStage::Vertex, VERTEX_SHADER)), | ||
fragment: Some(shaders.add(Shader::from_glsl(ShaderStage::Fragment, FRAGMENT_SHADER))), | ||
fragment: Some(asset_server.load::<Shader, _>("shaders/custom.frag")), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's probably worth having a comment here to explain why the vertex shader and fragment shader are being loaded in different ways, ie. to demonstrate the shader-as-asset functionality. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lets just create a new "hot_shader_reloading" example. I'd rather have multiple simple examples that illustrate specific features than pile too many features into the same example. |
||
})); | ||
|
||
// Add an AssetRenderResourcesNode to our Render Graph. This will bind MyMaterial resources to our shader | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
once we move shader updating to its own system, lets move these out of DrawContext (probably into a new Local resource used by the shader update system). I don't think they should live on DrawContext because "shader updating" is orthogonal to "drawing"