vcl/README.vars.md                      |    4 
 vcl/backendtest/outputdevice/common.cxx |    2 
 vcl/inc/skia/gdiimpl.hxx                |   20 ++
 vcl/inc/skia/osx/gdiimpl.hxx            |    3 
 vcl/inc/skia/utils.hxx                  |   61 +++++++-
 vcl/osx/salgdiutils.cxx                 |    5 
 vcl/skia/gdiimpl.cxx                    |  235 ++++++++++++++++++++++++--------
 vcl/skia/osx/gdiimpl.cxx                |   65 ++++++--
 vcl/skia/salbmp.cxx                     |   11 +
 9 files changed, 312 insertions(+), 94 deletions(-)

New commits:
commit b5983dbe2c41f38e653201574cf20cd4bd76e950
Author:     Luboš Luňák <l.lu...@collabora.com>
AuthorDate: Thu Nov 11 14:01:55 2021 +0100
Commit:     Luboš Luňák <l.lu...@collabora.com>
CommitDate: Tue Nov 16 10:38:54 2021 +0100

    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.lu...@collabora.com>

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

Reply via email to