From 356228d4d48bb597399ed1f0f0cec71237040517 Mon Sep 17 00:00:00 2001 From: Phantom Date: Fri, 15 May 2026 00:00:05 +0200 Subject: [PATCH 01/27] Initial Keyboard and Gamepad support on Slider Control --- Source/Engine/UI/GUI/Common/Slider.cs | 50 +++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/Source/Engine/UI/GUI/Common/Slider.cs b/Source/Engine/UI/GUI/Common/Slider.cs index 8c7b022fe..f1f33732e 100644 --- a/Source/Engine/UI/GUI/Common/Slider.cs +++ b/Source/Engine/UI/GUI/Common/Slider.cs @@ -1,6 +1,7 @@ // Copyright (c) Wojciech Figat. All rights reserved. using System; +using System.Collections.Generic; namespace FlaxEngine.GUI; @@ -408,6 +409,40 @@ public class Slider : ContainerControl base.OnLostFocus(); } + /// + public override Control OnNavigate(NavDirection direction, Float2 location, Control caller, List visited) + { + bool _isHorizontal = Direction is SliderDirection.HorizontalRight or SliderDirection.HorizontalLeft; + + float keyOrGamepadPosition = _isHorizontal ? location.X : location.Y; + + if (_thumbRect.Contains(ref location)) + { + // Start sliding + _isSliding = true; + SlidingStart?.Invoke(); + return this; + } + else + { + Value += (keyOrGamepadPosition < _thumbCenter ? -1 : 1) * 10; + } + + return base.OnNavigate(direction, location, caller, visited); + } + + /// + public override bool OnKeyDown(KeyboardKeys key) + { + if (key == KeyboardKeys.Escape) + { + Defocus(); + return true; + } + + return base.OnKeyDown(key); + } + /// public override bool OnMouseDown(Float2 location, MouseButton button) { @@ -443,6 +478,21 @@ public class Slider : ContainerControl return base.OnMouseDown(location, button); } + /// + public override bool OnTouchDown(Float2 location, int pointerId) + { + if (base.OnTouchDown(location, pointerId)) + return true; + + if (!new Rectangle(Float2.Zero, Size).Contains(ref location)) + { + Defocus(); + return true; + } + + return false; + } + /// public override void OnMouseMove(Float2 location) { From a1e03db3997cea035502f05389f7cc7bebab9a16 Mon Sep 17 00:00:00 2001 From: Phantom Date: Fri, 15 May 2026 00:24:05 +0200 Subject: [PATCH 02/27] -u Slider --- Source/Engine/UI/GUI/Common/Slider.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Source/Engine/UI/GUI/Common/Slider.cs b/Source/Engine/UI/GUI/Common/Slider.cs index f1f33732e..61c722dfe 100644 --- a/Source/Engine/UI/GUI/Common/Slider.cs +++ b/Source/Engine/UI/GUI/Common/Slider.cs @@ -524,6 +524,18 @@ public class Slider : ContainerControl } } + /// + public override void OnKeyUp(KeyboardKeys key) + { + if (key == KeyboardKeys.Escape && _isSliding) + { + EndSliding(); + return; + } + + base.OnKeyUp(key); + } + /// public override bool OnMouseUp(Float2 location, MouseButton button) { @@ -536,6 +548,18 @@ public class Slider : ContainerControl return base.OnMouseUp(location, button); } + /// + public override bool OnTouchUp(Float2 location, int pointerId) + { + if (base.OnTouchUp(location, pointerId) && _isSliding) + { + EndSliding(); + return true; + } + + return false; + } + /// public override void OnEndMouseCapture() { From 4c9f121e1ee147092cdc54d89a2e621f3a98eb4c Mon Sep 17 00:00:00 2001 From: Phantom Date: Fri, 15 May 2026 00:53:53 +0200 Subject: [PATCH 03/27] -u OnNavigate on Slider Control --- Source/Engine/UI/GUI/Common/Slider.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Source/Engine/UI/GUI/Common/Slider.cs b/Source/Engine/UI/GUI/Common/Slider.cs index 61c722dfe..9d8502c9c 100644 --- a/Source/Engine/UI/GUI/Common/Slider.cs +++ b/Source/Engine/UI/GUI/Common/Slider.cs @@ -413,20 +413,14 @@ public class Slider : ContainerControl public override Control OnNavigate(NavDirection direction, Float2 location, Control caller, List visited) { bool _isHorizontal = Direction is SliderDirection.HorizontalRight or SliderDirection.HorizontalLeft; + bool _isRevelant = _isHorizontal ? (direction is NavDirection.Left or NavDirection.Right) : (direction is NavDirection.Up or NavDirection.Down); - float keyOrGamepadPosition = _isHorizontal ? location.X : location.Y; - - if (_thumbRect.Contains(ref location)) + if (_isRevelant) { - // Start sliding - _isSliding = true; - SlidingStart?.Invoke(); + float _keyOrGamepadPosition = ((direction is NavDirection.Right or NavDirection.Down) != (Direction is SliderDirection.HorizontalLeft or SliderDirection.VerticalUp)) ? location.X : location.Y; + Value += (_keyOrGamepadPosition < _thumbCenter ? 1f : -1f) * 10f; return this; } - else - { - Value += (keyOrGamepadPosition < _thumbCenter ? -1 : 1) * 10; - } return base.OnNavigate(direction, location, caller, visited); } From 3de20d4a5c38202ac2a7bbde26cfd15b8c67bfdb Mon Sep 17 00:00:00 2001 From: Phantom Date: Fri, 15 May 2026 12:44:16 +0200 Subject: [PATCH 04/27] -u Slider --- Source/Engine/UI/GUI/Common/Slider.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Source/Engine/UI/GUI/Common/Slider.cs b/Source/Engine/UI/GUI/Common/Slider.cs index 9d8502c9c..3acbfec8a 100644 --- a/Source/Engine/UI/GUI/Common/Slider.cs +++ b/Source/Engine/UI/GUI/Common/Slider.cs @@ -413,15 +413,27 @@ public class Slider : ContainerControl public override Control OnNavigate(NavDirection direction, Float2 location, Control caller, List visited) { bool _isHorizontal = Direction is SliderDirection.HorizontalRight or SliderDirection.HorizontalLeft; - bool _isRevelant = _isHorizontal ? (direction is NavDirection.Left or NavDirection.Right) : (direction is NavDirection.Up or NavDirection.Down); + + float _keyOrGamepadPosition = _isHorizontal ? location.X : location.Y; - if (_isRevelant) + if (_thumbRect.Contains(ref location)) { - float _keyOrGamepadPosition = ((direction is NavDirection.Right or NavDirection.Down) != (Direction is SliderDirection.HorizontalLeft or SliderDirection.VerticalUp)) ? location.X : location.Y; - Value += (_keyOrGamepadPosition < _thumbCenter ? 1f : -1f) * 10f; + _isSliding = true; + SlidingStart?.Invoke(); return this; } + switch (Direction) + { + case SliderDirection.HorizontalRight or SliderDirection.VerticalDown: + Value += (_keyOrGamepadPosition < _thumbCenter ? -1 : 1) * 10; + break; + case SliderDirection.HorizontalLeft or SliderDirection.VerticalUp: + Value -= (_keyOrGamepadPosition < _thumbCenter ? -1 : 1) * 10; + break; + default: break; + } + return base.OnNavigate(direction, location, caller, visited); } From 0bb57793bb213d1e7787437f8b36f796db44c0bb Mon Sep 17 00:00:00 2001 From: Phantom Date: Sun, 17 May 2026 12:21:36 +0200 Subject: [PATCH 05/27] -u --- Source/Engine/UI/GUI/Common/Slider.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Source/Engine/UI/GUI/Common/Slider.cs b/Source/Engine/UI/GUI/Common/Slider.cs index 3acbfec8a..83a5c38e1 100644 --- a/Source/Engine/UI/GUI/Common/Slider.cs +++ b/Source/Engine/UI/GUI/Common/Slider.cs @@ -423,16 +423,8 @@ public class Slider : ContainerControl return this; } - switch (Direction) - { - case SliderDirection.HorizontalRight or SliderDirection.VerticalDown: - Value += (_keyOrGamepadPosition < _thumbCenter ? -1 : 1) * 10; - break; - case SliderDirection.HorizontalLeft or SliderDirection.VerticalUp: - Value -= (_keyOrGamepadPosition < _thumbCenter ? -1 : 1) * 10; - break; - default: break; - } + var SliderPosition = (Direction == SliderDirection.HorizontalRight || Direction == SliderDirection.VerticalDown) ? _keyOrGamepadPosition : - _keyOrGamepadPosition; + Value += (SliderPosition < _thumbCenter ? -1 : 1) * 10; return base.OnNavigate(direction, location, caller, visited); } From a5bebce5293ad42364b52b8d069ed66c31654f8c Mon Sep 17 00:00:00 2001 From: Andrei Gagua Date: Fri, 29 May 2026 23:54:52 +0300 Subject: [PATCH 06/27] Fix: Vulkan - preserve valid swapchain surfaces during resize Avoid recreating VkSurfaceKHR during normal Vulkan swapchain resize/out-of-date handling. Recreate the surface only when Vulkan reports surface loss or when the native window handle used to create the surface changes. --- .../Vulkan/GPUSwapChainVulkan.cpp | 84 ++++++++++++------- .../Vulkan/GPUSwapChainVulkan.h | 7 +- 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp index 0e9ec0ec8..7993dcb2b 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp @@ -41,6 +41,7 @@ void BackBufferVulkan::Release() GPUSwapChainVulkan::GPUSwapChainVulkan(GPUDeviceVulkan* device, Window* window) : GPUResourceVulkan(device, StringView::Empty) , _surface(VK_NULL_HANDLE) + , _surfaceWindowHandle(nullptr) , _swapChain(VK_NULL_HANDLE) , _currentImageIndex(-1) , _semaphoreIndex(0) @@ -59,16 +60,17 @@ void GPUSwapChainVulkan::ReleaseBackBuffer() _backBuffers.Clear(); } -void GPUSwapChainVulkan::OnReleaseGPU() +void GPUSwapChainVulkan::ReleaseSwapChain(bool releaseSurface) { - GPUDeviceLock lock(_device); - - _device->WaitForGPU(); + // The caller must ensure GPU work using the current swapchain is complete before destroying it. + if (_memoryUsage != 0) + { + PROFILE_MEM_DEC(Graphics, _memoryUsage); + _memoryUsage = 0; + } ReleaseBackBuffer(); - // Release data - PROFILE_MEM_DEC(Graphics, _memoryUsage); _currentImageIndex = -1; _semaphoreIndex = 0; _acquiredImageIndex = -1; @@ -78,13 +80,23 @@ void GPUSwapChainVulkan::OnReleaseGPU() vkDestroySwapchainKHR(_device->Device, _swapChain, nullptr); _swapChain = VK_NULL_HANDLE; } - if (_surface != VK_NULL_HANDLE) + // Resize only invalidates the swapchain. Destroy the native surface only if it is no longer valid + // or when the whole GPU resource is being released. + if (releaseSurface && _surface != VK_NULL_HANDLE) { vkDestroySurfaceKHR(GPUDeviceVulkan::Instance, _surface, nullptr); _surface = VK_NULL_HANDLE; + _surfaceWindowHandle = nullptr; } _width = _height = 0; - _memoryUsage = 0; +} + +void GPUSwapChainVulkan::OnReleaseGPU() +{ + GPUDeviceLock lock(_device); + + _device->WaitForGPU(); + ReleaseSwapChain(true); } bool GPUSwapChainVulkan::IsFullscreen() @@ -114,24 +126,28 @@ GPUTextureView* GPUSwapChainVulkan::GetBackBufferView() if (_acquiredImageIndex == -1) { PROFILE_CPU(); + auto context = _device->MainContext; + auto cmdBufferManager = context->GetCmdBufferManager(); + // Keep commands recorded before acquire independent from the image-acquired semaphore wait below. + if (cmdBufferManager->HasPendingActiveCmdBuffer()) + context->Flush(); + if (TryPresent(DoAcquireImageIndex) < 0) { LOG(Fatal, "Swapchain acquire image index failed!"); } ASSERT(_acquiredImageIndex != -1); - auto context = _device->MainContext; const auto backBuffer = &_backBuffers[_acquiredImageIndex].Handle; - auto cmdBufferManager = context->GetCmdBufferManager(); auto cmdBuffer = cmdBufferManager->GetCmdBuffer(); // Transition to render target (typical usage in most cases when calling backbuffer getter) context->AddImageBarrier(backBuffer, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); context->FlushBarriers(); - // Submit here so we can add a dependency with the acquired semaphore - cmdBuffer->AddWaitSemaphore(VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, _acquiredSemaphore); + // Wait until the presentation engine releases the image before the first color-output work can use it. + cmdBuffer->AddWaitSemaphore(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, _acquiredSemaphore); cmdBufferManager->SubmitActiveCmdBuffer(); cmdBufferManager->GetNewActiveCommandBuffer(); ASSERT(cmdBufferManager->HasPendingActiveCmdBuffer() && cmdBufferManager->GetActiveCmdBuffer()->GetState() == CmdBufferVulkan::State::IsInsideBegin); @@ -161,8 +177,9 @@ bool GPUSwapChainVulkan::Resize(int32 width, int32 height) if (width == _width && height == _height) return false; - // Wait for GPU to flush commands - _device->WaitForGPU(); + // Flush any pending commands referencing the previous backbuffer before waiting on the device. + if (_swapChain != VK_NULL_HANDLE) + _device->GetMainContext()->Flush(); return CreateSwapChain(width, height); } @@ -194,7 +211,7 @@ void GPUSwapChainVulkan::CopyBackbuffer(GPUContext* context, GPUTexture* dst) vkCmdCopyImage(contextVulkan->GetCmdBufferManager()->GetCmdBuffer()->GetHandle(), backBuffer->Image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstVulkan->GetHandle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); } -bool GPUSwapChainVulkan::CreateSwapChain(int32 width, int32 height) +bool GPUSwapChainVulkan::CreateSwapChain(int32 width, int32 height, bool recreateSurface) { // Skip if window handle is missing (eg. Android window is not yet visible) auto windowHandle = _window->GetNativePtr(); @@ -203,25 +220,35 @@ bool GPUSwapChainVulkan::CreateSwapChain(int32 width, int32 height) PROFILE_CPU(); GPUDeviceLock lock(_device); const auto device = _device->Device; - - // Check if surface has been created before - if (_surface != VK_NULL_HANDLE) + const bool hasSwapChain = _swapChain != VK_NULL_HANDLE; + if (_surface != VK_NULL_HANDLE && _surfaceWindowHandle != windowHandle) { - // Release previous data - ReleaseGPU(); + // Android can replace ANativeWindow during lifecycle changes; iOS can replace the UIView/CAMetalLayer. + // A VkSurfaceKHR must not outlive the native object it was created from. + recreateSurface = true; + } - // Flush removed resources + if (recreateSurface || hasSwapChain) + { + // Retire old swapchain resources before creating replacement images. On plain resize the VkSurfaceKHR + // is intentionally reused; recreate it only when Vulkan or the platform window lifetime requires it. + _device->WaitForGPU(); + ReleaseSwapChain(recreateSurface); _device->DeferredDeletionQueue.ReleaseResources(true); } - ASSERT(_surface == VK_NULL_HANDLE); + ASSERT(!recreateSurface || _surface == VK_NULL_HANDLE); ASSERT_LOW_LAYER(_backBuffers.Count() == 0); - // Create platform-dependent surface - VulkanPlatform::CreateSurface(_window, _device, GPUDeviceVulkan::Instance, &_surface); + // Create platform-dependent surface if this is the first swapchain or if the previous surface was lost. if (_surface == VK_NULL_HANDLE) { - LOG(Warning, "Failed to create Vulkan surface."); - return true; + VulkanPlatform::CreateSurface(_window, _device, GPUDeviceVulkan::Instance, &_surface); + if (_surface == VK_NULL_HANDLE) + { + LOG(Warning, "Failed to create Vulkan surface."); + return true; + } + _surfaceWindowHandle = windowHandle; } _memoryUsage = 1; @@ -517,8 +544,8 @@ int32 GPUSwapChainVulkan::TryPresent(Function // Recreate swapchain ASSERT(_swapChain != VK_NULL_HANDLE); int32 width = _width, height = _height; - ReleaseGPU(); - CreateSwapChain(width, height); + // Preserve the surface for regular out-of-date swaps; recreate it only when Vulkan reports loss. + CreateSwapChain(width, height, status == (int32)Status::LostSurface); // Flush commands _device->GetMainContext()->Flush(); @@ -602,7 +629,6 @@ void GPUSwapChainVulkan::Present(bool vsync) // Rebuild swapchain for the next present int32 width = _width, height = _height; - ReleaseGPU(); CreateSwapChain(width, height); _device->GetMainContext()->Flush(); _device->WaitForGPU(); diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.h b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.h index 638c16bea..8cadea250 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.h +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.h @@ -61,6 +61,7 @@ class GPUSwapChainVulkan : public GPUResourceVulkan, public Resour private: VkSurfaceKHR _surface; + void* _surfaceWindowHandle; VkSwapchainKHR _swapChain; int32 _currentImageIndex; int32 _semaphoreIndex; @@ -107,7 +108,11 @@ public: private: void ReleaseBackBuffer(); - bool CreateSwapChain(int32 width, int32 height); + // Releases swapchain-owned images and synchronization state. Keeps the VkSurfaceKHR alive during + // normal resize so the native surface is rebuilt only after surface loss or native window replacement. + void ReleaseSwapChain(bool releaseSurface); + // Recreates the swapchain for the requested size. Set recreateSurface when the VkSurfaceKHR is no longer valid. + bool CreateSwapChain(int32 width, int32 height, bool recreateSurface = false); public: // [GPUSwapChain] From 04f22742721986f8cfab4d72493bc7bdf2abb7b1 Mon Sep 17 00:00:00 2001 From: Andrei Gagua Date: Sat, 30 May 2026 18:18:43 +0300 Subject: [PATCH 07/27] New: Add Android build support on macOS Enable Android SDK/NDK discovery on macOS, use the darwin-x86_64 NDK toolchain, and support the modern NDK include layout used by NDK r27. --- .../Platform/Android/AndroidPlatformTools.cpp | 19 ++++++++++--------- Source/Engine/Core/Config/GameSettings.cs | 11 ++++++----- .../Platform/Android/AndroidPlatform.cpp | 2 +- Source/Engine/Platform/Mac/MacPlatform.cpp | 7 +++++++ .../Platforms/Android/AndroidNdk.cs | 1 + .../Platforms/Android/AndroidSdk.cs | 4 ++++ .../Platforms/Android/AndroidToolchain.cs | 15 +++++++++++++-- 7 files changed, 42 insertions(+), 17 deletions(-) diff --git a/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp b/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp index 1b6e7a616..be46f1c26 100644 --- a/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Android/AndroidPlatformTools.cpp @@ -330,18 +330,16 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data) GameCooker::PackageFiles(); // Validate environment variables - Dictionary envVars; - Platform::GetEnvironmentVariables(envVars); String javaHome; - if (!envVars.TryGet(TEXT("JAVA_HOME"), javaHome) || !FileSystem::DirectoryExists(javaHome)) + if (Platform::GetEnvironmentVariable(TEXT("JAVA_HOME"), javaHome) || !FileSystem::DirectoryExists(javaHome)) { LOG(Error, "Missing or invalid JAVA_HOME env variable. {0}", javaHome); return true; } String androidSdk; - if (!envVars.TryGet(TEXT("ANDROID_HOME"), androidSdk) || !FileSystem::DirectoryExists(androidSdk)) + if (Platform::GetEnvironmentVariable(TEXT("ANDROID_HOME"), androidSdk) || !FileSystem::DirectoryExists(androidSdk)) { - if (!envVars.TryGet(TEXT("ANDROID_SDK"), androidSdk) || !FileSystem::DirectoryExists(androidSdk)) + if (Platform::GetEnvironmentVariable(TEXT("ANDROID_SDK"), androidSdk) || !FileSystem::DirectoryExists(androidSdk)) { LOG(Error, "Missing or invalid ANDROID_HOME env variable. {0}", androidSdk); return true; @@ -355,10 +353,11 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data) #else const Char* gradlew = TEXT("gradlew"); #endif -#if PLATFORM_LINUX +#if PLATFORM_LINUX || PLATFORM_MAC { CreateProcessSettings procSettings; - procSettings.FileName = String::Format(TEXT("chmod +x \"{0}/gradlew\""), data.OriginalOutputPath); + procSettings.FileName = TEXT("/bin/chmod"); + procSettings.Arguments = String::Format(TEXT("+x \"{0}\""), data.OriginalOutputPath / gradlew); procSettings.WorkingDirectory = data.OriginalOutputPath; procSettings.HiddenWindow = true; Platform::CreateProcess(procSettings); @@ -371,7 +370,8 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data) // .aab { CreateProcessSettings procSettings; - procSettings.FileName = String::Format(TEXT("\"{0}\" {1}"), data.OriginalOutputPath / gradlew, distributionPackage ? TEXT(":app:bundle") : TEXT(":app:bundleDebug")); + procSettings.FileName = data.OriginalOutputPath / gradlew; + procSettings.Arguments = distributionPackage ? TEXT("--console=plain :app:bundle") : TEXT("--console=plain :app:bundleDebug"); procSettings.WorkingDirectory = data.OriginalOutputPath; const int32 result = Platform::CreateProcess(procSettings); if (result != 0) @@ -394,7 +394,8 @@ bool AndroidPlatformTools::OnPostProcess(CookingData& data) // .apk { CreateProcessSettings procSettings; - procSettings.FileName = String::Format(TEXT("\"{0}\" {1}"), data.OriginalOutputPath / gradlew, distributionPackage ? TEXT("assemble") : TEXT("assembleDebug")); + procSettings.FileName = data.OriginalOutputPath / gradlew; + procSettings.Arguments = distributionPackage ? TEXT("--console=plain assemble") : TEXT("--console=plain assembleDebug"); procSettings.WorkingDirectory = data.OriginalOutputPath; const int32 result = Platform::CreateProcess(procSettings); if (result != 0) diff --git a/Source/Engine/Core/Config/GameSettings.cs b/Source/Engine/Core/Config/GameSettings.cs index 4f66261be..5fa96f98c 100644 --- a/Source/Engine/Core/Config/GameSettings.cs +++ b/Source/Engine/Core/Config/GameSettings.cs @@ -14,6 +14,7 @@ namespace FlaxEditor.Content.Settings internal const string PS5PlatformSettingsTypename = "FlaxEditor.Content.Settings.PS5PlatformSettings"; internal const string XboxOnePlatformSettingsTypename = "FlaxEditor.Content.Settings.XboxOnePlatformSettings"; internal const string XboxScarlettPlatformSettingsTypename = "FlaxEditor.Content.Settings.XboxScarlettPlatformSettings"; + internal const string AndroidPlatformSettingsTypename = "FlaxEditor.Content.Settings.AndroidPlatformSettings"; internal const string SwitchPlatformSettingsTypename = "FlaxEditor.Content.Settings.SwitchPlatformSettings"; #if FLAX_EDITOR internal static string[] OptionalPlatformSettings = @@ -172,9 +173,9 @@ namespace FlaxEditor.Content.Settings #if FLAX_EDITOR || PLATFORM_ANDROID /// - /// Reference to asset. Used to apply configuration on Android platform. + /// Reference to Android Platform Settings asset. Used to apply configuration on Android platform. /// - [EditorOrder(2060), EditorDisplay("Platform Settings", "Android"), AssetReference(typeof(AndroidPlatformSettings), true), Tooltip("Reference to Android Platform Settings asset")] + [EditorOrder(2060), EditorDisplay("Platform Settings", "Android"), AssetReference(AndroidPlatformSettingsTypename, true), Tooltip("Reference to Android Platform Settings asset")] public JsonAsset AndroidPlatform; #endif @@ -334,8 +335,8 @@ namespace FlaxEditor.Content.Settings return Load(gameSettings.XboxScarlettPlatform, XboxScarlettPlatformSettingsTypename) as T; #endif #if FLAX_EDITOR || PLATFORM_ANDROID - if (type == typeof(AndroidPlatformSettings)) - return Load(gameSettings.AndroidPlatform) as T; + if (type.FullName == AndroidPlatformSettingsTypename) + return Load(gameSettings.AndroidPlatform, AndroidPlatformSettingsTypename) as T; #endif #if FLAX_EDITOR || PLATFORM_SWITCH if (type.FullName == SwitchPlatformSettingsTypename) @@ -436,7 +437,7 @@ namespace FlaxEditor.Content.Settings return gameSettings.XboxScarlettPlatform; #endif #if FLAX_EDITOR || PLATFORM_ANDROID - if (type == typeof(AndroidPlatformSettings)) + if (type.FullName == AndroidPlatformSettingsTypename) return gameSettings.AndroidPlatform; #endif #if FLAX_EDITOR || PLATFORM_SWITCH diff --git a/Source/Engine/Platform/Android/AndroidPlatform.cpp b/Source/Engine/Platform/Android/AndroidPlatform.cpp index 0e2f9e686..08aafe358 100644 --- a/Source/Engine/Platform/Android/AndroidPlatform.cpp +++ b/Source/Engine/Platform/Android/AndroidPlatform.cpp @@ -963,7 +963,7 @@ void AndroidPlatform::Tick() // Pool app events int events; android_poll_source* source; - while (ALooper_pollAll(0, nullptr, &events, reinterpret_cast(&source)) >= 0) + while (ALooper_pollOnce(0, nullptr, &events, reinterpret_cast(&source)) >= 0) { // Process event if (source != nullptr) diff --git a/Source/Engine/Platform/Mac/MacPlatform.cpp b/Source/Engine/Platform/Mac/MacPlatform.cpp index 4e268d3f9..2f8d64aed 100644 --- a/Source/Engine/Platform/Mac/MacPlatform.cpp +++ b/Source/Engine/Platform/Mac/MacPlatform.cpp @@ -471,6 +471,13 @@ int32 MacPlatform::CreateProcess(CreateProcessSettings& settings) task.arguments = AppleUtils::ParseArguments(AppleUtils::ToNSString(settings.Arguments)); if (settings.WorkingDirectory.HasChars()) task.currentDirectoryPath = AppleUtils::ToNSString(settings.WorkingDirectory); + [task setStandardInput:[NSFileHandle fileHandleWithNullDevice]]; + if (!captureStdOut) + { + NSFileHandle* nullDevice = [NSFileHandle fileHandleWithNullDevice]; + [task setStandardOutput:nullDevice]; + [task setStandardError:nullDevice]; + } int32 returnCode = 0; if (settings.WaitForEnd) diff --git a/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs b/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs index 00838a73d..a67e6dab7 100644 --- a/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs +++ b/Source/Tools/Flax.Build/Platforms/Android/AndroidNdk.cs @@ -22,6 +22,7 @@ namespace Flax.Build.Platforms { TargetPlatform.Windows, TargetPlatform.Linux, + TargetPlatform.Mac, }; /// diff --git a/Source/Tools/Flax.Build/Platforms/Android/AndroidSdk.cs b/Source/Tools/Flax.Build/Platforms/Android/AndroidSdk.cs index d3b169117..35eb4de94 100644 --- a/Source/Tools/Flax.Build/Platforms/Android/AndroidSdk.cs +++ b/Source/Tools/Flax.Build/Platforms/Android/AndroidSdk.cs @@ -22,6 +22,7 @@ namespace Flax.Build.Platforms { TargetPlatform.Windows, TargetPlatform.Linux, + TargetPlatform.Mac, }; /// @@ -106,6 +107,9 @@ namespace Flax.Build.Platforms case TargetPlatform.Linux: hostName = "linux-x86_64"; break; + case TargetPlatform.Mac: + hostName = "darwin-x86_64"; + break; default: throw new InvalidPlatformException(Platform.BuildPlatform.Target); } return hostName; diff --git a/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs b/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs index 34b77ae5a..3ce690df4 100644 --- a/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Android/AndroidToolchain.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Flax.Build.Graph; using Flax.Build.NativeCpp; @@ -37,10 +38,20 @@ namespace Flax.Build.Platforms : base(platform, architecture, toolchainRoot, null, string.Empty) { var toolchain = ToolsetRoot.Replace('\\', '/'); - SystemIncludePaths.Add(Path.Combine(toolchain, "sources/usr/include/c++/v1").Replace('\\', '/')); + var cxxIncludePath = Path.Combine(toolchain, "sysroot/usr/include/c++/v1"); + if (!Directory.Exists(cxxIncludePath)) + cxxIncludePath = Path.Combine(toolchain, "sources/usr/include/c++/v1"); + SystemIncludePaths.Add(cxxIncludePath.Replace('\\', '/')); SystemIncludePaths.Add(Path.Combine(toolchain, "sysroot/usr/include").Replace('\\', '/')); SystemIncludePaths.Add(Path.Combine(toolchain, "sysroot/usr/local/include").Replace('\\', '/')); - SystemIncludePaths.Add(Path.Combine(toolchain, "lib64/clang/9.0.8/include").Replace('\\', '/')); + var clangIncludeRoot = Path.Combine(toolchain, "lib/clang"); + if (!Directory.Exists(clangIncludeRoot)) + clangIncludeRoot = Path.Combine(toolchain, "lib64/clang"); + var clangIncludePath = Directory.Exists(clangIncludeRoot) + ? Directory.GetDirectories(clangIncludeRoot).OrderBy(x => x).LastOrDefault() + : null; + if (clangIncludePath != null) + SystemIncludePaths.Add(Path.Combine(clangIncludePath, "include").Replace('\\', '/')); } /// From fcae4846f114579237c128f395312fed2c5e4650 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 3 Jun 2026 16:47:49 +0200 Subject: [PATCH 08/27] remove project path from window title --- Source/Editor/Modules/WindowsModule.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index 7e01dff67..1a03c7c57 100644 --- a/Source/Editor/Modules/WindowsModule.cs +++ b/Source/Editor/Modules/WindowsModule.cs @@ -191,13 +191,9 @@ namespace FlaxEditor.Modules var mainWindow = MainWindow; if (mainWindow) { - var projectPath = Globals.ProjectFolder; -#if PLATFORM_WINDOWS - projectPath = projectPath.Replace('/', '\\'); -#endif var engineVersion = Editor.EngineProject.Version; var engineVersionText = engineVersion.Revision > 0 ? $"{engineVersion.Major}.{engineVersion.Minor}.{engineVersion.Revision}" : $"{engineVersion.Major}.{engineVersion.Minor}"; - var title = $"Flax Editor {engineVersionText} - \'{projectPath}\'"; + var title = $"Flax Editor {engineVersionText} - \'{Editor.GameProject.Name}\'"; mainWindow.Title = title; } } From 04fc93811965f2268329f077a5d60db77eddffe2 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 3 Jun 2026 16:48:07 +0200 Subject: [PATCH 09/27] add project path and large world to window icon tooltip --- Source/Editor/Modules/UIModule.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 1a9abe23c..2c658580d 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -824,10 +824,20 @@ namespace FlaxEditor.Modules driver = $" ({driver})"; #endif + var projectPath = Globals.ProjectFolder; +#if PLATFORM_WINDOWS + projectPath = projectPath.Replace('/', '\\'); +#endif + + string largeWorld = "Large Worlds Enabled: false"; +#if USE_LARGE_WORLDS + largeWorld = "Large Worlds Enabled: true"; +#endif + WindowDecorations = new MainWindowDecorations(mainWindow, !Utilities.Utils.UseCustomWindowDecorations(true)) { Parent = mainWindow, - IconTooltipText = $"{mainWindow.RootWindow.Title}\nVersion {Globals.EngineVersion}\nConfiguration {configuration}\nGraphics {GPUDevice.Instance.RendererType}{driver}", + IconTooltipText = $"{mainWindow.RootWindow.Title}\nPath {projectPath}\n\nEngine Version {Globals.EngineVersion}\n{largeWorld}\nConfiguration {configuration}\n\nGraphics {GPUDevice.Instance.RendererType}{driver}", }; } From 0b160c2e61e241debbacdb6b2672ac3af8ffcfeb Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 3 Jun 2026 16:54:03 +0200 Subject: [PATCH 10/27] remove : --- Source/Editor/Modules/UIModule.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 2c658580d..f4802f59f 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -829,9 +829,9 @@ namespace FlaxEditor.Modules projectPath = projectPath.Replace('/', '\\'); #endif - string largeWorld = "Large Worlds Enabled: false"; + string largeWorld = "Large Worlds Disabled"; #if USE_LARGE_WORLDS - largeWorld = "Large Worlds Enabled: true"; + largeWorld = "Large Worlds Enabled"; #endif WindowDecorations = new MainWindowDecorations(mainWindow, !Utilities.Utils.UseCustomWindowDecorations(true)) From d103ccdbc3296543a1521294e6e356e324906af7 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 6 Jun 2026 18:58:07 +0200 Subject: [PATCH 11/27] show large world only if needed --- Source/Editor/Modules/UIModule.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index f4802f59f..62f514040 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -829,15 +829,15 @@ namespace FlaxEditor.Modules projectPath = projectPath.Replace('/', '\\'); #endif - string largeWorld = "Large Worlds Disabled"; + string largeWorld = ""; #if USE_LARGE_WORLDS - largeWorld = "Large Worlds Enabled"; + largeWorld = "\nLarge Worlds Enabled"; #endif WindowDecorations = new MainWindowDecorations(mainWindow, !Utilities.Utils.UseCustomWindowDecorations(true)) { Parent = mainWindow, - IconTooltipText = $"{mainWindow.RootWindow.Title}\nPath {projectPath}\n\nEngine Version {Globals.EngineVersion}\n{largeWorld}\nConfiguration {configuration}\n\nGraphics {GPUDevice.Instance.RendererType}{driver}", + IconTooltipText = $"{mainWindow.RootWindow.Title}\nPath {projectPath}\n\nEngine Version {Globals.EngineVersion}{largeWorld}\nConfiguration {configuration}\n\nGraphics {GPUDevice.Instance.RendererType}{driver}", }; } From 0c354b85f3b898eb051f19904250fdd9fd75177b Mon Sep 17 00:00:00 2001 From: Andrei Gagua Date: Sat, 6 Jun 2026 22:08:43 +0300 Subject: [PATCH 12/27] Fix Vulkan swapchain wait for acquired backbuffer Move backbuffer submit waiting until after swapchain image acquire, and track the submitted fence counter per backbuffer to avoid waiting on stale command buffer objects. This preserves multi-buffered presentation and fixes iOS/MoltenVK frame pacing drops caused by waiting on the wrong swapchain image. --- .../Vulkan/GPUSwapChainVulkan.cpp | 38 ++++++++++--------- .../Vulkan/GPUSwapChainVulkan.h | 6 +++ Source/Engine/Platform/iOS/iOSPlatform.cpp | 8 +++- .../FlaxGame.xcodeproj/project.pbxproj | 2 + .../iOS/Binaries/Project/FlaxGame/Info.plist | 2 + 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp index 0e9ec0ec8..152d37f12 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp @@ -25,16 +25,23 @@ void BackBufferVulkan::Setup(GPUSwapChainVulkan* window, VkImage backbuffer, Pix ImageAcquiredSemaphore = New(Device); } +void BackBufferVulkan::WaitForSubmit() +{ + if (SubmitCmdBuffer) + { + if (SubmitCmdBufferFenceCounter == SubmitCmdBuffer->GetFenceSignaledCounter()) + SubmitCmdBuffer->Wait(); + SubmitCmdBuffer = nullptr; + SubmitCmdBufferFenceCounter = 0; + } +} + void BackBufferVulkan::Release() { + WaitForSubmit(); Handle.Release(); Delete(RenderingDoneSemaphore); Delete(ImageAcquiredSemaphore); - if (SubmitCmdBuffer) - { - SubmitCmdBuffer->Wait(); - SubmitCmdBuffer = nullptr; - } Device = nullptr; } @@ -121,7 +128,14 @@ GPUTextureView* GPUSwapChainVulkan::GetBackBufferView() ASSERT(_acquiredImageIndex != -1); auto context = _device->MainContext; - const auto backBuffer = &_backBuffers[_acquiredImageIndex].Handle; + + // Wait for prior GPU work that used this acquired image before recording + // commands against it again. Waiting before acquire can target a different image + // and unnecessarily serialize frames when the swapchain has multiple images. + auto& acquiredBackBuffer = _backBuffers[_acquiredImageIndex]; + acquiredBackBuffer.WaitForSubmit(); + + const auto backBuffer = &acquiredBackBuffer.Handle; auto cmdBufferManager = context->GetCmdBufferManager(); auto cmdBuffer = cmdBufferManager->GetCmdBuffer(); @@ -142,17 +156,6 @@ GPUTextureView* GPUSwapChainVulkan::GetBackBufferView() void GPUSwapChainVulkan::Begin(RenderTask* task) { GPUSwapChain::Begin(task); - - // Wait for the backbuffer to be available - if (_currentImageIndex != -1) - { - auto& backBuffer = _backBuffers[_currentImageIndex]; - if (backBuffer.SubmitCmdBuffer) - { - backBuffer.SubmitCmdBuffer->Wait(); - backBuffer.SubmitCmdBuffer = nullptr; - } - } } bool GPUSwapChainVulkan::Resize(int32 width, int32 height) @@ -589,6 +592,7 @@ void GPUSwapChainVulkan::Present(bool vsync) acquiredBackBuffer.SubmitCmdBuffer = context->GetCmdBufferManager()->GetActiveCmdBuffer(); context->GetCmdBufferManager()->SubmitActiveCmdBuffer(_backBuffers[_acquiredImageIndex].RenderingDoneSemaphore); + acquiredBackBuffer.SubmitCmdBufferFenceCounter = acquiredBackBuffer.SubmitCmdBuffer->GetSubmittedFenceCounter(); // Present the back buffer to the viewport window const auto result = TryPresent(DoPresent, _device->PresentQueue, true); diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.h b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.h index 638c16bea..87ef6fb07 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.h +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.h @@ -34,6 +34,11 @@ public: /// CmdBufferVulkan* SubmitCmdBuffer = nullptr; + /// + /// The fence counter value for SubmitCmdBuffer at the time it was submitted. + /// + uint64 SubmitCmdBufferFenceCounter = 0; + /// /// The render target surface handle. /// @@ -41,6 +46,7 @@ public: public: void Setup(GPUSwapChainVulkan* window, VkImage backbuffer, PixelFormat format, VkExtent3D extent); + void WaitForSubmit(); void Release(); public: diff --git a/Source/Engine/Platform/iOS/iOSPlatform.cpp b/Source/Engine/Platform/iOS/iOSPlatform.cpp index a6c57c587..6eb1a3fe2 100644 --- a/Source/Engine/Platform/iOS/iOSPlatform.cpp +++ b/Source/Engine/Platform/iOS/iOSPlatform.cpp @@ -350,7 +350,13 @@ MessagePipeline MainThreadPipeline; // Create UI thread update callback self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(UIThreadMain)]; - self.displayLink.preferredFramesPerSecond = 60; + const int32 targetFrameRate = 60; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000 + if (@available(iOS 15.0, *)) + self.displayLink.preferredFrameRateRange = CAFrameRateRangeMake(targetFrameRate, targetFrameRate, targetFrameRate); + else +#endif + self.displayLink.preferredFramesPerSecond = targetFrameRate; [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; // Run engine on a separate main thread diff --git a/Source/Platforms/iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj b/Source/Platforms/iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj index 047dde664..e5644554b 100644 --- a/Source/Platforms/iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj +++ b/Source/Platforms/iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj @@ -295,6 +295,7 @@ ${PBXResourcesGroup} GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = FlaxGame/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "${ProjectName}"; + INFOPLIST_KEY_CADisableMinimumFrameDurationOnPhone = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; @@ -336,6 +337,7 @@ ${PBXResourcesGroup} GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = FlaxGame/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "${ProjectName}"; + INFOPLIST_KEY_CADisableMinimumFrameDurationOnPhone = YES; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; diff --git a/Source/Platforms/iOS/Binaries/Project/FlaxGame/Info.plist b/Source/Platforms/iOS/Binaries/Project/FlaxGame/Info.plist index 99eb6f55c..1095d1e5a 100644 --- a/Source/Platforms/iOS/Binaries/Project/FlaxGame/Info.plist +++ b/Source/Platforms/iOS/Binaries/Project/FlaxGame/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes From eaf06e95238268d54c165f5fd3ffef71b5007bde Mon Sep 17 00:00:00 2001 From: Andrei Gagua Date: Tue, 9 Jun 2026 15:16:35 +0300 Subject: [PATCH 13/27] Add CLion CMake facade project generation Add CLion as a supported editor workflow for native C++ development. The new -clion generation option writes a CMake facade project under Cache/Projects/CMake/, exposing a CLion-friendly code model, build presets, and launcher targets while keeping Flax.Build as the source of truth for native builds. --- README.md | 12 +- .../DefaultSourceCodeEditor.cs | 3 + Source/Editor/Scripting/CodeEditor.cpp | 2 + Source/Editor/Scripting/CodeEditor.h | 5 + .../Scripting/CodeEditors/CLionCodeEditor.cpp | 369 +++++++++++ .../Scripting/CodeEditors/CLionCodeEditor.h | 46 ++ .../Flax.Build/Build/Builder.Projects.cs | 2 + Source/Tools/Flax.Build/Configuration.cs | 6 + .../Projects/CMake/CMakeProjectGenerator.cs | 624 ++++++++++++++++++ .../Flax.Build/Projects/ProjectFormat.cs | 5 + .../Flax.Build/Projects/ProjectGenerator.cs | 2 + 11 files changed, 1075 insertions(+), 1 deletion(-) create mode 100644 Source/Editor/Scripting/CodeEditors/CLionCodeEditor.cpp create mode 100644 Source/Editor/Scripting/CodeEditors/CLionCodeEditor.h create mode 100644 Source/Tools/Flax.Build/Projects/CMake/CMakeProjectGenerator.cs diff --git a/README.md b/README.md index 924995b4f..ccb75a475 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,17 @@ Follow the instructions below to compile and run the engine from source. * Open workspace with XCode or Visual Studio Code * Build and run (configuration `Editor.Mac.Development`) -#### Troubleshooting +## CLion + +CLion support is provided through generated CMake facade project files for native C++ development on desktop host platforms: Windows, Linux, and Mac. Generate them by passing `-clion` to the project generation script, for example `GenerateProjectFiles.bat -clion`, `./GenerateProjectFiles.sh -clion`, or `GenerateProjectFiles.command -clion`. + +The generated CMake project is written to `Cache/Projects/CMake/`. Open that directory in CLion as a CMake project. Flax does not generate `.idea` files; CLion owns and updates its own local IDE settings after the project is opened. + +This CMake project is an IDE facade, not the primary Flax build system. CMake is used to describe the code model and expose build presets, while actual native build steps are delegated to `Flax.Build`. Run configurations launch the generated engine executable after building it. + +CLion project generation intentionally excludes platforms that are not supported by this workflow, including Android, iOS, UWP, Web, GDK/Xbox, PlayStation, and Switch. The generated CLion project does not configure C# debugging; use an IDE or editor with .NET debugging support for managed code. + +## Troubleshooting * `Could not execute because the specified command or file was not found.` diff --git a/Source/Editor/Modules/SourceCodeEditing/DefaultSourceCodeEditor.cs b/Source/Editor/Modules/SourceCodeEditing/DefaultSourceCodeEditor.cs index 00057e9a7..dd9958530 100644 --- a/Source/Editor/Modules/SourceCodeEditing/DefaultSourceCodeEditor.cs +++ b/Source/Editor/Modules/SourceCodeEditing/DefaultSourceCodeEditor.cs @@ -40,6 +40,7 @@ namespace FlaxEditor.Modules.SourceCodeEditing var codeEditing = Editor.Instance.CodeEditing; var vsCode = codeEditing.GetInBuildEditor(CodeEditorTypes.VSCode); var rider = codeEditing.GetInBuildEditor(CodeEditorTypes.Rider); + var clion = codeEditing.GetInBuildEditor(CodeEditorTypes.CLion); #if PLATFORM_WINDOWS // Favor the newest Visual Studio @@ -66,6 +67,8 @@ namespace FlaxEditor.Modules.SourceCodeEditing _currentEditor = vsCode; else if (rider != null) _currentEditor = rider; + else if (clion != null) + _currentEditor = clion; else _currentEditor = codeEditing.GetInBuildEditor(CodeEditorTypes.SystemDefault); } diff --git a/Source/Editor/Scripting/CodeEditor.cpp b/Source/Editor/Scripting/CodeEditor.cpp index fcc8eef7c..5df4d58bf 100644 --- a/Source/Editor/Scripting/CodeEditor.cpp +++ b/Source/Editor/Scripting/CodeEditor.cpp @@ -6,6 +6,7 @@ #include "ScriptsBuilder.h" #include "CodeEditors/VisualStudioCodeEditor.h" #include "CodeEditors/RiderCodeEditor.h" +#include "CodeEditors/CLionCodeEditor.h" #if USE_VISUAL_STUDIO_DTE #include "CodeEditors/VisualStudio/VisualStudioEditor.h" #endif @@ -269,6 +270,7 @@ bool CodeEditingManagerService::Init() #endif VisualStudioCodeEditor::FindEditors(&CodeEditors); RiderCodeEditor::FindEditors(&CodeEditors); + CLionCodeEditor::FindEditors(&CodeEditors); CodeEditors.Add(New()); return false; diff --git a/Source/Editor/Scripting/CodeEditor.h b/Source/Editor/Scripting/CodeEditor.h index 0baae21b0..92c3fede7 100644 --- a/Source/Editor/Scripting/CodeEditor.h +++ b/Source/Editor/Scripting/CodeEditor.h @@ -82,6 +82,11 @@ API_ENUM(Namespace="FlaxEditor", Attributes="HideInEditor") enum class CodeEdito /// Rider, + /// + /// CLion + /// + CLion, + MAX }; diff --git a/Source/Editor/Scripting/CodeEditors/CLionCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/CLionCodeEditor.cpp new file mode 100644 index 000000000..185e9d95b --- /dev/null +++ b/Source/Editor/Scripting/CodeEditors/CLionCodeEditor.cpp @@ -0,0 +1,369 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#include "CLionCodeEditor.h" +#include "Engine/Platform/FileSystem.h" +#include "Engine/Core/Collections/Sorting.h" +#include "Engine/Engine/Globals.h" +#include "Engine/Platform/CreateProcessSettings.h" +#include "Engine/Platform/File.h" +#include "Engine/Serialization/Json.h" +#include "Editor/Editor.h" +#include "Editor/ProjectInfo.h" +#include "Editor/Scripting/ScriptsBuilder.h" + +#if PLATFORM_WINDOWS +#include "Engine/Platform/Win32/IncludeWindowsHeaders.h" +#elif PLATFORM_MAC +#include "Engine/Platform/Apple/AppleUtils.h" +#include +#endif + +namespace +{ + struct CLionInstallation + { + String path; + String argumentsPrefix; + String version; + + CLionInstallation(const String& path_, const String& argumentsPrefix_, const String& version_) + : path(path_) + , argumentsPrefix(argumentsPrefix_) + , version(version_) + { + } + }; + + void AddInstallation(Array* installations, String path, const String& version, const String& argumentsPrefix = String::Empty) + { + if (path.IsEmpty()) + return; + + StringUtils::PathRemoveRelativeParts(path); + for (CLionInstallation* installation : *installations) + { + if (installation->path == path && installation->argumentsPrefix == argumentsPrefix) + return; + } + installations->Add(New(path, argumentsPrefix, version)); + } + + bool IsCLionProduct(rapidjson_flax::Document& document) + { + auto productCodeMember = document.FindMember("productCode"); + if (productCodeMember != document.MemberEnd() && productCodeMember->value == "CL") + return true; + + auto nameMember = document.FindMember("name"); + if (nameMember == document.MemberEnd()) + return false; + + const String name = nameMember->value.GetText(); + return name == TEXT("CLion") || name == TEXT("JetBrains CLion"); + } + + void SearchDirectory(Array* installations, const String& directory, String launchOverridePath = String::Empty, String launchArgumentsPrefix = String::Empty) + { + if (!FileSystem::DirectoryExists(directory)) + return; + + // Load product info + Array productInfoData; + const String productInfoPath = directory / TEXT("product-info.json"); + if (!FileSystem::FileExists(productInfoPath) || File::ReadAllBytes(productInfoPath, productInfoData)) + return; + rapidjson_flax::Document document; + document.Parse((char*)productInfoData.Get(), productInfoData.Count()); + if (document.HasParseError() || !IsCLionProduct(document)) + return; + + // Find version + auto versionMember = document.FindMember("version"); + if (versionMember == document.MemberEnd()) + return; + + // Find executable file path + auto launchMember = document.FindMember("launch"); + if (launchMember == document.MemberEnd() || !launchMember->value.IsArray() || launchMember->value.Size() == 0) + return; + + auto launcherPathMember = launchMember->value[0].FindMember("launcherPath"); + if (launcherPathMember == launchMember->value[0].MemberEnd()) + return; + + auto launcherPath = launcherPathMember->value.GetText(); + auto exePath = directory / launcherPath; + if (!launcherPath.HasChars() || !FileSystem::FileExists(exePath)) + return; + + AddInstallation(installations, launchOverridePath != String::Empty ? launchOverridePath : exePath, versionMember->value.GetText(), launchArgumentsPrefix); + } + + void SearchExecutable(Array* installations, const String& path) + { + if (FileSystem::FileExists(path)) + AddInstallation(installations, path, TEXT("0.0.0")); + } + + void ParseVersion(const String& text, int32 version[3]) + { + version[0] = 0; + version[1] = 0; + version[2] = 0; + + Array values; + text.Split('.', values); + for (int32 i = 0; i < values.Count() && i < 3; i++) + StringUtils::Parse(values[i].Get(), &version[i]); + } + +#if PLATFORM_WINDOWS + bool FindRegistryKeyItems(HKEY hKey, Array& results) + { + Char nameBuffer[256]; + for (int32 i = 0;; i++) + { + const LONG result = RegEnumKeyW(hKey, i, nameBuffer, ARRAY_COUNT(nameBuffer)); + if (result == ERROR_NO_MORE_ITEMS) + break; + if (result != ERROR_SUCCESS) + return false; + results.Add(nameBuffer); + } + return true; + } + + void SearchRegistry(Array* installations, HKEY root, const Char* key, const Char* valueName = TEXT("")) + { + // Open key + HKEY keyH; + if (RegOpenKeyExW(root, key, 0, KEY_READ, &keyH) != ERROR_SUCCESS) + return; + + // Iterate over subkeys + Array subKeys; + if (FindRegistryKeyItems(keyH, subKeys)) + { + for (auto& subKey : subKeys) + { + HKEY subKeyH; + if (RegOpenKeyExW(keyH, *subKey, 0, KEY_READ, &subKeyH) != ERROR_SUCCESS) + continue; + + // Read subkey value + DWORD type; + DWORD cbData; + if (RegQueryValueExW(subKeyH, valueName, nullptr, &type, nullptr, &cbData) != ERROR_SUCCESS || type != REG_SZ) + { + RegCloseKey(subKeyH); + continue; + } + Array data; + data.Resize((int32)cbData / sizeof(Char)); + if (RegQueryValueExW(subKeyH, valueName, nullptr, nullptr, reinterpret_cast(data.Get()), &cbData) != ERROR_SUCCESS) + { + RegCloseKey(subKeyH); + continue; + } + + // Check if it's a valid installation path + String path(data.Get(), data.Count() - 1); + SearchDirectory(installations, path); + + RegCloseKey(subKeyH); + } + } + + RegCloseKey(keyH); + } +#endif +} + +bool SortInstallations(CLionInstallation* const& i1, CLionInstallation* const& i2) +{ + int32 version1[3], version2[3]; + ParseVersion(i1->version, version1); + ParseVersion(i2->version, version2); + + // Compare by MAJOR.MINOR.BUILD + if (version1[0] == version2[0]) + { + if (version1[1] == version2[1]) + return version1[2] > version2[2]; + return version1[1] > version2[1]; + } + return version1[0] > version2[0]; +} + +CLionCodeEditor::CLionCodeEditor(const String& execPath, const String& execArgsPrefix) + : _execPath(execPath) + , _execArgsPrefix(execArgsPrefix) + , _projectPath(Globals::ProjectFolder / TEXT("Cache/Projects/CMake") / Editor::Project->Name) +{ +} + +String CLionCodeEditor::GetProcessArguments(const String& arguments) const +{ + return _execArgsPrefix.HasChars() ? _execArgsPrefix + TEXT(" ") + arguments : arguments; +} + +void CLionCodeEditor::FindEditors(Array* output) +{ + Array installations; + Array subDirectories; + + String localAppDataPath; + FileSystem::GetSpecialFolderPath(SpecialFolder::LocalAppData, localAppDataPath); + +#if PLATFORM_WINDOWS + // Lookup from all known registry locations + SearchRegistry(&installations, HKEY_CURRENT_USER, TEXT("SOFTWARE\\JetBrains\\CLion")); + SearchRegistry(&installations, HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\JetBrains\\CLion")); + SearchRegistry(&installations, HKEY_CURRENT_USER, TEXT("SOFTWARE\\JetBrains\\CLion"), TEXT("InstallDir")); + SearchRegistry(&installations, HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\JetBrains\\CLion"), TEXT("InstallDir")); + SearchRegistry(&installations, HKEY_CURRENT_USER, TEXT("SOFTWARE\\WOW6432Node\\JetBrains\\CLion")); + SearchRegistry(&installations, HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\WOW6432Node\\JetBrains\\CLion")); + + // Versions installed via JetBrains Toolbox + FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("Programs")); + FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains\\Toolbox\\apps\\CLion\\ch-0\\")); + FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains\\Toolbox\\apps\\CLion\\ch-1\\")); // Beta versions +#endif +#if PLATFORM_LINUX + // TODO: detect Snap installations by reading the desktop file from ~/.local/share/applications and /usr/share/applications. + + SearchDirectory(&installations, TEXT("/usr/share/clion/")); + FileSystem::GetChildDirectories(subDirectories, TEXT("/usr/share/clion")); + + // Default suggested location for standalone installations + FileSystem::GetChildDirectories(subDirectories, TEXT("/opt/")); + + // Versions installed via JetBrains Toolbox + SearchDirectory(&installations, localAppDataPath / TEXT("JetBrains/Toolbox/apps/clion/")); + FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains/Toolbox/apps/CLion/ch-0")); + FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains/Toolbox/apps/CLion/ch-1")); // Beta versions + + // Detect Flatpak installations + SearchDirectory(&installations, + TEXT("/var/lib/flatpak/app/com.jetbrains.CLion/current/active/files/extra/clion/"), + TEXT("flatpak"), + TEXT("run com.jetbrains.CLion")); + + SearchExecutable(&installations, TEXT("/usr/bin/clion")); + SearchExecutable(&installations, TEXT("/snap/bin/clion")); +#endif + +#if PLATFORM_MAC + String applicationSupportFolder; + FileSystem::GetSpecialFolderPath(SpecialFolder::ProgramData, applicationSupportFolder); + + NSURL* appURL = [[NSWorkspace sharedWorkspace] URLForApplicationWithBundleIdentifier:@"com.jetbrains.CLion"]; + if (appURL != nullptr) + { + const String appPath = AppleUtils::ToString((CFStringRef)[appURL path]); + SearchDirectory(&installations, appPath / TEXT("Contents/Resources"), appPath); + } + + Array subMacDirectories; + FileSystem::GetChildDirectories(subMacDirectories, applicationSupportFolder / TEXT("JetBrains/Toolbox/apps/CLion/ch-0/")); + FileSystem::GetChildDirectories(subMacDirectories, applicationSupportFolder / TEXT("JetBrains/Toolbox/apps/CLion/ch-1/")); + for (const String& directory : subMacDirectories) + { + String clionAppPath = directory / TEXT("CLion.app"); + SearchDirectory(&installations, clionAppPath / TEXT("Contents/Resources"), clionAppPath); + } + + // Check the local installer version + SearchDirectory(&installations, TEXT("/Applications/CLion.app/Contents/Resources"), TEXT("/Applications/CLion.app")); + + String userFolder; + FileSystem::GetSpecialFolderPath(SpecialFolder::Documents, userFolder); + String clionAppPath = userFolder / TEXT("../Applications/CLion.app"); + SearchDirectory(&installations, clionAppPath / TEXT("Contents/Resources"), clionAppPath); +#endif + + for (const String& directory : subDirectories) + SearchDirectory(&installations, directory); + + // Sort found installations by version number + Sorting::QuickSort(installations.Get(), installations.Count(), &SortInstallations); + + for (CLionInstallation* installation : installations) + { + output->Add(New(installation->path, installation->argumentsPrefix)); + Delete(installation); + } +} + +CodeEditorTypes CLionCodeEditor::GetType() const +{ + return CodeEditorTypes::CLion; +} + +String CLionCodeEditor::GetName() const +{ + return TEXT("CLion"); +} + +String CLionCodeEditor::GetGenerateProjectCustomArgs() const +{ + return TEXT("-clion"); +} + +void CLionCodeEditor::OpenFile(const String& path, int32 line) +{ + // Generate project files if missing + if (!FileSystem::FileExists(_projectPath / TEXT("CMakeLists.txt")) || + !FileSystem::FileExists(_projectPath / TEXT("CMakePresets.json"))) + { + ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); + } + + // Open file + line = line > 0 ? line : 1; + CreateProcessSettings procSettings; + +#if !PLATFORM_MAC + procSettings.FileName = _execPath; + procSettings.Arguments = GetProcessArguments(String::Format(TEXT("\"{0}\" --line {2} \"{1}\""), _projectPath, path, line)); +#else + procSettings.FileName = "/usr/bin/open"; + procSettings.Arguments = String::Format(TEXT("-n -a \"{0}\" --args \"{1}\" --line {3} \"{2}\""), _execPath, _projectPath, path, line); +#endif + + procSettings.HiddenWindow = false; + procSettings.WaitForEnd = false; + procSettings.LogOutput = false; + procSettings.ShellExecute = true; + Platform::CreateProcess(procSettings); +} + +void CLionCodeEditor::OpenSolution() +{ + // Generate project files if missing + if (!FileSystem::FileExists(_projectPath / TEXT("CMakeLists.txt")) || + !FileSystem::FileExists(_projectPath / TEXT("CMakePresets.json"))) + { + ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); + } + + // Open solution + CreateProcessSettings procSettings; +#if !PLATFORM_MAC + procSettings.FileName = _execPath; + procSettings.Arguments = GetProcessArguments(String::Format(TEXT("\"{0}\""), _projectPath)); +#else + procSettings.FileName = "/usr/bin/open"; + procSettings.Arguments = String::Format(TEXT("-n -a \"{0}\" \"{1}\""), _execPath, _projectPath); +#endif + procSettings.HiddenWindow = false; + procSettings.WaitForEnd = false; + procSettings.LogOutput = false; + procSettings.ShellExecute = true; + Platform::CreateProcess(procSettings); +} + +void CLionCodeEditor::OnFileAdded(const String& path) +{ + ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); +} diff --git a/Source/Editor/Scripting/CodeEditors/CLionCodeEditor.h b/Source/Editor/Scripting/CodeEditors/CLionCodeEditor.h new file mode 100644 index 000000000..a94e0e157 --- /dev/null +++ b/Source/Editor/Scripting/CodeEditors/CLionCodeEditor.h @@ -0,0 +1,46 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#include "Editor/Scripting/CodeEditor.h" + +/// +/// Implementation of code editor utility that is using CLion from JetBrains. +/// +class CLionCodeEditor : public CodeEditor +{ +private: + + String _execPath; + String _execArgsPrefix; + String _projectPath; + + String GetProcessArguments(const String& arguments) const; + +public: + + /// + /// Initializes a new instance of the class. + /// + /// Executable file path + /// Additional arguments to pass before CLion arguments. + CLionCodeEditor(const String& execPath, const String& execArgsPrefix = String::Empty); + +public: + + /// + /// Tries to find installed CLion instances. Adds them to the result list. + /// + /// The output editors. + static void FindEditors(Array* output); + +public: + + // [CodeEditor] + CodeEditorTypes GetType() const override; + String GetName() const override; + String GetGenerateProjectCustomArgs() const override; + void OpenFile(const String& path, int32 line) override; + void OpenSolution() override; + void OnFileAdded(const String& path) override; +}; diff --git a/Source/Tools/Flax.Build/Build/Builder.Projects.cs b/Source/Tools/Flax.Build/Build/Builder.Projects.cs index 46925355c..7af540d52 100644 --- a/Source/Tools/Flax.Build/Build/Builder.Projects.cs +++ b/Source/Tools/Flax.Build/Build/Builder.Projects.cs @@ -204,6 +204,8 @@ namespace Flax.Build projectFormats.Add(ProjectFormat.VisualStudio2015); if (Configuration.ProjectFormatVSCode) projectFormats.Add(ProjectFormat.VisualStudioCode); + if (Configuration.ProjectFormatCLion) + projectFormats.Add(ProjectFormat.CMake); if (Configuration.ProjectFormatRider) projectFormats.Add(ProjectFormat.VisualStudio2022); if (!string.IsNullOrEmpty(Configuration.ProjectFormatCustom)) diff --git a/Source/Tools/Flax.Build/Configuration.cs b/Source/Tools/Flax.Build/Configuration.cs index 8a662df10..b2787386b 100644 --- a/Source/Tools/Flax.Build/Configuration.cs +++ b/Source/Tools/Flax.Build/Configuration.cs @@ -213,6 +213,12 @@ namespace Flax.Build [CommandLine("vscode", "Generates Visual Studio Code project format files. Valid only with -genproject option.")] public static bool ProjectFormatVSCode = false; + /// + /// Generates CMake facade project files for CLion. Valid only with -genproject option. + /// + [CommandLine("clion", "Generates CMake facade project files for CLion. Valid only with -genproject option.")] + public static bool ProjectFormatCLion = false; + /// /// Generates Visual Studio 2022 project format files for Rider. Valid only with -genproject option. /// diff --git a/Source/Tools/Flax.Build/Projects/CMake/CMakeProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/CMake/CMakeProjectGenerator.cs new file mode 100644 index 000000000..da68565fb --- /dev/null +++ b/Source/Tools/Flax.Build/Projects/CMake/CMakeProjectGenerator.cs @@ -0,0 +1,624 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Flax.Build.NativeCpp; + +namespace Flax.Build.Projects.CMake +{ + /// + /// Project generator for CMake-based IDE facade. + /// + public class CMakeProjectGenerator : ProjectGenerator + { + /// + public override string ProjectFileExtension => "cmake"; + + /// + public override string SolutionFileExtension => "cmake"; + + /// + public override TargetType? Type => null; + + /// + public override void GenerateProject(Project project, string solutionPath, bool isMainProject) + { + // Not used, solution contains all CMake project definitions. + } + + /// + public override void GenerateSolution(Solution solution) + { + var projectFolder = GetProjectFolder(solution); + if (!Directory.Exists(projectFolder)) + Directory.CreateDirectory(projectFolder); + + var generateCLionFiles = Configuration.ProjectFormatCLion; + var contents = new StringBuilder(); + var buildPresets = new List(); + var defaultBuildTarget = string.Empty; + var targetNames = new HashSet(); + var buildToolPath = Path.ChangeExtension(typeof(Builder).Assembly.Location, null) + Utilities.GetPlatformExecutableExt(); + + contents.AppendLine("cmake_minimum_required(VERSION 3.20)"); + contents.AppendLine(); + contents.AppendLine("# Generated by Flax.Build. Do not edit manually."); + contents.AppendLine("# CMake is used as an IDE facade. Actual builds are delegated to Flax.Build."); + contents.AppendLine($"project({EscapeIdentifier(solution.Name)} LANGUAGES C CXX)"); + contents.AppendLine(); + contents.AppendLine("set(CMAKE_EXPORT_COMPILE_COMMANDS ON)"); + contents.AppendLine($"set(FLAX_WORKSPACE {QuotePath(solution.WorkspaceRootPath, solution.WorkspaceRootPath)})"); + contents.AppendLine($"set(FLAX_BUILD_TOOL {QuotePath(buildToolPath, solution.WorkspaceRootPath)})"); + if (DotNetSdk.Instance.IsValid) + contents.AppendLine($"set(FLAX_DOTNET_ROOT {QuotePath(DotNetSdk.Instance.RootPath, solution.WorkspaceRootPath)})"); + contents.AppendLine(); + + foreach (var project in solution.Projects.Where(x => x.Type == TargetType.NativeCpp)) + { + var visibleFiles = GetProjectFiles(project, generateCLionFiles); + var hasCodeModelConfiguration = TryGetCodeModelConfiguration(project, out var codeModelConfiguration); + // Use Flax.Build-selected compile units for the code model. The wider project scan is only for visible files. + var compileFiles = hasCodeModelConfiguration + ? GetCodeModelCompileFiles(codeModelConfiguration, generateCLionFiles) + : visibleFiles.Where(x => IsCompileSourceFile(x, generateCLionFiles)).ToArray(); + var files = new HashSet(visibleFiles); + files.AddRange(compileFiles); + var allFiles = files.OrderBy(x => x).ToArray(); + var compileFileSet = new HashSet(compileFiles); + var nonCompileFiles = allFiles.Where(x => !compileFileSet.Contains(x)).ToArray(); + var includePaths = hasCodeModelConfiguration ? GetIncludePaths(project, codeModelConfiguration) : Array.Empty(); + var definitions = hasCodeModelConfiguration ? GetPreprocessorDefinitions(project, codeModelConfiguration) : project.Defines.OrderBy(x => x).ToArray(); + var cppStandard = hasCodeModelConfiguration ? GetCppStandard(codeModelConfiguration) : 14; + var ideTargetName = GetUniqueTargetName(targetNames, "IDE_" + project.Name); + + contents.AppendLine($"# {project.Name}"); + if (compileFiles.Length != 0) + { + contents.AppendLine($"add_library({ideTargetName} STATIC EXCLUDE_FROM_ALL)"); + contents.AppendLine($"set_target_properties({ideTargetName} PROPERTIES EXCLUDE_FROM_ALL TRUE)"); + AppendTargetSources(contents, ideTargetName, allFiles, solution.WorkspaceRootPath); + if (nonCompileFiles.Length != 0) + { + contents.AppendLine("set_source_files_properties("); + foreach (var file in nonCompileFiles) + contents.AppendLine($" {QuotePath(file, solution.WorkspaceRootPath)}"); + contents.AppendLine(" PROPERTIES HEADER_FILE_ONLY TRUE"); + contents.AppendLine(")"); + } + contents.AppendLine($"set_target_properties({ideTargetName} PROPERTIES CXX_STANDARD {cppStandard} CXX_STANDARD_REQUIRED YES)"); + AppendTargetPaths(contents, ideTargetName, "target_include_directories", "PRIVATE", includePaths, solution.WorkspaceRootPath); + AppendTargetValues(contents, ideTargetName, "target_compile_definitions", "PRIVATE", definitions); + } + else if (allFiles.Length != 0) + { + contents.AppendLine($"add_custom_target({ideTargetName} SOURCES"); + foreach (var file in allFiles) + contents.AppendLine($" {QuotePath(file, solution.WorkspaceRootPath)}"); + contents.AppendLine(")"); + } + + foreach (var configuration in project.Configurations) + { + if (generateCLionFiles && !IsCLionDevelopmentPlatform(configuration.Platform)) + continue; + + var buildTargetName = GetUniqueTargetName(targetNames, "Build_" + project.Name + "_" + configuration.Target.Name + "_" + configuration.PlatformName + "_" + configuration.ConfigurationName + "_" + configuration.ArchitectureName); + + AppendBuildTarget(contents, buildTargetName, "-build", configuration); + var presetSuffix = project.Name + "-" + configuration.Target.Name + "-" + configuration.PlatformName + "-" + configuration.ConfigurationName + "-" + configuration.ArchitectureName; + if (IsHostConfiguration(configuration)) + AddBuildPreset(buildPresets, "build-" + presetSuffix, "Build " + presetSuffix, buildTargetName); + + if (defaultBuildTarget.Length == 0 && project == solution.MainProject && configuration.Platform == Platform.BuildPlatform.Target && configuration.Architecture == Platform.BuildTargetArchitecture && configuration.Configuration == TargetConfiguration.Development) + defaultBuildTarget = buildTargetName; + + if (generateCLionFiles) + { + var runProgram = GetRunProgram(project, configuration, out var runArguments); + if (IsHostConfiguration(configuration) && !string.IsNullOrEmpty(runProgram)) + { + var runTargetName = GetUniqueTargetName(targetNames, "App_" + project.Name + "_" + configuration.Target.Name + "_" + configuration.PlatformName + "_" + configuration.ConfigurationName + "_" + configuration.ArchitectureName); + AppendRunTarget(contents, runTargetName, runProgram, runArguments); + contents.AppendLine($"add_dependencies({runTargetName} {buildTargetName})"); + contents.AppendLine(); + } + } + } + + contents.AppendLine(); + } + + if (!string.IsNullOrEmpty(defaultBuildTarget)) + { + contents.AppendLine($"add_custom_target({EscapeIdentifier(solution.Name)}_Build ALL DEPENDS {defaultBuildTarget})"); + contents.AppendLine(); + } + + Utilities.WriteFileIfChanged(Path.Combine(projectFolder, "CMakeLists.txt"), contents.ToString()); + GeneratePresets(solution, projectFolder, buildPresets); + } + + /// + /// Gets the directory that contains the generated CMake facade project. + /// + public static string GetProjectFolder(Solution solution) + { + return Path.Combine(solution.WorkspaceRootPath, "Cache", "Projects", "CMake", solution.Name); + } + + private struct CMakeBuildPreset + { + public string Name; + public string DisplayName; + public string Target; + } + + private static void AddBuildPreset(List presets, string name, string displayName, string target) + { + presets.Add(new CMakeBuildPreset + { + Name = EscapePresetName(name), + DisplayName = displayName, + Target = target, + }); + } + + private static void GeneratePresets(Solution solution, string projectFolder, List buildPresets) + { + var contents = new StringBuilder(); + contents.AppendLine("{"); + contents.AppendLine(" \"version\": 3,"); + contents.AppendLine(" \"cmakeMinimumRequired\": {"); + contents.AppendLine(" \"major\": 3,"); + contents.AppendLine(" \"minor\": 20,"); + contents.AppendLine(" \"patch\": 0"); + contents.AppendLine(" },"); + contents.AppendLine(" \"configurePresets\": ["); + contents.AppendLine(" {"); + contents.AppendLine(" \"name\": \"flax\","); + contents.AppendLine(" \"displayName\": \"Flax\","); + contents.AppendLine(" \"description\": \"Configure Flax CMake facade for IDE integration.\","); + contents.AppendLine(" \"generator\": \"Ninja\","); + contents.AppendLine(" \"binaryDir\": \"${sourceDir}/cmake-build-flax\","); + contents.AppendLine(" \"cacheVariables\": {"); + contents.AppendLine(" \"CMAKE_BUILD_TYPE\": \"Debug\","); + contents.AppendLine(" \"CMAKE_EXPORT_COMPILE_COMMANDS\": \"ON\""); + contents.AppendLine(" }"); + contents.AppendLine(" }"); + contents.AppendLine(" ],"); + contents.AppendLine(" \"buildPresets\": ["); + for (var i = 0; i < buildPresets.Count; i++) + { + var preset = buildPresets[i]; + contents.AppendLine(" {"); + contents.AppendLine($" \"name\": {JsonString(preset.Name)},"); + contents.AppendLine($" \"displayName\": {JsonString(preset.DisplayName)},"); + contents.AppendLine(" \"configurePreset\": \"flax\","); + contents.AppendLine($" \"targets\": [{JsonString(preset.Target)}]"); + contents.Append(" }"); + if (i != buildPresets.Count - 1) + contents.Append(','); + contents.AppendLine(); + } + contents.AppendLine(" ]"); + contents.AppendLine("}"); + + Utilities.WriteFileIfChanged(Path.Combine(projectFolder, "CMakePresets.json"), contents.ToString()); + } + + private static void AppendBuildTarget(StringBuilder contents, string targetName, string action, Project.ConfigurationData configuration) + { + contents.AppendLine($"add_custom_target({targetName}"); + contents.Append(" COMMAND "); + if (DotNetSdk.Instance.IsValid) + contents.Append("${CMAKE_COMMAND} -E env \"DOTNET_ROOT=${FLAX_DOTNET_ROOT}\" "); + contents.AppendLine($"\"${{FLAX_BUILD_TOOL}}\" {Quote(action)} \"-log\" \"-mutex\""); + contents.AppendLine(" \"-workspace=${FLAX_WORKSPACE}\""); + contents.AppendLine($" \"-arch={configuration.ArchitectureName}\""); + contents.AppendLine($" \"-configuration={configuration.ConfigurationName}\""); + contents.AppendLine($" \"-platform={configuration.PlatformName}\""); + contents.AppendLine($" \"-buildTargets={configuration.Target.Name}\""); + if (!string.IsNullOrEmpty(Configuration.Compiler)) + contents.AppendLine($" \"-compiler={EscapeString(Configuration.Compiler)}\""); + if (!string.IsNullOrEmpty(Configuration.Dotnet)) + contents.AppendLine($" \"-dotnet={EscapeString(Configuration.Dotnet)}\""); + if (Configuration.Sanitizers != Sanitizer.None) + contents.AppendLine($" \"-sanitizers={Configuration.Sanitizers}\""); + contents.AppendLine(" WORKING_DIRECTORY \"${FLAX_WORKSPACE}\""); + contents.AppendLine(" USES_TERMINAL"); + contents.AppendLine(" VERBATIM"); + contents.AppendLine(")"); + contents.AppendLine(); + } + + private static void AppendRunTarget(StringBuilder contents, string targetName, string program, string[] arguments) + { + var sourcePath = $"${{CMAKE_CURRENT_BINARY_DIR}}/{targetName}.cpp"; + contents.AppendLine($"file(WRITE \"{sourcePath}\" [=["); + contents.AppendLine("#include "); + contents.AppendLine("#include "); + contents.AppendLine("#ifdef _WIN32"); + contents.AppendLine("#include "); + contents.AppendLine("#else"); + contents.AppendLine("#include "); + contents.AppendLine("#endif"); + contents.AppendLine("int main(int argc, char** argv) {"); + contents.AppendLine($" const char* program = \"{CppString(program)}\";"); + contents.AppendLine(" std::vector args;"); + contents.AppendLine(" args.reserve(argc + 1);"); + contents.AppendLine(" args.push_back(program);"); + foreach (var arg in arguments) + contents.AppendLine($" args.push_back(\"{CppString(arg)}\");"); + contents.AppendLine(" for (int i = 1; i < argc; i++)"); + contents.AppendLine(" args.push_back(argv[i]);"); + contents.AppendLine(" args.push_back(nullptr);"); + contents.AppendLine("#ifdef _WIN32"); + contents.AppendLine(" _execv(args[0], args.data());"); + contents.AppendLine("#else"); + contents.AppendLine(" execv(args[0], const_cast(args.data()));"); + contents.AppendLine("#endif"); + contents.AppendLine(" perror(args[0]);"); + contents.AppendLine(" return 1;"); + contents.AppendLine("}"); + contents.AppendLine("]=])"); + contents.AppendLine($"add_executable({targetName} EXCLUDE_FROM_ALL \"{sourcePath}\")"); + contents.AppendLine($"set_target_properties({targetName} PROPERTIES"); + contents.AppendLine(" RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_CURRENT_BINARY_DIR}/FlaxLaunchers\""); + contents.AppendLine(")"); + } + + private static void AppendTargetSources(StringBuilder contents, string targetName, string[] files, string workspaceRoot) + { + contents.AppendLine($"target_sources({targetName} PRIVATE"); + foreach (var file in files) + contents.AppendLine($" {QuotePath(file, workspaceRoot)}"); + contents.AppendLine(")"); + } + + private static void AppendTargetPaths(StringBuilder contents, string targetName, string command, string visibility, string[] values, string workspaceRoot) + { + if (values.Length == 0) + return; + + contents.AppendLine($"{command}({targetName} {visibility}"); + foreach (var value in values) + contents.AppendLine($" {QuotePath(value, workspaceRoot)}"); + contents.AppendLine(")"); + } + + private static void AppendTargetValues(StringBuilder contents, string targetName, string command, string visibility, string[] values) + { + if (values.Length == 0) + return; + + contents.AppendLine($"{command}({targetName} {visibility}"); + foreach (var value in values) + contents.AppendLine($" {Quote(value)}"); + contents.AppendLine(")"); + } + + private static string[] GetProjectFiles(Project project, bool generateCLionFiles) + { + var files = new HashSet(); + if (project.SourceFiles != null) + files.AddRange(project.SourceFiles); + if (project.GeneratedSourceFiles != null) + files.AddRange(project.GeneratedSourceFiles); + if (project.SourceDirectories != null) + { + foreach (var folder in project.SourceDirectories) + { + if (Directory.Exists(folder)) + files.AddRange(Directory.GetFiles(folder, "*", SearchOption.AllDirectories)); + } + } + files.RemoveWhere(x => + !IsProjectFile(x) || + Path.GetFileName(x).Equals(".DS_Store", StringComparison.OrdinalIgnoreCase) || + (generateCLionFiles && !IsCLionDevelopmentSourceFile(x))); + return files.OrderBy(x => x).ToArray(); + } + + private static string[] GetCodeModelCompileFiles(Project.ConfigurationData configuration, bool generateCLionFiles) + { + var files = new HashSet(); + var definitions = new HashSet(configuration.TargetBuildOptions.CompileEnv.PreprocessorDefinitions); + AddCompileFiles(files, configuration.TargetBuildOptions.SourceFiles, generateCLionFiles, definitions); + if (configuration.Modules != null) + { + foreach (var module in configuration.Modules) + { + if (module.Key.BuildNativeCode) + AddCompileFiles(files, module.Value.SourceFiles, generateCLionFiles, definitions); + } + } + return files.OrderBy(x => x).ToArray(); + } + + private static void AddCompileFiles(HashSet files, IEnumerable sourceFiles, bool generateCLionFiles, HashSet definitions = null) + { + if (sourceFiles == null) + return; + + foreach (var file in sourceFiles) + { + if (IsCompileSourceFile(file, generateCLionFiles, definitions)) + files.Add(file); + } + } + + private static bool TryGetCodeModelConfiguration(Project project, out Project.ConfigurationData configuration) + { + foreach (var e in project.Configurations) + { + if (e.TargetBuildOptions != null && + e.Platform == Platform.BuildPlatform.Target && + e.Architecture == Platform.BuildTargetArchitecture && + e.Configuration == TargetConfiguration.Development) + { + configuration = e; + return true; + } + } + + foreach (var e in project.Configurations) + { + if (e.TargetBuildOptions != null) + { + configuration = e; + return true; + } + } + + configuration = default; + return false; + } + + private static bool IsHostConfiguration(Project.ConfigurationData configuration) + { + return configuration.Platform == Platform.BuildPlatform.Target && + configuration.Architecture == Platform.BuildTargetArchitecture; + } + + private static bool IsCLionDevelopmentPlatform(TargetPlatform platform) + { + switch (platform) + { + case TargetPlatform.Windows: + case TargetPlatform.Linux: + case TargetPlatform.Mac: + return true; + default: + return false; + } + } + + private static string[] GetIncludePaths(Project project, Project.ConfigurationData configuration) + { + var result = new HashSet(); + if (project.SearchPaths != null) + result.AddRange(project.SearchPaths); + result.AddRange(configuration.TargetBuildOptions.CompileEnv.IncludePaths); + return result.OrderBy(x => x).ToArray(); + } + + private static string[] GetPreprocessorDefinitions(Project project, Project.ConfigurationData configuration) + { + var result = new HashSet(); + result.AddRange(project.Defines); + result.AddRange(configuration.TargetBuildOptions.CompileEnv.PreprocessorDefinitions); + return result.OrderBy(x => x).ToArray(); + } + + private static int GetCppStandard(Project.ConfigurationData configuration) + { + var result = 14; + switch (configuration.TargetBuildOptions.CompileEnv.CppVersion) + { + case CppVersion.Cpp17: + result = Math.Max(result, 17); + break; + case CppVersion.Cpp20: + case CppVersion.Latest: + result = Math.Max(result, 20); + break; + } + return result; + } + + private static string GetRunProgram(Project project, Project.ConfigurationData configuration, out string[] arguments) + { + var target = configuration.Target; + var outputType = project.OutputType ?? target.OutputType; + if (ShouldRunEditorForProject(configuration, outputType)) + { + var program = Path.Combine(Globals.EngineRoot, Platform.GetEditorBinaryDirectory(), configuration.ConfigurationName, "FlaxEditor" + Utilities.GetPlatformExecutableExt()); + var args = new List(); + if (configuration.Platform == TargetPlatform.Linux || configuration.Platform == TargetPlatform.Mac) + args.Add("-std"); + args.Add("-project"); + args.Add(project.WorkspaceRootPath); + args.Add("-skipCompile"); + arguments = args.ToArray(); + return program; + } + + arguments = configuration.Platform == TargetPlatform.Linux || configuration.Platform == TargetPlatform.Mac ? new[] { "--std" } : Array.Empty(); + return target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable); + } + + private static bool ShouldRunEditorForProject(Project.ConfigurationData configuration, TargetOutputType outputType) + { + return outputType != TargetOutputType.Executable && configuration.Target.IsEditor; + } + + private static bool IsCppCompileFile(string path) + { + var extension = Path.GetExtension(path); + return extension.Equals(".c", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".cc", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".cpp", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".cxx", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsCompileSourceFile(string path, bool generateCLionFiles, HashSet definitions = null) + { + return IsCppCompileFile(path) && + (!generateCLionFiles || (IsCLionDevelopmentSourceFile(path) && IsCLionSourceGuardActive(path, definitions))); + } + + private static bool IsCLionDevelopmentSourceFile(string path) + { + path = Utilities.NormalizePath(path); + return !ContainsPathSegment(path, "Android") && + !ContainsPathSegment(path, "AndroidJNI") && + !ContainsPathSegment(path, "GDK") && + !ContainsPathSegment(path, "iOS") && + !ContainsPathSegment(path, "PS4") && + !ContainsPathSegment(path, "PS5") && + !ContainsPathSegment(path, "Switch") && + !ContainsPathSegment(path, "UWP") && + !ContainsPathSegment(path, "Web") && + !ContainsPathSegment(path, "XboxOne") && + !ContainsPathSegment(path, "XboxScarlett") && + !Path.GetFileName(path).Contains(".Web.", StringComparison.OrdinalIgnoreCase); + } + + private static bool ContainsPathSegment(string path, string segment) + { + return path.IndexOf("/" + segment + "/", StringComparison.OrdinalIgnoreCase) != -1; + } + + private static bool IsCLionSourceGuardActive(string path, HashSet definitions) + { + if (definitions == null) + return true; + + path = Utilities.NormalizePath(path); + if (ContainsSourcePath(path, "Source/Editor/Cooker/Platform/Windows")) + return HasDefinition(definitions, "PLATFORM_TOOLS_WINDOWS"); + if (ContainsSourcePath(path, "Source/Editor/Cooker/Platform/Linux")) + return HasDefinition(definitions, "PLATFORM_TOOLS_LINUX"); + if (ContainsSourcePath(path, "Source/Editor/Cooker/Platform/Mac")) + return HasDefinition(definitions, "PLATFORM_TOOLS_MAC"); + + if (ContainsSourcePath(path, "Source/Engine/Engine/Windows")) + return HasDefinition(definitions, "PLATFORM_WINDOWS") && !HasDefinition(definitions, "USE_EDITOR"); + if (ContainsSourcePath(path, "Source/Engine/Engine/Linux")) + return HasDefinition(definitions, "PLATFORM_LINUX") && !HasDefinition(definitions, "USE_EDITOR"); + if (ContainsSourcePath(path, "Source/Engine/Engine/Mac")) + return HasDefinition(definitions, "PLATFORM_MAC") && !HasDefinition(definitions, "USE_EDITOR"); + + if (ContainsSourcePath(path, "Source/Engine/GraphicsDevice/Vulkan/Win32")) + return HasDefinition(definitions, "GRAPHICS_API_VULKAN") && (HasDefinition(definitions, "PLATFORM_WIN32") || HasDefinition(definitions, "PLATFORM_WINDOWS")); + if (ContainsSourcePath(path, "Source/Engine/GraphicsDevice/Vulkan/Linux")) + return HasDefinition(definitions, "GRAPHICS_API_VULKAN") && HasDefinition(definitions, "PLATFORM_LINUX"); + if (ContainsSourcePath(path, "Source/Engine/GraphicsDevice/Vulkan/Mac")) + return HasDefinition(definitions, "GRAPHICS_API_VULKAN") && HasDefinition(definitions, "PLATFORM_MAC"); + + if (ContainsSourcePath(path, "Source/Engine/Platform/Win32")) + return HasDefinition(definitions, "PLATFORM_WIN32") || HasDefinition(definitions, "PLATFORM_WINDOWS"); + if (ContainsSourcePath(path, "Source/Engine/Platform/Windows")) + return HasDefinition(definitions, "PLATFORM_WINDOWS"); + if (ContainsSourcePath(path, "Source/Engine/Platform/Linux")) + return HasDefinition(definitions, "PLATFORM_LINUX"); + if (ContainsSourcePath(path, "Source/Engine/Platform/Mac")) + return HasDefinition(definitions, "PLATFORM_MAC"); + if (ContainsSourcePath(path, "Source/Engine/Platform/Apple")) + return HasDefinition(definitions, "PLATFORM_MAC") || HasDefinition(definitions, "PLATFORM_IOS"); + if (ContainsSourcePath(path, "Source/Engine/Platform/Unix")) + return HasDefinition(definitions, "PLATFORM_UNIX"); + + return true; + } + + private static bool ContainsSourcePath(string path, string sourcePath) + { + return path.IndexOf("/" + sourcePath + "/", StringComparison.OrdinalIgnoreCase) != -1; + } + + private static bool HasDefinition(HashSet definitions, string name) + { + return definitions.Contains(name) || + definitions.Any(x => x.StartsWith(name + "=", StringComparison.Ordinal)); + } + + private static bool IsProjectFile(string path) + { + var extension = Path.GetExtension(path); + return IsCppCompileFile(path) || + extension.Equals(".h", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".hh", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".hpp", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".hxx", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".inl", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".rc", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".manifest", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".natvis", StringComparison.OrdinalIgnoreCase); + } + + private static string GetUniqueTargetName(HashSet names, string name) + { + var result = EscapeIdentifier(name); + var baseName = result; + var index = 2; + while (!names.Add(result)) + result = baseName + "_" + index++; + return result; + } + + private static string EscapeIdentifier(string value) + { + if (string.IsNullOrEmpty(value)) + return "_"; + + var result = new StringBuilder(value.Length); + foreach (var c in value) + result.Append(char.IsLetterOrDigit(c) || c == '_' ? c : '_'); + if (char.IsDigit(result[0])) + result.Insert(0, '_'); + return result.ToString(); + } + + private static string EscapePresetName(string value) + { + var result = new StringBuilder(value.Length); + foreach (var c in value) + result.Append(char.IsLetterOrDigit(c) || c == '-' || c == '_' || c == '.' ? char.ToLowerInvariant(c) : '-'); + return result.ToString(); + } + + private static string QuotePath(string value, string workspaceRoot, bool includeQuotes = true) + { + value = Utilities.NormalizePath(value); + workspaceRoot = Utilities.NormalizePath(workspaceRoot).TrimEnd('/'); + if (value.StartsWith(workspaceRoot + "/", StringComparison.Ordinal)) + value = "${FLAX_WORKSPACE}/" + value.Substring(workspaceRoot.Length + 1); + return Quote(value, includeQuotes); + } + + private static string Quote(string value, bool includeQuotes = true) + { + value = EscapeString(value); + return includeQuotes ? "\"" + value + "\"" : value; + } + + private static string EscapeString(string value) + { + return value.Replace("\\", "\\\\").Replace(";", "\\;").Replace("\"", "\\\""); + } + + private static string JsonString(string value) + { + return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; + } + + private static string CppString(string value) + { + return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r"); + } + } +} diff --git a/Source/Tools/Flax.Build/Projects/ProjectFormat.cs b/Source/Tools/Flax.Build/Projects/ProjectFormat.cs index 3e5fd3a70..8eccd2448 100644 --- a/Source/Tools/Flax.Build/Projects/ProjectFormat.cs +++ b/Source/Tools/Flax.Build/Projects/ProjectFormat.cs @@ -47,6 +47,11 @@ namespace Flax.Build.Projects /// VisualStudioCode, + /// + /// CMake. + /// + CMake, + /// /// XCode. /// diff --git a/Source/Tools/Flax.Build/Projects/ProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/ProjectGenerator.cs index a24fff6bb..7a52ddc19 100644 --- a/Source/Tools/Flax.Build/Projects/ProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/ProjectGenerator.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using Flax.Build.Projects.CMake; using Flax.Build.Projects.VisualStudio; using Flax.Build.Projects.VisualStudioCode; @@ -150,6 +151,7 @@ namespace Flax.Build.Projects case ProjectFormat.VisualStudioCode: return type == TargetType.DotNet ? (ProjectGenerator)new CSProjectGenerator(VisualStudioVersion.VisualStudio2015) : (ProjectGenerator)new VisualStudioCodeProjectGenerator(); + case ProjectFormat.CMake: return new CMakeProjectGenerator(); case ProjectFormat.XCode: return new XCodeProjectGenerator(); case ProjectFormat.Custom: if (CustomProjectTypes.TryGetValue(Configuration.ProjectFormatCustom, out var factory)) From c27bc9e15093d2b84e830d67c8bdd66f3e7100da Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 27 Jul 2026 20:41:43 -0500 Subject: [PATCH 14/27] Prevent crash when split panel has zero width or height --- Source/Engine/UI/GUI/Panels/SplitPanel.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Engine/UI/GUI/Panels/SplitPanel.cs b/Source/Engine/UI/GUI/Panels/SplitPanel.cs index 060459723..3daf18941 100644 --- a/Source/Engine/UI/GUI/Panels/SplitPanel.cs +++ b/Source/Engine/UI/GUI/Panels/SplitPanel.cs @@ -162,7 +162,10 @@ namespace FlaxEngine.GUI if (_splitterClicked) { - SplitterValue = _orientation == Orientation.Horizontal ? location.X / Width : location.Y / Height; + if (_orientation == Orientation.Horizontal && Width > 0) + SplitterValue = location.X / Width; + else if (_orientation == Orientation.Vertical && Height > 0) + SplitterValue = location.Y / Height; Cursor = _orientation == Orientation.Horizontal ? CursorType.SizeWE : CursorType.SizeNS; _cursorChanged = true; } From 9c1817330d27c3b59b5baed1aa178b94c4f35b3b Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 27 Jul 2026 20:49:41 -0500 Subject: [PATCH 15/27] Fix text box highlighting not using DPI --- Source/Engine/UI/GUI/Common/TextBox.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/Engine/UI/GUI/Common/TextBox.cs b/Source/Engine/UI/GUI/Common/TextBox.cs index 1346a8c2b..c077597aa 100644 --- a/Source/Engine/UI/GUI/Common/TextBox.cs +++ b/Source/Engine/UI/GUI/Common/TextBox.cs @@ -66,7 +66,7 @@ namespace FlaxEngine.GUI } /// - /// The vertical alignment of the text. + /// The horizontal alignment of the text. /// [EditorDisplay("Text Style"), EditorOrder(2024), Tooltip("The horizontal alignment of the text.")] public TextAlignment HorizontalAlignment @@ -268,7 +268,7 @@ namespace FlaxEngine.GUI if (selectedLinesCount == 1) { // Selected is part of single line - Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, rightEdge.X - leftEdge.X, fontHeight); + Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, rightEdge.X - leftEdge.X, textHeight); Render2D.FillRectangle(r1, selectionColor); } else @@ -276,17 +276,17 @@ namespace FlaxEngine.GUI float leftMargin = _layout.Bounds.Location.X; // Selected is more than one line - Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, 1000000000, fontHeight); + Rectangle r1 = new Rectangle(leftEdge.X, leftEdge.Y, 1000000000, textHeight); Render2D.FillRectangle(r1, selectionColor); // for (int i = 3; i <= selectedLinesCount; i++) { leftEdge.Y += textHeight; - Rectangle r = new Rectangle(leftMargin, leftEdge.Y, 1000000000, fontHeight); + Rectangle r = new Rectangle(leftMargin, leftEdge.Y, 1000000000, textHeight); Render2D.FillRectangle(r, selectionColor); } // - Rectangle r2 = new Rectangle(leftMargin, rightEdge.Y, rightEdge.X - leftMargin, fontHeight); + Rectangle r2 = new Rectangle(leftMargin, rightEdge.Y, rightEdge.X - leftMargin, textHeight); Render2D.FillRectangle(r2, selectionColor); } } From d2057d9c4f7b6c64ae3e1af9b063dc8ef4b63f3b Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 27 Jul 2026 21:02:16 -0500 Subject: [PATCH 16/27] Fix caret height. DPI is handled in the GetCharPosition method of the text boxes. --- Source/Engine/UI/GUI/Common/TextBoxBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/UI/GUI/Common/TextBoxBase.cs b/Source/Engine/UI/GUI/Common/TextBoxBase.cs index 415a3c9c8..d3dbcc57a 100644 --- a/Source/Engine/UI/GUI/Common/TextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/TextBoxBase.cs @@ -563,7 +563,7 @@ namespace FlaxEngine.GUI caretPos.X - (caretWidth * 0.5f), caretPos.Y, caretWidth, - height * DpiScale); + height); } } From 2eb36590dcfecdf8a18894395964152a0c0e9c2e Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 27 Jul 2026 21:02:38 -0500 Subject: [PATCH 17/27] Fix highlight height and underline location to be dpi aware. --- Source/Engine/UI/GUI/Common/RichTextBoxBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs index ff94a73f0..97e1d88a5 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs @@ -378,7 +378,7 @@ namespace FlaxEngine.GUI { var leftEdge = selection.StartIndex <= textBlock.Range.StartIndex ? textBlock.Bounds.UpperLeft : GetCharPosition(selection.StartIndex, out _); var rightEdge = selection.EndIndex >= textBlock.Range.EndIndex ? textBlock.Bounds.UpperRight : GetCharPosition(selection.EndIndex, out _); - float height = font.Height; + float height = font.Height / DpiScale; #if PLATFORM_MAC && !PLATFORM_SDL height /= (float)Platform.Dpi / 96.0f; // TODO: refactor DPI support on macOS to skip such hacks #endif @@ -431,7 +431,7 @@ namespace FlaxEngine.GUI if (textBlock.Style.UnderlineBrush != null) { var underLineHeight = 2.0f; - var height = font.Height; + var height = font.Height / DpiScale; var underlineRect = new Rectangle(textBlock.Bounds.Location.X, textBlock.Bounds.Location.Y + height - underLineHeight * 0.5f, textBlock.Bounds.Width, underLineHeight); textBlock.Style.UnderlineBrush.Draw(underlineRect, textBlock.Style.Color); } From f2af741ede00776f3edef1deca836866daef81af Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 27 Jul 2026 21:36:22 -0500 Subject: [PATCH 18/27] Fix missing treeview folder shortcuts. --- Source/Editor/Content/Tree/TreeViewPanel.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Content/Tree/TreeViewPanel.cs b/Source/Editor/Content/Tree/TreeViewPanel.cs index 07af1e2d1..386ee8ae8 100644 --- a/Source/Editor/Content/Tree/TreeViewPanel.cs +++ b/Source/Editor/Content/Tree/TreeViewPanel.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FlaxEditor.GUI.Tree; using FlaxEditor.Options; using FlaxEngine; @@ -54,6 +54,10 @@ public class TreeViewPanel : Panel { Editor.Instance.Windows.ContentWin.Rename(contentNode.Item); } + else if (node is ContentFolderTreeNode folderNode) + { + Editor.Instance.Windows.ContentWin.Rename(folderNode.Folder); + } } } @@ -102,6 +106,10 @@ public class TreeViewPanel : Panel { items.Add(contentNode.Item); } + else if (node is ContentFolderTreeNode folderNode) + { + items.Add(folderNode.Folder); + } } Editor.Instance.Windows.ContentWin.Duplicate(items); @@ -121,8 +129,12 @@ public class TreeViewPanel : Panel return; var filePaths = new List(); foreach (var node in selection) + { if (node is ContentItemTreeNode contentNode) filePaths.Add(contentNode.Item.Path); + else if (node is ContentFolderTreeNode folderNode) + filePaths.Add(folderNode.Folder.Path); + } Clipboard.Files = filePaths.ToArray(); UpdateContentItemCut(false); @@ -168,6 +180,8 @@ public class TreeViewPanel : Panel { if (node is ContentItemTreeNode contentNode) _cutItems.Add(contentNode.Item); + else if (node is ContentFolderTreeNode folderNode) + _cutItems.Add(folderNode.Folder); } } From 8eb571ad515e004b18bc3c3957046cdd6a829afa Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Thu, 30 Jul 2026 22:31:28 -0500 Subject: [PATCH 19/27] Allow more than 1 skinned mesh imported LOD --- Source/Engine/Tools/ModelTool/ModelTool.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 5fe834ad0..e06e1aa03 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -1318,10 +1318,10 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option } break; case ModelType::SkinnedModel: - if (data.LODs.Count() > 1) + if (data.LODs.IsEmpty() || data.LODs[0].Meshes.IsEmpty()) { - LOG(Warning, "Imported skinned model has more than one LOD. Removing the lower LODs. Only single one is supported."); - data.LODs.Resize(1); + errorMsg = TEXT("Imported skinned model has no valid geometry."); + return true; } break; case ModelType::Animation: From d1b475e69061bc6ef6fd39ed7f1bf18deeb81b5d Mon Sep 17 00:00:00 2001 From: Daniel Macura Date: Mon, 3 Aug 2026 19:49:39 +0200 Subject: [PATCH 20/27] Fix terrain resolution in editor. --- Source/Editor/CustomEditors/Dedicated/TerrainEditor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/CustomEditors/Dedicated/TerrainEditor.cs b/Source/Editor/CustomEditors/Dedicated/TerrainEditor.cs index 7bb9e9f47..448c55443 100644 --- a/Source/Editor/CustomEditors/Dedicated/TerrainEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/TerrainEditor.cs @@ -29,8 +29,8 @@ namespace FlaxEditor.CustomEditors.Dedicated patchesCount, patchesCount * 16, chunkSize, - 1.0f / (resolution.X + 1e-9f), - 1.0f / (resolution.Z + 1e-9f), + Mathf.Abs(resolution.X), + Mathf.Abs(resolution.Z), totalSize.X / Units.Meters2Units * 0.001f, totalSize.Z / Units.Meters2Units * 0.001f ); From f52949d1dff9789450703e9176baddcc7ff9e37a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 4 Aug 2026 13:17:00 +0200 Subject: [PATCH 21/27] Fix shader error when using non-ASCII characters in material --- Source/Engine/Platform/Base/StringUtilsBase.cpp | 14 ++++++++++++++ Source/Engine/Platform/StringUtils.h | 3 +++ .../MaterialGenerator.Material.cpp | 4 +++- .../Tools/MaterialGenerator/MaterialGenerator.cpp | 3 ++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Platform/Base/StringUtilsBase.cpp b/Source/Engine/Platform/Base/StringUtilsBase.cpp index 96f0fd2dc..750758b33 100644 --- a/Source/Engine/Platform/Base/StringUtilsBase.cpp +++ b/Source/Engine/Platform/Base/StringUtilsBase.cpp @@ -88,6 +88,20 @@ const char* StringUtils::FindIgnoreCase(const char* str, const char* toFind) return nullptr; } +void StringUtils::ConvertUTF162ASCII(const Char* from, char* to, int32 len) +{ + if (!from || !to) + return; + for (int32 i = 0; i < len; i++) + { + Char c = from[i]; + if ((c == 0x09 || c == 0x0A || c == 0x0D || (0x20 <= c && c <= 0x7F))) + to[i] = (char)c; + else + to[i] = ' '; + } +} + void PrintUTF8Error(const char* from, uint32 fromLength) { LOG(Error, "Not a UTF-8 string. Length: {0}", fromLength); diff --git a/Source/Engine/Platform/StringUtils.h b/Source/Engine/Platform/StringUtils.h index faa8127f8..dad84bd64 100644 --- a/Source/Engine/Platform/StringUtils.h +++ b/Source/Engine/Platform/StringUtils.h @@ -150,6 +150,9 @@ public: // Converts characters from UTF-16 to ANSI static void ConvertUTF162ANSI(const Char* from, char* to, int32 len); + // Converts characters from UTF-16 to ASCII + static void ConvertUTF162ASCII(const Char* from, char* to, int32 len); + // Convert characters from UTF-8 to UTF-16 static void ConvertUTF82UTF16(const char* from, Char* to, int32 fromLength, int32& toLength); diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index 73e4ee31a..481cf2df7 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp @@ -140,7 +140,9 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) // Write code _writer.Write(TEXT("{\n")); - _writer.Write(*code); + _writer.Write(code); + if (!code.EndsWith(TEXT("\n")) && !code.EndsWith(TEXT(" "))) + _writer.Write(TEXT("\n")); _writer.Write(TEXT("}\n")); // Link output values to boxes diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp index 707c24416..685aa1040 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp @@ -627,8 +627,9 @@ bool MaterialGenerator::Generate(WriteStream& source, MaterialInfo& materialInfo if (in.Length() > 0) { tmp.EnsureCapacity(in.Length() + 1, false); - StringUtils::ConvertUTF162ANSI(*in, tmp.Get(), in.Length()); + StringUtils::ConvertUTF162ASCII(*in, tmp.Get(), in.Length()); source.WriteBytes(tmp.Get(), in.Length()); + tmp.Clear(); } } } From f7526d67623fbb0f696ce7f7ae32ea86d6300e08 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 4 Aug 2026 13:30:56 +0200 Subject: [PATCH 22/27] Fix error when reimporting model into different type when using LOD info --- Source/Editor/Viewport/Previews/ModelPreview.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Viewport/Previews/ModelPreview.cs b/Source/Editor/Viewport/Previews/ModelPreview.cs index aff9ebb4c..d8aad8c59 100644 --- a/Source/Editor/Viewport/Previews/ModelPreview.cs +++ b/Source/Editor/Viewport/Previews/ModelPreview.cs @@ -431,7 +431,7 @@ namespace FlaxEditor.Viewport.Previews { base.Draw(); - if (_showCurrentLOD) + if (_showCurrentLOD && Model) { var asset = Model; var lodIndex = ComputeLODIndex(asset, out var screenSize); From bae31b0d50c032f0e9e0da57dadbee345054502e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 4 Aug 2026 19:05:08 +0200 Subject: [PATCH 23/27] Fix #4099 --- Source/Engine/UI/GUI/Common/Slider.cs | 124 ++++++++++++++++---------- 1 file changed, 79 insertions(+), 45 deletions(-) diff --git a/Source/Engine/UI/GUI/Common/Slider.cs b/Source/Engine/UI/GUI/Common/Slider.cs index 83a5c38e1..7bf70f253 100644 --- a/Source/Engine/UI/GUI/Common/Slider.cs +++ b/Source/Engine/UI/GUI/Common/Slider.cs @@ -102,6 +102,7 @@ public class Slider : ContainerControl private Float2 _thumbSize = new Float2(16, 16); private bool _isSliding; private bool _mouseOverThumb; + private const float _step = 10; /// /// Gets or sets the value (normalized to range 0-100). @@ -341,6 +342,13 @@ public class Slider : ContainerControl } } + private void StartSliding() + { + _isSliding = true; + StartMouseCapture(); + SlidingStart?.Invoke(); + } + private void EndSliding() { _isSliding = false; @@ -412,20 +420,19 @@ public class Slider : ContainerControl /// public override Control OnNavigate(NavDirection direction, Float2 location, Control caller, List visited) { - bool _isHorizontal = Direction is SliderDirection.HorizontalRight or SliderDirection.HorizontalLeft; - - float _keyOrGamepadPosition = _isHorizontal ? location.X : location.Y; + // Auto-focus self when navigation comes in + if (!IsNavFocused) + return this; - if (_thumbRect.Contains(ref location)) + // Control slider via navigation actions + if (IsNavFocused && _isSliding) { - _isSliding = true; - SlidingStart?.Invoke(); + var isNavUp = direction == NavDirection.Right || direction == NavDirection.Up; + var isDirUp = _direction == SliderDirection.HorizontalRight || _direction == SliderDirection.VerticalUp; + Value += (isNavUp == isDirUp ? 1 : -1) * _step; return this; } - var SliderPosition = (Direction == SliderDirection.HorizontalRight || Direction == SliderDirection.VerticalDown) ? _keyOrGamepadPosition : - _keyOrGamepadPosition; - Value += (SliderPosition < _thumbCenter ? -1 : 1) * 10; - return base.OnNavigate(direction, location, caller, visited); } @@ -441,39 +448,44 @@ public class Slider : ContainerControl return base.OnKeyDown(key); } + private void OnClick(Float2 location) + { + Focus(); + float mousePosition = Direction is SliderDirection.HorizontalRight or SliderDirection.HorizontalLeft ? location.X : location.Y; + + if (_thumbRect.Contains(ref location)) + { + StartSliding(); + } + else + { + // Click change + switch (Direction) + { + case SliderDirection.HorizontalRight or SliderDirection.VerticalDown: + Value += (mousePosition < _thumbCenter ? -1 : 1) * _step; + break; + case SliderDirection.HorizontalLeft or SliderDirection.VerticalUp: + Value -= (mousePosition < _thumbCenter ? -1 : 1) * _step; + break; + default: break; + } + } + } + /// public override bool OnMouseDown(Float2 location, MouseButton button) { + if (base.OnMouseDown(location, button)) + return true; + if (button == MouseButton.Left) { - Focus(); - float mousePosition = Direction is SliderDirection.HorizontalRight or SliderDirection.HorizontalLeft ? location.X : location.Y; - - if (_thumbRect.Contains(ref location)) - { - // Start sliding - _isSliding = true; - StartMouseCapture(); - SlidingStart?.Invoke(); - return true; - } - else - { - // Click change - switch (Direction) - { - case SliderDirection.HorizontalRight or SliderDirection.VerticalDown: - Value += (mousePosition < _thumbCenter ? -1 : 1) * 10; - break; - case SliderDirection.HorizontalLeft or SliderDirection.VerticalUp: - Value -= (mousePosition < _thumbCenter ? -1 : 1) * 10; - break; - default: break; - } - } + OnClick(location); + return true; } - return base.OnMouseDown(location, button); + return false; } /// @@ -482,13 +494,8 @@ public class Slider : ContainerControl if (base.OnTouchDown(location, pointerId)) return true; - if (!new Rectangle(Float2.Zero, Size).Contains(ref location)) - { - Defocus(); - return true; - } - - return false; + OnClick(location); + return true; } /// @@ -549,13 +556,40 @@ public class Slider : ContainerControl /// public override bool OnTouchUp(Float2 location, int pointerId) { - if (base.OnTouchUp(location, pointerId) && _isSliding) + if (base.OnTouchUp(location, pointerId)) + return true; + + if (_isSliding) { EndSliding(); - return true; } + return true; + } - return false; + /// + public override void OnTouchMove(Float2 location, int pointerId) + { + base.OnTouchMove(location, pointerId); + + if (_isSliding) + { + OnMouseMove(location); + } + } + + /// + public override void OnSubmit() + { + base.OnSubmit(); + + if (_isSliding) + { + EndSliding(); + } + else + { + StartSliding(); + } } /// From ef7aefd7b8fd80055bc1e4c91079c406b848953c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 4 Aug 2026 19:19:34 +0200 Subject: [PATCH 24/27] Refactor window tooltip to use string builder #4135 --- Source/Editor/Modules/UIModule.cs | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 62f514040..1e6fec14c 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -817,27 +817,40 @@ namespace FlaxEditor.Modules private void InitWindowDecorations(RootControl mainWindow) { ScriptsBuilder.GetBinariesConfiguration(out _, out _, out _, out var configuration); - string driver = string.Empty; -#if PLATFORM_LINUX - driver = LinuxPlatform.DisplayServer; - if (!string.IsNullOrEmpty(driver)) - driver = $" ({driver})"; -#endif + var tooltip = new System.Text.StringBuilder(); + + // Project info + tooltip.AppendLine(Editor.GameProject.Name); var projectPath = Globals.ProjectFolder; #if PLATFORM_WINDOWS projectPath = projectPath.Replace('/', '\\'); #endif + tooltip.AppendLine(projectPath); - string largeWorld = ""; + // Engine info + tooltip.Append("Engine Version: ").AppendLine(Globals.EngineVersion); + var engineNickname = Editor.EngineProject.EngineNickname; + if (!string.IsNullOrEmpty(engineNickname)) + tooltip.Append($" ({engineNickname})"); + + // Build info #if USE_LARGE_WORLDS - largeWorld = "\nLarge Worlds Enabled"; + tooltip.AppendLine("Large Worlds Enabled"); #endif + tooltip.Append("Configuration: ").AppendLine(configuration); + tooltip.Append("Graphics: ").Append(GPUDevice.Instance.RendererType); +#if PLATFORM_LINUX + var driver = LinuxPlatform.DisplayServer; + if (!string.IsNullOrEmpty(driver)) + tooltip.Append($" ({driver})"); +#endif + tooltip.AppendLine(); WindowDecorations = new MainWindowDecorations(mainWindow, !Utilities.Utils.UseCustomWindowDecorations(true)) { Parent = mainWindow, - IconTooltipText = $"{mainWindow.RootWindow.Title}\nPath {projectPath}\n\nEngine Version {Globals.EngineVersion}{largeWorld}\nConfiguration {configuration}\n\nGraphics {GPUDevice.Instance.RendererType}{driver}", + IconTooltipText = tooltip.ToString(), }; } From 222daa99daf2c2f3ebd2fbaaed42fcd6fe4a7381 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 4 Aug 2026 19:39:25 +0200 Subject: [PATCH 25/27] Move CLion docs to FlaxDocs https://github.com/FlaxEngine/FlaxDocs/commit/6e89a8f1d9b033fedecbef8c560dd5107bdcbd53 --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index ccb75a475..b2786f4d6 100644 --- a/README.md +++ b/README.md @@ -80,16 +80,6 @@ Follow the instructions below to compile and run the engine from source. * Open workspace with XCode or Visual Studio Code * Build and run (configuration `Editor.Mac.Development`) -## CLion - -CLion support is provided through generated CMake facade project files for native C++ development on desktop host platforms: Windows, Linux, and Mac. Generate them by passing `-clion` to the project generation script, for example `GenerateProjectFiles.bat -clion`, `./GenerateProjectFiles.sh -clion`, or `GenerateProjectFiles.command -clion`. - -The generated CMake project is written to `Cache/Projects/CMake/`. Open that directory in CLion as a CMake project. Flax does not generate `.idea` files; CLion owns and updates its own local IDE settings after the project is opened. - -This CMake project is an IDE facade, not the primary Flax build system. CMake is used to describe the code model and expose build presets, while actual native build steps are delegated to `Flax.Build`. Run configurations launch the generated engine executable after building it. - -CLion project generation intentionally excludes platforms that are not supported by this workflow, including Android, iOS, UWP, Web, GDK/Xbox, PlayStation, and Switch. The generated CLion project does not configure C# debugging; use an IDE or editor with .NET debugging support for managed code. - ## Troubleshooting * `Could not execute because the specified command or file was not found.` From 0fc7bb723cf2d947e2f57b9a2b2617c046224fb3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 4 Aug 2026 20:06:22 +0200 Subject: [PATCH 26/27] Remove not needed newline --- Source/Editor/Modules/UIModule.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 1e6fec14c..f94b87ec3 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -845,7 +845,6 @@ namespace FlaxEditor.Modules if (!string.IsNullOrEmpty(driver)) tooltip.Append($" ({driver})"); #endif - tooltip.AppendLine(); WindowDecorations = new MainWindowDecorations(mainWindow, !Utilities.Utils.UseCustomWindowDecorations(true)) { From 470b12bfd36f0a7600e9cc31a99d99fde868cd26 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 4 Aug 2026 20:11:42 +0200 Subject: [PATCH 27/27] Post merge --- Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp index 005d935ae..b259ed688 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp @@ -145,8 +145,6 @@ GPUTextureView* GPUSwapChainVulkan::GetBackBufferView() } ASSERT(_acquiredImageIndex != -1); - auto context = _device->MainContext; - // Wait for prior GPU work that used this acquired image before recording // commands against it again. Waiting before acquire can target a different image // and unnecessarily serialize frames when the swapchain has multiple images.