Neste artigo, quero falar sobre como é fácil atualizar as texturas do OpenGLES por meio do DMABUF. Procurei em Habr e, para minha surpresa, não encontrei um único artigo sobre esse assunto. Em Habr, as perguntas e respostas também não encontraram nada disso. E isso é um pouco estranho para mim. A tecnologia apareceu há algum tempo, embora realmente não exista muita informação sobre ela na rede, tudo é vago e contraditório.
Eu coletei todas essas informações pouco a pouco de diferentes fontes antes de poder escrever um player de vídeo como na demonstração acima. Aqui, em uma demonstração, meu reprodutor de vídeo criado com base na biblioteca gstreamer carrega quadros de vídeo na textura OpenGLESv2 todas as vezes antes da renderização. Alimentado por Raspberry Pi4. Os quadros são simplesmente copiados para uma memória alocada especialmente - e o DMA os transfere para a memória da GPU, para a textura. Em seguida, vou lhe contar como fiz.
Normalmente, um programador usando o OpenGLESv2 cria uma textura apenas uma vez e, em seguida, simplesmente a renderiza em objetos de cena. Isso acontece, porque as roupas dos personagens raramente mudam e, às vezes, recarregar a textura com glTexSubImage2D () não é difícil. No entanto, os problemas reais começam quando a textura é dinâmica, quando você precisa atualizá-la quase todos os quadros durante a renderização. A função glTexSubImage2D () está muito lenta. Bem, quão lento - é claro, tudo depende do computador e da placa gráfica. Eu queria encontrar uma solução que funcionasse mesmo em cartões de placa única fracos como Raspberry.
A arquitetura de muitos computadores modernos, incluindo os de placa única SoC, é tal que a memória do processador é separada da memória da GPU. Normalmente, os programas do usuário não têm acesso direto à memória da GPU e você precisa usar várias funções da API como a mesma glTexSubImage2D (). Além disso, li em algum lugar que a representação interna da textura pode diferir da representação tradicional de imagens como uma sequência de pixels. Não sei como isso é verdade. Possivelmente.
Então, o que a tecnologia DMABUF me fornece? A memória é especialmente alocada e um processo a partir de qualquer thread pode simplesmente escrever pixels lá sempre que quiser. O próprio DMA transferirá todas as alterações para a textura na memória da GPU. Isso não é bonito?
Devo dizer imediatamente que eu sei sobre o PBO - Pixel Buffer Object, geralmente com a ajuda da atualização dinâmica da textura do PBO, o DMA parece ser usado também, mas o PBO apareceu apenas no OpenGLESv3 e não em todas as implementações. Então não - infelizmente, este não é o meu caminho.
O artigo pode ser de interesse tanto para programadores de Raspberry quanto para desenvolvedores de jogos, e provavelmente para programadores de Android, já que o OpenGLES também é usado lá e tenho certeza de que essa tecnologia DMABUF também está presente lá (pelo menos, tenho certeza de que você pode usá-la no Android NDK).
Vou escrever um programa usando DMABUF em um Raspberry Pi4. O programa também deve (e funcionará) em computadores Intel x86 / x86_64 comuns, digamos no ubuntu.
Neste artigo, presumo que você já saiba como programar gráficos com a API OpenGLESv2. Embora, não haverá muitos desses desafios. Principalmente teremos magia ioctl.
Portanto, a primeira coisa a fazer é garantir que a API disponível na plataforma tenha suporte ao DMABUF. Para fazer isso, verifique a lista de extensões EGL:
char* EglExtString = (char*)eglQueryString( esContext->eglDisplay, EGL_EXTENSIONS ); if( strstr( EglExtString, "EGL_EXT_image_dma_buf_import") ) { cout << "DMA_BUF feature must be supported!!!\n"; }
Portanto, entenderemos imediatamente se há alguma esperança de usar o DMABUF ou se não há esperança. Por exemplo, no Raspberry Pi3 e em todas as placas anteriores, não há esperança. Em geral, mesmo o OpenGLESv2 é desmembrado, através de bibliotecas especiais com o broche BRCM. E agora no Raspberry Pi4 existe um OpenGLES real, a extensão EGL_EXT_image_dma_buf_import é, hooray.
Anotarei imediatamente o SO que tenho em um Pi4 de placa única; caso contrário, também poderá haver problemas com isso:
pi@raspberrypi:~ $ lsb_release -a No LSB modules are available. Distributor ID: Raspbian Description: Raspbian GNU/Linux 10 (buster) Release: 10 Codename: buster pi@raspberrypi:~ $ uname -a Linux raspberrypi 4.19.75-v7l+ #1270 SMP Tue Sep 24 18:51:41 BST 2019 armv7l GNU/Linux
Também observo que a extensão EGL_EXT_image_dma_buf_import está no Orange Pi PC (Mali-400) / PC2 (Mali-450), a menos que você possa executar a GPU do Mali nessas placas (em assembléias oficiais não existe, instalei-a no Armbian, mais eu fiz isso sozinho conjunto do driver do kernel). Ou seja, o DMABUF está em quase todo lugar. Só é necessário levar e usar.
Em seguida, você precisa abrir o arquivo / dev / dri / card0 ou / dev / dri / card1 - um deles, depende da plataforma, acontece de forma diferente, é necessário procurar o arquivo que suporta DRM_CAP_DUMB_BUFFER:
int OpenDrm() { int fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); if( fd < 0 ) { cout << "cannot open /dev/dri/card0\n"; return -1; } uint64_t hasDumb = 0; if( drmGetCap(fd, DRM_CAP_DUMB_BUFFER, &hasDumb) < 0 ) { close( fd ); cout << "/dev/dri/card0 has no support for DUMB_BUFFER\n";
Aqui, a propósito, há uma sutileza inexplicável para mim. Algumas plataformas não possuem bibliotecas que fornecem a função DRI2Authenticate (). Por exemplo, não está no crack e na versão de 32 bits para o Orange Pi PC. Tudo isso é estranho. Mas eu encontrei um repositório no GITHUB:
github.com/robclark/libdri2, ele pode ser usado, montado e instalado, e tudo está bem. É estranho que no meu Ubuntu 18 (64 bits) em um laptop não haja problema.
Se você pudesse encontrar e abrir / dev / dri / cardX, poderá seguir em frente. Você precisa acessar as três funções muito necessárias do KHR (Khronos):
PFNEGLCREATEIMAGEKHRPROC funcEglCreateImageKHR = nullptr; PFNEGLDESTROYIMAGEKHRPROC funcEglDestroyImageKHR = nullptr; PFNGLEGLIMAGETARGETTEXTURE2DOESPROC funcGlEGLImageTargetTexture2DOES = nullptr; ... funcEglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC) eglGetProcAddress("eglCreateImageKHR"); funcEglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC) eglGetProcAddress("eglDestroyImageKHR"); funcGlEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES"); if( funcEglCreateImageKHR && funcEglDestroyImageKHR && funcGlEGLImageTargetTexture2DOES ) { cout << "DMA_BUF feature supported!!!\n"; } else { CloseDrm(); }
Agora precisamos de uma função que crie uma área de memória para DMABUF. A função usa parâmetros como largura, altura e bitmap de bitmap, para os quais será retornado o manipulador do descritor de arquivo DmaFd e um ponteiro para a memória de bitmap Plane.
nt CreateDmaBuf( int Width, int Height, int* DmaFd, void** Plane ) { int dmaFd = *DmaFd = 0; void* pplane = *Plane = nullptr;
Agora, precisamos criar uma imagem EGL associada ao manipulador DmaFd:
int CreateDmaBufferImage( ESContext* esContext, int Width, int Height, int* DmaFd, void** Plane, EGLImageKHR* Image ) { int dmaFd = 0; void* planePtr = nullptr; int Bpp = 32; int ret0 = CreateDmaBuf( Width, Height, &dmaFd, &planePtr ); if( ret0<0 ) return -1; EGLint img_attrs[] = { EGL_WIDTH, Width, EGL_HEIGHT, Height, EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_ABGR8888, EGL_DMA_BUF_PLANE0_FD_EXT, dmaFd, EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, EGL_DMA_BUF_PLANE0_PITCH_EXT, Width * Bpp / 8, EGL_NONE }; EGLImageKHR image = funcEglCreateImageKHR( esContext->eglDisplay, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, 0, &img_attrs[0] ); *Plane = planePtr; *DmaFd = dmaFd; *Image = image; cout << "DMA_BUF pointer " << (void*)planePtr << "\n"; cout << "DMA_BUF fd " << (int)dmaFd << "\n"; cout << "EGLImageKHR " << image << "\n"; return 0; }
E, finalmente, nossas provações estão quase no fim e precisamos vincular a imagem do EGL e a imagem do OpenGLESv2. A função retorna um ponteiro para a memória no espaço de endereço do processo. Lá você pode simplesmente escrever a partir de qualquer segmento do processador e todas as alterações ao longo do tempo aparecem automaticamente na textura da GPU através do DMABUF.
void* CreateVideoTexture( ESContext* esContext, int Width, int Height ) { CreateDmaBufferImage( esContext, Width, Height, &esContext->DmaFd, &esContext->Plane, &esContext->ImageKHR ); GLuint texId; glGenTextures ( 1, &texId ); glBindTexture ( GL_TEXTURE_2D, texId ); glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); funcGlEGLImageTargetTexture2DOES(GL_TEXTURE_2D, esContext->ImageKHR ); checkGlError( __LINE__ ); UserData *userData = (UserData*)esContext->userData; userData->textureV = texId; userData->textureV_ready = true; return esContext->Plane; }
A função GlEGLImageTargetTexture2DOES (..) faz essa ligação. Ele usa a criação normal de ID de textura glGenTextures (..) e o associa à imagem esContext-> ImageKHR EGL criada anteriormente. Depois disso, a textura userData-> textureV pode ser usada em shaders regulares. E o ponteiro esContext-> Plane é um ponteiro para a área na memória onde você precisa escrever para atualizar a textura.
Aqui está um trecho de código que copia um quadro de vídeo:
GstFlowReturn on_new_sample( GstAppSink *pAppsink, gpointer pParam ) { GstFlowReturn ret = GST_FLOW_OK; GstSample *Sample = gst_app_sink_pull_sample(pAppsink); if( Sample ) { if( VideoWidth==0 || VideoHeight==0 ) { GstCaps* caps = gst_sample_get_caps( Sample ); GstStructure* structure = gst_caps_get_structure (caps, 0); gst_structure_get_int (structure, "width", &VideoWidth); gst_structure_get_int (structure, "height", &VideoHeight); cout << "Stream Resolution " << VideoWidth << " " << VideoHeight << "\n"; } GstBuffer *Buffer = gst_sample_get_buffer( Sample ); if( Buffer ) { GstMapInfo MapInfo; memset(&MapInfo, 0, sizeof(MapInfo)); gboolean Mapped = gst_buffer_map( Buffer, &MapInfo, GST_MAP_READ ); if( Mapped ) { if( dmabuf_ptr ) memcpy( dmabuf_ptr, MapInfo.data, MapInfo.size ); gst_buffer_unmap( Buffer, &MapInfo); frame_ready = true; update_cv.notify_one(); } } gst_sample_unref( Sample ); } return ret; }
Essa função é chamada pelo próprio gstreamer toda vez que um novo quadro de vídeo aparece. Nós o recuperamos usando gst_app_sink_pull_sample (). Esta função possui memcpy (), que copia o quadro na memória DMABUF. Em seguida, o sinalizador frame_ready é definido e, por meio de std :: condition_variable update_cv.notify_one (), o fluxo processado é ativado.
Provavelmente é tudo ...
Embora não, eu estou mentindo. Ainda existem problemas de sincronização.
A primeira é que o processador grava na memória, mas esses registros podem acabar no cache do processador e permanecer lá, é necessário fazer um cache após a gravação. A segunda - não seria ruim saber exatamente quando o DMA já funcionou e você pode começar a renderizar. Honestamente, se o primeiro eu ainda imagino como fazer, então o segundo - não. Se você tiver idéias, escreva nos comentários.
E mais uma coisa. Estou usando o gstreamer, que reproduz um arquivo de vídeo. Adicionei um appsink genérico ao pipeline, que recebe os quadros de vídeo. Pego os pixels dos quadros de vídeo e simplesmente os copio memcpy () para a área de memória DMABUF. A renderização está em um encadeamento separado, main (). Mas eu gostaria de me livrar dessa cópia. Toda cópia é má. Existe até esse termo cópia zero. E, a julgar pela documentação, parece que o próprio gstreamer pode renderizar quadros imediatamente no DMABUF. Infelizmente, não encontrei um único exemplo real. Eu olhei para as fontes do gstreamer - há algo sobre isso, mas como usá-lo exatamente não está claro. Se você sabe como criar quadros reais de cópia zero com o gstreamer na textura OpenGLESv2 - escreva.
Talvez o último ponto: no meu projeto eu uso bitmaps de 32 bits, o que não é bom no meu caso. Seria muito mais razoável tirar YUV do gstreamer, então o tamanho do quadro de vídeo é significativamente menor, mas a lógica é complicada - eu precisaria fazer 3 DMABUF para três texturas separadamente Y, U, V. Bem, o shader também é complicado, você precisa converter YUV para ARGB bem no shader.
Você pode ver o projeto inteiro
no github . No entanto, peço desculpas antecipadamente aos amantes de códigos / estilos limpos e corretos. Admito que foi escrito de forma descuidada com a ajuda do Google-mine-paste.