Coverage Report

Created: 2025-12-31 10:39

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/canvas/source/tools/spriteredrawmanager.cxx
Line
Count
Source
1
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
/*
3
 * This file is part of the LibreOffice project.
4
 *
5
 * This Source Code Form is subject to the terms of the Mozilla Public
6
 * License, v. 2.0. If a copy of the MPL was not distributed with this
7
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8
 *
9
 * This file incorporates work covered by the following license notice:
10
 *
11
 *   Licensed to the Apache Software Foundation (ASF) under one or more
12
 *   contributor license agreements. See the NOTICE file distributed
13
 *   with this work for additional information regarding copyright
14
 *   ownership. The ASF licenses this file to you under the Apache
15
 *   License, Version 2.0 (the "License"); you may not use this file
16
 *   except in compliance with the License. You may obtain a copy of
17
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
18
 */
19
20
#include <sal/config.h>
21
22
#include <algorithm>
23
24
#include <basegfx/range/b2drectangle.hxx>
25
#include <basegfx/utils/canvastools.hxx>
26
#include <comphelper/diagnose_ex.hxx>
27
#include <sal/log.hxx>
28
29
#include <spriteredrawmanager.hxx>
30
#include <boost/range/adaptor/reversed.hpp>
31
#include <utility>
32
33
namespace canvas
34
{
35
    namespace
36
    {
37
        /** Helper class to condense sprite updates into a single action
38
39
            This class tracks the sprite changes over the recorded
40
            change list, and generates a single update action from
41
            that (note that per screen update, several moves,
42
            visibility changes and content updates might happen)
43
         */
44
        class SpriteTracer
45
        {
46
        public:
47
            explicit SpriteTracer( Sprite::Reference rAffectedSprite ) :
48
0
                mpAffectedSprite(std::move(rAffectedSprite)),
49
0
                mbIsMove( false ),
50
0
                mbIsGenericUpdate( false )
51
0
            {
52
0
            }
53
54
            void operator()( const SpriteRedrawManager::SpriteChangeRecord& rSpriteRecord )
55
0
            {
56
                // only deal with change events from the currently
57
                // affected sprite
58
0
                if( rSpriteRecord.mpAffectedSprite != mpAffectedSprite )
59
0
                    return;
60
61
0
                switch( rSpriteRecord.meChangeType )
62
0
                {
63
0
                    case SpriteRedrawManager::SpriteChangeRecord::ChangeType::move:
64
0
                        if( !mbIsMove )
65
0
                        {
66
                            // no move yet - this must be the first one
67
0
                            maMoveStartArea = ::basegfx::B2DRectangle(
68
0
                                rSpriteRecord.maOldPos,
69
0
                                rSpriteRecord.maOldPos + rSpriteRecord.maUpdateArea.getRange() );
70
0
                            mbIsMove        = true;
71
0
                        }
72
73
0
                        maMoveEndArea   = rSpriteRecord.maUpdateArea;
74
0
                        break;
75
76
0
                    case SpriteRedrawManager::SpriteChangeRecord::ChangeType::update:
77
                        // update end update area of the
78
                        // sprite. Thus, every update() action
79
                        // _after_ the last move will correctly
80
                        // update the final repaint area. And this
81
                        // does not interfere with subsequent
82
                        // moves, because moves always perform a
83
                        // hard set of maMoveEndArea to their
84
                        // stored value
85
0
                        maMoveEndArea.expand( rSpriteRecord.maUpdateArea );
86
0
                        mbIsGenericUpdate = true;
87
0
                        break;
88
89
0
                    default:
90
0
                        ENSURE_OR_THROW( false,
91
0
                                          "Unexpected case in SpriteUpdater::operator()" );
92
0
                        break;
93
0
                }
94
0
            }
95
96
            void commit( SpriteRedrawManager::SpriteConnectedRanges& rUpdateCollector ) const
97
0
            {
98
0
                if( mbIsMove )
99
0
                {
100
0
                    if( !maMoveStartArea.isEmpty() ||
101
0
                        !maMoveEndArea.isEmpty() )
102
0
                    {
103
                        // if mbIsGenericUpdate is false, this is a
104
                        // pure move (i.e. no other update
105
                        // operations). Pass that information on to
106
                        // the SpriteInfo
107
0
                        const bool bIsPureMove( !mbIsGenericUpdate );
108
109
                        // ignore the case that start and end update
110
                        // area overlap - the b2dconnectedranges
111
                        // handle that, anyway. doing it this way
112
                        // ensures that we have both old and new area
113
                        // stored
114
115
                        // round all given range up to enclosing
116
                        // integer rectangle - since the whole thing
117
                        // here is about
118
119
                        // first, draw the new sprite position
120
0
                        rUpdateCollector.addRange(
121
0
                            ::basegfx::unotools::b2DSurroundingIntegerRangeFromB2DRange( maMoveEndArea ),
122
0
                            SpriteRedrawManager::SpriteInfo(
123
0
                                mpAffectedSprite,
124
0
                                maMoveEndArea,
125
0
                                true,
126
0
                                bIsPureMove ) );
127
128
                        // then, clear the old place (looks smoother
129
                        // this way)
130
0
                        rUpdateCollector.addRange(
131
0
                            ::basegfx::unotools::b2DSurroundingIntegerRangeFromB2DRange( maMoveStartArea ),
132
0
                            SpriteRedrawManager::SpriteInfo(
133
0
                                Sprite::Reference(),
134
0
                                maMoveStartArea,
135
0
                                true,
136
0
                                bIsPureMove ) );
137
0
                    }
138
0
                }
139
0
                else if( mbIsGenericUpdate &&
140
0
                         !maMoveEndArea.isEmpty() )
141
0
                {
142
0
                    rUpdateCollector.addRange(
143
0
                        ::basegfx::unotools::b2DSurroundingIntegerRangeFromB2DRange( maMoveEndArea ),
144
0
                        SpriteRedrawManager::SpriteInfo(
145
0
                            mpAffectedSprite,
146
0
                            maMoveEndArea,
147
0
                            true ) );
148
0
                }
149
0
            }
150
151
        private:
152
            Sprite::Reference       mpAffectedSprite;
153
            ::basegfx::B2DRectangle maMoveStartArea;
154
            ::basegfx::B2DRectangle maMoveEndArea;
155
156
            /// True, if at least one move was encountered
157
            bool                    mbIsMove;
158
159
            /// True, if at least one generic update was encountered
160
            bool                    mbIsGenericUpdate;
161
        };
162
163
164
        /** SpriteChecker functor, which for every sprite checks the
165
            given update vector for necessary screen updates
166
         */
167
        class SpriteUpdater
168
        {
169
        public:
170
            /** Generate update area list
171
172
                @param rUpdater
173
                Reference to an updater object, which will receive the
174
                update areas.
175
176
                @param rChangeContainer
177
                Container with all sprite change requests
178
179
             */
180
            SpriteUpdater( SpriteRedrawManager::SpriteConnectedRanges&          rUpdater,
181
                           const SpriteRedrawManager::VectorOfChangeRecords&    rChangeContainer ) :
182
0
                mrUpdater( rUpdater ),
183
0
                mrChangeContainer( rChangeContainer )
184
0
            {
185
0
            }
186
187
            /** Call this method for every sprite on your screen
188
189
                This method scans the change container, collecting all
190
                update info for the given sprite into one or two
191
                update operations, which in turn are inserted into the
192
                connected ranges processor.
193
194
                @param rSprite
195
                Current sprite to collect update info for.
196
             */
197
            void operator()( const Sprite::Reference& rSprite )
198
0
            {
199
0
                SpriteTracer aSpriteTracer( rSprite );
200
201
0
                for (auto const& aChange : mrChangeContainer)
202
0
                    aSpriteTracer( aChange );
203
204
0
                aSpriteTracer.commit( mrUpdater );
205
0
            }
206
207
        private:
208
            SpriteRedrawManager::SpriteConnectedRanges&         mrUpdater;
209
            const SpriteRedrawManager::VectorOfChangeRecords&   mrChangeContainer;
210
        };
211
    }
212
213
    void SpriteRedrawManager::setupUpdateAreas( SpriteConnectedRanges& rUpdateAreas ) const
214
0
    {
215
        // TODO(T3): This is NOT thread safe at all. This only works
216
        // under the assumption that NOBODY changes ANYTHING
217
        // concurrently, while this method is on the stack. We should
218
        // really rework the canvas::Sprite interface, in such a way
219
        // that it dumps ALL its state with a single, atomic
220
        // call. Then, we store that state locally. This prolly goes
221
        // in line with the problem of having sprite state available
222
        // for the frame before the last frame; plus, it avoids
223
        // frequent locks of the object mutexes
224
0
        SpriteWeakOrder aSpriteComparator;
225
226
        // put all sprites that have changed content into update areas
227
0
        for( const auto& pSprite : maSprites )
228
0
        {
229
0
            if( pSprite->isContentChanged() )
230
0
                const_cast< SpriteRedrawManager* >( this )->updateSprite( pSprite,
231
0
                                                                          pSprite->getPosPixel(),
232
0
                                                                          pSprite->getUpdateArea() );
233
0
        }
234
235
        // sort sprites after prio
236
0
        VectorOfSprites aSortedSpriteVector( maSprites.begin(), maSprites.end() );
237
0
        std::sort( aSortedSpriteVector.begin(),
238
0
                     aSortedSpriteVector.end(),
239
0
                     aSpriteComparator );
240
241
        // extract all referenced sprites from the maChangeRecords
242
        // (copy sprites, make the list unique, regarding the
243
        // sprite pointer). This assumes that, until this scope
244
        // ends, nobody changes the maChangeRecords vector!
245
0
        VectorOfSprites aUpdatableSprites;
246
0
        for( const auto& rChangeRecord : maChangeRecords )
247
0
        {
248
0
            const Sprite::Reference& rSprite( rChangeRecord.getSprite() );
249
0
            if( rSprite.is() )
250
0
                aUpdatableSprites.push_back( rSprite );
251
0
        }
252
253
0
        std::sort( aUpdatableSprites.begin(),
254
0
                     aUpdatableSprites.end(),
255
0
                     aSpriteComparator );
256
257
0
        VectorOfSprites::iterator aEnd=
258
0
            std::unique( aUpdatableSprites.begin(),
259
0
                           aUpdatableSprites.end() );
260
261
        // for each unique sprite, check the change event vector,
262
        // calculate the update operation from that, and add the
263
        // result to the aUpdateArea.
264
0
        std::for_each( aUpdatableSprites.begin(),
265
0
                         aEnd,
266
0
                         SpriteUpdater( rUpdateAreas,
267
0
                                        maChangeRecords) );
268
269
        // TODO(P2): Implement your own output iterator adapter, to
270
        // avoid that totally superfluous temp aUnchangedSprites
271
        // vector.
272
273
        // add all sprites to rUpdateAreas, that are _not_ already
274
        // contained in the uniquified vector of changed ones
275
        // (i.e. the difference between aSortedSpriteVector and
276
        // aUpdatableSprites).
277
0
        VectorOfSprites aUnchangedSprites;
278
0
        std::set_difference( aSortedSpriteVector.begin(),
279
0
                               aSortedSpriteVector.end(),
280
0
                               aUpdatableSprites.begin(),
281
0
                               aEnd,
282
0
                               std::back_insert_iterator< VectorOfSprites >(aUnchangedSprites),
283
0
                               aSpriteComparator );
284
285
        // add each remaining unchanged sprite to connected ranges,
286
        // marked as "don't need update"
287
0
        for( const auto& pUnchangedSprite : aUnchangedSprites )
288
0
        {
289
0
            const ::basegfx::B2DRange aUpdateArea( pUnchangedSprite->getUpdateArea() );
290
0
            rUpdateAreas.addRange(
291
0
                ::basegfx::unotools::b2DSurroundingIntegerRangeFromB2DRange( aUpdateArea ),
292
0
                SpriteInfo( pUnchangedSprite,
293
0
                            aUpdateArea,
294
0
                            false ) );
295
0
        }
296
0
    }
297
298
#if OSL_DEBUG_LEVEL > 0
299
    static bool impIsEqualB2DRange(const basegfx::B2DRange& rRangeA, const basegfx::B2DRange& rRangeB, double fSmallValue)
300
    {
301
        return fabs(rRangeB.getMinX() - rRangeA.getMinX()) <= fSmallValue
302
            && fabs(rRangeB.getMinY() - rRangeA.getMinY()) <= fSmallValue
303
            && fabs(rRangeB.getMaxX() - rRangeA.getMaxX()) <= fSmallValue
304
            && fabs(rRangeB.getMaxY() - rRangeA.getMaxY()) <= fSmallValue;
305
    }
306
307
    static bool impIsEqualB2DVector(const basegfx::B2DVector& rVecA, const basegfx::B2DVector& rVecB, double fSmallValue)
308
    {
309
        return fabs(rVecB.getX() - rVecA.getX()) <= fSmallValue
310
            && fabs(rVecB.getY() - rVecA.getY()) <= fSmallValue;
311
    }
312
#endif
313
314
    bool SpriteRedrawManager::isAreaUpdateScroll( ::basegfx::B2DRectangle&  o_rMoveStart,
315
                                                  ::basegfx::B2DRectangle&  o_rMoveEnd,
316
                                                  const UpdateArea&         rUpdateArea,
317
                                                  std::size_t             nNumSprites ) const
318
0
    {
319
        // check for a solitary move, which consists of exactly two
320
        // pure-move entries, the first with valid, the second with
321
        // invalid sprite (see SpriteTracer::commit()).  Note that we
322
        // cannot simply store some flag in SpriteTracer::commit()
323
        // above and just check that here, since during the connected
324
        // range calculations, other sprites might get merged into the
325
        // same region (thus spoiling the scrolling move
326
        // optimization).
327
0
        if( nNumSprites != 2 )
328
0
            return false;
329
330
0
        const SpriteConnectedRanges::ComponentListType::const_iterator aFirst(
331
0
            rUpdateArea.maComponentList.begin() );
332
0
        SpriteConnectedRanges::ComponentListType::const_iterator aSecond(
333
0
            aFirst );
334
0
        ++aSecond;
335
336
0
        if( !aFirst->second.isPureMove() ||
337
0
            !aSecond->second.isPureMove() ||
338
0
            !aFirst->second.getSprite().is() ||
339
            // use _true_ update area, not the rounded version
340
0
            !aFirst->second.getSprite()->isAreaUpdateOpaque( aFirst->second.getUpdateArea() ) ||
341
0
            aSecond->second.getSprite().is() )
342
0
        {
343
            // either no move update, or incorrect sprite, or sprite
344
            // content not fully opaque over update region.
345
0
            return false;
346
0
        }
347
348
0
        o_rMoveStart      = aSecond->second.getUpdateArea();
349
0
        o_rMoveEnd        = aFirst->second.getUpdateArea();
350
351
#if OSL_DEBUG_LEVEL > 0
352
        ::basegfx::B2DRectangle aTotalBounds( o_rMoveStart );
353
        aTotalBounds.expand( o_rMoveEnd );
354
355
        SAL_WARN_IF(!impIsEqualB2DRange(rUpdateArea.maTotalBounds, basegfx::unotools::b2DSurroundingIntegerRangeFromB2DRange(aTotalBounds), 0.5),
356
            "canvas",
357
            "SpriteRedrawManager::isAreaUpdateScroll(): sprite area and total area mismatch");
358
        SAL_WARN_IF(!impIsEqualB2DVector(o_rMoveStart.getRange(), o_rMoveEnd.getRange(), 0.5),
359
            "canvas",
360
            "SpriteRedrawManager::isAreaUpdateScroll(): scroll start and end area have mismatching size");
361
#endif
362
363
0
        return true;
364
0
    }
365
366
    bool SpriteRedrawManager::isAreaUpdateNotOpaque( const ::basegfx::B2DRectangle& rUpdateRect,
367
                                                     const AreaComponent&           rComponent ) const
368
0
    {
369
0
        const Sprite::Reference& pAffectedSprite( rComponent.second.getSprite() );
370
371
0
        if( !pAffectedSprite.is() )
372
0
            return true; // no sprite, no opaque update!
373
374
0
        return !pAffectedSprite->isAreaUpdateOpaque( rUpdateRect );
375
0
    }
376
377
    bool SpriteRedrawManager::isAreaUpdateOpaque( const UpdateArea& rUpdateArea,
378
                                                  std::size_t     nNumSprites ) const
379
0
    {
380
        // check whether the sprites in the update area's list will
381
        // fully cover the given area _and_ do that in an opaque way
382
        // (i.e. no alpha, no non-rectangular sprite content).
383
384
        // TODO(P1): Come up with a smarter early-exit criterion here
385
        // (though, I think, the case that _lots_ of sprites _fully_
386
        // cover a rectangular area _without_ any holes is extremely
387
        // improbable)
388
389
        // avoid checking large number of sprites (and probably fail,
390
        // anyway). Note: the case nNumSprites < 1 should normally not
391
        // happen, as handleArea() calls backgroundPaint() then.
392
0
        if( nNumSprites > 3 || nNumSprites < 1 )
393
0
            return false;
394
395
        // now, calc the _true_ update area, by merging all sprite's
396
        // true update areas into one rectangle
397
0
        ::basegfx::B2DRange aTrueArea( rUpdateArea.maComponentList.begin()->second.getUpdateArea() );
398
0
        for( const auto& rArea : rUpdateArea.maComponentList )
399
0
            aTrueArea.expand(rArea.second.getUpdateArea());
400
401
0
        const SpriteConnectedRanges::ComponentListType::const_iterator aEnd(
402
0
            rUpdateArea.maComponentList.end() );
403
404
        // and check whether _any_ of the sprites tells that its area
405
        // update will not be opaque.
406
0
        return std::none_of( rUpdateArea.maComponentList.begin(),
407
0
                                aEnd,
408
0
                                [&aTrueArea, this]( const std::pair< ::basegfx::B2DRange, SpriteInfo >& cp )
409
0
                                { return this->isAreaUpdateNotOpaque(aTrueArea, cp); } );
410
0
    }
411
412
    bool SpriteRedrawManager::areSpritesChanged( const UpdateArea& rUpdateArea ) const
413
0
    {
414
        // check whether SpriteInfo::needsUpdate returns false for
415
        // all elements of this area's contained sprites
416
417
        // if not a single changed sprite found - just ignore this
418
        // component (return false)
419
0
        const SpriteConnectedRanges::ComponentListType::const_iterator aEnd(
420
0
            rUpdateArea.maComponentList.end() );
421
0
        return std::any_of( rUpdateArea.maComponentList.begin(),
422
0
                                aEnd,
423
0
                                []( const std::pair< ::basegfx::B2DRange, SpriteInfo >& cp )
424
0
                                { return cp.second.needsUpdate(); } );
425
0
    }
426
427
    SpriteRedrawManager::SpriteRedrawManager()
428
0
    {
429
0
    }
430
431
    void SpriteRedrawManager::disposing()
432
0
    {
433
        // drop all references
434
0
        maChangeRecords.clear();
435
436
        // dispose all sprites - the spritecanvas, and by delegation,
437
        // this object, is the owner of the sprites. After all, a
438
        // sprite without a canvas to render into makes not terribly
439
        // much sense.
440
0
        for( const auto& rCurr : boost::adaptors::reverse(maSprites) )
441
0
            rCurr->dispose();
442
443
0
        maSprites.clear();
444
0
    }
445
446
    void SpriteRedrawManager::clearChangeRecords()
447
0
    {
448
0
        maChangeRecords.clear();
449
0
    }
450
451
    void SpriteRedrawManager::showSprite( const Sprite::Reference& rSprite )
452
0
    {
453
0
        maSprites.push_back( rSprite );
454
0
    }
455
456
    void SpriteRedrawManager::hideSprite( const Sprite::Reference& rSprite )
457
0
    {
458
0
        std::erase(maSprites, rSprite);
459
0
    }
460
461
    void SpriteRedrawManager::moveSprite( const Sprite::Reference&      rSprite,
462
                                          const ::basegfx::B2DPoint&    rOldPos,
463
                                          const ::basegfx::B2DPoint&    rNewPos,
464
                                          const ::basegfx::B2DVector&   rSpriteSize )
465
0
    {
466
0
        maChangeRecords.emplace_back( rSprite,
467
0
                                      rOldPos,
468
0
                                      rNewPos,
469
0
                                      rSpriteSize );
470
0
    }
471
472
    void SpriteRedrawManager::updateSprite( const Sprite::Reference&    rSprite,
473
                                            const ::basegfx::B2DPoint&  rPos,
474
                                            const ::basegfx::B2DRange&  rUpdateArea )
475
0
    {
476
0
        maChangeRecords.emplace_back( rSprite,
477
0
                                      rPos,
478
0
                                      rUpdateArea );
479
0
    }
480
481
}
482
483
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */