summaryrefslogtreecommitdiffstats
path: root/vcl/skia
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
commited5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch)
tree7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /vcl/skia
parentInitial commit. (diff)
downloadlibreoffice-upstream/4%7.4.7.tar.xz
libreoffice-upstream/4%7.4.7.zip
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--vcl/skia/README87
-rw-r--r--vcl/skia/SkiaHelper.cxx824
-rw-r--r--vcl/skia/gdiimpl.cxx2182
-rw-r--r--vcl/skia/osx/bitmap.cxx97
-rw-r--r--vcl/skia/osx/gdiimpl.cxx345
-rw-r--r--vcl/skia/salbmp.cxx1406
-rw-r--r--vcl/skia/skia_denylist_vulkan.xml52
-rw-r--r--vcl/skia/win/gdiimpl.cxx441
-rw-r--r--vcl/skia/x11/gdiimpl.cxx175
-rw-r--r--vcl/skia/x11/salvd.cxx85
-rw-r--r--vcl/skia/x11/textrender.cxx103
-rw-r--r--vcl/skia/zone.cxx87
12 files changed, 5884 insertions, 0 deletions
diff --git a/vcl/skia/README b/vcl/skia/README
new file mode 100644
index 000000000..63f6073ba
--- /dev/null
+++ b/vcl/skia/README
@@ -0,0 +1,87 @@
+This is code for using the Skia library as a drawing library in VCL backends.
+See external/skia for info on the library itself.
+
+Environment variables:
+======================
+
+See README.vars in the toplevel vcl/ directory. Note that many backends do not
+use Skia. E.g. on Linux it is necessary to also use SAL_USE_VCLPLUGIN=gen .
+
+There are also GUI options for controlling whether Skia is enabled.
+
+Skia drawing methods:
+=====================
+
+Skia supports several methods to draw:
+- Raster - CPU-based drawing (here primarily used for fallback when Vulkan isn't available or for debugging)
+- Vulkan - Vulkan-based GPU drawing, this is the default
+- Metal - MACOSX GPU drawing, this is the Mac default
+
+There are more (OpenGL, Metal on Mac, etc.), but (as of now) they are not supported by VCL.
+
+Logging:
+========
+
+Run LO with 'SAL_LOG=+INFO.vcl.skia' to get log information about Skia including
+tracing each drawing operation. If you want log information without drawing operations,
+use 'SAL_LOG=+INFO.vcl.skia-INFO.vcl.skia.trace'.
+
+Debugging:
+==========
+
+Both SkiaSalBitmap and SkiaSalGraphicsImpl have a dump() method that writes a PNG
+with the current contents. There is also SkiaHelper::dump() for dumping contents
+of SkBitmap, SkImage and SkSurface. You can use these in a debugger too, for example
+'p SkiaHelper::dump(image, "/tmp/a.png")'.
+
+If there is a drawing problem, you can use something like the following piece of code
+to dump an image after each relevant operation (or do it in postDraw() if you don't
+know which operation is responsible). You can then find the relevant image
+and match it with the responsible operation (run LO with 'SAL_LOG=+INFO.vcl.skia').
+
+ static int cnt = 0;
+ ++cnt;
+ char buf[100];
+ sprintf(buf,"/tmp/a%05d.png", cnt);
+ SAL_DEBUG("CNT:" << cnt);
+ if(cnt > 4000) // skip some initial drawing operations
+ dump(buf);
+
+
+Testing:
+========
+
+Currently unittests always use the 'headless' VCL backend. Use something like the following
+to run VCL unittests with Skia (and possibly skip slowcheck):
+
+SAL_SKIA=raster SAL_ENABLESKIA=1 SAL_USE_VCLPLUGIN=gen make vcl.build vcl.unitcheck vcl.slowcheck
+
+You can also use 'visualbackendtest' to visually check some operations. Use something like:
+
+SAL_SKIA=raster SAL_ENABLESKIA=1 SAL_USE_VCLPLUGIN=gen [srcdir]/bin/run visualbackendtest
+
+
+Thread safety:
+==============
+
+SolarMutex must be held for most operations (asserted in SkiaSalGraphicsImpl::preDraw() and
+in SkiaZone constructor). The reason for this is that this restriction does not appear to be
+a problem, so there's no need to verify thread safety of the code (including the Skia library).
+There's probably no fundamental reason why the code couldn't be made thread-safe.
+
+
+GrDirectContext sharing:
+========================
+
+We use Skia's sk_app::WindowContext class for creating surfaces for windows, that class
+takes care of the internals. But of offscreen drawing, we need an instance of class
+GrDirectContext. There is sk_app::WindowContext::getGrDirectContext(), but each instance creates
+its own GrDirectContext, and apparently it does not work to mix them. Which means that
+for offscreen drawing we would need to know which window (and only that window)
+the contents will be eventually painted to, which is not possible (it may not even
+be known at the time).
+
+To solve this problem we patch sk_app::WindowContext to create just one GrDirectContext object
+and share it between instances. Additionally, using sk_app::WindowContext::SharedGrDirectContext
+it is possible to share it also for offscreen drawing, including keeping proper reference
+count.
diff --git a/vcl/skia/SkiaHelper.cxx b/vcl/skia/SkiaHelper.cxx
new file mode 100644
index 000000000..84c40baff
--- /dev/null
+++ b/vcl/skia/SkiaHelper.cxx
@@ -0,0 +1,824 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <sal/config.h>
+
+#include <string_view>
+
+#include <vcl/skia/SkiaHelper.hxx>
+
+#if !HAVE_FEATURE_SKIA
+
+namespace SkiaHelper
+{
+bool isVCLSkiaEnabled() { return false; }
+
+} // namespace
+
+#else
+
+#include <rtl/bootstrap.hxx>
+#include <vcl/svapp.hxx>
+#include <desktop/crashreport.hxx>
+#include <officecfg/Office/Common.hxx>
+#include <watchdog.hxx>
+#include <skia/zone.hxx>
+#include <sal/log.hxx>
+#include <driverblocklist.hxx>
+#include <skia/utils.hxx>
+#include <config_folders.h>
+#include <osl/file.hxx>
+#include <tools/stream.hxx>
+#include <list>
+#include <o3tl/lru_map.hxx>
+
+#include <SkBitmap.h>
+#include <SkCanvas.h>
+#include <SkEncodedImageFormat.h>
+#include <SkPaint.h>
+#include <SkSurface.h>
+#include <SkGraphics.h>
+#include <GrDirectContext.h>
+#include <SkRuntimeEffect.h>
+#include <SkOpts_spi.h>
+#include <skia_compiler.hxx>
+#include <skia_opts.hxx>
+#include <tools/sk_app/VulkanWindowContext.h>
+#include <tools/sk_app/MetalWindowContext.h>
+#include <fstream>
+
+namespace SkiaHelper
+{
+static OUString getCacheFolder()
+{
+ OUString url("${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER
+ "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/cache/");
+ rtl::Bootstrap::expandMacros(url);
+ osl::Directory::create(url);
+ return url;
+}
+
+static void writeToLog(SvStream& stream, const char* key, const char* value)
+{
+ stream.WriteCharPtr(key);
+ stream.WriteCharPtr(": ");
+ stream.WriteCharPtr(value);
+ stream.WriteChar('\n');
+}
+
+uint32_t vendorId = 0;
+
+#ifdef SK_VULKAN
+static void writeToLog(SvStream& stream, const char* key, std::u16string_view value)
+{
+ writeToLog(stream, key, OUStringToOString(value, RTL_TEXTENCODING_UTF8).getStr());
+}
+
+static OUString getDenylistFile()
+{
+ OUString url("$BRAND_BASE_DIR/" LIBO_SHARE_FOLDER);
+ rtl::Bootstrap::expandMacros(url);
+
+ return url + "/skia/skia_denylist_vulkan.xml";
+}
+
+static uint32_t driverVersion = 0;
+
+static OUString versionAsString(uint32_t version)
+{
+ return OUString::number(version >> 22) + "." + OUString::number((version >> 12) & 0x3ff) + "."
+ + OUString::number(version & 0xfff);
+}
+
+static std::string_view vendorAsString(uint32_t vendor)
+{
+ return DriverBlocklist::GetVendorNameFromId(vendor);
+}
+
+// Note that this function also logs system information about Vulkan.
+static bool isVulkanDenylisted(const VkPhysicalDeviceProperties& props)
+{
+ static const char* const types[]
+ = { "other", "integrated", "discrete", "virtual", "cpu", "??" }; // VkPhysicalDeviceType
+ driverVersion = props.driverVersion;
+ vendorId = props.vendorID;
+ OUString vendorIdStr = "0x" + OUString::number(props.vendorID, 16);
+ OUString deviceIdStr = "0x" + OUString::number(props.deviceID, 16);
+ OUString driverVersionString = versionAsString(driverVersion);
+ OUString apiVersion = versionAsString(props.apiVersion);
+ const char* deviceType = types[std::min<unsigned>(props.deviceType, SAL_N_ELEMENTS(types) - 1)];
+
+ CrashReporter::addKeyValue("VulkanVendor", vendorIdStr, CrashReporter::AddItem);
+ CrashReporter::addKeyValue("VulkanDevice", deviceIdStr, CrashReporter::AddItem);
+ CrashReporter::addKeyValue("VulkanAPI", apiVersion, CrashReporter::AddItem);
+ CrashReporter::addKeyValue("VulkanDriver", driverVersionString, CrashReporter::AddItem);
+ CrashReporter::addKeyValue("VulkanDeviceType", OUString::createFromAscii(deviceType),
+ CrashReporter::AddItem);
+ CrashReporter::addKeyValue("VulkanDeviceName", OUString::createFromAscii(props.deviceName),
+ CrashReporter::Write);
+
+ SvFileStream logFile(getCacheFolder() + "/skia.log", StreamMode::WRITE | StreamMode::TRUNC);
+ writeToLog(logFile, "RenderMethod", "vulkan");
+ writeToLog(logFile, "Vendor", vendorIdStr);
+ writeToLog(logFile, "Device", deviceIdStr);
+ writeToLog(logFile, "API", apiVersion);
+ writeToLog(logFile, "Driver", driverVersionString);
+ writeToLog(logFile, "DeviceType", deviceType);
+ writeToLog(logFile, "DeviceName", props.deviceName);
+
+ SAL_INFO("vcl.skia",
+ "Vulkan API version: " << apiVersion << ", driver version: " << driverVersionString
+ << ", vendor: " << vendorIdStr << " ("
+ << vendorAsString(vendorId) << "), device: " << deviceIdStr
+ << ", type: " << deviceType << ", name: " << props.deviceName);
+ bool denylisted
+ = DriverBlocklist::IsDeviceBlocked(getDenylistFile(), DriverBlocklist::VersionType::Vulkan,
+ driverVersionString, vendorIdStr, deviceIdStr);
+ writeToLog(logFile, "Denylisted", denylisted ? "yes" : "no");
+ return denylisted;
+}
+#endif
+
+#ifdef SK_METAL
+static void writeSkiaMetalInfo()
+{
+ SvFileStream logFile(getCacheFolder() + "/skia.log", StreamMode::WRITE | StreamMode::TRUNC);
+ writeToLog(logFile, "RenderMethod", "metal");
+}
+#endif
+
+static void writeSkiaRasterInfo()
+{
+ SvFileStream logFile(getCacheFolder() + "/skia.log", StreamMode::WRITE | StreamMode::TRUNC);
+ writeToLog(logFile, "RenderMethod", "raster");
+ // Log compiler, Skia works best when compiled with Clang.
+ writeToLog(logFile, "Compiler", skia_compiler_name());
+}
+
+#if defined(SK_VULKAN) || defined(SK_METAL)
+static std::unique_ptr<sk_app::WindowContext> getTemporaryWindowContext();
+#endif
+
+static void checkDeviceDenylisted(bool blockDisable = false)
+{
+ static bool done = false;
+ if (done)
+ return;
+
+ SkiaZone zone;
+
+ bool useRaster = false;
+ switch (renderMethodToUse())
+ {
+ case RenderVulkan:
+ {
+#ifdef SK_VULKAN
+ // First try if a GrDirectContext already exists.
+ std::unique_ptr<sk_app::WindowContext> temporaryWindowContext;
+ GrDirectContext* grDirectContext
+ = sk_app::VulkanWindowContext::getSharedGrDirectContext();
+ if (!grDirectContext)
+ {
+ // This function is called from isVclSkiaEnabled(), which
+ // may be called when deciding which X11 visual to use,
+ // and that visual is normally needed when creating
+ // Skia's VulkanWindowContext, which is needed for the GrDirectContext.
+ // Avoid the loop by creating a temporary WindowContext
+ // that will use the default X11 visual (that shouldn't matter
+ // for just finding out information about Vulkan) and destroying
+ // the temporary context will clean up again.
+ temporaryWindowContext = getTemporaryWindowContext();
+ grDirectContext = sk_app::VulkanWindowContext::getSharedGrDirectContext();
+ }
+ bool denylisted = true; // assume the worst
+ if (grDirectContext) // Vulkan was initialized properly
+ {
+ denylisted
+ = isVulkanDenylisted(sk_app::VulkanWindowContext::getPhysDeviceProperties());
+ SAL_INFO("vcl.skia", "Vulkan denylisted: " << denylisted);
+ }
+ else
+ SAL_INFO("vcl.skia", "Vulkan could not be initialized");
+ if (denylisted && !blockDisable)
+ {
+ disableRenderMethod(RenderVulkan);
+ useRaster = true;
+ }
+#else
+ SAL_WARN("vcl.skia", "Vulkan support not built in");
+ (void)blockDisable;
+ useRaster = true;
+#endif
+ break;
+ }
+ case RenderMetal:
+ {
+#ifdef SK_METAL
+ // First try if a GrDirectContext already exists.
+ std::unique_ptr<sk_app::WindowContext> temporaryWindowContext;
+ GrDirectContext* grDirectContext = sk_app::getMetalSharedGrDirectContext();
+ if (!grDirectContext)
+ {
+ // Create a temporary window context just to get the GrDirectContext,
+ // as an initial test of Metal functionality.
+ temporaryWindowContext = getTemporaryWindowContext();
+ grDirectContext = sk_app::getMetalSharedGrDirectContext();
+ }
+ if (grDirectContext) // Metal was initialized properly
+ {
+ // Try to assume Metal always works, given that Mac doesn't have such as wide range of HW vendors as PC.
+ // If there turns out to be problems, handle it similarly to Vulkan.
+ SAL_INFO("vcl.skia", "Using Skia Metal mode");
+ writeSkiaMetalInfo();
+ }
+ else
+ {
+ SAL_INFO("vcl.skia", "Metal could not be initialized");
+ disableRenderMethod(RenderMetal);
+ useRaster = true;
+ }
+#else
+ SAL_WARN("vcl.skia", "Metal support not built in");
+ useRaster = true;
+#endif
+ break;
+ }
+ case RenderRaster:
+ useRaster = true;
+ break;
+ }
+ if (useRaster)
+ {
+ SAL_INFO("vcl.skia", "Using Skia raster mode");
+ // software, never denylisted
+ writeSkiaRasterInfo();
+ }
+ done = true;
+}
+
+static bool skiaSupportedByBackend = false;
+static bool supportsVCLSkia()
+{
+ if (!skiaSupportedByBackend)
+ {
+ SAL_INFO("vcl.skia", "Skia not supported by VCL backend, disabling");
+ return false;
+ }
+ return getenv("SAL_DISABLESKIA") == nullptr;
+}
+
+bool isVCLSkiaEnabled()
+{
+ /**
+ * The !bSet part should only be called once! Changing the results in the same
+ * run will mix Skia and normal rendering.
+ */
+
+ static bool bSet = false;
+ static bool bEnable = false;
+ static bool bForceSkia = false;
+
+ // No hardware rendering, so no Skia
+ // TODO SKIA
+ if (Application::IsBitmapRendering())
+ return false;
+
+ if (bSet)
+ {
+ return bForceSkia || bEnable;
+ }
+
+ /*
+ * There are a number of cases that these environment variables cover:
+ * * SAL_FORCESKIA forces Skia if disabled by UI options or denylisted
+ * * SAL_DISABLESKIA avoids the use of Skia regardless of any option
+ */
+
+ bSet = true;
+ bForceSkia = !!getenv("SAL_FORCESKIA") || officecfg::Office::Common::VCL::ForceSkia::get();
+
+ bool bRet = false;
+ bool bSupportsVCLSkia = supportsVCLSkia();
+ if (bForceSkia && bSupportsVCLSkia)
+ {
+ bRet = true;
+ SkGraphics::Init();
+ SkLoOpts::Init();
+ // don't actually block if denylisted, but log it if enabled, and also get the vendor id
+ checkDeviceDenylisted(true);
+ }
+ else if (getenv("SAL_FORCEGL"))
+ {
+ // Skia usage is checked before GL usage, so if GL is forced (and Skia is not), do not
+ // enable Skia in order to allow GL.
+ bRet = false;
+ }
+ else if (bSupportsVCLSkia)
+ {
+ static bool bEnableSkiaEnv = !!getenv("SAL_ENABLESKIA");
+
+ bEnable = bEnableSkiaEnv;
+
+ if (officecfg::Office::Common::VCL::UseSkia::get())
+ bEnable = true;
+
+ // Force disable in safe mode
+ if (Application::IsSafeModeEnabled())
+ bEnable = false;
+
+ if (bEnable)
+ {
+ SkGraphics::Init();
+ SkLoOpts::Init();
+ checkDeviceDenylisted(); // switch to raster if driver is denylisted
+ }
+
+ bRet = bEnable;
+ }
+
+ if (bRet)
+ WatchdogThread::start();
+
+ CrashReporter::addKeyValue("UseSkia", OUString::boolean(bRet), CrashReporter::Write);
+
+ return bRet;
+}
+
+static RenderMethod methodToUse = RenderRaster;
+
+static bool initRenderMethodToUse()
+{
+ if (const char* env = getenv("SAL_SKIA"))
+ {
+ if (strcmp(env, "raster") == 0)
+ {
+ methodToUse = RenderRaster;
+ return true;
+ }
+#ifdef MACOSX
+ if (strcmp(env, "metal") == 0)
+ {
+ methodToUse = RenderMetal;
+ return true;
+ }
+#else
+ if (strcmp(env, "vulkan") == 0)
+ {
+ methodToUse = RenderVulkan;
+ return true;
+ }
+#endif
+ SAL_WARN("vcl.skia", "Unrecognized value of SAL_SKIA");
+ abort();
+ }
+ methodToUse = RenderRaster;
+ if (officecfg::Office::Common::VCL::ForceSkiaRaster::get())
+ return true;
+#ifdef SK_METAL
+ methodToUse = RenderMetal;
+#endif
+#ifdef SK_VULKAN
+ methodToUse = RenderVulkan;
+#endif
+ return true;
+}
+
+RenderMethod renderMethodToUse()
+{
+ static bool methodToUseInited = initRenderMethodToUse();
+ if (methodToUseInited) // Used just to ensure thread-safe one-time init.
+ return methodToUse;
+ abort();
+}
+
+void disableRenderMethod(RenderMethod method)
+{
+ if (renderMethodToUse() != method)
+ return;
+ // Choose a fallback, right now always raster.
+ methodToUse = RenderRaster;
+}
+
+// If needed, we'll allocate one extra window context so that we have a valid GrDirectContext
+// from Vulkan/MetalWindowContext.
+static std::unique_ptr<sk_app::WindowContext> sharedWindowContext;
+
+static std::unique_ptr<sk_app::WindowContext> (*createGpuWindowContextFunction)(bool) = nullptr;
+static void setCreateGpuWindowContext(std::unique_ptr<sk_app::WindowContext> (*function)(bool))
+{
+ createGpuWindowContextFunction = function;
+}
+
+GrDirectContext* getSharedGrDirectContext()
+{
+ SkiaZone zone;
+ assert(renderMethodToUse() != RenderRaster);
+ if (sharedWindowContext)
+ return sharedWindowContext->directContext();
+ // TODO mutex?
+ // Set up the shared GrDirectContext from Skia's (patched) Vulkan/MetalWindowContext, if it's been
+ // already set up.
+ switch (renderMethodToUse())
+ {
+ case RenderVulkan:
+#ifdef SK_VULKAN
+ if (GrDirectContext* context = sk_app::VulkanWindowContext::getSharedGrDirectContext())
+ return context;
+#endif
+ break;
+ case RenderMetal:
+#ifdef SK_METAL
+ if (GrDirectContext* context = sk_app::getMetalSharedGrDirectContext())
+ return context;
+#endif
+ break;
+ case RenderRaster:
+ abort();
+ }
+ static bool done = false;
+ if (done)
+ return nullptr;
+ done = true;
+ if (createGpuWindowContextFunction == nullptr)
+ return nullptr; // not initialized properly (e.g. used from a VCL backend with no Skia support)
+ sharedWindowContext = createGpuWindowContextFunction(false);
+ GrDirectContext* grDirectContext
+ = sharedWindowContext ? sharedWindowContext->directContext() : nullptr;
+ if (grDirectContext)
+ return grDirectContext;
+ SAL_WARN_IF(renderMethodToUse() == RenderVulkan, "vcl.skia",
+ "Cannot create Vulkan GPU context, falling back to Raster");
+ SAL_WARN_IF(renderMethodToUse() == RenderMetal, "vcl.skia",
+ "Cannot create Metal GPU context, falling back to Raster");
+ disableRenderMethod(renderMethodToUse());
+ return nullptr;
+}
+
+#if defined(SK_VULKAN) || defined(SK_METAL)
+static std::unique_ptr<sk_app::WindowContext> getTemporaryWindowContext()
+{
+ if (createGpuWindowContextFunction == nullptr)
+ return nullptr;
+ return createGpuWindowContextFunction(true);
+}
+#endif
+
+static RenderMethod renderMethodToUseForSize(const SkISize& size)
+{
+ // Do not use GPU for small surfaces. The problem is that due to the separate alpha hack
+ // we quite often may call GetBitmap() on VirtualDevice, which is relatively slow
+ // when the pixels need to be fetched from the GPU. And there are documents that use
+ // many tiny surfaces (bsc#1183308 for example), where this slowness adds up too much.
+ // This should be re-evaluated once the separate alpha hack is removed (SKIA_USE_BITMAP32)
+ // and we no longer (hopefully) fetch pixels that often.
+ if (size.width() <= 32 && size.height() <= 32)
+ return RenderRaster;
+ return renderMethodToUse();
+}
+
+sk_sp<SkSurface> createSkSurface(int width, int height, SkColorType type, SkAlphaType alpha)
+{
+ SkiaZone zone;
+ assert(type == kN32_SkColorType || type == kAlpha_8_SkColorType);
+ sk_sp<SkSurface> surface;
+ switch (renderMethodToUseForSize({ width, height }))
+ {
+ case RenderVulkan:
+ case RenderMetal:
+ {
+ if (GrDirectContext* grDirectContext = getSharedGrDirectContext())
+ {
+ surface = SkSurface::MakeRenderTarget(grDirectContext, SkBudgeted::kNo,
+ SkImageInfo::Make(width, height, type, alpha),
+ 0, surfaceProps());
+ if (surface)
+ {
+#ifdef DBG_UTIL
+ prefillSurface(surface);
+#endif
+ return surface;
+ }
+ SAL_WARN_IF(renderMethodToUse() == RenderVulkan, "vcl.skia",
+ "Cannot create Vulkan GPU offscreen surface, falling back to Raster");
+ SAL_WARN_IF(renderMethodToUse() == RenderMetal, "vcl.skia",
+ "Cannot create Metal GPU offscreen surface, falling back to Raster");
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ // Create raster surface as a fallback.
+ surface = SkSurface::MakeRaster(SkImageInfo::Make(width, height, type, alpha), surfaceProps());
+ assert(surface);
+ if (surface)
+ {
+#ifdef DBG_UTIL
+ prefillSurface(surface);
+#endif
+ return surface;
+ }
+ // In non-debug builds we could return SkSurface::MakeNull() and try to cope with the situation,
+ // but that can lead to unnoticed data loss, so better fail clearly.
+ abort();
+}
+
+sk_sp<SkImage> createSkImage(const SkBitmap& bitmap)
+{
+ SkiaZone zone;
+ assert(bitmap.colorType() == kN32_SkColorType || bitmap.colorType() == kAlpha_8_SkColorType);
+ switch (renderMethodToUseForSize(bitmap.dimensions()))
+ {
+ case RenderVulkan:
+ case RenderMetal:
+ {
+ if (GrDirectContext* grDirectContext = getSharedGrDirectContext())
+ {
+ sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(
+ grDirectContext, SkBudgeted::kNo,
+ bitmap.info().makeAlphaType(kPremul_SkAlphaType), 0, surfaceProps());
+ if (surface)
+ {
+ SkPaint paint;
+ paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha
+ surface->getCanvas()->drawImage(bitmap.asImage(), 0, 0, SkSamplingOptions(),
+ &paint);
+ return makeCheckedImageSnapshot(surface);
+ }
+ // Try to fall back in non-debug builds.
+ SAL_WARN_IF(renderMethodToUse() == RenderVulkan, "vcl.skia",
+ "Cannot create Vulkan GPU offscreen surface, falling back to Raster");
+ SAL_WARN_IF(renderMethodToUse() == RenderMetal, "vcl.skia",
+ "Cannot create Metal GPU offscreen surface, falling back to Raster");
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ // Create raster image as a fallback.
+ sk_sp<SkImage> image = SkImage::MakeFromBitmap(bitmap);
+ assert(image);
+ return image;
+}
+
+sk_sp<SkImage> makeCheckedImageSnapshot(sk_sp<SkSurface> surface)
+{
+ sk_sp<SkImage> ret = surface->makeImageSnapshot();
+ assert(ret);
+ if (ret)
+ return ret;
+ abort();
+}
+
+sk_sp<SkImage> makeCheckedImageSnapshot(sk_sp<SkSurface> surface, const SkIRect& bounds)
+{
+ sk_sp<SkImage> ret = surface->makeImageSnapshot(bounds);
+ assert(ret);
+ if (ret)
+ return ret;
+ abort();
+}
+
+namespace
+{
+// Image cache, for saving results of complex operations such as drawTransformedBitmap().
+struct ImageCacheItem
+{
+ OString key;
+ sk_sp<SkImage> image;
+ tools::Long size; // cost of the item
+};
+} //namespace
+
+// LRU cache, last item is the least recently used. Hopefully there won't be that many items
+// to require a hash/map. Using o3tl::lru_map would be simpler, but it doesn't support
+// calculating cost of each item.
+static std::list<ImageCacheItem> imageCache;
+static tools::Long imageCacheSize = 0; // sum of all ImageCacheItem.size
+
+void addCachedImage(const OString& key, sk_sp<SkImage> image)
+{
+ static bool disabled = getenv("SAL_DISABLE_SKIA_CACHE") != nullptr;
+ if (disabled)
+ return;
+ tools::Long size = static_cast<tools::Long>(image->width()) * image->height()
+ * SkColorTypeBytesPerPixel(image->imageInfo().colorType());
+ imageCache.push_front({ key, image, size });
+ imageCacheSize += size;
+ SAL_INFO("vcl.skia.trace", "addcachedimage " << image << " :" << size << "/" << imageCacheSize);
+ const tools::Long maxSize = maxImageCacheSize();
+ while (imageCacheSize > maxSize)
+ {
+ assert(!imageCache.empty());
+ imageCacheSize -= imageCache.back().size;
+ SAL_INFO("vcl.skia.trace",
+ "least used removal " << imageCache.back().image << ":" << imageCache.back().size);
+ imageCache.pop_back();
+ }
+}
+
+sk_sp<SkImage> findCachedImage(const OString& key)
+{
+ for (auto it = imageCache.begin(); it != imageCache.end(); ++it)
+ {
+ if (it->key == key)
+ {
+ sk_sp<SkImage> ret = it->image;
+ SAL_INFO("vcl.skia.trace", "findcachedimage " << key << " : " << it->image << " found");
+ imageCache.splice(imageCache.begin(), imageCache, it);
+ return ret;
+ }
+ }
+ SAL_INFO("vcl.skia.trace", "findcachedimage " << key << " not found");
+ return nullptr;
+}
+
+void removeCachedImage(sk_sp<SkImage> image)
+{
+ for (auto it = imageCache.begin(); it != imageCache.end();)
+ {
+ if (it->image == image)
+ {
+ imageCacheSize -= it->size;
+ assert(imageCacheSize >= 0);
+ it = imageCache.erase(it);
+ }
+ else
+ ++it;
+ }
+}
+
+tools::Long maxImageCacheSize()
+{
+ // Defaults to 4x 2000px 32bpp images, 64MiB.
+ return officecfg::Office::Common::Cache::Skia::ImageCacheSize::get();
+}
+
+static o3tl::lru_map<uint32_t, uint32_t> checksumCache(256);
+
+static uint32_t computeSkPixmapChecksum(const SkPixmap& pixmap)
+{
+ // Use uint32_t because that's what SkOpts::hash_fn() returns.
+ static_assert(std::is_same_v<uint32_t, decltype(SkOpts::hash_fn(nullptr, 0, 0))>);
+ const size_t dataRowBytes = pixmap.width() << pixmap.shiftPerPixel();
+ if (dataRowBytes == pixmap.rowBytes())
+ return SkOpts::hash_fn(pixmap.addr(), pixmap.height() * dataRowBytes, 0);
+ uint32_t sum = 0;
+ for (int row = 0; row < pixmap.height(); ++row)
+ sum = SkOpts::hash_fn(pixmap.addr(0, row), dataRowBytes, sum);
+ return sum;
+}
+
+uint32_t getSkImageChecksum(sk_sp<SkImage> image)
+{
+ // Cache the checksums based on the uniqueID() (which should stay the same
+ // for the same image), because it may be still somewhat expensive.
+ uint32_t id = image->uniqueID();
+ auto it = checksumCache.find(id);
+ if (it != checksumCache.end())
+ return it->second;
+ SkPixmap pixmap;
+ if (!image->peekPixels(&pixmap))
+ abort(); // Fetching of GPU-based pixels is expensive, and shouldn't(?) be needed anyway.
+ uint32_t checksum = computeSkPixmapChecksum(pixmap);
+ checksumCache.insert({ id, checksum });
+ return checksum;
+}
+
+static sk_sp<SkBlender> invertBlender;
+static sk_sp<SkBlender> xorBlender;
+
+// This does the invert operation, i.e. result = color(255-R,255-G,255-B,A).
+void setBlenderInvert(SkPaint* paint)
+{
+ if (!invertBlender)
+ {
+ // Note that the colors are premultiplied, so '1 - dst.r' must be
+ // written as 'dst.a - dst.r', since premultiplied R is in the range (0-A).
+ const char* const diff = R"(
+ vec4 main( vec4 src, vec4 dst )
+ {
+ return vec4( dst.a - dst.r, dst.a - dst.g, dst.a - dst.b, dst.a );
+ }
+ )";
+ auto effect = SkRuntimeEffect::MakeForBlender(SkString(diff));
+ if (!effect.effect)
+ {
+ SAL_WARN("vcl.skia",
+ "SKRuntimeEffect::MakeForBlender failed: " << effect.errorText.c_str());
+ abort();
+ }
+ invertBlender = effect.effect->makeBlender(nullptr);
+ }
+ paint->setBlender(invertBlender);
+}
+
+// This does the xor operation, i.e. bitwise xor of RGB values of both colors.
+void setBlenderXor(SkPaint* paint)
+{
+ if (!xorBlender)
+ {
+ // Note that the colors are premultiplied, converting to 0-255 range
+ // must also unpremultiply.
+ const char* const diff = R"(
+ vec4 main( vec4 src, vec4 dst )
+ {
+ return vec4(
+ float(int(src.r * src.a * 255.0) ^ int(dst.r * dst.a * 255.0)) / 255.0 / dst.a,
+ float(int(src.g * src.a * 255.0) ^ int(dst.g * dst.a * 255.0)) / 255.0 / dst.a,
+ float(int(src.b * src.a * 255.0) ^ int(dst.b * dst.a * 255.0)) / 255.0 / dst.a,
+ dst.a );
+ }
+ )";
+ SkRuntimeEffect::Options opts;
+ // Skia does not allow binary operators in the default ES2Strict mode, but that's only
+ // because of OpenGL support. We don't use OpenGL, and it's safe for all modes that we do use.
+ // https://groups.google.com/g/skia-discuss/c/EPLuQbg64Kc/m/2uDXFIGhAwAJ
+ opts.enforceES2Restrictions = false;
+ auto effect = SkRuntimeEffect::MakeForBlender(SkString(diff), opts);
+ if (!effect.effect)
+ {
+ SAL_WARN("vcl.skia",
+ "SKRuntimeEffect::MakeForBlender failed: " << effect.errorText.c_str());
+ abort();
+ }
+ xorBlender = effect.effect->makeBlender(nullptr);
+ }
+ paint->setBlender(xorBlender);
+}
+
+void cleanup()
+{
+ sharedWindowContext.reset();
+ imageCache.clear();
+ imageCacheSize = 0;
+ invertBlender.reset();
+ xorBlender.reset();
+}
+
+static SkSurfaceProps commonSurfaceProps;
+const SkSurfaceProps* surfaceProps() { return &commonSurfaceProps; }
+
+void setPixelGeometry(SkPixelGeometry pixelGeometry)
+{
+ commonSurfaceProps = SkSurfaceProps(commonSurfaceProps.flags(), pixelGeometry);
+}
+
+// Skia should not be used from VCL backends that do not actually support it, as there will be setup missing.
+// The code here (that is in the vcl lib) needs a function for creating Vulkan/Metal context that is
+// usually available only in the backend libs.
+void prepareSkia(std::unique_ptr<sk_app::WindowContext> (*createGpuWindowContext)(bool))
+{
+ setCreateGpuWindowContext(createGpuWindowContext);
+ skiaSupportedByBackend = true;
+}
+
+void dump(const SkBitmap& bitmap, const char* file) { dump(SkImage::MakeFromBitmap(bitmap), file); }
+
+void dump(const sk_sp<SkSurface>& surface, const char* file)
+{
+ surface->getCanvas()->flush();
+ dump(makeCheckedImageSnapshot(surface), file);
+}
+
+void dump(const sk_sp<SkImage>& image, const char* file)
+{
+ sk_sp<SkData> data = image->encodeToData(SkEncodedImageFormat::kPNG, 1);
+ std::ofstream ostream(file, std::ios::binary);
+ ostream.write(static_cast<const char*>(data->data()), data->size());
+}
+
+#ifdef DBG_UTIL
+void prefillSurface(const sk_sp<SkSurface>& surface)
+{
+ // Pre-fill the surface with deterministic garbage.
+ SkBitmap bitmap;
+ bitmap.allocN32Pixels(2, 2);
+ SkPMColor* scanline;
+ scanline = bitmap.getAddr32(0, 0);
+ *scanline++ = SkPreMultiplyARGB(0xFF, 0xBF, 0x80, 0x40);
+ *scanline++ = SkPreMultiplyARGB(0xFF, 0x40, 0x80, 0xBF);
+ scanline = bitmap.getAddr32(0, 1);
+ *scanline++ = SkPreMultiplyARGB(0xFF, 0xE3, 0x5C, 0x13);
+ *scanline++ = SkPreMultiplyARGB(0xFF, 0x13, 0x5C, 0xE3);
+ bitmap.setImmutable();
+ SkPaint paint;
+ paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha
+ paint.setShader(
+ bitmap.makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat, SkSamplingOptions()));
+ surface->getCanvas()->drawPaint(paint);
+}
+#endif
+
+} // namespace
+
+#endif // HAVE_FEATURE_SKIA
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/skia/gdiimpl.cxx b/vcl/skia/gdiimpl.cxx
new file mode 100644
index 000000000..896849bae
--- /dev/null
+++ b/vcl/skia/gdiimpl.cxx
@@ -0,0 +1,2182 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <skia/gdiimpl.hxx>
+
+#include <salgdi.hxx>
+#include <skia/salbmp.hxx>
+#include <vcl/idle.hxx>
+#include <vcl/svapp.hxx>
+#include <vcl/lazydelete.hxx>
+#include <vcl/gradient.hxx>
+#include <vcl/skia/SkiaHelper.hxx>
+#include <skia/utils.hxx>
+#include <skia/zone.hxx>
+
+#include <SkBitmap.h>
+#include <SkCanvas.h>
+#include <SkGradientShader.h>
+#include <SkPath.h>
+#include <SkRegion.h>
+#include <SkDashPathEffect.h>
+#include <GrBackendSurface.h>
+#include <SkTextBlob.h>
+#include <SkRSXform.h>
+
+#include <numeric>
+#include <basegfx/polygon/b2dpolygontools.hxx>
+#include <basegfx/polygon/b2dpolypolygontools.hxx>
+#include <basegfx/polygon/b2dpolypolygoncutter.hxx>
+#include <o3tl/sorted_vector.hxx>
+#include <rtl/math.hxx>
+
+using namespace SkiaHelper;
+
+namespace
+{
+// Create Skia Path from B2DPolygon
+// Note that polygons generally have the complication that when used
+// for area (fill) operations they usually miss the right-most and
+// bottom-most line of pixels of the bounding rectangle (see
+// https://lists.freedesktop.org/archives/libreoffice/2019-November/083709.html).
+// So be careful with rectangle->polygon conversions (generally avoid them).
+void addPolygonToPath(const basegfx::B2DPolygon& rPolygon, SkPath& rPath, sal_uInt32 nFirstIndex,
+ sal_uInt32 nLastIndex, const sal_uInt32 nPointCount, const bool bClosePath,
+ const bool bHasCurves, bool* hasOnlyOrthogonal = nullptr)
+{
+ assert(nFirstIndex < nPointCount);
+ assert(nLastIndex <= nPointCount);
+
+ if (nPointCount <= 1)
+ return;
+
+ bool bFirst = true;
+ sal_uInt32 nPreviousIndex = nFirstIndex == 0 ? nPointCount - 1 : nFirstIndex - 1;
+ basegfx::B2DPoint aPreviousPoint = rPolygon.getB2DPoint(nPreviousIndex);
+
+ for (sal_uInt32 nIndex = nFirstIndex; nIndex <= nLastIndex; nIndex++)
+ {
+ if (nIndex == nPointCount && !bClosePath)
+ continue;
+
+ // Make sure we loop the last point to first point
+ sal_uInt32 nCurrentIndex = nIndex % nPointCount;
+ basegfx::B2DPoint aCurrentPoint = rPolygon.getB2DPoint(nCurrentIndex);
+
+ if (bFirst)
+ {
+ rPath.moveTo(aCurrentPoint.getX(), aCurrentPoint.getY());
+ bFirst = false;
+ }
+ else if (!bHasCurves)
+ {
+ rPath.lineTo(aCurrentPoint.getX(), aCurrentPoint.getY());
+ // If asked for, check whether the polygon has a line that is not
+ // strictly horizontal or vertical.
+ if (hasOnlyOrthogonal != nullptr && aCurrentPoint.getX() != aPreviousPoint.getX()
+ && aCurrentPoint.getY() != aPreviousPoint.getY())
+ *hasOnlyOrthogonal = false;
+ }
+ else
+ {
+ basegfx::B2DPoint aPreviousControlPoint = rPolygon.getNextControlPoint(nPreviousIndex);
+ basegfx::B2DPoint aCurrentControlPoint = rPolygon.getPrevControlPoint(nCurrentIndex);
+
+ if (aPreviousControlPoint.equal(aPreviousPoint)
+ && aCurrentControlPoint.equal(aCurrentPoint))
+ {
+ rPath.lineTo(aCurrentPoint.getX(), aCurrentPoint.getY()); // a straight line
+ if (hasOnlyOrthogonal != nullptr && aCurrentPoint.getX() != aPreviousPoint.getX()
+ && aCurrentPoint.getY() != aPreviousPoint.getY())
+ *hasOnlyOrthogonal = false;
+ }
+ else
+ {
+ if (aPreviousControlPoint.equal(aPreviousPoint))
+ {
+ aPreviousControlPoint
+ = aPreviousPoint + ((aPreviousControlPoint - aCurrentPoint) * 0.0005);
+ }
+ if (aCurrentControlPoint.equal(aCurrentPoint))
+ {
+ aCurrentControlPoint
+ = aCurrentPoint + ((aCurrentControlPoint - aPreviousPoint) * 0.0005);
+ }
+ rPath.cubicTo(aPreviousControlPoint.getX(), aPreviousControlPoint.getY(),
+ aCurrentControlPoint.getX(), aCurrentControlPoint.getY(),
+ aCurrentPoint.getX(), aCurrentPoint.getY());
+ if (hasOnlyOrthogonal != nullptr)
+ *hasOnlyOrthogonal = false;
+ }
+ }
+ aPreviousPoint = aCurrentPoint;
+ nPreviousIndex = nCurrentIndex;
+ }
+ if (bClosePath && nFirstIndex == 0 && nLastIndex == nPointCount)
+ {
+ rPath.close();
+ }
+}
+
+void addPolygonToPath(const basegfx::B2DPolygon& rPolygon, SkPath& rPath,
+ bool* hasOnlyOrthogonal = nullptr)
+{
+ addPolygonToPath(rPolygon, rPath, 0, rPolygon.count(), rPolygon.count(), rPolygon.isClosed(),
+ rPolygon.areControlPointsUsed(), hasOnlyOrthogonal);
+}
+
+void addPolyPolygonToPath(const basegfx::B2DPolyPolygon& rPolyPolygon, SkPath& rPath,
+ bool* hasOnlyOrthogonal = nullptr)
+{
+ const sal_uInt32 nPolygonCount(rPolyPolygon.count());
+
+ if (nPolygonCount == 0)
+ return;
+
+ sal_uInt32 nPointCount = 0;
+ for (const auto& rPolygon : rPolyPolygon)
+ nPointCount += rPolygon.count() * 3; // because cubicTo is 3 elements
+ rPath.incReserve(nPointCount);
+
+ for (const auto& rPolygon : rPolyPolygon)
+ {
+ addPolygonToPath(rPolygon, rPath, hasOnlyOrthogonal);
+ }
+}
+
+// Check if the given polygon contains a straight line. If not, it consists
+// solely of curves.
+bool polygonContainsLine(const basegfx::B2DPolyPolygon& rPolyPolygon)
+{
+ if (!rPolyPolygon.areControlPointsUsed())
+ return true; // no curves at all
+ for (const auto& rPolygon : rPolyPolygon)
+ {
+ const sal_uInt32 nPointCount(rPolygon.count());
+ bool bFirst = true;
+
+ const bool bClosePath(rPolygon.isClosed());
+
+ sal_uInt32 nCurrentIndex = 0;
+ sal_uInt32 nPreviousIndex = nPointCount - 1;
+
+ basegfx::B2DPoint aCurrentPoint;
+ basegfx::B2DPoint aPreviousPoint;
+
+ for (sal_uInt32 nIndex = 0; nIndex <= nPointCount; nIndex++)
+ {
+ if (nIndex == nPointCount && !bClosePath)
+ continue;
+
+ // Make sure we loop the last point to first point
+ nCurrentIndex = nIndex % nPointCount;
+ if (bFirst)
+ bFirst = false;
+ else
+ {
+ basegfx::B2DPoint aPreviousControlPoint
+ = rPolygon.getNextControlPoint(nPreviousIndex);
+ basegfx::B2DPoint aCurrentControlPoint
+ = rPolygon.getPrevControlPoint(nCurrentIndex);
+
+ if (aPreviousControlPoint.equal(aPreviousPoint)
+ && aCurrentControlPoint.equal(aCurrentPoint))
+ {
+ return true; // found a straight line
+ }
+ }
+ aPreviousPoint = aCurrentPoint;
+ nPreviousIndex = nCurrentIndex;
+ }
+ }
+ return false; // no straight line found
+}
+
+// returns true if the source or destination rectangles are invalid
+bool checkInvalidSourceOrDestination(SalTwoRect const& rPosAry)
+{
+ return rPosAry.mnSrcWidth <= 0 || rPosAry.mnSrcHeight <= 0 || rPosAry.mnDestWidth <= 0
+ || rPosAry.mnDestHeight <= 0;
+}
+
+} // end anonymous namespace
+
+// Class that triggers flushing the backing buffer when idle.
+class SkiaFlushIdle : public Idle
+{
+ SkiaSalGraphicsImpl* mpGraphics;
+#ifndef NDEBUG
+ char* debugname;
+#endif
+
+public:
+ explicit SkiaFlushIdle(SkiaSalGraphicsImpl* pGraphics)
+ : Idle(get_debug_name(pGraphics))
+ , mpGraphics(pGraphics)
+ {
+ // We don't want to be swapping before we've painted.
+ SetPriority(TaskPriority::POST_PAINT);
+ }
+#ifndef NDEBUG
+ virtual ~SkiaFlushIdle() { free(debugname); }
+#endif
+ const char* get_debug_name(SkiaSalGraphicsImpl* pGraphics)
+ {
+#ifndef NDEBUG
+ // Idle keeps just a pointer, so we need to store the string
+ debugname = strdup(
+ OString("skia idle 0x" + OString::number(reinterpret_cast<sal_uIntPtr>(pGraphics), 16))
+ .getStr());
+ return debugname;
+#else
+ (void)pGraphics;
+ return "skia idle";
+#endif
+ }
+
+ virtual void Invoke() override
+ {
+ mpGraphics->performFlush();
+ Stop();
+ SetPriority(TaskPriority::HIGHEST);
+ }
+};
+
+SkiaSalGraphicsImpl::SkiaSalGraphicsImpl(SalGraphics& rParent, SalGeometryProvider* pProvider)
+ : mParent(rParent)
+ , mProvider(pProvider)
+ , mIsGPU(false)
+ , mLineColor(SALCOLOR_NONE)
+ , mFillColor(SALCOLOR_NONE)
+ , mXorMode(XorMode::None)
+ , mFlush(new SkiaFlushIdle(this))
+ , mScaling(1)
+{
+}
+
+SkiaSalGraphicsImpl::~SkiaSalGraphicsImpl()
+{
+ assert(!mSurface);
+ assert(!mWindowContext);
+}
+
+void SkiaSalGraphicsImpl::Init() {}
+
+void SkiaSalGraphicsImpl::createSurface()
+{
+ SkiaZone zone;
+ if (isOffscreen())
+ createOffscreenSurface();
+ else
+ createWindowSurface();
+ 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();
+ mFlush->SetPriority(TaskPriority::POST_PAINT);
+}
+
+void SkiaSalGraphicsImpl::createWindowSurface(bool forceRaster)
+{
+ SkiaZone zone;
+ assert(!isOffscreen());
+ assert(!mSurface);
+ createWindowSurfaceInternal(forceRaster);
+ if (!mSurface)
+ {
+ switch (forceRaster ? RenderRaster : renderMethodToUse())
+ {
+ case RenderVulkan:
+ SAL_WARN("vcl.skia",
+ "cannot create Vulkan GPU window surface, falling back to Raster");
+ destroySurface(); // destroys also WindowContext
+ return createWindowSurface(true); // try again
+ case RenderMetal:
+ SAL_WARN("vcl.skia",
+ "cannot create Metal GPU window surface, falling back to Raster");
+ destroySurface(); // destroys also WindowContext
+ return createWindowSurface(true); // try again
+ case RenderRaster:
+ abort(); // This should not really happen, do not even try to cope with it.
+ }
+ }
+ mIsGPU = mSurface->getCanvas()->recordingContext() != nullptr;
+#ifdef DBG_UTIL
+ prefillSurface(mSurface);
+#endif
+}
+
+bool SkiaSalGraphicsImpl::isOffscreen() const
+{
+ if (mProvider == nullptr || mProvider->IsOffScreen())
+ return true;
+ // HACK: Sometimes (tdf#131939, tdf#138022, tdf#140288) VCL passes us a zero-sized window,
+ // and zero size is invalid for Skia, so force offscreen surface, where we handle this.
+ if (GetWidth() <= 0 || GetHeight() <= 0)
+ return true;
+ return false;
+}
+
+void SkiaSalGraphicsImpl::createOffscreenSurface()
+{
+ SkiaZone zone;
+ assert(isOffscreen());
+ assert(!mSurface);
+ // HACK: See isOffscreen().
+ int width = std::max(1, GetWidth());
+ int height = std::max(1, GetHeight());
+ // 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;
+}
+
+void SkiaSalGraphicsImpl::destroySurface()
+{
+ SkiaZone zone;
+ if (mSurface)
+ {
+ // check setClipRegion() invariant
+ assert(mSurface->getCanvas()->getSaveCount() == 3);
+ // if this fails, something forgot to use SkAutoCanvasRestore
+ assert(mSurface->getCanvas()->getTotalMatrix() == SkMatrix::Scale(mScaling, mScaling));
+ }
+ mSurface.reset();
+ mWindowContext.reset();
+ mIsGPU = false;
+ mScaling = 1;
+}
+
+void SkiaSalGraphicsImpl::performFlush()
+{
+ SkiaZone zone;
+ flushDrawing();
+ if (mSurface)
+ {
+ if (mDirtyRect.intersect(SkIRect::MakeWH(GetWidth(), GetHeight())))
+ flushSurfaceToWindowContext();
+ mDirtyRect.setEmpty();
+ }
+}
+
+void SkiaSalGraphicsImpl::flushSurfaceToWindowContext()
+{
+ sk_sp<SkSurface> screenSurface = mWindowContext->getBackbufferSurface();
+ if (screenSurface != mSurface)
+ {
+ // GPU-based window contexts require calling getBackbufferSurface()
+ // for every swapBuffers(), for this reason mSurface is an offscreen surface
+ // where we keep the contents (LO does not do full redraws).
+ // So here blit the surface to the window context surface and then swap it.
+ 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.
+ mWindowContext->swapBuffers(nullptr); // Must swap the entire surface.
+ }
+ else
+ {
+ // For raster mode use directly the backbuffer surface, it's just a bitmap
+ // surface anyway, and for those there's no real requirement to call
+ // getBackbufferSurface() repeatedly. Using our own surface would duplicate
+ // memory and cost time copying pixels around.
+ assert(!isGPU());
+ SkIRect dirtyRect = mDirtyRect;
+ if (mScaling != 1) // Adjust to mSurface coordinates if needed.
+ dirtyRect = scaleRect(dirtyRect, mScaling);
+ mWindowContext->swapBuffers(&dirtyRect);
+ }
+}
+
+void SkiaSalGraphicsImpl::DeInit() { destroySurface(); }
+
+void SkiaSalGraphicsImpl::preDraw()
+{
+ assert(comphelper::SolarMutex::get()->IsCurrentThread());
+ SkiaZone::enter(); // matched in postDraw()
+ checkSurface();
+ checkPendingDrawing();
+}
+
+void SkiaSalGraphicsImpl::postDraw()
+{
+ scheduleFlush();
+ // Skia (at least when using Vulkan) queues drawing commands and executes them only later.
+ // But tdf#136369 leads to creating and queueing many tiny bitmaps, which makes
+ // Skia slow, and may make it even run out of memory. So force a flush if such
+ // a problematic operation has been performed too many times without a flush.
+ // Note that the counter is a static variable, as all drawing shares the same Skia drawing
+ // context (and so the flush here will also flush all drawing).
+ if (pendingOperationsToFlush > 1000)
+ {
+ mSurface->flushAndSubmit();
+ pendingOperationsToFlush = 0;
+ }
+ SkiaZone::leave(); // matched in preDraw()
+ // If there's a problem with the GPU context, abort.
+ if (GrDirectContext* context = GrAsDirectContext(mSurface->getCanvas()->recordingContext()))
+ {
+ // Running out of memory on the GPU technically could be possibly recoverable,
+ // but we don't know the exact status of the surface (and what has or has not been drawn to it),
+ // so in practice this is unrecoverable without possible data loss.
+ if (context->oomed())
+ {
+ SAL_WARN("vcl.skia", "GPU context has run out of memory, aborting.");
+ abort();
+ }
+ // Unrecoverable problem.
+ if (context->abandoned())
+ {
+ SAL_WARN("vcl.skia", "GPU context has been abandoned, aborting.");
+ abort();
+ }
+ }
+}
+
+void SkiaSalGraphicsImpl::scheduleFlush()
+{
+ if (!isOffscreen())
+ {
+ if (!Application::IsInExecute())
+ performFlush(); // otherwise nothing would trigger idle rendering
+ else if (!mFlush->IsActive())
+ mFlush->Start();
+ }
+}
+
+// VCL can sometimes resize us without telling us, update the surface if needed.
+// Also create the surface on demand if it has not been created yet (it is a waste
+// to create it in Init() if it gets recreated later anyway).
+void SkiaSalGraphicsImpl::checkSurface()
+{
+ if (!mSurface)
+ {
+ createSurface();
+ SAL_INFO("vcl.skia.trace",
+ "create(" << this << "): " << Size(mSurface->width(), mSurface->height()));
+ }
+ else if (GetWidth() * mScaling != mSurface->width()
+ || GetHeight() * mScaling != mSurface->height())
+ {
+ if (!avoidRecreateByResize())
+ {
+ Size oldSize(mSurface->width(), mSurface->height());
+ // Recreating a surface means that the old SkSurface contents will be lost.
+ // But if a window has been resized the windowing system may send repaint events
+ // only for changed parts and VCL would not repaint the whole area, assuming
+ // that some parts have not changed (this is what seems to cause tdf#131952).
+ // So carry over the old contents for windows, even though generally everything
+ // will be usually repainted anyway.
+ sk_sp<SkImage> snapshot;
+ if (!isOffscreen())
+ {
+ flushDrawing();
+ snapshot = makeCheckedImageSnapshot(mSurface);
+ }
+
+ destroySurface();
+ createSurface();
+
+ if (snapshot)
+ {
+ 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())
+ << " requested "
+ << Size(GetWidth(), GetHeight()));
+ }
+ }
+}
+
+bool SkiaSalGraphicsImpl::avoidRecreateByResize() const
+{
+ // Keep the old surface if VCL sends us a broken size (see isOffscreen()).
+ if (GetWidth() == 0 || GetHeight() == 0)
+ return true;
+ return false;
+}
+
+void SkiaSalGraphicsImpl::flushDrawing()
+{
+ if (!mSurface)
+ return;
+ checkPendingDrawing();
+ ++pendingOperationsToFlush;
+}
+
+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 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)
+ return true;
+ SkiaZone zone;
+ checkPendingDrawing();
+ checkSurface();
+ mClipRegion = region;
+ SAL_INFO("vcl.skia.trace", "setclipregion(" << this << "): " << region);
+ SkCanvas* canvas = mSurface->getCanvas();
+ assert(canvas->getSaveCount() == 3);
+ canvas->restore(); // undo previous clip state, see setCanvasScalingAndClipping()
+ canvas->save();
+ setCanvasClipRegion(canvas, region);
+ return true;
+}
+
+void SkiaSalGraphicsImpl::setCanvasClipRegion(SkCanvas* canvas, const vcl::Region& region)
+{
+ SkiaZone zone;
+ SkPath path;
+ // Always use region rectangles, regardless of what the region uses internally.
+ // That's what other VCL backends do, and trying to use addPolyPolygonToPath()
+ // in case a polygon is used leads to off-by-one errors such as tdf#133208.
+ RectangleVector rectangles;
+ region.GetRegionRectangles(rectangles);
+ path.incReserve(rectangles.size() + 1);
+ for (const tools::Rectangle& rectangle : rectangles)
+ path.addRect(SkRect::MakeXYWH(rectangle.getX(), rectangle.getY(), rectangle.GetWidth(),
+ rectangle.GetHeight()));
+ path.setFillType(SkPathFillType::kEvenOdd);
+ canvas->clipPath(path);
+}
+
+void SkiaSalGraphicsImpl::ResetClipRegion()
+{
+ setClipRegion(vcl::Region(tools::Rectangle(0, 0, GetWidth(), GetHeight())));
+}
+
+const vcl::Region& SkiaSalGraphicsImpl::getClipRegion() const { return mClipRegion; }
+
+sal_uInt16 SkiaSalGraphicsImpl::GetBitCount() const { return 32; }
+
+tools::Long SkiaSalGraphicsImpl::GetGraphicsWidth() const { return GetWidth(); }
+
+void SkiaSalGraphicsImpl::SetLineColor()
+{
+ checkPendingDrawing();
+ mLineColor = SALCOLOR_NONE;
+}
+
+void SkiaSalGraphicsImpl::SetLineColor(Color nColor)
+{
+ checkPendingDrawing();
+ mLineColor = nColor;
+}
+
+void SkiaSalGraphicsImpl::SetFillColor()
+{
+ checkPendingDrawing();
+ mFillColor = SALCOLOR_NONE;
+}
+
+void SkiaSalGraphicsImpl::SetFillColor(Color nColor)
+{
+ checkPendingDrawing();
+ mFillColor = nColor;
+}
+
+void SkiaSalGraphicsImpl::SetXORMode(bool set, bool invert)
+{
+ XorMode newMode = set ? (invert ? XorMode::Invert : XorMode::Xor) : XorMode::None;
+ if (newMode == mXorMode)
+ return;
+ checkPendingDrawing();
+ SAL_INFO("vcl.skia.trace", "setxormode(" << this << "): " << set << "/" << invert);
+ mXorMode = newMode;
+}
+
+void SkiaSalGraphicsImpl::SetROPLineColor(SalROPColor nROPColor)
+{
+ checkPendingDrawing();
+ switch (nROPColor)
+ {
+ case SalROPColor::N0:
+ mLineColor = Color(0, 0, 0);
+ break;
+ case SalROPColor::N1:
+ mLineColor = Color(0xff, 0xff, 0xff);
+ break;
+ case SalROPColor::Invert:
+ mLineColor = Color(0xff, 0xff, 0xff);
+ break;
+ }
+}
+
+void SkiaSalGraphicsImpl::SetROPFillColor(SalROPColor nROPColor)
+{
+ checkPendingDrawing();
+ switch (nROPColor)
+ {
+ case SalROPColor::N0:
+ mFillColor = Color(0, 0, 0);
+ break;
+ case SalROPColor::N1:
+ mFillColor = Color(0xff, 0xff, 0xff);
+ break;
+ case SalROPColor::Invert:
+ mFillColor = Color(0xff, 0xff, 0xff);
+ break;
+ }
+}
+
+void SkiaSalGraphicsImpl::drawPixel(tools::Long nX, tools::Long nY)
+{
+ drawPixel(nX, nY, mLineColor);
+}
+
+void SkiaSalGraphicsImpl::drawPixel(tools::Long nX, tools::Long nY, Color nColor)
+{
+ if (nColor == SALCOLOR_NONE)
+ return;
+ preDraw();
+ SAL_INFO("vcl.skia.trace", "drawpixel(" << this << "): " << Point(nX, nY) << ":" << nColor);
+ addUpdateRegion(SkRect::MakeXYWH(nX, nY, 1, 1));
+ SkPaint paint = makePixelPaint(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();
+}
+
+void SkiaSalGraphicsImpl::drawLine(tools::Long nX1, tools::Long nY1, tools::Long nX2,
+ tools::Long nY2)
+{
+ if (mLineColor == SALCOLOR_NONE)
+ return;
+ preDraw();
+ SAL_INFO("vcl.skia.trace", "drawline(" << this << "): " << Point(nX1, nY1) << "->"
+ << Point(nX2, nY2) << ":" << mLineColor);
+ addUpdateRegion(SkRect::MakeLTRB(nX1, nY1, nX2, nY2).makeSorted());
+ SkPaint paint = makeLinePaint();
+ 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();
+}
+
+void SkiaSalGraphicsImpl::privateDrawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth,
+ tools::Long nHeight, double fTransparency,
+ bool blockAA)
+{
+ preDraw();
+ SAL_INFO("vcl.skia.trace",
+ "privatedrawrect(" << this << "): " << SkIRect::MakeXYWH(nX, nY, nWidth, nHeight)
+ << ":" << mLineColor << ":" << mFillColor << ":" << fTransparency);
+ addUpdateRegion(SkRect::MakeXYWH(nX, nY, nWidth, nHeight));
+ SkCanvas* canvas = getDrawCanvas();
+ if (mFillColor != SALCOLOR_NONE)
+ {
+ SkPaint paint = makeFillPaint(fTransparency);
+ paint.setAntiAlias(!blockAA && mParent.getAntiAlias());
+ // HACK: If the polygon is just a line, it still should be drawn. But when filling
+ // Skia doesn't draw empty polygons, so in that case ensure the line is drawn.
+ if (mLineColor == SALCOLOR_NONE && SkSize::Make(nWidth, nHeight).isEmpty())
+ paint.setStyle(SkPaint::kStroke_Style);
+ canvas->drawIRect(SkIRect::MakeXYWH(nX, nY, nWidth, nHeight), paint);
+ }
+ if (mLineColor != SALCOLOR_NONE && mLineColor != mFillColor) // otherwise handled by fill
+ {
+ SkPaint paint = makeLinePaint(fTransparency);
+ paint.setAntiAlias(!blockAA && mParent.getAntiAlias());
+ if (mScaling != 1 && isUnitTestRunning())
+ {
+ // On HiDPI displays, do not draw just a hairline 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->drawRect(SkRect::MakeXYWH(toSkX(nX), toSkY(nY),
+ std::max(tools::Long(1), nWidth - 1),
+ std::max(tools::Long(1), nHeight - 1)),
+ paint);
+ }
+ postDraw();
+}
+
+void SkiaSalGraphicsImpl::drawRect(tools::Long nX, tools::Long nY, tools::Long nWidth,
+ tools::Long nHeight)
+{
+ privateDrawAlphaRect(nX, nY, nWidth, nHeight, 0.0, true);
+}
+
+void SkiaSalGraphicsImpl::drawPolyLine(sal_uInt32 nPoints, const Point* pPtAry)
+{
+ basegfx::B2DPolygon aPolygon;
+ aPolygon.append(basegfx::B2DPoint(pPtAry->getX(), pPtAry->getY()), nPoints);
+ for (sal_uInt32 i = 1; i < nPoints; ++i)
+ aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].getX(), pPtAry[i].getY()));
+ aPolygon.setClosed(false);
+
+ drawPolyLine(basegfx::B2DHomMatrix(), aPolygon, 0.0, 1.0, nullptr, basegfx::B2DLineJoin::Miter,
+ css::drawing::LineCap_BUTT, basegfx::deg2rad(15.0) /*default*/, false);
+}
+
+void SkiaSalGraphicsImpl::drawPolygon(sal_uInt32 nPoints, const Point* pPtAry)
+{
+ basegfx::B2DPolygon aPolygon;
+ aPolygon.append(basegfx::B2DPoint(pPtAry->getX(), pPtAry->getY()), nPoints);
+ for (sal_uInt32 i = 1; i < nPoints; ++i)
+ aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].getX(), pPtAry[i].getY()));
+
+ drawPolyPolygon(basegfx::B2DHomMatrix(), basegfx::B2DPolyPolygon(aPolygon), 0.0);
+}
+
+void SkiaSalGraphicsImpl::drawPolyPolygon(sal_uInt32 nPoly, const sal_uInt32* pPoints,
+ const Point** pPtAry)
+{
+ basegfx::B2DPolyPolygon aPolyPolygon;
+ for (sal_uInt32 nPolygon = 0; nPolygon < nPoly; ++nPolygon)
+ {
+ sal_uInt32 nPoints = pPoints[nPolygon];
+ if (nPoints)
+ {
+ const Point* pSubPoints = pPtAry[nPolygon];
+ basegfx::B2DPolygon aPolygon;
+ aPolygon.append(basegfx::B2DPoint(pSubPoints->getX(), pSubPoints->getY()), nPoints);
+ for (sal_uInt32 i = 1; i < nPoints; ++i)
+ aPolygon.setB2DPoint(i,
+ basegfx::B2DPoint(pSubPoints[i].getX(), pSubPoints[i].getY()));
+
+ aPolyPolygon.append(aPolygon);
+ }
+ }
+
+ drawPolyPolygon(basegfx::B2DHomMatrix(), aPolyPolygon, 0.0);
+}
+
+bool SkiaSalGraphicsImpl::drawPolyPolygon(const basegfx::B2DHomMatrix& rObjectToDevice,
+ const basegfx::B2DPolyPolygon& rPolyPolygon,
+ double fTransparency)
+{
+ const bool bHasFill(mFillColor != SALCOLOR_NONE);
+ const bool bHasLine(mLineColor != SALCOLOR_NONE);
+
+ if (rPolyPolygon.count() == 0 || !(bHasFill || bHasLine) || fTransparency < 0.0
+ || fTransparency >= 1.0)
+ return true;
+
+ basegfx::B2DPolyPolygon aPolyPolygon(rPolyPolygon);
+ aPolyPolygon.transform(rObjectToDevice);
+
+ SAL_INFO("vcl.skia.trace", "drawpolypolygon(" << this << "): " << aPolyPolygon << ":"
+ << mLineColor << ":" << mFillColor);
+
+ if (delayDrawPolyPolygon(aPolyPolygon, fTransparency))
+ {
+ scheduleFlush();
+ return true;
+ }
+
+ performDrawPolyPolygon(aPolyPolygon, fTransparency, mParent.getAntiAlias());
+ return true;
+}
+
+void SkiaSalGraphicsImpl::performDrawPolyPolygon(const basegfx::B2DPolyPolygon& aPolyPolygon,
+ double fTransparency, bool useAA)
+{
+ preDraw();
+
+ SkPath polygonPath;
+ bool hasOnlyOrthogonal = true;
+ addPolyPolygonToPath(aPolyPolygon, polygonPath, &hasOnlyOrthogonal);
+ polygonPath.setFillType(SkPathFillType::kEvenOdd);
+ addUpdateRegion(polygonPath.getBounds());
+
+ // For lines we use toSkX()/toSkY() in order to pass centers of pixels to Skia,
+ // as that leads to better results with floating-point coordinates
+ // (e.g. https://bugs.chromium.org/p/skia/issues/detail?id=9611).
+ // But that means that we generally need to use it also for areas, so that they
+ // line up properly if used together (tdf#134346).
+ // On the other hand, with AA enabled and rectangular areas, this leads to fuzzy
+ // edges (tdf#137329). But since rectangular areas line up perfectly to pixels
+ // everywhere, it shouldn't be necessary to do this for them.
+ // So if AA is enabled, avoid this fixup for rectangular areas.
+ if (!useAA || !hasOnlyOrthogonal)
+ {
+ // We normally use pixel at their center positions, but slightly off (see toSkX/Y()).
+ // With AA lines that "slightly off" causes tiny changes of color, making some tests
+ // fail. Since moving AA-ed line slightly to a side doesn't cause any real visual
+ // difference, just place exactly at the center. tdf#134346
+ const SkScalar posFix = useAA ? toSkXYFix : 0;
+ polygonPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr);
+ }
+ if (mFillColor != SALCOLOR_NONE)
+ {
+ SkPaint aPaint = makeFillPaint(fTransparency);
+ aPaint.setAntiAlias(useAA);
+ // HACK: If the polygon is just a line, it still should be drawn. But when filling
+ // Skia doesn't draw empty polygons, so in that case ensure the line is drawn.
+ if (mLineColor == SALCOLOR_NONE && polygonPath.getBounds().isEmpty())
+ aPaint.setStyle(SkPaint::kStroke_Style);
+ getDrawCanvas()->drawPath(polygonPath, aPaint);
+ }
+ if (mLineColor != SALCOLOR_NONE && mLineColor != mFillColor) // otherwise handled by fill
+ {
+ SkPaint aPaint = makeLinePaint(fTransparency);
+ aPaint.setAntiAlias(useAA);
+ getDrawCanvas()->drawPath(polygonPath, aPaint);
+ }
+ postDraw();
+}
+
+namespace
+{
+struct LessThan
+{
+ bool operator()(const basegfx::B2DPoint& point1, const basegfx::B2DPoint& point2) const
+ {
+ if (basegfx::fTools::equal(point1.getX(), point2.getX()))
+ return basegfx::fTools::less(point1.getY(), point2.getY());
+ return basegfx::fTools::less(point1.getX(), point2.getX());
+ }
+};
+} // namespace
+
+bool SkiaSalGraphicsImpl::delayDrawPolyPolygon(const basegfx::B2DPolyPolygon& aPolyPolygon,
+ double fTransparency)
+{
+ // There is some code that needlessly subdivides areas into adjacent rectangles,
+ // but Skia doesn't line them up perfectly if AA is enabled (e.g. Cairo, Qt5 do,
+ // but Skia devs claim it's working as intended
+ // https://groups.google.com/d/msg/skia-discuss/NlKpD2X_5uc/Vuwd-kyYBwAJ).
+ // An example is tdf#133016, which triggers SvgStyleAttributes::add_stroke()
+ // implementing a line stroke as a bunch of polygons instead of just one, and
+ // SvgLinearAtomPrimitive2D::create2DDecomposition() creates a gradient
+ // as a series of polygons of gradually changing color. Those places should be
+ // changed, but try to merge those split polygons back into the original one,
+ // where the needlessly created edges causing problems will not exist.
+ // This means drawing of such polygons needs to be delayed, so that they can
+ // be possibly merged with the next one.
+ // Merge only polygons of the same properties (color, etc.), so the gradient problem
+ // actually isn't handled here.
+
+ // Only AA polygons need merging, because they do not line up well because of the AA of the edges.
+ if (!mParent.getAntiAlias())
+ return false;
+ // Only filled polygons without an outline are problematic.
+ if (mFillColor == SALCOLOR_NONE || mLineColor != SALCOLOR_NONE)
+ return false;
+ // Merge only simple polygons, real polypolygons most likely aren't needlessly split,
+ // so they do not need joining.
+ if (aPolyPolygon.count() != 1)
+ return false;
+ // If the polygon is not closed, it doesn't mark an area to be filled.
+ if (!aPolyPolygon.isClosed())
+ return false;
+ // If a polygon does not contain a straight line, i.e. it's all curves, then do not merge.
+ // First of all that's even more expensive, and second it's very unlikely that it's a polygon
+ // split into more polygons.
+ if (!polygonContainsLine(aPolyPolygon))
+ return false;
+
+ if (mLastPolyPolygonInfo.polygons.size() != 0
+ && (mLastPolyPolygonInfo.transparency != fTransparency
+ || !mLastPolyPolygonInfo.bounds.overlaps(aPolyPolygon.getB2DRange())))
+ {
+ checkPendingDrawing(); // Cannot be parts of the same larger polygon, draw the last and reset.
+ }
+ if (!mLastPolyPolygonInfo.polygons.empty())
+ {
+ assert(aPolyPolygon.count() == 1);
+ assert(mLastPolyPolygonInfo.polygons.back().count() == 1);
+ // Check if the new and the previous polygon share at least one point. If not, then they
+ // cannot be adjacent polygons, so there's no point in trying to merge them.
+ bool sharePoint = false;
+ const basegfx::B2DPolygon& poly1 = aPolyPolygon.getB2DPolygon(0);
+ const basegfx::B2DPolygon& poly2 = mLastPolyPolygonInfo.polygons.back().getB2DPolygon(0);
+ o3tl::sorted_vector<basegfx::B2DPoint, LessThan> poly1Points; // for O(n log n)
+ poly1Points.reserve(poly1.count());
+ for (sal_uInt32 i = 0; i < poly1.count(); ++i)
+ poly1Points.insert(poly1.getB2DPoint(i));
+ for (sal_uInt32 i = 0; i < poly2.count(); ++i)
+ if (poly1Points.find(poly2.getB2DPoint(i)) != poly1Points.end())
+ {
+ sharePoint = true;
+ break;
+ }
+ if (!sharePoint)
+ checkPendingDrawing(); // Draw the previous one and reset.
+ }
+ // Collect the polygons that can be possibly merged. Do the merging only once at the end,
+ // because it's not a cheap operation.
+ mLastPolyPolygonInfo.polygons.push_back(aPolyPolygon);
+ mLastPolyPolygonInfo.bounds.expand(aPolyPolygon.getB2DRange());
+ mLastPolyPolygonInfo.transparency = fTransparency;
+ return true;
+}
+
+// Tdf#140848 - basegfx::utils::mergeToSinglePolyPolygon() seems to have rounding
+// errors that sometimes cause it to merge incorrectly.
+static void roundPolygonPoints(basegfx::B2DPolyPolygon& polyPolygon)
+{
+ for (basegfx::B2DPolygon& polygon : polyPolygon)
+ {
+ polygon.makeUnique();
+ for (sal_uInt32 i = 0; i < polygon.count(); ++i)
+ polygon.setB2DPoint(i, basegfx::B2DPoint(basegfx::fround(polygon.getB2DPoint(i))));
+ // Control points are saved as vectors relative to points, so hopefully
+ // there's no need to round those.
+ }
+}
+
+void SkiaSalGraphicsImpl::checkPendingDrawing()
+{
+ if (mLastPolyPolygonInfo.polygons.size() != 0)
+ { // Flush any pending polygon drawing.
+ basegfx::B2DPolyPolygonVector polygons;
+ std::swap(polygons, mLastPolyPolygonInfo.polygons);
+ double transparency = mLastPolyPolygonInfo.transparency;
+ mLastPolyPolygonInfo.bounds.reset();
+ if (polygons.size() == 1)
+ performDrawPolyPolygon(polygons.front(), transparency, true);
+ else
+ {
+ for (basegfx::B2DPolyPolygon& p : polygons)
+ roundPolygonPoints(p);
+ performDrawPolyPolygon(basegfx::utils::mergeToSinglePolyPolygon(polygons), transparency,
+ true);
+ }
+ }
+}
+
+bool SkiaSalGraphicsImpl::drawPolyLine(const basegfx::B2DHomMatrix& rObjectToDevice,
+ const basegfx::B2DPolygon& rPolyLine, double fTransparency,
+ double fLineWidth, const std::vector<double>* pStroke,
+ basegfx::B2DLineJoin eLineJoin,
+ css::drawing::LineCap eLineCap, double fMiterMinimumAngle,
+ bool bPixelSnapHairline)
+{
+ if (!rPolyLine.count() || fTransparency < 0.0 || fTransparency > 1.0
+ || mLineColor == SALCOLOR_NONE)
+ {
+ return true;
+ }
+
+ preDraw();
+ SAL_INFO("vcl.skia.trace", "drawpolyline(" << this << "): " << rPolyLine << ":" << mLineColor);
+
+ // 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);
+ aPolyLine.transform(rObjectToDevice);
+ if (bPixelSnapHairline)
+ {
+ aPolyLine = basegfx::utils::snapPointsOfHorizontalOrVerticalEdges(aPolyLine);
+ }
+
+ SkPaint aPaint = makeLinePaint(fTransparency);
+
+ switch (eLineJoin)
+ {
+ case basegfx::B2DLineJoin::Bevel:
+ aPaint.setStrokeJoin(SkPaint::kBevel_Join);
+ break;
+ case basegfx::B2DLineJoin::Round:
+ aPaint.setStrokeJoin(SkPaint::kRound_Join);
+ break;
+ case basegfx::B2DLineJoin::NONE:
+ break;
+ case basegfx::B2DLineJoin::Miter:
+ aPaint.setStrokeJoin(SkPaint::kMiter_Join);
+ // convert miter minimum angle to miter limit
+ aPaint.setStrokeMiter(1.0 / std::sin(fMiterMinimumAngle / 2.0));
+ break;
+ }
+
+ switch (eLineCap)
+ {
+ case css::drawing::LineCap_ROUND:
+ aPaint.setStrokeCap(SkPaint::kRound_Cap);
+ break;
+ case css::drawing::LineCap_SQUARE:
+ aPaint.setStrokeCap(SkPaint::kSquare_Cap);
+ break;
+ default: // css::drawing::LineCap_BUTT:
+ aPaint.setStrokeCap(SkPaint::kButt_Cap);
+ break;
+ }
+
+ aPaint.setStrokeWidth(fLineWidth);
+ aPaint.setAntiAlias(mParent.getAntiAlias());
+ // See the tdf#134346 comment above.
+ const SkScalar posFix = mParent.getAntiAlias() ? toSkXYFix : 0;
+
+ if (pStroke && std::accumulate(pStroke->begin(), pStroke->end(), 0.0) != 0)
+ {
+ std::vector<SkScalar> intervals;
+ // Transform size by the matrix.
+ for (double stroke : *pStroke)
+ intervals.push_back((rObjectToDevice * basegfx::B2DVector(stroke, 0)).getLength());
+ aPaint.setPathEffect(SkDashPathEffect::Make(intervals.data(), intervals.size(), 0));
+ }
+
+ // Skia does not support basegfx::B2DLineJoin::NONE, so in that case batch only if lines
+ // are not wider than a pixel.
+ if (eLineJoin != basegfx::B2DLineJoin::NONE || fLineWidth <= 1.0)
+ {
+ SkPath aPath;
+ aPath.incReserve(aPolyLine.count() * 3); // because cubicTo is 3 elements
+ addPolygonToPath(aPolyLine, aPath);
+ aPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr);
+ addUpdateRegion(aPath.getBounds());
+ getDrawCanvas()->drawPath(aPath, aPaint);
+ }
+ else
+ {
+ sal_uInt32 nPoints = aPolyLine.count();
+ bool bClosed = aPolyLine.isClosed();
+ bool bHasCurves = aPolyLine.areControlPointsUsed();
+ for (sal_uInt32 j = 0; j < nPoints; ++j)
+ {
+ SkPath aPath;
+ aPath.incReserve(2 * 3); // because cubicTo is 3 elements
+ addPolygonToPath(aPolyLine, aPath, j, j + 1, nPoints, bClosed, bHasCurves);
+ aPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr);
+ addUpdateRegion(aPath.getBounds());
+ getDrawCanvas()->drawPath(aPath, aPaint);
+ }
+ }
+
+ postDraw();
+
+ return true;
+}
+
+bool SkiaSalGraphicsImpl::drawPolyLineBezier(sal_uInt32, const Point*, const PolyFlags*)
+{
+ return false;
+}
+
+bool SkiaSalGraphicsImpl::drawPolygonBezier(sal_uInt32, const Point*, const PolyFlags*)
+{
+ return false;
+}
+
+bool SkiaSalGraphicsImpl::drawPolyPolygonBezier(sal_uInt32, const sal_uInt32*, const Point* const*,
+ const PolyFlags* const*)
+{
+ return false;
+}
+
+void SkiaSalGraphicsImpl::copyArea(tools::Long nDestX, tools::Long nDestY, tools::Long nSrcX,
+ tools::Long nSrcY, tools::Long nSrcWidth, tools::Long nSrcHeight,
+ bool /*bWindowInvalidate*/)
+{
+ if (nDestX == nSrcX && nDestY == nSrcY)
+ return;
+ preDraw();
+ SAL_INFO("vcl.skia.trace", "copyarea("
+ << this << "): " << Point(nSrcX, nSrcY) << "->"
+ << SkIRect::MakeXYWH(nDestX, nDestY, nSrcWidth, nSrcHeight));
+ // Using SkSurface::draw() should be more efficient, but it's too buggy.
+ SalTwoRect rPosAry(nSrcX, nSrcY, nSrcWidth, nSrcHeight, nDestX, nDestY, nSrcWidth, nSrcHeight);
+ privateCopyBits(rPosAry, this);
+ postDraw();
+}
+
+void SkiaSalGraphicsImpl::copyBits(const SalTwoRect& rPosAry, SalGraphics* pSrcGraphics)
+{
+ preDraw();
+ SkiaSalGraphicsImpl* src;
+ if (pSrcGraphics)
+ {
+ assert(dynamic_cast<SkiaSalGraphicsImpl*>(pSrcGraphics->GetImpl()));
+ src = static_cast<SkiaSalGraphicsImpl*>(pSrcGraphics->GetImpl());
+ src->checkSurface();
+ src->flushDrawing();
+ }
+ else
+ {
+ src = this;
+ assert(mXorMode == XorMode::None);
+ }
+ auto srcDebug = [&]() -> std::string {
+ if (src == this)
+ return "(self)";
+ else
+ {
+ std::ostringstream stream;
+ stream << "(" << src << ")";
+ return stream.str();
+ }
+ };
+ SAL_INFO("vcl.skia.trace", "copybits(" << this << "): " << srcDebug() << ": " << rPosAry);
+ privateCopyBits(rPosAry, src);
+ postDraw();
+}
+
+void SkiaSalGraphicsImpl::privateCopyBits(const SalTwoRect& rPosAry, SkiaSalGraphicsImpl* src)
+{
+ assert(mXorMode == XorMode::None);
+ addUpdateRegion(SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth,
+ rPosAry.mnDestHeight));
+ SkPaint paint;
+ paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha
+ SkIRect srcRect = SkIRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth,
+ rPosAry.mnSrcHeight);
+ SkRect destRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth,
+ rPosAry.mnDestHeight);
+
+ if (!SkIRect::Intersects(srcRect, SkIRect::MakeWH(src->GetWidth(), src->GetHeight()))
+ || !SkRect::Intersects(destRect, SkRect::MakeWH(GetWidth(), GetHeight())))
+ return;
+
+ if (src == this)
+ {
+ // Copy-to-self means that we'd take a snapshot, which would refcount the data,
+ // and then drawing would result in copy in write, copying the entire surface.
+ // Try to copy less by making a snapshot of only what is needed.
+ // A complication here is that drawImageRect() can handle coordinates outside
+ // of surface fine, but makeImageSnapshot() will crop to the surface area,
+ // so do that manually here in order to adjust also destination rectangle.
+ if (srcRect.x() < 0 || srcRect.y() < 0)
+ {
+ destRect.fLeft += -srcRect.x();
+ destRect.fTop += -srcRect.y();
+ srcRect.adjust(-srcRect.x(), -srcRect.y(), 0, 0);
+ }
+ // Note that right() and bottom() are not inclusive (are outside of the rect).
+ if (srcRect.right() - 1 > GetWidth() || srcRect.bottom() - 1 > GetHeight())
+ {
+ destRect.fRight += GetWidth() - srcRect.right();
+ destRect.fBottom += GetHeight() - srcRect.bottom();
+ srcRect.adjust(0, 0, GetWidth() - srcRect.right(), GetHeight() - srcRect.bottom());
+ }
+ // Scaling for source coordinates must be done manually.
+ if (src->mScaling != 1)
+ srcRect = scaleRect(srcRect, src->mScaling);
+ sk_sp<SkImage> image = makeCheckedImageSnapshot(src->mSurface, srcRect);
+ srcRect.offset(-srcRect.x(), -srcRect.y());
+ getDrawCanvas()->drawImageRect(image, SkRect::Make(srcRect), destRect,
+ makeSamplingOptions(rPosAry, mScaling, src->mScaling),
+ &paint, SkCanvas::kFast_SrcRectConstraint);
+ }
+ else
+ {
+ // 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::Make(srcRect), destRect,
+ makeSamplingOptions(rPosAry, mScaling, src->mScaling),
+ &paint, SkCanvas::kFast_SrcRectConstraint);
+ }
+}
+
+bool SkiaSalGraphicsImpl::blendBitmap(const SalTwoRect& rPosAry, const SalBitmap& rBitmap)
+{
+ if (checkInvalidSourceOrDestination(rPosAry))
+ return false;
+
+ assert(dynamic_cast<const SkiaSalBitmap*>(&rBitmap));
+ const SkiaSalBitmap& rSkiaBitmap = static_cast<const SkiaSalBitmap&>(rBitmap);
+ // This is used by VirtualDevice in the alpha mode for the "alpha" layer which
+ // is actually one-minus-alpha (opacity). Therefore white=0xff=transparent,
+ // black=0x00=opaque. So the result is transparent only if both the inputs
+ // are transparent. Since for blending operations white=1.0 and black=0.0,
+ // kMultiply should handle exactly that (transparent*transparent=transparent,
+ // opaque*transparent=opaque). And guessing from the "floor" in TYPE_BLEND in opengl's
+ // combinedTextureFragmentShader.glsl, the layer is not even alpha values but
+ // simply yes-or-no mask.
+ // See also blendAlphaBitmap().
+ if (rSkiaBitmap.IsFullyOpaqueAsAlpha())
+ {
+ // Optimization. If the bitmap means fully opaque, it's all zero's. In CPU
+ // mode it should be faster to just copy instead of SkBlendMode::kMultiply.
+ drawBitmap(rPosAry, rSkiaBitmap);
+ }
+ else
+ drawBitmap(rPosAry, rSkiaBitmap, SkBlendMode::kMultiply);
+ return true;
+}
+
+bool SkiaSalGraphicsImpl::blendAlphaBitmap(const SalTwoRect& rPosAry,
+ const SalBitmap& rSourceBitmap,
+ const SalBitmap& rMaskBitmap,
+ const SalBitmap& rAlphaBitmap)
+{
+ if (checkInvalidSourceOrDestination(rPosAry))
+ return false;
+
+ assert(dynamic_cast<const SkiaSalBitmap*>(&rSourceBitmap));
+ assert(dynamic_cast<const SkiaSalBitmap*>(&rMaskBitmap));
+ assert(dynamic_cast<const SkiaSalBitmap*>(&rAlphaBitmap));
+ const SkiaSalBitmap& rSkiaSourceBitmap = static_cast<const SkiaSalBitmap&>(rSourceBitmap);
+ const SkiaSalBitmap& rSkiaMaskBitmap = static_cast<const SkiaSalBitmap&>(rMaskBitmap);
+ const SkiaSalBitmap& rSkiaAlphaBitmap = static_cast<const SkiaSalBitmap&>(rAlphaBitmap);
+
+ if (rSkiaMaskBitmap.IsFullyOpaqueAsAlpha())
+ {
+ // Optimization. If the mask of the bitmap to be blended means it's actually opaque,
+ // just draw the bitmap directly (that's what the math below will result in).
+ drawBitmap(rPosAry, rSkiaSourceBitmap);
+ return true;
+ }
+ // This was originally implemented for the OpenGL drawing method and it is poorly documented.
+ // The source and mask bitmaps are the usual data and alpha bitmaps, and 'alpha'
+ // is the "alpha" layer of the VirtualDevice (the alpha in VirtualDevice is also stored
+ // as a separate bitmap). Now if I understand it correctly these two alpha masks first need
+ // to be combined into the actual alpha mask to be used. The formula for TYPE_BLEND
+ // in opengl's combinedTextureFragmentShader.glsl is
+ // "result_alpha = 1.0 - (1.0 - floor(alpha)) * mask".
+ // See also blendBitmap().
+
+ 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
+ = SkShaders::Blend(SkBlendMode::kDstOut, rSkiaMaskBitmap.GetAlphaSkShader(samplingOptions),
+ rSkiaAlphaBitmap.GetAlphaSkShader(samplingOptions));
+ // And now draw the bitmap with "1 - x", where x is the "( 1 - alpha ) * mask".
+ sk_sp<SkShader> shader = SkShaders::Blend(SkBlendMode::kSrcOut, shaderAlpha,
+ rSkiaSourceBitmap.GetSkShader(samplingOptions));
+ drawShader(rPosAry, shader);
+ return true;
+}
+
+void SkiaSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap)
+{
+ if (checkInvalidSourceOrDestination(rPosAry))
+ return;
+
+ assert(dynamic_cast<const SkiaSalBitmap*>(&rSalBitmap));
+ const SkiaSalBitmap& rSkiaSourceBitmap = static_cast<const SkiaSalBitmap&>(rSalBitmap);
+
+ drawBitmap(rPosAry, rSkiaSourceBitmap);
+}
+
+void SkiaSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap,
+ const SalBitmap& rMaskBitmap)
+{
+ drawAlphaBitmap(rPosAry, rSalBitmap, rMaskBitmap);
+}
+
+void SkiaSalGraphicsImpl::drawMask(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap,
+ Color nMaskColor)
+{
+ 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, mScaling))));
+}
+
+std::shared_ptr<SalBitmap> SkiaSalGraphicsImpl::getBitmap(tools::Long nX, tools::Long nY,
+ tools::Long nWidth, tools::Long nHeight)
+{
+ SkiaZone zone;
+ checkSurface();
+ SAL_INFO("vcl.skia.trace",
+ "getbitmap(" << this << "): " << SkIRect::MakeXYWH(nX, nY, nWidth, nHeight));
+ flushDrawing();
+ // 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, scaleRect(SkIRect::MakeXYWH(nX, nY, nWidth, nHeight), mScaling));
+ std::shared_ptr<SkiaSalBitmap> bitmap = std::make_shared<SkiaSalBitmap>(image);
+ // 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. The DirectImage::Yes handling
+ // in mergeCacheBitmaps() should access the original unscaled bitmap data to avoid
+ // pointless scaling back and forth.
+ if (mScaling != 1)
+ {
+ if (!isUnitTestRunning())
+ bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, goodScalingQuality());
+ 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_")
+ || isUnitTestRunning("GraphicsRenderTest__testDrawRectAAWithLine"))
+ {
+ bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, goodScalingQuality());
+ }
+ else
+ bitmap->Scale(1.0 / mScaling, 1.0 / mScaling, BmpScaleFlag::NearestNeighbor);
+ }
+ }
+ return bitmap;
+}
+
+Color SkiaSalGraphicsImpl::getPixel(tools::Long nX, tools::Long nY)
+{
+ SkiaZone zone;
+ checkSurface();
+ SAL_INFO("vcl.skia.trace", "getpixel(" << this << "): " << Point(nX, nY));
+ flushDrawing();
+ // This is presumably slow, but getPixel() should be generally used only by unit tests.
+ SkBitmap bitmap;
+ if (!bitmap.tryAllocN32Pixels(mSurface->width(), mSurface->height()))
+ abort();
+ if (!mSurface->readPixels(bitmap, 0, 0))
+ abort();
+ return fromSkColor(bitmap.getColor(nX * mScaling, nY * mScaling));
+}
+
+void SkiaSalGraphicsImpl::invert(basegfx::B2DPolygon const& rPoly, SalInvert eFlags)
+{
+ preDraw();
+ SAL_INFO("vcl.skia.trace", "invert(" << this << "): " << rPoly << ":" << int(eFlags));
+ assert(mXorMode == XorMode::None);
+ SkPath aPath;
+ aPath.incReserve(rPoly.count());
+ addPolygonToPath(rPoly, aPath);
+ aPath.setFillType(SkPathFillType::kEvenOdd);
+ addUpdateRegion(aPath.getBounds());
+ SkAutoCanvasRestore autoRestore(getDrawCanvas(), true);
+ SkPaint aPaint;
+ // There's no blend mode for inverting as such, but kExclusion is 's + d - 2*s*d',
+ // so with d = 1.0 (all channels) it becomes effectively '1 - s', i.e. inverted color.
+ aPaint.setBlendMode(SkBlendMode::kExclusion);
+ aPaint.setColor(SkColorSetARGB(255, 255, 255, 255));
+ // TrackFrame just inverts a dashed path around the polygon
+ if (eFlags == SalInvert::TrackFrame)
+ {
+ // TrackFrame is not supposed to paint outside of the polygon (usually rectangle),
+ // but wider stroke width usually results in that, so ensure the requirement
+ // by clipping.
+ getDrawCanvas()->clipRect(aPath.getBounds(), SkClipOp::kIntersect, false);
+ aPaint.setStrokeWidth(2);
+ constexpr float intervals[] = { 4.0f, 4.0f };
+ aPaint.setStyle(SkPaint::kStroke_Style);
+ aPaint.setPathEffect(SkDashPathEffect::Make(intervals, SK_ARRAY_COUNT(intervals), 0));
+ }
+ else
+ {
+ aPaint.setStyle(SkPaint::kFill_Style);
+
+ // N50 inverts in checker pattern
+ if (eFlags == SalInvert::N50)
+ {
+ // This creates 2x2 checker pattern bitmap
+ // TODO Use createSkSurface() and cache the image
+ SkBitmap aBitmap;
+ aBitmap.allocN32Pixels(2, 2);
+ const SkPMColor white = SkPreMultiplyARGB(0xFF, 0xFF, 0xFF, 0xFF);
+ const SkPMColor black = SkPreMultiplyARGB(0xFF, 0x00, 0x00, 0x00);
+ SkPMColor* scanline;
+ scanline = aBitmap.getAddr32(0, 0);
+ *scanline++ = white;
+ *scanline++ = black;
+ scanline = aBitmap.getAddr32(0, 1);
+ *scanline++ = black;
+ *scanline++ = white;
+ aBitmap.setImmutable();
+ // The bitmap is repeated in both directions the checker pattern is as big
+ // as the polygon (usually rectangle)
+ aPaint.setShader(
+ aBitmap.makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat, SkSamplingOptions()));
+ }
+ }
+ getDrawCanvas()->drawPath(aPath, aPaint);
+ postDraw();
+}
+
+void SkiaSalGraphicsImpl::invert(tools::Long nX, tools::Long nY, tools::Long nWidth,
+ tools::Long nHeight, SalInvert eFlags)
+{
+ basegfx::B2DRectangle aRectangle(nX, nY, nX + nWidth, nY + nHeight);
+ auto aRect = basegfx::utils::createPolygonFromRect(aRectangle);
+ invert(aRect, eFlags);
+}
+
+void SkiaSalGraphicsImpl::invert(sal_uInt32 nPoints, const Point* pPointArray, SalInvert eFlags)
+{
+ basegfx::B2DPolygon aPolygon;
+ aPolygon.append(basegfx::B2DPoint(pPointArray[0].getX(), pPointArray[0].getY()), nPoints);
+ for (sal_uInt32 i = 1; i < nPoints; ++i)
+ {
+ aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPointArray[i].getX(), pPointArray[i].getY()));
+ }
+ aPolygon.setClosed(true);
+
+ invert(aPolygon, eFlags);
+}
+
+bool SkiaSalGraphicsImpl::drawEPS(tools::Long, tools::Long, tools::Long, tools::Long, void*,
+ sal_uInt32)
+{
+ return false;
+}
+
+// Create SkImage from a bitmap and possibly an alpha mask (the usual VCL one-minus-alpha),
+// with the given target size. Result will be possibly cached, unless disabled.
+// Especially in raster mode scaling and alpha blending may be expensive if done repeatedly.
+sk_sp<SkImage> SkiaSalGraphicsImpl::mergeCacheBitmaps(const SkiaSalBitmap& bitmap,
+ const SkiaSalBitmap* alphaBitmap,
+ const Size& targetSize)
+{
+ if (alphaBitmap)
+ assert(bitmap.GetSize() == alphaBitmap->GetSize());
+
+ if (targetSize.IsEmpty())
+ return {};
+ if (alphaBitmap && alphaBitmap->IsFullyOpaqueAsAlpha())
+ alphaBitmap = nullptr; // the alpha can be ignored
+ if (bitmap.PreferSkShader() && (!alphaBitmap || alphaBitmap->PreferSkShader()))
+ return {};
+
+ // If the bitmap has SkImage that matches the required size, try to use it, even
+ // if it doesn't match bitmap.GetSize(). This can happen with delayed scaling.
+ // This will catch cases such as some code pre-scaling the bitmap, which would make GetSkImage()
+ // scale, changing GetImageKey() in the process so we'd have to re-cache, and then we'd need
+ // to scale again in this function.
+ bool bitmapReady = false;
+ bool alphaBitmapReady = false;
+ if (const sk_sp<SkImage>& image = bitmap.GetSkImage(DirectImage::Yes))
+ {
+ assert(!bitmap.PreferSkShader());
+ if (imageSize(image) == targetSize)
+ bitmapReady = true;
+ }
+ // If the image usable and there's no alpha, then it matches exactly what's wanted.
+ if (bitmapReady && !alphaBitmap)
+ return bitmap.GetSkImage(DirectImage::Yes);
+ if (alphaBitmap)
+ {
+ if (!alphaBitmap->GetAlphaSkImage(DirectImage::Yes)
+ && alphaBitmap->GetSkImage(DirectImage::Yes)
+ && imageSize(alphaBitmap->GetSkImage(DirectImage::Yes)) == targetSize)
+ {
+ // There's a usable non-alpha image, try to convert it to alpha.
+ assert(!alphaBitmap->PreferSkShader());
+ const_cast<SkiaSalBitmap*>(alphaBitmap)->TryDirectConvertToAlphaNoScaling();
+ }
+ if (const sk_sp<SkImage>& image = alphaBitmap->GetAlphaSkImage(DirectImage::Yes))
+ {
+ assert(!alphaBitmap->PreferSkShader());
+ if (imageSize(image) == targetSize)
+ alphaBitmapReady = true;
+ }
+ }
+
+ if (bitmapReady && (!alphaBitmap || alphaBitmapReady))
+ {
+ // Try to find a cached image based on the already existing images.
+ OString key = makeCachedImageKey(bitmap, alphaBitmap, targetSize, DirectImage::Yes,
+ DirectImage::Yes);
+ if (sk_sp<SkImage> image = findCachedImage(key))
+ {
+ assert(imageSize(image) == targetSize);
+ return image;
+ }
+ }
+
+ // Probably not much point in caching of just doing a copy.
+ if (alphaBitmap == nullptr && targetSize == bitmap.GetSize())
+ return {};
+ // Image too small to be worth caching if not scaling.
+ if (targetSize == bitmap.GetSize() && targetSize.Width() < 100 && targetSize.Height() < 100)
+ return {};
+ // GPU-accelerated drawing with SkShader should be fast enough to not need caching.
+ if (isGPU())
+ {
+ // tdf#140925: But if this is such an extensive downscaling that caching the result
+ // would noticeably reduce amount of data processed by the GPU on repeated usage, do it.
+ int reduceRatio = bitmap.GetSize().Width() * bitmap.GetSize().Height() / targetSize.Width()
+ / targetSize.Height();
+ if (reduceRatio < 10)
+ return {};
+ }
+ // Do not cache the result if it would take most of the cache and thus get evicted soon.
+ if (targetSize.Width() * targetSize.Height() * 4 > maxImageCacheSize() * 0.7)
+ return {};
+
+ // Use ready direct image if they are both available, now even the size doesn't matter
+ // (we'll scale as necessary and it's better to scale from the original). Require only
+ // that they are the same size, or that one prefers a shader or doesn't exist
+ // (i.e. avoid two images of different size).
+ bitmapReady = bitmap.GetSkImage(DirectImage::Yes) != nullptr;
+ alphaBitmapReady = alphaBitmap && alphaBitmap->GetAlphaSkImage(DirectImage::Yes) != nullptr;
+ if (bitmapReady && alphaBitmap && !alphaBitmapReady && !alphaBitmap->PreferSkShader())
+ bitmapReady = false;
+ if (alphaBitmapReady && !bitmapReady && bitmap.PreferSkShader())
+ alphaBitmapReady = false;
+
+ DirectImage bitmapType = bitmapReady ? DirectImage::Yes : DirectImage::No;
+ DirectImage alphaBitmapType = alphaBitmapReady ? DirectImage::Yes : DirectImage::No;
+
+ // Try to find a cached result, this time after possible delayed scaling.
+ OString key = makeCachedImageKey(bitmap, alphaBitmap, targetSize, bitmapType, alphaBitmapType);
+ if (sk_sp<SkImage> image = findCachedImage(key))
+ {
+ assert(imageSize(image) == targetSize);
+ return image;
+ }
+
+ // In some cases (tdf#134237) the target size may be very large. In that case it's
+ // better to rely on Skia to clip and draw only the necessary, rather than prepare
+ // a very large image only to not use most of it. Do this only after checking whether
+ // the image is already cached, since it might have been already cached in a previous
+ // call that had the draw area large enough to be seen as worth caching.
+ const Size drawAreaSize = mClipRegion.GetBoundRect().GetSize() * mScaling;
+ if (targetSize.Width() > drawAreaSize.Width() || targetSize.Height() > drawAreaSize.Height())
+ {
+ // This is a bit tricky. The condition above just checks that at least a part of the resulting
+ // image will not be used (it's larger then our drawing area). But this may often happen
+ // when just scrolling a document with a large image, where the caching may very well be worth it.
+ // Since the problem is mainly the cost of upscaling and then the size of the resulting bitmap,
+ // compute a ratio of how much this is going to be scaled up, how much this is larger than
+ // the drawing area, and then refuse to cache if it's too much.
+ const double upscaleRatio
+ = std::max(1.0, 1.0 * targetSize.Width() / bitmap.GetSize().Width()
+ * targetSize.Height() / bitmap.GetSize().Height());
+ const double oversizeRatio = 1.0 * targetSize.Width() / drawAreaSize.Width()
+ * targetSize.Height() / drawAreaSize.Height();
+ const double ratio = upscaleRatio * oversizeRatio;
+ if (ratio > 4)
+ {
+ SAL_INFO("vcl.skia.trace", "mergecachebitmaps("
+ << this << "): not caching, ratio:" << ratio << ", "
+ << bitmap.GetSize() << "->" << targetSize << " in "
+ << drawAreaSize);
+ return {};
+ }
+ }
+
+ Size sourceSize;
+ if (bitmapReady)
+ sourceSize = imageSize(bitmap.GetSkImage(DirectImage::Yes));
+ else if (alphaBitmapReady)
+ sourceSize = imageSize(alphaBitmap->GetAlphaSkImage(DirectImage::Yes));
+ else
+ sourceSize = bitmap.GetSize();
+
+ // Generate a new result and cache it.
+ sk_sp<SkSurface> tmpSurface
+ = createSkSurface(targetSize, alphaBitmap ? kPremul_SkAlphaType : bitmap.alphaType());
+ if (!tmpSurface)
+ return nullptr;
+ SkCanvas* canvas = tmpSurface->getCanvas();
+ SkAutoCanvasRestore autoRestore(canvas, true);
+ SkPaint paint;
+ SkSamplingOptions samplingOptions;
+ if (targetSize != sourceSize)
+ {
+ SkMatrix matrix;
+ matrix.set(SkMatrix::kMScaleX, 1.0 * targetSize.Width() / sourceSize.Width());
+ matrix.set(SkMatrix::kMScaleY, 1.0 * targetSize.Height() / sourceSize.Height());
+ canvas->concat(matrix);
+ if (!isUnitTestRunning()) // unittests want exact pixel values
+ samplingOptions = makeSamplingOptions(matrix, 1);
+ }
+ if (alphaBitmap != nullptr)
+ {
+ canvas->clear(SK_ColorTRANSPARENT);
+ paint.setShader(
+ SkShaders::Blend(SkBlendMode::kDstOut, bitmap.GetSkShader(samplingOptions, bitmapType),
+ alphaBitmap->GetAlphaSkShader(samplingOptions, alphaBitmapType)));
+ canvas->drawPaint(paint);
+ }
+ else if (bitmap.PreferSkShader())
+ {
+ paint.setShader(bitmap.GetSkShader(samplingOptions, bitmapType));
+ canvas->drawPaint(paint);
+ }
+ else
+ canvas->drawImage(bitmap.GetSkImage(bitmapType), 0, 0, samplingOptions, &paint);
+ if (isGPU())
+ SAL_INFO("vcl.skia.trace", "mergecachebitmaps(" << this << "): caching GPU downscaling:"
+ << bitmap.GetSize() << "->" << targetSize);
+ sk_sp<SkImage> image = makeCheckedImageSnapshot(tmpSurface);
+ addCachedImage(key, image);
+ return image;
+}
+
+OString SkiaSalGraphicsImpl::makeCachedImageKey(const SkiaSalBitmap& bitmap,
+ const SkiaSalBitmap* alphaBitmap,
+ const Size& targetSize, DirectImage bitmapType,
+ DirectImage alphaBitmapType)
+{
+ OString key = OString::number(targetSize.Width()) + "x" + OString::number(targetSize.Height())
+ + "_" + bitmap.GetImageKey(bitmapType);
+ if (alphaBitmap)
+ key += "_" + alphaBitmap->GetAlphaImageKey(alphaBitmapType);
+ return key;
+}
+
+bool SkiaSalGraphicsImpl::drawAlphaBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSourceBitmap,
+ const SalBitmap& rAlphaBitmap)
+{
+ assert(dynamic_cast<const SkiaSalBitmap*>(&rSourceBitmap));
+ assert(dynamic_cast<const SkiaSalBitmap*>(&rAlphaBitmap));
+ const SkiaSalBitmap& rSkiaSourceBitmap = static_cast<const SkiaSalBitmap&>(rSourceBitmap);
+ const SkiaSalBitmap& rSkiaAlphaBitmap = static_cast<const SkiaSalBitmap&>(rAlphaBitmap);
+ // Use mergeCacheBitmaps(), which may decide to cache the result, avoiding repeated
+ // alpha blending or scaling.
+ SalTwoRect imagePosAry(rPosAry);
+ Size imageSize = rSourceBitmap.GetSize();
+ // If the bitmap will be scaled, prefer to do it in mergeCacheBitmaps(), if possible.
+ if ((rPosAry.mnSrcWidth != rPosAry.mnDestWidth || rPosAry.mnSrcHeight != rPosAry.mnDestHeight)
+ && rPosAry.mnSrcX == 0 && rPosAry.mnSrcY == 0
+ && rPosAry.mnSrcWidth == rSourceBitmap.GetSize().Width()
+ && rPosAry.mnSrcHeight == rSourceBitmap.GetSize().Height())
+ {
+ imagePosAry.mnSrcWidth = imagePosAry.mnDestWidth;
+ imagePosAry.mnSrcHeight = imagePosAry.mnDestHeight;
+ imageSize = Size(imagePosAry.mnSrcWidth, imagePosAry.mnSrcHeight);
+ }
+ sk_sp<SkImage> image
+ = mergeCacheBitmaps(rSkiaSourceBitmap, &rSkiaAlphaBitmap, imageSize * mScaling);
+ if (image)
+ drawImage(imagePosAry, image, mScaling);
+ else if (rSkiaAlphaBitmap.IsFullyOpaqueAsAlpha()
+ && !rSkiaSourceBitmap.PreferSkShader()) // alpha can be ignored
+ drawBitmap(rPosAry, rSkiaSourceBitmap);
+ else
+ drawShader(rPosAry,
+ SkShaders::Blend(
+ SkBlendMode::kDstOut, // VCL alpha is one-minus-alpha.
+ rSkiaSourceBitmap.GetSkShader(makeSamplingOptions(rPosAry, mScaling)),
+ rSkiaAlphaBitmap.GetAlphaSkShader(makeSamplingOptions(rPosAry, mScaling))));
+ return true;
+}
+
+void SkiaSalGraphicsImpl::drawBitmap(const SalTwoRect& rPosAry, const SkiaSalBitmap& bitmap,
+ SkBlendMode blendMode)
+{
+ // Use mergeCacheBitmaps(), which may decide to cache the result, avoiding repeated
+ // scaling.
+ SalTwoRect imagePosAry(rPosAry);
+ Size imageSize = bitmap.GetSize();
+ // If the bitmap will be scaled, prefer to do it in mergeCacheBitmaps(), if possible.
+ if ((rPosAry.mnSrcWidth != rPosAry.mnDestWidth || rPosAry.mnSrcHeight != rPosAry.mnDestHeight)
+ && rPosAry.mnSrcX == 0 && rPosAry.mnSrcY == 0
+ && rPosAry.mnSrcWidth == bitmap.GetSize().Width()
+ && rPosAry.mnSrcHeight == bitmap.GetSize().Height())
+ {
+ imagePosAry.mnSrcWidth = imagePosAry.mnDestWidth;
+ imagePosAry.mnSrcHeight = imagePosAry.mnDestHeight;
+ imageSize = Size(imagePosAry.mnSrcWidth, imagePosAry.mnSrcHeight);
+ }
+ sk_sp<SkImage> image = mergeCacheBitmaps(bitmap, nullptr, imageSize * mScaling);
+ if (image)
+ drawImage(imagePosAry, image, mScaling, blendMode);
+ else if (bitmap.PreferSkShader())
+ drawShader(rPosAry, bitmap.GetSkShader(makeSamplingOptions(rPosAry, mScaling)), blendMode);
+ else
+ drawImage(rPosAry, bitmap.GetSkImage(), 1, blendMode);
+}
+
+void SkiaSalGraphicsImpl::drawImage(const SalTwoRect& rPosAry, const sk_sp<SkImage>& aImage,
+ int srcScaling, SkBlendMode eBlendMode)
+{
+ SkRect aSourceRect
+ = SkRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight);
+ if (srcScaling != 1)
+ aSourceRect = scaleRect(aSourceRect, srcScaling);
+ SkRect aDestinationRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY,
+ rPosAry.mnDestWidth, rPosAry.mnDestHeight);
+
+ SkPaint aPaint = makeBitmapPaint();
+ aPaint.setBlendMode(eBlendMode);
+
+ preDraw();
+ SAL_INFO("vcl.skia.trace",
+ "drawimage(" << this << "): " << rPosAry << ":" << SkBlendMode_Name(eBlendMode));
+ addUpdateRegion(aDestinationRect);
+ getDrawCanvas()->drawImageRect(aImage, aSourceRect, aDestinationRect,
+ makeSamplingOptions(rPosAry, mScaling, srcScaling), &aPaint,
+ SkCanvas::kFast_SrcRectConstraint);
+ ++pendingOperationsToFlush; // tdf#136369
+ postDraw();
+}
+
+// SkShader can be used to merge multiple bitmaps with appropriate blend modes (e.g. when
+// merging a bitmap with its alpha mask).
+void SkiaSalGraphicsImpl::drawShader(const SalTwoRect& rPosAry, const sk_sp<SkShader>& shader,
+ SkBlendMode blendMode)
+{
+ preDraw();
+ SAL_INFO("vcl.skia.trace", "drawshader(" << this << "): " << rPosAry);
+ SkRect destinationRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth,
+ rPosAry.mnDestHeight);
+ addUpdateRegion(destinationRect);
+ SkPaint paint = makeBitmapPaint();
+ paint.setBlendMode(blendMode);
+ paint.setShader(shader);
+ SkCanvas* canvas = getDrawCanvas();
+ // Scaling needs to be done explicitly using a matrix.
+ SkAutoCanvasRestore autoRestore(canvas, true);
+ SkMatrix matrix = SkMatrix::Translate(rPosAry.mnDestX, rPosAry.mnDestY)
+ * SkMatrix::Scale(1.0 * rPosAry.mnDestWidth / rPosAry.mnSrcWidth,
+ 1.0 * rPosAry.mnDestHeight / rPosAry.mnSrcHeight)
+ * SkMatrix::Translate(-rPosAry.mnSrcX, -rPosAry.mnSrcY);
+#ifndef NDEBUG
+ // Handle floating point imprecisions, round p1 to 2 decimal places.
+ auto compareRounded = [](const SkPoint& p1, const SkPoint& p2) {
+ return rtl::math::round(p1.x(), 2) == p2.x() && rtl::math::round(p1.y(), 2) == p2.y();
+ };
+#endif
+ assert(compareRounded(matrix.mapXY(rPosAry.mnSrcX, rPosAry.mnSrcY),
+ SkPoint::Make(rPosAry.mnDestX, rPosAry.mnDestY)));
+ assert(compareRounded(
+ matrix.mapXY(rPosAry.mnSrcX + rPosAry.mnSrcWidth, rPosAry.mnSrcY + rPosAry.mnSrcHeight),
+ SkPoint::Make(rPosAry.mnDestX + rPosAry.mnDestWidth,
+ rPosAry.mnDestY + rPosAry.mnDestHeight)));
+ canvas->concat(matrix);
+ SkRect sourceRect
+ = SkRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight);
+ canvas->drawRect(sourceRect, paint);
+ postDraw();
+}
+
+bool SkiaSalGraphicsImpl::hasFastDrawTransformedBitmap() const
+{
+ // Return true even in raster mode, even that way Skia is faster than e.g. GraphicObject
+ // trying to handle stuff manually.
+ return true;
+}
+
+// Whether applying matrix needs image smoothing for the transformation.
+static bool matrixNeedsHighQuality(const SkMatrix& matrix)
+{
+ if (matrix.isIdentity())
+ return false;
+ if (matrix.isScaleTranslate())
+ {
+ if (abs(matrix.getScaleX()) == 1 && abs(matrix.getScaleY()) == 1)
+ return false; // Only at most flipping and keeping the size.
+ return true;
+ }
+ assert(!matrix.hasPerspective()); // we do not use this
+ if (matrix.getScaleX() == 0 && matrix.getScaleY() == 0)
+ {
+ // Rotating 90 or 270 degrees while keeping the size.
+ if ((matrix.getSkewX() == 1 && matrix.getSkewY() == -1)
+ || (matrix.getSkewX() == -1 && matrix.getSkewY() == 1))
+ return false;
+ }
+ return true;
+}
+
+namespace SkiaTests
+{
+bool matrixNeedsHighQuality(const SkMatrix& matrix) { return ::matrixNeedsHighQuality(matrix); }
+}
+
+bool SkiaSalGraphicsImpl::drawTransformedBitmap(const basegfx::B2DPoint& rNull,
+ const basegfx::B2DPoint& rX,
+ const basegfx::B2DPoint& rY,
+ const SalBitmap& rSourceBitmap,
+ const SalBitmap* pAlphaBitmap, double fAlpha)
+{
+ assert(dynamic_cast<const SkiaSalBitmap*>(&rSourceBitmap));
+ assert(!pAlphaBitmap || dynamic_cast<const SkiaSalBitmap*>(pAlphaBitmap));
+
+ const SkiaSalBitmap& rSkiaBitmap = static_cast<const SkiaSalBitmap&>(rSourceBitmap);
+ const SkiaSalBitmap* pSkiaAlphaBitmap = static_cast<const SkiaSalBitmap*>(pAlphaBitmap);
+
+ if (pSkiaAlphaBitmap && pSkiaAlphaBitmap->IsFullyOpaqueAsAlpha())
+ pSkiaAlphaBitmap = nullptr; // the alpha can be ignored
+
+ // Setup the image transformation,
+ // using the rNull, rX, rY points as destinations for the (0,0), (Width,0), (0,Height) source points.
+ const basegfx::B2DVector aXRel = rX - rNull;
+ const basegfx::B2DVector aYRel = rY - rNull;
+
+ preDraw();
+ SAL_INFO("vcl.skia.trace", "drawtransformedbitmap(" << this << "): " << rSourceBitmap.GetSize()
+ << " " << rNull << ":" << rX << ":" << rY);
+
+ addUpdateRegion(SkRect::MakeWH(GetWidth(), GetHeight())); // can't tell, use whole area
+ // Use mergeCacheBitmaps(), which may decide to cache the result, avoiding repeated
+ // alpha blending or scaling.
+ // The extra fAlpha blending is not cached, with the assumption that it usually gradually changes
+ // for each invocation.
+ // Pass size * mScaling to mergeCacheBitmaps() so that it prepares the size that will be needed
+ // after the mScaling-scaling matrix, but otherwise calculate everything else using the VCL coordinates.
+ Size imageSize(round(aXRel.getLength()), round(aYRel.getLength()));
+ sk_sp<SkImage> imageToDraw
+ = mergeCacheBitmaps(rSkiaBitmap, pSkiaAlphaBitmap, imageSize * mScaling);
+ if (imageToDraw)
+ {
+ SkMatrix matrix;
+ // Round sizes for scaling, so that sub-pixel differences don't
+ // trigger unnecessary scaling. Image has already been scaled
+ // by mergeCacheBitmaps() and we shouldn't scale here again
+ // unless the drawing is also skewed.
+ matrix.set(SkMatrix::kMScaleX, round(aXRel.getX()) / imageSize.Width());
+ matrix.set(SkMatrix::kMScaleY, round(aYRel.getY()) / imageSize.Height());
+ matrix.set(SkMatrix::kMSkewY, aXRel.getY() / imageSize.Width());
+ matrix.set(SkMatrix::kMSkewX, aYRel.getX() / imageSize.Height());
+ matrix.set(SkMatrix::kMTransX, rNull.getX());
+ matrix.set(SkMatrix::kMTransY, rNull.getY());
+ SkCanvas* canvas = getDrawCanvas();
+ SkAutoCanvasRestore autoRestore(canvas, true);
+ canvas->concat(matrix);
+ SkSamplingOptions samplingOptions;
+ // If the matrix changes geometry, we need to smooth-scale. If there's mScaling,
+ // that's already been handled by mergeCacheBitmaps().
+ if (matrixNeedsHighQuality(matrix))
+ samplingOptions = makeSamplingOptions(matrix, 1);
+ if (fAlpha == 1.0)
+ {
+ // Specify sizes to scale the image size back if needed (because of mScaling).
+ SkRect dstRect = SkRect::MakeWH(imageSize.Width(), imageSize.Height());
+ SkRect srcRect = SkRect::MakeWH(imageToDraw->width(), imageToDraw->height());
+ SkPaint paint = makeBitmapPaint();
+ canvas->drawImageRect(imageToDraw, srcRect, dstRect, samplingOptions, &paint,
+ SkCanvas::kFast_SrcRectConstraint);
+ }
+ else
+ {
+ SkPaint paint = makeBitmapPaint();
+ // Scale the image size back if needed.
+ SkMatrix scale = SkMatrix::Scale(1.0 / mScaling, 1.0 / mScaling);
+ paint.setShader(SkShaders::Blend(
+ SkBlendMode::kDstIn, imageToDraw->makeShader(samplingOptions, &scale),
+ SkShaders::Color(SkColorSetARGB(fAlpha * 255, 0, 0, 0))));
+ canvas->drawRect(SkRect::MakeWH(imageSize.Width(), imageSize.Height()), paint);
+ }
+ }
+ else
+ {
+ SkMatrix matrix;
+ const Size aSize = rSourceBitmap.GetSize();
+ matrix.set(SkMatrix::kMScaleX, aXRel.getX() / aSize.Width());
+ matrix.set(SkMatrix::kMScaleY, aYRel.getY() / aSize.Height());
+ matrix.set(SkMatrix::kMSkewY, aXRel.getY() / aSize.Width());
+ matrix.set(SkMatrix::kMSkewX, aYRel.getX() / aSize.Height());
+ matrix.set(SkMatrix::kMTransX, rNull.getX());
+ matrix.set(SkMatrix::kMTransY, rNull.getY());
+ SkCanvas* canvas = getDrawCanvas();
+ SkAutoCanvasRestore autoRestore(canvas, true);
+ canvas->concat(matrix);
+ SkSamplingOptions samplingOptions;
+ if (matrixNeedsHighQuality(matrix) || (mScaling != 1 && !isUnitTestRunning()))
+ samplingOptions = makeSamplingOptions(matrix, mScaling);
+ if (pSkiaAlphaBitmap)
+ {
+ SkPaint paint = makeBitmapPaint();
+ paint.setShader(SkShaders::Blend(SkBlendMode::kDstOut, // VCL alpha is one-minus-alpha.
+ rSkiaBitmap.GetSkShader(samplingOptions),
+ pSkiaAlphaBitmap->GetAlphaSkShader(samplingOptions)));
+ if (fAlpha != 1.0)
+ paint.setShader(
+ SkShaders::Blend(SkBlendMode::kDstIn, paint.refShader(),
+ SkShaders::Color(SkColorSetARGB(fAlpha * 255, 0, 0, 0))));
+ canvas->drawRect(SkRect::MakeWH(aSize.Width(), aSize.Height()), paint);
+ }
+ else if (rSkiaBitmap.PreferSkShader() || fAlpha != 1.0)
+ {
+ SkPaint paint = makeBitmapPaint();
+ paint.setShader(rSkiaBitmap.GetSkShader(samplingOptions));
+ if (fAlpha != 1.0)
+ paint.setShader(
+ SkShaders::Blend(SkBlendMode::kDstIn, paint.refShader(),
+ SkShaders::Color(SkColorSetARGB(fAlpha * 255, 0, 0, 0))));
+ canvas->drawRect(SkRect::MakeWH(aSize.Width(), aSize.Height()), paint);
+ }
+ else
+ {
+ SkPaint paint = makeBitmapPaint();
+ canvas->drawImage(rSkiaBitmap.GetSkImage(), 0, 0, samplingOptions, &paint);
+ }
+ }
+ postDraw();
+ return true;
+}
+
+bool SkiaSalGraphicsImpl::drawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth,
+ tools::Long nHeight, sal_uInt8 nTransparency)
+{
+ privateDrawAlphaRect(nX, nY, nWidth, nHeight, nTransparency / 100.0);
+ return true;
+}
+
+bool SkiaSalGraphicsImpl::drawGradient(const tools::PolyPolygon& rPolyPolygon,
+ const Gradient& rGradient)
+{
+ if (rGradient.GetStyle() != GradientStyle::Linear
+ && rGradient.GetStyle() != GradientStyle::Axial
+ && rGradient.GetStyle() != GradientStyle::Radial)
+ return false; // unsupported
+ if (rGradient.GetSteps() != 0)
+ return false; // We can't tell Skia how many colors to use in the gradient.
+ preDraw();
+ SAL_INFO("vcl.skia.trace", "drawgradient(" << this << "): " << rPolyPolygon.getB2DPolyPolygon()
+ << ":" << static_cast<int>(rGradient.GetStyle()));
+ tools::Rectangle boundRect(rPolyPolygon.GetBoundRect());
+ if (boundRect.IsEmpty())
+ return true;
+ SkPath path;
+ if (rPolyPolygon.IsRect())
+ {
+ // Rect->Polygon conversion loses the right and bottom edge, fix that.
+ path.addRect(SkRect::MakeXYWH(boundRect.getX(), boundRect.getY(), boundRect.GetWidth(),
+ boundRect.GetHeight()));
+ boundRect.AdjustRight(1);
+ boundRect.AdjustBottom(1);
+ }
+ else
+ addPolyPolygonToPath(rPolyPolygon.getB2DPolyPolygon(), path);
+ path.setFillType(SkPathFillType::kEvenOdd);
+ addUpdateRegion(path.getBounds());
+
+ Gradient aGradient(rGradient);
+ tools::Rectangle aBoundRect;
+ Point aCenter;
+ aGradient.SetAngle(aGradient.GetAngle() + 2700_deg10);
+ aGradient.GetBoundRect(boundRect, aBoundRect, aCenter);
+
+ SkColor startColor
+ = toSkColorWithIntensity(rGradient.GetStartColor(), rGradient.GetStartIntensity());
+ SkColor endColor = toSkColorWithIntensity(rGradient.GetEndColor(), rGradient.GetEndIntensity());
+
+ sk_sp<SkShader> shader;
+ if (rGradient.GetStyle() == GradientStyle::Linear)
+ {
+ tools::Polygon aPoly(aBoundRect);
+ aPoly.Rotate(aCenter, aGradient.GetAngle() % 3600_deg10);
+ SkPoint points[2] = { SkPoint::Make(toSkX(aPoly[0].X()), toSkY(aPoly[0].Y())),
+ SkPoint::Make(toSkX(aPoly[1].X()), toSkY(aPoly[1].Y())) };
+ SkColor colors[2] = { startColor, endColor };
+ SkScalar pos[2] = { SkDoubleToScalar(aGradient.GetBorder() / 100.0), 1.0 };
+ shader = SkGradientShader::MakeLinear(points, colors, pos, 2, SkTileMode::kClamp);
+ }
+ else if (rGradient.GetStyle() == GradientStyle::Axial)
+ {
+ tools::Polygon aPoly(aBoundRect);
+ aPoly.Rotate(aCenter, aGradient.GetAngle() % 3600_deg10);
+ SkPoint points[2] = { SkPoint::Make(toSkX(aPoly[0].X()), toSkY(aPoly[0].Y())),
+ SkPoint::Make(toSkX(aPoly[1].X()), toSkY(aPoly[1].Y())) };
+ SkColor colors[3] = { endColor, startColor, endColor };
+ SkScalar border = SkDoubleToScalar(aGradient.GetBorder() / 100.0);
+ SkScalar pos[3]
+ = { std::min<SkScalar>(border, 0.5), 0.5, std::max<SkScalar>(1 - border, 0.5) };
+ shader = SkGradientShader::MakeLinear(points, colors, pos, 3, SkTileMode::kClamp);
+ }
+ else
+ {
+ // Move the center by (-1,-1) (the default VCL algorithm is a bit off-center that way,
+ // Skia is the opposite way).
+ SkPoint center = SkPoint::Make(toSkX(aCenter.X()) - 1, toSkY(aCenter.Y()) - 1);
+ SkScalar radius = std::max(aBoundRect.GetWidth() / 2.0, aBoundRect.GetHeight() / 2.0);
+ SkColor colors[2] = { endColor, startColor };
+ SkScalar pos[2] = { SkDoubleToScalar(aGradient.GetBorder() / 100.0), 1.0 };
+ shader = SkGradientShader::MakeRadial(center, radius, colors, pos, 2, SkTileMode::kClamp);
+ }
+
+ SkPaint paint = makeGradientPaint();
+ paint.setAntiAlias(mParent.getAntiAlias());
+ paint.setShader(shader);
+ getDrawCanvas()->drawPath(path, paint);
+ postDraw();
+ return true;
+}
+
+bool SkiaSalGraphicsImpl::implDrawGradient(const basegfx::B2DPolyPolygon& rPolyPolygon,
+ const SalGradient& rGradient)
+{
+ preDraw();
+ SAL_INFO("vcl.skia.trace",
+ "impldrawgradient(" << this << "): " << rPolyPolygon << ":" << rGradient.maPoint1
+ << "->" << rGradient.maPoint2 << ":" << rGradient.maStops.size());
+
+ SkPath path;
+ addPolyPolygonToPath(rPolyPolygon, path);
+ path.setFillType(SkPathFillType::kEvenOdd);
+ addUpdateRegion(path.getBounds());
+
+ SkPoint points[2]
+ = { SkPoint::Make(toSkX(rGradient.maPoint1.getX()), toSkY(rGradient.maPoint1.getY())),
+ SkPoint::Make(toSkX(rGradient.maPoint2.getX()), toSkY(rGradient.maPoint2.getY())) };
+ std::vector<SkColor> colors;
+ std::vector<SkScalar> pos;
+ for (const SalGradientStop& stop : rGradient.maStops)
+ {
+ colors.emplace_back(toSkColor(stop.maColor));
+ pos.emplace_back(stop.mfOffset);
+ }
+ sk_sp<SkShader> shader = SkGradientShader::MakeLinear(points, colors.data(), pos.data(),
+ colors.size(), SkTileMode::kDecal);
+ SkPaint paint = makeGradientPaint();
+ paint.setAntiAlias(mParent.getAntiAlias());
+ paint.setShader(shader);
+ getDrawCanvas()->drawPath(path, paint);
+ postDraw();
+ return true;
+}
+
+static double toRadian(Degree10 degree10th) { return toRadians(3600_deg10 - degree10th); }
+static double toCos(Degree10 degree10th) { return SkScalarCos(toRadian(degree10th)); }
+static double toSin(Degree10 degree10th) { return SkScalarSin(toRadian(degree10th)); }
+
+void SkiaSalGraphicsImpl::drawGenericLayout(const GenericSalLayout& layout, Color textColor,
+ const SkFont& font, const SkFont& verticalFont)
+{
+ SkiaZone zone;
+ std::vector<SkGlyphID> glyphIds;
+ std::vector<SkRSXform> glyphForms;
+ std::vector<bool> verticals;
+ glyphIds.reserve(256);
+ glyphForms.reserve(256);
+ verticals.reserve(256);
+ DevicePoint aPos;
+ const GlyphItem* pGlyph;
+ int nStart = 0;
+ while (layout.GetNextGlyph(&pGlyph, aPos, nStart))
+ {
+ glyphIds.push_back(pGlyph->glyphId());
+ Degree10 angle = layout.GetOrientation();
+ if (pGlyph->IsVertical())
+ angle += 900_deg10;
+ SkRSXform form = SkRSXform::Make(toCos(angle), toSin(angle), aPos.getX(), aPos.getY());
+ glyphForms.emplace_back(std::move(form));
+ verticals.emplace_back(pGlyph->IsVertical());
+ }
+ if (glyphIds.empty())
+ return;
+
+ preDraw();
+ auto getBoundRect = [&layout]() {
+ tools::Rectangle rect;
+ layout.GetBoundRect(rect);
+ return rect;
+ };
+ SAL_INFO("vcl.skia.trace", "drawtextblob(" << this << "): " << getBoundRect() << ", "
+ << glyphIds.size() << " glyphs, " << textColor);
+
+ // Vertical glyphs need a different font, so split drawing into runs that each
+ // draw only consecutive horizontal or vertical glyphs.
+ std::vector<bool>::const_iterator pos = verticals.cbegin();
+ std::vector<bool>::const_iterator end = verticals.cend();
+ while (pos != end)
+ {
+ bool verticalRun = *pos;
+ std::vector<bool>::const_iterator rangeEnd = std::find(pos + 1, end, !verticalRun);
+ size_t index = pos - verticals.cbegin();
+ size_t count = rangeEnd - pos;
+ sk_sp<SkTextBlob> textBlob = SkTextBlob::MakeFromRSXform(
+ glyphIds.data() + index, count * sizeof(SkGlyphID), glyphForms.data() + index,
+ verticalRun ? verticalFont : font, SkTextEncoding::kGlyphID);
+ addUpdateRegion(textBlob->bounds());
+ SkPaint paint = makeTextPaint(textColor);
+ getDrawCanvas()->drawTextBlob(textBlob, 0, 0, paint);
+ pos = rangeEnd;
+ }
+ postDraw();
+}
+
+bool SkiaSalGraphicsImpl::supportsOperation(OutDevSupportType eType) const
+{
+ switch (eType)
+ {
+ case OutDevSupportType::B2DDraw:
+ case OutDevSupportType::TransparentRect:
+ return true;
+ default:
+ return false;
+ }
+}
+
+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;
+}
+
+void SkiaSalGraphicsImpl::dump(const char* file) const
+{
+ assert(mSurface.get());
+ SkiaHelper::dump(mSurface, file);
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/skia/osx/bitmap.cxx b/vcl/skia/osx/bitmap.cxx
new file mode 100644
index 000000000..16d32191a
--- /dev/null
+++ b/vcl/skia/osx/bitmap.cxx
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * Some of this code is based on Skia source code, covered by the following
+ * license notice (see readlicense_oo for the full license):
+ *
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ */
+
+#include <skia/osx/bitmap.hxx>
+
+#include <vcl/bitmapex.hxx>
+#include <vcl/image.hxx>
+
+#include <skia/salbmp.hxx>
+#include <osx/saldata.hxx>
+
+#include <SkBitmap.h>
+#include <SkCanvas.h>
+
+using namespace SkiaHelper;
+
+namespace SkiaHelper
+{
+CGImageRef createCGImage(const Image& rImage)
+{
+ BitmapEx bitmapEx(rImage.GetBitmapEx());
+ Bitmap bitmap(bitmapEx.GetBitmap());
+
+ if (bitmap.IsEmpty() || !bitmap.ImplGetSalBitmap())
+ return nullptr;
+
+ assert(dynamic_cast<SkiaSalBitmap*>(bitmap.ImplGetSalBitmap().get()) != nullptr);
+ SkiaSalBitmap* skiaBitmap = static_cast<SkiaSalBitmap*>(bitmap.ImplGetSalBitmap().get());
+
+ SkBitmap targetBitmap;
+ if (!targetBitmap.tryAllocPixels(
+ SkImageInfo::Make(bitmap.GetSizePixel().getWidth(), bitmap.GetSizePixel().getHeight(),
+ kRGBA_8888_SkColorType, kPremul_SkAlphaType)))
+ return nullptr;
+ SkPaint paint;
+ paint.setBlendMode(SkBlendMode::kSrc); // set as is
+ SkMatrix matrix; // The image is needed upside-down.
+ matrix.preTranslate(0, targetBitmap.height());
+ matrix.setConcat(matrix, SkMatrix::Scale(1, -1));
+
+ if (!bitmapEx.IsAlpha())
+ {
+ SkCanvas canvas(targetBitmap);
+ canvas.concat(matrix);
+ canvas.drawImage(skiaBitmap->GetSkImage(), 0, 0, SkSamplingOptions(), &paint);
+ }
+ else
+ {
+ AlphaMask alpha(bitmapEx.GetAlpha());
+ Bitmap alphaBitmap(alpha.GetBitmap());
+ assert(dynamic_cast<SkiaSalBitmap*>(alphaBitmap.ImplGetSalBitmap().get()) != nullptr);
+ SkiaSalBitmap* skiaAlpha
+ = static_cast<SkiaSalBitmap*>(alphaBitmap.ImplGetSalBitmap().get());
+#if 0
+ // Drawing to a bitmap using a shader from a GPU-backed image fails silently.
+ // https://bugs.chromium.org/p/skia/issues/detail?id=12685
+ paint.setShader(SkShaders::Blend(SkBlendMode::kDstOut,
+ skiaBitmap->GetSkShader(SkSamplingOptions()),
+ skiaAlpha->GetAlphaSkShader(SkSamplingOptions())));
+#else
+ sk_sp<SkImage> imB = skiaBitmap->GetSkImage()->makeNonTextureImage();
+ sk_sp<SkImage> imA = skiaAlpha->GetAlphaSkImage()->makeNonTextureImage();
+ paint.setShader(SkShaders::Blend(SkBlendMode::kDstOut, imB->makeShader(SkSamplingOptions()),
+ imA->makeShader(SkSamplingOptions())));
+#endif
+ SkCanvas canvas(targetBitmap);
+ canvas.concat(matrix);
+ canvas.drawPaint(paint);
+ }
+
+ CGContextRef context = CGBitmapContextCreate(
+ targetBitmap.getAddr32(0, 0), targetBitmap.width(), targetBitmap.height(), 8,
+ targetBitmap.rowBytes(), GetSalData()->mxRGBSpace, kCGImageAlphaPremultipliedLast);
+ if (!context)
+ return nullptr;
+ CGImageRef screenImage = CGBitmapContextCreateImage(context);
+ CFRelease(context);
+ return screenImage;
+}
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/skia/osx/gdiimpl.cxx b/vcl/skia/osx/gdiimpl.cxx
new file mode 100644
index 000000000..ffc84ae89
--- /dev/null
+++ b/vcl/skia/osx/gdiimpl.cxx
@@ -0,0 +1,345 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * Some of this code is based on Skia source code, covered by the following
+ * license notice (see readlicense_oo for the full license):
+ *
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ */
+
+#include <sal/config.h>
+
+#include <skia/osx/gdiimpl.hxx>
+
+#include <skia/utils.hxx>
+#include <skia/zone.hxx>
+
+#include <tools/sk_app/mac/WindowContextFactory_mac.h>
+
+#include <quartz/ctfonts.hxx>
+
+#include <SkBitmap.h>
+#include <SkCanvas.h>
+#include <SkFont.h>
+#include <SkFontMgr_mac_ct.h>
+#include <SkTypeface_mac.h>
+
+using namespace SkiaHelper;
+
+AquaSkiaSalGraphicsImpl::AquaSkiaSalGraphicsImpl(AquaSalGraphics& rParent,
+ AquaSharedAttributes& rShared)
+ : SkiaSalGraphicsImpl(rParent, rShared.mpFrame)
+ , AquaGraphicsBackendBase(rShared)
+{
+ Init(); // mac code doesn't call Init()
+}
+
+AquaSkiaSalGraphicsImpl::~AquaSkiaSalGraphicsImpl()
+{
+ DeInit(); // mac code doesn't call DeInit()
+}
+
+void AquaSkiaSalGraphicsImpl::freeResources() {}
+
+void AquaSkiaSalGraphicsImpl::createWindowSurfaceInternal(bool forceRaster)
+{
+ assert(!mWindowContext);
+ assert(!mSurface);
+ SkiaZone zone;
+ sk_app::DisplayParams displayParams;
+ 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() * mScaling, GetHeight() * mScaling);
+ break;
+ case RenderMetal:
+ mWindowContext
+ = sk_app::window_context_factory::MakeMetalForMac(macWindow, displayParams);
+ // Like with other GPU contexts, create a proxy offscreen surface (see
+ // flushSurfaceToWindowContext()). Here it's additionally needed because
+ // it appears that Metal surfaces cannot be read from, which would break things
+ // like copyArea().
+ if (mWindowContext)
+ mSurface = createSkSurface(GetWidth() * mScaling, GetHeight() * mScaling);
+ break;
+ case RenderVulkan:
+ abort();
+ break;
+ }
+}
+
+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(); }
+
+void AquaSkiaSalGraphicsImpl::flushSurfaceToWindowContext()
+{
+ if (!isGPU())
+ flushSurfaceToScreenCG();
+ else
+ SkiaSalGraphicsImpl::flushSurfaceToWindowContext();
+}
+
+constexpr static uint32_t toCGBitmapType(SkColorType color, SkAlphaType alpha)
+{
+ if (alpha == kPremul_SkAlphaType)
+ {
+ return color == kBGRA_8888_SkColorType
+ ? (uint32_t(kCGImageAlphaPremultipliedFirst)
+ | uint32_t(kCGBitmapByteOrder32Little))
+ : (uint32_t(kCGImageAlphaPremultipliedLast) | uint32_t(kCGBitmapByteOrder32Big));
+ }
+ else
+ {
+ assert(alpha == kOpaque_SkAlphaType);
+ return color == kBGRA_8888_SkColorType
+ ? (uint32_t(kCGImageAlphaNoneSkipFirst) | uint32_t(kCGBitmapByteOrder32Little))
+ : (uint32_t(kCGImageAlphaNoneSkipLast) | uint32_t(kCGBitmapByteOrder32Big));
+ }
+}
+
+// For Raster we use our own screen blitting (see above).
+void AquaSkiaSalGraphicsImpl::flushSurfaceToScreenCG()
+{
+ // Based on AquaGraphicsBackend::drawBitmap().
+ if (!mrShared.checkContext())
+ return;
+
+ assert(mSurface.get());
+ // Do not use sub-rect, it creates copies of the data.
+ sk_sp<SkImage> image = makeCheckedImageSnapshot(mSurface);
+ 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.
+ if (pixmap.bounds() != mDirtyRect && pixmap.bounds().bottom() == mDirtyRect.bottom())
+ {
+ // HACK for tdf#145843: If mDirtyRect includes the last line but not the first pixel of it,
+ // then the rowBytes() trick would lead to the GC* functions thinking that even pixels after
+ // the pixmap data belong to the area (since the shifted x()+rowBytes() points there) and
+ // at least on Intel Mac they would actually read those data, even though I see no good reason
+ // to do that, as that's beyond the x()+width() for the last line. That could be handled
+ // by creating a subset SkImage (which as is said above copies data), or set the x coordinate
+ // to 0, which will then make rowBytes() match the actual data.
+ mDirtyRect.fLeft = 0;
+ assert(mDirtyRect.width() == pixmap.bounds().width());
+ }
+ CGContextRef context = CGBitmapContextCreate(
+ 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);
+ if (!screenImage)
+ {
+ CGContextRelease(context);
+ SAL_WARN("vcl.skia", "flushSurfaceToScreenGC(): Failed to allocate screen image");
+ return;
+ }
+ 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())
+ {
+ // 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());
+}
+
+bool AquaSkiaSalGraphicsImpl::drawNativeControl(ControlType nType, ControlPart nPart,
+ const tools::Rectangle& rControlRegion,
+ ControlState nState, const ImplControlValue& aValue)
+{
+ // rControlRegion is not the whole area that the control should be painted to (e.g. highlight
+ // around focused lineedit is outside of it). Since we draw to a temporary bitmap, we need tofind out
+ // the real size. Using getNativeControlRegion() might seem like the function to call, but we need
+ // the other direction - what is called rControlRegion here is rNativeContentRegion in that function
+ // what's called rControlRegion there is what we need here. Moreover getNativeControlRegion()
+ // in some cases returns a fixed size that does not depend on its input, so we have no way to
+ // actually find out what the original size was (or maybe the function is kind of broken, I don't know).
+ // So, add a generous margin and hope it's enough.
+ tools::Rectangle boundingRegion(rControlRegion);
+ boundingRegion.expand(50 * mScaling);
+ // Do a scaled bitmap in HiDPI in order not to lose precision.
+ const tools::Long width = boundingRegion.GetWidth() * mScaling;
+ const tools::Long height = boundingRegion.GetHeight() * mScaling;
+ const size_t bytes = width * height * 4;
+ std::unique_ptr<sal_uInt8[]> data(new sal_uInt8[bytes]);
+ memset(data.get(), 0, bytes);
+ CGContextRef context = CGBitmapContextCreate(
+ data.get(), width, height, 8, width * 4, GetSalData()->mxRGBSpace,
+ toCGBitmapType(mSurface->imageInfo().colorType(), kPremul_SkAlphaType));
+ if (!context)
+ {
+ SAL_WARN("vcl.skia", "drawNativeControl(): Failed to allocate bitmap context");
+ return false;
+ }
+ // Setup context state for drawing (performDrawNativeControl() e.g. fills background in some cases).
+ CGContextSetFillColorSpace(context, GetSalData()->mxRGBSpace);
+ CGContextSetStrokeColorSpace(context, GetSalData()->mxRGBSpace);
+ RGBAColor lineColor(mLineColor);
+ CGContextSetRGBStrokeColor(context, lineColor.GetRed(), lineColor.GetGreen(),
+ lineColor.GetBlue(), lineColor.GetAlpha());
+ RGBAColor fillColor(mFillColor);
+ CGContextSetRGBFillColor(context, fillColor.GetRed(), fillColor.GetGreen(), fillColor.GetBlue(),
+ fillColor.GetAlpha());
+ // Adjust for our drawn-to coordinates in the bitmap.
+ tools::Rectangle movedRegion(Point(rControlRegion.getX() - boundingRegion.getX(),
+ rControlRegion.getY() - boundingRegion.getY()),
+ rControlRegion.GetSize());
+ // Flip drawing upside down.
+ CGContextTranslateCTM(context, 0, height);
+ CGContextScaleCTM(context, 1, -1);
+ // And possibly scale the native drawing.
+ CGContextScaleCTM(context, mScaling, mScaling);
+ bool bOK = performDrawNativeControl(nType, nPart, movedRegion, nState, aValue, context,
+ mrShared.mpFrame);
+ CGContextRelease(context);
+ if (bOK)
+ {
+ SkBitmap bitmap;
+ if (!bitmap.installPixels(SkImageInfo::Make(width, height,
+ mSurface->imageInfo().colorType(),
+ kPremul_SkAlphaType),
+ data.get(), width * 4))
+ abort();
+
+ preDraw();
+ SAL_INFO("vcl.skia.trace", "drawnativecontrol(" << this << "): " << rControlRegion << ":"
+ << int(nType) << "/" << int(nPart));
+ tools::Rectangle updateRect = boundingRegion;
+ // For background update only part that is not clipped, the same
+ // as in AquaGraphicsBackend::drawNativeControl().
+ if (nType == ControlType::WindowBackground)
+ updateRect.Intersection(mClipRegion.GetBoundRect());
+ addUpdateRegion(SkRect::MakeXYWH(updateRect.getX(), updateRect.getY(),
+ updateRect.GetWidth(), updateRect.GetHeight()));
+ SkRect drawRect = SkRect::MakeXYWH(boundingRegion.getX(), boundingRegion.getY(),
+ boundingRegion.GetWidth(), boundingRegion.GetHeight());
+ assert(drawRect.width() * mScaling == bitmap.width()); // no scaling should be needed
+ getDrawCanvas()->drawImageRect(bitmap.asImage(), drawRect, SkSamplingOptions());
+ ++pendingOperationsToFlush; // tdf#136369
+ postDraw();
+ }
+ return bOK;
+}
+
+void AquaSkiaSalGraphicsImpl::drawTextLayout(const GenericSalLayout& rLayout,
+ bool bSubpixelPositioning)
+{
+ const CoreTextStyle& rStyle = *static_cast<const CoreTextStyle*>(&rLayout.GetFont());
+ const vcl::font::FontSelectPattern& rFontSelect = rStyle.GetFontSelectPattern();
+ int nHeight = rFontSelect.mnHeight;
+ int nWidth = rFontSelect.mnWidth ? rFontSelect.mnWidth : nHeight;
+ if (nWidth == 0 || nHeight == 0)
+ {
+ SAL_WARN("vcl.skia", "DrawTextLayout(): rFontSelect.mnHeight is zero!?");
+ return;
+ }
+
+ if (!fontManager)
+ {
+ SystemFontList* fontList = GetCoretextFontList();
+ if (fontList == nullptr)
+ {
+ SAL_WARN("vcl.skia", "DrawTextLayout(): No coretext font list");
+ fontManager = SkFontMgr_New_CoreText(nullptr);
+ }
+ else
+ {
+ fontManager = SkFontMgr_New_CoreText(fontList->fontCollection());
+ }
+ }
+
+ CTFontRef pFont
+ = static_cast<CTFontRef>(CFDictionaryGetValue(rStyle.GetStyleDict(), kCTFontAttributeName));
+ sk_sp<SkTypeface> typeface = SkMakeTypefaceFromCTFont(pFont);
+ SkFont font(typeface);
+ font.setSize(nHeight);
+ // font.setScaleX(rStyle.mfFontStretch); TODO
+ if (rStyle.mbFauxBold)
+ font.setEmbolden(true);
+
+ SkFont::Edging ePreferredAliasing
+ = bSubpixelPositioning ? SkFont::Edging::kSubpixelAntiAlias : SkFont::Edging::kAntiAlias;
+ if (bSubpixelPositioning)
+ {
+ // note that SkFont defaults to a BaselineSnap of true, so I think really only
+ // subpixel in text direction
+ font.setSubpixel(true);
+ }
+ font.setEdging(mrShared.mbNonAntialiasedText ? SkFont::Edging::kAlias : ePreferredAliasing);
+
+ // Vertical font, use width as "height".
+ SkFont verticalFont(font);
+ verticalFont.setSize(nHeight);
+ // verticalFont.setSize(nWidth); TODO
+ // verticalFont.setScaleX(1.0 * nHeight / nWidth);
+
+ drawGenericLayout(rLayout, mrShared.maTextColor, font, verticalFont);
+}
+
+namespace
+{
+std::unique_ptr<sk_app::WindowContext> createMetalWindowContext(bool /*temporary*/)
+{
+ sk_app::DisplayParams displayParams;
+ sk_app::window_context_factory::MacWindowInfo macWindow;
+ macWindow.fMainView = nullptr;
+ return sk_app::window_context_factory::MakeMetalForMac(macWindow, displayParams);
+}
+}
+
+void AquaSkiaSalGraphicsImpl::prepareSkia() { SkiaHelper::prepareSkia(createMetalWindowContext); }
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/skia/salbmp.cxx b/vcl/skia/salbmp.cxx
new file mode 100644
index 000000000..df3536b4d
--- /dev/null
+++ b/vcl/skia/salbmp.cxx
@@ -0,0 +1,1406 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <skia/salbmp.hxx>
+
+#include <o3tl/safeint.hxx>
+#include <tools/helpers.hxx>
+#include <boost/smart_ptr/make_shared.hpp>
+
+#include <salgdi.hxx>
+#include <salinst.hxx>
+#include <scanlinewriter.hxx>
+#include <svdata.hxx>
+#include <bitmap/bmpfast.hxx>
+#include <vcl/BitmapReadAccess.hxx>
+
+#include <skia/utils.hxx>
+#include <skia/zone.hxx>
+
+#include <SkCanvas.h>
+#include <SkImage.h>
+#include <SkPixelRef.h>
+#include <SkSurface.h>
+#include <SkSwizzle.h>
+#include <SkColorFilter.h>
+#include <SkColorMatrix.h>
+#include <skia_opts.hxx>
+
+#ifdef DBG_UTIL
+#include <fstream>
+#define CANARY "skia-canary"
+#endif
+
+using namespace SkiaHelper;
+
+// As constexpr here, evaluating it directly in code makes Clang warn about unreachable code.
+constexpr bool kN32_SkColorTypeIsBGRA = (kN32_SkColorType == kBGRA_8888_SkColorType);
+
+SkiaSalBitmap::SkiaSalBitmap() {}
+
+SkiaSalBitmap::~SkiaSalBitmap() {}
+
+SkiaSalBitmap::SkiaSalBitmap(const sk_sp<SkImage>& image)
+{
+ ResetAllData();
+ mImage = image;
+ mPalette = BitmapPalette();
+#if SKIA_USE_BITMAP32
+ mBitCount = 32;
+#else
+ mBitCount = 24;
+#endif
+ mSize = mPixelsSize = Size(image->width(), image->height());
+ ComputeScanlineSize();
+ mAnyAccessCount = 0;
+#ifdef DBG_UTIL
+ mWriteAccessCount = 0;
+#endif
+ SAL_INFO("vcl.skia.trace", "bitmapfromimage(" << this << ")");
+}
+
+bool SkiaSalBitmap::Create(const Size& rSize, vcl::PixelFormat ePixelFormat,
+ const BitmapPalette& rPal)
+{
+ assert(mAnyAccessCount == 0);
+ ResetAllData();
+ if (ePixelFormat == vcl::PixelFormat::INVALID)
+ return false;
+ mPalette = rPal;
+ mBitCount = vcl::pixelFormatBitCount(ePixelFormat);
+ mSize = rSize;
+ ResetPendingScaling();
+ if (!ComputeScanlineSize())
+ {
+ mBitCount = 0;
+ mSize = mPixelsSize = Size();
+ mScanlineSize = 0;
+ mPalette = BitmapPalette();
+ return false;
+ }
+ SAL_INFO("vcl.skia.trace", "create(" << this << ")");
+ return true;
+}
+
+bool SkiaSalBitmap::ComputeScanlineSize()
+{
+ int bitScanlineWidth;
+ if (o3tl::checked_multiply<int>(mPixelsSize.Width(), mBitCount, bitScanlineWidth))
+ {
+ SAL_WARN("vcl.skia", "checked multiply failed");
+ return false;
+ }
+ mScanlineSize = AlignedWidth4Bytes(bitScanlineWidth);
+ return true;
+}
+
+void SkiaSalBitmap::CreateBitmapData()
+{
+ assert(!mBuffer);
+ // Make sure code has not missed calling ComputeScanlineSize().
+ assert(mScanlineSize == int(AlignedWidth4Bytes(mPixelsSize.Width() * mBitCount)));
+ // The pixels could be stored in SkBitmap, but Skia only supports 8bit gray, 16bit and 32bit formats
+ // (e.g. 24bpp is actually stored as 32bpp). But some of our code accessing the bitmap assumes that
+ // when it asked for 24bpp, the format really will be 24bpp (e.g. the png loader), so we cannot use
+ // SkBitmap to store the data. And even 8bpp is problematic, since Skia does not support palettes
+ // and a VCL bitmap can change its grayscale status simply by changing the palette.
+ // Moreover creating SkImage from SkBitmap does a data copy unless the bitmap is immutable.
+ // So just always store pixels in our buffer and convert as necessary.
+ if (mScanlineSize == 0 || mPixelsSize.Height() == 0)
+ return;
+
+ size_t allocate = mScanlineSize * mPixelsSize.Height();
+#ifdef DBG_UTIL
+ allocate += sizeof(CANARY);
+#endif
+ mBuffer = boost::make_shared_noinit<sal_uInt8[]>(allocate);
+#ifdef DBG_UTIL
+ // fill with random garbage
+ sal_uInt8* buffer = mBuffer.get();
+ for (size_t i = 0; i < allocate; i++)
+ buffer[i] = (i & 0xFF);
+ memcpy(buffer + allocate - sizeof(CANARY), CANARY, sizeof(CANARY));
+#endif
+}
+
+bool SkiaSalBitmap::Create(const SalBitmap& rSalBmp)
+{
+ return Create(rSalBmp, vcl::bitDepthToPixelFormat(rSalBmp.GetBitCount()));
+}
+
+bool SkiaSalBitmap::Create(const SalBitmap& rSalBmp, SalGraphics* pGraphics)
+{
+ auto ePixelFormat = vcl::PixelFormat::INVALID;
+ if (pGraphics)
+ ePixelFormat = vcl::bitDepthToPixelFormat(pGraphics->GetBitCount());
+ else
+ ePixelFormat = vcl::bitDepthToPixelFormat(rSalBmp.GetBitCount());
+
+ return Create(rSalBmp, ePixelFormat);
+}
+
+bool SkiaSalBitmap::Create(const SalBitmap& rSalBmp, vcl::PixelFormat eNewPixelFormat)
+{
+ assert(mAnyAccessCount == 0);
+ assert(&rSalBmp != this);
+ ResetAllData();
+ const SkiaSalBitmap& src = static_cast<const SkiaSalBitmap&>(rSalBmp);
+ mImage = src.mImage;
+ mAlphaImage = src.mAlphaImage;
+ mBuffer = src.mBuffer;
+ mPalette = src.mPalette;
+ mBitCount = src.mBitCount;
+ mSize = src.mSize;
+ mPixelsSize = src.mPixelsSize;
+ mScanlineSize = src.mScanlineSize;
+ mScaleQuality = src.mScaleQuality;
+ mEraseColorSet = src.mEraseColorSet;
+ mEraseColor = src.mEraseColor;
+ if (vcl::pixelFormatBitCount(eNewPixelFormat) != src.GetBitCount())
+ {
+ // This appears to be unused(?). Implement this just in case, but be lazy
+ // about it and rely on EnsureBitmapData() doing the conversion from mImage
+ // if needed, even if that may need unnecessary to- and from- SkImage
+ // conversion.
+ ResetToSkImage(GetSkImage());
+ }
+ SAL_INFO("vcl.skia.trace", "create(" << this << "): (" << &src << ")");
+ return true;
+}
+
+bool SkiaSalBitmap::Create(const css::uno::Reference<css::rendering::XBitmapCanvas>&, Size&, bool)
+{
+ return false;
+}
+
+void SkiaSalBitmap::Destroy()
+{
+ SAL_INFO("vcl.skia.trace", "destroy(" << this << ")");
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount == 0);
+#endif
+ assert(mAnyAccessCount == 0);
+ ResetAllData();
+}
+
+Size SkiaSalBitmap::GetSize() const { return mSize; }
+
+sal_uInt16 SkiaSalBitmap::GetBitCount() const { return mBitCount; }
+
+BitmapBuffer* SkiaSalBitmap::AcquireBuffer(BitmapAccessMode nMode)
+{
+ switch (nMode)
+ {
+ case BitmapAccessMode::Write:
+ EnsureBitmapUniqueData();
+ if (!mBuffer)
+ return nullptr;
+ assert(mPixelsSize == mSize);
+ assert(!mEraseColorSet);
+ break;
+ case BitmapAccessMode::Read:
+ EnsureBitmapData();
+ if (!mBuffer)
+ return nullptr;
+ assert(mPixelsSize == mSize);
+ assert(!mEraseColorSet);
+ break;
+ case BitmapAccessMode::Info:
+ break;
+ }
+#ifdef DBG_UTIL
+ // BitmapWriteAccess stores also a copy of the palette and it can
+ // be modified, so concurrent reading of it might result in inconsistencies.
+ assert(mWriteAccessCount == 0 || nMode == BitmapAccessMode::Write);
+#endif
+ BitmapBuffer* buffer = new BitmapBuffer;
+ buffer->mnWidth = mSize.Width();
+ buffer->mnHeight = mSize.Height();
+ buffer->mnBitCount = mBitCount;
+ buffer->maPalette = mPalette;
+ if (nMode != BitmapAccessMode::Info)
+ buffer->mpBits = mBuffer.get();
+ else
+ buffer->mpBits = nullptr;
+ if (mPixelsSize == mSize)
+ buffer->mnScanlineSize = mScanlineSize;
+ else
+ {
+ // The value of mScanlineSize is based on internal mPixelsSize, but the outside
+ // world cares about mSize, the size that the report as the size of the bitmap,
+ // regardless of any internal state. So report scanline size for that size.
+ Size savedPixelsSize = mPixelsSize;
+ mPixelsSize = mSize;
+ ComputeScanlineSize();
+ buffer->mnScanlineSize = mScanlineSize;
+ mPixelsSize = savedPixelsSize;
+ ComputeScanlineSize();
+ }
+ switch (mBitCount)
+ {
+ case 1:
+ buffer->mnFormat = ScanlineFormat::N1BitMsbPal;
+ break;
+ case 8:
+ buffer->mnFormat = ScanlineFormat::N8BitPal;
+ break;
+ case 24:
+ // Make the RGB/BGR format match the default Skia 32bpp format, to allow
+ // easy conversion later.
+ buffer->mnFormat = kN32_SkColorTypeIsBGRA ? ScanlineFormat::N24BitTcBgr
+ : ScanlineFormat::N24BitTcRgb;
+ break;
+ case 32:
+ buffer->mnFormat = kN32_SkColorTypeIsBGRA ? ScanlineFormat::N32BitTcBgra
+ : ScanlineFormat::N32BitTcRgba;
+ break;
+ default:
+ abort();
+ }
+ buffer->mnFormat |= ScanlineFormat::TopDown;
+ ++mAnyAccessCount;
+#ifdef DBG_UTIL
+ if (nMode == BitmapAccessMode::Write)
+ ++mWriteAccessCount;
+#endif
+ return buffer;
+}
+
+void SkiaSalBitmap::ReleaseBuffer(BitmapBuffer* pBuffer, BitmapAccessMode nMode)
+{
+ ReleaseBuffer(pBuffer, nMode, false);
+}
+
+void SkiaSalBitmap::ReleaseBuffer(BitmapBuffer* pBuffer, BitmapAccessMode nMode,
+ bool dontChangeToErase)
+{
+ if (nMode == BitmapAccessMode::Write)
+ {
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount > 0);
+ --mWriteAccessCount;
+#endif
+ mPalette = pBuffer->maPalette;
+ ResetToBuffer();
+ DataChanged();
+ }
+ assert(mAnyAccessCount > 0);
+ --mAnyAccessCount;
+ // Are there any more ground movements underneath us ?
+ assert(pBuffer->mnWidth == mSize.Width());
+ assert(pBuffer->mnHeight == mSize.Height());
+ assert(pBuffer->mnBitCount == mBitCount);
+ assert(pBuffer->mpBits == mBuffer.get() || nMode == BitmapAccessMode::Info);
+ verify();
+ delete pBuffer;
+ if (nMode == BitmapAccessMode::Write && !dontChangeToErase)
+ {
+ // This saves memory and is also used by IsFullyOpaqueAsAlpha() to avoid unnecessary
+ // alpha blending.
+ if (IsAllBlack())
+ {
+ SAL_INFO("vcl.skia.trace", "releasebuffer(" << this << "): erasing to black");
+ EraseInternal(COL_BLACK);
+ }
+ }
+}
+
+static bool isAllZero(const sal_uInt8* data, size_t size)
+{ // For performance, check in larger data chunks.
+#ifdef UINT64_MAX
+ const int64_t* d = reinterpret_cast<const int64_t*>(data);
+#else
+ const int32_t* d = reinterpret_cast<const int32_t*>(data);
+#endif
+ constexpr size_t step = sizeof(*d) * 8;
+ for (size_t i = 0; i < size / step; ++i)
+ { // Unrolled loop.
+ if (d[0] != 0)
+ return false;
+ if (d[1] != 0)
+ return false;
+ if (d[2] != 0)
+ return false;
+ if (d[3] != 0)
+ return false;
+ if (d[4] != 0)
+ return false;
+ if (d[5] != 0)
+ return false;
+ if (d[6] != 0)
+ return false;
+ if (d[7] != 0)
+ return false;
+ d += 8;
+ }
+ for (size_t i = size / step * step; i < size; ++i)
+ if (data[i] != 0)
+ return false;
+ return true;
+}
+
+bool SkiaSalBitmap::IsAllBlack() const
+{
+ if (mBitCount % 8 != 0 || (!!mPalette && mPalette[0] != COL_BLACK))
+ return false; // Don't bother.
+ if (mSize.Width() * mBitCount / 8 == mScanlineSize)
+ return isAllZero(mBuffer.get(), mScanlineSize * mSize.Height());
+ for (tools::Long y = 0; y < mSize.Height(); ++y)
+ if (!isAllZero(mBuffer.get() + mScanlineSize * y, mSize.Width() * mBitCount / 8))
+ return false;
+ return true;
+}
+
+bool SkiaSalBitmap::GetSystemData(BitmapSystemData&)
+{
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount == 0);
+#endif
+ return false;
+}
+
+bool SkiaSalBitmap::ScalingSupported() const { return true; }
+
+bool SkiaSalBitmap::Scale(const double& rScaleX, const double& rScaleY, BmpScaleFlag nScaleFlag)
+{
+ SkiaZone zone;
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount == 0);
+#endif
+ Size newSize(FRound(mSize.Width() * rScaleX), FRound(mSize.Height() * rScaleY));
+ if (mSize == newSize)
+ return true;
+
+ SAL_INFO("vcl.skia.trace", "scale(" << this << "): " << mSize << "/" << mBitCount << "->"
+ << newSize << ":" << static_cast<int>(nScaleFlag));
+
+ if (mEraseColorSet)
+ { // Simple.
+ mSize = newSize;
+ ResetPendingScaling();
+ EraseInternal(mEraseColor);
+ return true;
+ }
+
+ if (mBitCount < 24 && !mPalette.IsGreyPalette8Bit())
+ {
+ // Scaling can introduce additional colors not present in the original
+ // bitmap (e.g. when smoothing). If the bitmap is indexed (has non-trivial palette),
+ // this would break the bitmap, because the actual scaling is done only somewhen later.
+ // Linear 8bit palette (grey) is ok, since there we use directly the values as colors.
+ SAL_INFO("vcl.skia.trace", "scale(" << this << "): indexed bitmap");
+ return false;
+ }
+ // The idea here is that the actual scaling will be delayed until the result
+ // is actually needed. Usually the scaled bitmap will be drawn somewhere,
+ // so delaying will mean the scaling can be done as a part of GetSkImage().
+ // That means it can be GPU-accelerated, while done here directly it would need
+ // to be either done by CPU, or with the CPU->GPU->CPU roundtrip required
+ // by GPU-accelerated scaling.
+ // Pending scaling is detected by 'mSize != mPixelsSize' for mBuffer,
+ // and 'imageSize(mImage) != mSize' for mImage. It is not intended to have 3 different
+ // sizes though, code below keeps only mBuffer or mImage. Note that imageSize(mImage)
+ // may or may not be equal to mPixelsSize, depending on whether mImage is set here
+ // (sizes will be equal) or whether it's set in GetSkImage() (will not be equal).
+ // Pending scaling is considered "done" by the time mBuffer is resized (or created).
+ // Resizing of mImage is somewhat independent of this, since mImage is primarily
+ // considered to be a cached object (although sometimes it's the only data available).
+
+ // If there is already one scale() pending, use the lowest quality of all requested.
+ switch (nScaleFlag)
+ {
+ 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;
+ break;
+ case BmpScaleFlag::BestQuality:
+ // Best is the maximum, set by default.
+ break;
+ default:
+ SAL_INFO("vcl.skia.trace", "scale(" << this << "): unsupported scale algorithm");
+ return false;
+ }
+ mSize = newSize;
+ // If we have both mBuffer and mImage, prefer mImage, since it likely will be drawn later.
+ // We could possibly try to keep the buffer as well, but that would complicate things
+ // with two different data structures to be scaled on-demand, and it's a question
+ // if that'd realistically help with anything.
+ if (mImage)
+ ResetToSkImage(mImage);
+ else
+ ResetToBuffer();
+ DataChanged();
+ // The rest will be handled when the scaled bitmap is actually needed,
+ // such as in EnsureBitmapData() or GetSkImage().
+ return true;
+}
+
+bool SkiaSalBitmap::Replace(const Color&, const Color&, sal_uInt8)
+{
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount == 0);
+#endif
+ return false;
+}
+
+bool SkiaSalBitmap::ConvertToGreyscale()
+{
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount == 0);
+#endif
+ // Normally this would need to convert contents of mBuffer for all possible formats,
+ // so just let the VCL algorithm do it.
+ // Avoid the costly SkImage->buffer->SkImage conversion.
+ if (!mBuffer && mImage && !mEraseColorSet)
+ {
+ if (mBitCount == 8 && mPalette.IsGreyPalette8Bit())
+ return true;
+ sk_sp<SkSurface> surface
+ = createSkSurface(imageSize(mImage), mImage->imageInfo().alphaType());
+ SkPaint paint;
+ paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha
+ // VCL uses different coefficients for conversion to gray than Skia, so use the VCL
+ // values from Bitmap::ImplMakeGreyscales(). Do not use kGray_8_SkColorType,
+ // Skia would use its gray conversion formula.
+ // NOTE: The matrix is 4x5 organized as columns (i.e. each line is a column, not a row).
+ constexpr SkColorMatrix toGray(77 / 256.0, 151 / 256.0, 28 / 256.0, 0, 0, // R column
+ 77 / 256.0, 151 / 256.0, 28 / 256.0, 0, 0, // G column
+ 77 / 256.0, 151 / 256.0, 28 / 256.0, 0, 0, // B column
+ 0, 0, 0, 1, 0); // don't modify alpha
+ paint.setColorFilter(SkColorFilters::Matrix(toGray));
+ surface->getCanvas()->drawImage(mImage, 0, 0, SkSamplingOptions(), &paint);
+ mBitCount = 8;
+ ComputeScanlineSize();
+ mPalette = Bitmap::GetGreyPalette(256);
+ ResetToSkImage(makeCheckedImageSnapshot(surface));
+ DataChanged();
+ SAL_INFO("vcl.skia.trace", "converttogreyscale(" << this << ")");
+ return true;
+ }
+ return false;
+}
+
+bool SkiaSalBitmap::InterpretAs8Bit()
+{
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount == 0);
+#endif
+ if (mBitCount == 8 && mPalette.IsGreyPalette8Bit())
+ return true;
+ if (mEraseColorSet)
+ {
+ mBitCount = 8;
+ ComputeScanlineSize();
+ mPalette = Bitmap::GetGreyPalette(256);
+ EraseInternal(mEraseColor);
+ SAL_INFO("vcl.skia.trace", "interpretas8bit(" << this << ") with erase color");
+ return true;
+ }
+ // This is usually used by AlphaMask, the point is just to treat
+ // the content as an alpha channel. This is often used
+ // by the horrible separate-alpha-outdev hack, where the bitmap comes
+ // from SkiaSalGraphicsImpl::GetBitmap(), so only mImage is set,
+ // and that is followed by a later call to GetAlphaSkImage().
+ // Avoid the costly SkImage->buffer->SkImage conversion and simply
+ // just treat the SkImage as being for 8bit bitmap. EnsureBitmapData()
+ // will do the conversion if needed, but the normal case will be
+ // GetAlphaSkImage() creating kAlpha_8_SkColorType SkImage from it.
+ if (mImage)
+ {
+ mBitCount = 8;
+ ComputeScanlineSize();
+ mPalette = Bitmap::GetGreyPalette(256);
+ ResetToSkImage(mImage); // keep mImage, it will be interpreted as 8bit if needed
+ DataChanged();
+ SAL_INFO("vcl.skia.trace", "interpretas8bit(" << this << ") with image");
+ return true;
+ }
+ SAL_INFO("vcl.skia.trace", "interpretas8bit(" << this << ") with pixel data, ignoring");
+ return false;
+}
+
+bool SkiaSalBitmap::Erase(const Color& color)
+{
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount == 0);
+#endif
+ // Optimized variant, just remember the color and apply it when needed,
+ // which may save having to do format conversions (e.g. GetSkImage()
+ // may directly erase the SkImage).
+ EraseInternal(color);
+ SAL_INFO("vcl.skia.trace", "erase(" << this << ")");
+ return true;
+}
+
+void SkiaSalBitmap::EraseInternal(const Color& color)
+{
+ ResetAllData();
+ mEraseColorSet = true;
+ mEraseColor = color;
+}
+
+bool SkiaSalBitmap::AlphaBlendWith(const SalBitmap& rSalBmp)
+{
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount == 0);
+#endif
+ const SkiaSalBitmap* otherBitmap = dynamic_cast<const SkiaSalBitmap*>(&rSalBmp);
+ if (!otherBitmap)
+ return false;
+ if (mSize != otherBitmap->mSize)
+ return false;
+ // We're called from AlphaMask, which should ensure 8bit.
+ assert(GetBitCount() == 8 && mPalette.IsGreyPalette8Bit());
+ // If neither bitmap have Skia images, then AlphaMask::BlendWith() will be faster,
+ // as it will operate on mBuffer pixel buffers, while for Skia we'd need to convert it.
+ // If one has and one doesn't, do it using Skia, under the assumption that after this
+ // the resulting Skia image will be needed for drawing.
+ if (!(mImage || mEraseColorSet) && !(otherBitmap->mImage || otherBitmap->mEraseColorSet))
+ return false;
+ // This is for AlphaMask, which actually stores the alpha as the pixel values.
+ // I.e. take value of the color channel (one of them, if >8bit, they should be the same).
+ if (mEraseColorSet && otherBitmap->mEraseColorSet)
+ {
+ const sal_uInt16 nGrey1 = mEraseColor.GetRed();
+ const sal_uInt16 nGrey2 = otherBitmap->mEraseColor.GetRed();
+ const sal_uInt8 nGrey = static_cast<sal_uInt8>(nGrey1 + nGrey2 - nGrey1 * nGrey2 / 255);
+ mEraseColor = Color(nGrey, nGrey, nGrey);
+ DataChanged();
+ SAL_INFO("vcl.skia.trace",
+ "alphablendwith(" << this << ") : with erase color " << otherBitmap);
+ return true;
+ }
+ std::unique_ptr<SkiaSalBitmap> otherBitmapAllocated;
+ if (otherBitmap->GetBitCount() != 8 || !otherBitmap->mPalette.IsGreyPalette8Bit())
+ { // Convert/interpret as 8bit if needed.
+ otherBitmapAllocated = std::make_unique<SkiaSalBitmap>();
+ if (!otherBitmapAllocated->Create(*otherBitmap) || !otherBitmapAllocated->InterpretAs8Bit())
+ return false;
+ otherBitmap = otherBitmapAllocated.get();
+ }
+ // This is 8-bit bitmap serving as mask, so the image itself needs no alpha.
+ sk_sp<SkSurface> surface = createSkSurface(mSize, kOpaque_SkAlphaType);
+ SkPaint paint;
+ paint.setBlendMode(SkBlendMode::kSrc); // set as is
+ surface->getCanvas()->drawImage(GetSkImage(), 0, 0, SkSamplingOptions(), &paint);
+ paint.setBlendMode(SkBlendMode::kScreen); // src+dest - src*dest/255 (in 0..1)
+ surface->getCanvas()->drawImage(otherBitmap->GetSkImage(), 0, 0, SkSamplingOptions(), &paint);
+ ResetToSkImage(makeCheckedImageSnapshot(surface));
+ DataChanged();
+ SAL_INFO("vcl.skia.trace", "alphablendwith(" << this << ") : with image " << otherBitmap);
+ return true;
+}
+
+SkBitmap SkiaSalBitmap::GetAsSkBitmap() const
+{
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount == 0);
+#endif
+ EnsureBitmapData();
+ assert(mSize == mPixelsSize); // data has already been scaled if needed
+ SkiaZone zone;
+ SkBitmap bitmap;
+ if (mBuffer)
+ {
+ if (mBitCount == 32)
+ {
+ // Make a copy, the bitmap should be immutable (otherwise converting it
+ // to SkImage will make a copy anyway).
+ const size_t bytes = mPixelsSize.Height() * mScanlineSize;
+ std::unique_ptr<sal_uInt8[]> data(new sal_uInt8[bytes]);
+ memcpy(data.get(), mBuffer.get(), bytes);
+ if (!bitmap.installPixels(
+ SkImageInfo::MakeS32(mPixelsSize.Width(), mPixelsSize.Height(), alphaType()),
+ data.release(), mScanlineSize,
+ [](void* addr, void*) { delete[] static_cast<sal_uInt8*>(addr); }, nullptr))
+ abort();
+ }
+ else if (mBitCount == 24)
+ {
+ // Convert 24bpp RGB/BGR to 32bpp RGBA/BGRA.
+ std::unique_ptr<uint32_t[]> data(
+ new uint32_t[mPixelsSize.Height() * mPixelsSize.Width()]);
+ uint32_t* dest = data.get();
+ // SkConvertRGBToRGBA() also works as BGR to BGRA (the function extends 3 bytes to 4
+ // by adding 0xFF alpha, so position of B and R doesn't matter).
+ if (mPixelsSize.Width() * 3 == mScanlineSize)
+ SkConvertRGBToRGBA(dest, mBuffer.get(), mPixelsSize.Height() * mPixelsSize.Width());
+ else
+ {
+ for (tools::Long y = 0; y < mPixelsSize.Height(); ++y)
+ {
+ const sal_uInt8* src = mBuffer.get() + mScanlineSize * y;
+ SkConvertRGBToRGBA(dest, src, mPixelsSize.Width());
+ dest += mPixelsSize.Width();
+ }
+ }
+ if (!bitmap.installPixels(
+ SkImageInfo::MakeS32(mPixelsSize.Width(), mPixelsSize.Height(),
+ kOpaque_SkAlphaType),
+ data.release(), mPixelsSize.Width() * 4,
+ [](void* addr, void*) { delete[] static_cast<sal_uInt8*>(addr); }, nullptr))
+ abort();
+ }
+ else if (mBitCount == 8 && mPalette.IsGreyPalette8Bit())
+ {
+ // Convert 8bpp gray to 32bpp RGBA/BGRA.
+ // There's also kGray_8_SkColorType, but it's probably simpler to make
+ // GetAsSkBitmap() always return 32bpp SkBitmap and then assume mImage
+ // is always 32bpp too.
+ std::unique_ptr<uint32_t[]> data(
+ new uint32_t[mPixelsSize.Height() * mPixelsSize.Width()]);
+ uint32_t* dest = data.get();
+ if (mPixelsSize.Width() * 1 == mScanlineSize)
+ SkConvertGrayToRGBA(dest, mBuffer.get(),
+ mPixelsSize.Height() * mPixelsSize.Width());
+ else
+ {
+ for (tools::Long y = 0; y < mPixelsSize.Height(); ++y)
+ {
+ const sal_uInt8* src = mBuffer.get() + mScanlineSize * y;
+ SkConvertGrayToRGBA(dest, src, mPixelsSize.Width());
+ dest += mPixelsSize.Width();
+ }
+ }
+ if (!bitmap.installPixels(
+ SkImageInfo::MakeS32(mPixelsSize.Width(), mPixelsSize.Height(),
+ kOpaque_SkAlphaType),
+ data.release(), mPixelsSize.Width() * 4,
+ [](void* addr, void*) { delete[] static_cast<sal_uInt8*>(addr); }, nullptr))
+ abort();
+ }
+ else
+ {
+ std::unique_ptr<sal_uInt8[]> data = convertDataBitCount(
+ mBuffer.get(), mPixelsSize.Width(), mPixelsSize.Height(), mBitCount, mScanlineSize,
+ mPalette, kN32_SkColorTypeIsBGRA ? BitConvert::BGRA : BitConvert::RGBA);
+ if (!bitmap.installPixels(
+ SkImageInfo::MakeS32(mPixelsSize.Width(), mPixelsSize.Height(),
+ kOpaque_SkAlphaType),
+ data.release(), mPixelsSize.Width() * 4,
+ [](void* addr, void*) { delete[] static_cast<sal_uInt8*>(addr); }, nullptr))
+ abort();
+ }
+ }
+ bitmap.setImmutable();
+ return bitmap;
+}
+
+// If mEraseColor is set, this is the color to use when the bitmap is used as alpha bitmap.
+// E.g. COL_BLACK actually means fully opaque and COL_WHITE means fully transparent.
+// This is because the alpha value is set as the color itself, not the alpha of the color.
+// Additionally VCL actually uses transparency and not opacity, so we should use "255 - value",
+// but we account for this by doing SkBlendMode::kDstOut when using alpha images (which
+// basically does another "255 - alpha"), so do not do it here.
+static SkColor fromEraseColorToAlphaImageColor(Color color)
+{
+ return SkColorSetARGB(color.GetBlue(), 0, 0, 0);
+}
+
+// SkiaSalBitmap can store data in both the SkImage and our mBuffer, which with large
+// images can waste quite a lot of memory. Ideally we should store the data in Skia's
+// SkBitmap, but LO wants us to support data formats that Skia doesn't support.
+// So try to conserve memory by keeping the data only once in that was the most
+// recently wanted storage, and drop the other one. Usually the other one won't be needed
+// for a long time, and especially with raster the conversion is usually fast.
+// Do this only with raster, to avoid GPU->CPU transfer in GPU mode (exception is 32bit
+// builds, where memory is more important). Also don't do this with paletted bitmaps,
+// where EnsureBitmapData() would be expensive.
+// Ideally SalBitmap should be able to say which bitmap formats it supports
+// and VCL code should oblige, which would allow reusing the same data.
+bool SkiaSalBitmap::ConserveMemory() const
+{
+ static bool keepBitmapBuffer = getenv("SAL_SKIA_KEEP_BITMAP_BUFFER") != nullptr;
+ constexpr bool is32Bit = sizeof(void*) == 4;
+ // 16MiB bitmap data at least (set to 0 for easy testing).
+ constexpr tools::Long maxBufferSize = 2000 * 2000 * 4;
+ return !keepBitmapBuffer && (renderMethodToUse() == RenderRaster || is32Bit)
+ && mPixelsSize.Height() * mScanlineSize > maxBufferSize
+ && (mBitCount > 8 || (mBitCount == 8 && mPalette.IsGreyPalette8Bit()));
+}
+
+const sk_sp<SkImage>& SkiaSalBitmap::GetSkImage(DirectImage direct) const
+{
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount == 0);
+#endif
+ if (direct == DirectImage::Yes)
+ return mImage;
+ if (mEraseColorSet)
+ {
+ if (mImage)
+ {
+ assert(imageSize(mImage) == mSize);
+ return mImage;
+ }
+ SkiaZone zone;
+ sk_sp<SkSurface> surface = createSkSurface(
+ mSize, mEraseColor.IsTransparent() ? kPremul_SkAlphaType : kOpaque_SkAlphaType);
+ assert(surface);
+ surface->getCanvas()->clear(toSkColor(mEraseColor));
+ SkiaSalBitmap* thisPtr = const_cast<SkiaSalBitmap*>(this);
+ thisPtr->mImage = makeCheckedImageSnapshot(surface);
+ SAL_INFO("vcl.skia.trace", "getskimage(" << this << ") from erase color " << mEraseColor);
+ return mImage;
+ }
+ if (mPixelsSize != mSize && !mImage && renderMethodToUse() != RenderRaster)
+ {
+ // The bitmap has a pending scaling, but no image. This function would below call GetAsSkBitmap(),
+ // which would do CPU-based pixel scaling, and then it would get converted to an image.
+ // Be more efficient, first convert to an image and then the block below will scale on the GPU.
+ SAL_INFO("vcl.skia.trace", "getskimage(" << this << "): shortcut image scaling "
+ << mPixelsSize << "->" << mSize);
+ SkiaSalBitmap* thisPtr = const_cast<SkiaSalBitmap*>(this);
+ Size savedSize = mSize;
+ thisPtr->mSize = mPixelsSize; // block scaling
+ SkiaZone zone;
+ sk_sp<SkImage> image = createSkImage(GetAsSkBitmap());
+ assert(image);
+ thisPtr->mSize = savedSize;
+ thisPtr->ResetToSkImage(image);
+ }
+ if (mImage)
+ {
+ if (imageSize(mImage) != mSize)
+ {
+ assert(!mBuffer); // This code should be only called if only mImage holds data.
+ SkiaZone zone;
+ sk_sp<SkSurface> surface = createSkSurface(mSize, mImage->imageInfo().alphaType());
+ assert(surface);
+ SkPaint paint;
+ paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha
+ surface->getCanvas()->drawImageRect(
+ mImage, SkRect::MakeWH(mSize.Width(), mSize.Height()),
+ makeSamplingOptions(mScaleQuality, imageSize(mImage), mSize, 1), &paint);
+ SAL_INFO("vcl.skia.trace", "getskimage(" << this << "): image scaled "
+ << Size(mImage->width(), mImage->height())
+ << "->" << mSize << ":"
+ << static_cast<int>(mScaleQuality));
+ SkiaSalBitmap* thisPtr = const_cast<SkiaSalBitmap*>(this);
+ thisPtr->mImage = makeCheckedImageSnapshot(surface);
+ }
+ return mImage;
+ }
+ SkiaZone zone;
+ sk_sp<SkImage> image = createSkImage(GetAsSkBitmap());
+ assert(image);
+ SkiaSalBitmap* thisPtr = const_cast<SkiaSalBitmap*>(this);
+ thisPtr->mImage = image;
+ // The data is now stored both in the SkImage and in our mBuffer, so drop the buffer
+ // if conserving memory. It'll be converted back by EnsureBitmapData() if needed.
+ if (ConserveMemory() && mAnyAccessCount == 0)
+ {
+ SAL_INFO("vcl.skia.trace", "getskimage(" << this << "): dropping buffer");
+ thisPtr->ResetToSkImage(mImage);
+ }
+ SAL_INFO("vcl.skia.trace", "getskimage(" << this << ")");
+ return mImage;
+}
+
+const sk_sp<SkImage>& SkiaSalBitmap::GetAlphaSkImage(DirectImage direct) const
+{
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount == 0);
+#endif
+ if (direct == DirectImage::Yes)
+ return mAlphaImage;
+ if (mEraseColorSet)
+ {
+ if (mAlphaImage)
+ {
+ assert(imageSize(mAlphaImage) == mSize);
+ return mAlphaImage;
+ }
+ SkiaZone zone;
+ sk_sp<SkSurface> surface = createSkSurface(mSize, kAlpha_8_SkColorType);
+ assert(surface);
+ surface->getCanvas()->clear(fromEraseColorToAlphaImageColor(mEraseColor));
+ SkiaSalBitmap* thisPtr = const_cast<SkiaSalBitmap*>(this);
+ thisPtr->mAlphaImage = makeCheckedImageSnapshot(surface);
+ SAL_INFO("vcl.skia.trace",
+ "getalphaskimage(" << this << ") from erase color " << mEraseColor);
+ return mAlphaImage;
+ }
+ if (mAlphaImage)
+ {
+ if (imageSize(mAlphaImage) == mSize)
+ return mAlphaImage;
+ }
+ if (mImage)
+ {
+ SkiaZone zone;
+ const bool scaling = imageSize(mImage) != mSize;
+ SkPixmap pixmap;
+ // Note: We cannot do this when 'scaling' because SkCanvas::drawImageRect()
+ // with kAlpha_8_SkColorType as source and destination would act as SkBlendMode::kSrcOver
+ // despite SkBlendMode::kSrc set (https://bugs.chromium.org/p/skia/issues/detail?id=9692).
+ if (mImage->peekPixels(&pixmap) && !scaling)
+ {
+ assert(pixmap.colorType() == kN32_SkColorType);
+ // In non-GPU mode, convert 32bit data to 8bit alpha, this is faster than
+ // the SkColorFilter below. Since this is the VCL alpha-vdev alpha, where
+ // all R,G,B are the same and in fact mean alpha, this means we simply take one
+ // 8bit channel from the input, and that's the output.
+ SkBitmap bitmap;
+ if (!bitmap.installPixels(pixmap))
+ abort();
+ SkBitmap alphaBitmap;
+ if (!alphaBitmap.tryAllocPixels(SkImageInfo::MakeA8(bitmap.width(), bitmap.height())))
+ abort();
+ if (int(bitmap.rowBytes()) == bitmap.width() * 4)
+ {
+ SkConvertRGBAToR(alphaBitmap.getAddr8(0, 0), bitmap.getAddr32(0, 0),
+ bitmap.width() * bitmap.height());
+ }
+ else
+ {
+ for (tools::Long y = 0; y < bitmap.height(); ++y)
+ SkConvertRGBAToR(alphaBitmap.getAddr8(0, y), bitmap.getAddr32(0, y),
+ bitmap.width());
+ }
+ alphaBitmap.setImmutable();
+ sk_sp<SkImage> alphaImage = createSkImage(alphaBitmap);
+ assert(alphaImage);
+ SAL_INFO("vcl.skia.trace", "getalphaskimage(" << this << ") from raster image");
+ // Don't bother here with ConserveMemory(), mImage -> mAlphaImage conversions should
+ // generally only happen with the separate-alpha-outdev hack, and those bitmaps should
+ // be temporary.
+ SkiaSalBitmap* thisPtr = const_cast<SkiaSalBitmap*>(this);
+ thisPtr->mAlphaImage = alphaImage;
+ return mAlphaImage;
+ }
+ // Move the R channel value to the alpha channel. This seems to be the only
+ // way to reinterpret data in SkImage as an alpha SkImage without accessing the pixels.
+ // NOTE: The matrix is 4x5 organized as columns (i.e. each line is a column, not a row).
+ constexpr SkColorMatrix redToAlpha(0, 0, 0, 0, 0, // R column
+ 0, 0, 0, 0, 0, // G column
+ 0, 0, 0, 0, 0, // B column
+ 1, 0, 0, 0, 0); // A column
+ SkPaint paint;
+ paint.setColorFilter(SkColorFilters::Matrix(redToAlpha));
+ if (scaling)
+ assert(!mBuffer); // This code should be only called if only mImage holds data.
+ sk_sp<SkSurface> surface = createSkSurface(mSize, kAlpha_8_SkColorType);
+ assert(surface);
+ 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, 1)
+ : SkSamplingOptions(),
+ &paint);
+ if (scaling)
+ SAL_INFO("vcl.skia.trace", "getalphaskimage(" << this << "): image scaled "
+ << Size(mImage->width(), mImage->height())
+ << "->" << mSize << ":"
+ << static_cast<int>(mScaleQuality));
+ else
+ SAL_INFO("vcl.skia.trace", "getalphaskimage(" << this << ") from image");
+ // Don't bother here with ConserveMemory(), mImage -> mAlphaImage conversions should
+ // generally only happen with the separate-alpha-outdev hack, and those bitmaps should
+ // be temporary.
+ SkiaSalBitmap* thisPtr = const_cast<SkiaSalBitmap*>(this);
+ thisPtr->mAlphaImage = makeCheckedImageSnapshot(surface);
+ return mAlphaImage;
+ }
+ SkiaZone zone;
+ EnsureBitmapData();
+ assert(mSize == mPixelsSize); // data has already been scaled if needed
+ SkBitmap alphaBitmap;
+ if (mBuffer && mBitCount <= 8)
+ {
+ assert(mBuffer.get());
+ verify();
+ std::unique_ptr<sal_uInt8[]> data
+ = convertDataBitCount(mBuffer.get(), mSize.Width(), mSize.Height(), mBitCount,
+ mScanlineSize, mPalette, BitConvert::A8);
+ if (!alphaBitmap.installPixels(
+ SkImageInfo::MakeA8(mSize.Width(), mSize.Height()), data.release(), mSize.Width(),
+ [](void* addr, void*) { delete[] static_cast<sal_uInt8*>(addr); }, nullptr))
+ abort();
+ alphaBitmap.setImmutable();
+ sk_sp<SkImage> image = createSkImage(alphaBitmap);
+ assert(image);
+ const_cast<sk_sp<SkImage>&>(mAlphaImage) = image;
+ }
+ else
+ {
+ sk_sp<SkSurface> surface = createSkSurface(mSize, kAlpha_8_SkColorType);
+ assert(surface);
+ SkPaint paint;
+ paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha
+ // Move the R channel value to the alpha channel. This seems to be the only
+ // way to reinterpret data in SkImage as an alpha SkImage without accessing the pixels.
+ // NOTE: The matrix is 4x5 organized as columns (i.e. each line is a column, not a row).
+ constexpr SkColorMatrix redToAlpha(0, 0, 0, 0, 0, // R column
+ 0, 0, 0, 0, 0, // G column
+ 0, 0, 0, 0, 0, // B column
+ 1, 0, 0, 0, 0); // A column
+ paint.setColorFilter(SkColorFilters::Matrix(redToAlpha));
+ surface->getCanvas()->drawImage(GetAsSkBitmap().asImage(), 0, 0, SkSamplingOptions(),
+ &paint);
+ SkiaSalBitmap* thisPtr = const_cast<SkiaSalBitmap*>(this);
+ thisPtr->mAlphaImage = makeCheckedImageSnapshot(surface);
+ }
+ // The data is now stored both in the SkImage and in our mBuffer, so drop the buffer
+ // if conserving memory and the conversion back would be simple (it'll be converted back
+ // by EnsureBitmapData() if needed).
+ if (ConserveMemory() && mBitCount == 8 && mPalette.IsGreyPalette8Bit() && mAnyAccessCount == 0)
+ {
+ SAL_INFO("vcl.skia.trace", "getalphaskimage(" << this << "): dropping buffer");
+ SkiaSalBitmap* thisPtr = const_cast<SkiaSalBitmap*>(this);
+ thisPtr->mBuffer.reset();
+ }
+ SAL_INFO("vcl.skia.trace", "getalphaskimage(" << this << ")");
+ return mAlphaImage;
+}
+
+void SkiaSalBitmap::TryDirectConvertToAlphaNoScaling()
+{
+ // This is a bit of a hack. Because of the VCL alpha hack where alpha is stored
+ // separately, we often convert mImage to mAlphaImage to represent the alpha
+ // channel. If code finds out that there is mImage but no mAlphaImage,
+ // this will create it from it, without checking for delayed scaling (i.e.
+ // it is "direct").
+ assert(mImage);
+ assert(!mAlphaImage);
+ // Set wanted size, trigger conversion.
+ Size savedSize = mSize;
+ mSize = imageSize(mImage);
+ GetAlphaSkImage();
+ assert(mAlphaImage);
+ mSize = savedSize;
+}
+
+// If the bitmap is to be erased, SkShader with the color set is more efficient
+// than creating an image filled with the color.
+bool SkiaSalBitmap::PreferSkShader() const { return mEraseColorSet; }
+
+sk_sp<SkShader> SkiaSalBitmap::GetSkShader(const SkSamplingOptions& samplingOptions,
+ DirectImage direct) const
+{
+ if (mEraseColorSet)
+ return SkShaders::Color(toSkColor(mEraseColor));
+ return GetSkImage(direct)->makeShader(samplingOptions);
+}
+
+sk_sp<SkShader> SkiaSalBitmap::GetAlphaSkShader(const SkSamplingOptions& samplingOptions,
+ DirectImage direct) const
+{
+ if (mEraseColorSet)
+ return SkShaders::Color(fromEraseColorToAlphaImageColor(mEraseColor));
+ return GetAlphaSkImage(direct)->makeShader(samplingOptions);
+}
+
+bool SkiaSalBitmap::IsFullyOpaqueAsAlpha() const
+{
+ if (!mEraseColorSet) // Set from Erase() or ReleaseBuffer().
+ return false;
+ // If the erase color is set so that this bitmap used as alpha would
+ // mean a fully opaque alpha mask (= noop), we can skip using it.
+ // Note that for alpha bitmaps we use the VCL "transparency" convention,
+ // i.e. alpha 0 is opaque.
+ return SkColorGetA(fromEraseColorToAlphaImageColor(mEraseColor)) == 0;
+}
+
+SkAlphaType SkiaSalBitmap::alphaType() const
+{
+ if (mEraseColorSet)
+ return mEraseColor.IsTransparent() ? kPremul_SkAlphaType : kOpaque_SkAlphaType;
+#if SKIA_USE_BITMAP32
+ // The bitmap's alpha matters only if SKIA_USE_BITMAP32 is set, otherwise
+ // the alpha is in a separate bitmap.
+ if (mBitCount == 32)
+ return kPremul_SkAlphaType;
+#endif
+ return kOpaque_SkAlphaType;
+}
+
+void SkiaSalBitmap::PerformErase()
+{
+ if (mPixelsSize.IsEmpty())
+ return;
+ BitmapBuffer* bitmapBuffer = AcquireBuffer(BitmapAccessMode::Write);
+ if (bitmapBuffer == nullptr)
+ abort();
+ Color fastColor = mEraseColor;
+ if (!!mPalette)
+ fastColor = Color(ColorTransparency, mPalette.GetBestIndex(fastColor));
+ if (!ImplFastEraseBitmap(*bitmapBuffer, fastColor))
+ {
+ FncSetPixel setPixel = BitmapReadAccess::SetPixelFunction(bitmapBuffer->mnFormat);
+ assert(bitmapBuffer->mnFormat & ScanlineFormat::TopDown);
+ // Set first scanline, copy to others.
+ Scanline scanline = bitmapBuffer->mpBits;
+ for (tools::Long x = 0; x < bitmapBuffer->mnWidth; ++x)
+ setPixel(scanline, x, mEraseColor, bitmapBuffer->maColorMask);
+ for (tools::Long y = 1; y < bitmapBuffer->mnHeight; ++y)
+ memcpy(scanline + y * bitmapBuffer->mnScanlineSize, scanline,
+ bitmapBuffer->mnScanlineSize);
+ }
+ ReleaseBuffer(bitmapBuffer, BitmapAccessMode::Write, true);
+}
+
+void SkiaSalBitmap::EnsureBitmapData()
+{
+ if (mEraseColorSet)
+ {
+ SkiaZone zone;
+ assert(mPixelsSize == mSize);
+ assert(!mBuffer);
+ CreateBitmapData();
+ // Unset now, so that repeated call will return mBuffer.
+ mEraseColorSet = false;
+ PerformErase();
+ verify();
+ SAL_INFO("vcl.skia.trace",
+ "ensurebitmapdata(" << this << ") from erase color " << mEraseColor);
+ return;
+ }
+
+ if (mBuffer)
+ {
+ if (mSize == mPixelsSize)
+ return;
+ // Pending scaling. Create raster SkImage from the bitmap data
+ // at the pixel size and then the code below will scale at the correct
+ // bpp from the image.
+ SAL_INFO("vcl.skia.trace", "ensurebitmapdata(" << this << "): pixels to be scaled "
+ << mPixelsSize << "->" << mSize << ":"
+ << static_cast<int>(mScaleQuality));
+ Size savedSize = mSize;
+ mSize = mPixelsSize;
+ ResetToSkImage(SkImage::MakeFromBitmap(GetAsSkBitmap()));
+ mSize = savedSize;
+ }
+
+ // Convert from alpha image, if the conversion is simple.
+ if (mAlphaImage && imageSize(mAlphaImage) == mSize && mBitCount == 8
+ && mPalette.IsGreyPalette8Bit())
+ {
+ assert(mAlphaImage->colorType() == kAlpha_8_SkColorType);
+ SkiaZone zone;
+ SkBitmap bitmap;
+ SkPixmap pixmap;
+ if (mAlphaImage->peekPixels(&pixmap))
+ bitmap.installPixels(pixmap);
+ else
+ {
+ if (!bitmap.tryAllocPixels(SkImageInfo::MakeA8(mSize.Width(), mSize.Height())))
+ abort();
+ SkCanvas canvas(bitmap);
+ SkPaint paint;
+ paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha
+ canvas.drawImage(mAlphaImage, 0, 0, SkSamplingOptions(), &paint);
+ canvas.flush();
+ }
+ bitmap.setImmutable();
+ ResetPendingScaling();
+ CreateBitmapData();
+ assert(mBuffer != nullptr);
+ assert(mPixelsSize == mSize);
+ if (int(bitmap.rowBytes()) == mScanlineSize)
+ memcpy(mBuffer.get(), bitmap.getPixels(), mSize.Height() * mScanlineSize);
+ else
+ {
+ for (tools::Long y = 0; y < mSize.Height(); ++y)
+ {
+ const uint8_t* src = static_cast<uint8_t*>(bitmap.getAddr(0, y));
+ sal_uInt8* dest = mBuffer.get() + mScanlineSize * y;
+ memcpy(dest, src, mScanlineSize);
+ }
+ }
+ verify();
+ // We've created the bitmap data from mAlphaImage, drop the image if conserving memory,
+ // it'll be converted back if needed.
+ if (ConserveMemory())
+ {
+ SAL_INFO("vcl.skia.trace", "ensurebitmapdata(" << this << "): dropping images");
+ ResetToBuffer();
+ }
+ SAL_INFO("vcl.skia.trace", "ensurebitmapdata(" << this << "): from alpha image");
+ return;
+ }
+
+ if (!mImage)
+ {
+ // No data at all, create uninitialized data.
+ CreateBitmapData();
+ SAL_INFO("vcl.skia.trace", "ensurebitmapdata(" << this << "): uninitialized");
+ return;
+ }
+ // Try to fill mBuffer from mImage.
+ assert(mImage->colorType() == kN32_SkColorType);
+ SkiaZone zone;
+ // If the source image has no alpha, then use no alpha (faster to convert), otherwise
+ // use kUnpremul_SkAlphaType to make Skia convert from premultiplied alpha when reading
+ // from the SkImage (the alpha will be ignored if converting to bpp<32 formats, but
+ // the color channels must be unpremultiplied. Unless bpp==32 and SKIA_USE_BITMAP32,
+ // in which case use kPremul_SkAlphaType, since SKIA_USE_BITMAP32 implies premultiplied alpha.
+ SkAlphaType alphaType = kUnpremul_SkAlphaType;
+ if (mImage->imageInfo().alphaType() == kOpaque_SkAlphaType)
+ alphaType = kOpaque_SkAlphaType;
+#if SKIA_USE_BITMAP32
+ if (mBitCount == 32)
+ alphaType = kPremul_SkAlphaType;
+#endif
+ SkBitmap bitmap;
+ SkPixmap pixmap;
+ if (imageSize(mImage) == mSize && mImage->imageInfo().alphaType() == alphaType
+ && mImage->peekPixels(&pixmap))
+ {
+ bitmap.installPixels(pixmap);
+ }
+ else
+ {
+ if (!bitmap.tryAllocPixels(SkImageInfo::MakeS32(mSize.Width(), mSize.Height(), alphaType)))
+ abort();
+ SkCanvas canvas(bitmap);
+ SkPaint paint;
+ paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha
+ if (imageSize(mImage) != mSize) // pending scaling?
+ {
+ canvas.drawImageRect(mImage, SkRect::MakeWH(mSize.getWidth(), mSize.getHeight()),
+ makeSamplingOptions(mScaleQuality, imageSize(mImage), mSize, 1),
+ &paint);
+ SAL_INFO("vcl.skia.trace",
+ "ensurebitmapdata(" << this << "): image scaled " << imageSize(mImage) << "->"
+ << mSize << ":" << static_cast<int>(mScaleQuality));
+ }
+ else
+ canvas.drawImage(mImage, 0, 0, SkSamplingOptions(), &paint);
+ canvas.flush();
+ }
+ bitmap.setImmutable();
+ ResetPendingScaling();
+ CreateBitmapData();
+ assert(mBuffer != nullptr);
+ assert(mPixelsSize == mSize);
+ if (mBitCount == 32)
+ {
+ if (int(bitmap.rowBytes()) == mScanlineSize)
+ memcpy(mBuffer.get(), bitmap.getPixels(), mSize.Height() * mScanlineSize);
+ else
+ {
+ for (tools::Long y = 0; y < mSize.Height(); ++y)
+ {
+ const uint8_t* src = static_cast<uint8_t*>(bitmap.getAddr(0, y));
+ sal_uInt8* dest = mBuffer.get() + mScanlineSize * y;
+ memcpy(dest, src, mScanlineSize);
+ }
+ }
+ }
+ else if (mBitCount == 24) // non-paletted
+ {
+ if (int(bitmap.rowBytes()) == mSize.Width() * 4 && mSize.Width() * 3 == mScanlineSize)
+ {
+ SkConvertRGBAToRGB(mBuffer.get(), bitmap.getAddr32(0, 0),
+ mSize.Height() * mSize.Width());
+ }
+ else
+ {
+ for (tools::Long y = 0; y < mSize.Height(); ++y)
+ {
+ const uint32_t* src = bitmap.getAddr32(0, y);
+ sal_uInt8* dest = mBuffer.get() + mScanlineSize * y;
+ SkConvertRGBAToRGB(dest, src, mSize.Width());
+ }
+ }
+ }
+ else if (mBitCount == 8 && mPalette.IsGreyPalette8Bit())
+ { // no actual data conversion, use one color channel as the gray value
+ if (int(bitmap.rowBytes()) == mSize.Width() * 4 && mSize.Width() * 1 == mScanlineSize)
+ {
+ SkConvertRGBAToR(mBuffer.get(), bitmap.getAddr32(0, 0), mSize.Height() * mSize.Width());
+ }
+ else
+ {
+ for (tools::Long y = 0; y < mSize.Height(); ++y)
+ {
+ const uint32_t* src = bitmap.getAddr32(0, y);
+ sal_uInt8* dest = mBuffer.get() + mScanlineSize * y;
+ SkConvertRGBAToR(dest, src, mSize.Width());
+ }
+ }
+ }
+ else
+ {
+ std::unique_ptr<vcl::ScanlineWriter> pWriter
+ = vcl::ScanlineWriter::Create(mBitCount, mPalette);
+ for (tools::Long y = 0; y < mSize.Height(); ++y)
+ {
+ const uint8_t* src = static_cast<uint8_t*>(bitmap.getAddr(0, y));
+ sal_uInt8* dest = mBuffer.get() + mScanlineSize * y;
+ pWriter->nextLine(dest);
+ for (tools::Long x = 0; x < mSize.Width(); ++x)
+ {
+ sal_uInt8 r = *src++;
+ sal_uInt8 g = *src++;
+ sal_uInt8 b = *src++;
+ ++src; // skip alpha
+ pWriter->writeRGB(r, g, b);
+ }
+ }
+ }
+ verify();
+ // We've created the bitmap data from mImage, drop the image if conserving memory,
+ // it'll be converted back if needed.
+ if (ConserveMemory())
+ {
+ SAL_INFO("vcl.skia.trace", "ensurebitmapdata(" << this << "): dropping images");
+ ResetToBuffer();
+ }
+ SAL_INFO("vcl.skia.trace", "ensurebitmapdata(" << this << ")");
+}
+
+void SkiaSalBitmap::EnsureBitmapUniqueData()
+{
+#ifdef DBG_UTIL
+ assert(mWriteAccessCount == 0);
+#endif
+ EnsureBitmapData();
+ assert(mPixelsSize == mSize);
+ if (mBuffer.use_count() > 1)
+ {
+ sal_uInt32 allocate = mScanlineSize * mSize.Height();
+#ifdef DBG_UTIL
+ assert(memcmp(mBuffer.get() + allocate, CANARY, sizeof(CANARY)) == 0);
+ allocate += sizeof(CANARY);
+#endif
+ boost::shared_ptr<sal_uInt8[]> newBuffer = boost::make_shared_noinit<sal_uInt8[]>(allocate);
+ memcpy(newBuffer.get(), mBuffer.get(), allocate);
+ mBuffer = newBuffer;
+ }
+}
+
+void SkiaSalBitmap::ResetToBuffer()
+{
+ SkiaZone zone;
+ // This should never be called to drop mImage if that's the only data we have.
+ assert(mBuffer || !mImage);
+ mImage.reset();
+ mAlphaImage.reset();
+ mEraseColorSet = false;
+}
+
+void SkiaSalBitmap::ResetToSkImage(sk_sp<SkImage> image)
+{
+ assert(mAnyAccessCount == 0); // can't reset mBuffer if there's a read access pointing to it
+ SkiaZone zone;
+ mBuffer.reset();
+ mImage = image;
+ mAlphaImage.reset();
+ mEraseColorSet = false;
+}
+
+void SkiaSalBitmap::ResetAllData()
+{
+ assert(mAnyAccessCount == 0);
+ SkiaZone zone;
+ mBuffer.reset();
+ mImage.reset();
+ mAlphaImage.reset();
+ mEraseColorSet = false;
+ mPixelsSize = mSize;
+ ComputeScanlineSize();
+ DataChanged();
+}
+
+void SkiaSalBitmap::DataChanged() { InvalidateChecksum(); }
+
+void SkiaSalBitmap::ResetPendingScaling()
+{
+ if (mPixelsSize == mSize)
+ return;
+ SkiaZone zone;
+ mScaleQuality = BmpScaleFlag::BestQuality;
+ mPixelsSize = mSize;
+ ComputeScanlineSize();
+ // Information about the pending scaling has been discarded, so make sure we do not
+ // keep around any cached images that would still need scaling.
+ if (mImage && imageSize(mImage) != mSize)
+ mImage.reset();
+ if (mAlphaImage && imageSize(mAlphaImage) != mSize)
+ mAlphaImage.reset();
+}
+
+OString SkiaSalBitmap::GetImageKey(DirectImage direct) const
+{
+ if (mEraseColorSet)
+ {
+ std::stringstream ss;
+ ss << std::hex << std::setfill('0') << std::setw(6)
+ << static_cast<sal_uInt32>(mEraseColor.GetRGBColor()) << std::setw(2)
+ << static_cast<int>(mEraseColor.GetAlpha());
+ return OString::Concat("E") + ss.str().c_str();
+ }
+ assert(direct == DirectImage::No || mImage);
+ sk_sp<SkImage> image = GetSkImage(direct);
+ // In some cases drawing code may try to draw the same content but using
+ // different bitmaps (even underlying bitmaps), for example canvas apparently
+ // copies the same things around in tdf#146095. For pixel-based images
+ // it should be still cheaper to compute a checksum and avoid re-caching.
+ if (!image->isTextureBacked())
+ return OString::Concat("C") + OString::number(getSkImageChecksum(image));
+ return OString::Concat("I") + OString::number(image->uniqueID());
+}
+
+OString SkiaSalBitmap::GetAlphaImageKey(DirectImage direct) const
+{
+ if (mEraseColorSet)
+ {
+ std::stringstream ss;
+ ss << std::hex << std::setfill('0') << std::setw(2)
+ << static_cast<int>(255 - SkColorGetA(fromEraseColorToAlphaImageColor(mEraseColor)));
+ return OString::Concat("E") + ss.str().c_str();
+ }
+ assert(direct == DirectImage::No || mAlphaImage);
+ sk_sp<SkImage> image = GetAlphaSkImage(direct);
+ if (!image->isTextureBacked())
+ return OString::Concat("C") + OString::number(getSkImageChecksum(image));
+ return OString::Concat("I") + OString::number(image->uniqueID());
+}
+
+void SkiaSalBitmap::dump(const char* file) const
+{
+ // Use a copy, so that debugging doesn't affect this instance.
+ SkiaSalBitmap copy;
+ copy.Create(*this);
+ SkiaHelper::dump(copy.GetSkImage(), file);
+}
+
+#ifdef DBG_UTIL
+void SkiaSalBitmap::verify() const
+{
+ if (!mBuffer)
+ return;
+ // Use mPixelsSize, that describes the size of the actual data.
+ assert(memcmp(mBuffer.get() + mScanlineSize * mPixelsSize.Height(), CANARY, sizeof(CANARY))
+ == 0);
+}
+#endif
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/skia/skia_denylist_vulkan.xml b/vcl/skia/skia_denylist_vulkan.xml
new file mode 100644
index 000000000..5ac2c4265
--- /dev/null
+++ b/vcl/skia/skia_denylist_vulkan.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+* This file is part of the LibreOffice project.
+*
+* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<!--
+ entry attributes:
+ os - "all", "7", "8", "8_1", "10", "windows", "linux", "osx_10_5", "osx_10_6", "osx_10_7", "osx_10_8", "osx"
+ vendor - "all", "intel", "amd", "nvidia", "microsoft"
+ compare - "less", "less_equal", "greater", "greater_equal", "equal", "not_equal", "between_exclusive", "between_inclusive", "between_inclusive_start"
+ version
+ minVersion
+ maxVersion
+-->
+
+<root>
+ <allowlist>
+ </allowlist>
+ <denylist>
+ <entry os="all" vendor="intel" compare="less_equal" version="0.16.2">
+ <device id="all"/>
+ </entry>
+ <entry os="all" vendor="intel" compare="equal" version="0.402.743"> <!-- tdf#138729 -->
+ <device id="all"/>
+ </entry>
+ <entry os="windows" vendor="intel"> <!-- tdf#144923 off-topic comment 14 + tdf#150232 -->
+ <device id="0x9a49"/>
+ </entry>
+ <entry os="all" vendor="amd" compare="less" version="2.0.198"> <!-- tdf#146402 -->
+ <device id="all"/>
+ </entry>
+ <entry os="windows" vendor="nvidia" compare="less" version="457.36.0">
+ <device id="all"/>
+ </entry>
+ <entry os="windows" vendor="nvidia"> <!-- tdf#151929 Crash with GTX 670 -->
+ <device id="0x1189"/>
+ </entry>
+ <entry os="windows" vendor="nvidia"> <!-- tdf#150232 -->
+ <device id="0x1401"/>
+ </entry>
+ <entry os="windows" vendor="nvidia"> <!-- tdf#151627 + tdf#152559 -->
+ <device id="0x1402"/>
+ </entry>
+ <entry os="7" vendor="all">
+ <device id="all"/>
+ </entry>
+ </denylist>
+</root>
diff --git a/vcl/skia/win/gdiimpl.cxx b/vcl/skia/win/gdiimpl.cxx
new file mode 100644
index 000000000..9d01d7257
--- /dev/null
+++ b/vcl/skia/win/gdiimpl.cxx
@@ -0,0 +1,441 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <sal/config.h>
+
+#include <skia/win/gdiimpl.hxx>
+
+#include <win/saldata.hxx>
+#include <vcl/skia/SkiaHelper.hxx>
+#include <skia/utils.hxx>
+#include <skia/zone.hxx>
+#include <skia/win/font.hxx>
+#include <comphelper/scopeguard.hxx>
+#include <comphelper/windowserrorstring.hxx>
+#include <sal/log.hxx>
+
+#include <SkCanvas.h>
+#include <SkPaint.h>
+#include <SkPixelRef.h>
+#include <SkTypeface_win.h>
+#include <SkFont.h>
+#include <SkFontMgr.h>
+#include <tools/sk_app/win/WindowContextFactory_win.h>
+#include <tools/sk_app/WindowContext.h>
+
+#include <windows.h>
+
+using namespace SkiaHelper;
+
+WinSkiaSalGraphicsImpl::WinSkiaSalGraphicsImpl(WinSalGraphics& rGraphics,
+ SalGeometryProvider* mpProvider)
+ : SkiaSalGraphicsImpl(rGraphics, mpProvider)
+ , mWinParent(rGraphics)
+{
+}
+
+void WinSkiaSalGraphicsImpl::createWindowSurfaceInternal(bool forceRaster)
+{
+ assert(!mWindowContext);
+ assert(!mSurface);
+ SkiaZone zone;
+ sk_app::DisplayParams displayParams;
+ assert(GetWidth() > 0 && GetHeight() > 0);
+ displayParams.fSurfaceProps = *surfaceProps();
+ switch (forceRaster ? RenderRaster : renderMethodToUse())
+ {
+ case RenderRaster:
+ mWindowContext = sk_app::window_context_factory::MakeRasterForWin(mWinParent.gethWnd(),
+ displayParams);
+ if (mWindowContext)
+ mSurface = mWindowContext->getBackbufferSurface();
+ break;
+ case RenderVulkan:
+ mWindowContext = sk_app::window_context_factory::MakeVulkanForWin(mWinParent.gethWnd(),
+ displayParams);
+ // See flushSurfaceToWindowContext().
+ if (mWindowContext)
+ mSurface = createSkSurface(GetWidth(), GetHeight());
+ break;
+ case RenderMetal:
+ abort();
+ break;
+ }
+}
+
+void WinSkiaSalGraphicsImpl::freeResources() {}
+
+void WinSkiaSalGraphicsImpl::Flush() { performFlush(); }
+
+bool WinSkiaSalGraphicsImpl::TryRenderCachedNativeControl(ControlCacheKey const& rControlCacheKey,
+ int nX, int nY)
+{
+ static bool gbCacheEnabled = !getenv("SAL_WITHOUT_WIDGET_CACHE");
+ if (!gbCacheEnabled)
+ return false;
+
+ auto& controlsCache = SkiaControlsCache::get();
+ SkiaControlCacheType::const_iterator iterator = controlsCache.find(rControlCacheKey);
+ if (iterator == controlsCache.end())
+ return false;
+
+ preDraw();
+ SAL_INFO("vcl.skia.trace", "tryrendercachednativecontrol("
+ << this << "): "
+ << SkIRect::MakeXYWH(nX, nY, iterator->second->width(),
+ iterator->second->height()));
+ addUpdateRegion(
+ SkRect::MakeXYWH(nX, nY, iterator->second->width(), iterator->second->height()));
+ mSurface->getCanvas()->drawImage(iterator->second, nX, nY);
+ postDraw();
+ return true;
+}
+
+bool WinSkiaSalGraphicsImpl::RenderAndCacheNativeControl(CompatibleDC& rWhite, CompatibleDC& rBlack,
+ int nX, int nY,
+ ControlCacheKey& aControlCacheKey)
+{
+ assert(dynamic_cast<SkiaCompatibleDC*>(&rWhite));
+ assert(dynamic_cast<SkiaCompatibleDC*>(&rBlack));
+
+ sk_sp<SkImage> image = static_cast<SkiaCompatibleDC&>(rBlack).getAsImageDiff(
+ static_cast<SkiaCompatibleDC&>(rWhite));
+ preDraw();
+ SAL_INFO("vcl.skia.trace",
+ "renderandcachednativecontrol("
+ << this << "): " << SkIRect::MakeXYWH(nX, nY, image->width(), image->height()));
+ addUpdateRegion(SkRect::MakeXYWH(nX, nY, image->width(), image->height()));
+ mSurface->getCanvas()->drawImage(image, nX, nY);
+ postDraw();
+
+ if (!aControlCacheKey.canCacheControl())
+ return true;
+ SkiaControlCachePair pair(aControlCacheKey, std::move(image));
+ SkiaControlsCache::get().insert(std::move(pair));
+ return true;
+}
+
+sk_sp<SkTypeface> WinSkiaSalGraphicsImpl::createDirectWriteTypeface(HDC hdc, HFONT hfont) try
+{
+ using sal::systools::ThrowIfFailed;
+ if (!dwriteDone)
+ {
+ ThrowIfFailed(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory),
+ reinterpret_cast<IUnknown**>(&dwriteFactory)),
+ SAL_WHERE);
+ ThrowIfFailed(dwriteFactory->GetGdiInterop(&dwriteGdiInterop), SAL_WHERE);
+ dwriteFontMgr = SkFontMgr_New_DirectWrite(dwriteFactory.get());
+ dwriteDone = true;
+ }
+ if (!dwriteFontMgr)
+ return nullptr;
+
+ // tdf#137122: We need to get the exact same font as HFONT refers to,
+ // since VCL core computes things like glyph ids based on that, and getting
+ // a different font could lead to mismatches (e.g. if there's a slightly
+ // different version of the same font installed system-wide).
+ // For that CreateFromFaceFromHdc() is necessary. The simpler
+ // CreateFontFromLOGFONT() seems to search for the best matching font,
+ // which may not be the exact font.
+ sal::systools::COMReference<IDWriteFontFace> fontFace;
+ {
+ comphelper::ScopeGuard g(
+ [ hdc, oldFont(SelectFont(hdc, hfont)) ] { SelectFont(hdc, oldFont); });
+ ThrowIfFailed(dwriteGdiInterop->CreateFontFaceFromHdc(hdc, &fontFace), SAL_WHERE);
+ }
+
+ sal::systools::COMReference<IDWriteFontCollection> collection;
+ ThrowIfFailed(dwriteFactory->GetSystemFontCollection(&collection), SAL_WHERE);
+ sal::systools::COMReference<IDWriteFont> font;
+ // As said above, this fails for our fonts.
+ if (FAILED(collection->GetFontFromFontFace(fontFace.get(), &font)))
+ {
+ // If not found in system collection, try our private font collection.
+ // If that's not possible we'll fall back to Skia's GDI-based font rendering.
+ if (!dwritePrivateCollection
+ || FAILED(dwritePrivateCollection->GetFontFromFontFace(fontFace.get(), &font)))
+ {
+ // Our private fonts are installed using AddFontResourceExW( FR_PRIVATE )
+ // and that does not make them available to the DWrite system font
+ // collection. For such cases attempt to update a collection of
+ // private fonts with this newly used font.
+
+ sal::systools::COMReference<IDWriteFactory3> dwriteFactory3;
+ ThrowIfFailed(dwriteFactory->QueryInterface(&dwriteFactory3), SAL_WHERE);
+
+ if (!dwriteFontSetBuilder)
+ ThrowIfFailed(dwriteFactory3->CreateFontSetBuilder(&dwriteFontSetBuilder),
+ SAL_WHERE);
+
+ UINT32 numberOfFiles;
+ ThrowIfFailed(fontFace->GetFiles(&numberOfFiles, nullptr), SAL_WHERE);
+ if (numberOfFiles != 1)
+ return nullptr;
+
+ sal::systools::COMReference<IDWriteFontFile> fontFile;
+ ThrowIfFailed(fontFace->GetFiles(&numberOfFiles, &fontFile), SAL_WHERE);
+
+ BOOL isSupported;
+ DWRITE_FONT_FILE_TYPE fileType;
+ UINT32 numberOfFonts;
+ ThrowIfFailed(fontFile->Analyze(&isSupported, &fileType, nullptr, &numberOfFonts),
+ SAL_WHERE);
+ if (!isSupported)
+ return nullptr;
+
+ // For each font within the font file, get a font face reference and add to the builder.
+ for (UINT32 fontIndex = 0; fontIndex < numberOfFonts; ++fontIndex)
+ {
+ sal::systools::COMReference<IDWriteFontFaceReference> fontFaceReference;
+ if (FAILED(dwriteFactory3->CreateFontFaceReference(fontFile.get(), fontIndex,
+ DWRITE_FONT_SIMULATIONS_NONE,
+ &fontFaceReference)))
+ continue;
+
+ // Leave it to DirectWrite to read properties directly out of the font files
+ dwriteFontSetBuilder->AddFontFaceReference(fontFaceReference.get());
+ }
+
+ sal::systools::COMReference<IDWriteFontSet> fontSet;
+ ThrowIfFailed(dwriteFontSetBuilder->CreateFontSet(&fontSet), SAL_WHERE);
+ ThrowIfFailed(dwriteFactory3->CreateFontCollectionFromFontSet(fontSet.get(),
+ &dwritePrivateCollection),
+ SAL_WHERE);
+ ThrowIfFailed(dwritePrivateCollection->GetFontFromFontFace(fontFace.get(), &font),
+ SAL_WHERE);
+ }
+ }
+ sal::systools::COMReference<IDWriteFontFamily> fontFamily;
+ ThrowIfFailed(font->GetFontFamily(&fontFamily), SAL_WHERE);
+ return sk_sp<SkTypeface>(
+ SkCreateTypefaceDirectWrite(dwriteFontMgr, fontFace.get(), font.get(), fontFamily.get()));
+}
+catch (const sal::systools::ComError& e)
+{
+ SAL_DETAIL_LOG_STREAM(SAL_DETAIL_ENABLE_LOG_INFO, ::SAL_DETAIL_LOG_LEVEL_INFO, "vcl.skia",
+ e.what(),
+ "HRESULT 0x" << OUString::number(e.GetHresult(), 16) << ": "
+ << WindowsErrorStringFromHRESULT(e.GetHresult()));
+ return nullptr;
+}
+
+bool WinSkiaSalGraphicsImpl::DrawTextLayout(const GenericSalLayout& rLayout)
+{
+ assert(dynamic_cast<const SkiaWinFontInstance*>(&rLayout.GetFont()));
+ const SkiaWinFontInstance* pWinFont
+ = static_cast<const SkiaWinFontInstance*>(&rLayout.GetFont());
+ const HFONT hLayoutFont = pWinFont->GetHFONT();
+ double hScale = pWinFont->getHScale();
+ LOGFONTW logFont;
+ if (GetObjectW(hLayoutFont, sizeof(logFont), &logFont) == 0)
+ {
+ assert(false);
+ return false;
+ }
+ sk_sp<SkTypeface> typeface = pWinFont->GetSkiaTypeface();
+ if (!typeface)
+ {
+ typeface = createDirectWriteTypeface(mWinParent.getHDC(), hLayoutFont);
+ bool dwrite = true;
+ if (!typeface) // fall back to GDI text rendering
+ {
+ // If lfWidth is kept, then with hScale != 1 characters get too wide, presumably
+ // because the horizontal scaling gets applied twice if GDI is used for drawing (tdf#141715).
+ // Using lfWidth /= hScale gives slightly incorrect sizes, for a reason I don't understand.
+ // LOGFONT docs say that 0 means GDI will find out the right value on its own somehow,
+ // and it apparently works.
+ logFont.lfWidth = 0;
+ // Reset LOGFONT orientation, the proper orientation is applied by drawGenericLayout(),
+ // and keeping this would make it get applied once more when doing the actual GDI drawing.
+ // Resetting it here does not seem to cause any problem.
+ logFont.lfOrientation = 0;
+ logFont.lfEscapement = 0;
+ typeface.reset(SkCreateTypefaceFromLOGFONT(logFont));
+ dwrite = false;
+ if (!typeface)
+ return false;
+ }
+ // Cache the typeface.
+ const_cast<SkiaWinFontInstance*>(pWinFont)->SetSkiaTypeface(typeface, dwrite);
+ }
+
+ SkFont font(typeface);
+
+ bool bSubpixelPositioning = mWinParent.getTextRenderModeForResolutionIndependentLayoutEnabled();
+ SkFont::Edging ePreferredAliasing
+ = bSubpixelPositioning ? SkFont::Edging::kSubpixelAntiAlias : fontEdging;
+ if (bSubpixelPositioning)
+ {
+ // note that SkFont defaults to a BaselineSnap of true, so I think really only
+ // subpixel in text direction
+ font.setSubpixel(true);
+ }
+ font.setEdging(logFont.lfQuality == NONANTIALIASED_QUALITY ? SkFont::Edging::kAlias
+ : ePreferredAliasing);
+
+ const vcl::font::FontSelectPattern& rFSD = pWinFont->GetFontSelectPattern();
+ int nHeight = rFSD.mnHeight;
+ int nWidth = rFSD.mnWidth ? rFSD.mnWidth : nHeight;
+ if (nWidth == 0 || nHeight == 0)
+ return false;
+ font.setSize(nHeight);
+ font.setScaleX(hScale);
+
+ // Unlike with Freetype-based font handling, use height even in vertical mode,
+ // additionally multiply it by horizontal scale to get the proper
+ // size and then scale the width back, otherwise the height would
+ // not be correct. I don't know why this is inconsistent.
+ SkFont verticalFont(font);
+ verticalFont.setSize(nHeight * hScale);
+ verticalFont.setScaleX(1.0 / hScale);
+
+ assert(dynamic_cast<SkiaSalGraphicsImpl*>(mWinParent.GetImpl()));
+ SkiaSalGraphicsImpl* impl = static_cast<SkiaSalGraphicsImpl*>(mWinParent.GetImpl());
+ COLORREF color = ::GetTextColor(mWinParent.getHDC());
+ Color salColor(GetRValue(color), GetGValue(color), GetBValue(color));
+ impl->drawGenericLayout(rLayout, salColor, font, verticalFont);
+ return true;
+}
+
+SkFont::Edging WinSkiaSalGraphicsImpl::fontEdging;
+
+void WinSkiaSalGraphicsImpl::initFontInfo()
+{
+ // Skia needs to be explicitly told what kind of antialiasing should be used,
+ // get it from system settings. This does not actually matter for the text
+ // rendering itself, since Skia has been patched to simply use the setting
+ // from the LOGFONT, which gets set by VCL's ImplGetLogFontFromFontSelect()
+ // and that one normally uses DEFAULT_QUALITY, so Windows will select
+ // the appropriate AA setting. But Skia internally chooses the format to which
+ // the glyphs will be rendered based on this setting (subpixel AA requires colors,
+ // others do not).
+ fontEdging = SkFont::Edging::kAlias;
+ SkPixelGeometry pixelGeometry = kUnknown_SkPixelGeometry;
+ BOOL set;
+ if (SystemParametersInfo(SPI_GETFONTSMOOTHING, 0, &set, 0) && set)
+ {
+ UINT set2;
+ if (SystemParametersInfo(SPI_GETFONTSMOOTHINGTYPE, 0, &set2, 0)
+ && set2 == FE_FONTSMOOTHINGCLEARTYPE)
+ {
+ fontEdging = SkFont::Edging::kSubpixelAntiAlias;
+ if (SystemParametersInfo(SPI_GETFONTSMOOTHINGORIENTATION, 0, &set2, 0)
+ && set2 == FE_FONTSMOOTHINGORIENTATIONBGR)
+ // No idea how to tell if it's horizontal or vertical.
+ pixelGeometry = kBGR_H_SkPixelGeometry;
+ else
+ pixelGeometry = kRGB_H_SkPixelGeometry; // default
+ }
+ else
+ fontEdging = SkFont::Edging::kAntiAlias;
+ }
+ setPixelGeometry(pixelGeometry);
+}
+
+void WinSkiaSalGraphicsImpl::ClearDevFontCache()
+{
+ dwriteFontMgr.reset();
+ dwriteFontSetBuilder.clear();
+ dwritePrivateCollection.clear();
+ dwriteFactory.clear();
+ dwriteGdiInterop.clear();
+ dwriteDone = false;
+ initFontInfo(); // get font info again, just in case
+}
+
+SkiaCompatibleDC::SkiaCompatibleDC(SalGraphics& rGraphics, int x, int y, int width, int height)
+ : CompatibleDC(rGraphics, x, y, width, height, false)
+{
+}
+
+sk_sp<SkImage> SkiaCompatibleDC::getAsImageDiff(const SkiaCompatibleDC& white) const
+{
+ SkiaZone zone;
+ assert(maRects.mnSrcWidth == white.maRects.mnSrcWidth
+ || maRects.mnSrcHeight == white.maRects.mnSrcHeight);
+ SkBitmap tmpBitmap;
+ if (!tmpBitmap.tryAllocPixels(SkImageInfo::Make(maRects.mnSrcWidth, maRects.mnSrcHeight,
+ kBGRA_8888_SkColorType, kPremul_SkAlphaType),
+ maRects.mnSrcWidth * 4))
+ abort();
+ // Native widgets are drawn twice on black/white background to synthetize alpha
+ // (commit c6b66646870cb2bffaa73565affcf80bf74e0b5c). The problem is that
+ // most widgets when drawn on transparent background are drawn properly (and the result
+ // is in premultiplied alpha format), some such as "Edit" (used by ControlType::Editbox)
+ // keep the alpha channel as transparent. Therefore the alpha is actually computed
+ // from the difference in the premultiplied red channels when drawn one black and on white.
+ // Alpha is computed as "alpha = 1.0 - abs(black.red - white.red)".
+ // I doubt this can be done using Skia, so do it manually here. Fortunately
+ // the bitmaps should be fairly small and are cached.
+ uint32_t* dest = tmpBitmap.getAddr32(0, 0);
+ assert(dest == tmpBitmap.getPixels());
+ const sal_uInt32* src = mpData;
+ const sal_uInt32* whiteSrc = white.mpData;
+ uint32_t* end = dest + tmpBitmap.width() * tmpBitmap.height();
+ while (dest < end)
+ {
+ uint32_t alpha = 255 - abs(int(*src & 0xff) - int(*whiteSrc & 0xff));
+ *dest = (*src & 0x00ffffff) | (alpha << 24);
+ ++dest;
+ ++src;
+ ++whiteSrc;
+ }
+ tmpBitmap.notifyPixelsChanged();
+ tmpBitmap.setImmutable();
+ sk_sp<SkSurface> surface = createSkSurface(tmpBitmap.width(), tmpBitmap.height());
+ SkPaint paint;
+ paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha
+ SkCanvas* canvas = surface->getCanvas();
+ canvas->save();
+ // The data we got is upside-down.
+ SkMatrix matrix;
+ matrix.preTranslate(0, tmpBitmap.height());
+ matrix.setConcat(matrix, SkMatrix::Scale(1, -1));
+ canvas->concat(matrix);
+ canvas->drawImage(tmpBitmap.asImage(), 0, 0, SkSamplingOptions(), &paint);
+ canvas->restore();
+ return makeCheckedImageSnapshot(surface);
+}
+
+SkiaControlsCache::SkiaControlsCache()
+ : cache(200)
+{
+}
+
+SkiaControlCacheType& SkiaControlsCache::get()
+{
+ SalData* data = GetSalData();
+ if (!data->m_pSkiaControlsCache)
+ data->m_pSkiaControlsCache.reset(new SkiaControlsCache);
+ return data->m_pSkiaControlsCache->cache;
+}
+
+namespace
+{
+std::unique_ptr<sk_app::WindowContext> createVulkanWindowContext(bool /*temporary*/)
+{
+ SkiaZone zone;
+ sk_app::DisplayParams displayParams;
+ return sk_app::window_context_factory::MakeVulkanForWin(nullptr, displayParams);
+}
+}
+
+void WinSkiaSalGraphicsImpl::prepareSkia()
+{
+ initFontInfo();
+ SkiaHelper::prepareSkia(createVulkanWindowContext);
+}
+
+void WinSkiaSalGraphicsImpl::ClearNativeControlCache()
+{
+ SalData* data = GetSalData();
+ data->m_pSkiaControlsCache.reset();
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/skia/x11/gdiimpl.cxx b/vcl/skia/x11/gdiimpl.cxx
new file mode 100644
index 000000000..6a7cce14d
--- /dev/null
+++ b/vcl/skia/x11/gdiimpl.cxx
@@ -0,0 +1,175 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * Some of this code is based on Skia source code, covered by the following
+ * license notice (see readlicense_oo for the full license):
+ *
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ */
+
+#include <skia/x11/gdiimpl.hxx>
+
+#include <tools/sk_app/unix/WindowContextFactory_unix.h>
+
+#include <skia/utils.hxx>
+#include <skia/zone.hxx>
+
+#include <X11/Xutil.h>
+
+using namespace SkiaHelper;
+
+X11SkiaSalGraphicsImpl::X11SkiaSalGraphicsImpl(X11SalGraphics& rParent)
+ : SkiaSalGraphicsImpl(rParent, rParent.GetGeometryProvider())
+ , mX11Parent(rParent)
+{
+}
+
+void X11SkiaSalGraphicsImpl::Init()
+{
+ // The m_pFrame and m_pVDev pointers are updated late in X11
+ setProvider(mX11Parent.GetGeometryProvider());
+ SkiaSalGraphicsImpl::Init();
+}
+
+void X11SkiaSalGraphicsImpl::createWindowSurfaceInternal(bool forceRaster)
+{
+ assert(!mWindowContext);
+ assert(!mSurface);
+ assert(mX11Parent.GetDrawable() != None);
+ RenderMethod renderMethod = forceRaster ? RenderRaster : renderMethodToUse();
+ mScaling = getWindowScaling();
+ mWindowContext = createWindowContext(mX11Parent.GetXDisplay(), mX11Parent.GetDrawable(),
+ &mX11Parent.GetVisual(), GetWidth() * mScaling,
+ GetHeight() * mScaling, renderMethod, false);
+ if (mWindowContext)
+ {
+ // See flushSurfaceToWindowContext().
+ if (renderMethod == RenderRaster)
+ mSurface = mWindowContext->getBackbufferSurface();
+ else
+ mSurface = createSkSurface(GetWidth(), GetHeight());
+ }
+}
+
+std::unique_ptr<sk_app::WindowContext>
+X11SkiaSalGraphicsImpl::createWindowContext(Display* display, Drawable drawable,
+ const XVisualInfo* visual, int width, int height,
+ RenderMethod renderMethod, bool temporary)
+{
+ SkiaZone zone;
+ sk_app::DisplayParams displayParams;
+ displayParams.fColorType = kN32_SkColorType;
+#if defined LINUX
+ // WORKAROUND: VSync causes freezes that can even temporarily freeze the entire desktop.
+ // This happens even with the latest 450.66 drivers despite them claiming a fix for vsync.
+ // https://forums.developer.nvidia.com/t/hangs-freezes-when-vulkan-v-sync-vk-present-mode-fifo-khr-is-enabled/67751
+ if (getVendor() == DriverBlocklist::VendorNVIDIA)
+ displayParams.fDisableVsync = true;
+#endif
+ sk_app::window_context_factory::XlibWindowInfo winInfo;
+ assert(display);
+ winInfo.fDisplay = display;
+ winInfo.fWindow = drawable;
+ winInfo.fFBConfig = nullptr; // not used
+ winInfo.fVisualInfo = const_cast<XVisualInfo*>(visual);
+ assert(winInfo.fVisualInfo->visual != nullptr); // make sure it's not an uninitialized SalVisual
+ winInfo.fWidth = width;
+ winInfo.fHeight = height;
+#if defined DBG_UTIL && !defined NDEBUG
+ // Our patched Skia has VulkanWindowContext that shares grDirectContext, which requires
+ // that the X11 visual is always the same. Ensure it is so.
+ static VisualID checkVisualID = -1U;
+ // Exception is for the temporary case during startup, when SkiaHelper's
+ // checkDeviceDenylisted() needs a WindowContext and may be called before SalVisual
+ // is ready.
+ if (!temporary)
+ {
+ assert(checkVisualID == -1U || winInfo.fVisualInfo->visualid == checkVisualID);
+ checkVisualID = winInfo.fVisualInfo->visualid;
+ }
+#else
+ (void)temporary;
+#endif
+ switch (renderMethod)
+ {
+ case RenderRaster:
+ // Make sure we ask for color type that matches the X11 visual. If red mask
+ // is larger value than blue mask, then on little endian this means blue is first.
+ // This should also preferably match SK_R32_SHIFT set in config_skia.h, as that
+ // improves performance, the common setup seems to be BGRA (possibly because of
+ // choosing OpenGL-capable visual).
+ displayParams.fColorType
+ = (visual->red_mask > visual->blue_mask ? kBGRA_8888_SkColorType
+ : kRGBA_8888_SkColorType);
+ return sk_app::window_context_factory::MakeRasterForXlib(winInfo, displayParams);
+ case RenderVulkan:
+ return sk_app::window_context_factory::MakeVulkanForXlib(winInfo, displayParams);
+ case RenderMetal:
+ abort();
+ break;
+ }
+ abort();
+}
+
+bool X11SkiaSalGraphicsImpl::avoidRecreateByResize() const
+{
+ if (SkiaSalGraphicsImpl::avoidRecreateByResize())
+ return true;
+ if (!mSurface || isOffscreen())
+ return false;
+ // Skia's WindowContext uses actual dimensions of the X window, which due to X11 being
+ // asynchronous may be temporarily different from what VCL thinks are the dimensions.
+ // That can lead to us repeatedly calling recreateSurface() because of "incorrect"
+ // size, and we otherwise need to check for size changes, because VCL does not inform us.
+ // Avoid the problem here by checking the size of the X window and bail out if Skia
+ // would just return the same size as it is now.
+ Window r;
+ int x, y;
+ unsigned int w, h, border, depth;
+ XGetGeometry(mX11Parent.GetXDisplay(), mX11Parent.GetDrawable(), &r, &x, &y, &w, &h, &border,
+ &depth);
+ return mSurface->width() == int(w) && mSurface->height() == int(h);
+}
+
+void X11SkiaSalGraphicsImpl::freeResources() {}
+
+void X11SkiaSalGraphicsImpl::Flush() { performFlush(); }
+
+std::unique_ptr<sk_app::WindowContext> createVulkanWindowContext(bool temporary)
+{
+ SalDisplay* salDisplay = vcl_sal::getSalDisplay(GetGenericUnixSalData());
+ const XVisualInfo* visual;
+ XVisualInfo* visuals = nullptr;
+ if (!temporary)
+ visual = &salDisplay->GetVisual(salDisplay->GetDefaultXScreen());
+ else
+ {
+ // SalVisual from salDisplay may not be setup yet at this point, get
+ // info for the default visual.
+ XVisualInfo search;
+ search.visualid = XVisualIDFromVisual(
+ DefaultVisual(salDisplay->GetDisplay(), salDisplay->GetDefaultXScreen().getXScreen()));
+ int count;
+ visuals = XGetVisualInfo(salDisplay->GetDisplay(), VisualIDMask, &search, &count);
+ assert(count == 1);
+ visual = visuals;
+ }
+ std::unique_ptr<sk_app::WindowContext> ret = X11SkiaSalGraphicsImpl::createWindowContext(
+ salDisplay->GetDisplay(), None, visual, 1, 1, RenderVulkan, temporary);
+ if (temporary)
+ XFree(visuals);
+ return ret;
+}
+
+void X11SkiaSalGraphicsImpl::prepareSkia() { SkiaHelper::prepareSkia(createVulkanWindowContext); }
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/skia/x11/salvd.cxx b/vcl/skia/x11/salvd.cxx
new file mode 100644
index 000000000..73488b8a1
--- /dev/null
+++ b/vcl/skia/x11/salvd.cxx
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <vcl/sysdata.hxx>
+
+#include <unx/salunx.h>
+#include <unx/saldisp.hxx>
+#include <unx/salgdi.h>
+#include <unx/salvd.h>
+
+#include <skia/x11/salvd.hxx>
+
+void X11SalGraphics::Init(X11SkiaSalVirtualDevice* pDevice)
+{
+ SalDisplay* pDisplay = pDevice->GetDisplay();
+
+ m_nXScreen = pDevice->GetXScreenNumber();
+ maX11Common.m_pColormap = &pDisplay->GetColormap(m_nXScreen);
+
+ m_pVDev = pDevice;
+ m_pFrame = nullptr;
+
+ bWindow_ = pDisplay->IsDisplay();
+ bVirDev_ = true;
+
+ mxImpl->Init();
+}
+
+X11SkiaSalVirtualDevice::X11SkiaSalVirtualDevice(const SalGraphics& rGraphics, tools::Long nDX,
+ tools::Long nDY, const SystemGraphicsData* pData,
+ std::unique_ptr<X11SalGraphics> pNewGraphics)
+ : mpGraphics(std::move(pNewGraphics))
+ , mbGraphics(false)
+ , mnXScreen(0)
+{
+ assert(mpGraphics);
+
+ // TODO Check where a VirtualDevice is created from SystemGraphicsData
+ assert(pData == nullptr);
+ (void)pData;
+
+ mpDisplay = vcl_sal::getSalDisplay(GetGenericUnixSalData());
+ mnXScreen = static_cast<const X11SalGraphics&>(rGraphics).GetScreenNumber();
+ mnWidth = nDX;
+ mnHeight = nDY;
+ mpGraphics->Init(this);
+}
+
+X11SkiaSalVirtualDevice::~X11SkiaSalVirtualDevice() {}
+
+SalGraphics* X11SkiaSalVirtualDevice::AcquireGraphics()
+{
+ if (mbGraphics)
+ return nullptr;
+
+ if (mpGraphics)
+ mbGraphics = true;
+
+ return mpGraphics.get();
+}
+
+void X11SkiaSalVirtualDevice::ReleaseGraphics(SalGraphics*) { mbGraphics = false; }
+
+bool X11SkiaSalVirtualDevice::SetSize(tools::Long nDX, tools::Long nDY)
+{
+ if (!nDX)
+ nDX = 1;
+ if (!nDY)
+ nDY = 1;
+
+ mnWidth = nDX;
+ mnHeight = nDY;
+ if (mpGraphics)
+ mpGraphics->Init(this);
+
+ return true;
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/skia/x11/textrender.cxx b/vcl/skia/x11/textrender.cxx
new file mode 100644
index 000000000..7d344b8a2
--- /dev/null
+++ b/vcl/skia/x11/textrender.cxx
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed
+ * with this work for additional information regarding copyright
+ * ownership. The ASF licenses this file to you under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of
+ * the License at http://www.apache.org/licenses/LICENSE-2.0 .
+ */
+
+#include <sal/config.h>
+
+#include <skia/x11/textrender.hxx>
+
+#include <unx/fc_fontoptions.hxx>
+#include <unx/freetype_glyphcache.hxx>
+#include <vcl/svapp.hxx>
+#include <sallayout.hxx>
+#include <skia/gdiimpl.hxx>
+
+#include <SkFont.h>
+#include <SkFontMgr_fontconfig.h>
+
+void SkiaTextRender::DrawTextLayout(const GenericSalLayout& rLayout, const SalGraphics& rGraphics)
+{
+ const FreetypeFontInstance& rInstance = static_cast<FreetypeFontInstance&>(rLayout.GetFont());
+ const FreetypeFont& rFont = rInstance.GetFreetypeFont();
+ const vcl::font::FontSelectPattern& rFSD = rInstance.GetFontSelectPattern();
+ int nHeight = rFSD.mnHeight;
+ int nWidth = rFSD.mnWidth ? rFSD.mnWidth : nHeight;
+ if (nWidth == 0 || nHeight == 0)
+ return;
+
+ if (FreetypeFont::AlmostHorizontalDrainsRenderingPool(nWidth * 10 / nHeight, rFSD))
+ return;
+
+ if (!fontManager)
+ {
+ // Get the global FcConfig that our VCL fontconfig code uses, and refcount it.
+ fontManager = SkFontMgr_New_FontConfig(FcConfigReference(nullptr));
+ }
+ sk_sp<SkTypeface> typeface
+ = SkFontMgr_createTypefaceFromFcPattern(fontManager, rFont.GetFontOptions()->GetPattern());
+ SkFont font(typeface);
+ font.setSize(nHeight);
+ font.setScaleX(1.0 * nWidth / nHeight);
+ if (rFont.NeedsArtificialItalic())
+ font.setSkewX(1.0 * -0x4000L / 0x10000L);
+ if (rFont.NeedsArtificialBold())
+ font.setEmbolden(true);
+
+ bool bSubpixelPositioning = rGraphics.getTextRenderModeForResolutionIndependentLayoutEnabled();
+ SkFont::Edging ePreferredAliasing
+ = bSubpixelPositioning ? SkFont::Edging::kSubpixelAntiAlias : SkFont::Edging::kAntiAlias;
+ if (bSubpixelPositioning)
+ {
+ // note that SkFont defaults to a BaselineSnap of true, so I think really only
+ // subpixel in text direction
+ font.setSubpixel(true);
+
+ SkFontHinting eHinting = font.getHinting();
+ bool bAllowedHintStyle
+ = eHinting == SkFontHinting::kNone || eHinting == SkFontHinting::kSlight;
+ if (!bAllowedHintStyle)
+ font.setHinting(SkFontHinting::kSlight);
+ }
+
+ font.setEdging(rFont.GetAntialiasAdvice() ? ePreferredAliasing : SkFont::Edging::kAlias);
+
+ // Vertical font, use width as "height".
+ SkFont verticalFont(font);
+ verticalFont.setSize(nWidth);
+ verticalFont.setScaleX(1.0 * nHeight / nWidth);
+
+ assert(dynamic_cast<SkiaSalGraphicsImpl*>(rGraphics.GetImpl()));
+ SkiaSalGraphicsImpl* impl = static_cast<SkiaSalGraphicsImpl*>(rGraphics.GetImpl());
+ impl->drawGenericLayout(rLayout, mnTextColor, font, verticalFont);
+}
+
+void SkiaTextRender::ClearDevFontCache() { fontManager.reset(); }
+
+#if 0
+// SKIA TODO
+void FontConfigFontOptions::cairo_font_options_substitute(FcPattern* pPattern)
+{
+ ImplSVData* pSVData = ImplGetSVData();
+ const cairo_font_options_t* pFontOptions = pSVData->mpDefInst->GetCairoFontOptions();
+ if( !pFontOptions )
+ return;
+ cairo_ft_font_options_substitute(pFontOptions, pPattern);
+}
+#endif
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/vcl/skia/zone.cxx b/vcl/skia/zone.cxx
new file mode 100644
index 000000000..f95417366
--- /dev/null
+++ b/vcl/skia/zone.cxx
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <skia/zone.hxx>
+
+#include <officecfg/Office/Common.hxx>
+#include <com/sun/star/util/XFlushable.hpp>
+#include <com/sun/star/configuration/theDefaultProvider.hpp>
+
+#include <sal/log.hxx>
+#include <o3tl/unreachable.hxx>
+
+#include <vcl/skia/SkiaHelper.hxx>
+
+#include <config_skia.h>
+
+using namespace SkiaHelper;
+
+/**
+ * Called from a signal handler or watchdog thread if we get
+ * a crash or hang in some driver.
+ */
+void SkiaZone::hardDisable()
+{
+ // protect ourselves from double calling etc.
+ static bool bDisabled = false;
+ if (bDisabled)
+ return;
+
+ bDisabled = true;
+
+ // Instead of disabling Skia as a whole, only force the CPU-based
+ // raster mode, which should be safe as it does not use drivers.
+ std::shared_ptr<comphelper::ConfigurationChanges> xChanges(
+ comphelper::ConfigurationChanges::create());
+ officecfg::Office::Common::VCL::ForceSkiaRaster::set(true, xChanges);
+ xChanges->commit();
+
+ // Force synchronous config write
+ css::uno::Reference<css::util::XFlushable>(
+ css::configuration::theDefaultProvider::get(comphelper::getProcessComponentContext()),
+ css::uno::UNO_QUERY_THROW)
+ ->flush();
+}
+
+void SkiaZone::checkDebug(int nUnchanged, const CrashWatchdogTimingsValues& aTimingValues)
+{
+ SAL_INFO("vcl.watchdog", "Skia watchdog - unchanged "
+ << nUnchanged << " enter count " << enterCount()
+ << " breakpoints mid: " << aTimingValues.mnDisableEntries
+ << " max " << aTimingValues.mnAbortAfter);
+}
+
+const CrashWatchdogTimingsValues& SkiaZone::getCrashWatchdogTimingsValues()
+{
+ switch (renderMethodToUse())
+ {
+ case RenderVulkan:
+ case RenderMetal:
+ {
+#if defined(SK_RELEASE)
+ static const CrashWatchdogTimingsValues gpuValues = { 6, 20 }; /* 1.5s, 5s */
+#elif defined(SK_DEBUG)
+ static const CrashWatchdogTimingsValues gpuValues = { 60, 200 }; /* 15s, 50s */
+#else
+#error Unknown Skia debug/release setting.
+#endif
+ return gpuValues;
+ }
+ case RenderRaster:
+ {
+ // CPU-based operations with large images may take a noticeably long time,
+ // so use large values. CPU-based rendering shouldn't use any unstable drivers anyway.
+ static const CrashWatchdogTimingsValues rasterValues = { 600, 2000 }; /* 150s, 500s */
+ return rasterValues;
+ }
+ }
+ O3TL_UNREACHABLE;
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */