Come acquisire lo schermo in DirectX 9 su una bitmap non elaborata in memoria senza utilizzare D3DXSaveSurfaceToFile


8

So che in OpenGL posso fare qualcosa del genere

glReadBuffer( GL_FRONT );
glReadPixels( 0, 0, _width, _height, GL_RGB, GL_UNSIGNED_BYTE, _buffer ); 

Ed è piuttosto veloce, ottengo la bitmap grezza in _buffer. Quando provo a farlo in DirectX. Supponendo che ho un oggetto D3DDevice, posso fare qualcosa del genere

if (SUCCEEDED(D3DDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &pBackbuffer))) {
   HResult hr = D3DXSaveSurfaceToFileA(filename, D3DXIFF_BMP, pBackbuffer, NULL, NULL); 

Ma D3DXSaveSurfaceToFile è piuttosto lento e non ho bisogno di scrivere la cattura su disco comunque, quindi mi chiedevo se ci fosse un modo più veloce per farlo.

EDIT: sto prendendo di mira solo le applicazioni DirectX. In realtà app DX9. Lo sto facendo con un gancio per presentare. (Sto usando Deviazioni se questo è rilevante), e lo sto facendo per ogni frame. Non ho bisogno (o non voglio) un particolare formato bitmap come BMP PNG JPEG ecc. E uno spazio colore di RGB24 è OK. Ottenere come YUV480p sarebbe meglio ma sicuramente non necessario. Grazie a tutti per le risposte molto utili


2
Cosa intendi con "bitmap"? Un file in un formato specifico o una matrice di colori di pixel? Se è un file, prova questo: msdn.microsoft.com/en-us/library/windows/desktop/… Se array, vorrei conoscere anche la risposta.
snake5,

Sì, intendevo una matrice di byte, 3 byte per pixel di informazioni RGB. Proprio la stessa cosa che glReadPixels fa in openGL. Intendevo una bitmap RAW. Aggiornerò la domanda per renderla più chiara
cloudraven

In realtà, quella funzione che menzioni lo salva in memoria, posso solo leggere il d3xbuffer che ottiene. Dovrei provarlo, se è abbastanza veloce potrei semplicemente accettarlo come soluzione. (Ancora se qualcuno conosce un modo più veloce per catturare lo schermo, è molto gradito)
cloudraven

Risposte:


6

Il modo in cui lo faccio è il seguente.

IDirect3DDevice9 :: GetBackBuffer : ottieni l'accesso a IDirect3DSurface9 che rappresenta il back buffer, lo stesso che hai attualmente. Non dimenticare di rilasciare questa superficie al termine poiché questa chiamata aumenterà il conteggio dei riferimenti!

IDirect3DSurface :: GetDesc : ottieni la descrizione della superficie del buffer posteriore, che ti darà larghezza, altezza e formato.

IDirect3DDevice9 :: CreateOffscreenPlainSurface : crea un nuovo oggetto superficie in D3DPOOL_SCRATCH; di solito vuoi usare la stessa larghezza, altezza e formato (ma in realtà non devi farlo con questo metodo). Ancora una volta, rilascia quando hai finito. Se stai eseguendo questa operazione ogni fotogramma (nel qual caso stai meglio guardando alternative come un approccio basato su shader a ciò che stai cercando di fare) potresti semplicemente creare la superficie normale fuori schermo una volta all'avvio e riutilizzarla invece di crearlo ogni frame.

D3DXLoadSurfaceFromSurface : copia dalla superficie del buffer posteriore alla superficie normale offsceen. Ciò comporterà automaticamente il ridimensionamento e la formattazione della conversione. In alternativa, se non si desidera o non è necessario ridimensionare o modificare il formato, è possibile utilizzare IDirect3DDevice9 :: GetRenderTargetData , ma in tal caso creare invece la superficie normale fuori schermo in D3DPOOL_SYSTEMMEM.

IDirect3DSurface9 :: LockRect : ottieni l'accesso ai dati nella superficie normale dello schermo esterno e segui la tua strada malvagia; UnlockRect al termine.

Sembra molto più codice ma scoprirai che è veloce quanto glReadPixels e può anche essere più veloce se non hai bisogno di fare una conversione del formato (cosa che glReadPixels usando GL_RGB quasi certamente fa).

Modifica per aggiungere: ho anche alcune funzioni di supporto (rought 'n' ready) che possono essere utili per l'utilizzo di questo metodo per gli screenshot:

// assumes pitch is measured in 32-bit texels, not bytes; use locked_rect.Pitch >> 2
void CollapseRowPitch (unsigned *data, int width, int height, int pitch)
{
    if (width != pitch)
    {
        unsigned *out = data;

        // as a minor optimization we can skip the first row
        // since out and data point to the same this is OK
        out += width;
        data += pitch;

        for (int h = 1; h < height; h++)
        {
            for (int w = 0; w < width; w++)
                out[w] = data[w];

            out += width;
            data += pitch;
        }
    }
}


void Compress32To24 (byte *data, int width, int height)
{
    byte *out = data;

    for (int h = 0; h < height; h++)
    {
        for (int w = 0; w < width; w++, data += 4, out += 3)
        {
            out[0] = data[0];
            out[1] = data[1];
            out[2] = data[2];
        }
    }
}

// bpp is bits, not bytes
void WriteDataToTGA (char *name, void *data, int width, int height, int bpp)
{
    if ((bpp == 24 || bpp == 8) && name && data && width > 0 && height > 0)
    {
        FILE *f = fopen (name, "wb");

        if (f)
        {
            byte header[18];

            memset (header, 0, 18);

            header[2] = 2;
            header[12] = width & 255;
            header[13] = width >> 8;
            header[14] = height & 255;
            header[15] = height >> 8;
            header[16] = bpp;
            header[17] = 0x20;

            fwrite (header, 18, 1, f);
            fwrite (data, (width * height * bpp) >> 3, 1, f);

            fclose (f);
        }
    }
}

In realtà lo sto facendo in ogni frame. Sto collegando la funzione Presente e quindi catturando la cornice all'interno del mio gancio. Potresti per favore approfondire l'uso di uno shader per farlo, potrebbe essere quello che sto cercando. Grazie mille per la tua risposta approfondita.
cloudraven

1
Un approccio basato su shader è appropriato solo se si esegue il rendering successivo con l'immagine acquisita. Dalla tua descrizione sembra più che tu stia facendo qualcosa di simile alla cattura video, sì? Puoi confermare questo?
Massimo

Hai indovinato bene, ma non sto facendo esattamente l'acquisizione di video, è molto vicino. Non sto codificando tutti i frame (ma abbastanza per richiedere una buona prestazione) e sto applicando un paio di filtri 2D all'immagine acquisita prima di codificarli.
cloudraven

Ho cambiato il mio codice per usare BGRA. È andata meglio. Grazie per l'intuizione
cloudraven

A proposito, ho pubblicato una domanda di follow-up sull'approccio dello shader. Sarei grato se lo guardassi. gamedev.stackexchange.com/questions/44587/…
cloudraven

1

Credo che sia necessario LockRectangle su pBackBuffer e quindi leggere i pixel da D3DLOCKED_RECT.pBits . Questo dovrebbe essere abbastanza veloce. Se hai bisogno di leggere solo pochi pixel, considera di copiare l'intero backbuffer in un buffer più piccolo tramite qualcosa come DX11 CopySubresourceRegion (sono sicuro che esiste anche un'alternativa in DX9), che viene eseguito su GPU e quindi trasmetti questo piccolo buffer da GPU a CPU tramite LockRectangle : risparmi il trasferimento di grandi backbuffer sul bus.


1
Alla risposta manca la parte più importante: la conversione. pBits punterà ai dati nel formato di superficie. Molto probabilmente non sarà uguale a un array di triplet di byte (RGB24).
snake5,

1
A parte la conversione del formato, questo richiede CreateDevice con il flag backbuffer bloccabile (che la documentazione avverte è un hit delle prestazioni) e il blocco del backbuffer comporta sempre uno stallo della pipeline (che sarà un problema molto più grande del trasferimento di dati).
Massimo

1
ecco perché ho consigliato di copiare una porzione di backbuffer in un buffer diverso. Se riesci a leggere i dati grezzi dalla superficie, la conversione in formato "bitmap" dipende da te ...
GPUquant

Dovrò effettivamente convertirlo in Yuv420p in qualche parte durante il processo. Quindi DirectX utilizza un formato interno che non è RGB?
cloudraven

2
Non esiste un formato interno RGB - i formati interni sono 32 bpp - anche in OpenGL GL_RGB significa solo che gli 8 bit di riserva non sono utilizzati. Vedi opengl.org/wiki/Common_Mistakes#Texture_upload_and_pixel_reads - quindi sia OpenGL che D3D diventeranno BGRA internamente, motivo per cui l'uso di GL_RGB è lento - deve comprimere e spostare i dati su RGB, che è un processo software lento.
Massimo

1

Non è possibile acquisire lo schermo con GetBackbuffer, questa funzione ottiene solo un puntatore del buffer posteriore, non dei dati.

Il modo corretto in cui posso pensare è il seguente

  1. Crea una superficie fuori schermo chiamando CreateOffscreenPlainSurface .
  2. Chiamare GetFrontBufferData per ottenere l'immagine dello schermo sulla superficie esterna dello schermo
  3. Chiama LockRect della superficie per recuperare i dati in superficie, una superficie fuori schermo era sempre bloccabile indipendentemente dal tipo di pool.

1
IDirect3DDevice9 :: GetFrontBufferData - " Questa funzione è molto lenta, in base alla progettazione, e non dovrebbe essere utilizzata in alcun percorso critico per le prestazioni " - msdn.microsoft.com/en-us/library/windows/desktop/…
Maximus Minimus

1
Sì, è lento, ma AFAIK, è l'unico modo per ottenere l'immagine dello schermo tramite DirectX, oppure puoi fare qualche aggancio prima di chiamare la funzione EndScene.
zdd

Grazie! In realtà lo sto facendo come un hook, ma non per EndScene ma per Present, e sto collegando specificamente le applicazioni directX. Detto questo, c'è qualcosa di più intelligente che posso fare al riguardo.
cloudraven

@cloudraven, non ho familiarità con l'aggancio, ma puoi dare un'occhiata a questa pagina come riferimento. stackoverflow.com/questions/1994676/...
zdd

1

Un po 'tardi. Ma spero che questo aiuti a qualcuno.

la creazione di

        if (SUCCEEDED(hr = _device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &backBuf)))
        {
            D3DSURFACE_DESC sd;
            backBuf->GetDesc(&sd);

            if (SUCCEEDED(hr = _device->CreateOffscreenPlainSurface(sd.Width, sd.Height, sd.Format, D3DPOOL_SYSTEMMEM, &_surface, NULL)))

cattura

    if (SUCCEEDED(hr = _device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &backBuf)))
    {
        if (SUCCEEDED(hr = _device->GetRenderTargetData(backBuf, _surface)))
        {
            if (SUCCEEDED(hr = D3DXSaveSurfaceToFileInMemory(&buf, D3DXIFF_DIB, _surface, NULL, NULL)))
            {
                LPVOID imgBuf = buf->GetBufferPointer();
                DWORD length = buf->GetBufferSize();

In buf avrai dati di immagine grezzi. Non dimenticare di rilasciare tutto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.