/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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/. */ #ifndef TextDrawTarget_h #define TextDrawTarget_h #include "mozilla/gfx/2D.h" #include "mozilla/layers/RenderRootStateManager.h" #include "mozilla/layers/WebRenderLayerManager.h" #include "mozilla/layers/WebRenderBridgeChild.h" #include "mozilla/webrender/WebRenderAPI.h" #include "mozilla/layers/StackingContextHelper.h" #include "mozilla/layers/IpcResourceUpdateQueue.h" namespace mozilla { namespace layout { using namespace gfx; // This class is a fake DrawTarget, used to intercept text draw calls, while // also collecting up the other aspects of text natively. // // When using advanced-layers in nsDisplayText's constructor, we construct this // and run the full painting algorithm with this as the DrawTarget. This is // done to avoid having to massively refactor gecko's text painting code (which // has lots of components shared between other rendering algorithms). // // In some phases of the painting algorithm, we can grab the relevant values // and feed them directly into TextDrawTarget. For instance, selections, // decorations, and shadows are handled in this manner. In those cases we can // also short-circuit the painting algorithm to save work. // // In other phases, the computed values are sufficiently buried in complex // code that it's best for us to just intercept the final draw calls. This // is how we handle computing the glyphs of the main text and text-emphasis // (see our overloaded FillGlyphs implementation). // // To be clear: this is a big hack. With time we hope to refactor the codebase // so that all the elements of text are handled directly by TextDrawTarget, // which is to say everything is done like we do selections and shadows now. // This design is a good step for doing this work incrementally. // // This is also likely to be a bit buggy (missing or misinterpreted info) // while we further develop the design. // // TextDrawTarget doesn't yet support all features. See mHasUnsupportedFeatures // for details. class TextDrawTarget : public DrawTarget { public: explicit TextDrawTarget(wr::DisplayListBuilder& aBuilder, wr::IpcResourceUpdateQueue& aResources, const layers::StackingContextHelper& aSc, layers::RenderRootStateManager* aManager, nsDisplayItem* aItem, nsRect& aBounds, bool aCallerDoesSaveRestore = false) : mCallerDoesSaveRestore(aCallerDoesSaveRestore), mBuilder(aBuilder) { Reinitialize(aResources, aSc, aManager, aItem, aBounds); } // Prevent this from being copied TextDrawTarget(const TextDrawTarget& src) = delete; TextDrawTarget& operator=(const TextDrawTarget&) = delete; ~TextDrawTarget() { MOZ_ASSERT(mFinished); } void Reinitialize(wr::IpcResourceUpdateQueue& aResources, const layers::StackingContextHelper& aSc, layers::RenderRootStateManager* aManager, nsDisplayItem* aItem, nsRect& aBounds) { mResources = &aResources; mSc = &aSc; mManager = aManager; mHasUnsupportedFeatures = false; mHasShadows = false; SetPermitSubpixelAA(true); // Compute clip/bounds auto appUnitsPerDevPixel = aItem->Frame()->PresContext()->AppUnitsPerDevPixel(); LayoutDeviceRect layoutBoundsRect = LayoutDeviceRect::FromAppUnits(aBounds, appUnitsPerDevPixel); LayoutDeviceRect layoutClipRect = layoutBoundsRect; mBoundsRect = wr::ToLayoutRect(layoutBoundsRect); // Add 1 pixel of dirty area around clip rect to allow us to paint // antialiased pixels beyond the measured text extents. layoutClipRect.Inflate(1); mSize = IntSize::Ceil(layoutClipRect.Width(), layoutClipRect.Height()); mClipStack.ClearAndRetainStorage(); mClipStack.AppendElement(layoutClipRect); mBackfaceVisible = !aItem->BackfaceIsHidden(); if (!mCallerDoesSaveRestore) { mBuilder.Save(); } } void FoundUnsupportedFeature() { mHasUnsupportedFeatures = true; } bool CheckHasUnsupportedFeatures() { MOZ_ASSERT(mCallerDoesSaveRestore); #ifdef DEBUG MOZ_ASSERT(!mFinished); mFinished = true; #endif return mHasUnsupportedFeatures; } bool Finish() { MOZ_ASSERT(!mCallerDoesSaveRestore); #ifdef DEBUG mFinished = true; #endif if (mHasUnsupportedFeatures) { mBuilder.Restore(); return false; } mBuilder.ClearSave(); return true; } wr::FontInstanceFlags GetWRGlyphFlags() const { return mWRGlyphFlags; } void SetWRGlyphFlags(wr::FontInstanceFlags aFlags) { mWRGlyphFlags = aFlags; } class AutoRestoreWRGlyphFlags { public: ~AutoRestoreWRGlyphFlags() { if (mTarget) { mTarget->SetWRGlyphFlags(mFlags); } } void Save(TextDrawTarget* aTarget) { // This allows for recursive saves, in case the flags need to be modified // under multiple conditions (i.e. transforms and synthetic italics), // since the flags will be restored to the first saved value in the // destructor on scope exit. if (!mTarget) { // Only record the first save with the original flags that will be // restored. mTarget = aTarget; mFlags = aTarget->GetWRGlyphFlags(); } else { // Ensure that this is actually a recursive save to the same target MOZ_ASSERT( mTarget == aTarget, "Recursive save of WR glyph flags to different TextDrawTargets"); } } private: TextDrawTarget* mTarget = nullptr; wr::FontInstanceFlags mFlags = {0}; }; // This overload just stores the glyphs/font/color. void FillGlyphs(ScaledFont* aFont, const GlyphBuffer& aBuffer, const Pattern& aPattern, const DrawOptions& aOptions) override { // Make sure we're only given boring color patterns MOZ_RELEASE_ASSERT(aOptions.mCompositionOp == CompositionOp::OP_OVER); MOZ_RELEASE_ASSERT(aOptions.mAlpha == 1.0f); MOZ_RELEASE_ASSERT(aPattern.GetType() == PatternType::COLOR); // Make sure the font exists, and can be serialized MOZ_RELEASE_ASSERT(aFont); if (!aFont->CanSerialize()) { FoundUnsupportedFeature(); return; } auto* colorPat = static_cast(&aPattern); auto color = wr::ToColorF(colorPat->mColor); MOZ_ASSERT(aBuffer.mNumGlyphs); auto glyphs = Range( reinterpret_cast(aBuffer.mGlyphs), aBuffer.mNumGlyphs); // MSVC won't let us use offsetof on the following directly so we give it a // name with typedef typedef std::remove_reference::type GlyphType; // Compare gfx::Glyph and wr::GlyphInstance to make sure that they are // structurally equivalent to ensure that our cast above was ok static_assert( std::is_same() && std::is_same() && std::is_same() && offsetof(GlyphType, mIndex) == offsetof(wr::GlyphInstance, index) && offsetof(GlyphType, mPosition) == offsetof(wr::GlyphInstance, point) && offsetof(decltype(aBuffer.mGlyphs[0].mPosition), x) == offsetof(decltype(glyphs[0].point), x) && offsetof(decltype(aBuffer.mGlyphs[0].mPosition), y) == offsetof(decltype(glyphs[0].point), y) && std::is_standard_layout< std::remove_reference>::value && std::is_standard_layout< std::remove_reference>::value && sizeof(aBuffer.mGlyphs[0]) == sizeof(glyphs[0]) && sizeof(aBuffer.mGlyphs[0].mPosition) == sizeof(glyphs[0].point), "glyph buf types don't match"); wr::GlyphOptions glyphOptions; glyphOptions.render_mode = wr::ToFontRenderMode(aOptions.mAntialiasMode, GetPermitSubpixelAA()); glyphOptions.flags = mWRGlyphFlags; mManager->WrBridge()->PushGlyphs(mBuilder, *mResources, glyphs, aFont, color, *mSc, mBoundsRect, ClipRect(), mBackfaceVisible, &glyphOptions); } void PushClipRect(const Rect& aRect) override { LayoutDeviceRect rect = LayoutDeviceRect::FromUnknownRect(aRect); rect = rect.Intersect(mClipStack.LastElement()); mClipStack.AppendElement(rect); } void PopClip() override { mClipStack.RemoveLastElement(); } IntSize GetSize() const override { return mSize; } void AppendShadow(const wr::Shadow& aShadow, bool aInflate) { mBuilder.PushShadow(mBoundsRect, ClipRect(), mBackfaceVisible, aShadow, aInflate); mHasShadows = true; } void TerminateShadows() { if (mHasShadows) { mBuilder.PopAllShadows(); mHasShadows = false; } } void AppendSelectionRect(const LayoutDeviceRect& aRect, const DeviceColor& aColor) { auto rect = wr::ToLayoutRect(aRect); auto color = wr::ToColorF(aColor); mBuilder.PushRect(rect, ClipRect(), mBackfaceVisible, false, false, color); } // This function is basically designed to slide into the decoration drawing // code of nsCSSRendering with minimum disruption, to minimize the // chances of implementation drift. As such, it mostly looks like a call // to a skia-style StrokeLine method: two end-points, with a thickness // and style. Notably the end-points are *centered* in the block direction, // even though webrender wants a rect-like representation, where the points // are on corners. // // So we mangle the format here in a single centralized place, where neither // webrender nor nsCSSRendering has to care about this mismatch. // // NOTE: we assume the points are axis-aligned, and aStart should be used // as the top-left corner of the rect. void AppendDecoration(const Point& aStart, const Point& aEnd, const float aThickness, const bool aVertical, const DeviceColor& aColor, const StyleTextDecorationStyle aStyle) { auto pos = LayoutDevicePoint::FromUnknownPoint(aStart); LayoutDeviceSize size; if (aVertical) { pos.x -= aThickness / 2; // adjust from center to corner size = LayoutDeviceSize(aThickness, ViewAs(aEnd.y - aStart.y)); } else { pos.y -= aThickness / 2; // adjust from center to corner size = LayoutDeviceSize(ViewAs(aEnd.x - aStart.x), aThickness); } wr::Line decoration; decoration.bounds = wr::ToLayoutRect(LayoutDeviceRect(pos, size)); decoration.wavyLineThickness = 0; // dummy value, unused decoration.color = wr::ToColorF(aColor); decoration.orientation = aVertical ? wr::LineOrientation::Vertical : wr::LineOrientation::Horizontal; switch (aStyle) { case StyleTextDecorationStyle::Solid: decoration.style = wr::LineStyle::Solid; break; case StyleTextDecorationStyle::Dotted: decoration.style = wr::LineStyle::Dotted; break; case StyleTextDecorationStyle::Dashed: decoration.style = wr::LineStyle::Dashed; break; // Wavy lines should go through AppendWavyDecoration case StyleTextDecorationStyle::Wavy: // Double lines should be lowered to two solid lines case StyleTextDecorationStyle::Double: default: MOZ_CRASH("TextDrawTarget received unsupported line style"); } mBuilder.PushLine(ClipRect(), mBackfaceVisible, decoration); } // Seperated out from AppendDecoration because Wavy Lines are completely // different, and trying to merge the concept is more of a mess than it's // worth. void AppendWavyDecoration(const Rect& aBounds, const float aThickness, const bool aVertical, const DeviceColor& aColor) { wr::Line decoration; decoration.bounds = wr::ToLayoutRect(LayoutDeviceRect::FromUnknownRect(aBounds)); decoration.wavyLineThickness = aThickness; decoration.color = wr::ToColorF(aColor); decoration.orientation = aVertical ? wr::LineOrientation::Vertical : wr::LineOrientation::Horizontal; decoration.style = wr::LineStyle::Wavy; mBuilder.PushLine(ClipRect(), mBackfaceVisible, decoration); } layers::WebRenderBridgeChild* WrBridge() { return mManager->WrBridge(); } layers::WebRenderLayerManager* WrLayerManager() { return mManager->LayerManager(); } Maybe DefineImage(const IntSize& aSize, uint32_t aStride, SurfaceFormat aFormat, const uint8_t* aData) { wr::ImageKey key = mManager->WrBridge()->GetNextImageKey(); wr::ImageDescriptor desc(aSize, aStride, aFormat); Range bytes(const_cast(aData), aStride * aSize.height); if (mResources->AddImage(key, desc, bytes)) { return Some(key); } return Nothing(); } void PushImage(wr::ImageKey aKey, const Rect& aBounds, const Rect& aClip, wr::ImageRendering aFilter, const wr::ColorF& aColor) { if (!aClip.Intersects(GeckoClipRect().ToUnknownRect())) { return; } mBuilder.PushImage(wr::ToLayoutRect(aBounds), wr::ToLayoutRect(aClip), true, false, aFilter, aKey, true, aColor); } LayoutDeviceRect GeckoClipRect() { return mClipStack.LastElement(); } private: wr::LayoutRect ClipRect() { return wr::ToLayoutRect(mClipStack.LastElement()); } // Whether anything unsupported was encountered. This will result in this // text being emitted as a blob, which means subpixel-AA can't be used and // that performance will probably be a bit worse. At this point, we've // properly implemented everything that shows up a lot, so you can assume // that the remaining things we don't implement are fairly rare. The complete // set of things that we don't implement are as follows: // // * Unserializable Fonts: WR lives across an IPC boundary // * Text-Combine-Upright Squishing: no one's really bothered to impl it yet // * Text-Stroke: not a real standard (exists for webcompat) // * SVG Glyphs: not a real standard (we got overzealous with svg) // * Color Glyphs (Emoji) With Transparency: requires us to apply transparency // with a composited layer (a single emoji can be many single-color glyphs) // // The transparent colored-glyphs issue is probably the most valuable to fix, // since ideally it would also result in us fixing transparency for all // intersecting glyphs (which currently look bad with or without webrender, // so there's no fallback like with emoji). Specifically, transparency // looks bad for "cursive" fonts where glyphs overlap at the seams. Since // this is more common for non-latin scripts (e.g. मनीष), this amounts to us // treating non-latin scripts poorly... unless they're emoji. Yikes! bool mHasUnsupportedFeatures = false; // The caller promises to call Save/Restore on the builder as needed. bool mCallerDoesSaveRestore = false; #ifdef DEBUG bool mFinished = false; #endif // Whether PopAllShadows needs to be called bool mHasShadows = false; // Things used to push to webrender wr::DisplayListBuilder& mBuilder; wr::IpcResourceUpdateQueue* mResources; const layers::StackingContextHelper* mSc; layers::RenderRootStateManager* mManager; // Computed facts IntSize mSize; wr::LayoutRect mBoundsRect; AutoTArray mClipStack; bool mBackfaceVisible; wr::FontInstanceFlags mWRGlyphFlags = {0}; // The rest of this is dummy implementations of DrawTarget's API public: DrawTargetType GetType() const override { return DrawTargetType::SOFTWARE_RASTER; } BackendType GetBackendType() const override { return BackendType::WEBRENDER_TEXT; } bool IsRecording() const override { return true; } already_AddRefed Snapshot() override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); return nullptr; } already_AddRefed IntoLuminanceSource( LuminanceType aLuminanceType, float aOpacity) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); return nullptr; } void Flush() override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void DrawSurface(SourceSurface* aSurface, const Rect& aDest, const Rect& aSource, const DrawSurfaceOptions& aSurfOptions, const DrawOptions& aOptions) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void DrawFilter(FilterNode* aNode, const Rect& aSourceRect, const Point& aDestPoint, const DrawOptions& aOptions) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void DrawSurfaceWithShadow(SourceSurface* aSurface, const Point& aDest, const ShadowOptions& aShadow, CompositionOp aOperator) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void ClearRect(const Rect& aRect) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void CopySurface(SourceSurface* aSurface, const IntRect& aSourceRect, const IntPoint& aDestination) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void FillRect(const Rect& aRect, const Pattern& aPattern, const DrawOptions& aOptions = DrawOptions()) override { MOZ_RELEASE_ASSERT(aPattern.GetType() == PatternType::COLOR); if (!aRect.Intersects(GeckoClipRect().ToUnknownRect())) { return; } auto rect = wr::ToLayoutRect(LayoutDeviceRect::FromUnknownRect(aRect)); auto color = wr::ToColorF(static_cast(aPattern).mColor); mBuilder.PushRect(rect, ClipRect(), mBackfaceVisible, false, false, color); } void StrokeRect(const Rect& aRect, const Pattern& aPattern, const StrokeOptions& aStrokeOptions, const DrawOptions& aOptions) override { MOZ_RELEASE_ASSERT(aPattern.GetType() == PatternType::COLOR && aStrokeOptions.mDashLength == 0); wr::LayoutSideOffsets widths = { aStrokeOptions.mLineWidth, aStrokeOptions.mLineWidth, aStrokeOptions.mLineWidth, aStrokeOptions.mLineWidth}; wr::ColorF color = wr::ToColorF(static_cast(aPattern).mColor); wr::BorderSide sides[4] = {{color, wr::BorderStyle::Solid}, {color, wr::BorderStyle::Solid}, {color, wr::BorderStyle::Solid}, {color, wr::BorderStyle::Solid}}; wr::BorderRadius radius = {{0, 0}, {0, 0}, {0, 0}, {0, 0}}; LayoutDeviceRect rect = LayoutDeviceRect::FromUnknownRect(aRect); rect.Inflate(aStrokeOptions.mLineWidth / 2); if (!rect.Intersects(GeckoClipRect())) { return; } wr::LayoutRect bounds = wr::ToLayoutRect(rect); mBuilder.PushBorder(bounds, ClipRect(), true, widths, Range(sides, 4), radius); } void StrokeLine(const Point& aStart, const Point& aEnd, const Pattern& aPattern, const StrokeOptions& aStrokeOptions, const DrawOptions& aOptions) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void Stroke(const Path* aPath, const Pattern& aPattern, const StrokeOptions& aStrokeOptions, const DrawOptions& aOptions) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void Fill(const Path* aPath, const Pattern& aPattern, const DrawOptions& aOptions) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void StrokeGlyphs(ScaledFont* aFont, const GlyphBuffer& aBuffer, const Pattern& aPattern, const StrokeOptions& aStrokeOptions, const DrawOptions& aOptions) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void Mask(const Pattern& aSource, const Pattern& aMask, const DrawOptions& aOptions) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void MaskSurface(const Pattern& aSource, SourceSurface* aMask, Point aOffset, const DrawOptions& aOptions) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } bool Draw3DTransformedSurface(SourceSurface* aSurface, const Matrix4x4& aMatrix) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void PushClip(const Path* aPath) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void PushDeviceSpaceClipRects(const IntRect* aRects, uint32_t aCount) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } void PushLayer(bool aOpaque, Float aOpacity, SourceSurface* aMask, const Matrix& aMaskTransform, const IntRect& aBounds, bool aCopyBackground) override { // Fine to pretend we do this } void PopLayer() override { // Fine to pretend we do this } already_AddRefed CreateSourceSurfaceFromData( unsigned char* aData, const IntSize& aSize, int32_t aStride, SurfaceFormat aFormat) const override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); return nullptr; } already_AddRefed OptimizeSourceSurface( SourceSurface* aSurface) const override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); return nullptr; } already_AddRefed CreateSourceSurfaceFromNativeSurface( const NativeSurface& aSurface) const override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); return nullptr; } already_AddRefed CreateSimilarDrawTarget( const IntSize& aSize, SurfaceFormat aFormat) const override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); return nullptr; } bool CanCreateSimilarDrawTarget(const IntSize& aSize, SurfaceFormat aFormat) const override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); return false; } virtual RefPtr CreateClippedDrawTarget( const Rect& aBounds, SurfaceFormat aFormat) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); return nullptr; } already_AddRefed CreatePathBuilder( FillRule aFillRule) const override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); return nullptr; } already_AddRefed CreateFilter(FilterType aType) override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); return nullptr; } already_AddRefed CreateGradientStops( GradientStop* aStops, uint32_t aNumStops, ExtendMode aExtendMode) const override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); return nullptr; } void DetachAllSnapshots() override { MOZ_CRASH("TextDrawTarget: Method shouldn't be called"); } }; } // namespace layout } // namespace mozilla #endif