Gerenciando estado e componentes gráficos?

11

Costumo fazer muita otimização prematura ao lidar com gráficos. Existem alguns princípios que sempre tento seguir:

  • Mantenha o número de componentes D3D no mínimo. (Estados de renderização, buffers, shaders, etc.)
  • Somente ligue componentes se for absolutamente necessário. (Não está vinculado, etc.)
  • Especialize os componentes o máximo possível. (Defina apenas as BindFlags necessárias, etc.)

Isso me levou a criar invólucros muito elaborados para gerenciar os componentes criados e o estado atual da tubulação. Isso não apenas consome muito do meu valioso tempo de desenvolvimento, mas também adiciona outra grande camada de complexidade.

E o pior de tudo: nem sei se tudo vale a pena.

Algumas das minhas considerações de otimização já podem ser implementadas em um nível inferior e estou apenas replicando-as, perdendo tempo adicional na CPU. Outras considerações podem ser completamente desnecessárias, pois o efeito no desempenho é insignificante.

Então, minhas perguntas são:

  1. Quais das diretrizes acima são válidas e em que medida devo segui-las?
  2. Como a GPU lida com as mudanças de estado?
  3. O que acontece se eu alterar um estado que nunca é usado? (Nenhuma chamada de empate sendo feita enquanto estiver ativa.)
  4. Quais são as penalidades de desempenho reais para vincular os vários componentes diferentes?
  5. Que outras considerações de desempenho devem ser feitas?

Por favor, não me diga apenas que eu não deveria me importar com desempenho até atingir os limites reais. Embora isso seja obviamente verdade do ponto de vista prático, estou principalmente interessado na teoria. De alguma forma, preciso combater o desejo de criar a estrutura gráfica ideal e acho que não posso fazer isso com a habitual "palestra prematura de otimização".

Gerenciando componentes

Atualmente, estou escrevendo aplicativos DirectX 11 em C # usando o SlimDX como um wrapper gerenciado. É um invólucro de nível muito baixo e minha abstração atual é construída sobre ele.

Existem algumas vantagens óbvias ao usar uma abstração do Direct3D. Configurar o ambiente, carregar shaders, definir constantes e desenhar uma malha é muito mais simples e usa muito menos código. Além disso, como ele gerencia a criação e o descarte da maioria dos componentes, eles podem ser reutilizados automaticamente em todos os lugares e evito quase completamente o vazamento de memória.

  1. Como você geralmente gerencia todos os componentes e recursos gráficos?
  2. Você pode recomendar que os wrappers gerenciados façam algo semelhante ao meu exemplo abaixo?

Aqui está um exemplo da minha implementação atual. Estou muito feliz com a interface. Possui flexibilidade suficiente para minhas necessidades e é muito simples de usar e entender:

// Init D3D environment
var window = new RenderForm();
var d3d = new Direct3D(window, GraphicsSettings.Default);
var graphics = new GraphicsManager(d3d.Device);

// Load assets
var mesh = GeometryPackage.FromFile(d3d, "teapot.gp");
var texture = Texture.FromFile(d3d, "bricks.dds");

// Render states
graphics.SetViewports(new Viewport(0, 0, 800, 600);
graphics.SetRasterizer(wireFrame: false, culling: CullMode.Back);
graphics.SetDepthState(depthEnabled: true, depthWriteEnabled: true);
graphics.SetBlendState(BlendMethod.Transparency);

// Input layout
graphics.SetLayout("effect.fx", "VS", "vs_4_0",
    new InputElement("POSITION", 0, Format.R32G32B32_Float, 0),
    new InputElement("TEXCOORD", 0, Format.R32G32_Float, 0)
);

// Vertex shader
graphics.SetShader(Shader.Vertex, "effect.fx", "VS", "vs_4_0");
graphics.SetConstants(Shader.Vertex, 0, 4, stream => stream.Write(wvpMatrix));

// Pixel shader
graphics.SetShader(Shader.Pixel, "effect.fx", "PS", "ps_4_0");
graphics.SetTexture(Shader.Pixel, 0, texture);
graphics.SetSampler(Shader.Pixel, 0, Sampler.AnisotropicWrap);
graphics.SetConstants(Shader.Pixel, 0, 1, stream => stream.Write(new Color4(1, 0, 1, 0);

d3d.Run(() =>
{
    // Draw and present
    d3d.BackBuffer.Clear(new Color4(1, 0, 0.5f, 1));
    graphics.SetOutput(d3d.BackBuffer);
    graphics.Draw(mesh);
    d3d.Present();
}
Lucius
fonte
8
Para esse tipo de pergunta, eu não daria a palestra "otimização prematura", daria a palestra "alterações de perfil para que você possa ver por si mesmo".
Tetrad
@ Tetrad Tenho quase vergonha de admitir que este é um conselho bastante decente. Eu definitivamente deveria fazer mais perfis.
Lucius
1
Números de perfis são a versão gamedev de "fotos ou isso não aconteceu" Com certeza =)
Patrick Hughes

Respostas:

3

Eu gosto da abordagem de abstração descrita por Hodgman nestes tópicos no gamedev.net:

Ele descreve um sistema de renderização em três camadas:

  1. API de renderização de baixo nível que aceita "comandos", abstraindo não mais do que as diferenças entre APIs gráficas diferentes, como Direct3D 9, Direct3D 11 e OpenGL. Cada "comando" é mapeado para um estado ou chamada de chamada diferente, como vincular um fluxo ou textura de vértice ou desenhar as primitivas.
  2. API que aceita "itens de renderização", que agrupam todos os estados e uma única chamada de desenho necessária para renderizar um determinado objeto, e os classifica e os converte em comandos enviados ao primeiro nível. Um estado de renderização contém uma chamada de empate e uma pilha de "grupos de estados", que agrupam logicamente as alterações de estado. Por exemplo, você teria um grupo de estados para o passe de renderização, um grupo de estados para o material, um grupo de estados para a geometria, um grupo de estados para a instância e assim por diante. Esse nível é responsável por classificar esses itens de renderização para reduzir alterações de estado redundantes e selecionar quaisquer alterações de estado que sejam, de fato, redundantes.
  3. Sistemas de alto nível, como um gráfico de cena ou renderizador da GUI, que enviam itens de renderização para o segundo nível. O importante a ser observado é que esses sistemas não conhecem os algoritmos de classificação de estados ou a API de renderização específica, tornando-os completamente independentes de plataforma. Eles também são fáceis de usar quando as APIs de nível inferior foram implementadas.

Em conclusão, este modelo resolve os dois problemas de uma só vez.

jmegaffin
fonte