dx8Diagnostics and dx8Dynamic 1.2

Overview

I started out writing a "quick" tool to dump out Direct3D capabilities, for remote debugging purposes. Two days and an endless number of revisions later, I have a library with two subsystems:

dx8Diagnostics
Dumps all kinds of astounding configuration information about the user's computer to an HTML file.
dx8Dynamic
Dynamically links to Direct3D 8, DirectSound 8, and DirectInput 8 at runtime instead of at loadtime, allowing you to tell the user they don't have DirectX 8 (instead of the Windows loader refusing to run you at all), and
dx8Dynamic can be used standalone. However, dx8Diagnostics depends on dx8Dynamic, so it can act intelligently when run on a computer without DirectX 8.

The dx8Diagnostics web page is here:

http://www.midwinter.com/~larry/programming/dx8diagnostics/
And you can download a fresh copy of the source code here:
http://www.midwinter.com/~larry/programming/dx8diagnostics/dx8diagnostics.zip

dx8Dynamic

What It's For

Have you ever gotten one of those support emails that said:
I TRYED TO RUN YUOR GAME BUT IT DOESNT WORKS!!!!!!1
IT SAYS UNABEL TO LOCATE COMPONANT
Or, if you're lucky, the email read:
I attempted to run your game, but it doesn't work on my machine!
It displays the following error message:
	SpaceGoblin.EXE - Unable To Locate Component
	The application has failed to start because D3D8.DLL was not
	found.  Re-installing the application may fix this problem.
and only gives me an "OK" button.
Often, the user gets this message because their computer doesn't have the version of DirectX that your game requires. This is especially true of DirectX 8, as they renamed all the DLLs.

The real problem here is the incomprehensible error message that Windows gives when this happens. If the dialog box instead said something nice, like this:

You don't have DirectX 8 installed! SpaceGoblin requires DirectX 8 or above, and can't run until you install it. Would you like to go to the Microsoft DirectX download site right now?
with lovely Yes and No buttons below, you'd be far less likely to get that annoying support call. But you don't have an opportunity to display a nice dialog box like that, because Windows isn't even running your program. (The error message the user receives is evidence of that.)

Well, now you can display that dialog box, or do anything else you like. dx8Dynamic lets your game start even on machines that don't have DirectX 8 installed. You can detect whether DirectX 8 started up correctly, and if it didn't you can display a nice message and exit cleanly.

How It Works

Whenever Windows runs a program, it goes through the following astonishingly oversimplified process:
  1. Load the .EXE file into memory.
  2. Go down the executable's Import Address Table, and call LoadLibrary() on each DLL listed. If it loads, use GetProcAddress() to find all functions called by the executable.
  3. Jump to the executable's entry point.
(For more on this process, read Matt Pietrek's excellent article An In-Depth Look into the Win32 Portable Executable File Format parts 1 and 2, published in MSDN, February and March 2002.)

The problem occurs in the middle step there, in the statement "if it loads". If Windows fails to load even one of the DLLs listed, the program cannot run, and it displays that tiresome error message. If you use Direct3D 8 normally, your Import Address Table will list d3d8.dll; if your user doesn't have DirectX 8 or above installed, they won't have d3d8.dll, and your game won't run.

dx8Dynamic lets you sidestep the whole problem, by not using Windows to load DirectX—instead, it is loaded manually once your program is already up and running. It loads the DLLs for you, then loads their entry points into global function pointers. The result isn't any slower than using DirectX normally, and it gives you the opportunity to display your own error message.

How To Use It

To use dx8Dynamic:
  1. Include the dx8dynamic.cpp in your project, and include dx8dynamic.h in any files where you use the big DirectX entry points.
  2. When your program starts up, call dx8DynamicStartup().
  3. When your program shuts down, call dx8DynamicShutdown().
  4. Every place you make a call to a global function in DirectX, add the word dx8Dynamic to the front. For instance, instead of calling Direct3DCreate8(), you would call dx8DynamicDirect3DCreate8(). Note that you don't need to touch method calls on COM objects, like the Direct3D object itself.
  5. If you want to check whether DirectX 8 is available, call dx8DynamicIsAvailable(). This returns a BOOL which is TRUE if and only if DirectX 8 is available.

Remarks

dx8Diagnostics

What's It For

For many game developers, the first line of defense when a user is having problems with their game is to say "Could you run DxDiag and send me the results?" And this is a worthwhile endeavor, and has helped lots of developers in solving problems. But there were three things about this procedure I didn't like:
  1. It dumps high-level information about the graphics and sound cards, but tells you nothing about their capabilities. I don't know about you, but I don't remember how many texture stages a GeForce 2 MX has right off the top of my head.
  2. It forces your user to run a program, then go find a file, then send it to you. If your user is a computer novice, walking them through this process can be quite a trial.
  3. Its output is a text file, and its only formatting is in assuming you're reading it using a monospaced font. Oh, how eighties.
Thankfully, the Direct X engineers (hi Jason!) have fixed these deficiencies, starting with Direct X 9—now on store shelves! You can call DxDiag from a COM interface, and dump its information anywhere you like, and said information can be formatted as text or XML. But that's Direct X 9, and I dunno about you, but I'm still laboring away in the backwards, antiquated, stone-knives and bearskins world of Direct X 8.

But wait! What's that up in the sky? Is it a bird? Is it a plane? No, it's dx8Diagnostics to the rescue! dx8Diagnostics has none of these shortcomings of DirectX 8's DxDiag:

  1. It dumps all the DirectX 8 DSCAPS, DIDEVICEINSTANCE, and D3DCAPS fields, as well as all supported D3DDISPLAYMODE modes and all compressed DirectX texture formats (DXT[1-5]) that can be used natively by the card.
  2. It's a library that you can call from your program. You can write the output to a file or to a memory buffer; after that, you can do what you like with it.
  3. Its output is reasonable-looking HTML. (Me, I think it's right purty, but your tastes may vary.)
  4. If used correctly (see Using The Nonce below), your users won't notice that you're using it.
If you'd like to see what its output looks like, you can see it here.

How It Works

There's no magic in this sort of programming. It just takes a lot of reading the Windows and DirectX API documentation, and a willingness to churn through all the different data you could possibly dump out.

How To Use It

To use dx8Diagnostics, add the following files to your project: Also, make sure you're linking against the following libraries: Now you're ready to call it. Here's what a minimal program using dx8Diagnostics looks like:
	if (dx8DiagnosticsStartup() == S_OK)
		{
		dx8Diagnostics *diagnostics;
		dx8DiagnosticsCreate(&diagnostics, "spacegoblin");
		dx8DiagnosticsWrite(diagnostics);
		dx8DiagnosticsDestroy(&diagnostics);

		dx8DiagnosticsShutdown();
		}
This would create a diagnostics object with the default diagnostics printers (all of 'em, but without making the slow WHQL date/time stamp calls), and write the results to a file called dx8diagnostics.html in the current directory.

To write to a different filename, you'd call dx8diagnosticsWriteToFile(diagnostics, filename) before calling dx8diagnosticsWrite(diagnostics).

To write to a buffer in memory, you'd call dx8DiagnosticsWriteToMemory(diagnostics, buffer, bufferSize) before calling dx8DiagnosticsWrite(diagnostics). When dx8Diagnostics is done, the buffer will contain a zero-terminated string that is equivalent to what would have been written to a file. There's currently no mechanism for expanding the buffer; if dx8Diagnostics runs out of space in the buffer, it simply stops writing.

Using The Nonce

Sadly, dx8diagnostics isn't lightning-fast. I've seen it take anywhere from one second to nearly sixteen seconds (!) to write its output. It doesn't seem to be gated on processor speed; rather, it's proportional to the number of devices installed (particularly display devices). So it's probably not a good idea to dump the diagnostics to a file every time your game runs... you'll have some impatient users on your hands.

On the other hand, dumping it only once (say, when your game is installed) and then never again also seems like a bad idea. If your user installs a new display driver (or a new display!), you'd probably like that reflected in your diagnostic output.

The best solution would be to dump the diagnostics either if they haven't been written yet, or if any relevant part of the system has changed. And luckily, DirectX makes it easy to do so—and as of version 1.1 so does dx8diagnostics. This is accomplished using a nonce.

What is a "nonce"? Traditionally, it means "a word of the moment"... a word used once and never again. In computer encryption parlance, it means a unique string that will be emitted once and never be repeated. That's not how I'm using it here; the dx8diagnostics "nonce" is a string that represents the current state of the computer's devices. To produce it, dx8diagnostics iterates over all the devices it would print information about, and gets from DirectX a unique number (a GUID) that represents the combination of that hardware device with its currently installed driver. The "nonce" will therefore change every time the user installs new hardware, or merely installs a new driver for existing hardware.

So how do you use the nonce? The best way is this: every time you cache the diagnostic information to a file, save the current nonce to some separate configuration file. And every time your program runs, calculate the current nonce, and compare it to the nonce you stored in your configuration file. If they're different, it's time to regenerate.

Here's some sample code that does just that:

	if (dx8DiagnosticsStartup() == S_OK)
		{
		dx8Diagnostics *diagnostics;
		dx8DiagnosticsCreate(&diagnostics, "spacegoblin");

		char *nonce = dx8DiagnosticsGetNonce(diagnostics);

		char previousNonce[2048];
		GetPrivateProfileString("diagnostics", "nonce", previousNonce, sizeof(previousNonce), myIniFile);

		if (strcmp(nonce, previousNonce) != 0)
			dx8DiagnosticsWrite(diagnostics);

		dx8DiagnosticsDestroy(&diagnostics);
		dx8DiagnosticsShutdown();
		}
One nice side-effect of this sample code is this: when the game is freshly installed, the stored nonce will presumably be blank (or at least not match the user's computer). That ensures your game will also dump its diagnostics the first time it runs, in addition to every time the user installs new hardware/drivers.

Happily the nonce is quick to generate. In release mode, generating the nonce is usually 2x faster than generating the full diagnostics. On my computer it takes about 0.25 seconds (not counting DirectX one-time startup time). I feel it's safe to calculate the nonce every time your program runs; that way you can always keep the diagnostics current, with only a minor speed hit during startup.

If you measure the time it takes to generate the nonce in yourself, keep in mind that there's some one-time startup code (or something) that happens the first time an application uses some of the DirectX APIs. If I generate the nonce twice in a row, the second time is much faster; if I generate the nonce after doing some other DirectX-y things it's also much faster. This one-time slowdown seems to be built-in to DirectX—sooner or later, your game is going to hit it. So: if you see the nonce take a second or two to calculate, don't worry.

Remarks

Licensing

Here's the license:

/*
** [BEGIN NOTICE]
**
** Copyright (C) 2003 Larry Hastings
**
** This software is provided 'as-is', without any express or implied warranty.
** In no event will the authors be held liable for any damages arising from
** the use of this software.
**
** Permission is granted to anyone to use this software for any purpose,
** including commercial applications, and to alter it and redistribute
** it freely, subject to the following restrictions:
**
** 1. The origin of this software must not be misrepresented; you must not
**    claim that you wrote the original software. If you use this software
**    in a product, an acknowledgment in the product documentation would be
**    appreciated but is not required.
** 2. Altered source versions must be plainly marked as such, and must not be
**    misrepresented as being the original software.
** 3. This notice may not be removed or altered from any source distribution.
**
** The dx8Diagnostics / dx8Dynamic homepage is here:
**		http://www.midwinter.com/~larry/programming/dx8diagnostics/
**
** [END NOTICE]
*/
In non-legalese, my goal was to allow you to do anything you like with the software, except claim that you wrote the original version. If my license prevents you from doing something you'd like to do, contact me (my email address is in the source) and we can discuss it.

The Future

Obviously, it'd be nice if dx8Diagnostics printed out more stuff... processor speed would be nice. But I think I've spent enough time on this thing... maybe some kind programmer somewhere will send me some spiffy dx8DiagnosticsPrinter classes? Maybe?

Version History

1.2
Friday, June 6th, 2003
Note: anyone running version 1.1 is advised to upgrade. See first change.
1.1
Thursday, May 29th, 2003
1.0
Monday, January 20th, 2003
Initial public release.

Happy runtime-linking and diagnosing!


larry