vulkano
vulkano copied to clipboard
Compile, load and run GLSL at runtime
I can currently transpile GLSL to SPIR-V at runtime via:
let vs_spirv = glsl_to_spirv::compile(SHADER_VERTEX, ShaderType::Vertex)
.expect("failed to transpile the vertex shader");
let fs_spirv = glsl_to_spirv::compile(SHADER_FRAGMENT, ShaderType::Fragment)
.expect("failed to transpile the fragment shader");
Then, I edited vulkano-shaders
so I could do this:
let vs = vulkano_shaders::load_spirv(vs_spirv, &device);
let fs = vulkano_shaders::load_spirv(fs_spirv, &device);
The last thing I need, is to get the main_entry_point
to run the shader, if I understand this correctly. I am willing to implement it myself, so it complies with the current code. Would you mind guiding me?
Getting the entry point is the trickiest aspect, as it's at this moment that you must specify the input/output interfaces, the specialization constants and the pipeline layout.
For example if you call vertex_shader_entry_point
, the arguments you must pass are the name of the entry point (usually main
), then two objects that must implement the ShaderInterfaceDef
trait, then one object that must implement PipelineLayoutDesc
. Also, the S
parameter must implement SpecializationConstants
.
If you're looking for a quick solution, you can implement them in a dummy way (returning empty sets). Doing it properly would require a rework of vulkano_shaders
in order to separate the part that introspects the SPIR-V from the part that emits Rust code.
To elaborate a bit more on this.
I guess it should also be possible to write wrappers around ComputePipeline
and GraphicsPipeline
that automatically build ShaderModule
objects at runtime and reload the modules whenever their corresponding files are modified.
The reload can fail obviously if the source code of the shader doesn't compile, but also if the shader's interface no longer matches the pipeline layout or subpass passed used to build the pipeline.
You would essentially create either a ComputePipeline
or a RuntimeComputePipeline
for example.
The drawback of the RuntimeComputePipeline
is of course that it needs to compare the pipeline layout or subpass with values loaded at runtime, rather than optimizing out these comparisons at compile-time.
I wouldn't see it as a drawback. Which situations require you to recompile the shaders? It would be tools! As for tools, it does not matter if they stutter a bit when reloading the shader, so imho it's ok :)
I would love to see this feature in vulkano!
More plan:
- [ ] Merge
VertexShaderEntryPoint
,TessControlShaderEntryPoint
, etc. into a single struct namedShaderEntryPoint
. One of the members will need to contain details specific to each entry point. - [ ] Add a new trait
ShaderEntryPointLoad
that has a method namedload()
that returns aShaderEntryPoint
. The various template parameters ofShaderEntryPoint
are associated types ofShaderEntryPointLoad
.ShaderEntryPoint
itself implements the trait. - [ ] Add a trait
ShaderEntryPointRuntimeLoad
that requiresShaderEntryPointLoad
and that adds a method namedreload_if_changed()
. - [ ] Make
GraphicsPipelineParams
andComputePipelineParams
generic over the entry point loader. In other words, instead of having a member of typeShaderEntryPoint<...>
, have it be of typeVs
whereVs
must implementShaderEntryPointLoad
. - [ ] Add
AutoReloadGraphicsPipeline
andAutoReloadComputePipeline
that wrap aroundGraphicsPipeline
andComputePipeline
. - [ ] Add a struct named
SpirvFileShaderLoader
that can be created from aPath
and that implementsShaderEntryPointRuntimeLoad
. The struct will check whether the file has changed from time to time, and if so re-parse it. - [ ] Add a feature to vulkano named
glsl
, which corresponds to runtime compilation of glsl. Vulkano depends onglsl-to-spirv
if that feature is enabled, and wraps around glsl-to-spirv's capabilities. - [ ] Move the parsing and analyzing capabilities, plus the glsl compilation, from
vulkano-shaders
tovulkano
. Make the SPIR-V analyzer return enums. Makevulkano-shaders
depend onvulkano
to parse and analyze its SPIR-V files and to compile GLSL. The only role ofvulkano-shaders
will be to generate Rust code at compile-time using the output of vulkano's SPIR-V analyzer. - [ ] Add another struct named
GlslFileShaderLoader
behind the featureglsl
. The struct does the same asSpirvFileShaderLoader
but compiles the file from GLSL first.
The remaining question is: should the shaders be reloaded automatically, or does the user need to manually call a method? Another small problem is that if you record a command buffer that uses a pipeline, it can't be reloaded (unless you rebuild the command buffer).
Merge VertexShaderEntryPoint, TessControlShaderEntryPoint, etc. into a single struct named ShaderEntryPoint. One of the members will need to contain details specific to each entry point.
How about something like this? Am I heading the right direction?
@Limeth Yes
I suggest adding getters in ShaderEntryPoint
, for example fn input(&self) -> Option<&I>
or fn geometry_execution_mode(&self) -> Option<GeometryShaderExecutionMode>
.
What exactly should happen, when a file change is detected by SpirvFileShaderLoader
?
The reload_if_changed()
method implemented on SpirvFileShaderLoader
should check for file changes and if so reload the file, reparse it, and build a new ShaderEntryPoint
.
So SpirvFileShaderLoader
itself will have to insert it into GraphicsPipeline
or GraphicsPipelineParams
, meaning that it will have to hold a reference to them, correct?
No, the idea is this:
pub struct GraphicsPipelineParams<Vs: ShaderEntryPointLoad> {
pub vertex_shader: Vs,
...
}
// GraphicsPipeline doesn't get any change except in its constructor
pub struct RuntimeGraphicsPipeline<...> {
vs_loader: Box<ShaderEntryPointRuntimeLoad>,
fs_loader: ...,
...
pipeline: GraphicsPipeline<...>,
}
impl RuntimeGraphicsPipeline<...> {
pub fn new<Vs>(device: &Arc<Device>, params: GraphicsPipelineParams<Vs>)
where Vs: ShaderEntryPointRuntimeLoad
{
...
}
}
It's only RuntimeGraphicsPipeline
that knows about stuff related to reloading.
You will probably face ownership issues with the fact that you'll need the ShaderEntryPointLoad
for both GraphicsPipeline::new()
and to store in RuntimeGraphicsPipeline
.
To solve that you can add impl<'a, T> ShaderEntryPointLoad for &'a T where T: 'a + ShaderEntryPointLoad { ... }
and pass the loader by reference instead of by value within RuntimeGraphicsPipeline::new()
.
Would you mind giving me some feedback? I am not too sure about all this. :D
Hmm you have a different approach than what I was thinking of.
In my mind, the module
member of ShaderEntryPoint
would become a Cow
, so that the ShaderEntryPointLoad
trait can choose whether to return a module+an entry point (by returning an entry point that has ownership of its module), or just an entry point of an existing module.
With your approach you're supposing that you're assuming that the implementation of ShaderEntryPointLoad
holds a ShaderModule
by reference. This is problematic because if, for example, the RuntimeComputePipeline
struct needs to hold the implementation ShaderEntryPointLoad
, then that means the RuntimeComputePipeline
will also need a lifetime and the user won't be able to manipulate a standalone Arc<RuntimeComputePipeline>
.
Note that any change you make to vulkano-shaders
will likely heavily conflict with some changes I'm going to make in the incoming
branch. Please tell me if you start anything so that we get in sync.
Has there been any update on this? I am currently trying to load glsl shaders from a file on runtime as well but didn't find a way till now.
Is this issue still open? I have recently started adopting Vulkan on Rust. Currently trying to evaluate which wrapper to use, between vulkano
, ash
and gfx-ll
(Vulkan backend). Started with Vulkano because it's safer and easier to get off the ground but I found this brick wall on compiling and loading shaders at runtime - which is a non-negotiable requirement.
Before I plan to switch to one of the (more painful) alternatives, is this going to be supported any time soon? I need to know sooner than later as switching later would just be even more painful.
What does @rukai say/suggest? Does https://github.com/vulkano-rs/vulkano/blob/master/examples/src/bin/runtime-shader/main.rs solve the problem?
@norru you might be interested in @freesig's shade_runner crate (you can find a nannou hot-loading example that uses it here). It's still brand new, but at least demonstrates how this is possible with current vulkano.
Vulkano already allows for run-time SPIR-V shader loading as you pointed out with that example - the real trick is finding the nicest way of going from GLSL -> SPIR-V.
To be honest, I totally understand why vulkano or any other vulkan lib would feel it is not their responsibility as, in theory, the SPIR-V you submit for your shader component of your vulkan pipeline could be from anywhere (e.g. RLSL, nim, haskell, any other language that might compile to SPIR-V in the future).
In the case of shade_runner
, shaderc
is used to compile the GLSL at runtime and spirv_reflect
is used to produce the information necessary for integration with the vulkano pipeline.
In the future it might be nice for vulkano-shaders
to provide a runtime compilation option alongside its existing compile-time option, but it looks like there's some inclination to wait for a pure-rust GLSL->SPIR-V front-end to appear before that goes ahead #910 #1039.
@mitchmindtree Thanks for the suggestion!
Not having to depend on GLSL compiling at runtime is kind of the point of Vulkan in itself, but I'm ok with feeding it SPIR-V code instead if everything else fails.
shaderc
+ spirv_reflect
or even glslValidator
+ spirv_reflect
sounds reasonable. Also rspirv
https://github.com/google/rspirv sounds really promising, I'll have a look at how difficult it is to implement a working combination.
I also need to be in control on when and how a shader gets reloaded so the "autoreload" stuff needs to be skippable.
shade_runner is basically just shaderc + spirv_reflect
. Still some more work to make it complete though. I'll put some more examples up when I get a chance but you can use the load()
and parse()
functions directly if you don't want the auto reloaded.
Not having to depend on GLSL compiling at runtime is kind of the point of Vulkan in itself, but I'm ok with feeding it SPIR-V code instead if everything else fails.
What is your desired workflow here?
I have got two use cases. Something of the ilk of:
myapp --shader_folder=../my_shaders
myappgui --shader_folder=../my_shaders
myapp
is a Rust console binary which is built once and run multiple times (possibly concurrently) on different ../my_shaders_X folders.
myappgui
is an application which, after loading the shaders dynamically once, reacts by some kind of changes in the ../my_shaders
folder, including both shader code/configuration and data.
Shader compilation at compile time is not required in either case.
This is a very common use case for game engines (editor/tools and runtime).
If the shaders in ../my_shaders
are .glsl
then you could do both use cases with shade_runner.
Alternatively for use case 1 you could store the shaders as compiled spirv and then just use shade_runner
to parse them and generate the Entry
point.
There's still a little more work for shade_runner to be complete but I'm actively working on it. Feel free to open an issue if something isn't working.
Ultimately it would be nice to have this built into vulkano. Or atleast have vulkano be able to compile and generate the entry points at run time. Hopefully shade_runner
can show one way of achieving this.
@freesig For the time being, GLSL is fine. Looking forward to seeing the upcoming examples.
If it helps, my priority is on compute shaders. It would be great to see a variant of https://github.com/vulkano-rs/vulkano-examples/blob/master/src/bin/basic-compute-shader.rs with the compile-time shaders replaced by your runtime stuff.
Would is be desireable to use Naga for shader compilation in the future? It seems like a pure rust solution usable at compile or runtime would resolve some headaches. And dropping the FFI dependency on shaderc would also ease project setup and portability.
Would is be desireable to use Naga for shader compilation in the future? It seems like a pure rust solution usable at compile or runtime would resolve some headaches. And dropping the FFI dependency on shaderc would also ease project setup and portability.
It certainly would be preferred over shaderc, but how production-ready is it?
Has glslc being considered in the meantime? I'm 99% sure its bundled with the Vulkan SDK so binary versions should be available on everyone's computer
Would is be desireable to use Naga for shader compilation in the future? It seems like a pure rust solution usable at compile or runtime would resolve some headaches. And dropping the FFI dependency on shaderc would also ease project setup and portability.
It certainly would be preferred over shaderc, but how production-ready is it?
I am not certain on that. It has not yet reached a 1.0 major version, but that doesn't necessarily mean it's not production ready. The readme lists glsl as secondary support target, with support only for glsl versions 440+ and only vulkan semantics. The latter should be no problem. I don't now how much the former is, but I would guess not a big one. Overall, I couldn't find any statement regarding production readiness one way or the other.
Has glslc being considered in the meantime? I'm 99% sure its bundled with the Vulkan SDK so binary versions should be available on everyone's computer
That's what we use currently.