Skip to content
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

Merged
merged 9 commits into from
Dec 7, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions assets/shaders/custom.frag
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;
}
7 changes: 5 additions & 2 deletions crates/bevy_render/src/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use crate::{
},
shader::Shader,
};
use bevy_asset::{Assets, Handle};
use bevy_ecs::{Query, Res, ResMut, SystemParam};
use bevy_app::{EventReader, Events};
use bevy_asset::{AssetEvent, Assets, Handle};
use bevy_ecs::{Local, Query, Res, ResMut, SystemParam};
use bevy_reflect::Reflect;
use std::{ops::Range, sync::Arc};
use thiserror::Error;
Expand Down Expand Up @@ -123,6 +124,8 @@ pub enum DrawError {
pub struct DrawContext<'a> {
pub pipelines: ResMut<'a, Assets<PipelineDescriptor>>,
pub shaders: ResMut<'a, Assets<Shader>>,
pub shader_events: ResMut<'a, Events<AssetEvent<Shader>>>,
Copy link
Member

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"

pub shader_event_reader: Local<'a, EventReader<AssetEvent<Shader>>>,
pub pipeline_compiler: ResMut<'a, PipelineCompiler>,
pub render_resource_context: Res<'a, Box<dyn RenderResourceContext>>,
pub shared_buffers: Res<'a, SharedBuffers>,
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ use render_graph::{
RenderGraph,
};
use renderer::{AssetRenderResourceBindings, RenderResourceBindings};
use shader::ShaderLoader;
#[cfg(feature = "hdr")]
use texture::HdrTextureLoader;
#[cfg(feature = "png")]
Expand Down Expand Up @@ -87,6 +88,8 @@ impl Plugin for RenderPlugin {
app.init_asset_loader::<HdrTextureLoader>();
}

app.init_asset_loader::<ShaderLoader>();

if app.resources().get::<ClearColor>().is_none() {
app.resources_mut().insert(ClearColor::default());
}
Expand Down
18 changes: 18 additions & 0 deletions crates/bevy_render/src/pipeline/pipeline_compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,22 @@ impl PipelineCompiler {
})
.flatten()
}

/// Remove any specialized shaders or pipelines that match the
/// changed shader set.
Copy link
Member

Choose a reason for hiding this comment

The 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(
Copy link
Member

@cart cart Dec 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things:

  1. I think we should change this to a remove_shader operation instead of removing an arbitrary set of shaders
  2. Rather than doing scans, I think when we compile pipelines we should build maps that maintain specialized_pipeline <-> specialized_shader mappings. That way we can quickly clean up the relevant resources.
  3. We currently aren't cleaning up the specialized Assets and Assets. We should do that.

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| {
                        &current_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))
});
}
}
24 changes: 23 additions & 1 deletion crates/bevy_render/src/pipeline/render_pipelines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Copy link
Member

Choose a reason for hiding this comment

The 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;
Expand Down
31 changes: 30 additions & 1 deletion crates/bevy_render/src/shader/shader.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::ShaderLayout;
use bevy_asset::Handle;
use bevy_asset::{AssetLoader, Handle, LoadContext, LoadedAsset};
use bevy_reflect::TypeUuid;
use bevy_utils::BoxedFuture;
use std::marker::Copy;

/// The stage of a shader
Expand Down Expand Up @@ -188,3 +189,31 @@ impl ShaderStages {
}
}
}

#[derive(Default)]
pub struct ShaderLoader;

impl AssetLoader for ShaderLoader {
fn load<'a>(
&'a self,
bytes: &'a [u8],
load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<(), anyhow::Error>> {
Box::pin(async move {
let ext = load_context.path().extension().unwrap().to_str().unwrap();

let shader = match ext {
"vert" => Shader::from_glsl(ShaderStage::Vertex, std::str::from_utf8(bytes)?),
"frag" => Shader::from_glsl(ShaderStage::Fragment, std::str::from_utf8(bytes)?),
_ => panic!("unhandled extension: {}", ext),
};

load_context.set_default_asset(LoadedAsset::new(shader));
Ok(())
})
}

fn extensions(&self) -> &[&str] {
&["vert", "frag"]
}
}
16 changes: 4 additions & 12 deletions examples/shader/shader_custom_material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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
Expand Down