librsvg source for verification 2026-05-22
This commit is contained in:
352
devel-docs/render_tree.rst
Normal file
352
devel-docs/render_tree.rst
Normal file
@@ -0,0 +1,352 @@
|
||||
Render tree
|
||||
===========
|
||||
|
||||
For historical reasons, librsvg's code flow during rendering is as
|
||||
follows. The rendering code traverses the SVG tree of elements, and
|
||||
for each one, its ``::draw()`` method is called; its signature looks
|
||||
like this (some arguments omitted):
|
||||
|
||||
.. code-block:: rust
|
||||
|
||||
pub fn draw(
|
||||
&self,
|
||||
...
|
||||
draw_ctx: &mut DrawingCtx,
|
||||
) -> Result<BoundingBox, RenderingError> { ... }
|
||||
|
||||
The draw() methods perform the actual rendering as side effects on the
|
||||
``draw_ctx``, and return a ``BoundingBox``. That is, the bounding box of
|
||||
an element is computed at the same time that it is rendered. This is
|
||||
suboptimal for several reasons:
|
||||
|
||||
- Many things that happen during rendering depend on knowing the
|
||||
bounding box. For example, gradients, patterns, and filters with
|
||||
units set to ``objectBoundingBox`` need to know the bounds. The
|
||||
rendering code in drawing_ctx.rs is cluttered because it must
|
||||
resolve bounding boxes very late.
|
||||
|
||||
- This is especially problematic for filters, since a Cairo surface
|
||||
needs to be created *before* rendering, and that surface should have
|
||||
a size relative to the bounding box of the element being filtered!
|
||||
:issue:`Bug #1 <1>` is precisely about this: librsvg instead creates
|
||||
a temporary surface as big as the document's toplevel viewport and filters
|
||||
it, but this doesn't work well for filters like Gaussian blur that should
|
||||
actually reference pixels outside of the document's area (think of a
|
||||
shape that extends past the document's area, which then gets
|
||||
blurred).
|
||||
|
||||
- The way for an element to signal that it is not drawable
|
||||
(e.g. ``<defs>`` is by returning an empty bounding box and not
|
||||
rendering anything. This is awkward.
|
||||
|
||||
- When rendering to a temporary surface for filtering or masking,
|
||||
there is a set of affine transformations that needs to be maintained
|
||||
carefully: an affine for the clipping path outside the temporary
|
||||
surface, an affine for drawing inside the surface, an affine to
|
||||
composite the surface into the final result. This is hard to
|
||||
understand and hard to test.
|
||||
|
||||
These problems can be solved by having a **render tree**.
|
||||
|
||||
What is a render tree?
|
||||
----------------------
|
||||
|
||||
As of 2022/Oct/06, librsvg does not compute a render tree data
|
||||
structure prior to rendering. Instead, in a very 2000s fashion, it
|
||||
walks the tree of elements and calls a ``.draw()`` method for each
|
||||
one. Each element then calls whatever methods it needs from
|
||||
``DrawingCtx`` to draw itself. Elements which don't produce graphical
|
||||
output (e.g. ``<defs>`` or ``<marker>``) simply have an empty
|
||||
``draw()`` method.
|
||||
|
||||
Over time we have been refactoring that in the direction of actually
|
||||
being able to produce a render tree. What would that look like?
|
||||
Consider an SVG document like this:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
||||
<defs>
|
||||
<rect id="TheRect" x="10" y="10" width="20" height="20" fill="blue"/>
|
||||
</defs>
|
||||
|
||||
<g>
|
||||
<use href="#TheRect" stroke="red" stroke-width="2"/>
|
||||
|
||||
<circle cx="50" cy="50" r="20" fill="yellow"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
A render tree would be a list of nested instructions like this:
|
||||
|
||||
::
|
||||
|
||||
group { # refers to the toplevel SVG
|
||||
width: 100
|
||||
height: 100
|
||||
establishes_viewport: true # because it is an <svg> element
|
||||
|
||||
children {
|
||||
group { # refers to the <g>
|
||||
establishes_viewport: false # because it is a simple <g>
|
||||
|
||||
children {
|
||||
shape {
|
||||
path="the <rect> above but resolved to path commands"
|
||||
|
||||
# note how the following is the cascaded style and the <use> semantics
|
||||
fill: blue
|
||||
stroke: red
|
||||
stroke-width: 2
|
||||
}
|
||||
|
||||
shape {
|
||||
path="the <circle> above but resolved to path commands"
|
||||
|
||||
fill: yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
That is, we take the high-level SVG instructions and "lower" them to a
|
||||
few possible drawing primitives like path-based shapes that can be
|
||||
grouped. All the primitives have everything that is needed to draw
|
||||
them, like their set of computed values for styles, and their
|
||||
coordinates resolved to their user-space coordinate system.
|
||||
|
||||
Browser engines produce render trees more or less similar to the above
|
||||
(they don't always call them that), and get various benefits:
|
||||
|
||||
- The various recursively-nested subtrees can be rendered concurrently.
|
||||
|
||||
- Having low-level primitives makes it easier to switch to another
|
||||
rendering engine in the future.
|
||||
|
||||
- The tree can be re-rendered without recomputation, or subtrees can
|
||||
be recomputed efficiently if e.g. an animated element changes a few
|
||||
of its properties.
|
||||
|
||||
Why did librsvg not do that since the beginning?
|
||||
------------------------------------------------
|
||||
|
||||
Librsvg was originally written in the early 2000s, when several things
|
||||
were happening at the same time:
|
||||
|
||||
- libxml2 (one of the early widely-available parsers for XML) had
|
||||
recently gotten a SAX API for parsing XML. This lets an application
|
||||
stream in the parsed XML elements and process them one by one,
|
||||
without having to build a tree of elements+attributes first. In
|
||||
those days, memory was at a premium and "not producing a tree" was
|
||||
seen as beneficial.
|
||||
|
||||
- The SVG spec itself was being written, and it did not have all of
|
||||
the features we know now. In particular, maybe at some point it
|
||||
didn't have elements that worked by referencing others, like
|
||||
``<use>`` or ``<filter>``. The CSS cascade could be done on the fly
|
||||
for the XML elements being streamed in, and one could emit rendering
|
||||
commands for each element to produce the final result.
|
||||
|
||||
That is, at that time, it was indeed feasible to do this: stream in
|
||||
parsed XML elements one by one as produced by libxml2, and for each
|
||||
element, compute its CSS cascade and render it.
|
||||
|
||||
This scheme probably stopped working at some point when SVG got
|
||||
features that allowed referencing elements that have not been declared
|
||||
yet (think of ``<use href="#foo"/>`` but with the ``<defs> <path
|
||||
id="foo" .../> </defs>`` declared until later in the document). Or
|
||||
elements that referenced others, like ``<rect filter="url(#blah)">``.
|
||||
In both cases, one needs to actually build an in-memory tree of parsed
|
||||
elements, and *then* resolve the references between them.
|
||||
|
||||
That is where much of the complexity of librsvg's code flow comes from:
|
||||
|
||||
- ``AcquiredNodes`` is the thing that resolves references when needed.
|
||||
It also detects reference cycles, which are an error.
|
||||
|
||||
- ``ComputedValues`` often get resolved until pretty late, by passing
|
||||
the ``CascadedValues`` state down to children as they are drawn.
|
||||
|
||||
- ``DrawingCtx`` was originally a giant ball of mutable state, but we
|
||||
have been whittling it down and moving part of that state elsewhere.
|
||||
|
||||
|
||||
Summary of the SVG rendering model
|
||||
----------------------------------
|
||||
|
||||
In the SVG2 spec, this has been offloaded to the "`Order of graphical
|
||||
operations
|
||||
<https://www.w3.org/TR/compositing/#compositingandblendingorder>`_"
|
||||
section of the Compositing and Blending Level 1 spec. Once the render
|
||||
tree is resolved, each node is painted like this, conceptually to a
|
||||
transparent, temporary surface:
|
||||
|
||||
- Paint the shape/text/etc.
|
||||
- Filters.
|
||||
- Clip paths.
|
||||
- Masks.
|
||||
- Blend/composite the temporary surface onto the result.
|
||||
|
||||
The most critical function in librsvg is probably
|
||||
:internals:struct-method:`rsvg::drawing_ctx::DrawingCtx::with_discrete_layer`;
|
||||
it implements this drawing model.
|
||||
|
||||
Current state (2023/03/30)
|
||||
--------------------------
|
||||
|
||||
``layout.rs`` has the beginnings of the render tree. It's probably mis-named? It contains this:
|
||||
|
||||
- A ``LayerKind`` with primitives for path-based shapes, text, and images.
|
||||
|
||||
- A `stacking context
|
||||
<https://www.w3.org/TR/SVG2/render.html#EstablishingStackingContex>`_,
|
||||
which indicates each layer's opacity/clip/mask/filters.
|
||||
|
||||
- A ``Layer`` which composes the previous two. The ``StackingContext``
|
||||
provides the compositing/masking/filtering parameters, while the
|
||||
``LayerKind`` determines the primitive contents of the layer.
|
||||
|
||||
- Various ancillary structures that try to have only user-space
|
||||
coordinates (e.g. a number of CSS pixels instead of ``5cm``) and no
|
||||
references to other things.
|
||||
|
||||
The last point is not yet fully realized. For example,
|
||||
``StackingContext.clip_in_user_space`` has a reference to an element,
|
||||
which will be used as the clip path — that one needs to be normalized
|
||||
to user-space coordinates in the end. Also,
|
||||
``StackingContext.filter`` is a filter list as parsed from the SVG,
|
||||
not a ``FilterSpec`` that has been resolved to user space.
|
||||
|
||||
It would be good to resolve everything as early as possible to allow
|
||||
lowering concepts to their final renderable form. Whenever we have
|
||||
done this via refactoring, it has simplified the code closer to the
|
||||
actual rendering via Cairo.
|
||||
|
||||
Major subprojects
|
||||
-----------------
|
||||
|
||||
Path based shapes (``layout::Shape``) and text primitives
|
||||
(``layout::Text``) are almost done. The only missing thing for shapes
|
||||
would be to "explode" their markers into the actual primitives that
|
||||
would be rendered for them. However...
|
||||
|
||||
There is no primitive for groups yet. Every SVG element that allows
|
||||
renderable children must produce a group primitive of some sort:
|
||||
``svg``, ``g``, ``use``, ``marker``, etc. Among those, ``use`` and
|
||||
``marker`` are especially interesting since they must explode their
|
||||
referenced subtree into a shadow DOM, which librsvg doesn't support
|
||||
yet for CSS cascading purposes (the reference subtree gets rendered
|
||||
properly, but the full semantics of shadow DOM are not implemented
|
||||
yet).
|
||||
|
||||
Elements that establish a viewport (``svg``, ``symbol``, ``image``,
|
||||
``marker``, ``pattern``) need to carry information about this
|
||||
viewport, which is a ``viewBox`` plus ``preserveAspectRatio`` and
|
||||
``overflow``. See :issue:`298` for a somewhat obsolete description
|
||||
of the refactoring work needed to unify this logic.
|
||||
|
||||
The ``layout::StackingContext`` struct should contain another field,
|
||||
probably called ``layer``, with something like this:
|
||||
|
||||
.. code-block:: rust
|
||||
|
||||
struct StackingContext {
|
||||
// ... all its current fields
|
||||
|
||||
layer: Layer
|
||||
}
|
||||
|
||||
enum Layer {
|
||||
Shape(Box<Shape>),
|
||||
Text(Box<Text>),
|
||||
StackingContext(Box<StackingContext>)
|
||||
}
|
||||
|
||||
That is, every stacking context should contain the thing that it will
|
||||
draw, and that thing may be a shape/text or another stacking context!
|
||||
|
||||
Bounding boxes
|
||||
--------------
|
||||
|
||||
SVG depends on the ``objectBoundingBox`` of an element in many places:
|
||||
to resolve a gradient's or pattern's units, to determine the size of
|
||||
masks and clips, to determine the size of the filter region.
|
||||
|
||||
The current big bug to solve is :issue:`778`, which requires
|
||||
knowing the ``objectBoundingBox`` of an element **before** rendering
|
||||
it, so that a temporary surface of the appropriate size can be created
|
||||
for rendering the element if it has isolated opacity or masks/filters.
|
||||
Currently librsvg creates a temporary surface with the size and
|
||||
position of the toplevel viewport, and this is wrong for shapes that
|
||||
fall outside the viewport.
|
||||
|
||||
The problem is that librsvg computes bounding boxes at the time of
|
||||
rendering, not before that. However, now ``layout::Shape`` and
|
||||
``layout::Text`` already know their bounding box beforehand. Work
|
||||
needs to be done to do the same for a ``layout::Group`` or whatever
|
||||
that primitive ends up being called (by taking the union of its
|
||||
children's bounding boxes, so e.g. that a group with a filter can
|
||||
create a temporary surface to be able to render all of its children
|
||||
and then filter the surface).
|
||||
|
||||
Being able to compute the ``objectBoundingBox`` of an element before
|
||||
rendering it would open the door to fixing bug :issue:`1` (yeah, really):
|
||||
currently, the temporary surface used for filtering has the size of
|
||||
the toplevel viewport, but this doesn't work well when one tries to
|
||||
Gaussian-blur an element that lies partially outside that viewport.
|
||||
The filter should apply to the element's extents plus the filter
|
||||
region, which takes into account the extra space needed for a Gaussian
|
||||
blur to work around a shape. Since librsvg cannot render the full
|
||||
shape if it lies partially outside of the toplevel viewport, the
|
||||
blurred result shows up with a halo near the image's edge, since
|
||||
transparent pixels get "blurred in" with the shape's pixels.
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
* 2023/Mar/30 - the "current viewport" is no longer part of
|
||||
``DrawingCtx``'s mutable state. Instead, a ``Viewport`` struct is
|
||||
passed down the call chain via a function argument. This is not
|
||||
complete yet, since the code modifies the current ``cr``'s transform
|
||||
apart from the current viewport's transform. The goal is to have
|
||||
the current viewport actually have the full transform to be applied
|
||||
to the object being rendered. This should simplify gnarly code
|
||||
paths like the one for rendering ``<pattern>``.
|
||||
|
||||
* 2025/Apr/09 - The current ``Viewport`` is passed as an argument to
|
||||
functions that need it, and it holds the current transform
|
||||
correctly. Evidence of this is that the code does not call
|
||||
``cr.transform()`` anymore; only
|
||||
``cr.set_matrix(viewport.transform)`` in the innermost code, right
|
||||
before drawing operations on the ``cr``.
|
||||
|
||||
Elements that "establish a new viewport", per the SVG spec, use a
|
||||
``LayoutViewport`` and pass it to
|
||||
``DrawingCtx::with_discrete_layer()``. This composes the
|
||||
appropriate transform into the current ``Viewport`` and passes it on
|
||||
to the drawing functions. Only the markers code remains to be
|
||||
cleaned up for this, see :issue:`1162`.
|
||||
|
||||
Next steps: :issue:`1162`, :issue:`1163`.
|
||||
|
||||
We can actually start defining ``layout::Group`` now, or rather, the
|
||||
implementation for ``DrawingCtx::draw_group()``.
|
||||
|
||||
* 2026/Mar/31 - clipping paths (from the ``clip-path`` property) are
|
||||
now resolved into a :internals:struct:`rsvg::layout::ClipPath`
|
||||
struct that lives inside
|
||||
:internals:struct:`rsvg::layout::StackingContext`. This is just
|
||||
used for ``clipPathUnits=userSpaceOnUse``, since the other case,
|
||||
``clipPathUnits=objectBoundingBox``, depends on the layout system
|
||||
being able to handle all the structural elements, which it does not
|
||||
yet do. When all elements like `<svg>`, `<switch>`, etc. are able
|
||||
to generate a :internals:struct:`rsvg::layout::Group` with a known
|
||||
bounding box, the clipping code will be able to use the bounding box
|
||||
for ``objectBoundingBox``.
|
||||
|
||||
Next steps: :issue:`271`. Instead of using ``cr.clip()``, actually
|
||||
render clipping paths as alpha masks. This will let us perform
|
||||
arithmetic on them (union, intersection) to handle all the cases
|
||||
that the ``clipPath`` element supports (librsvg does not support
|
||||
proper unions of clipping paths right now).
|
||||
Reference in New Issue
Block a user