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.