Otimizações de desempenho (Direct3D 9)

Todos os desenvolvedores que criam aplicativos em tempo real que usam gráficos 3D estão preocupados com a otimização de desempenho. Esta seção fornece diretrizes para obter o melhor desempenho do seu código.

Dicas gerais de desempenho

  • Limpe somente quando precisar.
  • Minimize as alterações de estado e agrupe as alterações de estado restantes.
  • Use texturas menores, se você puder fazer isso.
  • Desenhe objetos em sua cena de frente para trás.
  • Use faixas de triângulo em vez de listas e ventiladores. Para obter o desempenho ideal do cache de vértice, organize as faixas para reutilizar vértices de triângulo mais cedo, em vez de mais tarde.
  • Degradar normalmente efeitos especiais que exigem uma parcela desproporcional dos recursos do sistema.
  • Teste constantemente o desempenho do aplicativo.
  • Minimize as opções de buffer de vértice.
  • Use buffers de vértice estáticos sempre que possível.
  • Use um buffer de vértice estático grande por FVF para objetos estáticos, em vez de um por objeto.
  • Se o aplicativo precisar de acesso aleatório ao buffer de vértice na memória AGP, escolha um tamanho de formato de vértice que seja um múltiplo de 32 bytes. Caso contrário, selecione o menor formato apropriado.
  • Desenhe usando primitivos indexados. Isso pode permitir um cache de vértice mais eficiente dentro do hardware.
  • Se o formato do buffer de profundidade contiver um canal de estêncil, sempre limpe os canais de profundidade e estêncil ao mesmo tempo.
  • Combine a instrução de sombreador e a saída de dados sempre que possível. Por exemplo:
    // 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 
    

Bancos de dados e abate

Criar um banco de dados confiável dos objetos em seu mundo é fundamental para um excelente desempenho no Direct3D. É mais importante do que melhorias na rasterização ou hardware.

Você deve manter a menor contagem de polígonos que você pode gerenciar. Projete para uma contagem baixa de polígonos criando modelos de baixo polígono desde o início. Adicione polígonos se você puder fazer isso sem sacrificar o desempenho posteriormente no processo de desenvolvimento. Lembre-se, os polígonos mais rápidos são os que você não desenha.

Primitivos de envio em lote

Para obter o melhor desempenho de renderização durante a execução, tente trabalhar com primitivos em lotes e manter o número de alterações de estado de renderização o mais baixo possível. Por exemplo, se você tiver um objeto com duas texturas, agrupe os triângulos que usam a primeira textura e siga-os com o estado de renderização necessário para alterar a textura. Em seguida, agrupe todos os triângulos que usam a segunda textura. O suporte de hardware mais simples para Direct3D é chamado com lotes de estados de renderização e lotes de primitivos por meio da HAL (camada de abstração de hardware). Quanto mais eficazes forem as instruções em lote, menos chamadas HAL serão executadas durante a execução.

Dicas de iluminação

Como as luzes adicionam um custo por vértice a cada quadro renderizado, você pode melhorar significativamente o desempenho tendo cuidado com a maneira como usá-las em seu aplicativo. A maioria das dicas a seguir derivam da máxima: "o código mais rápido é o código que nunca é chamado".

  • Use o mínimo possível de fontes de luz. Para aumentar o nível geral de iluminação, por exemplo, use a luz ambiente em vez de adicionar uma nova fonte de luz.
  • As luzes direcionais são mais eficientes do que luzes de ponto ou holofotes. Para luzes direcionais, a direção para a luz é fixa e não precisa ser calculada por vértice.
  • Os holofotes podem ser mais eficientes do que as luzes de ponto, porque a área fora do cone de luz é calculada rapidamente. Se os holofotes são mais eficientes ou não depende de quanto da sua cena é iluminada pelos holofotes.
  • Use o parâmetro range para limitar suas luzes apenas às partes da cena que você precisa iluminar. Todos os tipos de luz saem bem cedo quando estão fora do alcance.
  • Os realces especular quase dobram o custo de uma luz. Use-os somente quando precisar. Defina o estado de renderização D3DRS_SPECULARENABLE como 0, o valor padrão, sempre que possível. Ao definir materiais, você deve definir o valor de energia especular como zero para desativar os realces especulares desse material; apenas definir a cor especular como 0,0,0 não é suficiente.

Tamanho da textura

O desempenho de mapeamento de textura depende muito da velocidade da memória. Há várias maneiras de maximizar o desempenho de cache das texturas do aplicativo.

  • Mantenha as texturas pequenas. Quanto menores forem as texturas, maior a chance de serem mantidas no cache secundário da CPU main.
  • Não altere as texturas por primitiva. Tente manter polígonos agrupados em ordem das texturas que eles usam.
  • Use texturas quadradas sempre que possível. Texturas cujas dimensões são 256x256 são as mais rápidas. Se o aplicativo usar quatro texturas 128x128, por exemplo, tente garantir que elas usem a mesma paleta e coloque todas elas em uma textura de 256 x 256. Essa técnica também reduz a quantidade de troca de textura. É claro que você não deve usar texturas 256x256, a menos que seu aplicativo exija tanta texturização porque, conforme mencionado, as texturas devem ser mantidas o menor possível.

Transformações de matriz

O Direct3D usa as matrizes de mundo e modo de exibição que você definiu para configurar várias estruturas de dados internas. Cada vez que você define um novo mundo ou uma matriz de exibição, o sistema recalcula as estruturas internas associadas. Definir essas matrizes com frequência - por exemplo, milhares de vezes por quadro - é computacionalmente demorado. Você pode minimizar o número de cálculos necessários concatenando as matrizes de mundo e modo de exibição em uma matriz de exibição de mundo que você define como matriz de mundo, e, em seguida, definindo a matriz de visualização para a identidade. Mantenha cópias em cache das matrizes individuais de mundo e modo de exibição para que você possa modificar, concatenar e restaurar a matriz de mundo conforme necessário. Para maior clareza nesta documentação, os exemplos do Direct3D raramente empregam essa otimização.

Usando texturas dinâmicas

Para descobrir se o driver dá suporte a texturas dinâmicas, marcar o sinalizador D3DCAPS2_DYNAMICTEXTURES da estrutura D3DCAPS9.

Tenha em mente o seguinte ao trabalhar com texturas dinâmicas.

  • Eles não podem ser gerenciados. Por exemplo, seu pool não pode ser D3DPOOL_MANAGED.
  • Texturas dinâmicas podem ser bloqueadas, mesmo que sejam criadas em D3DPOOL_DEFAULT.
  • D3DLOCK_DISCARD é um sinalizador de bloqueio válido para texturas dinâmicas.

É uma boa ideia criar apenas uma textura dinâmica por formato e possivelmente por tamanho. Mipmaps dinâmicos, cubos e volumes não são recomendados devido à sobrecarga adicional no bloqueio de todos os níveis. Para mipmaps, D3DLOCK_DISCARD é permitido somente no nível superior. Todos os níveis são descartados bloqueando apenas o nível superior. Esse comportamento é o mesmo para volumes e cubos. Para cubos, o nível superior e a face 0 estão bloqueados.

O pseudocódigo a seguir mostra um exemplo de como usar uma 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();
}

Usando buffers de vértice dinâmico e índice

Bloquear um buffer de vértice estático enquanto o processador de gráficos está usando o buffer pode ter uma penalidade de desempenho significativa. A chamada de bloqueio deve aguardar até que o processador de gráficos termine de ler dados de vértice ou de índice do buffer antes que ele possa retornar ao aplicativo de chamada, um atraso significativo. Bloquear e renderizar de um buffer estático várias vezes por quadro também impede que o processador gráfico armague comandos de renderização em buffer, pois ele deve concluir comandos antes de retornar o ponteiro de bloqueio. Sem comandos em buffer, o processador de gráficos permanece ocioso até que o aplicativo termine de preencher o buffer de vértice ou o buffer de índice e emita um comando de renderização.

Idealmente, os dados de vértice ou índice nunca seriam alterados, no entanto, isso nem sempre é possível. Há muitas situações em que o aplicativo precisa alterar dados de vértice ou de índice a cada quadro, talvez até mesmo várias vezes por quadro. Para essas situações, o vértice ou buffer de índice deve ser criado com D3DUSAGE_DYNAMIC. Esse sinalizador de uso faz com que o Direct3D otimize para operações de bloqueio frequentes. D3DUSAGE_DYNAMIC só é útil quando o buffer é bloqueado com frequência; os dados que permanecem constantes devem ser colocados em um vértice estático ou buffer de índice.

Para receber uma melhoria de desempenho ao usar buffers de vértice dinâmicos, o aplicativo deve chamar IDirect3DVertexBuffer9::Lock ou IDirect3DIndexBuffer9::Lock com os sinalizadores apropriados. D3DLOCK_DISCARD indica que o aplicativo não precisa manter os dados de vértice ou índice antigos no buffer. Se o processador de gráficos ainda estiver usando o buffer quando lock for chamado com D3DLOCK_DISCARD, um ponteiro para uma nova região de memória será retornado em vez dos dados de buffer antigos. Isso permite que o processador de gráficos continue usando os dados antigos enquanto o aplicativo coloca os dados no novo buffer. Nenhum gerenciamento de memória adicional é necessário no aplicativo; o buffer antigo é reutilizado ou destruído automaticamente quando o processador de gráficos é concluído com ele. Observe que o bloqueio de um buffer com D3DLOCK_DISCARD sempre descarta todo o buffer, especificando um deslocamento diferente de zero ou um campo de tamanho limitado não preserva informações em áreas desbloqueadas do buffer.

Há casos em que a quantidade de dados que o aplicativo precisa armazenar por bloqueio é pequena, como a adição de quatro vértices para renderizar um sprite. D3DLOCK_NOOVERWRITE indica que o aplicativo não substituirá os dados já em uso no buffer dinâmico. A chamada de bloqueio retornará um ponteiro para os dados antigos, permitindo que o aplicativo adicione novos dados em regiões não utilizados do vértice ou buffer de índice. O aplicativo não deve modificar vértices ou índices usados em uma operação de desenho, pois eles ainda podem estar em uso pelo processador de gráficos. Em seguida, o aplicativo deve usar D3DLOCK_DISCARD depois que o buffer dinâmico estiver cheio para receber uma nova região de memória, descartando os dados antigos de vértice ou índice após a conclusão do processador de gráficos.

O mecanismo de consulta assíncrona é útil para determinar se os vértices ainda estão em uso pelo processador de gráficos. Emita uma consulta do tipo D3DQUERYTYPE_EVENT após a última chamada DrawPrimitive que usa os vértices. Os vértices não estão mais em uso quando IDirect3DQuery9::GetData retorna S_OK. Bloquear um buffer com D3DLOCK_DISCARD ou nenhum sinalizador sempre garantirá que os vértices sejam sincronizados corretamente com o processador de gráficos, no entanto, o uso de bloqueio sem sinalizadores incorrerá na penalidade de desempenho descrita anteriormente. Outras chamadas à API, como IDirect3DDevice9::BeginScene, IDirect3DDevice9::EndScene e IDirect3DDevice9::P resent , não garantem que o processador de gráficos seja concluído usando vértices.

Veja abaixo maneiras de usar buffers dinâmicos e os sinalizadores de bloqueio adequados.

    // 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;

Usando malhas

Você pode otimizar malhas usando triângulos indexados direct3D em vez de faixas de triângulo indexadas. O hardware descobrirá que 95% dos triângulos sucessivos realmente formam faixas e se ajustam adequadamente. Muitos drivers também fazem isso para hardware mais antigo.

Os objetos de malha D3DX podem ter cada triângulo, ou rosto, marcado com um DWORD, chamado de atributo desse rosto. A semântica do DWORD é definida pelo usuário. Eles são usados pelo D3DX para classificar a malha em subconjuntos. O aplicativo define atributos por rosto usando a chamada ID3DXMesh::LockAttributeBuffer . O método ID3DXMesh::Optimize tem a opção de agrupar os vértices de malha e os rostos em atributos usando a opção D3DXMESHOPT_ATTRSORT. Quando isso é feito, o objeto de malha calcula uma tabela de atributos que pode ser obtida pelo aplicativo chamando ID3DXBaseMesh::GetAttributeTable. Essa chamada retornará 0 se a malha não for classificada por atributos. Não há como um aplicativo definir uma tabela de atributos porque ela é gerada pelo método ID3DXMesh::Optimize . A classificação de atributo diferencia dados, portanto, se o aplicativo souber que uma malha é classificada por atributo, ele ainda precisará chamar ID3DXMesh::Optimize para gerar a tabela de atributos.

Os tópicos a seguir descrevem os diferentes atributos de uma malha.

ID do atributo

Uma ID de atributo é um valor que associa um grupo de faces a um grupo de atributos. Essa id descreve qual subconjunto de faces ID3DXBaseMesh::D rawSubset deve desenhar. IDs de atributo são especificadas para os rostos no buffer de atributo. Os valores reais das IDs de atributo podem ser qualquer coisa que se ajuste em 32 bits, mas é comum usar 0 a n em que n é o número de atributos.

Buffer de atributo

O buffer de atributo é uma matriz de DWORDs (um por rosto) que especifica em qual grupo de atributos cada rosto pertence. Esse buffer é inicializado como zero na criação de uma malha, mas é preenchido pelas rotinas de carga ou deve ser preenchido pelo usuário se mais de um atributo com id 0 for desejado. Esse buffer contém as informações usadas para classificar a malha com base em atributos em ID3DXMesh::Optimize. Se nenhuma tabela de atributos estiver presente, ID3DXBaseMesh::D rawSubset examinará esse buffer para selecionar os rostos do atributo a ser desenhado.

Tabela de atributos

A tabela de atributos é uma estrutura de propriedade e mantida pela malha. A única maneira de gerar um é chamando ID3DXMesh::Optimize com classificação de atributo ou otimização mais forte habilitada. A tabela de atributos é usada para iniciar rapidamente uma única chamada primitiva de desenho para ID3DXBaseMesh::D rawSubset. O único outro uso é que as malhas em andamento também mantêm essa estrutura, portanto, é possível ver quais rostos e vértices estão ativos no nível atual de detalhes.

Desempenho do buffer Z

Os aplicativos podem aumentar o desempenho ao usar o buffer z e texturização, garantindo que as cenas sejam renderizadas da frente para trás. Buffers z texturizados primitivos são pré-testados em relação ao buffer z em uma base de linha de varredura. Se uma linha de varredura for ocultada por um polígono renderizado anteriormente, o sistema a rejeita com rapidez e eficiência. O buffer Z pode melhorar o desempenho, mas a técnica é mais útil quando uma cena desenha os mesmos pixels mais de uma vez. Isso é difícil de calcular exatamente, mas geralmente, você pode obter uma boa aproximação. Se os mesmos pixels forem desenhados menos de duas vezes, você pode obter o melhor desempenho desativando o buffer z e renderizando a cena de trás para frente.

Dicas de programação