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