| /* | |
| * 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 { | |
| if (ends->empty()) { | |
| return; | |
| } | |
| 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 |