Merge pull request #680 from Ralith/immutable-image

ImmutableImage refactor
This commit is contained in:
tomaka 2017-07-24 10:35:41 +02:00 committed by GitHub
commit bfc30a5f37
7 changed files with 297 additions and 63 deletions

View File

@ -11,7 +11,7 @@ extern crate vulkano;
use vulkano::device::{Device, DeviceExtensions};
use vulkano::format::Format;
use vulkano::image::ImmutableImage;
use vulkano::image::{ImmutableImage, ImageUsage, ImageLayout};
use vulkano::image::Dimensions;
use vulkano::instance;
use vulkano::instance::{Instance, InstanceExtensions, PhysicalDevice};
@ -99,7 +99,9 @@ fn main() {
// Create an image in order to generate some additional logging:
let pixel_format = Format::R8G8B8A8Uint;
let dimensions = Dimensions::Dim2d { width: 4096, height: 4096 };
ImmutableImage::new(device.clone(), dimensions, pixel_format, Some(queue.family())).unwrap();
const data: [[u8; 4]; 4096*4096] = [[0; 4]; 4096 * 4096];
let (image, _) = ImmutableImage::from_iter(data.iter().cloned(), dimensions, pixel_format,
Some(queue.family()), queue.clone()).unwrap();
// (At this point you should see a bunch of messages printed to the terminal window - have fun debugging!)
}

View File

@ -24,7 +24,6 @@ use vulkano_win::VkSurfaceBuild;
use vulkano::sync::GpuFuture;
use std::sync::Arc;
use std::time::Duration;
fn main() {
// The start of this example is exactly the same as `triangle`. You should read the
@ -102,22 +101,17 @@ fn main() {
).unwrap()
);
let texture = vulkano::image::immutable::ImmutableImage::new(device.clone(), vulkano::image::Dimensions::Dim2d { width: 93, height: 93 },
vulkano::format::R8G8B8A8Srgb, Some(queue.family())).unwrap();
let pixel_buffer = {
let (texture, tex_future) = {
let image = image::load_from_memory_with_format(include_bytes!("image_img.png"),
image::ImageFormat::PNG).unwrap().to_rgba();
let image_data = image.into_raw().clone();
let image_data_chunks = image_data.chunks(4).map(|c| [c[0], c[1], c[2], c[3]]);
// TODO: staging buffer instead
vulkano::buffer::cpu_access::CpuAccessibleBuffer::<[[u8; 4]]>
::from_iter(device.clone(), vulkano::buffer::BufferUsage::all(),
Some(queue.family()), image_data_chunks)
.expect("failed to create buffer")
vulkano::image::immutable::ImmutableImage::from_iter(
image_data.iter().cloned(),
vulkano::image::Dimensions::Dim2d { width: 93, height: 93 },
vulkano::format::R8G8B8A8Srgb,
Some(queue.family()),
queue.clone()).unwrap()
};
@ -138,6 +132,7 @@ fn main() {
dimensions: [images[0].dimensions()[0] as f32, images[0].dimensions()[1] as f32],
}))
.fragment_shader(fs.main_entry_point(), ())
.blend_alpha_blending()
.render_pass(vulkano::framebuffer::Subpass::from(renderpass.clone(), 0).unwrap())
.build(device.clone())
.unwrap());
@ -152,7 +147,7 @@ fn main() {
.add(image.clone()).unwrap().build().unwrap())
}).collect::<Vec<_>>();
let mut previous_frame_end = Box::new(vulkano::sync::now(device.clone())) as Box<GpuFuture>;
let mut previous_frame_end = Box::new(tex_future) as Box<GpuFuture>;
loop {
previous_frame_end.cleanup_finished();
@ -161,12 +156,9 @@ fn main() {
let cb = vulkano::command_buffer::AutoCommandBufferBuilder::primary_one_time_submit(device.clone(), queue.family())
.unwrap()
.copy_buffer_to_image(pixel_buffer.clone(), texture.clone())
.unwrap()
//.clear_color_image(&texture, [0.0, 1.0, 0.0, 1.0])
.begin_render_pass(
framebuffers[image_num].clone(), false,
vec![[0.0, 0.0, 1.0, 1.0].into()]).unwrap()
vec![[1.0, 1.0, 1.0, 1.0].into()]).unwrap()
.draw(pipeline.clone(), vulkano::command_buffer::DynamicState::none(), vertex_buffer.clone(),
set.clone(), ()).unwrap()
.end_render_pass().unwrap()

View File

@ -16,3 +16,4 @@ shared_library = "0.1.5"
smallvec = "0.3.1"
lazy_static = "0.2.2"
vk-sys = { version = "0.3.0", path = "../vk-sys" }
half = "1"

View File

@ -103,6 +103,10 @@
//!
use std::vec::IntoIter as VecIntoIter;
use std::{mem, error, fmt};
use half::f16;
use vk;
// TODO: add enumerations for color, depth, stencil and depthstencil formats
@ -132,6 +136,39 @@ unsafe impl Data for u8 {
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IncompatiblePixelsType;
impl error::Error for IncompatiblePixelsType {
#[inline]
fn description(&self) -> &str { "supplied pixels' type is incompatible with this format" }
}
impl fmt::Display for IncompatiblePixelsType {
#[inline]
fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(fmt, "{}", error::Error::description(self))
}
}
pub unsafe trait AcceptsPixels<T> {
/// Returns an error if `T` cannot be used as a source of pixels for `Self`.
fn ensure_accepts(&self) -> Result<(), IncompatiblePixelsType>;
/// The number of `T`s which make up a single pixel.
///
/// ```
/// use vulkano::format::{AcceptsPixels, R8G8B8A8Srgb};
/// assert_eq!(<R8G8B8A8Srgb as AcceptsPixels<[u8; 4]>>::rate(&R8G8B8A8Srgb), 1);
/// assert_eq!(<R8G8B8A8Srgb as AcceptsPixels<u8>>::rate(&R8G8B8A8Srgb), 4);
/// ```
///
/// # Panics
///
/// May panic if `ensure_accepts` would not return `Ok(())`.
fn rate(&self) -> u32 { 1 }
}
macro_rules! formats {
($($name:ident => $vk:ident [$sz:expr] [$($f_ty:tt)*] {$($d_ty:tt)*},)+) => (
/// An enumeration of all the possible formats.
@ -357,12 +394,26 @@ macro_rules! formats {
(__inner_ty__ $name:ident compressed=$f:tt) => { FormatTy::Compressed };
(__inner_strongstorage__ $name:ident [$ty:ty; $dim:expr]) => {
formats!(__inner_strongstorage_common__ $name [$ty; $dim]);
unsafe impl AcceptsPixels<$ty> for $name {
fn ensure_accepts(&self) -> Result<(), IncompatiblePixelsType> { Ok(()) }
fn rate(&self) -> u32 { $dim }
}
};
(__inner_strongstorage__ $name:ident $ty:ty) => {
formats!(__inner_strongstorage_common__ $name $ty);
};
(__inner_strongstorage__ $name:ident ) => {};
(__inner_strongstorage_common__ $name:ident $ty:ty) => {
unsafe impl StrongStorage for $name {
type Pixel = $ty;
}
unsafe impl AcceptsPixels<$ty> for $name {
fn ensure_accepts(&self) -> Result<(), IncompatiblePixelsType> { Ok(()) }
}
};
(__inner_strongstorage__ $name:ident ) => {};
}
formats! {
@ -441,28 +492,28 @@ formats! {
R16Sscaled => FORMAT_R16_SSCALED [Some(2)] [float=1] {i16},
R16Uint => FORMAT_R16_UINT [Some(2)] [uint=1] {u16},
R16Sint => FORMAT_R16_SINT [Some(2)] [sint=1] {i16},
R16Sfloat => FORMAT_R16_SFLOAT [Some(2)] [float=1] {},
R16Sfloat => FORMAT_R16_SFLOAT [Some(2)] [float=1] {f16},
R16G16Unorm => FORMAT_R16G16_UNORM [Some(4)] [float=2] {[u16; 2]},
R16G16Snorm => FORMAT_R16G16_SNORM [Some(4)] [float=2] {[i16; 2]},
R16G16Uscaled => FORMAT_R16G16_USCALED [Some(4)] [float=2] {[u16; 2]},
R16G16Sscaled => FORMAT_R16G16_SSCALED [Some(4)] [float=2] {[i16; 2]},
R16G16Uint => FORMAT_R16G16_UINT [Some(4)] [uint=2] {[u16; 2]},
R16G16Sint => FORMAT_R16G16_SINT [Some(4)] [sint=2] {[i16; 2]},
R16G16Sfloat => FORMAT_R16G16_SFLOAT [Some(4)] [float=2] {},
R16G16Sfloat => FORMAT_R16G16_SFLOAT [Some(4)] [float=2] {[f16; 2]},
R16G16B16Unorm => FORMAT_R16G16B16_UNORM [Some(6)] [float=3] {[u16; 3]},
R16G16B16Snorm => FORMAT_R16G16B16_SNORM [Some(6)] [float=3] {[i16; 3]},
R16G16B16Uscaled => FORMAT_R16G16B16_USCALED [Some(6)] [float=3] {[u16; 3]},
R16G16B16Sscaled => FORMAT_R16G16B16_SSCALED [Some(6)] [float=3] {[i16; 3]},
R16G16B16Uint => FORMAT_R16G16B16_UINT [Some(6)] [uint=3] {[u16; 3]},
R16G16B16Sint => FORMAT_R16G16B16_SINT [Some(6)] [sint=3] {[i16; 3]},
R16G16B16Sfloat => FORMAT_R16G16B16_SFLOAT [Some(6)] [float=3] {},
R16G16B16Sfloat => FORMAT_R16G16B16_SFLOAT [Some(6)] [float=3] {[f16; 3]},
R16G16B16A16Unorm => FORMAT_R16G16B16A16_UNORM [Some(8)] [float=4] {[u16; 4]},
R16G16B16A16Snorm => FORMAT_R16G16B16A16_SNORM [Some(8)] [float=4] {[i16; 4]},
R16G16B16A16Uscaled => FORMAT_R16G16B16A16_USCALED [Some(8)] [float=4] {[u16; 4]},
R16G16B16A16Sscaled => FORMAT_R16G16B16A16_SSCALED [Some(8)] [float=4] {[i16; 4]},
R16G16B16A16Uint => FORMAT_R16G16B16A16_UINT [Some(8)] [uint=4] {[u16; 4]},
R16G16B16A16Sint => FORMAT_R16G16B16A16_SINT [Some(8)] [sint=4] {[i16; 4]},
R16G16B16A16Sfloat => FORMAT_R16G16B16A16_SFLOAT [Some(8)] [float=4] {},
R16G16B16A16Sfloat => FORMAT_R16G16B16A16_SFLOAT [Some(8)] [float=4] {[f16; 4]},
R32Uint => FORMAT_R32_UINT [Some(4)] [uint=1] {u32},
R32Sint => FORMAT_R32_SINT [Some(4)] [sint=1] {i32},
R32Sfloat => FORMAT_R32_SFLOAT [Some(4)] [float=1] {f32},
@ -674,6 +725,39 @@ unsafe impl PossibleFloatOrCompressedFormatDesc for Format {
}
}
macro_rules! impl_pixel {
{$($ty:ty;)+} => {
$(impl_pixel!(inner $ty);)*
$(impl_pixel!(inner [$ty; 1]);)*
$(impl_pixel!(inner [$ty; 2]);)*
$(impl_pixel!(inner [$ty; 3]);)*
$(impl_pixel!(inner [$ty; 4]);)*
$(impl_pixel!(inner ($ty,));)*
$(impl_pixel!(inner ($ty, $ty));)*
$(impl_pixel!(inner ($ty, $ty, $ty));)*
$(impl_pixel!(inner ($ty, $ty, $ty, $ty));)*
};
(inner $ty:ty) => {
unsafe impl AcceptsPixels<$ty> for Format {
fn ensure_accepts(&self) -> Result<(), IncompatiblePixelsType> {
// TODO: Be more strict: accept only if the format has a matching AcceptsPixels impl.
if self.size().map_or(false, |x| x % mem::size_of::<$ty>() == 0) {
Ok(())
} else {
Err(IncompatiblePixelsType)
}
}
fn rate(&self) -> u32 {
(self.size().expect("this format cannot accept pixels") / mem::size_of::<$ty>()) as u32
}
}
}
}
impl_pixel! {
u8; i8; u16; i16; u32; i32; u64; i64; f16; f32; f64;
}
pub unsafe trait StrongStorage: FormatDesc {
type Pixel: Copy;
}

View File

@ -9,9 +9,21 @@
use smallvec::SmallVec;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::iter;
use buffer::BufferAccess;
use buffer::BufferUsage;
use buffer::CpuAccessibleBuffer;
use buffer::TypedBufferAccess;
use command_buffer::AutoCommandBuffer;
use command_buffer::AutoCommandBufferBuilder;
use command_buffer::CommandBuffer;
use command_buffer::CommandBufferExecFuture;
use device::Device;
use device::Queue;
use format::AcceptsPixels;
use format::FormatDesc;
use image::Dimensions;
use image::ImageInner;
@ -28,33 +40,40 @@ use instance::QueueFamily;
use memory::pool::AllocLayout;
use memory::pool::MemoryPool;
use memory::pool::MemoryPoolAlloc;
use memory::pool::StdMemoryPool;
use memory::pool::StdMemoryPoolAlloc;
use sync::AccessError;
use sync::Sharing;
use sync::NowFuture;
/// Image whose purpose is to be used for read-only purposes. You can write to the image once,
/// but then you must only ever read from it. TODO: clarify because of blit operations
/// but then you must only ever read from it.
// TODO: type (2D, 3D, array, etc.) as template parameter
#[derive(Debug)]
pub struct ImmutableImage<F, A = Arc<StdMemoryPool>>
where A: MemoryPool
{
pub struct ImmutableImage<F, A = StdMemoryPoolAlloc> {
image: UnsafeImage,
view: UnsafeImageView,
dimensions: Dimensions,
memory: A::Alloc,
memory: A,
format: F,
initialized: AtomicBool,
layout: ImageLayout,
}
// Must not implement Clone, as that would lead to multiple `used` values.
pub struct ImmutableImageInitialization<F, A = StdMemoryPoolAlloc> {
image: Arc<ImmutableImage<F, A>>,
used: AtomicBool,
}
impl<F> ImmutableImage<F> {
/// Builds a new immutable image.
// TODO: one mipmap is probably not a great default
#[deprecated(note = "use ImmutableImage::uninitialized instead")]
#[inline]
pub fn new<'a, I>(device: Arc<Device>, dimensions: Dimensions, format: F, queue_families: I)
-> Result<Arc<ImmutableImage<F>>, ImageCreationError>
where F: FormatDesc,
I: IntoIterator<Item = QueueFamily<'a>>
{
#[allow(deprecated)]
ImmutableImage::with_mipmaps(device,
dimensions,
format,
@ -62,13 +81,14 @@ impl<F> ImmutableImage<F> {
queue_families)
}
/// Builds a new immutable image with the given number of mipmaps.
#[deprecated(note = "use ImmutableImage::uninitialized instead")]
#[inline]
pub fn with_mipmaps<'a, I, M>(device: Arc<Device>, dimensions: Dimensions, format: F,
mipmaps: M, queue_families: I)
-> Result<Arc<ImmutableImage<F>>, ImageCreationError>
where F: FormatDesc,
I: IntoIterator<Item = QueueFamily<'a>>,
M: Into<MipmapsCount>
where F: FormatDesc,
I: IntoIterator<Item = QueueFamily<'a>>,
M: Into<MipmapsCount>
{
let usage = ImageUsage {
transfer_source: true, // for blits
@ -77,6 +97,21 @@ impl<F> ImmutableImage<F> {
..ImageUsage::none()
};
let (image, _) = ImmutableImage::uninitialized(device, dimensions, format, mipmaps, usage, ImageLayout::ShaderReadOnlyOptimal, queue_families)?;
image.initialized.store(true, Ordering::Relaxed); // Allow uninitialized access for backwards compatibility
Ok(image)
}
/// Builds an uninitialized immutable image.
///
/// Returns two things: the image, and a special access that should be used for the initial upload to the image.
pub fn uninitialized<'a, I, M>(device: Arc<Device>, dimensions: Dimensions, format: F,
mipmaps: M, usage: ImageUsage, layout: ImageLayout, queue_families: I)
-> Result<(Arc<ImmutableImage<F>>, ImmutableImageInitialization<F>), ImageCreationError>
where F: FormatDesc,
I: IntoIterator<Item = QueueFamily<'a>>,
M: Into<MipmapsCount>
{
let queue_families = queue_families
.into_iter()
.map(|f| f.id())
@ -130,19 +165,84 @@ impl<F> ImmutableImage<F> {
0 .. image.dimensions().array_layers())?
};
Ok(Arc::new(ImmutableImage {
image: image,
view: view,
memory: mem,
dimensions: dimensions,
format: format,
}))
let image = Arc::new(ImmutableImage {
image: image,
view: view,
memory: mem,
dimensions: dimensions,
format: format,
initialized: AtomicBool::new(false),
layout: layout,
});
let init = ImmutableImageInitialization {
image: image.clone(),
used: AtomicBool::new(false),
};
Ok((image, init))
}
/// Construct an ImmutableImage from the contents of `iter`.
///
/// TODO: Support mipmaps
#[inline]
pub fn from_iter<'a, P, I, J>(iter: I, dimensions: Dimensions, format: F, queue_families: J, queue: Arc<Queue>)
-> Result<(Arc<Self>, CommandBufferExecFuture<NowFuture, AutoCommandBuffer>),
ImageCreationError>
where P: Send + Sync + Clone + 'static,
F: FormatDesc + AcceptsPixels<P> + 'static + Send + Sync,
I: ExactSizeIterator<Item = P>,
J: IntoIterator<Item = QueueFamily<'a>>,
{
let source = CpuAccessibleBuffer::from_iter(queue.device().clone(),
BufferUsage::transfer_source(),
iter::once(queue.family()),
iter)?;
ImmutableImage::from_buffer(source, dimensions, format, queue_families, queue)
}
/// Construct an ImmutableImage containing a copy of the data in `source`.
///
/// TODO: Support mipmaps
pub fn from_buffer<'a, B, P, I>(source: B, dimensions: Dimensions, format: F, queue_families: I, queue: Arc<Queue>)
-> Result<(Arc<Self>, CommandBufferExecFuture<NowFuture, AutoCommandBuffer>),
ImageCreationError>
where B: BufferAccess + TypedBufferAccess<Content = [P]> + 'static + Clone + Send + Sync,
P: Send + Sync + Clone + 'static,
F: FormatDesc + AcceptsPixels<P> + 'static + Send + Sync,
I: IntoIterator<Item = QueueFamily<'a>>,
{
let usage = ImageUsage { transfer_destination: true, sampled: true, ..ImageUsage::none() };
let layout = ImageLayout::ShaderReadOnlyOptimal;
// TODO: The following panics should be removed in favor of propagating errors from copy_buffer_to_image.
format.ensure_accepts().unwrap();
if source.len() % format.rate() as usize != 0 {
panic!("cannot divide {} datums into an image with {} channels", source.len(), format.rate());
}
if dimensions.num_texels() as usize * format.rate() as usize != source.len() {
panic!("image with {} texels cannot be initialized with {}", dimensions.num_texels(), source.len() / format.rate() as usize);
}
let (buffer, init) = ImmutableImage::uninitialized(source.device().clone(),
dimensions, format,
MipmapsCount::One, usage, layout,
queue_families)?;
let cb = AutoCommandBufferBuilder::new(source.device().clone(), queue.family())?
.copy_buffer_to_image_dimensions(source, init, [0, 0, 0], dimensions.width_height_depth(), 0, dimensions.array_layers_with_cube(), 0).unwrap()
.build().unwrap();
let future = match cb.execute(queue) {
Ok(f) => f,
Err(_) => unreachable!(),
};
Ok((buffer, future))
}
}
impl<F, A> ImmutableImage<F, A>
where A: MemoryPool
{
impl<F, A> ImmutableImage<F, A> {
/// Returns the dimensions of the image.
#[inline]
pub fn dimensions(&self) -> Dimensions {
@ -158,7 +258,6 @@ impl<F, A> ImmutableImage<F, A>
unsafe impl<F, A> ImageAccess for ImmutableImage<F, A>
where F: 'static + Send + Sync,
A: MemoryPool
{
#[inline]
fn inner(&self) -> ImageInner {
@ -173,12 +272,12 @@ unsafe impl<F, A> ImageAccess for ImmutableImage<F, A>
#[inline]
fn initial_layout_requirement(&self) -> ImageLayout {
ImageLayout::ShaderReadOnlyOptimal // TODO: ?
self.layout
}
#[inline]
fn final_layout_requirement(&self) -> ImageLayout {
ImageLayout::ShaderReadOnlyOptimal // TODO: ?
self.layout
}
#[inline]
@ -188,23 +287,26 @@ unsafe impl<F, A> ImageAccess for ImmutableImage<F, A>
#[inline]
fn try_gpu_lock(&self, exclusive_access: bool, queue: &Queue) -> Result<(), AccessError> {
Ok(()) // FIXME:
if exclusive_access {
return Err(AccessError::ExclusiveDenied);
}
if !self.initialized.load(Ordering::Relaxed) {
return Err(AccessError::BufferNotInitialized);
}
Ok(())
}
#[inline]
unsafe fn increase_gpu_lock(&self) {
// FIXME:
}
unsafe fn increase_gpu_lock(&self) {}
#[inline]
unsafe fn unlock(&self) {
// TODO:
}
unsafe fn unlock(&self) {}
}
unsafe impl<P, F, A> ImageContent<P> for ImmutableImage<F, A>
where F: 'static + Send + Sync,
A: MemoryPool
{
#[inline]
fn matches_format(&self) -> bool {
@ -214,7 +316,6 @@ unsafe impl<P, F, A> ImageContent<P> for ImmutableImage<F, A>
unsafe impl<F: 'static, A> ImageViewAccess for ImmutableImage<F, A>
where F: 'static + Send + Sync,
A: MemoryPool
{
#[inline]
fn parent(&self) -> &ImageAccess {
@ -233,22 +334,22 @@ unsafe impl<F: 'static, A> ImageViewAccess for ImmutableImage<F, A>
#[inline]
fn descriptor_set_storage_image_layout(&self) -> ImageLayout {
ImageLayout::ShaderReadOnlyOptimal
self.layout
}
#[inline]
fn descriptor_set_combined_image_sampler_layout(&self) -> ImageLayout {
ImageLayout::ShaderReadOnlyOptimal
self.layout
}
#[inline]
fn descriptor_set_sampled_image_layout(&self) -> ImageLayout {
ImageLayout::ShaderReadOnlyOptimal
self.layout
}
#[inline]
fn descriptor_set_input_attachment_layout(&self) -> ImageLayout {
ImageLayout::ShaderReadOnlyOptimal
self.layout
}
#[inline]
@ -256,3 +357,51 @@ unsafe impl<F: 'static, A> ImageViewAccess for ImmutableImage<F, A>
true
}
}
unsafe impl<F, A> ImageAccess for ImmutableImageInitialization<F, A>
where F: 'static + Send + Sync,
{
#[inline]
fn inner(&self) -> ImageInner {
ImageAccess::inner(&self.image)
}
#[inline]
fn initial_layout_requirement(&self) -> ImageLayout {
ImageLayout::Undefined
}
#[inline]
fn final_layout_requirement(&self) -> ImageLayout {
self.image.layout
}
#[inline]
fn conflict_key(&self, _: u32, _: u32, _: u32, _: u32) -> u64 {
self.image.image.key()
}
#[inline]
fn try_gpu_lock(&self, exclusive_access: bool, queue: &Queue) -> Result<(), AccessError> {
if self.image.initialized.load(Ordering::Relaxed) {
return Err(AccessError::AlreadyInUse);
}
// FIXME: Mipmapped textures require multiple writes to initialize
if !self.used.compare_and_swap(false, true, Ordering::Relaxed) {
Ok(())
} else {
Err(AccessError::AlreadyInUse)
}
}
#[inline]
unsafe fn increase_gpu_lock(&self) {
debug_assert!(self.used.load(Ordering::Relaxed));
}
#[inline]
unsafe fn unlock(&self) {
self.image.initialized.store(true, Ordering::Relaxed);
}
}

View File

@ -313,6 +313,11 @@ impl Dimensions {
Dimensions::CubemapArray { .. } => ViewType::CubemapArray,
}
}
#[inline]
pub fn num_texels(&self) -> u32 {
self.width() * self.height() * self.depth() * self.array_layers_with_cube()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]

View File

@ -68,6 +68,7 @@ extern crate lazy_static;
extern crate shared_library;
extern crate smallvec;
extern crate vk_sys as vk;
pub extern crate half;
#[macro_use]
mod tests;