diff --git a/filament/backend/CMakeLists.txt b/filament/backend/CMakeLists.txt index 0ba1c7bdac..a6d941d140 100644 --- a/filament/backend/CMakeLists.txt +++ b/filament/backend/CMakeLists.txt @@ -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 diff --git a/filament/backend/src/metal/MetalBlitter.h b/filament/backend/src/metal/MetalBlitter.h index 3882c18df3..7634c737ce 100644 --- a/filament/backend/src/metal/MetalBlitter.h +++ b/filament/backend/src/metal/MetalBlitter.h @@ -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; } }; diff --git a/filament/backend/src/metal/MetalBlitter.mm b/filament/backend/src/metal/MetalBlitter.mm index 8aee28b62f..05d3259a50 100644 --- a/filament/backend/src/metal/MetalBlitter.mm +++ b/filament/backend/src/metal/MetalBlitter.mm @@ -17,6 +17,7 @@ #include "MetalBlitter.h" #include "MetalContext.h" +#include "MetalEnums.h" #include "MetalUtils.h" #include @@ -28,6 +29,9 @@ static const char* functionLibrary = R"( #include #include +#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 sourceColor [[texture(0)]], #else - texture2d sourceColor [[texture(0)]], + texture2d sourceColor [[texture(0)]], #endif // MSAA_COLOR_SOURCE constant FragmentArgs* args [[buffer(0)]]) @@ -183,9 +187,20 @@ void MetalBlitter::blitDepthPlane(id 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 const fragmentFunction = getBlitFragmentFunction(key); MetalPipelineState const pipelineState { @@ -311,6 +326,18 @@ id 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 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 const library = [mContext.device newLibraryWithSource:objcSource - options:nil + options:options error:&error]; id const function = [library newFunctionWithName:@"blitterVertex"]; diff --git a/filament/backend/src/metal/MetalDriver.h b/filament/backend/src/metal/MetalDriver.h index 7bea1119bf..5e5d75823e 100644 --- a/filament/backend/src/metal/MetalDriver.h +++ b/filament/backend/src/metal/MetalDriver.h @@ -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; diff --git a/filament/backend/src/metal/MetalDriver.mm b/filament/backend/src/metal/MetalDriver.mm index 117e7e8a46..3edb2bdf8a 100644 --- a/filament/backend/src/metal/MetalDriver.mm +++ b/filament/backend/src/metal/MetalDriver.mm @@ -1508,14 +1508,6 @@ void MetalDriver::readPixels(Handle 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 diff --git a/filament/backend/src/metal/MetalEnums.h b/filament/backend/src/metal/MetalEnums.h index 46e0dac622..77a24525a4 100644 --- a/filament/backend/src/metal/MetalEnums.h +++ b/filament/backend/src/metal/MetalEnums.h @@ -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) { diff --git a/filament/backend/test/Skip.h b/filament/backend/test/Skip.h index 580b441c13..381e15de17 100644 --- a/filament/backend/test/Skip.h +++ b/filament/backend/test/Skip.h @@ -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); \ diff --git a/filament/backend/test/test_MetalBlitter.mm b/filament/backend/test/test_MetalBlitter.mm new file mode 100644 index 0000000000..056ba66484 --- /dev/null +++ b/filament/backend/test/test_MetalBlitter.mm @@ -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> { + MetalBlitterTest() {} +}; + +constexpr int kTextureSize = 32; + +template +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 +void fillTextureWithRed(id texture) { + std::vector textureData(kTextureSize * kTextureSize, redValue()); + [texture replaceRegion:MTLRegionMake2D(0, 0, kTextureSize, kTextureSize) + mipmapLevel:0 + withBytes:textureData.data() + bytesPerRow:kTextureSize * sizeof(T)]; +} + +template +void verifyTextureIsRed(id texture) { + std::vector resultData(kTextureSize * kTextureSize); + [texture getBytes:resultData.data() + bytesPerRow:kTextureSize * sizeof(T) + fromRegion:MTLRegionMake2D(0, 0, kTextureSize, kTextureSize) + mipmapLevel:0]; + + std::vector expectedData(kTextureSize * kTextureSize, redValue()); + 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(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 srcTexture = [metalContext.device newTextureWithDescriptor:desc]; + + // Create a destination texture with the parameterized format. + desc.pixelFormat = dstFormat; + id dstTexture = [metalContext.device newTextureWithDescriptor:desc]; + + // Fill the source texture with red. + switch (srcFormat) { + case MTLPixelFormatRGBA32Float: + fillTextureWithRed(srcTexture); + break; + case MTLPixelFormatRG32Float: + fillTextureWithRed(srcTexture); + break; + case MTLPixelFormatRGBA32Uint: + fillTextureWithRed(srcTexture); + break; + case MTLPixelFormatRG32Uint: + fillTextureWithRed(srcTexture); + break; + case MTLPixelFormatRGBA32Sint: + fillTextureWithRed(srcTexture); + break; + case MTLPixelFormatRG32Sint: + fillTextureWithRed(srcTexture); + break; + default: + FAIL() << "Source format not implemented in test"; + } + + id 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(dstTexture); + break; + case MTLPixelFormatRG32Float: + verifyTextureIsRed(dstTexture); + break; + case MTLPixelFormatRGBA32Uint: + verifyTextureIsRed(dstTexture); + break; + case MTLPixelFormatRG32Uint: + verifyTextureIsRed(dstTexture); + break; + case MTLPixelFormatRGBA32Sint: + verifyTextureIsRed(dstTexture); + break; + case MTLPixelFormatRG32Sint: + verifyTextureIsRed(dstTexture); + break; + default: + FAIL() << "Destination format not implemented in test"; + } +} + +// Helper to give names to the tests. +static std::string testNameGenerator( + const testing::TestParamInfo& 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