blob: 671c881f9556604aa016cf85ed38693044dd8c25 [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/DrawListLayer.h"
#include "include/core/SkTypes.h"
#include "include/gpu/graphite/Recorder.h"
#include "src/core/SkTraceEvent.h"
#include "src/gpu/graphite/DrawPass.h"
#include "src/gpu/graphite/DrawWriter.h"
#include "src/gpu/graphite/KeyContext.h"
#include "src/gpu/graphite/RecorderPriv.h"
#include "src/gpu/graphite/Renderer.h"
#include "src/gpu/graphite/geom/Geometry.h"
namespace skgpu::graphite {
void DrawListLayer::reset(LoadOp loadOp, SkColor4f color) {
DrawListBase::reset(loadOp, color);
fStorage.reset();
fLayers.reset();
fDrawCount = 0;
fOrderCounter = CompressedPaintersOrder::First();
}
std::pair<Layer*, BindingList*> DrawListLayer::searchBackwards(
const RenderStep* step,
const LayerKey& key,
SkEnumBitMask<BoundsFlags> testMask,
const DrawParams* drawParams,
CompressedPaintersOrder stop) {
// CPU performance is sensitive to increasing this value. Searching for longer *can* reduce the
// draw count and pipeline change count
static constexpr int kMaxSearchLimit = 8;
Rect::ComplementRect drawBounds{drawParams->drawBounds()};
Layer* targetLayer = nullptr;
Layer* current = fLayers.tail();
for (int limit = kMaxSearchLimit; limit > 0 && current; --limit) {
// NOTE: This test does not search within the layer's binding lists for a match. Searching
// each layer that can allow the draw adds overhead for minimal batching improvements.
// Instead the heuristic to just add to the deepest layer possible and search only that
// layer for a good binding match batches about as well in the limit.
auto result = current->test(drawBounds, testMask);
if (result & BoundsTestResult::kAllowedInLayer) {
// Allowed in the layer, so remember it. In complex scenes, we want to search deeper
// in the layer list than just the first compatible overlap we encounter. Stopping early
// reduces search time but fragments batching. Inserting early blocks subsequent draws
// from reaching those denser, later candidates (particularly when this is a clip draw
// as that propagates into the stop layer for subsequent draws).
targetLayer = current;
}
if (!SkToBool(result & BoundsTestResult::kAllowedBeforeLayer) || current->fOrder == stop) {
break;
} else {
current = current->fPrev;
}
}
SkASSERT(!targetLayer || targetLayer->fOrder >= stop);
BindingList* targetMatch = nullptr;
BindingList* forwardMerge = nullptr;
if (targetLayer) {
// `targetLayer` is non-null only if the test returned kAllowedInLayer, which means it is
// disjoint from everything else in the layer. We can safely combine it with an exact match
// or place it near a partial match.
targetMatch = targetLayer->searchBinding(key);
// if `targetMatch` is null, we could try and search `current` for a match but it would only
// be valid if the key is simple-shading and the match was the last, at which point we can
// allow for overlap. We could also try to do a more detailed per-binding list bounds check
// to see if the draw could skip past some of the bindings to find a match. However, since
// we have a targetLayer already, `current` would only be one deeper, so it's often not
// worth the trade off of additional search time.
// FIXME this didn't come up with just checking the tail binding, but maybe if we did
// per binding list bounds checks, we would find more matches?
} else if (key.isSimpleShading() && fLayers.tail()) {
// As a simple-shading draw, there is the potential to pull a previous binding list
// forward to a new layer. This must be an exact match so that we can rely on rasterization
// order resolving any overlap (since !targetLayer implies the test originally failed for
// fLayers.tail()).
if ((forwardMerge = fLayers.tail()->searchBinding(key))) {
if (!forwardMerge->fKey.isEqual(key)) {
forwardMerge = nullptr;
} else if (!forwardMerge->fPrev && !forwardMerge->fNext) {
// The tail had a single exact matching binding list, so just append to it. There
// are no other incompatible bindings whose overlap we need to worry about.
targetLayer = fLayers.tail();
targetMatch = forwardMerge;
forwardMerge = nullptr;
} // else we'll transfer the forward merge list into a new layer below
} // else didn't find a match in the last layer
} // else not forward-merge eligible and no target, so we'll put it in a new layer
if (!targetLayer) {
fOrderCounter = fOrderCounter.next();
targetLayer = fStorage.make<Layer>(fOrderCounter);
if (forwardMerge) {
fLayers.tail()->transfer(forwardMerge, targetLayer);
targetMatch = forwardMerge;
}
fLayers.addToTail(targetLayer);
}
if (!targetMatch || !targetMatch->fKey.isEqual(key)) {
// If targetMatch is just a pipeline match, we can insert right before it because such a
// match is only returned when the new draw can be ordered in front of it.
targetMatch = targetLayer->addNewBinding(&fStorage, targetMatch, key, step);
} else {
SkASSERT(targetLayer->fBindings.isInList(targetMatch));
}
return {targetLayer, targetMatch};
}
BindingList* DrawListLayer::findOrCreateBindingInLayer(Layer* layer,
BindingList* parent,
const RenderStep* step,
const LayerKey& key) {
// If we're recording a new step in the layer, there better have been a draw that searched
// backwards for the layer first!
SkASSERT(layer);
// If we have a parent step's BindingList to insert before, it must be in `layer`.
SkASSERT(!parent || layer->fBindings.isInList(parent));
BindingList* targetMatch = nullptr;
// If we don't have a parent, search through all bindings of the layer as this is the first time
// through the layer. If we do have a parent, search through the preceding bindings (exclusive).
// This is handled automatically by searchBinding's `parent` handling; when there are no
// preceding bindings (e.g. parent && !parent->fPrev), `match` will just be null.
BindingList* match = layer->searchBinding(key, parent);
if (match) {
if (match->fKey.isEqual(key)) {
targetMatch = match;
} else {
// NOTE: Treat any pipeline match as the new parent that a new binding list will be
// inserted before. Since the search started from the original parent (exclusive),
// any found pipeline match will still be before that parent.
parent = match;
}
}
if (!targetMatch) {
targetMatch = layer->addNewBinding(&fStorage, parent, key, step);
}
return targetMatch;
}
// Layer has dual purpose here:
// 1) (Producer) If recording a depth-only draw, the returned Layer* pointer is remembered as
// the earliest possible layer that a later clipped draw can be added to. This is stored on the
// ClipStack::Element that produced the depth-only draw.
// 2) (Consumer) If recording a clipped draw, the pointer is the latest layer inserted into across
// *all depth only draws* which affect this draw. If the draw has no other bounds dependencies,
// this represents the Layer that it can be directly added to.
std::pair<DrawParams*, Layer*> DrawListLayer::recordDraw(const Renderer* renderer,
const Transform& localToDevice,
const Geometry& geometry,
const Clip& clip,
DrawOrder ordering,
UniquePaintParamsID paintID,
SkEnumBitMask<DstUsage> dstUsage,
BarrierType barrierBeforeDraws,
PipelineDataGatherer* gatherer,
StorageContext* storageContext,
const StrokeStyle* stroke,
Layer* lastInsertion) {
SkASSERT(localToDevice.valid());
SkASSERT(!geometry.isEmpty() && !clip.drawBounds().isEmptyNegativeOrNaN());
// `testMask` limits what we test against when searching backwards, which is based on the
// Renderer's aggregate requirements so that the layer we find will be valid for all steps. This
// is particularly important for stencil-based renderers, which consist of a non-shading
// "producer" step, which writes into the stencil buffer, and shading "consumer" render steps
// which test against the stencil mask and clear the buffer afterwards. This guarantees
// atomicity within a single layer, where the last step finds a safe layer and all earlier steps
// are explicitly inserted before that. This minimizes pipeline switches as rendering can
// proceed through the steps in bulk.
SkEnumBitMask<BoundsFlags> testMask;
if (SkToBool(renderer->depthStencilFlags() & DepthStencilFlags::kStencil)) {
testMask |= BoundsFlags::kStencil;
}
// Draws that blend must respect painter's order, and clipping depth-only draws cannot be
// ordered in front of shading draws.
const bool isDepthOnly = !paintID.isValid();
const bool dependsOnDst = SkToBool(dstUsage & DstUsage::kDependsOnDst);
if (dependsOnDst || isDepthOnly) {
testMask |= BoundsFlags::kColor;
}
// In simple situations, we can allow overlaps within a BindingList and let GPU rasterization
// resolve the rendering order automatically. This does not apply if barriers are required,
// and it does not apply when the Renderer has multiple steps (must keep the sets of draws in
// each step disjoint so there isn't interference).
SkEnumBitMask<BoundsFlags> baseLayerMask = BoundsFlags::kNone;
if (barrierBeforeDraws != BarrierType::kNone || renderer->numRenderSteps() > 1) {
baseLayerMask |= BoundsFlags::kMustBeDisjoint;
}
// Currently, the draw params are created once per record draw call, and the pointer is passed
// to each draw call. This is storage effecient but will still introduce some pointer chasing,
// because the params will likely no longer be on the same cache line for successor render
// steps. We should test whether it is faster for each step to hold a copy of the params except
// in the case of clipped draws (which must share a copy because they are mutated later).
DrawParams* drawParams = fStorage.make<DrawParams>(this->deduplicateTransform(localToDevice),
geometry,
clip,
ordering,
stroke,
barrierBeforeDraws);
Layer* insertionLayer = nullptr;
BindingList* lastStepBinding = nullptr;
// If we're an easy draw, jump to the latestInsertion layer since we don't have to test
if (testMask == BoundsFlags::kNone && baseLayerMask == BoundsFlags::kNone) {
insertionLayer = lastInsertion ? lastInsertion : fLayers.head();
}
SkEnumBitMask<BoundsFlags> allLayerMasks;
for (int stepIndex = renderer->numRenderSteps() - 1; stepIndex >= 0; --stepIndex) {
const RenderStep* const step = renderer->steps()[stepIndex];
const bool performsShading = step->performsShading() && paintID.isValid();
if (storageContext && step->storageUniformStride() > 0) {
storageContext->recordAlignment(step->storageUniformStride(),
step->storageUniformAlignment());
}
gatherer->markOffsetAndAlign(performsShading, step->uniformAlignment());
GraphicsPipelineCache::Index pipelineIndex = fPipelineCache.insert(
{step->renderStepID(),
performsShading ? paintID : UniquePaintParamsID::Invalid()});
step->writeUniformsAndTextures(*drawParams, gatherer);
auto [combinedUniforms, combinedTextures] =
gatherer->endCombinedData(performsShading);
UniformDataCache::Index uniformIndex = combinedUniforms
? fUniformDataCache.insert(combinedUniforms)
: UniformDataCache::kInvalidIndex;
TextureDataCache::Index textureBindingIndex =
combinedTextures ? fTextureDataCache.insert(combinedTextures)
: TextureDataCache::kInvalidIndex;
// `layerMask` defines what this draw will block in new draws from going backwards. This is
// per-step so that stencil-only draws can be grouped between shading and clip draws.
SkEnumBitMask<BoundsFlags> layerMask = baseLayerMask;
if (step->depthStencilFlags() & DepthStencilFlags::kStencil) {
layerMask |= BoundsFlags::kStencil;
}
if (step->performsShading() && paintID.isValid()) {
// NOTE: This is not dependsOnDst because it represents what is written by the draw,
// not what might be read for blending the draw.
layerMask |= BoundsFlags::kColor;
}
LayerKey key{pipelineIndex,
textureBindingIndex,
fStorageBufferSupport ? UniformDataCache::kInvalidIndex : uniformIndex,
layerMask};
allLayerMasks |= layerMask;
if (!insertionLayer) {
// Since we don't have a layer yet, search from the most recent layer back.
CompressedPaintersOrder stop = lastInsertion ? lastInsertion->fOrder
: DrawOrder::kNoIntersection;
std::tie(insertionLayer, lastStepBinding) = this->searchBackwards(step,
key,
testMask,
drawParams,
stop);
} else {
// Put the earlier steps in the same layer (valid because we used BoundsFlags for the
// whole Renderer).
lastStepBinding = this->findOrCreateBindingInLayer(insertionLayer,
lastStepBinding,
step,
key);
}
SkASSERT(lastStepBinding);
lastStepBinding->addDraw(&fStorage, drawParams, uniformIndex, /*backToFront=*/dependsOnDst);
gatherer->rewindForRenderStep();
}
// This must be called once for the layer the draw's rendersteps were added into, so do it at
// the end since we'll always have the layer at this point. This uses bounds flags applying
// to whole Renderer.
SkASSERT(insertionLayer);
insertionLayer->updateForDraw(drawParams->drawBounds(), allLayerMasks);
fRenderStepCount += renderer->numRenderSteps();
fDrawCount++;
fPassBounds.join(clip.drawBounds());
fRequiresMSAA |= renderer->requiresMSAA();
fDepthStencilFlags |= renderer->depthStencilFlags();
if (dstUsage & DstUsage::kDstReadRequired) {
// For paints that read from the dst, update the bounds. It may later be determined that the
// DstReadStrategy does not require them, but they are inexpensive to track.
fDstReadBounds.join(clip.drawBounds());
}
#if defined(SK_DEBUG)
if (geometry.isCoverageMaskShape()) {
fCoverageMaskShapeDrawCount++;
}
#endif
return {drawParams, insertionLayer};
}
std::unique_ptr<DrawPass> DrawListLayer::snapDrawPass(Recorder* recorder,
StorageContext* storageContext,
sk_sp<TextureProxy> target,
const SkImageInfo& targetInfo,
const DstReadStrategy dstReadStrategy) {
TRACE_EVENT1_ALWAYS("skia.gpu", TRACE_FUNC, "draw count", fDrawCount);
std::unique_ptr<DrawPass> drawPass(new DrawPass(target,
{fLoadOp, StoreOp::kStore},
fClearColor));
DrawBufferManager* bufferMgr = recorder->priv().drawBufferManager();
DrawWriter drawWriter(&drawPass->fCommandList, bufferMgr);
UniformTracker uniformTracker(fStorageBufferSupport);
TextureTracker textureBindingTracker(&fTextureDataCache);
const bool rebindTexturesOnPipelineChange = dstReadStrategy == DstReadStrategy::kTextureCopy;
if (fStorageBufferSupport) {
SkASSERT(storageContext);
storageContext->finalizePrecachedStorageData();
}
GraphicsPipelineCache::Index lastPipeline = GraphicsPipelineCache::kInvalidIndex;
const SkIRect targetBounds = SkIRect::MakeSize(targetInfo.dimensions());
SkIRect lastScissor = targetBounds;
SkASSERT(drawPass->fTarget->isFullyLazy() ||
SkIRect::MakeSize(drawPass->fTarget->dimensions()).contains(lastScissor));
drawPass->fCommandList.setScissor(lastScissor);
// Accumulate rough pixel area touched by each pipeline
drawPass->fPipelineDrawAreas.push_back_n(fPipelineCache.count(), 0.f);
auto recordDraw = [&](const LayerKey& key,
const RenderStep* renderStep,
const Draw* draw,
bool bindingsAreInvariant,
bool startOfLayer) -> const Draw* {
SkASSERT(renderStep && draw);
const DrawParams& drawParams = *draw->fDrawParams;
bool pipelineChange = false;
bool textureBindingsChange = false;
if (!bindingsAreInvariant) {
pipelineChange = key.fPipelineIndex != lastPipeline;
textureBindingsChange =
textureBindingTracker.setCurrentTextureBindings(key.fTextureIndex) ||
(rebindTexturesOnPipelineChange && pipelineChange &&
key.fTextureIndex != TextureDataCache::kInvalidIndex);
}
// Uniforms are binding invariant when SSBOs are disabled, but it's simpler to just let
// `uniformBindingChange` eval to false more often. The uniform index must come from the
// Draw to get the right value when SSBOs are enabled.
bool uniformBindingChange =
uniformTracker.writeUniforms(fUniformDataCache, bufferMgr, draw->fUniformIndex);
drawPass->fPipelineDrawAreas[key.fPipelineIndex] += drawParams.drawBounds().area();
std::optional<SkIRect> newScissor =
renderStep->getScissor(drawParams, lastScissor, targetBounds);
if (pipelineChange) {
drawWriter.newPipelineState(renderStep->primitiveType(),
renderStep->staticDataStride(),
renderStep->appendDataStride(),
renderStep->getRenderStateFlags(),
drawParams.barrierBeforeDraws());
} else if (uniformBindingChange || textureBindingsChange || newScissor.has_value()) {
drawWriter.newDynamicState();
} else if (drawParams.barrierBeforeDraws() != BarrierType::kNone && startOfLayer) {
// Taking this branch means there were no state or pipeline changes between old layer
// and this layer's first draw. This only happens if the draws overlap, so flush the
// drawWriter since the draw requires a barrier.
drawWriter.flush();
}
if (pipelineChange) {
drawPass->fCommandList.bindGraphicsPipeline(key.fPipelineIndex);
lastPipeline = key.fPipelineIndex;
}
if (uniformBindingChange) {
uniformTracker.bindUniforms(UniformSlot::kCombinedUniforms, &drawPass->fCommandList);
}
if (textureBindingsChange) {
textureBindingTracker.bindTextures(&drawPass->fCommandList);
}
if (newScissor.has_value()) {
drawPass->fCommandList.setScissor(*newScissor);
lastScissor = *newScissor;
}
uint32_t uniformSsboIndex = fStorageBufferSupport ? uniformTracker.ssboIndex() : 0;
renderStep->writeVertices(&drawWriter, storageContext, drawParams, uniformSsboIndex);
// Either stop early on failure, or advance to the next Draw
return bufferMgr->hasMappingFailed() ? nullptr : draw->fNext;
};
for (Layer* layer : fLayers) {
for (const BindingList* list : layer->fBindings) {
SkASSERT(list->fHead); // not empty
// The first draw of the BindingList will be changing bindings
const Draw* current = recordDraw(list->fKey, list->fStep, list->fHead,
/*bindingsAreInvariant=*/false,
/*startOfLayer=*/!list->fPrev);
while (current) {
// Any remaining draws can skip checking for pipeline/texture binding changes.
current = recordDraw(list->fKey, list->fStep, current,
/*bindingsAreInvariant=*/true,
/*startOfLayer=*/false);
}
}
}
drawWriter.flush();
if (fStorageBufferSupport) {
SkASSERT(storageContext);
drawPass->fStorageBufferInfo = storageContext->finalize(bufferMgr);
if (!storageContext->isEmpty() && !drawPass->fStorageBufferInfo) SK_UNLIKELY {
SKIA_LOG_W("Failed to write Storage Data for Draw pass, dropping!");
this->reset(LoadOp::kLoad);
return nullptr;
}
}
drawPass->fBounds = fPassBounds.roundOut().asSkIRect();
drawPass->fPipelineDescs = fPipelineCache.detach();
drawPass->fSampledTextures = fTextureDataCache.detachTextures();
TRACE_EVENT_INSTANT2_ALWAYS("skia.gpu",
"DrawPass Stats",
TRACE_EVENT_SCOPE_THREAD,
"# commands", drawPass->fCommandList.count(),
"# textures", drawPass->fSampledTextures.size());
this->reset(LoadOp::kLoad);
if (bufferMgr->hasMappingFailed()) {
SKIA_LOG_W("Failed to write necessary vertex/instance data for DrawPass, dropping!");
return nullptr;
} else {
return drawPass;
}
}
} // namespace skgpu::graphite