| // 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 <limits> |
| #include <string> |
| #include <vector> |
| |
| #include "absl/strings/str_format.h" |
| #include "absl/strings/str_join.h" |
| #include "dawn/tests/unittests/validation/ValidationTest.h" |
| #include "dawn/utils/ComboRenderPipelineDescriptor.h" |
| #include "dawn/utils/WGPUHelpers.h" |
| |
| namespace dawn { |
| namespace { |
| |
| class CompatValidationTest : public ValidationTest { |
| protected: |
| bool UseCompatibilityMode() const override { return true; } |
| }; |
| |
| TEST_F(CompatValidationTest, CanNotCreateCubeArrayTextureView) { |
| wgpu::TextureDescriptor descriptor; |
| descriptor.size = {1, 1, 6}; |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| descriptor.format = wgpu::TextureFormat::RGBA8Unorm; |
| descriptor.usage = wgpu::TextureUsage::TextureBinding; |
| wgpu::Texture cubeTexture = device.CreateTexture(&descriptor); |
| |
| { |
| wgpu::TextureViewDescriptor cubeViewDescriptor; |
| cubeViewDescriptor.dimension = wgpu::TextureViewDimension::Cube; |
| cubeViewDescriptor.format = wgpu::TextureFormat::RGBA8Unorm; |
| |
| cubeTexture.CreateView(&cubeViewDescriptor); |
| } |
| |
| { |
| wgpu::TextureViewDescriptor cubeArrayViewDescriptor; |
| cubeArrayViewDescriptor.dimension = wgpu::TextureViewDimension::CubeArray; |
| cubeArrayViewDescriptor.format = wgpu::TextureFormat::RGBA8Unorm; |
| |
| ASSERT_DEVICE_ERROR(cubeTexture.CreateView(&cubeArrayViewDescriptor)); |
| } |
| |
| cubeTexture.Destroy(); |
| } |
| |
| TEST_F(CompatValidationTest, CanNotSpecifyAlternateCompatibleViewFormatRGBA8Unorm) { |
| constexpr wgpu::TextureFormat viewFormat = wgpu::TextureFormat::RGBA8UnormSrgb; |
| |
| wgpu::TextureDescriptor descriptor; |
| descriptor.size = {1, 1, 1}; |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| descriptor.format = wgpu::TextureFormat::RGBA8Unorm; |
| descriptor.usage = wgpu::TextureUsage::TextureBinding; |
| descriptor.viewFormatCount = 1; |
| descriptor.viewFormats = &viewFormat; |
| wgpu::Texture texture; |
| ASSERT_DEVICE_ERROR(texture = device.CreateTexture(&descriptor), |
| testing::HasSubstr("must match format")); |
| texture.Destroy(); |
| } |
| |
| TEST_F(CompatValidationTest, CanNotSpecifyAlternateCompatibleViewFormatRGBA8UnormSrgb) { |
| constexpr wgpu::TextureFormat viewFormat = wgpu::TextureFormat::RGBA8Unorm; |
| |
| wgpu::TextureDescriptor descriptor; |
| descriptor.size = {1, 1, 1}; |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| descriptor.format = wgpu::TextureFormat::RGBA8UnormSrgb; |
| descriptor.usage = wgpu::TextureUsage::TextureBinding; |
| descriptor.viewFormatCount = 1; |
| descriptor.viewFormats = &viewFormat; |
| wgpu::Texture texture; |
| ASSERT_DEVICE_ERROR(texture = device.CreateTexture(&descriptor), |
| testing::HasSubstr("must match format")); |
| texture.Destroy(); |
| } |
| |
| TEST_F(CompatValidationTest, CanNotCreatePipelineWithDifferentPerTargetBlendStateOrWriteMask) { |
| wgpu::ShaderModule module = utils::CreateShaderModule(device, R"( |
| @vertex fn vs() -> @builtin(position) vec4f { |
| return vec4f(0); |
| } |
| |
| struct FragmentOut { |
| @location(0) fragColor0 : vec4f, |
| @location(1) fragColor1 : vec4f, |
| @location(2) fragColor2 : vec4f, |
| } |
| |
| @fragment fn fs() -> FragmentOut { |
| var output : FragmentOut; |
| output.fragColor0 = vec4f(0); |
| output.fragColor1 = vec4f(0); |
| output.fragColor2 = vec4f(0); |
| return output; |
| } |
| )"); |
| |
| utils::ComboRenderPipelineDescriptor testDescriptor; |
| testDescriptor.layout = {}; |
| testDescriptor.vertex.module = module; |
| testDescriptor.cFragment.module = module; |
| testDescriptor.cFragment.targetCount = 3; |
| testDescriptor.cTargets[1].format = wgpu::TextureFormat::Undefined; |
| |
| for (int i = 0; i < 10; ++i) { |
| wgpu::BlendState blend0; |
| wgpu::BlendState blend2; |
| |
| // Blend state intentionally omitted for target 1 |
| testDescriptor.cTargets[0].blend = &blend0; |
| testDescriptor.cTargets[2].blend = &blend2; |
| |
| bool expectError = true; |
| switch (i) { |
| case 0: // default |
| expectError = false; |
| break; |
| case 1: // no blend |
| testDescriptor.cTargets[0].blend = nullptr; |
| break; |
| case 2: // no blend second target |
| testDescriptor.cTargets[2].blend = nullptr; |
| break; |
| case 3: // color.operation |
| blend2.color.operation = wgpu::BlendOperation::Subtract; |
| break; |
| case 4: // color.srcFactor |
| blend2.color.srcFactor = wgpu::BlendFactor::SrcAlpha; |
| break; |
| case 5: // color.dstFactor |
| blend2.color.dstFactor = wgpu::BlendFactor::DstAlpha; |
| break; |
| case 6: // alpha.operation |
| blend2.alpha.operation = wgpu::BlendOperation::Subtract; |
| break; |
| case 7: // alpha.srcFactor |
| blend2.alpha.srcFactor = wgpu::BlendFactor::SrcAlpha; |
| break; |
| case 8: // alpha.dstFactor |
| blend2.alpha.dstFactor = wgpu::BlendFactor::DstAlpha; |
| break; |
| case 9: // writeMask |
| testDescriptor.cTargets[2].writeMask = wgpu::ColorWriteMask::Green; |
| break; |
| default: |
| DAWN_UNREACHABLE(); |
| } |
| |
| if (expectError) { |
| ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&testDescriptor)); |
| } else { |
| device.CreateRenderPipeline(&testDescriptor); |
| } |
| } |
| } |
| |
| TEST_F(CompatValidationTest, CanNotCreatePipelineWithNonZeroDepthBiasClamp) { |
| wgpu::ShaderModule module = utils::CreateShaderModule(device, R"( |
| @vertex fn vs() -> @builtin(position) vec4f { |
| return vec4f(0); |
| } |
| |
| @fragment fn fs() -> @location(0) vec4f { |
| return vec4f(0); |
| } |
| )"); |
| |
| utils::ComboRenderPipelineDescriptor testDescriptor; |
| testDescriptor.layout = {}; |
| testDescriptor.vertex.module = module; |
| testDescriptor.cFragment.module = module; |
| testDescriptor.cFragment.targetCount = 1; |
| testDescriptor.cTargets[1].format = wgpu::TextureFormat::RGBA8Unorm; |
| |
| wgpu::DepthStencilState* depthStencil = |
| testDescriptor.EnableDepthStencil(wgpu::TextureFormat::Depth24Plus); |
| depthStencil->depthWriteEnabled = true; |
| depthStencil->depthBias = 0; |
| depthStencil->depthBiasSlopeScale = 0; |
| |
| depthStencil->depthBiasClamp = 0; |
| device.CreateRenderPipeline(&testDescriptor); |
| |
| depthStencil->depthBiasClamp = 1; |
| ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&testDescriptor)); |
| } |
| |
| TEST_F(CompatValidationTest, CanNotUseFragmentShaderWithSampleMask) { |
| wgpu::ShaderModule moduleSampleMaskOutput = utils::CreateShaderModule(device, R"( |
| @vertex fn vs() -> @builtin(position) vec4f { |
| return vec4f(1); |
| } |
| struct Output { |
| @builtin(sample_mask) mask_out: u32, |
| @location(0) color : vec4f, |
| } |
| @fragment fn fsWithoutSampleMaskUsage() -> @location(0) vec4f { |
| return vec4f(1.0, 1.0, 1.0, 1.0); |
| } |
| @fragment fn fsWithSampleMaskUsage() -> Output { |
| var o: Output; |
| // We need to make sure this sample_mask isn't optimized out even its value equals "no op". |
| o.mask_out = 0xFFFFFFFFu; |
| o.color = vec4f(1.0, 1.0, 1.0, 1.0); |
| return o; |
| } |
| )"); |
| |
| // Check we can use a fragment shader that doesn't use sample_mask from |
| // the same module as one that does. |
| { |
| utils::ComboRenderPipelineDescriptor descriptor; |
| descriptor.vertex.module = moduleSampleMaskOutput; |
| descriptor.cFragment.module = moduleSampleMaskOutput; |
| descriptor.cFragment.entryPoint = "fsWithoutSampleMaskUsage"; |
| descriptor.multisample.count = 4; |
| descriptor.multisample.alphaToCoverageEnabled = false; |
| |
| device.CreateRenderPipeline(&descriptor); |
| } |
| |
| // Check we can not use a fragment shader that uses sample_mask. |
| { |
| utils::ComboRenderPipelineDescriptor descriptor; |
| descriptor.vertex.module = moduleSampleMaskOutput; |
| descriptor.cFragment.module = moduleSampleMaskOutput; |
| descriptor.cFragment.entryPoint = "fsWithSampleMaskUsage"; |
| descriptor.multisample.count = 4; |
| descriptor.multisample.alphaToCoverageEnabled = false; |
| |
| ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&descriptor), |
| testing::HasSubstr("sample_mask")); |
| } |
| } |
| |
| TEST_F(CompatValidationTest, CanNotUseFragmentShaderWithSampleIndex) { |
| wgpu::ShaderModule moduleSampleMaskOutput = utils::CreateShaderModule(device, R"( |
| @vertex fn vs() -> @builtin(position) vec4f { |
| return vec4f(1); |
| } |
| struct Output { |
| @location(0) color : vec4f, |
| } |
| @fragment fn fsWithoutSampleIndexUsage() -> @location(0) vec4f { |
| return vec4f(1.0, 1.0, 1.0, 1.0); |
| } |
| @fragment fn fsWithSampleIndexUsage(@builtin(sample_index) sNdx: u32) -> Output { |
| var o: Output; |
| _ = sNdx; |
| o.color = vec4f(1.0, 1.0, 1.0, 1.0); |
| return o; |
| } |
| )"); |
| |
| // Check we can use a fragment shader that doesn't use sample_index from |
| // the same module as one that does. |
| { |
| utils::ComboRenderPipelineDescriptor descriptor; |
| descriptor.vertex.module = moduleSampleMaskOutput; |
| descriptor.vertex.entryPoint = "vs"; |
| descriptor.cFragment.module = moduleSampleMaskOutput; |
| descriptor.cFragment.entryPoint = "fsWithoutSampleIndexUsage"; |
| descriptor.multisample.count = 4; |
| descriptor.multisample.alphaToCoverageEnabled = false; |
| |
| device.CreateRenderPipeline(&descriptor); |
| } |
| |
| // Check we can not use a fragment shader that uses sample_index. |
| { |
| utils::ComboRenderPipelineDescriptor descriptor; |
| descriptor.vertex.module = moduleSampleMaskOutput; |
| descriptor.vertex.entryPoint = "vs"; |
| descriptor.cFragment.module = moduleSampleMaskOutput; |
| descriptor.cFragment.entryPoint = "fsWithSampleIndexUsage"; |
| descriptor.multisample.count = 4; |
| descriptor.multisample.alphaToCoverageEnabled = false; |
| |
| ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&descriptor), |
| testing::HasSubstr("sample_index")); |
| } |
| } |
| |
| TEST_F(CompatValidationTest, CanNotUseShaderWithUnsupportedInterpolateTypeOrSampling) { |
| static const char* interpolateParams[] = { |
| "linear", |
| "perspective, sample", |
| }; |
| for (auto interpolateParam : interpolateParams) { |
| auto wgsl = absl::StrFormat(R"( |
| struct Vertex { |
| @builtin(position) pos: vec4f, |
| @location(0) @interpolate(%s) color : vec4f, |
| }; |
| @vertex fn vs() -> Vertex { |
| var v: Vertex; |
| v.pos = vec4f(1); |
| v.color = vec4f(1); |
| return v; |
| } |
| @fragment fn fsWithoutBadInterpolationUsage() -> @location(0) vec4f { |
| return vec4f(1); |
| } |
| @fragment fn fsWithBadInterpolationUsage1(v: Vertex) -> @location(0) vec4f { |
| return vec4f(1); |
| } |
| @fragment fn fsWithBadInterpolationUsage2(v: Vertex) -> @location(0) vec4f { |
| return v.pos; |
| } |
| @fragment fn fsWithBadInterpolationUsage3(v: Vertex) -> @location(0) vec4f { |
| return v.color; |
| } |
| )", |
| interpolateParam); |
| wgpu::ShaderModule moduleInterpolationLinear = |
| utils::CreateShaderModule(device, wgsl.c_str()); |
| |
| static const char* entryPoints[] = { |
| "fsWithoutBadInterpolationUsage", |
| "fsWithBadInterpolationUsage1", |
| "fsWithBadInterpolationUsage2", |
| "fsWithBadInterpolationUsage3", |
| }; |
| for (auto entryPoint : entryPoints) { |
| utils::ComboRenderPipelineDescriptor descriptor; |
| descriptor.vertex.module = moduleInterpolationLinear; |
| descriptor.cFragment.module = moduleInterpolationLinear; |
| descriptor.cFragment.entryPoint = entryPoint; |
| |
| bool shouldSucceed = entryPoint == entryPoints[0]; |
| |
| if (shouldSucceed) { |
| device.CreateRenderPipeline(&descriptor); |
| } else { |
| ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&descriptor), |
| testing::HasSubstr("in compatibility mode")); |
| } |
| } |
| } |
| } |
| |
| TEST_F(CompatValidationTest, CanNotCreateRGxxxStorageTexture) { |
| const wgpu::TextureFormat formats[] = { |
| wgpu::TextureFormat::RGBA8Unorm, // pass check |
| wgpu::TextureFormat::RG32Sint, |
| wgpu::TextureFormat::RG32Uint, |
| wgpu::TextureFormat::RG32Float, |
| }; |
| for (auto format : formats) { |
| wgpu::TextureDescriptor descriptor; |
| descriptor.size = {1, 1, 1}; |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| descriptor.format = format; |
| descriptor.usage = wgpu::TextureUsage::StorageBinding; |
| wgpu::Texture texture; |
| |
| if (format == wgpu::TextureFormat::RGBA8Unorm) { |
| texture = device.CreateTexture(&descriptor); |
| } else { |
| ASSERT_DEVICE_ERROR(texture = device.CreateTexture(&descriptor)); |
| } |
| texture.Destroy(); |
| } |
| } |
| |
| constexpr const char* kRenderTwoTexturesOneBindgroupWGSL = R"( |
| @vertex |
| fn vs(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4f { |
| var pos = array( |
| vec4f(-1, 3, 0, 1), |
| vec4f( 3, -1, 0, 1), |
| vec4f(-1, -1, 0, 1)); |
| return pos[VertexIndex]; |
| } |
| |
| @group(0) @binding(0) var tex0 : texture_2d<f32>; |
| @group(0) @binding(1) var tex1 : texture_2d<f32>; |
| |
| @fragment |
| fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f { |
| _ = tex0; |
| _ = tex1; |
| return vec4f(0); |
| } |
| )"; |
| |
| constexpr const char* kRenderTwoTexturesTwoBindgroupsWGSL = R"( |
| @vertex |
| fn vs(@builtin(vertex_index) VertexIndex : u32) -> @builtin(position) vec4f { |
| var pos = array( |
| vec4f(-1, 3, 0, 1), |
| vec4f( 3, -1, 0, 1), |
| vec4f(-1, -1, 0, 1)); |
| return pos[VertexIndex]; |
| } |
| |
| @group(0) @binding(0) var tex0 : texture_2d<f32>; |
| @group(1) @binding(0) var tex1 : texture_2d<f32>; |
| |
| @fragment |
| fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f { |
| _ = tex0; |
| _ = tex1; |
| return vec4f(0); |
| } |
| )"; |
| |
| void TestMultipleTextureViewValidationInRenderPass( |
| wgpu::Device device, |
| const char* wgsl, |
| std::function<void(wgpu::Device device, |
| wgpu::Texture texture, |
| wgpu::RenderPipeline pipeline, |
| std::function<void(wgpu::RenderPassEncoder pass)> drawFn)> fn) { |
| wgpu::TextureDescriptor descriptor; |
| descriptor.size = {2, 1, 1}; |
| descriptor.mipLevelCount = 2; |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| descriptor.format = wgpu::TextureFormat::RGBA8Unorm; |
| descriptor.usage = wgpu::TextureUsage::TextureBinding; |
| wgpu::Texture texture = device.CreateTexture(&descriptor); |
| |
| constexpr uint32_t indices[] = {0, 1, 2}; |
| wgpu::Buffer indexBuffer = |
| utils::CreateBufferFromData(device, indices, sizeof indices, wgpu::BufferUsage::Index); |
| |
| // Create a pipeline that will sample from 2 2D textures and output to an attachment. |
| wgpu::ShaderModule module = utils::CreateShaderModule(device, wgsl); |
| |
| utils::ComboRenderPipelineDescriptor pDesc; |
| pDesc.vertex.module = module; |
| pDesc.cFragment.module = module; |
| pDesc.cTargets[0].format = wgpu::TextureFormat::RGBA8Unorm; |
| wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pDesc); |
| |
| fn(device, texture, pipeline, [](wgpu::RenderPassEncoder pass) { pass.Draw(3); }); |
| |
| fn(device, texture, pipeline, [indexBuffer](wgpu::RenderPassEncoder pass) { |
| pass.SetIndexBuffer(indexBuffer, wgpu::IndexFormat::Uint32); |
| pass.DrawIndexed(3); |
| }); |
| |
| indexBuffer.Destroy(); |
| texture.Destroy(); |
| } |
| |
| // Test we get a validation error if we have 2 different views of a texture |
| // in the same bind group. |
| TEST_F(CompatValidationTest, CanNotDrawDifferentMipsSameTextureSameBindGroup) { |
| TestMultipleTextureViewValidationInRenderPass( |
| device, kRenderTwoTexturesOneBindgroupWGSL, |
| [this](wgpu::Device device, wgpu::Texture texture, wgpu::RenderPipeline pipeline, |
| std::function<void(wgpu::RenderPassEncoder pass)> drawFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip0ViewDesc.baseMipLevel = 0; |
| mip0ViewDesc.mipLevelCount = 1; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip1ViewDesc.baseMipLevel = 1; |
| mip1ViewDesc.mipLevelCount = 1; |
| |
| wgpu::BindGroup bindGroup = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), |
| {{0, texture.CreateView(&mip0ViewDesc)}, {1, texture.CreateView(&mip1ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| utils::BasicRenderPass rp = utils::CreateBasicRenderPass(device, 4, 1); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup); |
| drawFn(pass); |
| pass.End(); |
| |
| ASSERT_DEVICE_ERROR(encoder.Finish(), testing::HasSubstr("different views")); |
| }); |
| } |
| |
| // Test we get a validation error if we have 2 different views of a texture spanning |
| // different bind groups. |
| TEST_F(CompatValidationTest, CanNotDrawDifferentMipsSameTextureDifferentBindGroups) { |
| TestMultipleTextureViewValidationInRenderPass( |
| device, kRenderTwoTexturesTwoBindgroupsWGSL, |
| [this](wgpu::Device device, wgpu::Texture texture, wgpu::RenderPipeline pipeline, |
| std::function<void(wgpu::RenderPassEncoder pass)> drawFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip0ViewDesc.baseMipLevel = 0; |
| mip0ViewDesc.mipLevelCount = 1; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip1ViewDesc.baseMipLevel = 1; |
| mip1ViewDesc.mipLevelCount = 1; |
| |
| wgpu::BindGroup bindGroup0 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), {{0, texture.CreateView(&mip0ViewDesc)}}); |
| |
| wgpu::BindGroup bindGroup1 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(1), {{0, texture.CreateView(&mip1ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| utils::BasicRenderPass rp = utils::CreateBasicRenderPass(device, 4, 1); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup0); |
| pass.SetBindGroup(1, bindGroup1); |
| drawFn(pass); |
| pass.End(); |
| |
| ASSERT_DEVICE_ERROR(encoder.Finish(), testing::HasSubstr("different views")); |
| }); |
| } |
| |
| // Test that it's possible to set a bindgroup that uses a texture with multiple views |
| // which would be an error if you issued a draw command but, you then fix the issue by replacing |
| // the bindgroup with one that does not have multiple views. We're trying to test |
| // that the implementation does the validation at draw command time and not before. |
| TEST_F(CompatValidationTest, CanBindDifferentMipsSameTextureSameBindGroupAndFixWithoutError) { |
| TestMultipleTextureViewValidationInRenderPass( |
| device, kRenderTwoTexturesOneBindgroupWGSL, |
| [](wgpu::Device device, wgpu::Texture texture, wgpu::RenderPipeline pipeline, |
| std::function<void(wgpu::RenderPassEncoder pass)> drawFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip0ViewDesc.baseMipLevel = 0; |
| mip0ViewDesc.mipLevelCount = 1; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip1ViewDesc.baseMipLevel = 1; |
| mip1ViewDesc.mipLevelCount = 1; |
| |
| // Bindgroup with different views of same texture |
| wgpu::BindGroup badBindGroup = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), |
| {{0, texture.CreateView(&mip0ViewDesc)}, {1, texture.CreateView(&mip1ViewDesc)}}); |
| |
| // Bindgroup with same views of texture |
| wgpu::BindGroup goodBindGroup = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), |
| {{0, texture.CreateView(&mip0ViewDesc)}, {1, texture.CreateView(&mip0ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| utils::BasicRenderPass rp = utils::CreateBasicRenderPass(device, 4, 1); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, badBindGroup); |
| pass.SetBindGroup(0, goodBindGroup); |
| drawFn(pass); |
| pass.End(); |
| |
| // No Error is expected |
| encoder.Finish(); |
| }); |
| } |
| |
| // Test that having 2 texture views that have the same settings, in 2 different |
| // bindgroups, does not generate a validation error. |
| TEST_F(CompatValidationTest, CanBindSameViewIn2BindGroups) { |
| TestMultipleTextureViewValidationInRenderPass( |
| device, kRenderTwoTexturesTwoBindgroupsWGSL, |
| [](wgpu::Device device, wgpu::Texture texture, wgpu::RenderPipeline pipeline, |
| std::function<void(wgpu::RenderPassEncoder pass)> drawFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| |
| wgpu::BindGroup bindGroup0 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), {{0, texture.CreateView(&mip0ViewDesc)}}); |
| |
| // Bindgroup with same view of texture as bindGroup0 |
| wgpu::BindGroup bindGroup1 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(1), {{0, texture.CreateView(&mip1ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| utils::BasicRenderPass rp = utils::CreateBasicRenderPass(device, 4, 1); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup0); |
| pass.SetBindGroup(1, bindGroup1); |
| drawFn(pass); |
| pass.End(); |
| |
| // No Error is expected |
| encoder.Finish(); |
| }); |
| } |
| |
| // Test that no validation error happens if we have multiple views of a texture |
| // but don't draw. |
| TEST_F(CompatValidationTest, NoErrorIfMultipleDifferentViewsOfTextureAreNotUsed) { |
| TestMultipleTextureViewValidationInRenderPass( |
| device, kRenderTwoTexturesTwoBindgroupsWGSL, |
| [](wgpu::Device device, wgpu::Texture texture, wgpu::RenderPipeline pipeline, |
| std::function<void(wgpu::RenderPassEncoder pass)> drawFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip1ViewDesc.baseMipLevel = 1; |
| mip1ViewDesc.mipLevelCount = 1; |
| |
| // Bindgroup with different views of same texture |
| wgpu::BindGroup bindGroup0 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), {{0, texture.CreateView(&mip0ViewDesc)}}); |
| |
| // Bindgroup with same views of texture |
| wgpu::BindGroup bindGroup1 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(1), {{0, texture.CreateView(&mip1ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| utils::BasicRenderPass rp = utils::CreateBasicRenderPass(device, 4, 1); |
| wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&rp.renderPassInfo); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup0); |
| pass.SetBindGroup(1, bindGroup1); |
| pass.End(); |
| |
| // No Error is expected because draw was never called |
| encoder.Finish(); |
| }); |
| } |
| |
| constexpr const char* kComputeTwoTexturesOneBindgroupWGSL = R"( |
| @group(0) @binding(0) var tex0 : texture_2d<f32>; |
| @group(0) @binding(1) var tex1 : texture_2d<f32>; |
| |
| @compute @workgroup_size(1) |
| fn cs() { |
| _ = tex0; |
| _ = tex1; |
| } |
| )"; |
| |
| constexpr const char* kComputeTwoTexturesTwoBindgroupsWGSL = R"( |
| @group(0) @binding(0) var tex0 : texture_2d<f32>; |
| @group(1) @binding(0) var tex1 : texture_2d<f32>; |
| |
| @compute @workgroup_size(1) |
| fn cs() { |
| _ = tex0; |
| _ = tex1; |
| } |
| )"; |
| |
| void TestMultipleTextureViewValidationInComputePass( |
| wgpu::Device device, |
| const char* wgsl, |
| wgpu::TextureUsage textureUsage, |
| std::function<void(wgpu::Device device, |
| wgpu::Texture texture, |
| wgpu::ComputePipeline pipeline, |
| std::function<void(wgpu::ComputePassEncoder pass)> dispatchFn)> fn) { |
| wgpu::TextureDescriptor descriptor; |
| descriptor.size = {2, 1, 1}; |
| descriptor.mipLevelCount = 2; |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| descriptor.format = wgpu::TextureFormat::RGBA8Unorm; |
| descriptor.usage = textureUsage; |
| wgpu::Texture texture = device.CreateTexture(&descriptor); |
| |
| constexpr float indirectData[] = {1, 1, 1}; |
| wgpu::Buffer indirectBuffer = utils::CreateBufferFromData( |
| device, indirectData, sizeof indirectData, wgpu::BufferUsage::Indirect); |
| |
| // Create a pipeline that will sample from 2 2D textures and output to an attachment. |
| wgpu::ShaderModule module = utils::CreateShaderModule(device, wgsl); |
| |
| wgpu::ComputePipelineDescriptor pDesc; |
| pDesc.compute.module = module; |
| wgpu::ComputePipeline pipeline = device.CreateComputePipeline(&pDesc); |
| |
| fn(device, texture, pipeline, |
| [](wgpu::ComputePassEncoder pass) { pass.DispatchWorkgroups(1); }); |
| |
| fn(device, texture, pipeline, [indirectBuffer](wgpu::ComputePassEncoder pass) { |
| pass.DispatchWorkgroupsIndirect(indirectBuffer, 0); |
| }); |
| |
| indirectBuffer.Destroy(); |
| texture.Destroy(); |
| } |
| |
| // Test we get a validation error if we have 2 different views of a texture |
| // in the same bind group. |
| TEST_F(CompatValidationTest, CanNotComputeWithDifferentMipsSameTextureSameBindGroup) { |
| TestMultipleTextureViewValidationInComputePass( |
| device, kComputeTwoTexturesOneBindgroupWGSL, wgpu::TextureUsage::TextureBinding, |
| [this](wgpu::Device device, wgpu::Texture texture, wgpu::ComputePipeline pipeline, |
| std::function<void(wgpu::ComputePassEncoder pass)> dispatchFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip0ViewDesc.baseMipLevel = 0; |
| mip0ViewDesc.mipLevelCount = 1; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip1ViewDesc.baseMipLevel = 1; |
| mip1ViewDesc.mipLevelCount = 1; |
| |
| wgpu::BindGroup bindGroup = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), |
| {{0, texture.CreateView(&mip0ViewDesc)}, {1, texture.CreateView(&mip1ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass({}); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup); |
| dispatchFn(pass); |
| pass.End(); |
| |
| ASSERT_DEVICE_ERROR(encoder.Finish(), testing::HasSubstr("different views")); |
| }); |
| } |
| |
| // Test we get a validation error if we have 2 different views of a texture spanning |
| // different bind groups. |
| TEST_F(CompatValidationTest, CanNotComputeWithDifferentMipsSameTextureDifferentBindGroups) { |
| TestMultipleTextureViewValidationInComputePass( |
| device, kComputeTwoTexturesTwoBindgroupsWGSL, wgpu::TextureUsage::TextureBinding, |
| [this](wgpu::Device device, wgpu::Texture texture, wgpu::ComputePipeline pipeline, |
| std::function<void(wgpu::ComputePassEncoder pass)> dispatchFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip0ViewDesc.baseMipLevel = 0; |
| mip0ViewDesc.mipLevelCount = 1; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip1ViewDesc.baseMipLevel = 1; |
| mip1ViewDesc.mipLevelCount = 1; |
| |
| wgpu::BindGroup bindGroup0 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), {{0, texture.CreateView(&mip0ViewDesc)}}); |
| |
| wgpu::BindGroup bindGroup1 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(1), {{0, texture.CreateView(&mip1ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass({}); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup0); |
| pass.SetBindGroup(1, bindGroup1); |
| dispatchFn(pass); |
| pass.End(); |
| |
| ASSERT_DEVICE_ERROR(encoder.Finish(), testing::HasSubstr("different views")); |
| }); |
| } |
| |
| // Test that it's possible to set a bindgroup that uses a texture with multiple views |
| // which would be an error if you issued a draw command but, you then fix the issue by replacing |
| // the bindgroup with one that does not have multiple views. We're trying to test |
| // that the implementation does the validation at draw command time and not before. |
| TEST_F(CompatValidationTest, |
| CanBindDifferentMipsSameTextureSameBindGroupAndFixWithoutErrorInComputePass) { |
| TestMultipleTextureViewValidationInComputePass( |
| device, kComputeTwoTexturesOneBindgroupWGSL, wgpu::TextureUsage::TextureBinding, |
| [](wgpu::Device device, wgpu::Texture texture, wgpu::ComputePipeline pipeline, |
| std::function<void(wgpu::ComputePassEncoder pass)> dispatchFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip0ViewDesc.baseMipLevel = 0; |
| mip0ViewDesc.mipLevelCount = 1; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip1ViewDesc.baseMipLevel = 1; |
| mip1ViewDesc.mipLevelCount = 1; |
| |
| // Bindgroup with different views of same texture |
| wgpu::BindGroup badBindGroup = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), |
| {{0, texture.CreateView(&mip0ViewDesc)}, {1, texture.CreateView(&mip1ViewDesc)}}); |
| |
| // Bindgroup with same views of texture |
| wgpu::BindGroup goodBindGroup = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), |
| {{0, texture.CreateView(&mip0ViewDesc)}, {1, texture.CreateView(&mip0ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass({}); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, badBindGroup); |
| pass.SetBindGroup(0, goodBindGroup); |
| dispatchFn(pass); |
| pass.End(); |
| |
| // No Error is expected |
| encoder.Finish(); |
| }); |
| } |
| |
| // Test that having 2 texture views that have the same settings, in 2 different |
| // bindgroups, does not generate a validation error. |
| TEST_F(CompatValidationTest, CanBindSameViewIn2BindGroupsInComputePass) { |
| TestMultipleTextureViewValidationInComputePass( |
| device, kComputeTwoTexturesTwoBindgroupsWGSL, wgpu::TextureUsage::TextureBinding, |
| [](wgpu::Device device, wgpu::Texture texture, wgpu::ComputePipeline pipeline, |
| std::function<void(wgpu::ComputePassEncoder pass)> dispatchFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| |
| wgpu::BindGroup bindGroup0 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), {{0, texture.CreateView(&mip0ViewDesc)}}); |
| |
| // Bindgroup with same view of texture as bindGroup0 |
| wgpu::BindGroup bindGroup1 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(1), {{0, texture.CreateView(&mip1ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass({}); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup0); |
| pass.SetBindGroup(1, bindGroup1); |
| dispatchFn(pass); |
| pass.End(); |
| |
| // No Error is expected |
| encoder.Finish(); |
| }); |
| } |
| |
| // Test that no validation error happens if we have multiple views of a texture |
| // but don't draw. |
| TEST_F(CompatValidationTest, NoErrorIfMultipleDifferentViewsOfTextureAreNotUsedInComputePass) { |
| TestMultipleTextureViewValidationInComputePass( |
| device, kComputeTwoTexturesTwoBindgroupsWGSL, wgpu::TextureUsage::TextureBinding, |
| [](wgpu::Device device, wgpu::Texture texture, wgpu::ComputePipeline pipeline, |
| std::function<void(wgpu::ComputePassEncoder pass)> dispatchFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip1ViewDesc.baseMipLevel = 1; |
| mip1ViewDesc.mipLevelCount = 1; |
| |
| // Bindgroup with different views of same texture |
| wgpu::BindGroup bindGroup0 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), {{0, texture.CreateView(&mip0ViewDesc)}}); |
| |
| // Bindgroup with same views of texture |
| wgpu::BindGroup bindGroup1 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(1), {{0, texture.CreateView(&mip1ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass({}); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup0); |
| pass.SetBindGroup(1, bindGroup1); |
| pass.End(); |
| |
| // No Error is expected because draw was never called |
| encoder.Finish(); |
| }); |
| } |
| |
| constexpr const char* kComputeTwoStorageTexturesOneBindgroupWGSL = R"( |
| @group(0) @binding(0) var tex0 : texture_storage_2d<rgba8unorm, write>; |
| @group(0) @binding(1) var tex1 : texture_storage_2d<rgba8unorm, write>; |
| |
| @compute @workgroup_size(1) |
| fn cs() { |
| _ = tex0; |
| _ = tex1; |
| } |
| )"; |
| |
| constexpr const char* kComputeTwoStorageTexturesTwoBindgroupsWGSL = R"( |
| @group(0) @binding(0) var tex0 : texture_storage_2d<rgba8unorm, write>; |
| @group(1) @binding(0) var tex1 : texture_storage_2d<rgba8unorm, write>; |
| |
| @compute @workgroup_size(1) |
| fn cs() { |
| _ = tex0; |
| _ = tex1; |
| } |
| )"; |
| |
| // Test we get a validation error if we have 2 different views of a storage texture |
| // in the same bind group. |
| TEST_F(CompatValidationTest, CanNotComputeWithDifferentMipsSameStorageTextureSameBindGroup) { |
| TestMultipleTextureViewValidationInComputePass( |
| device, kComputeTwoStorageTexturesOneBindgroupWGSL, wgpu::TextureUsage::StorageBinding, |
| [this](wgpu::Device device, wgpu::Texture texture, wgpu::ComputePipeline pipeline, |
| std::function<void(wgpu::ComputePassEncoder pass)> dispatchFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip0ViewDesc.baseMipLevel = 0; |
| mip0ViewDesc.mipLevelCount = 1; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip1ViewDesc.baseMipLevel = 1; |
| mip1ViewDesc.mipLevelCount = 1; |
| |
| wgpu::BindGroup bindGroup = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), |
| {{0, texture.CreateView(&mip0ViewDesc)}, {1, texture.CreateView(&mip1ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass({}); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup); |
| dispatchFn(pass); |
| pass.End(); |
| |
| ASSERT_DEVICE_ERROR(encoder.Finish(), testing::HasSubstr("different views")); |
| }); |
| } |
| |
| // Test we get a validation error if we have 2 different views of a texture spanning |
| // different bind groups. |
| TEST_F(CompatValidationTest, CanNotComputeWithDifferentMipsSameStorageTextureDifferentBindGroups) { |
| TestMultipleTextureViewValidationInComputePass( |
| device, kComputeTwoStorageTexturesTwoBindgroupsWGSL, wgpu::TextureUsage::StorageBinding, |
| [this](wgpu::Device device, wgpu::Texture texture, wgpu::ComputePipeline pipeline, |
| std::function<void(wgpu::ComputePassEncoder pass)> dispatchFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip0ViewDesc.baseMipLevel = 0; |
| mip0ViewDesc.mipLevelCount = 1; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip1ViewDesc.baseMipLevel = 1; |
| mip1ViewDesc.mipLevelCount = 1; |
| |
| wgpu::BindGroup bindGroup0 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), {{0, texture.CreateView(&mip0ViewDesc)}}); |
| |
| wgpu::BindGroup bindGroup1 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(1), {{0, texture.CreateView(&mip1ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass({}); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup0); |
| pass.SetBindGroup(1, bindGroup1); |
| dispatchFn(pass); |
| pass.End(); |
| |
| ASSERT_DEVICE_ERROR(encoder.Finish(), testing::HasSubstr("different views")); |
| }); |
| } |
| |
| // Test that it's possible to set a bindgroup that uses a texture with multiple views |
| // which would be an error if you issued a draw command but, you then fix the issue by replacing |
| // the bindgroup with one that does not have multiple views. We're trying to test |
| // that the implementation does the validation at draw command time and not before. |
| TEST_F(CompatValidationTest, |
| CanBindDifferentMipsSameStorageTextureSameBindGroupAndFixWithoutErrorInComputePass) { |
| TestMultipleTextureViewValidationInComputePass( |
| device, kComputeTwoStorageTexturesOneBindgroupWGSL, wgpu::TextureUsage::StorageBinding, |
| [](wgpu::Device device, wgpu::Texture texture, wgpu::ComputePipeline pipeline, |
| std::function<void(wgpu::ComputePassEncoder pass)> dispatchFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip0ViewDesc.baseMipLevel = 0; |
| mip0ViewDesc.mipLevelCount = 1; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip1ViewDesc.baseMipLevel = 1; |
| mip1ViewDesc.mipLevelCount = 1; |
| |
| // Bindgroup with different views of same texture |
| wgpu::BindGroup badBindGroup = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), |
| {{0, texture.CreateView(&mip0ViewDesc)}, {1, texture.CreateView(&mip1ViewDesc)}}); |
| |
| wgpu::TextureDescriptor descriptor; |
| descriptor.size = {2, 1, 1}; |
| descriptor.mipLevelCount = 2; |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| descriptor.format = wgpu::TextureFormat::RGBA8Unorm; |
| descriptor.usage = wgpu::TextureUsage::StorageBinding; |
| wgpu::Texture texture2 = device.CreateTexture(&descriptor); |
| |
| // Bindgroup with same views of texture |
| wgpu::BindGroup goodBindGroup = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), |
| {{0, texture.CreateView(&mip0ViewDesc)}, {1, texture2.CreateView(&mip0ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass({}); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, badBindGroup); |
| pass.SetBindGroup(0, goodBindGroup); |
| dispatchFn(pass); |
| pass.End(); |
| |
| // No Error is expected |
| encoder.Finish(); |
| }); |
| } |
| |
| // Test that no validation error happens if we have multiple views of a texture |
| // but don't draw. |
| TEST_F(CompatValidationTest, |
| NoErrorIfMultipleDifferentViewsOfStorageTextureAreNotUsedInComputePass) { |
| TestMultipleTextureViewValidationInComputePass( |
| device, kComputeTwoStorageTexturesTwoBindgroupsWGSL, wgpu::TextureUsage::StorageBinding, |
| [](wgpu::Device device, wgpu::Texture texture, wgpu::ComputePipeline pipeline, |
| std::function<void(wgpu::ComputePassEncoder pass)> dispatchFn) { |
| wgpu::TextureViewDescriptor mip0ViewDesc; |
| mip0ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip0ViewDesc.mipLevelCount = 1; |
| |
| wgpu::TextureViewDescriptor mip1ViewDesc; |
| mip1ViewDesc.dimension = wgpu::TextureViewDimension::e2D; |
| mip1ViewDesc.baseMipLevel = 1; |
| mip1ViewDesc.mipLevelCount = 1; |
| |
| // Bindgroup with different views of same texture |
| wgpu::BindGroup bindGroup0 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(0), {{0, texture.CreateView(&mip0ViewDesc)}}); |
| |
| // Bindgroup with same views of texture |
| wgpu::BindGroup bindGroup1 = utils::MakeBindGroup( |
| device, pipeline.GetBindGroupLayout(1), {{0, texture.CreateView(&mip1ViewDesc)}}); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| |
| wgpu::ComputePassEncoder pass = encoder.BeginComputePass({}); |
| pass.SetPipeline(pipeline); |
| pass.SetBindGroup(0, bindGroup0); |
| pass.SetBindGroup(1, bindGroup1); |
| pass.End(); |
| |
| // No Error is expected because draw was never called |
| encoder.Finish(); |
| }); |
| } |
| |
| TEST_F(CompatValidationTest, CanNotCreateBGRA8UnormSRGBTexture) { |
| wgpu::TextureDescriptor descriptor; |
| descriptor.size = {1, 1, 1}; |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| descriptor.format = wgpu::TextureFormat::BGRA8UnormSrgb; |
| descriptor.usage = wgpu::TextureUsage::TextureBinding; |
| |
| ASSERT_DEVICE_ERROR(device.CreateTexture(&descriptor), |
| testing::HasSubstr("not supported in compatibility mode")); |
| } |
| |
| TEST_F(CompatValidationTest, CanNotCreateBGRA8UnormTextureWithBGRA8UnormSrgbView) { |
| constexpr wgpu::TextureFormat viewFormat = wgpu::TextureFormat::BGRA8UnormSrgb; |
| |
| wgpu::TextureDescriptor descriptor; |
| descriptor.size = {1, 1, 1}; |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| descriptor.format = wgpu::TextureFormat::BGRA8Unorm; |
| descriptor.usage = wgpu::TextureUsage::TextureBinding; |
| descriptor.viewFormatCount = 1; |
| descriptor.viewFormats = &viewFormat; |
| |
| ASSERT_DEVICE_ERROR(device.CreateTexture(&descriptor), |
| testing::HasSubstr("not supported in compatibility mode")); |
| } |
| |
| TEST_F(CompatValidationTest, CanNotCopyMultisampleTextureToTexture) { |
| wgpu::TextureDescriptor srcDescriptor; |
| srcDescriptor.size = {4, 4, 1}; |
| srcDescriptor.dimension = wgpu::TextureDimension::e2D; |
| srcDescriptor.format = wgpu::TextureFormat::RGBA8Unorm; |
| srcDescriptor.usage = wgpu::TextureUsage::CopySrc | wgpu::TextureUsage::RenderAttachment; |
| srcDescriptor.sampleCount = 4; |
| wgpu::Texture srcTexture = device.CreateTexture(&srcDescriptor); |
| |
| wgpu::TextureDescriptor dstDescriptor; |
| dstDescriptor.size = {4, 4, 1}; |
| dstDescriptor.dimension = wgpu::TextureDimension::e2D; |
| dstDescriptor.format = wgpu::TextureFormat::RGBA8Unorm; |
| dstDescriptor.usage = wgpu::TextureUsage::CopyDst | wgpu::TextureUsage::RenderAttachment; |
| dstDescriptor.sampleCount = 4; |
| wgpu::Texture dstTexture = device.CreateTexture(&dstDescriptor); |
| |
| wgpu::ImageCopyTexture source = utils::CreateImageCopyTexture(srcTexture); |
| wgpu::ImageCopyTexture destination = utils::CreateImageCopyTexture(dstTexture); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.CopyTextureToTexture(&source, &destination, &srcDescriptor.size); |
| ASSERT_DEVICE_ERROR(encoder.Finish(), |
| testing::HasSubstr("cannot be copied in compatibility mode")); |
| } |
| |
| // Regression test for crbug.com/339704108 |
| // Error texture should not resolve mCompatibilityTextureBindingViewDimension, |
| // as dimension could be in bad form. |
| TEST_F(CompatValidationTest, DoNotResolveDefaultTextureBindingViewDimensionOnErrorTexture) { |
| // Set incompatible texture format and view format. |
| // This validation happens before texture dimension validation and binding view dimension |
| // resolving and shall return an error texture. |
| constexpr wgpu::TextureFormat format = wgpu::TextureFormat::BGRA8Unorm; |
| constexpr wgpu::TextureFormat viewFormat = wgpu::TextureFormat::RGBA8UnormSrgb; |
| |
| wgpu::TextureDescriptor descriptor; |
| descriptor.size = {1, 1, 1}; |
| descriptor.dimension = wgpu::TextureDimension::Undefined; |
| descriptor.format = format; |
| descriptor.usage = wgpu::TextureUsage::TextureBinding; |
| descriptor.viewFormatCount = 1; |
| descriptor.viewFormats = &viewFormat; |
| |
| ASSERT_DEVICE_ERROR(device.CreateTexture(&descriptor)); |
| } |
| |
| class CompatTextureViewDimensionValidationTests : public CompatValidationTest { |
| protected: |
| void TestBindingTextureViewDimensions( |
| const uint32_t depth, |
| const wgpu::TextureViewDimension textureBindingViewDimension, |
| const wgpu::TextureViewDimension viewDimension, |
| bool success) { |
| wgpu::BindGroupLayout layout = utils::MakeBindGroupLayout( |
| device, {{0, wgpu::ShaderStage::Fragment, wgpu::TextureSampleType::Float, |
| viewDimension == wgpu::TextureViewDimension::Undefined |
| ? wgpu::TextureViewDimension::e2D |
| : viewDimension}}); |
| |
| wgpu::Texture texture = CreateTextureWithViewDimension(depth, wgpu::TextureDimension::e2D, |
| textureBindingViewDimension); |
| |
| wgpu::TextureViewDescriptor viewDesc = {}; |
| viewDesc.dimension = viewDimension; |
| |
| if (success) { |
| utils::MakeBindGroup(device, layout, {{0, texture.CreateView(&viewDesc)}}); |
| } else { |
| ASSERT_DEVICE_ERROR( |
| utils::MakeBindGroup(device, layout, {{0, texture.CreateView(&viewDesc)}}), |
| testing::HasSubstr("must match textureBindingViewDimension")); |
| } |
| |
| texture.Destroy(); |
| } |
| |
| void TestCreateTextureWithViewDimensionImpl( |
| const uint32_t depth, |
| const wgpu::TextureDimension dimension, |
| const wgpu::TextureViewDimension textureBindingViewDimension, |
| bool success, |
| const char* expectedSubstr) { |
| if (success) { |
| CreateTextureWithViewDimension(depth, dimension, textureBindingViewDimension); |
| } else { |
| ASSERT_DEVICE_ERROR( |
| CreateTextureWithViewDimension(depth, dimension, textureBindingViewDimension); |
| testing::HasSubstr(expectedSubstr)); |
| } |
| } |
| |
| void TestCreateTextureIsCompatibleWithViewDimension( |
| const uint32_t depth, |
| const wgpu::TextureDimension dimension, |
| const wgpu::TextureViewDimension textureBindingViewDimension, |
| bool success) { |
| TestCreateTextureWithViewDimensionImpl(depth, dimension, textureBindingViewDimension, |
| success, "is not compatible with the dimension"); |
| } |
| |
| void TestCreateTextureLayersIsCompatibleWithViewDimension( |
| const uint32_t depth, |
| const wgpu::TextureDimension dimension, |
| const wgpu::TextureViewDimension textureBindingViewDimension, |
| bool success) { |
| TestCreateTextureWithViewDimensionImpl(depth, dimension, textureBindingViewDimension, |
| success, |
| "is only compatible with depthOrArrayLayers =="); |
| } |
| |
| wgpu::Texture CreateTextureWithViewDimension( |
| const uint32_t depth, |
| const wgpu::TextureDimension dimension, |
| const wgpu::TextureViewDimension textureBindingViewDimension) { |
| constexpr wgpu::TextureFormat viewFormat = wgpu::TextureFormat::RGBA8Unorm; |
| |
| wgpu::TextureDescriptor textureDesc; |
| textureDesc.size = {1, 1, depth}; |
| textureDesc.dimension = dimension; |
| textureDesc.format = wgpu::TextureFormat::RGBA8Unorm; |
| textureDesc.usage = wgpu::TextureUsage::TextureBinding; |
| textureDesc.viewFormatCount = 1; |
| textureDesc.viewFormats = &viewFormat; |
| |
| wgpu::TextureBindingViewDimensionDescriptor textureBindingViewDimensionDesc; |
| |
| if (textureBindingViewDimension != wgpu::TextureViewDimension::Undefined) { |
| textureDesc.nextInChain = &textureBindingViewDimensionDesc; |
| textureBindingViewDimensionDesc.textureBindingViewDimension = |
| textureBindingViewDimension; |
| } |
| |
| return device.CreateTexture(&textureDesc); |
| } |
| }; |
| |
| // Note: CubeArray is not included because CubeArray is not allowed |
| // in compatibility mode. |
| const wgpu::TextureViewDimension kViewDimensions[] = { |
| wgpu::TextureViewDimension::e1D, wgpu::TextureViewDimension::e2D, |
| wgpu::TextureViewDimension::e3D, wgpu::TextureViewDimension::e2DArray, |
| wgpu::TextureViewDimension::Cube, |
| }; |
| |
| // Test creating 1d textures with each view dimension. |
| TEST_F(CompatTextureViewDimensionValidationTests, E1D) { |
| for (auto viewDimension : kViewDimensions) { |
| TestCreateTextureIsCompatibleWithViewDimension( |
| 1, wgpu::TextureDimension::e1D, viewDimension, |
| viewDimension == wgpu::TextureViewDimension::e1D); |
| } |
| } |
| |
| // Test creating 2d textures with each view dimension. |
| TEST_F(CompatTextureViewDimensionValidationTests, E2D) { |
| for (auto viewDimension : kViewDimensions) { |
| TestCreateTextureIsCompatibleWithViewDimension( |
| viewDimension == wgpu::TextureViewDimension::e2D ? 1 : 6, wgpu::TextureDimension::e2D, |
| viewDimension, |
| viewDimension != wgpu::TextureViewDimension::e1D && |
| viewDimension != wgpu::TextureViewDimension::e3D); |
| } |
| } |
| |
| // Test creating 1d textures with each view dimension. |
| TEST_F(CompatTextureViewDimensionValidationTests, E3D) { |
| for (auto viewDimension : kViewDimensions) { |
| TestCreateTextureIsCompatibleWithViewDimension( |
| 1, wgpu::TextureDimension::e3D, viewDimension, |
| viewDimension == wgpu::TextureViewDimension::e3D); |
| } |
| } |
| |
| // Test creating a 2d texture with a 2d view and depthOrArrayLayers > 1 fails |
| TEST_F(CompatTextureViewDimensionValidationTests, E2DViewMoreThan1Layer) { |
| TestCreateTextureLayersIsCompatibleWithViewDimension(2, wgpu::TextureDimension::e2D, |
| wgpu::TextureViewDimension::e2D, false); |
| } |
| |
| // Test creating a 2d texture with a cube view with depthOrArrayLayers != 6 fails |
| TEST_F(CompatTextureViewDimensionValidationTests, CubeViewMoreWhereLayersIsNot6) { |
| uint32_t layers[] = {1, 5, 6, 7, 12}; |
| for (auto numLayers : layers) { |
| TestCreateTextureLayersIsCompatibleWithViewDimension(numLayers, wgpu::TextureDimension::e2D, |
| wgpu::TextureViewDimension::Cube, |
| numLayers == 6); |
| } |
| } |
| |
| TEST_F(CompatTextureViewDimensionValidationTests, OneLayerIs2DView) { |
| TestBindingTextureViewDimensions(1, wgpu::TextureViewDimension::Undefined, |
| wgpu::TextureViewDimension::e2D, true); |
| } |
| |
| // Test 2 layer texture gets a 2d-array viewDimension |
| TEST_F(CompatTextureViewDimensionValidationTests, TwoLayersIs2DArrayView) { |
| TestBindingTextureViewDimensions(2, wgpu::TextureViewDimension::Undefined, |
| wgpu::TextureViewDimension::e2DArray, true); |
| } |
| |
| // Test 6 layer texture gets a 2d-array viewDimension |
| TEST_F(CompatTextureViewDimensionValidationTests, SixLayersIs2DArrayView) { |
| TestBindingTextureViewDimensions(6, wgpu::TextureViewDimension::Undefined, |
| wgpu::TextureViewDimension::e2DArray, true); |
| } |
| |
| // Test 2d texture can not be viewed as 2D array |
| TEST_F(CompatTextureViewDimensionValidationTests, TwoDTextureViewDimensionCanNotBeViewedAs2DArray) { |
| TestBindingTextureViewDimensions(1, wgpu::TextureViewDimension::e2D, |
| wgpu::TextureViewDimension::e2DArray, false); |
| } |
| |
| // Test 2d-array texture can not be viewed as cube |
| TEST_F(CompatTextureViewDimensionValidationTests, |
| TwoDArrayTextureViewDimensionCanNotBeViewedAsCube) { |
| TestBindingTextureViewDimensions(6, wgpu::TextureViewDimension::e2DArray, |
| wgpu::TextureViewDimension::Cube, false); |
| } |
| |
| // Test cube texture can not be viewed as 2d-array |
| TEST_F(CompatTextureViewDimensionValidationTests, CubeTextureViewDimensionCanNotBeViewedAs2DArray) { |
| TestBindingTextureViewDimensions(6, wgpu::TextureViewDimension::Cube, |
| wgpu::TextureViewDimension::e2DArray, false); |
| } |
| |
| // Test 2Darray != 2d |
| // Test cube !== 2d |
| // Test cube !== 2d-array |
| |
| class CompatCompressedCopyT2BAndCopyT2TValidationTests : public CompatValidationTest { |
| protected: |
| std::vector<wgpu::FeatureName> GetRequiredFeatures() override { |
| std::vector<wgpu::FeatureName> requiredFeatures; |
| for (TextureInfo textureInfo : textureInfos) { |
| if (adapter.HasFeature(textureInfo.feature)) { |
| requiredFeatures.push_back(textureInfo.feature); |
| } |
| } |
| return requiredFeatures; |
| } |
| |
| struct TextureInfo { |
| wgpu::FeatureName feature; |
| wgpu::TextureFormat format; |
| }; |
| static constexpr TextureInfo textureInfos[] = { |
| { |
| wgpu::FeatureName::TextureCompressionBC, |
| wgpu::TextureFormat::BC2RGBAUnorm, |
| }, |
| { |
| wgpu::FeatureName::TextureCompressionETC2, |
| wgpu::TextureFormat::ETC2RGB8Unorm, |
| }, |
| { |
| wgpu::FeatureName::TextureCompressionASTC, |
| wgpu::TextureFormat::ASTC4x4Unorm, |
| }, |
| }; |
| }; |
| |
| TEST_F(CompatCompressedCopyT2BAndCopyT2TValidationTests, CanNotCopyCompressedTextureToBuffer) { |
| for (TextureInfo textureInfo : textureInfos) { |
| if (!device.HasFeature(textureInfo.feature)) { |
| continue; |
| } |
| |
| wgpu::TextureDescriptor descriptor; |
| descriptor.size = {4, 4, 1}; |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| descriptor.format = textureInfo.format; |
| descriptor.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc; |
| wgpu::Texture texture = device.CreateTexture(&descriptor); |
| |
| wgpu::BufferDescriptor bufferDescriptor; |
| bufferDescriptor.size = 256 * 4; |
| bufferDescriptor.usage = wgpu::BufferUsage::CopyDst; |
| wgpu::Buffer buffer = device.CreateBuffer(&bufferDescriptor); |
| |
| wgpu::ImageCopyTexture source = utils::CreateImageCopyTexture(texture); |
| wgpu::ImageCopyBuffer destination = utils::CreateImageCopyBuffer(buffer, 0, 256, 4); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.CopyTextureToBuffer(&source, &destination, &descriptor.size); |
| ASSERT_DEVICE_ERROR(encoder.Finish(), testing::HasSubstr("cannot be used")); |
| } |
| } |
| |
| TEST_F(CompatCompressedCopyT2BAndCopyT2TValidationTests, CanNotCopyCompressedTextureToTexture) { |
| for (TextureInfo textureInfo : textureInfos) { |
| if (!device.HasFeature(textureInfo.feature)) { |
| continue; |
| } |
| |
| wgpu::TextureDescriptor descriptor; |
| descriptor.size = {4, 4, 1}; |
| descriptor.dimension = wgpu::TextureDimension::e2D; |
| descriptor.format = textureInfo.format; |
| descriptor.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopySrc; |
| wgpu::Texture srcTexture = device.CreateTexture(&descriptor); |
| |
| descriptor.usage = wgpu::TextureUsage::TextureBinding | wgpu::TextureUsage::CopyDst; |
| wgpu::Texture dstTexture = device.CreateTexture(&descriptor); |
| |
| wgpu::ImageCopyTexture source = utils::CreateImageCopyTexture(srcTexture); |
| wgpu::ImageCopyTexture destination = utils::CreateImageCopyTexture(dstTexture); |
| |
| wgpu::CommandEncoder encoder = device.CreateCommandEncoder(); |
| encoder.CopyTextureToTexture(&source, &destination, &descriptor.size); |
| ASSERT_DEVICE_ERROR(encoder.Finish(), testing::HasSubstr("cannot be used")); |
| } |
| } |
| |
| class CompatMaxVertexAttributesTest : public CompatValidationTest { |
| protected: |
| void TestMaxVertexAttributes(bool usesVertexIndex, bool usesInstanceIndex) { |
| wgpu::SupportedLimits limits; |
| device.GetLimits(&limits); |
| |
| uint32_t maxAttributes = limits.limits.maxVertexAttributes; |
| uint32_t numAttributesUsedByBuiltins = |
| (usesVertexIndex ? 1 : 0) + (usesInstanceIndex ? 1 : 0); |
| |
| TestAttributes(maxAttributes - numAttributesUsedByBuiltins, usesVertexIndex, |
| usesInstanceIndex, true); |
| if (usesVertexIndex || usesInstanceIndex) { |
| TestAttributes(maxAttributes - numAttributesUsedByBuiltins + 1, usesVertexIndex, |
| usesInstanceIndex, false); |
| } |
| } |
| |
| void TestAttributes(uint32_t numAttributes, |
| bool usesVertexIndex, |
| bool usesInstanceIndex, |
| bool expectSuccess) { |
| std::vector<std::string> inputs; |
| std::vector<std::string> outputs; |
| |
| utils::ComboRenderPipelineDescriptor descriptor; |
| descriptor.layout = {}; |
| descriptor.vertex.bufferCount = 1; |
| descriptor.cBuffers[0].arrayStride = 16; |
| descriptor.cBuffers[0].attributeCount = numAttributes; |
| |
| for (uint32_t i = 0; i < numAttributes; ++i) { |
| inputs.push_back(absl::StrFormat("@location(%u) v%u: vec4f", i, i)); |
| outputs.push_back(absl::StrFormat("v%u", i)); |
| descriptor.cAttributes[i].format = wgpu::VertexFormat::Float32x4; |
| descriptor.cAttributes[i].shaderLocation = i; |
| } |
| |
| if (usesVertexIndex) { |
| inputs.push_back("@builtin(vertex_index) vNdx: u32"); |
| outputs.push_back("vec4f(f32(vNdx))"); |
| } |
| |
| if (usesInstanceIndex) { |
| inputs.push_back("@builtin(instance_index) iNdx: u32"); |
| outputs.push_back("vec4f(f32(iNdx))"); |
| } |
| |
| auto wgsl = absl::StrFormat(R"( |
| @fragment fn fs() -> @location(0) vec4f { |
| return vec4f(1); |
| } |
| @vertex fn vs(%s) -> @builtin(position) vec4f { |
| return %s; |
| } |
| )", |
| absl::StrJoin(inputs, ", "), absl::StrJoin(outputs, " + ")); |
| |
| wgpu::ShaderModule module = utils::CreateShaderModule(device, wgsl.c_str()); |
| descriptor.vertex.module = module; |
| descriptor.cFragment.module = module; |
| |
| if (expectSuccess) { |
| device.CreateRenderPipeline(&descriptor); |
| } else { |
| ASSERT_DEVICE_ERROR(device.CreateRenderPipeline(&descriptor), |
| testing::HasSubstr("compat")); |
| } |
| } |
| }; |
| |
| TEST_F(CompatMaxVertexAttributesTest, CanUseMaxVertexAttributes) { |
| TestMaxVertexAttributes(false, false); |
| } |
| |
| TEST_F(CompatMaxVertexAttributesTest, VertexIndexTakesAnAttribute) { |
| TestMaxVertexAttributes(true, false); |
| } |
| |
| TEST_F(CompatMaxVertexAttributesTest, InstanceIndexTakesAnAttribute) { |
| TestMaxVertexAttributes(false, true); |
| } |
| |
| TEST_F(CompatMaxVertexAttributesTest, VertexAndInstanceIndexEachTakeAnAttribute) { |
| TestMaxVertexAttributes(true, true); |
| } |
| |
| } // anonymous namespace |
| } // namespace dawn |