diff --git a/README.md b/README.md index 0312fbf3c..70d383f93 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ 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 +## Troubleshooting * `Could not execute because the specified command or file was not found.` 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); } } 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/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 ); 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/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 5edba3fb9..ce81925e3 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -811,17 +811,39 @@ namespace FlaxEditor.Modules private void InitWindowDecorations(RootControl mainWindow) { ScriptsBuilder.GetBinariesConfiguration(out _, out _, out _, out var configuration); - string driver = string.Empty; + + 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); + + // 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 + tooltip.AppendLine("Large Worlds Enabled"); +#endif + tooltip.Append("Configuration: ").AppendLine(configuration); + tooltip.Append("Graphics: ").Append(GPUDevice.Instance.RendererType); #if PLATFORM_LINUX - driver = LinuxPlatform.DisplayServer; + var driver = LinuxPlatform.DisplayServer; if (!string.IsNullOrEmpty(driver)) - driver = $" ({driver})"; + tooltip.Append($" ({driver})"); #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 = tooltip.ToString(), }; } diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index dd522534d..b4f8089e2 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; } } 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/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); 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/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp index 7ddfb73e1..2a945c2b0 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp @@ -27,22 +27,30 @@ 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; } GPUSwapChainVulkan::GPUSwapChainVulkan(GPUDeviceVulkan* device, Window* window) : GPUResourceVulkan(device, StringView::Empty) , _surface(VK_NULL_HANDLE) + , _surfaceWindowHandle(nullptr) , _swapChain(VK_NULL_HANDLE) , _currentImageIndex(-1) , _semaphoreIndex(0) @@ -61,16 +69,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; @@ -80,13 +89,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() @@ -116,24 +135,34 @@ 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; + // 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 = &_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); @@ -145,17 +174,6 @@ 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; - } - } - // Rebuild swapchain if need to if (_vsyncPending != _vsyncCurrent) { @@ -171,8 +189,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); } @@ -204,7 +223,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(); @@ -213,14 +232,20 @@ 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); } if (_vsyncInit) @@ -231,15 +256,19 @@ bool GPUSwapChainVulkan::CreateSwapChain(int32 width, int32 height) _vsyncPending &= _window == Engine::MainWindow; // Don't use VSync on new context menus or tooltips in Editor _vsyncInit = false; } - 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; @@ -542,8 +571,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(); @@ -617,6 +646,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); @@ -630,7 +660,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 34972edd4..5ee655d41 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: @@ -61,6 +67,7 @@ class GPUSwapChainVulkan : public GPUResourceVulkan, public Resour private: VkSurfaceKHR _surface; + void* _surfaceWindowHandle; VkSwapchainKHR _swapChain; int32 _currentImageIndex; int32 _semaphoreIndex; @@ -106,7 +113,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] diff --git a/Source/Engine/Platform/Android/AndroidPlatform.cpp b/Source/Engine/Platform/Android/AndroidPlatform.cpp index 03040a864..e2adcad53 100644 --- a/Source/Engine/Platform/Android/AndroidPlatform.cpp +++ b/Source/Engine/Platform/Android/AndroidPlatform.cpp @@ -956,7 +956,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/Base/StringUtilsBase.cpp b/Source/Engine/Platform/Base/StringUtilsBase.cpp index 350d43272..ab7f96d8e 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/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/Engine/Platform/StringUtils.h b/Source/Engine/Platform/StringUtils.h index be877fc6a..03ac298e2 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/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/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index c26789514..80f53924a 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 f23ee3e43..22a07f873 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(); } } } diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 192db62c2..5103b26f8 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -1309,10 +1309,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: 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); } diff --git a/Source/Engine/UI/GUI/Common/Slider.cs b/Source/Engine/UI/GUI/Common/Slider.cs index 8c7b022fe..7bf70f253 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; @@ -101,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). @@ -340,6 +342,13 @@ public class Slider : ContainerControl } } + private void StartSliding() + { + _isSliding = true; + StartMouseCapture(); + SlidingStart?.Invoke(); + } + private void EndSliding() { _isSliding = false; @@ -409,38 +418,84 @@ public class Slider : ContainerControl } /// - public override bool OnMouseDown(Float2 location, MouseButton button) + public override Control OnNavigate(NavDirection direction, Float2 location, Control caller, List visited) { - if (button == MouseButton.Left) - { - Focus(); - float mousePosition = Direction is SliderDirection.HorizontalRight or SliderDirection.HorizontalLeft ? location.X : location.Y; + // Auto-focus self when navigation comes in + if (!IsNavFocused) + return this; - 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; - } - } + // Control slider via navigation actions + if (IsNavFocused && _isSliding) + { + var isNavUp = direction == NavDirection.Right || direction == NavDirection.Up; + var isDirUp = _direction == SliderDirection.HorizontalRight || _direction == SliderDirection.VerticalUp; + Value += (isNavUp == isDirUp ? 1 : -1) * _step; + return this; } - return base.OnMouseDown(location, button); + return base.OnNavigate(direction, location, caller, visited); + } + + /// + public override bool OnKeyDown(KeyboardKeys key) + { + if (key == KeyboardKeys.Escape) + { + Defocus(); + return true; + } + + 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) + { + OnClick(location); + return true; + } + + return false; + } + + /// + public override bool OnTouchDown(Float2 location, int pointerId) + { + if (base.OnTouchDown(location, pointerId)) + return true; + + OnClick(location); + return true; } /// @@ -474,6 +529,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) { @@ -486,6 +553,45 @@ public class Slider : ContainerControl return base.OnMouseUp(location, button); } + /// + public override bool OnTouchUp(Float2 location, int pointerId) + { + if (base.OnTouchUp(location, pointerId)) + return true; + + if (_isSliding) + { + EndSliding(); + } + return true; + } + + /// + 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(); + } + } + /// public override void OnEndMouseCapture() { 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); } } 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); } } 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; } 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 diff --git a/Source/Tools/Flax.Build/Build/Builder.Projects.cs b/Source/Tools/Flax.Build/Build/Builder.Projects.cs index a0e043f64..fafa8e419 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.VisualStudio2026); if (!string.IsNullOrEmpty(Configuration.ProjectFormatCustom)) diff --git a/Source/Tools/Flax.Build/Configuration.cs b/Source/Tools/Flax.Build/Configuration.cs index 590a141e7..f63d1b1db 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/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('\\', '/')); } /// 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))