Cómo capturar la pantalla en DirectX 9 en un mapa de bits sin procesar en la memoria sin usar D3DXSaveSurfaceToFile

8

Sé que en OpenGL puedo hacer algo como esto

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

Y es bastante rápido, obtengo el mapa de bits sin procesar en _buffer. Cuando trato de hacer esto en DirectX. Suponiendo que tengo un objeto D3DDevice, puedo hacer algo como esto

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

Pero D3DXSaveSurfaceToFile es bastante lento, y de todos modos no necesito escribir la captura en el disco, así que me preguntaba si había una forma más rápida de hacerlo.

EDITAR: solo apunto a aplicaciones DirectX. En realidad aplicaciones DX9. Estoy haciendo esto a través de un gancho para presentar. (Estoy usando Detours si eso es relevante), y lo estoy haciendo para cada cuadro. No necesito (o quiero) un formato de mapa de bits particular como BMP PNG JPEG, etc., y un espacio de color de RGB24 está bien. Conseguirlo como YUV480p sería mejor, pero definitivamente no es necesario. Gracias a todos por las respuestas muy útiles.

Cloudraven
fuente
2
¿Qué quieres decir con "mapa de bits"? ¿Un archivo en un formato específico o una matriz de colores de píxeles? Si es un archivo, intente esto: msdn.microsoft.com/en-us/library/windows/desktop/… If array, me gustaría saber la respuesta también.
serpiente5
Sí, me refería a una matriz de bytes, 3 bytes por píxel de información RGB. Justo lo mismo que glReadPixels hace en openGL. Me refería a un mapa de bits RAW. Actualizaré la pregunta para que quede más clara
cloudraven,
En realidad, esa función que mencionas la guarda en la memoria, solo puedo leer el d3xbuffer que obtiene. Debería probarlo, si es lo suficientemente rápido, puedo aceptarlo como la solución. (Aún así, si alguien conoce una forma más rápida de capturar la pantalla, es muy bienvenido)
cloudraven

Respuestas:

6

La forma en que hago esto es la siguiente.

IDirect3DDevice9 :: GetBackBuffer : Obtenga acceso a IDirect3DSurface9 que representa el búfer posterior, igual que lo que tiene actualmente. ¡No olvide liberar esta superficie cuando termine ya que esta llamada incrementará el recuento de referencias!

IDirect3DSurface :: GetDesc : Obtenga la descripción de la superficie del búfer posterior, que le dará su ancho, alto y formato.

IDirect3DDevice9 :: CreateOffscreenPlainSurface : crea un nuevo objeto de superficie en D3DPOOL_SCRATCH; normalmente desea usar el mismo ancho, alto y formato (pero en realidad no tiene que hacerlo con este método). Nuevamente, suelte cuando haya terminado. Si está realizando esta operación en cada cuadro (en cuyo caso es mejor que busque alternativas como un enfoque basado en sombreador para lo que está tratando de hacer), podría crear la superficie plana fuera de la pantalla una vez al inicio y reutilizarla. en lugar de crearlo cada cuadro.

D3DXLoadSurfaceFromSurface : copie desde la superficie del búfer posterior a la superficie plana descentrada. Esto hará un cambio de tamaño y conversión de formato automáticamente para usted. Alternativamente, si no desea o necesita cambiar el tamaño o cambiar el formato, puede usar IDirect3DDevice9 :: GetRenderTargetData , pero si es así, cree la superficie plana fuera de la pantalla en D3DPOOL_SYSTEMMEM.

IDirect3DSurface9 :: LockRect : Obtenga acceso a los datos en la superficie plana fuera de la pantalla y tenga su propio mal camino; DesbloquearRect cuando haya terminado.

Esto parece mucho más código, pero descubrirá que es tan rápido como glReadPixels, e incluso puede ser más rápido si no necesita hacer una conversión de formato (que glReadPixels usando GL_RGB casi seguramente lo hace).

Editar para agregar: algunas funciones auxiliares (rought 'n' ready) que también tengo que pueden ser útiles para usar este método para capturas de pantalla:

// 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
fuente
Realmente lo estoy haciendo cada cuadro. Estoy enganchando la función Presente y luego obtengo el marco capturado dentro de mi gancho. ¿Podría explicarme cómo usar un sombreador para hacer esto? Eso puede ser lo que estoy buscando. Muchas gracias por su minuciosa respuesta.
cloudraven
1
Un enfoque basado en sombreador solo es apropiado si está realizando un procesamiento posterior con la imagen capturada. Según su descripción, parece que está haciendo algo como la captura de video, ¿sí? ¿Puedes confirmar esto?
Maximus Minimus
Adivinaste bien, pero no estoy haciendo exactamente captura de video, está muy cerca. No estoy codificando todos los cuadros (pero lo suficiente como para requerir un buen rendimiento), y estoy aplicando un par de filtros 2D a la imagen capturada antes de codificarlos.
cloudraven
Cambié mi código para usar BGRA. Se puso mejor. Gracias por la información
cloudraven
Por cierto, publiqué una pregunta de seguimiento sobre el enfoque del sombreador. Le agradecería que lo eche un vistazo. gamedev.stackexchange.com/questions/44587/…
cloudraven
1

Creo que necesita LockRectangle en el pBackBuffer y luego leer los píxeles de D3DLOCKED_RECT.pBits . Esto debería ser bastante rápido. Si necesita leer solo unos pocos píxeles, considere copiar todo el backbuffer a un buffer más pequeño a través de algo como DX11 CopySubresourceRegion (estoy seguro de que también hay una alternativa en DX9), que se realiza en GPU y luego transmita este pequeño buffer desde GPU a CPU a través de LockRectangle : guarda la transferencia del gran backbuffer en el bus.

GPUquant
fuente
1
A la respuesta le falta la parte más importante: la conversión. pBits apuntará a datos en el formato de superficie. Lo más probable es que no sea igual a una matriz de tripletes de bytes (RGB24).
serpiente5
1
Además de la conversión de formato, esto requiere CreateDevice con el indicador de backbuffer bloqueable (que la documentación advierte es un golpe de rendimiento) y el bloqueo del backbuffer siempre incurrirá en un bloqueo de canalización (que será un problema mucho mayor que la transferencia de datos).
Maximus Minimus el
1
Es por eso que aconsejé copiar una porción de backbuffer a un buffer diferente. Si puede leer datos sin procesar desde la superficie, la conversión al formato de "mapa de bits" depende de usted ...
GPUquant
De hecho, necesitaré convertirlo a Yuv420p en alguna parte durante el proceso. Entonces, ¿DirectX usa algún formato interno que no sea RGB?
cloudraven
2
No existe un formato interno RGB (los formatos internos son de 32 bpp), incluso en OpenGL GL_RGB solo significa que los 8 bits de repuesto no están en uso. Vea opengl.org/wiki/Common_Mistakes#Texture_upload_and_pixel_reads , por lo que tanto OpenGL como D3D en realidad serán BGRA internamente, por lo que usar GL_RGB es lento; necesita comprimir y mezclar los datos a RGB, que es un proceso de software lento.
Maximus Minimus
1

No puede capturar la pantalla con GetBackbuffer, esta función solo obtiene un puntero del búfer posterior, no los datos.

La forma correcta en que puedo pensar es la siguiente

  1. Cree una superficie fuera de la pantalla llamando a CreateOffscreenPlainSurface .
  2. Llame a GetFrontBufferData para obtener la imagen de la pantalla en la superficie fuera de la pantalla
  3. Llamar a LockRect de la superficie para recuperar los datos en la superficie, una superficie fuera de la pantalla siempre se podía bloquear independientemente de sus tipos de grupo.
zdd
fuente
1
IDirect3DDevice9 :: GetFrontBufferData - " Esta función es muy lenta, por diseño, y no debe usarse en ninguna ruta crítica de rendimiento " - msdn.microsoft.com/en-us/library/windows/desktop/…
Maximus Minimus
1
Sí, es lento, pero AFAIK, es la única forma de obtener la imagen de la pantalla mediante DirectX, o puede enganchar un poco antes de llamar a la función EndScene.
zdd
¡Gracias! En realidad lo estoy haciendo como un enlace, pero no para EndScene sino para Present, y estoy conectando específicamente aplicaciones directX. Dado eso, ¿hay algo más inteligente que pueda hacer al respecto?
cloudraven
@cloudraven, no estoy familiarizado con el enganche, pero puede consultar esta página como referencia. stackoverflow.com/questions/1994676/…
zdd
1

Un poco tarde. Pero espero que esto ayude a alguien.

creando

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

capturar

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

En buf tendrás datos de imagen en bruto. No te olvides de liberar todo.

vik_78
fuente