1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
//! This crate provides macros for writing simple vulkan compute shader tests
//! using the tomaka/[`vulkano`] library.
//!
//! ## About
//!
//! A core problem of developing shaders is the rather difficult environment in which they are
//! executed. Even simple things can go wrong and cost the developer a lot of time to fix.
//! This crate aims at providing a simple-to-use environment for writing vulkan compute shader tests.
//! It uses the [`vulkano`] rust-vulkan bindings end exports macros for a fast implementation of tests.
//! These macros mostly generate [`vulkano`] boilerplate instantiation code. The interface to the
//! shader are CPU accessible buffers which you can read and write at will and a function for
//! executing the shader code and waiting for the result.
//!
//! ## Import (Usage in integration test modules)
//!
//! Due to the reexport of utility function from the [`vulkano`] crate (which you don't need to access,
//! unless you want to) you need to use the following crates in your test module header:
//!
//! ```
//! #[macro_use]
//! extern crate vulkano;
//! #[macro_use]
//! extern crate vulkanology;
//! #
//! # fn main() {
//! # instance!();
//! # }
//! ```
//!
//! For basic usage of the library you can refer to the doc-tests and `tests/shaders/example.comp`.
//! For a working example of a fairly elaborate shader test please refer to: `tests/random.rs`
//! and `tests/shaders/random.comp`.
//!
//! ## Building GLSL shaders
//!
//! This utility pack is built around the [`vulkano`] library, which also provides `vulkano-shaders`,
//! a library which compiles GLSL shaders into Rust interface modules.
//! For examples on how to build shaders with `vulkano-shaders` see `build.rs` and [this].
//! 
//! ## Composite shader tests
//!
//! `vulkanology` also provides some build utilities for working with segmented shaders.
//! `src/build_utils.rs` contains working examples on how to use these utilities in your `build.rs`.
//!
//! [this]: https://github.com/tomaka/vulkano/blob/master/examples/build.rs
//! [`vulkano`]: https://github.com/tomaka/vulkano
//!
#![deny(missing_docs)]
#![feature(macro_reexport)]

pub mod build_utils;

/// Creates a [`vulkano`] [`Instance`]. Does not enable any instance extensions.
///
/// # Panics
///
/// Panics if the instance loading procedure fails.
///
/// # Example
///
/// ```
/// # // These tests du not require vulkano-macros,
/// # // therefore the `macro_use` will be omitted here, unless required.
/// # extern crate vulkano;
/// # #[macro_use]
/// # extern crate vulkanology;
/// #
/// # #[allow(unused_variables)]
/// # fn main() {
/// // Simply invoke the macro and assign the result.
/// let instance = instance!();
/// # }
/// ```
///
/// [`vulkano`]: https://github.com/tomaka/vulkano
/// [`Instance`]: https://docs.rs/vulkano/0.3.1/vulkano/instance/struct.Instance.html
///
#[macro_export]
macro_rules! instance {
    () => ({
        use vulkano::instance::{Instance, InstanceExtensions};
        let extensions = &InstanceExtensions::none();
        Instance::new(None, extensions, None).expect("Failed to initialize vulkano.")
    })
}

/// This macro generates code for loading a [`PhysicalDevice`]. It takes
/// the instance variable name and an optional list of features which the device
/// should support. All available features are defined [here].
///
/// # Panics
///
/// Panics if no device matching the requirements has been found.
///
/// # Example
///
/// ```
/// # extern crate vulkano;
/// # #[macro_use]
/// # extern crate vulkanology;
/// #
/// # #[allow(unused_variables)]
/// # fn main() {
/// // First initialize a `vulkano::Instance`.
/// let instance = instance!();
///
/// // Select the first physical device which supports compute shaders.
/// {
///     // With no explicitly required features:
///     let physical_device = physical_device!(instance);
/// }
/// {
///     // With some features:
///     let physical_device = physical_device!(
///         instance,
///         robust_buffer_access,
///         full_draw_index_uint32);
/// }
/// # }
/// ```
///
/// [here]: https://github.com/tomaka/vulkano/blob/master/vulkano/src/features.rs
/// [`PhysicalDevice`]: https://docs.rs/vulkano/0.3.1/vulkano/instance/struct.PhysicalDevice.html
///
#[macro_export]
macro_rules! physical_device {
    // Rule for selecting a device with specific features.
    ($instance:ident, $($feature:ident),+) => ({
        use vulkano::instance::{PhysicalDevice};
        PhysicalDevice::enumerate(&$instance).find(|p| {
            let supported_features = p.supported_features();
            true $( && supported_features.$feature )*
        }).expect("No physical devices are available.")
    });

    // Rule for selecting the first available physical
    // device when no features are required.
    ($instance:ident) => ({
        use vulkano::instance::{PhysicalDevice};
        PhysicalDevice::enumerate(&$instance).next()
            .expect("No physical devices are available.")
    })
}

/// Creates a [`Device`] and a [`Queue`] for compute operations.
///
/// # Panics
///
/// Panics if no compute-compatible queue has been found, or the
/// device could not be initialized.
///
/// # Example
///
/// ```
/// # extern crate vulkano;
/// # #[macro_use]
/// # extern crate vulkanology;
/// #
/// # #[allow(unused_variables)]
/// # fn main() {
/// let instance = instance!();
/// let physical_device = physical_device!(instance);
/// let (device, queue) = device_and_queue!(physical_device);
/// # }
/// ```
///
/// [`Device`]: https://docs.rs/vulkano/0.3.1/vulkano/device/struct.Device.html
/// [`Queue`]: https://docs.rs/vulkano/0.3.1/vulkano/device/struct.Queue.html
///
#[macro_export]
macro_rules! device_and_queue {
    ($physical_device:ident) => ({
        use vulkano::device::{Device, DeviceExtensions};

        // Select a queue family which supports compute operations.
        let mut queue_families = $physical_device.queue_families();
        let queue_family = queue_families.find(|q| q.supports_compute())
            .expect("Couldn't find a compute queue family.");

        // Initialize a device and a queue.
        let device_extensions = DeviceExtensions::none();
        let (device, mut queues) = Device::new(&$physical_device,
                                               &$physical_device.supported_features(),
                                               &device_extensions,
                                               [(queue_family, 0.5)].iter().cloned())
            .expect("Failed to create device.");

        // We only requested one queue, so `queues` is an array with only one element.
        (device, queues.next().unwrap())
    })
}

/// Creates a new uninitialized [buffer] of type `$buf_type` of length `$buf_len`.
///
/// # Panics
///
/// If the array fails to be initialized.
///
/// # Examples
///
/// ```
/// # extern crate vulkano;
/// # #[macro_use]
/// # extern crate vulkanology;
/// #
/// # #[allow(unused_variables)]
/// # fn main() {
/// let instance = instance!();
/// let physical_device = physical_device!(instance);
/// let (ref device, ref queue) = device_and_queue!(physical_device);
///
/// // Initialize a buffer.
/// let buffer = cpu_array_buffer!(device, queue, u32, 13*31);
/// # }
/// ```
///
/// [buffer]: https://docs.rs/vulkano/0.3.1/vulkano/buffer/cpu_access/struct.CpuAccessibleBuffer.html
///
#[macro_export]
macro_rules! cpu_array_buffer {
    ($device:ident, $queue:ident, $buf_type:ty, $buf_len:expr) => ({
        use vulkano::buffer::{BufferUsage, CpuAccessibleBuffer};
        unsafe {
            CpuAccessibleBuffer::<[$buf_type]>::uninitialized_array(
                $device,
                $buf_len,
                &BufferUsage::all(),
                Some($queue.family()))
                .expect("Failed to create a cpu accessible buffer.")
        }
    })
}

/// This macro is the core of the shader-testing framework.
/// It generates code for initializing the vulkano environment,
/// it allocates [`CpuAccessibleBuffer`]s, it compiles the shader,
/// it sets up a [`ComputePipeline`] and provides a function
/// for executing the shader.
///
/// # Panics
///
/// * If the `instance`, `physical_device`, `device` or `queue` cannot be selected/initialized.
/// * If the buffers cannot be initialized.
/// * If the shader cannot be loaded.
/// * If the pipeline cannot be created.
///
/// # Example
///
/// ```
/// # #[macro_use]
/// # extern crate vulkano;
/// # #[macro_use]
/// # extern crate vulkanology;
/// # extern crate rand;
/// #
/// # #[allow(unused_variables)]
/// # fn main() {
/// // The total number of invocations of your shader is defined in two places:
/// //      - The workgroup_count, which is defined in the pipeline macro.
/// //      - The workgroup_size which is defined in the shader program header.
///
/// // Here we compute the total number of invocations. The workgroup size is `8x8x1`,
/// // and the workgroup count will be `100x100x1`.
/// let total_num_invocations = (8 * 8) * (100 * 100);
///
/// // I. Invoke the `pipeline!` macro.
/// // The macro parameters are:
/// //    1. The path to the shader program, relative to the crate root.
/// //        `shader_path: "path/to/shader/program.comp"`
/// //    2. A three-dimensional array defining the workgroup count:
/// //        `workgroup_count: [1, 2, 3],`
/// //    3. The buffers that your test shader uses:
/// //        `buffers: { input_data: [u32;4], some_buffer: [Dennis;42] },`
/// //    4. The name of the shader execution:
/// //        `execution_command: run_example_shader_function_name`
/// pipeline!{
///     shader_path: "tests/shaders/example.comp",
///     workgroup_count: [100, 100, 1],
///     buffers: {
///        data: [u32; total_num_invocations],
///        result: [u32; total_num_invocations]
///     },
///     execution_command: execute_shader
/// }
///
/// // II. Fill your buffers with input data. The buffers are bound to the
/// //      names given in the `pipeline!` macro.
/// {
///     use std::time::Duration;
///     use rand::random;
///
///     use vulkano::buffer::cpu_access::WriteLock;
///     let mut mapping: WriteLock<[u32]> = data.write(Duration::new(1, 0)).unwrap();
///
///     for item in mapping.iter_mut() {
///         *item = random::<u32>();
///     }
/// }
///
/// // III. Execute the shader.
/// //    `run_example_shader_function_name();`
/// execute_shader();
///
/// // IV. Assert validity of the results.
/// //    `assert!(datainbuffersisvalid())`
/// {
///     use std::time::Duration;
///     use vulkano::buffer::cpu_access::ReadLock;
///     let input: ReadLock<[u32]> = data.read(Duration::new(1, 0)).unwrap();
///     let output: ReadLock<[u32]> = result.read(Duration::new(1, 0)).unwrap();
///     let zipped = input.iter().zip(output.iter());
///
///     for (invocation_uid, (item_in, item_out)) in zipped.enumerate() {
///         assert_eq!(*item_out, (*item_in).wrapping_mul(invocation_uid as u32));
///     }
/// }
/// # }
/// ```
///
/// [`CpuAccessibleBuffer`]:
///     https://docs.rs/vulkano/0.3.1/vulkano/buffer/cpu_access/struct.CpuAccessibleBuffer.html
/// [`ComputePipeline`]: https://docs.rs/vulkano/0.3.1/vulkano/pipeline/struct.ComputePipeline.html
///
#[macro_export]
macro_rules! pipeline {
    {
        shader_path: $shader_path:expr,
        workgroup_count: $workgroup_count:expr,
        buffers: { $( $buf_ident:ident : [$buf_type:ty;$buf_len:expr] ),* },
        execution_command: $exec_cmd:ident
    } => {
        use vulkano::command_buffer::PrimaryCommandBufferBuilder;
        use vulkano::command_buffer::submit as submit_command;
        use vulkano::descriptor::descriptor_set::DescriptorPool;
        use vulkano::pipeline::ComputePipeline;

        // Include the shader wrapper.
        mod shader {
            #![allow(dead_code)]
            include!{concat!(env!("OUT_DIR"), concat!("/shaders/", $shader_path))}
        }

        // Create the pipeline layout wrapper.
        mod layout_definition {
            pipeline_layout!{
                buffers: {
                    $( $buf_ident: StorageBuffer<[$buf_type]> ),*
                }
            }
        }

        // Init `vulkano`.
        let instance = instance!();
        let physical_device = physical_device!(instance);
        let (ref device, ref queue) = device_and_queue!(physical_device);

        // Allocate buffers.
        $( let $buf_ident = cpu_array_buffer!(device, queue, $buf_type, $buf_len); )*

        // Create descriptor pool.
        let descriptor_pool = DescriptorPool::new(device);

        // Create pipeline layout.
        let pipeline_layout = layout_definition::CustomPipeline::new(device).unwrap();
        let buffer_descriptors = layout_definition::buffers::Descriptors {
            $( $buf_ident: &$buf_ident, )*
        };
        let buffer_set = layout_definition::buffers::Set::new(&descriptor_pool,
                                                              &pipeline_layout,
                                                              &buffer_descriptors);

        // Load the shader and assemble the pipeline.
        let compute_shader = shader::Shader::load(device).expect("Failed to create shader module.");
        let pipeline = ComputePipeline::new(device,
                                            &pipeline_layout,
                                            &compute_shader.main_entry_point(),
                                            &())
            .expect("Failed to create compute pipeline.");

        // Assemble and return the execution command.
        let execution_command = PrimaryCommandBufferBuilder::new(device, queue.family())
            .dispatch(&pipeline, buffer_set, $workgroup_count, &())
            .build();
        let $exec_cmd = || {
            submit_command(&execution_command, queue).unwrap();
        };
    }
}