This chapter provides an overview of how Heidi is structured and how it communicates with applications and renderers.
Figure 1 shows a high-level block diagram of an example application, Heidi, and three possible Heidi device drivers. The first one is based on Microsoft GDI, the second on OpenGL, and the other is a custom driver.


Physical
Renderers
The bottom-most renderer in a renderer stack is called
a physical renderer. A physical renderer
does not require a sink renderer. The most common example of a physical
renderer is a device driver. There are also
physical renderers that are not device drivers. For example, a selector
is a renderer whose purpose is to test for pick hits. It does not draw
to any device. For more information on selection renderers, see "Chapter
7. The Selection Renderer."
You can think of most renderers as device drivers for virtual graphics devices. These virtual graphics devices and their associated renderers have different capabilities—some might be photorealistic, some might be especially fast, while others support hidden-surface removal using a Z buffer. (For more information on hidden line renderers, see "Chapter 9. The Hidden Line Renderer.") They all respond to the same commands and essentially do the same things (draw lines, circles and so forth), but they perform these same tasks in different ways.
Device drivers are renderers in Heidi, and renderers can be dynamically loaded. This means that new graphics hardware with new capabilities can be dynamically added to a Heidi-based application. This process makes it easier to add new hardware to a previously shipped base of application users and helps to promote hardware sales.
Example Renderer
Stack
This section presents an example of a simple renderer
stack. The discussion of the renderers in this section, is oversimplified
to expose the major features of the Heidi architecture: that is, the discussion
applies to hypothetical, simplified renderers. It does not completely
reflect the implementation details of the rendering software supplied with
the Heidi product.

The application sends drawing commands to the software Z-buffer renderer. The Z-buffer renderer removes hidden surfaces using the standard Z-buffer algorithm and draws into its frame buffer. At this point nothing appears on the output screen because this renderer draws only into its software frame buffer and not into the device frame buffer. Nothing is sent to the physical renderer yet.
When an update command is sent to the software Z-buffer renderer, it transfers the contents of its software frame buffer to its sink, the physical renderer. Depending on how the physical renderer is written, this may cause output to appear on the screen immediately. On the other hand, if the output does not appear immediately, the graphics hardware may be using double buffering. In this case, geometry sent to it does not appear until the hardware is told to switch buffers. Finally, an update command is sent to the physical renderer, and you are guaranteed that the graphics you have sent appears on the screen. The update command instructs a renderer to send any changes to its sink. This occurs only if the renderer buffers the drawing.
Alternatively, a renderer can send output to its sink immediately after each drawing command. For example, you can build a software Z buffer that does not have its own frame buffer. In this case, as each drawing command is sent to this renderer, it rasterizes the geometry and compares the depth of the resulting pixels to the pixel depths in the Z buffer. It then sends the visible pixels to its sink. If its sink is a physical renderer that does no buffering, then the output appears immediately on the screen. Thus, as each drawing command is sent to the first renderer, output appears immediately on the screen. In this case, after all drawing is done, the application still sends an update command to each renderer. The update command, however, does nothing for this particular renderer.
Because each update command transfers information only to the next renderer in the stack, it is important that the update commands are sent in the correct order. For a simple situation like the one previously described, an application updates the renderers from the top down—the top renderer first, then its sink, the physical renderer.
A renderer might buffer the drawing using a frame buffer (as in the software frame buffer example) or it might buffer the graphics primitives. For example, a renderer that implements the painter’s algorithm buffers all of the graphics primitives sent to it. Then, when it receives an update command it does a depth-first sort (from back to front) on the primitives and sends them to its sink without rasterizing them. For more information on painter renderers, see the section "Painter Renderers," in this chapter.
Choosing
a Renderer Stack
By choosing which renderers to put in the renderer stack,
an application makes trade-offs that can affect the rendering speed and
image quality. In the example renderer stack (shown in figure 3), it might
appear wasteful to have two separate frame buffers, one in each renderer
(frame buffers can use up much memory). However, there are other factors
to consider: assume that the physical renderer does not do any buffering.
Having a separate frame buffer in the first renderer results in double-buffering.
This reduces screen flashing and provides for smoother animation.
Even more importantly, on many platforms (including Microsoft Windows), transferring data to the hardware frame buffer is much slower than writing to an equivalent buffer in main memory. The hardware frame buffer is usually connected to the processor over an I/O bus that is slower than the main memory bus. Each write to a hardware device, such as a frame buffer, involves a system call and usually a context switch. Thus, rendering each graphics primitive into a software frame buffer, and then transferring the buffer to the graphics device with a single system call, is usually much faster (often twice as fast) than rendering each piece of geometry into the hardware frame buffer.
If multiple renderers in the renderer stack have their own frame buffers, it is not necessary for these frame buffers to be the same size or even the same pixel depth (number of colors). When a frame buffer is transferred to the sink renderer, it is sent using a regular renderer drawing command—the raster bit-blit command. Transferring a frame buffer is no different from any normal bit-blit operation on a renderer. Thus, the output of a renderer is always a sequence of renderer commands, which are sent to the sink renderer. The physical renderer is an exception because it has no sink.
It is also possible for renderers to share a buffer. For example, if a renderer stack contains two software Z buffers, then these can be configured to use the same buffer.
Sending Commands
to Multiple Renderers
An application is not limited to sending drawing commands
to the first renderer. Commands can be sent to any renderer in the stack.
Using the renderer stack in the example in figure 3, the application might
draw a 3D scene to the software Z buffer and then send annotation text
to the physical renderer so that it appears overlaid on the scene.
Again, the application must send update commands at the proper times: in this case, an update command should be sent to the software Z-buffer renderer after the 3D scene and before any annotation text is sent to the physical renderer. The sequence should occur in the following order:

Note: The update_all method is a convenient way to allow the renderer to perform all of the work needed following all of the drawing calls comprising a complete frame, similar to update, but for all parents of the current device.
Renderer
Trees
The renderer stack does not have to
be a single linear stack, with one renderer on top of another. Instead,
a renderer can have more than one source. Each renderer, however, can have
only one sink. Thus, the renderer stack can actually take the form of a
tree, with the physical renderer at the root of the tree. For example,
figure
5 shows a renderer stack with four renderers, one physical
renderer, and three software Z-buffer renderers.

The physical renderer corresponds to the overall window, and the top three renderers correspond to each subregion. Each of the top three renderers can have different options set. They don’t need to be the same kind of renderer. For example, one could be a photorealistic renderer.
Each of the top three renderers has its position and screen size set separately as options. When one of these three renderers is sent an update command, it transfers its contents to its sink—the single renderer controlling the overall window. If the positions of the subregions overlap, then the last renderer updated appears on top. This means that if the application has overlapping subregions, it should update them from back to front.
Note: Even though these subregions act like overlapping subwindows, they are not Windows system windows.
If a renderer stack is configured as a tree, it is still called a stack. A renderer tree is built exactly like a stack—by pushing and popping renderers—however, it is all right to push more than one renderer on top of the same renderer, as in figure 5. In addition, at any one time references are made only about a single path through the tree, which is a stack.
Multiple
Renderer Stacks
The application can also have multiple renderer stacks.
This is useful for supporting multiple output devices. For example, to
print a scene the application can build a renderer stack on top of a physical
renderer for a printer. The same drawing commands that were used to draw
the scene are then sent to this new renderer stack, and the scene is printed.
When an application builds a new renderer stack, it starts
with a different physical renderer, but any renderers pushed on top of
that renderer might be required to be different as well. For example, if
the physical renderer is for a high-resolution printer, then it is preferable
not to use a Z-buffer renderer to remove hidden surfaces. Allocating Z-buffer
memory for such a voluminous number of pixels is prohibitive. In this case
a renderer based on an object space algorithm
is more appropriate. Likewise, if the physical renderer is for a pen plotter,
rasterizing primitives is not necessarily a good approach; you might want
to draw lines as lines and add a separate renderer to do pen optimization.
Note: If the application uses a scene manager or
stores bounding boxes for geometry, then
it may be more efficient to perform hit-testing another way.
Spatial subdivision divides the scene into increments of areas containing fewer objects. These areas are further subdivided if necessary, effectively speeding up the renderer exponentially. However, this method is usually not used and defaults to off.
Transparent-only determines whether you capture all of the objects or just the transparent ones. If true, opaque objects are sent straight through and the transparent objects are stored and later sorted. The option defaults to true. If false, everything is sorted which is useful for Post Script printers.
The Z-sort only option is used when speed is more important. It is not a full painter option of intersecting polygons and does no chopping. Whereas the older default renderer option does effectively chop up the objects into pieces that could appear to be over and under other objects, more useful for pen plotting.
Support for handling 3D items has been added to the Heidi Painter's renderer. This allows opaque objects to be drawn with full 3D hardware acceleration while holding on to transparent objects for proper sorting and subdivision if necessary. For more information on the painter renderer options, see class Paint_Options.
Note: You can set the RGB color and indexed color simultaneously in Heidi. This is necessary when drawing a material and using the Painter's renderer. In this case, the Painter's renderer expects the opacity to be set on the material as well as on the rendition color.
Life
Cycle of a Driver
Heidi drivers are typically used as
the lowest-level renderers in a Heidi renderer stack. As part of its operation,
a driver goes through the life cycle shown in figure 7
and explained in the following steps:

HT_Update_Status
is an abstract base class defining the interface to support update progress
or cancel functionality. It provides a mechanism to monitor the progress
of potentially lengthy update() operations and to cancel update(),
if necessary to save time. No mechanism is provided for the draw calls
as it is assumed that each draw call will be relatively short, precluding
the need for cancel. Also, within the draw call, there is no notion of
"overall" progress.
For example, if your driver is capable of performing double-buffering, then it might provide an option whose name is the string Double Buffering, which can take on a boolean option value that enables or disables this feature. The option definition contains the name of the option, as well as its type specifier: in this case "Double Buffering" and Boolean, respectively. The option value also contains a matching type specifier, as well as the actual values of the option, stored in an array. Multiple options are stored in an option table, which is a linked list of options.
The default constructors for classes HT_Renderer and HT_Device initialize the option tables to have entries for the renderer options (window_size, window_origin, buffer_depth, pixel_aspect, color_system, palette, alpha_blending and so forth) and the driver options (window_id, context_id, colormap_id). Further, the default constructors initialize these options to have trivial values, which almost certainly need to be reset at some stage of the configuration process. Generally, the initialization routine HT_Device calls a constructor for the driver object.
Drivers may also add driver-specific options, which the application may wish to control. For example, if the device supports an upper and lower paper tray, the driver object may include an option that controls which paper tray is in effect. The constructor for the driver object has the responsibility of adding entries for these driver-specific options to the option tables.
Driver Linkage
A driver is a module that the application loads dynamically
at runtime. In particular, a driver for Windows platforms is contained
in a DLL module. This module must export a routine called HD_Device,
which initializes the driver and returns a pointer to an object of the
class HT_Device.
The
GDI driver, for example, fills in the HD_Device
function as in the following:
HEIDI_LOADABLE HT_Device_Data alter * HD_Device () {
return new Driver ();
}
Action Tables
Each renderer has an action table associated with it.
Figure 8 shows a simple example. The action table contains an entry for
each draw command understood by the renderer (plus a few miscellaneous
commands). The draw commands are divided into three categories:
DC stands for device coordinates,
but in reality the 2D commands also take device coordinates. The difference
between 2D and DC commands is that 2D commands do clipping, and DC commands
assume that you are writing into valid pixels and do not want any error
checking.
|
|
|
|
The action table itself is arranged into a kind of hierarchy with more complex commands at the right end and simpler commands at the left end. At the simplest level is the Draw_DC_Image command that draws only an array of pixels.
Heidi contains a library of standard drawing actions that can be called to draw geometry in terms of other geometry that the driver can handle. By default, Heidi initializes its action table to point to these standard drawing actions. The driver may override these actions with its own implementations that take advantage of the hardware’s specific capabilities. For a list of the default drawing actions, see class HT_Drawing_Action.
Every renderer is required to respond to every command in the action table. This sounds like a large amount of work, but with the help of the standard drawing actions and action inheritance, this is quite easy. If a renderer does not provide a particular entry in the action table by providing its own action or inserting a standard drawing action, it inherits the action from the sink renderer.
This inheritance makes it easy to implement extension renderers. The purpose of these renderers is to provide specific valuable rendering services, but not to provide a complete replacement of functionality with respect to its sink. For example, there might be a different method of drawing 3D polytriangles that you might find useful in some situations. In that case, the developer need fill in only that particular action, leaving the remainder free. By not explicitly filling in the remainder of the actions with standard entries, the renderer inherits actions from its parent.
More complex commands in the action table are normally implemented in terms of simpler commands. Consider a physical renderer for a simple full-color display card. The only two commands that must be written specifically for the display card are the Draw_DC_Image command, which provides the lowest level way to transfer pixels to the device frame buffer, and the Clear_drawing_buffer command, which provides a way to clear the background of the window. Once you have established these, all other commands in the action table can be implemented using these two commands, either directly or indirectly. For example, the Draw_3D_Polyline command can be implemented as follows:
Of course, this command sequence is only one of many ways that you could implement Draw_3D_Polyline. For example, the 3D polyline can be clipped in 3D rather than 2D, or some of the steps can be done by the hardware, rather than using the Heidi utility routines.
If your graphics device has any graphics acceleration hardware, you should probably implement more of the draw commands directly. For instance, many graphics cards have hardware to draw lines. In this case, the Draw_DC_Polyline command can be implemented using this hardware, rather than rasterizing the line in software and transferring it into the frame buffer with Draw_DC_Image. If your graphics hardware implements polylines, then the Draw_DC_Polyline command should be implemented directly. At the high end, some graphics hardware implements 3D primitives directly. For example, the best way to implement Draw_3D_Polyline might be to send the 3D polyline straight to the device.
Implementing any command other than Draw_DC_Image is only a matter of speed optimization, and optimization can have a big effect on speed. In some cases, however, the Heidi software routines might actually be faster than using graphics hardware. The architecture of Heidi allows you to dynamically switch between hardware and software routines to compare their speeds.
The Heidi action table implements a scaleable device interface: the interface scales easily to match a wide range of graphics hardware, from simple dumb frame buffers to high-speed graphics pipelines, to printers and plotters. For inexpensive hardware, the action table fills in with software, but it is possible to take advantage of high-end hardware when it is available.
For information on updating the action table, see the section "Updating the Driver Action Table," in chapter 3, "Implementing a New Driver."
Delegating
Drawing Actions
The driver writer may not wish to implement every type
of drawing action that is provided by Heidi. In some cases, the display
hardware may implement some drawing primitives with high end hardware assistance
while others may not be implemented in hardware at all. For example, some
display cards are able to draw lines or polygons, but not ellipses or elliptical
arcs.
In Heidi, drivers can delegate actions in three ways:
For example, the OpenGL sample driver implements polylines through OpenGL only when there is no line cap or join style enabled. If it finds that one of these attributes is enabled, it delegates the line drawing action to the Heidi standard drawing action: HD_Standard_Draw_DC_Polyline. The code looks like this:
void opengl_draw_dc_polyline (
stack WOpenGL_Driver alter * rc,
stack HT_Rendition const * hr,
stack int count,
register HT_DC_Point const * points) {
// ...
// OpenGL can only handle butt lines with miter join styles
if ((hr->line_cap() != HT_Line_Cap::Butt ||
hr->line_join() != HT_Line_Join::Miter)) {
HD_Standard_Draw_DC_Polyline (rc, hr, count, points);
return;
}
// Otherwise draw the polyline via OpenGL ...
}
Drawing
Primitive Actions
Every drawing primitive corresponds to a drawing action
in the renderer action table and thus to a method of the HT_Renderer
class. The name of each drawing action indicates the sort of geometric
figure to be drawn. Every drawing action takes some sort of geometry argument
that determines where the primitive is to be drawn and, by implication,
its size, orientation, and so forth. Every drawing action takes an HT_Rendition
argument, which specifies the values of the applicable attributes to be
used in drawing the primitive. Of course, different attributes in the rendition
are applicable to different primitives. For a description of the HT_Rendition
class, see appendix A, "Heidi Class Reference." Some drawing actions take
additional arguments giving further data that supplement or override the
attribute values in the rendition argument.
Note: All of the DC (floating point) calls are rounded instead of truncated. Calls less than .5 are rounded down and calls greater than .5 are rounded up. Furthermore, Heidi standard routines convert the DC version to DCI for you. If you are writing a Heidi driver or renderer, you can simply fill in the DCI version and Heidi will do the rest.
The draw_3d_... actions are specified with respect to 3D object coordinates.
For every action draw_2d_xxx, there is a corresponding action draw_dc_xxx with the same xxx part of the name and the same argument list. The difference between draw_2d_xxx and draw_dc_xxx is that the former performs clipping and the latter does not: that is, draw_dc_xxx assumes that the caller has taken care to be sure that the entire figure lies inside the applicable clipping region.
The draw_3d_... routines may include additional data such as vertex or face normals. The draw_dc_... routines have the plainest parameters that are not grouped, whereas the 3d routines try to pack the primitive description into a "rich" data structure. For example, the draw_dc_polyline routine takes a count and an array of vertices, while draw_3d_polyline takes a class HT_Polyline as input.
The draw_2d_... and draw_dc_... drawing actions are subject to the general attributes of color, weight, style, and so forth of the rendition argument but are not directly subject to the lighting and shading attributes.
There are multiple draw_dc_*_image and draw_dc_depth*_image functions but there is only one read_dc_color_image and read_dc_depth_image function. This is because Heidi reads in image data in any image format, and the user does not have to specify the kind of data being read. However, while the users are drawing, they know the image data format and, hence, can specify the respective draw function.
Note: Heidi now draws the last pixel of a polyline in order to facilitate the display of weighted lines rasterized as polytriangles. In the past this pixel was drawn by applications, for example, AutoCAD through its DisplayList, adding it as an extra point to the geometry.
Heidi drivers and renderers need to do Z
interpolation,
while doing a single raster scan, in other words the height of the input
image is 1 in the following image routines:
draw_2d_polyline
draw_2d_polytriangle
draw_2d_polymarker
draw_2d_polygon
draw_2d_ellipse
draw_2d_elliptical_arc
draw_2d_dot
draw_2d_colorized_polyline
draw_2d_colorized_polymarker
draw_2d_colorized_polytriangle
For more information, see the action specifications for each HT_Renderer::draw_... primitive in appendix A, "Heidi Class Reference."
NOTE: For a continuation of Chapter 2, "Heidi Architecture", see the "Renditions" section.