-
Notifications
You must be signed in to change notification settings - Fork 6k
Composite multiple layers in Windows software rendering #51759
Changes from 18 commits
8672c29
c50fde4
06b27c8
ce31f49
4836c57
09a7265
0c3640f
f35f8dd
b7d0d19
0237513
9385e81
bfb94f0
e012e1e
1d11b21
4361321
b0835af
0dba35b
b64637d
27d3405
e2d7bbb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,8 @@ | |
|
||
namespace flutter { | ||
|
||
constexpr int kOpaqueBlack = 0xff000000; | ||
|
||
CompositorSoftware::CompositorSoftware() {} | ||
|
||
bool CompositorSoftware::CreateBackingStore( | ||
|
@@ -47,18 +49,101 @@ bool CompositorSoftware::Present(FlutterWindowsView* view, | |
return view->ClearSoftwareBitmap(); | ||
} | ||
|
||
// TODO: Support compositing layers and platform views. | ||
// See: https://github.com/flutter/flutter/issues/31713 | ||
FML_DCHECK(layers_count == 1); | ||
FML_DCHECK(layers[0]->offset.x == 0 && layers[0]->offset.y == 0); | ||
FML_DCHECK(layers[0]->type == kFlutterLayerContentTypeBackingStore); | ||
FML_DCHECK(layers[0]->backing_store->type == | ||
kFlutterBackingStoreTypeSoftware); | ||
// Bypass composition logic if there is only one layer. | ||
if (layers_count == 1) { | ||
const FlutterLayer* layer = layers[0]; | ||
FML_DCHECK(layer != nullptr); | ||
if (layer->type == kFlutterLayerContentTypeBackingStore && | ||
layer->offset.x == 0 && layer->offset.y == 0) { | ||
auto& backing_store = *layer->backing_store; | ||
FML_DCHECK(backing_store.type == kFlutterBackingStoreTypeSoftware); | ||
auto& software = backing_store.software; | ||
return view->PresentSoftwareBitmap(software.allocation, | ||
software.row_bytes, software.height); | ||
} | ||
} | ||
|
||
// Composite many layers. | ||
int x_min = INT_MAX; | ||
int x_max = INT_MIN; | ||
int y_min = INT_MAX; | ||
int y_max = INT_MIN; | ||
for (const FlutterLayer** layer = layers; layer < layers + layers_count; | ||
layer++) { | ||
yaakovschectman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const FlutterPoint& offset = (*layer)->offset; | ||
const FlutterSize& size = (*layer)->size; | ||
// FlutterPoint and FlutterSize store coordinates as doubles. | ||
// Coordinates must be truncated to integers to represent whole pixels. | ||
x_min = std::min(x_min, static_cast<int>(offset.x)); | ||
y_min = std::min(y_min, static_cast<int>(offset.y)); | ||
yaakovschectman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
x_max = std::max(x_max, static_cast<int>(offset.x + size.width)); | ||
y_max = std::max(y_max, static_cast<int>(offset.y + size.height)); | ||
} | ||
|
||
int width = x_max - x_min; | ||
int height = y_max - y_min; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we add a helper for the logic to calculate the position/size of the present? |
||
std::vector<uint32_t> allocation(width * height, kOpaqueBlack); | ||
|
||
for (const FlutterLayer** layer = layers; layer < layers + layers_count; | ||
layer++) { | ||
// TODO(schectman): handle platform view type layers. | ||
// https://github.com/flutter/flutter/issues/143375 | ||
if ((*layer)->type == kFlutterLayerContentTypeBackingStore) { | ||
BlendLayer(allocation, **layer, x_min, y_min, width, height); | ||
} else { | ||
FML_UNREACHABLE(); | ||
return false; | ||
} | ||
} | ||
|
||
return view->PresentSoftwareBitmap(static_cast<void*>(allocation.data()), | ||
width * 4, height); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For someone not familiar with the bitmap allocation, this |
||
} | ||
|
||
void CompositorSoftware::BlendLayer(std::vector<uint32_t>& allocation, | ||
yaakovschectman marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of an instance method that's declared in the header, should we make this be a top-level function in an anonymous namespace in this file? In my mind the header file is best for public APIs or methods that need to interact with instance fields. However, this helper is self-contained logic that doesn't require any instance members. |
||
const FlutterLayer& layer, | ||
int x_min, | ||
int y_min, | ||
int width, | ||
int height) const { | ||
FML_DCHECK(layer.type == kFlutterLayerContentTypeBackingStore); | ||
auto& backing_store = *layer.backing_store; | ||
FML_DCHECK(backing_store.type == kFlutterBackingStoreTypeSoftware); | ||
auto src_data = | ||
static_cast<const uint32_t*>(backing_store.software.allocation); | ||
const FlutterPoint& offset = layer.offset; | ||
const FlutterSize& size = layer.size; | ||
|
||
// Bounds for iteration to prevent out-of-bounds destination coordinates. | ||
int y_src_min = std::max(0., y_min - offset.y); | ||
int y_src_max = std::min(size.height, height + y_min - offset.y); | ||
int x_src_min = std::max(0., x_min - offset.x); | ||
int x_src_max = std::min(size.width, width + x_min - offset.x); | ||
for (int y_src = y_src_min; y_src < y_src_max; y_src++) { | ||
int y_dst = y_src + offset.y - y_min; | ||
for (int x_src = x_src_min; x_src < x_src_max; x_src++) { | ||
int x_dst = x_src + offset.x + x_min; | ||
size_t i_src = y_src * size.width + x_src; | ||
size_t i_dst = y_dst * width + x_dst; | ||
uint32_t src = src_data[i_src]; | ||
uint32_t dst = allocation[i_dst]; | ||
|
||
const auto& backing_store = layers[0]->backing_store->software; | ||
int r_src = (src >> 0) & 0xff; | ||
int g_src = (src >> 8) & 0xff; | ||
int b_src = (src >> 16) & 0xff; | ||
int a_src = (src >> 24) & 0xff; | ||
|
||
return view->PresentSoftwareBitmap( | ||
backing_store.allocation, backing_store.row_bytes, backing_store.height); | ||
int r_dst = (dst >> 0) & 0xff; | ||
int g_dst = (dst >> 8) & 0xff; | ||
int b_dst = (dst >> 16) & 0xff; | ||
|
||
int r = (r_dst * 255 + (r_src - r_dst) * a_src) / 255; | ||
int g = (g_dst * 255 + (g_src - g_dst) * a_src) / 255; | ||
int b = (b_dst * 255 + (b_src - b_dst) * a_src) / 255; | ||
|
||
allocation[i_dst] = (r << 0) | (g << 8) | (b << 16) | (0xff << 24); | ||
} | ||
} | ||
} | ||
|
||
} // namespace flutter |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -107,5 +107,114 @@ TEST_F(CompositorSoftwareTest, PresentEmpty) { | |
EXPECT_TRUE(compositor.Present(view(), nullptr, 0)); | ||
} | ||
|
||
// Test compositing an upper layer on a base layer, each 2x2 pixels. | ||
// Base layer has opaque pixel values: | ||
// BLACK RED | ||
// GREEN WHITE | ||
// Overlay layer has pixel values: | ||
// RED: 127 WHITE: 0 | ||
// BLUE: 127 BLACK: 255 | ||
TEST_F(CompositorSoftwareTest, PresentMultiLayers) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we also have a test for layers that don't overlap fully? |
||
UseEngineWithView(); | ||
|
||
CompositorSoftware compositor; | ||
|
||
FlutterBackingStoreConfig config = {sizeof(config), {2, 2}}; | ||
FlutterBackingStore backing_store0 = {sizeof(FlutterBackingStore), nullptr}; | ||
FlutterBackingStore backing_store1 = {sizeof(FlutterBackingStore), nullptr}; | ||
|
||
ASSERT_TRUE(compositor.CreateBackingStore(config, &backing_store0)); | ||
ASSERT_TRUE(compositor.CreateBackingStore(config, &backing_store1)); | ||
|
||
uint32_t pixels0[4] = {0xff000000, 0xff0000ff, 0xff00ff00, 0xffffffff}; | ||
uint32_t pixels1[4] = {0x7f0000ff, 0x00ffffff, 0x7fff0000, 0xff000000}; | ||
|
||
std::memcpy(const_cast<void*>(backing_store0.software.allocation), pixels0, | ||
sizeof(uint32_t) * 4); | ||
std::memcpy(const_cast<void*>(backing_store1.software.allocation), pixels1, | ||
sizeof(uint32_t) * 4); | ||
|
||
FlutterLayer layer0 = {}; | ||
layer0.type = kFlutterLayerContentTypeBackingStore; | ||
layer0.backing_store = &backing_store0; | ||
layer0.offset = {0, 0}; | ||
layer0.size = {2, 2}; | ||
|
||
FlutterLayer layer1 = layer0; | ||
layer1.backing_store = &backing_store1; | ||
const FlutterLayer* layer_ptr[2] = {&layer0, &layer1}; | ||
|
||
EXPECT_CALL(*view(), PresentSoftwareBitmap) | ||
.WillOnce([&](const void* allocation, size_t row_bytes, size_t height) { | ||
auto pixel_data = static_cast<const uint32_t*>(allocation); | ||
EXPECT_EQ(row_bytes, 2 * sizeof(uint32_t)); | ||
EXPECT_EQ(height, 2); | ||
EXPECT_EQ(pixel_data[0], 0xff00007f); | ||
EXPECT_EQ(pixel_data[1], 0xff0000ff); | ||
EXPECT_EQ(pixel_data[2], 0xff7f8000); | ||
EXPECT_EQ(pixel_data[3], 0xff000000); | ||
return true; | ||
}); | ||
EXPECT_TRUE(compositor.Present(view(), layer_ptr, 2)); | ||
|
||
ASSERT_TRUE(compositor.CollectBackingStore(&backing_store0)); | ||
ASSERT_TRUE(compositor.CollectBackingStore(&backing_store1)); | ||
} | ||
|
||
// Test compositing layers with offsets. | ||
// 0th layer is a single red pixel in the top-left. | ||
// 1st layer is a row of two blue pixels on the second row. | ||
TEST_F(CompositorSoftwareTest, PresentOffsetLayers) { | ||
UseEngineWithView(); | ||
|
||
CompositorSoftware compositor; | ||
|
||
FlutterBackingStoreConfig config0 = {sizeof(FlutterBackingStoreConfig), | ||
{1, 1}}; | ||
FlutterBackingStore backing_store0 = {sizeof(FlutterBackingStore), nullptr}; | ||
FlutterBackingStoreConfig config1 = {sizeof(FlutterBackingStoreConfig), | ||
{2, 1}}; | ||
FlutterBackingStore backing_store1 = {sizeof(FlutterBackingStore), nullptr}; | ||
|
||
ASSERT_TRUE(compositor.CreateBackingStore(config0, &backing_store0)); | ||
ASSERT_TRUE(compositor.CreateBackingStore(config1, &backing_store1)); | ||
|
||
uint32_t pixels0 = 0xff0000ff; | ||
uint32_t pixels1[2] = {0xffff0000, 0xffff0000}; | ||
|
||
std::memcpy(const_cast<void*>(backing_store0.software.allocation), &pixels0, | ||
sizeof(uint32_t) * 1); | ||
std::memcpy(const_cast<void*>(backing_store1.software.allocation), pixels1, | ||
sizeof(uint32_t) * 2); | ||
|
||
FlutterLayer layer0 = {}; | ||
layer0.type = kFlutterLayerContentTypeBackingStore; | ||
layer0.backing_store = &backing_store0; | ||
layer0.offset = {0, 0}; | ||
layer0.size = {1, 1}; | ||
|
||
FlutterLayer layer1 = layer0; | ||
layer1.backing_store = &backing_store1; | ||
layer1.offset = {0, 1}; | ||
layer1.size = {2, 1}; | ||
const FlutterLayer* layer_ptr[2] = {&layer0, &layer1}; | ||
|
||
EXPECT_CALL(*view(), PresentSoftwareBitmap) | ||
.WillOnce([&](const void* allocation, size_t row_bytes, size_t height) { | ||
auto pixel_data = static_cast<const uint32_t*>(allocation); | ||
EXPECT_EQ(row_bytes, 2 * sizeof(uint32_t)); | ||
EXPECT_EQ(height, 2); | ||
EXPECT_EQ(pixel_data[0], 0xff0000ff); | ||
EXPECT_EQ(pixel_data[1], 0xff000000); | ||
EXPECT_EQ(pixel_data[2], 0xffff0000); | ||
EXPECT_EQ(pixel_data[3], 0xffff0000); | ||
return true; | ||
}); | ||
EXPECT_TRUE(compositor.Present(view(), layer_ptr, 2)); | ||
|
||
ASSERT_TRUE(compositor.CollectBackingStore(&backing_store0)); | ||
ASSERT_TRUE(compositor.CollectBackingStore(&backing_store1)); | ||
} | ||
|
||
} // namespace testing | ||
} // namespace flutter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we split this up and have a helper for the compositing step?