Comment capturer l'écran dans DirectX 9 sur une image bitmap brute en mémoire sans utiliser D3DXSaveSurfaceToFile

8

Je sais que dans OpenGL je peux faire quelque chose comme ça

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

Et c'est assez rapide, j'obtiens le bitmap brut dans _buffer. Lorsque j'essaie de le faire dans DirectX. En supposant que j'ai un objet D3DDevice, je peux faire quelque chose comme ça

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

Mais D3DXSaveSurfaceToFile est assez lent, et je n'ai pas besoin d'écrire la capture sur le disque de toute façon, donc je me demandais s'il y avait un moyen plus rapide de le faire.

EDIT: Je cible uniquement les applications DirectX. En fait, les applications DX9. Je fais cela à travers un crochet pour présenter. (J'utilise Detours si cela est pertinent), et je le fais pour chaque image. Je n'ai pas besoin (ou ne veux pas) d'un format bitmap particulier comme BMP PNG JPEG, etc., et un espace colorimétrique de RGB24 est OK. L'obtenir en tant que YUV480p serait mieux mais certainement pas nécessaire. Merci à tous pour les réponses très utiles

cloudraven
la source
2
Qu'entendez-vous par "bitmap"? Un fichier dans un format spécifique ou un tableau de couleurs de pixels? S'il s'agit d'un fichier, essayez ceci: msdn.microsoft.com/en-us/library/windows/desktop/… Si tableau, j'aimerais également connaître la réponse.
snake5
oui, je voulais dire un tableau d'octets, 3 octets par pixel d'informations RVB. Exactement la même chose que glReadPixels fait dans openGL. Je voulais dire un bitmap RAW. Je
mettrai à
En fait, cette fonction que vous mentionnez l'enregistre en mémoire, je peux simplement lire le d3xbuffer qu'il obtient. Je devrais l'essayer, s'il est assez rapide, je peux simplement l'accepter comme solution. (Pourtant, si quelqu'un connaît un moyen plus rapide de capturer l'écran, il est le bienvenu)
cloudraven

Réponses:

6

La façon dont je fais cela est la suivante.

IDirect3DDevice9 :: GetBackBuffer : Accédez à l'IDirect3DSurface9 représentant le tampon arrière, comme vous l'avez actuellement. N'oubliez pas de libérer cette surface une fois terminé car cet appel augmentera le nombre de références!

IDirect3DSurface :: GetDesc : Obtenez la description de la surface du tampon arrière, qui vous donnera sa largeur, sa hauteur et son format.

IDirect3DDevice9 :: CreateOffscreenPlainSurface : Créez un nouvel objet surface dans D3DPOOL_SCRATCH; vous voulez généralement utiliser la même largeur, la même hauteur et le même format (mais vous n'avez pas vraiment besoin de cette méthode). Encore une fois, relâchez lorsque vous avez terminé. Si vous effectuez cette opération à chaque image (dans ce cas, vous feriez mieux de chercher des alternatives telles qu'une approche basée sur des shaders pour ce que vous essayez de faire), vous pouvez simplement créer la surface lisse hors écran une fois au démarrage et réutiliser au lieu de le créer à chaque image.

D3DXLoadSurfaceFromSurface : Copie de la surface tampon arrière vers la surface lisse de l'écran. Cela fera automatiquement une conversion de redimensionnement et de formatage pour vous. Alternativement, si vous ne voulez pas ou ne devez pas redimensionner ou changer le format, vous pouvez utiliser IDirect3DDevice9 :: GetRenderTargetData , mais si c'est le cas, créez plutôt la surface unie hors écran dans D3DPOOL_SYSTEMMEM.

IDirect3DSurface9 :: LockRect : Accédez aux données dans la surface plate hors écran et ayez votre propre chemin mal avec elle; UnlockRect une fois terminé.

Cela ressemble à beaucoup plus de code, mais vous constaterez qu'il est tout aussi rapide que glReadPixels, et peut même être plus rapide si vous n'avez pas besoin de faire une conversion de format (ce que glReadPixels utilisant GL_RGB fait presque certainement).

Modifier pour ajouter: certaines fonctions d'assistance (rought 'n' ready) que j'ai également qui peuvent être utiles pour utiliser cette méthode pour les captures d'écran:

// 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);
        }
    }
}
Maximus Minimus
la source
En fait, je le fais à chaque image. J'accroche la fonction Présent puis j'obtiens l'image capturée dans mon crochet. Pourriez-vous s'il vous plaît élaborer sur l'utilisation d'un shader pour ce faire, c'est peut-être ce que je recherche. Merci beaucoup pour votre réponse complète.
cloudraven
1
Une approche basée sur un shader n'est appropriée que si vous effectuez un rendu ultérieur avec l'image capturée. D'après votre description, il semble que vous fassiez quelque chose comme la capture vidéo, non? Pouvez-vous le confirmer?
Maximus Minimus
Vous l'avez deviné, mais je ne fais pas exactement de capture vidéo, c'est très proche. Je n'encode pas toutes les images (mais suffisamment pour exiger de bonnes performances), et j'applique quelques filtres 2D à l'image capturée avant de les encoder.
cloudraven
J'ai changé mon code pour utiliser BGRA. Ça s'est amélioré. Merci pour la perspicacité
cloudraven
Au fait, j'ai posté une question de suivi sur l'approche du shader. Je vous serais reconnaissant de bien vouloir y jeter un œil. gamedev.stackexchange.com/questions/44587/…
cloudraven
1

Je crois que vous devez LockRectangle sur le pBackBuffer puis lire les pixels de D3DLOCKED_RECT.pBits . Cela devrait être assez rapide. Si vous avez besoin de lire seulement quelques pixels, envisagez de copier tout le backbuffer dans un tampon plus petit via quelque chose comme DX11 CopySubresourceRegion (je suis sûr qu'il existe également une alternative dans DX9), ce qui se fait sur le GPU, puis diffusez ce petit tampon du GPU vers CPU via LockRectangle - vous enregistrez le transfert de gros backbuffer sur le bus.

GPUquant
la source
1
La réponse manque la partie la plus importante: la conversion. pBits pointera vers des données au format de surface. Il ne sera probablement pas égal à un tableau de triplets d'octets (RGB24).
snake5
1
Mis à part la conversion de format, cela nécessite CreateDevice avec l'indicateur de backbuffer verrouillable (que la documentation avertit est un impact sur les performances) et le verrouillage du backbuffer entraînera toujours un blocage du pipeline (ce qui va être un problème beaucoup plus important que le transfert de données).
Maximus Minimus
1
c'est pourquoi j'ai conseillé de copier une partie du backbuffer dans un tampon différent. Si vous pouvez lire des données brutes depuis la surface, la conversion au format "bitmap" dépend de vous ...
GPUquant
J'aurai en fait besoin de le convertir en Yuv420p à un moment donné au cours du processus. DirectX utilise-t-il donc un format interne qui n'est pas RVB?
cloudraven
2
Un format interne RVB n'existe pas - les formats internes sont 32bpp - même dans OpenGL GL_RGB signifie simplement que les 8 bits de rechange ne sont pas utilisés. Voir opengl.org/wiki/Common_Mistakes#Texture_upload_and_pixel_reads - donc OpenGL et D3D vont en fait être BGRA en interne, c'est pourquoi l'utilisation de GL_RGB est lente - il faut à la fois compresser et transférer les données vers RVB, qui est un processus logiciel lent.
Maximus Minimus
1

Vous ne pouvez pas capturer d'écran avec GetBackbuffer, cette fonction obtient uniquement un pointeur du tampon arrière, pas les données.

La bonne façon de penser est comme ci-dessous

  1. Créez une surface hors écran en appelant CreateOffscreenPlainSurface .
  2. Appelez GetFrontBufferData pour obtenir l'image d'écran sur la surface hors écran
  3. Appelez LockRect de la surface pour récupérer les données dans la surface, une surface hors écran était toujours verrouillable quels que soient leurs types de pool.
zdd
la source
1
IDirect3DDevice9 :: GetFrontBufferData - " Cette fonction est très lente, de par sa conception, et ne doit être utilisée dans aucun chemin critique pour les performances " - msdn.microsoft.com/en-us/library/windows/desktop/…
Maximus Minimus
1
Oui, c'est lent, mais AFAIK, c'est le seul moyen d'obtenir l'image de l'écran par DirectX, ou vous pouvez faire quelques accrochages avant d'appeler la fonction EndScene.
zdd
Merci! Je le fais en fait comme un hook, mais pas vers EndScene mais vers Present, et je connecte spécifiquement des applications DirectX. Compte tenu de cela, y a-t-il quelque chose de plus intelligent que je puisse faire à ce sujet.
cloudraven
@cloudraven, je ne suis pas familier avec le crochet, mais vous pouvez jeter un oeil à cette page pour référence. stackoverflow.com/questions/1994676/…
zdd
1

Un peu tard. Mais j'espère que cela aide quelqu'un.

création

        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)))

capturer

    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();

Dans buf, vous aurez des données d'image brutes. N'oubliez pas de tout libérer.

vik_78
la source