MetalBlitter: add support for blitting integer formats (#9045)

This commit is contained in:
Ben Doherty
2025-08-12 16:16:47 -04:00
committed by GitHub
parent f5c0fabb10
commit c60860368d
8 changed files with 284 additions and 24 deletions

View File

@@ -576,6 +576,12 @@ if (APPLE OR LINUX)
test/test_CommandBufferQueue.cpp
test/test_Template.cpp
)
if (APPLE)
# Metal-specific tests
list(APPEND BACKEND_TEST_SRC
test/test_MetalBlitter.mm
)
endif()
set(BACKEND_TEST_LIBS
absl::str_format
backend
@@ -597,6 +603,7 @@ if (APPLE AND NOT IOS)
test/test_RenderExternalImage.cpp)
add_library(backend_test STATIC ${BACKEND_TEST_SRC})
target_link_libraries(backend_test PUBLIC ${BACKEND_TEST_LIBS})
target_compile_options(backend_test PRIVATE "-fobjc-arc")
set(BACKEND_TEST_DEPS
OSDependent

View File

@@ -68,9 +68,15 @@ private:
const BlitArgs& args, uint32_t depthPlane);
struct BlitFunctionKey {
enum DataFormat : uint8_t {
FLOAT,
UINT,
INT
};
bool msaaColorSource{};
bool sources3D{};
char padding[2]{};
DataFormat inputFormat{};
DataFormat outputFormat{};
bool isValid() const noexcept {
// MSAA 3D textures do not exist.
@@ -79,8 +85,8 @@ private:
}
bool operator==(const BlitFunctionKey& rhs) const noexcept {
return msaaColorSource == rhs.msaaColorSource &&
sources3D == rhs.sources3D;
return msaaColorSource == rhs.msaaColorSource && sources3D == rhs.sources3D &&
inputFormat == rhs.inputFormat && outputFormat == rhs.outputFormat;
}
};

View File

@@ -17,6 +17,7 @@
#include "MetalBlitter.h"
#include "MetalContext.h"
#include "MetalEnums.h"
#include "MetalUtils.h"
#include <utils/Logger.h>
@@ -28,6 +29,9 @@ static const char* functionLibrary = R"(
#include <metal_stdlib>
#include <simd/simd.h>
#define CAT(a,b) a##b
#define FORMAT_4(x) CAT(x, 4)
using namespace metal;
struct VertexOut
@@ -37,7 +41,7 @@ struct VertexOut
struct FragmentOut
{
float4 color [[color(0)]];
FORMAT_4(OUTPUT_FORMAT) color [[color(0)]];
};
vertex VertexOut
@@ -66,7 +70,7 @@ blitterFrag(VertexOut in [[stage_in]],
#elif SOURCES_3D
texture3d<float, access::sample> sourceColor [[texture(0)]],
#else
texture2d<float, access::sample> sourceColor [[texture(0)]],
texture2d<INPUT_FORMAT, access::sample> sourceColor [[texture(0)]],
#endif // MSAA_COLOR_SOURCE
constant FragmentArgs* args [[buffer(0)]])
@@ -183,9 +187,20 @@ void MetalBlitter::blitDepthPlane(id<MTLCommandBuffer> cmdBuffer, const BlitArgs
[cmdBuffer renderCommandEncoderWithDescriptor:descriptor];
encoder.label = @(label);
BlitFunctionKey key;
auto getDataFormat = [](MTLPixelFormat format) {
if (isMetalFormatUnsignedInteger(format)) {
return BlitFunctionKey::DataFormat::UINT;
} else if (isMetalFormatSignedInteger(format)) {
return BlitFunctionKey::DataFormat::INT;
}
return BlitFunctionKey::DataFormat::FLOAT;
};
BlitFunctionKey key{};
key.msaaColorSource = args.source.texture.textureType == MTLTextureType2DMultisample;
key.sources3D = args.source.texture.textureType == MTLTextureType3D;
key.inputFormat = getDataFormat(args.source.texture.pixelFormat);
key.outputFormat = getDataFormat(args.destination.texture.pixelFormat);
id<MTLFunction> const fragmentFunction = getBlitFragmentFunction(key);
MetalPipelineState const pipelineState {
@@ -311,6 +326,18 @@ id<MTLFunction> MetalBlitter::compileFragmentFunction(BlitFunctionKey key) const
if (key.sources3D) {
macros[@"SOURCES_3D"] = @"1";
}
auto getDataFormatString = [](BlitFunctionKey::DataFormat f) {
switch (f) {
case BlitFunctionKey::DataFormat::FLOAT:
return @"float";
case BlitFunctionKey::DataFormat::UINT:
return @"uint";
case BlitFunctionKey::DataFormat::INT:
return @"int";
}
};
macros[@"INPUT_FORMAT"] = getDataFormatString(key.inputFormat);
macros[@"OUTPUT_FORMAT"] = getDataFormatString(key.outputFormat);
options.preprocessorMacros = macros;
NSString* const objcSource = [NSString stringWithCString:functionLibrary
encoding:NSUTF8StringEncoding];
@@ -337,11 +364,17 @@ id<MTLFunction> MetalBlitter::getBlitVertexFunction() {
return mVertexFunction;
}
MTLCompileOptions* const options = [MTLCompileOptions new];
NSMutableDictionary* const macros = [NSMutableDictionary dictionary];
// these can be anything for the vertex shader
macros[@"INPUT_FORMAT"] = @"float";
macros[@"OUTPUT_FORMAT"] = @"float";
options.preprocessorMacros = macros;
NSString* const objcSource = [NSString stringWithCString:functionLibrary
encoding:NSUTF8StringEncoding];
NSError* error = nil;
id <MTLLibrary> const library = [mContext.device newLibraryWithSource:objcSource
options:nil
options:options
error:&error];
id<MTLFunction> const function = [library newFunctionWithName:@"blitterVertex"];

View File

@@ -58,6 +58,8 @@ class MetalDriver final : public DriverBase {
public:
static Driver* create(PlatformMetal* platform, const Platform::DriverConfig& driverConfig);
MetalContext* getContext() { return mContext; }
private:
friend class MetalSwapChain;

View File

@@ -1508,14 +1508,6 @@ void MetalDriver::readPixels(Handle<HwRenderTarget> src, uint32_t x, uint32_t y,
<< ") is not supported for "
"readPixels.";
const bool formatConversionNecessary = srcTexture.pixelFormat != format;
// TODO: MetalBlitter does not currently support format conversions to integer types.
// The format and type must match the source pixel format exactly.
FILAMENT_CHECK_PRECONDITION(!formatConversionNecessary || !isMetalFormatInteger(format))
<< "readPixels does not support integer format conversions from MTLPixelFormat ("
<< (int)srcTexture.pixelFormat << ") to (" << (int)format << ").";
MTLTextureDescriptor* textureDescriptor =
[MTLTextureDescriptor texture2DDescriptorWithPixelFormat:format
width:srcTextureSize.width

View File

@@ -236,32 +236,47 @@ inline MTLPixelFormat getMetalFormatLinear(MTLPixelFormat format) {
return format;
}
constexpr inline bool isMetalFormatInteger(MTLPixelFormat format) {
constexpr inline bool isMetalFormatUnsignedInteger(MTLPixelFormat format) {
switch (format) {
case MTLPixelFormatR8Uint:
case MTLPixelFormatR8Sint:
case MTLPixelFormatR16Uint:
case MTLPixelFormatR16Sint:
case MTLPixelFormatRG8Uint:
case MTLPixelFormatRG8Sint:
case MTLPixelFormatR32Uint:
case MTLPixelFormatR32Sint:
case MTLPixelFormatRG16Uint:
case MTLPixelFormatRG16Sint:
case MTLPixelFormatRGBA8Uint:
case MTLPixelFormatRGBA8Sint:
case MTLPixelFormatRGB10A2Uint:
case MTLPixelFormatRG32Uint:
case MTLPixelFormatRG32Sint:
case MTLPixelFormatRGBA16Uint:
case MTLPixelFormatRGBA16Sint:
case MTLPixelFormatRGBA32Uint:
return true;
default:
return false;
}
return false;
}
constexpr inline bool isMetalFormatSignedInteger(MTLPixelFormat format) {
switch (format) {
case MTLPixelFormatR8Sint:
case MTLPixelFormatR16Sint:
case MTLPixelFormatRG8Sint:
case MTLPixelFormatR32Sint:
case MTLPixelFormatRG16Sint:
case MTLPixelFormatRGBA8Sint:
case MTLPixelFormatRG32Sint:
case MTLPixelFormatRGBA16Sint:
case MTLPixelFormatRGBA32Sint:
return true;
default:
return false;
}
return false;
}
constexpr inline bool isMetalFormatInteger(MTLPixelFormat format) {
return isMetalFormatUnsignedInteger(format) || isMetalFormatSignedInteger(format);
}
constexpr inline bool isMetalFormatStencil(MTLPixelFormat format) {

View File

@@ -32,6 +32,15 @@ do {
} \
} while (false)
#define SKIP_IF_NOT(skipEnvironment, rationale) \
do { \
SkipEnvironment skip(skipEnvironment); \
if (!skip.matches()) { \
GTEST_SKIP() << "Skipping test as the " << skip.describe() << "\n" \
<< " This test can't run there because " << rationale; \
} \
} while (false)
#define NONFATAL_FAIL_IF(skipEnvironment, rationale) \
do { \
SkipEnvironment skip(skipEnvironment); \

View File

@@ -0,0 +1,196 @@
/*
* Copyright (C) 2025 The Android Open Source Project
*
* 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 "BackendTest.h"
#include "metal/MetalBlitter.h"
#include "metal/MetalDriver.h"
#include "metal/MetalContext.h"
#include "Lifetimes.h"
#include "Skip.h"
namespace test {
using namespace filament;
using namespace filament::backend;
using namespace filament::math;
using namespace utils;
struct MetalBlitterTest
: public BackendTest,
public testing::WithParamInterface<std::tuple<MTLPixelFormat, MTLPixelFormat>> {
MetalBlitterTest() {}
};
constexpr int kTextureSize = 32;
template<typename T>
constexpr T redValue() {
if constexpr (T::SIZE == 1) return T{ 1 };
if constexpr (T::SIZE == 2) return T{ 1, 0 };
if constexpr (T::SIZE == 3) return T{ 1, 0, 0 };
if constexpr (T::SIZE == 4) return T{ 1, 0, 0, 1 };
}
template<typename T>
void fillTextureWithRed(id<MTLTexture> texture) {
std::vector<T> textureData(kTextureSize * kTextureSize, redValue<T>());
[texture replaceRegion:MTLRegionMake2D(0, 0, kTextureSize, kTextureSize)
mipmapLevel:0
withBytes:textureData.data()
bytesPerRow:kTextureSize * sizeof(T)];
}
template<typename T>
void verifyTextureIsRed(id<MTLTexture> texture) {
std::vector<T> resultData(kTextureSize * kTextureSize);
[texture getBytes:resultData.data()
bytesPerRow:kTextureSize * sizeof(T)
fromRegion:MTLRegionMake2D(0, 0, kTextureSize, kTextureSize)
mipmapLevel:0];
std::vector<T> expectedData(kTextureSize * kTextureSize, redValue<T>());
EXPECT_EQ(resultData, expectedData);
}
TEST_P(MetalBlitterTest, Blit) {
SKIP_IF_NOT(Backend::METAL, "MetalBlitter only works with the Metal backend");
const auto& [srcFormat, dstFormat] = GetParam();
MetalDriver& metalDriver = static_cast<MetalDriver&>(getDriver());
MetalContext& metalContext = *metalDriver.getContext();
MetalBlitter blitter(metalContext);
// Create a source texture
MTLTextureDescriptor* desc = [MTLTextureDescriptor new];
desc.width = kTextureSize;
desc.height = kTextureSize;
desc.pixelFormat = srcFormat;
desc.usage = MTLTextureUsageRenderTarget;
id<MTLTexture> srcTexture = [metalContext.device newTextureWithDescriptor:desc];
// Create a destination texture with the parameterized format.
desc.pixelFormat = dstFormat;
id<MTLTexture> dstTexture = [metalContext.device newTextureWithDescriptor:desc];
// Fill the source texture with red.
switch (srcFormat) {
case MTLPixelFormatRGBA32Float:
fillTextureWithRed<float4>(srcTexture);
break;
case MTLPixelFormatRG32Float:
fillTextureWithRed<float2>(srcTexture);
break;
case MTLPixelFormatRGBA32Uint:
fillTextureWithRed<uint4>(srcTexture);
break;
case MTLPixelFormatRG32Uint:
fillTextureWithRed<uint2>(srcTexture);
break;
case MTLPixelFormatRGBA32Sint:
fillTextureWithRed<int4>(srcTexture);
break;
case MTLPixelFormatRG32Sint:
fillTextureWithRed<int2>(srcTexture);
break;
default:
FAIL() << "Source format not implemented in test";
}
id<MTLCommandBuffer> cmdBuffer = [metalContext.commandQueue commandBuffer];
MetalBlitter::BlitArgs args{};
args.source.texture = srcTexture;
args.source.region = MTLRegionMake2D(0, 0, kTextureSize, kTextureSize);
args.destination.texture = dstTexture;
args.destination.region = MTLRegionMake2D(0, 0, kTextureSize, kTextureSize);
args.filter = SamplerMagFilter::NEAREST;
blitter.blit(cmdBuffer, args, "MetalBlitterTest");
[cmdBuffer commit];
[cmdBuffer waitUntilCompleted];
// Verify the destination texture is red.
switch (dstFormat) {
case MTLPixelFormatRGBA32Float:
verifyTextureIsRed<float4>(dstTexture);
break;
case MTLPixelFormatRG32Float:
verifyTextureIsRed<float2>(dstTexture);
break;
case MTLPixelFormatRGBA32Uint:
verifyTextureIsRed<uint4>(dstTexture);
break;
case MTLPixelFormatRG32Uint:
verifyTextureIsRed<uint2>(dstTexture);
break;
case MTLPixelFormatRGBA32Sint:
verifyTextureIsRed<int4>(dstTexture);
break;
case MTLPixelFormatRG32Sint:
verifyTextureIsRed<int2>(dstTexture);
break;
default:
FAIL() << "Destination format not implemented in test";
}
}
// Helper to give names to the tests.
static std::string testNameGenerator(
const testing::TestParamInfo<MetalBlitterTest::ParamType>& info) {
auto const& [srcFormat, dstFormat] = info.param;
auto formatToString = [](MTLPixelFormat format) {
switch (format) {
case MTLPixelFormatRGBA32Float: return "RGBA32Float";
case MTLPixelFormatRG32Float: return "RG32Float";
case MTLPixelFormatRGBA32Uint: return "RGBA32Uint";
case MTLPixelFormatRG32Uint: return "RG32Uint";
case MTLPixelFormatRGBA32Sint: return "RGBA32Sint";
case MTLPixelFormatRG32Sint: return "RG32Sint";
default: return "Unknown";
}
};
return std::string(formatToString(srcFormat)) + "_to_" + std::string(formatToString(dstFormat));
}
// This instantiates all the test cases. Each call to std::make_tuple defines a single test case
// that uses MetalBlitter to blit from a source texture to a destination texture.
// The first argument is the source texture format.
// The second argument is the destination texture format.
INSTANTIATE_TEST_SUITE_P(MetalBlitterTests, MetalBlitterTest,
testing::Values(
// equal formats, fast path
std::make_tuple(MTLPixelFormatRGBA32Float, MTLPixelFormatRGBA32Float),
// the rest of the test cases take the slow path
std::make_tuple(MTLPixelFormatRGBA32Float, MTLPixelFormatRG32Float),
std::make_tuple(MTLPixelFormatRG32Float, MTLPixelFormatRGBA32Float),
std::make_tuple(MTLPixelFormatRGBA32Uint, MTLPixelFormatRG32Uint),
std::make_tuple(MTLPixelFormatRG32Uint, MTLPixelFormatRGBA32Uint),
std::make_tuple(MTLPixelFormatRG32Sint, MTLPixelFormatRGBA32Sint),
std::make_tuple(MTLPixelFormatRGBA32Sint, MTLPixelFormatRG32Sint)
),
testNameGenerator
);
} // namespace test