blob: d4a186c7dd0641a5fa71754b5ef6cd6b19c206ab [file] [log] [blame]
/*
* Copyright 2024 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "tests/Test.h"
#if defined(SK_GRAPHITE)
#include "include/core/SkCanvas.h"
#include "include/core/SkPaint.h"
#include "include/core/SkTextBlob.h"
#include "include/effects/SkGradientShader.h"
#include "include/gpu/graphite/PrecompileContext.h"
#include "include/gpu/graphite/Surface.h"
#include "include/gpu/graphite/precompile/PaintOptions.h"
#include "include/gpu/graphite/precompile/Precompile.h"
#include "include/gpu/graphite/precompile/PrecompileShader.h"
#include "src/gpu/graphite/ContextPriv.h"
#include "src/gpu/graphite/GraphicsPipelineDesc.h"
#include "src/gpu/graphite/RenderPassDesc.h"
#include "src/gpu/graphite/ResourceProvider.h"
#include "tools/fonts/FontToolUtils.h"
#include "tools/graphite/UniqueKeyUtils.h"
#include <algorithm>
#include <random>
#include <thread>
using namespace::skgpu::graphite;
namespace {
static constexpr int kMaxNumStops = 9;
static constexpr SkColor gColors[kMaxNumStops] = {
SK_ColorRED,
SK_ColorGREEN,
SK_ColorBLUE,
SK_ColorCYAN,
SK_ColorMAGENTA,
SK_ColorYELLOW,
SK_ColorBLACK,
SK_ColorDKGRAY,
SK_ColorLTGRAY,
};
static constexpr SkPoint gPts[kMaxNumStops] = {
{ -100.0f, -100.0f },
{ -50.0f, -50.0f },
{ -25.0f, -25.0f },
{ -12.5f, -12.5f },
{ 0.0f, 0.0f },
{ 12.5f, 12.5f },
{ 25.0f, 25.0f },
{ 50.0f, 50.0f },
{ 100.0f, 100.0f }
};
static constexpr float gOffsets[kMaxNumStops] =
{ 0.0f, 0.125f, 0.25f, 0.375f, 0.5f, 0.625f, 0.75f, 0.875f, 1.0f };
std::pair<SkPaint, PaintOptions> linear(int numStops) {
SkASSERT(numStops <= kMaxNumStops);
PaintOptions paintOptions;
paintOptions.setShaders({ PrecompileShaders::LinearGradient() });
paintOptions.setBlendModes({ SkBlendMode::kSrcOver });
SkPaint paint;
paint.setShader(SkGradientShader::MakeLinear(gPts,
gColors, gOffsets, numStops,
SkTileMode::kClamp));
paint.setBlendMode(SkBlendMode::kSrcOver);
return { paint, paintOptions };
}
std::pair<SkPaint, PaintOptions> radial(int numStops) {
SkASSERT(numStops <= kMaxNumStops);
PaintOptions paintOptions;
paintOptions.setShaders({ PrecompileShaders::RadialGradient() });
paintOptions.setBlendModes({ SkBlendMode::kSrcOver });
SkPaint paint;
paint.setShader(SkGradientShader::MakeRadial(/* center= */ {0, 0}, /* radius= */ 100,
gColors, gOffsets, numStops,
SkTileMode::kClamp));
paint.setBlendMode(SkBlendMode::kSrcOver);
return { paint, paintOptions };
}
std::pair<SkPaint, PaintOptions> sweep(int numStops) {
SkASSERT(numStops <= kMaxNumStops);
PaintOptions paintOptions;
paintOptions.setShaders({ PrecompileShaders::SweepGradient() });
paintOptions.setBlendModes({ SkBlendMode::kSrcOver });
SkPaint paint;
paint.setShader(SkGradientShader::MakeSweep(/* cx= */ 0, /* cy= */ 0,
gColors, gOffsets, numStops,
SkTileMode::kClamp,
/* startAngle= */ 0, /* endAngle= */ 359,
/* flags= */ 0, /* localMatrix= */ nullptr));
paint.setBlendMode(SkBlendMode::kSrcOver);
return { paint, paintOptions };
}
std::pair<SkPaint, PaintOptions> conical(int numStops) {
SkASSERT(numStops <= kMaxNumStops);
PaintOptions paintOptions;
paintOptions.setShaders({ PrecompileShaders::TwoPointConicalGradient() });
paintOptions.setBlendModes({ SkBlendMode::kSrcOver });
SkPaint paint;
paint.setShader(SkGradientShader::MakeTwoPointConical(/* start= */ {100, 100},
/* startRadius= */ 100,
/* end= */ {-100, -100},
/* endRadius= */ 100,
gColors, gOffsets, numStops,
SkTileMode::kClamp));
paint.setBlendMode(SkBlendMode::kSrcOver);
return { paint, paintOptions };
}
// The 12 comes from 4 types of gradient times 3 combinations (i.e., 4,8,N) for each one.
static constexpr int kNumDiffPipelines = 12;
typedef std::pair<SkPaint, PaintOptions> (*GradientCreationFunc)(int numStops);
struct Combo {
GradientCreationFunc fCreateOptionsMtd;
int fNumStops;
};
void precompile_gradients(std::unique_ptr<PrecompileContext> precompileContext,
bool permute,
skiatest::Reporter* /* reporter */,
int /* threadID */) {
std::array<Combo, 4> combos;
// numStops doesn't influence the paintOptions
combos[0] = { linear, /* fNumStops= */ 2 };
combos[1] = { radial, /* fNumStops= */ 2 };
combos[2] = { sweep, /* fNumStops= */ 2 };
combos[3] = { conical, /* fNumStops= */ 2 };
if (permute) {
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(combos.begin(), combos.end(), g);
}
const RenderPassProperties kProps = { DepthStencilFlags::kDepth,
kBGRA_8888_SkColorType,
/* dstColorSpace= */ nullptr,
/* requiresMSAA= */ false };
for (auto c : combos) {
auto [_, paintOptions] = c.fCreateOptionsMtd(c.fNumStops);
Precompile(precompileContext.get(),
paintOptions,
DrawTypeFlags::kBitmapText_Mask,
{ &kProps, 1 });
}
precompileContext.reset();
}
void purge_on_thread(std::unique_ptr<PrecompileContext> precompileContext,
std::atomic_bool* keepLooping,
skiatest::Reporter* /* reporter */,
int /* threadID */) {
const auto kSleepDuration = std::chrono::milliseconds(1);
while (*keepLooping) {
std::this_thread::sleep_for(kSleepDuration);
precompileContext->purgePipelinesNotUsedInMs(kSleepDuration);
}
precompileContext.reset();
}
// A simple helper to call Context::insertRecording on the Recordings generated on the
// recorder threads. It collects (and keeps ownership) of all the generated Recordings.
class Listener : public SkRefCnt {
public:
Listener(int numSenders) : fNumActiveSenders(numSenders) {}
void addRecording(std::unique_ptr<Recording> recording) SK_EXCLUDES(fLock) {
{
SkAutoMutexExclusive lock(fLock);
fRecordings.push_back(std::move(recording));
}
fWorkAvailable.signal(1);
}
void deregister() SK_EXCLUDES(fLock) {
{
SkAutoMutexExclusive lock(fLock);
fNumActiveSenders--;
}
fWorkAvailable.signal(1);
}
void insertRecordings(Context* context) {
do {
fWorkAvailable.wait();
} while (this->insertRecording(context));
}
private:
// This entry point is run in a loop waiting on the 'fWorkAvailable' semaphore until there
// are no senders remaining (at which point it returns false) c.f. 'insertRecordings'.
bool insertRecording(Context* context) SK_EXCLUDES(fLock) {
Recording* recording = nullptr;
int numSendersLeft;
{
SkAutoMutexExclusive lock(fLock);
numSendersLeft = fNumActiveSenders;
SkASSERT(fRecordings.size() >= fCurHandled);
if (fRecordings.size() > fCurHandled) {
recording = fRecordings[fCurHandled++].get();
}
}
if (recording) {
context->insertRecording({recording});
return true; // continue looping
}
return SkToBool(numSendersLeft); // continue looping if there are still active senders
}
SkMutex fLock;
SkSemaphore fWorkAvailable;
skia_private::TArray<std::unique_ptr<Recording>> fRecordings SK_GUARDED_BY(fLock);
int fCurHandled SK_GUARDED_BY(fLock) = 0;
int fNumActiveSenders SK_GUARDED_BY(fLock);
};
void compile_gradients(std::unique_ptr<Recorder> recorder,
sk_sp<Listener> listener,
bool permute,
skiatest::Reporter* /* reporter */,
int /* threadID */) {
std::array<Combo, kNumDiffPipelines> combos;
int i = 0;
for (auto createOptionsMtd : { linear, radial, sweep, conical }) {
for (int numStops: { 2, 7, kMaxNumStops }) {
combos[i++] = { createOptionsMtd, numStops };
}
}
if (permute) {
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(combos.begin(), combos.end(), g);
}
SkFont font(ToolUtils::DefaultPortableTypeface(), /* size= */ 16);
const char text[] = "hambur1";
sk_sp<SkTextBlob> blob = SkTextBlob::MakeFromText(text, strlen(text), font);
SkImageInfo ii = SkImageInfo::Make(16, 16,
kBGRA_8888_SkColorType,
kPremul_SkAlphaType);
sk_sp<SkSurface> surf = SkSurfaces::RenderTarget(recorder.get(), ii,
skgpu::Mipmapped::kNo,
/* surfaceProps= */ nullptr);
SkCanvas* canvas = surf->getCanvas();
for (auto c : combos) {
auto [paint, _] = c.fCreateOptionsMtd(c.fNumStops);
canvas->drawTextBlob(blob, 0, 16, paint);
// This will trigger pipeline creation via TaskList::prepareResources
std::unique_ptr<skgpu::graphite::Recording> recording = recorder->snap();
listener->addRecording(std::move(recording));
}
listener->deregister();
}
void run_test(Context* context,
skiatest::Reporter* reporter,
int numPurgingThreads,
int numRecordingThreads,
int numPrecompileThreads,
bool permute) {
const int totNumThreads = numPurgingThreads + numRecordingThreads + numPrecompileThreads;
sk_sp<Listener> listener;
if (numRecordingThreads) {
listener = sk_make_sp<Listener>(numRecordingThreads);
}
std::atomic_bool keepPurging = true; // controls the looping in the purging thread(s)
std::vector<std::thread> threads;
threads.reserve(totNumThreads);
int threadID = 0;
for (int i = 0; i < numPurgingThreads; ++i, ++threadID) {
std::unique_ptr<PrecompileContext> precompileContext = context->makePrecompileContext();
threads.push_back(std::thread(purge_on_thread,
std::move(precompileContext),
&keepPurging,
reporter,
threadID));
}
for (int i = 0; i < numRecordingThreads; ++i, ++threadID) {
std::unique_ptr<Recorder> recorder = context->makeRecorder();
threads.push_back(std::thread(compile_gradients,
std::move(recorder),
listener,
permute,
reporter,
threadID));
}
for (int i = 0; i < numPrecompileThreads; ++i, ++threadID) {
std::unique_ptr<PrecompileContext> precompileContext = context->makePrecompileContext();
threads.push_back(std::thread(precompile_gradients,
std::move(precompileContext),
permute,
reporter,
threadID));
}
// Process the work generated by the recording threads
if (listener) {
listener->insertRecordings(context);
}
keepPurging = false; // stop the loops in the purging thread(s)
for (auto& thread : threads) {
if (thread.joinable()) {
thread.join();
}
}
context->submit(SyncToCpu::kYes);
}
[[maybe_unused]] void dump_stats(skgpu::BackendApi api, const GlobalCache::PipelineStats& stats) {
SkDebugf("%s ------------------------------------------------------------------------------\n"
"CacheHits: %d\n"
"CacheMisses: %d\n"
"CacheAdditions: %d\n"
"Races: %d\n"
"Purges: %d\n",
BackendApiToStr(api),
stats.fGraphicsCacheHits,
stats.fGraphicsCacheMisses,
stats.fGraphicsCacheAdditions,
stats.fGraphicsRaces,
stats.fGraphicsPurges);
}
} // anonymous namespace
// This test precompiles all four flavors of gradient sequentially but on multiple
// threads with the goal of creating cache races.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelinePrecompileTest,
reporter,
context,
CtsEnforcement::kNever) {
constexpr int kNumPurgingThreads = 0;
constexpr int kNumRecordingThreads = 0;
constexpr int kNumPrecompileThreads = 4;
constexpr bool kDontPermute = false;
run_test(context, reporter, kNumPurgingThreads, kNumRecordingThreads, kNumPrecompileThreads,
kDontPermute);
const GlobalCache::PipelineStats stats = context->priv().globalCache()->getStats();
// The 48 comes from:
// 4 gradient flavors (linear, radial, ...) *
// 3 types of each flavor (4, 8, N) *
// 4 precompile threads
REPORTER_ASSERT(reporter, stats.fGraphicsCacheHits + stats.fGraphicsCacheMisses == 48);
REPORTER_ASSERT(reporter, stats.fGraphicsCacheAdditions == kNumDiffPipelines);
REPORTER_ASSERT(reporter, stats.fGraphicsRaces > 0);
REPORTER_ASSERT(reporter, stats.fGraphicsPurges == 0);
REPORTER_ASSERT(reporter, stats.fGraphicsCacheMisses == stats.fGraphicsCacheAdditions +
stats.fGraphicsRaces);
}
// This test runs two threads compiling the gradient flavours and two threads
// pre-compiling the gradient flavors. This is to exercise the tracking of the
// various race combinations (i.e., Normal vs Precompile, Normal vs. Normal, etc.).
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelinePrecompileCompileTest,
reporter,
context,
CtsEnforcement::kNever) {
constexpr int kNumPurgingThreads = 0;
constexpr int kNumRecordingThreads = 2;
constexpr int kNumPrecompileThreads = 2;
constexpr bool kDontPermute = false;
run_test(context, reporter, kNumPurgingThreads, kNumRecordingThreads, kNumPrecompileThreads,
kDontPermute);
const GlobalCache::PipelineStats stats = context->priv().globalCache()->getStats();
// The 48 comes from:
// 4 gradient flavors (linear, radial, ...) *
// 3 types of each flavor (4, 8, N) *
// (2 normal-compile threads + 2 pre-compile threads)
REPORTER_ASSERT(reporter, stats.fGraphicsCacheHits + stats.fGraphicsCacheMisses == 48);
REPORTER_ASSERT(reporter, stats.fGraphicsCacheAdditions == kNumDiffPipelines);
REPORTER_ASSERT(reporter, stats.fGraphicsRaces > 0);
REPORTER_ASSERT(reporter, stats.fGraphicsPurges == 0);
REPORTER_ASSERT(reporter, stats.fGraphicsCacheMisses == stats.fGraphicsCacheAdditions +
stats.fGraphicsRaces);
}
// This test compiles the gradient flavors on a thread and then tests out the time-based
// purging.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelineCompilePurgingTest,
reporter,
context,
CtsEnforcement::kNever) {
constexpr int kNumPurgingThreads = 0;
constexpr int kNumRecordingThreads = 1;
constexpr int kNumPrecompileThreads = 0;
constexpr bool kDontPermute = false;
std::unique_ptr<PrecompileContext> precompileContext = context->makePrecompileContext();
auto begin = std::chrono::steady_clock::now();
run_test(context, reporter, kNumPurgingThreads, kNumRecordingThreads, kNumPrecompileThreads,
kDontPermute);
auto end = std::chrono::steady_clock::now();
auto deltaMS = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
precompileContext->purgePipelinesNotUsedInMs(2*deltaMS);
GlobalCache::PipelineStats stats = context->priv().globalCache()->getStats();
REPORTER_ASSERT(reporter, stats.fGraphicsCacheHits == 0);
REPORTER_ASSERT(reporter, stats.fGraphicsCacheMisses == kNumDiffPipelines);
REPORTER_ASSERT(reporter, stats.fGraphicsCacheAdditions == kNumDiffPipelines);
REPORTER_ASSERT(reporter, stats.fGraphicsRaces == 0);
// Every created Pipeline should've been used since the start of this test
REPORTER_ASSERT(reporter, stats.fGraphicsPurges == 0, "num purges: %d", stats.fGraphicsPurges);
//--------------------------------------------------------------------------------------------
const auto kSleepDuration = std::chrono::milliseconds(1);
std::this_thread::sleep_for(kSleepDuration);
precompileContext->purgePipelinesNotUsedInMs(kSleepDuration);
stats = context->priv().globalCache()->getStats();
REPORTER_ASSERT(reporter, stats.fGraphicsCacheHits == 0);
REPORTER_ASSERT(reporter, stats.fGraphicsCacheMisses == kNumDiffPipelines);
REPORTER_ASSERT(reporter, stats.fGraphicsCacheAdditions == kNumDiffPipelines);
REPORTER_ASSERT(reporter, stats.fGraphicsRaces == 0);
// None of the created Pipelines should've been used since we started to sleep - so they
// all get purged.
REPORTER_ASSERT(reporter, stats.fGraphicsPurges == kNumDiffPipelines);
}
// This test *precompiles* the gradient flavors on a thread and then tests out the time-based
// purging.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelinePrecompilePurgingTest,
reporter,
context,
CtsEnforcement::kNever) {
constexpr int kNumPurgingThreads = 0;
constexpr int kNumRecordingThreads = 0;
constexpr int kNumPrecompileThreads = 1;
constexpr bool kDontPermute = false;
std::unique_ptr<PrecompileContext> precompileContext = context->makePrecompileContext();
auto begin = std::chrono::steady_clock::now();
run_test(context, reporter, kNumPurgingThreads, kNumRecordingThreads, kNumPrecompileThreads,
kDontPermute);
auto end = std::chrono::steady_clock::now();
auto deltaMS = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
precompileContext->purgePipelinesNotUsedInMs(deltaMS);
GlobalCache::PipelineStats stats = context->priv().globalCache()->getStats();
REPORTER_ASSERT(reporter, stats.fGraphicsCacheHits == 0);
REPORTER_ASSERT(reporter, stats.fGraphicsCacheMisses == kNumDiffPipelines);
REPORTER_ASSERT(reporter, stats.fGraphicsCacheAdditions == kNumDiffPipelines);
REPORTER_ASSERT(reporter, stats.fGraphicsRaces == 0);
// Precompilation doesn't count as a use so all the Pipelines will be purged even though
// they were created w/in 'deltaMS'
REPORTER_ASSERT(reporter, stats.fGraphicsPurges == kNumDiffPipelines);
}
// This test fires off two compilation threads, two precompilation threads and one
// purging thread. This is intended to stress test the Pipeline cache's thread safety and
// the purging behavior.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelinePrecompileCompilePurgingTest,
reporter,
context,
CtsEnforcement::kNever) {
constexpr int kNumPurgingThreads = 1;
constexpr int kNumRecordingThreads = 2;
constexpr int kNumPrecompileThreads = 2;
constexpr bool kPermute = true;
run_test(context, reporter, kNumPurgingThreads, kNumRecordingThreads, kNumPrecompileThreads,
kPermute);
GlobalCache::PipelineStats stats = context->priv().globalCache()->getStats();
// The 48 comes from:
// 4 gradient flavors (linear, radial, ...) *
// 3 types of each flavor (4, 8, N) *
// (2 normal-compile threads + 2 pre-compile threads)
REPORTER_ASSERT(reporter, stats.fGraphicsCacheHits + stats.fGraphicsCacheMisses == 48);
REPORTER_ASSERT(reporter, stats.fGraphicsCacheMisses == stats.fGraphicsCacheAdditions +
stats.fGraphicsRaces);
// Purges can force recreation of a Pipeline
REPORTER_ASSERT(reporter, stats.fGraphicsCacheAdditions >= kNumDiffPipelines);
// Given the use of permutations it is possible, though unlikely, that there are
// no races (particularly on Dawn/Metal).
//REPORTER_ASSERT(reporter, stats.fGraphicsRaces > 0);
}
#endif // SK_GRAPHITE