blob: c522d50884bf723534bed01d420791fac4433504 [file] [log] [blame]
/*
* Copyright 2023 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/RasterPathAtlas.h"
#include "include/core/SkColorSpace.h"
#include "include/gpu/graphite/Recorder.h"
#include "src/core/SkBlitter_A8.h"
#include "src/core/SkDrawBase.h"
#include "src/core/SkIPoint16.h"
#include "src/core/SkRasterClip.h"
#include "src/gpu/graphite/AtlasProvider.h"
#include "src/gpu/graphite/DrawContext.h"
#include "src/gpu/graphite/Log.h"
#include "src/gpu/graphite/RecorderPriv.h"
#include "src/gpu/graphite/TextureProxy.h"
namespace skgpu::graphite {
namespace {
} // namespace
RasterPathAtlas::RasterPathAtlas()
: PathAtlas(kDefaultAtlasDim, kDefaultAtlasDim) {
// set up LRU list
Page* currPage = &fPageArray[0];
for (int i = 0; i < kMaxPages; ++i) {
fPageList.addToHead(currPage);
currPage->fIdentifier = i;
++currPage;
}
}
void RasterPathAtlas::recordUploads(DrawContext* dc, Recorder* recorder) {
// Cycle through all the pages and handle their uploads
PageList::Iter pageIter;
pageIter.init(fPageList, PageList::Iter::kHead_IterStart);
while (Page* currPage = pageIter.get()) {
// build an upload for the dirty rect and record it
if (!currPage->fDirtyRect.isEmpty()) {
size_t rowBytes = currPage->fPixels.rowBytes();
const uint8_t* dataPtr = (const uint8_t*) currPage->fPixels.addr();
dataPtr += rowBytes * currPage->fDirtyRect.fTop;
dataPtr += currPage->fPixels.info().bytesPerPixel() * currPage->fDirtyRect.fLeft;
std::vector<MipLevel> levels;
levels.push_back({dataPtr, rowBytes});
SkColorInfo colorInfo(kAlpha_8_SkColorType, kUnknown_SkAlphaType, nullptr);
if (!dc->recordUpload(recorder, currPage->fTexture, colorInfo, colorInfo, levels,
currPage->fDirtyRect, nullptr)) {
SKGPU_LOG_W("Coverage mask upload failed!");
return;
}
currPage->fDirtyRect.setEmpty();
}
pageIter.next();
}
this->reset();
}
bool RasterPathAtlas::Page::initializeTextureIfNeeded(Recorder* recorder, uint16_t identifier) {
if (!fTexture) {
AtlasProvider* atlasProvider = recorder->priv().atlasProvider();
fTexture = atlasProvider->getAtlasTexture(recorder,
fRectanizer.width(),
fRectanizer.height(),
kAlpha_8_SkColorType,
identifier,
/*requiresStorageUsage=*/false);
}
return fTexture != nullptr;
}
void RasterPathAtlas::makeMRU(Page* page) {
if (fPageList.head() == page) {
return;
}
fPageList.remove(page);
fPageList.addToHead(page);
}
const TextureProxy* RasterPathAtlas::addRect(Recorder* recorder,
skvx::float2 atlasSize,
SkIPoint16* outPos) {
// Look through all pages in MRU order and find the first one with room, and move that to MRU
PageList::Iter pageIter;
pageIter.init(fPageList, PageList::Iter::kHead_IterStart);
for (Page* currPage = pageIter.get(); currPage; currPage = pageIter.next()) {
if (!currPage->initializeTextureIfNeeded(recorder, currPage->fIdentifier)) {
SKGPU_LOG_E("Failed to instantiate an atlas texture");
return nullptr;
}
// An empty mask always fits, so just return the texture.
// TODO: This may not be needed if we can handle clipped out bounds with inverse fills
// another way. See PathAtlas::addShape().
if (!all(atlasSize)) {
return currPage->fTexture.get();
}
if (!currPage->fRectanizer.addRect(atlasSize.x(), atlasSize.y(), outPos)) {
continue;
}
this->makeMRU(currPage);
return currPage->fTexture.get();
}
// No room in any Page
return nullptr;
}
namespace {
skgpu::UniqueKey generate_key(const Shape& shape,
const Transform& transform,
const SkStrokeRec& strokeRec,
skvx::float2 atlasSize) {
skgpu::UniqueKey maskKey;
{
static const skgpu::UniqueKey::Domain kDomain = skgpu::UniqueKey::GenerateDomain();
skgpu::UniqueKey::Builder builder(&maskKey, kDomain, 7 + shape.keySize(),
"Raster Path Mask");
builder[0] = atlasSize.x();
builder[1] = atlasSize.y();
// We require the upper left 2x2 of the matrix to match exactly for a cache hit.
SkMatrix mat = transform.matrix().asM33();
SkScalar sx = mat.get(SkMatrix::kMScaleX);
SkScalar sy = mat.get(SkMatrix::kMScaleY);
SkScalar kx = mat.get(SkMatrix::kMSkewX);
SkScalar ky = mat.get(SkMatrix::kMSkewY);
#ifdef SK_BUILD_FOR_ANDROID_FRAMEWORK
// Fractional translate does not affect caching on Android. This is done for better cache
// hit ratio and speed and is matching HWUI behavior, which didn't consider the matrix
// at all when caching paths.
SkFixed fracX = 0;
SkFixed fracY = 0;
#else
SkScalar tx = mat.get(SkMatrix::kMTransX);
SkScalar ty = mat.get(SkMatrix::kMTransY);
// Allow 8 bits each in x and y of subpixel positioning.
SkFixed fracX = SkScalarToFixed(SkScalarFraction(tx)) & 0x0000FF00;
SkFixed fracY = SkScalarToFixed(SkScalarFraction(ty)) & 0x0000FF00;
#endif
builder[2] = SkFloat2Bits(sx);
builder[3] = SkFloat2Bits(sy);
builder[4] = SkFloat2Bits(kx);
builder[5] = SkFloat2Bits(ky);
// FracX and fracY are &ed with 0x0000ff00, so need to shift one down to fill 16 bits.
uint32_t fracBits = fracX | (fracY >> 8);
// Distinguish between hairline and filled paths. For hairlines, we also need to include
// the cap. (SW grows hairlines by 0.5 pixel with round and square caps). Note that
// stroke-and-fill of hairlines is turned into pure fill by SkStrokeRec, so this covers
// all cases we might see.
uint32_t styleBits = strokeRec.isHairlineStyle() ? ((strokeRec.getCap() << 1) | 1) : 0;
builder[6] = fracBits | (styleBits << 16);
shape.writeKey(&builder[7]);
}
return maskKey;
}
} // namespace
const TextureProxy* RasterPathAtlas::onAddShape(Recorder* recorder,
const Shape& shape,
const Transform& transform,
const SkStrokeRec& strokeRec,
skvx::float2 atlasSize,
skvx::int2 deviceOffset,
skvx::half2* outPos) {
skgpu::UniqueKey maskKey;
bool hasKey = shape.hasKey();
if (hasKey) {
// Iterate through pagelist in MRU order and see if this shape is cached
PageList::Iter pageIter;
pageIter.init(fPageList, PageList::Iter::kHead_IterStart);
maskKey = generate_key(shape, transform, strokeRec, atlasSize);
while (Page* currPage = pageIter.get()) {
// Look up shape and use cached texture and position if found.
skvx::half2* found = currPage->fCachedShapes.find(maskKey);
if (found) {
*outPos = *found;
this->makeMRU(currPage);
return currPage->fTexture.get();
}
pageIter.next();
}
}
// Try to add to Rectanizer
SkIPoint16 iPos;
const TextureProxy* texProxy = this->addRect(recorder, atlasSize, &iPos);
if (!texProxy) {
// Reset LRU Page
fPageList.tail()->fNeedsReset = true;
return nullptr;
}
*outPos = skvx::half2(iPos.x(), iPos.y());
// If the mask is empty, just return.
// TODO: This may not be needed if we can handle clipped out bounds with inverse fills
// another way. See PathAtlas::addShape().
if (!all(atlasSize)) {
return texProxy;
}
// Handle render
Page* mru = fPageList.head(); // set up by addRect()
SkASSERT(mru->fTexture.get() == texProxy);
// Allocate pixmap if needed
if (!mru->fPixels.addr()) {
const SkImageInfo bmImageInfo = SkImageInfo::MakeA8(mru->fRectanizer.width(),
mru->fRectanizer.height());
if (!mru->fPixels.tryAlloc(bmImageInfo)) {
return nullptr;
}
mru->fPixels.erase(0);
}
// Rasterize path to backing pixmap
// TODO: render in a separate thread?
SkDrawBase draw;
draw.fBlitterChooser = SkA8Blitter_Choose;
draw.fDst = mru->fPixels;
SkRasterClip rasterClip;
SkIRect iAtlasBounds = SkIRect::MakeXYWH(iPos.x(), iPos.y(),
atlasSize.x(), atlasSize.y());
rasterClip.setRect(iAtlasBounds);
draw.fRC = &rasterClip;
SkPaint paint;
paint.setBlendMode(SkBlendMode::kSrc); // "Replace" mode
paint.setAntiAlias(true);
// SkPaint's color is unpremul so this will produce alpha in every channel.
paint.setColor(SK_ColorWHITE);
strokeRec.applyToPaint(&paint);
SkMatrix translatedMatrix = SkMatrix(transform);
// The atlas transform of the shape is the linear-components (scale, rotation, skew) of
// `localToDevice` translated by the top-left offset of `atlasBounds`, accounting for the 1
// pixel-wide border we added earlier, so that the shape is correctly centered.
translatedMatrix.postTranslate(iAtlasBounds.x() + 1 - deviceOffset.x(),
iAtlasBounds.y() + 1 - deviceOffset.y());
draw.fCTM = &translatedMatrix;
SkPath path = shape.asPath();
if (path.isInverseFillType()) {
// The shader will handle the inverse fill in this case
path.toggleInverseFillType();
}
draw.drawPathCoverage(path, paint);
// Add atlasBounds to dirtyRect for later upload
mru->fDirtyRect.join(iAtlasBounds);
// Add to cache
if (hasKey) {
mru->fCachedShapes.set(maskKey, *outPos);
}
return texProxy;
}
void RasterPathAtlas::reset() {
// Only reset LRU Page if needed
Page* lru = fPageList.tail();
SkASSERT(lru);
if (lru->fNeedsReset) {
lru->fRectanizer.reset();
// clear backing data for next pass
SkASSERT(lru->fDirtyRect.isEmpty());
lru->fPixels.erase(0);
lru->fCachedShapes.reset();
lru->fNeedsReset = false;
}
}
} // namespace skgpu::graphite