| |
| |
|
|
| #include "tsystem.h" |
| #include "tstopwatch.h" |
| #include "tthreadmessage.h" |
| #include "timagecache.h" |
| #include "tlevel_io.h" |
| #include "trasterimage.h" |
| #include "timageinfo.h" |
| #include "trop.h" |
| #include "tsop.h" |
| |
| |
| #include "toonz/toonzscene.h" |
| #include "toonz/sceneproperties.h" |
| #include "toonz/txsheet.h" |
| #include "toonz/tcamera.h" |
| #include "toonz/preferences.h" |
| #include "toonz/trasterimageutils.h" |
| #include "toonz/levelupdater.h" |
| #include "toutputproperties.h" |
| |
| |
| #include "tcg/tcg_macros.h" |
| |
| |
| #include <QCoreApplication> |
| |
| #include "toonz/movierenderer.h" |
| |
| |
| |
| |
| |
| namespace |
| { |
| |
| int RenderSessionId = 0; |
| |
| |
| |
| void addMark(const TRasterP &mark, TRasterImageP img) |
| { |
| TRasterP raster = img->getRaster(); |
| |
| if (raster->getLx() >= mark->getLx() && raster->getLy() >= mark->getLy()) { |
| TRasterP ras = raster->clone(); |
| |
| int borderx = troundp(0.035 * (ras->getLx() - mark->getLx())); |
| int bordery = troundp(0.035 * (ras->getLy() - mark->getLy())); |
| |
| TRect rect = TRect(borderx, bordery, borderx + mark->getLx() - 1, bordery + mark->getLy() - 1); |
| TRop::over(ras->extract(rect), mark); |
| |
| img->setRaster(ras); |
| } |
| } |
| |
| |
| |
| void getRange(ToonzScene *scene, bool isPreview, int &from, int &to) |
| { |
| TSceneProperties *sprop = scene->getProperties(); |
| |
| int step; |
| if (isPreview) |
| sprop->getPreviewProperties()->getRange(from, to, step); |
| else |
| sprop->getOutputProperties()->getRange(from, to, step); |
| |
| if (to < 0) { |
| TXsheet *xs = scene->getXsheet(); |
| |
| |
| from = (std::numeric_limits<int>::max)(), to = (std::numeric_limits<int>::min)(); |
| |
| for (int k = 0; k < xs->getColumnCount(); ++k) { |
| int r0, r1; |
| xs->getCellRange(k, r0, r1); |
| |
| from = tmin(from, r0), to = tmax(to, r1); |
| } |
| } |
| } |
| |
| |
| |
| QString getPreviewName(unsigned long renderSessionId) |
| { |
| return "previewed" + QString::number(renderSessionId) + ".noext"; |
| } |
| |
| } |
| |
| |
| |
| |
| |
| class MovieRenderer::Imp : public TRenderPort, public TSmartObject |
| { |
| public: |
| ToonzScene *m_scene; |
| TRenderer m_renderer; |
| TFilePath m_fp; |
| |
| TRenderSettings m_renderSettings; |
| TDimension m_frameSize; |
| double m_xDpi, m_yDpi; |
| |
| std::set<MovieRenderer::Listener *> m_listeners; |
| std::auto_ptr<LevelUpdater> m_levelUpdaterA, m_levelUpdaterB; |
| TSoundTrackP m_st; |
| |
| std::map<double, std::pair<TRasterP, TRasterP>> m_toBeSaved; |
| std::vector<pair<double, TFxPair>> m_framesToBeRendered; |
| std::string m_renderCacheId; |
| |
| |
| |
| std::map<double, bool> m_toBeAppliedGamma; |
| |
| TThread::Mutex m_mutex; |
| |
| int m_renderSessionId; |
| long m_whiteSample; |
| |
| int m_nextFrameIdxToSave; |
| int m_savingThreadsCount; |
| bool m_firstCompletedRaster; |
| bool m_failure; |
| bool m_cacheResults; |
| bool m_preview; |
| bool m_movieType; |
| |
| public: |
| Imp(ToonzScene *scene, const TFilePath &moviePath, int threadCount, bool cacheResults); |
| ~Imp(); |
| |
| |
| |
| void onRenderRasterCompleted(const RenderData &renderData); |
| void onRenderFailure(const RenderData &renderData, TException &e); |
| |
| |
| virtual void onRenderFinished(bool isCanceled = false); |
| |
| void doRenderRasterCompleted(const RenderData &renderData); |
| void doPreviewRasterCompleted(const RenderData &renderData); |
| |
| |
| |
| void prepareForStart(); |
| void addSoundtrack(int r0, int r1, double fps); |
| void postProcessImage(const TRasterImageP &img, bool has64bitOutputSupport, const TRasterP &mark, int frame); |
| |
| |
| |
| std::pair<bool, int> saveFrame(double frame, const std::pair<TRasterP, TRasterP> &rasters); |
| std::string getRenderCacheId(); |
| }; |
| |
| |
| |
| MovieRenderer::Imp::Imp(ToonzScene *scene, |
| const TFilePath &moviePath, |
| int threadCount, |
| bool cacheResults) |
| : m_scene(scene), m_renderer(threadCount), m_fp(moviePath), m_frameSize(scene->getCurrentCamera()->getRes()), m_xDpi(72), m_yDpi(72), m_renderSessionId(RenderSessionId++), m_nextFrameIdxToSave(0), m_savingThreadsCount(0), m_whiteSample(0), m_firstCompletedRaster(true) |
| , |
| m_failure(false) |
| , |
| m_cacheResults(cacheResults), m_preview(moviePath.isEmpty()), m_movieType(isMovieType(moviePath)) |
| { |
| m_renderCacheId = m_fp.withName(m_fp.getName() + "#RENDERID" + QString::number(m_renderSessionId).toStdString()) |
| .getLevelName(); |
| |
| m_renderer.addPort(this); |
| } |
| |
| |
| |
| MovieRenderer::Imp::~Imp() |
| { |
| m_renderer.removePort(this); |
| } |
| |
| |
| |
| void MovieRenderer::Imp::prepareForStart() |
| { |
| struct locals { |
| static void eraseUncompatibleExistingLevel(const TFilePath &fp, const TDimension &imageSize) |
| { |
| assert(!fp.isEmpty()); |
| |
| if (TSystem::doesExistFileOrLevel(fp)) { |
| bool remove = false; |
| |
| |
| |
| try { |
| TLevelReaderP lr(fp); |
| lr->loadInfo(); |
| |
| const TImageInfo *info = lr->getImageInfo(); |
| if (!info || info->m_lx != imageSize.lx || info->m_ly != imageSize.ly) |
| TSystem::removeFileOrLevel(fp); |
| } catch (...) { |
| |
| TSystem::removeFileOrLevel(fp); |
| } |
| |
| |
| |
| |
| } |
| } |
| }; |
| |
| TOutputProperties *oprop = m_scene->getProperties()->getOutputProperties(); |
| double frameRate = (double)oprop->getFrameRate(); |
| |
| |
| double stretchFactor = oprop->getRenderSettings().m_timeStretchTo / oprop->getRenderSettings().m_timeStretchFrom; |
| frameRate *= stretchFactor; |
| |
| |
| int shrinkX = m_renderSettings.m_shrinkX, shrinkY = m_renderSettings.m_shrinkY; |
| |
| |
| TPointD cameraPos(-0.5 * m_frameSize.lx, -0.5 * m_frameSize.ly); |
| TDimensionD cameraRes(double(m_frameSize.lx) / shrinkX, double(m_frameSize.ly) / shrinkY); |
| TDimension cameraResI(cameraRes.lx, cameraRes.ly); |
| |
| TRectD renderArea(cameraPos.x, cameraPos.y, cameraPos.x + cameraRes.lx, cameraPos.y + cameraRes.ly); |
| setRenderArea(renderArea); |
| |
| if (!m_fp.isEmpty()) { |
| try |
| { |
| if (!m_renderSettings.m_stereoscopic) { |
| locals::eraseUncompatibleExistingLevel(m_fp, cameraResI); |
| |
| m_levelUpdaterA.reset(new LevelUpdater(m_fp, oprop->getFileFormatProperties(m_fp.getType()))); |
| m_levelUpdaterA->getLevelWriter()->setFrameRate(frameRate); |
| } else { |
| TFilePath leftFp = m_fp.withName(m_fp.getName() + "_l"); |
| TFilePath rightFp = m_fp.withName(m_fp.getName() + "_r"); |
| |
| locals::eraseUncompatibleExistingLevel(leftFp, cameraResI); |
| locals::eraseUncompatibleExistingLevel(rightFp, cameraResI); |
| |
| m_levelUpdaterA.reset(new LevelUpdater(leftFp, oprop->getFileFormatProperties(leftFp.getType()))); |
| m_levelUpdaterA->getLevelWriter()->setFrameRate(frameRate); |
| |
| m_levelUpdaterB.reset(new LevelUpdater(rightFp, oprop->getFileFormatProperties(rightFp.getType()))); |
| m_levelUpdaterB->getLevelWriter()->setFrameRate(frameRate); |
| } |
| } catch (...) { |
| |
| |
| m_levelUpdaterA.reset(); |
| m_levelUpdaterB.reset(); |
| } |
| } |
| } |
| |
| |
| |
| void MovieRenderer::Imp::addSoundtrack(int r0, int r1, double fps) |
| { |
| TCG_ASSERT(r0 <= r1, return ); |
| |
| TXsheet::SoundProperties *prop = new TXsheet::SoundProperties(); |
| prop->m_frameRate = fps; |
| |
| TSoundTrack *snd = m_scene->getXsheet()->makeSound(prop); |
| if (!snd) { |
| |
| m_whiteSample = (r1 - r0 + 1) * 918; |
| return; |
| } |
| |
| |
| double samplePerFrame = snd->getSampleRate() / fps; |
| |
| |
| TSoundTrackP snd1 = snd->extract((TINT32)(r0 * samplePerFrame), |
| (TINT32)(r1 * samplePerFrame)); |
| assert(!m_st); |
| if (!m_st) { |
| |
| m_st = TSoundTrack::create(snd1->getFormat(), m_whiteSample); |
| m_whiteSample = 0; |
| } |
| |
| |
| TINT32 fromSample = m_st->getSampleCount(); |
| TINT32 numSample = tmax(TINT32((r1 - r0 + 1) * samplePerFrame), |
| snd1->getSampleCount()); |
| |
| m_st = TSop::insertBlank(m_st, fromSample, numSample + m_whiteSample); |
| m_st->copy(snd1, TINT32(fromSample + m_whiteSample)); |
| |
| m_whiteSample = 0; |
| } |
| |
| |
| |
| void MovieRenderer::Imp::onRenderRasterCompleted(const RenderData &renderData) |
| { |
| if (m_preview) |
| doPreviewRasterCompleted(renderData); |
| else |
| doRenderRasterCompleted(renderData); |
| } |
| |
| |
| |
| void MovieRenderer::Imp::postProcessImage(const TRasterImageP &img, bool has64bitOutputSupport, const TRasterP &mark, int frame) |
| { |
| img->setDpi(m_xDpi, m_yDpi); |
| |
| if (img->getRaster()->getPixelSize() == 8 && !has64bitOutputSupport) { |
| TRaster32P aux(img->getRaster()->getLx(), img->getRaster()->getLy()); |
| TRop::convert(aux, img->getRaster()); |
| img->setRaster(aux); |
| } |
| |
| if (mark) |
| addMark(mark, img); |
| |
| if (Preferences::instance()->isSceneNumberingEnabled()) |
| TRasterImageUtils::addGlobalNumbering(img, m_scene->getSceneName(), frame); |
| } |
| |
| |
| |
| std::pair<bool, int> MovieRenderer::Imp::saveFrame(double frame, const std::pair<TRasterP, TRasterP> &rasters) |
| { |
| bool success = false; |
| |
| |
| double stretchFac = double(m_renderSettings.m_timeStretchTo) / m_renderSettings.m_timeStretchFrom; |
| |
| int fr = (stretchFac != 1) ? tround(frame * stretchFac) : int(frame); |
| TFrameId fid(fr + 1); |
| |
| if (m_levelUpdaterA.get()) { |
| assert(m_levelUpdaterB.get() || !rasters.second); |
| |
| |
| bool has64bitOutputSupport = false; |
| { |
| if (TImageWriterP writerA = m_levelUpdaterA->getLevelWriter()->getFrameWriter(fid)) |
| has64bitOutputSupport = writerA->is64bitOutputSupported(); |
| |
| |
| |
| } |
| |
| |
| TRasterP rasterA = rasters.first, rasterB = rasters.second; |
| assert(rasterA); |
| |
| |
| |
| |
| if (m_renderSettings.m_gamma != 1.0 && m_toBeAppliedGamma[frame]) { |
| TRop::gammaCorrect(rasterA, m_renderSettings.m_gamma); |
| if (rasterB) |
| TRop::gammaCorrect(rasterB, m_renderSettings.m_gamma); |
| } |
| |
| |
| try { |
| TRasterImageP imgA(rasterA); |
| postProcessImage(imgA, has64bitOutputSupport, m_renderSettings.m_mark, fid.getNumber()); |
| |
| m_levelUpdaterA->update(fid, imgA); |
| |
| if (rasterB) { |
| TRasterImageP imgB(rasterB); |
| postProcessImage(imgB, has64bitOutputSupport, m_renderSettings.m_mark, fid.getNumber()); |
| |
| m_levelUpdaterB->update(fid, imgB); |
| } |
| |
| |
| |
| if (m_cacheResults) { |
| if (imgA->getRaster()->getPixelSize() == 8) { |
| |
| TRaster32P aux(imgA->getRaster()->getLx(), imgA->getRaster()->getLy()); |
| |
| TRop::convert(aux, imgA->getRaster()); |
| imgA->setRaster(aux); |
| } |
| |
| TImageCache::instance()->add(m_renderCacheId + toString(fid.getNumber()), imgA); |
| } |
| |
| success = true; |
| } catch (...) { |
| |
| |
| } |
| } |
| |
| return std::make_pair(success, fr); |
| } |
| |
| |
| |
| void MovieRenderer::Imp::doRenderRasterCompleted(const RenderData &renderData) |
| { |
| assert(!(m_cacheResults && m_levelUpdaterB.get())); |
| |
| QMutexLocker locker(&m_mutex); |
| |
| |
| if (m_firstCompletedRaster && m_movieType && !m_st) { |
| int from, to; |
| getRange(m_scene, false, from, to); |
| |
| TLevelP oldLevel(m_levelUpdaterA->getInputLevel()); |
| if (oldLevel) { |
| from = tmin(from, oldLevel->begin()->first.getNumber() - 1); |
| to = tmax(to, (--oldLevel->end())->first.getNumber() - 1); |
| } |
| |
| addSoundtrack(from, to, m_scene->getProperties()->getOutputProperties()->getFrameRate()); |
| |
| if (m_st) { |
| m_levelUpdaterA->getLevelWriter()->saveSoundTrack(m_st.getPointer()); |
| if (m_levelUpdaterB.get()) |
| m_levelUpdaterB->getLevelWriter()->saveSoundTrack(m_st.getPointer()); |
| } |
| } |
| |
| |
| TRasterP toBeSavedRasA = renderData.m_rasA->clone(); |
| TRasterP toBeSavedRasB = renderData.m_rasB ? renderData.m_rasB->clone() : TRasterP(); |
| |
| m_toBeSaved[renderData.m_frames[0]] = std::make_pair(toBeSavedRasA, toBeSavedRasB); |
| |
| m_toBeAppliedGamma[renderData.m_frames[0]] = true; |
| |
| |
| std::vector<double>::const_iterator jt; |
| for (jt = renderData.m_frames.begin(), ++jt; jt != renderData.m_frames.end(); ++jt) { |
| m_toBeSaved[*jt] = std::make_pair(toBeSavedRasA, toBeSavedRasB); |
| m_toBeAppliedGamma[*jt] = false; |
| } |
| |
| |
| while (!m_toBeSaved.empty()) { |
| std::map<double, std::pair<TRasterP, TRasterP>>::iterator ft = m_toBeSaved.begin(); |
| |
| |
| |
| if (m_movieType && (ft->first != m_framesToBeRendered[m_nextFrameIdxToSave].first)) |
| break; |
| |
| |
| |
| double frame = ft->first; |
| std::pair<TRasterP, TRasterP> rasters = ft->second; |
| |
| ++m_nextFrameIdxToSave; |
| m_toBeSaved.erase(ft); |
| |
| |
| std::pair<bool, int> savedFrame; |
| { |
| |
| struct SaveTimer { |
| int &m_count; |
| SaveTimer(int &count) : m_count(count) |
| { |
| if (m_count++ == 0) |
| TStopWatch::global(0).start(); |
| } |
| ~SaveTimer() |
| { |
| if (--m_count == 0) |
| TStopWatch::global(0).stop(); |
| } |
| } saveTimer(m_savingThreadsCount); |
| |
| |
| struct MutexUnlocker { |
| QMutexLocker *m_locker; |
| ~MutexUnlocker() |
| { |
| if (m_locker) |
| m_locker->relock(); |
| } |
| } unlocker = {m_movieType ? (QMutexLocker *)0 : (locker.unlock(), &locker)}; |
| |
| savedFrame = saveFrame(frame, rasters); |
| } |
| |
| |
| bool okToContinue = true; |
| |
| std::set<MovieRenderer::Listener *>::iterator lt = m_listeners.begin(); |
| |
| if (savedFrame.first) { |
| for (; lt != m_listeners.end(); ++lt) |
| okToContinue &= (*lt)->onFrameCompleted(savedFrame.second); |
| } else { |
| for (; lt != m_listeners.end(); ++lt) { |
| TException e; |
| okToContinue &= (*lt)->onFrameFailed(savedFrame.second, e); |
| } |
| } |
| |
| if (!okToContinue) { |
| |
| |
| |
| |
| { |
| int from, to; |
| getRange(m_scene, false, from, to); |
| |
| for (int i = from; i < to; i++) |
| TImageCache::instance()->remove(m_renderCacheId + toString(i + 1)); |
| } |
| |
| m_renderer.stopRendering(); |
| |
| m_levelUpdaterA.reset(); |
| m_levelUpdaterB.reset(); |
| } |
| } |
| |
| m_firstCompletedRaster = false; |
| } |
| |
| |
| |
| void MovieRenderer::Imp::doPreviewRasterCompleted(const RenderData &renderData) |
| { |
| |
| |
| assert(!m_levelUpdaterA.get()); |
| |
| QMutexLocker sl(&m_mutex); |
| |
| QString name = getPreviewName(m_renderSessionId); |
| |
| TRasterP ras = renderData.m_rasA->clone(); |
| if (renderData.m_rasB) { |
| assert(m_renderSettings.m_stereoscopic); |
| TRop::makeStereoRaster(ras, renderData.m_rasB); |
| } |
| |
| TRasterImageP img(ras); |
| img->setDpi(m_xDpi, m_yDpi); |
| |
| try { |
| if (renderData.m_info.m_mark != TRasterP()) |
| addMark(renderData.m_info.m_mark, img); |
| |
| if (img->getRaster()->getPixelSize() == 8) { |
| TRaster32P aux(img->getRaster()->getLx(), img->getRaster()->getLy()); |
| TRop::convert(aux, img->getRaster()); |
| img->setRaster(aux); |
| } |
| |
| QString frameName = name + QString::number(renderData.m_frames[0] + 1); |
| |
| TImageCache::instance()->add(frameName.toStdString(), img); |
| |
| |
| |
| std::vector<double>::const_iterator jt; |
| for (jt = renderData.m_frames.begin(), ++jt; jt != renderData.m_frames.end(); ++jt) { |
| frameName = name + QString::number(*jt + 1); |
| TImageCache::instance()->add(frameName.toStdString(), img); |
| } |
| } catch (...) { |
| } |
| |
| std::set<MovieRenderer::Listener *>::iterator listenerIt = m_listeners.begin(); |
| |
| bool okToContinue = true; |
| |
| for (; listenerIt != m_listeners.end(); ++listenerIt) |
| okToContinue &= (*listenerIt)->onFrameCompleted(renderData.m_frames[0]); |
| |
| if (!okToContinue) { |
| |
| int from, to; |
| getRange(m_scene, true, from, to); |
| for (int i = from; i < to; i++) { |
| QString frameName = name + QString::number(i + 1); |
| TImageCache::instance()->remove(frameName.toStdString()); |
| } |
| |
| m_renderer.stopRendering(); |
| } |
| |
| m_firstCompletedRaster = false; |
| } |
| |
| |
| |
| void MovieRenderer::Imp::onRenderFailure(const RenderData &renderData, TException &e) |
| { |
| QMutexLocker sl(&m_mutex); |
| |
| m_failure = true; |
| |
| |
| |
| if (!m_levelUpdaterA.get()) |
| return; |
| |
| |
| m_toBeSaved[0.0] = std::make_pair(TRasterP(), TRasterP()); |
| |
| std::map<double, std::pair<TRasterP, TRasterP>>::iterator it = m_toBeSaved.begin(); |
| while (it != m_toBeSaved.end()) { |
| if (m_movieType && (it->first != m_framesToBeRendered[m_nextFrameIdxToSave].first)) |
| break; |
| |
| |
| |
| |
| |
| double stretchFac = (double)renderData.m_info.m_timeStretchTo / renderData.m_info.m_timeStretchFrom; |
| |
| int fr; |
| if (stretchFac != 1) |
| fr = tround(it->first * stretchFac); |
| else |
| fr = (int)it->first; |
| |
| |
| |
| std::set<MovieRenderer::Listener *>::iterator lt = m_listeners.begin(); |
| bool okToContinue = true; |
| |
| for (; lt != m_listeners.end(); ++lt) |
| okToContinue &= (*lt)->onFrameFailed((int)it->first, e); |
| |
| if (!okToContinue) |
| m_renderer.stopRendering(); |
| |
| ++m_nextFrameIdxToSave; |
| m_toBeSaved.erase(it++); |
| } |
| } |
| |
| |
| |
| void MovieRenderer::Imp::onRenderFinished(bool isCanceled) |
| { |
| TFilePath levelName(m_levelUpdaterA.get() ? m_fp : TFilePath(getPreviewName(m_renderSessionId).toStdWString())); |
| |
| |
| m_levelUpdaterA.reset(); |
| m_levelUpdaterB.reset(); |
| |
| if (!m_failure) { |
| |
| std::set<MovieRenderer::Listener *>::iterator it; |
| for (it = m_listeners.begin(); it != m_listeners.end(); ++it) |
| (*it)->onSequenceCompleted(levelName); |
| |
| |
| } |
| |
| release(); |
| } |
| |
| |
| |
| |
| |
| |
| |
| MovieRenderer::MovieRenderer(ToonzScene *scene, const TFilePath &moviePath, int threadCount, bool cacheResults) |
| : m_imp(new Imp(scene, moviePath, threadCount, cacheResults)) |
| { |
| m_imp->addRef(); |
| } |
| |
| |
| |
| MovieRenderer::~MovieRenderer() |
| { |
| m_imp->release(); |
| } |
| |
| |
| |
| void MovieRenderer::setRenderSettings(const TRenderSettings &renderSettings) |
| { |
| m_imp->m_renderSettings = renderSettings; |
| } |
| |
| |
| |
| void MovieRenderer::setDpi(double xDpi, double yDpi) |
| { |
| m_imp->m_xDpi = xDpi; |
| m_imp->m_yDpi = yDpi; |
| } |
| |
| |
| |
| void MovieRenderer::addListener(Listener *listener) |
| { |
| m_imp->m_listeners.insert(listener); |
| } |
| |
| |
| |
| void MovieRenderer::addFrame(double frame, const TFxPair &fxPair) |
| { |
| m_imp->m_framesToBeRendered.push_back(std::make_pair(frame, fxPair)); |
| } |
| |
| |
| |
| void MovieRenderer::enablePrecomputing(bool on) |
| { |
| m_imp->m_renderer.enablePrecomputing(on); |
| } |
| |
| |
| |
| bool MovieRenderer::isPrecomputingEnabled() const |
| { |
| return m_imp->m_renderer.isPrecomputingEnabled(); |
| } |
| |
| |
| |
| void MovieRenderer::start() |
| { |
| m_imp->prepareForStart(); |
| |
| |
| |
| |
| m_imp->addRef(); |
| |
| |
| RenderDataVector *datasToBeRendered = new RenderDataVector; |
| size_t i, size = m_imp->m_framesToBeRendered.size(); |
| for (i = 0; i < size; ++i) |
| datasToBeRendered->push_back(TRenderer::RenderData( |
| m_imp->m_framesToBeRendered[i].first, |
| m_imp->m_renderSettings, |
| m_imp->m_framesToBeRendered[i].second)); |
| |
| m_imp->m_renderer.startRendering(datasToBeRendered); |
| } |
| |
| |
| |
| void MovieRenderer::onCanceled() |
| { |
| m_imp->m_renderer.stopRendering(true); |
| } |
| |
| |
| |
| TRenderer *MovieRenderer::getTRenderer() |
| { |
| |
| |
| |
| |
| |
| |
| return &m_imp->m_renderer; |
| } |
| |