1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
10 #include <vcl/skia/SkiaHelper.hxx>
12 #if !HAVE_FEATURE_SKIA
16 bool isVCLSkiaEnabled() { return false; }
22 #include <rtl/bootstrap.hxx>
23 #include <vcl/svapp.hxx>
24 #include <desktop/crashreport.hxx>
25 #include <officecfg/Office/Common.hxx>
26 #include <watchdog.hxx>
27 #include <skia/zone.hxx>
28 #include <sal/log.hxx>
29 #include <driverblocklist.hxx>
30 #include <skia/utils.hxx>
31 #include <config_folders.h>
32 #include <osl/file.hxx>
33 #include <tools/stream.hxx>
35 #include <string_view>
39 #include <SkSurface.h>
40 #include <SkGraphics.h>
41 #include <GrDirectContext.h>
42 #include <skia_compiler.hxx>
43 #include <skia_opts.hxx>
51 static OUString
getDenylistFile()
53 OUString
url("$BRAND_BASE_DIR/" LIBO_SHARE_FOLDER
);
54 rtl::Bootstrap::expandMacros(url
);
56 return url
+ "/skia/skia_denylist_vulkan.xml";
59 static uint32_t driverVersion
= 0;
60 uint32_t vendorId
= 0;
62 static OUString
versionAsString(uint32_t version
)
64 return OUString::number(version
>> 22) + "." + OUString::number((version
>> 12) & 0x3ff) + "."
65 + OUString::number(version
& 0xfff);
68 static std::string_view
vendorAsString(uint32_t vendor
)
70 return DriverBlocklist::GetVendorNameFromId(vendor
);
73 static OUString
getCacheFolder()
75 OUString
url("${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER
76 "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/cache/");
77 rtl::Bootstrap::expandMacros(url
);
78 osl::Directory::create(url
);
82 static void writeToLog(SvStream
& stream
, const char* key
, const char* value
)
84 stream
.WriteCharPtr(key
);
85 stream
.WriteCharPtr(": ");
86 stream
.WriteCharPtr(value
);
87 stream
.WriteChar('\n');
90 static void writeToLog(SvStream
& stream
, const char* key
, const OUString
& value
)
92 writeToLog(stream
, key
, OUStringToOString(value
, RTL_TEXTENCODING_UTF8
).getStr());
95 // Note that this function also logs system information about Vulkan.
96 static bool isVulkanDenylisted(const VkPhysicalDeviceProperties
& props
)
98 static const char* const types
[]
99 = { "other", "integrated", "discrete", "virtual", "cpu", "??" }; // VkPhysicalDeviceType
100 driverVersion
= props
.driverVersion
;
101 vendorId
= props
.vendorID
;
102 OUString vendorIdStr
= "0x" + OUString::number(props
.vendorID
, 16);
103 OUString deviceIdStr
= "0x" + OUString::number(props
.deviceID
, 16);
104 OUString driverVersionString
= versionAsString(driverVersion
);
105 OUString apiVersion
= versionAsString(props
.apiVersion
);
106 const char* deviceType
= types
[std::min
<unsigned>(props
.deviceType
, SAL_N_ELEMENTS(types
) - 1)];
108 CrashReporter::addKeyValue("VulkanVendor", vendorIdStr
, CrashReporter::AddItem
);
109 CrashReporter::addKeyValue("VulkanDevice", deviceIdStr
, CrashReporter::AddItem
);
110 CrashReporter::addKeyValue("VulkanAPI", apiVersion
, CrashReporter::AddItem
);
111 CrashReporter::addKeyValue("VulkanDriver", driverVersionString
, CrashReporter::AddItem
);
112 CrashReporter::addKeyValue("VulkanDeviceType", OUString::createFromAscii(deviceType
),
113 CrashReporter::AddItem
);
114 CrashReporter::addKeyValue("VulkanDeviceName", OUString::createFromAscii(props
.deviceName
),
115 CrashReporter::Write
);
117 SvFileStream
logFile(getCacheFolder() + "/skia.log", StreamMode::WRITE
| StreamMode::TRUNC
);
118 writeToLog(logFile
, "RenderMethod", "vulkan");
119 writeToLog(logFile
, "Vendor", vendorIdStr
);
120 writeToLog(logFile
, "Device", deviceIdStr
);
121 writeToLog(logFile
, "API", apiVersion
);
122 writeToLog(logFile
, "Driver", driverVersionString
);
123 writeToLog(logFile
, "DeviceType", deviceType
);
124 writeToLog(logFile
, "DeviceName", props
.deviceName
);
127 "Vulkan API version: " << apiVersion
<< ", driver version: " << driverVersionString
128 << ", vendor: " << vendorIdStr
<< " ("
129 << vendorAsString(vendorId
) << "), device: " << deviceIdStr
130 << ", type: " << deviceType
<< ", name: " << props
.deviceName
);
132 = DriverBlocklist::IsDeviceBlocked(getDenylistFile(), DriverBlocklist::VersionType::Vulkan
,
133 driverVersionString
, vendorIdStr
, deviceIdStr
);
134 writeToLog(logFile
, "Denylisted", denylisted
? "yes" : "no");
138 static void writeSkiaRasterInfo()
140 SvFileStream
logFile(getCacheFolder() + "/skia.log", StreamMode::WRITE
| StreamMode::TRUNC
);
141 writeToLog(logFile
, "RenderMethod", "raster");
142 // Log compiler, Skia works best when compiled with Clang.
143 writeToLog(logFile
, "Compiler", skia_compiler_name());
146 static sk_app::VulkanWindowContext::SharedGrDirectContext
getTemporaryGrDirectContext();
148 static void checkDeviceDenylisted(bool blockDisable
= false)
150 static bool done
= false;
156 switch (renderMethodToUse())
160 // First try if a GrDirectContext already exists.
161 sk_app::VulkanWindowContext::SharedGrDirectContext grDirectContext
162 = sk_app::VulkanWindowContext::getSharedGrDirectContext();
163 if (!grDirectContext
.getGrDirectContext())
165 // This function is called from isVclSkiaEnabled(), which
166 // may be called when deciding which X11 visual to use,
167 // and that visual is normally needed when creating
168 // Skia's VulkanWindowContext, which is needed for the GrDirectContext.
169 // Avoid the loop by creating a temporary GrDirectContext
170 // that will use the default X11 visual (that shouldn't matter
171 // for just finding out information about Vulkan) and destroying
172 // the temporary context will clean up again.
173 grDirectContext
= getTemporaryGrDirectContext();
175 bool denylisted
= true; // assume the worst
176 if (grDirectContext
.getGrDirectContext()) // Vulkan was initialized properly
179 = isVulkanDenylisted(sk_app::VulkanWindowContext::getPhysDeviceProperties());
180 SAL_INFO("vcl.skia", "Vulkan denylisted: " << denylisted
);
183 SAL_INFO("vcl.skia", "Vulkan could not be initialized");
184 if (denylisted
&& !blockDisable
)
186 disableRenderMethod(RenderVulkan
);
187 writeSkiaRasterInfo();
192 SAL_INFO("vcl.skia", "Using Skia raster mode");
193 writeSkiaRasterInfo();
194 return; // software, never denylisted
199 static bool skiaSupportedByBackend
= false;
200 static bool supportsVCLSkia()
202 if (!skiaSupportedByBackend
)
204 SAL_INFO("vcl.skia", "Skia not supported by VCL backend, disabling");
207 return getenv("SAL_DISABLESKIA") == nullptr;
210 bool isVCLSkiaEnabled()
213 * The !bSet part should only be called once! Changing the results in the same
214 * run will mix Skia and normal rendering.
217 static bool bSet
= false;
218 static bool bEnable
= false;
219 static bool bForceSkia
= false;
221 // No hardware rendering, so no Skia
223 if (Application::IsBitmapRendering())
228 return bForceSkia
|| bEnable
;
232 * There are a number of cases that these environment variables cover:
233 * * SAL_FORCESKIA forces Skia if disabled by UI options or denylisted
234 * * SAL_DISABLESKIA avoids the use of Skia regardless of any option
238 bForceSkia
= !!getenv("SAL_FORCESKIA") || officecfg::Office::Common::VCL::ForceSkia::get();
241 bool bSupportsVCLSkia
= supportsVCLSkia();
242 if (bForceSkia
&& bSupportsVCLSkia
)
247 // don't actually block if denylisted, but log it if enabled, and also get the vendor id
248 checkDeviceDenylisted(true);
250 else if (getenv("SAL_FORCEGL"))
252 // Skia usage is checked before GL usage, so if GL is forced (and Skia is not), do not
253 // enable Skia in order to allow GL.
256 else if (bSupportsVCLSkia
)
258 static bool bEnableSkiaEnv
= !!getenv("SAL_ENABLESKIA");
260 bEnable
= bEnableSkiaEnv
;
262 if (officecfg::Office::Common::VCL::UseSkia::get())
265 // Force disable in safe mode
266 if (Application::IsSafeModeEnabled())
273 checkDeviceDenylisted(); // switch to raster if driver is denylisted
280 WatchdogThread::start();
282 CrashReporter::addKeyValue("UseSkia", OUString::boolean(bRet
), CrashReporter::Write
);
287 static RenderMethod methodToUse
= RenderRaster
;
289 static bool initRenderMethodToUse()
291 if (const char* env
= getenv("SAL_SKIA"))
293 if (strcmp(env
, "raster") == 0)
295 methodToUse
= RenderRaster
;
298 if (strcmp(env
, "vulkan") == 0)
300 methodToUse
= RenderVulkan
;
303 SAL_WARN("vcl.skia", "Unrecognized value of SAL_SKIA");
306 if (officecfg::Office::Common::VCL::ForceSkiaRaster::get())
308 methodToUse
= RenderRaster
;
311 methodToUse
= RenderVulkan
;
315 RenderMethod
renderMethodToUse()
317 static bool methodToUseInited
= initRenderMethodToUse();
318 if (methodToUseInited
) // Used just to ensure thread-safe one-time init.
323 void disableRenderMethod(RenderMethod method
)
325 if (renderMethodToUse() != method
)
327 // Choose a fallback, right now always raster.
328 methodToUse
= RenderRaster
;
331 static sk_app::VulkanWindowContext::SharedGrDirectContext
* sharedGrDirectContext
;
333 static std::unique_ptr
<sk_app::WindowContext
> (*createVulkanWindowContextFunction
)(bool) = nullptr;
334 static void setCreateVulkanWindowContext(std::unique_ptr
<sk_app::WindowContext
> (*function
)(bool))
336 createVulkanWindowContextFunction
= function
;
339 GrDirectContext
* getSharedGrDirectContext()
342 assert(renderMethodToUse() == RenderVulkan
);
343 if (sharedGrDirectContext
)
344 return sharedGrDirectContext
->getGrDirectContext();
346 // Set up the shared GrDirectContext from Skia's (patched) VulkanWindowContext, if it's been
348 sk_app::VulkanWindowContext::SharedGrDirectContext context
349 = sk_app::VulkanWindowContext::getSharedGrDirectContext();
350 GrDirectContext
* grDirectContext
= context
.getGrDirectContext();
353 sharedGrDirectContext
= new sk_app::VulkanWindowContext::SharedGrDirectContext(context
);
354 return grDirectContext
;
356 static bool done
= false;
360 if (createVulkanWindowContextFunction
== nullptr)
361 return nullptr; // not initialized properly (e.g. used from a VCL backend with no Skia support)
362 std::unique_ptr
<sk_app::WindowContext
> tmpContext
= createVulkanWindowContextFunction(false);
363 // Set up using the shared context created by the call above, if successful.
364 context
= sk_app::VulkanWindowContext::getSharedGrDirectContext();
365 grDirectContext
= context
.getGrDirectContext();
368 sharedGrDirectContext
= new sk_app::VulkanWindowContext::SharedGrDirectContext(context
);
369 return grDirectContext
;
371 disableRenderMethod(RenderVulkan
);
375 static sk_app::VulkanWindowContext::SharedGrDirectContext
getTemporaryGrDirectContext()
377 if (createVulkanWindowContextFunction
== nullptr)
378 return sk_app::VulkanWindowContext::SharedGrDirectContext();
379 std::unique_ptr
<sk_app::WindowContext
> tmpContext
= createVulkanWindowContextFunction(true);
380 // Set up using the shared context created by the call above, if successful.
381 return sk_app::VulkanWindowContext::getSharedGrDirectContext();
384 sk_sp
<SkSurface
> createSkSurface(int width
, int height
, SkColorType type
, SkAlphaType alpha
)
387 assert(type
== kN32_SkColorType
|| type
== kAlpha_8_SkColorType
);
388 sk_sp
<SkSurface
> surface
;
389 switch (SkiaHelper::renderMethodToUse())
391 case SkiaHelper::RenderVulkan
:
393 if (GrDirectContext
* grDirectContext
= getSharedGrDirectContext())
395 surface
= SkSurface::MakeRenderTarget(grDirectContext
, SkBudgeted::kNo
,
396 SkImageInfo::Make(width
, height
, type
, alpha
),
401 prefillSurface(surface
);
406 "cannot create Vulkan GPU offscreen surface, falling back to Raster");
413 // Create raster surface as a fallback.
414 surface
= SkSurface::MakeRaster(SkImageInfo::Make(width
, height
, type
, alpha
), surfaceProps());
419 prefillSurface(surface
);
423 // In non-debug builds we could return SkSurface::MakeNull() and try to cope with the situation,
424 // but that can lead to unnoticed data loss, so better fail clearly.
428 sk_sp
<SkImage
> createSkImage(const SkBitmap
& bitmap
)
431 assert(bitmap
.colorType() == kN32_SkColorType
|| bitmap
.colorType() == kAlpha_8_SkColorType
);
432 switch (SkiaHelper::renderMethodToUse())
434 case SkiaHelper::RenderVulkan
:
436 if (GrDirectContext
* grDirectContext
= getSharedGrDirectContext())
438 sk_sp
<SkSurface
> surface
= SkSurface::MakeRenderTarget(
439 grDirectContext
, SkBudgeted::kNo
,
440 bitmap
.info().makeAlphaType(kPremul_SkAlphaType
), 0, surfaceProps());
444 paint
.setBlendMode(SkBlendMode::kSrc
); // set as is, including alpha
445 surface
->getCanvas()->drawBitmap(bitmap
, 0, 0, &paint
);
446 return makeCheckedImageSnapshot(surface
);
448 // Try to fall back in non-debug builds.
450 "cannot create Vulkan GPU offscreen surface, falling back to Raster");
457 // Create raster image as a fallback.
458 sk_sp
<SkImage
> image
= SkImage::MakeFromBitmap(bitmap
);
463 sk_sp
<SkImage
> makeCheckedImageSnapshot(sk_sp
<SkSurface
> surface
)
465 sk_sp
<SkImage
> ret
= surface
->makeImageSnapshot();
472 sk_sp
<SkImage
> makeCheckedImageSnapshot(sk_sp
<SkSurface
> surface
, const SkIRect
& bounds
)
474 sk_sp
<SkImage
> ret
= surface
->makeImageSnapshot(bounds
);
483 // Image cache, for saving results of complex operations such as drawTransformedBitmap().
484 struct ImageCacheItem
487 sk_sp
<SkImage
> image
;
488 tools::Long size
; // cost of the item
492 // LRU cache, last item is the least recently used. Hopefully there won't be that many items
493 // to require a hash/map. Using o3tl::lru_cache would be simpler, but it doesn't support
494 // calculating cost of each item.
495 static std::list
<ImageCacheItem
>* imageCache
= nullptr;
496 static tools::Long imageCacheSize
= 0; // sum of all ImageCacheItem.size
498 void addCachedImage(const OString
& key
, sk_sp
<SkImage
> image
)
500 static bool disabled
= getenv("SAL_DISABLE_SKIA_CACHE") != nullptr;
503 if (imageCache
== nullptr)
504 imageCache
= new std::list
<ImageCacheItem
>;
505 tools::Long size
= static_cast<tools::Long
>(image
->width()) * image
->height()
506 * SkColorTypeBytesPerPixel(image
->imageInfo().colorType());
507 imageCache
->push_front({ key
, image
, size
});
508 imageCacheSize
+= size
;
509 SAL_INFO("vcl.skia.trace", "addcachedimage " << image
<< " :" << size
<< "/" << imageCacheSize
);
510 const tools::Long maxSize
= maxImageCacheSize();
511 while (imageCacheSize
> maxSize
)
513 assert(!imageCache
->empty());
514 imageCacheSize
-= imageCache
->back().size
;
515 SAL_INFO("vcl.skia.trace", "least used removal " << imageCache
->back().image
<< ":"
516 << imageCache
->back().size
);
517 imageCache
->pop_back();
521 sk_sp
<SkImage
> findCachedImage(const OString
& key
)
523 if (imageCache
!= nullptr)
525 for (auto it
= imageCache
->begin(); it
!= imageCache
->end(); ++it
)
529 sk_sp
<SkImage
> ret
= it
->image
;
530 SAL_INFO("vcl.skia.trace",
531 "findcachedimage " << key
<< " : " << it
->image
<< " found");
532 imageCache
->splice(imageCache
->begin(), *imageCache
, it
);
537 SAL_INFO("vcl.skia.trace", "findcachedimage " << key
<< " not found");
541 void removeCachedImage(sk_sp
<SkImage
> image
)
543 if (imageCache
== nullptr)
545 for (auto it
= imageCache
->begin(); it
!= imageCache
->end();)
547 if (it
->image
== image
)
549 imageCacheSize
-= it
->size
;
550 assert(imageCacheSize
>= 0);
551 it
= imageCache
->erase(it
);
558 tools::Long
maxImageCacheSize()
560 // Defaults to 4x 2000px 32bpp images, 64MiB.
561 return officecfg::Office::Common::Cache::Skia::ImageCacheSize::get();
566 delete sharedGrDirectContext
;
567 sharedGrDirectContext
= nullptr;
569 imageCache
= nullptr;
573 static SkSurfaceProps commonSurfaceProps
;
574 const SkSurfaceProps
* surfaceProps() { return &commonSurfaceProps
; }
576 void setPixelGeometry(SkPixelGeometry pixelGeometry
)
578 commonSurfaceProps
= SkSurfaceProps(commonSurfaceProps
.flags(), pixelGeometry
);
581 // Skia should not be used from VCL backends that do not actually support it, as there will be setup missing.
582 // The code here (that is in the vcl lib) needs a function for creating Vulkan context that is
583 // usually available only in the backend libs.
584 void prepareSkia(std::unique_ptr
<sk_app::WindowContext
> (*createVulkanWindowContext
)(bool))
586 setCreateVulkanWindowContext(createVulkanWindowContext
);
587 skiaSupportedByBackend
= true;
591 void prefillSurface(const sk_sp
<SkSurface
>& surface
)
593 // Pre-fill the surface with deterministic garbage.
595 bitmap
.allocN32Pixels(2, 2);
597 scanline
= bitmap
.getAddr32(0, 0);
598 *scanline
++ = SkPreMultiplyARGB(0xFF, 0xBF, 0x80, 0x40);
599 *scanline
++ = SkPreMultiplyARGB(0xFF, 0x40, 0x80, 0xBF);
600 scanline
= bitmap
.getAddr32(0, 1);
601 *scanline
++ = SkPreMultiplyARGB(0xFF, 0xE3, 0x5C, 0x13);
602 *scanline
++ = SkPreMultiplyARGB(0xFF, 0x13, 0x5C, 0xE3);
603 bitmap
.setImmutable();
605 paint
.setBlendMode(SkBlendMode::kSrc
); // set as is, including alpha
606 paint
.setShader(bitmap
.makeShader(SkTileMode::kRepeat
, SkTileMode::kRepeat
));
607 surface
->getCanvas()->drawPaint(paint
);
610 void dump(const SkBitmap
& bitmap
, const char* file
) { dump(SkImage::MakeFromBitmap(bitmap
), file
); }
612 void dump(const sk_sp
<SkSurface
>& surface
, const char* file
)
614 surface
->getCanvas()->flush();
615 dump(makeCheckedImageSnapshot(surface
), file
);
618 void dump(const sk_sp
<SkImage
>& image
, const char* file
)
620 sk_sp
<SkData
> data
= image
->encodeToData();
621 std::ofstream
ostream(file
, std::ios::binary
);
622 ostream
.write(static_cast<const char*>(data
->data()), data
->size());
629 #endif // HAVE_FEATURE_SKIA
631 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */