| /* |
| * Copyright 2021 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_geom_BoundsManager_DEFINED |
| #define skgpu_graphite_geom_BoundsManager_DEFINED |
| |
| #include "include/core/SkSize.h" |
| #include "include/private/base/SkTemplates.h" |
| |
| #include "src/base/SkTBlockList.h" |
| #include "src/base/SkVx.h" |
| #include "src/gpu/graphite/DrawOrder.h" |
| #include "src/gpu/graphite/geom/Rect.h" |
| |
| #include <cstdint> |
| |
| namespace skgpu::graphite { |
| |
| /** |
| * BoundsManager is an acceleration structure for device-space related pixel bounds queries. |
| * The BoundsManager tracks a single ordinal value per bounds: the CompressedPaintersOrder of a draw |
| * The CompressedPaintersOrder enforces a specific submission order of draws to the GPU but can |
| * re-arrange draws out of their original painter's order if the GREATER depth test and the draw's Z |
| * value resolve out-of-order rendering. |
| * |
| * It supports querying the most recent draw intersecting a bounding rect (represented as a |
| * CompressedPaintersOrder value), and recording a (bounds, CompressedPaintersOrder) pair. |
| */ |
| class BoundsManager { |
| public: |
| virtual ~BoundsManager() {} |
| |
| virtual CompressedPaintersOrder getMostRecentDraw(const Rect& bounds) const = 0; |
| |
| virtual void recordDraw(const Rect& bounds, CompressedPaintersOrder order) = 0; |
| |
| virtual void reset() = 0; |
| }; |
| |
| // TODO: Select one most-effective BoundsManager implementation, make it the only option, and remove |
| // virtual-ness. For now, this seems useful for correctness testing by comparing against trivial |
| // implementations and for identifying how much "smarts" are actually worthwhile. |
| |
| // A BoundsManager that produces exact painter's order and assumes nothing is occluded. |
| class NaiveBoundsManager final : public BoundsManager { |
| public: |
| ~NaiveBoundsManager() override {} |
| |
| CompressedPaintersOrder getMostRecentDraw(const Rect& bounds) const override { |
| return fLatestDraw; |
| } |
| |
| |
| void recordDraw(const Rect& bounds, CompressedPaintersOrder order) override { |
| if (fLatestDraw < order) { |
| fLatestDraw = order; |
| } |
| } |
| |
| void reset() override { |
| fLatestDraw = CompressedPaintersOrder::First(); |
| } |
| |
| private: |
| CompressedPaintersOrder fLatestDraw = CompressedPaintersOrder::First(); |
| }; |
| |
| // A BoundsManager that tracks every draw and can exactly determine all queries |
| // using a brute force search. |
| class BruteForceBoundsManager : public BoundsManager { |
| public: |
| ~BruteForceBoundsManager() override {} |
| |
| CompressedPaintersOrder getMostRecentDraw(const Rect& bounds) const override { |
| SkASSERT(fRects.count() == fOrders.count()); |
| |
| Rect::ComplementRect boundsComplement(bounds); |
| CompressedPaintersOrder max = CompressedPaintersOrder::First(); |
| auto orderIter = fOrders.items().begin(); |
| for (const Rect& r : fRects.items()) { |
| if (r.intersects(boundsComplement) && max < *orderIter) { |
| max = *orderIter; |
| } |
| ++orderIter; |
| } |
| return max; |
| } |
| |
| void recordDraw(const Rect& bounds, CompressedPaintersOrder order) override { |
| fRects.push_back(bounds); |
| fOrders.push_back(order); |
| } |
| |
| void reset() override { |
| fRects.reset(); |
| fOrders.reset(); |
| } |
| |
| int count() const { return fRects.count(); } |
| |
| void replayDraws(BoundsManager* manager) const { |
| auto orderIter = fOrders.items().begin(); |
| for (const Rect& r : fRects.items()) { |
| manager->recordDraw(r, *orderIter); |
| ++orderIter; |
| } |
| } |
| |
| private: |
| // fRects and fOrders are parallel, but kept separate to avoid wasting padding since Rect is |
| // an over-aligned type. |
| SkTBlockList<Rect> fRects{16, SkBlockAllocator::GrowthPolicy::kFibonacci}; |
| SkTBlockList<CompressedPaintersOrder> fOrders{16, SkBlockAllocator::GrowthPolicy::kFibonacci}; |
| }; |
| |
| // A BoundsManager that tracks highest CompressedPaintersOrder over a uniform spatial grid. |
| class GridBoundsManager : public BoundsManager { |
| public: |
| // 'gridSize' is the number of cells in the X and Y directions, splitting the pixels from [0,0] |
| // to 'deviceSize' into uniformly-sized cells. |
| static std::unique_ptr<GridBoundsManager> Make(const SkISize& deviceSize, |
| const SkISize& gridSize) { |
| SkASSERT(deviceSize.width() > 0 && deviceSize.height() > 0); |
| SkASSERT(gridSize.width() >= 1 && gridSize.height() >= 1); |
| |
| return std::unique_ptr<GridBoundsManager>(new GridBoundsManager(deviceSize, gridSize)); |
| } |
| |
| static std::unique_ptr<GridBoundsManager> Make(const SkISize& deviceSize, int gridSize) { |
| return Make(deviceSize, {gridSize, gridSize}); |
| } |
| |
| static std::unique_ptr<GridBoundsManager> MakeRes(const SkISize& deviceSize, int gridCellSize) { |
| SkASSERT(deviceSize.width() > 0 && deviceSize.height() > 0); |
| SkASSERT(gridCellSize >= 1); |
| |
| int gridWidth = SkScalarCeilToInt(deviceSize.width() / (float) gridCellSize); |
| int gridHeight = SkScalarCeilToInt(deviceSize.height() / (float) gridCellSize); |
| |
| // This keeps the grid cells exactly at the requested resolution, but pads the right and |
| // bottom edges out to a multiple of the cell size. The alternative is pass in the unpadded |
| // device size, which would mean the actual cell size will be smaller than the requested |
| // (by (deviceSize % gridCellSize)/gridDims). |
| SkISize paddedDeviceSize = {gridWidth * gridCellSize, gridHeight * gridCellSize}; |
| return Make(paddedDeviceSize, {gridWidth, gridHeight}); |
| } |
| |
| ~GridBoundsManager() override {} |
| |
| |
| CompressedPaintersOrder getMostRecentDraw(const Rect& bounds) const override { |
| SkASSERT(!bounds.isEmptyNegativeOrNaN()); |
| |
| auto ltrb = this->getGridCoords(bounds); |
| const CompressedPaintersOrder* p = fNodes.data() + ltrb[1] * fGridWidth + ltrb[0]; |
| int h = ltrb[3] - ltrb[1]; |
| int w = ltrb[2] - ltrb[0]; |
| |
| CompressedPaintersOrder max = CompressedPaintersOrder::First(); |
| for (int y = 0; y <= h; ++y ) { |
| for (int x = 0; x <= w; ++x) { |
| CompressedPaintersOrder v = *(p + x); |
| if (v > max) { |
| max = v; |
| } |
| } |
| p = p + fGridWidth; |
| } |
| |
| return max; |
| } |
| |
| void recordDraw(const Rect& bounds, CompressedPaintersOrder order) override { |
| SkASSERT(!bounds.isEmptyNegativeOrNaN()); |
| |
| auto ltrb = this->getGridCoords(bounds); |
| CompressedPaintersOrder* p = fNodes.data() + ltrb[1] * fGridWidth + ltrb[0]; |
| int h = ltrb[3] - ltrb[1]; |
| int w = ltrb[2] - ltrb[0]; |
| |
| for (int y = 0; y <= h; ++y) { |
| for (int x = 0; x <= w; ++x) { |
| CompressedPaintersOrder v = *(p + x); |
| if (order > v) { |
| *(p + x) = order; |
| } |
| } |
| p = p + fGridWidth; |
| } |
| } |
| |
| void reset() override { |
| memset(fNodes.data(), 0, sizeof(CompressedPaintersOrder) * fGridWidth * fGridHeight); |
| } |
| |
| private: |
| GridBoundsManager(const SkISize& deviceSize, const SkISize& gridSize) |
| : fScaleX(gridSize.width() / (float) deviceSize.width()) |
| , fScaleY(gridSize.height() / (float) deviceSize.height()) |
| , fGridWidth(gridSize.width()) |
| , fGridHeight(gridSize.height()) |
| , fNodes((size_t) fGridWidth * fGridHeight) { |
| // Reset is needed to zero-out the uninitialized fNodes values. |
| this->reset(); |
| } |
| |
| skvx::int4 getGridCoords(const Rect& bounds) const { |
| // Normalize bounds by 1/wh of device bounds, then scale up to number of cells per side. |
| // fScaleXY includes both 1/wh and the grid dimension scaling, then clamp to [0, gridDim-1]. |
| return pin(skvx::cast<int32_t>(bounds.ltrb() * skvx::float2(fScaleX, fScaleY).xyxy()), |
| skvx::int4(0), |
| skvx::int2(fGridWidth, fGridHeight).xyxy() - 1); |
| } |
| |
| const float fScaleX; |
| const float fScaleY; |
| |
| const int fGridWidth; |
| const int fGridHeight; |
| |
| skia_private::AutoTMalloc<CompressedPaintersOrder> fNodes; |
| }; |
| |
| // A BoundsManager that first relies on BruteForceBoundsManager for N draw calls, and then switches |
| // to the GridBoundsManager if it exceeds its limit. For low N, the brute force approach is |
| // surprisingly efficient, has the highest accuracy, and very low memory overhead. Once the draw |
| // call count is large enough, the grid's lower performance complexity outweigh its memory cost and |
| // reduced accuracy. |
| class HybridBoundsManager : public BoundsManager { |
| public: |
| HybridBoundsManager(const SkISize& deviceSize, |
| int gridCellSize, |
| int maxBruteForceN) |
| : fDeviceSize(deviceSize) |
| , fGridCellSize(gridCellSize) |
| , fMaxBruteForceN(maxBruteForceN) |
| , fCurrentManager(&fBruteForceManager) { |
| SkASSERT(deviceSize.width() >= 1 && deviceSize.height() >= 1 && |
| gridCellSize >= 1 && maxBruteForceN >= 1); |
| } |
| |
| CompressedPaintersOrder getMostRecentDraw(const Rect& bounds) const override { |
| return fCurrentManager->getMostRecentDraw(bounds); |
| } |
| |
| void recordDraw(const Rect& bounds, CompressedPaintersOrder order) override { |
| this->updateCurrentManagerIfNeeded(); |
| fCurrentManager->recordDraw(bounds, order); |
| } |
| |
| void reset() override { |
| const bool usedGrid = fCurrentManager == fGridManager.get(); |
| if (usedGrid) { |
| // Reset the grid manager so it's ready to use next frame, but don't delete it. |
| fGridManager->reset(); |
| // Assume brute force manager was reset when we swapped to the grid originally |
| fCurrentManager = &fBruteForceManager; |
| } else { |
| if (fGridManager) { |
| // Clean up the grid manager that was created over a frame ago without being used. |
| // This could lead to re-allocating the grid every-other frame, but it's a simple |
| // way to ensure we don't hold onto the grid in perpetuity if it's not needed. |
| fGridManager = nullptr; |
| } |
| fBruteForceManager.reset(); |
| SkASSERT(fCurrentManager == &fBruteForceManager); |
| } |
| } |
| |
| private: |
| const SkISize fDeviceSize; |
| const int fGridCellSize; |
| const int fMaxBruteForceN; |
| |
| BoundsManager* fCurrentManager; |
| |
| BruteForceBoundsManager fBruteForceManager; |
| |
| // The grid manager starts out null and is created the first time we exceed fMaxBruteForceN. |
| // However, even if we reset back to the brute force manager, we keep the grid around under the |
| // assumption that the owning Device will have similar frame-to-frame draw counts and will need |
| // to upgrade to the grid manager again. |
| std::unique_ptr<GridBoundsManager> fGridManager; |
| |
| void updateCurrentManagerIfNeeded() { |
| if (fCurrentManager == fGridManager.get() || |
| fBruteForceManager.count() < fMaxBruteForceN) { |
| // Already using the grid or the about-to-be-recorded draw will not cause us to exceed |
| // the brute force limit, so no need to change the current manager implementation. |
| return; |
| } |
| // Else we need to switch from the brute force manager to the grid manager |
| if (!fGridManager) { |
| fGridManager = GridBoundsManager::MakeRes(fDeviceSize, fGridCellSize); |
| } |
| fCurrentManager = fGridManager.get(); |
| |
| // Fill out the grid manager with the recorded draws in the brute force manager |
| fBruteForceManager.replayDraws(fCurrentManager); |
| fBruteForceManager.reset(); |
| } |
| }; |
| |
| } // namespace skgpu::graphite |
| |
| #endif // skgpu_graphite_geom_BoundsManager_DEFINED |