1/1 Alumne: José Manuel Solís Rejas Director/Ponent
Transcripción
1/1 Alumne: José Manuel Solís Rejas Director/Ponent
Títol: Extensión a Direct3D del driver de un simulador de GPU Volum: 1/1 Alumne: José Manuel Solís Rejas Director/Ponent: Agustín Fernández Jiménez Departament: Arquitectura de Computadors Data: 27 de Junio del 2007 DADES DEL PROJECTE Títol del Projecte: Extensión a Direct3D del driver de un simulador de GPU Nom de l'estudiant: José Manuel Solís Rejas Titulació: Enginyeria Informàtica Crèdits: 37’5 Director/Ponent: Agustín Fernández Jiménez Departament: Arquitectura de Computadors MEMBRES DEL TRIBUNAL (nom i signatura) President: Leandro Navarro Moldes Vocal: Juan Trias Pairo Secretari: Agustín Fernández Jiménez QUALIFICACIÓ Qualificació numèrica: Qualificació descriptiva: Data: 3 Tabla de contenidos 1. Presentación.............................................................................................................................. 7 2. Conceptos previos................................................................................................................... 11 2.1 Introducción al rendering interactivo................................................................................................13 2.1.1 Definición de rendering. ............................................................................................................13 2.1.2 La ecuación general del rendering.............................................................................................13 2.1.3 Modelos de iluminación. ...........................................................................................................15 Modelos de iluminación globales. ..................................................................................................15 Modelos de iluminación locales .....................................................................................................16 Modelo de iluminación de Phong ...............................................................................................17 Modelo de iluminación de Blinn – Phong. .................................................................................19 2.1.4 Rendering interactivo y rendering no interactivo. .....................................................................19 2.1.5Modelado de escenas ..................................................................................................................20 2.1.6 Sistemas de coordenadas y transformaciones geométricas........................................................20 Cambio de coordenadas directo......................................................................................................21 Cambio de coordenadas inverso .....................................................................................................24 Coordenadas homogéneas ..............................................................................................................25 2.1.7 Utilización del rendering en aplicaciones: API’s 3D.................................................................27 2.1.8 Soporte hardware al rendering interactivo: Graphics Processing Units.....................................28 Concepto de batch ..........................................................................................................................29 2.2 Direct3D ...........................................................................................................................................31 2.2.1 Direct3D Rendering pipeline.....................................................................................................31 Fixed Function Pipeline y Programmable Pipeline ........................................................................35 Fixed function pipeline...................................................................................................................35 Streaming....................................................................................................................................35 Fixed Function Vertex Shading ..................................................................................................38 Transformación.......................................................................................................................38 Transformación de modelo .................................................................................................38 Transformación de observador. ..........................................................................................40 Transformación de proyección. ..........................................................................................40 Iluminación.............................................................................................................................43 Geometry Processing..................................................................................................................45 Texture sampling ........................................................................................................................48 Fixed function pixel shading ......................................................................................................51 Pixel processing..........................................................................................................................54 Programmable pipeline...................................................................................................................58 Lenguaje de shading ...................................................................................................................59 Registros.................................................................................................................................60 Instrucciones...........................................................................................................................61 Modificadores.........................................................................................................................64 High Level Shading Language ...............................................................................................65 2.2.2 Direct3D API.............................................................................................................................66 Modelo COM .................................................................................................................................66 Interfaces Direct3D ........................................................................................................................67 Una aplicación de ejemplo .............................................................................................................70 2.3 El simulador ATTILA ......................................................................................................................75 2.3.1 El pipeline del simulador...........................................................................................................75 Streaming........................................................................................................................................76 Vertex Shading ...............................................................................................................................76 Geometry Processing......................................................................................................................77 Fragment Shading...........................................................................................................................78 Pixel Rendering ..............................................................................................................................78 Texture Sampling ...........................................................................................................................79 Lenguaje de shading .......................................................................................................................79 2.3.2 El hardware simulado ................................................................................................................81 5 2.3.3 La interfaz del simulador...........................................................................................................82 2.3.4 Uso del simulador......................................................................................................................83 3. Trabajo realizado..................................................................................................................... 85 3.1 Captura, reproducción y análisis de trazas........................................................................................87 3.1.1 Subsistema de captura: Aplicación PIX.....................................................................................88 Aplicación PIX ...............................................................................................................................88 Examen del formato de traza ..........................................................................................................89 Localización de las llamadas al API Direct3D ...............................................................................91 Interpretación de una llamada: Packed Call Package .....................................................................91 Llamadas Lock/Unlock ..................................................................................................................92 3.1.2 Subsistema de reproducción: D3DPixRunPlayer ......................................................................93 Tratamiento de llamadas.................................................................................................................94 Objetos originales y sustitutos........................................................................................................96 Tratamiento de las operaciones lock/unlock...................................................................................97 Reproducción a nivel de batch........................................................................................................98 Reproducción con configuraciones alternativas .............................................................................99 Diseño de PixRunPlayer...............................................................................................................100 Pruebas de uso ..............................................................................................................................101 3.1.3 Subsistema de análisis de trazas ..............................................................................................102 Informes mediante PERL .............................................................................................................103 3.2 Implementación de Direct3D sobre el simulador ATTILA............................................................105 3.2.1 Método de trabajo....................................................................................................................107 3.2.2 Arquitectura.............................................................................................................................108 3.2.3 Subsistema D3DProgrammablePipeline..................................................................................109 Comunicación con el simulador ...................................................................................................110 Interfaz con el exterior..................................................................................................................111 Patrón singleton ........................................................................................................................112 Relación entre el estado Direct3D y el estado del simulador. ......................................................112 Recursos Direct3D y búferes del simulador .................................................................................113 Depuración interactiva..................................................................................................................115 3.2.4 Subsistema D3DShaderTranslation .........................................................................................117 Proceso de traducción...................................................................................................................118 Tratamiento de versiones..............................................................................................................120 Estudio de requisitos.....................................................................................................................121 Análisis del bytecode....................................................................................................................123 Representación intermedia ...........................................................................................................125 Diseño del traductor .....................................................................................................................126 Pruebas de uso ..............................................................................................................................127 3.2.5 Subsistema FixedFunctionGeneration .....................................................................................128 Generación de shaders de función fija..........................................................................................130 Emulador de función fija ..............................................................................................................135 Pruebas interactivas ......................................................................................................................138 4. Conclusión............................................................................................................................. 139 Análisis del tiempo empleado y coste económico del proyecto. ..........................................................141 Futuras líneas de trabajo.......................................................................................................................141 Bibliografía...........................................................................................................................................143 6 1. Presentación El crecimiento de la industria del videojuego ha impulsado la investigación y desarrollo de arquitecturas hardware especializadas en representar gráficos tridimensionales en tiempo real. Esto ha posibilitado que la CPU tradicional delegue el costoso proceso de los gráficos en un procesador especializado, la Graphics Processing Unit (GPU). Podemos encontrarlas en equipos de escritorio, portátiles, consolas e incluso en dispositivos móviles. En el Departamento de Arquitectura de Computadores existe una línea de investigación dedicada al estudio de arquitecturas gráficas. La línea dispone del simulador de GPU ATTILA. El simulador es altamente configurable, permite simular a nivel de micro arquitectura y tomar multitud de estadísticas. Capture Verify Simulate Application GLInterceptor Trace GLPlayer GLPlayer Trace stats OpenGL Driver OpenGL Driver ATTILA OpenGL Driver API stats Real GPU Real GPU ATTILA Simulated GPU GPU stats Rendered Frames Rendered Frames Rendered Frames Es esencial estudiar las diferentes configuraciones del simulador en condiciones similares a las de una GPU física. Por este motivo las operaciones que forman la entrada de la simulación se obtienen de aplicaciones reales, típicamente videojuegos. Como es habitual, las aplicaciones no operan sobre el hardware gráfico directamente sino a través de librerías que les abstraen de los detalles de bajo nivel, conocidas como API’s 3D. El procedimiento que se utiliza consiste en monitorizar en tiempo de 7 ejecución las operaciones que invoca el videojuego sobre el API, serializándolas en un fichero. A este proceso lo llamamos captura y al fichero resultante la traza, que será la entrada para el simulador. Durante la simulación las operaciones almacenadas en la traza se deserializan y se ejecutan de nuevo. Son procesadas por una implementación del API que las traduce en operaciones sencillas sobre el hardware simulado, como escrituras de registros y memoria. Actualmente el simulador ATTILA dispone de una implementación del API OpenGL. Esto significa que puede simular trazas obtenidas a partir de aplicaciones que utilicen este API. OpenGL se vincula a ámbitos científicos y académicos y muchas aplicaciones desarrolladas en dichos ámbitos lo utilizan, sin embargo la mayoría de videojuegos disponibles para PC’s utilizan el API Direct3D de Microsoft. Disponer de una implementación de este API supondría poder simular un gran número de videojuegos. Capture Verify Simulate Application Direct3D Capture Trace Direct3D Player Direct3D Player Trace stats Direct3D Direct3D ATTILA Direct3D driver API stats Real GPU Real GPU ATTILA Simulated GPU GPU stats Rendered Frames Rendered Frames Rendered Frames El objetivo principal de este proyecto es dar soporte a la simulación de videojuegos Direct3D en el simulador ATTILA. Esto supone realizar una implementación del API Direct3D y también proveer de un mecanismo para capturar y reproducir las trazas. Alrededor del simulador ATTILA se han desarrollado varios proyectos, de forma que es importante delimitar en qué medida este proyecto se relaciona con ellos. En concreto: 8 La aplicación DXCodegen, un framework para la generación automática de código se ha utilizado en la implementación de algunos componentes. Esta aplicación la ha desarrollado el alumno de la FIB David Abella como parte de su PFC, cuya temática también está relacionada con Direct3D y ATTILA. Parte del código utilizado en la extensión de estadísticas del reproductor de trazas forma parte del repositorio de la línea de investigación en arquitecturas gráficas del DAC y fue programada por uno de sus miembros, Jordi Roca. El propio simulador de GPU ha sido desarrollado por el profesor Víctor Moya como parte de su tesis doctoral. El resto de la memoria consiste en una exposición de conceptos previos, la descripción del trabajo realizado y el capítulo de conclusiones. Está pensada para leerse en orden. 9 2. Conceptos previos 11 2.1 Introducción al rendering interactivo. 2.1.1 Definición de rendering. En el campo de los gráficos por computador se conoce como rendering al proceso de representación del modelo de una escena tridimensional (3D) como imagen raster bidimensional (2D). El modelo se recibe como una estructura de datos que almacena las características de la escena. En el caso de una escena del mundo real, podría almacenar datos como la forma de los objetos y la intensidad de las fuentes de luz. Las imágenes raster por su lado consisten en una matriz de píxeles. En el proceso de rendering cada uno de ellos almacena la intensidad de la luz para cada posición 2D representada. 2.1.2 La ecuación general del rendering. La base teórica del rendering se formula en la ecuación general del rendering de James T. Kajiya. La ecuación general describe, basándose en la física de la luz y en la conservación de la energía, cómo la luz viaja a través de cualquier punto de la superficie de los objetos de una escena. 13 Es la cantidad de luz que sale del punto x en dirección w. Si este punto fuera visible ésta sería la luz que recibiríamos de él. Esta luz se descompone en una suma de luz emitida y reflejada. Es la cantidad de luz emitida por el punto x en dirección w. Si este término no es cero significa que el punto está actuando como una fuente de luz. Representa la luz reflejada, que se obtiene integrando sobre todas las direcciones posibles w’ en que se puede recibir luz entrante. Es la luz entrante al punto x desde la dirección w’. Hay dos factores que influyen en la cantidad de luz que finalmente se reflejará en la dirección w. Es la función de distribución define la cantidad de luz que el punto x refleja en dirección w cuando la recibe desde la dirección w’. Es la atenuación por el ángulo en que incide la luz entrante sobre la superficie. De su expresión se deduce que la atenuación es menor cuanto más perpendicularmente incida la luz. 14 Computando esta ecuación es posible obtener una representación perfecta de todos los objetos visibles en una escena, obteniendo la cantidad de luz que llega desde cada punto de sus superficies hacia la posición desde la que se quiere observar. Sin embargo el coste computacional lo hace impracticable para escenas con un mínimo de complejidad, ya que es una ecuación recursiva: Para calcular el valor para un punto x hemos de calcular la cantidad de luz que entra proveniente de todas las demás direcciones, lo cual implica calcular de nuevo la ecuación para todos los puntos visibles desde x. Dado que el coste temporal de la solución es exponencial, ninguna técnica de rendering realiza un cómputo completo de la ecuación general. Cada técnica adopta un compromiso entre fidelidad de la representación y coste temporal. 2.1.3 Modelos de iluminación. Modelos de iluminación globales. Los modelos de iluminación globales tienen en cuenta tanto la luz que refleja una superficie proveniente directamente de una fuente de iluminación como la que recibe indirectamente reflejada por el resto de objetos. Su coste computacional es elevado pero practicable, ya que establecen un límite en el número de interacciones de la luz con los objetos. El grado de realismo que se obtiene es muy elevado, se suelen utilizar en técnicas de rendering no interactivas. 15 Modelos de iluminación locales Los modelos de iluminación locales tan sólo tienen en cuenta la luz que recibe una objeto directamente desde las fuentes de luz. De este modo no es necesario calcular la contribución de la luz que se recibe de forma indirecta en una superficie, reflejada por el resto de objetos de la escena. Esta simplificación implica un menor coste computacional, ya que eliminan el componente recursivo de la ecuación general del rendering. Cada punto es independiente de los demás. Esto hace que de los modelos de iluminación locales se utilicen para las técnicas de rendering interactivo, como se verá más adelante. 16 Modelo de iluminación de Phong El modelo de reflexión de Phong establece que la intensidad de la luz que recibe un observador (viewer) reflejado desde un punto de una superficie se puede dividir en tres componentes: Ambiente, especular y difuso. La intensidad de luz ambiente Ia es una propiedad de la escena, y representa la luz proveniente indirectamente del resto de superficies. Es una representación muy simplificada respecto a la ecuación general: El modelo asume un valor constante que reciben todas las superficies. La proporción en que esta luz es reflejada por la superficie se representa por el parámetro Ka, que es una propiedad de la superficie. 17 Para cada una de las luces presentes en la escena se considera que emite dos tipos de luz: Una que se refleja en forma difusa Id y otra que se refleja en forma especular Is. La reflexión difusa es aquella en la que la luz se distribuye en todas la direcciones por igual. Se expresa como el producto escalar de N y L. El componente difuso proviene de la aplicación del modelo de reflexión de Lambert. La reflexión especular es aquella que se da preferentemente en la dirección de reflexión R. La proporción que recibe el observador depende de la distancia angular entre el vector V y R, siendo máxima cuando coinciden. El exponente alfa representa una función de distribución de la luz alrededor de R. Cuanto mayor es alfa, más concentrada está luz en la dirección R. 18 Modelo de iluminación de Blinn – Phong. El modelo de iluminación de Blinn – Phong añade una simplificación al modelo de Phong de cara a un menor coste computacional. La modificación consiste en utilizar el half vector H en sustitución del vector reflejado R. Ajustando debidamente el exponente los resultados son similares al modelo de Phong. 2.1.4 Rendering interactivo y rendering no interactivo. Las técnicas de rendering interactivas pretenden representar escenas de forma lo suficientemente rápida para permitir la interacción por parte de una persona. Esto supone producir del orden de decenas de imágenes (frames) por segundo, medida que se conoce como framerate. Por este motivo las técnicas de rendering interactivas suelen utilizar modelos de iluminación local y algoritmos deliberadamente sencillos. Por el contrario las técnicas no interactivas suelen estar enfocadas a la calidad de la imagen final. Su coste computacional es elevado, pues suelen utilizar modelos de iluminación global. Ejemplos de estas técnicas son el raytracing, que asume que las superficies se comportan de modo especular puro, reflejando la luz en una sola 19 dirección y recibiéndola de un número limitado de direcciones y la técnica de Radiosity, que asume que las superficies presentan tan sólo reflexión difusa. 2.1.5Modelado de escenas Modelo Vértices y primitivas Textura En muchos sistemas de rendering el modelo de un objeto se da a través de la descripción de su superficie. Esta descripción se compone de vértices, primitivas y texturas: Un vértice en el contexto del rendering es una posición de una superficie para la que se conocen los valores exactos de algunas propiedades. Estas propiedades se llaman las componentes del vértice. Adicionalmente a la posición los vértices pueden incluir otros componentes como el color de la superficie y el vector normal, que se indica en la ilustración. El color usualmente se describe como una serie de valores de intensidad luminosa para de rojo, verde y azul. Para indicar la transparencia se utiliza un cuarto valor, conocido como canal alfa. Una primitiva es una región de la superficie definida mediante sus vértices. La primitiva más utilizada es el triángulo, porque presenta la ventaja de que sus tres vértices siempre definen un plano. Una textura es una matriz que almacena los valores de una propiedad de la superficie en los puntos interiores de las primitivas. Es una manera de obtener un mayor nivel de detalle. 2.1.6 Sistemas de coordenadas y transformaciones geométricas. La posición de un punto en el espacio 3D se puede determinar utilizando sus coordenadas respecto a un sistema de referencia. 20 Cambio de coordenadas directo a P1 a P1 Un cambio de coordenadas directo consiste en encontrar a partir de las coordenadas de un punto P1 en un sistema de referencia A un nuevo punto P2 que tenga las mismas coordenadas en otro sistema de referencia B. a P1 Un caso sencillo sería que el sistema B tuviera la misma orientación que A. En este caso bastaría con sumar el vector de la posición respecto a A del punto de origen del sistema B. 21 Bw Bv Bw Bv Cuando los sistemas tienen orientaciones diferentes se utiliza la matriz de cambio de coordenadas Mab, que consiste en expresar los vectores del sistema de referencia B en el sistema de referencia A. Bv Bu P2 P 3a Bw P3 Bu SR A Bv Bw Ao P3a = MBA * P1a Multiplicando el vector de coordenadas de en A del punto P1 por la matriz Mab se obtienen las coordenadas de un punto P3 respecto a un sistema de referencia centrado en el origen de A con misma orientación que B. 22 P2 AB P2 a P3 SR A Ao P2a = MBA * P1a + AB De este modo ahora podemos proceder como en el caso anterior, ya que P3 y P2 están expresados en sistemas de referencia con la misma orientación. Existen diferentes matrices de cambio de coordenadas para algunos casos básicos como la rotación sobre un eje, translación, escalado, etc. La utilidad de esta operación es encontrar la posición que ocupa un punto de la geometría de un objeto cuando éste se traslada a otro lugar. 23 Cambio de coordenadas inverso Un mismo punto puede definirse utilizando diferentes sistemas de referencia. Un cambio de coordenadas inverso consiste en encontrar las coordenadas de un punto en un sistema de referencia B a partir de sus coordenadas en otro sistema de referencia A. P Pb Pa Bo AB SR B Ao SR A Pb = Pa - AB En el caso sencillo en que A y B tengan la misma orientación bastará con restar el vector de la posición en A del origen del sistema de referencia B. 24 Si no tienen la misma orientación previamente se multiplica por la matriz de cambio de coordenadas MAB. Es importante observar que MAB es la inversa de MBA. La utilidad de esta operación consiste en que nos permite describir fácilmente la geometría del objeto desde el sistema de coordenadas nuevo, viéndolo desde otro lugar a modo de una cámara. Coordenadas homogéneas A pesar de que las escenas que se representan son 3D, lo habitual es utilizar un espacio proyectivo 3D. En este tipo de espacio los puntos tienen coordenadas 4D, llamadas coordenadas homogéneas, y se proyectan en puntos 3D. Un punto de coordenadas homogéneas (x, y, z, 1) se proyecta en el punto 3D (x, y, z) y en general dividiendo por la componente w obtenemos el punto 3D proyectado, por lo que los puntos de la forma (aw, bw, cw, w) son equivalentes. 25 Rotación y translación 3D: P2 = R * P1 + T P2 w R = 0 0 T 0 1 * P1 1 Rotación y translación 4D: P2 = M * P1 La ventaja de utilizar este sistema es que se puede representar un cambio de coordenadas mediante una única matriz 4x4. Se añade al punto 3D a transformar una cuarta componente con valor 1 y se multiplica por la matriz 4x4. Para recuperar el punto 3D transformado se divide por la cuarta componente w si es diferente de 1 y se descarta. Varias transformaciones 4D P4 = M4 * P2 P3 = M3 * P2 P2 = M2 * P1 Sustituyendo P4 = M4 * M3 * M2 * P1 Combinando las matrices M432 = M4 * M3 * M2 P4 = M432 * P1 Otra ventaja es que una serie de cambios de coordenadas se expresan como una serie de multiplicaciones de matrices, pudiendo combinarse en una única matriz que multiplicaremos por las coordenadas homogéneas del punto. 26 Los puntos con w = 0 no tienen proyección en el espacio 3D se sitúan en el plano del infinito del espacio 4D. Utilizando estos puntos se pueden representar conceptos como el de eje de rotación. 2.1.7 Utilización del rendering en aplicaciones: API’s 3D Las API’s 3D ofrecen funcionalidades de rendering ocultando los detalles de implementación. Esto supone una gran ventaja a la hora de crear aplicaciones que realicen rendering, ya que realizar una implementación de los procesos de rendering suele ser un tarea compleja. Otro beneficio es que favorece la portabilidad de las aplicaciones. El API OpenGL es una especificación para un sistema de rendering interactivo inicialmente propuesta por Silicon Graphics Inc. Existen implementaciones de este API en multitud de lenguajes y plataformas. Se utiliza en todo tipo de aplicaciones, pero especialmente en aplicaciones de CAD, visualización científica y realidad virtual. Es popular en ámbitos académicos. Direct3D es la API para rendering interactivo de la empresa Microsoft. Su primera versión se incorporó en el sistema operativo Windows 95 para posibilitar la programación de videojuegos en esta plataforma. Es un API orientada a ofrecer acceso a las capacidades del hardware gráfico, por lo que ha evolucionado en paralelo a través de sus diferentes versiones al desarrollo de éste. Está disponible en la mayoría de equipos que disponen de sistemas operativos Windows. Se utiliza fundamentalmente para la programación de videojuegos. Las API’s suelen especificar un proceso que consta de varios bloques funcionales conocido como rendering pipeline. Los datos del modelo fluyen de un bloque a otro sufriendo diferentes transformaciones hasta formar la imagen renderizada. 27 2.1.8 Soporte hardware al rendering interactivo: Graphics Processing Units Los equipos actuales disponen de procesadores específicos para el procesamiento de gráficos conocidos como Graphics Processing Units (GPU’s). De este modo la CPU suele delegar total o parcialmente las tareas de rendering interactivo en la GPU. La motivación del desarrollo de hardware específico para el procesamiento de gráficos es doble. Por un lado tenemos que muchas aplicaciones de usuario, especialmente videojuegos y aplicaciones de diseño gráfico, requieren rendering interactivo con la mayor calidad posible. Por otro lado tenemos que una CPU es un procesador de propósito general y el procesamiento de gráficos tiene características específicas. Supone procesar un gran número de elementos de un mismo tipo, sin embargo no hay dependencias entre elementos: El proceso de un elemento es independiente del procesamiento de los demás. Tomando el caso del modelo de iluminación local, observamos que la iluminación de un punto no depende de la iluminación de los demás. El proceso que a cada elemento es sencillo. Los algoritmos que se usan utilizan en rendering interactivo suelen ser simples y poco costosos. 28 Los elementos suelen ser magnitudes vectoriales reales. Tanto la posición como el color y otros elementos geométricos se representan cómodamente mediante vectores. Los algoritmos utilizan operaciones vectoriales como suma, multiplicación, producto escalar. En consecuencia la GPU se diseña como un Stream Processor. Estos procesadores están especializados en procesar gran cantidad de elementos en paralelo, distribuyéndolos en pequeñas unidades de proceso. Las unidades disponen de operaciones para tratar con vectores de forma eficiente y algunas son programables. Concepto de batch La GPU no se suele utilizar para dibujar la escena de golpe, sino que se dibuja un grupo de primitivas cada vez. Esta unidad de trabajo se llama batch. Utilicemos un ejemplo. Las primitivas de este personaje tienen diferentes materiales (texturas y propiedades ópticas) y la GPU se ha de configurar de diferente forma para cada uno. El modo habitual es establecer una configuración de la GPU para un material y renderizar todas las primitivas que tienen ese material y proceder así con el resto. De esta forma se usarían cuatro batches para renderizar el personaje. Si no se renderizan todas las primitivas de un material juntas habrá que reconfigurar la GPU más veces, esto supone una penalización de rendimiento doble. Por un lado supone más llamadas al API 3D. Por otro lado y más importante: la GPU está diseñada para procesar batches, es capaz de procesar un gran número de primitivas 29 en paralelo si no cambian los parámetros de rendering. La razón es que, en esas condiciones, el cálculo de cada píxel de la imagen renderizada es totalmente independiente del cálculo de los demás. 30 2.2 Direct3D 2.2.1 Direct3D Rendering pipeline. Vertex Data Streaming Assembled Vertexes Vertex Shading Programmable Vertex Shading Primitive Data Fixed Function Vertex Shading Shaded Vertexes Geometry Processing Texture Data Interpolated Pixels Pixel Shading Texture Sampling Programmable Pixel Shading Fixed Function Pixel Shading Shaded Pixels Pixel Processing Rendered Pixels Rendered Image Direct3D define un proceso en diferentes etapas, o rendering pipeline. Como entrada a dicho proceso tenemos los datos del modelo de la escena: Vértices, Primitivas y Texturas. Como resultado obtenemos la imagen renderizada o frame. 31 La etapa de Streaming se encarga de extraer los vértices del batch que se va a dibujar a partir de las estructuras de datos que almacenan la geometría de la escena. La etapa de Vertex Shading realiza modificaciones en las componentes de los vértices. Existen dos variantes excluyentes: El Fixed Function Vertex Shading consiste en una serie de cálculos predefinidos. Calcula una nueva posición para los vértices que corresponde a su proyección en la imagen 2D final y además modifica las componentes de color del vértice, simulando que éste está iluminado utilizando un modelo de iluminación local. 32 El Programmable Vertex Shading es más flexible, permitiendo a la aplicación especificar el procedimiento a aplicar al vértice expresándolo mediante un código ensamblador. Este pequeño programa se llama Vertex Shader. La etapa de Geometry Processing se encarga de las primitivas. A partir de los vértices de sus extremos genera píxeles que representan los puntos interiores de la superficie. Estos píxeles también tienen componentes, como los vértices, pero sus valores se obtienen por interpolación a partir de los vértices de la primitiva. El Pixel Shading consiste en calcular la componente final de color para los píxeles. Nuevamente tenemos dos versiones: 33 Texture Interpolated Pixels Fixed Function Pixel Shading Shaded Pixels El Fixed Function Pixel Shading utiliza una secuencia de cálculos con los valores de color del píxel, típicamente mezclándolos con colores obtenidos de las texturas. El Programmable Pixel Shading permite calcular el valor del píxel mediante código, de forma que hablamos también de un Pixel Shader. La etapa de Texture Sampling recupera valores de las Texturas a petición de la etapa de Pixel Shading. Ésta le indica que posición de la textura se requiere y le devuelve el valor, aplicando mecanismos de filtrado de imagen. En el Pixel Processing los píxeles, ya con su valor de color calculado, se someten a una serie de pruebas que determinarán si finalmente aparecerán en la imagen renderizada o no. Por ejemplo los píxeles de la pata de la mesa que queda oculta han 34 sido calculados como todos los otros, sin embargo no aparecerán en la imagen final siendo descartados en favor de los más cercanos. Fixed Function Pipeline y Programmable Pipeline Como hemos visto las etapas de Vertex y Pixel Shading disponen de dos variantes cada una. Se habla de Fixed Function Pipeline para referirse al Pipeline cuando se utilizan las versiones fijas. En contraposición el Programmable Pipeline es el nombre que se utiliza cuando se usan shaders. El Fixed Function Pipeline no se utiliza demasiado hoy en día, su origen se encuentra en épocas en que el hardware gráfico no era programable. Fixed function pipeline Streaming Una vista en detalle de la etapa de Streaming nos permitirá entender cómo la aplicación provee los vértices del modelo y cómo esta etapa es capaz de recuperar los que se van a renderizar en el batch. Los Vertex Buffers son objetos que almacenan una serie de Vértices. Son la manera preferente de proveer de vértices al pipeline. Para que el pipeline acceda a ellos es necesario asociarlos a uno de los canales, o Streams, disponibles. Usaremos un sencillo ejemplo de una aplicación que dibuja un triángulo. 35 2 0 1 La primera opción que mostramos es utilizar un sólo Vertex Buffer, podemos observar en su contenidos que los Vértices poseen componentes de posición y color. Como segunda opción la aplicación podría utilizar dos Vertex Buffers, uno en el que almacena la posición y otro en que almacena el color. En este caso los vértices tienen sus componentes repartidas. 36 Observando este último ejemplo se entiende el sentido de ensamblar un vértice. La tarea de la etapa de streaming consiste en ir recogiendo, para cada vértice que se va a dibujar, sus componentes repartidas en los vertex buffers y ensamblándolas para formar un Vértice con todas estas componentes. La información sobre cómo está distribuido cada vértice en los diferentes streams se provee en el objeto Vertex Declaration. Simplemente consiste en una lista de declaraciones, una por componente, en que se especifica su tipo y el offset en que se encuentra. Típicamente en los modelos un mismo vértice forma parte de varias primitivas. Para evitar almacenar varias veces el vértice se utiliza un Index Buffer. Éste simplemente contiene una serie de índices que hacen referencia a los vértices. 37 Fixed Function Vertex Shading En esta etapa las componentes del Vértice se someten al proceso conocido como Transform & Lighting. Transformación Transformación de modelo Consideremos una escena en la que hay que ubicar unos objetos. Cada objeto tiene una descripción que dice que se ha de ubicar en una cierta posición y orientación respecto a algún punto de referencia. La trasformación de modelo consiste precisamente en esto, desplazar los vértices del objeto a su posición final en la escena a partir de una matriz de transformación que describe donde se han de colocar. 38 World Transform World Coordinate System (WCS) La posición de un vértice del modelo se recibe como unas coordenadas en un sistema de referencia propio del modelo. Típicamente se querrá situar al modelo en alguna posición y orientación respecta a un sistema de referencia absoluto. Para ello se realizarán diversos cambios de coordenadas directos como translaciones y rotaciones. Como hemos expuesto, estas transformaciones se pueden expresar de forma combinada en una única matriz. Multiplicando la posición del vértice que recibimos del modelo por esta matriz obtendremos su ubicación en la escena. 39 Transformación de observador. Una vez el objeto está situado en la escena1 se tiene en cuenta desde donde se va a observar. Es como situar una cámara en la escena, geométricamente, un sistema de referencia desde el cual describirla. Por tanto la siguiente transformación consiste en cambiar el sistema de coordenadas en que están definidos los vértices al sistema de referencia del observador. Este es un cambio de sistema de referencia inverso, ya que queremos las coordenadas del mismo punto respecto a otro sistema. De hecho, se pueden realizar una serie de cambios de coordenadas inversos hasta alcanzar el sistema de referencia que se desee. Nuevamente estos cambios se expresan de forma combinada en una matriz. Transformación de proyección. Siguiendo con el símil hemos colocado la cámara en la escena, pero al igual que en una fotografía la escena se representa proyectada en un rectángulo, y existen muchas 1 En este apartado hablare de “objeto” aunque propiamente debería hablar del “grupo de primitivas que forman el batch”. 40 formas de realizar esta proyección. La transformación de proyección, más compleja que las anteriores, define cómo se realizará. El volumen de visión es la parte de la escena que puede llegar a aparecer en la representación 2D. En este volumen de visión se sitúa un plano Near y un plano Far que limitan los objetos más cercanos y lejanos que se representarán. La proyección consiste en transformar las coordenadas de los vértices a un nuevo espacio de coordenadas llamado de clipping en el que las coordenadas están normalizadas entre -1 y 1. Según como se defina el volumen de visión tenemos varias proyecciones. 41 Viewing Volume Znear plane (Screen) Clipping Space En la proyección ortográfica la escena se observa desde una distancia infinita, de esta forma se conservan las dimensiones y orientaciones relativas de los objetos. Es útil, por ejemplo, en planos industriales. En la proyección en perspectiva el volumen de visión es un tronco de pirámide, y los objetos cercanos parecen más grandes. 42 Iluminación La iluminación de Direct3D se basa en el modelo de iluminación local de Blinn – Phong introduciendo algunas modificaciones para lograr un resultado más realista. Los cálculos utilizan como entrada diversos componentes del vértice, entre ellos posición, normal, color difuso y especular. Como resultado se computan unas nuevas componentes de color difuso y especular para el vértice, representando la luz difusa y especular que refleja. Las fuentes de iluminación, luces, son más sofisticadas. Además de luz difusa y especular contribuyen a la luz ambiente. Además se incluyen coeficientes de atenuación, que hacen que la intensidad de la luz no sea uniforme en el espacio. Existen varios tipos de luz: La luz ambient es una luz cuyo efecto es uniforme para toda la escena. Tan sólo contribuye en forma de luz ambiente. La luz direccional es una luz para la que no se define una posición, sino tan sólo la dirección en la que se recibe, es decir el vector L. El ejemplo típico de este tipo de luz 43 es el Sol, cuya luz se recibe prácticamente en un mismo ángulo al estar situado a gran distancia. La luz posicional se sitúa en una posición del espacio y emite luz en todas las direcciones, de modo similar a una bombilla. El vector L, por tanto, se calculará por cada vértice a partir de su posición respecto a la luz. El efecto de una luz posicional disminuye con la distancia, simulando la atenuación de la radiación por el medio en que se propaga. Esta atenuación se produce en una curva descendente cuya forma se puede ajustar. Una luz spot se comporta como una luz posicional que emite luz tan sólo en algunas direcciones. Se puede pensar en ella como en un foco. Adicionalmente a los parámetros de una luz posicional su luz se atenúa alrededor de una cierta dirección formando un cono de luz. Cuanto más coincidente sea el vector L con esta dirección mayor será la luz que reciba el vértice. 44 Las constantes superficiales del modelo de iluminación de Blinn-Phong forman parte del material. Éste tiene unos valores globales para la superficie, sin embargo los coeficientes difuso y especular se pueden tomar de las componentes del vértice. Como detalle adicional el material incluye además una constante emisiva, que permite simular superficies fluorescentes. Geometry Processing Una vez que los vértices de la primitiva han pasado por el proceso de shading se tiene en cuenta a la primitiva como tal. Dado que hasta aquí llegan todas las primitivas de la escena, una primera tarea es eliminar las que no vayan a ser visibles, de cara a un menor coste computacional. 45 El test de face culling consiste en descartar las primitivas, que son superficies orientadas, en función de si están de cara al observador o no. Generalmente el test se configura para que las que no miran al observador se descarten. Before frustrum clipping After frustrum clipping Rendered faces El proceso de frustrum clipping consiste en descartar las primitivas que quedan fuera del espacio de coordenadas normalizado. De esta forma las primitivas que continúan son sólo aquellas que estaban dentro del volumen de visión. 46 After Rasterization Screen Space Culling Space Before Rasterization VERTEXES PIXELS La Rasterización es uno de los procesos más importantes de esta etapa. Si antes de ella hablábamos de vértices y primitivas ahora pasamos a hablar de píxeles. Para una primitiva, se genera uno de estos píxeles2 por cada píxel de la imagen final que esta cubra. Estos píxeles son similares a los vértices en cuanto tienen posición, incluida la coordenada z, y otras componentes como normal y color. La diferencia es que representan zonas interiores de la primitiva. Su propósito es servir para calcular el color final del píxel correspondiente en la imagen renderizada. 2 No hay que confundir los píxeles que se generan en la rasterización con los píxeles de la imagen final, que solo tienen posición 2D y color. De hecho, para evitar esta confusión, en el API OpenGL se les llama fragmentos a los primeros. 47 Flat Shading Gouraud Shading Una vez generados los píxeles es necesario dar valor a sus componentes. Al no contar en este punto con más datos que los vértices las componentes de los píxeles se calculan a partir de las componentes de los vértices. La técnica de Flat Shading consiste en copiar los componentes del primer vértice de la primitiva en todos los píxeles. Esto da un aspecto uniforme a la primitiva, por lo que los objetos aparentan estar formados de caras planas. Como alternativa, la técnica de Gouraud Shading consiste en interpolar el valor de las componentes del píxel a partir de las componentes de los vértices ponderándolas según las coordenadas baricéntricas del punto. Esto da un aspecto suavizado a la primitiva, ya que los píxeles interiores tienen componentes con valores graduales. Texture sampling Además de los métodos de shading es posible incorporar un mayor nivel de detalle en los píxeles interiores de las primitivas a través de las técnicas de texturización. Éstas permiten definir valores precisos para sus componentes. Una textura almacena los valores de una propiedad en el espacio para unas posiciones concretas llamadas téxeles. Un caso sencillo sería una textura 2D, que define estas propiedades para un área rectangular a través de una matriz 2D de téxeles. 48 Las texturas se aplican a las primitivas mediante la inclusión de coordenadas de textura en los vértices. Estos componentes definen qué posición de la textura corresponde al vértice. Usualmente los píxeles interiores generados en la rasterización tienen estas coordenadas interpoladas por el método de Gouraud, de forma que cada uno de ellos se corresponde a un punto diferente de la textura. Calcular el valor de una textura para una cierta coordenada, tomar un sample, no siempre es algo directo. La mayor calidad se obtiene cuando existe una correspondencia exacta entre el tamaño del píxel y el tamaño del téxel. Habitualmente este no es el caso, por ello hablaremos aquí de técnicas de sampling. El filtro de minificación es método para calcular el sample cuando el téxel es menor que el píxel. 49 El filtro de magnificación es el caso contrario, es decir, el téxel es mayor que el píxel. El mipmapping consiste en definir varios mipmaps para una única textura. Éstos son matrices de téxeles que utilizan resoluciones cada vez menores, dividiendo entre dos la resolución base. 50 Al representar una escena, en los píxeles que presenten minificación se utilizará el mipmap cuyo tamaño de téxel se adapte mejor al píxel. Los objetos más alejados en la figura utilizarán los mipmaps más pequeños. Fixed function pixel shading En esta etapa cada uno de los píxeles generados en la etapa anterior se somete a un proceso en el que se establece su color final a partir de sus componentes. Éstos pueden ser un color especular, un color difuso y un número determinado de coordenadas de textura. 51 El proceso se realiza en diferentes etapas llamadas texture stages. Tomando como valor inicial el color difuso del píxel cada etapa lo combina con un sample de su textura, que obtiene a través del sampler utilizando los componentes de coordenadas de textura correspondientes. Como paso final se añade el color especular. Una texture stage realiza una operación de combinación sobre sus argumentos. Estas operaciones son vectoriales, y se realizan para cada componente del color. En las fórmulas se tiene en cuenta así mismo la componente de transparencia. 52 DIFFUSE Texture Stage Texture Stage MULTIPLY ADD MODULATE De esta forma las texture stages se configuran para obtener diferentes efectos visuales. Típicamente se utilizarán de la manera expuesta, aunque admiten un gran número de configuraciones alternativas. 53 Pixel processing En esta etapa se realizan las operaciones finales del pipeline, que darán como resultado la imagen representada de la escena. El píxel que se recibe de la etapa anterior ya tiene su color final calculado. Inicialmente se somete a una serie de tests de aceptación o rechazo. Éstos determinan si este píxel se utilizará en los cálculos posteriores. Finalmente los píxeles que superen todos los tests se utilizan para calcular el color del píxel de la imagen representada, posiblemente combinando varios de ellos en el proceso de alpha blend. 54 El Z Test descarta los píxeles en función de su coordenada z. Típicamente de todos los que correspondan a una misma posición de la imagen final el test sólo deja pasar aquel que tenga la coordenada z menor. 55 Hidden surface removal Conceptualmente es un test de determinación de visibilidad, ya que selecciona el píxel más cercano al observador. Los píxeles descartados corresponden a áreas de las primitivas que quedan ocultas por otras más cercanas. 56 Scene without reflections Reflected geometry Stencil Buffer Rendered image El stencil test permite definir áreas de la imagen de forma arbitraria y descartar los píxeles en función de si están dentro de un área o no. Este test se utiliza para simular efectos como sombras y espejos planos. Otros tests son el scissor test, que descarta aquellos píxeles que quedan fuera de un área rectangular de la imagen final y el alpha test, que descarta los píxeles según su transparencia. 57 La última operación que se realiza es el alpha blending. Cuando las primitivas son transparentes varios píxeles pueden corresponder a un mismo píxel en la imagen final. La configuración de este proceso determina en qué medida contribuye cada píxel al píxel final. Programmable pipeline Interpolated Pixel Sampler 0 Pixel Shader ALU Const Regs Temporal Registers Constant Registers Sampler 7 Temporal Registers ... Samp Regs Input Registers Output Registers Shaded Pixel En el Fixed Function Pipeline el procedimiento a aplicar es predeterminado. Tomando como ejemplo la etapa de iluminación vemos que se pueden utilizar diferentes parámetros para las luces, pero no utilizar un modelo de iluminación diferente del de Blinn-Phong. 58 Las aplicaciones que requieren de esta clase de expresividad utilizan el Programmable Pipeline. El procedimiento que se quiere aplicar a vértices o píxeles se especifica en programas llamados respectivamente Vertex Shader y Píxel Shader. De esta forma pueden implementarse no sólo modelos de iluminación alternativos sino multitud de efectos especiales. Lenguaje de shading Los shaders consisten en un código ensamblador que se ejecutará una vez por cada Vértice o Píxel. Atendiendo únicamente a su entrada y su salida su efecto es computar para cada vértice o píxel los valores finales de sus componentes de salida a partir de sus componentes de entrada. 59 Los registros que utilizan los vertex shaders son vectores de cuatro componentes. De esta forma se puede almacenar valores como color, posición, normal de forma directa. Adicionalmente las instrucciones usualmente operan sobre todos los componentes del vector en paralelo. Los juegos de instrucciones que presentan estas características se conocen como Single Instruction Multiple Data (SIMD). Existen juegos de instrucciones y registros separados para vertex y píxel shaders, aunque son similares en apariencia y funcionalidad. Las explicaremos conjuntamente, aunque en rigor dos instrucciones con el mismo nombre y funcionalidad, una del juego de instrucciones del vertex shader y otra del píxel shader, son diferentes. Por otro lado existen diferentes versiones, cada una de ellas refleja las capacidades del hardware gráfico en una determinada época. Estas versiones se conocen como modelos de shader. Registros Los registros de entrada almacenan los componentes del vértice o píxel tal como se reciben de la etapa anterior. Son sólo de lectura y se han de declarar al principio del shader. Los registros de salida se utilizan para guardar las componentes del vértice o píxel calculadas que se pasarán a la etapa siguiente. Se han de declarar y son sólo de escritura. Para los cálculos intermedios se utilizan registros temporales. Lógicamente estos registros no conservan su valor más allá de la ejecución del shader y admiten tanto lectura como escritura. Los registros constantes o, simplemente, las constantes se utilizan para que la aplicación provea parámetros para la función implementada por el shader. Se denominan constantes porque su valor es el mismo para todos los vértices o píxeles del batch. 60 Por ejemplo, un shader que deforme un objeto aplicando una onda sinusoidal podría utilizar una constante para almacenar la amplitud de dicha onda. Los sampler registers dan acceso a los samplers. De esta forma el shader es capaz de utilizar valores recuperados de las texturas en sus cálculos. Es importante darse cuenta de que esto permite utilizar las texturas no sólo para almacenar datos de color, sino para almacenar cualquier tipo de información. Existen otros registros para usos muy específicos. Un ejemplo es el registro de dirección, cuya utilidad es permitir acceso indexado a las constantes. Esto quiere decir que podemos obtener a partir de una constante base la constante cuya posición relativa corresponda con el valor de este registro. Instrucciones Una instrucción se compone de un identificador y posiblemente un parámetro de destino y varios de origen. 61 Name Description abs - vs Absolute value add - vs Add two vectors break - vs Break out of a loop - vs...endloop - vs or rep...endrep block break_comp - vs Conditionally break out of a loop - vs...endloop - vs or rep...endrep block, with a comparison breakp - vs Break out of a loop - vs...endloop - vs or rep...endrep block, based on a predicate call - vs Call a subroutine callnz bool - vs Call a subroutine if a Boolean register is not zero callnz pred - vs Call a subroutine if a predicate register is not zero crs - vs Cross product dcl_usage input register - vs Declare input vertex registers (see Registers - vs_3_0) dcl_samplerType - vs Declare the texture dimension for a sampler def - vs Define constants defb - vs Declare a Boolean constant defi - vs Declare an integer constant dp3 - vs Three-component dot product dp4 - vs Four-component dot product dst - vs Distance else - vs Begin an else block endif - vs End an if bool - vs...else block endloop - vs End of a loop - vs block endrep - vs End of a repeat block exp - vs Full precision 2x exp - vs Partial precision 2x frc - vs Fractional component if bool - vs Begin an if bool - vs block (using a Boolean condition) if_comp - vs Begin an if bool - vs block, with a comparison if pred - vs Begin an if bool - vs block with a predicate condition label - vs Label lit - vs Calculate lighting log - vs Full precision log2(x) logp - vs Partial precision log2(x) loop - vs Loop lrp - vs Linear interpolation m3x2 - vs 3x2 multiply m3x3 - vs 3x3 multiply m3x4 - vs 3x4 multiply m4x3 - vs 4x3 multiply m4x4 - vs 4x4 multiply mad - vs Multiply and add max - vs Maximum min - vs Minimum mov - vs Move mova - vs Move data from a floating point register to an integer register mul - vs Multiply nop - vs No operation nrm - vs Normalize pow - vs xy rcp - vs Reciprocal rep - vs Repeat ret - vs End of a subroutine rsq - vs Reciprocal square root setp_comp - vs Set the predicate register sge - vs Greater than or equal compare sgn - vs Sign sincos - vs Sine and cosine slt - vs Less than compare sub - vs Subtract texldl - vs Texture load with user-adjustable level-of-detail vs Version El juego de instrucciones está adaptado especialmente al tratamiento de gráficos. Lo componen unas decenas de instrucciones. En la figura podemos ver el juego de instrucciones para un vertex shader 3.0, se pueden clasificar en varios grupos. 62 Las instrucciones de declaración o directivas se sitúan al principio del shader. Los shaders se inician declarando su versión. El primer número, o versión mayor, corresponde al modelo de shader. El segundo corresponde a la versión menor, que refleja típicamente pequeños cambios, como un mayor número de registros temporales disponibles. La instrucción de declaración de entrada asocia un registro de entrada con la componente del vértice o píxel que se especifique como sufijo. La instrucción de declaración de salida hace lo propio con los registros de salida. La instrucción de declaración de constante permite dar un valor inicial a una constante. Un uso típico sería almacenar el valor de PI. Las instrucciones aritméticas permiten una amplia variedad de operaciones sobre vectores, algunas muy comunes como el producto escalar. Otras son específicas del dominio de los gráficos, como el cálculo del exponente de Phong. Las instrucciones de textura tienen como parámetros de origen un registro de sampler y unas coordenadas de textura. A partir de ellos almacenan en el parámetro de destino un sample de la textura. 63 Las instrucciones de control de flujo permiten realizar saltos en la secuencia de instrucciones. Se soportan construcciones comunes como salto condicional y bucles e incluso llamadas a subrutinas. Modificadores Tanto los parámetros de origen como los de destino admiten modificadores. Estos afectan al valor que se escribe o se lee del registro. El modificador de Swizziling permite que las componentes del parámetro de origen tomen su valor de las componentes del registro que se especifiquen. De esta forma se pueden obtener un valor en un orden distinto o incluso con algunas componentes replicadas. 64 add r0.xy, v0, r1 Operation result Destination register X X Y Y Z Z W W El modificador de Write Masking permite controlar en qué componentes del registro de destino es posible escribir. Se utiliza, por ejemplo, para trabajar con vectores de dos dimensiones. Del resto de modificadores destacamos el de negación, que permite utilizar un parámetro de entrada que sea el negado de un registro; el de valor absoluto, que fuerza a positivo el signo de las componentes del registro y el de saturación, que controla que los valores que se escriban en un registro no sobrepasen el valor uno, truncando el resultado si es necesario. Una utilidad de éste último es impedir que las componentes de los registros que representen colore superen el valor máximo para una componente de color, que es precisamente uno. High Level Shading Language 65 El High Level Shading Language (HLSL) es un lenguaje de alto nivel para la programación de shaders de sintaxis similar a C. Microsoft pone a disposición de los programadores un compilador que permite obtener el shader equivalente en ensamblador. Es importante notar que el Direct3D sólo recibe shaders en ensamblador, por lo que a efectos de realizar un implementación del API no es necesario dar soporte al HLSL. 2.2.2 Direct3D API Modelo COM Un componente es simplemente una entidad software que realiza un servicio y de la cual únicamente disponemos de una interfaz. Ésta nos permite solicitar los servicios pero oculta completamente no sólo la implementación sino también la naturaleza de la entidad que lo realiza. Es un concepto importado de los componentes electrónicos. 66 El Component Object Model (COM) es una plataforma de componentes de Microsoft. Siguiendo sus especificaciones es posible crear componentes software que puedan ser utilizados desde cualquier lenguaje que soporte el modelo COM. Las aplicaciones utilizan los interfaces mediante referencias a ellos. Típicamente las aplicaciones solicitan una o más referencias a un interfaz e invocan sus servicios a través de éstas, cuando no necesitan los servicios de la interfaz liberan las referencias. Los componentes mantienen una cuenta de referencias, de forma que pueden liberarse recursos cuando un componente no es referenciado por ninguna aplicación. interface IUnknown { virtual HRESULT QueryInterface(REFIID riid, void **ppvObject) = 0; virtual ULONG AddRef(void) = 0; virtual ULONG Release(void) = 0; }; Las interfaces especifican una serie de funciones propias y pueden heredar funciones de otras interfaces. De esta forma amplían los servicios que se ofrecen en la interfaz base. El modelo COM especifica la interfaz base IUnknown de la que todos los interfaces han de heredar. Los métodos AddRef y Release se utilizan para informar al componente de cuantas referencias se están utilizando. Por otro lado el método QueryInterface sirve para solicitar un interfaz diferente para el un mismo componente. Interfaces Direct3D Direct3D es una API que sigue el modelo COM. De esta forma existe un componente Direct3D en los sistemas Windows que da servicio independientemente del lenguaje en que estén implementadas. 67 a las aplicaciones VertexDeclaration VertexBuffer Device IndexBuffer Resource VertexShader PixelShader Direct3D BaseTexture VolumeTexture Texture Surface Surface CubeTexture Surface Direct3D define una serie de interfaces relacionados entre sí. Observamos como Direct3D es un objeto COM, por lo que tiene su propia interfaz. A partir de ésta podemos acceder al interfaz Device, que da acceso al componente que realiza el rendering. Es el interfaz más amplio, dispone de decenas de funciones: A través de éste interfaz es posible crear instancias del resto de componentes, que representan otras entidades del rendering pipeline. 68 Para definir que entidades se utilizarán durante la renderización se ofrecen funciones para relacionarlas con el Device. Por último existen varias funciones para renderizar batches. Un grupo de interfaces son aquellos que representan recursos. Los recursos son entidades que tienen asignada una o más áreas de memoria. El acceso a la memoria asignada a un recurso para, por ejemplo, escribir nuevos contenidos se realiza mediante unas funciones llamadas lock y unlock. 69 Una aplicación de ejemplo Presentaremos para finalizar el apartado el código de una aplicación mínima en Direct3D. Primero veremos la versión de función fija y luego la programable. En ambos casos el resultado será un sólo triángulo. IDIRECT3D9 * D3D = NULL; IDIRECT3DDEVICE9 * Device = NULL; D3D = Direct3DCreate9( D3D_SDK_VERSION ); D3DPRESENT_PARAMETERS PresentParameters; ZeroMemory( &PresentParameters, sizeof(PresentParameters) ); PresentParameters.Windowed = TRUE; PresentParameters.Width = 400; PresentParameters.Height = 400; PresentParameters.SwapEffect = D3DSWAPEFFECT_DISCARD; PresentParameters.BackBufferFormat = D3DFMT_A8R8G8B8; D3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &PresentParameters, &Device ); El primer paso que realiza una aplicación es obtener una referencia al componente Direct3D y utilizarlo para crear un Device. La configuración del device se especifica mediante la estructura PresentationParameters. 70 A continuación la aplicación crea los recursos que va a necesitar para el rendering, en este caso un vertex buffer. Para establecer sus contenidos realiza una operación lock, que le devuelve un puntero a un buffer de memoria. Tras copiar a este buffer los vértices indica que los contenidos están actualizados mediante la operación unlock. D3DXMATRIXA16 matWorld; D3DXMatrixIdentity( &matWorld); Device->SetTransform( D3DTS_WORLD, &matWorld ); D3DXMATRIXA16 matView; D3DXMatrixIdentity( &matView); Device->SetTransform( D3DTS_VIEW, &matView ); D3DXMATRIXA16 matProj; D3DXMatrixPerspectiveFovLH(&matProj, 3.14f/4.0f, 1.0f, 1.0f, 100.0f ); Device->SetTransform( D3DTS_PROJECTION, &matProj ); Device->SetRenderState( D3DRS_LIGHTING, FALSE ); Como parte de la configuración de función fija se establecen las matrices de transformación y se desactiva la iluminación, de forma que los colores de los vértices no se alterarán. 71 Device->Clear( 0, NULL, D3DCLEAR_TARGET, 0xff0000ff, 0 ); Device->BeginScene(); Device->SetStreamSource(0, VertexBuffer, 0, sizeof(CUSTOMVERTEX)); Device->SetFVF( D3DFVF_CUSTOMVERTEX ); Device->DrawPrimitive( D3DPT_TRIANGLELIST, 0, 1 ); Device->EndScene(); Device->Present( NULL, NULL, NULL, NULL ); En este punto se inicializa el back buffer con el color inicial. Después se procede al dibujado de las primitivas y finalmente los contenidos del back buffer se promueven al front buffer. Por último la aplicación indica que no va a necesitar más los interfaces. Una aplicación que utilice el programmable pipeline inicializa el device y los recursos de forma idéntica, creando adicionalmente un píxel y un vertex shader. El código ensamblador ha de proveer como un bytecode, no como una cadena de texto. Por este motivo la aplicación ensambla el shader utilizando la librería auxiliar D3DX. 72 La aplicación crea un interfaz VertexShader y un interfaz PixelShader con el bytecode que ha obtenido previamente. Device->SetVertexShader(VertexShader); Device->SetPixelShader(VertexShader); En el momento de configurar el pipeline no se realiza la configuración de función fija. En su sustitución se asocia el VertexShader y el PixelShader. Las operaciones siguientes son idénticas, adicionalmente se liberan el VertexShader y el PixelShader. 73 2.3 El simulador ATTILA 2.3.1 El pipeline del simulador A nivel funcional el simulador se comporta como un pipeline programable. 75 Streaming La primera etapa del pipeline es el Streaming. El simulador cuenta con una serie de streams a los que se pueden asociar búferes de memoria a través de su dirección. Los vértices se almacenan en estos búferes, aunque para el simulador son áreas de memoria. El simulador soporta modo indexado, sin embargo no hay un stream dedicado para índices, sino que se usa uno de los streams genéricos. El vértice se ensambla indicando para cada uno de sus atributos en qué stream está situado. La configuración de esta etapa, como la de las siguientes, se realiza a través de registros. Vertex Shading Una vez ensamblado el vértice se somete al proceso de shading. Para ello se ejecuta un vertex shader almacenado en un buffer de memoria. La unidad de shading realiza así un cálculo sobre los atributos del vértice que recibe en y deja los resultados en los registros de salida, que corresponden al vértice transformado. 76 Geometry Processing La etapa de geometry processing comienza ensamblando la primitiva, que en este caso siempre es un triangulo, una vez se han procesado sus tres vértices. La primitiva se somete entonces a un test de clipping muy sencillo, que la descarta sólo si está completamente fuera del espacio de clipping. El siguiente test es un test de culling, es decir, descarta la primitiva en función de si está de cara o no al observador. La primitiva se rasteriza generando fragmentos3 a partir de sus vértices, sin embargo estos fragmentos resultantes sólo tienen interpolada su posición, no el resto de atributos. Si se dan ciertas condiciones, una de ellas que se esté renderizando sin tener en cuenta la transparencia, es posible descartar el fragmento en este punto a través de tests de visibilidad. El Hierarchical Z aplica un sofisticado método de división espacial mientras que el Early Z es un Z Test corriente. De esta forma se evita realizar cálculos sobre un fragmento que luego se descartará en el Z Test. A los fragmentos que superan estos tests se les interpolan el resto de atributos y se dirigen a la siguiente etapa. 3 El simulador ATTILA sigue la terminología de OpenGL, y llama fragmentos a los elementos que resultan de la pasterización. Recordemos también que Direct3D les llama píxeles. 77 Fragment Shading El fragmento se procesa a través del fragment shader, de forma análoga al vertex shader el proceso consiste en calcular los atributos de salida a partir de los de entrada. Pixel Rendering Una vez conocidos los valores finales de los atributos el fragmento pasa por el Alpha, Z (si no lo había hecho ya) y Stencil Test. Si no es descartado se realiza el Alpha blending y se actualiza el píxel correspondiente en el back buffer. 78 Texture Sampling El simulador soporta texturas 2D, 3D y cúbicas, así como mipmapping. Una textura ocupa así varios búferes, uno por mipmap. Los téxeles no se sitúan en memoria siguiendo una ordenación lineal, sino que utilizan un orden especial en zigzag conocido como orden de Morton. Las texturas se asocian a Texture Units que se encargan del sampling, aplicando los filtrados correspondientes. Las Shader Units realizan peticiones a las Texture Units cuando el shader que ejecutan lo requiere. Lenguaje de shading Físicamente el simulador dispone de un conjunto de pequeños procesadores vectoriales, llamados Shader Units, que pueden realizar las funciones de una Vertex Shader Unit o una Fragment Shader Unit. Esto se conoce como modelo de shader unificado. Una consecuencia importante es que los registros y el juego de instrucciones disponibles para la etapa de Vertex Shading y la de Fragment Shading son prácticamente iguales. Una diferencia notable es que en la etapa de Vertex Shading no se puede acceder a texturas. 79 Opcode NOP ADD ARA ARL ARR COS DP3 DP4 DPH DST EX2 EXP FLR FRC LG2 LIT LOG MAD MAX MIN MOV MUL RCC RCP RSQ SEQ SFL SGE SGT SIN SLE SLT SNE SSG STR TEX TXB TXP KIL CMP END Description no operation add address register add address register load address register load (rounded) cosine 3-component dot product 4-component dot product Homogeneous dot product distance vector exponential base 2 (full precission) exponential base 2 (partial precission) floor fractional part full precission log 2 light coefficients logarithm base 2 multiply and add maximum minimum move multiply reciprocal (clamped) reciprocal reciprocal square root set on equal set on false set on greater equal than set on greater sine set on less or equal set on less than set on non equal set sign set on true texture lookup texture lookup with LOD bias projective texture lookup kill fragment compare program end El juego de instrucciones está basado en el Nvidia Shader Model 1. Disponemos de instrucciones SIMD aritméticas, lógicas y de acceso a texturas. En sus parámetros admiten modificadores de Swizzling, Masking, saturación, negación y valor absoluto. Los registros disponibles son vectoriales de cuatro componentes, que se organizan en varios bancos de registros: de entrada, de salida, de textura, parámetros constantes y temporales. Los registros de textura representan a las unidades de textura. Así mismo existen dos registros de dirección para acceder a las constantes de forma relativa. 80 2.3.2 El hardware simulado El simulador de GPU ATTILA está formado por diferentes componentes que podríamos encontrar en la micro arquitectura de una GPU real. Todos estos componentes están implementados en software, pero nos referiremos a ellos como si fueran un hardware físico. 81 Streaming stage registers VERTEX_ATTRIBUTE_MAP VERTEX_ATTRIBUTE_DEFAULT_ VALUE STREAM_ADDRESS STREAM_STRIDE STREAM_DATA STREAM_START STREAM_COUNT INDEX_MODE INDEX_STREAM stores the stream where each vertex attribute is located default value for an attribute used if not available from a stream memory address of vertex/index data for the stream offset in bytes between indexes/vertices for each stream type of data to stream first vertex/index to be streamed in a batch count of vertexes/indexes to be streamed enables or disables indexed mode stream to be used for indexes El simulador dispone de una serie de registros que controlan el funcionamiento de sus diferentes componentes. Para almacenar los recursos que se utilizan en el rendering el simulador dispone de su propia memoria gráfica. 2.3.3 La interfaz del simulador. El simulador recibe como entrada transacciones. Éstas representan las órdenes de bajo nivel que se transmitirían a una GPU real a través del bus al que estuviera conectada. Transaction Types WRITE REG_WRITE COMMAND Commands DRAW SWAPBUFFERS CLEARZSTENCILBUFFER CLEARCOLORBUFFER LOAD_VERTEX_PROGRAM LOAD_FRAGMENT_PROGRAM write from system memory (system to local). write access to GPU registers control command to the GPU Command Processor Draw a batch of vertexes. Swap the front and back buffer. clear z/stencil buffer to the current clear value clear color buffer with the current clear color Load a vertex program into the vertex instruction memory. Load a fragment program into the fragment instruction memory. Las transacciones permiten establecer los valores necesarios en los registros del simulador, de esta forma se determina el comportamiento de las diferentes etapas. Los datos, por ejemplo los vértices, se proveen mediante transacciones de escritura de memoria. Finalmente se pueden enviar órdenes para la unidad de control del simulador, que representan operaciones de un nivel más alto. Dos ordenes básicas son DRAW, que ordena la renderización de un batch y SWAPBUFFERS, que produce la imagen final. Disponemos de un pequeño driver que ofrece servicios para generar transacciones de forma cómoda, así como una gestión de memoria básica que permite reservar áreas 82 de memoria local o de sistema y referirnos a ellas mediante identificadores, abstrayéndonos de las direcciones físicas. 2.3.4 Uso del simulador Capture Verify Simulate Application GLInterceptor Trace GLPlayer GLPlayer Trace stats OpenGL Driver OpenGL Driver ATTILA OpenGL Driver API stats Real GPU Real GPU ATTILA Simulated GPU GPU stats Rendered Frames Rendered Frames Rendered Frames Para realizar una simulación que sea fiel a las condiciones en que trabaja una GPU real se utilizan aplicaciones reales, típicamente juegos. Estos utilizan los servicios de rendering a través de API’s como OpenGL o Direct3D. El simulador actualmente da un soporte completo a OpenGL. La etapa de captura tiene como objetivo conocer exactamente que operaciones invoca una aplicación sobre el API y registrarlas en un fichero, llamado traza. El método que se utiliza es interponer un software entre la aplicación y el API dedicado a interceptar el tráfico de llamadas. De esta forma la traza almacena la parte del comportamiento de la aplicación que interesa estudiar. Una vez se dispone de una traza ésta se puede reproducir esto significa volver a ejecutar las operaciones que están almacenadas en ella. En una primera etapa y como comprobación se ejecutan sobre la implementación del API en que se realizó la captura. Finalmente se realiza la simulación propiamente dicha, reproduciendo la traza sobre la implementación del API propia del simulador. Como resultado se obtienen los frames renderizados, así como ficheros de estadísticas para su posterior análisis. 83 3. Trabajo realizado 85 3.1 Captura, reproducción y análisis de trazas. Capture Verify Simulate Application D3D Capture Trace D3D Player D3D Player Trace stats Windows D3D Windows D3D ATTILA D3D Driver API stats Real GPU Real GPU ATTILA Simulated GPU GPU stats Rendered Frames Rendered Frames Rendered Frames Es necesario probar el simulador en condiciones cercanas a las de una GPU física. Para ello el trabajo que debe realizar se extrae de ejecuciones de videojuegos4, una de las aplicaciones que utiliza con más intensidad la GPU. Los videojuegos se comunican con la GPU a través del API gráfico 3D, en este caso Direct3D. La secuencia de operaciones que realizan sobre el API es la traza del videojuego. Esta traza, almacenada en un fichero, se usará como entrada para el simulador. Un software reproductor recuperará las operaciones de la traza y las ejecutará de nuevo. Antes de llegar a la GPU simulada las operaciones pasan por el driver Direct3D del simulador. La traza se analiza, para conocer qué demandas tiene el videojuego. De esta forma se puede caracterizar el uso del API y conocer a priori qué funciones del driver han de ser operativas para poder ejecutarla. Mantenemos un grupo de trazas de referencia, que corresponden a varios videojuegos representativos. 4 Propiamente debería hablar de “aplicaciones que usen el API Direct3D”. Pero la mayoría de las que estudiamos son videojuegos, así que las llamaré así. 87 3.1.1 Subsistema de captura: Aplicación PIX La captura se realiza en un PC corriente equipado con una tarjeta gráfica de altas prestaciones y el sistema operativo Windows. Los videojuegos acceden al componente Direct3D de Windows a través de una librería dinámica que expone los interfaces de Direct3D. El objetivo es, pues detectar las llamadas a esta librería. Me referiré a la implementación de Direct3D de Microsoft como el componente Direct3D de Windows, para distinguirlo de nuestra implementación de Direct3D. La tarea de captura justificaría de hecho un proyecto aparte, para evitar esto y tras evaluar las diferentes alternativas se optó por utilizar como capturador el Performance Investigator for DirectX en adelante PIX, una utilidad propietaria de Microsoft y que se distribuye dentro del Standard Development Kit de DirectX. PIX genera un fichero de traza binario que contiene todos los datos que necesitamos, pero su formato no es público. Por tanto el problema de la captura se reduce a descubrir el formato del fichero de traza que usa PIX. Aplicación PIX A diferencia de las aplicaciones de ofimática en el campo de las aplicaciones gráficas interactivas el rendimiento es muy importante, ya que los usuarios perciben con facilidad las caídas en el número de frames por segundo que se producen cuando la 88 GPU se satura. Por este motivo existe un gran interés en utilizar lo más eficientemente los recursos disponibles, dando lugar a aplicaciones como PIX, que se utiliza para que los desarrolladores puedan evaluar el rendimiento de las aplicaciones Direct3D. Sin embargo de cara a este proyecto el único aspecto que interesa de PIX es su capacidad para guardar en un fichero toda la interacción de una aplicación con el API Direct3D en un fichero que utilizaremos como traza. 1: INTERCEPT 2: PROPAGATE DrawPrimitive(prim_type, prim_count) DrawPrimitive(prim_type, prim_count) PIX Direct3D Impostor Component 3D Application result 3: STORE result Direct3D Windows Component Store(draw_primitive_id, prim_type, prim_count, result) PixRun Trace File En esencia el método que utiliza PIX para realizar la captura es colocar una librería dinámica impostora que expone las mismas operaciones que Direct3D. La aplicación realiza las operaciones sobre la impostora y ésta las realiza a su vez sobre Direct3D. Una vez la operación se ha ejecutado se almacena en el fichero de traza y se devuelve el control a la aplicación. Examen del formato de traza El formato de fichero que usa PIX es binario, y afortunadamente no está encriptado ni usa compresión. A partir de un examen visual utilizando un editor de archivos binarios se determinó que el archivo seguía una organización jerárquica en diferentes estructuras y campos, por lo que se decidió que podía determinarse el formato en un tiempo razonable. El tamaño de los ficheros de traza que produce PIX para un videojuego es del orden de gigabytes. Por este motivo comencé programando una serie de aplicaciones muy sencillas que utilizan un conjunto controlado de operaciones de Direct3D. Estas aplicaciones se limitan a realizar unas decenas de operaciones del API, de este modo 89 los ficheros de traza resultantes son muy pequeños y pueden examinarse con comodidad con un editor de ficheros binarios. Tamaño de sección Tipo de sección Fichero Binario PixRun aplicando la plantilla desarrollada Fichero Binario PixRun Examinando directamente la representación hexadecimal encontramos una estructura en secciones. Cada sección comienza con un campo que indica su tamaño y otro campo que indica el tipo. Los datos que completan la sección se interpretan según el tipo. Examinar el fichero de este modo es incómodo (y daña la vista). Como otras aplicaciones parecidas el editor que uso permite el uso de plantillas, que permiten estructurar el formato binario mediante un sencillo lenguaje de expresiones regulares. Escribí una plantilla que fui completando con las diferentes aplicaciones de test hasta dar sentido a los campos que me interesaban. 90 Localización de las llamadas al API Direct3D PIX guarda en el fichero de traza los Eventos que detecta durante la captura. Éstos incluyen llamadas a Direct3D que haya realizado la aplicación, pero no se limitan a ello. SECTION HEADER ATTRIBUTE DESCRIPTOR EVENT DESCRIPTOR EVENT ASYNC ATTRIBUTE D3DCALL FOOTER Size Type Data =0x03E8 =0x03E9 ADID Unknown Zero Name Format =0x03EA EDID Unknown Name attributesCount ADID Initialization … =0x03EB EDID EID data =0x03EC EID ADID data =0x03EC =0x03ED =0x13 size CID one return parameters El fichero de traza contiene meta datos en sus secciones iniciales. Las secciones de tipo Event Descriptor contienen la descripción de los atributos que PIX almacena para cada evento. Las secciones de tipo Attribute Descriptor describen estos atributos. Examinar estas secciones ayuda a hacerse una idea general de cómo se estructura el resto del fichero. Las secciones de tipo Event vienen a continuación y son las más abundantes. El tipo de Evento que se utiliza para las llamadas a Direct3D se llama D3DCall, y entre sus atributos encontramos uno llamado Packed Call Package (PCP), que es justo lo que buscábamos. Interpretación de una llamada: Packed Call Package Un Packed Call Package (PCP) es un atributo que almacena una llamada a una operación de Direct3D. Su estructura incluye el Call Id, que identifica la operación, el 91 valor de retorno y el resto de parámetros. Relacionando el identificador con la operación del API Direct3D que representa podemos conocer la sintaxis de la operación e interpretar los parámetros. En este caso se trata de la llamada CreateDevice del interfaz Direct3D. La interpretación de los parámetros, siguiendo el orden de la figura, comienza por la dirección del objeto interfaz. De los cuatro parámetros siguientes se almacena su valor, ya que son tipos simples. El siguiente es un tipo estructurado que se pasa como un puntero y recibe un tratamiento diferente, primero se guarda la dirección de la estructura y a continuación los contenidos de ésta. Como indica su nombre el efecto de esta llamada es crear un objeto device, de este modo el último parámetro es de salida. El primer valor es la dirección de la variable puntero y a continuación se encuentra su valor después de la llamada, es decir la dirección del objeto device que se creó. Llamadas Lock/Unlock En Direct3D el acceso a recursos se realiza mediante las operaciones Lock y Unlock. La operación Lock retorna un puntero a un área de memoria donde la aplicación puede leer o escribir. La aplicación indica que ha terminado de trabajar con los contenidos del 92 área de memoria mediante la operación Unlock. Para registrar el acceso a recursos el fichero de traza almacena los contenidos del área de memoria en este tipo de operaciones. Este PCP corresponde a una aplicación que ha terminado de actualizar un vertex buffer. La aplicación ha invocado previamente una operación inicial Lock y mediante esta llamada indica que ya ha escrito los vértices en memoria. En la figura pueden verse como PIX almacena los contenidos del área de memoria que se ha utilizado en la actualización. 3.1.2 Subsistema de reproducción: D3DPixRunPlayer La reproducción de trazas consiste en deserializar las operaciones almacenadas en el fichero de traza PIX y ejecutarlas de nuevo sobre Direct3D. El responsable de esta reproducción es el subsistema D3DPixRunPlayer. Es importante señalar que, mientras que la captura se realiza en un PC corriente equipado con Windows la reproducción se ha de poder realizar tanto en sistemas Windows como en Linux. Por este motivo el subsistema de reproducción de trazas ha de ser portable. 93 Tratamiento de llamadas Una de las principales dificultades que encontré es que cada operación tiene una sintaxis diferente. Al no encontrar esa meta información en el fichero, es necesario tratar cada operación mediante un código diferente. El bucle de reproducción lee el identificador de llamada y ejecuta el tratamiento correspondiente a cada una. Hay cientos de ramas condicionales, y cada una de ellas tiene un procedimiento único para obtener los parámetros de entrada, ejecutar la operación y tratar los parámetros de salida. 94 Como ejemplo, para leer los parámetros de entrada de la operación CreateDevice es necesario deserializar cada uno de ellos en orden, distinguiendo entre si son valores, estructuras u objetos. La clave es que procedimiento de lectura puede expresar de forma genérica para cualquier operación, ya que su sintaxis determina la secuencia de tipos que se encuentran en el fichero de traza. Para particular este procedimiento sólo necesito disponer en algún tipo de estructura de datos de la sintaxis de cada operación. 95 DXCodegen (clase PixRunPlayerGenerator) Tratamiento Genérico de las operaciones Tratamientos Específicos Preprocesador C++ Código PixRunPlayer Código de usuario La aplicación DXCodegen incluye clases que recuperan la sintaxis de las operaciones a partir de los ficheros de cabecera del API Direct3D. Esta aplicación me viene dada, ya que forma parte de otro proyecto. Utilizando DXCodegen como entorno de trabajo implementé las clases que generan el código5 del reproductor de trazas PixRunPlayer. El código generado implementa el tratamiento genérico de las operaciones y sigue unas pautas similares a las del código para la lectura de parámetros que he comentado. Sin embargo seguir unas reglas generales no es suficiente ya que entre los cientos de operaciones se encuentran algunas que suponen una excepción a estas reglas, un ejemplo serían las operaciones Lock/Unlock. Por este motivo es necesario incorporar al código generado automáticamente un mecanismo para incluir código específico para las operaciones que lo requieran. La técnica que se utiliza es generar para cada operación varias constantes del preprocesador de C++. De este modo se pueden definir las constantes con el código que realice el tratamiento específico de la operación. Por otro lado es posible incluir con esta técnica código de usuario para cualquier otro propósito, un ejemplo es la obtención de estadísticas. Objetos originales y sustitutos Las direcciones que encontramos en el fichero de traza corresponden a cada objeto de la interfaz de Direct3D que la aplicación utilizó durante la captura. Estos objetos ya no existen, por lo que sus direcciones no son válidas. 5 Resulta muy curioso escribir en C++ un generador de código C++. 96 VertexBuffer2 VertexBuffer2 VertexBuffer1 VertexBuffer1 Device Direct3D Device Texture1 Texture2 1 3D Application PIX Direct3D Impostor Component Surface1 Surface1 Direct3D Texture1 Surface2 Texture2 3 Surface3 2 Direct3D Pixrun Player PixRun Trace File Surface2 Surface3 4 PixRun Trace File Los objetos interfaz de Direct3D forman una red de relaciones entre ellos, por ejemplo durante la captura existía un interfaz textura relacionado con un interfaz surface. Durante la reproducción esta red de objetos se recrea y cuando la aplicación interroga al interfaz textura sobre su surface asociado este devuelve una dirección diferente que la que hubiera devuelto durante la captura. Por tanto durante la reproducción hablamos de unos objetos sustitutos, presentes en memoria para recibir las operaciones que se invocaron sobre los objetos originales que existían durante la captura. Una de las funciones del reproductor será mantener la asociación entre las direcciones de los objetos interfaz originales y la de los sustitutos. Tratamiento de las operaciones lock/unlock Durante la reproducción es necesario actualizar áreas de memoria como resultado de las operaciones de acceso a recursos Lock/Unlock. Actualizar un área de memoria no es simplemente copiar los contenidos originales, ya que la disposición de la memoria puede ser diferente y depende de la arquitectura sobre la que opere Direct3D. 97 Como ejemplo al acceder a la matriz de téxeles de una textura encontramos que las filas no se encuentran forzosamente seguidas en memoria, y su separación es variable. Esta separación, llamada pitch para distinguirla del ancho de la fila, depende de cómo se almacene la textura en memoria y es propia de cada arquitectura gráfica. Reproducción a nivel de batch Para favorecer la capacidad de depuración la reproducción puede realizarse a diferentes granularidades. A nivel de frame, es decir, cuando se actualiza el 98 frontbuffer; a nivel de batch, cuando se dibuja un conjunto de primitivas o una llamada cada vez. En el ejemplo se reproduce la traza a nivel de batch sobre el componente Direct3D de Windows. Se aprecia cómo el frame se construye progresivamente. Reproducción con configuraciones alternativas [SubstituteDevice] ; This section allows to override some original presentation parameters to ; run the trace with a different configuration for the device. ; Windowed = true ; BackBufferWidth = 1280 ; BackBufferHeight = 1024 ; BackBufferCount = 1 ; D3DSWAPEFFECT_DISCARD -> 1 ; D3DSWAPEFFECT_FLIP -> 2 ; D3DSWAPEFFECT_COPY -> 3 ; ; SwapEffect = 3 ; D3DDEVTYPE_HAL = 1, ; D3DDEVTYPE_REF = 2, ; ; DeviceType = 2 ; D3DCREATE_SOFTWARE_VERTEXPROCESSING = 0x20 ; D3DCREATE_MIXED_VERTEXPROCESSING = 0x80 ; D3DCREATE_HARDWARE_VERTEXPROCESSING = 0x40 ; ; BehaviorFlags = 0x20 ; D3DMULTISAMPLE_NONE = 0, ; D3DMULTISAMPLE_NONMASKABLE = 1, ; D3DMULTISAMPLE_2_SAMPLES = 2, ; D3DMULTISAMPLE_3_SAMPLES = 3, ; D3DMULTISAMPLE_4_SAMPLES = 4, ; D3DMULTISAMPLE_5_SAMPLES = 5, ; D3DMULTISAMPLE_6_SAMPLES = 6, ; D3DMULTISAMPLE_7_SAMPLES = 7, ; D3DMULTISAMPLE_8_SAMPLES = 8, ; D3DMULTISAMPLE_9__SAMPLES = 9, ; D3DMULTISAMPLE_10_SAMPLES = 10, ; D3DMULTISAMPLE_11_SAMPLES = 11, ; D3DMULTISAMPLE_12_SAMPLES = 12, ; D3DMULTISAMPLE_13_SAMPLES = 13, ; D3DMULTISAMPLE_14_SAMPLES = 14, ; D3DMULTISAMPLE_15_SAMPLES = 15, ; D3DMULTISAMPLE_16_SAMPLES = 16, ; ; MultiSampleType = 0 ; MultiSampleQuality = 0 ; FullScreen_RefreshRateInHz = 0 ; PresentationInterval = 0 Por otro lado en ocasiones resulta conveniente alterar las condiciones de la reproducción. Para ello se invocan operaciones de Direct3D que no están presentes en la traza o se alteran los parámetros de algunas operaciones. Existen diferentes propósitos para hacer esto. Un caso típico sería reproducir utilizando una resolución diferente en un equipo que no soporte la resolución requerida por la traza. Por conveniencia, este comportamiento se controla a través de un fichero de configuración. 99 Diseño de PixRunPlayer Player PixRunReader 1 Configuration PixRunPlayer Status 1 * LockUpdater SurfaceLockUpdater VolumeLockUpdater VertexBufferLockUpdater IndexBufferLockUpdater PixRunReader localiza las estructuras y campos en que están almacenadas las operaciones dentro del fichero PIX. Las operaciones que ofrece son de bajo nivel, actuando en ciertos aspectos como un analizador léxico. PixRunPlayer es la clase central, generada automáticamente en gran parte. Esta clase conoce la secuencia de parámetros que forman cada operación y utiliza el lector para recuperarlos. Se comporta en este aspecto como un analizador sintáctico. Status mantiene la asociación entre los componentes de Direct3D originales y sustitutos y es responsable del tratamiento específico de las operaciones Lock/Unlock. La actualización también se realiza de forma diferente según el tipo recurso, por este motivo se incluyen diferentes tipos de LockUpdater, especializados en cado uno de ellos. Configuration se encarga de leer el fichero de configuración. 100 Pruebas de uso El reproductor reproduce correctamente todas las trazas de referencia que utilizamos, provenientes de videojuegos comerciales y demostraciones técnicas. 101 3.1.3 Subsistema de análisis de trazas El análisis de trazas es útil para conocer cómo los videojuegos usan el API Direct3D. Dado que los recursos de que disponemos no permiten realizar una implementación completa de cada una de las características que ofrece Direct3D, el análisis nos sirve para averiguar qué funcionalidad es necesaria para una determinada traza. Para este fin añadí clases para el tratamiento de estadísticas al reproductor, algunas de ellas disponibles ya en el departamento. La comunicación con estas clases se realiza mediante código de usuario. Durante la reproducción se registran las estadísticas en una serie de ficheros de texto, la mayoría de ellos consisten en datos tabulados. 102 Informes mediante PERL Para obtener informes a partir de los ficheros de estadísticas decidí utilizar PERL, un lenguaje especializado en este tipo de procesamiento. 103 Una ampliación a este mecanismo que se realicé por motivos no directamente relacionados con este proyecto fue la generación automatizada de gráficas a partir de los ficheros de estadísticas utilizando la aplicación Microsoft Excel. Aproveché que Excel utiliza el modelo COM, lo que permite programarlo desde PERL utilizando un módulo de interoperabilidad. 104 Dependiente de la plataforma nativa Independiente de la plataforma nativa 3.2 Implementación de Direct3D sobre el simulador ATTILA La función del driver Direct3D, en adelante el driver, es realizar las operaciones de este API utilizando al simulador como plataforma nativa. Direct3D es un API muy extensa. Por tanto una cuestión importante es: ¿Hasta que punto se requiere una implementación completa? Claramente escribir una implementación completa sobrepasaría el tiempo y los recursos de que dispongo. Por este motivo se establecen unos criterios generales que limitan el alcance de la implementación: El criterio básico es que si una funcionalidad no se necesita aún, no se implementa. Me refiero a las funcionalidades que requieren los videojuegos que se quieren simular. Más concretamente, si una funcionalidad no aparece en las trazas, no se implementa. Es la manera de concentrar el esfuerzo y guiar el desarrollo, sin invertir recursos en previsión de lo que se pueda necesitar ni perderse entre la multitud de funcionalidades disponibles en Direct3D. No es necesario implementar funcionalidades de consulta. El driver Direct3D del simulador ejecuta las operaciones almacenadas en una traza. En realidad no hay un videojuego tomando decisiones al otro lado. El videojuego realizó consultas mientras se capturaba la traza, el componente Direct3D de Windows ya le proporcionó las respuestas. 105 No es necesario validar los parámetros de las operaciones ni controlar las numerosas situaciones de error que podrían darse. El driver sólo ejecutará trazas correctas, es decir, trazas originadas por una ejecución exitosa del videojuego, libres de errores. Cuando llega el momento de implementar una funcionalidad necesito las especificaciones: ¿Qué ha de hacer exactamente esta operación? Lo ideal sería disponer de una especificación formal de las operaciones. Sin embargo Microsoft no publica las especificaciones formales de su API Direct3D, precisamente para evitar implementaciones de terceros6. Por fortuna Microsoft pone a disposición de los desarrolladores una completísima documentación que uso como especificación. En ocasiones me he encontrado con ambigüedades, viéndome obligado a programar pequeñas aplicaciones 3D para conocer cómo reacciona el componente Direct3D de Windows en casos muy concretos. Otras cuestiones tienen que ver con aspectos no funcionales del driver: Es necesario que el driver sea portable. El desarrollo lo hago en un PC corriente equipado con el sistema operativo Windows, en este equipo la simulación de un frame con unos pocos triángulos puede suponer un minuto. Un videojuego puede utilizar hasta diez millones de triángulos por frame, para este tipo de simulación se utilizan los supercomputadores del laboratorio de cálculo del departamento, que utilizan el sistema operativo Linux. No es necesario optimizar demasiado la implementación. El driver realiza una pequeña parte del trabajo, el grueso corresponde al simulador: Una sola operación a nivel del driver se corresponde con miles de ciclos en el simulador. Desde la perspectiva del hardware el driver se utiliza sólo durante un ciclo, aplicando la Ley de Amdahl es de los últimos elementos a optimizar. Un requisito que he aportado es que el driver se pueda depurar de forma interactiva, mi idea es que se pueda consultar con facilidad aspectos como el código fuente del vertex shader actual mientras ejecuto las pruebas. Trabajando en programación gráfica he observado y experimentado lo difícil de depurar que puede llegar a ser una aplicación 3D atendiendo solo al frame final que produce. Un caso que me he encontrado es programar una aplicación que maneja una cámara virtual a través de un escenario y encontrar frames completamente negros. Tras dedicar mucho tiempo a 6 El caso contrario es el de OpenGL que es, de hecho, una especificación y no una implementación concreta. 106 buscar errores en el código que dibuja el escenario encuentro que el error está en la orientación de la cámara, que está mirando en una dirección en que no hay nada. Está claro que el driver ha de tener parte de la funcionalidad del componente Direct3D de Windows pero eso no quiere decir que su comportamiento interno sea el mismo. El objetivo es simular la GPU, no simular cómo funciona la implementación oficial de Direct3D, cuyos detalles son privados. El driver ha de usar la GPU de forma lo más eficiente posible. Sino los resultados de las simulaciones no representarían las condiciones en que, suponemos, trabaja una GPU real. 3.2.1 Método de trabajo El objetivo es que se puedan ejecutar trazas de videojuegos comerciales sobre el driver. Para llegar a ese punto el driver pasará por muchas versiones intermedias, que ejecutarán trazas de aplicaciones 3D cada vez más complejas. Esta aproximación es parecida a la que se utiliza en metodologías como el Open Unified Process, que me ha servido como guía. Se trata de producir una versión funcional del software cada poco tiempo, en períodos que denominan iteraciones. Las iteraciones se agrupan en fases, en cada fase se persigue un objetivo diferente. En las iteraciones de incepción el énfasis es entender el objetivo que se persigue, por lo que el producto son simplemente prototipos de usar y tirar. Las iteraciones de elaboración tienen como objetivo encontrar la arquitectura de la solución final, esto es, un software cuya estructura sea estable y suficiente para llegar a la versión definitiva. 107 Superada esta fase se realizan las iteraciones de construcción, en las que la arquitectura es estable y sobre ella el software se va ampliando con nuevas capacidades hasta que es operativo. Las últimas iteraciones son las de transición. En ellas las tareas se centran en los destinatarios del software, por ejemplo ofreciéndoles cursos de formación o realizando pruebas beta. 3.2.2 Arquitectura D3DPlayer D3DShaderTranslator D3DProgrammablePipeline D3DFixedFunctionEmulator GPUDriver La arquitectura del driver divide las responsabilidades entre varios subsistemas. D3DProgrammablePipeline es el subsistema central y modela entidades propias del pipeline programable. Actúa como interfaz del driver y recibe todas las operaciones del API Direct3D, pero reparte algunas responsabilidades: Las propias del pipeline de función fija recaen en D3DFixedFunctionEmulator y las de traducción de shaders en D3DShaderTranslator. Es el único subsistema que se comunica con el driver del simulador. D3DShaderTranslator está dedicado a traducir los shaders de Direct3D en shaders ejecutables por el simulador, es decir, shaders que utilicen el juego de instrucciones nativo. Su papel es parecido al de un compilador. D3DFixedFuncionEmulator está dedicado a generar shaders D3D que permitan que el pipeline programable Direct3D emule un pipeline de función fija. Es un concepto que 108 puede costar de entender al principio, pero es fundamental y lo trataré con detalle más adelante. 3.2.3 Subsistema D3DProgrammablePipeline El subsistema D3DProgrammablePipeline modela el pipeline de referencia del driver. Es un pipeline de tipo programable, ya que pienso que de esta forma es más sencillo establecer una relación con los elementos del pipeline del simulador, que es 109 puramente programable. Actúa también como interfaz del driver, por lo que recibe también operaciones del pipeline de función fija. Las operaciones del pipeline de función fija las redirige a D3DFixedFunctionEmulator. He modelado entidades que son propias del pipeline Direct3D descrito previamente, por lo que también pueden deducirse sus responsabilidades dentro de las diferentes etapas que implementan. Junto a éstas se encuentran entidades que son propias del simulador. Para distinguirlas éstas últimas llevan el prefijo native, dando a entender que representan la plataforma hardware nativa. Comunicación con el simulador La comunicación con el simulador se realiza a través de las entidades nativas, que constituyen un pequeño subsistema orientado a objetos que envuelve al GPUDriver, la capa inferior. El NativeDevice representa a la GPU del simulador, recibe los comandos de alto nivel y da acceso al resto de entidades nativas. Los registros y búferes nativos actúan como representantes de los registros y búferes del simulador. Aprovechando que gran parte de la comunicación pasa por ellos se pueden realizar optimizaciones de cara a un uso eficiente de la GPU. Una de las más sencillas es evitar escrituras de un mismo valor en un registro, comparando el último valor escrito con el actual. Otra optimización sencilla es limitar la parte del búfer que se ha de escribir en la memoria del simulador. Adicionalmente ofrecen servicios de depuración para poder consultar externamente el estado de los registros o la memoria. 110 Interfaz con el exterior Algunas de las entidades modeladas se corresponden exactamente con uno de los interfaces que presenta el API Direct3D, por lo que recibirán las llamadas de la traza cuando las ejecute el reproductor. Esta interfaz se define a través de clases virtuales puras C++ definidas en los ficheros de cabecera del API, de las que heredan las entidades. Para tratar el requerimiento de portabilidad se han creado versiones portables de estos ficheros. La versión para Linux de estos ficheros incluye la declaración de todas las clases y estructuras propias del sistema operativo Windows que utiliza Direct3D. Implementación de streaming básico Implementación inicial Llamadas D3D Intefaces no implementados Interfaces implementados VertexDeclaration 1 VertexDeclaration 1 VertexDeclaration 2 VertexBuffer 1 VertexBuffer 1 VertexBuffer 2 Buffer 1 Buffer 2 VertexBuffer 3 IndexBuffer 1 Buffer 3 Device 1 VertexShader 1 Interfaces no implementados PixelShader 1 Buffer común IndexBuffer 1 Texture 1 VertexShader 1 Surface 1 PixelShader 1 Buffer común Device 1 Texture 1 Surface 1 Una dificultad de implementación es que toda la interfaz ha de estar presente aunque sólo una parte esté implementada. Por este motivo parto como base de una implementación inicial donde todos los interfaces tienen métodos válidos, aunque estén vacíos o semivacíos. Esta interfaz no operativa tiene cientos de métodos por lo que, al igual que con el reproductor de trazas, recurro al uso de código generado automáticamente. Otro problema similar al del reproductor es que hay que dar direcciones válidas para los objetos interfaz. Este aspecto lo soluciono mediante la creación de una única instancia para cada interfaz no implementada. Por otro lado creo un búfer común que sirva para simular que los recursos tienen datos internamente. 111 Patrón singleton Para los objetos de instancia única, que utilizo para la interfaz del driver y también entre sistemas, utilizo el patrón de diseño singleton. Los objetos Singleton disponen de un constructor privado y mantienen una sola instancia de sí mismos. Un método de clase sirve para acceder a esta instancia única. Relación entre el estado Direct3D y el estado del simulador. Cada registro nativo representa un estado del pipeline del simulador, es decir, un parámetro que determina cómo se realizará el rendering. Los atributos de las entidades propias de Direct3D se corresponden a estados del pipeline Direct3D. La idea es que el pipeline del simulador se mantenga en un estado semánticamente equivalente al del pipeline Direct3D. Para conseguirlo cada entidad propia de Direct3D asigna valores a un grupo de registros nativos. Modos de culling en Direct3D D3DCULL_NONE Modos de culling del simulador CULL_NONE D3DCULL_CW CULL_FRONT D3DCULL_CCW CULL_BACK Semántica Dejar pasar todas las primitivas. Descartar las primitivas que miren al observador. Descartar las primitivas que no miren al observador. Hay casos sencillos en que la correspondencia entre el estado Direct3D y el del simulador es directa. Por ejemplo los modos de culling disponibles en Direct3D tienen un modo semánticamente equivalente en el simulador. En este caso mantener el 112 estado equivalente significa asignar el valor correspondiente en el registro que controla el modo de culling en el simulador. Los casos más complicados se dan cuando no se da esta correspondencia directa. Es el caso de la etapa de streaming del simulador, que presenta unos streams más sencillos que los de Direct3D. En la figura el VertexBuffer contiene naturales para el color y reales para la posición. En Direct3D se puede configurar un único stream para recuperar los vértices, ya que los streams pueden leer valores de tipos distintos sin restricciones. Sin embargo los streams del simulador no tienen esa capacidad. Por este motivo una vez se conoce la distribución de los componentes del vértice es necesario relacionar varios streams nativos, uno por tipo de datos, a un solo stream de Direct3D. Esto justifica la relación uno a varios que existe entre la entidad Stream y NativeStream. De esta forma, en nuestro ejemplo, un stream nativo se utiliza para color y otro para posición. Recursos Direct3D y búferes del simulador Mientras que externamente el API Direct3D expone los recursos en un formato independiente del hardware cómodo para los programadores, internamente cada hardware tiene su propia manera de almacenar los recursos. Por este motivo en el driver las entidades que modelan recursos de Direct3D realizan una conversión al formato que usa el simulador cuando los almacenan en el búfer nativo. 113 Surface NativeBuffer Formato Direct3D (Lineal) Formato nativo (Ordenación de Morton) Un ejemplo de estas conversiones de formato son las texturas. La entidad Surface, cuando modela un nivel de mipmap de una textura, expone los téxeles siguiendo una ordenación lineal mientras que ha de almacenarlas para el simulador en un orden especial. Esta ordenación especial, conocida como ordenación de Morton está diseñada para favorecer la localidad espacial en las cachees internas del simulador. La razón de que 114 funcione es que el acceso a una textura se realiza para un área rectangular, en orden de Morton es más probable encontrar los téxeles próximos en memoria. Los shaders de Direct3D, aunque no se consideran recursos en un sentido estricto, tienen la tarea de conversión más compleja: Convertir un shader Direct3D en un shader nativo. Por este motivo estas clases delegan en el subsistema dedicado a la traducción de shaders. Depuración interactiva En el diseño de la mayoría de entidades se incluyen operaciones específicas para la depuración, que permiten examinar su estado. 115 Para consultar esta información de forma interactiva he implementado una sencilla interfaz de usuario. Ésta permite controlar la reproducción de la traza, visualizar el historial de llamadas Direct3D, visualizar los comandos recibidos por la GPU así como el estado de los registros y la memoria, ofreciendo varias vistas para ésta última. Un ejemplo notable es que podemos consultar el código ensamblador del vertex shader que se está ejecutando. Mediante esta aplicación se detectan errores con facilidad y el ahorro de tiempo de desarrollo es considerable. 116 3.2.4 Subsistema D3DShaderTranslation shader D3D shader traducido vs_3_0 dcl_position v3 dcl_position o8 add r7, v3, c2 m4x4 r6, r7, c5 mov o8, r6 add t0, i0, c0 dp4 t1.x, t0, c1 dp4 t1.y, t0, c2 dp4 t1.z, t0, c3 dp4 t1.w, t0, c4 mov o0, t1 end En Direct3D los shaders asumen un juego de instrucciones y una disponibilidad de registros que no ha de corresponderse forzosamente con las capacidades de la arquitectura en que se ejecute. La traducción de un shader de Direct3D consiste en crear un shader equivalente pero que utilice el juego de instrucciones y registros7 nativo, en este caso los disponibles en las shader units del simulador. Propiamente un traductor debería hacer comprobaciones de tipo y comprobaciones de semántica. En otras palabras se verificaría que el programa está correctamente escrito. Sin embargo los shaders que traduciremos pertenecen a aplicaciones que en su diseño realizaron este tipo de comprobación y uno de nuestros criterios es asumimos que sólo tratamos con trazas correctas. 7 Cuando hablo de shaders “registro” se refiere a los registros de las shader units del simulador. 117 Proceso de traducción El primer aspecto de la traducción son las instrucciones. Hablamos de soporte nativo cuando tenemos una instrucción en el simulador con la misma funcionalidad que la instrucción del shader Direct3D. Cuando no se dispone de dicha instrucción pero sí de una serie de instrucciones nativas que realizan la misma función lo calificamos como soporte emulado. 118 El segundo aspecto son los registros. Una forma sencilla de realizar la traducción es asignar según vayan siendo necesarios registros nativos del simulador para que jueguen el papel de los registros del shader Direct3D, en una correspondencia uno a uno. Declaración del shader traducido Entrada position i0 Salida position o0 Constantes c2 c5 c0 c1 Por último el traductor ha de adjuntar al código traducido la declaración de parte de las asignaciones de registros que ha realizado. Esto es de utilidad para las etapas del pipeline sepan dónde espera el driver que se le provean los datos de entrada y dónde va a dejar los datos de salida. 119 En el ejemplo, el registro constante D3D c2 está asignado al registro constante del simulador c0. Esto significa que si la aplicación invoca al API Direct3D para modificar el valor de la constante D3D c2 la clase responsable de la asignación ha de asignar el valor al registro del simulador c0. Esta clase, por tanto, ha de disponer de un mecanismo para conocer la asignación de registros del shader traducido. En el caso los registros de entrada basta con declarar qué componente del vértice almacenan, pues esta es la información que necesita la etapa de streaming para saber en qué registro tiene que asignar las diferentes componentes para cada vértice. Tratamiento de versiones Sin ser complicado en su implementación este subsistema no ha sido fácil de especificar. Para entender mejor la tarea de especificación de este subsistema es necesario entrar en detalle en aspectos detallados del modelo de shading de Direct3D. Hay que tener en cuenta que aparte de la clasificación en dos tipos de shaders de Direct3D, Vertex Shaders y Pixel Shaders, existen varias versiones para cada uno de ellos y, peor aún, no hay restricciones en el uso de diferentes versiones en una misma traza. Cada nueva versión especifica las capacidades que tendrá el procesador de shaders idealizado: Los cambios de versión mayor suponen cambios de arquitectura, incorporar o eliminar instrucciones y tipos de registro. Los cambios de versión menor, con alguna excepción, suponen una diferencia en la cantidad de registros disponibles. Una de las tareas de especificación respecto a la versión mayor consiste en reducir a un único modelo de shader unit los tres modelos existentes. Esto es, cada versión mayor describe una maquina virtual diferente para ejecutar los shaders, pero cada una de estas máquinas es más general que la anterior, por lo que el sistema de traducción trabajará según el último modelo. 120 Estudio de requisitos Observando por un lado el procesador de shaders que describe Direct3D y por otro los procesadores de shaders del simulador vemos que éste último es mucho más sencillo. Es cierto que, en su versión actual, no pueden implementarse todas las capacidades requeridas por el modelo de shader de Direct3D, especialmente las referentes a control de flujo. Sin embargo recordemos que uno de los requerimientos es sólo implementar aquellas características que se utilicen realmente. Por ello se realizó un estudio con los shaders utilizados por las trazas de referencia. Instrucción Abs Add Break break_gt break_lt break_ge break_le break_eq break_ne Crs dcl_position dcl_blendweight dcl_blendindices dcl_normal dcl_psize dcl_texcoord dcl_tangent dcl_binormal dcl_tessfactor dcl_positiont dcl_color dcl_fog dcl_depth dcl_sample dcl_2d dcl_cube dcl_volume Def Defb Defi dp3 dp4 Dst Else Endif Endloop Endrep Exp Exp Frc Total occurrences 142 4816 0 0 0 0 0 0 0 0 672 174 246 444 0 618 374 374 0 0 260 0 0 0 1508 26 0 2272 0 0 10872 5470 0 0 0 0 0 0 0 398 Instruction if if_gt if_lt if_ge if_le if_eq if_ne if label lit log logp loop lrp m3x2 m3x3 m3x4 m4x3 m4x4 mad max min mov mova mul nop nrm pow rcp rep ret rsq setp_gt setp_lt setp_ge setp_le setp_eq setp_ne sge sgn sincos slt sub texldl vs Total occurrences 0 0 0 0 0 0 0 0 0 0 0 0 0 118 0 0 0 0 0 5572 608 484 4946 224 4726 0 1088 78 2088 0 0 1452 0 0 0 0 0 0 46 0 56 72 0 0 0 Estos datos que corresponden a un videojuego comercial son representativos del resto de trazas de referencia. Prácticamente ningún videojuego de referencia utiliza instrucciones de control de flujo. La explicación a estos resultados proviene del método de desarrollo que se utiliza para escribir los shaders. 121 ASM Shader float4 Gold: register(c1); float BlurScale: register(c0); sampler renderTexture: register(s0); const float2 offsets[12] = { -0.326212, -0.405805, -0.840144, -0.073580, -0.695914, 0.457137, -0.203345, 0.620716, 0.962340, -0.194983, 0.473434, -0.480026, 0.519456, 0.767022, 0.185461, -0.893124, 0.507431, 0.064425, 0.896420, 0.412458, -0.321940, -0.932615, -0.791559, -0.597705, }; // Simple blur filter float4 main(float2 texCoord: TEXCOORD0) : COLOR { float4 sum = tex2D(renderTexture, texCoord); for (int i = 0; i < 12; i++){ sum += tex2D(renderTexture, texCoord + BlurScale * offsets[i]); } return Gold * sum / 13; } ps_2_0 def c14, 0.0769230798, 0, 0, 0 dcl t0.xy dcl_2d s0 mov r11.w, c0.x mad r0.xy, r11.w, c2, t0 mad r9.xy, r11.w, c3, t0 mad r8.xy, r11.w, c4, t0 mad r7.xy, r11.w, c5, t0 mad r6.xy, r11.w, c6, t0 mad r5.xy, r11.w, c7, t0 mad r4.xy, r11.w, c8, t0 mad r3.xy, r11.w, c9, t0 mad r2.xy, r11.w, c10, t0 mad r1.xy, r11.w, c11, t0 texld r10, r0, s0 texld r0, t0, s0 texld r9, r9, s0 texld r8, r8, s0 texld r7, r7, s0 texld r6, r6, s0 texld r5, r5, s0 texld r4, r4, s0 texld r3, r3, s0 texld r2, r2, s0 texld r1, r1, s0 add r0, r10, r0 add r0, r9, r0 add r0, r8, r0 add r0, r7, r0 add r0, r6, r0 add r0, r5, r0 add r0, r4, r0 add r0, r3, r0 add r0, r2, r0 add r0, r1, r0 mad r2.xy, r11.w, c12, t0 mad r1.xy, r11.w, c13, t0 texld r2, r2, s0 texld r1, r1, s0 add r0, r0, r2 add r0, r1, r0 mul r0, r0, c1 mul r0, r0, c14.x mov oC0, r0 Los desarrolladores de videojuegos utilizan el lenguaje de alto nivel HLSL, de forma que gran parte del código ensamblador es resultado de este compilador. El compilador de HLSL de Microsoft realiza una serie de optimizaciones que en muchas ocasiones permiten evitar el uso de instrucciones de salto. Los shaders de estos últimos años suelen ser cortos y sencillos, lo que favorece que se apliquen casi siempre las optimizaciones. En el ejemplo el bucle se desenrolla en la serie de instrucciones secuenciales equivalente, esto es posible porque el número de iteraciones se conoce a priori. Otra conclusión de este estudio es confirmar que las aplicaciones utilizan las versiones con una cierta libertad, creando shaders de varias versiones durante una misma ejecución. Incluso pueden utilizar simultáneamente, aunque no es una práctica recomendada, un Vertex Shader y un Pixel Shader de versiones diferentes. 122 Para finalizar el estudio de requisitos asigné una categoría a cada instrucción utilizada por los videojuegos como nativa, emulada o no soportada, según dispusiera de una instrucción equivalente en el simulador, existiera una vía para emularla o esta emulación fuera imposible o desconocida. Análisis del bytecode El código ensamblador de los shaders se recibe como un bytecode. Un primer problema fue determinar su formato. Afortunadamente existe documentación acerca del formato del bytecode. 123 Sin embargo el nivel de detalle requerido es mayor que el que ofrecía en la documentación por lo que se realizó una tarea adicional de análisis hasta determinar completamente el significado de cada bit. Fueron de especial ayuda las utilidades de C++ para el acceso a bits, como los campos de bits. El compilador de D3DX genera el bytecode del shader como una serie de tokens de 32 bits. El primero es siempre un token de versión, tras éste cada instrucción del shader genera un token de instrucción y un número variable de tokens de parámetro, según 124 su sintaxis. Existen tipos adicionales de tokens, por ejemplo los relacionados con almacenar comentarios. Representación intermedia El método elegido para la traducción es típico dentro de la construcción de compiladores y traductores. Considerando los tokens como unidades léxicas se construye un abstract syntax tree un tipo de árbol cuyos nodos reflejan la estructura sintáctica de las instrucciones. Este árbol es nuestra representación intermedia (siglas IR, en inglés). Una vez construida la IR se realizan recorridos sobre ella con distintos propósitos, uno de ellos generar el shader traducido. 125 Diseño del traductor Gran parte de las clases del traductor se corresponden a los subtipos de nodos del árbol sintáctico. Probablemente en un futuro se vayan incorporando más subtipos, por este motivo el patrón de diseño que uso es el patrón visitor, que minimiza el impacto de agregar una nueva subclase de nodo. Este patrón establece una clase base para los objetos que realizan los recorridos, en este caso es IRVisitor. 126 El objeto IRBuilder se encarga de crear la IR a partir del bytecode. Para ello se ayuda de una clase auxiliar que almacena la sintaxis de las operaciones para cada versión de vertex shader o pixel shader. IRTranslator realiza el recorrido de la IR generando el código nativo. Internamente mantiene tablas que almacenan los registros de que dispone para la traducción, también según versión y tipo de shader, asignándolos según el proceso de traducción que he descrito. Como he comentado existen varias versiones de pixel shader y vertex shader, sin embargo el tratamiento es bastante homogéneo por lo que no hay subclases para el tratamiento de versiones particulares por separado. IRPrinter ayuda a la depuración del código recorriendo la IR e imprimiendo sus contenidos en una cadena de texto. TranslatedShader es el resultado del proceso de traducción y almacena el código nativo junto con la declaración de registros utilizados. Así mismo incluye información de depuración. Pruebas de uso De cara a la depuración he desarrollado una aplicación de prueba que permite editar shaders Direct3D y examinar el proceso de traducción interactivamente. Al tratarse de 127 un bytecode los editores binarios también son útiles, sin embargo el ciclo de pruebas se acorta al disponer en un único entorno del ensamblador de D3DX, el bytecode y el traductor. Los shaders que utilizo para realizar pruebas de este sistema los obtengo de los videojuegos de referencia. El reproductor de trazas, a través de su extensión de estadísticas, vuelca a ficheros de texto todos los shaders utilizados. 3.2.5 Subsistema FixedFunctionGeneration El pipeline implementado por el simulador es un pipeline programable, como en la mayoría de las GPU’s actuales. La necesidad de flexibilidad que requieren las aplicaciones modernas hace que se opte este pipeline, donde uno puede programar la GPU para obtener gran variedad de efectos. La fixed function, el proceso predeterminado de transformación e iluminación de vértices y multi texturización de píxeles/fragmentos, se sigue soportando por compatibilidad con aplicaciones antiguas. 128 Antiguamente la función fija se soportaba directamente por hardware. Esto significa que había componente físicos especializados en cada tarea: la transformación de modelo, de vista, de perspectiva, las texture stages, etc. Hoy en día los drivers la soportan de forma emulada, programando la GPU con un vertex shader y un pixel shader equivalentes que realicen por software los mismos cálculos que anteriormente se hacían por hardware. Éste es el método que seguiremos para soportar la fixed function de Direct3D. La función fija se activa cuando no se provee el vertex shader o el pixel shader, en estos casos el driver Direct3D utilizará su propio vertex shader o pixel shader. Una implicación es que se puede utilizar la función fija en una etapa y en la otra la versión programable. 129 Siguiendo el requisito de implementar sólo lo que se utilice nuestra implementación es muy limitada, ya que en el momento actual las aplicaciones tan sólo utilizan la función fija para elementos muy concretos y sencillos, por ejemplo menús y marcadores. Los elementos que soportaremos serán parte de los expuestos en el apartado de función fija: Transformación de modelo, vista y perspectiva, iluminación con los tres tipos de luces mediante el modelo de blinn-phong y Texture Stages que implementen las operaciones básicas. Una de las primeras decisiones es si el shader equivalente será un shader Direct3D o directamente un shader nativo. He optado por realizar un shader Direct3D por haber documentación disponible sobre la emulación de función fija y también porque de este modo es más fácil que interoperen las etapas de procesamiento de vértices y píxeles cuando una trabaje en modo programable y la otra con función fija. Respecto a la versión generaremos un vertex shader y un píxel shader del modelo 3, por ser el más general y claro. Lógicamente, evitaremos utilizar instrucciones que no se soporten en el simulador, ya que el shader pasará por el proceso de traducción como cualquier otro shader D3D. Generación de shaders de función fija Recordemos que la función fija es parametrizable, es decir, pueden establecerse valores diferentes manteniendo el mismo proceso, pensemos por ejemplo en un cambio de matriz de proyección. El estado de función fija es el conjunto de valores de todas aquellas variables internas que determinan el comportamiento del pipeline de función fija. Estas variables no sólo comprenden parámetros, sino también otras que 130 determinan el procedimiento a aplicar, por ejemplo como parte del estado de función fija encontramos variables que determinan si se aplicarán o no los cálculos de iluminación. tuple VertexIn position: Vector4F, normal: Vector4F end; tuple VertexOut position: Vector4F, diffuse: Color, specular: Color end; worldViewIT: Matrix4x4; viewIT: Matrix4x4; worldViewProj: Matrix4x4; material: Material; lights: vector[1..8]: Light; ambient: Color; enabledLights: vector[1..8] : Bool; funcion vertex_shader(input: VertexIn) : VertexOut output: VertexOut; P: Vector4F; L: Vector4F; N: Vector4F; V: Vector4F; H: Vector4F; output.position := multiply(input.position, worlViewProj); N := multiply(input.normal, worldViewIT); P := multiply(input.position, worldView); V := - normalize(P); output.diffuse := ambient; output.specular := Color(0); for i := 1 to 8 do if enabledLights[i] then L := mul(matViewIT, - normalize( lights[i].direction ); output.color := output.color + dot(N, L) * material.diffuse; H := normalize( L + V ); output.specular := output.specular + pow(max(0, dot(H, N)), material.power) * lights[i].specular; end end return VertexOut; end Tomemos un ejemplo expresado en algún lenguaje de alto nivel que cubre una etapa de iluminación simplificada para un algoritmo de función fija. En este algoritmo el vertex shader recibe una estructura vértice con una serie de componentes y produce un vértice como resultado con un color difuso y especular resultado de aplicar la 131 iluminación. El algoritmo realiza un bucle para las hasta ocho luces activas y aplica los cálculos propios de una luz direccional en el modelo de Phong-Lambert modificado que usa Direct3D. Tomando este procedimiento como base la función del generador de función fija es similar al problema de la traducción de shaders8 en el sentido de que hemos de implementar un algoritmo expresado en un lenguaje que no podemos ejecutar directamente, sino que hemos de adaptarlo a otro lenguaje más limitado. En este caso disponemos de las instrucciones y registros disponibles los shaders D3D. En este aspecto, el generador actúa como el compilador de HLSL. La primera parte del algoritmo, los componentes del vértice de entrada y salida se corresponden directamente con la declaración del vertex shader. 8 O incluso más al de la generación de PixRunPlayer en C++. 132 Algunas de las variables globales se corresponden a parámetros de la función fija que expresaremos en el shader equivalente mediante constantes. El generador construye una tabla que registra la asignación de las constantes a los parámetros que se han necesitado. Los valores de estas constantes los establecerá el emulador de función fija. Para las variables locales del algoritmo se asignan registros temporales en el shader equivalente. Respecto a las instrucciones de cálculo se genera una serie de instrucciones equivalente, los parámetros se determinan según las tablas de asignación de variables. 133 Instrucciones H := normalize( L + V ); Código con resultados intermedios add r5, r1, r3 dp3 r6.w, r5, r5 rsq r7.w, r6.w mul r4, r7.w, r5 En ocasiones el código equivalente necesita de variables temporales para los cálculos intermedios. En este caso la suma de L y V se almacena en un registro temporal. Estos registros temporales se reservan durante la generación de este segmento de código, sin embargo su valor final no tiene utilidad por lo que se vuelven a estar disponibles para la generación del resto de código. Instrucciones y variables de control de flujo enabledLights: vector[1..8] : bool for i := 1 to 8 do if enabledLights[i] then ... end end Generación condicional de código for(int i = 0; i < 8; i ++) { if(ffstate.enabledLights[i]) { generateCodeLight(i); } } Respecto al control de flujo en el algoritmo no podemos expresarlo directamente en el shader ya que, aunque el shader D3D modelo 3.0 soporta control de flujo el simulador carece de este soporte. Lo que haremos será generar un shader diferente según el estado de las variables globales que afecten al control de flujo, es decir, estas variables pasarán a afectar al comportamiento del generador. El efecto en este caso es que generamos un código secuencial en que el bucle se desenrolla y sólo se genera código para las luces que estén activas. 134 Emulador de función fija El subsistema de emulación de función fija tiene la responsabilidad de mantener el pipeline programable actualizado, con un vertex y píxel shader y los valores de sus constantes, de forma que el efecto sea el equivalente a que se estuviera calculando la función fija. Sus clases principales son: FFStatus almacena todas las variables que determinan el estado de función fija. Hay dos instancias de este estado, una que representa el estado actual y otra que representa el último estado que se ha aplicado al pipeline programable. FFShaderGenerator es el responsable de generar un píxel o un vertex shader de función fija para un estado dado. Implementa el procedimiento de generación descrito y tiene como atributos las tablas de reserva de registros. FFConstantsDeclaration indica al emulador qué constantes se han asociado al los parámetros de función fija. 135 D3DProgrammablePipeline set_render_state(LIGHTING, TRUE) D3DFixedFunctionEmulator set_render_state(LIGHTING, TRUE) ToCommitFFStat CommitedFFStat set_lighting(TRUE) commit() FFShaderGenerator generate_ff_vertex_shader(ToCommitFFState) create_vertex_shader(ffshader) ffshader shader set_vertex_shader(shader) shader Cuando se recibe una llamada que afecta al estado de función fija el pipeline programable la delega sin más en el emulador de función fija. Éste recibe el cambio y lo en el estado actual. A la hora de dibujar, o cuando se decida establecer la configuración del pipeline del simulador, el pipeline programable lo notifica al emulador de función fija. En este momento el emulador evalúa si los cambios del estado requieren un nuevo vertex o píxel shader. En este caso el cambio lo justifica y se genera el código para un nuevo vertex shader. Éste se crea como un vertex shader más en el pipeline programable y se establece como actual. 136 D3DProgrammablePipeline D3DFixedFunctionEmulator set_light(LIGHT_0, params) ToCommitFFStat CommitedFFStat set_light(0, params) commit() FFConstantsDeclaration get_constants(LIGHT_0) set_vs_constant(constants, params) constants En otras ocasiones el cambio de estado sólo afecta a los parámetros de la función fija y no requiere generar un nuevo shader. En el ejemplo se cambian las propiedades de la luz, pero no se requiere un código diferente para tratarla. En estos casos el emulador detecta el cambio, pero se limita a cambiar el valor de las constantes asociadas a la luz, utilizando la declaración de constantes para encontrarlas. 137 Pruebas interactivas El shader que emula la función fija es un shader Direct3d corriente. Por este motivo es posible programar una aplicación que lo utilice con el componente Direct3D de Windows. Esta aplicación permite comparar el frame producido por la función fija real respecto al resultado del shader que la emula. 138 4. Conclusión El trabajo relacionado con la captura y reproducción de trazas se ha completado con éxito. Tanto PIX, una vez determinado su formato de traza, como el software de reproducción y análisis realizan los requerimientos que se esperaba de ellos. El desarrollo ha sido relativamente rápido y actualmente su grado de estabilidad es lo suficientemente grande como para utilizarse con trazas del orden de gigabytes de videojuegos comerciales sin dificultades. Se ha determinado el formato de fichero de traza usado por la aplicación Microsoft PIX. Esto ha supuesto una tarea de ingeniería inversa de un formato binario cerrado. El análisis ha llegado hasta un nivel suficiente como para producir el mismo resultado que la propia aplicación. Se ha construido un reproductor de trazas PIX capaz de procesar individualmente centenares de operaciones Direct3D con sintaxis únicas. El diseño permite que para cada operación se aplique un tratamiento general y un posible tratamiento excepcional. Las operaciones se remiten al API Direct3D, pudiendo ejecutarse sin alteraciones o modificando parámetros para ajustarse a las capacidades del equipo. Así mismo se puede asociar código arbitrario a cualquier operación para el propósito que se desee. El reproductor ha sido probado con trazas de videojuegos comerciales y es portable entre Linux y Windows. Como extensión al reproductor se ha añadido código destinado al análisis de la traza. De esta forma se pueden conocer a priori diversas estadísticas de uso del API Direct3D. Estas estadísticas se procesan con scripts PERL, extrayendo informes tanto textuales como gráficos, mediante la interacción con la aplicación Microsoft Excel. El driver Direct3D ha superado la primera fase de su desarrollo. En esta fase el énfasis ha sido delimitar con precisión las especificaciones y estudiar las tecnologías implicadas. Se ha estudiado el API Direct3D hasta entender su funcionalidad con la profundidad necesaria para realizar una implementación. Esto ha supuesto consultar documentación especializada en del desarrollo de drivers gráficos adicionalmente a adquirir los conocimientos que se esperan de un programador del API. Por otro lado he estudiado el simulador ATTILA y el funcionamiento de las GPU’s en general. Esto me ha permitido conocer un campo que no he tratado en las asignaturas de gráficos durante la carrera y que está cobrando gran importancia en los últimos años. Así mismo me he familiarizado con el trabajo que realizamos en la línea de investigación de gráficos del Departamento de Arquitectura de Computadores. Dada la extensión del API Direct3D se han delimitado las funcionalidades esenciales a través de un extenso estudio de requisitos utilizando las herramientas de análisis desarrolladas. Se ha estudiado el workload de videojuegos de última generación que guían el desarrollo del driver Direct3D. Dentro de esta fase se han construido varios prototipos que una vez validados con trazas sencillas han servido para comprender el papel del driver Direct3D dentro del proyecto ATTILA. Actualmente se está completando la segunda fase. Esto significa que se ha diseñado una arquitectura cercana a la definitiva y que el diseño de los diferentes subsistemas es suficiente y ampliable. De esta forma en un futuro irá asumiendo nuevas funcionalidades según sean necesarias. Se ha desarrollado una versión programable del pipeline de Direct3D, con representación para cada una de las etapas y funcionalidades esenciales. Este pipeline se comunica directamente con el simulador ATTILA, de forma que éste refleja su estado. Contamos con un traductor capaz de producir shaders que utilizan el juego de instrucciones nativo del simulador a partir de cualquiera de las diversas versiones de shaders de Direct3D. Contamos con un soporte básico para el pipeline de función fija, con un subsistema dedicado a generar shaders Direct3D que imiten el comportamiento una vez aplicados sobre el pipeline programable. Los subsistemas disponen de interfaces gráficos de usuario para realizar pruebas consultando interactivamente datos de depuración. La depuración se ha tenido en cuenta desde el principio por lo que todos los componentes disponen de métodos para su examen. 140 El driver es capaz de ejecutar trazas básicas en su configuración actual y se está trabajando en dar soporte a trazas de nivel medio. Análisis del tiempo empleado y coste económico del proyecto. Tarea Estudio del API Direct3D Análisis del formato de traza PIX Construcción del reproductor y la extensión de análisis Análisis de requerimientos del driver Estudio del simulador ATTILA Prototipos iniciales del driver Estudio de shaders Direct3D Desarrollo del traductor de shaders Construcción del pipeline programable Estudio de la función fija Direct3D Desarrollo del emulador de función fija. Documentación Total Analista 100 60 20 30 80 10 50 10 30 30 20 150 455 Desarrollador Tester 50 30 20 10 30 10 50 60 20 20 40 20 250 110 Esta tabla resume la estimación del tiempo que emplearía un equipo formado por un analista, un desarrollador y una persona encargada de realizar las pruebas. El orden de las tareas es cronológico. Con esta estimación del tiempo podemos hacer una estimación del coste económico. Analista / Hora 50 € Coste analista 22750 € Desarrollador /Hora 40 € Coste desarrollador 10000 € Coste tester 2750 € Tester /Hora 25 € Coste total 35500 € Estimando el salario así como costes indirectos propios de cada tarea podemos estimar el coste total del proyecto. Futuras líneas de trabajo Una vez sentadas las bases con este proyecto el desarrollo del driver Direct3D continuará en la línea de investigación de arquitecturas gráficas. Lógicamente no podemos compararnos en medios con el equipo de desarrollo de Direct3D de Microsoft, por lo que nunca completaremos todas las funcionalidades posibles. Sin embargo confiamos en que soportaremos lo necesario para ejecutar videojuegos comerciales Direct3D en el simulador. La propia API Direct3D está evolucionando y recientemente ha sufrido una renovación importantísima en su última versión, de forma 141 que ya se están haciendo los primeros movimientos para que el simulador ATTILA no se quede atrás. 142 Bibliografía Centro de recursos oficial de DirectX, http://msdn.microsoft.com/directx/ Website oficial del simulador ATTILA http://attila.ac.upc.edu Red de recursos para el desarrollo de videojuegos http://www.gamedev.net Frank D. Luna, Introduction to 3D Game Programming with DirectX 9.0, Wordware, 2003. Victor Moya, ATTILA: A Cycle-Level Execution-Driven Simulator for Modern GPU Architectures, ISPASS, 2006 Ron Foster, Real-Time Shader Programming, Morgan Kauffman, 2003. Sebastien St-Laurent, The Complete HLSL Reference, Paradoxal Press, 2005 Kelly Dempski, Real Time Rendering Tricks and Techniques in DirectX, Thomson Course Technology, 2002 Matthias Wloka, Where is that instruction? How to implement “missing” Vertex Shader Instructions, NVidia Corporation, 2001 http://developer.nvidia.com/object/Implementation_Missing_Instructions.html Jason Mitchell, Shader Models Tutorial GDC 2004, ATI Research, 2004 https://attila.ac.upc.edu/wiki/images/a/af/D3d9_shader_models_tutorial.pps Craig Larman, Applying UML and Patterns – An introduction to object oriented analysis and design and the RUP, Prentice Hall, 2002 Pat Brown, Vertex Program Specification, NVIDIA Corporation, 2004 http://www.opengl.org/registry/specs/NV/fragment_program.txt 143 Mark J. Kilgard, NVidia Vertex Program Specification, NVIDIA Corporation, 2004 http://www.opengl.org/registry/specs/NV/vertex_program.txt Richard Thompson, The Direct3D Graphics Pipeline, no publicado http://www.xmission.com/~legalize/book/index.html Wikipedia contributors, 'Rendering equation', Wikipedia, The Free Encyclopedia, 2007 Wikipedia contributors, 'Blinn–Phong shading model', Encyclopedia, 2007 David Blythe, The Direct3D 10 system, SIGGRAPH 2006 144 Wikipedia, The Free