kileyi
kileyi

Reputation: 11

How to speed up drawing with Cairo on Opengl windows?

My question is how to speed up drawing on OpenGL on windows.

The test code is below. I copied it from some cairo example on the web.

the fps drop to 30 to 40 per second, even slower than a web browser.

just draw line every frame, I tried write javascript on html5. The same function just draws a line, and it runs much faster.

why cairo draw line on opengl so slow? Did I do something wrong? and how can I speed it up?

I think c++ should be much faster than javascript

#include <stdlib.h>
#include <stdio.h>
#include <malloc.h>

#define _USE_MATH_DEFINES
#include <math.h>

#include <iostream>
#include <chrono>
#include <random>

#include <gl/glut.h>
#include <gl/glext.h>

#include <cairo.h>

using namespace std;

double win_width = 800;
double win_height = 600;
double hw = win_width / 2;
double hh = win_height / 2;
double line_width = 1;
//double line_width = 1 / win_width;

cairo_surface_t * surf = NULL;
cairo_t         * cr = NULL;
unsigned char   * surf_data = NULL;

GLuint texture_id;

// Interface //

void opengl_init(void)
{
    printf("OpenGL version: %s\n", glGetString(GL_VERSION));
    printf("OpenGL vendor: %s\n", glGetString(GL_VENDOR));
    printf("OpenGL renderer: %s\n", glGetString(GL_RENDERER));

    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glDisable(GL_DEPTH_TEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_TEXTURE_RECTANGLE_ARB);
}

void opengl_cleanup(void)
{
    glDeleteTextures(1, &texture_id);
}

void opengl_draw(int width, int height, unsigned char * surf_data)
{
    if (!surf_data)
    {
        printf("draw_func() - No valid pointer to surface-data passed\n");
        return;
    }

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glClear(GL_COLOR_BUFFER_BIT);

    glPushMatrix();

    glBindTexture(GL_TEXTURE_RECTANGLE_ARB, texture_id);
    glTexImage2D(GL_TEXTURE_RECTANGLE_ARB,
        0,
        GL_RGBA,
        width,
        height,
        0,
        GL_BGRA,
        GL_UNSIGNED_BYTE,
        surf_data);

    glColor3f(0.25f, 0.5f, 1.0f);
    glBegin(GL_QUADS);
    glTexCoord2f(0.0f, 0.0f);
    glVertex2f(0.0f, 0.0f);
    glTexCoord2f((GLfloat)width, 0.0f);
    glVertex2f(1.0f, 0.0f);
    glTexCoord2f((GLfloat)width, (GLfloat)height);
    glVertex2f(1.0f, 1.0f);
    glTexCoord2f(0.0f, (GLfloat)height);
    glVertex2f(0.0f, 1.0f);
    glEnd();

    glPopMatrix();
}

void opengl_resize(int width, int height)
{
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f);

    glClear(GL_COLOR_BUFFER_BIT);

    glDeleteTextures(1, &texture_id);
    glGenTextures(1, &texture_id);
    glBindTexture(GL_TEXTURE_RECTANGLE_ARB, texture_id);
    glTexImage2D(GL_TEXTURE_RECTANGLE_ARB,
        0,
        GL_RGBA,
        width,
        height,
        0,
        GL_BGRA,
        GL_UNSIGNED_BYTE,
        NULL);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
}


void drawShape()
{
    //save current brush
    cairo_save(cr);

    // clear background
    cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
    //cairo_scale(cr, (double)win_height / 1.0f, (double)win_height / 1.0f);
    cairo_set_source_rgba(cr, 1, 1, 1, 1);
    cairo_paint(cr);

    //set line color and style
    cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
    cairo_set_line_width(cr, line_width);



    static double angle = 0;
    angle += 0.01f;

    //draw rect
    cairo_set_source_rgba(cr, 1, 0, 0, 1);
    //cairo_rectangle(cr, 0.5f + sinf(angle) * 0.1f, 0.5f, 0.1f, 0.1f);
    cairo_rectangle(cr, hw + sin(angle) * 100, hh, 100, 100);
    cairo_fill(cr);
    cairo_stroke(cr);

    //draw circle
    cairo_set_source_rgba(cr, 0, 0, 1, 1);
    cairo_arc(cr, 300, hh, 100, 0, 2 * M_PI);
    //cairo_fill(cr);
    cairo_stroke(cr);

    //draw line
    static double r = 100;
    static double posx = 500;
    static double posy = 500;
    static double x = 0;
    static double y = 0;

    x = r * cosf(angle);
    y = r * sinf(angle);

    cairo_set_source_rgba(cr, 0, 1, 0, 1);
    cairo_move_to(cr, x + posx, y + posy);
    cairo_line_to(cr, -x + posx, -y + posy);
    cairo_stroke(cr);


    int minx = 5;
    int maxx = win_width - 5;
    int miny = 5;
    int maxy = win_height - 5;
    int n = 50 * 2;

    std::default_random_engine randomEngine;
    randomEngine.seed(std::chrono::steady_clock::now().time_since_epoch().count());


    std::uniform_real_distribution<float> rangeX(minx, maxx);
    std::uniform_real_distribution<float> rangeY(miny, maxy);

    cairo_set_source_rgba(cr, 0, 0, 0, 1);
    for (int i = 0; i < n * 2; i += 4)
    {
        float x1 = rangeX(randomEngine);
        float y1 = rangeY(randomEngine);

        float x2 = rangeX(randomEngine);
        float y2 = rangeY(randomEngine);

        cairo_move_to(cr, x1, y1);
        cairo_line_to(cr, x2, y2);

    }
    cairo_stroke(cr);


    //restore previous brush
    cairo_restore(cr);
}


void display(void)
{
    static int fps = 0;
    static int frame = 0;
    static long long startTime = chrono::system_clock::now().time_since_epoch().count();
    static long long lastTime = 2;
    long long now = chrono::system_clock::now().time_since_epoch().count();

    ++frame;

    //update per second
    if (now - lastTime > 10000000)
    {
        lastTime = now;
        fps = frame;
        frame = 0;
        cout << fps << endl;
    }

    drawShape();

    opengl_draw(win_width, win_height, surf_data);

    glutSwapBuffers();
}

cairo_t*
create_cairo_context(int               width,
    int               height,
    int               channels,
    cairo_surface_t** surf,
    unsigned char**   buffer)
{
    cairo_t* cr;

    // create cairo-surface/context to act as OpenGL-texture source
    *buffer = (unsigned char*)calloc(channels * width * height, sizeof(unsigned char));
    if (!*buffer)
    {
        printf("create_cairo_context() - Couldn't allocate buffer\n");
        return NULL;
    }

    *surf = cairo_image_surface_create_for_data(*buffer,
        CAIRO_FORMAT_ARGB32,
        width,
        height,
        channels * width);
    if (cairo_surface_status(*surf) != CAIRO_STATUS_SUCCESS)
    {
        free(*buffer);
        printf("create_cairo_context() - Couldn't create surface\n");
        return NULL;
    }

    cr = cairo_create(*surf);
    if (cairo_status(cr) != CAIRO_STATUS_SUCCESS)
    {
        free(*buffer);
        printf("create_cairo_context() - Couldn't create context\n");
        return NULL;
    }

    return cr;
}

void cleanup(void)
{
    opengl_cleanup();
    free(surf_data);
    cairo_destroy(cr);
    exit(0);
}

void keyboard(unsigned char key, int x, int y)
{
    switch (key)
    {
    //27 is ESC key
    case 27:
    case 'q':
        cleanup();
        break;

    case 'd':
        cairo_surface_write_to_png(surf, "frame.png");
        break;

    case '+':
        if (line_width < 10)
            line_width += 1;
        break;

    case '-':
        if (line_width > 1)
            line_width -= 1;
        break;

    }
}

void idle(void)
{
    glutPostRedisplay();
}

int main(int argc, char ** argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowSize(win_width, win_height);

    if (glutCreateWindow("Opengl Test") == 0)
        exit(-2);

    // create cairo-surface/context to act as OpenGL-texture source
    cr = create_cairo_context(win_width, win_height, 4, &surf, &surf_data);

    // setup "GL-context"
    opengl_init();

    glutDisplayFunc(display);
    glutKeyboardFunc(keyboard);
    glutIdleFunc(idle);
    opengl_resize(win_width, win_height);

    glutMainLoop();

    return 0;
}

and here is the html and js i use

index.html

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <script type="text/javascript" src="main.js"></script>
    <style type="text/css">
    html, body {
        margin: 0px;
    }

    canvas {
        display: block;
    }

    </style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>

main.js

window.onload = function() {
    var canvas = document.getElementById("canvas"),
        context = canvas.getContext("2d"),
        width = canvas.width = window.innerWidth,
        height = canvas.height = window.innerHeight;

    render();

    function render() {
        context.clearRect(0, 0, width, height);
        for(var i = 0; i < 100; i += 1){
            context.beginPath();
            context.moveTo(Math.random() * width, Math.random() * height);
            context.lineTo(Math.random() * width, Math.random() * height);
            context.stroke();
        }
        requestAnimationFrame(render);
    }   
};

Upvotes: 1

Views: 4513

Answers (2)

user2249675
user2249675

Reputation: 484

It is very painful to compile cairo with OpenGL backend on Windows. But here's how I did it. Works on cygwin and msys2 with x86_64-w64-mingw32 toolchain.

  1. Cygwin provides a precompiled cairo package for Win64. It doesn't support OpenGL. However if you install that package it will automatically pull in all the dependencies of cairo, including pixman. Also install the GLEW package (OpenGL Extension Wrangler).

  2. Download the cairo source code, and extract it to your workspace.

  3. Open the configure file. Search for -lGL and replace all instances with -lglew32 -lopengl32. This will make cairo link to the Windows OpenGL library instead of the Linux one. The library name here is case-sensitive, and must match the opengl32.dll in C:\Windows\system32. If not then the build system will complain about not able to find opengl32.dll.

  4. Open src/cairo-gl.h. Under the line

    #if CAIRO_HAS_GL_SURFACE || CAIRO_HAS_GLESV2_SURFACE
    

    Add

    #include <GL/glew.h>
    

    Under the line

    #if CAIRO_HAS_WGL_FUNCTIONS
    

    Add

    #include <GL/wglew.h>
    

    This file must be included before windows.h, otherwise GLEW will complain that gl.h is alreaded included.

  5. There is a bug in cairo wgl device creation process. Basically cairo will create an invisible window, and attach the GL context you provide to that window. However if your context does not have the same pixel format as the dummy window, then the binding will fail. Here's my quick fix. Open src/cairo-wgl-context.c, find static cairo_status_t _wgl_dummy_ctx (cairo_wgl_context_t *ctx).

    Before the line

    wglMakeCurrent(ctx->dummy_dc, ctx->rc);
    

    Add

    ctx->rc = wglCreateContextAttribsARB(ctx->dummy_dc,ctx->rc,NULL);
    
  6. Now execute

    ./configure --host=x86_64-w64-mingw32 --enable-gl --enable-wgl
    make
    

    If configure complains it cannot find certain programs, first make sure the binutils for Win64 toolchain is installed. Then use ln to make a hard link to the program with x86_64-w64-mingw32 prefix.

    The make process will inevitably fail in the test folder, because it attempts to compile tests that require the Linux GL library. Ignore the failure, and go to /src/.libs. You should see libcairo-2.dll and other files needed for linking.

    The most common issue is that only libcairo.a is produced, but no libcairo-2.dll. Make sure you modified the configure file exactly as I said.

  7. Copy the libcairo-2.dll to wherever you want. If your program links dynamically to cairo, it should now work fine. However if want to link statically, there is another complication. Some parts of cairo use Windows critical section to implement mutex. The critical sections are initialized during DllMain. However if you link statically then you must manually initialize them. Just call the void _cairo_mutex_initialize (void) function within cairo-mutex.c.

Upvotes: 1

datenwolf
datenwolf

Reputation: 162194

Your bottleneck is actually not OpenGL but Cairo. You're using Cairo with its standard software rasterizer backend; so the CPU is doing all the heavy lifting and OpenGL is just used as a glorified surface blitter. Admittedly the method for loading the finished image into OpenGL is not optimal (glTexSubImage2D should be used instead of glTexImage2D), but this is hardly your bottleneck there.

So what should you do: Ideally you'd be using a OpenGL accelerated backend for Cairo as described in http://cairographics.org/OpenGL/

Another option is ditching Cairo and use a vector rendering library directly targeted at OpenGL; I'm thinking of NanoVG here (I have no affiliations to this project). The main advantage of NanoVG is, that its whole internal architecture has been designed with OpenGL as backend in mind.

If you want to profile the influence on the improperly chosen method for texture upload here's a fixed variant of the code (remove opengl_cleanup, it does nothing good for this very example and also get rid of opengl_resize it's very bad practice to do projection setup in the resize handler).

void opengl_draw(int width, int height, void const * surf_data)
{
    static GLuint texture_id = 0;
    static int tex_width = 0;
    static int tex_height = 0;

    if (!surf_data)
    {
        printf("draw_func() - No valid pointer to surface-data passed\n");
        return;
    }

    if( !texture_id ) {
        glGenTextures(1, &texture_id);
    }
    glBindTexture(GL_TEXTURE_RECTANGLE_ARB, texture_id);
    if( width != tex_width || height != tex_height ) {
        glTexImage2D(GL_TEXTURE_RECTANGLE_ARB,
            0,
            GL_RGBA,
            tex_width = width,
            tex_height = height,
            0,
            GL_BGRA,
            GL_UNSIGNED_BYTE,
            surf_data);
    } else {
        glTexSubImage2D(GL_TEXTURE_RECTANGLE_ARB,
            0, 0, 0,
            tex_width, tex_height,
            GL_BGRA,
            GL_UNSIGNED_BYTE,
            surf_data);
    }

    glViewport(0, 0, width, height);

    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();
    glOrtho(0.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glPushMatrix();

    glColor3f(0.25f, 0.5f, 1.0f);
    glBegin(GL_QUADS);
    glTexCoord2f(0.0f, 0.0f);
    glVertex2f(0.0f, 0.0f);
    glTexCoord2f((GLfloat)width, 0.0f);
    glVertex2f(1.0f, 0.0f);
    glTexCoord2f((GLfloat)width, (GLfloat)height);
    glVertex2f(1.0f, 1.0f);
    glTexCoord2f(0.0f, (GLfloat)height);
    glVertex2f(0.0f, 1.0f);
    glEnd();

    glMatrixMode(GL_PROJECTION);
    glPopMatrix();
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();
}

just draw line every frame, I tried write javascript on html5. The same function just draws a line, and it runs much faster.

And what do you think does this tell you? HTML Canvas may me implemented in any way that satisfies the specification. The Browser may use Cairo, its own rendering engine, may or may not use the GPU. This is not a useful comparison, because you don't know what you're actually comparing there.

Upvotes: 3

Related Questions