Files
filament/filament/backend/test/test_ReadPixels.cpp
2020-10-28 12:58:54 -07:00

322 lines
12 KiB
C++

/*
* 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>
using namespace filament;
using namespace filament::backend;
#ifndef IOS
#include <imageio/ImageEncoder.h>
#include <image/ColorTransform.h>
using namespace image;
#endif
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 {
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;
}
void exportScreenshot(void* pixelData) const {
#ifndef IOS
const size_t width = readRect.width, height = readRect.height;
LinearImage image(width, height, 4);
if (format == PixelDataFormat::RGBA && type == PixelDataType::UBYTE) {
image = toLinearWithAlpha<uint8_t>(width, height, width * 4, (uint8_t*) pixelData);
}
if (format == PixelDataFormat::RGBA && type == PixelDataType::FLOAT) {
memcpy(image.getPixelRef(), pixelData, width * height * sizeof(math::float4));
}
std::string png = std::string(testName) + ".png";
std::ofstream outputStream(png.c_str(), std::ios::binary | std::ios::trunc);
ImageEncoder::encode(outputStream, ImageEncoder::Format::PNG, image, "",
png.c_str());
#endif
}
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);
test->exportScreenshot(buffer);
// 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