blob: 8175f862cd64e114a2967c5a155689e68647c33e [file] [log] [blame]
/*
* Copyright 2024 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/gpu/graphite/ClipAtlasManager.h"
#include "include/core/SkBitmap.h"
#include "include/gpu/graphite/Recorder.h"
#include "include/private/base/SkFixed.h"
#include "src/base/SkFloatBits.h"
#include "src/gpu/graphite/AtlasProvider.h"
#include "src/gpu/graphite/ProxyCache.h"
#include "src/gpu/graphite/RasterPathUtils.h"
#include "src/gpu/graphite/RecorderPriv.h"
#include "src/gpu/graphite/TextureProxy.h"
namespace skgpu::graphite {
static constexpr int kClipAtlasWidth = 2048;
static constexpr int kClipAtlasHeight = 2048;
ClipAtlasManager::ClipAtlasManager(Recorder* recorder)
: fRecorder(recorder)
, fPathKeyAtlasMgr(kClipAtlasWidth, kClipAtlasHeight,
/*plotWidth=*/kClipAtlasWidth/2, /*plotHeight=*/kClipAtlasHeight/2,
DrawAtlas::UseStorageTextures::kNo,
"PathKeyClipAtlas", recorder->priv().caps())
// Examining the results from the top 20 or so webpages, the SaveRecord keyed clips
// tend to be considerably smaller and rarer, so we use a smaller atlas here.
, fSaveRecordKeyAtlasMgr(kClipAtlasWidth/2, kClipAtlasHeight/2,
/*plotWidth=*/kClipAtlasWidth/2, /*plotHeight=*/kClipAtlasHeight/2,
DrawAtlas::UseStorageTextures::kNo,
"SaveRecordKeyClipAtlas", recorder->priv().caps()) {}
namespace {
// Needed to ensure that we have surrounding context, e.g. for inverse clips this would be solid.
constexpr int kEntryPadding = 1;
// Copied and modified from Ganesh ClipStack
void draw_to_sw_mask(RasterMaskHelper* helper,
const ClipStack::Element& e,
bool isFirstElement,
SkIRect drawBounds) {
// If the first element to draw is an intersect, we clear to 0 and will draw it directly with
// coverage 1 (subsequent intersect elements will be inverse-filled and draw 0 outside).
// If the first element to draw is a difference, we clear to 1, and in all cases we draw the
// difference element directly with coverage 0.
if (isFirstElement) {
helper->clear(e.fOp == SkClipOp::kIntersect ? 0x00 : 0xFF, drawBounds);
}
uint8_t alpha;
bool invert;
if (e.fOp == SkClipOp::kIntersect) {
// Intersect modifies pixels outside of its geometry. If this is the first element,
// we can draw directly with coverage 1 since we cleared to 0. Otherwise we draw the
// inverse-filled shape with 0 coverage to erase everything outside the element.
if (isFirstElement) {
alpha = 0xFF;
invert = false;
} else {
alpha = 0x00;
invert = true;
}
} else {
// For difference ops, can always just subtract the shape directly by drawing 0 coverage
SkASSERT(e.fOp == SkClipOp::kDifference);
alpha = 0x00;
invert = false;
}
// Draw the shape; based on how we've initialized the buffer and chosen alpha+invert,
// every element is drawn with the kReplace_Op
if (invert != e.fShape.inverted()) {
Shape inverted(e.fShape);
inverted.setInverted(invert);
helper->drawClip(inverted, e.fLocalToDevice, alpha, drawBounds);
} else {
helper->drawClip(e.fShape, e.fLocalToDevice, alpha, drawBounds);
}
}
void draw_clip_mask_to_pixmap(const ClipStack::ElementList* elementList,
SkIRect maskDeviceBounds,
SkISize renderSize,
SkIRect drawBounds,
SkAutoPixmapStorage* dst) {
// The shape bounds are expanded by kEntryPadding so we need to take that into account here.
SkIVector transformedMaskOffset = {maskDeviceBounds.left() - kEntryPadding,
maskDeviceBounds.top() - kEntryPadding};
RasterMaskHelper helper(dst);
if (!helper.init(renderSize, transformedMaskOffset)) {
return;
}
SkASSERT(!elementList->empty());
for (int i = 0; i < elementList->size(); ++i) {
draw_to_sw_mask(&helper, *(*elementList)[i], i == 0, drawBounds);
}
}
} // anonymous namespace
sk_sp<TextureProxy> ClipAtlasManager::findOrCreateEntry(uint32_t stackRecordID,
const ClipStack::ElementList* elementList,
SkIRect maskDeviceBounds,
SkIPoint* outPos) {
// For the ClipAtlas cache, we don't include the bounds in the key
skgpu::UniqueKey maskKey;
bool usesPathKey;
// The keyBounds are the maskDeviceBounds relative to the full transformed mask. We use this
// to ensure we capture the situation where the maskDeviceBounds are equal in two cases but
// actually enclose different regions of the full mask due to a difference in integer
// translation (which is not captured in the key) in the element transforms.
SkIRect keyBounds;
maskKey = GenerateClipMaskKey(stackRecordID, elementList, maskDeviceBounds,
/*includeBounds=*/false, &keyBounds, &usesPathKey);
sk_sp<TextureProxy> atlasProxy;
if (usesPathKey) {
atlasProxy = fPathKeyAtlasMgr.findOrCreateEntry(fRecorder, maskKey, elementList,
maskDeviceBounds, keyBounds, outPos);
} else {
atlasProxy = fSaveRecordKeyAtlasMgr.findOrCreateEntry(fRecorder, maskKey, elementList,
maskDeviceBounds, keyBounds, outPos);
}
if (atlasProxy) {
return atlasProxy;
}
// We need to include the bounds in the key when using the ProxyCache
maskKey = GenerateClipMaskKey(stackRecordID, elementList, maskDeviceBounds,
/*includeBounds=*/true, &keyBounds, &usesPathKey);
// Bounds relative to the bitmap origin
// Expanded to include padding as well (so we clear correctly for inverse clip)
SkIRect drawBounds = SkIRect::MakeXYWH(0, 0,
maskDeviceBounds.width() + 2*kEntryPadding,
maskDeviceBounds.height() + 2*kEntryPadding);
const struct ClipDrawContext {
const ClipStack::ElementList* fElementList;
SkIRect fMaskDeviceBounds;
SkIRect fDrawBounds;
} context = { elementList, maskDeviceBounds, drawBounds };
sk_sp<TextureProxy> proxy = fRecorder->priv().proxyCache()->findOrCreateCachedProxy(
fRecorder, maskKey, &context,
[](const void* ctx) {
const ClipDrawContext* cdc = static_cast<const ClipDrawContext*>(ctx);
SkAutoPixmapStorage dst;
draw_clip_mask_to_pixmap(cdc->fElementList, cdc->fMaskDeviceBounds,
cdc->fDrawBounds.size(), cdc->fDrawBounds, &dst);
SkBitmap bm;
// SkPixmap::detachPixels() clears these so we need to make a copy.
SkImageInfo ii = dst.info();
size_t rowBytes = dst.rowBytes();
// The bitmap needs to take ownership of the pixels, so we detach them from the
// SkAutoPixmapStorage and pass them to SkBitmap::installPixels().
SkAssertResult(bm.installPixels(ii, dst.detachPixels(), rowBytes,
[](void* addr, void* context) { sk_free(addr); },
nullptr));
bm.setImmutable();
return bm;
});
*outPos = { kEntryPadding, kEntryPadding };
return proxy;
}
bool ClipAtlasManager::recordUploads(DrawContext* dc) {
return fPathKeyAtlasMgr.recordUploads(dc, fRecorder) ||
fSaveRecordKeyAtlasMgr.recordUploads(dc, fRecorder);
}
void ClipAtlasManager::compact() {
fPathKeyAtlasMgr.compact(fRecorder);
fSaveRecordKeyAtlasMgr.compact(fRecorder);
}
void ClipAtlasManager::freeGpuResources() {
fPathKeyAtlasMgr.freeGpuResources(fRecorder);
fSaveRecordKeyAtlasMgr.freeGpuResources(fRecorder);
}
void ClipAtlasManager::evictAtlases() {
fPathKeyAtlasMgr.evictAll();
fSaveRecordKeyAtlasMgr.evictAll();
}
//////////////////////////////////////////////////////////////////////////////////////////////
ClipAtlasManager::DrawAtlasMgr::DrawAtlasMgr(size_t width, size_t height,
size_t plotWidth, size_t plotHeight,
DrawAtlas::UseStorageTextures useStorageTextures,
std::string_view label, const Caps* caps) {
static constexpr SkColorType kColorType = kAlpha_8_SkColorType;
fDrawAtlas = DrawAtlas::Make(kColorType,
SkColorTypeBytesPerPixel(kColorType),
width, height,
plotWidth, plotHeight,
/*generationCounter=*/this,
caps->allowMultipleAtlasTextures() ?
DrawAtlas::AllowMultitexturing::kYes :
DrawAtlas::AllowMultitexturing::kNo,
useStorageTextures,
/*evictor=*/this,
label);
SkASSERT(fDrawAtlas);
fKeyLists.resize(fDrawAtlas->numPlots() * fDrawAtlas->maxPages());
for (int i = 0; i < fKeyLists.size(); ++i) {
fKeyLists[i].reset();
}
}
sk_sp<TextureProxy> ClipAtlasManager::DrawAtlasMgr::findOrCreateEntry(
Recorder* recorder,
const skgpu::UniqueKey& maskKey,
const ClipStack::ElementList* elementList,
SkIRect maskDeviceBounds,
SkIRect keyBounds,
SkIPoint* outPos) {
MaskHashEntry* entry = fMaskCache.find(maskKey);
while (entry) {
// If this entry is large enough to contain the clip, use it
if (entry->fBounds.contains(keyBounds)) {
SkIPoint topLeft = entry->fLocator.topLeft();
// We need to adjust the returned outPos to reflect the subset we're using
SkIPoint subsetRelativePos = keyBounds.topLeft() - entry->fBounds.topLeft();
*outPos = SkIPoint::Make(topLeft.x() + kEntryPadding + subsetRelativePos.x(),
topLeft.y() + kEntryPadding + subsetRelativePos.y());
fDrawAtlas->setLastUseToken(entry->fLocator,
recorder->priv().tokenTracker()->nextFlushToken());
return fDrawAtlas->getProxies()[entry->fLocator.pageIndex()];
}
entry = entry->fNext;
}
AtlasLocator locator;
sk_sp<TextureProxy> proxy = this->addToAtlas(recorder, elementList, maskDeviceBounds, outPos,
&locator);
if (!proxy) {
return nullptr;
}
// Look up again (in case this entry got purged during addToAtlas())
MaskHashEntry* entryList = fMaskCache.find(maskKey);
// Add locator and bounds to MaskCache.
if (entryList) {
// Add new list entry to the end. This will sort them from smallest bounds to largest,
// so that when we search above we'll pick the one with the smallest bounds that contains
// the clip.
MaskHashEntry* currEntry = entryList;
while (currEntry->fNext) {
currEntry = currEntry->fNext;
}
SkASSERT(currEntry);
SkASSERT(currEntry->fNext == nullptr); // Should be at the end
currEntry->fNext = new MaskHashEntry{keyBounds, locator, nullptr};
++fHashEntryCount;
} else {
MaskHashEntry newEntry{keyBounds, locator, nullptr};
fMaskCache.set(maskKey, newEntry);
++fHashEntryCount;
}
// Add key to Plot's MaskKeyList.
uint32_t index = fDrawAtlas->getListIndex(locator.plotLocator());
MaskKeyEntry* keyEntry = new MaskKeyEntry{maskKey, keyBounds};
fKeyLists[index].addToTail(keyEntry);
++fListEntryCount;
SkASSERTF_RELEASE(fHashEntryCount == fListEntryCount,
"=ClipAtlas=: Entry counts don't match after add: %d %d",
fHashEntryCount, fListEntryCount);
return proxy;
}
sk_sp<TextureProxy> ClipAtlasManager::DrawAtlasMgr::addToAtlas(
Recorder* recorder,
const ClipStack::ElementList* elementsForMask,
SkIRect maskDeviceBounds,
SkIPoint* outPos,
AtlasLocator* locator) {
// Render mask.
SkISize maskSize = maskDeviceBounds.size();
if (maskSize.isEmpty()) {
return nullptr;
}
// Expand to include padding as well (so we clear correctly for inverse clip)
maskSize.fWidth += 2*kEntryPadding;
maskSize.fHeight += 2*kEntryPadding;
// Request space in DrawAtlas, including padding
DrawAtlas::ErrorCode errorCode = fDrawAtlas->addRect(recorder,
maskSize.width(),
maskSize.height(),
locator);
if (errorCode != DrawAtlas::ErrorCode::kSucceeded) {
return nullptr;
}
// Rasterize path to backing pixmap.
// This pixmap will be the size of the Plot that contains the given rect, not the entire atlas,
// and hence the position we render at will be relative to that Plot.
// The value of outPos is relative to the entire texture, to be used for texture coords.
SkAutoPixmapStorage dst;
SkIPoint renderPos = fDrawAtlas->prepForRender(*locator, &dst);
// Bounds relative to the AtlasLocator
SkIRect drawBounds = SkIRect::MakeXYWH(0, 0, maskSize.width(), maskSize.height());
// Offset bounds to plot location for draw
drawBounds.offset(renderPos.x(), renderPos.y());
draw_clip_mask_to_pixmap(elementsForMask, maskDeviceBounds, fDrawAtlas->plotSize(),
drawBounds, &dst);
SkIPoint topLeft = locator->topLeft();
*outPos = SkIPoint::Make(topLeft.x() + kEntryPadding, topLeft.y() + kEntryPadding);
fDrawAtlas->setLastUseToken(*locator,
recorder->priv().tokenTracker()->nextFlushToken());
return fDrawAtlas->getProxies()[locator->pageIndex()];
}
bool ClipAtlasManager::DrawAtlasMgr::recordUploads(DrawContext* dc, Recorder* recorder) {
return (fDrawAtlas && !fDrawAtlas->recordUploads(dc, recorder));
}
void ClipAtlasManager::DrawAtlasMgr::evict(PlotLocator plotLocator) {
// Remove all entries for this Plot from the MaskCache
uint32_t index = fDrawAtlas->getListIndex(plotLocator);
MaskKeyList::Iter iter;
iter.init(fKeyLists[index], MaskKeyList::Iter::kHead_IterStart);
MaskKeyEntry* currKeyEntry;
while ((currKeyEntry = iter.get())) {
iter.next();
MaskHashEntry* currHashEntry = fMaskCache.find(currKeyEntry->fKey);
SkASSERT(currHashEntry);
MaskHashEntry* prevHashEntry = nullptr;
while (currHashEntry) {
if (currHashEntry->fBounds == currKeyEntry->fBounds) {
// Remove entry from hash list
if (prevHashEntry) {
prevHashEntry->fNext = currHashEntry->fNext;
delete currHashEntry;
--fHashEntryCount;
} else if (currHashEntry->fNext) {
MaskHashEntry* next = currHashEntry->fNext;
currHashEntry->fBounds = next->fBounds;
currHashEntry->fLocator = next->fLocator;
currHashEntry->fNext = next->fNext;
delete next;
--fHashEntryCount;
} else {
// Remove hash entry itself
fMaskCache.remove(currKeyEntry->fKey);
--fHashEntryCount;
}
break;
}
prevHashEntry = currHashEntry;
currHashEntry = currHashEntry->fNext;
}
fKeyLists[index].remove(currKeyEntry);
delete currKeyEntry;
--fListEntryCount;
SkASSERTF_RELEASE(fHashEntryCount == fListEntryCount,
"=ClipAtlas=: Entry counts don't match after delete: %d %d",
fHashEntryCount, fListEntryCount);
}
}
void ClipAtlasManager::DrawAtlasMgr::evictAll() {
fDrawAtlas->evictAllPlots();
SkASSERT(fMaskCache.empty());
}
void ClipAtlasManager::DrawAtlasMgr::compact(Recorder* recorder) {
auto tokenTracker = recorder->priv().tokenTracker();
if (fDrawAtlas) {
fDrawAtlas->compact(tokenTracker->nextFlushToken());
}
}
void ClipAtlasManager::DrawAtlasMgr::freeGpuResources(Recorder* recorder) {
auto tokenTracker = recorder->priv().tokenTracker();
if (fDrawAtlas) {
fDrawAtlas->freeGpuResources(tokenTracker->nextFlushToken());
}
}
} // namespace skgpu::graphite