aboutsummaryrefslogtreecommitdiffhomepage
path: root/dm
diff options
context:
space:
mode:
Diffstat (limited to 'dm')
-rw-r--r--dm/DM.cpp166
-rw-r--r--dm/DMComparisonTask.cpp22
-rw-r--r--dm/DMComparisonTask.h31
-rw-r--r--dm/DMCpuTask.cpp57
-rw-r--r--dm/DMCpuTask.h44
-rw-r--r--dm/DMGpuTask.cpp63
-rw-r--r--dm/DMGpuTask.h45
-rw-r--r--dm/DMReplayTask.cpp50
-rw-r--r--dm/DMReplayTask.h40
-rw-r--r--dm/DMReporter.cpp24
-rw-r--r--dm/DMReporter.h39
-rw-r--r--dm/DMTask.cpp42
-rw-r--r--dm/DMTask.h43
-rw-r--r--dm/DMTaskRunner.cpp28
-rw-r--r--dm/DMTaskRunner.h28
-rw-r--r--dm/DMUtil.cpp23
-rw-r--r--dm/DMUtil.h23
-rw-r--r--dm/README37
18 files changed, 805 insertions, 0 deletions
diff --git a/dm/DM.cpp b/dm/DM.cpp
new file mode 100644
index 0000000000..d149b484a8
--- /dev/null
+++ b/dm/DM.cpp
@@ -0,0 +1,166 @@
+// Main binary for DM.
+// For a high-level overview, please see dm/README.
+
+#include "GrContext.h"
+#include "GrContextFactory.h"
+#include "SkCommandLineFlags.h"
+#include "SkForceLinking.h"
+#include "SkGraphics.h"
+#include "gm.h"
+
+#include "DMReporter.h"
+#include "DMTask.h"
+#include "DMTaskRunner.h"
+#include "DMCpuTask.h"
+#include "DMGpuTask.h"
+
+#include <string.h>
+
+using skiagm::GM;
+using skiagm::GMRegistry;
+using skiagm::Expectations;
+using skiagm::ExpectationsSource;
+using skiagm::JsonExpectationsSource;
+
+DEFINE_int32(cpuThreads, -1, "Threads for CPU work. Default NUM_CPUS.");
+DEFINE_int32(gpuThreads, 1, "Threads for GPU work.");
+DEFINE_string(expectations, "", "Compare generated images against JSON expectations at this path.");
+DEFINE_string(resources, "resources", "Path to resources directory.");
+DEFINE_string(match, "", "[~][^]substring[$] [...] of GM name to run.\n"
+ "Multiple matches may be separated by spaces.\n"
+ "~ causes a matching GM to always be skipped\n"
+ "^ requires the start of the GM to match\n"
+ "$ requires the end of the GM to match\n"
+ "^ and $ requires an exact match\n"
+ "If a GM does not match any list entry,\n"
+ "it is skipped unless some list entry starts with ~");
+DEFINE_string(config, "8888 gpu",
+ "Options: 565 8888 gpu msaa4 msaa16 gpunull gpudebug angle mesa"); // TODO(mtklein): pdf
+
+__SK_FORCE_IMAGE_DECODER_LINKING;
+
+// Split str on any characters in delimiters into out. (Think, strtok with a sane API.)
+static void split(const char* str, const char* delimiters, SkTArray<SkString>* out) {
+ const char* end = str + strlen(str);
+ while (str != end) {
+ // Find a token.
+ const size_t len = strcspn(str, delimiters);
+ out->push_back().set(str, len);
+ str += len;
+ // Skip any delimiters.
+ str += strspn(str, delimiters);
+ }
+}
+
+// "FooBar" -> "foobar". Obviously, ASCII only.
+static SkString lowercase(SkString s) {
+ for (size_t i = 0; i < s.size(); i++) {
+ s[i] = tolower(s[i]);
+ }
+ return s;
+}
+
+static void kick_off_tasks(const SkTDArray<GMRegistry::Factory>& gms,
+ const SkTArray<SkString>& configs,
+ const ExpectationsSource& expectations,
+ DM::Reporter* reporter,
+ DM::TaskRunner* tasks) {
+ const SkBitmap::Config _565 = SkBitmap::kRGB_565_Config;
+ const SkBitmap::Config _8888 = SkBitmap::kARGB_8888_Config;
+ const GrContextFactory::GLContextType native = GrContextFactory::kNative_GLContextType;
+ const GrContextFactory::GLContextType null = GrContextFactory::kNull_GLContextType;
+ const GrContextFactory::GLContextType debug = GrContextFactory::kDebug_GLContextType;
+ const GrContextFactory::GLContextType angle =
+ #if SK_ANGLE
+ GrContextFactory::kANGLE_GLContextType;
+ #else
+ native;
+ #endif
+ const GrContextFactory::GLContextType mesa =
+ #if SK_MESA
+ GLContextFactory::kMESA_GLContextType;
+ #else
+ native;
+ #endif
+
+ for (int i = 0; i < gms.count(); i++) {
+ SkAutoTDelete<GM> gmForName(gms[i](NULL));
+ if (SkCommandLineFlags::ShouldSkip(FLAGS_match, gmForName->shortName())) continue;
+
+#define START(name, type, ...) \
+ if (lowercase(configs[j]).equals(name)) { \
+ tasks->add(SkNEW_ARGS(DM::type, \
+ (name, reporter, tasks, expectations, gms[i], __VA_ARGS__))); \
+ }
+ for (int j = 0; j < configs.count(); j++) {
+ START("565", CpuTask, _565);
+ START("8888", CpuTask, _8888);
+ START("gpu", GpuTask, _8888, native, 0);
+ START("msaa4", GpuTask, _8888, native, 4);
+ START("msaa16", GpuTask, _8888, native, 16);
+ START("gpunull", GpuTask, _8888, null, 0);
+ START("gpudebug", GpuTask, _8888, debug, 0);
+ START("angle", GpuTask, _8888, angle, 0);
+ START("mesa", GpuTask, _8888, mesa, 0);
+ //START("pdf", PdfTask, _8888);
+ }
+ }
+#undef START
+}
+
+static void report_failures(const DM::Reporter& reporter) {
+ SkTArray<SkString> failures;
+ reporter.getFailures(&failures);
+
+ if (failures.count() == 0) {
+ return;
+ }
+
+ SkDebugf("Failures:\n");
+ for (int i = 0; i < failures.count(); i++) {
+ SkDebugf(" %s\n", failures[i].c_str());
+ }
+}
+
+class NoExpectations : public ExpectationsSource {
+public:
+ Expectations get(const char* /*testName*/) const SK_OVERRIDE {
+ return Expectations();
+ }
+};
+
+
+int main(int argc, char** argv) {
+ SkGraphics::Init();
+
+ SkCommandLineFlags::Parse(argc, argv);
+ GM::SetResourcePath(FLAGS_resources[0]);
+ SkTArray<SkString> configs;
+ for (int i = 0; i < FLAGS_config.count(); i++) {
+ split(FLAGS_config[i], ", ", &configs);
+ }
+
+ SkTDArray<GMRegistry::Factory> gms;
+ for (const GMRegistry* reg = GMRegistry::Head(); reg != NULL; reg = reg->next()) {
+ *gms.append() = reg->factory();
+ }
+ SkDebugf("%d GMs x %d configs\n", gms.count(), configs.count());
+
+ SkAutoTUnref<ExpectationsSource> expectations(SkNEW(NoExpectations));
+ if (FLAGS_expectations.count() > 0) {
+ expectations.reset(SkNEW_ARGS(JsonExpectationsSource, (FLAGS_expectations[0])));
+ }
+
+ DM::Reporter reporter;
+ DM::TaskRunner tasks(FLAGS_cpuThreads, FLAGS_gpuThreads);
+ kick_off_tasks(gms, configs, *expectations, &reporter, &tasks);
+ tasks.wait();
+
+ reporter.updateStatusLine();
+ SkDebugf("\n");
+ report_failures(reporter);
+
+ SkGraphics::Term();
+
+ return reporter.failed() > 0;
+}
diff --git a/dm/DMComparisonTask.cpp b/dm/DMComparisonTask.cpp
new file mode 100644
index 0000000000..f4f742c921
--- /dev/null
+++ b/dm/DMComparisonTask.cpp
@@ -0,0 +1,22 @@
+#include "DMComparisonTask.h"
+#include "DMUtil.h"
+
+namespace DM {
+
+ComparisonTask::ComparisonTask(const Task& parent,
+ skiagm::Expectations expectations,
+ SkBitmap bitmap)
+ : Task(parent)
+ , fName(parent.name()) // Masquerade as parent so failures are attributed to it.
+ , fExpectations(expectations)
+ , fBitmap(bitmap)
+ {}
+
+void ComparisonTask::draw() {
+ const skiagm::GmResultDigest digest(fBitmap);
+ if (!meetsExpectations(fExpectations, digest)) {
+ this->fail();
+ }
+}
+
+} // namespace DM
diff --git a/dm/DMComparisonTask.h b/dm/DMComparisonTask.h
new file mode 100644
index 0000000000..265a58ce32
--- /dev/null
+++ b/dm/DMComparisonTask.h
@@ -0,0 +1,31 @@
+#ifndef DMComparisonTask_DEFINED
+#define DMComparisonTask_DEFINED
+
+#include "DMTask.h"
+#include "SkBitmap.h"
+#include "SkString.h"
+#include "gm_expectations.h"
+
+namespace DM {
+
+// We use ComparisonTask to move CPU-bound comparison work of GpuTasks back to
+// the main thread pool, where we probably have more threads available.
+
+class ComparisonTask : public Task {
+public:
+ ComparisonTask(const Task& parent, skiagm::Expectations, SkBitmap);
+
+ virtual void draw() SK_OVERRIDE;
+ virtual bool usesGpu() const SK_OVERRIDE { return false; }
+ virtual bool shouldSkip() const SK_OVERRIDE { return false; }
+ virtual SkString name() const SK_OVERRIDE { return fName; }
+
+private:
+ const SkString fName;
+ const skiagm::Expectations fExpectations;
+ const SkBitmap fBitmap;
+};
+
+} // namespace DM
+
+#endif // DMComparisonTask_DEFINED
diff --git a/dm/DMCpuTask.cpp b/dm/DMCpuTask.cpp
new file mode 100644
index 0000000000..5534ba4a43
--- /dev/null
+++ b/dm/DMCpuTask.cpp
@@ -0,0 +1,57 @@
+#include "DMCpuTask.h"
+#include "DMReplayTask.h"
+#include "DMUtil.h"
+#include "SkCommandLineFlags.h"
+
+DEFINE_bool(replay, false, "If true, run replay tests for each CpuTask.");
+// TODO(mtklein): add the other various options
+
+namespace DM {
+
+CpuTask::CpuTask(const char* name,
+ Reporter* reporter,
+ TaskRunner* taskRunner,
+ const skiagm::ExpectationsSource& expectations,
+ skiagm::GMRegistry::Factory gmFactory,
+ SkBitmap::Config config)
+ : Task(reporter, taskRunner)
+ , fGMFactory(gmFactory)
+ , fGM(fGMFactory(NULL))
+ , fName(underJoin(fGM->shortName(), name))
+ , fExpectations(expectations.get(png(fName).c_str()))
+ , fConfig(config)
+ {}
+
+void CpuTask::draw() {
+ SkBitmap bitmap;
+ bitmap.setConfig(fConfig, fGM->width(), fGM->height());
+ bitmap.allocPixels();
+ bitmap.eraseColor(0x00000000);
+ SkCanvas canvas(bitmap);
+
+ canvas.concat(fGM->getInitialTransform());
+ fGM->draw(&canvas);
+ canvas.flush();
+
+ const skiagm::GmResultDigest digest(bitmap);
+ if (!meetsExpectations(fExpectations, digest)) {
+ this->fail();
+ }
+
+ if (FLAGS_replay) {
+ this->spawnChild(SkNEW_ARGS(ReplayTask,
+ ("replay", *this, fGMFactory(NULL), digest, fConfig)));
+ }
+}
+
+bool CpuTask::shouldSkip() const {
+ if (SkBitmap::kRGB_565_Config == fConfig && (fGM->getFlags() & skiagm::GM::kSkip565_Flag)) {
+ return true;
+ }
+ if (fGM->getFlags() & skiagm::GM::kGPUOnly_Flag) {
+ return true;
+ }
+ return false;
+}
+
+} // namespace DM
diff --git a/dm/DMCpuTask.h b/dm/DMCpuTask.h
new file mode 100644
index 0000000000..998ed7ba4d
--- /dev/null
+++ b/dm/DMCpuTask.h
@@ -0,0 +1,44 @@
+#ifndef DMCpuTask_DEFINED
+#define DMCpuTask_DEFINED
+
+#include "DMReporter.h"
+#include "DMTask.h"
+#include "DMTaskRunner.h"
+#include "SkBitmap.h"
+#include "SkString.h"
+#include "SkTemplates.h"
+#include "gm.h"
+#include "gm_expectations.h"
+
+// This is the main entry point for drawing GMs with the CPU. Commandline
+// flags control whether this kicks off various comparison tasks when done.
+// Currently:
+// --replay: spawn a DMReplayTask to record into a picture, draw the picture, and compare.
+
+namespace DM {
+
+class CpuTask : public Task {
+public:
+ CpuTask(const char* name,
+ Reporter*,
+ TaskRunner*,
+ const skiagm::ExpectationsSource&,
+ skiagm::GMRegistry::Factory,
+ SkBitmap::Config);
+
+ virtual void draw() SK_OVERRIDE;
+ virtual bool usesGpu() const SK_OVERRIDE { return false; }
+ virtual bool shouldSkip() const SK_OVERRIDE;
+ virtual SkString name() const SK_OVERRIDE { return fName; }
+
+private:
+ skiagm::GMRegistry::Factory fGMFactory;
+ SkAutoTDelete<skiagm::GM> fGM;
+ const SkString fName;
+ const skiagm::Expectations fExpectations;
+ const SkBitmap::Config fConfig;
+};
+
+} // namespace DM
+
+#endif // DMCpuTask_DEFINED
diff --git a/dm/DMGpuTask.cpp b/dm/DMGpuTask.cpp
new file mode 100644
index 0000000000..9205cb996b
--- /dev/null
+++ b/dm/DMGpuTask.cpp
@@ -0,0 +1,63 @@
+#include "DMGpuTask.h"
+
+#include "DMComparisonTask.h"
+#include "DMUtil.h"
+#include "SkCommandLineFlags.h"
+#include "SkGpuDevice.h"
+#include "SkTLS.h"
+
+namespace DM {
+
+GpuTask::GpuTask(const char* name,
+ Reporter* reporter,
+ TaskRunner* taskRunner,
+ const skiagm::ExpectationsSource& expectations,
+ skiagm::GMRegistry::Factory gmFactory,
+ SkBitmap::Config config,
+ GrContextFactory::GLContextType contextType,
+ int sampleCount)
+ : Task(reporter, taskRunner)
+ , fGM(gmFactory(NULL))
+ , fName(underJoin(fGM->shortName(), name))
+ , fExpectations(expectations.get(png(fName).c_str()))
+ , fConfig(config)
+ , fContextType(contextType)
+ , fSampleCount(sampleCount)
+ {}
+
+static void* new_gr_context_factory() {
+ return SkNEW(GrContextFactory);
+}
+
+static void delete_gr_context_factory(void* factory) {
+ return SkDELETE((GrContextFactory*) factory);
+}
+
+static GrContextFactory* get_gr_factory() {
+ return reinterpret_cast<GrContextFactory*>(SkTLS::Get(&new_gr_context_factory,
+ &delete_gr_context_factory));
+}
+
+void GpuTask::draw() {
+ GrContext* gr = get_gr_factory()->get(fContextType); // Will be owned by device.
+ SkGpuDevice device(gr, fConfig, fGM->width(), fGM->height(), fSampleCount);
+ SkCanvas canvas(&device);
+
+ canvas.concat(fGM->getInitialTransform());
+ fGM->draw(&canvas);
+ canvas.flush();
+
+ SkBitmap bitmap;
+ bitmap.setConfig(fConfig, fGM->width(), fGM->height());
+ canvas.readPixels(&bitmap, 0, 0);
+
+ // We offload checksum comparison to the main CPU threadpool.
+ // This cuts run time by about 30%.
+ this->spawnChild(SkNEW_ARGS(ComparisonTask, (*this, fExpectations, bitmap)));
+}
+
+bool GpuTask::shouldSkip() const {
+ return fGM->getFlags() & skiagm::GM::kSkipGPU_Flag;
+}
+
+} // namespace DM
diff --git a/dm/DMGpuTask.h b/dm/DMGpuTask.h
new file mode 100644
index 0000000000..87c530b825
--- /dev/null
+++ b/dm/DMGpuTask.h
@@ -0,0 +1,45 @@
+#ifndef DMGpuTask_DEFINED
+#define DMGpuTask_DEFINED
+
+#include "DMReporter.h"
+#include "DMTask.h"
+#include "DMTaskRunner.h"
+#include "GrContextFactory.h"
+#include "SkBitmap.h"
+#include "SkString.h"
+#include "SkTemplates.h"
+#include "gm.h"
+#include "gm_expectations.h"
+
+// This is the main entry point for drawing GMs with the GPU.
+
+namespace DM {
+
+class GpuTask : public Task {
+public:
+ GpuTask(const char* name,
+ Reporter*,
+ TaskRunner*,
+ const skiagm::ExpectationsSource&,
+ skiagm::GMRegistry::Factory,
+ SkBitmap::Config,
+ GrContextFactory::GLContextType,
+ int sampleCount);
+
+ virtual void draw() SK_OVERRIDE;
+ virtual bool usesGpu() const SK_OVERRIDE { return true; }
+ virtual bool shouldSkip() const SK_OVERRIDE;
+ virtual SkString name() const SK_OVERRIDE { return fName; }
+
+private:
+ SkAutoTDelete<skiagm::GM> fGM;
+ const SkString fName;
+ const skiagm::Expectations fExpectations;
+ const SkBitmap::Config fConfig;
+ const GrContextFactory::GLContextType fContextType;
+ const int fSampleCount;
+};
+
+} // namespace DM
+
+#endif // DMGpuTask_DEFINED
diff --git a/dm/DMReplayTask.cpp b/dm/DMReplayTask.cpp
new file mode 100644
index 0000000000..bc94f73cb1
--- /dev/null
+++ b/dm/DMReplayTask.cpp
@@ -0,0 +1,50 @@
+#include "DMReplayTask.h"
+#include "DMUtil.h"
+
+#include "SkPicture.h"
+
+namespace DM {
+
+ReplayTask::ReplayTask(const char* suffix,
+ const Task& parent,
+ skiagm::GM* gm,
+ skiagm::GmResultDigest reference,
+ SkBitmap::Config config)
+ : Task(parent)
+ , fName(underJoin(parent.name().c_str(), suffix))
+ , fGM(gm)
+ , fReference(reference)
+ , fConfig(config)
+ {}
+
+void ReplayTask::draw() {
+ SkPicture picture;
+ SkCanvas* canvas = picture.beginRecording(fGM->width(), fGM->height(), 0 /*flags*/);
+
+ canvas->concat(fGM->getInitialTransform());
+ fGM->draw(canvas);
+ canvas->flush();
+
+ picture.endRecording();
+
+ SkBitmap bitmap;
+ bitmap.setConfig(fConfig, fGM->width(), fGM->height());
+ bitmap.allocPixels();
+ bitmap.eraseColor(0x00000000);
+
+ SkCanvas replay(bitmap);
+ replay.drawPicture(picture);
+ replay.flush();
+
+ const skiagm::GmResultDigest replayDigest(bitmap);
+ if (!replayDigest.equals(fReference)) {
+ this->fail();
+ }
+}
+
+bool ReplayTask::shouldSkip() const {
+ return fGM->getFlags() & skiagm::GM::kGPUOnly_Flag ||
+ fGM->getFlags() & skiagm::GM::kSkipPicture_Flag;
+}
+
+} // namespace
diff --git a/dm/DMReplayTask.h b/dm/DMReplayTask.h
new file mode 100644
index 0000000000..0ed9351c13
--- /dev/null
+++ b/dm/DMReplayTask.h
@@ -0,0 +1,40 @@
+#ifndef DMReplayTask_DEFINED
+#define DMReplayTask_DEFINED
+
+#include "DMReporter.h"
+#include "DMTask.h"
+#include "DMTaskRunner.h"
+#include "SkBitmap.h"
+#include "SkString.h"
+#include "SkTemplates.h"
+#include "gm.h"
+#include "gm_expectations.h"
+
+// Records a GM through an SkPicture, draws it, and compares against the reference checksum.
+
+namespace DM {
+
+class ReplayTask : public Task {
+
+public:
+ ReplayTask(const char* name,
+ const Task& parent,
+ skiagm::GM*,
+ skiagm::GmResultDigest reference,
+ SkBitmap::Config);
+
+ virtual void draw() SK_OVERRIDE;
+ virtual bool usesGpu() const SK_OVERRIDE { return false; }
+ virtual bool shouldSkip() const SK_OVERRIDE;
+ virtual SkString name() const SK_OVERRIDE { return fName; }
+
+private:
+ const SkString fName;
+ SkAutoTDelete<skiagm::GM> fGM;
+ const skiagm::GmResultDigest fReference;
+ const SkBitmap::Config fConfig;
+};
+
+} // namespace DM
+
+#endif // DMReplayTask_DEFINED
diff --git a/dm/DMReporter.cpp b/dm/DMReporter.cpp
new file mode 100644
index 0000000000..7a0c20edd3
--- /dev/null
+++ b/dm/DMReporter.cpp
@@ -0,0 +1,24 @@
+#include "DMReporter.h"
+
+namespace DM {
+
+void Reporter::updateStatusLine() const {
+ SkDebugf("\r\033[K%d / %d, %d failed", this->finished(), this->started(), this->failed());
+}
+
+int32_t Reporter::failed() const {
+ SkAutoMutexAcquire reader(&fMutex);
+ return fFailures.count();
+}
+
+void Reporter::fail(SkString name) {
+ SkAutoMutexAcquire writer(&fMutex);
+ fFailures.push_back(name);
+}
+
+void Reporter::getFailures(SkTArray<SkString>* failures) const {
+ SkAutoMutexAcquire reader(&fMutex);
+ *failures = fFailures;
+}
+
+} // namespace DM
diff --git a/dm/DMReporter.h b/dm/DMReporter.h
new file mode 100644
index 0000000000..4f4ad432d5
--- /dev/null
+++ b/dm/DMReporter.h
@@ -0,0 +1,39 @@
+#ifndef DMReporter_DEFINED
+#define DMReporter_DEFINED
+
+#include "SkString.h"
+#include "SkTArray.h"
+#include "SkThread.h"
+#include "SkTypes.h"
+
+// Used to report status changes including failures. All public methods are threadsafe.
+
+namespace DM {
+
+class Reporter : SkNoncopyable {
+public:
+ Reporter() : fStarted(0), fFinished(0) {}
+
+ void start() { sk_atomic_inc(&fStarted); }
+ void finish() { sk_atomic_inc(&fFinished); }
+ void fail(SkString name);
+
+ int32_t started() const { return fStarted; }
+ int32_t finished() const { return fFinished; }
+ int32_t failed() const;
+
+ void updateStatusLine() const;
+
+ void getFailures(SkTArray<SkString>*) const;
+
+private:
+ int32_t fStarted, fFinished;
+
+ mutable SkMutex fMutex; // Guards fFailures.
+ SkTArray<SkString> fFailures;
+};
+
+
+} // namespace DM
+
+#endif // DMReporter_DEFINED
diff --git a/dm/DMTask.cpp b/dm/DMTask.cpp
new file mode 100644
index 0000000000..9b463f9003
--- /dev/null
+++ b/dm/DMTask.cpp
@@ -0,0 +1,42 @@
+#include "DMTask.h"
+
+#include "DMTaskRunner.h"
+#include "DMUtil.h"
+#include "SkBitmap.h"
+#include "SkCommandLineFlags.h"
+
+namespace DM {
+
+Task::Task(Reporter* reporter, TaskRunner* taskRunner)
+ : fReporter(reporter), fTaskRunner(taskRunner) {
+ fReporter->start();
+}
+
+Task::Task(const Task& that) : fReporter(that.fReporter), fTaskRunner(that.fTaskRunner) {
+ fReporter->start();
+}
+
+Task::~Task() {}
+
+void Task::run() {
+ if (!this->shouldSkip()) {
+ this->draw();
+ }
+ fReporter->finish();
+ fReporter->updateStatusLine();
+ delete this;
+}
+
+void Task::spawnChild(Task* task) {
+ if (!task->usesGpu()) {
+ fTaskRunner->add(task);
+ } else {
+ SkDEBUGFAIL("Sorry, we can't spawn GPU tasks. :( See comment in TaskRunner::wait().");
+ }
+}
+
+void Task::fail() {
+ fReporter->fail(this->name());
+}
+
+} // namespace DM
diff --git a/dm/DMTask.h b/dm/DMTask.h
new file mode 100644
index 0000000000..744fd6bb9c
--- /dev/null
+++ b/dm/DMTask.h
@@ -0,0 +1,43 @@
+#ifndef DMTask_DEFINED
+#define DMTask_DEFINED
+
+#include "DMReporter.h"
+#include "SkRunnable.h"
+#include "SkThreadPool.h"
+
+// DM will run() these tasks on one of two threadpools, depending on the result
+// of usesGpu(). The subclasses can call fail() to mark this task as failed,
+// or make any number of spawnChild() calls to kick off dependent tasks.
+//
+// Task deletes itself when run.
+
+namespace DM {
+
+class TaskRunner;
+
+class Task : public SkRunnable {
+public:
+ Task(Reporter* reporter, TaskRunner* taskRunner);
+ Task(const Task& that);
+ virtual ~Task();
+
+ void run();
+
+ virtual void draw() = 0;
+ virtual bool usesGpu() const = 0;
+ virtual bool shouldSkip() const = 0;
+ virtual SkString name() const = 0;
+
+protected:
+ void spawnChild(Task* task);
+ void fail();
+
+private:
+ // Both unowned.
+ Reporter* fReporter;
+ TaskRunner* fTaskRunner;
+};
+
+} // namespace DM
+
+#endif // DMTask_DEFINED
diff --git a/dm/DMTaskRunner.cpp b/dm/DMTaskRunner.cpp
new file mode 100644
index 0000000000..22269a4d70
--- /dev/null
+++ b/dm/DMTaskRunner.cpp
@@ -0,0 +1,28 @@
+#include "DMTaskRunner.h"
+#include "DMTask.h"
+
+namespace DM {
+
+TaskRunner::TaskRunner(int cputhreads, int gpuThreads)
+ : fMain(cputhreads)
+ , fGpu(gpuThreads)
+ {}
+
+void TaskRunner::add(Task* task) {
+ if (task->usesGpu()) {
+ fGpu.add(task);
+ } else {
+ fMain.add(task);
+ }
+}
+
+void TaskRunner::wait() {
+ // These wait calls block until the threadpool is done. We don't allow
+ // children to spawn new GPU tasks so we can wait for that first knowing
+ // we'll never try to add to it later. Same can't be said of fMain: fGpu
+ // and fMain can both add tasks to fMain, so we have to wait for that last.
+ fGpu.wait();
+ fMain.wait();
+}
+
+} // namespace DM
diff --git a/dm/DMTaskRunner.h b/dm/DMTaskRunner.h
new file mode 100644
index 0000000000..5d7b320d6c
--- /dev/null
+++ b/dm/DMTaskRunner.h
@@ -0,0 +1,28 @@
+#ifndef DMTaskRunner_DEFINED
+#define DMTaskRunner_DEFINED
+
+#include "SkThreadPool.h"
+#include "SkTypes.h"
+
+// TaskRunner runs Tasks on one of two threadpools depending on the Task's usesGpu() method.
+// This lets us drive the GPU with a small number of threads (e.g. 2 or 4 can be faster than 1)
+// while not swamping it with requests from the full fleet of threads that CPU-bound tasks run on.
+
+namespace DM {
+
+class Task;
+
+class TaskRunner : SkNoncopyable {
+public:
+ TaskRunner(int cputhreads, int gpuThreads);
+
+ void add(Task* task);
+ void wait();
+
+private:
+ SkThreadPool fMain, fGpu;
+};
+
+} // namespace DM
+
+#endif // DMTaskRunner_DEFINED
diff --git a/dm/DMUtil.cpp b/dm/DMUtil.cpp
new file mode 100644
index 0000000000..803c338adc
--- /dev/null
+++ b/dm/DMUtil.cpp
@@ -0,0 +1,23 @@
+#include "DMUtil.h"
+
+namespace DM {
+
+SkString underJoin(const char* a, const char* b) {
+ SkString s;
+ s.appendf("%s_%s", a, b);
+ return s;
+}
+
+SkString png(SkString s) {
+ s.appendf(".png");
+ return s;
+}
+
+bool meetsExpectations(const skiagm::Expectations& expectations,
+ const skiagm::GmResultDigest& digest) {
+ return expectations.ignoreFailure()
+ || expectations.empty()
+ || expectations.match(digest);
+}
+
+} // namespace DM
diff --git a/dm/DMUtil.h b/dm/DMUtil.h
new file mode 100644
index 0000000000..e808ca0f8f
--- /dev/null
+++ b/dm/DMUtil.h
@@ -0,0 +1,23 @@
+#ifndef DMUtil_DEFINED
+#define DMUtil_DEFINED
+
+#include "SkString.h"
+#include "gm_expectations.h"
+
+// Small free functions used in more than one place in DM.
+
+namespace DM {
+
+// underJoin("a", "b") -> "a_b"
+SkString underJoin(const char* a, const char* b);
+
+// png("a") -> "a.png"
+SkString png(SkString s);
+
+// Roughly, expectations.match(digest), but only does it if we're not ignoring the result.
+bool meetsExpectations(const skiagm::Expectations& expectations,
+ const skiagm::GmResultDigest& digest);
+
+} // namespace DM
+
+#endif // DMUtil_DEFINED
diff --git a/dm/README b/dm/README
new file mode 100644
index 0000000000..bce9a7e768
--- /dev/null
+++ b/dm/README
@@ -0,0 +1,37 @@
+DM is like GM, but multithreaded. It doesn't do everything GM does yet.
+
+Current approximate list of missing features:
+ --mismatchPath
+ --missingExpectationsPath
+ --writePath
+ --writePicturePath
+
+ --deferred / --pipe
+ --rtree
+ --serialize
+ --tiledGrid
+
+
+DM's design is based around Tasks and a TaskRunner.
+
+A Task represents an independent unit of work that might fail. We make a task
+for each GM/configuration pair we want to run. Tasks can kick off new tasks
+themselves. For example, a CpuTask can kick off a ReplayTask to make sure
+recording and playing back an SkPicture gives the same result as direct
+rendering.
+
+The TaskRunner runs all tasks on one of two threadpools, whose sizes are
+configurable by --cpuThreads and --gpuThreads. Ideally we'd run these on a
+single threadpool but it can swamp the GPU if we shove too much work into it at
+once. --cpuThreads defaults to the number of cores on the machine.
+--gpuThreads defaults to 1, but you may find 2 or 4 runs a little faster.
+
+So the main flow of DM is:
+
+ for each GM:
+ for each configuration:
+ kick off a new task
+ < tasks run, maybe fail, and maybe kick off new tasks >
+ wait for all tasks to finish
+ report failures
+