diff --git a/Cargo.toml b/Cargo.toml index 48733783e68..475f721c77d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "src/backend/metal", "src/backend/vulkan", "src/hal", + "src/warden", "src/render", "examples/hal/quad", "examples/render/quad_render", diff --git a/Makefile b/Makefile index 937a2075122..8c5fb4c1e75 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ RUST_BACKTRACE:=1 EXCLUDES:= +FEATURES_WARDEN:= FEATURES_RENDER:= FEATURES_RENDER_ADD:= mint serialize FEATURES_QUAD:= @@ -14,12 +15,14 @@ SDL2_PPA=http://ppa.launchpad.net/zoogie/sdl2-snapshots/ubuntu/pool/main/libs/li ifeq ($(OS),Windows_NT) EXCLUDES+= --exclude gfx-backend-metal FEATURES_QUAD=vulkan + FEATURES_WARDEN+=vulkan ifeq ($(TARGET),x86_64-pc-windows-gnu) # No d3d12 support on GNU windows ATM # context: https://github.com/gfx-rs/gfx/pull/1417 EXCLUDES+= --exclude gfx-backend-dx12 else FEATURES_QUAD2=dx12 + FEATURES_WARDEN+=dx12 endif else UNAME_S:=$(shell uname -s) @@ -28,24 +31,32 @@ else ifeq ($(UNAME_S),Linux) EXCLUDES+= --exclude gfx-backend-metal FEATURES_QUAD=vulkan + FEATURES_WARDEN+=vulkan endif ifeq ($(UNAME_S),Darwin) EXCLUDES+= --exclude gfx-backend-vulkan EXCLUDES+= --exclude quad-render FEATURES_QUAD=metal + FEATURES_WARDEN+=metal CMD_QUAD_RENDER=pwd endif endif -.PHONY: all check ex-hal-quad render ex-render-quad travis-sdl2 +.PHONY: all check ex-hal-quad warden reftests render ex-render-quad travis-sdl2 -all: check ex-hal-quad render ex-render-quad +all: check ex-hal-quad warden render ex-render-quad check: cargo check --all $(EXCLUDES) cargo test --all $(EXCLUDES) +warden: + cd src/warden && cargo test + +reftests: warden + cd src/warden && cargo run --bin reftest --features "$(FEATURES_WARDEN)" + render: cd src/render && cargo test --features "$(FEATURES_RENDER)" cd src/render && cargo test --features "$(FEATURES_RENDER) $(FEATURES_RENDER_ADD)" diff --git a/examples/hal/quad/main.rs b/examples/hal/quad/main.rs index c5a5c1539ca..ed75ac7c1a1 100644 --- a/examples/hal/quad/main.rs +++ b/examples/hal/quad/main.rs @@ -190,7 +190,7 @@ fn main() { accesses: i::Access::empty() .. (i::COLOR_ATTACHMENT_READ | i::COLOR_ATTACHMENT_WRITE), }; - device.create_renderpass(&[attachment], &[subpass], &[dependency]) + device.create_render_pass(&[attachment], &[subpass], &[dependency]) }; // diff --git a/reftests/scenes/basic.ron b/reftests/scenes/basic.ron new file mode 100644 index 00000000000..f17beedc8f5 --- /dev/null +++ b/reftests/scenes/basic.ron @@ -0,0 +1,59 @@ +( + resources: { + "im-color": Image( + kind: D2(1, 1, Single), + num_levels: 1, + format: (R8_G8_B8_A8, Unorm), + usage: (bits: 4), + ), + "pass": RenderPass( + attachments: { + "c": ( + format: (R8_G8_B8_A8, Unorm), + ops: (load: Clear, store: Store), + layouts: (start: General, end: General), + ), + }, + subpasses: { + "main": ( + colors: [("c", General)], + depth_stencil: None, + ) + }, + dependencies: [], + ), + "im-color-view": ImageView( + image: "im-color", + format: (R8_G8_B8_A8, Unorm), + range: ( + aspects: (bits: 1), + levels: (start: 0, end: 1), + layers: (start: 0, end: 1), + ), + ), + "fbo": Framebuffer( + pass: "pass", + views: { + "c": "im-color-view" + }, + extent: ( + width: 1, + height: 1, + depth: 1, + ), + ), + }, + jobs: { + "empty": Graphics( + descriptors: {}, + framebuffer: "fbo", + clear_values: [ + Color(Float((0.8, 0.8, 0.8, 1.0))), + ], + pass: ("pass", { + "main": (commands: [ + ]), + }), + ), + }, +) \ No newline at end of file diff --git a/reftests/suite.ron b/reftests/suite.ron new file mode 100644 index 00000000000..d0a90ca05bf --- /dev/null +++ b/reftests/suite.ron @@ -0,0 +1,8 @@ +{ + "basic": { + "render-pass-clear": ( + jobs: ["empty"], + expect: ImageRow("im-color", 0, [204,204,204,255]) + ), + }, +} \ No newline at end of file diff --git a/src/backend/dx12/src/conv.rs b/src/backend/dx12/src/conv.rs index ed60129dc44..ceea67d2bbe 100644 --- a/src/backend/dx12/src/conv.rs +++ b/src/backend/dx12/src/conv.rs @@ -423,7 +423,7 @@ pub fn map_buffer_resource_state(access: buffer::Access) -> D3D12_RESOURCE_STATE let mut state = D3D12_RESOURCE_STATE_COMMON; if access.contains(buffer::TRANSFER_READ) { - state = state | D3D12_RESOURCE_STATE_COPY_SOURCE | D3D12_RESOURCE_STATE_RESOLVE_DEST; + state = state | D3D12_RESOURCE_STATE_COPY_SOURCE; } if access.contains(buffer::INDEX_BUFFER_READ) { state = state | D3D12_RESOURCE_STATE_INDEX_BUFFER; @@ -472,7 +472,7 @@ pub fn map_image_resource_state(access: image::Access, layout: image::ImageLayou let mut state = D3D12_RESOURCE_STATE_COMMON; if access.contains(image::TRANSFER_READ) { - state = state | D3D12_RESOURCE_STATE_COPY_SOURCE | D3D12_RESOURCE_STATE_RESOLVE_DEST; + state = state | D3D12_RESOURCE_STATE_COPY_SOURCE; } if access.contains(image::INPUT_ATTACHMENT_READ) { state = state | D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE; diff --git a/src/backend/dx12/src/device.rs b/src/backend/dx12/src/device.rs index d060e04cb4a..fc9f26bff37 100644 --- a/src/backend/dx12/src/device.rs +++ b/src/backend/dx12/src/device.rs @@ -427,7 +427,7 @@ impl d::Device for Device { }) } - fn create_renderpass( + fn create_render_pass( &mut self, attachments: &[pass::Attachment], subpasses: &[pass::SubpassDesc], diff --git a/src/backend/dx12/src/lib.rs b/src/backend/dx12/src/lib.rs index b6eade4a219..f40bc00a99b 100644 --- a/src/backend/dx12/src/lib.rs +++ b/src/backend/dx12/src/lib.rs @@ -585,7 +585,9 @@ impl Instance { } } -impl core::Instance for Instance { +impl core::Instance for Instance { + type Backend = Backend; + fn enumerate_adapters(&self) -> Vec { // Enumerate adapters let mut cur_index = 0; diff --git a/src/backend/empty/src/lib.rs b/src/backend/empty/src/lib.rs index 8d1dbd27563..3033f32fe9d 100644 --- a/src/backend/empty/src/lib.rs +++ b/src/backend/empty/src/lib.rs @@ -88,7 +88,7 @@ impl core::Device for Device { unimplemented!() } - fn create_renderpass(&mut self, _: &[pass::Attachment], _: &[pass::SubpassDesc], _: &[pass::SubpassDependency]) -> () { + fn create_render_pass(&mut self, _: &[pass::Attachment], _: &[pass::SubpassDesc], _: &[pass::SubpassDependency]) -> () { unimplemented!() } @@ -562,7 +562,8 @@ impl core::Swapchain for Swapchain { } pub struct Instance; -impl core::Instance for Instance { +impl core::Instance for Instance { + type Backend = Backend; fn enumerate_adapters(&self) -> Vec { Vec::new() } diff --git a/src/backend/gl/src/device.rs b/src/backend/gl/src/device.rs index 59cc346e3e2..fc7c16e2cce 100644 --- a/src/backend/gl/src/device.rs +++ b/src/backend/gl/src/device.rs @@ -159,7 +159,7 @@ impl d::Device for Device { }) } - fn create_renderpass( + fn create_render_pass( &mut self, attachments: &[pass::Attachment], subpasses: &[pass::SubpassDesc], diff --git a/src/backend/gl/src/window/glutin.rs b/src/backend/gl/src/window/glutin.rs index 94fd14e1bb8..518f2316894 100644 --- a/src/backend/gl/src/window/glutin.rs +++ b/src/backend/gl/src/window/glutin.rs @@ -113,7 +113,8 @@ impl core::Surface for Surface { } } -impl core::Instance for Surface { +impl core::Instance for Surface { + type Backend = B; fn enumerate_adapters(&self) -> Vec { unsafe { self.window.make_current().unwrap() }; let adapter = Adapter::new(|s| self.window.get_proc_address(s) as *const _); @@ -142,7 +143,8 @@ pub fn config_context( pub struct Headless(pub glutin::HeadlessContext); -impl core::Instance for Headless { +impl core::Instance for Headless { + type Backend = B; fn enumerate_adapters(&self) -> Vec { unsafe { self.0.make_current().unwrap() }; let adapter = Adapter::new(|s| self.0.get_proc_address(s) as *const _); diff --git a/src/backend/metal/src/device.rs b/src/backend/metal/src/device.rs index e32ba1e6950..7de6f5756d9 100644 --- a/src/backend/metal/src/device.rs +++ b/src/backend/metal/src/device.rs @@ -466,7 +466,7 @@ impl core::Device for Device { &self.limits } - fn create_renderpass( + fn create_render_pass( &mut self, attachments: &[pass::Attachment], _subpasses: &[pass::SubpassDesc], diff --git a/src/backend/metal/src/lib.rs b/src/backend/metal/src/lib.rs index a17c6eca687..ccaf8fd8f96 100644 --- a/src/backend/metal/src/lib.rs +++ b/src/backend/metal/src/lib.rs @@ -39,7 +39,9 @@ use core_graphics::geometry::CGRect; pub struct Instance { } -impl core::Instance for Instance { +impl core::Instance for Instance { + type Backend = Backend; + fn enumerate_adapters(&self) -> Vec { // TODO: enumerate all devices diff --git a/src/backend/vulkan/src/device.rs b/src/backend/vulkan/src/device.rs index c8900923af8..4d6617831f9 100644 --- a/src/backend/vulkan/src/device.rs +++ b/src/backend/vulkan/src/device.rs @@ -82,7 +82,7 @@ impl d::Device for Device { Ok(n::Memory { inner: memory, ptr }) } - fn create_renderpass(&mut self, attachments: &[pass::Attachment], + fn create_render_pass(&mut self, attachments: &[pass::Attachment], subpasses: &[pass::SubpassDesc], dependencies: &[pass::SubpassDependency]) -> n::RenderPass { let map_subpass_ref = |pass: pass::SubpassRef| { diff --git a/src/backend/vulkan/src/lib.rs b/src/backend/vulkan/src/lib.rs index 48c0c12b8d0..a078c9a4b42 100644 --- a/src/backend/vulkan/src/lib.rs +++ b/src/backend/vulkan/src/lib.rs @@ -235,7 +235,9 @@ impl Instance { } } -impl core::Instance for Instance { +impl core::Instance for Instance { + type Backend = Backend; + fn enumerate_adapters(&self) -> Vec { self.raw.0.enumerate_physical_devices() .expect("Unable to enumerate adapter") diff --git a/src/hal/src/device.rs b/src/hal/src/device.rs index f51a039439f..9973896329b 100644 --- a/src/hal/src/device.rs +++ b/src/hal/src/device.rs @@ -124,7 +124,7 @@ pub trait Device: Clone { fn allocate_memory(&mut self, &MemoryType, size: u64) -> Result; /// - fn create_renderpass( + fn create_render_pass( &mut self, &[pass::Attachment], &[pass::SubpassDesc], diff --git a/src/hal/src/format.rs b/src/hal/src/format.rs index 1fa6fae6b8b..caac70be8ec 100644 --- a/src/hal/src/format.rs +++ b/src/hal/src/format.rs @@ -276,6 +276,12 @@ impl Swizzle { pub const NO: Swizzle = Swizzle(Component::R, Component::G, Component::B, Component::A); } +impl Default for Swizzle { + fn default() -> Self { + Self::NO + } +} + /// Complete run-time surface format. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[cfg_attr(feature="serialize", derive(Serialize, Deserialize))] diff --git a/src/hal/src/lib.rs b/src/hal/src/lib.rs index 2495b6481ba..c287c619cce 100644 --- a/src/hal/src/lib.rs +++ b/src/hal/src/lib.rs @@ -236,9 +236,11 @@ pub struct MemoryType { } /// Basic backend instance trait. -pub trait Instance { +pub trait Instance { + /// Associated backend type of this instance. + type Backend: Backend; /// Enumerate all available adapters. - fn enumerate_adapters(&self) -> Vec; + fn enumerate_adapters(&self) -> Vec<::Adapter>; } /// Different types of a specific API. diff --git a/src/hal/src/pass.rs b/src/hal/src/pass.rs index 9fb184d6637..1c6b00b64b6 100644 --- a/src/hal/src/pass.rs +++ b/src/hal/src/pass.rs @@ -54,6 +54,10 @@ impl AttachmentOps { store, } } + + fn whatever() -> Self { + Self::DONT_CARE + } } /// @@ -65,6 +69,7 @@ pub struct Attachment { /// Load and store operations of the attachment pub ops: AttachmentOps, /// Load and store operations of the stencil aspect, if any + #[cfg_attr(feature = "serialize", serde(default = "AttachmentOps::whatever"))] pub stencil_ops: AttachmentOps, /// Initial and final image layouts of the renderpass. pub layouts: Range, diff --git a/src/hal/src/pool.rs b/src/hal/src/pool.rs index 551b49b5f8e..aedc5305ed8 100644 --- a/src/hal/src/pool.rs +++ b/src/hal/src/pool.rs @@ -96,7 +96,7 @@ impl CommandPool { /// You can only record to one command buffer per pool at the same time. /// If more command buffers are requested than allocated, new buffers will be reserved. /// The command buffer will be returned in 'recording' state. - pub fn acquire_command_buffer<'a>(&'a mut self) -> CommandBuffer<'a, B, C> { + pub fn acquire_command_buffer(&mut self) -> CommandBuffer { self.reserve(1); let buffer = &mut self.buffers[self.next_buffer]; diff --git a/src/hal/src/pso/input_assembler.rs b/src/hal/src/pso/input_assembler.rs index 131b926f79e..4f8fbd40146 100644 --- a/src/hal/src/pso/input_assembler.rs +++ b/src/hal/src/pso/input_assembler.rs @@ -90,7 +90,7 @@ pub struct VertexBufferSet<'a, B: Backend>( impl<'a, B: Backend> VertexBufferSet<'a, B> { /// Create an empty set - pub fn new() -> VertexBufferSet<'a, B> { + pub fn new() -> Self { VertexBufferSet(Vec::new()) } } diff --git a/src/render/src/allocators/stack.rs b/src/render/src/allocators/stack.rs index b8356403572..8df1b8bfd35 100644 --- a/src/render/src/allocators/stack.rs +++ b/src/render/src/allocators/stack.rs @@ -68,7 +68,7 @@ impl Allocator for StackAllocator { device.mut_raw(), &buffer, usage); let memory_type = device.find_usage_memory(inner.usage, requirements.type_mask) .expect("could not find suitable memory"); - let mut stack = inner.stacks.entry(memory_type.id) + let stack = inner.stacks.entry(memory_type.id) .or_insert_with(|| ChunkStack::new(memory_type)); let (memory, offset, release) = stack.allocate( device, @@ -91,7 +91,7 @@ impl Allocator for StackAllocator { let requirements = device.mut_raw().get_image_requirements(&image); let memory_type = device.find_usage_memory(inner.usage, requirements.type_mask) .expect("could not find suitable memory"); - let mut stack = inner.stacks.entry(memory_type.id) + let stack = inner.stacks.entry(memory_type.id) .or_insert_with(|| ChunkStack::new(memory_type)); let (memory, offset, release) = stack.allocate( device, diff --git a/src/render/src/device.rs b/src/render/src/device.rs index 5b3083eb432..41e0978d897 100644 --- a/src/render/src/device.rs +++ b/src/render/src/device.rs @@ -414,13 +414,13 @@ impl Device { } #[doc(hidden)] - pub fn create_renderpass_raw( + pub fn create_render_pass_raw( &mut self, attachments: &[core::pass::Attachment], subpasses: &[core::pass::SubpassDesc], dependencies: &[core::pass::SubpassDependency], ) -> handle::raw::RenderPass { - let pass = self.raw.create_renderpass(attachments, subpasses, dependencies); + let pass = self.raw.create_render_pass(attachments, subpasses, dependencies); RenderPass::new(pass, (), self.garbage.clone()).into() } diff --git a/src/render/src/macros.rs b/src/render/src/macros.rs index 097f30fba9a..f1599b636d4 100644 --- a/src/render/src/macros.rs +++ b/src/render/src/macros.rs @@ -224,7 +224,7 @@ macro_rules! gfx_graphics_pipeline { preserves: &[], }; - device.create_renderpass_raw(&attachments[..], &[subpass], &[]) + device.create_render_pass_raw(&attachments[..], &[subpass], &[]) }; let mut pipeline_desc = cpso::GraphicsPipelineDesc::new( diff --git a/src/warden/Cargo.toml b/src/warden/Cargo.toml new file mode 100644 index 00000000000..cbce76b78ee --- /dev/null +++ b/src/warden/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "gfx-warden" +version = "0.1.0" +description = "gfx-rs reftest framework" +homepage = "https://github.com/gfx-rs/gfx" +repository = "https://github.com/gfx-rs/gfx" +keywords = ["graphics", "gamedev"] +license = "Apache-2.0" +authors = ["The Gfx-rs Developers"] +readme = "../../README.md" +documentation = "https://docs.rs/gfx-render" +categories = ["rendering::graphics-api"] +workspace = "../.." + +[lib] +name = "gfx_warden" +path = "src/lib.rs" + +[features] +default = [] +logger = ["env_logger"] +vulkan = ["gfx-backend-vulkan"] +dx12 = ["gfx-backend-dx12"] +metal = ["gfx-backend-metal"] + +#TODO: keep Warden backend-agnostic? + +[dependencies] +gfx-hal = { path = "../hal", version = "0.1", features = ["serialize"] } +log = "0.3" +ron = "0.1" +serde = { version = "1.0", features = ["serde_derive"] } +env_logger = { version = "0.4", optional = true } + +[dependencies.gfx-backend-vulkan] +path = "../../src/backend/vulkan" +version = "0.1" +optional = true + +[target.'cfg(windows)'.dependencies.gfx-backend-dx12] +path = "../../src/backend/dx12" +version = "0.1" +optional = true + +[target.'cfg(target_os = "macos")'.dependencies.gfx-backend-metal] +path = "../../src/backend/metal" +version = "0.1" +optional = true diff --git a/src/warden/README.md b/src/warden/README.md new file mode 100644 index 00000000000..7bcb0aa2991 --- /dev/null +++ b/src/warden/README.md @@ -0,0 +1,23 @@ +# Warden + +Warden is the data-driven reference test framework for gfx-rs Hardware Abstraction Layer (`gfx-hal`), heavily inspired by the Wrench component of [WebRender](https://github.com/servo/webrender/). Warden's main purpose is to run a suite of GPU workloads on all native backends supported by the host platform, then match the results against provided expectations. Both the workloads and expectations are backend-agnostic. The backend discovery and initialization is done by the `reftest` binary. All that needs to be done by a developer is typing `make reftests` from the project root and ensuring that every test passes. + +Warden has two types of definitions: scene and suite. Both are written in [Ron](https://github.com/ron-rs/ron) format, but technically the code should work with any `serde`-enabled format given minimal tweaking. + +## Scene definition + +A scene consists of a number of resources and jobs that can be run on them. Resources are buffers, images, render passes, and so on. Jobs are sets of either transfer, compute, or graphics operations. The latter is contained within a single render pass. Please refer to [raw.rs](src/raw.rs) for the formal definition of the scene format. Actual reference scenes can be found in [reftests](../../reftests/scenes). + +### Resource states + +Internally, a scene has a command buffer to fill up all the initial data for resources. This command buffer needs to change the resource access and image layouts, so we establish a convention here by which every resource has an associated "stable" state that the user (and the reftest framework) promises to deliver at the end of each job. + +For images with no source data, the stable layout is `ColorAttachmentOptimal` or `DepthStencilAttachmentOptimal` depending on the format. For sourced images, it's `ShaderReadOnlyOptimal`. + +## Test suite + +A test suite is just a set of scenes, each with multiple tests. A test is defined as a sequence of jobs being run on the scene and an expectation result. The central suite file can be found in [reftests](../../reftests/suite.ron), and the serialization structures are in [reftest.rs](src/bin/reftest.rs). + +## Warning + +This gfx-rs component is heavy WIP, provided under no warranty! There is a lot of logic missing, especially with regards to error reporting. diff --git a/src/warden/examples/basic.rs b/src/warden/examples/basic.rs new file mode 100644 index 00000000000..d18cb137f02 --- /dev/null +++ b/src/warden/examples/basic.rs @@ -0,0 +1,41 @@ +#[cfg(feature = "vulkan")] +extern crate gfx_backend_vulkan as back; +extern crate gfx_hal as hal; +extern crate gfx_warden as warden; +extern crate ron; +extern crate serde; + +use std::fs::File; +use std::io::Read; + +use hal::Instance; +use ron::de::Deserializer; +use serde::de::Deserialize; + + +fn main() { + let raw_scene = { + let path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../reftests/scenes/basic.ron", + ); + let mut raw_data = Vec::new(); + File::open(path) + .unwrap() + .read_to_end(&mut raw_data) + .unwrap(); + let mut deserializer = Deserializer::from_bytes(&raw_data); + warden::raw::Scene::deserialize(&mut deserializer) + .unwrap() + }; + + #[cfg(feature = "vulkan")] + { + let instance = back::Instance::create("warden", 1); + let adapters = instance.enumerate_adapters(); + let mut scene = warden::gpu::Scene::::new(&adapters[0], &raw_scene, ""); + scene.run(Some("empty")); + let guard = scene.fetch_image("im-color"); + println!("row: {:?}", guard.row(0)); + } +} diff --git a/src/warden/src/bin/reftest.rs b/src/warden/src/bin/reftest.rs new file mode 100644 index 00000000000..1e5fd7115fd --- /dev/null +++ b/src/warden/src/bin/reftest.rs @@ -0,0 +1,120 @@ +#![cfg_attr(not(any(feature = "vulkan", feature = "dx12", feature = "metal")), allow(dead_code))] + +extern crate gfx_hal as hal; +extern crate gfx_warden as warden; +extern crate ron; +#[macro_use] +extern crate serde; + +#[cfg(feature = "logger")] +extern crate env_logger; +#[cfg(feature = "vulkan")] +extern crate gfx_backend_vulkan; +#[cfg(feature = "dx12")] +extern crate gfx_backend_dx12; +#[cfg(feature = "metal")] +extern crate gfx_backend_metal; + +use std::collections::HashMap; +use std::fs::File; + +use ron::de; + + +#[derive(Debug, Deserialize)] +enum Expectation { + ImageRow(String, usize, Vec), +} + +#[derive(Debug, Deserialize)] +struct Test { + jobs: Vec, + expect: Expectation, +} + +type Suite = HashMap>; + + +struct Harness { + base_path: &'static str, + suite: Suite, +} + +impl Harness { + fn new(suite_name: &str) -> Self { + let base_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../reftests", + ); + let suite = File::open(format!("{}/{}.ron", base_path, suite_name)) + .map_err(de::Error::from) + .and_then(de::from_reader) + .expect("failed to parse the suite definition"); + Harness { + base_path, + suite, + } + } + + fn run(&self, instance: I) { + use hal::Adapter; + + let adapters = instance.enumerate_adapters(); + let adapter = &adapters[0]; + println!("\t{:?}", adapter.get_info()); + + for (scene_name, tests) in &self.suite { + println!("\tLoading scene '{}':", scene_name); + let raw_scene = File::open(format!("{}/scenes/{}.ron", self.base_path, scene_name)) + .map_err(de::Error::from) + .and_then(de::from_reader) + .expect("failed to open/parse the scene"); + + let data_path = format!("{}/data", self.base_path); + let mut scene = warden::gpu::Scene::::new(adapter, &raw_scene, &data_path); + + for (test_name, test) in tests { + print!("\t\tTest '{}' ...", test_name); + scene.run(test.jobs.iter().map(|x| x.as_str())); + + print!("\tran: "); + match test.expect { + Expectation::ImageRow(ref image, row, ref data) => { + let guard = scene.fetch_image(image); + if data.as_slice() == guard.row(row) { + println!("PASS"); + } else { + println!("FAIL {:?}", guard.row(row)); + } + } + } + } + } + } +} + +fn main() { + #[cfg(feature = "logger")] + env_logger::init().unwrap(); + + let harness = Harness::new("suite"); + #[cfg(feature = "vulkan")] + { + println!("Warding Vulkan:"); + let instance = gfx_backend_vulkan::Instance::create("warden", 1); + harness.run(instance); + } + #[cfg(feature = "dx12")] + { + println!("Warding DX12:"); + let instance = gfx_backend_dx12::Instance::create("warden", 1); + harness.run(instance); + } + #[cfg(feature = "metal")] + { + println!("Warding Metal:"); + let instance = gfx_backend_metal::Instance::create("warden", 1); + harness.run(instance); + } + let _ = harness; +} diff --git a/src/warden/src/gpu.rs b/src/warden/src/gpu.rs new file mode 100644 index 00000000000..c43601b03aa --- /dev/null +++ b/src/warden/src/gpu.rs @@ -0,0 +1,592 @@ +use std::collections::HashMap; +use std::io::Read; +use std::fs::File; +use std::slice; + +use hal::{self, image as i}; +use hal::{Adapter, Device, DescriptorPool}; + +use raw; + + +const COLOR_RANGE: i::SubresourceRange = i::SubresourceRange { + aspects: i::ASPECT_COLOR, + levels: 0 .. 1, + layers: 0 .. 1, +}; + +pub struct FetchGuard<'a, B: hal::Backend> { + device: &'a mut B::Device, + buffer: Option, + memory: Option, + mapping: *const u8, + row_pitch: usize, + width: usize, +} + +impl<'a, B: hal::Backend> FetchGuard<'a, B> { + pub fn row(&self, i: usize) -> &[u8] { + let offset = (i * self.row_pitch) as isize; + unsafe { + slice::from_raw_parts(self.mapping.offset(offset), self.width) + } + } +} + +impl<'a, B: hal::Backend> Drop for FetchGuard<'a, B> { + fn drop(&mut self) { + let buffer = self.buffer.take().unwrap(); + let memory = self.memory.take().unwrap(); + self.device.release_mapping_raw(&buffer, None); + self.device.destroy_buffer(buffer); + self.device.free_memory(memory); + } +} + +pub struct Image { + pub handle: B::Image, + #[allow(dead_code)] + memory: B::Memory, + kind: i::Kind, + format: hal::format::Format, + stable_state: i::State, +} + +pub struct RenderPass { + pub handle: B::RenderPass, + attachments: Vec, + subpasses: Vec, +} + +pub struct Resources { + pub buffers: HashMap, + pub images: HashMap>, + pub image_views: HashMap, + pub render_passes: HashMap>, + pub framebuffers: HashMap, + pub desc_set_layouts: HashMap, + pub desc_pools: HashMap, + pub desc_sets: HashMap, + pub pipeline_layouts: HashMap, +} + +pub struct Scene { + pub resources: Resources, + pub jobs: HashMap>, + init_submit: Option>, + device: B::Device, + queue: hal::CommandQueue, + command_pool: hal::CommandPool, + upload_buffers: HashMap, + download_type: hal::MemoryType, +} + +fn align(x: usize, y: usize) -> usize { + if x > 0 && y > 0 { + ((x - 1) | (y - 1)) + 1 + } else { + x + } +} + +impl Scene { + pub fn new(adapter: &B::Adapter, raw: &raw::Scene, data_path: &str) -> Self { + info!("creating Scene from {}", data_path); + // initialize graphics + let hal::Gpu { mut device, mut graphics_queues, memory_types, .. } = { + let (ref family, queue_type) = adapter.get_queue_families()[0]; + assert!(queue_type.supports_graphics()); + adapter.open(&[(family, hal::QueueType::Graphics, 1)]) + }; + let upload_type = memory_types + .iter() + .find(|mt| { + mt.properties.contains(hal::memory::CPU_VISIBLE) + //&&!mt.properties.contains(hal::memory::CPU_CACHED) + }) + .unwrap(); + let download_type = memory_types + .iter() + .find(|mt| { + mt.properties.contains(hal::memory::CPU_VISIBLE | hal::memory::CPU_CACHED) + }) + .unwrap() + .clone(); + info!("upload memory: {:?}", upload_type); + info!("download memory: {:?}", &download_type); + + let limits = device.get_limits().clone(); + let queue = graphics_queues.remove(0); + let mut command_pool = queue.create_graphics_pool( + 1 + raw.jobs.len(), + hal::pool::CommandPoolCreateFlags::empty(), + ); + + // create resources + let mut resources = Resources { + buffers: HashMap::new(), + images: HashMap::new(), + image_views: HashMap::new(), + render_passes: HashMap::new(), + framebuffers: HashMap::new(), + desc_set_layouts: HashMap::new(), + desc_pools: HashMap::new(), + desc_sets: HashMap::new(), + pipeline_layouts: HashMap::new(), + }; + let mut upload_buffers = HashMap::new(); + let init_submit = { + let mut init_cmd = command_pool.acquire_command_buffer(); + + // Pass[1]: images, buffers, passes, descriptor set layouts/pools + for (name, resource) in &raw.resources { + match *resource { + raw::Resource::Buffer => { + } + raw::Resource::Image { kind, num_levels, format, usage, ref data } => { + let unbound = device.create_image(kind, num_levels, format, usage) + .unwrap(); + let requirements = device.get_image_requirements(&unbound); + let memory_type = memory_types + .iter() + .find(|mt| { + requirements.type_mask & (1 << mt.id) != 0 && + mt.properties.contains(hal::memory::DEVICE_LOCAL) + }) + .unwrap(); + let memory = device.allocate_memory(memory_type, requirements.size) + .unwrap(); + let image = device.bind_image_memory(&memory, 0, unbound) + .unwrap(); + let bits = format.0.describe_bits(); + + // process initial data for the image + let stable_state = if data.is_empty() { + let (aspects, access, layout) = if bits.color != 0 { + (i::ASPECT_COLOR, i::COLOR_ATTACHMENT_WRITE, i::ImageLayout::ColorAttachmentOptimal) + } else { + (i::ASPECT_DEPTH | i::ASPECT_STENCIL, i::DEPTH_STENCIL_ATTACHMENT_WRITE, i::ImageLayout::DepthStencilAttachmentOptimal) + }; + if false { //TODO + let image_barrier = hal::memory::Barrier::Image { + states: (i::Access::empty(), i::ImageLayout::Undefined) .. (access, layout), + target: &image, + range: i::SubresourceRange { + aspects, + .. COLOR_RANGE.clone() + }, + }; + init_cmd.pipeline_barrier(hal::pso::TOP_OF_PIPE .. hal::pso::BOTTOM_OF_PIPE, &[image_barrier]); + } + (access, layout) + } else { + // calculate required sizes + let (w, h, d, aa) = kind.get_dimensions(); + assert_eq!(aa, i::AaMode::Single); + let width_bytes = bits.total as usize * w as usize / 8; + let row_pitch = align(width_bytes, limits.min_buffer_copy_pitch_alignment); + let upload_size = row_pitch as u64 * h as u64 * d as u64; + // create upload buffer + let unbound_buffer = device.create_buffer(upload_size, bits.total as _, hal::buffer::TRANSFER_SRC) + .unwrap(); + let upload_req = device.get_buffer_requirements(&unbound_buffer); + assert_ne!(upload_req.type_mask & (1<(&upload_buffer, 0..upload_size) + .unwrap(); + for y in 0 .. (h as usize * d as usize) { + let dest_range = y as usize * row_pitch .. y as usize * row_pitch + width_bytes; + file.read_exact(&mut mapping[dest_range]) + .unwrap(); + } + device.release_mapping_writer(mapping); + } + // add init commands + let final_state = (i::SHADER_READ, i::ImageLayout::ShaderReadOnlyOptimal); + let image_barrier = hal::memory::Barrier::Image { + states: (i::Access::empty(), i::ImageLayout::Undefined) .. + (i::TRANSFER_WRITE, i::ImageLayout::TransferDstOptimal), + target: &image, + range: COLOR_RANGE.clone(), //TODO + }; + init_cmd.pipeline_barrier(hal::pso::TOP_OF_PIPE .. hal::pso::TRANSFER, &[image_barrier]); + init_cmd.copy_buffer_to_image( + &upload_buffer, + &image, + i::ImageLayout::TransferDstOptimal, + &[hal::command::BufferImageCopy { + buffer_offset: 0, + buffer_row_pitch: row_pitch as u32, + buffer_slice_pitch: row_pitch as u32 * h as u32, + image_layers: i::SubresourceLayers { + aspects: i::ASPECT_COLOR, + level: 0, + layers: 0 .. 1, + }, + image_offset: hal::command::Offset { x: 0, y: 0, z: 0 }, + image_extent: hal::device::Extent { + width: w as _, + height: h as _, + depth: d as _, + }, + }]); + let image_barrier = hal::memory::Barrier::Image { + states: (i::TRANSFER_WRITE, i::ImageLayout::TransferDstOptimal) .. final_state, + target: &image, + range: COLOR_RANGE.clone(), //TODO + }; + init_cmd.pipeline_barrier(hal::pso::TRANSFER .. hal::pso::BOTTOM_OF_PIPE, &[image_barrier]); + // done + upload_buffers.insert(name.clone(), (upload_buffer, upload_memory)); + final_state + }; + + resources.images.insert(name.clone(), Image { + handle: image, + memory, + kind, + format, + stable_state, + }); + } + raw::Resource::RenderPass { ref attachments, ref subpasses, ref dependencies } => { + let att_ref = |aref: &raw::AttachmentRef| { + let id = attachments.keys().position(|s| s == &aref.0).unwrap(); + (id, aref.1) + }; + let subpass_ref = |name: &String| { + if name.is_empty() { + hal::pass::SubpassRef::External + } else { + let id = subpasses.keys().position(|s| s == name).unwrap(); + hal::pass::SubpassRef::Pass(id) + } + }; + + let raw_atts = attachments + .values() + .cloned() + .collect::>(); + let temp = subpasses + .values() + .map(|sp| { + let colors = sp.colors + .iter() + .map(&att_ref) + .collect::>(); + let ds = sp.depth_stencil + .as_ref() + .map(&att_ref); + let inputs = sp.inputs + .iter() + .map(&att_ref) + .collect::>(); + let preserves = sp.preserves + .iter() + .map(|name| { + attachments.keys().position(|s| s == name).unwrap() + }) + .collect::>(); + (colors, ds, inputs, preserves) + }) + .collect::>(); + let raw_subs = temp + .iter() + .map(|t| hal::pass::SubpassDesc { + colors: &t.0, + depth_stencil: t.1.as_ref(), + inputs: &t.2, + preserves: &t.3, + }) + .collect::>(); + let raw_deps = dependencies + .iter() + .map(|dep| hal::pass::SubpassDependency { + passes: subpass_ref(&dep.passes.start) .. subpass_ref(&dep.passes.end), + stages: dep.stages.clone(), + accesses: dep.accesses.clone(), + }) + .collect::>(); + + let rp = RenderPass { + handle: device.create_render_pass(&raw_atts, &raw_subs, &raw_deps), + attachments: attachments.keys().cloned().collect(), + subpasses: subpasses.keys().cloned().collect(), + }; + resources.render_passes.insert(name.clone(), rp); + } + raw::Resource::DescriptorSetLayout { ref bindings } => { + let layout = device.create_descriptor_set_layout(bindings); + resources.desc_set_layouts.insert(name.clone(), layout); + } + raw::Resource::DescriptorPool { capacity, ref ranges } => { + let pool = device.create_descriptor_pool(capacity, ranges); + resources.desc_pools.insert(name.clone(), pool); + } + _ => {} + } + } + + // Pass[2]: image & buffer views, descriptor sets, pipeline layouts + for (name, resource) in &raw.resources { + match *resource { + raw::Resource::ImageView { ref image, format, swizzle, ref range } => { + let image = &resources.images[image].handle; + let view = device.create_image_view(image, format, swizzle, range.clone()) + .unwrap(); + resources.image_views.insert(name.clone(), view); + } + raw::Resource::DescriptorSet { ref pool, ref layout } => { + let set_layout = &resources.desc_set_layouts[layout]; + let dest_pool: &mut B::DescriptorPool = resources.desc_pools + .get_mut(pool) + .unwrap(); + let set = dest_pool + .allocate_sets(&[set_layout]) + .pop() + .unwrap(); + resources.desc_sets.insert(name.clone(), set); + } + raw::Resource::PipelineLayout { ref set_layouts } => { + let layout = { + let layouts = set_layouts + .iter() + .map(|sl| &resources.desc_set_layouts[sl]) + .collect::>(); + device.create_pipeline_layout(&layouts) + }; + resources.pipeline_layouts.insert(name.clone(), layout); + } + _ => {} + } + } + + // Pass[3]: framebuffers + for (name, resource) in &raw.resources { + match *resource { + raw::Resource::Framebuffer { ref pass, ref views, extent } => { + let rp = &resources.render_passes[pass]; + let framebuffer = { + let image_views = rp.attachments + .iter() + .map(|name| { + let entry = views + .iter() + .find(|entry| entry.0 == name) + .unwrap(); + &resources.image_views[entry.1] + }) + .collect::>(); + device.create_framebuffer(&rp.handle, &image_views, extent) + .unwrap() + }; + resources.framebuffers.insert(name.clone(), (framebuffer, extent)); + } + _ => {} + } + } + + Some(init_cmd.finish()) + }; + + // fill up command buffers + let mut jobs = HashMap::new(); + for (name, job) in &raw.jobs { + let mut command_buf = command_pool.acquire_command_buffer(); + match *job { + raw::Job::Transfer { ref commands } => { + use raw::TransferCommand as Tc; + for command in commands { + match *command { + //TODO + Tc::CopyBufferToImage => {} + } + } + } + raw::Job::Graphics { ref descriptors, ref framebuffer, ref pass, ref clear_values } => { + let _ = descriptors; //TODO + let (ref fb, extent) = resources.framebuffers[framebuffer]; + let rp = &resources.render_passes[&pass.0]; + let rect = hal::target::Rect { + x: 0, + y: 0, + w: extent.width as _, + h: extent.height as _, + }; + let mut encoder = command_buf.begin_renderpass_inline(&rp.handle, fb, rect, clear_values); + for subpass in &rp.subpasses { + if Some(subpass) != rp.subpasses.first() { + encoder = encoder.next_subpass_inline(); + } + for command in &pass.1[subpass].commands { + use raw::DrawCommand as Dc; + match *command { + Dc::BindIndexBuffer { ref buffer, offset, index_type } => { + let view = hal::buffer::IndexBufferView { + buffer: &resources.buffers[buffer].0, + offset, + index_type, + }; + encoder.bind_index_buffer(view); + } + Dc::BindVertexBuffers(ref buffers) => { + let buffers_raw = buffers + .iter() + .map(|&(ref name, offset)| { + (&resources.buffers[name].0, offset) + }) + .collect::>(); + let set = hal::pso::VertexBufferSet(buffers_raw); + encoder.bind_vertex_buffers(set); + } + Dc::BindPipeline(_) => { + unimplemented!() + } + Dc::BindDescriptorSets { .. } => { //ref layout, first, ref sets + unimplemented!() + } + Dc::Draw { ref vertices, ref instances } => { + encoder.draw(vertices.clone(), instances.clone()); + } + Dc::DrawIndexed { ref indices, base_vertex, ref instances } => { + encoder.draw_indexed(indices.clone(), base_vertex, instances.clone()); + } + } + } + } + } + } + jobs.insert(name.clone(), command_buf.finish()); + } + + // done + Scene { + resources, + jobs, + init_submit, + device, + queue, + command_pool, + upload_buffers, + download_type, + } + } +} + +impl Scene { + pub fn run<'a, I>(&mut self, jobs: I) + where + I: IntoIterator + { + //TODO: re-use submits! + let values = jobs.into_iter() + .map(|name| self.jobs.remove(name).unwrap()) + .collect::>(); + let submission = hal::queue::Submission::new() + .submit(&[self.init_submit.take().unwrap()]) + .submit(&values); + self.queue.submit(submission, None); + } + + pub fn fetch_image(&mut self, name: &str) -> FetchGuard { + let image = &self.resources.images[name]; + let limits = self.device.get_limits().clone(); + + let (width, height, depth, aa) = image.kind.get_dimensions(); + assert_eq!(aa, i::AaMode::Single); + let bpp = image.format.0.describe_bits().total as usize; + let width_bytes = bpp * width as usize / 8; + let row_pitch = align(width_bytes, limits.min_buffer_copy_pitch_alignment); + let down_size = row_pitch as u64 * height as u64 * depth as u64; + + let unbound_buffer = self.device.create_buffer(down_size, bpp as _, hal::buffer::TRANSFER_DST) + .unwrap(); + let down_req = self.device.get_buffer_requirements(&unbound_buffer); + assert_ne!(down_req.type_mask & (1< Drop for Scene { + fn drop(&mut self) { + for (_, (buffer, memory)) in self.upload_buffers.drain() { + self.device.destroy_buffer(buffer); + self.device.free_memory(memory); + } + //TODO: free those properly + let _ = &self.queue; + let _ = &self.command_pool; + //queue.destroy_command_pool(command_pool); + } +} diff --git a/src/warden/src/lib.rs b/src/warden/src/lib.rs new file mode 100644 index 00000000000..fc16918a8a3 --- /dev/null +++ b/src/warden/src/lib.rs @@ -0,0 +1,11 @@ +//! Data-driven reference test framework for warding +//! against breaking changes. + +extern crate gfx_hal as hal; +#[macro_use] +extern crate log; +#[macro_use] +extern crate serde; + +pub mod gpu; +pub mod raw; diff --git a/src/warden/src/raw.rs b/src/warden/src/raw.rs new file mode 100644 index 00000000000..098ab017d8b --- /dev/null +++ b/src/warden/src/raw.rs @@ -0,0 +1,131 @@ +use std::collections::HashMap; +use std::ops::Range; + +use hal; + + +#[derive(Debug, Deserialize)] +pub struct AttachmentRef(pub String, pub hal::pass::AttachmentLayout); + +#[derive(Debug, Deserialize)] +pub struct Subpass { + pub colors: Vec, + pub depth_stencil: Option, + #[serde(default)] + pub inputs: Vec, + #[serde(default)] + pub preserves: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct SubpassDependency { + pub passes: Range, + pub stages: Range, + pub accesses: Range, +} + +#[derive(Debug, Deserialize)] +pub enum Resource { + Shader, + Buffer, + Image { + kind: hal::image::Kind, + num_levels: hal::image::Level, + format: hal::format::Format, + usage: hal::image::Usage, + #[serde(default)] + data: String, + }, + ImageView { + image: String, + format: hal::format::Format, + #[serde(default)] + swizzle: hal::format::Swizzle, + range: hal::image::SubresourceRange, + }, + RenderPass { + attachments: HashMap, + subpasses: HashMap, + dependencies: Vec, + }, + DescriptorSetLayout { + bindings: Vec, + }, + DescriptorPool { + capacity: usize, + ranges: Vec, + }, + DescriptorSet { + pool: String, + layout: String, + }, + PipelineLayout { + set_layouts: Vec, + }, + GraphicsPipeline, + Framebuffer { + pass: String, + views: HashMap, + extent: hal::device::Extent, + }, +} + +#[derive(Debug, Deserialize)] +pub enum TransferCommand { + CopyBufferToImage, + //CopyImageToBuffer, +} + +#[derive(Debug, Deserialize)] +pub struct DescriptorSetData { + //TODO: update_descriptor_sets +} + +#[derive(Debug, Deserialize)] +pub enum DrawCommand { + BindIndexBuffer { + buffer: String, + offset: u64, + index_type: hal::IndexType, + }, + BindVertexBuffers(Vec<(String, hal::pso::BufferOffset)>), + BindPipeline(String), + BindDescriptorSets { + layout: String, + first: usize, + sets: Vec, + }, + Draw { + vertices: Range, + instances: Range, + }, + DrawIndexed { + indices: Range, + base_vertex: hal::VertexOffset, + instances: Range, + }, +} + +#[derive(Debug, Deserialize)] +pub struct DrawPass { + pub commands: Vec, +} + +#[derive(Debug, Deserialize)] +pub enum Job { + Transfer { + commands: Vec, + }, + Graphics { + descriptors: HashMap, + framebuffer: String, + clear_values: Vec, + pass: (String, HashMap), + }, +} + +#[derive(Debug, Deserialize)] +pub struct Scene { + pub resources: HashMap, + pub jobs: HashMap, +}