# 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.