C'est essentiellement parce que tous les GPU ne peuvent pas prendre en charge les appels de fonction - et même s'ils le peuvent, les appels de fonction peuvent être assez lents ou avoir des limitations telles qu'une très faible profondeur de pile.
Le code de shader et le code de calcul GPU peuvent sembler avoir des appels de fonction partout, mais dans des circonstances normales, ils sont tous 100% alignés par le compilateur. Le code machine exécuté par le GPU contient des branches et des boucles, mais aucun appel de fonction. Cependant, les appels de fonction récursifs ne peuvent pas être alignés pour des raisons évidentes. (À moins que certains des arguments ne soient des constantes au moment de la compilation, de telle sorte que le compilateur puisse les replier et aligner l'arborescence complète des appels.)
Pour implémenter de vrais appels de fonction, vous avez besoin d'une pile. La plupart du temps, le code de shader n'utilise pas du tout de pile: les GPU ont de gros fichiers de registre et les shaders peuvent conserver toutes leurs données dans des registres tout le temps. Il est difficile de faire fonctionner une pile car (a) vous auriez besoin de beaucoup d'espace de pile pour fournir toutes les nombreuses chaînes pouvant être en vol à la fois, et (b) le système de mémoire GPU est optimisé pour regrouper beaucoup de transactions de mémoire pour atteindre un débit élevé, mais cela se fait au détriment de la latence, donc je suppose que les opérations de pile comme la sauvegarde / restauration de variables locales seraient terriblement lentes.
Historiquement, les appels de fonction au niveau matériel n'ont pas été trop utiles sur le GPU, car il était plus logique de tout intégrer dans le compilateur. Les architectes GPU n'ont donc pas cherché à les rendre rapides. Des compromis différents pourraient probablement être faits, s'il existe une demande d'appels au niveau matériel efficaces à l'avenir, mais (comme pour tout ce qui concerne l'ingénierie), cela entraînera un coût ailleurs.
En ce qui concerne le lancer de rayons, la façon dont les gens gèrent généralement ce genre de chose consiste à créer des files d'attente de rayons qui sont en train d'être tracées. Au lieu de récursif, vous ajoutez un rayon à une file d'attente, et au niveau élevé quelque part, vous avez une boucle qui continue le traitement jusqu'à ce que toutes les files d'attente soient vides. Cependant, cela nécessite une réorganisation importante de votre code de rendu si vous commencez à partir d'un raytracer récursif classique. Pour plus d'informations, un bon article à lire à ce sujet est Wavefront Path Tracing .