2018-01-09 17:34:11 +00:00
|
|
|
/*
|
|
|
|
* Copyright 2018 Google Inc.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
|
|
* found in the LICENSE file.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "SkCanvas.h"
|
|
|
|
#include "SkPathEffect.h"
|
|
|
|
#include "SkMaskFilter.h"
|
|
|
|
#include "SkData.h"
|
|
|
|
#include "SkDescriptor.h"
|
|
|
|
#include "SkGraphics.h"
|
|
|
|
#include "SkSemaphore.h"
|
|
|
|
#include "SkPictureRecorder.h"
|
|
|
|
#include "SkSerialProcs.h"
|
|
|
|
#include "SkSurface.h"
|
|
|
|
#include "SkTypeface.h"
|
|
|
|
#include "SkWriteBuffer.h"
|
|
|
|
|
2018-01-09 17:34:11 +00:00
|
|
|
#include <chrono>
|
2018-01-09 17:34:11 +00:00
|
|
|
#include <ctype.h>
|
|
|
|
#include <err.h>
|
|
|
|
#include <memory>
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <thread>
|
|
|
|
#include <iostream>
|
|
|
|
#include <unordered_map>
|
|
|
|
|
|
|
|
#include <sys/types.h>
|
|
|
|
#include <sys/wait.h>
|
|
|
|
#include <unistd.h>
|
|
|
|
#include <sys/mman.h>
|
|
|
|
#include "SkTypeface_remote.h"
|
2018-01-26 21:47:54 +00:00
|
|
|
#include "SkRemoteGlyphCache.h"
|
|
|
|
#include "SkMakeUnique.h"
|
2018-01-09 17:34:11 +00:00
|
|
|
|
|
|
|
static const size_t kPageSize = 4096;
|
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
static bool gUseGpu = true;
|
|
|
|
static bool gPurgeFontCaches = true;
|
|
|
|
static bool gUseProcess = true;
|
|
|
|
|
2018-02-02 22:33:26 +00:00
|
|
|
enum class OpCode : int32_t {
|
|
|
|
kFontMetrics = 0,
|
|
|
|
kGlyphMetrics = 1,
|
|
|
|
kGlyphImage = 2,
|
|
|
|
kGlyphPath = 3,
|
|
|
|
kGlyphMetricsAndImage = 4,
|
|
|
|
};
|
|
|
|
|
2018-01-09 17:34:11 +00:00
|
|
|
class Op {
|
|
|
|
public:
|
2018-02-02 22:33:26 +00:00
|
|
|
Op(OpCode opCode, SkFontID typefaceId, const SkScalerContextRec& rec)
|
|
|
|
: opCode{opCode}
|
|
|
|
, typefaceId{typefaceId}
|
|
|
|
, descriptor{rec} { }
|
|
|
|
const OpCode opCode;
|
|
|
|
const SkFontID typefaceId;
|
|
|
|
const SkScalerContextRecDescriptor descriptor;
|
2018-01-09 17:34:11 +00:00
|
|
|
union {
|
|
|
|
// op 0
|
|
|
|
SkPaint::FontMetrics fontMetrics;
|
|
|
|
// op 1 and 2
|
|
|
|
SkGlyph glyph;
|
|
|
|
// op 3
|
|
|
|
struct {
|
|
|
|
SkGlyphID glyphId;
|
|
|
|
size_t pathSize;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2018-01-26 21:47:54 +00:00
|
|
|
class RemoteScalerContextFIFO : public SkRemoteScalerContext {
|
2018-01-09 17:34:11 +00:00
|
|
|
public:
|
2018-01-26 21:47:54 +00:00
|
|
|
explicit RemoteScalerContextFIFO(int readFd, int writeFd)
|
2018-01-09 17:34:11 +00:00
|
|
|
: fReadFd{readFd}
|
|
|
|
, fWriteFd{writeFd} { }
|
|
|
|
void generateFontMetrics(const SkTypefaceProxy& tf,
|
|
|
|
const SkScalerContextRec& rec,
|
|
|
|
SkPaint::FontMetrics* metrics) override {
|
2018-02-02 22:33:26 +00:00
|
|
|
Op* op = this->createOp(OpCode::kFontMetrics, tf, rec);
|
2018-01-09 17:34:11 +00:00
|
|
|
write(fWriteFd, fBuffer, sizeof(*op));
|
2018-01-09 17:34:11 +00:00
|
|
|
read(fReadFd, fBuffer, sizeof(fBuffer));
|
2018-01-09 17:34:11 +00:00
|
|
|
memcpy(metrics, &op->fontMetrics, sizeof(op->fontMetrics));
|
|
|
|
op->~Op();
|
|
|
|
}
|
|
|
|
|
|
|
|
void generateMetrics(const SkTypefaceProxy& tf,
|
|
|
|
const SkScalerContextRec& rec,
|
|
|
|
SkGlyph* glyph) override {
|
2018-02-02 22:33:26 +00:00
|
|
|
Op* op = this->createOp(OpCode::kGlyphMetrics, tf, rec);
|
2018-01-09 17:34:11 +00:00
|
|
|
memcpy(&op->glyph, glyph, sizeof(*glyph));
|
|
|
|
write(fWriteFd, fBuffer, sizeof(*op));
|
|
|
|
read(fReadFd, fBuffer, sizeof(fBuffer));
|
|
|
|
memcpy(glyph, &op->glyph, sizeof(op->glyph));
|
|
|
|
op->~Op();
|
|
|
|
}
|
|
|
|
|
|
|
|
void generateImage(const SkTypefaceProxy& tf,
|
|
|
|
const SkScalerContextRec& rec,
|
|
|
|
const SkGlyph& glyph) override {
|
2018-02-02 22:33:26 +00:00
|
|
|
SK_ABORT("generateImage should not be called.");
|
|
|
|
Op* op = this->createOp(OpCode::kGlyphImage, tf, rec);
|
2018-01-09 17:34:11 +00:00
|
|
|
memcpy(&op->glyph, &glyph, sizeof(glyph));
|
|
|
|
write(fWriteFd, fBuffer, sizeof(*op));
|
|
|
|
read(fReadFd, fBuffer, sizeof(fBuffer));
|
|
|
|
memcpy(glyph.fImage, fBuffer + sizeof(Op), glyph.rowBytes() * glyph.fHeight);
|
|
|
|
op->~Op();
|
|
|
|
}
|
|
|
|
|
2018-02-02 22:33:26 +00:00
|
|
|
void generateMetricsAndImage(const SkTypefaceProxy& tf,
|
|
|
|
const SkScalerContextRec& rec,
|
|
|
|
SkArenaAlloc* alloc,
|
|
|
|
SkGlyph* glyph) override {
|
|
|
|
Op* op = this->createOp(OpCode::kGlyphMetricsAndImage, tf, rec);
|
|
|
|
memcpy(&op->glyph, glyph, sizeof(op->glyph));
|
|
|
|
write(fWriteFd, fBuffer, sizeof(*op));
|
|
|
|
read(fReadFd, fBuffer, sizeof(fBuffer));
|
|
|
|
memcpy(glyph, &op->glyph, sizeof(*glyph));
|
|
|
|
glyph->allocImage(alloc);
|
|
|
|
memcpy(glyph->fImage, fBuffer + sizeof(Op), glyph->rowBytes() * glyph->fHeight);
|
|
|
|
op->~Op();
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:34:11 +00:00
|
|
|
void generatePath(const SkTypefaceProxy& tf,
|
|
|
|
const SkScalerContextRec& rec,
|
|
|
|
SkGlyphID glyph, SkPath* path) override {
|
2018-02-02 22:33:26 +00:00
|
|
|
Op* op = this->createOp(OpCode::kGlyphPath, tf, rec);
|
2018-01-09 17:34:11 +00:00
|
|
|
op->glyphId = glyph;
|
|
|
|
write(fWriteFd, fBuffer, sizeof(*op));
|
|
|
|
read(fReadFd, fBuffer, sizeof(fBuffer));
|
|
|
|
path->readFromMemory(fBuffer + sizeof(Op), op->pathSize);
|
|
|
|
op->~Op();
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2018-02-02 22:33:26 +00:00
|
|
|
Op* createOp(OpCode opCode, const SkTypefaceProxy& tf,
|
2018-01-09 17:34:11 +00:00
|
|
|
const SkScalerContextRec& rec) {
|
2018-02-02 22:33:26 +00:00
|
|
|
Op* op = new (fBuffer) Op(opCode, tf.fontID(), rec);
|
2018-01-09 17:34:11 +00:00
|
|
|
|
|
|
|
return op;
|
|
|
|
}
|
|
|
|
|
|
|
|
const int fReadFd,
|
2018-02-02 17:54:55 +00:00
|
|
|
fWriteFd;
|
2018-01-09 17:34:11 +00:00
|
|
|
uint8_t fBuffer[1024 * kPageSize];
|
|
|
|
};
|
|
|
|
|
2018-01-09 17:34:11 +00:00
|
|
|
static void final_draw(std::string outFilename,
|
|
|
|
SkDeserialProcs* procs,
|
|
|
|
uint8_t* picData,
|
|
|
|
size_t picSize) {
|
|
|
|
|
|
|
|
auto pic = SkPicture::MakeFromData(picData, picSize, procs);
|
|
|
|
|
|
|
|
auto cullRect = pic->cullRect();
|
|
|
|
auto r = cullRect.round();
|
|
|
|
|
2018-01-09 17:34:11 +00:00
|
|
|
auto s = SkSurface::MakeRasterN32Premul(r.width(), r.height());
|
2018-01-09 17:34:11 +00:00
|
|
|
auto c = s->getCanvas();
|
2018-01-09 17:34:11 +00:00
|
|
|
auto picUnderTest = SkPicture::MakeFromData(picData, picSize, procs);
|
|
|
|
|
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
std::chrono::duration<double> total_seconds{0.0};
|
|
|
|
for (int i = 0; i < 20; i++) {
|
|
|
|
if (gPurgeFontCaches) {
|
|
|
|
SkGraphics::PurgeFontCache();
|
|
|
|
}
|
|
|
|
auto start = std::chrono::high_resolution_clock::now();
|
2018-01-09 17:34:11 +00:00
|
|
|
c->drawPicture(picUnderTest);
|
2018-02-02 17:54:55 +00:00
|
|
|
auto end = std::chrono::high_resolution_clock::now();
|
|
|
|
std::chrono::duration<double> elapsed_seconds = end-start;
|
|
|
|
total_seconds += elapsed_seconds;
|
2018-01-09 17:34:11 +00:00
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
}
|
2018-01-09 17:34:11 +00:00
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
std::cout << "useProcess: " << gUseProcess
|
|
|
|
<< " useGPU: " << gUseGpu
|
|
|
|
<< " purgeCache: " << gPurgeFontCaches << std::endl;
|
|
|
|
std::cerr << "elapsed time: " << total_seconds.count() << "s\n";
|
2018-01-09 17:34:11 +00:00
|
|
|
|
|
|
|
auto i = s->makeImageSnapshot();
|
|
|
|
auto data = i->encodeToData();
|
|
|
|
SkFILEWStream f(outFilename.c_str());
|
|
|
|
f.write(data->data(), data->size());
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:34:11 +00:00
|
|
|
static void gpu(int readFd, int writeFd) {
|
|
|
|
|
|
|
|
size_t picSize = 0;
|
2018-02-02 17:54:55 +00:00
|
|
|
ssize_t r = read(readFd, &picSize, sizeof(picSize));
|
|
|
|
if (r > 0) {
|
|
|
|
|
|
|
|
static constexpr size_t kBufferSize = 10 * 1024 * kPageSize;
|
|
|
|
std::unique_ptr<uint8_t[]> picBuffer{new uint8_t[kBufferSize]};
|
|
|
|
|
|
|
|
size_t readSoFar = 0;
|
|
|
|
while (readSoFar < picSize) {
|
|
|
|
ssize_t readSize;
|
|
|
|
if ((readSize = read(readFd, &picBuffer[readSoFar], kBufferSize - readSoFar)) <= 0) {
|
|
|
|
if (readSize == 0) return;
|
|
|
|
err(1, "gpu pic read error %d", errno);
|
|
|
|
}
|
|
|
|
readSoFar += readSize;
|
2018-01-09 17:34:11 +00:00
|
|
|
}
|
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
SkRemoteGlyphCacheGPU rc{
|
|
|
|
skstd::make_unique<RemoteScalerContextFIFO>(readFd, writeFd)
|
|
|
|
};
|
2018-01-09 17:34:11 +00:00
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
SkDeserialProcs procs;
|
|
|
|
rc.prepareDeserializeProcs(&procs);
|
2018-01-09 17:34:11 +00:00
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
final_draw("test.png", &procs, picBuffer.get(), picSize);
|
|
|
|
|
|
|
|
}
|
2018-01-09 17:34:11 +00:00
|
|
|
|
|
|
|
close(writeFd);
|
|
|
|
close(readFd);
|
|
|
|
}
|
|
|
|
|
2018-01-26 21:47:54 +00:00
|
|
|
static int renderer(
|
|
|
|
const std::string& skpName, int readFd, int writeFd)
|
|
|
|
{
|
2018-01-09 17:34:11 +00:00
|
|
|
std::string prefix{"skps/"};
|
|
|
|
std::string fileName{prefix + skpName + ".skp"};
|
|
|
|
|
|
|
|
auto skp = SkData::MakeFromFileName(fileName.c_str());
|
2018-02-02 17:54:55 +00:00
|
|
|
std::cout << "skp stream is " << skp->size() << " bytes long " << std::endl;
|
2018-01-09 17:34:11 +00:00
|
|
|
|
2018-01-26 21:47:54 +00:00
|
|
|
SkRemoteGlyphCacheRenderer rc;
|
2018-01-09 17:34:11 +00:00
|
|
|
SkSerialProcs procs;
|
2018-02-02 17:54:55 +00:00
|
|
|
sk_sp<SkData> stream;
|
|
|
|
if (gUseGpu) {
|
|
|
|
auto pic = SkPicture::MakeFromData(skp.get());
|
2018-01-26 21:47:54 +00:00
|
|
|
rc.prepareSerializeProcs(&procs);
|
2018-02-02 17:54:55 +00:00
|
|
|
stream = pic->serialize(&procs);
|
|
|
|
} else {
|
|
|
|
stream = skp;
|
2018-01-09 17:34:11 +00:00
|
|
|
}
|
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
std::cout << "stream is " << stream->size() << " bytes long" << std::endl;
|
2018-01-09 17:34:11 +00:00
|
|
|
|
|
|
|
size_t picSize = stream->size();
|
|
|
|
uint8_t* picBuffer = (uint8_t*) stream->data();
|
2018-01-09 17:34:11 +00:00
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
if (!gUseGpu) {
|
2018-01-09 17:34:11 +00:00
|
|
|
final_draw("test-direct.png", nullptr, picBuffer, picSize);
|
|
|
|
close(writeFd);
|
|
|
|
close(readFd);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:34:11 +00:00
|
|
|
write(writeFd, &picSize, sizeof(picSize));
|
|
|
|
|
|
|
|
size_t writeSoFar = 0;
|
|
|
|
while (writeSoFar < picSize) {
|
|
|
|
ssize_t writeSize = write(writeFd, &picBuffer[writeSoFar], picSize - writeSoFar);
|
|
|
|
if (writeSize <= 0) {
|
|
|
|
if (writeSize == 0) {
|
2018-02-02 17:54:55 +00:00
|
|
|
std::cout << "Exit" << std::endl;
|
2018-01-09 17:34:11 +00:00
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
perror("Can't write picture from render to GPU ");
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
writeSoFar += writeSize;
|
|
|
|
}
|
2018-02-02 17:54:55 +00:00
|
|
|
std::cout << "Waiting for scaler context ops." << std::endl;
|
2018-01-09 17:34:11 +00:00
|
|
|
|
|
|
|
static constexpr size_t kBufferSize = 1024 * kPageSize;
|
|
|
|
std::unique_ptr<uint8_t[]> glyphBuffer{new uint8_t[kBufferSize]};
|
|
|
|
|
|
|
|
Op* op = (Op*)glyphBuffer.get();
|
|
|
|
while (true) {
|
|
|
|
ssize_t size = read(readFd, glyphBuffer.get(), sizeof(*op));
|
2018-02-02 17:54:55 +00:00
|
|
|
if (size <= 0) { std::cout << "Exit op loop" << std::endl; break;}
|
2018-01-09 17:34:11 +00:00
|
|
|
size_t writeSize = sizeof(*op);
|
|
|
|
|
2018-02-02 22:33:26 +00:00
|
|
|
auto sc = rc.generateScalerContext(op->descriptor, op->typefaceId);
|
|
|
|
switch (op->opCode) {
|
|
|
|
case OpCode::kFontMetrics : {
|
2018-01-09 17:34:11 +00:00
|
|
|
sc->getFontMetrics(&op->fontMetrics);
|
|
|
|
break;
|
|
|
|
}
|
2018-02-02 22:33:26 +00:00
|
|
|
case OpCode::kGlyphMetrics : {
|
2018-01-09 17:34:11 +00:00
|
|
|
sc->getMetrics(&op->glyph);
|
|
|
|
break;
|
|
|
|
}
|
2018-02-02 22:33:26 +00:00
|
|
|
case OpCode::kGlyphImage : {
|
2018-01-09 17:34:11 +00:00
|
|
|
// TODO: check for buffer overflow.
|
|
|
|
op->glyph.fImage = &glyphBuffer[sizeof(Op)];
|
|
|
|
sc->getImage(op->glyph);
|
|
|
|
writeSize += op->glyph.rowBytes() * op->glyph.fHeight;
|
|
|
|
break;
|
|
|
|
}
|
2018-02-02 22:33:26 +00:00
|
|
|
case OpCode::kGlyphPath : {
|
2018-01-09 17:34:11 +00:00
|
|
|
// TODO: check for buffer overflow.
|
|
|
|
SkPath path;
|
|
|
|
sc->getPath(op->glyphId, &path);
|
|
|
|
op->pathSize = path.writeToMemory(&glyphBuffer[sizeof(Op)]);
|
|
|
|
writeSize += op->pathSize;
|
|
|
|
break;
|
|
|
|
}
|
2018-02-02 22:33:26 +00:00
|
|
|
case OpCode::kGlyphMetricsAndImage : {
|
|
|
|
// TODO: check for buffer overflow.
|
|
|
|
sc->getMetrics(&op->glyph);
|
|
|
|
if (op->glyph.fWidth <= 0 || op->glyph.fWidth >= kMaxGlyphWidth) {
|
|
|
|
op->glyph.fImage = nullptr;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
op->glyph.fImage = &glyphBuffer[sizeof(Op)];
|
|
|
|
sc->getImage(op->glyph);
|
|
|
|
writeSize += op->glyph.rowBytes() * op->glyph.fHeight;
|
|
|
|
break;
|
|
|
|
}
|
2018-01-09 17:34:11 +00:00
|
|
|
default:
|
2018-02-02 22:33:26 +00:00
|
|
|
SK_ABORT("Bad op");
|
2018-01-09 17:34:11 +00:00
|
|
|
}
|
2018-02-02 22:33:26 +00:00
|
|
|
|
2018-01-09 17:34:11 +00:00
|
|
|
write(writeFd, glyphBuffer.get(), writeSize);
|
2018-01-09 17:34:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
close(readFd);
|
|
|
|
close(writeFd);
|
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
std::cout << "Returning from render" << std::endl;
|
2018-01-09 17:34:11 +00:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:34:11 +00:00
|
|
|
enum direction : int {kRead = 0, kWrite = 1};
|
|
|
|
|
|
|
|
static void start_gpu(int render_to_gpu[2], int gpu_to_render[2]) {
|
|
|
|
std::cout << "gpu - Starting GPU" << std::endl;
|
|
|
|
close(gpu_to_render[kRead]);
|
|
|
|
close(render_to_gpu[kWrite]);
|
|
|
|
gpu(render_to_gpu[kRead], gpu_to_render[kWrite]);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void start_render(std::string& skpName, int render_to_gpu[2], int gpu_to_render[2]) {
|
|
|
|
std::cout << "renderer - Starting Renderer" << std::endl;
|
|
|
|
close(render_to_gpu[kRead]);
|
|
|
|
close(gpu_to_render[kWrite]);
|
|
|
|
renderer(skpName, gpu_to_render[kRead], render_to_gpu[kWrite]);
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:34:11 +00:00
|
|
|
int main(int argc, char** argv) {
|
|
|
|
std::string skpName = argc > 1 ? std::string{argv[1]} : std::string{"desk_nytimes"};
|
2018-02-02 17:54:55 +00:00
|
|
|
int mode = argc > 2 ? atoi(argv[2]) : -1;
|
2018-01-09 17:34:11 +00:00
|
|
|
printf("skp: %s\n", skpName.c_str());
|
|
|
|
|
|
|
|
int render_to_gpu[2],
|
|
|
|
gpu_to_render[2];
|
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
for (int m = 0; m < 8; m++) {
|
|
|
|
int r = pipe(render_to_gpu);
|
|
|
|
if (r < 0) {
|
|
|
|
perror("Can't write picture from render to GPU ");
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
r = pipe(gpu_to_render);
|
|
|
|
if (r < 0) {
|
|
|
|
perror("Can't write picture from render to GPU ");
|
|
|
|
return 1;
|
|
|
|
}
|
2018-01-09 17:34:11 +00:00
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
gPurgeFontCaches = (m & 4) == 4;
|
|
|
|
gUseGpu = (m & 2) == 2;
|
|
|
|
gUseProcess = (m & 1) == 1;
|
2018-01-09 17:34:11 +00:00
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
if (mode >= 0 && mode < 8 && mode != m) {
|
|
|
|
continue;
|
|
|
|
}
|
2018-01-09 17:34:11 +00:00
|
|
|
|
2018-02-02 17:54:55 +00:00
|
|
|
if (gUseProcess) {
|
|
|
|
pid_t child = fork();
|
|
|
|
SkGraphics::Init();
|
|
|
|
|
|
|
|
if (child == 0) {
|
|
|
|
start_gpu(render_to_gpu, gpu_to_render);
|
|
|
|
} else {
|
|
|
|
start_render(skpName, render_to_gpu, gpu_to_render);
|
|
|
|
waitpid(child, nullptr, 0);
|
|
|
|
}
|
2018-01-26 21:47:54 +00:00
|
|
|
} else {
|
2018-02-02 17:54:55 +00:00
|
|
|
SkGraphics::Init();
|
|
|
|
std::thread(gpu, render_to_gpu[kRead], gpu_to_render[kWrite]).detach();
|
|
|
|
renderer(skpName, gpu_to_render[kRead], render_to_gpu[kWrite]);
|
2018-01-09 17:34:11 +00:00
|
|
|
}
|
2018-01-09 17:34:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|