diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:51:28 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:51:28 +0000 |
commit | 940b4d1848e8c70ab7642901a68594e8016caffc (patch) | |
tree | eb72f344ee6c3d9b80a7ecc079ea79e9fba8676d /vcl/skia | |
parent | Initial commit. (diff) | |
download | libreoffice-940b4d1848e8c70ab7642901a68594e8016caffc.tar.xz libreoffice-940b4d1848e8c70ab7642901a68594e8016caffc.zip |
Adding upstream version 1:7.0.4.upstream/1%7.0.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vcl/skia')
-rw-r--r-- | vcl/skia/README | 35 | ||||
-rw-r--r-- | vcl/skia/SkiaHelper.cxx | 611 | ||||
-rw-r--r-- | vcl/skia/gdiimpl.cxx | 1829 | ||||
-rw-r--r-- | vcl/skia/salbmp.cxx | 819 | ||||
-rw-r--r-- | vcl/skia/skia_blacklist_vulkan.xml | 37 | ||||
-rw-r--r-- | vcl/skia/win/gdiimpl.cxx | 393 | ||||
-rw-r--r-- | vcl/skia/x11/gdiimpl.cxx | 161 | ||||
-rw-r--r-- | vcl/skia/x11/salvd.cxx | 86 | ||||
-rw-r--r-- | vcl/skia/x11/textrender.cxx | 80 | ||||
-rw-r--r-- | vcl/skia/zone.cxx | 76 |
10 files changed, 4127 insertions, 0 deletions
diff --git a/vcl/skia/README b/vcl/skia/README new file mode 100644 index 000000000..f1248d90c --- /dev/null +++ b/vcl/skia/README @@ -0,0 +1,35 @@ +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 debugging) +- Vulkan - Vulkan-based GPU drawing, this is the default + +There are more (OpenGL, Metal on Mac, etc.), but (as of now) they are not supported by VCL. + +GrContext 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 +GrContext. There is sk_app::WindowContext::getGrContext(), but each instance creates +its own GrContext, 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 GrContext object +and share it between instances. Additionally, using sk_app::WindowContext::SharedGrContext +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..343ebc284 --- /dev/null +++ b/vcl/skia/SkiaHelper.cxx @@ -0,0 +1,611 @@ +/* -*- 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/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 <SkCanvas.h> +#include <SkPaint.h> +#include <SkSurface.h> +#include <SkGraphics.h> +#include <skia_compiler.hxx> + +#ifdef DBG_UTIL +#include <fstream> +#endif + +namespace SkiaHelper +{ +static OUString getBlacklistFile() +{ + OUString url("$BRAND_BASE_DIR/" LIBO_SHARE_FOLDER); + rtl::Bootstrap::expandMacros(url); + + return url + "/skia/skia_blacklist_vulkan.xml"; +} + +static uint32_t driverVersion = 0; +uint32_t vendorId = 0; + +static OUString versionAsString(uint32_t version) +{ + return OUString::number(version >> 22) + "." + OUString::number((version >> 12) & 0x3ff) + "." + + OUString::number(version & 0xfff); +} + +static OUStringLiteral vendorAsString(uint32_t vendor) +{ + return DriverBlocklist::GetVendorNameFromId(vendor); +} + +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'); +} + +static void writeToLog(SvStream& stream, const char* key, const OUString& value) +{ + writeToLog(stream, key, OUStringToOString(value, RTL_TEXTENCODING_UTF8).getStr()); +} + +// Note that this function also logs system information about Vulkan. +static bool isVulkanBlacklisted(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 blacklisted + = DriverBlocklist::IsDeviceBlocked(getBlacklistFile(), DriverBlocklist::VersionType::Vulkan, + driverVersionString, vendorIdStr, deviceIdStr); + writeToLog(logFile, "Blacklisted", blacklisted ? "yes" : "no"); + return blacklisted; +} + +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()); +} + +static sk_app::VulkanWindowContext::SharedGrContext getTemporaryGrContext(); + +static void checkDeviceBlacklisted(bool blockDisable = false) +{ + static bool done = false; + if (!done) + { + SkiaZone zone; + + switch (renderMethodToUse()) + { + case RenderVulkan: + { + // First try if a GrContext already exists. + sk_app::VulkanWindowContext::SharedGrContext grContext + = sk_app::VulkanWindowContext::getSharedGrContext(); + if (!grContext.getGrContext()) + { + // 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 GrContext. + // Avoid the loop by creating a temporary GrContext + // 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. + grContext = getTemporaryGrContext(); + } + bool blacklisted = true; // assume the worst + if (grContext.getGrContext()) // Vulkan was initialized properly + { + blacklisted = isVulkanBlacklisted( + sk_app::VulkanWindowContext::getPhysDeviceProperties()); + SAL_INFO("vcl.skia", "Vulkan blacklisted: " << blacklisted); + } + else + SAL_INFO("vcl.skia", "Vulkan could not be initialized"); + if (blacklisted && !blockDisable) + { + disableRenderMethod(RenderVulkan); + writeSkiaRasterInfo(); + } + break; + } + case RenderRaster: + SAL_INFO("vcl.skia", "Using Skia raster mode"); + writeSkiaRasterInfo(); + return; // software, never blacklisted + } + 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 blacklisted + * * 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(); + // don't actually block if blacklisted, but log it if enabled, and also get the vendor id + checkDeviceBlacklisted(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(); + checkDeviceBlacklisted(); // switch to raster if driver is blacklisted + } + + 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; + } + if (strcmp(env, "vulkan") == 0) + { + methodToUse = RenderVulkan; + return true; + } + SAL_WARN("vcl.skia", "Unrecognized value of SAL_SKIA"); + abort(); + } + if (officecfg::Office::Common::VCL::ForceSkiaRaster::get()) + { + methodToUse = RenderRaster; + return true; + } + methodToUse = RenderVulkan; + 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; +} + +static sk_app::VulkanWindowContext::SharedGrContext* sharedGrContext; + +static std::unique_ptr<sk_app::WindowContext> (*createVulkanWindowContextFunction)(bool) = nullptr; +static void setCreateVulkanWindowContext(std::unique_ptr<sk_app::WindowContext> (*function)(bool)) +{ + createVulkanWindowContextFunction = function; +} + +GrContext* getSharedGrContext() +{ + SkiaZone zone; + assert(renderMethodToUse() == RenderVulkan); + if (sharedGrContext) + return sharedGrContext->getGrContext(); + // TODO mutex? + // Set up the shared GrContext from Skia's (patched) VulkanWindowContext, if it's been + // already set up. + sk_app::VulkanWindowContext::SharedGrContext context + = sk_app::VulkanWindowContext::getSharedGrContext(); + GrContext* grContext = context.getGrContext(); + if (grContext) + { + sharedGrContext = new sk_app::VulkanWindowContext::SharedGrContext(context); + return grContext; + } + static bool done = false; + if (done) + return nullptr; + done = true; + if (createVulkanWindowContextFunction == nullptr) + return nullptr; // not initialized properly (e.g. used from a VCL backend with no Skia support) + std::unique_ptr<sk_app::WindowContext> tmpContext = createVulkanWindowContextFunction(false); + // Set up using the shared context created by the call above, if successful. + context = sk_app::VulkanWindowContext::getSharedGrContext(); + grContext = context.getGrContext(); + if (grContext) + { + sharedGrContext = new sk_app::VulkanWindowContext::SharedGrContext(context); + return grContext; + } + disableRenderMethod(RenderVulkan); + return nullptr; +} + +static sk_app::VulkanWindowContext::SharedGrContext getTemporaryGrContext() +{ + if (createVulkanWindowContextFunction == nullptr) + return sk_app::VulkanWindowContext::SharedGrContext(); + std::unique_ptr<sk_app::WindowContext> tmpContext = createVulkanWindowContextFunction(true); + // Set up using the shared context created by the call above, if successful. + return sk_app::VulkanWindowContext::getSharedGrContext(); +} + +sk_sp<SkSurface> createSkSurface(int width, int height, SkColorType type) +{ + SkiaZone zone; + assert(type == kN32_SkColorType || type == kAlpha_8_SkColorType); + sk_sp<SkSurface> surface; + switch (SkiaHelper::renderMethodToUse()) + { + case SkiaHelper::RenderVulkan: + { + if (GrContext* grContext = getSharedGrContext()) + { + surface = SkSurface::MakeRenderTarget( + grContext, SkBudgeted::kNo, + SkImageInfo::Make(width, height, type, kPremul_SkAlphaType)); + if (surface) + { +#ifdef DBG_UTIL + prefillSurface(surface); +#endif + return surface; + } + SAL_WARN("vcl.skia", + "cannot create Vulkan GPU offscreen surface, falling back to Raster"); + } + break; + } + default: + break; + } + // Create raster surface as a fallback. + surface = SkSurface::MakeRaster(SkImageInfo::Make(width, height, type, kPremul_SkAlphaType)); + 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 (SkiaHelper::renderMethodToUse()) + { + case SkiaHelper::RenderVulkan: + { + if (GrContext* grContext = getSharedGrContext()) + { + sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget( + grContext, SkBudgeted::kNo, bitmap.info().makeAlphaType(kPremul_SkAlphaType)); + if (surface) + { + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha + surface->getCanvas()->drawBitmap(bitmap, 0, 0, &paint); + return makeCheckedImageSnapshot(surface); + } + // Try to fall back in non-debug builds. + SAL_WARN("vcl.skia", + "cannot create Vulkan 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; + int 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_cache would be simpler, but it doesn't support +// calculating cost of each item. +static std::list<ImageCacheItem>* imageCache = nullptr; +static int 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; + if (imageCache == nullptr) + imageCache = new std::list<ImageCacheItem>; + int size = 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 int MAX_CACHE_SIZE = 4 * 1000 * 1000 * 4; // 4x 1000px 32bpp images, 16MiB + while (imageCacheSize > MAX_CACHE_SIZE) + { + assert(!imageCache->empty()); + imageCacheSize -= imageCache->back().size; + SAL_INFO("vcl.skia.trace", + "least used removal " << image << ":" << imageCache->back().size); + imageCache->pop_back(); + } +} + +sk_sp<SkImage> findCachedImage(const OString& key) +{ + if (imageCache != nullptr) + { + 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) +{ + if (imageCache == nullptr) + return; + for (auto it = imageCache->begin(); it != imageCache->end();) + { + if (it->image == image) + { + imageCacheSize -= it->size; + assert(imageCacheSize >= 0); + it = imageCache->erase(it); + } + else + ++it; + } +} + +void cleanup() +{ + delete sharedGrContext; + sharedGrContext = nullptr; + delete imageCache; + imageCache = nullptr; + imageCacheSize = 0; +} + +// 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 context that is +// usually available only in the backend libs. +void prepareSkia(std::unique_ptr<sk_app::WindowContext> (*createVulkanWindowContext)(bool)) +{ + setCreateVulkanWindowContext(createVulkanWindowContext); + skiaSupportedByBackend = true; +} + +#ifdef DBG_UTIL +void prefillSurface(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)); + surface->getCanvas()->drawPaint(paint); +} + +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(); + std::ofstream ostream(file, std::ios::binary); + ostream.write(static_cast<const char*>(data->data()), data->size()); +} + +#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..bd497d7f8 --- /dev/null +++ b/vcl/skia/gdiimpl.cxx @@ -0,0 +1,1829 @@ +/* -*- 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/skia/SkiaHelper.hxx> +#include <skia/utils.hxx> +#include <skia/zone.hxx> + +#include <SkCanvas.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> + +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, + bool* hasOnlyOrthogonal = nullptr) +{ + const sal_uInt32 nPointCount(rPolygon.count()); + + if (nPointCount <= 1) + return; + + const bool bClosePath(rPolygon.isClosed()); + const bool bHasCurves(rPolygon.areControlPointsUsed()); + + bool bFirst = true; + + 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; + 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)) + { + 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) + { + rPath.close(); + } +} + +void addPolyPolygonToPath(const basegfx::B2DPolyPolygon& rPolyPolygon, SkPath& rPath, + bool* hasOnlyOrthogonal = nullptr) +{ + const sal_uInt32 nPolygonCount(rPolyPolygon.count()); + + if (nPolygonCount == 0) + return; + + 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 +} + +SkColor toSkColor(Color color) +{ + return SkColorSetARGB(255 - color.GetTransparency(), color.GetRed(), color.GetGreen(), + color.GetBlue()); +} + +SkColor toSkColorWithTransparency(Color aColor, double fTransparency) +{ + return SkColorSetA(toSkColor(aColor), 255 * (1.0 - fTransparency)); +} + +Color fromSkColor(SkColor color) +{ + return Color(255 - SkColorGetA(color), SkColorGetR(color), SkColorGetG(color), + SkColorGetB(color)); +} + +// 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); } + const char* get_debug_name(SkiaSalGraphicsImpl* pGraphics) + { + // 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 + const char* get_debug_name(SkiaSalGraphicsImpl*) { 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(false) + , mFlush(new SkiaFlushIdle(this)) +{ +} + +SkiaSalGraphicsImpl::~SkiaSalGraphicsImpl() +{ + assert(!mSurface); + assert(!mWindowContext); +} + +void SkiaSalGraphicsImpl::Init() {} + +void SkiaSalGraphicsImpl::createSurface() +{ + SkiaZone zone; + if (isOffscreen()) + createOffscreenSurface(); + else + createWindowSurface(); + mSurface->getCanvas()->save(); // see SetClipRegion() + mClipRegion = vcl::Region(tools::Rectangle(0, 0, GetWidth(), GetHeight())); + + // 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); + assert(!mWindowContext); + createWindowContext(forceRaster); + if (mWindowContext) + mSurface = mWindowContext->getBackbufferSurface(); + if (!mSurface) + { + switch (SkiaHelper::renderMethodToUse()) + { + case SkiaHelper::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 SkiaHelper::RenderRaster: + abort(); // This should not really happen, do not even try to cope with it. + } + } + mIsGPU = mSurface->getCanvas()->getGrContext() != nullptr; +#ifdef DBG_UTIL + SkiaHelper::prefillSurface(mSurface); +#endif +} + +void SkiaSalGraphicsImpl::createOffscreenSurface() +{ + SkiaZone zone; + assert(isOffscreen()); + assert(!mSurface); + assert(!mWindowContext); + // When created (especially on Windows), Init() gets called with size (0,0), which is invalid size + // for Skia. May happen also in rare cases such as shutting down (tdf#131939). + int width = std::max(1, GetWidth()); + int height = std::max(1, GetHeight()); + switch (SkiaHelper::renderMethodToUse()) + { + case SkiaHelper::RenderVulkan: + { + if (SkiaHelper::getSharedGrContext()) + { + mSurface = SkiaHelper::createSkSurface(width, height); + if (mSurface) + { + mIsGPU = mSurface->getCanvas()->getGrContext() != nullptr; + return; + } + } + break; + } + default: + break; + } + // Create raster surface as a fallback. + mSurface = SkiaHelper::createSkSurface(width, height); + assert(mSurface); + assert(!mSurface->getCanvas()->getGrContext()); // is not GPU-backed + mIsGPU = false; +} + +void SkiaSalGraphicsImpl::destroySurface() +{ + SkiaZone zone; + if (mSurface) + { + // check setClipRegion() invariant + assert(mSurface->getCanvas()->getSaveCount() == 2); + // if this fails, something forgot to use SkAutoCanvasRestore + assert(mSurface->getCanvas()->getTotalMatrix().isIdentity()); + } + // If we use e.g. Vulkan, we must destroy the surface before the context, + // otherwise destroying the surface will reference the context. This is + // handled by calling destroySurface() before destroying the context. + // However we also need to flush the surface before destroying it, + // otherwise when destroying the context later there still could be queued + // commands referring to the surface data. This is probably a Skia bug, + // but work around it here. + if (mSurface) + mSurface->flushAndSubmit(); + mSurface.reset(); + mWindowContext.reset(); + mIsGPU = false; +} + +void SkiaSalGraphicsImpl::DeInit() { destroySurface(); } + +void SkiaSalGraphicsImpl::preDraw() +{ + assert(comphelper::SolarMutex::get()->IsCurrentThread()); + SkiaZone::enter(); // matched in postDraw() + checkSurface(); + checkPendingDrawing(); +} + +void SkiaSalGraphicsImpl::postDraw() +{ + scheduleFlush(); + SkiaZone::leave(); // matched in preDraw() + // If there's a problem with the GPU context, abort. + if (GrContext* context = mSurface->getCanvas()->getGrContext()) + { + // 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() != mSurface->width() || GetHeight() != mSurface->height()) + { + if (avoidRecreateByResize()) + return; + + if (!GetWidth() || !GetHeight()) + { + SAL_WARN("vcl.skia", "recreate(" << this << "): can't create empty surface " + << Size(GetWidth(), GetHeight()) + << " => keeping old one!"); + return; + } + + { + 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 = SkiaHelper::makeCheckedImageSnapshot(mSurface); + } + + destroySurface(); + createSurface(); + + if (snapshot) + { + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is + mSurface->getCanvas()->drawImage(snapshot, 0, 0, &paint); + } + SAL_INFO("vcl.skia.trace", "recreate(" << this << "): old " << oldSize << " new " + << Size(mSurface->width(), mSurface->height()) + << " requested " + << Size(GetWidth(), GetHeight())); + } + } +} + +void SkiaSalGraphicsImpl::flushDrawing() +{ + if (!mSurface) + return; + checkPendingDrawing(); + if (mXorMode) + applyXor(); + mSurface->flushAndSubmit(); +} + +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(); + // SkCanvas::clipRegion() can only further reduce the clip region, + // but we need to set the given region, which may extend it. + // So handle that by always having the full clip region saved on the stack + // and always go back to that. SkCanvas::restore() only affects the clip + // and the matrix. + assert(canvas->getSaveCount() == 2); // = there is just one save() + canvas->restore(); + 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); + 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; } + +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) +{ + if (mXorMode == set) + return; + checkPendingDrawing(); + SAL_INFO("vcl.skia.trace", "setxormode(" << this << "): " << set); + if (set) + mXorRegion.setEmpty(); + else + applyXor(); + mXorMode = set; +} + +SkCanvas* SkiaSalGraphicsImpl::getXorCanvas() +{ + SkiaZone zone; + assert(mXorMode); + // Skia does not implement xor drawing, so we need to handle it manually by redirecting + // to a temporary SkBitmap and then doing the xor operation on the data ourselves. + // There's no point in using SkSurface for GPU, we'd immediately need to get the pixels back. + if (!mXorCanvas) + { + // Use unpremultiplied alpha (see xor applying in applyXor()). + if (!mXorBitmap.tryAllocPixels(mSurface->imageInfo().makeAlphaType(kUnpremul_SkAlphaType))) + abort(); + mXorBitmap.eraseARGB(0, 0, 0, 0); + mXorCanvas = std::make_unique<SkCanvas>(mXorBitmap); + setCanvasClipRegion(mXorCanvas.get(), mClipRegion); + } + return mXorCanvas.get(); +} + +void SkiaSalGraphicsImpl::applyXor() +{ + // Apply the result from the temporary bitmap manually. This is indeed + // slow, but it doesn't seem to be needed often and is optimized + // in each operation by extending mXorRegion with the area that should be + // updated. + assert(mXorMode); + if (!mSurface || !mXorCanvas + || !mXorRegion.op(SkIRect::MakeXYWH(0, 0, mSurface->width(), mSurface->height()), + SkRegion::kIntersect_Op)) + { + mXorRegion.setEmpty(); + return; + } + SAL_INFO("vcl.skia.trace", "applyxor(" << this << "): " << mXorRegion); + // Copy the surface contents to another pixmap. + SkBitmap surfaceBitmap; + // Use unpremultiplied alpha format, so that we do not have to do the conversions to get + // the RGB and back (Skia will do it when converting, but it'll be presumably faster at it). + if (!surfaceBitmap.tryAllocPixels(mSurface->imageInfo().makeAlphaType(kUnpremul_SkAlphaType))) + abort(); + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is + SkCanvas canvas(surfaceBitmap); + canvas.drawImageRect(SkiaHelper::makeCheckedImageSnapshot(mSurface), mXorRegion.getBounds(), + SkRect::Make(mXorRegion.getBounds()), &paint); + // xor to surfaceBitmap + assert(surfaceBitmap.info().alphaType() == kUnpremul_SkAlphaType); + assert(mXorBitmap.info().alphaType() == kUnpremul_SkAlphaType); + assert(surfaceBitmap.bytesPerPixel() == 4); + assert(mXorBitmap.bytesPerPixel() == 4); + for (SkRegion::Iterator it(mXorRegion); !it.done(); it.next()) + { + for (int y = it.rect().top(); y < it.rect().bottom(); ++y) + { + uint8_t* data = static_cast<uint8_t*>(surfaceBitmap.getAddr(it.rect().x(), y)); + const uint8_t* xordata = static_cast<uint8_t*>(mXorBitmap.getAddr(it.rect().x(), y)); + for (int x = 0; x < it.rect().width(); ++x) + { + *data++ ^= *xordata++; + *data++ ^= *xordata++; + *data++ ^= *xordata++; + // alpha is not xor-ed + data++; + xordata++; + } + } + } + surfaceBitmap.notifyPixelsChanged(); + mSurface->getCanvas()->drawBitmapRect(surfaceBitmap, mXorRegion.getBounds(), + SkRect::Make(mXorRegion.getBounds()), &paint); + mXorCanvas.reset(); + mXorBitmap.reset(); + mXorRegion.setEmpty(); +} + +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(long nX, long nY) { drawPixel(nX, nY, mLineColor); } + +void SkiaSalGraphicsImpl::drawPixel(long nX, long nY, Color nColor) +{ + if (nColor == SALCOLOR_NONE) + return; + preDraw(); + SAL_INFO("vcl.skia.trace", "drawpixel(" << this << "): " << Point(nX, nY) << ":" << nColor); + addXorRegion(SkRect::MakeXYWH(nX, nY, 1, 1)); + SkPaint paint; + paint.setColor(toSkColor(nColor)); + // Apparently drawPixel() is actually expected to set the pixel and not draw it. + paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha + getDrawCanvas()->drawPoint(toSkX(nX), toSkY(nY), paint); + postDraw(); +} + +void SkiaSalGraphicsImpl::drawLine(long nX1, long nY1, long nX2, long nY2) +{ + if (mLineColor == SALCOLOR_NONE) + return; + preDraw(); + SAL_INFO("vcl.skia.trace", "drawline(" << this << "): " << Point(nX1, nY1) << "->" + << Point(nX2, nY2) << ":" << mLineColor); + addXorRegion(SkRect::MakeLTRB(nX1, nY1, nX2, nY2).makeSorted()); + SkPaint paint; + paint.setColor(toSkColor(mLineColor)); + paint.setAntiAlias(mParent.getAntiAliasB2DDraw()); + getDrawCanvas()->drawLine(toSkX(nX1), toSkY(nY1), toSkX(nX2), toSkY(nY2), paint); + postDraw(); +} + +void SkiaSalGraphicsImpl::privateDrawAlphaRect(long nX, long nY, long nWidth, long nHeight, + double fTransparency, bool blockAA) +{ + preDraw(); + SAL_INFO("vcl.skia.trace", + "privatedrawrect(" << this << "): " << SkIRect::MakeXYWH(nX, nY, nWidth, nHeight) + << ":" << mLineColor << ":" << mFillColor << ":" << fTransparency); + addXorRegion(SkRect::MakeXYWH(nX, nY, nWidth, nHeight)); + SkCanvas* canvas = getDrawCanvas(); + SkPaint paint; + paint.setAntiAlias(!blockAA && mParent.getAntiAliasB2DDraw()); + if (mFillColor != SALCOLOR_NONE) + { + paint.setColor(toSkColorWithTransparency(mFillColor, fTransparency)); + paint.setStyle(SkPaint::kFill_Style); + // 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) + { + paint.setColor(toSkColorWithTransparency(mLineColor, fTransparency)); + paint.setStyle(SkPaint::kStroke_Style); + // The obnoxious "-1 DrawRect()" hack that I don't understand the purpose of (and I'm not sure + // if anybody does), but without it some cases do not work. The max() is needed because Skia + // will not draw anything if width or height is 0. + canvas->drawIRect( + SkIRect::MakeXYWH(nX, nY, std::max(1L, nWidth - 1), std::max(1L, nHeight - 1)), paint); + } + postDraw(); +} + +void SkiaSalGraphicsImpl::drawRect(long nX, long nY, long nWidth, long nHeight) +{ + privateDrawAlphaRect(nX, nY, nWidth, nHeight, 0.0, true); +} + +void SkiaSalGraphicsImpl::drawPolyLine(sal_uInt32 nPoints, const SalPoint* pPtAry) +{ + basegfx::B2DPolygon aPolygon; + aPolygon.append(basegfx::B2DPoint(pPtAry->mnX, pPtAry->mnY), nPoints); + for (sal_uInt32 i = 1; i < nPoints; ++i) + aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].mnX, pPtAry[i].mnY)); + 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 SalPoint* pPtAry) +{ + basegfx::B2DPolygon aPolygon; + aPolygon.append(basegfx::B2DPoint(pPtAry->mnX, pPtAry->mnY), nPoints); + for (sal_uInt32 i = 1; i < nPoints; ++i) + aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].mnX, pPtAry[i].mnY)); + + drawPolyPolygon(basegfx::B2DHomMatrix(), basegfx::B2DPolyPolygon(aPolygon), 0.0); +} + +void SkiaSalGraphicsImpl::drawPolyPolygon(sal_uInt32 nPoly, const sal_uInt32* pPoints, + PCONSTSALPOINT* pPtAry) +{ + basegfx::B2DPolyPolygon aPolyPolygon; + for (sal_uInt32 nPolygon = 0; nPolygon < nPoly; ++nPolygon) + { + sal_uInt32 nPoints = pPoints[nPolygon]; + if (nPoints) + { + PCONSTSALPOINT pSalPoints = pPtAry[nPolygon]; + basegfx::B2DPolygon aPolygon; + aPolygon.append(basegfx::B2DPoint(pSalPoints->mnX, pSalPoints->mnY), nPoints); + for (sal_uInt32 i = 1; i < nPoints; ++i) + aPolygon.setB2DPoint(i, basegfx::B2DPoint(pSalPoints[i].mnX, pSalPoints[i].mnY)); + + 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.getAntiAliasB2DDraw()); + 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); + addXorRegion(polygonPath.getBounds()); + + SkPaint aPaint; + aPaint.setAntiAlias(useAA); + + // 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) + { + aPaint.setColor(toSkColorWithTransparency(mFillColor, fTransparency)); + aPaint.setStyle(SkPaint::kFill_Style); + // 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) + { + aPaint.setColor(toSkColorWithTransparency(mLineColor, fTransparency)); + aPaint.setStyle(SkPaint::kStroke_Style); + getDrawCanvas()->drawPath(polygonPath, aPaint); + } + postDraw(); +#if defined LINUX + // WORKAROUND: The logo in the about dialog has drawing errors. This seems to happen + // only on Linux (not Windows on the same machine), with both AMDGPU and Mesa, + // and only when antialiasing is enabled. Flushing seems to avoid the problem. + if (useAA && SkiaHelper::getVendor() == DriverBlocklist::VendorAMD) + mSurface->flushAndSubmit(); +#endif +} + +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.getAntiAliasB2DDraw()) + 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; +} + +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 + // TODO: tdf#136222 shows that basegfx::utils::mergeToSinglePolyPolygon() is unreliable + // in corner cases, possibly either a bug or rounding errors somewhere. + 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); + + // tdf#124848 get correct LineWidth in discrete coordinates, + if (fLineWidth == 0) // hairline + fLineWidth = 1.0; + else // Adjust line width for object-to-device scale. + fLineWidth = (rObjectToDevice * basegfx::B2DVector(fLineWidth, 0)).getLength(); + + // Transform to DeviceCoordinates, get DeviceLineWidth, execute PixelSnapHairline + basegfx::B2DPolyPolygon aPolyPolygonLine; + aPolyPolygonLine.append(rPolyLine); + aPolyPolygonLine.transform(rObjectToDevice); + if (bPixelSnapHairline) + { + aPolyPolygonLine = basegfx::utils::snapPointsOfHorizontalOrVerticalEdges(aPolyPolygonLine); + } + + // Setup Line Join + SkPaint::Join eSkLineJoin = SkPaint::kMiter_Join; + switch (eLineJoin) + { + case basegfx::B2DLineJoin::Bevel: + eSkLineJoin = SkPaint::kBevel_Join; + break; + case basegfx::B2DLineJoin::Round: + eSkLineJoin = SkPaint::kRound_Join; + break; + case basegfx::B2DLineJoin::NONE: + case basegfx::B2DLineJoin::Miter: + eSkLineJoin = SkPaint::kMiter_Join; + break; + } + + // convert miter minimum angle to miter limit + double fMiterLimit = 1.0 / std::sin(fMiterMinimumAngle / 2.0); + + // Setup Line Cap + SkPaint::Cap eSkLineCap(SkPaint::kButt_Cap); + + switch (eLineCap) + { + case css::drawing::LineCap_ROUND: + eSkLineCap = SkPaint::kRound_Cap; + break; + case css::drawing::LineCap_SQUARE: + eSkLineCap = SkPaint::kSquare_Cap; + break; + default: // css::drawing::LineCap_BUTT: + eSkLineCap = SkPaint::kButt_Cap; + break; + } + + SkPaint aPaint; + aPaint.setStyle(SkPaint::kStroke_Style); + aPaint.setStrokeCap(eSkLineCap); + aPaint.setStrokeJoin(eSkLineJoin); + aPaint.setColor(toSkColorWithTransparency(mLineColor, fTransparency)); + aPaint.setStrokeMiter(fMiterLimit); + aPaint.setStrokeWidth(fLineWidth); + aPaint.setAntiAlias(mParent.getAntiAliasB2DDraw()); + // See the tdf#134346 comment above. + const SkScalar posFix = mParent.getAntiAliasB2DDraw() ? 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.setFillType(SkPathFillType::kEvenOdd); + for (sal_uInt32 a(0); a < aPolyPolygonLine.count(); a++) + addPolygonToPath(aPolyPolygonLine.getB2DPolygon(a), aPath); + aPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr); + addXorRegion(aPath.getBounds()); + getDrawCanvas()->drawPath(aPath, aPaint); + } + else + { + for (sal_uInt32 i = 0; i < aPolyPolygonLine.count(); ++i) + { + const basegfx::B2DPolygon& rPolygon = aPolyPolygonLine.getB2DPolygon(i); + sal_uInt32 nPoints = rPolygon.count(); + bool bClosed = rPolygon.isClosed(); + for (sal_uInt32 j = 0; j < (bClosed ? nPoints : nPoints - 1); ++j) + { + sal_uInt32 index1 = (j + 0) % nPoints; + sal_uInt32 index2 = (j + 1) % nPoints; + SkPath aPath; + aPath.moveTo(rPolygon.getB2DPoint(index1).getX(), + rPolygon.getB2DPoint(index1).getY()); + aPath.lineTo(rPolygon.getB2DPoint(index2).getX(), + rPolygon.getB2DPoint(index2).getY()); + + aPath.offset(toSkX(0) + posFix, toSkY(0) + posFix, nullptr); + addXorRegion(aPath.getBounds()); + getDrawCanvas()->drawPath(aPath, aPaint); + } + } + } + + postDraw(); + + return true; +} + +bool SkiaSalGraphicsImpl::drawPolyLineBezier(sal_uInt32, const SalPoint*, const PolyFlags*) +{ + // TODO? + return false; +} + +bool SkiaSalGraphicsImpl::drawPolygonBezier(sal_uInt32, const SalPoint*, const PolyFlags*) +{ + // TODO? + return false; +} + +bool SkiaSalGraphicsImpl::drawPolyPolygonBezier(sal_uInt32, const sal_uInt32*, + const SalPoint* const*, const PolyFlags* const*) +{ + // TODO? + return false; +} + +static void copyArea(SkCanvas* canvas, sk_sp<SkSurface> surface, long nDestX, long nDestY, + long nSrcX, long nSrcY, long nSrcWidth, long nSrcHeight, bool srcIsRaster, + bool destIsRaster) +{ + // Using SkSurface::draw() should be more efficient than SkSurface::makeImageSnapshot(), + // because it may detect copying to itself and avoid some needless copies. + // But it has problems with drawing to itself + // (https://groups.google.com/forum/#!topic/skia-discuss/6yiuw24jv0I) and also + // raster surfaces do not avoid a copy of the source + // (https://groups.google.com/forum/#!topic/skia-discuss/S3FMpCi82k0). + // Finally, there's not much point if one of them is raster and the other is not (chrome/m86 even crashes). + if (canvas == surface->getCanvas() || srcIsRaster || (srcIsRaster != destIsRaster)) + { + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + canvas->drawImageRect(SkiaHelper::makeCheckedImageSnapshot(surface), + SkIRect::MakeXYWH(nSrcX, nSrcY, nSrcWidth, nSrcHeight), + SkRect::MakeXYWH(nDestX, nDestY, nSrcWidth, nSrcHeight), &paint); + return; + } + // SkCanvas::draw() cannot do a subrectangle, so clip. + canvas->save(); + canvas->clipRect(SkRect::MakeXYWH(nDestX, nDestY, nSrcWidth, nSrcHeight)); + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + surface->draw(canvas, nDestX - nSrcX, nDestY - nSrcY, &paint); + canvas->restore(); +} + +void SkiaSalGraphicsImpl::copyArea(long nDestX, long nDestY, long nSrcX, long nSrcY, long nSrcWidth, + 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)); + assert(!mXorMode); + ::copyArea(getDrawCanvas(), mSurface, nDestX, nDestY, nSrcX, nSrcY, nSrcWidth, nSrcHeight, + !isGPU(), !isGPU()); + 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); + } + if (rPosAry.mnSrcWidth == rPosAry.mnDestWidth && rPosAry.mnSrcHeight == rPosAry.mnDestHeight) + { + 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() << " copy area: " << rPosAry); + ::copyArea(getDrawCanvas(), src->mSurface, rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnSrcX, + rPosAry.mnSrcY, rPosAry.mnDestWidth, rPosAry.mnDestHeight, !src->isGPU(), + !isGPU()); + } + else + { + SAL_INFO("vcl.skia.trace", "copybits(" << this << "): (" << src << "): " << rPosAry); + // Do not use makeImageSnapshot(rect), as that one may make a needless data copy. + sk_sp<SkImage> image = SkiaHelper::makeCheckedImageSnapshot(src->mSurface); + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + if (rPosAry.mnSrcWidth != rPosAry.mnDestWidth + || rPosAry.mnSrcHeight != rPosAry.mnDestHeight) + paint.setFilterQuality(kHigh_SkFilterQuality); + getDrawCanvas()->drawImageRect(image, + SkIRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, + rPosAry.mnSrcWidth, rPosAry.mnSrcHeight), + SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, + rPosAry.mnDestWidth, rPosAry.mnDestHeight), + &paint); + } + assert(!mXorMode); + postDraw(); +} + +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(). + drawImage(rPosAry, rSkiaBitmap.GetSkImage(), 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); + + // 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(). + + // 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.GetAlphaSkImage()->makeShader(), + rSkiaAlphaBitmap.GetAlphaSkImage()->makeShader()); + // 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.GetSkImage()->makeShader()); + 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); + + drawImage(rPosAry, rSkiaSourceBitmap.GetSkImage()); +} + +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.GetAlphaSkImage()->makeShader())); +} + +std::shared_ptr<SalBitmap> SkiaSalGraphicsImpl::getBitmap(long nX, long nY, long nWidth, + 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 = SkiaHelper::makeCheckedImageSnapshot( + mSurface, SkIRect::MakeXYWH(nX, nY, nWidth, nHeight)); + return std::make_shared<SkiaSalBitmap>(image); +} + +Color SkiaSalGraphicsImpl::getPixel(long nX, 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(GetWidth(), GetHeight())) + abort(); + if (!mSurface->readPixels(bitmap, 0, 0)) + abort(); + return fromSkColor(bitmap.getColor(nX, nY)); +} + +void SkiaSalGraphicsImpl::invert(basegfx::B2DPolygon const& rPoly, SalInvert eFlags) +{ + preDraw(); + SAL_INFO("vcl.skia.trace", "invert(" << this << "): " << rPoly << ":" << int(eFlags)); + assert(!mXorMode); + // Intel Vulkan drivers (up to current 0.401.3889) have a problem + // with SkBlendMode::kDifference(?) and surfaces wider than 1024 pixels, resulting + // in drawing errors. Work that around by fetching the relevant part of the surface + // and drawing using CPU. + bool intelHack + = (isGPU() && SkiaHelper::getVendor() == DriverBlocklist::VendorIntel && !mXorMode); + // TrackFrame just inverts a dashed path around the polygon + if (eFlags == SalInvert::TrackFrame) + { + SkPath aPath; + addPolygonToPath(rPoly, aPath); + aPath.setFillType(SkPathFillType::kEvenOdd); + // 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. + SkAutoCanvasRestore autoRestore(getDrawCanvas(), true); + getDrawCanvas()->clipRect(aPath.getBounds(), SkClipOp::kIntersect, false); + SkPaint aPaint; + aPaint.setStrokeWidth(2); + float intervals[] = { 4.0f, 4.0f }; + aPaint.setStyle(SkPaint::kStroke_Style); + aPaint.setPathEffect(SkDashPathEffect::Make(intervals, SK_ARRAY_COUNT(intervals), 0)); + aPaint.setColor(SkColorSetARGB(255, 255, 255, 255)); + aPaint.setBlendMode(SkBlendMode::kDifference); + if (!intelHack) + getDrawCanvas()->drawPath(aPath, aPaint); + else + { + SkRect area; + aPath.getBounds().roundOut(&area); + SkRect size = SkRect::MakeWH(area.width(), area.height()); + sk_sp<SkSurface> surface = SkSurface::MakeRasterN32Premul(area.width(), area.height()); + SkPaint copy; + copy.setBlendMode(SkBlendMode::kSrc); + flushDrawing(); + surface->getCanvas()->drawImageRect(SkiaHelper::makeCheckedImageSnapshot(mSurface), + area, size, ©); + aPath.offset(-area.x(), -area.y()); + surface->getCanvas()->drawPath(aPath, aPaint); + getDrawCanvas()->drawImageRect(SkiaHelper::makeCheckedImageSnapshot(surface), size, + area, ©); + } + } + else + { + SkPath aPath; + addPolygonToPath(rPoly, aPath); + aPath.setFillType(SkPathFillType::kEvenOdd); + SkPaint aPaint; + aPaint.setColor(SkColorSetARGB(255, 255, 255, 255)); + aPaint.setStyle(SkPaint::kFill_Style); + aPaint.setBlendMode(SkBlendMode::kDifference); + + // N50 inverts in checker pattern + if (eFlags == SalInvert::N50) + { + // This creates 2x2 checker pattern bitmap + // TODO Use SkiaHelper::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)); + } + if (!intelHack) + getDrawCanvas()->drawPath(aPath, aPaint); + else + { + SkRect area; + aPath.getBounds().roundOut(&area); + SkRect size = SkRect::MakeWH(area.width(), area.height()); + sk_sp<SkSurface> surface = SkSurface::MakeRasterN32Premul(area.width(), area.height()); + SkPaint copy; + copy.setBlendMode(SkBlendMode::kSrc); + flushDrawing(); + surface->getCanvas()->drawImageRect(SkiaHelper::makeCheckedImageSnapshot(mSurface), + area, size, ©); + aPath.offset(-area.x(), -area.y()); + surface->getCanvas()->drawPath(aPath, aPaint); + getDrawCanvas()->drawImageRect(SkiaHelper::makeCheckedImageSnapshot(surface), size, + area, ©); + } + } + postDraw(); +} + +void SkiaSalGraphicsImpl::invert(long nX, long nY, long nWidth, 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 SalPoint* pPointArray, SalInvert eFlags) +{ + basegfx::B2DPolygon aPolygon; + aPolygon.append(basegfx::B2DPoint(pPointArray[0].mnX, pPointArray[0].mnY), nPoints); + for (sal_uInt32 i = 1; i < nPoints; ++i) + { + aPolygon.setB2DPoint(i, basegfx::B2DPoint(pPointArray[i].mnX, pPointArray[i].mnY)); + } + aPolygon.setClosed(true); + + invert(aPolygon, eFlags); +} + +bool SkiaSalGraphicsImpl::drawEPS(long, long, long, long, void*, sal_uInt32) +{ + // TODO? + 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. +sk_sp<SkImage> SkiaSalGraphicsImpl::mergeCacheBitmaps(const SkiaSalBitmap& bitmap, + const SkiaSalBitmap* alphaBitmap, + const Size targetSize) +{ + sk_sp<SkImage> image; + // GPU-accelerated drawing with SkShader should be fast enough to not need caching. + if (isGPU()) + return image; + if (targetSize.IsEmpty()) + return image; + // Probably not much point in caching of just doing a copy. + if (alphaBitmap == nullptr && targetSize == bitmap.GetSize()) + return image; + // Image too small to be worth caching. + if (bitmap.GetSize().Width() < 100 && bitmap.GetSize().Height() < 100 + && targetSize.Width() < 100 && targetSize.Height() < 100) + return image; + // In some cases (tdf#134237) the draw 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. + if (targetSize.Width() > GetWidth() || targetSize.Height() > GetHeight()) + { + // 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 = 1.0 * targetSize.Width() / bitmap.GetSize().Width() + * targetSize.Height() / bitmap.GetSize().Height(); + const double oversizeRatio + = 1.0 * targetSize.Width() / GetWidth() * targetSize.Height() / GetHeight(); + const double ratio = upscaleRatio * oversizeRatio; + if (ratio > 10) + { + SAL_INFO("vcl.skia.trace", "mergecachebitmaps(" + << this << "): not caching upscaling, ratio:" << ratio + << ", " << bitmap.GetSize() << "->" << targetSize + << " in " << Size(GetWidth(), GetHeight())); + return image; + } + } + OString key; + OStringBuffer keyBuf; + keyBuf.append(targetSize.Width()) + .append("x") + .append(targetSize.Height()) + .append("_0x") + .append(reinterpret_cast<sal_IntPtr>(&bitmap), 16) + .append("_0x") + .append(reinterpret_cast<sal_IntPtr>(alphaBitmap), 16) + .append("_") + .append(static_cast<sal_Int64>(bitmap.GetSkImage()->uniqueID())); + if (alphaBitmap) + keyBuf.append("_").append( + static_cast<sal_Int64>(alphaBitmap->GetAlphaSkImage()->uniqueID())); + key = keyBuf.makeStringAndClear(); + image = SkiaHelper::findCachedImage(key); + if (image) + { + assert(image->width() == targetSize.Width() && image->height() == targetSize.Height()); + return image; + } + // Combine bitmap + alpha bitmap into one temporary bitmap with alpha. + // If scaling is needed, first apply the alpha, then scale, otherwise the scaling might affect the alpha values. + if (alphaBitmap && targetSize != bitmap.GetSize()) + { + sk_sp<SkSurface> mergedSurface = SkiaHelper::createSkSurface(bitmap.GetSize()); + if (!mergedSurface) + return nullptr; + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + mergedSurface->getCanvas()->drawImage(bitmap.GetSkImage(), 0, 0, &paint); + paint.setBlendMode(SkBlendMode::kDstOut); // VCL alpha is one-minus-alpha + mergedSurface->getCanvas()->drawImage(alphaBitmap->GetAlphaSkImage(), 0, 0, &paint); + sk_sp<SkSurface> scaledSurface = SkiaHelper::createSkSurface(targetSize); + if (!scaledSurface) + return nullptr; + paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + paint.setFilterQuality(kHigh_SkFilterQuality); + scaledSurface->getCanvas()->drawImageRect( + mergedSurface->makeImageSnapshot(), + SkRect::MakeXYWH(0, 0, bitmap.GetSize().Width(), bitmap.GetSize().Height()), + SkRect::MakeXYWH(0, 0, targetSize.Width(), targetSize.Height()), &paint); + image = scaledSurface->makeImageSnapshot(); + } + else // No alpha or no scaling, scale directly. + { + sk_sp<SkSurface> tmpSurface = SkiaHelper::createSkSurface(targetSize); + if (!tmpSurface) + return nullptr; + SkCanvas* canvas = tmpSurface->getCanvas(); + SkAutoCanvasRestore autoRestore(canvas, true); + SkPaint paint; + if (targetSize != bitmap.GetSize()) + { + SkMatrix matrix; + matrix.set(SkMatrix::kMScaleX, 1.0 * targetSize.Width() / bitmap.GetSize().Width()); + matrix.set(SkMatrix::kMScaleY, 1.0 * targetSize.Height() / bitmap.GetSize().Height()); + canvas->concat(matrix); + paint.setFilterQuality(kHigh_SkFilterQuality); + } + paint.setBlendMode(SkBlendMode::kSrc); // copy as is, including alpha + canvas->drawImage(bitmap.GetSkImage(), 0, 0, &paint); + if (alphaBitmap != nullptr) + { + paint.setBlendMode(SkBlendMode::kDstOut); // VCL alpha is one-minus-alpha + canvas->drawImage(alphaBitmap->GetAlphaSkImage(), 0, 0, &paint); + } + image = SkiaHelper::makeCheckedImageSnapshot(tmpSurface); + } + SkiaHelper::addCachedImage(key, image); + return image; +} + +bool SkiaSalGraphicsImpl::drawAlphaBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSourceBitmap, + const SalBitmap& rAlphaBitmap) +{ + assert(dynamic_cast<const SkiaSalBitmap*>(&rSourceBitmap)); + assert(dynamic_cast<const SkiaSalBitmap*>(&rAlphaBitmap)); + // In raster mode use mergeCacheBitmaps(), which will cache the result, avoiding repeated + // alpha blending or scaling. In GPU mode it is simpler to just use SkShader. + 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(static_cast<const SkiaSalBitmap&>(rSourceBitmap), + static_cast<const SkiaSalBitmap*>(&rAlphaBitmap), imageSize); + if (image) + drawImage(imagePosAry, image); + else + drawShader( + rPosAry, + SkShaders::Blend( + SkBlendMode::kDstOut, // VCL alpha is one-minus-alpha. + static_cast<const SkiaSalBitmap&>(rSourceBitmap).GetSkImage()->makeShader(), + static_cast<const SkiaSalBitmap*>(&rAlphaBitmap)->GetAlphaSkImage()->makeShader())); + return true; +} + +void SkiaSalGraphicsImpl::drawImage(const SalTwoRect& rPosAry, const sk_sp<SkImage>& aImage, + SkBlendMode eBlendMode) +{ + SkRect aSourceRect + = SkRect::MakeXYWH(rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight); + SkRect aDestinationRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, + rPosAry.mnDestWidth, rPosAry.mnDestHeight); + + SkPaint aPaint; + aPaint.setBlendMode(eBlendMode); + if (rPosAry.mnSrcWidth != rPosAry.mnDestWidth || rPosAry.mnSrcHeight != rPosAry.mnDestHeight) + aPaint.setFilterQuality(kHigh_SkFilterQuality); + + preDraw(); + SAL_INFO("vcl.skia.trace", + "drawimage(" << this << "): " << rPosAry << ":" << SkBlendMode_Name(eBlendMode)); + addXorRegion(aDestinationRect); + getDrawCanvas()->drawImageRect(aImage, aSourceRect, aDestinationRect, &aPaint); + 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) +{ + preDraw(); + SAL_INFO("vcl.skia.trace", "drawshader(" << this << "): " << rPosAry); + SkRect destinationRect = SkRect::MakeXYWH(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, + rPosAry.mnDestHeight); + addXorRegion(destinationRect); + SkPaint paint; + paint.setShader(shader); + if (rPosAry.mnSrcWidth != rPosAry.mnDestWidth || rPosAry.mnSrcHeight != rPosAry.mnDestHeight) + paint.setFilterQuality(kHigh_SkFilterQuality); + 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); + assert(matrix.mapXY(rPosAry.mnSrcX, rPosAry.mnSrcY) + == SkPoint::Make(rPosAry.mnDestX, rPosAry.mnDestY)); + assert(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::drawTransformedBitmap(const basegfx::B2DPoint& rNull, + const basegfx::B2DPoint& rX, + const basegfx::B2DPoint& rY, + const SalBitmap& rSourceBitmap, + const SalBitmap* pAlphaBitmap) +{ + 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); + + // 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); + + addXorRegion(SkRect::MakeWH(GetWidth(), GetHeight())); // can't tell, use whole area + // In raster mode scaling and alpha blending is still somewhat expensive if done repeatedly, + // so use mergeCacheBitmaps(), which will cache the result if useful. + // It is better to use SkShader if in GPU mode, if the operation is simple or if the temporary + // image would be very large. + sk_sp<SkImage> imageToDraw = mergeCacheBitmaps( + rSkiaBitmap, pSkiaAlphaBitmap, Size(round(aXRel.getLength()), round(aYRel.getLength()))); + 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()) / imageToDraw->width()); + matrix.set(SkMatrix::kMScaleY, round(aYRel.getY()) / imageToDraw->height()); + matrix.set(SkMatrix::kMSkewY, aXRel.getY() / imageToDraw->width()); + matrix.set(SkMatrix::kMSkewX, aYRel.getX() / imageToDraw->height()); + matrix.set(SkMatrix::kMTransX, rNull.getX()); + matrix.set(SkMatrix::kMTransY, rNull.getY()); + SkCanvas* canvas = getDrawCanvas(); + SkAutoCanvasRestore autoRestore(canvas, true); + canvas->concat(matrix); + SkPaint paint; + paint.setFilterQuality(kHigh_SkFilterQuality); + canvas->drawImage(imageToDraw, 0, 0, &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); + SkPaint paint; + paint.setFilterQuality(kHigh_SkFilterQuality); + if (pSkiaAlphaBitmap) + { + paint.setShader(SkShaders::Blend(SkBlendMode::kDstOut, // VCL alpha is one-minus-alpha. + rSkiaBitmap.GetSkImage()->makeShader(), + pSkiaAlphaBitmap->GetAlphaSkImage()->makeShader())); + canvas->drawRect(SkRect::MakeWH(aSize.Width(), aSize.Height()), paint); + } + else + { + canvas->drawImage(rSkiaBitmap.GetSkImage(), 0, 0, &paint); + } + } + postDraw(); + return true; +} + +bool SkiaSalGraphicsImpl::drawAlphaRect(long nX, long nY, long nWidth, long nHeight, + sal_uInt8 nTransparency) +{ + privateDrawAlphaRect(nX, nY, nWidth, nHeight, nTransparency / 100.0); + return true; +} + +bool SkiaSalGraphicsImpl::drawGradient(const tools::PolyPolygon&, const Gradient&) +{ + // TODO? + return false; +} + +static double toRadian(int degree10th) { return (3600 - degree10th) * M_PI / 1800.0; } +static double toCos(int degree10th) { return SkScalarCos(toRadian(degree10th)); } +static double toSin(int degree10th) { return SkScalarSin(toRadian(degree10th)); } + +void SkiaSalGraphicsImpl::drawGenericLayout(const GenericSalLayout& layout, Color textColor, + const SkFont& font, GlyphOrientation glyphOrientation) +{ + SkiaZone zone; + std::vector<SkGlyphID> glyphIds; + std::vector<SkRSXform> glyphForms; + glyphIds.reserve(256); + glyphForms.reserve(256); + Point aPos; + const GlyphItem* pGlyph; + int nStart = 0; + while (layout.GetNextGlyph(&pGlyph, aPos, nStart)) + { + glyphIds.push_back(pGlyph->glyphId()); + int angle = 0; // 10th of degree + if (glyphOrientation == GlyphOrientation::Apply) + { + angle = layout.GetOrientation(); + if (pGlyph->IsVertical()) + angle += 900; // 90 degree + } + SkRSXform form = SkRSXform::Make(toCos(angle), toSin(angle), aPos.X(), aPos.Y()); + glyphForms.emplace_back(std::move(form)); + } + if (glyphIds.empty()) + return; + sk_sp<SkTextBlob> textBlob + = SkTextBlob::MakeFromRSXform(glyphIds.data(), glyphIds.size() * sizeof(SkGlyphID), + glyphForms.data(), font, SkTextEncoding::kGlyphID); + preDraw(); + SAL_INFO("vcl.skia.trace", + "drawtextblob(" << this << "): " << textBlob->bounds() << ":" << textColor); + addXorRegion(textBlob->bounds()); + SkPaint paint; + paint.setColor(toSkColor(textColor)); + getDrawCanvas()->drawTextBlob(textBlob, 0, 0, paint); + postDraw(); +} + +bool SkiaSalGraphicsImpl::supportsOperation(OutDevSupportType eType) const +{ + switch (eType) + { + case OutDevSupportType::B2DDraw: + case OutDevSupportType::TransparentRect: + return true; + default: + return false; + } +} + +#ifdef DBG_UTIL +void SkiaSalGraphicsImpl::dump(const char* file) const +{ + assert(mSurface.get()); + SkiaHelper::dump(mSurface, file); +} +#endif + +/* 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..0f62f7dc5 --- /dev/null +++ b/vcl/skia/salbmp.cxx @@ -0,0 +1,819 @@ +/* -*- 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 <SkCanvas.h> +#include <SkImage.h> +#include <SkPixelRef.h> +#include <SkSurface.h> +#include <SkSwizzle.h> +#include <SkColorFilter.h> +#include <SkColorMatrix.h> + +#include <skia/utils.hxx> +#include <skia/zone.hxx> + +#ifdef DBG_UTIL +#include <fstream> +#define CANARY "skia-canary" +#endif + +SkiaSalBitmap::SkiaSalBitmap() {} + +SkiaSalBitmap::~SkiaSalBitmap() {} + +static bool isValidBitCount(sal_uInt16 nBitCount) +{ + return (nBitCount == 1) || (nBitCount == 4) || (nBitCount == 8) || (nBitCount == 24) + || (nBitCount == 32); +} + +SkiaSalBitmap::SkiaSalBitmap(const sk_sp<SkImage>& image) +{ + ResetCachedData(); + mBuffer.reset(); + mImage = image; + mPalette = BitmapPalette(); + mBitCount = 32; + mSize = mPixelsSize = Size(image->width(), image->height()); +#ifdef DBG_UTIL + mWriteAccessCount = 0; +#endif + SAL_INFO("vcl.skia.trace", "bitmapfromimage(" << this << ")"); +} + +bool SkiaSalBitmap::Create(const Size& rSize, sal_uInt16 nBitCount, const BitmapPalette& rPal) +{ + ResetCachedData(); + mBuffer.reset(); + if (!isValidBitCount(nBitCount)) + return false; + mPalette = rPal; + mBitCount = nBitCount; + mSize = mPixelsSize = rSize; +#ifdef DBG_UTIL + mWriteAccessCount = 0; +#endif + if (!CreateBitmapData()) + { + mBitCount = 0; + mSize = mPixelsSize = Size(); + mPalette = BitmapPalette(); + return false; + } + SAL_INFO("vcl.skia.trace", "create(" << this << ")"); + return true; +} + +bool SkiaSalBitmap::CreateBitmapData() +{ + assert(!mBuffer); + // 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. + int bitScanlineWidth; + if (o3tl::checked_multiply<int>(mSize.Width(), mBitCount, bitScanlineWidth)) + { + SAL_WARN("vcl.skia", "checked multiply failed"); + return false; + } + mScanlineSize = AlignedWidth4Bytes(bitScanlineWidth); + if (mScanlineSize != 0 && mSize.Height() != 0) + { + size_t allocate = mScanlineSize * mSize.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 + } + return true; +} + +bool SkiaSalBitmap::Create(const SalBitmap& rSalBmp) +{ + return Create(rSalBmp, rSalBmp.GetBitCount()); +} + +bool SkiaSalBitmap::Create(const SalBitmap& rSalBmp, SalGraphics* pGraphics) +{ + return Create(rSalBmp, pGraphics ? pGraphics->GetBitCount() : rSalBmp.GetBitCount()); +} + +bool SkiaSalBitmap::Create(const SalBitmap& rSalBmp, sal_uInt16 nNewBitCount) +{ + 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; +#ifdef DBG_UTIL + mWriteAccessCount = 0; +#endif + if (nNewBitCount != 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) +{ + // TODO? + return false; +} + +void SkiaSalBitmap::Destroy() +{ + SAL_INFO("vcl.skia.trace", "destroy(" << this << ")"); +#ifdef DBG_UTIL + assert(mWriteAccessCount == 0); +#endif + ResetCachedData(); + mBuffer.reset(); +} + +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; + break; + case BitmapAccessMode::Read: + EnsureBitmapData(); + if (!mBuffer) + return nullptr; + 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; + buffer->mpBits = mBuffer.get(); + buffer->mnScanlineSize = mScanlineSize; + switch (mBitCount) + { + case 1: + buffer->mnFormat = ScanlineFormat::N1BitMsbPal; + break; + case 4: + buffer->mnFormat = ScanlineFormat::N4BitMsnPal; + 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. +// Use a macro to hide an unreachable code warning. +#define GET_FORMAT \ + (kN32_SkColorType == kBGRA_8888_SkColorType ? ScanlineFormat::N24BitTcBgr \ + : ScanlineFormat::N24BitTcRgb) + buffer->mnFormat = GET_FORMAT; +#undef GET_FORMAT + break; + } + case 32: +#define GET_FORMAT \ + (kN32_SkColorType == kBGRA_8888_SkColorType ? ScanlineFormat::N32BitTcBgra \ + : ScanlineFormat::N32BitTcRgba) + buffer->mnFormat = GET_FORMAT; +#undef GET_FORMAT + break; + default: + abort(); + } + buffer->mnFormat |= ScanlineFormat::TopDown; +#ifdef DBG_UTIL + if (nMode == BitmapAccessMode::Write) + ++mWriteAccessCount; +#endif + return buffer; +} + +void SkiaSalBitmap::ReleaseBuffer(BitmapBuffer* pBuffer, BitmapAccessMode nMode) +{ + if (nMode == BitmapAccessMode::Write) + { +#ifdef DBG_UTIL + assert(mWriteAccessCount > 0); + --mWriteAccessCount; +#endif + mPalette = pBuffer->maPalette; + ResetCachedData(); + } + // Are there any more ground movements underneath us ? + assert(pBuffer->mnWidth == mSize.Width()); + assert(pBuffer->mnHeight == mSize.Height()); + assert(pBuffer->mnBitCount == mBitCount); + verify(); + delete pBuffer; +} + +bool SkiaSalBitmap::GetSystemData(BitmapSystemData&) +{ +#ifdef DBG_UTIL + assert(mWriteAccessCount == 0); +#endif + // TODO? + 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)); + + // 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'. + SkFilterQuality currentQuality; + switch (nScaleFlag) + { + case BmpScaleFlag::Fast: + currentQuality = kNone_SkFilterQuality; + break; + case BmpScaleFlag::Default: + currentQuality = kMedium_SkFilterQuality; + break; + case BmpScaleFlag::BestQuality: + currentQuality = kHigh_SkFilterQuality; + break; + default: + SAL_INFO("vcl.skia.trace", "scale(" << this << "): unsupported scale algorithm"); + return false; + } + 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; + } + // if there is already one scale() pending, use the lowest quality of all requested + static_assert(kMedium_SkFilterQuality < kHigh_SkFilterQuality); + mScaleQuality = std::min(mScaleQuality, currentQuality); + // scaling will be actually done on-demand when needed, the need will be recognized + // by mSize != mPixelsSize + mSize = newSize; + // Do not reset cached data if mImage is possibly the only data we have. + if (mBuffer) + ResetCachedData(); + // 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 + // TODO? + 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) + { + if (mBitCount == 8 && mPalette.IsGreyPalette8Bit()) + return true; + sk_sp<SkSurface> surface = SkiaHelper::createSkSurface(mPixelsSize); + 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, &paint); + mBitCount = 8; + mPalette = Bitmap::GetGreyPalette(256); + ResetToSkImage(SkiaHelper::makeCheckedImageSnapshot(surface)); + 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; + // 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 (!mBuffer && mImage) + { + mBitCount = 8; + mPalette = Bitmap::GetGreyPalette(256); + ResetToSkImage(mImage); // keep mImage, it will be interpreted as 8bit if needed + SAL_INFO("vcl.skia.trace", "interpretas8bit(" << this << ")"); + return true; + } + return false; +} + +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 SKIA_USE_BITMAP32 + SkAlphaType alphaType = kPremul_SkAlphaType; +#else + SkAlphaType alphaType = kUnpremul_SkAlphaType; +#endif + if (!bitmap.installPixels( + SkImageInfo::MakeS32(mPixelsSize.Width(), mPixelsSize.Height(), alphaType), + data.release(), mPixelsSize.Width() * 4, + [](void* addr, void*) { delete[] static_cast<sal_uInt8*>(addr); }, nullptr)) + abort(); + bitmap.setImmutable(); + } + 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(); + for (long y = 0; y < mPixelsSize.Height(); ++y) + { + const sal_uInt8* src = mBuffer.get() + mScanlineSize * y; + // This 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). + SkExtendRGBToRGBA(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(); + bitmap.setImmutable(); + } + // Skia has a format for 8bit grayscale SkBitmap, but it seems to cause a problem + // with our PNG loader (tdf#121120), so convert it to RGBA below as well. + else + { +// Use a macro to hide an unreachable code warning. +#define GET_FORMAT \ + (kN32_SkColorType == kBGRA_8888_SkColorType ? BitConvert::BGRA : BitConvert::RGBA) + std::unique_ptr<sal_uInt8[]> data + = convertDataBitCount(mBuffer.get(), mPixelsSize.Width(), mPixelsSize.Height(), + mBitCount, mScanlineSize, mPalette, GET_FORMAT); +#undef GET_FORMAT + 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; +} + +const sk_sp<SkImage>& SkiaSalBitmap::GetSkImage() const +{ +#ifdef DBG_UTIL + assert(mWriteAccessCount == 0); +#endif + if (mPixelsSize != mSize && !mImage + && SkiaHelper::renderMethodToUse() != SkiaHelper::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 = SkiaHelper::createSkImage(GetAsSkBitmap()); + assert(image); + thisPtr->mSize = savedSize; + thisPtr->ResetToSkImage(image); + } + if (mImage) + { + if (mImage->width() != mSize.Width() || mImage->height() != mSize.Height()) + { + assert(!mBuffer); // This code should be only called if only mImage holds data. + SkiaZone zone; + sk_sp<SkSurface> surface = SkiaHelper::createSkSurface(mSize); + assert(surface); + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha + paint.setFilterQuality(mScaleQuality); + surface->getCanvas()->drawImageRect( + mImage, SkRect::MakeWH(mImage->width(), mImage->height()), + SkRect::MakeWH(mSize.Width(), mSize.Height()), &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 = SkiaHelper::makeCheckedImageSnapshot(surface); + } + return mImage; + } + SkiaZone zone; + sk_sp<SkImage> image = SkiaHelper::createSkImage(GetAsSkBitmap()); + assert(image); + const_cast<sk_sp<SkImage>&>(mImage) = image; + SAL_INFO("vcl.skia.trace", "getskimage(" << this << ")"); + return mImage; +} + +const sk_sp<SkImage>& SkiaSalBitmap::GetAlphaSkImage() const +{ +#ifdef DBG_UTIL + assert(mWriteAccessCount == 0); +#endif + if (mAlphaImage) + { + assert(mSize == mPixelsSize); // data has already been scaled if needed + return mAlphaImage; + } + if (mImage) + { + SkiaZone zone; + sk_sp<SkSurface> surface = SkiaHelper::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)); + bool scaling = mImage->width() != mSize.Width() || mImage->height() != mSize.Height(); + if (scaling) + { + assert(!mBuffer); // This code should be only called if only mImage holds data. + paint.setFilterQuality(mScaleQuality); + } + surface->getCanvas()->drawImageRect(mImage, + SkRect::MakeWH(mImage->width(), mImage->height()), + SkRect::MakeWH(mSize.Width(), mSize.Height()), &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"); + SkiaSalBitmap* thisPtr = const_cast<SkiaSalBitmap*>(this); + thisPtr->mAlphaImage = SkiaHelper::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 = SkiaHelper::createSkImage(alphaBitmap); + assert(image); + const_cast<sk_sp<SkImage>&>(mAlphaImage) = image; + } + else + { + sk_sp<SkSurface> surface = SkiaHelper::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()->drawBitmap(GetAsSkBitmap(), 0, 0, &paint); + SkiaSalBitmap* thisPtr = const_cast<SkiaSalBitmap*>(this); + thisPtr->mAlphaImage = SkiaHelper::makeCheckedImageSnapshot(surface); + } + SAL_INFO("vcl.skia.trace", "getalphaskimage(" << this << ")"); + return mAlphaImage; +} + +void SkiaSalBitmap::EnsureBitmapData() +{ + 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; + } + // Try to fill mBuffer from mImage. + if (!mImage) + return; + SkiaZone zone; + if (!CreateBitmapData()) + abort(); + SkAlphaType alphaType = kUnpremul_SkAlphaType; +#if SKIA_USE_BITMAP32 + if (mBitCount == 32) + alphaType = kPremul_SkAlphaType; +#endif + SkBitmap bitmap; + 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 (mSize != mPixelsSize) // pending scaling? + { + paint.setFilterQuality(mScaleQuality); + canvas.drawImageRect(mImage, + SkRect::MakeWH(mPixelsSize.getWidth(), mPixelsSize.getHeight()), + SkRect::MakeWH(mSize.getWidth(), mSize.getHeight()), &paint); + SAL_INFO("vcl.skia.trace", "ensurebitmapdata(" << this << "): image scaled " << mPixelsSize + << "->" << mSize << ":" + << static_cast<int>(mScaleQuality)); + mPixelsSize = mSize; + mScaleQuality = kHigh_SkFilterQuality; + // Information about the pending scaling has been discarded, so make sure we do not + // keep around any cached images that would still need scaling. + ResetCachedDataBySize(); + } + else + canvas.drawImage(mImage, 0, 0, &paint); + canvas.flush(); + bitmap.setImmutable(); + assert(mBuffer != nullptr); + if (mBitCount == 32) + { + for (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 + { + for (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; + for (long x = 0; x < mSize.Width(); ++x) + { + *dest++ = *src++; + *dest++ = *src++; + *dest++ = *src++; + ++src; // skip alpha + } + } + } + else if (mBitCount == 8 && mPalette.IsGreyPalette8Bit()) + { + for (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; + // no actual data conversion, use one color channel as the gray value + for (long x = 0; x < mSize.Width(); ++x) + dest[x] = src[x * 4]; + } + } + else + { + std::unique_ptr<vcl::ScanlineWriter> pWriter + = vcl::ScanlineWriter::Create(mBitCount, mPalette); + for (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 (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(); + SAL_INFO("vcl.skia.trace", "ensurebitmapdata(" << this << ")"); +} + +void SkiaSalBitmap::EnsureBitmapUniqueData() +{ + EnsureBitmapData(); + 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::ResetCachedData() +{ + SkiaZone zone; + // There may be a case when only mImage is set and CreatBitmapData() will create + // mBuffer from it if needed, in that case ResetToSkImage() should be used. + assert(mBuffer.get() || !mImage); + mImage.reset(); + mAlphaImage.reset(); +} + +void SkiaSalBitmap::ResetToSkImage(sk_sp<SkImage> image) +{ + SkiaZone zone; + mBuffer.reset(); + mImage = image; + mAlphaImage.reset(); +} + +void SkiaSalBitmap::ResetCachedDataBySize() +{ + SkiaZone zone; + assert(mSize == mPixelsSize); + if (mImage && (mImage->width() != mSize.getWidth() || mImage->height() != mSize.getHeight())) + mImage.reset(); + if (mAlphaImage + && (mAlphaImage->width() != mSize.getWidth() || mAlphaImage->height() != mSize.getHeight())) + mAlphaImage.reset(); +} + +#ifdef DBG_UTIL +void SkiaSalBitmap::dump(const char* file) const +{ + sk_sp<SkImage> saveImage = mImage; + sk_sp<SkImage> saveAlphaImage = mAlphaImage; + bool resetBuffer = !mBuffer; + int saveWriteAccessCount = mWriteAccessCount; + Size savePrescaleSize = mPixelsSize; + SkiaSalBitmap* thisPtr = const_cast<SkiaSalBitmap*>(this); + // avoid possible assert + thisPtr->mWriteAccessCount = 0; + SkiaHelper::dump(GetSkImage(), file); + // restore old state, so that debugging doesn't affect it + if (resetBuffer) + thisPtr->mBuffer.reset(); + thisPtr->mImage = saveImage; + thisPtr->mAlphaImage = saveAlphaImage; + thisPtr->mWriteAccessCount = saveWriteAccessCount; + thisPtr->mPixelsSize = savePrescaleSize; +} + +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_blacklist_vulkan.xml b/vcl/skia/skia_blacklist_vulkan.xml new file mode 100644 index 000000000..8a222d005 --- /dev/null +++ b/vcl/skia/skia_blacklist_vulkan.xml @@ -0,0 +1,37 @@ +<?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> + <whitelist> + </whitelist> + <blacklist> + <entry os="all" vendor="intel" compare="less_equal" version="0.16.2"> + <device id="all"/> + </entry> + <entry os="all" vendor="amd" compare="less_equal" version="2.0.49"> + <device id="all"/> + </entry> + <entry os="windows" vendor="nvidia"> + <device id="all"/> + </entry> + <entry os="7" vendor="all"> + <device id="all"/> + </entry> + </blacklist> +</root> diff --git a/vcl/skia/win/gdiimpl.cxx b/vcl/skia/win/gdiimpl.cxx new file mode 100644 index 000000000..a4cbe062f --- /dev/null +++ b/vcl/skia/win/gdiimpl.cxx @@ -0,0 +1,393 @@ +/* -*- 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/win/gdiimpl.hxx> + +#include <win/saldata.hxx> +#include <vcl/skia/SkiaHelper.hxx> +#include <skia/utils.hxx> +#include <skia/zone.hxx> +#include <win/winlayout.hxx> +#include <comphelper/windowserrorstring.hxx> + +#include <SkCanvas.h> +#include <SkPaint.h> +#include <SkPixelRef.h> +#include <SkTypeface_win.h> +#include <SkFont.h> +#include <SkFontMgr.h> +#include <SkFontLCDConfig.h> +#include <tools/sk_app/win/WindowContextFactory_win.h> +#include <tools/sk_app/WindowContext.h> + +#include <windows.h> + +WinSkiaSalGraphicsImpl::WinSkiaSalGraphicsImpl(WinSalGraphics& rGraphics, + SalGeometryProvider* mpProvider) + : SkiaSalGraphicsImpl(rGraphics, mpProvider) + , mWinParent(rGraphics) +{ +} + +void WinSkiaSalGraphicsImpl::createWindowContext(bool forceRaster) +{ + SkiaZone zone; + sk_app::DisplayParams displayParams; + switch (forceRaster ? SkiaHelper::RenderRaster : SkiaHelper::renderMethodToUse()) + { + case SkiaHelper::RenderRaster: + mWindowContext = sk_app::window_context_factory::MakeRasterForWin(mWinParent.gethWnd(), + displayParams); + break; + case SkiaHelper::RenderVulkan: + mWindowContext = sk_app::window_context_factory::MakeVulkanForWin(mWinParent.gethWnd(), + displayParams); + break; + } +} + +void WinSkiaSalGraphicsImpl::DeInit() +{ + SkiaZone zone; + SkiaSalGraphicsImpl::DeInit(); + mWindowContext.reset(); +} + +void WinSkiaSalGraphicsImpl::freeResources() {} + +void WinSkiaSalGraphicsImpl::performFlush() +{ + SkiaZone zone; + flushDrawing(); + if (mWindowContext) + mWindowContext->swapBuffers(); +} + +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())); + 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())); + 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; +} + +#ifdef SAL_LOG_INFO +static HRESULT checkResult(HRESULT hr, const char* file, size_t line) +{ + if (FAILED(hr)) + { + OUString sLocationString + = OUString::createFromAscii(file) + ":" + OUString::number(line) + " "; + SAL_DETAIL_LOG_STREAM(SAL_DETAIL_ENABLE_LOG_INFO, ::SAL_DETAIL_LOG_LEVEL_INFO, "vcl.skia", + sLocationString.toUtf8().getStr(), + "HRESULT failed with: 0x" << OUString::number(hr, 16) << ": " + << WindowsErrorStringFromHRESULT(hr)); + } + return hr; +} + +#define CHECKHR(funct) checkResult(funct, __FILE__, __LINE__) +#else +#define CHECKHR(funct) (funct) +#endif + +sk_sp<SkTypeface> WinSkiaSalGraphicsImpl::createDirectWriteTypeface(const LOGFONTW& logFont) +{ + if (!dwriteDone) + { + if (SUCCEEDED( + CHECKHR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), + reinterpret_cast<IUnknown**>(&dwriteFactory))))) + { + if (SUCCEEDED(CHECKHR(dwriteFactory->GetGdiInterop(&dwriteGdiInterop)))) + dwriteFontMgr = SkFontMgr_New_DirectWrite(dwriteFactory); + else + dwriteFactory->Release(); + } + dwriteDone = true; + } + IDWriteFont* font = nullptr; + IDWriteFontFace* fontFace; + IDWriteFontFamily* fontFamily; + if (FAILED(CHECKHR(dwriteGdiInterop->CreateFontFromLOGFONT(&logFont, &font)))) + return nullptr; + if (FAILED(CHECKHR(font->CreateFontFace(&fontFace)))) + return nullptr; + if (FAILED(CHECKHR(font->GetFontFamily(&fontFamily)))) + return nullptr; + return sk_sp<SkTypeface>( + SkCreateTypefaceDirectWrite(dwriteFontMgr, fontFace, font, fontFamily)); +} + +bool WinSkiaSalGraphicsImpl::DrawTextLayout(const GenericSalLayout& rLayout) +{ + const WinFontInstance& rWinFont = static_cast<const WinFontInstance&>(rLayout.GetFont()); + float fHScale = rWinFont.getHScale(); + + assert(dynamic_cast<const WinFontInstance*>(&rLayout.GetFont())); + const WinFontInstance* pWinFont = static_cast<const WinFontInstance*>(&rLayout.GetFont()); + const HFONT hLayoutFont = pWinFont->GetHFONT(); + LOGFONTW logFont; + if (GetObjectW(hLayoutFont, sizeof(logFont), &logFont) == 0) + { + assert(false); + return false; + } + sk_sp<SkTypeface> typeface = createDirectWriteTypeface(logFont); + GlyphOrientation glyphOrientation = GlyphOrientation::Apply; + if (!typeface) // fall back to GDI text rendering + { + typeface.reset(SkCreateTypefaceFromLOGFONT(logFont)); + glyphOrientation = GlyphOrientation::Ignore; + } + // lfHeight actually depends on DPI, so it's not really font height as such, + // but for LOGFONT-based typefaces Skia simply sets lfHeight back to this value + // directly. + double fontHeight = logFont.lfHeight; + if (fontHeight < 0) + fontHeight = -fontHeight; + SkFont font(typeface, fontHeight, fHScale, 0); + font.setEdging(getFontEdging()); + 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)); + // The font already is set up to have glyphs rotated as needed. + impl->drawGenericLayout(rLayout, salColor, font, glyphOrientation); + return true; +} + +SkFont::Edging WinSkiaSalGraphicsImpl::getFontEdging() +{ + if (fontEdgingDone) + return fontEdging; + // 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; + SkFontLCDConfig::LCDOrder lcdOrder = SkFontLCDConfig::kNONE_LCDOrder; + 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) + lcdOrder = SkFontLCDConfig::kBGR_LCDOrder; + else + lcdOrder = SkFontLCDConfig::kRGB_LCDOrder; // default + } + else + fontEdging = SkFont::Edging::kAntiAlias; + } + SkFontLCDConfig::SetSubpixelOrder(lcdOrder); + // Cache this, it is actually visible a little bit when profiling. + fontEdgingDone = true; + return fontEdging; +} + +void WinSkiaSalGraphicsImpl::ClearDevFontCache() +{ + dwriteFontMgr.reset(); + dwriteDone = false; + fontEdgingDone = false; +} + +SkiaCompatibleDC::SkiaCompatibleDC(SalGraphics& rGraphics, int x, int y, int width, int height) + : CompatibleDC(rGraphics, x, y, width, height, false) +{ +} + +std::unique_ptr<CompatibleDC::Texture> SkiaCompatibleDC::getAsMaskTexture() const +{ + auto ret = std::make_unique<SkiaCompatibleDC::Texture>(); + ret->image = getAsMaskImage(); + return ret; +} + +sk_sp<SkImage> SkiaCompatibleDC::getAsMaskImage() const +{ + SkiaZone zone; + // mpData is in the BGRA format, with A unused (and set to 0), and RGB are grey, + // so convert it to Skia format, then to 8bit and finally use as alpha mask + SkBitmap tmpBitmap; + if (!tmpBitmap.installPixels(SkImageInfo::Make(maRects.mnSrcWidth, maRects.mnSrcHeight, + kBGRA_8888_SkColorType, kOpaque_SkAlphaType), + mpData, maRects.mnSrcWidth * 4)) + abort(); + tmpBitmap.setImmutable(); + SkBitmap bitmap8; + if (!bitmap8.tryAllocPixels(SkImageInfo::Make(maRects.mnSrcWidth, maRects.mnSrcHeight, + kGray_8_SkColorType, kOpaque_SkAlphaType))) + abort(); + SkCanvas canvas8(bitmap8); + SkPaint paint8; + paint8.setBlendMode(SkBlendMode::kSrc); // copy and convert depth + // The data we got is upside-down. + SkMatrix matrix; + matrix.preTranslate(0, maRects.mnSrcHeight); + matrix.setConcat(matrix, SkMatrix::Scale(1, -1)); + canvas8.concat(matrix); + canvas8.drawBitmap(tmpBitmap, 0, 0, &paint8); + bitmap8.setImmutable(); + // use the 8bit data as an alpha channel + SkBitmap alpha; + alpha.setInfo(bitmap8.info().makeColorType(kAlpha_8_SkColorType), bitmap8.rowBytes()); + alpha.setPixelRef(sk_ref_sp(bitmap8.pixelRef()), bitmap8.pixelRefOrigin().x(), + bitmap8.pixelRefOrigin().y()); + alpha.setImmutable(); + return SkiaHelper::createSkImage(alpha); +} + +sk_sp<SkImage> SkiaCompatibleDC::getAsImage() const +{ + SkiaZone zone; + SkBitmap tmpBitmap; + if (!tmpBitmap.installPixels(SkImageInfo::Make(maRects.mnSrcWidth, maRects.mnSrcHeight, + kBGRA_8888_SkColorType, kUnpremul_SkAlphaType), + mpData, maRects.mnSrcWidth * 4)) + abort(); + tmpBitmap.setImmutable(); + sk_sp<SkSurface> surface = SkiaHelper::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, maRects.mnSrcHeight); + matrix.setConcat(matrix, SkMatrix::Scale(1, -1)); + canvas->concat(matrix); + canvas->drawBitmapRect(tmpBitmap, + SkRect::MakeXYWH(0, 0, maRects.mnSrcWidth, maRects.mnSrcHeight), + SkRect::MakeXYWH(0, 0, maRects.mnSrcWidth, maRects.mnSrcHeight), &paint); + canvas->restore(); + return SkiaHelper::makeCheckedImageSnapshot(surface); +} + +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)". + // TODO 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 = SkiaHelper::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->drawBitmap(tmpBitmap, 0, 0, &paint); + canvas->restore(); + return SkiaHelper::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() { SkiaHelper::prepareSkia(createVulkanWindowContext); } + +/* 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..1602218c1 --- /dev/null +++ b/vcl/skia/x11/gdiimpl.cxx @@ -0,0 +1,161 @@ +/* -*- 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> + +X11SkiaSalGraphicsImpl::X11SkiaSalGraphicsImpl(X11SalGraphics& rParent) + : SkiaSalGraphicsImpl(rParent, rParent.GetGeometryProvider()) + , mX11Parent(rParent) +{ +} + +X11SkiaSalGraphicsImpl::~X11SkiaSalGraphicsImpl() {} + +void X11SkiaSalGraphicsImpl::Init() +{ + // The m_pFrame and m_pVDev pointers are updated late in X11 + setProvider(mX11Parent.GetGeometryProvider()); + SkiaSalGraphicsImpl::Init(); +} + +void X11SkiaSalGraphicsImpl::createWindowContext(bool forceRaster) +{ + assert(mX11Parent.GetDrawable() != None); + mWindowContext = createWindowContext( + mX11Parent.GetXDisplay(), mX11Parent.GetDrawable(), &mX11Parent.GetVisual(), GetWidth(), + GetHeight(), forceRaster ? SkiaHelper::RenderRaster : SkiaHelper::renderMethodToUse(), + false); +} + +std::unique_ptr<sk_app::WindowContext> +X11SkiaSalGraphicsImpl::createWindowContext(Display* display, Drawable drawable, + const XVisualInfo* visual, int width, int height, + SkiaHelper::RenderMethod renderMethod, bool temporary) +{ + SkiaZone zone; + sk_app::DisplayParams displayParams; + displayParams.fColorType = kN32_SkColorType; + 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; +#ifdef DBG_UTIL + // Our patched Skia has VulkanWindowContext that shares GrContext, 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 + // checkDeviceBlacklisted() 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 SkiaHelper::RenderRaster: + // TODO The Skia Xlib code actually requires the non-native color type to work properly. + displayParams.fColorType + = (displayParams.fColorType == kBGRA_8888_SkColorType ? kRGBA_8888_SkColorType + : kBGRA_8888_SkColorType); + return sk_app::window_context_factory::MakeRasterForXlib(winInfo, displayParams); + case SkiaHelper::RenderVulkan: + return sk_app::window_context_factory::MakeVulkanForXlib(winInfo, displayParams); + } + abort(); +} + +bool X11SkiaSalGraphicsImpl::avoidRecreateByResize() const +{ + 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::DeInit() +{ + SkiaZone zone; + SkiaSalGraphicsImpl::DeInit(); + mWindowContext.reset(); +} + +void X11SkiaSalGraphicsImpl::freeResources() {} + +void X11SkiaSalGraphicsImpl::performFlush() +{ + SkiaZone zone; + flushDrawing(); + // TODO XPutImage() is somewhat inefficient, XShmPutImage() should be preferred. + mWindowContext->swapBuffers(); +} + +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, SkiaHelper::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..a4db9c75d --- /dev/null +++ b/vcl/skia/x11/salvd.cxx @@ -0,0 +1,86 @@ +/* -*- 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(); + m_pColormap = &pDisplay->GetColormap(m_nXScreen); + + m_pVDev = pDevice; + m_pFrame = nullptr; + + bWindow_ = pDisplay->IsDisplay(); + bVirDev_ = true; + + mxImpl->Init(); +} + +X11SkiaSalVirtualDevice::X11SkiaSalVirtualDevice(SalGraphics const* pGraphics, long nDX, 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 = pGraphics ? static_cast<X11SalGraphics const*>(pGraphics)->GetScreenNumber() + : vcl_sal::getSalDisplay(GetGenericUnixSalData())->GetDefaultXScreen(); + 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(long nDX, 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..13eff3012 --- /dev/null +++ b/vcl/skia/x11/textrender.cxx @@ -0,0 +1,80 @@ +/* -*- 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/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 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, nHeight); + // TODO are these correct? + if (rFont.NeedsArtificialItalic()) + font.setSkewX(-0x6000L / 0x10000L); + if (rFont.NeedsArtificialBold()) + font.setEmbolden(true); + font.setEdging(rFont.GetAntialiasAdvice() ? SkFont::Edging::kAntiAlias + : SkFont::Edging::kAlias); + + assert(dynamic_cast<SkiaSalGraphicsImpl*>(rGraphics.GetImpl())); + SkiaSalGraphicsImpl* impl = static_cast<SkiaSalGraphicsImpl*>(rGraphics.GetImpl()); + impl->drawGenericLayout(rLayout, mnTextColor, font, + SkiaSalGraphicsImpl::GlyphOrientation::Apply); +} + +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..50f5e1ea7 --- /dev/null +++ b/vcl/skia/zone.cxx @@ -0,0 +1,76 @@ +/* -*- 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> + +/** + * 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) + { + 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 (SkiaHelper::renderMethodToUse()) + { + case SkiaHelper::RenderVulkan: + { + static const CrashWatchdogTimingsValues vulkanValues = { 6, 20 }; /* 1.5s, 5s */ + return vulkanValues; + } + case SkiaHelper::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: */ |