//======================================================================== // // TileCache.cc // // Copyright 2014 Glyph & Cog, LLC // //======================================================================== #include #ifdef USE_GCC_PRAGMAS #pragma implementation #endif #include "gmem.h" #include "gmempp.h" #include "GList.h" #include "GMutex.h" #ifdef _WIN32 # include #else # include # include #endif #include "Object.h" #include "PDFDoc.h" #include "SplashBitmap.h" #include "SplashOutputDev.h" #include "DisplayState.h" #include "GfxState.h" #include "TileMap.h" #include "TileCache.h" //------------------------------------------------------------------------ // CachedTileDesc //------------------------------------------------------------------------ enum CachedTileState { cachedTileUnstarted, // worker thread hasn't started // rasterization yet cachedTileStarted, // worker thread is rasterizing the tile cachedTileFinished, // rasterization is done cachedTileCanceled // worker thread should stop rasterizing // and remove this tile from the cache }; class CachedTileDesc: public TileDesc { public: CachedTileDesc(TileDesc *tile): TileDesc(tile->page, tile->rotate, tile->dpi, tile->tx, tile->ty, tile->tw, tile->th), state(cachedTileUnstarted), active(gTrue), bitmap(NULL), freeBitmap(gFalse) {} ~CachedTileDesc(); CachedTileState state; GBool active; SplashBitmap *bitmap; GBool freeBitmap; }; CachedTileDesc::~CachedTileDesc() { if (freeBitmap) { delete bitmap; } } //------------------------------------------------------------------------ // OS-dependent threading support code // // NB: This wrapper code is not meant to be general purpose. Pthreads // condition objects are not equivalent to Windows event objects, in // general. //------------------------------------------------------------------------ //-------------------- Windows -------------------- #ifdef _WIN32 typedef HANDLE GThreadID; typedef DWORD (WINAPI *GThreadFunc)(void *); #define GThreadReturn DWORD WINAPI static void gCreateThread(GThreadID *thr, GThreadFunc threadFunc, void *data) { *thr = CreateThread(NULL, 0, threadFunc, data, 0, NULL); } static void gJoinThread(GThreadID thr) { WaitForSingleObject(thr, INFINITE); CloseHandle(thr); } typedef HANDLE GCondition; static void gInitCondition(GCondition *c) { *c = CreateEvent(NULL, TRUE, FALSE, NULL); } static void gDestroyCondition(GCondition *c) { CloseHandle(*c); } static void gSignalCondition(GCondition *c) { SetEvent(*c); } static void gClearCondition(GCondition *c) { ResetEvent(*c); } static void gWaitCondition(GCondition *c, GMutex *m) { LeaveCriticalSection(m); WaitForSingleObject(*c, INFINITE); EnterCriticalSection(m); } //-------------------- pthreads -------------------- #else typedef pthread_t GThreadID; typedef void *(*GThreadFunc)(void *); #define GThreadReturn void* static void gCreateThread(GThreadID *thr, GThreadFunc threadFunc, void *data) { pthread_create(thr, NULL, threadFunc, data); } static void gJoinThread(GThreadID thr) { pthread_join(thr, NULL); } typedef pthread_cond_t GCondition; static void gInitCondition(GCondition *c) { pthread_cond_init(c, NULL); } static void gDestroyCondition(GCondition *c) { pthread_cond_destroy(c); } static void gSignalCondition(GCondition *c) { pthread_cond_broadcast(c); } static void gClearCondition(GCondition *c) { } static void gWaitCondition(GCondition *c, GMutex *m) { pthread_cond_wait(c, m); } #endif //------------------------------------------------------------------------ // TileCacheThreadPool //------------------------------------------------------------------------ class TileCacheThreadPool { public: TileCacheThreadPool(TileCache *tileCacheA, int nThreadsA); ~TileCacheThreadPool(); // Called by the client when one or more jobs have been added to the // queue. This must be called with the mutex unlocked. void jobAdded(); // Wait for at least one job to be finished. This must be called // with the mutex locked. void waitForFinishedJob(); // The client should use the mutex to protect the queue data // structure. void lockMutex() { gLockMutex(&mutex); } void unlockMutex() { gUnlockMutex(&mutex); } private: static GThreadReturn threadFunc(void *arg); void worker(); TileCache *tileCache; int nThreads; GThreadID *threads; GBool quit; GMutex mutex; GCondition cond; // signalled when a job is added to the // queue and when the quit flag is set GCondition finishCond; // signalled whenever a worker thread // finishes rasterizing a tile }; TileCacheThreadPool::TileCacheThreadPool(TileCache *tileCacheA, int nThreadsA) { int i; tileCache = tileCacheA; nThreads = nThreadsA; quit = gFalse; gInitMutex(&mutex); gInitCondition(&cond); gInitCondition(&finishCond); threads = (GThreadID *)gmallocn(nThreads, sizeof(GThreadID)); for (i = 0; i < nThreads; ++i) { gCreateThread(&threads[i], &threadFunc, this); } } TileCacheThreadPool::~TileCacheThreadPool() { int i; gLockMutex(&mutex); quit = true; gSignalCondition(&cond); gUnlockMutex(&mutex); for (i = 0; i < nThreads; ++i) { gJoinThread(threads[i]); } gDestroyCondition(&cond); gDestroyCondition(&finishCond); gDestroyMutex(&mutex); gfree(threads); } void TileCacheThreadPool::jobAdded() { gLockMutex(&mutex); gSignalCondition(&cond); gUnlockMutex(&mutex); } GThreadReturn TileCacheThreadPool::threadFunc(void *arg) { ((TileCacheThreadPool *)arg)->worker(); return 0; } void TileCacheThreadPool::worker() { CachedTileDesc *ct; while (1) { gLockMutex(&mutex); while (!quit && !(ct = tileCache->getUnstartedTile())) { gWaitCondition(&cond, &mutex); } if (quit) { gUnlockMutex(&mutex); break; } if (!tileCache->hasUnstartedTiles()) { gClearCondition(&cond); } gUnlockMutex(&mutex); tileCache->rasterizeTile(ct); gSignalCondition(&finishCond); } } void TileCacheThreadPool::waitForFinishedJob() { gWaitCondition(&finishCond, &mutex); } //------------------------------------------------------------------------ // TileCache //------------------------------------------------------------------------ TileCache::TileCache(DisplayState *stateA) { state = stateA; state->setTileCache(this); cache = new GList(); threadPool = new TileCacheThreadPool(this, state->getNWorkerThreads()); tileDoneCbk = NULL; tileDoneCbkData = NULL; } TileCache::~TileCache() { flushCache(gFalse); delete threadPool; delete cache; } void TileCache::setActiveTileList(GList *tiles) { TileDesc *tile; CachedTileDesc *ct; int tileIdx, cacheIdx; GBool newTiles; threadPool->lockMutex(); // remove any unstarted tiles not on the new active list; // cancel any started tiles not on the new active list; // mark all other tiles as inactive (active tiles will be marked later) cacheIdx = 0; while (cacheIdx < cache->getLength()) { ct = (CachedTileDesc *)cache->get(cacheIdx); if (ct->state == cachedTileUnstarted && findTile(ct, tiles) < 0) { delete (CachedTileDesc *)cache->del(cacheIdx); } else if (ct->state == cachedTileStarted && findTile(ct, tiles) < 0) { ct->state = cachedTileCanceled; ++cacheIdx; } else { ct->active = gFalse; ++cacheIdx; } } // mark cached tiles as active; add any new tiles to the cache newTiles = gFalse; for (tileIdx = 0; tileIdx < tiles->getLength(); ++tileIdx) { tile = (TileDesc *)tiles->get(tileIdx); cacheIdx = findTile(tile, cache); if (cacheIdx >= 0) { ct = (CachedTileDesc *)cache->del(cacheIdx); } else { ct = new CachedTileDesc(tile); newTiles = gTrue; } ct->active = gTrue; cache->insert(0, ct); } cleanCache(); threadPool->unlockMutex(); if (newTiles) { threadPool->jobAdded(); } } SplashBitmap *TileCache::getTileBitmap(TileDesc *tile, GBool *finished) { CachedTileDesc *ct; SplashBitmap *bitmap; int cacheIdx; threadPool->lockMutex(); cacheIdx = findTile(tile, cache); if (cacheIdx < 0) { threadPool->unlockMutex(); return NULL; } ct = (CachedTileDesc *)cache->get(cacheIdx); if (ct->state != cachedTileCanceled) { bitmap = ct->bitmap; } else { bitmap = NULL; } if (finished) { *finished = ct->state == cachedTileFinished; } threadPool->unlockMutex(); return bitmap; } void TileCache::paperColorChanged() { flushCache(gFalse); } void TileCache::reverseVideoChanged() { flushCache(gFalse); } void TileCache::optionalContentChanged() { flushCache(gFalse); } void TileCache::docChanged() { flushCache(gTrue); } void TileCache::forceRedraw() { flushCache(gFalse); } // Search for on , and return its index if found, or // -1 if not found. If is part of the cache, or if // is the cache, the caller must have locked the ThreadPool mutex. int TileCache::findTile(TileDesc *tile, GList *tileList) { TileDesc *t; int i; for (i = 0; i < tileList->getLength(); ++i) { t = (TileDesc *)tileList->get(i); if (t->matches(tile)) { return i; } } return -1; } // If there are too many tiles in the cache, remove the least recently // used tiles. Never removes active tiles. The caller must have // locked the ThreadPool mutex. void TileCache::cleanCache() { CachedTileDesc *ct; int n, i; // count the number of non-canceled tiles n = 0; for (i = 0; i < cache->getLength(); ++i) { ct = (CachedTileDesc *)cache->get(i); if (ct->state != cachedTileCanceled) { ++n; } } // if there are too many non-canceled tiles, remove tiles i = cache->getLength() - 1; while (n > state->getTileCacheSize() && i >= 0) { ct = (CachedTileDesc *)cache->get(i); if (ct->active) { break; } // any non-active tiles with state == cachedTileUnstarted should // already have been removed by setActiveTileList() if (ct->state == cachedTileFinished) { delete (CachedTileDesc *)cache->del(i); --n; --i; } else { --i; } } } // Remove all cached tiles. For tiles that are being rasterized, sets // state to canceled. If is true, this function won't return // until the cache is empty, i.e., until all possible users of the // PDFDoc are done. void TileCache::flushCache(GBool wait) { CachedTileDesc *ct; int i; threadPool->lockMutex(); i = 0; while (i < cache->getLength()) { ct = (CachedTileDesc *)cache->get(i); switch (ct->state) { case cachedTileUnstarted: case cachedTileFinished: delete (CachedTileDesc *)cache->del(i); break; case cachedTileStarted: ct->state = cachedTileCanceled; ++i; break; case cachedTileCanceled: default: ++i; break; } } if (wait) { while (cache->getLength() > 0) { threadPool->waitForFinishedJob(); } } threadPool->unlockMutex(); } // Remove a tile from the cache and delete it. This will be called // with the TileCacheThreadPool mutex locked. void TileCache::removeTile(CachedTileDesc *ct) { int i; for (i = 0; i < cache->getLength(); ++i) { if (cache->get(i) == ct) { delete (CachedTileDesc *)cache->del(i); break; } } } // Return true if there are one or more unstarted tiles. This will be // called with the TileCacheThreadPool mutex locked. GBool TileCache::hasUnstartedTiles() { CachedTileDesc *ct; int i; for (i = 0; i < cache->getLength(); ++i) { ct = (CachedTileDesc *)cache->get(i); if (ct->state == cachedTileUnstarted) { return gTrue; } } return gFalse; } // Return the next unstarted tile, changing its state to // cachedTileStarted. If there are no unstarted tiles, return NULL. // This will be called with the TileCacheThreadPool mutex locked. CachedTileDesc *TileCache::getUnstartedTile() { CachedTileDesc *ct; int i; for (i = 0; i < cache->getLength(); ++i) { ct = (CachedTileDesc *)cache->get(i); if (ct->state == cachedTileUnstarted) { ct->state = cachedTileStarted; return ct; } } return NULL; } struct TileCacheStartPageInfo { TileCache *tileCache; CachedTileDesc *ct; SplashOutputDev *out; }; void TileCache::startPageCbk(void *data) { TileCacheStartPageInfo *info = (TileCacheStartPageInfo *)data; info->tileCache->threadPool->lockMutex(); info->ct->bitmap = info->out->getBitmap(); info->ct->freeBitmap = gFalse; info->tileCache->threadPool->unlockMutex(); } // Rasterize . The state has already been set to // cachedTileStarted. void TileCache::rasterizeTile(CachedTileDesc *ct) { SplashOutputDev *out; TileCacheStartPageInfo info; out = new SplashOutputDev(state->getColorMode(), 1, state->getReverseVideo(), state->getPaperColor()); info.tileCache = this; info.ct = ct; info.out = out; out->setStartPageCallback(&TileCache::startPageCbk, &info); out->startDoc(state->getDoc()->getXRef()); state->getDoc()->displayPageSlice(out, ct->page, ct->dpi, ct->dpi, ct->rotate, gFalse, gTrue, gFalse, ct->tx, ct->ty, ct->tw, ct->th, &abortCheckCbk, ct); if (ct->state == cachedTileCanceled) { threadPool->lockMutex(); removeTile(ct); threadPool->unlockMutex(); } else { threadPool->lockMutex(); ct->bitmap = out->takeBitmap(); ct->freeBitmap = gTrue; ct->state = cachedTileFinished; threadPool->unlockMutex(); if (tileDoneCbk) { (*tileDoneCbk)(tileDoneCbkData); } } delete out; } GBool TileCache::abortCheckCbk(void *data) { CachedTileDesc *ct = (CachedTileDesc *)data; return ct->state == cachedTileCanceled; }