mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Feature] (Damage): damage-region and buffer-age bookkeeping with host tests
This commit is contained in:
@@ -53,7 +53,26 @@ target_link_libraries(
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
# The partial-present bookkeeping: damage rectangles, the y-flip, the per-buffer missed damage of
|
||||
# rotating shared images and the per-image buffer age. Pure, no GL context.
|
||||
add_executable(
|
||||
DamageTest
|
||||
DamageTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(DamageTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
DamageTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(JobNodeTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(LogLevelTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(PipeStatsTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
gtest_discover_tests(DamageTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
// MobileGL - MobileGL/MG_Test/Util/DamageTest.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// The partial-present bookkeeping (MG_Util/Damage/Damage.h): the y-flip between EGL's bottom-left
|
||||
// rectangles and a top-down buffer's, the clamping and merging that fits damage into a record, the
|
||||
// per-buffer damage a rotating set of shared images has missed, and the per-image buffer age.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <MG_Util/Damage/Damage.h>
|
||||
|
||||
using namespace MobileGL;
|
||||
using namespace MobileGL::MG_Util::Damage;
|
||||
|
||||
namespace {
|
||||
// Every pixel of `inner` lies inside some rectangle of `region` (or the region is Full).
|
||||
Bool Covers(const Region& region, const Rect& inner, Int32 width, Int32 height) {
|
||||
if (region.IsFull()) return true;
|
||||
const Rect clipped = Clip(inner, width, height);
|
||||
for (Int32 y = clipped.Y; y < clipped.Y + clipped.Height; ++y) {
|
||||
for (Int32 x = clipped.X; x < clipped.X + clipped.Width; ++x) {
|
||||
Bool inside = false;
|
||||
for (const Rect& r : region.Rects()) {
|
||||
if (x >= r.X && x < r.X + r.Width && y >= r.Y && y < r.Y + r.Height) {
|
||||
inside = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!inside) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Region Of(std::initializer_list<Rect> rects) { return Region::FromRects(rects.begin(), rects.size()); }
|
||||
} // namespace
|
||||
|
||||
// EGL damage (origin bottom-left) -> wl_surface.damage_buffer / shared-image rows (origin top-left).
|
||||
TEST(Damage, FlipYMovesTheOriginBetweenBottomAndTop) {
|
||||
// A 30x40 rectangle 20 rows above the bottom of a 100-row surface starts 40 rows below its top.
|
||||
EXPECT_EQ(FlipY({10, 20, 30, 40}, 100), (Rect{10, 40, 30, 40}));
|
||||
// The bottom row is the last row from the top, and the top row the first.
|
||||
EXPECT_EQ(FlipY({0, 0, 5, 1}, 100), (Rect{0, 99, 5, 1}));
|
||||
EXPECT_EQ(FlipY({0, 99, 5, 1}, 100), (Rect{0, 0, 5, 1}));
|
||||
// Its own inverse.
|
||||
const Rect r{7, 13, 21, 8};
|
||||
EXPECT_EQ(FlipY(FlipY(r, 77), 77), r);
|
||||
// A Region flips every rectangle and leaves Full alone.
|
||||
EXPECT_EQ(Of({{0, 0, 10, 10}}).FlippedY(50), Of({{0, 40, 10, 10}}));
|
||||
EXPECT_TRUE(Region::Full().FlippedY(50).IsFull());
|
||||
}
|
||||
|
||||
TEST(Damage, ClipKeepsTheSurfacePartOnly) {
|
||||
EXPECT_EQ(Clip({-5, -5, 10, 10}, 100, 100), (Rect{0, 0, 5, 5}));
|
||||
EXPECT_EQ(Clip({95, 90, 10, 20}, 100, 100), (Rect{95, 90, 5, 10}));
|
||||
EXPECT_TRUE(Clip({100, 0, 10, 10}, 100, 100).Empty());
|
||||
EXPECT_TRUE(Clip({0, -20, 10, 10}, 100, 100).Empty());
|
||||
}
|
||||
|
||||
TEST(Damage, EglRectsWithNoneMeanTheWholeSurface) {
|
||||
EXPECT_TRUE(Region::FromEglRects<Int32>(nullptr, 0).IsFull());
|
||||
const Int32 rects[] = {1, 2, 3, 4, 10, 20, 30, 40};
|
||||
EXPECT_TRUE(Region::FromEglRects(rects, 0).IsFull());
|
||||
const Region two = Region::FromEglRects(rects, 2);
|
||||
ASSERT_FALSE(two.IsFull());
|
||||
EXPECT_EQ(two.Rects(), (Vector<Rect>{{1, 2, 3, 4}, {10, 20, 30, 40}}));
|
||||
}
|
||||
|
||||
TEST(Damage, NormalizeClipsDropsAndPromotesToFull) {
|
||||
Region region = Of({{-10, -10, 20, 20}, {200, 200, 5, 5}, {50, 50, 0, 3}});
|
||||
region.Normalize(100, 100);
|
||||
EXPECT_EQ(region, Of({{0, 0, 10, 10}}));
|
||||
|
||||
Region outside = Of({{200, 200, 5, 5}});
|
||||
outside.Normalize(100, 100);
|
||||
EXPECT_TRUE(outside.IsEmpty()) << "damage entirely off the surface damages nothing";
|
||||
|
||||
Region whole = Of({{10, 10, 5, 5}, {-1, -1, 102, 102}});
|
||||
whole.Normalize(100, 100);
|
||||
EXPECT_TRUE(whole.IsFull()) << "a rectangle covering the surface is the whole surface";
|
||||
}
|
||||
|
||||
TEST(Damage, NormalizeMergesDownToTheLimitAndStillCoversEverything) {
|
||||
Vector<Rect> input;
|
||||
for (Int32 i = 0; i < 40; ++i) input.push_back({(i * 37) % 900, (i * 53) % 600, 9 + i % 5, 7 + i % 3});
|
||||
Region region = Region::FromRects(input.data(), input.size());
|
||||
region.Normalize(1000, 700, kMaxRects);
|
||||
ASSERT_FALSE(region.IsFull());
|
||||
EXPECT_LE(region.Rects().size(), kMaxRects);
|
||||
for (const Rect& r : input) EXPECT_TRUE(Covers(region, r, 1000, 700)) << r.X << "," << r.Y;
|
||||
|
||||
// Two neighbours and one far away, limit 2: the neighbours merge, the far one stays apart.
|
||||
Region three = Of({{0, 0, 10, 10}, {10, 0, 10, 10}, {500, 500, 10, 10}});
|
||||
three.Normalize(1000, 1000, 2);
|
||||
EXPECT_EQ(three, Of({{0, 0, 20, 10}, {500, 500, 10, 10}}));
|
||||
}
|
||||
|
||||
// Three shared images rotate; each write must bring over what that image missed.
|
||||
TEST(Damage, EachBufferCopiesTheDamageItMissedSinceItsLastWrite) {
|
||||
constexpr Int32 W = 200, H = 100;
|
||||
BufferDamageTracker tracker(3);
|
||||
const Region a = Of({{0, 0, 10, 10}});
|
||||
const Region b = Of({{20, 0, 10, 10}});
|
||||
const Region c = Of({{40, 0, 10, 10}});
|
||||
const Region d = Of({{60, 0, 10, 10}});
|
||||
const Region e = Of({{80, 0, 10, 10}});
|
||||
|
||||
// A buffer never written has unknown contents: its first write is the whole frame.
|
||||
EXPECT_TRUE(tracker.TakeForWrite(0, a, W, H).IsFull());
|
||||
EXPECT_TRUE(tracker.TakeForWrite(1, b, W, H).IsFull());
|
||||
EXPECT_TRUE(tracker.TakeForWrite(2, c, W, H).IsFull());
|
||||
|
||||
// Buffer 0 last held frame A; frames B and C went elsewhere, D comes now.
|
||||
Region copy0 = tracker.TakeForWrite(0, d, W, H);
|
||||
ASSERT_FALSE(copy0.IsFull());
|
||||
for (const Region* r : {&b, &c, &d}) EXPECT_TRUE(Covers(copy0, r->Rects()[0], W, H));
|
||||
EXPECT_FALSE(Covers(copy0, a.Rects()[0], W, H)) << "A is already in buffer 0";
|
||||
|
||||
Region copy1 = tracker.TakeForWrite(1, e, W, H);
|
||||
for (const Region* r : {&c, &d, &e}) EXPECT_TRUE(Covers(copy1, r->Rects()[0], W, H));
|
||||
EXPECT_FALSE(Covers(copy1, b.Rects()[0], W, H));
|
||||
|
||||
// A reallocated buffer starts over.
|
||||
tracker.Invalidate(2);
|
||||
EXPECT_TRUE(tracker.TakeForWrite(2, a, W, H).IsFull());
|
||||
|
||||
// A full-surface frame makes every other buffer copy everything next time.
|
||||
EXPECT_TRUE(tracker.TakeForWrite(0, Region::Full(), W, H).IsFull());
|
||||
EXPECT_TRUE(tracker.TakeForWrite(1, a, W, H).IsFull());
|
||||
EXPECT_TRUE(tracker.TakeForWrite(2, a, W, H).IsFull());
|
||||
// ... and once caught up, only the frames' own damage.
|
||||
EXPECT_EQ(tracker.TakeForWrite(0, b, W, H), Of({{0, 0, 10, 10}, {20, 0, 10, 10}}))
|
||||
<< "buffer 0 missed A twice (buffers 1 and 2) and takes B";
|
||||
}
|
||||
|
||||
TEST(Damage, OneBufferInUseCopiesOnlyEachFramesDamage) {
|
||||
BufferDamageTracker tracker(3);
|
||||
EXPECT_TRUE(tracker.TakeForWrite(0, Of({{0, 0, 4, 4}}), 64, 64).IsFull());
|
||||
EXPECT_EQ(tracker.TakeForWrite(0, Of({{8, 8, 4, 4}}), 64, 64), Of({{8, 8, 4, 4}}));
|
||||
EXPECT_EQ(tracker.TakeForWrite(0, Of({{16, 8, 4, 4}}), 64, 64), Of({{16, 8, 4, 4}}));
|
||||
// Nothing changed: nothing to copy.
|
||||
EXPECT_TRUE(tracker.TakeForWrite(0, Region(), 64, 64).IsEmpty());
|
||||
}
|
||||
|
||||
TEST(Damage, MissedDamageStaysBoundedAndClipped) {
|
||||
BufferDamageTracker tracker(2);
|
||||
// Buffer 0 is up to date with a full frame; the next fifty frames all go to buffer 1.
|
||||
(void)tracker.TakeForWrite(1, Region::Full(), 100, 100);
|
||||
(void)tracker.TakeForWrite(0, Region::Full(), 100, 100);
|
||||
Vector<Rect> written;
|
||||
for (Int32 i = 0; i < 50; ++i) {
|
||||
const Rect r{(i * 13) % 90, (i * 29) % 90, 3, 3};
|
||||
written.push_back(r);
|
||||
(void)tracker.TakeForWrite(1, Of({r, {150, 150, 5, 5}}), 100, 100);
|
||||
}
|
||||
const Region copy = tracker.TakeForWrite(0, Region(), 100, 100);
|
||||
ASSERT_FALSE(copy.IsFull());
|
||||
EXPECT_LE(copy.Rects().size(), kMaxRects);
|
||||
for (const Rect& r : copy.Rects()) EXPECT_FALSE(Clip(r, 100, 100) != r) << "clipped to the surface";
|
||||
for (const Rect& r : written) EXPECT_TRUE(Covers(copy, r, 100, 100));
|
||||
}
|
||||
|
||||
// EGL_EXT_buffer_age per swapchain image.
|
||||
TEST(Damage, SwapchainAgeCountsPresentsSinceTheImageWasShown) {
|
||||
SwapchainAgeTracker ages;
|
||||
ages.Reset(2);
|
||||
EXPECT_EQ(ages.AgeOf(0), 0) << "never presented";
|
||||
ages.OnPresented(0, true);
|
||||
EXPECT_EQ(ages.AgeOf(1), 0);
|
||||
ages.OnPresented(1, true);
|
||||
EXPECT_EQ(ages.AgeOf(0), 2) << "double buffering: the image shown two presents ago";
|
||||
ages.OnPresented(0, true);
|
||||
EXPECT_EQ(ages.AgeOf(1), 2);
|
||||
|
||||
// Triple buffering, in order.
|
||||
ages.Reset(3);
|
||||
for (SizeT i = 0; i < 3; ++i) ages.OnPresented(i, true);
|
||||
EXPECT_EQ(ages.AgeOf(0), 3);
|
||||
ages.OnPresented(0, true);
|
||||
EXPECT_EQ(ages.AgeOf(1), 3);
|
||||
// The same image again (a chain that hands one image back at once).
|
||||
EXPECT_EQ(ages.AgeOf(0), 1);
|
||||
}
|
||||
|
||||
TEST(Damage, SwapchainAgeIsUnknownWhenContentWasNotKeptOrTheChainChanged) {
|
||||
SwapchainAgeTracker ages;
|
||||
ages.Reset(2);
|
||||
ages.OnPresented(0, false);
|
||||
ages.OnPresented(1, true);
|
||||
EXPECT_EQ(ages.AgeOf(0), 0) << "presented without keeping its content";
|
||||
EXPECT_EQ(ages.AgeOf(1), 1);
|
||||
|
||||
ages.OnPresented(0, true);
|
||||
ages.OnFrameDropped();
|
||||
EXPECT_EQ(ages.AgeOf(0), 0) << "a dropped frame breaks the count the application keeps";
|
||||
EXPECT_EQ(ages.AgeOf(1), 0);
|
||||
|
||||
ages.OnPresented(0, true);
|
||||
ages.OnPresented(1, true);
|
||||
ages.Reset(3);
|
||||
EXPECT_EQ(ages.AgeOf(0), 0) << "a rebuilt chain";
|
||||
EXPECT_EQ(ages.AgeOf(1), 0);
|
||||
EXPECT_EQ(ages.AgeOf(7), 0) << "an index the chain does not have";
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// MobileGL - MobileGL/MG_Util/Damage/Damage.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
// DAMAGE AND BUFFER AGE: the bookkeeping behind partial presents, kept free of any GL, EGL, Vulkan
|
||||
// or wire type so a host test pins it.
|
||||
//
|
||||
// Coordinates. A Rect is in pixels of a surface. EGL's swap-with-damage rectangles have their
|
||||
// origin at the BOTTOM-left (GL window coordinates); a wl_buffer, a shared image's memory rows and
|
||||
// a Vulkan image are addressed from the TOP-left. FlipY converts between the two and is its own
|
||||
// inverse.
|
||||
//
|
||||
// Damage. A Region is either the whole surface or a bounded list of rectangles that may overlap
|
||||
// (a copy or a repaint of an overlap twice is harmless). Normalize clamps it to the surface and
|
||||
// keeps it within kMaxRects, merging the pair that grows least, so it fits a fixed-size record.
|
||||
//
|
||||
// Rotating buffers. A buffer that is written only every Nth frame (one of a window's shared
|
||||
// images) has missed the damage of the frames in between: BufferDamageTracker keeps, per buffer,
|
||||
// what changed since that buffer was last written, which is what the next write must bring over.
|
||||
//
|
||||
// Buffer age (EGL_EXT_buffer_age). SwapchainAgeTracker answers, per image of a swapchain, how many
|
||||
// presents ago the image's content was the frame then presented: 0 when unknown (never presented,
|
||||
// its content not kept, or the chain was rebuilt).
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace MobileGL::MG_Util::Damage {
|
||||
struct Rect {
|
||||
Int32 X = 0;
|
||||
Int32 Y = 0;
|
||||
Int32 Width = 0;
|
||||
Int32 Height = 0;
|
||||
|
||||
constexpr Bool Empty() const { return Width <= 0 || Height <= 0; }
|
||||
constexpr Int64 Area() const { return Empty() ? 0 : static_cast<Int64>(Width) * Height; }
|
||||
constexpr Bool operator==(const Rect&) const = default;
|
||||
};
|
||||
|
||||
// The most rectangles a damage record carries.
|
||||
inline constexpr Uint32 kMaxRects = 16;
|
||||
|
||||
// Bottom-left origin <-> top-left origin, on a surface `height` rows tall.
|
||||
constexpr Rect FlipY(const Rect& rect, Int32 height) {
|
||||
return {rect.X, height - rect.Y - rect.Height, rect.Width, rect.Height};
|
||||
}
|
||||
|
||||
// The bounding box of two rectangles (an empty one adds nothing).
|
||||
constexpr Rect Bound(const Rect& a, const Rect& b) {
|
||||
if (a.Empty()) return b;
|
||||
if (b.Empty()) return a;
|
||||
const Int32 x0 = std::min(a.X, b.X);
|
||||
const Int32 y0 = std::min(a.Y, b.Y);
|
||||
const Int32 x1 = std::max(a.X + a.Width, b.X + b.Width);
|
||||
const Int32 y1 = std::max(a.Y + a.Height, b.Y + b.Height);
|
||||
return {x0, y0, x1 - x0, y1 - y0};
|
||||
}
|
||||
|
||||
// `rect` clipped to the surface (0, 0, width, height); empty when it lies outside.
|
||||
constexpr Rect Clip(const Rect& rect, Int32 width, Int32 height) {
|
||||
const Int64 x0 = std::max<Int64>(rect.X, 0);
|
||||
const Int64 y0 = std::max<Int64>(rect.Y, 0);
|
||||
const Int64 x1 = std::min<Int64>(static_cast<Int64>(rect.X) + rect.Width, width);
|
||||
const Int64 y1 = std::min<Int64>(static_cast<Int64>(rect.Y) + rect.Height, height);
|
||||
if (x1 <= x0 || y1 <= y0) return {};
|
||||
return {static_cast<Int32>(x0), static_cast<Int32>(y0), static_cast<Int32>(x1 - x0),
|
||||
static_cast<Int32>(y1 - y0)};
|
||||
}
|
||||
|
||||
// Whether `outer` contains every pixel of `inner`.
|
||||
constexpr Bool Contains(const Rect& outer, const Rect& inner) {
|
||||
return inner.X >= outer.X && inner.Y >= outer.Y && inner.X + inner.Width <= outer.X + outer.Width &&
|
||||
inner.Y + inner.Height <= outer.Y + outer.Height;
|
||||
}
|
||||
|
||||
class Region {
|
||||
public:
|
||||
static Region Full() {
|
||||
Region region;
|
||||
region.m_full = true;
|
||||
return region;
|
||||
}
|
||||
static Region FromRects(const Rect* rects, SizeT count) {
|
||||
Region region;
|
||||
for (SizeT i = 0; i < count; ++i) region.Add(rects[i]);
|
||||
return region;
|
||||
}
|
||||
// EGL_KHR/EXT_swap_buffers_with_damage's argument: `count` groups of x, y, width, height,
|
||||
// bottom-left origin. No rectangles means the whole surface.
|
||||
template <typename IntT>
|
||||
static Region FromEglRects(const IntT* rects, IntT count) {
|
||||
if (rects == nullptr || count <= 0) return Full();
|
||||
Region region;
|
||||
for (IntT i = 0; i < count; ++i) {
|
||||
region.Add({static_cast<Int32>(rects[i * 4 + 0]), static_cast<Int32>(rects[i * 4 + 1]),
|
||||
static_cast<Int32>(rects[i * 4 + 2]), static_cast<Int32>(rects[i * 4 + 3])});
|
||||
}
|
||||
return region;
|
||||
}
|
||||
|
||||
Bool IsFull() const { return m_full; }
|
||||
// Nothing damaged (and not the whole surface).
|
||||
Bool IsEmpty() const { return !m_full && m_rects.empty(); }
|
||||
const Vector<Rect>& Rects() const { return m_rects; }
|
||||
|
||||
void Add(const Rect& rect) {
|
||||
if (m_full || rect.Empty()) return;
|
||||
m_rects.push_back(rect);
|
||||
}
|
||||
void Unite(const Region& other) {
|
||||
if (m_full) return;
|
||||
if (other.m_full) {
|
||||
SetFull();
|
||||
return;
|
||||
}
|
||||
for (const Rect& rect : other.m_rects) Add(rect);
|
||||
}
|
||||
void SetFull() {
|
||||
m_full = true;
|
||||
m_rects.clear();
|
||||
}
|
||||
void Clear() {
|
||||
m_full = false;
|
||||
m_rects.clear();
|
||||
}
|
||||
|
||||
// Clipped to a `width` x `height` surface; a rectangle that covers it makes the region Full;
|
||||
// more than `maxRects` rectangles are merged, cheapest pair first, until they fit.
|
||||
void Normalize(Int32 width, Int32 height, Uint32 maxRects = kMaxRects) {
|
||||
if (m_full) return;
|
||||
Vector<Rect> kept;
|
||||
kept.reserve(m_rects.size());
|
||||
for (const Rect& rect : m_rects) {
|
||||
const Rect clipped = Clip(rect, width, height);
|
||||
if (clipped.Empty()) continue;
|
||||
if (clipped.X == 0 && clipped.Y == 0 && clipped.Width == width && clipped.Height == height) {
|
||||
SetFull();
|
||||
return;
|
||||
}
|
||||
kept.push_back(clipped);
|
||||
}
|
||||
// A rectangle inside another (a repeat, most often) adds nothing.
|
||||
Vector<Rect> distinct;
|
||||
distinct.reserve(kept.size());
|
||||
for (SizeT i = 0; i < kept.size(); ++i) {
|
||||
Bool covered = false;
|
||||
for (SizeT j = 0; j < kept.size() && !covered; ++j) {
|
||||
if (i == j || !Contains(kept[j], kept[i])) continue;
|
||||
// Of two equal rectangles the first stays.
|
||||
covered = !(kept[j] == kept[i]) || j < i;
|
||||
}
|
||||
if (!covered) distinct.push_back(kept[i]);
|
||||
}
|
||||
const SizeT limit = std::max<Uint32>(maxRects, 1);
|
||||
// Far too many to pair up every frame: their bounding box.
|
||||
if (distinct.size() > 4 * kMaxRects && distinct.size() > limit) {
|
||||
Rect box;
|
||||
for (const Rect& rect : distinct) box = Bound(box, rect);
|
||||
distinct.assign(1, box);
|
||||
}
|
||||
while (distinct.size() > limit) {
|
||||
SizeT bestI = 0, bestJ = 1;
|
||||
Int64 bestGrowth = std::numeric_limits<Int64>::max();
|
||||
for (SizeT i = 0; i < distinct.size(); ++i) {
|
||||
for (SizeT j = i + 1; j < distinct.size(); ++j) {
|
||||
const Int64 growth =
|
||||
Bound(distinct[i], distinct[j]).Area() - distinct[i].Area() - distinct[j].Area();
|
||||
if (growth < bestGrowth) {
|
||||
bestGrowth = growth;
|
||||
bestI = i;
|
||||
bestJ = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
distinct[bestI] = Bound(distinct[bestI], distinct[bestJ]);
|
||||
distinct.erase(distinct.begin() + static_cast<std::ptrdiff_t>(bestJ));
|
||||
}
|
||||
if (distinct.size() == 1 && distinct[0] == Rect{0, 0, width, height}) {
|
||||
SetFull();
|
||||
return;
|
||||
}
|
||||
m_rects = std::move(distinct);
|
||||
}
|
||||
|
||||
// The rectangles flipped between bottom-left and top-left origin (a Full region stays Full).
|
||||
Region FlippedY(Int32 height) const {
|
||||
Region region = *this;
|
||||
for (Rect& rect : region.m_rects) rect = FlipY(rect, height);
|
||||
return region;
|
||||
}
|
||||
|
||||
Bool operator==(const Region&) const = default;
|
||||
|
||||
private:
|
||||
Bool m_full = false;
|
||||
Vector<Rect> m_rects;
|
||||
};
|
||||
|
||||
// Per buffer of a rotating set: the damage each has missed since it was last written.
|
||||
class BufferDamageTracker {
|
||||
public:
|
||||
explicit BufferDamageTracker(SizeT buffers = 0) { Resize(buffers); }
|
||||
|
||||
void Resize(SizeT buffers) {
|
||||
m_missed.assign(buffers, Region::Full());
|
||||
}
|
||||
SizeT Size() const { return m_missed.size(); }
|
||||
|
||||
// The buffer's contents are unknown (new, reallocated, resized): its next write copies all.
|
||||
void Invalidate(SizeT index) {
|
||||
if (index < m_missed.size()) m_missed[index].SetFull();
|
||||
}
|
||||
void InvalidateAll() {
|
||||
for (Region& region : m_missed) region.SetFull();
|
||||
}
|
||||
|
||||
// A frame whose damage is `frame` is written into buffer `index` of a `width` x `height`
|
||||
// surface: the answer is what to copy into it - the frame's damage and everything the
|
||||
// buffer missed - and every other buffer now misses the frame's damage too.
|
||||
Region TakeForWrite(SizeT index, const Region& frame, Int32 width, Int32 height) {
|
||||
Region copy = Region::Full();
|
||||
if (index < m_missed.size()) {
|
||||
copy = std::move(m_missed[index]);
|
||||
copy.Unite(frame);
|
||||
copy.Normalize(width, height);
|
||||
m_missed[index].Clear();
|
||||
}
|
||||
for (SizeT i = 0; i < m_missed.size(); ++i) {
|
||||
if (i == index) continue;
|
||||
m_missed[i].Unite(frame);
|
||||
m_missed[i].Normalize(width, height);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
private:
|
||||
Vector<Region> m_missed;
|
||||
};
|
||||
|
||||
// EGL_EXT_buffer_age over the images of one swapchain.
|
||||
class SwapchainAgeTracker {
|
||||
public:
|
||||
// A new (or rebuilt) chain of `imageCount` images: every age is unknown.
|
||||
void Reset(SizeT imageCount) {
|
||||
m_presentedAt.assign(imageCount, 0);
|
||||
m_presents = 0;
|
||||
}
|
||||
// Image `index` was presented. `contentKept`: its content stays what was presented until
|
||||
// it is drawn into again (otherwise its next age is unknown).
|
||||
void OnPresented(SizeT index, Bool contentKept) {
|
||||
++m_presents;
|
||||
if (index < m_presentedAt.size()) m_presentedAt[index] = contentKept ? m_presents : 0;
|
||||
}
|
||||
// A frame was swapped without reaching any image: the counting no longer matches the
|
||||
// application's, so every age is unknown.
|
||||
void OnFrameDropped() { std::fill(m_presentedAt.begin(), m_presentedAt.end(), Uint64{0}); }
|
||||
// The age of image `index` as the next frame's target: 0 = unknown.
|
||||
Int32 AgeOf(SizeT index) const {
|
||||
if (index >= m_presentedAt.size() || m_presentedAt[index] == 0) return 0;
|
||||
const Uint64 age = m_presents - m_presentedAt[index] + 1;
|
||||
return age > static_cast<Uint64>(std::numeric_limits<Int32>::max()) ? 0 : static_cast<Int32>(age);
|
||||
}
|
||||
|
||||
private:
|
||||
Vector<Uint64> m_presentedAt; // the present count at which the image was last presented, 0 = none
|
||||
Uint64 m_presents = 0;
|
||||
};
|
||||
} // namespace MobileGL::MG_Util::Damage
|
||||
Reference in New Issue
Block a user