diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..93615d0
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,78 @@
+name: Unit tests
+
+on:
+ pull_request:
+ push:
+ paths-ignore:
+ - '.gitignore'
+ - 'LICENSE'
+ - 'README.md'
+
+jobs:
+ build:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-24.04]
+ arch: [x86_64]
+
+ runs-on: ${{ matrix.os }}
+ if: "!contains(github.event.head_commit.message, 'ci skip')"
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - uses: NcStudios/VulkanCI@v1.0
+
+ - name: Install system dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get -y install mesa-common-dev clang libsdl2-2.0-0 libsdl2-dev build-essential libvulkan-dev
+
+ # Build the lib
+ - name: Build MacroLibX
+ run: make -j DEBUG=true
+
+ # Force xmake to a specific folder (for cache)
+ - name: Set xmake env
+ run: echo "XMAKE_GLOBALDIR=${{ runner.workspace }}/xmake-global" >> $GITHUB_ENV
+
+ # Install xmake
+ - name: Setup xmake
+ if: ${{ matrix.confs.plat != 'mingw' }}
+ uses: xmake-io/github-action-setup-xmake@v1
+ with:
+ xmake-version: latest
+ actions-cache-folder: .xmake-cache-W${{ steps.cache_key.outputs.key }}
+
+ # Update xmake repository (in order to have the file that will be cached)
+ - name: Update xmake repository
+ run: xmake repo --update
+
+ # Fetch xmake dephash
+ - name: Retrieve dependencies hash
+ id: dep_hash
+ run: echo "hash=$(xmake l utils.ci.packageskey)" >> $GITHUB_OUTPUT
+
+ - name: Install unit tester
+ run: git clone https://github.com/seekrs/MacroUnitTest.git ../MacroUnitTest
+
+ # Setup compilation mode and install project dependencies
+ - name: Configure xmake and install dependencies
+ run: |
+ cd ../MacroUnitTest
+ xmake config --toolchain=clang --ccache=n --yes
+
+ # Save dependencies
+ - name: Save cached xmake dependencies
+ if: ${{ !steps.restore-depcache.outputs.cache-hit }}
+ uses: actions/cache/save@v4
+ with:
+ path: ${{ env.XMAKE_GLOBALDIR }}/.xmake/packages
+ key: ${{ steps.restore-depcache.outputs.cache-primary-key }}
+
+ - name: Build and run unit tester
+ run: |
+ cd ../MacroUnitTest
+ xmake run MacroUnitTest --headless --path="${{ runner.workspace }}/MacroLibX/libmlx.so"
diff --git a/README.md b/README.md
index d2a2111..63f0097 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,9 @@
+
+

+
###### MacroLibX, a rewrite of 42 School's MiniLibX using SDL2 and Vulkan.
diff --git a/runtime/Includes/Core/UUID.h b/runtime/Includes/Core/UUID.h
index 9911302..2caa3f7 100644
--- a/runtime/Includes/Core/UUID.h
+++ b/runtime/Includes/Core/UUID.h
@@ -7,9 +7,7 @@ namespace mlx
{
public:
UUID();
- UUID(std::uint64_t uuid);
-
- inline operator std::uint64_t() const { return m_uuid; }
+ inline operator std::uint64_t() const noexcept { return m_uuid; }
private:
std::uint64_t m_uuid;
diff --git a/runtime/Sources/Core/SDLManager.cpp b/runtime/Sources/Core/SDLManager.cpp
index 5f3aa50..51aa85d 100644
--- a/runtime/Sources/Core/SDLManager.cpp
+++ b/runtime/Sources/Core/SDLManager.cpp
@@ -22,7 +22,7 @@ namespace mlx
MLX_PROFILE_FUNCTION();
s_instance = this;
- m_drop_sdl_responsability = SDL_WasInit(SDL_INIT_VIDEO);
+ m_drop_sdl_responsability = SDL_WasInit(SDL_INIT_VIDEO) || std::getenv("MLX_HEADLESS_MODE") != nullptr;
if(m_drop_sdl_responsability) // is case the mlx is running in a sandbox like MacroUnitTester where SDL is already init
return;
SDL_SetMemoryFunctions(MemManager::Get().Malloc, MemManager::Get().Calloc, MemManager::Get().Realloc, MemManager::Get().Free);
diff --git a/runtime/Sources/Core/UUID.cpp b/runtime/Sources/Core/UUID.cpp
index cf2855a..5a370dd 100644
--- a/runtime/Sources/Core/UUID.cpp
+++ b/runtime/Sources/Core/UUID.cpp
@@ -7,7 +7,14 @@ namespace mlx
static std::random_device random_device;
static std::mt19937_64 engine(random_device());
static std::uniform_int_distribution uniform_distribution;
+ static std::unordered_set registry;
- UUID::UUID() : m_uuid(uniform_distribution(engine)) {}
- UUID::UUID(std::uint64_t uuid) : m_uuid(uuid) {}
+ UUID::UUID()
+ {
+ do
+ {
+ m_uuid = uniform_distribution(engine);
+ } while(registry.contains(m_uuid));
+ registry.emplace(m_uuid);
+ }
}
diff --git a/runtime/Sources/Renderer/Buffer.cpp b/runtime/Sources/Renderer/Buffer.cpp
index c34f965..f8ef1b9 100644
--- a/runtime/Sources/Renderer/Buffer.cpp
+++ b/runtime/Sources/Renderer/Buffer.cpp
@@ -81,7 +81,7 @@ namespace mlx
VkCommandBuffer cmd = kvfCreateCommandBuffer(RenderCore::Get().GetDevice());
kvfBeginCommandBuffer(cmd, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT);
- kvfCopyBufferToBuffer(cmd, m_buffer, buffer.Get(), m_size);
+ kvfCopyBufferToBuffer(cmd, m_buffer, buffer.Get(), m_size, 0, 0);
kvfEndCommandBuffer(cmd);
VkFence fence = kvfCreateFence(RenderCore::Get().GetDevice());
kvfSubmitSingleTimeCommandBuffer(RenderCore::Get().GetDevice(), cmd, KVF_GRAPHICS_QUEUE, fence);
diff --git a/runtime/Sources/Renderer/RenderCore.cpp b/runtime/Sources/Renderer/RenderCore.cpp
index 4bddadc..1bbe5f6 100644
--- a/runtime/Sources/Renderer/RenderCore.cpp
+++ b/runtime/Sources/Renderer/RenderCore.cpp
@@ -87,15 +87,24 @@ namespace mlx
kvfSetValidationErrorCallback(&ValidationErrorCallback);
kvfSetValidationWarningCallback(&WarningCallback);
- mlx_window_create_info info{};
- info.title = "";
- info.width = 1;
- info.height = 1;
- Window window(&info, true);
- std::vector instance_extensions = window.GetRequiredVulkanInstanceExtentions();
- #ifdef MLX_PLAT_MACOS
- instance_extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
- #endif
+ std::vector instance_extensions;
+ VkSurfaceKHR surface = VK_NULL_HANDLE;
+ std::unique_ptr window;
+
+ bool is_headless = std::getenv("MLX_HEADLESS_MODE") != nullptr;
+
+ if(!is_headless)
+ {
+ mlx_window_create_info info{};
+ info.title = "";
+ info.width = 1;
+ info.height = 1;
+ window = std::make_unique(&info, true);
+ instance_extensions = window->GetRequiredVulkanInstanceExtentions();
+ #ifdef MLX_PLAT_MACOS
+ instance_extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
+ #endif
+ }
m_instance = kvfCreateInstance(instance_extensions.data(), instance_extensions.size());
DebugLog("Vulkan: instance created");
@@ -103,25 +112,34 @@ namespace mlx
loader->LoadInstance(m_instance);
LoadKVFInstanceVulkanFunctionPointers();
- VkSurfaceKHR surface = window.CreateVulkanSurface(m_instance);
+ if(!is_headless)
+ {
+ surface = window->CreateVulkanSurface(m_instance);
+ m_physical_device = kvfPickGoodDefaultPhysicalDevice(m_instance, surface);
+ }
+ else
+ m_physical_device = kvfPickGoodPhysicalDevice(m_instance, VK_NULL_HANDLE, nullptr, 0);
- m_physical_device = kvfPickGoodDefaultPhysicalDevice(m_instance, surface);
+ Verify(m_physical_device != VK_NULL_HANDLE, "Could not find a suitable physical device");
// just for style
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(m_physical_device, &props);
DebugLog("Vulkan: physical device picked '%'", props.deviceName);
- const char* device_extensions[] = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
+ std::vector device_extensions;
+ if(!is_headless)
+ device_extensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
VkPhysicalDeviceFeatures features{};
vkGetPhysicalDeviceFeatures(m_physical_device, &features);
- m_device = kvfCreateDevice(m_physical_device, device_extensions, sizeof(device_extensions) / sizeof(device_extensions[0]), &features);
+ m_device = kvfCreateDevice(m_physical_device, device_extensions.data(), device_extensions.size(), &features);
DebugLog("Vulkan: logical device created");
loader->LoadDevice(m_device);
LoadKVFDeviceVulkanFunctionPointers();
- vkDestroySurfaceKHR(m_instance, surface, nullptr);
+ if(surface != VK_NULL_HANDLE)
+ vkDestroySurfaceKHR(m_instance, surface, nullptr);
VkAllocationCallbacks callbacks;
callbacks.pUserData = nullptr;
diff --git a/runtime/Sources/Renderer/Vulkan/VulkanLoader.cpp b/runtime/Sources/Renderer/Vulkan/VulkanLoader.cpp
index 4f63d54..7fc5483 100644
--- a/runtime/Sources/Renderer/Vulkan/VulkanLoader.cpp
+++ b/runtime/Sources/Renderer/Vulkan/VulkanLoader.cpp
@@ -28,6 +28,10 @@ namespace mlx
{
static inline PFN_vkVoidFunction vkGetInstanceProcAddrStub(Handle context, const char* name)
{
+ bool is_headless = std::getenv("MLX_HEADLESS_MODE") != nullptr;
+ if(is_headless && std::string_view(name).find("KHR") != std::string_view::npos)
+ return nullptr;
+
PFN_vkVoidFunction function = RenderCore::Get().vkGetInstanceProcAddr(static_cast(context), name);
if(!function)
FatalError("Vulkan Loader: could not load '%'", name);
@@ -37,6 +41,10 @@ namespace mlx
static inline PFN_vkVoidFunction vkGetDeviceProcAddrStub(Handle context, const char* name)
{
+ bool is_headless = std::getenv("MLX_HEADLESS_MODE") != nullptr;
+ if(is_headless && std::string_view(name).find("KHR") != std::string_view::npos)
+ return nullptr;
+
PFN_vkVoidFunction function = RenderCore::Get().vkGetDeviceProcAddr(static_cast(context), name);
if(!function)
FatalError("Vulkan Loader: could not load '%'", name);
diff --git a/third_party/kvf.h b/third_party/kvf.h
index 1308495..bda9a57 100755
--- a/third_party/kvf.h
+++ b/third_party/kvf.h
@@ -1475,9 +1475,6 @@ int32_t __kvfScorePhysicalDevice(VkPhysicalDevice device, VkSurfaceKHR surface,
if(device_props.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU)
score += 1000;
- if(!device_features.geometryShader)
- return -1;
-
score += device_props.limits.maxImageDimension2D;
score += device_props.limits.maxBoundDescriptorSets;
diff --git a/xmake.lua b/xmake.lua
index 074e4ce..f7445a4 100644
--- a/xmake.lua
+++ b/xmake.lua
@@ -1,6 +1,6 @@
-- Global settings
-add_requires("libsdl", { configs = { sdlmain = false } })
+add_requires("libsdl2", { configs = { sdlmain = false } })
add_rules("mode.debug", "mode.release", "mode.releasedbg")
set_languages("cxx20", "c11")
@@ -63,7 +63,7 @@ target("mlx")
add_files("runtime/Sources/**.cpp")
- add_packages("libsdl")
+ add_packages("libsdl2")
if is_mode("debug") then
add_defines("DEBUG")
@@ -98,5 +98,5 @@ target("Test")
add_defines("SDL_MAIN_HANDLED")
- add_packages("libsdl")
+ add_packages("libsdl2")
target_end()