Debugging a Graphics Driver

In this chapter...

Overview

This chapter assumes that you're writing, building, and debugging your graphics driver using the QNX Momentics IDE, and that you're familiar with the IDE and the debugger view. You can debug a graphics driver from the command-line using gdb; the general approach is similar, but you'll have to perform the steps listed here using the command-line equivalent of IDE commands. Since the IDE simply presents a GUI front end to gdb, the steps are similar.

The architecture of the QNX graphics framework complicates graphics driver debugging. Some graphics-related functions involve a message pass to io-display, which then sends a command to the graphics driver, while other functions call directly from the application level via the GF library to the graphics driver. In general, draw commands are in the client address space, while all other commands (such as surface creation and queries) are in the io-display address space.

This means that to debug draw functions, you need to run a client application in the debugger, load the symbols for the driver after the application itself loads the driver, and then trace through your code.

To debug other functions, you need to run io-display in the debugger, and then load the driver symbols. You may also require a client application to call into the driver to create surfaces and make queries.

See the Address spaces and calling rules section of the Writing Graphics Drivers chapter for details about which functions are called from io-display's address space, and which are called from the client application's.

The setup

Let's assume you have two machines: a host machine running the QNX Momentics IDE (Machine A), and your QNX Neutrino target (Machine B). In addition to running a debug session on Machine B, it's helpful if you can telnet to the target machine to run additional commands.


Note: In order to support a debug session on Machine B, be sure to run the inetd services (e.g. as root, type: inetd &) and qconn.

Once you have a telnet session, you should be able to login to Machine B from Machine A and get a shell prompt. If you're using a serial connection to connect to Machine B, you can use the Terminal view in the IDE; otherwise, you'll need to use another telnet client.

As an alternative to telnet, you could use qtalk to communicate to both machines over a serial link.

Compiling shared objects

After downloading the source code from Foundry27 (see Getting the source code in the Writing a Graphics Driver chapter of this guide), you'll first need to compile the shared objects needed by the graphics driver:

  1. Open the driver source code in the IDE.
  2. Make sure you've got a debug variant for the libffb and libdisputil libraries. This depends on how you've set up the project for the driver:
  3. Build the project.

You can individually go in to each subdirectory (ffb, disputil, etc.) and make from the lowest level using the Make Targets view in the IDE, if you choose. You may need to add a make target before building.


Note: Note that the make environment used for the Graphics DDK is the same as the recursive make as described in the Conventions for Recursive Makefiles and Directories in appendix of the QNX Neutrino Programmer's Guide.

You can easily specify the hardware platform (e.g. x86) using the Project Properties dialog.

In order to run our debug driver, we'll need these newer libraries that we just compiled. We'll need to compile against them as well as have them available in our library path so that they're loaded in when we start our driver.

We recommend that you make a backup of the original libdisputil.so and libffb.so objects and then copy the newly compiled ones on top of the existing versions.

Shipping modified libs with your product

If you wish to ship modified versions of the ffb library with your product, you should rename it so that it's not confused with the QNX-supplied library.

You'll also need to modify your driver's common.mk, so that it's linked against the renamed library. Generally, however, you link and test your driver against the QNX-supplied library binaries.

Setting the LD_LIBRARY_PATH environment variable

To make sure the loader can find all the binaries when io-display and the driver are started, you should set the LD_LIBRARY_PATH variable to point at their location. (Or, you could simply install them in /lib/dll, but using the environment variable is probably better for testing purposes).

For example, before starting io-display, type:

export LD_LIBRARY_PATH=/home/barney/test_drivers

The display.conf file

The /etc/system/config/display.conf file tells io-display what driver to load for the graphics hardware it detects using pci, and what settings to use for the display. You need to create an entry in this file to have io-display load your driver on startup. For example:

device {
    drivername=driver_g
    vid=0x10cf
    did=0x201e
    deviceindex=0
    modeopts=/etc/system/config/driver_g.conf
    display {
        xres=640
        yres=480
        refresh=60
        pixel_format=argb1555
    }
}

Then start io-display like this to load your driver_g:

io-display -dvid=0x10cf,did=0x201e

Note:
  • The drivername item specifies the name of the DLL that io-display loads. Be sure to include the “_g” portion of the name to load the debug version of your driver.
  • The modeopts item specifies the location of a driver-specific configuration file.

Making a debug version of a driver

In this example, we'll make a debug version of the devg-chips driver.

  1. Open the devg/chips/nto/x86 directory in the IDE's Navigator view. You'll see a dll directory.
  2. Create a new dll.g directory at the same level as the dll directory.
  3. Copy the Makefile from inside the dll directory to the dll.g directory.

    Note: Both dll and dll.g use the very same Makefile — the directory-naming convention is what tells make to compile with the -g debugging option.

  4. Open the dll.g directory in the IDE's Make Targets view, and add a new “all” target.
  5. Run the target by double-clicking on it, or right-clicking and selecting Build Make Target.

    This should create a devg-chips_g.so shared object.


    Note: We recommend that you copy the debug object to the same spot on disk as the non-debug object. This is okay because they have different names, and you'll invoke them differently.

  6. Copy the debug object to the target using ftp or the IDE's Target File System Navigator view.

    The debug example assumes that the debug object resides under the /lib/dll directory.

Running the debug driver and setting a breakpoint

Now let's try to run your debug driver (devg-chips_g.so) and set a breakpoint on chips_init(), which is a function within the devg-chips_g.so shared object. Open init.c and put a breakpoint in the chips_init() function. This function is called into when an application calls gf_dev_attach().

As mentioned above, the functions in your driver you want to debug determines whether you run io-display or a client application. Because we're going to trace through a function that's called from the io-display address space, let's step through running io-display first.

First, you need to create a launch configuration for io-display:

  1. Right-click the io-display executable in the DDK project, and select Debug As-->Debug…. This opens the Create, manage, and run configurations dialog.
  2. Click New to create a new debug configuration for io-display. The correct project and application information is filled in for you.
  3. Create a target (if you haven't already done so) associated with your target Machine B. Follow the steps outlined in the “Preparing Your Target” and “Debugging Programs” chapters of the IDE User's Guide.
  4. On the Arguments tab, enter the vendor and device ID for the driver, corresponding to the entry in display.config. For example:
        -dvid=0x1002,did=0x5144
        
  5. For the debugger to load the symbols for the driver, we need to add additional source locations to this run configuration. Click on the Source tab and add the source location for your driver.
  6. The debugger also needs to be able to find your driver in order to download it to the target and load it at run-time. On the Debugger tab, click on the Shared Libraries sub-tab, and add the location for the debug version of your graphics driver.
  7. Click Apply to save your changes.

At this point you can run io-display on the target by clicking Debug. By default, a process launched in a debug configuration will stop in the main() function. However, at this point io-display hasn't yet loaded a graphics driver, so the debugger can't load the symbols for devg-chips_g.so. If io-display is paused, run it again. Now pause it the process. In the Shared Libraries tab, you'll see that the graphics driver is now loaded. Right-click devg-chips_g.so and select Load Symbols.

Now you need to run an application that will cause the graphics driver to hit the breakpoint you previously set. You can either create a launch configuration for an application like vsync, or run it from the command line on your target. Switch to the Debug view in the IDE, and you'll see that one of io-display's threads is stopped at the breakpoint in chips_init(). You can trace through the code using the debugger, inspect variables, and so on.