Using cairo to render graphics in WDL-OL

WDL-OL is a nice framework for building audio plugins to integrate with your favorite DAW like Ableton/Reaper.

Currently supporting many targets for many DAW environment like Windows, Mac (and much more like iOs!), this framework is ready out of the box, and, with the excellent Martin Finke tutorials, a really solid tool for starting your next hit plugin !

After doing many tiny things with this framework, I found it lack a feature: some overkill graphics. When you use graphics in WDL-OL, basically you directly talk to IGraphics (which rely on LICE), and draw what you have to do with it. Enough and good in many cases, there is also some cases where the system show some limits:

wdl-ol-lice-render
LICE Render
wdl-ol-cairo-render
CAIRO Render

You basically have two problems:

  • The first is easy to see, the smooth algorithm (aliasing stuff), is working, but not perfect in LICE;
  • Lice mostly use integer data inside, which basically means it doesn’t handle well if you submit a point like x(0.5:0) and y(0.7:0.2), it will approximate to nearest int values, so it will render x(1:0) and y(1:0), which creates some tiny glitch sometimes.

The purpose of this article, is to mix cairo into WDL-OL, to switch from LICE render to cairo render, when necessary, as LICE is doing a good job mostly, we don’t need to fully replace it with cairo.

Use Case

Before entering into the subject, a little part about « when to use » cairo:

  • Most of times, you should, as much as possible, rely on LICE;
  • Only when you find something uncool, switch to cairo.

Few cases I found cairo usefull are:

  • You need to render curve/bezier, cairo is probably the right choice;
  • Your graphic needs to be extremely precise, does not suit with int approximation, use cairo (should be rare), some spectrum may be related to this case;
  • Your graphic/element needs to zoom in/zoom out, of course cairo will be a good choice, but LICE will do it fine too, as you are not working on a fixed image, but you redraw!
  • You need to render a simple line graphic, test before choosing cairo, usually LICE is able to do with nice looking such things, curve are more problematic.

And, at the end, the choose of cairo itself is based to it’s high platform compatibility (ARM, linux, windows…), and the fact it’s a mature/highly supported library (as for example used inside Firefox to render canvas, or used in inkscape…).

Let’s dive into !

Build cairo

I will not ask you to build cairo, as it can be quite annoying, and I already did it:

This build includes:

  • libpng-1.6.16
  • pixman-0.32.6
  • zlib-1.2.8
  • cairo-1.12.18

All build are for static use, and following (not exactly but close) to this article. The main difference rely on pixman which is build without MMX support, because of a bug with MSVC x64.

The cairo used here is reduced to minimum, no pdf export, or other stuff like that. I also build the « normal » cairo, you can found it here, BUT it’s not mandatory at all for this tutorial. Who knows, maybe one day a guy will need to render PDF for his VST!

Here is the folder structure I come up with (using the archive above):

  • include
    • cairo
      • cairo.h
      • cairo-deprecated.h
      • cairo-features.h
      • cairo-ps.h
      • cairo-version.h
    • libpng15
      • png.h
      • pngconf.h
      • pnglibconf.h
    • pixman-1
      • pixman.h
      • pixman-version.h
    • png.h
    • pngconf.h
    • pnglibconf.h
    • zconf.h
    • zlib.h
  • lib
    • Win32
      • debug
        • cairo-static.lib
        • libpngd.lib
        • pixman-1.lib
        • zlibd.lib
      • release
        • cairo-static.lib
        • libpngd.lib
        • pixman-1.lib
        • zlibd.lib
    • x64
      • debug
        • cairo-static.lib
        • libpngd.lib
        • pixman-1.lib
        • zlibd.lib
      • release
        • cairo-static.lib
        • libpngd.lib
        • pixman-1.lib
        • zlibd.lib

As you see, I remove cairo.dll and cairo.lib in all *release and *debug folders, as we will use here only the static build of cairo (and those are for the DLL build). Those are still here in case of problem. And we make the lib having many subfolder for every rendering.

A little word about « Tracer » output, for getting it working with cairo, simply duplicate the release folder in both Win32 and x64, and name those duplicate folder « tracer », will do the trick.

Now you have a ready to use folder to include in every project you need cairo, let’s go for WDL-OL part !

WDL-OL template

We will create a basic project here, and see how to add cairo/what to do to get it working.

Note: I strongly recommand you to be familiar with WDL-OL before continuing, there is a really nice tutorials series above, we will not cover what is convered there already, here is the tutorial if you need again.

So first of all, like always, duplicate a project to create a new copy, go in the IPlugExamples folder as usual:

python.exe duplicate.py IPlugEffect MyFirstCairoPlugin YourName

Now you have a new folder MyFirstCairoPlugin which will be our base to work with WDL-OL. Now, we add the folder we create above, here is the final -only folders- structure:

  • app_wrapper
  • include
  • installer
  • lib
  • manual
  • MyFirstCairoPlugin.xcodeproj
  • resources

Where the lib folder contains cairo-static.lib, zlib.lib… And the include folder contains all the *.h of our previous folder downloaded. But it’s not enough to link cairo.

First method

Fire up the MyFirstCairoPlugin.sln project to load Visual Studio. I’m using VS2013 here, so the tutorial follow VS2013 structure.

First of all, go to the project properties (you actually need to do that for every project, vst2, vst3, app, x86, x64… Erf), and go to C/C++ tab, check the line Additional Include Directories and add here a new folder pointing to %your_project_path%\include. Visual Studio supports relative path, so .\include should also work, and is a better choice.

Secondly, we need to add the *.lib, so go to Linker tab, expend it, and check input, in Additional Dependencies add cairo-static.lib (you actually don’t need zlib and other, they are sometimes needed but not for main compilation).

Now, we still didn’t say where to find the *.lib. In the Linker tab, update the field Additional Library Directories with the lib path:

  • .\lib\x64\debug if you are on Debug-x64
  • .\lib\Win32\debug if you are on Debug-x86
  • .\lib\x64\release if you are on Release-x64
  • .\lib\Win32\release if you are on Release-x86

Note: later, if you get an error like LNK2005 (already define in LIBCMTD.lib), it certainly means you use for example .\lib\x64-debug include directory for compiling a release. The release and debug are important here as we are in static mode, this is a common problem.

This is enough when using cairo as dynamic library, but here, we use the static version. We need a tiny things to do: said to cairo we use static.

Quite simple, go to C/C++ tabs, and in the Preprocessor add the command CAIRO_WIN32_STATIC_BUILD to the list. If for any reason you have trouble, you can also activate this by using before the cairo #include in the code #define CAIRO_WIN32_STATIC_BUILD, will produce the same result.

Alternative method

This method has been used by OLI himself, so it would be the recommanded way. But, it’s a little bit more tricky. The idea remains the same: link to your project the include and libs.

The result will of course be similar, but the way to achieve it is different, here we will go threw the file MyFirstCairoPlugin.props file, and edit it, around line 14:

    <ADDITIONAL_INCLUDES>$(ProjectDir)\..\..\..\MyDSP\;.\include;</ADDITIONAL_INCLUDES>

And inside the line 34 (the link tag):

<link>
    <AdditionalDependencies>IPlug.lib;lice.lib;cairo-static.lib;%(AdditionalDependencies)</AdditionalDependencies>
    <AdditionalLibraryDirectories>.\lib\$(Platform)\$(Configuration)\;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</link>

Time to play with cairo !

Creating your IControl

Before cairo, one last step, we need to create a class for handling our graphics, so create a new MyCairoControl.h:

#ifndef __MY_CAIRO_CONTROL_H__
#define __MY_CAIRO_CONTROL_H__

#include "IControl.h"

class MyCairoControl : public IControl {
public:
	MyCairoControl(IPlugBase *pPlug, IRECT pR) : IControl(pPlug, pR) {};
	~MyCairoControl() {};
	bool IsDirty() { return true; };

	bool Draw(IGraphics*) {

	};
};

#endif

I put everything in the H file, which is bad, but we are in « demo » mode. The IsDirty is voluntary always set to true, it’s also a bad idea, but we are in test afterall.

Build and check everything is fine, now we can add cairo. If no problems occurs, go on there is nothing special to do/check here (I recommand you to test every configuration, Debug-x86, Debug-x64, Release…).

Cairo usage

We will create a basic graphic here, where the button already here modify one point in the graphic (so you can see how to link a button to our graphic).
Let’s change our class:

#ifndef __MY_CAIRO_CONTROL_H__
#define __MY_CAIRO_CONTROL_H__

// In case you forget it
#ifndef CAIRO_WIN32_STATIC_BUILD
#define CAIRO_WIN32_STATIC_BUILD
#endif

#include <cairo\cairo.h>

#include "IControl.h"

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

class MyCairoControl : public IControl {
protected:
	cairo_surface_t *surface;
	cairo_t *cr;

public:
	MyCairoControl(IPlugBase *pPlug, IRECT pR) : IControl(pPlug, pR) {
		surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, this->mRECT.W(), this->mRECT.H());
		cr = cairo_create(surface);
	};
	~MyCairoControl() {
		cairo_destroy(cr);
		cairo_surface_destroy(surface);
	};

	bool IsDirty() { return true; };

	bool Draw(IGraphics* pGraphics) {
		// We clear the surface
		cairo_save(cr);
		cairo_set_source_rgba(cr, 0, 0, 0, 0);
		cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
		cairo_paint(cr);
		cairo_restore(cr);

		// This parameter (unused for now) will be used soon for the button
		double position = 0.;

		surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, this->mRECT.W(), this->mRECT.H());
		cr = cairo_create(surface);

		// We define the main color to use, a red color
		cairo_set_source_rgba(cr, 0.8, 0., 0., 0.75);

		// We draw two bezier lines with one point at top
		cairo_set_line_width(cr, 4.);

		// First bezier, 0 to middle of graphic
		// NOTE that 0:0 coordinate is the top/left corner
		// And cairo is position to 0 not where the IControl is placed
		// So the bottom/left is at 0 - height position
		double width = (double)this->mRECT.W();
		double height = (double)this->mRECT.H();

		// This is the 0 - half X path
		cairo_move_to(cr, 0, height);
		cairo_curve_to(cr, width / 4, height, width / 4, position, width / 2, position);

		// This is the half - full width X path
		cairo_move_to(cr, width / 2, position);
		cairo_curve_to(cr, 3 * width / 4, position, 3 * width / 4, height, width, height);

		// Render
		cairo_stroke(cr);

		// Now we draw the center points
		// We make it more red
		cairo_set_source_rgba(cr, 1, 0., 0., 0.75);
		cairo_set_line_width(cr, 1.);
		// Make the circle and fill it
		cairo_arc(cr, width / 2, position, 4., 0, 2 * M_PI);
		cairo_stroke_preserve(cr);
		cairo_fill(cr);

		
		// And we render
		cairo_surface_flush(surface);
		unsigned int *data = (unsigned int*)cairo_image_surface_get_data(surface);
		// This is the important part where you bind the cairo data to LICE
		LICE_WrapperBitmap WrapperBitmap = LICE_WrapperBitmap(data, this->mRECT.W(), this->mRECT.H(), this->mRECT.W(), false);

		// And we render
		IBitmap result(&WrapperBitmap, WrapperBitmap.getWidth(), WrapperBitmap.getHeight());
		return pGraphics->DrawBitmap(&result, &this->mRECT);
	};
};

#endif

As you see, code get bigger, most of it is usual cairo code, but the last part (starting at cairo_surface_flush) is the point we are looking for. We take the raw pointer of cairo’s data, map it to a LICE Wrapper, and then insert that into LICE like if it was a bitmap. And this is actually working like a charm.

I strongly recommand you to keep the width/height ratio to 1:1 between cairo, your IControl size, and migration from cairo to IGraphics. For that, simply define cairo surface at the same size as your IControl, and LICE Wrapper/IBitmap creation, keep same width/height again.

NOTE: the new update, which change the creation of surface to constructor may lead to problem if you want to resize the control. In this case, you have to take care of any resize action to delete the surface and context, and re-create them with new width and height.

Before rendering everything, let’s make some change in MyFirstCairoPlugin.cpp, especially in the constructor, change:

pGraphics->AttachPanelBackground(&COLOR_RED);

To

pGraphics->AttachPanelBackground(&COLOR_BLACK);

Little bit less ugly 😀

On top, add our include (still in MyFirstCairoPlugin.cpp):

#include "resource.h"
#include "MyCairoControl.h"

And between the color and the knob, add:

  pGraphics->AttachPanelBackground(&COLOR_BLACK);
  pGraphics->AttachControl(new MyCairoControl(this, IRECT(0, 0, kWidth, kHeight)));
  IBitmap knob = pGraphics->LoadIBitmap(KNOB_ID, KNOB_FN, kKnobFrames);

We make the graphic full width/height. Run it, you should get this:

myFirstCairoPlugin

Cool, but we still miss the link between the knob and the graphic, therefore, we need some modifications.

Still on the MyFirstCairoPlugin.cpp, change the « new MyCairoControl » like so:

  pGraphics->AttachControl(new MyCairoControl(this, IRECT(0, 0, kWidth, kHeight), kGain));

We simply add « kGain » at the end. Now we need to say to MyCairoPlugin to use this parameter. In the constructor (file: MyCairoControl.h):

	MyCairoControl(IPlugBase *pPlug, IRECT pR, int paramIdx) : IControl(pPlug, pR, paramIdx) {};

Now the button and the graphic share the same param identifier. We still need to get it’s value. In the draw function (still MyCairoControl.h):

	// This parameter (unused for now) will be used soon for the button
	double position = (this->GetParam()->GetMax() - this->GetParam()->Value()) * this->mRECT.H() / 100.;

Here, instead of position = 0, we get the value of the knob (range [0..100]), so we need to divide by 100. and invert the value.

If you run, now it’s working! Urahai!

myFirstCairoPlugin 2

Have fun!

If you have any problem, you can found the github of this example: here

Publicités

6 Commentaires

  1. Hi

    Excellent article! Though I had some problems to get it work when loading the project in Visual Studio 2015 (Toolset v140). The linker complained about ‘unresolved external symbol ___iob_func’ in cairo_static.lib. I’m using your build of cairo, VS 2013 compiler – x64/x86 – release/debug. After changing the Platform Toolset to version Visual Studio 2013 (v120) I got it working nicely! 🙂

    stonerich

    • deisss

      Yes be really carefull with static linking, if you want to use a VS 2015 unfortunately you probably need to rebuild the full static elements…

  2. styro

    Hello,
    thanks very much for this great tutorial!
    i noticed the plugin is eating up memory constanly,
    by commenting lines 45 & 46

    // surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, this->mRECT.W(), this->mRECT.H());
    // cr = cairo_create(surface);

    it seems to behave normally…

    is there a reason for getting every time a new memory chunk in the Draw-method?

    many thanks for some enlightenment!
    styro

    • deisss

      That shouldn’t bet the case…

      Especially with this line should clean everything prior to render: cairo_surface_flush(surface);

  3. Michael

    Thanks so much for putting together this tutorial! I’ve been looking for something like this for ages, this is huge. Just got it up and running on Mac, very excited.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :