descriptor layout
This commit is contained in:
@@ -57,8 +57,16 @@ unsafe impl PipelineLayoutDesc for FragLayout {
|
|||||||
fn num_bindings_in_set(&self, set: usize) -> Option<usize> {
|
fn num_bindings_in_set(&self, set: usize) -> Option<usize> {
|
||||||
self.layout_data.num_bindings.get(&set).map(|&i| i)
|
self.layout_data.num_bindings.get(&set).map(|&i| i)
|
||||||
}
|
}
|
||||||
fn descriptor(&self, _set: usize, _binding: usize) -> Option<DescriptorDesc> {
|
fn descriptor(&self, set: usize, binding: usize) -> Option<DescriptorDesc> {
|
||||||
None
|
self.layout_data.descriptions.get(&set)
|
||||||
|
.and_then(|s|s.get(&binding))
|
||||||
|
.map(|desc| {
|
||||||
|
let mut desc = desc.clone();
|
||||||
|
dbg!(&self.stages);
|
||||||
|
desc.stages = self.stages.clone();
|
||||||
|
desc
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
fn num_push_constants_ranges(&self) -> usize {
|
fn num_push_constants_ranges(&self) -> usize {
|
||||||
0
|
0
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::sr;
|
use crate::sr;
|
||||||
use crate::srvk::SpirvTy;
|
use crate::srvk::{SpirvTy,DescriptorDescInfo};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use crate::vk::pipeline::shader::ShaderInterfaceDefEntry;
|
use crate::vk::pipeline::shader::ShaderInterfaceDefEntry;
|
||||||
use crate::vk::descriptor::descriptor::ShaderStages;
|
use crate::vk::descriptor::descriptor::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use crate::CompiledShaders;
|
use crate::CompiledShaders;
|
||||||
use crate::layouts::*;
|
use crate::layouts::*;
|
||||||
@@ -16,11 +16,13 @@ pub struct ShaderInterfaces {
|
|||||||
pub struct LayoutData {
|
pub struct LayoutData {
|
||||||
pub num_sets: usize,
|
pub num_sets: usize,
|
||||||
pub num_bindings: HashMap<usize, usize>,
|
pub num_bindings: HashMap<usize, usize>,
|
||||||
|
pub descriptions: HashMap<usize, HashMap<usize, DescriptorDesc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_entry(shaders: &CompiledShaders) -> Entry {
|
pub fn create_entry(shaders: &CompiledShaders) -> Entry {
|
||||||
let vertex_interfaces = create_interfaces(&shaders.vertex);
|
let vertex_interfaces = create_interfaces(&shaders.vertex);
|
||||||
let fragment_interfaces = create_interfaces(&shaders.fragment);
|
let fragment_interfaces = create_interfaces(&shaders.fragment);
|
||||||
|
let fragment_layout = create_layouts(&shaders.fragment);
|
||||||
let frag_input = FragInput{ inputs: fragment_interfaces.inputs };
|
let frag_input = FragInput{ inputs: fragment_interfaces.inputs };
|
||||||
let frag_output = FragOutput{ outputs: fragment_interfaces.outputs };
|
let frag_output = FragOutput{ outputs: fragment_interfaces.outputs };
|
||||||
let frag_layout = FragLayout {
|
let frag_layout = FragLayout {
|
||||||
@@ -28,7 +30,7 @@ pub fn create_entry(shaders: &CompiledShaders) -> Entry {
|
|||||||
fragment: true,
|
fragment: true,
|
||||||
..ShaderStages::none()
|
..ShaderStages::none()
|
||||||
},
|
},
|
||||||
layout_data: Default::default(),
|
layout_data: fragment_layout,
|
||||||
};
|
};
|
||||||
let vert_input = VertInput{ inputs: vertex_interfaces.inputs };
|
let vert_input = VertInput{ inputs: vertex_interfaces.inputs };
|
||||||
let vert_output = VertOutput{ outputs: vertex_interfaces.outputs };
|
let vert_output = VertOutput{ outputs: vertex_interfaces.outputs };
|
||||||
@@ -88,3 +90,53 @@ fn create_interfaces(data: &[u32]) -> ShaderInterfaces {
|
|||||||
})
|
})
|
||||||
.expect("failed to load module")
|
.expect("failed to load module")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_layouts(data: &[u32]) -> LayoutData {
|
||||||
|
sr::ShaderModule::load_u32_data(data)
|
||||||
|
.map(|m| {
|
||||||
|
m.enumerate_descriptor_sets(None)
|
||||||
|
.map(|sets| {
|
||||||
|
let num_sets = sets.len();
|
||||||
|
let num_bindings = sets
|
||||||
|
.iter()
|
||||||
|
.map(|i| {
|
||||||
|
dbg!(&i);
|
||||||
|
(i.set as usize, i.bindings.len())
|
||||||
|
})
|
||||||
|
.collect::<HashMap<usize, usize>>();
|
||||||
|
let descriptions = sets
|
||||||
|
.iter()
|
||||||
|
.map(|i| {
|
||||||
|
let desc = i.bindings.iter()
|
||||||
|
.map(|b| {
|
||||||
|
let info = DescriptorDescInfo{
|
||||||
|
descriptor_type: b.descriptor_type,
|
||||||
|
image: b.image,
|
||||||
|
};
|
||||||
|
let ty = SpirvTy::<DescriptorDescTy>::from(info).inner();
|
||||||
|
let stages = ShaderStages::none();
|
||||||
|
let d = DescriptorDesc {
|
||||||
|
ty,
|
||||||
|
array_count: b.count,
|
||||||
|
stages,
|
||||||
|
// TODO this is what vulkan_shaders does but I don't think
|
||||||
|
// it's correct
|
||||||
|
readonly: true,
|
||||||
|
};
|
||||||
|
(b.binding as usize, d)
|
||||||
|
})
|
||||||
|
.collect::<HashMap<usize, DescriptorDesc>>();
|
||||||
|
(i.set as usize, desc)
|
||||||
|
})
|
||||||
|
.collect::<HashMap<usize, HashMap<usize, DescriptorDesc>>>();
|
||||||
|
LayoutData {
|
||||||
|
num_sets,
|
||||||
|
num_bindings,
|
||||||
|
descriptions,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("Failed to pass outputs")
|
||||||
|
})
|
||||||
|
.expect("failed to load module")
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
67
src/srvk.rs
67
src/srvk.rs
@@ -1,7 +1,6 @@
|
|||||||
use crate::sr;
|
use crate::sr;
|
||||||
use crate::vk;
|
use crate::vk;
|
||||||
use vk::descriptor::descriptor::*;
|
use vk::descriptor::descriptor::*;
|
||||||
use vk::pipeline::shader::ShaderInterfaceDefEntry;
|
|
||||||
use vk::format::Format;
|
use vk::format::Format;
|
||||||
|
|
||||||
pub struct SpirvTy<T> {
|
pub struct SpirvTy<T> {
|
||||||
@@ -9,8 +8,8 @@ pub struct SpirvTy<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct DescriptorDescInfo {
|
pub struct DescriptorDescInfo {
|
||||||
descriptor_type: sr::types::ReflectDescriptorType,
|
pub descriptor_type: sr::types::ReflectDescriptorType,
|
||||||
image: sr::types::ReflectImageTraits,
|
pub image: sr::types::ReflectImageTraits,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> SpirvTy<T> {
|
impl<T> SpirvTy<T> {
|
||||||
@@ -56,7 +55,9 @@ impl From<sr::types::ReflectImageTraits> for SpirvTy<DescriptorImageDesc> {
|
|||||||
let t = DescriptorImageDesc {
|
let t = DescriptorImageDesc {
|
||||||
sampled: d.sampled != 0,
|
sampled: d.sampled != 0,
|
||||||
dimensions: SpirvTy::from(d.dim).inner(),
|
dimensions: SpirvTy::from(d.dim).inner(),
|
||||||
format: Some(SpirvTy::from(d.image_format).inner()),
|
// TODO figure out how to do format correctly
|
||||||
|
//format: Some(SpirvTy::from(d.image_format).inner()),
|
||||||
|
format: None,
|
||||||
multisampled: d.ms != 0,
|
multisampled: d.ms != 0,
|
||||||
array_layers: conv_array_layers(d.arrayed, d.depth),
|
array_layers: conv_array_layers(d.arrayed, d.depth),
|
||||||
};
|
};
|
||||||
@@ -66,13 +67,67 @@ impl From<sr::types::ReflectImageTraits> for SpirvTy<DescriptorImageDesc> {
|
|||||||
|
|
||||||
impl From<sr::types::variable::ReflectDimension> for SpirvTy<DescriptorImageDescDimensions> {
|
impl From<sr::types::variable::ReflectDimension> for SpirvTy<DescriptorImageDescDimensions> {
|
||||||
fn from(d: sr::types::variable::ReflectDimension) -> Self {
|
fn from(d: sr::types::variable::ReflectDimension) -> Self {
|
||||||
unimplemented!()
|
use sr::types::variable::ReflectDimension::*;
|
||||||
|
use DescriptorImageDescDimensions::*;
|
||||||
|
let inner = match d {
|
||||||
|
Type1d => OneDimensional,
|
||||||
|
Type2d => TwoDimensional,
|
||||||
|
Type3d => ThreeDimensional,
|
||||||
|
sr::types::variable::ReflectDimension::Cube => DescriptorImageDescDimensions::Cube,
|
||||||
|
_ => unimplemented!(),
|
||||||
|
};
|
||||||
|
SpirvTy{ inner }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I think this is wrong and currently is unused
|
||||||
impl From<sr::types::image::ReflectImageFormat> for SpirvTy<Format> {
|
impl From<sr::types::image::ReflectImageFormat> for SpirvTy<Format> {
|
||||||
fn from(d: sr::types::image::ReflectImageFormat) -> Self {
|
fn from(d: sr::types::image::ReflectImageFormat) -> Self {
|
||||||
unimplemented!()
|
use sr::types::image::ReflectImageFormat::*;
|
||||||
|
use Format::*;
|
||||||
|
let inner = match d {
|
||||||
|
Undefined => unimplemented!(),
|
||||||
|
RGBA32_FLOAT => R32G32B32A32Sfloat,
|
||||||
|
RGBA16_FLOAT => R16G16B16A16Sfloat,
|
||||||
|
R32_FLOAT => R32Sfloat,
|
||||||
|
RGBA8 => unimplemented!(),
|
||||||
|
RGBA8_SNORM => R8G8B8A8Snorm,
|
||||||
|
RG32_FLOAT => R32G32Sfloat,
|
||||||
|
RG16_FLOAT => R16G16Sfloat,
|
||||||
|
R11G11B10_FLOAT => unimplemented!(),
|
||||||
|
R16_FLOAT => R16Sfloat,
|
||||||
|
RGBA16 => unimplemented!(),
|
||||||
|
RGB10A2 => unimplemented!(),
|
||||||
|
RG16 => unimplemented!(),
|
||||||
|
RG8 => unimplemented!(),
|
||||||
|
R16 => unimplemented!(),
|
||||||
|
R8 => unimplemented!(),
|
||||||
|
RGBA16_SNORM => R16G16B16A16Snorm,
|
||||||
|
RG16_SNORM => R16G16Snorm,
|
||||||
|
RG8_SNORM => R8G8Snorm,
|
||||||
|
R16_SNORM => R16Snorm,
|
||||||
|
R8_SNORM => R8Snorm,
|
||||||
|
RGBA32_INT => R32G32B32A32Sint,
|
||||||
|
RGBA16_INT => R16G16B16A16Sint,
|
||||||
|
RGBA8_INT => R8G8B8A8Sint,
|
||||||
|
R32_INT => R32Sint,
|
||||||
|
RG32_INT => R32G32Sint,
|
||||||
|
RG16_INT => R16G16Sint,
|
||||||
|
RG8_INT => R8G8Sint,
|
||||||
|
R16_INT => R16Sint,
|
||||||
|
R8_INT => R8Sint,
|
||||||
|
RGBA32_UINT => R32G32B32A32Uint,
|
||||||
|
RGBA16_UINT => R16G16B16A16Uint,
|
||||||
|
RGBA8_UINT => R8G8B8A8Uint,
|
||||||
|
R32_UINT => R32Uint,
|
||||||
|
RGB10A2_UINT => A2R10G10B10UintPack32,
|
||||||
|
RG32_UINT => R32G32Uint,
|
||||||
|
RG16_UINT => R16G16Uint,
|
||||||
|
RG8_UINT => R8G8Uint,
|
||||||
|
R16_UINT =>R16Uint,
|
||||||
|
R8_UINT =>R8Uint,
|
||||||
|
};
|
||||||
|
SpirvTy{ inner }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
tests/shaders/frag3.glsl
Normal file
9
tests/shaders/frag3.glsl
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) out vec4 f_color;
|
||||||
|
|
||||||
|
layout(set = 0, binding = 0) uniform sampler2D tex;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
f_color = texture(tex, vec2(0.0, 0.5));
|
||||||
|
}
|
||||||
8
tests/shaders/vert3.glsl
Normal file
8
tests/shaders/vert3.glsl
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 position;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(position, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
161
tests/tests.rs
161
tests/tests.rs
@@ -4,9 +4,10 @@ use shade_runner::*;
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use vulkano::descriptor::descriptor::ShaderStages;
|
use vulkano::descriptor::descriptor::*;
|
||||||
use vulkano::format::*;
|
use vulkano::format::*;
|
||||||
use vulkano::pipeline::shader::ShaderInterfaceDefEntry;
|
use vulkano::pipeline::shader::ShaderInterfaceDefEntry;
|
||||||
|
use vulkano::descriptor::pipeline_layout::PipelineLayoutDesc;
|
||||||
|
|
||||||
fn setup() {
|
fn setup() {
|
||||||
color_backtrace::install();
|
color_backtrace::install();
|
||||||
@@ -31,6 +32,27 @@ fn difference(e: &str, t: &str) -> String {
|
|||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn descriptor_layout<T>(desc: &T) -> String
|
||||||
|
where
|
||||||
|
T: PipelineLayoutDesc,
|
||||||
|
{
|
||||||
|
let num_sets = desc.num_sets();
|
||||||
|
let mut r = format!("{:?}", num_sets);
|
||||||
|
for n in 0..num_sets {
|
||||||
|
let num_bindings = desc.num_bindings_in_set(n);
|
||||||
|
r = format!("{:?}{:?}", r, num_bindings);
|
||||||
|
for b in num_bindings {
|
||||||
|
r = format!("{:?}{:?}", r, desc.descriptor(n, b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let num_push_constants = desc.num_push_constants_ranges();
|
||||||
|
r = format!("{:?}{:?}", r, num_push_constants);
|
||||||
|
for i in 0..num_push_constants {
|
||||||
|
r = format!("{:?}{:?}", r, desc.push_constants_range(i));
|
||||||
|
}
|
||||||
|
r
|
||||||
|
}
|
||||||
|
|
||||||
fn parse<T>(vertex: T, fragment: T) -> shade_runner::Entry
|
fn parse<T>(vertex: T, fragment: T) -> shade_runner::Entry
|
||||||
where
|
where
|
||||||
T: AsRef<Path>,
|
T: AsRef<Path>,
|
||||||
@@ -66,6 +88,7 @@ fn test_shade1() {
|
|||||||
layout_data: LayoutData {
|
layout_data: LayoutData {
|
||||||
num_sets: 0,
|
num_sets: 0,
|
||||||
num_bindings: HashMap::new(),
|
num_bindings: HashMap::new(),
|
||||||
|
descriptions: HashMap::new(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
vert_input: VertInput {
|
vert_input: VertInput {
|
||||||
@@ -98,23 +121,23 @@ fn test_shade1() {
|
|||||||
fn test_shade2() {
|
fn test_shade2() {
|
||||||
setup();
|
setup();
|
||||||
let target = Entry {
|
let target = Entry {
|
||||||
frag_input: FragInput {
|
frag_input: FragInput {
|
||||||
inputs: vec![
|
inputs: vec![
|
||||||
ShaderInterfaceDefEntry {
|
ShaderInterfaceDefEntry {
|
||||||
location: 0..1,
|
location: 0..1,
|
||||||
format: Format::R32G32B32A32Sfloat,
|
format: Format::R32G32B32A32Sfloat,
|
||||||
name: Some(Cow::Borrowed("cool")),
|
name: Some(Cow::Borrowed("cool")),
|
||||||
},
|
},
|
||||||
ShaderInterfaceDefEntry {
|
ShaderInterfaceDefEntry {
|
||||||
location: 1..2,
|
location: 1..2,
|
||||||
format: Format::R32G32Sfloat,
|
format: Format::R32G32Sfloat,
|
||||||
name: Some(Cow::Borrowed("yep")),
|
name: Some(Cow::Borrowed("yep")),
|
||||||
},
|
},
|
||||||
ShaderInterfaceDefEntry {
|
ShaderInterfaceDefEntry {
|
||||||
location: 2..3,
|
location: 2..3,
|
||||||
format: Format::R32Sfloat,
|
format: Format::R32Sfloat,
|
||||||
name: Some(Cow::Borrowed("monkey")),
|
name: Some(Cow::Borrowed("monkey")),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
frag_output: FragOutput {
|
frag_output: FragOutput {
|
||||||
@@ -132,6 +155,7 @@ fn test_shade2() {
|
|||||||
layout_data: LayoutData {
|
layout_data: LayoutData {
|
||||||
num_sets: 0,
|
num_sets: 0,
|
||||||
num_bindings: HashMap::new(),
|
num_bindings: HashMap::new(),
|
||||||
|
descriptions: HashMap::new(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
vert_input: VertInput {
|
vert_input: VertInput {
|
||||||
@@ -144,20 +168,20 @@ fn test_shade2() {
|
|||||||
vert_output: VertOutput {
|
vert_output: VertOutput {
|
||||||
outputs: vec![
|
outputs: vec![
|
||||||
ShaderInterfaceDefEntry {
|
ShaderInterfaceDefEntry {
|
||||||
location: 0..1,
|
location: 0..1,
|
||||||
format: Format::R32G32B32A32Sfloat,
|
format: Format::R32G32B32A32Sfloat,
|
||||||
name: Some(Cow::Borrowed("cool")),
|
name: Some(Cow::Borrowed("cool")),
|
||||||
},
|
},
|
||||||
ShaderInterfaceDefEntry {
|
ShaderInterfaceDefEntry {
|
||||||
location: 1..2,
|
location: 1..2,
|
||||||
format: Format::R32G32Sfloat,
|
format: Format::R32G32Sfloat,
|
||||||
name: Some(Cow::Borrowed("yep")),
|
name: Some(Cow::Borrowed("yep")),
|
||||||
},
|
},
|
||||||
ShaderInterfaceDefEntry {
|
ShaderInterfaceDefEntry {
|
||||||
location: 2..3,
|
location: 2..3,
|
||||||
format: Format::R32Sfloat,
|
format: Format::R32Sfloat,
|
||||||
name: Some(Cow::Borrowed("monkey")),
|
name: Some(Cow::Borrowed("monkey")),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
vert_layout: VertLayout(ShaderStages {
|
vert_layout: VertLayout(ShaderStages {
|
||||||
@@ -176,3 +200,88 @@ fn test_shade2() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shade3() {
|
||||||
|
setup();
|
||||||
|
let target = Entry {
|
||||||
|
frag_input: FragInput { inputs: Vec::new() },
|
||||||
|
frag_output: FragOutput {
|
||||||
|
outputs: vec![ShaderInterfaceDefEntry {
|
||||||
|
location: 0..1,
|
||||||
|
format: Format::R32G32B32A32Sfloat,
|
||||||
|
name: Some(Cow::Borrowed("f_color")),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
frag_layout: FragLayout {
|
||||||
|
stages: ShaderStages {
|
||||||
|
fragment: true,
|
||||||
|
..ShaderStages::none()
|
||||||
|
},
|
||||||
|
layout_data: LayoutData {
|
||||||
|
num_sets: 1,
|
||||||
|
num_bindings: vec![(0, 1)].into_iter().collect(),
|
||||||
|
descriptions: vec![(
|
||||||
|
0,
|
||||||
|
vec![(
|
||||||
|
0,
|
||||||
|
DescriptorDesc {
|
||||||
|
ty: DescriptorDescTy::CombinedImageSampler(DescriptorImageDesc {
|
||||||
|
sampled: true,
|
||||||
|
dimensions: DescriptorImageDescDimensions::TwoDimensional,
|
||||||
|
format: None,
|
||||||
|
multisampled: false,
|
||||||
|
array_layers: DescriptorImageDescArray::NonArrayed,
|
||||||
|
}),
|
||||||
|
array_count: 1,
|
||||||
|
stages: ShaderStages {
|
||||||
|
fragment: true,
|
||||||
|
..ShaderStages::none()
|
||||||
|
},
|
||||||
|
readonly: true,
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
)]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vert_input: VertInput {
|
||||||
|
inputs: vec![ShaderInterfaceDefEntry {
|
||||||
|
location: 0..1,
|
||||||
|
format: Format::R32G32Sfloat,
|
||||||
|
name: Some(Cow::Borrowed("position")),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
vert_output: VertOutput {
|
||||||
|
outputs: Vec::new(),
|
||||||
|
},
|
||||||
|
vert_layout: VertLayout(ShaderStages {
|
||||||
|
vertex: true,
|
||||||
|
..ShaderStages::none()
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let entry = parse("vert3.glsl", "frag3.glsl");
|
||||||
|
do_test(&entry.frag_input, &target.frag_input);
|
||||||
|
do_test(&entry.frag_output, &target.frag_output);
|
||||||
|
do_test(&entry.vert_input, &target.vert_input);
|
||||||
|
do_test(&entry.vert_output, &target.vert_output);
|
||||||
|
do_test(&descriptor_layout(&entry.frag_layout), &descriptor_layout(&target.frag_layout));
|
||||||
|
do_test(&descriptor_layout(&entry.vert_layout), &descriptor_layout(&target.vert_layout));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_test<T>(a: &T, b: &T)
|
||||||
|
where
|
||||||
|
T: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let a = format!("{:?}", a);
|
||||||
|
let b = format!("{:?}", b);
|
||||||
|
assert_eq!(
|
||||||
|
&a,
|
||||||
|
&b,
|
||||||
|
"\n\nDifference: {}",
|
||||||
|
difference(&a, &b)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user