Wednesday, November 26, 2008

Some recent UI rendering fixes

Over the last week or so I've fixed several OpenGL rendering problems in the Factor UI.

The first problem is that on Linux, with certain ATI video cards, calling glPolygonMode() to disable polygon fill does not work, and as a result the UI would render with no text visible; borders around buttons would be drawn as filled rectangles and this would overwrite all of the text. I fixed this and modernized the UI rendering code to use vertex arrays at the same time.

Then, I decided to address a long-standing issue: rectangles and lines were not rendered pixel-perfect on all systems. You see, with OpenGL, simply rendering a rectangle with integer co-ordinates doesn't give you the results you would expect. On my old Windows laptop for example, rectangles would render with the bottom-right pixel missing. Getting this to work in a cross-platform manner is surprisingly hard. It took me a lot of back-and-forth to get consistent rendering across the machines I tested on (An Intel iMac from a year ago or so, my MacBook Pro with NVidia graphics, Linux and Windows in VirtualBox, and an old Dell Laptop with Windows XP and Intel integrated graphics). For the record, the "correct" way to draw an outlined rectangle is to offset the top-left pixel by 0.5,0.5, and the bottom-right pixel by -0.3,-0.3. Similar heroics were required for line rendering. In the end, I made my job semi-automated by writing a simple program, which is in the ui.render.test vocabulary now, that renders some 2D shapes with OpenGL;

The vocabulary renders the shapes and then compares the results with a reference drawing. It reads the rendered pixels with glReadPixels() and loads the drawing with Doug's graphics.bitmap library.

Of course, it turned out that vertex arrays themselves have issues on at least one OpenGL implementation. I started to receive reports of random segfaults on older MacBooks with the Intel X3100 graphics chip. Chris Double then came across a mailing list post which explained the situation. Turns out that with this driver, rendering a GL_LINE_LOOP vertex array causes a segfault. The workaround is to render a GL_LINE_STRIP where the last vertex is a copy of the first vertex.

There are still a couple of OpenGL problems I haven't been able to resolve. A few users report random screen corruption on Windows machines with Intel graphics. There is also a problem where the Factor UI comes up black if used with Compiz on Linux, however this appears to affect any GL app -- even glxgears. The workaround is to enable indirect rendering with Compiz.

It has been three years since I first ported Factor's UI to OpenGL and I'm still finding new issues in drivers that need workarounds. It's a bit disappointing, but it does seem that in recent times, OpenGL driver quality is improving. I still think OpenGL is a good way to render graphics in a cross-platform manner; it has plenty of features and even runs on mobile platforms (OpenGL ES). Our UI only needs a tiny bit of platform-specific code for each platform: opening windows, creating GL contexts, receiving events, and working with the clipboard. Rendering is cross-platform.

A few people have suggested using Cairo instead, and while Factor has a Cairo binding now, I'm not keen on using it to render the entire UI. I worry that by switching from OpenGL to Cairo, we'll just have to replace one set of bugs and workarounds with another. Another problem is that Cairo would be another external dependency that deployed Factor applications would have to ship. It seems Cairo is quite bloated these days, so that would be unacceptable. One other alternative is to use native APIs: perhaps Cairo rendering on Linux, GDI+ on Windows, and CoreGraphics on Mac. However, this would require too much effort on my part, so its not something I'm willing to dive into at this point.


Samuel A. Falvo II said...

Just an FYI explaining the logic behind the Windows GDI functions and their "missing pixels" effect.

Those familiar with the Python language understand that slice notation uses an open-closed interval notation. That is, given a list of data 10 elements long, a slice [0:5] gives back elements 0..4 inclusive, but not 5. Mathematical notation would use this instead: [0,5). Likewise, [5:10] gives back 5..9 ([5,10)). In this way, a slice [0:5] appended to another slice [5:10] yields the original list [0:10].

One can think of slice indices as addressing not individual storage boxes, but rather the walls between the boxes. I'd draw an ASCII art representation to help explain, but it'd look horrible in the comment box. Instead, see this ASCII art at

Anyway, the Windows GDI treats pixel locations in the same manner: a pixel at (x,y) is bounded by a box with infinitely thin walls from (x,y) to (x+1,y+1). When drawing individual pixels, (x,y) maps to how we intuitively would address pixels. When drawing filled objects, however, "slice notation" is used.

Note that X11 uses the same pixel addressing mechanism.

Slice notation such as this has the advantage that adjoining figures on the screen will mesh with each other. Without it, as I've often had to do under AmigaOS, for example, you'll inevitably end up mucking about with coordinates in an attempt to prevent displayed objects from either partially overlapping, or having an inadvertent gap of one pixel.

dh said...

Slava, is there a reason why you do not use a webbrowser as a front end UI for the factor IDE? Wouldn't that give you more platform independence? I'm just curious to know your design rationale.

Cheers, Dominikus

Lukas Mach said...

Wouldn't this help?

"If exact pixelization is required, you might want to put a small translation in the ModelView matrix, as shown below:

glMatrixMode (GL_MODELVIEW); glLoadIdentity (); glTranslatef (0.375, 0.375, 0.);"