La prueba que no pudo encontrar dxcompiler.dll
Un runtime que se cae cuando falta una DLL opcional se ha confinado silenciosamente a un solo tipo de máquina. La portabilidad real significa arrancar en todas partes y degradarse ruidosamente, y eso es lo que los socios despliegan.
Sábado por la mañana, panel de CI, ocho pruebas en rojo. Las ocho fallaron con el mismo código de error de Windows: 0xc0000135. Cualquiera que haya pasado tiempo en desarrollo nativo de Windows lo reconoce de inmediato. STATUS_DLL_NOT_FOUND. El proceso intentó cargar una DLL que necesitaba y la DLL no estaba en el sistema.
La DLL en cuestión era dxcompiler.dll, el runtime del compilador de shaders DirectX de Microsoft. La máquina de CI no la tenía. Las máquinas de usuarios en producción podrían no tenerla. En cualquier lugar fuera de la estación de trabajo bien equipada de un desarrollador, el runtime había estado asumiendo silenciosamente que dxcompiler.dll estaba presente y colapsando de forma catastrófica cuando no lo estaba.
Para el final del sábado, se corrigieron tres problemas y la suite de pruebas estaba al 89% de aprobación, desde el 71%. Este es el post sobre cada uno.
Por qué la dependencia era rígida en primer lugar
La ruta de cross-compilación de shaders usa el compilador DXC de Microsoft para tomar código fuente de shader HLSL y producir bytecode SPIR-V o DXIL. La mayoría de los shaders distribuidos del motor están precompilados. Algunas rutas de código en el runtime pueden compilar shaders nuevos en tiempo de ejecución: recarga en caliente de permutaciones de shader durante el desarrollo, edición dinámica de materiales en el editor, y un par de rutas de depuración.
La implementación original enlazaba contra dxcompiler.lib con una directiva #pragma comment(lib, "dxcompiler.lib") en el código fuente de shader-cross. Esto producía una dependencia rígida en tiempo de carga. El cargador de Windows resuelve las dependencias en tiempo de carga cuando el proceso arranca; si la DLL falta, el proceso nunca llega a main(). El ejecutable del runtime no podía iniciar sin la DLL en el sistema.
Esa es la respuesta correcta para un desarrollador que tiene instalado el SDK completo de DXC. Es la respuesta equivocada para un runner de CI, una máquina de usuario, o cualquier otro lugar donde DXC no esté presente. El runtime debería arrancar. La función de recarga en caliente debería reportar “no disponible.” Todo lo demás debería continuar.
Cómo se veía la corrección
Tres cambios, cada uno pequeño, cada uno preciso.
El código fuente de shader-cross pasó de carga de DLL en tiempo de carga a carga en tiempo de ejecución. Se reemplazó la directiva #pragma comment(lib, ...) con llamadas explícitas a LoadLibrary y GetProcAddress. La DLL se busca en el primer uso, no al arrancar el proceso. Si LoadLibrary falla, la ruta de código devuelve un error claro.
Se añadieron rutas de respaldo para cuando falta DXC. Cuando dxcompiler.dll no está presente, el subsistema shader-cross recae en una de dos rutas. Si el D3DCompiler más antiguo está disponible, recae en ese con funcionalidad reducida (sin salida SPIR-V, solo DXIL). Si incluso D3DCompiler falta, recae en un blob SPIR-V de placeholder que produce un fragment shader ruidoso, solo en magenta. El placeholder asegura que el runtime siga funcionando de extremo a extremo en entornos donde no hay disponible ningún compilador de shaders; la señal visual hace obvio que estás ejecutándote en modo placeholder.
La ruta de fallo elegante registra y emite telemetría. Cada respaldo se registra en nivel WARNING con el subsistema específico que recayó y la razón. La telemetría registra el respaldo para visibilidad de operaciones. Un desarrollador ejecutándose localmente sin DXC ve la advertencia en su consola y sabe que debe instalar DXC si necesita compilación real de shaders. Un usuario ejecutando el runtime con la suposición de que todo está precompilado no ve ninguna advertencia porque los shaders precompilados funcionan bien sin DXC.
Esta es la forma correcta para la dependencia. El runtime es portable. La capacidad es opcional según lo que esté instalado. El modo de fallo es observable.
Dos bugs adyacentes que salieron a la luz en la misma auditoría
Mientras estaba en el código de carga de shaders con la tapa abierta, otras dos pruebas fallaban de formas adyacentes y las corregí en el mismo pase.
Desajuste de ABI en efectos de bus de audio. Las funciones de API en C raku_audio_bus_add_effect y raku_audio_bus_remove_effect se habían escrito con una firma (handle, struct*), pero la suite de pruebas las llamaba con (handle, handle) porque eso es lo que usaba el resto de la API de audio. Las pruebas producían segfault porque la desreferencia del puntero de struct leía lo que fuera que residiera en la dirección que los bits del segundo handle parecían apuntar. Desajuste de ABI.
La corrección fue cambiar la API en C para que coincida con el resto del módulo de audio: (handle, handle). Esta es la forma correcta para la API porque las cadenas de efectos en el bus de audio son objetos de primera clase que el runtime rastrea. La versión con puntero de struct había sido un resabio de un diseño de API anterior que no sobrevivió al resto de la refactorización. La suite de pruebas tenía razón; la implementación estaba obsoleta.
Stubs de prueba de fuga de memoria sobrescribiendo implementaciones reales. El subsistema de streaming de assets tiene una prueba de fuga de memoria que ejercita el conteo de referencias en assets transmitidos. La prueba fallaba porque una implementación stub de AssetStreamingManager había quedado en el fixture de prueba y estaba sobrescribiendo la implementación real de la DLL del runtime. La prueba estaba ejercitando el stub, no el código real. El stub tenía una fuga de memoria. La implementación real no. La prueba tenía razón en que había una fuga; se equivocaba en de quién era la fuga.
La corrección fue eliminar el stub del fixture de prueba y añadir exportaciones RAKU_STREAMING_API en la implementación real para que la prueba pudiera enlazarse contra ella limpiamente. Una vez enlazada correctamente, la prueba pasó contra el código real.
Qué desbloquearon juntas las tres correcciones
Ocho pruebas se pusieron en verde con la corrección de dxcompiler. Dos con la corrección del bus de audio. Tres con la corrección de fuga de memoria. El total de pruebas pasando subió de 39/55 a 49/55. El número del 89% es el hito correcto para celebrar, pero el hito más profundo es que el runtime ahora funciona sin DXC, lo que significa que funcionará en entornos que aún no he anticipado.
Qué aprendí
Tres cosas.
Las dependencias rígidas en tiempo de carga son un defecto de portabilidad. En cualquier lugar donde tu runtime tenga una dependencia en tiempo de carga sobre una DLL u objeto compartido que no esté presente universalmente, el runtime se ha confinado a entornos que incluyen esa DLL. Esa restricción está bien hacerla a propósito. Es mala hacerla por accidente. La auditoría de “qué requiere nuestro runtime en tiempo de carga” vale la pena ejecutarla.
0xc0000135 es el código de error de DLL más común de Windows, y el mensaje no revela nada. El error te dice que falta una DLL. No te dice cuál. Las herramientas de diagnóstico para averiguar cuál (Process Monitor, Dependencies.exe, el nuevo trazado ETW de Windows) funcionan todas, pero todas requieren que sepas que existen y las configures antes de que ocurra el fallo. El runtime ahora emite un mensaje de error claro cuando se activa un respaldo elegante, nombrando la DLL que faltaba. El yo del futuro lo agradecerá.
Los desajustes de ABI entre pruebas e implementaciones usualmente tienen razón del lado de la prueba. Las pruebas se escribieron contra la API que exponía el resto de la base de código. Cuando las pruebas discrepaban con la implementación, las pruebas tenían razón. Este es el inverso del instinto habitual, que es “corregir la prueba.” El instinto correcto es mirar la superficie de API más amplia y preguntar qué lado es el atípico.
Qué deberían llevarse los socios y constructores de esto
Si estás evaluando un motor para una asociación y despliegas en entornos donde la máquina del desarrollador y el entorno de producción son diferentes (que es casi todo despliegue), pregúntale al equipo sobre sus dependencias rígidas en tiempo de carga. La respuesta correcta es una lista corta, toda justificada. La respuesta equivocada es una lista larga, de la que el equipo ha olvidado la mitad.
Si eres un desarrollador nativo de Windows y aún no estás usando LoadLibrary más GetProcAddress para cualquier DLL que no esté presente universalmente, este es el empujón amable. El patrón es pequeño. La ganancia en portabilidad es grande.
Si eres un laboratorio de IA cuyo agente de codificación escribe código de Windows, los patrones a vigilar en código escrito por agentes son directivas #pragma comment(lib, ...) que deberían ser cargas en tiempo de ejecución y firmas de ABI que se desvían del resto de la base de código. Ambos son señalados por análisis estático. Ambos deberían estar en la lista de verificación de revisión del agente.
Cierre de fin de semana. El runtime arranca limpiamente en máquinas que no tienen DXC. Once pruebas se pusieron en verde en dos días. La próxima auditoría está en el calendario.
De vuelta a construir.
Un runtime que funciona donde vive tu hardware
RakuAI es el runtime espacial construido para gafas inteligentes y el mundo real desordenado: portable, elegante ante dependencias faltantes, observable cuando recae en un respaldo. Descubre lo que se necesita para desplegar en todas partes.