Add end-to-end TextureFormatsTier1 tests for Snorm render attachments
Confirm Snorm texture formats function as render attachments with texture-formats-tier1 enabled. These tests validate correct fragment shader output conversion and storage to Snorm textures, matching readback values against expected ranges considering float-to-Snorm rules and a 0.6f ULP tolerance.
Bug: 421941589
Change-Id: Id31e4f89a1e5ad853567d9f99a7bf07779b6c466
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/246015
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Jiawei Shao <jiawei.shao@intel.com>
Reviewed-by: Jiawei Shao <jiawei.shao@intel.com>
diff --git a/src/dawn/native/d3d11/PhysicalDeviceD3D11.cpp b/src/dawn/native/d3d11/PhysicalDeviceD3D11.cpp
index 3f1b4c1..210dafb 100644
--- a/src/dawn/native/d3d11/PhysicalDeviceD3D11.cpp
+++ b/src/dawn/native/d3d11/PhysicalDeviceD3D11.cpp
@@ -161,6 +161,7 @@
EnableFeature(Feature::DawnLoadResolveTexture);
EnableFeature(Feature::DawnPartialLoadResolveTexture);
EnableFeature(Feature::RG11B10UfloatRenderable);
+ EnableFeature(Feature::TextureFormatsTier1);
if (mDeviceInfo.isUMA && mDeviceInfo.supportsMapNoOverwriteDynamicBuffers) {
// With UMA we should allow mapping usages on more type of buffers.
EnableFeature(Feature::BufferMapExtendedUsages);
diff --git a/src/dawn/native/d3d12/PhysicalDeviceD3D12.cpp b/src/dawn/native/d3d12/PhysicalDeviceD3D12.cpp
index 77a1668..e8a8e03 100644
--- a/src/dawn/native/d3d12/PhysicalDeviceD3D12.cpp
+++ b/src/dawn/native/d3d12/PhysicalDeviceD3D12.cpp
@@ -168,6 +168,7 @@
EnableFeature(Feature::MultiDrawIndirect);
EnableFeature(Feature::ClipDistances);
EnableFeature(Feature::FlexibleTextureViews);
+ EnableFeature(Feature::TextureFormatsTier1);
if (AreTimestampQueriesSupported()) {
EnableFeature(Feature::TimestampQuery);
diff --git a/src/dawn/native/metal/PhysicalDeviceMTL.mm b/src/dawn/native/metal/PhysicalDeviceMTL.mm
index 7904642..1fa71ae 100644
--- a/src/dawn/native/metal/PhysicalDeviceMTL.mm
+++ b/src/dawn/native/metal/PhysicalDeviceMTL.mm
@@ -678,6 +678,7 @@
EnableFeature(Feature::ClipDistances);
EnableFeature(Feature::Float32Blendable);
EnableFeature(Feature::FlexibleTextureViews);
+ EnableFeature(Feature::TextureFormatsTier1);
// The function subgroupBroadcast(f16) fails for some edge cases on intel gen-9 devices.
// See crbug.com/391680973
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index efd1809..78dab01 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -684,6 +684,7 @@
"end2end/Texture3DTests.cpp",
"end2end/TextureCorruptionTests.cpp",
"end2end/TextureFormatTests.cpp",
+ "end2end/TextureFormatsTier1Tests.cpp",
"end2end/TextureShaderBuiltinTests.cpp",
"end2end/TextureSubresourceTests.cpp",
"end2end/TextureViewTests.cpp",
diff --git a/src/dawn/tests/DawnTest.cpp b/src/dawn/tests/DawnTest.cpp
index e12fe30..be1452b 100644
--- a/src/dawn/tests/DawnTest.cpp
+++ b/src/dawn/tests/DawnTest.cpp
@@ -2125,5 +2125,26 @@
}
template class ExpectBetweenColors<utils::RGBA8>;
+
+template <typename T>
+testing::AssertionResult ExpectBetweenSnormTextureBounds<T>::Check(const void* data, size_t size) {
+ DAWN_ASSERT(size == sizeof(T) * expectedLower.size());
+ DAWN_ASSERT(expectedLower.size() == expectedUpper.size());
+
+ const T* actual = static_cast<const T*>(data);
+
+ for (size_t i = 0; i < expectedLower.size(); ++i) {
+ if (!(actual[i] >= expectedLower[i] && actual[i] <= expectedUpper[i])) {
+ return testing::AssertionFailure()
+ << absl::StrFormat("Expected data[%d] to be between %d and %d, actual %d\n", i,
+ expectedLower[i], expectedUpper[i], actual[i]);
+ }
+ }
+
+ return testing::AssertionSuccess();
+}
+
+template class ExpectBetweenSnormTextureBounds<int8_t>;
+
} // namespace detail
} // namespace dawn
diff --git a/src/dawn/tests/DawnTest.h b/src/dawn/tests/DawnTest.h
index c6d171d..87a8d8e 100644
--- a/src/dawn/tests/DawnTest.h
+++ b/src/dawn/tests/DawnTest.h
@@ -125,6 +125,9 @@
#define EXPECT_TEXTURE_FLOAT16_EQ(...) \
AddTextureExpectation<float, uint16_t>(__FILE__, __LINE__, __VA_ARGS__)
+#define EXPECT_TEXTURE_SNORM_BETWEEN(...) \
+ AddSnormTextureBoundsExpectation<int8_t>(__FILE__, __LINE__, __VA_ARGS__)
+
// Matcher for C++ types to verify that their internal C-handles are identical.
MATCHER_P(CHandleIs, cType, "") {
return arg.Get() == cType;
@@ -170,6 +173,8 @@
class ExpectEq;
template <typename T>
class ExpectBetweenColors;
+template <typename T>
+class ExpectBetweenSnormTextureBounds;
} // namespace detail
namespace wire {
@@ -548,6 +553,46 @@
texture, {x, y}, {1, 1}, level, aspect, sizeof(T), bytesPerRow);
}
+ template <typename T>
+ std::ostringstream& AddSnormTextureBoundsExpectation(
+ const char* file,
+ int line,
+ const std::vector<T>& expectedL,
+ const std::vector<T>& expectedU,
+ const wgpu::Texture& texture,
+ wgpu::Origin3D origin,
+ wgpu::Extent3D extent,
+ wgpu::TextureFormat format,
+ uint32_t level = 0,
+ wgpu::TextureAspect aspect = wgpu::TextureAspect::All,
+ uint32_t bytesPerRow = 0) {
+ // No device passed explicitly. Default it, and forward the rest of the args.
+ return AddSnormTextureBoundsExpectation(file, line, this->device, expectedL, expectedU,
+ texture, origin, extent, format, level, aspect,
+ bytesPerRow);
+ }
+
+ template <typename T>
+ std::ostringstream& AddSnormTextureBoundsExpectation(
+ const char* file,
+ int line,
+ const wgpu::Device& targetDevice,
+ const std::vector<T>& expectedL,
+ const std::vector<T>& expectedU,
+ const wgpu::Texture& texture,
+ wgpu::Origin3D origin,
+ wgpu::Extent3D extent,
+ wgpu::TextureFormat format,
+ uint32_t level = 0,
+ wgpu::TextureAspect aspect = wgpu::TextureAspect::All,
+ uint32_t bytesPerRow = 0) {
+ uint32_t texelBlockSize = utils::GetTexelBlockSizeInBytes(format);
+ return AddTextureExpectationImpl(
+ file, line, std::move(targetDevice),
+ new detail::ExpectBetweenSnormTextureBounds<T>(expectedL, expectedU), texture, origin,
+ extent, level, aspect, texelBlockSize, bytesPerRow);
+ }
+
std::ostringstream& ExpectSampledFloatData(wgpu::Texture texture,
uint32_t width,
uint32_t height,
@@ -910,6 +955,21 @@
// lerp(color0, color1, t) where t is [0,1]. But I don't want to be too strict here.
extern template class ExpectBetweenColors<utils::RGBA8>;
+template <typename T>
+class ExpectBetweenSnormTextureBounds : public Expectation {
+ public:
+ // Inclusive for now
+ ExpectBetweenSnormTextureBounds(const std::vector<T>& expectedL,
+ const std::vector<T>& expectedU)
+ : expectedLower(expectedL), expectedUpper(expectedU) {}
+ testing::AssertionResult Check(const void* data, size_t size) override;
+
+ private:
+ std::vector<T> expectedLower;
+ std::vector<T> expectedUpper;
+};
+extern template class ExpectBetweenSnormTextureBounds<int8_t>;
+
class CustomTextureExpectation : public Expectation {
public:
~CustomTextureExpectation() override = default;
diff --git a/src/dawn/tests/end2end/TextureFormatsTier1Tests.cpp b/src/dawn/tests/end2end/TextureFormatsTier1Tests.cpp
new file mode 100644
index 0000000..acbb9eb
--- /dev/null
+++ b/src/dawn/tests/end2end/TextureFormatsTier1Tests.cpp
@@ -0,0 +1,179 @@
+// Copyright 2025 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 <cmath>
+#include <vector>
+
+#include "dawn/tests/DawnTest.h"
+#include "dawn/utils/ComboRenderPipelineDescriptor.h"
+#include "dawn/utils/WGPUHelpers.h"
+
+namespace dawn {
+namespace {
+
+class RenderAttachmentSnormFormatsTest : public DawnTest {
+ protected:
+ std::vector<wgpu::FeatureName> GetRequiredFeatures() override {
+ std::vector<wgpu::FeatureName> requiredFeatures = {};
+ if (SupportsFeatures({wgpu::FeatureName::TextureFormatsTier1})) {
+ requiredFeatures.push_back(wgpu::FeatureName::TextureFormatsTier1);
+ }
+ return requiredFeatures;
+ }
+
+ const char* GetSinglePointVS() {
+ return R"(
+ @vertex
+ fn main() -> @builtin(position) vec4<f32> {
+ return vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ }
+ )";
+ }
+
+ int8_t ConvertFloatToSnorm8(float value) {
+ float roundedValue = (value >= 0) ? (value + 0.5f) : (value - 0.5f);
+ float clampedValue = std::clamp(roundedValue, -128.0f, 127.0f);
+ return static_cast<int8_t>(clampedValue);
+ }
+
+ void RunSingleFormatTest(wgpu::TextureFormat format,
+ const char* fsCode,
+ const std::vector<float>& originData) {
+ wgpu::Extent3D textureSize = {1, 1, 1};
+ wgpu::TextureDescriptor textureDesc;
+ textureDesc.usage = wgpu::TextureUsage::RenderAttachment | wgpu::TextureUsage::CopySrc;
+ textureDesc.dimension = wgpu::TextureDimension::e2D;
+ textureDesc.size = textureSize;
+ textureDesc.format = format;
+
+ wgpu::Texture texture = device.CreateTexture(&textureDesc);
+
+ wgpu::ShaderModule vsModule = utils::CreateShaderModule(device, GetSinglePointVS());
+ wgpu::ShaderModule fsModule = utils::CreateShaderModule(device, fsCode);
+
+ utils::ComboRenderPipelineDescriptor pipelineDesc;
+ pipelineDesc.vertex.module = vsModule;
+ pipelineDesc.cFragment.module = fsModule;
+ pipelineDesc.primitive.topology = wgpu::PrimitiveTopology::PointList;
+ pipelineDesc.cFragment.targetCount = 1;
+ pipelineDesc.cTargets[0].format = format;
+ wgpu::RenderPipeline pipeline = device.CreateRenderPipeline(&pipelineDesc);
+
+ wgpu::TextureView textureView = texture.CreateView();
+
+ utils::ComboRenderPassDescriptor renderPass;
+ renderPass.cColorAttachments[0].view = textureView;
+ renderPass.cColorAttachments[0].loadOp = wgpu::LoadOp::Clear;
+ renderPass.cColorAttachments[0].storeOp = wgpu::StoreOp::Store;
+ renderPass.cColorAttachments[0].clearValue = {0.0f, 0.0f, 0.0f, 0.0f};
+ renderPass.colorAttachmentCount = 1;
+
+ wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
+ wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderPass);
+
+ pass.SetPipeline(pipeline);
+ pass.Draw(1);
+ pass.End();
+
+ wgpu::CommandBuffer commands = encoder.Finish();
+ queue.Submit(1, &commands);
+
+ std::vector<int8_t> expectedLowerBounds;
+ std::vector<int8_t> expectedUpperBounds;
+
+ for (uint32_t i = 0; i < originData.size(); ++i) {
+ float floatComponent = originData[i];
+ float scaledComponent = floatComponent * 127.f;
+
+ int8_t lowerSnormExpectation = ConvertFloatToSnorm8(scaledComponent - 0.6f);
+ int8_t upperSnormExpectation = ConvertFloatToSnorm8(scaledComponent + 0.6f);
+
+ expectedLowerBounds.push_back(lowerSnormExpectation);
+ expectedUpperBounds.push_back(upperSnormExpectation);
+ }
+
+ EXPECT_TEXTURE_SNORM_BETWEEN(expectedLowerBounds, expectedUpperBounds, texture, {0, 0},
+ {1, 1}, format);
+ }
+};
+
+// Test that r8snorm format is valid as renderable texture if
+// 'texture-formats-tier1' is enabled.
+TEST_P(RenderAttachmentSnormFormatsTest, R8SnormRenderAttachment) {
+ DAWN_TEST_UNSUPPORTED_IF(!device.HasFeature(wgpu::FeatureName::TextureFormatsTier1));
+
+ const char* fs_r8snorm = R"(
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(-0.5, 0.0, 0.0, 1.0);
+ }
+ )";
+ std::vector<float> originData = {-0.5};
+
+ RunSingleFormatTest(wgpu::TextureFormat::R8Snorm, fs_r8snorm, originData);
+}
+
+// Test that rg8snorm format is valid as renderable texture if
+// 'texture-formats-tier1' is enabled.
+TEST_P(RenderAttachmentSnormFormatsTest, RG8SnormRenderAttachment) {
+ DAWN_TEST_UNSUPPORTED_IF(!device.HasFeature(wgpu::FeatureName::TextureFormatsTier1));
+
+ const char* fs_rg8snorm = R"(
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(-0.5, 0.25, 0.0, 1.0);
+ }
+ )";
+ std::vector<float> originData = {-0.5, 0.25};
+
+ RunSingleFormatTest(wgpu::TextureFormat::RG8Snorm, fs_rg8snorm, originData);
+}
+
+// Test that r8snorm format is valid as renderable texture if
+// 'texture-formats-tier1' is enabled.
+TEST_P(RenderAttachmentSnormFormatsTest, RGBA8SnormRenderAttachment) {
+ DAWN_TEST_UNSUPPORTED_IF(!device.HasFeature(wgpu::FeatureName::TextureFormatsTier1));
+
+ const char* fs_rgba8snorm = R"(
+ @fragment
+ fn main() -> @location(0) vec4<f32> {
+ return vec4<f32>(-0.5, 0.25, -1.0, 1.0);
+ }
+ )";
+ std::vector<float> originData = {-0.5, 0.25, -1.0, 1.0};
+ RunSingleFormatTest(wgpu::TextureFormat::RGBA8Snorm, fs_rgba8snorm, originData);
+}
+
+DAWN_INSTANTIATE_TEST(RenderAttachmentSnormFormatsTest,
+ D3D11Backend(),
+ D3D12Backend(),
+ MetalBackend(),
+ VulkanBackend(),
+ OpenGLBackend());
+
+} // anonymous namespace
+} // namespace dawn