diff options
author | Luboš Luňák <l.lunak@collabora.com> | 2021-11-11 14:01:55 +0100 |
---|---|---|
committer | Luboš Luňák <l.lunak@collabora.com> | 2021-11-16 10:38:54 +0100 |
commit | b5983dbe2c41f38e653201574cf20cd4bd76e950 (patch) | |
tree | a8254c2dc2b9ef2c7d91aa9dbf2b6994b3295645 | |
parent | 67669707e0c6c8a390d352e7060ad5862d727433 (diff) |
implement HiDPI support for Skia/Mac (tdf#144214)
The basic idea is the same as the 'aqua' backend, simply set up
a scaling matrix for all drawing. That will take care of the basic
drawing everything twice as large, which is twice the resolution.
And then blit this data to the window, which expects data this way.
Converting back from backing surface needs explicit coordinate
conversions, and when converting to a bitmap the bitmap needs
to be scaled down in order to appear normally sized. Fortunately
I've already implemented delayed scaling, which means that if
the bitmap is drawn later again without any modifications, no
data would be lost (to be done in a follow-up commit).
Unittests occassionally need special handling, as such scaling
down to bitmap not being smoothed, because they expect exact
color values.
Change-Id: Ieadf2c3693f7c9676c31c7394d46299addf7880c
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/125060
Tested-by: Jenkins
Reviewed-by: Luboš Luňák <l.lunak@collabora.com>
-rw-r--r-- | vcl/README.vars.md | 4 | ||||
-rw-r--r-- | vcl/backendtest/outputdevice/common.cxx | 2 | ||||
-rw-r--r-- | vcl/inc/skia/gdiimpl.hxx | 20 | ||||
-rw-r--r-- | vcl/inc/skia/osx/gdiimpl.hxx | 3 | ||||
-rw-r--r-- | vcl/inc/skia/utils.hxx | 61 | ||||
-rw-r--r-- | vcl/osx/salgdiutils.cxx | 5 | ||||
-rw-r--r-- | vcl/skia/gdiimpl.cxx | 235 | ||||
-rw-r--r-- | vcl/skia/osx/gdiimpl.cxx | 65 | ||||
-rw-r--r-- | vcl/skia/salbmp.cxx | 11 |
9 files changed, 312 insertions, 94 deletions
diff --git a/vcl/README.vars.md b/vcl/README.vars.md index cdf356f6a2e0..7e0c3c2db0ad 100644 --- a/vcl/README.vars.md +++ b/vcl/README.vars.md @@ -64,3 +64,7 @@ will be used to write the log under `instdir/uitest/`. ## Kf5 * `SAL_VCL_KF5_USE_QFONT` - use `QFont` for text rendering (default for qt5, but not kf5) + +## Mac + +* `SAL_FORCE_HIDPI_SCALING` - set to 2 to fake HiDPI drawing (useful for unittests, windows may draw only top-left 1/4 of the content scaled) diff --git a/vcl/backendtest/outputdevice/common.cxx b/vcl/backendtest/outputdevice/common.cxx index 21a32635ab85..80408fac70fe 100644 --- a/vcl/backendtest/outputdevice/common.cxx +++ b/vcl/backendtest/outputdevice/common.cxx @@ -1370,7 +1370,7 @@ TestResult OutputDeviceTestCommon::checkRadialGradient(Bitmap& bitmap) int nNumberOfErrors = 0; // The default VCL implementation is off-center in the direction to the top-left. // This means not all corners will be pure white => quirks. - checkValue(pAccess, 1, 1, COL_WHITE, nNumberOfQuirks, nNumberOfErrors, 255 / 10, 255 / 3); + checkValue(pAccess, 1, 1, COL_WHITE, nNumberOfQuirks, nNumberOfErrors, 255 / 10, 255 / 2); checkValue(pAccess, 1, 10, COL_WHITE, nNumberOfQuirks, nNumberOfErrors, 255 / 10, 255 / 5); checkValue(pAccess, 10, 1, COL_WHITE, nNumberOfQuirks, nNumberOfErrors, 255 / 10, 255 / 5); checkValue(pAccess, 10, 10, COL_WHITE, nNumberOfQuirks, nNumberOfErrors, 255 / 10, 255 / 5); diff --git a/vcl/inc/skia/gdiimpl.hxx b/vcl/inc/skia/gdiimpl.hxx index 03a4d5cf0413..70bbcf5c4dcc 100644 --- a/vcl/inc/skia/gdiimpl.hxx +++ b/vcl/inc/skia/gdiimpl.hxx @@ -239,6 +239,7 @@ protected: void privateDrawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, double nTransparency, bool blockAA = false); + void privateCopyBits(const SalTwoRect& rPosAry, SkiaSalGraphicsImpl* src); void setProvider(SalGeometryProvider* provider) { mProvider = provider; } @@ -256,6 +257,8 @@ protected: int GetWidth() const { return mProvider ? mProvider->GetWidth() : 1; } // get the height of the device int GetHeight() const { return mProvider ? mProvider->GetHeight() : 1; } + // Get the global HiDPI scaling factor. + virtual int getWindowScaling() const; SkCanvas* getXorCanvas(); void applyXor(); @@ -277,6 +280,8 @@ protected: // and swapping to the screen is not _that_slow. mDirtyRect.join(addedRect); } + void setCanvasScalingAndClipping(); + void resetCanvasScalingAndClipping(); static void setCanvasClipRegion(SkCanvas* canvas, const vcl::Region& region); sk_sp<SkImage> mergeCacheBitmaps(const SkiaSalBitmap& bitmap, const SkiaSalBitmap* alphaBitmap, const Size targetSize); @@ -305,9 +310,12 @@ protected: if (graphics == nullptr) return stream << "(null)"; // O - offscreen, G - GPU-based, R - raster - return stream << static_cast<const void*>(graphics) << " " - << Size(graphics->GetWidth(), graphics->GetHeight()) - << (graphics->isGPU() ? "G" : "R") << (graphics->isOffscreen() ? "O" : ""); + stream << static_cast<const void*>(graphics) << " " + << Size(graphics->GetWidth(), graphics->GetHeight()); + if (graphics->mScaling != 1) + stream << "*" << graphics->mScaling; + stream << (graphics->isGPU() ? "G" : "R") << (graphics->isOffscreen() ? "O" : ""); + return stream; } SalGraphics& mParent; @@ -318,14 +326,15 @@ protected: // Note that mSurface may be a proxy surface and not the one from the window context. std::unique_ptr<sk_app::WindowContext> mWindowContext; bool mIsGPU; // whether the surface is GPU-backed - SkIRect mDirtyRect; // the area that has been changed since the last performFlush() + // Note that we generally use VCL coordinates, which is not mSurface coordinates if mScaling!=1. + SkIRect mDirtyRect; // The area that has been changed since the last performFlush(). vcl::Region mClipRegion; + SkRegion mXorRegion; // The area that needs updating for the xor operation. Color mLineColor; Color mFillColor; bool mXorMode; SkBitmap mXorBitmap; std::unique_ptr<SkCanvas> mXorCanvas; - SkRegion mXorRegion; // the area that needs updating for the xor operation std::unique_ptr<SkiaFlushIdle> mFlush; // Info about pending polygons to draw (we try to merge adjacent polygons into one). struct LastPolyPolygonInfo @@ -336,6 +345,7 @@ protected: }; LastPolyPolygonInfo mLastPolyPolygonInfo; int mPendingOperationsToFlush; + int mScaling; // The scale factor for HiDPI screens. }; #endif diff --git a/vcl/inc/skia/osx/gdiimpl.hxx b/vcl/inc/skia/osx/gdiimpl.hxx index c4892ab45b43..42a8257f8b8f 100644 --- a/vcl/inc/skia/osx/gdiimpl.hxx +++ b/vcl/inc/skia/osx/gdiimpl.hxx @@ -43,6 +43,9 @@ public: virtual void Flush() override; virtual void Flush(const tools::Rectangle&) override; +protected: + virtual int getWindowScaling() const override; + private: virtual void createWindowSurfaceInternal(bool forceRaster = false) override; virtual void flushSurfaceToWindowContext() override; diff --git a/vcl/inc/skia/utils.hxx b/vcl/inc/skia/utils.hxx index ba479c58f234..ed404f7cc3eb 100644 --- a/vcl/inc/skia/utils.hxx +++ b/vcl/inc/skia/utils.hxx @@ -33,6 +33,8 @@ #include <tools/sk_app/WindowContext.h> #include <postmac.h> +#include <string_view> + namespace SkiaHelper { // Get the one shared GrDirectContext instance. @@ -90,6 +92,17 @@ VCL_DLLPUBLIC const SkSurfaceProps* surfaceProps(); // Set pixel geometry to be used by SkSurfaceProps. VCL_DLLPUBLIC void setPixelGeometry(SkPixelGeometry pixelGeometry); +inline bool isUnitTestRunning(const char* name = nullptr) +{ + if (name == nullptr) + { + static const char* const testname = getenv("LO_TESTNAME"); + return testname != nullptr; + } + const char* const testname = getenv("LO_TESTNAME"); + return testname != nullptr && std::string_view(name) == testname; +} + // Normal scaling algorithms have a poor quality when downscaling a lot. // https://bugs.chromium.org/p/skia/issues/detail?id=11810 suggests to use mipmaps // in such a case, which is annoying to do explicitly instead of Skia deciding which @@ -98,11 +111,14 @@ VCL_DLLPUBLIC void setPixelGeometry(SkPixelGeometry pixelGeometry); // Anything scaled down at least this ratio will use linear+mipmaps. constexpr int downscaleRatioThreshold = 4; -inline SkSamplingOptions makeSamplingOptions(BmpScaleFlag scaling, const SkMatrix& matrix) +inline SkSamplingOptions makeSamplingOptions(BmpScaleFlag scalingType, SkMatrix matrix, + int scalingFactor) { - switch (scaling) + switch (scalingType) { case BmpScaleFlag::BestQuality: + if (scalingFactor != 1) + matrix.postScale(scalingFactor, scalingFactor); if (matrix.getScaleX() <= 1.0 / downscaleRatioThreshold || matrix.getScaleY() <= 1.0 / downscaleRatioThreshold) return SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kLinear); @@ -110,6 +126,7 @@ inline SkSamplingOptions makeSamplingOptions(BmpScaleFlag scaling, const SkMatri case BmpScaleFlag::Default: return SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNone); case BmpScaleFlag::Fast: + case BmpScaleFlag::NearestNeighbor: return SkSamplingOptions(SkFilterMode::kNearest, SkMipmapMode::kNone); default: assert(false); @@ -117,12 +134,14 @@ inline SkSamplingOptions makeSamplingOptions(BmpScaleFlag scaling, const SkMatri } } -inline SkSamplingOptions makeSamplingOptions(BmpScaleFlag scaling, const Size& srcSize, - const Size& destSize) +inline SkSamplingOptions makeSamplingOptions(BmpScaleFlag scalingType, const Size& srcSize, + Size destSize, int scalingFactor) { - switch (scaling) + switch (scalingType) { case BmpScaleFlag::BestQuality: + if (scalingFactor != 1) + destSize *= scalingFactor; if (srcSize.Width() / destSize.Width() >= downscaleRatioThreshold || srcSize.Height() / destSize.Height() >= downscaleRatioThreshold) return SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kLinear); @@ -130,6 +149,7 @@ inline SkSamplingOptions makeSamplingOptions(BmpScaleFlag scaling, const Size& s case BmpScaleFlag::Default: return SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNone); case BmpScaleFlag::Fast: + case BmpScaleFlag::NearestNeighbor: return SkSamplingOptions(SkFilterMode::kNearest, SkMipmapMode::kNone); default: assert(false); @@ -137,18 +157,41 @@ inline SkSamplingOptions makeSamplingOptions(BmpScaleFlag scaling, const Size& s } } -inline SkSamplingOptions makeSamplingOptions(const SalTwoRect& rPosAry) +inline SkSamplingOptions makeSamplingOptions(const SalTwoRect& rPosAry, int scalingFactor, + int srcScalingFactor = 1) { - if (rPosAry.mnSrcWidth != rPosAry.mnDestWidth || rPosAry.mnSrcHeight != rPosAry.mnDestHeight) + // If there will be scaling, make it smooth, but not in unittests, as those often + // require exact color values and would be confused by this. + if (isUnitTestRunning()) + return SkSamplingOptions(); // none + Size srcSize(rPosAry.mnSrcWidth, rPosAry.mnSrcHeight); + Size destSize(rPosAry.mnDestWidth, rPosAry.mnDestHeight); + if (scalingFactor != 1) + destSize *= scalingFactor; + if (srcScalingFactor != 1) + srcSize *= srcScalingFactor; + if (srcSize != destSize) { - if (rPosAry.mnSrcWidth / rPosAry.mnDestWidth >= downscaleRatioThreshold - || rPosAry.mnSrcHeight / rPosAry.mnDestHeight >= downscaleRatioThreshold) + if (srcSize.Width() / destSize.Width() >= downscaleRatioThreshold + || srcSize.Height() / destSize.Height() >= downscaleRatioThreshold) return SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kLinear); return SkSamplingOptions(SkCubicResampler::Mitchell()); // best } return SkSamplingOptions(); // none } +inline SkRect scaleRect(const SkRect& rect, int scaling) +{ + return SkRect::MakeXYWH(rect.x() * scaling, rect.y() * scaling, rect.width() * scaling, + rect.height() * scaling); +} + +inline SkIRect scaleRect(const SkIRect& rect, int scaling) +{ + return SkIRect::MakeXYWH(rect.x() * scaling, rect.y() * scaling, rect.width() * scaling, + rect.height() * scaling); +} + #ifdef DBG_UTIL void prefillSurface(const sk_sp<SkSurface>& surface); VCL_DLLPUBLIC void dump(const SkBitmap& bitmap, const char* file); diff --git a/vcl/osx/salgdiutils.cxx b/vcl/osx/salgdiutils.cxx index da1d3ab2138a..7b088864d111 100644 --- a/vcl/osx/salgdiutils.cxx +++ b/vcl/osx/salgdiutils.cxx @@ -64,6 +64,11 @@ float getWindowScaling() } bWindowScaling = true; } + if( const char* env = getenv("SAL_FORCE_HIDPI_SCALING")) + { + fWindowScale = atof(env); + bWindowScaling = true; + } } return fWindowScale; } diff --git a/vcl/skia/gdiimpl.cxx b/vcl/skia/gdiimpl.cxx index fd86928c24c9..ebd1389c5970 100644 --- a/vcl/skia/gdiimpl.cxx +++ b/vcl/skia/gdiimpl.cxx @@ -286,6 +286,7 @@ SkiaSalGraphicsImpl::SkiaSalGraphicsImpl(SalGraphics& rParent, SalGeometryProvid , mXorMode(false) , mFlush(new SkiaFlushIdle(this)) , mPendingOperationsToFlush(0) + , mScaling(1) { } @@ -304,9 +305,9 @@ void SkiaSalGraphicsImpl::createSurface() createOffscreenSurface(); else createWindowSurface(); - mSurface->getCanvas()->save(); // see SetClipRegion() mClipRegion = vcl::Region(tools::Rectangle(0, 0, GetWidth(), GetHeight())); mDirtyRect = SkIRect::MakeWH(GetWidth(), GetHeight()); + setCanvasScalingAndClipping(); // We don't want to be swapping before we've painted. mFlush->Stop(); @@ -362,7 +363,11 @@ void SkiaSalGraphicsImpl::createOffscreenSurface() // HACK: See isOffscreen(). int width = std::max(1, GetWidth()); int height = std::max(1, GetHeight()); - mSurface = createSkSurface(width, height); + // We need to use window scaling even for offscreen surfaces, because the common usage is rendering something + // into an offscreen surface and then copy it to a window, so without scaling here the result would be originally + // drawn without scaling and only upscaled when drawing to a window. + mScaling = getWindowScaling(); + mSurface = createSkSurface(width * mScaling, height * mScaling); assert(mSurface); mIsGPU = mSurface->getCanvas()->recordingContext() != nullptr; } @@ -373,9 +378,9 @@ void SkiaSalGraphicsImpl::destroySurface() if (mSurface) { // check setClipRegion() invariant - assert(mSurface->getCanvas()->getSaveCount() == 2); + assert(mSurface->getCanvas()->getSaveCount() == 3); // if this fails, something forgot to use SkAutoCanvasRestore - assert(mSurface->getCanvas()->getTotalMatrix().isIdentity()); + assert(mSurface->getCanvas()->getTotalMatrix() == SkMatrix::Scale(mScaling, mScaling)); } // If we use e.g. Vulkan, we must destroy the surface before the context, // otherwise destroying the surface will reference the context. This is @@ -389,6 +394,7 @@ void SkiaSalGraphicsImpl::destroySurface() mSurface.reset(); mWindowContext.reset(); mIsGPU = false; + mScaling = 1; } void SkiaSalGraphicsImpl::performFlush() @@ -415,6 +421,8 @@ void SkiaSalGraphicsImpl::flushSurfaceToWindowContext() assert(isGPU()); // Raster should always draw directly to backbuffer to save copying SkPaint paint; paint.setBlendMode(SkBlendMode::kSrc); // copy as is + // We ignore mDirtyRect here, and mSurface already is in screenSurface coordinates, + // so no transformation needed. screenSurface->getCanvas()->drawImage(makeCheckedImageSnapshot(mSurface), 0, 0, SkSamplingOptions(), &paint); screenSurface->flushAndSubmit(); // Otherwise the window is not drawn sometimes. @@ -427,7 +435,10 @@ void SkiaSalGraphicsImpl::flushSurfaceToWindowContext() // getBackbufferSurface() repeatedly. Using our own surface would duplicate // memory and cost time copying pixels around. assert(!isGPU()); - mWindowContext->swapBuffers(&mDirtyRect); + SkIRect dirtyRect = mDirtyRect; + if (mScaling != 1) // Adjust to mSurface coordinates if needed. + dirtyRect = scaleRect(dirtyRect, mScaling); + mWindowContext->swapBuffers(&dirtyRect); } } @@ -496,7 +507,8 @@ void SkiaSalGraphicsImpl::checkSurface() SAL_INFO("vcl.skia.trace", "create(" << this << "): " << Size(mSurface->width(), mSurface->height())); } - else if (GetWidth() != mSurface->width() || GetHeight() != mSurface->height()) + else if (GetWidth() * mScaling != mSurface->width() + || GetHeight() * mScaling != mSurface->height()) { if (!avoidRecreateByResize()) { @@ -521,7 +533,11 @@ void SkiaSalGraphicsImpl::checkSurface() { SkPaint paint; paint.setBlendMode(SkBlendMode::kSrc); // copy as is + // Scaling by current mScaling is active, undo that. We assume that the scaling + // does not change. + resetCanvasScalingAndClipping(); mSurface->getCanvas()->drawImage(snapshot, 0, 0, SkSamplingOptions(), &paint); + setCanvasScalingAndClipping(); } SAL_INFO("vcl.skia.trace", "recreate(" << this << "): old " << oldSize << " new " << Size(mSurface->width(), mSurface->height()) @@ -550,6 +566,36 @@ void SkiaSalGraphicsImpl::flushDrawing() mPendingOperationsToFlush = 0; } +void SkiaSalGraphicsImpl::setCanvasScalingAndClipping() +{ + SkCanvas* canvas = mSurface->getCanvas(); + assert(canvas->getSaveCount() == 1); + // If HiDPI scaling is active, simply set a scaling matrix for the canvas. This means + // that all painting can use VCL coordinates and they'll be automatically translated to mSurface + // scaled coordinates. If that is not wanted, the scale() state needs to be temporarily unset. + // State such as mDirtyRect and mXorRegion is not scaled, the scaling matrix applies to clipping too, + // and the rest needs to be handled explicitly. + // When reading mSurface contents there's no automatic scaling and it needs to be handled explicitly. + canvas->save(); // keep the original state without any scaling + canvas->scale(mScaling, mScaling); + + // SkCanvas::clipRegion() can only further reduce the clip region, + // but we need to set the given region, which may extend it. + // So handle that by always having the full clip region saved on the stack + // and always go back to that. SkCanvas::restore() only affects the clip + // and the matrix. + canvas->save(); // keep scaled state without clipping + setCanvasClipRegion(canvas, mClipRegion); +} + +void SkiaSalGraphicsImpl::resetCanvasScalingAndClipping() +{ + SkCanvas* canvas = mSurface->getCanvas(); + assert(canvas->getSaveCount() == 3); + canvas->restore(); // undo clipping + canvas->restore(); // undo scaling +} + bool SkiaSalGraphicsImpl::setClipRegion(const vcl::Region& region) { if (mClipRegion == region) @@ -560,13 +606,8 @@ bool SkiaSalGraphicsImpl::setClipRegion(const vcl::Region& region) mClipRegion = region; SAL_INFO("vcl.skia.trace", "setclipregion(" << this << "): " << region); SkCanvas* canvas = mSurface->getCanvas(); - // SkCanvas::clipRegion() can only further reduce the clip region, - // but we need to set the given region, which may extend it. - // So handle that by always having the full clip region saved on the stack - // and always go back to that. SkCanvas::restore() only affects the clip - // and the matrix. - assert(canvas->getSaveCount() == 2); // = there is just one save() - canvas->restore(); + assert(canvas->getSaveCount() == 3); + canvas->restore(); // undo previous clip state, see setCanvasScalingAndClipping() canvas->save(); setCanvasClipRegion(canvas, region); return true; @@ -651,6 +692,8 @@ SkCanvas* SkiaSalGraphicsImpl::getXorCanvas() abort(); mXorBitmap.eraseARGB(0, 0, 0, 0); mXorCanvas = std::make_unique<SkCanvas>(mXorBitmap); + if (mScaling != 1) + mXorCanvas->scale(mScaling, mScaling); setCanvasClipRegion(mXorCanvas.get(), mClipRegion); } return mXorCanvas.get(); @@ -663,6 +706,14 @@ void SkiaSalGraphicsImpl::applyXor() // in each operation by extending mXorRegion with the area that should be // updated. assert(mXorMode); + if (mScaling != 1 && !mXorRegion.isEmpty()) + { + // Scale mXorRegion to mSurface coordinates if needed. + std::vector<SkIRect> rects; + for (SkRegion::Iterator it(mXorRegion); !it.done(); it.next()) + rects.push_back(scaleRect(it.rect(), mScaling)); + mXorRegion.setRects(rects.data(), rects.size()); + } if (!mSurface || !mXorCanvas || !mXorRegion.op(SkIRect::MakeXYWH(0, 0, mSurface->width(), mSurface->height()), SkRegion::kIntersect_Op)) @@ -709,8 +760,11 @@ void SkiaSalGraphicsImpl::applyXor() } surfaceBitmap.notifyPixelsChanged(); surfaceBitmap.setImmutable(); + // Copy without any clipping or scaling. + resetCanvasScalingAndClipping(); mSurface->getCanvas()->drawImageRect(surfaceBitmap.asImage(), area, area, SkSamplingOptions(), &paint, SkCanvas::kFast_SrcRectConstraint); + setCanvasScalingAndClipping(); mXorCanvas.reset(); mXorBitmap.reset(); mXorRegion.setEmpty(); @@ -766,6 +820,13 @@ void SkiaSalGraphicsImpl::drawPixel(tools::Long nX, tools::Long nY, Color nColor paint.setColor(toSkColor(nColor)); // Apparently drawPixel() is actually expected to set the pixel and not draw it. paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha + if (mScaling != 1 && isUnitTestRunning()) + { + // On HiDPI displays, draw a square on the entire non-hidpi "pixel" when running unittests, + // since tests often require precise pixel drawing. + paint.setStrokeWidth(1); // this will be scaled by mScaling + paint.setStrokeCap(SkPaint::kSquare_Cap); + } getDrawCanvas()->drawPoint(toSkX(nX), toSkY(nY), paint); postDraw(); } @@ -782,6 +843,13 @@ void SkiaSalGraphicsImpl::drawLine(tools::Long nX1, tools::Long nY1, tools::Long SkPaint paint; paint.setColor(toSkColor(mLineColor)); paint.setAntiAlias(mParent.getAntiAlias()); + if (mScaling != 1 && isUnitTestRunning()) + { + // On HiDPI displays, do not draw hairlines, draw 1-pixel wide lines in order to avoid + // smoothing that would confuse unittests. + paint.setStrokeWidth(1); // this will be scaled by mScaling + paint.setStrokeCap(SkPaint::kSquare_Cap); + } getDrawCanvas()->drawLine(toSkX(nX1), toSkY(nY1), toSkX(nX2), toSkY(nY2), paint); postDraw(); } @@ -812,12 +880,20 @@ void SkiaSalGraphicsImpl::privateDrawAlphaRect(tools::Long nX, tools::Long nY, t { paint.setColor(toSkColorWithTransparency(mLineColor, fTransparency)); paint.setStyle(SkPaint::kStroke_Style); + if (mScaling != 1 && isUnitTestRunning()) + { + // On HiDPI displays, do not draw just a harline but instead a full-width "pixel" when running unittests, + // since tests often require precise pixel drawing. + paint.setStrokeWidth(1); // this will be scaled by mScaling + paint.setStrokeCap(SkPaint::kSquare_Cap); + } // The obnoxious "-1 DrawRect()" hack that I don't understand the purpose of (and I'm not sure // if anybody does), but without it some cases do not work. The max() is needed because Skia // will not draw anything if width or height is 0. - canvas->drawIRect(SkIRect::MakeXYWH(nX, nY, std::max(tools::Long(1), nWidth - 1), - std::max(tools::Long(1), nHeight - 1)), - paint); + canvas->drawRect(SkRect::MakeXYWH(toSkX(nX), toSkY(nY), + std::max(tools::Long(1), nWidth - 1), + std::max(tools::Long(1), nHeight - 1)), + paint); } postDraw(); } @@ -1096,6 +1172,10 @@ bool SkiaSalGraphicsImpl::drawPolyLine(const basegfx::B2DHomMatrix& rObjectToDev // Adjust line width for object-to-device scale. fLineWidth = (rObjectToDevice * basegfx::B2DVector(fLineWidth, 0)).getLength(); + // On HiDPI displays, do not draw hairlines, draw 1-pixel wide lines in order to avoid + // smoothing that would confuse unittests. + if (fLineWidth == 0 && mScaling != 1 && isUnitTestRunning()) + fLineWidth = 1; // this will be scaled by mScaling // Transform to DeviceCoordinates, get DeviceLineWidth, execute PixelSnapHairline basegfx::B2DPolygon aPolyLine(rPolyLine); @@ -1223,15 +1303,9 @@ void SkiaSalGraphicsImpl::copyArea(tools::Long nDestX, tools::Long nDestY, tools SAL_INFO("vcl.skia.trace", "copyarea(" << this << "): " << Point(nSrcX, nSrcY) << "->" << SkIRect::MakeXYWH(nDestX, nDestY, nSrcWidth, nSrcHeight)); - assert(!mXorMode); - addUpdateRegion(SkRect::MakeXYWH(nDestX, nDestY, nSrcWidth, nSrcHeight)); // Using SkSurface::draw() should be more efficient, but it's too buggy. - SkPaint paint; - paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha - getDrawCanvas()->drawImageRect(makeCheckedImageSnapshot(mSurface), - SkRect::MakeXYWH(nSrcX, nSrcY, nSrcWidth, nSrcHeight), - SkRect::MakeXYWH(nDestX, nDestY, nSrcWidth, nSrcHeight), - SkSamplingOptions(), &paint, SkCanvas::kFast_SrcRectConstraint); + SalTwoRect rPosAry(nSrcX, nSrcY, nSrcWidth, nSrcHeight, nDestX, nDestY, nSrcWidth, nSrcHeight); + privateCopyBits(rPosAry, this); postDraw(); } @@ -1251,9 +1325,6 @@ void SkiaSalGraphicsImpl::copyBits(const SalTwoRect& rPosAry, SalGraphics* pSrcG src = this; assert(!mXorMode); } - assert(!mXorMode); - addUpdateRegion(SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, - rPosAry.mnDestHeight)); auto srcDebug = [&]() -> std::string { if (src == this) return "(self)"; @@ -1265,16 +1336,28 @@ void SkiaSalGraphicsImpl::copyBits(const SalTwoRect& rPosAry, SalGraphics* pSrcG } }; SAL_INFO("vcl.skia.trace", "copybits(" << this << "): " << srcDebug() << ": " << rPosAry); + privateCopyBits(rPosAry, src); + postDraw(); +} + +void SkiaSalGraphicsImpl::privateCopyBits(const SalTwoRect& rPosAry, SkiaSalGraphicsImpl* src) +{ + assert(!mXorMode); + addUpdateRegion(SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, + rPosAry.mnDestHeight)); SkPaint paint; paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + SkRect srcRect + = SkRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight); + SkRect destRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, + rPosAry.mnDestHeight); + // Scaling for source coordinates must be done manually. + if (src->mScaling != 1) + srcRect = scaleRect(srcRect, src->mScaling); // Do not use makeImageSnapshot(rect), as that one may make a needless data copy. - getDrawCanvas()->drawImageRect( - makeCheckedImageSnapshot(src->mSurface), - SkRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight), - SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, - rPosAry.mnDestHeight), - makeSamplingOptions(rPosAry), &paint, SkCanvas::kFast_SrcRectConstraint); - postDraw(); + getDrawCanvas()->drawImageRect(makeCheckedImageSnapshot(src->mSurface), srcRect, destRect, + makeSamplingOptions(rPosAry, mScaling, src->mScaling), &paint, + SkCanvas::kFast_SrcRectConstraint); } bool SkiaSalGraphicsImpl::blendBitmap(const SalTwoRect& rPosAry, const SalBitmap& rBitmap) @@ -1335,7 +1418,7 @@ bool SkiaSalGraphicsImpl::blendAlphaBitmap(const SalTwoRect& rPosAry, // "result_alpha = 1.0 - (1.0 - floor(alpha)) * mask". // See also blendBitmap(). - SkSamplingOptions samplingOptions = makeSamplingOptions(rPosAry); + SkSamplingOptions samplingOptions = makeSamplingOptions(rPosAry, mScaling); // First do the "( 1 - alpha ) * mask" // (no idea how to do "floor", but hopefully not needed in practice). sk_sp<SkShader> shaderAlpha @@ -1370,10 +1453,11 @@ void SkiaSalGraphicsImpl::drawMask(const SalTwoRect& rPosAry, const SalBitmap& r { assert(dynamic_cast<const SkiaSalBitmap*>(&rSalBitmap)); const SkiaSalBitmap& skiaBitmap = static_cast<const SkiaSalBitmap&>(rSalBitmap); - drawShader(rPosAry, - SkShaders::Blend(SkBlendMode::kDstOut, // VCL alpha is one-minus-alpha. - SkShaders::Color(toSkColor(nMaskColor)), - skiaBitmap.GetAlphaSkShader(makeSamplingOptions(rPosAry)))); + drawShader( + rPosAry, + SkShaders::Blend(SkBlendMode::kDstOut, // VCL alpha is one-minus-alpha. + SkShaders::Color(toSkColor(nMaskColor)), + skiaBitmap.GetAlphaSkShader(makeSamplingOptions(rPosAry, mScaling)))); } std::shared_ptr<SalBitmap> SkiaSalGraphicsImpl::getBitmap(tools::Long nX, tools::Long nY, @@ -1387,9 +1471,32 @@ std::shared_ptr<SalBitmap> SkiaSalGraphicsImpl::getBitmap(tools::Long nX, tools: // TODO makeImageSnapshot(rect) may copy the data, which may be a waste if this is used // e.g. for VirtualDevice's lame alpha blending, in which case the image will eventually end up // in blendAlphaBitmap(), where we could simply use the proper rect of the image. - sk_sp<SkImage> image - = makeCheckedImageSnapshot(mSurface, SkIRect::MakeXYWH(nX, nY, nWidth, nHeight)); - return std::make_shared<SkiaSalBitmap>(image); + sk_sp<SkImage> image = makeCheckedImageSnapshot( + mSurface, scaleRect(SkIRect::MakeXYWH(nX, nY, nWidth, nHeight), mScaling)); + std::shared_ptr<SkiaSalBitmap> bitmap = std::make_shared<SkiaSalBitmap>(image); + // TODO: If the surface is scaled for HiDPI, the bitmap needs to be scaled down, otherwise + // it would have incorrect size from the API point of view. This could lead to loss of quality + // if the bitmap is drawn to another scaled surface. Since the bitmap scaling is done only + // on-demand, this state should be detected when drawing the bitmap and the scaling + // should be ignored. + if (mScaling != 1) + { + if (!isUnitTestRunning()) + bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, BmpScaleFlag::BestQuality); + else + { + // Some tests require exact pixel values and would be confused by smooth-scaling. + // And some draw something smooth and not smooth-scaling there would break the checks. + if (isUnitTestRunning("BackendTest__testDrawHaflEllipseAAWithPolyLineB2D_") + || isUnitTestRunning("BackendTest__testDrawRectAAWithLine_")) + { + bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, BmpScaleFlag::BestQuality); + } + else + bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, BmpScaleFlag::NearestNeighbor); + } + } + return bitmap; } Color SkiaSalGraphicsImpl::getPixel(tools::Long nX, tools::Long nY) @@ -1400,11 +1507,11 @@ Color SkiaSalGraphicsImpl::getPixel(tools::Long nX, tools::Long nY) flushDrawing(); // This is presumably slow, but getPixel() should be generally used only by unit tests. SkBitmap bitmap; - if (!bitmap.tryAllocN32Pixels(GetWidth(), GetHeight())) + if (!bitmap.tryAllocN32Pixels(mSurface->width(), mSurface->height())) abort(); if (!mSurface->readPixels(bitmap, 0, 0)) abort(); - return fromSkColor(bitmap.getColor(nX, nY)); + return fromSkColor(bitmap.getColor(nX * mScaling, nY * mScaling)); } void SkiaSalGraphicsImpl::invert(basegfx::B2DPolygon const& rPoly, SalInvert eFlags) @@ -1499,6 +1606,7 @@ sk_sp<SkImage> SkiaSalGraphicsImpl::mergeCacheBitmaps(const SkiaSalBitmap& bitma const SkiaSalBitmap* alphaBitmap, const Size targetSize) { + // TODO This should take into account mScaling!=1, and callers should use that too. sk_sp<SkImage> image; if (targetSize.IsEmpty()) return image; @@ -1574,7 +1682,7 @@ sk_sp<SkImage> SkiaSalGraphicsImpl::mergeCacheBitmaps(const SkiaSalBitmap& bitma matrix.set(SkMatrix::kMScaleX, 1.0 * targetSize.Width() / bitmap.GetSize().Width()); matrix.set(SkMatrix::kMScaleY, 1.0 * targetSize.Height() / bitmap.GetSize().Height()); canvas->concat(matrix); - samplingOptions = makeSamplingOptions(BmpScaleFlag::BestQuality, matrix); + samplingOptions = makeSamplingOptions(BmpScaleFlag::BestQuality, matrix, 1); } if (alphaBitmap != nullptr) { @@ -1625,11 +1733,11 @@ bool SkiaSalGraphicsImpl::drawAlphaBitmap(const SalTwoRect& rPosAry, const SalBi else if (rSkiaAlphaBitmap.IsFullyOpaqueAsAlpha()) // alpha can be ignored drawBitmap(rPosAry, rSkiaSourceBitmap); else - drawShader( - rPosAry, - SkShaders::Blend(SkBlendMode::kDstOut, // VCL alpha is one-minus-alpha. - rSkiaSourceBitmap.GetSkShader(makeSamplingOptions(rPosAry)), - rSkiaAlphaBitmap.GetAlphaSkShader(makeSamplingOptions(rPosAry)))); + drawShader(rPosAry, + SkShaders::Blend( + SkBlendMode::kDstOut, // VCL alpha is one-minus-alpha. + rSkiaSourceBitmap.GetSkShader(makeSamplingOptions(rPosAry, mScaling)), + rSkiaAlphaBitmap.GetAlphaSkShader(makeSamplingOptions(rPosAry, mScaling)))); return true; } @@ -1638,7 +1746,7 @@ void SkiaSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SkiaSalBit { if (bitmap.PreferSkShader()) { - drawShader(rPosAry, bitmap.GetSkShader(makeSamplingOptions(rPosAry)), blendMode); + drawShader(rPosAry, bitmap.GetSkShader(makeSamplingOptions(rPosAry, mScaling)), blendMode); return; } // Use mergeCacheBitmaps(), which may decide to cache the result, avoiding repeated @@ -1678,7 +1786,7 @@ void SkiaSalGraphicsImpl::drawImage(const SalTwoRect& rPosAry, const sk_sp<SkIma "drawimage(" << this << "): " << rPosAry << ":" << SkBlendMode_Name(eBlendMode)); addUpdateRegion(aDestinationRect); getDrawCanvas()->drawImageRect(aImage, aSourceRect, aDestinationRect, - makeSamplingOptions(rPosAry), &aPaint, + makeSamplingOptions(rPosAry, mScaling), &aPaint, SkCanvas::kFast_SrcRectConstraint); ++mPendingOperationsToFlush; // tdf#136369 postDraw(); @@ -1805,8 +1913,8 @@ bool SkiaSalGraphicsImpl::drawTransformedBitmap(const basegfx::B2DPoint& rNull, SkAutoCanvasRestore autoRestore(canvas, true); canvas->concat(matrix); SkSamplingOptions samplingOptions; - if (matrixNeedsHighQuality(matrix)) - samplingOptions = makeSamplingOptions(BmpScaleFlag::BestQuality, matrix); + if (matrixNeedsHighQuality(matrix) || (mScaling != 1 && !isUnitTestRunning())) + samplingOptions = makeSamplingOptions(BmpScaleFlag::BestQuality, matrix, mScaling); if (fAlpha == 1.0) canvas->drawImage(imageToDraw, 0, 0, samplingOptions); else @@ -1832,8 +1940,8 @@ bool SkiaSalGraphicsImpl::drawTransformedBitmap(const basegfx::B2DPoint& rNull, SkAutoCanvasRestore autoRestore(canvas, true); canvas->concat(matrix); SkSamplingOptions samplingOptions; - if (matrixNeedsHighQuality(matrix)) - samplingOptions = makeSamplingOptions(BmpScaleFlag::BestQuality, matrix); + if (matrixNeedsHighQuality(matrix) || (mScaling != 1 && !isUnitTestRunning())) + samplingOptions = makeSamplingOptions(BmpScaleFlag::BestQuality, matrix, mScaling); if (pSkiaAlphaBitmap) { SkPaint paint; @@ -2059,6 +2167,21 @@ bool SkiaSalGraphicsImpl::supportsOperation(OutDevSupportType eType) const } } +static int getScaling() +{ + // It makes sense to support the debugging flag on all platforms + // for unittests purpose, even if the actual windows cannot do it. + if (const char* env = getenv("SAL_FORCE_HIDPI_SCALING")) + return atoi(env); + return 1; +} + +int SkiaSalGraphicsImpl::getWindowScaling() const +{ + static const int scaling = getScaling(); + return scaling; +} + #ifdef DBG_UTIL void SkiaSalGraphicsImpl::dump(const char* file) const { diff --git a/vcl/skia/osx/gdiimpl.cxx b/vcl/skia/osx/gdiimpl.cxx index f4e2a63bee80..73e5e09d20d0 100644 --- a/vcl/skia/osx/gdiimpl.cxx +++ b/vcl/skia/osx/gdiimpl.cxx @@ -58,13 +58,14 @@ void AquaSkiaSalGraphicsImpl::createWindowSurfaceInternal(bool forceRaster) displayParams.fColorType = kN32_SkColorType; sk_app::window_context_factory::MacWindowInfo macWindow; macWindow.fMainView = mrShared.mpFrame->mpNSView; + mScaling = getWindowScaling(); RenderMethod renderMethod = forceRaster ? RenderRaster : renderMethodToUse(); switch (renderMethod) { case RenderRaster: // RasterWindowContext_mac uses OpenGL internally, which we don't want, // so use our own surface and do blitting to the screen ourselves. - mSurface = createSkSurface(GetWidth(), GetHeight()); + mSurface = createSkSurface(GetWidth() * mScaling, GetHeight() * mScaling); break; case RenderMetal: mWindowContext @@ -74,7 +75,7 @@ void AquaSkiaSalGraphicsImpl::createWindowSurfaceInternal(bool forceRaster) // it appears that Metal surfaces cannot be read from, which would break things // like copyArea(). if (mWindowContext) - mSurface = createSkSurface(GetWidth(), GetHeight()); + mSurface = createSkSurface(GetWidth() * mScaling, GetHeight() * mScaling); break; case RenderVulkan: abort(); @@ -82,6 +83,12 @@ void AquaSkiaSalGraphicsImpl::createWindowSurfaceInternal(bool forceRaster) } } +int AquaSkiaSalGraphicsImpl::getWindowScaling() const +{ + // The system function returns float, but only integer multiples realistically make sense. + return sal::aqua::getWindowScaling(); +} + void AquaSkiaSalGraphicsImpl::Flush() { performFlush(); } void AquaSkiaSalGraphicsImpl::Flush(const tools::Rectangle&) { performFlush(); } @@ -125,36 +132,54 @@ void AquaSkiaSalGraphicsImpl::flushSurfaceToScreenCG() SkPixmap pixmap; if (!image->peekPixels(&pixmap)) abort(); + // If window scaling, then mDirtyRect is in VCL coordinates, mSurface has screen size (=points,HiDPI), + // maContextHolder has screen size but a scale matrix set so its inputs are in VCL coordinates (see + // its setup in AquaSharedAttributes::checkContext()). // This creates the bitmap context from the cropped part, writable_addr32() will get // the first pixel of mDirtyRect.topLeft(), and using pixmap.rowBytes() ensures the following // pixel lines will be read from correct positions. CGContextRef context = CGBitmapContextCreate( - pixmap.writable_addr32(mDirtyRect.x(), mDirtyRect.y()), mDirtyRect.width(), - mDirtyRect.height(), 8, pixmap.rowBytes(), GetSalData()->mxRGBSpace, - toCGBitmapType(image->colorType(), image->alphaType())); - assert(context); // TODO + pixmap.writable_addr32(mDirtyRect.x() * mScaling, mDirtyRect.y() * mScaling), + mDirtyRect.width() * mScaling, mDirtyRect.height() * mScaling, 8, pixmap.rowBytes(), + GetSalData()->mxRGBSpace, toCGBitmapType(image->colorType(), image->alphaType())); + if (!context) + { + SAL_WARN("vcl.skia", "flushSurfaceToScreenGC(): Failed to allocate bitmap context"); + return; + } CGImageRef screenImage = CGBitmapContextCreateImage(context); - assert(screenImage); // TODO - if (mrShared.isFlipped()) + if (!screenImage) { - const CGRect screenRect - = CGRectMake(mDirtyRect.x(), GetHeight() - mDirtyRect.y() - mDirtyRect.height(), - mDirtyRect.width(), mDirtyRect.height()); - mrShared.maContextHolder.saveState(); - CGContextTranslateCTM(mrShared.maContextHolder.get(), 0, pixmap.height()); - CGContextScaleCTM(mrShared.maContextHolder.get(), 1, -1); - CGContextDrawImage(mrShared.maContextHolder.get(), screenRect, screenImage); - mrShared.maContextHolder.restoreState(); + CGContextRelease(context); + SAL_WARN("vcl.skia", "flushSurfaceToScreenGC(): Failed to allocate screen image"); + return; } - else + mrShared.maContextHolder.saveState(); + // Drawing to the actual window has scaling active, so use unscaled coordinates, the scaling matrix will scale them + // to the proper screen coordinates. Unless the scaling is fake for debugging, in which case scale them to draw + // at the scaled size. + int windowScaling = 1; + static const char* env = getenv("SAL_FORCE_HIDPI_SCALING"); + if (env != nullptr) + windowScaling = atoi(env); + CGRect drawRect + = CGRectMake(mDirtyRect.x() * windowScaling, mDirtyRect.y() * windowScaling, + mDirtyRect.width() * windowScaling, mDirtyRect.height() * windowScaling); + if (mrShared.isFlipped()) { - const CGRect screenRect - = CGRectMake(mDirtyRect.x(), mDirtyRect.y(), mDirtyRect.width(), mDirtyRect.height()); - CGContextDrawImage(mrShared.maContextHolder.get(), screenRect, screenImage); + // I don't understand why, but apparently it's needed to explicitly to flip the drawing, even though maContextHelper + // has this set up, so this unsets the flipping. + CGFloat invertedY = drawRect.origin.y + drawRect.size.height; + CGContextTranslateCTM(mrShared.maContextHolder.get(), 0, invertedY); + CGContextScaleCTM(mrShared.maContextHolder.get(), 1, -1); + drawRect.origin.y = 0; } + CGContextDrawImage(mrShared.maContextHolder.get(), drawRect, screenImage); + mrShared.maContextHolder.restoreState(); CGImageRelease(screenImage); CGContextRelease(context); + // This is also in VCL coordinates. mrShared.refreshRect(mDirtyRect.x(), mDirtyRect.y(), mDirtyRect.width(), mDirtyRect.height()); } diff --git a/vcl/skia/salbmp.cxx b/vcl/skia/salbmp.cxx index 31a369724259..c064f00ad565 100644 --- a/vcl/skia/salbmp.cxx +++ b/vcl/skia/salbmp.cxx @@ -422,6 +422,11 @@ bool SkiaSalBitmap::Scale(const double& rScaleX, const double& rScaleY, BmpScale case BmpScaleFlag::Fast: mScaleQuality = nScaleFlag; break; + case BmpScaleFlag::NearestNeighbor: + // We handle this the same way as Fast by mapping to Skia's nearest-neighbor, + // and it's needed for unittests (mScaling and testTdf132367()). + mScaleQuality = nScaleFlag; + break; case BmpScaleFlag::Default: if (mScaleQuality == BmpScaleFlag::BestQuality) mScaleQuality = nScaleFlag; @@ -781,7 +786,7 @@ const sk_sp<SkImage>& SkiaSalBitmap::GetSkImage() const paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha surface->getCanvas()->drawImageRect( mImage, SkRect::MakeWH(mSize.Width(), mSize.Height()), - makeSamplingOptions(mScaleQuality, imageSize(mImage), mSize), &paint); + makeSamplingOptions(mScaleQuality, imageSize(mImage), mSize, 1), &paint); SAL_INFO("vcl.skia.trace", "getskimage(" << this << "): image scaled " << Size(mImage->width(), mImage->height()) << "->" << mSize << ":" @@ -893,7 +898,7 @@ const sk_sp<SkImage>& SkiaSalBitmap::GetAlphaSkImage() const paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha surface->getCanvas()->drawImageRect( mImage, SkRect::MakeWH(mSize.Width(), mSize.Height()), - scaling ? makeSamplingOptions(mScaleQuality, imageSize(mImage), mSize) + scaling ? makeSamplingOptions(mScaleQuality, imageSize(mImage), mSize, 1) : SkSamplingOptions(), &paint); if (scaling) @@ -1149,7 +1154,7 @@ void SkiaSalBitmap::EnsureBitmapData() if (imageSize(mImage) != mSize) // pending scaling? { canvas.drawImageRect(mImage, SkRect::MakeWH(mSize.getWidth(), mSize.getHeight()), - makeSamplingOptions(mScaleQuality, imageSize(mImage), mSize), + makeSamplingOptions(mScaleQuality, imageSize(mImage), mSize, 1), &paint); SAL_INFO("vcl.skia.trace", "ensurebitmapdata(" << this << "): image scaled " << imageSize(mImage) << "->" |