/***** * rendererloader.cc * On Unix: Probe for Vulkan and OpenGL availability at runtime via dlopen. * On Windows: Vulkan is linked against vulkan-1.dll; the renderer is * directly instantiated. If no hardware GPU is detected (e.g., pre-2012 * hardware, VirtualBox VM), a llvmpipe fallback is activated by writing * an ICD manifest (lvp_icd.json) and loading vulkan_lvp.dll (Lavapipe). * On macOS (Intel x86_64): If Metal is unavailable (e.g., headless VM), * fall back to a shipped llvmpipe driver by locating libvulkan_lvp.dylib * via the Asymptote search path and writing an ICD manifest. * Apple Silicon (arm64) is not supported by llvmpipe; use SwiftShader instead. * * The main asy binary has zero link-time dependencies on Vulkan or OpenGL * on Unix. All Vulkan-specific code lives in libasyvulkan.so, loaded at runtime. * All OpenGL-specific code lives in libasyopengl.so, loaded at runtime. * * On Windows, Vulkan is linked directly into the asy binary. * * For WebGL (html) and v3d output, NoRender is used instead, which * requires no GPU libraries - it only sets up state for client-side rendering. *****/ #include "rendererloader.h" #include "camperror.h" #ifdef __APPLE__ // Include glfw3.h with Vulkan support BEFORE renderBase.h, which includes // it with GLFW_INCLUDE_NONE. This ensures we get both the Vulkan types // (PFN_vkGetInstanceProcAddr) and the glfwInitVulkanLoader declaration. #define VK_NO_PROTOTYPES #define GLFW_INCLUDE_VULKAN #include #endif #include "renderBase.h" #include "norender.h" extern pthread_mutex_t main_wait_mutex; extern pthread_cond_t main_wait_cond; #ifdef _WIN32 #include "vkrender.h" #endif bool vulkan = false; #ifdef HAVE_RENDERER #ifdef _WIN32 #include #include // For llvmpipe fallback: we need raw Vulkan types to probe device availability. #define VK_NO_PROTOTYPES #include #else #include #endif #include #include #include #include #include "settings.h" // for settings::verbose #include "locate.h" // for settings::locateFile namespace camp { bool headlessRenderer = false; // True when using software renderer without display (e.g., llvmpipe on macOS without Metal) static bool initializedRenderer = false; static void signalRendererReady() { #ifdef HAVE_PTHREAD pthread_mutex_lock(&main_wait_mutex); pthread_cond_broadcast(&main_wait_cond); pthread_mutex_unlock(&main_wait_mutex); #endif } // Opaque handles to the loaded renderer shared libraries. #ifdef _WIN32 static HMODULE vulkanLibHandle = nullptr; static HMODULE glLibHandle = nullptr; static HMODULE lvpLibHandle = nullptr; #else static void *vulkanLibHandle = nullptr; static void *glLibHandle = nullptr; #endif /** * Load a renderer shared library by searching the Asymptote path. * Returns a valid handle on success, or nullptr on failure. */ #ifndef _WIN32 static void *loadRendererLib(const char *libName) { mem::string locPath = settings::locateFile(libName, true, ""); std::string pathStr = mem::stdString(locPath); return dlopen(pathStr.c_str(), RTLD_NOW | RTLD_LOCAL); } #endif /** * Attempt to load libasyvulkan.so and create a Vulkan renderer. * Returns true on success (gl is set to the new AsyVkRender, vulkan=true). * Returns false on failure (gl remains nullptr, vulkan=false). */ static bool tryLoadVulkanLib() { #ifdef _WIN32 // On Windows, try LoadLibrary. vulkanLibHandle = LoadLibraryA("libasyvulkan.dll"); if (!vulkanLibHandle) { if (settings::verbose > 1) std::cout << "Failed to load libasyvulkan.dll; falling back to OpenGL" << std::endl; return false; } // Get the factory function. typedef void *(*CreateAsyVkRenderFn)(); CreateAsyVkRenderFn fn = reinterpret_cast(GetProcAddress(vulkanLibHandle, "createAsyVkRender")); #else mem::string locPath = settings::locateFile("libasyvulkan.so", true, ""); std::string pathStr = mem::stdString(locPath); vulkanLibHandle = dlopen(pathStr.c_str(), RTLD_NOW | RTLD_LOCAL); if (!vulkanLibHandle) { if (settings::verbose > 1) std::cout << "Failed to load libasyvulkan.so (" << dlerror() << "); falling back to OpenGL" << std::endl; return false; } if (settings::verbose > 2) std::cout << "Loaded libasyvulkan.so from: " << pathStr << std::endl; // Get the factory function. typedef void *(*CreateAsyVkRenderFn)(); CreateAsyVkRenderFn fn = reinterpret_cast(dlsym(vulkanLibHandle, "createAsyVkRender")); #endif if (!fn) { if (settings::verbose > 1) std::cout << "Failed to find createAsyVkRender in libasyvulkan.so" << std::endl; return false; } // Create the Vulkan renderer via the factory function. void *vkRenderer = fn(); if (!vkRenderer) { if (settings::verbose > 1) std::cout << "createAsyVkRender() returned NULL" << std::endl; return false; } gl = static_cast(vkRenderer); #ifdef HAVE_PTHREAD if (gl) gl->threadMgr.mainthread = pthread_self(); #endif signalRendererReady(); vulkan = true; return true; } /** * Attempt to load libasyopengl.so and create an OpenGL renderer. * Returns true on success (gl is set to the new AsyGLRender, vulkan=false). * Returns false on failure (gl remains nullptr). */ static bool tryLoadOpenGLLib() { #ifdef _WIN32 // On Windows, try LoadLibrary. glLibHandle = LoadLibraryA("libasyopengl.dll"); if (!glLibHandle) { if (settings::verbose > 1) std::cout << "Failed to load libasyopengl.dll" << std::endl; return false; } // Get the factory function. typedef void *(*CreateAsyGLRenderFn)(); CreateAsyGLRenderFn fn = reinterpret_cast(GetProcAddress(glLibHandle, "createAsyGLRender")); #else mem::string locPath = settings::locateFile("libasyopengl.so", true, ""); std::string pathStr = mem::stdString(locPath); glLibHandle = dlopen(pathStr.c_str(), RTLD_NOW | RTLD_LOCAL); if (!glLibHandle) { if (settings::verbose > 1) std::cout << "Failed to load libasyopengl.so (" << dlerror() << ")" << std::endl; return false; } if (settings::verbose > 2) std::cout << "Loaded libasyopengl.so from: " << pathStr << std::endl; // Get the factory function. typedef void *(*CreateAsyGLRenderFn)(); CreateAsyGLRenderFn fn = reinterpret_cast(dlsym(glLibHandle, "createAsyGLRender")); #endif if (!fn) { if (settings::verbose > 1) std::cout << "Failed to find createAsyGLRender in libasyopengl.so" << std::endl; return false; } // Create the OpenGL renderer via the factory function. void *glRenderer = fn(); if (!glRenderer) { if (settings::verbose > 1) std::cout << "createAsyGLRender() returned NULL" << std::endl; return false; } gl = static_cast(glRenderer); #ifdef HAVE_PTHREAD if (gl) gl->threadMgr.mainthread = pthread_self(); #endif signalRendererReady(); vulkan = false; return true; } /** * Lightweight probe: check if libasyvulkan.so can be loaded. * Used by settings.cc to report enabled/disabled options. * Does NOT create a renderer or set the global vulkan flag. */ bool tryLoadVulkan() { #ifdef _WIN32 // On Windows, Vulkan is statically linked; always available. return true; #else void *h = loadRendererLib("libasyvulkan.so"); if (h) { dlclose(h); return true; } return false; #endif } /** * Lightweight probe: check if libasyopengl.so can be loaded. * Used by settings.cc to report enabled/disabled options. * Does NOT create a renderer or set the global vulkan flag. */ bool tryLoadOpenGL() { #ifdef _WIN32 // On Windows, OpenGL is not supported; only Vulkan is available. return false; #else void *h = loadRendererLib("libasyopengl.so"); if (h) { dlclose(h); return true; } return false; #endif } void unloadVulkan() { if (vulkanLibHandle) { #ifdef _WIN32 FreeLibrary(vulkanLibHandle); #else dlclose(vulkanLibHandle); #endif vulkanLibHandle = nullptr; } // Also unload the llvmpipe fallback DLL on Windows. #ifdef _WIN32 if (lvpLibHandle) { FreeLibrary(lvpLibHandle); lvpLibHandle = nullptr; } #endif } void unloadOpenGL() { if (glLibHandle) { #ifdef _WIN32 FreeLibrary(glLibHandle); #else dlclose(glLibHandle); #endif glLibHandle = nullptr; } } /** * Create a NoRender instance for html/v3d output. * This does NOT require Vulkan or OpenGL libraries - it only sets up state * needed by jsfile.cc and v3dfile.cc to generate the output files. * * If a Vulkan/OpenGL renderer was already created (e.g., by createRenderer()), * we replace the global pointer with NoRender. The old renderer is NOT * deleted to avoid triggering cleanup code (like glslang::FinalizeProcess()) * that can cause issues when called during program shutdown. */ static void createNoRenderer() { // Replace the global pointer without deleting the old renderer. // This avoids triggering Vulkan/OpenGL cleanup code that can cause // assertion failures at program exit (e.g., glslang::FinalizeProcess()). // The old renderer will be cleaned up when the process exits. if (gl != nullptr) { // Don't delete - just leak the old renderer (acceptable for short-lived processes) gl = nullptr; } gl = new camp::NoRender(); #ifdef HAVE_PTHREAD if (gl) gl->threadMgr.mainthread = pthread_self(); #endif signalRendererReady(); #ifdef HAVE_RENDERER vulkan = false; // NoRender is not Vulkan #endif } /** * Create the renderer object by probing for Vulkan/OpenGL availability. * Called lazily from initRenderer() when shipout3() first needs GPU rendering. * On Unix this loads the appropriate shared library (libasyvulkan.so or * libasyopengl.so) via dlopen. The render thread's glrenderWrapper() is * safe with a null gl pointer, so there is no need to pre-initialise the * renderer in main(). * * On Windows: Vulkan is statically linked; directly instantiate AsyVkRender. * On Unix: Try to load the requested renderer via dlopen/LoadLibrary. * If the user requested Vulkan (-vulkan, which is the default), attempt * to load libasyvulkan.so first. If that fails or the user requested * OpenGL (-novulkan), load libasyopengl.so as a fallback. */ void createRenderer() { if (gl != nullptr) return; // Already created bool useVulkan = settings::getSetting("vulkan"); #ifdef _WIN32 // On Windows, Vulkan is linked against vulkan-1.dll. Initialize the // Vulkan-Hpp dynamic dispatcher before creating the renderer. VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr); // Probe for a hardware GPU. If none is available (e.g., pre-2012 // hardware, VirtualBox VM), fall back to llvmpipe via Lavapipe. bool hasHardwareGPU = false; try { VkInstanceCreateInfo info = {}; info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; VkInstance tmpInst; if (vkCreateInstance(&info, nullptr, &tmpInst) == VK_SUCCESS) { uint32_t devCount = 0; vkEnumeratePhysicalDevices(tmpInst, &devCount, nullptr); if (devCount > 0) { std::vector devices(devCount); vkEnumeratePhysicalDevices(tmpInst, &devCount, devices.data()); for (uint32_t i = 0; i < devCount; ++i) { VkPhysicalDeviceProperties props; vkGetPhysicalDeviceProperties(devices[i], &props); if (props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU || props.deviceType == VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU || props.deviceType == VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU) { hasHardwareGPU = true; break; } } } vkDestroyInstance(tmpInst, nullptr); } } catch (...) { // If probing fails for any reason, proceed without the check. } if (!hasHardwareGPU) { // No hardware GPU found -- set up llvmpipe (Lavapipe) fallback. // Strategy: write lvp_icd.json next to the executable and set // VK_ICD_FILENAMES so the Vulkan loader picks it up on the next // instance creation. // 1) Determine the directory of our own executable. std::string exeDir; { char buf[MAX_PATH]; DWORD len = GetModuleFileNameA(nullptr, buf, MAX_PATH); if (len > 0 && len < MAX_PATH) { std::string exePath(buf); size_t slash = exePath.find_last_of("\\/"); if (slash != std::string::npos) exeDir = exePath.substr(0, slash + 1); } } // 2) Write lvp_icd.json next to the executable. std::string icdName = "lvp_icd.json"; std::string icdPath; if (!exeDir.empty()) icdPath = exeDir + icdName; else icdPath = icdName; { std::ofstream ofs(icdPath.c_str(), std::ios::trunc); if (ofs.is_open()) { ofs << "{\n" << " \"file_format_version\": \"1.0.0\",\n" << " \"ICD\": {\n" << " \"library_path\": \".\\\\vulkan_lvp.dll\",\n" << " \"api_version\": \"1.3.0\"\n" << " }\n" << "}\n"; if (settings::verbose > 1) std::cout << "Wrote Lavapipe ICD manifest: " << icdPath << std::endl; } } // 3) Set VK_ICD_FILENAMES so the loader finds our manifest. _putenv_s("VK_ICD_FILENAMES", icdPath.c_str()); // 4) Load vulkan_lvp.dll (the Lavapipe driver). std::string lvpDllPath; if (!exeDir.empty()) lvpDllPath = exeDir + "vulkan_lvp.dll"; else lvpDllPath = "vulkan_lvp.dll"; lvpLibHandle = LoadLibraryA(lvpDllPath.c_str()); if (lvpLibHandle) { if (settings::verbose > 1) std::cout << "Loaded llvmpipe fallback: " << lvpDllPath << std::endl; } else { if (settings::verbose > 1) std::cout << "Warning: failed to load " << lvpDllPath << "; proceeding without llvmpipe" << std::endl; } // 5) Re-initialize the Vulkan dispatcher so it picks up the new ICD. VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr); } // Directly instantiate the Vulkan renderer. try { gl = new camp::AsyVkRender(); #ifdef HAVE_PTHREAD if (gl) gl->threadMgr.mainthread = pthread_self(); #endif signalRendererReady(); vulkan = true; if (settings::verbose > 1) std::cout << "Using Vulkan renderer" << std::endl; } catch (const std::exception &e) { // Vulkan renderer failed to initialize (e.g., no display, no GPU). // Leave gl as nullptr so initRenderer() reports the error. std::cerr << "Vulkan renderer initialization failed: " << e.what() << std::endl; } catch (...) { std::cerr << "Vulkan renderer initialization failed (unknown error)" << std::endl; } #else #if defined(__APPLE__) && defined(__x86_64__) // On Intel Macs, detect Metal availability BEFORE any Vulkan calls. // MoltenVK's ICD is found by the Vulkan loader, but if Metal is not // available (e.g., headless VM), vkCreateInstance fails with // ErrorIncompatibleDriver before we can enumerate devices. // Solution: probe Metal directly via dlopen/dlsym, and if unavailable, // use a shipped llvmpipe driver found via the Asymptote search path. { // Step 1: Open Metal.framework void *metalHandle = dlopen("/System/Library/Frameworks/Metal.framework/Metal", RTLD_NOW | RTLD_LOCAL); bool metalAvailable = false; if (metalHandle) { // Step 2: Resolve MTLCreateSystemDefaultDevice typedef void *(*PFN_MTLCreateSystemDefaultDevice)(); PFN_MTLCreateSystemDefaultDevice createDevice = reinterpret_cast( dlsym(metalHandle, "MTLCreateSystemDefaultDevice")); if (createDevice) { // Step 3: Call it - nil means Metal is not available void *device = createDevice(); if (device != nullptr) { metalAvailable = true; } } dlclose(metalHandle); } // Step 4: If Metal is NOT available and Vulkan was requested, // use the shipped llvmpipe driver from the Asymptote search path. if (!metalAvailable && useVulkan) { // Locate the shipped llvmpipe ICD JSON via the Asymptote path. mem::string icdJsonPath = settings::locateFile("lvp_icd.x86_64.json", true, ""); std::string icdJsonStr = mem::stdString(icdJsonPath); if (!icdJsonStr.empty()) { // Convert to absolute path (the Vulkan loader needs it). char *absPath = realpath(icdJsonStr.c_str(), nullptr); if (absPath != nullptr) { icdJsonStr = absPath; free(absPath); } setenv("VK_ICD_FILENAMES", icdJsonStr.c_str(), true); headlessRenderer = true; if (settings::verbose > 1) std::cout << "Metal unavailable on macOS Intel; " << "using llvmpipe fallback (" << icdJsonStr << ")" << std::endl; } else { if (settings::verbose > 1) std::cout << "Metal unavailable and llvmpipe ICD not found" << std::endl; } } } #endif // __APPLE__ && __x86_64__ // If user wants Vulkan, try to load the shared library first. if (useVulkan) { if (tryLoadVulkanLib()) { #ifdef __APPLE__ // GLFW lazily calls dlopen("libvulkan.1.dylib") inside // glfwCreateWindowSurface to get vkGetInstanceProcAddr. // In a portable/staged build the bundled Vulkan loader's // install name is "@executable_path/lib/libvulkan.1.dylib", // so that bare-name dlopen fails on a machine without the SDK. // Fix: hand GLFW the function pointer from our already-loaded // Vulkan loader so it never needs to dlopen it itself. { PFN_vkGetInstanceProcAddr pfn = (PFN_vkGetInstanceProcAddr)dlsym( vulkanLibHandle, "vkGetInstanceProcAddr"); if (pfn) glfwInitVulkanLoader(pfn); } #endif // __APPLE__ return; } // Vulkan failed to load; fall through to try OpenGL. if (settings::verbose > 1) std::cout << "Vulkan unavailable, falling back to OpenGL" << std::endl; } // Try to load the OpenGL renderer. if (tryLoadOpenGLLib()) { return; } #endif // _WIN32 // On Unix: both renderers failed to load. Leave gl as nullptr; the error will be // reported lazily in initRenderer() when 3D rendering is actually requested. #ifndef _WIN32 vulkan = false; #endif } /** * Initialise the renderer, optionally selecting based on output format. * * For WebGL (html) and v3d formats, creates NoRender which requires * no GPU libraries - it only sets up state for client-side rendering. * * For all other formats, lazily calls createRenderer() to probe for Vulkan/ * OpenGL and instantiate the appropriate renderer. This defers all GPU * library loading until a shipout3() call actually requires rendering. * * @param format Output format string (e.g., "html", "v3d", or empty/NULL for default) */ void initRenderer(const char* format) { // For WebGL and v3d output, use the lightweight NoRender // which doesn't require Vulkan or OpenGL libraries bool isFormat3D = (format != nullptr && (strcmp(format, "html") == 0 || strcmp(format, "v3d") == 0)); // If we have a NoRender but now need GPU rendering (or vice versa), // reset and re-initialize with the appropriate renderer if (initializedRenderer) { bool currentIsNoRender = dynamic_cast(gl) != nullptr; if (currentIsNoRender != isFormat3D) { initializedRenderer = false; // Clear the old renderer so a new one gets created below. // We don't delete it to avoid triggering cleanup code // (e.g., glslang::FinalizeProcess()) that can cause issues. if (currentIsNoRender) gl = nullptr; } } if (initializedRenderer) return; // Already fully initialised for this format type if (isFormat3D) { createNoRenderer(); } else if (gl == nullptr) { // Lazy initialisation: probe for Vulkan/OpenGL only when GPU // rendering is actually needed. #ifdef HAVE_PTHREAD // In threaded mode, delegate renderer creation to the glrenderWrapper // thread (the OS main thread) to avoid a race condition caused by // dlopen from a secondary thread. // since gl is still nullptr and we can't use gl->threadMgr yet. if(AsyRender::threads) { pthread_mutex_lock(&main_wait_mutex); pthread_cond_signal(&main_wait_cond); // Wake glrenderWrapper pthread_cond_wait(&main_wait_cond, &main_wait_mutex); // Wait for done pthread_mutex_unlock(&main_wait_mutex); } else #endif createRenderer(); } if (gl == nullptr) { camp::reportError("No 3D rendering available"); } initializedRenderer = true; } } // namespace camp #else // !HAVE_RENDERER namespace camp { [[maybe_unused]] static void signalRendererReady() { #ifdef HAVE_PTHREAD pthread_mutex_lock(&main_wait_mutex); pthread_cond_broadcast(&main_wait_cond); pthread_mutex_unlock(&main_wait_mutex); #endif } bool tryLoadVulkan() { return false; } bool tryLoadOpenGL() { return false; } void unloadVulkan() {} void unloadOpenGL() {} void createRenderer() {} /** * Create a NoRender instance for html/v3d output. * This does NOT require Vulkan or OpenGL libraries - it only sets up state * needed by jsfile.cc and v3dfile.cc to generate the output files. */ #ifdef HAVE_LIBGLM static void createNoRenderer() { if (gl != nullptr) gl = nullptr; gl = new camp::NoRender(); signalRendererReady(); } #endif void initRenderer(const char* format) { #ifdef HAVE_LIBGLM bool isFormat3D = (format != nullptr && (strcmp(format, "html") == 0 || strcmp(format, "v3d") == 0)); if (isFormat3D) { createNoRenderer(); } #endif // For non-format3d output without GPU libraries, picture.cc will report // a more specific error message. } } // namespace camp #endif // HAVE_RENDERER