Example #1
0
static int guess_loops(double overhead, Benchmark* bench, SkCanvas* canvas) {
    WallTimer timer;

    // Measure timer overhead and bench time together.
    do {
        timer.start();
        bench->draw(1, canvas);
        safe_flush(canvas);
        timer.end();
    } while (timer.fWall < overhead);  // Shouldn't normally happen.

    // Later we'll just start and stop the timer once, but loop N times.
    // We'll pick N to make timer overhead negligible:
    //
    //           Timer Overhead
    //  -------------------------------  < FLAGS_overheadGoal
    //  Timer Overhead + N * Bench Time
    //
    // where timer.fWall ≈ Timer Overhead + Bench Time.
    //
    // Doing some math, we get:
    //
    //  (Timer Overhead / FLAGS_overheadGoal) - Timer Overhead
    //  -----------------------------------------------------  < N
    //           (timer.fWall - Timer Overhead)
    //
    // Luckily, this also works well in practice. :)
    const double numer = overhead / FLAGS_overheadGoal - overhead;
    const double denom = timer.fWall - overhead;
    return (int)ceil(numer / denom);
}
Example #2
0
File: DM.cpp Project: tiftof/skia
static void run_test(skiatest::Test* test) {
    struct : public skiatest::Reporter {
        void reportFailed(const skiatest::Failure& failure) override {
            fail(failure.toString());
            JsonWriter::AddTestFailure(failure);
        }
        bool allowExtendedTest() const override {
            return FLAGS_pathOpsExtended;
        }
        bool verbose() const override { return FLAGS_veryVerbose; }
    } reporter;

    SkString note;
    SkString whyBlacklisted = is_blacklisted("_", "tests", "_", test->name);
    if (!whyBlacklisted.isEmpty()) {
        note.appendf(" (--blacklist %s)", whyBlacklisted.c_str());
    }

    WallTimer timer;
    timer.start();
    if (!FLAGS_dryRun && whyBlacklisted.isEmpty()) {
        start("unit", "test", "", test->name);
        GrContextFactory factory;
        if (FLAGS_pre_log) {
            SkDebugf("\nRunning test %s", test->name);
        }
        test->proc(&reporter, &factory);
    }
    timer.end();
    done(timer.fWall, "unit", "test", "", test->name, note, "");
}
Example #3
0
static double estimate_timer_overhead() {
    double overhead = 0;
    WallTimer timer;
    for (int i = 0; i < FLAGS_overheadLoops; i++) {
        timer.start();
        timer.end();
        overhead += timer.fWall;
    }
    return overhead / FLAGS_overheadLoops;
}
Example #4
0
int tool_main(int argc, char** argv) {
    SkCommandLineFlags::Parse(argc, argv);
    SkAutoGraphics autoGraphics;

    if (FLAGS_bbh.count() > 1) {
        SkDebugf("Multiple bbh arguments supplied.\n");
        return 1;
    }

    SkAutoTDelete<SkBBHFactory> bbhFactory(parse_FLAGS_bbh());

    // Each run will use this timer overhead estimate to guess how many times it should run.
    static const int kOverheadLoops = 10000000;
    WallTimer timer;
    double overheadEstimate = 0.0;
    const double scale = timescale();
    for (int i = 0; i < kOverheadLoops; i++) {
        timer.start();
        timer.end();
        overheadEstimate += timer.fWall * scale;
    }
    overheadEstimate /= kOverheadLoops;

    SkOSFile::Iter it(FLAGS_skps[0], ".skp");
    SkString filename;
    bool failed = false;
    while (it.next(&filename)) {
        if (SkCommandLineFlags::ShouldSkip(FLAGS_match, filename.c_str())) {
            continue;
        }

        const SkString path = SkOSPath::Join(FLAGS_skps[0], filename.c_str());

        SkAutoTUnref<SkStream> stream(SkStream::NewFromFile(path.c_str()));
        if (!stream) {
            SkDebugf("Could not read %s.\n", path.c_str());
            failed = true;
            continue;
        }
        SkAutoTUnref<SkPicture> src(
            SkPicture::CreateFromStream(stream, sk_tools::LazyDecodeBitmap));
        if (!src) {
            SkDebugf("Could not read %s as an SkPicture.\n", path.c_str());
            failed = true;
            continue;
        }
        bench_record(*src, overheadEstimate, filename.c_str(), bbhFactory.get());
    }
    return failed ? 1 : 0;
}
Example #5
0
static void bench_record(const SkPicture& src,
                         const double timerOverhead,
                         const char* name,
                         SkBBHFactory* bbhFactory) {
    // Rerecord once to warm up any caches.  Otherwise the first sample can be very noisy.
    rerecord(src, bbhFactory);

    // Rerecord once to see how many times we should loop to make timer overhead insignificant.
    WallTimer timer;
    const double scale = timescale();
    do {
        timer.start();
        rerecord(src, bbhFactory);
        timer.end();
    } while (timer.fWall * scale < timerOverhead);  // Loop just in case something bizarre happens.

    // We want (timer overhead / measurement) to be less than FLAGS_overheadGoal.
    // So in each sample, we'll loop enough times to have made that true for our first measurement.
    const int loops = (int)ceil(timerOverhead / timer.fWall / FLAGS_overheadGoal);

    SkAutoTMalloc<double> samples(FLAGS_samples);
    for (int i = 0; i < FLAGS_samples; i++) {
        timer.start();
        for (int j = 0; j < loops; j++) {
            rerecord(src, bbhFactory);
        }
        timer.end();
        samples[i] = timer.fWall * scale / loops;
    }

    Stats stats(samples.get(), FLAGS_samples);
    if (FLAGS_verbose == 0) {
        printf("%g\t%s\n", stats.min, name);
    } else if (FLAGS_verbose == 1) {
        // Get a rough idea of how noisy the measurements were.
        const double noisePercent = 100 * sqrt(stats.var) / stats.mean;
        printf("%g\t%g\t%g\t±%.0f%%\t%s\n", stats.min, stats.mean, stats.max, noisePercent, name);
    } else if (FLAGS_verbose == 2) {
        printf("%s", name);
        for (int i = 0; i < FLAGS_samples; i++) {
            printf("\t%g", samples[i]);
        }
        printf("\n");
    }
}
Example #6
0
static void run_test(skiatest::Test* test) {
    struct : public skiatest::Reporter {
        void reportFailed(const skiatest::Failure& failure) override {
            fail(failure.toString());
            JsonWriter::AddTestFailure(failure);
        }
        bool allowExtendedTest() const override {
            return FLAGS_pathOpsExtended;
        }
        bool verbose() const override { return FLAGS_veryVerbose; }
    } reporter;
    WallTimer timer;
    timer.start();
    if (!FLAGS_dryRun) {
        start("unit", "test", "", test->name);
        GrContextFactory factory;
        test->proc(&reporter, &factory);
    }
    timer.end();
    done(timer.fWall, "unit", "test", "", test->name, "", "");
}
Example #7
0
static void bench(SkPMColor* scratch, SkPicture& src, const char* name) {
    SkAutoTUnref<SkPicture> picture(rerecord_with_tilegrid(src));
    SkAutoTDelete<EXPERIMENTAL::SkPlayback> record(rerecord_with_skr(src));

    SkAutoTDelete<SkCanvas> canvas(SkCanvas::NewRasterDirectN32(src.width(),
                                                                src.height(),
                                                                scratch,
                                                                src.width() * sizeof(SkPMColor)));
    canvas->clipRect(SkRect::MakeWH(SkIntToScalar(FLAGS_tile), SkIntToScalar(FLAGS_tile)));

    // Draw once to warm any caches.  The first sample otherwise can be very noisy.
    draw(*record, *picture, canvas.get());

    WallTimer timer;
    const double scale = timescale();
    SkAutoTMalloc<double> samples(FLAGS_samples);
    for (int i = 0; i < FLAGS_samples; i++) {
        // We assume timer overhead (typically, ~30ns) is insignificant
        // compared to draw runtime (at least ~100us, usually several ms).
        timer.start();
        draw(*record, *picture, canvas.get());
        timer.end();
        samples[i] = timer.fWall * scale;
    }

    Stats stats(samples.get(), FLAGS_samples);
    if (FLAGS_verbose == 0) {
        printf("%g\t%s\n", stats.min, name);
    } else if (FLAGS_verbose == 1) {
        // Get a rough idea of how noisy the measurements were.
        const double noisePercent = 100 * sqrt(stats.var) / stats.mean;
        printf("%g\t%g\t%g\t±%.0f%%\t%s\n", stats.min, stats.mean, stats.max, noisePercent, name);
    } else if (FLAGS_verbose == 2) {
        printf("%s", name);
        for (int i = 0; i < FLAGS_samples; i++) {
            printf("\t%g", samples[i]);
        }
        printf("\n");
    }
}
Example #8
0
int main(int argc, char** argv) {
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <input_vcf_file> <output_vcf_file>\n";
        return 1;
    }

    boost::ptr_vector<IBenchmark> tests;
//    tests.push_back(new TestInOut);
    tests.push_back(new TestInOnly);

    for (auto iter = tests.begin(); iter != tests.end(); ++iter) {
        StreamHandler streams;
        auto in = streams.openForReading(argv[1]);
        auto reader = openStream<Vcf::Entry>(*in);
        ostream* out = streams.get<ostream>(argv[2]);

        WallTimer timer;
        size_t count = iter->run(reader, *out);
        std::cout << iter->name() << ": " << count << " entries in "
            << timer.elapsed() << "\n";
    }

    return 0;
}
Example #9
0
File: DM.cpp Project: tiftof/skia
    static void Run(Task* task) {
        SkString name = task->src->name();

        // We'll skip drawing this Src/Sink pair if:
        //   - the Src vetoes the Sink;
        //   - this Src / Sink combination is on the blacklist;
        //   - it's a dry run.
        SkString note(task->src->veto(task->sink->flags()) ? " (veto)" : "");
        SkString whyBlacklisted = is_blacklisted(task->sink.tag, task->src.tag,
                                                 task->src.options, name.c_str());
        if (!whyBlacklisted.isEmpty()) {
            note.appendf(" (--blacklist %s)", whyBlacklisted.c_str());
        }

        SkString log;
        WallTimer timer;
        timer.start();
        if (!FLAGS_dryRun && note.isEmpty()) {
            SkBitmap bitmap;
            SkDynamicMemoryWStream stream;
            if (FLAGS_pre_log) {
                SkDebugf("\nRunning %s->%s", name.c_str(), task->sink.tag);
            }
            start(task->sink.tag, task->src.tag, task->src.options, name.c_str());
            Error err = task->sink->draw(*task->src, &bitmap, &stream, &log);
            if (!err.isEmpty()) {
                timer.end();
                if (err.isFatal()) {
                    fail(SkStringPrintf("%s %s %s %s: %s",
                                        task->sink.tag,
                                        task->src.tag,
                                        task->src.options,
                                        name.c_str(),
                                        err.c_str()));
                } else {
                    note.appendf(" (skipped: %s)", err.c_str());
                }
                done(timer.fWall, task->sink.tag, task->src.tag, task->src.options,
                     name, note, log);
                return;
            }
            SkAutoTDelete<SkStreamAsset> data(stream.detachAsStream());

            SkString md5;
            if (!FLAGS_writePath.isEmpty() || !FLAGS_readPath.isEmpty()) {
                SkMD5 hash;
                if (data->getLength()) {
                    hash.writeStream(data, data->getLength());
                    data->rewind();
                } else {
                    // If we're BGRA (Linux, Windows), swizzle over to RGBA (Mac, Android).
                    // This helps eliminate multiple 0-pixel-diff hashes on gold.skia.org.
                    // (Android's general slow speed breaks the tie arbitrarily in RGBA's favor.)
                    // We might consider promoting 565 to RGBA too.
                    if (bitmap.colorType() == kBGRA_8888_SkColorType) {
                        SkBitmap swizzle;
                        SkAssertResult(bitmap.copyTo(&swizzle, kRGBA_8888_SkColorType));
                        hash.write(swizzle.getPixels(), swizzle.getSize());
                    } else {
                        hash.write(bitmap.getPixels(), bitmap.getSize());
                    }
                }
                SkMD5::Digest digest;
                hash.finish(digest);
                for (int i = 0; i < 16; i++) {
                    md5.appendf("%02x", digest.data[i]);
                }
            }

            if (!FLAGS_readPath.isEmpty() &&
                !gGold.contains(Gold(task->sink.tag, task->src.tag,
                                     task->src.options, name, md5))) {
                fail(SkStringPrintf("%s not found for %s %s %s %s in %s",
                                    md5.c_str(),
                                    task->sink.tag,
                                    task->src.tag,
                                    task->src.options,
                                    name.c_str(),
                                    FLAGS_readPath[0]));
            }

            if (!FLAGS_writePath.isEmpty()) {
                const char* ext = task->sink->fileExtension();
                if (data->getLength()) {
                    WriteToDisk(*task, md5, ext, data, data->getLength(), nullptr);
                    SkASSERT(bitmap.drawsNothing());
                } else if (!bitmap.drawsNothing()) {
                    WriteToDisk(*task, md5, ext, nullptr, 0, &bitmap);
                }
            }
        }
        timer.end();
        done(timer.fWall, task->sink.tag, task->src.tag, task->src.options, name, note, log);
    }
Example #10
0
U8 MCTSEngine::runSim(double remaining_time) {
    wtimer.start();
    simulations = 0ULL;
    MCTNode *node = NULL;

    double turn_time = remaining_time / 10.0;

    cerr << "   --- Run simulation for move no. " << (int)root_state.next_move << " ---" << endl;
    cerr << "   --- Turn time: " << turn_time << " sec." << endl;

    //for now never stop searching
    while(!wtimer.out_of_time(turn_time)) {
        simulations++;
        node = root_node;
        sim_state = root_state;

        //selection
        while(node->untried_moves.empty() && !node->child_nodes.empty()) {
            //cur_node is fully expanded and non-terminal
            node = node->selectChildUCT();
            sim_state.makeMove(node->move);
        }

        if(MCTS_used_bytes / 1024 / 1024 < 60) {
            //expansion
            if(!node->untried_moves.empty()) {
                //there are still child nodes to expand
                node = node->addRandomChild(sim_state);
            }
        }

        //simulation
        U8 win_color = sim_state.randomFill();

        //backpropagation
        while(node != NULL) {

            if(win_color == node->color) { //node is win
                node->update(1);
            } else { //node is loss
                node->update(0);
            }

            if(node != root_node) {
                //update siblings with amaf scores
                for(auto sib_iter = node->parent_node->child_nodes.begin(); sib_iter != node->parent_node->child_nodes.end(); sib_iter++) {
                    if(win_color == sim_state.stones[sib_iter->second->move]) {
                        sib_iter->second->update(1);
                    } else {
                        sib_iter->second->update(0);
                    }

                }
            }
            node = node->parent_node;
        }

    }


    U32 most_visits = 0;
    U8 best_move = root_state.possible_moves[0];
    U32 wins = 0;

    cerr << "   --- " << simulations << " simulations run." << endl;    
    double score;

    for(auto &p : root_node->child_nodes) {
        if(p.second->visits > most_visits) {
            most_visits = p.second->visits;
            best_move = p.second->move;
            wins = p.second->wins;
        }
        score = (double) p.second->wins / (double) p.second->visits * 100;
        cerr << "   # Move: " << (int) p.second->move << " Score: " << score << "%" << endl;
    }

    cerr << "---> # Best Move: " << (int) best_move << "   visits: " << most_visits << "   wins: " << wins << " == " << (double)wins/(double)most_visits*100.0 << " %" << endl;

    return best_move;
}
Example #11
0
    static void Run(Task* task) {
        SkString name = task->src->name();
        SkString note;
        SkString whyBlacklisted = is_blacklisted(task->sink.tag, task->src.tag,
                                                 task->src.options, name.c_str());
        if (!whyBlacklisted.isEmpty()) {
            note.appendf(" (--blacklist %s)", whyBlacklisted.c_str());
        }
        SkString log;
        WallTimer timer;
        timer.start();
        if (!FLAGS_dryRun && whyBlacklisted.isEmpty()) {
            SkBitmap bitmap;
            SkDynamicMemoryWStream stream;
            start(task->sink.tag, task->src.tag, task->src.options, name.c_str());
            Error err = task->sink->draw(*task->src, &bitmap, &stream, &log);
            if (!err.isEmpty()) {
                timer.end();
                if (err.isFatal()) {
                    fail(SkStringPrintf("%s %s %s %s: %s",
                                        task->sink.tag,
                                        task->src.tag,
                                        task->src.options,
                                        name.c_str(),
                                        err.c_str()));
                } else {
                    note.appendf(" (skipped: %s)", err.c_str());
                }
                done(timer.fWall, task->sink.tag, task->src.tag, task->src.options,
                     name, note, log);
                return;
            }
            SkAutoTDelete<SkStreamAsset> data(stream.detachAsStream());

            SkString md5;
            if (!FLAGS_writePath.isEmpty() || !FLAGS_readPath.isEmpty()) {
                SkMD5 hash;
                if (data->getLength()) {
                    hash.writeStream(data, data->getLength());
                    data->rewind();
                } else {
                    hash.write(bitmap.getPixels(), bitmap.getSize());
                }
                SkMD5::Digest digest;
                hash.finish(digest);
                for (int i = 0; i < 16; i++) {
                    md5.appendf("%02x", digest.data[i]);
                }
            }

            if (!FLAGS_readPath.isEmpty() &&
                !gGold.contains(Gold(task->sink.tag, task->src.tag,
                                     task->src.options, name, md5))) {
                fail(SkStringPrintf("%s not found for %s %s %s %s in %s",
                                    md5.c_str(),
                                    task->sink.tag,
                                    task->src.tag,
                                    task->src.options,
                                    name.c_str(),
                                    FLAGS_readPath[0]));
            }

            if (!FLAGS_writePath.isEmpty()) {
                const char* ext = task->sink->fileExtension();
                if (data->getLength()) {
                    WriteToDisk(*task, md5, ext, data, data->getLength(), NULL);
                    SkASSERT(bitmap.drawsNothing());
                } else if (!bitmap.drawsNothing()) {
                    WriteToDisk(*task, md5, ext, NULL, 0, &bitmap);
                }
            }
        }
        timer.end();
        done(timer.fWall, task->sink.tag, task->src.tag, task->src.options, name, note, log);
    }
Example #12
0
int tool_main(int argc, char** argv) {
    SetupCrashHandler();
    SkAutoGraphics ag;
    SkCommandLineFlags::Parse(argc, argv);

    const double overhead = estimate_timer_overhead();

    if (FLAGS_verbose) {
        // No header.
    } else if (FLAGS_quiet) {
        SkDebugf("min\tbench\tconfig\n");
    } else {
        SkDebugf("loops\tmin\tmean\tmax\tstddev\tbench\tconfig\n");
    }

    for (const BenchRegistry* r = BenchRegistry::Head(); r != NULL; r = r->next()) {
        SkAutoTDelete<Benchmark> bench(r->factory()(NULL));
        if (SkCommandLineFlags::ShouldSkip(FLAGS_match, bench->getName())) {
            continue;
        }

        SkTDArray<SkSurface*> surfaces;
        SkTDArray<const char*> configs;
        create_surfaces(bench.get(), &surfaces, &configs);

        bench->preDraw();
        for (int j = 0; j < surfaces.count(); j++) {
            SkCanvas* canvas = surfaces[j] ? surfaces[j]->getCanvas() : NULL;
            const char* config = configs[j];

            bench->draw(1, canvas);  // Just paranoid warmup.
            safe_flush(canvas);
            const int loops = guess_loops(overhead, bench.get(), canvas);

            SkAutoTMalloc<double> samples(FLAGS_samples);
            WallTimer timer;
            for (int i = 0; i < FLAGS_samples; i++) {
                timer.start();
                bench->draw(loops, canvas);
                safe_flush(canvas);
                timer.end();
                samples[i] = timer.fWall / loops;
            }

            Stats stats(samples.get(), FLAGS_samples);

            if (FLAGS_verbose) {
                for (int i = 0; i < FLAGS_samples; i++) {
                    SkDebugf("%s  ", humanize(samples[i]).c_str());
                }
                SkDebugf("%s\n", bench->getName());
            } else if (FLAGS_quiet) {
                if (configs.count() == 1) {
                    config = ""; // Only print the config if we run the same bench on more than one.
                }
                SkDebugf("%s\t%s\t%s\n", humanize(stats.min).c_str(), bench->getName(), config);
            } else {
                const double stddev_percent = 100 * sqrt(stats.var) / stats.mean;
                SkDebugf("%d\t%s\t%s\t%s\t%.0f%%\t%s\t%s\n"
                        , loops
                        , humanize(stats.min).c_str()
                        , humanize(stats.mean).c_str()
                        , humanize(stats.max).c_str()
                        , stddev_percent
                        , bench->getName()
                        , config
                        );
            }
        }
        surfaces.deleteAll();
    }

    return 0;
}
Example #13
0
int main(void) {
    GLFWwindow* window;
    glfwSetErrorCallback(error_callback);
    if (!glfwInit()) {
        exit(EXIT_FAILURE);
    }
    
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_SRGB_CAPABLE, GL_TRUE);
    
    window = glfwCreateWindow(kWidth, kHeight, "Simple example", NULL, NULL);
    if (!window) {
        glfwTerminate();
        exit(EXIT_FAILURE);
    }
    glfwMakeContextCurrent(window);
    
    init_skia(kWidth, kHeight);
    
    SkAutoTUnref<SkImage> atlas;
    SkRSXform   xform[kGrid*kGrid+1];
    SkRect      tex[kGrid*kGrid+1];
    WallTimer   timer;
    float       times[32];
    int         currentTime;

    SkAutoTUnref<SkData> imageData(SkData::NewFromFileName("ship.png"));
    atlas.reset(SkImage::NewFromEncoded(imageData));
    if (!atlas) {
        SkDebugf("\nCould not decode file ship.png\n");
        
        cleanup_skia();
        glfwDestroyWindow(window);
        glfwTerminate();
        exit(EXIT_FAILURE);
    }
    
    SkScalar anchorX = atlas->width()*0.5f;
    SkScalar anchorY = atlas->height()*0.5f;
    int currIndex = 0;
    for (int x = 0; x < kGrid; x++) {
        for (int y = 0; y < kGrid; y++) {
            float xPos = (x / (kGrid - 1.0)) * kWidth;
            float yPos = (y / (kGrid - 1.0)) * kWidth;
            
            tex[currIndex] = SkRect::MakeLTRB(0.0f, 0.0f, atlas->width(), atlas->height());
            xform[currIndex] = SkRSXform::MakeFromRadians(2.0f, SK_ScalarPI*0.5f,
                                                          xPos, yPos, anchorX, anchorY);
            currIndex++;
        }
    }
    tex[currIndex] = SkRect::MakeLTRB(0.0f, 0.0f, atlas->width(), atlas->height());
    xform[currIndex] = SkRSXform::MakeFromRadians(2.0f, SK_ScalarPI*0.5f,
                                                  kWidth*0.5f, kHeight*0.5f, anchorX, anchorY);
    
    currentTime = 0;
    
    glfwSwapInterval(1);
    glfwSetKeyCallback(window, key_callback);
    
    // Draw to the surface via its SkCanvas.
    SkCanvas* canvas = sSurface->getCanvas();   // We don't manage this pointer's lifetime.
    SkPaint paint;
    paint.setFilterQuality(kLow_SkFilterQuality);
    paint.setColor(SK_ColorWHITE);
    paint.setTextSize(15.0f);

    while (!glfwWindowShouldClose(window)) {
        const float kCosDiff = 0.99984769515f;
        const float kSinDiff = 0.01745240643f;

        timer.start();
        
        glfwPollEvents();
        
        float meanTime = 0.0f;
        for (int i = 0; i < 32; ++i) {
            meanTime += times[i];
        }
        meanTime /= 32.f;
        char outString[64];
        float fps = 1000.f/meanTime;
        sprintf(outString, "fps: %f ms: %f", fps, meanTime);
        
        for (int i = 0; i < kGrid*kGrid+1; ++i) {
            SkScalar c = xform[i].fSCos;
            SkScalar s = xform[i].fSSin;
        
            SkScalar dx = c*anchorX - s*anchorY;
            SkScalar dy = s*anchorX + c*anchorY;

            xform[i].fSCos = kCosDiff*c - kSinDiff*s;
            xform[i].fSSin = kSinDiff*c + kCosDiff*s;
            
            dx -= xform[i].fSCos*anchorX - xform[i].fSSin*anchorY;
            dy -= xform[i].fSSin*anchorX + xform[i].fSCos*anchorY;
            xform[i].fTx += dx;
            xform[i].fTy += dy;
        }
        
        canvas->clear(SK_ColorBLACK);
        canvas->drawAtlas(atlas, xform, tex, nullptr, kGrid*kGrid+1, SkXfermode::kSrcOver_Mode,
                          nullptr, &paint);
        canvas->drawText(outString, strlen(outString), 100.f, 100.f, paint);
        
        canvas->flush();
        
        timer.end();
        times[currentTime] = (float)(timer.fWall);
        currentTime = (currentTime + 1) & 0x1f;
        
        glfwSwapBuffers(window);
    }
  
    cleanup_skia();
    
    glfwDestroyWindow(window);
    glfwTerminate();
    exit(EXIT_SUCCESS);
}