Building a 3D Engine in Perl, Part 4
by Geoff Broadwell
|
Pages: 1, 2, 3, 4, 5, 6, 7, 8
Display Lists
I expect no one will find it surprising that OpenGL provides exactly this function, with the display lists facility. A display list is a list of OpenGL commands to execute to perform some function. The OpenGL driver stores it (sometimes in a mildly optimized format) and further code refers to it by number. Later, the program can request that OpenGL run the commands in some particular list as many times as desired. Lists can even call other lists; a bicycle model might call a wheel display list twice, and the wheel display list might itself call a spoke display list dozens of times.
I added init_models to create display lists for each shape I
want to model:
sub init_models
{
my $self = shift;
my %models = (
cube => \&draw_cube,
);
my $count = keys %models;
my $base = glGenLists($count);
my %display_lists;
foreach my $model (keys %models) {
glNewList($base, GL_COMPILE);
$models{$model}->();
glEndList;
$display_lists{$model} = $base++;
}
$self->{models}{dls} = \%display_lists;
}
%models associates each model with the code needed to draw it.
Because the engine already knows how to draw a cube, I simply reused
draw_cube here. The next two lines begin the work of building the
display lists. The code first determines how many display lists it needs and
then calls glGenLists to allocate them. OpenGL numbers the
allocated lists in sequence, returning the first number in the sequence (the
list base). For example, if the code had requested four lists, OpenGL
might have numbered them 1051, 1052, 1053, and 1054, and would then return 1051
as the list base.
For each defined model, init_models calls
glNewList to tell OpenGL that it is ready to compile a new display
list at the number $base. OpenGL then prepares to convert any
subsequent OpenGL calls to entries in the list, rather than rendering them
immediately. If I had chosen GL_COMPILE_AND_EXECUTE instead of
GL_COMPILE, OpenGL would perform the rendering and save the calls
in the display list at the same time. GL_COMPILE_AND_EXECUTE is
useful for on-the-fly caching when code needs active rendering anyway. Because
init_models is simply precaching the rendering commands and
nothing should render while this occurs, GL_COMPILE is the better
choice.
The code then calls the drawing routine, which conveniently submits all of
the OpenGL calls needed for the new list. The call to glEndList
then tells OpenGL to stop recording entries in the display list and return to
normal operation. The model loop then records the display list number used by
the current model in the %display_lists hash, and increments
$base for the next iteration. After processing all of the models,
init_models saves %display_lists into a new structure
in the engine object.
init calls init_models just before
init_objects:
$self->init_models;
$self->init_objects;
With this initialization in place, the next step was to change
draw_view to draw from either a model or a draw routine. To do
this, I replaced the $o->{draw}->() call with:
if ($o->{model}) {
my $dl = $self->{models}{dls}{$o->{model}};
glCallList($dl);
}
else {
$o->{draw}->();
}
If the object has an associated model, draw_view looks up the
display list in the hash created by init_models, and then calls
the list using glCallList. Otherwise, draw_view falls
back to calling the object's draw routine as before. A quick run confirmed that
the fallback works and adding init_models didn't break anything,
so it was safe to change init_objects to use models instead of
draw routines for the cubes. This involved replacement of just three lines--I
changed each copy of:
draw =& \&draw_cube,
to:
model =& 'cube',
Suddenly, the engine was much faster and more responsive. A
dprofpp run confirmed this:
$ dprofpp -Q -p step068
Done.
$ dprofpp -I -g main::main_loop
Total Elapsed Time = 4.053240 Seconds
User+System Time = 0.973250 Seconds
Inclusive Times
%Time ExclSec CumulS #Calls sec/call Csec/c Name
99.9 - 0.973 1 - 0.9733 main::main_loop
86.5 0.024 0.842 413 0.0001 0.0020 main::do_frame
58.1 0.203 0.566 413 0.0005 0.0014 main::draw_view
56.9 0.016 0.554 413 0.0000 0.0013 main::draw_frame
20.1 0.196 0.196 413 0.0005 0.0005 SDL::GLSwapBuffers
19.3 - 0.188 413 - 0.0005 SDL::App::sync
18.4 - 0.180 413 - 0.0004 main::end_frame
16.7 0.163 0.163 2891 0.0001 0.0001 SDL::OpenGL::CallList
9.14 0.028 0.089 413 0.0001 0.0002 main::do_events
8.53 0.035 0.083 413 0.0001 0.0002 main::prep_frame
6.68 0.008 0.065 413 0.0000 0.0002 main::process_events
5.03 0.049 0.049 3304 0.0000 0.0000 SDL::OpenGL::GL_LIGHTING
4.93 0.002 0.048 413 0.0000 0.0001 SDL::Event::pump
4.73 0.046 0.046 413 0.0001 0.0001 SDL::PumpEvents
4.11 0.012 0.040 413 0.0000 0.0001 main::update_time
Note that I had to run dprofpp -Q -p again with the new code
before doing the analysis, or dprofpp would have just reused the
old tmon.out.
The first thing to note in this report is that previously the engine only
managed seven frames (calls to do_frame) before timing out, but now
managed 413 in the same time! Secondly, as intended, main_loop
never calls draw_cube, having replaced all such calls with calls
to glCallList. Because of this it is no longer necessary to do
many thousands of low-level OpenGL calls to draw the scene each frame, with the
attendant Perl and XS overhead. Instead, the OpenGL driver handles all of those
calls internally, with minimal overhead.
This has the added advantage that it is now feasible to run the engine on
one computer and display the window on another, as the OpenGL driver on the
displaying computer saves the display lists. Once init_models
compiles the display lists, they are loaded into the display driver, and future
frames require minimal network traffic to handle glCallList.
(Adventurous users running X can do this by logging in locally to the display
computer, sshing to the computer that has the engine and SDL_perl
on it, and running the program there. If your ssh has X11
forwarding turned on, your reward should be a local window. And there was much
rejoicing.)

