mirror of
https://github.com/thorvg/thorvg.git
synced 2025-06-19 14:31:39 +00:00
renderer: add partial rendering support
Partial Rendering refers to a rendering technique where only a portion of the scene or screen is updated, rather than redrawing the entire output. It is commonly used as a performance optimization strategy, focusing on redrawing only the regions that have changed, often called dirty regions. This introduces RenderDirtyRegion, which assists in collecting a compact dirty region from render tasks. Each backend can utilize this class to support efficient partial rendering. This is implemented using a Line Sweep and Subdivision Merging O(NlogN). The basic per-frame workflow is as follows: 1. RenderDirtyRegion::prepare() //Call this in Renderer::preRender(). 2. RenderDirtyRegion::add() //Add all dirty paints for the frame before rendering. 3. RenderDirtyRegion::commit() //Generate the partial rendering region list before rendering. 4. RenderDirtyRegion::get() //Retrieve the current dirty region list and use it when drawing paints. 5. RenderDirtyRegion::clear() //Reset the state. also removed the skip() from paint since engines need to capture the drawable object in any case. issue: https://github.com/thorvg/thorvg/issues/1747
This commit is contained in:
parent
f6bfeda4fa
commit
a005ef2811
5 changed files with 226 additions and 7 deletions
|
@ -144,6 +144,11 @@ namespace tvg
|
|||
return (renderFlag & flag) ? true : false;
|
||||
}
|
||||
|
||||
void damage()
|
||||
{
|
||||
if (renderer) renderer->damage(bounds(renderer));
|
||||
}
|
||||
|
||||
void mark(RenderUpdateFlag flag)
|
||||
{
|
||||
renderFlag |= flag;
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include "tvgMath.h"
|
||||
#include "tvgRender.h"
|
||||
|
||||
|
@ -54,6 +55,18 @@ bool RenderMethod::viewport(const RenderRegion& vp)
|
|||
}
|
||||
|
||||
|
||||
void RenderMethod::damage(const RenderRegion& region)
|
||||
{
|
||||
dirtyRegion.add(region);
|
||||
}
|
||||
|
||||
|
||||
bool RenderMethod::partial(bool disable)
|
||||
{
|
||||
std::swap(dirtyRegion.disabled, disable);
|
||||
return disable;
|
||||
}
|
||||
|
||||
/************************************************************************/
|
||||
/* RenderPath Class Implementation */
|
||||
/************************************************************************/
|
||||
|
@ -130,6 +143,130 @@ void RenderRegion::intersect(const RenderRegion& rhs)
|
|||
if (max.y < min.y) max.y = min.y;
|
||||
}
|
||||
|
||||
|
||||
void RenderDirtyRegion::subdivide(Array<RenderRegion>& targets, uint32_t idx, RenderRegion& lhs, RenderRegion& rhs)
|
||||
{
|
||||
RenderRegion temp[5];
|
||||
int cnt = 0;
|
||||
temp[cnt++] = RenderRegion::intersect(lhs, rhs);
|
||||
auto max = std::min(lhs.max.x, rhs.max.x);
|
||||
|
||||
auto subtract = [&](RenderRegion& lhs, RenderRegion& rhs) {
|
||||
//top
|
||||
if (rhs.min.y < lhs.min.y) {
|
||||
temp[cnt++] = {{rhs.min.x, rhs.min.y}, {rhs.max.x, lhs.min.y}};
|
||||
rhs.min.y = lhs.min.y;
|
||||
}
|
||||
//bottom
|
||||
if (rhs.max.y > lhs.max.y) {
|
||||
temp[cnt++] = {{rhs.min.x, lhs.max.y}, {rhs.max.x, rhs.max.y}};
|
||||
rhs.max.y = lhs.max.y;
|
||||
}
|
||||
//left
|
||||
if (rhs.min.x < lhs.min.x) {
|
||||
temp[cnt++] = {{rhs.min.x, rhs.min.y}, {lhs.min.x, rhs.max.y}};
|
||||
rhs.min.x = lhs.min.x;
|
||||
}
|
||||
//right
|
||||
if (rhs.max.x > lhs.max.x) {
|
||||
temp[cnt++] = {{lhs.max.x, rhs.min.y}, {rhs.max.x, rhs.max.y}};
|
||||
//rhs.max.x = lhs.max.x;
|
||||
}
|
||||
};
|
||||
|
||||
subtract(temp[0], lhs);
|
||||
subtract(temp[0], rhs);
|
||||
|
||||
//TODO: remove this
|
||||
if (targets.reserved < targets.count + cnt - 1) {
|
||||
TVGERR("RENDERER", "reserved: %d, required: %d (+%d)\n", targets.reserved, targets.count + cnt - 1, cnt - 1);
|
||||
abort();
|
||||
}
|
||||
|
||||
/* Note: We considered using a list to avoid memory shifting,
|
||||
but ultimately, the array outperformed the list due to better cache locality. */
|
||||
|
||||
//shift data
|
||||
auto dst = &targets[idx + cnt];
|
||||
memmove(dst, &targets[idx + 1], sizeof(RenderRegion) * (targets.count - idx - 1));
|
||||
memcpy(&targets[idx], temp, sizeof(RenderRegion) * cnt);
|
||||
targets.count += (cnt - 1);
|
||||
|
||||
//sorting by x coord again, only for the updated region
|
||||
while (dst < targets.end() && dst->min.x < max) ++dst;
|
||||
stable_sort(&targets[idx], dst, [](const RenderRegion& a, const RenderRegion& b) -> bool {
|
||||
return a.min.x < b.min.x;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void RenderDirtyRegion::commit()
|
||||
{
|
||||
if (disabled) return;
|
||||
|
||||
auto& targets = list[current];
|
||||
if (targets.empty()) return;
|
||||
|
||||
current = !current; //swapping buffers
|
||||
|
||||
auto& output = list[current];
|
||||
|
||||
//sorting by x coord. guarantee the stable performance: O(NlogN)
|
||||
stable_sort(targets.begin(), targets.end(), [](const RenderRegion& a, const RenderRegion& b) -> bool {
|
||||
return a.min.x < b.min.x;
|
||||
});
|
||||
|
||||
//Optimized using sweep-line algorithm: O(NlogN)
|
||||
for (uint32_t i = 0; i < targets.count; ++i) {
|
||||
auto& lhs = targets[i];
|
||||
if (lhs.invalid()) continue;
|
||||
auto merged = false;
|
||||
|
||||
for (uint32_t j = i + 1; j < targets.count; ++j) {
|
||||
auto& rhs = targets[j];
|
||||
if (rhs.invalid()) continue;
|
||||
if (lhs.max.x < rhs.min.x) break; //line sweeping
|
||||
|
||||
//fully overlapped. drop lhs
|
||||
if (rhs.contained(lhs)) {
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
//fully overlapped. replace the lhs with rhs
|
||||
if (lhs.contained(rhs)) {
|
||||
rhs = {};
|
||||
continue;
|
||||
}
|
||||
//just merge & expand on x axis
|
||||
if (lhs.min.y == rhs.min.y && lhs.max.y == rhs.max.y) {
|
||||
if (lhs.min.x <= rhs.max.x && rhs.min.x <= lhs.max.x) {
|
||||
rhs.min.x = std::min(lhs.min.x, rhs.min.x);
|
||||
rhs.max.x = std::max(lhs.max.x, rhs.max.x);
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//just merge & expand on y axis
|
||||
if (lhs.min.x == rhs.min.x && lhs.max.x == rhs.max.x) {
|
||||
if (lhs.min.y <= rhs.max.y && rhs.min.y < lhs.max.y) {
|
||||
rhs.min.y = std::min(lhs.min.y, rhs.min.y);
|
||||
rhs.max.y = std::max(lhs.max.y, rhs.max.y);
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
//subdivide regions
|
||||
if (lhs.intersected(rhs)) {
|
||||
subdivide(targets, j, lhs, rhs);
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!merged) output.push(lhs); //this region is complete isolated
|
||||
lhs = {};
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************/
|
||||
/* RenderTrimPath Class Implementation */
|
||||
/************************************************************************/
|
||||
|
|
|
@ -50,7 +50,6 @@ static inline RenderUpdateFlag operator|(const RenderUpdateFlag a, const RenderU
|
|||
return RenderUpdateFlag(uint16_t(a) | uint16_t(b));
|
||||
}
|
||||
|
||||
|
||||
struct RenderSurface
|
||||
{
|
||||
union {
|
||||
|
@ -111,6 +110,11 @@ struct RenderRegion
|
|||
return ret;
|
||||
}
|
||||
|
||||
static constexpr RenderRegion add(const RenderRegion& lhs, const RenderRegion& rhs)
|
||||
{
|
||||
return {{std::min(lhs.min.x, rhs.min.x), std::min(lhs.min.y, rhs.min.y)}, {std::max(lhs.max.x, rhs.max.x), std::max(lhs.max.y, rhs.max.y)}};
|
||||
}
|
||||
|
||||
void intersect(const RenderRegion& rhs);
|
||||
|
||||
void add(const RenderRegion& rhs)
|
||||
|
@ -121,6 +125,16 @@ struct RenderRegion
|
|||
if (rhs.max.y > max.y) max.y = rhs.max.y;
|
||||
}
|
||||
|
||||
bool contained(const RenderRegion& rhs)
|
||||
{
|
||||
return (min.x <= rhs.min.x && max.x >= rhs.max.x && min.y <= rhs.min.y && max.y >= rhs.max.y);
|
||||
}
|
||||
|
||||
bool intersected(const RenderRegion& rhs) const
|
||||
{
|
||||
return (rhs.min.x < max.x && rhs.max.x > min.x && rhs.min.y < max.y && rhs.max.y > min.y);
|
||||
}
|
||||
|
||||
bool operator==(const RenderRegion& rhs) const
|
||||
{
|
||||
return (min.x == rhs.min.x && min.y == rhs.min.y && max.x == rhs.max.x && max.y == rhs.max.y);
|
||||
|
@ -141,6 +155,56 @@ struct RenderRegion
|
|||
uint32_t h() const { return (uint32_t) sh(); }
|
||||
};
|
||||
|
||||
struct RenderDirtyRegion
|
||||
{
|
||||
void add(const RenderRegion& region)
|
||||
{
|
||||
if (disabled || region.invalid()) return;
|
||||
ScopedLock lock(key);
|
||||
list[current].push(region);
|
||||
}
|
||||
|
||||
bool prepare(uint32_t count = 0)
|
||||
{
|
||||
if (disabled) return false;
|
||||
|
||||
count *= 120; //FIXME: enough?
|
||||
|
||||
list[0].reserve(count);
|
||||
list[1].reserve(count);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool deactivated()
|
||||
{
|
||||
if (disabled) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
list[0].clear();
|
||||
list[1].clear();
|
||||
}
|
||||
|
||||
const Array<RenderRegion>& get()
|
||||
{
|
||||
return list[current];
|
||||
}
|
||||
|
||||
void commit();
|
||||
|
||||
bool disabled = false;
|
||||
|
||||
private:
|
||||
void subdivide(Array<RenderRegion>& targets, uint32_t idx, RenderRegion& lhs, RenderRegion& rhs);
|
||||
|
||||
Array<RenderRegion> list[2]; //double buffer swapping
|
||||
Key key;
|
||||
uint8_t current = 0; //list index. 0 or 1
|
||||
};
|
||||
|
||||
struct RenderPath
|
||||
{
|
||||
Array<PathCommand> cmds;
|
||||
|
@ -420,11 +484,12 @@ struct RenderEffectTritone : RenderEffect
|
|||
class RenderMethod
|
||||
{
|
||||
private:
|
||||
uint32_t refCnt = 0; //reference count
|
||||
uint32_t refCnt = 0;
|
||||
Key key;
|
||||
|
||||
protected:
|
||||
RenderRegion vport; //viewport
|
||||
RenderDirtyRegion dirtyRegion;
|
||||
|
||||
public:
|
||||
//common implementation
|
||||
|
@ -448,11 +513,10 @@ public:
|
|||
virtual bool blend(BlendMethod method) = 0;
|
||||
virtual ColorSpace colorSpace() = 0;
|
||||
virtual const RenderSurface* mainSurface() = 0;
|
||||
|
||||
virtual bool clear() = 0;
|
||||
virtual bool sync() = 0;
|
||||
|
||||
//compositions
|
||||
//composition
|
||||
virtual RenderCompositor* target(const RenderRegion& region, ColorSpace cs, CompositionFlag flags) = 0;
|
||||
virtual bool beginComposite(RenderCompositor* cmp, MaskMethod method, uint8_t opacity) = 0;
|
||||
virtual bool endComposite(RenderCompositor* cmp) = 0;
|
||||
|
@ -462,6 +526,10 @@ public:
|
|||
virtual bool region(RenderEffect* effect) = 0;
|
||||
virtual bool render(RenderCompositor* cmp, const RenderEffect* effect, bool direct) = 0;
|
||||
virtual void dispose(RenderEffect* effect) = 0;
|
||||
|
||||
//partial rendering
|
||||
virtual void damage(const RenderRegion& region);
|
||||
virtual bool partial(bool disable);
|
||||
};
|
||||
|
||||
static inline bool MASK_REGION_MERGING(MaskMethod method)
|
||||
|
|
|
@ -131,6 +131,9 @@ struct SceneImpl : Scene
|
|||
vport = renderer->viewport();
|
||||
vdirty = true;
|
||||
|
||||
//bounds(renderer) here hinders parallelization.
|
||||
if (effects) renderer->damage(vport);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -257,7 +260,10 @@ struct SceneImpl : Scene
|
|||
{
|
||||
auto itr = paints.begin();
|
||||
while (itr != paints.end()) {
|
||||
PAINT((*itr))->unref();
|
||||
auto paint = PAINT((*itr));
|
||||
//when the paint is destroyed damage will be triggered
|
||||
if (paint->refCnt > 1) paint->damage();
|
||||
paint->unref();
|
||||
paints.erase(itr++);
|
||||
}
|
||||
return Result::Success;
|
||||
|
@ -266,6 +272,8 @@ struct SceneImpl : Scene
|
|||
Result remove(Paint* paint)
|
||||
{
|
||||
if (PAINT(paint)->parent != this) return Result::InsufficientCondition;
|
||||
//when the paint is destroyed damage will be triggered
|
||||
if (PAINT(paint)->refCnt > 1) PAINT(paint)->damage();
|
||||
PAINT(paint)->unref();
|
||||
paints.remove(paint);
|
||||
return Result::Success;
|
||||
|
@ -310,6 +318,7 @@ struct SceneImpl : Scene
|
|||
}
|
||||
delete(effects);
|
||||
effects = nullptr;
|
||||
impl.renderer->damage(vport);
|
||||
}
|
||||
return Result::Success;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue