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 PlatformInterface::PathData class. For the earlier example path, the outputs are as follows:
Function | output |
---|---|
PathData::segmentCount() | 5 |
PathData::segments() | Qul::PlatformInterface::PathData::SegmentType [5] { MoveSegment, LineSegment, LineSegment, CubicBezierSegment, CloseSegment } |
PathData::controlElementCount() | 12 |
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 PlatformInterface::DrawingEngine and override the following functions:
- PlatformInterface::DrawingEngine::allocatePath
- PlatformInterface::DrawingEngine::setStrokeProperties
- PlatformInterface::DrawingEngine::blendPath
class ExampleDrawingEngine : public PlatformInterface::DrawingEngine { public: ... PlatformInterface::DrawingEngine::Path *allocatePath(const PlatformInterface::PathData *pathData, PlatformInterface::PathFillRule fillRule) QUL_DECL_OVERRIDE; void setStrokeProperties(PlatformInterface::DrawingEngine::Path *path, const PlatformInterface::StrokeProperties &strokeProperties) QUL_DECL_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) QUL_DECL_OVERRIDE; };
Path preprocessing
Next, create a sub-class of the PlatformInterface::DrawingEngine::Path 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 const 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 std::vector<int32_t, Qul::PlatformInterface::Allocator<int32_t> > fillElements; std::vector<int32_t, Qul::PlatformInterface::Allocator<int32_t> > strokeElements; };
The fillRule
can be converted to a 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 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::PathSeparatorSegment: 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: QUL_ASSERT(false, QulError_PathData_UnknownSegmentType, segment.type()); return; } } addElement(static_cast<int32_t>(HW_PATH_END)); processingDone = true; }
Path allocation
Next, implement the 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 PlatformInterface::DrawingEngine::blendPath function should be implemented next. (This requires you to create a custom drawing engine like described in Hardware accelerated blending.) This function 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 tonullptr
, path is filled by the givenfillBrush
according to thePathFillRule
set when allocating the path. - If
strokeBrush
is not set tonullptr
, path is stroked by the givenstrokeBrush
according to theStrokeProperties
set bysetStrokeProperties
.
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(), ...}; if (fillBrush->pattern() == Qul::PlatformInterface::Brush::LinearGradientPattern) { const Qul::PlatformInterface::GradientStops &gradientStops = fillBrush->linearGradient().stops(); // HW_GradientColorTable_Handle hw_gradientColorTable = nullptr; // hw_gradientColorTable = generateHW_GradientColorTable(gradientStops); // HW_DrawWithLinearGradient(..., &hw_path, destinationPath->fillRule, &matrix, HWblendMode, hw_gradientColorTable, hwLinearGradientParameters(fillBrush->linearGradient()), ...); // As an optimization it might make sense to keep a cache of HW gradient color tables // if (hw_gradientColorTable) // freeHW_GradientColorTable(hw_gradientColorTable); } else if (fillBrush->pattern() == Qul::PlatformInterface::Brush::SolidPattern) { // HW_Draw(..., &hw_path, destinationPath->fillRule, &matrix, HWblendMode, fillBrush->color().value, ...); } } if (strokeBrush) { // HW_Path_t hw_path = {..., destinationPath->strokePathSize(), destinationPath->getStrokePathData(), ...}; if (strokeBrush->pattern() == Qul::PlatformInterface::Brush::LinearGradientPattern) { const Qul::PlatformInterface::GradientStops &gradientStops = fillBrush->linearGradient().stops(); // HW_GradientColorTable_Handle hw_gradientColorTable = nullptr; // hw_gradientColorTable = generateHW_GradientColorTable(gradientStops); // HW_DrawWithLinearGradient(..., &hw_path, destinationPath->fillRule, &matrix, HWblendMode, hw_gradientColorTable, hwLinearGradientParameters(strokeBrush->linearGradient()), ...); // As an optimization it might make sense to keep a cache of HW gradient color tables // if (hw_gradientColorTable) // freeHW_GradientColorTable(hw_gradientColorTable); } else if (strokeBrush->pattern() == Qul::PlatformInterface::Brush::SolidPattern) { // 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:
- Qul::PlatformInterface::PathDataStroker::beginStroke
- Qul::PlatformInterface::PathDataStroker::endStroke
- Qul::PlatformInterface::PathDataStroker::lineTo
- Qul::PlatformInterface::PathDataStroker::moveTo
- Qul::PlatformInterface::PathDataStroker::cubicTo
- Qul::PlatformInterface::PathDataStroker::arcTo
class ExamplePathDataStroker : public PlatformInterface::PathDataStroker { public: ExamplePathDataStroker(ExamplePath *data); protected: void beginStroke() QUL_DECL_OVERRIDE; void endStroke() QUL_DECL_OVERRIDE; void lineTo(float x, float y) QUL_DECL_OVERRIDE; void moveTo(float x, float y) QUL_DECL_OVERRIDE; void cubicTo(float c1x, float c1y, float c2x, float c2y, float ex, float ey) QUL_DECL_OVERRIDE; void arcTo(float x, float y, float rx, float ry, float rotation, bool largeArc, bool clockwise) QUL_DECL_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.