Optimización de la representación de una escena de la caricatura de Disney "Moana". Parte 1

Walt Disney Animation Studios (WDAS) recientemente hizo un regalo invaluable a la comunidad de investigación de renderización al lanzar una escena completa de la isla de la caricatura de Moana . La geometría y las texturas para un cuadro ocupan más de 70 GB de espacio en disco. Este es un excelente ejemplo del grado de complejidad que los sistemas de renderizado tienen que enfrentar hoy; Nunca antes los investigadores y desarrolladores involucrados en la representación fuera de los estudios de cine habían podido trabajar con escenas tan realistas.

Así es como se ve el resultado de renderizar una escena con pbrt moderno:


Una isla de Moana renderizada por pbrt-v3 a una resolución de 2048x858 con 256 muestras por píxel. El tiempo total de representación en una instancia de 12 núcleos / 24 hilos de Google Compute Engine con una frecuencia de 2 GHz con la última versión de pbrt-v3 fue de 1 h 44 min 45 s.

Por parte de Disney, fue un trabajo enorme, tuvo que extraer la escena de su propio formato interno y convertirla al habitual; Un agradecimiento especial a ella por el tiempo dedicado a empaquetar y preparar estos datos para un uso generalizado. Estoy seguro de que su trabajo será bien recompensado en el futuro porque los investigadores usan esta escena para estudiar los problemas de renderizar eficientemente escenas de este nivel de complejidad.

Esta escena ya me enseñó mucho y me permitió mejorar el renderizador pbrt, pero antes de entrar en esto, contaré una historia corta para comprender el contexto.

El hash que no era


Hace muchos años, mientras hacía una pasantía en el equipo de renderizado de Pixar, aprendí una curiosa lección: las cosas "interesantes" casi siempre aparecen cuando los datos de entrada se pasan al sistema del programa que es significativamente diferente de todo lo que era antes. Incluso en sistemas de software bien escritos y maduros, los nuevos tipos de entrada casi siempre conducen al descubrimiento de defectos desconocidos en una implementación existente.

Aprendí esta lección por primera vez durante la producción de Toy Story 2 . Un día, alguien notó que se pasó una increíble cantidad de tiempo analizando archivos de descripción de escenas RIB. Alguien más del equipo de renderizado (supongo que fue Craig Kolb) lanzó el generador de perfiles y comenzó a resolverlo.

Resultó que la mayor parte del tiempo de análisis estaba ocupado por búsquedas en la tabla hash utilizada para el internamiento de cadenas . La tabla hash era bastante pequeña, probablemente 256 elementos, y cuando varios valores se agruparon en una celda, organizó una cadena. Después de la primera implementación de la tabla hash, pasó mucho tiempo y ahora había decenas de miles de objetos en las escenas, por lo que una tabla tan pequeña se pobló rápidamente y se volvió ineficaz.

Lo más recomendable era simplemente aumentar el tamaño de la tabla: todo esto sucedió en la altura del flujo de trabajo, por lo que no hubo tiempo para algún tipo de solución elegante, por ejemplo, expandir el tamaño de la tabla al llenarla. Realizamos un cambio en una línea, reconstruimos la aplicación, realizamos una prueba rápida antes de confirmar y ... no se producen mejoras de velocidad. Buscar una tabla hash lleva la misma cantidad de tiempo. Impresionante!

Después de más estudios, descubrimos que la función de tabla hash utilizada era similar a la siguiente:

int hash(const char *str) { return str[0]; } 

(Perdóname, Pixar, si revelé tu código fuente de RenderMan de alto secreto).

La función "hash" se implementó en la década de 1980. En ese momento, el programador probablemente consideró que el costo computacional de verificar el efecto de todos los caracteres en la cadena sobre el valor hash sería demasiado alto y no valía la pena. (Creo que si solo hubiera unos pocos objetos y 256 elementos en la tabla hash en la escena, eso sería suficiente).

Otra implementación obsoleta contribuyó: desde el momento en que Pixar comenzó a crear sus películas, los nombres de los objetos en las escenas han crecido bastante, por ejemplo, "BuzzLightyear / LeftArm / Hand / IndexFinger / Knuckle2". Sin embargo, alguna etapa inicial de la tubería usó un búfer de longitud fija para almacenar los nombres de los objetos y acortó todos los nombres largos, conservando solo el final, y con suerte, agregó una elipsis al principio, dejando en claro que parte del nombre se perdió: "... año / LeftArm / Hand / IndexFinger / Knuckle2 ".

Posteriormente, todos los nombres de los objetos que vio el renderizador tenían esta forma, la función hash los ha acumulado en una sola pieza de memoria como ".", Y la tabla hash era en realidad una gran lista vinculada. Buenos viejos tiempos Al menos, habiéndolo descubierto, arreglamos rápidamente este error.

Innovación intrigante


Me recordaron esta lección el año pasado cuando Heather Pritchet y Rasmus Tamstorf de WDAS se pusieron en contacto conmigo y me preguntaron si estaría interesado en verificar la posible calidad de la representación de la escena de Moana en pbrt 1 . Naturalmente, estuve de acuerdo. Estaba feliz de ayudar y me preguntaba cómo resultará todo.

El ingenuo optimista dentro de mí esperaba que no hubiera grandes sorpresas: al final, la primera versión de pbrt se lanzó hace unos 15 años, y muchas personas usaron y estudiaron su código durante muchos años. Puede estar seguro de que no habrá interferencias como la antigua función hash de RenderMan, ¿verdad?

Por supuesto, la respuesta fue no. (Y es por eso que estoy escribiendo esto y algunas otras publicaciones). Aunque estaba un poco decepcionado de que pbrt no fuera perfecto "fuera de la caja", pero creo que mi experiencia con la escena de Moana fue la primera confirmación del valor de publicar esta escena ; pbrt ya se ha convertido en un mejor sistema debido al hecho de que descubrí cómo manejar esta escena.

Primeras representaciones


Una vez que accedí a la escena, la descargué de inmediato (me tomó varias horas con la conexión a Internet de mi casa) y la descomprimí de tar, recibiendo 29 GB de archivos pbrt y 38 GB de mapas de textura ptex 2 . Intenté alegremente representar la escena en mi sistema doméstico (con 16 GB de RAM y una CPU de 4 núcleos). Después de regresar a la computadora después de un tiempo, vi que estaba congelada, toda la RAM estaba llena y pbrt todavía estaba tratando de completar el análisis de la descripción de la escena. El sistema operativo buscó hacer frente a la tarea utilizando memoria virtual, pero parecía inútil. Después de haber superado el proceso, tuve que esperar otro minuto antes de que el sistema comenzara a responder a mis acciones.

El siguiente intento fue una instancia de Google Compute Engine, que le permite usar más RAM (120 GB) y más CPU (32 hilos en 16 CPU). La buena noticia es que pbrt pudo renderizar con éxito la escena (gracias al trabajo de Heather y Rasmus para convertirla al formato pbrt). Fue muy emocionante ver que pbrt puede generar píxeles relativamente buenos para contenido de película de alta calidad, pero la velocidad resultó no ser tan sorprendente: 34 min 58 s solo para analizar la descripción de la escena, y durante el procesamiento del sistema gastó hasta 70 GB de RAM.

Sí, había 29 gigabytes de archivos de descripción de escena en formato pbrt en el disco que debían ser dispersados, por lo que no esperaba que la primera etapa tomara un par de segundos. ¿Pero pasar media hora incluso antes de que los rayos comiencen a trazar? Esto complica enormemente el trabajo con la escena.

Por otro lado, esta velocidad nos dijo que probablemente algo muy maloliente estaba sucediendo en el código; no solo "la inversión matricial se puede realizar un 10% más rápido"; más bien, algo al nivel de "oh, estamos revisando una lista vinculada de 100 mil elementos". Era optimista y esperaba que, una vez que lo descubriera, pudiera acelerar significativamente el proceso.

Las estadísticas no ayudan


El primer lugar donde comencé a buscar pistas fue las estadísticas de volcado de pbrt después de la representación. Las etapas principales de la ejecución de pbrt se configuran para que pueda recopilar datos de perfil aproximados mediante la reparación de operaciones con interrupciones periódicas durante el proceso de representación. Desafortunadamente, las estadísticas no nos ayudaron mucho: según los informes, desde casi 35 minutos antes del inicio del renderizado, se emplearon 4 minutos y 22 segundos para construir el BVH, pero no se proporcionaron detalles sobre el resto del tiempo.

Construir BVH es la única tarea computacional significativa realizada durante el análisis de escenas; todo lo demás es esencialmente una deserialización de la geometría y las descripciones de los materiales. Saber cuánto tiempo se dedicó a crear el BVH dio una idea de cuán (in) efectivo fue el sistema: el tiempo restante, es decir, aproximadamente 30 minutos, analizó 29 GB de datos, es decir, la velocidad fue de 16.5 MB / s. Los analizadores JSON bien optimizados, que esencialmente realizan la misma tarea, funcionan a una velocidad de 50-200 MB / s. Claramente, todavía hay margen de mejora.

Para comprender mejor qué tiempo está perdiendo, lancé pbrt con una herramienta de perf de Linux que nunca había usado antes. Pero, al parecer, hizo frente a la tarea. Le --call-graph dwarf caracteres DWARF para obtener nombres de funciones ( --call-graph dwarf ), y para no obtener archivos de rastreo de 100 GB, tuve que reducir la frecuencia de muestreo de 4000 a 100 muestras por segundo ( -F 100 ). Pero con estos parámetros todo salió bien, y me sorprendió gratamente que la herramienta de perf report tenga una interfaz con buenas maldiciones.

Esto es lo que podría decirme después de comenzar con pbrt:


No bromeaba cuando hablaba de la "interfaz con buenas maldiciones".

Vemos que se dedica más de la mitad del tiempo a la mecánica de análisis: yyparse() es el yyparse() generado por bison , y yylex() es el analizador léxico (lexer) generado por flex . Más de la mitad del tiempo en yylex() se gasta en strtod() , que convierte cadenas en valores dobles. yyparse() ataque en yyparse() y yylex() hasta el tercer artículo de esta serie, pero ahora ya podemos entender que podría ser una buena idea reducir la cantidad de datos arrojados al renderizador.

De texto a PLY


Una forma de pasar menos tiempo analizando datos de texto es convertir los datos a un formato que se analice de manera más eficiente. La mayoría de los 29 GB de estos archivos de descripción de escena son mallas triangulares, y pbrt ya tiene soporte nativo para el formato PLY , que es una representación binaria efectiva de mallas poligonales. También en pbrt hay un indicador de línea de comando --toply , que analiza el archivo de descripción de escena pbrt, convierte todas las mallas triangulares encontradas en archivos PLY y crea un nuevo archivo pbrt que se refiere a estos archivos PLY.

El problema es que las texturas ptex se usan activamente en la escena de Disney, lo que, a su vez, requiere que se faceIndex un valor faceIndex con cada triángulo, lo que determina de qué cara de la faceIndex original se toma. Para transferir estos valores, fue suficiente simplemente agregar soporte para nuevos campos en el archivo PLY . La investigación adicional reveló que en el caso de convertir cada malla, incluso si tiene solo una docena de triángulos, en un archivo PLY, se crean decenas de miles de pequeños archivos PLY en la carpeta, y esto crea sus propios problemas de rendimiento; Logramos deshacernos de este problema agregando a la implementación la capacidad de dejar pequeñas mallas sin cambios .

Escribí un pequeño script de línea de comando para convertir todos los archivos *_geometry.pbrt en una carpeta para usar PLY para mallas grandes. Tenga en cuenta que tiene suposiciones codificadas sobre las rutas que deben cambiarse para que el script funcione en otro lugar.

Primer impulso de velocidad


Después de convertir todas las mallas grandes a PLY, el tamaño de la descripción de la escena en el disco disminuyó de 29 a 22 GB: 16.9 GB de archivos de escena pbrt y 5.1 GB de archivos binarios PLY. Después de la conversión, el tiempo total de la primera etapa del sistema disminuyó a 27 minutos y 35 segundos, y los ahorros ascendieron a 7 minutos y 23 segundos, es decir, aceleramos 1.3 veces 3 . Procesar un archivo PLY es mucho más eficiente que procesar un archivo de texto pbrt: solo pasaron 40 segundos de tiempo de inicio analizando archivos PLY, y vemos que los archivos PLY se procesaron a una velocidad de aproximadamente 130 MB / s, o aproximadamente 8 veces más rápido que el formato de texto pbrt .

Fue una buena victoria fácil, pero todavía teníamos mucho que hacer.

La próxima vez, descubriremos dónde se usa realmente toda la memoria, solucionaremos algunos errores aquí y lograremos aún más velocidad en el proceso.

Notas


  1. Ahora debería tener una mejor comprensión de la motivación para agregar soporte ptex de mi parte y convertir Disney BSDF a pbrt el año pasado.
  2. Todo el tiempo aquí y en publicaciones posteriores está indicado para la versión WIP (Work In Progress), con la que trabajé antes del lanzamiento oficial. Parece que la versión final es un poco más grande. Nos atendremos a los resultados que grabé cuando trabajé con la escena original, a pesar de que no se corresponden exactamente con los resultados de la versión final. Sospecho que las lecciones de ellos pueden ser las mismas.
  3. Tenga en cuenta que el aumento de la velocidad es esencialmente lo que esperaría con una reducción de aproximadamente el 50 por ciento en el volumen de análisis de datos. La cantidad de tiempo que pasamos según el generador de perfiles confirma nuestra idea.

Source: https://habr.com/ru/post/es417407/


All Articles