RenderCore/RCComputeRootLayout.mm (110 lines of code) (raw):
// (c) Facebook, Inc. and its affiliates. Confidential and proprietary.
#import "RCComputeRootLayout.h"
#import <unordered_map>
#import <RenderCore/CKInternalHelpers.h>
#import <RenderCore/CKMountable.h>
#import <RenderCore/CKSizeRange.h>
#import <RenderCore/RCLayout.h>
// Considers NaNs equal to each other (unlike CGSizeEqualToSize). This is important for the layout cache
// keys as identical keys that contain NaNs will be otherwise treated as different.
static bool sizesAreEqual(const CGSize &lhs, const CGSize &rhs)
{
return CKFloatsEqual(lhs.width, rhs.width) && CKFloatsEqual(lhs.height, rhs.height);
}
struct RCLayoutCacheKey {
CKSizeRange constrainingSize;
CGSize parentSize;
bool operator==(const RCLayoutCacheKey &other) const
{
return constrainingSize == other.constrainingSize && sizesAreEqual(parentSize, other.parentSize);
}
};
namespace std {
template <>
struct hash<RCLayoutCacheKey> {
size_t operator()(const RCLayoutCacheKey &key) const noexcept
{
return key.constrainingSize.hash();
}
};
}
/** A layout cache is really just an unordered_map, under the covers. */
struct RCLayoutCache {
std::unordered_map<id<CKMountable>, std::unordered_map<RCLayoutCacheKey, RCLayout>, RC::hash<id>> map;
};
thread_local const RCLayoutCache *currentLayoutReadCache;
thread_local RCLayoutCache *currentLayoutWriteCache;
/**
Recursively copies layout cache entries for the layout and all of its
children. This ensures that the layout cache has comprehensive coverage
of all component layouts, even when we get a cache hit.
*/
static void copyFromReadCacheToWriteCache(const RCLayout &layout,
const RCLayoutCache *readCache,
RCLayoutCache *writeCache)
{
const auto &matches = readCache->map.find(layout.component);
if (matches != readCache->map.end()) {
// Copy all entries in the readCache's map to the writeCache's map,
// skipping any that are already present.
writeCache->map[layout.component].insert(
matches->second.begin(),
matches->second.end()
);
}
if (layout.children) {
for (const auto &child : *layout.children) {
copyFromReadCacheToWriteCache(child.layout, readCache, writeCache);
}
}
}
RCLayout RCFetchOrComputeLayout(id<CKMountable> mountable,
const CKSizeRange &sizeRange,
CGSize parentSize,
RCLayout (*layoutFunction)(id<CKMountable> mountable, const CKSizeRange &sizeRange, CGSize parentSize))
{
const RCLayoutCacheKey key {sizeRange, parentSize};
if (currentLayoutWriteCache) {
const auto it = currentLayoutWriteCache->map.find(mountable);
if (it != currentLayoutWriteCache->map.end()) {
const auto match = it->second.find(key);
if (match != it->second.end()) {
return match->second;
}
}
}
if (currentLayoutReadCache) {
const auto it = currentLayoutReadCache->map.find(mountable);
if (it != currentLayoutReadCache->map.end()) {
const auto match = it->second.find(key);
if (match != it->second.end()) {
// Found a hit! Copy the cached layout for this node *and descendants*
// from the read cache to the write cache, if there is one.
if (currentLayoutWriteCache) {
copyFromReadCacheToWriteCache(
match->second,
currentLayoutReadCache,
currentLayoutWriteCache
);
}
return match->second;
}
}
}
const RCLayout layout = layoutFunction(mountable, sizeRange, parentSize);
if (currentLayoutWriteCache) {
currentLayoutWriteCache->map[mountable].emplace(std::make_pair(key, layout));
}
return layout;
}
RCLayoutResult RCComputeRootLayout(id<CKMountable> model,
const CKSizeRange &constrainingSize,
std::shared_ptr<RCLayoutCache> cache)
{
const auto writeCache = std::make_shared<RCLayoutCache>();
if (cache) {
// We expect the writeCache to have about as many elements as the readCache.
// Reserve the appropriate number of buckets now to avoid rehashing later.
writeCache->map.reserve(cache->map.size());
}
// We don't expect nested root layouts, so the thread-local caches should generally be null.
// But if a nested root layout *does* happen, we restore the previous caches before returning.
const RCLayoutCache *const previousReadCache = currentLayoutReadCache;
RCLayoutCache *const previousWriteCache = currentLayoutWriteCache;
currentLayoutReadCache = cache.get();
currentLayoutWriteCache = writeCache.get();
RCLayout layout = [model layoutThatFits:constrainingSize parentSize:constrainingSize.max];
currentLayoutReadCache = previousReadCache;
currentLayoutWriteCache = previousWriteCache;
return {
.layout = layout,
.cache = writeCache,
};
}
BOOL RCLayoutCacheContainsEntryForMountable(const RCLayoutCache &cache, id<CKMountable> mountable)
{
return cache.map.find(mountable) != cache.map.end();
}