| // Copyright 2023 The Dawn & Tint Authors |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, this |
| // list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // 3. Neither the name of the copyright holder nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| #include <string> |
| #include <vector> |
| |
| #include "dawn/tests/DawnTest.h" |
| #include "dawn/utils/ComboRenderPipelineDescriptor.h" |
| #include "dawn/utils/WGPUHelpers.h" |
| |
| namespace dawn { |
| namespace { |
| |
| class FramebufferFetchTests : public DawnTest { |
| protected: |
| void SetUp() override { |
| DawnTest::SetUp(); |
| DAWN_TEST_UNSUPPORTED_IF(!device.HasFeature(wgpu::FeatureName::FramebufferFetch)); |
| } |
| |
| std::vector<wgpu::FeatureName> GetRequiredFeatures() override { |
| std::vector<wgpu::FeatureName> requiredFeatures = {}; |
| if (SupportsFeatures({wgpu::FeatureName::FramebufferFetch})) { |
| requiredFeatures.push_back(wgpu::FeatureName::FramebufferFetch); |
| } |
| return requiredFeatures; |
| } |
| |
| void InitForSinglePoint(utils::ComboRenderPipelineDescriptor* desc) { |
| desc->vertex.module = utils::CreateShaderModule(device, R"( |
| @vertex fn vs() -> @builtin(position) vec4f { |
| return vec4f(0, 0, 0, 1); |
| } |
| )"); |
| desc->primitive.topology = wgpu::PrimitiveTopology::PointList; |
| } |
| |
| wgpu::Texture MakeAttachment(wgpu::TextureFormat format, |
| wgpu::Extent3D size = {1, 1, 1}, |
| uint32_t sampleCount = 1) { |
| wgpu::TextureDescriptor tDesc; |
| tDesc.size = size; |
| tDesc.sampleCount = sampleCount; |
| tDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc | |
| wgpu::TextureUsage::CopyDst; |
| tDesc.format = format; |
| return device.CreateTexture(&tDesc); |
| } |
| |
| std::string kExt = "enable chromium_experimental_framebuffer_fetch;\n"; |
| }; |
| |
| // A basic test of framebuffer fetch |
| TEST_P(FramebufferFetchTests, Basic) { |
| // Pipeline that draw a point at the center that adds one to the attachment. |
| utils::ComboRenderPipelineDescriptor pDesc; |
| InitForSinglePoint(&pDesc); |
| pDesc.cFragment.module = utils::CreateShaderModule(device, kExt + R"( |
| @fragment fn main(@color(0) in : u32) -> @location(0) u32 { |
| return in + 1; |
| } |
| )"); |
| pDesc.cTargets[0].format = wgpu::TextureFormat::R32Uint; |
| wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pDesc); |
| |
| wgpu::Texture texture = MakeAttachment(wgpu::TextureFormat::R32Uint); |
| |
| // Draw 10 points with the pipeline on the render target. |
| utils::ComboRenderPassDescriptor passDesc({texture.CreateView()}); |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc); |
| pass.SetPipeline(pipeline); |
| pass.Draw(10); |
| pass.End(); |
| |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| // The 10 points should have successfully used framebuffer fetch to do increment ten times |
| // without races. |
| EXPECT_TEXTURE_EQ(uint32_t(10), texture, {0, 0}); |
| } |
| |
| // Check that it is post-blend framebuffer fetch |
| TEST_P(FramebufferFetchTests, PostBlend) { |
| // Pipeline that draw a point at the center that outputs the current attachment value with |
| // additive blend, so multiplies by two. |
| utils::ComboRenderPipelineDescriptor pDesc; |
| InitForSinglePoint(&pDesc); |
| pDesc.cFragment.module = utils::CreateShaderModule(device, kExt + R"( |
| @fragment fn main(@color(0) in : vec4f) -> @location(0) vec4f { |
| return in; |
| } |
| )"); |
| pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm; |
| pDesc.cTargets[0].blend = &pDesc.cBlends[0]; |
| pDesc.cBlends[0].color.srcFactor = wgpu::BlendFactor::One; |
| pDesc.cBlends[0].color.dstFactor = wgpu::BlendFactor::One; |
| pDesc.cBlends[0].color.operation = wgpu::BlendOperation::Add; |
| pDesc.cBlends[0].alpha = pDesc.cBlends[0].color; |
| wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pDesc); |
| |
| wgpu::Texture texture = MakeAttachment(wgpu::TextureFormat::RGBA8Unorm); |
| |
| // Starting with small value, then do *2 4 times. |
| utils::ComboRenderPassDescriptor passDesc({texture.CreateView()}); |
| passDesc.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear; |
| passDesc.cColorAttachments[0].clearValue = {1 / 255.0, 2 / 255.0, 3 / 255.0, 4 / 255.0}; |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc); |
| pass.SetPipeline(pipeline); |
| pass.Draw(4); |
| pass.End(); |
| |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| // The 10 points should have successfully used framebuffer fetch to do increment ten times |
| // without races. |
| EXPECT_TEXTURE_EQ(utils::RGBA8(16, 32, 48, 64), texture, {0, 0}); |
| } |
| |
| // Check with multiple render attachments |
| TEST_P(FramebufferFetchTests, MultipleAttachments) { |
| // Pipeline that draw a point at the center that adds 0/1/2/3 to multiple attachments. |
| utils::ComboRenderPipelineDescriptor pDesc; |
| InitForSinglePoint(&pDesc); |
| pDesc.cFragment.module = utils::CreateShaderModule(device, kExt + R"( |
| struct Out { |
| @location(0) out0 : u32, |
| @location(1) out1 : u32, |
| @location(2) out2 : u32, |
| @location(3) out3 : u32, |
| } |
| @fragment fn main( |
| @color(0) in0 : u32, |
| @color(1) in1 : u32, |
| @color(2) in2 : u32, |
| @color(3) in3 : u32, |
| ) -> Out { |
| var out : Out; |
| out.out0 = in0 + 0; |
| out.out1 = in1 + 1; |
| out.out2 = in2 + 2; |
| out.out3 = in3 + 3; |
| return out; |
| } |
| )"); |
| pDesc.cFragment.targetCount = 4; |
| pDesc.cTargets[0].format = wgpu::TextureFormat::R32Uint; |
| pDesc.cTargets[1].format = wgpu::TextureFormat::R32Uint; |
| pDesc.cTargets[2].format = wgpu::TextureFormat::R32Uint; |
| pDesc.cTargets[3].format = wgpu::TextureFormat::R32Uint; |
| wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pDesc); |
| |
| wgpu::Texture texture0 = MakeAttachment(wgpu::TextureFormat::R32Uint); |
| wgpu::Texture texture1 = MakeAttachment(wgpu::TextureFormat::R32Uint); |
| wgpu::Texture texture2 = MakeAttachment(wgpu::TextureFormat::R32Uint); |
| wgpu::Texture texture3 = MakeAttachment(wgpu::TextureFormat::R32Uint); |
| |
| // Draw 10 points with the pipeline on the render target. |
| utils::ComboRenderPassDescriptor passDesc({ |
| texture0.CreateView(), |
| texture1.CreateView(), |
| texture2.CreateView(), |
| texture3.CreateView(), |
| }); |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc); |
| pass.SetPipeline(pipeline); |
| pass.Draw(10); |
| pass.End(); |
| |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| // The 10 points should have successfully used framebuffer fetch to do increment ten times |
| // without races. |
| EXPECT_TEXTURE_EQ(uint32_t(0), texture0, {0, 0}); |
| EXPECT_TEXTURE_EQ(uint32_t(10), texture1, {0, 0}); |
| EXPECT_TEXTURE_EQ(uint32_t(20), texture2, {0, 0}); |
| EXPECT_TEXTURE_EQ(uint32_t(30), texture3, {0, 0}); |
| } |
| |
| // Check with multiple render attachments |
| TEST_P(FramebufferFetchTests, VariousFormats) { |
| // Pipeline that draw a point at the center that adds / substracts one to attachment of various |
| // formats. |
| utils::ComboRenderPipelineDescriptor pDesc; |
| InitForSinglePoint(&pDesc); |
| pDesc.cFragment.module = utils::CreateShaderModule(device, kExt + R"( |
| struct Out { |
| @location(0) out0 : u32, |
| @location(1) out1 : i32, |
| @location(2) out2 : f32, |
| @location(3) out3 : vec4f, |
| } |
| @fragment fn main( |
| @color(0) in0 : u32, |
| @color(1) in1 : i32, |
| @color(2) in2 : f32, |
| @color(3) in3 : vec4f, |
| ) -> Out { |
| var out : Out; |
| out.out0 = in0 + 1; |
| out.out1 = in1 - 1; |
| out.out2 = in2 + 1; |
| out.out3 = in3 + vec4f(1 / 255.0); |
| return out; |
| } |
| )"); |
| pDesc.cFragment.targetCount = 4; |
| pDesc.cTargets[0].format = wgpu::TextureFormat::R32Uint; |
| pDesc.cTargets[1].format = wgpu::TextureFormat::R32Sint; |
| pDesc.cTargets[2].format = wgpu::TextureFormat::R32Float; |
| pDesc.cTargets[3].format = wgpu::TextureFormat::RGBA8Unorm; |
| wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pDesc); |
| |
| wgpu::Texture texture0 = MakeAttachment(wgpu::TextureFormat::R32Uint); |
| wgpu::Texture texture1 = MakeAttachment(wgpu::TextureFormat::R32Sint); |
| wgpu::Texture texture2 = MakeAttachment(wgpu::TextureFormat::R32Float); |
| wgpu::Texture texture3 = MakeAttachment(wgpu::TextureFormat::RGBA8Unorm); |
| |
| // Draw 10 points with the pipeline on the render target. |
| utils::ComboRenderPassDescriptor passDesc({ |
| texture0.CreateView(), |
| texture1.CreateView(), |
| texture2.CreateView(), |
| texture3.CreateView(), |
| }); |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc); |
| pass.SetPipeline(pipeline); |
| pass.Draw(10); |
| pass.End(); |
| |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| // The 10 points should have successfully used framebuffer fetch to do in/decrement ten times |
| // without races. |
| EXPECT_TEXTURE_EQ(uint32_t(10), texture0, {0, 0}); |
| EXPECT_TEXTURE_EQ(int32_t(-10), texture1, {0, 0}); |
| EXPECT_TEXTURE_EQ(static_cast<float>(10), texture2, {0, 0}); |
| EXPECT_TEXTURE_EQ(utils::RGBA8(10, 10, 10, 10), texture3, {0, 0}); |
| } |
| |
| // Tests load and store operations |
| // LoadOp::Clear and StoreOp::Store are basically always tested by other tests, so this one tests |
| // LoadOp::Load and StoreOp::Discard |
| TEST_P(FramebufferFetchTests, LoadAndStoreOpsReallyItsLoadAndDiscard) { |
| // Pipeline that adds one to the first attachment but also saves it to the second attachment. |
| utils::ComboRenderPipelineDescriptor pDesc; |
| InitForSinglePoint(&pDesc); |
| pDesc.cFragment.module = utils::CreateShaderModule(device, kExt + R"( |
| struct Out { |
| @location(0) out0 : u32, |
| @location(1) out1 : u32, |
| } |
| @fragment fn main(@color(0) in0 : u32, @color(1) in1 : u32) -> Out { |
| var out : Out; |
| out.out0 = in0 + 1; |
| out.out1 = out.out0; |
| return out; |
| } |
| )"); |
| pDesc.cFragment.targetCount = 2; |
| pDesc.cTargets[0].format = wgpu::TextureFormat::R32Uint; |
| pDesc.cTargets[1].format = wgpu::TextureFormat::R32Uint; |
| wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pDesc); |
| |
| wgpu::Texture texture0 = MakeAttachment(wgpu::TextureFormat::R32Uint); |
| wgpu::Texture texture1 = MakeAttachment(wgpu::TextureFormat::R32Uint); |
| |
| // Clear the first attachment to 1789 with a render pass. |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| utils::ComboRenderPassDescriptor clearPassDesc({texture0.CreateView()}); |
| clearPassDesc.cColorAttachments[0].clearValue.r = 1789; |
| wgpu::RenderPassEncoder clearPass = encoder.BeginRenderPass(&clearPassDesc); |
| clearPass.End(); |
| |
| // Draw 10 points with the pipeline on the render target. |
| utils::ComboRenderPassDescriptor passDesc({texture0.CreateView(), texture1.CreateView()}); |
| passDesc.cColorAttachments[0].loadOp = wgpu::LoadOp::Load; |
| passDesc.cColorAttachments[0].storeOp = wgpu::StoreOp::Discard; |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc); |
| pass.SetPipeline(pipeline); |
| pass.Draw(10); |
| pass.End(); |
| |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| // The first attachment is discard, but it's value of (ten increments + loaded value) is in the |
| // second attachment. |
| EXPECT_TEXTURE_EQ(uint32_t(0), texture0, {0, 0}); |
| EXPECT_TEXTURE_EQ(uint32_t(10 + 1789), texture1, {0, 0}); |
| } |
| |
| // Test the behavior with multisampling. |
| // Checks that each sample has its own framebuffer fetch by add sample_index to that sample multiple |
| // times. Checks that any framebuffer fetch forces sample shading, even if all the samples have the |
| // same value. |
| TEST_P(FramebufferFetchTests, MultisamplingIsSampleRasterization) { |
| wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout( |
| device, {{0, wgpu::ShaderStage::Fragment, wgpu::BufferBindingType::Storage}}); |
| |
| utils::ComboRenderPipelineDescriptor pDesc; |
| InitForSinglePoint(&pDesc); |
| pDesc.layout = utils::MakePipelineLayout(device, {bgl}); |
| pDesc.multisample.count = 4; |
| pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm; |
| pDesc.cFragment.module = utils::CreateShaderModule(device, kExt + R"( |
| @group(0) @binding(0) var<storage, read_write> invocationCount : atomic<u32>; |
| |
| @fragment fn addOne(@color(0) in : vec4f) -> @location(0) vec4f { |
| atomicAdd(&invocationCount, 1); |
| return in + vec4(1 / 255.0); |
| } |
| |
| @fragment fn addSampleIndex( |
| @color(0) in : vec4f, |
| @builtin(sample_index) sampleIndex : u32, |
| ) -> @location(0) vec4f { |
| atomicAdd(&invocationCount, 1); |
| var out = in; |
| out[sampleIndex] += f32(4 * sampleIndex) / 255.0; |
| return out; |
| } |
| )"); |
| |
| pDesc.cFragment.entryPoint = "addOne"; |
| wgpu::RenderPipeline addOnePipeline = device.CreateRenderPipeline(&pDesc); |
| |
| pDesc.cFragment.entryPoint = "addSampleIndex"; |
| wgpu::RenderPipeline addSampleCountPipeline = device.CreateRenderPipeline(&pDesc); |
| |
| // Create the buffer that will contain the number of invocations. |
| wgpu::BufferDescriptor bufDesc; |
| bufDesc.size = 4; |
| bufDesc.usage = wgpu::BufferUsage::Storage | wgpu::BufferUsage::CopySrc; |
| wgpu::Buffer invocationBuffer = device.CreateBuffer(&bufDesc); |
| wgpu::BindGroup bg = utils::MakeBindGroup(device, bgl, {{0, invocationBuffer}}); |
| |
| wgpu::Texture attachment = MakeAttachment(wgpu::TextureFormat::RGBA8Unorm, {1, 1}, 4); |
| wgpu::Texture resolve = MakeAttachment(wgpu::TextureFormat::RGBA8Unorm); |
| |
| // Run the test!! |
| utils::ComboRenderPassDescriptor passDesc({attachment.CreateView()}); |
| passDesc.cColorAttachments[0].resolveTarget = resolve.CreateView(); |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&passDesc); |
| pass.SetBindGroup(0, bg); |
| // This FS should use sample shading and run 4 times, adding one each time. |
| pass.SetPipeline(addOnePipeline); |
| pass.Draw(1); |
| // This adds sample_index * 10 to each sample. |
| pass.SetPipeline(addSampleCountPipeline); |
| pass.Draw(10); |
| // This FS should use sample shading and run 4 times, adding one each time. |
| pass.SetPipeline(addOnePipeline); |
| pass.Draw(1); |
| pass.End(); |
| |
| wgpu::CommandBuffer commands = encoder.Finish(); |
| queue.Submit(1, &commands); |
| |
| EXPECT_BUFFER_U32_EQ(4 + 10 * 4 + 4, invocationBuffer, 0); |
| EXPECT_TEXTURE_EQ(utils::RGBA8(2 + 0 * 10, 2 + 1 * 10, 2 + 2 * 10, 2 + 3 * 10), resolve, |
| {0, 0}); |
| } |
| |
| DAWN_INSTANTIATE_TEST(FramebufferFetchTests, MetalBackend()); |
| |
| } // anonymous namespace |
| } // namespace dawn |