Coverage Report

Created: 2026-10-14 09:33

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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: */