zero_copy_video_player/guide.html

250 lines
11 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Comparativa: Decodificación Tradicional vs CUDA-GL Interop Zero-Copy</title>
<style>
body {
font-family: 'Liberation Sans', 'DejaVu Sans', sans-serif;
background-color: #ffffff;
color: #111111;
line-height: 1.6;
font-size: 14px;
margin: 0;
padding: 0;
}
.container {
max-width: 850px;
margin: 0 auto;
padding: 30px;
}
.header-container {
text-align: center;
margin-bottom: 25px;
padding: 10px 0;
border: none;
}
h1 {
color: #111111;
font-size: 24px;
margin: 0 0 10px 0;
text-transform: uppercase;
letter-spacing: 1px;
border: none;
}
.subtitle {
color: #555555;
font-size: 14px;
font-weight: normal;
margin: 0;
letter-spacing: 0.5px;
border: none;
}
h2 {
color: #111111;
font-size: 16px;
border-bottom: 1px solid #cccccc;
padding-bottom: 4px;
margin-top: 35px;
text-transform: uppercase;
}
h3 {
color: #222222;
font-size: 13px;
margin-top: 20px;
}
p {
text-align: justify;
color: #222222;
}
.highlight {
color: #000000;
font-weight: bold;
}
.code-block {
background-color: #000000;
border: none;
border-radius: 4px;
font-family: 'Liberation Mono', 'Courier New', monospace;
font-size: 12px;
padding: 12px;
margin: 15px 0;
white-space: pre-wrap;
color: #d4d4d4;
line-height: 1.4;
}
.code-keyword { color: #569cd6; font-weight: bold; }
.code-type { color: #4ec9b0; font-weight: bold; }
.code-comment { color: #6a9955; font-style: italic; }
.code-string { color: #ce9178; }
.diagram-container {
text-align: center;
margin: 25px 0;
background-color: #ffffff;
border: none;
border-radius: 0px;
padding: 0px;
}
.badge {
background-color: #f0f0f0;
border: 1px solid #cccccc;
color: #333333;
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: bold;
display: inline-block;
margin-right: 5px;
}
.badge-pink {
background-color: #f0f0f0;
border: 1px solid #cccccc;
color: #333333;
}
.bug-box {
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin: 20px 0;
}
.bug-title {
color: #111111;
font-weight: bold;
margin-bottom: 5px;
font-size: 13px;
text-transform: uppercase;
}
.footer {
text-align: center;
margin-top: 60px;
font-size: 11px;
color: #666666;
border-top: 1px solid #e0e0e0;
padding-top: 15px;
}
</style>
</head>
<body>
<div class="container">
<div class="header-container">
<h1>Flujos de Arquitectura Gráfica</h1>
<div class="subtitle">Decodificación Tradicional vs Interoperabilidad CUDA-GL</div>
</div>
<h2>1. Caso A: Decodificación Acelerada Tradicional (Con Copia PCIe Redundante)</h2>
<p>
En la decodificación acelerada por GPU clásica (la que usa la mayoría de reproductores estándar), el video se procesa en el chip decodificador físico de la GPU (NVDEC). Sin embargo, al no estar enlazado con la API gráfica, la CPU tiene que realizar una descarga forzada de los píxeles a la RAM del sistema (`av_hwframe_transfer_data`). Posteriormente, el hilo de renderizado vuelve a subir la imagen a la GPU como una textura de OpenGL (`glTexSubImage2D`).
</p>
<p>
Este flujo cruza el bus <span class="highlight">PCIe</span> dos veces por cada fotograma, lo que sobrecarga la CPU en un 15% - 30% a altas resoluciones y limita la fluidez.
</p>
<div class="diagram-container">
<img src="assets/diagram_traditional.png" width="480" height="104" alt="Diagrama de flujo tradicional" style="display: block; margin: 0 auto;">
</div>
<h2>2. Caso B: Decodificación con Interoperabilidad CUDA-GL (Zero-Copy)</h2>
<p>
En la solución optimizada, el video se decodifica en VRAM por NVDEC. Luego, las texturas de OpenGL se mapean directamente en CUDA. Realizamos una transferencia interna **dentro del mismo bus de memoria de la tarjeta gráfica** mediante la función `cudaMemcpy2DToArray` con la instrucción `cudaMemcpyDeviceToDevice`.
</p>
<p>
Los píxeles nunca abandonan la VRAM. El bus PCIe queda libre al 100%, la CPU no procesa copias de datos (0% de consumo) y el renderizado se realiza a la velocidad física de la memoria del procesador gráfico (Giga/Terabytes por segundo).
</p>
<div class="diagram-container">
<img src="assets/diagram_interop.png" width="480" height="104" alt="Diagrama de flujo de interoperabilidad" style="display: block; margin: 0 auto;">
</div>
<h2>3. Detalles Técnicos de Implementación</h2>
<h3>Punto 1: Inicialización del Decodificador de Hardware NVIDIA (NVDEC)</h3>
<p>
En el archivo <span class="highlight">videodecoder.cpp</span>, configuramos FFmpeg para crear un contexto de hardware forzado a utilizar la API de <strong>CUDA</strong>:
</p>
<div class="code-block"><span class="code-type">AVHWDeviceType</span> devType = AV_HWDEVICE_TYPE_CUDA;
m_hwPixFmt = AV_PIX_FMT_CUDA;
<span class="code-keyword">av_hwdevice_ctx_create</span>(&amp;m_hwDeviceCtx, devType, <span class="code-keyword">nullptr</span>, <span class="code-keyword">nullptr</span>, 0);</div>
<p>
El decodificador físico de NVIDIA (<strong>NVDEC</strong>) descomprime la imagen directamente en VRAM en formato **NV12**, retornando un fotograma (`AVFrame`) que contiene punteros de CUDA en VRAM.
</p>
<h3>Punto 2: Creación y Registro de Texturas OpenGL</h3>
<p>
En el archivo <span class="highlight">glwidget.cpp</span>, creamos las texturas vacías en OpenGL. Como el formato del video en VRAM es NV12, se crean dos texturas: una para la Luminancia ($Y$) y otra para la Crominancia entrelazada ($UV$). Reservamos el tamaño de almacenamiento en VRAM usando `nullptr` como puntero de datos inicial:
</p>
<div class="code-block"><span class="code-comment">// Reservar textura Y (Luminancia - 1 canal)</span>
<span class="code-keyword">glBindTexture</span>(GL_TEXTURE_2D, m_texY);
<span class="code-keyword">glTexImage2D</span>(GL_TEXTURE_2D, 0, GL_RED, w, h, 0, GL_RED, GL_UNSIGNED_BYTE, <span class="code-keyword">nullptr</span>);
<span class="code-comment">// Reservar textura UV (Crominancia - 2 canales)</span>
<span class="code-keyword">glBindTexture</span>(GL_TEXTURE_2D, m_texUV);
<span class="code-keyword">glTexImage2D</span>(GL_TEXTURE_2D, 0, GL_RG, w / 2, h / 2, 0, GL_RG, GL_UNSIGNED_BYTE, <span class="code-keyword">nullptr</span>);</div>
<p>
Luego, registramos los identificadores de estas texturas en CUDA a través de la API CUDA-GL Interop:
</p>
<div class="code-block"><span class="code-keyword">cudaGraphicsGLRegisterImage</span>(&amp;m_cudaResourceY, m_texY, GL_TEXTURE_2D, cudaGraphicsRegisterFlagsWriteDiscard);
<span class="code-keyword">cudaGraphicsGLRegisterImage</span>(&amp;m_cudaResourceUV, m_texUV, GL_TEXTURE_2D, cudaGraphicsRegisterFlagsWriteDiscard);</div>
<h3>Punto 3: Transferencia Directa VRAM-to-VRAM (Device-to-Device)</h3>
<p>
En el método `paintGL()`, el hilo de pintado de OpenGL bloquea las texturas de lectura externa y delega temporalmente el control a CUDA. Luego, se realiza una copia directa de memoria entre las superficies del decodificador y las texturas:
</p>
<div class="code-block"><span class="code-comment">// Bloqueo de textura para OpenGL</span>
<span class="code-keyword">cudaGraphicsMapResources</span>(1, &amp;m_cudaResourceY, 0);
<span class="code-keyword">cudaGraphicsMapResources</span>(1, &amp;m_cudaResourceUV, 0);
<span class="code-comment">// Copia ultra-rápida interna en GPU</span>
<span class="code-keyword">cudaMemcpy2DToArray</span>(arrayY, 0, 0, (<span class="code-type">void</span>*)frame-&gt;data[0], frame-&gt;linesize[0], w, h, <span class="code-type">cudaMemcpyDeviceToDevice</span>);
<span class="code-keyword">cudaMemcpy2DToArray</span>(arrayUV, 0, 0, (<span class="code-type">void</span>*)frame-&gt;data[1], frame-&gt;linesize[1], w, h / 2, <span class="code-type">cudaMemcpyDeviceToDevice</span>);
<span class="code-comment">// Devolución del control de las texturas a OpenGL</span>
<span class="code-keyword">cudaGraphicsUnmapResources</span>(1, &amp;m_cudaResourceY, 0);</div>
<h3>Punto 4: Conversión Cromática y Filtros en el Fragment Shader</h3>
<p>
Para corregir el origen invertido entre OpenGL y CUDA, las coordenadas del Quad están configuradas con el eje vertical invertido ($V = 1.0 - V$). En el fragment shader (<span class="highlight">shader.frag</span>), leemos ambas texturas en paralelo y realizamos la conversión matemática de NV12 a RGB:
</p>
<div class="code-block"><span class="code-type">float</span> y = texture(u_texY, uv).r;
<span class="code-type">vec2</span> uvVal = texture(u_texUV, uv).rg;
<span class="code-type">float</span> u = uvVal.r - 0.5;
<span class="code-type">float</span> v = uvVal.g - 0.5;
<span class="code-type">float</span> r = y + 1.402 * v;
<span class="code-type">float</span> g = y - 0.344136 * u - 0.714136 * v;
<span class="code-type">float</span> b = y + 1.772 * u;
<span class="code-type">vec3</span> color = <span class="code-type">vec3</span>(r, g, b);</div>
<p>
Por último, sobre esta señal de color RGB, se aplican los filtros de curvatura 3D de la lente, líneas de barrido (scanlines), aberración cromática y viñeteado.
</p>
<h2>4. Resolución de Bugs Críticos en el Proyecto</h2>
<div class="bug-box">
<div class="badge">Efecto Espejo</div>
<div class="bug-title">Inversión de Coordenadas Y (OpenGL vs CUDA)</div>
<p>
OpenGL lee las texturas desde la esquina inferior izquierda, mientras que CUDA genera los frames con su origen en la esquina superior izquierda. La imagen aparecía invertida verticalmente. Esto se corrigió invirtiendo directamente la coordenada vertical de textura en el Quad de OpenGL en C++, evitando añadir costo aritmético al Fragment Shader.
</p>
</div>
<div class="bug-box">
<div class="badge badge-pink">Tono Verde</div>
<div class="bug-title">Copia de Crominancia UV en bytes</div>
<p>
El formato de video NV12 almacena el color agrupando U y V de forma entrelazada (2 bytes por píxel). Aunque el número de píxeles horizontales es de $W/2$, el ancho en bytes es de **$2 \times (W / 2) = W$ bytes**. La API de CUDA `cudaMemcpy2DToArray` exige el ancho en <strong>bytes</strong>.
</p>
<p>
Al indicarle previamente `w / 2`, solo se copiaba el lado izquierdo de los datos de color de la imagen. La mitad derecha del buffer se mantenía en `0.0`. Al procesar la conversión matemática YUV-a-RGB en el fragment shader, una crominancia nula se desvía fuertemente hacia una tonalidad verde. Se corrigió modificando el ancho de transferencia de la copia a `w` bytes en `glwidget.cpp`.
</p>
</div>
<div class="footer">
Guía de Integración Técnica • Debian 13 • GPU NVIDIA
</div>
</div>
</body>
</html>