Implement headless swap chains and readPixels for Metal (#1885)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -79,6 +79,7 @@ struct MetalContext {
|
||||
// Surface-related properties.
|
||||
MetalSwapChain* currentSurface = nullptr;
|
||||
id<CAMetalDrawable> currentDrawable = nullptr;
|
||||
id<MTLTexture> 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<CAMetalDrawable> 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<MTLTexture> acquireDrawable(MetalContext* context);
|
||||
|
||||
id<MTLCommandBuffer> acquireCommandBuffer(MetalContext* context);
|
||||
|
||||
|
||||
@@ -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<CAMetalDrawable> acquireDrawable(MetalContext* context) {
|
||||
if (!context->currentDrawable) {
|
||||
context->currentDrawable = [context->currentSurface->layer nextDrawable];
|
||||
|
||||
if (context->frameFinishedCallback) {
|
||||
id<CAMetalDrawable> 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<MTLCommandBuffer> 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<MTLTexture> 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<CAMetalDrawable> 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<MTLCommandBuffer> 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<MTLCommandBuffer> acquireCommandBuffer(MetalContext* context) {
|
||||
|
||||
@@ -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<MTLCommandBuffer> oneOffBuffer = [mContext->commandQueue commandBuffer];
|
||||
[oneOffBuffer commit];
|
||||
[oneOffBuffer waitUntilCompleted];
|
||||
}
|
||||
|
||||
void MetalDriver::createVertexBufferR(Handle<HwVertexBuffer> vbh, uint8_t bufferCount,
|
||||
@@ -186,6 +190,8 @@ void MetalDriver::createRenderTargetR(Handle<HwRenderTarget> rth,
|
||||
auto getColorTexture = [&]() -> id<MTLTexture> {
|
||||
if (color.handle) {
|
||||
auto colorTexture = handle_cast<MetalTexture>(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<HwRenderTarget> rth,
|
||||
auto getDepthTexture = [&]() -> id<MTLTexture> {
|
||||
if (depth.handle) {
|
||||
auto depthTexture = handle_cast<MetalTexture>(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<HwSwapChain> sch, void* nativeWindow,
|
||||
|
||||
void MetalDriver::createSwapChainHeadlessR(Handle<HwSwapChain> sch,
|
||||
uint32_t width, uint32_t height, uint64_t flags) {
|
||||
// TODO: implement headless swapchain
|
||||
construct_handle<MetalSwapChain>(mHandleMap, sch, width, height);
|
||||
}
|
||||
|
||||
void MetalDriver::createStreamFromTextureIdR(Handle<HwStream>, intptr_t externalTextureId,
|
||||
@@ -697,6 +705,7 @@ void MetalDriver::commit(Handle<HwSwapChain> sch) {
|
||||
[mContext->currentCommandBuffer commit];
|
||||
mContext->currentCommandBuffer = nil;
|
||||
mContext->currentDrawable = nil;
|
||||
mContext->headlessDrawable = nil;
|
||||
}
|
||||
|
||||
void MetalDriver::bindUniformBuffer(size_t index, Handle<HwUniformBuffer> ubh) {
|
||||
@@ -741,10 +750,84 @@ void MetalDriver::stopCapture(int) {
|
||||
[[MTLCaptureManager sharedCaptureManager] stopCapture];
|
||||
}
|
||||
|
||||
|
||||
void MetalDriver::readPixels(Handle<HwRenderTarget> 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<MetalRenderTarget>(mHandleMap, src);
|
||||
id<MTLTexture> 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<MTLTexture> 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 <MTLBlitCommandEncoder> 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<MTLCommandBuffer> 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<HwStream> sh, uint32_t x, uint32_t y, uint32_t width,
|
||||
|
||||
@@ -43,7 +43,13 @@ namespace metal {
|
||||
struct MetalSwapChain : public HwSwapChain {
|
||||
MetalSwapChain(id<MTLDevice> 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -62,9 +62,13 @@ MetalSwapChain::MetalSwapChain(id<MTLDevice> 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<MTLDevice> 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<MTLTexture> MetalRenderTarget::getColor() {
|
||||
if (defaultRenderTarget) {
|
||||
return acquireDrawable(context).texture;
|
||||
return acquireDrawable(context);
|
||||
}
|
||||
if (multisampledColor) {
|
||||
return multisampledColor;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
#include "ShaderGenerator.h"
|
||||
#include "TrianglePrimitive.h"
|
||||
|
||||
#include <CoreVideo/CoreVideo.h>
|
||||
|
||||
namespace {
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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 <utils/Hash.h>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
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<HwTexture> texture = getDriverApi().createTexture(
|
||||
SamplerType::SAMPLER_2D, // target
|
||||
t.mipLevels, // levels
|
||||
TextureFormat::RGBA8, // format
|
||||
1, // samples
|
||||
renderTargetBaseSize, // width
|
||||
renderTargetBaseSize, // height
|
||||
1, // depth
|
||||
usage); // usage
|
||||
|
||||
Handle<HwRenderTarget> 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<HwRenderTarget> 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
|
||||
@@ -38,6 +38,7 @@
|
||||
{
|
||||
CAMetalLayer* metalLayer = (CAMetalLayer*) self.layer;
|
||||
metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
|
||||
metalLayer.framebufferOnly = NO;
|
||||
|
||||
CGRect nativeBounds = [UIScreen mainScreen].nativeBounds;
|
||||
metalLayer.drawableSize = nativeBounds.size;
|
||||
|
||||
Reference in New Issue
Block a user