diff --git a/filament/backend/CMakeLists.txt b/filament/backend/CMakeLists.txt index dd68c4cf49..cf2aa5030c 100644 --- a/filament/backend/CMakeLists.txt +++ b/filament/backend/CMakeLists.txt @@ -291,7 +291,8 @@ if (APPLE) test/ShaderGenerator.cpp test/TrianglePrimitive.cpp test/Arguments.cpp - test/test_MissingRequiredAttributes.cpp) + test/test_MissingRequiredAttributes.cpp + test/test_ReadPixels.cpp) target_link_libraries(backend_test PRIVATE backend diff --git a/filament/backend/src/metal/MetalContext.h b/filament/backend/src/metal/MetalContext.h index ba8fa48de6..63564beed2 100644 --- a/filament/backend/src/metal/MetalContext.h +++ b/filament/backend/src/metal/MetalContext.h @@ -79,6 +79,7 @@ struct MetalContext { // Surface-related properties. MetalSwapChain* currentSurface = nullptr; id currentDrawable = nullptr; + id headlessDrawable = nil; MTLPixelFormat currentSurfacePixelFormat = MTLPixelFormatInvalid; MTLPixelFormat currentDepthPixelFormat = MTLPixelFormatInvalid; @@ -98,9 +99,10 @@ struct MetalContext { }; // Acquire the current surface's CAMetalDrawable for the current frame if it has not already been -// acquired. -// This method returns the drawable and stores it in the context's currentDrawable field. -id acquireDrawable(MetalContext* context); +// acquired. This method stores it in the context's currentDrawable field and returns the +// drawable's texture. +// For headless swapchains a new texture is created. +id acquireDrawable(MetalContext* context); id acquireCommandBuffer(MetalContext* context); diff --git a/filament/backend/src/metal/MetalContext.mm b/filament/backend/src/metal/MetalContext.mm index 9faa50bb4c..7ba6c8f6e8 100644 --- a/filament/backend/src/metal/MetalContext.mm +++ b/filament/backend/src/metal/MetalContext.mm @@ -33,27 +33,48 @@ void presentDrawable(bool presentFrame, void* user) { // The drawable will be released here when the "drawable" variable goes out of scope. } -id acquireDrawable(MetalContext* context) { - if (!context->currentDrawable) { - context->currentDrawable = [context->currentSurface->layer nextDrawable]; - - if (context->frameFinishedCallback) { - id drawable = context->currentDrawable; - backend::FrameFinishedCallback callback = context->frameFinishedCallback; - void* userData = context->frameFinishedUserData; - // This block strongly captures drawable to keep it alive until the handler executes. - [context->currentCommandBuffer addScheduledHandler:^(id cb) { - // CFBridgingRetain is used here to give the drawable a +1 retain count before - // casting it to a void*. - PresentCallable callable(presentDrawable, (void*) CFBridgingRetain(drawable)); - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - callback(callable, userData); - }); - }]; +id acquireDrawable(MetalContext* context) { + if (context->currentDrawable) { + return context->currentDrawable.texture; + } + if (context->currentSurface->isHeadless()) { + if (context->headlessDrawable) { + return context->headlessDrawable; } + // For headless surfaces we construct a "fake" drawable, which is simply a renderable + // texture. + MTLTextureDescriptor* textureDescriptor = [MTLTextureDescriptor new]; + textureDescriptor.pixelFormat = MTLPixelFormatBGRA8Unorm; + textureDescriptor.width = context->currentSurface->surfaceWidth; + textureDescriptor.height = context->currentSurface->surfaceHeight; + textureDescriptor.usage = MTLTextureUsageRenderTarget; +#if defined(IOS) + textureDescriptor.storageMode = MTLStorageModeShared; +#else + textureDescriptor.storageMode = MTLStorageModeManaged; +#endif + context->headlessDrawable = [context->device newTextureWithDescriptor:textureDescriptor]; + return context->headlessDrawable; + } + + context->currentDrawable = [context->currentSurface->layer nextDrawable]; + + if (context->frameFinishedCallback) { + id drawable = context->currentDrawable; + backend::FrameFinishedCallback callback = context->frameFinishedCallback; + void* userData = context->frameFinishedUserData; + // This block strongly captures drawable to keep it alive until the handler executes. + [context->currentCommandBuffer addScheduledHandler:^(id cb) { + // CFBridgingRetain is used here to give the drawable a +1 retain count before + // casting it to a void*. + PresentCallable callable(presentDrawable, (void*) CFBridgingRetain(drawable)); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + callback(callable, userData); + }); + }]; } ASSERT_POSTCONDITION(context->currentDrawable != nil, "Could not obtain drawable."); - return context->currentDrawable; + return context->currentDrawable.texture; } id acquireCommandBuffer(MetalContext* context) { diff --git a/filament/backend/src/metal/MetalDriver.mm b/filament/backend/src/metal/MetalDriver.mm index c0b4fe384f..6404cc44ce 100644 --- a/filament/backend/src/metal/MetalDriver.mm +++ b/filament/backend/src/metal/MetalDriver.mm @@ -124,6 +124,7 @@ void MetalDriver::endFrame(uint32_t frameId) { // If we acquired a drawable for this frame, ensure that we release it here. mContext->currentDrawable = nil; + mContext->headlessDrawable = nil; CVMetalTextureCacheFlush(mContext->textureCache, 0); } @@ -133,7 +134,10 @@ void MetalDriver::flush(int) { } void MetalDriver::finish(int) { - // TODO: implement finish, equivalent of glFinish() (needed for unit tests) + // Wait for all frames to finish by submitting and waiting on a dummy command buffer. + id oneOffBuffer = [mContext->commandQueue commandBuffer]; + [oneOffBuffer commit]; + [oneOffBuffer waitUntilCompleted]; } void MetalDriver::createVertexBufferR(Handle vbh, uint8_t bufferCount, @@ -186,6 +190,8 @@ void MetalDriver::createRenderTargetR(Handle rth, auto getColorTexture = [&]() -> id { if (color.handle) { auto colorTexture = handle_cast(mHandleMap, color.handle); + ASSERT_PRECONDITION(colorTexture->texture, + "Color texture passed to render target has no texture allocation"); return colorTexture->texture; } else if (any(targetBufferFlags & TargetBufferFlags::COLOR)) { ASSERT_POSTCONDITION(false, "The COLOR flag was specified, but no color texture provided."); @@ -196,6 +202,8 @@ void MetalDriver::createRenderTargetR(Handle rth, auto getDepthTexture = [&]() -> id { if (depth.handle) { auto depthTexture = handle_cast(mHandleMap, depth.handle); + ASSERT_PRECONDITION(depthTexture->texture, + "Depth texture passed to render target has no texture allocation."); return depthTexture->texture; } else if (any(targetBufferFlags & TargetBufferFlags::DEPTH)) { ASSERT_POSTCONDITION(false, "The DEPTH flag was specified, but no depth texture provided."); @@ -224,7 +232,7 @@ void MetalDriver::createSwapChainR(Handle sch, void* nativeWindow, void MetalDriver::createSwapChainHeadlessR(Handle sch, uint32_t width, uint32_t height, uint64_t flags) { - // TODO: implement headless swapchain + construct_handle(mHandleMap, sch, width, height); } void MetalDriver::createStreamFromTextureIdR(Handle, intptr_t externalTextureId, @@ -697,6 +705,7 @@ void MetalDriver::commit(Handle sch) { [mContext->currentCommandBuffer commit]; mContext->currentCommandBuffer = nil; mContext->currentDrawable = nil; + mContext->headlessDrawable = nil; } void MetalDriver::bindUniformBuffer(size_t index, Handle ubh) { @@ -741,10 +750,84 @@ void MetalDriver::stopCapture(int) { [[MTLCaptureManager sharedCaptureManager] stopCapture]; } - void MetalDriver::readPixels(Handle src, uint32_t x, uint32_t y, uint32_t width, uint32_t height, PixelBufferDescriptor&& data) { + ASSERT_PRECONDITION(mContext->currentCommandBuffer != nil && + mContext->currentCommandEncoder == nil, + "readPixels must be called during a frame, but outside of a render pass."); + auto srcTarget = handle_cast(mHandleMap, src); + id srcTexture = srcTarget->getColor(); + size_t miplevel = srcTarget->getColorLevel(); + + auto chooseMetalPixelFormat = [] (PixelDataFormat format, PixelDataType type) { + // TODO: Add support for UINT and INT + if (format == PixelDataFormat::RGBA && type == PixelDataType::UBYTE) { + return MTLPixelFormatRGBA8Unorm; + } + + if (format == PixelDataFormat::RGBA && type == PixelDataType::FLOAT) { + return MTLPixelFormatRGBA32Float; + } + + return MTLPixelFormatInvalid; + }; + + const MTLPixelFormat format = chooseMetalPixelFormat(data.format, data.type); + ASSERT_PRECONDITION(format != MTLPixelFormatInvalid, + "The chosen combination of PixelDataFormat and PixelDataType is not supported for " + "readPixels."); + MTLTextureDescriptor* textureDescriptor = + [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:format + width:(srcTexture.width >> miplevel) + height:(srcTexture.height >> miplevel) + mipmapped:NO]; +#if defined(IOS) + textureDescriptor.storageMode = MTLStorageModeShared; +#else + textureDescriptor.storageMode = MTLStorageModeManaged; +#endif + textureDescriptor.usage = MTLTextureUsageShaderWrite | MTLTextureUsageRenderTarget; + id readPixelsTexture = [mContext->device newTextureWithDescriptor:textureDescriptor]; + + MetalBlitter::BlitArgs args; + args.filter = SamplerMagFilter::NEAREST; + args.source.level = miplevel; + args.source.region = MTLRegionMake2D(0, 0, srcTexture.width >> miplevel, srcTexture.height >> miplevel); + args.destination.level = 0; + args.destination.region = MTLRegionMake2D(0, 0, readPixelsTexture.width, readPixelsTexture.height); + args.source.color = srcTexture; + args.destination.color = readPixelsTexture; + + mContext->blitter->blit(args); + +#if !defined(IOS) + // Managed textures on macOS require explicit synchronization between GPU / CPU. + id blitEncoder = [mContext->currentCommandBuffer blitCommandEncoder]; + [blitEncoder synchronizeResource:readPixelsTexture]; + [blitEncoder endEncoding]; +#endif + + // TODO: right now, every Filament frame gets its own command buffer. We should adopt a more + // granular approach to command buffer allocation, as this command buffer won't start executing + // until the entire frame has been encoded. + + PixelBufferDescriptor* p = new PixelBufferDescriptor(std::move(data)); + [mContext->currentCommandBuffer addCompletedHandler:^(id cb) { + size_t stride = p->stride ? p->stride : width; + size_t bpp = PixelBufferDescriptor::computeDataSize(p->format, p->type, 1, 1, 1); + size_t bpr = PixelBufferDescriptor::computeDataSize(p->format, p->type, stride, 1, p->alignment); + // Metal's texture coordinates have (0, 0) at the top-left of the texture, but readPixels + // assumes (0, 0) at bottom-left. + MTLRegion srcRegion = MTLRegionMake2D(x, readPixelsTexture.height - y - height, width, height); + const uint8_t* bufferStart = (const uint8_t*) p->buffer + (p->left * bpp) + + (p->top * bpr); + [readPixelsTexture getBytes:(void*) bufferStart + bytesPerRow:bpr + fromRegion:srcRegion + mipmapLevel:0]; + scheduleDestroy(std::move(*p)); + }]; } void MetalDriver::readStreamPixels(Handle sh, uint32_t x, uint32_t y, uint32_t width, diff --git a/filament/backend/src/metal/MetalHandles.h b/filament/backend/src/metal/MetalHandles.h index 42536085e1..e1451c87b8 100644 --- a/filament/backend/src/metal/MetalHandles.h +++ b/filament/backend/src/metal/MetalHandles.h @@ -43,7 +43,13 @@ namespace metal { struct MetalSwapChain : public HwSwapChain { MetalSwapChain(id device, CAMetalLayer* nativeWindow); + // Instantiate a headless SwapChain. + MetalSwapChain(int32_t width, int32_t height); + + bool isHeadless() { return layer == nullptr; } + CAMetalLayer* layer = nullptr; + NSUInteger surfaceWidth = 0; NSUInteger surfaceHeight = 0; }; diff --git a/filament/backend/src/metal/MetalHandles.mm b/filament/backend/src/metal/MetalHandles.mm index 31bc77d51a..3c908dae89 100644 --- a/filament/backend/src/metal/MetalHandles.mm +++ b/filament/backend/src/metal/MetalHandles.mm @@ -62,9 +62,13 @@ MetalSwapChain::MetalSwapChain(id device, CAMetalLayer* nativeWindow) : layer(nativeWindow) { layer.device = device; CGSize size = layer.drawableSize; - surfaceHeight = (NSUInteger)(size.height); + surfaceHeight = (NSUInteger) (size.height); + surfaceWidth = (NSUInteger) (size.width); } +MetalSwapChain::MetalSwapChain(int32_t width, int32_t height) : surfaceWidth(width), + surfaceHeight(height) { } + MetalVertexBuffer::MetalVertexBuffer(id device, uint8_t bufferCount, uint8_t attributeCount, uint32_t vertexCount, AttributeArray const& attributes) : HwVertexBuffer(bufferCount, attributeCount, vertexCount, attributes) { @@ -439,7 +443,7 @@ MetalRenderTarget::MetalRenderTarget(MetalContext* context, uint32_t width, uint id MetalRenderTarget::getColor() { if (defaultRenderTarget) { - return acquireDrawable(context).texture; + return acquireDrawable(context); } if (multisampledColor) { return multisampledColor; diff --git a/filament/backend/test/BackendTest.cpp b/filament/backend/test/BackendTest.cpp index cdd883bcf2..b7c96b10b9 100644 --- a/filament/backend/test/BackendTest.cpp +++ b/filament/backend/test/BackendTest.cpp @@ -23,42 +23,6 @@ static constexpr size_t CONFIG_MIN_COMMAND_BUFFERS_SIZE = 1 * 1024 * 1024; static constexpr size_t CONFIG_COMMAND_BUFFERS_SIZE = 3 * CONFIG_MIN_COMMAND_BUFFERS_SIZE; -namespace { - -//////////////////////////////////////////////////////////////////////////////////////////////////// -// Shaders -//////////////////////////////////////////////////////////////////////////////////////////////////// - -std::string vertex (R"(#version 450 core - -layout(location = 0) in vec4 mesh_position; - -layout(set = 0, binding = 0, std140) uniform TestUniforms -{ - vec4 color; -} uniforms; - -void main() { - gl_Position = vec4(mesh_position.xy, uniforms.color.z, 1.0); -} -)"); - -std::string fragment (R"(#version 450 core - -layout(location = 0) out vec4 fragColor; - -layout(set = 0, binding = 0, std140) uniform TestUniforms -{ - vec4 color; -} uniforms; - -void main() { - fragColor = uniforms.color; -} -)"); - -} - namespace test { using namespace filament; diff --git a/filament/backend/test/BackendTest.h b/filament/backend/test/BackendTest.h index 7de7cfecd4..5188e06a81 100644 --- a/filament/backend/test/BackendTest.h +++ b/filament/backend/test/BackendTest.h @@ -59,6 +59,7 @@ protected: static void fullViewport(filament::backend::Viewport& viewport); filament::backend::DriverApi& getDriverApi() { return commandStream; } + filament::backend::Driver& getDriver() { return *driver; } private: diff --git a/filament/backend/test/mac_runner.mm b/filament/backend/test/mac_runner.mm index 5d3e55254e..aff756ee19 100644 --- a/filament/backend/test/mac_runner.mm +++ b/filament/backend/test/mac_runner.mm @@ -78,6 +78,9 @@ test::NativeView getNativeView() { CAMetalLayer* metalLayer = [CAMetalLayer layer]; metalLayer.bounds = view.bounds; + // This is important, as it allows us to read pixels from the default swap chain. + metalLayer.framebufferOnly = NO; + // It's important to set the drawableSize to the actual backing pixels. When rendering // full-screen, we can skip the macOS compositor if the size matches the display size. metalLayer.drawableSize = [view convertSizeToBacking:view.bounds.size]; diff --git a/filament/backend/test/test_MissingRequiredAttributes.cpp b/filament/backend/test/test_MissingRequiredAttributes.cpp index 81054664c1..850885ab0d 100644 --- a/filament/backend/test/test_MissingRequiredAttributes.cpp +++ b/filament/backend/test/test_MissingRequiredAttributes.cpp @@ -19,8 +19,6 @@ #include "ShaderGenerator.h" #include "TrianglePrimitive.h" -#include - namespace { //////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/filament/backend/test/test_ReadPixels.cpp b/filament/backend/test/test_ReadPixels.cpp new file mode 100644 index 0000000000..de8172c6f8 --- /dev/null +++ b/filament/backend/test/test_ReadPixels.cpp @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2019 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 "ShaderGenerator.h" +#include "TrianglePrimitive.h" + +#include + +#include + +namespace { + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// Shaders +//////////////////////////////////////////////////////////////////////////////////////////////////// + +std::string vertex (R"(#version 450 core + +layout(location = 0) in vec4 mesh_position; + +void main() { + gl_Position = vec4(mesh_position.xy, 0.0, 1.0); +} +)"); + +std::string fragment (R"(#version 450 core + +layout(location = 0) out vec4 fragColor; + +void main() { + fragColor = vec4(1.0); +} + +)"); + +} + +namespace test { + +using namespace filament; +using namespace filament::backend; + +TEST_F(BackendTest, ReadPixels) { + // These test scenarios use a known hash of the result pixel buffer to decide pass / fail, + // asserting an exact pixel-for-pixel match. So far, rendering on macOS and iPhone have had + // deterministic results. Take this test with a grain of salt, however, as other platform / GPU + // combinations may vary ever-so-slightly, which would cause this test to fail. + + const size_t renderTargetBaseSize = 512; + + struct TestCase { + const char* testName = "readPixels_normal"; + + // The murmur3 hash of the read pixel buffer result, used to determine success. + uint32_t hash = 0x899fb4a9; + + // The number of mip levels in the texture we're rendering into. + size_t mipLevels = 1; + + // The mip level to render into. Must be < mipLevels. + size_t mipLevel = 0; + + // The number of samples for MSAA rendering. + size_t samples = 1; + + // The size of the actual render target, taking mip level into account; + size_t getRenderTargetSize () const { + return renderTargetBaseSize >> mipLevel; + } + + // The rect that read pixels will read from. + struct Rect { + size_t x, y, width, height; + } readRect = { 0, 0, renderTargetBaseSize, renderTargetBaseSize }; + + // The size of the pixel buffer read pixels will write into. + size_t bufferDimension = getRenderTargetSize(); + + size_t getBufferSizeBytes() const { + auto getPixelSize = [] (PixelDataType type) { + switch(type) { + case PixelDataType::FLOAT: + return sizeof(float); + + case PixelDataType::UBYTE: + return sizeof(uint8_t); + + case PixelDataType::UINT: + return sizeof(uint32_t); + + default: + return 0ul; + } + }; + return bufferDimension * bufferDimension * 4 * getPixelSize(type); + } + + // The offset and stride set on the pixel buffer. + size_t left = 0, top = 0, alignment = 1; + + size_t getPixelBufferStride() const { + return bufferDimension; + } + + PixelDataFormat format = PixelDataFormat::RGBA; + PixelDataType type = PixelDataType::UBYTE; + }; + + // The normative read pixels test case. Render a white triangle over a blue background and read + // the full viewport into a pixel buffer. + TestCase t; + + // Check that a subregion of the render target can be read into a pixel buffer. + TestCase t2; + t2.testName = "readPixels_subregion"; + t2.readRect.x = 90; + t2.readRect.y = 403; + t2.readRect.width = 64; + t2.readRect.height = 64; + t2.bufferDimension = 64; + t2.hash = 0xcba7675a; + + // Check that readPixels works when rendering into and reading from a mip level. + TestCase t3; + t3.testName = "readPixels_mip"; + t3.mipLevels = 4; + t3.mipLevel = 2; + t3.bufferDimension = t3.getRenderTargetSize(); + t3.readRect.width = t3.getRenderTargetSize(); + t3.readRect.height = t3.getRenderTargetSize(); + t3.hash = 0xe6fa6c55; + + // Check that readPixels can return pixels in floating point RGBA format. + TestCase t4; + t4.testName = "readPixels_float"; + t4.format = PixelDataFormat::RGBA; + t4.type = PixelDataType::FLOAT; + t4.hash = 0xd8f5a7df; + + // Check that readPixels can read a region of the render target into a subregion of a large + // buffer. + TestCase t5; + t5.testName = "readPixels_subbuffer"; + t5.readRect.x = 90; + t5.readRect.y = 403; + t5.readRect.width = 64; + t5.readRect.height = 64; + t5.bufferDimension = 512; + t5.left = 64; + t5.top = 64; + t5.hash = 0xbaefdb54; + + TestCase testCases[] = { t, t2, t3, t4, t5 }; + + for (const auto& t : testCases) + { + // Create a platform-specific SwapChain and make it current. + auto swapChain = getDriverApi().createSwapChainHeadless(t.getRenderTargetSize(), + t.getRenderTargetSize(), 0); + getDriverApi().makeCurrent(swapChain, swapChain); + + // Create a program. + ShaderGenerator shaderGen(vertex, fragment, sBackend, sIsMobilePlatform); + Program p = shaderGen.getProgram(); + auto program = getDriverApi().createProgram(std::move(p)); + + // Create a Texture and RenderTarget to render into. + auto usage = TextureUsage::COLOR_ATTACHMENT | TextureUsage::SAMPLEABLE; + Handle texture = getDriverApi().createTexture( + SamplerType::SAMPLER_2D, // target + t.mipLevels, // levels + TextureFormat::RGBA8, // format + 1, // samples + renderTargetBaseSize, // width + renderTargetBaseSize, // height + 1, // depth + usage); // usage + + Handle renderTarget = getDriverApi().createRenderTarget( + TargetBufferFlags::COLOR, + // The width and height must match the width and height of the respective mip + // level (at least for OpenGL). + t.getRenderTargetSize(), // width + t.getRenderTargetSize(), // height + t.samples, // samples + TargetBufferInfo(texture, t.mipLevel), // color + {}, // depth + {}); // stencil + + TrianglePrimitive triangle(getDriverApi()); + + RenderPassParams params = {}; + fullViewport(params); + params.flags.clear = TargetBufferFlags::COLOR; + params.clearColor = {0.f, 0.f, 1.f, 1.f}; + params.flags.discardStart = TargetBufferFlags::ALL; + params.flags.discardEnd = TargetBufferFlags::NONE; + params.viewport.height = t.getRenderTargetSize(); + params.viewport.width = t.getRenderTargetSize(); + + getDriverApi().makeCurrent(swapChain, swapChain); + getDriverApi().beginFrame(0, 0, nullptr, nullptr); + + // Render a white triangle over blue. + getDriverApi().beginRenderPass(renderTarget, params); + + PipelineState state; + state.program = program; + state.rasterState.colorWrite = true; + state.rasterState.depthWrite = false; + state.rasterState.depthFunc = RasterState::DepthFunc::A; + state.rasterState.culling = CullingMode::NONE; + getDriverApi().draw(state, triangle.getRenderPrimitive()); + + getDriverApi().endRenderPass(); + + if (t.mipLevel > 0) { + // Render red to the first mip level to check that the backend is actually reading the + // correct mip. + RenderPassParams p = params; + Handle mipLevelOneRT = getDriverApi().createRenderTarget( + TargetBufferFlags::COLOR, + renderTargetBaseSize, // width + renderTargetBaseSize, // height + 1, // samples + TargetBufferInfo(texture, 0), // color + {}, // depth + {}); // stencil + p.clearColor = {1.f, 0.f, 0.f, 1.f}; + getDriverApi().beginRenderPass(mipLevelOneRT, p); + getDriverApi().endRenderPass(); + getDriverApi().destroyRenderTarget(mipLevelOneRT); + } + + // Read pixels. + void* buffer = calloc(1, t.getBufferSizeBytes()); + + PixelBufferDescriptor descriptor(buffer, t.getBufferSizeBytes(), t.format, t.type, + t.alignment, t.left, t.top, t.getPixelBufferStride(), [](void* buffer, size_t size, + void* user) { + const TestCase* test = (const TestCase*) user; + assert(test); + + // Hash the contents of the buffer and check that they match. + uint32_t hash = utils::hash::murmur3((const uint32_t*) buffer, size / 4, 0); + + ASSERT_EQ(test->hash, hash) << test->testName << + " failed: hashes do not match." << std::endl; + + free(buffer); + }, (void*) &t); + getDriverApi().readPixels(renderTarget, t.readRect.x, t.readRect.y, t.readRect.width, + t.readRect.height, std::move(descriptor)); + + // Now render red over what was just rendered. This ensures that readPixels captures the + // state of rendering between render passes. + params.clearColor = {1.f, 0.f, 0.f, 1.f}; + getDriverApi().beginRenderPass(renderTarget, params); + getDriverApi().endRenderPass(); + + getDriverApi().flush(); + getDriverApi().commit(swapChain); + getDriverApi().endFrame(0); + + getDriverApi().destroyProgram(program); + getDriverApi().destroySwapChain(swapChain); + getDriverApi().destroyRenderTarget(renderTarget); + getDriverApi().destroyTexture(texture); + } + + // This ensures all driver commands have finished before exiting the test. + getDriverApi().finish(); + + executeCommands(); + + getDriver().purge(); +} + +} // namespace test diff --git a/ios/samples/backend-test/backend-test/FilamentView.mm b/ios/samples/backend-test/backend-test/FilamentView.mm index 744cafc95f..d06b10f839 100644 --- a/ios/samples/backend-test/backend-test/FilamentView.mm +++ b/ios/samples/backend-test/backend-test/FilamentView.mm @@ -38,6 +38,7 @@ { CAMetalLayer* metalLayer = (CAMetalLayer*) self.layer; metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm; + metalLayer.framebufferOnly = NO; CGRect nativeBounds = [UIScreen mainScreen].nativeBounds; metalLayer.drawableSize = nativeBounds.size;