| /* | |
| * Copyright 2022 Rive | |
| */ | |
| #include "rive/renderer/render_context.hpp" | |
| #include "gr_inner_fan_triangulator.hpp" | |
| #include "intersection_board.hpp" | |
| #include "gradient.hpp" | |
| #include "rive_render_paint.hpp" | |
| #include "rive/renderer/draw.hpp" | |
| #ifdef RIVE_CANVAS | |
| #include "rive/renderer/render_canvas.hpp" | |
| #endif | |
| #include "rive/renderer/rive_render_image.hpp" | |
| #include "rive/renderer/render_context_impl.hpp" | |
| #include "rive/gpu_texture_format.hpp" | |
| #include "rive/renderer/stack_vector.hpp" | |
| #include "rive/profiler/profiler_macros.h" | |
| #include "shaders/constants.glsl" | |
| #include <algorithm> | |
| #include <limits> | |
| #include <string_view> | |
| #ifdef RIVE_DECODERS | |
| #include "rive/decoders/bitmap_decoder.hpp" | |
| #endif | |
| #ifdef RIVE_KTX2 | |
| #include "rive/decoders/decode_ktx2.hpp" | |
| #endif | |
| #include "sort_key_builder.hpp" | |
| namespace rive::gpu | |
| { | |
| constexpr size_t kDefaultSimpleGradientCapacity = 512; | |
| constexpr size_t kDefaultComplexGradientCapacity = 1024; | |
| constexpr size_t kDefaultDrawCapacity = 2048; | |
| // TODO: Move this variable to PlatformFeatures. | |
| constexpr uint32_t kMaxTextureHeight = 2048; | |
| constexpr size_t kMaxTessellationVertexCount = | |
| kMaxTextureHeight * kTessTextureWidth; | |
| constexpr size_t kMaxTessellationPaddingVertexCount = | |
| gpu::kMidpointFanPatchSegmentSpan + // Padding at the beginning of the | |
| // tessellation texture | |
| gpu::OuterCubicPatchSegmentSpan + // Max padding between patch types in the | |
| // tessellation texture | |
| 1; // Padding at the end of the tessellation texture | |
| constexpr size_t kMaxTessellationVertexCountBeforePadding = | |
| kMaxTessellationVertexCount - kMaxTessellationPaddingVertexCount; | |
| // Metal requires vertex buffers to be 256-byte aligned. | |
| constexpr size_t kMaxTessellationAlignmentVertices = | |
| gpu::kTessVertexBufferAlignmentInElements - 1; | |
| // We can only reorder 32767 draws at a time since the one-based groupIndex | |
| // returned by IntersectionBoard is a signed 16-bit integer. | |
| constexpr size_t kMaxReorderedDrawPassCount = | |
| std::numeric_limits<int16_t>::max(); | |
| // 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; | |
| } | |
| constexpr static size_t gradient_data_height(size_t simpleRampCount, | |
| size_t complexRampCount) | |
| { | |
| return resource_texture_height<gpu::kGradTextureWidthInSimpleRamps>( | |
| simpleRampCount) + | |
| complexRampCount; | |
| } | |
| // Returns true if the current bounds poke outside of the containing bounds (and | |
| // thus would need to be clipped against them) | |
| inline bool needsScissor(IAABB currentBounds, | |
| IAABB containingBounds, | |
| uint32_t renderTargetWidth, | |
| uint32_t renderTargetHeight) | |
| { | |
| // intersect the current bounds with the screen dimensions before testing so | |
| // that if we end up outside the containing bounds on a side that is also a | |
| // screen edge it doesn't matter. | |
| return !containingBounds.contains(currentBounds.intersect( | |
| IAABB::MakeWH(renderTargetWidth, renderTargetHeight))); | |
| } | |
| inline GradientContentKey::GradientContentKey(rcp<const Gradient> 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 Gradient* 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; | |
| } | |
| RenderContext::RenderContext(std::unique_ptr<RenderContextImpl> impl) : | |
| m_impl(std::move(impl)), | |
| // -1 from m_maxPathID so we reserve a path record for the clearColor paint | |
| // (for atomic mode). This also allows us to index the storage buffers | |
| // directly by pathID. | |
| m_maxPathID(MaxPathID(m_impl->platformFeatures().pathIDGranularity) - 1) | |
| { | |
| // Validate platformFeatures: if supportsBlendAdvancedCoherentKHR is set, | |
| // supportsBlendAdvancedKHR must also be. | |
| assert(!m_impl->platformFeatures().supportsBlendAdvancedCoherentKHR || | |
| m_impl->platformFeatures().supportsBlendAdvancedKHR); | |
| #ifdef RIVE_GENERATE_FEATHER_LUT | |
| float table[GAUSSIAN_TABLE_SIZE]; | |
| generate_gausian_integral_table(table); | |
| generate_inverse_gausian_integral_table(table); | |
| #endif | |
| setResourceSizes(ResourceAllocationCounts(), /*forceRealloc =*/true); | |
| releaseResources(); | |
| } | |
| RenderContext::~RenderContext() | |
| { | |
| // Always call flush() to avoid deadlock. | |
| assert(!m_didBeginFrame); | |
| // Delete the logical flushes before the block allocators let go of their | |
| // allocations. | |
| m_logicalFlushes.clear(); | |
| #ifdef RIVE_CANVAS | |
| m_oreContext.reset(); | |
| #endif | |
| } | |
| const gpu::PlatformFeatures& RenderContext::platformFeatures() const | |
| { | |
| return m_impl->platformFeatures(); | |
| } | |
| rcp<RenderBuffer> RenderContext::makeRenderBuffer(RenderBufferType type, | |
| RenderBufferFlags flags, | |
| size_t sizeInBytes) | |
| { | |
| return m_impl->makeRenderBuffer(type, flags, sizeInBytes); | |
| } | |
| #ifdef RIVE_CANVAS | |
| rcp<RenderCanvas> RenderContext::makeRenderCanvas(uint32_t width, | |
| uint32_t height) | |
| { | |
| return m_impl->makeRenderCanvas(width, height); | |
| } | |
| rcp<RenderCanvas> RenderContext::makeDeferredRenderCanvas(uint32_t width, | |
| uint32_t height) | |
| { | |
| return m_impl->makeDeferredRenderCanvas(width, height); | |
| } | |
| rive::ore::Context* RenderContext::ore() | |
| { | |
| if (m_oreContext == nullptr) | |
| m_oreContext = m_impl->makeOreContext(); | |
| return m_oreContext.get(); | |
| } | |
| #endif | |
| rcp<RenderImage> RenderContext::decodeImage(Span<const uint8_t> encodedBytes) | |
| { | |
| RIVE_PROF_SCOPE_L(1) | |
| rcp<Texture> texture = m_impl->platformDecodeImageTexture(encodedBytes); | |
| #ifdef RIVE_KTX2 | |
| // KTX2 magic = «KTX 20»\r\n\x1A\n. Match the first 4 bytes for the cheap | |
| // dispatch; full magic is re-checked inside DecodeKtx2. | |
| if (texture == nullptr && encodedBytes.size() >= 12 && | |
| encodedBytes[0] == 0xAB && encodedBytes[1] == 0x4B && | |
| encodedBytes[2] == 0x54 && encodedBytes[3] == 0x58) | |
| { | |
| const Ktx2HwSupport hwSupport = { | |
| platformFeatures().supportsTextureCompressionBC, | |
| platformFeatures().supportsTextureCompressionASTC, | |
| platformFeatures().supportsTextureCompressionETC2, | |
| }; | |
| Ktx2DecodeResult ktx2; | |
| if (DecodeKtx2(encodedBytes.data(), | |
| encodedBytes.size(), | |
| ktx2, | |
| hwSupport)) | |
| { | |
| // KTX2 provides the full level chain (or just level 0). The | |
| // backends never auto-generate; whatever the file ships with is | |
| // exactly what gets uploaded. | |
| texture = m_impl->makeImageTexture(ktx2.pixelWidth, | |
| ktx2.pixelHeight, | |
| ktx2.levelCount, | |
| ktx2.format, | |
| ktx2.blocks.data(), | |
| ktx2.blockWidth, | |
| ktx2.blockHeight, | |
| ktx2.srgb); | |
| } | |
| } | |
| #endif | |
| #ifdef RIVE_DECODERS | |
| if (texture == nullptr) | |
| { | |
| auto bitmap = Bitmap::decode(encodedBytes.data(), encodedBytes.size()); | |
| if (bitmap) | |
| { | |
| // Bitmap::decode always produces RGBA — convert if needed. | |
| if (bitmap->pixelFormat() != Bitmap::PixelFormat::RGBAPremul) | |
| { | |
| bitmap->pixelFormat(Bitmap::PixelFormat::RGBAPremul); | |
| } | |
| uint32_t width = bitmap->width(); | |
| uint32_t height = bitmap->height(); | |
| uint32_t mipLevelCount = math::msb(height | width); | |
| texture = m_impl->makeImageTexture(width, | |
| height, | |
| mipLevelCount, | |
| GPUTextureFormat::rgba32, | |
| bitmap->bytes(), | |
| /*blockWidth=*/1, | |
| /*blockHeight=*/1, | |
| /*srgb=*/false, | |
| /*generateRemainingMips=*/true); | |
| } | |
| } | |
| #endif | |
| return texture != nullptr ? make_rcp<RiveRenderImage>(std::move(texture)) | |
| : nullptr; | |
| } | |
| void RenderContext::releaseResources() | |
| { | |
| assert(!m_didBeginFrame); | |
| resetContainers(); | |
| setResourceSizes(ResourceAllocationCounts()); | |
| m_maxRecentResourceRequirements = ResourceAllocationCounts(); | |
| m_lastResourceTrimTimeInSeconds = m_impl->secondsNow(); | |
| } | |
| void RenderContext::resetContainers() | |
| { | |
| assert(!m_didBeginFrame); | |
| if (!m_logicalFlushes.empty()) | |
| { | |
| // Should get reset to 1 after flush(). | |
| assert(m_logicalFlushes.size() == 1); | |
| m_logicalFlushes.resize(1); | |
| m_logicalFlushes.front()->resetContainers(); | |
| } | |
| m_indirectDrawList.clear(); | |
| m_indirectDrawList.shrink_to_fit(); | |
| m_intersectionBoard = nullptr; | |
| } | |
| RenderContext::LogicalFlush::LogicalFlush(RenderContext* parent) : m_ctx(parent) | |
| { | |
| rewind(); | |
| } | |
| void RenderContext::LogicalFlush::rewind() | |
| { | |
| RIVE_PROF_SCOPE_L(1) | |
| m_resourceCounts = Draw::ResourceCounters(); | |
| m_drawPassCount = 0; | |
| m_simpleGradients.clear(); | |
| m_pendingSimpleGradDraws.clear(); | |
| m_complexGradients.clear(); | |
| m_pendingComplexGradDraws.clear(); | |
| m_pendingGradSpanCount = 0; | |
| m_clips.clear(); | |
| m_draws.clear(); | |
| m_combinedDrawBounds = IAABB::makeMaximallyNegative(); | |
| m_combinedDrawContents = gpu::DrawContents::none; | |
| m_pathPaddingCount = 0; | |
| m_paintPaddingCount = 0; | |
| m_paintAuxPaddingCount = 0; | |
| m_contourPaddingCount = 0; | |
| m_gradSpanPaddingCount = 0; | |
| m_midpointFanTessEndLocation = 0; | |
| m_outerCubicTessEndLocation = 0; | |
| m_outerCubicTessVertexIdx = 0; | |
| m_midpointFanTessVertexIdx = 0; | |
| m_baselineShaderMiscFlags = gpu::ShaderMiscFlags::none; | |
| m_flushDesc = FlushDescriptor(); | |
| m_drawList.reset(); | |
| m_firstDstBlendBarrier = nullptr; | |
| m_dstBlendBarrierListTail = &m_firstDstBlendBarrier; | |
| m_combinedShaderFeatures = gpu::ShaderFeatures::NONE; | |
| m_currentPathID = 0; | |
| m_currentContourID = 0; | |
| if (m_featherAtlasRectanizer != nullptr) | |
| { | |
| m_featherAtlasRectanizer->reset(); | |
| } | |
| m_featherAtlasMaxX = 0; | |
| m_featherAtlasMaxY = 0; | |
| m_pendingFeatherAtlasDraws.clear(); | |
| m_coverageBufferLength = 0; | |
| m_pendingBarriers = BarrierFlags::none; | |
| m_currentZIndex = 0; | |
| RIVE_DEBUG_CODE(m_hasDoneLayout = false;) | |
| } | |
| void RenderContext::LogicalFlush::resetContainers() | |
| { | |
| m_clips.clear(); | |
| m_clips.shrink_to_fit(); | |
| m_draws.clear(); | |
| m_draws.shrink_to_fit(); | |
| m_draws.reserve(kDefaultDrawCapacity); | |
| m_simpleGradients.rehash(0); | |
| m_simpleGradients.reserve(kDefaultSimpleGradientCapacity); | |
| m_pendingSimpleGradDraws.clear(); | |
| m_pendingSimpleGradDraws.shrink_to_fit(); | |
| m_pendingSimpleGradDraws.reserve(kDefaultSimpleGradientCapacity); | |
| m_complexGradients.rehash(0); | |
| m_complexGradients.reserve(kDefaultComplexGradientCapacity); | |
| m_pendingComplexGradDraws.clear(); | |
| m_pendingComplexGradDraws.shrink_to_fit(); | |
| m_pendingComplexGradDraws.reserve(kDefaultComplexGradientCapacity); | |
| m_pendingFeatherAtlasDraws.clear(); | |
| m_pendingFeatherAtlasDraws.shrink_to_fit(); | |
| // Don't reserve any space in m_pendingFeatherAtlasDraws since there are | |
| // many usecases where it isn't used at all. | |
| } | |
| static gpu::InterlockMode select_interlock_mode( | |
| const RenderContext::FrameDescriptor& frameDescriptor, | |
| const gpu::PlatformFeatures& platformFeatures) | |
| { | |
| if (frameDescriptor.msaaSampleCount != 0) | |
| { | |
| return gpu::InterlockMode::depthStencil; | |
| } | |
| if (frameDescriptor.clockwiseFillOverride) | |
| { | |
| if (platformFeatures.supportsClockwiseMode && | |
| !frameDescriptor.disableRasterOrdering) | |
| { | |
| return gpu::InterlockMode::clockwise; | |
| } | |
| if (platformFeatures.supportsClockwiseAtomicMode) | |
| { | |
| return gpu::InterlockMode::clockwiseAtomic; | |
| } | |
| } | |
| if (platformFeatures.supportsRasterOrderingMode && | |
| (!frameDescriptor.disableRasterOrdering || | |
| // Only respect "disableRasterOrdering" if we have atomic mode to fall | |
| // back on. | |
| // FIXME: This API can be improved. | |
| !platformFeatures.supportsAtomicMode)) | |
| { | |
| return gpu::InterlockMode::rasterOrdering; | |
| } | |
| if (platformFeatures.supportsAtomicMode) | |
| { | |
| return gpu::InterlockMode::atomics; | |
| } | |
| return gpu::InterlockMode::depthStencil; | |
| } | |
| void RenderContext::beginFrame(const FrameDescriptor& frameDescriptor) | |
| { | |
| RIVE_PROF_SCOPE_L(0) | |
| m_impl->preBeginFrame(this); | |
| assert(!m_didBeginFrame); | |
| assert(frameDescriptor.renderTargetWidth > 0); | |
| assert(frameDescriptor.renderTargetHeight > 0); | |
| m_frameDescriptor = frameDescriptor; | |
| m_frameInterlockMode = | |
| select_interlock_mode(m_frameDescriptor, platformFeatures()); | |
| if (m_frameInterlockMode == gpu::InterlockMode::depthStencil && | |
| m_frameDescriptor.msaaSampleCount == 0) | |
| { | |
| // Use 4x MSAA if msaaSampleCount wasn't already specified. | |
| m_frameDescriptor.msaaSampleCount = 4; | |
| } | |
| m_frameShaderFeaturesMask = | |
| gpu::ShaderFeaturesMaskFor(m_frameInterlockMode); | |
| m_triangulationController.beginFrame( | |
| m_frameDescriptor.triangulationThresholds); | |
| if (m_logicalFlushes.empty()) | |
| { | |
| m_logicalFlushes.emplace_back(new LogicalFlush(this)); | |
| } | |
| RIVE_DEBUG_CODE(m_didBeginFrame = true); | |
| } | |
| bool RenderContext::isOutsideCurrentFrame(const IAABB& pixelBounds) | |
| { | |
| assert(m_didBeginFrame); | |
| int4 bounds = simd::load4i(&pixelBounds); | |
| auto renderTargetSize = | |
| simd::cast<int32_t>(uint2{m_frameDescriptor.renderTargetWidth, | |
| m_frameDescriptor.renderTargetHeight}); | |
| return simd::any(bounds.xy >= renderTargetSize || bounds.zw <= 0 || | |
| bounds.xy >= bounds.zw); | |
| } | |
| bool RenderContext::frameSupportsClipRects() const | |
| { | |
| assert(m_didBeginFrame); | |
| return m_frameInterlockMode != gpu::InterlockMode::depthStencil || | |
| platformFeatures().supportsClipPlanes; | |
| } | |
| bool RenderContext::frameSupportsImagePaintForPaths() const | |
| { | |
| assert(m_didBeginFrame); | |
| return m_frameInterlockMode != gpu::InterlockMode::atomics; | |
| } | |
| uint32_t RenderContext::generateClipID(IAABB contentBounds, | |
| uint32_t parentClipID, | |
| AABBu16 tightenedBounds) | |
| { | |
| assert(m_didBeginFrame); | |
| assert(!m_logicalFlushes.empty()); | |
| return m_logicalFlushes.back()->generateClipID(contentBounds, | |
| parentClipID, | |
| tightenedBounds); | |
| } | |
| uint32_t RenderContext::LogicalFlush::generateClipID(IAABB contentBounds, | |
| uint32_t parentClipID, | |
| AABBu16 tightenedBounds) | |
| { | |
| if (m_clips.size() < m_ctx->m_maxPathID) // maxClipID == maxPathID. | |
| { | |
| m_clips.emplace_back(contentBounds, parentClipID, tightenedBounds); | |
| assert(m_ctx->m_clipContentID != m_clips.size()); | |
| return math::lossless_numeric_cast<uint32_t>(m_clips.size()); | |
| } | |
| return 0; // There are no available clip IDs. The caller should flush and | |
| // try again. | |
| } | |
| RenderContext::LogicalFlush::ClipInfo& RenderContext::LogicalFlush:: | |
| getWritableClipInfo(uint32_t clipID) | |
| { | |
| assert(clipID > 0); | |
| assert(clipID <= m_clips.size()); | |
| return m_clips[clipID - 1]; | |
| } | |
| bool RenderContext::pushDraws(DrawUniquePtr draws[], size_t drawCount) | |
| { | |
| assert(m_didBeginFrame); | |
| assert(!m_logicalFlushes.empty()); | |
| return m_logicalFlushes.back()->pushDraws(draws, drawCount); | |
| } | |
| bool RenderContext::LogicalFlush::pushDraws(DrawUniquePtr draws[], | |
| size_t drawCount) | |
| { | |
| RIVE_PROF_SCOPE_L(1) | |
| assert(!m_hasDoneLayout); | |
| PUSH_DISABLE_CLANG_SIMD_ABI_WARNING() | |
| auto countsVector = m_resourceCounts.toVec(); | |
| for (size_t i = 0; i < drawCount; ++i) | |
| { | |
| assert(!draws[i]->pixelBounds().empty()); | |
| assert(m_ctx->frameSupportsClipRects() || | |
| draws[i]->clipRectInverseMatrix() == nullptr); | |
| countsVector += draws[i]->resourceCounts().toVec(); | |
| } | |
| POP_DISABLE_CLANG_SIMD_ABI_WARNING() | |
| Draw::ResourceCounters countsWithNewBatch = countsVector; | |
| // Textures and buffers have hard size limits. If the new batch doesn't fit | |
| // within our constraints, the caller needs to flush and try again. | |
| if (countsWithNewBatch.pathCount > m_ctx->m_maxPathID || | |
| countsWithNewBatch.contourCount > kMaxContourID || | |
| countsWithNewBatch.midpointFanTessVertexCount + | |
| countsWithNewBatch.outerCubicTessVertexCount > | |
| kMaxTessellationVertexCountBeforePadding) | |
| { | |
| return false; | |
| } | |
| // Allocate subpasses. | |
| int passCountInBatch = 0; | |
| for (size_t i = 0; i < drawCount; ++i) | |
| { | |
| draws[i]->countSubpasses(platformFeatures()); | |
| assert(draws[i]->prepassCount() >= 0); | |
| assert(draws[i]->subpassCount() >= 0); | |
| assert(draws[i]->prepassCount() + draws[i]->subpassCount() >= 1); | |
| passCountInBatch += draws[i]->prepassCount() + draws[i]->subpassCount(); | |
| } | |
| // We can only reorder 32k draws at a time in atomic and depthStencil modes | |
| // since the sort key addresses them with a signed 16-bit index. Make sure | |
| // we don't exceed that limit. | |
| if (m_ctx->frameInterlockMode() != gpu::InterlockMode::rasterOrdering && | |
| m_drawPassCount + passCountInBatch > kMaxReorderedDrawPassCount) | |
| { | |
| return false; | |
| } | |
| // Allocate final resources. | |
| for (size_t i = 0; i < drawCount; ++i) | |
| { | |
| if (!draws[i]->allocateResources(this)) | |
| { | |
| // The draw failed to allocate resources. Give up and let the caller | |
| // flush and try again. | |
| // | |
| // FIXME: This works today, but the surrounding code could be | |
| // modified to inadvertently leave a stale dangling reference to one | |
| // of these draws in m_pendingFeatherAtlasDraws. This needs to be | |
| // revisited. | |
| return false; | |
| } | |
| } | |
| for (size_t i = 0; i < drawCount; ++i) | |
| { | |
| m_draws.push_back(std::move(draws[i])); | |
| // Note: not updating m_combinedDrawBounds here because it will get done | |
| // in tightenClipBounds later, after we've determined the minimal write | |
| // sizes for any clips. | |
| m_combinedDrawContents |= m_draws.back()->drawContents(); | |
| } | |
| m_resourceCounts = countsWithNewBatch; | |
| m_drawPassCount += passCountInBatch; | |
| return true; | |
| } | |
| bool RenderContext::LogicalFlush::allocateGradient( | |
| const Gradient* gradient, | |
| gpu::ColorRampLocation* colorRampLocation) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| assert(!m_hasDoneLayout); | |
| const float* stops = gradient->stops(); | |
| size_t stopCount = gradient->count(); | |
| assert(stopCount > 0); // RiveRenderFactory guarantees this. | |
| if (stopCount == 1 || (stopCount == 2 && stops[0] == 0 && stops[1] == 1)) | |
| { | |
| // This is a simple gradient that can be implemented by a two-texel | |
| // color ramp. | |
| const ColorInt* colors = gradient->colors(); | |
| TwoTexelRamp colorRamp = {colors[0], | |
| // Handle ramps with a single stop. | |
| colors[std::min<size_t>(1, stopCount - 1)]}; | |
| uint64_t simpleKey; | |
| static_assert(sizeof(simpleKey) == sizeof(ColorInt) * 2); | |
| RIVE_INLINE_MEMCPY(&simpleKey, &colorRamp, sizeof(ColorInt) * 2); | |
| uint32_t rampTexelsIdx; | |
| auto iter = m_simpleGradients.find(simpleKey); | |
| if (iter != m_simpleGradients.end()) | |
| { | |
| // This gradient is already in the texture. | |
| rampTexelsIdx = iter->second; | |
| } | |
| else | |
| { | |
| if (gradient_data_height(m_simpleGradients.size() + 1, | |
| m_complexGradients.size()) > | |
| kMaxTextureHeight) | |
| { | |
| // We ran out of rows in the gradient texture. Caller has to | |
| // flush and try again. | |
| return false; | |
| } | |
| rampTexelsIdx = math::lossless_numeric_cast<uint32_t>( | |
| m_simpleGradients.size() * 2); | |
| m_simpleGradients.insert({simpleKey, rampTexelsIdx}); | |
| m_pendingSimpleGradDraws.push_back(colorRamp); | |
| // Simple gradients get uploaded to the GPU as a single GradientSpan | |
| // instance. | |
| ++m_pendingGradSpanCount; | |
| } | |
| colorRampLocation->row = rampTexelsIdx / kGradTextureWidth; | |
| colorRampLocation->col = rampTexelsIdx % kGradTextureWidth; | |
| } | |
| else | |
| { | |
| // This is a complex gradient. Render it to an entire row of the | |
| // gradient texture. | |
| GradientContentKey key(ref_rcp(gradient)); | |
| auto iter = m_complexGradients.find(key); | |
| uint16_t row; | |
| if (iter != m_complexGradients.end()) | |
| { | |
| row = iter->second; // This gradient is already in the texture. | |
| } | |
| else | |
| { | |
| if (gradient_data_height(m_simpleGradients.size(), | |
| m_complexGradients.size() + 1) > | |
| kMaxTextureHeight) | |
| { | |
| // We ran out of rows in the gradient texture. Caller has to | |
| // flush and try again. | |
| return false; | |
| } | |
| row = static_cast<uint32_t>(m_complexGradients.size()); | |
| m_complexGradients.emplace(std::move(key), row); | |
| m_pendingComplexGradDraws.push_back(gradient); | |
| size_t spanCount = stopCount - 1; | |
| m_pendingGradSpanCount += spanCount; | |
| } | |
| // Store the row relative to the first complex gradient for now. | |
| // PaintData::set() will offset this value by the number of simple | |
| // gradient rows once its final value is known. | |
| colorRampLocation->row = row; | |
| colorRampLocation->col = ColorRampLocation::kComplexGradientMarker; | |
| } | |
| return true; | |
| } | |
| bool RenderContext::LogicalFlush::allocateFeatherAtlasDraw( | |
| PathDraw* pathDraw, | |
| uint16_t drawWidth, | |
| uint16_t drawHeight, | |
| uint16_t desiredPadding, | |
| uint16_t* x, | |
| uint16_t* y, | |
| AABBu16* paddedRegion) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| if (m_featherAtlasRectanizer == nullptr) | |
| { | |
| uint16_t atlasMaxSize = m_ctx->featherAtlasMaxSize(); | |
| // Use an atlas larger than featherAtlasMaxSize if it's too small for | |
| // the request (meaning the render target is larger than | |
| // featherAtlasMaxSize). | |
| m_featherAtlasRectanizer = std::make_unique<rive::RectanizerSkyline>( | |
| std::max(atlasMaxSize, drawWidth), | |
| std::max(atlasMaxSize, drawHeight)); | |
| } | |
| const uint16_t atlasMaxWidth = m_featherAtlasRectanizer->width(); | |
| const uint16_t atlasMaxHeight = m_featherAtlasRectanizer->height(); | |
| uint16_t paddedWidth = | |
| std::min<uint16_t>(drawWidth + desiredPadding * 2, atlasMaxWidth); | |
| uint16_t paddedHeight = | |
| std::min<uint16_t>(drawHeight + desiredPadding * 2, atlasMaxHeight); | |
| int16_t ix, iy; | |
| if (!m_featherAtlasRectanizer->addRect(paddedWidth, paddedHeight, &ix, &iy)) | |
| { | |
| // Delete the rectanizer of it wasn't big enough for this path. It will | |
| // be reallocated to a large enough size on the next call. | |
| if (drawWidth > atlasMaxWidth || drawHeight > atlasMaxHeight) | |
| { | |
| m_featherAtlasRectanizer = nullptr; | |
| } | |
| m_featherAtlasRectanizer = nullptr; | |
| return false; | |
| } | |
| assert(ix >= 0); | |
| assert(iy >= 0); | |
| assert(ix + paddedWidth <= atlasMaxWidth); | |
| assert(iy + paddedHeight <= atlasMaxHeight); | |
| *x = ix + (paddedWidth - drawWidth) / 2; | |
| *y = iy + (paddedHeight - drawHeight) / 2; | |
| *paddedRegion = {math::lossless_numeric_cast<uint16_t>(ix), | |
| math::lossless_numeric_cast<uint16_t>(iy), | |
| math::lossless_numeric_cast<uint16_t>(ix + paddedWidth), | |
| math::lossless_numeric_cast<uint16_t>(iy + paddedHeight)}; | |
| assert( | |
| (AABBu16{0, 0, atlasMaxWidth, atlasMaxHeight}).contains(*paddedRegion)); | |
| m_featherAtlasMaxX = | |
| std::max<uint32_t>(m_featherAtlasMaxX, paddedRegion->right); | |
| m_featherAtlasMaxY = | |
| std::max<uint32_t>(m_featherAtlasMaxY, paddedRegion->bottom); | |
| assert(m_featherAtlasMaxX <= atlasMaxWidth); | |
| assert(m_featherAtlasMaxY <= atlasMaxHeight); | |
| m_pendingFeatherAtlasDraws.push_back(pathDraw); | |
| return true; | |
| } | |
| size_t RenderContext::LogicalFlush::allocateCoverageBufferRange(size_t length) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| assert(m_ctx->frameInterlockMode() == gpu::InterlockMode::clockwiseAtomic); | |
| // Allocations must be aligned to the tile size. | |
| assert(length % (BUFFER_IMAGE_TILE_SIZE * BUFFER_IMAGE_TILE_SIZE) == 0u); | |
| uint32_t offset = m_coverageBufferLength; | |
| if (offset + length > m_ctx->platformFeatures().maxCoverageBufferLength) | |
| { | |
| return -1; | |
| } | |
| m_coverageBufferLength += length; | |
| return offset; | |
| } | |
| void RenderContext::logicalFlush() | |
| { | |
| assert(m_didBeginFrame); | |
| // Reset clipping state after every logical flush because the clip buffer is | |
| // not preserved between render passes. | |
| m_clipContentID = 0; | |
| // Don't issue any GPU commands between logical flushes. Instead, build up a | |
| // list of flushes that we will submit all at once at the end of the frame. | |
| m_logicalFlushes.emplace_back(new LogicalFlush(this)); | |
| } | |
| void RenderContext::flush(const FlushResources& flushResources) | |
| { | |
| RIVE_PROF_SCOPE_L(0) | |
| assert(m_didBeginFrame); | |
| assert(flushResources.renderTarget->width() == | |
| m_frameDescriptor.renderTargetWidth); | |
| assert(flushResources.renderTarget->height() == | |
| m_frameDescriptor.renderTargetHeight); | |
| m_triangulationController.endFrame(); | |
| m_clipContentID = 0; | |
| // Layout this frame's resource buffers and textures. | |
| LogicalFlush::ResourceCounters totalFrameResourceCounts; | |
| LogicalFlush::LayoutCounters layoutCounts; | |
| for (size_t i = 0; i < m_logicalFlushes.size(); ++i) | |
| { | |
| m_logicalFlushes[i]->layoutResources(flushResources, | |
| i, | |
| &totalFrameResourceCounts, | |
| &layoutCounts); | |
| } | |
| // Determine the minimum required resource allocation sizes to service this | |
| // flush. | |
| const ResourceAllocationCounts resourceRequirements = { | |
| .flushUniformBufferCount = m_logicalFlushes.size(), | |
| .pathBufferCount = | |
| totalFrameResourceCounts.pathCount + layoutCounts.pathPaddingCount, | |
| .paintBufferCount = | |
| totalFrameResourceCounts.pathCount + layoutCounts.paintPaddingCount, | |
| .paintAuxBufferCount = totalFrameResourceCounts.pathCount + | |
| layoutCounts.paintAuxPaddingCount, | |
| .contourBufferCount = totalFrameResourceCounts.contourCount + | |
| layoutCounts.contourPaddingCount, | |
| .gradSpanBufferCount = | |
| layoutCounts.gradSpanCount + layoutCounts.gradSpanPaddingCount, | |
| .tessSpanBufferCount = | |
| totalFrameResourceCounts.maxTessellatedSegmentCount, | |
| .triangleVertexBufferCount = | |
| totalFrameResourceCounts.maxTriangleVertexCount, | |
| .imageDrawInstanceBufferCount = totalFrameResourceCounts.imageDrawCount, | |
| .gradTextureHeight = layoutCounts.maxGradTextureHeight, | |
| .tessTextureHeight = layoutCounts.maxTessTextureHeight, | |
| .featherAtlasTextureWidth = layoutCounts.maxFeatherAtlasWidth, | |
| .featherAtlasTextureHeight = layoutCounts.maxFeatherAtlasHeight, | |
| .plsTransientBackingWidth = | |
| (layoutCounts.maxPLSTransientBackingPlaneCount > 0) | |
| ? static_cast<size_t>(m_frameDescriptor.renderTargetWidth) | |
| : 0, | |
| .plsTransientBackingHeight = | |
| (layoutCounts.maxPLSTransientBackingPlaneCount > 0) | |
| ? static_cast<size_t>(m_frameDescriptor.renderTargetHeight) | |
| : 0, | |
| .plsTransientBackingPlaneCount = | |
| layoutCounts.maxPLSTransientBackingPlaneCount, | |
| .plsAtomicCoverageBackingWidth = | |
| (frameInterlockMode() == gpu::InterlockMode::atomics) | |
| ? static_cast<size_t>(m_frameDescriptor.renderTargetWidth) | |
| : 0, | |
| .plsAtomicCoverageBackingHeight = | |
| (frameInterlockMode() == gpu::InterlockMode::atomics) | |
| ? static_cast<size_t>(m_frameDescriptor.renderTargetHeight) | |
| : 0, | |
| .coverageBufferLength = layoutCounts.maxCoverageBufferLength, | |
| }; | |
| // Ensure we're within hardware limits. | |
| assert(resourceRequirements.gradTextureHeight <= kMaxTextureHeight); | |
| assert(resourceRequirements.tessTextureHeight <= kMaxTextureHeight); | |
| assert(resourceRequirements.featherAtlasTextureWidth <= | |
| featherAtlasMaxSize() || | |
| resourceRequirements.featherAtlasTextureWidth <= | |
| frameDescriptor().renderTargetWidth); | |
| assert(resourceRequirements.featherAtlasTextureHeight <= | |
| featherAtlasMaxSize() || | |
| resourceRequirements.featherAtlasTextureHeight <= | |
| frameDescriptor().renderTargetHeight); | |
| assert(resourceRequirements.plsTransientBackingWidth <= | |
| m_frameDescriptor.renderTargetWidth); | |
| assert(resourceRequirements.plsTransientBackingHeight <= | |
| m_frameDescriptor.renderTargetHeight); | |
| assert(resourceRequirements.coverageBufferLength <= | |
| platformFeatures().maxCoverageBufferLength); | |
| PUSH_DISABLE_CLANG_SIMD_ABI_WARNING() | |
| // Track m_maxRecentResourceRequirements so we can trim GPU allocations when | |
| // steady-state usage goes down. | |
| m_maxRecentResourceRequirements = ResourceAllocationCounts::FromVec( | |
| simd::max(resourceRequirements.toVec(), | |
| m_maxRecentResourceRequirements.toVec())); | |
| // Grow resources enough to handle this flush. | |
| // If "allocs" already fits in our current allocations, then don't change | |
| // them. | |
| // If they don't fit, overallocate by the specified amount in order to | |
| // create some slack for growth. | |
| constexpr static ResourceAllocationCounts OVERALLOC_x4 = { | |
| .flushUniformBufferCount = 5, // 125% | |
| .pathBufferCount = 5, // 125% | |
| .paintBufferCount = 5, // 125% | |
| .paintAuxBufferCount = 5, // 125% | |
| .contourBufferCount = 5, // 125% | |
| .gradSpanBufferCount = 5, // 125% | |
| .tessSpanBufferCount = 5, // 125% | |
| .triangleVertexBufferCount = 5, // 125% | |
| .imageDrawInstanceBufferCount = 5, // 125% | |
| .gradTextureHeight = 5, // 125% | |
| .tessTextureHeight = 5, // 125% | |
| .featherAtlasTextureWidth = 5, // 125% | |
| .featherAtlasTextureHeight = 5, // 125% | |
| .plsTransientBackingWidth = 4, // 100% (i.e., don't overallocate) | |
| .plsTransientBackingHeight = 4, // 100% (i.e., don't overallocate) | |
| .plsTransientBackingPlaneCount = 4, // 100% (i.e., don't overallocate) | |
| .plsAtomicCoverageBackingWidth = 4, // 100% (i.e., don't overallocate) | |
| .plsAtomicCoverageBackingHeight = 4, // 100% (i.e., don't overallocate) | |
| .coverageBufferLength = 5, // 125% | |
| }; | |
| ResourceAllocationCounts allocs = | |
| ResourceAllocationCounts::FromVec(simd::if_then_else( | |
| resourceRequirements.toVec() <= | |
| m_currentResourceAllocations.toVec(), | |
| m_currentResourceAllocations.toVec(), | |
| (resourceRequirements.toVec() * OVERALLOC_x4.toVec()) >> 2)); | |
| // In case the 25% growth pushed us above limits. | |
| allocs.gradTextureHeight = | |
| std::min<size_t>(allocs.gradTextureHeight, kMaxTextureHeight); | |
| allocs.tessTextureHeight = | |
| std::min<size_t>(allocs.tessTextureHeight, kMaxTextureHeight); | |
| allocs.featherAtlasTextureWidth = std::min<size_t>( | |
| allocs.featherAtlasTextureWidth, | |
| std::max(featherAtlasMaxSize(), frameDescriptor().renderTargetWidth)); | |
| allocs.featherAtlasTextureHeight = std::min<size_t>( | |
| allocs.featherAtlasTextureHeight, | |
| std::max(featherAtlasMaxSize(), frameDescriptor().renderTargetHeight)); | |
| allocs.coverageBufferLength = | |
| std::min(allocs.coverageBufferLength, | |
| platformFeatures().maxCoverageBufferLength); | |
| // Additionally, every 5 seconds, trim resources down to the most recent | |
| // steady-state usage. | |
| double flushTime = m_impl->secondsNow(); | |
| bool needsResourceTrim = flushTime - m_lastResourceTrimTimeInSeconds >= 5; | |
| if (needsResourceTrim) | |
| { | |
| // Trim GPU resource allocations to their maximum recent usage, plus | |
| // overallocation, and only if the recent usage is below a certain | |
| // threshold. | |
| constexpr static ResourceAllocationCounts SHRINK_THRESHOLD_x3 = { | |
| .flushUniformBufferCount = 2, // 66.7% | |
| .pathBufferCount = 2, // 66.7% | |
| .paintBufferCount = 2, // 66.7% | |
| .paintAuxBufferCount = 2, // 66.7% | |
| .contourBufferCount = 2, // 66.7% | |
| .gradSpanBufferCount = 2, // 66.7% | |
| .tessSpanBufferCount = 2, // 66.7% | |
| .triangleVertexBufferCount = 2, // 66.7% | |
| .imageDrawInstanceBufferCount = 2, // 66.7% | |
| .gradTextureHeight = 2, // 66.7% | |
| .tessTextureHeight = 2, // 66.7% | |
| .featherAtlasTextureWidth = 2, // 66.7% | |
| .featherAtlasTextureHeight = 2, // 66.7% | |
| .plsTransientBackingWidth = 3, // 100% (i.e., always shrink) | |
| .plsTransientBackingHeight = 3, // 100% (i.e., always shrink) | |
| .plsTransientBackingPlaneCount = 3, // 100% (i.e., always shrink) | |
| .plsAtomicCoverageBackingWidth = 3, // 100% (i.e., always shrink) | |
| .plsAtomicCoverageBackingHeight = 3, // 100% (i.e., always shrink) | |
| .coverageBufferLength = 2, // 66.7% | |
| }; | |
| allocs = ResourceAllocationCounts::FromVec(simd::if_then_else( | |
| m_maxRecentResourceRequirements.toVec() <= | |
| (allocs.toVec() * SHRINK_THRESHOLD_x3.toVec()) / size_t(3), | |
| // TODO: Do we actually need overallocation here?? Or should we just | |
| // trust the past 5 seconds of steady usage? | |
| (m_maxRecentResourceRequirements.toVec() * OVERALLOC_x4.toVec()) >> | |
| 2, | |
| allocs.toVec())); | |
| // Ensure we stayed within limits. | |
| assert(allocs.gradTextureHeight <= kMaxTextureHeight); | |
| assert(allocs.tessTextureHeight <= kMaxTextureHeight); | |
| assert(allocs.featherAtlasTextureWidth <= featherAtlasMaxSize() || | |
| allocs.featherAtlasTextureWidth <= | |
| frameDescriptor().renderTargetWidth); | |
| assert(allocs.featherAtlasTextureHeight <= featherAtlasMaxSize() || | |
| allocs.featherAtlasTextureHeight <= | |
| frameDescriptor().renderTargetHeight); | |
| assert(allocs.coverageBufferLength <= | |
| platformFeatures().maxCoverageBufferLength); | |
| // Zero out m_maxRecentResourceRequirements for the next interval. | |
| m_maxRecentResourceRequirements = ResourceAllocationCounts(); | |
| m_lastResourceTrimTimeInSeconds = flushTime; | |
| } | |
| assert(simd::all(allocs.toVec() >= resourceRequirements.toVec())); | |
| POP_DISABLE_CLANG_SIMD_ABI_WARNING() | |
| setResourceSizes(allocs); | |
| m_impl->prepareToFlush(flushResources.currentFrameNumber, | |
| flushResources.safeFrameNumber); | |
| if (mapResourceBuffers(resourceRequirements)) | |
| { | |
| for (const auto& flush : m_logicalFlushes) | |
| { | |
| flush->writeResources(); | |
| } | |
| assert(m_flushUniformData.elementsWritten() == m_logicalFlushes.size()); | |
| assert(m_imageDrawInstanceData.elementsWritten() == | |
| totalFrameResourceCounts.imageDrawCount); | |
| assert(m_pathData.elementsWritten() == | |
| totalFrameResourceCounts.pathCount + | |
| layoutCounts.pathPaddingCount); | |
| assert(m_paintData.elementsWritten() == | |
| totalFrameResourceCounts.pathCount + | |
| layoutCounts.paintPaddingCount); | |
| assert(m_paintAuxData.elementsWritten() == | |
| totalFrameResourceCounts.pathCount + | |
| layoutCounts.paintAuxPaddingCount); | |
| assert(m_contourData.elementsWritten() == | |
| totalFrameResourceCounts.contourCount + | |
| layoutCounts.contourPaddingCount); | |
| assert(m_gradSpanData.elementsWritten() == | |
| layoutCounts.gradSpanCount + layoutCounts.gradSpanPaddingCount); | |
| assert(m_tessSpanData.elementsWritten() <= | |
| totalFrameResourceCounts.maxTessellatedSegmentCount); | |
| assert(m_triangleVertexData.elementsWritten() <= | |
| totalFrameResourceCounts.maxTriangleVertexCount); | |
| unmapResourceBuffers(resourceRequirements); | |
| // Issue logical flushes to the backend. | |
| for (const auto& flush : m_logicalFlushes) | |
| { | |
| m_impl->flush(flush->desc()); | |
| } | |
| } | |
| else | |
| { | |
| fprintf(stderr, "Buffer mapping failed, cannot render.\n"); | |
| unmapResourceBuffers(resourceRequirements); | |
| } | |
| m_impl->postFlush(flushResources); | |
| if (!m_logicalFlushes.empty()) | |
| { | |
| m_logicalFlushes.resize(1); | |
| m_logicalFlushes.front()->rewind(); | |
| } | |
| // Drop all memory that was allocated for this frame using | |
| // TrivialBlockAllocator. | |
| m_perFrameAllocator.reset(); | |
| m_numChopsAllocator.reset(); | |
| m_chopVerticesAllocator.reset(); | |
| m_tangentPairsAllocator.reset(); | |
| m_polarSegmentCountsAllocator.reset(); | |
| m_parametricSegmentCountsAllocator.reset(); | |
| m_frameDescriptor = FrameDescriptor(); | |
| RIVE_DEBUG_CODE(m_didBeginFrame = false;) | |
| // Wait to reset CPU-side containers until after the flush has finished. | |
| if (needsResourceTrim) | |
| { | |
| resetContainers(); | |
| } | |
| } | |
| static uint32_t pls_transient_backing_plane_count( | |
| gpu::InterlockMode interlockMode, | |
| gpu::DrawContents combinedDrawContents) | |
| { | |
| switch (interlockMode) | |
| { | |
| case gpu::InterlockMode::rasterOrdering: | |
| return 3; // clip, scratch, coverage | |
| case gpu::InterlockMode::atomics: | |
| case gpu::InterlockMode::clockwiseAtomic: | |
| return 1; // only clip (coverage is atomic) | |
| case gpu::InterlockMode::clockwise: | |
| { | |
| uint32_t n = 1; // coverage | |
| if (enums::any_flag_set(combinedDrawContents, | |
| gpu::DrawContents::activeClip | | |
| gpu::DrawContents::clipUpdate)) | |
| { | |
| ++n; // clip | |
| } | |
| if (enums::is_flag_set(combinedDrawContents, | |
| gpu::DrawContents::advancedBlend)) | |
| { | |
| ++n; // scratch color | |
| } | |
| return n; | |
| } | |
| case gpu::InterlockMode::depthStencil: | |
| return 0; // N/A | |
| } | |
| RIVE_UNREACHABLE(); | |
| } | |
| static bool wants_fixed_function_color_output( | |
| const gpu::PlatformFeatures& platformFeatures, | |
| gpu::InterlockMode interlockMode, | |
| gpu::DrawContents combinedDrawContents, | |
| bool manuallyResolved) | |
| { | |
| switch (interlockMode) | |
| { | |
| case gpu::InterlockMode::rasterOrdering: | |
| // rasterOrdering shaders always read the framebuffer, even with | |
| // srcOver blend. | |
| return false; | |
| case gpu::InterlockMode::atomics: | |
| case gpu::InterlockMode::clockwiseAtomic: | |
| return !enums::is_flag_set(combinedDrawContents, | |
| gpu::DrawContents::advancedBlend); | |
| case gpu::InterlockMode::clockwise: | |
| assert(enums::no_flags_set(combinedDrawContents, | |
| gpu::DrawContents::nonZeroFill | | |
| gpu::DrawContents::evenOddFill)); | |
| return platformFeatures.supportsClockwiseFixedFunctionMode && | |
| !enums::is_flag_set(combinedDrawContents, | |
| gpu::DrawContents::advancedBlend); | |
| case gpu::InterlockMode::depthStencil: | |
| // Manual MSAA resolves read the framebuffer, so they can't use | |
| // fixedFunctionColorOutput. | |
| return !manuallyResolved && | |
| !enums::is_flag_set(combinedDrawContents, | |
| gpu::DrawContents::advancedBlend); | |
| } | |
| RIVE_UNREACHABLE(); | |
| } | |
| void RenderContext::LogicalFlush::layoutResources( | |
| const FlushResources& flushResources, | |
| size_t logicalFlushIdx, | |
| ResourceCounters* runningFrameResourceCounts, | |
| LayoutCounters* runningFrameLayoutCounts) | |
| { | |
| RIVE_PROF_SCOPE_L(1) | |
| assert(!m_hasDoneLayout); | |
| const FrameDescriptor& frameDescriptor = m_ctx->frameDescriptor(); | |
| // Reserve a path record for the clearColor paint (used by atomic mode). | |
| // This also allows us to index the storage buffers directly by pathID. | |
| ++m_resourceCounts.pathCount; | |
| // Storage buffer offsets are required to be aligned on multiples of 256. | |
| m_pathPaddingCount = | |
| math::padding_to_align_up<gpu::kPathBufferAlignmentInElements>( | |
| m_resourceCounts.pathCount); | |
| m_paintPaddingCount = | |
| math::padding_to_align_up<gpu::kPaintBufferAlignmentInElements>( | |
| m_resourceCounts.pathCount); | |
| m_paintAuxPaddingCount = | |
| math::padding_to_align_up<gpu::kPaintAuxBufferAlignmentInElements>( | |
| m_resourceCounts.pathCount); | |
| m_contourPaddingCount = | |
| math::padding_to_align_up<gpu::kContourBufferAlignmentInElements>( | |
| m_resourceCounts.contourCount); | |
| // Metal requires vertex buffers to be 256-byte aligned. | |
| m_gradSpanPaddingCount = | |
| math::padding_to_align_up<gpu::kGradSpanBufferAlignmentInElements>( | |
| m_pendingGradSpanCount); | |
| size_t totalTessVertexCountWithPadding = 0; | |
| if ((m_resourceCounts.midpointFanTessVertexCount | | |
| m_resourceCounts.outerCubicTessVertexCount) != 0) | |
| { | |
| // midpointFan tessellation vertices reside at the beginning of the | |
| // tessellation texture, after 1 patch of padding vertices. | |
| constexpr uint32_t kPrePadding = gpu::kMidpointFanPatchSegmentSpan; | |
| m_midpointFanTessVertexIdx = kPrePadding; | |
| m_midpointFanTessEndLocation = | |
| m_midpointFanTessVertexIdx + | |
| math::lossless_numeric_cast<uint32_t>( | |
| m_resourceCounts.midpointFanTessVertexCount); | |
| // outerCubic tessellation vertices reside after the midpointFan | |
| // vertices, aligned on a multiple of the outerCubic patch size. | |
| uint32_t interiorPadding = | |
| math::padding_to_align_up<gpu::OuterCubicPatchSegmentSpanPlusJoin>( | |
| m_midpointFanTessEndLocation); | |
| m_outerCubicTessVertexIdx = | |
| m_midpointFanTessEndLocation + interiorPadding; | |
| m_outerCubicTessEndLocation = | |
| m_outerCubicTessVertexIdx + | |
| math::lossless_numeric_cast<uint32_t>( | |
| m_resourceCounts.outerCubicTessVertexCount); | |
| // We need one more padding vertex after all the tessellation vertices. | |
| constexpr uint32_t kPostPadding = 1; | |
| totalTessVertexCountWithPadding = | |
| m_outerCubicTessEndLocation + kPostPadding; | |
| assert(kPrePadding + interiorPadding + kPostPadding <= | |
| kMaxTessellationPaddingVertexCount); | |
| assert(totalTessVertexCountWithPadding <= kMaxTessellationVertexCount); | |
| } | |
| uint32_t tessDataHeight = math::lossless_numeric_cast<uint32_t>( | |
| resource_texture_height<kTessTextureWidth>( | |
| totalTessVertexCountWithPadding)); | |
| if (m_resourceCounts.maxTessellatedSegmentCount != 0) | |
| { | |
| // Conservatively account for line breaks and padding in the | |
| // tessellation span count. Line breaks potentially introduce a new | |
| // span. Count the maximum number of line breaks we might encounter, | |
| // which is at most TWO for every line in the tessellation texture (one | |
| // for a forward span, and one for its reflection.) | |
| size_t maxSpanBreakCount = tessDataHeight * 2; | |
| // The tessellation texture requires 3 separate spans of padding | |
| // vertices (see above and below). | |
| constexpr size_t kPaddingSpanCount = 3; | |
| m_resourceCounts.maxTessellatedSegmentCount += | |
| maxSpanBreakCount + kPaddingSpanCount + | |
| kMaxTessellationAlignmentVertices; | |
| } | |
| // Complex gradients begin on the first row immediately after the simple | |
| // gradients. | |
| m_gradTextureLayout.complexOffsetY = math::lossless_numeric_cast<uint32_t>( | |
| resource_texture_height<gpu::kGradTextureWidthInSimpleRamps>( | |
| m_simpleGradients.size())); | |
| m_flushDesc.renderTarget = flushResources.renderTarget; | |
| m_flushDesc.interlockMode = m_ctx->frameInterlockMode(); | |
| m_flushDesc.msaaSampleCount = frameDescriptor.msaaSampleCount; | |
| // In atomic mode, we may be able to skip the explicit clear of the color | |
| // buffer and fold it into the atomic "resolve" operation instead. | |
| bool doClearDuringAtomicResolve = false; | |
| if (logicalFlushIdx != 0) | |
| { | |
| // We always have to preserve the renderTarget between logical flushes. | |
| m_flushDesc.colorLoadAction = gpu::LoadAction::preserveRenderTarget; | |
| } | |
| else if (frameDescriptor.loadAction == gpu::LoadAction::clear) | |
| { | |
| // In atomic mode, we can clear during the resolve operation if the | |
| // clearColor is opaque (because we don't want or have a "source only" | |
| // blend mode). | |
| doClearDuringAtomicResolve = | |
| m_ctx->frameInterlockMode() == gpu::InterlockMode::atomics && | |
| colorAlpha(frameDescriptor.clearColor) == 255; | |
| m_flushDesc.colorLoadAction = doClearDuringAtomicResolve | |
| ? gpu::LoadAction::dontCare | |
| : gpu::LoadAction::clear; | |
| } | |
| else | |
| { | |
| m_flushDesc.colorLoadAction = frameDescriptor.loadAction; | |
| } | |
| m_flushDesc.colorClearValue = frameDescriptor.clearColor; | |
| if (doClearDuringAtomicResolve) | |
| { | |
| // In atomic mode we can accomplish a clear of the color buffer while | |
| // the shader resolves coverage, instead of actually clearing. | |
| // writeResources() will configure the fill for pathID=0 to be a solid | |
| // fill matching the clearColor, so if we just initialize coverage | |
| // buffer to solid coverage with pathID=0, the resolve step will write | |
| // out the correct clear color. | |
| assert(m_flushDesc.interlockMode == gpu::InterlockMode::atomics); | |
| m_flushDesc.coverageClearValue = | |
| static_cast<uint32_t>(FIXED_COVERAGE_ONE); | |
| } | |
| else if (m_flushDesc.interlockMode == gpu::InterlockMode::atomics) | |
| { | |
| // When we don't skip the initial clear in atomic mode, clear the | |
| // coverage buffer to pathID=0 and a transparent coverage value. | |
| // pathID=0 meets the requirement that pathID is always monotonically | |
| // increasing. Transparent coverage makes sure the clearColor doesn't | |
| // get written out while resolving. | |
| m_flushDesc.coverageClearValue = | |
| static_cast<uint32_t>(FIXED_COVERAGE_ZERO); | |
| } | |
| else | |
| { | |
| // In non-atomic mode, the coverage buffer just needs to be initialized | |
| // with "pathID=0" to avoid collisions with any pathIDs being rendered. | |
| m_flushDesc.coverageClearValue = 0; | |
| } | |
| // Adjust the clip bounds so that they are as tight on the writes/reads as | |
| // possible, to enable minimal scissor rectangle sizes. | |
| // Note: This is done here so that m_combinedDrawBounds are updated before | |
| // we try to use them, to ensure they're also tightened in on the clipping | |
| // (when scissor is supported). | |
| tightenClipBounds(); | |
| if (doClearDuringAtomicResolve || | |
| m_flushDesc.colorLoadAction == gpu::LoadAction::clear) | |
| { | |
| // If we're clearing then we always update the entire render target. | |
| m_flushDesc.renderTargetUpdateBounds = | |
| m_flushDesc.renderTarget->bounds(); | |
| } | |
| else | |
| { | |
| // When we don't clear, we only update the draw bounds. | |
| m_flushDesc.renderTargetUpdateBounds = | |
| m_flushDesc.renderTarget->bounds().intersect(m_combinedDrawBounds); | |
| } | |
| if (m_flushDesc.renderTargetUpdateBounds.empty()) | |
| { | |
| // If this is empty it means there are no draws and no clear. | |
| m_flushDesc.renderTargetUpdateBounds = {0, 0, 0, 0}; | |
| } | |
| m_flushDesc.virtualTileWidth = frameDescriptor.virtualTileWidth; | |
| m_flushDesc.virtualTileHeight = frameDescriptor.virtualTileHeight; | |
| m_flushDesc.manuallyResolved = m_ctx->m_impl->wantsManualRenderPassResolve( | |
| m_flushDesc.interlockMode, | |
| m_flushDesc.renderTarget, | |
| m_flushDesc.renderTargetUpdateBounds, | |
| m_flushDesc.virtualTileWidth, | |
| m_flushDesc.virtualTileHeight, | |
| m_combinedDrawContents); | |
| m_flushDesc.fixedFunctionColorOutput = | |
| wants_fixed_function_color_output(m_ctx->platformFeatures(), | |
| m_ctx->frameInterlockMode(), | |
| m_combinedDrawContents, | |
| m_flushDesc.manuallyResolved); | |
| if (m_flushDesc.fixedFunctionColorOutput) | |
| { | |
| m_baselineShaderMiscFlags |= | |
| gpu::ShaderMiscFlags::fixedFunctionColorOutput; | |
| } | |
| m_flushDesc.featherAtlasContentWidth = m_featherAtlasMaxX; | |
| m_flushDesc.featherAtlasContentHeight = m_featherAtlasMaxY; | |
| m_flushDesc.flushUniformDataOffsetInBytes = | |
| logicalFlushIdx * sizeof(gpu::FlushUniforms); | |
| m_flushDesc.pathCount = | |
| math::lossless_numeric_cast<uint32_t>(m_resourceCounts.pathCount); | |
| m_flushDesc.firstPath = runningFrameResourceCounts->pathCount + | |
| runningFrameLayoutCounts->pathPaddingCount; | |
| m_flushDesc.firstPaint = runningFrameResourceCounts->pathCount + | |
| runningFrameLayoutCounts->paintPaddingCount; | |
| m_flushDesc.firstPaintAux = runningFrameResourceCounts->pathCount + | |
| runningFrameLayoutCounts->paintAuxPaddingCount; | |
| m_flushDesc.contourCount = | |
| math::lossless_numeric_cast<uint32_t>(m_resourceCounts.contourCount); | |
| m_flushDesc.firstContour = runningFrameResourceCounts->contourCount + | |
| runningFrameLayoutCounts->contourPaddingCount; | |
| m_flushDesc.gradSpanCount = | |
| math::lossless_numeric_cast<uint32_t>(m_pendingGradSpanCount); | |
| m_flushDesc.firstGradSpan = runningFrameLayoutCounts->gradSpanCount + | |
| runningFrameLayoutCounts->gradSpanPaddingCount; | |
| m_flushDesc.gradDataHeight = math::lossless_numeric_cast<uint32_t>( | |
| m_gradTextureLayout.complexOffsetY + m_complexGradients.size()); | |
| m_flushDesc.tessDataHeight = tessDataHeight; | |
| m_flushDesc.clockwiseFillOverride = frameDescriptor.clockwiseFillOverride; | |
| m_flushDesc.wireframe = frameDescriptor.wireframe; | |
| m_flushDesc.ditherMode = m_ctx->frameDescriptor().ditherMode; | |
| #ifdef WITH_RIVE_TOOLS | |
| m_flushDesc.synthesizedFailureType = frameDescriptor.synthesizedFailureType; | |
| #endif | |
| m_flushDesc.externalCommandBuffer = flushResources.externalCommandBuffer; | |
| PUSH_DISABLE_CLANG_SIMD_ABI_WARNING() | |
| *runningFrameResourceCounts = | |
| runningFrameResourceCounts->toVec() + m_resourceCounts.toVec(); | |
| POP_DISABLE_CLANG_SIMD_ABI_WARNING() | |
| runningFrameLayoutCounts->pathPaddingCount += m_pathPaddingCount; | |
| runningFrameLayoutCounts->paintPaddingCount += m_paintPaddingCount; | |
| runningFrameLayoutCounts->paintAuxPaddingCount += m_paintAuxPaddingCount; | |
| runningFrameLayoutCounts->contourPaddingCount += m_contourPaddingCount; | |
| runningFrameLayoutCounts->gradSpanCount += m_pendingGradSpanCount; | |
| runningFrameLayoutCounts->gradSpanPaddingCount += m_gradSpanPaddingCount; | |
| runningFrameLayoutCounts->maxGradTextureHeight = | |
| std::max(m_flushDesc.gradDataHeight, | |
| runningFrameLayoutCounts->maxGradTextureHeight); | |
| runningFrameLayoutCounts->maxTessTextureHeight = | |
| std::max(m_flushDesc.tessDataHeight, | |
| runningFrameLayoutCounts->maxTessTextureHeight); | |
| runningFrameLayoutCounts->maxFeatherAtlasWidth = | |
| std::max(m_featherAtlasMaxX, | |
| runningFrameLayoutCounts->maxFeatherAtlasWidth); | |
| runningFrameLayoutCounts->maxFeatherAtlasHeight = | |
| std::max(m_featherAtlasMaxY, | |
| runningFrameLayoutCounts->maxFeatherAtlasHeight); | |
| runningFrameLayoutCounts->maxPLSTransientBackingPlaneCount = | |
| std::max(pls_transient_backing_plane_count(m_flushDesc.interlockMode, | |
| m_combinedDrawContents), | |
| runningFrameLayoutCounts->maxPLSTransientBackingPlaneCount); | |
| runningFrameLayoutCounts->maxCoverageBufferLength = | |
| std::max<size_t>(m_coverageBufferLength, | |
| runningFrameLayoutCounts->maxCoverageBufferLength); | |
| assert(m_flushDesc.firstPath % gpu::kPathBufferAlignmentInElements == 0); | |
| assert(m_flushDesc.firstPaint % gpu::kPaintBufferAlignmentInElements == 0); | |
| assert(m_flushDesc.firstPaintAux % | |
| gpu::kPaintAuxBufferAlignmentInElements == | |
| 0); | |
| assert(m_flushDesc.firstContour % gpu::kContourBufferAlignmentInElements == | |
| 0); | |
| assert(m_flushDesc.firstGradSpan % | |
| gpu::kGradSpanBufferAlignmentInElements == | |
| 0); | |
| RIVE_DEBUG_CODE(m_hasDoneLayout = true;) | |
| } | |
| void RenderContext::LogicalFlush::pushBarriers(BarrierFlags barrierFlags) | |
| { | |
| if (m_ctx->platformFeatures() | |
| .clockwiseAtomicBorrowedCoverageBarrierNeedsRenderPassInit && | |
| enums::is_flag_set(barrierFlags, | |
| gpu::BarrierFlags::clockwiseBorrowedCoverage)) | |
| { | |
| // We need a workaround in order for input attachments to work. | |
| BarrierFlags workaroundBarriers = | |
| BarrierFlags::clockwiseBorrowedCoverage | BarrierFlags::plsAtomic; | |
| if (enums::is_flag_set(m_combinedDrawContents, | |
| gpu::DrawContents::advancedBlend)) | |
| { | |
| workaroundBarriers |= BarrierFlags::dstBlend; | |
| } | |
| m_drawList.emplace_back(m_ctx->perFrameAllocator(), | |
| gpu::DrawType::renderPassInitialize, | |
| m_baselineShaderMiscFlags, | |
| gpu::DrawContents::none, | |
| 1, | |
| 0, | |
| BlendMode::overlay, | |
| ImageSampler::LinearClamp(), | |
| workaroundBarriers); | |
| barrierFlags &= ~gpu::BarrierFlags::clockwiseBorrowedCoverage; | |
| } | |
| m_pendingBarriers |= barrierFlags; | |
| } | |
| void RenderContext::LogicalFlush::writeResources() | |
| { | |
| RIVE_PROF_SCOPE_L(1) | |
| const gpu::PlatformFeatures& platformFeatures = m_ctx->platformFeatures(); | |
| assert(m_hasDoneLayout); | |
| assert(m_flushDesc.firstPath == m_ctx->m_pathData.elementsWritten()); | |
| assert(m_flushDesc.firstPaint == m_ctx->m_paintData.elementsWritten()); | |
| assert(m_flushDesc.firstPaintAux == | |
| m_ctx->m_paintAuxData.elementsWritten()); | |
| // Wait until here before we record these texture sizes; they aren't decided | |
| // until after all LogicalFlushes have run layoutResources(). | |
| m_flushDesc.featherAtlasTextureWidth = | |
| math::lossless_numeric_cast<uint32_t>( | |
| m_ctx->m_currentResourceAllocations.featherAtlasTextureWidth); | |
| m_flushDesc.featherAtlasTextureHeight = | |
| math::lossless_numeric_cast<uint32_t>( | |
| m_ctx->m_currentResourceAllocations.featherAtlasTextureHeight); | |
| m_gradTextureLayout.inverseHeight = | |
| 1.f / m_ctx->m_currentResourceAllocations.gradTextureHeight; | |
| // Exact tessSpan/triangleVertex counts aren't known until after their data | |
| // is written out. | |
| size_t firstTessVertexSpan = m_ctx->m_tessSpanData.elementsWritten(); | |
| size_t initialTriangleVertexDataSize = | |
| m_ctx->m_triangleVertexData.bytesWritten(); | |
| // Metal requires vertex buffers to be 256-byte aligned. | |
| size_t tessAlignmentPadding = | |
| math::padding_to_align_up<gpu::kTessVertexBufferAlignmentInElements>( | |
| firstTessVertexSpan); | |
| assert(tessAlignmentPadding <= kMaxTessellationAlignmentVertices); | |
| m_ctx->m_tessSpanData.push_back_n(nullptr, tessAlignmentPadding); | |
| m_flushDesc.firstTessVertexSpan = | |
| firstTessVertexSpan + tessAlignmentPadding; | |
| assert(m_flushDesc.firstTessVertexSpan == | |
| m_ctx->m_tessSpanData.elementsWritten()); | |
| // Write out the simple gradient data. | |
| constexpr static uint32_t ONE_TEXEL_FIXED = 65536 / gpu::kGradTextureWidth; | |
| assert(m_simpleGradients.size() == m_pendingSimpleGradDraws.size()); | |
| if (!m_pendingSimpleGradDraws.empty()) | |
| { | |
| for (size_t i = 0; i < m_pendingSimpleGradDraws.size(); ++i) | |
| { | |
| // Render each simple gradient as a single, empty GradientSpan with | |
| // 1px borders to the left and right. | |
| auto [color0, color1] = m_pendingSimpleGradDraws[i]; | |
| uint32_t y = math::lossless_numeric_cast<uint32_t>( | |
| i / gpu::kGradTextureWidthInSimpleRamps); | |
| size_t centerX = (i % gpu::kGradTextureWidthInSimpleRamps) * 2 + 1; | |
| uint32_t centerXFixed = math::lossless_numeric_cast<uint32_t>( | |
| centerX * ONE_TEXEL_FIXED); | |
| m_ctx->m_gradSpanData.set_back(centerXFixed, | |
| centerXFixed, | |
| y, | |
| GRAD_SPAN_FLAG_LEFT_BORDER | | |
| GRAD_SPAN_FLAG_RIGHT_BORDER, | |
| color0, | |
| color1); | |
| } | |
| } | |
| // Write out the vertex data for rendering complex gradients. | |
| assert(m_complexGradients.size() == m_pendingComplexGradDraws.size()); | |
| if (!m_pendingComplexGradDraws.empty()) | |
| { | |
| // The viewport will start at simpleGradDataHeight when rendering color | |
| // ramps. | |
| for (uint32_t i = 0; i < m_pendingComplexGradDraws.size(); ++i) | |
| { | |
| // Push "GradientSpan" instances that will render each section of | |
| // this color ramp's gradient. | |
| const Gradient* gradient = m_pendingComplexGradDraws[i]; | |
| const float* stops = gradient->stops(); | |
| const ColorInt* colors = gradient->colors(); | |
| size_t stopCount = gradient->count(); | |
| uint32_t y = i + m_gradTextureLayout.complexOffsetY; | |
| // "stop * m + a" converts a stop position to a fixed-point x | |
| // coordinate in the gradient texture. (In an ideal world, stops | |
| // would all be aligned on pixel centers for the texture sampling to | |
| // be identical to the gradient, but here we just stretch it across | |
| // kGradTextureWidth pixels and hope everything looks ok.) | |
| float m = (kGradTextureWidth - 1.f) * ONE_TEXEL_FIXED; | |
| float a = .5f * ONE_TEXEL_FIXED; | |
| uint32_t lastXFixed = static_cast<uint32_t>(stops[0] * m + a); | |
| ColorInt lastColor = colors[0]; | |
| assert(stopCount >= 2); | |
| for (size_t i = 1; i < stopCount; ++i) | |
| { | |
| uint32_t xFixed = static_cast<uint32_t>(stops[i] * m + a); | |
| // stops[] must be ordered. | |
| assert(lastXFixed <= xFixed && xFixed < 65536); | |
| uint32_t flags = GRAD_SPAN_FLAG_COMPLEX_BORDER; | |
| if (i == 1) | |
| flags |= GRAD_SPAN_FLAG_LEFT_BORDER; | |
| if (i == stopCount - 1) | |
| flags |= GRAD_SPAN_FLAG_RIGHT_BORDER; | |
| m_ctx->m_gradSpanData.set_back(lastXFixed, | |
| xFixed, | |
| y, | |
| flags, | |
| lastColor, | |
| colors[i]); | |
| lastColor = colors[i]; | |
| lastXFixed = xFixed; | |
| } | |
| } | |
| } | |
| // Write a path record for the clearColor paint (used by atomic mode). | |
| // This also allows us to index the storage buffers directly by pathID. | |
| gpu::SimplePaintValue clearColorValue; | |
| clearColorValue.color = m_ctx->frameDescriptor().clearColor; | |
| m_ctx->m_pathData.skip_back(); | |
| m_ctx->m_paintData.set_back(gpu::DrawContents::none, | |
| PaintType::solidColor, | |
| clearColorValue, | |
| GradTextureLayout(), | |
| /*clipID =*/0, | |
| /*hasClipRect =*/false, | |
| /*hasImage =*/false, | |
| BlendMode::srcOver); | |
| m_ctx->m_paintAuxData.skip_back(); | |
| // Render padding vertices in the tessellation texture. | |
| if (m_flushDesc.tessDataHeight > 0) | |
| { | |
| // Padding at the beginning of the tessellation texture. | |
| pushPaddingVertices(gpu::kMidpointFanPatchSegmentSpan, 0); | |
| // Padding between patch types in the tessellation texture. | |
| if (m_outerCubicTessVertexIdx > m_midpointFanTessEndLocation) | |
| { | |
| pushPaddingVertices(m_outerCubicTessVertexIdx - | |
| m_midpointFanTessEndLocation, | |
| m_midpointFanTessEndLocation); | |
| } | |
| // 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. | |
| pushPaddingVertices(1, m_outerCubicTessEndLocation); | |
| } | |
| // Write out all the data for our high level draws, and build up a low-level | |
| // draw list. | |
| if (!platformFeatures.supportsClipScissor && | |
| m_ctx->frameInterlockMode() == gpu::InterlockMode::rasterOrdering) | |
| { | |
| for (const DrawUniquePtr& draw : m_draws) | |
| { | |
| // TODO: We don't currently support a front-to-back prepass in | |
| // rasterOrdering mode. If we decide to support this, we will either | |
| // need to walk the draws backwards here, or, more likely, start | |
| // sorting and re-ordering in rasterOrdering mode as well. | |
| assert(draw->prepassCount() == 0); | |
| assert(draw->subpassCount() > 0); | |
| for (int i = 0; i < draw->subpassCount(); ++i) | |
| { | |
| draw->pushToRenderContext(this, i); | |
| } | |
| } | |
| } | |
| else | |
| { | |
| assert(m_drawPassCount <= kMaxReorderedDrawPassCount); | |
| // Sort the draw list to optimize batching, since we can only batch | |
| // non-overlapping draws. | |
| auto& indirectDrawList = m_ctx->m_indirectDrawList; | |
| indirectDrawList.clear(); | |
| indirectDrawList.reserve(m_drawPassCount); | |
| // TODO: For clockwiseAtomic, these next values aren't constant (they're | |
| // constants now just to have stand-in values representing the default | |
| // case). Instead: | |
| // - There would be (at least) three relevant "overlap bits": | |
| // - color buffer write | |
| // - clip buffer read | |
| // - clip buffer write | |
| // - groupingType should be GroupingType::overlapAllowed (unless there | |
| // is some reason the current draw could *never* overlap anything | |
| // else) | |
| // - Any draws that write to the color buffer (which may include draws | |
| // that also use the *clip* buffer) would set the "color buffer | |
| // write" bit in its overlap bits | |
| // - Draws that are using advanced blending would set the "color buffer | |
| // write" bit in its disallow mask, so that they are not allowed to | |
| // overlap things that write to the color buffer (there is nothing | |
| // extra for advanced blending that goes into the overlap bits - | |
| // advanced blending has no bearing on whether or not things can | |
| // overlap on top of it!) | |
| // - Any draws that read from the clip buffer: | |
| // - set the "clip buffer read" bit in `overlapBits` - this gets | |
| // stored with the rectangle and signifies that the rectangle is | |
| // involved in a clip buffer read | |
| // - sets the "clip buffer write" bit in `disallowOverlapMask` - this | |
| // tells the intersection board that if this draw overlaps a clip | |
| // buffer write, it needs to go in the next draw group (there needs | |
| // to be a barrier) | |
| // - Any draws that write to the clip buffer: | |
| // - set the "clip buffer write" bit in `overlapBits` | |
| // - sets *both* the "clip buffer read/write" bits in | |
| // `disallowOverlapMask` - this means that these draws would need a | |
| // barrier between any previous overlapping clip buffer reads or | |
| // writes. | |
| // - Similarly, the ordering of the bits in the sort key would likely | |
| // want to change for this mode to ensure that the sorting preserves | |
| // proper ordering within a given draw group, since now there are | |
| // overlaps and thus draw ordering can matter. | |
| // (it also might be worth double checking that there aren't other | |
| // modes where a different sort ordering could be more efficient, to | |
| // perhaps better group like things together that don't cause | |
| // barriers) | |
| constexpr static uint16_t kOverlapBits = 0; | |
| constexpr static uint16_t kDisallowOverlapMask = 0; | |
| constexpr static GroupingType kGroupingType = GroupingType::disjoint; | |
| if (m_ctx->m_intersectionBoard == nullptr) | |
| { | |
| m_ctx->m_intersectionBoard = | |
| std::make_unique<IntersectionBoard>(kGroupingType); | |
| } | |
| assert(m_ctx->m_intersectionBoard->groupingType() == kGroupingType); | |
| IntersectionBoard* intersectionBoard = m_ctx->m_intersectionBoard.get(); | |
| intersectionBoard->resizeAndReset(m_flushDesc.renderTarget->width(), | |
| m_flushDesc.renderTarget->height()); | |
| static constexpr SortKeyBuilder keyBuilder{ | |
| // Our top priority in re-ordering is to group non-overlapping draws | |
| // together, in order to maximize batching while preserving | |
| // correctness. | |
| {.entry = SortEntry::drawGroup, .bitCount = 15}, | |
| // Within sub-groups of non-overlapping draws, sort similar draw | |
| // types together. | |
| {.entry = SortEntry::drawType, .bitCount = 3}, | |
| // Within sub-groups of matching draw type, sort by texture binding. | |
| {.entry = SortEntry::textureHash, .bitCount = 14}, | |
| // It's less expensive to change the scissorID than texture, but | |
| // more expensive than the blend mode, so here's where it lives. | |
| {.entry = SortEntry::scissorID, .bitCount = 15}, | |
| // If using KHR_blend_equation_advanced, we need a batching barrier | |
| // between draws with different blend modes. If not using | |
| // KHR_blend_equation_advanced, sorting by blend mode may still give | |
| // us better branching on the GPU. | |
| {.entry = SortEntry::blendMode, .bitCount = 4}, | |
| // depthStencil mode draws strokes, fills, and even/odd with | |
| // different stencil settings. | |
| {.entry = SortEntry::drawContents, .bitCount = 9}, | |
| // Finally, we need sorting by subpass. Without this, the | |
| // depthStencil subpasses (and maybe others) won't run in the | |
| // correct order when allSubpassesInSameDrawGroup was true. | |
| {.entry = SortEntry::subpassIndex, .bitCount = 3}, | |
| }; | |
| m_ctx->m_scissorIDLookup.clear(); | |
| // Set this to 0, any actual scissor IDs used will then start at 1. | |
| m_ctx->m_prevScissorID = 0; | |
| for (int16_t drawIndex = 0; drawIndex < int16_t(m_draws.size()); | |
| ++drawIndex) | |
| { | |
| Draw* draw = m_draws[drawIndex].get(); | |
| int16_t scissorID = 0; | |
| auto drawPixelBoundRect = draw->pixelBounds(); | |
| if (platformFeatures.supportsClipScissor && | |
| (draw->clipID() != 0 || | |
| draw->clippingPixelBounds().has_value())) | |
| { | |
| const auto drawClipID = draw->clipID(); | |
| // Start with either the clipping pixel bounds (if they exist) | |
| // or a maximally-large rectangle. | |
| auto clipBounds = | |
| draw->clippingPixelBounds().value_or(IAABB::makeMaximal()); | |
| if (drawClipID != 0) | |
| { | |
| // Intersect with the tightened clip bounds if there was a | |
| // clip in the stack (which may be tighter than it was when | |
| // originally rendered - but also there may have been a clip | |
| // rect that happened after this clip path, which is why the | |
| // intersect still needs to happen) | |
| clipBounds = clipBounds.intersect( | |
| getClipInfo(drawClipID).tightenedBounds); | |
| } | |
| const auto drawBounds = draw->pixelBounds(); | |
| if (needsScissor(drawBounds, | |
| clipBounds, | |
| frameDescriptor().renderTargetWidth, | |
| frameDescriptor().renderTargetHeight)) | |
| { | |
| drawPixelBoundRect = clipBounds; | |
| const auto clipBoundsU16 = | |
| clipBounds.clamp_cast<uint16_t>(); | |
| // If the value is already in the map, get it, otherwise | |
| // we'll add the next new ID (which is 1 + the size of | |
| // the array, since we're using "0" as "no scissor") | |
| auto result = m_ctx->m_scissorIDLookup.try_emplace( | |
| clipBoundsU16, | |
| m_ctx->m_prevScissorID + 1); | |
| scissorID = result.first->second; | |
| assert(scissorID > 0); | |
| if (scissorID > m_ctx->m_prevScissorID) | |
| { | |
| ++m_ctx->m_prevScissorID; | |
| } | |
| // Update the scissor rect for this draw so we can | |
| // ensure it doesn't batch with draws with different | |
| // scissor rects. | |
| draw->setScissorRect(clipBoundsU16); | |
| } | |
| } | |
| int4 drawBounds = simd::load4i(&drawPixelBoundRect); | |
| // Add one extra pixel of padding to the draw bounds to make | |
| // absolutely certain we get no overlapping pixels, which destroy | |
| // the atomic shader. | |
| constexpr int32_t Max32i = std::numeric_limits<int32_t>::max(); | |
| constexpr int32_t Min32i = std::numeric_limits<int32_t>::min(); | |
| drawBounds = simd::if_then_else( | |
| drawBounds != int4{Min32i, Min32i, Max32i, Max32i}, | |
| drawBounds + int4{-1, -1, 1, 1}, | |
| drawBounds); | |
| if (m_ctx->frameInterlockMode() == | |
| gpu::InterlockMode::clockwiseAtomic && | |
| enums::is_flag_set(draw->drawContents(), | |
| gpu::DrawContents::clipUpdate)) | |
| { | |
| // ***FIXME***: until we implement scissors for clipping, | |
| // clockwiseAtomic clip updates can't be reordered. Expand their | |
| // pixel bounds to block reordering. | |
| drawBounds = { | |
| 0, | |
| 0, | |
| static_cast<int32_t>( | |
| m_ctx->frameDescriptor().renderTargetWidth), | |
| static_cast<int32_t>( | |
| m_ctx->frameDescriptor().renderTargetHeight), | |
| }; | |
| } | |
| // When the dstBlend barrier has no other option than to copy out a | |
| // texture, this copy destroys MSAA information and we can no longer | |
| // put subpasses in different drawGroups. | |
| // Otherwise, we put subpasses into different draw groups because it | |
| // yields better reordering. | |
| const bool allSubpassesInSameDrawGroup = | |
| m_ctx->frameInterlockMode() == | |
| gpu::InterlockMode::depthStencil && | |
| !platformFeatures.supportsBlendAdvancedKHR && | |
| enums::is_flag_set(m_combinedDrawContents, | |
| gpu::DrawContents::advancedBlend); | |
| // Our top priority in re-ordering is to group non-overlapping draws | |
| // together, in order to maximize batching while preserving | |
| // correctness. | |
| const int8_t maxSubpasses = math::lossless_numeric_cast<int8_t>( | |
| std::max(draw->prepassCount(), draw->subpassCount())); | |
| int16_t drawGroupIdx = intersectionBoard->addRectangle( | |
| drawBounds, | |
| kOverlapBits, | |
| kDisallowOverlapMask, | |
| allSubpassesInSameDrawGroup ? 1 : maxSubpasses); | |
| assert(drawGroupIdx > 0); | |
| const auto textureHash = | |
| (draw->imageTexture() != nullptr) | |
| ? draw->imageTexture()->textureResourceHash() | |
| : 0; | |
| int64_t key = keyBuilder.buildKey({ | |
| {SortEntry::blendMode, | |
| gpu::ConvertBlendModeToPLSBlendMode(draw->blendMode())}, | |
| {SortEntry::drawContents, draw->drawContents()}, | |
| {SortEntry::drawGroup, drawGroupIdx}, | |
| {SortEntry::drawType, draw->type()}, | |
| {SortEntry::scissorID, scissorID}, | |
| {SortEntry::subpassIndex, 0}, // This gets added later | |
| // The hash may lose bits in the key | |
| {SortEntry::textureHash, textureHash, ValidateKeyEntry::no}, | |
| }); | |
| // Add the first prepass and subpass, if any. | |
| if (draw->prepassCount() > 0) | |
| { | |
| // Negating the key is an easy way to sort the prepasses | |
| // front-to-back, and before the subpasses. | |
| indirectDrawList.push_back({ | |
| .sortKey = -key, | |
| .drawIndex = drawIndex, | |
| }); | |
| } | |
| if (draw->subpassCount() > 0) | |
| { | |
| indirectDrawList.push_back({ | |
| .sortKey = key, | |
| .drawIndex = drawIndex, | |
| }); | |
| } | |
| // Add any additional passes. | |
| if (maxSubpasses > 1) | |
| { | |
| const auto subpassKeyIncrement = | |
| allSubpassesInSameDrawGroup | |
| // Special case: All subpasses belong to the same | |
| // drawGroup, so only increment subpassIndex. | |
| ? keyBuilder.buildPartialKey({ | |
| {SortEntry::subpassIndex, 1}, | |
| }) | |
| // Usual case: Increment the drawGroup and subpassIndex | |
| // both at once. (The intersectionBoard already reserved | |
| // "maxPasses" layers of drawGroupIndices for us.) | |
| : keyBuilder.buildPartialKey({ | |
| {SortEntry::drawGroup, 1}, | |
| {SortEntry::subpassIndex, 1}, | |
| }); | |
| for (int8_t subpassIndex = 1; subpassIndex < maxSubpasses; | |
| ++subpassIndex) | |
| { | |
| key += subpassKeyIncrement; | |
| assert(keyBuilder.extract<int16_t>(SortEntry::drawGroup, | |
| key) == | |
| int16_t(allSubpassesInSameDrawGroup | |
| ? drawGroupIdx | |
| : drawGroupIdx + subpassIndex)); | |
| if (subpassIndex < draw->prepassCount()) | |
| { | |
| // Negating the key is an easy way to sort the prepasses | |
| // front-to-back, and before the subpasses. | |
| indirectDrawList.push_back({ | |
| .sortKey = -key, | |
| .drawIndex = drawIndex, | |
| }); | |
| } | |
| if (subpassIndex < draw->subpassCount()) | |
| { | |
| indirectDrawList.push_back({ | |
| .sortKey = key, | |
| .drawIndex = drawIndex, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| assert(indirectDrawList.size() == m_drawPassCount); | |
| // Re-order the draws | |
| // TODO: If we have any overlappable draws, then we will actually need | |
| // to sort using the draw index as well (negatively to go front-to-back | |
| // for pre-passes and positively for standard passes). Otherwise, the | |
| // order of draws *within* a given sorted key does not matter at all. | |
| std::sort( | |
| indirectDrawList.begin(), | |
| indirectDrawList.end(), | |
| [](const auto& a, const auto& b) { return a.sortKey < b.sortKey; }); | |
| assert(m_pendingBarriers == BarrierFlags::none); | |
| if (m_ctx->frameInterlockMode() == gpu::InterlockMode::atomics && | |
| platformFeatures.atomicPLSInitNeedsDraw) | |
| { | |
| // Atomic mode sometimes needs to initialize PLS with a draw when | |
| // the backend can't do it with typical clear/load APIs. | |
| // So far only Metal needs this, and its implementation doesn't | |
| // require a barrier before or after. | |
| m_drawList.emplace_back(m_ctx->perFrameAllocator(), | |
| gpu::DrawType::renderPassInitialize, | |
| m_baselineShaderMiscFlags, | |
| gpu::DrawContents::none, | |
| 1, | |
| 0, | |
| BlendMode::srcOver, | |
| ImageSampler::LinearClamp(), | |
| BarrierFlags::none); | |
| } | |
| else if (m_ctx->frameInterlockMode() == | |
| gpu::InterlockMode::depthStencil && | |
| m_flushDesc.colorLoadAction == | |
| gpu::LoadAction::preserveRenderTarget && | |
| platformFeatures.msaaColorPreserveNeedsDraw) | |
| { | |
| // When implemented with a transient attachment, MSAA needs us to | |
| // draw the old renderTarget contents into the framebuffer at the | |
| // beginning of the render pass when | |
| // LoadAction::preserveRenderTarget is specified. | |
| m_drawList.emplace_back( | |
| m_ctx->perFrameAllocator(), | |
| gpu::DrawType::renderPassInitialize, | |
| m_baselineShaderMiscFlags, | |
| gpu::DrawContents::opaquePaint, | |
| 1, | |
| 0, | |
| // A more realistic value here would be "BlendMode::none" (which | |
| // is what actually happens), but since that isn't a Rive blend | |
| // mode, we just need any value here. It will get ignored by the | |
| // draw. | |
| BlendMode::srcOver, | |
| ImageSampler{.filter = ImageFilter::bilinear}, | |
| // The MSAA init reads the framebuffer, so it needs the | |
| // equivalent of a "dstBlend" barrier. | |
| BarrierFlags::dstBlend); | |
| m_combinedDrawContents |= m_drawList.tail()->drawContents; | |
| // The draw that follows the this init will need a special | |
| // "msaaPostInit" barrier. | |
| pushBarriers(BarrierFlags::msaaPostInit); | |
| assert(m_dstBlendBarrierListTail == &m_firstDstBlendBarrier); | |
| assert(m_firstDstBlendBarrier == nullptr); | |
| m_firstDstBlendBarrier = m_drawList.tail(); | |
| m_dstBlendBarrierListTail = &m_drawList.tail()->nextDstBlendBarrier; | |
| } | |
| // Indicates required barriers between draws whose keys differ on the | |
| // given mask. | |
| struct BarriersForKeyDiff | |
| { | |
| int64_t mask; | |
| BarrierFlags barrier; | |
| }; | |
| StackVector<BarriersForKeyDiff, 3> barriersForKeyDiffs; | |
| // Find a mask that tells us when to insert barriers, and which barriers | |
| // are needed. When the keys of two adjacent draws differ within this | |
| // bitmask, we insert a barrier between them. | |
| switch (m_flushDesc.interlockMode) | |
| { | |
| case gpu::InterlockMode::atomics: | |
| { | |
| // In atomic mode, we need barriers any time draws overlap. | |
| // Insert a barrier every time the drawGroupIdx changes. | |
| barriersForKeyDiffs.push_back( | |
| {keyBuilder.mask(SortEntry::drawGroup), | |
| BarrierFlags::plsAtomic | BarrierFlags::drawBatchBreak}); | |
| // We need a plsAtomic barrier after the initial clears, loads, | |
| // etc. | |
| pushBarriers(BarrierFlags::plsAtomic | | |
| BarrierFlags::drawBatchBreak); | |
| break; | |
| } | |
| case gpu::InterlockMode::rasterOrdering: | |
| case gpu::InterlockMode::clockwise: | |
| // clockwise and rasterOrdering modes don't need barriers, but | |
| // we still reorder in order to improve batching. | |
| break; | |
| case gpu::InterlockMode::clockwiseAtomic: | |
| { | |
| // In clockwiseAtomic mode, we only need a barrier between the | |
| // borrowedCoverage prepasses and the main rendering. Prepasses | |
| // have a negative key, so just insert a barrier when the sign | |
| // changes. | |
| constexpr static int64_t SIGN_BIT = (1ll << 63); | |
| barriersForKeyDiffs.push_back( | |
| {SIGN_BIT, | |
| BarrierFlags::clockwiseBorrowedCoverage | | |
| BarrierFlags::drawBatchBreak}); | |
| // Just break batching between draw groups. If we also need a | |
| // dstBlend or "clip read" (plsAtomic) barrier, that will be | |
| // handled with more sophisticated logic later on. | |
| barriersForKeyDiffs.push_back( | |
| {keyBuilder.mask(SortEntry::drawGroup), | |
| BarrierFlags::drawBatchBreak}); | |
| if (indirectDrawList.empty() || | |
| indirectDrawList[0].sortKey >= 0) | |
| { | |
| // There are no borrowed coverage passes. Initiate the | |
| // transition to the main subpass immediately. | |
| pushBarriers(BarrierFlags::clockwiseBorrowedCoverage); | |
| } | |
| break; | |
| } | |
| case gpu::InterlockMode::depthStencil: | |
| { | |
| // depthStencil mode can't batch draws that overlap because they | |
| // both rely on the stencil buffer across subpasses. Stop | |
| // batching every time the drawGroupIdx changes. | |
| int64_t needsBreakMask = keyBuilder.mask(SortEntry::drawGroup); | |
| // depthStencil mode draws clips, strokes, fills, and even/odd | |
| // with different stencil settings, so these can't be batched. | |
| needsBreakMask |= keyBuilder.mask(SortEntry::drawContents); | |
| if (platformFeatures.supportsBlendAdvancedKHR) | |
| { | |
| // If using KHR_blend_equation_advanced, we also need to | |
| // stop batching between blend modes in order to change the | |
| // blend equation. | |
| needsBreakMask |= keyBuilder.mask(SortEntry::blendMode); | |
| } | |
| // depthStencil barriers only need to prevent batching of draws | |
| // for now. If we also need a dstBlend barrier, that will be | |
| // decided later. | |
| barriersForKeyDiffs.push_back( | |
| {needsBreakMask, BarrierFlags::drawBatchBreak}); | |
| break; | |
| } | |
| } | |
| // Write out the draw data from the sorted draw list, and build up a | |
| // condensed/batched list of low-level draws. | |
| constexpr int64_t BEGIN_KEY = std::numeric_limits<int64_t>::min(); | |
| int64_t priorSignedKey = BEGIN_KEY; | |
| int16_t currentDrawGroup = -1; | |
| DrawBatch* firstBatchInCurrentDrawGroup = nullptr; | |
| // clockwiseAtomic (CWA) needs more sophisticated barrier logic for | |
| // clips. | |
| bool hasCWAClipReadBarrier = false; | |
| bool currentDrawGroupHasCWAClipUpdate = false; | |
| for (const auto& sortEntry : indirectDrawList) | |
| { | |
| auto signedKey = sortEntry.sortKey; | |
| assert(signedKey >= priorSignedKey); | |
| // The first draw never gets simple barriers. If barriers are | |
| // required before the first draw, those get scheduled outside this | |
| // loop. | |
| if (priorSignedKey != BEGIN_KEY) | |
| { | |
| for (auto [mask, barriers] : barriersForKeyDiffs) | |
| { | |
| if ((priorSignedKey & mask) != (signedKey & mask)) | |
| { | |
| pushBarriers(barriers); | |
| } | |
| } | |
| } | |
| auto key = abs(signedKey); | |
| auto drawIndex = sortEntry.drawIndex; | |
| auto subpassIndex = | |
| keyBuilder.extract<int8_t>(SortEntry::subpassIndex, key); | |
| if (signedKey < 0) | |
| { | |
| // Negative keys are a prepass. Update the subpassIndex to be | |
| // negative. | |
| subpassIndex = -1 - subpassIndex; | |
| } | |
| // FIXME: m_currentZIndex shouldn't be a stateful variable; it | |
| // should be passed to pushToRenderContext() instead. | |
| const int16_t drawGroup = | |
| keyBuilder.extract<int16_t>(SortEntry::drawGroup, key); | |
| assert(drawGroup > 0); | |
| m_currentZIndex = drawGroup; | |
| Draw* draw = m_draws[drawIndex].get(); | |
| assert( | |
| draw->drawContents() == | |
| keyBuilder.extract<gpu::DrawContents>(SortEntry::drawContents, | |
| key)); | |
| assert(draw->blendMode() != BlendMode::srcOver == | |
| draw->hasAdvancedBlend()); | |
| DrawBatch* batch = draw->pushToRenderContext(this, subpassIndex); | |
| if (batch != nullptr && platformFeatures.supportsClipScissor) | |
| { | |
| batch->scissorRect = draw->scissorRect(); | |
| } | |
| // Some barriers need more sophisticated logic than "do my keys | |
| // differ". | |
| if ((m_ctx->frameInterlockMode() == | |
| gpu::InterlockMode::clockwiseAtomic || | |
| m_ctx->frameInterlockMode() == | |
| gpu::InterlockMode::depthStencil) && | |
| subpassIndex == 0 && batch != nullptr) | |
| { | |
| // Barriers at this level have to go on the first batch in the | |
| // current drawGroup. Otherwise we might see something get | |
| // reordered like this: | |
| // | |
| // - drawA, subpass0 | |
| // - dstBlendBarrier (because drawB has a dstBlend) | |
| // - drawB, subpass0 | |
| // - drawA, subpass1 | |
| // - drawB, subpass1 | |
| // | |
| // In this scenario, drawA gets a dstBlend barrier between | |
| // subpasses. When dstBlend is implemented as a texture copy, it | |
| // interrupts the render pass and resolves MSAA, causing the | |
| // MSAA data to be lost between supbasses of drawA. | |
| // | |
| // Since drawA and drawB don't overlap, the correct solution is | |
| // to only apply barriers on the first batch of a drawGroup: | |
| // | |
| // - dstBlendBarrier (because drawB has a dstBlend) | |
| // - drawA, subpass0 | |
| // - drawB, subpass0 | |
| // - drawA, subpass1 | |
| // - drawB, subpass1 | |
| // | |
| // (This also leads to fewer barriers overall.) | |
| if (currentDrawGroup != drawGroup) | |
| { | |
| if (currentDrawGroupHasCWAClipUpdate) | |
| { | |
| // Now that we're moving on to a new drawGroup, reset | |
| // hasCWAClipReadBarrier if the clip got written, since | |
| // any future clip reads may overlap what was written. | |
| hasCWAClipReadBarrier = false; | |
| currentDrawGroupHasCWAClipUpdate = false; | |
| } | |
| firstBatchInCurrentDrawGroup = batch; | |
| currentDrawGroup = drawGroup; | |
| } | |
| assert(firstBatchInCurrentDrawGroup != nullptr); | |
| if (draw->hasAdvancedBlend() && | |
| (m_ctx->frameInterlockMode() != | |
| gpu::InterlockMode::depthStencil || | |
| !m_ctx->platformFeatures() | |
| .supportsBlendAdvancedCoherentKHR)) | |
| { | |
| // An implementation-dependent barrier is required between | |
| // overlapping draws. Add a "dstBlend" barrier and build up | |
| // a list of "dstReads" for the batch. The dstRead list will | |
| // be required in the event that the implementation has to | |
| // handle dstReads by copying out a texture. | |
| assert(draw->nextDstRead() == nullptr); | |
| firstBatchInCurrentDrawGroup->dstReadList = | |
| draw->addToDstReadList( | |
| firstBatchInCurrentDrawGroup->dstReadList); | |
| if (!enums::is_flag_set( | |
| firstBatchInCurrentDrawGroup->barriers, | |
| BarrierFlags::dstBlend)) | |
| { | |
| firstBatchInCurrentDrawGroup->barriers |= | |
| BarrierFlags::dstBlend; | |
| addBatchToDstBarrierList(firstBatchInCurrentDrawGroup); | |
| } | |
| // We either added ourselves to the dstBlendBarrier | |
| // list or merged into a batch that was already part | |
| // of it. | |
| assert(m_dstBlendBarrierListTail == | |
| &firstBatchInCurrentDrawGroup->nextDstBlendBarrier); | |
| } | |
| // clockwiseAtomic (CWA) needs more sophisticated barrier logic | |
| // for clips. | |
| if (m_ctx->frameInterlockMode() == | |
| gpu::InterlockMode::clockwiseAtomic) | |
| { | |
| if (draw->isClipUpdate()) | |
| { | |
| // Once the clip gets written, it needs a barrier before | |
| // it can be read again from fragment shaders. | |
| // | |
| // NOTE: This won't immediately reset | |
| // "hasCWAClipReadBarrier" because clip reads and writes | |
| // in the same drawGroup don't overlap. Instead, we | |
| // defer resetting hasCWAClipReadBarrier until we begin | |
| // the next drawGroup. | |
| // | |
| // NOTE2: It's ok of activeClip is also set here | |
| // (i.e., nested clip updates, or "clipUpdate | | |
| // activeClip"). Those don't need a barrier. Nested | |
| // clips use hardware blend to apply the existing clip, | |
| // rather than reading it in the fragment shader. | |
| currentDrawGroupHasCWAClipUpdate = true; | |
| } | |
| else if (draw->hasActiveClip() && !hasCWAClipReadBarrier) | |
| { | |
| // Clipped path draws need a barrier because they access | |
| // the clip buffer via input attachment in the fragment | |
| // shader. | |
| firstBatchInCurrentDrawGroup->barriers |= | |
| gpu::BarrierFlags::plsAtomic; | |
| hasCWAClipReadBarrier = true; | |
| } | |
| } | |
| else | |
| { | |
| assert(m_ctx->frameInterlockMode() == | |
| gpu::InterlockMode::depthStencil); | |
| // depthStencil doesn't mix srcOver draws with advanced | |
| // blend draws. | |
| assert(enums::is_flag_set( | |
| batch->shaderFeatures, | |
| gpu::ShaderFeatures::ENABLE_ADVANCED_BLEND) == | |
| (draw->blendMode() != BlendMode::srcOver)); | |
| // If using KHR_blend_equation_advanced, we can't mix blend | |
| // modes in a batch. | |
| assert( | |
| !m_ctx->platformFeatures().supportsBlendAdvancedKHR || | |
| batch->firstBlendMode == draw->blendMode()); | |
| } | |
| } | |
| priorSignedKey = signedKey; | |
| } | |
| } | |
| // Some modes need one more draw to resolve all the pixels. | |
| if (m_ctx->frameInterlockMode() == gpu::InterlockMode::atomics || | |
| m_flushDesc.manuallyResolved) | |
| { | |
| m_drawList.emplace_back( | |
| m_ctx->perFrameAllocator(), | |
| gpu::DrawType::renderPassResolve, | |
| m_baselineShaderMiscFlags, | |
| (m_ctx->frameInterlockMode() == gpu::InterlockMode::atomics) | |
| ? gpu::DrawContents::none | |
| : gpu::DrawContents::opaquePaint, | |
| 1, | |
| 0, | |
| BlendMode::srcOver, | |
| ImageSampler::LinearClamp(), | |
| (m_ctx->frameInterlockMode() == gpu::InterlockMode::atomics) | |
| ? BarrierFlags::plsAtomicPreResolve | |
| : BarrierFlags::preManualResolve); | |
| m_combinedDrawContents |= m_drawList.tail()->drawContents; | |
| } | |
| // Write out the draws to the feather atlas. Do this after the main draws | |
| // (even though the atlas ones execute first) so that our path info and Z | |
| // index are decided and available to pushFeatherAtlasTessellation(). | |
| if (!m_pendingFeatherAtlasDraws.empty()) | |
| { | |
| AABBu16 fullAtlasViewport = {0, | |
| 0, | |
| m_flushDesc.featherAtlasContentWidth, | |
| m_flushDesc.featherAtlasContentHeight}; | |
| gpu::AtlasDrawBatch* currentBatch = | |
| m_ctx->m_perFrameAllocator.makePODArray<gpu::AtlasDrawBatch>( | |
| m_pendingFeatherAtlasDraws.size()); | |
| // Iterate the atlas draws 4 times so we can sort by fill / stroke / | |
| // scissored / not, and batch together the draws that don't have | |
| // scissor. | |
| for (bool stroked : {false, true}) | |
| { | |
| if (stroked) | |
| { | |
| m_flushDesc.featherAtlasStrokeBatches = currentBatch; | |
| } | |
| else | |
| { | |
| m_flushDesc.featherAtlasFillBatches = currentBatch; | |
| } | |
| for (bool scissored : {false, true}) | |
| { | |
| gpu::AtlasDrawBatch* lastBatch = nullptr; | |
| for (PathDraw* draw : m_pendingFeatherAtlasDraws) | |
| { | |
| if (draw->isStroke() != stroked || | |
| draw->featherAtlasScissorEnabled() != scissored) | |
| { | |
| continue; | |
| } | |
| uint32_t tessVertexCount, tessBaseVertex; | |
| draw->pushFeatherAtlasTessellation(this, | |
| &tessVertexCount, | |
| &tessBaseVertex); | |
| if (tessVertexCount == 0) | |
| { | |
| continue; | |
| } | |
| uint32_t patchCount = | |
| tessVertexCount / gpu::kMidpointFanPatchSegmentSpan; | |
| uint32_t basePatch = | |
| tessBaseVertex / gpu::kMidpointFanPatchSegmentSpan; | |
| assert(patchCount * gpu::kMidpointFanPatchSegmentSpan == | |
| tessVertexCount); | |
| assert(basePatch * gpu::kMidpointFanPatchSegmentSpan == | |
| tessBaseVertex); | |
| if (lastBatch == nullptr || scissored) | |
| { | |
| lastBatch = currentBatch++; | |
| *lastBatch = { | |
| lastBatch->scissor = | |
| scissored ? draw->featherAtlasScissor() | |
| : fullAtlasViewport, | |
| lastBatch->patchCount = patchCount, | |
| lastBatch->basePatch = basePatch, | |
| }; | |
| } | |
| else | |
| { | |
| assert(lastBatch->basePatch + lastBatch->patchCount == | |
| basePatch); | |
| lastBatch->patchCount += patchCount; | |
| } | |
| } | |
| } | |
| if (stroked) | |
| { | |
| m_flushDesc.featherAtlasStrokeBatchCount = | |
| currentBatch - m_flushDesc.featherAtlasStrokeBatches; | |
| } | |
| else | |
| { | |
| m_flushDesc.featherAtlasFillBatchCount = | |
| currentBatch - m_flushDesc.featherAtlasFillBatches; | |
| } | |
| } | |
| assert(m_flushDesc.featherAtlasFillBatchCount + | |
| m_flushDesc.featherAtlasStrokeBatchCount == | |
| currentBatch - m_flushDesc.featherAtlasFillBatches); | |
| assert(m_flushDesc.featherAtlasFillBatchCount + | |
| m_flushDesc.featherAtlasStrokeBatchCount <= | |
| m_pendingFeatherAtlasDraws.size()); | |
| } | |
| // Pad our buffers to 256-byte alignment. | |
| m_ctx->m_pathData.push_back_n(nullptr, m_pathPaddingCount); | |
| m_ctx->m_paintData.push_back_n(nullptr, m_paintPaddingCount); | |
| m_ctx->m_paintAuxData.push_back_n(nullptr, m_paintAuxPaddingCount); | |
| m_ctx->m_contourData.push_back_n(nullptr, m_contourPaddingCount); | |
| m_ctx->m_gradSpanData.push_back_n(nullptr, m_gradSpanPaddingCount); | |
| assert(m_ctx->m_pathData.elementsWritten() == | |
| m_flushDesc.firstPath + m_resourceCounts.pathCount + | |
| m_pathPaddingCount); | |
| assert(m_ctx->m_paintData.elementsWritten() == | |
| m_flushDesc.firstPaint + m_resourceCounts.pathCount + | |
| m_paintPaddingCount); | |
| assert(m_ctx->m_paintAuxData.elementsWritten() == | |
| m_flushDesc.firstPaintAux + m_resourceCounts.pathCount + | |
| m_paintAuxPaddingCount); | |
| assert(m_ctx->m_contourData.elementsWritten() == | |
| m_flushDesc.firstContour + m_resourceCounts.contourCount + | |
| m_contourPaddingCount); | |
| assert(m_ctx->m_gradSpanData.elementsWritten() == | |
| m_flushDesc.firstGradSpan + m_pendingGradSpanCount + | |
| m_gradSpanPaddingCount); | |
| assert(m_midpointFanTessVertexIdx == m_midpointFanTessEndLocation); | |
| assert(m_outerCubicTessVertexIdx == m_outerCubicTessEndLocation); | |
| // Some of the flushDescriptor's data isn't known until after | |
| // writeResources(). Update it now that it's known. | |
| m_flushDesc.combinedShaderFeatures = m_combinedShaderFeatures; | |
| if (m_coverageBufferLength > 0) | |
| { | |
| assert(m_flushDesc.interlockMode == | |
| gpu::InterlockMode::clockwiseAtomic); | |
| // The coverage buffer prefix gets reset to zero when the buffer is | |
| // reallocated, so wait until here to get the prefix. | |
| m_flushDesc.coverageBufferPrefix = m_ctx->incrementCoverageBufferPrefix( | |
| &m_flushDesc.needsCoverageBufferClear); | |
| } | |
| m_flushDesc.tessVertexSpanCount = math::lossless_numeric_cast<uint32_t>( | |
| m_ctx->m_tessSpanData.elementsWritten() - | |
| m_flushDesc.firstTessVertexSpan); | |
| m_flushDesc.hasTriangleVertices = | |
| m_ctx->m_triangleVertexData.bytesWritten() != | |
| initialTriangleVertexDataSize; | |
| m_flushDesc.drawList = &m_drawList; | |
| m_flushDesc.firstDstBlendBarrier = m_firstDstBlendBarrier; | |
| m_flushDesc.unresolvedBarriers = m_pendingBarriers; | |
| // Write out the uniforms for this flush now that the flushDescriptor is | |
| // complete. | |
| m_ctx->m_flushUniformData.emplace_back(m_flushDesc, platformFeatures); | |
| #ifndef NDEBUG | |
| for (const DrawBatch& batch : *m_flushDesc.drawList) | |
| { | |
| assert((batch.drawContents & m_combinedDrawContents) == | |
| batch.drawContents); | |
| assert((batch.shaderFeatures & m_flushDesc.combinedShaderFeatures) == | |
| batch.shaderFeatures); | |
| } | |
| #endif | |
| } | |
| void RenderContext::LogicalFlush::tightenClipBounds() | |
| { | |
| assert(m_combinedDrawBounds == IAABB::makeMaximallyNegative() && | |
| "m_combinedDrawBounds should not have been updated yet"); | |
| // Iterate through the draws in reverse - this ensures that all paths | |
| // clipped by a given clip update will update read bounds first, then any | |
| // nested clips will update, and all bounds state should bubble nicely to | |
| // the top. | |
| for (size_t i = m_draws.size() - 1; i != size_t(-1); i--) | |
| { | |
| const auto& draw = m_draws[i]; | |
| // Depending on whether the platform supports clip scissor or not, use | |
| // the clipped bounds or the pixel bounds as the default bounds for | |
| // calculating the combined bounds. | |
| IAABB drawBoundsForCombinedBounds = | |
| m_ctx->platformFeatures().supportsClipScissor | |
| ? draw->clippedPixelBounds() | |
| : draw->pixelBounds(); | |
| if (draw->clipID() == 0) | |
| { | |
| // Do nothing here, but we'll update the combined draw bounds after | |
| // the `else`s. | |
| } | |
| else if (draw->isClipUpdate()) | |
| { | |
| auto& clipInfo = getWritableClipInfo(draw->clipID()); | |
| // Ensure the clip's write bounds are as tight on both the clip | |
| // shape and all reads as possible. | |
| clipInfo.tightenedBounds = | |
| clipInfo.tightenedBounds.intersect(clipInfo.readBounds); | |
| if (m_ctx->platformFeatures().supportsClipScissor) | |
| { | |
| // Bring in the draw bounds for combining based on the | |
| // newly-tightened bounds. | |
| assert(drawBoundsForCombinedBounds.contains( | |
| clipInfo.tightenedBounds)); | |
| drawBoundsForCombinedBounds = | |
| clipInfo.tightenedBounds.lossless_numeric_cast<int32_t>(); | |
| } | |
| if (draw->hasActiveClip()) | |
| { | |
| assert(clipInfo.parentClipID != 0); | |
| // This is a nested clip so we need to additionally add its | |
| // adjusted bounds to its parent clip as its read bounds. | |
| auto& parentClipInfo = | |
| getWritableClipInfo(clipInfo.parentClipID); | |
| parentClipInfo.readBounds = | |
| parentClipInfo.readBounds.join(clipInfo.tightenedBounds); | |
| } | |
| else | |
| { | |
| assert(clipInfo.parentClipID == 0); | |
| } | |
| } | |
| else if (draw->hasActiveClip()) | |
| { | |
| // Anything else with a clip should add itself to the clip's read | |
| // bounds. | |
| auto& clipInfo = getWritableClipInfo(draw->clipID()); | |
| clipInfo.readBounds = clipInfo.readBounds.join( | |
| draw->clippedPixelBounds().clamp_cast<uint16_t>()); | |
| } | |
| m_combinedDrawBounds = | |
| m_combinedDrawBounds.join(drawBoundsForCombinedBounds); | |
| } | |
| } | |
| void RenderContext::setResourceSizes(ResourceAllocationCounts allocs, | |
| bool forceRealloc) | |
| { | |
| RIVE_PROF_SCOPE_L(1) | |
| #if 0 | |
| class Logger | |
| { | |
| public: | |
| void logSize(const char* name, | |
| size_t oldSize, | |
| size_t newSize, | |
| size_t newSizeInBytes) | |
| { | |
| m_totalSizeInBytes += newSizeInBytes; | |
| if (oldSize == newSize) | |
| { | |
| return; | |
| } | |
| if (!m_hasChanged) | |
| { | |
| printf("RenderContext::setResourceSizes():\n"); | |
| m_hasChanged = true; | |
| } | |
| printf(" resize %s: %zu -> %zu (%zu KiB)\n", | |
| name, | |
| oldSize, | |
| newSize, | |
| newSizeInBytes >> 10); | |
| } | |
| void logTextureSize(const char* widthName, | |
| const char* heightName, | |
| size_t oldWidth, | |
| size_t oldHeight, | |
| size_t newWidth, | |
| size_t newHeight, | |
| size_t bytesPerPixel) | |
| { | |
| m_totalSizeInBytes += newHeight * newWidth * bytesPerPixel; | |
| if (oldWidth == newWidth && oldHeight == newHeight) | |
| { | |
| return; | |
| } | |
| if (!m_hasChanged) | |
| { | |
| printf("RenderContext::setResourceSizes():\n"); | |
| m_hasChanged = true; | |
| } | |
| printf(" resize %s x %s: %zu x %zu -> %zu x %zu (%zu KiB)\n", | |
| widthName, | |
| heightName, | |
| oldWidth, | |
| oldHeight, | |
| newWidth, | |
| newHeight, | |
| (newHeight * newWidth * bytesPerPixel) >> 10); | |
| } | |
| void logTexture3dSize(const char* name, | |
| size_t oldWidth, | |
| size_t oldHeight, | |
| size_t oldDepth, | |
| size_t newWidth, | |
| size_t newHeight, | |
| size_t newDepth, | |
| size_t bytesPerPixel) | |
| { | |
| m_totalSizeInBytes += newHeight * newWidth * bytesPerPixel; | |
| if (oldWidth == newWidth && oldHeight == newHeight && | |
| oldDepth == newDepth) | |
| { | |
| return; | |
| } | |
| if (!m_hasChanged) | |
| { | |
| printf("RenderContext::setResourceSizes():\n"); | |
| m_hasChanged = true; | |
| } | |
| printf(" resize %s: [%zu x %zu x %zu] -> [%zu x %zu x %zu] " | |
| "(%zu KiB)\n", | |
| name, | |
| oldWidth, | |
| oldHeight, | |
| oldDepth, | |
| newWidth, | |
| newHeight, | |
| newDepth, | |
| (newHeight * newWidth * newDepth * bytesPerPixel) >> 10); | |
| } | |
| ~Logger() | |
| { | |
| if (!m_hasChanged) | |
| { | |
| return; | |
| } | |
| printf(" TOTAL GPU resource usage: %zu KiB\n", | |
| m_totalSizeInBytes >> 10); | |
| } | |
| private: | |
| size_t m_totalSizeInBytes = 0; | |
| bool m_hasChanged = false; | |
| } logger; | |
| #define LOG_BUFFER_RING_SIZE(NAME, ITEM_SIZE_IN_BYTES) \ | |
| logger.logSize(#NAME, \ | |
| m_currentResourceAllocations.NAME, \ | |
| allocs.NAME, \ | |
| allocs.NAME* ITEM_SIZE_IN_BYTES* gpu::kBufferRingSize) | |
| #define LOG_TEXTURE_SIZE(NAME, BYTES_PER_VALUE) \ | |
| logger.logSize(#NAME, \ | |
| m_currentResourceAllocations.NAME, \ | |
| allocs.NAME, \ | |
| allocs.NAME*(BYTES_PER_VALUE)) | |
| #define LOG_TEXTURE_2D_SIZE(NAME, WIDTH_NAME, HEIGHT_NAME, BYTES_PER_PIXEL) \ | |
| logger.logTexture3dSize(NAME, \ | |
| m_currentResourceAllocations.WIDTH_NAME, \ | |
| m_currentResourceAllocations.HEIGHT_NAME, \ | |
| 1, \ | |
| allocs.WIDTH_NAME, \ | |
| allocs.HEIGHT_NAME, \ | |
| 1, \ | |
| BYTES_PER_PIXEL) | |
| #define LOG_TEXTURE_3D_SIZE(NAME, \ | |
| WIDTH_NAME, \ | |
| HEIGHT_NAME, \ | |
| DEPTH_NAME, \ | |
| BYTES_PER_PIXEL) \ | |
| logger.logTexture3dSize(NAME, \ | |
| m_currentResourceAllocations.WIDTH_NAME, \ | |
| m_currentResourceAllocations.HEIGHT_NAME, \ | |
| m_currentResourceAllocations.DEPTH_NAME, \ | |
| allocs.WIDTH_NAME, \ | |
| allocs.HEIGHT_NAME, \ | |
| allocs.DEPTH_NAME, \ | |
| BYTES_PER_PIXEL) | |
| #define LOG_BUFFER_SIZE(NAME, BYTES_PER_ELEMENT) \ | |
| logger.logSize(#NAME, \ | |
| m_currentResourceAllocations.NAME, \ | |
| allocs.NAME, \ | |
| allocs.NAME* BYTES_PER_ELEMENT) | |
| #else | |
| #define LOG_BUFFER_RING_SIZE(NAME, ITEM_SIZE_IN_BYTES) | |
| #define LOG_TEXTURE_SIZE(NAME, BYTES_PER_ROW) | |
| #define LOG_TEXTURE_2D_SIZE(NAME, WIDTH_NAME, HEIGHT_NAME, BYTES_PER_PIXEL) | |
| #define LOG_TEXTURE_3D_SIZE(NAME, \ | |
| WIDTH_NAME, \ | |
| HEIGHT_NAME, \ | |
| DEPTH_NAME, \ | |
| BYTES_PER_PIXEL) | |
| #define LOG_BUFFER_SIZE(NAME, BYTES_PER_ELEMENT) | |
| #endif | |
| LOG_BUFFER_RING_SIZE(flushUniformBufferCount, sizeof(gpu::FlushUniforms)); | |
| if (allocs.flushUniformBufferCount != | |
| m_currentResourceAllocations.flushUniformBufferCount || | |
| forceRealloc) | |
| { | |
| m_impl->resizeFlushUniformBuffer(allocs.flushUniformBufferCount * | |
| sizeof(gpu::FlushUniforms)); | |
| } | |
| LOG_BUFFER_RING_SIZE(pathBufferCount, sizeof(gpu::PathData)); | |
| if (allocs.pathBufferCount != | |
| m_currentResourceAllocations.pathBufferCount || | |
| forceRealloc) | |
| { | |
| m_impl->resizePathBuffer(allocs.pathBufferCount * sizeof(gpu::PathData), | |
| gpu::PathData::kBufferStructure); | |
| } | |
| LOG_BUFFER_RING_SIZE(paintBufferCount, sizeof(gpu::PaintData)); | |
| if (allocs.paintBufferCount != | |
| m_currentResourceAllocations.paintBufferCount || | |
| forceRealloc) | |
| { | |
| m_impl->resizePaintBuffer(allocs.paintBufferCount * | |
| sizeof(gpu::PaintData), | |
| gpu::PaintData::kBufferStructure); | |
| } | |
| LOG_BUFFER_RING_SIZE(paintAuxBufferCount, sizeof(gpu::PaintAuxData)); | |
| if (allocs.paintAuxBufferCount != | |
| m_currentResourceAllocations.paintAuxBufferCount || | |
| forceRealloc) | |
| { | |
| m_impl->resizePaintAuxBuffer(allocs.paintAuxBufferCount * | |
| sizeof(gpu::PaintAuxData), | |
| gpu::PaintAuxData::kBufferStructure); | |
| } | |
| LOG_BUFFER_RING_SIZE(contourBufferCount, sizeof(gpu::ContourData)); | |
| if (allocs.contourBufferCount != | |
| m_currentResourceAllocations.contourBufferCount || | |
| forceRealloc) | |
| { | |
| m_impl->resizeContourBuffer(allocs.contourBufferCount * | |
| sizeof(gpu::ContourData), | |
| gpu::ContourData::kBufferStructure); | |
| } | |
| LOG_BUFFER_RING_SIZE(gradSpanBufferCount, sizeof(gpu::GradientSpan)); | |
| if (allocs.gradSpanBufferCount != | |
| m_currentResourceAllocations.gradSpanBufferCount || | |
| forceRealloc) | |
| { | |
| m_impl->resizeGradSpanBuffer(allocs.gradSpanBufferCount * | |
| sizeof(gpu::GradientSpan)); | |
| } | |
| LOG_BUFFER_RING_SIZE(tessSpanBufferCount, sizeof(gpu::TessVertexSpan)); | |
| if (allocs.tessSpanBufferCount != | |
| m_currentResourceAllocations.tessSpanBufferCount || | |
| forceRealloc) | |
| { | |
| m_impl->resizeTessVertexSpanBuffer(allocs.tessSpanBufferCount * | |
| sizeof(gpu::TessVertexSpan)); | |
| } | |
| LOG_BUFFER_RING_SIZE(triangleVertexBufferCount, | |
| sizeof(gpu::TriangleVertex)); | |
| if (allocs.triangleVertexBufferCount != | |
| m_currentResourceAllocations.triangleVertexBufferCount || | |
| forceRealloc) | |
| { | |
| m_impl->resizeTriangleVertexBuffer(allocs.triangleVertexBufferCount * | |
| sizeof(gpu::TriangleVertex)); | |
| } | |
| LOG_BUFFER_RING_SIZE(imageDrawInstanceBufferCount, | |
| sizeof(gpu::ImageDrawInstance)); | |
| if (allocs.imageDrawInstanceBufferCount != | |
| m_currentResourceAllocations.imageDrawInstanceBufferCount || | |
| forceRealloc) | |
| { | |
| m_impl->resizeImageDrawInstanceBuffer( | |
| allocs.imageDrawInstanceBufferCount * | |
| sizeof(gpu::ImageDrawInstance)); | |
| } | |
| assert(allocs.gradTextureHeight <= kMaxTextureHeight); | |
| LOG_TEXTURE_SIZE(gradTextureHeight, gpu::kGradTextureWidth * 4); | |
| if (allocs.gradTextureHeight != | |
| m_currentResourceAllocations.gradTextureHeight || | |
| forceRealloc) | |
| { | |
| m_impl->resizeGradientTexture( | |
| gpu::kGradTextureWidth, | |
| math::lossless_numeric_cast<uint32_t>(allocs.gradTextureHeight)); | |
| } | |
| assert(allocs.tessTextureHeight <= kMaxTextureHeight); | |
| LOG_TEXTURE_SIZE(tessTextureHeight, gpu::kTessTextureWidth * 4 * 4); | |
| if (allocs.tessTextureHeight != | |
| m_currentResourceAllocations.tessTextureHeight || | |
| forceRealloc) | |
| { | |
| m_impl->resizeTessellationTexture( | |
| gpu::kTessTextureWidth, | |
| math::lossless_numeric_cast<uint32_t>(allocs.tessTextureHeight)); | |
| } | |
| assert(allocs.featherAtlasTextureWidth <= featherAtlasMaxSize() || | |
| allocs.featherAtlasTextureWidth <= | |
| frameDescriptor().renderTargetWidth); | |
| assert(allocs.featherAtlasTextureHeight <= featherAtlasMaxSize() || | |
| allocs.featherAtlasTextureHeight <= | |
| frameDescriptor().renderTargetHeight); | |
| LOG_TEXTURE_2D_SIZE("featherAtlasTexture", | |
| featherAtlasTextureWidth, | |
| featherAtlasTextureHeight, | |
| sizeof(uint16_t)); | |
| if (allocs.featherAtlasTextureWidth != | |
| m_currentResourceAllocations.featherAtlasTextureWidth || | |
| allocs.featherAtlasTextureHeight != | |
| m_currentResourceAllocations.featherAtlasTextureHeight || | |
| forceRealloc) | |
| { | |
| m_impl->resizeFeatherAtlasTexture( | |
| math::lossless_numeric_cast<uint32_t>( | |
| allocs.featherAtlasTextureWidth), | |
| math::lossless_numeric_cast<uint32_t>( | |
| allocs.featherAtlasTextureHeight)); | |
| } | |
| assert(allocs.plsTransientBackingPlaneCount <= | |
| RenderContextImpl::PLS_TRANSIENT_BACKING_MAX_PLANE_COUNT); | |
| LOG_TEXTURE_3D_SIZE("plsTransientBacking", | |
| plsTransientBackingWidth, | |
| plsTransientBackingHeight, | |
| plsTransientBackingPlaneCount, | |
| sizeof(uint32_t)); | |
| if (allocs.plsTransientBackingWidth != | |
| m_currentResourceAllocations.plsTransientBackingWidth || | |
| allocs.plsTransientBackingHeight != | |
| m_currentResourceAllocations.plsTransientBackingHeight || | |
| allocs.plsTransientBackingPlaneCount != | |
| m_currentResourceAllocations.plsTransientBackingPlaneCount || | |
| forceRealloc) | |
| { | |
| m_impl->resizeTransientPLSBacking( | |
| math::lossless_numeric_cast<uint32_t>( | |
| allocs.plsTransientBackingWidth), | |
| math::lossless_numeric_cast<uint32_t>( | |
| allocs.plsTransientBackingHeight), | |
| math::lossless_numeric_cast<uint32_t>( | |
| allocs.plsTransientBackingPlaneCount)); | |
| } | |
| assert(allocs.plsAtomicCoverageBackingWidth <= | |
| allocs.plsTransientBackingWidth); | |
| assert(allocs.plsAtomicCoverageBackingHeight <= | |
| allocs.plsTransientBackingHeight); | |
| LOG_TEXTURE_2D_SIZE("plsAtomicCoverageBacking", | |
| plsAtomicCoverageBackingWidth, | |
| plsAtomicCoverageBackingHeight, | |
| sizeof(uint32_t)); | |
| if (allocs.plsAtomicCoverageBackingWidth != | |
| m_currentResourceAllocations.plsAtomicCoverageBackingWidth || | |
| allocs.plsAtomicCoverageBackingHeight != | |
| m_currentResourceAllocations.plsAtomicCoverageBackingHeight || | |
| forceRealloc) | |
| { | |
| m_impl->resizeAtomicCoverageBacking( | |
| math::lossless_numeric_cast<uint32_t>( | |
| allocs.plsAtomicCoverageBackingWidth), | |
| math::lossless_numeric_cast<uint32_t>( | |
| allocs.plsAtomicCoverageBackingHeight)); | |
| } | |
| assert(allocs.coverageBufferLength <= | |
| platformFeatures().maxCoverageBufferLength); | |
| LOG_BUFFER_SIZE(coverageBufferLength, sizeof(uint32_t)); | |
| if (allocs.coverageBufferLength != | |
| m_currentResourceAllocations.coverageBufferLength || | |
| forceRealloc) | |
| { | |
| m_impl->resizeCoverageBuffer(allocs.coverageBufferLength * | |
| sizeof(uint32_t)); | |
| // Start the coverageBufferPrefix over at zero. This ensure the new | |
| // buffer gets cleared because the only criteria for clearing it is when | |
| // the prefix wraps around to 0. | |
| m_coverageBufferPrefix = 0; | |
| } | |
| m_currentResourceAllocations = allocs; | |
| } | |
| bool RenderContext::mapResourceBuffers( | |
| const ResourceAllocationCounts& mapCounts) | |
| { | |
| RIVE_PROF_SCOPE_L(1) | |
| #define HANDLE_MAP_FAILURE(...) \ | |
| do \ | |
| { \ | |
| if (!(__VA_ARGS__)) \ | |
| { \ | |
| return false; \ | |
| } \ | |
| } while (false) | |
| if (mapCounts.flushUniformBufferCount > 0) | |
| { | |
| HANDLE_MAP_FAILURE(m_flushUniformData.mapElements( | |
| m_impl.get(), | |
| &RenderContextImpl::mapFlushUniformBuffer, | |
| mapCounts.flushUniformBufferCount)); | |
| } | |
| assert(m_flushUniformData.hasRoomFor(mapCounts.flushUniformBufferCount)); | |
| if (mapCounts.pathBufferCount > 0) | |
| { | |
| HANDLE_MAP_FAILURE( | |
| m_pathData.mapElements(m_impl.get(), | |
| &RenderContextImpl::mapPathBuffer, | |
| mapCounts.pathBufferCount)); | |
| } | |
| assert(m_pathData.hasRoomFor(mapCounts.pathBufferCount)); | |
| if (mapCounts.paintBufferCount > 0) | |
| { | |
| HANDLE_MAP_FAILURE( | |
| m_paintData.mapElements(m_impl.get(), | |
| &RenderContextImpl::mapPaintBuffer, | |
| mapCounts.paintBufferCount)); | |
| } | |
| assert(m_paintData.hasRoomFor(mapCounts.paintBufferCount)); | |
| if (mapCounts.paintAuxBufferCount > 0) | |
| { | |
| HANDLE_MAP_FAILURE( | |
| m_paintAuxData.mapElements(m_impl.get(), | |
| &RenderContextImpl::mapPaintAuxBuffer, | |
| mapCounts.paintAuxBufferCount)); | |
| } | |
| assert(m_paintAuxData.hasRoomFor(mapCounts.paintAuxBufferCount)); | |
| if (mapCounts.contourBufferCount > 0) | |
| { | |
| HANDLE_MAP_FAILURE( | |
| m_contourData.mapElements(m_impl.get(), | |
| &RenderContextImpl::mapContourBuffer, | |
| mapCounts.contourBufferCount)); | |
| } | |
| assert(m_contourData.hasRoomFor(mapCounts.contourBufferCount)); | |
| if (mapCounts.gradSpanBufferCount > 0) | |
| { | |
| HANDLE_MAP_FAILURE( | |
| m_gradSpanData.mapElements(m_impl.get(), | |
| &RenderContextImpl::mapGradSpanBuffer, | |
| mapCounts.gradSpanBufferCount)); | |
| } | |
| assert(m_gradSpanData.hasRoomFor(mapCounts.gradSpanBufferCount)); | |
| if (mapCounts.tessSpanBufferCount > 0) | |
| { | |
| HANDLE_MAP_FAILURE(m_tessSpanData.mapElements( | |
| m_impl.get(), | |
| &RenderContextImpl::mapTessVertexSpanBuffer, | |
| mapCounts.tessSpanBufferCount)); | |
| } | |
| assert(m_tessSpanData.hasRoomFor(mapCounts.tessSpanBufferCount)); | |
| if (mapCounts.triangleVertexBufferCount > 0) | |
| { | |
| HANDLE_MAP_FAILURE(m_triangleVertexData.mapElements( | |
| m_impl.get(), | |
| &RenderContextImpl::mapTriangleVertexBuffer, | |
| mapCounts.triangleVertexBufferCount)); | |
| } | |
| assert( | |
| m_triangleVertexData.hasRoomFor(mapCounts.triangleVertexBufferCount)); | |
| if (mapCounts.imageDrawInstanceBufferCount > 0) | |
| { | |
| HANDLE_MAP_FAILURE(m_imageDrawInstanceData.mapElements( | |
| m_impl.get(), | |
| &RenderContextImpl::mapImageDrawInstanceBuffer, | |
| mapCounts.imageDrawInstanceBufferCount)); | |
| } | |
| assert(m_imageDrawInstanceData.hasRoomFor( | |
| mapCounts.imageDrawInstanceBufferCount > 0)); | |
| #undef HANDLE_MAP_FAILURE | |
| return true; | |
| } | |
| void RenderContext::unmapResourceBuffers( | |
| const ResourceAllocationCounts& mapCounts) | |
| { | |
| RIVE_PROF_SCOPE_L(1) | |
| if (m_flushUniformData) | |
| { | |
| m_flushUniformData.unmapElements( | |
| m_impl.get(), | |
| &RenderContextImpl::unmapFlushUniformBuffer, | |
| mapCounts.flushUniformBufferCount); | |
| } | |
| if (m_pathData) | |
| { | |
| m_pathData.unmapElements(m_impl.get(), | |
| &RenderContextImpl::unmapPathBuffer, | |
| mapCounts.pathBufferCount); | |
| } | |
| if (m_paintData) | |
| { | |
| m_paintData.unmapElements(m_impl.get(), | |
| &RenderContextImpl::unmapPaintBuffer, | |
| mapCounts.paintBufferCount); | |
| } | |
| if (m_paintAuxData) | |
| { | |
| m_paintAuxData.unmapElements(m_impl.get(), | |
| &RenderContextImpl::unmapPaintAuxBuffer, | |
| mapCounts.paintAuxBufferCount); | |
| } | |
| if (m_contourData) | |
| { | |
| m_contourData.unmapElements(m_impl.get(), | |
| &RenderContextImpl::unmapContourBuffer, | |
| mapCounts.contourBufferCount); | |
| } | |
| if (m_gradSpanData) | |
| { | |
| m_gradSpanData.unmapElements(m_impl.get(), | |
| &RenderContextImpl::unmapGradSpanBuffer, | |
| mapCounts.gradSpanBufferCount); | |
| } | |
| if (m_tessSpanData) | |
| { | |
| m_tessSpanData.unmapElements( | |
| m_impl.get(), | |
| &RenderContextImpl::unmapTessVertexSpanBuffer, | |
| mapCounts.tessSpanBufferCount); | |
| } | |
| if (m_triangleVertexData) | |
| { | |
| m_triangleVertexData.unmapElements( | |
| m_impl.get(), | |
| &RenderContextImpl::unmapTriangleVertexBuffer, | |
| mapCounts.triangleVertexBufferCount); | |
| } | |
| if (m_imageDrawInstanceData) | |
| { | |
| m_imageDrawInstanceData.unmapElements( | |
| m_impl.get(), | |
| &RenderContextImpl::unmapImageDrawInstanceBuffer, | |
| mapCounts.imageDrawInstanceBufferCount); | |
| } | |
| } | |
| uint32_t RenderContext::incrementCoverageBufferPrefix( | |
| bool* needsCoverageBufferClear) | |
| { | |
| RIVE_PROF_SCOPE_L(1) | |
| assert(m_didBeginFrame); | |
| assert(frameInterlockMode() == gpu::InterlockMode::clockwiseAtomic); | |
| do | |
| { | |
| if (m_coverageBufferPrefix == 0) | |
| { | |
| // When the prefix wraps around to 0, we need to clear the coverage | |
| // buffer because our shaders require coverageBufferPrefix to be | |
| // monotonically increasing. | |
| *needsCoverageBufferClear = true; | |
| } | |
| m_coverageBufferPrefix += CLOCKWISE_COVERAGE_PREFIX_ONE_VALUE; | |
| } while (m_coverageBufferPrefix == 0); | |
| return m_coverageBufferPrefix; | |
| } | |
| uint32_t RenderContext::LogicalFlush::allocateMidpointFanTessVertices( | |
| uint32_t count) | |
| { | |
| uint32_t location = m_midpointFanTessVertexIdx; | |
| m_midpointFanTessVertexIdx += count; | |
| assert(m_midpointFanTessVertexIdx <= m_midpointFanTessEndLocation); | |
| return location; | |
| } | |
| uint32_t RenderContext::LogicalFlush::allocateOuterCubicTessVertices( | |
| uint32_t count) | |
| { | |
| uint32_t location = m_outerCubicTessVertexIdx; | |
| m_outerCubicTessVertexIdx += count; | |
| assert(m_outerCubicTessVertexIdx <= m_outerCubicTessEndLocation); | |
| return location; | |
| } | |
| uint32_t RenderContext::LogicalFlush::pushPath(const PathDraw* draw) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| assert(m_hasDoneLayout); | |
| ++m_currentPathID; | |
| assert(0 < m_currentPathID && m_currentPathID <= m_ctx->m_maxPathID); | |
| m_ctx->m_pathData.set_back(draw->paintMatrix(), | |
| draw->strokeRadius(), | |
| draw->featherRadius(), | |
| m_currentZIndex, | |
| draw->featherAtlasTransform(), | |
| draw->coverageBufferRange()); | |
| m_ctx->m_paintData.set_back(draw->drawContents(), | |
| draw->paintType(), | |
| draw->simplePaintValue(), | |
| m_gradTextureLayout, | |
| draw->clipID(), | |
| draw->hasClipRect(), | |
| draw->hasImageTexture(), | |
| draw->blendMode()); | |
| m_ctx->m_paintAuxData.set_back(draw->paintMatrix(), | |
| draw->imageMatrix(), | |
| draw->paintType(), | |
| draw->simplePaintValue(), | |
| draw->gradient(), | |
| draw->imageTexture(), | |
| draw->clipRectInverseMatrix(), | |
| m_flushDesc.renderTarget, | |
| m_ctx->platformFeatures()); | |
| assert(m_flushDesc.firstPath + m_currentPathID + 1 == | |
| m_ctx->m_pathData.elementsWritten()); | |
| assert(m_flushDesc.firstPaint + m_currentPathID + 1 == | |
| m_ctx->m_paintData.elementsWritten()); | |
| assert(m_flushDesc.firstPaintAux + m_currentPathID + 1 == | |
| m_ctx->m_paintAuxData.elementsWritten()); | |
| return m_currentPathID; | |
| } | |
| RenderContext::TessellationWriter::TessellationWriter( | |
| LogicalFlush* flush, | |
| uint32_t pathID, | |
| gpu::ContourDirections contourDirections, | |
| uint32_t forwardTessVertexCount, | |
| uint32_t forwardTessLocation, | |
| uint32_t mirroredTessVertexCount, | |
| uint32_t mirroredTessLocation) : | |
| m_flush(flush), | |
| m_tessSpanData(m_flush->m_ctx->m_tessSpanData), | |
| m_pathID(pathID), | |
| m_contourDirections(contourDirections), | |
| m_pathTessLocation(forwardTessLocation), | |
| m_pathMirroredTessLocation(mirroredTessLocation) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| RIVE_DEBUG_CODE(m_expectedPathTessEndLocation = | |
| m_pathTessLocation + forwardTessVertexCount;) | |
| RIVE_DEBUG_CODE(m_expectedPathMirroredTessEndLocation = | |
| m_pathMirroredTessLocation - mirroredTessVertexCount;) | |
| assert(m_flush->m_hasDoneLayout); | |
| assert(m_flush->m_ctx->m_pathData.elementsWritten() > 0); | |
| assert(forwardTessVertexCount == 0 || mirroredTessVertexCount == 0 || | |
| forwardTessVertexCount == mirroredTessVertexCount); | |
| assert(!gpu::ContourDirectionsAreDoubleSided(m_contourDirections) || | |
| forwardTessVertexCount == mirroredTessVertexCount); | |
| assert(m_pathTessLocation >= 0); | |
| assert(m_pathMirroredTessLocation <= kMaxTessellationVertexCount); | |
| assert(m_expectedPathTessEndLocation <= kMaxTessellationVertexCount); | |
| assert(m_expectedPathMirroredTessEndLocation >= 0); | |
| } | |
| RenderContext::TessellationWriter::~TessellationWriter() | |
| { | |
| assert(m_pathTessLocation == m_expectedPathTessEndLocation); | |
| assert(m_pathMirroredTessLocation == m_expectedPathMirroredTessEndLocation); | |
| } | |
| uint32_t RenderContext::LogicalFlush::pushContour(uint32_t pathID, | |
| Vec2D midpoint, | |
| bool isStroke, | |
| bool closed, | |
| uint32_t vertexIndex0) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| assert(pathID != 0); | |
| assert(isStroke || closed); | |
| if (isStroke) | |
| { | |
| midpoint.x = closed ? 1 : 0; | |
| } | |
| m_ctx->m_contourData.emplace_back(midpoint, pathID, vertexIndex0); | |
| ++m_currentContourID; | |
| assert(0 < m_currentContourID && m_currentContourID <= gpu::kMaxContourID); | |
| assert(m_flushDesc.firstContour + m_currentContourID == | |
| m_ctx->m_contourData.elementsWritten()); | |
| return m_currentContourID; | |
| } | |
| uint32_t RenderContext::TessellationWriter::pushContour( | |
| Vec2D midpoint, | |
| bool isStroke, | |
| bool closed, | |
| uint32_t paddingVertexCount) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| // 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 math::padding_to_align_up().) | |
| m_nextCubicPaddingVertexCount = paddingVertexCount; | |
| return m_flush->pushContour(m_pathID, | |
| midpoint, | |
| isStroke, | |
| closed, | |
| nextVertexIndex()); | |
| } | |
| void RenderContext::TessellationWriter::pushCubic( | |
| const Vec2D pts[4], | |
| gpu::ContourDirections contourDirections, | |
| Vec2D joinTangent, | |
| uint32_t parametricSegmentCount, | |
| uint32_t polarSegmentCount, | |
| uint32_t joinSegmentCount, | |
| uint32_t contourIDWithFlags) | |
| { | |
| RIVE_PROF_SCOPE_L(3) | |
| assert(0 <= parametricSegmentCount && | |
| parametricSegmentCount <= kMaxParametricSegments); | |
| assert(0 <= polarSegmentCount && polarSegmentCount <= kMaxPolarSegments); | |
| assert(joinSegmentCount > 0); | |
| assert((contourIDWithFlags & CONTOUR_ID_MASK) == | |
| (m_flush->m_currentContourID & CONTOUR_ID_MASK)); | |
| // contourID can't be zero. | |
| assert((contourIDWithFlags & CONTOUR_ID_MASK) != 0); | |
| // contourID can't be out of range in the contour buffer. (Contour buffer | |
| // indices are 1-based.) | |
| assert((contourIDWithFlags & CONTOUR_ID_MASK) <= | |
| m_flush->desc().contourCount); | |
| // 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_nextCubicPaddingVertexCount + | |
| curveMergedVertexCount + joinSegmentCount - 1; | |
| // Only the first curve of a contour gets padding vertices. | |
| m_nextCubicPaddingVertexCount = 0; | |
| switch (contourDirections) | |
| { | |
| case gpu::ContourDirections::forward: | |
| pushTessellationSpans(pts, | |
| joinTangent, | |
| totalVertexCount, | |
| parametricSegmentCount, | |
| polarSegmentCount, | |
| joinSegmentCount, | |
| contourIDWithFlags); | |
| break; | |
| case gpu::ContourDirections::reverse: | |
| pushMirroredTessellationSpans(pts, | |
| joinTangent, | |
| totalVertexCount, | |
| parametricSegmentCount, | |
| polarSegmentCount, | |
| joinSegmentCount, | |
| contourIDWithFlags); | |
| break; | |
| case gpu::ContourDirections::reverseThenForward: | |
| case gpu::ContourDirections::forwardThenReverse: | |
| // m_pathTessLocation and m_pathMirroredTessLocation are already | |
| // configured, so at ths point we don't need to handle | |
| // reverseThenForward or forwardThenReverse differently. | |
| pushDoubleSidedTessellationSpans(pts, | |
| joinTangent, | |
| totalVertexCount, | |
| parametricSegmentCount, | |
| polarSegmentCount, | |
| joinSegmentCount, | |
| contourIDWithFlags); | |
| break; | |
| } | |
| } | |
| void RenderContext::TessellationWriter::pushRetrofitCubicTriStrip( | |
| const Vec2D pts[], | |
| size_t numPts, | |
| gpu::ContourDirections contourDirections, | |
| uint32_t contourIDWithFlags) | |
| { | |
| // gpu::TessVertexSpan has 5 points into which we can retrofit strip | |
| // vertices (pts[4] and joinTangent), giving us a maximum of 3 triangles we | |
| // can shoehorn into a single patch. Due to the topology of an outer cubic | |
| // patch, and the structure of the tessellation shader, the strip ordering | |
| // has to be: | |
| // | |
| // p0, p1, p3, p2, joinTangent | |
| // | |
| assert(3 <= numPts && numPts <= 5); | |
| Vec2D cubicTriangleStrip[4] = {pts[0], | |
| pts[1], | |
| pts[std::min<size_t>(3, numPts - 1)], | |
| pts[2]}; | |
| pushCubic(cubicTriangleStrip, | |
| contourDirections, | |
| pts[std::min<size_t>(4, numPts - 1)], | |
| gpu::OuterCubicPatchSegmentSpan, | |
| 1, | |
| 1, | |
| contourIDWithFlags | RETROFIT_TRI_STRIP_CONTOUR_FLAG); | |
| } | |
| RIVE_ALWAYS_INLINE void RenderContext::TessellationWriter:: | |
| pushTessellationSpans(const Vec2D pts[4], | |
| Vec2D joinTangent, | |
| uint32_t totalVertexCount, | |
| uint32_t parametricSegmentCount, | |
| uint32_t polarSegmentCount, | |
| uint32_t joinSegmentCount, | |
| uint32_t contourIDWithFlags) | |
| { | |
| RIVE_PROF_SCOPE_L(3) | |
| assert(totalVertexCount > 0); | |
| uint32_t y = m_pathTessLocation / kTessTextureWidth; | |
| int32_t x0 = m_pathTessLocation % kTessTextureWidth; | |
| int32_t x1 = x0 + totalVertexCount; | |
| for (;;) | |
| { | |
| m_tessSpanData.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_pathTessLocation + totalVertexCount - 1) / kTessTextureWidth); | |
| m_pathTessLocation += totalVertexCount; | |
| assert(m_pathTessLocation <= m_expectedPathTessEndLocation); | |
| } | |
| RIVE_ALWAYS_INLINE void RenderContext::TessellationWriter:: | |
| pushMirroredTessellationSpans(const Vec2D pts[4], | |
| Vec2D joinTangent, | |
| uint32_t totalVertexCount, | |
| uint32_t parametricSegmentCount, | |
| uint32_t polarSegmentCount, | |
| uint32_t joinSegmentCount, | |
| uint32_t contourIDWithFlags) | |
| { | |
| assert(totalVertexCount > 0); | |
| uint32_t reflectionY = (m_pathMirroredTessLocation - 1) / kTessTextureWidth; | |
| int32_t reflectionX0 = | |
| (m_pathMirroredTessLocation - 1) % kTessTextureWidth + 1; | |
| int32_t reflectionX1 = reflectionX0 - totalVertexCount; | |
| for (;;) | |
| { | |
| m_tessSpanData.set_back(pts, | |
| joinTangent, | |
| static_cast<float>(reflectionY), | |
| reflectionX0, | |
| reflectionX1, | |
| parametricSegmentCount, | |
| polarSegmentCount, | |
| joinSegmentCount, | |
| contourIDWithFlags); | |
| if (reflectionX1 < 0) | |
| { | |
| --reflectionY; | |
| reflectionX0 += kTessTextureWidth; | |
| reflectionX1 += kTessTextureWidth; | |
| continue; | |
| } | |
| break; | |
| } | |
| m_pathMirroredTessLocation -= totalVertexCount; | |
| assert(m_pathMirroredTessLocation >= m_expectedPathMirroredTessEndLocation); | |
| } | |
| RIVE_ALWAYS_INLINE void RenderContext::TessellationWriter:: | |
| pushDoubleSidedTessellationSpans(const Vec2D pts[4], | |
| Vec2D joinTangent, | |
| uint32_t totalVertexCount, | |
| uint32_t parametricSegmentCount, | |
| uint32_t polarSegmentCount, | |
| uint32_t joinSegmentCount, | |
| uint32_t contourIDWithFlags) | |
| { | |
| assert(totalVertexCount > 0); | |
| int32_t y = m_pathTessLocation / kTessTextureWidth; | |
| int32_t x0 = m_pathTessLocation % kTessTextureWidth; | |
| int32_t x1 = x0 + totalVertexCount; | |
| uint32_t reflectionY = (m_pathMirroredTessLocation - 1) / kTessTextureWidth; | |
| int32_t reflectionX0 = | |
| (m_pathMirroredTessLocation - 1) % kTessTextureWidth + 1; | |
| int32_t reflectionX1 = reflectionX0 - totalVertexCount; | |
| for (;;) | |
| { | |
| m_tessSpanData.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 both of them again, this time beyond | |
| // 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_pathTessLocation += totalVertexCount; | |
| assert(m_pathTessLocation <= m_expectedPathTessEndLocation); | |
| m_pathMirroredTessLocation -= totalVertexCount; | |
| assert(m_pathMirroredTessLocation >= m_expectedPathMirroredTessEndLocation); | |
| } | |
| void RenderContext::LogicalFlush::pushPaddingVertices(uint32_t count, | |
| uint32_t tessLocation) | |
| { | |
| RIVE_PROF_SCOPE_L(3) | |
| assert(m_hasDoneLayout); | |
| assert(count > 0); | |
| constexpr static Vec2D kEmptyCubic[4]{}; | |
| TessellationWriter(this, | |
| /*pathID=*/0, | |
| gpu::ContourDirections::forward, | |
| count, | |
| tessLocation) | |
| .pushTessellationSpans(kEmptyCubic, | |
| {0, 0}, | |
| count, | |
| 0, | |
| 0, | |
| 1, | |
| INVALID_CONTOUR_ID_WITH_FLAGS); | |
| } | |
| gpu::DrawBatch& RenderContext::LogicalFlush::pushMidpointFanDraw( | |
| const PathDraw* draw, | |
| gpu::DrawType drawType, | |
| uint32_t tessVertexCount, | |
| uint32_t tessLocation, | |
| gpu::ShaderMiscFlags shaderMiscFlags) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| assert(m_hasDoneLayout); | |
| uint32_t baseInstance = math::lossless_numeric_cast<uint32_t>( | |
| tessLocation / kMidpointFanPatchSegmentSpan); | |
| // flush() is responsible for alignment. | |
| assert(baseInstance * kMidpointFanPatchSegmentSpan == tessLocation); | |
| uint32_t instanceCount = tessVertexCount / kMidpointFanPatchSegmentSpan; | |
| // flush() is responsible for alignment. | |
| assert(instanceCount * kMidpointFanPatchSegmentSpan == tessVertexCount); | |
| return pushPathDraw(draw, | |
| drawType, | |
| shaderMiscFlags, | |
| instanceCount, | |
| baseInstance); | |
| } | |
| gpu::DrawBatch& RenderContext::LogicalFlush::pushOuterCubicsDraw( | |
| const PathDraw* draw, | |
| gpu::DrawType drawType, | |
| uint32_t tessVertexCount, | |
| uint32_t tessLocation, | |
| gpu::ShaderMiscFlags shaderMiscFlags) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| assert(m_hasDoneLayout); | |
| uint32_t baseInstance = math::lossless_numeric_cast<uint32_t>( | |
| tessLocation / OuterCubicPatchSegmentSpanPlusJoin); | |
| // flush() is responsible for alignment. | |
| assert(baseInstance * OuterCubicPatchSegmentSpanPlusJoin == tessLocation); | |
| uint32_t instanceCount = | |
| tessVertexCount / OuterCubicPatchSegmentSpanPlusJoin; | |
| // flush() is responsible for alignment. | |
| assert(instanceCount * OuterCubicPatchSegmentSpanPlusJoin == | |
| tessVertexCount); | |
| return pushPathDraw(draw, | |
| drawType, | |
| shaderMiscFlags, | |
| instanceCount, | |
| baseInstance); | |
| } | |
| gpu::DrawBatch* RenderContext::LogicalFlush::pushInteriorTriangulationDraw( | |
| const PathDraw* draw, | |
| uint32_t pathID, | |
| gpu::WindingFaces windingFaces, | |
| gpu::ShaderMiscFlags shaderMiscFlags RIVE_DEBUG_CODE(, | |
| size_t* vertexCounter)) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| assert(m_hasDoneLayout); | |
| assert(pathID != 0); | |
| uint32_t baseVertex = math::lossless_numeric_cast<uint32_t>( | |
| m_ctx->m_triangleVertexData.elementsWritten()); | |
| size_t actualVertexCount = draw->triangulator()->polysToTriangles( | |
| pathID, | |
| draw->pathFillRule(), | |
| draw->triangulatorReverseTriangles(), | |
| draw->triangulatorNegateWinding(), | |
| windingFaces, | |
| &m_ctx->m_triangleVertexData); | |
| assert(baseVertex + actualVertexCount == | |
| m_ctx->m_triangleVertexData.elementsWritten()); | |
| RIVE_DEBUG_CODE(*vertexCounter += actualVertexCount;) | |
| if (actualVertexCount > 0) | |
| { | |
| return &pushPathDraw( | |
| draw, | |
| DrawType::interiorTriangulation, | |
| shaderMiscFlags, | |
| math::lossless_numeric_cast<uint32_t>(actualVertexCount), | |
| baseVertex); | |
| } | |
| return nullptr; | |
| } | |
| gpu::DrawBatch& RenderContext::LogicalFlush::pushFeatherAtlasBlit( | |
| PathDraw* draw, | |
| uint32_t pathID) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| auto baseVertex = math::lossless_numeric_cast<uint32_t>( | |
| m_ctx->m_triangleVertexData.elementsWritten()); | |
| auto [l, t, r, b] = AABB(draw->pixelBounds()); | |
| m_ctx->m_triangleVertexData.emplace_back(Vec2D{l, b}, 1, pathID); | |
| m_ctx->m_triangleVertexData.emplace_back(Vec2D{l, t}, 1, pathID); | |
| m_ctx->m_triangleVertexData.emplace_back(Vec2D{r, b}, 1, pathID); | |
| m_ctx->m_triangleVertexData.emplace_back(Vec2D{r, b}, 1, pathID); | |
| m_ctx->m_triangleVertexData.emplace_back(Vec2D{l, t}, 1, pathID); | |
| m_ctx->m_triangleVertexData.emplace_back(Vec2D{r, t}, 1, pathID); | |
| return pushPathDraw(draw, | |
| DrawType::featherAtlasBlit, | |
| m_baselineShaderMiscFlags, | |
| 6, | |
| baseVertex); | |
| } | |
| gpu::DrawBatch& RenderContext::LogicalFlush::pushImageRectDraw( | |
| ImageRectDraw* draw) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| assert(m_hasDoneLayout); | |
| // If we support image paints for paths, the client should use pushPath() | |
| // with an image paint instead of calling this method. | |
| assert(!m_ctx->frameSupportsImagePaintForPaths()); | |
| const uint32_t imageDrawBaseInstance = | |
| math::lossless_numeric_cast<uint32_t>( | |
| m_ctx->m_imageDrawInstanceData.elementsWritten()); | |
| m_ctx->m_imageDrawInstanceData.emplace_back(draw->imageMatrix(), | |
| draw->opacity(), | |
| draw->clipRectInverseMatrix(), | |
| draw->clipID(), | |
| draw->blendMode(), | |
| m_currentZIndex); | |
| DrawBatch& batch = pushDraw(draw, | |
| DrawType::imageRect, | |
| m_baselineShaderMiscFlags, | |
| PaintType::solidColor, | |
| 1, | |
| imageDrawBaseInstance); | |
| return batch; | |
| } | |
| gpu::DrawBatch& RenderContext::LogicalFlush::pushImageMeshDraw( | |
| ImageMeshDraw* draw) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| assert(m_hasDoneLayout); | |
| const uint32_t imageDrawBaseInstance = | |
| math::lossless_numeric_cast<uint32_t>( | |
| m_ctx->m_imageDrawInstanceData.elementsWritten()); | |
| m_ctx->m_imageDrawInstanceData.emplace_back(draw->imageMatrix(), | |
| draw->opacity(), | |
| draw->clipRectInverseMatrix(), | |
| draw->clipID(), | |
| draw->blendMode(), | |
| m_currentZIndex); | |
| DrawBatch& batch = pushDraw(draw, | |
| DrawType::imageMesh, | |
| m_baselineShaderMiscFlags, | |
| PaintType::solidColor, | |
| 1, // one instance (the mesh) | |
| imageDrawBaseInstance); | |
| batch.indexCountPerInstance = draw->indexCount(); | |
| batch.vertexBuffer = draw->vertexBuffer(); | |
| batch.uvBuffer = draw->uvBuffer(); | |
| batch.indexBuffer = draw->indexBuffer(); | |
| return batch; | |
| } | |
| gpu::DrawBatch& RenderContext::LogicalFlush::pushClipResetDraw(ClipReset* draw) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| assert(m_hasDoneLayout); | |
| uint32_t baseVertex = math::lossless_numeric_cast<uint32_t>( | |
| m_ctx->m_triangleVertexData.elementsWritten()); | |
| auto [l, t, r, b] = AABB(getClipInfo(draw->previousClipID()).contentBounds); | |
| uint32_t z = m_currentZIndex; | |
| assert(AABB(l, t, r, b).round() == draw->pixelBounds()); | |
| assert(draw->resourceCounts().maxTriangleVertexCount == 6); | |
| assert(m_ctx->m_triangleVertexData.hasRoomFor(6)); | |
| m_ctx->m_triangleVertexData.emplace_back(Vec2D{l, b}, 0, z); | |
| m_ctx->m_triangleVertexData.emplace_back(Vec2D{l, t}, 0, z); | |
| m_ctx->m_triangleVertexData.emplace_back(Vec2D{r, b}, 0, z); | |
| m_ctx->m_triangleVertexData.emplace_back(Vec2D{r, b}, 0, z); | |
| m_ctx->m_triangleVertexData.emplace_back(Vec2D{l, t}, 0, z); | |
| m_ctx->m_triangleVertexData.emplace_back(Vec2D{r, t}, 0, z); | |
| return pushDraw(draw, | |
| DrawType::clipReset, | |
| gpu::ShaderMiscFlags::none, | |
| PaintType::clipUpdate, | |
| 6, | |
| baseVertex); | |
| } | |
| gpu::DrawBatch& RenderContext::LogicalFlush::pushPathDraw( | |
| const PathDraw* draw, | |
| DrawType drawType, | |
| gpu::ShaderMiscFlags shaderMiscFlags, | |
| uint32_t vertexCount, | |
| uint32_t baseVertex) | |
| { | |
| RIVE_PROF_SCOPE_L(3) | |
| assert(m_hasDoneLayout); | |
| // Clockwise fills get their own shaders in rasterOrdering mode. | |
| // TODO: eventually we will use draw_clockwise_path.frag for these | |
| // draws in rasterOrdering mode, instead of just making a variant of | |
| // draw_raster_order_path.frag. | |
| if (m_ctx->frameInterlockMode() == gpu::InterlockMode::rasterOrdering && | |
| enums::is_flag_set(draw->drawContents(), | |
| gpu::DrawContents::clockwiseFill)) | |
| { | |
| shaderMiscFlags |= gpu::ShaderMiscFlags::clockwiseFill; | |
| } | |
| DrawBatch& batch = pushDraw(draw, | |
| drawType, | |
| shaderMiscFlags, | |
| draw->paintType(), | |
| vertexCount, | |
| baseVertex); | |
| auto pathShaderFeatures = gpu::ShaderFeatures::NONE; | |
| if (draw->featherRadius() != 0 && | |
| drawType != gpu::DrawType::interiorTriangulation && | |
| drawType != gpu::DrawType::featherAtlasBlit) | |
| { | |
| pathShaderFeatures |= ShaderFeatures::ENABLE_FEATHER; | |
| } | |
| if (enums::is_flag_set(draw->drawContents(), | |
| gpu::DrawContents::evenOddFill)) | |
| { | |
| assert(!enums::is_flag_set(batch.shaderMiscFlags, | |
| gpu::ShaderMiscFlags::clockwiseFill)); | |
| pathShaderFeatures |= ShaderFeatures::ENABLE_EVEN_ODD; | |
| } | |
| constexpr static gpu::DrawContents NESTED_CLIP_FLAGS = | |
| gpu::DrawContents::clipUpdate | gpu::DrawContents::activeClip; | |
| if ((draw->drawContents() & NESTED_CLIP_FLAGS) == NESTED_CLIP_FLAGS) | |
| { | |
| pathShaderFeatures |= ShaderFeatures::ENABLE_NESTED_CLIPPING; | |
| } | |
| batch.shaderFeatures |= | |
| pathShaderFeatures & m_ctx->m_frameShaderFeaturesMask; | |
| assert( | |
| (batch.shaderFeatures & | |
| gpu::ShaderFeaturesMaskFor(drawType, m_ctx->frameInterlockMode())) == | |
| batch.shaderFeatures); | |
| m_combinedShaderFeatures |= batch.shaderFeatures; | |
| return batch; | |
| } | |
| RIVE_ALWAYS_INLINE static bool can_combine_shader_misc_flags( | |
| const gpu::DrawBatch* batch, | |
| const Draw* draw, | |
| gpu::ShaderMiscFlags shaderMiscFlags) | |
| { | |
| // If a path doesn't have ANY_PATH_FILL bits, it means it's a stroke. | |
| constexpr static auto ANY_PATH_FILL = gpu::DrawContents::clockwiseFill | | |
| gpu::DrawContents::evenOddFill | | |
| gpu::DrawContents::nonZeroFill; | |
| gpu::ShaderMiscFlags compareMask = ~gpu::ShaderMiscFlags::none; | |
| // Strokes draw identically in the clockwise and legacy shaders, so strokes | |
| // can be combined with paths of any fill type. | |
| if ((enums::no_flags_set(batch->drawContents, ANY_PATH_FILL) || | |
| enums::no_flags_set(draw->drawContents(), ANY_PATH_FILL))) | |
| { | |
| compareMask &= ~gpu::ShaderMiscFlags::clockwiseFill; | |
| } | |
| return (batch->shaderMiscFlags & compareMask) == | |
| (shaderMiscFlags & compareMask); | |
| } | |
| RIVE_ALWAYS_INLINE static bool can_combine_draw_images( | |
| const Texture* currentDrawTexture, | |
| const Texture* nextDrawTexture, | |
| const ImageSampler currentImageSamplerKey, | |
| const ImageSampler nextImageSamplerKey) | |
| { | |
| if (currentDrawTexture == nullptr || nextDrawTexture == nullptr) | |
| { | |
| // We can always combine two draws if one or both do not use an image | |
| // paint. | |
| return true; | |
| } | |
| // Since the image paint's texture must be bound to a specific slot, we | |
| // can't combine draws that use different textures. | |
| return (currentDrawTexture == nextDrawTexture) && | |
| (currentImageSamplerKey == nextImageSamplerKey); | |
| } | |
| constexpr uint32_t patchIndexCount(DrawType drawType) | |
| { | |
| switch (drawType) | |
| { | |
| case DrawType::midpointFanPatches: | |
| return kMidpointFanPatchIndexCount; | |
| case DrawType::midpointFanCenterAAPatches: | |
| return kMidpointFanCenterAAPatchIndexCount; | |
| case DrawType::outerCurvePatches: | |
| return kOuterCurvePatchIndexCount; | |
| case DrawType::depthStrokes: | |
| return kMidpointFanPatchBorderIndexCount; | |
| case DrawType::stencilMidpointFanBorrowedCoverage: | |
| case DrawType::stencilDynamicMidpointFans: | |
| case DrawType::stencilMidpointFans: | |
| case DrawType::stencilMidpointFanReset: | |
| case DrawType::stencilMidpointFanWinding: | |
| case DrawType::stencilMidpointFanCover: | |
| return kMidpointFanPatchIndexCount - | |
| kMidpointFanPatchBorderIndexCount; | |
| case DrawType::stencilOuterCubicBorrowedCoverage: | |
| case DrawType::stencilDynamicOuterCubics: | |
| case DrawType::stencilOuterCubics: | |
| case DrawType::stencilOuterCubicReset: | |
| case DrawType::stencilOuterCubicWinding: | |
| case DrawType::stencilOuterCubicCover: | |
| return kOuterCurvePatchIndexCount - | |
| kOuterCurvePatchBorderIndexCount; | |
| case DrawType::interiorTriangulation: | |
| case DrawType::featherAtlasBlit: | |
| case DrawType::imageRect: | |
| case DrawType::imageMesh: | |
| case DrawType::clipReset: | |
| case DrawType::renderPassInitialize: | |
| case DrawType::renderPassResolve: | |
| RIVE_UNREACHABLE(); | |
| } | |
| RIVE_UNREACHABLE(); | |
| } | |
| constexpr uint32_t patchBaseIndex(DrawType drawType) | |
| { | |
| switch (drawType) | |
| { | |
| case DrawType::midpointFanPatches: | |
| case DrawType::depthStrokes: | |
| return kMidpointFanPatchBaseIndex; | |
| case DrawType::midpointFanCenterAAPatches: | |
| return kMidpointFanCenterAAPatchBaseIndex; | |
| case DrawType::outerCurvePatches: | |
| return kOuterCurvePatchBaseIndex; | |
| case DrawType::stencilMidpointFanBorrowedCoverage: | |
| case DrawType::stencilDynamicMidpointFans: | |
| case DrawType::stencilMidpointFans: | |
| case DrawType::stencilMidpointFanReset: | |
| case DrawType::stencilMidpointFanWinding: | |
| case DrawType::stencilMidpointFanCover: | |
| return kMidpointFanPatchBaseIndex + | |
| kMidpointFanPatchBorderIndexCount; | |
| case DrawType::stencilOuterCubicBorrowedCoverage: | |
| case DrawType::stencilDynamicOuterCubics: | |
| case DrawType::stencilOuterCubics: | |
| case DrawType::stencilOuterCubicReset: | |
| case DrawType::stencilOuterCubicWinding: | |
| case DrawType::stencilOuterCubicCover: | |
| return kOuterCurvePatchBaseIndex + kOuterCurvePatchBorderIndexCount; | |
| case DrawType::interiorTriangulation: | |
| case DrawType::featherAtlasBlit: | |
| case DrawType::imageRect: | |
| case DrawType::imageMesh: | |
| case DrawType::clipReset: | |
| case DrawType::renderPassInitialize: | |
| case DrawType::renderPassResolve: | |
| RIVE_UNREACHABLE(); | |
| } | |
| RIVE_UNREACHABLE(); | |
| } | |
| static void assignDrawIndices(DrawType drawType, gpu::DrawBatch* batch) | |
| { | |
| switch (drawType) | |
| { | |
| case DrawType::midpointFanPatches: | |
| case DrawType::midpointFanCenterAAPatches: | |
| case DrawType::outerCurvePatches: | |
| case DrawType::depthStrokes: | |
| case DrawType::stencilMidpointFanBorrowedCoverage: | |
| case DrawType::stencilDynamicMidpointFans: | |
| case DrawType::stencilMidpointFans: | |
| case DrawType::stencilMidpointFanReset: | |
| case DrawType::stencilMidpointFanWinding: | |
| case DrawType::stencilMidpointFanCover: | |
| case DrawType::stencilOuterCubicBorrowedCoverage: | |
| case DrawType::stencilDynamicOuterCubics: | |
| case DrawType::stencilOuterCubics: | |
| case DrawType::stencilOuterCubicReset: | |
| case DrawType::stencilOuterCubicWinding: | |
| case DrawType::stencilOuterCubicCover: | |
| batch->indexCountPerInstance = patchIndexCount(drawType); | |
| batch->baseIndex = patchBaseIndex(drawType); | |
| break; | |
| case DrawType::imageRect: | |
| batch->indexCountPerInstance = std::size(kImageRectIndices); | |
| batch->baseIndex = 0; | |
| break; | |
| case DrawType::imageMesh: | |
| case DrawType::interiorTriangulation: | |
| case DrawType::featherAtlasBlit: | |
| case DrawType::clipReset: | |
| case DrawType::renderPassInitialize: | |
| case DrawType::renderPassResolve: | |
| batch->indexCountPerInstance = 0; | |
| batch->baseIndex = 0; | |
| break; | |
| default: | |
| RIVE_UNREACHABLE(); | |
| } | |
| } | |
| gpu::DrawBatch& RenderContext::LogicalFlush::pushDraw( | |
| const Draw* draw, | |
| DrawType drawType, | |
| gpu::ShaderMiscFlags shaderMiscFlags, | |
| gpu::PaintType paintType, | |
| uint32_t elementCount, | |
| uint32_t baseElement) | |
| { | |
| RIVE_PROF_SCOPE_L(3) | |
| assert(m_hasDoneLayout); | |
| assert(elementCount > 0); | |
| shaderMiscFlags |= m_baselineShaderMiscFlags; | |
| if ((m_ctx->frameInterlockMode() == gpu::InterlockMode::clockwise || | |
| (m_ctx->frameInterlockMode() == gpu::InterlockMode::clockwiseAtomic && | |
| !enums::is_flag_set(shaderMiscFlags, | |
| gpu::ShaderMiscFlags::borrowedCoveragePass))) && | |
| enums::is_flag_set(draw->drawContents(), gpu::DrawContents::clipUpdate)) | |
| { | |
| // Clockwise modes give clip updates a dedicated draw by setting | |
| // gpu::ShaderMiscFlags::clipUpdateOnly. | |
| shaderMiscFlags |= gpu::ShaderMiscFlags::clipUpdateOnly; | |
| if (m_ctx->frameInterlockMode() == | |
| gpu::InterlockMode::clockwiseAtomic && | |
| enums::is_flag_set(draw->drawContents(), | |
| gpu::DrawContents::activeClip)) | |
| { | |
| // clockwiseAtomic takes it a step futher and separates out nested | |
| // clip updates into their own draw type. | |
| shaderMiscFlags |= gpu::ShaderMiscFlags::nestedClipUpdateOnly; | |
| } | |
| } | |
| // In clockwiseAtomic and depthStencil modes, individual draws can use | |
| // fixedFunctionColorOutput even if the render pass as a whole does not. | |
| if (m_ctx->frameInterlockMode() == gpu::InterlockMode::clockwiseAtomic) | |
| { | |
| if (enums::is_flag_set(shaderMiscFlags, | |
| gpu::ShaderMiscFlags::borrowedCoveragePass) || | |
| draw->blendMode() == BlendMode::srcOver) | |
| { | |
| shaderMiscFlags |= gpu::ShaderMiscFlags::fixedFunctionColorOutput; | |
| } | |
| } | |
| else if (m_ctx->frameInterlockMode() == gpu::InterlockMode::depthStencil && | |
| draw->blendMode() == BlendMode::srcOver) | |
| { | |
| shaderMiscFlags |= gpu::ShaderMiscFlags::fixedFunctionColorOutput; | |
| } | |
| bool canMergeWithPreviousBatch; | |
| switch (drawType) | |
| { | |
| case DrawType::midpointFanPatches: | |
| case DrawType::midpointFanCenterAAPatches: | |
| case DrawType::outerCurvePatches: | |
| case DrawType::interiorTriangulation: | |
| case DrawType::featherAtlasBlit: | |
| case DrawType::depthStrokes: | |
| case DrawType::stencilMidpointFanBorrowedCoverage: | |
| case DrawType::stencilDynamicMidpointFans: | |
| case DrawType::stencilMidpointFans: | |
| case DrawType::stencilMidpointFanReset: | |
| case DrawType::stencilMidpointFanWinding: | |
| case DrawType::stencilMidpointFanCover: | |
| case DrawType::stencilOuterCubicBorrowedCoverage: | |
| case DrawType::stencilDynamicOuterCubics: | |
| case DrawType::stencilOuterCubics: | |
| case DrawType::stencilOuterCubicReset: | |
| case DrawType::stencilOuterCubicWinding: | |
| case DrawType::stencilOuterCubicCover: | |
| case DrawType::clipReset: | |
| if (!m_drawList.empty() && | |
| !enums::is_flag_set(m_pendingBarriers, | |
| gpu::BarrierFlags::drawBatchBreak)) | |
| { | |
| const DrawBatch* currentBatch = m_drawList.tail(); | |
| canMergeWithPreviousBatch = | |
| currentBatch->drawType == drawType && | |
| can_combine_shader_misc_flags(currentBatch, | |
| draw, | |
| shaderMiscFlags) && | |
| can_combine_draw_images(currentBatch->imageTexture, | |
| draw->imageTexture(), | |
| currentBatch->imageSampler, | |
| draw->imageSampler()); | |
| if (canMergeWithPreviousBatch && | |
| currentBatch->baseElement + currentBatch->elementCount != | |
| baseElement) | |
| { | |
| // In depthStencil mode, multiple subpasses reference the | |
| // same tessellation data. Although rare, this breaks the | |
| // guarantee we have in other modes that mergeable batches | |
| // will always have contiguous patches. | |
| assert(m_ctx->frameInterlockMode() == | |
| gpu::InterlockMode::depthStencil); | |
| canMergeWithPreviousBatch = false; | |
| } | |
| // Also break if there is a scissor rect mismatch | |
| if (m_ctx->platformFeatures().supportsClipScissor) | |
| { | |
| if (currentBatch->scissorRect != draw->scissorRect()) | |
| { | |
| canMergeWithPreviousBatch = false; | |
| } | |
| } | |
| break; | |
| } | |
| [[fallthrough]]; | |
| // Image draws can't be combined for now because they each have their | |
| // own unique uniforms. | |
| case DrawType::imageRect: | |
| case DrawType::imageMesh: | |
| case DrawType::renderPassInitialize: | |
| case DrawType::renderPassResolve: | |
| canMergeWithPreviousBatch = false; | |
| break; | |
| } | |
| DrawBatch* batch; | |
| if (!canMergeWithPreviousBatch) | |
| { | |
| batch = m_drawList.emplace_back(m_ctx->perFrameAllocator(), | |
| drawType, | |
| shaderMiscFlags, | |
| draw->drawContents(), | |
| elementCount, | |
| baseElement, | |
| draw->blendMode(), | |
| draw->imageSampler(), | |
| m_pendingBarriers); | |
| assignDrawIndices(drawType, batch); | |
| } | |
| else | |
| { | |
| batch = m_drawList.tail(); | |
| assert(batch->drawType == drawType); | |
| assert(batch->baseElement + batch->elementCount == baseElement); | |
| batch->elementCount += elementCount; | |
| // clockwise doesn't mix regular draws and clip updates. | |
| assert(m_ctx->frameInterlockMode() != gpu::InterlockMode::clockwise || | |
| (batch->drawContents & gpu::DrawContents::clipUpdate) == | |
| (draw->drawContents() & gpu::DrawContents::clipUpdate)); | |
| // Feathered fills should never combine with fills, strokes, or | |
| // feathered strokes because they use a different DrawType. | |
| assert((batch->drawContents & gpu::DrawContents::featheredFill) == | |
| (draw->drawContents() & gpu::DrawContents::featheredFill)); | |
| // depthStencil can't mix drawContents in a batch. | |
| assert(m_ctx->frameInterlockMode() != | |
| gpu::InterlockMode::depthStencil || | |
| batch->drawContents == draw->drawContents()); | |
| batch->shaderMiscFlags |= shaderMiscFlags; | |
| batch->drawContents |= draw->drawContents(); | |
| batch->barriers |= m_pendingBarriers; | |
| } | |
| m_pendingBarriers = gpu::BarrierFlags::none; | |
| // If the batch was merged into a previous one, this ensures it was a valid | |
| // merge. | |
| assert(batch->drawType == drawType); | |
| assert(can_combine_draw_images(batch->imageTexture, | |
| draw->imageTexture(), | |
| batch->imageSampler, | |
| draw->imageSampler())); | |
| assert(m_pendingBarriers == BarrierFlags::none); | |
| auto shaderFeatures = ShaderFeatures::NONE; | |
| if (draw->clipID() != 0) | |
| { | |
| shaderFeatures |= ShaderFeatures::ENABLE_CLIPPING; | |
| } | |
| if (draw->hasClipRect() && paintType != PaintType::clipUpdate) | |
| { | |
| shaderFeatures |= ShaderFeatures::ENABLE_CLIP_RECT; | |
| } | |
| if (frameDescriptor().ditherMode == | |
| gpu::DitherMode::interleavedGradientNoise) | |
| { | |
| shaderFeatures |= ShaderFeatures::ENABLE_DITHER; | |
| } | |
| if (draw->imageTexture() != nullptr && | |
| m_ctx->frameInterlockMode() != gpu::InterlockMode::atomics && | |
| drawType != DrawType::imageRect && drawType != DrawType::imageMesh) | |
| { | |
| shaderFeatures |= ShaderFeatures::ENABLE_MODULATED_IMAGE; | |
| } | |
| if (paintType != PaintType::clipUpdate && | |
| !enums::is_flag_set(shaderMiscFlags, | |
| gpu::ShaderMiscFlags::borrowedCoveragePass)) | |
| { | |
| assert(!enums::is_flag_set(shaderMiscFlags, | |
| gpu::ShaderMiscFlags::clipUpdateOnly)); | |
| switch (draw->blendMode()) | |
| { | |
| case BlendMode::hue: | |
| case BlendMode::saturation: | |
| case BlendMode::color: | |
| case BlendMode::luminosity: | |
| shaderFeatures |= ShaderFeatures::ENABLE_HSL_BLEND_MODES; | |
| [[fallthrough]]; | |
| case BlendMode::screen: | |
| case BlendMode::overlay: | |
| case BlendMode::darken: | |
| case BlendMode::lighten: | |
| case BlendMode::colorDodge: | |
| case BlendMode::colorBurn: | |
| case BlendMode::hardLight: | |
| case BlendMode::softLight: | |
| case BlendMode::difference: | |
| case BlendMode::exclusion: | |
| case BlendMode::multiply: | |
| shaderFeatures |= ShaderFeatures::ENABLE_ADVANCED_BLEND; | |
| break; | |
| case BlendMode::srcOver: | |
| break; | |
| } | |
| } | |
| batch->shaderFeatures |= shaderFeatures & m_ctx->m_frameShaderFeaturesMask; | |
| assert( | |
| (batch->shaderFeatures & | |
| gpu::ShaderFeaturesMaskFor(drawType, m_ctx->frameInterlockMode())) == | |
| batch->shaderFeatures); | |
| if (draw->imageTexture() != nullptr) | |
| { | |
| if (batch->imageTexture == nullptr) | |
| { | |
| // We merged in with a batch that did not already have an image so | |
| // we need to ensure the sampler is correct. | |
| batch->imageSampler = draw->imageSampler(); | |
| } | |
| if (batch->imageTexture == nullptr) | |
| { | |
| batch->imageTexture = draw->imageTexture(); | |
| } | |
| assert(batch->imageTexture == draw->imageTexture()); | |
| } | |
| assert(draw->imageTexture() == nullptr || | |
| batch->imageSampler == draw->imageSampler()); | |
| m_combinedShaderFeatures |= batch->shaderFeatures; | |
| return *batch; | |
| } | |
| } // namespace rive::gpu |