// 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 #include #include 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(fWorkElapsed).count(); } void dump() const { duration totalThreadTime = fThreadStop - fThreadStart; double totalThreadTimeSeconds = std::chrono::duration(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 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(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) { 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 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 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>; SkTArray fThreads; WorkList fWork; SkMutex fWorkLock; SkSemaphore fWorkAvailable; }; class GrTaskGroup : SkNoncopyable { public: explicit GrTaskGroup(SkSpan threadInfo) : fPending(0) , fThreadPool(std::make_unique(threadInfo)) { } ~GrTaskGroup() { this->wait(); } // Add a task to this SkTaskGroup. void add(std::function 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 fPending; std::unique_ptr fThreadPool; }; static bool create_contexts(GrContextFactory* factory, GrContextFactory::ContextType contextType, const GrContextFactory::ContextOverrides& overrides, ThreadInfo* gpuThread, SkSpan 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 create_shared_skp(const char* src, GrDirectContext* dContext, DDLPromiseImageHelper* promiseImageHelper) { SkString srcfile(src); std::unique_ptr srcstream(SkStream::MakeFromFile(srcfile.c_str())); if (!srcstream) { exitf("failed to open file %s", srcfile.c_str()); } sk_sp 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 mainContext(new ThreadInfo); std::unique_ptr utilityContexts(new ThreadInfo[FLAGS_numRecordingThreads]); if (!create_contexts(&factory, kContextType, kOverrides, mainContext.get(), SkSpan(utilityContexts.get(), FLAGS_numRecordingThreads))) { return 1; } mainContext->fTestContext->makeCurrent(); SkYUVAPixmapInfo::SupportedDataTypes supportedYUVADTypes(*mainContext->fDirectContext); DDLPromiseImageHelper promiseImageHelper(supportedYUVADTypes); sk_sp 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 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(mainContext.get(), 1)); GrTaskGroup recordingTaskGroup(SkSpan(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; }