Qual padrão de design melhor se adequa ao gerenciamento de alças para objetos, sem passar alças ou Manager ao redor?

8

Estou escrevendo um jogo em C ++ usando o OpenGL.

Para quem não sabe, com a API do OpenGL, você faz muitas chamadas para coisas como glGenBufferse glCreateShaderetc. Esses tipos de retorno GLuintsão identificadores exclusivos do que você acabou de criar. A coisa que está sendo criada fica na memória da GPU.

Considerando que a memória da GPU às vezes é limitada, você não deseja criar duas coisas iguais quando forem usadas por vários objetos.

Por exemplo, Shaders. Você vincula um programa Shader e, em seguida, possui um GLuint. Quando terminar o Shader, você deve ligar glDeleteShader(ou algo a esse respeito).

Agora, digamos que eu tenha uma hierarquia de classes superficial como:

class WorldEntity
{
public:
    /* ... */
protected:
    ShaderProgram* shader;
    /* ... */
};

class CarEntity : public WorldEntity 
{
    /* ... */
};

class PersonEntity: public WorldEntity
{
    /* ... */
};

Qualquer código que eu já vi exigiria que todos os Construtores ShaderProgram*passassem para ele para serem armazenados no WorldEntity. ShaderProgramé minha classe que encapsula a ligação de a GLuintao estado atual do shader no contexto OpenGL, além de algumas outras coisas úteis que você precisa fazer com o Shaders.

O problema que tenho com isso é:

  • Existem muitos parâmetros necessários para construir um WorldEntity(considere que pode haver uma malha, um sombreador, um monte de texturas etc., todos os quais podem ser compartilhados, para que sejam passados ​​como ponteiros)
  • O que quer que esteja criando as WorldEntitynecessidades para saber o ShaderProgramque precisa
  • Provavelmente, isso requer algum tipo de classe gulp EntityManager que sabe qual instância do que ShaderProgrampassar para diferentes entidades.

Portanto, agora, porque existe uma Managerclasse, as classes precisam se registrar EntityManagerjunto com a ShaderPrograminstância de que precisam, ou eu preciso de um imbecil switchno gerenciador que preciso atualizar para cada novo WorldEntitytipo derivado.

Meu primeiro pensamento foi criar uma ShaderManagerclasse (eu sei, os gerentes são ruins) que passo por referência ou ponteiro para as WorldEntityclasses, para que elas possam criar o ShaderProgramque quiserem, através do ShaderManagere ShaderManagerpossam acompanhar os ShaderPrograms já existentes , para que possam retorne um que já exista ou crie um novo, se necessário.

(Eu poderia armazenar os ShaderPrograms através do hash dos nomes de arquivos do ShaderProgramcódigo fonte real do s)

Então agora:

  • Agora estou passando ponteiros para, em ShaderManagervez de ShaderProgram, então ainda há muitos parâmetros
  • Não preciso de um EntityManager, as próprias entidades saberão em que instância ShaderProgramcriar e ShaderManagermanipularão os ShaderPrograms reais .
  • Mas agora não sei quando é ShaderManagerpossível excluir com segurança um ShaderProgramque ele contém.

Então agora eu adicionei uma contagem de referência à minha ShaderProgramclasse que exclui sua GLuintvia interna glDeletePrograme acabo com ela ShaderManager.

Então agora:

  • Um objeto pode criar o ShaderProgramque precisar
  • Mas agora existem ShaderPrograms duplicados porque não há um gerente externo acompanhando

Finalmente, tomo uma de duas decisões:

1. Classe estática

Um static classque é chamado para criar ShaderPrograms. Ele mantém uma faixa interna de ShaderPrograms com base em um hash dos nomes de arquivos - isso significa que não preciso mais passar ponteiros ou referências a ShaderPrograms ou ShaderManagers ao redor, portanto, menos parâmetros - eles WorldEntitiestêm todo o conhecimento sobre a instância ShaderProgramque desejam criar

Este novo static ShaderManagerprecisa:

  • manter uma contagem do número de vezes que a ShaderProgramé usada e eu ShaderProgramnão faço nenhuma cópia OU
  • ShaderPrograms contam suas referências e só chamam glDeleteProgramseu destruidor quando a contagem é 0AND ShaderManagerperiodicamente verifica se há ShaderProgramuma contagem de 1 e as descarta.

As desvantagens dessa abordagem que vejo são:

  1. Eu tenho classe estática global que pode ser um problema. O contexto do OpenGL precisa ser criado antes da chamada de qualquer glXfunção. Então, potencialmente, um WorldEntitypode ser criado e tente criar um ShaderProgramantes da criação do OpenGL Context, o que resultará em uma falha.

    A única maneira de contornar isso é repassar tudo ao redor como ponteiros / referências, ou ter uma classe GLContext global que pode ser consultada ou manter tudo em uma classe que cria o Contexto na construção. Ou talvez apenas um booleano global IsContextCreatedque possa ser verificado. Mas eu me preocupo que isso me dê um código feio em todo lugar.

    O que eu posso ver é:

    • A grande Engineclasse que tem todas as outras classes ocultas dentro dela para poder controlar a ordem de construção / desconstrução de maneira apropriada. Parece uma grande confusão de código de interface entre o usuário e o mecanismo, como um invólucro sobre um invólucro
    • Uma série de classes "Manager" que controlam instâncias e excluem coisas quando necessário. Isso pode ser um mal necessário?

E

  1. Quando realmente limpar ShaderPrograms fora do static ShaderManager? A cada poucos minutos? Cada loop de jogo? Estou lidando graciosamente com a recompilação de um sombreador no caso em que um ShaderProgramfoi excluído, mas depois um novo WorldEntitysolicita; mas tenho certeza de que há uma maneira melhor.

2. Um método melhor

É isso que estou pedindo aqui

NeomerArcana
fonte
2
O que vem à mente quando você diz "Há muitos parâmetros necessários para construir uma WorldEntity" é que é necessário um tipo de padrão de fábrica para lidar com a conexão. Além disso, não estou dizendo que você necessariamente queira injeção de dependência aqui, mas se você ainda não examinou esse caminho antes, pode achar interessante. Os "gerentes" de que você está falando aqui soam semelhantes aos manipuladores de escopo vitalício.
J Trana
Então, digamos que eu implemente uma classe factory para construir WorldEntitys; Isso não está mudando parte do problema? Porque agora a classe WorldFactory precisa passar para cada WolrdEntity o ShaderProgram correto.
NeomerArcana
Boa pergunta. Muitas vezes, não - e aqui está o porquê. Em muitos casos, você não precisa ter um ShaderProgram específico, ou pode querer mudar qual deles é instanciado, ou talvez queira escrever um teste de unidade com um ShaderProgram completamente simulado. Uma pergunta que eu faria é: realmente importa para essa entidade qual programa de sombreador possui? Em alguns casos, talvez, mas como você está usando um ponteiro ShaderProgram em vez de um ponteiro MySpecificShaderProgram, talvez não. Além disso, o problema do escopo do ShaderProgram agora pode mudar para o nível de fábrica, permitindo alterações entre singletons etc. facilmente.
precisa

Respostas:

4
  1. Um método melhor É o que eu estou pedindo aqui

Pedimos desculpas pela necromancia, mas já vi muitos tropeçando em problemas semelhantes com o gerenciamento de recursos OpenGL, incluindo eu no passado. E muitas das dificuldades que lutei com as quais reconheço em outros vieram da tentação de encerrar e, às vezes, abstrair e até encapsular os recursos de OGL necessários para que uma entidade de jogo analógica fosse renderizada.

E o "caminho melhor" que encontrei (pelo menos um que encerrou minhas lutas particulares lá) foi fazer as coisas do contrário. Ou seja, não se preocupe com os aspectos de baixo nível do OGL ao projetar suas entidades e componentes do jogo e afaste-se de idéias como essas que você Modeldeve armazenar como primitivas de triângulo e vértice na forma de quebra de objetos ou até mesmo abstraindo VBOs.

Preocupações de renderização vs. preocupações de design de jogos

Existem conceitos de nível um pouco mais alto que as texturas de GPU, por exemplo, com requisitos de gerenciamento mais simples, como imagens de CPU (e você precisa deles de qualquer maneira, pelo menos temporariamente, antes de poder criar e vincular uma textura de GPU). A falta de renderização diz respeito a um modelo pode ser suficiente apenas armazenando uma propriedade indicando o nome do arquivo a ser usado para o arquivo que contém os dados para o modelo. Você pode ter um componente "material" que seja de nível superior e mais abstrato e descreva as propriedades desse material que um shader GLSL.

E existe apenas um lugar na base de código relacionado a coisas como shaders e texturas de GPU e contextos VAOs / VBOs e OpenGL, e é a implementação do sistema de renderização . O sistema de renderização pode percorrer as entidades na cena do jogo (no meu caso, passa por um índice espacial, mas você pode entender mais facilmente e começar com um loop simples antes de implementar otimizações como seleção de frustum com um índice espacial) e descobre seus componentes de alto nível, como "materiais" e "imagens" e nomes de arquivos de modelo.

E seu trabalho é pegar os dados de nível superior que não estão diretamente relacionados à GPU e carregar / criar / associar / vincular / usar / desassociar / destruir os recursos OpenGL necessários com base no que ele descobre em cena e no que está acontecendo com o cena. E isso elimina a tentação de usar coisas como singletons e versões estáticas de "gerentes" e o que não, porque agora todo o seu gerenciamento de recursos OGL está centralizado em um sistema / objeto em sua base de código (embora, é claro, você possa decompô-lo em outros objetos encapsulados pelo renderizador para tornar o código mais gerenciável). Também evita naturalmente alguns pontos de tropeço com coisas como tentar destruir recursos fora de um contexto válido do OGL,

Evitando alterações de design

Além disso, oferece muito espaço para respirar para evitar alterações dispendiosas no design central, porque, em retrospectiva, você descobre que alguns materiais exigem várias passagens de renderização (e vários shaders) para renderizar, como uma passagem de dispersão na subsuperfície e shader para materiais de pele, enquanto anteriormente você queria misturar um material com um único shader da GPU. Nesse caso, não há alteração de design dispendiosa nas interfaces centrais usadas por muitas coisas. Tudo o que você faz é atualizar a implementação local do sistema de renderização para lidar com esse caso anteriormente imprevisto quando encontrar propriedades de capa no seu componente de material de nível superior.

A estratégia geral

E essa é a estratégia geral que uso agora, e ela se torna cada vez mais útil, quanto mais complexas são as suas preocupações com renderização. Como desvantagem, requer um trabalho um pouco mais adiantado do que injetar shaders e VBOs nas entidades do jogo e coisas assim, além de associar mais o seu renderizador ao seu mecanismo de jogo específico (ou suas abstrações, embora em troca o nível superior) entidades e conceitos de jogos tornam-se completamente dissociados das preocupações de renderização de baixo nível). E seu renderizador pode precisar de itens como retornos de chamada para notificá-lo quando as entidades forem destruídas, para que possa desassociar e destruir todos os dados associados a ele (você pode usar a contagem regressiva aqui oushared_ptrpara recursos compartilhados, mas apenas localmente dentro do renderizador). E você pode querer uma maneira eficiente de associar e desassociar todos os tipos de renderização de dados a quaisquer entidades em tempo constante (um ECS tende a fornecer isso imediatamente para todos os sistemas, com a forma de associar novos tipos de componentes em tempo real, se você tiver um ECS - caso contrário, não deve ser muito difícil de qualquer maneira) ... mas, de cabeça para baixo, todos esses tipos de coisas provavelmente serão úteis para outros sistemas que não o renderizador.

É certo que a implementação real fica muito mais sutil do que isso e pode embaçar um pouco mais essas coisas, como seu mecanismo pode querer lidar com coisas como triângulos e vértices em outras áreas além da renderização (por exemplo: física pode querer que esses dados detectem colisões ) Mas onde a vida começou a ficar muito mais fácil (pelo menos para mim) foi abraçar esse tipo de inversão de mentalidade e estratégia como ponto de partida.

E projetar um renderizador em tempo real é muito difícil na minha experiência - a coisa mais difícil que eu já projetei (e continuo redesenhando) com as rápidas mudanças de hardware, recursos de sombreamento e técnicas descobertas. Mas essa abordagem elimina a preocupação imediata de quando os recursos da GPU podem ser criados / destruídos, centralizando tudo isso na implementação de renderização, e ainda mais benéfico para mim é que ela mudou o que de outra forma seria caro e as alterações em cascata do projeto (que poderiam se espalhar código que não se preocupe imediatamente com a renderização) apenas na implementação do próprio renderizador. E essa redução no custo da mudança pode resultar em enormes economias com algo que muda nos requisitos a cada ano ou dois tão rapidamente quanto a renderização em tempo real.

Seu exemplo de sombreamento

A maneira como abordo seu exemplo de sombreamento é que não me preocupo com coisas como sombreadores GLSL em coisas como entidades de carro e pessoa. Eu me preocupo com "materiais" que são objetos de CPU muito leves que contêm apenas propriedades que descrevem que tipo de material é (pele, pintura de carro, etc.). No meu caso atual, é um pouco sofisticado, pois tenho um DSEL semelhante ao Unreal Blueprints para programar shaders usando um tipo visual de linguagem, mas os materiais não armazenam identificadores de shader GLSL.

Os ShaderPrograms contam suas referências e chamam apenas glDeleteProgram em seu destruidor quando a contagem é 0 E o ShaderManager verifica periodicamente os ShaderProgram's com uma contagem de 1 e os descarta.

Eu costumava fazer coisas semelhantes quando estava armazenando e gerenciando esses recursos "lá fora no espaço" fora do renderizador, porque minhas primeiras tentativas ingênuas que tentavam destruir diretamente esses recursos em um destruidor geralmente tentavam destruí-los fora de um contexto GL válido (e às vezes eu tentava acidentalmente criá-los em script ou algo assim quando não estava em um contexto válido), então eu precisava adiar a criação e a destruição para casos em que eu pudesse garantir que estava em um contexto válido que levam a projetos de "gerente" semelhantes que você descreve.

Todos esses problemas desaparecem se você estiver armazenando um recurso da CPU em seu lugar e se o renderizador lidar com as preocupações do gerenciamento de recursos da GPU. Não posso destruir um shader OGL em nenhum lugar, mas posso destruir um material da CPU em qualquer lugar e usá shared_ptr-lo facilmente sem problemas.

Quando realmente remover ShaderPrograms do ShaderManager estático? A cada poucos minutos? Cada loop de jogo? Estou lidando graciosamente com a recompilação de um sombreador no caso em que um ShaderProgram foi excluído, mas uma nova WorldEntity solicita; mas tenho certeza de que há uma maneira melhor.

Agora, essa preocupação é realmente complicada, mesmo no meu caso, se você deseja gerenciar com eficiência os recursos da GPU e descarregá-los quando não for mais necessário. No meu caso, posso lidar com cenas massivas e trabalho em efeitos visuais, em vez de jogos em que os artistas podem ter um conteúdo particularmente intenso não otimizado para renderização em tempo real (texturas épicas, modelos que abrangem milhões de polígonos etc.).

É muito útil para o desempenho não apenas evitar renderizá-los quando estiver fora da tela (fora do cenário de visualização), mas também descarregar os recursos da GPU quando não for mais necessário por um tempo (digamos que o usuário não veja algo muito distante) espaço por um tempo).

Portanto, a solução que costumo usar com mais freqüência é o tipo de solução "com registro de data e hora", embora não tenha certeza de como isso é aplicável aos jogos. Quando começo a usar / vincular recursos para renderização (por exemplo: eles passam no teste de seleção de frustum), armazeno o horário atual com eles. Periodicamente, há uma verificação para ver se esses recursos não são usados ​​há algum tempo e, se forem, são descarregados / destruídos (embora os dados originais da CPU usados ​​para gerar o recurso GPU sejam mantidos até que a entidade real que armazena esses componentes seja destruída ou até que esses componentes sejam removidos da entidade). À medida que o número de recursos aumenta e mais memória é usada, o sistema se torna mais agressivo ao descarregar / destruir esses recursos (a quantidade de tempo ocioso permitido por um antigo,

Eu imagino que depende muito do seu design de jogo. Como se você tiver um jogo com uma abordagem mais segmentada com níveis / zonas menores, poderá (e encontrar o tempo mais fácil para manter as taxas de quadros estáveis) carregar todos os recursos necessários para esse nível com antecedência e descarregá-los quando o o usuário passa para o próximo nível. Considerando que, se você tem um jogo de mundo aberto maciço que é fácil dessa maneira, pode ser necessário uma estratégia muito mais sofisticada para controlar quando criar e destruir esses recursos, e pode haver um desafio maior para fazer tudo isso sem gaguejar. No meu domínio VFX, um pequeno problema para as taxas de quadros não é tão importante (eu tento eliminá-las dentro do razoável), já que o usuário não vai acabar com o jogo por causa disso.

Toda essa complexidade no meu caso ainda está isolada no sistema de renderização e, embora eu tenha generalizado classes e código para ajudar a implementá-lo, não há preocupações com contextos de GL válidos e tentações de usar globais ou qualquer coisa assim.

Dragon Energy
fonte
1

Em vez de fazer a contagem de referência na ShaderProgramprópria classe, é melhor delegar isso para uma classe de ponteiro inteligente, como std::shared_ptr<>. Dessa forma, você garante que cada classe tenha apenas um único trabalho a fazer.

Para evitar o esgotamento acidental de seus recursos OpenGL, você pode tornar ShaderProgramnão copiável (construtor de cópias privado / excluído e operador de atribuição de cópia).
Para manter um repositório central de ShaderPrograminstâncias que podem ser compartilhadas, você pode usar um SharedShaderProgramFactory(semelhante ao seu gerente estático, mas com um nome melhor) como este:

class SharedShaderProgramFactory {
private:
  std::weak_ptr<ShaderProgram> program_a;

  std::shared_ptr<ShaderProgram> get_progam_a()
  {
    shared_ptr<ShaderProgram> temp = program_a.lock();
    if (!temp)
    {
      // Requested program does not currently exist, so (re-)create it
      temp = new ShaderProgramA();
      program_a = temp; // Save for future requests
    }
    return temp;
  }
};

A classe de fábrica pode ser implementada como uma classe estática, um Singleton ou uma dependência que é aprovada quando necessário.

Bart van Ingen Schenau
fonte
-3

O Opengl foi projetado como uma biblioteca C e possui as características do software procedural. Uma das regras do opengl que vem de ser uma biblioteca C é assim:

"Quando a complexidade da sua cena aumentar, você terá mais identificadores que precisam ser passados ​​pelo código"

Esta é uma característica da API opengl. Basicamente, assume que todo o seu código está dentro da função main () e todos esses identificadores são passados ​​pelas variáveis ​​locais do main ().

As consistências desta regra são as seguintes:

  1. Você não deve tentar colocar um tipo ou interface no caminho da passagem de dados. A razão é que esse tipo seria instável, exigindo alterações constantes quando a complexidade da sua cena aumentar.
  2. O caminho de passagem de dados deve estar na função main ().
tp1
fonte
Se estiver interessado em saber por que isso está atraindo votos negativos. Como alguém não familiarizado com o tópico, seria útil saber o que há de errado com esta resposta.
RubberDuck
1
Eu não fiz voto negativo aqui e curioso também, mas talvez a idéia de que o OGL seja projetado em torno de todo o código esteja dentro mainde mim parecer um pouco complicada para mim (pelo menos na redação).
Dragon Energy