dm is like gm, but faster and with fewer features.

This is sort of the near-minimal proof-of-concept skeleton.

  - It can run existing GMs.
  - It supports most configs (just not PDF).
  - --replay is the only "fancy" feature it currently supports

Hopefully you will be disturbed by its speed.

BUG=
R=epoger@google.com

Review URL: https://codereview.chromium.org/22839016

git-svn-id: http://skia.googlecode.com/svn/trunk@11802 2bbb7eff-a529-9590-31e7-b0007b416f81
diff --git a/dm/DM.cpp b/dm/DM.cpp
new file mode 100644
index 0000000..d149b48
--- /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 0000000..f4f742c
--- /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 0000000..265a58c
--- /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 0000000..5534ba4
--- /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 0000000..998ed7b
--- /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 0000000..9205cb9
--- /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 0000000..87c530b
--- /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 0000000..bc94f73
--- /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 0000000..0ed9351
--- /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 0000000..7a0c20e
--- /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 0000000..4f4ad43
--- /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 0000000..9b463f9
--- /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 0000000..744fd6b
--- /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 0000000..22269a4
--- /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 0000000..5d7b320
--- /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 0000000..803c338
--- /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 0000000..e808ca0
--- /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 0000000..bce9a7e
--- /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
+