nannou-org / nannou

A Creative Coding Framework for Rust.
https://nannou.cc/
5.97k stars 304 forks source link

Add a `vk_hotload.rs` example that demonstrates compiling GLSL to SPIR-V and hotloading it at runtime. #223

Open mitchmindtree opened 5 years ago

mitchmindtree commented 5 years ago

This is a super useful technique for getting near-realtime feedback while prototyping graphics applications or sketching.

Currently vulkano only provides provides a way for loading SPIR-V shader code at runtime via the ShaderModule::new function. Originally I thought we might want to add an alternative ShaderModule::from_glsl constructor, but it looks like keeping a clear boundary and only handling SPIR-V is a conscious choice for vulkano itself.

Instead, it is the role of the vulkano-shaders crate for compiling GLSL to SPIR-V, however it currently only exposes a way to do this at compile time via a macro. It would be worth investigating how the vulkano-shaders crate does this compilation and exposing it in a way so that it can be shared between both the compile-time macro and a runtime function.

JoshuaBatty commented 5 years ago

Wouldn't it just be a matter of us somehow invoking the glslangValidator during runtime to convert the .frag shader for example to a .spv file and then load the resulting spir-v files using the techniques in this example?

mitchmindtree commented 5 years ago

Yeah that's probably what vulkano-shaders is currently doing at compile time, I reckon we just want to contribute a patch so that it also offers a runtime version of what it's currently doing at compile time.

mitchmindtree commented 5 years ago

There's an interesting issue here at vulkano-rs/vulkano#910 on dropping vulkano-shaders in favour of using rspirv for generating rust code from shaderc.

I wonder if it's worth just using shaderc directly (and not touching vulkano-shaders for now), or if rspirv might be useful for validation.

mitchmindtree commented 5 years ago

We could use notify for watching the glsl files, or alternatively I just came across hotwatch which looks like a friendlier wrapper around notify which might be more suitable for the small example.

freesig commented 5 years ago

Ok I've got this to atleast load a fragment shader at run time. Todo:

freesig commented 5 years ago

So there is some code that needs to be specific to each shader. This is the what I need for the example:

impl Iterator for FragInputIter {
    type Item = ShaderInterfaceDefEntry;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        None
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        let len = (0 - self.0) as usize;
        (len, Some(len))
    }
}
impl Iterator for FragOutputIter {
    type Item = ShaderInterfaceDefEntry;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        // Note that color fragment color entry will be determined
        // automatically by Vulkano.
        if self.0 == 0 {
            self.0 += 1;
            return Some(ShaderInterfaceDefEntry {
                location: 0..1,
                format: vk::Format::R32G32B32A32Sfloat,
                name: Some(Cow::Borrowed("f_color"))
            })
        }
        None
    }
    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        let len = (1 - self.0) as usize;
        (len, Some(len))
    }
}

We can't dynamically link types but we can probably generate the impl Iterator or maybe just get next() and size_hint() to call some dynamically linked functions so that we can update them to reflect the correct inputs and outputs to each shader at runtime.

freesig commented 5 years ago

The more I look into this the more I think we should probably just be using vulkan_shaders to compile a dynamic lib at runtime. As there is so many checks that it is already doing. But this isn't really possible because we can't have runtime types

freesig commented 5 years ago

I'm not sure what the best way to move forward here is.

  1. Use glslangValidator and don't allow changes to shader input / outputs at runtime. Easiest
  2. Use shaderc to get some feedback (I can't get this work yet, keeps segfaulting. It also makes compiling slow.) Hard
  3. Modify vulkano_shaders so that there is a way to just produce runtime function tokens and not the types. (We'd have to get this to land in vulkano) Medium
  4. Use vulkano_shaders and sort through the tokens and just use the ones we need. (It could be hard to find the tokens we need but we don't need to modify vulkano) Medium
mitchmindtree commented 5 years ago
  1. shaderc ...It also makes compiling slow

Oh true, have you run some tests that have confirmed this? I would have imagined it would be just as fast as glslangValidator at least, as the compilation process would get compiled into our exe rather than I/Oing out to glslangValidator. Both shaderc and glslangValidator have to type check etc before they can compile to SPIR-V anyways, so it shouldn't be much more expensive at all to get some runtime checks from either (not sure if you can just request type info from glslangValidator, but you should be able to from shaderc).

I think I'd be more inclined to go with shaderc as it seems like a more portable solution (a user doesn't have to also separately install glslangValidator, or we don't have to work out how to ship an instance compatible with each OS). That said, shaderc does need some deps like python and cmake, though we already need those for the vulkano-shaders crate to work anyways. I guess the other issue is that while shaderc is a more easily portable option for us, it's still not at all pure-rust (hence you're hitting segfaults... :mask: ). Ideally, something like rspirv will gain a GLSL->SPIR-V front-end for a pure-rust SPIR-V stack eventually, but I'm not sure what progress is like on this yet.

  1. Modify vulkano_shaders so that there is a way to just produce runtime function tokens and not the types. (We'd have to get this to land in vulkano)

There's actually already a bit of discussion about refactoring vulkano-shaders - vulkano-rs/vulkano#945 is worth a read. I think there was also another issue there about eventually moving to use a pure-rust solution like rspirv (not sure if it was that issue or another) so I'm not sure how easy it would be to land a run-time alternative in vulkano-shaders where there's already talk of breaking it up and switching from shaderc some day.

The most promising options in my mind are:

Hope I'm not making this more confusing haha. What are your thoughts?

mitchmindtree commented 5 years ago

Oh yeah this is the more general follow up on refactoring vulkano-shaders by the current maintainer vulkano-rs/vulkano#1039.

freesig commented 5 years ago

Whoops should have clarified. It makes the rust example slower to compile not the shader but I haven't tested this to confirm.

You're option 2. where we use shaderc to do compilation and all the checks. We could use vulkano-shaders' compile time checks as a reference on how to do this, however we'd obviously be applying these at run-time instead.

I think if we start writing test like what is in vulkan_shaders then we will just end up with vulkan_shaders with a runtime option.

I'm not sure I was being super clear above. The main issue is that we need to generate rust code at runtime if any inputs or outputs change to any shaders. vulkan_shaders does this. I was thinking of doing something similar to gantz where we compile a little rust crate at runtime with these functions in it. The problem is vulkan_shaders currently outputs the whole types and not just the impl blocks (which could be made just functions). I was thinking of just adding another function to vulkan_shaders that only outputs the tokens for these functions.

But you're right I don't think they would land it easily because they probably want to eventually modify vulkano so that this runtime codegen isn't required.

I sort of feel like the best option is just glslvalidator or shaderc without any checks.

mitchmindtree commented 5 years ago

The main issue is that we need to generate rust code at runtime if any inputs or outputs change to any shaders.

What do you mean by inputs or outputs here? It's not clear to me why we need to generate rust code at runtime. I'm fairly sure compiling rust at runtime shouldn't be necessary, but for a run-time solution to work we might need to implement some unsafe vulkano traits for a custom "run-time" checked version (where Results are returned) of some existing vulkano types that are normally compile-time checked.

freesig commented 5 years ago

Basically to do this:

    let vert_main = unsafe { vs2.graphics_entry_point(
        CStr::from_bytes_with_nul_unchecked(b"main\0"),
        VertInput,
        VertOutput,
        VertLayout(vk::ShaderStages { vertex: true, ..vk::ShaderStages::none() }),
        vk::pipeline::shader::GraphicsShaderType::Vertex
    ) };

You need VertInput etc. which looks like this:

// This structure will tell Vulkan how input entries of our vertex shader look like
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
struct VertInput;

unsafe impl ShaderInterfaceDef for VertInput {
    type Iter = VertInputIter;

    fn elements(&self) -> VertInputIter {
        VertInputIter(0)
    }
}

#[derive(Debug, Copy, Clone)]
struct VertInputIter(u16);

impl Iterator for VertInputIter {
    type Item = ShaderInterfaceDefEntry;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        // There are things to consider when giving out entries:
        // * There must be only one entry per one location, you can't have
        //   `color' and `position' entries both at 0..1 locations.  They also
        //   should not overlap.
        // * Format of each element must be no larger than 128 bits.
        if self.0 == 0 {
            self.0 += 1;
            return Some(ShaderInterfaceDefEntry {
                location: 0..1,
                format: vk::Format::R32G32Sfloat,
                name: Some(Cow::Borrowed("position"))
            })
        }
        None
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        // We must return exact number of entries left in iterator.
        let len = (1 - self.0) as usize;
        (len, Some(len))
    }
}

impl ExactSizeIterator for VertInputIter { }

Where the next() returns a ShaderInterfaceDefEntry which matches the uniforms or constants etc. Like it an input would need to match this:

layout(location = 0) in vec2 position;

we might need to implement some unsafe vulkano traits

That would be cool if we could avoid the above by doing this

mitchmindtree commented 5 years ago

It should be possible to change the VertInput type to store fields for whatever inputs might change at runtime and then update these fields each time the shader changes at runtime. Then the ShaderInterfaceDef and Iterator method implementations use these fields to construct the necessary ShaderInterfaceDefEntrys to be returned.

I think the only reason VertInput has no fields in this example is because it's a simple-as-possible demonstration of how to run-time load a SPIR-V shader.

freesig commented 5 years ago

This is the trait that the pipeline builder needs. Perhaps it could be implemented. We would still need a way to say what the inputs / outputs are. But this might be about to be done without codegen. I think it would just be a matter of making the iterator.

norru commented 5 years ago

Might need to use notify. (Do we care about this? Would someone be saving so often that the response should be < 2secs?)

I have used notify for my gfx + gl shader reloading system. It's pretty much instantaneous and I haven't observed any performance issues at all.

freesig commented 5 years ago

I've hit another road block with this. I'm trying to use the vulkan_shaders crate to parse the spirv and find the values for the interfaces between each shader. However everything in vulkan_shaders is private except the macro. I tried to pull out just what I needed to do this from the crate but pretty much ended up with the whole crate. Options:

What do people think is a good option here? In the meantime I'm going to search for other ways to solve this.

freesig commented 5 years ago

I just found spirv-reflect-rs which looks like it can do just this. I'm going to try and implement the example using this crate now.