Como capturar a tela no DirectX 9 para um bitmap bruto na memória sem usar D3DXSaveSurfaceToFile

8

Eu sei que no OpenGL eu posso fazer algo assim

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

E é bem rápido, eu recebo o bitmap bruto no _buffer. Quando tento fazer isso no DirectX. Supondo que eu tenha um objeto D3DDevice, posso fazer algo assim

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

Mas o D3DXSaveSurfaceToFile é bem lento, e eu não preciso gravar a captura no disco, então fiquei pensando se havia uma maneira mais rápida de fazer isso.

EDIT: Estou direcionando apenas aplicativos DirectX. Na verdade, aplicativos DX9. Estou fazendo isso através de um gancho para apresentar. (Estou usando Detours, se isso for relevante), e estou fazendo isso para todos os quadros. Não preciso (ou quero) de um formato de bitmap específico, como BMP PNG JPEG etc., e um espaço de cores de RGB24 está OK. Obtê-lo como YUV480p seria melhor, mas definitivamente não é necessário. Obrigado a todos pelas respostas muito úteis

cloudraven
fonte
2
O que você quer dizer com "bitmap"? Um arquivo em um formato específico ou uma matriz de cores de pixels? Se for um arquivo, tente o seguinte: msdn.microsoft.com/en-us/library/windows/desktop/… Se array, gostaria de saber a resposta também.
precisa saber é o seguinte
Sim, eu quis dizer uma matriz de bytes, 3 bytes por pixel de informações RGB. Exatamente a mesma coisa que o glReadPixels faz no openGL. Eu quis dizer um bitmap RAW. Atualizarei a pergunta para torná-la mais clara
#
Na verdade, essa função que você menciona salva na memória, eu posso apenas ler o d3xbuffer que ele obtém. Eu deveria tentar, se for rápido o suficiente, posso apenas aceitá-lo como a solução. (Ainda assim, se alguém sabe uma maneira mais rápida para capturar a tela, é muito bem-vindos)
cloudraven

Respostas:

6

A maneira como faço isso é a seguinte.

IDirect3DDevice9 :: GetBackBuffer : obtenha acesso ao IDirect3DSurface9 que representa o buffer traseiro, o mesmo que você possui atualmente. Não se esqueça de Liberar esta superfície quando terminar, pois esta chamada aumentará a contagem de referência!

IDirect3DSurface :: GetDesc : obtenha a descrição da superfície do buffer traseiro, que fornecerá largura, altura e formato.

IDirect3DDevice9 :: CreateOffscreenPlainSurface : Crie um novo objeto de superfície em D3DPOOL_SCRATCH; geralmente você deseja usar a mesma largura, altura e formato (mas na verdade não precisa desse método). Novamente, solte quando terminar. Se você estiver executando esta operação em todos os quadros (nesse caso, é melhor procurar alternativas como uma abordagem baseada em shader para o que você está tentando fazer), basta criar a superfície plana fora da tela uma vez na inicialização e reutilizar em vez de criar todos os quadros.

D3DXLoadSurfaceFromSurface : copie da superfície do buffer traseiro para a superfície plana do offsceen. Isso fará um redimensionamento e conversão de formato automaticamente para você. Como alternativa, se você não quiser ou precisar redimensionar ou alterar o formato, poderá usar IDirect3DDevice9 :: GetRenderTargetData , mas, se for o caso, crie a superfície plana fora da tela em D3DPOOL_SYSTEMMEM.

IDirect3DSurface9 :: LockRect : Tenha acesso aos dados na superfície plana fora da tela e faça o que quiser ; UnlockRect quando terminar.

Isso parece muito mais código, mas você descobrirá que é tão rápido quanto o glReadPixels e pode ser ainda mais rápido se você não precisar fazer uma conversão de formato (o que o glReadPixels usando GL_RGB quase certamente faz).

Edite para adicionar: algumas funções auxiliares (prontas para uso) que também tenho, que podem ser úteis para usar este método para capturas de tela:

// 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
fonte
Na verdade, estou fazendo isso em todos os quadros. Estou conectando a função Presente e, em seguida, obtendo o quadro capturado dentro do meu gancho. Você poderia, por favor, usar um sombreador para fazer isso, pode ser o que estou procurando. Muito obrigado pela sua resposta completa.
cloudraven
11
Uma abordagem baseada em sombreador é apropriada apenas se você estiver fazendo a renderização subsequente com a imagem capturada. Pela sua descrição, parece mais que você está fazendo algo como captura de vídeo, sim? Você pode confirmar isso?
Maximus Minimus
Você adivinhou certo, mas eu não estou exatamente capturando vídeo, está muito perto. Não estou codificando todos os quadros (mas o suficiente para exigir um bom desempenho) e estou aplicando alguns filtros 2D à imagem capturada antes de codificá-los.
cloudraven
Mudei meu código para usar o BGRA. Melhorou. Obrigado pela visão
cloudraven
A propósito, postei uma pergunta de acompanhamento sobre a abordagem de sombreador. Ficaria agradecido se você desse uma olhada. gamedev.stackexchange.com/questions/44587/...
cloudraven
1

Eu acredito que você precisa LockRectangle no pBackBuffer e, em seguida, leia os pixels de D3DLOCKED_RECT.pBits . Isso deve ser bem rápido. Se você precisar ler apenas alguns pixels, considere copiar o backbuffer inteiro para um buffer menor através de algo como DX11 CopySubresourceRegion (tenho certeza de que também há alternativa no DX9), o que é feito na GPU e depois transmite esse pequeno buffer da GPU para CPU via LockRectangle - você salva a transferência do grande backbuffer no barramento.

GPUquant
fonte
11
A resposta está faltando a parte mais importante: conversão. O pBits apontará para os dados no formato da superfície. Provavelmente não será igual a uma matriz de trigêmeos de bytes (RGB24).
precisa saber é o seguinte
11
Além da conversão de formato, isso requer o CreateDevice com o sinalizador bloqueador de backbuffer (que a documentação avisa ser um impacto no desempenho) e o bloqueio do backbuffer sempre ocorrerá uma paralisação de pipeline (o que será um problema muito maior do que a transferência de dados).
Maximus Minimus
11
é por isso que aconselhei copiar uma parte do backbuffer para um buffer diferente. Se você pode ler dados brutos de superfície, a conversão para o formato "bitmap" é com você ...
GPUquant
Na verdade, precisarei convertê-lo para Yuv420p em alguma parte durante o processo. Então, o DirectX está usando algum formato interno que não é RGB?
Cloud12 #
2
Não existe um formato interno RGB - os formatos internos são 32bpp - mesmo no OpenGL GL_RGB significa apenas que os 8 bits restantes não são utilizados. Veja opengl.org/wiki/Common_Mistakes#Texture_upload_and_pixel_reads - para que o OpenGL e o D3D sejam realmente BGRA internamente e é por isso que o GL_RGB é lento - ele precisa compactar e misturar os dados em RGB, o que é um processo lento de software.
Maximus Minimus
1

Você não pode capturar a tela com GetBackbuffer, esta função obtém apenas um ponteiro do buffer de fundo, não os dados.

A maneira correta de pensar é como abaixo

  1. Crie uma superfície fora da tela chamando CreateOffscreenPlainSurface .
  2. Ligar GetFrontBufferData para obter a imagem da tela na superfície fora da tela
  3. Chamada LockRect da superfície para recuperar os dados na superfície, uma superfície fora da tela sempre era bloqueada, independentemente do tipo de pool.
zdd
fonte
11
IDirect3DDevice9 :: GetFrontBufferData - " Esta função é muito lenta por design e não deve ser usada em nenhum caminho crítico de desempenho " - msdn.microsoft.com/en-us/library/windows/desktop/…
Maximus Minimus
11
Sim, é lento, mas AFAIK, é a única maneira de obter a imagem da tela pelo DirectX, ou você pode fazer algumas ligações antes de chamar a função EndScene.
Zdd
Obrigado! Na verdade, estou fazendo isso como um gancho, mas não para o EndScene, mas para o Present, e estou conectando especificamente aplicativos DirectX. Dado isso, há algo mais inteligente que eu posso fazer sobre isso.
cloudraven
@cloudraven, eu não estou familiarizado com enganchar, mas você pode dar uma olhada nesta página para referência. stackoverflow.com/questions/1994676/…
zdd 2/11/12
1

Um pouco tarde. Mas espero que isso ajude alguém.

criando

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

capturando

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

No buf, você terá dados de imagem brutos. Não se esqueça de liberar tudo.

vik_78
fonte