Depuis la version 3.5 SP1 du Framework .Net, il est possible en WPF de consommer une surface rendue à l’aide de DirectX 9 (grâce à XNA, Managed DirectX ou en C++ / CLI). Malheureusement, de base, WPF ne supporte pas les textures produites par DirectX 10. D’autres solutions existent, comme par exemple l’utilisation de SlimDX, mais jusqu’à maintenant, elles passaient toutes par de l’interopérabilité Win32, et ne permettaient donc pas d’intégrer le contenu DX10 au moteur d’animations / transformations de WPF.
Le but de cet article est de montrer une technique permettant sous Windows VISTA, 7 et à priori supérieur d’injecter une surface rendue en DirectX 10 dans le moteur de composition de WPF.
Un nain rendu en DirectX10 / HLSL4 et mélangé à du contenu WPF
Le principe
Le principe est d’exploiter les fonctionnalités apportées par Windows VISTA et son système WDDM (Windows Display Driver Model) à DirectX 9 et plus particulièrement le système de partage de ressources, qui permet de partager des objets en mémoire sur le GPU entre plusieurs devices DirectX. On va donc créer à partir d’un Device DirectX 9 Ex (DirectX 9 Ex est la version de DirectX9 adaptée au WDDM de VISTA / 7) une texture que l’on va partager avec un Device DirectX10, effectuer un rendu dedans à l’aide de DirectX 10, et la présenter à WPF grâce à DirectX 9. Les performances sont assurées grâce au fait que le partage de ressources tel qu’implémenté dans Windows VISTA / 7 n’implique pas de recopie en mémoire système : la texture ne quitte jamais la mémoire de la carte graphique, de son rendu en DirectX10 jusqu’à son exploitation par le moteur de composition de WPF (basé lui-même sur DirectX 9 Ex).
Les obstacles
L’obstacle le plus gênant a été de trouver de la documentation sur le Resource Sharing entre DirectX 9 et DirectX 10. Il y’a effectivement pas mal de contraintes, et les formats de textures supportés à la fois par DirectX9 et DirectX 10 ne sont pas légions… Et aucun d’entre eux n’est compatible avec WPF !
Il a donc fallu passer par une technique de conversion bien connue des développeurs Direct3D (et dont les performances sont impressionnantes par rapport à ce qu’on obtiendrait en faisant ca sur le CPU) baptisée empiriquement la technique des 2 gros triangles. Il s’agit simplement de créer une texture DirectX 9 compatible WPF, et d’effectuer dedans le rendu de 2 polygones recouvrant l’intégralité du ViewPort, auxquels on applique la texture non supportée (pour des performances optimales, on désactive le plus de traitements possibles, notamment la rastérisation, l’éclairage, le Z-Buffer…).
La réalisation
La réalisation ensuite est relativement simple (pour peu que l’on connaisse un minimum WPF, C++ / CLI et les API DirectX).
On se retrouve effectivement avec une classe C++ Native dont le but est de créer les Devices DirectX 9 EX et DirectX 10, leurs ressources associées et d’effectuer le rendu, une classe C++ / CLI servant de proxy à la classe de rendu, et un contrôle WPF récupérant le handle de surface DirectX 9 Ex et l’affichant grâce au composant D3DImage (faites une recherche sur votre moteur de recherche préféré sur D3DImage, y’a pas mal de documentation sur le sujet). Dans l’exemple illustrant cet article, le composant WPF envoie les paramètres de rendu à la classe proxy qui appelle la classe de rendu qui charge alors les fichiers de modèles, les shaders et quelques paramètres associés.
Voici quelques morceaux de choix du code de l’exemple :
Création des textures DirectX 9 (NativeRenderer.cpp) :
// Creating DX9 textures
HRESULT hr = m_deviceEx->CreateTexture(width,
height,
1,
D3DUSAGE_RENDERTARGET,
D3DFMT_A16B16G16R16F, // Required for DirectX10 Interop
// but does not work with WPF D3DImage. So we need to copy into an other
// D3D9 texture with correct WPF format. For this we simply render a rectangle
D3DPOOL_DEFAULT,
&m_renderTexture,
&shareHandle);
assert(SUCCEEDED(hr));
// This texture will be passed to WPF D3DImage
hr = m_deviceEx->CreateTexture(width,
height,
1,
D3DUSAGE_RENDERTARGET,
D3DFMT_A8R8G8B8,
D3DPOOL_DEFAULT,
&m_renderTextureCopy,
NULL);
assert(SUCCEEDED(hr));
Ouverture de la texture partagée par le device DirectX 10
// Now we open the shared texture from the DX10 Device
ID3D10Resource * pTempResource;
hr = m_device10->OpenSharedResource(shareHandle, __uuidof(ID3D10Resource), (void**)&pTempResource);
assert(SUCCEEDED(hr));
hr = pTempResource->QueryInterface(__uuidof(ID3D10Texture2D), (void**)&m_renderTexture10);
assert(SUCCEEDED(hr));
SAFE_RELEASE(pTempResource);
// And we create a Render Target View from it
hr = m_device10->CreateRenderTargetView(m_renderTexture10, NULL, &m_rtv);
assert(SUCCEEDED(hr));
Configuration du pipeline DX9 pour la conversion de texture à l’aide des 2 gros triangles
// The texture in which DX10 content is drawn is set as an input Texture
// in the DX9 device
hr = m_deviceEx->SetTexture(0, m_renderTexture);
assert(SUCCEEDED(hr));
// And the output is set to the texture which has a WPF compatible format
hr = m_renderTextureCopy->GetSurfaceLevel(0, &m_renderTarget);
assert(SUCCEEDED(hr));
hr = m_deviceEx->SetRenderTarget(0, m_renderTarget);
assert(SUCCEEDED(hr));
// This is the rectangle that will be used for the texture conversion
// rendering pass
m_quad[0].x = 0;
m_quad[0].y = 0;
m_quad[0].z = 0.5;
m_quad[0].rhw = 1.0;
m_quad[0].u = 0;
m_quad[0].v = 0;
m_quad[1].x = (float)width;
m_quad[1].y = 0;
m_quad[1].z = 0.5;
m_quad[1].rhw = 1.0;
m_quad[1].u = 1;
m_quad[1].v = 0;
m_quad[2].x = 0;
m_quad[2].y = (float)height;
m_quad[2].z = 0.5;
m_quad[2].rhw = 1.0;
m_quad[2].u = 0;
m_quad[2].v = 1;
m_quad[3].x = (float)width;
m_quad[3].y = (float)height;
m_quad[3].z = 0.5;
m_quad[3].rhw = 1.0;
m_quad[3].u = 1;
m_quad[3].v = 1;
IDirect3DVertexBuffer9* vertBuffer;
hr = m_deviceEx->CreateVertexBuffer(4*sizeof(QuadVertex),
D3DUSAGE_WRITEONLY,
QUAD_FORMAT,
D3DPOOL_DEFAULT, &vertBuffer,NULL);
assert(SUCCEEDED(hr));
void* bufferData = NULL;
hr = vertBuffer->Lock(0,0, &bufferData, 0);
memcpy(bufferData, (void*)m_quad, 4*sizeof(QuadVertex));
vertBuffer->Unlock();
hr = m_deviceEx->SetStreamSource(0, vertBuffer, 0, sizeof(QuadVertex));
assert(SUCCEEDED(hr));
SAFE_RELEASE(vertBuffer);
Rendu (effectué sur l’évènement Rendering du CompositionTarget) :
HRESULT hr;
if(m_rtv == NULL)
return;
// Send the transformation matrices to the GPU
ApplyMatrices();
// Update the BumpForce shader variable
m_fBumpForceVariable->SetFloat(m_bumpForce);
// Update the fTime shader variable
float time = (float)m_stopWatch.GetElapsedSeconds();
m_fTimeVariable->SetFloat(time);
// Clear the Depth Buffer
m_device10->ClearDepthStencilView(m_dsv, D3D10_CLEAR_DEPTH, 1.0,0);
// Clear the render target
float clearColor[] = {0.0,0.0,0.0,.0};
m_device10->ClearRenderTargetView(m_rtv, clearColor);
// Render the mesh
if(m_meshManager != NULL)
{
ID3DX10Mesh *mesh = m_meshManager->GetMesh();
ID3D10InputLayout * inputLayout = m_meshManager->GetInputLayout();
m_device10->IASetInputLayout(inputLayout);
DWORD partCount = m_meshManager->GetMaterialCount();
D3D10_TECHNIQUE_DESC td;
for(DWORD i = 0;i<partCount;i++)
{
const MATERIAL10* mat = m_meshManager->GetMaterial(i);
m_txDiffuseVariable->SetResource(mat->diffuseMap);
m_txBumpVariable->SetResource(mat->bumpMap);
m_renderTechnique->GetDesc(&td);
m_hasEffect9Variable->SetBool((BOOL)mat->hasEffect9 && m_activateSpecularLighting);
for (DWORD p = 0; p < td.Passes;p++)
{
ID3D10EffectPass *pass = m_renderTechnique->GetPassByIndex(p);
pass->Apply(0);
HRESULT hr;
hr = mesh->DrawSubset(i);
assert(SUCCEEDED(hr));
}
}
}
// Flush the generated content
m_device10->Flush();
// Begin the DX9 pass
m_deviceEx->BeginScene();
// Clear the WPF Render Target
hr = m_deviceEx->Clear(0,NULL,D3DCLEAR_TARGET, D3DCOLOR_ARGB(0,0,0,0), 1.0,0);
// Draw the 2 big triangles
hr = m_deviceEx->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2);
// End the DX9 pass
m_deviceEx->EndScene();
hr = m_deviceEx->Present(NULL,NULL,NULL,NULL);
Si ce code ne vous fait pas peur, à priori je n’ai pas besoin de vous mettre en évidence le code de la classe proxy ni celle du contrôle WPF. Vous pouvez retrouver l’intégralité du code (et des assets graphiques tirés des samples du SDK DirectX) ici.
Les pré-requis sont :
- VS 2008 SP1 (avec support de C# et C++)
- Windows Vista ou 7
- Une carte 3D DirectX 10
- Le SDK DirectX de mars 2009
- Les C++ Directories bien configurés dans Visual Studio pour pouvoir accéder aux includes / libs du sdk DirectX
Conclusion
Cette technique ouvre la porte à de nouveaux scénarios d’intéraction avec du contenu DirectX. On peut imaginer des outils de production 3D, des gestionnaires d’assets pour des éditeurs de jeu vidéo, des environements de développement de hlsl, des interfaces de simulation en 3D… Le tout en obtenant à la fois les avantages de la productivité apportée par WPF, et les performances et les possibilités en terme de rendu 3D de DirectX 10 (et apriori, la même chose sera possible avec DirectX 11 !)