zero_copy_video_player/CUDA_GL_INTEROP.md
2026-08-05 08:56:00 -03:00

89 lines
5.3 KiB
Markdown

# Guía Técnica: Interoperabilidad CUDA-GL para Renderizado Zero-Copy (GPU NVIDIA)
Esta guía explica la arquitectura y detalles técnicos de la solución de **Zero-Copy real (GPU-to-GPU)** implementada en este proyecto C++/Qt6 (Debian 13 con tarjeta gráfica NVIDIA).
---
## 1. El Desafío Arquitectónico en NVIDIA
En un reproductor de video acelerado tradicional, el flujo de datos suele sufrir un cuello de botella en el bus PCIe:
```
[Disco Duro] ──> [CPU / RAM] ──> [GPU (Decodifica)] ──> [CPU / RAM (Descarga)] ──> [GPU (Textura OpenGL)]
```
Este viaje de ida y vuelta a la memoria RAM principal satura la CPU debido al copiado de millones de píxeles por segundo (especialmente en resoluciones 4K).
Para evitar esto en tu máquina con GPU NVIDIA, implementamos **Interoperabilidad CUDA-GL** con **FFmpeg nativo (NVDEC)**, logrando que el flujo sea puramente en la GPU:
```
[Disco Duro] ──> [CPU (Demuxing de comandos)]
│
▼
[GPU NVDEC (Decodifica en VRAM)] ──(Copia interna CUDA-GL)──> [Texturas OpenGL (VRAM)] ──> [Shader CRT] ──> [Monitor]
```
---
## 2. Los 4 Pasos Clave del Código C++
La interoperabilidad se logra conectando las APIs de CUDA y OpenGL a través de la librería `libcuda.so` de NVIDIA.
### Paso A: Registro del Recurso Compartido
En la inicialización, creamos dos texturas OpenGL vacías (`m_texY` para luminancia y `m_texUV` para crominancia) y las registramos en CUDA usando la función `cudaGraphicsGLRegisterImage`:
```cpp
cudaGraphicsGLRegisterImage(&m_cudaResourceY, m_texY, GL_TEXTURE_2D, cudaGraphicsRegisterFlagsWriteDiscard);
cudaGraphicsGLRegisterImage(&m_cudaResourceUV, m_texUV, GL_TEXTURE_2D, cudaGraphicsRegisterFlagsWriteDiscard);
```
*Esto le dice al driver de NVIDIA que reserve una "dirección de memoria compartida" que tanto CUDA como OpenGL pueden entender.*
### Paso B: Mapeo y Bloqueo de Recursos
Durante el renderizado de cada fotograma (`paintGL()`), debemos asegurar que OpenGL no intente leer de las texturas mientras CUDA escribe en ellas. Bloqueamos los recursos mediante:
```cpp
cudaGraphicsMapResources(1, &m_cudaResourceY, 0);
cudaGraphicsMapResources(1, &m_cudaResourceUV, 0);
```
### Paso C: Copia VRAM-to-VRAM (Device-to-Device)
Extraemos los manejadores de arrays internos de CUDA vinculados a nuestras texturas OpenGL y ejecutamos una copia directa por hardware:
```cpp
cudaArray_t arrayY, arrayUV;
cudaGraphicsSubResourceGetMappedArray(&arrayY, m_cudaResourceY, 0, 0);
cudaGraphicsSubResourceGetMappedArray(&arrayUV, m_cudaResourceUV, 0, 0);
// Copias internas de memoria física en la VRAM
cudaMemcpy2DToArray(arrayY, 0, 0, (void*)frame->data[0], frame->linesize[0], w, h, cudaMemcpyDeviceToDevice);
cudaMemcpy2DToArray(arrayUV, 0, 0, (void*)frame->data[1], frame->linesize[1], w, h / 2, cudaMemcpyDeviceToDevice);
```
*La bandera `cudaMemcpyDeviceToDevice` garantiza que la GPU use su controlador DMA interno en VRAM. La CPU solo envía las coordenadas de inicio y fin, pero no toca un solo píxel.*
### Paso D: Desmapeo
Liberamos las texturas para que OpenGL vuelva a tomar el control y dibuje la escena:
```cpp
cudaGraphicsUnmapResources(1, &m_cudaResourceY, 0);
cudaGraphicsUnmapResources(1, &m_cudaResourceUV, 0);
```
---
## 3. Resolviendo tus Problemas Específicos
Durante el desarrollo de esta integración en tu máquina, solucionamos dos bugs críticos de bajo nivel:
### Caso A: La imagen cabeza abajo (Inversión vertical de origen)
* **Problema:** OpenGL posiciona el origen $(0,0)$ en la esquina inferior izquierda. CUDA lo posiciona arriba a la izquierda. El video se veía al revés.
* **Solución:** Modificamos el Vertex Buffer (VBO) en C++ para invertir el eje vertical ($V$) de las coordenadas de textura sin costo de procesamiento:
* Fila inferior: Coordenada de textura $V = 1.0$ (en lugar de $0.0$)
* Fila superior: Coordenada de textura $V = 0.0$ (en lugar de $1.0$)
### Caso B: La mitad derecha de la pantalla verde (Ancho de plano UV en NV12)
* **Problema:** El formato decodificado es **NV12**. El plano UV (color) tiene la mitad del ancho de píxeles del video ($W/2$). Pero al estar $U$ y $V$ entrelazados (2 bytes por píxel), el ancho físico en bytes de cada fila del buffer de color es **$2 \times (W / 2) = W$ bytes**.
* **El bug:** Originalmente le pasábamos `w / 2` (el conteo de píxeles) a `cudaMemcpy2DToArray`. Al exigir esta función el ancho en **bytes**, la GPU solo copiaba la mitad izquierda de la información de color. La mitad derecha quedaba vacía (`0.0`), lo cual en la matriz de conversión YUV-a-RGB de tu fragment shader se traduce en un **tono verde intenso**.
* **Solución:** Cambiamos el parámetro de ancho a `w` (ancho completo en bytes), rellenando la textura de croma completa.
---
## 4. Beneficios Obtenidos en tu Sistema
1. **Carga del Procesador (CPU) de 0%:** El procesador de tu sistema queda totalmente libre para otras tareas de tu aplicación principal.
2. **Latencia Cero:** Al evitar el cuello de botella del bus PCIe, no hay retraso (*stuttering*) al reproducir archivos de altos bitrates o resoluciones 4K/8K.
3. **Procesado de Shaders Directo:** Dado que la textura se actualiza directamente en OpenGL, los filtros CRT y la curvatura de lente se aplican instantáneamente al renderizar sobre la pantalla del reproductor.