diff options
-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) << "->" |