Как сделать снимок экрана в DirectX 9 в виде необработанного растрового изображения в памяти без использования D3DXSaveSurfaceToFile

8

Я знаю, что в OpenGL я могу сделать что-то вроде этого

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

И это довольно быстро, я получаю необработанное растровое изображение в _buffer. Когда я пытаюсь сделать это в DirectX. Предполагая, что у меня есть объект D3DDevice, я могу сделать что-то вроде этого

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

Но D3DXSaveSurfaceToFile работает довольно медленно, и мне все равно не нужно записывать запись на диск, поэтому мне было интересно, есть ли более быстрый способ сделать это.

РЕДАКТИРОВАТЬ: я ориентируюсь только на приложения DirectX. На самом деле приложения DX9. Я делаю это через крючок для подарка. (Я использую Detours, если это уместно), и я делаю это для каждого кадра. Мне не нужен (или не нужен) конкретный растровый формат, такой как BMP PNG JPEG и т. Д., И цветовое пространство RGB24 - это нормально. Получить его как YUV480p было бы лучше, но определенно не обязательно. Спасибо всем за очень полезные ответы

cloudraven
источник
2
Что вы подразумеваете под "растровым изображением"? Файл в определенном формате или массив цветов пикселей? Если это файл, попробуйте это: msdn.microsoft.com/en-us/library/windows/desktop/… Если массив, я бы тоже хотел узнать ответ.
snake5
да, я имел в виду массив байтов, 3 байта на пиксель информации RGB. То же самое, что glReadPixels делает в openGL. Я имел в виду RAW растровое изображение. Я
уточню
На самом деле, та функция, которую вы упоминаете, сохраняет ее в памяти, я могу просто прочитать d3xbuffer, который она получает. Я должен попробовать это, если это достаточно быстро, я могу просто принять это как решение. (Тем не менее, если кто-то знает более быстрый способ захвата экрана, это очень приветствуется)
cloudraven

Ответы:

6

Я делаю это следующим образом.

IDirect3DDevice9 :: GetBackBuffer : Получить доступ к IDirect3DSurface9, представляющему задний буфер, так же, как вы в настоящее время получили. Не забудьте освободить эту поверхность после завершения, так как этот вызов увеличит счетчик ссылок!

IDirect3DSurface :: GetDesc : Получить описание задней поверхности буфера, которая даст вам ее ширину, высоту и формат.

IDirect3DDevice9 :: CreateOffscreenPlainSurface : создать новый объект поверхности в D3DPOOL_SCRATCH; Вы обычно хотите использовать одинаковую ширину, высоту и формат (но на самом деле это не обязательно). Снова отпустите, когда закончите. Если вы выполняете эту операцию каждый кадр (в этом случае вам лучше смотреть на альтернативы, такие как шейдерный подход к тому, что вы пытаетесь сделать), вы можете просто создать закадровую плоскую поверхность один раз при запуске и повторно использовать это, вместо того, чтобы создавать его каждый кадр.

D3DXLoadSurfaceFromSurface : Копировать с задней поверхности буфера на невидимую плоскую поверхность. Это сделает изменение размера и форматирование автоматически для вас. В качестве альтернативы, если вы не хотите или не хотите изменять размер или изменять формат, вы можете использовать IDirect3DDevice9 :: GetRenderTargetData , но если это так, то вместо этого создайте плоскую поверхность вне экрана в D3DPOOL_SYSTEMMEM.

IDirect3DSurface9 :: LockRect : получите доступ к данным на простой экранной поверхности и используйте свой собственный злой путь; UnlockRect, когда закончите.

Это выглядит как гораздо больше кода, но вы обнаружите, что он такой же быстрый, как glReadPixels, и может быть даже быстрее, если вам не нужно выполнять преобразование формата (что почти наверняка делает glReadPixels, использующий GL_RGB).

Редактировать, чтобы добавить: некоторые (готовые и готовые) вспомогательные функции, которые у меня также есть, которые могут быть полезны для использования этого метода для снимков экрана:

// 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);
        }
    }
}
Максимус Минимус
источник
Я фактически делаю это каждый кадр. Я перехватываю функцию Present, а затем получаю кадр, захваченный в моем хуке. Не могли бы вы рассказать об использовании шейдера для этого, возможно, это то, что я ищу. Большое спасибо за ваш исчерпывающий ответ.
облачный ворон
1
Шейдерный подход подходит только в том случае, если вы выполняете последующий рендеринг с захваченным изображением. Из вашего описания это звучит больше как будто вы делаете что-то вроде захвата видео, да? Вы можете это подтвердить?
Максимус Минимус
Вы правильно догадались, но я не совсем делаю захват видео, это очень близко. Я не кодирую все кадры (но достаточно, чтобы требовать хорошей производительности), и я применяю пару 2D-фильтров к захваченному изображению перед их кодированием.
облачный ворон
Я изменил свой код для использования BGRA. Это стало лучше. Спасибо за понимание
cloudraven
Кстати, я написал следующий вопрос о шейдерном подходе. Я был бы благодарен, если вы посмотрите на это. gamedev.stackexchange.com/questions/44587/…
cloudraven
1

Я считаю, что вам нужно LockRectangle на pBackBuffer, а затем прочитать пиксели из D3DLOCKED_RECT.pBits . Это должно быть довольно быстро. Если вам нужно прочитать только несколько пикселей, рассмотрите возможность копирования всего буфера в меньший буфер через что-то вроде DX11 CopySubresourceRegion (я уверен, что есть альтернатива и в DX9), что делается на GPU, а затем передайте этот небольшой буфер из GPU в CPU через LockRectangle - вы сохраняете передачу большого буфера на шину.

GPUquant
источник
1
В ответе отсутствует самая важная часть: конверсия. pBits будет указывать на данные в формате поверхности. Скорее всего, он не будет равен массиву байтовых триплетов (RGB24).
snake5
1
Помимо преобразования формата, для этого требуется CreateDevice с блокируемым флагом обратного буфера (который в документации предупреждает о снижении производительности), а блокировка обратного буфера всегда приведет к остановке конвейера (что будет гораздо большей проблемой, чем передача данных).
Максимус Минимус
1
поэтому я посоветовал скопировать часть буфера в другой буфер. Если вы можете читать необработанные данные с поверхности, переход к формату «растровое изображение» остается за вами ...
GPUquant
На самом деле мне нужно будет преобразовать его в Yuv420p в какой-то момент в процессе. Так использует ли DirectX какой-то внутренний формат, который не является RGB?
облачный ворон
2
Нет такого понятия, как внутренний формат RGB - внутренние форматы 32bpp - даже в OpenGL GL_RGB просто означает, что свободные 8 бит не используются. См. Opengl.org/wiki/Common_Mistakes#Texture_upload_and_pixel_reads - так что OpenGL и D3D фактически будут внутренними BGRA, поэтому использование GL_RGB медленное - нужно сжимать и перебрасывать данные в RGB, что является медленным программным процессом.
Максимус Минимус
1

Вы не можете захватывать экран с помощью GetBackbuffer, эта функция получает только указатель на задний буфер, а не данные.

Правильный способ, которым я могу думать, как ниже

  1. Создайте поверхность вне экрана, вызывая CreateOffscreenPlainSurface .
  2. Вызовите GetFrontBufferData, чтобы получить изображение экрана за пределами экрана
  3. Вызовите LockRect поверхности для извлечения данных на поверхности, внеэкранная поверхность всегда была блокируемой независимо от их типов пула.
ZDD
источник
1
IDirect3DDevice9 :: GetFrontBufferData - « Эта функция очень медленная по своей конструкции и не должна использоваться ни на одном из критичных для производительности путей » - msdn.microsoft.com/en-us/library/windows/desktop/…
Maximus Minimus
1
Да, это медленно, но AFAIK, это единственный способ получить изображение экрана с помощью DirectX, или вы можете сделать несколько перехватов перед вызовом функции EndScene.
ZDD
Спасибо! На самом деле я делаю это как ловушку, но не для EndScene, а для Present, и я специально подключаю приложения DirectX. Учитывая это, есть ли что-то умнее, что я могу с этим поделать.
облачный ворон
@cloudraven, я не знаком с перехватами, но вы можете взглянуть на эту страницу для справки. stackoverflow.com/questions/1994676/…
zdd
1

Немного поздно. Но надеюсь, что это кому-то поможет.

создание

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

отлов

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

В buf у вас будут необработанные данные изображения. Не забудьте выпустить все.

vik_78
источник