| // Copyright 2023 The Dawn Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include <string> |
| #include <vector> |
| |
| #include "dawn/common/NonCopyable.h" |
| #include "dawn/tests/unittests/validation/ValidationTest.h" |
| #include "dawn/utils/ComboRenderPipelineDescriptor.h" |
| #include "dawn/utils/WGPUHelpers.h" |
| |
| namespace dawn { |
| namespace { |
| |
| class PixelLocalStorageDisabledTest : public ValidationTest {}; |
| |
| // Check that creating a StorageAttachment texture is disallowed without the extension. |
| TEST_F(PixelLocalStorageDisabledTest, StorageAttachmentTextureNotAllowed) { |
| wgpu::TextureDescriptor desc; |
| desc.size = {1, 1, 1}; |
| desc.format = wgpu::TextureFormat::R32Uint; |
| desc.usage = wgpu::TextureUsage::TextureBinding; |
| |
| // Control case: creating the texture without StorageAttachment is allowed. |
| device.CreateTexture(&desc); |
| |
| // Error case: creating the texture without StorageAttachment is disallowed. |
| desc.usage = wgpu::TextureUsage::StorageAttachment; |
| ASSERT_DEVICE_ERROR(device.CreateTexture(&desc)); |
| } |
| |
| // Check that creating a pipeline layout with a PipelineLayoutPixelLocalStorage is disallowed |
| // without the extension. |
| TEST_F(PixelLocalStorageDisabledTest, PipelineLayoutPixelLocalStorageDisallowed) { |
| wgpu::PipelineLayoutDescriptor desc; |
| desc.bindGroupLayoutCount = 0; |
| |
| // Control case: creating the pipeline layout without the PLS is allowed. |
| device.CreatePipelineLayout(&desc); |
| |
| // Error case: creating the pipeline layout with a PLS is disallowed even if it is empty. |
| wgpu::PipelineLayoutPixelLocalStorage pls; |
| pls.totalPixelLocalStorageSize = 0; |
| pls.storageAttachmentCount = 0; |
| desc.nextInChain = &pls; |
| |
| ASSERT_DEVICE_ERROR(device.CreatePipelineLayout(&desc)); |
| } |
| |
| // Check that a render pass with a RenderPassPixelLocalStorage is disallowed without the extension. |
| TEST_F(PixelLocalStorageDisabledTest, RenderPassPixelLocalStorageDisallowed) { |
| utils::BasicRenderPass rp = utils::CreateBasicRenderPass(device, 1, 1); |
| |
| // Control case: beginning the render pass without the PLS is allowed. |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.End(); |
| encoder.Finish(); |
| } |
| |
| // Error case: beginning the render pass without the PLS is disallowed, even if it is empty. |
| { |
| wgpu::RenderPassPixelLocalStorage pls; |
| pls.totalPixelLocalStorageSize = 0; |
| pls.storageAttachmentCount = 0; |
| rp.renderPassInfo.nextInChain = &pls; |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.End(); |
| ASSERT_DEVICE_ERROR(encoder.Finish()); |
| } |
| } |
| |
| // Check that it is not possible to use the WGSL extension without the device extension enabled. |
| TEST_F(PixelLocalStorageDisabledTest, WGSLExtensionDisallowed) { |
| ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_pixel_local; |
| )")); |
| } |
| |
| // Check that PixelLocalStorageBarrier() is disallowed without the extension. |
| TEST_F(PixelLocalStorageDisabledTest, PixelLocalStorageBarrierDisallowed) { |
| utils::BasicRenderPass rp = utils::CreateBasicRenderPass(device, 1, 1); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.PixelLocalStorageBarrier(); |
| pass.End(); |
| ASSERT_DEVICE_ERROR(encoder.Finish()); |
| } |
| |
| class PixelLocalStorageOtherExtensionTest : public ValidationTest { |
| protected: |
| WGPUDevice CreateTestDevice(native::Adapter dawnAdapter, |
| wgpu::DeviceDescriptor descriptor) override { |
| // Only test the coherent extension. The non-coherent one has the rest of the validation |
| // tests. |
| wgpu::FeatureName requiredFeatures[1] = {wgpu::FeatureName::PixelLocalStorageCoherent}; |
| descriptor.requiredFeatures = requiredFeatures; |
| descriptor.requiredFeatureCount = 1; |
| return dawnAdapter.CreateDevice(&descriptor); |
| } |
| }; |
| |
| // Simple test checking all the various things that are normally validated out without PLS are |
| // available if the coherent PLS extension is enabled. |
| TEST_F(PixelLocalStorageOtherExtensionTest, SmokeTest) { |
| // Creating a StorageAttachment texture is allowed. |
| wgpu::TextureDescriptor textureDesc; |
| textureDesc.size = {1, 1, 1}; |
| textureDesc.format = wgpu::TextureFormat::R32Uint; |
| textureDesc.usage = wgpu::TextureUsage::StorageAttachment; |
| wgpu::Texture tex = device.CreateTexture(&textureDesc); |
| |
| // Creating a pipeline layout with PLS is allowed. |
| wgpu::PipelineLayoutPixelLocalStorage plPlsDesc; |
| plPlsDesc.totalPixelLocalStorageSize = 0; |
| plPlsDesc.storageAttachmentCount = 0; |
| |
| wgpu::PipelineLayoutDescriptor plDesc; |
| plDesc.bindGroupLayoutCount = 0; |
| plDesc.nextInChain = &plPlsDesc; |
| device.CreatePipelineLayout(&plDesc); |
| |
| // Creating a PLS render pass is allowed. |
| wgpu::RenderPassPixelLocalStorage rpPlsDesc; |
| rpPlsDesc.totalPixelLocalStorageSize = 0; |
| rpPlsDesc.storageAttachmentCount = 0; |
| |
| utils::BasicRenderPass rp = utils::CreateBasicRenderPass(device, 1, 1); |
| rp.renderPassInfo.nextInChain = &rpPlsDesc; |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| // Calling PixelLocalStorageBarrier is allowed. |
| pass.PixelLocalStorageBarrier(); |
| pass.End(); |
| encoder.Finish(); |
| |
| // Creating a shader with the extension is allowed. |
| utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_pixel_local; |
| )"); |
| } |
| |
| struct OffsetAndFormat { |
| uint64_t offset; |
| wgpu::TextureFormat format; |
| }; |
| struct PLSSpec { |
| uint64_t totalSize; |
| std::vector<OffsetAndFormat> attachments; |
| bool active = true; |
| }; |
| |
| constexpr std::array<wgpu::TextureFormat, 3> kStorageAttachmentFormats = { |
| wgpu::TextureFormat::R32Float, |
| wgpu::TextureFormat::R32Uint, |
| wgpu::TextureFormat::R32Sint, |
| }; |
| bool IsStorageAttachmentFormat(wgpu::TextureFormat format) { |
| return std::find(kStorageAttachmentFormats.begin(), kStorageAttachmentFormats.end(), format) != |
| kStorageAttachmentFormats.end(); |
| } |
| |
| struct ComboTestPLSRenderPassDescriptor : NonMovable { |
| std::array<wgpu::RenderPassStorageAttachment, 8> storageAttachments; |
| wgpu::RenderPassPixelLocalStorage pls; |
| wgpu::RenderPassColorAttachment colorAttachment; |
| wgpu::RenderPassDescriptor rpDesc; |
| }; |
| |
| class PixelLocalStorageTest : public ValidationTest { |
| protected: |
| WGPUDevice CreateTestDevice(native::Adapter dawnAdapter, |
| wgpu::DeviceDescriptor descriptor) override { |
| // Test only the non-coherent version, and assume that the same validation code paths are |
| // taken for the coherent path. |
| wgpu::FeatureName requiredFeatures[1] = {wgpu::FeatureName::PixelLocalStorageNonCoherent}; |
| descriptor.requiredFeatures = requiredFeatures; |
| descriptor.requiredFeatureCount = 1; |
| return dawnAdapter.CreateDevice(&descriptor); |
| } |
| |
| void InitializePLSRenderPass(ComboTestPLSRenderPassDescriptor* desc) { |
| // Set up a single storage attachment. |
| wgpu::TextureDescriptor storageDesc; |
| storageDesc.size = {1, 1}; |
| storageDesc.format = wgpu::TextureFormat::R32Uint; |
| storageDesc.usage = wgpu::TextureUsage::StorageAttachment; |
| wgpu::Texture storage = device.CreateTexture(&storageDesc); |
| |
| desc->storageAttachments[0].storage = storage.CreateView(); |
| desc->storageAttachments[0].offset = 0; |
| desc->storageAttachments[0].loadOp = wgpu::LoadOp::Load; |
| desc->storageAttachments[0].storeOp = wgpu::StoreOp::Store; |
| |
| desc->pls.totalPixelLocalStorageSize = 4; |
| desc->pls.storageAttachmentCount = 1; |
| desc->pls.storageAttachments = desc->storageAttachments.data(); |
| |
| // Add at least one color attachment to make the render pass valid if there's no storage |
| // attachment. |
| wgpu::TextureDescriptor colorDesc; |
| colorDesc.size = {1, 1}; |
| colorDesc.format = kColorAttachmentFormat; |
| colorDesc.usage = wgpu::TextureUsage::RenderAttachment; |
| wgpu::Texture color = device.CreateTexture(&colorDesc); |
| |
| desc->colorAttachment.view = color.CreateView(); |
| desc->colorAttachment.loadOp = wgpu::LoadOp::Load; |
| desc->colorAttachment.storeOp = wgpu::StoreOp::Store; |
| |
| desc->rpDesc.nextInChain = &desc->pls; |
| desc->rpDesc.colorAttachmentCount = 1; |
| desc->rpDesc.colorAttachments = &desc->colorAttachment; |
| } |
| |
| void RecordRenderPass(const wgpu::RenderPassDescriptor* desc, |
| wgpu::RenderPipeline pipeline = {}) { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(desc); |
| |
| if (pipeline) { |
| pass.SetPipeline(pipeline); |
| } |
| |
| pass.End(); |
| encoder.Finish(); |
| } |
| |
| void RecordPLSRenderPass(const PLSSpec& spec, wgpu::RenderPipeline pipeline = {}) { |
| ComboTestPLSRenderPassDescriptor desc; |
| InitializePLSRenderPass(&desc); |
| |
| // Convert the PLSSpec to a RenderPassPLS |
| for (size_t i = 0; i < spec.attachments.size(); i++) { |
| wgpu::TextureDescriptor tDesc; |
| tDesc.size = {1, 1}; |
| tDesc.format = spec.attachments[i].format; |
| tDesc.usage = wgpu::TextureUsage::StorageAttachment; |
| wgpu::Texture texture = device.CreateTexture(&tDesc); |
| |
| desc.storageAttachments[i].storage = texture.CreateView(); |
| desc.storageAttachments[i].offset = spec.attachments[i].offset; |
| desc.storageAttachments[i].loadOp = wgpu::LoadOp::Load; |
| desc.storageAttachments[i].storeOp = wgpu::StoreOp::Store; |
| } |
| |
| desc.pls.totalPixelLocalStorageSize = spec.totalSize; |
| desc.pls.storageAttachmentCount = spec.attachments.size(); |
| |
| // Add the PLS if needed and record the render pass. |
| if (!spec.active) { |
| desc.rpDesc.nextInChain = nullptr; |
| } |
| |
| RecordRenderPass(&desc.rpDesc, pipeline); |
| } |
| |
| wgpu::PipelineLayout MakePipelineLayout(const PLSSpec& spec) { |
| // Convert the PLSSpec to a PipelineLayoutPLS |
| std::vector<wgpu::PipelineLayoutStorageAttachment> storageAttachments; |
| for (const auto& attachmentSpec : spec.attachments) { |
| wgpu::PipelineLayoutStorageAttachment attachment; |
| attachment.format = attachmentSpec.format; |
| attachment.offset = attachmentSpec.offset; |
| storageAttachments.push_back(attachment); |
| } |
| |
| wgpu::PipelineLayoutPixelLocalStorage pls; |
| pls.totalPixelLocalStorageSize = spec.totalSize; |
| pls.storageAttachmentCount = storageAttachments.size(); |
| pls.storageAttachments = storageAttachments.data(); |
| |
| // Add the PLS if needed and make the pipeline layout. |
| wgpu::PipelineLayoutDescriptor plDesc; |
| plDesc.bindGroupLayoutCount = 0; |
| if (spec.active) { |
| plDesc.nextInChain = &pls; |
| } |
| return device.CreatePipelineLayout(&plDesc); |
| } |
| |
| wgpu::RenderPipeline MakePipeline(const PLSSpec& spec) { |
| std::vector<const char*> plsTypes; |
| plsTypes.resize(spec.totalSize / kPLSSlotByteSize, "u32"); |
| for (const auto& attachment : spec.attachments) { |
| switch (attachment.format) { |
| case wgpu::TextureFormat::R32Uint: |
| plsTypes[attachment.offset / kPLSSlotByteSize] = "u32"; |
| break; |
| case wgpu::TextureFormat::R32Sint: |
| plsTypes[attachment.offset / kPLSSlotByteSize] = "i32"; |
| break; |
| case wgpu::TextureFormat::R32Float: |
| plsTypes[attachment.offset / kPLSSlotByteSize] = "f32"; |
| break; |
| default: |
| DAWN_UNREACHABLE(); |
| } |
| } |
| |
| bool outputPLS = spec.active && !plsTypes.empty(); |
| |
| std::ostringstream fsStream; |
| fsStream << "enable chromium_experimental_pixel_local;\n"; |
| if (outputPLS) { |
| fsStream << "struct PLS {\n"; |
| for (size_t i = 0; i < plsTypes.size(); i++) { |
| fsStream << " a" << i << " : " << plsTypes[i] << ",\n"; |
| } |
| fsStream << "}\n"; |
| fsStream << "var<pixel_local> pls : PLS;\n"; |
| } |
| fsStream << "@fragment fn fs() -> @location(0) u32 {\n"; |
| if (outputPLS) { |
| fsStream << " _ = pls;\n"; |
| } |
| fsStream << " return 0u;\n"; |
| fsStream << "}\n"; |
| |
| utils::ComboRenderPipelineDescriptor desc; |
| desc.layout = MakePipelineLayout(spec); |
| desc.cFragment.module = utils::CreateShaderModule(device, fsStream.str().c_str()); |
| desc.cFragment.entryPoint = "fs"; |
| desc.vertex.module = utils::CreateShaderModule(device, R"( |
| @vertex fn vs() -> @builtin(position) vec4f { |
| return vec4f(); |
| } |
| )"); |
| desc.vertex.entryPoint = "vs"; |
| desc.cTargets[0].format = kColorAttachmentFormat; |
| return device.CreateRenderPipeline(&desc); |
| } |
| |
| void CheckPLSStateMatching(const PLSSpec& rpSpec, const PLSSpec& pipelineSpec, bool success) { |
| wgpu::RenderPipeline pipeline = MakePipeline(pipelineSpec); |
| |
| if (success) { |
| RecordPLSRenderPass(rpSpec, pipeline); |
| } else { |
| ASSERT_DEVICE_ERROR(RecordPLSRenderPass(rpSpec, pipeline)); |
| } |
| } |
| |
| void TestFragmentAndLayoutCompat(const PLSSpec& layoutSpec, |
| absl::string_view fs, |
| bool success) { |
| TestFragmentAndLayoutCompat(MakePipelineLayout(layoutSpec), fs, success); |
| } |
| |
| void TestFragmentAndLayoutCompat(const wgpu::PipelineLayout& layout, |
| absl::string_view fs, |
| bool success) { |
| wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, fs.data()); |
| wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, R"( |
| @vertex fn vs() -> @builtin(position) vec4f { |
| return vec4f(); |
| } |
| )"); |
| |
| utils::ComboRenderPipelineDescriptor desc; |
| desc.layout = layout; |
| desc.cFragment.module = fsModule; |
| desc.cFragment.entryPoint = "fs"; |
| desc.vertex.module = vsModule; |
| desc.vertex.entryPoint = "vs"; |
| desc.cTargets[0].format = kColorAttachmentFormat; |
| desc.cTargets[0].writeMask = wgpu::ColorWriteMask::None; |
| |
| if (success) { |
| device.CreateRenderPipeline(&desc); |
| } else { |
| ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&desc)); |
| } |
| } |
| |
| static constexpr wgpu::TextureFormat kColorAttachmentFormat = wgpu::TextureFormat::R32Uint; |
| }; |
| |
| // Check that it is possible to use the WGSL extension when the device extension is enabled. |
| TEST_F(PixelLocalStorageTest, WGSLExtensionAllowed) { |
| utils::CreateShaderModule(device, R"( |
| enable chromium_experimental_pixel_local; |
| )"); |
| } |
| |
| // Check that StorageAttachment textures must be one of the supported formats. |
| TEST_F(PixelLocalStorageTest, TextureFormatMustSupportStorageAttachment) { |
| for (wgpu::TextureFormat format : utils::kAllTextureFormats) { |
| wgpu::TextureDescriptor desc; |
| desc.size = {1, 1}; |
| desc.format = format; |
| desc.usage = wgpu::TextureUsage::StorageAttachment; |
| |
| if (IsStorageAttachmentFormat(format)) { |
| device.CreateTexture(&desc); |
| } else { |
| ASSERT_DEVICE_ERROR(device.CreateTexture(&desc)); |
| } |
| } |
| } |
| |
| // Check that StorageAttachment textures must have a sample count of 1. |
| TEST_F(PixelLocalStorageTest, TextureMustBeSingleSampled) { |
| wgpu::TextureDescriptor desc; |
| desc.size = {1, 1}; |
| desc.format = wgpu::TextureFormat::R32Uint; |
| desc.usage = wgpu::TextureUsage::StorageAttachment; |
| |
| // Control case: sampleCount = 1 is valid. |
| desc.sampleCount = 1; |
| device.CreateTexture(&desc); |
| |
| // Error case: sampledCount != 1 is an error. |
| desc.sampleCount = 4; |
| ASSERT_DEVICE_ERROR(device.CreateTexture(&desc)); |
| } |
| |
| // Check that the format in PLS must be one of the enabled ones. |
| TEST_F(PixelLocalStorageTest, PLSStateFormatMustSupportStorageAttachment) { |
| for (wgpu::TextureFormat format : utils::kFormatsInCoreSpec) { |
| PLSSpec spec = {4, {{0, format}}}; |
| |
| // Note that BeginRenderPass is not tested here as a different test checks that the |
| // StorageAttachment texture must indeed have been created with the StorageAttachment usage. |
| if (IsStorageAttachmentFormat(format)) { |
| MakePipelineLayout(spec); |
| } else { |
| ASSERT_DEVICE_ERROR(MakePipelineLayout(spec)); |
| } |
| } |
| } |
| |
| // Check that the total size must be a multiple of 4. |
| TEST_F(PixelLocalStorageTest, PLSStateTotalSizeMultipleOf4) { |
| // Control case: total size is a multiple of 4. |
| { |
| PLSSpec spec = {4, {}}; |
| MakePipelineLayout(spec); |
| RecordPLSRenderPass(spec); |
| } |
| |
| // Control case: total size isn't a multiple of 4. |
| { |
| PLSSpec spec = {2, {}}; |
| ASSERT_DEVICE_ERROR(MakePipelineLayout(spec)); |
| ASSERT_DEVICE_ERROR(RecordPLSRenderPass(spec)); |
| } |
| } |
| |
| // Check that the total size must be less than 16. |
| // TODO(dawn:1704): Have a proper limit for totalSize. |
| TEST_F(PixelLocalStorageTest, PLSStateTotalLessThan16) { |
| // Control case: total size is 16. |
| { |
| PLSSpec spec = {16, {}}; |
| MakePipelineLayout(spec); |
| RecordPLSRenderPass(spec); |
| } |
| |
| // Control case: total size is greater than 16. |
| { |
| PLSSpec spec = {20, {}}; |
| ASSERT_DEVICE_ERROR(MakePipelineLayout(spec)); |
| ASSERT_DEVICE_ERROR(RecordPLSRenderPass(spec)); |
| } |
| } |
| |
| // Check that the offset of a storage attachment must be a multiple of 4. |
| TEST_F(PixelLocalStorageTest, PLSStateOffsetMultipleOf4) { |
| // Control case: offset is a multiple of 4. |
| { |
| PLSSpec spec = {8, {{4, wgpu::TextureFormat::R32Uint}}}; |
| MakePipelineLayout(spec); |
| RecordPLSRenderPass(spec); |
| } |
| |
| // Error case: offset isn't a multiple of 4. |
| { |
| PLSSpec spec = {8, {{2, wgpu::TextureFormat::R32Uint}}}; |
| ASSERT_DEVICE_ERROR(MakePipelineLayout(spec)); |
| ASSERT_DEVICE_ERROR(RecordPLSRenderPass(spec)); |
| } |
| } |
| |
| // Check that the storage attachment is in bounds of the total size. |
| TEST_F(PixelLocalStorageTest, PLSStateAttachmentInBoundsOfTotalSize) { |
| // Note that all storage attachment formats are currently 4 byte wide. |
| |
| // Control case: 0 + 4 <= 4 |
| { |
| PLSSpec spec = {4, {{0, wgpu::TextureFormat::R32Uint}}}; |
| MakePipelineLayout(spec); |
| RecordPLSRenderPass(spec); |
| } |
| |
| // Error case: 4 + 4 > 4 |
| { |
| PLSSpec spec = {4, {{4, wgpu::TextureFormat::R32Uint}}}; |
| ASSERT_DEVICE_ERROR(MakePipelineLayout(spec)); |
| ASSERT_DEVICE_ERROR(RecordPLSRenderPass(spec)); |
| } |
| |
| // Control case: 8 + 4 <= 12 |
| { |
| PLSSpec spec = {12, {{8, wgpu::TextureFormat::R32Uint}}}; |
| MakePipelineLayout(spec); |
| RecordPLSRenderPass(spec); |
| } |
| |
| // Error case: 12 + 4 > 12 |
| { |
| PLSSpec spec = {4, {{12, wgpu::TextureFormat::R32Uint}}}; |
| ASSERT_DEVICE_ERROR(MakePipelineLayout(spec)); |
| ASSERT_DEVICE_ERROR(RecordPLSRenderPass(spec)); |
| } |
| |
| // Check that overflows don't incorrectly pass the validation. |
| { |
| PLSSpec spec = {4, {{uint64_t(0) - uint64_t(4), wgpu::TextureFormat::R32Uint}}}; |
| ASSERT_DEVICE_ERROR(MakePipelineLayout(spec)); |
| ASSERT_DEVICE_ERROR(RecordPLSRenderPass(spec)); |
| } |
| } |
| |
| // Check that collisions between storage attachments are not allowed. |
| TEST_F(PixelLocalStorageTest, PLSStateCollisionsDisallowed) { |
| // Control case: no collisions, all is good! |
| { |
| PLSSpec spec = {8, {{0, wgpu::TextureFormat::R32Uint}, {4, wgpu::TextureFormat::R32Uint}}}; |
| MakePipelineLayout(spec); |
| RecordPLSRenderPass(spec); |
| } |
| // Error case: collisions, boo! |
| { |
| PLSSpec spec = {8, {{0, wgpu::TextureFormat::R32Uint}, {0, wgpu::TextureFormat::R32Uint}}}; |
| ASSERT_DEVICE_ERROR(MakePipelineLayout(spec)); |
| ASSERT_DEVICE_ERROR(RecordPLSRenderPass(spec)); |
| } |
| { |
| PLSSpec spec = {8, |
| {{0, wgpu::TextureFormat::R32Uint}, |
| {4, wgpu::TextureFormat::R32Uint}, |
| {0, wgpu::TextureFormat::R32Uint}}}; |
| ASSERT_DEVICE_ERROR(MakePipelineLayout(spec)); |
| ASSERT_DEVICE_ERROR(RecordPLSRenderPass(spec)); |
| } |
| } |
| |
| // Check that using an error view as storage attachment is an error. |
| TEST_F(PixelLocalStorageTest, RenderPassStorageAttachmentErrorView) { |
| ComboTestPLSRenderPassDescriptor desc; |
| InitializePLSRenderPass(&desc); |
| |
| wgpu::TextureDescriptor tDesc; |
| tDesc.size = {1, 1}; |
| tDesc.usage = wgpu::TextureUsage::StorageAttachment; |
| tDesc.format = wgpu::TextureFormat::R32Uint; |
| wgpu::Texture t = device.CreateTexture(&tDesc); |
| |
| wgpu::TextureViewDescriptor viewDesc; |
| |
| // Control case: valid texture view. |
| desc.storageAttachments[0].storage = t.CreateView(&viewDesc); |
| RecordRenderPass(&desc.rpDesc); |
| |
| // Error case: invalid texture view because of the base array layer. |
| viewDesc.baseArrayLayer = 10; |
| ASSERT_DEVICE_ERROR(desc.storageAttachments[0].storage = t.CreateView(&viewDesc)); |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| } |
| |
| // Check that using a multi-subresource view as a storage attachment is an error (layers and levels |
| // cases). |
| TEST_F(PixelLocalStorageTest, RenderPassStorageAttachmentSingleSubresource) { |
| ComboTestPLSRenderPassDescriptor desc; |
| InitializePLSRenderPass(&desc); |
| |
| wgpu::TextureDescriptor colorDesc; |
| colorDesc.size = {2, 2}; |
| colorDesc.usage = wgpu::TextureUsage::RenderAttachment; |
| colorDesc.format = kColorAttachmentFormat; |
| |
| // Replace the render pass attachment with a 2x2 texture for mip level testing. |
| desc.colorAttachment.view = device.CreateTexture(&colorDesc).CreateView(); |
| |
| // Control case: single subresource view. |
| wgpu::TextureDescriptor tDesc; |
| tDesc.size = {2, 2}; |
| tDesc.usage = wgpu::TextureUsage::StorageAttachment; |
| tDesc.format = wgpu::TextureFormat::R32Uint; |
| |
| desc.storageAttachments[0].storage = device.CreateTexture(&tDesc).CreateView(); |
| RecordRenderPass(&desc.rpDesc); |
| |
| // Error case: two array layers. |
| tDesc.size.depthOrArrayLayers = 2; |
| desc.storageAttachments[0].storage = device.CreateTexture(&tDesc).CreateView(); |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| |
| // Error case: two mip levels. |
| tDesc.size.depthOrArrayLayers = 1; |
| tDesc.mipLevelCount = 2; |
| desc.storageAttachments[0].storage = device.CreateTexture(&tDesc).CreateView(); |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| } |
| |
| // Check that the size of storage attachments must match the size of other attachments. |
| TEST_F(PixelLocalStorageTest, RenderPassStorageAttachmentSizeMustMatch) { |
| ComboTestPLSRenderPassDescriptor desc; |
| InitializePLSRenderPass(&desc); |
| |
| // Explicitly set the color attachment to a 1x1 texture. |
| wgpu::TextureDescriptor colorDesc; |
| colorDesc.size = {1, 1}; |
| colorDesc.usage = wgpu::TextureUsage::RenderAttachment; |
| colorDesc.format = kColorAttachmentFormat; |
| desc.colorAttachment.view = device.CreateTexture(&colorDesc).CreateView(); |
| |
| // Control case: the storage attachment size matches |
| wgpu::TextureDescriptor tDesc; |
| tDesc.size = {1, 1}; |
| tDesc.usage = wgpu::TextureUsage::StorageAttachment; |
| tDesc.format = wgpu::TextureFormat::R32Uint; |
| |
| desc.storageAttachments[0].storage = device.CreateTexture(&tDesc).CreateView(); |
| RecordRenderPass(&desc.rpDesc); |
| |
| // Error case: width doesn't match. |
| tDesc.size = {2, 1}; |
| desc.storageAttachments[0].storage = device.CreateTexture(&tDesc).CreateView(); |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| |
| // Error case: height doesn't match. |
| tDesc.size = {1, 2}; |
| desc.storageAttachments[0].storage = device.CreateTexture(&tDesc).CreateView(); |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| } |
| |
| // Check that the textures used as storage attachment must have the StorageAttachment TextureUsage. |
| TEST_F(PixelLocalStorageTest, RenderPassStorageAttachmentUsage) { |
| ComboTestPLSRenderPassDescriptor desc; |
| InitializePLSRenderPass(&desc); |
| |
| // Control case: the storage attachment has the correct usage. |
| wgpu::TextureDescriptor tDesc; |
| tDesc.size = {1, 1}; |
| tDesc.usage = wgpu::TextureUsage::StorageAttachment; |
| tDesc.format = wgpu::TextureFormat::R32Uint; |
| |
| desc.storageAttachments[0].storage = device.CreateTexture(&tDesc).CreateView(); |
| RecordRenderPass(&desc.rpDesc); |
| |
| // Error case: the storage attachment doesn't have the usage. |
| tDesc.usage = wgpu::TextureUsage::RenderAttachment; |
| desc.storageAttachments[0].storage = device.CreateTexture(&tDesc).CreateView(); |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| } |
| |
| // Check that the same texture subresource cannot be used twice as a storage attachment. |
| TEST_F(PixelLocalStorageTest, RenderPassSubresourceUsedTwiceAsStorage) { |
| ComboTestPLSRenderPassDescriptor desc; |
| InitializePLSRenderPass(&desc); |
| |
| // Control case: two different subresources for two storage attachments. |
| wgpu::TextureDescriptor tDesc; |
| tDesc.size = {1, 1}; |
| tDesc.usage = wgpu::TextureUsage::StorageAttachment; |
| tDesc.format = wgpu::TextureFormat::R32Uint; |
| |
| desc.storageAttachments[0].storage = device.CreateTexture(&tDesc).CreateView(); |
| |
| desc.storageAttachments[1].storage = device.CreateTexture(&tDesc).CreateView(); |
| desc.storageAttachments[1].offset = 4; |
| desc.storageAttachments[1].loadOp = wgpu::LoadOp::Load; |
| desc.storageAttachments[1].storeOp = wgpu::StoreOp::Store; |
| desc.pls.storageAttachmentCount = 2; |
| desc.pls.totalPixelLocalStorageSize = 8; |
| |
| RecordRenderPass(&desc.rpDesc); |
| |
| // Error case: the same subresource is used twice as a storage attachment. |
| desc.storageAttachments[0].storage = desc.storageAttachments[1].storage; |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| } |
| |
| // Check that the same texture subresource cannot be used twice as a storage and render attachment. |
| TEST_F(PixelLocalStorageTest, RenderPassSubresourceUsedAsStorageAndRender) { |
| ComboTestPLSRenderPassDescriptor desc; |
| InitializePLSRenderPass(&desc); |
| |
| // Control case: two different subresources for storage and render attachments. |
| wgpu::TextureDescriptor tDesc; |
| tDesc.size = {1, 1}; |
| tDesc.usage = wgpu::TextureUsage::StorageAttachment | wgpu::TextureUsage::RenderAttachment; |
| tDesc.format = kColorAttachmentFormat; |
| |
| desc.storageAttachments[0].storage = device.CreateTexture(&tDesc).CreateView(); |
| RecordRenderPass(&desc.rpDesc); |
| |
| // Error case: the same view is used twice, once as storage, once as render attachment. |
| desc.colorAttachment.view = desc.storageAttachments[0].storage; |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| } |
| |
| // Check that using a subresource as storage attachment prevents other usages in the render pass. |
| TEST_F(PixelLocalStorageTest, RenderPassSubresourceUsedInsidePass) { |
| ComboTestPLSRenderPassDescriptor desc; |
| InitializePLSRenderPass(&desc); |
| |
| wgpu::TextureDescriptor tDesc; |
| tDesc.size = {1, 1}; |
| tDesc.usage = wgpu::TextureUsage::StorageAttachment | wgpu::TextureUsage::TextureBinding; |
| tDesc.format = wgpu::TextureFormat::R32Uint; |
| |
| desc.storageAttachments[0].storage = device.CreateTexture(&tDesc).CreateView(); |
| |
| // Control case: the storage attachment is used only as storage attachment. |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&desc.rpDesc); |
| pass.End(); |
| encoder.Finish(); |
| } |
| |
| // Error case: the storage attachment is also used as a texture binding in a bind group. |
| { |
| wgpu::BindGroupLayout bgl = utils::MakeBindGroupLayout( |
| device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Uint}}); |
| wgpu::BindGroup bg = |
| utils::MakeBindGroup(device, bgl, {{0, desc.storageAttachments[0].storage}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&desc.rpDesc); |
| pass.SetBindGroup(0, bg); |
| pass.End(); |
| ASSERT_DEVICE_ERROR(encoder.Finish()); |
| } |
| } |
| |
| // Check that the load and store op must not be undefined. |
| TEST_F(PixelLocalStorageTest, RenderPassLoadAndStoreOpNotUndefined) { |
| ComboTestPLSRenderPassDescriptor desc; |
| InitializePLSRenderPass(&desc); |
| |
| // Control case: ops are not undefined. |
| RecordRenderPass(&desc.rpDesc); |
| |
| // Error case: LoadOp::Undefined |
| desc.storageAttachments[0].loadOp = wgpu::LoadOp::Undefined; |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| desc.storageAttachments[0].loadOp = wgpu::LoadOp::Load; |
| |
| // Error case: StoreOp::Undefined |
| desc.storageAttachments[0].storeOp = wgpu::StoreOp::Undefined; |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| } |
| |
| // Check that the clear value, if used, must not have NaNs. |
| TEST_F(PixelLocalStorageTest, RenderPassClearValueNaNs) { |
| ComboTestPLSRenderPassDescriptor desc; |
| InitializePLSRenderPass(&desc); |
| |
| const float kNaN = std::nan(""); |
| |
| // Check that NaNs are allowed if the loadOp is not Clear. |
| desc.storageAttachments[0].loadOp = wgpu::LoadOp::Load; |
| desc.storageAttachments[0].clearValue = {kNaN, kNaN, kNaN, kNaN}; |
| RecordRenderPass(&desc.rpDesc); |
| |
| // Control case, a non-NaN clear value is allowed. |
| desc.storageAttachments[0].loadOp = wgpu::LoadOp::Clear; |
| desc.storageAttachments[0].clearValue = {0, 0, 0, 0}; |
| RecordRenderPass(&desc.rpDesc); |
| |
| // Error case: NaN in one of the components of clearValue. |
| desc.storageAttachments[0].clearValue = {kNaN, 0, 0, 0}; |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| |
| desc.storageAttachments[0].clearValue = {0, kNaN, 0, 0}; |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| |
| desc.storageAttachments[0].clearValue = {0, 0, kNaN, 0}; |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| |
| desc.storageAttachments[0].clearValue = {0, 0, 0, kNaN}; |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| } |
| |
| // Check that using a subresource as storage attachment prevents other usages in the render pass. |
| TEST_F(PixelLocalStorageTest, PixelLocalStorageBarrierRequiresPLS) { |
| ComboTestPLSRenderPassDescriptor desc; |
| InitializePLSRenderPass(&desc); |
| |
| // Control case: there is a PLS, the barrier is allowed. |
| { |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&desc.rpDesc); |
| pass.PixelLocalStorageBarrier(); |
| pass.End(); |
| encoder.Finish(); |
| } |
| |
| // Error case: there is no PLS (it is unlinked from chained structs), the barrier is not |
| // allowed. |
| { |
| desc.rpDesc.nextInChain = nullptr; |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&desc.rpDesc); |
| pass.PixelLocalStorageBarrier(); |
| pass.End(); |
| ASSERT_DEVICE_ERROR(encoder.Finish()); |
| } |
| } |
| |
| // Check that PLS state differs between no PLS and empty PLS |
| TEST_F(PixelLocalStorageTest, PLSStateMatching_EmptyPLSVsNoPLS) { |
| PLSSpec emptyPLS = {0, {}, true}; |
| PLSSpec noPLS = {0, {}, false}; |
| |
| CheckPLSStateMatching(emptyPLS, emptyPLS, true); |
| CheckPLSStateMatching(noPLS, noPLS, true); |
| CheckPLSStateMatching(emptyPLS, noPLS, false); |
| CheckPLSStateMatching(noPLS, emptyPLS, false); |
| } |
| |
| // Check that PLS state differs between empty PLS and non-empty PLS with no storage attachments |
| TEST_F(PixelLocalStorageTest, PLSStateMatching_EmptyPLSVsNotEmpty) { |
| PLSSpec emptyPLS = {0, {}}; |
| PLSSpec notEmptyPLS = {4, {}}; |
| |
| CheckPLSStateMatching(emptyPLS, emptyPLS, true); |
| CheckPLSStateMatching(notEmptyPLS, notEmptyPLS, true); |
| CheckPLSStateMatching(emptyPLS, notEmptyPLS, false); |
| CheckPLSStateMatching(notEmptyPLS, emptyPLS, false); |
| } |
| |
| // Check that PLS state differs between implicit PLS vs storage attachment |
| TEST_F(PixelLocalStorageTest, PLSStateMatching_AttachmentVsImplicit) { |
| PLSSpec implicitPLS = {4, {}}; |
| PLSSpec storagePLS = {4, {{0, wgpu::TextureFormat::R32Uint}}}; |
| |
| CheckPLSStateMatching(implicitPLS, implicitPLS, true); |
| CheckPLSStateMatching(storagePLS, storagePLS, true); |
| CheckPLSStateMatching(implicitPLS, storagePLS, false); |
| CheckPLSStateMatching(storagePLS, implicitPLS, false); |
| } |
| |
| // Check that PLS state differs between storage attachment formats |
| TEST_F(PixelLocalStorageTest, PLSStateMatching_Format) { |
| PLSSpec intPLS = {4, {{0, wgpu::TextureFormat::R32Sint}}}; |
| PLSSpec uintPLS = {4, {{0, wgpu::TextureFormat::R32Uint}}}; |
| |
| CheckPLSStateMatching(intPLS, intPLS, true); |
| CheckPLSStateMatching(uintPLS, uintPLS, true); |
| CheckPLSStateMatching(intPLS, uintPLS, false); |
| CheckPLSStateMatching(uintPLS, intPLS, false); |
| } |
| |
| // Check that PLS state are equal even if attachments are specified in different orders |
| TEST_F(PixelLocalStorageTest, PLSStateMatching_StorageAttachmentOrder) { |
| PLSSpec pls1 = {8, {{4, wgpu::TextureFormat::R32Uint}, {0, wgpu::TextureFormat::R32Sint}}}; |
| PLSSpec pls2 = {8, {{0, wgpu::TextureFormat::R32Sint}, {4, wgpu::TextureFormat::R32Uint}}}; |
| |
| CheckPLSStateMatching(pls1, pls2, true); |
| } |
| |
| // Check that a shader using pixel_local cannot be used for an implicit layout. |
| TEST_F(PixelLocalStorageTest, ImplicitLayoutDisallowed) { |
| // Control case: not using a pixel_local block is ok. |
| TestFragmentAndLayoutCompat(nullptr, R"( |
| enable chromium_experimental_pixel_local; |
| @fragment fn fs() {} |
| )", |
| true); |
| |
| // Error case: using a pixel_local block is not ok. |
| TestFragmentAndLayoutCompat(nullptr, R"( |
| enable chromium_experimental_pixel_local; |
| struct PLS { a : u32 } |
| var<pixel_local> pls : PLS; |
| @fragment fn fs() { |
| _ = pls; |
| } |
| )", |
| false); |
| } |
| |
| // Check that the FS can have PLS iff the layout has it. |
| TEST_F(PixelLocalStorageTest, Reflection_PLSPresenceMatches) { |
| // Control case: both without PLS is ok. |
| TestFragmentAndLayoutCompat({0, {}, false}, R"( |
| enable chromium_experimental_pixel_local; |
| @fragment fn fs() {} |
| )", |
| true); |
| |
| // Control case: both with PLS is ok. |
| TestFragmentAndLayoutCompat({4, {}}, R"( |
| enable chromium_experimental_pixel_local; |
| struct PLS { a : u32 } |
| var<pixel_local> pls : PLS; |
| @fragment fn fs() { |
| _ = pls; |
| } |
| )", |
| true); |
| |
| // Error case: only shader has PLS |
| TestFragmentAndLayoutCompat({0, {}, false}, R"( |
| enable chromium_experimental_pixel_local; |
| struct PLS { a : u32 } |
| var<pixel_local> pls : PLS; |
| @fragment fn fs() { |
| _ = pls; |
| } |
| )", |
| false); |
| |
| // Error case: only layout has PLS |
| TestFragmentAndLayoutCompat({4, {}}, R"( |
| enable chromium_experimental_pixel_local; |
| @fragment fn fs() { |
| } |
| )", |
| false); |
| |
| // Special valid case: shader doesn't have PLS but the layout's PLS is empty |
| TestFragmentAndLayoutCompat({0, {}}, R"( |
| enable chromium_experimental_pixel_local; |
| @fragment fn fs() { |
| } |
| )", |
| true); |
| } |
| |
| // Check that layout's total PLS size must match the shader's pixel_local block size. |
| TEST_F(PixelLocalStorageTest, Reflection_PLSSize) { |
| // Control case: 8 bytes for both! |
| TestFragmentAndLayoutCompat({8, {}}, R"( |
| enable chromium_experimental_pixel_local; |
| struct PLS { |
| a : u32, |
| b : u32, |
| } |
| var<pixel_local> pls : PLS; |
| @fragment fn fs() { |
| _ = pls; |
| } |
| )", |
| true); |
| |
| // Error case: shader PLS is 4 bytes smaller. |
| TestFragmentAndLayoutCompat({8, {}}, R"( |
| enable chromium_experimental_pixel_local; |
| struct PLS { a : u32 } |
| var<pixel_local> pls : PLS; |
| @fragment fn fs() { |
| _ = pls; |
| } |
| )", |
| false); |
| |
| // Error case: layout PLS is 4 bytes smaller. |
| TestFragmentAndLayoutCompat({4, {}}, R"( |
| enable chromium_experimental_pixel_local; |
| struct PLS { |
| a : u32, |
| b : u32, |
| } |
| var<pixel_local> pls : PLS; |
| @fragment fn fs() { |
| _ = pls; |
| } |
| )", |
| false); |
| } |
| |
| // Check the validation of the layout's PLS format with the shader types. |
| TEST_F(PixelLocalStorageTest, Reflection_FormatMatching) { |
| std::array<wgpu::TextureFormat, 4> testFormats = { |
| wgpu::TextureFormat::R32Uint, wgpu::TextureFormat::R32Sint, wgpu::TextureFormat::R32Float, |
| wgpu::TextureFormat::Undefined, // No storageAttachment. |
| }; |
| |
| std::array<std::string, 3> shaderTypes = {"f32", "i32", "u32"}; |
| |
| for (wgpu::TextureFormat format : testFormats) { |
| for (const std::string& type : shaderTypes) { |
| PLSSpec spec = {4, {}}; |
| if (format != wgpu::TextureFormat::Undefined) { |
| spec = PLSSpec{4, {{0, format}}}; |
| } |
| |
| std::string shader = R"( |
| enable chromium_experimental_pixel_local; |
| struct PLS { a : )" + |
| type + R"(} |
| var<pixel_local> pls : PLS; |
| @fragment fn fs() { |
| _ = pls; |
| })"; |
| |
| // List valid combinations to avoid writing the exact same switch statement as in |
| // ShaderModule.cpp |
| bool success = (format == wgpu::TextureFormat::R32Uint && type == "u32") || |
| (format == wgpu::TextureFormat::R32Sint && type == "i32") || |
| (format == wgpu::TextureFormat::R32Float && type == "f32") || |
| (format == wgpu::TextureFormat::Undefined && type == "u32"); |
| TestFragmentAndLayoutCompat(spec, shader, success); |
| } |
| } |
| } |
| |
| class PixelLocalStorageAndRenderToSingleSampledTest : public PixelLocalStorageTest { |
| protected: |
| WGPUDevice CreateTestDevice(native::Adapter dawnAdapter, |
| wgpu::DeviceDescriptor descriptor) override { |
| // TODO(dawn:1704): Do we need to test both extensions? |
| wgpu::FeatureName requiredFeatures[2] = {wgpu::FeatureName::PixelLocalStorageNonCoherent, |
| wgpu::FeatureName::MSAARenderToSingleSampled}; |
| descriptor.requiredFeatures = requiredFeatures; |
| descriptor.requiredFeatureCount = 2; |
| return dawnAdapter.CreateDevice(&descriptor); |
| } |
| }; |
| |
| // Check that PLS + MSAA render to single sampled is not allowed |
| TEST_F(PixelLocalStorageAndRenderToSingleSampledTest, CombinationIsNotAllowed) { |
| ComboTestPLSRenderPassDescriptor desc; |
| InitializePLSRenderPass(&desc); |
| |
| // Control case: no MSAA render to single sampled. |
| RecordRenderPass(&desc.rpDesc); |
| |
| // Error case: MSAA render to single sampled is added to the color attachment. |
| wgpu::DawnRenderPassColorAttachmentRenderToSingleSampled msaaRenderToSingleSampledDesc; |
| msaaRenderToSingleSampledDesc.implicitSampleCount = 4; |
| desc.colorAttachment.nextInChain = &msaaRenderToSingleSampledDesc; |
| ASSERT_DEVICE_ERROR(RecordRenderPass(&desc.rpDesc)); |
| } |
| |
| // TODO(dawn:1704): Add tests for limits |
| |
| } // anonymous namespace |
| } // namespace dawn |