O Teste Que Não Conseguia Encontrar o dxcompiler.dll
Um runtime que trava quando uma DLL opcional está ausente silenciosamente se confinou a um único tipo de máquina. Portabilidade de verdade significa inicializar em qualquer lugar e degradar bem alto — e é isso que parceiros implantam.
Sábado de manhã, painel de CI, oito testes em vermelho. Todos os oito falharam com o mesmo código de erro do Windows: 0xc0000135. Qualquer um que tenha passado tempo com desenvolvimento nativo para Windows reconhece esse na hora. STATUS_DLL_NOT_FOUND. O processo tentou carregar uma DLL que precisava e a DLL não estava no sistema.
A DLL em questão era dxcompiler.dll, o runtime do compilador de shader DirectX da Microsoft. A máquina de CI não a tinha. Máquinas de usuário de produção podem não tê-la. Em qualquer lugar fora da estação de trabalho bem equipada de um desenvolvedor, o runtime tinha estado silenciosamente presumindo que dxcompiler.dll estava presente e travando duro quando não estava.
Ao final do sábado, três problemas estavam corrigidos e a suíte de testes estava em 89% de aprovação, subindo de 71%. Este é o post sobre cada um deles.
Por que a dependência era rígida para começar
O caminho de cross-compilação de shader usa o compilador DXC da Microsoft para pegar código-fonte de shader HLSL e produzir bytecode SPIR-V ou DXIL. A maioria dos shaders entregues pelo engine é pré-compilada. Alguns caminhos de código no runtime conseguem compilar novos shaders em tempo de execução: recarga a quente de permutação de shader durante o desenvolvimento, edição dinâmica de material no editor, e alguns caminhos de depuração.
A implementação original linkava contra dxcompiler.lib com uma diretiva #pragma comment(lib, "dxcompiler.lib") na fonte de cross-compilação de shader. Isso produzia uma dependência rígida em tempo de carregamento. O carregador do Windows resolve dependências em tempo de carregamento quando o processo inicia; se a DLL estiver ausente, o processo nunca chega ao main(). O executável do runtime não conseguia iniciar sem a DLL no sistema.
Essa é a resposta certa para um desenvolvedor que tem o SDK completo do DXC instalado. É a resposta errada para um executor de CI, uma máquina de usuário, ou qualquer outro lugar onde o DXC não está presente. O runtime deveria inicializar. O recurso de recarga a quente deveria reportar “não disponível.” Tudo o mais deveria continuar.
Como a correção foi
Três mudanças, cada uma pequena, cada uma precisa.
A fonte de cross-compilação de shader mudou de carregamento em tempo de carregamento para carregamento de DLL em tempo de execução. Substituí a diretiva #pragma comment(lib, ...) por chamadas explícitas de LoadLibrary e GetProcAddress. A DLL é buscada no primeiro uso, não na inicialização do processo. Se LoadLibrary falha, o caminho de código retorna um erro claro.
Caminhos de fallback adicionados para quando o DXC está ausente. Quando dxcompiler.dll não está presente, o subsistema de cross-compilação de shader cai para um de dois caminhos. Se o D3DCompiler mais antigo estiver disponível, ele cai para esse com funcionalidade reduzida (sem saída SPIR-V, só DXIL). Se até o D3DCompiler estiver ausente, ele cai para um blob SPIR-V placeholder que produz um fragment shader ruidoso apenas magenta. O placeholder garante que o runtime continue a funcionar de ponta a ponta em ambientes onde nenhum compilador de shader está disponível; o sinal visual torna óbvio que você está rodando em modo placeholder.
O caminho de falha graciosa registra e emite telemetria. Todo fallback é registrado em nível WARNING com o subsistema específico que caiu para o fallback e a razão. A telemetria registra o fallback para visibilidade de operações. Um desenvolvedor rodando localmente sem o DXC vê o aviso no console dele e sabe que precisa instalar o DXC se precisar de compilação real de shader. Um usuário rodando o runtime com a presunção de que tudo está pré-compilado não vê nenhum aviso porque os shaders pré-compilados funcionam bem sem o DXC.
Essa é a forma certa para a dependência. O runtime é portátil. A capacidade é opcional baseada no que está instalado. O modo de falha é observável.
Dois bugs adjacentes que caíram da mesma auditoria
Enquanto eu estava no código de carregamento de shader com a tampa aberta, outros dois testes estavam falhando de formas adjacentes e os corrigi na mesma passagem.
Incompatibilidade de ABI dos efeitos do barramento de áudio. As funções da API em C raku_audio_bus_add_effect e raku_audio_bus_remove_effect tinham sido escritas com uma assinatura (handle, struct*), mas a suíte de testes estava as chamando com (handle, handle) porque é isso que o resto da API de áudio usava. Os testes estavam dando segfault porque a desreferência do ponteiro de struct estava lendo o que quer que estivesse no endereço que os bits do segundo handle pareciam. Incompatibilidade de ABI.
A correção foi mudar a API em C para bater com o resto do módulo de áudio: (handle, handle). Essa é a forma certa para a API porque cadeias de efeito no barramento de áudio são objetos de primeira classe que o runtime rastreia. A versão de ponteiro de struct tinha sido um resquício de um design de API anterior que não sobreviveu ao resto da refatoração. A suíte de testes estava certa; a implementação estava desatualizada.
Stubs de teste de vazamento de memória sobrescrevendo implementações reais. O subsistema de streaming de assets tem um teste de vazamento de memória que exercita a contagem de referências em assets em streaming. O teste estava falhando porque uma implementação stub de AssetStreamingManager tinha ficado na fixture de teste e estava sobrescrevendo a implementação real da DLL do runtime. O teste estava exercitando o stub, não o código real. O stub tinha um vazamento de memória. A implementação real não tinha. O teste estava certo sobre haver um vazamento; estava errado sobre de quem era o vazamento.
A correção foi remover o stub da fixture de teste e adicionar exports RAKU_STREAMING_API na implementação real para que o teste pudesse linkar contra ela de forma limpa. Uma vez linkado corretamente, o teste passou contra o código real.
O que as três correções desbloquearam juntas
Oito testes ficaram verdes com a correção do dxcompiler. Dois com a correção do barramento de áudio. Três com a correção do vazamento de memória. O total de testes passando subiu de 39/55 para 49/55. O número de 89% é o marco certo para celebrar, mas o marco mais profundo é que o runtime agora funciona sem o DXC, o que significa que vai funcionar em ambientes que eu ainda não antecipei.
O que eu aprendi
Três coisas.
Dependências rígidas em tempo de carregamento são um defeito de portabilidade. Em qualquer lugar onde seu runtime tem uma dependência em tempo de carregamento de uma DLL ou objeto compartilhado que não está universalmente presente, o runtime se restringiu a ambientes que incluem aquela DLL. Essa restrição é aceitável se feita de propósito. É ruim se feita por acidente. A auditoria de “o que nosso runtime exige em tempo de carregamento” vale a pena rodar.
0xc0000135 é o código de erro de DLL do Windows mais comum, e a mensagem não revela nada. O erro te diz que uma DLL está ausente. Não te diz qual. As ferramentas de diagnóstico para descobrir qual (Process Monitor, Dependencies.exe, o novo rastreamento ETW do Windows) todas funcionam, mas todas exigem que você saiba que existem e as configure antes da falha acontecer. O runtime agora emite uma mensagem de erro clara quando um fallback gracioso dispara, nomeando a DLL que estava ausente. O eu do futuro vai ficar grato.
Incompatibilidades de ABI entre testes e implementações geralmente estão corretas do lado do teste. Os testes foram escritos contra a API que o resto da base de código expunha. Quando os testes discordavam da implementação, os testes estavam certos. Esse é o inverso do instinto usual, que é “corrigir o teste.” O instinto certo é olhar para a superfície de API mais ampla e perguntar qual lado é a exceção.
O que parceiros e construtores devem tirar disso
Se você está avaliando um engine para parceria e está implantando em ambientes onde a máquina do desenvolvedor e o ambiente de produção são diferentes (o que é quase toda implantação), pergunte ao time sobre suas dependências rígidas em tempo de carregamento. A resposta certa é uma lista curta, toda justificada. A resposta errada é uma lista longa, metade da qual o time esqueceu.
Se você é um desenvolvedor nativo para Windows e ainda não está usando LoadLibrary mais GetProcAddress para qualquer DLL que não esteja universalmente presente, esse é o empurrãozinho gentil. O padrão é pequeno. O ganho de portabilidade é grande.
Se você é um laboratório de IA cujo agente de codificação escreve código para Windows, os padrões a observar em código escrito por agentes são diretivas #pragma comment(lib, ...) que deveriam ser carregamentos em tempo de execução e assinaturas de ABI que se desviam do resto da base de código. Ambos são sinalizados por análise estática. Ambos deveriam estar na lista de verificação de revisão do agente.
Fechamento de fim de semana. O runtime inicializa de forma limpa em máquinas que não têm o DXC. Onze testes ficaram verdes em dois dias. A próxima auditoria está na agenda.
De volta a construir.
Um runtime que roda onde seu hardware vive
O RakuAI é o runtime espacial construído para óculos inteligentes e o mundo real bagunçado — portátil, gracioso sob dependências ausentes, observável quando cai para o fallback. Veja o que é preciso para implantar em qualquer lugar.