C

Implementing vector graphics support

This topic explains how hardware-accelerated vector graphics support could be implemented if the target platform supports it.

Overview

Vector path

Vector path is typically expressed as a series of path commands. For example, the path in the earlier image could be defined as an SVG path:

M 21,26.50 V 60 H 69 C 69,60 21,60 21,26.50 Z

Where M: MoveTo, V: LineTo (vertical), H: LineTo (horizontal), C: Cubic Bézier Curve, Z: ClosePath are the path commands. The numeric values are the coordinates for each command. As these commands are in capital letters, their arguments are handled as absolute coordinates.

Vector path representation

Vector path in the platform interface is represented by the Qul::PlatformInterface::PathData class. For the earlier example path, the outputs are as follows:

Functionoutput
Qul::PlatformInterface::PathData::segmentCount()5
Qul::PlatformInterface::PathData::segments()
Qul::PlatformInterface::PathData::SegmentType [5] { MoveSegment, LineSegment, LineSegment, CubicBezierSegment, CloseSegment }
Qul::PlatformInterface::PathData::controlElementCount()12
Qul::PlatformInterface::PathData::controlElements()Values for the example path are stored as:
float [12] { x1, y1, x2, y2, x3, y3, cx1, cy1, cx2, cy2, x4, y4 }

With the coordinates:

float [12] { 21.0f, 26.5f, 21.0f, 60.0f, 69.0f, 60.0f, 69.0f, 60.0f, 21.0f, 60.0f, 21.0f, 26.5f }

Note: Vertical and horizontal lines are stored as LineSegments with both x and y coordinates.

Example graphics driver path

The example graphics driver interface uses int32_t for both path commands and arguments to represent a vector path. It uses S16.15 fixed point format for the coordinates and every vector path ends with the END command. Path commands are predefined integer values. The vector path used in the earlier example would be given as:

int32_t [] { MOVETO, x1, y1, LINETO, x2, y2, LINETO, x3, y3, CUBIC, cx1, cy1, cx2, cy2, x4, y4, CLOSE, END }

The coordinates are converted to S16.15:

int32_t [] { MOVETO, 21.0*(1<<15), 26.5*(1<<15), LINETO, 21.0*(1<<15), 60.0*(1<<15), LINETO, ... }

Implementing vector graphics support

Drawing engine overrides

To implement support for the vector graphics, subclass Qul::PlatformInterface::DrawingEngine and override the following functions:

class ExampleDrawingEngine : public PlatformInterface::DrawingEngine
{
public:
    ...
    PlatformInterface::DrawingEngine::Path *allocatePath(const PlatformInterface::PathData *pathData,
                                                         PlatformInterface::PathFillRule fillRule) override;

    void setStrokeProperties(PlatformInterface::DrawingEngine::Path *path,
                             const PlatformInterface::StrokeProperties &strokeProperties) override;

    void blendPath(PlatformInterface::DrawingDevice *drawingDevice,
                   PlatformInterface::DrawingEngine::Path *path,
                   const PlatformInterface::Transform &transform,
                   const PlatformInterface::Rect &clipRect,
                   const PlatformInterface::Brush *fillBrush,
                   const PlatformInterface::Brush *strokeBrush,
                   int sourceOpacity,
                   PlatformInterface::DrawingEngine::BlendMode blendMode) override;
};

Path preprocessing

Next, create a sub-class of the Qul::PlatformInterface::DrawingEngine::Path. It can be used to store platform-specific path data. This ExamplePath uses the same format as the example graphics driver described earlier.

struct ExamplePath : public PlatformInterface::DrawingEngine::Path
{
    // S16.15 fixed point factor
    static constexpr int32_t fixedPointFactor = (1 << 15);

    int16_t fillRule;

    ExamplePath(const PlatformInterface::PathData *pathData, PlatformInterface::PathFillRule fillRule);

    void free() { PlatformInterface::qul_delete(this); }

    const PlatformInterface::PathData *getPathData() { return path; }

    // Functions to access stored path/stroke elements
    int32_t *getFillPathData(void) { return fillElements.data(); }
    int32_t *getStrokePathData(void) { return strokeElements.data(); }

    // Functions to store fill elements in the platform optimized format
    inline void addElement(int32_t element) { fillElements.push_back(element); }
    inline void addElement(float element)
    {
        // Convert floating point values to fixed point
        fillElements.push_back(static_cast<int32_t>(element * fixedPointFactor));
    }

    // Functions to store stroke elements in the platform optimized format
    inline void addStrokeElement(int32_t element) { strokeElements.push_back(element); }
    inline void addStrokeElement(float element)
    {
        // Convert floating point values to fixed point
        strokeElements.push_back(static_cast<int32_t>(element * fixedPointFactor));
    }

    // Function to convert fill path data to platform optimized format
    void processFillPath();

    // Functions to return current element counts
    int32_t fillPathSize() { return fillElements.size() * sizeof(int32_t); }
    int32_t strokePathSize() { return strokeElements.size() * sizeof(int32_t); }

    // Functions to remove fill/stroke elements
    void clearStroke() { strokeElements.clear(); }
    void clearFill() { fillElements.clear(); }

private:
    // Status flag of the vector path preprocessing
    bool processingDone;

    // Path data in the common format
    const PlatformInterface::PathData *path;

    // Fill/stroke data format optimized for the platform
    PlatformInterface::Vector<int32_t> fillElements;
    PlatformInterface::Vector<int32_t> strokeElements;
};

The fillRule can be converted to platform-specific format in the struct constructor.

ExamplePath::ExamplePath(const PlatformInterface::PathData *pathData, PlatformInterface::PathFillRule fillRule)
    : fillRule(fillRule == PlatformInterface::PathWindingFill ? HW_PATH_FILL_NON_ZERO : HW_PATH_FILL_FILL_EVEN_ODD)
    , path(pathData)
    , processingDone(false)
{}

Next, create a function that traverses through the path data with the Qul::PlatformInterface::PathDataIterator, and converts it to the platform-specific format.

void ExamplePath::processFillPath()
{
    if (processingDone)
        return;

    PlatformInterface::PointF current(0.0, 0.0);

    PlatformInterface::PathDataIterator it(path);

    while (it.hasNext()) {
        PlatformInterface::PathDataSegment segment = it.next();
        switch (segment.type()) {
        case PlatformInterface::PathData::CloseSegment: {
            addElement(static_cast<int32_t>(HW_PATH_CLOSE));
            break;
        }
        case PlatformInterface::PathData::MoveSegment: {
            const PlatformInterface::PathDataMoveSegment *moveSegment
                = segment.as<PlatformInterface::PathDataMoveSegment>();
            addElement(static_cast<int32_t>(HW_PATH_MOVETO));
            addElement(moveSegment->target().x());
            addElement(moveSegment->target().y());
            current = moveSegment->target();
            break;
        }
        case PlatformInterface::PathData::LineSegment: {
            const PlatformInterface::PathDataLineSegment *lineSegment
                = segment.as<PlatformInterface::PathDataLineSegment>();
            addElement(static_cast<int32_t>(HW_PATH_LINETO));
            addElement(lineSegment->target().x());
            addElement(lineSegment->target().y());
            current = lineSegment->target();
            break;
        }
        case PlatformInterface::PathData::QuadraticBezierSegment: {
            const PlatformInterface::PathDataQuadraticBezierSegment *bezierSegment
                = segment.as<PlatformInterface::PathDataQuadraticBezierSegment>();
            // ...
            break;
        }
        case PlatformInterface::PathData::CubicBezierSegment: {
            const PlatformInterface::PathDataCubicBezierSegment *bezierSegment
                = segment.as<PlatformInterface::PathDataCubicBezierSegment>();
            // ...
            break;
        }
        case PlatformInterface::PathData::SmallCounterClockWiseArcSegment: {
            const PlatformInterface::PathDataSmallCounterClockWiseArcSegment *arcSegment
                = segment.as<PlatformInterface::PathDataSmallCounterClockWiseArcSegment>();
            // ...
            break;
        }
        case PlatformInterface::PathData::SmallClockWiseArcSegment: {
            const PlatformInterface::PathDataSmallClockWiseArcSegment *arcSegment
                = segment.as<PlatformInterface::PathDataSmallClockWiseArcSegment>();
            // ...
            break;
        }
        case PlatformInterface::PathData::LargeCounterClockWiseArcSegment: {
            const PlatformInterface::PathDataLargeCounterClockWiseArcSegment *arcSegment
                = segment.as<PlatformInterface::PathDataLargeCounterClockWiseArcSegment>();
            // ...
            break;
        }
        case PlatformInterface::PathData::LargeClockWiseArcSegment: {
            const PlatformInterface::PathDataLargeClockWiseArcSegment *arcSegment
                = segment.as<PlatformInterface::PathDataLargeClockWiseArcSegment>();
            // ...
            break;
        }
        default:
            assert(false);
            return;
        }
    }

    addElement(static_cast<int32_t>(HW_PATH_END));
    processingDone = true;
}

Path allocation

Next, implement the Qul::PlatformInterface::DrawingEngine::allocatePath function, which is called when a path handle is allocated.

PlatformInterface::DrawingEngine::Path *ExampleDrawingEngine::allocatePath(const PlatformInterface::PathData *pathData,
                                                                           PlatformInterface::PathFillRule fillRule)
{
    return PlatformInterface::qul_new<Private::ExamplePath>(pathData, fillRule);
}

Path blending to drawing device

The Qul::PlatformInterface::DrawingEngine::blendPath function should be implemented next. It is called when the path is about to be blended onto a drawing device using transform and blendMode. The result must be clipped by clipRect, which is in the drawing device coordinates. In addition, sourceOpacity defines the overall opacity within a range of 0 to 256, with 256 being fully opaque.

  • If fillBrush is not set to nullptr, path is filled by the given fillBrush according to the PathFillRule set when allocating the path.
  • If strokeBrush is not set to nullptr, path is stroked by the given strokeBrush according to the StrokeProperties set by setStrokeProperties.
void ExampleDrawingEngine::blendPath(PlatformInterface::DrawingDevice *drawingDevice,
                                     PlatformInterface::DrawingEngine::Path *path,
                                     const PlatformInterface::Transform &transform,
                                     const PlatformInterface::Rect &clipRect,
                                     const PlatformInterface::Brush *fillBrush,
                                     const PlatformInterface::Brush *strokeBrush,
                                     int sourceOpacity,
                                     PlatformInterface::DrawingEngine::BlendMode blendMode)
{
    Private::ExamplePath *destinationPath = static_cast<Private::ExamplePath *>(path);

    float matrix[3][3];
    toHwMatrix3x3(transform, matrix);

    // HW_SetClip(clipRect.x(), clipRect.y(), clipRect.width(), clipRect.height());

    // HW_BlendMode_t HWblendMode =
    //     (blendMode == PlatformInterface::DrawingEngine::BlendMode_SourceOver ? HW_BLEND_SRC_OVER : HW_BLEND_NONE);

    if (fillBrush) {
        destinationPath->processFillPath();
        // HW_Path_t hw_path = {..., destinationPath->fillPathSize(), destinationPath->getFillPathData(), ...};
        // HW_Draw(..., &hw_path, destinationPath->fillRule, &matrix, HWblendMode, fillBrush->color().value, ...);
    }

    if (strokeBrush) {
        // HW_Path_t hw_path = {..., destinationPath->strokePathSize(), destinationPath->getStrokePathData(), ...};
        // HW_Draw(..., &hw_path, destinationPath->fillRule, &matrix, HWblendMode, strokeBrush->color().value, ...);
    }

    // HW_SetClip(0, 0, screen->width(), screen->height());
}

Path stroking

Finally, implement the Qul::PlatformInterface::DrawingEngine::setStrokeProperties function. The Stroke properties such as LineCapStyle and MiterLimit are accessible from Qul::PlatformInterface::StrokeProperties.

void ExampleDrawingEngine::setStrokeProperties(PlatformInterface::DrawingEngine::Path *path,
                                               const PlatformInterface::StrokeProperties &strokeProperties)
{
    Private::ExamplePath *destinationPath = static_cast<Private::ExamplePath *>(path);
    ...
}

Generating a path for the stroke

Qul::PlatformInterface::PathDataStroker can be used to create a stroke outline path of the given path. This is useful when the platform driver interface does not support stroke properties. PathDataStroker is taken into use by subclassing it and overriding the following functions:

class ExamplePathDataStroker : public PlatformInterface::PathDataStroker
{
public:
    ExamplePathDataStroker(ExamplePath *data);

protected:
    void beginStroke() override;
    void endStroke() override;
    void lineTo(float x, float y) override;
    void moveTo(float x, float y) override;
    void cubicTo(float c1x, float c1y, float c2x, float c2y, float ex, float ey) override;
    void arcTo(float x, float y, float rx, float ry, float rotation, bool largeArc, bool clockwise) override;

private:
    ExamplePath *destinationPath;
    PlatformInterface::PointF current;
};

Implement support for each function to fill the path data in an optimized format for the platform. This generates a path representing the stroke and fills it.

ExamplePathDataStroker::ExamplePathDataStroker(ExamplePath *data)
    : PathDataStroker(data->getPathData())
    , destinationPath(data)
{}

void ExamplePathDataStroker::beginStroke()
{
    destinationPath->clearStroke();
}

void ExamplePathDataStroker::endStroke()
{
    destinationPath->addStrokeElement(static_cast<int32_t>(HW_PATH_END));
}

void ExamplePathDataStroker::lineTo(float x, float y)
{
    destinationPath->addStrokeElement(static_cast<int32_t>(HW_PATH_LINETO));
    destinationPath->addStrokeElement(x);
    destinationPath->addStrokeElement(y);
    current.setX(x);
    current.setY(y);
}

void ExamplePathDataStroker::moveTo(float x, float y)
{
    destinationPath->addStrokeElement(static_cast<int32_t>(HW_PATH_MOVETO));
    destinationPath->addStrokeElement(x);
    destinationPath->addStrokeElement(y);
    current.setX(x);
    current.setY(y);
}

void ExamplePathDataStroker::cubicTo(float c1x, float c1y, float c2x, float c2y, float ex, float ey)
{
    // ...
    current.setX(ex);
    current.setY(ey);
}

void ExamplePathDataStroker::arcTo(float x, float y, float rx, float ry, float rotation, bool largeArc, bool clockwise)
{
    // ...
    current.setX(x);
    current.setY(y);
}

Stroke paths can be precalculated in the Qul::PlatformInterface::DrawingEngine::setStrokeProperties function.

void ExampleDrawingEngine::setStrokeProperties(PlatformInterface::DrawingEngine::Path *path,
                                               const PlatformInterface::StrokeProperties &strokeProperties)
{
    Private::ExamplePath *destinationPath = static_cast<Private::ExamplePath *>(path);

    Private::ExamplePathDataStroker stroker(destinationPath);
    stroker.setStrokeProperties(strokeProperties);
    stroker.stroke();
}

Available under certain Qt licenses.
Find out more.