mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Test] (Magma): descriptor pool growth and trim - the pure sizing policy adds O(log N) pools for an N-set frame, and DescriptorPoolGrowthScenario's 4096-distinct-set frame lands whole on both backends, grows at most 7 pools, and is given back after quiet frames and re-grown while they are still in flight
This commit is contained in:
@@ -129,6 +129,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/RenderbufferBlendFormatScenario.cpp
|
||||
Scenarios/DualSourceBlendScenario.cpp
|
||||
Scenarios/OversizedRecordingSplitScenario.cpp
|
||||
Scenarios/DescriptorPoolGrowthScenario.cpp
|
||||
)
|
||||
|
||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DescriptorPoolGrowthScenario.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
|
||||
//
|
||||
// Scenario - ONE FRAME THAT NEEDS THOUSANDS OF DISTINCT DESCRIPTOR SETS.
|
||||
//
|
||||
// DirectVulkan takes a draw's descriptor set from a per-frame-slot cache and adds a pool to the
|
||||
// slot when the cache runs dry (UniformManager). Two things went wrong with a burst like
|
||||
// Minecraft rd12's loading frame (15,016 sets in one slot):
|
||||
// - Growth was linear. The next pool was sized at twice the slot's FIRST pool of the flavour,
|
||||
// which is always its oldest and smallest, so every grown pool came out the same size: the
|
||||
// loading slot ended with 59 pools of 256 sets.
|
||||
// - The burst was kept forever. The cache only rewinds, so the slot held all 15k sets (and the
|
||||
// pools behind them) for the life of the process, while steady frames used a few dozen.
|
||||
// Now each pool doubles the slot's largest one (capped), and a slot whose pools stay under a
|
||||
// quarter full for MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES frames is handed back whole and re-grows.
|
||||
//
|
||||
// Each of kDraws draws paints its own pixel of a 64x64 target and samples one of kTextures
|
||||
// textures, a different one from each of the previous draws' so neither the renderer's per-draw
|
||||
// descriptor reuse memo (four entries) nor its dynamic-offset-only rebind can let two draws share
|
||||
// a set: one frame needs kDraws sets. Every pixel must hold its own draw's texture colour, on both
|
||||
// backends. On DirectVulkan the cases also read the pools through GetDescriptorPoolCensus - which
|
||||
// only the desktop build can reach, since the integration harness links the static library there
|
||||
// and the hidden-visibility shipping library on Android.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#if !defined(__ANDROID__)
|
||||
#include <MG_Backend/DirectVulkan/DescriptorPoolCensus.h>
|
||||
#endif
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
|
||||
constexpr int kSide = 64;
|
||||
constexpr int kDraws = kSide * kSide; // well under the 16384-draw command-buffer split budget
|
||||
// More than the descriptor reuse memo's four entries, so the texture a draw samples is never
|
||||
// one of the last four draws'.
|
||||
constexpr int kTextures = 8;
|
||||
// The base pool size (VulkanRenderer's kDescriptorSetsPerFrame) and the growth cap
|
||||
// (UniformManager::kMaxDescriptorPoolSets).
|
||||
constexpr std::uint64_t kBaseSets = 64;
|
||||
constexpr std::uint64_t kMaxPoolSets = 4096;
|
||||
|
||||
constexpr const char* kVS = R"(#version 330 core
|
||||
in vec2 aPos;
|
||||
uniform int uIndex;
|
||||
void main() {
|
||||
vec2 cell = vec2(float(uIndex % 64), float(uIndex / 64));
|
||||
gl_Position = vec4((cell + aPos) / 32.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
constexpr const char* kFS = R"(#version 330 core
|
||||
uniform sampler2D uTex;
|
||||
out vec4 o_color;
|
||||
void main() { o_color = texture(uTex, vec2(0.5)); }
|
||||
)";
|
||||
|
||||
// Texture t's single texel; every channel differs between any two textures.
|
||||
Rgba8 TextureColor(int t) {
|
||||
return Rgba8{static_cast<std::uint8_t>(16 + 32 * t), static_cast<std::uint8_t>(240 - 32 * t),
|
||||
static_cast<std::uint8_t>(8 + 29 * t), 255};
|
||||
}
|
||||
|
||||
std::string Describe(const Rgba8& c) {
|
||||
std::ostringstream os;
|
||||
os << "rgba(" << static_cast<int>(c.r) << ", " << static_cast<int>(c.g) << ", " << static_cast<int>(c.b)
|
||||
<< ", " << static_cast<int>(c.a) << ")";
|
||||
return os.str();
|
||||
}
|
||||
|
||||
unsigned long EnvUnsigned(const char* name, unsigned long fallback) {
|
||||
const char* value = std::getenv(name);
|
||||
if (value == nullptr || *value == '\0') return fallback;
|
||||
char* end = nullptr;
|
||||
const unsigned long parsed = std::strtoul(value, &end, 10);
|
||||
return (end != nullptr && *end == '\0') ? parsed : fallback;
|
||||
}
|
||||
|
||||
// Pools a geometric policy adds for n sets from an empty flavour: 64, 128, ... up to the cap,
|
||||
// then one per cap. The old first-pool rule needed n/128 or n/256 of them.
|
||||
std::uint64_t GeometricPoolBound(std::uint64_t n) {
|
||||
std::uint64_t pools = 0, capacity = 0, size = kBaseSets;
|
||||
while (capacity < n) {
|
||||
capacity += size;
|
||||
++pools;
|
||||
size = std::min(size * 2, kMaxPoolSets);
|
||||
}
|
||||
return pools;
|
||||
}
|
||||
|
||||
struct Census {
|
||||
bool available = false;
|
||||
std::uint64_t pools = 0;
|
||||
std::uint64_t capacitySets = 0;
|
||||
std::uint64_t cachedSets = 0;
|
||||
std::uint64_t slotTrims = 0;
|
||||
};
|
||||
|
||||
Census ReadCensus() {
|
||||
Census out;
|
||||
#if !defined(__ANDROID__)
|
||||
const auto census = MobileGL::MG_Backend::DirectVulkan::GetDescriptorPoolCensus();
|
||||
out.available = census.available;
|
||||
out.pools = census.pools;
|
||||
out.capacitySets = census.capacitySets;
|
||||
out.cachedSets = census.cachedSets;
|
||||
out.slotTrims = census.slotTrims;
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string Describe(const Census& c) {
|
||||
std::ostringstream os;
|
||||
os << "pools=" << c.pools << " capacity=" << c.capacitySets << " cachedSets=" << c.cachedSets
|
||||
<< " slotTrims=" << c.slotTrims;
|
||||
return os.str();
|
||||
}
|
||||
|
||||
::testing::AssertionResult EveryPixelHoldsItsDrawsTexture(const Image& image, const char* when) {
|
||||
int wrong = 0;
|
||||
int firstX = -1, firstY = -1;
|
||||
Rgba8 firstSeen{};
|
||||
for (int y = 0; y < kSide; ++y) {
|
||||
for (int x = 0; x < kSide; ++x) {
|
||||
const Rgba8 expected = TextureColor((y * kSide + x) % kTextures);
|
||||
const Rgba8 seen = image.At(x, y);
|
||||
if (seen != expected) {
|
||||
if (wrong == 0) {
|
||||
firstX = x;
|
||||
firstY = y;
|
||||
firstSeen = seen;
|
||||
}
|
||||
++wrong;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wrong == 0) return ::testing::AssertionSuccess();
|
||||
const int draw = firstY * kSide + firstX;
|
||||
return ::testing::AssertionFailure()
|
||||
<< when << ": " << wrong << " of " << kDraws << " pixels do not hold their own draw's texture. "
|
||||
<< "First: (" << firstX << ", " << firstY << ") = " << Describe(firstSeen) << ", drawn by draw "
|
||||
<< draw << " with texture " << (draw % kTextures) << " = "
|
||||
<< Describe(TextureColor(draw % kTextures)) << " (a pixel no draw reached holds 0,0,0,0)";
|
||||
}
|
||||
|
||||
class DescriptorPoolGrowthScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
static const float kVertices[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f};
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glBindVertexArray(m_vao);
|
||||
glGenBuffers(1, &m_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
|
||||
|
||||
m_textures.resize(kTextures);
|
||||
glGenTextures(kTextures, m_textures.data());
|
||||
for (int t = 0; t < kTextures; ++t) {
|
||||
const Rgba8 c = TextureColor(t);
|
||||
const std::uint8_t texel[4] = {c.r, c.g, c.b, c.a};
|
||||
glBindTexture(GL_TEXTURE_2D, m_textures[t]);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, texel);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (Ready()) {
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
BindDefaultFramebuffer();
|
||||
if (m_program != 0) glDeleteProgram(m_program);
|
||||
if (!m_textures.empty()) glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
|
||||
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||
DestroyColorFbo(m_target);
|
||||
}
|
||||
ScenarioTest::TearDown();
|
||||
}
|
||||
|
||||
::testing::AssertionResult Prepare() {
|
||||
std::string error;
|
||||
m_program = CompileProgram(kVS, kFS, &error);
|
||||
if (m_program == 0) return ::testing::AssertionFailure() << error;
|
||||
m_indexLocation = glGetUniformLocation(m_program, "uIndex");
|
||||
m_target = MakeColorFbo(kSide, kSide);
|
||||
if (m_target.fbo == 0) return ::testing::AssertionFailure() << "MakeColorFbo failed";
|
||||
glUseProgram(m_program);
|
||||
glUniform1i(glGetUniformLocation(m_program, "uTex"), 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindVertexArray(m_vao);
|
||||
BindFbo(m_target);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
return ::testing::AssertionSuccess();
|
||||
}
|
||||
|
||||
// Draw i paints pixel (i % 64, i / 64) with texture i % kTextures.
|
||||
void Draw(int i) {
|
||||
glBindTexture(GL_TEXTURE_2D, m_textures[i % kTextures]);
|
||||
glUniform1i(m_indexLocation, i);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
// The burst: every pixel, one draw and one descriptor set each.
|
||||
void DrawBurst() {
|
||||
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
for (int i = 0; i < kDraws; ++i) Draw(i);
|
||||
}
|
||||
|
||||
static bool IsVulkan() { return Gl().BackendName() == std::string("DirectVulkan"); }
|
||||
|
||||
GLuint m_vao = 0;
|
||||
GLuint m_vbo = 0;
|
||||
GLuint m_program = 0;
|
||||
GLint m_indexLocation = -1;
|
||||
std::vector<GLuint> m_textures;
|
||||
ColorFbo m_target;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// One frame, kDraws distinct sets. Every pixel lands, and the frame slot grows O(log N) pools
|
||||
// for it, not N / 128.
|
||||
TEST_F(DescriptorPoolGrowthScenario, AFrameOfDistinctSetsLandsWholeAndGrowsItsPoolsGeometrically) {
|
||||
if (!Ready()) return;
|
||||
ASSERT_TRUE(Prepare());
|
||||
|
||||
const Census before = ReadCensus();
|
||||
DrawBurst();
|
||||
const Image image = ReadPixels(kSide, kSide);
|
||||
const Census after = ReadCensus();
|
||||
|
||||
EXPECT_TRUE(EveryPixelHoldsItsDrawsTexture(image, "one frame of distinct descriptor sets"));
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
RecordProperty("census_checked", (IsVulkan() && after.available) ? "yes" : "no");
|
||||
if (!IsVulkan() || !after.available) return;
|
||||
// Falsifiability first: had the renderer's memos collapsed the draws onto shared sets, the
|
||||
// slot would never have needed to grow and the bound below would pass on any policy.
|
||||
ASSERT_GE(after.cachedSets, before.cachedSets + kDraws)
|
||||
<< "the frame did not allocate one descriptor set per draw, so it does not exercise growth. Before: "
|
||||
<< Describe(before) << "; after: " << Describe(after);
|
||||
const std::uint64_t added = after.pools - before.pools;
|
||||
RecordProperty("census_before", Describe(before));
|
||||
RecordProperty("census_after", Describe(after));
|
||||
EXPECT_LE(added, GeometricPoolBound(kDraws + 1))
|
||||
<< "a frame needing " << kDraws << " descriptor sets added " << added << " pools; doubling the slot's "
|
||||
<< "largest pool needs at most " << GeometricPoolBound(kDraws + 1) << ". Before: " << Describe(before)
|
||||
<< "; after: " << Describe(after);
|
||||
}
|
||||
|
||||
// Burst, then quiet frames until the renderer hands the burst back, then the burst again while
|
||||
// the quiet frames are still in flight. Nothing waits between frames except where noted, so the
|
||||
// trim runs with other frame slots' work outstanding.
|
||||
TEST_F(DescriptorPoolGrowthScenario, AGivenBackBurstLeavesNoStaleSetBehind) {
|
||||
if (!Ready()) return;
|
||||
ASSERT_TRUE(Prepare());
|
||||
const unsigned long trimFrames = EnvUnsigned("MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES", 120);
|
||||
const unsigned long framesInFlight = EnvUnsigned("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3);
|
||||
// The trim fires once trimFrames quiet frames have followed the burst, at the burst slot's next
|
||||
// turn; past this many frames it is not coming.
|
||||
const unsigned long quietFrameLimit = trimFrames + 2 * framesInFlight + 16;
|
||||
|
||||
DrawBurst();
|
||||
Gl().EndFrame();
|
||||
const Census afterBurst = ReadCensus();
|
||||
|
||||
// Quiet frames: one draw each, the same pixel and texture the burst gave draw 0.
|
||||
const bool observe = IsVulkan() && afterBurst.available && trimFrames != 0;
|
||||
unsigned long quietFrames = 0;
|
||||
Census afterQuiet = afterBurst;
|
||||
for (; quietFrames < quietFrameLimit; ++quietFrames) {
|
||||
Draw(0);
|
||||
Gl().EndFrame();
|
||||
if (observe) {
|
||||
afterQuiet = ReadCensus();
|
||||
if (afterQuiet.slotTrims > afterBurst.slotTrims) {
|
||||
++quietFrames;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The burst again, straight away. The slot it lands in re-grows from its base pool.
|
||||
DrawBurst();
|
||||
const Image image = ReadPixels(kSide, kSide);
|
||||
const Census afterSecondBurst = ReadCensus();
|
||||
|
||||
EXPECT_TRUE(EveryPixelHoldsItsDrawsTexture(image, "the burst drawn after the first one was given back"));
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
|
||||
RecordProperty("census_checked", observe ? "yes" : "no");
|
||||
if (!observe) return;
|
||||
RecordProperty("census_after_burst", Describe(afterBurst));
|
||||
RecordProperty("quiet_frames", std::to_string(quietFrames));
|
||||
RecordProperty("census_after_quiet", Describe(afterQuiet));
|
||||
RecordProperty("census_after_second_burst", Describe(afterSecondBurst));
|
||||
ASSERT_GE(afterBurst.cachedSets, static_cast<std::uint64_t>(kDraws))
|
||||
<< "the burst did not allocate one descriptor set per draw: " << Describe(afterBurst);
|
||||
EXPECT_GT(afterQuiet.slotTrims, afterBurst.slotTrims)
|
||||
<< quietFrames << " frames of one draw each followed a " << kDraws << "-set frame, but no frame slot was "
|
||||
<< "trimmed (MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES=" << trimFrames << "). After the burst: "
|
||||
<< Describe(afterBurst) << "; after the quiet frames: " << Describe(afterQuiet);
|
||||
EXPECT_LE(afterQuiet.cachedSets + kDraws / 2, afterBurst.cachedSets)
|
||||
<< "the trim did not give the burst's sets back. After the burst: " << Describe(afterBurst)
|
||||
<< "; after the quiet frames: " << Describe(afterQuiet);
|
||||
EXPECT_LT(afterQuiet.pools, afterBurst.pools)
|
||||
<< "the trim did not give the burst's pools back. After the burst: " << Describe(afterBurst)
|
||||
<< "; after the quiet frames: " << Describe(afterQuiet);
|
||||
EXPECT_GE(afterSecondBurst.cachedSets, afterQuiet.cachedSets + kDraws)
|
||||
<< "the second burst was not served by freshly allocated sets: " << Describe(afterSecondBurst);
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
@@ -3061,6 +3061,100 @@ TEST(DirectVulkanSanity, GraphicsSamplerFeedbackOnlyAliasesWritableOverlappingMi
|
||||
EXPECT_FALSE(UniformManager::SamplerOverlapsWritableImageSubresource(1, 3, 4, GL_WRITE_ONLY));
|
||||
}
|
||||
|
||||
// Magma's per-frame descriptor pools (UniformManager::NextDescriptorPoolMaxSets). The old rule
|
||||
// doubled the FIRST pool of the flavour, which is always the slot's oldest and smallest one, so
|
||||
// every grown pool came out the same size: rd12's loading frame (15,016 sets) grew one frame slot
|
||||
// to 59 update-after-bind pools of 256 sets. The policy must double the LARGEST pool of the
|
||||
// flavour, up to the cap.
|
||||
namespace {
|
||||
using MobileGL::MG_Backend::DirectVulkan::UniformManager;
|
||||
constexpr MobileGL::Uint32 kBaseSets = 64; // VulkanRenderer's kDescriptorSetsPerFrame
|
||||
|
||||
// A frame slot as GrowFrameDescriptorPool leaves it after a frame needing `needed` sets of one
|
||||
// flavour: the base pool (non-update-after-bind, never usable by the other flavour) plus the
|
||||
// policy's next pool, added until the flavour's pools hold `needed` sets.
|
||||
std::vector<UniformManager::DescriptorPoolBucket> GrowSlotUntil(MobileGL::Uint64 needed, bool updateAfterBind) {
|
||||
std::vector<UniformManager::DescriptorPoolBucket> pools{{VK_NULL_HANDLE, kBaseSets, 0, false}};
|
||||
const auto flavourCapacity = [&]() {
|
||||
MobileGL::Uint64 sum = 0;
|
||||
for (const auto& pool : pools) {
|
||||
if (pool.updateAfterBind == updateAfterBind) sum += pool.maxSets;
|
||||
}
|
||||
return sum;
|
||||
};
|
||||
while (flavourCapacity() < needed && pools.size() < 100000) {
|
||||
pools.push_back({VK_NULL_HANDLE, UniformManager::NextDescriptorPoolMaxSets(pools, updateAfterBind, kBaseSets),
|
||||
0, updateAfterBind});
|
||||
}
|
||||
return pools;
|
||||
}
|
||||
|
||||
// Pools a geometric policy needs for n sets: 64, 128, ... up to the cap, then one per cap.
|
||||
MobileGL::Uint64 GeometricPoolBound(MobileGL::Uint64 n) {
|
||||
MobileGL::Uint64 pools = 0, capacity = 0, size = kBaseSets;
|
||||
while (capacity < n) {
|
||||
capacity += size;
|
||||
++pools;
|
||||
size = std::min<MobileGL::Uint64>(size * 2, UniformManager::kMaxDescriptorPoolSets);
|
||||
}
|
||||
return pools;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST(DirectVulkanSanity, DescriptorPoolGrowthIsGeometricInTheSetsAFrameNeeds) {
|
||||
for (const bool updateAfterBind : {false, true}) {
|
||||
for (const MobileGL::Uint64 needed : {65ull, 1000ull, 4096ull, 8128ull, 15016ull, 40000ull}) {
|
||||
const auto pools = GrowSlotUntil(needed, updateAfterBind);
|
||||
MobileGL::Uint64 flavourPools = 0, capacity = 0;
|
||||
MobileGL::Uint32 previous = 0;
|
||||
for (const auto& pool : pools) {
|
||||
if (pool.updateAfterBind != updateAfterBind) continue;
|
||||
++flavourPools;
|
||||
capacity += pool.maxSets;
|
||||
EXPECT_LE(pool.maxSets, UniformManager::kMaxDescriptorPoolSets) << "one pool past the cap";
|
||||
EXPECT_GE(pool.maxSets, previous) << "a later pool is smaller than an earlier one";
|
||||
previous = pool.maxSets;
|
||||
}
|
||||
// O(log N) under the cap (8128 = 64 + ... + 4096 is where the cap is reached), then one
|
||||
// pool per 4096 sets. The first-match rule needed ~N/128 here: 118 pools for 15,016.
|
||||
EXPECT_LE(flavourPools, GeometricPoolBound(needed))
|
||||
<< needed << " sets (updateAfterBind=" << updateAfterBind << ") took " << flavourPools << " pools";
|
||||
EXPECT_GE(capacity, needed);
|
||||
// The slot does not over-reserve by more than the last pool it had to add.
|
||||
EXPECT_LT(capacity - needed, static_cast<MobileGL::Uint64>(previous));
|
||||
}
|
||||
}
|
||||
// rd12's loading frame, the case that motivated this: 9 pools in the slot, not 59.
|
||||
const auto rd12 = GrowSlotUntil(15016, true);
|
||||
EXPECT_EQ(rd12.size(), 1u + 9u);
|
||||
}
|
||||
|
||||
TEST(DirectVulkanSanity, DescriptorPoolGrowthDoublesTheLargestPoolOfTheSameFlavour) {
|
||||
using Bucket = UniformManager::DescriptorPoolBucket;
|
||||
// The largest pool decides, wherever it sits in the list - a trim or a flavour switch can
|
||||
// leave a small pool after a large one.
|
||||
EXPECT_EQ(UniformManager::NextDescriptorPoolMaxSets(
|
||||
{{VK_NULL_HANDLE, 64, 0, false}, {VK_NULL_HANDLE, 512, 0, false}, {VK_NULL_HANDLE, 128, 0, false}},
|
||||
false, kBaseSets),
|
||||
1024u);
|
||||
// Flavours never size each other: the base pool is non-update-after-bind, so the first
|
||||
// update-after-bind pool starts at the base size however large the other flavour grew.
|
||||
const std::vector<Bucket> mixed{{VK_NULL_HANDLE, 64, 0, false},
|
||||
{VK_NULL_HANDLE, 2048, 0, false},
|
||||
{VK_NULL_HANDLE, 64, 0, true},
|
||||
{VK_NULL_HANDLE, 128, 0, true}};
|
||||
EXPECT_EQ(UniformManager::NextDescriptorPoolMaxSets(mixed, true, kBaseSets), 256u);
|
||||
EXPECT_EQ(UniformManager::NextDescriptorPoolMaxSets(mixed, false, kBaseSets), 4096u);
|
||||
EXPECT_EQ(UniformManager::NextDescriptorPoolMaxSets({{VK_NULL_HANDLE, 64, 0, false}}, true, kBaseSets), 64u);
|
||||
// The cap holds, and the base size is a floor.
|
||||
EXPECT_EQ(UniformManager::NextDescriptorPoolMaxSets({{VK_NULL_HANDLE, 4096, 0, false}}, false, kBaseSets),
|
||||
UniformManager::kMaxDescriptorPoolSets);
|
||||
EXPECT_EQ(UniformManager::NextDescriptorPoolMaxSets({{VK_NULL_HANDLE, 3000, 0, false}}, false, kBaseSets),
|
||||
UniformManager::kMaxDescriptorPoolSets);
|
||||
EXPECT_EQ(UniformManager::NextDescriptorPoolMaxSets({{VK_NULL_HANDLE, 16, 0, false}}, false, kBaseSets), 64u);
|
||||
EXPECT_EQ(UniformManager::NextDescriptorPoolMaxSets({}, false, kBaseSets), 64u);
|
||||
}
|
||||
|
||||
// GL_MAX_COMBINED_*_UNIFORM_COMPONENTS is components + blocks * (blockSize / 4). The product was
|
||||
// formed in signed 32-bit, and a Vulkan host that reports a large VkPhysicalDeviceLimits::
|
||||
// maxUniformBufferRange (a Mali driver answers 0xFFFFFFFF, which the loader saturates to
|
||||
|
||||
Reference in New Issue
Block a user