blob: c7b645627c84013e4c39921433999ce4c7cf06ed [file] [log] [blame]
/*
* Copyright 2022 Rive
*/
#include "rive/pls/pls_render_context.hpp"
#include "gr_inner_fan_triangulator.hpp"
#include "pls_path.hpp"
#include "pls_paint.hpp"
#include "rive/math/math_types.hpp"
#include <string_view>
namespace rive::pls
{
// Overallocate GPU resources by 25% of current usage, in order to create padding for increase.
constexpr static double kGPUResourcePadding = 1.25;
// When we exceed the capacity of a GPU resource mid-flush, double it immediately.
constexpr static double kGPUResourceIntermediateGrowthFactor = 2;
constexpr size_t kMinSimpleColorRampRows = 1;
constexpr size_t kMaxSimpleColorRampRows = 256; // 65k simple gradients.
constexpr size_t kMinComplexGradients = 31;
constexpr size_t kMinGradTextureHeight = kMinSimpleColorRampRows + kMinComplexGradients;
constexpr size_t kMaxGradTextureHeight = 2048; // TODO: Move this variable to PlatformFeatures.
constexpr size_t kMaxComplexGradients = kMaxGradTextureHeight - kMaxSimpleColorRampRows;
constexpr size_t kMinTessTextureHeight = 32;
constexpr size_t kMaxTessTextureHeight = 2048; // GL_MAX_TEXTURE_SIZE spec minimum.
constexpr size_t kMaxTessellationVertices = kMaxTessTextureHeight * kTessTextureWidth;
uint32_t PLSRenderContext::ShaderFeatures::getPreprocessorDefines(SourceType sourceType) const
{
uint32_t defines = 0;
if (programFeatures.blendTier != BlendTier::srcOver)
{
defines |= PreprocessorDefines::ENABLE_ADVANCED_BLEND;
}
if (programFeatures.enablePathClipping)
{
defines |= PreprocessorDefines::ENABLE_PATH_CLIPPING;
}
if (sourceType != SourceType::vertexOnly)
{
if (fragmentFeatures.enableEvenOdd)
{
defines |= PreprocessorDefines::ENABLE_EVEN_ODD;
}
if (programFeatures.blendTier == BlendTier::advancedHSL)
{
defines |= PreprocessorDefines::ENABLE_HSL_BLEND_MODES;
}
}
return defines;
}
PLSRenderContext::BlendTier PLSRenderContext::BlendTierForBlendMode(PLSBlendMode blendMode)
{
switch (blendMode)
{
case PLSBlendMode::srcOver:
return BlendTier::srcOver;
case PLSBlendMode::screen:
case PLSBlendMode::overlay:
case PLSBlendMode::darken:
case PLSBlendMode::lighten:
case PLSBlendMode::colorDodge:
case PLSBlendMode::colorBurn:
case PLSBlendMode::hardLight:
case PLSBlendMode::softLight:
case PLSBlendMode::difference:
case PLSBlendMode::exclusion:
case PLSBlendMode::multiply:
return BlendTier::advanced;
case PLSBlendMode::hue:
case PLSBlendMode::saturation:
case PLSBlendMode::color:
case PLSBlendMode::luminosity:
return BlendTier::advancedHSL;
}
RIVE_UNREACHABLE();
}
inline GradientContentKey::GradientContentKey(rcp<const PLSGradient> gradient) :
m_gradient(std::move(gradient))
{}
inline GradientContentKey::GradientContentKey(GradientContentKey&& other) :
m_gradient(std::move(other.m_gradient))
{}
bool GradientContentKey::operator==(const GradientContentKey& other) const
{
if (m_gradient.get() == other.m_gradient.get())
{
return true;
}
else
{
return m_gradient->count() == other.m_gradient->count() &&
!memcmp(m_gradient->stops(),
other.m_gradient->stops(),
m_gradient->count() * sizeof(float)) &&
!memcmp(m_gradient->colors(),
other.m_gradient->colors(),
m_gradient->count() * sizeof(ColorInt));
}
}
size_t DeepHashGradient::operator()(const GradientContentKey& key) const
{
const PLSGradient* grad = key.gradient();
std::hash<std::string_view> hash;
size_t x = hash(std::string_view(reinterpret_cast<const char*>(grad->stops()),
grad->count() * sizeof(float)));
size_t y = hash(std::string_view(reinterpret_cast<const char*>(grad->colors()),
grad->count() * sizeof(ColorInt)));
return x ^ y;
}
PLSRenderContext::PLSRenderContext(const PlatformFeatures& platformFeatures) :
m_platformFeatures(platformFeatures),
m_maxPathID(MaxPathID(m_platformFeatures.pathIDGranularity))
{}
PLSRenderContext::~PLSRenderContext()
{
// Always call flush() to avoid deadlock.
assert(!m_didBeginFrame);
}
void PLSRenderContext::resetGPUResources()
{
assert(!m_didBeginFrame);
// Reset all GPU allocations to the minimum size.
allocateGPUResources(GPUResourceLimits{}, [](size_t targetSize, size_t currentSize) {
return targetSize != currentSize;
});
// Zero out m_maxRecentResourceUsage.
m_maxRecentResourceUsage = {};
}
void PLSRenderContext::shrinkGPUResourcesToFit()
{
assert(!m_didBeginFrame);
// Shrink GPU allocations to 125% of their maximum recent usage, and only if it would reduce
// memory by 1/3 or more.
allocateGPUResources(m_maxRecentResourceUsage.makeScaled(kGPUResourcePadding),
[](size_t targetSize, size_t currentSize) {
// Only shrink if it would reduce memory by at least 1/3.
return targetSize <= currentSize * 2 / 3;
});
// Zero out m_maxRecentResourceUsage for the next interval.
m_maxRecentResourceUsage = {};
}
void PLSRenderContext::growExceededGPUResources(const GPUResourceLimits& targetLimits,
double scaleFactor)
{
// Reallocate any GPU resource whose size in 'targetLimits' is larger than its size in
// 'm_currentResourceLimits'.
//
// The new allocation size will be "targetLimits.<resource> * scaleFactor".
allocateGPUResources(
targetLimits.makeScaledIfLarger(m_currentResourceLimits, scaleFactor),
[](size_t targetSize, size_t currentSize) { return targetSize > currentSize; });
}
// How tall to make a resource texture in order to support the given number of items.
template <size_t WidthInItems> constexpr static size_t resource_texture_height(size_t itemCount)
{
return (itemCount + WidthInItems - 1) / WidthInItems;
}
void PLSRenderContext::allocateGPUResources(
const GPUResourceLimits& targets,
std::function<bool(size_t targetSize, size_t currentSize)> shouldReallocate)
{
#if 0
class Logger
{
public:
void logChangedSize(const char* name, size_t oldSize, size_t newSize, size_t newSizeInBytes)
{
if (!m_hasChanged)
{
printf("PLSRenderContext::allocateGPUResources():\n");
m_hasChanged = true;
}
printf(" resize %s: %zu -> %zu (%zu KiB)\n",
name,
oldSize,
newSize,
newSizeInBytes >> 10);
}
void countResourceSize(size_t sizeInBytes) { m_totalSizeInBytes += sizeInBytes; }
~Logger()
{
if (m_hasChanged)
{
printf(" TOTAL GPU resource usage: %zu KiB\n", m_totalSizeInBytes >> 10);
}
}
private:
bool m_hasChanged = false;
size_t m_totalSizeInBytes = 0;
} logger;
#define LOG_CHANGED_SIZE(NAME, OLD_SIZE, NEW_SIZE, NEW_SIZE_IN_BYTES) \
logger.logChangedSize(NAME, OLD_SIZE, NEW_SIZE, NEW_SIZE_IN_BYTES)
#define COUNT_RESOURCE_SIZE(SIZE_IN_BYTES) logger.countResourceSize(SIZE_IN_BYTES)
#else
#define LOG_CHANGED_SIZE(NAME, OLD_SIZE, NEW_SIZE, NEW_SIZE_IN_BYTES)
#define COUNT_RESOURCE_SIZE(SIZE_IN_BYTES)
#endif
// One-time allocation of the uniform buffer ring.
if (m_uniformBuffer.impl() == nullptr)
{
m_uniformBuffer.reset(makeUniformBufferRing(sizeof(FlushUniforms)));
}
COUNT_RESOURCE_SIZE(m_uniformBuffer.totalSizeInBytes());
// Path data texture ring.
constexpr size_t kMinPathIDCount = kPathTextureWidthInItems * 32; // 32 texels tall.
size_t targetMaxPathID = resource_texture_height<kPathTextureWidthInItems>(targets.maxPathID) *
kPathTextureWidthInItems;
targetMaxPathID = std::clamp(targetMaxPathID, kMinPathIDCount, m_maxPathID);
size_t targetPathTextureHeight =
resource_texture_height<kPathTextureWidthInItems>(targetMaxPathID);
size_t currentPathTextureHeight =
resource_texture_height<kPathTextureWidthInItems>(m_currentResourceLimits.maxPathID);
if (shouldReallocate(targetPathTextureHeight, currentPathTextureHeight))
{
assert(!m_pathBuffer.mapped());
m_pathBuffer.reset(makeTexelBufferRing(TexelBufferRing::Format::rgba32ui,
kPathTextureWidthInItems,
targetPathTextureHeight,
kPathTexelsPerItem,
kPathTextureIdx,
TexelBufferRing::Filter::nearest));
LOG_CHANGED_SIZE("path texture height",
currentPathTextureHeight,
targetPathTextureHeight,
m_pathBuffer.totalSizeInBytes());
m_currentResourceLimits.maxPathID = targetMaxPathID;
}
COUNT_RESOURCE_SIZE(m_pathBuffer.totalSizeInBytes());
// Contour data texture ring.
constexpr size_t kMinContourIDCount = kContourTextureWidthInItems * 32; // 32 texels tall.
size_t targetMaxContourID =
resource_texture_height<kContourTextureWidthInItems>(targets.maxContourID) *
kContourTextureWidthInItems;
targetMaxContourID = std::clamp(targetMaxContourID, kMinContourIDCount, kMaxContourID);
size_t targetContourTextureHeight =
resource_texture_height<kContourTextureWidthInItems>(targetMaxContourID);
size_t currentContourTextureHeight =
resource_texture_height<kContourTextureWidthInItems>(m_currentResourceLimits.maxContourID);
if (shouldReallocate(targetContourTextureHeight, currentContourTextureHeight))
{
assert(!m_contourBuffer.mapped());
m_contourBuffer.reset(makeTexelBufferRing(TexelBufferRing::Format::rgba32ui,
kContourTextureWidthInItems,
targetContourTextureHeight,
kContourTexelsPerItem,
pls::kContourTextureIdx,
TexelBufferRing::Filter::nearest));
LOG_CHANGED_SIZE("contour texture height",
currentContourTextureHeight,
targetContourTextureHeight,
m_contourBuffer.totalSizeInBytes());
m_currentResourceLimits.maxContourID = targetMaxContourID;
}
COUNT_RESOURCE_SIZE(m_contourBuffer.totalSizeInBytes());
// Simple gradient color ramp pixel unpack buffer ring.
size_t targetSimpleGradientRows =
resource_texture_height<kGradTextureWidthInSimpleRamps>(targets.maxSimpleGradients);
targetSimpleGradientRows =
std::clamp(targetSimpleGradientRows, kMinSimpleColorRampRows, kMaxSimpleColorRampRows);
assert(m_currentResourceLimits.maxSimpleGradients % kGradTextureWidthInSimpleRamps == 0);
assert(m_reservedGradTextureRowsForSimpleRamps ==
resource_texture_height<kGradTextureWidthInSimpleRamps>(
m_currentResourceLimits.maxSimpleGradients));
if (shouldReallocate(targetSimpleGradientRows, m_reservedGradTextureRowsForSimpleRamps))
{
assert(!m_simpleColorRampsBuffer.mapped());
m_simpleColorRampsBuffer.reset(
makePixelUnpackBufferRing(targetSimpleGradientRows * kGradTextureWidthInSimpleRamps,
sizeof(TwoTexelRamp)));
LOG_CHANGED_SIZE("maxSimpleGradients",
m_reservedGradTextureRowsForSimpleRamps * kGradTextureWidthInSimpleRamps,
targetSimpleGradientRows * kGradTextureWidthInSimpleRamps,
m_simpleColorRampsBuffer.totalSizeInBytes());
m_currentResourceLimits.maxSimpleGradients =
targetSimpleGradientRows * kGradTextureWidthInSimpleRamps;
m_reservedGradTextureRowsForSimpleRamps = targetSimpleGradientRows;
}
COUNT_RESOURCE_SIZE(m_simpleColorRampsBuffer.totalSizeInBytes());
// Instance buffer ring for rendering complex gradients.
constexpr size_t kMinComplexGradientSpans = kMinComplexGradients * 32;
constexpr size_t kMaxComplexGradientSpans = kMaxComplexGradients * 64;
size_t targetComplexGradientSpans = std::clamp(targets.maxComplexGradientSpans,
kMinComplexGradientSpans,
kMaxComplexGradientSpans);
if (shouldReallocate(targetComplexGradientSpans,
m_currentResourceLimits.maxComplexGradientSpans))
{
assert(!m_gradSpanBuffer.mapped());
m_gradSpanBuffer.reset(
makeVertexBufferRing(targetComplexGradientSpans, sizeof(GradientSpan)));
LOG_CHANGED_SIZE("maxComplexGradientSpans",
m_currentResourceLimits.maxComplexGradientSpans,
targetComplexGradientSpans,
m_gradSpanBuffer.totalSizeInBytes());
m_currentResourceLimits.maxComplexGradientSpans = targetComplexGradientSpans;
}
COUNT_RESOURCE_SIZE(m_gradSpanBuffer.totalSizeInBytes());
// Instance buffer ring for rendering path tessellation vertices.
constexpr size_t kMinTessellationSpans = kMinTessTextureHeight * kTessTextureWidth / 4;
const size_t maxTessellationSpans = kMaxTessTextureHeight * kTessTextureWidth / 8; // ~100MiB
size_t targetTessellationSpans =
std::clamp(targets.maxTessellationSpans, kMinTessellationSpans, maxTessellationSpans);
if (shouldReallocate(targetTessellationSpans, m_currentResourceLimits.maxTessellationSpans))
{
assert(!m_tessSpanBuffer.mapped());
m_tessSpanBuffer.reset(
makeVertexBufferRing(targetTessellationSpans, sizeof(TessVertexSpan)));
LOG_CHANGED_SIZE("maxTessellationSpans",
m_currentResourceLimits.maxTessellationSpans,
targetTessellationSpans,
m_tessSpanBuffer.totalSizeInBytes());
m_currentResourceLimits.maxTessellationSpans = targetTessellationSpans;
}
COUNT_RESOURCE_SIZE(m_tessSpanBuffer.totalSizeInBytes());
// Instance buffer ring for literal triangles fed directly by the CPU.
constexpr size_t kMinTriangleVertices = 3072 * 3; // 324 KiB
// Triangle vertices don't have a maximum limit; we let the other components be the limiting
// factor and allocate whatever buffer size we need at flush time.
size_t targetTriangleVertices =
std::max(targets.triangleVertexBufferSize, kMinTriangleVertices);
if (shouldReallocate(targetTriangleVertices, m_currentResourceLimits.triangleVertexBufferSize))
{
assert(!m_triangleBuffer.mapped());
m_triangleBuffer.reset(
makeVertexBufferRing(targetTriangleVertices, sizeof(TriangleVertex)));
LOG_CHANGED_SIZE("triangleVertexBufferSize",
m_currentResourceLimits.triangleVertexBufferSize,
targetTriangleVertices,
m_triangleBuffer.totalSizeInBytes());
m_currentResourceLimits.triangleVertexBufferSize = targetTriangleVertices;
}
COUNT_RESOURCE_SIZE(m_triangleBuffer.totalSizeInBytes());
// Gradient color ramp texture.
size_t targetGradTextureHeight =
std::clamp(targets.gradientTextureHeight, kMinGradTextureHeight, kMaxGradTextureHeight);
if (shouldReallocate(targetGradTextureHeight, m_currentResourceLimits.gradientTextureHeight))
{
allocateGradientTexture(targetGradTextureHeight);
LOG_CHANGED_SIZE("gradientTextureHeight",
m_currentResourceLimits.gradientTextureHeight,
targetGradTextureHeight,
targetGradTextureHeight * kGradTextureWidth * 4 * sizeof(uint8_t));
m_currentResourceLimits.gradientTextureHeight = targetGradTextureHeight;
}
COUNT_RESOURCE_SIZE((m_currentResourceLimits.gradientTextureHeight) * kGradTextureWidth * 4 *
sizeof(uint8_t));
// Texture that path tessellation data is rendered into.
size_t targetTessTextureHeight =
std::clamp(targets.tessellationTextureHeight, kMinTessTextureHeight, kMaxTessTextureHeight);
if (shouldReallocate(targetTessTextureHeight,
m_currentResourceLimits.tessellationTextureHeight))
{
allocateTessellationTexture(targetTessTextureHeight);
LOG_CHANGED_SIZE("tessellationTextureHeight",
m_currentResourceLimits.tessellationTextureHeight,
targetTessTextureHeight,
targetTessTextureHeight * kTessTextureWidth * 4 * sizeof(uint32_t));
m_currentResourceLimits.tessellationTextureHeight = targetTessTextureHeight;
}
COUNT_RESOURCE_SIZE(targetTessTextureHeight * kTessTextureWidth * 4 * sizeof(uint32_t));
}
void PLSRenderContext::beginFrame(FrameDescriptor&& frameDescriptor)
{
assert(!m_didBeginFrame);
// Auto-grow GPU allocations to service the maximum recent usage. If the recent usage is larger
// than the current allocation, scale it by an additional kGPUResourcePadding since we have to
// reallocate anyway.
// Also don't preemptively grow the resources we allocate a flush time, since we can just
// allocate the right sizes once we know exactly how big they need to be.
growExceededGPUResources(m_maxRecentResourceUsage.resetFlushTimeLimits(), kGPUResourcePadding);
m_frameDescriptor = std::move(frameDescriptor);
m_isFirstFlushOfFrame = true;
onBeginFrame();
RIVE_DEBUG_CODE(m_didBeginFrame = true);
}
uint32_t PLSRenderContext::generateClipID()
{
assert(m_didBeginFrame);
if (m_lastGeneratedClipID < m_maxPathID) // maxClipID == maxPathID.
{
++m_lastGeneratedClipID;
assert(m_clipContentID != m_lastGeneratedClipID); // Totally unexpected, but just in case.
return m_lastGeneratedClipID;
}
return 0; // There are no available clip IDs. The caller should flush and try again.
}
bool PLSRenderContext::reservePathData(size_t pathCount,
size_t contourCount,
size_t curveCount,
const TessVertexCounter& tessVertexCounter)
{
assert(m_didBeginFrame);
assert(m_tessVertexCount == m_expectedTessVertexCountAtNextReserve);
// +1 for the padding vertex at the end of the tessellation data.
size_t maxTessVertexCountWithInternalPadding =
tessVertexCounter.totalVertexCountIncludingReflectionsAndPadding() + 1;
// Line breaks potentially introduce a new span. Count the maximum number of line breaks we
// might encounter. Since line breaks may also occur from the reflection, just find a simple
// upper bound.
size_t maxSpanBreakCount = (1 + maxTessVertexCountWithInternalPadding / kTessTextureWidth) * 2;
// +pathCount for a span of padding vertices at the beginning of each path.
// +1 for the padding vertex at the end of the entire tessellation texture (in case this happens
// to be the final batch of paths in the flush).
size_t maxPaddingVertexSpans = pathCount + 1;
size_t maxTessellationSpans = maxPaddingVertexSpans + curveCount + maxSpanBreakCount;
// Guard against the case where a single draw overwhelms our GPU resources. Since nothing has
// been mapped yet on the first draw, we have a unique opportunity at this time to grow our
// resources if needed.
if (m_currentPathID == 0)
{
GPUResourceLimits newLimits{};
bool needsRealloc = false;
if (newLimits.maxPathID < pathCount)
{
newLimits.maxPathID = pathCount;
needsRealloc = true;
}
if (newLimits.maxContourID < contourCount)
{
newLimits.maxContourID = contourCount;
needsRealloc = true;
}
if (newLimits.maxTessellationSpans < maxTessellationSpans)
{
newLimits.maxTessellationSpans = maxTessellationSpans;
needsRealloc = true;
}
assert(!m_pathBuffer.mapped());
assert(!m_contourBuffer.mapped());
assert(!m_tessSpanBuffer.mapped());
if (needsRealloc)
{
// The very first draw of the flush overwhelmed our GPU resources. Since we haven't
// mapped any buffers yet, grow these buffers to double the size that overwhelmed them.
growExceededGPUResources(newLimits, kGPUResourceIntermediateGrowthFactor);
}
}
m_pathBuffer.ensureMapped();
m_contourBuffer.ensureMapped();
m_tessSpanBuffer.ensureMapped();
// Does the path fit in our current buffers?
if (m_currentPathID + pathCount <= m_currentResourceLimits.maxPathID &&
m_currentContourID + contourCount <= m_currentResourceLimits.maxContourID &&
m_tessSpanBuffer.hasRoomFor(maxTessellationSpans) &&
m_tessVertexCount + maxTessVertexCountWithInternalPadding <= kMaxTessellationVertices)
{
assert(m_pathBuffer.hasRoomFor(pathCount));
assert(m_contourBuffer.hasRoomFor(contourCount));
RIVE_DEBUG_CODE(m_expectedTessVertexCountAtNextReserve =
m_tessVertexCount +
tessVertexCounter.totalVertexCountIncludingReflectionsAndPadding());
assert(m_expectedTessVertexCountAtNextReserve <= kMaxTessellationVertices);
return true;
}
return false;
}
bool PLSRenderContext::pushPaint(const PLSPaint* paint, PaintData* data)
{
assert(m_didBeginFrame);
if (const PLSGradient* gradient = paint->getGradient())
{
if (!pushGradient(gradient, data))
{
return false;
}
}
else
{
data->setColor(paint->getColor());
}
return true;
}
bool PLSRenderContext::pushGradient(const PLSGradient* gradient, PaintData* paintData)
{
const ColorInt* colors = gradient->colors();
const float* stops = gradient->stops();
size_t stopCount = gradient->count();
uint32_t row, left, right;
if (stopCount == 2 && stops[0] == 0)
{
// This is a simple gradient that can be implemented by a two-texel color ramp.
assert(stops[1] == 1); // PLSFactory transforms gradients so that the final stop == 1.
uint64_t simpleKey;
static_assert(sizeof(simpleKey) == sizeof(ColorInt) * 2);
RIVE_INLINE_MEMCPY(&simpleKey, gradient->colors(), sizeof(ColorInt) * 2);
uint32_t rampTexelsIdx;
auto iter = m_simpleGradients.find(simpleKey);
if (iter != m_simpleGradients.end())
{
rampTexelsIdx = iter->second; // This gradient is already in the texture.
}
else
{
if (m_simpleGradients.size() >= m_currentResourceLimits.maxSimpleGradients)
{
// We ran out of texels in the section for simple color ramps. The caller needs to
// flush and try again.
return false;
}
rampTexelsIdx = m_simpleGradients.size() * 2;
m_simpleColorRampsBuffer.ensureMapped();
m_simpleColorRampsBuffer.set_back(colors);
m_simpleGradients.insert({simpleKey, rampTexelsIdx});
}
row = rampTexelsIdx / kGradTextureWidth;
left = rampTexelsIdx % kGradTextureWidth;
right = left + 2;
}
else
{
// This is a complex gradient. Render it to an entire row of the gradient texture.
left = 0;
right = kGradTextureWidth;
GradientContentKey key(ref_rcp(gradient));
auto iter = m_complexGradients.find(key);
if (iter != m_complexGradients.end())
{
row = iter->second; // This gradient is already in the texture.
}
else
{
if (m_complexGradients.size() >= kMaxComplexGradients)
{
// We ran out of the maximum supported number of complex gradients. The caller needs
// to issue an intermediate flush.
return false;
}
// Guard against the case where a gradient draw overwhelms gradient span buffer. When
// the gradient span buffer hasn't been mapped yet, we have a unique opportunity to grow
// it if needed.
size_t spanCount = stopCount + 1;
if (!m_gradSpanBuffer.mapped())
{
if (spanCount > m_currentResourceLimits.maxComplexGradientSpans)
{
// The very first gradient draw of the flush overwhelmed our gradient span
// buffer. Since we haven't mapped the buffer yet, grow it to double the size
// that overwhelmed it.
GPUResourceLimits newLimits{};
newLimits.maxComplexGradientSpans = spanCount;
growExceededGPUResources(newLimits, kGPUResourceIntermediateGrowthFactor);
}
m_gradSpanBuffer.ensureMapped();
}
if (!m_gradSpanBuffer.hasRoomFor(spanCount))
{
// We ran out of instances for rendering complex color ramps. The caller needs to
// flush and try again.
return false;
}
// Push "GradientSpan" instances that will render each section of the color ramp.
ColorInt lastColor = colors[0];
uint32_t lastXFixed = 0;
// The viewport will start at m_reservedGradTextureRowsForSimpleRamps when rendering
// color ramps.
uint32_t y = static_cast<uint32_t>(m_complexGradients.size());
// "stop * w + .5" converts a stop position to an x-coordinate in the gradient texture.
// Stops should be aligned (ideally) on pixel centers to prevent bleed.
// Render half-pixel-wide caps at the beginning and end to ensure the boundary pixels
// get filled.
float w = kGradTextureWidth - 1.f;
for (size_t i = 0; i < stopCount; ++i)
{
float x = stops[i] * w + .5f;
uint32_t xFixed = static_cast<uint32_t>(x * (65536.f / kGradTextureWidth));
assert(lastXFixed <= xFixed && xFixed < 65536);
m_gradSpanBuffer.set_back(lastXFixed, xFixed, y, lastColor, colors[i]);
lastColor = colors[i];
lastXFixed = xFixed;
}
m_gradSpanBuffer.set_back(lastXFixed, 65535u, y, lastColor, lastColor);
row = m_reservedGradTextureRowsForSimpleRamps + m_complexGradients.size();
m_complexGradients.emplace(std::move(key), row);
}
}
paintData->setGradient(row, left, right, gradient->coeffs());
return true;
}
void PLSRenderContext::pushPath(PatchType patchType,
const Mat2D& matrix,
float strokeRadius,
FillRule fillRule,
PaintType paintType,
uint32_t clipID,
PLSBlendMode blendMode,
const PaintData& paintData,
uint32_t tessVertexCount,
uint32_t paddingVertexCount)
{
assert(m_didBeginFrame);
assert(m_tessVertexCount == m_expectedTessVertexCountAtEndOfPath);
assert(m_mirroredTessLocation == m_expectedMirroredTessLocationAtEndOfPath);
m_currentPathIsStroked = strokeRadius != 0;
m_currentPathNeedsMirroredContours = !m_currentPathIsStroked;
m_pathBuffer.set_back(matrix, strokeRadius, fillRule, paintType, clipID, blendMode, paintData);
++m_currentPathID;
assert(0 < m_currentPathID && m_currentPathID <= m_maxPathID);
assert(m_currentPathID == m_pathBuffer.bytesWritten() / sizeof(PathData));
auto drawType = patchType == PatchType::midpointFan ? DrawType::midpointFanPatches
: DrawType::outerCurvePatches;
uint32_t baseVertexToDraw = m_tessVertexCount + paddingVertexCount;
uint32_t patchSize = PatchSegmentSpan(drawType);
uint32_t baseInstance = baseVertexToDraw / patchSize;
// The caller is responsible to pad each path so it begins on a multiple of the patch size.
// (See PLSRenderContext::PathPaddingCalculator.)
assert(baseInstance * patchSize == baseVertexToDraw);
pushDraw(drawType, baseInstance, fillRule, paintType, clipID, blendMode);
assert(m_drawList.tail().baseVertexOrInstance + m_drawList.tail().vertexOrInstanceCount ==
baseInstance);
uint32_t vertexCountToDraw = tessVertexCount - paddingVertexCount;
if (m_currentPathNeedsMirroredContours)
{
vertexCountToDraw *= 2;
}
uint32_t instanceCount = vertexCountToDraw / patchSize;
// The caller is responsible to pad each contour so it ends on a multiple of the patch size.
assert(instanceCount * patchSize == vertexCountToDraw);
m_drawList.tail().vertexOrInstanceCount += instanceCount;
// The first curve of the path will be pre-padded with 'paddingVertexCount' tessellation
// vertices, colocated at T=0. The caller must use this argument align the beginning of the path
// on a boundary of the patch size. (See PLSRenderContext::TessVertexCounter.)
if (paddingVertexCount > 0)
{
pushPaddingVertices(paddingVertexCount);
}
size_t tessVertexCountWithoutPadding = tessVertexCount - paddingVertexCount;
if (m_currentPathNeedsMirroredContours)
{
m_tessVertexCount = m_mirroredTessLocation =
m_tessVertexCount + tessVertexCountWithoutPadding;
RIVE_DEBUG_CODE(m_expectedMirroredTessLocationAtEndOfPath =
m_mirroredTessLocation - tessVertexCountWithoutPadding);
}
RIVE_DEBUG_CODE(m_expectedTessVertexCountAtEndOfPath =
m_tessVertexCount + tessVertexCountWithoutPadding);
assert(m_expectedTessVertexCountAtEndOfPath <= m_expectedTessVertexCountAtNextReserve);
assert(m_expectedTessVertexCountAtEndOfPath <= kMaxTessellationVertices);
}
void PLSRenderContext::pushContour(Vec2D midpoint, bool closed, uint32_t paddingVertexCount)
{
assert(m_didBeginFrame);
assert(!m_pathBuffer.empty());
assert(m_currentPathIsStroked || closed);
assert(m_currentPathID != 0); // pathID can't be zero.
if (m_currentPathIsStroked)
{
midpoint.x = closed ? 1 : 0;
}
m_contourBuffer.emplace_back(midpoint,
m_currentPathID,
static_cast<uint32_t>(m_tessVertexCount));
++m_currentContourID;
assert(0 < m_currentContourID && m_currentContourID <= kMaxContourID);
assert(m_currentContourID == m_contourBuffer.bytesWritten() / sizeof(ContourData));
// The first curve of the contour will be pre-padded with 'paddingVertexCount' tessellation
// vertices, colocated at T=0. The caller must use this argument align the end of the contour on
// a boundary of the patch size. (See pls::PaddingToAlignUp().)
m_currentContourPaddingVertexCount = paddingVertexCount;
}
void PLSRenderContext::pushCubic(const Vec2D pts[4],
Vec2D joinTangent,
uint32_t additionalPLSFlags,
uint32_t parametricSegmentCount,
uint32_t polarSegmentCount,
uint32_t joinSegmentCount)
{
assert(m_didBeginFrame);
assert(0 <= parametricSegmentCount && parametricSegmentCount <= kMaxParametricSegments);
assert(0 <= polarSegmentCount && polarSegmentCount <= kMaxPolarSegments);
assert(joinSegmentCount > 0);
assert(m_currentContourID != 0); // contourID can't be zero.
// Polar and parametric segments share the same beginning and ending vertices, so the merged
// *vertex* count is equal to the sum of polar and parametric *segment* counts.
uint32_t curveMergedVertexCount = parametricSegmentCount + polarSegmentCount;
// -1 because the curve and join share an ending/beginning vertex.
uint32_t totalVertexCount =
m_currentContourPaddingVertexCount + curveMergedVertexCount + joinSegmentCount - 1;
// Only the first curve of a contour gets padding vertices.
m_currentContourPaddingVertexCount = 0;
if (m_currentPathNeedsMirroredContours)
{
pushMirroredTessellationSpans(pts,
joinTangent,
totalVertexCount,
parametricSegmentCount,
polarSegmentCount,
joinSegmentCount,
m_currentContourID | additionalPLSFlags);
}
else
{
pushTessellationSpans(pts,
joinTangent,
totalVertexCount,
parametricSegmentCount,
polarSegmentCount,
joinSegmentCount,
m_currentContourID | additionalPLSFlags);
}
}
void PLSRenderContext::pushPaddingVertices(uint32_t count)
{
constexpr static Vec2D kEmptyCubic[4]{};
// This is guaranteed to not collide with a neighboring contour ID.
constexpr static uint32_t kInvalidContourID = 0;
assert(m_tessVertexCount == m_expectedTessVertexCountAtEndOfPath);
RIVE_DEBUG_CODE(m_expectedTessVertexCountAtEndOfPath = m_tessVertexCount + count;)
assert(m_expectedTessVertexCountAtEndOfPath <= kMaxTessellationVertices);
pushTessellationSpans(kEmptyCubic, {0, 0}, count, 0, 0, 1, kInvalidContourID);
assert(m_tessVertexCount == m_expectedTessVertexCountAtEndOfPath);
}
RIVE_ALWAYS_INLINE void PLSRenderContext::pushTessellationSpans(const Vec2D pts[4],
Vec2D joinTangent,
uint32_t totalVertexCount,
uint32_t parametricSegmentCount,
uint32_t polarSegmentCount,
uint32_t joinSegmentCount,
uint32_t contourIDWithFlags)
{
uint32_t y = m_tessVertexCount / kTessTextureWidth;
int32_t x0 = m_tessVertexCount % kTessTextureWidth;
int32_t x1 = x0 + totalVertexCount;
for (;;)
{
m_tessSpanBuffer.set_back(pts,
joinTangent,
static_cast<float>(y),
x0,
x1,
parametricSegmentCount,
polarSegmentCount,
joinSegmentCount,
contourIDWithFlags);
if (x1 > static_cast<int32_t>(kTessTextureWidth))
{
// The span was too long to fit on the current line. Wrap and draw it again, this
// time behind the left edge of the texture so we capture what got clipped off last
// time.
++y;
x0 -= kTessTextureWidth;
x1 -= kTessTextureWidth;
continue;
}
break;
}
assert(y == (m_tessVertexCount + totalVertexCount - 1) / kTessTextureWidth);
m_tessVertexCount += totalVertexCount;
assert(m_tessVertexCount <= m_expectedTessVertexCountAtEndOfPath);
}
RIVE_ALWAYS_INLINE void PLSRenderContext::pushMirroredTessellationSpans(
const Vec2D pts[4],
Vec2D joinTangent,
uint32_t totalVertexCount,
uint32_t parametricSegmentCount,
uint32_t polarSegmentCount,
uint32_t joinSegmentCount,
uint32_t contourIDWithFlags)
{
int32_t y = m_tessVertexCount / kTessTextureWidth;
int32_t x0 = m_tessVertexCount % kTessTextureWidth;
int32_t x1 = x0 + totalVertexCount;
uint32_t reflectionY = (m_mirroredTessLocation - 1) / kTessTextureWidth;
int32_t reflectionX0 = (m_mirroredTessLocation - 1) % kTessTextureWidth + 1;
int32_t reflectionX1 = reflectionX0 - totalVertexCount;
for (;;)
{
m_tessSpanBuffer.set_back(pts,
joinTangent,
static_cast<float>(y),
x0,
x1,
static_cast<float>(reflectionY),
reflectionX0,
reflectionX1,
parametricSegmentCount,
polarSegmentCount,
joinSegmentCount,
contourIDWithFlags);
if (x1 > static_cast<int32_t>(kTessTextureWidth) || reflectionX1 < 0)
{
// Either the span or its reflection was too long to fit on the current line. Wrap and
// draw one both of them both again, this time behind the opposite edge of the texture
// so we capture what got clipped off last time.
++y;
x0 -= kTessTextureWidth;
x1 -= kTessTextureWidth;
--reflectionY;
reflectionX0 += kTessTextureWidth;
reflectionX1 += kTessTextureWidth;
continue;
}
break;
}
m_tessVertexCount += totalVertexCount;
assert(m_tessVertexCount <= m_expectedTessVertexCountAtEndOfPath);
m_mirroredTessLocation -= totalVertexCount;
assert(m_mirroredTessLocation >= m_expectedMirroredTessLocationAtEndOfPath);
}
void PLSRenderContext::pushInteriorTriangulation(GrInnerFanTriangulator* triangulator,
PaintType paintType,
uint32_t clipID,
PLSBlendMode blendMode)
{
pushDraw(DrawType::interiorTriangulation,
0,
triangulator->fillRule(),
paintType,
clipID,
blendMode);
m_maxTriangleVertexCount += triangulator->maxVertexCount();
triangulator->setPathID(m_currentPathID);
m_drawList.tail().triangulator = triangulator;
}
void PLSRenderContext::pushDraw(DrawType drawType,
size_t baseVertex,
FillRule fillRule,
PaintType paintType,
uint32_t clipID,
PLSBlendMode blendMode)
{
if (m_drawList.empty() || m_drawList.tail().drawType != drawType)
{
m_drawList.emplace_back(this, drawType, baseVertex);
++m_drawListCount;
}
ShaderFeatures* shaderFeatures = &m_drawList.tail().shaderFeatures;
if (blendMode > PLSBlendMode::srcOver)
{
assert(paintType != PaintType::clipReplace);
shaderFeatures->programFeatures.blendTier =
std::max(shaderFeatures->programFeatures.blendTier, BlendTierForBlendMode(blendMode));
}
if (clipID != 0)
{
shaderFeatures->programFeatures.enablePathClipping = true;
}
if (fillRule == FillRule::evenOdd)
{
shaderFeatures->fragmentFeatures.enableEvenOdd = true;
}
}
template <typename T> bool bits_equal(const T* a, const T* b)
{
return memcmp(a, b, sizeof(T)) == 0;
}
void PLSRenderContext::flush(FlushType flushType)
{
assert(m_didBeginFrame);
if (flushType == FlushType::intermediate)
{
// We might not have pushed as many tessellation vertices as expected if we ran out of room
// for the paint and had to flush.
assert(m_tessVertexCount <= m_expectedTessVertexCountAtNextReserve);
}
else
{
assert(m_tessVertexCount == m_expectedTessVertexCountAtNextReserve);
}
assert(m_tessVertexCount == m_expectedTessVertexCountAtEndOfPath);
assert(m_mirroredTessLocation == m_expectedMirroredTessLocationAtEndOfPath);
// The final vertex of the final patch of each contour crosses over into the next contour. (This
// is how we wrap around back to the beginning.) Therefore, the final contour of the flush needs
// an out-of-contour vertex to cross into as well, so we emit a padding vertex here at the end.
if (!m_tessSpanBuffer.empty())
{
pushPaddingVertices(1);
}
// Since we don't write these resources until flush time, we can wait to resize them until now,
// when we know exactly how large they need to be.
GPUResourceLimits newLimitsForFlushTimeResources{};
bool needsFlushTimeRealloc = false;
assert(m_triangleBuffer.capacity() == m_currentResourceLimits.triangleVertexBufferSize);
if (m_currentResourceLimits.triangleVertexBufferSize < m_maxTriangleVertexCount)
{
newLimitsForFlushTimeResources.triangleVertexBufferSize = m_maxTriangleVertexCount;
needsFlushTimeRealloc = true;
}
size_t requiredGradTextureHeight =
m_reservedGradTextureRowsForSimpleRamps + m_complexGradients.size();
if (m_currentResourceLimits.gradientTextureHeight < requiredGradTextureHeight)
{
newLimitsForFlushTimeResources.gradientTextureHeight = requiredGradTextureHeight;
needsFlushTimeRealloc = true;
}
size_t requiredTessTextureHeight =
resource_texture_height<kTessTextureWidth>(m_tessVertexCount);
if (m_currentResourceLimits.tessellationTextureHeight < requiredTessTextureHeight)
{
newLimitsForFlushTimeResources.tessellationTextureHeight = requiredTessTextureHeight;
needsFlushTimeRealloc = true;
}
if (needsFlushTimeRealloc)
{
growExceededGPUResources(newLimitsForFlushTimeResources, kGPUResourcePadding);
}
if (m_maxTriangleVertexCount > 0)
{
m_triangleBuffer.ensureMapped();
assert(m_triangleBuffer.hasRoomFor(m_maxTriangleVertexCount));
}
assert(m_complexGradients.size() <=
m_currentResourceLimits.gradientTextureHeight - m_reservedGradTextureRowsForSimpleRamps);
assert(m_tessVertexCount <=
m_currentResourceLimits.tessellationTextureHeight * kTessTextureWidth);
// Finish calculating our DrawList.
bool needsClipBuffer = false;
RIVE_DEBUG_CODE(size_t drawIdx = 0;)
size_t writtenTriangleVertexCount = 0;
for (Draw& draw : m_drawList)
{
switch (draw.drawType)
{
case DrawType::midpointFanPatches:
case DrawType::outerCurvePatches:
break;
case DrawType::interiorTriangulation:
{
size_t maxVertexCount = draw.triangulator->maxVertexCount();
assert(writtenTriangleVertexCount + maxVertexCount <= m_maxTriangleVertexCount);
size_t actualVertexCount = maxVertexCount;
if (maxVertexCount > 0)
{
actualVertexCount = draw.triangulator->polysToTriangles(&m_triangleBuffer);
}
assert(actualVertexCount <= maxVertexCount);
draw.baseVertexOrInstance = writtenTriangleVertexCount;
draw.vertexOrInstanceCount = actualVertexCount;
writtenTriangleVertexCount += actualVertexCount;
break;
}
}
needsClipBuffer = needsClipBuffer || draw.shaderFeatures.programFeatures.enablePathClipping;
RIVE_DEBUG_CODE(++drawIdx;)
}
assert(drawIdx == m_drawListCount);
// Determine how much to draw.
size_t simpleColorRampCount = m_simpleColorRampsBuffer.bytesWritten() / sizeof(TwoTexelRamp);
size_t gradSpanCount = m_gradSpanBuffer.bytesWritten() / sizeof(GradientSpan);
size_t tessVertexSpanCount = m_tessSpanBuffer.bytesWritten() / sizeof(TessVertexSpan);
size_t tessDataHeight = resource_texture_height<kTessTextureWidth>(m_tessVertexCount);
// Upload all non-empty buffers before flushing.
m_pathBuffer.submit();
m_contourBuffer.submit();
m_simpleColorRampsBuffer.submit();
m_gradSpanBuffer.submit();
m_tessSpanBuffer.submit();
m_triangleBuffer.submit();
// Update the uniform buffer for drawing if needed.
FlushUniforms uniformData(m_complexGradients.size(),
tessDataHeight,
m_frameDescriptor.renderTarget->width(),
m_frameDescriptor.renderTarget->height(),
m_currentResourceLimits.gradientTextureHeight,
m_platformFeatures);
if (!bits_equal(&m_cachedUniformData, &uniformData))
{
m_uniformBuffer.ensureMapped();
m_uniformBuffer.emplace_back(uniformData);
m_uniformBuffer.submit();
m_cachedUniformData = uniformData;
}
FlushDescriptor flushDesc;
flushDesc.flushType = flushType;
flushDesc.loadAction =
m_isFirstFlushOfFrame ? frameDescriptor().loadAction : LoadAction::preserveRenderTarget;
flushDesc.complexGradSpanCount = gradSpanCount;
flushDesc.tessVertexSpanCount = tessVertexSpanCount;
flushDesc.simpleGradTexelsWidth = std::min(simpleColorRampCount * 2, kGradTextureWidth);
flushDesc.simpleGradTexelsHeight =
resource_texture_height<kGradTextureWidthInSimpleRamps>(simpleColorRampCount);
flushDesc.complexGradRowsTop = m_reservedGradTextureRowsForSimpleRamps;
flushDesc.complexGradRowsHeight = m_complexGradients.size();
flushDesc.tessDataHeight = tessDataHeight;
flushDesc.needsClipBuffer = needsClipBuffer;
onFlush(flushDesc);
m_currentFrameResourceUsage.maxPathID += m_currentPathID;
m_currentFrameResourceUsage.maxContourID += m_currentContourID;
m_currentFrameResourceUsage.maxSimpleGradients += m_simpleGradients.size();
m_currentFrameResourceUsage.maxComplexGradientSpans += gradSpanCount;
m_currentFrameResourceUsage.maxTessellationSpans += tessVertexSpanCount;
m_currentFrameResourceUsage.triangleVertexBufferSize += m_maxTriangleVertexCount;
m_currentFrameResourceUsage.gradientTextureHeight +=
resource_texture_height<kGradTextureWidthInSimpleRamps>(m_simpleGradients.size()) +
m_complexGradients.size();
m_currentFrameResourceUsage.tessellationTextureHeight +=
resource_texture_height<kTessTextureWidth>(m_tessVertexCount);
static_assert(sizeof(m_currentFrameResourceUsage) ==
sizeof(size_t) * 8); // Make sure we got every field.
if (flushType == FlushType::intermediate)
{
// Intermediate flushes in a single frame are BAD. If the current frame's accumulative usage
// (across all flushes) of any resource is larger than the current allocation, double it!
// Also don't preemptively grow the resources we allocate a flush time, since we can just
// allocate the right sizes once we know exactly how big they need to be.
growExceededGPUResources(m_currentFrameResourceUsage.resetFlushTimeLimits(),
kGPUResourceIntermediateGrowthFactor);
}
else
{
assert(flushType == FlushType::endOfFrame);
m_frameDescriptor = FrameDescriptor{};
m_maxRecentResourceUsage.accumulateMax(m_currentFrameResourceUsage);
m_currentFrameResourceUsage = GPUResourceLimits{};
RIVE_DEBUG_CODE(m_didBeginFrame = false;)
}
m_lastGeneratedClipID = 0;
m_clipContentID = 0;
m_currentPathID = 0;
m_currentContourID = 0;
m_simpleGradients.clear();
m_complexGradients.clear();
m_tessVertexCount = 0;
m_mirroredTessLocation = 0;
RIVE_DEBUG_CODE(m_expectedTessVertexCountAtNextReserve = 0);
RIVE_DEBUG_CODE(m_expectedTessVertexCountAtEndOfPath = 0);
RIVE_DEBUG_CODE(m_expectedMirroredTessLocationAtEndOfPath = 0);
m_maxTriangleVertexCount = 0;
m_isFirstFlushOfFrame = false;
m_drawList.reset();
m_drawListCount = 0;
// Delete all objects that were allocted for this flush using the TrivialBlockAllocator.
m_trivialPerFlushAllocator.reset();
}
} // namespace rive::pls