| /* |
| * Copyright 2026 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/sparse_strips/AlphaAtlasManager.h" |
| |
| #include "src/gpu/graphite/Caps.h" |
| #include "src/gpu/graphite/DrawContext.h" |
| #include "src/gpu/graphite/RecorderPriv.h" |
| #include "src/gpu/graphite/TextureInfoPriv.h" |
| #include "src/gpu/graphite/task/UploadTask.h" |
| |
| #include <algorithm> |
| #include <cstring> |
| |
| namespace skgpu::graphite { |
| |
| AlphaAtlasManager::AlphaAtlasManager(Recorder* recorder) |
| : fRecorder(recorder) |
| , fNullBufferUsedBytes(0) |
| , fActiveSlot(0) |
| , fLastGpuSlot(0) |
| , fLastRetiredSlot(kInvalidSlot) |
| , fNextPageRowCount(SparseStripConfig::kInitialAtlasRows) {} |
| |
| bool AlphaAtlasManager::createPageInSlot(int32_t slot, int32_t minRequiredBytes) { |
| SkASSERT(slot < SparseStripConfig::kMaxTexturePages); |
| SkASSERT(!fPages[slot].isValid()); |
| |
| int32_t rows = fNextPageRowCount; |
| if (minRequiredBytes > 0) { |
| int32_t neededRows = (minRequiredBytes + SparseStripConfig::kAtlasWidthBytes - 1) / |
| SparseStripConfig::kAtlasWidthBytes; |
| if (neededRows > SparseStripConfig::kMaxAtlasRows) { |
| return false; |
| } |
| rows = std::max(rows, neededRows); |
| } |
| rows = std::min(rows, SparseStripConfig::kMaxAtlasRows); |
| |
| const Caps* caps = fRecorder->priv().caps(); |
| TextureInfo info = caps->getDefaultSampledTextureInfo(SparseStripConfig::kColorType, |
| Mipmapped::kNo, |
| fRecorder->priv().isProtected(), |
| Renderable::kNo); |
| |
| sk_sp<TextureProxy> proxy = |
| TextureProxy::Make(caps, |
| fRecorder->priv().resourceProvider(), |
| SkISize::Make(SparseStripConfig::kAtlasWidth, rows), |
| info, |
| Budgeted::kYes, |
| "AlphaAtlas"); |
| |
| if (!proxy) { |
| return false; |
| } |
| |
| TexturePage& page = fPages[slot]; |
| page.fTexture = std::move(proxy); |
| page.fRowCount = rows; |
| page.fCapacityBytes = rows * SparseStripConfig::kAtlasWidthBytes; |
| page.fUsedBytes = 0; |
| page.fUploadedRows = 0; |
| page.fAlphaBuffer.clear(); |
| page.fAlphaBuffer.reserve(page.fCapacityBytes); |
| |
| // Double row count for the next created page, clamped to max atlas rows |
| fNextPageRowCount = std::min(SparseStripConfig::kMaxAtlasRows, rows * 2); |
| return true; |
| } |
| |
| uint8_t* AlphaAtlasManager::requestAlphaSpace(int32_t numBytes) { |
| if (numBytes <= 0 || numBytes > SparseStripConfig::kMaxAtlasBytes) { |
| return nullptr; |
| } |
| |
| // 0. If already on the nullbuffer slot, append directly to it. |
| if (fActiveSlot == kNullSlot) { |
| return fNullBuffer.append(numBytes); |
| } |
| |
| // 1. Ensure current active slot has a valid page allocated |
| SkASSERT(fActiveSlot >= 0 && fActiveSlot < SparseStripConfig::kMaxTexturePages); |
| if (!fPages[fActiveSlot].isValid()) { |
| if (!this->createPageInSlot(fActiveSlot, numBytes)) { |
| return nullptr; |
| } |
| } |
| |
| // 2. If the active page is already full, proactively flip to the other slot |
| if (fPages[fActiveSlot].fUsedBytes >= fPages[fActiveSlot].fCapacityBytes) { |
| int32_t nextSlot = fActiveSlot ^ 1; |
| // Both pages are in use, transition to null buffer. |
| if (fPages[nextSlot].isValid()) { |
| fActiveSlot = kNullSlot; |
| return fNullBuffer.append(numBytes); |
| } |
| if (!this->createPageInSlot(nextSlot, numBytes)) { |
| return nullptr; |
| } |
| fActiveSlot = nextSlot; |
| } |
| |
| TexturePage& activePage = fPages[fActiveSlot]; |
| fLastGpuSlot = fActiveSlot; |
| return activePage.fAlphaBuffer.append(numBytes); |
| } |
| |
| std::optional<std::pair<int32_t, uint16_t>> AlphaAtlasManager::finalizeRun() { |
| // Case 0: We are already on the nullbuffer |
| if (fActiveSlot == kNullSlot) { |
| SkASSERT(fNullBuffer.size() > fNullBufferUsedBytes); |
| int32_t runStartIdx = fNullBufferUsedBytes; |
| fNullBufferUsedBytes = fNullBuffer.size(); |
| return std::make_pair(runStartIdx, kNullSlot); |
| } |
| |
| TexturePage& activePage = fPages[fActiveSlot]; |
| if (!activePage.isValid()) { |
| return std::nullopt; |
| } |
| |
| SkASSERT(activePage.fAlphaBuffer.size() > activePage.fUsedBytes); |
| int32_t runBytes = activePage.fAlphaBuffer.size() - activePage.fUsedBytes; |
| |
| // Case 1: Run completely fits in active page (no straddle) |
| int32_t runStartIdx = activePage.fUsedBytes; |
| if (activePage.fAlphaBuffer.size() <= activePage.fCapacityBytes) { |
| activePage.fUsedBytes = activePage.fAlphaBuffer.size(); |
| return std::make_pair(runStartIdx, static_cast<uint16_t>(fActiveSlot)); |
| } |
| |
| // Case 2: Run straddled the page boundary! |
| int32_t nextSlot = fActiveSlot ^ 1; |
| if (fPages[nextSlot].isValid()) { |
| // Both GPU slots are full! Switch to null buffer. |
| // Currently this only occurs when all atlas pages have been exhausted, so fNullBuffer is |
| // guaranteed to be empty at this point, making offset 0 (below) valid. If fNullBuffer is |
| // ever reused or shared across draws in the future, this should return the appended run's |
| // actual start offset (runStartIdx in fNullBuffer) instead. |
| SkASSERT(fNullBuffer.empty()); |
| |
| uint8_t* dst = fNullBuffer.append(runBytes); |
| std::memcpy(dst, |
| activePage.fAlphaBuffer.data() + runStartIdx, |
| runBytes); |
| |
| // Trim overrun from activePage back to its committed size |
| activePage.fAlphaBuffer.resize(activePage.fUsedBytes); |
| |
| // Switch active slot to kNullSlot |
| fActiveSlot = kNullSlot; |
| fNullBufferUsedBytes = fNullBuffer.size(); |
| |
| return std::make_pair(0, kNullSlot); |
| } |
| |
| if (!this->createPageInSlot(nextSlot, runBytes)) { |
| return std::nullopt; |
| } |
| |
| TexturePage& newPage = fPages[nextSlot]; |
| |
| // Append runBytes to newPage and copy the overrun data |
| uint8_t* dst = newPage.fAlphaBuffer.append(runBytes); |
| std::memcpy(dst, |
| activePage.fAlphaBuffer.data() + runStartIdx, |
| runBytes); |
| |
| // Trim overrun from activePage back to its committed size |
| activePage.fAlphaBuffer.resize(activePage.fUsedBytes); |
| |
| // Switch active slot to nextSlot |
| fActiveSlot = nextSlot; |
| newPage.fUsedBytes = runBytes; |
| |
| return std::make_pair(0, static_cast<uint16_t>(fActiveSlot)); |
| } |
| |
| void AlphaAtlasManager::recordUploads(DrawContext* dc) { |
| for (int32_t i = 0; i < SparseStripConfig::kMaxTexturePages; ++i) { |
| TexturePage& page = fPages[i]; |
| if (!page.isValid()) { |
| continue; |
| } |
| int32_t startRow = page.fUploadedRows; |
| int32_t totalUsedRows = |
| (page.fUsedBytes + SparseStripConfig::kAtlasWidthBytes - 1) / |
| SparseStripConfig::kAtlasWidthBytes; |
| int32_t pendingRows = totalUsedRows - startRow; |
| if (pendingRows > 0) { |
| SkASSERT(page.fTexture); |
| |
| int32_t paddedSize = totalUsedRows * SparseStripConfig::kAtlasWidthBytes; |
| if (page.fAlphaBuffer.size() < paddedSize) { |
| int32_t diff = paddedSize - page.fAlphaBuffer.size(); |
| uint8_t* ptr = page.fAlphaBuffer.append(diff); |
| std::memset(ptr, 0, diff); |
| } |
| |
| MipLevel level; |
| level.fPixels = |
| page.fAlphaBuffer.data() + (startRow * SparseStripConfig::kAtlasWidthBytes); |
| level.fRowBytes = SparseStripConfig::kAtlasWidthBytes; |
| SkIRect dstRect = |
| SkIRect::MakeXYWH(0, startRow, SparseStripConfig::kAtlasWidth, pendingRows); |
| |
| Swizzle readSwizzle = ReadSwizzleForColorType( |
| SparseStripConfig::kColorType, |
| TextureInfoPriv::ViewFormat(page.fTexture->textureInfo())); |
| TextureProxyView proxyView(page.fTexture, readSwizzle); |
| |
| SkColorInfo srcColorInfo(SparseStripConfig::kColorType, kPremul_SkAlphaType, nullptr); |
| SkColorInfo dstColorInfo(SparseStripConfig::kColorType, kPremul_SkAlphaType, nullptr); |
| UploadSource source = UploadSource::Make(fRecorder->priv().caps(), |
| proxyView, |
| srcColorInfo, |
| dstColorInfo, |
| SkSpan<const MipLevel>(&level, 1), |
| dstRect); |
| |
| if (dc) { |
| dc->recordUpload(fRecorder, source, nullptr); |
| } |
| |
| page.fUploadedRows = totalUsedRows; |
| page.fUsedBytes = paddedSize; |
| } |
| } |
| |
| // At flush time, retire the older GPU slot if both were valid. |
| if (fPages[0].isValid() && fPages[1].isValid()) { |
| // If we're on the null texture, the active slot is instead the last gpu slot. |
| int32_t activeGpuSlot = |
| (fActiveSlot < SparseStripConfig::kMaxTexturePages) ? fActiveSlot : fLastGpuSlot; |
| int32_t oldSlot = activeGpuSlot ^ 1; |
| SkASSERT(fPages[oldSlot].isValid()); |
| fPages[oldSlot].reset(); |
| fLastRetiredSlot = oldSlot; |
| } |
| } |
| |
| bool AlphaAtlasManager::resolveNullCaps(EndCaps* ends) { |
| SkASSERT(ends && ends->hasNullCaps() && fNullBuffer.size() > 0); |
| int32_t firstNull = ends->firstNullCapIndex(); |
| auto& caps = ends->caps(); |
| SkASSERT(firstNull >= 0 && firstNull < caps.size()); |
| |
| // Use the slot that was freed at flush time |
| if (fLastRetiredSlot == kInvalidSlot) { |
| return false; |
| } |
| SkASSERT(!fPages[fLastRetiredSlot].isValid()); |
| int32_t currentSlot = fLastRetiredSlot; |
| |
| // Allocate currentSlot up to max capacity |
| int32_t nullBytes = fNullBuffer.size() - caps[firstNull].fAlphaIndex; |
| if (!this->createPageInSlot(currentSlot, |
| std::min(nullBytes, SparseStripConfig::kMaxAtlasBytes))) { |
| return false; |
| } |
| fLastRetiredSlot = kInvalidSlot; |
| |
| // Copy the data from the null buffer into the new texture pages. |
| int32_t nextNull = firstNull; |
| int32_t roundStartCap = firstNull; |
| for (; nextNull < caps.size(); ++nextNull) { |
| int32_t alphaIndex = caps[nextNull].fAlphaIndex; |
| int32_t capBytes = caps[nextNull].fWidth * SparseStripConfig::kTileHeight; |
| SkASSERT(capBytes > 0); |
| |
| // If the run exceeds the current slot, try to instantiate the other page |
| if (fPages[currentSlot].fUsedBytes + capBytes > fPages[currentSlot].fCapacityBytes) { |
| int32_t otherSlot = currentSlot ^ 1; |
| if (!fPages[otherSlot].isValid() || fPages[otherSlot].fUsedBytes == 0) { |
| if (fPages[otherSlot].isValid()) { |
| fPages[otherSlot].reset(); |
| } |
| int32_t remainingBytes = fNullBuffer.size() - alphaIndex; |
| if (!this->createPageInSlot( |
| otherSlot, |
| std::min(remainingBytes, SparseStripConfig::kMaxAtlasBytes))) { |
| break; |
| } |
| if (fPages[otherSlot].fUsedBytes + capBytes > |
| fPages[otherSlot].fCapacityBytes) { |
| fPages[otherSlot].reset(); |
| break; |
| } |
| currentSlot = otherSlot; |
| } else { |
| // Both slots have been filled for this round |
| break; |
| } |
| } |
| |
| TexturePage& page = fPages[currentSlot]; |
| uint8_t* dst = page.fAlphaBuffer.append(capBytes); |
| std::memcpy(dst, fNullBuffer.data() + alphaIndex, capBytes); |
| |
| caps[nextNull].fTexPage = static_cast<uint16_t>(currentSlot); |
| caps[nextNull].fAlphaIndex = page.fUsedBytes; |
| page.fUsedBytes += capBytes; |
| } |
| |
| if (nextNull == roundStartCap) { |
| // Made zero progress |
| return false; |
| } |
| |
| ends->setDrawStartIndex(roundStartCap); |
| if (nextNull == caps.size()) { |
| // We're done, no more rounds of flushing are necessary. |
| ends->clearNullCaps(); |
| fNullBuffer.clear(); |
| fNullBufferUsedBytes = 0; |
| } else { |
| ends->setFirstNullCapIndex(nextNull); |
| } |
| |
| this->populateProxies(ends); |
| |
| fActiveSlot = currentSlot; |
| fLastGpuSlot = currentSlot; |
| return true; |
| } |
| |
| void AlphaAtlasManager::populateProxies(EndCaps* ends) const { |
| int32_t startSlot = 0; |
| while (startSlot < SparseStripConfig::kMaxTexturePages && !fPages[startSlot].isValid()) { |
| startSlot++; |
| } |
| SkASSERT(startSlot < SparseStripConfig::kMaxTexturePages); |
| |
| sk_sp<TextureProxy> lastValid = fPages[startSlot].fTexture; |
| for (int32_t i = 0; i < SparseStripConfig::kMaxTexturePages; ++i) { |
| int32_t slot = (startSlot + i) % SparseStripConfig::kMaxTexturePages; |
| if (fPages[slot].isValid()) { |
| lastValid = fPages[slot].fTexture; |
| } |
| ends->setProxy(slot, lastValid); |
| } |
| } |
| |
| void AlphaAtlasManager::freeGpuResources() { |
| for (int32_t i = 0; i < SparseStripConfig::kMaxTexturePages; ++i) { |
| fPages[i].reset(); |
| } |
| fNullBuffer.clear(); |
| fNullBufferUsedBytes = 0; |
| fActiveSlot = 0; |
| fLastGpuSlot = 0; |
| fLastRetiredSlot = kInvalidSlot; |
| fNextPageRowCount = SparseStripConfig::kInitialAtlasRows; |
| } |
| |
| } // namespace skgpu::graphite |