Aproveitando o multithreading entre o loop do jogo e o openGL

10

Falando no contexto de um jogo baseado no renderizador openGL:

Vamos supor que existem dois threads:

  1. Atualiza a lógica e a física do jogo, etc. para os objetos do jogo

  2. Faz chamadas de openGL para cada objeto de jogo com base nos dados nos objetos de jogo (esse segmento 1 continua atualizando)

A menos que você tenha duas cópias de cada objeto de jogo no estado atual do jogo, terá que pausar o Tópico 1 enquanto o Tópico 2 faz as chamadas de empate, caso contrário, os objetos do jogo serão atualizados no meio de uma chamada de empate para esse objeto, o que é indesejável!

Mas a interrupção do encadeamento 1 para fazer chamadas de desenho com segurança do encadeamento 2 mata todo o propósito de multithreading / simultaneidade

Existe uma abordagem melhor para isso além de usar centenas ou milhares ou sincronizar objetos / cercas para que a arquitetura multicore possa ser explorada para desempenho?

Sei que ainda posso usar o multithreading para carregar textura e compilar shaders para os objetos que ainda farão parte do estado atual do jogo, mas como faço para os objetos ativos / visíveis sem causar conflito com o desenho e a atualização?

E se eu usar um bloqueio de sincronização separado em cada objeto do jogo? Dessa forma, qualquer encadeamento bloquearia apenas um objeto (caso ideal) e não durante todo o ciclo de atualização / desenho! Mas qual o custo das fechaduras em cada objeto (o jogo pode ter mil objetos)?

Allahjane
fonte
1. Supondo que haverá apenas dois threads que não escalam bem, 4 núcleos são muito comuns e só aumentam. 2. Resposta da estratégia de melhores práticas: aplique a segregação de responsabilidade de consulta de comando e o modelo de ator / sistema de entidade de componente. 3. Resposta realista: apenas corte-o da maneira tradicional em C ++ com bloqueios e outras coisas.
Den
Um armazenamento de dados comum, um bloqueio.
Justin
@justin, como eu disse com um bloqueio de sincronização A única vantagem do multithreading será brutalmente assassinada durante o dia, o thread de atualização terá que esperar até que o thread de chamada faça chamadas para todos os objetos e o thread de espera esperará até o loop de atualização concluir a atualização ! o que é pior do que a abordagem de rosca única
Allahjane
1
Parece abordagem de vários segmentos em jogos com api assíncronos gráficos (por exemplo openGL) ainda são tema ativo e não há solução perfeita padrão ou perto deste
Allahjane
1
O armazenamento de dados comum ao seu mecanismo e ao seu renderizador não deve ser seu objeto de jogo. Deve haver alguma representação que o renderizador possa processar o mais rápido possível.
Justin

Respostas:

13

A abordagem que você descreveu, usando bloqueios, seria muito ineficiente e provavelmente mais lenta do que usar um único encadeamento. A outra abordagem de manter cópias de dados em cada encadeamento provavelmente funcionaria bem "em termos de velocidade", mas com um custo proibitivo de memória e complexidade de código para manter as cópias sincronizadas.

Existem várias abordagens alternativas para isso, uma solução popular para renderização multithread é usando um buffer duplo de comandos. Isso consiste em executar o backend do renderizador em um encadeamento separado, onde todas as chamadas de empate e comunicação com a API de renderização são realizadas. O thread de front-end que executa a lógica do jogo se comunica com o renderizador de back-end por meio de um buffer de comando (com buffer duplo). Com essa configuração, você tem apenas um ponto de sincronização na conclusão de um quadro. Enquanto o front-end está preenchendo um buffer com comandos de renderização, o back-end está consumindo o outro. Se os dois fios estiverem bem equilibrados, nenhum deve passar fome. Essa abordagem é subótima, no entanto, uma vez que introduz latência nos quadros renderizados, além disso, é provável que o driver OpenGL já esteja fazendo isso em seu próprio processo, portanto, os ganhos de desempenho precisam ser cuidadosamente medidos. Ele também usa apenas dois núcleos, na melhor das hipóteses. Essa abordagem foi usada em vários jogos de sucesso, como Doom 3 e Quake 3

As abordagens mais escaláveis ​​que utilizam melhor as CPUs com vários núcleos são baseadas em tarefas independentes , nas quais você dispara uma solicitação assíncrona que é atendida em um encadeamento secundário, enquanto o encadeamento que disparou a solicitação continua com algum outro trabalho. Idealmente, a tarefa não deve ter dependências com os outros encadeamentos, para evitar bloqueios (também evite dados compartilhados / globais como a praga!). As arquiteturas baseadas em tarefas são mais utilizáveis ​​em partes localizadas de um jogo, como animações computacionais, busca de caminhos de IA, geração de procedimentos, carregamento dinâmico de objetos de cena etc. Os jogos são naturalmente cheios de eventos, a maioria dos tipos de eventos é assíncrona. é fácil fazê-los rodar em threads separados.

Por fim, recomendo a leitura:

glampert
fonte
apenas curioso! como você sabe que o Doom 3 usou essa abordagem? Eu pensei que os desenvolvedores nunca divulgaram técnicas!
Allahjane
4
@ Allahjane, Doom 3 é Open Source . No link que forneci, você encontrará uma revisão da arquitetura geral do jogo. E você está enganado, sim, é raro encontrar jogos totalmente de código aberto, mas os desenvolvedores geralmente expõem suas técnicas e truques em blogs, documentos e eventos como o GDC .
glampert
1
Hmm. Esta foi uma leitura incrível! obrigado por me informar disso! Você começa o carrapato :)
Allahjane