Sign In/My Account | View Cart  
advertisement


Listen Print Discuss

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.)

Pages: 1, 2, 3, 4, 5, 6, 7, 8

Next Pagearrow