MetalBlitter: add support for blitting integer formats (#9045)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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); \
|
||||
|
||||
196
filament/backend/test/test_MetalBlitter.mm
Normal file
196
filament/backend/test/test_MetalBlitter.mm
Normal 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
|
||||
Reference in New Issue
Block a user