* engine: add program cache This is another chunky change. The core of this change is to cache programs in MaterialCache according to a "specialization" (ProgramSpecialization) which is defined as the program cache ID (the same key used for the OpenGL binary blob cache), the variant, and the set of spec constants. As part of this change, a lot of the implementation details of shader compilation were refactored from Material to MaterialDefinition. The resulting flow is a lot cleaner and easier to reason about, since shader compilation is now a pure function of the MaterialDefinition + ProgramSpecialization. Since the global cache program lookups might take a bit of time to compute hashes, etc, I left the set of cached programs in Material as well, which kind of acts like an L1 cache. The effect is that prepareProgram() and getProgram() should be no slower than HEAD, even with the more complex caching requirements. I'm planning on writing a document about this (and all changes up until this point), but I'm being asked to work on higher priority things and I wanted to have this PR out for review in the meantime so it doesn't bitrot. * engine: fix unit tests * engine: fix spec constants intern pool memory leak * engine: address program cache comments * engine: address more program cache comments * engine: matdbg support for program cache * engine: reinstate descriptorLayout calls * engine: address bitrot * engine: add feature flag to disable program cache * engine: use material CRC32 for program cache The "cache ID" of a material is supposed to uniquely identify a shader program and all its variants. This is true to a certain extent, but does not account for the code generation that happens at runtime. Two materials may have "identical" shader programs, but due to each material's differing unique metadata, the final compiled programs may end up very different. Unfortunately, this means we cannot rely on the "cache ID" alone to determine a shader program's reusability. Ideally, we should hash this "cache ID" with the exact set of changes to each shader program so that we could reuse programs across materials. Instead, as a stopgap solution, use the material's CRC32 instead. * engine: fix double-free in program cache * engine: address comments * engine: assert_invariant empty material cache
155 lines
4.0 KiB
C++
155 lines
4.0 KiB
C++
/*
|
|
* Copyright (C) 2025 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 <gtest/gtest.h>
|
|
|
|
#include <utils/FixedCapacityVector.h>
|
|
#include <utils/InternPool.h>
|
|
#include <utils/Slice.h>
|
|
|
|
using namespace utils;
|
|
|
|
TEST(InternPoolTest, AcquireWithCopy) {
|
|
InternPool<int> pool;
|
|
|
|
FixedCapacityVector<int> value = { 1, 3, 3, 7 };
|
|
Slice<const int> interned = pool.acquire(value);
|
|
|
|
EXPECT_FALSE(pool.empty());
|
|
EXPECT_EQ(value.as_slice(), interned);
|
|
}
|
|
|
|
TEST(InternPoolTest, AcquireWithMove) {
|
|
InternPool<int> pool;
|
|
|
|
FixedCapacityVector<int> value = { 1, 3, 3, 7 };
|
|
FixedCapacityVector<int> copy = value;
|
|
const int* data = value.data();
|
|
Slice<const int> interned = pool.acquire(std::move(value));
|
|
|
|
EXPECT_FALSE(pool.empty());
|
|
EXPECT_EQ(copy.as_slice(), interned);
|
|
EXPECT_EQ(data, interned.data());
|
|
}
|
|
|
|
TEST(InternPoolTest, InternIsUnique) {
|
|
InternPool<int> pool;
|
|
|
|
FixedCapacityVector<int> value = { 1, 3, 3, 7 };
|
|
Slice<const int> interned1 = pool.acquire(value);
|
|
Slice<const int> interned2 = pool.acquire(value);
|
|
Slice<const int> interned3 = pool.acquire(value);
|
|
|
|
EXPECT_FALSE(pool.empty());
|
|
EXPECT_EQ(interned1.begin(), interned2.begin());
|
|
EXPECT_EQ(interned1.begin(), interned3.begin());
|
|
EXPECT_EQ(interned1.end(), interned2.end());
|
|
EXPECT_EQ(interned1.end(), interned3.end());
|
|
}
|
|
|
|
TEST(InternPoolTest, ReleaseByValue) {
|
|
InternPool<int> pool;
|
|
|
|
FixedCapacityVector<int> value = { 1, 3, 3, 7 };
|
|
Slice<const int> interned = pool.acquire(value);
|
|
|
|
EXPECT_FALSE(pool.empty());
|
|
|
|
pool.release(value);
|
|
|
|
EXPECT_TRUE(pool.empty());
|
|
}
|
|
|
|
TEST(InternPoolTest, ReleaseByInterned) {
|
|
InternPool<int> pool;
|
|
|
|
FixedCapacityVector<int> value = { 1, 3, 3, 7 };
|
|
Slice<const int> interned = pool.acquire(value);
|
|
|
|
EXPECT_FALSE(pool.empty());
|
|
|
|
pool.release(interned);
|
|
|
|
EXPECT_TRUE(pool.empty());
|
|
}
|
|
|
|
TEST(InternPoolTest, AcquireAndReleaseEmpty) {
|
|
InternPool<int> pool;
|
|
|
|
FixedCapacityVector<int> value = {};
|
|
Slice<const int> interned = pool.acquire(value);
|
|
|
|
EXPECT_TRUE(pool.empty());
|
|
EXPECT_EQ(interned.begin(), nullptr);
|
|
EXPECT_EQ(interned.end(), nullptr);
|
|
|
|
// Shouldn't crash to release an empty slice even if the pool is empty.
|
|
pool.release(value);
|
|
pool.release(interned);
|
|
}
|
|
|
|
TEST(InternPoolTest, AcquireAndReleaseManyEqual) {
|
|
InternPool<int> pool;
|
|
|
|
FixedCapacityVector<int> value = { 1, 3, 3, 7 };
|
|
pool.acquire(value);
|
|
pool.acquire(value);
|
|
pool.acquire(value);
|
|
|
|
EXPECT_FALSE(pool.empty());
|
|
|
|
pool.release(value);
|
|
EXPECT_FALSE(pool.empty());
|
|
pool.release(value);
|
|
EXPECT_FALSE(pool.empty());
|
|
pool.release(value);
|
|
EXPECT_TRUE(pool.empty());
|
|
|
|
#ifdef GTEST_HAS_DEATH_TEST
|
|
ASSERT_DEATH(pool.release(value), "");
|
|
#endif
|
|
}
|
|
|
|
TEST(InternPoolTest, AcquireAndReleaseManyDifferent) {
|
|
InternPool<int> pool;
|
|
|
|
FixedCapacityVector<int> value1 = { 1, 3, 3, 7 };
|
|
FixedCapacityVector<int> value2 = { 4, 2, 0 };
|
|
FixedCapacityVector<int> value3 = { 9999999 };
|
|
pool.acquire(value1);
|
|
pool.acquire(value2);
|
|
pool.acquire(value3);
|
|
|
|
EXPECT_FALSE(pool.empty());
|
|
|
|
pool.release(value1);
|
|
EXPECT_FALSE(pool.empty());
|
|
pool.release(value2);
|
|
EXPECT_FALSE(pool.empty());
|
|
pool.release(value3);
|
|
EXPECT_TRUE(pool.empty());
|
|
}
|
|
|
|
#ifdef GTEST_HAS_DEATH_TEST
|
|
TEST(InternPoolTest, PanicsIfReleaseMissing) {
|
|
InternPool<int> pool;
|
|
|
|
FixedCapacityVector<int> value = { 1, 3, 3, 7 };
|
|
|
|
ASSERT_DEATH(pool.release(value), "");
|
|
}
|
|
#endif // GTEST_HAS_DEATH_TEST
|