sd/qa/unit/tiledrendering/data/SlideRenderingTest_WithFields.odp |binary sd/qa/unit/tiledrendering/tiledrendering.cxx | 27 sd/source/ui/inc/SlideshowLayerRenderer.hxx | 73 +- sd/source/ui/tools/SlideshowLayerRenderer.cxx | 273 +++++----- 4 files changed, 218 insertions(+), 155 deletions(-)
New commits: commit 3227919ea158378a6a4435410ff6c30b1be6e459 Author: Tomaž Vajngerl <tomaz.vajng...@collabora.co.uk> AuthorDate: Wed Oct 23 11:30:19 2024 +0200 Commit: Tomaž Vajngerl <qui...@gmail.com> CommitDate: Thu Nov 7 11:08:38 2024 +0100 slideshow: simplify rendering, render text fields each in own layer This change significantly simplifies the rendering of layers by introducing an analysis pass (when we render the background) that creates rendering pass objects, which contain which objects and paragraphs need to be rendered in each pass. Previously we tried to figure this out durign the rendering, which meant we had to track a lot of various states. This all is not longer needed. This chaneg also means that the new RenderPassObjectRedirector is fairly simple. In addition this changes the test and implementation to render each text field in its own layer as it is expected by the online implementation (to enable master pages caching), but it still doesn't first render the placeholders for text fields as MasterPage layer and the TextField layer as a separate bitmap layer. This will be done next. Change-Id: I5377f4c5e5ebff67da70b248c831837cb9f2e559 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/175568 Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com> Reviewed-by: Tomaž Vajngerl <qui...@gmail.com> diff --git a/sd/qa/unit/tiledrendering/data/SlideRenderingTest_WithFields.odp b/sd/qa/unit/tiledrendering/data/SlideRenderingTest_WithFields.odp index 960adc4c5c55..1d7cc1f15e05 100644 Binary files a/sd/qa/unit/tiledrendering/data/SlideRenderingTest_WithFields.odp and b/sd/qa/unit/tiledrendering/data/SlideRenderingTest_WithFields.odp differ diff --git a/sd/qa/unit/tiledrendering/tiledrendering.cxx b/sd/qa/unit/tiledrendering/tiledrendering.cxx index 8442f9332c79..2e85ab088c1b 100644 --- a/sd/qa/unit/tiledrendering/tiledrendering.cxx +++ b/sd/qa/unit/tiledrendering/tiledrendering.cxx @@ -3336,6 +3336,7 @@ CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_WithFie bool bIsBitmapLayer = false; OUString rJsonMsg; CPPUNIT_ASSERT(!pXImpressDocument->renderNextSlideLayer(pBuffer.data(), bIsBitmapLayer, rJsonMsg)); + debugWriteImageToFile(0, pBuffer, nViewWidth, nViewHeight, rJsonMsg.toUtf8().getStr()); } { @@ -3374,10 +3375,13 @@ CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_WithFie CPPUNIT_ASSERT_EQUAL(aTransparentColor, aBitmapEx.GetPixelColor(20, 20)); // bottom-left corner - CPPUNIT_ASSERT_EQUAL(Color(0x90, 0x80, 0xff), aBitmapEx.GetPixelColor(20, nViewHeight - 20)); + CPPUNIT_ASSERT_EQUAL(aTransparentColor, aBitmapEx.GetPixelColor(20, nViewHeight - 20)); // bottom-right corner CPPUNIT_ASSERT_EQUAL(aTransparentColor, aBitmapEx.GetPixelColor(nViewWidth - 20, nViewHeight - 20)); + + // bottom-center + CPPUNIT_ASSERT_EQUAL(Color(0x20, 0xaa, 0x00), aBitmapEx.GetPixelColor(nViewWidth / 2, nViewHeight - 20)); } { @@ -3391,6 +3395,27 @@ CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_WithFie BitmapEx aBitmapEx = vcl::bitmap::CreateFromData(pBuffer.data(), nViewWidth, nViewHeight, nViewWidth * 4, /*nBitsPerPixel*/32, true, true); + // top-left corner + CPPUNIT_ASSERT_EQUAL(aTransparentColor, aBitmapEx.GetPixelColor(20, 20)); + + // bottom-left corner + CPPUNIT_ASSERT_EQUAL(Color(0x90, 0x80, 0xff), aBitmapEx.GetPixelColor(20, nViewHeight - 20)); + + // bottom-right corner + CPPUNIT_ASSERT_EQUAL(aTransparentColor, aBitmapEx.GetPixelColor(nViewWidth - 20, nViewHeight - 20)); + } + + { + std::vector<sal_uInt8> pBuffer(nViewWidth * nViewHeight * 4); + bool bIsBitmapLayer = false; + OUString rJsonMsg; + CPPUNIT_ASSERT(!pXImpressDocument->renderNextSlideLayer(pBuffer.data(), bIsBitmapLayer, rJsonMsg)); + CPPUNIT_ASSERT(bIsBitmapLayer); + // TODO - check JSON content + debugWriteImageToFile(4, pBuffer, nViewWidth, nViewHeight, rJsonMsg.toUtf8().getStr()); + + BitmapEx aBitmapEx = vcl::bitmap::CreateFromData(pBuffer.data(), nViewWidth, nViewHeight, nViewWidth * 4, /*nBitsPerPixel*/32, true, true); + // top-left corner CPPUNIT_ASSERT_EQUAL(Color(0x00, 0x50, 0x90), aBitmapEx.GetPixelColor(20, 20)); diff --git a/sd/source/ui/inc/SlideshowLayerRenderer.hxx b/sd/source/ui/inc/SlideshowLayerRenderer.hxx index 7a48e3366c1f..1d8f427735bb 100644 --- a/sd/source/ui/inc/SlideshowLayerRenderer.hxx +++ b/sd/source/ui/inc/SlideshowLayerRenderer.hxx @@ -34,17 +34,21 @@ namespace com::sun::star::animations class XAnimate; } +namespace sdr::contact +{ +class ViewObjectContactRedirector; +} + namespace sd { -struct RenderContext; +class RenderContext; enum class RenderStage { - Background, - Master, - Slide, - TextFields, - Count + Background = 0, + Master = 1, + Slide = 2, + TextFields = 3, }; struct AnimationLayerInfo @@ -60,31 +64,36 @@ struct AnimationRenderInfo std::unordered_map<sal_Int32, AnimationLayerInfo> maParagraphInfos; }; -/** Holds rendering state, properties and switches through all rendering passes */ -struct RenderState +// Holds information used when doing one rendering pass +struct RenderPass { RenderStage meStage = RenderStage::Background; + std::unordered_map<SdrObject*, std::deque<sal_Int32>> maObjectsAndParagraphs; + bool mbRenderObjectBackground = false; - bool mbStopRenderingWhenField = true; - - std::unordered_set<SdrObject*> maObjectsDone; + bool mbAnimation = false; + SdrObject* mpAnimatedObject = nullptr; + sal_Int32 mnAnimatedParagraph = -1; - std::unordered_map<SdrObject*, AnimationRenderInfo> maAnimationRenderInfoList; + bool isEmpty() { return maObjectsAndParagraphs.empty(); } +}; - sal_Int32 mnIndex[static_cast<unsigned>(RenderStage::Count)] = { 0, 0, 0, 0 }; - SdrObject* mpCurrentTarget = nullptr; - sal_Int32 mnCurrentTargetParagraph = -1; +/** Holds rendering state, properties and switches through all rendering passes */ +struct RenderState +{ + std::deque<RenderPass> maRenderPasses; - sal_Int32 mnRenderedObjectsInPass = 0; + RenderStage meStage = RenderStage::Background; - bool mbSkipAllInThisPass = false; + std::unordered_map<SdrObject*, AnimationRenderInfo> maAnimationRenderInfoList; - sal_Int32 mnCurrentPass = 0; + std::array<sal_Int32, 4> maIndices = { 0, 0, 0, 0 }; - std::deque<sal_Int32> maParagraphsToRender; + SdrObject* mpCurrentTarget = nullptr; + sal_Int32 mnCurrentTargetParagraph = -1; /// increments index depending on the current render stage - void incrementIndex() { mnIndex[static_cast<unsigned>(meStage)]++; } + void incrementIndex() { maIndices[size_t(meStage)]++; } /// returns the current stage as string OString stageString() const @@ -99,7 +108,7 @@ struct RenderState } /// returns the current index depending on the current render stage - sal_Int32 currentIndex() const { return mnIndex[static_cast<unsigned>(meStage)]; } + sal_Int32 currentIndex() const { return maIndices[size_t(meStage)]; } /// returns the current target element for which layer is created if any SdrObject* currentTarget() const { return mpCurrentTarget; } @@ -110,32 +119,13 @@ struct RenderState /// resets properties that are valid for one pass void resetPass() { - mnRenderedObjectsInPass = 0; - mbSkipAllInThisPass = false; mpCurrentTarget = nullptr; mnCurrentTargetParagraph = -1; } - bool hasPassAnyRenderedOutput() const { return mnRenderedObjectsInPass > 0; } - - /// is first rendered object in pass - bool isFirstObjectInPass() const { return mnRenderedObjectsInPass == 0; } - - /// return if there was no rendering output in the pass - bool noMoreOutput() const - { - // no output and we didn't skip anything and nothing was rendered - return !hasPassAnyRenderedOutput() && !mbSkipAllInThisPass; - } - /// should include background in rendering bool includeBackground() const { return meStage == RenderStage::Background; } - bool isObjectAlreadyRendered(SdrObject* pObject) const - { - return maObjectsDone.find(pObject) != maObjectsDone.end(); - } - static std::string getObjectHash(SdrObject* pObject) { css::uno::Reference<css::drawing::XShape> xShape = GetXShapeForSdrObject(pObject); @@ -161,7 +151,8 @@ private: Size maSlideSize; RenderState maRenderState; - void createViewAndDraw(RenderContext& rRenderContext); + void createViewAndDraw(RenderContext& rRenderContext, + sdr::contact::ViewObjectContactRedirector* pRedirector); void writeJSON(OString& rJsonMsg); void setupAnimations(); diff --git a/sd/source/ui/tools/SlideshowLayerRenderer.cxx b/sd/source/ui/tools/SlideshowLayerRenderer.cxx index 07a9f018a6fd..0ccde3ebefb8 100644 --- a/sd/source/ui/tools/SlideshowLayerRenderer.cxx +++ b/sd/source/ui/tools/SlideshowLayerRenderer.cxx @@ -44,12 +44,16 @@ using namespace ::com::sun::star; namespace sd { -struct RenderContext +/// Sets up the virtual device for rendering, and cleans up afterwards +class RenderContext { +private: SdrModel& mrModel; SdrPage& mrPage; EEControlBits mnSavedControlBits; + +public: ScopedVclPtrInstance<VirtualDevice> maVirtualDevice; RenderContext(unsigned char* pBuffer, SdrModel& rModel, SdrPage& rPage, Size const& rSlideSize) @@ -106,6 +110,7 @@ bool hasFields(SdrObject* pObject) return false; } +/// Sets visible for all kinds of polypolys in the container void changePolyPolys(drawinglayer::primitive2d::Primitive2DContainer& rContainer, bool bRenderObject) { @@ -122,6 +127,7 @@ void changePolyPolys(drawinglayer::primitive2d::Primitive2DContainer& rContainer } } +/// Searches for rectangle primitive and changes if the background should be rendered void changeBackground(drawinglayer::primitive2d::Primitive2DContainer const& rContainer, bool bRenderObject) { @@ -138,6 +144,7 @@ void changeBackground(drawinglayer::primitive2d::Primitive2DContainer const& rCo } } +/// Find the text block in the primitive containder, decompose if necessary drawinglayer::primitive2d::TextHierarchyBlockPrimitive2D* findTextBlock(drawinglayer::primitive2d::Primitive2DContainer const& rContainer, drawinglayer::geometry::ViewInformation2D const& rViewInformation2D) @@ -182,6 +189,7 @@ findTextBlock(drawinglayer::primitive2d::Primitive2DContainer const& rContainer, return nullptr; } +/// show/hide paragraphs in the container void modifyParagraphs(drawinglayer::primitive2d::Primitive2DContainer& rContainer, drawinglayer::geometry::ViewInformation2D const& rViewInformation2D, std::deque<sal_Int32> const& rPreserveIndices, bool bRenderObject) @@ -217,44 +225,51 @@ void modifyParagraphs(drawinglayer::primitive2d::Primitive2DContainer& rContaine } } -/** VOC redirector to control which object should be rendered and which not */ -class ObjectRedirector : public sdr::contact::ViewObjectContactRedirector +/// Analyze the renderng and create rendering passes +class AnalyzeRenderingRedirector : public sdr::contact::ViewObjectContactRedirector { protected: RenderState& mrRenderState; + RenderPass* mpCurrentRenderPass; + RenderStage mePreviousStage = RenderStage::Master; + public: - ObjectRedirector(RenderState& rRenderState) + AnalyzeRenderingRedirector(RenderState& rRenderState) : mrRenderState(rRenderState) + , mpCurrentRenderPass(newRenderPass()) { } - virtual void createRedirectedPrimitive2DSequence( - const sdr::contact::ViewObjectContact& rOriginal, - const sdr::contact::DisplayInfo& rDisplayInfo, - drawinglayer::primitive2d::Primitive2DDecompositionVisitor& rVisitor) override + // Adds a new rendering pass to the list and returns it + RenderPass* newRenderPass() { - // Generate single pass for background layer - if (mrRenderState.meStage == RenderStage::Background) - { - mrRenderState.mnRenderedObjectsInPass++; - mrRenderState.mbSkipAllInThisPass = true; - return; - } + mrRenderState.maRenderPasses.emplace_back(); + return &mrRenderState.maRenderPasses.back(); + } - if (mrRenderState.mbSkipAllInThisPass) + // Closes current rendering pass, and creates a new empty current one + void closeRenderPass() + { + if (mpCurrentRenderPass->maObjectsAndParagraphs.empty()) return; - SdrObject* pObject = rOriginal.GetViewContact().TryToGetSdrObject(); + mpCurrentRenderPass = newRenderPass(); + } - drawinglayer::geometry::ViewInformation2D const& rViewInformation2D - = rOriginal.GetObjectContact().getViewInformation2D(); + virtual void createRedirectedPrimitive2DSequence( + const sdr::contact::ViewObjectContact& rOriginal, + const sdr::contact::DisplayInfo& rDisplayInfo, + drawinglayer::primitive2d::Primitive2DDecompositionVisitor& /*rVisitor*/) override + { + SdrObject* pObject = rOriginal.GetViewContact().TryToGetSdrObject(); // Check if we are rendering an object that is valid to render (exists, and not empty) if (pObject == nullptr || pObject->IsEmptyPresObj()) return; SdrPage* pPage = pObject->getSdrPageFromSdrObject(); + // Does the object have a page if (pPage == nullptr) return; @@ -266,124 +281,134 @@ public: if (!bVisible) return; - // Check if we have already rendered the object - if (mrRenderState.isObjectAlreadyRendered(pObject)) - return; + // Determine the current stage, depending on the page + RenderStage eCurrentStage + = pPage->IsMasterPage() ? RenderStage::Master : RenderStage::Slide; - // Check if we are in correct stage - if (mrRenderState.meStage == RenderStage::Master && !pPage->IsMasterPage()) - { - if (mrRenderState.isFirstObjectInPass()) - { - // if this is the first object - change from master to slide - // means we are done with rendering of master layers - mrRenderState.meStage = RenderStage::Slide; - } - else - { - // if not, we have to stop rendering all further objects - mrRenderState.mbSkipAllInThisPass = true; - return; - } - } + // We switched from master objecst to slide objects + if (eCurrentStage == RenderStage::Slide && mePreviousStage == RenderStage::Master) + closeRenderPass(); - // Paragraph rendering switches - bool bRenderOtherParagraphs = false; - std::deque<sal_Int32> nOtherParagraphs; - - // check if object is in animation + // check if object is in an animation auto aIterator = mrRenderState.maAnimationRenderInfoList.find(pObject); if (aIterator != mrRenderState.maAnimationRenderInfoList.end()) { - // Animated object has to be only one in the render - mrRenderState.mbSkipAllInThisPass = true; // skip all next objects - - // Force a new layer - if (!mrRenderState.isFirstObjectInPass()) - return; + closeRenderPass(); AnimationRenderInfo aInfo = aIterator->second; - if (mrRenderState.maParagraphsToRender.empty() - && !aInfo.maParagraphs.empty()) // we need to render paragraphs + if (!aInfo.maParagraphs.empty()) // we need to render paragraphs { auto* pTextObject = dynamic_cast<SdrTextObj*>(pObject); if (pTextObject) { sal_Int32 nNumberOfParagraphs = pTextObject->GetOutlinerParaObject()->Count(); + std::deque<sal_Int32> nOtherParagraphs; for (sal_Int32 nParagraph = 0; nParagraph < nNumberOfParagraphs; ++nParagraph) - nOtherParagraphs.push_back(nParagraph); - + { + if (std::find(aInfo.maParagraphs.begin(), aInfo.maParagraphs.end(), + nParagraph) + == aInfo.maParagraphs.end()) + nOtherParagraphs.push_back(nParagraph); + } + // Add the non-animated part of the object that has animated paragraphs + mpCurrentRenderPass->maObjectsAndParagraphs.emplace(pObject, nOtherParagraphs); + mpCurrentRenderPass->meStage = eCurrentStage; + mpCurrentRenderPass->mbRenderObjectBackground = true; + mpCurrentRenderPass->mbAnimation = true; + mpCurrentRenderPass->mpAnimatedObject = pObject; + closeRenderPass(); + + // Add all the animated paragraphs for (sal_Int32 nParagraph : aInfo.maParagraphs) { - mrRenderState.maParagraphsToRender.push_back(nParagraph); - std::erase(nOtherParagraphs, nParagraph); + mpCurrentRenderPass->maObjectsAndParagraphs.emplace( + pObject, std::deque<sal_Int32>{ nParagraph }); + mpCurrentRenderPass->meStage = eCurrentStage; + mpCurrentRenderPass->mbAnimation = true; + mpCurrentRenderPass->mpAnimatedObject = pObject; + mpCurrentRenderPass->mnAnimatedParagraph = nParagraph; + closeRenderPass(); } - bRenderOtherParagraphs = true; } } + else + { + // Add the animated object - paragraphs are not animated + mpCurrentRenderPass->maObjectsAndParagraphs.emplace(pObject, + std::deque<sal_Int32>()); + mpCurrentRenderPass->meStage = eCurrentStage; + mpCurrentRenderPass->mbAnimation = true; + mpCurrentRenderPass->mpAnimatedObject = pObject; + closeRenderPass(); + } } - else if (mrRenderState.meStage == RenderStage::Master && hasFields(pObject) - && mrRenderState.mbStopRenderingWhenField && !mrRenderState.isFirstObjectInPass()) + // Check if the object has fields (slide number) + else if (eCurrentStage == RenderStage::Master && hasFields(pObject)) { - mrRenderState.mbStopRenderingWhenField = false; - mrRenderState.mbSkipAllInThisPass = true; - return; + closeRenderPass(); + + mpCurrentRenderPass->maObjectsAndParagraphs.emplace(pObject, std::deque<sal_Int32>()); + mpCurrentRenderPass->meStage = eCurrentStage; + closeRenderPass(); + } + // No specal handling is needed, just add the object to the current rendering pass + else + { + mpCurrentRenderPass->maObjectsAndParagraphs.emplace(pObject, std::deque<sal_Int32>()); + mpCurrentRenderPass->meStage = eCurrentStage; } + mePreviousStage = eCurrentStage; + } +}; - mrRenderState.mpCurrentTarget = pObject; +/// Render one render pass +class RenderPassObjectRedirector : public sdr::contact::ViewObjectContactRedirector +{ +protected: + RenderState& mrRenderState; + RenderPass const& mrRenderPass; + +public: + RenderPassObjectRedirector(RenderState& rRenderState, RenderPass const& rRenderPass) + : mrRenderState(rRenderState) + , mrRenderPass(rRenderPass) + { + } + + virtual void createRedirectedPrimitive2DSequence( + const sdr::contact::ViewObjectContact& rOriginal, + const sdr::contact::DisplayInfo& rDisplayInfo, + drawinglayer::primitive2d::Primitive2DDecompositionVisitor& rVisitor) override + { + SdrObject* pObject = rOriginal.GetViewContact().TryToGetSdrObject(); + + if (!pObject) + return; + + // check if object is in animation + auto aIterator = mrRenderPass.maObjectsAndParagraphs.find(pObject); + if (aIterator == mrRenderPass.maObjectsAndParagraphs.end()) + return; // render the object sdr::contact::ViewObjectContactRedirector::createRedirectedPrimitive2DSequence( rOriginal, rDisplayInfo, rVisitor); - if (!mrRenderState.maParagraphsToRender.empty()) + auto const& rParagraphs = aIterator->second; + + if (!rParagraphs.empty()) { + auto const& rViewInformation2D = rOriginal.GetObjectContact().getViewInformation2D(); auto rContainer = static_cast<drawinglayer::primitive2d::Primitive2DContainer&>(rVisitor); - - if (bRenderOtherParagraphs) - { - modifyParagraphs(rContainer, rViewInformation2D, nOtherParagraphs, - true); // render the object - mrRenderState.mnCurrentTargetParagraph = -1; - } - else - { - sal_Int32 nParagraph = mrRenderState.maParagraphsToRender.front(); - mrRenderState.maParagraphsToRender.pop_front(); - - std::deque<sal_Int32> aPreserveParagraphs{ nParagraph }; - mrRenderState.mnCurrentTargetParagraph = nParagraph; - // render only the paragraphs - modifyParagraphs(rContainer, rViewInformation2D, aPreserveParagraphs, false); - } - } - - if (mrRenderState.maParagraphsToRender.empty()) - { - mrRenderState.mnRenderedObjectsInPass++; - mrRenderState.maObjectsDone.insert(pObject); + modifyParagraphs(rContainer, rViewInformation2D, rParagraphs, + mrRenderPass.mbRenderObjectBackground); } } }; -bool hasEmptyMaster(SdrPage const& rPage) -{ - if (!rPage.TRG_HasMasterPage()) - return true; - - SdrPage& rMaster = rPage.TRG_GetMasterPage(); - for (size_t i = 0; i < rMaster.GetObjCount(); i++) - { - auto pObject = rMaster.GetObj(i); - if (!pObject->IsEmptyPresObj()) - return false; - } - return true; -} - SdrObject* getObjectForShape(uno::Reference<drawing::XShape> const& xShape) { if (!xShape.is()) @@ -507,7 +532,8 @@ Size SlideshowLayerRenderer::calculateAndSetSizePixel(Size const& rDesiredSizePi return maSlideSize; } -void SlideshowLayerRenderer::createViewAndDraw(RenderContext& rRenderContext) +void SlideshowLayerRenderer::createViewAndDraw( + RenderContext& rRenderContext, sdr::contact::ViewObjectContactRedirector* pRedirector) { SdrView aView(mrModel, rRenderContext.maVirtualDevice); aView.SetPageVisible(false); @@ -524,8 +550,7 @@ void SlideshowLayerRenderer::createViewAndDraw(RenderContext& rRenderContext) Point aPoint; vcl::Region aRegion(::tools::Rectangle(aPoint, aPageSize)); - ObjectRedirector aRedirector(maRenderState); - aView.CompleteRedraw(rRenderContext.maVirtualDevice, aRegion, &aRedirector); + aView.CompleteRedraw(rRenderContext.maVirtualDevice, aRegion, pRedirector); } namespace @@ -623,21 +648,43 @@ bool SlideshowLayerRenderer::render(unsigned char* pBuffer, OString& rJsonMsg) maRenderState.resetPass(); RenderContext aRenderContext(pBuffer, mrModel, mrPage, maSlideSize); - createViewAndDraw(aRenderContext); - // Check if we are done rendering all passes and there is no more output - if (maRenderState.noMoreOutput()) - return false; + // Render Background and analyze other passes + if (maRenderState.meStage == RenderStage::Background) + { + // Render no objects, just the background, but analyze and create rendering passes + AnalyzeRenderingRedirector aRedirector(maRenderState); + createViewAndDraw(aRenderContext, &aRedirector); - writeJSON(rJsonMsg); + // Last rendering pass might be empty - delete + if (maRenderState.maRenderPasses.back().isEmpty()) + maRenderState.maRenderPasses.pop_back(); - maRenderState.mnCurrentPass++; + // Write JSON for the Background layer + writeJSON(rJsonMsg); - if (maRenderState.meStage == RenderStage::Background) maRenderState.meStage = RenderStage::Master; + } + else + { + if (maRenderState.maRenderPasses.empty()) + return false; - if (hasEmptyMaster(mrPage)) - maRenderState.meStage = RenderStage::Slide; + auto const& rRenderPass = maRenderState.maRenderPasses.front(); + maRenderState.meStage = rRenderPass.meStage; + RenderPassObjectRedirector aRedirector(maRenderState, rRenderPass); + createViewAndDraw(aRenderContext, &aRedirector); + + if (rRenderPass.mbAnimation) + { + maRenderState.mpCurrentTarget = rRenderPass.mpAnimatedObject; + maRenderState.mnCurrentTargetParagraph = rRenderPass.mnAnimatedParagraph; + } + + writeJSON(rJsonMsg); + + maRenderState.maRenderPasses.pop_front(); + } return true; }