Partilhar via


Otimizações de desempenho (Direct3D 9)

Todo desenvolvedor que cria aplicativos em tempo real que usam gráficos 3D está preocupado 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 apenas quando precisar.
  • Minimize as alterações de estado e agrupe as alterações de estado restantes.
  • Use texturas menores, se puder.
  • Desenhe objetos em sua cena da frente para trás.
  • Use tiras triangulares em vez de listas e ventiladores. Para um desempenho ideal do cache de vértices, organize tiras para reutilizar vértices triangulares mais cedo, em vez de mais tarde.
  • Degradar graciosamente efeitos especiais que exigem uma parcela desproporcional de recursos do sistema.
  • Teste constantemente o desempenho do seu 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 seu 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 no hardware.
  • Se o formato do buffer de profundidade contiver um canal de estêncil, limpe sempre os canais de profundidade e estêncil ao mesmo tempo.
  • Combine a instrução do 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 
    

Bases de Dados e Abate

Criar um banco de dados confiável dos objetos em seu mundo é a chave 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 puder gerenciar. Projete para uma baixa contagem de polígonos construindo modelos de baixo polígono desde o início. Adicione polígonos se puder fazê-lo sem sacrificar o desempenho mais tarde no processo de desenvolvimento. Lembre-se, os polígonos mais rápidos são aqueles que você não desenha.

Primitivos de lote

Para obter o melhor desempenho de renderização durante a execução, tente trabalhar com primitivos em lotes e mantenha 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 através da camada de abstração de hardware (HAL). Quanto mais eficazmente as instruções são enviadas em lote, menos chamadas HAL são realizadas 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 forma como as usa em seu aplicativo. A maioria das dicas a seguir deriva da máxima, "o código mais rápido é o código que nunca é chamado".

  • Use o menor número 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 as luzes pontuais ou os holofotes. Para luzes direcionais, a direção da luz é fixa e não precisa ser calculada por vértice.
  • Os holofotes podem ser mais eficientes do que as luzes pontuais, 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 bastante cedo quando estão fora de alcance.
  • Especular destaca quase o dobro do custo de uma luz. Use-os apenas quando precisar. Defina o D3DRS_SPECULARENABLE estado de renderização como 0, o valor padrão, sempre que possível. Ao definir materiais, você deve definir o valor de potência especular como zero para desativar os destaques especulares para esse material; apenas definir a cor especular para 0,0,0 não é suficiente.

Tamanho da textura

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

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

Transformações matriciais

O Direct3D usa as matrizes de mundo e exibição definidas para configurar várias estruturas de dados internas. Cada vez que você define uma nova matriz de mundo ou visã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 suas matrizes de mundo e exibição em uma matriz de visão de mundo que você define como a matriz de mundo e, em seguida, definindo a matriz de exibição para a identidade. Mantenha cópias em cache de matrizes de mundo e visualização individuais para que você possa modificar, concatenar e redefinir a matriz mundial conforme necessário. Para maior clareza nesta documentação, os exemplos Direct3D raramente empregam essa otimização.

Usando texturas dinâmicas

Para saber se o condutor suporta texturas dinâmicas, verifique a bandeira D3DCAPS2_DYNAMICTEXTURES da estrutura D3DCAPS9.

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

  • Não podem ser geridos. Por exemplo, a sua piscina não pode ser D3DPOOL_MANAGED.
  • As 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, cubos e volumes dinâmicos não são recomendados devido à sobrecarga adicional no bloqueio de todos os níveis. Para mipmaps, D3DLOCK_DISCARD é permitido apenas 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 uso de 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 gráfico está usando o buffer pode ter uma penalidade de desempenho significativa. A chamada de bloqueio deve aguardar até que o processador gráfico termine de ler dados de vértice ou índice do buffer antes de retornar ao aplicativo de chamada, um atraso significativo. Bloquear e, em seguida, renderizar a partir de um buffer estático várias vezes por quadro também impede que o processador gráfico armazene comandos de renderização em buffer, uma vez que ele deve concluir os comandos antes de retornar o ponteiro de bloqueio. Sem comandos em buffer, o processador gráfico 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 mudariam, no entanto, isso nem sempre é possível. Há muitas situações em que o aplicativo precisa alterar dados de vértice ou índice a cada quadro, talvez até 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 gráfico ainda estiver usando o buffer quando o bloqueio for chamado com D3DLOCK_DISCARD, um ponteiro para uma nova região de memória será retornado em vez dos dados do buffer antigo. Isso permite que o processador gráfico 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 gráfico termina de usá-lo. Observe que bloquear um buffer com D3DLOCK_DISCARD sempre descarta todo o buffer, especificando um deslocamento diferente de zero ou 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 utilizadas 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 gráfico. 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 de vértice ou índice antigos após a conclusão do processador gráfico.

O mecanismo de consulta assíncrona é útil para determinar se os vértices ainda estão em uso pelo processador gráfico. 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 estejam sincronizados corretamente com o processador gráfico, no entanto, usar o bloqueio sem sinalizadores incorrerá na penalidade de desempenho descrita anteriormente. Outras chamadas de API, como IDirect3DDevice9::BeginScene, IDirect3DDevice9::EndScenee IDirect3DDevice9::P resent não garantem que o processador gráfico termine de usar vértices.

Abaixo estão 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ângulos indexados. O hardware descobrirá que 95% dos triângulos sucessivos realmente formam tiras e se ajustam de acordo. Muitos drivers fazem isso para hardware mais antigo também.

Os objetos de malha D3DX podem ter cada triângulo, ou face, marcado com um DWORD, chamado de atributo dessa face. 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 face usando o ID3DXMesh::LockAttributeBuffer chamada. O método ID3DXMesh::Otimize tem uma opção para agrupar os vértices e faces da malha em atributos usando a opção D3DXMESHOPT_ATTRSORT. Quando isso é feito, o objeto mesh 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::Otimize . A classificação de atributo é sensível a dados, portanto, se o aplicativo souber que uma malha é classificada por atributo, ele ainda precisará chamar ID3DXMesh::Otimize para gerar a tabela de atributos.

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

ID do atributo

Um id de atributo é um valor que associa um grupo de faces a um grupo de atributos. Este id descreve qual subconjunto de faces ID3DXBaseMesh::D rawSubset deve desenhar. As ids de atributo são especificadas para as faces no buffer de atributos. Os valores reais dos ids de atributo podem ser qualquer coisa que caiba em 32 bits, mas é comum usar 0 a n onde n é o número de atributos.

Buffer de atributos

O buffer de atributos é uma matriz de DWORDs (um por face) que especifica a qual grupo de atributos cada face 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::Otimize. Se nenhuma tabela de atributos estiver presente, ID3DXBaseMesh::D rawSubset verificará esse buffer para selecionar as faces do atributo determinado 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::Otimize com classificação de atributos 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 progressivas também mantêm essa estrutura, então é possível ver quais faces e vértices estão ativos no nível atual de detalhes.

Desempenho do Z-Buffer

Os aplicativos podem aumentar o desempenho ao usar z-buffering e texturização, garantindo que as cenas sejam renderizadas de frente para trás. As primitivas z-buffered texturizadas são pré-testadas em relação ao z-buffer com base na linha de varredura. Se uma linha de verificação estiver oculta por um polígono renderizado anteriormente, o sistema a rejeitará de forma rápida e eficiente. Z-buffering 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 muitas vezes você pode fazer uma aproximação próxima. Se os mesmos pixels forem desenhados menos de duas vezes, você poderá obter o melhor desempenho desativando o z-buffering e renderizando a cena de trás para frente.

Dicas de Programação