diff --git a/CHANGELOG_VULKANO.md b/CHANGELOG_VULKANO.md index ec7861f8..67abf86c 100644 --- a/CHANGELOG_VULKANO.md +++ b/CHANGELOG_VULKANO.md @@ -1,5 +1,8 @@ # Unreleased +- Add support for `#include "..."` and `#include <...>` directives within source + files. + # Version 0.11.1 (2018-11-16) - Expose `CopyImageError` and `DrawIndexedIndirectError`. diff --git a/examples/src/bin/shader-include/main.rs b/examples/src/bin/shader-include/main.rs new file mode 100644 index 00000000..92224a9f --- /dev/null +++ b/examples/src/bin/shader-include/main.rs @@ -0,0 +1,91 @@ +// Copyright (c) 2017 The vulkano developers +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , +// at your option. All files in the project carrying such +// notice may not be copied, modified, or distributed except +// according to those terms. + +// This example demonstrates how to use the standard and relative include directives within +// shader source code. The boilerplate is taken from the "basic-compute-shader.rs" example, where +// most of the boilerplate is explained. + +extern crate vulkano; +extern crate vulkano_shaders; + +use vulkano::buffer::{BufferUsage, CpuAccessibleBuffer}; +use vulkano::command_buffer::AutoCommandBufferBuilder; +use vulkano::descriptor::descriptor_set::PersistentDescriptorSet; +use vulkano::device::{Device, DeviceExtensions}; +use vulkano::instance::{Instance, InstanceExtensions, PhysicalDevice}; +use vulkano::pipeline::ComputePipeline; +use vulkano::sync::GpuFuture; +use vulkano::sync; + +use std::sync::Arc; + +fn main() { + let instance = Instance::new(None, &InstanceExtensions::none(), None).unwrap(); + let physical = PhysicalDevice::enumerate(&instance).next().unwrap(); + let queue_family = physical.queue_families().find(|&q| q.supports_compute()).unwrap(); + let (device, mut queues) = Device::new(physical, physical.supported_features(), + &DeviceExtensions::none(), [(queue_family, 0.5)].iter().cloned()).unwrap(); + let queue = queues.next().unwrap(); + + println!("Device initialized"); + + let pipeline = Arc::new({ + mod cs { + vulkano_shaders::shader!{ + ty: "compute", + // We declare what directories to search for when using the `#include <...>` + // syntax. Specified directories have descending priorities based on their order. + include: [ "examples/src/bin/shader-include/standard-shaders" ], + src: " +#version 450 +// Substitutes this line with the contents of the file `common.glsl` found in one of the standard +// `include` directories specified above. +// Note, that relative inclusion (`#include \"...\"`), although it falls back to standard +// inclusion, should not be used for **embedded** shader source, as it may be misleading and/or +// confusing. +#include + +layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in; + +layout(set = 0, binding = 0) buffer Data { + uint data[]; +} data; + +void main() { + uint idx = gl_GlobalInvocationID.x; + data.data[idx] = multiply_by_12(data.data[idx]); +}" + } + } + let shader = cs::Shader::load(device.clone()).unwrap(); + ComputePipeline::new(device.clone(), &shader.main_entry_point(), &()).unwrap() + }); + + let data_buffer = { + let data_iter = (0 .. 65536u32).map(|n| n); + CpuAccessibleBuffer::from_iter(device.clone(), BufferUsage::all(), data_iter).unwrap() + }; + let set = Arc::new(PersistentDescriptorSet::start(pipeline.clone(), 0) + .add_buffer(data_buffer.clone()).unwrap() + .build().unwrap() + ); + let command_buffer = AutoCommandBufferBuilder::primary_one_time_submit(device.clone(), queue.family()).unwrap() + .dispatch([1024, 1, 1], pipeline.clone(), set.clone(), ()).unwrap() + .build().unwrap(); + let future = sync::now(device.clone()) + .then_execute(queue.clone(), command_buffer).unwrap() + .then_signal_fence_and_flush().unwrap(); + + future.wait(None).unwrap(); + + let data_buffer_content = data_buffer.read().unwrap(); + for n in 0 .. 65536u32 { + assert_eq!(data_buffer_content[n as usize], n * 12); + } +} diff --git a/examples/src/bin/shader-include/relative-shaders/relative-include.glsl b/examples/src/bin/shader-include/relative-shaders/relative-include.glsl new file mode 100644 index 00000000..9704f0f9 --- /dev/null +++ b/examples/src/bin/shader-include/relative-shaders/relative-include.glsl @@ -0,0 +1,3 @@ +uint multiply_by_2(in uint arg) { + return 2 * arg; +} diff --git a/examples/src/bin/shader-include/standard-shaders/common.glsl b/examples/src/bin/shader-include/standard-shaders/common.glsl new file mode 100644 index 00000000..06898f54 --- /dev/null +++ b/examples/src/bin/shader-include/standard-shaders/common.glsl @@ -0,0 +1,10 @@ +// Include the file `standard-include.glsl` from one of the standard +// directories. +#include +// Try to locate the file `relative-include.glsl` in a sibling directory +// and include it. +#include "../relative-shaders/relative-include.glsl" + +uint multiply_by_12(in uint arg) { + return 2 * multiply_by_3(multiply_by_2(arg)); +} diff --git a/examples/src/bin/shader-include/standard-shaders/standard-include.glsl b/examples/src/bin/shader-include/standard-shaders/standard-include.glsl new file mode 100644 index 00000000..e87583dc --- /dev/null +++ b/examples/src/bin/shader-include/standard-shaders/standard-include.glsl @@ -0,0 +1,3 @@ +uint multiply_by_3(in uint arg) { + return 3 * arg; +} diff --git a/vulkano-shaders/src/codegen.rs b/vulkano-shaders/src/codegen.rs index b9149c56..44844c93 100644 --- a/vulkano-shaders/src/codegen.rs +++ b/vulkano-shaders/src/codegen.rs @@ -8,12 +8,13 @@ // according to those terms. use std::io::Error as IoError; +use std::path::Path; use syn::Ident; use proc_macro2::{Span, TokenStream}; use shaderc::{Compiler, CompileOptions}; -pub use shaderc::{CompilationArtifact, ShaderKind}; +pub use shaderc::{CompilationArtifact, ShaderKind, IncludeType, ResolvedInclude}; pub use parse::ParseError; use parse::Instruction; @@ -24,13 +25,130 @@ use entry_point; use structs; use descriptor_sets; use spec_consts; +use read_file_to_string; -pub fn compile(code: &str, ty: ShaderKind) -> Result { +fn include_callback(requested_source_path_raw: &str, directive_type: IncludeType, + contained_within_path_raw: &str, recursion_depth: usize, + include_directories: &[String], root_source_has_path: bool) -> Result { + let file_to_include = match directive_type { + IncludeType::Relative => { + let requested_source_path = Path::new(requested_source_path_raw); + + // Is embedded current shader source embedded within a rust macro? + // If so, abort. + if !root_source_has_path && recursion_depth == 1 { + let requested_source_name = requested_source_path.file_name() + .expect("Could not get the name of the requested source file.") + .to_string_lossy(); + let requested_source_directory = requested_source_path.parent() + .expect("Could not get the directory of the requested source file.") + .to_string_lossy(); + + return Err(format!("Usage of relative paths in imports in embedded GLSL is not \ + allowed, try using `#include <{}>` and adding the directory \ + `{}` to the `include` array in your `shader!` macro call \ + instead.", + requested_source_name, requested_source_directory)); + } + + let parent_of_current_source = Path::new(contained_within_path_raw).parent() + .unwrap_or_else(|| panic!("The file `{}` does not reside in a directory. This is \ + an implementation error.", + contained_within_path_raw)); + let resolved_requested_source_path = parent_of_current_source.join(requested_source_path); + + if !resolved_requested_source_path.is_file() { + return Err(format!("Invalid inclusion path `{}`, the path does not point to a file.", + requested_source_path_raw)); + } + + resolved_requested_source_path + }, + IncludeType::Standard => { + let requested_source_path = Path::new(requested_source_path_raw); + + if requested_source_path.is_absolute() { + // This message is printed either when using a missing file with an absolute path + // in the relative include directive or when using absolute paths in a standard + // include directive. + return Err(format!("No such file found, as specified by the absolute path. \ + Keep in mind, that absolute paths cannot be used with \ + inclusion from standard directories (`#include <...>`), try \ + using `#include \"...\"` instead. Requested path: {}", + requested_source_path_raw)); + } + + let mut found_requested_source_path = None; + + for include_directory in include_directories { + let include_directory_path = Path::new(include_directory).canonicalize() + .unwrap_or_else(|_| panic!("Invalid standard shader inclusion directory `{}`.", + include_directory)); + let resolved_requested_source_path_rel = include_directory_path + .join(requested_source_path); + let resolved_requested_source_path = resolved_requested_source_path_rel + .canonicalize() + .map_err(|_| format!("Invalid inclusion path `{}`.", + resolved_requested_source_path_rel.to_string_lossy()))?; + + if !resolved_requested_source_path.starts_with(include_directory_path) { + return Err(format!("Cannot use `..` with inclusion from standard directories \ + (`#include <...>`), try using `#include \"...\"` instead. \ + Requested path: {}", requested_source_path.to_string_lossy())); + } + + if resolved_requested_source_path.is_file() { + found_requested_source_path = Some(resolved_requested_source_path); + break; + } + } + + if found_requested_source_path.is_none() { + return Err(format!("Could not include the file `{}` from any include directories.", + requested_source_path_raw)); + } + + found_requested_source_path.unwrap() + }, + }; + + let canonical_file_to_include = file_to_include.canonicalize() + .unwrap_or_else(|_| file_to_include); + let canonical_file_to_include_string = canonical_file_to_include.to_str() + .expect("Could not stringify the file to be included. Make sure the path consists of \ + valid unicode characters.") + .to_string(); + let content = read_file_to_string(canonical_file_to_include.as_path()) + .map_err(|_| format!("Could not read the contents of file `{}` to be included in the \ + shader source.", + &canonical_file_to_include_string))?; + + Ok(ResolvedInclude { + resolved_name: canonical_file_to_include_string, + content, + }) +} + +pub fn compile(path: Option, code: &str, ty: ShaderKind, include_directories: &[String]) -> Result { let mut compiler = Compiler::new().ok_or("failed to create GLSL compiler")?; - let compile_options = CompileOptions::new().ok_or("failed to initialize compile option")?; + let mut compile_options = CompileOptions::new() + .ok_or("failed to initialize compile option")?; + let root_source_path = if let &Some(ref path) = &path { + path + } else { + // An arbitrary placeholder file name for embedded shaders + "shader.glsl" + }; + + // Specify file resolution callback for the `#include` directive + compile_options.set_include_callback(|requested_source_path, directive_type, + contained_within_path, recursion_depth| { + include_callback(requested_source_path, directive_type, contained_within_path, + recursion_depth, include_directories, path.is_some()) + }); let content = compiler - .compile_into_spirv(&code, ty, "shader.glsl", "main", Some(&compile_options)) + .compile_into_spirv(&code, ty, root_source_path, "main", Some(&compile_options)) .map_err(|e| e.to_string())?; Ok(content) @@ -265,7 +383,7 @@ mod tests { // byte, but in a rust [[f32;3];2], the second element starts on the // 12th byte. Since we can't generate code for these types, we should // create an error instead of generating incorrect code. - let comp = compile(" + let comp = compile(None, " #version 450 struct MyStruct { vec3 vs[2]; @@ -274,14 +392,14 @@ mod tests { MyStruct s; }; void main() {} - ", ShaderKind::Vertex).unwrap(); + ", ShaderKind::Vertex, &[]).unwrap(); let doc = parse::parse_spirv(comp.as_binary()).unwrap(); let res = std::panic::catch_unwind(|| structs::write_structs(&doc)); assert!(res.is_err()); } #[test] fn test_trivial_alignment() { - let comp = compile(" + let comp = compile(None, " #version 450 struct MyStruct { vec4 vs[2]; @@ -290,7 +408,7 @@ mod tests { MyStruct s; }; void main() {} - ", ShaderKind::Vertex).unwrap(); + ", ShaderKind::Vertex, &[]).unwrap(); let doc = parse::parse_spirv(comp.as_binary()).unwrap(); structs::write_structs(&doc); } @@ -298,7 +416,7 @@ mod tests { fn test_wrap_alignment() { // This is a workaround suggested in the case of test_bad_alignment, // so we should make sure it works. - let comp = compile(" + let comp = compile(None, " #version 450 struct Vec3Wrap { vec3 v; @@ -310,7 +428,7 @@ mod tests { MyStruct s; }; void main() {} - ", ShaderKind::Vertex).unwrap(); + ", ShaderKind::Vertex, &[]).unwrap(); let doc = parse::parse_spirv(comp.as_binary()).unwrap(); structs::write_structs(&doc); } diff --git a/vulkano-shaders/src/lib.rs b/vulkano-shaders/src/lib.rs index 807b7f44..63c462e4 100644 --- a/vulkano-shaders/src/lib.rs +++ b/vulkano-shaders/src/lib.rs @@ -131,6 +131,14 @@ //! Provides the path to the GLSL source to be compiled, relative to `Cargo.toml`. //! Cannot be used in conjunction with the `src` field. //! +//! ## `include: ["...", "...", ..., "..."]` +//! +//! Specifies the standard include directories to be searched through when using the +//! `#include <...>` directive within a shader source. +//! If `path` was specified, relative paths can also be used (`#include "..."`), without the need +//! to specify one or more standard include directories. Relative paths are relative to the +//! directory, which contains the source file the `#include "..."` directive is declared in. +//! //! ## `dump: true` //! //! The crate fails to compile but prints the generated rust code to stdout. @@ -158,7 +166,7 @@ use std::env; use std::fs::File; -use std::io::Read; +use std::io::{Read, Result as IoResult}; use std::path::Path; use syn::parse::{Parse, ParseStream, Result}; @@ -183,6 +191,7 @@ enum SourceKind { struct MacroInput { shader_kind: ShaderKind, source_kind: SourceKind, + include_directories: Vec, dump: bool, } @@ -191,6 +200,7 @@ impl Parse for MacroInput { let mut dump = None; let mut shader_kind = None; let mut source_kind = None; + let mut include_directories = Vec::new(); while !input.is_empty() { let name: Ident = input.parse()?; @@ -230,6 +240,20 @@ impl Parse for MacroInput { let path: LitStr = input.parse()?; source_kind = Some(SourceKind::Path(path.value())); } + "include" => { + let in_brackets; + bracketed!(in_brackets in input); + + while !in_brackets.is_empty() { + let path: LitStr = in_brackets.parse()?; + + include_directories.push(path.value()); + + if !in_brackets.is_empty() { + in_brackets.parse::()?; + } + } + } "dump" => { if dump.is_some() { panic!("Only one `dump` can be defined") @@ -257,32 +281,36 @@ impl Parse for MacroInput { let dump = dump.unwrap_or(false); - Ok(MacroInput { shader_kind, source_kind, dump }) + Ok(MacroInput { shader_kind, source_kind, include_directories, dump }) } } +pub(self) fn read_file_to_string(full_path: &Path) -> IoResult { + let mut buf = String::new(); + File::open(full_path) + .and_then(|mut file| file.read_to_string(&mut buf))?; + Ok(buf) +} + #[proc_macro] pub fn shader(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as MacroInput); - let source_code = match input.source_kind { - SourceKind::Src(source) => source, - SourceKind::Path(path) => { + let (path, source_code) = match input.source_kind { + SourceKind::Src(source) => (None, source), + SourceKind::Path(path) => (Some(path.clone()), { let root = env::var("CARGO_MANIFEST_DIR").unwrap_or(".".into()); let full_path = Path::new(&root).join(&path); if full_path.is_file() { - let mut buf = String::new(); - File::open(full_path) - .and_then(|mut file| file.read_to_string(&mut buf)) - .expect(&format!("Error reading source from {:?}", path)); - buf + read_file_to_string(&full_path) + .expect(&format!("Error reading source from {:?}", path)) } else { panic!("File {:?} was not found ; note that the path must be relative to your Cargo.toml", path); } - } + }) }; - let content = codegen::compile(&source_code, input.shader_kind).unwrap(); + let content = codegen::compile(path, &source_code, input.shader_kind, &input.include_directories).unwrap(); codegen::reflect("Shader", content.as_binary(), input.dump).unwrap().into() }