Suporte ao OpenGL 4.6 com certificação de conformidade no M1
(rosenzweig.io)- O M1 ficou por anos no OpenGL 4.1, mas o driver mais recente para M1/M2 do Fedora Asahi Remix agora oferece suporte completo a OpenGL 4.6 e OpenGL ES 3.2
- O driver Linux open source da Asahi, ao contrário do driver 4.1 não conforme do fornecedor, é um driver com certificação de conformidade listado pela Khronos, ampliando a compatibilidade com cargas de trabalho modernas de OpenGL como o Blender
- O OpenGL 4.6 exige robustness, SPIR-V, clip control, cull distance, compute shaders e transform feedback aprimorado, mas o hardware do M1 não se encaixa diretamente nos padrões gráficos mais recentes
- Os recursos ausentes no hardware foram compensados com técnicas de driver e compilador: geometry shaders, tessellation e transform feedback são tratados com compute shaders, enquanto cull distance e clip control são processados por transformações de shader
- Para passar em mais de 100.000 testes de conformidade, a robustness de buffers e imagens foi implementada em software, com otimizações de clamp, preamble e contorno de mipmap para reduzir o custo adicional
OpenGL 4.6/ES 3.2 disponível no Fedora Asahi Remix
- O M1 antes só suportava até OpenGL 4.1, mas agora passa a suportar OpenGL 4.6 e OpenGL ES 3.2
- Os drivers mais recentes da série M1/M2 podem ser usados instalando o Fedora Asahi Remix
- Quem já instalou pode atualizar com o seguinte comando
dnf upgrade --refresh
- O driver Mesa open source da Asahi, ao contrário do driver 4.1 não conforme do fornecedor, possui conformidade com versões mais novas do OpenGL
- Um driver 4.6/3.2 conforme precisa passar em mais de 100.000 testes para garantir a correção
- A lista oficial da Khronos inclui o OpenGL 4.6 e o OpenGL ES 3.2 da Asahi
- Seis meses após surgir o primeiro driver conforme para a API gráfica padrão do M1 com OpenGL ES 3.1, agora o trabalho foi concluído também para OpenGL 4.6
- O suporte a Vulkan também está em andamento
Implementação de recursos para superar a barreira do OpenGL 4.1
- O OpenGL 4.6 adiciona vários recursos obrigatórios em comparação ao 4.1
- Robustness
- SPIR-V
- Clip control
- Cull distance
- Compute shaders
- Transform feedback aprimorado
- O M1 não se adapta tão bem a padrões gráficos mais novos do que o OpenGL ES 3.1
- O Vulkan torna alguns recursos opcionais, mas para colocar DirectX e OpenGL sobre ele, os recursos ausentes são necessários
- A solução existente do M1 não conseguia ir além do conjunto de recursos do OpenGL 4.1
- Os novos recursos sem suporte em hardware foram implementados com técnicas de driver
- Geometry shaders, tessellation e transform feedback são tratados com compute shaders
- Cull distance é tratado com valores interpolados transformados
- Clip control é implementado no epílogo do vertex shader
Robustness de buffer e correção em software no M1
- Tradicionalmente, GPUs priorizam desempenho acima de segurança, então código incorreto em que um shader lê fora dos limites de um buffer pode causar comportamento indefinido
- Em aplicações que lidam com shaders não confiáveis, como navegadores web, esse tipo de compromisso não é desejável
- A própria API gráfica não é um limite de segurança, então algum nível de sanitização é necessário
- Reduzir comportamentos indefinidos da API ajuda na defesa em profundidade
- Ao ativar robustness, a aplicação pode escolher um comportamento definido para acessos fora do intervalo em troca de abrir mão de parte do desempenho
- O resultado de carregamentos fora do intervalo em buffers varia conforme a API
- Direct3D e Vulkan
robustBufferAccess2: retorna 0 - OpenGL e Vulkan
robustBufferAccess: retorna 0 ou alguns dados de dentro do buffer - OpenGL ES: pode retornar um valor arbitrário, mas não pode travar
- Direct3D e Vulkan
- Como o OpenGL exige retornar 0 ou dados de dentro do buffer em acessos fora do intervalo, é possível carregar com um índice seguro calculando o mínimo sem sinal entre o último índice válido e o índice acessado
- carregamento de uniform buffer sem robustness:
load.i32 result, buffer, index - após aplicar robustness:
umin idx, index, lastseguido deload.i32 result, buffer, idx
- carregamento de uniform buffer sem robustness:
- O preamble do M1 calcula uma vez e reutiliza valores em vez de recalculá-los repetidamente em todas as threads
- Como o tamanho do uniform buffer é fixo, a aritmética extra de robustness também pode ser movida para o preamble
- Mesmo em storage buffers robustos, ainda que o próprio load/store não possa ser movido, o cálculo de clamp pode ser levado ao preamble
Implementação de robustness de vertex buffer
- Em APIs gráficas, a aplicação define o endereço base na GPU do vertex buffer e o layout dos atributos
- Cada atributo tem offset e formato
- O buffer tem stride, que representa o número de bytes por vértice
- O vertex shader lê os atributos indexando implicitamente com base no vértice
- Alguns hardwares implementam robust vertex fetch nativamente ou aceleram fetch em software com buffers checados por limites, mas o M1 não tem nenhum dos dois
- Os carregamentos de memória da GPU do M1 recebem um endereço base de 64 bits e um offset em unidades de elemento, além de oferecer a instrução
imadde multiplicação e soma inteira- Um carregamento de atributo de 32 bits pode ser implementado com duas instruções:
imad idx, stride/4, vertex, offset/4eload.i32 result, base, idx - Um atributo vetorial com 4 valores de 32 bits densamente empacotados pode ser carregado com uma única instrução:
load.v4i32 result, base, vertex << 2
- Um carregamento de atributo de 32 bits pode ser implementado com duas instruções:
- A robustness exige clamp, mas o tamanho do vertex buffer está em bytes e o carregamento otimizado usa índice em unidades de vértice
- O problema foi resolvido reinterpretando múltiplos atributos e offsets dentro de um mesmo buffer como se fossem endereços base separados por atributo
- Em vez de somar o offset no shader, passa-se uma base por atributo
- Assim, o tamanho do buffer em bytes pode ser convertido para um tamanho em unidades de vértice para cada atributo
- O que recebe clamp é o índice do vértice, não o offset
- O driver calcula previamente o último índice de vértice válido para cada atributo usando o tamanho do formato do atributo e o envia ao shader
- Se o buffer for pequeno demais para carregar qualquer coisa, o clamp não resolve; nesse caso, o buffer daquele atributo é trocado por um pequeno zero buffer
- Como são usados endereços base por atributo, essa decisão também pode ser tomada por atributo
- No fim, um vertex buffer robusto foi implementado com um pequeno cálculo no driver e o custo de apenas um
umin
Robustness de imagem e contorno de mipmap
- Além da robustness de buffers, também é necessária robustness de imagem, e carregamentos fora do intervalo em imagens devem retornar 0
- Imagens com mipmap contêm vários níveis de detalhe
- O nível base é a imagem original
- Cada nível seguinte é uma versão reduzida do anterior
- Na renderização, o hardware escolhe um nível próximo ao tamanho na tela para melhorar eficiência e qualidade visual
- A especificação exige que, com robustness, image load retorne 0 nos seguintes casos
- Quando a coordenada X ou Y está fora do intervalo
- Quando o nível está fora do intervalo
- O comportamento de image load na GPU do M1 é diferente do exigido
- Se a coordenada X ou Y estiver fora do intervalo, retorna 0
- Se o nível estiver fora do intervalo, retorna o valor do último nível
- Como o fornecedor não publica documentação do hardware, não é possível saber se esse comportamento é intencional ou um bug de hardware, e foi necessário um contorno para passar na conformidade
- O contorno mais simples seria usar branch: carregar só quando o nível for válido e, caso contrário, retornar 0, mas branches são ineficientes
- Uma forma melhor é aproveitar que o carregamento não trava mesmo com nível fora do intervalo, fazer o load primeiro e depois escolher 0 com compare-select
- Porém, o conjunto de instruções da GPU do M1 é escalar, e image load retorna um vetor com 4 componentes red/green/blue/alpha
- Isso exigiria
uleselpara cada componente, aumentando o assembly
- O contorno final aproveita que o hardware retorna 0 quando X ou Y estão fora do intervalo
- Como a largura máxima da imagem é 16384 px, trocar X por um valor como 20000 o coloca fora do intervalo
- Se o nível for válido, usa-se o X original; se não for válido, X vira 20000 para fazer image load retornar 0
- Esse método gera assembly compacto porque altera apenas um escalar, sem precisar selecionar o vetor inteiro
- Se a constante for carregada previamente em um registrador uniforme, o custo do contorno é de uma única instrução
- Com isso, foi possível passar na conformidade
1 comentários
Comentários do Hacker News
Alyssa Rosenzweig parece um grande presente que continua contribuindo para a comunidade
A cada post do blog, sempre acabo aprendendo algo que não sabia sobre a estrutura interna do hardware gráfico moderno
É um trabalho que mostra, toda vez, que competência vence palavras
Só de ler o blog, há tanta coisa para destrinchar que a cabeça esquenta; e, embora a conclusão esteja na segunda frase, não na última, você acaba descendo pela toca do coelho acompanhando cada manipulação de bits
Se existisse um benchmark de número de insights por parágrafo, acho que Alyssa ficaria em primeiro em todos
Talvez, se um dia a Apple abandonar o OpenGL 3.3 core, todo mundo acabe abandonando também
Em geral, ouvi dizer que OpenGL é mais fácil de usar que Vulkan, mas APIs complexas demais podem dificultar o aproveitamento da GPU por desenvolvedores com menos experiência e virar uma barreira de entrada, afastando desenvolvedores indie de jogos
Hoje em dia todo mundo usa Unity e Unreal, então criar algo do zero ou usar outro motor parece estranho; ver o desenvolvimento de jogos acordar depois que a Unity tentou fechar mais o cerco é interessante, mas também irritante
Open source em desenvolvimento de jogos sempre foi apertado e, embora exista o Godot, parece difícil competir seriamente com Unity e Unreal
Mesmo que o Godot seja plenamente viável, é bem provável que desenvolvedores indie continuem onde estão por serem mais familiarizados com Unity e Unreal
O estado do open source no desenvolvimento de jogos às vezes parece desesperador, e a chegada das APIs gráficas de próxima geração não torna as coisas mais fáceis
O exemplo de renderização de triângulo em OpenGL tem cerca de 200 linhas, enquanto o exemplo de renderização de triângulo em Vulkan tem cerca de 1000 linhas
Vulkan foi projetado para ser muito flexível, mas não oferece muitos recursos de conveniência
De qualquer forma, OpenGL era de alto nível demais para ser exposto como API direta do driver; faz mais sentido ter uma API de baixo nível como Vulkan como camada base e colocar algo como OpenGL por cima dela, porque isso combina melhor com o modo como o hardware de GPU funciona
Além disso, nem todo mundo usa Unity e Unreal
Os 6 indicados a Jogo do Ano no The Game Awards 2023 foram todos feitos com motores próprios, e mesmo no indie ainda há desenvolvedores que criam seus próprios motores, como em Hades
Dito isso, é verdade que a maioria usa motores prontos
Se ele atende a todos os recursos necessários e você aceita um pipeline de renderização baseado em estado, é uma boa escolha também para projetos novos
Ainda funciona e, hoje em dia, roda como uma camada sobre Metal, mas, ao compilar código GL para macOS ou iOS, os avisos de descontinuação aparecem continuamente
Dá para desativar com um define
O problema do OpenGL é que ele fica distante demais do modo como a GPU funciona, o que dificulta obter bom desempenho
Fico curioso para saber quanto deste trabalho está preso ao código da GPU do M1 e quanto dessa implementação de recurso sobre recurso poderia ser reutilizado em outros lugares
Parece muito parecido com a forma como o Zink executa recursos complexos de OpenGL sobre um Vulkan mais primitivo, mas o M1 ainda não tem um backend Vulkan como alvo
No fim, é uma questão de volume de trabalho, e isso pode ser reutilizado em vários hardwares
Também pode ajudar em hardwares antigos e bem compreendidos, mas difíceis de usar sozinhos em cargas de trabalho modernas
Tenho muita curiosidade sobre qual é o impacto no desempenho disso, especialmente em comparação com usar Metal diretamente no macOS
A resposta certamente será “depende da situação”, mas ainda assim fico curioso
Talvez a resposta esteja no texto, mas não entendi a maior parte
“Suporte de hardware” normalmente também é implementado por microcódigo da GPU e muitas vezes passa pelo mesmo silício
Qualquer recurso pode virar gargalo de desempenho, e é difícil saber onde ele vai travar antes de testar de fato
É verdade que a GPU da Apple não oferece suporte nativo a geometry shaders, mas geometry shaders têm um design ruim e não se encaixam bem no hardware de GPU
Mesmo em hardwares que de fato oferecem suporte, eles são conhecidos por serem lentos, e há um motivo para a Nvidia ter projetado mesh shading
Transform feedback também é citado com frequência, mas a GPU da Apple consegue escrever em posições arbitrárias de memória a partir de qualquer estágio de shader, então transform feedback é praticamente desnecessário
O ponto central é que a Apple implementou uma arquitetura de computação enxuta e cortou muitos resquícios antigos e recursos conhecidos por não funcionarem bem
A expressão “o M1 ficou preso ao OpenGL 4.1” não me parece adequada
Parei de acompanhar OpenGL há muito tempo, então não sei a quais recursos após a 4.1 isso se refere, mas eu ficaria muito surpreso se houvesse algo possível em OpenGL e impossível em Metal
Por outro lado, há muitas coisas possíveis em Metal que são totalmente impossíveis em OpenGL, e o fato de a linguagem de shading do Metal ter ponteiros completos já é um exemplo
Isto é para o Fedora no M1
Seria surpreendente se isso também se tornasse possível no macOS, mas fico curioso sobre o que seria necessário para criar algo assim
O driver Mesa inicial para a GPU do M1 também foi bootstrapado enviando buffers de comando para o driver AGX do macOS via IOKit
https://rosenzweig.io/blog/asahi-gpu-part-2.html
https://github.com/AsahiLinux/gpu/blob/main/demo/iokit.c
Então seria necessário um pouco mais de código de integração no lado do Mesa para passar a surface da GPU como algo que possa ser composto na tela do macOS
Pelo que entendo, para criar uma implementação adequada de Vulkan ou OpenGL é necessário um par no lado do kernel responsável pelo processamento da GPU
Talvez seja por isso que ninguém pareça estar tentando implementar Vulkan nativo para macOS
Mas não sei se seria possível sobre os drivers da Apple
Mas não sei se há motivo para gastar tantos recursos em uma API legada que não é ideal
É bem engraçado chamar de robustez a mudança de acesso fora dos limites de trap para retorno de dados arbitrários
Programação gráfica é definitivamente estranha
O ponto central de um driver de GPU é fazer aplicações quebradas rodarem, ou rodarem mais rápido
Tornar o padrão mais estrito não vai corrigir os problemas estruturais da indústria de videogames que lança código quebrado; só vai afastar os usuários
Em hardware em que desvios geralmente são muito caros, parece útil uma flag que diga ao sistema para lidar silenciosamente com casos de borda da forma mais eficiente
Também deve haver muitos usos válidos em que o programador pode ter uma confiança razoável de que esses casos de borda quase não afetam o quadro final renderizado
Juntando isso ao fato de que GPUs, em geral, não gostam muito de traps, faz sentido
Carmack também já disse que, ao projetar megatexture, foi doloroso fazer os fabricantes aceitarem a ideia de memória virtual
Em áreas dominadas pela cultura de “desempenho antes de segurança”, falar de outras linguagens de programação é quase como falar com uma parede
Sem dúvida é algo muito interessante, mas fico me perguntando por que não mirar primeiro em Vulkan
Hoje em dia parece um alvo mais importante, e já existe uma implementação de OpenGL sobre ele
Para dar suporte a um recurso específico do OpenGL, o driver Vulkan precisa suportar o recurso correspondente, e normalmente é necessária uma extensão
Ou seja, implementar apenas um driver Vulkan básico não dá suporte a OGL 4.6 de graça; para fazer o Mesa traduzir OGL 4.6 para Vulkan, é preciso implementar no driver Vulkan todos os recursos do OGL 4.6
Além disso, Alyssa já trabalhou com engenharia reversa e em projetos de driver OpenGL
Não sei os detalhes, mas é bem possível que criar um driver para uma API familiar seja muito mais fácil e rápido do que para uma API desconhecida
A partir daí, subir até um OpenGL mais recente provavelmente exigiu menos trabalho do que uma implementação completa de Vulkan, e eles provavelmente aprenderam muito do que é necessário para Vulkan também
Então parece que escolheram começar por uma versão mais baixa do OpenGL para obter algo funcionando mais rapidamente
É bem surpreendente pensar que, se John Carmack não tivesse insistido em usar OpenGL em Quake II nos anos 90, talvez o OpenGL não tivesse ganhado presença nos jogos 3D
O que acabou transformando o OpenGL no que ele se tornou foi a SGI e o enorme esforço para criar implementações compatíveis em vários sistemas e arquiteturas
https://web.archive.org/web/19970707113513/http://www.opengl.org/
Muitas coisas relacionadas a jogos 3D existem graças a Doom e Quake
Ele via que a Microsoft tinha coragem de continuar fazendo grandes mudanças incompatíveis para melhorar a API, enquanto o OpenGL ficava travado por preocupações de compatibilidade
Disse que o Direct3D lidava melhor com multithreading e que as versões mais recentes também tinham melhor gerenciamento de estado
Ainda assim, afirmou que a id Software continuava no OpenGL por inércia e que, apesar das vantagens, não havia planos de migrar para o Direct3D
Fonte: https://www.bit-tech.net/news/gaming/pc/carmack-directx-better-opengl/1/