/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // Main header first: #include "FilterInstance.h" #include // Keep others in (case-insensitive) order: #include "CSSFilterInstance.h" #include "FilterSupport.h" #include "ImgDrawResult.h" #include "SVGIntegrationUtils.h" #include "gfx2DGlue.h" #include "gfxContext.h" #include "gfxPlatform.h" #include "gfxUtils.h" #include "mozilla/ISVGDisplayableFrame.h" #include "mozilla/SVGFilterInstance.h" #include "mozilla/SVGObserverUtils.h" #include "mozilla/SVGUtils.h" #include "mozilla/StaticPrefs_gfx.h" #include "mozilla/dom/Document.h" #include "mozilla/gfx/Filters.h" #include "mozilla/gfx/Helpers.h" #include "mozilla/gfx/Logging.h" #include "mozilla/gfx/PatternHelpers.h" #include "nsLayoutUtils.h" using namespace mozilla::dom; using namespace mozilla::gfx; using namespace mozilla::image; namespace mozilla { FilterDescription FilterInstance::GetFilterDescription( nsIContent* aFilteredElement, Span aFilterChain, ISVGFilterObserverList* aFiltersObserverList, bool aFilterInputIsTainted, const UserSpaceMetrics& aMetrics, const gfxRect& aBBox, nsTArray>& aOutAdditionalImages) { gfxMatrix identity; nsTArray filterFrames; if (SVGObserverUtils::GetAndObserveFilters(aFiltersObserverList, &filterFrames) == SVGObserverUtils::ReferenceState::HasRefsSomeInvalid) { return FilterDescription(); } FilterInstance instance(nullptr, aFilteredElement, aMetrics, aFilterChain, filterFrames, aFilterInputIsTainted, nullptr, identity, nullptr, nullptr, nullptr, &aBBox); if (!instance.IsInitialized()) { return FilterDescription(); } return instance.ExtractDescriptionAndAdditionalImages(aOutAdditionalImages); } static std::unique_ptr UserSpaceMetricsForFrame( nsIFrame* aFrame) { if (auto* element = SVGElement::FromNodeOrNull(aFrame->GetContent())) { return std::make_unique(element); } return std::make_unique(aFrame); } void FilterInstance::PaintFilteredFrame( nsIFrame* aFilteredFrame, Span aFilterChain, const nsTArray& aFilterFrames, gfxContext* aCtx, const SVGFilterPaintCallback& aPaintCallback, const nsRegion* aDirtyArea, imgDrawingParams& aImgParams, float aOpacity, const gfxRect* aOverrideBBox) { std::unique_ptr metrics = UserSpaceMetricsForFrame(aFilteredFrame); gfxContextMatrixAutoSaveRestore autoSR(aCtx); auto scaleFactors = aCtx->CurrentMatrixDouble().ScaleFactors(); if (scaleFactors.xScale == 0 || scaleFactors.yScale == 0) { return; } gfxMatrix scaleMatrix(scaleFactors.xScale, 0.0f, 0.0f, scaleFactors.yScale, 0.0f, 0.0f); gfxMatrix reverseScaleMatrix = scaleMatrix; DebugOnly invertible = reverseScaleMatrix.Invert(); MOZ_ASSERT(invertible); gfxMatrix scaleMatrixInDevUnits = scaleMatrix * SVGUtils::GetCSSPxToDevPxMatrix(aFilteredFrame); // Hardcode InputIsTainted to true because we don't want JS to be able to // read the rendered contents of aFilteredFrame. FilterInstance instance(aFilteredFrame, aFilteredFrame->GetContent(), *metrics, aFilterChain, aFilterFrames, /* InputIsTainted */ true, aPaintCallback, scaleMatrixInDevUnits, aDirtyArea, nullptr, nullptr, aOverrideBBox); if (instance.IsInitialized()) { // Pull scale vector out of aCtx's transform, put all scale factors, which // includes css and css-to-dev-px scale, into scaleMatrixInDevUnits. aCtx->SetMatrixDouble(reverseScaleMatrix * aCtx->CurrentMatrixDouble()); instance.Render(aCtx, aImgParams, aOpacity); } else { // Render the unfiltered contents. aPaintCallback(*aCtx, aImgParams, nullptr, nullptr); } } static mozilla::wr::ComponentTransferFuncType FuncTypeToWr( SVGFEComponentTransferType aFuncType) { MOZ_ASSERT(aFuncType != SVGFEComponentTransferType::SameAsR); switch (aFuncType) { case SVGFEComponentTransferType::Table: return mozilla::wr::ComponentTransferFuncType::Table; case SVGFEComponentTransferType::Discrete: return mozilla::wr::ComponentTransferFuncType::Discrete; case SVGFEComponentTransferType::Linear: return mozilla::wr::ComponentTransferFuncType::Linear; case SVGFEComponentTransferType::Gamma: return mozilla::wr::ComponentTransferFuncType::Gamma; case SVGFEComponentTransferType::Identity: default: return mozilla::wr::ComponentTransferFuncType::Identity; } MOZ_ASSERT_UNREACHABLE("all func types not handled?"); return mozilla::wr::ComponentTransferFuncType::Identity; } WrFiltersStatus FilterInstance::BuildWebRenderFilters( nsIFrame* aFilteredFrame, Span aFilters, StyleFilterType aStyleFilterType, WrFiltersHolder& aWrFilters, const nsPoint& aOffsetForSVGFilters) { WrFiltersStatus status = WrFiltersStatus::BLOB_FALLBACK; if (StaticPrefs::gfx_webrender_svg_filter_effects()) { status = BuildWebRenderSVGFiltersImpl(aFilteredFrame, aFilters, aStyleFilterType, aWrFilters, aOffsetForSVGFilters); } if (status == WrFiltersStatus::BLOB_FALLBACK) { status = BuildWebRenderFiltersImpl(aFilteredFrame, aFilters, aStyleFilterType, aWrFilters); } if (status == WrFiltersStatus::BLOB_FALLBACK) { aFilteredFrame->PresContext()->Document()->SetUseCounter( eUseCounter_custom_WrFilterFallback); } return status; } WrFiltersStatus FilterInstance::BuildWebRenderFiltersImpl( nsIFrame* aFilteredFrame, Span aFilters, StyleFilterType aStyleFilterType, WrFiltersHolder& aWrFilters) { aWrFilters.filters.Clear(); aWrFilters.filter_datas.Clear(); aWrFilters.values.Clear(); aWrFilters.post_filters_clip = Nothing(); nsIFrame* firstFrame = nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFilteredFrame); nsTArray filterFrames; if (SVGObserverUtils::GetAndObserveFilters(firstFrame, &filterFrames, aStyleFilterType) == SVGObserverUtils::ReferenceState::HasRefsSomeInvalid) { return WrFiltersStatus::UNSUPPORTED; } std::unique_ptr metrics = UserSpaceMetricsForFrame(firstFrame); // TODO: simply using an identity matrix here, was pulling the scale from a // gfx context for the non-wr path. gfxMatrix scaleMatrix; gfxMatrix scaleMatrixInDevUnits = scaleMatrix * SVGUtils::GetCSSPxToDevPxMatrix(firstFrame); // Hardcode inputIsTainted to true because we don't want JS to be able to // read the rendered contents of aFilteredFrame. FilterInstance instance(firstFrame, firstFrame->GetContent(), *metrics, aFilters, filterFrames, /* inputIsTainted */ true, nullptr, scaleMatrixInDevUnits, nullptr, nullptr, nullptr, nullptr); if (!instance.IsInitialized()) { return WrFiltersStatus::UNSUPPORTED; } // If there are too many filters to render, then just pretend that we // succeeded, and don't render any of them. if (instance.mFilterDescription.mPrimitives.Length() > StaticPrefs::gfx_webrender_max_filter_ops_per_chain()) { return WrFiltersStatus::DISABLED_FOR_PERFORMANCE; } Maybe finalClip; bool srgb = true; // We currently apply the clip on the stacking context after applying filters, // but primitive subregions imply clipping after each filter and not just the // end of the chain. For some types of filter it doesn't matter, but for those // which sample outside of the location of the destination pixel like blurs, // only clipping after could produce incorrect results, so we bail out in this // case. // We can lift this restriction once we have added support for primitive // subregions to WebRender's filters. for (uint32_t i = 0; i < instance.mFilterDescription.mPrimitives.Length(); i++) { const auto& primitive = instance.mFilterDescription.mPrimitives[i]; // WebRender only supports filters with one input. if (primitive.NumberOfInputs() != 1) { return WrFiltersStatus::BLOB_FALLBACK; } // The first primitive must have the source graphic as the input, all // other primitives must have the prior primitive as the input, otherwise // it's not supported by WebRender. if (i == 0) { if (primitive.InputPrimitiveIndex(0) != FilterPrimitiveDescription::kPrimitiveIndexSourceGraphic) { return WrFiltersStatus::BLOB_FALLBACK; } } else if (primitive.InputPrimitiveIndex(0) != int32_t(i - 1)) { return WrFiltersStatus::BLOB_FALLBACK; } bool previousSrgb = srgb; bool primNeedsSrgb = primitive.InputColorSpace(0) == gfx::ColorSpace::SRGB; if (srgb && !primNeedsSrgb) { aWrFilters.filters.AppendElement(wr::FilterOp::SrgbToLinear()); } else if (!srgb && primNeedsSrgb) { aWrFilters.filters.AppendElement(wr::FilterOp::LinearToSrgb()); } srgb = primitive.OutputColorSpace() == gfx::ColorSpace::SRGB; const PrimitiveAttributes& attr = primitive.Attributes(); bool filterIsNoop = false; if (attr.is()) { float opacity = attr.as().mOpacity; aWrFilters.filters.AppendElement(wr::FilterOp::Opacity( wr::PropertyBinding::Value(opacity), opacity)); } else if (attr.is()) { const ColorMatrixAttributes& attributes = attr.as(); float transposed[20]; if (gfx::ComputeColorMatrix(attributes, transposed)) { float matrix[20] = { transposed[0], transposed[5], transposed[10], transposed[15], transposed[1], transposed[6], transposed[11], transposed[16], transposed[2], transposed[7], transposed[12], transposed[17], transposed[3], transposed[8], transposed[13], transposed[18], transposed[4], transposed[9], transposed[14], transposed[19]}; aWrFilters.filters.AppendElement(wr::FilterOp::ColorMatrix(matrix)); } else { filterIsNoop = true; } } else if (attr.is()) { if (finalClip) { // There's a clip that needs to apply before the blur filter, but // WebRender only lets us apply the clip at the end of the filter // chain. Clipping after a blur is not equivalent to clipping before // a blur, so bail out. return WrFiltersStatus::BLOB_FALLBACK; } const GaussianBlurAttributes& blur = attr.as(); const Size& stdDev = blur.mStdDeviation; if (stdDev.width != 0.0 || stdDev.height != 0.0) { aWrFilters.filters.AppendElement( wr::FilterOp::Blur(stdDev.width, stdDev.height)); } else { filterIsNoop = true; } } else if (attr.is()) { if (finalClip) { // We have to bail out for the same reason we would with a blur filter. return WrFiltersStatus::BLOB_FALLBACK; } const DropShadowAttributes& shadow = attr.as(); const Size& stdDev = shadow.mStdDeviation; if (stdDev.width != stdDev.height) { return WrFiltersStatus::BLOB_FALLBACK; } sRGBColor color = shadow.mColor; if (!primNeedsSrgb) { color = FilterWrappers::SRGBToLinearRGB(color); } wr::Shadow wrShadow; wrShadow.offset = {shadow.mOffset.x, shadow.mOffset.y}; wrShadow.color = wr::ToColorF(ToDeviceColor(color)); wrShadow.blur_radius = stdDev.width; wr::FilterOp filterOp = wr::FilterOp::DropShadow(wrShadow); aWrFilters.filters.AppendElement(filterOp); } else if (attr.is()) { const ComponentTransferAttributes& attributes = attr.as(); size_t numValues = attributes.mValues[0].Length() + attributes.mValues[1].Length() + attributes.mValues[2].Length() + attributes.mValues[3].Length(); if (numValues > 1024) { // Depending on how the wr shaders are implemented we may need to // limit the total number of values. return WrFiltersStatus::BLOB_FALLBACK; } wr::FilterOp filterOp = {wr::FilterOp::Tag::ComponentTransfer}; wr::WrFilterData filterData; aWrFilters.values.AppendElement(nsTArray()); nsTArray* values = &aWrFilters.values.LastElement(); values->SetCapacity(numValues); filterData.funcR_type = FuncTypeToWr(attributes.mTypes[0]); size_t R_startindex = values->Length(); values->AppendElements(attributes.mValues[0]); filterData.R_values_count = attributes.mValues[0].Length(); size_t indexToUse = attributes.mTypes[1] == SVGFEComponentTransferType::SameAsR ? 0 : 1; filterData.funcG_type = FuncTypeToWr(attributes.mTypes[indexToUse]); size_t G_startindex = values->Length(); values->AppendElements(attributes.mValues[indexToUse]); filterData.G_values_count = attributes.mValues[indexToUse].Length(); indexToUse = attributes.mTypes[2] == SVGFEComponentTransferType::SameAsR ? 0 : 2; filterData.funcB_type = FuncTypeToWr(attributes.mTypes[indexToUse]); size_t B_startindex = values->Length(); values->AppendElements(attributes.mValues[indexToUse]); filterData.B_values_count = attributes.mValues[indexToUse].Length(); filterData.funcA_type = FuncTypeToWr(attributes.mTypes[3]); size_t A_startindex = values->Length(); values->AppendElements(attributes.mValues[3]); filterData.A_values_count = attributes.mValues[3].Length(); filterData.R_values = filterData.R_values_count > 0 ? &((*values)[R_startindex]) : nullptr; filterData.G_values = filterData.G_values_count > 0 ? &((*values)[G_startindex]) : nullptr; filterData.B_values = filterData.B_values_count > 0 ? &((*values)[B_startindex]) : nullptr; filterData.A_values = filterData.A_values_count > 0 ? &((*values)[A_startindex]) : nullptr; aWrFilters.filters.AppendElement(filterOp); aWrFilters.filter_datas.AppendElement(filterData); } else { return WrFiltersStatus::BLOB_FALLBACK; } if (filterIsNoop && !aWrFilters.filters.IsEmpty() && (aWrFilters.filters.LastElement().tag == wr::FilterOp::Tag::SrgbToLinear || aWrFilters.filters.LastElement().tag == wr::FilterOp::Tag::LinearToSrgb)) { // We pushed a color space conversion filter in prevision of applying // another filter which turned out to be a no-op, so the conversion is // unnecessary. Remove it from the filter list. // This is both an optimization and a way to pass the wptest // css/filter-effects/filter-scale-001.html for which the needless // sRGB->linear->no-op->sRGB roundtrip introduces a slight error and we // cannot add fuzziness to the test. (void)aWrFilters.filters.PopLastElement(); srgb = previousSrgb; } if (!filterIsNoop) { if (finalClip.isNothing()) { finalClip = Some(primitive.PrimitiveSubregion()); } else { finalClip = Some(primitive.PrimitiveSubregion().Intersect(finalClip.value())); } } } if (!srgb) { aWrFilters.filters.AppendElement(wr::FilterOp::LinearToSrgb()); } if (finalClip) { aWrFilters.post_filters_clip = Some(instance.FilterSpaceToFrameSpace(finalClip.value())); } return WrFiltersStatus::CHAIN; } static WrFiltersStatus WrSVGFEInputBuild(wr::FilterOpGraphPictureReference& pic, int32_t aSource, int16_t aNodeOutput, int16_t aSourceGraphic, int16_t aSourceAlpha, const int16_t aBufferIdMapping[]) { switch (aSource) { case FilterPrimitiveDescription::kPrimitiveIndexSourceGraphic: pic.buffer_id = wr::FilterOpGraphPictureBufferId::BufferId(aSourceGraphic); break; case FilterPrimitiveDescription::kPrimitiveIndexSourceAlpha: pic.buffer_id = wr::FilterOpGraphPictureBufferId::BufferId(aSourceAlpha); break; case FilterPrimitiveDescription::kPrimitiveIndexFillPaint: case FilterPrimitiveDescription::kPrimitiveIndexStrokePaint: // https://bugzilla.mozilla.org/show_bug.cgi?id=1897878 // Fill and Stroke paints are not yet supported by WebRender, they may be // a color (most common) or pattern fill, so we could implement them with // feFlood or feImage + feTile depending on the nature of the fill. return WrFiltersStatus::BLOB_FALLBACK; default: MOZ_RELEASE_ASSERT( aSource >= 0, "Unrecognized SVG filter primitive enum value - added another?"); MOZ_RELEASE_ASSERT(aSource < aNodeOutput, "Invalid DAG - nodes can only refer to earlier nodes"); if (aSource < 0 || aSource >= aNodeOutput) { return WrFiltersStatus::UNSUPPORTED; } // Look up the node we remapped this id to. // This can't overflow because aSource < aNodeOutput and the table is the // same size. pic.buffer_id = wr::FilterOpGraphPictureBufferId::BufferId(aBufferIdMapping[aSource]); break; } return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFEOpacity( WrFiltersHolder& aWrFilters, const wr::FilterOpGraphNode& aGraphNode, const OpacityAttributes& aAttributes) { // CSS opacity // This is the only CSS property that is has no direct analog in SVG, although // technically it can be represented as SVGFEComponentTransfer or // SVGFEColorMatrix or SVGFECompositeArithmetic, those would be inefficient // approaches. if (!StaticPrefs::gfx_webrender_svg_filter_effects_opacity()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } float opacity = aAttributes.mOpacity; if (opacity != 1.0f) { aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEOpacity( aGraphNode, wr::PropertyBinding::Value(opacity), opacity)); } else { // If it's a no-op, we still have to generate a graph node aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEIdentity(aGraphNode)); } return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFEToAlpha( WrFiltersHolder& aWrFilters, const wr::FilterOpGraphNode& aGraphNode, const ToAlphaAttributes& aAttributes) { // Convert a color image to an alpha channel - internal use; generated by // SVGFilterInstance::GetOrCreateSourceAlphaIndex(). if (!StaticPrefs::GetPrefName_gfx_webrender_svg_filter_effects_toalpha()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEToAlpha(aGraphNode)); return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFEBlend( WrFiltersHolder& aWrFilters, const wr::FilterOpGraphNode& aGraphNode, const BlendAttributes& aAttributes) { // SVGFEBlend - common if (!StaticPrefs::gfx_webrender_svg_filter_effects_feblend()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } switch (aAttributes.mBlendMode) { case SVGFEBlendMode::Color: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendColor(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::ColorBurn: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendColorBurn(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::ColorDodge: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendColorDodge(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::Darken: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendDarken(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::Difference: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendDifference(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::Exclusion: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendExclusion(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::HardLight: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendHardLight(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::Hue: aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEBlendHue(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::Lighten: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendLighten(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::Luminosity: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendLuminosity(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::Multiply: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendMultiply(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::Normal: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendNormal(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::Overlay: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendOverlay(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::Saturation: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendSaturation(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::Screen: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendScreen(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFEBlendMode::SoftLight: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEBlendSoftLight(aGraphNode)); return WrFiltersStatus::SVGFE; default: break; } MOZ_CRASH("Unrecognized SVG_FEBLEND_MODE"); return WrFiltersStatus::BLOB_FALLBACK; } static WrFiltersStatus WrFilterOpSVGFEComposite( WrFiltersHolder& aWrFilters, const wr::FilterOpGraphNode& aGraphNode, const CompositeAttributes& aAttributes) { // SVGFEComposite - common if (!StaticPrefs::gfx_webrender_svg_filter_effects_fecomposite()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } switch (aAttributes.mOperator) { case SVGFECompositeOperator::Arithmetic: aWrFilters.filters.AppendElement(wr::FilterOp::SVGFECompositeArithmetic( aGraphNode, aAttributes.mCoefficients[0], aAttributes.mCoefficients[1], aAttributes.mCoefficients[2], aAttributes.mCoefficients[3])); return WrFiltersStatus::SVGFE; case SVGFECompositeOperator::Atop: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFECompositeATop(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFECompositeOperator::In: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFECompositeIn(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFECompositeOperator::Lighter: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFECompositeLighter(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFECompositeOperator::Out: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFECompositeOut(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFECompositeOperator::Over: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFECompositeOver(aGraphNode)); return WrFiltersStatus::SVGFE; case SVGFECompositeOperator::Xor: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFECompositeXOR(aGraphNode)); return WrFiltersStatus::SVGFE; default: break; } MOZ_CRASH("Unrecognized SVG_FECOMPOSITE_OPERATOR"); return WrFiltersStatus::BLOB_FALLBACK; } static WrFiltersStatus WrFilterOpSVGFEColorMatrix( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const ColorMatrixAttributes& aAttributes) { // SVGFEColorMatrix - common if (!StaticPrefs::gfx_webrender_svg_filter_effects_fecolormatrix()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } float transposed[20]; if (gfx::ComputeColorMatrix(aAttributes, transposed)) { float matrix[20] = { transposed[0], transposed[5], transposed[10], transposed[15], transposed[1], transposed[6], transposed[11], transposed[16], transposed[2], transposed[7], transposed[12], transposed[17], transposed[3], transposed[8], transposed[13], transposed[18], transposed[4], transposed[9], transposed[14], transposed[19]}; aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEColorMatrix(aGraphNode, matrix)); } else { // If it's a no-op, we still have to generate a graph node aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEIdentity(aGraphNode)); } return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFEComponentTransfer( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const ComponentTransferAttributes& aAttributes) { // SVGFEComponentTransfer - common if (!StaticPrefs::gfx_webrender_svg_filter_effects_fecomponenttransfer()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } // We ensure that there are at least 256 values for each channel so that // the shader can skip interpolation math for simplicity. static constexpr size_t kStops = 256; aWrFilters.values.AppendElement(nsTArray()); nsTArray& values = aWrFilters.values.LastElement(); values.SetCapacity(kStops * 4); // Set the FilterData funcs for whether or not to interpolate the values // between stops, although we use enough stops that it may not matter. // The only type that doesn't use interpolation is discrete. wr::WrFilterData filterData{}; filterData.funcR_type = aAttributes.mTypes[0] != SVGFEComponentTransferType::Discrete ? mozilla::wr::ComponentTransferFuncType::Table : mozilla::wr::ComponentTransferFuncType::Discrete; filterData.funcG_type = aAttributes.mTypes[1] != SVGFEComponentTransferType::Discrete ? mozilla::wr::ComponentTransferFuncType::Table : mozilla::wr::ComponentTransferFuncType::Discrete; filterData.funcB_type = aAttributes.mTypes[2] != SVGFEComponentTransferType::Discrete ? mozilla::wr::ComponentTransferFuncType::Table : mozilla::wr::ComponentTransferFuncType::Discrete; filterData.funcA_type = aAttributes.mTypes[3] != SVGFEComponentTransferType::Discrete ? mozilla::wr::ComponentTransferFuncType::Table : mozilla::wr::ComponentTransferFuncType::Discrete; // This is a bit of a strange way to store the table, it is an interleaved // array of RGBA values that we want to store in a single gpucache array // of raw pixels, so it's easiest to send it to WebRender as a single // channel, but FilterData requires it to be 4 channels, so we send it as // 4 groups of values but the data is interleaved. values.SetLength(kStops * 4); filterData.R_values = &(values[0]); filterData.R_values_count = kStops; filterData.G_values = &(values[kStops]); filterData.G_values_count = kStops; filterData.B_values = &(values[kStops * 2]); filterData.B_values_count = kStops; filterData.A_values = &(values[kStops * 3]); filterData.A_values_count = kStops; // This builds a single interleaved RGBA table as it is well suited to GPU // texture fetches without any dynamic component indexing in the shader which // can confuse buggy shader compilers. for (size_t c = 0; c < 4; c++) { auto f = aAttributes.mTypes[c]; // Check if there's no data (we have crashtests for this). if (aAttributes.mValues[c].IsEmpty() && f != SVGFEComponentTransferType::SameAsR) { f = SVGFEComponentTransferType::Identity; } // Check for misuse of SVGFEComponentTransferType::SameAsR. if (c == 0 && f == SVGFEComponentTransferType::SameAsR) { f = SVGFEComponentTransferType::Identity; } switch (f) { case SVGFEComponentTransferType::Discrete: { size_t length = (size_t)aAttributes.mValues[c].Length(); size_t length1 = length - 1; float step = (float)length / (float)kStops; for (size_t i = 0; i < kStops; i++) { // find the corresponding color in the table // this can not overflow due to the length check float kf = (float)i * step; float floorkf = std::floor(kf); size_t k = std::min((size_t)floorkf, length1); float v = aAttributes.mValues[c][k]; values[i * 4 + c] = std::clamp(v, 0.0f, 1.0f); } break; } case SVGFEComponentTransferType::Gamma: { float step = 1.0f / (float)(kStops - 1); float amplitude = aAttributes.mValues[c][0]; float exponent = aAttributes.mValues[c][1]; float offset = aAttributes.mValues[c][2]; for (size_t i = 0; i < kStops; i++) { float v = amplitude * pow((float)i * step, exponent) + offset; values[i * 4 + c] = std::clamp(v, 0.0f, 1.0f); } break; } case SVGFEComponentTransferType::Identity: { float step = 1.0f / (float)(kStops - 1); for (size_t i = 0; i < kStops; i++) { float v = (float)i * step; values[i * 4 + c] = std::clamp(v, 0.0f, 1.0f); } break; } case SVGFEComponentTransferType::Linear: { float step = aAttributes.mValues[c][0] / (float)(kStops - 1); float intercept = aAttributes.mValues[c][1]; for (size_t i = 0; i < kStops; i++) { float v = (float)i * step + intercept; values[i * 4 + c] = std::clamp(v, 0.0f, 1.0f); } break; } case SVGFEComponentTransferType::Table: { size_t length1 = (size_t)aAttributes.mValues[c].Length() - 1; float step = (float)length1 / (float)(kStops - 1); for (size_t i = 0; i < kStops; i++) { // Find the corresponding color in the table and interpolate float kf = (float)i * step; float floorkf = floor(kf); size_t k = std::min((size_t)floorkf, length1); float v1 = aAttributes.mValues[c][k]; float v2 = aAttributes.mValues[c][(k + 1 <= length1) ? k + 1 : k]; float v = v1 + (v2 - v1) * (kf - floorkf); values[i * 4 + c] = std::clamp(v, 0.0f, 1.0f); } break; } case SVGFEComponentTransferType::SameAsR: { // We already checked c > 0 above. for (size_t i = 0; i < kStops; i++) { values[i * 4 + c] = values[i * 4]; } break; } default: { MOZ_CRASH("Unrecognized feComponentTransfer type"); return WrFiltersStatus::BLOB_FALLBACK; } } } aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEComponentTransfer(aGraphNode)); aWrFilters.filter_datas.AppendElement(filterData); return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFEConvolveMatrix( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const ConvolveMatrixAttributes& aAttributes) { // SVGFEConvolveMatrix - extremely rare if (!StaticPrefs::gfx_webrender_svg_filter_effects_feconvolvematrix()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } // Limited kernel size for performance reasons - spec allows us to drop // the whole filter graph if anything is unreasonable, so we only support // up to 5x5 kernel as that is pretty fast in hardware static constexpr int32_t width = 5; static constexpr int32_t height = 5; if (aAttributes.mKernelSize.Width() < 1 || aAttributes.mKernelSize.Width() > width || aAttributes.mKernelSize.Height() < 1 || aAttributes.mKernelSize.Height() > height || (size_t)aAttributes.mKernelSize.Width() * (size_t)aAttributes.mKernelSize.Height() > width * height) { return WrFiltersStatus::BLOB_FALLBACK; } // Reject kernel matrix if it is fewer values than dimensions suggest if (aAttributes.mKernelMatrix.Length() < (size_t)aAttributes.mKernelSize.Width() * (size_t)aAttributes.mKernelSize.Height()) { return WrFiltersStatus::UNSUPPORTED; } // Arrange the values in the order the shader expects float matrix[width * height]; for (size_t y = 0; y < height; y++) { for (size_t x = 0; x < width; x++) { if (x < (size_t)aAttributes.mKernelSize.Width() && y < (size_t)aAttributes.mKernelSize.Height()) { matrix[y * width + x] = aAttributes.mKernelMatrix[y * aAttributes.mKernelSize.Width() + x]; } else { matrix[y * width + x] = 0.0f; } } } switch (aAttributes.mEdgeMode) { case SVGEdgeMode::Unknown: case SVGEdgeMode::Duplicate: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEConvolveMatrixEdgeModeDuplicate( aGraphNode, aAttributes.mKernelSize.Width(), aAttributes.mKernelSize.Height(), matrix, aAttributes.mDivisor, aAttributes.mBias, aAttributes.mTarget.x.value, aAttributes.mTarget.y.value, aAttributes.mKernelUnitLength.Width(), aAttributes.mKernelUnitLength.Height(), aAttributes.mPreserveAlpha)); return WrFiltersStatus::SVGFE; case SVGEdgeMode::None: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEConvolveMatrixEdgeModeNone( aGraphNode, aAttributes.mKernelSize.Width(), aAttributes.mKernelSize.Height(), matrix, aAttributes.mDivisor, aAttributes.mBias, aAttributes.mTarget.x.value, aAttributes.mTarget.y.value, aAttributes.mKernelUnitLength.Width(), aAttributes.mKernelUnitLength.Height(), aAttributes.mPreserveAlpha)); return WrFiltersStatus::SVGFE; case SVGEdgeMode::Wrap: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEConvolveMatrixEdgeModeWrap( aGraphNode, aAttributes.mKernelSize.Width(), aAttributes.mKernelSize.Height(), matrix, aAttributes.mDivisor, aAttributes.mBias, aAttributes.mTarget.x.value, aAttributes.mTarget.y.value, aAttributes.mKernelUnitLength.Width(), aAttributes.mKernelUnitLength.Height(), aAttributes.mPreserveAlpha)); return WrFiltersStatus::SVGFE; default: break; } MOZ_CRASH("Unrecognized SVG_EDGEMODE"); return WrFiltersStatus::BLOB_FALLBACK; } static WrFiltersStatus WrFilterOpSVGFEDiffuseLighting( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const DiffuseLightingAttributes& aAttributes, const LayoutDevicePoint& aUserspaceOffset) { // SVGFEDiffuseLighting - extremely rare if (!StaticPrefs::gfx_webrender_svg_filter_effects_fediffuselighting()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } switch (aAttributes.mLightType) { case LightType::Distant: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEDiffuseLightingDistant( aGraphNode, aAttributes.mSurfaceScale, aAttributes.mLightingConstant, aAttributes.mKernelUnitLength.width, aAttributes.mKernelUnitLength.height, aAttributes.mLightValues[0], aAttributes.mLightValues[1])); return WrFiltersStatus::SVGFE; case LightType::Point: aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEDiffuseLightingPoint( aGraphNode, aAttributes.mSurfaceScale, aAttributes.mLightingConstant, aAttributes.mKernelUnitLength.width, aAttributes.mKernelUnitLength.height, aAttributes.mLightValues[0] + aUserspaceOffset.x.value, aAttributes.mLightValues[1] + aUserspaceOffset.y.value, aAttributes.mLightValues[2])); return WrFiltersStatus::SVGFE; case LightType::Spot: aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEDiffuseLightingSpot( aGraphNode, aAttributes.mSurfaceScale, aAttributes.mLightingConstant, aAttributes.mKernelUnitLength.width, aAttributes.mKernelUnitLength.height, aAttributes.mLightValues[0] + aUserspaceOffset.x.value, aAttributes.mLightValues[1] + aUserspaceOffset.y.value, aAttributes.mLightValues[2], aAttributes.mLightValues[3] + aUserspaceOffset.x.value, aAttributes.mLightValues[4] + aUserspaceOffset.y.value, aAttributes.mLightValues[5], aAttributes.mLightValues[6], aAttributes.mLightValues[7])); return WrFiltersStatus::SVGFE; case LightType::None: case LightType::Max: // No default case, so that the compiler will warn if new enums are added break; } MOZ_CRASH("Unrecognized LightType"); return WrFiltersStatus::BLOB_FALLBACK; } static WrFiltersStatus WrFilterOpSVGFEDisplacementMap( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const DisplacementMapAttributes& aAttributes) { // SVGFEDisplacementMap - extremely rare if (!StaticPrefs::gfx_webrender_svg_filter_effects_fedisplacementmap()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEDisplacementMap( aGraphNode, aAttributes.mScale, static_cast(aAttributes.mXChannel), static_cast(aAttributes.mYChannel))); return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFEDropShadow( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const DropShadowAttributes& aAttributes) { // SVGFEDropShadow - extremely rare if (!StaticPrefs::gfx_webrender_svg_filter_effects_fedropshadow()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } // This color is used in a shader coefficient that is in sRGB color space, // so it needs to go through the regular device color transformation. // This does not premultiply the alpha - the shader will do that. aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEDropShadow( aGraphNode, wr::ToColorF(ToDeviceColor(aAttributes.mColor)), aAttributes.mOffset.x, aAttributes.mOffset.y, aAttributes.mStdDeviation.width, aAttributes.mStdDeviation.height)); return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFEFlood( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const FloodAttributes& aAttributes) { // SVGFEFlood - common if (!StaticPrefs::gfx_webrender_svg_filter_effects_feflood()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } // This color is used in a shader coefficient that is in sRGB color space, // so it needs to go through the regular device color transformation. // This does not premultiply the alpha - the shader will do that. aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEFlood( aGraphNode, wr::ToColorF(ToDeviceColor(aAttributes.mColor)))); return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFEGaussianBlur( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const GaussianBlurAttributes& aAttributes) { // SVGFEGaussianBlur - common if (!StaticPrefs::gfx_webrender_svg_filter_effects_fegaussianblur()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEGaussianBlur( aGraphNode, aAttributes.mStdDeviation.width, aAttributes.mStdDeviation.height)); return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFEImage( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const ImageAttributes& aAttributes, const LayoutDevicePoint& aUserspaceOffset) { // SVGFEImage - Extremely rare if (!StaticPrefs::gfx_webrender_svg_filter_effects_feimage()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } float matrix[6]; matrix[0] = aAttributes.mTransform.components[0]; matrix[1] = aAttributes.mTransform.components[1]; matrix[2] = aAttributes.mTransform.components[2]; matrix[3] = aAttributes.mTransform.components[3]; matrix[4] = aAttributes.mTransform.components[4] + aUserspaceOffset.x.value; matrix[5] = aAttributes.mTransform.components[5] + aUserspaceOffset.y.value; // TODO: We need to resolve aAttributes.mInputIndex to an actual image // somehow. aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEImage(aGraphNode, aAttributes.mFilter, matrix)); return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFEMerge( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const MergeAttributes& aAttributes, FilterPrimitiveDescription& aPrimitive, int16_t aNodeOutput, int16_t aSourceGraphic, int16_t aSourceAlpha, const int16_t aBufferIdMapping[], size_t aMaxFilters) { // SVGFEMerge - common if (!StaticPrefs::gfx_webrender_svg_filter_effects_femerge()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } // There is no SVGFEMerge, so we need to expand the provided inputs to a // chain of SVGFECompositeOver ops before handing it to WebRender. if (aPrimitive.NumberOfInputs() >= 2) { wr::FilterOpGraphPictureReference previous{}; for (size_t index = 0; index < aPrimitive.NumberOfInputs(); index++) { wr::FilterOpGraphPictureReference current{}; WrFiltersStatus status = WrSVGFEInputBuild( current, aPrimitive.InputPrimitiveIndex(index), aNodeOutput, aSourceGraphic, aSourceAlpha, aBufferIdMapping); if (status != WrFiltersStatus::SVGFE) { // If the input is an invalid ref, we have to disable filters on this. return status; } aGraphNode.input = current; aGraphNode.input2 = previous; if (aWrFilters.filters.Length() >= aMaxFilters) { // Reject the graph if it has too many filters to even process return WrFiltersStatus::DISABLED_FOR_PERFORMANCE; } if (index >= 1) { // Emit a node that composites this pic over the previous pics. aWrFilters.filters.AppendElement( wr::FilterOp::SVGFECompositeOver(aGraphNode)); // Use this graph node as input2 (background) on the next node. previous.buffer_id = wr::FilterOpGraphPictureBufferId::BufferId( (int16_t)(aWrFilters.filters.Length() - 1)); } else { // Conceptually the first pic is composited over transparent black // which is a no-op, so we just use the first pic as a direct input // on the first node we actually emit. previous.buffer_id = current.buffer_id; } } } else if (aPrimitive.NumberOfInputs() == 1) { // If we only got a single feMergeNode pic, we still want to apply // the subregion clip, so make an SVGFEIdentity op. aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEIdentity(aGraphNode)); } else { // feMerge with no feMergeNodes is just blank. wr::ColorF blank = {0.0f, 0.0f, 0.0f, 0.0f}; aWrFilters.filters.AppendElement( wr::FilterOp::SVGFEFlood(aGraphNode, blank)); } return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFEMorphology( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const MorphologyAttributes& aAttributes) { // SVGFEMorphology - Rare if (!StaticPrefs::gfx_webrender_svg_filter_effects_femorphology()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } switch (aAttributes.mOperator) { case SVGMorphologyOperator::Dilate: aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEMorphologyDilate( aGraphNode, aAttributes.mRadii.width, aAttributes.mRadii.height)); return WrFiltersStatus::SVGFE; case SVGMorphologyOperator::Erode: aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEMorphologyErode( aGraphNode, aAttributes.mRadii.width, aAttributes.mRadii.height)); return WrFiltersStatus::SVGFE; default: break; } MOZ_CRASH("Unrecognized SVG_OPERATOR"); return WrFiltersStatus::BLOB_FALLBACK; } static WrFiltersStatus WrFilterOpSVGFEOffset( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const OffsetAttributes& aAttributes) { // SVGFEOffset - Common if (!StaticPrefs::gfx_webrender_svg_filter_effects_feoffset()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } aWrFilters.filters.AppendElement(wr::FilterOp::SVGFEOffset( aGraphNode, (float)aAttributes.mValue.x, (float)aAttributes.mValue.y)); return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFETile(WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const TileAttributes& aAttributes) { // SVGFETile - Extremely rare if (!StaticPrefs::gfx_webrender_svg_filter_effects_fetile()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } aWrFilters.filters.AppendElement(wr::FilterOp::SVGFETile(aGraphNode)); return WrFiltersStatus::SVGFE; } static WrFiltersStatus WrFilterOpSVGFESpecularLighting( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const SpecularLightingAttributes& aAttributes, const LayoutDevicePoint& aUserspaceOffset) { // SVGFESpecularLighting - extremely rare if (!StaticPrefs::gfx_webrender_svg_filter_effects_fespecularlighting()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } switch (aAttributes.mLightType) { case LightType::Distant: aWrFilters.filters.AppendElement( wr::FilterOp::SVGFESpecularLightingDistant( aGraphNode, aAttributes.mSurfaceScale, aAttributes.mLightingConstant, aAttributes.mSpecularExponent, aAttributes.mKernelUnitLength.width, aAttributes.mKernelUnitLength.height, aAttributes.mLightValues[0], aAttributes.mLightValues[1])); return WrFiltersStatus::SVGFE; case LightType::Point: aWrFilters.filters.AppendElement(wr::FilterOp::SVGFESpecularLightingPoint( aGraphNode, aAttributes.mSurfaceScale, aAttributes.mLightingConstant, aAttributes.mSpecularExponent, aAttributes.mKernelUnitLength.width, aAttributes.mKernelUnitLength.height, aAttributes.mLightValues[0] + aUserspaceOffset.x.value, aAttributes.mLightValues[1] + aUserspaceOffset.y.value, aAttributes.mLightValues[2])); return WrFiltersStatus::SVGFE; case LightType::Spot: aWrFilters.filters.AppendElement(wr::FilterOp::SVGFESpecularLightingSpot( aGraphNode, aAttributes.mSurfaceScale, aAttributes.mLightingConstant, aAttributes.mSpecularExponent, aAttributes.mKernelUnitLength.width, aAttributes.mKernelUnitLength.height, aAttributes.mLightValues[0] + aUserspaceOffset.x.value, aAttributes.mLightValues[1] + aUserspaceOffset.y.value, aAttributes.mLightValues[2], aAttributes.mLightValues[3] + aUserspaceOffset.x.value, aAttributes.mLightValues[4] + aUserspaceOffset.y.value, aAttributes.mLightValues[5], aAttributes.mLightValues[6], aAttributes.mLightValues[7])); return WrFiltersStatus::SVGFE; case LightType::None: case LightType::Max: // No default case, so that the compiler will warn if new enums are added break; } MOZ_CRASH("Unrecognized LightType"); return WrFiltersStatus::BLOB_FALLBACK; } static WrFiltersStatus WrFilterOpSVGFETurbulence( WrFiltersHolder& aWrFilters, wr::FilterOpGraphNode& aGraphNode, const TurbulenceAttributes& aAttributes, const LayoutDevicePoint& aUserspaceOffset) { // SVGFETurbulence - Rare if (!StaticPrefs::gfx_webrender_svg_filter_effects_feturbulence()) { // Fallback if pref is disabled return WrFiltersStatus::BLOB_FALLBACK; } // The software implementation we use converts float to uint32_t and then // to int32_t, so we do that here to get identical results to the prior // implementation, in contrast to the spec which uses purely signed math // for setting up the seed. int32_t m1 = 2147483647 - 1; int32_t seed = (int32_t)((uint32_t)aAttributes.mSeed); if (seed <= 0) { seed = -(seed % m1) + 1; } if (seed > m1) { seed = m1; } switch (aAttributes.mType) { case SVGTurbulenceType::FractalNoise: if (aAttributes.mStitchable) { aWrFilters.filters.AppendElement( wr::FilterOp::SVGFETurbulenceWithFractalNoiseWithStitching( aGraphNode, aAttributes.mBaseFrequency.width, aAttributes.mBaseFrequency.height, aAttributes.mOctaves, seed)); } else { aWrFilters.filters.AppendElement( wr::FilterOp::SVGFETurbulenceWithFractalNoiseWithNoStitching( aGraphNode, aAttributes.mBaseFrequency.width, aAttributes.mBaseFrequency.height, aAttributes.mOctaves, seed)); } return WrFiltersStatus::SVGFE; case SVGTurbulenceType::Turbulence: if (aAttributes.mStitchable) { aWrFilters.filters.AppendElement( wr::FilterOp::SVGFETurbulenceWithTurbulenceNoiseWithStitching( aGraphNode, aAttributes.mBaseFrequency.width, aAttributes.mBaseFrequency.height, aAttributes.mOctaves, seed)); } else { aWrFilters.filters.AppendElement( wr::FilterOp::SVGFETurbulenceWithTurbulenceNoiseWithNoStitching( aGraphNode, aAttributes.mBaseFrequency.width, aAttributes.mBaseFrequency.height, aAttributes.mOctaves, seed)); } return WrFiltersStatus::SVGFE; default: break; } MOZ_CRASH("Unrecognized SVG_TURBULENCE_TYPE"); return WrFiltersStatus::BLOB_FALLBACK; } /// Builds filter DAG for fully accelerated rendering of SVG filter primitives /// and CSS filter chains using SVG filter primitives WrFiltersStatus FilterInstance::BuildWebRenderSVGFiltersImpl( nsIFrame* aFilteredFrame, Span aFilters, StyleFilterType aStyleFilterType, WrFiltersHolder& aWrFilters, const nsPoint& aOffsetForSVGFilters) { // If we return without making a valid filter graph, we need to restore // aInitialized before the fallback code is run. aWrFilters.filters.Clear(); aWrFilters.filter_datas.Clear(); aWrFilters.values.Clear(); aWrFilters.post_filters_clip = Nothing(); nsIFrame* firstFrame = nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFilteredFrame); nsTArray filterFrames; if (SVGObserverUtils::GetAndObserveFilters(firstFrame, &filterFrames, aStyleFilterType) == SVGObserverUtils::ReferenceState::HasRefsSomeInvalid) { return WrFiltersStatus::UNSUPPORTED; } std::unique_ptr metrics = UserSpaceMetricsForFrame(firstFrame); gfxRect filterSpaceBoundsNotSnapped; // TODO: simply using an identity matrix here, was pulling the scale from a // gfx context for the non-wr path. gfxMatrix scaleMatrix; gfxMatrix scaleMatrixInDevUnits = scaleMatrix * SVGUtils::GetCSSPxToDevPxMatrix(firstFrame); // Hardcode inputIsTainted to true because we don't want JS to be able to // read the rendered contents of aFilteredFrame. FilterInstance instance(firstFrame, firstFrame->GetContent(), *metrics, aFilters, filterFrames, /* inputIsTainted */ true, nullptr, scaleMatrixInDevUnits, nullptr, nullptr, nullptr, nullptr, &filterSpaceBoundsNotSnapped); if (!instance.IsInitialized()) { return WrFiltersStatus::UNSUPPORTED; } // If there more filters than the limit pref allows, we can drop the entire // filter graph and pretend we succeeded, the SVG spec allows us to drop any // overly complex graph, very large graphs tend to OOM anyway. if (instance.mFilterDescription.mPrimitives.Length() > StaticPrefs::gfx_webrender_max_filter_ops_per_chain()) { return WrFiltersStatus::DISABLED_FOR_PERFORMANCE; } // We have to remap the input nodes to a possibly larger number of output // nodes due to expanding feMerge. static constexpr size_t maxFilters = wr::SVGFE_GRAPH_MAX; int16_t bufferIdMapping[maxFilters]; // Just drop the graph if there are too many filters to process. if (instance.mFilterDescription.mPrimitives.Length() > maxFilters) { return WrFiltersStatus::DISABLED_FOR_PERFORMANCE; } // For subregions and filter parameters, we need to transform into the SVG // User Space coordinate system, which is the parent stacking context // coordinate system, not to be confused with Filter Space (which is this // stacking context's child coordinate system) or Frame Space. // // See nsLayoutUtils::ComputeOffsetToUserSpace for further explanation, and // SVGIntegrationUtils.cpp EffectOffsets::ComputeEffectOffset is how this is // done in the blob fallback. // // The display list we are building already puts the child elements' geometry // (if any) in SVG User Space, so we want the filter region and primitive // subregions to be in SVG User Space, so uerspaceOffset represents the offset // from Filter to User Space, which is in LayoutDevice units. // // As a practical matter, things like regular view zoom change Filter Space // scale so we don't have to do anything for that, pinch zoom in apz can be // doing its own thing but doesn't affect the coordinate system we use here, // as everything is multiplied by subregion and divided by filterRegion, so // they only need to be matching scale from WebRender perspective. LayoutDevicePoint userspaceOffset = LayoutDevicePoint::FromAppUnits( aOffsetForSVGFilters, aFilteredFrame->PresContext()->AppUnitsPerDevPixel()); // The bounds of SourceGraphic are defined in the spec as being equal to the // filter region, so we need to compute that, and while subregion bounds are // always integer, the bounds of the filter element (and hence filter region) // are not actually integer, so we need to account for the non-integer filter // region clip by using filterSpaceBoundsNotSnapped, this matters in: // ./mach reftest layout/reftests/svg/filter-scaled-01.svg wr::LayoutRect filterRegion = { {(float)(filterSpaceBoundsNotSnapped.TopLeft().x + userspaceOffset.x.value), (float)(filterSpaceBoundsNotSnapped.TopLeft().y + userspaceOffset.y.value)}, {(float)(filterSpaceBoundsNotSnapped.BottomRight().x + userspaceOffset.x.value), (float)(filterSpaceBoundsNotSnapped.BottomRight().y + userspaceOffset.y.value)}}; // To enforce the filterRegion clipping SourceGraphic before it enters the // graph we have to create a SourceGraphic node and SourceAlpha node, when we // implement StrokePaint and FillPaint they will need to create nodes on // demand however as they have custom colors (feFlood) and patterns (feTile). auto sourceGraphicNode = (int16_t)aWrFilters.filters.Length(); auto sourceNode = wr::FilterOpGraphNode{}; sourceNode.subregion = filterRegion; aWrFilters.filters.AppendElement( wr::FilterOp::SVGFESourceGraphic(sourceNode)); auto sourceAlphaNode = (int16_t)aWrFilters.filters.Length(); aWrFilters.filters.AppendElement(wr::FilterOp::SVGFESourceAlpha(sourceNode)); // We have some failure modes that can occur when processing the graph. WrFiltersStatus status = WrFiltersStatus::SVGFE; for (uint32_t i = 0; i < instance.mFilterDescription.mPrimitives.Length(); i++) { const auto& primitive = instance.mFilterDescription.mPrimitives[i]; const PrimitiveAttributes& attr = primitive.Attributes(); const bool linear = primitive.OutputColorSpace() == ColorSpace::LinearRGB; const size_t inputs = primitive.NumberOfInputs(); wr::FilterOpGraphNode graphNode = wr::FilterOpGraphNode{}; // Physical (linear) colorspace is the default in SVG filters, whereas all // CSS filters use sRGB (curved / naive) colorspace calculations for math, // this is the color-interpolation-filter property in SVG spec. Note that // feFlood cares about the color-interpolation property on the color value // provided, rather than the regular color-interpolation-filter property. graphNode.linear = linear; // Transform the subregion into SVG 'user space' which WebRender expects. graphNode.subregion = wr::ToLayoutRect(Rect(primitive.PrimitiveSubregion()) + userspaceOffset.ToUnknownPoint()); // We need to clip the final output node by the filterRegion, as it could // be non-integer (whereas the subregions were computed by SVGFilterInstance // code as integer only). if (i == instance.mFilterDescription.mPrimitives.Length() - 1) { if (graphNode.subregion.min.x < filterRegion.min.x) { graphNode.subregion.min.x = filterRegion.min.x; } if (graphNode.subregion.min.y < filterRegion.min.y) { graphNode.subregion.min.y = filterRegion.min.y; } if (graphNode.subregion.max.x > filterRegion.max.x) { graphNode.subregion.max.x = filterRegion.max.x; } if (graphNode.subregion.max.y > filterRegion.max.y) { graphNode.subregion.max.y = filterRegion.max.y; } } // Buffer ids are matched up later by WebRender to understand the DAG, we // hold the following assumptions (and verify them regularly): // * Inputs referencing buffer ids are always < node index // (This means the DAG can be walked sequentially as a flat array and // always evaluate correctly) // * node index < maxFilters graphNode.input.buffer_id = wr::FilterOpGraphPictureBufferId::None(); graphNode.input2.buffer_id = wr::FilterOpGraphPictureBufferId::None(); if (inputs >= 1) { status = WrSVGFEInputBuild( graphNode.input, primitive.InputPrimitiveIndex(0), (int16_t)i, sourceGraphicNode, sourceAlphaNode, bufferIdMapping); if (status != WrFiltersStatus::SVGFE) { break; } if (inputs >= 2) { status = WrSVGFEInputBuild( graphNode.input2, primitive.InputPrimitiveIndex(1), (int16_t)i, sourceGraphicNode, sourceAlphaNode, bufferIdMapping); if (status != WrFiltersStatus::SVGFE) { break; } } } // If there are too many filters (after feMerge expansion) to keep track of // in bufferIdMapping[] then we can just drop the entire graph, the SVG spec // allows us to drop overly complex graphs and maxFilters is not a small // quantity. if (aWrFilters.filters.Length() >= maxFilters) { status = WrFiltersStatus::DISABLED_FOR_PERFORMANCE; break; } if (attr.is()) { status = WrFilterOpSVGFEOpacity(aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFEToAlpha(aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFEBlend(aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFEColorMatrix(aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFEComponentTransfer( aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFEComposite(aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFEConvolveMatrix( aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFEDiffuseLighting( aWrFilters, graphNode, attr.as(), userspaceOffset); } else if (attr.is()) { status = WrFilterOpSVGFEDisplacementMap( aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFEDropShadow(aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFEFlood(aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFEGaussianBlur(aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFEImage( aWrFilters, graphNode, attr.as(), userspaceOffset); } else if (attr.is()) { status = WrFilterOpSVGFEMerge( aWrFilters, graphNode, attr.as(), instance.mFilterDescription.mPrimitives[i], (int16_t)i, sourceGraphicNode, sourceAlphaNode, bufferIdMapping, maxFilters); } else if (attr.is()) { status = WrFilterOpSVGFEMorphology(aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFEOffset(aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFESpecularLighting( aWrFilters, graphNode, attr.as(), userspaceOffset); } else if (attr.is()) { status = WrFilterOpSVGFETile(aWrFilters, graphNode, attr.as()); } else if (attr.is()) { status = WrFilterOpSVGFETurbulence(aWrFilters, graphNode, attr.as(), userspaceOffset); } else { // Unknown attributes type? status = WrFiltersStatus::BLOB_FALLBACK; } if (status != WrFiltersStatus::SVGFE) { break; } // Set the remapping table entry bufferIdMapping[i] = (int16_t)(aWrFilters.filters.Length() - 1); } if (status != WrFiltersStatus::SVGFE) { // If we couldn't handle this graph, clear the filters before returning. aWrFilters.filters.Clear(); aWrFilters.filter_datas.Clear(); aWrFilters.values.Clear(); aWrFilters.post_filters_clip = Nothing(); } return status; } nsRegion FilterInstance::GetPreFilterNeededArea( nsIFrame* aFilteredFrame, const nsTArray& aFilterFrames, const nsRegion& aPostFilterDirtyRegion) { gfxMatrix tm = SVGUtils::GetCanvasTM(aFilteredFrame); auto filterChain = aFilteredFrame->StyleEffects()->mFilters.AsSpan(); std::unique_ptr metrics = UserSpaceMetricsForFrame(aFilteredFrame); // Hardcode InputIsTainted to true because we don't want JS to be able to // read the rendered contents of aFilteredFrame. FilterInstance instance(aFilteredFrame, aFilteredFrame->GetContent(), *metrics, filterChain, aFilterFrames, /* InputIsTainted */ true, nullptr, tm, &aPostFilterDirtyRegion); if (!instance.IsInitialized()) { return nsRect(); } // Now we can ask the instance to compute the area of the source // that's needed. return instance.ComputeSourceNeededRect(); } Maybe FilterInstance::GetPostFilterBounds( nsIFrame* aFilteredFrame, const nsTArray& aFilterFrames, const gfxRect* aOverrideBBox, const nsRect* aPreFilterBounds) { MOZ_ASSERT(!aFilteredFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT) || !aFilteredFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), "Non-display SVG do not maintain ink overflow rects"); nsRegion preFilterRegion; nsRegion* preFilterRegionPtr = nullptr; if (aPreFilterBounds) { preFilterRegion = *aPreFilterBounds; preFilterRegionPtr = &preFilterRegion; } gfxMatrix tm = SVGUtils::GetCanvasTM(aFilteredFrame); auto filterChain = aFilteredFrame->StyleEffects()->mFilters.AsSpan(); std::unique_ptr metrics = UserSpaceMetricsForFrame(aFilteredFrame); // Hardcode InputIsTainted to true because we don't want JS to be able to // read the rendered contents of aFilteredFrame. FilterInstance instance(aFilteredFrame, aFilteredFrame->GetContent(), *metrics, filterChain, aFilterFrames, /* InputIsTainted */ true, nullptr, tm, nullptr, preFilterRegionPtr, aPreFilterBounds, aOverrideBBox); if (!instance.IsInitialized()) { return Nothing(); } return Some(instance.ComputePostFilterExtents()); } FilterInstance::FilterInstance( nsIFrame* aTargetFrame, nsIContent* aTargetContent, const UserSpaceMetrics& aMetrics, Span aFilterChain, const nsTArray& aFilterFrames, bool aFilterInputIsTainted, const SVGFilterPaintCallback& aPaintCallback, const gfxMatrix& aPaintTransform, const nsRegion* aPostFilterDirtyRegion, const nsRegion* aPreFilterDirtyRegion, const nsRect* aPreFilterInkOverflowRectOverride, const gfxRect* aOverrideBBox, gfxRect* aFilterSpaceBoundsNotSnapped) : mTargetFrame(aTargetFrame), mTargetContent(aTargetContent), mMetrics(aMetrics), mPaintCallback(aPaintCallback), mPaintTransform(aPaintTransform), mInitialized(false) { if (aOverrideBBox) { mTargetBBox = *aOverrideBBox; } else { MOZ_ASSERT(mTargetFrame, "Need to supply a frame when there's no aOverrideBBox"); mTargetBBox = SVGUtils::GetBBox(mTargetFrame, {SVGBBoxFlag::UseFrameBoundsForOuterSVG, SVGBBoxFlag::IncludeFillGeometry}); } // Compute user space to filter space transforms. if (!ComputeUserSpaceToFilterSpaceScale()) { return; } if (!ComputeTargetBBoxInFilterSpace()) { return; } // Get various transforms: gfxMatrix filterToUserSpace(mFilterSpaceToUserSpaceScale.xScale, 0.0f, 0.0f, mFilterSpaceToUserSpaceScale.yScale, 0.0f, 0.0f); mFilterSpaceToFrameSpaceInCSSPxTransform = filterToUserSpace * GetUserSpaceToFrameSpaceInCSSPxTransform(); // mFilterSpaceToFrameSpaceInCSSPxTransform is always invertible mFrameSpaceInCSSPxToFilterSpaceTransform = mFilterSpaceToFrameSpaceInCSSPxTransform; mFrameSpaceInCSSPxToFilterSpaceTransform.Invert(); nsIntRect targetBounds; if (aPreFilterInkOverflowRectOverride) { targetBounds = FrameSpaceToFilterSpace(aPreFilterInkOverflowRectOverride); } else if (mTargetFrame) { nsRect preFilterVOR = mTargetFrame->PreEffectsInkOverflowRect(); targetBounds = FrameSpaceToFilterSpace(&preFilterVOR); } mTargetBounds.UnionRect(mTargetBBoxInFilterSpace, targetBounds); // Build the filter graph. if (NS_FAILED(BuildPrimitives(aFilterChain, aFilterFrames, aFilterInputIsTainted))) { return; } // Convert the passed in rects from frame space to filter space: mPostFilterDirtyRegion = FrameSpaceToFilterSpace(aPostFilterDirtyRegion); mPreFilterDirtyRegion = FrameSpaceToFilterSpace(aPreFilterDirtyRegion); if (aFilterSpaceBoundsNotSnapped) { *aFilterSpaceBoundsNotSnapped = mFilterSpaceBoundsNotSnapped; } mInitialized = true; } bool FilterInstance::ComputeTargetBBoxInFilterSpace() { gfxRect targetBBoxInFilterSpace = UserSpaceToFilterSpace(mTargetBBox); targetBBoxInFilterSpace.RoundOut(); return gfxUtils::GfxRectToIntRect(targetBBoxInFilterSpace, &mTargetBBoxInFilterSpace); } bool FilterInstance::ComputeUserSpaceToFilterSpaceScale() { if (mTargetFrame) { mUserSpaceToFilterSpaceScale = mPaintTransform.ScaleFactors(); if (mUserSpaceToFilterSpaceScale.xScale <= 0.0f || mUserSpaceToFilterSpaceScale.yScale <= 0.0f) { // Nothing should be rendered. return false; } } else { mUserSpaceToFilterSpaceScale = MatrixScalesDouble(); } mFilterSpaceToUserSpaceScale = MatrixScalesDouble(1.0f / mUserSpaceToFilterSpaceScale.xScale, 1.0f / mUserSpaceToFilterSpaceScale.yScale); return true; } gfxRect FilterInstance::UserSpaceToFilterSpace( const gfxRect& aUserSpaceRect) const { gfxRect filterSpaceRect = aUserSpaceRect; filterSpaceRect.Scale(mUserSpaceToFilterSpaceScale); return filterSpaceRect; } gfxRect FilterInstance::FilterSpaceToUserSpace( const gfxRect& aFilterSpaceRect) const { gfxRect userSpaceRect = aFilterSpaceRect; userSpaceRect.Scale(mFilterSpaceToUserSpaceScale); return userSpaceRect; } nsresult FilterInstance::BuildPrimitives( Span aFilterChain, const nsTArray& aFilterFrames, bool aFilterInputIsTainted) { AutoTArray primitiveDescriptions; uint32_t filterIndex = 0; for (const auto& filter : aFilterChain) { if (filter.IsUrl() && aFilterFrames.IsEmpty()) { return NS_ERROR_FAILURE; } auto* filterFrame = filter.IsUrl() ? aFilterFrames[filterIndex++] : nullptr; bool inputIsTainted = primitiveDescriptions.IsEmpty() ? aFilterInputIsTainted : primitiveDescriptions.LastElement().IsTainted(); nsresult rv = BuildPrimitivesForFilter(filter, filterFrame, inputIsTainted, primitiveDescriptions); if (NS_FAILED(rv)) { return rv; } } mFilterDescription = FilterDescription(std::move(primitiveDescriptions)); return NS_OK; } nsresult FilterInstance::BuildPrimitivesForFilter( const StyleFilter& aFilter, SVGFilterFrame* aFilterFrame, bool aInputIsTainted, nsTArray& aPrimitiveDescriptions) { NS_ASSERTION(mUserSpaceToFilterSpaceScale.xScale > 0.0f && mFilterSpaceToUserSpaceScale.yScale > 0.0f, "scale factors between spaces should be positive values"); if (aFilter.IsUrl()) { // Build primitives for an SVG filter. SVGFilterInstance svgFilterInstance( aFilter, aFilterFrame, mTargetContent, mMetrics, mTargetBBox, mUserSpaceToFilterSpaceScale, mFilterSpaceBoundsNotSnapped); if (!svgFilterInstance.IsInitialized()) { return NS_ERROR_FAILURE; } return svgFilterInstance.BuildPrimitives(aPrimitiveDescriptions, mInputImages, aInputIsTainted); } // Build primitives for a CSS filter. // If we don't have a frame, use opaque black for shadows with unspecified // shadow colors. nscolor shadowFallbackColor = mTargetFrame ? mTargetFrame->StyleText()->mColor.ToColor() : NS_RGB(0, 0, 0); CSSFilterInstance cssFilterInstance(aFilter, shadowFallbackColor, mTargetBounds, mFrameSpaceInCSSPxToFilterSpaceTransform); return cssFilterInstance.BuildPrimitives(aPrimitiveDescriptions, aInputIsTainted); } static void UpdateNeededBounds(const nsIntRegion& aRegion, nsIntRect& aBounds) { aBounds = aRegion.GetBounds(); if (aBounds.IsEmpty()) { return; } bool overflow; IntSize surfaceSize = SVGUtils::ConvertToSurfaceSize(SizeDouble(aBounds.Size()), &overflow); if (overflow) { aBounds.SizeTo(surfaceSize); } } void FilterInstance::ComputeNeededBoxes() { if (mFilterDescription.mPrimitives.IsEmpty()) { return; } nsIntRegion sourceGraphicNeededRegion; nsIntRegion fillPaintNeededRegion; nsIntRegion strokePaintNeededRegion; FilterSupport::ComputeSourceNeededRegions( mFilterDescription, mPostFilterDirtyRegion, sourceGraphicNeededRegion, fillPaintNeededRegion, strokePaintNeededRegion); sourceGraphicNeededRegion.AndWith(mTargetBounds); UpdateNeededBounds(sourceGraphicNeededRegion, mSourceGraphic.mNeededBounds); UpdateNeededBounds(fillPaintNeededRegion, mFillPaint.mNeededBounds); UpdateNeededBounds(strokePaintNeededRegion, mStrokePaint.mNeededBounds); } void FilterInstance::BuildSourcePaint(SourceInfo* aSource, imgDrawingParams& aImgParams) { MOZ_ASSERT(mTargetFrame); nsIntRect neededRect = aSource->mNeededBounds; if (neededRect.IsEmpty()) { return; } RefPtr offscreenDT = gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget( neededRect.Size(), SurfaceFormat::B8G8R8A8); if (!offscreenDT || !offscreenDT->IsValid()) { return; } gfxContext ctx(offscreenDT); gfxContextAutoSaveRestore saver(&ctx); ctx.SetMatrixDouble(mPaintTransform * gfxMatrix::Translation(-neededRect.TopLeft())); GeneralPattern pattern; if (aSource == &mFillPaint) { SVGUtils::MakeFillPatternFor(mTargetFrame, &ctx, &pattern, aImgParams); } else if (aSource == &mStrokePaint) { SVGUtils::MakeStrokePatternFor(mTargetFrame, &ctx, &pattern, aImgParams); } if (pattern.GetPattern()) { offscreenDT->FillRect( ToRect(FilterSpaceToUserSpace(ThebesRect(neededRect))), pattern); } aSource->mSourceSurface = offscreenDT->Snapshot(); aSource->mSurfaceRect = neededRect; } void FilterInstance::BuildSourcePaints(imgDrawingParams& aImgParams) { if (!mFillPaint.mNeededBounds.IsEmpty()) { BuildSourcePaint(&mFillPaint, aImgParams); } if (!mStrokePaint.mNeededBounds.IsEmpty()) { BuildSourcePaint(&mStrokePaint, aImgParams); } } void FilterInstance::BuildSourceImage(DrawTarget* aDest, imgDrawingParams& aImgParams, FilterNode* aFilter, FilterNode* aSource, const Rect& aSourceRect) { MOZ_ASSERT(mTargetFrame); nsIntRect neededRect = mSourceGraphic.mNeededBounds; if (neededRect.IsEmpty()) { return; } RefPtr offscreenDT; SurfaceFormat format = SurfaceFormat::B8G8R8A8; if (aDest->CanCreateSimilarDrawTarget(neededRect.Size(), format)) { offscreenDT = aDest->CreateSimilarDrawTargetForFilter( neededRect.Size(), format, aFilter, aSource, aSourceRect, Point(0, 0)); } if (!offscreenDT || !offscreenDT->IsValid()) { return; } gfxRect r = FilterSpaceToUserSpace(ThebesRect(neededRect)); r.RoundOut(); nsIntRect dirty; if (!gfxUtils::GfxRectToIntRect(r, &dirty)) { return; } // SVG graphics paint to device space, so we need to set an initial device // space to filter space transform on the gfxContext that SourceGraphic // and SourceAlpha will paint to. // // (In theory it would be better to minimize error by having filtered SVG // graphics temporarily paint to user space when painting the sources and // only set a user space to filter space transform on the gfxContext // (since that would eliminate the transform multiplications from user // space to device space and back again). However, that would make the // code more complex while being hard to get right without introducing // subtle bugs, and in practice it probably makes no real difference.) gfxContext ctx(offscreenDT); gfxMatrix devPxToCssPxTM = SVGUtils::GetCSSPxToDevPxMatrix(mTargetFrame); DebugOnly invertible = devPxToCssPxTM.Invert(); MOZ_ASSERT(invertible); ctx.SetMatrixDouble(devPxToCssPxTM * mPaintTransform * gfxMatrix::Translation(-neededRect.TopLeft())); auto imageFlags = aImgParams.imageFlags; if (mTargetFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { // We're coming from a mask or pattern instance. Patterns // are painted into a separate surface and it seems we can't // handle the differently sized surface that might be returned // with FLAG_HIGH_QUALITY_SCALING imageFlags &= ~imgIContainer::FLAG_HIGH_QUALITY_SCALING; } imgDrawingParams imgParams(imageFlags); mPaintCallback(ctx, imgParams, &mPaintTransform, &dirty); aImgParams.result = imgParams.result; mSourceGraphic.mSourceSurface = offscreenDT->Snapshot(); mSourceGraphic.mSurfaceRect = neededRect; } void FilterInstance::Render(gfxContext* aCtx, imgDrawingParams& aImgParams, float aOpacity) { MOZ_ASSERT(mTargetFrame, "Need a frame for rendering"); if (mFilterDescription.mPrimitives.IsEmpty()) { // An filter without any primitive. Treat it as success and paint nothing. return; } nsIntRect filterRect = mPostFilterDirtyRegion.GetBounds().Intersect(OutputFilterSpaceBounds()); if (filterRect.IsEmpty() || mPaintTransform.IsSingular()) { return; } gfxContextMatrixAutoSaveRestore autoSR(aCtx); aCtx->SetMatrix( aCtx->CurrentMatrix().PreTranslate(filterRect.x, filterRect.y)); ComputeNeededBoxes(); Rect renderRect = IntRectToRect(filterRect); RefPtr dt = aCtx->GetDrawTarget(); MOZ_ASSERT(dt); if (!dt->IsValid()) { return; } BuildSourcePaints(aImgParams); RefPtr sourceGraphic, fillPaint, strokePaint; if (mFillPaint.mSourceSurface) { fillPaint = FilterWrappers::ForSurface(dt, mFillPaint.mSourceSurface, mFillPaint.mSurfaceRect.TopLeft()); } if (mStrokePaint.mSourceSurface) { strokePaint = FilterWrappers::ForSurface( dt, mStrokePaint.mSourceSurface, mStrokePaint.mSurfaceRect.TopLeft()); } // We make the sourceGraphic filter but don't set its inputs until after so // that we can make the sourceGraphic size depend on the filter chain sourceGraphic = dt->CreateFilter(FilterType::TRANSFORM); if (sourceGraphic) { // Make sure we set the translation before calling BuildSourceImage // so that CreateSimilarDrawTargetForFilter works properly IntPoint offset = mSourceGraphic.mNeededBounds.TopLeft(); sourceGraphic->SetAttribute(ATT_TRANSFORM_MATRIX, Matrix::Translation(offset.x, offset.y)); } RefPtr resultFilter = FilterNodeGraphFromDescription( dt, mFilterDescription, renderRect, sourceGraphic, mSourceGraphic.mSurfaceRect, fillPaint, strokePaint, mInputImages); if (!resultFilter) { gfxWarning() << "Filter is NULL."; return; } BuildSourceImage(dt, aImgParams, resultFilter, sourceGraphic, renderRect); if (sourceGraphic) { if (mSourceGraphic.mSourceSurface) { sourceGraphic->SetInput(IN_TRANSFORM_IN, mSourceGraphic.mSourceSurface); } else { RefPtr clear = FilterWrappers::Clear(aCtx->GetDrawTarget()); sourceGraphic->SetInput(IN_TRANSFORM_IN, clear); } } dt->DrawFilter(resultFilter, renderRect, Point(0, 0), DrawOptions(aOpacity)); } nsRegion FilterInstance::ComputePostFilterDirtyRegion() { if (mPreFilterDirtyRegion.IsEmpty() || mFilterDescription.mPrimitives.IsEmpty()) { return nsRegion(); } nsIntRegion resultChangeRegion = FilterSupport::ComputeResultChangeRegion( mFilterDescription, mPreFilterDirtyRegion, nsIntRegion(), nsIntRegion()); return FilterSpaceToFrameSpace(resultChangeRegion); } nsRect FilterInstance::ComputePostFilterExtents() { if (mFilterDescription.mPrimitives.IsEmpty()) { return nsRect(); } nsIntRegion postFilterExtents = FilterSupport::ComputePostFilterExtents( mFilterDescription, mTargetBounds); return FilterSpaceToFrameSpace(postFilterExtents.GetBounds()); } nsRect FilterInstance::ComputeSourceNeededRect() { ComputeNeededBoxes(); return FilterSpaceToFrameSpace(mSourceGraphic.mNeededBounds); } nsIntRect FilterInstance::OutputFilterSpaceBounds() const { if (mFilterDescription.mPrimitives.IsEmpty()) { return nsIntRect(); } return mFilterDescription.mPrimitives.LastElement().PrimitiveSubregion(); } nsIntRect FilterInstance::FrameSpaceToFilterSpace(const nsRect* aRect) const { nsIntRect rect = OutputFilterSpaceBounds(); if (aRect) { if (aRect->IsEmpty()) { return nsIntRect(); } gfxRect rectInCSSPx = nsLayoutUtils::RectToGfxRect(*aRect, AppUnitsPerCSSPixel()); gfxRect rectInFilterSpace = mFrameSpaceInCSSPxToFilterSpaceTransform.TransformBounds(rectInCSSPx); rectInFilterSpace.RoundOut(); nsIntRect intRect; if (gfxUtils::GfxRectToIntRect(rectInFilterSpace, &intRect)) { rect = intRect; } } return rect; } nsRect FilterInstance::FilterSpaceToFrameSpace(const nsIntRect& aRect) const { if (aRect.IsEmpty()) { return nsRect(); } gfxRect r(aRect.x, aRect.y, aRect.width, aRect.height); r = mFilterSpaceToFrameSpaceInCSSPxTransform.TransformBounds(r); // nsLayoutUtils::RoundGfxRectToAppRect rounds out. return nsLayoutUtils::RoundGfxRectToAppRect(r, AppUnitsPerCSSPixel()); } nsIntRegion FilterInstance::FrameSpaceToFilterSpace( const nsRegion* aRegion) const { if (!aRegion) { return OutputFilterSpaceBounds(); } nsIntRegion result; for (auto iter = aRegion->RectIter(); !iter.Done(); iter.Next()) { // FrameSpaceToFilterSpace rounds out, so this works. nsRect rect = iter.Get(); result.OrWith(FrameSpaceToFilterSpace(&rect)); } return result; } nsRegion FilterInstance::FilterSpaceToFrameSpace( const nsIntRegion& aRegion) const { nsRegion result; for (auto iter = aRegion.RectIter(); !iter.Done(); iter.Next()) { // FilterSpaceToFrameSpace rounds out, so this works. result.OrWith(FilterSpaceToFrameSpace(iter.Get())); } return result; } gfxMatrix FilterInstance::GetUserSpaceToFrameSpaceInCSSPxTransform() const { if (!mTargetFrame) { return gfxMatrix(); } return gfxMatrix::Translation( -SVGUtils::FrameSpaceInCSSPxToUserSpaceOffset(mTargetFrame)); } } // namespace mozilla