From 886fba56c03c0b6135742bd4e34e3cb21a10ac5a Mon Sep 17 00:00:00 2001 From: utzcoz Date: Wed, 5 Aug 2026 23:12:51 +0800 Subject: [PATCH 1/7] third_party: angle: build ANGLE on macOS ANGLE's macOS sources are Objective-C++, which cc_library cannot compile, so they move into a dedicated angle_apple objc_library that the main libraries depend on when targeting macOS. The Vulkan back end is built against the Metal and IOSurface window system integration, and the platform selects gain a macOS arm so that the target graph resolves on Darwin. This produces libEGL.dylib and libGLESv2.dylib, which the GLES tests need in order to run on macOS at all. --- third_party/angle/BUILD.angle.bazel | 251 +++++++++++++++++++++++----- 1 file changed, 212 insertions(+), 39 deletions(-) diff --git a/third_party/angle/BUILD.angle.bazel b/third_party/angle/BUILD.angle.bazel index b28fcfd89..14a9beb34 100644 --- a/third_party/angle/BUILD.angle.bazel +++ b/third_party/angle/BUILD.angle.bazel @@ -1,11 +1,50 @@ load("@bazel_skylib//lib:selects.bzl", "selects") -load("@rules_cc//cc:defs.bzl", "cc_library", "cc_shared_library") +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_shared_library", "objc_library") load("@rules_python//python:defs.bzl", "py_binary") package( default_visibility = ["@//:gfxstream"], ) +# The Vulkan window-system-integration (display/surface) sources are +# platform-specific. On Linux these are compiled directly; on macOS the +# equivalents are Objective-C++ and are built in the `angle_apple` objc_library +# below (cc_library can not compile .mm files). +ANGLE_VULKAN_LINUX_WSI_SRCS = [ + "src/libANGLE/renderer/vulkan/linux/DeviceVkLinux.cpp", + "src/libANGLE/renderer/vulkan/linux/DisplayVkLinux.cpp", + "src/libANGLE/renderer/vulkan/linux/DisplayVkOffscreen.cpp", + "src/libANGLE/renderer/vulkan/linux/DmaBufImageSiblingVkLinux.cpp", + "src/libANGLE/renderer/vulkan/linux/display/DisplayVkSimple.cpp", + "src/libANGLE/renderer/vulkan/linux/display/WindowSurfaceVkSimple.cpp", + "src/libANGLE/renderer/vulkan/linux/headless/DisplayVkHeadless.cpp", + "src/libANGLE/renderer/vulkan/linux/headless/WindowSurfaceVkHeadless.cpp", +] + +# Linux-only local_defines. On macOS ANGLE selects its platform via the +# compiler-provided __APPLE__ / ANGLE_PLATFORM_APPLE macros, uses the Metal/ +# IOSurface Vulkan display (not the Linux vulkan-display), and has no X11. +ANGLE_PLATFORM_LOCAL_DEFINES = select({ + "@platforms//os:macos": [], + "//conditions:default": [ + "ANGLE_IS_LINUX", + "ANGLE_USE_VULKAN_DISPLAY", + "ANGLE_VULKAN_DISPLAY_MODE_OFFSCREEN", + "EGL_NO_X11", + ], +}) + +# Frameworks required by ANGLE's macOS Objective-C++ sources. +ANGLE_APPLE_SDK_FRAMEWORKS = [ + "AppKit", + "Cocoa", + "CoreGraphics", + "IOKit", + "IOSurface", + "Metal", + "QuartzCore", +] + cc_library( name = "angle_common", srcs = [ @@ -31,13 +70,21 @@ cc_library( "src/common/platform_helpers.cpp", "src/common/string_utils.cpp", "src/common/system_utils.cpp", - "src/common/system_utils_linux.cpp", "src/common/system_utils_posix.cpp", "src/common/third_party/xxhash/xxhash.c", "src/common/tls.cpp", "src/common/uniform_type_info_autogen.cpp", "src/common/utilities.cpp", - ], + ] + select({ + "@platforms//os:macos": [ + "src/common/gl/cgl/FunctionsCGL.cpp", + "src/common/system_utils_apple.cpp", + "src/common/system_utils_mac.cpp", + ], + "//conditions:default": [ + "src/common/system_utils_linux.cpp", + ], + }), hdrs = [ "src/common/BinaryStream.h", "src/common/CircularBuffer.h", @@ -103,7 +150,15 @@ cc_library( "src/common/utilities.h", "src/common/vector_utils.h", "src/libANGLE/CLBitField.h", - ], + ] + select({ + "@platforms//os:macos": [ + "src/common/apple/ObjCPtr.h", + "src/common/apple/SoftLinking.h", + "src/common/apple/apple_platform.h", + "src/common/gl/cgl/FunctionsCGL.h", + ], + "//conditions:default": [], + }), defines = [ # TODO: use bazel `select({})` on debug build condition. # Useful for debugging: @@ -211,6 +266,9 @@ cc_library( "@platforms//os:linux": [ "third_party/glslang/src/glslang/OSDependent/Unix/ossource.cpp", ], + "@platforms//os:macos": [ + "third_party/glslang/src/glslang/OSDependent/Unix/ossource.cpp", + ], "@platforms//os:windows": [ "third_party/glslang/src/glslang/OSDependent/Windows/ossource.cpp", ], @@ -475,9 +533,14 @@ cc_library( name = "angle_gpu_info_util", srcs = [ "src/gpu_info_util/SystemInfo.cpp", - "src/gpu_info_util/SystemInfo_linux.cpp", "src/gpu_info_util/SystemInfo_vulkan.cpp", - ], + ] + select({ + # macOS SystemInfo (.mm) is built in :angle_apple. + "@platforms//os:macos": [], + "//conditions:default": [ + "src/gpu_info_util/SystemInfo_linux.cpp", + ], + }), hdrs = [ "src/gpu_info_util/SystemInfo.h", "src/gpu_info_util/SystemInfo_internal.h", @@ -676,6 +739,12 @@ cc_library( hdrs = [ "src/third_party/volk/volk.h", ], + # On macOS volk must define the Metal surface entry points (e.g. + # vkCreateMetalSurfaceEXT) that the mac WSI calls. + local_defines = select({ + "@platforms//os:macos": ["VK_USE_PLATFORM_METAL_EXT"], + "//conditions:default": [], + }), includes = [ "src/third_party/volk", "third_party/vulkan-headers/src/include", @@ -1156,7 +1225,6 @@ cc_library( ], linkstatic = True, local_defines = [ - 'ANGLE_DISPATCH_LIBRARY=\\\"libGLESv2.so\\\"', 'ANGLE_EGL_LIBRARY_NAME=\\"libEGL\\"', "ANGLE_ENABLE_CONTEXT_MUTEX=1", "ANGLE_ENABLE_SHARE_CONTEXT_LOCK=1", @@ -1175,7 +1243,16 @@ cc_library( "GL_GLES_PROTOTYPES=1", "GL_GLEXT_PROTOTYPES", "LIBEGL_IMPLEMENTATION", - ], + ] + select({ + # ANGLE appends the platform shared-library extension, so the name must + # not already include one (libGLESv2.dylib on macOS, libGLESv2.so else). + "@platforms//os:macos": [ + 'ANGLE_DISPATCH_LIBRARY=\\\"libGLESv2\\\"', + ], + "//conditions:default": [ + 'ANGLE_DISPATCH_LIBRARY=\\\"libGLESv2.so\\\"', + ], + }), deps = [ ":angle_common", ":includes", @@ -2112,14 +2189,6 @@ cc_library( "src/libANGLE/renderer/vulkan/VkImageImageSiblingVk.cpp", "src/libANGLE/renderer/vulkan/VulkanSecondaryCommandBuffer.cpp", "src/libANGLE/renderer/vulkan/android/vk_android_utils.cpp", - "src/libANGLE/renderer/vulkan/linux/DeviceVkLinux.cpp", - "src/libANGLE/renderer/vulkan/linux/DisplayVkLinux.cpp", - "src/libANGLE/renderer/vulkan/linux/DisplayVkOffscreen.cpp", - "src/libANGLE/renderer/vulkan/linux/DmaBufImageSiblingVkLinux.cpp", - "src/libANGLE/renderer/vulkan/linux/display/DisplayVkSimple.cpp", - "src/libANGLE/renderer/vulkan/linux/display/WindowSurfaceVkSimple.cpp", - "src/libANGLE/renderer/vulkan/linux/headless/DisplayVkHeadless.cpp", - "src/libANGLE/renderer/vulkan/linux/headless/WindowSurfaceVkHeadless.cpp", "src/libANGLE/renderer/vulkan/spv_utils.cpp", "src/libANGLE/renderer/vulkan/vk_cache_utils.cpp", "src/libANGLE/renderer/vulkan/vk_caps_utils.cpp", @@ -2132,7 +2201,11 @@ cc_library( "src/libANGLE/renderer/vulkan/vk_renderer.cpp", "src/libANGLE/renderer/vulkan/vk_resource.cpp", "src/libANGLE/renderer/vulkan/vk_utils.cpp", - ], + ] + select({ + # macOS uses the Metal/IOSurface Vulkan WSI, built in :angle_apple. + "@platforms//os:macos": [], + "//conditions:default": ANGLE_VULKAN_LINUX_WSI_SRCS, + }), hdrs = [ "src/libANGLE/renderer/vulkan/AllocatorHelperPool.h", "src/libANGLE/renderer/vulkan/AllocatorHelperRing.h", @@ -2387,19 +2460,98 @@ cc_library( alwayslink = True, ) +# ANGLE's macOS Objective-C++ sources: apple platform utilities, GPU info, and +# the Metal/IOSurface Vulkan window-system-integration. These can not live in the +# cc_library targets above (cc_library rejects .mm), so they are compiled here and +# linked into the final libEGL/libGLESv2 shared libraries. +objc_library( + name = "angle_apple", + srcs = [ + "src/common/apple_platform_utils.mm", + "src/gpu_info_util/SystemInfo_apple.mm", + "src/gpu_info_util/SystemInfo_macos.mm", + "src/libANGLE/renderer/driver_utils_mac.mm", + "src/libANGLE/renderer/vulkan/mac/DisplayVkMac.h", + "src/libANGLE/renderer/vulkan/mac/DisplayVkMac.mm", + "src/libANGLE/renderer/vulkan/mac/IOSurfaceSurfaceVkMac.h", + "src/libANGLE/renderer/vulkan/mac/IOSurfaceSurfaceVkMac.mm", + "src/libANGLE/renderer/vulkan/mac/WindowSurfaceVkMac.h", + "src/libANGLE/renderer/vulkan/mac/WindowSurfaceVkMac.mm", + ], + # ANGLE's Objective-C++ uses manual reference counting. The -D flags mirror + # the local_defines of the cc_library targets whose headers these sources use + # (objc_library has no local_defines attribute). + copts = [ + "-fno-objc-arc", + "-DANGLE_ENABLE_CONTEXT_MUTEX=1", + "-DANGLE_ENABLE_CRC_FOR_PIPELINE_CACHE", + "-DANGLE_ENABLE_SHARE_CONTEXT_LOCK=1", + "-DANGLE_ENABLE_VULKAN", + "-DANGLE_ENABLE_VULKAN_SHARED_RING_BUFFER_CMD_ALLOC=false", + "-DANGLE_OUTSIDE_WEBKIT", + "-DANGLE_PLATFORM_EXPORT=", + "-DANGLE_SHARED_LIBVULKAN=1", + "-DANGLE_USE_CUSTOM_VULKAN_OUTSIDE_RENDER_PASS_CMD_BUFFERS=1", + "-DANGLE_USE_CUSTOM_VULKAN_RENDER_PASS_CMD_BUFFERS=1", + "-DLIBANGLE_IMPLEMENTATION", + # Enable the Vulkan Metal surface extension (VkMetalSurfaceCreateInfoEXT, + # VK_EXT_METAL_SURFACE_EXTENSION_NAME, CAMetalLayer) used by the mac WSI. + "-DVK_USE_PLATFORM_METAL_EXT", + ], + includes = [ + "include/", + "src/", + "src/common/base/", + "src/common/third_party/xxhash", + "third_party/spirv-headers/src/include/", + "third_party/vulkan-headers/src/include", + ], + sdk_frameworks = ANGLE_APPLE_SDK_FRAMEWORKS, + target_compatible_with = [ + "@platforms//os:macos", + ], + deps = [ + ":angle_common", + ":angle_gpu_info_util", + ":angle_image_util", + ":angle_libvulkan_loader", + ":angle_vk_mem_alloc_wrapper", + ":angle_volk", + ":angle_vulkan_backend", + ":angle_vulkan_entry_points", + ":angle_vulkan_headers", + ":libangle_headers", + ":vulkan", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/container:flat_hash_set", + ], + alwayslink = True, +) + cc_shared_library( name = "libEGL", - additional_linker_inputs = [ - "@//third_party/angle:libEGL_angle.lds", - ], - shared_lib_name = "libEGL.so", - user_link_flags = [ - "-ldl", - "-Wl,--gc-sections", - "-Wl,-rpath=$$ORIGIN", - "-Wl,--version-script=$(location @//third_party/angle:libEGL_angle.lds)", - ] + selects.with_or({ + additional_linker_inputs = select({ + "@platforms//os:macos": [], + "//conditions:default": ["@//third_party/angle:libEGL_angle.lds"], + }), + shared_lib_name = select({ + "@platforms//os:macos": "libEGL.dylib", + "//conditions:default": "libEGL.so", + }), + user_link_flags = select({ + "@platforms//os:macos": [ + "-Wl,-dead_strip", + "-Wl,-rpath,@loader_path", + ], + "//conditions:default": [ + "-ldl", + "-Wl,--gc-sections", + "-Wl,-rpath=$$ORIGIN", + "-Wl,--version-script=$(location @//third_party/angle:libEGL_angle.lds)", + ], + }) + selects.with_or({ ( + "@platforms//os:macos", "@//toolchain/bazel:asan_build", "@//toolchain/bazel:msan_build", "@//toolchain/bazel:ubsan_build", @@ -2410,22 +2562,38 @@ cc_shared_library( }), deps = [ ":libegl", - ], + ] + select({ + "@platforms//os:macos": [ + ":angle_apple", + ], + "//conditions:default": [], + }), ) cc_shared_library( name = "libGLESv2", - additional_linker_inputs = [ - "@//third_party/angle:libGLESv2_angle.lds", - ], - shared_lib_name = "libGLESv2.so", - user_link_flags = [ - "-ldl", - "-Wl,--gc-sections", - "-Wl,-rpath=$$ORIGIN", - "-Wl,--version-script=$(location @//third_party/angle:libGLESv2_angle.lds)", - ] + selects.with_or({ + additional_linker_inputs = select({ + "@platforms//os:macos": [], + "//conditions:default": ["@//third_party/angle:libGLESv2_angle.lds"], + }), + shared_lib_name = select({ + "@platforms//os:macos": "libGLESv2.dylib", + "//conditions:default": "libGLESv2.so", + }), + user_link_flags = select({ + "@platforms//os:macos": [ + "-Wl,-dead_strip", + "-Wl,-rpath,@loader_path", + ], + "//conditions:default": [ + "-ldl", + "-Wl,--gc-sections", + "-Wl,-rpath=$$ORIGIN", + "-Wl,--version-script=$(location @//third_party/angle:libGLESv2_angle.lds)", + ], + }) + selects.with_or({ ( + "@platforms//os:macos", "@//toolchain/bazel:asan_build", "@//toolchain/bazel:msan_build", "@//toolchain/bazel:ubsan_build", @@ -2436,5 +2604,10 @@ cc_shared_library( }), deps = [ ":libglesv2", - ], + ] + select({ + "@platforms//os:macos": [ + ":angle_apple", + ], + "//conditions:default": [], + }), ) From 2f6537c3aa3ac26b3799d0340ff4fc4015a09ad9 Mon Sep 17 00:00:00 2001 From: utzcoz Date: Wed, 5 Aug 2026 23:12:51 +0800 Subject: [PATCH 2/7] host: search LD_LIBRARY_PATH by hand when dlopen fails macOS System Integrity Protection strips DYLD_LIBRARY_PATH from the environment, so a bare dlopen cannot find a library that was staged into a directory named there. Fall back to trying each LD_LIBRARY_PATH entry as a full path, which is not subject to that stripping. Needed so that tests can point the loader at a driver directory they staged themselves. --- host/library/shared_library.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/host/library/shared_library.cpp b/host/library/shared_library.cpp index f8736fe56..12fcb29a5 100644 --- a/host/library/shared_library.cpp +++ b/host/library/shared_library.cpp @@ -185,6 +185,25 @@ SharedLibrary* SharedLibrary::do_open(const char* libraryName, libraryName, libPath); lib = dlopen(libPath, RTLD_NOW); } + if (lib == NULL) { + // DYLD_LIBRARY_PATH is stripped from the environment in some contexts + // (e.g. macOS System Integrity Protection), so a bare dlopen can not find + // libraries there. Search LD_LIBRARY_PATH manually with full paths, which + // is not subject to that stripping. + const std::vector libraryPaths = + gfxstream::Split(gfxstream::base::getEnvironmentVariable("LD_LIBRARY_PATH"), ":"); + for (const std::string& libraryPath : libraryPaths) { + if (libraryPath.empty()) { + continue; + } + const std::string fullpath = PathUtils::join(libraryPath, libPath); + GFXSTREAM_VERBOSE("Calling dlopen on %s.", fullpath.c_str()); + lib = dlopen(fullpath.c_str(), RTLD_NOW); + if (lib != nullptr) { + break; + } + } + } #else GFXSTREAM_VERBOSE("SharedLibrary::open for [%s] (posix,linux): call dlopen on [%s]", libraryName, libPath); void* lib = nullptr; From d53b830202233710f93afc492100649b0bf16b01 Mon Sep 17 00:00:00 2001 From: utzcoz Date: Wed, 5 Aug 2026 23:12:51 +0800 Subject: [PATCH 3/7] host: vulkan: disable MoltenVK's Metal argument buffers on macOS The external driver selected through VK_DRIVER_FILES on macOS is normally MoltenVK. With Metal argument buffers enabled, pipelines that bind an immutable YCbCr sampler, which is what the YUV compositor pipelines do, fail SPIR-V-to-MSL conversion and are never built. Compositing a planar-YUV layer then aborts the renderer. MVK_CONFIG_* variables are ignored by drivers that are not MoltenVK, so this is safe to set unconditionally on macOS. --- host/vulkan/vulkan_dispatch.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/host/vulkan/vulkan_dispatch.cpp b/host/vulkan/vulkan_dispatch.cpp index 4a64b48df..d27a7b5c8 100644 --- a/host/vulkan/vulkan_dispatch.cpp +++ b/host/vulkan/vulkan_dispatch.cpp @@ -119,6 +119,16 @@ static void initIcdPaths(bool forTesting) { #endif if (androidIcd == "") { // Rely on user to set VK_DRIVER_FILES +#ifdef __APPLE__ + // The external driver selected via VK_DRIVER_FILES on macOS is normally + // MoltenVK. Apply the MoltenVK argument-buffer workaround here too: with + // Metal argument buffers enabled, pipelines that bind an immutable YCbCr + // sampler (the YUV compositor pipelines) fail SPIR-V-to-MSL conversion + // ("argument buffer resource base type could not be determined") and are + // never built, which later aborts compositing a YUV layer. MVK_CONFIG_* + // variables are ignored by non-MoltenVK drivers. See b/364055067. + gfxstream::base::setEnvironmentVariable("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", "0"); +#endif return; } From cd53a90c01a06df938ed4198ab9c91f15c47de48 Mon Sep 17 00:00:00 2001 From: utzcoz Date: Wed, 5 Aug 2026 23:12:51 +0800 Subject: [PATCH 4/7] host: testlibs: compile the macOS OSWindow as Objective-C++ CreateOSWindow on macOS is written in Objective-C++, which cc_library cannot compile. Move it into a dedicated objc_library that the test support library depends on when targeting macOS, and split the headers out so that both can include them. --- host/testlibs/oswindow/BUILD.bazel | 47 ++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/host/testlibs/oswindow/BUILD.bazel b/host/testlibs/oswindow/BUILD.bazel index a4297aa4e..6798b393a 100644 --- a/host/testlibs/oswindow/BUILD.bazel +++ b/host/testlibs/oswindow/BUILD.bazel @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("@rules_cc//cc:defs.bzl", "cc_library") +load("@rules_cc//cc:defs.bzl", "cc_library", "objc_library") load("//:build_variables.bzl", "GFXSTREAM_HOST_COPTS", "GFXSTREAM_HOST_DEFINES") package( @@ -20,6 +20,43 @@ package( default_visibility = ["//:gfxstream"], ) +# The macOS OSWindow implementation (CreateOSWindow) is Objective-C++, which +# cc_library can not compile, so it lives in a dedicated objc_library that the +# main library depends on when targeting macOS. +objc_library( + name = "gfxstream_oswindow_test_support_darwin", + testonly = True, + srcs = [ + "osx/OSXWindow.h", + "osx/OSXWindow.mm", + ], + # OSXWindow.mm uses manual reference counting, so disable ARC. + copts = GFXSTREAM_HOST_COPTS + ["-fno-objc-arc"], + defines = GFXSTREAM_HOST_DEFINES, + # OSXWindow.mm includes "osx/OSXWindow.h" relative to this package. + includes = ["."], + sdk_frameworks = [ + "AppKit", + "QuartzCore", + ], + target_compatible_with = [ + "@platforms//os:macos", + ], + deps = [ + ":gfxstream_oswindow_test_support_headers", + "//common/logging:gfxstream_common_logging", + "//third_party/opengl:gfxstream_egl_headers", + ], + alwayslink = True, +) + +cc_library( + name = "gfxstream_oswindow_test_support_headers", + testonly = True, + hdrs = glob(["include/**/*.h"]), + strip_include_prefix = "include", +) + cc_library( name = "gfxstream_oswindow_test_support", testonly = True, @@ -27,7 +64,6 @@ cc_library( "OSWindow.cpp", ] + select({ "@platforms//os:macos": [ - "osx/OSXWindow.h", ], "@platforms//os:windows": [ "windows/Windows_system_utils.cpp", @@ -58,5 +94,10 @@ cc_library( deps = [ "//common/logging:gfxstream_common_logging", "//third_party/opengl:gfxstream_egl_headers", - ], + ] + select({ + "@platforms//os:macos": [ + ":gfxstream_oswindow_test_support_darwin", + ], + "//conditions:default": [], + }), ) From 99733fbe0ea56ee7428a586dab0e20e9ba898a25 Mon Sep 17 00:00:00 2001 From: utzcoz Date: Wed, 5 Aug 2026 23:12:51 +0800 Subject: [PATCH 5/7] common: testenv: add an ANGLE on MoltenVK test environment Add gles_angle_vulkan_moltenvk, which uses ANGLE for GLES on top of the system MoltenVK for Vulkan. MoltenVK is provided at runtime rather than as a build artifact, so only the ANGLE libraries are staged as data. ANGLE looks for the Vulkan loader in its own module directory, and DYLD_LIBRARY_PATH is unavailable under System Integrity Protection, so the environment copies the ANGLE libraries and the loader into a writable directory and loads ANGLE from there. The staged copies inherit the read-only permissions of their build outputs, so each destination is removed before it is written; that also keeps staging idempotent, since it runs once per test in a process. The driver basenames differ by platform, and the filesystem calls now report their error_code instead of discarding it. --- common/testenv/BUILD.bazel | 24 +++++ common/testenv/graphics_test_environment.cpp | 95 +++++++++++++++++++- 2 files changed, 115 insertions(+), 4 deletions(-) diff --git a/common/testenv/BUILD.bazel b/common/testenv/BUILD.bazel index 8533dcd13..be0d887e0 100644 --- a/common/testenv/BUILD.bazel +++ b/common/testenv/BUILD.bazel @@ -14,6 +14,7 @@ string_flag( "host", "gles_angle_vulkan_lavapipe", "gles_angle_vulkan_swiftshader", + "gles_angle_vulkan_moltenvk", ], ) @@ -38,6 +39,13 @@ config_setting( }, ) +config_setting( + name = "graphics_test_environment_gles_angle_vulkan_moltenvk", + flag_values = { + ":graphics_test_environment": "gles_angle_vulkan_moltenvk", + }, +) + copy_to_directory( name = "graphics_test_environment_drivers", srcs = select({ @@ -55,6 +63,12 @@ copy_to_directory( "@angle//:libGLESv2", "@swiftshader//:vk_swiftshader", ], + # macOS: ANGLE for GLES, running on top of the system MoltenVK Vulkan + # driver (provided at runtime, not as a Bazel artifact). + ":graphics_test_environment_gles_angle_vulkan_moltenvk": [ + "@angle//:libEGL", + "@angle//:libGLESv2", + ], "//conditions:default": [ # Default does not change anything and uses host drivers. ], @@ -85,6 +99,9 @@ cc_library( ":graphics_test_environment_gles_angle_vulkan_swiftshader": [ ":graphics_test_environment_drivers", ], + ":graphics_test_environment_gles_angle_vulkan_moltenvk": [ + ":graphics_test_environment_drivers", + ], "//conditions:default": [], }), defines = select({ @@ -96,6 +113,10 @@ cc_library( "GFXSTREAM_TESTING_USE_GLES_ANGLE", "GFXSTREAM_TESTING_USE_VULKAN_SWIFTSHADER", ], + ":graphics_test_environment_gles_angle_vulkan_moltenvk": [ + "GFXSTREAM_TESTING_USE_GLES_ANGLE", + "GFXSTREAM_TESTING_USE_VULKAN_MOLTENVK", + ], "//conditions:default": [ # Default does not change anything and uses host drivers. ], @@ -108,6 +129,9 @@ cc_library( ":graphics_test_environment_gles_angle_vulkan_swiftshader": [ "@rules_cc//cc/runfiles", ], + ":graphics_test_environment_gles_angle_vulkan_moltenvk": [ + "@rules_cc//cc/runfiles", + ], "//conditions:default": [], }) + [ "//common/base:gfxstream_common_base", diff --git a/common/testenv/graphics_test_environment.cpp b/common/testenv/graphics_test_environment.cpp index 6d2c48171..f753c1828 100644 --- a/common/testenv/graphics_test_environment.cpp +++ b/common/testenv/graphics_test_environment.cpp @@ -19,6 +19,10 @@ #include #include +#if defined(__APPLE__) +#include +#endif + #ifdef BAZEL_CURRENT_REPOSITORY #include #endif @@ -95,14 +99,21 @@ bool SetupGraphicsTestEnvironment() { // gfxstream::base::setEnvironmentVariable("__EGL_VENDOR_LIBRARY_FILENAMES", driverEglIcd); // // For now, assume the ANGLE libs are directly used: - const auto driverGlesOpt = GetGraphicsDriverPath("libGLESv2.so"); +#if defined(__APPLE__) + static constexpr const char* kGlesDriverBasename = "libGLESv2.dylib"; + static constexpr const char* kEglDriverBasename = "libEGL.dylib"; +#else + static constexpr const char* kGlesDriverBasename = "libGLESv2.so"; + static constexpr const char* kEglDriverBasename = "libEGL.so"; +#endif + const auto driverGlesOpt = GetGraphicsDriverPath(kGlesDriverBasename); if (!driverGlesOpt) { - GFXSTREAM_ERROR("Failed to find libGLESv2.so."); + GFXSTREAM_ERROR("Failed to find %s.", kGlesDriverBasename); return false; } - const auto driverEglOpt = GetGraphicsDriverPath("libEGL.so"); + const auto driverEglOpt = GetGraphicsDriverPath(kEglDriverBasename); if (!driverEglOpt) { - GFXSTREAM_ERROR("Failed to find libEGL.so"); + GFXSTREAM_ERROR("Failed to find %s", kEglDriverBasename); return false; } const std::filesystem::path driverEgl = *driverEglOpt; @@ -111,6 +122,67 @@ bool SetupGraphicsTestEnvironment() { const std::string currentLdLibraryPath = gfxstream::base::getEnvironmentVariable("LD_LIBRARY_PATH"); const std::string updatedLdLibraryPath = driverDirectory.string() + ":" + currentLdLibraryPath; gfxstream::base::setEnvironmentVariable("LD_LIBRARY_PATH", updatedLdLibraryPath); + +#if defined(GFXSTREAM_TESTING_USE_VULKAN_MOLTENVK) + // ANGLE searches its own module directory for the Vulkan loader, and + // DYLD_LIBRARY_PATH is unavailable under macOS System Integrity Protection. + // The Bazel output directory holding the ANGLE libraries is read-only, so + // stage the ANGLE libraries and the system Vulkan loader together in a + // writable directory and load ANGLE from there. The loader path and the + // MoltenVK ICD are provided by the caller (see CI). + const std::string vulkanLoaderSource = + gfxstream::base::getEnvironmentVariable("GFXSTREAM_TESTING_VULKAN_LOADER"); + if (vulkanLoaderSource.empty()) { + GFXSTREAM_ERROR("GFXSTREAM_TESTING_VULKAN_LOADER is not set for the MoltenVK environment."); + return false; + } + std::error_code stagingError; + const std::filesystem::path temporaryDirectory = + std::filesystem::temp_directory_path(stagingError); + if (stagingError) { + GFXSTREAM_ERROR("Failed to find a temporary directory: %s.", + stagingError.message().c_str()); + return false; + } + const std::filesystem::path stagingDirectory = + temporaryDirectory / ("gfxstream_moltenvk_drivers_" + std::to_string(getpid())); + std::filesystem::create_directories(stagingDirectory, stagingError); + if (stagingError) { + GFXSTREAM_ERROR("Failed to create staging directory %s: %s.", + stagingDirectory.string().c_str(), stagingError.message().c_str()); + return false; + } + // The staged copies inherit the read-only permissions of their Bazel source, + // so a later call (SetupGraphicsTestEnvironment runs once per test) cannot + // truncate them to overwrite. Remove any existing destination first, which + // also keeps staging idempotent across tests in the same process. + const auto stageInto = [&](const std::filesystem::path& source, + const std::filesystem::path& destination) -> bool { + std::error_code removeError; + std::filesystem::remove(destination, removeError); + std::error_code copyError; + std::filesystem::copy_file(source, destination, + std::filesystem::copy_options::overwrite_existing, copyError); + if (copyError) { + GFXSTREAM_ERROR("Failed to stage %s into %s: %s.", source.string().c_str(), + destination.string().c_str(), copyError.message().c_str()); + return false; + } + return true; + }; + const auto stageFile = [&](const std::filesystem::path& source) -> bool { + return stageInto(source, stagingDirectory / source.filename()); + }; + if (!stageFile(*driverGlesOpt) || !stageFile(driverEgl)) { + return false; + } + // Stage the loader under the name ANGLE looks for. + if (!stageInto(vulkanLoaderSource, stagingDirectory / "libvulkan.dylib")) { + return false; + } + gfxstream::base::setEnvironmentVariable( + "LD_LIBRARY_PATH", stagingDirectory.string() + ":" + updatedLdLibraryPath); +#endif // defined(GFXSTREAM_TESTING_USE_VULKAN_MOLTENVK) #else GFXSTREAM_INFO("GraphicsTestEnvironment: not changing host EGL/GLES driver configuration."); #endif // defined(GFXSTREAM_TESTING_USE_GLES_ANGLE) @@ -149,6 +221,19 @@ bool SetupGraphicsTestEnvironment() { const std::string driverLavapipeIcd = driverLavapipeIcdOpt->string(); gfxstream::base::setEnvironmentVariable("VK_DRIVER_FILES", driverLavapipeIcd); gfxstream::base::setEnvironmentVariable("VK_ICD_FILENAMES", driverLavapipeIcd); +#elif defined(GFXSTREAM_TESTING_USE_VULKAN_MOLTENVK) + GFXSTREAM_INFO("GraphicsTestEnvironment: configuring MoltenVK as the Vulkan driver."); + + // MoltenVK is provided by the system (e.g. Homebrew), not as a build + // artifact. The caller provides the ICD via GFXSTREAM_TESTING_VULKAN_ICD. + const std::string moltenVkIcd = + gfxstream::base::getEnvironmentVariable("GFXSTREAM_TESTING_VULKAN_ICD"); + if (moltenVkIcd.empty()) { + GFXSTREAM_ERROR("GFXSTREAM_TESTING_VULKAN_ICD is not set for the MoltenVK environment."); + return false; + } + gfxstream::base::setEnvironmentVariable("VK_DRIVER_FILES", moltenVkIcd); + gfxstream::base::setEnvironmentVariable("VK_ICD_FILENAMES", moltenVkIcd); #else GFXSTREAM_INFO("GraphicsTestEnvironment: not changing host Vulkan driver configuration."); #endif // defined(GFXSTREAM_TESTING_USE_VULKAN_LAVAPIPE) || defined(GFXSTREAM_TESTING_USE_VULKAN_SWIFTSHADER) @@ -161,6 +246,8 @@ bool IsGraphicsTestEnvironmentProvidingVulkanDriver() { return true; #elif defined(GFXSTREAM_TESTING_USE_VULKAN_SWIFTSHADER) return true; +#elif defined(GFXSTREAM_TESTING_USE_VULKAN_MOLTENVK) + return true; #else return false; #endif From 085a2ad2d73d4c311f2fe1bc3050c4b127110f03 Mon Sep 17 00:00:00 2001 From: utzcoz Date: Wed, 5 Aug 2026 23:12:51 +0800 Subject: [PATCH 6/7] host: vulkan: request VK_KHR_portability_subset only where it exists A portability-subset implementation such as MoltenVK requires the extension to be enabled when the portability enumeration flag is set, but requesting it on a device that does not report it fails device creation. Ask the device first. --- host/vulkan/compositor_vk_unittest.cpp | 50 +++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/host/vulkan/compositor_vk_unittest.cpp b/host/vulkan/compositor_vk_unittest.cpp index f119a7d25..ffbadeb74 100644 --- a/host/vulkan/compositor_vk_unittest.cpp +++ b/host/vulkan/compositor_vk_unittest.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -339,11 +340,20 @@ class CompositorVkTest : public ::testing::Test { .engineVersion = VK_MAKE_VERSION(1, 0, 0), .apiVersion = VK_API_VERSION_1_1, }; + std::vector instanceExtensions; + VkInstanceCreateFlags instanceCreateFlags = 0; +#if defined(__APPLE__) + // MoltenVK is a portability driver; the Vulkan loader only enumerates it + // when the portability enumeration extension and flag are set. + instanceExtensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + instanceCreateFlags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; +#endif const VkInstanceCreateInfo instanceCi = { .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, + .flags = instanceCreateFlags, .pApplicationInfo = &appInfo, - .enabledExtensionCount = 0, - .ppEnabledExtensionNames = nullptr, + .enabledExtensionCount = static_cast(instanceExtensions.size()), + .ppEnabledExtensionNames = instanceExtensions.data(), }; ASSERT_NE(k_vk->vkCreateInstance, nullptr); ASSERT_EQ(k_vk->vkCreateInstance(&instanceCi, nullptr, &m_vkInstance), VK_SUCCESS); @@ -385,6 +395,25 @@ class CompositorVkTest : public ::testing::Test { FAIL() << "Can't find a suitable VkPhysicalDevice."; } +#if defined(__APPLE__) + bool deviceSupportsExtension(const char* extensionName) { + uint32_t extensionCount = 0; + if (k_vk->vkEnumerateDeviceExtensionProperties(m_vkPhysicalDevice, nullptr, &extensionCount, + nullptr) != VK_SUCCESS) { + return false; + } + std::vector extensions(extensionCount); + if (k_vk->vkEnumerateDeviceExtensionProperties(m_vkPhysicalDevice, nullptr, &extensionCount, + extensions.data()) != VK_SUCCESS) { + return false; + } + return std::any_of(extensions.begin(), extensions.end(), + [extensionName](const VkExtensionProperties& extension) { + return strcmp(extension.extensionName, extensionName) == 0; + }); + } +#endif + void createLogicalDevice() { const float queuePriority = 1.0f; const VkDeviceQueueCreateInfo queueCi = { @@ -397,14 +426,27 @@ class CompositorVkTest : public ::testing::Test { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = nullptr, }; + std::vector deviceExtensions; +#if defined(__APPLE__) + // A portability driver such as MoltenVK only exposes the Vulkan + // portability subset, which must be enabled explicitly when creating the + // device. Requesting it on a driver that does not advertise it fails with + // VK_ERROR_EXTENSION_NOT_PRESENT, so only enable it when it is present. + // The extension name macro is gated behind VK_ENABLE_BETA_EXTENSIONS, so + // use the literal string. + static constexpr const char* kPortabilitySubset = "VK_KHR_portability_subset"; + if (deviceSupportsExtension(kPortabilitySubset)) { + deviceExtensions.push_back(kPortabilitySubset); + } +#endif const VkDeviceCreateInfo deviceCi = { .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, .pNext = &features, .queueCreateInfoCount = 1, .pQueueCreateInfos = &queueCi, .enabledLayerCount = 0, - .enabledExtensionCount = 0, - .ppEnabledExtensionNames = nullptr, + .enabledExtensionCount = static_cast(deviceExtensions.size()), + .ppEnabledExtensionNames = deviceExtensions.data(), }; ASSERT_EQ(k_vk->vkCreateDevice(m_vkPhysicalDevice, &deviceCi, nullptr, &m_vkDevice), VK_SUCCESS); From e687d23b42db25acd4ec01ac4448feac1d8a1c82 Mon Sep 17 00:00:00 2001 From: utzcoz Date: Wed, 5 Aug 2026 23:13:04 +0800 Subject: [PATCH 7/7] ci: run the macOS host and GLES tests The macOS Bazel job only built until now. Give it the host driver environment and run the Vulkan host tests against MoltenVK, and add a second job that runs the GLES framebuffer tests through ANGLE on MoltenVK. The reusable workflow grows the inputs those need: which driver environment to select, which test targets to run, and extra flags for the test step only. They are passed through the environment rather than expanded into the script, the way the existing inputs are, and globbing is disabled around the test command so that a value such as --test_arg=--gtest_filter=*Foo* survives verbatim. macOS runners now also install molten-vk and vulkan-loader, which together give the tests a Vulkan implementation to run against. --- .github/workflows/presubmit.yaml | 41 +++++++++++++++++++-- .github/workflows/presubmit_bazel.yml | 53 ++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/.github/workflows/presubmit.yaml b/.github/workflows/presubmit.yaml index 79e42298e..ac368f37e 100644 --- a/.github/workflows/presubmit.yaml +++ b/.github/workflows/presubmit.yaml @@ -79,10 +79,43 @@ jobs: //host:gfxstream_backend_static //host:gfxstream_backend_shared //host:gfxstream_backend - # Nothing is run yet: this job exists to keep the macOS host build - # compiling. Tests need a graphics driver environment that macOS does not - # have here yet. - run-tests: false + # Use the default host driver environment: SwiftShader can not be built on + # macOS. Vulkan-backed tests instead run against MoltenVK (installed via + # brew), pointed at by the ANDROID_EMU_VK_LOADER_PATH / VK_*_FILES env + # vars below. + graphics-drivers: host + run-tests: true + # The GLES framebuffer test needs the ANGLE GLES driver and runs in a + # separate job below, as do the end2end tests (which additionally need the + # mesa guest driver and the kumquat server built on top of it). + test-targets: >- + //host:gfxstream_vsyncthread_tests + //host/vulkan:vk_format_utils_tests + //host/vulkan:gfxstream_emulatedphysicalmemory_tests + //host/vulkan:gfxstream_emulatedphysicalqueue_tests + //host/vulkan:vk_common_operations_tests + //host/vulkan:gfxstream_displayvk_tests + //host/vulkan:gfxstream_compositorvk_tests + test-bazel-args: >- + --test_env=ANDROID_EMU_VK_LOADER_PATH=/opt/homebrew/lib/libvulkan.dylib + --test_env=VK_DRIVER_FILES=/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json + --test_env=VK_ICD_FILENAMES=/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json + + # GLES tests use ANGLE (built on macOS) for the EGL/GLES driver, running on top + # of MoltenVK for Vulkan. This uses the gles_angle_vulkan_moltenvk test + # environment, which stages the ANGLE libraries and the system Vulkan loader + # together and points them at MoltenVK. + run-gfxstream-bazel-gles-build-macos: + uses: ./.github/workflows/presubmit_bazel.yml + with: + runner: macos-26 + build-targets: //host:gfxstream_framebuffer_tests + graphics-drivers: gles_angle_vulkan_moltenvk + run-tests: true + test-targets: //host:gfxstream_framebuffer_tests + test-bazel-args: >- + --test_env=GFXSTREAM_TESTING_VULKAN_LOADER=/opt/homebrew/lib/libvulkan.1.dylib + --test_env=GFXSTREAM_TESTING_VULKAN_ICD=/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json run-gfxstream-cmake-build: runs-on: ubuntu-22.04 diff --git a/.github/workflows/presubmit_bazel.yml b/.github/workflows/presubmit_bazel.yml index a972cb79f..c80573289 100644 --- a/.github/workflows/presubmit_bazel.yml +++ b/.github/workflows/presubmit_bazel.yml @@ -25,6 +25,31 @@ on: type: boolean default: true + # Which graphics drivers/test environment to use. Defaults to the ANGLE + + # SwiftShader environment. Platforms without a buildable SwiftShader (e.g. + # macOS) can override this with `host`. + graphics-drivers: + type: string + default: 'gles_angle_vulkan_swiftshader' + + # Extra flags passed only to the `bazel test` step (e.g. --test_env for + # pointing the tests at a Vulkan driver). Not passed to `bazel build`. + test-bazel-args: + type: string + default: '' + + # Which test targets to run when run-tests is true. Platforms that can only + # run a subset (e.g. macOS) can override this with a curated list. + test-targets: + type: string + default: >- + host:gfxstream_framebuffer_tests + host/vulkan:gfxstream_compositorvk_tests + host/vulkan:gfxstream_emulatedphysicalmemory_tests + host/vulkan:vk_common_operations_tests + host/vulkan:vk_format_utils_tests + tests/end2end:gfxstream_end2end_tests + permissions: contents: read @@ -57,9 +82,12 @@ jobs: if: runner.os == 'Linux' run: sudo apt-get install -y libvulkan1 - - name: Install bazel (macOS) + - name: Install dependencies (macOS) if: runner.os == 'macOS' - run: brew install bazelisk + # molten-vk provides a Vulkan-on-Metal driver and vulkan-loader provides + # libvulkan, which together give the host-driver tests a Vulkan + # implementation to run against. + run: brew install bazelisk molten-vk vulkan-loader - name: Load cache config run: cat .config/cache-config.env >> $GITHUB_ENV @@ -82,10 +110,11 @@ jobs: env: BUILD_TARGETS: ${{ inputs.build-targets }} ADDITIONAL_BAZEL_ARGS: ${{ inputs.additional-bazel-args }} + GRAPHICS_DRIVERS: ${{ inputs.graphics-drivers }} run: | bazel build $BUILD_TARGETS \ --disk_cache=$HOME/bazel-disk-cache \ - --graphics_drivers=gles_angle_vulkan_swiftshader \ + --graphics_drivers=$GRAPHICS_DRIVERS \ --verbose_failures \ $ADDITIONAL_BAZEL_ARGS @@ -93,16 +122,20 @@ jobs: if: ${{ inputs.run-tests }} env: ADDITIONAL_BAZEL_ARGS: ${{ inputs.additional-bazel-args }} + GRAPHICS_DRIVERS: ${{ inputs.graphics-drivers }} + TEST_TARGETS: ${{ inputs.test-targets }} + TEST_BAZEL_ARGS: ${{ inputs.test-bazel-args }} run: | + # The variables below are deliberately unquoted so that the shell splits + # them into separate arguments. Disable globbing so that a value such as + # --test_arg=--gtest_filter=*Foo* is passed through literally rather than + # being expanded against the files in the workspace. + set -f bazel test \ --disk_cache=$HOME/bazel-disk-cache \ - --graphics_drivers=gles_angle_vulkan_swiftshader \ + --graphics_drivers=$GRAPHICS_DRIVERS \ --test_output=streamed \ --verbose_failures \ - host:gfxstream_framebuffer_tests \ - host/vulkan:gfxstream_compositorvk_tests \ - host/vulkan:gfxstream_emulatedphysicalmemory_tests \ - host/vulkan:vk_common_operations_tests \ - host/vulkan:vk_format_utils_tests \ - tests/end2end:gfxstream_end2end_tests \ + $TEST_TARGETS \ + $TEST_BAZEL_ARGS \ $ADDITIONAL_BAZEL_ARGS