A Method of Avoiding Redraw in Graphics
By Joshua Cantrell
last updated: 6-8-99
INTRODUCTION
In the age of graphics accelerator cards, I find it hard to believe that
anyone would need such an algorithm. I had enough interest at one time
to develop this into a class (although I don't know if I'll ever use it).
Maybe the idea may be useful, maybe all of the gurus already have thought
of it (and found it really quite useless). Whatever the case, I'd like
to present this entertaining algorithm because it's the last one I came up
with before I considered software acceleration a lost cause.
IDEAS BEHIND THE METHOD
The main reason why someone would like to avoid redrawing over pixels
is the slow access time for memory compared to the speed at which
microprocessors run. Faster memories or large caches could help
eliminate this problem, but let's assume we don't have either. What
we need is a way to access memory as little as possible, and that's
what I propose. (Writing to memory can take longer than reading from
memory, so replacing writes to memory with reads from memory can also
help increase the speed of drawing.)
I also considered the move of graphics from low resolutions to high
resolutions. This could mean larger images are trying to be displayed
with relatively small holes or transparent regions. Most of the image would
have pixels to be drawn. An algorithm that simply drew over previously
drawn pixels would have a large amount of cycles used on a redraw. This
would mean a method with little or no overwriting of pixels designed so
that few, yet detailed, holes could be allowed in displayed images would
be beneficial.
One possible method would be to use a z-buffer. Sort the images so that
they are drawn from closest to furthest. This would reduce the number of
redrawn pixels in the image and z-buffer. Of course, a read per pixel
would be required in the z-buffer. Every pixel drawn also requires a write
to the z-buffer, so this causes a penalty of an extra write per pixel.
If there is an incredible amount of overlap, this might pay off in the
end, but I didn't want this two writes per pixel penalty.
My method would still require the images to be sorted and drawn
from closest to furthest. A special buffer of memory would then be used to
determine whether a pixel had been already drawn in a location or not.
Initially this seems like the z-buffer approach, except that we actually
loose the chance to have images with depth. Looking closer however, we
can design this buffer so that it reduces the number of writes and reads
to memory. All we need is an intuitive interface and then build something
to handle the way the buffer is used. All it needs to do is determine
whether pixels are present or not. If it can do this efficiently, more
time can be spent drawing to memory than anything else.
MASS SKIP AND DRAW
One way to organize the data to increase the efficiency of drawing would
be to make the buffer contain information about horizontal lines of pixels
(henceforth known as a run). Instead of saying if a particular pixel is
set or not, it can say whether or not a whole row of pixels are set.
Assuming our graphics are drawn using runs, large numbers of pixels can
be skipped (not even checked) if we can determine that they will not
be drawn. Similarly, large numbers of pixels can be drawn without
further checking.
An important consideration once we have decided to represent the buffer
as a row of set/unset runs would be the data structure to store them in.
Since we will undoubtantly be modifying the size of the runs and perhaps
adding new set runs in the middle of unset runs, it needs to be a structure
which facilitates adding new runs in between old ones. A linked list type
structure would appear to be the best structure in this case.
When checking to see if a run of pixels is either set or unset, we must
start from the front of the list and search for the correct position in
the list. Once we have located the starting location we can tell how
many pixels in a row are either set or unset. Now we have another
problem! If we have hundreds of thousands of pixels, we may have hundreds
of unset runs! This wouldn't be good because drawing graphics found
at the end of the list might take awhile! We must find a way
to either shorten the list or add a random access element to our buffer...
or both.
DIVIDE AND CONQUER
If the buffer was made of multiple linked lists, we could reduce the
problems involved in traversing them. The linked lists would be
divided equally across the entire display. This way each linked list
could be accessed given a pixel location. Since the linked lists would
be reached via array addressing, it would be a random access process
(not as slow as moving through a huge linked list).
There would still be some linked list traversal, but instead of actually
moving through hundreds of thousands of pixels, we'd only move through
a specified number of pixels. Maybe a thousand. Maybe a hundred or so.
The memory allocated for the links in the list should be static instead
of dynamic (each link should not be freed and deleted separately). For
this reason I suggest allocating a memory block proportional to the size
of the graphics display. This should hold enough memory for the needed
links.
LINKED LIST DESIGN
Since we allocate an amount of memory proportional to the number of pixels
in the display, the simplest type of linked list structure to use would be
one where the data doubled as a pointer to the next link. The data would
be a number describing the number of following set or unset pixels. At
the same time this value could be added to the pointer to the current link
and advance it to the next link. The way this would work is by switching
from filled to empty runs. If the current one was filled
the next should be empty and visa versa.
Now we simply need to determine what the first run is: filled or
empty. There are two ways of dealing with this problem. One
method requires using the fact that none of the runs can be of zero
length. The other sets aside the most significant bit as a flag to
let us know whether the run is filled or empty.
If the first value is zero, it means that this one pixel isn't in the default
state (the default probably being unset, so it would be set). The following
pixels would then be treated as though they are filled (set). Now there is
the possibility that an unset pixel follows this single set pixel, in which
case another zero could indicate that it isn't set. This alternating case
could go on for awhile, but once it's broken, it shouldn't occure anymore
in the list. This isolates this special case to the beginning of the list.
If the most significant bit is used as a flag so that we know what the
first run is, we can know what type of run follows because it will always
be the alternate (and can subsequently ignore the flag bit). The problem
with using the significant bit as a flag is the need to check the most
significant bit with a mask and then remove it. These are two operations
which could use up precious cycles when we could possibly have been simply
checking for a zero.
PUTTING IT ALL TOGETHER
Now we know how to traverse each list and find each list. There's just
one problem. The lists are not doubly linked lists, so we need to keep track
of the previous link so we can modify a previous link if necessary. This
means we need at least two pointers at all times while traversing a list, and
a way to tell which type of run we are on (empty or filled). In my
implementation I had an additional pointer called the goal pointer.
Its function is to point at the start of any draw (or erase?) operation.
Depending on the type of run it is in, you know whether pixels should be
drawn here or you should skip ahead and draw pixels elsewhere. If you fill
from the goal pointer you know how to manipulate the pointers in the
linked list to represent the newly filled area. This could be simply changing
current pointers in the linked list, or adding new links. Don't forget
to combine any neighboring filled or empty areas caused by this operation!
If you don't, you'll get a performance hit due to fragmentation!
COMPLETED DESIGN
The finished product merely consists of functions that give you information
about the runs available. For example, when drawing a graphic, you'll
want to know where an empty run is so you can draw something there. This
is an opportunity to define the function: findEmpty. This would
take an offset (place to start searching) and a range to search within.
It would then return the place on the screen where something could be
drawn and the number of pixels that could be drawn at that position. Other
functions would be necessary for filling the buffer, like fillEmpty
and emptyFill. All of these should be fairly
straightforward to implement given the information above.
MY SOURCE CODE
I attempted to implement this with some success. However I haven't tested
my code comprehensively. This is due to the fact that I'm now more
interested in accelerated graphics cards, and I think I may have made my
class more complex than necessary. I chose to use the most significant
bit flag to determine the type of run at the beginning of a linked list,
although I think I would have preferred to use a zero instead. I also have
a confusing method of using three pointers, where I think the code could work
just as well with two pointers and something to indicate the type of
run the goal pointer is on, as I discussed above.
With these many downfalls, I believe the code is as clearly commented as
possible. I also tested and debugged it with some simple cases, so I know
it's not as buggy as when I first typed it. Send any bug reports to me,
and let me know if you use my code in any of your own programs.
joshuacantrell@yahoo.com
(My Home Page)
- (My Computer Science Page)
- (My Personal Page)
- (My Resume)