GimpPreview Reference Manual | |||
---|---|---|---|
<<< Previous Page | Home | Up | Next Page >>> |
This section describes some design and implementation issues with respect to the preview.
One category of plug-ins that badly need a preview are the plug-ins that have a rendering algorithm that is slow. In many cases, these rendering algorithms will run much faster when they only have to render a small part of the final image.
Often the rendering algorithms are too slow to compute a rendered image in real-time, while the user is scrolling. But it is important to give feed-back about the current position while the user is scrolling the image. A solution, that works very well with many plug-ins, is to show the original image while scrolling, and only call the rendering algorithm when the user has stopped scrolling.
A previous version of this preview implemented this behavior in a straight-forward way. It turned out that this did not work very well. While the rendering algorithm was computing, the preview seemed "frozen" and the user got no feed back on changes in position or scale. As it turned out, it is very normal behavior for a user to change the preview position or scale, and almost immediately after that, to decide to scroll or scale somewhat more (or less). But in these cases, the user got no immediate feedback because the program was busy with rendering the new image.
A somewhat smarter version of this preview used a different approach. The rendering function had to call back to the GTK main loop, so it could process new events. In this way, the preview could process new scroll or scale events and update its contents. This worked very well for scrolling, the preview was updated with the original image. But drawing the rendered image frequently gave incorrect results. What happened when the user scrolled multiple times was the following: during the first invocation of the render algorithm it passed back control to the GTK main loop to process new events. During the processing of these events, the display was first updated with the original image, and then the preview called back to the plug-in for a second time to render the new image. When this happened, there were two instances of the render function active. This gave confusing results. When the most recent instance finished, it updated the preview with its results. So far so good, but when there were no more events, the GTK main loop passed control back to the first invocation of the render function. When this first invocation had rendered its image it happily drew its results, that were obsolete, on the preview. From the user's perspective this was very confusing. Sometimes, the correct image was visible for a short while, but then the preview jumped back to an old position. This happened so frequently that a different approach was needed.
Basically, this is a multi-tasking problem. The rendering function should be activated as soon as possible, but even while the rendering function is busy computing, the GUI should remain responsive to new actions by the user. When the user has scrolled or scaled the preview, the results of existing rendering tasks have become obsolete, because they use the wrong position or scale. So at this point there are two things that should be done: these obsolete tasks should be prevented from updating the preview and they should stop their computations as soon as possible.
A possible solution, that was never considered very seriously, was to use a separate thread for the rendering function. The rendering thread should be killed when the user scrolled or scaled and a new rendering thread should then be created. This solution was almost immediately rejected because writing correct multi-threading programs is a very difficult job, and plug-in developers should not be burdened with such problems.
The standard solution for terminating a task in cooperative multi-tasking systems like GTK is to use polling. The task should poll the other task at regular intervals to determine whether it can halt.
Polling seemed a good solution for terminating existing tasks, but it was not a fully robust solution for preventing incorrect updates. When the plug-in did not poll immediately before drawing to the preview, it was still possible that (parts of) obsolete images would be drawn to the preview.
The final solution that is incorporated in the current design is that the preview internally maintains a "rendering-state" that determines if the preview should render a new image for the preview. The rendering-state can have the following values:
No update to the preview is needed.
The rendering function must be invoked because the current rendered image is obsolete. This occurs when the user scrolls or zooms the preview, or when the plug-in calls gimp_preview_update().
This is the state when there is an active signal handler that is rendering an image that is not obsolete.
Always when the user scrolls or zooms the preview, or when the plug-in calls gimp_preview_update(), the render-state is changed to update_needed. When the preview emits the "update-preview" signal, it changes the state to rendering. After the "update-preview" signal handler has terminated the preview checks the rendering state. If at that point the rendering-state is still equal to rendering the rendering-state is changed to OK. It is possible that the rendering state has changed back to update_needed after the signal handler was invoked. In that case the preview will call the rendering function again.
Normally, the plug-in's render function should call the function gimp_preview_progress_set_fraction() at regular intervals. This function performs several tasks:
This function checks if the rendering-state is still equal to rendering current and returns FALSE when this is not the case. This is the standard interface, that render functions should use to poll the preview.
The first thing that this function does, is calling the GLib main loop to process new events. This ensures that new actions by the user will be processed by the preview. A possible result of processing these events is that the preview is scrolled or zoomed, when this happens, the function of course returns FALSE.
Finally, as an additional bonus, this function also updates the progress-bar of the preview.
In the current implementation, gimp_preview_progress_set_fraction is a comparatively slow function. At the moment I am not really certain what the bottle-neck is, but I suspect that it is the event processing. Currently the current implementation uses the following code to process events:
while (g_main_iteration (FALSE)); |
When the render-state has changed to update_needed, the render function should be called. In the current design the rendering function is not called immediately, but an idle function has added with G_PRIORITY_LOW when the preview widget was created. GTK will call this function when there are no other waiting idle functions with a higher priority. This idle function will then emit the actual signal to the plug-in that it should render a new image.
When the preview is scaled or zoomed, the (scaled) original image is not redrawn immediately. The adjustments are updated, and GTK is informed with gtk_widget_queue_draw that it must redraw the preview. GTK will at some later time send the preview an "expose-event" signal. In its signal handler the preview will then draw the new scaled image on the GdkPixbuf.
Because scaling is an expensive operation, the preview uses a simplistic nearest neighbor scaling.
When the user has selected a magnification greater than one, this algorithm does the right thing, because presumably the user wants a more precise view of individual pixels. In this case, interpolation-based scaling algorithms are not suitable.
At magnification that are less than one, the results of this scaling algorithm are not very smooth. In the current implement, scaling is already the main performance bottle-neck, so it seems unwise to add more sophisticated algorithms.
The GimpPreview widget is a direct descendant of GtkTable, The GimpPreview widget has multiple child widgets: the GimpPreviewImage, a horizontal and a vertical scroll-bar, a progress bar and a GtkHBox for the zoom controls. The table has been arranged in such a way that the GimpPreview expands or shrinks to fill the remaining space. One minor issue is, that the widget tries to expand itself, when one of its child widgets become visible.
The GimpPreviewImage is a direct descendant of GtkImage. It uses a GdkPixbuf to store the image data. When the visible size of the preview is changed a new GdkPixbuf is created.
A possible alternative would have been to use GtkDrawingArea. This seemed more to difficult implement because in this case the preview image would have to implement its own expose handler and memory management. Moreover, there appear to be no good reasons to expect that this alternative would be faster than using a GtkImage.
In the current design there basically two strategies for drawing on the preview, either one row at a time or an entire drawable at once. For converting existing plug-in's, the easiest method probably is to draw a drawable. Using temporary drawables has some disadvantages. When the plug-in crashes, the temporary drawable will not be deleted in the main GIMP process. Another disadvantage is that it requires communication with the Gimp process, which is an expensive operation. In many cases, this communication is logically unnecessary since the drawable is only used locally in the plug-in. For example, there is no reason why tiles for this drawable should be stored by the main GIMP process. A solution would be to develop a kind of "local" drawable that is fully managed by the plug-in. Developing such a new type of drawable does not appear to be an easy task.