blob: 92f62b542b8e1b8c3e2509ad8428363644d9dcf4 [file] [edit]
/*
* 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