From 18163f1223ed9db2bdda88cefa0ea3cb645a9689 Mon Sep 17 00:00:00 2001 From: Austin Eng Date: Tue, 1 Aug 2017 14:20:43 -0400 Subject: [PATCH] end2end: Add blend state tests --- src/tests/CMakeLists.txt | 1 + src/tests/NXTTest.cpp | 1 + src/tests/end2end/BlendStateTests.cpp | 1029 +++++++++++++++++++++++++ 3 files changed, 1031 insertions(+) create mode 100644 src/tests/end2end/BlendStateTests.cpp diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index fd6d569aa9..b61a014215 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -69,6 +69,7 @@ NXTInternalTarget("tests" nxt_unittests) add_executable(nxt_end2end_tests ${END2END_TESTS_DIR}/BasicTests.cpp ${END2END_TESTS_DIR}/BufferTests.cpp + ${END2END_TESTS_DIR}/BlendStateTests.cpp ${END2END_TESTS_DIR}/CopyTests.cpp ${END2END_TESTS_DIR}/DepthStencilStateTests.cpp ${END2END_TESTS_DIR}/InputStateTests.cpp diff --git a/src/tests/NXTTest.cpp b/src/tests/NXTTest.cpp index 00391d482d..94f4a4ff21 100644 --- a/src/tests/NXTTest.cpp +++ b/src/tests/NXTTest.cpp @@ -97,6 +97,7 @@ NXTTest::~NXTTest() { readbackSlots.clear(); queue = nxt::Queue(); device = nxt::Device(); + swapchain = nxt::SwapChain(); delete binding; binding = nullptr; diff --git a/src/tests/end2end/BlendStateTests.cpp b/src/tests/end2end/BlendStateTests.cpp new file mode 100644 index 0000000000..a2ba0ad070 --- /dev/null +++ b/src/tests/end2end/BlendStateTests.cpp @@ -0,0 +1,1029 @@ + +// Copyright 2017 The NXT 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 +#include + +#include "tests/NXTTest.h" + +#include "common/Assert.h" +#include "common/Constants.h" +#include "utils/NXTHelpers.h" + +constexpr static unsigned int kRTSize = 64; + +class BlendStateTest : public NXTTest { + protected: + void SetUp() override { + NXTTest::SetUp(); + + vsModule = utils::CreateShaderModule(device, nxt::ShaderStage::Vertex, R"( + #version 450 + void main() { + const vec2 pos[3] = vec2[3](vec2(-1.f, -1.f), vec2(3.f, -1.f), vec2(-1.f, 3.f)); + gl_Position = vec4(pos[gl_VertexIndex], 0.f, 1.f); + } + )"); + + + bindGroupLayout = device.CreateBindGroupLayoutBuilder() + .SetBindingsType(nxt::ShaderStageBit::Fragment, nxt::BindingType::UniformBuffer, 0, 1) + .GetResult(); + + pipelineLayout = device.CreatePipelineLayoutBuilder() + .SetBindGroupLayout(0, bindGroupLayout) + .GetResult(); + + renderTarget = device.CreateTextureBuilder() + .SetDimension(nxt::TextureDimension::e2D) + .SetExtent(kRTSize, kRTSize, 1) + .SetFormat(nxt::TextureFormat::R8G8B8A8Unorm) + .SetMipLevels(1) + .SetAllowedUsage(nxt::TextureUsageBit::OutputAttachment | nxt::TextureUsageBit::TransferSrc) + .SetInitialUsage(nxt::TextureUsageBit::OutputAttachment) + .GetResult(); + + renderTargetView = renderTarget.CreateTextureViewBuilder().GetResult(); + + renderpass = device.CreateRenderPassBuilder() + .SetAttachmentCount(1) + .SetSubpassCount(1) + .AttachmentSetFormat(0, nxt::TextureFormat::R8G8B8A8Unorm) + .SubpassSetColorAttachment(0, 0, 0) + .GetResult(); + + framebuffer = device.CreateFramebufferBuilder() + .SetRenderPass(renderpass) + .SetDimensions(kRTSize, kRTSize) + .SetAttachment(0, renderTargetView) + .GetResult(); + } + + struct TriangleSpec { + RGBA8 color; + std::array blendFactor = {}; + }; + + // Set up basePipeline and testPipeline. testPipeline has the given blend state on the first attachment. basePipeline has no blending + void SetupSingleSourcePipelines(const nxt::BlendState &blendState) { + nxt::ShaderModule fsModule = utils::CreateShaderModule(device, nxt::ShaderStage::Fragment, R"( + #version 450 + layout(set = 0, binding = 0) uniform myBlock { + vec4 color; + } myUbo; + + layout(location = 0) out vec4 fragColor; + + void main() { + fragColor = myUbo.color; + } + )"); + + basePipeline = device.CreateRenderPipelineBuilder() + .SetSubpass(renderpass, 0) + .SetLayout(pipelineLayout) + .SetStage(nxt::ShaderStage::Vertex, vsModule, "main") + .SetStage(nxt::ShaderStage::Fragment, fsModule, "main") + .GetResult(); + + testPipeline = device.CreateRenderPipelineBuilder() + .SetSubpass(renderpass, 0) + .SetLayout(pipelineLayout) + .SetStage(nxt::ShaderStage::Vertex, vsModule, "main") + .SetStage(nxt::ShaderStage::Fragment, fsModule, "main") + .SetColorAttachmentBlendState(0, blendState) + .GetResult(); + } + + void SetupDualSourcePipelines(const nxt::BlendState &blendState) { + nxt::ShaderModule fsModule = utils::CreateShaderModule(device, nxt::ShaderStage::Fragment, R"( + #version 450 + layout(set = 0, binding = 0) uniform myBlock { + vec4 color0; + vec4 color1; + } myUbo; + + layout(location = 0) out vec4 fragColor0; + layout(location = 1) out vec4 fragColor1; + + void main() { + fragColor0 = myUbo.color0; + fragColor1 = myUbo.color1; + } + )"); + + basePipeline = device.CreateRenderPipelineBuilder() + .SetSubpass(renderpass, 0) + .SetLayout(pipelineLayout) + .SetStage(nxt::ShaderStage::Vertex, vsModule, "main") + .SetStage(nxt::ShaderStage::Fragment, fsModule, "main") + .GetResult(); + + testPipeline = device.CreateRenderPipelineBuilder() + .SetSubpass(renderpass, 0) + .SetLayout(pipelineLayout) + .SetStage(nxt::ShaderStage::Vertex, vsModule, "main") + .SetStage(nxt::ShaderStage::Fragment, fsModule, "main") + .SetColorAttachmentBlendState(0, blendState) + .GetResult(); + } + + // Create a bind group to set the colors as a uniform buffer + template + nxt::BindGroup MakeBindGroupForColors(std::array colors) { + std::array data; + for (unsigned int i = 0; i < N; ++i) { + data[4 * i + 0] = static_cast(colors[i].r) / 255.f; + data[4 * i + 1] = static_cast(colors[i].g) / 255.f; + data[4 * i + 2] = static_cast(colors[i].b) / 255.f; + data[4 * i + 3] = static_cast(colors[i].a) / 255.f; + } + + uint32_t bufferSize = static_cast(4 * N * sizeof(float)); + + nxt::Buffer buffer = utils::CreateFrozenBufferFromData(device, &data, bufferSize, nxt::BufferUsageBit::Uniform); + + nxt::BufferView view = buffer.CreateBufferViewBuilder() + .SetExtent(0, bufferSize) + .GetResult(); + + return device.CreateBindGroupBuilder() + .SetLayout(bindGroupLayout) + .SetUsage(nxt::BindGroupUsage::Frozen) + .SetBufferViews(0, 1, &view) + .GetResult(); + } + + // Test that after drawing a triangle with the base color, and then the given triangle spec, the color is as expected + void DoSingleSourceTest(RGBA8 base, const TriangleSpec& triangle, const RGBA8& expected) { + renderTarget.TransitionUsage(nxt::TextureUsageBit::OutputAttachment); + + nxt::CommandBuffer commands = device.CreateCommandBufferBuilder() + .BeginRenderPass(renderpass, framebuffer) + .BeginRenderSubpass() + // First use the base pipeline to draw a triangle with no blending + .SetRenderPipeline(basePipeline) + .SetBindGroup(0, MakeBindGroupForColors(std::array({ { base } }))) + .DrawArrays(3, 1, 0, 0) + + // Then use the test pipeline to draw the test triangle with blending + .SetRenderPipeline(testPipeline) + .SetBindGroup(0, MakeBindGroupForColors(std::array({ { triangle.color } }))) + .SetBlendColor(triangle.blendFactor[0], triangle.blendFactor[1], triangle.blendFactor[2], triangle.blendFactor[3]) + .DrawArrays(3, 1, 0, 0) + .EndRenderSubpass() + .EndRenderPass() + .GetResult(); + + + queue.Submit(1, &commands); + + EXPECT_PIXEL_RGBA8_EQ(expected, renderTarget, kRTSize / 2, kRTSize / 2); + } + + void DoDualSourceTest(RGBA8 base, const TriangleSpec& triangle, const RGBA8& color1, const RGBA8& expected) { + renderTarget.TransitionUsage(nxt::TextureUsageBit::OutputAttachment); + + nxt::CommandBuffer commands = device.CreateCommandBufferBuilder() + .BeginRenderPass(renderpass, framebuffer) + .BeginRenderSubpass() + .SetRenderPipeline(basePipeline) + .SetBindGroup(0, MakeBindGroupForColors(std::array({ { base, base } }))) + .DrawArrays(3, 1, 0, 0) + + .SetRenderPipeline(testPipeline) + .SetBindGroup(0, MakeBindGroupForColors(std::array({ { triangle.color, color1 } }))) + .SetBlendColor(triangle.blendFactor[0], triangle.blendFactor[1], triangle.blendFactor[2], triangle.blendFactor[3]) + .DrawArrays(3, 1, 0, 0) + .EndRenderSubpass() + .EndRenderPass() + .GetResult(); + + + queue.Submit(1, &commands); + + EXPECT_PIXEL_RGBA8_EQ(expected, renderTarget, kRTSize / 2, kRTSize / 2); + } + + // Given a vector of tests where each element is , check that all expectations are true for the given blend operation + void CheckBlendOperation(RGBA8 base, nxt::BlendOperation operation, std::vector> tests) { + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(operation, nxt::BlendFactor::One, nxt::BlendFactor::One) + .SetAlphaBlend(operation, nxt::BlendFactor::One, nxt::BlendFactor::One) + .GetResult(); + + SetupSingleSourcePipelines(blendState); + + for (const auto& test : tests) { + DoSingleSourceTest(base, { test.first }, test.second); + } + } + + // Given a vector of tests where each element is , check that all expectations are true for the given blend factors + void CheckBlendFactor(RGBA8 base, nxt::BlendFactor colorSrcFactor, nxt::BlendFactor colorDstFactor, nxt::BlendFactor alphaSrcFactor, nxt::BlendFactor alphaDstFactor, std::vector> tests) { + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, colorSrcFactor, colorDstFactor) + .SetAlphaBlend(nxt::BlendOperation::Add, alphaSrcFactor, alphaDstFactor) + .GetResult(); + + SetupSingleSourcePipelines(blendState); + + for (const auto& test : tests) { + DoSingleSourceTest(base, test.first, test.second); + } + } + + void CheckSrcBlendFactor(RGBA8 base, nxt::BlendFactor colorFactor, nxt::BlendFactor alphaFactor, std::vector> tests) { + CheckBlendFactor(base, colorFactor, nxt::BlendFactor::One, alphaFactor, nxt::BlendFactor::One, tests); + } + + void CheckDstBlendFactor(RGBA8 base, nxt::BlendFactor colorFactor, nxt::BlendFactor alphaFactor, std::vector> tests) { + CheckBlendFactor(base, nxt::BlendFactor::One, colorFactor, nxt::BlendFactor::One, alphaFactor, tests); + } + + nxt::Framebuffer framebuffer; + nxt::RenderPass renderpass; + nxt::RenderPipeline basePipeline; + nxt::RenderPipeline testPipeline; + nxt::Texture renderTarget; + nxt::TextureView renderTargetView; + nxt::ShaderModule vsModule; + nxt::BindGroupLayout bindGroupLayout; + nxt::PipelineLayout pipelineLayout; +}; + +namespace { + // Add two colors and clamp + constexpr RGBA8 operator+(const RGBA8& col1, const RGBA8& col2) { + int r = static_cast(col1.r) + static_cast(col2.r); + int g = static_cast(col1.g) + static_cast(col2.g); + int b = static_cast(col1.b) + static_cast(col2.b); + int a = static_cast(col1.a) + static_cast(col2.a); + r = (r > 255 ? 255 : (r < 0 ? 0 : r)); + g = (g > 255 ? 255 : (g < 0 ? 0 : g)); + b = (b > 255 ? 255 : (b < 0 ? 0 : b)); + a = (a > 255 ? 255 : (a < 0 ? 0 : a)); + + return RGBA8(static_cast(r), static_cast(g), static_cast(b), static_cast(a)); + } + + // Subtract two colors and clamp + constexpr RGBA8 operator-(const RGBA8& col1, const RGBA8& col2) { + int r = static_cast(col1.r) - static_cast(col2.r); + int g = static_cast(col1.g) - static_cast(col2.g); + int b = static_cast(col1.b) - static_cast(col2.b); + int a = static_cast(col1.a) - static_cast(col2.a); + r = (r > 255 ? 255 : (r < 0 ? 0 : r)); + g = (g > 255 ? 255 : (g < 0 ? 0 : g)); + b = (b > 255 ? 255 : (b < 0 ? 0 : b)); + a = (a > 255 ? 255 : (a < 0 ? 0 : a)); + + return RGBA8(static_cast(r), static_cast(g), static_cast(b), static_cast(a)); + } + + // Get the component-wise minimum of two colors + RGBA8 min(const RGBA8& col1, const RGBA8& col2) { + return RGBA8( + std::min(col1.r, col2.r), + std::min(col1.g, col2.g), + std::min(col1.b, col2.b), + std::min(col1.a, col2.a) + ); + } + + // Get the component-wise maximum of two colors + RGBA8 max(const RGBA8& col1, const RGBA8& col2) { + return RGBA8( + std::max(col1.r, col2.r), + std::max(col1.g, col2.g), + std::max(col1.b, col2.b), + std::max(col1.a, col2.a) + ); + } + + // Blend two RGBA8 color values parameterized by the provided factors in the range [0.f, 1.f] + RGBA8 mix(const RGBA8& col1, const RGBA8& col2, std::array fac) { + float r = static_cast(col1.r) * (1.f - fac[0]) + static_cast(col2.r) * fac[0]; + float g = static_cast(col1.g) * (1.f - fac[1]) + static_cast(col2.g) * fac[1]; + float b = static_cast(col1.b) * (1.f - fac[2]) + static_cast(col2.b) * fac[2]; + float a = static_cast(col1.a) * (1.f - fac[3]) + static_cast(col2.a) * fac[3]; + + return RGBA8({ static_cast(std::round(r)), static_cast(std::round(g)), static_cast(std::round(b)), static_cast(std::round(a)) }); + } + + // Blend two RGBA8 color values parameterized by the provided RGBA8 factor + RGBA8 mix(const RGBA8& col1, const RGBA8& col2, const RGBA8& fac) { + std::array f = { { + static_cast(fac.r) / 255.f, + static_cast(fac.g) / 255.f, + static_cast(fac.b) / 255.f, + static_cast(fac.a) / 255.f, + } }; + return mix(col1, col2, f); + } + + constexpr std::array kColors = { { + // check operations over multiple channels + RGBA8(64,0,0,0), + RGBA8(0,64,0,0), + RGBA8(64,0,32,0), + RGBA8(0,64,32,0), + RGBA8(128,0,128,128), + RGBA8(0,128,128,128), + + // check cases that may cause overflow + RGBA8(0,0,0,0), + RGBA8(255,255,255,255), + } }; +} + +// Test compilation and usage of the fixture +TEST_P(BlendStateTest, Basic) { + nxt::BlendState blendState = device.CreateBlendStateBuilder().GetResult(); + SetupSingleSourcePipelines(blendState); + + DoSingleSourceTest(RGBA8(0, 0, 0, 0), { RGBA8(255, 0, 0, 0) }, RGBA8(255, 0, 0, 0)); +} + +// The following tests check test that the blend operation works +TEST_P(BlendStateTest, BlendOperationAdd) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + return std::make_pair(color, base + color); + }); + CheckBlendOperation(base, nxt::BlendOperation::Add, tests); +} + +TEST_P(BlendStateTest, BlendOperationSubtract) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + return std::make_pair(color, color - base); + }); + CheckBlendOperation(base, nxt::BlendOperation::Subtract, tests); +} + +TEST_P(BlendStateTest, BlendOperationReverseSubtract) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + return std::make_pair(color, base - color); + }); + CheckBlendOperation(base, nxt::BlendOperation::ReverseSubtract, tests); +} + +TEST_P(BlendStateTest, BlendOperationMin) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + return std::make_pair(color, min(base, color)); + }); + CheckBlendOperation(base, nxt::BlendOperation::Min, tests); +} + +TEST_P(BlendStateTest, BlendOperationMax) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + return std::make_pair(color, max(base, color)); + }); + CheckBlendOperation(base, nxt::BlendOperation::Max, tests); +} + +// The following tests check that the Source blend factor works +TEST_P(BlendStateTest, SrcBlendFactorZero) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + return std::make_pair(TriangleSpec({ { color } }), base); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::Zero, nxt::BlendFactor::Zero, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorOne) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + return std::make_pair(TriangleSpec({ { color } }), base + color); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::One, nxt::BlendFactor::One, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorSrcColor) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac = color; + fac.a = 0; + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::SrcColor, nxt::BlendFactor::Zero, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorOneMinusSrcColor) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac = RGBA8(255, 255, 255, 255) - color; + fac.a = 0; + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::OneMinusSrcColor, nxt::BlendFactor::Zero, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorSrcAlpha) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac(color.a, color.a, color.a, color.a); + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::SrcAlpha, nxt::BlendFactor::SrcAlpha, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorOneMinusSrcAlpha) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac = RGBA8(255, 255, 255, 255) - RGBA8(color.a, color.a, color.a, color.a); + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::OneMinusSrcAlpha, nxt::BlendFactor::OneMinusSrcAlpha, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorDstColor) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac = base; + fac.a = 0; + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::DstColor, nxt::BlendFactor::Zero, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorOneMinusDstColor) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac = RGBA8(255, 255, 255, 255) - base; + fac.a = 0; + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::OneMinusDstColor, nxt::BlendFactor::Zero, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorDstAlpha) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac(base.a, base.a, base.a, base.a); + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::DstAlpha, nxt::BlendFactor::DstAlpha, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorOneMinusDstAlpha) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac = RGBA8(255, 255, 255, 255) - RGBA8(base.a, base.a, base.a, base.a); + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::OneMinusDstAlpha, nxt::BlendFactor::OneMinusDstAlpha, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorSrcAlphaSaturated) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + uint8_t f = std::min(color.a, static_cast(255 - base.a)); + RGBA8 fac(f, f, f, 255); + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::SrcAlphaSaturated, nxt::BlendFactor::SrcAlphaSaturated, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorBlendColor) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + auto triangleSpec = TriangleSpec({ { color }, {{ 0.2f, 0.4f, 0.6f, 0.8f }} }); + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, triangleSpec.blendFactor); + return std::make_pair(triangleSpec, expected); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::BlendColor, nxt::BlendFactor::BlendColor, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorOneMinusBlendColor) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + auto triangleSpec = TriangleSpec({ { color }, {{ 0.2f, 0.4f, 0.6f, 0.8f }} }); + std::array f = { { 0.8f, 0.6f, 0.4f, 0.2f } }; + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, f); + return std::make_pair(triangleSpec, expected); + }); + CheckSrcBlendFactor(base, nxt::BlendFactor::OneMinusBlendColor, nxt::BlendFactor::OneMinusBlendColor, tests); +} + +TEST_P(BlendStateTest, SrcBlendFactorSrc1Color) { + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, nxt::BlendFactor::Src1Color, nxt::BlendFactor::One) + .GetResult(); + SetupDualSourcePipelines(blendState); + + RGBA8 base(0, 0, 0, 0); + for (auto& color : kColors) { + RGBA8 color1(64, 32, 192, 128); + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, color1); + expected.a = color.a; + DoDualSourceTest(base, { color }, color1, expected); + } +} + +TEST_P(BlendStateTest, SrcBlendFactorOneMinusSrc1Color) { + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, nxt::BlendFactor::OneMinusSrc1Color, nxt::BlendFactor::One) + .GetResult(); + SetupDualSourcePipelines(blendState); + + RGBA8 base(0, 0, 0, 0); + for (auto& color : kColors) { + RGBA8 color1(64, 32, 192, 128); + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, RGBA8(255, 255, 255, 255) - color1); + expected.a = color.a; + DoDualSourceTest(base, { color }, color1, expected); + } +} + +TEST_P(BlendStateTest, SrcBlendFactorSrc1Alpha) { + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, nxt::BlendFactor::Src1Alpha, nxt::BlendFactor::One) + .SetAlphaBlend(nxt::BlendOperation::Add, nxt::BlendFactor::Src1Alpha, nxt::BlendFactor::One) + .GetResult(); + SetupDualSourcePipelines(blendState); + + RGBA8 base(0, 0, 0, 0); + for (auto& color : kColors) { + RGBA8 color1(64, 32, 192, 128); + RGBA8 fac(color1.a, color1.a, color1.a, color1.a); + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, fac); + DoDualSourceTest(base, { color }, color1, expected); + } +} + +TEST_P(BlendStateTest, SrcBlendFactorOneMinusSrc1Alpha) { + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, nxt::BlendFactor::OneMinusSrc1Alpha, nxt::BlendFactor::One) + .SetAlphaBlend(nxt::BlendOperation::Add, nxt::BlendFactor::OneMinusSrc1Alpha, nxt::BlendFactor::One) + .GetResult(); + SetupDualSourcePipelines(blendState); + + RGBA8 base(0, 0, 0, 0); + for (auto& color : kColors) { + RGBA8 color1(64, 32, 192, 128); + RGBA8 fac = RGBA8(255, 255, 255, 255) - RGBA8(color1.a, color1.a, color1.a, color1.a); + RGBA8 expected = base + mix(RGBA8(0, 0, 0, 0), color, fac); + DoDualSourceTest(base, { color }, color1, expected); + } +} + +// The following tests check that the Destination blend factor works +TEST_P(BlendStateTest, DstBlendFactorZero) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + return std::make_pair(TriangleSpec({ { color } }), color); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::Zero, nxt::BlendFactor::Zero, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorOne) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + return std::make_pair(TriangleSpec({ { color } }), base + color); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::One, nxt::BlendFactor::One, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorSrcColor) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac = color; + fac.a = 0; + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::SrcColor, nxt::BlendFactor::Zero, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorOneMinusSrcColor) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac = RGBA8(255, 255, 255, 255) - color; + fac.a = 0; + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::OneMinusSrcColor, nxt::BlendFactor::Zero, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorSrcAlpha) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac(color.a, color.a, color.a, color.a); + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::SrcAlpha, nxt::BlendFactor::SrcAlpha, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorOneMinusSrcAlpha) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac = RGBA8(255, 255, 255, 255) - RGBA8(color.a, color.a, color.a, color.a); + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::OneMinusSrcAlpha, nxt::BlendFactor::OneMinusSrcAlpha, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorDstColor) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac = base; + fac.a = 0; + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::DstColor, nxt::BlendFactor::Zero, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorOneMinusDstColor) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac = RGBA8(255, 255, 255, 255) - base; + fac.a = 0; + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::OneMinusDstColor, nxt::BlendFactor::Zero, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorDstAlpha) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac(base.a, base.a, base.a, base.a); + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::DstAlpha, nxt::BlendFactor::DstAlpha, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorOneMinusDstAlpha) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + RGBA8 fac = RGBA8(255, 255, 255, 255) - RGBA8(base.a, base.a, base.a, base.a); + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::OneMinusDstAlpha, nxt::BlendFactor::OneMinusDstAlpha, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorSrcAlphaSaturated) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + uint8_t f = std::min(color.a, static_cast(255 - base.a)); + RGBA8 fac(f, f, f, 255); + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, fac); + return std::make_pair(TriangleSpec({ { color } }), expected); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::SrcAlphaSaturated, nxt::BlendFactor::SrcAlphaSaturated, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorBlendColor) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + auto triangleSpec = TriangleSpec({ { color }, {{ 0.2f, 0.4f, 0.6f, 0.8f }} }); + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, triangleSpec.blendFactor); + return std::make_pair(triangleSpec, expected); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::BlendColor, nxt::BlendFactor::BlendColor, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorOneMinusBlendColor) { + RGBA8 base(32, 64, 128, 192); + std::vector> tests; + std::transform(kColors.begin(), kColors.end(), std::back_inserter(tests), [&](const RGBA8& color) { + auto triangleSpec = TriangleSpec({ { color }, {{ 0.2f, 0.4f, 0.6f, 0.8f }} }); + std::array f = { { 0.8f, 0.6f, 0.4f, 0.2f } }; + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, f); + return std::make_pair(triangleSpec, expected); + }); + CheckDstBlendFactor(base, nxt::BlendFactor::OneMinusBlendColor, nxt::BlendFactor::OneMinusBlendColor, tests); +} + +TEST_P(BlendStateTest, DstBlendFactorSrc1Color) { + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::Src1Color) + .GetResult(); + SetupDualSourcePipelines(blendState); + + RGBA8 base(0, 0, 0, 0); + for (auto& color : kColors) { + RGBA8 color1(64, 32, 192, 128); + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, color1); + expected.a = color.a; + DoDualSourceTest(base, { color }, color1, expected); + } +} + +TEST_P(BlendStateTest, DstBlendFactorOneMinusSrc1Color) { + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::OneMinusSrc1Color) + .GetResult(); + SetupDualSourcePipelines(blendState); + + RGBA8 base(0, 0, 0, 0); + for (auto& color : kColors) { + RGBA8 color1(64, 32, 192, 128); + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, RGBA8(255, 255, 255, 255) - color1); + expected.a = color.a; + DoDualSourceTest(base, { color }, color1, expected); + } +} + +TEST_P(BlendStateTest, DstBlendFactorSrc1Alpha) { + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::Src1Alpha) + .SetAlphaBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::Src1Alpha) + .GetResult(); + SetupDualSourcePipelines(blendState); + + RGBA8 base(0, 0, 0, 0); + for (auto& color : kColors) { + RGBA8 color1(64, 32, 192, 128); + RGBA8 fac(color1.a, color1.a, color1.a, color1.a); + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, fac); + DoDualSourceTest(base, { color }, color1, expected); + } +} + +TEST_P(BlendStateTest, DstBlendFactorOneMinusSrc1Alpha) { + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::OneMinusSrc1Alpha) + .SetAlphaBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::OneMinusSrc1Alpha) + .GetResult(); + SetupDualSourcePipelines(blendState); + + RGBA8 base(0, 0, 0, 0); + for (auto& color : kColors) { + RGBA8 color1(64, 32, 192, 128); + RGBA8 fac = RGBA8(255, 255, 255, 255) - RGBA8(color1.a, color1.a, color1.a, color1.a); + RGBA8 expected = color + mix(RGBA8(0, 0, 0, 0), base, fac); + DoDualSourceTest(base, { color }, color1, expected); + } +} + +// Check that the color write mask works +TEST_P(BlendStateTest, ColorWriteMask) { + { + // Test single channel color write + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::One) + .SetAlphaBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::One) + .SetColorWriteMask(nxt::ColorWriteMask::Red) + .GetResult(); + SetupSingleSourcePipelines(blendState); + + RGBA8 base(32, 64, 128, 192); + for (auto& color : kColors) { + RGBA8 expected = base + RGBA8(color.r, 0, 0, 0); + DoSingleSourceTest(base, { color }, expected); + } + } + + { + // Test multi channel color write + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::One) + .SetAlphaBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::One) + .SetColorWriteMask(nxt::ColorWriteMask::Green | nxt::ColorWriteMask::Alpha) + .GetResult(); + SetupSingleSourcePipelines(blendState); + + RGBA8 base(32, 64, 128, 192); + for (auto& color : kColors) { + RGBA8 expected = base + RGBA8(0, color.g, 0, color.a); + DoSingleSourceTest(base, { color }, expected); + } + } + + { + // Test no channel color write + nxt::BlendState blendState = device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::One) + .SetAlphaBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::One) + .SetColorWriteMask(nxt::ColorWriteMask::None) + .GetResult(); + SetupSingleSourcePipelines(blendState); + + RGBA8 base(32, 64, 128, 192); + for (auto& color : kColors) { + DoSingleSourceTest(base, { color }, base); + } + } +} + +// Test that independent blend states on render targets works +TEST_P(BlendStateTest, IndependentBlendState) { + + std::array renderTargets; + std::array renderTargetViews; + + for (uint32_t i = 0; i < 5; ++i) { + renderTargets[i] = device.CreateTextureBuilder() + .SetDimension(nxt::TextureDimension::e2D) + .SetExtent(kRTSize, kRTSize, 1) + .SetFormat(nxt::TextureFormat::R8G8B8A8Unorm) + .SetMipLevels(1) + .SetAllowedUsage(nxt::TextureUsageBit::OutputAttachment | nxt::TextureUsageBit::TransferSrc) + .SetInitialUsage(nxt::TextureUsageBit::OutputAttachment) + .GetResult(); + renderTargetViews[i] = renderTargets[i].CreateTextureViewBuilder().GetResult(); + } + + renderpass = device.CreateRenderPassBuilder() + .SetAttachmentCount(5) + .SetSubpassCount(1) + .AttachmentSetFormat(0, nxt::TextureFormat::R8G8B8A8Unorm) + .AttachmentSetFormat(1, nxt::TextureFormat::R8G8B8A8Unorm) + .AttachmentSetFormat(2, nxt::TextureFormat::R8G8B8A8Unorm) + .AttachmentSetFormat(3, nxt::TextureFormat::R8G8B8A8Unorm) + .AttachmentSetFormat(4, nxt::TextureFormat::R8G8B8A8Unorm) + // Scatter these so we know indexing to the right shader location is working + .SubpassSetColorAttachment(0, 0, 2) + // We skip attachment index 1 to check the case where the blend states in the pipeline state are not tightly packed + .SubpassSetColorAttachment(0, 1, 4) + .SubpassSetColorAttachment(0, 2, 3) + .SubpassSetColorAttachment(0, 3, 0) + .GetResult(); + + framebuffer = device.CreateFramebufferBuilder() + .SetRenderPass(renderpass) + .SetDimensions(kRTSize, kRTSize) + .SetAttachment(0, renderTargetViews[0]) + .SetAttachment(1, renderTargetViews[1]) + .SetAttachment(2, renderTargetViews[2]) + .SetAttachment(3, renderTargetViews[3]) + .SetAttachment(4, renderTargetViews[4]) + .GetResult(); + + nxt::ShaderModule fsModule = utils::CreateShaderModule(device, nxt::ShaderStage::Fragment, R"( + #version 450 + layout(set = 0, binding = 0) uniform myBlock { + vec4 color0; + vec4 color1; + vec4 color2; + vec4 color3; + } myUbo; + + layout(location = 0) out vec4 fragColor0; + layout(location = 1) out vec4 fragColor1; + layout(location = 2) out vec4 fragColor2; + layout(location = 3) out vec4 fragColor3; + + void main() { + fragColor0 = myUbo.color0; + fragColor1 = myUbo.color1; + fragColor2 = myUbo.color2; + fragColor3 = myUbo.color3; + } + )"); + + std::array blendStates = { { + device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::One) + .SetAlphaBlend(nxt::BlendOperation::Add, nxt::BlendFactor::One, nxt::BlendFactor::One) + .GetResult(), + device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Subtract, nxt::BlendFactor::One, nxt::BlendFactor::One) + .SetAlphaBlend(nxt::BlendOperation::Subtract, nxt::BlendFactor::One, nxt::BlendFactor::One) + .GetResult(), + device.CreateBlendStateBuilder() + .SetBlendEnabled(true) + .SetColorBlend(nxt::BlendOperation::Min, nxt::BlendFactor::One, nxt::BlendFactor::One) + .SetAlphaBlend(nxt::BlendOperation::Min, nxt::BlendFactor::One, nxt::BlendFactor::One) + .GetResult(), + } }; + + basePipeline = device.CreateRenderPipelineBuilder() + .SetSubpass(renderpass, 0) + .SetLayout(pipelineLayout) + .SetStage(nxt::ShaderStage::Vertex, vsModule, "main") + .SetStage(nxt::ShaderStage::Fragment, fsModule, "main") + .GetResult(); + + testPipeline = device.CreateRenderPipelineBuilder() + .SetSubpass(renderpass, 0) + .SetLayout(pipelineLayout) + .SetStage(nxt::ShaderStage::Vertex, vsModule, "main") + .SetStage(nxt::ShaderStage::Fragment, fsModule, "main") + .SetColorAttachmentBlendState(0, blendStates[0]) + .SetColorAttachmentBlendState(1, blendStates[1]) + // Blend state not set on third color attachment. It should be default + .SetColorAttachmentBlendState(3, blendStates[2]) + .GetResult(); + + + for (unsigned int c = 0; c < kColors.size(); ++c) { + RGBA8 base = kColors[((c + 31) * 29) % kColors.size()]; + RGBA8 color0 = kColors[((c + 19) * 13) % kColors.size()]; + RGBA8 color1 = kColors[((c + 11) * 43) % kColors.size()]; + RGBA8 color2 = kColors[((c + 7) * 3) % kColors.size()]; + RGBA8 color3 = kColors[((c + 13) * 71) % kColors.size()]; + + RGBA8 expected0 = color0 + base; + RGBA8 expected1 = color1 - base; + RGBA8 expected2 = color2; + RGBA8 expected3 = min(color3, base); + + renderTargets[2].TransitionUsage(nxt::TextureUsageBit::OutputAttachment); + renderTargets[4].TransitionUsage(nxt::TextureUsageBit::OutputAttachment); + renderTargets[3].TransitionUsage(nxt::TextureUsageBit::OutputAttachment); + renderTargets[0].TransitionUsage(nxt::TextureUsageBit::OutputAttachment); + + nxt::CommandBuffer commands = device.CreateCommandBufferBuilder() + .BeginRenderPass(renderpass, framebuffer) + .BeginRenderSubpass() + .SetRenderPipeline(basePipeline) + .SetBindGroup(0, MakeBindGroupForColors(std::array({ { base, base, base, base } }))) + .DrawArrays(3, 1, 0, 0) + + .SetRenderPipeline(testPipeline) + .SetBindGroup(0, MakeBindGroupForColors(std::array({ { color0, color1, color2, color3 } }))) + .DrawArrays(3, 1, 0, 0) + .EndRenderSubpass() + .EndRenderPass() + .GetResult(); + + queue.Submit(1, &commands); + + EXPECT_PIXEL_RGBA8_EQ(expected0, renderTargets[2], kRTSize / 2, kRTSize / 2) << "Attachment slot 0 using render target 2 should have been " << color0 << " + " << base << " = " << expected0; + EXPECT_PIXEL_RGBA8_EQ(expected1, renderTargets[4], kRTSize / 2, kRTSize / 2) << "Attachment slot 1 using render target 4 should have been " << color1 << " - " << base << " = " << expected1; + EXPECT_PIXEL_RGBA8_EQ(expected2, renderTargets[3], kRTSize / 2, kRTSize / 2) << "Attachment slot 2 using render target 3 should have been " << color2 << " = " << expected2 << "(no blending)"; + EXPECT_PIXEL_RGBA8_EQ(expected3, renderTargets[0], kRTSize / 2, kRTSize / 2) << "Attachment slot 3 using render target 0 should have been min(" << color3 << ", " << base << ") = " << expected3; + } +} + +NXT_INSTANTIATE_TEST(BlendStateTest, D3D12Backend, MetalBackend, OpenGLBackend)