Compartir a través de


Optimizaciones de rendimiento (Direct3D 9)

Cada desarrollador que crea aplicaciones en tiempo real que usan gráficos 3D se preocupa por la optimización del rendimiento. En esta sección se proporcionan instrucciones para obtener el mejor rendimiento del código.

Sugerencias generales de rendimiento

  • Solo se borra cuando sea necesario.
  • Minimice los cambios de estado y agrupe los cambios de estado restantes.
  • Use texturas más pequeñas, si puede hacerlo.
  • Dibuje objetos en la escena de delante a atrás.
  • Use tiras de triángulos en lugar de listas y ventiladores. Para obtener un rendimiento óptimo de caché de vértices, organice tiras para reutilizar vértices de triángulo antes, en lugar de más tarde.
  • Degradar correctamente los efectos especiales que requieren una proporción desproporcionada de los recursos del sistema.
  • Pruebe constantemente el rendimiento de la aplicación.
  • Minimice los conmutadores de búfer de vértices.
  • Use búferes de vértices estáticos siempre que sea posible.
  • Use un búfer de vértices estáticos grande por FVF para objetos estáticos, en lugar de uno por objeto.
  • Si la aplicación necesita acceso aleatorio al búfer de vértices en la memoria de AGP, elija un tamaño de formato de vértice que sea un múltiplo de 32 bytes. De lo contrario, seleccione el formato más pequeño adecuado.
  • Dibujar mediante primitivos indizado. Esto puede permitir el almacenamiento en caché de vértices más eficaz dentro del hardware.
  • Si el formato del búfer de profundidad contiene un canal de galería de símbolos, borre siempre los canales de profundidad y galería de símbolos al mismo tiempo.
  • Combine la instrucción del sombreador y la salida de datos siempre que sea posible. Por ejemplo:
    // Rather than doing a multiply and add, and then output the data with 
    //   two instructions:
    mad r2, r1, v0, c0
    mov oD0, r2
    
    // Combine both in a single instruction, because this eliminates an  
    //   additional register copy.
    mad oD0, r1, v0, c0 
    

Bases de datos y selección

La creación de una base de datos confiable de los objetos en su mundo es clave para un excelente rendimiento en Direct3D. Es más importante que mejoras en la rasterización o el hardware.

Debe mantener el recuento de polígonos más bajo que pueda administrar. Diseñe para un recuento de polígonos bajo mediante la creación de modelos de polígono bajo desde el principio. Agregue polígonos si puede hacerlo sin sacrificar el rendimiento más adelante en el proceso de desarrollo. Recuerde que los polígonos más rápidos son los que no dibujas.

Primitivos de procesamiento por lotes

Para obtener el mejor rendimiento de representación durante la ejecución, intente trabajar con primitivos en lotes y mantenga el número de cambios de estado de representación lo más bajo posible. Por ejemplo, si tiene un objeto con dos texturas, agrupe los triángulos que usan la primera textura y siga con el estado de representación necesario para cambiar la textura. A continuación, agrupe todos los triángulos que usan la segunda textura. Se llama a la compatibilidad de hardware más sencilla para Direct3D con lotes de estados de representación y lotes de primitivos a través de la capa de abstracción de hardware (HAL). Cuanto más eficaz sean las instrucciones se procesan por lotes, se realizan menos llamadas HAL durante la ejecución.

Sugerencias de iluminación

Dado que las luces agregan un costo por vértice a cada fotograma representado, puede mejorar significativamente el rendimiento al tener cuidado de cómo los usa en la aplicación. La mayoría de las sugerencias siguientes derivan de la máxima, "el código más rápido es el código que nunca se llama".

  • Use tantas fuentes de luz como sea posible. Para aumentar el nivel de iluminación general, por ejemplo, use la luz ambiente en lugar de agregar una nueva fuente de luz.
  • Las luces direccionales son más eficientes que las luces puntuales o los focos de luz. Para las luces direccionales, la dirección a la luz es fija y no es necesario calcularla por vértice.
  • Los focos de luz pueden ser más eficientes que las luces puntuales, ya que el área fuera del cono de la luz se calcula rápidamente. Tanto si los focos son más eficaces como si no dependen de la cantidad de la escena iluminada por el contenido destacado.
  • Use el parámetro range para limitar las luces solo a las partes de la escena que necesita para iluminar. Todos los tipos de luz salen bastante temprano cuando están fuera del alcance.
  • Resaltado especular casi doble el costo de una luz. Úselos solo cuando deba. Establezca el estado de representación D3DRS_SPECULARENABLE en 0, el valor predeterminado, siempre que sea posible. Al definir materiales, debe establecer el valor de potencia especular en cero para desactivar los resaltados especulares para ese material; simplemente establecer el color especular en 0,0,0 no es suficiente.

Tamaño de textura

El rendimiento de la asignación de texturas depende en gran medida de la velocidad de la memoria. Hay varias maneras de maximizar el rendimiento de la memoria caché de las texturas de la aplicación.

  • Mantenga las texturas pequeñas. Cuantos más pequeños sean las texturas, mejor probabilidad de mantenerse en la memoria caché secundaria de la CPU principal.
  • No cambie las texturas por primitivo. Intente mantener los polígonos agrupados en orden de las texturas que usan.
  • Use texturas cuadradas siempre que sea posible. Texturas cuyas dimensiones son 256x256 son las más rápidas. Si la aplicación usa cuatro texturas de 128 x 128, por ejemplo, intente asegurarse de que usan la misma paleta y colóquelas en una textura de 256 x 256. Esta técnica también reduce la cantidad de intercambio de texturas. Por supuesto, no debe usar texturas de 256 x 256 a menos que la aplicación requiera tanto texturing porque, como se mencionó, las texturas deben mantenerse lo más pequeñas posible.

Transformaciones de matriz

Direct3D usa las matrices de mundo y vista establecidas para configurar varias estructuras de datos internas. Cada vez que establece una nueva matriz de mundo o vista, el sistema vuelve a calcular las estructuras internas asociadas. Establecer estas matrices con frecuencia (por ejemplo, miles de veces por fotograma) requiere mucho tiempo de cálculo. Puede minimizar el número de cálculos necesarios mediante la concatenación de las matrices de mundo y vista en una matriz de vista global establecida como matriz de mundo y, a continuación, estableciendo la matriz de vista en la identidad. Mantenga las copias almacenadas en caché de matrices de vista y mundo individuales para que pueda modificar, concatenar y restablecer la matriz del mundo según sea necesario. Para mayor claridad en esta documentación, los ejemplos de Direct3D rara vez emplean esta optimización.

Uso de texturas dinámicas

Para averiguar si el controlador admite texturas dinámicas, compruebe la marca D3DCAPS2_DYNAMICTEXTURES de la estructura D3DCAPS9 .

Tenga en cuenta lo siguiente al trabajar con texturas dinámicas.

  • No se pueden administrar. Por ejemplo, su grupo no puede ser D3DPOOL_MANAGED.
  • Las texturas dinámicas se pueden bloquear, incluso si se crean en D3DPOOL_DEFAULT.
  • D3DLOCK_DISCARD es una marca de bloqueo válida para texturas dinámicas.

Es recomendable crear solo una textura dinámica por formato y posiblemente por tamaño. No se recomiendan mapas mip dinámicos, cubos y volúmenes debido a la sobrecarga adicional en el bloqueo de cada nivel. En el caso de los mapas mip, solo se permite D3DLOCK_DISCARD en el nivel superior. Todos los niveles se descartan bloqueando solo el nivel superior. Este comportamiento es el mismo para volúmenes y cubos. En el caso de los cubos, el nivel superior y la cara 0 están bloqueados.

El pseudocódigo siguiente muestra un ejemplo de uso de una textura dinámica.

DrawProceduralTexture(pTex)
{
    // pTex should not be very small because overhead of 
    //   calling driver every D3DLOCK_DISCARD will not 
    //   justify the performance gain. Experimentation is encouraged.
    pTex->Lock(D3DLOCK_DISCARD);
    <Overwrite *entire* texture>
    pTex->Unlock();
    pDev->SetTexture();
    pDev->DrawPrimitive();
}

Uso de vértices dinámicos e búferes de índice

Bloquear un búfer de vértices estático mientras el procesador de gráficos usa el búfer puede tener una penalización significativa del rendimiento. La llamada de bloqueo debe esperar hasta que el procesador de gráficos termine de leer los datos de vértices o índices del búfer para poder volver a la aplicación que realiza la llamada, un retraso significativo. Bloquear y, a continuación, representar desde un búfer estático varias veces por fotograma también impide que el procesador de gráficos almacene en búfer los comandos de representación, ya que debe finalizar los comandos antes de devolver el puntero de bloqueo. Sin comandos almacenados en búfer, el procesador de gráficos permanece inactivo hasta que la aplicación haya terminado de rellenar el búfer de vértices o el búfer de índice y emite un comando de representación.

Lo ideal es que los datos de vértices o índices nunca cambien, pero esto no siempre es posible. Hay muchas situaciones en las que la aplicación necesita cambiar los datos de vértices o de índice de cada fotograma, quizás incluso varias veces por fotograma. En estas situaciones, se debe crear el búfer de vértices o índices con D3DUSAGE_DYNAMIC. Esta marca de uso hace que Direct3D optimice para las operaciones de bloqueo frecuentes. D3DUSAGE_DYNAMIC solo es útil cuando el búfer se bloquea con frecuencia; los datos que permanecen constantes deben colocarse en un búfer estático de vértices o índices.

Para recibir una mejora del rendimiento al usar búferes de vértices dinámicos, la aplicación debe llamar a IDirect3DVertexBuffer9::Lock o IDirect3DIndexBuffer9::Lock con las marcas adecuadas. D3DLOCK_DISCARD indica que la aplicación no necesita mantener los datos antiguos de vértices o índices en el búfer. Si el procesador de gráficos sigue usando el búfer cuando se llama al bloqueo con D3DLOCK_DISCARD, se devuelve un puntero a una nueva región de memoria en lugar de los datos antiguos del búfer. Esto permite que el procesador de gráficos siga usando los datos antiguos mientras la aplicación coloca los datos en el nuevo búfer. No se requiere ninguna administración de memoria adicional en la aplicación; el búfer antiguo se reutiliza o destruye automáticamente cuando el procesador de gráficos finaliza con él. Tenga en cuenta que bloquear un búfer con D3DLOCK_DISCARD siempre descarta todo el búfer, especificando un campo de tamaño limitado o desplazamiento distinto de cero no conserva la información en áreas desbloqueadas del búfer.

Hay casos en los que la cantidad de datos que la aplicación necesita almacenar por bloqueo es pequeña, como agregar cuatro vértices para representar un sprite. D3DLOCK_NOOVERWRITE indica que la aplicación no sobrescribirá los datos que ya están en uso en el búfer dinámico. La llamada de bloqueo devolverá un puntero a los datos antiguos, lo que permite a la aplicación agregar nuevos datos en regiones sin usar del búfer de índices o vértices. La aplicación no debe modificar los vértices ni los índices usados en una operación de dibujo, ya que el procesador de gráficos todavía podría estar en uso. A continuación, la aplicación debe usar D3DLOCK_DISCARD después de que el búfer dinámico esté lleno para recibir una nueva región de memoria, descartando los datos antiguos de vértices o índices una vez finalizado el procesador de gráficos.

El mecanismo de consulta asincrónica es útil para determinar si el procesador de gráficos sigue usando vértices. Emita una consulta de tipo D3DQUERYTYPE_EVENT después de la última llamada drawPrimitive que usa los vértices. Los vértices ya no se usan cuando IDirect3DQuery9::GetData devuelve S_OK. Bloquear un búfer con D3DLOCK_DISCARD o sin marcas siempre garantizará que los vértices se sincronizan correctamente con el procesador de gráficos, pero el uso de bloqueo sin marcas incurrirá en la penalización de rendimiento descrita anteriormente. Otras llamadas API como IDirect3DDevice9::BeginScene, IDirect3DDevice9::EndScene e IDirect3DDevice9::P resent no garantizan que el procesador de gráficos haya terminado de usar vértices.

A continuación se muestran formas de usar búferes dinámicos y las marcas de bloqueo adecuadas.

    // USAGE STYLE 1
    // Discard the entire vertex buffer and refill with thousands of vertices.
    // Might contain multiple objects and/or require multiple DrawPrimitive 
    //   calls separated by state changes, etc.
 
    // Determine the size of data to be moved into the vertex buffer.
    UINT nSizeOfData = nNumberOfVertices * m_nVertexStride;
 
    // Discard and refill the used portion of the vertex buffer.
    CONST DWORD dwLockFlags = D3DLOCK_DISCARD;
    
    // Lock the vertex buffer.
    BYTE* pBytes;
    if( FAILED( m_pVertexBuffer->Lock( 0, 0, &pBytes, dwLockFlags ) ) )
        return false;
    
    // Copy the vertices into the vertex buffer.
    memcpy( pBytes, pVertices, nSizeOfData );
    m_pVertexBuffer->Unlock();
 
    // Render the primitives.
    m_pDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, nNumberOfVertices/3)
    // USAGE STYLE 2
    // Reusing one vertex buffer for multiple objects
 
    // Determine the size of data to be moved into the vertex buffer.
    UINT nSizeOfData = nNumberOfVertices * m_nVertexStride;
 
    // No overwrite will be used if the vertices can fit into 
    //   the space remaining in the vertex buffer.
    DWORD dwLockFlags = D3DLOCK_NOOVERWRITE;
    
    // Check to see if the entire vertex buffer has been used up yet.
    if( m_nNextVertexData > m_nSizeOfVB - nSizeOfData )
    {
        // No space remains. Start over from the beginning 
        //   of the vertex buffer.
        dwLockFlags = D3DLOCK_DISCARD;
        m_nNextVertexData = 0;
    }
    
    // Lock the vertex buffer.
    BYTE* pBytes;
    if( FAILED( m_pVertexBuffer->Lock( (UINT)m_nNextVertexData, nSizeOfData, 
               &pBytes, dwLockFlags ) ) )
        return false;
    
    // Copy the vertices into the vertex buffer.
    memcpy( pBytes, pVertices, nSizeOfData );
    m_pVertexBuffer->Unlock();
 
    // Render the primitives.
    m_pDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 
               m_nNextVertexData/m_nVertexStride, nNumberOfVertices/3)
 
    // Advance to the next position in the vertex buffer.
    m_nNextVertexData += nSizeOfData;

Uso de mallas

Puede optimizar las mallas mediante triángulos indizado de Direct3D en lugar de franjas de triángulos indizado. El hardware descubrirá que el 95 % de los triángulos sucesivos forman bandas y se ajustan en consecuencia. Muchos controladores lo hacen también para hardware más antiguo.

Los objetos de malla D3DX pueden tener cada triángulo, o cara, etiquetados con un DWORD, denominado atributo de esa cara. La semántica de DWORD está definida por el usuario. D3DX los usa para clasificar la malla en subconjuntos. La aplicación establece atributos por cara mediante la llamada ID3DXMesh::LockAttributeBuffer . El método ID3DXMesh::Optimize tiene una opción para agrupar los vértices de malla y caras en atributos mediante la opción D3DXMESHOPT_ATTRSORT. Cuando esto se hace, el objeto mesh calcula una tabla de atributos que la aplicación puede obtener llamando a ID3DXBaseMesh::GetAttributeTable. Esta llamada devuelve 0 si la malla no está ordenada por atributos. No hay ninguna manera de que una aplicación establezca una tabla de atributos porque la genera el método ID3DXMesh::Optimize . El criterio de ordenación de atributos es confidencial, por lo que si la aplicación sabe que una malla está ordenada por atributos, debe llamar a ID3DXMesh::Optimize para generar la tabla de atributos.

En los temas siguientes se describen los distintos atributos de una malla.

Identificador de atributo

Un identificador de atributo es un valor que asocia un grupo de caras a un grupo de atributos. Este identificador describe qué subconjunto de caras debe dibujar ID3DXBaseMesh::D rawSubset . Los identificadores de atributo se especifican para las caras del búfer de atributos. Los valores reales de los identificadores de atributo pueden ser cualquier cosa que se ajuste a 32 bits, pero es habitual usar 0 a n, donde n es el número de atributos.

Búfer de atributos

El búfer de atributos es una matriz de DWORD (una por cara) que especifica en qué grupo de atributos pertenece cada cara. Este búfer se inicializa en cero al crear una malla, pero lo rellenan las rutinas de carga o el usuario debe rellenarlo si se desea más de un atributo con el identificador 0. Este búfer contiene la información que se usa para ordenar la malla en función de los atributos de ID3DXMesh::Optimize. Si no hay ninguna tabla de atributos, ID3DXBaseMesh::D rawSubset examina este búfer para seleccionar las caras del atributo especificado que se va a dibujar.

Tabla de atributos

La tabla de atributos es una estructura propiedad y mantenida por la malla. La única manera de generar una es llamar a ID3DXMesh::Optimize con la ordenación de atributos o una optimización más segura habilitada. La tabla de atributos se usa para iniciar rápidamente una llamada primitiva de dibujo única a ID3DXBaseMesh::D rawSubset. El único uso es que las mallas que progresan también mantienen esta estructura, por lo que es posible ver qué caras y vértices están activas en el nivel actual de detalle.

Rendimiento del búfer Z

Las aplicaciones pueden aumentar el rendimiento al usar z-buffering y texturing asegurándose de que las escenas se representan de delante a atrás. Las primitivas con búfer z con textura se prueban previamente en el búfer z en una línea de examen. Si un polígono representado previamente oculta una línea de examen, el sistema lo rechaza de forma rápida y eficaz. El almacenamiento en búfer Z puede mejorar el rendimiento, pero la técnica es más útil cuando una escena dibuja los mismos píxeles más de una vez. Esto es difícil de calcular exactamente, pero a menudo puede realizar una aproximación cercana. Si los mismos píxeles se dibujan menos de dos veces, puede lograr el mejor rendimiento desactivando el almacenamiento en búfer z y representando la escena de atrás a delante.

Sugerencias de programación