Otimizações de desempenho (Direct3D 9)
Todos os desenvolvedores que criam aplicativos em tempo real que usam elementos 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
- bancos de dados e de abate
- primitivas em lote
- dicas de iluminação
- de tamanho da textura
- Transformes de Matriz
- usando texturas dinâmicas
- usando vértice dinâmico e buffers de índice
- usando malhas
- de desempenho do buffer Z
Dicas gerais de desempenho
- Limpe somente quando for necessário.
- 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 tiras para reutilizar vértices de triângulo mais cedo ou 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ático 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 dentro do hardware.
- Se o formato de 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
A criação de 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 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 por meio da camada de abstração de hardware (HAL). Quanto mais eficientes 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 com cuidado sobre 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 de iluminação geral, 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 de ponto ou os holofotes. Para luzes direcionais, a direção da luz é fixa e não precisa ser calculada por vértice.
- Os destaques podem ser mais eficientes do que as luzes de ponto, pois a área fora do cone de luz é calculada rapidamente. Se os holofotes são mais eficientes ou não dependem de quanto da sua cena é iluminada pelos holofotes.
- Use o parâmetro de intervalo 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 do alcance.
- A especulação realça quase o dobro do custo de uma luz. Use-os somente quando for necessário. 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 menor for a textura, maior a chance de serem mantidas no cache secundário da CPU principal.
- 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 seu aplicativo usa quatro texturas 128x128, por exemplo, tente garantir que elas usem a mesma paleta e coloquem todas elas em uma textura 256x256. 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 o mundo e as matrizes de exibição definidas para configurar várias estruturas de dados internas. Cada vez que você define um novo mundo ou 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 seu mundo e exibindo matrizes em uma matriz de exibição de mundo que você definiu como a matriz do mundo e definindo a matriz de exibição como a identidade. Mantenha cópias armazenadas em cache do mundo individual e exiba matrizes para que você possa modificar, concatenar e redefinir a matriz mundial 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, verifique o sinalizador de D3DCAPS2_DYNAMICTEXTURES da estrutura D3DCAPS9.
Tenha em mente as seguintes coisas 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 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 vértice dinâmico e buffers de í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 í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 de gráficos faça buffer de comandos de renderização, 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.
O ideal é que os dados de vértice ou índice nunca sejam alterados, 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 de gráficos 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 de buffer antigos. Isso permite que o processador de gráficos continue usando os dados antigos enquanto o aplicativo coloca 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 bloquear 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 de vértice ou índice antigos após a conclusão do processador de gráficos.
O mecanismo de consulta assíncrono é ú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::EndScenee 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 tiras de triângulo indexados. O hardware descobrirá que 95% dos triângulos sucessivos realmente formam tiras e se ajustam adequadamente. Muitos drivers também fazem isso para hardware mais antigo.
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 chamadaID3DXMesh::LockAttributeBuffer. O método ID3DXMesh::Optimize tem uma opção para agrupar os vértices de malha e 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. O tipo de atributo diferencia dados, portanto, se o aplicativo souber que uma malha é classificada por atributo, ele ainda precisa chamar ID3DXMesh::Otimizar 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. Esta ID descreve qual subconjunto de rostos ID3DXBaseMesh::D rawSubset deve desenhar. As 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 a ID 0 for desejado. Esse buffer contém as informações usadas para classificar a malha com base em atributos no ID3DXMesh::Optimize. Se nenhuma tabela de atributo estiver presente, ID3DXBaseMesh::D rawSubset examinará esse buffer para selecionar os rostos do atributo fornecido 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 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 a texturização, garantindo que as cenas sejam renderizadas de frente para trás. Primitivos com buffer z texturizado são pré-teste em relação ao buffer z em uma base de linha de verificação. Se uma linha de verificação estiver oculta por um polígono renderizado anteriormente, o sistema a rejeitará de forma rápida e eficiente. 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 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 buffer z e renderizando a cena de trás para frente.
Tópicos relacionados