diff options
Diffstat (limited to '')
-rw-r--r-- | vcl/skia/SkiaHelper.cxx | 611 |
1 files changed, 611 insertions, 0 deletions
diff --git a/vcl/skia/SkiaHelper.cxx b/vcl/skia/SkiaHelper.cxx new file mode 100644 index 000000000..343ebc284 --- /dev/null +++ b/vcl/skia/SkiaHelper.cxx @@ -0,0 +1,611 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <vcl/skia/SkiaHelper.hxx> + +#if !HAVE_FEATURE_SKIA + +namespace SkiaHelper +{ +bool isVCLSkiaEnabled() { return false; } + +} // namespace + +#else + +#include <rtl/bootstrap.hxx> +#include <vcl/svapp.hxx> +#include <desktop/crashreport.hxx> +#include <officecfg/Office/Common.hxx> +#include <watchdog.hxx> +#include <skia/zone.hxx> +#include <sal/log.hxx> +#include <driverblocklist.hxx> +#include <skia/utils.hxx> +#include <config_folders.h> +#include <osl/file.hxx> +#include <tools/stream.hxx> +#include <list> + +#include <SkCanvas.h> +#include <SkPaint.h> +#include <SkSurface.h> +#include <SkGraphics.h> +#include <skia_compiler.hxx> + +#ifdef DBG_UTIL +#include <fstream> +#endif + +namespace SkiaHelper +{ +static OUString getBlacklistFile() +{ + OUString url("$BRAND_BASE_DIR/" LIBO_SHARE_FOLDER); + rtl::Bootstrap::expandMacros(url); + + return url + "/skia/skia_blacklist_vulkan.xml"; +} + +static uint32_t driverVersion = 0; +uint32_t vendorId = 0; + +static OUString versionAsString(uint32_t version) +{ + return OUString::number(version >> 22) + "." + OUString::number((version >> 12) & 0x3ff) + "." + + OUString::number(version & 0xfff); +} + +static OUStringLiteral vendorAsString(uint32_t vendor) +{ + return DriverBlocklist::GetVendorNameFromId(vendor); +} + +static OUString getCacheFolder() +{ + OUString url("${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER + "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/cache/"); + rtl::Bootstrap::expandMacros(url); + osl::Directory::create(url); + return url; +} + +static void writeToLog(SvStream& stream, const char* key, const char* value) +{ + stream.WriteCharPtr(key); + stream.WriteCharPtr(": "); + stream.WriteCharPtr(value); + stream.WriteChar('\n'); +} + +static void writeToLog(SvStream& stream, const char* key, const OUString& value) +{ + writeToLog(stream, key, OUStringToOString(value, RTL_TEXTENCODING_UTF8).getStr()); +} + +// Note that this function also logs system information about Vulkan. +static bool isVulkanBlacklisted(const VkPhysicalDeviceProperties& props) +{ + static const char* const types[] + = { "other", "integrated", "discrete", "virtual", "cpu", "??" }; // VkPhysicalDeviceType + driverVersion = props.driverVersion; + vendorId = props.vendorID; + OUString vendorIdStr = "0x" + OUString::number(props.vendorID, 16); + OUString deviceIdStr = "0x" + OUString::number(props.deviceID, 16); + OUString driverVersionString = versionAsString(driverVersion); + OUString apiVersion = versionAsString(props.apiVersion); + const char* deviceType = types[std::min<unsigned>(props.deviceType, SAL_N_ELEMENTS(types) - 1)]; + + CrashReporter::addKeyValue("VulkanVendor", vendorIdStr, CrashReporter::AddItem); + CrashReporter::addKeyValue("VulkanDevice", deviceIdStr, CrashReporter::AddItem); + CrashReporter::addKeyValue("VulkanAPI", apiVersion, CrashReporter::AddItem); + CrashReporter::addKeyValue("VulkanDriver", driverVersionString, CrashReporter::AddItem); + CrashReporter::addKeyValue("VulkanDeviceType", OUString::createFromAscii(deviceType), + CrashReporter::AddItem); + CrashReporter::addKeyValue("VulkanDeviceName", OUString::createFromAscii(props.deviceName), + CrashReporter::Write); + + SvFileStream logFile(getCacheFolder() + "/skia.log", StreamMode::WRITE | StreamMode::TRUNC); + writeToLog(logFile, "RenderMethod", "vulkan"); + writeToLog(logFile, "Vendor", vendorIdStr); + writeToLog(logFile, "Device", deviceIdStr); + writeToLog(logFile, "API", apiVersion); + writeToLog(logFile, "Driver", driverVersionString); + writeToLog(logFile, "DeviceType", deviceType); + writeToLog(logFile, "DeviceName", props.deviceName); + + SAL_INFO("vcl.skia", + "Vulkan API version: " << apiVersion << ", driver version: " << driverVersionString + << ", vendor: " << vendorIdStr << " (" + << vendorAsString(vendorId) << "), device: " << deviceIdStr + << ", type: " << deviceType << ", name: " << props.deviceName); + bool blacklisted + = DriverBlocklist::IsDeviceBlocked(getBlacklistFile(), DriverBlocklist::VersionType::Vulkan, + driverVersionString, vendorIdStr, deviceIdStr); + writeToLog(logFile, "Blacklisted", blacklisted ? "yes" : "no"); + return blacklisted; +} + +static void writeSkiaRasterInfo() +{ + SvFileStream logFile(getCacheFolder() + "/skia.log", StreamMode::WRITE | StreamMode::TRUNC); + writeToLog(logFile, "RenderMethod", "raster"); + // Log compiler, Skia works best when compiled with Clang. + writeToLog(logFile, "Compiler", skia_compiler_name()); +} + +static sk_app::VulkanWindowContext::SharedGrContext getTemporaryGrContext(); + +static void checkDeviceBlacklisted(bool blockDisable = false) +{ + static bool done = false; + if (!done) + { + SkiaZone zone; + + switch (renderMethodToUse()) + { + case RenderVulkan: + { + // First try if a GrContext already exists. + sk_app::VulkanWindowContext::SharedGrContext grContext + = sk_app::VulkanWindowContext::getSharedGrContext(); + if (!grContext.getGrContext()) + { + // This function is called from isVclSkiaEnabled(), which + // may be called when deciding which X11 visual to use, + // and that visual is normally needed when creating + // Skia's VulkanWindowContext, which is needed for the GrContext. + // Avoid the loop by creating a temporary GrContext + // that will use the default X11 visual (that shouldn't matter + // for just finding out information about Vulkan) and destroying + // the temporary context will clean up again. + grContext = getTemporaryGrContext(); + } + bool blacklisted = true; // assume the worst + if (grContext.getGrContext()) // Vulkan was initialized properly + { + blacklisted = isVulkanBlacklisted( + sk_app::VulkanWindowContext::getPhysDeviceProperties()); + SAL_INFO("vcl.skia", "Vulkan blacklisted: " << blacklisted); + } + else + SAL_INFO("vcl.skia", "Vulkan could not be initialized"); + if (blacklisted && !blockDisable) + { + disableRenderMethod(RenderVulkan); + writeSkiaRasterInfo(); + } + break; + } + case RenderRaster: + SAL_INFO("vcl.skia", "Using Skia raster mode"); + writeSkiaRasterInfo(); + return; // software, never blacklisted + } + done = true; + } +} + +static bool skiaSupportedByBackend = false; +static bool supportsVCLSkia() +{ + if (!skiaSupportedByBackend) + { + SAL_INFO("vcl.skia", "Skia not supported by VCL backend, disabling"); + return false; + } + return getenv("SAL_DISABLESKIA") == nullptr; +} + +bool isVCLSkiaEnabled() +{ + /** + * The !bSet part should only be called once! Changing the results in the same + * run will mix Skia and normal rendering. + */ + + static bool bSet = false; + static bool bEnable = false; + static bool bForceSkia = false; + + // No hardware rendering, so no Skia + // TODO SKIA + if (Application::IsBitmapRendering()) + return false; + + if (bSet) + { + return bForceSkia || bEnable; + } + + /* + * There are a number of cases that these environment variables cover: + * * SAL_FORCESKIA forces Skia if disabled by UI options or blacklisted + * * SAL_DISABLESKIA avoids the use of Skia regardless of any option + */ + + bSet = true; + bForceSkia = !!getenv("SAL_FORCESKIA") || officecfg::Office::Common::VCL::ForceSkia::get(); + + bool bRet = false; + bool bSupportsVCLSkia = supportsVCLSkia(); + if (bForceSkia && bSupportsVCLSkia) + { + bRet = true; + SkGraphics::Init(); + // don't actually block if blacklisted, but log it if enabled, and also get the vendor id + checkDeviceBlacklisted(true); + } + else if (getenv("SAL_FORCEGL")) + { + // Skia usage is checked before GL usage, so if GL is forced (and Skia is not), do not + // enable Skia in order to allow GL. + bRet = false; + } + else if (bSupportsVCLSkia) + { + static bool bEnableSkiaEnv = !!getenv("SAL_ENABLESKIA"); + + bEnable = bEnableSkiaEnv; + + if (officecfg::Office::Common::VCL::UseSkia::get()) + bEnable = true; + + // Force disable in safe mode + if (Application::IsSafeModeEnabled()) + bEnable = false; + + if (bEnable) + { + SkGraphics::Init(); + checkDeviceBlacklisted(); // switch to raster if driver is blacklisted + } + + bRet = bEnable; + } + + if (bRet) + WatchdogThread::start(); + + CrashReporter::addKeyValue("UseSkia", OUString::boolean(bRet), CrashReporter::Write); + + return bRet; +} + +static RenderMethod methodToUse = RenderRaster; + +static bool initRenderMethodToUse() +{ + if (const char* env = getenv("SAL_SKIA")) + { + if (strcmp(env, "raster") == 0) + { + methodToUse = RenderRaster; + return true; + } + if (strcmp(env, "vulkan") == 0) + { + methodToUse = RenderVulkan; + return true; + } + SAL_WARN("vcl.skia", "Unrecognized value of SAL_SKIA"); + abort(); + } + if (officecfg::Office::Common::VCL::ForceSkiaRaster::get()) + { + methodToUse = RenderRaster; + return true; + } + methodToUse = RenderVulkan; + return true; +} + +RenderMethod renderMethodToUse() +{ + static bool methodToUseInited = initRenderMethodToUse(); + if (methodToUseInited) // Used just to ensure thread-safe one-time init. + return methodToUse; + abort(); +} + +void disableRenderMethod(RenderMethod method) +{ + if (renderMethodToUse() != method) + return; + // Choose a fallback, right now always raster. + methodToUse = RenderRaster; +} + +static sk_app::VulkanWindowContext::SharedGrContext* sharedGrContext; + +static std::unique_ptr<sk_app::WindowContext> (*createVulkanWindowContextFunction)(bool) = nullptr; +static void setCreateVulkanWindowContext(std::unique_ptr<sk_app::WindowContext> (*function)(bool)) +{ + createVulkanWindowContextFunction = function; +} + +GrContext* getSharedGrContext() +{ + SkiaZone zone; + assert(renderMethodToUse() == RenderVulkan); + if (sharedGrContext) + return sharedGrContext->getGrContext(); + // TODO mutex? + // Set up the shared GrContext from Skia's (patched) VulkanWindowContext, if it's been + // already set up. + sk_app::VulkanWindowContext::SharedGrContext context + = sk_app::VulkanWindowContext::getSharedGrContext(); + GrContext* grContext = context.getGrContext(); + if (grContext) + { + sharedGrContext = new sk_app::VulkanWindowContext::SharedGrContext(context); + return grContext; + } + static bool done = false; + if (done) + return nullptr; + done = true; + if (createVulkanWindowContextFunction == nullptr) + return nullptr; // not initialized properly (e.g. used from a VCL backend with no Skia support) + std::unique_ptr<sk_app::WindowContext> tmpContext = createVulkanWindowContextFunction(false); + // Set up using the shared context created by the call above, if successful. + context = sk_app::VulkanWindowContext::getSharedGrContext(); + grContext = context.getGrContext(); + if (grContext) + { + sharedGrContext = new sk_app::VulkanWindowContext::SharedGrContext(context); + return grContext; + } + disableRenderMethod(RenderVulkan); + return nullptr; +} + +static sk_app::VulkanWindowContext::SharedGrContext getTemporaryGrContext() +{ + if (createVulkanWindowContextFunction == nullptr) + return sk_app::VulkanWindowContext::SharedGrContext(); + std::unique_ptr<sk_app::WindowContext> tmpContext = createVulkanWindowContextFunction(true); + // Set up using the shared context created by the call above, if successful. + return sk_app::VulkanWindowContext::getSharedGrContext(); +} + +sk_sp<SkSurface> createSkSurface(int width, int height, SkColorType type) +{ + SkiaZone zone; + assert(type == kN32_SkColorType || type == kAlpha_8_SkColorType); + sk_sp<SkSurface> surface; + switch (SkiaHelper::renderMethodToUse()) + { + case SkiaHelper::RenderVulkan: + { + if (GrContext* grContext = getSharedGrContext()) + { + surface = SkSurface::MakeRenderTarget( + grContext, SkBudgeted::kNo, + SkImageInfo::Make(width, height, type, kPremul_SkAlphaType)); + if (surface) + { +#ifdef DBG_UTIL + prefillSurface(surface); +#endif + return surface; + } + SAL_WARN("vcl.skia", + "cannot create Vulkan GPU offscreen surface, falling back to Raster"); + } + break; + } + default: + break; + } + // Create raster surface as a fallback. + surface = SkSurface::MakeRaster(SkImageInfo::Make(width, height, type, kPremul_SkAlphaType)); + assert(surface); + if (surface) + { +#ifdef DBG_UTIL + prefillSurface(surface); +#endif + return surface; + } + // In non-debug builds we could return SkSurface::MakeNull() and try to cope with the situation, + // but that can lead to unnoticed data loss, so better fail clearly. + abort(); +} + +sk_sp<SkImage> createSkImage(const SkBitmap& bitmap) +{ + SkiaZone zone; + assert(bitmap.colorType() == kN32_SkColorType || bitmap.colorType() == kAlpha_8_SkColorType); + switch (SkiaHelper::renderMethodToUse()) + { + case SkiaHelper::RenderVulkan: + { + if (GrContext* grContext = getSharedGrContext()) + { + sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget( + grContext, SkBudgeted::kNo, bitmap.info().makeAlphaType(kPremul_SkAlphaType)); + if (surface) + { + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha + surface->getCanvas()->drawBitmap(bitmap, 0, 0, &paint); + return makeCheckedImageSnapshot(surface); + } + // Try to fall back in non-debug builds. + SAL_WARN("vcl.skia", + "cannot create Vulkan GPU offscreen surface, falling back to Raster"); + } + break; + } + default: + break; + } + // Create raster image as a fallback. + sk_sp<SkImage> image = SkImage::MakeFromBitmap(bitmap); + assert(image); + return image; +} + +sk_sp<SkImage> makeCheckedImageSnapshot(sk_sp<SkSurface> surface) +{ + sk_sp<SkImage> ret = surface->makeImageSnapshot(); + assert(ret); + if (ret) + return ret; + abort(); +} + +sk_sp<SkImage> makeCheckedImageSnapshot(sk_sp<SkSurface> surface, const SkIRect& bounds) +{ + sk_sp<SkImage> ret = surface->makeImageSnapshot(bounds); + assert(ret); + if (ret) + return ret; + abort(); +} + +namespace +{ +// Image cache, for saving results of complex operations such as drawTransformedBitmap(). +struct ImageCacheItem +{ + OString key; + sk_sp<SkImage> image; + int size; // cost of the item +}; +} //namespace + +// LRU cache, last item is the least recently used. Hopefully there won't be that many items +// to require a hash/map. Using o3tl::lru_cache would be simpler, but it doesn't support +// calculating cost of each item. +static std::list<ImageCacheItem>* imageCache = nullptr; +static int imageCacheSize = 0; // sum of all ImageCacheItem.size + +void addCachedImage(const OString& key, sk_sp<SkImage> image) +{ + static bool disabled = getenv("SAL_DISABLE_SKIA_CACHE") != nullptr; + if (disabled) + return; + if (imageCache == nullptr) + imageCache = new std::list<ImageCacheItem>; + int size = image->width() * image->height() + * SkColorTypeBytesPerPixel(image->imageInfo().colorType()); + imageCache->push_front({ key, image, size }); + imageCacheSize += size; + SAL_INFO("vcl.skia.trace", "addcachedimage " << image << " :" << size << "/" << imageCacheSize); + const int MAX_CACHE_SIZE = 4 * 1000 * 1000 * 4; // 4x 1000px 32bpp images, 16MiB + while (imageCacheSize > MAX_CACHE_SIZE) + { + assert(!imageCache->empty()); + imageCacheSize -= imageCache->back().size; + SAL_INFO("vcl.skia.trace", + "least used removal " << image << ":" << imageCache->back().size); + imageCache->pop_back(); + } +} + +sk_sp<SkImage> findCachedImage(const OString& key) +{ + if (imageCache != nullptr) + { + for (auto it = imageCache->begin(); it != imageCache->end(); ++it) + { + if (it->key == key) + { + sk_sp<SkImage> ret = it->image; + SAL_INFO("vcl.skia.trace", + "findcachedimage " << key << " : " << it->image << " found"); + imageCache->splice(imageCache->begin(), *imageCache, it); + return ret; + } + } + } + SAL_INFO("vcl.skia.trace", "findcachedimage " << key << " not found"); + return nullptr; +} + +void removeCachedImage(sk_sp<SkImage> image) +{ + if (imageCache == nullptr) + return; + for (auto it = imageCache->begin(); it != imageCache->end();) + { + if (it->image == image) + { + imageCacheSize -= it->size; + assert(imageCacheSize >= 0); + it = imageCache->erase(it); + } + else + ++it; + } +} + +void cleanup() +{ + delete sharedGrContext; + sharedGrContext = nullptr; + delete imageCache; + imageCache = nullptr; + imageCacheSize = 0; +} + +// Skia should not be used from VCL backends that do not actually support it, as there will be setup missing. +// The code here (that is in the vcl lib) needs a function for creating Vulkan context that is +// usually available only in the backend libs. +void prepareSkia(std::unique_ptr<sk_app::WindowContext> (*createVulkanWindowContext)(bool)) +{ + setCreateVulkanWindowContext(createVulkanWindowContext); + skiaSupportedByBackend = true; +} + +#ifdef DBG_UTIL +void prefillSurface(sk_sp<SkSurface>& surface) +{ + // Pre-fill the surface with deterministic garbage. + SkBitmap bitmap; + bitmap.allocN32Pixels(2, 2); + SkPMColor* scanline; + scanline = bitmap.getAddr32(0, 0); + *scanline++ = SkPreMultiplyARGB(0xFF, 0xBF, 0x80, 0x40); + *scanline++ = SkPreMultiplyARGB(0xFF, 0x40, 0x80, 0xBF); + scanline = bitmap.getAddr32(0, 1); + *scanline++ = SkPreMultiplyARGB(0xFF, 0xE3, 0x5C, 0x13); + *scanline++ = SkPreMultiplyARGB(0xFF, 0x13, 0x5C, 0xE3); + bitmap.setImmutable(); + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha + paint.setShader(bitmap.makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat)); + surface->getCanvas()->drawPaint(paint); +} + +void dump(const SkBitmap& bitmap, const char* file) { dump(SkImage::MakeFromBitmap(bitmap), file); } + +void dump(const sk_sp<SkSurface>& surface, const char* file) +{ + surface->getCanvas()->flush(); + dump(makeCheckedImageSnapshot(surface), file); +} + +void dump(const sk_sp<SkImage>& image, const char* file) +{ + sk_sp<SkData> data = image->encodeToData(); + std::ofstream ostream(file, std::ios::binary); + ostream.write(static_cast<const char*>(data->data()), data->size()); +} + +#endif + +} // namespace + +#endif // HAVE_FEATURE_SKIA + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |