Cara menangkap layar di DirectX 9 ke bitmap mentah dalam memori tanpa menggunakan D3DXSaveSurfaceToFile

8

Saya tahu bahwa di OpenGL saya dapat melakukan sesuatu seperti ini

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

Dan ini cukup cepat, saya mendapatkan bitmap mentah di _buffer. Ketika saya mencoba melakukan ini di DirectX. Dengan asumsi bahwa saya memiliki objek D3DDevice, saya dapat melakukan sesuatu seperti ini

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

Tapi D3DXSaveSurfaceToFile cukup lambat, dan saya tidak perlu menulis tangkapan ke disk, jadi saya bertanya-tanya apakah ada cara yang lebih cepat untuk melakukan ini.

EDIT: Saya hanya menargetkan aplikasi DirectX. Sebenarnya aplikasi DX9. Saya melakukan ini melalui hook to Present. (Saya menggunakan jalan memutar jika itu relevan), dan saya melakukan ini untuk setiap frame. Saya tidak perlu (atau ingin) format bitmap tertentu seperti BMP PNG JPEG dll, dan ruang warna dari RGB24 adalah OK. Mendapatkannya sebagai YUV480p akan lebih baik tapi jelas tidak perlu. Terima kasih semuanya atas jawaban yang sangat membantu

cloudraven
sumber
2
Apa yang Anda maksud dengan "bitmap"? File dalam format tertentu atau array warna piksel? Jika ini file, coba ini: msdn.microsoft.com/en-us/library/windows/desktop/… Jika array, saya juga ingin tahu jawabannya.
snake5
ya, maksud saya array byte, 3 byte per piksel informasi RGB. Sama seperti yang dilakukan glReadPixels di openGL. Saya maksudkan bitmap RAW. Saya akan memperbarui pertanyaan untuk membuatnya lebih jelas
cloudraven
Sebenarnya, fungsi yang Anda sebutkan menyimpannya di memori, saya hanya bisa membaca d3xbuffer yang didapatnya. Saya harus mencobanya, jika cukup cepat saya mungkin menerimanya sebagai solusinya. (Namun jika seseorang mengetahui cara yang lebih cepat untuk menangkap layar, itu sangat disambut baik)
cloudraven

Jawaban:

6

Cara saya melakukan ini adalah sebagai berikut.

IDirect3DDevice9 :: GetBackBuffer : Dapatkan akses ke IDirect3DSurface9 yang mewakili buffer belakang, sama seperti yang Anda miliki saat ini. Jangan lupa untuk melepaskan permukaan ini ketika dilakukan karena panggilan ini akan menambah jumlah referensi!

IDirect3DSurface :: GetDesc : Dapatkan deskripsi permukaan buffer belakang, yang akan memberi Anda lebar, tinggi, dan format.

IDirect3DDevice9 :: CreateOffscreenPlainSurface : Buat objek permukaan baru di D3DPOOL_SCRATCH; Anda biasanya ingin menggunakan lebar, tinggi, dan format yang sama (tetapi Anda sebenarnya tidak harus menggunakan metode ini). Sekali lagi, Lepaskan setelah selesai. Jika Anda melakukan operasi ini setiap frame (dalam hal ini Anda lebih baik melihat alternatif seperti pendekatan berbasis shader untuk apa yang Anda coba lakukan), Anda bisa membuat permukaan polos layar sekali pada saat startup dan menggunakan kembali itu, alih-alih membuatnya setiap frame.

D3DXLoadSurfaceFromSurface : Salin dari permukaan buffer belakang ke permukaan polos offsceen. Ini akan melakukan perubahan ukuran dan memformat konversi secara otomatis untuk Anda. Atau, jika Anda tidak ingin atau perlu mengubah ukuran atau mengubah format, Anda bisa menggunakan IDirect3DDevice9 :: GetRenderTargetData , tetapi jika demikian, buat permukaan polos layar di D3DPOOL_SYSTEMMEM sebagai gantinya.

IDirect3DSurface9 :: LockRect : Dapatkan akses ke data di permukaan polos layar dan miliki cara jahat Anda sendiri dengannya; UnlockRect saat selesai.

Ini terlihat seperti kode yang lebih banyak tetapi Anda akan menemukan bahwa itu secepat glReadPixels, dan bahkan dapat lebih cepat jika Anda tidak perlu melakukan konversi format (yang glReadPixels menggunakan GL_RGB hampir pasti melakukannya).

Sunting untuk menambahkan: beberapa fungsi pembantu yang telah saya siapkan yang mungkin berguna untuk menggunakan metode ini untuk tangkapan layar:

// 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
sumber
Saya benar-benar melakukannya setiap frame. Saya mengaitkan fungsi Present dan kemudian mengambil bingkai yang ditangkap dalam hook saya. Bisakah Anda jelaskan menggunakan shader untuk melakukan ini, itu mungkin yang saya cari. Terima kasih banyak atas jawaban menyeluruh Anda.
cloudraven
1
Pendekatan berbasis shader hanya tepat jika Anda melakukan rendering berikutnya dengan gambar yang diambil. Dari uraian Anda, sepertinya Anda melakukan sesuatu seperti menangkap video, ya? Bisakah Anda mengkonfirmasi ini?
Maximus Minimus
Anda menebak dengan benar, tetapi saya tidak benar-benar melakukan pengambilan video, itu sangat dekat. Saya tidak menyandikan semua bingkai (tapi cukup untuk membutuhkan kinerja yang baik), dan saya menerapkan beberapa filter 2D ke gambar yang diambil sebelum menyandikannya.
cloudraven
Saya mengubah kode saya untuk menggunakan BGRA. Itu menjadi lebih baik. Terima kasih atas wawasannya
cloudraven
Ngomong-ngomong, saya memposting pertanyaan tindak lanjut tentang pendekatan shader. Saya akan berterima kasih jika Anda melihatnya. gamedev.stackexchange.com/questions/44587/…
cloudraven
1

Saya percaya Anda perlu LockRectangle di pBackBuffer dan kemudian membaca piksel dari D3DLOCKED_RECT.pBits . Ini harusnya cukup cepat. Jika Anda hanya perlu membaca beberapa piksel, pertimbangkan untuk menyalin seluruh backbuffer ke buffer yang lebih kecil melalui sesuatu seperti DX11 CopySubresourceRegion (Saya yakin ada alternatif di DX9 juga), yang dilakukan pada GPU dan kemudian mengalirkan buffer kecil ini dari GPU ke CPU via LockRectangle - Anda menghemat transfer backbuffer besar di bus.

GPUquant
sumber
1
Jawabannya hilang bagian terpenting: konversi. pBits akan menunjuk ke data dalam format permukaan. Kemungkinan besar tidak akan sama dengan array byte triplet (RGB24).
snake5
1
Selain dari konversi format, ini membutuhkan CreateDevice dengan flag backbuffer yang dapat dikunci (yang diperingatkan oleh dokumentasi adalah hit kinerja) dan mengunci backbuffer akan selalu menimbulkan kios pipa (yang akan menjadi masalah yang jauh lebih besar daripada transfer data).
Maximus Minimus
1
itu sebabnya saya disarankan untuk menyalin sebagian backbuffer ke buffer yang berbeda. Jika Anda dapat membaca data mentah dari permukaan, konversi ke format "bitmap" terserah Anda ...
GPUquant
Saya benar-benar perlu mengubahnya ke Yuv420p di beberapa bagian selama proses. Jadi apakah DirectX menggunakan beberapa format internal yang bukan RGB?
cloudraven
2
Tidak ada yang namanya format internal RGB - format internal 32bpp - bahkan di OpenGL GL_RGB hanya berarti bahwa 8 bit cadangan tidak digunakan. Lihat opengl.org/wiki/Common_Mistakes#Texture_upload_and_pixel_reads - sehingga OpenGL dan D3D benar-benar akan menjadi BGRA secara internal yang mengapa menggunakan GL_RGB lambat - ia perlu mengompres dan mengaduk data ke RGB, yang merupakan proses perangkat lunak yang lambat.
Maximus Minimus
1

Anda tidak dapat menangkap layar dengan GetBackbuffer, fungsi ini hanya mendapatkan pointer buffer belakang, bukan data.

Cara yang benar yang bisa saya pikirkan adalah seperti di bawah ini

  1. Buat permukaan di luar layar dengan memanggil CreateOffscreenPlainSurface .
  2. Panggil GetFrontBufferData untuk mendapatkan gambar layar ke permukaan di luar layar
  3. Panggil LockRect dari permukaan untuk mengambil data di permukaan, permukaan di luar layar selalu bisa dikunci terlepas dari jenis kolam mereka.
zdd
sumber
1
IDirect3DDevice9 :: GetFrontBufferData - " Fungsi ini sangat lambat, karena desain, dan tidak boleh digunakan dalam jalur kinerja-kritis " - msdn.microsoft.com/en-us/library/windows/desktop/…
Maximus Minimus
1
Ya, ini lambat, tetapi AFAIK, itu satu-satunya cara untuk mendapatkan gambar layar dengan DirectX, atau Anda dapat melakukan beberapa pengait sebelum memanggil fungsi EndScene.
zdd
Terima kasih! Saya benar-benar melakukannya sebagai pengait, tetapi bukan untuk EndScene tetapi untuk Present, dan saya mengaitkan aplikasi directX khusus. Mengingat itu, apakah ada sesuatu yang lebih pintar yang bisa saya lakukan tentang hal itu.
cloudraven
@cloudraven, saya tidak terbiasa dengan hooking, tetapi Anda dapat melihat halaman ini untuk referensi. stackoverflow.com/questions/1994676/…
zdd
1

Sedikit terlambat. Tapi semoga ini membantu seseorang.

menciptakan

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

menangkap

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

Di buf Anda akan memiliki data gambar mentah. Jangan lupa untuk melepaskan semuanya.

vik_78
sumber