441 lines
15 KiB
C++
441 lines
15 KiB
C++
// Copyright 2021 Google LLC.
|
|
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
|
|
|
|
#include "include/core/SkCanvas.h"
|
|
#include "include/core/SkGraphics.h"
|
|
#include "include/core/SkPicture.h"
|
|
#include "include/core/SkScalar.h"
|
|
#include "include/core/SkSurface.h"
|
|
#include "include/gpu/GrDirectContext.h"
|
|
#include "include/private/SkSLDefines.h"
|
|
#include "src/core/SkOSFile.h"
|
|
#include "src/gpu/GrCaps.h"
|
|
#include "src/gpu/GrDirectContextPriv.h"
|
|
#include "src/utils/SkOSPath.h"
|
|
#include "tools/DDLPromiseImageHelper.h"
|
|
#include "tools/ToolUtils.h"
|
|
#include "tools/flags/CommandLineFlags.h"
|
|
#include "tools/gpu/GrContextFactory.h"
|
|
#include "tools/gpu/TestContext.h"
|
|
|
|
#include <chrono>
|
|
#include <deque>
|
|
#include <thread>
|
|
|
|
using hires_clock = std::chrono::high_resolution_clock;
|
|
using duration = std::chrono::nanoseconds;
|
|
|
|
using sk_gpu_test::ContextInfo;
|
|
using sk_gpu_test::GrContextFactory;
|
|
using sk_gpu_test::TestContext;
|
|
|
|
static DEFINE_int(numRecordingThreads, 1, "number of DDL recording threads");
|
|
static DEFINE_int(numTilesX, 3, "number of tiles horizontally");
|
|
static DEFINE_int(numTilesY, 3, "number of tiles vertically");
|
|
static DEFINE_int(numSamples, 1, "number of MSAA samples");
|
|
static DEFINE_string(png, "", "if set, save a .png proof to disk at this file location");
|
|
static DEFINE_string(src, "", "input .skp file");
|
|
|
|
static void exitf(const char* format, ...) {
|
|
va_list args;
|
|
va_start(args, format);
|
|
vfprintf(stderr, format, args);
|
|
va_end(args);
|
|
|
|
exit(1);
|
|
}
|
|
|
|
// Each thread holds this chunk of data thread_locally
|
|
struct ThreadInfo {
|
|
ThreadInfo() = default;
|
|
|
|
ThreadInfo(const SkString& name, GrDirectContext* directContext, TestContext* testContext)
|
|
: fName(name)
|
|
, fDirectContext(directContext)
|
|
, fTestContext(testContext) {
|
|
}
|
|
|
|
double elapsedWorkSeconds() const {
|
|
return std::chrono::duration<double>(fWorkElapsed).count();
|
|
}
|
|
|
|
void dump() const {
|
|
duration totalThreadTime = fThreadStop - fThreadStart;
|
|
double totalThreadTimeSeconds = std::chrono::duration<double>(totalThreadTime).count();
|
|
|
|
printf("%s: num work units %d work: %.2gs total: %.2gs utilization %.2g%%\n",
|
|
fName.c_str(),
|
|
fWorkUnit,
|
|
this->elapsedWorkSeconds(),
|
|
totalThreadTimeSeconds,
|
|
100.0f * this->elapsedWorkSeconds() / totalThreadTimeSeconds);
|
|
}
|
|
|
|
SkString fName;
|
|
|
|
// These two can be null on recording/utility threads
|
|
GrDirectContext* fDirectContext = nullptr;
|
|
TestContext* fTestContext = nullptr;
|
|
|
|
int fWorkUnit = 0;
|
|
duration fWorkElapsed {0};
|
|
hires_clock::time_point fThreadStart;
|
|
hires_clock::time_point fThreadStop;
|
|
};
|
|
|
|
#if SKSL_USE_THREAD_LOCAL
|
|
|
|
static thread_local ThreadInfo* gThreadInfo;
|
|
|
|
static ThreadInfo* get_thread_local_info() {
|
|
return gThreadInfo;
|
|
}
|
|
|
|
static void set_thread_local_info(ThreadInfo* threadInfo) {
|
|
gThreadInfo = threadInfo;
|
|
}
|
|
|
|
#else
|
|
|
|
#include <pthread.h>
|
|
|
|
static pthread_key_t get_pthread_key() {
|
|
static pthread_key_t sKey = []{
|
|
pthread_key_t key;
|
|
int result = pthread_key_create(&key, /*destructor=*/nullptr);
|
|
if (result != 0) {
|
|
SK_ABORT("pthread_key_create failure: %d", result);
|
|
}
|
|
return key;
|
|
}();
|
|
return sKey;
|
|
}
|
|
|
|
static ThreadInfo* get_thread_local_info() {
|
|
return static_cast<ThreadInfo*>(pthread_getspecific(get_pthread_key()));
|
|
}
|
|
|
|
static void set_thread_local_info(ThreadInfo* threadInfo) {
|
|
pthread_setspecific(get_pthread_key(), threadInfo);
|
|
}
|
|
|
|
#endif
|
|
|
|
static void set_up_context_on_thread(ThreadInfo* threadInfo) {
|
|
if (threadInfo->fDirectContext) {
|
|
threadInfo->fTestContext->makeCurrent();
|
|
}
|
|
|
|
threadInfo->fThreadStart = hires_clock::now();
|
|
|
|
set_thread_local_info(threadInfo);
|
|
}
|
|
|
|
class GrThreadPool {
|
|
public:
|
|
explicit GrThreadPool(SkSpan<ThreadInfo> threadInfo) {
|
|
for (size_t i = 0; i < threadInfo.size(); i++) {
|
|
fThreads.emplace_back(&Loop, this, &threadInfo[i]);
|
|
}
|
|
}
|
|
|
|
~GrThreadPool() {
|
|
// Signal each thread that it's time to shut down.
|
|
for (int i = 0; i < fThreads.count(); i++) {
|
|
this->add(nullptr);
|
|
}
|
|
// Wait for each thread to shut down.
|
|
for (int i = 0; i < fThreads.count(); i++) {
|
|
fThreads[i].join();
|
|
}
|
|
}
|
|
|
|
void add(std::function<void(void)> work) {
|
|
// Add some work to our pile of work to do.
|
|
{
|
|
SkAutoMutexExclusive lock(fWorkLock);
|
|
fWork.emplace_back(std::move(work));
|
|
}
|
|
// Tell the Loop() threads to pick it up.
|
|
fWorkAvailable.signal(1);
|
|
}
|
|
|
|
private:
|
|
// This method should be called only when fWorkAvailable indicates there's work to do.
|
|
bool do_work(ThreadInfo* threadInfo) {
|
|
std::function<void(void)> work;
|
|
{
|
|
SkAutoMutexExclusive lock(fWorkLock);
|
|
SkASSERT(!fWork.empty()); // TODO: if (fWork.empty()) { return true; } ?
|
|
work = std::move(fWork.front());
|
|
fWork.pop_front();
|
|
}
|
|
|
|
if (!work) {
|
|
threadInfo->fThreadStop = hires_clock::now();
|
|
return false; // This is Loop()'s signal to shut down.
|
|
}
|
|
|
|
hires_clock::time_point start = hires_clock::now();
|
|
work();
|
|
threadInfo->fWorkElapsed = hires_clock::now() - start;
|
|
threadInfo->fWorkUnit++;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void Loop(void* ctx, ThreadInfo* threadInfo) {
|
|
set_up_context_on_thread(threadInfo);
|
|
|
|
auto pool = (GrThreadPool*)ctx;
|
|
do {
|
|
pool->fWorkAvailable.wait();
|
|
} while (pool->do_work(threadInfo));
|
|
}
|
|
|
|
using WorkList = std::deque<std::function<void(void)>>;
|
|
|
|
SkTArray<std::thread> fThreads;
|
|
WorkList fWork;
|
|
SkMutex fWorkLock;
|
|
SkSemaphore fWorkAvailable;
|
|
};
|
|
|
|
class GrTaskGroup : SkNoncopyable {
|
|
public:
|
|
explicit GrTaskGroup(SkSpan<ThreadInfo> threadInfo)
|
|
: fPending(0)
|
|
, fThreadPool(std::make_unique<GrThreadPool>(threadInfo)) {
|
|
}
|
|
|
|
~GrTaskGroup() { this->wait(); }
|
|
|
|
// Add a task to this SkTaskGroup.
|
|
void add(std::function<void(void)> fn) {
|
|
fPending.fetch_add(+1, std::memory_order_relaxed);
|
|
fThreadPool->add([this, fn{std::move(fn)}] {
|
|
fn();
|
|
fPending.fetch_add(-1, std::memory_order_release);
|
|
});
|
|
}
|
|
|
|
// Returns true if all Tasks previously add()ed to this SkTaskGroup have run.
|
|
// It is safe to reuse this SkTaskGroup once done().
|
|
bool done() const {
|
|
return fPending.load(std::memory_order_acquire) == 0;
|
|
}
|
|
|
|
// Block until done().
|
|
void wait() {
|
|
while (!this->done()) {
|
|
std::this_thread::yield();
|
|
}
|
|
}
|
|
|
|
private:
|
|
std::atomic<int32_t> fPending;
|
|
std::unique_ptr<GrThreadPool> fThreadPool;
|
|
};
|
|
|
|
static bool create_contexts(GrContextFactory* factory,
|
|
GrContextFactory::ContextType contextType,
|
|
const GrContextFactory::ContextOverrides& overrides,
|
|
ThreadInfo* gpuThread,
|
|
SkSpan<ThreadInfo> utilityThreads) {
|
|
|
|
ContextInfo mainInfo = factory->getContextInfo(contextType, overrides);
|
|
if (!mainInfo.directContext()) {
|
|
exitf("Could not create primary direct context.");
|
|
}
|
|
|
|
*gpuThread = { SkString("g0"), mainInfo.directContext(), mainInfo.testContext() };
|
|
|
|
bool allSucceeded = true, allFailed = true;
|
|
// Create the utility contexts in a share group with the primary one. This is allowed to fail
|
|
// but either they should all work or the should all fail.
|
|
for (size_t i = 0; i < utilityThreads.size(); ++i) {
|
|
SkString name = SkStringPrintf("r%zu", i);
|
|
|
|
ContextInfo tmp = factory->getSharedContextInfo(mainInfo.directContext(), i);
|
|
|
|
utilityThreads[i] = { name, tmp.directContext(), tmp.testContext() };
|
|
allSucceeded &= SkToBool(tmp.directContext());
|
|
allFailed &= !tmp.directContext();
|
|
}
|
|
|
|
return allSucceeded || allFailed;
|
|
}
|
|
|
|
static sk_sp<SkPicture> create_shared_skp(const char* src,
|
|
GrDirectContext* dContext,
|
|
DDLPromiseImageHelper* promiseImageHelper) {
|
|
SkString srcfile(src);
|
|
|
|
std::unique_ptr<SkStream> srcstream(SkStream::MakeFromFile(srcfile.c_str()));
|
|
if (!srcstream) {
|
|
exitf("failed to open file %s", srcfile.c_str());
|
|
}
|
|
|
|
sk_sp<SkPicture> skp = SkPicture::MakeFromStream(srcstream.get());
|
|
if (!skp) {
|
|
exitf("failed to parse file %s", srcfile.c_str());
|
|
}
|
|
|
|
return promiseImageHelper->recreateSKP(dContext, skp.get());
|
|
}
|
|
|
|
static void check_params(GrDirectContext* dContext,
|
|
int width, int height, SkColorType ct, SkAlphaType at, int numSamples) {
|
|
|
|
if (dContext->maxRenderTargetSize() < std::max(width, height)) {
|
|
exitf("render target size %ix%i not supported by platform (max: %i)",
|
|
width, height, dContext->maxRenderTargetSize());
|
|
}
|
|
|
|
GrBackendFormat format = dContext->defaultBackendFormat(ct, GrRenderable::kYes);
|
|
if (!format.isValid()) {
|
|
exitf("failed to get GrBackendFormat from SkColorType: %d", ct);
|
|
}
|
|
|
|
int supportedSampleCount = dContext->priv().caps()->getRenderTargetSampleCount(numSamples,
|
|
format);
|
|
if (supportedSampleCount != numSamples) {
|
|
exitf("sample count %i not supported by platform", numSamples);
|
|
}
|
|
}
|
|
|
|
static bool mkdir_p(const SkString& dirname) {
|
|
if (dirname.isEmpty() || dirname == SkString("/")) {
|
|
return true;
|
|
}
|
|
return mkdir_p(SkOSPath::Dirname(dirname.c_str())) && sk_mkdir(dirname.c_str());
|
|
}
|
|
|
|
static void maybe_save_file(SkSurface* surface) {
|
|
if (FLAGS_png.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
SkBitmap bmp;
|
|
bmp.allocPixels(surface->imageInfo());
|
|
if (!surface->getCanvas()->readPixels(bmp, 0, 0)) {
|
|
exitf("failed to read canvas pixels for png");
|
|
}
|
|
if (!mkdir_p(SkOSPath::Dirname(FLAGS_png[0]))) {
|
|
exitf("failed to create directory for png \"%s\"", FLAGS_png[0]);
|
|
}
|
|
if (!ToolUtils::EncodeImageToFile(FLAGS_png[0], bmp, SkEncodedImageFormat::kPNG, 100)) {
|
|
exitf("failed to save png to \"%s\"", FLAGS_png[0]);
|
|
}
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
CommandLineFlags::Parse(argc, argv);
|
|
|
|
if (FLAGS_src.count() != 1) {
|
|
exitf("Missing input file");
|
|
}
|
|
|
|
// TODO: get these from the command line flags
|
|
const GrContextFactory::ContextType kContextType = GrContextFactory::kGL_ContextType;
|
|
const GrContextOptions kContextOptions;
|
|
const GrContextFactory::ContextOverrides kOverrides = GrContextFactory::ContextOverrides::kNone;
|
|
SkColorType ct = kRGBA_8888_SkColorType;
|
|
SkAlphaType at = kPremul_SkAlphaType;
|
|
|
|
SkGraphics::Init();
|
|
|
|
GrContextFactory factory(kContextOptions);
|
|
|
|
std::unique_ptr<ThreadInfo> mainContext(new ThreadInfo);
|
|
std::unique_ptr<ThreadInfo[]> utilityContexts(new ThreadInfo[FLAGS_numRecordingThreads]);
|
|
|
|
if (!create_contexts(&factory,
|
|
kContextType,
|
|
kOverrides,
|
|
mainContext.get(),
|
|
SkSpan<ThreadInfo>(utilityContexts.get(), FLAGS_numRecordingThreads))) {
|
|
return 1;
|
|
}
|
|
|
|
mainContext->fTestContext->makeCurrent();
|
|
|
|
SkYUVAPixmapInfo::SupportedDataTypes supportedYUVADTypes(*mainContext->fDirectContext);
|
|
DDLPromiseImageHelper promiseImageHelper(supportedYUVADTypes);
|
|
|
|
sk_sp<SkPicture> skp = create_shared_skp(FLAGS_src[0],
|
|
mainContext->fDirectContext,
|
|
&promiseImageHelper);
|
|
|
|
int width = std::min(SkScalarCeilToInt(skp->cullRect().width()), 2048);
|
|
int height = std::min(SkScalarCeilToInt(skp->cullRect().height()), 2048);
|
|
|
|
check_params(mainContext->fDirectContext, width, height, ct, at, FLAGS_numSamples);
|
|
|
|
// TODO: do this later on a utility thread!
|
|
promiseImageHelper.uploadAllToGPU(nullptr, mainContext->fDirectContext);
|
|
|
|
SkImageInfo info = SkImageInfo::Make(width, height, ct, at, nullptr);
|
|
|
|
sk_sp<SkSurface> dstSurface = SkSurface::MakeRenderTarget(mainContext->fDirectContext,
|
|
SkBudgeted::kNo, info,
|
|
FLAGS_numSamples, nullptr);
|
|
if (!dstSurface) {
|
|
exitf("Could not create a surface.");
|
|
}
|
|
|
|
if (FLAGS_numRecordingThreads == 0) {
|
|
mainContext->fThreadStart = hires_clock::now();
|
|
|
|
dstSurface->getCanvas()->drawPicture(skp);
|
|
|
|
mainContext->fThreadStop = hires_clock::now();
|
|
|
|
mainContext->fWorkElapsed = mainContext->fThreadStop - mainContext->fThreadStart;
|
|
mainContext->fWorkUnit++;
|
|
} else {
|
|
mainContext->fTestContext->makeNotCurrent();
|
|
|
|
GrTaskGroup gpuTaskGroup(SkSpan<ThreadInfo>(mainContext.get(), 1));
|
|
GrTaskGroup recordingTaskGroup(SkSpan<ThreadInfo>(utilityContexts.get(),
|
|
FLAGS_numRecordingThreads));
|
|
|
|
for (int i = 0; i < FLAGS_numRecordingThreads; ++i) {
|
|
recordingTaskGroup.add([] {
|
|
ThreadInfo* threadLocal = get_thread_local_info();
|
|
printf("%s: dContext %p\n", threadLocal->fName.c_str(),
|
|
threadLocal->fDirectContext);
|
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
});
|
|
}
|
|
|
|
gpuTaskGroup.add([] {
|
|
ThreadInfo* threadLocal = get_thread_local_info();
|
|
printf("%s: dContext %p\n", threadLocal->fName.c_str(),
|
|
threadLocal->fDirectContext);
|
|
});
|
|
|
|
gpuTaskGroup.add([] {
|
|
ThreadInfo* threadLocal = get_thread_local_info();
|
|
threadLocal->fTestContext->makeNotCurrent();
|
|
});
|
|
|
|
recordingTaskGroup.wait();
|
|
gpuTaskGroup.wait();
|
|
|
|
mainContext->fTestContext->makeCurrent();
|
|
}
|
|
|
|
maybe_save_file(dstSurface.get());
|
|
|
|
promiseImageHelper.deleteAllFromGPU(nullptr, mainContext->fDirectContext);
|
|
|
|
// Dump out the timing stats
|
|
mainContext->dump();
|
|
for (int i = 0; i < FLAGS_numRecordingThreads; ++i) {
|
|
utilityContexts[i].dump();
|
|
}
|
|
|
|
return 0;
|
|
}
|