Screensavers Redux: The DirectX 8.0 Screensaver Framework

 

Philip Taylor
Microsoft Corporation

August 20, 2001

Download D3DSaver.exe and DX8Sparkles.exe.

In this month's issue of Driving DirectX, I showcase a new screensaver framework for Microsoft® DirectX® 8.0. The DirectX 8.0 screensaver framework is full featured and goes far beyond the DirectX 7.0 SDK screensaver framework by providing test, configure, and run modes along with preview, password, and multi-monitor support. This is a far cry from the bare-bones support of the DirectX 7.0 framework.

In addition to updating MSDNSparkles from the September 2000 column, Alpha Sparkles, to use the new framework and DirectX 8.0, I rework it to use both vertex and pixel shaders. Next month I will continue to examine pixel shaders and will present more detailed examples, but for now simple will suffice.

MSDNSparkles provides a simple yet effective combination of a billboard system coupled to a particle system, rendered using texture/color modulation and alpha-blending to produce some reasonably nice visuals.

Figure 1. MSDNSparkles screenshot

In this combination, the billboard system provides the geometry (front-facing quads, or billboards); the particle system provides the animation (positional as well as texture and color animation); while the rendering is texture/color modulated quads with ONE:ONE framebuffer alpha-blending. Figure 1 shows a screenshot of MSDNSparkles on modern shader hardware.

Examining the screensaver framework is the first order of business. Once its clear what the pieces of the framework are—and how to incorporate them in creating a new screensaver—I will briefly review what Sparkles does and how to embed rendering the "sparkles billboard system" within the screensaver framework. I will also explain how to create a config dialog, an important part of a professional-looking screensaver. Finally, the modifications necessary to Sparkles to enable shader-based rendering will round out the coverage.

DirectX 8.0 Screensaver Framework

The Direct3D screensaver framework consists of three parts: the infrastructure, the overridable methods, and the config dialog. The screensaver framework provides an implementation of a CD3DScreensaver class as the infrastructure and support that all screensavers inherit. The implementation is contained in the source file d3dsaver.cpp and header file d3dsaver.h. These files are meant to extend the functionality provided in the "common" folder, and can simply be dropped in the src and include folders respectively. I am not going to cover most of what is supplied in the framework source, due to time and space considerations. I will briefly cover the overridable methods, the configuration dialog, and multi-monitor support.

The overridable methods are defined in the CD3DScreensaver class, and are shown here:

virtual VOID    SetDevice( UINT iDevice )                { }
virtual HRESULT RegisterSoftwareDevice()                 { return S_OK; }
virtual HRESULT ConfirmDevice(D3DCAPS8* pCaps, DWORD dwBehavior, 
                              D3DFORMAT fmtBackBuffer)   { return S_OK; }
virtual HRESULT ConfirmMode( LPDIRECT3DDEVICE8 pd3dDev ) { return S_OK; }
virtual HRESULT OneTimeSceneInit()                       { return S_OK; }
virtual HRESULT InitDeviceObjects()                      { return S_OK; }
virtual HRESULT RestoreDeviceObjects()                   { return S_OK; }
virtual HRESULT FrameMove()                              { return S_OK; }
virtual HRESULT Render()                                 { return S_OK; }
virtual HRESULT InvalidateDeviceObjects()                { return S_OK; }
virtual HRESULT DeleteDeviceObjects()                    { return S_OK; }
virtual HRESULT FinalCleanup()                           { return S_OK; }

All you need to do to create a new D3D application using the sample framework is to create a new project and new implementations of the overridable methods to generate a new framework sample. Below is a brief description of the methods used here:

  • OneTimeSceneInit is called by the framework when the application starts up, and can be used to initialize structures that aren't tied to a D3DDevice.
  • The InitDeviceObjects method can be used to create per-device objects.
  • RestoreDeviceObjects is used to recreate objects after a Reset on the D3DDevice.
  • The FrameMove method above allows us to specify animation actions that happen once per frame.
  • The Render method performs the drawing for the screensaver.
  • InvalidateDeviceObjects provides a method for releasing per-device objects before a Reset on the D3DDevice.
  • DeleteDeviceObjects provides a method for deleting per-device objects.
  • ConfirmDevice is used to validate the device against the rendering requirements of the screensaver.
  • SetDevice is special; it plays a role in enabling multi-monitor support.
  • I don't use ConfirmMode or RegisterSoftwareDevice, so I won't discuss them.

I want to reiterate that one important distinction to remember is between InitDeviceObjects/RestoreDeviceObjects and InvalidateDeviceObjects/DeleteDeviceObjects. Init/Delete are called when you are creating/destroying a device completely; Restore/Invalidate are called before/after calling Reset on a device, which happens when you change some aspects of the device or have to deal with a lost device. To make a well-formed new framework sample, you need to understand where to put your code—for example, managed textures can be created or destroyed in Init/Delete, but unmanaged textures need to be created or destroyed in Restore/Invalidate.

Configuration Dialog Support

The configuration dialog support consists of 3 parts: pre-defined resource IDs, several "must-have" controls for the screensavers' configuration dialog, and the per-monitor tabs that help enable multi-monitor support. Figure 2 shows a screenshot of the MSDNSparkles configuration dialog.

Figure 2. Config dialog

The predefined resource IDs are part of the MSDNSparkles project. It is best to simply copy them into your project. You need to use these so that the configuration dialog, the multi-monitor tabs, and various error messages are correctly propagated to your screensaver.

Each screensaver that uses the framework should provide both a Screen Settings button and a checkbox to indicate whether the same image should be shown on all monitors or each monitor render its own image. Here I use the "show same image" checkbox for that duty. The special control for image rendering uses predefined ID IDC_SAME. The special control for screen settings doesn't need a hard-coded resource ID, but the screensaver configuration dialog procedure should launch the settings dialog using code like this:

case IDC_SCREENSETTINGS:
   DoScreenSettingsDialog( hwndDlg ); 

When the screensaver configuration dialog procedure executes that code, a tabbed dialog containing tabs for each monitor pops up, allowing the user to configure the screensaver for each monitor, as shown in Figures 3 and 4 below.

Figure 3. Monitor 1

Figure 4. Monitor 2

Multi-Monitor Support

In addition to the nifty tabbed dialogs to allow users to configure the screensaver on a per-monitor basis, there are several other key parts of the screensaver framework multi-monitor support: RenderUnits, per adapter deviceobject support, the SetDevice method, and a tweak to the Render() overridable method.

Without going into exhaustive detail, the framework uses the concept of a RenderUnit, defined in the structure shown below, to capture per-adapter information and enable multi-monitor support.

struct RenderUnit
{
    UINT                  iAdapter;
    UINT                  iMonitor;
    D3DDEVTYPE            DeviceType;      // Reference, HAL, etc.
    DWORD                 dwBehavior;
    IDirect3DDevice8*     pd3dDevice;
    D3DPRESENT_PARAMETERS d3dpp;
    BOOL                  bDeviceObjectsInited; 
    BOOL                  bDeviceObjectsRestored;
    TCHAR                 strDeviceStats[90];
    TCHAR                 strFrameStats[40]; 
};

If you are interested in how the framework performs its multi-monitor magic, read the code to see how RenderUnits are used.

Once multi-monitor support is enabled in the framework, the screensaver has work to do in order to operate correctly. The first change is to be multi-device aware. This means all per-device objects, initialized in InitDeviceObjects() and RestoreDeviceObjects(), need to be modified. The concept of a device objects pointer is introduced in the screensaver class, as shown below:

class   CSparklesScreensaver : public CD3DScreensaver
{
protected:
    DeviceObjects  m_DeviceObjects[MAX_DEVICE_OBJECTS];
    DeviceObjects* m_pDeviceObjects;
….
}

And a per-device structure is used in the screensaver, as shown below, to contain the per-device objects:

struct DeviceObjects
{
    CD3DFont* m_pStatsFont;
   //sparkles particle geometry
   LPDIRECT3DVERTEXBUFFER8 m_pvbSparkles;
   LVertex* pVertices;
   LPDIRECT3DINDEXBUFFER8  m_pibSparkles;
   WORD* pIndices;
   //sparkles textures
   LPDIRECT3DTEXTURE8      m_pSparkleTextures[MAX_TEXTURES];
   //shaders
   DWORD      m_hVertexShader;
   DWORD      m_hPixelShader;
   //using pixel shaders?
   BOOL      m_bUsingPixelShaders;
};

Now we just need a way to set the m_pDeviceObjects pointer to the right struct at the right time. Enter SetDevice. SetDevice provides just such a vehicle. The framework calls this method before switching between devices, to allow clients of the framework to adjust their internals to point to the correct device objects. SetDevice for MSDNSparkles is shown below:

VOID CSparklesScreensaver::SetDevice( UINT iDevice )
{
   m_pDeviceObjects = &m_DeviceObjectsArray[iDevice];   
}

Here the screensaver sets the m_pDeviceObjects member that the sample framework uses, and all is well in terms of correct rendering to each device.

Finally, each screensaver must set the projection matrix in the Render method, so the framework can correctly use the setting of the special control that allows the user to specify whether the screensaver generates the same image on every monitor or one image. I use helper method BuildProjectionMatrix, which returns the projection matrix so that the screensaver framework can manage the screensaver across multiple monitors, as shown below:

BuildProjectionMatrix( 1.0f, 100.0f, &proj );

The returned projection matrix is used later in the shader processing.

MSDNSparkles in DirectX 8.0 Trappings

With those details out of the way, let's get started examining the details of MSDNSparkles. There are two parts to consider: the implementation of the overridable methods, and the mini-API implemented internal to MSDNSparkles.

Overridable Methods

I am going to examine a subset of the available overridable methods, as these are the only parts used in MSDNSparkles, shown here:

virtual HRESULT ConfirmDevice(D3DCAPS8* pCaps, DWORD dwBehavior, 
                              D3DFORMAT fmtBackBuffer)   { return S_OK; }
virtual HRESULT InitDeviceObjects()                      { return S_OK; }
virtual HRESULT RestoreDeviceObjects()                   { return S_OK; }
virtual HRESULT FrameMove()                              { return S_OK; }
virtual HRESULT Render()                                 { return S_OK; }
virtual HRESULT InvalidateDeviceObjects()                { return S_OK; }
virtual HRESULT DeleteDeviceObjects()                    { return S_OK; }

ConfirmDevice

The MSDNSparkles ConfirmDevice method first verifies that the device can do ONE:ONE alpha blending. This is the simplest form of alpha; in my experience, all cards can perform this alpha blending. As long as this support exists, then vertex shader support is checked. If we don't have at least vertex shader support, this screensaver cannot execute. If pixel-shader support exists, the screensaver will run using pixel shaders, if not, the fixed-function, multi-texture pipeline will be used. Below is the MSDNSparkles implementation of ConfirmDevice.

HRESULT CSparklesScreensaver::ConfirmDevice( D3DCAPS8* pCaps, DWORD dwBehavior,
D3DFORMAT Format )
{   
    // Make sure device can do ONE:ONE alphablending
    if( 0 == ( pCaps->SrcBlendCaps & D3DPBLENDCAPS_ONE ) )
        return E_FAIL;
    if( 0 == ( pCaps->DestBlendCaps & D3DPBLENDCAPS_ONE ) )
        return E_FAIL;
   //always use vertex shaders 1.1
   if( (dwBehavior & D3DCREATE_HARDWARE_VERTEXPROCESSING ) ||
      (dwBehavior & D3DCREATE_MIXED_VERTEXPROCESSING ) )
   {
      if( pCaps->VertexShaderVersion < D3DVS_VERSION(1,1) )
         return E_FAIL;
   }

   return S_OK;
}

InitDeviceObjects

The InitDeviceObjects method determines whether pixel-shader support exists, and if so, enables the use of pixel shaders. This method also initializes the shader handles for the device to the known uninitialized state.

HRESULT CSparklesScreensaver::InitDeviceObjects()
{
   D3DCAPS8 l_d3dCaps;
   m_pd3dDevice->GetDeviceCaps(&l_d3dCaps);

   if( D3DSHADER_VERSION_MAJOR( l_d3dCaps.PixelShaderVersion ) >= 1 ) 
   {
      if( D3DSHADER_VERSION_MINOR( l_d3dCaps.PixelShaderVersion ) >= 1 ) 
         m_pDeviceObjects->m_bUsingPixelShaders = TRUE;
      else
         m_pDeviceObjects->m_bUsingPixelShaders = FALSE;
   }
   
   //so dont set until created at restore time
   m_pDeviceObjects->m_hVertexShader   = 0xffffffff;
   m_pDeviceObjects->m_hPixelShader   = 0xffffffff;

   return S_OK;
}

RestoreDeviceObjects

The RestoreDeviceObjects method uses the mini-API defined internal to MSDNSparkles to create textures, geometry, and shaders—using CreateTextures, CreateGeometry, and CreateShaders respectively. Then we initialize device state and set the geometry state of the device using InitStates and InitStreams. This method also creates the statistics font used to show current frame-rate, driver, and rendering statistics.

HRESULT CSparklesScreensaver::RestoreDeviceObjects()
{
   if( m_pd3dDevice == NULL )
      return S_OK;

   //create textures
   CreateTextures(m_pd3dDevice);
   
   //create geometry, vb and ib
   CreateGeometry(m_pd3dDevice);

   //create shaders
   CreateShaders(m_pd3dDevice);

   //init device state
   InitStates(m_pd3dDevice);

   //init device geometry
   InitStreams(m_pd3dDevice);

   //stats font
   m_pDeviceObjects->m_pStatsFont = new CD3DFont( _T("Arial"), 12, 
D3DFONT_BOLD );
   m_pDeviceObjects->m_pStatsFont->InitDeviceObjects( m_pd3dDevice );
   m_pDeviceObjects->m_pStatsFont->RestoreDeviceObjects();

   return S_OK;
}

InvalidateDeviceObjects

InvalidateDeviceObjects provides a method for the screensaver to release device objects, either for a Reset on the device or for some other operation that means loss of video memory. It does this using mini-API methods ReleaseGeometry, ReleaseTextures, and ReleaseShaders.

HRESULT CSparklesScreensaver::InvalidateDeviceObjects()
{
   //stats
   m_pDeviceObjects->m_pStatsFont->InvalidateDeviceObjects();


   //geometry
   ReleaseGeometry(m_pd3dDevice);
   
      //textures
   ReleaseTextures(m_pd3dDevice);

   //shaders
   ReleaseShaders(m_pd3dDevice);

   return S_OK;
}

DeleteDeviceObjects

The DeleteDeviceObjects method deletes all device objects owned by the statistics font, and then uses the SAFE_DELETE macro to delete the object itself.

HRESULT CSparklesScreensaver::DeleteDeviceObjects()
{   
   //stats
   m_pDeviceObjects->m_pStatsFont->DeleteDeviceObjects();
   SAFE_DELETE( m_pDeviceObjects->m_pStatsFont );

   return S_OK;
}

FrameMove

The FrameMove method uses mini-API method UpdateSparkles to update the particle system simulation.

HRESULT CSparklesScreensaver::FrameMove( LPDIRECT3DDEVICE7 pd3dDevice,
 FLOAT fTimeKey )
{
   // ok, now play with sparkles
   UpdateSparkles();   
   
    return S_OK;
}

Render

The Render method is pretty simple. First we set the transforms and shaders using the mini-API routines. Then we clear and enter the BeginScene/EndScene pair. Within the pair, we set the geometry on the device using SetStreams, and then use DrawSparkles to draw the billboarded particle system. Finally, we use CalcRate to calculate a framerate, and then display the statistics info using the statistics font. Too easy!

HRESULT CSparklesScreensaver::Render()
{
   D3DXVECTOR3       from(0.0f, 0.0f, 0.0f);
   D3DXVECTOR3       at(  0.0f, 0.0f, 0.0f);
   D3DXVECTOR3       up(  0.0f, 1.0f, 0.0f);

   // set transforms up for vshader
   SetTransforms( m_pd3dDevice, from, at, up );

   // set shaders
   SetShaders( m_pd3dDevice);

   // Clear the viewport
   m_pd3dDevice->Clear( 0L, NULL, D3DCLEAR_TARGET,//no zbuffer required
                  m_bckColor, 
                  1.0f, 0L );

   // Begin the scene
   if( SUCCEEDED( m_pd3dDevice->BeginScene() ) )
   {
      //reset streams for sparkles
      if ( m_bShowStats)      
         InitStreams(m_pd3dDevice);

      // Draw sparkles
      DrawSparkles( m_pd3dDevice, from, at, up );

      // calc rates and scale
      CalcRate(m_pd3dDevice,m_fTime);

      // Show stats
      if ( m_bShowStats)
      {
         m_pDeviceObjects->m_pStatsFont->DrawText( 3,  1, 
            D3DCOLOR_ARGB(255,0,0,0), m_strFrameStats );
         m_pDeviceObjects->m_pStatsFont->DrawText( 2,  0, 
            D3DCOLOR_ARGB(255,255,255,0), m_strFrameStats );

         m_pDeviceObjects->m_pStatsFont->DrawText( 3, 21, 
            D3DCOLOR_ARGB(255,0,0,0), m_strDeviceStats );
         m_pDeviceObjects->m_pStatsFont->DrawText( 2, 20, 
            D3DCOLOR_ARGB(255,255,255,0), m_strDeviceStats );
     
         m_pDeviceObjects->m_pStatsFont->DrawText( 3, 41, 
            D3DCOLOR_ARGB(255,0,0,0), m_strSparkleStats );
         m_pDeviceObjects->m_pStatsFont->DrawText( 2, 40, 
            D3DCOLOR_ARGB(255,255,255,0), m_strSparkleStats );    
      }   

      // End the scene.
      m_pd3dDevice->EndScene();
   }

   return S_OK;
}

That finishes off the implementation of MSDNSparkles' overridable methods.

The Internal Mini-API

This screensaver uses a mini-API internally to cut down clutter. There are three parts to the internals: the Sparkles API, the object-management API, and the shader API.

The Sparkles Methods

The Sparkles methods are essentially unchanged from the previous implementation. The methods are listed below, DrawSparkles, UpdateSparkles, InitSparkles, and RandomSparkles. Since these are essentially the same, again, I will refer you to the previous article, Alpha Sparkles.

BOOL    DrawSparkles( LPDIRECT3DDEVICE8 pd3dDevice, D3DXVECTOR3 from, 
D3DXVECTOR3 at, D3DXVECTOR3 up);
BOOL    UpdateSparkles( LPDIRECT3DDEVICE8 pd3dDevice );
VOID    InitSparkles();
Sparkle RandomSparkle();

The D3D Object Management Methods

The D3D object management methods consist of two parts: lifetime management and state management. D3D objects need to be created and released as devices are created and released (or reset), so it makes sense for these methods to reflect this. We have creation methods for geometry, textures, and shaders—for example, CreateGeometry, CreateTextures, and CreateShaders. D3D objects need to be set on the device, so we have state management methods InitStates and InitStreams to set device state and set geometry state. We also need to deal with transform state for the device, and have routine SetTransforms. These method prototypes are shown below:

//d3d object lifetime mgmt
BOOL    CreateGeometry( LPDIRECT3DDEVICE8 pd3dDevice );
BOOL    ReleaseGeometry( LPDIRECT3DDEVICE8 pd3dDevice );   
BOOL    CreateTextures( LPDIRECT3DDEVICE8 pd3dDevice );
BOOL    ReleaseTextures( LPDIRECT3DDEVICE8 pd3dDevice );
// obj state   
BOOL    InitStreams( LPDIRECT3DDEVICE8 pd3dDevice );
BOOL    InitStates( LPDIRECT3DDEVICE8 pd3dDevice );   
BOOL    SetTransforms( LPDIRECT3DDEVICE8 pd3dDevice, 
              D3DXVECTOR3& from, D3DXVECTOR3 at, D3DXVECTOR3 up   );

CreateGeometry

The CreateGeometry method creates an indexbuffer and a vertexbuffer for use by the particle-system renderer. This means the renderer makes use of DrawIndexedPrimitive. The index buffer is initialized here, since we have a fixed set of particles to render.

BOOL CSparklesScreensaver::CreateGeometry( LPDIRECT3DDEVICE8 pd3dDevice )
{
   //create index buffer
   if ( FAILED( m_pd3dDevice->CreateIndexBuffer( MAX_SPARKLES * 6 * 
sizeof(WORD), 
                              D3DUSAGE_WRITEONLY|D3DUSAGE_DYNAMIC, 
                              D3DFMT_INDEX16, 
                              D3DPOOL_DEFAULT,
                              &m_pDeviceObjects->m_pibSparkles) ) )
      return E_FAIL;

   // Set up indices
   m_pDeviceObjects->m_pibSparkles->Lock( 0,MAX_SPARKLES*6*sizeof(WORD),   
(BYTE**)&m_pDeviceObjects->pIndices,   0);      
   if ( m_pDeviceObjects->pIndices == 0 ) 
      return E_FAIL;

   for( int i=0; i<MAX_SPARKLES; i++ )
   {
      m_pDeviceObjects->pIndices[i*6+0] = 4*i + 0;
      m_pDeviceObjects->pIndices[i*6+1] = 4*i + 1;
      m_pDeviceObjects->pIndices[i*6+2] = 4*i + 2;
      m_pDeviceObjects->pIndices[i*6+3] = 4*i + 0;
      m_pDeviceObjects->pIndices[i*6+4] = 4*i + 2;
      m_pDeviceObjects->pIndices[i*6+5] = 4*i + 3;
   }
   m_pDeviceObjects->m_pibSparkles->Unlock();
   
   //create vertex buffers
   if( FAILED( m_pd3dDevice->CreateVertexBuffer( MAX_SPARKLES*6* 
sizeof(LVertex),                           
D3DUSAGE_WRITEONLY|D3DUSAGE_DYNAMIC, 
      LVertexFVF,
      D3DPOOL_DEFAULT,
            &m_pDeviceObjects->m_pvbSparkles ) ) )
      return E_FAIL;   

   return TRUE;
}

ReleaseGeometry

The ReleaseGeometry method uses the SAFE_RELEASE macro to release the indexbuffer and vertexbuffer. Remember this method is used by InvalidateDeviceObjects.

BOOL CSparklesScreensaver::ReleaseGeometry( LPDIRECT3DDEVICE8 pd3dDevice )
{
   //geom
   SAFE_RELEASE( m_pDeviceObjects->m_pibSparkles );
   SAFE_RELEASE( m_pDeviceObjects->m_pvbSparkles );

   return TRUE;
}

CreateTextures

The CreateTextures method initializes a list of texture names, stored in the resource, and then uses that list to call D3DX function D3DXCreateTextureFromResourceEx to load the textures.

BOOL CSparklesScreensaver::CreateTextures( LPDIRECT3DDEVICE8 pd3dDevice )
{
   //set up bitmap names
   memcpy(m_szSparkleTextures[0],"dx5.bmp",sizeof("dx5.bmp"));
   memcpy(m_szSparkleTextures[1],"dx8.bmp",sizeof("dx8.bmp"));
   memcpy(m_szSparkleTextures[2],"flare1.bmp",sizeof("flare1.bmp"));
   memcpy(m_szSparkleTextures[3],"flare2.bmp",sizeof("flare2.bmp"));
   memcpy(m_szSparkleTextures[4],"flare3.bmp",sizeof("flare3.bmp"));
   memcpy(m_szSparkleTextures[5],"flare4.bmp",sizeof("flare4.bmp"));
   memcpy(m_szSparkleTextures[6],"flare5.bmp",sizeof("flare5.bmp"));
   memcpy(m_szSparkleTextures[7],"flare6.bmp",sizeof("flare6.bmp"));
   memcpy(m_szSparkleTextures[8],"flare7.bmp",sizeof("flare7.bmp"));
   memcpy(m_szSparkleTextures[9],"flare8.bmp",sizeof("flare8.bmp"));
   memcpy(m_szSparkleTextures[10],"shine1.bmp",sizeof("shine1.bmp"));
   memcpy(m_szSparkleTextures[11],"shine2.bmp",sizeof("shine2.bmp"));
   memcpy(m_szSparkleTextures[12],"shine3.bmp",sizeof("shine3.bmp"));
   memcpy(m_szSparkleTextures[13],"shine4.bmp",sizeof("shine4.bmp"));
   memcpy(m_szSparkleTextures[14],"shine5.bmp",sizeof("shine5.bmp"));
   memcpy(m_szSparkleTextures[15],"shine6.bmp",sizeof("shine6.bmp"));

   //load the bitmaps
   for ( int t = 0; t < NumTextures ;t++)
   {
      D3DXCreateTextureFromResourceExA( m_pd3dDevice, 0, 
(const char *)m_szSparkleTextures[t],
           D3DX_DEFAULT,D3DX_DEFAULT,D3DX_DEFAULT,0,
               D3DFMT_UNKNOWN,D3DPOOL_MANAGED,
       D3DX_FILTER_LINEAR ,D3DX_FILTER_LINEAR,0,NULL,NULL,
      &m_pDeviceObjects->m_pSparkleTextures[t]);
   }

   return TRUE;
}

ReleaseTextures

The ReleaseTextures method also uses the SAFE_RELEASE macro to release the textures on the device.

BOOL CSparklesScreensaver::ReleaseTextures( LPDIRECT3DDEVICE8 pd3dDevice )
{
   //textures
   for ( int i = 0;i <NumTextures; i++)
      SAFE_RELEASE( m_pDeviceObjects->m_pSparkleTextures[i] );

   return TRUE;
}

InitStates

The InitStates method takes care of setting render states on the device. One important part of that task involves determining whether or not pixel-shader support exists; if it does not, the fixed-function, multi-texture pipeline texture stage states need to be initialized.

BOOL CSparklesScreensaver::InitStates( LPDIRECT3DDEVICE8 pd3dDevice )
{
   //MT states if not using pixel shaders
   if ( !m_pDeviceObjects->m_bUsingPixelShaders )
   {
      //color = tex mod diffuse
      pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG1, 
D3DTA_TEXTURE );
      pd3dDevice->SetTextureStageState( 0, D3DTSS_COLOROP,   
D3DTOP_MODULATE );
      pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, 
D3DTA_DIFFUSE );
      //alpha = select texture alpha
      pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG1, 
D3DTA_TEXTURE );
      pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAOP,  
D3DTOP_SELECTARG1 );
      pd3dDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG2, 
D3DTA_DIFFUSE );

      //stage 1
      pd3dDevice->SetTextureStageState( 1, D3DTSS_COLOROP,   
D3DTOP_DISABLE );
      pd3dDevice->SetTextureStageState( 1, D3DTSS_ALPHAOP,  
 D3DTOP_DISABLE );
   }

   // Filter states
   pd3dDevice->SetTextureStageState(0,D3DTSS_MAGFILTER,D3DTEXF_LINEAR);
   pd3dDevice->SetTextureStageState(0,D3DTSS_MINFILTER,D3DTEXF_LINEAR);
   
   // cull,spec, dither states
   pd3dDevice->SetRenderState( D3DRS_CULLMODE,      D3DCULL_CCW);
   pd3dDevice->SetRenderState( D3DRS_SPECULARENABLE, FALSE );
   if( m_bDitherEnable)
      pd3dDevice->SetRenderState( D3DRS_DITHERENABLE, TRUE );
   else
      pd3dDevice->SetRenderState( D3DRS_DITHERENABLE, FALSE );

   // disable z, since not needed
   pd3dDevice->SetRenderState( D3DRS_ZWRITEENABLE,   FALSE );
   pd3dDevice->SetRenderState( D3DRS_ZENABLE,        FALSE );

      // Alpha blending states
   pd3dDevice->SetRenderState( D3DRS_ALPHATESTENABLE,   FALSE );
   pd3dDevice->SetRenderState( D3DRS_ALPHABLENDENABLE, TRUE ); 
   pd3dDevice->SetRenderState( D3DRS_SRCBLEND,       D3DBLEND_ONE );
   pd3dDevice->SetRenderState( D3DRS_DESTBLEND,      D3DBLEND_ONE );

   // Note: Setting D3DRENDERSTATE_LIGHTING to FALSE is needed to 
   // turn off vertex lighting (and use the color in the vertex instead.)
   pd3dDevice->SetRenderState( D3DRS_LIGHTING, FALSE );

   return TRUE;
}

InitStreams

The InitStreams method sets the geometry on the device, in this case a single vertex buffer and a single index buffer.

BOOL CSparklesScreensaver::InitStreams( LPDIRECT3DDEVICE8 pd3dDevice )
{
   //
   pd3dDevice->SetStreamSource( 0, m_pDeviceObjects->m_pvbSparkles, 
sizeof(LVertex) );
   pd3dDevice->SetIndices( m_pDeviceObjects->m_pibSparkles, 0L );

   return TRUE;
}

SetTransforms

The SetTransforms method calculates a view and a projection matrix, and sends the matrix down as a constant for the vertex shader. I reuse LoadMatrix4 from previous vertex shader articles.

BOOL CSparklesScreensaver::SetTransforms( LPDIRECT3DDEVICE8 pd3dDevice,
 D3DXVECTOR3& from, D3DXVECTOR3 at, D3DXVECTOR3 up   )
{
   static float      tic = -rnd() * 10000.0f;


   // tic to move view around
   tic += 0.005f;
   start_scale = 0.05f + ((float)sin(tic * 0.1) + 1.0f)*0.4f;
   world_size = 0.1f + (float)(sin(tic * 0.072) + 1.0)*10.0f;

   //view
   from = D3DXVECTOR3(orbit_size * (float)sin(tic*0.59), 
orbit_size*(float)sin(tic*0.72), 
orbit_size*(float)cos(tic*0.59));   
   cam.SetViewParams( from, at, up);
   view = cam.GetViewMatrix();

   //proj   
   BuildProjectionMatrix( 1.0f, 100.0f, &proj );

   //load for vertex shader
   LoadMatrix4( m_pd3dDevice , 4 , world * view * proj );

   return TRUE;
}

That leaves the shaders and the shader part of the mini-API.

MSDNSparkles and Shaders

The shader usage in the screensaver can be broken down into two parts: the shader mini-API methods, and the shader code itself.

The shader mini-API consists of a creation method, a release method, and a method to set the shaders on the device, as shown below.

//shader
BOOL    CreateShaders( LPDIRECT3DDEVICE8 pd3dDevice );
BOOL    ReleaseShaders( LPDIRECT3DDEVICE8 pd3dDevice ); 
BOOL    SetShaders( LPDIRECT3DDEVICE8 pd3dDevice );   

CreateShaders

The CrateShaders method creates and assembles vertex and pixel shaders. The vertex-shader section performing these operations uses D3DXAssembleShader and CreateVertexShader. The pixel-shader section uses D3DXAssembleShader and CreatePixelShader. Next month I will cover pixel shaders in more detail. For now suffice it to say that in their use we follow a parallel process, and the code involved for the shader itself is quite simple.

BOOL CSparklesScreensaver::CreateShaders(LPDIRECT3DDEVICE8 pd3dDevice )
{   
   HRESULT hr;
   ID3DXBuffer*   pshader0;
   ID3DXBuffer*   perrors;
   // Assemble the vertex shader   
   hr = D3DXAssembleShader( SingleTextureAndDiffuseVertexShader , 
sizeof(SingleTextureAndDiffuseVertexShader)-1 , 
                      0 , NULL , &pshader0 ,
                      &perrors );
   if ( FAILED(hr) )
   {
      OutputDebugString( "Failed to assemble shader, errors:\n" );
      OutputDebugString( (char*)perrors->GetBufferPointer() );
      OutputDebugString( "\n" );
      D3DXGetErrorStringA(hr,szBuffer,sizeof(szBuffer));
      OutputDebugString( szBuffer );
      OutputDebugString( "\n" );
   }
   // Create the vertex shader
   hr = m_pd3dDevice->CreateVertexShader( dwDecl, (
DWORD*)pshader0->GetBufferPointer(),
          &m_pDeviceObjects->m_hVertexShader, 0 );
   if ( FAILED(hr) )
   {
      OutputDebugString( "Failed to create shader, errors:\n" );
      D3DXGetErrorStringA(hr,szBuffer,sizeof(szBuffer));
      OutputDebugString( szBuffer );
      OutputDebugString( "\n" );
   }

      
   if ( m_pDeviceObjects->m_bUsingPixelShaders )
   {
      // Assemble the shader   
      hr = D3DXAssembleShader( TextureModDiffusePixelShader, 
sizeof(TextureModDiffusePixelShader)-1 , 
         0 , NULL , &pshader0 ,
         &perrors );
      if ( FAILED(hr) )
      {
         OutputDebugString( "Failed to assembleshader, errors:\n" );
         OutputDebugString( (char*)
                          perrors->GetBufferPointer() );
         OutputDebugString( "\n" );
         D3DXGetErrorStringA(hr,szBuffer,sizeof(szBuffer));
         OutputDebugString( szBuffer );
         OutputDebugString( "\n" );
      }
      // Create the pixel shader
      hr = m_pd3dDevice->CreatePixelShader( 
                  (DWORD*)pshader0->GetBufferPointer(),
                  &m_pDeviceObjects->m_hPixelShader );
      if ( FAILED(hr) )
      {
         OutputDebugString( "Failed to createshader, errors:\n" );
         D3DXGetErrorStringA(hr,szBuffer,sizeof(szBuffer));
         OutputDebugString( szBuffer );
         OutputDebugString( "\n" );
      }
   }

   //release buffer objs
   SAFE_RELEASE(pshader0);
   if ( FAILED(hr) )
      return hr;
   SAFE_RELEASE(perrors);
   if ( FAILED(hr) )
      return hr;

   return TRUE;
}

ReleaseShaders

The ReleaseShaders method calls DeleteVertexShader and DeletePixelShader on the shader handles; it then initializes the handles to the known uninitialized value.

BOOL CSparklesScreensaver::ReleaseShaders( LPDIRECT3DDEVICE8 pd3dDevice )
{
   //
   if ( m_pDeviceObjects->m_hVertexShader != 0xffffffff )
   {
      m_pd3dDevice->DeleteVertexShader( m_pDeviceObjects->m_hVertexShader );
      m_pDeviceObjects->m_hVertexShader = 0xffffffff;
   }
   if ( m_pDeviceObjects->m_hPixelShader != 0xffffffff )
   {
      m_pd3dDevice->DeletePixelShader( m_pDeviceObjects->m_hPixelShader );
      m_pDeviceObjects->m_hPixelShader = 0xffffffff;
   }

   return TRUE;
}

SetShaders

The SetShaders method first deals with the vertex pipeline and the vertex shader, and then deals with the pixel pipeline and the pixel shader. Vertex shaders are always used, so as long as we have a valid vertex-shader handle. SetShaders sets the vertex-shader handle on the device, using device method SetVertexShader to enable the vertex shader to control the operation of the vertex pipeline. If the device supports the use of pixel shaders, and we have a valid pixel-shader handle, then we perform a similar operation on the device, using device method SetPixelShader to set the pixel-shader handle on the device to enable that pixel shader to control the operation of the pixel pipe. If the device does not support pixel shaders, we fall back to the fixed-function multi-texture syntax, used in RestoreDevice, to set up the texture stages to control the pixel pipeline.

BOOL CSparklesScreensaver::SetShaders( LPDIRECT3DDEVICE8 pd3dDevice )
{
   HRESULT hr = S_OK;

   if ( m_pDeviceObjects->m_hVertexShader != 0xffffffff )
   {
      hr = m_pd3dDevice->SetVertexShader( m_pDeviceObjects->m_hVertexShader );   
   }
   if ( FAILED(hr) )
   {
      sprintf(szBuffer,"setting vs %d failed\n",
         m_pDeviceObjects->m_hVertexShader);
      OutputDebugString(szBuffer);
      D3DXGetErrorStringA(hr,szBuffer,sizeof(szBuffer));
      OutputDebugString( szBuffer );
      OutputDebugString( "\n" );
   }
   if ( m_pDeviceObjects->m_bUsingPixelShaders )
   {
      if ( m_pDeviceObjects->m_hPixelShader != 0xffffffff )
      {
         hr = m_pd3dDevice->SetPixelShader( m_pDeviceObjects->m_hPixelShader );   
      }
      if ( FAILED(hr) )
      {
         sprintf(szBuffer,"setting ps %d failed\n",
            m_pDeviceObjects->m_hPixelShader);
         OutputDebugString(szBuffer);
         D3DXGetErrorStringA(hr,szBuffer,sizeof(szBuffer));
         OutputDebugString( szBuffer );
         OutputDebugString( "\n" );
      }
      return hr;
   }

   return TRUE;
}

With that in hand, all that remains are the shader declarations themselves.

Shader Declarations and Functions

The vertex shader shown below declares position, color, and one set of texture coordinates. The shader transforms the position and copies the color and the texture coordinates. Very simple.

float   c[4] = {0.0f,0.5f,1.0f,2.0f};
DWORD dwDecl[] =
{
   D3DVSD_STREAM(0),
   D3DVSD_REG(D3DVSDE_POSITION, D3DVSDT_FLOAT3 ),         // POSITION,  0   
   D3DVSD_REG(D3DVSDE_DIFFUSE, D3DVSDT_D3DCOLOR ),       // DIFFUSE,   5 
   D3DVSD_REG(D3DVSDE_TEXCOORD0, D3DVSDT_FLOAT2 ),       // TEXCOORD0, 7   
   D3DVSD_CONST(0,1),*(DWORD*)&c[0],*(DWORD*)&c[1],
                     *(DWORD*)&c[2],*(DWORD*)&c[3],
   D3DVSD_END()
};

// single texture mod diffuse shader
// Constants
//      reg c0      = (0,0.5,1.0,2.0)
//      reg c4-7   = WorldViewProj matrix
//      reg c8      = constant color
// Stream 0
//      reg v0      = position ( 4x1 vector )
//      reg v5      = diffuse color
//      reg v7      = texcoords ( 2x1 vector )
const char SingleTextureAndDiffuseVertexShader[] =
"vs.1.1                          ; Shader version 1.1         \n"\
"m4x4    oPos   , v0   , c4     ; emit projected x position       \n"\
"mov     oD0    , v5            ; emit diffuse color = vert col \n"\
"mov     oT0.xy , v7            ; emit texcoord set 0          \n";

The pixel shader shown below loads a single texture using the tex t0 instruction, then modulates diffuse color in input register v0 with the texture t0, and stores the result in r0 using mul r0, v0, t0. Also very simple—and I will cover pixel shaders in more detail over the next several months.

// tex mod diffuse pixel shader3
const char TextureModDiffusePixelShader[] =
"ps.1.1                            ; Shader version 1.1      \n"\
"tex     t0                        ; declare texture 0      \n"\
"mul     r0  , v0, t0            ; diffuse * tex0         \n";

That finishes off the implementation of MSDNSparkles. When you build the project, you can test, configure, and install the resulting MSDNSparkles.scr by right-clicking. Figure 5 illustrates this. The result is basic but somewhat pleasing.

Figure 5. Right-click to test, configure, and install screensavers

This screensaver enables multi-monitor support using DirectX 8.0 graphics, for which there currently is no SDK sample. It also uses shaders in a scalable fashion. First, the screensaver uses vertex shaders on hardware if available, and drops back to software vertex shaders if no hardware is available. Next, the screensaver uses pixel shaders if pixel-shader hardware is available; if it is unavailable, the screensaver falls back to using fixed-function, multi-texturing operations. The screensaver can go further and attempt to use more sparkles if pixel-shader hardware is available. In other words, it can scale up as well as scale down, but that’s an added benefit, and one you can experiment with further.

Last Word

I'd like to acknowledge the help of Mike Anderson and Jason Sandlin (Microsoft) in producing this column.

Your feedback is welcome. Feel free to drop me a line at the address below with your comments, questions, topic ideas, or links to your own variations on topics the column covers. Please, though, don't expect an individual reply or send me support questions. Remember, Microsoft maintains active mailing lists as forums for like-minded developers to share information:

DirectXAV for audio and video issues at https://DISCUSS.MICROSOFT.COM/archives/DIRECTXAV.html

DirectXDev for graphics, networking, and input at https://DISCUSS.MICROSOFT.COM/archives/DIRECTXDEV.html

 

Driving DirectX

Philip Taylor is the DirectX SDK PM. He has been working with DirectX since the first public beta of DirectX 1, and, once upon a time, actually shipped DirectX 2 games. In his spare time, he can be found lurking on many 3-D graphics programming mailing lists and Usenet newsgroups. You can reach him at msdn@microsoft.com.