
-
Aprimore jogos com o Metal 4
Fique por dentro dos últimos avanços do Metal 4. Apresentaremos os novos recursos de traçado de raios que ajudam o Apple Silicon a processar suas cargas de trabalho mais complexas e visualmente robustas. Saiba como o MetalFX pode ajudar a aumentar as cargas de trabalho melhorando renderizações, interpolando quadros e reduzindo ruídos visuais em cenas. Para aproveitar ao máximo esta sessão, recomendamos assistir primeiro aos vídeos “Conheça o Metal 4” e “Explore jogos feitos com o Metal 4”.
Capítulos
- 0:00 - Introdução
- 2:13 - Ampliar a renderização
- 7:17 - Interpolar quadros
- 13:50 - Traçado de raios com o Metal 4
- 19:25 - Reduzir ruídos durante a ampliação
- 26:08 - Próximas etapas
Recursos
Vídeos relacionados
WWDC25
- Combine o aprendizado de máquina e os gráficos do Metal 4
- Conheça o Metal 4
- Explore jogos feitos com o Metal 4
- Melhore ainda mais seus jogos
- Novidades na renderização no Metal para apps imersivos
WWDC23
WWDC22
-
Buscar neste vídeo...
Oi, meu nome é Matias Koskela. Vou apresentar técnicas e melhores práticas para impulsionar seus jogos avançados e apps profissionais nas plataformas da Apple.
Antes de começar, confira “Conhecer o Metal 4” para ver uma visão geral do Metal 4 e “Explorar os jogos feitos com o Metal 4” para aprender a usar a iteração mais recente do Metal. Este vídeo é a segunda parte da nossa série sobre jogos do Metal 4. Saiba como o Metal 4 pode ajudar a combinar aprendizado de máquina e elementos gráficos em uma trilha separada.
Jogos, como o Cyberpunk 2077, estão se tornando mais realistas com renderização de alta qualidade. Isso aumenta o custo por pixel, tornando ainda maior o desafio de manter altas resoluções e taxas de quadros. Com o Metal, você renderiza quadros de alta qualidade nas plataformas da Apple, de iPhones a Macs. Se você está usando técnicas como rasterização ou traçado de raios, o Metal oferece APIs práticos.
Com o MetalFX Upscaling, amplie cargas de trabalho para resoluções e taxas de quadros ainda mais altas.
Para ir ainda mais longe, use o novo interpolador de quadros do MetalFX.
Jogos, como o Cyberpunk 2077, oferecem traçado de caminho em tempo real realista. Essas funcionalidades expandidas na renderização em tempo real são possíveis com os novos recursos do Metal 4. Isso inclui melhorias no traçado de raios e o novo upscaler com remoção de ruído do MetalFX, que facilita a ampliação ao reduzir o número de raios necessários no jogo.
O MetalFX Upscaler pode alcançar resoluções mais altas e taxas de quadros mais rápidas. Você pode deixar a jogabilidade mais fluído com o novo interpolador de quadros do MetalFX. Os novos recursos de traçado de raios do Metal 4 melhoram o desempenho e podem ser combinados com o novo upscaler com a remoção de ruído do MetalFX.
Upscaling é uma técnica amplamente usada que ajuda a melhorar o desempenho na maioria dos cenários. O MetalFX tem um upscaler baseado em aprendizado de máquina, faz parte das plataformas da Apple desde 2022 e vem melhorando a cada ano.
O MetalFX Upscaling agora inclui novas ferramentas e técnicas para melhorar a qualidade e o desempenho do seu jogo. Primeiro, aplique corretamente o upscaling temporal no seu jogo. Uma parte desse processo é ajustar corretamente o parâmetro de exposição. Melhore o desempenho com resolução dinâmica e aprimore a qualidade em certos cenários usando dicas de reatividade.
Imagine um pipeline de renderização. O quadro é renderizado, traçado por raios, antes dos efeitos de pós-processamento, como desfoque de movimento. Depois, aplica-se a exposição e o mapeamento de tons, a interface é renderizada, e o quadro é exibido ao jogador.
Adicione o MetalFX Upscaling após a renderização com tremulação e antes dos efeitos de pós-processamento. Confira “Melhorar o desempenho com o MetalFX Upscaling” para saber mais sobre como integrar o upscaler. Este ano, há ainda mais ferramentas e recursos disponíveis para melhorar o desempenho do jogo.
Definir a exposição correta no upscaler é fundamental para um resultado de alta qualidade.
Um valor errado pode causar cintilação e efeito fantasma.
No pipeline de renderização, as cores de entrada e saída do upscaler estão no espaço de cor linear. O upscaler recebe um parâmetro chamado exposição, que, ao ser multiplicado pela cor de entrada, gera um brilho que deve corresponder à exposição usada no seu mapeamento de tons.
Isso garante que o upscaler entenda quais são as características visíveis do quadro quando exibidas ao jogador. O valor é apenas uma indicação para o upscaler e não altera o brilho da imagem de saída. O MetalFX inclui uma nova ferramenta para ajustar o valor de exposição enviado ao upscaler.
É o depurador de exposição. Para ativá-lo, defina a variável de ambiente MTLFX_EXPOSURE_TOOL_ENABLED. O upscaler renderiza um tabuleiro de xadrez cinza sobre o quadro e aplica o inverso do valor de exposição.
Então você pode revisar como o padrão aparece no final do pipeline, na tela.
Se o valor de exposição passado ao upscaler não corresponder ao do mapeador de tons, o tabuleiro de xadrez estará muito escuro ou muito claro.
Outro indicativo de incompatibilidade é quando o brilho do tabuleiro de xadrez se altera durante o jogo.
Quando o valor de exposição está correto, o padrão da grade é cinza médio constante.
Como a complexidade dos jogos pode variar de cena para cena, muitos jogos adotaram a renderização em resolução dinâmica.
Quando o quadro é complexo, a resolução de entrada do upscaler é reduzida. Quando fica ainda mais desafiador, o jogo reduz dinamicamente a resolução de entrada. O upscaler temporal do MetalFX aceita entradas com tamanho dinâmico, em vez de exigir entradas do mesmo tamanho a cada quadro. Para obter a melhor qualidade de scaling, seu jogo não deve definir a escala máxima acima de 2x, a menos que seja necessário.
Outro recurso novo no upscaler temporal do MetalFX é uma funcionalidade opcional para indicar ao upscaler a reatividade dos pixels.
Quando o jogo renderiza efeitos transparentes, ou partículas como fogos de artifício, eles não são renderizados em texturas de movimento e profundidade.
Em altas taxas de scaling e baixas resoluções de entrada, essas partículas se misturam com o fundo ou podem apresentar efeitos de fantasma. Isso ocorre porque, na renderização, elas podem se parecer com detalhes de textura ou realces especulares.
Para ter controle sobre como as partículas são tratadas, o upscaler aceita uma nova entrada opcional chamada máscara reativa. Essa máscara permite marcar as áreas cobertas por esses efeitos.
Para usá-la, defina um valor de máscara reativa no shader com base no tipo de material presente no G-buffer. No código do host, vincule a textura ao objeto do upscaler temporal antes de codificá-lo.
Use a máscara reativa apenas se aumentar a resolução de entrada não for uma opção. Não use uma máscara reativa ajustada para outro upscaler, pois ela pode estar mascarando áreas que já têm boa qualidade na saída do upscaler do MetalFX. Usar o upscaler proporciona ótimo desempenho com excelente qualidade. Você pode querer alcançar taxas de atualização ainda mais altas. Este ano, o MetalFX lança a interpolação de quadros para as plataformas da Apple.
A interpolação de quadros do MetalFX é fácil de integrar ao jogo. Primeiro, configure um objeto interpolador, renderize a interface nos quadros interpolados e apresente e sincronize os quadros.
A interpolação de quadros permite usar pixels já renderizados, oferecendo uma experiência de jogo mais fluida.
Este é o mesmo pipeline de renderização, mas sem a renderização da interface.
Interpole os quadros após a etapa de mapeamento de tom. Para resoluções e taxas de quadros ainda maiores, use upscaling e interpolação no mesmo pipeline.
Para usar o interpolador de quadros, o app fornece dois quadros renderizados, vetores de movimento e profundidade. Se você já usa o upscaler, pode usar os mesmos vetores de movimento e profundidade. A textura de movimento tem cor nos objetos porque eles se moveram para a direita. Com essas entradas, o MetalFX gera um quadro entre os dois quadros renderizados.
Para configurar o interpolador e obter melhor desempenho, forneça o objeto de upscaling ao descritor do interpolador. Ao criar o interpolador, defina a escala do movimento e a convenção de profundidade. Em seguida, vincule as cinco texturas necessárias ao interpolador.
Assim que começar a obter quadros interpolados, pense na renderização da interface.
No pipeline de renderização típico, um jogo renderiza sua interface no final de cada quadro, no mesmo ponto em que a interpolação de quadros deve ocorrer.
A renderização faz a mesclagem alfa dos elementos no quadro, pode conter textos que se alternam a cada quadro e não modifica as texturas de movimento ou profundidade.
Há várias formas de obter uma interface com boa aparência com a interpolação de quadros ativada.
Há três técnicas mais comuns para renderizar a interface com interpolação de quadros. São: interface composta, interface fora da tela e interface atualizada a cada quadro.
Na interface composta, o interpolador recebe o quadro anterior N - 1, o quadro atual N sem interface e o mesmo quadro N com interface. A interface composta é a mais fácil de adotar. Nesse modo, o interpolador de quadros consegue ver o delta entre a textura com e sem interface. Dessa forma, ele pode tentar remover a interface e posicioná-la corretamente no quadro interpolado. Não é possível desfazer a mesclagem de um pixel já mesclado com perfeição. Portanto, você pode ajudar o interpolador utilizando uma das outras opções.
Como a interface fora da tela: a interface é renderizada em uma textura separada.
O interpolador então a adiciona sobre o quadro interpolado. Fornecê-la ao interpolador evita uma carga e armazenamento extra, já que o interpolador pode escrever a interface na saída.
Por fim, na interface atualizada a cada quadro, o tratamento da interface fica por conta do seu código, o que pode exigir as maiores alterações da sua parte. Nesse caso, você também pode atualizar a interface para o quadro interpolado, oferecendo a experiência mais fluída para o jogador.
Agora você tem uma interface com ótima aparência sobre o quadro interpolado. Chegou a hora de pensar em como apresentar os quadros interpolados e os renderizados nativamente na ordem correta e com os intervalos adequados.
A renderização do jogo consiste na thread de Renderização, na GPU e na thread de Apresentação. A thread de Renderização configura o trabalho necessário para a GPU e a Apresentação. Quando um quadro é renderizado, o interpolador pode gerar um quadro com um registro de data/hora entre o quadro renderizado e o quadro anterior. E seu jogo pode então apresentar o quadro interpolado. Após um intervalo de apresentação, seu jogo pode exibir o último quadro renderizado.
Determinar a duração desse intervalo de modo consistente pode ser difícil. Mas isso é necessário para acertar o ritmo do jogo.
O novo Metal HUD pode ser uma ótima ferramenta para identificar se o ritmo está desalinhado. confira o vídeo “Aprimorar seus jogos” para obter mais detalhes sobre como ativá-lo e para conhecer seus novos recursos incríveis.
Veja o gráfico de intervalo de quadros, com tempo no eixo horizontal e duração do intervalo no eixo vertical.
Se o gráfico mostrar um padrão irregular e picos indicando intervalos maiores de atualização, isso indicará que o ritmo está desalinhado.
O ritmo está desalinhado quando há mais de dois intervalos de quadro no histograma.
Depois de ajustar o ritmo, você verá uma linha reta se atingir a taxa de atualização desejada, ou um padrão regular e repetitivo se estiver abaixo dela, com no máximo dois intervalos no histograma.
Aqui está um exemplo de como isso pode ser feito com uma classe presentHelper. Durante o loop de desenho, tudo é renderizado em uma textura de baixa resolução e ampliado pelo upscaler do MetalFX. A interface é renderizada após avisar o auxiliar do início da renderização. Por fim, a chamada do interpolador é tratada pela classe presentHelper. Confira o código de exemplo para ver detalhes da implementação.
Além do ritmo, também é importante acertar o tempo delta e os parâmetros da câmera. A área de oclusão pode ter artefatos se algum parâmetro não estiver correto.
Com os parâmetros corretos, a área de oclusão se alinha perfeitamente.
Isso ocorre porque o interpolador pode ajustar os vetores de movimento para coincidir com a duração do movimento na simulação real.
Depois de acertar todas as entradas e o ritmo, os quadros interpolados devem ficar ótimos. Sua entrada de interpolação deve ter uma taxa de quadros razoavelmente alta. Tente ter, no mínimo, 30 quadros por segundo antes da interpolação.
O upscaler e o interpolador de quadros são técnicas que você pode usar para ampliar qualquer estilo de renderização. Já o traçado de raios é usado em cenários de renderização de alto desempenho. O Metal 4 incorpora novos recursos de traçado de raios voltados para a construção de estruturas de aceleração e funções de interseção.
Mais jogos estão usando traçado de raios do Metal nas plataformas da Apple.
Nesta demonstração, a iluminação é realista, e o drone é visível nos reflexos no chão. As técnicas de traçado de raios e as complexidades variam de jogo para jogo.
Isso exige flexibilidade no gerenciamento das funções de interseção e mais opções para a construção das estruturas de aceleração.
O Metal 4 apresenta novos recursos para ajudar a otimizar ambos.
Aprenda o básico do traçado de raios no Metal, como a construção de estruturas de aceleração e funções de interseção em “Seu guia de traçado de raios no Metal”.
Um jogo que aplica traçados de raio em uma cena simples com grama ao redor de uma árvore.
Mesmo nessa cena simples, há vários tipos de materiais, como folhagem com teste alpha e o tronco opaco da árvore.
Assim, são necessárias várias funções de interseção para traçado de raios. Separadamente para raios primários e raios de sombra.
Um buffer de funções de interseção é um buffer de argumentos que contém identificadores para as funções de interseção da cena.
A grama e as folhas podem precisar de funcionalidades semelhantes para rastrear raios primários. Buffers de funções de interseção permitem que seu jogo tenha múltiplas entradas apontando para a mesma função de interseção.
Para configurar os índices do buffer de funções de interseção, defina o estado no nível da instância. Esta cena tem duas instâncias. No nível da geometria, a grama tem apenas uma geometria e a árvore tem duas. O intersetor precisa saber qual função de interseção usar para os raios de sombra que atingem o tronco.
Ao criar suas estruturas de aceleração de instância, especifique o intersectionFunctionTableOffset em cada descritor de instância.
Ao construir sua estrutura de aceleração primitiva, você também define o intersectionFunctionTableOffset nos descritores de geometria.
Ao configurar o intersetor no seu shader, adicione “intersection_function_buffer” às tags.
Defina o multiplicador de geometria no intersetor. O multiplicador é o número de tipos de raios no buffer de funções de interseção.
O exemplo tem dois tipos de raios para cada geometria. Portanto, o valor correto aqui é dois. Dentro desses dois tipos de raios, forneça o índice base para o tipo de raio sendo rastreado. No exemplo, o índice base para rastrear raios primários é 0.
Para o rastreamento de sombras, o ID base é 1.
Ao combinar a contribuição da instância e da geometria do tronco da árvore, o multiplicador de geometria e o ID base do tipo de raio de sombra, o ponteiro acaba na função de interseção desejada.
Finalize o código passando os argumentos do buffer de funções de interseção para o método intersect.
Especificando o buffer, seu tamanho e stride. Isso dá uma flexibilidade extra, comparado ao que costuma haver em outras APIs. Ao fazer a portabilidade do DirectX, migre as tabelas de vinculação do shader para buffers de funções de interseção no Metal.
No DirectX, você define o endereço e o stride do buffer de funções de interseção no host, ao criar o descritor para despachar os raios. No Metal, isso é definido no shader. As threads no grupo SIMD devem definir o mesmo valor, caso contrário, o comportamento será indefinido. Índice do tipo de raio e multiplicador de geometria são tratados igualmente no DirectX e no Metal. Seu app pode defini-los no shader. No DirectX e no Metal, você define o índice de deslocamento de instância por instância ao criar a estrutura de aceleração de instância. O índice de deslocamento da geometria é gerado automaticamente no DirectX e manualmente no Metal.
Os buffers de funções de interseção melhoram a experiência de portabilidade para o Metal no seu jogo com traçado de raios. Depois que tudo estiver funcionando, o Metal 4 também permite otimizar a forma como o Metal constrói suas estruturas de aceleração.
O Metal já oferece controle sobre a construção das estruturas de aceleração. Além do comportamento padrão, você pode otimizar para refit, ativar cenas maiores ou construir a estrutura de aceleração mais depressa. Neste ano, você ganha mais flexibilidade e pode priorizar interseções rápidas para reduzir o tempo de rastreamento dos raios.
Ou pode optar por minimizar o uso de memória da sua estrutura de aceleração.
Os sinalizadores de uso podem ser definidos por construção de estrutura de aceleração e não precisam ser iguais para todas as estruturas de aceleração.
Os novos sinalizadores de estrutura de aceleração personalizam mais a parte de traçado de raios do pipeline de renderização. Caso você o utilize para efeitos estocásticos, será necessário um removedor de ruído. A remoção de ruído pode fazer parte do seu upscaler do MetalFX.
O traçado de raios em tempo real está sendo usado cada vez mais, desde o híbrido mais simples até o complexo. Nesta imagem de exemplo, o traçado de raios torna tudo mais realista e melhora os reflexos. Alcance o equilíbrio entre qualidade e desempenho no traçado de raios usando a remoção de ruído com menos raios.
Com a nova API MetalFX, combinar upscaling e remoção de ruído pode ser tão simples quanto adicionar entradas extras. Mas você pode melhorar a qualidade ajudando o upscaler com remoção de ruído adicionando mais entradas e ajustando os detalhes.
Antes de combinar upscaler e removedor de ruído, vamos ver como essas etapas são feitas.
Pipelines de renderização em tempo real e interativa com traçado de raios rastreiam efeitos separadamente, removem o ruído de cada um e compõem o resultado em uma textura única, sem ruído e com tremulação, que é ampliada pelo upscaler temporal do MetalFX. Seguido pelo pós-processamento.
Removedores de ruído tradicionais exigem ajustes de parâmetros artísticos para cada cena. Veja como alguns removedores de ruído ficam sem os parâmetros ajustados por um artista. Em contraste, não é necessário ajustar parâmetros com o upscaler com remoção de ruído do MetalFX, que é aplicado após a renderização principal e antes do pós-processamento. As técnicas baseadas em aprendizado de máquina no MetalFX oferecem uma remoção de ruído e upscaling robustos, de alto desempenho e alta qualidade. E é mais fácil de integrar. Integrar o upscaler é um bom início para integrar o upscaler com remoção de ruído. Podemos ver as entradas para o upscaler. Cor, movimento e profundidade. A nova API combinada é um superconjunto da API do upscaler.
Para a nova API, precisamos adicionar buffers auxiliares livres de ruído, que são mostrados à esquerda. Seu app já pode ter a maioria deles. Vamos nos aprofundar em cada um deles a seguir.
A primeira nova entrada é as normais. Para ter os melhores resultados, devem estar em espaço mundial.
Em seguida, o albedo difuso, que é a cor base da radiação difusa do material.
A rugosidade, que representa se a superfície é lisa ou áspera, sendo um valor linear. E a última entrada é o albedo especular. Isso deve ser uma aproximação livre de ruído da radiância especular da sua renderização. Deve incluir um componente de Fresnel. Em código, a adição dessas novas entradas é simples.
A criação de um upscaler temporal típico leva cerca de 10 linhas de código. Para ativar a versão com remoção de ruído, altere o tipo de scaler e adicione os tipos das texturas adicionais.
Da mesma forma, ao codificar o scaler, essa seria a chamada para o upscaler. A única diferença é que você precisa vincular as texturas de entrada extras.
Após configurar o uso básico do removedor de ruído, aprimore-o usando algumas das entradas opcionais. E evitando algumas armadilhas típicas de integração.
Há algumas texturas de entrada opcionais que podem ser usadas para melhorar a qualidade.
A distância do impacto especular indica o comprimento do raio do ponto de visibilidade primária do pixel até o ponto de impacto da segunda reflexão. A máscara de intensidade do removedor de ruído, que marca áreas que não precisam de remoção de ruído. Por fim, a sobreposição de transparência, que usa o canal alfa para misturar a cor que é ampliada e não submetida à remoção de ruído.
O problema na integração é a entrada que está muito ruidosa. Resolva usando todas as melhorias padrão de amostragem em traçado de caminho, como estimativa de próximo evento, técnicas de amostragem por importância e, em cenas maiores com várias fontes de luz, foco as fontes que contribuem para a área.
A qualidade da amostragem no traçado de raios é influenciada pelos números aleatórios correlacionados. Não use geradores de números aleatórios que sejam muito correlacionados. A correlação espacial e a temporal podem causar artefatos.
Uma armadilha potencial relacionada aos dados auxiliares é o albedo difuso de materiais metálicos. Neste exemplo, as peças de xadrez são metálicas e apresentam cor no albedo especular. Nesse caso, o albedo difuso das peças de xadrez deve ser mais escuro.
Por fim, existem algumas armadilhas comuns relacionadas às normais. O upscaler com remoção de ruído do MetalFX espera que as normais estejam em espaço mundial para melhorar as decisões de remoção de ruído. Você precisa usar um tipo de dado de textura que tenha bit de sinal, ou a qualidade pode ser subótima, dependendo da orientação da câmera.
Depois de acertar esses detalhes, você deve obter quadros bem sem ruídos e ampliados.
Vamos ver o que acontece quando você reúne todos esses recursos em um único renderizador.
Meus colegas prepararam uma demonstração que usa o pipeline de renderização que mencionei. A demonstração utiliza os novos recursos de traçado de raios do Metal 4 para otimizar a renderização. Ela realiza remoção de ruído e upscaling com o upscaler com removedor de ruído do MetalFX. Após a exposição e o mapeamento de tons, os quadros são interpolados pelo interpolador de quadros do MetalFX.
A demonstração usa efeitos avançados de iluminação por traçado de raios, como iluminação global, reflexos, sombras e oclusão de ambiente, para dar vida a uma cena com dois robôs jogando xadrez.
Na visualização no canto superior direito, veja a renderização antes do processamento do MetalFX. E outras entradas do MetalFX em outras visualizações.
Adotamos o upscaler com remoção de ruído e o interpolador de quadros do MetalFX. O removedor de ruído simplificou a renderização ao eliminar a necessidade de ajustes manuais na aparência final.
Se você já integrou o upscaler do MetalFX, esta é a chance de fazer o upgrade para a interpolação de quadros. Se você é iniciante no MetalFX, comece com o upscaler. Depois, verifique se seus efeitos de traçado de raios estão usando as melhores práticas, como os buffers de função de interseção. E reduza o orçamento de raios do jogo com o upscaler com remoção de ruído.
Quero ver esses novos recursos em ação nos seus jogos. E o que vocês vão criar usando o Metal 4. Agradeço sua participação.
-
-
6:46 - Reactive Mask
// Create reactive mask setup in shader out.reactivity = m_material_id == eRain ? (m_material_id == eSpark ? 1.0f : 0.0f) : 0.8f; // Set reactive mask before encoding upscaler on host temporalUpscaler.reactiveMask = reactiveMaskTexture;
-
8:35 - MetalFX Frame Interpolator
// Create and configure the interpolator descriptor MTLFXFrameInterpolatorDescriptor* desc = [MTLFXFrameInterpolatorDescriptor new]; desc.scaler = temporalScaler; // ... // Create the effect and configure your effect id<MTLFXFrameInterpolator> interpolator = [desc newFrameInterpolatorWithDevice:device]; interpolator.motionVectorScaleX = mvecScaleX; interpolator.motionVectorScaleY = mvecScaleY; interpolator.depthReversed = YES; // Set input textures interpolator.colorTexture = colorTexture; interpolator.prevColorTexture = prevColorTexture; interpolator.depthTexture = depthTexture; interpolator.motionTexture = motionTexture; interpolator.outputTexture = outputTexture;
-
12:45 - Interpolator present helper class
#include <thread> #include <mutex> #include <sys/event.h> #include <mach/mach_time.h> class PresentThread { int m_timerQueue; std::thread m_encodingThread, m_pacingThread; std::mutex m_mutex; std::condition_variable m_scheduleCV, m_threadCV, m_pacingCV; float m_minDuration; uint32_t m_width, m_height; MTLPixelFormat m_pixelFormat; const static uint32_t kNumBuffers = 3; uint32_t m_bufferIndex, m_inputIndex; bool m_renderingUI, m_presentsPending; CAMetalLayer *m_metalLayer; id<MTLCommandQueue> m_presentQueue; id<MTLEvent> m_event; id<MTLSharedEvent> m_paceEvent, m_paceEvent2; uint64_t m_eventValue; uint32_t m_paceCount; int32_t m_numQueued, m_framesInFlight; id<MTLTexture> m_backBuffers[kNumBuffers]; id<MTLTexture> m_interpolationOutputs[kNumBuffers]; id<MTLTexture> m_interpolationInputs[2]; id<MTLRenderPipelineState> m_copyPipeline; std::function<void(id<MTLRenderCommandEncoder>)> m_uiCallback = nullptr; void PresentThreadFunction(); void PacingThreadFunction(); void CopyTexture(id<MTLCommandBuffer> commandBuffer, id<MTLTexture> dest, id<MTLTexture> src, NSString *label); public: PresentThread(float minDuration, CAMetalLayer *metalLayer); ~PresentThread() { std::unique_lock<std::mutex> lock(m_mutex); m_numQueued = -1; m_threadCV.notify_one(); m_encodingThread.join(); } void StartFrame(id<MTLCommandBuffer> commandBuffer) { [commandBuffer encodeWaitForEvent:m_event value:m_eventValue++]; } void StartUI(id<MTLCommandBuffer> commandBuffer) { assert(m_uiCallback == nullptr); if(!m_renderingUI) { CopyTexture(commandBuffer, m_interpolationInputs[m_inputIndex], m_backBuffers[m_bufferIndex], @"Copy HUDLESS"); m_renderingUI = true; } } void Present(id<MTLFXFrameInterpolator> frameInterpolator, id<MTLCommandQueue> queue); id<MTLTexture> GetBackBuffer() { return m_backBuffers[m_bufferIndex]; } void Resize(uint32_t width, uint32_t height, MTLPixelFormat pixelFormat); void DrainPendingPresents() { std::unique_lock<std::mutex> lock(m_mutex); while(m_presentsPending) m_scheduleCV.wait(lock); } bool UICallbackEnabled() const { return m_uiCallback != nullptr; } void SetUICallback(std::function<void(id<MTLRenderCommandEncoder>)> callback) { m_uiCallback = callback; } }; PresentThread::PresentThread(float minDuration, CAMetalLayer *metalLayer) : m_encodingThread(&PresentThread::PresentThreadFunction, this) , m_pacingThread(&PresentThread::PacingThreadFunction, this) , m_minDuration(minDuration) , m_numQueued(0) , m_metalLayer(metalLayer) , m_inputIndex(0u) , m_bufferIndex(0u) , m_renderingUI(false) , m_presentsPending(false) , m_framesInFlight(0) , m_paceCount(0) , m_eventValue(0) { id<MTLDevice> device = metalLayer.device; m_presentQueue = [device newCommandQueue]; m_presentQueue.label = @"presentQ"; m_timerQueue = kqueue(); metalLayer.maximumDrawableCount = 3; Resize(metalLayer.drawableSize.width, metalLayer.drawableSize.height, metalLayer.pixelFormat); m_event = [device newEvent]; m_paceEvent = [device newSharedEvent]; m_paceEvent2 = [device newSharedEvent]; } void PresentThread::Present(id<MTLFXFrameInterpolator> frameInterpolator, id<MTLCommandQueue> queue) { id<MTLCommandBuffer> commandBuffer = [queue commandBuffer]; if(m_renderingUI) { frameInterpolator.colorTexture = m_interpolationInputs[m_inputIndex]; frameInterpolator.prevColorTexture = m_interpolationInputs[m_inputIndex^1]; frameInterpolator.uiTexture = m_backBuffers[m_bufferIndex]; } else { frameInterpolator.colorTexture = m_backBuffers[m_bufferIndex]; frameInterpolator.prevColorTexture = m_backBuffers[(m_bufferIndex + kNumBuffers - 1) % kNumBuffers]; frameInterpolator.uiTexture = nullptr; } frameInterpolator.outputTexture = m_interpolationOutputs[m_bufferIndex]; [frameInterpolator encodeToCommandBuffer:commandBuffer]; [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> _Nonnull) { std::unique_lock<std::mutex> lock(m_mutex); m_framesInFlight--; m_scheduleCV.notify_one(); m_paceCount++; m_pacingCV.notify_one(); }]; [commandBuffer encodeSignalEvent:m_event value:m_eventValue++]; [commandBuffer commit]; std::unique_lock<std::mutex> lock(m_mutex); m_framesInFlight++; m_numQueued++; m_presentsPending = true; m_threadCV.notify_one(); while((m_framesInFlight >= 2) || (m_numQueued >= 2)) m_scheduleCV.wait(lock); m_bufferIndex = (m_bufferIndex + 1) % kNumBuffers; m_inputIndex = m_inputIndex^1u; m_renderingUI = false; } void PresentThread::CopyTexture(id<MTLCommandBuffer> commandBuffer, id<MTLTexture> dest, id<MTLTexture> src, NSString *label) { MTLRenderPassDescriptor *desc = [MTLRenderPassDescriptor new]; desc.colorAttachments[0].texture = dest; desc.colorAttachments[0].loadAction = MTLLoadActionDontCare; desc.colorAttachments[0].storeAction = MTLStoreActionStore; id<MTLRenderCommandEncoder> renderEncoder = [commandBuffer renderCommandEncoderWithDescriptor:desc]; [renderEncoder setFragmentTexture:src atIndex:0]; [renderEncoder setRenderPipelineState:m_copyPipeline]; [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; if(m_uiCallback) m_uiCallback(renderEncoder); renderEncoder.label = label; [renderEncoder endEncoding]; } void PresentThread::PacingThreadFunction() { NSThread *thread = [NSThread currentThread]; [thread setName:@"PacingThread"]; [thread setQualityOfService:NSQualityOfServiceUserInteractive]; [thread setThreadPriority:1.f]; mach_timebase_info_data_t info; mach_timebase_info(&info); // maximum delta (0.1ms) in machtime units const uint64_t maxDeltaInNanoSecs = 100000000; const uint64_t maxDelta = maxDeltaInNanoSecs * info.denom / info.numer; uint64_t time = mach_absolute_time(); uint64_t paceEventValue = 0; for(;;) { std::unique_lock<std::mutex> lock(m_mutex); while(m_paceCount == 0) m_pacingCV.wait(lock); m_paceCount--; lock.unlock(); // we get signal... const uint64_t prevTime = time; time = mach_absolute_time(); m_paceEvent.signaledValue = ++paceEventValue; const uint64_t delta = std::min(time - prevTime, maxDelta); const uint64_t timeStamp = time + ((delta*31)>>6); struct kevent64_s timerEvent, eventOut; struct timespec timeout; timeout.tv_nsec = maxDeltaInNanoSecs; timeout.tv_sec = 0; EV_SET64(&timerEvent, 0, EVFILT_TIMER, EV_ADD | EV_ONESHOT | EV_ENABLE, NOTE_CRITICAL | NOTE_LEEWAY | NOTE_MACHTIME | NOTE_ABSOLUTE, timeStamp, 0, 0, 0); kevent64(m_timerQueue, &timerEvent, 1, &eventOut, 1, 0, &timeout); // main screen turn on... m_paceEvent2.signaledValue = ++paceEventValue; } } void PresentThread::PresentThreadFunction() { NSThread *thread = [NSThread currentThread]; [thread setName:@"PresentThread"]; [thread setQualityOfService:NSQualityOfServiceUserInteractive]; [thread setThreadPriority:1.f]; uint64_t eventValue = 0; uint32_t bufferIndex = 0; uint64_t paceEventValue = 0; for(;;) { std::unique_lock<std::mutex> lock(m_mutex); if(m_numQueued == 0) { m_presentsPending = false; m_scheduleCV.notify_one(); } while(m_numQueued == 0) m_threadCV.wait(lock); if(m_numQueued < 0) break; lock.unlock(); @autoreleasepool { id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable]; lock.lock(); m_numQueued--; m_scheduleCV.notify_one(); lock.unlock(); id<MTLCommandBuffer> commandBuffer = [m_presentQueue commandBuffer]; [commandBuffer encodeWaitForEvent:m_event value:++eventValue]; CopyTexture(commandBuffer, drawable.texture, m_interpolationOutputs[bufferIndex], @"Copy Interpolated"); [commandBuffer encodeSignalEvent:m_event value:++eventValue]; [commandBuffer encodeWaitForEvent:m_paceEvent value:++paceEventValue]; if(m_minDuration > 0.f) [commandBuffer presentDrawable:drawable afterMinimumDuration:m_minDuration]; else [commandBuffer presentDrawable:drawable]; [commandBuffer commit]; } @autoreleasepool { id<MTLCommandBuffer> commandBuffer = [m_presentQueue commandBuffer]; id<CAMetalDrawable> drawable = [m_metalLayer nextDrawable]; CopyTexture(commandBuffer, drawable.texture, m_backBuffers[bufferIndex], @"Copy Rendered"); [commandBuffer encodeWaitForEvent:m_paceEvent2 value:++paceEventValue]; if(m_minDuration > 0.f) [commandBuffer presentDrawable:drawable afterMinimumDuration:m_minDuration]; else [commandBuffer presentDrawable:drawable]; [commandBuffer commit]; } bufferIndex = (bufferIndex + 1) % kNumBuffers; } } void PresentThread::Resize(uint32_t width, uint32_t height, MTLPixelFormat pixelFormat) { if((m_width != width) || (m_height != height) || (m_pixelFormat != pixelFormat)) { id<MTLDevice> device = m_metalLayer.device; if(m_pixelFormat != pixelFormat) { id<MTLLibrary> lib = [device newDefaultLibrary]; MTLRenderPipelineDescriptor *pipelineDesc = [MTLRenderPipelineDescriptor new]; pipelineDesc.vertexFunction = [lib newFunctionWithName:@"FSQ_VS_V4T2"]; pipelineDesc.fragmentFunction = [lib newFunctionWithName:@"FSQ_simpleCopy"]; pipelineDesc.colorAttachments[0].pixelFormat = pixelFormat; m_copyPipeline = [device newRenderPipelineStateWithDescriptor:pipelineDesc error:nil]; m_pixelFormat = pixelFormat; } DrainPendingPresents(); m_width = width; m_height = height; MTLTextureDescriptor *texDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:pixelFormat width:width height:height mipmapped:NO]; texDesc.storageMode = MTLStorageModePrivate; for(uint32_t i = 0; i < kNumBuffers; i++) { texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageShaderWrite|MTLTextureUsageRenderTarget; m_backBuffers[i] = [device newTextureWithDescriptor:texDesc]; texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget; m_interpolationOutputs[i] = [device newTextureWithDescriptor:texDesc]; } texDesc.usage = MTLTextureUsageShaderRead|MTLTextureUsageRenderTarget; m_interpolationInputs[0] = [device newTextureWithDescriptor:texDesc]; m_interpolationInputs[1] = [device newTextureWithDescriptor:texDesc]; } }
-
13:00 - Set intersection function table offset
// Set intersection function table offset on host-side geometry descriptors NSMutableArray<MTLAccelerationStructureGeometryDescriptor *> *geomDescs ...; for (auto g = 0; g < geomList.size(); ++g) { MTLAccelerationStructureGeometryDescriptor *descriptor = ...; descriptor.intersectionFunctionTableOffset = g; ... [geomDescs addObject:descriptor]; }
-
13:01 - Set up the intersector
// Set up the intersector metal::raytracing::intersector<intersection_function_buffer, instancing, triangle> trace; trace.set_geometry_multiplier(2); // Number of ray types, defaults to 1 trace.set_base_id(1); // Set ray type index, defaults to 0
-
13:02 - Ray trace intersection function buffers
// Ray trace intersection function buffers // Set up intersection function buffer arguments intersection_function_buffer_arguments ifb_arguments; ifb_arguments.intersection_function_buffer = raytracingResources.ifbBuffer; ifb_arguments.intersection_function_buffer_size = raytracingResources.ifbBufferSize; ifb_arguments.intersection_function_stride = raytracingResources.ifbBufferStride; // Set up the ray and finish intersecting metal::raytracing::ray r = { origin, direction }; auto result = trace.intersect(r, ads, ifb_arguments);
-
13:02 - Change of temporal scaler setup to denoised temporal scaler setup
// Change of temporal scaler setup to denoised temporal scaler setup MTLFXTemporalScalerDescriptor* desc = [MTLFXTemporalScalerDescriptor new]; desc.colorTextureFormat = MTLPixelFormatBGRA8Unorm_sRGB; desc.outputTextureFormat = MTLPixelFormatBGRA8Unorm_sRGB; desc.depthTextureFormat = DepthStencilFormat; desc.motionTextureFormat = MotionVectorFormat; desc.diffuseAlbedoTextureFormat = DiffuseAlbedoFormat; desc.specularAlbedoTextureFormat = SpecularAlbedoFormat; desc.normalTextureFormat = NormalVectorFormat; desc.roughnessTextureFormat = RoughnessFormat; desc.inputWidth = _mainViewWidth; desc.inputHeight = _mainViewHeight; desc.outputWidth = _screenWidth; desc.outputHeight = _screenHeight; temporalScaler = [desc newTemporalDenoisedScalerWithDevice:_device];
-
13:04 - Change temporal scaler encode to denoiser temporal scaler encode
// Change temporal scaler encode to denoiser temporal scaler encode temporalScaler.colorTexture = _mainView; temporalScaler.motionTexture = _motionTexture; temporalScaler.diffuseAlbedoTexture = _diffuseAlbedoTexture; temporalScaler.specularAlbedoTexture = _specularAlbedoTexture; temporalScaler.normalTexture = _normalTexture; temporalScaler.roughnessTexture = _roughnessTexture; temporalScaler.depthTexture = _depthTexture; temporalScaler.jitterOffsetX = _pixelJitter.x; temporalScaler.jitterOffsetY = -_pixelJitter.y; temporalScaler.outputTexture = _upscaledColorTarget; temporalScaler.motionVectorScaleX = (float)_motionTexture.width; temporalScaler.motionVectorScaleY = (float)_motionTexture.height; [temporalScaler encodeToCommandBuffer:commandBuffer];
-
16:04 - Creating instance descriptors for instance acceleration structure
// Creating instance descriptors for instance acceleration structure MTLAccelerationStructureInstanceDescriptor *grassInstanceDesc, *treeInstanceDesc = . . .; grassInstanceDesc.intersectionFunctionTableOffset = 0; treeInstanceDesc.intersectionFunctionTableOffset = 1; // Create buffer for instance descriptors of as many trees/grass instances the scene holds id <MTLBuffer> instanceDescs = . . .; for (auto i = 0; i < scene.instances.size(); ++i) . . .
-
-
- 0:00 - Introdução
Saiba mais sobre a série de jogos Metal 4, incluindo técnicas avançadas e melhores práticas para o desenvolvimento de jogos avançados e apps profissionais nas plataformas Apple. As APIs no Metal 4 permitem dimensionar suas cargas de trabalho para resoluções e taxas de quadros mais altas em dispositivos Apple.
- 2:13 - Ampliar a renderização
O MetalFX tem um upscaler baseado em aprendizado de máquina que pode ajudar você a alcançar resoluções mais altas e taxas de quadros mais rápidas. A novidade deste ano, o upscaler temporal do MetalFX agora oferece suporte a entradas com tamanho dinâmico, permitindo que se reduza dinamicamente a resolução de entrada de quadros particularmente complexos. Se desejar, você pode usar as Dicas de Reatividade para sugerir ao upscaler sobre a reatividade dos pixels, a fim de obter resultados mais nítidos em áreas com efeitos ou partículas transparentes. E, usando a nova ferramenta de depuração de exposição, você pode verificar se o valor de exposição passado ao upscaler está correto.
- 7:17 - Interpolar quadros
Neste ano, o MetalFX Frame Interpolation introduza a capacidade de gerar um frame adicional entre dois quadros renderizados. Existem várias técnicas para renderizar a interface do usuário com o Frame Interpolation. Também há considerações a serem feitas ao controlar o ritmo e apresentar quadros interpolados e nativamente renderizados.
- 13:50 - Traçado de raios com o Metal 4
O Metal 4 incorpora novos recursos de traçado de raios relacionados à construção de estruturas de aceleração e funções de interseção. Se você estiver migrando de outras APIs, é fácil portar suas Shader Binding Tables para os buffers de função de interseção do Metal. Depois que tudo estiver funcionando, também é possível otimizar como o Metal 4 constrói suas estruturas de aceleração.
- 19:25 - Reduzir ruídos durante a ampliação
Ao traçar raios em cenas, você pode alcançar o melhor equilíbrio entre qualidade e desempenho usando técnicas de remoção de ruído com menos raios lançados. A nova API do MetalFX aprimora os pipelines de renderização de traçado de raios em tempo real ao integrar a remoção de ruído diretamente ao processo de upscaling. Isso simplifica a abordagem tradicional, que envolve etapas separadas de traçado, remoção de ruído e composição. Ao especificar buffers auxiliares sem ruído, como normais, albedo difuso, rugosidade e albedo especular, você pode obter resultados robustos, de alto desempenho e alta qualidade, sem a necessidade de ajustar parâmetros separadamente para cada cena.
- 26:08 - Próximas etapas
Se você já integrou o Upscaler do MetalFX, esta é a chance de fazer upgrade para a interpolação de frame. Se você é iniciante no MetalFX, comece com o upscaler. Em seguida, certifique-se de que seus efeitos de traçado de raios usem buffers de função de interseção e o upscaler com remoção de ruído.