blob: 8d2e48b33e74722d36c008659d769301de858957 [file]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Golden tests for the low-level renderer API. Unlike renderer_unittests.cc,
// which opens an interactive playground, the tests here render through the
// golden harness and have their output uploaded to Skia Gold. They only build
// as part of the golden test executable.
#ifdef IMPELLER_GOLDEN_TESTS
#include <array>
#include <cstdint>
#include <vector>
#include "flutter/impeller/golden_tests/golden_playground_test.h"
#include "impeller/core/device_buffer.h"
#include "impeller/core/formats.h"
#include "impeller/core/host_buffer.h"
#include "impeller/core/sampler_descriptor.h"
#include "impeller/core/texture_descriptor.h"
#include "impeller/fixtures/baby.frag.h"
#include "impeller/fixtures/baby.vert.h"
#include "impeller/fixtures/instanced_attributes.frag.h"
#include "impeller/fixtures/instanced_attributes.vert.h"
#include "impeller/fixtures/mipmaps.frag.h"
#include "impeller/fixtures/mipmaps.vert.h"
#include "impeller/fixtures/texture.frag.h"
#include "impeller/fixtures/texture.vert.h"
#include "impeller/geometry/color.h"
#include "impeller/geometry/matrix.h"
#include "impeller/playground/playground_test.h"
#include "impeller/renderer/blit_pass.h"
#include "impeller/renderer/command_buffer.h"
#include "impeller/renderer/command_queue.h"
#include "impeller/renderer/pipeline_builder.h"
#include "impeller/renderer/pipeline_library.h"
#include "impeller/renderer/render_pass.h"
#include "impeller/renderer/vertex_buffer_builder.h"
// TODO(zanderso): https://github.com/flutter/flutter/issues/127701
// NOLINTBEGIN(bugprone-unchecked-optional-access)
namespace impeller {
namespace testing {
using RendererGoldenTest = GoldenPlaygroundTest;
INSTANTIATE_PLAYGROUND_SUITE(RendererGoldenTest);
// Ported from RendererTest.BabysFirstTriangle. Draws a single gradient
// triangle straight through the renderer API. The shader's time uniform is
// pinned to zero so the golden is deterministic.
TEST_P(RendererGoldenTest, BabysFirstTriangle) {
using VS = BabyVertexShader;
using FS = BabyFragmentShader;
std::shared_ptr<Context> context = GetContext();
ASSERT_TRUE(context);
auto desc = PipelineBuilder<VS, FS>::MakeDefaultPipelineDescriptor(*context);
ASSERT_TRUE(desc.has_value());
// Match the golden harness render target: single-sampled, no depth/stencil.
// `ClearStencilAttachments` also resets the stencil pixel format on the
// pipeline, which Metal validation requires to match the target's lack of a
// stencil texture; `SetStencilAttachmentDescriptors(nullopt)` alone leaves
// the format set and trips that validation.
desc->SetSampleCount(SampleCount::kCount1);
desc->ClearStencilAttachments();
desc->ClearDepthAttachment();
auto pipeline = context->GetPipelineLibrary()->GetPipeline(desc).Get();
ASSERT_TRUE(pipeline);
VertexBufferBuilder<VS::PerVertexData> vertex_buffer_builder;
vertex_buffer_builder.AddVertices({
{{-0.5, -0.5}, Color::Red(), Color::Green()},
{{0.0, 0.5}, Color::Green(), Color::Blue()},
{{0.5, -0.5}, Color::Blue(), Color::Red()},
});
auto vertex_buffer = vertex_buffer_builder.CreateVertexBuffer(
*context->GetResourceAllocator());
auto host_buffer = HostBuffer::Create(
context->GetResourceAllocator(), context->GetIdleWaiter(),
context->GetCapabilities()->GetMinimumUniformAlignment());
ASSERT_TRUE(OpenPlaygroundHere([&](RenderPass& pass) -> bool {
// The harness runs the callback once per pass; start each from a clean
// host buffer.
host_buffer->Reset();
pass.SetPipeline(pipeline);
pass.SetVertexBuffer(vertex_buffer);
FS::FragInfo frag_info;
frag_info.time = 0.0f;
FS::BindFragInfo(pass, host_buffer->EmplaceUniform(frag_info));
return pass.Draw().ok();
}));
}
// Ported from RendererTest.CanRenderInstancedWithVertexAttributes. Renders an
// instanced draw whose per-instance data arrives through an instance-rate
// vertex buffer binding rather than an instance-ID builtin. This is the only
// portable instancing mechanism on OpenGL ES. Per-instance offsets and colors
// are fixed so the golden is deterministic.
TEST_P(RendererGoldenTest, CanRenderInstancedWithVertexAttributes) {
using VS = InstancedAttributesVertexShader;
using FS = InstancedAttributesFragmentShader;
std::shared_ptr<Context> context = GetContext();
ASSERT_TRUE(context);
auto desc = PipelineBuilder<VS, FS>::MakeDefaultPipelineDescriptor(*context);
ASSERT_TRUE(desc.has_value());
// Match the golden harness render target: single-sampled, no depth/stencil.
desc->SetSampleCount(SampleCount::kCount1);
desc->ClearStencilAttachments();
desc->ClearDepthAttachment();
// Per-instance data is laid out contiguously, one record per instance.
struct InstanceData {
Vector2 offset;
Vector4 color;
};
// Two vertex bindings: binding 0 carries per-vertex geometry and advances
// once per vertex; binding 1 carries per-instance data and advances once
// per instance.
auto vertex_desc = std::make_shared<VertexDescriptor>();
ShaderStageIOSlot position_slot = VS::kInputVertexPosition;
ShaderStageIOSlot offset_slot = VS::kInputInstanceOffset;
ShaderStageIOSlot color_slot = VS::kInputInstanceColor;
position_slot.binding = 0;
position_slot.offset = 0;
offset_slot.binding = 1;
offset_slot.offset = offsetof(InstanceData, offset);
color_slot.binding = 1;
color_slot.offset = offsetof(InstanceData, color);
const std::vector<ShaderStageIOSlot> io_slots = {position_slot, offset_slot,
color_slot};
const std::vector<ShaderStageBufferLayout> layouts = {
ShaderStageBufferLayout{.stride = sizeof(Vector2),
.binding = 0,
.input_rate = VertexInputRate::kVertex},
ShaderStageBufferLayout{.stride = sizeof(InstanceData),
.binding = 1,
.input_rate = VertexInputRate::kInstance},
};
vertex_desc->RegisterDescriptorSetLayouts(VS::kDescriptorSetLayouts);
vertex_desc->RegisterDescriptorSetLayouts(FS::kDescriptorSetLayouts);
vertex_desc->SetStageInputs(io_slots, layouts);
desc->SetVertexDescriptor(std::move(vertex_desc));
auto pipeline =
context->GetPipelineLibrary()->GetPipeline(std::move(desc)).Get();
ASSERT_TRUE(pipeline);
// A single triangle, drawn once per instance.
std::array<Vector2, 3> geometry = {
Vector2{0, 0},
Vector2{0, 100},
Vector2{100, 0},
};
static constexpr size_t kInstanceCount = 4u;
std::array<InstanceData, kInstanceCount> instances = {
InstanceData{Vector2{0, 0}, Vector4{1, 0, 0, 1}},
InstanceData{Vector2{120, 0}, Vector4{0, 1, 0, 1}},
InstanceData{Vector2{0, 120}, Vector4{0, 0, 1, 1}},
InstanceData{Vector2{120, 120}, Vector4{1, 1, 0, 1}},
};
auto geometry_buffer = context->GetResourceAllocator()->CreateBufferWithCopy(
reinterpret_cast<uint8_t*>(geometry.data()),
geometry.size() * sizeof(Vector2));
auto instance_buffer = context->GetResourceAllocator()->CreateBufferWithCopy(
reinterpret_cast<uint8_t*>(instances.data()),
instances.size() * sizeof(InstanceData));
ASSERT_TRUE(geometry_buffer && instance_buffer);
auto host_buffer = HostBuffer::Create(
context->GetResourceAllocator(), context->GetIdleWaiter(),
context->GetCapabilities()->GetMinimumUniformAlignment());
ASSERT_TRUE(OpenPlaygroundHere([&](RenderPass& pass) -> bool {
// The harness runs the callback once per pass; start each from a clean
// host buffer.
host_buffer->Reset();
pass.SetCommandLabel("InstancedAttributes");
pass.SetPipeline(pipeline);
std::array<BufferView, 2> vertex_buffers = {
BufferView(geometry_buffer,
Range(0, geometry.size() * sizeof(Vector2))),
BufferView(instance_buffer,
Range(0, instances.size() * sizeof(InstanceData))),
};
pass.SetVertexBuffer(vertex_buffers.data(), vertex_buffers.size());
pass.SetElementCount(geometry.size());
pass.SetInstanceCount(kInstanceCount);
VS::FrameInfo frame_info;
frame_info.mvp =
pass.GetOrthographicTransform() * Matrix::MakeScale(GetContentScale());
VS::BindFrameInfo(pass, host_buffer->EmplaceUniform(frame_info));
return pass.Draw().ok();
}));
}
// Samples a sample-only block-compressed texture across a fullscreen quad
// through the golden harness. The pixel format and the raw block bytes are the
// only things that differ between the compressed families; the texture upload,
// pipeline, quad, and draw are shared by every compressed-format golden below.
static void DrawCompressedTextureGolden(GoldenPlaygroundTest& test,
PixelFormat format,
const std::vector<uint8_t>& block_data,
ISize size) {
using VS = TextureVertexShader;
using FS = TextureFragmentShader;
std::shared_ptr<Context> context = test.GetContext();
ASSERT_TRUE(context);
TextureDescriptor texture_desc;
texture_desc.storage_mode = StorageMode::kHostVisible;
texture_desc.format = format;
texture_desc.size = size;
texture_desc.mip_count = 1u;
texture_desc.usage = TextureUsage::kShaderRead;
auto texture = context->GetResourceAllocator()->CreateTexture(texture_desc);
ASSERT_TRUE(texture);
ASSERT_TRUE(texture->SetContents(block_data.data(), block_data.size()));
auto desc = PipelineBuilder<VS, FS>::MakeDefaultPipelineDescriptor(*context);
ASSERT_TRUE(desc.has_value());
desc->SetSampleCount(SampleCount::kCount1);
desc->ClearStencilAttachments();
desc->ClearDepthAttachment();
auto pipeline = context->GetPipelineLibrary()->GetPipeline(desc).Get();
ASSERT_TRUE(pipeline);
// A fullscreen quad in normalized device coordinates with an identity MVP.
VertexBufferBuilder<VS::PerVertexData> vertex_buffer_builder;
vertex_buffer_builder.AddVertices({
{{-1, -1, 0.0}, {0.0, 0.0}},
{{1, -1, 0.0}, {1.0, 0.0}},
{{1, 1, 0.0}, {1.0, 1.0}},
{{-1, -1, 0.0}, {0.0, 0.0}},
{{1, 1, 0.0}, {1.0, 1.0}},
{{-1, 1, 0.0}, {0.0, 1.0}},
});
auto vertex_buffer = vertex_buffer_builder.CreateVertexBuffer(
*context->GetResourceAllocator());
const auto& sampler = context->GetSamplerLibrary()->GetSampler({});
auto host_buffer = HostBuffer::Create(
context->GetResourceAllocator(), context->GetIdleWaiter(),
context->GetCapabilities()->GetMinimumUniformAlignment());
ASSERT_TRUE(test.OpenPlaygroundHere([&](RenderPass& pass) -> bool {
host_buffer->Reset();
pass.SetPipeline(pipeline);
pass.SetVertexBuffer(vertex_buffer);
VS::UniformBuffer uniforms;
uniforms.mvp = Matrix();
VS::BindUniformBuffer(pass, host_buffer->EmplaceUniform(uniforms));
FS::BindTextureContents(pass, texture, sampler);
return pass.Draw().ok();
}));
}
// Uploads a block-compressed (BC1/DXT1) texture and samples it onto a
// fullscreen quad. The texture is an 8x8 image laid out as a 2x2 grid of solid
// color blocks, so the golden is four colored quadrants. BC1 is the most widely
// supported compressed family on desktop GPUs; backends without it are skipped.
TEST_P(RendererGoldenTest, CanRenderBC1CompressedTexture) {
std::shared_ptr<Context> context = GetContext();
ASSERT_TRUE(context);
if (!context->GetCapabilities()->SupportsTextureCompression(
CompressedTextureFamily::kBC)) {
GTEST_SKIP() << "Backend does not support BC texture compression.";
}
// A solid-color BC1 block stores the color in both RGB565 endpoints with
// all-zero selector bits, which decodes to a single opaque color.
auto bc1_solid_block = [](uint16_t rgb565) -> std::array<uint8_t, 8> {
const auto lo = static_cast<uint8_t>(rgb565 & 0xFF);
const auto hi = static_cast<uint8_t>(rgb565 >> 8);
return {{lo, hi, lo, hi, 0, 0, 0, 0}};
};
// RGB565: red, green, blue, white.
const std::array<std::array<uint8_t, 8>, 4> blocks = {
{bc1_solid_block(0xF800), bc1_solid_block(0x07E0),
bc1_solid_block(0x001F), bc1_solid_block(0xFFFF)}};
std::vector<uint8_t> data;
for (const auto& block : blocks) {
data.insert(data.end(), block.begin(), block.end());
}
DrawCompressedTextureGolden(*this, PixelFormat::kBC1RGBAUNormInt, data,
ISize{8, 8});
}
// Uploads an ETC2 RGB8 texture and samples it onto a fullscreen quad. ETC2 is
// the standard compressed family on OpenGL ES 3.0 and mobile GPUs; backends
// without it are skipped. The 8x8 image is a 2x2 grid of solid color blocks, so
// the golden is the same four colored quadrants as the BC1 and ASTC goldens.
TEST_P(RendererGoldenTest, CanRenderETC2CompressedTexture) {
std::shared_ptr<Context> context = GetContext();
ASSERT_TRUE(context);
if (!context->GetCapabilities()->SupportsTextureCompression(
CompressedTextureFamily::kETC2)) {
GTEST_SKIP() << "Backend does not support ETC2 texture compression.";
}
// A solid-color ETC2 RGB8 block in "individual" mode (differential bit 0,
// which decodes like ETC1). The 64-bit block is laid out big-endian (byte 0
// most significant): byte 0 = R nibbles (R1,R2), byte 1 = G, byte 2 = B,
// byte 3 = codeword/diff/flip bits (all 0), bytes 4..7 = the two pixel-index
// bit planes. Both sub-blocks share the base color and every texel uses index
// 0 (all-zero planes), so the block is one flat color.
auto etc2_solid_block = [](uint8_t r, uint8_t g,
uint8_t b) -> std::array<uint8_t, 8> {
return {{r, g, b, 0x00, 0x00, 0x00, 0x00, 0x00}};
};
// Red, green, blue, white. Each channel byte is 0xFF (nibble 0xF, ~255) or
// 0x00.
const std::array<std::array<uint8_t, 8>, 4> blocks = {
{etc2_solid_block(0xFF, 0x00, 0x00), etc2_solid_block(0x00, 0xFF, 0x00),
etc2_solid_block(0x00, 0x00, 0xFF), etc2_solid_block(0xFF, 0xFF, 0xFF)}};
std::vector<uint8_t> data;
for (const auto& block : blocks) {
data.insert(data.end(), block.begin(), block.end());
}
DrawCompressedTextureGolden(*this, PixelFormat::kETC2RGB8UNormInt, data,
ISize{8, 8});
}
// Uploads an ASTC 4x4 LDR texture and samples it onto a fullscreen quad. ASTC
// is common on modern mobile and some desktop GPUs; backends without it are
// skipped. The 8x8 image is a 2x2 grid of solid color blocks, so the golden is
// the same four colored quadrants as the BC1 and ETC2 goldens.
TEST_P(RendererGoldenTest, CanRenderASTCCompressedTexture) {
std::shared_ptr<Context> context = GetContext();
ASSERT_TRUE(context);
if (!context->GetCapabilities()->SupportsTextureCompression(
CompressedTextureFamily::kASTC)) {
GTEST_SKIP() << "Backend does not support ASTC texture compression.";
}
// An ASTC void-extent block encodes one constant color directly: the 0xFC
// 0xFD header marks a 2D LDR void-extent with the "no extent" sentinel
// coordinates (all ones), followed by four little-endian UNORM16 channels
// (R, G, B, A). Alpha is opaque.
auto astc_solid_block = [](uint16_t r, uint16_t g,
uint16_t b) -> std::array<uint8_t, 16> {
return {{0xFC, 0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
static_cast<uint8_t>(r & 0xFF), static_cast<uint8_t>(r >> 8),
static_cast<uint8_t>(g & 0xFF), static_cast<uint8_t>(g >> 8),
static_cast<uint8_t>(b & 0xFF), static_cast<uint8_t>(b >> 8), 0xFF,
0xFF}};
};
// Red, green, blue, white.
const std::array<std::array<uint8_t, 16>, 4> blocks = {
{astc_solid_block(0xFFFF, 0, 0), astc_solid_block(0, 0xFFFF, 0),
astc_solid_block(0, 0, 0xFFFF),
astc_solid_block(0xFFFF, 0xFFFF, 0xFFFF)}};
std::vector<uint8_t> data;
for (const auto& block : blocks) {
data.insert(data.end(), block.begin(), block.end());
}
DrawCompressedTextureGolden(*this, PixelFormat::kASTC4x4LDR, data,
ISize{8, 8});
}
// Samples a texture whose mip chain was populated by hand (the base level via
// SetContents, the 4x4 second level via a blit copy) rather than by the
// GenerateMipmap blit, then samples it at the given LOD. Such a texture has
// mip_count > 1 with NeedsMipmapGeneration() == true; sampling it used to fail
// the (now-removed) bind-time mipmap validation on desktop Metal and OpenGL ES,
// so the golden would have been blank there. The base is four colored quadrants
// and the second level is solid orange, so LOD 0 renders the quadrants and LOD
// 1 renders solid orange.
static void DrawManuallyMippedTextureGolden(GoldenPlaygroundTest& test,
float lod) {
using VS = MipmapsVertexShader;
using FS = MipmapsFragmentShader;
std::shared_ptr<Context> context = test.GetContext();
ASSERT_TRUE(context);
TextureDescriptor texture_desc;
texture_desc.storage_mode = StorageMode::kHostVisible;
texture_desc.format = PixelFormat::kR8G8B8A8UNormInt;
texture_desc.size = ISize{8, 8};
texture_desc.mip_count = 2u; // base 8x8 + one 4x4 mip; populated by hand.
texture_desc.usage = TextureUsage::kShaderRead;
auto texture = context->GetResourceAllocator()->CreateTexture(texture_desc);
ASSERT_TRUE(texture);
// Base level: a 2x2 grid of red/green/blue/white quadrants.
std::vector<uint8_t> base_data(8 * 8 * 4);
for (int y = 0; y < 8; ++y) {
for (int x = 0; x < 8; ++x) {
const size_t i = (static_cast<size_t>(y) * 8 + x) * 4;
const bool right = x >= 4;
const bool bottom = y >= 4;
uint8_t r = 0, g = 0, b = 0;
if (!right && !bottom) {
r = 0xFF; // top-left: red.
} else if (right && !bottom) {
g = 0xFF; // top-right: green.
} else if (!right && bottom) {
b = 0xFF; // bottom-left: blue.
} else {
r = g = b = 0xFF; // bottom-right: white.
}
base_data[i] = r;
base_data[i + 1] = g;
base_data[i + 2] = b;
base_data[i + 3] = 0xFF;
}
}
ASSERT_TRUE(texture->SetContents(base_data.data(), base_data.size()));
// Second level (4x4), solid orange so it is distinct from the base and from
// an empty level. Uploaded by hand via a blit copy rather than the
// GenerateMipmap blit, which keeps NeedsMipmapGeneration() true. This
// initializes the whole mip chain so the texture is complete on backends
// that require it (OpenGL ES samples a mipmapped texture as incomplete
// otherwise).
std::vector<uint8_t> mip_data(4 * 4 * 4);
for (size_t i = 0; i < mip_data.size(); i += 4) {
mip_data[i] = 0xFF; // r
mip_data[i + 1] = 0x80; // g
mip_data[i + 2] = 0x00; // b
mip_data[i + 3] = 0xFF; // a
}
auto mip_buffer = context->GetResourceAllocator()->CreateBufferWithCopy(
mip_data.data(), mip_data.size());
ASSERT_TRUE(mip_buffer);
auto cmd_buffer = context->CreateCommandBuffer();
ASSERT_TRUE(cmd_buffer);
auto blit_pass = cmd_buffer->CreateBlitPass();
ASSERT_TRUE(blit_pass);
// The destination region must match the 4x4 second level, not the 8x8 base,
// or the copy size check rejects the smaller source buffer.
ASSERT_TRUE(
blit_pass->AddCopy(DeviceBuffer::AsBufferView(mip_buffer), texture,
/*destination_region=*/IRect::MakeSize(ISize{4, 4}),
/*label=*/"Upload mip 1", /*mip_level=*/1u));
ASSERT_TRUE(blit_pass->EncodeCommands());
ASSERT_TRUE(context->GetCommandQueue()->Submit({cmd_buffer}).ok());
auto desc = PipelineBuilder<VS, FS>::MakeDefaultPipelineDescriptor(*context);
ASSERT_TRUE(desc.has_value());
desc->SetSampleCount(SampleCount::kCount1);
desc->ClearStencilAttachments();
desc->ClearDepthAttachment();
auto pipeline = context->GetPipelineLibrary()->GetPipeline(desc).Get();
ASSERT_TRUE(pipeline);
// A fullscreen quad in normalized device coordinates with an identity MVP.
VertexBufferBuilder<VS::PerVertexData> vertex_buffer_builder;
vertex_buffer_builder.AddVertices({
{{-1, -1}, {0.0, 0.0}},
{{1, -1}, {1.0, 0.0}},
{{1, 1}, {1.0, 1.0}},
{{-1, -1}, {0.0, 0.0}},
{{1, 1}, {1.0, 1.0}},
{{-1, 1}, {0.0, 1.0}},
});
auto vertex_buffer = vertex_buffer_builder.CreateVertexBuffer(
*context->GetResourceAllocator());
const auto& sampler = context->GetSamplerLibrary()->GetSampler({});
auto host_buffer = HostBuffer::Create(
context->GetResourceAllocator(), context->GetIdleWaiter(),
context->GetCapabilities()->GetMinimumUniformAlignment());
ASSERT_TRUE(test.OpenPlaygroundHere([&](RenderPass& pass) -> bool {
host_buffer->Reset();
pass.SetPipeline(pipeline);
pass.SetVertexBuffer(vertex_buffer);
VS::FrameInfo frame_info;
frame_info.mvp = Matrix();
VS::BindFrameInfo(pass, host_buffer->EmplaceUniform(frame_info));
FS::FragInfo frag_info;
frag_info.lod = lod;
FS::BindFragInfo(pass, host_buffer->EmplaceUniform(frag_info));
FS::BindTex(pass, texture, sampler);
return pass.Draw().ok();
}));
}
// LOD 0 reads the base level, so the golden is the four colored quadrants.
TEST_P(RendererGoldenTest, CanSampleManuallyMippedTexture) {
DrawManuallyMippedTextureGolden(*this, /*lod=*/0.0f);
}
// LOD 1 reads the hand-uploaded second level, so the golden is solid orange.
TEST_P(RendererGoldenTest, CanSampleManuallyMippedTextureLod1) {
DrawManuallyMippedTextureGolden(*this, /*lod=*/1.0f);
}
} // namespace testing
} // namespace impeller
// NOLINTEND(bugprone-unchecked-optional-access)
#endif // IMPELLER_GOLDEN_TESTS