/src/libreoffice/vcl/skia/SkiaHelper.cxx
Line | Count | Source |
1 | | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | /* |
3 | | * This file is part of the LibreOffice project. |
4 | | * |
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/. |
8 | | */ |
9 | | |
10 | | #include <sal/config.h> |
11 | | |
12 | | #include <config_features.h> |
13 | | |
14 | | #include <string_view> |
15 | | |
16 | | #include <vcl/skia/SkiaHelper.hxx> |
17 | | #include <rtl/ustrbuf.hxx> |
18 | | |
19 | | #if !HAVE_FEATURE_SKIA |
20 | | |
21 | | namespace SkiaHelper |
22 | | { |
23 | 0 | bool isVCLSkiaEnabled() { return false; } |
24 | 0 | bool isAlphaMaskBlendingEnabled() { return false; } |
25 | | |
26 | | } // namespace |
27 | | |
28 | | #else |
29 | | |
30 | | #include <rtl/bootstrap.hxx> |
31 | | #include <vcl/svapp.hxx> |
32 | | #include <desktop/crashreport.hxx> |
33 | | #include <officecfg/Office/Common.hxx> |
34 | | #include <watchdog.hxx> |
35 | | #include <skia/zone.hxx> |
36 | | #include <sal/log.hxx> |
37 | | #include <driverblocklist.hxx> |
38 | | #include <skia/utils.hxx> |
39 | | #include <config_folders.h> |
40 | | #include <config_skia.h> |
41 | | #include <osl/file.hxx> |
42 | | #include <tools/stream.hxx> |
43 | | #include <atomic> |
44 | | #include <list> |
45 | | #include <o3tl/lru_map.hxx> |
46 | | |
47 | | #include <com/sun/star/configuration/theDefaultProvider.hpp> |
48 | | #include <com/sun/star/util/XFlushable.hpp> |
49 | | |
50 | | #include <SkBitmap.h> |
51 | | #include <SkCanvas.h> |
52 | | #include <include/codec/SkEncodedImageFormat.h> |
53 | | #include <SkPaint.h> |
54 | | #include <SkSurface.h> |
55 | | #include <SkGraphics.h> |
56 | | #include <ganesh/GrDirectContext.h> |
57 | | #include <SkRuntimeEffect.h> |
58 | | #include <SkStream.h> |
59 | | #include <SkTileMode.h> |
60 | | #include <SkTypeface.h> |
61 | | #include <SkFontArguments.h> |
62 | | #include <skia_compiler.hxx> |
63 | | #include <skia_opts.hxx> |
64 | | #if defined(MACOSX) |
65 | | #include <premac.h> |
66 | | #endif |
67 | | #ifdef SK_VULKAN |
68 | | #include <tools/window/VulkanWindowContext.h> |
69 | | #endif |
70 | | #ifdef SK_METAL |
71 | | #include <tools/window/MetalWindowContext.h> |
72 | | #endif |
73 | | #if defined(MACOSX) |
74 | | #include <postmac.h> |
75 | | #endif |
76 | | #include <src/core/SkOpts.h> |
77 | | #include <src/core/SkChecksum.h> |
78 | | #include <include/encode/SkPngEncoder.h> |
79 | | #include <ganesh/SkSurfaceGanesh.h> |
80 | | #if defined _MSC_VER |
81 | | #pragma warning(push) |
82 | | #pragma warning(disable : 4100) // "unreferenced formal parameter" |
83 | | #pragma warning(disable : 4324) // "structure was padded due to alignment specifier" |
84 | | #endif |
85 | | #if defined __GNUC__ || defined __clang__ |
86 | | #pragma GCC diagnostic push |
87 | | #pragma GCC diagnostic ignored "-Wunused-parameter" |
88 | | #endif |
89 | | #include <src/image/SkImage_Base.h> |
90 | | #if defined __GNUC__ || defined __clang__ |
91 | | #pragma GCC diagnostic pop |
92 | | #endif |
93 | | #if defined _MSC_VER |
94 | | #pragma warning(pop) |
95 | | #endif |
96 | | |
97 | | #include <fstream> |
98 | | |
99 | | #ifdef SK_METAL |
100 | | #ifdef MACOSX |
101 | | #include <quartz/cgutils.h> |
102 | | #endif |
103 | | #endif |
104 | | |
105 | | namespace SkiaHelper |
106 | | { |
107 | | static OUString getCacheFolder() |
108 | | { |
109 | | OUString url(u"${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER |
110 | | "/" SAL_CONFIGFILE("bootstrap") ":UserInstallation}/cache/"_ustr); |
111 | | rtl::Bootstrap::expandMacros(url); |
112 | | osl::Directory::create(url); |
113 | | return url; |
114 | | } |
115 | | |
116 | | static void writeToLog(SvStream& stream, const char* key, const char* value) |
117 | | { |
118 | | stream.WriteOString(key); |
119 | | stream.WriteOString(": "); |
120 | | stream.WriteOString(value); |
121 | | stream.WriteChar('\n'); |
122 | | } |
123 | | |
124 | | OUString readLog() |
125 | | { |
126 | | SvFileStream logFile(getCacheFolder() + "/skia.log", StreamMode::READ); |
127 | | |
128 | | OUStringBuffer sResult; |
129 | | OStringBuffer sLine; |
130 | | while (logFile.ReadLine(sLine)) |
131 | | sResult.append(OStringToOUString(sLine, RTL_TEXTENCODING_UTF8) + "\n"); |
132 | | |
133 | | return sResult.toString(); |
134 | | } |
135 | | |
136 | | uint32_t vendorId = 0; |
137 | | |
138 | | #ifdef SK_VULKAN |
139 | | static void writeToLog(SvStream& stream, const char* key, std::u16string_view value) |
140 | | { |
141 | | writeToLog(stream, key, OUStringToOString(value, RTL_TEXTENCODING_UTF8).getStr()); |
142 | | } |
143 | | |
144 | | static OUString getDenylistFile() |
145 | | { |
146 | | OUString url(u"$BRAND_BASE_DIR/" LIBO_SHARE_FOLDER ""_ustr); |
147 | | rtl::Bootstrap::expandMacros(url); |
148 | | |
149 | | return url + "/skia/skia_denylist_vulkan.xml"; |
150 | | } |
151 | | |
152 | | static OUString versionAsString(uint32_t version) |
153 | | { |
154 | | return OUString::number(version >> 22) + "." + OUString::number((version >> 12) & 0x3ff) + "." |
155 | | + OUString::number(version & 0xfff); |
156 | | } |
157 | | |
158 | | static std::string_view vendorAsString(uint32_t vendor) |
159 | | { |
160 | | return DriverBlocklist::GetVendorNameFromId(vendor); |
161 | | } |
162 | | |
163 | | // returns old value |
164 | | static bool setForceSkiaGPU(bool val) |
165 | | { |
166 | | const bool oldValue = officecfg::Office::Common::VCL::ForceSkiaGPU::get(); |
167 | | if (oldValue != val && !officecfg::Office::Common::VCL::ForceSkiaGPU::isReadOnly()) |
168 | | { |
169 | | auto batch(comphelper::ConfigurationChanges::create()); |
170 | | officecfg::Office::Common::VCL::ForceSkiaGPU::set(val, batch); |
171 | | batch->commit(); |
172 | | |
173 | | // make sure the change is written to the configuration |
174 | | if (auto xFlushable{ css::configuration::theDefaultProvider::get( |
175 | | comphelper::getProcessComponentContext()) |
176 | | .query<css::util::XFlushable>() }) |
177 | | xFlushable->flush(); |
178 | | } |
179 | | return oldValue; |
180 | | } |
181 | | |
182 | | // Note that this function also logs system information about Vulkan. |
183 | | static bool isVulkanDenylisted(const VkPhysicalDeviceProperties& props) |
184 | | { |
185 | | static const char* const types[] |
186 | | = { "other", "integrated", "discrete", "virtual", "cpu", "??" }; // VkPhysicalDeviceType |
187 | | vendorId = props.vendorID; |
188 | | OUString vendorIdStr = "0x" + OUString::number(props.vendorID, 16); |
189 | | OUString deviceIdStr = "0x" + OUString::number(props.deviceID, 16); |
190 | | OUString driverVersionString = versionAsString(props.driverVersion); |
191 | | OUString apiVersion = versionAsString(props.apiVersion); |
192 | | const char* deviceType = types[std::min<unsigned>(props.deviceType, SAL_N_ELEMENTS(types) - 1)]; |
193 | | |
194 | | CrashReporter::addKeyValue(u"VulkanVendor"_ustr, vendorIdStr, CrashReporter::AddItem); |
195 | | CrashReporter::addKeyValue(u"VulkanDevice"_ustr, deviceIdStr, CrashReporter::AddItem); |
196 | | CrashReporter::addKeyValue(u"VulkanAPI"_ustr, apiVersion, CrashReporter::AddItem); |
197 | | CrashReporter::addKeyValue(u"VulkanDriver"_ustr, driverVersionString, CrashReporter::AddItem); |
198 | | CrashReporter::addKeyValue(u"VulkanDeviceType"_ustr, OUString::createFromAscii(deviceType), |
199 | | CrashReporter::AddItem); |
200 | | CrashReporter::addKeyValue(u"VulkanDeviceName"_ustr, |
201 | | OUString::createFromAscii(props.deviceName), CrashReporter::Write); |
202 | | |
203 | | SvFileStream logFile(getCacheFolder() + "/skia.log", StreamMode::WRITE | StreamMode::TRUNC); |
204 | | writeToLog(logFile, "RenderMethod", "vulkan"); |
205 | | writeToLog(logFile, "Vendor", vendorIdStr); |
206 | | writeToLog(logFile, "Device", deviceIdStr); |
207 | | writeToLog(logFile, "API", apiVersion); |
208 | | writeToLog(logFile, "Driver", driverVersionString); |
209 | | writeToLog(logFile, "DeviceType", deviceType); |
210 | | writeToLog(logFile, "DeviceName", props.deviceName); |
211 | | |
212 | | SAL_INFO("vcl.skia", |
213 | | "Vulkan API version: " << apiVersion << ", driver version: " << driverVersionString |
214 | | << ", vendor: " << vendorIdStr << " (" |
215 | | << vendorAsString(vendorId) << "), device: " << deviceIdStr |
216 | | << ", type: " << deviceType << ", name: " << props.deviceName); |
217 | | bool denylisted |
218 | | = DriverBlocklist::IsDeviceBlocked(getDenylistFile(), DriverBlocklist::VersionType::Vulkan, |
219 | | driverVersionString, vendorIdStr, deviceIdStr); |
220 | | writeToLog(logFile, "Denylisted", denylisted ? "yes" : "no"); |
221 | | return denylisted; |
222 | | } |
223 | | #endif |
224 | | |
225 | | #ifdef SK_METAL |
226 | | static void writeSkiaMetalInfo() |
227 | | { |
228 | | SvFileStream logFile(getCacheFolder() + "/skia.log", StreamMode::WRITE | StreamMode::TRUNC); |
229 | | writeToLog(logFile, "RenderMethod", "metal"); |
230 | | } |
231 | | #endif |
232 | | |
233 | | static void writeSkiaRasterInfo() |
234 | | { |
235 | | SvFileStream logFile(getCacheFolder() + "/skia.log", StreamMode::WRITE | StreamMode::TRUNC); |
236 | | writeToLog(logFile, "RenderMethod", "raster"); |
237 | | // Log compiler, Skia works best when compiled with Clang. |
238 | | writeToLog(logFile, "Compiler", skia_compiler_name()); |
239 | | } |
240 | | |
241 | | #if defined(SK_VULKAN) || defined(SK_METAL) |
242 | | static std::unique_ptr<skwindow::WindowContext> getTemporaryWindowContext(); |
243 | | #endif |
244 | | |
245 | | static RenderMethod initRenderMethodToUse() |
246 | | { |
247 | | if (Application::IsBitmapRendering()) |
248 | | return RenderRaster; |
249 | | #if defined(MACOSX) || defined(_WIN32) |
250 | | // macOS/win can __only__ render via skia - there, we can only disable HW acceleration |
251 | | if (Application::IsSafeModeEnabled()) |
252 | | return RenderRaster; |
253 | | #endif |
254 | | |
255 | | if (const char* env = getenv("SAL_SKIA")) |
256 | | { |
257 | | if (strcmp(env, "raster") == 0) |
258 | | return RenderRaster; |
259 | | #if defined SK_METAL |
260 | | if (strcmp(env, "metal") == 0) |
261 | | return RenderMetal; |
262 | | #elif defined SK_VULKAN |
263 | | if (strcmp(env, "vulkan") == 0) |
264 | | return RenderVulkan; |
265 | | #endif |
266 | | SAL_WARN("vcl.skia", "Unrecognized value of SAL_SKIA"); |
267 | | abort(); |
268 | | } |
269 | | if (officecfg::Office::Common::VCL::ForceSkiaGPU::get()) |
270 | | { |
271 | | #if defined SK_METAL |
272 | | return RenderMetal; |
273 | | #elif defined SK_VULKAN |
274 | | return RenderVulkan; |
275 | | #endif |
276 | | } |
277 | | return RenderRaster; |
278 | | } |
279 | | |
280 | | static std::atomic<RenderMethod>& accessRenderMethodToUse() |
281 | | { |
282 | | static std::atomic<RenderMethod> methodToUse = initRenderMethodToUse(); |
283 | | return methodToUse; |
284 | | } |
285 | | |
286 | | RenderMethod renderMethodToUse() { return accessRenderMethodToUse(); } |
287 | | |
288 | | static void forceRasterRenderMethod() { accessRenderMethodToUse() = RenderRaster; } |
289 | | |
290 | | static void checkDeviceDenylisted(bool blockDisable) |
291 | | { |
292 | | static bool done = false; |
293 | | if (done) |
294 | | return; |
295 | | |
296 | | SkiaZone zone; |
297 | | |
298 | | bool useRaster = false; |
299 | | switch (renderMethodToUse()) |
300 | | { |
301 | | case RenderVulkan: |
302 | | { |
303 | | #ifdef SK_VULKAN |
304 | | // Temporarily change config to force software rendering. If the following HW check |
305 | | // crashes, this config change will stay active, and will make sure to avoid use of |
306 | | // faulty HW/driver on the nest start |
307 | | const bool oldForceSkiaGPUValue = setForceSkiaGPU(false); |
308 | | |
309 | | // First try if a GrDirectContext already exists. |
310 | | std::unique_ptr<skwindow::WindowContext> temporaryWindowContext; |
311 | | GrDirectContext* grDirectContext |
312 | | = skwindow::internal::VulkanWindowContext::getSharedGrDirectContext(); |
313 | | if (!grDirectContext) |
314 | | { |
315 | | // This function is called from isVclSkiaEnabled(), which |
316 | | // may be called when deciding which X11 visual to use, |
317 | | // and that visual is normally needed when creating |
318 | | // Skia's VulkanWindowContext, which is needed for the GrDirectContext. |
319 | | // Avoid the loop by creating a temporary WindowContext |
320 | | // that will use the default X11 visual (that shouldn't matter |
321 | | // for just finding out information about Vulkan) and destroying |
322 | | // the temporary context will clean up again. |
323 | | temporaryWindowContext = getTemporaryWindowContext(); |
324 | | grDirectContext |
325 | | = skwindow::internal::VulkanWindowContext::getSharedGrDirectContext(); |
326 | | } |
327 | | bool denylisted = true; // assume the worst |
328 | | if (grDirectContext) // Vulkan was initialized properly |
329 | | { |
330 | | denylisted = isVulkanDenylisted( |
331 | | skwindow::internal::VulkanWindowContext::getPhysDeviceProperties()); |
332 | | SAL_INFO("vcl.skia", "Vulkan denylisted: " << denylisted); |
333 | | } |
334 | | else |
335 | | SAL_INFO("vcl.skia", "Vulkan could not be initialized"); |
336 | | if (denylisted && !blockDisable) |
337 | | { |
338 | | forceRasterRenderMethod(); |
339 | | useRaster = true; |
340 | | } |
341 | | |
342 | | // The check succeeded; restore the original value |
343 | | setForceSkiaGPU(oldForceSkiaGPUValue); |
344 | | #else |
345 | | SAL_WARN("vcl.skia", "Vulkan support not built in"); |
346 | | (void)blockDisable; |
347 | | useRaster = true; |
348 | | #endif |
349 | | break; |
350 | | } |
351 | | case RenderMetal: |
352 | | { |
353 | | #ifdef SK_METAL |
354 | | // First try if a GrDirectContext already exists. |
355 | | std::unique_ptr<skwindow::WindowContext> temporaryWindowContext; |
356 | | GrDirectContext* grDirectContext = skwindow::internal::getMetalSharedGrDirectContext(); |
357 | | if (!grDirectContext) |
358 | | { |
359 | | // Create a temporary window context just to get the GrDirectContext, |
360 | | // as an initial test of Metal functionality. |
361 | | temporaryWindowContext = getTemporaryWindowContext(); |
362 | | grDirectContext = skwindow::internal::getMetalSharedGrDirectContext(); |
363 | | } |
364 | | if (grDirectContext) // Metal was initialized properly |
365 | | { |
366 | | #ifdef MACOSX |
367 | | if (!blockDisable && !DefaultMTLDeviceIsSupported()) |
368 | | { |
369 | | SAL_INFO("vcl.skia", "Metal default device not supported"); |
370 | | forceRasterRenderMethod(); |
371 | | useRaster = true; |
372 | | } |
373 | | else |
374 | | #endif |
375 | | { |
376 | | SAL_INFO("vcl.skia", "Using Skia Metal mode"); |
377 | | writeSkiaMetalInfo(); |
378 | | } |
379 | | } |
380 | | else |
381 | | { |
382 | | SAL_INFO("vcl.skia", "Metal could not be initialized"); |
383 | | forceRasterRenderMethod(); |
384 | | useRaster = true; |
385 | | } |
386 | | #else |
387 | | SAL_WARN("vcl.skia", "Metal support not built in"); |
388 | | useRaster = true; |
389 | | #endif |
390 | | break; |
391 | | } |
392 | | case RenderRaster: |
393 | | useRaster = true; |
394 | | break; |
395 | | } |
396 | | if (useRaster) |
397 | | { |
398 | | SAL_INFO("vcl.skia", "Using Skia raster mode"); |
399 | | // software, never denylisted |
400 | | writeSkiaRasterInfo(); |
401 | | } |
402 | | done = true; |
403 | | } |
404 | | |
405 | | static std::atomic<bool> skiaSupportedByBackend = false; |
406 | | static bool supportsVCLSkia() |
407 | | { |
408 | | if (skiaSupportedByBackend) |
409 | | return true; |
410 | | SAL_INFO("vcl.skia", "Skia not supported by VCL backend, disabling"); |
411 | | return false; |
412 | | } |
413 | | |
414 | | static bool initVCLSkiaEnabled() |
415 | | { |
416 | | /** |
417 | | * Should only be called once! Changing the results in the same |
418 | | * run will mix Skia and normal rendering. |
419 | | */ |
420 | | |
421 | | // allow global disable when testing SystemPrimitiveRenderer since current Skia on Win does not |
422 | | // harmonize with using Direct2D and D2DPixelProcessor2D |
423 | | if (std::getenv("TEST_SYSTEM_PRIMITIVE_RENDERER") != nullptr) |
424 | | return false; |
425 | | |
426 | | /* |
427 | | * There are a number of cases that these environment variables cover: |
428 | | * * SAL_FORCESKIA forces Skia if disabled by UI options or denylisted |
429 | | * * SAL_DISABLESKIA avoids the use of Skia regardless of any option |
430 | | */ |
431 | | |
432 | | bool bSalDisableSkia = getenv("SAL_DISABLESKIA") != nullptr; |
433 | | #if defined(MACOSX) || defined(_WIN32) |
434 | | if (bSalDisableSkia) |
435 | | { |
436 | | SAL_WARN("vcl", "macOS/win requires Skia, so ignoring SAL_DISABLESKIA"); |
437 | | bSalDisableSkia = false; |
438 | | } |
439 | | #endif |
440 | | |
441 | | bool bRet = false; |
442 | | if (supportsVCLSkia() && !bSalDisableSkia) |
443 | | { |
444 | | const bool bForceSkia = getenv("SAL_FORCESKIA") != nullptr |
445 | | || officecfg::Office::Common::VCL::ForceSkia::get(); |
446 | | #if defined(MACOSX) || defined(_WIN32) |
447 | | bRet = true; // macOS/win can __only__ render via skia |
448 | | #else |
449 | | bRet = bForceSkia; |
450 | | // If not forced, don't enable in troubleshoot mode |
451 | | if (!bRet && !Application::IsSafeModeEnabled()) |
452 | | { |
453 | | bRet = getenv("SAL_ENABLESKIA") != nullptr |
454 | | || officecfg::Office::Common::VCL::UseSkia::get(); |
455 | | } |
456 | | #endif |
457 | | |
458 | | if (bRet) |
459 | | { |
460 | | // Set up all things needed for using Skia. |
461 | | SkGraphics::Init(); |
462 | | SkLoOpts::Init(); |
463 | | // if bForceSkia, don't actually block if denylisted, but log it if enabled, |
464 | | // and also get the vendor id; otherwise, switch to raster if driver is denylisted |
465 | | checkDeviceDenylisted(bForceSkia); |
466 | | WatchdogThread::start(); |
467 | | } |
468 | | } |
469 | | |
470 | | CrashReporter::addKeyValue(u"UseSkia"_ustr, OUString::boolean(bRet), CrashReporter::Write); |
471 | | |
472 | | return bRet; |
473 | | } |
474 | | |
475 | | bool isVCLSkiaEnabled() |
476 | | { |
477 | | static const bool val = initVCLSkiaEnabled(); |
478 | | return val; |
479 | | } |
480 | | |
481 | | bool isAlphaMaskBlendingEnabled() { return false; } |
482 | | |
483 | | // If needed, we'll allocate one extra window context so that we have a valid GrDirectContext |
484 | | // from Vulkan/MetalWindowContext. |
485 | | static std::unique_ptr<skwindow::WindowContext> sharedWindowContext; |
486 | | |
487 | | static std::unique_ptr<skwindow::WindowContext> (*createGpuWindowContextFunction)(bool) = nullptr; |
488 | | static void setCreateGpuWindowContext(std::unique_ptr<skwindow::WindowContext> (*function)(bool)) |
489 | | { |
490 | | createGpuWindowContextFunction = function; |
491 | | } |
492 | | |
493 | | GrDirectContext* getSharedGrDirectContext() |
494 | | { |
495 | | SkiaZone zone; |
496 | | assert(renderMethodToUse() != RenderRaster); |
497 | | if (sharedWindowContext) |
498 | | return sharedWindowContext->directContext(); |
499 | | // TODO mutex? |
500 | | // Set up the shared GrDirectContext from Skia's (patched) Vulkan/MetalWindowContext, if it's been |
501 | | // already set up. |
502 | | switch (renderMethodToUse()) |
503 | | { |
504 | | case RenderVulkan: |
505 | | #ifdef SK_VULKAN |
506 | | if (GrDirectContext* context |
507 | | = skwindow::internal::VulkanWindowContext::getSharedGrDirectContext()) |
508 | | return context; |
509 | | #endif |
510 | | break; |
511 | | case RenderMetal: |
512 | | #ifdef SK_METAL |
513 | | if (GrDirectContext* context = skwindow::internal::getMetalSharedGrDirectContext()) |
514 | | return context; |
515 | | #endif |
516 | | break; |
517 | | case RenderRaster: |
518 | | abort(); |
519 | | } |
520 | | static bool done = false; |
521 | | if (done) |
522 | | return nullptr; |
523 | | done = true; |
524 | | if (createGpuWindowContextFunction == nullptr) |
525 | | return nullptr; // not initialized properly (e.g. used from a VCL backend with no Skia support) |
526 | | sharedWindowContext = createGpuWindowContextFunction(false); |
527 | | GrDirectContext* grDirectContext |
528 | | = sharedWindowContext ? sharedWindowContext->directContext() : nullptr; |
529 | | if (grDirectContext) |
530 | | return grDirectContext; |
531 | | SAL_WARN_IF(renderMethodToUse() == RenderVulkan, "vcl.skia", |
532 | | "Cannot create Vulkan GPU context, falling back to Raster"); |
533 | | SAL_WARN_IF(renderMethodToUse() == RenderMetal, "vcl.skia", |
534 | | "Cannot create Metal GPU context, falling back to Raster"); |
535 | | forceRasterRenderMethod(); |
536 | | return nullptr; |
537 | | } |
538 | | |
539 | | #if defined(SK_VULKAN) || defined(SK_METAL) |
540 | | static std::unique_ptr<skwindow::WindowContext> getTemporaryWindowContext() |
541 | | { |
542 | | if (createGpuWindowContextFunction == nullptr) |
543 | | return nullptr; |
544 | | return createGpuWindowContextFunction(true); |
545 | | } |
546 | | #endif |
547 | | |
548 | | static RenderMethod renderMethodToUseForSize(const SkISize& size) |
549 | | { |
550 | | // Do not use GPU for small surfaces. The problem is that due to the separate alpha hack |
551 | | // we quite often may call GetBitmap() on VirtualDevice, which is relatively slow |
552 | | // when the pixels need to be fetched from the GPU. And there are documents that use |
553 | | // many tiny surfaces (bsc#1183308 for example), where this slowness adds up too much. |
554 | | // This should be re-evaluated once the separate alpha hack is removed (SKIA_USE_BITMAP32) |
555 | | // and we no longer (hopefully) fetch pixels that often. |
556 | | if (size.width() <= 32 && size.height() <= 32) |
557 | | return RenderRaster; |
558 | | return renderMethodToUse(); |
559 | | } |
560 | | |
561 | | sk_sp<SkSurface> createSkSurface(int width, int height, SkColorType type, SkAlphaType alpha) |
562 | | { |
563 | | SkiaZone zone; |
564 | | assert(type == kN32_SkColorType || type == kAlpha_8_SkColorType); |
565 | | sk_sp<SkSurface> surface; |
566 | | switch (renderMethodToUseForSize({ width, height })) |
567 | | { |
568 | | case RenderVulkan: |
569 | | case RenderMetal: |
570 | | { |
571 | | if (GrDirectContext* grDirectContext = getSharedGrDirectContext()) |
572 | | { |
573 | | surface = SkSurfaces::RenderTarget(grDirectContext, skgpu::Budgeted::kNo, |
574 | | SkImageInfo::Make(width, height, type, alpha), 0, |
575 | | surfaceProps()); |
576 | | if (surface) |
577 | | { |
578 | | #ifdef DBG_UTIL |
579 | | prefillSurface(surface); |
580 | | #endif |
581 | | return surface; |
582 | | } |
583 | | SAL_WARN_IF(renderMethodToUse() == RenderVulkan, "vcl.skia", |
584 | | "Cannot create Vulkan GPU offscreen surface, falling back to Raster"); |
585 | | SAL_WARN_IF(renderMethodToUse() == RenderMetal, "vcl.skia", |
586 | | "Cannot create Metal GPU offscreen surface, falling back to Raster"); |
587 | | } |
588 | | break; |
589 | | } |
590 | | default: |
591 | | break; |
592 | | } |
593 | | // Create raster surface as a fallback. |
594 | | surface = SkSurfaces::Raster(SkImageInfo::Make(width, height, type, alpha), surfaceProps()); |
595 | | assert(surface); |
596 | | if (surface) |
597 | | { |
598 | | #ifdef DBG_UTIL |
599 | | prefillSurface(surface); |
600 | | #endif |
601 | | return surface; |
602 | | } |
603 | | // In non-debug builds we could return SkSurface::MakeNull() and try to cope with the situation, |
604 | | // but that can lead to unnoticed data loss, so better fail clearly. |
605 | | abort(); |
606 | | } |
607 | | |
608 | | sk_sp<SkImage> createSkImage(const SkBitmap& bitmap) |
609 | | { |
610 | | SkiaZone zone; |
611 | | assert(bitmap.colorType() == kN32_SkColorType || bitmap.colorType() == kAlpha_8_SkColorType); |
612 | | switch (renderMethodToUseForSize(bitmap.dimensions())) |
613 | | { |
614 | | case RenderVulkan: |
615 | | case RenderMetal: |
616 | | { |
617 | | if (GrDirectContext* grDirectContext = getSharedGrDirectContext()) |
618 | | { |
619 | | sk_sp<SkSurface> surface = SkSurfaces::RenderTarget( |
620 | | grDirectContext, skgpu::Budgeted::kNo, |
621 | | bitmap.info().makeAlphaType(kPremul_SkAlphaType), 0, surfaceProps()); |
622 | | if (surface) |
623 | | { |
624 | | SkPaint paint; |
625 | | paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha |
626 | | surface->getCanvas()->drawImage(bitmap.asImage(), 0, 0, SkSamplingOptions(), |
627 | | &paint); |
628 | | return makeCheckedImageSnapshot(surface); |
629 | | } |
630 | | // Try to fall back in non-debug builds. |
631 | | SAL_WARN_IF(renderMethodToUse() == RenderVulkan, "vcl.skia", |
632 | | "Cannot create Vulkan GPU offscreen surface, falling back to Raster"); |
633 | | SAL_WARN_IF(renderMethodToUse() == RenderMetal, "vcl.skia", |
634 | | "Cannot create Metal GPU offscreen surface, falling back to Raster"); |
635 | | } |
636 | | break; |
637 | | } |
638 | | default: |
639 | | break; |
640 | | } |
641 | | // Create raster image as a fallback. |
642 | | sk_sp<SkImage> image = SkImages::RasterFromBitmap(bitmap); |
643 | | assert(image); |
644 | | return image; |
645 | | } |
646 | | |
647 | | sk_sp<SkImage> makeCheckedImageSnapshot(sk_sp<SkSurface> surface) |
648 | | { |
649 | | sk_sp<SkImage> ret = surface->makeImageSnapshot(); |
650 | | assert(ret); |
651 | | if (ret) |
652 | | return ret; |
653 | | abort(); |
654 | | } |
655 | | |
656 | | sk_sp<SkImage> makeCheckedImageSnapshot(sk_sp<SkSurface> surface, const SkIRect& bounds) |
657 | | { |
658 | | sk_sp<SkImage> ret = surface->makeImageSnapshot(bounds); |
659 | | assert(ret); |
660 | | if (ret) |
661 | | return ret; |
662 | | abort(); |
663 | | } |
664 | | |
665 | | namespace |
666 | | { |
667 | | // Image cache, for saving results of complex operations such as drawTransformedBitmap(). |
668 | | struct ImageCacheItem |
669 | | { |
670 | | OString key; |
671 | | sk_sp<SkImage> image; |
672 | | tools::Long size; // cost of the item |
673 | | long long keepInCacheUntilMilliseconds; // don't remove from cache before this timestamp |
674 | | }; |
675 | | } //namespace |
676 | | |
677 | | // LRU cache, last item is the least recently used. Hopefully there won't be that many items |
678 | | // to require a hash/map. Using o3tl::lru_map would be simpler, but it doesn't support |
679 | | // calculating cost of each item. |
680 | | static std::list<ImageCacheItem> imageCache; |
681 | | static tools::Long imageCacheSize = 0; // sum of all ImageCacheItem.size |
682 | | |
683 | | // Related: tdf#166994 set the minimum amount of time that an image must |
684 | | // remain in the image cache to 5 seconds. 2 seconds appears to be enough |
685 | | // on macOS but set it to 5 seconds just to be safe. |
686 | | const long long imageMinKeepInCacheMilliseconds = 5000; |
687 | | |
688 | | void addCachedImage(const OString& key, sk_sp<SkImage> image) |
689 | | { |
690 | | static bool disabled = getenv("SAL_DISABLE_SKIA_CACHE") != nullptr; |
691 | | if (disabled) |
692 | | return; |
693 | | tools::Long size = static_cast<tools::Long>(image->width()) * image->height() |
694 | | * SkColorTypeBytesPerPixel(image->imageInfo().colorType()); |
695 | | auto time = std::chrono::steady_clock::now().time_since_epoch(); |
696 | | auto now = std::chrono::duration_cast<std::chrono::milliseconds>(time).count(); |
697 | | imageCache.push_front({ key, image, size, now + imageMinKeepInCacheMilliseconds }); |
698 | | imageCacheSize += size; |
699 | | SAL_INFO("vcl.skia.trace", "addcachedimage " << image << " :" << size << "/" << imageCacheSize); |
700 | | const tools::Long maxSize = maxImageCacheSize(); |
701 | | while (imageCacheSize > maxSize) |
702 | | { |
703 | | assert(!imageCache.empty()); |
704 | | |
705 | | // tdf#166994 Don't remove image from cache too quickly |
706 | | // While an animation is running, the same images are drawn in a |
707 | | // repeating cycle but each frame is removed from the cache before |
708 | | // it is drawn again. |
709 | | // Blending a bitmap with an alpha channel can be very slow so |
710 | | // it is much faster to keep all of the frames in the image cache. |
711 | | // So, allow the image cache to temporarily increase in size by |
712 | | // deferring removal of an image if it was cached within the last |
713 | | // few seconds. |
714 | | if (now < imageCache.back().keepInCacheUntilMilliseconds) |
715 | | break; |
716 | | |
717 | | imageCacheSize -= imageCache.back().size; |
718 | | SAL_INFO("vcl.skia.trace", |
719 | | "least used removal " << imageCache.back().image << ":" << imageCache.back().size); |
720 | | imageCache.pop_back(); |
721 | | } |
722 | | } |
723 | | |
724 | | sk_sp<SkImage> findCachedImage(const OString& key) |
725 | | { |
726 | | for (auto it = imageCache.begin(); it != imageCache.end(); ++it) |
727 | | { |
728 | | if (it->key == key) |
729 | | { |
730 | | auto time = std::chrono::steady_clock::now().time_since_epoch(); |
731 | | auto now = std::chrono::duration_cast<std::chrono::milliseconds>(time).count(); |
732 | | it->keepInCacheUntilMilliseconds = now + imageMinKeepInCacheMilliseconds; |
733 | | sk_sp<SkImage> ret = it->image; |
734 | | SAL_INFO("vcl.skia.trace", "findcachedimage " << key << " : " << it->image << " found"); |
735 | | imageCache.splice(imageCache.begin(), imageCache, it); |
736 | | return ret; |
737 | | } |
738 | | } |
739 | | SAL_INFO("vcl.skia.trace", "findcachedimage " << key << " not found"); |
740 | | return nullptr; |
741 | | } |
742 | | |
743 | | tools::Long maxImageCacheSize() |
744 | | { |
745 | | // Defaults to 4x 2000px 32bpp images, 64MiB. |
746 | | return officecfg::Office::Common::Cache::Skia::ImageCacheSize::get(); |
747 | | } |
748 | | |
749 | | static o3tl::lru_map<uint32_t, uint32_t> checksumCache(256); |
750 | | |
751 | | static uint32_t computeSkPixmapChecksum(const SkPixmap& pixmap) |
752 | | { |
753 | | // Use uint32_t because that's what SkChecksum::Hash32() returns. |
754 | | static_assert(std::is_same_v<uint32_t, decltype(SkChecksum::Hash32(nullptr, 0, 0))>); |
755 | | const size_t dataRowBytes = pixmap.width() << pixmap.shiftPerPixel(); |
756 | | if (dataRowBytes == pixmap.rowBytes()) |
757 | | return SkChecksum::Hash32(pixmap.addr(), pixmap.height() * dataRowBytes, 0); |
758 | | uint32_t sum = 0; |
759 | | for (int row = 0; row < pixmap.height(); ++row) |
760 | | sum = SkChecksum::Hash32(pixmap.addr(0, row), dataRowBytes, sum); |
761 | | return sum; |
762 | | } |
763 | | |
764 | | uint32_t getSkImageChecksum(sk_sp<SkImage> image) |
765 | | { |
766 | | // Cache the checksums based on the uniqueID() (which should stay the same |
767 | | // for the same image), because it may be still somewhat expensive. |
768 | | uint32_t id = image->uniqueID(); |
769 | | auto it = checksumCache.find(id); |
770 | | if (it != checksumCache.end()) |
771 | | return it->second; |
772 | | SkPixmap pixmap; |
773 | | if (!image->peekPixels(&pixmap)) |
774 | | abort(); // Fetching of GPU-based pixels is expensive, and shouldn't(?) be needed anyway. |
775 | | uint32_t checksum = computeSkPixmapChecksum(pixmap); |
776 | | checksumCache.insert({ id, checksum }); |
777 | | return checksum; |
778 | | } |
779 | | |
780 | | static sk_sp<SkBlender> invertBlender; |
781 | | static sk_sp<SkBlender> xorBlender; |
782 | | |
783 | | // This does the invert operation, i.e. result = color(255-R,255-G,255-B,A). |
784 | | void setBlenderInvert(SkPaint* paint) |
785 | | { |
786 | | if (!invertBlender) |
787 | | { |
788 | | // Note that the colors are premultiplied, so '1 - dst.r' must be |
789 | | // written as 'dst.a - dst.r', since premultiplied R is in the range (0-A). |
790 | | const char* const diff = R"( |
791 | | vec4 main( vec4 src, vec4 dst ) |
792 | | { |
793 | | return vec4( dst.a - dst.r, dst.a - dst.g, dst.a - dst.b, dst.a ); |
794 | | } |
795 | | )"; |
796 | | auto effect = SkRuntimeEffect::MakeForBlender(SkString(diff)); |
797 | | if (!effect.effect) |
798 | | { |
799 | | SAL_WARN("vcl.skia", |
800 | | "SKRuntimeEffect::MakeForBlender failed: " << effect.errorText.c_str()); |
801 | | abort(); |
802 | | } |
803 | | invertBlender = effect.effect->makeBlender(nullptr); |
804 | | } |
805 | | paint->setBlender(invertBlender); |
806 | | } |
807 | | |
808 | | // This does the xor operation, i.e. bitwise xor of RGB values of both colors. |
809 | | void setBlenderXor(SkPaint* paint) |
810 | | { |
811 | | if (!xorBlender) |
812 | | { |
813 | | // Note that the colors are premultiplied, converting to 0-255 range |
814 | | // must also unpremultiply. |
815 | | const char* const diff = R"( |
816 | | vec4 main( vec4 src, vec4 dst ) |
817 | | { |
818 | | return vec4( |
819 | | float(int(src.r * src.a * 255.0) ^ int(dst.r * dst.a * 255.0)) / 255.0 / dst.a, |
820 | | float(int(src.g * src.a * 255.0) ^ int(dst.g * dst.a * 255.0)) / 255.0 / dst.a, |
821 | | float(int(src.b * src.a * 255.0) ^ int(dst.b * dst.a * 255.0)) / 255.0 / dst.a, |
822 | | dst.a ); |
823 | | } |
824 | | )"; |
825 | | SkRuntimeEffect::Options opts; |
826 | | // Skia does not allow binary operators in the default ES2Strict mode, but that's only |
827 | | // because of OpenGL support. We don't use OpenGL, and it's safe for all modes that we do use. |
828 | | // https://groups.google.com/g/skia-discuss/c/EPLuQbg64Kc/m/2uDXFIGhAwAJ |
829 | | opts.maxVersionAllowed = SkSL::Version::k300; |
830 | | auto effect = SkRuntimeEffect::MakeForBlender(SkString(diff), opts); |
831 | | if (!effect.effect) |
832 | | { |
833 | | SAL_WARN("vcl.skia", |
834 | | "SKRuntimeEffect::MakeForBlender failed: " << effect.errorText.c_str()); |
835 | | abort(); |
836 | | } |
837 | | xorBlender = effect.effect->makeBlender(nullptr); |
838 | | } |
839 | | paint->setBlender(xorBlender); |
840 | | } |
841 | | |
842 | | void cleanup() |
843 | | { |
844 | | sharedWindowContext.reset(); |
845 | | imageCache.clear(); |
846 | | imageCacheSize = 0; |
847 | | invertBlender.reset(); |
848 | | xorBlender.reset(); |
849 | | } |
850 | | |
851 | | static SkSurfaceProps commonSurfaceProps; |
852 | | const SkSurfaceProps* surfaceProps() { return &commonSurfaceProps; } |
853 | | |
854 | | void setPixelGeometry(SkPixelGeometry pixelGeometry) |
855 | | { |
856 | | commonSurfaceProps = SkSurfaceProps(commonSurfaceProps.flags(), pixelGeometry); |
857 | | } |
858 | | |
859 | | // Skia should not be used from VCL backends that do not actually support it, as there will be setup missing. |
860 | | // The code here (that is in the vcl lib) needs a function for creating Vulkan/Metal context that is |
861 | | // usually available only in the backend libs. |
862 | | void prepareSkia(std::unique_ptr<skwindow::WindowContext> (*createGpuWindowContext)(bool)) |
863 | | { |
864 | | setCreateGpuWindowContext(createGpuWindowContext); |
865 | | skiaSupportedByBackend = true; |
866 | | } |
867 | | |
868 | | void dump(const SkBitmap& bitmap, const char* file) |
869 | | { |
870 | | dump(SkImages::RasterFromBitmap(bitmap), file); |
871 | | } |
872 | | |
873 | | void dump(const sk_sp<SkSurface>& surface, const char* file) |
874 | | { |
875 | | if (auto dContext = GrAsDirectContext(surface->getCanvas()->recordingContext())) |
876 | | dContext->flushAndSubmit(); |
877 | | dump(makeCheckedImageSnapshot(surface), file); |
878 | | } |
879 | | |
880 | | void dump(const sk_sp<SkImage>& image, const char* file) |
881 | | { |
882 | | SkBitmap bm; |
883 | | if (!as_IB(image)->getROPixels(getSharedGrDirectContext(), &bm)) |
884 | | return; |
885 | | SkPixmap pixmap; |
886 | | if (!bm.peekPixels(&pixmap)) |
887 | | return; |
888 | | SkPngEncoder::Options opts; |
889 | | opts.fFilterFlags = SkPngEncoder::FilterFlag::kNone; |
890 | | opts.fZLibLevel = 1; |
891 | | SkDynamicMemoryWStream stream; |
892 | | if (!SkPngEncoder::Encode(&stream, pixmap, opts)) |
893 | | return; |
894 | | sk_sp<SkData> data = stream.detachAsData(); |
895 | | std::ofstream ostream(file, std::ios::binary); |
896 | | ostream.write(static_cast<const char*>(data->data()), data->size()); |
897 | | } |
898 | | |
899 | | sk_sp<SkTypeface> applyVariations(const sk_sp<SkTypeface>& skTypeface, |
900 | | const LogicalFontInstance& font) |
901 | | { |
902 | | const auto& variations = font.GetVariations(); |
903 | | if (variations.empty() || !skTypeface) |
904 | | return skTypeface; |
905 | | |
906 | | std::vector<SkFontArguments::VariationPosition::Coordinate> skCoords; |
907 | | skCoords.reserve(variations.size()); |
908 | | for (const auto& var : variations) |
909 | | skCoords.push_back({ var.nTag, var.fValue }); |
910 | | |
911 | | SkFontArguments fontArgs; |
912 | | SkFontArguments::VariationPosition varPosition |
913 | | = { skCoords.data(), static_cast<int>(skCoords.size()) }; |
914 | | fontArgs.setVariationDesignPosition(varPosition); |
915 | | |
916 | | sk_sp<SkTypeface> variationFace = skTypeface->makeClone(fontArgs); |
917 | | if (variationFace) |
918 | | return variationFace; |
919 | | return skTypeface; |
920 | | } |
921 | | |
922 | | #ifdef DBG_UTIL |
923 | | void prefillSurface(const sk_sp<SkSurface>& surface) |
924 | | { |
925 | | // Pre-fill the surface with deterministic garbage. |
926 | | SkBitmap bitmap; |
927 | | bitmap.allocN32Pixels(2, 2); |
928 | | SkPMColor* scanline; |
929 | | scanline = bitmap.getAddr32(0, 0); |
930 | | *scanline++ = SkPreMultiplyARGB(0xFF, 0xBF, 0x80, 0x40); |
931 | | *scanline++ = SkPreMultiplyARGB(0xFF, 0x40, 0x80, 0xBF); |
932 | | scanline = bitmap.getAddr32(0, 1); |
933 | | *scanline++ = SkPreMultiplyARGB(0xFF, 0xE3, 0x5C, 0x13); |
934 | | *scanline++ = SkPreMultiplyARGB(0xFF, 0x13, 0x5C, 0xE3); |
935 | | bitmap.setImmutable(); |
936 | | SkPaint paint; |
937 | | paint.setBlendMode(SkBlendMode::kSrc); // set as is, including alpha |
938 | | paint.setShader( |
939 | | bitmap.makeShader(SkTileMode::kRepeat, SkTileMode::kRepeat, SkSamplingOptions())); |
940 | | surface->getCanvas()->drawPaint(paint); |
941 | | } |
942 | | #endif |
943 | | |
944 | | } // namespace |
945 | | |
946 | | #endif // HAVE_FEATURE_SKIA |
947 | | |
948 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |