diff --git a/gn/graphite.gni b/gn/graphite.gni index 084b4757b7..adf3914189 100644 --- a/gn/graphite.gni +++ b/gn/graphite.gni @@ -39,6 +39,8 @@ skia_graphite_sources = [ "$_src/CopyTask.h", "$_src/Device.cpp", "$_src/Device.h", + "$_src/DrawAtlas.cpp", + "$_src/DrawAtlas.h", "$_src/DrawBufferManager.cpp", "$_src/DrawBufferManager.h", "$_src/DrawContext.cpp", @@ -127,6 +129,8 @@ skia_graphite_sources = [ "$_src/render/TessellateStrokesRenderStep.h", "$_src/render/TessellateWedgesRenderStep.cpp", "$_src/render/TessellateWedgesRenderStep.h", + "$_src/text/AtlasManager.cpp", + "$_src/text/AtlasManager.h", ] skia_graphite_mtl_sources = [ diff --git a/include/gpu/graphite/Recorder.h b/include/gpu/graphite/Recorder.h index bf7ada1bd5..e2b02dcc49 100644 --- a/include/gpu/graphite/Recorder.h +++ b/include/gpu/graphite/Recorder.h @@ -17,8 +17,13 @@ class SkTextureDataBlock; class SkUniformDataBlock; class SkUniformDataBlockPassThrough; // TODO: remove +namespace skgpu { class TokenTracker; } + +namespace sktext::gpu { class StrikeCache; } + namespace skgpu::graphite { +class AtlasManager; class Caps; class Device; class DrawBufferManager; @@ -93,6 +98,10 @@ private: std::unique_ptr fUploadBufferManager; std::vector fTrackedDevices; + std::unique_ptr fAtlasManager; + std::unique_ptr fTokenTracker; + std::unique_ptr fStrikeCache; + // In debug builds we guard against improper thread handling // This guard is passed to the ResourceCache. // TODO: Should we also pass this to Device, DrawContext, and similar classes? diff --git a/src/gpu/AtlasTypes.h b/src/gpu/AtlasTypes.h index 90e48909d9..2dab56b6c4 100644 --- a/src/gpu/AtlasTypes.h +++ b/src/gpu/AtlasTypes.h @@ -454,6 +454,7 @@ public: void resetFlushesSinceLastUsed() { fFlushesSinceLastUse = 0; } void incFlushesSinceLastUsed() { fFlushesSinceLastUse++; } + bool needsUpload() { return !fDirtyRect.isEmpty(); } std::pair prepareForUpload(); void resetRects(); diff --git a/src/gpu/graphite/DrawAtlas.cpp b/src/gpu/graphite/DrawAtlas.cpp new file mode 100644 index 0000000000..c603b6678e --- /dev/null +++ b/src/gpu/graphite/DrawAtlas.cpp @@ -0,0 +1,502 @@ +/* + * Copyright 2022 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/DrawAtlas.h" + +#include + +#include "include/gpu/graphite/Recorder.h" +#include "include/private/SkTPin.h" +#include "src/core/SkMathPriv.h" +#include "src/core/SkOpts.h" +#include "src/core/SkTraceEvent.h" +#include "src/gpu/AtlasTypes.h" +#include "src/gpu/graphite/Caps.h" +#include "src/gpu/graphite/CommandTypes.h" +#include "src/gpu/graphite/DrawContext.h" +#include "src/gpu/graphite/RecorderPriv.h" +#include "src/gpu/graphite/TextureProxy.h" + +namespace skgpu::graphite { + +#if defined(DUMP_ATLAS_DATA) +static const constexpr bool kDumpAtlasData = true; +#else +static const constexpr bool kDumpAtlasData = false; +#endif + +#ifdef SK_DEBUG +void DrawAtlas::validate(const AtlasLocator& atlasLocator) const { + // Verify that the plotIndex stored in the PlotLocator is consistent with the glyph rectangle + int numPlotsX = fTextureWidth / fPlotWidth; + int numPlotsY = fTextureHeight / fPlotHeight; + + int plotIndex = atlasLocator.plotIndex(); + auto topLeft = atlasLocator.topLeft(); + int plotX = topLeft.x() / fPlotWidth; + int plotY = topLeft.y() / fPlotHeight; + SkASSERT(plotIndex == (numPlotsY - plotY - 1) * numPlotsX + (numPlotsX - plotX - 1)); +} +#endif + +std::unique_ptr DrawAtlas::Make(SkColorType colorType, size_t bpp, int width, + int height, int plotWidth, int plotHeight, + AtlasGenerationCounter* generationCounter, + AllowMultitexturing allowMultitexturing, + PlotEvictionCallback* evictor, + std::string_view label) { + std::unique_ptr atlas(new DrawAtlas(colorType, bpp, width, height, + plotWidth, plotHeight, generationCounter, + allowMultitexturing, label)); + + if (evictor != nullptr) { + atlas->fEvictionCallbacks.emplace_back(evictor); + } + return atlas; +} + +/////////////////////////////////////////////////////////////////////////////// + +DrawAtlas::DrawAtlas(SkColorType colorType, size_t bpp, int width, int height, + int plotWidth, int plotHeight, AtlasGenerationCounter* generationCounter, + AllowMultitexturing allowMultitexturing, std::string_view label) + : fColorType(colorType) + , fBytesPerPixel(bpp) + , fTextureWidth(width) + , fTextureHeight(height) + , fPlotWidth(plotWidth) + , fPlotHeight(plotHeight) + , fLabel(label) + , fGenerationCounter(generationCounter) + , fAtlasGeneration(fGenerationCounter->next()) + , fPrevFlushToken(DrawToken::AlreadyFlushedToken()) + , fFlushesSinceLastUse(0) + , fMaxPages(AllowMultitexturing::kYes == allowMultitexturing ? + PlotLocator::kMaxMultitexturePages : 1) + , fNumActivePages(0) { + int numPlotsX = width/plotWidth; + int numPlotsY = height/plotHeight; + SkASSERT(numPlotsX * numPlotsY <= PlotLocator::kMaxPlots); + SkASSERT(fPlotWidth * numPlotsX == fTextureWidth); + SkASSERT(fPlotHeight * numPlotsY == fTextureHeight); + + fNumPlots = numPlotsX * numPlotsY; + + this->createPages(generationCounter); +} + +inline void DrawAtlas::processEviction(PlotLocator plotLocator) { + for (PlotEvictionCallback* evictor : fEvictionCallbacks) { + evictor->evict(plotLocator); + } + + fAtlasGeneration = fGenerationCounter->next(); +} + +inline bool DrawAtlas::updatePlot(AtlasLocator* atlasLocator, Plot* plot) { + int pageIdx = plot->pageIndex(); + this->makeMRU(plot, pageIdx); + + // The actual upload will be created in recordUploads(). + + atlasLocator->updatePlotLocator(plot->plotLocator()); + SkDEBUGCODE(this->validate(*atlasLocator);) + return true; +} + +bool DrawAtlas::addToPage(unsigned int pageIdx, int width, int height, const void* image, + AtlasLocator* atlasLocator) { + SkASSERT(fProxies[pageIdx]); + + // look through all allocated plots for one we can share, in Most Recently Refed order + PlotList::Iter plotIter; + plotIter.init(fPages[pageIdx].fPlotList, PlotList::Iter::kHead_IterStart); + + for (Plot* plot = plotIter.get(); plot; plot = plotIter.next()) { + if (plot->addSubImage(width, height, image, atlasLocator)) { + return this->updatePlot(atlasLocator, plot); + } + } + + return false; +} + +bool DrawAtlas::recordUploads(DrawContext* dc, Recorder* recorder) { + TRACE_EVENT0("skia.gpu", TRACE_FUNC); + for (uint32_t pageIdx = 0; pageIdx < fNumActivePages; ++pageIdx) { + PlotList::Iter plotIter; + plotIter.init(fPages[pageIdx].fPlotList, PlotList::Iter::kHead_IterStart); + for (Plot* plot = plotIter.get(); plot; plot = plotIter.next()) { + if (plot->needsUpload()) { + TextureProxy* proxy = fProxies[pageIdx].get(); + SkASSERT(proxy); + + const void* dataPtr; + SkIRect dstRect; + std::tie(dataPtr, dstRect) = plot->prepareForUpload(); + + std::vector levels; + levels.push_back({dataPtr, fBytesPerPixel*fPlotWidth}); + + if (!dc->recordUpload(recorder, sk_ref_sp(proxy), fColorType, levels, dstRect)) { + return false; + } + } + } + } + return true; +} + +// Number of atlas-related flushes beyond which we consider a plot to no longer be in use. +// +// This value is somewhat arbitrary -- the idea is to keep it low enough that +// a page with unused plots will get removed reasonably quickly, but allow it +// to hang around for a bit in case it's needed. The assumption is that flushes +// are rare; i.e., we are not continually refreshing the frame. +static constexpr auto kPlotRecentlyUsedCount = 32; +static constexpr auto kAtlasRecentlyUsedCount = 128; + +DrawAtlas::ErrorCode DrawAtlas::addToAtlas(Recorder* recorder, + int width, int height, const void* image, + AtlasLocator* atlasLocator) { + if (width > fPlotWidth || height > fPlotHeight) { + return ErrorCode::kError; + } + + // Look through each page to see if we can upload without having to flush + // We prioritize this upload to the first pages, not the most recently used, to make it easier + // to remove unused pages in reverse page order. + for (unsigned int pageIdx = 0; pageIdx < fNumActivePages; ++pageIdx) { + if (this->addToPage(pageIdx, width, height, image, atlasLocator)) { + return ErrorCode::kSucceeded; + } + } + + // If the above fails, then see if the least recently used plot per page has already been + // queued for upload if we're at max page allocation, or if the plot has aged out otherwise. + // We wait until we've grown to the full number of pages to begin evicting already queued + // plots so that we can maximize the opportunity for reuse. + // As before we prioritize this upload to the first pages, not the most recently used. + if (fNumActivePages == this->maxPages()) { + for (unsigned int pageIdx = 0; pageIdx < fNumActivePages; ++pageIdx) { + Plot* plot = fPages[pageIdx].fPlotList.tail(); + SkASSERT(plot); + if (plot->lastUseToken() < recorder->priv().tokenTracker()->nextTokenToFlush()) { + this->processEvictionAndResetRects(plot); + SkDEBUGCODE(bool verify = )plot->addSubImage(width, height, image, atlasLocator); + SkASSERT(verify); + if (!this->updatePlot(atlasLocator, plot)) { + return ErrorCode::kError; + } + return ErrorCode::kSucceeded; + } + } + } else { + // If we haven't activated all the available pages, try to create a new one and add to it + if (!this->activateNewPage(recorder)) { + return ErrorCode::kError; + } + + if (this->addToPage(fNumActivePages-1, width, height, image, atlasLocator)) { + return ErrorCode::kSucceeded; + } else { + // If we fail to upload to a newly activated page then something has gone terribly + // wrong - return an error + return ErrorCode::kError; + } + } + + if (!fNumActivePages) { + return ErrorCode::kError; + } + + // All plots are currently in use by the current set of draws, so we need to fail. This + // gives the Device a chance to snap the current set of uploads and draws, advance the draw + // token, and call back into this function. The subsequent call will have plots available + // for fresh uploads. + return ErrorCode::kTryAgain; +} + +void DrawAtlas::compact(DrawToken startTokenForNextFlush) { + if (fNumActivePages < 1) { + fPrevFlushToken = startTokenForNextFlush; + return; + } + + // For all plots, reset number of flushes since used if used this frame. + PlotList::Iter plotIter; + bool atlasUsedThisFlush = false; + for (uint32_t pageIndex = 0; pageIndex < fNumActivePages; ++pageIndex) { + plotIter.init(fPages[pageIndex].fPlotList, PlotList::Iter::kHead_IterStart); + while (Plot* plot = plotIter.get()) { + // Reset number of flushes since used + if (plot->lastUseToken().inInterval(fPrevFlushToken, startTokenForNextFlush)) { + plot->resetFlushesSinceLastUsed(); + atlasUsedThisFlush = true; + } + + plotIter.next(); + } + } + + if (atlasUsedThisFlush) { + fFlushesSinceLastUse = 0; + } else { + ++fFlushesSinceLastUse; + } + + // We only try to compact if the atlas was used in the recently completed flush or + // hasn't been used in a long time. + // This is to handle the case where a lot of text or path rendering has occurred but then just + // a blinking cursor is drawn. + if (atlasUsedThisFlush || fFlushesSinceLastUse > kAtlasRecentlyUsedCount) { + SkTArray availablePlots; + uint32_t lastPageIndex = fNumActivePages - 1; + + // For all plots but the last one, update number of flushes since used, and check to see + // if there are any in the first pages that the last page can safely upload to. + for (uint32_t pageIndex = 0; pageIndex < lastPageIndex; ++pageIndex) { + if constexpr (kDumpAtlasData) { + SkDebugf("page %d: ", pageIndex); + } + + plotIter.init(fPages[pageIndex].fPlotList, PlotList::Iter::kHead_IterStart); + while (Plot* plot = plotIter.get()) { + // Update number of flushes since plot was last used + // We only increment the 'sinceLastUsed' count for flushes where the atlas was used + // to avoid deleting everything when we return to text drawing in the blinking + // cursor case + if (!plot->lastUseToken().inInterval(fPrevFlushToken, startTokenForNextFlush)) { + plot->incFlushesSinceLastUsed(); + } + + if constexpr (kDumpAtlasData) { + SkDebugf("%d ", plot->flushesSinceLastUsed()); + } + + // Count plots we can potentially upload to in all pages except the last one + // (the potential compactee). + if (plot->flushesSinceLastUsed() > kPlotRecentlyUsedCount) { + availablePlots.push_back() = plot; + } + + plotIter.next(); + } + + if constexpr (kDumpAtlasData) { + SkDebugf("\n"); + } + } + + // Count recently used plots in the last page and evict any that are no longer in use. + // Since we prioritize uploading to the first pages, this will eventually + // clear out usage of this page unless we have a large need. + plotIter.init(fPages[lastPageIndex].fPlotList, PlotList::Iter::kHead_IterStart); + unsigned int usedPlots = 0; + if constexpr (kDumpAtlasData) { + SkDebugf("page %d: ", lastPageIndex); + } + while (Plot* plot = plotIter.get()) { + // Update number of flushes since plot was last used + if (!plot->lastUseToken().inInterval(fPrevFlushToken, startTokenForNextFlush)) { + plot->incFlushesSinceLastUsed(); + } + + if constexpr (kDumpAtlasData) { + SkDebugf("%d ", plot->flushesSinceLastUsed()); + } + + // If this plot was used recently + if (plot->flushesSinceLastUsed() <= kPlotRecentlyUsedCount) { + usedPlots++; + } else if (plot->lastUseToken() != DrawToken::AlreadyFlushedToken()) { + // otherwise if aged out just evict it. + this->processEvictionAndResetRects(plot); + } + plotIter.next(); + } + + if constexpr (kDumpAtlasData) { + SkDebugf("\n"); + } + + // If recently used plots in the last page are using less than a quarter of the page, try + // to evict them if there's available space in lower index pages. Since we prioritize + // uploading to the first pages, this will eventually clear out usage of this page unless + // we have a large need. + if (availablePlots.count() && usedPlots && usedPlots <= fNumPlots / 4) { + plotIter.init(fPages[lastPageIndex].fPlotList, PlotList::Iter::kHead_IterStart); + while (Plot* plot = plotIter.get()) { + // If this plot was used recently + if (plot->flushesSinceLastUsed() <= kPlotRecentlyUsedCount) { + // See if there's room in an lower index page and if so evict. + // We need to be somewhat harsh here so that a handful of plots that are + // consistently in use don't end up locking the page in memory. + if (availablePlots.count() > 0) { + this->processEvictionAndResetRects(plot); + this->processEvictionAndResetRects(availablePlots.back()); + availablePlots.pop_back(); + --usedPlots; + } + if (!usedPlots || !availablePlots.count()) { + break; + } + } + plotIter.next(); + } + } + + // If none of the plots in the last page have been used recently, delete it. + if (!usedPlots) { + if constexpr (kDumpAtlasData) { + SkDebugf("delete %d\n", fNumActivePages-1); + } + + this->deactivateLastPage(); + fFlushesSinceLastUse = 0; + } + } + + fPrevFlushToken = startTokenForNextFlush; +} + +bool DrawAtlas::createPages(AtlasGenerationCounter* generationCounter) { + SkASSERT(SkIsPow2(fTextureWidth) && SkIsPow2(fTextureHeight)); + + int numPlotsX = fTextureWidth/fPlotWidth; + int numPlotsY = fTextureHeight/fPlotHeight; + + for (uint32_t i = 0; i < this->maxPages(); ++i) { + // Proxies are uncreated at first + fProxies[i] = nullptr; + + // set up allocated plots + fPages[i].fPlotArray = std::make_unique[]>(numPlotsX * numPlotsY); + + sk_sp* currPlot = fPages[i].fPlotArray.get(); + for (int y = numPlotsY - 1, r = 0; y >= 0; --y, ++r) { + for (int x = numPlotsX - 1, c = 0; x >= 0; --x, ++c) { + uint32_t plotIndex = r * numPlotsX + c; + currPlot->reset(new Plot( + i, plotIndex, generationCounter, x, y, fPlotWidth, fPlotHeight, fColorType, + fBytesPerPixel)); + + // build LRU list + fPages[i].fPlotList.addToHead(currPlot->get()); + ++currPlot; + } + } + + } + + return true; +} + +bool DrawAtlas::activateNewPage(Recorder* recorder) { + SkASSERT(fNumActivePages < this->maxPages()); + SkASSERT(!fProxies[fNumActivePages]); + + auto textureInfo = recorder->priv().caps()->getDefaultSampledTextureInfo(fColorType, + /*levelCount=*/1, + Protected::kNo, + Renderable::kNo); + fProxies[fNumActivePages].reset(new TextureProxy({fTextureWidth, fTextureHeight}, textureInfo, + SkBudgeted::kYes)); + if (!fProxies[fNumActivePages]) { + return false; + } + + if constexpr (kDumpAtlasData) { + SkDebugf("activated page#: %d\n", fNumActivePages); + } + + ++fNumActivePages; + return true; +} + +inline void DrawAtlas::deactivateLastPage() { + SkASSERT(fNumActivePages); + + uint32_t lastPageIndex = fNumActivePages - 1; + + int numPlotsX = fTextureWidth/fPlotWidth; + int numPlotsY = fTextureHeight/fPlotHeight; + + fPages[lastPageIndex].fPlotList.reset(); + for (int r = 0; r < numPlotsY; ++r) { + for (int c = 0; c < numPlotsX; ++c) { + uint32_t plotIndex = r * numPlotsX + c; + + Plot* currPlot = fPages[lastPageIndex].fPlotArray[plotIndex].get(); + currPlot->resetRects(); + currPlot->resetFlushesSinceLastUsed(); + + // rebuild the LRU list + SkDEBUGCODE(currPlot->resetListPtrs()); + fPages[lastPageIndex].fPlotList.addToHead(currPlot); + } + } + + // remove ref to the texture proxy + fProxies[lastPageIndex].reset(); + --fNumActivePages; +} + +DrawAtlasConfig::DrawAtlasConfig(int maxTextureSize, size_t maxBytes) { + static const SkISize kARGBDimensions[] = { + {256, 256}, // maxBytes < 2^19 + {512, 256}, // 2^19 <= maxBytes < 2^20 + {512, 512}, // 2^20 <= maxBytes < 2^21 + {1024, 512}, // 2^21 <= maxBytes < 2^22 + {1024, 1024}, // 2^22 <= maxBytes < 2^23 + {2048, 1024}, // 2^23 <= maxBytes + }; + + // Index 0 corresponds to maxBytes of 2^18, so start by dividing it by that + maxBytes >>= 18; + // Take the floor of the log to get the index + int index = maxBytes > 0 + ? SkTPin(SkPrevLog2(maxBytes), 0, SK_ARRAY_COUNT(kARGBDimensions) - 1) + : 0; + + SkASSERT(kARGBDimensions[index].width() <= kMaxAtlasDim); + SkASSERT(kARGBDimensions[index].height() <= kMaxAtlasDim); + fARGBDimensions.set(std::min(kARGBDimensions[index].width(), maxTextureSize), + std::min(kARGBDimensions[index].height(), maxTextureSize)); + fMaxTextureSize = std::min(maxTextureSize, kMaxAtlasDim); +} + +SkISize DrawAtlasConfig::atlasDimensions(MaskFormat type) const { + if (MaskFormat::kA8 == type) { + // A8 is always 2x the ARGB dimensions, clamped to the max allowed texture size + return { std::min(2 * fARGBDimensions.width(), fMaxTextureSize), + std::min(2 * fARGBDimensions.height(), fMaxTextureSize) }; + } else { + return fARGBDimensions; + } +} + +SkISize DrawAtlasConfig::plotDimensions(MaskFormat type) const { + if (MaskFormat::kA8 == type) { + SkISize atlasDimensions = this->atlasDimensions(type); + // For A8 we want to grow the plots at larger texture sizes to accept more of the + // larger SDF glyphs. Since the largest SDF glyph can be 170x170 with padding, this + // allows us to pack 3 in a 512x256 plot, or 9 in a 512x512 plot. + + // This will give us 512x256 plots for 2048x1024, 512x512 plots for 2048x2048, + // and 256x256 plots otherwise. + int plotWidth = atlasDimensions.width() >= 2048 ? 512 : 256; + int plotHeight = atlasDimensions.height() >= 2048 ? 512 : 256; + + return { plotWidth, plotHeight }; + } else { + // ARGB and LCD always use 256x256 plots -- this has been shown to be faster + return { 256, 256 }; + } +} + +} // namespace skgpu::graphite diff --git a/src/gpu/graphite/DrawAtlas.h b/src/gpu/graphite/DrawAtlas.h new file mode 100644 index 0000000000..2eefb21c64 --- /dev/null +++ b/src/gpu/graphite/DrawAtlas.h @@ -0,0 +1,263 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef skgpu_graphite_DrawAtlas_DEFINED +#define skgpu_graphite_DrawAtlas_DEFINED + +#include +#include +#include +#include + +#include "src/core/SkIPoint16.h" +#include "src/gpu/AtlasTypes.h" +#include "src/gpu/RectanizerSkyline.h" + +namespace skgpu::graphite { + +class DrawContext; +class Recorder; +class TextureProxy; + +/** + * TODO: the process described here is tentative, and this comment revised once locked down. + * + * This class manages one or more atlas textures on behalf of primitive draws in Device. The + * drawing processes that use the atlas add preceding UploadTasks when generating RenderPassTasks. + * The class provides facilities for using DrawTokens to detect data hazards. Plots that need + * uploads are tracked until it is impossible to add data without overwriting texels read by draws + * that have not yet been snapped to a RenderPassTask. At that point, the atlas will attempt to + * allocate a new atlas texture (or "page") of the same size, up to a maximum number of textures, + * and upload to that texture. If that's not possible, then the atlas will fail to add a subimage. + * This gives the Device the chance to end the current draw, snap a RenderpassTask, and begin a new + * one. Additional uploads will then succeed. + * + * When the atlas has multiple pages, new uploads are prioritized to the lower index pages, i.e., + * it will try to upload to page 0 before page 1 or 2. To keep the atlas from continually using + * excess space, periodic garbage collection is needed to shift data from the higher index pages to + * the lower ones, and then eventually remove any pages that are no longer in use. "In use" is + * determined by using the DrawToken system: After a DrawPass is snapped a subarea of the page, or + * "plot" is checked to see whether it was used in that DrawPass. If less than a quarter of the + * plots have been used recently (within kPlotRecentlyUsedCount iterations) and there are available + * plots in lower index pages, the higher index page will be deactivated, and its glyphs will + * gradually migrate to other pages via the usual upload system. + * + * Garbage collection is initiated by the DrawAtlas's client via the compact() method. + */ +class DrawAtlas { +public: + /** Is the atlas allowed to use more than one texture? */ + enum class AllowMultitexturing : bool { kNo, kYes }; + + /** + * Returns a DrawAtlas. + * @param ct The colorType which this atlas will store. + * @param bpp Size in bytes of each pixel. + * @param width Width in pixels of the atlas. + * @param height Height in pixels of the atlas. + * @param plotWidth The width of each plot. width/plotWidth should be an integer. + * @param plotWidth The height of each plot. height/plotHeight should be an integer. + * @param atlasGeneration A pointer to the context's generation counter. + * @param allowMultitexturing Can the atlas use more than one texture. + * @param evictor A pointer to an eviction callback class. + * + * @return An initialized DrawAtlas, or nullptr if creation fails. + */ + static std::unique_ptr Make(SkColorType ct, size_t bpp, + int width, int height, + int plotWidth, int plotHeight, + AtlasGenerationCounter* generationCounter, + AllowMultitexturing allowMultitexturing, + PlotEvictionCallback* evictor, + std::string_view label); + + /** + * TODO: the process described here is tentative, and this comment revised once locked down. + * + * Adds a width x height subimage to the atlas. Upon success it returns 'kSucceeded' and returns + * the ID and the subimage's coordinates in the backing texture. 'kTryAgain' is returned if + * the subimage cannot fit in the atlas without overwriting texels that will be read in the + * current list of draws. This indicates that the Device should end its current draw, snap a + * DrawPass, and begin another before adding more data. 'kError' will be returned when some + * unrecoverable error was encountered while trying to add the subimage. In this case the draw + * being created should be discarded. + * + * This tracking does not generate UploadTasks per se. Instead, when the RenderPassTask is + * ready to be snapped, recordUploads() will be called by the Device and that will generate the + * necessary UploadTasks. + * + * NOTE: When a draw that reads from the atlas is added to the DrawList, the client using this + * DrawAtlas must immediately call 'setLastUseToken' with the currentToken from the Recorder, + * otherwise the next call to addToAtlas might cause the previous data to be overwritten before + * it has been read. + */ + + enum class ErrorCode { + kError, + kSucceeded, + kTryAgain + }; + + ErrorCode addToAtlas(Recorder*, int width, int height, const void* image, AtlasLocator*); + bool recordUploads(DrawContext*, Recorder*); + + const sk_sp* getProxies() const { return fProxies; } + + uint64_t atlasGeneration() const { return fAtlasGeneration; } + + bool hasID(const PlotLocator& plotLocator) { + if (!plotLocator.isValid()) { + return false; + } + + uint32_t plot = plotLocator.plotIndex(); + uint32_t page = plotLocator.pageIndex(); + uint64_t plotGeneration = fPages[page].fPlotArray[plot]->genID(); + uint64_t locatorGeneration = plotLocator.genID(); + return plot < fNumPlots && page < fNumActivePages && plotGeneration == locatorGeneration; + } + + /** To ensure the atlas does not evict a given entry, the client must set the last use token. */ + void setLastUseToken(const AtlasLocator& atlasLocator, DrawToken token) { + SkASSERT(this->hasID(atlasLocator.plotLocator())); + uint32_t plotIdx = atlasLocator.plotIndex(); + SkASSERT(plotIdx < fNumPlots); + uint32_t pageIdx = atlasLocator.pageIndex(); + SkASSERT(pageIdx < fNumActivePages); + Plot* plot = fPages[pageIdx].fPlotArray[plotIdx].get(); + this->makeMRU(plot, pageIdx); + plot->setLastUseToken(token); + } + + uint32_t numActivePages() { return fNumActivePages; } + + void setLastUseTokenBulk(const BulkUsePlotUpdater& updater, + DrawToken token) { + int count = updater.count(); + for (int i = 0; i < count; i++) { + const BulkUsePlotUpdater::PlotData& pd = updater.plotData(i); + // it's possible we've added a plot to the updater and subsequently the plot's page + // was deleted -- so we check to prevent a crash + if (pd.fPageIndex < fNumActivePages) { + Plot* plot = fPages[pd.fPageIndex].fPlotArray[pd.fPlotIndex].get(); + this->makeMRU(plot, pd.fPageIndex); + plot->setLastUseToken(token); + } + } + } + + void compact(DrawToken startTokenForNextFlush); + + uint32_t maxPages() const { + return fMaxPages; + } + + int numAllocated_TestingOnly() const; + void setMaxPages_TestingOnly(uint32_t maxPages); + +private: + DrawAtlas(SkColorType, size_t bpp, int width, int height, int plotWidth, int plotHeight, + AtlasGenerationCounter* generationCounter, + AllowMultitexturing allowMultitexturing, std::string_view label); + + bool updatePlot(AtlasLocator*, Plot* plot); + + inline void makeMRU(Plot* plot, int pageIdx) { + if (fPages[pageIdx].fPlotList.head() == plot) { + return; + } + + fPages[pageIdx].fPlotList.remove(plot); + fPages[pageIdx].fPlotList.addToHead(plot); + + // No MRU update for pages -- since we will always try to add from + // the front and remove from the back there is no need for MRU. + } + + bool addToPage(unsigned int pageIdx, int width, int height, const void* image, AtlasLocator*); + + bool createPages(AtlasGenerationCounter*); + bool activateNewPage(Recorder*); + void deactivateLastPage(); + + void processEviction(PlotLocator); + inline void processEvictionAndResetRects(Plot* plot) { + this->processEviction(plot->plotLocator()); + plot->resetRects(); + } + + SkColorType fColorType; + size_t fBytesPerPixel; + int fTextureWidth; + int fTextureHeight; + int fPlotWidth; + int fPlotHeight; + unsigned int fNumPlots; + const std::string fLabel; + + // A counter to track the atlas eviction state for Glyphs. Each Glyph has a PlotLocator + // which contains its current generation. When the atlas evicts a plot, it increases + // the generation counter. If a Glyph's generation is less than the atlas's + // generation, then it knows it's been evicted and is either free to be deleted or + // re-added to the atlas if necessary. + AtlasGenerationCounter* const fGenerationCounter; + uint64_t fAtlasGeneration; + + // nextTokenToFlush() value at the end of the previous DrawPass + // TODO: rename + DrawToken fPrevFlushToken; + + // the number of flushes since this atlas has been last used + // TODO: rename + int fFlushesSinceLastUse; + + std::vector fEvictionCallbacks; + + struct Page { + // allocated array of Plots + std::unique_ptr[]> fPlotArray; + // LRU list of Plots (MRU at head - LRU at tail) + PlotList fPlotList; + }; + // proxies kept separate to make it easier to pass them up to client + sk_sp fProxies[PlotLocator::kMaxMultitexturePages]; + Page fPages[PlotLocator::kMaxMultitexturePages]; + uint32_t fMaxPages; + + uint32_t fNumActivePages; + + SkDEBUGCODE(void validate(const AtlasLocator& atlasLocator) const;) +}; + +// For text there are three atlases (A8, 565, ARGB) that are kept in relation with one another. In +// general, because A8 is the most frequently used mask format its dimensions are 2x the 565 and +// ARGB dimensions, with the constraint that an atlas size will always contain at least one plot. +// Since the ARGB atlas takes the most space, its dimensions are used to size the other two atlases. +class DrawAtlasConfig { +public: + // The capabilities of the GPU define maxTextureSize. The client provides maxBytes, and this + // represents the largest they want a single atlas texture to be. Due to multitexturing, we + // may expand temporarily to use more space as needed. + DrawAtlasConfig(int maxTextureSize, size_t maxBytes); + + SkISize atlasDimensions(MaskFormat type) const; + SkISize plotDimensions(MaskFormat type) const; + +private: + // On some systems texture coordinates are represented using half-precision floating point + // with 11 significant bits, which limits the largest atlas dimensions to 2048x2048. + // For simplicity we'll use this constraint for all of our atlas textures. + // This can be revisited later if we need larger atlases. + inline static constexpr int kMaxAtlasDim = 2048; + + SkISize fARGBDimensions; + int fMaxTextureSize; +}; + +} // namespace skgpu::graphite + +#endif diff --git a/src/gpu/graphite/Recorder.cpp b/src/gpu/graphite/Recorder.cpp index c4e7170a58..1715814342 100644 --- a/src/gpu/graphite/Recorder.cpp +++ b/src/gpu/graphite/Recorder.cpp @@ -9,6 +9,7 @@ #include "include/gpu/graphite/Recording.h" #include "src/core/SkPipelineData.h" +#include "src/gpu/AtlasTypes.h" #include "src/gpu/graphite/Caps.h" #include "src/gpu/graphite/CommandBuffer.h" #include "src/gpu/graphite/ContextPriv.h" @@ -20,6 +21,8 @@ #include "src/gpu/graphite/ResourceProvider.h" #include "src/gpu/graphite/TaskGraph.h" #include "src/gpu/graphite/UploadBufferManager.h" +#include "src/gpu/graphite/text/AtlasManager.h" +#include "src/text/gpu/StrikeCache.h" namespace skgpu::graphite { @@ -29,7 +32,13 @@ Recorder::Recorder(sk_sp gpu, sk_sp globalCache) : fGpu(std::move(gpu)) , fGraph(new TaskGraph) , fUniformDataCache(new UniformDataCache) - , fTextureDataCache(new TextureDataCache) { + , fTextureDataCache(new TextureDataCache) + // TODO: add config to control maxTextureBytes + , fAtlasManager(std::make_unique(this, 2048*2048, + DrawAtlas::AllowMultitexturing::kYes, + false)) + , fTokenTracker(std::make_unique()) + , fStrikeCache(std::make_unique()) { fResourceProvider = fGpu->makeResourceProvider(std::move(globalCache), this->singleOwner()); fDrawBufferManager.reset(new DrawBufferManager(fResourceProvider.get(), @@ -43,6 +52,9 @@ Recorder::~Recorder() { for (auto& device : fTrackedDevices) { device->abandonRecorder(); } + + // TODO: needed? + fStrikeCache->freeAll(); } std::unique_ptr Recorder::snap() { diff --git a/src/gpu/graphite/RecorderPriv.cpp b/src/gpu/graphite/RecorderPriv.cpp index acd5db469c..836178ae23 100644 --- a/src/gpu/graphite/RecorderPriv.cpp +++ b/src/gpu/graphite/RecorderPriv.cpp @@ -44,6 +44,18 @@ UploadBufferManager* RecorderPriv::uploadBufferManager() const { return fRecorder->fUploadBufferManager.get(); } +AtlasManager* RecorderPriv::atlasManager() { + return fRecorder->fAtlasManager.get(); +} + +TokenTracker* RecorderPriv::tokenTracker() { + return fRecorder->fTokenTracker.get(); +} + +sktext::gpu::StrikeCache* RecorderPriv::strikeCache() { + return fRecorder->fStrikeCache.get(); +} + void RecorderPriv::add(sk_sp task) { ASSERT_SINGLE_OWNER fRecorder->fGraph->add(std::move(task)); diff --git a/src/gpu/graphite/RecorderPriv.h b/src/gpu/graphite/RecorderPriv.h index f524125187..1bc9410c08 100644 --- a/src/gpu/graphite/RecorderPriv.h +++ b/src/gpu/graphite/RecorderPriv.h @@ -21,6 +21,9 @@ public: TextureDataCache* textureDataCache() const; DrawBufferManager* drawBufferManager() const; UploadBufferManager* uploadBufferManager() const; + AtlasManager* atlasManager(); + TokenTracker* tokenTracker(); + sktext::gpu::StrikeCache* strikeCache(); const Caps* caps() const; sk_sp refCaps() const; diff --git a/src/gpu/graphite/text/AtlasManager.cpp b/src/gpu/graphite/text/AtlasManager.cpp new file mode 100644 index 0000000000..2641e4ab26 --- /dev/null +++ b/src/gpu/graphite/text/AtlasManager.cpp @@ -0,0 +1,353 @@ +/* + * Copyright 2022 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/text/AtlasManager.h" + +#include "include/core/SkColorSpace.h" +#include "include/gpu/graphite/Recorder.h" +#include "src/codec/SkMasks.h" +#include "src/core/SkAutoMalloc.h" +#include "src/core/SkDistanceFieldGen.h" +#include "src/gpu/graphite/DrawAtlas.h" +#include "src/gpu/graphite/RecorderPriv.h" +#include "src/gpu/graphite/TextureProxy.h" +#include "src/text/gpu/Glyph.h" +#include "src/text/gpu/GlyphVector.h" +#include "src/text/gpu/StrikeCache.h" + +using Glyph = sktext::gpu::Glyph; + +namespace skgpu::graphite { + +AtlasManager::AtlasManager(Recorder* recorder, + size_t maxTextureBytes, + DrawAtlas::AllowMultitexturing allowMultitexturing, + bool supportBilerpAtlas) + : fRecorder(recorder) + , fAllowMultitexturing{allowMultitexturing} + , fSupportBilerpAtlas{supportBilerpAtlas} + , fAtlasConfig{recorder->priv().caps()->maxTextureSize(), maxTextureBytes} { } + +AtlasManager::~AtlasManager() = default; + +void AtlasManager::freeAll() { + for (int i = 0; i < kMaskFormatCount; ++i) { + fAtlases[i] = nullptr; + } +} + +bool AtlasManager::hasGlyph(MaskFormat format, Glyph* glyph) { + SkASSERT(glyph); + return this->getAtlas(format)->hasID(glyph->fAtlasLocator.plotLocator()); +} + +template +static void expand_bits(INT_TYPE* dst, + const uint8_t* src, + int width, + int height, + int dstRowBytes, + int srcRowBytes) { + for (int y = 0; y < height; ++y) { + int rowWritesLeft = width; + const uint8_t* s = src; + INT_TYPE* d = dst; + while (rowWritesLeft > 0) { + unsigned mask = *s++; + for (int x = 7; x >= 0 && rowWritesLeft; --x, --rowWritesLeft) { + *d++ = (mask & (1 << x)) ? (INT_TYPE)(~0UL) : 0; + } + } + dst = reinterpret_cast(reinterpret_cast(dst) + dstRowBytes); + src += srcRowBytes; + } +} + +static void get_packed_glyph_image( + const SkGlyph& glyph, int dstRB, MaskFormat expectedMaskFormat, void* dst) { + const int width = glyph.width(); + const int height = glyph.height(); + const void* src = glyph.image(); + SkASSERT(src != nullptr); + + MaskFormat maskFormat = Glyph::FormatFromSkGlyph(glyph.maskFormat()); + if (maskFormat == expectedMaskFormat) { + int srcRB = glyph.rowBytes(); + // Notice this comparison is with the glyphs raw mask format, and not its MaskFormat. + if (glyph.maskFormat() != SkMask::kBW_Format) { + if (srcRB != dstRB) { + const int bbp = MaskFormatBytesPerPixel(expectedMaskFormat); + for (int y = 0; y < height; y++) { + memcpy(dst, src, width * bbp); + src = (const char*) src + srcRB; + dst = (char*) dst + dstRB; + } + } else { + memcpy(dst, src, dstRB * height); + } + } else { + // Handle 8-bit format by expanding the mask to the expected format. + const uint8_t* bits = reinterpret_cast(src); + switch (expectedMaskFormat) { + case MaskFormat::kA8: { + uint8_t* bytes = reinterpret_cast(dst); + expand_bits(bytes, bits, width, height, dstRB, srcRB); + break; + } + case MaskFormat::kA565: { + uint16_t* rgb565 = reinterpret_cast(dst); + expand_bits(rgb565, bits, width, height, dstRB, srcRB); + break; + } + default: + SK_ABORT("Invalid MaskFormat"); + } + } + } else if (maskFormat == MaskFormat::kA565 && + expectedMaskFormat == MaskFormat::kARGB) { + // Convert if the glyph uses a 565 mask format since it is using LCD text rendering + // but the expected format is 8888 (will happen on Intel MacOS with Metal since that + // combination does not support 565). + static constexpr SkMasks masks{ + {0b1111'1000'0000'0000, 11, 5}, // Red + {0b0000'0111'1110'0000, 5, 6}, // Green + {0b0000'0000'0001'1111, 0, 5}, // Blue + {0, 0, 0} // Alpha + }; + constexpr int a565Bpp = MaskFormatBytesPerPixel(MaskFormat::kA565); + constexpr int argbBpp = MaskFormatBytesPerPixel(MaskFormat::kARGB); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + uint16_t color565 = 0; + memcpy(&color565, src, a565Bpp); + // TODO: create Graphite version of GrColorPackRGBA? + uint32_t colorRGBA = masks.getRed(color565) | + (masks.getGreen(color565) << 8) | + (masks.getBlue(color565) << 16) | + (0xFF << 24); + memcpy(dst, &colorRGBA, argbBpp); + src = (char*)src + a565Bpp; + dst = (char*)dst + argbBpp; + } + } + } else { + SkUNREACHABLE; + } +} + +MaskFormat AtlasManager::resolveMaskFormat(MaskFormat format) const { + if (MaskFormat::kA565 == format && + !fRecorder->priv().caps()->getDefaultSampledTextureInfo(kRGB_565_SkColorType, 1, + Protected::kNo, + Renderable::kNo).isValid()) { + format = MaskFormat::kARGB; + } + return format; +} + +// Returns kSucceeded if glyph successfully added to texture atlas, kTryAgain if a RenderPassTask +// needs to be snapped before adding the glyph, and kError if it can't be added at all. +DrawAtlas::ErrorCode AtlasManager::addGlyphToAtlas(const SkGlyph& skGlyph, + Glyph* glyph, + int srcPadding) { + SkASSERT(0 <= srcPadding && srcPadding <= SK_DistanceFieldInset); + + if (skGlyph.image() == nullptr) { + return DrawAtlas::ErrorCode::kError; + } + SkASSERT(glyph != nullptr); + + MaskFormat glyphFormat = Glyph::FormatFromSkGlyph(skGlyph.maskFormat()); + MaskFormat expectedMaskFormat = this->resolveMaskFormat(glyphFormat); + int bytesPerPixel = MaskFormatBytesPerPixel(expectedMaskFormat); + + int padding; + switch (srcPadding) { + case 0: + // The direct mask/image case. + padding = 0; + if (fSupportBilerpAtlas) { + // Force direct masks (glyph with no padding) to have padding. + padding = 1; + srcPadding = 1; + } + break; + case 1: + // The transformed mask/image case. + padding = 1; + break; + case SK_DistanceFieldInset: + // The SDFT case. + // If the srcPadding == SK_DistanceFieldInset (SDFT case) then the padding is built + // into the image on the glyph; no extra padding needed. + // TODO: can the SDFT glyph image in the cache be reduced by the padding? + padding = 0; + break; + default: + // The padding is not one of the know forms. + return DrawAtlas::ErrorCode::kError; + } + + const int width = skGlyph.width() + 2*padding; + const int height = skGlyph.height() + 2*padding; + int rowBytes = width * bytesPerPixel; + size_t size = height * rowBytes; + + // Temporary storage for normalizing glyph image. + SkAutoSMalloc<1024> storage(size); + void* dataPtr = storage.get(); + if (padding > 0) { + sk_bzero(dataPtr, size); + // Advance in one row and one column. + dataPtr = (char*)(dataPtr) + rowBytes + bytesPerPixel; + } + + get_packed_glyph_image(skGlyph, rowBytes, expectedMaskFormat, dataPtr); + + DrawAtlas* atlas = this->getAtlas(expectedMaskFormat); + auto errorCode = atlas->addToAtlas(fRecorder, + width, + height, + storage.get(), + &glyph->fAtlasLocator); + + if (errorCode == DrawAtlas::ErrorCode::kSucceeded) { + glyph->fAtlasLocator.insetSrc(srcPadding); + } + + return errorCode; +} + +bool AtlasManager::recordUploads(DrawContext* dc) { + for (int i = 0; i < skgpu::kMaskFormatCount; i++) { + if (fAtlases[i] && !fAtlases[i]->recordUploads(dc, fRecorder)) { + return false; + } + } + return true; +} + +void AtlasManager::addGlyphToBulkAndSetUseToken(BulkUsePlotUpdater* updater, + MaskFormat format, Glyph* glyph, + DrawToken token) { + SkASSERT(glyph); + if (updater->add(glyph->fAtlasLocator)) { + this->getAtlas(format)->setLastUseToken(glyph->fAtlasLocator, token); + } +} + +void AtlasManager::setAtlasDimensionsToMinimum_ForTesting() { + // Delete any old atlases. + // This should be safe to do as long as we are not in the middle of a flush. + for (int i = 0; i < skgpu::kMaskFormatCount; i++) { + fAtlases[i] = nullptr; + } + + // Set all the atlas sizes to 1x1 plot each. + new (&fAtlasConfig) DrawAtlasConfig{2048, 0}; +} + +bool AtlasManager::initAtlas(MaskFormat format) { + int index = MaskFormatToAtlasIndex(format); + if (fAtlases[index] == nullptr) { + SkColorType colorType = MaskFormatToColorType(format); + SkISize atlasDimensions = fAtlasConfig.atlasDimensions(format); + SkISize plotDimensions = fAtlasConfig.plotDimensions(format); + fAtlases[index] = DrawAtlas::Make(colorType, + SkColorTypeBytesPerPixel(colorType), + atlasDimensions.width(), atlasDimensions.height(), + plotDimensions.width(), plotDimensions.height(), + this, + fAllowMultitexturing, + nullptr, + /*label=*/"TextAtlas"); + if (!fAtlases[index]) { + return false; + } + } + return true; +} + +} // namespace skgpu::graphite + +//////////////////////////////////////////////////////////////////////////////////////////////// + +namespace sktext::gpu { + +using DrawAtlas = skgpu::graphite::DrawAtlas; + +std::tuple GlyphVector::regenerateAtlas(int begin, int end, + skgpu::MaskFormat maskFormat, + int srcPadding, + skgpu::graphite::Recorder* recorder) { + auto atlasManager = recorder->priv().atlasManager(); + auto tokenTracker = recorder->priv().tokenTracker(); + + // TODO: this is not a great place for this -- need a better way to init atlases when needed + unsigned int numActiveProxies; + const sk_sp* proxies = + atlasManager->getProxies(maskFormat, &numActiveProxies); + if (!proxies) { + SkDebugf("Could not allocate backing texture for atlas\n"); + return {false, 0}; + } + + uint64_t currentAtlasGen = atlasManager->atlasGeneration(maskFormat); + + this->packedGlyphIDToGlyph(recorder->priv().strikeCache()); + + if (fAtlasGeneration != currentAtlasGen) { + // Calculate the texture coordinates for the vertexes during first use (fAtlasGeneration + // is set to kInvalidAtlasGeneration) or the atlas has changed in subsequent calls.. + fBulkUseUpdater.reset(); + + SkBulkGlyphMetricsAndImages metricsAndImages{fTextStrike->strikeSpec()}; + + // Update the atlas information in the GrStrike. + auto glyphs = fGlyphs.subspan(begin, end - begin); + int glyphsPlacedInAtlas = 0; + bool success = true; + for (const Variant& variant : glyphs) { + Glyph* gpuGlyph = variant.glyph; + SkASSERT(gpuGlyph != nullptr); + + if (!atlasManager->hasGlyph(maskFormat, gpuGlyph)) { + const SkGlyph& skGlyph = *metricsAndImages.glyph(gpuGlyph->fPackedID); + auto code = atlasManager->addGlyphToAtlas(skGlyph, gpuGlyph, srcPadding); + if (code != DrawAtlas::ErrorCode::kSucceeded) { + success = code != DrawAtlas::ErrorCode::kError; + break; + } + } + atlasManager->addGlyphToBulkAndSetUseToken( + &fBulkUseUpdater, maskFormat, gpuGlyph, + tokenTracker->nextDrawToken()); + glyphsPlacedInAtlas++; + } + + // Update atlas generation if there are no more glyphs to put in the atlas. + if (success && begin + glyphsPlacedInAtlas == SkCount(fGlyphs)) { + // Need to get the freshest value of the atlas' generation because + // updateTextureCoordinates may have changed it. + fAtlasGeneration = atlasManager->atlasGeneration(maskFormat); + } + + return {success, glyphsPlacedInAtlas}; + } else { + // The atlas hasn't changed, so our texture coordinates are still valid. + if (end == SkCount(fGlyphs)) { + // The atlas hasn't changed and the texture coordinates are all still valid. Update + // all the plots used to the new use token. + atlasManager->setUseTokenBulk(fBulkUseUpdater, + tokenTracker->nextDrawToken(), + maskFormat); + } + return {true, end - begin}; + } +} + +} // namespace sktext::gpu diff --git a/src/gpu/graphite/text/AtlasManager.h b/src/gpu/graphite/text/AtlasManager.h new file mode 100644 index 0000000000..097c77228e --- /dev/null +++ b/src/gpu/graphite/text/AtlasManager.h @@ -0,0 +1,118 @@ +/* + * Copyright 2022 Google LLC + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef skgpu_graphite_AtlasManager_DEFINED +#define skgpu_graphite_AtlasManager_DEFINED + +#include "include/gpu/graphite/TextureInfo.h" +#include "src/gpu/AtlasTypes.h" +#include "src/gpu/graphite/Caps.h" +#include "src/gpu/graphite/DrawAtlas.h" + +namespace sktext::gpu { +class Glyph; +} +class SkGlyph; + +namespace skgpu::graphite { + +class DrawContext; +class Recorder; + +////////////////////////////////////////////////////////////////////////////////////////////////// +/** The AtlasManager manages the lifetime of and access to DrawAtlases. + */ +class AtlasManager : public AtlasGenerationCounter { +public: + AtlasManager(Recorder*, + size_t maxTextureBytes, + DrawAtlas::AllowMultitexturing, + bool supportBilerpAtlas); + ~AtlasManager(); + + // Used to get proxies for test rendering + const sk_sp* getProxies(MaskFormat format, + unsigned int* numActiveProxies) { + format = this->resolveMaskFormat(format); + if (this->initAtlas(format)) { + *numActiveProxies = this->getAtlas(format)->numActivePages(); + return this->getAtlas(format)->getProxies(); + } + *numActiveProxies = 0; + return nullptr; + } + + void freeAll(); + + bool hasGlyph(MaskFormat, sktext::gpu::Glyph*); + + DrawAtlas::ErrorCode addGlyphToAtlas(const SkGlyph&, + sktext::gpu::Glyph*, + int srcPadding); + + // To ensure the DrawAtlas does not evict the Glyph Mask from its texture backing store, + // the client must pass in the current draw token along with the sktext::gpu::Glyph. + // A BulkUsePlotUpdater is used to manage bulk last use token updating in the Atlas. + // For convenience, this function will also set the use token for the current glyph if required + // NOTE: the bulk uploader is only valid if the subrun has a valid atlasGeneration + void addGlyphToBulkAndSetUseToken(BulkUsePlotUpdater*, MaskFormat, + sktext::gpu::Glyph*, DrawToken); + + void setUseTokenBulk(const BulkUsePlotUpdater& updater, + DrawToken token, + MaskFormat format) { + this->getAtlas(format)->setLastUseTokenBulk(updater, token); + } + + bool recordUploads(DrawContext* dc); + + // Some clients may wish to verify the integrity of the texture backing store of the + // GrDrawOpAtlas. The atlasGeneration returned below is a monotonically increasing number which + // changes every time something is removed from the texture backing store. + uint64_t atlasGeneration(skgpu::MaskFormat format) const { + return this->getAtlas(format)->atlasGeneration(); + } + + /////////////////////////////////////////////////////////////////////////// + // Functions intended debug only + + void setAtlasDimensionsToMinimum_ForTesting(); + void setMaxPages_TestingOnly(uint32_t maxPages); + +private: + bool initAtlas(MaskFormat); + // Change an expected 565 mask format to 8888 if 565 is not supported (will happen when using + // Metal on Intel MacOS). The actual conversion of the data is handled in + // get_packed_glyph_image() in StrikeCache.cpp + MaskFormat resolveMaskFormat(MaskFormat format) const; + + // There is a 1:1 mapping between skgpu::MaskFormats and atlas indices + static int MaskFormatToAtlasIndex(skgpu::MaskFormat format) { + return static_cast(format); + } + static skgpu::MaskFormat AtlasIndexToMaskFormat(int idx) { + return static_cast(idx); + } + + DrawAtlas* getAtlas(skgpu::MaskFormat format) const { + format = this->resolveMaskFormat(format); + int atlasIndex = MaskFormatToAtlasIndex(format); + SkASSERT(fAtlases[atlasIndex]); + return fAtlases[atlasIndex].get(); + } + + Recorder* fRecorder; + DrawAtlas::AllowMultitexturing fAllowMultitexturing; + std::unique_ptr fAtlases[kMaskFormatCount]; + static_assert(kMaskFormatCount == 3); + bool fSupportBilerpAtlas; + DrawAtlasConfig fAtlasConfig; +}; + +} // namespace skgpu::graphite + +#endif // skgpu_graphite_AtlasManager_DEFINED diff --git a/src/text/gpu/GlyphVector.h b/src/text/gpu/GlyphVector.h index 5ae7097546..1a52beebe2 100644 --- a/src/text/gpu/GlyphVector.h +++ b/src/text/gpu/GlyphVector.h @@ -20,6 +20,9 @@ class SkStrikeClient; #if SK_SUPPORT_GPU class GrMeshDrawTarget; #endif +#if defined(SK_GRAPHITE_ENABLED) +namespace skgpu::graphite { class Recorder; } +#endif namespace sktext::gpu { @@ -63,6 +66,14 @@ public: GrMeshDrawTarget*); #endif +#if defined(SK_GRAPHITE_ENABLED) + std::tuple regenerateAtlas( + int begin, int end, + skgpu::MaskFormat maskFormat, + int srcPadding, + skgpu::graphite::Recorder*); +#endif + static size_t GlyphVectorSize(size_t count) { return sizeof(Variant) * count; }