Display Different images per monitor directX 10
Asked Answered
W

1

6

I am fairly new to DirectX 10 programming, and I have been trying to do the following with my limited skills (though I have a strong background with OpenGL)

I am trying to display 2 different textured Quads, 1 per monitor. To do so, I understood that I need a single D3D10 Device, multiple (2) swap chains, and single VertexBuffer

While I think I'm able to create all of those, I'm still pretty unsure how to handle all of them. Do I need multiple ID3D10RenderTargetView(s) ? How and where should I Use OMSetRenderTargets(...) ?

Other than MSDN, documentation or explaination of those concepts are rather limited, so any help would be very welcome. Here is some code I have :

Here's the rendering code

for(int i = 0; i < screenNumber; i++){
    //clear scene
    pD3DDevice->ClearRenderTargetView( pRenderTargetView, D3DXCOLOR(0,1,0,0) );

    //fill vertex buffer with vertices
    UINT numVertices = 4;   
    vertex* v = NULL;   

    //lock vertex buffer for CPU use
    pVertexBuffer->Map(D3D10_MAP_WRITE_DISCARD, 0, (void**) &v );

    v[0] = vertex( D3DXVECTOR3(-1,-1,0), D3DXVECTOR4(1,0,0,1), D3DXVECTOR2(0.0f, 1.0f) );
    v[1] = vertex( D3DXVECTOR3(-1,1,0), D3DXVECTOR4(0,1,0,1), D3DXVECTOR2(0.0f, 0.0f) );
    v[2] = vertex( D3DXVECTOR3(1,-1,0), D3DXVECTOR4(0,0,1,1), D3DXVECTOR2(1.0f, 1.0f) );
    v[3] = vertex( D3DXVECTOR3(1,1,0), D3DXVECTOR4(1,1,0,1), D3DXVECTOR2(1.0f, 0.0f) ); 

    pVertexBuffer->Unmap();

    // Set primitive topology 
    pD3DDevice->IASetPrimitiveTopology( D3D10_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP );

    //set texture
    pTextureSR->SetResource( textureSRV[textureIndex] );

    //get technique desc
    D3D10_TECHNIQUE_DESC techDesc;
    pBasicTechnique->GetDesc( &techDesc );


    // This is where you actually use the shader code
    for( UINT p = 0; p < techDesc.Passes; ++p )
    {
        //apply technique
        pBasicTechnique->GetPassByIndex( p )->Apply( 0 );

        //draw
        pD3DDevice->Draw( numVertices, 0 );
    }

    //flip buffers
    pSwapChain[i]->Present(0,0);
}

And here's the code for creating rendering targets, which I am not sure is good

for(int i = 0; i < screenNumber; ++i){

    //try to get the back buffer
    ID3D10Texture2D* pBackBuffer;   
    if ( FAILED( pSwapChain[1]->GetBuffer(0, __uuidof(ID3D10Texture2D), (LPVOID*) &pBackBuffer) ) ) return fatalError("Could not get back buffer");

    //try to create render target view
    if ( FAILED( pD3DDevice->CreateRenderTargetView(pBackBuffer, NULL, &pRenderTargetView) ) ) return fatalError("Could not create render target view");

    pBackBuffer->Release();
    pD3DDevice->OMSetRenderTargets(1, &pRenderTargetView, NULL);
}

return true;

}

Whippet answered 24/5, 2012 at 20:26 Comment(0)
C
33

I hope I got the gist of what you wish to do - render different content on two different monitors while using a single graphics card (graphics adapter) which maps its output to those monitors. For that, you're going to need one device (for the single graphics card/adapter) and enumerate just how many outputs there are at the user's machine.

So, in total - that means one device, two outputs, two windows and therefore - two swap chains.

Here's a quick result of my little experiment:

The result

A little introduction

With DirectX 10+, this falls into the DXGI (DirectX Graphics Infrastructure) which manages the common low-level logistics involved with DirectX 10+ development which, as you probably know, dumped the old requirement of enumerating feature sets and the like - requiring every DX10+ capable card to share in on all of the features defined by the API. The only thing that varies is the extent and capability of the card (in other words, lousy performance is preferable to the app crashing and burning). This was all within DirectX 9 in the past, but people at Microsoft decided to pull it out and call it DXGI. Now, we can use DXGI functionality to set up our multi monitor environment.

Do I need multiple ID3D10RenderTargetView(s) ?

Yes, you do need multiple render target views, count depends (like the swap chains and windows) on the number of monitors you have. But, to save you from spewing words, let's write it out as simple as possible and additional information where it's needed:

  • Enumerate all adapters available on the system.
  • For each adapter, enumerate all outputs available (and active) and create a device to accompany it.
  • With the enumerated data stored in a suitable structure (think arrays which can quickly relinquish size information), use it to create n windows, swap chains, render target views, depth/stencil textures and their respective views where n is equal to the number of outputs.
  • With everything created, for each window you are rendering into, you can define special routines which will use the available geometry (and other) data to output your results - which resolves to what each monitor gets in fullscreen (don't forget to adjust the viewport for every window accordingly).
  • Present your data by iterating over every swap chain which is linked to its respective window and swap buffers with Present()

Now, while this is rich in word count, some code is worth a lot more. This is designed to give you a coarse idea of what goes into implementing a simple multimonitor application. So, assumptions are that there is only one adapter ( a rather bold statement nowadays ) and multiple outputs - and no failsafes. I'll leave the fun part to you. Answer to the second question is downstairs...

Do note there's no memory management involved. We assume everything magically gets cleaned up when it is not needed for illustration purposes. Be a good memory citizen.

Getting the adapter

IDXGIAdapter* adapter = NULL;
void GetAdapter() // applicable for multiple ones with little effort
{
    // remember, we assume there's only one adapter (example purposes)
    for( int i = 0; DXGI_ERROR_NOT_FOUND != factory->EnumAdapters( i, &adapter ); ++i )
    {

        // get the description of the adapter, assuming no failure
        DXGI_ADAPTER_DESC adapterDesc;
        HRESULT hr = adapter->GetDesc( &adapterDesc );

        // Getting the outputs active on our adapter
        EnumOutputsOnAdapter();

}

Acquiring the outputs on our adapter

std::vector<IDXGIOutput*> outputArray; // contains outputs per adapter
void EnumOutputsOnAdapter()
{
    IDXGIOutput* output = NULL;
    for(int i = 0; DXGI_ERROR_NOT_FOUND != adapter->EnumOutputs(i, &output); ++i)
    {

        // get the description
        DXGI_OUTPUT_DESC outputDesc;
        HRESULT hr = output->GetDesc( &outputDesc );

        outputArray.push_back( output );
    }

}

Now, I must assume that you're at least aware of the Win32 API considerations, creating window classes, registering with the system, creating windows, etc... Therefore, I will not qualify its creation, only elaborate how it pertains to multiple windows. Also, I will only consider the fullscreen case here, but creating it in windowed mode is more than possible and rather trivial.

Creating the actual windows for our outputs

Since we assume existence of just one adapter, we only consider the enumerated outputs linked to that particular adapter. It would be preferable to organize all window data in neat little structures, but for the purposes of this answer, we'll just shove them into a simple struct and then into yet another std::vector object, and by them I mean handles to respective windows (HWND) and their size (although for our case it's constant).

But still, we have to address the fact that we have one swap chain, one render target view, one depth/stencil view per window. So, why not feed all of that in that little struct which describes each of our windows? Makes sense, right?

struct WindowDataContainer
{
    //Direct3D 10 stuff per window data
    IDXGISwapChain* swapChain;
    ID3D10RenderTargetView* renderTargetView;
    ID3D10DepthStencilView* depthStencilView;

    // window goodies
    HWND hWnd;
    int width;
    int height;
}

Nice. Well, not really. But still... Moving on! Now to create the windows for outputs:

std::vector<WindowDataContainer*> windowsArray;
void CreateWindowsForOutputs()
{
    for( int i = 0; i < outputArray.size(); ++i )
    {

        IDXGIOutput* output = outputArray.at(i);
        DXGI_OUTPUT_DESC outputDesc;
        p_Output->GetDesc( &outputDesc );
        int x = outputDesc.DesktopCoordinates.left;
        int y = outputDesc.DesktopCoordinates.top;
        int width = outputDesc.DesktopCoordinates.right - x;
        int height = outputDesc.DesktopCoordinates.bottom - y;

        // Don't forget to clean this up. And all D3D COM objects.
        WindowDataContainer* window = new WindowDataContainer;

        window->hWnd = CreateWindow( windowClassName,
                                        windowName,
                                        WS_POPUP,
                                        x,
                                        y,
                                        width,
                                        height,
                                        NULL,
                                        0,
                                        instance,
                                        NULL );

        // show the window
        ShowWindow( window->hWnd, SW_SHOWDEFAULT );

        // set width and height
        window->width = width;
        window->height = height;

        // shove it in the std::vector
        windowsArray.push_back( window );

        //if first window, associate it with DXGI so it can jump in
        // when there is something of interest in the message queue
        // think fullscreen mode switches etc. MSDN for more info.
        if(i == 0)
            factory->MakeWindowAssociation( window->hWnd, 0 );

    }
}

Cute, now that's done. Since we only have one adapter and therefore only one device to accompany it, create it as usual. In my case, it's simply a global interface pointer which can be accessed all over the place. We are not going for code of the year here, so why the hell not, eh?

Creating the swap chains, views and the depth/stencil 2D texture

Now, our friendly swap chains... You might be used to actually creating them by invoking the "naked" function D3D10CreateDeviceAndSwapChain(...), but as you know, we've already made our device. We only want one. And multiple swap chains. Well, that's a pickle. Luckily, our DXGIFactory interface has swap chains on its production line which we can receive for free with complementary kegs of rum. Onto the swap chains then, create for every window one:

void CreateSwapChainsAndViews()
{
    for( int i = 0; i < windowsArray.size(); i++ )
    {
        WindowDataContainer* window = windowsArray.at(i);

        // get the dxgi device
        IDXGIDevice* DXGIDevice = NULL;
        device->QueryInterface( IID_IDXGIDevice, ( void** )&DXGIDevice ); // COM stuff, hopefully you are familiar

        // create a swap chain
        DXGI_SWAP_CHAIN_DESC swapChainDesc;
        
        // fill it in

        HRESULT hr = factory->CreateSwapChain( DXGIDevice, &swapChainDesc, &p_Window->swapChain );
        DXGIDevice->Release();
        DXGIDevice = NULL;

         // get the backbuffer
        ID3D10Texture2D* backBuffer = NULL;
        hr = window->swapChain->GetBuffer( 0, IID_ID3D10Texture2D, ( void** )&backBuffer );

        // get the backbuffer desc
        D3D10_TEXTURE2D_DESC backBufferDesc;
        backBuffer->GetDesc( &backBufferDesc );

        // create the render target view
        D3D10_RENDER_TARGET_VIEW_DESC RTVDesc;

        // fill it in

        device->CreateRenderTargetView( backBuffer, &RTVDesc, &window->renderTargetView );
        backBuffer->Release();
        backBuffer = NULL;

        // Create depth stencil texture
        ID3D10Texture2D* depthStencil = NULL;
        D3D10_TEXTURE2D_DESC descDepth;

        // fill it in


        device->CreateTexture2D( &descDepth, NULL, &depthStencil );

        // Create the depth stencil view
        D3D10_DEPTH_STENCIL_VIEW_DESC descDSV;

        // fill it in

        device->CreateDepthStencilView( depthStencil, &descDSV, &window->depthStencilView );

    }

}

We now have everything we need. All that you need to do is define a function which iterates over all windows and draws different stuff appropriately.

How and where should I Use OMSetRenderTargets(...) ?

In the just mentioned function which iterates over all windows and uses the appropriate render target (courtesy of our per-window data container):

void MultiRender( )
{
    // Clear them all
    for( int i = 0; i < windowsArray.size(); i++ )
    {
        WindowDataContainer* window = windowsArray.at(i);

        // There is the answer to your second question:
        device->OMSetRenderTargets( 1, &window->renderTargetView, window->depthStencilView );

        // Don't forget to adjust the viewport, in fullscreen it's not important...
        D3D10_VIEWPORT Viewport;
        Viewport.TopLeftX = 0;
        Viewport.TopLeftY = 0;
        Viewport.Width = window->width;
        Viewport.Height = window->height;
        Viewport.MinDepth = 0.0f;
        Viewport.MaxDepth = 1.0f;
        device->RSSetViewports( 1, &Viewport );

        // TO DO: AMAZING STUFF PER WINDOW
    }
}

Of course, don't forget to run through all the swap chains and swap buffers per window basis. The code here is just for the purposes of this answer, it requires a bit more work, error checking (failsafes) and contemplation to get it working just the way you like it - in other words - it should give you a simplified overview, not a production solution.

Good luck and happy coding! Sheesh, this is huge.

Choplogic answered 25/5, 2012 at 3:28 Comment(3)
Amazing and way over my expectations! Thanks a lot, I will try to integrate all of these delicious informations ASAPWhippet
That effort deserves a salute.. Thanks :)Synodic
@user1309389:I have tried to put the thongs together as per this article but i am still not able to display different images on different monitors connected to my pcSynodic

© 2022 - 2024 — McMap. All rights reserved.