353 lines
14 KiB
ReStructuredText
353 lines
14 KiB
ReStructuredText
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).
|