89 lines
5.3 KiB
Markdown
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.
|