diff --git a/.gitignore b/.gitignore index 54907892f..b7e11e554 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ Source/*.csproj /Package_*/ !Source/Engine/Debug /Source/Platforms/Editor/Linux/Mono/etc/mono/registry +PackageEditor_Cert.command PackageEditor_Cert.bat PackagePlatforms_Cert.bat diff --git a/Content/Editor/Particles/Smoke.flax b/Content/Editor/Particles/Smoke.flax index 1335a84f4..b42c2f325 100644 --- a/Content/Editor/Particles/Smoke.flax +++ b/Content/Editor/Particles/Smoke.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3dc51e7805056006ca6cbb481ba202583a9b2287c152fc04e28e1d07747d6ce -size 14706 +oid sha256:334ac0d00495fc88b10839061ff0c3f45323d4f75ab6176b19005199a7324a19 +size 14569 diff --git a/Content/Editor/Particles/Sparks.flax b/Content/Editor/Particles/Sparks.flax index 7977e231b..7ee0ed6e9 100644 --- a/Content/Editor/Particles/Sparks.flax +++ b/Content/Editor/Particles/Sparks.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77d902ab5f79426cc66dc5f19a3b8280136a58aa3c6fd317554d1a032357c65a -size 15275 +oid sha256:87046a9bfe275cac290b4764de8a512c222ccc386d01af9026d57c3e4b7773b6 +size 13625 diff --git a/Content/Editor/Scripting/ScriptTemplate.cs b/Content/Editor/Scripting/ScriptTemplate.cs index 663acf05f..30fbe9d86 100644 --- a/Content/Editor/Scripting/ScriptTemplate.cs +++ b/Content/Editor/Scripting/ScriptTemplate.cs @@ -33,4 +33,3 @@ public class %class% : Script // Here you can add code that needs to be called every frame } } - diff --git a/Content/Shaders/GlobalSignDistanceField.flax b/Content/Shaders/GlobalSignDistanceField.flax index 18f653f86..0e94c5c06 100644 --- a/Content/Shaders/GlobalSignDistanceField.flax +++ b/Content/Shaders/GlobalSignDistanceField.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c8aa181a814d69b15ffec6493a71a6f42ae816ce04f7803cff2d5073b4b3c4f -size 11790 +oid sha256:e075583620e62407503c73f52487c204ddcad421e80fa621aebd55af1cfb08d5 +size 11798 diff --git a/Flax.flaxproj b/Flax.flaxproj index ebcdc2830..a3157a032 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -3,7 +3,8 @@ "Version": { "Major": 1, "Minor": 7, - "Build": 6401 + "Revision": 0, + "Build": 6404 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.", diff --git a/GenerateProjectFiles.bat b/GenerateProjectFiles.bat index 622939c34..28970a203 100644 --- a/GenerateProjectFiles.bat +++ b/GenerateProjectFiles.bat @@ -15,7 +15,7 @@ if errorlevel 1 goto BuildToolFailed :: Build bindings for all editor configurations echo Building C# bindings... -Binaries\Tools\Flax.Build.exe -build -BuildBindingsOnly -arch=x64 -platform=Windows --buildTargets=FlaxEditor,FlaxGame +Binaries\Tools\Flax.Build.exe -build -BuildBindingsOnly -arch=x64 -platform=Windows --buildTargets=FlaxEditor popd echo Done! diff --git a/GenerateProjectFiles.command b/GenerateProjectFiles.command index 5ee5c0783..a42121252 100755 --- a/GenerateProjectFiles.command +++ b/GenerateProjectFiles.command @@ -14,4 +14,4 @@ bash ./Development/Scripts/Mac/CallBuildTool.sh --genproject "$@" # Build bindings for all editor configurations echo Building C# bindings... # TODO: Detect the correct architecture here -Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=ARM64 -platform=Mac --buildTargets=FlaxEditor,FlaxGame +Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=ARM64 -platform=Mac --buildTargets=FlaxEditor diff --git a/GenerateProjectFiles.sh b/GenerateProjectFiles.sh index dceb8abe8..76d96c7ef 100755 --- a/GenerateProjectFiles.sh +++ b/GenerateProjectFiles.sh @@ -14,4 +14,4 @@ bash ./Development/Scripts/Linux/CallBuildTool.sh --genproject "$@" # Build bindings for all editor configurations echo Building C# bindings... # TODO: Detect the correct architecture here -Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=x64 -platform=Linux --buildTargets=FlaxEditor,FlaxGame +Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=x64 -platform=Linux --buildTargets=FlaxEditor diff --git a/README.md b/README.md index fac631a6a..d6688bd03 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Flax Engine is a high quality modern 3D game engine written in C++ and C#. -From stunning graphics to powerful scripts - Flax can give everything for your games. Designed for fast workflow with many ready to use features waiting for you right now. To learn more see the website ([www.flaxengine.com](https://flaxengine.com)). +From stunning graphics to powerful scripts, it's designed for fast workflow with many ready-to-use features waiting for you right now. To learn more see the website ([www.flaxengine.com](https://flaxengine.com)). This repository contains full source code of the Flax Engine (excluding NDA-protected platforms support). Anyone is welcome to contribute or use the modified source in Flax-based games. diff --git a/Source/Editor/Content/AssetPickerValidator.cs b/Source/Editor/Content/AssetPickerValidator.cs new file mode 100644 index 000000000..f43ab6a29 --- /dev/null +++ b/Source/Editor/Content/AssetPickerValidator.cs @@ -0,0 +1,292 @@ +using System; +using System.IO; +using FlaxEditor.Scripting; +using FlaxEngine; +using FlaxEngine.Utilities; + +namespace FlaxEditor.Content; + +/// +/// Manages and converts the selected content item to the appropriate types. Useful for drag operations. +/// +public class AssetPickerValidator : IContentItemOwner +{ + private Asset _selected; + private ContentItem _selectedItem; + private ScriptType _type; + private string _fileExtension; + + /// + /// Gets or sets the selected item. + /// + public ContentItem SelectedItem + { + get => _selectedItem; + set + { + if (_selectedItem == value) + return; + if (value == null) + { + if (_selected == null && _selectedItem is SceneItem) + { + // Deselect scene reference + _selectedItem.RemoveReference(this); + _selectedItem = null; + _selected = null; + OnSelectedItemChanged(); + return; + } + + // Deselect + _selectedItem?.RemoveReference(this); + _selectedItem = null; + _selected = null; + OnSelectedItemChanged(); + } + else if (value is SceneItem item) + { + if (_selectedItem == item) + return; + if (!IsValid(item)) + item = null; + + // Change value to scene reference (cannot load asset because scene can be already loaded - duplicated ID issue) + _selectedItem?.RemoveReference(this); + _selectedItem = item; + _selected = null; + _selectedItem?.AddReference(this); + OnSelectedItemChanged(); + } + else if (value is AssetItem assetItem) + { + SelectedAsset = FlaxEngine.Content.LoadAsync(assetItem.ID); + } + else + { + // Change value + _selectedItem?.RemoveReference(this); + _selectedItem = value; + _selected = null; + OnSelectedItemChanged(); + } + } + } + + /// + /// Gets or sets the selected asset identifier. + /// + public Guid SelectedID + { + get + { + if (_selected != null) + return _selected.ID; + if (_selectedItem is AssetItem assetItem) + return assetItem.ID; + return Guid.Empty; + } + set => SelectedItem = Editor.Instance.ContentDatabase.FindAsset(value); + } + + /// + /// Gets or sets the selected content item path. + /// + public string SelectedPath + { + get + { + string path = _selectedItem?.Path ?? _selected?.Path; + if (path != null) + { + // Convert into path relative to the project (cross-platform) + var projectFolder = Globals.ProjectFolder; + if (path.StartsWith(projectFolder)) + path = path.Substring(projectFolder.Length + 1); + } + return path; + } + set + { + if (string.IsNullOrEmpty(value)) + { + SelectedItem = null; + } + else + { + var path = StringUtils.IsRelative(value) ? Path.Combine(Globals.ProjectFolder, value) : value; + SelectedItem = Editor.Instance.ContentDatabase.Find(path); + } + } + } + + /// + /// Gets or sets the selected asset object. + /// + public Asset SelectedAsset + { + get => _selected; + set + { + // Check if value won't change + if (value == _selected) + return; + + // Find item from content database and check it + var item = value ? Editor.Instance.ContentDatabase.FindAsset(value.ID) : null; + if (item != null && !IsValid(item)) + item = null; + + // Change value + _selectedItem?.RemoveReference(this); + _selectedItem = item; + _selected = value; + _selectedItem?.AddReference(this); + OnSelectedItemChanged(); + } + } + + /// + /// Gets or sets the assets types that this picker accepts (it supports types derived from the given type). Use for generic file picker. + /// + public ScriptType AssetType + { + get => _type; + set + { + if (_type != value) + { + _type = value; + + // Auto deselect if the current value is invalid + if (_selectedItem != null && !IsValid(_selectedItem)) + SelectedItem = null; + } + } + } + + /// + /// Gets or sets the content items extensions filter. Null if unused. + /// + public string FileExtension + { + get => _fileExtension; + set + { + if (_fileExtension != value) + { + _fileExtension = value; + + // Auto deselect if the current value is invalid + if (_selectedItem != null && !IsValid(_selectedItem)) + SelectedItem = null; + } + } + } + + /// + /// Occurs when selected item gets changed. + /// + public event Action SelectedItemChanged; + + /// + /// The custom callback for assets validation. Cane be used to implement a rule for assets to pick. + /// + public Func CheckValid; + + /// + /// Returns whether item is valid. + /// + /// + /// + public bool IsValid(ContentItem item) + { + if (_fileExtension != null && !item.Path.EndsWith(_fileExtension)) + return false; + if (CheckValid != null && !CheckValid(item)) + return false; + if (_type == ScriptType.Null) + return true; + + if (item is AssetItem assetItem) + { + // Faster path for binary items (in-built) + if (assetItem is BinaryAssetItem binaryItem) + return _type.IsAssignableFrom(new ScriptType(binaryItem.Type)); + + // Type filter + var type = TypeUtils.GetType(assetItem.TypeName); + if (_type.IsAssignableFrom(type)) + return true; + + // Json assets can contain any type of the object defined by the C# type (data oriented design) + if (assetItem is JsonAssetItem && (_type.Type == typeof(JsonAsset) || _type.Type == typeof(Asset))) + return true; + + // Special case for scene asset references + if (_type.Type == typeof(SceneReference) && assetItem is SceneItem) + return true; + } + + return false; + } + + /// + /// Initializes a new instance of the class. + /// + public AssetPickerValidator() + : this(new ScriptType(typeof(Asset))) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The assets types that this picker accepts. + public AssetPickerValidator(ScriptType assetType) + { + _type = assetType; + } + + /// + /// Called when selected item gets changed. + /// + protected virtual void OnSelectedItemChanged() + { + SelectedItemChanged?.Invoke(); + } + + /// + public void OnItemDeleted(ContentItem item) + { + // Deselect item + SelectedItem = null; + } + + /// + public void OnItemRenamed(ContentItem item) + { + } + + /// + public void OnItemReimported(ContentItem item) + { + } + + /// + public void OnItemDispose(ContentItem item) + { + // Deselect item + SelectedItem = null; + } + + /// + /// Call to remove reference from the selected item. + /// + public void OnDestroy() + { + _selectedItem?.RemoveReference(this); + _selectedItem = null; + _selected = null; + } +} diff --git a/Source/Editor/Content/GUI/ContentView.cs b/Source/Editor/Content/GUI/ContentView.cs index e1a3acdf6..259be104b 100644 --- a/Source/Editor/Content/GUI/ContentView.cs +++ b/Source/Editor/Content/GUI/ContentView.cs @@ -220,8 +220,9 @@ namespace FlaxEditor.Content.GUI // Remove references and unlink items for (int i = 0; i < _items.Count; i++) { - _items[i].Parent = null; - _items[i].RemoveReference(this); + var item = _items[i]; + item.Parent = null; + item.RemoveReference(this); } _items.Clear(); @@ -263,11 +264,12 @@ namespace FlaxEditor.Content.GUI // Add references and link items for (int i = 0; i < items.Count; i++) { - if (items[i].Visible) + var item = items[i]; + if (item.Visible && !_items.Contains(item)) { - items[i].Parent = this; - items[i].AddReference(this); - _items.Add(items[i]); + item.Parent = this; + item.AddReference(this); + _items.Add(item); } } if (selection != null) @@ -279,6 +281,8 @@ namespace FlaxEditor.Content.GUI // Sort items depending on sortMethod parameter _children.Sort(((control, control1) => { + if (control == null || control1 == null) + return 0; if (sortType == SortType.AlphabeticReverse) { if (control.CompareTo(control1) > 0) diff --git a/Source/Editor/Content/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs index 6bbdc0a51..604caa704 100644 --- a/Source/Editor/Content/Items/ContentItem.cs +++ b/Source/Editor/Content/Items/ContentItem.cs @@ -323,8 +323,6 @@ namespace FlaxEditor.Content /// The new path. internal virtual void UpdatePath(string value) { - Assert.AreNotEqual(Path, value); - // Set path Path = StringUtils.NormalizePath(value); FileName = System.IO.Path.GetFileName(value); diff --git a/Source/Editor/Content/Proxy/SceneProxy.cs b/Source/Editor/Content/Proxy/SceneProxy.cs index 78d88b440..004c2aed7 100644 --- a/Source/Editor/Content/Proxy/SceneProxy.cs +++ b/Source/Editor/Content/Proxy/SceneProxy.cs @@ -30,6 +30,12 @@ namespace FlaxEditor.Content return item is SceneItem; } + /// + public override bool AcceptsAsset(string typeName, string path) + { + return (typeName == Scene.AssetTypename || typeName == Scene.EditorPickerTypename) && path.EndsWith(FileExtension, StringComparison.OrdinalIgnoreCase); + } + /// public override bool CanCreate(ContentFolder targetLocation) { diff --git a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs index 160c783ed..1282e4daa 100644 --- a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs +++ b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs @@ -406,18 +406,16 @@ namespace FlaxEditor.Content.Thumbnails for (int i = 0; i < maxChecks; i++) { var request = _requests[i]; - try { if (request.IsReady) - { return request; - } } catch (Exception ex) { - Editor.LogWarning(ex); Editor.LogWarning($"Failed to prepare thumbnail rendering for {request.Item.ShortName}."); + Editor.LogWarning(ex); + _requests.RemoveAt(i--); } } @@ -515,7 +513,6 @@ namespace FlaxEditor.Content.Thumbnails for (int i = 0; i < checks; i++) { var request = _requests[i]; - try { if (request.IsReady) @@ -529,8 +526,9 @@ namespace FlaxEditor.Content.Thumbnails } catch (Exception ex) { - Editor.LogWarning(ex); Editor.LogWarning($"Failed to prepare thumbnail rendering for {request.Item.ShortName}."); + Editor.LogWarning(ex); + _requests.RemoveAt(i--); } } diff --git a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp index ec7b8e7e1..42ef6fcdf 100644 --- a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp @@ -104,4 +104,19 @@ bool LinuxPlatformTools::OnDeployBinaries(CookingData& data) return false; } +void LinuxPlatformTools::OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) +{ + // Pick the first executable file + Array files; + FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly); + for (auto& file : files) + { + if (FileSystem::GetExtension(file).IsEmpty()) + { + executableFile = file; + break; + } + } +} + #endif diff --git a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h index 562b38962..432240d00 100644 --- a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h +++ b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h @@ -20,6 +20,7 @@ public: ArchitectureType GetArchitecture() const override; bool UseSystemDotnet() const override; bool OnDeployBinaries(CookingData& data) override; + void OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) override; }; #endif diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp index a1db61dbb..aa56a6b95 100644 --- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp @@ -249,4 +249,19 @@ bool MacPlatformTools::OnPostProcess(CookingData& data) return false; } +void MacPlatformTools::OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) +{ + // Pick the first executable file + Array files; + FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly); + for (auto& file : files) + { + if (FileSystem::GetExtension(file).IsEmpty()) + { + executableFile = file; + break; + } + } +} + #endif diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h index 21d9141e3..efdd0b733 100644 --- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h +++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h @@ -27,6 +27,7 @@ public: bool IsNativeCodeFile(CookingData& data, const String& file) override; void OnBuildStarted(CookingData& data) override; bool OnPostProcess(CookingData& data) override; + void OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) override; }; #endif diff --git a/Source/Editor/Cooker/Steps/DeployDataStep.cpp b/Source/Editor/Cooker/Steps/DeployDataStep.cpp index c9bbc016e..8306475d1 100644 --- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp +++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp @@ -131,7 +131,8 @@ bool DeployDataStep::Perform(CookingData& data) if (FileSystem::DirectoryExists(dstDotnet)) { String cachedData; - File::ReadAllText(dotnetCacheFilePath, cachedData); + if (FileSystem::FileExists(dotnetCacheFilePath)) + File::ReadAllText(dotnetCacheFilePath, cachedData); if (cachedData != dotnetCachedValue) { FileSystem::DeleteDirectory(dstDotnet); @@ -360,7 +361,7 @@ bool DeployDataStep::Perform(CookingData& data) data.AddRootEngineAsset(PRE_INTEGRATED_GF_ASSET_NAME); data.AddRootEngineAsset(SMAA_AREA_TEX); data.AddRootEngineAsset(SMAA_SEARCH_TEX); - if (data.Configuration != BuildConfiguration::Release) + if (!buildSettings.SkipDefaultFonts) data.AddRootEngineAsset(TEXT("Editor/Fonts/Roboto-Regular")); // Register custom assets (eg. plugins) diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs index 03b21e12b..32111e51c 100644 --- a/Source/Editor/CustomEditors/CustomEditor.cs +++ b/Source/Editor/CustomEditors/CustomEditor.cs @@ -157,6 +157,12 @@ namespace FlaxEditor.CustomEditors var values = _values; var presenter = _presenter; var layout = _layout; + if (layout.Editors.Count != 1) + { + // There are more editors using the same layout so rebuild parent editor to prevent removing others editors + _parent?.RebuildLayout(); + return; + } var control = layout.ContainerControl; var parent = _parent; var parentScrollV = (_presenter?.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1; diff --git a/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs index 71c5f6daf..4099e5aee 100644 --- a/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs @@ -225,8 +225,15 @@ namespace FlaxEditor.CustomEditors.Dedicated } _actor = actor; - var showActorPicker = actor == null || ParentEditor.Values.All(x => x is not Cloth); - if (showActorPicker) + if (ParentEditor.Values.Any(x => x is Cloth)) + { + // Cloth always picks the parent model mesh + if (actor == null) + { + layout.Label("Cloth needs to be added as a child to model actor."); + } + } + else { // Actor reference picker _actorPicker = layout.Custom(); @@ -242,7 +249,10 @@ namespace FlaxEditor.CustomEditors.Dedicated { var model = staticModel.Model; if (model == null || model.WaitForLoaded()) + { + layout.Label("No model."); return; + } var materials = model.MaterialSlots; var lods = model.LODs; meshNames = new string[lods.Length][]; @@ -267,7 +277,10 @@ namespace FlaxEditor.CustomEditors.Dedicated { var skinnedModel = animatedModel.SkinnedModel; if (skinnedModel == null || skinnedModel.WaitForLoaded()) + { + layout.Label("No model."); return; + } var materials = skinnedModel.MaterialSlots; var lods = skinnedModel.LODs; meshNames = new string[lods.Length][]; diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs index a6c4e6623..8fb742b5e 100644 --- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs @@ -1,6 +1,13 @@ -using FlaxEditor.CustomEditors.Editors; +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using FlaxEditor.Actions; +using FlaxEditor.CustomEditors.Editors; +using FlaxEditor.GUI; +using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; +using System.Collections.Generic; namespace FlaxEditor.CustomEditors.Dedicated; @@ -10,6 +17,10 @@ namespace FlaxEditor.CustomEditors.Dedicated; [CustomEditor(typeof(MissingScript)), DefaultEditor] public class MissingScriptEditor : GenericEditor { + private DropPanel _dropPanel; + private Button _replaceScriptButton; + private CheckBox _shouldReplaceAllCheckbox; + /// public override void Initialize(LayoutElementsContainer layout) { @@ -18,9 +29,137 @@ public class MissingScriptEditor : GenericEditor base.Initialize(layout); return; } + _dropPanel = dropPanel; + _dropPanel.HeaderTextColor = Color.OrangeRed; - dropPanel.HeaderTextColor = Color.OrangeRed; + var replaceScriptPanel = new Panel + { + Parent = _dropPanel, + Height = 64, + }; + + _replaceScriptButton = new Button + { + Text = "Replace Script", + TooltipText = "Replaces the missing script with a given script type", + AnchorPreset = AnchorPresets.TopCenter, + Width = 240, + Height = 24, + X = -120, + Y = 0, + Parent = replaceScriptPanel, + }; + _replaceScriptButton.Clicked += OnReplaceScriptButtonClicked; + + var replaceAllLabel = new Label + { + Text = "Replace all matching missing scripts", + TooltipText = "Whether or not to apply this script change to all scripts missing the same type.", + AnchorPreset = AnchorPresets.BottomCenter, + Y = -34, + Parent = replaceScriptPanel, + }; + replaceAllLabel.X -= FlaxEngine.GUI.Style.Current.FontSmall.MeasureText(replaceAllLabel.Text).X; + + _shouldReplaceAllCheckbox = new CheckBox + { + TooltipText = replaceAllLabel.TooltipText, + AnchorPreset = AnchorPresets.BottomCenter, + Y = -34, + Parent = replaceScriptPanel, + }; + + float centerDifference = (_shouldReplaceAllCheckbox.Right - replaceAllLabel.Left) / 2; + replaceAllLabel.X += centerDifference; + _shouldReplaceAllCheckbox.X += centerDifference; base.Initialize(layout); } + + private void FindActorsWithMatchingMissingScript(List missingScripts) + { + foreach (Actor actor in Level.GetActors(typeof(Actor))) + { + for (int scriptIndex = 0; scriptIndex < actor.ScriptsCount; scriptIndex++) + { + Script actorScript = actor.Scripts[scriptIndex]; + if (actorScript is not MissingScript missingActorScript) + continue; + + MissingScript currentMissing = Values[0] as MissingScript; + if (missingActorScript.MissingTypeName != currentMissing.MissingTypeName) + continue; + + missingScripts.Add(missingActorScript); + } + } + } + + private void RunReplacementMultiCast(List actions) + { + if (actions.Count == 0) + { + Editor.LogWarning("Failed to replace scripts!"); + return; + } + + var multiAction = new MultiUndoAction(actions); + multiAction.Do(); + var presenter = ParentEditor.Presenter; + if (presenter != null) + { + presenter.Undo.AddAction(multiAction); + presenter.Control.Focus(); + } + } + + private void ReplaceScript(ScriptType script, bool replaceAllInScene) + { + var actions = new List(4); + + var missingScripts = new List(); + if (!replaceAllInScene) + missingScripts.Add((MissingScript)Values[0]); + else + FindActorsWithMatchingMissingScript(missingScripts); + + foreach (var missingScript in missingScripts) + actions.Add(AddRemoveScript.Add(missingScript.Actor, script)); + RunReplacementMultiCast(actions); + + for (int actionIdx = 0; actionIdx < actions.Count; actionIdx++) + { + AddRemoveScript addRemoveScriptAction = (AddRemoveScript)actions[actionIdx]; + int orderInParent = addRemoveScriptAction.GetOrderInParent(); + + Script newScript = missingScripts[actionIdx].Actor.Scripts[orderInParent]; + missingScripts[actionIdx].ReferenceScript = newScript; + } + actions.Clear(); + + foreach (var missingScript in missingScripts) + actions.Add(AddRemoveScript.Remove(missingScript)); + RunReplacementMultiCast(actions); + } + + private void OnReplaceScriptButtonClicked() + { + var scripts = Editor.Instance.CodeEditing.Scripts.Get(); + if (scripts.Count == 0) + { + // No scripts + var cm1 = new ContextMenu(); + cm1.AddButton("No scripts in project"); + cm1.Show(_dropPanel, _replaceScriptButton.BottomLeft); + return; + } + + // Show context menu with list of scripts to add + var cm = new ItemsListContextMenu(180); + for (int i = 0; i < scripts.Count; i++) + cm.AddItem(new TypeSearchPopup.TypeItemView(scripts[i])); + cm.ItemClicked += item => ReplaceScript((ScriptType)item.Tag, _shouldReplaceAllCheckbox.Checked); + cm.SortItems(); + cm.Show(_dropPanel, _replaceScriptButton.BottomLeft - new Float2((cm.Width - _replaceScriptButton.Width) / 2, 0)); + } } diff --git a/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs b/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs index 5215e23a4..296560507 100644 --- a/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs @@ -695,7 +695,41 @@ namespace FlaxEditor.CustomEditors.Dedicated private void SetType(ref ScriptType controlType, UIControl uiControl) { string previousName = uiControl.Control?.GetType().Name ?? nameof(UIControl); - uiControl.Control = (Control)controlType.CreateInstance(); + + var oldControlType = (Control)uiControl.Control; + var newControlType = (Control)controlType.CreateInstance(); + + // copy old control data to new control + if (oldControlType != null) + { + newControlType.Visible = oldControlType.Visible; + newControlType.Enabled = oldControlType.Enabled; + newControlType.AutoFocus = oldControlType.AutoFocus; + + newControlType.AnchorMin = oldControlType.AnchorMin; + newControlType.AnchorMax = oldControlType.AnchorMax; + newControlType.Offsets = oldControlType.Offsets; + + newControlType.LocalLocation = oldControlType.LocalLocation; + newControlType.Scale = oldControlType.Scale; + newControlType.Bounds = oldControlType.Bounds; + newControlType.Width = oldControlType.Width; + newControlType.Height = oldControlType.Height; + newControlType.Center = oldControlType.Center; + newControlType.PivotRelative = oldControlType.PivotRelative; + + newControlType.Pivot = oldControlType.Pivot; + newControlType.Shear = oldControlType.Shear; + newControlType.Rotation = oldControlType.Rotation; + } + if (oldControlType is ContainerControl oldContainer && newControlType is ContainerControl newContainer) + { + newContainer.CullChildren = oldContainer.CullChildren; + newContainer.ClipChildren = oldContainer.ClipChildren; + } + + uiControl.Control = newControlType; + if (uiControl.Name.StartsWith(previousName)) { string newName = controlType.Name + uiControl.Name.Substring(previousName.Length); diff --git a/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs b/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs index bf087feda..4829bd91a 100644 --- a/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs @@ -34,7 +34,7 @@ namespace FlaxEditor.CustomEditors.Editors value = 0; // If selected is single actor that has children, ask if apply layer to the sub objects as well - if (Values.IsSingleObject && (int)Values[0] != value && ParentEditor.Values[0] is Actor actor && actor.HasChildren) + if (Values.IsSingleObject && (int)Values[0] != value && ParentEditor.Values[0] is Actor actor && actor.HasChildren && !Editor.IsPlayMode) { var valueText = comboBox.SelectedItem; diff --git a/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs b/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs index cfba940c2..1f3359fd5 100644 --- a/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs +++ b/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs @@ -71,7 +71,7 @@ namespace FlaxEditor.CustomEditors.Editors { // Generic file picker assetType = ScriptType.Null; - Picker.FileExtension = assetReference.TypeName; + Picker.Validator.FileExtension = assetReference.TypeName; } else { @@ -85,7 +85,7 @@ namespace FlaxEditor.CustomEditors.Editors } } - Picker.AssetType = assetType; + Picker.Validator.AssetType = assetType; Picker.Height = height; Picker.SelectedItemChanged += OnSelectedItemChanged; } @@ -95,15 +95,15 @@ namespace FlaxEditor.CustomEditors.Editors if (_isRefreshing) return; if (typeof(AssetItem).IsAssignableFrom(_valueType.Type)) - SetValue(Picker.SelectedItem); + SetValue(Picker.Validator.SelectedItem); else if (_valueType.Type == typeof(Guid)) - SetValue(Picker.SelectedID); + SetValue(Picker.Validator.SelectedID); else if (_valueType.Type == typeof(SceneReference)) - SetValue(new SceneReference(Picker.SelectedID)); + SetValue(new SceneReference(Picker.Validator.SelectedID)); else if (_valueType.Type == typeof(string)) - SetValue(Picker.SelectedPath); + SetValue(Picker.Validator.SelectedPath); else - SetValue(Picker.SelectedAsset); + SetValue(Picker.Validator.SelectedAsset); } /// @@ -115,15 +115,15 @@ namespace FlaxEditor.CustomEditors.Editors { _isRefreshing = true; if (Values[0] is AssetItem assetItem) - Picker.SelectedItem = assetItem; + Picker.Validator.SelectedItem = assetItem; else if (Values[0] is Guid guid) - Picker.SelectedID = guid; + Picker.Validator.SelectedID = guid; else if (Values[0] is SceneReference sceneAsset) - Picker.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID); + Picker.Validator.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID); else if (Values[0] is string path) - Picker.SelectedPath = path; + Picker.Validator.SelectedPath = path; else - Picker.SelectedAsset = Values[0] as Asset; + Picker.Validator.SelectedAsset = Values[0] as Asset; _isRefreshing = false; } } diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 8922e2d25..6f623fb23 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -3,9 +3,12 @@ using System; using System.Collections; using System.Linq; +using FlaxEditor.Content; using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.GUI.Drag; +using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; @@ -110,7 +113,7 @@ namespace FlaxEditor.CustomEditors.Editors public override void Initialize(LayoutElementsContainer layout) { // No support for different collections for now - if (HasDifferentValues || HasDifferentTypes) + if (HasDifferentTypes) return; var size = Count; @@ -135,14 +138,43 @@ namespace FlaxEditor.CustomEditors.Editors spacing = collection.Spacing; } + var dragArea = layout.CustomContainer(); + dragArea.CustomControl.Editor = this; + dragArea.CustomControl.ElementType = ElementType; + + // Check for the AssetReferenceAttribute. In JSON assets, it can be used to filter + // which scripts can be dragged over and dropped on this collection editor. + var assetReference = (AssetReferenceAttribute)attributes?.FirstOrDefault(x => x is AssetReferenceAttribute); + if (assetReference != null) + { + if (string.IsNullOrEmpty(assetReference.TypeName)) + { + } + else if (assetReference.TypeName.Length > 1 && assetReference.TypeName[0] == '.') + { + dragArea.CustomControl.ElementType = ScriptType.Null; + dragArea.CustomControl.FileExtension = assetReference.TypeName; + } + else + { + var customType = TypeUtils.GetType(assetReference.TypeName); + if (customType != ScriptType.Null) + dragArea.CustomControl.ElementType = customType; + else if (!Content.Settings.GameSettings.OptionalPlatformSettings.Contains(assetReference.TypeName)) + Debug.LogWarning(string.Format("Unknown asset type '{0}' to use for drag and drop filter.", assetReference.TypeName)); + else + dragArea.CustomControl.ElementType = ScriptType.Void; + } + } + // Size if (_readOnly || (NotNullItems && size == 0)) { - layout.Label("Size", size.ToString()); + dragArea.Label("Size", size.ToString()); } else { - _size = layout.IntegerValue("Size"); + _size = dragArea.IntegerValue("Size"); _size.IntValue.MinValue = 0; _size.IntValue.MaxValue = ushort.MaxValue; _size.IntValue.Value = size; @@ -152,7 +184,7 @@ namespace FlaxEditor.CustomEditors.Editors // Elements if (size > 0) { - var panel = layout.VerticalPanel(); + var panel = dragArea.VerticalPanel(); panel.Panel.BackgroundColor = _background; var elementType = ElementType; @@ -212,37 +244,33 @@ namespace FlaxEditor.CustomEditors.Editors // Add/Remove buttons if (!_readOnly) { - var area = layout.Space(20); - var addButton = new Button(area.ContainerControl.Width - (16 + 16 + 2 + 2), 2, 16, 16) - { - Text = "+", - TooltipText = "Add new item", - AnchorPreset = AnchorPresets.TopRight, - Parent = area.ContainerControl, - Enabled = !NotNullItems || size > 0, - }; - addButton.Clicked += () => - { - if (IsSetBlocked) - return; + var panel = dragArea.HorizontalPanel(); + panel.Panel.Size = new Float2(0, 20); + panel.Panel.Margin = new Margin(2); - Resize(Count + 1); - }; - var removeButton = new Button(addButton.Right + 2, addButton.Y, 16, 16) - { - Text = "-", - TooltipText = "Remove last item", - AnchorPreset = AnchorPresets.TopRight, - Parent = area.ContainerControl, - Enabled = size > 0, - }; - removeButton.Clicked += () => + var removeButton = panel.Button("-", "Remove last item"); + removeButton.Button.Size = new Float2(16, 16); + removeButton.Button.Enabled = size > 0; + removeButton.Button.AnchorPreset = AnchorPresets.TopRight; + removeButton.Button.Clicked += () => { if (IsSetBlocked) return; Resize(Count - 1); }; + + var addButton = panel.Button("+", "Add new item"); + addButton.Button.Size = new Float2(16, 16); + addButton.Button.Enabled = !NotNullItems || size > 0; + addButton.Button.AnchorPreset = AnchorPresets.TopRight; + addButton.Button.Clicked += () => + { + if (IsSetBlocked) + return; + + Resize(Count + 1); + }; } } @@ -369,5 +397,232 @@ namespace FlaxEditor.CustomEditors.Editors } return base.OnDirty(editor, value, token); } + + private class DragAreaControl : VerticalPanel + { + private DragItems _dragItems; + private DragActors _dragActors; + private DragHandlers _dragHandlers; + private AssetPickerValidator _pickerValidator; + + public ScriptType ElementType + { + get => _pickerValidator?.AssetType ?? ScriptType.Null; + set => _pickerValidator = new AssetPickerValidator(value); + } + + public CollectionEditor Editor { get; set; } + + public string FileExtension + { + set => _pickerValidator.FileExtension = value; + } + + /// + public override void Draw() + { + if (_dragHandlers is { HasValidDrag: true }) + { + var area = new Rectangle(Float2.Zero, Size); + Render2D.FillRectangle(area, Color.Orange * 0.5f); + Render2D.DrawRectangle(area, Color.Black); + } + + base.Draw(); + } + + public override void OnDestroy() + { + _pickerValidator.OnDestroy(); + } + + private bool ValidateActors(ActorNode node) + { + return node.Actor.GetScript(ElementType.Type) || ElementType.Type.IsAssignableTo(typeof(Actor)); + } + + /// + public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) + { + var result = base.OnDragEnter(ref location, data); + if (result != DragDropEffect.None) + return result; + + if (_dragHandlers == null) + { + _dragItems = new DragItems(_pickerValidator.IsValid); + _dragActors = new DragActors(ValidateActors); + _dragHandlers = new DragHandlers + { + _dragActors, + _dragItems + }; + } + return _dragHandlers.OnDragEnter(data); + } + + /// + public override DragDropEffect OnDragMove(ref Float2 location, DragData data) + { + var result = base.OnDragMove(ref location, data); + if (result != DragDropEffect.None) + return result; + + return _dragHandlers.Effect; + } + + /// + public override void OnDragLeave() + { + _dragHandlers.OnDragLeave(); + + base.OnDragLeave(); + } + + /// + public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) + { + var result = base.OnDragDrop(ref location, data); + if (result != DragDropEffect.None) + { + _dragHandlers.OnDragDrop(null); + return result; + } + + if (_dragHandlers.HasValidDrag) + { + if (_dragItems.HasValidDrag) + { + var list = Editor.CloneValues(); + if (list == null) + { + if (Editor.Values.Type.IsArray) + { + list = TypeUtils.CreateArrayInstance(Editor.Values.Type.GetElementType(), 0); + } + else + { + list = Editor.Values.Type.CreateInstance() as IList; + } + } + if (list.IsFixedSize) + { + var oldSize = list.Count; + var newSize = list.Count + _dragItems.Objects.Count; + var type = Editor.Values.Type.GetElementType(); + var array = TypeUtils.CreateArrayInstance(type, newSize); + list.CopyTo(array, 0); + + for (var i = oldSize; i < newSize; i++) + { + var validator = new AssetPickerValidator + { + FileExtension = _pickerValidator.FileExtension, + AssetType = _pickerValidator.AssetType, + SelectedItem = _dragItems.Objects[i - oldSize], + }; + + if (typeof(AssetItem).IsAssignableFrom(ElementType.Type)) + array.SetValue(validator.SelectedItem, i); + else if (ElementType.Type == typeof(Guid)) + array.SetValue(validator.SelectedID, i); + else if (ElementType.Type == typeof(SceneReference)) + array.SetValue(new SceneReference(validator.SelectedID), i); + else if (ElementType.Type == typeof(string)) + array.SetValue(validator.SelectedPath, i); + else + array.SetValue(validator.SelectedAsset, i); + + validator.OnDestroy(); + } + Editor.SetValue(array); + } + else + { + foreach (var item in _dragItems.Objects) + { + var validator = new AssetPickerValidator + { + FileExtension = _pickerValidator.FileExtension, + AssetType = _pickerValidator.AssetType, + SelectedItem = item, + }; + + if (typeof(AssetItem).IsAssignableFrom(ElementType.Type)) + list.Add(validator.SelectedItem); + else if (ElementType.Type == typeof(Guid)) + list.Add(validator.SelectedID); + else if (ElementType.Type == typeof(SceneReference)) + list.Add(new SceneReference(validator.SelectedID)); + else if (ElementType.Type == typeof(string)) + list.Add(validator.SelectedPath); + else + list.Add(validator.SelectedAsset); + + validator.OnDestroy(); + } + Editor.SetValue(list); + } + } + else if (_dragActors.HasValidDrag) + { + var list = Editor.CloneValues(); + if (list == null) + { + if (Editor.Values.Type.IsArray) + { + list = TypeUtils.CreateArrayInstance(Editor.Values.Type.GetElementType(), 0); + } + else + { + list = Editor.Values.Type.CreateInstance() as IList; + } + } + + if (list.IsFixedSize) + { + var oldSize = list.Count; + var newSize = list.Count + _dragActors.Objects.Count; + var type = Editor.Values.Type.GetElementType(); + var array = TypeUtils.CreateArrayInstance(type, newSize); + list.CopyTo(array, 0); + + for (var i = oldSize; i < newSize; i++) + { + var actor = _dragActors.Objects[i - oldSize].Actor; + if (ElementType.Type.IsAssignableTo(typeof(Actor))) + { + array.SetValue(actor, i); + } + else + { + array.SetValue(actor.GetScript(ElementType.Type), i); + } + } + Editor.SetValue(array); + } + else + { + foreach (var actorNode in _dragActors.Objects) + { + if (ElementType.Type.IsAssignableTo(typeof(Actor))) + { + list.Add(actorNode.Actor); + } + else + { + list.Add(actorNode.Actor.GetScript(ElementType.Type)); + } + } + Editor.SetValue(list); + } + } + + _dragHandlers.OnDragDrop(null); + } + + return result; + } + } } } diff --git a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs index 9607680f2..f901b20d9 100644 --- a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs @@ -28,14 +28,16 @@ namespace FlaxEditor.CustomEditors.Editors var group = layout.Group("Entry"); _group = group; - if (ParentEditor == null) + if (ParentEditor == null || HasDifferentTypes) return; var entry = (ModelInstanceEntry)Values[0]; var entryIndex = ParentEditor.ChildrenEditors.IndexOf(this); var materialLabel = new PropertyNameLabel("Material"); materialLabel.TooltipText = "The mesh surface material used for the rendering."; - if (ParentEditor.ParentEditor?.Values[0] is ModelInstanceActor modelInstance) + var parentEditorValues = ParentEditor.ParentEditor?.Values; + if (parentEditorValues?[0] is ModelInstanceActor modelInstance) { + // TODO: store _modelInstance and _material in array for each selected model instance actor _entryIndex = entryIndex; _modelInstance = modelInstance; var slots = modelInstance.MaterialSlots; @@ -56,6 +58,8 @@ namespace FlaxEditor.CustomEditors.Editors // Create material picker var materialValue = new CustomValueContainer(new ScriptType(typeof(MaterialBase)), _material, (instance, index) => _material, (instance, index, value) => _material = value as MaterialBase); + for (var i = 1; i < parentEditorValues.Count; i++) + materialValue.Add(_material); var materialEditor = (AssetRefEditor)_group.Property(materialLabel, materialValue); materialEditor.Values.SetDefaultValue(defaultValue); materialEditor.RefreshDefaultValue(); @@ -72,14 +76,14 @@ namespace FlaxEditor.CustomEditors.Editors return; _isRefreshing = true; var slots = _modelInstance.MaterialSlots; - var material = _materialEditor.Picker.SelectedAsset as MaterialBase; + var material = _materialEditor.Picker.Validator.SelectedAsset as MaterialBase; var defaultMaterial = GPUDevice.Instance.DefaultMaterial; var value = (ModelInstanceEntry)Values[0]; var prevMaterial = value.Material; if (!material) { // Fallback to default material - _materialEditor.Picker.SelectedAsset = defaultMaterial; + _materialEditor.Picker.Validator.SelectedAsset = defaultMaterial; value.Material = defaultMaterial; } else if (material == slots[_entryIndex].Material) diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index 8d6b0f9e2..1476832c4 100644 --- a/Source/Editor/GUI/AssetPicker.cs +++ b/Source/Editor/GUI/AssetPicker.cs @@ -5,6 +5,7 @@ using System.IO; using FlaxEditor.Content; using FlaxEditor.GUI.Drag; using FlaxEditor.Scripting; +using FlaxEditor.Utilities; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -17,189 +18,21 @@ namespace FlaxEditor.GUI /// /// [HideInEditor] - public class AssetPicker : Control, IContentItemOwner + public class AssetPicker : Control { private const float DefaultIconSize = 64; private const float ButtonsOffset = 2; private const float ButtonsSize = 12; - private Asset _selected; - private ContentItem _selectedItem; - private ScriptType _type; - private string _fileExtension; - private bool _isMouseDown; private Float2 _mouseDownPos; private Float2 _mousePos; private DragItems _dragOverElement; /// - /// Gets or sets the selected item. + /// The asset validator. Used to ensure only appropriate items can be picked. /// - public ContentItem SelectedItem - { - get => _selectedItem; - set - { - if (_selectedItem == value) - return; - if (value == null) - { - if (_selected == null && _selectedItem is SceneItem) - { - // Deselect scene reference - _selectedItem.RemoveReference(this); - _selectedItem = null; - _selected = null; - OnSelectedItemChanged(); - return; - } - - // Deselect - _selectedItem?.RemoveReference(this); - _selectedItem = null; - _selected = null; - OnSelectedItemChanged(); - } - else if (value is SceneItem item) - { - if (_selectedItem == item) - return; - if (!IsValid(item)) - item = null; - - // Change value to scene reference (cannot load asset because scene can be already loaded - duplicated ID issue) - _selectedItem?.RemoveReference(this); - _selectedItem = item; - _selected = null; - _selectedItem?.AddReference(this); - OnSelectedItemChanged(); - } - else if (value is AssetItem assetItem) - { - SelectedAsset = FlaxEngine.Content.LoadAsync(assetItem.ID); - } - else - { - // Change value - _selectedItem?.RemoveReference(this); - _selectedItem = value; - _selected = null; - OnSelectedItemChanged(); - } - } - } - - /// - /// Gets or sets the selected asset identifier. - /// - public Guid SelectedID - { - get - { - if (_selected != null) - return _selected.ID; - if (_selectedItem is AssetItem assetItem) - return assetItem.ID; - return Guid.Empty; - } - set => SelectedItem = Editor.Instance.ContentDatabase.FindAsset(value); - } - - /// - /// Gets or sets the selected content item path. - /// - public string SelectedPath - { - get - { - string path = _selectedItem?.Path ?? _selected?.Path; - if (path != null) - { - // Convert into path relative to the project (cross-platform) - var projectFolder = Globals.ProjectFolder; - if (path.StartsWith(projectFolder)) - path = path.Substring(projectFolder.Length + 1); - } - return path; - } - set - { - if (string.IsNullOrEmpty(value)) - { - SelectedItem = null; - } - else - { - var path = StringUtils.IsRelative(value) ? Path.Combine(Globals.ProjectFolder, value) : value; - SelectedItem = Editor.Instance.ContentDatabase.Find(path); - } - } - } - - /// - /// Gets or sets the selected asset object. - /// - public Asset SelectedAsset - { - get => _selected; - set - { - // Check if value won't change - if (value == _selected) - return; - - // Find item from content database and check it - var item = value ? Editor.Instance.ContentDatabase.FindAsset(value.ID) : null; - if (item != null && !IsValid(item)) - item = null; - - // Change value - _selectedItem?.RemoveReference(this); - _selectedItem = item; - _selected = value; - _selectedItem?.AddReference(this); - OnSelectedItemChanged(); - } - } - - /// - /// Gets or sets the assets types that this picker accepts (it supports types derived from the given type). Use for generic file picker. - /// - public ScriptType AssetType - { - get => _type; - set - { - if (_type != value) - { - _type = value; - - // Auto deselect if the current value is invalid - if (_selectedItem != null && !IsValid(_selectedItem)) - SelectedItem = null; - } - } - } - - /// - /// Gets or sets the content items extensions filter. Null if unused. - /// - public string FileExtension - { - get => _fileExtension; - set - { - if (_fileExtension != value) - { - _fileExtension = value; - - // Auto deselect if the current value is invalid - if (_selectedItem != null && !IsValid(_selectedItem)) - SelectedItem = null; - } - } - } + public AssetPickerValidator Validator { get; } /// /// Occurs when selected item gets changed. @@ -216,38 +49,6 @@ namespace FlaxEditor.GUI /// public bool CanEdit = true; - private bool IsValid(ContentItem item) - { - if (_fileExtension != null && !item.Path.EndsWith(_fileExtension)) - return false; - if (CheckValid != null && !CheckValid(item)) - return false; - if (_type == ScriptType.Null) - return true; - - if (item is AssetItem assetItem) - { - // Faster path for binary items (in-built) - if (assetItem is BinaryAssetItem binaryItem) - return _type.IsAssignableFrom(new ScriptType(binaryItem.Type)); - - // Type filter - var type = TypeUtils.GetType(assetItem.TypeName); - if (_type.IsAssignableFrom(type)) - return true; - - // Json assets can contain any type of the object defined by the C# type (data oriented design) - if (assetItem is JsonAssetItem && (_type.Type == typeof(JsonAsset) || _type.Type == typeof(Asset))) - return true; - - // Special case for scene asset references - if (_type.Type == typeof(SceneReference) && assetItem is SceneItem) - return true; - } - - return false; - } - /// /// Initializes a new instance of the class. /// @@ -264,7 +65,8 @@ namespace FlaxEditor.GUI public AssetPicker(ScriptType assetType, Float2 location) : base(location, new Float2(DefaultIconSize + ButtonsOffset + ButtonsSize, DefaultIconSize)) { - _type = assetType; + Validator = new AssetPickerValidator(assetType); + Validator.SelectedItemChanged += OnSelectedItemChanged; _mousePos = Float2.Minimum; } @@ -275,10 +77,10 @@ namespace FlaxEditor.GUI { // Update tooltip string tooltip; - if (_selectedItem is AssetItem assetItem) + if (Validator.SelectedItem is AssetItem assetItem) tooltip = assetItem.NamePath; else - tooltip = SelectedPath; + tooltip = Validator.SelectedPath; TooltipText = tooltip; SelectedItemChanged?.Invoke(); @@ -289,37 +91,13 @@ namespace FlaxEditor.GUI // Do the drag drop operation if has selected element if (new Rectangle(Float2.Zero, Size).Contains(ref _mouseDownPos)) { - if (_selected != null) - DoDragDrop(DragAssets.GetDragData(_selected)); - else if (_selectedItem != null) - DoDragDrop(DragItems.GetDragData(_selectedItem)); + if (Validator.SelectedAsset != null) + DoDragDrop(DragAssets.GetDragData(Validator.SelectedAsset)); + else if (Validator.SelectedItem != null) + DoDragDrop(DragItems.GetDragData(Validator.SelectedItem)); } } - /// - public void OnItemDeleted(ContentItem item) - { - // Deselect item - SelectedItem = null; - } - - /// - public void OnItemRenamed(ContentItem item) - { - } - - /// - public void OnItemReimported(ContentItem item) - { - } - - /// - public void OnItemDispose(ContentItem item) - { - // Deselect item - SelectedItem = null; - } - private Rectangle IconRect => new Rectangle(0, 0, Height, Height); private Rectangle Button1Rect => new Rectangle(Height + ButtonsOffset, 0, ButtonsSize, ButtonsSize); @@ -341,10 +119,10 @@ namespace FlaxEditor.GUI if (CanEdit) Render2D.DrawSprite(style.ArrowDown, button1Rect, button1Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey); - if (_selectedItem != null) + if (Validator.SelectedItem != null) { // Draw item preview - _selectedItem.DrawThumbnail(ref iconRect); + Validator.SelectedItem.DrawThumbnail(ref iconRect); // Draw buttons if (CanEdit) @@ -363,7 +141,7 @@ namespace FlaxEditor.GUI { Render2D.DrawText( style.FontSmall, - _selectedItem.ShortName, + Validator.SelectedItem.ShortName, new Rectangle(button1Rect.Right + 2, 0, sizeForTextLeft, ButtonsSize), style.Foreground, TextAlignment.Near, @@ -371,7 +149,7 @@ namespace FlaxEditor.GUI } } // Check if has no item but has an asset (eg. virtual asset) - else if (_selected) + else if (Validator.SelectedAsset) { // Draw remove button Render2D.DrawSprite(style.Cross, button3Rect, button3Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey); @@ -380,8 +158,8 @@ namespace FlaxEditor.GUI float sizeForTextLeft = Width - button1Rect.Right; if (sizeForTextLeft > 30) { - var name = _selected.GetType().Name; - if (_selected.IsVirtual) + var name = Validator.SelectedAsset.GetType().Name; + if (Validator.SelectedAsset.IsVirtual) name += " (virtual)"; Render2D.DrawText( style.FontSmall, @@ -407,9 +185,7 @@ namespace FlaxEditor.GUI /// public override void OnDestroy() { - _selectedItem?.RemoveReference(this); - _selectedItem = null; - _selected = null; + Validator.OnDestroy(); base.OnDestroy(); } @@ -463,57 +239,57 @@ namespace FlaxEditor.GUI // Buttons logic if (!CanEdit) { - if (Button1Rect.Contains(location) && _selectedItem != null) + if (Button1Rect.Contains(location) && Validator.SelectedItem != null) { // Select asset - Editor.Instance.Windows.ContentWin.Select(_selectedItem); + Editor.Instance.Windows.ContentWin.Select(Validator.SelectedItem); } } else if (Button1Rect.Contains(location)) { Focus(); - if (_type != ScriptType.Null) + if (Validator.AssetType != ScriptType.Null) { // Show asset picker popup - var popup = AssetSearchPopup.Show(this, Button1Rect.BottomLeft, IsValid, item => + var popup = AssetSearchPopup.Show(this, Button1Rect.BottomLeft, Validator.IsValid, item => { - SelectedItem = item; + Validator.SelectedItem = item; RootWindow.Focus(); Focus(); }); - if (_selected != null) + if (Validator.SelectedAsset != null) { - var selectedAssetName = Path.GetFileNameWithoutExtension(_selected.Path); + var selectedAssetName = Path.GetFileNameWithoutExtension(Validator.SelectedAsset.Path); popup.ScrollToAndHighlightItemByName(selectedAssetName); } } else { // Show content item picker popup - var popup = ContentSearchPopup.Show(this, Button1Rect.BottomLeft, IsValid, item => + var popup = ContentSearchPopup.Show(this, Button1Rect.BottomLeft, Validator.IsValid, item => { - SelectedItem = item; + Validator.SelectedItem = item; RootWindow.Focus(); Focus(); }); - if (_selectedItem != null) + if (Validator.SelectedItem != null) { - popup.ScrollToAndHighlightItemByName(_selectedItem.ShortName); + popup.ScrollToAndHighlightItemByName(Validator.SelectedItem.ShortName); } } } - else if (_selected != null || _selectedItem != null) + else if (Validator.SelectedAsset != null || Validator.SelectedItem != null) { - if (Button2Rect.Contains(location) && _selectedItem != null) + if (Button2Rect.Contains(location) && Validator.SelectedItem != null) { // Select asset - Editor.Instance.Windows.ContentWin.Select(_selectedItem); + Editor.Instance.Windows.ContentWin.Select(Validator.SelectedItem); } else if (Button3Rect.Contains(location)) { // Deselect asset Focus(); - SelectedItem = null; + Validator.SelectedItem = null; } } } @@ -540,10 +316,10 @@ namespace FlaxEditor.GUI { Focus(); - if (_selectedItem != null && IconRect.Contains(location)) + if (Validator.SelectedItem != null && IconRect.Contains(location)) { // Open it - Editor.Instance.ContentEditing.Open(_selectedItem); + Editor.Instance.ContentEditing.Open(Validator.SelectedItem); } // Handled @@ -557,7 +333,7 @@ namespace FlaxEditor.GUI // Check if drop asset if (_dragOverElement == null) - _dragOverElement = new DragItems(IsValid); + _dragOverElement = new DragItems(Validator.IsValid); if (CanEdit && _dragOverElement.OnDragEnter(data)) { } @@ -590,7 +366,7 @@ namespace FlaxEditor.GUI if (CanEdit && _dragOverElement.HasValidDrag) { // Select element - SelectedItem = _dragOverElement.Objects[0]; + Validator.SelectedItem = _dragOverElement.Objects[0]; } // Clear cache diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs index 161b3f4ae..27878a763 100644 --- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs +++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs @@ -319,7 +319,9 @@ namespace FlaxEditor.GUI.Dialogs protected override void OnShow() { // Auto cancel on lost focus +#if !PLATFORM_LINUX ((WindowRootControl)Root).Window.LostFocus += OnCancel; +#endif base.OnShow(); } diff --git a/Source/Editor/GUI/Dialogs/Dialog.cs b/Source/Editor/GUI/Dialogs/Dialog.cs index 5054aee98..86e24e3b3 100644 --- a/Source/Editor/GUI/Dialogs/Dialog.cs +++ b/Source/Editor/GUI/Dialogs/Dialog.cs @@ -39,6 +39,11 @@ namespace FlaxEditor.GUI.Dialogs /// public DialogResult Result => _result; + /// + /// Returns the size of the dialog. + /// + public Float2 DialogSize => _dialogSize; + /// /// Initializes a new instance of the class. /// diff --git a/Source/Editor/GUI/Docking/DockHintWindow.cs b/Source/Editor/GUI/Docking/DockHintWindow.cs index 52c5dcd3c..6e2353441 100644 --- a/Source/Editor/GUI/Docking/DockHintWindow.cs +++ b/Source/Editor/GUI/Docking/DockHintWindow.cs @@ -44,11 +44,11 @@ namespace FlaxEditor.GUI.Docking var mousePos = window.MousePosition; var previousSize = window.Size; window.Restore(); - window.Position = FlaxEngine.Input.MouseScreenPosition - mousePos * window.Size / previousSize; + window.Position = Platform.MousePosition - mousePos * window.Size / previousSize; } // Calculate dragging offset and move window to the destination position - var mouseScreenPosition = FlaxEngine.Input.MouseScreenPosition; + var mouseScreenPosition = Platform.MousePosition; // If the _toMove window was not focused when initializing this window, the result vector only contains zeros // and to prevent a failure, we need to perform an update for the drag offset at later time which will be done in the OnMouseMove event handler. @@ -83,6 +83,7 @@ namespace FlaxEditor.GUI.Docking // Enable hit window presentation Proxy.Window.RenderingEnabled = true; Proxy.Window.Show(); + Proxy.Window.Focus(); } /// @@ -113,7 +114,7 @@ namespace FlaxEditor.GUI.Docking var window = _toMove.Window?.Window; if (window == null) return; - var mouse = FlaxEngine.Input.MouseScreenPosition; + var mouse = Platform.MousePosition; // Move base window window.Position = mouse - _dragOffset; @@ -193,7 +194,7 @@ namespace FlaxEditor.GUI.Docking // Move window to the mouse position (with some offset for caption bar) var window = (WindowRootControl)toMove.Root; - var mouse = FlaxEngine.Input.MouseScreenPosition; + var mouse = Platform.MousePosition; window.Window.Position = mouse - new Float2(8, 8); // Get floating panel @@ -244,7 +245,7 @@ namespace FlaxEditor.GUI.Docking private void UpdateRects() { // Cache mouse position - _mouse = FlaxEngine.Input.MouseScreenPosition; + _mouse = Platform.MousePosition; // Check intersection with any dock panel var uiMouse = _mouse; @@ -270,15 +271,16 @@ namespace FlaxEditor.GUI.Docking // Cache dock rectangles var size = _rectDock.Size; var offset = _rectDock.Location; - float BorderMargin = 4.0f; - float ProxyHintWindowsSize2 = Proxy.HintWindowsSize * 0.5f; - float centerX = size.X * 0.5f; - float centerY = size.Y * 0.5f; - _rUpper = new Rectangle(centerX - ProxyHintWindowsSize2, BorderMargin, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rBottom = new Rectangle(centerX - ProxyHintWindowsSize2, size.Y - Proxy.HintWindowsSize - BorderMargin, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rLeft = new Rectangle(BorderMargin, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rRight = new Rectangle(size.X - Proxy.HintWindowsSize - BorderMargin, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rCenter = new Rectangle(centerX - ProxyHintWindowsSize2, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; + var borderMargin = 4.0f; + var hintWindowsSize = Proxy.HintWindowsSize * Platform.DpiScale; + var hintWindowsSize2 = hintWindowsSize * 0.5f; + var centerX = size.X * 0.5f; + var centerY = size.Y * 0.5f; + _rUpper = new Rectangle(centerX - hintWindowsSize2, borderMargin, hintWindowsSize, hintWindowsSize) + offset; + _rBottom = new Rectangle(centerX - hintWindowsSize2, size.Y - hintWindowsSize - borderMargin, hintWindowsSize, hintWindowsSize) + offset; + _rLeft = new Rectangle(borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; + _rRight = new Rectangle(size.X - hintWindowsSize - borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; + _rCenter = new Rectangle(centerX - hintWindowsSize2, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; // Hit test DockState toSet = DockState.Float; @@ -428,7 +430,6 @@ namespace FlaxEditor.GUI.Docking { if (Window == null) { - // Create proxy window var settings = CreateWindowSettings.Default; settings.Title = "DockHint.Window"; settings.Size = initSize; @@ -440,12 +441,10 @@ namespace FlaxEditor.GUI.Docking settings.IsRegularWindow = false; settings.SupportsTransparency = true; settings.ShowInTaskbar = false; - settings.ShowAfterFirstPaint = true; + settings.ShowAfterFirstPaint = false; settings.IsTopmost = true; Window = Platform.CreateWindow(ref settings); - - // Set opacity and background color Window.Opacity = 0.6f; Window.GUI.BackgroundColor = Style.Current.DragWindow; } @@ -465,7 +464,7 @@ namespace FlaxEditor.GUI.Docking var settings = CreateWindowSettings.Default; settings.Title = name; - settings.Size = new Float2(HintWindowsSize); + settings.Size = new Float2(HintWindowsSize * Platform.DpiScale); settings.AllowInput = false; settings.AllowMaximize = false; settings.AllowMinimize = false; @@ -479,7 +478,6 @@ namespace FlaxEditor.GUI.Docking settings.ShowAfterFirstPaint = false; win = Platform.CreateWindow(ref settings); - win.Opacity = 0.6f; win.GUI.BackgroundColor = Style.Current.DragWindow; } diff --git a/Source/Editor/GUI/Docking/DockPanel.cs b/Source/Editor/GUI/Docking/DockPanel.cs index e544285a5..b04aad08c 100644 --- a/Source/Editor/GUI/Docking/DockPanel.cs +++ b/Source/Editor/GUI/Docking/DockPanel.cs @@ -465,36 +465,47 @@ namespace FlaxEditor.GUI.Docking { if (Parent.Parent is SplitPanel splitter) { - // Check if has any child panels - var childPanel = new List(_childPanels); - for (int i = 0; i < childPanel.Count; i++) + // Check if there is another nested dock panel inside this dock panel and extract it here + var childPanels = _childPanels.ToArray(); + if (childPanels.Length != 0) { - // Undock all tabs - var panel = childPanel[i]; - int count = panel.TabsCount; - while (count-- > 0) + // Move tabs from child panels into this one + DockWindow selectedTab = null; + foreach (var childPanel in childPanels) { - panel.GetTab(0).Close(); + var childPanelTabs = childPanel.Tabs.ToArray(); + for (var i = 0; i < childPanelTabs.Length; i++) + { + var childPanelTab = childPanelTabs[i]; + if (selectedTab == null && childPanelTab.IsSelected) + selectedTab = childPanelTab; + childPanel.UndockWindow(childPanelTab); + AddTab(childPanelTab, false); + } } + if (selectedTab != null) + SelectTab(selectedTab); } - - // Unlink splitter - var splitterParent = splitter.Parent; - Assert.IsNotNull(splitterParent); - splitter.Parent = null; - - // Move controls from second split panel to the split panel parent - var scrPanel = Parent == splitter.Panel2 ? splitter.Panel1 : splitter.Panel2; - var srcPanelChildrenCount = scrPanel.ChildrenCount; - for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--) + else { - scrPanel.GetChild(i).Parent = splitterParent; - } - Assert.IsTrue(scrPanel.ChildrenCount == 0); - Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount); + // Unlink splitter + var splitterParent = splitter.Parent; + Assert.IsNotNull(splitterParent); + splitter.Parent = null; - // Delete - splitter.Dispose(); + // Move controls from second split panel to the split panel parent + var scrPanel = Parent == splitter.Panel2 ? splitter.Panel1 : splitter.Panel2; + var srcPanelChildrenCount = scrPanel.ChildrenCount; + for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--) + { + scrPanel.GetChild(i).Parent = splitterParent; + } + Assert.IsTrue(scrPanel.ChildrenCount == 0); + Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount); + + // Delete + splitter.Dispose(); + } } else if (!IsMaster) { @@ -582,19 +593,17 @@ namespace FlaxEditor.GUI.Docking /// Adds the tab. /// /// The window to insert as a tab. - protected virtual void AddTab(DockWindow window) + /// True if auto-select newly added tab. + protected virtual void AddTab(DockWindow window, bool autoSelect = true) { - // Dock _tabs.Add(window); window.ParentDockPanel = this; - - // Select tab - SelectTab(window); + if (autoSelect) + SelectTab(window); } private void CreateTabsProxy() { - // Check if has no tabs proxy created if (_tabsProxy == null) { // Create proxy and make set simple full dock diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs index f8ffe62ad..7bb85751a 100644 --- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs +++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs @@ -72,7 +72,7 @@ namespace FlaxEditor.GUI.Docking settings.Size = size; settings.Position = location; settings.MinimumSize = new Float2(1); - settings.MaximumSize = new Float2(4096); + settings.MaximumSize = Float2.Zero; // Unlimited size settings.Fullscreen = false; settings.HasBorder = true; settings.SupportsTransparency = false; diff --git a/Source/Editor/GUI/Input/ColorValueBox.cs b/Source/Editor/GUI/Input/ColorValueBox.cs index 167cc65bb..fdd7d073a 100644 --- a/Source/Editor/GUI/Input/ColorValueBox.cs +++ b/Source/Editor/GUI/Input/ColorValueBox.cs @@ -14,6 +14,8 @@ namespace FlaxEditor.GUI.Input [HideInEditor] public class ColorValueBox : Control { + private bool _isMouseDown; + /// /// Delegate function used for the color picker events handling. /// @@ -134,11 +136,22 @@ namespace FlaxEditor.GUI.Input Render2D.DrawRectangle(r, IsMouseOver || IsNavFocused ? style.BackgroundSelected : Color.Black); } + /// + public override bool OnMouseDown(Float2 location, MouseButton button) + { + _isMouseDown = true; + return base.OnMouseDown(location, button); + } + /// public override bool OnMouseUp(Float2 location, MouseButton button) { - Focus(); - OnSubmit(); + if (_isMouseDown) + { + _isMouseDown = false; + Focus(); + OnSubmit(); + } return true; } diff --git a/Source/Editor/GUI/Tabs/Tab.cs b/Source/Editor/GUI/Tabs/Tab.cs index 3968cc17d..85f805af0 100644 --- a/Source/Editor/GUI/Tabs/Tab.cs +++ b/Source/Editor/GUI/Tabs/Tab.cs @@ -13,6 +13,8 @@ namespace FlaxEditor.GUI.Tabs [HideInEditor] public class Tab : ContainerControl { + internal Tabs _selectedInTabs; + /// /// Gets or sets the text. /// @@ -86,5 +88,25 @@ namespace FlaxEditor.GUI.Tabs { return new Tabs.TabHeader((Tabs)Parent, this); } + + /// + protected override void OnParentChangedInternal() + { + if (_selectedInTabs != null) + _selectedInTabs.SelectedTab = null; + + base.OnParentChangedInternal(); + } + + /// + public override void OnDestroy() + { + if (IsDisposing) + return; + if (_selectedInTabs != null) + _selectedInTabs.SelectedTab = null; + + base.OnDestroy(); + } } } diff --git a/Source/Editor/GUI/Tabs/Tabs.cs b/Source/Editor/GUI/Tabs/Tabs.cs index f9bfd3f6c..b5e2cfe39 100644 --- a/Source/Editor/GUI/Tabs/Tabs.cs +++ b/Source/Editor/GUI/Tabs/Tabs.cs @@ -191,6 +191,8 @@ namespace FlaxEditor.GUI.Tabs get => _autoTabsSizeAuto; set { + if (_autoTabsSizeAuto == value) + return; _autoTabsSizeAuto = value; PerformLayout(); } @@ -204,11 +206,11 @@ namespace FlaxEditor.GUI.Tabs get => _orientation; set { + if (_orientation == value) + return; _orientation = value; - if (UseScroll) TabsPanel.ScrollBars = _orientation == Orientation.Horizontal ? ScrollBars.Horizontal : ScrollBars.Vertical; - PerformLayout(); } } @@ -261,7 +263,12 @@ namespace FlaxEditor.GUI.Tabs // Check if index will change if (_selectedIndex != index) { - SelectedTab?.OnDeselected(); + var prev = SelectedTab; + if (prev != null) + { + prev._selectedInTabs = null; + prev.OnDeselected(); + } _selectedIndex = index; PerformLayout(); OnSelectedTabChanged(); @@ -340,8 +347,13 @@ namespace FlaxEditor.GUI.Tabs /// protected virtual void OnSelectedTabChanged() { + var selectedTab = SelectedTab; SelectedTabChanged?.Invoke(this); - SelectedTab?.OnSelected(); + if (selectedTab != null) + { + selectedTab._selectedInTabs = this; + selectedTab.OnSelected(); + } } /// @@ -402,6 +414,14 @@ namespace FlaxEditor.GUI.Tabs tabHeader.Size = tabsSize; } } + else if (UseScroll) + { + // If scroll bar is visible it covers part of the tab header so include this in tab size to improve usability + if (_orientation == Orientation.Horizontal && TabsPanel.HScrollBar.Visible) + tabsSize.Y += TabsPanel.HScrollBar.Height; + else if (_orientation == Orientation.Vertical && TabsPanel.VScrollBar.Visible) + tabsSize.X += TabsPanel.VScrollBar.Width; + } // Fit the tabs panel TabsPanel.Size = _orientation == Orientation.Horizontal diff --git a/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs b/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs index b7c87cb01..3bbca15ef 100644 --- a/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs +++ b/Source/Editor/GUI/Timeline/Tracks/SingleMediaAssetTrack.cs @@ -39,7 +39,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks if (AssetID == value?.ID) return; AssetID = value?.ID ?? Guid.Empty; - _picker.SelectedAsset = value; + _picker.Validator.SelectedAsset = value; OnAssetChanged(); Timeline?.MarkAsEdited(); } @@ -63,10 +63,10 @@ namespace FlaxEditor.GUI.Timeline.Tracks private void OnPickerSelectedItemChanged() { - if (Asset == (TAsset)_picker.SelectedAsset) + if (Asset == (TAsset)_picker.Validator.SelectedAsset) return; using (new TrackUndoBlock(this)) - Asset = (TAsset)_picker.SelectedAsset; + Asset = (TAsset)_picker.Validator.SelectedAsset; } /// diff --git a/Source/Editor/GUI/Tree/TreeNode.cs b/Source/Editor/GUI/Tree/TreeNode.cs index 7e3af05bf..703079469 100644 --- a/Source/Editor/GUI/Tree/TreeNode.cs +++ b/Source/Editor/GUI/Tree/TreeNode.cs @@ -776,11 +776,20 @@ namespace FlaxEditor.GUI.Tree // Check if mouse hits arrow if (_mouseOverArrow && HasAnyVisibleChild) { - // Toggle open state - if (_opened) - Collapse(); + if (ParentTree.Root.GetKey(KeyboardKeys.Alt)) + { + if (_opened) + CollapseAll(); + else + ExpandAll(); + } else - Expand(); + { + if (_opened) + Collapse(); + else + Expand(); + } } // Check if mouse hits bar diff --git a/Source/Editor/Gizmo/IGizmoOwner.cs b/Source/Editor/Gizmo/IGizmoOwner.cs index a28810179..ff653196e 100644 --- a/Source/Editor/Gizmo/IGizmoOwner.cs +++ b/Source/Editor/Gizmo/IGizmoOwner.cs @@ -11,6 +11,11 @@ namespace FlaxEditor.Gizmo [HideInEditor] public interface IGizmoOwner { + /// + /// Gets the gizmos collection. + /// + FlaxEditor.Viewport.EditorViewport Viewport { get; } + /// /// Gets the gizmos collection. /// diff --git a/Source/Editor/Gizmo/TransformGizmoBase.cs b/Source/Editor/Gizmo/TransformGizmoBase.cs index dd5cc6c76..6123d348a 100644 --- a/Source/Editor/Gizmo/TransformGizmoBase.cs +++ b/Source/Editor/Gizmo/TransformGizmoBase.cs @@ -162,10 +162,23 @@ namespace FlaxEditor.Gizmo // Scale gizmo to fit on-screen Vector3 position = Position; - Vector3 vLength = Owner.ViewPosition - position; - float gizmoSize = Editor.Instance.Options.Options.Visual.GizmoSize; - _screenScale = (float)(vLength.Length / GizmoScaleFactor * gizmoSize); - + if (Owner.Viewport.UseOrthographicProjection) + { + //[hack] this is far form ideal the View Position is in wrong location, any think using the View Position will have problem + //the camera system needs rewrite the to be a camera on springarm, similar how the ArcBallCamera is handled + //the ortho projection cannot exist with fps camera because there is no + // - focus point to calculate correct View Position with Orthographic Scale as a reference and Orthographic Scale from View Position + // with make the camera jump + // - and deaph so w and s movment in orto mode moves the cliping plane now + float gizmoSize = Editor.Instance.Options.Options.Visual.GizmoSize; + _screenScale = gizmoSize * (50 * Owner.Viewport.OrthographicScale); + } + else + { + Vector3 vLength = Owner.ViewPosition - position; + float gizmoSize = Editor.Instance.Options.Options.Visual.GizmoSize; + _screenScale = (float)(vLength.Length / GizmoScaleFactor * gizmoSize); + } // Setup world Quaternion orientation = GetSelectedObject(0).Orientation; _gizmoWorld = new Transform(position, orientation, new Float3(_screenScale)); diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp index e01fd04bb..969608676 100644 --- a/Source/Editor/Managed/ManagedEditor.Internal.cpp +++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp @@ -513,7 +513,9 @@ DEFINE_INTERNAL_CALL(void) EditorInternal_RunVisualScriptBreakpointLoopTick(floa WindowsManager::WindowsLocker.Unlock(); } WindowsManager::WindowsLocker.Lock(); - for (auto& win : WindowsManager::Windows) + Array> windows; + windows.Add(WindowsManager::Windows); + for (Window* win : windows) { if (win->IsVisible()) win->OnUpdate(deltaTime); diff --git a/Source/Editor/Managed/ManagedEditor.cpp b/Source/Editor/Managed/ManagedEditor.cpp index fd9219d35..bed742885 100644 --- a/Source/Editor/Managed/ManagedEditor.cpp +++ b/Source/Editor/Managed/ManagedEditor.cpp @@ -330,14 +330,15 @@ bool ManagedEditor::CanReloadScripts() bool ManagedEditor::CanAutoBuildCSG() { + if (!ManagedEditorOptions.AutoRebuildCSG) + return false; + // Skip calls from non-managed thread (eg. physics worker) if (!MCore::Thread::IsAttached()) return false; if (!HasManagedInstance()) return false; - if (!ManagedEditorOptions.AutoRebuildCSG) - return false; if (Internal_CanAutoBuildCSG == nullptr) { Internal_CanAutoBuildCSG = GetClass()->GetMethod("Internal_CanAutoBuildCSG"); @@ -348,14 +349,15 @@ bool ManagedEditor::CanAutoBuildCSG() bool ManagedEditor::CanAutoBuildNavMesh() { + if (!ManagedEditorOptions.AutoRebuildNavMesh) + return false; + // Skip calls from non-managed thread (eg. physics worker) if (!MCore::Thread::IsAttached()) return false; if (!HasManagedInstance()) return false; - if (!ManagedEditorOptions.AutoRebuildNavMesh) - return false; if (Internal_CanAutoBuildNavMesh == nullptr) { Internal_CanAutoBuildNavMesh = GetClass()->GetMethod("Internal_CanAutoBuildNavMesh"); diff --git a/Source/Editor/Modules/ContentDatabaseModule.cs b/Source/Editor/Modules/ContentDatabaseModule.cs index eec55c788..80f419a6f 100644 --- a/Source/Editor/Modules/ContentDatabaseModule.cs +++ b/Source/Editor/Modules/ContentDatabaseModule.cs @@ -649,8 +649,6 @@ namespace FlaxEditor.Modules // Special case for folders if (item is ContentFolder folder) { - // TODO: maybe don't remove folders recursive but at once? - // Delete all children if (folder.Children.Count > 0) { @@ -664,6 +662,9 @@ namespace FlaxEditor.Modules // Remove directory if (deletedByUser && Directory.Exists(path)) { + // Flush files removal before removing folder (loaded assets remove file during object destruction in Asset::OnDeleteObject) + FlaxEngine.Scripting.FlushRemovedObjects(); + try { Directory.Delete(path, true); @@ -810,10 +811,9 @@ namespace FlaxEditor.Modules { if (node == null) return; - - // Temporary data var folder = node.Folder; var path = folder.Path; + var canHaveAssets = node.CanHaveAssets; if (_isDuringFastSetup) { @@ -832,20 +832,38 @@ namespace FlaxEditor.Modules var child = folder.Children[i]; if (!child.Exists) { - // Send info + // Item doesn't exist anymore Editor.Log(string.Format($"Content item \'{child.Path}\' has been removed")); - - // Destroy it Delete(child, false); - i--; } + else if (canHaveAssets && child is AssetItem childAsset) + { + // Check if asset type doesn't match the item proxy (eg. item reimported as Material Instance instead of Material) + if (FlaxEngine.Content.GetAssetInfo(child.Path, out var assetInfo)) + { + bool changed = assetInfo.ID != childAsset.ID; + if (!changed && assetInfo.TypeName != childAsset.TypeName) + { + // Use proxy check (eg. scene asset might accept different typename than AssetInfo reports) + var proxy = GetAssetProxy(childAsset.TypeName, child.Path); + if (proxy == null) + proxy = GetAssetProxy(assetInfo.TypeName, child.Path); + changed = !proxy.AcceptsAsset(assetInfo.TypeName, child.Path); + } + if (changed) + { + OnAssetTypeInfoChanged(childAsset, ref assetInfo); + i--; + } + } + } } } // Find files var files = Directory.GetFiles(path, "*.*", SearchOption.TopDirectoryOnly); - if (node.CanHaveAssets) + if (canHaveAssets) { LoadAssets(node, files); } @@ -1134,17 +1152,19 @@ namespace FlaxEditor.Modules RebuildInternal(); - Editor.ContentImporting.ImportFileEnd += ContentImporting_ImportFileDone; + Editor.ContentImporting.ImportFileEnd += (obj, failed) => + { + var path = obj.ResultUrl; + if (!failed) + FlaxEngine.Scripting.InvokeOnUpdate(() => OnImportFileDone(path)); + }; _enableEvents = true; } - private void ContentImporting_ImportFileDone(IFileEntryAction obj, bool failed) + private void OnImportFileDone(string path) { - if (failed) - return; - // Check if already has that element - var item = Find(obj.ResultUrl); + var item = Find(path); if (item is BinaryAssetItem binaryAssetItem) { // Get asset info from the registry (content layer will update cache it just after import) @@ -1154,19 +1174,8 @@ namespace FlaxEditor.Modules // For eg. change texture to sprite atlas on reimport if (binaryAssetItem.TypeName != assetInfo.TypeName) { - // Asset type has been changed! - Editor.LogWarning(string.Format("Asset \'{0}\' changed type from {1} to {2}", item.Path, binaryAssetItem.TypeName, assetInfo.TypeName)); - Editor.Windows.CloseAllEditors(item); - - // Remove this item from the database and some related data var toRefresh = binaryAssetItem.ParentFolder; - binaryAssetItem.Dispose(); - toRefresh.Children.Remove(binaryAssetItem); - if (!binaryAssetItem.HasDefaultThumbnail) - { - // Delete old thumbnail and remove it from the cache - Editor.Instance.Thumbnails.DeletePreview(binaryAssetItem); - } + OnAssetTypeInfoChanged(binaryAssetItem, ref assetInfo); // Refresh the parent folder to find the new asset (it should have different type or some other format) RefreshFolder(toRefresh, false); @@ -1183,6 +1192,23 @@ namespace FlaxEditor.Modules } } + private void OnAssetTypeInfoChanged(AssetItem assetItem, ref AssetInfo assetInfo) + { + // Asset type has been changed! + Editor.LogWarning(string.Format("Asset \'{0}\' changed type from {1} to {2}", assetItem.Path, assetItem.TypeName, assetInfo.TypeName)); + Editor.Windows.CloseAllEditors(assetItem); + + // Remove this item from the database and some related data + assetItem.Dispose(); + assetItem.ParentFolder.Children.Remove(assetItem); + + // Delete old thumbnail and remove it from the cache + if (!assetItem.HasDefaultThumbnail) + { + Editor.Instance.Thumbnails.DeletePreview(assetItem); + } + } + internal void OnDirectoryEvent(MainContentTreeNode node, FileSystemEventArgs e) { // Ensure to be ready for external events diff --git a/Source/Editor/Modules/ContentImportingModule.cs b/Source/Editor/Modules/ContentImportingModule.cs index c94ba650e..85dd50d35 100644 --- a/Source/Editor/Modules/ContentImportingModule.cs +++ b/Source/Editor/Modules/ContentImportingModule.cs @@ -55,7 +55,7 @@ namespace FlaxEditor.Modules public event Action ImportingQueueBegin; /// - /// Occurs when file is being imported. + /// Occurs when file is being imported. Can be called on non-main thread. /// public event Action ImportFileBegin; @@ -67,12 +67,12 @@ namespace FlaxEditor.Modules public delegate void ImportFileEndDelegate(IFileEntryAction entry, bool failed); /// - /// Occurs when file importing end. + /// Occurs when file importing end. Can be called on non-main thread. /// public event ImportFileEndDelegate ImportFileEnd; /// - /// Occurs when assets importing ends. + /// Occurs when assets importing ends. Can be called on non-main thread. /// public event Action ImportingQueueEnd; diff --git a/Source/Editor/Modules/SceneModule.cs b/Source/Editor/Modules/SceneModule.cs index 7789eb7c4..4fb28ac76 100644 --- a/Source/Editor/Modules/SceneModule.cs +++ b/Source/Editor/Modules/SceneModule.cs @@ -266,6 +266,19 @@ namespace FlaxEditor.Modules Editor.StateMachine.ChangingScenesState.LoadScene(sceneId, additive); } + /// + /// Reload all loaded scenes. + /// + public void ReloadScenes() + { + foreach (var scene in Level.Scenes) + { + var sceneId = scene.ID; + if (!Level.UnloadScene(scene)) + Level.LoadScene(sceneId); + } + } + /// /// Closes scene (async). /// diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs index 225bad082..082439402 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs @@ -147,6 +147,17 @@ namespace FlaxEditor.Modules.SourceCodeEditing } if (key != null) xml.TryGetValue(key, out text); + + // Customize tooltips for properties to be more human-readable in UI + if (text != null && memberType.HasFlag(MemberTypes.Property) && text.StartsWith("Gets or sets ", StringComparison.Ordinal)) + { + text = text.Substring(13); + unsafe + { + fixed (char* e = text) + e[0] = char.ToUpper(e[0]); + } + } } } diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs index e9ff4c5b5..96cf44e09 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs @@ -410,9 +410,11 @@ namespace FlaxEditor.Modules.SourceCodeEditing base.OnUpdate(); // Automatic project files generation after workspace modifications - if (_autoGenerateScriptsProjectFiles && ScriptsBuilder.IsSourceWorkspaceDirty) + if (_autoGenerateScriptsProjectFiles && ScriptsBuilder.IsSourceWorkspaceDirty && !ScriptsBuilder.IsCompiling) { - Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync(); + // Try to delay generation when a lot of files are added at once + if (ScriptsBuilder.IsSourceDirtyFor(TimeSpan.FromMilliseconds(150))) + Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync(); } } diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 492d78918..3386d411c 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -2,7 +2,6 @@ using System; using System.IO; -using System.Linq; using System.Collections.Generic; using FlaxEditor.Gizmo; using FlaxEditor.GUI; @@ -11,7 +10,6 @@ using FlaxEditor.GUI.Dialogs; using FlaxEditor.GUI.Input; using FlaxEditor.Progress.Handlers; using FlaxEditor.SceneGraph; -using FlaxEditor.SceneGraph.Actors; using FlaxEditor.Utilities; using FlaxEditor.Viewport.Cameras; using FlaxEditor.Windows; @@ -41,6 +39,7 @@ namespace FlaxEditor.Modules ContextMenuSingleSelectGroup _numberOfClientsGroup = new ContextMenuSingleSelectGroup(); private ContextMenuButton _menuFileSaveScenes; + private ContextMenuButton _menuFileReloadScenes; private ContextMenuButton _menuFileCloseScenes; private ContextMenuButton _menuFileOpenScriptsProject; private ContextMenuButton _menuFileGenerateScriptsProjectFiles; @@ -208,6 +207,7 @@ namespace FlaxEditor.Modules _toolStripScale.Checked = gizmoMode == TransformGizmoBase.Mode.Scale; // _toolStripBuildScenes.Enabled = (canEditScene && !isPlayMode) || Editor.StateMachine.BuildingScenesState.IsActive; + _toolStripBuildScenes.Visible = Editor.Options.Options.General.BuildActions?.Length != 0; _toolStripCook.Enabled = Editor.Windows.GameCookerWin.CanBuild(Platform.PlatformType) && !GameCooker.IsRunning; // var play = _toolStripPlay; @@ -471,13 +471,13 @@ namespace FlaxEditor.Modules // Place dialog nearby the target control var targetControlDesktopCenter = targetControl.PointToScreen(targetControl.Size * 0.5f); var desktopSize = Platform.GetMonitorBounds(targetControlDesktopCenter); - var pos = targetControlDesktopCenter + new Float2(10.0f, -dialog.Height * 0.5f); - var dialogEnd = pos + dialog.Size; + var pos = targetControlDesktopCenter + new Float2(10.0f, -dialog.DialogSize.Y * 0.5f); + var dialogEnd = pos + dialog.DialogSize; var desktopEnd = desktopSize.BottomRight - new Float2(10.0f); if (dialogEnd.X >= desktopEnd.X || dialogEnd.Y >= desktopEnd.Y) - pos = targetControl.PointToScreen(Float2.Zero) - new Float2(10.0f + dialog.Width, dialog.Height); + pos = targetControl.PointToScreen(Float2.Zero) - new Float2(10.0f + dialog.DialogSize.X, dialog.DialogSize.Y); var desktopBounds = Platform.VirtualDesktopBounds; - pos = Float2.Clamp(pos, desktopBounds.UpperLeft, desktopBounds.BottomRight - dialog.Size); + pos = Float2.Clamp(pos, desktopBounds.UpperLeft, desktopBounds.BottomRight - dialog.DialogSize); dialog.RootWindow.Window.Position = pos; // Register for context menu (prevent auto-closing context menu when selecting color) @@ -528,6 +528,7 @@ namespace FlaxEditor.Modules _menuFileSaveAll = cm.AddButton("Save All", inputOptions.Save, Editor.SaveAll); _menuFileSaveScenes = cm.AddButton("Save scenes", inputOptions.SaveScenes, Editor.Scene.SaveScenes); _menuFileCloseScenes = cm.AddButton("Close scenes", inputOptions.CloseScenes, Editor.Scene.CloseAllScenes); + _menuFileReloadScenes = cm.AddButton("Reload scenes", Editor.Scene.ReloadScenes); cm.AddSeparator(); _menuFileOpenScriptsProject = cm.AddButton("Open scripts project", inputOptions.OpenScriptsProject, Editor.CodeEditing.OpenSolution); _menuFileGenerateScriptsProjectFiles = cm.AddButton("Generate scripts project files", inputOptions.GenerateScriptsProject, Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync); @@ -653,7 +654,7 @@ namespace FlaxEditor.Modules cm.AddButton("Information about Flax", () => new AboutDialog().Show()); } - private void OnOptionsChanged(FlaxEditor.Options.EditorOptions options) + private void OnOptionsChanged(EditorOptions options) { var inputOptions = options.Input; @@ -688,6 +689,8 @@ namespace FlaxEditor.Modules _menuToolsTakeScreenshot.ShortKeys = inputOptions.TakeScreenshot.ToString(); MainMenuShortcutKeysUpdated?.Invoke(); + + UpdateToolstrip(); } private void InitToolstrip(RootControl mainWindow) @@ -709,11 +712,11 @@ namespace FlaxEditor.Modules _toolStripScale = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Scale32, () => Editor.MainTransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip($"Change Gizmo tool mode to Scale ({inputOptions.ScaleMode})"); ToolStrip.AddSeparator(); - // Cook scenes + // Build scenes _toolStripBuildScenes = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.Build64, Editor.BuildScenesOrCancel).LinkTooltip($"Build scenes data - CSG, navmesh, static lighting, env probes - configurable via Build Actions in editor options ({inputOptions.BuildScenesData})"); // Cook and run - _toolStripCook = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.ShipIt64, Editor.Windows.GameCookerWin.BuildAndRun).LinkTooltip($"Cook & Run - build game for the current platform and run it locally ({inputOptions.Play})"); + _toolStripCook = (ToolStripButton)ToolStrip.AddButton(Editor.Icons.ShipIt64, Editor.Windows.GameCookerWin.BuildAndRun).LinkTooltip($"Cook & Run - build game for the current platform and run it locally ({inputOptions.CookAndRun})"); _toolStripCook.ContextMenu = new ContextMenu(); _toolStripCook.ContextMenu.AddButton("Run cooked game", Editor.Windows.GameCookerWin.RunCooked); _toolStripCook.ContextMenu.AddSeparator(); @@ -829,6 +832,7 @@ namespace FlaxEditor.Modules _menuFileSaveScenes.Enabled = hasOpenedScene; _menuFileCloseScenes.Enabled = hasOpenedScene; + _menuFileReloadScenes.Enabled = hasOpenedScene; _menuFileGenerateScriptsProjectFiles.Enabled = !Editor.ProgressReporting.GenerateScriptsProjectFiles.IsActive; c.PerformLayout(); diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index d0c8b9747..cda83b56e 100644 --- a/Source/Editor/Modules/WindowsModule.cs +++ b/Source/Editor/Modules/WindowsModule.cs @@ -171,9 +171,13 @@ namespace FlaxEditor.Modules var mainWindow = MainWindow; if (mainWindow) { - var projectPath = Globals.ProjectFolder.Replace('/', '\\'); - var platformBit = Platform.Is64BitApp ? "64" : "32"; - var title = string.Format("Flax Editor - \'{0}\' ({1}-bit)", projectPath, platformBit); + 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}\'"; mainWindow.Title = title; } } @@ -237,7 +241,11 @@ namespace FlaxEditor.Modules /// public void LoadDefaultLayout() { - LoadLayout(StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/LayoutDefault.xml")); + var path = StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/LayoutDefault.xml"); + if (File.Exists(path)) + { + LoadLayout(path); + } } /// @@ -276,9 +284,6 @@ namespace FlaxEditor.Modules // Get metadata int version = int.Parse(root.Attributes["Version"].Value, CultureInfo.InvariantCulture); - var virtualDesktopBounds = Platform.VirtualDesktopBounds; - var virtualDesktopSafeLeftCorner = virtualDesktopBounds.Location; - var virtualDesktopSafeRightCorner = virtualDesktopBounds.BottomRight; switch (version) { @@ -288,31 +293,9 @@ namespace FlaxEditor.Modules if (MainWindow) { var mainWindowNode = root["MainWindow"]; - Rectangle bounds = LoadBounds(mainWindowNode["Bounds"]); - bool isMaximized = bool.Parse(mainWindowNode.GetAttribute("IsMaximized")); - - // Clamp position to match current desktop dimensions (if window was on desktop that is now inactive) - if (bounds.X < virtualDesktopSafeLeftCorner.X || bounds.Y < virtualDesktopSafeLeftCorner.Y || bounds.X > virtualDesktopSafeRightCorner.X || bounds.Y > virtualDesktopSafeRightCorner.Y) - bounds.Location = virtualDesktopSafeLeftCorner; - - if (isMaximized) - { - if (MainWindow.IsMaximized) - MainWindow.Restore(); - MainWindow.ClientPosition = bounds.Location; - MainWindow.Maximize(); - } - else - { - if (Mathf.Min(bounds.Size.X, bounds.Size.Y) >= 1) - { - MainWindow.ClientBounds = bounds; - } - else - { - MainWindow.ClientPosition = bounds.Location; - } - } + bool isMaximized = true, isMinimized = false; + Rectangle bounds = LoadBounds(mainWindowNode, ref isMaximized, ref isMinimized); + LoadWindow(MainWindow, ref bounds, isMaximized, false); } // Load master panel structure @@ -332,11 +315,13 @@ namespace FlaxEditor.Modules continue; // Get window properties - Rectangle bounds = LoadBounds(child["Bounds"]); + bool isMaximized = false, isMinimized = false; + Rectangle bounds = LoadBounds(child, ref isMaximized, ref isMinimized); // Create window and floating dock panel var window = FloatWindowDockPanel.CreateFloatWindow(MainWindow.GUI, bounds.Location, bounds.Size, WindowStartPosition.Manual, string.Empty); var panel = new FloatWindowDockPanel(masterPanel, window.GUI); + LoadWindow(panel.Window.Window, ref bounds, isMaximized, isMinimized); // Load structure LoadPanel(child, panel); @@ -493,23 +478,67 @@ namespace FlaxEditor.Modules private static void SaveBounds(XmlWriter writer, Window win) { - var bounds = win.ClientBounds; - - writer.WriteAttributeString("X", bounds.X.ToString(CultureInfo.InvariantCulture)); - writer.WriteAttributeString("Y", bounds.Y.ToString(CultureInfo.InvariantCulture)); - writer.WriteAttributeString("Width", bounds.Width.ToString(CultureInfo.InvariantCulture)); - writer.WriteAttributeString("Height", bounds.Height.ToString(CultureInfo.InvariantCulture)); + writer.WriteStartElement("Bounds"); + { + var bounds = win.ClientBounds; + writer.WriteAttributeString("X", bounds.X.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("Y", bounds.Y.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("Width", bounds.Width.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("Height", bounds.Height.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("IsMaximized", win.IsMaximized.ToString()); + writer.WriteAttributeString("IsMinimized", win.IsMinimized.ToString()); + } + writer.WriteEndElement(); } - private static Rectangle LoadBounds(XmlElement node) + private static Rectangle LoadBounds(XmlElement node, ref bool isMaximized, ref bool isMinimized) { - float x = float.Parse(node.GetAttribute("X"), CultureInfo.InvariantCulture); - float y = float.Parse(node.GetAttribute("Y"), CultureInfo.InvariantCulture); - float width = float.Parse(node.GetAttribute("Width"), CultureInfo.InvariantCulture); - float height = float.Parse(node.GetAttribute("Height"), CultureInfo.InvariantCulture); + var bounds = node["Bounds"]; + var isMaximizedText = bounds.GetAttribute("IsMaximized"); + if (!string.IsNullOrEmpty(isMaximizedText)) + isMaximized = bool.Parse(isMaximizedText); + var isMinimizedText = bounds.GetAttribute("IsMinimized"); + if (!string.IsNullOrEmpty(isMinimizedText)) + isMinimized = bool.Parse(isMinimizedText); + float x = float.Parse(bounds.GetAttribute("X"), CultureInfo.InvariantCulture); + float y = float.Parse(bounds.GetAttribute("Y"), CultureInfo.InvariantCulture); + float width = float.Parse(bounds.GetAttribute("Width"), CultureInfo.InvariantCulture); + float height = float.Parse(bounds.GetAttribute("Height"), CultureInfo.InvariantCulture); return new Rectangle(x, y, width, height); } + private static void LoadWindow(Window win, ref Rectangle bounds, bool isMaximized, bool isMinimized) + { + var virtualDesktopBounds = Platform.VirtualDesktopBounds; + var virtualDesktopSafeLeftCorner = virtualDesktopBounds.Location; + var virtualDesktopSafeRightCorner = virtualDesktopBounds.BottomRight; + + // Clamp position to match current desktop dimensions (if window was on desktop that is now inactive) + if (bounds.X < virtualDesktopSafeLeftCorner.X || bounds.Y < virtualDesktopSafeLeftCorner.Y || bounds.X > virtualDesktopSafeRightCorner.X || bounds.Y > virtualDesktopSafeRightCorner.Y) + bounds.Location = virtualDesktopSafeLeftCorner; + + if (isMaximized) + { + if (win.IsMaximized) + win.Restore(); + win.ClientPosition = bounds.Location; + win.Maximize(); + } + else + { + if (Mathf.Min(bounds.Size.X, bounds.Size.Y) >= 1) + { + win.ClientBounds = bounds; + } + else + { + win.ClientPosition = bounds.Location; + } + if (isMinimized) + win.Minimize(); + } + } + private class LayoutNameDialog : Dialog { private TextBox _textbox; @@ -609,13 +638,8 @@ namespace FlaxEditor.Modules if (MainWindow) { writer.WriteStartElement("MainWindow"); - writer.WriteAttributeString("IsMaximized", MainWindow.IsMaximized.ToString()); - - writer.WriteStartElement("Bounds"); SaveBounds(writer, MainWindow); writer.WriteEndElement(); - - writer.WriteEndElement(); } // Master panel structure @@ -628,22 +652,13 @@ namespace FlaxEditor.Modules { var panel = masterPanel.FloatingPanels[i]; var window = panel.Window; - if (window == null) - { - Editor.LogWarning("Floating panel has missing window"); continue; - } writer.WriteStartElement("Float"); - SavePanel(writer, panel); - - writer.WriteStartElement("Bounds"); SaveBounds(writer, window.Window); writer.WriteEndElement(); - - writer.WriteEndElement(); } writer.WriteEndElement(); @@ -724,7 +739,6 @@ namespace FlaxEditor.Modules settings.Size = Platform.DesktopSize * 0.75f; settings.StartPosition = WindowStartPosition.CenterScreen; settings.ShowAfterFirstPaint = true; - #if PLATFORM_WINDOWS if (!Editor.Instance.Options.Options.Interface.UseNativeWindowSystem) { @@ -736,12 +750,9 @@ namespace FlaxEditor.Modules #elif PLATFORM_LINUX settings.HasBorder = false; #endif - MainWindow = Platform.CreateWindow(ref settings); - if (MainWindow == null) { - // Error Editor.LogError("Failed to create editor main window!"); return; } diff --git a/Source/Editor/Options/GeneralOptions.cs b/Source/Editor/Options/GeneralOptions.cs index 33124bab0..9f29f4bdb 100644 --- a/Source/Editor/Options/GeneralOptions.cs +++ b/Source/Editor/Options/GeneralOptions.cs @@ -117,7 +117,7 @@ namespace FlaxEditor.Options /// /// Gets or sets the sequence of actions to perform when using Build Scenes button. Can be used to configure this as button (eg. compile code or just update navmesh). /// - [EditorDisplay("General"), EditorOrder(200), Tooltip("The sequence of actions to perform when using Build Scenes button. Can be used to configure this as button (eg. compile code or just update navmesh).")] + [EditorDisplay("General"), EditorOrder(200), ExpandGroups, Tooltip("The sequence of actions to perform when using Build Scenes button. Can be used to configure this as button (eg. compile code or just update navmesh).")] public BuildAction[] BuildActions { get; set; } = { BuildAction.CSG, diff --git a/Source/Editor/Options/InputBinding.cs b/Source/Editor/Options/InputBinding.cs index eb61c0f68..95c3c1d6f 100644 --- a/Source/Editor/Options/InputBinding.cs +++ b/Source/Editor/Options/InputBinding.cs @@ -259,10 +259,7 @@ namespace FlaxEditor.Options public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) - { return true; - } - return base.CanConvertFrom(context, sourceType); } @@ -270,9 +267,7 @@ namespace FlaxEditor.Options public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) - { return false; - } return base.CanConvertTo(context, destinationType); } @@ -284,7 +279,6 @@ namespace FlaxEditor.Options InputBinding.TryParse(str, out var result); return result; } - return base.ConvertFrom(context, culture, value); } @@ -295,7 +289,6 @@ namespace FlaxEditor.Options { return ((InputBinding)value).ToString(); } - return base.ConvertTo(context, culture, value, destinationType); } } diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index 90d098bb6..e2e7d0e71 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -76,6 +76,10 @@ namespace FlaxEditor.Options [EditorDisplay("Common"), EditorOrder(230)] public InputBinding RotateSelection = new InputBinding(KeyboardKeys.R); + [DefaultValue(typeof(InputBinding), "F11")] + [EditorDisplay("Common"), EditorOrder(240)] + public InputBinding ToggleFullscreen = new InputBinding(KeyboardKeys.F11); + #endregion #region File @@ -208,16 +212,20 @@ namespace FlaxEditor.Options [EditorDisplay("Debugger", "Continue"), EditorOrder(810)] public InputBinding DebuggerContinue = new InputBinding(KeyboardKeys.F5); + [DefaultValue(typeof(InputBinding), "Shift+F11")] + [EditorDisplay("Debugger", "Unlock mouse in Play Mode"), EditorOrder(820)] + public InputBinding DebuggerUnlockMouse = new InputBinding(KeyboardKeys.F11, KeyboardKeys.Shift); + [DefaultValue(typeof(InputBinding), "F10")] - [EditorDisplay("Debugger", "Step Over"), EditorOrder(820)] + [EditorDisplay("Debugger", "Step Over"), EditorOrder(830)] public InputBinding DebuggerStepOver = new InputBinding(KeyboardKeys.F10); [DefaultValue(typeof(InputBinding), "F11")] - [EditorDisplay("Debugger", "Step Into"), EditorOrder(830)] + [EditorDisplay("Debugger", "Step Into"), EditorOrder(840)] public InputBinding DebuggerStepInto = new InputBinding(KeyboardKeys.F11); [DefaultValue(typeof(InputBinding), "Shift+F11")] - [EditorDisplay("Debugger", "Step Out"), EditorOrder(840)] + [EditorDisplay("Debugger", "Step Out"), EditorOrder(850)] public InputBinding DebuggerStepOut = new InputBinding(KeyboardKeys.F11, KeyboardKeys.Shift); #endregion diff --git a/Source/Editor/Options/OptionsModule.cs b/Source/Editor/Options/OptionsModule.cs index c2d744239..07e899c5e 100644 --- a/Source/Editor/Options/OptionsModule.cs +++ b/Source/Editor/Options/OptionsModule.cs @@ -244,11 +244,11 @@ namespace FlaxEditor.Options CollectionBackgroundColor = Color.FromBgra(0x14CCCCCC), ProgressNormal = Color.FromBgra(0xFF0ad328), - Statusbar = new Style.StatusbarStyle() + Statusbar = new Style.StatusbarStyle { PlayMode = Color.FromBgra(0xFF2F9135), Failed = Color.FromBgra(0xFF9C2424), - Loading = Color.FromBgra(0xFF2D2D30) + Loading = Color.FromBgra(0xFF2D2D30), }, // Fonts @@ -271,7 +271,7 @@ namespace FlaxEditor.Options Scale = Editor.Icons.Scale32, Scalar = Editor.Icons.Scalar32, - SharedTooltip = new Tooltip() + SharedTooltip = new Tooltip(), }; style.DragWindow = style.BackgroundSelected * 0.7f; diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs index cee63a562..0fd018cbc 100644 --- a/Source/Editor/Options/ViewportOptions.cs +++ b/Source/Editor/Options/ViewportOptions.cs @@ -26,45 +26,108 @@ namespace FlaxEditor.Options public float MouseWheelSensitivity { get; set; } = 1.0f; /// - /// Gets or sets the default movement speed for the viewport camera (must match the dropdown menu values in the viewport). + /// Gets or sets the total amount of steps the camera needs to go from minimum to maximum speed. /// - [DefaultValue(1.0f), Limit(0.01f, 100.0f)] - [EditorDisplay("Defaults"), EditorOrder(110), Tooltip("The default movement speed for the viewport camera (must match the dropdown menu values in the viewport).")] - public float DefaultMovementSpeed { get; set; } = 1.0f; + [DefaultValue(64), Limit(1, 128)] + [EditorDisplay("Camera"), EditorOrder(110), Tooltip("The total amount of steps the camera needs to go from minimum to maximum speed.")] + public int TotalCameraSpeedSteps { get; set; } = 64; + + /// + /// Gets or sets the degree to which the camera will be eased when using camera flight in the editor window. + /// + [DefaultValue(3.0f), Limit(1.0f, 8.0f)] + [EditorDisplay("Camera"), EditorOrder(111), Tooltip("The degree to which the camera will be eased when using camera flight in the editor window (ignored if camera easing degree is enabled).")] + public float CameraEasingDegree { get; set; } = 3.0f; + + /// + /// Gets or sets the default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values). + /// + [DefaultValue(1.0f), Limit(0.05f, 32.0f)] + [EditorDisplay("Defaults"), EditorOrder(120), Tooltip("The default movement speed for the viewport camera (must be in range between minimum and maximum movement speed values).")] + public float MovementSpeed { get; set; } = 1.0f; + + /// + /// Gets or sets the default minimum camera movement speed. + /// + [DefaultValue(0.05f), Limit(0.05f, 32.0f)] + [EditorDisplay("Defaults"), EditorOrder(121), Tooltip("The default minimum movement speed for the viewport camera.")] + public float MinMovementSpeed { get; set; } = 0.05f; + + /// + /// Gets or sets the default maximum camera movement speed. + /// + [DefaultValue(32.0f), Limit(16.0f, 1000.0f)] + [EditorDisplay("Defaults"), EditorOrder(122), Tooltip("The default maximum movement speed for the viewport camera.")] + public float MaxMovementSpeed { get; set; } = 32f; + + /// + /// Gets or sets the default camera easing mode. + /// + [DefaultValue(true)] + [EditorDisplay("Defaults"), EditorOrder(130), Tooltip("The default camera easing mode.")] + public bool UseCameraEasing { get; set; } = true; /// /// Gets or sets the default near clipping plane distance for the viewport camera. /// [DefaultValue(10.0f), Limit(0.001f, 1000.0f)] - [EditorDisplay("Defaults"), EditorOrder(120), Tooltip("The default near clipping plane distance for the viewport camera.")] - public float DefaultNearPlane { get; set; } = 10.0f; + [EditorDisplay("Defaults"), EditorOrder(140), Tooltip("The default near clipping plane distance for the viewport camera.")] + public float NearPlane { get; set; } = 10.0f; /// /// Gets or sets the default far clipping plane distance for the viewport camera. /// [DefaultValue(40000.0f), Limit(10.0f)] - [EditorDisplay("Defaults"), EditorOrder(130), Tooltip("The default far clipping plane distance for the viewport camera.")] - public float DefaultFarPlane { get; set; } = 40000.0f; + [EditorDisplay("Defaults"), EditorOrder(150), Tooltip("The default far clipping plane distance for the viewport camera.")] + public float FarPlane { get; set; } = 40000.0f; /// /// Gets or sets the default field of view angle (in degrees) for the viewport camera. /// [DefaultValue(60.0f), Limit(35.0f, 160.0f, 0.1f)] - [EditorDisplay("Defaults", "Default Field Of View"), EditorOrder(140), Tooltip("The default field of view angle (in degrees) for the viewport camera.")] - public float DefaultFieldOfView { get; set; } = 60.0f; + [EditorDisplay("Defaults"), EditorOrder(160), Tooltip("The default field of view angle (in degrees) for the viewport camera.")] + public float FieldOfView { get; set; } = 60.0f; /// - /// Gets or sets if the panning direction is inverted for the viewport camera. + /// Gets or sets the default camera orthographic mode. /// [DefaultValue(false)] - [EditorDisplay("Defaults"), EditorOrder(150), Tooltip("Invert the panning direction for the viewport camera.")] - public bool DefaultInvertPanning { get; set; } = false; + [EditorDisplay("Defaults"), EditorOrder(170), Tooltip("The default camera orthographic mode.")] + public bool UseOrthographicProjection { get; set; } = false; /// - /// Scales editor viewport grid. + /// Gets or sets the default camera orthographic scale (if camera uses orthographic mode). + /// + [DefaultValue(5.0f), Limit(0.001f, 100000.0f, 0.1f)] + [EditorDisplay("Defaults"), EditorOrder(180), Tooltip("The default camera orthographic scale (if camera uses orthographic mode).")] + public float OrthographicScale { get; set; } = 5.0f; + + /// + /// Gets or sets the default panning direction for the viewport camera. + /// + [DefaultValue(false)] + [EditorDisplay("Defaults"), EditorOrder(190), Tooltip("The default panning direction for the viewport camera.")] + public bool InvertPanning { get; set; } = false; + + /// + /// Gets or sets the default relative panning mode. + /// + [DefaultValue(true)] + [EditorDisplay("Defaults"), EditorOrder(200), Tooltip("The default relative panning mode. Uses distance between camera and target to determine panning speed.")] + public bool UseRelativePanning { get; set; } = true; + + /// + /// Gets or sets the default panning speed (ignored if relative panning is speed enabled). + /// + [DefaultValue(0.8f), Limit(0.01f, 128.0f, 0.1f)] + [EditorDisplay("Defaults"), EditorOrder(210), Tooltip("The default camera panning speed (ignored if relative panning is enabled).")] + public float PanningSpeed { get; set; } = 0.8f; + + /// + /// Gets or sets the default editor viewport grid scale. /// [DefaultValue(50.0f), Limit(25.0f, 500.0f, 5.0f)] - [EditorDisplay("Defaults"), EditorOrder(160), Tooltip("Scales editor viewport grid.")] + [EditorDisplay("Defaults"), EditorOrder(220), Tooltip("The default editor viewport grid scale.")] public float ViewportGridScale { get; set; } = 50.0f; } } diff --git a/Source/Editor/Progress/Handlers/ImportAssetsProgress.cs b/Source/Editor/Progress/Handlers/ImportAssetsProgress.cs index 7bbefbc2d..fa1e943f0 100644 --- a/Source/Editor/Progress/Handlers/ImportAssetsProgress.cs +++ b/Source/Editor/Progress/Handlers/ImportAssetsProgress.cs @@ -19,25 +19,25 @@ namespace FlaxEditor.Progress.Handlers public ImportAssetsProgress() { var importing = Editor.Instance.ContentImporting; - importing.ImportingQueueBegin += OnStart; - importing.ImportingQueueEnd += OnEnd; + importing.ImportingQueueBegin += () => FlaxEngine.Scripting.InvokeOnUpdate(OnStart); + importing.ImportingQueueEnd += () => FlaxEngine.Scripting.InvokeOnUpdate(OnEnd); importing.ImportFileBegin += OnImportFileBegin; } private void OnImportFileBegin(IFileEntryAction importFileEntry) { + string info; if (importFileEntry is ImportFileEntry) - _currentInfo = string.Format("Importing \'{0}\'", System.IO.Path.GetFileName(importFileEntry.SourceUrl)); + info = string.Format("Importing \'{0}\'", System.IO.Path.GetFileName(importFileEntry.SourceUrl)); else - _currentInfo = string.Format("Creating \'{0}\'", importFileEntry.SourceUrl); - UpdateProgress(); - } - - private void UpdateProgress() - { - var importing = Editor.Instance.ContentImporting; - var info = string.Format("{0} ({1}/{2})...", _currentInfo, importing.ImportBatchDone, importing.ImportBatchSize); - OnUpdate(importing.ImportingProgress, info); + info = string.Format("Creating \'{0}\'", importFileEntry.SourceUrl); + FlaxEngine.Scripting.InvokeOnUpdate(() => + { + _currentInfo = info; + var importing = Editor.Instance.ContentImporting; + var text = string.Format("{0} ({1}/{2})...", _currentInfo, importing.ImportBatchDone, importing.ImportBatchSize); + OnUpdate(importing.ImportingProgress, text); + }); } } } diff --git a/Source/Editor/ProjectInfo.cpp b/Source/Editor/ProjectInfo.cpp index 4e7ee4483..30c558b5d 100644 --- a/Source/Editor/ProjectInfo.cpp +++ b/Source/Editor/ProjectInfo.cpp @@ -154,7 +154,8 @@ bool ProjectInfo::LoadProject(const String& projectPath) Version = ::Version( JsonTools::GetInt(version, "Major", 0), JsonTools::GetInt(version, "Minor", 0), - JsonTools::GetInt(version, "Build", 0)); + JsonTools::GetInt(version, "Build", -1), + JsonTools::GetInt(version, "Revision", -1)); } } if (Version.Revision() == 0) diff --git a/Source/Editor/ProjectInfo.cs b/Source/Editor/ProjectInfo.cs index b00c4e042..083665f0f 100644 --- a/Source/Editor/ProjectInfo.cs +++ b/Source/Editor/ProjectInfo.cs @@ -23,17 +23,11 @@ namespace FlaxEditor public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { if (value == null) - { writer.WriteNull(); - } else if (value is Version) - { writer.WriteValue(value.ToString()); - } else - { throw new JsonSerializationException("Expected Version object value"); - } } /// @@ -47,65 +41,60 @@ namespace FlaxEditor public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) - { return null; - } - else + + if (reader.TokenType == JsonToken.StartObject) { - if (reader.TokenType == JsonToken.StartObject) + try { - try + reader.Read(); + var values = new Dictionary(); + while (reader.TokenType == JsonToken.PropertyName) { + var key = reader.Value as string; reader.Read(); - Dictionary values = new Dictionary(); - while (reader.TokenType == JsonToken.PropertyName) - { - var key = reader.Value as string; - reader.Read(); - var val = (long)reader.Value; - reader.Read(); - values.Add(key, (int)val); - } + var val = (long)reader.Value; + reader.Read(); + values.Add(key, (int)val); + } - int major = 0, minor = 0, build = 0; - values.TryGetValue("Major", out major); - values.TryGetValue("Minor", out minor); - values.TryGetValue("Build", out build); + values.TryGetValue("Major", out var major); + values.TryGetValue("Minor", out var minor); + if (!values.TryGetValue("Build", out var build)) + build = -1; + if (!values.TryGetValue("Revision", out var revision)) + revision = -1; - Version v = new Version(major, minor, build); - return v; - } - catch (Exception ex) - { - throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex); - } + if (build <= 0) + return new Version(major, minor); + if (revision <= 0) + return new Version(major, minor, build); + return new Version(major, minor, build, revision); } - else if (reader.TokenType == JsonToken.String) + catch (Exception ex) { - try - { - Version v = new Version((string)reader.Value!); - return v; - } - catch (Exception ex) - { - throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex); - } - } - else - { - throw new Exception(String.Format("Unexpected token or value when parsing version. Token: {0}, Value: {1}", reader.TokenType, reader.Value)); + throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex); } } + if (reader.TokenType == JsonToken.String) + { + try + { + return new Version((string)reader.Value!); + } + catch (Exception ex) + { + throw new Exception(String.Format("Error parsing version string: {0}", reader.Value), ex); + } + } + throw new Exception(String.Format("Unexpected token or value when parsing version. Token: {0}, Value: {1}", reader.TokenType, reader.Value)); } /// /// Determines whether this instance can convert the specified object type. /// /// Type of the object. - /// - /// true if this instance can convert the specified object type; otherwise, false. - /// + /// true if this instance can convert the specified object type; otherwise, false. public override bool CanConvert(Type objectType) { return objectType == typeof(Version); diff --git a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs index c09e1a246..ed6fc08d6 100644 --- a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs +++ b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs @@ -7,6 +7,7 @@ using Real = System.Single; #endif using FlaxEngine; +using FlaxEngine.GUI; namespace FlaxEditor.SceneGraph.Actors { @@ -30,6 +31,13 @@ namespace FlaxEditor.SceneGraph.Actors // Rotate to match the space (GUI uses upper left corner as a root) Actor.LocalOrientation = Quaternion.Euler(0, -180, -180); + var uiControl = new UIControl + { + Name = "Canvas Scalar", + Transform = Actor.Transform, + Control = new CanvasScaler() + }; + Root.Spawn(uiControl, Actor); } /// diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index d392e8309..f64e46385 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -66,7 +66,8 @@ namespace FlaxEditor.SceneGraph.GUI _orderInParent = actor.OrderInParent; Visible = (actor.HideFlags & HideFlags.HideInHierarchy) == 0; - var id = actor.ID; + // Pick the correct id when inside a prefab window. + var id = actor.HasPrefabLink && actor.Scene == null ? actor.PrefabObjectID : actor.ID; if (Editor.Instance.ProjectCache.IsExpandedActor(ref id)) { Expand(true); @@ -171,7 +172,8 @@ namespace FlaxEditor.SceneGraph.GUI // Restore cached state on query filter clear if (noFilter && actor != null) { - var id = actor.ID; + // Pick the correct id when inside a prefab window. + var id = actor.HasPrefabLink && actor.Scene.Scene == null ? actor.PrefabObjectID : actor.ID; isExpanded = Editor.Instance.ProjectCache.IsExpandedActor(ref id); } @@ -264,7 +266,7 @@ namespace FlaxEditor.SceneGraph.GUI /// /// Starts the actor renaming action. /// - public void StartRenaming(EditorWindow window) + public void StartRenaming(EditorWindow window, Panel treePanel = null) { // Block renaming during scripts reload if (Editor.Instance.ProgressReporting.CompileScripts.IsActive) @@ -279,7 +281,13 @@ namespace FlaxEditor.SceneGraph.GUI (window as PrefabWindow).ScrollingOnTreeView(false); // Start renaming the actor - var dialog = RenamePopup.Show(this, TextRect, _actorNode.Name, false); + var rect = TextRect; + if (treePanel != null) + { + treePanel.ScrollViewTo(this, true); + rect.Size = new Float2(treePanel.Width - TextRect.Location.X, TextRect.Height); + } + var dialog = RenamePopup.Show(this, rect, _actorNode.Name, false); dialog.Renamed += OnRenamed; dialog.Closed += popup => { @@ -301,10 +309,12 @@ namespace FlaxEditor.SceneGraph.GUI protected override void OnExpandedChanged() { base.OnExpandedChanged(); + var actor = Actor; - if (!IsLayoutLocked && Actor) + if (!IsLayoutLocked && actor) { - var id = Actor.ID; + // Pick the correct id when inside a prefab window. + var id = actor.HasPrefabLink && actor.Scene == null ? actor.PrefabObjectID : actor.ID; Editor.Instance.ProjectCache.SetExpandedActor(ref id, IsExpanded); } } diff --git a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp index 7f5ca6f17..de0a30e40 100644 --- a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp +++ b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp @@ -207,9 +207,9 @@ void RiderCodeEditor::FindEditors(Array* output) FileSystem::GetChildDirectories(subDirectories, TEXT("/opt/")); // Versions installed via JetBrains Toolbox - SearchDirectory(&installations, localAppDataPath / TEXT(".local/share/JetBrains/Toolbox/apps/rider/")); - FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT(".local/share/JetBrains/Toolbox/apps/Rider/ch-0")); - FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT(".local/share/JetBrains/Toolbox/apps/Rider/ch-1")); // Beta versions + SearchDirectory(&installations, localAppDataPath / TEXT("JetBrains/Toolbox/apps/rider/")); + FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains/Toolbox/apps/Rider/ch-0")); + FileSystem::GetChildDirectories(subDirectories, localAppDataPath / TEXT("JetBrains/Toolbox/apps/Rider/ch-1")); // Beta versions // Detect Flatpak installations SearchDirectory(&installations, diff --git a/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioConnection.cpp b/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioConnection.cpp index 2357fd241..a65b3ec9e 100644 --- a/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioConnection.cpp +++ b/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioConnection.cpp @@ -286,81 +286,17 @@ namespace VisualStudio return "Visual Studio open timout"; } - static ComPtr FindItem(const ComPtr& projectItems, BSTR filePath) - { - long count; - projectItems->get_Count(&count); - if (count == 0) - return nullptr; - - for (long i = 1; i <= count; i++) // They are counting from [1..Count] - { - ComPtr projectItem; - projectItems->Item(_variant_t(i), &projectItem); - if (!projectItem) - continue; - - short fileCount = 0; - projectItem->get_FileCount(&fileCount); - for (short fileIndex = 1; fileIndex <= fileCount; fileIndex++) - { - _bstr_t filename; - projectItem->get_FileNames(fileIndex, filename.GetAddress()); - - if (filename.GetBSTR() != nullptr && AreFilePathsEqual(filePath, filename)) - { - return projectItem; - } - } - - ComPtr childProjectItems; - projectItem->get_ProjectItems(&childProjectItems); - if (childProjectItems) - { - ComPtr result = FindItem(childProjectItems, filePath); - if (result) - return result; - } - } - - return nullptr; - } - - static ComPtr FindItem(const ComPtr& solution, BSTR filePath) - { + static ComPtr FindItem(const ComPtr& solution, BSTR filePath) + { HRESULT result; - ComPtr projects; - result = solution->get_Projects(&projects); + ComPtr projectItem; + result = solution->FindProjectItem(filePath, &projectItem); if (FAILED(result)) return nullptr; - long projectsCount = 0; - result = projects->get_Count(&projectsCount); - if (FAILED(result)) - return nullptr; - - for (long projectIndex = 1; projectIndex <= projectsCount; projectIndex++) // They are counting from [1..Count] - { - ComPtr project; - result = projects->Item(_variant_t(projectIndex), &project); - if (FAILED(result) || !project) - continue; - - ComPtr projectItems; - result = project->get_ProjectItems(&projectItems); - if (FAILED(result) || !projectItems) - continue; - - auto projectItem = FindItem(projectItems, filePath); - if (projectItem) - { - return projectItem; - } - } - - return nullptr; - } + return projectItem; + } // Opens a file on a specific line in a running Visual Studio instance. // diff --git a/Source/Editor/Scripting/ScriptsBuilder.cpp b/Source/Editor/Scripting/ScriptsBuilder.cpp index 8807379cf..0b3cf6452 100644 --- a/Source/Editor/Scripting/ScriptsBuilder.cpp +++ b/Source/Editor/Scripting/ScriptsBuilder.cpp @@ -170,7 +170,7 @@ bool ScriptsBuilder::IsSourceWorkspaceDirty() bool ScriptsBuilder::IsSourceDirtyFor(const TimeSpan& timeout) { ScopeLock scopeLock(_locker); - return _lastSourceCodeEdited > (_lastCompileAction + timeout); + return _lastSourceCodeEdited > _lastCompileAction && DateTime::Now() > _lastSourceCodeEdited + timeout; } bool ScriptsBuilder::IsCompiling() @@ -266,7 +266,7 @@ bool ScriptsBuilder::RunBuildTool(const StringView& args, const StringView& work bool ScriptsBuilder::GenerateProject(const StringView& customArgs) { - String args(TEXT("-log -genproject ")); + String args(TEXT("-log -mutex -genproject ")); args += customArgs; _wasProjectStructureChanged = false; return RunBuildTool(args); @@ -669,7 +669,7 @@ void ScriptsBuilderService::Update() } // Check if compile code (if has been edited) - const TimeSpan timeToCallCompileIfDirty = TimeSpan::FromMilliseconds(50); + const TimeSpan timeToCallCompileIfDirty = TimeSpan::FromMilliseconds(150); auto mainWindow = Engine::MainWindow; if (ScriptsBuilder::IsSourceDirtyFor(timeToCallCompileIfDirty) && mainWindow && mainWindow->IsFocused()) { diff --git a/Source/Editor/Scripting/ScriptsBuilder.h b/Source/Editor/Scripting/ScriptsBuilder.h index f954b0fd0..0a11bd9b7 100644 --- a/Source/Editor/Scripting/ScriptsBuilder.h +++ b/Source/Editor/Scripting/ScriptsBuilder.h @@ -68,7 +68,7 @@ public: /// /// Time to use for checking. /// True if source code is dirty, otherwise false. - static bool IsSourceDirtyFor(const TimeSpan& timeout); + API_FUNCTION() static bool IsSourceDirtyFor(const TimeSpan& timeout); /// /// Returns true if scripts are being now compiled/reloaded. diff --git a/Source/Editor/States/LoadingState.cs b/Source/Editor/States/LoadingState.cs index 698dc192f..d48918e1b 100644 --- a/Source/Editor/States/LoadingState.cs +++ b/Source/Editor/States/LoadingState.cs @@ -56,12 +56,14 @@ namespace FlaxEditor.States else if (Editor.Options.Options.General.ForceScriptCompilationOnStartup && !skipCompile) { // Generate project files when Cache is missing or was cleared previously - if (!Directory.Exists(Path.Combine(Editor.GameProject?.ProjectFolderPath, "Cache", "Intermediate")) || - !Directory.Exists(Path.Combine(Editor.GameProject?.ProjectFolderPath, "Cache", "Projects"))) + var projectFolderPath = Editor.GameProject?.ProjectFolderPath; + if (!Directory.Exists(Path.Combine(projectFolderPath, "Cache", "Intermediate")) || + !Directory.Exists(Path.Combine(projectFolderPath, "Cache", "Projects"))) { - var customArgs = Editor.Instance.CodeEditing.SelectedEditor.GenerateProjectCustomArgs; + var customArgs = Editor.CodeEditing.SelectedEditor?.GenerateProjectCustomArgs; ScriptsBuilder.GenerateProject(customArgs); } + // Compile scripts before loading any scenes, then we load them and can open scenes ScriptsBuilder.Compile(); } diff --git a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs index 9773e9695..7c3aa4f13 100644 --- a/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs +++ b/Source/Editor/Surface/Archetypes/Animation.MultiBlend.cs @@ -465,7 +465,7 @@ namespace FlaxEditor.Surface.Archetypes if (selectedIndex != -1) { var index = 5 + selectedIndex * 2; - SetValue(index, _animationPicker.SelectedID); + SetValue(index, _animationPicker.Validator.SelectedID); } } @@ -495,7 +495,7 @@ namespace FlaxEditor.Surface.Archetypes { if (isValid) { - _animationPicker.SelectedID = data1; + _animationPicker.Validator.SelectedID = data1; _animationSpeed.Value = data0.W; var path = string.Empty; @@ -505,7 +505,7 @@ namespace FlaxEditor.Surface.Archetypes } else { - _animationPicker.SelectedID = Guid.Empty; + _animationPicker.Validator.SelectedID = Guid.Empty; _animationSpeed.Value = 1.0f; } _animationPicker.Enabled = isValid; diff --git a/Source/Editor/Surface/Archetypes/Bitwise.cs b/Source/Editor/Surface/Archetypes/Bitwise.cs index 06f719adc..43dcb91b5 100644 --- a/Source/Editor/Surface/Archetypes/Bitwise.cs +++ b/Source/Editor/Surface/Archetypes/Bitwise.cs @@ -60,7 +60,7 @@ namespace FlaxEditor.Surface.Archetypes Op1(1, "Bitwise NOT", "Negates the value using bitwise operation", new[] { "!", "~" }), Op2(2, "Bitwise AND", "Performs a bitwise conjunction on two values", new[] { "&" }), Op2(3, "Bitwise OR", "", new[] { "|" }), - Op2(4, "Bitwise XOR", ""), + Op2(4, "Bitwise XOR", "", new[] { "^" }), }; } } diff --git a/Source/Editor/Surface/Archetypes/Boolean.cs b/Source/Editor/Surface/Archetypes/Boolean.cs index 153b2fead..ed97a9642 100644 --- a/Source/Editor/Surface/Archetypes/Boolean.cs +++ b/Source/Editor/Surface/Archetypes/Boolean.cs @@ -60,7 +60,7 @@ namespace FlaxEditor.Surface.Archetypes Op1(1, "Boolean NOT", "Negates the boolean value", new[] { "!", "~" }), Op2(2, "Boolean AND", "Performs a logical conjunction on two values", new[] { "&&" }), Op2(3, "Boolean OR", "Returns true if either (or both) of its operands is true", new[] { "||" }), - Op2(4, "Boolean XOR", ""), + Op2(4, "Boolean XOR", "", new [] { "^" } ), Op2(5, "Boolean NOR", ""), Op2(6, "Boolean NAND", ""), }; diff --git a/Source/Editor/Surface/Archetypes/Comparisons.cs b/Source/Editor/Surface/Archetypes/Comparisons.cs index a0efbd92f..b72111c46 100644 --- a/Source/Editor/Surface/Archetypes/Comparisons.cs +++ b/Source/Editor/Surface/Archetypes/Comparisons.cs @@ -14,7 +14,7 @@ namespace FlaxEditor.Surface.Archetypes [HideInEditor] public static class Comparisons { - private static NodeArchetype Op(ushort id, string title, string desc) + private static NodeArchetype Op(ushort id, string title, string desc, string[] altTitles = null) { return new NodeArchetype { @@ -22,6 +22,7 @@ namespace FlaxEditor.Surface.Archetypes Title = title, Description = desc, Flags = NodeFlags.AllGraphs, + AlternativeTitles = altTitles, ConnectionsHints = ConnectionsHint.Value, Size = new Float2(100, 40), IndependentBoxes = new[] @@ -170,12 +171,12 @@ namespace FlaxEditor.Surface.Archetypes /// public static NodeArchetype[] Nodes = { - Op(1, "==", "Determines whether two values are equal"), - Op(2, "!=", "Determines whether two values are not equal"), - Op(3, ">", "Determines whether the first value is greater than the other"), - Op(4, "<", "Determines whether the first value is less than the other"), - Op(5, "<=", "Determines whether the first value is less or equal to the other"), - Op(6, ">=", "Determines whether the first value is greater or equal to the other"), + Op(1, "==", "Determines whether two values are equal", new[] { "equals" }), + Op(2, "!=", "Determines whether two values are not equal", new[] { "not equals" }), + Op(3, ">", "Determines whether the first value is greater than the other", new[] { "greater than", "larger than", "bigger than" }), + Op(4, "<", "Determines whether the first value is less than the other", new[] { "less than", "smaller than" }), + Op(5, "<=", "Determines whether the first value is less or equal to the other", new[] { "less equals than", "smaller equals than" }), + Op(6, ">=", "Determines whether the first value is greater or equal to the other", new[] { "greater equals than", "larger equals than", "bigger equals than" }), new NodeArchetype { TypeID = 7, diff --git a/Source/Editor/Surface/Archetypes/Constants.cs b/Source/Editor/Surface/Archetypes/Constants.cs index cbe7822e3..e58d917d1 100644 --- a/Source/Editor/Surface/Archetypes/Constants.cs +++ b/Source/Editor/Surface/Archetypes/Constants.cs @@ -7,11 +7,12 @@ using Real = System.Single; #endif using System; -using System.Reflection; +using System.Linq; using FlaxEditor.CustomEditors.Editors; using FlaxEditor.GUI; using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; +using FlaxEditor.Surface.Undo; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -24,6 +25,109 @@ namespace FlaxEditor.Surface.Archetypes [HideInEditor] public static class Constants { + /// + /// A special type of node that adds the functionality to convert nodes to parameters. + /// + internal class ConvertToParameterNode : SurfaceNode + { + private readonly ScriptType _type; + private readonly Func _convertFunction; + + /// + public ConvertToParameterNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch, ScriptType type, Func convertFunction = null) + : base(id, context, nodeArch, groupArch) + { + _type = type; + _convertFunction = convertFunction; + } + + /// + public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) + { + base.OnShowSecondaryContextMenu(menu, location); + + menu.AddSeparator(); + menu.AddButton("Convert to Parameter", OnConvertToParameter); + } + + private void OnConvertToParameter() + { + if (Surface.Owner is not IVisjectSurfaceWindow window) + throw new Exception("Surface owner is not a Visject Surface Window"); + + Asset asset = Surface.Owner.SurfaceAsset; + if (asset == null || !asset.IsLoaded) + { + Editor.LogError("Asset is null or not loaded"); + return; + } + + // Add parameter to editor + var paramIndex = Surface.Parameters.Count; + var initValue = _convertFunction == null ? Values[0] : _convertFunction.Invoke(Values); + var paramAction = new AddRemoveParamAction + { + Window = window, + IsAdd = true, + Name = Utilities.Utils.IncrementNameNumber("New parameter", OnParameterRenameValidate), + Type = _type, + Index = paramIndex, + InitValue = initValue, + }; + paramAction.Do(); + Surface.AddBatchedUndoAction(paramAction); + + // Spawn Get Parameter Node based on the added parameter + Guid parameterGuid = Surface.Parameters[paramIndex].ID; + bool undoEnabled = Surface.Undo.Enabled; + Surface.Undo.Enabled = false; + NodeArchetype arch = Surface.GetParameterGetterNodeArchetype(out var groupId); + SurfaceNode node = Surface.Context.SpawnNode(groupId, arch.TypeID, Location, new object[] { parameterGuid }); + Surface.Undo.Enabled = undoEnabled; + if (node is not Parameters.SurfaceNodeParamsGet getNode) + throw new Exception("Node is not a ParamsGet node!"); + Surface.AddBatchedUndoAction(new AddRemoveNodeAction(getNode, true)); + + // Recreate connections of constant node + // Constant nodes and parameter nodes should have the same ports, so we can just iterate through the connections + var editConnectionsAction1 = new EditNodeConnections(Context, this); + var editConnectionsAction2 = new EditNodeConnections(Context, node); + var boxes = GetBoxes(); + for (int i = 0; i < boxes.Count; i++) + { + Box box = boxes[i]; + if (!box.HasAnyConnection) + continue; + if (!getNode.TryGetBox(box.ID, out Box paramBox)) + continue; + + // Iterating backwards, because the CreateConnection method deletes entries from the box connections when target box IsSingle is set to true + for (int k = box.Connections.Count - 1; k >= 0; k--) + { + Box connectedBox = box.Connections[k]; + paramBox.CreateConnection(connectedBox); + } + } + editConnectionsAction1.End(); + editConnectionsAction2.End(); + Surface.AddBatchedUndoAction(editConnectionsAction1); + Surface.AddBatchedUndoAction(editConnectionsAction2); + + // Add undo actions and remove constant node + var removeConstantAction = new AddRemoveNodeAction(this, false); + Surface.AddBatchedUndoAction(removeConstantAction); + removeConstantAction.Do(); + Surface.MarkAsEdited(); + } + + private bool OnParameterRenameValidate(string value) + { + if (Surface.Owner is not IVisjectSurfaceWindow window) + throw new Exception("Surface owner is not a Visject Surface Window"); + return !string.IsNullOrWhiteSpace(value) && window.VisjectSurface.Parameters.All(x => x.Name != value); + } + } + private class EnumNode : SurfaceNode { private EnumComboBox _picker; @@ -356,6 +460,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 1, Title = "Bool", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(bool))), Description = "Constant boolean value", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -388,6 +493,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 2, Title = "Integer", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(int))), Description = "Constant integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -415,6 +521,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 3, Title = "Float", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(float))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -442,6 +549,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Title = "Float2", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Float2))), Description = "Constant Float2", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 60), @@ -472,6 +580,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 5, Title = "Float3", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Float3))), Description = "Constant Float3", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 80), @@ -504,6 +613,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 6, Title = "Float4", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Float4))), Description = "Constant Float4", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 100), @@ -538,6 +648,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 7, Title = "Color", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Color))), Description = "RGBA color", Flags = NodeFlags.AllGraphs, Size = new Float2(70, 100), @@ -570,6 +681,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 8, Title = "Rotation", + Create = (id, context, arch, groupArch) => + new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Quaternion)), values => Quaternion.Euler((float)values[0], (float)values[1], (float)values[2])), Description = "Euler angle rotation", Flags = NodeFlags.AnimGraph | NodeFlags.VisualScriptGraph | NodeFlags.ParticleEmitterGraph, Size = new Float2(110, 60), @@ -594,6 +707,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 9, Title = "String", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(string))), Description = "Text", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph, Size = new Float2(200, 20), @@ -644,6 +758,8 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 12, Title = "Unsigned Integer", + AlternativeTitles = new[] { "UInt", "U Int" }, + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(uint))), Description = "Constant unsigned integer value", Flags = NodeFlags.AllGraphs, Size = new Float2(170, 20), @@ -683,6 +799,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 15, Title = "Double", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(double))), Description = "Constant floating point", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -700,6 +817,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 16, Title = "Vector2", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Vector2))), Description = "Constant Vector2", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 60), @@ -720,6 +838,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 17, Title = "Vector3", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Vector3))), Description = "Constant Vector3", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 80), @@ -742,6 +861,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 18, Title = "Vector4", + Create = (id, context, arch, groupArch) => new ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Vector4))), Description = "Constant Vector4", Flags = NodeFlags.AllGraphs, Size = new Float2(130, 100), diff --git a/Source/Editor/Surface/Archetypes/Function.cs b/Source/Editor/Surface/Archetypes/Function.cs index 7d12a0625..53950dad2 100644 --- a/Source/Editor/Surface/Archetypes/Function.cs +++ b/Source/Editor/Surface/Archetypes/Function.cs @@ -95,7 +95,7 @@ namespace FlaxEditor.Surface.Archetypes private void OnAssetPickerSelectedItemChanged() { - SetValue(0, _assetPicker.SelectedID); + SetValue(0, _assetPicker.Validator.SelectedID); } private void TryRestoreConnections(Box box, Box[] prevBoxes, ref NodeElementArchetype arch) @@ -133,7 +133,7 @@ namespace FlaxEditor.Surface.Archetypes var prevOutputs = _outputs; // Extract function signature parameters (inputs and outputs packed) - _asset = LoadSignature(_assetPicker.SelectedID, out var typeNames, out var names); + _asset = LoadSignature(_assetPicker.Validator.SelectedID, out var typeNames, out var names); if (typeNames != null && names != null) { var types = new Type[typeNames.Length]; @@ -174,7 +174,7 @@ namespace FlaxEditor.Surface.Archetypes _outputs[i] = box; } - Title = _assetPicker.SelectedItem.ShortName; + Title = _assetPicker.Validator.SelectedItem.ShortName; } else { @@ -2470,6 +2470,7 @@ namespace FlaxEditor.Surface.Archetypes Title = string.Empty, Description = "Overrides the base class method with custom implementation", Flags = NodeFlags.VisualScriptGraph | NodeFlags.NoSpawnViaGUI | NodeFlags.NoSpawnViaPaste, + SortScore = 10, IsInputCompatible = MethodOverrideNode.IsInputCompatible, IsOutputCompatible = MethodOverrideNode.IsOutputCompatible, Size = new Float2(240, 60), diff --git a/Source/Editor/Surface/Archetypes/Layers.cs b/Source/Editor/Surface/Archetypes/Layers.cs index 4a46276c3..0308cab15 100644 --- a/Source/Editor/Surface/Archetypes/Layers.cs +++ b/Source/Editor/Surface/Archetypes/Layers.cs @@ -176,7 +176,7 @@ namespace FlaxEditor.Surface.Archetypes Size = new Float2(200, 100), DefaultValues = new object[] { - 0.0f, + 0.5f, }, Elements = new[] { diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index b85d1c9d4..f65804023 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -882,6 +882,60 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Output(1, "Inv Size", typeof(Float2), 1), } }, + new NodeArchetype + { + TypeID = 40, + Title = "Rectangle Mask", + Description = "Creates a rectangle mask", + Flags = NodeFlags.MaterialGraph, + Size = new Float2(150, 40), + ConnectionsHints = ConnectionsHint.Vector, + DefaultValues = new object[] + { + new Float2(0.5f, 0.5f), + }, + Elements = new[] + { + NodeElementArchetype.Factory.Input(0, "UV", true, typeof(Float2), 0), + NodeElementArchetype.Factory.Input(1, "Rectangle", true, typeof(Float2), 1, 0), + NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 2), + } + }, + new NodeArchetype + { + TypeID = 41, + Title = "FWidth", + Description = "Creates a partial derivative (fwidth)", + Flags = NodeFlags.MaterialGraph, + Size = new Float2(150, 20), + ConnectionsHints = ConnectionsHint.Numeric, + IndependentBoxes = new[] { 0 }, + DependentBoxes = new[] { 1 }, + Elements = new[] + { + NodeElementArchetype.Factory.Input(0, "Value", true, null, 0), + NodeElementArchetype.Factory.Output(0, string.Empty, null, 1), + } + }, + new NodeArchetype + { + TypeID = 42, + Title = "AA Step", + Description = "Smooth version of step function with less aliasing", + Flags = NodeFlags.MaterialGraph, + Size = new Float2(150, 40), + ConnectionsHints = ConnectionsHint.Vector, + DefaultValues = new object[] + { + 0.5f + }, + Elements = new[] + { + NodeElementArchetype.Factory.Input(0, "Value", true, typeof(float), 0), + NodeElementArchetype.Factory.Input(1, "Gradient", true, typeof(float), 1, 0), + NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 2), + } + }, }; } } diff --git a/Source/Editor/Surface/Archetypes/Math.cs b/Source/Editor/Surface/Archetypes/Math.cs index fe2f1e044..7f4afa6a6 100644 --- a/Source/Editor/Surface/Archetypes/Math.cs +++ b/Source/Editor/Surface/Archetypes/Math.cs @@ -13,6 +13,11 @@ namespace FlaxEditor.Surface.Archetypes public static class Math { private static NodeArchetype Op1(ushort id, string title, string desc, ConnectionsHint hints = ConnectionsHint.Numeric, Type type = null) + { + return Op1(id, title, desc, null, hints, type); + } + + private static NodeArchetype Op1(ushort id, string title, string desc, string[] altTitles, ConnectionsHint hints = ConnectionsHint.Numeric, Type type = null) { return new NodeArchetype { @@ -20,6 +25,7 @@ namespace FlaxEditor.Surface.Archetypes Title = title, Description = desc, Flags = NodeFlags.AllGraphs, + AlternativeTitles = altTitles, Size = new Float2(110, 20), DefaultType = new ScriptType(type), ConnectionsHints = hints, @@ -92,6 +98,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 11, Title = "Length", + AlternativeTitles = new[] { "Magnitude", "Mag" }, Description = "Returns the length of A vector", Flags = NodeFlags.AllGraphs, Size = new Float2(110, 20), @@ -107,10 +114,10 @@ namespace FlaxEditor.Surface.Archetypes Op1(13, "Round", "Rounds A to the nearest integer"), Op1(14, "Saturate", "Clamps A to the range [0, 1]"), Op1(15, "Sine", "Returns sine of A"), - Op1(16, "Sqrt", "Returns square root of A"), + Op1(16, "Sqrt", "Returns square root of A", new [] { "Square Root", "Square", "Root" }), Op1(17, "Tangent", "Returns tangent of A"), Op2(18, "Cross", "Returns the cross product of A and B", ConnectionsHint.None, typeof(Float3)), - Op2(19, "Distance", "Returns a distance scalar between A and B", ConnectionsHint.Vector, null, typeof(float), false), + Op2(19, "Distance", "Returns a distance scalar between A and B", new [] { "Magnitude", "Mag", "Length" }, ConnectionsHint.Vector, null, typeof(float), false), Op2(20, "Dot", "Returns the dot product of A and B", ConnectionsHint.Vector, null, typeof(float), false), Op2(21, "Max", "Selects the greater of A and B"), Op2(22, "Min", "Selects the lesser of A and B"), @@ -185,7 +192,7 @@ namespace FlaxEditor.Surface.Archetypes } }, // - Op1(27, "Negate", "Returns opposite value"), + Op1(27, "Negate", "Returns opposite value", new [] { "Invert" }), Op1(28, "One Minus", "Returns 1 - value"), // new NodeArchetype @@ -225,6 +232,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 31, Title = "Mad", + AlternativeTitles = new [] { "Multiply", "Add", "*+" }, Description = "Performs value multiplication and addition at once", Flags = NodeFlags.AllGraphs, Size = new Float2(160, 60), diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index 18c21ecea..e35af7192 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -3,6 +3,7 @@ using System; using FlaxEditor.Content.Settings; using FlaxEditor.GUI; +using FlaxEditor.Scripting; using FlaxEngine; namespace FlaxEditor.Surface.Archetypes @@ -95,6 +96,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 1, Title = "Texture", + Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(Texture))), Description = "Two dimensional texture object", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), @@ -131,6 +133,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 3, Title = "Cube Texture", + Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(CubeTexture))), Description = "Set of 6 textures arranged in a cube", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), @@ -154,6 +157,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 4, Title = "Normal Map", + Create = (id, context, arch, groupArch) => new Constants.ConvertToParameterNode(id, context, arch, groupArch, new ScriptType(typeof(NormalMap))), Description = "Two dimensional texture object sampled as a normal map", Flags = NodeFlags.MaterialGraph, Size = new Float2(140, 120), diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 6d2d31b8c..029422d05 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1483,7 +1483,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 11, Title = "Comment", - AlternativeTitles = new[] { "//" }, + AlternativeTitles = new[] { "//" , "Group" }, TryParseText = (string filterText, out object[] data) => { data = null; @@ -1510,6 +1510,7 @@ namespace FlaxEditor.Surface.Archetypes "Comment", // Title new Color(1.0f, 1.0f, 1.0f, 0.2f), // Color new Float2(400.0f, 400.0f), // Size + -1, // Order }, }, CurveNode.GetArchetype(12, "Curve", typeof(float), 0.0f, 1.0f), @@ -1638,6 +1639,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 22, Title = "As", + AlternativeTitles = new [] { "Cast" }, Create = (id, context, arch, groupArch) => new AsNode(id, context, arch, groupArch), Description = "Casts the object to a different type. Returns null if cast fails.", Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph, diff --git a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs index 37da7d74d..207875a92 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCMItem.cs @@ -78,6 +78,7 @@ namespace FlaxEditor.Surface.ContextMenu if (!Visible) return; + SortScore += _archetype.SortScore; if (selectedBox != null && CanConnectTo(selectedBox)) SortScore += 1; if (Data != null) diff --git a/Source/Editor/Surface/Elements/AssetSelect.cs b/Source/Editor/Surface/Elements/AssetSelect.cs index e38989e08..5984ce9aa 100644 --- a/Source/Editor/Surface/Elements/AssetSelect.cs +++ b/Source/Editor/Surface/Elements/AssetSelect.cs @@ -38,13 +38,13 @@ namespace FlaxEditor.Surface.Elements private void OnNodeValuesChanged() { - SelectedID = (Guid)ParentNode.Values[Archetype.ValueIndex]; + Validator.SelectedID = (Guid)ParentNode.Values[Archetype.ValueIndex]; } /// protected override void OnSelectedItemChanged() { - var selectedId = SelectedID; + var selectedId = Validator.SelectedID; if (ParentNode != null && (Guid)ParentNode.Values[Archetype.ValueIndex] != selectedId) { ParentNode.SetValue(Archetype.ValueIndex, selectedId); diff --git a/Source/Editor/Surface/NodeArchetype.cs b/Source/Editor/Surface/NodeArchetype.cs index 6760b3ec5..eeed6f6ae 100644 --- a/Source/Editor/Surface/NodeArchetype.cs +++ b/Source/Editor/Surface/NodeArchetype.cs @@ -160,6 +160,11 @@ namespace FlaxEditor.Surface /// public object Tag; + /// + /// Custom score value to use when sorting node archetypes in Editor. If positive (eg. 1, 2) can be used to add more importance for a specific node type. + /// + public float SortScore; + /// /// Default node values. This array supports types: bool, int, float, Vector2, Vector3, Vector4, Color, Rectangle, Guid, string, Matrix and byte[]. /// @@ -215,14 +220,17 @@ namespace FlaxEditor.Surface Size = Size, Flags = Flags, Title = Title, - Description = Title, + SubTitle = SubTitle, + Description = Description, AlternativeTitles = (string[])AlternativeTitles?.Clone(), Tag = Tag, + SortScore = SortScore, DefaultValues = (object[])DefaultValues?.Clone(), DefaultType = DefaultType, ConnectionsHints = ConnectionsHints, IndependentBoxes = (int[])IndependentBoxes?.Clone(), DependentBoxes = (int[])DependentBoxes?.Clone(), + DependentBoxFilter = DependentBoxFilter, Elements = (NodeElementArchetype[])Elements?.Clone(), TryParseText = TryParseText, }; diff --git a/Source/Editor/Surface/ParticleEmitterSurface.cs b/Source/Editor/Surface/ParticleEmitterSurface.cs index 76f96f06c..b7cf83c62 100644 --- a/Source/Editor/Surface/ParticleEmitterSurface.cs +++ b/Source/Editor/Surface/ParticleEmitterSurface.cs @@ -93,7 +93,7 @@ namespace FlaxEditor.Surface } /// - protected override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) + protected internal override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) { groupId = 6; return Archetypes.Parameters.Nodes[1]; diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index ff38b1aa8..aad45190e 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -2,6 +2,7 @@ using System; using FlaxEditor.GUI; +using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Input; using FlaxEngine; using FlaxEngine.GUI; @@ -52,6 +53,12 @@ namespace FlaxEditor.Surface set => SetValue(2, value, false); } + private int OrderValue + { + get => (int)Values[3]; + set => SetValue(3, value, false); + } + /// public SurfaceComment(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) @@ -67,6 +74,19 @@ namespace FlaxEditor.Surface Title = TitleValue; Color = ColorValue; Size = SizeValue; + + // Order + // Backwards compatibility - When opening with an older version send the old comments to the back + if (Values.Length < 4) + { + if (IndexInParent > 0) + IndexInParent = 0; + OrderValue = IndexInParent; + } + else if(OrderValue != -1) + { + IndexInParent = OrderValue; + } } /// @@ -76,6 +96,10 @@ namespace FlaxEditor.Surface // Randomize color Color = ColorValue = Color.FromHSV(new Random().NextFloat(0, 360), 0.7f, 0.25f, 0.8f); + + if(OrderValue == -1) + OrderValue = Context.CommentCount - 1; + IndexInParent = OrderValue; } /// @@ -314,5 +338,38 @@ namespace FlaxEditor.Surface Color = ColorValue = color; Surface.MarkAsEdited(false); } + + /// + public override void OnShowSecondaryContextMenu(FlaxEditor.GUI.ContextMenu.ContextMenu menu, Float2 location) + { + base.OnShowSecondaryContextMenu(menu, location); + + menu.AddSeparator(); + ContextMenuChildMenu cmOrder = menu.AddChildMenu("Order"); + { + cmOrder.ContextMenu.AddButton("Bring Forward", () => + { + if(IndexInParent < Context.CommentCount-1) + IndexInParent++; + OrderValue = IndexInParent; + }); + cmOrder.ContextMenu.AddButton("Bring to Front", () => + { + IndexInParent = Context.CommentCount-1; + OrderValue = IndexInParent; + }); + cmOrder.ContextMenu.AddButton("Send Backward", () => + { + if(IndexInParent > 0) + IndexInParent--; + OrderValue = IndexInParent; + }); + cmOrder.ContextMenu.AddButton("Send to Back", () => + { + IndexInParent = 0; + OrderValue = IndexInParent; + }); + } + } } } diff --git a/Source/Editor/Surface/SurfaceParameter.cs b/Source/Editor/Surface/SurfaceParameter.cs index 6eafdc2aa..dae82e111 100644 --- a/Source/Editor/Surface/SurfaceParameter.cs +++ b/Source/Editor/Surface/SurfaceParameter.cs @@ -3,7 +3,6 @@ using System; using FlaxEditor.Scripting; using FlaxEngine; -using FlaxEngine.Utilities; namespace FlaxEditor.Surface { @@ -27,7 +26,7 @@ namespace FlaxEditor.Surface /// /// Parameter unique ID /// - public Guid ID; + public Guid ID = Guid.Empty; /// /// Parameter name @@ -49,23 +48,5 @@ namespace FlaxEditor.Surface /// [NoSerialize, HideInEditor] public readonly SurfaceMeta Meta = new SurfaceMeta(); - - /// - /// Creates the new parameter of the given type. - /// - /// The type. - /// The name. - /// The created parameter. - public static SurfaceParameter Create(ScriptType type, string name) - { - return new SurfaceParameter - { - ID = Guid.NewGuid(), - IsPublic = true, - Name = name, - Type = type, - Value = TypeUtils.GetDefaultValue(type), - }; - } } } diff --git a/Source/Editor/Surface/SurfaceUtils.cs b/Source/Editor/Surface/SurfaceUtils.cs index de1de1e81..2a0c177d7 100644 --- a/Source/Editor/Surface/SurfaceUtils.cs +++ b/Source/Editor/Surface/SurfaceUtils.cs @@ -541,13 +541,15 @@ namespace FlaxEditor.Surface Action save, Action showWholeGraph, Action toggleGridSnap, InputActionsContainer actionsContainer, out ToolStripButton saveButton, out ToolStripButton undoButton, out ToolStripButton redoButton, out ToolStripButton gridSnapButton) { + var inputOptions = editor.Options.Options.Input; + // Toolstrip saveButton = (ToolStripButton)toolStrip.AddButton(editor.Icons.Save64, save).LinkTooltip("Save"); toolStrip.AddSeparator(); - undoButton = (ToolStripButton)toolStrip.AddButton(editor.Icons.Undo64, undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - redoButton = (ToolStripButton)toolStrip.AddButton(editor.Icons.Redo64, undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + undoButton = (ToolStripButton)toolStrip.AddButton(editor.Icons.Undo64, undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + redoButton = (ToolStripButton)toolStrip.AddButton(editor.Icons.Redo64, undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); toolStrip.AddSeparator(); - toolStrip.AddButton(editor.Icons.Search64, editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); + toolStrip.AddButton(editor.Icons.Search64, editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})"); toolStrip.AddButton(editor.Icons.CenterView64, showWholeGraph).LinkTooltip("Show whole graph"); gridSnapButton = (ToolStripButton)toolStrip.AddButton(editor.Icons.Stop64, toggleGridSnap).LinkTooltip("Toggle grid snapping for nodes."); gridSnapButton.BackgroundColor = Style.Current.Background; // Default color for grid snap button. diff --git a/Source/Editor/Surface/VisjectSurface.DragDrop.cs b/Source/Editor/Surface/VisjectSurface.DragDrop.cs index 1728c282f..2ff82c269 100644 --- a/Source/Editor/Surface/VisjectSurface.DragDrop.cs +++ b/Source/Editor/Surface/VisjectSurface.DragDrop.cs @@ -151,7 +151,7 @@ namespace FlaxEditor.Surface /// /// The group ID. /// The node archetype. - protected virtual NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) + protected internal virtual NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) { groupId = 6; return Archetypes.Parameters.Nodes[0]; diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 9d9199b3b..3e0992eed 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -726,7 +726,18 @@ namespace FlaxEditor.Surface return null; Rectangle surfaceArea = GetNodesBounds(selection).MakeExpanded(80.0f); - return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f)); + // Order below other selected comments + bool hasCommentsSelected = false; + int lowestCommentOrder = int.MaxValue; + for (int i = 0; i < selection.Count; i++) + { + if (selection[i] is not SurfaceComment || selection[i].IndexInParent >= lowestCommentOrder) + continue; + hasCommentsSelected = true; + lowestCommentOrder = selection[i].IndexInParent; + } + + return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f), hasCommentsSelected ? lowestCommentOrder : -1); } private static Rectangle GetNodesBounds(List nodes) diff --git a/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs b/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs index 7d75e006b..12f01d4f1 100644 --- a/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs +++ b/Source/Editor/Surface/VisjectSurfaceContext.Serialization.cs @@ -920,12 +920,6 @@ namespace FlaxEditor.Surface // Link control control.OnLoaded(action); control.Parent = RootControl; - - if (control is SurfaceComment) - { - // Move comments to the background - control.IndexInParent = 0; - } } /// diff --git a/Source/Editor/Surface/VisjectSurfaceContext.cs b/Source/Editor/Surface/VisjectSurfaceContext.cs index 36e811c48..0886996b6 100644 --- a/Source/Editor/Surface/VisjectSurfaceContext.cs +++ b/Source/Editor/Surface/VisjectSurfaceContext.cs @@ -85,6 +85,27 @@ namespace FlaxEditor.Surface } } + /// + /// Gets the amount of surface comments + /// + /// + /// This is used as an alternative to , if only the amount of comments is important. + /// Is faster and doesn't allocate as much memory + /// + public int CommentCount + { + get + { + int count = 0; + for (int i = 0; i < RootControl.Children.Count; i++) + { + if (RootControl.Children[i] is SurfaceComment) + count++; + } + return count; + } + } + /// /// Gets a value indicating whether this context is modified (needs saving and flushing with surface data context source). /// @@ -285,14 +306,16 @@ namespace FlaxEditor.Surface /// The surface area to create comment. /// The comment title. /// The comment color. + /// The comment order or -1 to use default. /// The comment object - public virtual SurfaceComment SpawnComment(ref Rectangle surfaceArea, string title, Color color) + public virtual SurfaceComment SpawnComment(ref Rectangle surfaceArea, string title, Color color, int customOrder = -1) { var values = new object[] { title, // Title color, // Color surfaceArea.Size, // Size + customOrder, // Order }; return (SurfaceComment)SpawnNode(7, 11, surfaceArea.Location, values); } @@ -303,11 +326,12 @@ namespace FlaxEditor.Surface /// The surface area to create comment. /// The comment title. /// The comment color. + /// The comment order or -1 to use default. /// The comment object - public SurfaceComment CreateComment(ref Rectangle surfaceArea, string title, Color color) + public SurfaceComment CreateComment(ref Rectangle surfaceArea, string title, Color color, int customOrder = -1) { // Create comment - var comment = SpawnComment(ref surfaceArea, title, color); + var comment = SpawnComment(ref surfaceArea, title, color, customOrder); if (comment == null) { Editor.LogWarning("Failed to create comment."); diff --git a/Source/Editor/Surface/VisjectSurfaceWindow.cs b/Source/Editor/Surface/VisjectSurfaceWindow.cs index eff8ec8a3..bcd058985 100644 --- a/Source/Editor/Surface/VisjectSurfaceWindow.cs +++ b/Source/Editor/Surface/VisjectSurfaceWindow.cs @@ -17,6 +17,7 @@ using FlaxEditor.Viewport.Previews; using FlaxEditor.Windows.Assets; using FlaxEngine; using FlaxEngine.GUI; +using FlaxEngine.Utilities; namespace FlaxEditor.Surface { @@ -258,6 +259,11 @@ namespace FlaxEditor.Surface /// public IVisjectSurfaceWindow Window; + /// + /// The identifier of the parameter. Empty to auto generate it. + /// + public Guid Id = Guid.NewGuid(); + /// /// True if adding, false if removing parameter. /// @@ -278,6 +284,11 @@ namespace FlaxEditor.Surface /// public ScriptType Type; + /// + /// The value to initialize the parameter with. Can be null to use default one for the parameter type. + /// + public object InitValue; + /// public string ActionString => IsAdd ? "Add parameter" : "Remove parameter"; @@ -304,7 +315,14 @@ namespace FlaxEditor.Surface var type = Type; if (IsAdd && type.Type == typeof(NormalMap)) type = new ScriptType(typeof(Texture)); - var param = SurfaceParameter.Create(type, Name); + var param = new SurfaceParameter + { + ID = Id, + IsPublic = true, + Name = Name, + Type = type, + Value = InitValue ?? TypeUtils.GetDefaultValue(type), + }; if (IsAdd && Type.Type == typeof(NormalMap)) param.Value = FlaxEngine.Content.LoadAsyncInternal("Engine/Textures/NormalTexture"); Window.VisjectSurface.Parameters.Insert(Index, param); @@ -726,6 +744,8 @@ namespace FlaxEditor.Surface protected VisjectSurfaceWindow(Editor editor, AssetItem item, bool useTabs = false) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new FlaxEditor.Undo(); _undo.UndoDone += OnUndoRedo; @@ -1054,7 +1074,6 @@ namespace FlaxEditor.Surface public virtual void OnParamRemoveUndo() { _refreshPropertiesOnLoad = true; - //_propertiesEditor.BuildLayoutOnUpdate(); _propertiesEditor.BuildLayout(); } diff --git a/Source/Editor/Surface/VisualScriptSurface.cs b/Source/Editor/Surface/VisualScriptSurface.cs index 902582311..2f8d4cda8 100644 --- a/Source/Editor/Surface/VisualScriptSurface.cs +++ b/Source/Editor/Surface/VisualScriptSurface.cs @@ -144,7 +144,7 @@ namespace FlaxEditor.Surface } /// - protected override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) + protected internal override NodeArchetype GetParameterGetterNodeArchetype(out ushort groupId) { groupId = 6; return Archetypes.Parameters.Nodes[2]; diff --git a/Source/Editor/Tools/ClothPainting.cs b/Source/Editor/Tools/ClothPainting.cs index 226c7b49c..f12fca9db 100644 --- a/Source/Editor/Tools/ClothPainting.cs +++ b/Source/Editor/Tools/ClothPainting.cs @@ -225,6 +225,7 @@ namespace FlaxEngine.Tools var cloth = _cloth; if (cloth == null) return; + var hasPaintInput = Owner.IsLeftMouseButtonDown && !Owner.IsAltKeyDown; // Perform detailed tracing to find cursor location for the brush var ray = Owner.MouseRay; @@ -240,7 +241,7 @@ namespace FlaxEngine.Tools // Cursor hit other object or nothing PaintEnd(); - if (Owner.IsLeftMouseButtonDown) + if (hasPaintInput) { // Select something else var view = new Ray(Owner.ViewPosition, Owner.ViewDirection); @@ -253,7 +254,7 @@ namespace FlaxEngine.Tools } // Handle painting - if (Owner.IsLeftMouseButtonDown) + if (hasPaintInput) PaintStart(); else PaintEnd(); diff --git a/Source/Editor/Tools/Terrain/EditTab.cs b/Source/Editor/Tools/Terrain/EditTab.cs index 008be64f4..d52c1ae1d 100644 --- a/Source/Editor/Tools/Terrain/EditTab.cs +++ b/Source/Editor/Tools/Terrain/EditTab.cs @@ -290,7 +290,7 @@ namespace FlaxEditor.Tools.Terrain var patchCoord = Gizmo.SelectedPatchCoord; var chunkCoord = Gizmo.SelectedChunkCoord; - var action = new EditChunkMaterialAction(CarveTab.SelectedTerrain, ref patchCoord, ref chunkCoord, _chunkOverrideMaterial.SelectedAsset as MaterialBase); + var action = new EditChunkMaterialAction(CarveTab.SelectedTerrain, ref patchCoord, ref chunkCoord, _chunkOverrideMaterial.Validator.SelectedAsset as MaterialBase); action.Do(); CarveTab.Editor.Undo.AddAction(action); } @@ -336,12 +336,12 @@ namespace FlaxEditor.Tools.Terrain _isUpdatingUI = true; if (terrain.HasPatch(ref patchCoord)) { - _chunkOverrideMaterial.SelectedAsset = terrain.GetChunkOverrideMaterial(ref patchCoord, ref chunkCoord); + _chunkOverrideMaterial.Validator.SelectedAsset = terrain.GetChunkOverrideMaterial(ref patchCoord, ref chunkCoord); _chunkOverrideMaterial.Enabled = true; } else { - _chunkOverrideMaterial.SelectedAsset = null; + _chunkOverrideMaterial.Validator.SelectedAsset = null; _chunkOverrideMaterial.Enabled = false; } _isUpdatingUI = false; diff --git a/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs b/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs index a5710494c..dac2900f4 100644 --- a/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs +++ b/Source/Editor/Undo/Actions/AddRemoveScriptAction.cs @@ -75,6 +75,11 @@ namespace FlaxEditor.Actions _enabled = true; } + public int GetOrderInParent() + { + return _orderInParent; + } + /// /// Creates a new added script undo action. /// @@ -184,6 +189,7 @@ namespace FlaxEditor.Actions script.Parent = parentActor; if (_orderInParent != -1) script.OrderInParent = _orderInParent; + _orderInParent = script.OrderInParent; // Ensure order is correct for script that want to use it later if (_prefabObjectId != Guid.Empty) SceneObject.Internal_LinkPrefab(Object.GetUnmanagedPtr(script), ref _prefabId, ref _prefabObjectId); Editor.Instance.Scene.MarkSceneEdited(parentActor.Scene); diff --git a/Source/Editor/Utilities/EditorUtilities.cpp b/Source/Editor/Utilities/EditorUtilities.cpp index 8e3fc0e4a..a2418ceb1 100644 --- a/Source/Editor/Utilities/EditorUtilities.cpp +++ b/Source/Editor/Utilities/EditorUtilities.cpp @@ -394,8 +394,12 @@ bool EditorUtilities::UpdateExeIcon(const String& path, const TextureData& icon) // - icon/cursor/etc data std::fstream stream; +#if PLATFORM_WINDOWS + stream.open(path.Get(), std::ios::in | std::ios::out | std::ios::binary); +#else StringAsANSI<> pathAnsi(path.Get()); stream.open(pathAnsi.Get(), std::ios::in | std::ios::out | std::ios::binary); +#endif if (!stream.is_open()) { LOG(Warning, "Cannot open file"); diff --git a/Source/Editor/Utilities/ScreenUtilities.cpp b/Source/Editor/Utilities/ScreenUtilities.cpp index 44f52350e..730a69aa3 100644 --- a/Source/Editor/Utilities/ScreenUtilities.cpp +++ b/Source/Editor/Utilities/ScreenUtilities.cpp @@ -73,6 +73,7 @@ Color32 ScreenUtilities::GetColorAt(const Float2& pos) outputColor.R = color.red / 256; outputColor.G = color.green / 256; outputColor.B = color.blue / 256; + outputColor.A = 255; return outputColor; } diff --git a/Source/Editor/Viewport/Cameras/FPSCamera.cs b/Source/Editor/Viewport/Cameras/FPSCamera.cs index e578933cf..bf2e840ea 100644 --- a/Source/Editor/Viewport/Cameras/FPSCamera.cs +++ b/Source/Editor/Viewport/Cameras/FPSCamera.cs @@ -259,7 +259,10 @@ namespace FlaxEditor.Viewport.Cameras // Pan if (input.IsPanning) { - var panningSpeed = 0.8f; + var panningSpeed = (Viewport.RelativePanning) + ? Mathf.Abs((position - TargetPoint).Length) * 0.005f + : Viewport.PanningSpeed; + if (Viewport.InvertPanning) { position += up * (mouseDelta.Y * panningSpeed); diff --git a/Source/Editor/Viewport/EditorGizmoViewport.cs b/Source/Editor/Viewport/EditorGizmoViewport.cs index 9a4fd34a2..a24dd2f38 100644 --- a/Source/Editor/Viewport/EditorGizmoViewport.cs +++ b/Source/Editor/Viewport/EditorGizmoViewport.cs @@ -41,6 +41,8 @@ namespace FlaxEditor.Viewport Gizmos[i].Update(deltaTime); } } + /// + public EditorViewport Viewport => this; /// public GizmosCollection Gizmos { get; } diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index e20069120..c49392d01 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -128,12 +128,26 @@ namespace FlaxEditor.Viewport public const int FpsCameraFilteringFrames = 3; /// - /// The speed widget button. + /// The camera settings widget. /// - protected ViewportWidgetButton _speedWidget; + protected ViewportWidgetsContainer _cameraWidget; + + /// + /// The camera settings widget button. + /// + protected ViewportWidgetButton _cameraButton; + + /// + /// The orthographic mode widget button. + /// + protected ViewportWidgetButton _orthographicModeButton; + + private readonly Editor _editor; private float _mouseSensitivity; private float _movementSpeed; + private float _minMovementSpeed; + private float _maxMovementSpeed; private float _mouseAccelerationScale; private bool _useMouseFiltering; private bool _useMouseAcceleration; @@ -174,11 +188,17 @@ namespace FlaxEditor.Viewport private float _fieldOfView; private float _nearPlane; private float _farPlane; - private float _orthoSize = 1.0f; - private bool _isOrtho = false; - private float _wheelMovementChangeDeltaSum = 0; + private float _orthoSize; + private bool _isOrtho; + private bool _useCameraEasing; + private float _cameraEasingDegree; + private float _panningSpeed; + private bool _relativePanning; private bool _invertPanning; + private int _speedStep; + private int _maxSpeedSteps; + /// /// Speed of the mouse. /// @@ -189,6 +209,25 @@ namespace FlaxEditor.Viewport /// public float MouseWheelZoomSpeedFactor = 1; + /// + /// Format of the text for the camera move speed. + /// + private string MovementSpeedTextFormat + { + get + { + if (Mathf.Abs(_movementSpeed - _maxMovementSpeed) < Mathf.Epsilon || Mathf.Abs(_movementSpeed - _minMovementSpeed) < Mathf.Epsilon) + return "{0:0.##}"; + + if (_movementSpeed < 10.0f) + return "{0:0.00}"; + else if (_movementSpeed < 100.0f) + return "{0:0.0}"; + else + return "{0:#}"; + } + } + /// /// Gets or sets the camera movement speed. /// @@ -197,19 +236,40 @@ namespace FlaxEditor.Viewport get => _movementSpeed; set { - for (int i = 0; i < EditorViewportCameraSpeedValues.Length; i++) - { - if (Math.Abs(value - EditorViewportCameraSpeedValues[i]) < 0.001f) - { - _movementSpeed = EditorViewportCameraSpeedValues[i]; - if (_speedWidget != null) - _speedWidget.Text = _movementSpeed.ToString(); - break; - } - } + _movementSpeed = value; + + if (_cameraButton != null) + _cameraButton.Text = string.Format(MovementSpeedTextFormat, _movementSpeed); } } + /// + /// Gets or sets the minimum camera movement speed. + /// + public float MinMovementSpeed + { + get => _minMovementSpeed; + set => _minMovementSpeed = value; + } + + /// + /// Gets or sets the maximum camera movement speed. + /// + public float MaxMovementSpeed + { + get => _maxMovementSpeed; + set => _maxMovementSpeed = value; + } + + /// + /// Gets or sets the camera easing mode. + /// + public bool UseCameraEasing + { + get => _useCameraEasing; + set => _useCameraEasing = value; + } + /// /// Gets the mouse movement position delta (user press and move). /// @@ -396,6 +456,15 @@ namespace FlaxEditor.Viewport set => _isOrtho = value; } + /// + /// Gets or sets if the panning speed should be relative to the camera target. + /// + public bool RelativePanning + { + get => _relativePanning; + set => _relativePanning = value; + } + /// /// Gets or sets if the panning direction is inverted. /// @@ -405,6 +474,15 @@ namespace FlaxEditor.Viewport set => _invertPanning = value; } + /// + /// Gets or sets the camera panning speed. + /// + public float PanningSpeed + { + get => _panningSpeed; + set => _panningSpeed = value; + } + /// /// The input actions collection to processed during user input. /// @@ -419,6 +497,8 @@ namespace FlaxEditor.Viewport public EditorViewport(SceneRenderTask task, ViewportCamera camera, bool useWidgets) : base(task) { + _editor = Editor.Instance; + _mouseAccelerationScale = 0.1f; _useMouseFiltering = false; _useMouseAcceleration = false; @@ -431,43 +511,299 @@ namespace FlaxEditor.Viewport // Setup options { - var options = Editor.Instance.Options.Options; - _movementSpeed = options.Viewport.DefaultMovementSpeed; - _nearPlane = options.Viewport.DefaultNearPlane; - _farPlane = options.Viewport.DefaultFarPlane; - _fieldOfView = options.Viewport.DefaultFieldOfView; - _invertPanning = options.Viewport.DefaultInvertPanning; - Editor.Instance.Options.OptionsChanged += OnEditorOptionsChanged; - OnEditorOptionsChanged(options); + SetupViewportOptions(); } + // Initialize camera values from cache + if (_editor.ProjectCache.TryGetCustomData("CameraMovementSpeedValue", out var cachedState)) + MovementSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraMinMovementSpeedValue", out cachedState)) + _minMovementSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraMaxMovementSpeedValue", out cachedState)) + _maxMovementSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("UseCameraEasingState", out cachedState)) + _useCameraEasing = bool.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraPanningSpeedValue", out cachedState)) + _panningSpeed = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraInvertPanningState", out cachedState)) + _invertPanning = bool.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraRelativePanningState", out cachedState)) + _relativePanning = bool.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraOrthographicState", out cachedState)) + _isOrtho = bool.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraOrthographicSizeValue", out cachedState)) + _orthoSize = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraFieldOfViewValue", out cachedState)) + _fieldOfView = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraNearPlaneValue", out cachedState)) + _nearPlane = float.Parse(cachedState); + if (_editor.ProjectCache.TryGetCustomData("CameraFarPlaneValue", out cachedState)) + _farPlane = float.Parse(cachedState); + + OnCameraMovementProgressChanged(); + if (useWidgets) { - var largestText = "Invert Panning"; + #region Camera settings widget + + var largestText = "Relative Panning"; var textSize = Style.Current.FontMedium.MeasureText(largestText); var xLocationForExtras = textSize.X + 5; - // Camera speed widget - var camSpeed = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); - var camSpeedCM = new ContextMenu(); - var camSpeedButton = new ViewportWidgetButton(_movementSpeed.ToString(), Editor.Instance.Icons.CamSpeed32, camSpeedCM) + var cameraSpeedTextWidth = Style.Current.FontMedium.MeasureText("0.00").X; + + // Camera Settings Widget + _cameraWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); + + // Camera Settings Menu + var cameraCM = new ContextMenu(); + _cameraButton = new ViewportWidgetButton(string.Format(MovementSpeedTextFormat, _movementSpeed), Editor.Instance.Icons.Camera64, cameraCM, false, cameraSpeedTextWidth) { Tag = this, - TooltipText = "Camera speed scale" + TooltipText = "Camera Settings", + Parent = _cameraWidget }; - _speedWidget = camSpeedButton; - for (int i = 0; i < EditorViewportCameraSpeedValues.Length; i++) - { - var v = EditorViewportCameraSpeedValues[i]; - var button = camSpeedCM.AddButton(v.ToString()); - button.Tag = v; - } - camSpeedCM.ButtonClicked += button => MovementSpeed = (float)button.Tag; - camSpeedCM.VisibleChanged += WidgetCamSpeedShowHide; - camSpeedButton.Parent = camSpeed; - camSpeed.Parent = this; + _cameraWidget.Parent = this; + + // Orthographic/Perspective Mode Widget + _orthographicModeButton = new ViewportWidgetButton(string.Empty, Editor.Instance.Icons.CamSpeed32, null, true) + { + Checked = !_isOrtho, + TooltipText = "Toggle Orthographic/Perspective Mode", + Parent = _cameraWidget + }; + _orthographicModeButton.Toggled += OnOrthographicModeToggled; + + // Camera Speed + var camSpeedButton = cameraCM.AddButton("Camera Speed"); + camSpeedButton.CloseMenuOnClick = false; + var camSpeedValue = new FloatValueBox(_movementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, _maxMovementSpeed, 0.5f) + { + Parent = camSpeedButton + }; + + camSpeedValue.ValueChanged += () => OnMovementSpeedChanged(camSpeedValue); + cameraCM.VisibleChanged += control => camSpeedValue.Value = _movementSpeed; + + // Minimum & Maximum Camera Speed + var minCamSpeedButton = cameraCM.AddButton("Min Cam Speed"); + minCamSpeedButton.CloseMenuOnClick = false; + var minCamSpeedValue = new FloatValueBox(_minMovementSpeed, xLocationForExtras, 2, 70.0f, 0.05f, _maxMovementSpeed, 0.5f) + { + Parent = minCamSpeedButton + }; + var maxCamSpeedButton = cameraCM.AddButton("Max Cam Speed"); + maxCamSpeedButton.CloseMenuOnClick = false; + var maxCamSpeedValue = new FloatValueBox(_maxMovementSpeed, xLocationForExtras, 2, 70.0f, _minMovementSpeed, 1000.0f, 0.5f) + { + Parent = maxCamSpeedButton + }; + + minCamSpeedValue.ValueChanged += () => + { + OnMinMovementSpeedChanged(minCamSpeedValue); + + maxCamSpeedValue.MinValue = minCamSpeedValue.Value; + + if (Math.Abs(camSpeedValue.MinValue - minCamSpeedValue.Value) > Mathf.Epsilon) + camSpeedValue.MinValue = minCamSpeedValue.Value; + }; + cameraCM.VisibleChanged += control => minCamSpeedValue.Value = _minMovementSpeed; + maxCamSpeedValue.ValueChanged += () => + { + OnMaxMovementSpeedChanged(maxCamSpeedValue); + + minCamSpeedValue.MaxValue = maxCamSpeedValue.Value; + + if (Math.Abs(camSpeedValue.MaxValue - maxCamSpeedValue.Value) > Mathf.Epsilon) + camSpeedValue.MaxValue = maxCamSpeedValue.Value; + }; + cameraCM.VisibleChanged += control => maxCamSpeedValue.Value = _maxMovementSpeed; + + // Camera Easing + { + var useCameraEasing = cameraCM.AddButton("Camera Easing"); + useCameraEasing.CloseMenuOnClick = false; + var useCameraEasingValue = new CheckBox(xLocationForExtras, 2, _useCameraEasing) + { + Parent = useCameraEasing + }; + + useCameraEasingValue.StateChanged += OnCameraEasingToggled; + cameraCM.VisibleChanged += control => useCameraEasingValue.Checked = _useCameraEasing; + } + + // Panning Speed + { + var panningSpeed = cameraCM.AddButton("Panning Speed"); + panningSpeed.CloseMenuOnClick = false; + var panningSpeedValue = new FloatValueBox(_panningSpeed, xLocationForExtras, 2, 70.0f, 0.01f, 128.0f, 0.1f) + { + Parent = panningSpeed + }; + + panningSpeedValue.ValueChanged += () => OnPanningSpeedChanged(panningSpeedValue); + cameraCM.VisibleChanged += control => + { + panningSpeed.Visible = !_relativePanning; + panningSpeedValue.Value = _panningSpeed; + }; + } + + // Relative Panning + { + var relativePanning = cameraCM.AddButton("Relative Panning"); + relativePanning.CloseMenuOnClick = false; + var relativePanningValue = new CheckBox(xLocationForExtras, 2, _relativePanning) + { + Parent = relativePanning + }; + + relativePanningValue.StateChanged += checkBox => + { + if (checkBox.Checked != _relativePanning) + { + OnRelativePanningToggled(checkBox); + cameraCM.Hide(); + } + }; + cameraCM.VisibleChanged += control => relativePanningValue.Checked = _relativePanning; + } + + // Invert Panning + { + var invertPanning = cameraCM.AddButton("Invert Panning"); + invertPanning.CloseMenuOnClick = false; + var invertPanningValue = new CheckBox(xLocationForExtras, 2, _invertPanning) + { + Parent = invertPanning + }; + + invertPanningValue.StateChanged += OnInvertPanningToggled; + cameraCM.VisibleChanged += control => invertPanningValue.Checked = _invertPanning; + } + + cameraCM.AddSeparator(); + + // Camera Viewpoints + { + var cameraView = cameraCM.AddChildMenu("Viewpoints").ContextMenu; + for (int i = 0; i < EditorViewportCameraViewpointValues.Length; i++) + { + var co = EditorViewportCameraViewpointValues[i]; + var button = cameraView.AddButton(co.Name); + button.Tag = co.Orientation; + } + + cameraView.ButtonClicked += OnViewpointChanged; + } + + // Orthographic Mode + { + var ortho = cameraCM.AddButton("Orthographic"); + ortho.CloseMenuOnClick = false; + var orthoValue = new CheckBox(xLocationForExtras, 2, _isOrtho) + { + Parent = ortho + }; + + orthoValue.StateChanged += checkBox => + { + if (checkBox.Checked != _isOrtho) + { + OnOrthographicModeToggled(checkBox); + cameraCM.Hide(); + } + }; + cameraCM.VisibleChanged += control => orthoValue.Checked = _isOrtho; + } + + // Field of View + { + var fov = cameraCM.AddButton("Field Of View"); + fov.CloseMenuOnClick = false; + var fovValue = new FloatValueBox(_fieldOfView, xLocationForExtras, 2, 70.0f, 35.0f, 160.0f, 0.1f) + { + Parent = fov + }; + + fovValue.ValueChanged += () => OnFieldOfViewChanged(fovValue); + cameraCM.VisibleChanged += control => + { + fov.Visible = !_isOrtho; + fovValue.Value = _fieldOfView; + }; + } + + // Orthographic Scale + { + var orthoSize = cameraCM.AddButton("Ortho Scale"); + orthoSize.CloseMenuOnClick = false; + var orthoSizeValue = new FloatValueBox(_orthoSize, xLocationForExtras, 2, 70.0f, 0.001f, 100000.0f, 0.01f) + { + Parent = orthoSize + }; + + orthoSizeValue.ValueChanged += () => OnOrthographicSizeChanged(orthoSizeValue); + cameraCM.VisibleChanged += control => + { + orthoSize.Visible = _isOrtho; + orthoSizeValue.Value = _orthoSize; + }; + } + + // Near Plane + { + var nearPlane = cameraCM.AddButton("Near Plane"); + nearPlane.CloseMenuOnClick = false; + var nearPlaneValue = new FloatValueBox(_nearPlane, xLocationForExtras, 2, 70.0f, 0.001f, 1000.0f) + { + Parent = nearPlane + }; + + nearPlaneValue.ValueChanged += () => OnNearPlaneChanged(nearPlaneValue); + cameraCM.VisibleChanged += control => nearPlaneValue.Value = _nearPlane; + } + + // Far Plane + { + var farPlane = cameraCM.AddButton("Far Plane"); + farPlane.CloseMenuOnClick = false; + var farPlaneValue = new FloatValueBox(_farPlane, xLocationForExtras, 2, 70.0f, 10.0f) + { + Parent = farPlane + }; + + farPlaneValue.ValueChanged += () => OnFarPlaneChanged(farPlaneValue); + cameraCM.VisibleChanged += control => farPlaneValue.Value = _farPlane; + } + + cameraCM.AddSeparator(); + + // Reset Button + { + var reset = cameraCM.AddButton("Reset to default"); + reset.ButtonClicked += button => + { + SetupViewportOptions(); + + // if the context menu is opened without triggering the value changes beforehand, + // the movement speed will not be correctly reset to its default value in certain cases + // therefore, a UI update needs to be triggered here + minCamSpeedValue.Value = _minMovementSpeed; + camSpeedValue.Value = _movementSpeed; + maxCamSpeedValue.Value = _maxMovementSpeed; + }; + } + + #endregion Camera settings widget + + #region View mode widget + + largestText = "Brightness"; + textSize = Style.Current.FontMedium.MeasureText(largestText); + xLocationForExtras = textSize.X + 5; - // View mode widget var viewMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperLeft); ViewWidgetButtonMenu = new ContextMenu(); var viewModeButton = new ViewportWidgetButton("View", SpriteHandle.Invalid, ViewWidgetButtonMenu) @@ -484,8 +820,8 @@ namespace FlaxEditor.Viewport // Show FPS { InitFpsCounter(); - _showFpsButon = ViewWidgetShowMenu.AddButton("FPS Counter", () => ShowFpsCounter = !ShowFpsCounter); - _showFpsButon.CloseMenuOnClick = false; + _showFpsButton = ViewWidgetShowMenu.AddButton("FPS Counter", () => ShowFpsCounter = !ShowFpsCounter); + _showFpsButton.CloseMenuOnClick = false; } } @@ -593,12 +929,14 @@ namespace FlaxEditor.Viewport { ref var vv = ref v.Options[j]; var button = childMenu.AddButton(vv.Name); + button.CloseMenuOnClick = false; button.Tag = vv.Mode; } } else { var button = debugView.AddButton(v.Name); + button.CloseMenuOnClick = false; button.Tag = v.Mode; } } @@ -608,104 +946,6 @@ namespace FlaxEditor.Viewport ViewWidgetButtonMenu.AddSeparator(); - // Orthographic - { - var ortho = ViewWidgetButtonMenu.AddButton("Orthographic"); - ortho.CloseMenuOnClick = false; - var orthoValue = new CheckBox(xLocationForExtras, 2, _isOrtho) - { - Parent = ortho - }; - orthoValue.StateChanged += checkBox => - { - if (checkBox.Checked != _isOrtho) - { - _isOrtho = checkBox.Checked; - ViewWidgetButtonMenu.Hide(); - if (_isOrtho) - { - var orient = ViewOrientation; - OrientViewport(ref orient); - } - } - }; - ViewWidgetButtonMenu.VisibleChanged += control => orthoValue.Checked = _isOrtho; - } - - // Camera Viewpoints - { - var cameraView = ViewWidgetButtonMenu.AddChildMenu("Viewpoints").ContextMenu; - for (int i = 0; i < EditorViewportCameraViewpointValues.Length; i++) - { - var co = EditorViewportCameraViewpointValues[i]; - var button = cameraView.AddButton(co.Name); - button.Tag = co.Orientation; - } - cameraView.ButtonClicked += button => - { - var orient = Quaternion.Euler((Float3)button.Tag); - OrientViewport(ref orient); - }; - } - - // Field of View - { - var fov = ViewWidgetButtonMenu.AddButton("Field Of View"); - fov.CloseMenuOnClick = false; - var fovValue = new FloatValueBox(1, xLocationForExtras, 2, 70.0f, 35.0f, 160.0f, 0.1f) - { - Parent = fov - }; - - fovValue.ValueChanged += () => _fieldOfView = fovValue.Value; - ViewWidgetButtonMenu.VisibleChanged += control => - { - fov.Visible = !_isOrtho; - fovValue.Value = _fieldOfView; - }; - } - - // Ortho Scale - { - var orthoSize = ViewWidgetButtonMenu.AddButton("Ortho Scale"); - orthoSize.CloseMenuOnClick = false; - var orthoSizeValue = new FloatValueBox(_orthoSize, xLocationForExtras, 2, 70.0f, 0.001f, 100000.0f, 0.01f) - { - Parent = orthoSize - }; - - orthoSizeValue.ValueChanged += () => _orthoSize = orthoSizeValue.Value; - ViewWidgetButtonMenu.VisibleChanged += control => - { - orthoSize.Visible = _isOrtho; - orthoSizeValue.Value = _orthoSize; - }; - } - - // Near Plane - { - var nearPlane = ViewWidgetButtonMenu.AddButton("Near Plane"); - nearPlane.CloseMenuOnClick = false; - var nearPlaneValue = new FloatValueBox(2.0f, xLocationForExtras, 2, 70.0f, 0.001f, 1000.0f) - { - Parent = nearPlane - }; - nearPlaneValue.ValueChanged += () => _nearPlane = nearPlaneValue.Value; - ViewWidgetButtonMenu.VisibleChanged += control => nearPlaneValue.Value = _nearPlane; - } - - // Far Plane - { - var farPlane = ViewWidgetButtonMenu.AddButton("Far Plane"); - farPlane.CloseMenuOnClick = false; - var farPlaneValue = new FloatValueBox(1000, xLocationForExtras, 2, 70.0f, 10.0f) - { - Parent = farPlane - }; - farPlaneValue.ValueChanged += () => _farPlane = farPlaneValue.Value; - ViewWidgetButtonMenu.VisibleChanged += control => farPlaneValue.Value = _farPlane; - } - // Brightness { var brightness = ViewWidgetButtonMenu.AddButton("Brightness"); @@ -730,24 +970,7 @@ namespace FlaxEditor.Viewport ViewWidgetButtonMenu.VisibleChanged += control => resolutionValue.Value = ResolutionScale; } - // Invert Panning - { - var invert = ViewWidgetButtonMenu.AddButton("Invert Panning"); - invert.CloseMenuOnClick = false; - var invertValue = new CheckBox(xLocationForExtras, 2, _invertPanning) - { - Parent = invert - }; - - invertValue.StateChanged += checkBox => - { - if (checkBox.Checked != _invertPanning) - { - _invertPanning = checkBox.Checked; - } - }; - ViewWidgetButtonMenu.VisibleChanged += control => invertValue.Checked = _invertPanning; - } + #endregion View mode widget } InputActions.Add(options => options.ViewpointTop, () => OrientViewport(Quaternion.Euler(EditorViewportCameraViewpointValues.First(vp => vp.Name == "Top").Orientation))); @@ -764,6 +987,135 @@ namespace FlaxEditor.Viewport task.Begin += OnRenderBegin; } + /// + /// Sets the viewport options to the default values. + /// + private void SetupViewportOptions() + { + var options = Editor.Instance.Options.Options; + _minMovementSpeed = options.Viewport.MinMovementSpeed; + MovementSpeed = options.Viewport.MovementSpeed; + _maxMovementSpeed = options.Viewport.MaxMovementSpeed; + _useCameraEasing = options.Viewport.UseCameraEasing; + _panningSpeed = options.Viewport.PanningSpeed; + _invertPanning = options.Viewport.InvertPanning; + _relativePanning = options.Viewport.UseRelativePanning; + + _isOrtho = options.Viewport.UseOrthographicProjection; + _orthoSize = options.Viewport.OrthographicScale; + _fieldOfView = options.Viewport.FieldOfView; + _nearPlane = options.Viewport.NearPlane; + _farPlane = options.Viewport.FarPlane; + + OnEditorOptionsChanged(options); + } + + private void OnMovementSpeedChanged(FloatValueBox control) + { + var value = Mathf.Clamp(control.Value, _minMovementSpeed, _maxMovementSpeed); + MovementSpeed = value; + + OnCameraMovementProgressChanged(); + _editor.ProjectCache.SetCustomData("CameraMovementSpeedValue", _movementSpeed.ToString()); + } + + private void OnMinMovementSpeedChanged(FloatValueBox control) + { + var value = Mathf.Clamp(control.Value, 0.05f, _maxMovementSpeed); + _minMovementSpeed = value; + + if (_movementSpeed < value) + MovementSpeed = value; + + OnCameraMovementProgressChanged(); + _editor.ProjectCache.SetCustomData("CameraMinMovementSpeedValue", _minMovementSpeed.ToString()); + } + + private void OnMaxMovementSpeedChanged(FloatValueBox control) + { + var value = Mathf.Clamp(control.Value, _minMovementSpeed, 1000.0f); + _maxMovementSpeed = value; + + if (_movementSpeed > value) + MovementSpeed = value; + + OnCameraMovementProgressChanged(); + _editor.ProjectCache.SetCustomData("CameraMaxMovementSpeedValue", _maxMovementSpeed.ToString()); + } + + private void OnCameraEasingToggled(Control control) + { + _useCameraEasing = !_useCameraEasing; + + OnCameraMovementProgressChanged(); + _editor.ProjectCache.SetCustomData("UseCameraEasingState", _useCameraEasing.ToString()); + } + + private void OnPanningSpeedChanged(FloatValueBox control) + { + _panningSpeed = control.Value; + _editor.ProjectCache.SetCustomData("CameraPanningSpeedValue", _panningSpeed.ToString()); + } + + private void OnRelativePanningToggled(Control control) + { + _relativePanning = !_relativePanning; + _editor.ProjectCache.SetCustomData("CameraRelativePanningState", _relativePanning.ToString()); + } + + private void OnInvertPanningToggled(Control control) + { + _invertPanning = !_invertPanning; + _editor.ProjectCache.SetCustomData("CameraInvertPanningState", _invertPanning.ToString()); + } + + + private void OnViewpointChanged(ContextMenuButton button) + { + var orient = Quaternion.Euler((Float3)button.Tag); + OrientViewport(ref orient); + } + + private void OnFieldOfViewChanged(FloatValueBox control) + { + _fieldOfView = control.Value; + _editor.ProjectCache.SetCustomData("CameraFieldOfViewValue", _fieldOfView.ToString()); + } + + private void OnOrthographicModeToggled(Control control) + { + _isOrtho = !_isOrtho; + + if (_orthographicModeButton != null) + _orthographicModeButton.Checked = !_isOrtho; + + if (_isOrtho) + { + var orient = ViewOrientation; + OrientViewport(ref orient); + } + + _editor.ProjectCache.SetCustomData("CameraOrthographicState", _isOrtho.ToString()); + } + + private void OnOrthographicSizeChanged(FloatValueBox control) + { + _orthoSize = control.Value; + _editor.ProjectCache.SetCustomData("CameraOrthographicSizeValue", _orthoSize.ToString()); + } + + private void OnNearPlaneChanged(FloatValueBox control) + { + _nearPlane = control.Value; + _editor.ProjectCache.SetCustomData("CameraNearPlaneValue", _nearPlane.ToString()); + } + + private void OnFarPlaneChanged(FloatValueBox control) + { + _farPlane = control.Value; + _editor.ProjectCache.SetCustomData("CameraNearPlaneValue", _farPlane.ToString()); + } + /// /// Gets a value indicating whether this viewport is using mouse currently (eg. user moving objects). /// @@ -796,33 +1148,59 @@ namespace FlaxEditor.Viewport } } + private void OnCameraMovementProgressChanged() + { + // prevent NaN + if (Math.Abs(_minMovementSpeed - _maxMovementSpeed) < Mathf.Epsilon) + { + _speedStep = 0; + return; + } + + if (Math.Abs(_movementSpeed - _maxMovementSpeed) < Mathf.Epsilon) + { + _speedStep = _maxSpeedSteps; + return; + } + else if (Math.Abs(_movementSpeed - _minMovementSpeed) < Mathf.Epsilon) + { + _speedStep = 0; + return; + } + + // calculate current linear/eased progress + var progress = Mathf.Remap(_movementSpeed, _minMovementSpeed, _maxMovementSpeed, 0.0f, 1.0f); + + if (_useCameraEasing) + progress = Mathf.Pow(progress, 1.0f / _cameraEasingDegree); + + _speedStep = Mathf.RoundToInt(progress * _maxSpeedSteps); + } + /// /// Increases or decreases the camera movement speed. /// /// The stepping direction for speed adjustment. protected void AdjustCameraMoveSpeed(int step) { - int camValueIndex = -1; - for (int i = 0; i < EditorViewportCameraSpeedValues.Length; i++) - { - if (Mathf.NearEqual(EditorViewportCameraSpeedValues[i], _movementSpeed)) - { - camValueIndex = i; - break; - } - } - if (camValueIndex == -1) - return; + _speedStep = Mathf.Clamp(_speedStep + step, 0, _maxSpeedSteps); - if (step > 0) - MovementSpeed = EditorViewportCameraSpeedValues[Mathf.Min(camValueIndex + 1, EditorViewportCameraSpeedValues.Length - 1)]; - else if (step < 0) - MovementSpeed = EditorViewportCameraSpeedValues[Mathf.Max(camValueIndex - 1, 0)]; + // calculate new linear/eased progress + var progress = _useCameraEasing + ? Mathf.Pow((float)_speedStep / _maxSpeedSteps, _cameraEasingDegree) + : (float)_speedStep / _maxSpeedSteps; + + var speed = Mathf.Lerp(_minMovementSpeed, _maxMovementSpeed, progress); + MovementSpeed = (float)Math.Round(speed, 3); + _editor.ProjectCache.SetCustomData("CameraMovementSpeedValue", _movementSpeed.ToString()); } private void OnEditorOptionsChanged(EditorOptions options) { _mouseSensitivity = options.Viewport.MouseSensitivity; + _maxSpeedSteps = options.Viewport.TotalCameraSpeedSteps; + _cameraEasingDegree = options.Viewport.CameraEasingDegree; + OnCameraMovementProgressChanged(); } private void OnRenderBegin(RenderTask task, GPUContext context) @@ -861,7 +1239,7 @@ namespace FlaxEditor.Viewport } private FpsCounter _fpsCounter; - private ContextMenuButton _showFpsButon; + private ContextMenuButton _showFpsButton; /// /// Gets or sets a value indicating whether show or hide FPS counter. @@ -873,7 +1251,7 @@ namespace FlaxEditor.Viewport { _fpsCounter.Visible = value; _fpsCounter.Enabled = value; - _showFpsButon.Icon = value ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; + _showFpsButton.Icon = value ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; } } @@ -1014,8 +1392,6 @@ namespace FlaxEditor.Viewport /// The parent window. protected virtual void OnControlMouseBegin(Window win) { - _wheelMovementChangeDeltaSum = 0; - // Hide cursor and start tracking mouse movement win.StartTrackingMouse(false); win.Cursor = CursorType.Hidden; @@ -1111,8 +1487,8 @@ namespace FlaxEditor.Viewport _camera.Update(deltaTime); useMovementSpeed = _camera.UseMovementSpeed; - if (_speedWidget != null) - _speedWidget.Parent.Visible = useMovementSpeed; + if (_cameraButton != null) + _cameraButton.Parent.Visible = useMovementSpeed; } // Get parent window @@ -1215,18 +1591,8 @@ namespace FlaxEditor.Viewport rmbWheel = useMovementSpeed && (_input.IsMouseRightDown || _isVirtualMouseRightDown) && wheelInUse; if (rmbWheel) { - const float step = 4.0f; - _wheelMovementChangeDeltaSum += _input.MouseWheelDelta * options.Viewport.MouseWheelSensitivity; - if (_wheelMovementChangeDeltaSum >= step) - { - _wheelMovementChangeDeltaSum -= step; - AdjustCameraMoveSpeed(1); - } - else if (_wheelMovementChangeDeltaSum <= -step) - { - _wheelMovementChangeDeltaSum += step; - AdjustCameraMoveSpeed(-1); - } + var step = _input.MouseWheelDelta * options.Viewport.MouseWheelSensitivity; + AdjustCameraMoveSpeed(step > 0.0f ? 1 : -1); } } @@ -1495,22 +1861,6 @@ namespace FlaxEditor.Viewport new CameraViewpoint("Bottom", new Float3(-90, 0, 0)) }; - private readonly float[] EditorViewportCameraSpeedValues = - { - 0.05f, - 0.1f, - 0.25f, - 0.5f, - 1.0f, - 2.0f, - 4.0f, - 6.0f, - 8.0f, - 16.0f, - 32.0f, - 64.0f, - }; - private struct ViewModeOptions { public readonly string Name; @@ -1566,28 +1916,17 @@ namespace FlaxEditor.Viewport new ViewModeOptions(ViewMode.GlobalIllumination, "Global Illumination"), }; - private void WidgetCamSpeedShowHide(Control cm) - { - if (cm.Visible == false) - return; - - var ccm = (ContextMenu)cm; - foreach (var e in ccm.Items) - { - if (e is ContextMenuButton b) - { - var v = (float)b.Tag; - b.Icon = Mathf.Abs(MovementSpeed - v) < 0.001f - ? Style.Current.CheckBoxTick - : SpriteHandle.Invalid; - } - } - } - private void WidgetViewModeShowHideClicked(ContextMenuButton button) { if (button.Tag is ViewMode v) + { Task.ViewMode = v; + var cm = button.ParentContextMenu; + WidgetViewModeShowHide(cm); + var mainCM = ViewWidgetButtonMenu.GetChildMenu("Debug View").ContextMenu; + if (mainCM != null && cm != mainCM) + WidgetViewModeShowHide(mainCM); + } } private void WidgetViewModeShowHide(Control cm) @@ -1599,7 +1938,7 @@ namespace FlaxEditor.Viewport foreach (var e in ccm.Items) { if (e is ContextMenuButton b && b.Tag is ViewMode v) - b.Icon = Task.View.Mode == v ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; + b.Icon = Task.ViewMode == v ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; } } diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index 1dc459135..eb462deb1 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -306,6 +306,8 @@ namespace FlaxEditor.Viewport var orient = ViewOrientation; ((FPSCamera)ViewportCamera).ShowActors(TransformGizmo.SelectedParents, ref orient); } + /// + public EditorViewport Viewport => this; /// public GizmosCollection Gizmos { get; } diff --git a/Source/Editor/Viewport/Previews/MaterialPreview.cs b/Source/Editor/Viewport/Previews/MaterialPreview.cs index 5520d6c4c..46aac1cdf 100644 --- a/Source/Editor/Viewport/Previews/MaterialPreview.cs +++ b/Source/Editor/Viewport/Previews/MaterialPreview.cs @@ -7,6 +7,8 @@ using FlaxEngine.GUI; using FlaxEditor.Viewport.Widgets; using FlaxEditor.GUI.ContextMenu; using Object = FlaxEngine.Object; +using FlaxEditor.GUI; +using FlaxEditor.Scripting; namespace FlaxEditor.Viewport.Previews { @@ -49,6 +51,8 @@ namespace FlaxEditor.Viewport.Previews private Image _guiMaterialControl; private readonly MaterialBase[] _postFxMaterialsCache = new MaterialBase[1]; private ContextMenu _modelWidgetButtonMenu; + private AssetPicker _customModelPicker; + private Model _customModel; /// /// Gets or sets the material asset to preview. It can be or . @@ -74,15 +78,66 @@ namespace FlaxEditor.Viewport.Previews get => _selectedModelIndex; set { + if (value == -1) // Using Custom Model + return; if (value < 0 || value > Models.Length) throw new ArgumentOutOfRangeException(); + if (_customModelPicker != null) + _customModelPicker.Validator.SelectedAsset = null; _selectedModelIndex = value; _previewModel.Model = FlaxEngine.Content.LoadAsyncInternal("Editor/Primitives/" + Models[value]); _previewModel.Transform = Transforms[value]; } } + // Used to automatically update which entry is checked. + // TODO: Maybe a better system with predicate bool checks could be used? + private void ResetModelContextMenu() + { + _modelWidgetButtonMenu.ItemsContainer.DisposeChildren(); + + // Fill out all models + for (int i = 0; i < Models.Length; i++) + { + var index = i; + var button = _modelWidgetButtonMenu.AddButton(Models[index]); + button.ButtonClicked += _ => SelectedModelIndex = index; + button.Checked = SelectedModelIndex == index && _customModel == null; + button.Tag = index; + } + + _modelWidgetButtonMenu.AddSeparator(); + _customModelPicker = new AssetPicker(new ScriptType(typeof(Model)), Float2.Zero); + + // Label button + var customModelPickerLabel = _modelWidgetButtonMenu.AddButton("Custom Model:"); + customModelPickerLabel.CloseMenuOnClick = false; + customModelPickerLabel.Checked = _customModel != null; + + // Container button + var customModelPickerButton = _modelWidgetButtonMenu.AddButton(""); + customModelPickerButton.Height = _customModelPicker.Height + 4; + customModelPickerButton.CloseMenuOnClick = false; + _customModelPicker.Parent = customModelPickerButton; + _customModelPicker.Validator.SelectedAsset = _customModel; + _customModelPicker.SelectedItemChanged += () => + { + _customModel = _customModelPicker.Validator.SelectedAsset as Model; + if (_customModelPicker.Validator.SelectedAsset == null) + { + SelectedModelIndex = 0; + ResetModelContextMenu(); + return; + } + + _previewModel.Model = _customModel; + _previewModel.Transform = Transforms[0]; + SelectedModelIndex = -1; + ResetModelContextMenu(); + }; + } + /// /// Initializes a new instance of the class. /// @@ -107,17 +162,7 @@ namespace FlaxEditor.Viewport.Previews { if (!control.Visible) return; - _modelWidgetButtonMenu.ItemsContainer.DisposeChildren(); - - // Fill out all models - for (int i = 0; i < Models.Length; i++) - { - var index = i; - var button = _modelWidgetButtonMenu.AddButton(Models[index]); - button.ButtonClicked += _ => SelectedModelIndex = index; - button.Checked = SelectedModelIndex == index; - button.Tag = index; - } + ResetModelContextMenu(); }; new ViewportWidgetButton("Model", SpriteHandle.Invalid, _modelWidgetButtonMenu) { diff --git a/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs b/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs index 481bc3f1b..5ff5cdb6d 100644 --- a/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs +++ b/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs @@ -19,6 +19,7 @@ namespace FlaxEditor.Viewport.Widgets private bool _checked; private bool _autoCheck; private bool _isMosueDown; + private float _forcedTextWidth; /// /// Event fired when user toggles checked state. @@ -63,14 +64,16 @@ namespace FlaxEditor.Viewport.Widgets /// The text. /// The icon. /// The context menu. - /// if set to true will be automatic checked on mouse click. - public ViewportWidgetButton(string text, SpriteHandle icon, ContextMenu contextMenu = null, bool autoCheck = false) - : base(0, 0, CalculateButtonWidth(0, icon.IsValid), ViewportWidgetsContainer.WidgetsHeight) + /// If set to true will be automatic checked on mouse click. + /// Forces the text to be drawn with the specified width. + public ViewportWidgetButton(string text, SpriteHandle icon, ContextMenu contextMenu = null, bool autoCheck = false, float textWidth = 0.0f) + : base(0, 0, CalculateButtonWidth(textWidth, icon.IsValid), ViewportWidgetsContainer.WidgetsHeight) { _text = text; Icon = icon; _cm = contextMenu; _autoCheck = autoCheck; + _forcedTextWidth = textWidth; if (_cm != null) _cm.VisibleChanged += CmOnVisibleChanged; @@ -160,7 +163,7 @@ namespace FlaxEditor.Viewport.Widgets var style = Style.Current; if (style != null && style.FontMedium) - Width = CalculateButtonWidth(style.FontMedium.MeasureText(_text).X, Icon.IsValid); + Width = CalculateButtonWidth(_forcedTextWidth > 0.0f ? _forcedTextWidth : style.FontMedium.MeasureText(_text).X, Icon.IsValid); } } } diff --git a/Source/Editor/Windows/AssetReferencesGraphWindow.cs b/Source/Editor/Windows/AssetReferencesGraphWindow.cs index d49896e2e..b9e0e7257 100644 --- a/Source/Editor/Windows/AssetReferencesGraphWindow.cs +++ b/Source/Editor/Windows/AssetReferencesGraphWindow.cs @@ -46,14 +46,14 @@ namespace FlaxEditor.Windows if (asset != null) { var path = asset.Path; - picker.SelectedAsset = asset; + picker.Validator.SelectedAsset = asset; Title = System.IO.Path.GetFileNameWithoutExtension(path); TooltipText = asset.TypeName + '\n' + path; } else { - picker.SelectedID = AssetId; - var assetItem = picker.SelectedItem as AssetItem; + picker.Validator.SelectedID = AssetId; + var assetItem = picker.Validator.SelectedItem as AssetItem; if (assetItem != null) { Title = assetItem.ShortName; diff --git a/Source/Editor/Windows/Assets/AnimationWindow.cs b/Source/Editor/Windows/Assets/AnimationWindow.cs index a765c2faa..8f88d93f6 100644 --- a/Source/Editor/Windows/Assets/AnimationWindow.cs +++ b/Source/Editor/Windows/Assets/AnimationWindow.cs @@ -230,6 +230,8 @@ namespace FlaxEditor.Windows.Assets public AnimationWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -265,8 +267,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/animation/animation/index.html")).LinkTooltip("See documentation to learn more"); diff --git a/Source/Editor/Windows/Assets/AssetEditorWindow.cs b/Source/Editor/Windows/Assets/AssetEditorWindow.cs index 0d244479c..2e048b924 100644 --- a/Source/Editor/Windows/Assets/AssetEditorWindow.cs +++ b/Source/Editor/Windows/Assets/AssetEditorWindow.cs @@ -388,14 +388,16 @@ namespace FlaxEditor.Windows.Assets protected override void OnShow() { // Check if has no asset (but has item linked) - if (_asset == null && _item != null) + var item = _item; + if (_asset == null && item != null) { // Load asset _asset = LoadAsset(); if (_asset == null) { - Editor.LogError(string.Format("Cannot load asset \'{0}\' ({1})", _item.Path, typeof(T))); + Editor.LogError(string.Format("Cannot load asset \'{0}\' ({1})", item.Path, typeof(T))); Close(); + Editor.ContentDatabase.RefreshFolder(item, false); return; } diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs index 8cbf6cf75..01ee9cb8a 100644 --- a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs +++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs @@ -130,6 +130,8 @@ namespace FlaxEditor.Windows.Assets public BehaviorTreeWindow(Editor editor, BinaryAssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -172,10 +174,10 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); - _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); + _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip($"Open content search tool ({inputOptions.Search})"); _toolstrip.AddButton(editor.Icons.CenterView64, _surface.ShowWholeGraph).LinkTooltip("Show whole graph"); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/scripting/ai/behavior-trees/index.html")).LinkTooltip("See documentation to learn more"); diff --git a/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs b/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs index 623c4ef5b..a8121162a 100644 --- a/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs +++ b/Source/Editor/Windows/Assets/GameplayGlobalsWindow.cs @@ -395,6 +395,8 @@ namespace FlaxEditor.Windows.Assets public GameplayGlobalsWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + _undo = new Undo(); _undo.ActionDone += OnUndo; _undo.UndoDone += OnUndo; @@ -411,10 +413,10 @@ namespace FlaxEditor.Windows.Assets _proxy = new PropertiesProxy(); _propertiesEditor.Select(_proxy); - _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip("Save asset"); + _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip($"Save asset ({inputOptions.Save})"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _resetButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Rotate32, Reset).LinkTooltip("Resets the variables values to the default values"); diff --git a/Source/Editor/Windows/Assets/JsonAssetWindow.cs b/Source/Editor/Windows/Assets/JsonAssetWindow.cs index 097d4992a..a1178fb68 100644 --- a/Source/Editor/Windows/Assets/JsonAssetWindow.cs +++ b/Source/Editor/Windows/Assets/JsonAssetWindow.cs @@ -34,6 +34,8 @@ namespace FlaxEditor.Windows.Assets public JsonAssetWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -43,8 +45,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); // Panel var panel = new Panel(ScrollBars.Vertical) diff --git a/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs b/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs index 0de5ce315..85f351fef 100644 --- a/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs +++ b/Source/Editor/Windows/Assets/LocalizedStringTableWindow.cs @@ -126,6 +126,8 @@ namespace FlaxEditor.Windows.Assets public LocalizedStringTableWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -135,8 +137,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(Editor.Icons.Up64, OnExport).LinkTooltip("Export localization table entries for translation to .pot file"); diff --git a/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs b/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs index 5f1273999..775e0c0dc 100644 --- a/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs +++ b/Source/Editor/Windows/Assets/MaterialInstanceWindow.cs @@ -375,6 +375,8 @@ namespace FlaxEditor.Windows.Assets public MaterialInstanceWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -384,8 +386,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(Editor.Icons.Rotate64, OnRevertAllParameters).LinkTooltip("Revert all the parameters to the default values"); _toolstrip.AddSeparator(); @@ -521,8 +523,11 @@ namespace FlaxEditor.Windows.Assets /// protected override void OnClose() { - // Discard unsaved changes - _properties.DiscardChanges(); + if (Asset) + { + // Discard unsaved changes + _properties.DiscardChanges(); + } // Cleanup _undo.Clear(); diff --git a/Source/Editor/Windows/Assets/MaterialWindow.cs b/Source/Editor/Windows/Assets/MaterialWindow.cs index 6fbe32d7e..5aa77dbc3 100644 --- a/Source/Editor/Windows/Assets/MaterialWindow.cs +++ b/Source/Editor/Windows/Assets/MaterialWindow.cs @@ -41,8 +41,6 @@ namespace FlaxEditor.Windows.Assets new ScriptType(typeof(Vector3)), new ScriptType(typeof(Vector4)), new ScriptType(typeof(Color)), - new ScriptType(typeof(Quaternion)), - new ScriptType(typeof(Transform)), new ScriptType(typeof(Matrix)), }; diff --git a/Source/Editor/Windows/Assets/ParticleSystemWindow.cs b/Source/Editor/Windows/Assets/ParticleSystemWindow.cs index 17eda1358..4a5e02e6e 100644 --- a/Source/Editor/Windows/Assets/ParticleSystemWindow.cs +++ b/Source/Editor/Windows/Assets/ParticleSystemWindow.cs @@ -306,6 +306,8 @@ namespace FlaxEditor.Windows.Assets public ParticleSystemWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -359,8 +361,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _toolstrip.AddButton(editor.Icons.Docs64, () => Platform.OpenUrl(Utilities.Constants.DocsUrl + "manual/particles/index.html")).LinkTooltip("See documentation to learn more"); diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs index 70aa1dca3..a8d9ae1be 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs @@ -339,7 +339,7 @@ namespace FlaxEditor.Windows.Assets { if (selection.Count != 0) Select(actor); - actor.TreeNode.StartRenaming(this); + actor.TreeNode.StartRenaming(this, _treePanel); } } diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs index 4face0dc0..f50a832a1 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.cs @@ -94,6 +94,8 @@ namespace FlaxEditor.Windows.Assets public PrefabWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoEvent; @@ -149,6 +151,7 @@ namespace FlaxEditor.Windows.Assets // Prefab structure tree Graph = new LocalSceneGraph(new CustomRootNode(this)); + Graph.Root.TreeNode.Expand(true); _tree = new PrefabTree { Margin = new Margin(0.0f, 0.0f, -16.0f, _treePanel.ScrollBarsSize), // Hide root node @@ -175,12 +178,12 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _toolStripUndo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _toolStripRedo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _toolStripUndo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _toolStripRedo = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); - _toolStripTranslate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Translate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate).LinkTooltip("Change Gizmo tool mode to Translate (1)"); - _toolStripRotate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Rotate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate).LinkTooltip("Change Gizmo tool mode to Rotate (2)"); - _toolStripScale = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Scale32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip("Change Gizmo tool mode to Scale (3)"); + _toolStripTranslate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Translate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate).LinkTooltip($"Change Gizmo tool mode to Translate ({inputOptions.TranslateMode})"); + _toolStripRotate = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Rotate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate).LinkTooltip($"Change Gizmo tool mode to Rotate ({inputOptions.RotateMode})"); + _toolStripScale = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Scale32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip($"Change Gizmo tool mode to Scale ({inputOptions.ScaleMode})"); _toolstrip.AddSeparator(); _toolStripLiveReload = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Refresh64, () => LiveReload = !LiveReload).SetChecked(true).SetAutoCheck(true).LinkTooltip("Live changes preview (applies prefab changes on modification by auto)"); @@ -317,7 +320,7 @@ namespace FlaxEditor.Windows.Assets Graph.MainActor = _viewport.Instance; Selection.Clear(); Select(Graph.Main); - Graph.Root.TreeNode.ExpandAll(true); + Graph.Root.TreeNode.Expand(true); _undo.Clear(); ClearEditedFlag(); } @@ -413,7 +416,7 @@ namespace FlaxEditor.Windows.Assets _focusCamera = true; Selection.Clear(); Select(Graph.Main); - Graph.Root.TreeNode.ExpandAll(true); + Graph.Root.TreeNode.Expand(true); _undo.Clear(); ClearEditedFlag(); diff --git a/Source/Editor/Windows/Assets/SceneAnimationWindow.cs b/Source/Editor/Windows/Assets/SceneAnimationWindow.cs index 162944144..05435cc77 100644 --- a/Source/Editor/Windows/Assets/SceneAnimationWindow.cs +++ b/Source/Editor/Windows/Assets/SceneAnimationWindow.cs @@ -627,6 +627,8 @@ namespace FlaxEditor.Windows.Assets public SceneAnimationWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; @@ -652,8 +654,8 @@ namespace FlaxEditor.Windows.Assets // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); - _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); - _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); + _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip($"Undo ({inputOptions.Undo})"); + _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip($"Redo ({inputOptions.Redo})"); _toolstrip.AddSeparator(); _previewButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Refresh64, OnPreviewButtonClicked).SetAutoCheck(true).LinkTooltip("If checked, enables live-preview of the animation on a scene while editing"); _renderButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Build64, OnRenderButtonClicked).LinkTooltip("Open the scene animation rendering utility..."); diff --git a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs index d8790172b..ccfd5233c 100644 --- a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs +++ b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs @@ -837,7 +837,7 @@ namespace FlaxEditor.Windows.Assets sourceAssetPicker.CheckValid = CheckSourceAssetValid; sourceAssetPicker.SelectedItemChanged += () => { - proxy.Setups.Add(sourceAssetPicker.SelectedAsset, new SetupProxy()); + proxy.Setups.Add(sourceAssetPicker.Validator.SelectedAsset, new SetupProxy()); proxy.Window.MarkAsEdited(); RebuildLayout(); }; @@ -856,7 +856,7 @@ namespace FlaxEditor.Windows.Assets // Source asset picker var sourceAssetPicker = setupGroup.AddPropertyItem("Source Asset").Custom().CustomControl; - sourceAssetPicker.SelectedAsset = sourceAsset; + sourceAssetPicker.Validator.SelectedAsset = sourceAsset; sourceAssetPicker.CanEdit = false; sourceAssetPicker.Height = 48; @@ -916,12 +916,12 @@ namespace FlaxEditor.Windows.Assets { // Show skeleton asset picker var sourceSkeletonPicker = setupGroup.AddPropertyItem("Skeleton", "Skinned model that contains a skeleton for this animation retargeting.").Custom().CustomControl; - sourceSkeletonPicker.AssetType = new ScriptType(typeof(SkinnedModel)); - sourceSkeletonPicker.SelectedAsset = setup.Value.Skeleton; + sourceSkeletonPicker.Validator.AssetType = new ScriptType(typeof(SkinnedModel)); + sourceSkeletonPicker.Validator.SelectedAsset = setup.Value.Skeleton; sourceSkeletonPicker.Height = 48; sourceSkeletonPicker.SelectedItemChanged += () => { - setup.Value.Skeleton = (SkinnedModel)sourceSkeletonPicker.SelectedAsset; + setup.Value.Skeleton = (SkinnedModel)sourceSkeletonPicker.Validator.SelectedAsset; proxy.Window.MarkAsEdited(); }; } diff --git a/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs b/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs index 9750fa3e6..b285ddbed 100644 --- a/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs +++ b/Source/Editor/Windows/Assets/VisjectFunctionSurfaceWindow.cs @@ -62,6 +62,8 @@ namespace FlaxEditor.Windows.Assets protected VisjectFunctionSurfaceWindow(Editor editor, AssetItem item) : base(editor, item) { + var inputOptions = Editor.Options.Options.Input; + // Undo _undo = new Undo(); _undo.UndoDone += OnUndoRedo; diff --git a/Source/Editor/Windows/Assets/VisualScriptWindow.cs b/Source/Editor/Windows/Assets/VisualScriptWindow.cs index 74187652e..d6e3098a5 100644 --- a/Source/Editor/Windows/Assets/VisualScriptWindow.cs +++ b/Source/Editor/Windows/Assets/VisualScriptWindow.cs @@ -562,6 +562,7 @@ namespace FlaxEditor.Windows.Assets : base(editor, item) { var isPlayMode = Editor.IsPlayMode; + var inputOptions = Editor.Options.Options.Input; // Undo _undo = new Undo(); @@ -607,11 +608,11 @@ namespace FlaxEditor.Windows.Assets _debugToolstripControls = new[] { _toolstrip.AddSeparator(), - _toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip("Continue (F5)"), + _toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip($"Continue ({inputOptions.DebuggerContinue})"), _toolstrip.AddButton(editor.Icons.Search64, OnDebuggerNavigateToCurrentNode).LinkTooltip("Navigate to the current stack trace node"), - _toolstrip.AddButton(editor.Icons.Right64, OnDebuggerStepOver).LinkTooltip("Step Over (F10)"), - _toolstrip.AddButton(editor.Icons.Down64, OnDebuggerStepInto).LinkTooltip("Step Into (F11)"), - _toolstrip.AddButton(editor.Icons.Up64, OnDebuggerStepOut).LinkTooltip("Step Out (Shift+F11)"), + _toolstrip.AddButton(editor.Icons.Right64, OnDebuggerStepOver).LinkTooltip($"Step Over ({inputOptions.DebuggerStepOver})"), + _toolstrip.AddButton(editor.Icons.Down64, OnDebuggerStepInto).LinkTooltip($"Step Into ({inputOptions.DebuggerStepInto})"), + _toolstrip.AddButton(editor.Icons.Up64, OnDebuggerStepOut).LinkTooltip($"Step Out ({inputOptions.DebuggerStepOut})"), _toolstrip.AddButton(editor.Icons.Stop64, OnDebuggerStop).LinkTooltip("Stop debugging"), }; foreach (var control in _debugToolstripControls) diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 03873df57..168067977 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -2,6 +2,7 @@ using System; using System.IO; +using System.Linq; using FlaxEditor.Content; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Scripting; @@ -186,12 +187,12 @@ namespace FlaxEditor.Windows continue; // Get context proxy - ContentProxy p; + ContentProxy p = null; if (type.Type.IsSubclassOf(typeof(ContentProxy))) { p = Editor.ContentDatabase.Proxy.Find(x => x.GetType() == type.Type); } - else + else if (type.CanCreateInstance) { // User can use attribute to put their own assets into the content context menu var generic = typeof(SpawnableJsonAssetProxy<>).MakeGenericType(type.Type); @@ -249,6 +250,10 @@ namespace FlaxEditor.Windows }); } + // Remove any leftover separator + if (cm.ItemsContainer.Children.LastOrDefault() is ContextMenuSeparator) + cm.ItemsContainer.Children.Last().Dispose(); + // Show it cm.Show(this, location); } @@ -364,7 +369,7 @@ namespace FlaxEditor.Windows } var pluginPath = Path.Combine(Globals.ProjectFolder, "Source", nameTextBox.Text); - if (Directory.Exists(pluginPath)) + if (!IsValidModuleName(nameTextBox.Text) || Directory.Exists(pluginPath)) { nameTextBox.BorderColor = Color.Red; nameTextBox.BorderSelectedColor = Color.Red; @@ -424,6 +429,12 @@ namespace FlaxEditor.Windows submitButton.Clicked += () => { // TODO: Check all modules in project including plugins + if (!IsValidModuleName(nameTextBox.Text)) + { + Editor.LogWarning("Invalid module name. Module names cannot contain spaces, start with a number or contain non-alphanumeric characters."); + return; + } + if (Directory.Exists(Path.Combine(Globals.ProjectFolder, "Source", nameTextBox.Text))) { Editor.LogWarning("Cannot create module due to name conflict."); @@ -455,5 +466,16 @@ namespace FlaxEditor.Windows button.ParentContextMenu.Hide(); }; } + + private static bool IsValidModuleName(string text) + { + if (text.Contains(' ')) + return false; + if (char.IsDigit(text[0])) + return false; + if (text.Any(c => !char.IsLetterOrDigit(c) && c != '_')) + return false; + return true; + } } } diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index a06ec839d..d43174cf3 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -629,8 +629,9 @@ namespace FlaxEditor.Windows if (items.Count == 0) return; - // TODO: remove items that depend on different items in the list: use wants to remove `folderA` and `folderA/asset.x`, we should just remove `folderA` + // Sort items to remove files first, then folders var toDelete = new List(items); + toDelete.Sort((a, b) => a.IsFolder ? 1 : b.IsFolder ? -1 : a.Compare(b)); string msg = toDelete.Count == 1 ? string.Format("Are you sure to delete \'{0}\'?\nThis action cannot be undone. Files will be deleted permanently.", items[0].Path) diff --git a/Source/Editor/Windows/GameCookerWindow.cs b/Source/Editor/Windows/GameCookerWindow.cs index 5ece067a0..2ef9c05cf 100644 --- a/Source/Editor/Windows/GameCookerWindow.cs +++ b/Source/Editor/Windows/GameCookerWindow.cs @@ -155,29 +155,42 @@ namespace FlaxEditor.Windows public virtual void OnNotAvailableLayout(LayoutElementsContainer layout) { - layout.Label("Missing platform data tools for the target platform.", TextAlignment.Center); + string text = "Missing platform data tools for the target platform."; if (FlaxEditor.Editor.IsOfficialBuild()) { switch (BuildPlatform) { +#if PLATFORM_WINDOWS case BuildPlatform.Windows32: case BuildPlatform.Windows64: case BuildPlatform.UWPx86: case BuildPlatform.UWPx64: case BuildPlatform.LinuxX64: case BuildPlatform.AndroidARM64: - layout.Label("Use Flax Launcher and download the required package.", TextAlignment.Center); + text += "\nUse Flax Launcher and download the required package."; break; +#endif default: - layout.Label("Engine source is required to target this platform.", TextAlignment.Center); + text += "\nEngine source is required to target this platform."; break; } } else { - var label = layout.Label("To target this platform separate engine source package is required.\nTo get access please contact via https://flaxengine.com/contact", TextAlignment.Center); - label.Label.AutoHeight = true; + text += "\nTo target this platform separate engine source package is required."; + switch (BuildPlatform) + { + case BuildPlatform.XboxOne: + case BuildPlatform.XboxScarlett: + case BuildPlatform.PS4: + case BuildPlatform.PS5: + case BuildPlatform.Switch: + text += "\nTo get access please contact via https://flaxengine.com/contact"; + break; + } } + var label = layout.Label(text, TextAlignment.Center); + label.Label.AutoHeight = true; } public virtual void Build() diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index fdedbb3c2..8c6f5dc06 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -271,8 +271,6 @@ namespace FlaxEditor.Windows Title = "Game"; AutoFocus = true; - FlaxEditor.Utilities.Utils.SetupCommonInputActions(this); - var task = MainRenderTask.Instance; // Setup viewport @@ -304,6 +302,12 @@ namespace FlaxEditor.Windows // Link editor options Editor.Options.OptionsChanged += OnOptionsChanged; OnOptionsChanged(Editor.Options.Options); + + InputActions.Add(options => options.TakeScreenshot, () => Screenshot.Capture(string.Empty)); + InputActions.Add(options => options.DebuggerUnlockMouse, UnlockMouseInPlay); + InputActions.Add(options => options.ToggleFullscreen, () => { if (Editor.IsPlayMode) IsMaximized = !IsMaximized; }); + + FlaxEditor.Utilities.Utils.SetupCommonInputActions(this); } private void ChangeViewportRatio(ViewportScaleOptions v) @@ -945,27 +949,6 @@ namespace FlaxEditor.Windows /// public override bool OnKeyDown(KeyboardKeys key) { - switch (key) - { - case KeyboardKeys.F12: - Screenshot.Capture(string.Empty); - return true; - case KeyboardKeys.F11: - if (Root.GetKey(KeyboardKeys.Shift)) - { - // Unlock mouse in game mode - UnlockMouseInPlay(); - return true; - } - else if (Editor.IsPlayMode) - { - // Maximized game window toggle - IsMaximized = !IsMaximized; - return true; - } - break; - } - // Prevent closing the game window tab during a play session if (Editor.StateMachine.IsPlayMode && Editor.Options.Options.Input.CloseTab.Process(this, key)) { diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs index 3665b7073..6526d7c8a 100644 --- a/Source/Editor/Windows/OutputLogWindow.cs +++ b/Source/Editor/Windows/OutputLogWindow.cs @@ -467,6 +467,7 @@ namespace FlaxEditor.Windows if (_isDirty) { _isDirty = false; + var wasEmpty = _output.TextLength == 0; // Cache fonts _output.DefaultStyle.Font.GetFont(); @@ -589,7 +590,7 @@ namespace FlaxEditor.Windows // Update the output var cachedScrollValue = _vScroll.Value; var cachedSelection = _output.SelectionRange; - var isBottomScroll = _vScroll.Value >= _vScroll.Maximum - 20.0f; + var isBottomScroll = _vScroll.Value >= _vScroll.Maximum - 20.0f || wasEmpty; _output.Text = _textBuffer.ToString(); _textBufferCount = _entries.Count; if (!_vScroll.IsThumbClicked) diff --git a/Source/Editor/Windows/Profiler/Network.cs b/Source/Editor/Windows/Profiler/Network.cs index 78d84ad9d..d29000d27 100644 --- a/Source/Editor/Windows/Profiler/Network.cs +++ b/Source/Editor/Windows/Profiler/Network.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using FlaxEditor.GUI; using FlaxEngine; using FlaxEngine.GUI; +using FlaxEngine.Networking; namespace FlaxEngine { @@ -37,11 +38,14 @@ namespace FlaxEditor.Windows.Profiler { private readonly SingleChart _dataSentChart; private readonly SingleChart _dataReceivedChart; + private readonly SingleChart _dataSentRateChart; + private readonly SingleChart _dataReceivedRateChart; private readonly Table _tableRpc; private readonly Table _tableRep; - private SamplesBuffer _events; private List _tableRowsCache; - private FlaxEngine.Networking.NetworkDriverStats _prevStats; + private SamplesBuffer _events; + private NetworkDriverStats _prevStats; + private List _stats; public Network() : base("Network") @@ -76,6 +80,20 @@ namespace FlaxEditor.Windows.Profiler Parent = layout, }; _dataReceivedChart.SelectedSampleChanged += OnSelectedSampleChanged; + _dataSentRateChart = new SingleChart + { + Title = "Data Sent Rate", + FormatSample = FormatSampleBytesRate, + Parent = layout, + }; + _dataSentRateChart.SelectedSampleChanged += OnSelectedSampleChanged; + _dataReceivedRateChart = new SingleChart + { + Title = "Data Received Rate", + FormatSample = FormatSampleBytesRate, + Parent = layout, + }; + _dataReceivedRateChart.SelectedSampleChanged += OnSelectedSampleChanged; // Tables _tableRpc = InitTable(layout, "RPC Name"); @@ -87,24 +105,52 @@ namespace FlaxEditor.Windows.Profiler { _dataSentChart.Clear(); _dataReceivedChart.Clear(); + _dataSentRateChart.Clear(); + _dataReceivedRateChart.Clear(); _events?.Clear(); + _stats?.Clear(); + _prevStats = new NetworkDriverStats(); } /// public override void Update(ref SharedUpdateData sharedData) { // Gather peer stats - var peers = FlaxEngine.Networking.NetworkPeer.Peers; - var stats = new FlaxEngine.Networking.NetworkDriverStats(); + var peers = NetworkPeer.Peers; + var thisStats = new NetworkDriverStats(); + thisStats.RTT = Time.UnscaledGameTime; // Store sample time in RTT foreach (var peer in peers) { var peerStats = peer.NetworkDriver.GetStats(); - stats.TotalDataSent += peerStats.TotalDataSent; - stats.TotalDataReceived += peerStats.TotalDataReceived; + thisStats.TotalDataSent += peerStats.TotalDataSent; + thisStats.TotalDataReceived += peerStats.TotalDataReceived; } - _dataSentChart.AddSample(Mathf.Max((long)stats.TotalDataSent - (long)_prevStats.TotalDataSent, 0)); - _dataReceivedChart.AddSample(Mathf.Max((long)stats.TotalDataReceived - (long)_prevStats.TotalDataReceived, 0)); - _prevStats = stats; + var stats = thisStats; + stats.TotalDataSent = (uint)Mathf.Max((long)thisStats.TotalDataSent - (long)_prevStats.TotalDataSent, 0); + stats.TotalDataReceived = (uint)Mathf.Max((long)thisStats.TotalDataReceived - (long)_prevStats.TotalDataReceived, 0); + _dataSentChart.AddSample(stats.TotalDataSent); + _dataReceivedChart.AddSample(stats.TotalDataReceived); + _prevStats = thisStats; + if (_stats == null) + _stats = new List(); + _stats.Add(stats); + + // Remove all stats older than 1 second + while (_stats.Count > 0 && thisStats.RTT - _stats[0].RTT >= 1.0f) + _stats.RemoveAt(0); + + // Calculate average data rates (from last second) + var avgStats = new NetworkDriverStats(); + foreach (var e in _stats) + { + avgStats.TotalDataSent += e.TotalDataSent; + avgStats.TotalDataReceived += e.TotalDataReceived; + } + avgStats.TotalDataSent /= (uint)_stats.Count; + avgStats.TotalDataReceived /= (uint)_stats.Count; + _dataSentRateChart.AddSample(avgStats.TotalDataSent); + _dataReceivedRateChart.AddSample(avgStats.TotalDataReceived); + // Gather network events var events = ProfilingTools.EventsNetwork; @@ -118,6 +164,8 @@ namespace FlaxEditor.Windows.Profiler { _dataSentChart.SelectedSampleIndex = selectedFrame; _dataReceivedChart.SelectedSampleIndex = selectedFrame; + _dataSentRateChart.SelectedSampleIndex = selectedFrame; + _dataReceivedRateChart.SelectedSampleIndex = selectedFrame; // Update events tables if (_events != null) @@ -257,6 +305,11 @@ namespace FlaxEditor.Windows.Profiler return Utilities.Utils.FormatBytesCount((ulong)v); } + private static string FormatSampleBytesRate(float v) + { + return Utilities.Utils.FormatBytesCount((ulong)v) + "/s"; + } + private static string FormatCellBytes(object x) { return Utilities.Utils.FormatBytesCount((int)x); diff --git a/Source/Editor/Windows/Profiler/SamplesBuffer.cs b/Source/Editor/Windows/Profiler/SamplesBuffer.cs index 999156dca..2e0169d47 100644 --- a/Source/Editor/Windows/Profiler/SamplesBuffer.cs +++ b/Source/Editor/Windows/Profiler/SamplesBuffer.cs @@ -49,7 +49,7 @@ namespace FlaxEditor.Windows.Profiler /// The sample value public T Get(int index) { - if (index >= _data.Length || _data.Length == 0) + if (_count == 0 || index >= _data.Length || _data.Length == 0) return default; return index == -1 ? _data[_count - 1] : _data[index]; } diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index cbaa27371..250bd2301 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -142,7 +142,7 @@ namespace FlaxEditor.Windows { if (selection.Count != 0) Editor.SceneEditing.Select(actor); - actor.TreeNode.StartRenaming(this); + actor.TreeNode.StartRenaming(this, _sceneTreePanel); } } diff --git a/Source/Editor/Windows/VisualScriptDebuggerWindow.cs b/Source/Editor/Windows/VisualScriptDebuggerWindow.cs index c84a9ba33..57f10f2f4 100644 --- a/Source/Editor/Windows/VisualScriptDebuggerWindow.cs +++ b/Source/Editor/Windows/VisualScriptDebuggerWindow.cs @@ -399,6 +399,8 @@ namespace FlaxEditor.Windows { Title = "Visual Script Debugger"; + var inputOptions = editor.Options.Options.Input; + var toolstrip = new ToolStrip { Parent = this @@ -407,7 +409,7 @@ namespace FlaxEditor.Windows _debugToolstripControls = new[] { toolstrip.AddSeparator(), - toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip("Continue (F5)"), + toolstrip.AddButton(editor.Icons.Play64, OnDebuggerContinue).LinkTooltip($"Continue ({inputOptions.DebuggerContinue})"), toolstrip.AddButton(editor.Icons.Search64, OnDebuggerNavigateToCurrentNode).LinkTooltip("Navigate to the current stack trace node"), toolstrip.AddButton(editor.Icons.Stop64, OnDebuggerStop).LinkTooltip("Stop debugging"), }; diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index 0518fe248..5cceb31e3 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -2109,7 +2109,8 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu bucket.LoopsLeft--; bucket.LoopsDone++; } - value = SampleAnimation(node, loop, length, 0.0f, bucket.TimePosition, newTimePos, anim, slot.Speed); + // Speed is accounted for in the new time pos, so keep sample speed at 1 + value = SampleAnimation(node, loop, length, 0.0f, bucket.TimePosition, newTimePos, anim, 1); bucket.TimePosition = newTimePos; if (bucket.LoopsLeft == 0 && slot.BlendOutTime > 0.0f && length - slot.BlendOutTime < bucket.TimePosition) { diff --git a/Source/Engine/Audio/AudioClip.cpp b/Source/Engine/Audio/AudioClip.cpp index d04da7274..689f38d12 100644 --- a/Source/Engine/Audio/AudioClip.cpp +++ b/Source/Engine/Audio/AudioClip.cpp @@ -225,6 +225,7 @@ bool AudioClip::ExtractDataRaw(Array& resultData, AudioDataInfo& resultDat void AudioClip::CancelStreaming() { + Asset::CancelStreaming(); CancelStreamingTasks(); } diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index 2f65a4b6c..ace8b6591 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -33,7 +33,8 @@ int alError = alGetError(); \ if (alError != 0) \ { \ - LOG(Error, "OpenAL method {0} failed with error 0x{1:X} (at line {2})", TEXT(#method), alError, __LINE__ - 1); \ + const Char* errorStr = GetOpenALErrorString(alError); \ + LOG(Error, "OpenAL method {0} failed with error 0x{1:X}:{2} (at line {3})", TEXT(#method), alError, errorStr, __LINE__ - 1); \ } \ } #endif @@ -290,6 +291,28 @@ ALenum GetOpenALBufferFormat(uint32 numChannels, uint32 bitDepth) return 0; } +const Char* GetOpenALErrorString(int error) +{ + switch (error) + { + case AL_NO_ERROR: + return TEXT("AL_NO_ERROR"); + case AL_INVALID_NAME: + return TEXT("AL_INVALID_NAME"); + case AL_INVALID_ENUM: + return TEXT("AL_INVALID_ENUM"); + case AL_INVALID_VALUE: + return TEXT("AL_INVALID_VALUE"); + case AL_INVALID_OPERATION: + return TEXT("AL_INVALID_OPERATION"); + case AL_OUT_OF_MEMORY: + return TEXT("AL_OUT_OF_MEMORY"); + default: + break; + } + return TEXT("???"); +} + void AudioBackendOAL::Listener_OnAdd(AudioListener* listener) { #if ALC_MULTIPLE_LISTENERS @@ -838,7 +861,11 @@ bool AudioBackendOAL::Base_Init() // Init Base_SetDopplerFactor(AudioSettings::Get()->DopplerFactor); alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); // Default attenuation model - ALC::RebuildContexts(true); + int32 clampedIndex = Math::Clamp(activeDeviceIndex, -1, Audio::Devices.Count() - 1); + if (clampedIndex == Audio::GetActiveDeviceIndex()) + { + ALC::RebuildContexts(true); + } Audio::SetActiveDeviceIndex(activeDeviceIndex); #ifdef AL_SOFT_source_spatialize if (ALC::IsExtensionSupported("AL_SOFT_source_spatialize")) diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index 309a82e04..93d904d5e 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -16,11 +16,13 @@ AssetReferenceBase::~AssetReferenceBase() { - if (_asset) + Asset* asset = _asset; + if (asset) { - _asset->OnLoaded.Unbind(this); - _asset->OnUnloaded.Unbind(this); - _asset->RemoveReference(); + _asset = nullptr; + asset->OnLoaded.Unbind(this); + asset->OnUnloaded.Unbind(this); + asset->RemoveReference(); } } @@ -70,8 +72,12 @@ void AssetReferenceBase::OnUnloaded(Asset* asset) WeakAssetReferenceBase::~WeakAssetReferenceBase() { - if (_asset) - _asset->OnUnloaded.Unbind(this); + Asset* asset = _asset; + if (asset) + { + _asset = nullptr; + asset->OnUnloaded.Unbind(this); + } } String WeakAssetReferenceBase::ToString() const @@ -101,6 +107,20 @@ void WeakAssetReferenceBase::OnUnloaded(Asset* asset) _asset = nullptr; } +SoftAssetReferenceBase::~SoftAssetReferenceBase() +{ + Asset* asset = _asset; + if (asset) + { + _asset = nullptr; + asset->OnUnloaded.Unbind(this); + asset->RemoveReference(); + } +#if !BUILD_RELEASE + _id = Guid::Empty; +#endif +} + String SoftAssetReferenceBase::ToString() const { return _asset ? _asset->ToString() : (_id.IsValid() ? _id.ToString() : TEXT("")); @@ -502,6 +522,14 @@ void Asset::InitAsVirtual() void Asset::CancelStreaming() { + // Cancel loading task but go over asset locker to prevent case if other load threads still loads asset while it's reimported on other thread + Locker.Lock(); + ContentLoadTask* loadTask = _loadingTask; + Locker.Unlock(); + if (loadTask) + { + loadTask->Cancel(); + } } #if USE_EDITOR @@ -538,11 +566,7 @@ ContentLoadTask* Asset::createLoadingTask() void Asset::startLoading() { - // Check if is already loaded - if (IsLoaded()) - return; - - // Start loading (using async tasks) + ASSERT(!IsLoaded()); ASSERT(_loadingTask == nullptr); _loadingTask = createLoadingTask(); ASSERT(_loadingTask != nullptr); diff --git a/Source/Engine/Content/AssetReference.h b/Source/Engine/Content/AssetReference.h index 6aee12246..b9d54f30a 100644 --- a/Source/Engine/Content/AssetReference.h +++ b/Source/Engine/Content/AssetReference.h @@ -9,9 +9,6 @@ /// class FLAXENGINE_API AssetReferenceBase { -public: - typedef Delegate<> EventType; - protected: Asset* _asset = nullptr; @@ -19,17 +16,17 @@ public: /// /// The asset loaded event (fired when asset gets loaded or is already loaded after change). /// - EventType Loaded; + Action Loaded; /// /// The asset unloading event (should cleanup refs to it). /// - EventType Unload; + Action Unload; /// /// Action fired when field gets changed (link a new asset or change to the another value). /// - EventType Changed; + Action Changed; public: NON_COPYABLE(AssetReferenceBase); diff --git a/Source/Engine/Content/Assets/Material.cpp b/Source/Engine/Content/Assets/Material.cpp index 76ed34b80..276b6b1f7 100644 --- a/Source/Engine/Content/Assets/Material.cpp +++ b/Source/Engine/Content/Assets/Material.cpp @@ -20,6 +20,7 @@ #include "Engine/ShadersCompilation/Config.h" #if BUILD_DEBUG #include "Engine/Engine/Globals.h" +#include "Engine/Scripting/BinaryModule.h" #endif #endif @@ -256,7 +257,9 @@ Asset::LoadResult Material::load() #if BUILD_DEBUG && USE_EDITOR // Dump generated material source to the temporary file + BinaryModule::Locker.Lock(); source.SaveToFile(Globals::ProjectCacheFolder / TEXT("material.txt")); + BinaryModule::Locker.Unlock(); #endif // Encrypt source code diff --git a/Source/Engine/Content/Assets/Model.cpp b/Source/Engine/Content/Assets/Model.cpp index 5a008645d..691b00a50 100644 --- a/Source/Engine/Content/Assets/Model.cpp +++ b/Source/Engine/Content/Assets/Model.cpp @@ -783,6 +783,7 @@ void Model::InitAsVirtual() void Model::CancelStreaming() { + Asset::CancelStreaming(); CancelStreamingTasks(); } diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index 5871087d9..b823db5a3 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -969,6 +969,7 @@ void SkinnedModel::InitAsVirtual() void SkinnedModel::CancelStreaming() { + Asset::CancelStreaming(); CancelStreamingTasks(); } diff --git a/Source/Engine/Content/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp index 105d4ad2d..9748ba60c 100644 --- a/Source/Engine/Content/Assets/VisualScript.cpp +++ b/Source/Engine/Content/Assets/VisualScript.cpp @@ -1428,6 +1428,10 @@ Asset::LoadResult VisualScript::load() #if USE_EDITOR if (_instances.HasItems()) { + // Mark as already loaded so any WaitForLoaded checks during GetDefaultInstance bellow will handle this Visual Script as ready to use + _loadFailed = false; + _isLoaded = true; + // Setup scripting type CacheScriptingType(); @@ -1512,7 +1516,7 @@ void VisualScript::unload(bool isReloading) // Note: preserve the registered scripting type but invalidate the locally cached handle if (_scriptingTypeHandle) { - VisualScriptingModule.Locker.Lock(); + VisualScriptingBinaryModule::Locker.Lock(); auto& type = VisualScriptingModule.Types[_scriptingTypeHandle.TypeIndex]; if (type.Script.DefaultInstance) { @@ -1523,7 +1527,7 @@ void VisualScript::unload(bool isReloading) VisualScriptingModule.Scripts[_scriptingTypeHandle.TypeIndex] = nullptr; _scriptingTypeHandleCached = _scriptingTypeHandle; _scriptingTypeHandle = ScriptingTypeHandle(); - VisualScriptingModule.Locker.Unlock(); + VisualScriptingBinaryModule::Locker.Unlock(); } } @@ -1534,8 +1538,8 @@ AssetChunksFlag VisualScript::getChunksToPreload() const void VisualScript::CacheScriptingType() { + ScopeLock lock(VisualScriptingBinaryModule::Locker); auto& binaryModule = VisualScriptingModule; - ScopeLock lock(binaryModule.Locker); // Find base type const StringAnsi baseTypename(Meta.BaseTypename); diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index ea627f2c7..bd27abc5a 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -154,7 +154,7 @@ void ContentService::LateUpdate() // Unload marked assets for (int32 i = 0; i < ToUnload.Count(); i++) { - Asset* asset = ToUnload[i]; + Asset* asset = ToUnload[i]; // Check if has no references if (asset->GetReferencesCount() <= 0) @@ -521,37 +521,33 @@ Asset* Content::GetAsset(const Guid& id) void Content::DeleteAsset(Asset* asset) { - ScopeLock locker(AssetsLocker); - - // Validate if (asset == nullptr || asset->_deleteFileOnUnload) - { - // Back return; - } LOG(Info, "Deleting asset {0}...", asset->ToString()); + // Ensure that asset is loaded (easier than cancel in-flight loading) + asset->WaitForLoaded(); + // Mark asset for delete queue (delete it after auto unload) asset->_deleteFileOnUnload = true; // Unload - UnloadAsset(asset); + asset->DeleteObject(); } void Content::DeleteAsset(const StringView& path) { - ScopeLock locker(AssetsLocker); - - // Check if is loaded + // Try to delete already loaded asset Asset* asset = GetAsset(path); if (asset != nullptr) { - // Delete asset DeleteAsset(asset); return; } + ScopeLock locker(AssetsLocker); + // Remove from registry AssetInfo info; if (Cache.DeleteAsset(path, &info)) @@ -573,7 +569,6 @@ void Content::deleteFileSafety(const StringView& path, const Guid& id) // Check if given id is invalid if (!id.IsValid()) { - // Cancel operation LOG(Warning, "Cannot remove file \'{0}\'. Given ID is invalid.", path); return; } @@ -585,7 +580,6 @@ void Content::deleteFileSafety(const StringView& path, const Guid& id) storage->CloseFileHandles(); // Close file handle to allow removing it if (!storage->HasAsset(id)) { - // Skip removing LOG(Warning, "Cannot remove file \'{0}\'. It doesn\'t contain asset {1}.", path, id); return; } @@ -703,13 +697,11 @@ bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPat LOG(Warning, "Cannot copy file to destination."); return true; } - if (JsonStorageProxy::ChangeId(dstPath, dstId)) { LOG(Warning, "Cannot change asset ID."); return true; } - return false; } @@ -774,12 +766,9 @@ bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPat FileSystem::DeleteFile(tmpPath); // Reload storage + if (auto storage = ContentStorageManager::GetStorage(dstPath)) { - auto storage = ContentStorageManager::GetStorage(dstPath); - if (storage) - { - storage->Reload(); - } + storage->Reload(); } } @@ -790,10 +779,8 @@ bool Content::CloneAssetFile(const StringView& dstPath, const StringView& srcPat void Content::UnloadAsset(Asset* asset) { - // Check input if (asset == nullptr) return; - asset->DeleteObject(); } @@ -919,12 +906,8 @@ bool Content::IsAssetTypeIdInvalid(const ScriptingTypeHandle& type, const Script Asset* Content::LoadAsync(const Guid& id, const ScriptingTypeHandle& type) { - // Early out if (!id.IsValid()) - { - // Back return nullptr; - } // Check if asset has been already loaded Asset* result = GetAsset(id); @@ -936,7 +919,6 @@ Asset* Content::LoadAsync(const Guid& id, const ScriptingTypeHandle& type) LOG(Warning, "Different loaded asset type! Asset: \'{0}\'. Expected type: {1}", result->ToString(), type.ToString()); return nullptr; } - return result; } @@ -954,12 +936,8 @@ Asset* Content::LoadAsync(const Guid& id, const ScriptingTypeHandle& type) LoadCallAssetsLocker.Lock(); const bool contains = LoadCallAssets.Contains(id); LoadCallAssetsLocker.Unlock(); - if (!contains) - { return GetAsset(id); - } - Platform::Sleep(1); } } @@ -967,7 +945,6 @@ Asset* Content::LoadAsync(const Guid& id, const ScriptingTypeHandle& type) { // Mark asset as loading LoadCallAssets.Add(id); - LoadCallAssetsLocker.Unlock(); } @@ -988,7 +965,7 @@ Asset* Content::load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo& // Get cached asset info (from registry) if (!GetAssetInfo(id, assetInfo)) { - LOG(Warning, "Invalid or missing asset ({0}, {1}).", id.ToString(Guid::FormatType::N), type.ToString()); + LOG(Warning, "Invalid or missing asset ({0}, {1}).", id, type.ToString()); return nullptr; } @@ -1032,11 +1009,13 @@ Asset* Content::load(const Guid& id, const ScriptingTypeHandle& type, AssetInfo& ASSERT(!Assets.ContainsKey(id)); #endif Assets.Add(id, result); - AssetsLocker.Unlock(); // Start asset loading + // TODO: refactor this to create asset loading task-chain before AssetsLocker.Lock() to allow better parallelization result->startLoading(); + AssetsLocker.Unlock(); + return result; } diff --git a/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h b/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h index 23cf5f787..4a7bbb2bb 100644 --- a/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h +++ b/Source/Engine/Content/Loading/Tasks/LoadAssetDataTask.h @@ -48,6 +48,8 @@ protected: // [ContentLoadTask] Result run() override { + if (IsCancelRequested()) + return Result::Ok; PROFILE_CPU(); AssetReference ref = _asset.Get(); @@ -67,8 +69,6 @@ protected: { if (IsCancelRequested()) return Result::Ok; - - // Load it #if TRACY_ENABLE ZoneScoped; ZoneName(*name, name.Length()); diff --git a/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h b/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h index 5ee384769..19d6fdd31 100644 --- a/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h +++ b/Source/Engine/Content/Loading/Tasks/LoadAssetTask.h @@ -31,10 +31,13 @@ public: if (Asset) { Asset->Locker.Lock(); - Asset->_loadFailed = true; - Asset->_isLoaded = false; - LOG(Error, "Loading asset \'{0}\' result: {1}.", ToString(), ToString(Result::TaskFailed)); - Asset->_loadingTask = nullptr; + if (Asset->_loadingTask == this) + { + Asset->_loadFailed = true; + Asset->_isLoaded = false; + LOG(Error, "Loading asset \'{0}\' result: {1}.", ToString(), ToString(Result::TaskFailed)); + Asset->_loadingTask = nullptr; + } Asset->Locker.Unlock(); } } @@ -73,7 +76,10 @@ protected: { if (Asset) { - Asset->_loadingTask = nullptr; + Asset->Locker.Lock(); + if (Asset->_loadingTask == this) + Asset->_loadingTask = nullptr; + Asset->Locker.Unlock(); Asset = nullptr; } @@ -84,7 +90,10 @@ protected: { if (Asset) { - Asset->_loadingTask = nullptr; + Asset->Locker.Lock(); + if (Asset->_loadingTask == this) + Asset->_loadingTask = nullptr; + Asset->Locker.Unlock(); Asset = nullptr; } diff --git a/Source/Engine/Content/SoftAssetReference.h b/Source/Engine/Content/SoftAssetReference.h index fe1cde8c2..d237b5fd7 100644 --- a/Source/Engine/Content/SoftAssetReference.h +++ b/Source/Engine/Content/SoftAssetReference.h @@ -30,9 +30,7 @@ public: /// /// Finalizes an instance of the class. /// - ~SoftAssetReferenceBase() - { - } + ~SoftAssetReferenceBase(); public: /// diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index d530e5456..a47e0bd0e 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -1302,15 +1302,15 @@ void FlaxStorage::CloseFileHandles() // In those situations all the async tasks using this storage should be cancelled externally // Ensure that no one is using this resource - int32 waitTime = 10; + int32 waitTime = 100; while (Platform::AtomicRead(&_chunksLock) != 0 && waitTime-- > 0) - Platform::Sleep(10); + Platform::Sleep(1); if (Platform::AtomicRead(&_chunksLock) != 0) { // File can be locked by some streaming tasks (eg. AudioClip::StreamingTask or StreamModelLODTask) + Entry e; for (int32 i = 0; i < GetEntriesCount(); i++) { - Entry e; GetEntry(i, e); Asset* asset = Content::GetAsset(e.ID); if (asset) @@ -1320,8 +1320,12 @@ void FlaxStorage::CloseFileHandles() } } } + waitTime = 100; + while (Platform::AtomicRead(&_chunksLock) != 0 && waitTime-- > 0) + Platform::Sleep(1); ASSERT(_chunksLock == 0); + // Close file handles (from all threads) _file.DeleteAll(); } diff --git a/Source/Engine/ContentImporters/ImportModelFile.cpp b/Source/Engine/ContentImporters/ImportModelFile.cpp index b9a4f5675..b34bbfaaf 100644 --- a/Source/Engine/ContentImporters/ImportModelFile.cpp +++ b/Source/Engine/ContentImporters/ImportModelFile.cpp @@ -124,7 +124,8 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) // Import model file ModelData modelData; String errorMsg; - String autoImportOutput = String(StringUtils::GetDirectoryName(context.TargetAssetPath)) / StringUtils::GetFileNameWithoutExtension(context.InputPath); + String autoImportOutput(StringUtils::GetDirectoryName(context.TargetAssetPath)); + autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath)); if (ModelTool::ImportModel(context.InputPath, modelData, options, errorMsg, autoImportOutput)) { LOG(Error, "Cannot import model file. {0}", errorMsg); diff --git a/Source/Engine/Core/Collections/BitArray.h b/Source/Engine/Core/Collections/BitArray.h index 01238d434..eeadc82e9 100644 --- a/Source/Engine/Core/Collections/BitArray.h +++ b/Source/Engine/Core/Collections/BitArray.h @@ -22,6 +22,16 @@ private: int32 _capacity; AllocationData _allocation; + FORCE_INLINE static int32 ToItemCount(int32 size) + { + return Math::DivideAndRoundUp(size, sizeof(ItemType)); + } + + FORCE_INLINE static int32 ToItemCapacity(int32 size) + { + return Math::Max(Math::DivideAndRoundUp(size, sizeof(ItemType)), 1); + } + public: /// /// Initializes a new instance of the class. @@ -41,7 +51,7 @@ public: , _capacity(capacity) { if (capacity > 0) - _allocation.Allocate(Math::Max(capacity / sizeof(ItemType), 1)); + _allocation.Allocate(ToItemCapacity(capacity)); } /// @@ -53,7 +63,7 @@ public: _count = _capacity = other.Count(); if (_capacity > 0) { - const uint64 itemsCapacity = Math::Max(_capacity / sizeof(ItemType), 1); + const int32 itemsCapacity = ToItemCapacity(_capacity); _allocation.Allocate(itemsCapacity); Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType)); } @@ -69,7 +79,7 @@ public: _count = _capacity = other.Count(); if (_capacity > 0) { - const uint64 itemsCapacity = Math::Max(_capacity / sizeof(ItemType), 1); + const int32 itemsCapacity = ToItemCapacity(_capacity); _allocation.Allocate(itemsCapacity); Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType)); } @@ -101,7 +111,7 @@ public: { _allocation.Free(); _capacity = other._count; - const uint64 itemsCapacity = Math::Max(_capacity / sizeof(ItemType), 1); + const int32 itemsCapacity = ToItemCapacity(_capacity); _allocation.Allocate(itemsCapacity); Platform::MemoryCopy(Get(), other.Get(), itemsCapacity * sizeof(ItemType)); } @@ -246,7 +256,7 @@ public: return; ASSERT(capacity >= 0); const int32 count = preserveContents ? (_count < capacity ? _count : capacity) : 0; - _allocation.Relocate(Math::Max(capacity / sizeof(ItemType), 1), _count / sizeof(ItemType), count / sizeof(ItemType)); + _allocation.Relocate(ToItemCapacity(capacity), ToItemCount(_count), ToItemCount(count)); _capacity = capacity; _count = count; } @@ -272,7 +282,7 @@ public: { if (_capacity < minCapacity) { - const int32 capacity = _allocation.CalculateCapacityGrow(Math::Max(_capacity / sizeof(ItemType), 1), minCapacity); + const int32 capacity = _allocation.CalculateCapacityGrow(ToItemCapacity(_capacity), minCapacity); SetCapacity(capacity, preserveContents); } } @@ -284,7 +294,7 @@ public: void SetAll(const bool value) { if (_count != 0) - Platform::MemorySet(_allocation.Get(), Math::Max(_count / sizeof(ItemType), 1), value ? MAX_int32 : 0); + Platform::MemorySet(_allocation.Get(), ToItemCount(_count) * sizeof(ItemType), value ? MAX_uint32 : 0); } /// diff --git a/Source/Engine/Core/Collections/ChunkedArray.h b/Source/Engine/Core/Collections/ChunkedArray.h index 38bf92fb8..b3765a9fe 100644 --- a/Source/Engine/Core/Collections/ChunkedArray.h +++ b/Source/Engine/Core/Collections/ChunkedArray.h @@ -100,7 +100,7 @@ public: int32 _chunkIndex; int32 _index; - Iterator(ChunkedArray const* collection, const int32 index) + Iterator(const ChunkedArray* collection, const int32 index) : _collection(const_cast(collection)) , _chunkIndex(index / ChunkSize) , _index(index % ChunkSize) @@ -122,29 +122,29 @@ public: { } - public: - FORCE_INLINE ChunkedArray* GetChunkedArray() const + Iterator(Iterator&& i) + : _collection(i._collection) + , _chunkIndex(i._chunkIndex) + , _index(i._index) { - return _collection; } + public: FORCE_INLINE int32 Index() const { return _chunkIndex * ChunkSize + _index; } - public: - bool IsEnd() const + FORCE_INLINE bool IsEnd() const { - return Index() == _collection->Count(); + return (_chunkIndex * ChunkSize + _index) == _collection->_count; } - bool IsNotEnd() const + FORCE_INLINE bool IsNotEnd() const { - return Index() != _collection->Count(); + return (_chunkIndex * ChunkSize + _index) != _collection->_count; } - public: FORCE_INLINE T& operator*() const { return _collection->_chunks[_chunkIndex]->At(_index); @@ -155,7 +155,6 @@ public: return &_collection->_chunks[_chunkIndex]->At(_index); } - public: FORCE_INLINE bool operator==(const Iterator& v) const { return _collection == v._collection && _chunkIndex == v._chunkIndex && _index == v._index; @@ -166,17 +165,22 @@ public: return _collection != v._collection || _chunkIndex != v._chunkIndex || _index != v._index; } - public: + Iterator& operator=(const Iterator& v) + { + _collection = v._collection; + _chunkIndex = v._chunkIndex; + _index = v._index; + return *this; + } + Iterator& operator++() { // Check if it is not at end - const int32 end = _collection->Count(); - if (Index() != end) + if ((_chunkIndex * ChunkSize + _index) != _collection->_count) { // Move forward within chunk _index++; - // Check if need to change chunk if (_index == ChunkSize && _chunkIndex < _collection->_chunks.Count() - 1) { // Move to next chunk @@ -189,9 +193,9 @@ public: Iterator operator++(int) { - Iterator temp = *this; - ++temp; - return temp; + Iterator i = *this; + ++i; + return i; } Iterator& operator--() @@ -199,7 +203,6 @@ public: // Check if it's not at beginning if (_index != 0 || _chunkIndex != 0) { - // Check if need to change chunk if (_index == 0) { // Move to previous chunk @@ -217,9 +220,9 @@ public: Iterator operator--(int) { - Iterator temp = *this; - --temp; - return temp; + Iterator i = *this; + --i; + return i; } }; @@ -294,7 +297,7 @@ public: { if (IsEmpty()) return; - ASSERT(i.GetChunkedArray() == this); + ASSERT(i._collection == this); ASSERT(i._chunkIndex < _chunks.Count() && i._index < ChunkSize); ASSERT(i.Index() < Count()); @@ -432,11 +435,31 @@ public: Iterator End() const { - return Iterator(this, Count()); + return Iterator(this, _count); } Iterator IteratorAt(int32 index) const { return Iterator(this, index); } + + FORCE_INLINE Iterator begin() + { + return Iterator(this, 0); + } + + FORCE_INLINE Iterator end() + { + return Iterator(this, _count); + } + + FORCE_INLINE const Iterator begin() const + { + return Iterator(this, 0); + } + + FORCE_INLINE const Iterator end() const + { + return Iterator(this, _count); + } }; diff --git a/Source/Engine/Core/Collections/Config.h b/Source/Engine/Core/Collections/Config.h index 792ae57c8..ce7656dcd 100644 --- a/Source/Engine/Core/Collections/Config.h +++ b/Source/Engine/Core/Collections/Config.h @@ -2,13 +2,26 @@ #pragma once -/// -/// Default capacity for the dictionaries (amount of space for the elements) -/// -#define DICTIONARY_DEFAULT_CAPACITY 256 +#include "Engine/Platform/Defines.h" /// -/// Function for dictionary that tells how change hash index during iteration (size param is a buckets table size) +/// Default capacity for the dictionaries (amount of space for the elements). +/// +#ifndef DICTIONARY_DEFAULT_CAPACITY +#if PLATFORM_DESKTOP +#define DICTIONARY_DEFAULT_CAPACITY 256 +#else +#define DICTIONARY_DEFAULT_CAPACITY 64 +#endif +#endif + +/// +/// Default slack space divider for the dictionaries. +/// +#define DICTIONARY_DEFAULT_SLACK_SCALE 3 + +/// +/// Function for dictionary that tells how change hash index during iteration (size param is a buckets table size). /// #define DICTIONARY_PROB_FUNC(size, numChecks) (numChecks) //#define DICTIONARY_PROB_FUNC(size, numChecks) (1) diff --git a/Source/Engine/Core/Collections/Dictionary.h b/Source/Engine/Core/Collections/Dictionary.h index 575863dc9..4d65a4123 100644 --- a/Source/Engine/Core/Collections/Dictionary.h +++ b/Source/Engine/Core/Collections/Dictionary.h @@ -40,7 +40,7 @@ public: private: State _state; - void Free() + FORCE_INLINE void Free() { if (_state == Occupied) { @@ -50,7 +50,7 @@ public: _state = Empty; } - void Delete() + FORCE_INLINE void Delete() { _state = Deleted; Memory::DestructItem(&Key); @@ -58,7 +58,7 @@ public: } template - void Occupy(const KeyComparableType& key) + FORCE_INLINE void Occupy(const KeyComparableType& key) { Memory::ConstructItems(&Key, &key, 1); Memory::ConstructItem(&Value); @@ -66,7 +66,7 @@ public: } template - void Occupy(const KeyComparableType& key, const ValueType& value) + FORCE_INLINE void Occupy(const KeyComparableType& key, const ValueType& value) { Memory::ConstructItems(&Key, &key, 1); Memory::ConstructItems(&Value, &value, 1); @@ -74,7 +74,7 @@ public: } template - void Occupy(const KeyComparableType& key, ValueType&& value) + FORCE_INLINE void Occupy(const KeyComparableType& key, ValueType&& value) { Memory::ConstructItems(&Key, &key, 1); Memory::MoveItems(&Value, &value, 1); @@ -132,9 +132,6 @@ public: /// /// The other collection to move. Dictionary(Dictionary&& other) noexcept - : _elementsCount(other._elementsCount) - , _deletedCount(other._deletedCount) - , _size(other._size) { _elementsCount = other._elementsCount; _deletedCount = other._deletedCount; @@ -375,8 +372,12 @@ public: template ValueType& At(const KeyComparableType& key) { + // Check if need to rehash elements (prevent many deleted elements that use too much of capacity) + if (_deletedCount > _size / DICTIONARY_DEFAULT_SLACK_SCALE) + Compact(); + // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + _deletedCount + 1); + EnsureCapacity((_elementsCount + 1) * DICTIONARY_DEFAULT_SLACK_SCALE + _deletedCount); // Find location of the item or place to insert it FindPositionResult pos; @@ -388,9 +389,9 @@ public: // Insert ASSERT(pos.FreeSlotIndex != -1); + _elementsCount++; Bucket& bucket = _allocation.Get()[pos.FreeSlotIndex]; bucket.Occupy(key); - _elementsCount++; return bucket.Value; } @@ -493,7 +494,7 @@ public: for (Iterator i = Begin(); i.IsNotEnd(); ++i) { if (i->Value) - Delete(i->Value); + ::Delete(i->Value); } Clear(); } @@ -533,13 +534,22 @@ public: } _size = capacity; Bucket* oldData = oldAllocation.Get(); - if (oldElementsCount != 0 && preserveContents) + if (oldElementsCount != 0 && capacity != 0 && preserveContents) { - // TODO; move keys and values on realloc + FindPositionResult pos; for (int32 i = 0; i < oldSize; i++) { - if (oldData[i].IsOccupied()) - Add(oldData[i].Key, MoveTemp(oldData[i].Value)); + Bucket& oldBucket = oldData[i]; + if (oldBucket.IsOccupied()) + { + FindPosition(oldBucket.Key, pos); + ASSERT(pos.FreeSlotIndex != -1); + Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Memory::MoveItems(&bucket->Key, &oldBucket.Key, 1); + Memory::MoveItems(&bucket->Value, &oldBucket.Value, 1); + bucket->_state = Bucket::Occupied; + _elementsCount++; + } } } if (oldElementsCount != 0) @@ -558,9 +568,9 @@ public: { if (_size >= minCapacity) return; - if (minCapacity < DICTIONARY_DEFAULT_CAPACITY) - minCapacity = DICTIONARY_DEFAULT_CAPACITY; - const int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); + int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); + if (capacity < DICTIONARY_DEFAULT_CAPACITY) + capacity = DICTIONARY_DEFAULT_CAPACITY; SetCapacity(capacity, preserveContents); } @@ -584,24 +594,10 @@ public: /// The value. /// Weak reference to the stored bucket. template - Bucket* Add(const KeyComparableType& key, const ValueType& value) + FORCE_INLINE Bucket* Add(const KeyComparableType& key, const ValueType& value) { - // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + _deletedCount + 1); - - // Find location of the item or place to insert it - FindPositionResult pos; - FindPosition(key, pos); - - // Ensure key is unknown - ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); - - // Insert - ASSERT(pos.FreeSlotIndex != -1); - Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Bucket* bucket = OnAdd(key); bucket->Occupy(key, value); - _elementsCount++; - return bucket; } @@ -612,24 +608,10 @@ public: /// The value. /// Weak reference to the stored bucket. template - Bucket* Add(const KeyComparableType& key, ValueType&& value) + FORCE_INLINE Bucket* Add(const KeyComparableType& key, ValueType&& value) { - // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + _deletedCount + 1); - - // Find location of the item or place to insert it - FindPositionResult pos; - FindPosition(key, pos); - - // Ensure key is unknown - ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); - - // Insert - ASSERT(pos.FreeSlotIndex != -1); - Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Bucket* bucket = OnAdd(key); bucket->Occupy(key, MoveTemp(value)); - _elementsCount++; - return bucket; } @@ -851,7 +833,7 @@ public: return Iterator(this, _size); } -protected: +private: /// /// The result container of the dictionary item lookup searching. /// @@ -911,4 +893,66 @@ protected: result.ObjectIndex = -1; result.FreeSlotIndex = insertPos; } + + template + Bucket* OnAdd(const KeyComparableType& key) + { + // Check if need to rehash elements (prevent many deleted elements that use too much of capacity) + if (_deletedCount > _size / DICTIONARY_DEFAULT_SLACK_SCALE) + Compact(); + + // Ensure to have enough memory for the next item (in case of new element insertion) + EnsureCapacity((_elementsCount + 1) * DICTIONARY_DEFAULT_SLACK_SCALE + _deletedCount); + + // Find location of the item or place to insert it + FindPositionResult pos; + FindPosition(key, pos); + + // Ensure key is unknown + ASSERT(pos.ObjectIndex == -1 && "That key has been already added to the dictionary."); + + // Insert + ASSERT(pos.FreeSlotIndex != -1); + _elementsCount++; + return &_allocation.Get()[pos.FreeSlotIndex]; + } + + void Compact() + { + if (_elementsCount == 0) + { + // Fast path if it's empty + Bucket* data = _allocation.Get(); + for (int32 i = 0; i < _size; i++) + data[i]._state = Bucket::Empty; + } + else + { + // Rebuild entire table completely + AllocationData oldAllocation; + oldAllocation.Swap(_allocation); + _allocation.Allocate(_size); + Bucket* data = _allocation.Get(); + for (int32 i = 0; i < _size; i++) + data[i]._state = Bucket::Empty; + Bucket* oldData = oldAllocation.Get(); + FindPositionResult pos; + for (int32 i = 0; i < _size; i++) + { + Bucket& oldBucket = oldData[i]; + if (oldBucket.IsOccupied()) + { + FindPosition(oldBucket.Key, pos); + ASSERT(pos.FreeSlotIndex != -1); + Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Memory::MoveItems(&bucket->Key, &oldBucket.Key, 1); + Memory::MoveItems(&bucket->Value, &oldBucket.Value, 1); + bucket->_state = Bucket::Occupied; + } + } + for (int32 i = 0; i < _size; i++) + oldData[i].Free(); + } + _deletedCount = 0; + } }; diff --git a/Source/Engine/Core/Collections/HashSet.h b/Source/Engine/Core/Collections/HashSet.h index 107e42e65..4a4f3e924 100644 --- a/Source/Engine/Core/Collections/HashSet.h +++ b/Source/Engine/Core/Collections/HashSet.h @@ -37,26 +37,33 @@ public: private: State _state; - void Free() + FORCE_INLINE void Free() { if (_state == Occupied) Memory::DestructItem(&Item); _state = Empty; } - void Delete() + FORCE_INLINE void Delete() { _state = Deleted; Memory::DestructItem(&Item); } template - void Occupy(const ItemType& item) + FORCE_INLINE void Occupy(const ItemType& item) { Memory::ConstructItems(&Item, &item, 1); _state = Occupied; } + template + FORCE_INLINE void Occupy(ItemType& item) + { + Memory::MoveItems(&Item, &item, 1); + _state = Occupied; + } + FORCE_INLINE bool IsEmpty() const { return _state == Empty; @@ -108,9 +115,6 @@ public: /// /// The other collection to move. HashSet(HashSet&& other) noexcept - : _elementsCount(other._elementsCount) - , _deletedCount(other._deletedCount) - , _size(other._size) { _elementsCount = other._elementsCount; _deletedCount = other._deletedCount; @@ -169,7 +173,7 @@ public: /// ~HashSet() { - SetCapacity(0, false); + Clear(); } public: @@ -216,6 +220,7 @@ public: HashSet* _collection; int32 _index; + public: Iterator(HashSet* collection, const int32 index) : _collection(collection) , _index(index) @@ -228,7 +233,12 @@ public: { } - public: + Iterator() + : _collection(nullptr) + , _index(-1) + { + } + Iterator(const Iterator& i) : _collection(i._collection) , _index(i._index) @@ -242,6 +252,11 @@ public: } public: + FORCE_INLINE int32 Index() const + { + return _index; + } + FORCE_INLINE bool IsEnd() const { return _index == _collection->_size; @@ -398,13 +413,21 @@ public: } _size = capacity; Bucket* oldData = oldAllocation.Get(); - if (oldElementsCount != 0 && preserveContents) + if (oldElementsCount != 0 && capacity != 0 && preserveContents) { - // TODO; move keys and values on realloc + FindPositionResult pos; for (int32 i = 0; i < oldSize; i++) { - if (oldData[i].IsOccupied()) - Add(oldData[i].Item); + Bucket& oldBucket = oldData[i]; + if (oldBucket.IsOccupied()) + { + FindPosition(oldBucket.Item, pos); + ASSERT(pos.FreeSlotIndex != -1); + Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Memory::MoveItems(&bucket->Item, &oldBucket.Item, 1); + bucket->_state = Bucket::Occupied; + _elementsCount++; + } } } if (oldElementsCount != 0) @@ -421,14 +444,26 @@ public: /// True if preserve collection data when changing its size, otherwise collection after resize will be empty. void EnsureCapacity(int32 minCapacity, bool preserveContents = true) { - if (Capacity() >= minCapacity) + if (_size >= minCapacity) return; - if (minCapacity < DICTIONARY_DEFAULT_CAPACITY) - minCapacity = DICTIONARY_DEFAULT_CAPACITY; - const int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); + int32 capacity = _allocation.CalculateCapacityGrow(_size, minCapacity); + if (capacity < DICTIONARY_DEFAULT_CAPACITY) + capacity = DICTIONARY_DEFAULT_CAPACITY; SetCapacity(capacity, preserveContents); } + /// + /// Swaps the contents of collection with the other object without copy operation. Performs fast internal data exchange. + /// + /// The other collection. + void Swap(HashSet& other) + { + ::Swap(_elementsCount, other._elementsCount); + ::Swap(_deletedCount, other._deletedCount); + ::Swap(_size, other._size); + _allocation.Swap(other._allocation); + } + public: /// /// Add element to the collection. @@ -438,24 +473,23 @@ public: template bool Add(const ItemType& item) { - // Ensure to have enough memory for the next item (in case of new element insertion) - EnsureCapacity(_elementsCount + _deletedCount + 1); + Bucket* bucket = OnAdd(item); + if (bucket) + bucket->Occupy(item); + return bucket != nullptr; + } - // Find location of the item or place to insert it - FindPositionResult pos; - FindPosition(item, pos); - - // Check if object has been already added - if (pos.ObjectIndex != -1) - return false; - - // Insert - ASSERT(pos.FreeSlotIndex != -1); - Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; - bucket->Occupy(item); - _elementsCount++; - - return true; + /// + /// Add element to the collection. + /// + /// The element to add to the set. + /// True if element has been added to the collection, otherwise false if the element is already present. + bool Add(T&& item) + { + Bucket* bucket = OnAdd(item); + if (bucket) + bucket->Occupy(MoveTemp(item)); + return bucket != nullptr; } /// @@ -593,7 +627,7 @@ public: return Iterator(this, _size); } -protected: +private: /// /// The result container of the set item lookup searching. /// @@ -654,4 +688,66 @@ protected: result.ObjectIndex = -1; result.FreeSlotIndex = insertPos; } + + template + Bucket* OnAdd(const ItemType& key) + { + // Check if need to rehash elements (prevent many deleted elements that use too much of capacity) + if (_deletedCount > _size / DICTIONARY_DEFAULT_SLACK_SCALE) + Compact(); + + // Ensure to have enough memory for the next item (in case of new element insertion) + EnsureCapacity((_elementsCount + 1) * DICTIONARY_DEFAULT_SLACK_SCALE + _deletedCount); + + // Find location of the item or place to insert it + FindPositionResult pos; + FindPosition(key, pos); + + // Check if object has been already added + if (pos.ObjectIndex != -1) + return nullptr; + + // Insert + ASSERT(pos.FreeSlotIndex != -1); + _elementsCount++; + return &_allocation.Get()[pos.FreeSlotIndex]; + } + + void Compact() + { + if (_elementsCount == 0) + { + // Fast path if it's empty + Bucket* data = _allocation.Get(); + for (int32 i = 0; i < _size; i++) + data[i]._state = Bucket::Empty; + } + else + { + // Rebuild entire table completely + AllocationData oldAllocation; + oldAllocation.Swap(_allocation); + _allocation.Allocate(_size); + Bucket* data = _allocation.Get(); + for (int32 i = 0; i < _size; i++) + data[i]._state = Bucket::Empty; + Bucket* oldData = oldAllocation.Get(); + FindPositionResult pos; + for (int32 i = 0; i < _size; i++) + { + Bucket& oldBucket = oldData[i]; + if (oldBucket.IsOccupied()) + { + FindPosition(oldBucket.Item, pos); + ASSERT(pos.FreeSlotIndex != -1); + Bucket* bucket = &_allocation.Get()[pos.FreeSlotIndex]; + Memory::MoveItems(&bucket->Item, &oldBucket.Item, 1); + bucket->_state = Bucket::Occupied; + } + } + for (int32 i = 0; i < _size; i++) + oldData[i].Free(); + } + _deletedCount = 0; + } }; diff --git a/Source/Engine/Core/Config/BuildSettings.h b/Source/Engine/Core/Config/BuildSettings.h index 318c304e0..3232c6b02 100644 --- a/Source/Engine/Core/Config/BuildSettings.h +++ b/Source/Engine/Core/Config/BuildSettings.h @@ -4,6 +4,7 @@ #include "Engine/Core/Config/Settings.h" #include "Engine/Serialization/Serialization.h" +#include "Engine/Core/Collections/Array.h" #include "Engine/Content/Asset.h" #include "Engine/Content/AssetReference.h" #include "Engine/Content/SceneReference.h" @@ -76,6 +77,12 @@ public: API_FIELD(Attributes="EditorOrder(2010), EditorDisplay(\"Content\")") bool ShadersGenerateDebugData = false; + /// + /// If checked, skips bundling default engine fonts for UI. Use if to reduce build size if you don't use default engine fonts but custom ones only. + /// + API_FIELD(Attributes="EditorOrder(2100), EditorDisplay(\"Content\")") + bool SkipDefaultFonts = false; + /// /// If checked, .NET Runtime won't be packaged with a game and will be required by user to be installed on system upon running game build. Available only on supported platforms such as Windows, Linux and macOS. /// @@ -106,6 +113,7 @@ public: DESERIALIZE(AdditionalAssetFolders); DESERIALIZE(ShadersNoOptimize); DESERIALIZE(ShadersGenerateDebugData); + DESERIALIZE(SkipDefaultFonts); DESERIALIZE(SkipDotnetPackaging); DESERIALIZE(SkipUnusedDotnetLibsPackaging); } diff --git a/Source/Engine/Core/Delegate.h b/Source/Engine/Core/Delegate.h index 4e081faef..f0f8fdef0 100644 --- a/Source/Engine/Core/Delegate.h +++ b/Source/Engine/Core/Delegate.h @@ -226,7 +226,7 @@ public: /// Function result FORCE_INLINE ReturnType operator()(Params... params) const { - ASSERT(_function); + ASSERT_LOW_LAYER(_function); return _function(_callee, Forward(params)...); } @@ -289,8 +289,13 @@ protected: intptr volatile _ptr = 0; intptr volatile _size = 0; #else - HashSet* _functions = nullptr; - CriticalSection* _locker = nullptr; + struct Data + { + HashSet Functions; + CriticalSection Locker; + }; + // Holds pointer to Data with Functions and Locker. Thread-safe access via atomic operations. + intptr volatile _data = 0; #endif typedef void (*StubSignature)(void*, Params...); @@ -314,15 +319,12 @@ public: _ptr = (intptr)newBindings; _size = newSize; #else - if (other._functions == nullptr) + Data* otherData = (Data*)Platform::AtomicRead(&_data); + if (otherData == nullptr) return; - _functions = New>(*other._functions); - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) - { - if (i->Item._function && i->Item._lambda) - i->Item.LambdaCtor(); - } - _locker = other._locker; + ScopeLock lock(otherData->Locker); + for (auto i = otherData->Functions.Begin(); i.IsNotEnd(); ++i) + Bind(i->Item); #endif } @@ -334,10 +336,8 @@ public: other._ptr = 0; other._size = 0; #else - _functions = other._functions; - _locker = other._locker; - other._functions = nullptr; - other._locker = nullptr; + _data = other._data; + other._data = 0; #endif } @@ -356,20 +356,11 @@ public: Allocator::Free((void*)_ptr); } #else - if (_locker != nullptr) + Data* data = (Data*)_data; + if (data) { - Allocator::Free(_locker); - _locker = nullptr; - } - if (_functions != nullptr) - { - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) - { - if (i->Item._lambda) - i->Item.LambdaCtor(); - } - Allocator::Free(_functions); - _functions = nullptr; + _data = 0; + Delete(data); } #endif } @@ -385,8 +376,13 @@ public: for (intptr i = 0; i < size; i++) Bind(bindings[i]); #else - for (auto i = other._functions->Begin(); i.IsNotEnd(); ++i) - Bind(i->Item); + Data* otherData = (Data*)Platform::AtomicRead(&_data); + if (otherData != nullptr) + { + ScopeLock lock(otherData->Locker); + for (auto i = otherData->Functions.Begin(); i.IsNotEnd(); ++i) + Bind(i->Item); + } #endif } return *this; @@ -402,10 +398,8 @@ public: other._ptr = 0; other._size = 0; #else - _functions = other._functions; - _locker = other._locker; - other._functions = nullptr; - other._locker = nullptr; + _data = other._data; + other._data = 0; #endif } return *this; @@ -507,12 +501,20 @@ public: Allocator::Free(bindings); } #else - if (_locker == nullptr) - _locker = New(); - ScopeLock lock(*_locker); - if (_functions == nullptr) - _functions = New>(32); - _functions->Add(f); + Data* data = (Data*)Platform::AtomicRead(&_data); + while (!data) + { + Data* newData = New(); + Data* oldData = (Data*)Platform::InterlockedCompareExchange(&_data, (intptr)newData, (intptr)data); + if (oldData != data) + { + // Other thread already set the new data so free it and try again + Delete(newData); + } + data = (Data*)Platform::AtomicRead(&_data); + } + ScopeLock lock(data->Locker); + data->Functions.Add(f); #endif } @@ -568,13 +570,22 @@ public: } } #else - if (_locker == nullptr) - _locker = New(); - ScopeLock lock(*_locker); - if (_functions && _functions->Contains(f)) - return; + Data* data = (Data*)Platform::AtomicRead(&_data); + if (data) + { + data->Locker.Lock(); + if (data->Functions.Contains(f)) + { + data->Locker.Unlock(); + return; + } + } #endif Bind(f); +#if !DELEGATE_USE_ATOMIC + if (data) + data->Locker.Unlock(); +#endif } /// @@ -583,18 +594,9 @@ public: template void Unbind() { -#if DELEGATE_USE_ATOMIC FunctionType f; f.template Bind(); Unbind(f); -#else - if (_functions == nullptr) - return; - FunctionType f; - f.template Bind(); - ScopeLock lock(*_locker); - _functions->Remove(f); -#endif } /// @@ -604,18 +606,9 @@ public: template void Unbind(T* callee) { -#if DELEGATE_USE_ATOMIC FunctionType f; f.template Bind(callee); Unbind(f); -#else - if (_functions == nullptr) - return; - FunctionType f; - f.template Bind(callee); - ScopeLock lock(*_locker); - _functions->Remove(f); -#endif } /// @@ -624,16 +617,8 @@ public: /// The method. void Unbind(Signature method) { -#if DELEGATE_USE_ATOMIC FunctionType f(method); Unbind(f); -#else - if (_functions == nullptr) - return; - FunctionType f(method); - ScopeLock lock(*_locker); - _functions->Remove(f); -#endif } /// @@ -666,10 +651,11 @@ public: Unbind(f); } #else - if (_functions == nullptr) + Data* data = (Data*)Platform::AtomicRead(&_data); + if (!data) return; - ScopeLock lock(*_locker); - _functions->Remove(f); + ScopeLock lock(data->Locker); + data->Functions.Remove(f); #endif } @@ -692,15 +678,11 @@ public: Platform::AtomicStore((intptr volatile*)&bindings[i]._callee, 0); } #else - if (_functions == nullptr) + Data* data = (Data*)Platform::AtomicRead(&_data); + if (!data) return; - ScopeLock lock(*_locker); - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) - { - if (i->Item._lambda) - i->Item.LambdaDtor(); - } - _functions->Clear(); + ScopeLock lock(data->Locker); + data->Functions.Clear(); #endif } @@ -710,22 +692,24 @@ public: /// The bound functions count. int32 Count() const { + int32 result = 0; #if DELEGATE_USE_ATOMIC - int32 count = 0; const intptr size = Platform::AtomicRead((intptr volatile*)&_size); FunctionType* bindings = (FunctionType*)Platform::AtomicRead((intptr volatile*)&_ptr); for (intptr i = 0; i < size; i++) { if (Platform::AtomicRead((intptr volatile*)&bindings[i]._function) != 0) - count++; + result++; } - return count; #else - if (_functions == nullptr) - return 0; - ScopeLock lock(*_locker); - return _functions->Count(); + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (data) + { + ScopeLock lock(data->Locker); + result = data->Functions.Count(); + } #endif + return result; } /// @@ -736,10 +720,14 @@ public: #if DELEGATE_USE_ATOMIC return (int32)Platform::AtomicRead((intptr volatile*)&_size); #else - if (_functions == nullptr) - return 0; - ScopeLock lock(*_locker); - return _functions->Capacity(); + int32 result = 0; + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (data) + { + ScopeLock lock(data->Locker); + result = data->Functions.Capacity(); + } + return result; #endif } @@ -759,10 +747,14 @@ public: } return false; #else - if (_functions == nullptr) - return false; - ScopeLock lock(*_locker); - return _functions->Count() > 0; + bool result = false; + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (data) + { + ScopeLock lock(data->Locker); + result = data->Functions.Count() != 0; + } + return result; #endif } @@ -791,18 +783,13 @@ public: } } #else - if (_functions == nullptr) - return 0; - ScopeLock lock(*_locker); - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (data) { - if (i->Item._function != nullptr) + ScopeLock lock(data->Locker); + for (auto i = data->Functions.Begin(); i.IsNotEnd(); ++i) { - buffer[count]._function = (StubSignature)i->Item._function; - buffer[count]._callee = (void*)i->Item._callee; - buffer[count]._lambda = (typename FunctionType::Lambda*)i->Item._lambda; - if (buffer[count]._lambda) - buffer[count].LambdaCtor(); + new(buffer + count) FunctionType((const FunctionType&)i->Item); count++; } } @@ -828,15 +815,15 @@ public: ++bindings; } #else - if (_functions == nullptr) + Data* data = (Data*)Platform::AtomicRead((intptr volatile*)&_data); + if (!data) return; - ScopeLock lock(*_locker); - for (auto i = _functions->Begin(); i.IsNotEnd(); ++i) + ScopeLock lock(data->Locker); + for (auto i = data->Functions.Begin(); i.IsNotEnd(); ++i) { - auto function = (StubSignature)(i->Item._function); - auto callee = (void*)(i->Item._callee); - if (function != nullptr) - function(callee, Forward(params)...); + const FunctionType& item = i->Item; + ASSERT_LOW_LAYER(item._function); + item._function(item._callee, Forward(params)...); } #endif } diff --git a/Source/Engine/Core/Log.cpp b/Source/Engine/Core/Log.cpp index e3d97e149..bb8d42aba 100644 --- a/Source/Engine/Core/Log.cpp +++ b/Source/Engine/Core/Log.cpp @@ -3,16 +3,15 @@ #include "Log.h" #include "Engine/Engine/CommandLine.h" #include "Engine/Core/Types/DateTime.h" -#include "Engine/Core/Collections/Sorting.h" +#include "Engine/Core/Collections/Array.h" #include "Engine/Engine/Time.h" #include "Engine/Engine/Globals.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Platform/CriticalSection.h" #include "Engine/Serialization/FileWriteStream.h" -#include "Engine/Serialization/MemoryWriteStream.h" #include "Engine/Debug/Exceptions/Exceptions.h" #if USE_EDITOR -#include "Engine/Core/Collections/Array.h" +#include "Engine/Core/Collections/Sorting.h" #endif #include @@ -199,35 +198,36 @@ void Log::Logger::WriteFloor() void Log::Logger::ProcessLogMessage(LogType type, const StringView& msg, fmt_flax::memory_buffer& w) { const TimeSpan time = DateTime::Now() - LogStartTime; + const int32 msgLength = msg.Length(); fmt_flax::format(w, TEXT("[ {0} ]: [{1}] "), *time.ToString('a'), ToString(type)); // On Windows convert all '\n' into '\r\n' #if PLATFORM_WINDOWS - const int32 msgLength = msg.Length(); - if (msgLength > 1) + bool hasWindowsNewLine = false; + for (int32 i = 1; i < msgLength && !hasWindowsNewLine; i++) + hasWindowsNewLine |= msg.Get()[i - 1] != '\r' && msg.Get()[i] == '\n'; + if (hasWindowsNewLine) { - MemoryWriteStream msgStream(msgLength * sizeof(Char)); - msgStream.WriteChar(msg[0]); + Array msgStream; + msgStream.EnsureCapacity(msgLength); + msgStream.Add(msg.Get()[0]); for (int32 i = 1; i < msgLength; i++) { - if (msg[i - 1] != '\r' && msg[i] == '\n') - msgStream.WriteChar(TEXT('\r')); - msgStream.WriteChar(msg[i]); + if (msg.Get()[i - 1] != '\r' && msg.Get()[i] == '\n') + msgStream.Add(TEXT('\r')); + msgStream.Add(msg.Get()[i]); } - msgStream.WriteChar(msg[msgLength]); - msgStream.WriteChar(TEXT('\0')); - fmt_flax::format(w, TEXT("{}"), (const Char*)msgStream.GetHandle()); - //w.append(msgStream.GetHandle(), msgStream.GetHandle() + msgStream.GetPosition()); + msgStream.Add(TEXT('\0')); + w.append(msgStream.Get(), (const Char*)(msgStream.Get() + msgStream.Count())); + //fmt_flax::format(w, TEXT("{}"), (const Char*)msgStream.Get()); + return; } - else - { - //w.append(msg.Get(), msg.Get() + msg.Length()); - fmt_flax::format(w, TEXT("{}"), msg); - } -#else - fmt_flax::format(w, TEXT("{}"), msg); #endif + + // Output raw message to the log + w.append(msg.Get(), msg.Get() + msg.Length()); + //fmt_flax::format(w, TEXT("{}"), msg); } void Log::Logger::Write(LogType type, const StringView& msg) diff --git a/Source/Engine/Core/Math/Quaternion.cpp b/Source/Engine/Core/Math/Quaternion.cpp index bdbe3037d..1ca05a9c3 100644 --- a/Source/Engine/Core/Math/Quaternion.cpp +++ b/Source/Engine/Core/Math/Quaternion.cpp @@ -382,9 +382,8 @@ void Quaternion::GetRotationFromTo(const Float3& from, const Float3& to, Quatern v0.Normalize(); v1.Normalize(); - const float d = Float3::Dot(v0, v1); - // If dot == 1, vectors are the same + const float d = Float3::Dot(v0, v1); if (d >= 1.0f) { result = Identity; diff --git a/Source/Engine/Core/Math/Quaternion.cs b/Source/Engine/Core/Math/Quaternion.cs index ff50a98bf..34ee160f0 100644 --- a/Source/Engine/Core/Math/Quaternion.cs +++ b/Source/Engine/Core/Math/Quaternion.cs @@ -1077,6 +1077,115 @@ namespace FlaxEngine } } + /// + /// Gets the shortest arc quaternion to rotate this vector to the destination vector. + /// + /// The source vector. + /// The destination vector. + /// The result. + /// The fallback axis. + public static void GetRotationFromTo(ref Float3 from, ref Float3 to, out Quaternion result, ref Float3 fallbackAxis) + { + // Based on Stan Melax's article in Game Programming Gems + + Float3 v0 = from; + Float3 v1 = to; + v0.Normalize(); + v1.Normalize(); + + // If dot == 1, vectors are the same + float d = Float3.Dot(ref v0, ref v1); + if (d >= 1.0f) + { + result = Identity; + return; + } + + if (d < 1e-6f - 1.0f) + { + if (fallbackAxis != Float3.Zero) + { + // Rotate 180 degrees about the fallback axis + RotationAxis(ref fallbackAxis, Mathf.Pi, out result); + } + else + { + // Generate an axis + Float3 axis = Float3.Cross(Float3.UnitX, from); + if (axis.LengthSquared < Mathf.Epsilon) // Pick another if colinear + axis = Float3.Cross(Float3.UnitY, from); + axis.Normalize(); + RotationAxis(ref axis, Mathf.Pi, out result); + } + } + else + { + float s = Mathf.Sqrt((1 + d) * 2); + float invS = 1 / s; + Float3.Cross(ref v0, ref v1, out var c); + result.X = c.X * invS; + result.Y = c.Y * invS; + result.Z = c.Z * invS; + result.W = s * 0.5f; + result.Normalize(); + } + } + + /// + /// Gets the shortest arc quaternion to rotate this vector to the destination vector. + /// + /// The source vector. + /// The destination vector. + /// The fallback axis. + /// The rotation. + public static Quaternion GetRotationFromTo(Float3 from, Float3 to, Float3 fallbackAxis) + { + GetRotationFromTo(ref from, ref to, out var result, ref fallbackAxis); + return result; + } + + /// + /// Gets the quaternion that will rotate vector from into vector to, around their plan perpendicular axis.The input vectors don't need to be normalized. + /// + /// The source vector. + /// The destination vector. + /// The result. + public static void FindBetween(ref Float3 from, ref Float3 to, out Quaternion result) + { + // http://lolengine.net/blog/2014/02/24/quaternion-from-two-vectors-final + float normFromNormTo = Mathf.Sqrt(from.LengthSquared * to.LengthSquared); + if (normFromNormTo < Mathf.Epsilon) + { + result = Identity; + return; + } + float w = normFromNormTo + Float3.Dot(from, to); + if (w < 1.0-6f * normFromNormTo) + { + result = Mathf.Abs(from.X) > Mathf.Abs(from.Z) + ? new Quaternion(-from.Y, from.X, 0.0f, 0.0f) + : new Quaternion(0.0f, -from.Z, from.Y, 0.0f); + } + else + { + Float3 cross = Float3.Cross(from, to); + result = new Quaternion(cross.X, cross.Y, cross.Z, w); + } + result.Normalize(); + } + + /// + /// Gets the quaternion that will rotate vector from into vector to, around their plan perpendicular axis.The input vectors don't need to be normalized. + /// + /// The source vector. + /// The destination vector. + /// The rotation. + public static Quaternion FindBetween(Float3 from, Float3 to) + { + FindBetween(ref from, ref to, out var result); + return result; + } + /// /// Creates a left-handed spherical billboard that rotates around a specified object position. /// diff --git a/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs b/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs index afd34bbfd..65377acaa 100644 --- a/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs +++ b/Source/Engine/Core/Math/TypeConverters/ColorConverter.cs @@ -13,9 +13,7 @@ namespace FlaxEngine.TypeConverters public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) - { return true; - } return base.CanConvertFrom(context, sourceType); } @@ -23,9 +21,7 @@ namespace FlaxEngine.TypeConverters public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) - { return false; - } return base.CanConvertTo(context, destinationType); } diff --git a/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs index 4eebbfce4..e0670df05 100644 --- a/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Double2Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Double2Converter : TypeConverter + internal class Double2Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Double2(double.Parse(v[0], culture), double.Parse(v[1], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs index 420e0016c..a66892ecb 100644 --- a/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Double3Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Double3Converter : TypeConverter + internal class Double3Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Double3(double.Parse(v[0], culture), double.Parse(v[1], culture), double.Parse(v[2], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs index fc1d9a7fe..d085217ef 100644 --- a/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Double4Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Double4Converter : TypeConverter + internal class Double4Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Double4(double.Parse(v[0], culture), double.Parse(v[1], culture), double.Parse(v[2], culture), double.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs index a41a0f4d5..4b2ffadf5 100644 --- a/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Float2Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Float2Converter : TypeConverter + internal class Float2Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Float2(float.Parse(v[0], culture), float.Parse(v[1], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs index aded4117e..3739c44ef 100644 --- a/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Float3Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Float3Converter : TypeConverter + internal class Float3Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Float3(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs index 58c76ac65..620f2c838 100644 --- a/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Float4Converter.cs @@ -7,15 +7,13 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Float4Converter : TypeConverter + internal class VectorConverter : TypeConverter { /// public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) - { return true; - } return base.CanConvertFrom(context, sourceType); } @@ -23,18 +21,32 @@ namespace FlaxEngine.TypeConverters public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) - { return false; - } return base.CanConvertTo(context, destinationType); } + internal static string[] GetParts(string str) + { + string[] v = str.Split(','); + if (v.Length == 1) + { + // When converting from ToString() + v = str.Split(' '); + for (int i = 0; i < v.Length; i++) + v[i] = v[i].Substring(v[i].IndexOf(':') + 1); + } + return v; + } + } + + internal class Float4Converter : VectorConverter + { /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Float4(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture), float.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs index c4989c085..f528aa46b 100644 --- a/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Int2Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Int2Converter : TypeConverter + internal class Int2Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Int2(int.Parse(v[0], culture), int.Parse(v[1], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs index fe01f91fd..520f806d0 100644 --- a/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Int3Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Int3Converter : TypeConverter + internal class Int3Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Int3(int.Parse(v[0], culture), int.Parse(v[1], culture), int.Parse(v[2], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs index 2ce0fc202..e9a27dfda 100644 --- a/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Int4Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Int4Converter : TypeConverter + internal class Int4Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Int4(int.Parse(v[0], culture), int.Parse(v[1], culture), int.Parse(v[2], culture), int.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs b/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs index 23bb901be..5d9aa206b 100644 --- a/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs +++ b/Source/Engine/Core/Math/TypeConverters/QuaternionConverter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class QuaternionConverter : TypeConverter + internal class QuaternionConverter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Quaternion(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture), float.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs b/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs index 96d6beadc..acb5b5817 100644 --- a/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Vector2Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Vector2Converter : TypeConverter + internal class Vector2Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Vector2(float.Parse(v[0], culture), float.Parse(v[1], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs b/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs index 23ee4df11..66ec831f0 100644 --- a/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Vector3Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Vector3Converter : TypeConverter + internal class Vector3Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Vector3(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs b/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs index c3b4d074b..f4781f45b 100644 --- a/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs +++ b/Source/Engine/Core/Math/TypeConverters/Vector4Converter.cs @@ -7,34 +7,14 @@ using System.Globalization; namespace FlaxEngine.TypeConverters { - internal class Vector4Converter : TypeConverter + internal class Vector4Converter : VectorConverter { - /// - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - if (sourceType == typeof(string)) - { - return true; - } - return base.CanConvertFrom(context, sourceType); - } - - /// - public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) - { - if (destinationType == typeof(string)) - { - return false; - } - return base.CanConvertTo(context, destinationType); - } - /// public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { if (value is string str) { - string[] v = str.Split(','); + string[] v = GetParts(str); return new Vector4(float.Parse(v[0], culture), float.Parse(v[1], culture), float.Parse(v[2], culture), float.Parse(v[3], culture)); } return base.ConvertFrom(context, culture, value); diff --git a/Source/Engine/Core/Math/Vector2.h b/Source/Engine/Core/Math/Vector2.h index 83deb009a..cea03ec10 100644 --- a/Source/Engine/Core/Math/Vector2.h +++ b/Source/Engine/Core/Math/Vector2.h @@ -646,6 +646,12 @@ inline Vector2Base operator/(typename TOtherFloat::Type a, const Vector2Ba return Vector2Base(a) / b; } +template +inline uint32 GetHash(const Vector2Base& key) +{ + return (*(uint32*)&key.X * 397) ^ *(uint32*)&key.Y; +} + namespace Math { template diff --git a/Source/Engine/Core/Math/Vector3.h b/Source/Engine/Core/Math/Vector3.h index c0ebdf01d..01b55e9bd 100644 --- a/Source/Engine/Core/Math/Vector3.h +++ b/Source/Engine/Core/Math/Vector3.h @@ -977,6 +977,12 @@ inline Vector3Base operator/(typename TOtherFloat::Type a, const Vector3Ba return Vector3Base(a) / b; } +template +inline uint32 GetHash(const Vector3Base& key) +{ + return (((*(uint32*)&key.X * 397) ^ *(uint32*)&key.Y) * 397) ^ *(uint32*)&key.Z; +} + namespace Math { template diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h index 5c7b24c4a..1cc6d4db8 100644 --- a/Source/Engine/Core/Math/Vector4.h +++ b/Source/Engine/Core/Math/Vector4.h @@ -552,6 +552,12 @@ inline Vector4Base operator/(typename TOtherFloat::Type a, const Vector4Ba return Vector4Base(a) / b; } +template +inline uint32 GetHash(const Vector4Base& key) +{ + return (((((*(uint32*)&key.X * 397) ^ *(uint32*)&key.Y) * 397) ^ *(uint32*)&key.Z) * 397) ^*(uint32*)&key.W; +} + namespace Math { template diff --git a/Source/Engine/Core/Memory/Allocation.h b/Source/Engine/Core/Memory/Allocation.h index 89d2f2003..8d7188d91 100644 --- a/Source/Engine/Core/Memory/Allocation.h +++ b/Source/Engine/Core/Memory/Allocation.h @@ -43,14 +43,14 @@ public: return Capacity; } - FORCE_INLINE void Allocate(uint64 capacity) + FORCE_INLINE void Allocate(int32 capacity) { #if ENABLE_ASSERTION_LOW_LAYERS ASSERT(capacity <= Capacity); #endif } - FORCE_INLINE void Relocate(uint64 capacity, int32 oldCount, int32 newCount) + FORCE_INLINE void Relocate(int32 capacity, int32 oldCount, int32 newCount) { #if ENABLE_ASSERTION_LOW_LAYERS ASSERT(capacity <= Capacity); @@ -120,12 +120,15 @@ public: capacity |= capacity >> 4; capacity |= capacity >> 8; capacity |= capacity >> 16; - capacity = (capacity + 1) * 2; + uint64 capacity64 = (uint64)(capacity + 1) * 2; + if (capacity64 > MAX_int32) + capacity64 = MAX_int32; + capacity = (int32)capacity64; } return capacity; } - FORCE_INLINE void Allocate(uint64 capacity) + FORCE_INLINE void Allocate(int32 capacity) { #if ENABLE_ASSERTION_LOW_LAYERS ASSERT(!_data); @@ -137,7 +140,7 @@ public: #endif } - FORCE_INLINE void Relocate(uint64 capacity, int32 oldCount, int32 newCount) + FORCE_INLINE void Relocate(int32 capacity, int32 oldCount, int32 newCount) { T* newData = capacity != 0 ? (T*)Allocator::Allocate(capacity * sizeof(T)) : nullptr; #if !BUILD_RELEASE @@ -210,7 +213,7 @@ public: return minCapacity <= Capacity ? Capacity : _other.CalculateCapacityGrow(capacity, minCapacity); } - FORCE_INLINE void Allocate(uint64 capacity) + FORCE_INLINE void Allocate(int32 capacity) { if (capacity > Capacity) { @@ -219,7 +222,7 @@ public: } } - FORCE_INLINE void Relocate(uint64 capacity, int32 oldCount, int32 newCount) + FORCE_INLINE void Relocate(int32 capacity, int32 oldCount, int32 newCount) { // Check if the new allocation will fit into inlined storage if (capacity <= Capacity) diff --git a/Source/Engine/Core/ObjectsRemovalService.cpp b/Source/Engine/Core/ObjectsRemovalService.cpp index a020f7844..a185ce8b3 100644 --- a/Source/Engine/Core/ObjectsRemovalService.cpp +++ b/Source/Engine/Core/ObjectsRemovalService.cpp @@ -5,6 +5,7 @@ #include "Collections/Dictionary.h" #include "Engine/Engine/Time.h" #include "Engine/Engine/EngineService.h" +#include "Engine/Platform/CriticalSection.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Scripting/ScriptingObject.h" diff --git a/Source/Engine/Core/Types/Span.h b/Source/Engine/Core/Types/Span.h index e0518ec77..b51c858ec 100644 --- a/Source/Engine/Core/Types/Span.h +++ b/Source/Engine/Core/Types/Span.h @@ -107,6 +107,26 @@ public: ASSERT(index >= 0 && index < _length); return _data[index]; } + + FORCE_INLINE T* begin() + { + return _data; + } + + FORCE_INLINE T* end() + { + return _data + _length; + } + + FORCE_INLINE const T* begin() const + { + return _data; + } + + FORCE_INLINE const T* end() const + { + return _data + _length; + } }; template diff --git a/Source/Engine/Core/Types/StringView.h b/Source/Engine/Core/Types/StringView.h index 27c63c999..3e2bbd5f3 100644 --- a/Source/Engine/Core/Types/StringView.h +++ b/Source/Engine/Core/Types/StringView.h @@ -327,6 +327,18 @@ public: bool operator!=(const String& other) const; public: + using StringViewBase::StartsWith; + FORCE_INLINE bool StartsWith(const StringView& prefix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const + { + return StringViewBase::StartsWith(prefix, searchCase); + } + + using StringViewBase::EndsWith; + FORCE_INLINE bool EndsWith(const StringView& suffix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const + { + return StringViewBase::EndsWith(suffix, searchCase); + } + /// /// Gets the left most given number of characters. /// @@ -511,6 +523,18 @@ public: bool operator!=(const StringAnsi& other) const; public: + using StringViewBase::StartsWith; + FORCE_INLINE bool StartsWith(const StringAnsiView& prefix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const + { + return StringViewBase::StartsWith(prefix, searchCase); + } + + using StringViewBase::EndsWith; + FORCE_INLINE bool EndsWith(const StringAnsiView& suffix, StringSearchCase searchCase = StringSearchCase::IgnoreCase) const + { + return StringViewBase::EndsWith(suffix, searchCase); + } + /// /// Retrieves substring created from characters starting from startIndex to the String end. /// diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index df044b299..952e648e6 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -2821,7 +2821,10 @@ void Variant::Inline() type = VariantType::Types::Vector4; } if (type != VariantType::Null) + { + ASSERT(sizeof(data) >= AsBlob.Length); Platform::MemoryCopy(data, AsBlob.Data, AsBlob.Length); + } } if (type != VariantType::Null) { @@ -2912,6 +2915,60 @@ void Variant::Inline() } } +void Variant::InvertInline() +{ + byte data[sizeof(Matrix)]; + switch (Type.Type) + { + case VariantType::Bool: + case VariantType::Int: + case VariantType::Uint: + case VariantType::Int64: + case VariantType::Uint64: + case VariantType::Float: + case VariantType::Double: + case VariantType::Pointer: + case VariantType::String: + case VariantType::Float2: + case VariantType::Float3: + case VariantType::Float4: + case VariantType::Color: +#if !USE_LARGE_WORLDS + case VariantType::BoundingSphere: + case VariantType::BoundingBox: + case VariantType::Ray: +#endif + case VariantType::Guid: + case VariantType::Quaternion: + case VariantType::Rectangle: + case VariantType::Int2: + case VariantType::Int3: + case VariantType::Int4: + case VariantType::Int16: + case VariantType::Uint16: + case VariantType::Double2: + case VariantType::Double3: + case VariantType::Double4: + static_assert(sizeof(data) >= sizeof(AsData), "Invalid memory size."); + Platform::MemoryCopy(data, AsData, sizeof(AsData)); + break; +#if USE_LARGE_WORLDS + case VariantType::BoundingSphere: + case VariantType::BoundingBox: + case VariantType::Ray: +#endif + case VariantType::Transform: + case VariantType::Matrix: + ASSERT(sizeof(data) >= AsBlob.Length); + Platform::MemoryCopy(data, AsBlob.Data, AsBlob.Length); + break; + default: + return; // Not used + } + SetType(VariantType(VariantType::Structure, InBuiltTypesTypeNames[Type.Type])); + CopyStructure(data); +} + Variant Variant::NewValue(const StringAnsiView& typeName) { Variant v; diff --git a/Source/Engine/Core/Types/Variant.h b/Source/Engine/Core/Types/Variant.h index 8cc1e133b..adb49249f 100644 --- a/Source/Engine/Core/Types/Variant.h +++ b/Source/Engine/Core/Types/Variant.h @@ -372,6 +372,9 @@ public: // Inlines potential value type into in-built format (eg. Vector3 stored as Structure, or String stored as ManagedObject). void Inline(); + // Inverts the inlined value from in-built format into generic storage (eg. Float3 from inlined format into Structure). + void InvertInline(); + // Allocates the Variant of the specific type (eg. structure or object or value). static Variant NewValue(const StringAnsiView& typeName); diff --git a/Source/Engine/Core/Types/Version.cpp b/Source/Engine/Core/Types/Version.cpp index 4a11a0af6..c0410d1cb 100644 --- a/Source/Engine/Core/Types/Version.cpp +++ b/Source/Engine/Core/Types/Version.cpp @@ -7,15 +7,15 @@ Version::Version(int32 major, int32 minor, int32 build, int32 revision) { _major = Math::Max(major, 0); _minor = Math::Max(minor, 0); - _build = Math::Max(build, 0); - _revision = Math::Max(revision, 0); + _build = Math::Max(build, -1); + _revision = Math::Max(revision, -1); } Version::Version(int32 major, int32 minor, int32 build) { _major = Math::Max(major, 0); _minor = Math::Max(minor, 0); - _build = Math::Max(build, 0); + _build = Math::Max(build, -1); _revision = -1; } diff --git a/Source/Engine/Core/Utilities.h b/Source/Engine/Core/Utilities.h index 36339baf5..737e423b2 100644 --- a/Source/Engine/Core/Utilities.h +++ b/Source/Engine/Core/Utilities.h @@ -51,10 +51,14 @@ namespace Utilities int32 i = 0; double dblSUnits = static_cast(units); for (; static_cast(units / static_cast(divider)) > 0; i++, units /= divider) - dblSUnits = units / static_cast(divider); + dblSUnits = (double)units / (double)divider; if (i >= sizes.Length()) i = 0; - return String::Format(TEXT("{0} {1}"), RoundTo2DecimalPlaces(dblSUnits), sizes[i]); + String text = String::Format(TEXT("{}"), RoundTo2DecimalPlaces(dblSUnits)); + const int32 dot = text.FindLast('.'); + if (dot != -1) + text = text.Left(dot + 3); + return String::Format(TEXT("{0} {1}"), text, sizes[i]); } // Converts size of the file (in bytes) to the best fitting string diff --git a/Source/Engine/Debug/DebugDraw.cpp b/Source/Engine/Debug/DebugDraw.cpp index 48fa31c91..059ebbd5d 100644 --- a/Source/Engine/Debug/DebugDraw.cpp +++ b/Source/Engine/Debug/DebugDraw.cpp @@ -696,12 +696,15 @@ void* DebugDraw::AllocateContext() void DebugDraw::FreeContext(void* context) { + ASSERT(context); Memory::DestructItem((DebugDrawContext*)context); Allocator::Free(context); } void DebugDraw::UpdateContext(void* context, float deltaTime) { + if (!context) + context = &GlobalContext; ((DebugDrawContext*)context)->DebugDrawDefault.Update(deltaTime); ((DebugDrawContext*)context)->DebugDrawDepthTest.Update(deltaTime); } diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp index 5b93ab588..c0cfad34c 100644 --- a/Source/Engine/Engine/Engine.cpp +++ b/Source/Engine/Engine/Engine.cpp @@ -20,7 +20,6 @@ #include "Engine/Threading/MainThreadTask.h" #include "Engine/Threading/ThreadRegistry.h" #include "Engine/Graphics/GPUDevice.h" -#include "Engine/Scripting/ManagedCLR/MCore.h" #include "Engine/Scripting/ScriptingType.h" #include "Engine/Content/Content.h" #include "Engine/Content/JsonAsset.h" @@ -327,14 +326,6 @@ void Engine::OnUpdate() // Update services EngineService::OnUpdate(); - -#ifdef USE_NETCORE - // Force GC to run in background periodically to avoid large blocking collections causing hitches - if (Time::Update.TicksCount % 60 == 0) - { - MCore::GC::Collect(MCore::GC::MaxGeneration(), MGCCollectionMode::Forced, false, false); - } -#endif } void Engine::OnLateUpdate() @@ -596,11 +587,13 @@ void EngineImpl::InitPaths() Globals::ProjectCacheFolder = Globals::ProjectFolder / TEXT("Cache"); #endif +#if USE_MONO // We must ensure that engine is located in folder which path contains only ANSI characters // Why? Mono lib must have etc and lib folders at ANSI path // But project can be located on Unicode path if (!Globals::StartupFolder.IsANSI()) Platform::Fatal(TEXT("Cannot start application in directory which name contains non-ANSI characters.")); +#endif #if !PLATFORM_SWITCH && !FLAX_TESTS // Setup directories diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index 2094a528f..0391974b6 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -188,7 +188,7 @@ namespace FlaxEngine.Interop internal static void RegisterNativeLibrary(IntPtr moduleNamePtr, IntPtr modulePathPtr) { string moduleName = Marshal.PtrToStringAnsi(moduleNamePtr); - string modulePath = Marshal.PtrToStringAnsi(modulePathPtr); + string modulePath = Marshal.PtrToStringUni(modulePathPtr); libraryPaths[moduleName] = modulePath; } diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index 2f74e421c..fea1fadda 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -1302,7 +1302,8 @@ namespace FlaxEngine.Interop #if !USE_AOT internal bool TryGetDelegate(out Invoker.MarshalAndInvokeDelegate outDeleg, out object outDelegInvoke) { - if (invokeDelegate == null) + // Skip using in-built delegate for value types (eg. Transform) to properly handle instance value passing to method + if (invokeDelegate == null && !method.DeclaringType.IsValueType) { List methodTypes = new List(); if (!method.IsStatic) diff --git a/Source/Engine/Engine/Screen.cpp b/Source/Engine/Engine/Screen.cpp index fc7e8a022..ede49cf92 100644 --- a/Source/Engine/Engine/Screen.cpp +++ b/Source/Engine/Engine/Screen.cpp @@ -22,7 +22,7 @@ class ScreenService : public EngineService { public: ScreenService() - : EngineService(TEXT("Screen"), 120) + : EngineService(TEXT("Screen"), 500) { } diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index 82765f151..898139ce7 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -634,15 +634,12 @@ void Foliage::RemoveFoliageType(int32 index) int32 Foliage::GetFoliageTypeInstancesCount(int32 index) const { PROFILE_CPU(); - int32 result = 0; - - for (auto i = Instances.Begin(); i.IsNotEnd(); i++) + for (auto i = Instances.Begin(); i.IsNotEnd(); ++i) { if (i->Type == index) result++; } - return result; } diff --git a/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h b/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h index 86c723530..f57327fa4 100644 --- a/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h +++ b/Source/Engine/Graphics/Async/Tasks/GPUCopyResourceTask.h @@ -26,12 +26,12 @@ public: , _srcResource(src) , _dstResource(dst) { - _srcResource.OnUnload.Bind(this); - _dstResource.OnUnload.Bind(this); + _srcResource.Released.Bind(this); + _dstResource.Released.Bind(this); } private: - void OnResourceUnload(GPUResourceReference* ref) + void OnResourceReleased() { Cancel(); } @@ -47,14 +47,11 @@ protected: // [GPUTask] Result run(GPUTasksContext* context) override { - if (_srcResource.IsMissing() || _dstResource.IsMissing()) + if (!_srcResource || !_dstResource) return Result::MissingResources; - context->GPU->CopyResource(_dstResource, _srcResource); - return Result::Ok; } - void OnEnd() override { _srcResource.Unlink(); diff --git a/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h b/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h index ab8f1ffad..193eb965d 100644 --- a/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h +++ b/Source/Engine/Graphics/Async/Tasks/GPUCopySubresourceTask.h @@ -31,12 +31,12 @@ public: , _srcSubresource(srcSubresource) , _dstSubresource(dstSubresource) { - _srcResource.OnUnload.Bind(this); - _dstResource.OnUnload.Bind(this); + _srcResource.Released.Bind(this); + _dstResource.Released.Bind(this); } private: - void OnResourceUnload(GPUResourceReference* ref) + void OnResourceReleased() { Cancel(); } @@ -52,14 +52,11 @@ protected: // [GPUTask] Result run(GPUTasksContext* context) override { - if (_srcResource.IsMissing() || _dstResource.IsMissing()) + if (!_srcResource || !_dstResource) return Result::MissingResources; - context->GPU->CopySubresource(_dstResource, _dstSubresource, _srcResource, _srcSubresource); - return Result::Ok; } - void OnEnd() override { _srcResource.Unlink(); diff --git a/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h b/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h index d2f20449c..3d38ce58b 100644 --- a/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h +++ b/Source/Engine/Graphics/Async/Tasks/GPUUploadBufferTask.h @@ -31,7 +31,7 @@ public: , _buffer(buffer) , _offset(offset) { - _buffer.OnUnload.Bind(this); + _buffer.Released.Bind(this); if (copyData) _data.Copy(data); @@ -40,7 +40,7 @@ public: } private: - void OnResourceUnload(BufferReference* ref) + void OnResourceReleased() { Cancel(); } @@ -56,14 +56,11 @@ protected: // [GPUTask] Result run(GPUTasksContext* context) override { - if (_buffer.IsMissing()) + if (!_buffer) return Result::MissingResources; - context->GPU->UpdateBuffer(_buffer, _data.Get(), _data.Length(), _offset); - return Result::Ok; } - void OnEnd() override { _buffer.Unlink(); diff --git a/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h b/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h index 6e9cca7fd..2aff3511b 100644 --- a/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h +++ b/Source/Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h @@ -35,7 +35,7 @@ public: , _rowPitch(rowPitch) , _slicePitch(slicePitch) { - _texture.OnUnload.Bind(this); + _texture.Released.Bind(this); if (copyData) _data.Copy(data); @@ -44,7 +44,7 @@ public: } private: - void OnResourceUnload(GPUTextureReference* ref) + void OnResourceReleased() { Cancel(); } diff --git a/Source/Engine/Graphics/GPUDevice.cpp b/Source/Engine/Graphics/GPUDevice.cpp index 019cfad5a..2a54c7c4d 100644 --- a/Source/Engine/Graphics/GPUDevice.cpp +++ b/Source/Engine/Graphics/GPUDevice.cpp @@ -3,6 +3,7 @@ #include "GPUDevice.h" #include "RenderTargetPool.h" #include "GPUPipelineState.h" +#include "GPUResourceProperty.h" #include "GPUSwapChain.h" #include "RenderTask.h" #include "RenderTools.h" @@ -25,6 +26,39 @@ #include "Engine/Renderer/RenderList.h" #include "Engine/Scripting/Enums.h" +GPUResourcePropertyBase::~GPUResourcePropertyBase() +{ + const auto e = _resource; + if (e) + { + _resource = nullptr; + e->Releasing.Unbind(this); + } +} + +void GPUResourcePropertyBase::OnSet(GPUResource* resource) +{ + auto e = _resource; + if (e != resource) + { + if (e) + e->Releasing.Unbind(this); + _resource = e = resource; + if (e) + e->Releasing.Bind(this); + } +} + +void GPUResourcePropertyBase::OnReleased() +{ + auto e = _resource; + if (e) + { + _resource = nullptr; + e->Releasing.Unbind(this); + } +} + GPUPipelineState* GPUPipelineState::Spawn(const SpawnParams& params) { return GPUDevice::Instance->CreatePipelineState(); @@ -313,6 +347,8 @@ bool GPUDevice::Init() _res->TasksManager.SetExecutor(CreateTasksExecutor()); LOG(Info, "Total graphics memory: {0}", Utilities::BytesToText(TotalGraphicsMemory)); + if (!Limits.HasCompute) + LOG(Warning, "Compute Shaders are not supported"); return false; } @@ -503,6 +539,9 @@ void GPUDevice::DrawEnd() // Call present on all used tasks int32 presentCount = 0; bool anyVSync = false; +#if COMPILE_WITH_PROFILER + const double presentStart = Platform::GetTimeSeconds(); +#endif for (int32 i = 0; i < RenderTask::Tasks.Count(); i++) { const auto task = RenderTask::Tasks[i]; @@ -537,6 +576,10 @@ void GPUDevice::DrawEnd() #endif GetMainContext()->Flush(); } +#if COMPILE_WITH_PROFILER + const double presentEnd = Platform::GetTimeSeconds(); + ProfilerGPU::OnPresentTime((float)((presentEnd - presentStart) * 1000.0)); +#endif _wasVSyncUsed = anyVSync; _isRendering = false; diff --git a/Source/Engine/Graphics/GPUResourceProperty.h b/Source/Engine/Graphics/GPUResourceProperty.h index b3c56007d..0b5d73c40 100644 --- a/Source/Engine/Graphics/GPUResourceProperty.h +++ b/Source/Engine/Graphics/GPUResourceProperty.h @@ -8,28 +8,39 @@ /// /// GPU Resource container utility object. /// -template -class GPUResourceProperty +class FLAXENGINE_API GPUResourcePropertyBase { -private: - T* _resource; +protected: + GPUResource* _resource = nullptr; -private: - // Disable copy actions - GPUResourceProperty(const GPUResourceProperty& other) = delete; +public: + NON_COPYABLE(GPUResourcePropertyBase); + + GPUResourcePropertyBase() = default; + ~GPUResourcePropertyBase(); public: /// - /// Action fired when resource gets unloaded (reference gets cleared bu async tasks should stop execution). + /// Action fired when resource gets released (reference gets cleared bu async tasks should stop execution). /// - Delegate OnUnload; + Action Released; +protected: + void OnSet(GPUResource* resource); + void OnReleased(); +}; + +/// +/// GPU Resource container utility object. +/// +template +class GPUResourceProperty : public GPUResourcePropertyBase +{ public: /// /// Initializes a new instance of the class. /// GPUResourceProperty() - : _resource(nullptr) { } @@ -38,9 +49,37 @@ public: /// /// The resource. GPUResourceProperty(T* resource) - : _resource(nullptr) { - Set(resource); + OnSet(resource); + } + + /// + /// Initializes a new instance of the class. + /// + /// The other value. + GPUResourceProperty(const GPUResourceProperty& other) + { + OnSet(other.Get()); + } + + /// + /// Initializes a new instance of the class. + /// + /// The other value. + GPUResourceProperty(GPUResourceProperty&& other) + { + OnSet(other.Get()); + other.OnSet(nullptr); + } + + GPUResourceProperty& operator=(GPUResourceProperty&& other) + { + if (&other != this) + { + OnSet(other._resource); + other.OnSet(nullptr); + } + return *this; } /// @@ -48,13 +87,6 @@ public: /// ~GPUResourceProperty() { - // Check if object has been binded - if (_resource) - { - // Unlink - _resource->Releasing.template Unbind(this); - _resource = nullptr; - } } public: @@ -63,43 +95,34 @@ public: return Get() == other; } - FORCE_INLINE bool operator==(GPUResourceProperty& other) const + FORCE_INLINE bool operator==(const GPUResourceProperty& other) const { return Get() == other.Get(); } - GPUResourceProperty& operator=(const GPUResourceProperty& other) - { - if (this != &other) - Set(other.Get()); - return *this; - } - FORCE_INLINE GPUResourceProperty& operator=(T& other) { - Set(&other); + OnSet(&other); return *this; } FORCE_INLINE GPUResourceProperty& operator=(T* other) { - Set(other); + OnSet(other); return *this; } /// /// Implicit conversion to GPU Resource /// - /// Resource FORCE_INLINE operator T*() const { - return _resource; + return (T*)_resource; } /// /// Implicit conversion to resource /// - /// True if resource has been binded, otherwise false FORCE_INLINE operator bool() const { return _resource != nullptr; @@ -108,37 +131,17 @@ public: /// /// Implicit conversion to resource /// - /// Resource FORCE_INLINE T* operator->() const { - return _resource; + return (T*)_resource; } /// /// Gets linked resource /// - /// Resource FORCE_INLINE T* Get() const { - return _resource; - } - - /// - /// Checks if resource has been binded - /// - /// True if resource has been binded, otherwise false - FORCE_INLINE bool IsBinded() const - { - return _resource != nullptr; - } - - /// - /// Checks if resource is missing - /// - /// True if resource is missing, otherwise false - FORCE_INLINE bool IsMissing() const - { - return _resource == nullptr; + return (T*)_resource; } public: @@ -148,19 +151,7 @@ public: /// Value to assign void Set(T* value) { - if (_resource != value) - { - // Remove reference from the old one - if (_resource) - _resource->Releasing.template Unbind(this); - - // Change referenced object - _resource = value; - - // Add reference to the new one - if (_resource) - _resource->Releasing.template Bind(this); - } + OnSet(value); } /// @@ -168,22 +159,7 @@ public: /// void Unlink() { - if (_resource) - { - // Remove reference from the old one - _resource->Releasing.template Unbind(this); - _resource = nullptr; - } - } - -private: - void onResourceUnload() - { - if (_resource) - { - _resource = nullptr; - OnUnload(this); - } + OnSet(nullptr); } }; diff --git a/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp b/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp index 9ba2975b2..36dfc4615 100644 --- a/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/DecalMaterialShader.cpp @@ -92,6 +92,8 @@ bool DecalMaterialShader::Load() { GPUPipelineState::Description psDesc0 = GPUPipelineState::Description::DefaultNoDepth; psDesc0.VS = _shader->GetVS("VS_Decal"); + if (psDesc0.VS == nullptr) + return true; psDesc0.PS = _shader->GetPS("PS_Decal"); psDesc0.CullMode = CullMode::Normal; diff --git a/Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp b/Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp index 52a0bd551..a899dbc12 100644 --- a/Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/DeferredMaterialShader.cpp @@ -136,6 +136,7 @@ void DeferredMaterialShader::Unload() bool DeferredMaterialShader::Load() { + bool failed = false; auto psDesc = GPUPipelineState::Description::Default; psDesc.DepthWriteEnable = (_info.FeaturesFlags & MaterialFeaturesFlags::DisableDepthWrite) == MaterialFeaturesFlags::None; if (EnumHasAnyFlags(_info.FeaturesFlags, MaterialFeaturesFlags::DisableDepthTest)) @@ -155,16 +156,20 @@ bool DeferredMaterialShader::Load() // GBuffer Pass psDesc.VS = _shader->GetVS("VS"); + failed |= psDesc.VS == nullptr; psDesc.PS = _shader->GetPS("PS_GBuffer"); _cache.Default.Init(psDesc); psDesc.VS = _shader->GetVS("VS", 1); + failed |= psDesc.VS == nullptr; _cacheInstanced.Default.Init(psDesc); // GBuffer Pass with lightmap (pixel shader permutation for USE_LIGHTMAP=1) psDesc.VS = _shader->GetVS("VS"); + failed |= psDesc.VS == nullptr; psDesc.PS = _shader->GetPS("PS_GBuffer", 1); _cache.DefaultLightmap.Init(psDesc); psDesc.VS = _shader->GetVS("VS", 1); + failed |= psDesc.VS == nullptr; _cacheInstanced.DefaultLightmap.Init(psDesc); // GBuffer Pass with skinning @@ -233,5 +238,5 @@ bool DeferredMaterialShader::Load() psDesc.VS = _shader->GetVS("VS_Skinned"); _cache.DepthSkinned.Init(psDesc); - return false; + return failed; } diff --git a/Source/Engine/Graphics/Materials/ForwardMaterialShader.cpp b/Source/Engine/Graphics/Materials/ForwardMaterialShader.cpp index d391f3295..9fb532b4e 100644 --- a/Source/Engine/Graphics/Materials/ForwardMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/ForwardMaterialShader.cpp @@ -174,6 +174,8 @@ bool ForwardMaterialShader::Load() // Forward Pass psDesc.VS = _shader->GetVS("VS"); + if (psDesc.VS == nullptr) + return true; psDesc.PS = _shader->GetPS("PS_Forward"); psDesc.DepthWriteEnable = false; psDesc.BlendMode = BlendingMode::AlphaBlend; diff --git a/Source/Engine/Graphics/Materials/MaterialInfo.h b/Source/Engine/Graphics/Materials/MaterialInfo.h index 86fab4548..8e4933150 100644 --- a/Source/Engine/Graphics/Materials/MaterialInfo.h +++ b/Source/Engine/Graphics/Materials/MaterialInfo.h @@ -86,7 +86,7 @@ API_ENUM() enum class MaterialBlendMode : byte API_ENUM() enum class MaterialShadingModel : byte { /// - /// The unlit material. Emissive channel is used as an output color. Can perform custom lighting operations or just glow. Won't be affected by the lighting pipeline. + /// The unlit material. The emissive channel is used as an output color. Can perform custom lighting operations or just glow. Won't be affected by the lighting pipeline. /// Unlit = 0, @@ -96,7 +96,7 @@ API_ENUM() enum class MaterialShadingModel : byte Lit = 1, /// - /// The subsurface material. Intended for materials like vax or skin that need light scattering to transport simulation through the object. + /// The subsurface material. Intended for materials like wax or skin that need light scattering to transport simulation through the object. /// Subsurface = 2, @@ -366,12 +366,12 @@ API_ENUM() enum class MaterialDecalBlendingMode : byte API_ENUM() enum class MaterialTransparentLightingMode : byte { /// - /// Default directional lighting evaluated per-pixel at the material surface. Use it for semi-transparent surfaces - with both diffuse and specular lighting component active. + /// Default directional lighting evaluated per-pixel at the material surface. Use it for semi-transparent surfaces - with both diffuse and specular lighting components active. /// Surface = 0, /// - /// Non-directional lighting evaluated per-pixel at material surface. Use it for volumetric objects such as smoke, rain or dust - only diffuse lighting term is active (no specular highlights). + /// Non-directional lighting evaluated per-pixel at material surface. Use it for volumetric objects such as smoke, rain or dust - only the diffuse lighting term is active (no specular highlights). /// SurfaceNonDirectional = 1, }; diff --git a/Source/Engine/Graphics/Models/Mesh.cpp b/Source/Engine/Graphics/Models/Mesh.cpp index f03c013c6..1e531d0e0 100644 --- a/Source/Engine/Graphics/Models/Mesh.cpp +++ b/Source/Engine/Graphics/Models/Mesh.cpp @@ -609,7 +609,7 @@ bool Mesh::DownloadDataCPU(MeshBufferType type, BytesContainer& result, int32& c ScopeLock lock(model->Locker); if (model->IsVirtual()) { - LOG(Error, "Cannot access CPU data of virtual models. Use GPU data download"); + LOG(Error, "Cannot access CPU data of virtual models. Use GPU data download."); return true; } diff --git a/Source/Engine/Graphics/Models/ModelData.Tool.cpp b/Source/Engine/Graphics/Models/ModelData.Tool.cpp index d061d0745..727fe7754 100644 --- a/Source/Engine/Graphics/Models/ModelData.Tool.cpp +++ b/Source/Engine/Graphics/Models/ModelData.Tool.cpp @@ -318,7 +318,9 @@ void MeshData::BuildIndexBuffer() } const auto endTime = Platform::GetTimeSeconds(); - LOG(Info, "Generated index buffer for mesh in {0}s ({1} vertices, {2} indices)", Utilities::RoundTo2DecimalPlaces(endTime - startTime), Positions.Count(), Indices.Count()); + const double time = Utilities::RoundTo2DecimalPlaces(endTime - startTime); + if (time > 0.5f) // Don't log if generation was fast enough + LOG(Info, "Generated {3} for mesh in {0}s ({1} vertices, {2} indices)", time, vertexCount, Indices.Count(), TEXT("indices")); } void MeshData::FindPositions(const Float3& position, float epsilon, Array& result) @@ -449,7 +451,9 @@ bool MeshData::GenerateNormals(float smoothingAngle) } const auto endTime = Platform::GetTimeSeconds(); - LOG(Info, "Generated tangents for mesh in {0}s ({1} vertices, {2} indices)", Utilities::RoundTo2DecimalPlaces(endTime - startTime), vertexCount, indexCount); + const double time = Utilities::RoundTo2DecimalPlaces(endTime - startTime); + if (time > 0.5f) // Don't log if generation was fast enough + LOG(Info, "Generated {3} for mesh in {0}s ({1} vertices, {2} indices)", time, vertexCount, indexCount, TEXT("normals")); return false; } @@ -685,7 +689,10 @@ bool MeshData::GenerateTangents(float smoothingAngle) #endif const auto endTime = Platform::GetTimeSeconds(); - LOG(Info, "Generated tangents for mesh in {0}s ({1} vertices, {2} indices)", Utilities::RoundTo2DecimalPlaces(endTime - startTime), vertexCount, indexCount); + const double time = Utilities::RoundTo2DecimalPlaces(endTime - startTime); + if (time > 0.5f) // Don't log if generation was fast enough + LOG(Info, "Generated {3} for mesh in {0}s ({1} vertices, {2} indices)", time, vertexCount, indexCount, TEXT("tangents")); + return false; } @@ -872,7 +879,9 @@ void MeshData::ImproveCacheLocality() Allocator::Free(piCandidates); const auto endTime = Platform::GetTimeSeconds(); - LOG(Info, "Cache relevant optimize for {0} vertices and {1} indices. Average output ACMR is {2}. Time: {3}s", vertexCount, indexCount, (float)iCacheMisses / indexCount / 3, Utilities::RoundTo2DecimalPlaces(endTime - startTime)); + const double time = Utilities::RoundTo2DecimalPlaces(endTime - startTime); + if (time > 0.5f) // Don't log if generation was fast enough + LOG(Info, "Generated {3} for mesh in {0}s ({1} vertices, {2} indices)", time, vertexCount, indexCount, TEXT("optimized indices")); } float MeshData::CalculateTrianglesArea() const diff --git a/Source/Engine/Graphics/Models/ModelData.h b/Source/Engine/Graphics/Models/ModelData.h index 32caa2d01..d7359d905 100644 --- a/Source/Engine/Graphics/Models/ModelData.h +++ b/Source/Engine/Graphics/Models/ModelData.h @@ -90,6 +90,21 @@ public: /// Array BlendShapes; + /// + /// Global translation for this mesh to be at it's local origin. + /// + Vector3 OriginTranslation = Vector3::Zero; + + /// + /// Orientation for this mesh at it's local origin. + /// + Quaternion OriginOrientation = Quaternion::Identity; + + /// + /// Meshes scaling. + /// + Vector3 Scaling = Vector3::One; + public: /// /// Determines whether this instance has any mesh data. diff --git a/Source/Engine/Graphics/PostProcessSettings.h b/Source/Engine/Graphics/PostProcessSettings.h index f3440c4a5..d2ace6cea 100644 --- a/Source/Engine/Graphics/PostProcessSettings.h +++ b/Source/Engine/Graphics/PostProcessSettings.h @@ -5,6 +5,7 @@ #include "Engine/Core/Math/Vector3.h" #include "Engine/Core/Math/Vector4.h" #include "Engine/Content/AssetReference.h" +#include "Engine/Content/SoftAssetReference.h" #include "Engine/Core/ISerializable.h" #include "Engine/Content/Assets/Texture.h" #include "Engine/Content/Assets/MaterialBase.h" @@ -850,7 +851,7 @@ API_STRUCT() struct FLAXENGINE_API ColorGradingSettings : ISerializable /// The Lookup Table (LUT) used to perform color correction. /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(22), PostProcessSetting((int)ColorGradingSettingsOverride.LutTexture)") - AssetReference LutTexture; + SoftAssetReference LutTexture; /// /// The LUT blending weight (normalized to range 0-1). Default is 1.0. @@ -1277,7 +1278,7 @@ API_STRUCT() struct FLAXENGINE_API LensFlaresSettings : ISerializable /// Fullscreen lens dirt texture. /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(8), PostProcessSetting((int)LensFlaresSettingsOverride.LensDirt)") - AssetReference LensDirt; + SoftAssetReference LensDirt; /// /// Fullscreen lens dirt intensity parameter. Allows to tune dirt visibility. @@ -1289,13 +1290,13 @@ API_STRUCT() struct FLAXENGINE_API LensFlaresSettings : ISerializable /// Custom lens color texture (1D) used for lens color spectrum. /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(10), PostProcessSetting((int)LensFlaresSettingsOverride.LensColor)") - AssetReference LensColor; + SoftAssetReference LensColor; /// /// Custom lens star texture sampled by lens flares. /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(11), PostProcessSetting((int)LensFlaresSettingsOverride.LensStar)") - AssetReference LensStar; + SoftAssetReference LensStar; public: /// @@ -1487,7 +1488,7 @@ API_STRUCT() struct FLAXENGINE_API DepthOfFieldSettings : ISerializable /// If BokehShape is set to Custom, then this texture will be used for the bokeh shapes. For best performance, use small, compressed, grayscale textures (for instance 32px). /// API_FIELD(Attributes="DefaultValue(null), EditorOrder(11), PostProcessSetting((int)DepthOfFieldSettingsOverride.BokehShapeCustom)") - AssetReference BokehShapeCustom; + SoftAssetReference BokehShapeCustom; /// /// The minimum pixel brightness to create bokeh. Pixels with lower brightness will be skipped. diff --git a/Source/Engine/Graphics/Shaders/GPUShader.cpp b/Source/Engine/Graphics/Shaders/GPUShader.cpp index e76ee9996..86d983fd8 100644 --- a/Source/Engine/Graphics/Shaders/GPUShader.cpp +++ b/Source/Engine/Graphics/Shaders/GPUShader.cpp @@ -87,7 +87,11 @@ bool GPUShader::Create(MemoryReadStream& stream) GPUShaderProgramInitializer initializer; #if !BUILD_RELEASE initializer.Owner = this; + const StringView name = GetName(); +#else + const StringView name; #endif + const bool hasCompute = GPUDevice::Instance->Limits.HasCompute; for (int32 i = 0; i < shadersCount; i++) { const ShaderStage type = static_cast(stream.ReadByte()); @@ -117,10 +121,15 @@ bool GPUShader::Create(MemoryReadStream& stream) stream.ReadBytes(&initializer.Bindings, sizeof(ShaderBindings)); // Create shader program + if (type == ShaderStage::Compute && !hasCompute) + { + LOG(Warning, "Failed to create {} Shader program '{}' ({}).", ::ToString(type), String(initializer.Name), name); + continue; + } GPUShaderProgram* shader = CreateGPUShaderProgram(type, initializer, cache, cacheSize, stream); if (shader == nullptr) { - LOG(Error, "Failed to create {} Shader program '{}'.", ::ToString(type), String(initializer.Name)); + LOG(Error, "Failed to create {} Shader program '{}' ({}).", ::ToString(type), String(initializer.Name), name); return true; } diff --git a/Source/Engine/Graphics/Shaders/GPUShaderProgram.h b/Source/Engine/Graphics/Shaders/GPUShaderProgram.h index 09c45506f..31f6638dc 100644 --- a/Source/Engine/Graphics/Shaders/GPUShaderProgram.h +++ b/Source/Engine/Graphics/Shaders/GPUShaderProgram.h @@ -122,6 +122,19 @@ public: /// class GPUShaderProgramVS : public GPUShaderProgram { +public: + // Input element run-time data (see VertexShaderMeta::InputElement for compile-time data) + PACK_STRUCT(struct InputElement + { + byte Type; // VertexShaderMeta::InputType + byte Index; + byte Format; // PixelFormat + byte InputSlot; + uint32 AlignedByteOffset; // Fixed value or INPUT_LAYOUT_ELEMENT_ALIGN if auto + byte InputSlotClass; // INPUT_LAYOUT_ELEMENT_PER_VERTEX_DATA or INPUT_LAYOUT_ELEMENT_PER_INSTANCE_DATA + uint32 InstanceDataStepRate; // 0 if per-vertex + }); + public: /// /// Gets input layout description handle (platform dependent). diff --git a/Source/Engine/Graphics/Textures/StreamingTexture.cpp b/Source/Engine/Graphics/Textures/StreamingTexture.cpp index 409125838..173b0ef85 100644 --- a/Source/Engine/Graphics/Textures/StreamingTexture.cpp +++ b/Source/Engine/Graphics/Textures/StreamingTexture.cpp @@ -22,10 +22,10 @@ TextureHeader::TextureHeader() TextureGroup = -1; } -TextureHeader::TextureHeader(TextureHeader_Deprecated& old) +TextureHeader::TextureHeader(const TextureHeader_Deprecated& old) { Platform::MemoryClear(this, sizeof(*this)); - Width = old.Width;; + Width = old.Width; Height = old.Height; MipLevels = old.MipLevels; Format = old.Format; @@ -49,7 +49,7 @@ StreamingTexture::StreamingTexture(ITextureOwner* parent, const String& name) , _texture(nullptr) , _isBlockCompressed(false) { - ASSERT(_owner != nullptr); + ASSERT(parent != nullptr); // Always have created texture object ASSERT(GPUDevice::Instance); @@ -329,11 +329,11 @@ public: , _dataLock(_streamingTexture->GetOwner()->LockData()) { _streamingTexture->_streamingTasks.Add(this); - _texture.OnUnload.Bind(this); + _texture.Released.Bind(this); } private: - void onResourceUnload2(GPUTextureReference* ref) + void OnResourceReleased2() { // Unlink texture if (_streamingTexture) diff --git a/Source/Engine/Graphics/Textures/TextureBase.cpp b/Source/Engine/Graphics/Textures/TextureBase.cpp index 538b15a4a..181955fce 100644 --- a/Source/Engine/Graphics/Textures/TextureBase.cpp +++ b/Source/Engine/Graphics/Textures/TextureBase.cpp @@ -660,6 +660,7 @@ uint64 TextureBase::GetMemoryUsage() const void TextureBase::CancelStreaming() { + Asset::CancelStreaming(); _texture.CancelStreamingTasks(); } diff --git a/Source/Engine/Graphics/Textures/Types.h b/Source/Engine/Graphics/Textures/Types.h index 9593cbbc2..31e0b1afc 100644 --- a/Source/Engine/Graphics/Textures/Types.h +++ b/Source/Engine/Graphics/Textures/Types.h @@ -106,5 +106,5 @@ struct FLAXENGINE_API TextureHeader byte CustomData[10]; TextureHeader(); - TextureHeader(TextureHeader_Deprecated& old); + TextureHeader(const TextureHeader_Deprecated& old); }; diff --git a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp index b58684a4e..52192c634 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp +++ b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderDX11.cpp @@ -15,32 +15,21 @@ GPUShaderProgram* GPUShaderDX11::CreateGPUShaderProgram(ShaderStage type, const { case ShaderStage::Vertex: { - D3D11_INPUT_ELEMENT_DESC inputLayoutDesc[VERTEX_SHADER_MAX_INPUT_ELEMENTS]; - - // Temporary variables - byte Type, Format, Index, InputSlot, InputSlotClass; - uint32 AlignedByteOffset, InstanceDataStepRate; - - // Load Input Layout (it may be empty) + // Load Input Layout byte inputLayoutSize; stream.ReadByte(&inputLayoutSize); ASSERT(inputLayoutSize <= VERTEX_SHADER_MAX_INPUT_ELEMENTS); + D3D11_INPUT_ELEMENT_DESC inputLayoutDesc[VERTEX_SHADER_MAX_INPUT_ELEMENTS]; for (int32 a = 0; a < inputLayoutSize; a++) { // Read description - // TODO: maybe use struct and load at once? - stream.ReadByte(&Type); - stream.ReadByte(&Index); - stream.ReadByte(&Format); - stream.ReadByte(&InputSlot); - stream.ReadUint32(&AlignedByteOffset); - stream.ReadByte(&InputSlotClass); - stream.ReadUint32(&InstanceDataStepRate); + GPUShaderProgramVS::InputElement inputElement; + stream.Read(inputElement); // Get semantic name const char* semanticName = nullptr; // TODO: maybe use enum+mapping ? - switch (Type) + switch (inputElement.Type) { case 1: semanticName = "POSITION"; @@ -70,7 +59,7 @@ GPUShaderProgram* GPUShaderDX11::CreateGPUShaderProgram(ShaderStage type, const semanticName = "BLENDWEIGHT"; break; default: - LOG(Fatal, "Invalid vertex shader element semantic type: {0}", Type); + LOG(Fatal, "Invalid vertex shader element semantic type: {0}", inputElement.Type); break; } @@ -78,12 +67,12 @@ GPUShaderProgram* GPUShaderDX11::CreateGPUShaderProgram(ShaderStage type, const inputLayoutDesc[a] = { semanticName, - static_cast(Index), - static_cast(Format), - static_cast(InputSlot), - static_cast(AlignedByteOffset), - static_cast(InputSlotClass), - static_cast(InstanceDataStepRate) + static_cast(inputElement.Index), + static_cast(inputElement.Format), + static_cast(inputElement.InputSlot), + static_cast(inputElement.AlignedByteOffset), + static_cast(inputElement.InputSlotClass), + static_cast(inputElement.InstanceDataStepRate) }; } diff --git a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp index e1e716853..07352b674 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp +++ b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUShaderDX12.cpp @@ -20,32 +20,21 @@ GPUShaderProgram* GPUShaderDX12::CreateGPUShaderProgram(ShaderStage type, const { case ShaderStage::Vertex: { - D3D12_INPUT_ELEMENT_DESC inputLayout[VERTEX_SHADER_MAX_INPUT_ELEMENTS]; - - // Temporary variables - byte Type, Format, Index, InputSlot, InputSlotClass; - uint32 AlignedByteOffset, InstanceDataStepRate; - // Load Input Layout (it may be empty) byte inputLayoutSize; stream.ReadByte(&inputLayoutSize); ASSERT(inputLayoutSize <= VERTEX_SHADER_MAX_INPUT_ELEMENTS); + D3D12_INPUT_ELEMENT_DESC inputLayout[VERTEX_SHADER_MAX_INPUT_ELEMENTS]; for (int32 a = 0; a < inputLayoutSize; a++) { // Read description - // TODO: maybe use struct and load at once? - stream.ReadByte(&Type); - stream.ReadByte(&Index); - stream.ReadByte(&Format); - stream.ReadByte(&InputSlot); - stream.ReadUint32(&AlignedByteOffset); - stream.ReadByte(&InputSlotClass); - stream.ReadUint32(&InstanceDataStepRate); + GPUShaderProgramVS::InputElement inputElement; + stream.Read(inputElement); // Get semantic name const char* semanticName = nullptr; // TODO: maybe use enum+mapping ? - switch (Type) + switch (inputElement.Type) { case 1: semanticName = "POSITION"; @@ -75,7 +64,7 @@ GPUShaderProgram* GPUShaderDX12::CreateGPUShaderProgram(ShaderStage type, const semanticName = "BLENDWEIGHT"; break; default: - LOG(Fatal, "Invalid vertex shader element semantic type: {0}", Type); + LOG(Fatal, "Invalid vertex shader element semantic type: {0}", inputElement.Type); break; } @@ -83,12 +72,12 @@ GPUShaderProgram* GPUShaderDX12::CreateGPUShaderProgram(ShaderStage type, const inputLayout[a] = { semanticName, - static_cast(Index), - static_cast(Format), - static_cast(InputSlot), - static_cast(AlignedByteOffset), - static_cast(InputSlotClass), - static_cast(InstanceDataStepRate) + static_cast(inputElement.Index), + static_cast(inputElement.Format), + static_cast(inputElement.InputSlot), + static_cast(inputElement.AlignedByteOffset), + static_cast(inputElement.InputSlotClass), + static_cast(inputElement.InstanceDataStepRate) }; } diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUShaderVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUShaderVulkan.cpp index 5e583d713..852ce5bad 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUShaderVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUShaderVulkan.cpp @@ -14,9 +14,9 @@ #include "Engine/Graphics/PixelFormatExtensions.h" #if PLATFORM_DESKTOP -#define VULKAN_UNIFORM_RING_BUFFER_SIZE 24 * 1024 * 1024 +#define VULKAN_UNIFORM_RING_BUFFER_SIZE (24 * 1024 * 1024) #else -#define VULKAN_UNIFORM_RING_BUFFER_SIZE 8 * 1024 * 1024 +#define VULKAN_UNIFORM_RING_BUFFER_SIZE (8 * 1024 * 1024) #endif UniformBufferUploaderVulkan::UniformBufferUploaderVulkan(GPUDeviceVulkan* device) @@ -153,10 +153,6 @@ GPUShaderProgram* GPUShaderVulkan::CreateGPUShaderProgram(ShaderStage type, cons vertexBindingDescriptions[i].inputRate = VK_VERTEX_INPUT_RATE_VERTEX; } - // Temporary variables - byte Type, Format, Index, InputSlot, InputSlotClass; - uint32 AlignedByteOffset, InstanceDataStepRate; - // Load Input Layout (it may be empty) byte inputLayoutSize; stream.ReadByte(&inputLayoutSize); @@ -167,32 +163,26 @@ GPUShaderProgram* GPUShaderVulkan::CreateGPUShaderProgram(ShaderStage type, cons for (int32 a = 0; a < inputLayoutSize; a++) { // Read description - // TODO: maybe use struct and load at once? - stream.ReadByte(&Type); - stream.ReadByte(&Index); - stream.ReadByte(&Format); - stream.ReadByte(&InputSlot); - stream.ReadUint32(&AlignedByteOffset); - stream.ReadByte(&InputSlotClass); - stream.ReadUint32(&InstanceDataStepRate); + GPUShaderProgramVS::InputElement inputElement; + stream.Read(inputElement); - const auto size = PixelFormatExtensions::SizeInBytes((PixelFormat)Format); - if (AlignedByteOffset != INPUT_LAYOUT_ELEMENT_ALIGN) - offset = AlignedByteOffset; + const auto size = PixelFormatExtensions::SizeInBytes((PixelFormat)inputElement.Format); + if (inputElement.AlignedByteOffset != INPUT_LAYOUT_ELEMENT_ALIGN) + offset = inputElement.AlignedByteOffset; - auto& vertexBindingDescription = vertexBindingDescriptions[InputSlot]; - vertexBindingDescription.binding = InputSlot; + auto& vertexBindingDescription = vertexBindingDescriptions[inputElement.InputSlot]; + vertexBindingDescription.binding = inputElement.InputSlot; vertexBindingDescription.stride = Math::Max(vertexBindingDescription.stride, (uint32_t)(offset + size)); - vertexBindingDescription.inputRate = InputSlotClass == INPUT_LAYOUT_ELEMENT_PER_VERTEX_DATA ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE; - ASSERT(InstanceDataStepRate == 0 || InstanceDataStepRate == 1); + vertexBindingDescription.inputRate = inputElement.InputSlotClass == INPUT_LAYOUT_ELEMENT_PER_VERTEX_DATA ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE; + ASSERT(inputElement.InstanceDataStepRate == 0 || inputElement.InstanceDataStepRate == 1); auto& vertexAttributeDescription = vertexAttributeDescriptions[a]; vertexAttributeDescription.location = a; - vertexAttributeDescription.binding = InputSlot; - vertexAttributeDescription.format = RenderToolsVulkan::ToVulkanFormat((PixelFormat)Format); + vertexAttributeDescription.binding = inputElement.InputSlot; + vertexAttributeDescription.format = RenderToolsVulkan::ToVulkanFormat((PixelFormat)inputElement.Format); vertexAttributeDescription.offset = offset; - bindingsCount = Math::Max(bindingsCount, (uint32)InputSlot + 1); + bindingsCount = Math::Max(bindingsCount, (uint32)inputElement.InputSlot + 1); offset += size; } diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 75e3c12d4..addb4861e 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -1406,7 +1406,7 @@ Script* Actor::FindScript(const MClass* type) const CHECK_RETURN(type, nullptr); for (auto script : Scripts) { - if (script->GetClass()->IsSubClassOf(type)) + if (script->GetClass()->IsSubClassOf(type) || script->GetClass()->HasInterface(type)) return script; } for (auto child : Children) diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index 459e1fb6b..88f7e6876 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -706,13 +706,12 @@ void AnimatedModel::UpdateBounds() const int32 bonesCount = skeleton.Bones.Count(); for (int32 boneIndex = 0; boneIndex < bonesCount; boneIndex++) box.Merge(_transform.LocalToWorld(GraphInstance.NodesPose[skeleton.Bones.Get()[boneIndex].NodeIndex].GetTranslation())); - _box = box; } // Apply margin based on model dimensions const Vector3 modelBoxSize = modelBox.GetSize(); - const Vector3 center = _box.GetCenter(); - const Vector3 sizeHalf = Vector3::Max(_box.GetSize() + modelBoxSize * 0.2f, modelBoxSize) * 0.5f; + const Vector3 center = box.GetCenter(); + const Vector3 sizeHalf = Vector3::Max(box.GetSize() + modelBoxSize * 0.2f, modelBoxSize) * 0.5f; _box = BoundingBox(center - sizeHalf, center + sizeHalf); } else diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index d070d99a2..0c5c4d73b 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -12,7 +12,7 @@ /// /// Performs an animation and renders a skinned model. /// -API_CLASS(Attributes="ActorContextMenu(\"New/Other/Animated Model\"), ActorToolbox(\"Other\")") +API_CLASS(Attributes="ActorContextMenu(\"New/Other/Animated Model\"), ActorToolbox(\"Visuals\")") class FLAXENGINE_API AnimatedModel : public ModelInstanceActor { DECLARE_SCENE_OBJECT(AnimatedModel); diff --git a/Source/Engine/Level/Actors/Camera.h b/Source/Engine/Level/Actors/Camera.h index 22d950dd4..c63a5dcf6 100644 --- a/Source/Engine/Level/Actors/Camera.h +++ b/Source/Engine/Level/Actors/Camera.h @@ -66,7 +66,7 @@ public: /// /// Gets the value indicating if camera should use perspective rendering mode, otherwise it will use orthographic projection. /// - API_PROPERTY(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Camera\"), Tooltip(\"Enables perspective projection mode, otherwise uses orthographic.\")") + API_PROPERTY(Attributes="EditorOrder(10), DefaultValue(true), EditorDisplay(\"Camera\")") bool GetUsePerspective() const; /// @@ -77,7 +77,7 @@ public: /// /// Gets the camera's field of view (in degrees). /// - API_PROPERTY(Attributes="EditorOrder(10), DefaultValue(60.0f), Limit(0, 179), EditorDisplay(\"Camera\", \"Field Of View\"), Tooltip(\"Field of view angle in degrees.\")") + API_PROPERTY(Attributes="EditorOrder(20), DefaultValue(60.0f), Limit(0, 179), EditorDisplay(\"Camera\", \"Field Of View\"), VisibleIf(nameof(UsePerspective))") float GetFieldOfView() const; /// @@ -88,7 +88,7 @@ public: /// /// Gets the custom aspect ratio. 0 if not use custom value. /// - API_PROPERTY(Attributes="EditorOrder(50), DefaultValue(0.0f), Limit(0, 10, 0.01f), EditorDisplay(\"Camera\"), Tooltip(\"Custom aspect ratio to use. Set to 0 to disable.\")") + API_PROPERTY(Attributes="EditorOrder(50), DefaultValue(0.0f), Limit(0, 10, 0.01f), EditorDisplay(\"Camera\"), VisibleIf(nameof(UsePerspective))") float GetCustomAspectRatio() const; /// @@ -99,7 +99,7 @@ public: /// /// Gets camera's near plane distance. /// - API_PROPERTY(Attributes="EditorOrder(30), DefaultValue(10.0f), Limit(0, 1000, 0.05f), EditorDisplay(\"Camera\"), Tooltip(\"Near clipping plane distance\")") + API_PROPERTY(Attributes="EditorOrder(30), DefaultValue(10.0f), Limit(0, 1000, 0.05f), EditorDisplay(\"Camera\")") float GetNearPlane() const; /// @@ -110,7 +110,7 @@ public: /// /// Gets camera's far plane distance. /// - API_PROPERTY(Attributes="EditorOrder(40), DefaultValue(40000.0f), Limit(0, float.MaxValue, 5), EditorDisplay(\"Camera\"), Tooltip(\"Far clipping plane distance\")") + API_PROPERTY(Attributes="EditorOrder(40), DefaultValue(40000.0f), Limit(0, float.MaxValue, 5), EditorDisplay(\"Camera\")") float GetFarPlane() const; /// @@ -121,7 +121,7 @@ public: /// /// Gets the orthographic projection scale. /// - API_PROPERTY(Attributes="EditorOrder(60), DefaultValue(1.0f), Limit(0.0001f, 1000, 0.01f), EditorDisplay(\"Camera\"), Tooltip(\"Orthographic projection scale\")") + API_PROPERTY(Attributes="EditorOrder(60), DefaultValue(1.0f), Limit(0.0001f, 1000, 0.01f), EditorDisplay(\"Camera\"), VisibleIf(nameof(UsePerspective), true)") float GetOrthographicScale() const; /// diff --git a/Source/Engine/Level/Actors/SpotLight.h b/Source/Engine/Level/Actors/SpotLight.h index 37735692a..2c16d38e4 100644 --- a/Source/Engine/Level/Actors/SpotLight.h +++ b/Source/Engine/Level/Actors/SpotLight.h @@ -24,7 +24,7 @@ private: public: /// - /// Light source bulb radius + /// Light source bulb radius. /// API_FIELD(Attributes="EditorOrder(2), DefaultValue(0.0f), EditorDisplay(\"Light\"), Limit(0, 1000, 0.01f)") float SourceRadius = 0.0f; @@ -42,54 +42,51 @@ public: float FallOffExponent = 8.0f; /// - /// IES texture (light profiles from real world measured data) + /// IES texture (light profiles from real world measured data). /// API_FIELD(Attributes="EditorOrder(211), DefaultValue(null), EditorDisplay(\"IES Profile\", \"IES Texture\")") AssetReference IESTexture; /// - /// Enable/disable using light brightness from IES profile + /// Enable/disable using light brightness from IES profile. /// API_FIELD(Attributes="EditorOrder(212), DefaultValue(false), EditorDisplay(\"IES Profile\", \"Use IES Brightness\")") bool UseIESBrightness = false; /// - /// Global scale for IES brightness contribution + /// Global scale for IES brightness contribution. /// API_FIELD(Attributes="EditorOrder(213), DefaultValue(1.0f), Limit(0, 10000, 0.01f), EditorDisplay(\"IES Profile\", \"Brightness Scale\")") float IESBrightnessScale = 1.0f; public: /// - /// Computes light brightness value + /// Computes light brightness value. /// - /// Brightness float ComputeBrightness() const; /// - /// Gets scaled light radius + /// Gets scaled light radius. /// float GetScaledRadius() const; /// - /// Gets light radius + /// Gets light radius. /// - API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(1000.0f), EditorDisplay(\"Light\"), Tooltip(\"Light radius\"), Limit(0, 10000, 0.1f)") + API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(1000.0f), EditorDisplay(\"Light\"), Limit(0, 10000, 0.1f)") FORCE_INLINE float GetRadius() const { return _radius; } /// - /// Sets light radius + /// Sets light radius. /// - /// New radius API_PROPERTY() void SetRadius(float value); /// - /// Gets the spot light's outer cone angle (in degrees) + /// Gets the spot light's outer cone angle (in degrees). /// - /// Outer angle (in degrees) API_PROPERTY(Attributes="EditorOrder(22), DefaultValue(43.0f), EditorDisplay(\"Light\"), Limit(1, 89, 0.1f)") FORCE_INLINE float GetOuterConeAngle() const { @@ -97,15 +94,13 @@ public: } /// - /// Sets the spot light's outer cone angle (in degrees) + /// Sets the spot light's outer cone angle (in degrees). /// - /// Value to assign API_PROPERTY() void SetOuterConeAngle(float value); /// - /// Sets the spot light's inner cone angle (in degrees) + /// Sets the spot light's inner cone angle (in degrees). /// - /// Inner angle (in degrees) API_PROPERTY(Attributes="EditorOrder(21), DefaultValue(10.0f), EditorDisplay(\"Light\"), Limit(1, 89, 0.1f)") FORCE_INLINE float GetInnerConeAngle() const { @@ -113,9 +108,8 @@ public: } /// - /// Sets the spot light's inner cone angle (in degrees) + /// Sets the spot light's inner cone angle (in degrees). /// - /// Value to assign API_PROPERTY() void SetInnerConeAngle(float value); private: diff --git a/Source/Engine/Level/Actors/StaticModel.h b/Source/Engine/Level/Actors/StaticModel.h index 2e932096e..2f064eb8a 100644 --- a/Source/Engine/Level/Actors/StaticModel.h +++ b/Source/Engine/Level/Actors/StaticModel.h @@ -10,7 +10,8 @@ /// /// Renders model on the screen. /// -API_CLASS(Attributes="ActorContextMenu(\"New/Model\")") class FLAXENGINE_API StaticModel : public ModelInstanceActor +API_CLASS(Attributes="ActorContextMenu(\"New/Model\"), ActorToolbox(\"Visuals\")") +class FLAXENGINE_API StaticModel : public ModelInstanceActor { DECLARE_SCENE_OBJECT(StaticModel); private: diff --git a/Source/Engine/Level/LargeWorlds.h b/Source/Engine/Level/LargeWorlds.h index 514c1d5ac..2d92b7228 100644 --- a/Source/Engine/Level/LargeWorlds.h +++ b/Source/Engine/Level/LargeWorlds.h @@ -19,7 +19,7 @@ API_CLASS(Static) class FLAXENGINE_API LargeWorlds /// /// Defines the size of a single chunk. Large world (64-bit) gets divided into smaller chunks so all the math operations (32-bit) can be performed relative to the chunk origin without precision loss. /// - API_FIELD() static constexpr Real ChunkSize = 262144; + API_FIELD() static constexpr Real ChunkSize = 8192; /// /// Updates the large world origin to match the input position. The origin is snapped to the best matching chunk location. diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index df65fec3e..cfdf11a6f 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -132,7 +132,7 @@ class LevelService : public EngineService { public: LevelService() - : EngineService(TEXT("Scene Manager"), 30) + : EngineService(TEXT("Scene Manager"), 200) { } @@ -416,7 +416,7 @@ public: } // Load scene - if (Level::loadScene(SceneAsset.Get())) + if (Level::loadScene(SceneAsset)) { LOG(Error, "Failed to deserialize scene {0}", SceneId); CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId); @@ -816,40 +816,10 @@ bool LevelImpl::unloadScenes() return false; } -bool Level::loadScene(const Guid& sceneId) -{ - const auto sceneAsset = Content::LoadAsync(sceneId); - return loadScene(sceneAsset); -} - -bool Level::loadScene(const String& scenePath) -{ - LOG(Info, "Loading scene from file. Path: \'{0}\'", scenePath); - - // Check for missing file - if (!FileSystem::FileExists(scenePath)) - { - LOG(Error, "Missing scene file."); - return true; - } - - // Load file - BytesContainer sceneData; - if (File::ReadAllBytes(scenePath, sceneData)) - { - LOG(Error, "Cannot load data from file."); - return true; - } - - return loadScene(sceneData); -} - bool Level::loadScene(JsonAsset* sceneAsset) { // Keep reference to the asset (prevent unloading during action) AssetReference ref = sceneAsset; - - // Wait for loaded if (sceneAsset == nullptr || sceneAsset->WaitForLoaded()) { LOG(Error, "Cannot load scene asset."); @@ -879,6 +849,7 @@ bool Level::loadScene(const BytesContainer& sceneData, Scene** outScene) return true; } + ScopeLock lock(ScenesLock); return loadScene(document, outScene); } @@ -975,6 +946,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou SceneObject** objects = sceneObjects->Get(); if (context.Async) { + ScenesLock.Unlock(); // Unlock scenes from Main Thread so Job Threads can use it to safely setup actors hierarchy (see Actor::Deserialize) JobSystem::Execute([&](int32 i) { i++; // Start from 1. at index [0] was scene @@ -992,6 +964,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou else SceneObjectsFactory::HandleObjectDeserializationError(stream); }, objectsCount - 1); + ScenesLock.Lock(); } else { @@ -1330,6 +1303,7 @@ bool Level::LoadScene(const Guid& id) } // Load scene + ScopeLock lock(ScenesLock); if (loadScene(sceneAsset)) { LOG(Error, "Failed to deserialize scene {0}", id); diff --git a/Source/Engine/Level/Level.h b/Source/Engine/Level/Level.h index 1f0acda2d..e09f8e376 100644 --- a/Source/Engine/Level/Level.h +++ b/Source/Engine/Level/Level.h @@ -541,8 +541,8 @@ private: }; static void callActorEvent(ActorEventType eventType, Actor* a, Actor* b); - static bool loadScene(const Guid& sceneId); - static bool loadScene(const String& scenePath); + + // All loadScene assume that ScenesLock has been taken by the calling thread static bool loadScene(JsonAsset* sceneAsset); static bool loadScene(const BytesContainer& sceneData, Scene** outScene = nullptr); static bool loadScene(rapidjson_flax::Document& document, Scene** outScene = nullptr); diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp index 123013064..de164343b 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.cpp +++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp @@ -29,7 +29,7 @@ class PrefabManagerService : public EngineService { public: PrefabManagerService() - : EngineService(TEXT("Prefab Manager"), 110) + : EngineService(TEXT("Prefab Manager")) { } }; @@ -39,7 +39,7 @@ PrefabManagerService PrefabManagerServiceInstance; Actor* PrefabManager::SpawnPrefab(Prefab* prefab) { Actor* parent = Level::Scenes.Count() != 0 ? Level::Scenes.Get()[0] : nullptr; - return SpawnPrefab(prefab, Transform::Identity, parent, nullptr); + return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, nullptr); } Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Vector3& position) @@ -73,12 +73,12 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, const Transform Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent) { - return SpawnPrefab(prefab, Transform::Identity, parent, nullptr); + return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, nullptr); } Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization) { - return SpawnPrefab(prefab, Transform::Identity, parent, objectsCache, withSynchronization); + return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, objectsCache, withSynchronization); } Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization) @@ -191,7 +191,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac parent->Children.Add(root); // Move root to the right location - if (transform != Transform::Identity) + if (transform.Translation != Vector3::Minimum) root->SetTransform(transform); // Link actors hierarchy diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index 7f62d943b..2936da3da 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -14,6 +14,10 @@ #include "Engine/Serialization/JsonWriters.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Threading/ThreadLocal.h" +#if !BUILD_RELEASE || USE_EDITOR +#include "Engine/Level/Level.h" +#include "Engine/Threading/Threading.h" +#endif SceneObjectsFactory::Context::Context(ISerializeModifier* modifier) : Modifier(modifier) @@ -254,6 +258,10 @@ void SceneObjectsFactory::Deserialize(Context& context, SceneObject* obj, ISeria void SceneObjectsFactory::HandleObjectDeserializationError(const ISerializable::DeserializeStream& value) { +#if !BUILD_RELEASE || USE_EDITOR + // Prevent race-conditions when logging missing objects (especially when adding dummy MissingScript) + ScopeLock lock(Level::ScenesLock); + // Print invalid object data contents rapidjson_flax::StringBuffer buffer; PrettyJsonWriter writer(buffer); @@ -272,14 +280,15 @@ void SceneObjectsFactory::HandleObjectDeserializationError(const ISerializable:: #if USE_EDITOR // Add dummy script auto* dummyScript = parent->AddScript(); - const auto parentIdMember = value.FindMember("TypeName"); - if (parentIdMember != value.MemberEnd() && parentIdMember->value.IsString()) - dummyScript->MissingTypeName = parentIdMember->value.GetString(); + const auto typeNameMember = value.FindMember("TypeName"); + if (typeNameMember != value.MemberEnd() && typeNameMember->value.IsString()) + dummyScript->MissingTypeName = typeNameMember->value.GetString(); dummyScript->Data = MoveTemp(bufferStr); #endif LOG(Warning, "Parent actor of the missing object: {0}", parent->GetName()); } } +#endif } Actor* SceneObjectsFactory::CreateActor(int32 typeId, const Guid& id) @@ -361,14 +370,14 @@ SceneObjectsFactory::PrefabSyncData::PrefabSyncData(Array& sceneOb { } -void SceneObjectsFactory::SetupPrefabInstances(Context& context, PrefabSyncData& data) +void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyncData& data) { PROFILE_CPU_NAMED("SetupPrefabInstances"); const int32 count = data.Data.Size(); ASSERT(count <= data.SceneObjects.Count()); for (int32 i = 0; i < count; i++) { - SceneObject* obj = data.SceneObjects[i]; + const SceneObject* obj = data.SceneObjects[i]; if (!obj) continue; const auto& stream = data.Data[i]; @@ -409,6 +418,21 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, PrefabSyncData& // Add to the prefab instance IDs mapping auto& prefabInstance = context.Instances[index]; prefabInstance.IdsMapping[prefabObjectId] = id; + + // Walk over nested prefabs to link any subobjects into this object (eg. if nested prefab uses cross-object references to link them correctly) + NESTED_PREFAB_WALK: + const ISerializable::DeserializeStream* prefabData; + if (prefab->ObjectsDataCache.TryGet(prefabObjectId, prefabData) && JsonTools::GetGuidIfValid(prefabObjectId, *prefabData, "PrefabObjectID")) + { + prefabId = JsonTools::GetGuid(stream, "PrefabID"); + prefab = Content::LoadAsync(prefabId); + if (prefab && !prefab->WaitForLoaded()) + { + // Map prefab object ID to the deserialized instance ID + prefabInstance.IdsMapping[prefabObjectId] = id; + goto NESTED_PREFAB_WALK; + } + } } } diff --git a/Source/Engine/Level/SceneObjectsFactory.h b/Source/Engine/Level/SceneObjectsFactory.h index f7f495449..97cf6b9ce 100644 --- a/Source/Engine/Level/SceneObjectsFactory.h +++ b/Source/Engine/Level/SceneObjectsFactory.h @@ -100,7 +100,7 @@ public: /// /// The serialization context. /// The sync data. - static void SetupPrefabInstances(Context& context, PrefabSyncData& data); + static void SetupPrefabInstances(Context& context, const PrefabSyncData& data); /// /// Synchronizes the new prefab instances by spawning missing objects that were added to prefab but were not saved with scene objects collection. diff --git a/Source/Engine/Localization/CultureInfo.cpp b/Source/Engine/Localization/CultureInfo.cpp index 3e02ccf6e..fc678d419 100644 --- a/Source/Engine/Localization/CultureInfo.cpp +++ b/Source/Engine/Localization/CultureInfo.cpp @@ -30,6 +30,20 @@ typedef struct } MonoCultureInfo; #endif +namespace +{ + const CultureInfoEntry* FindEntry(const StringAnsiView& name) + { + for (int32 i = 0; i < NUM_CULTURE_ENTRIES; i++) + { + const CultureInfoEntry& e = culture_entries[i]; + if (name == idx2string(e.name)) + return &e; + } + return nullptr; + } +}; + CultureInfo::CultureInfo(int32 lcid) { _lcid = lcid; @@ -80,23 +94,24 @@ CultureInfo::CultureInfo(const StringAnsiView& name) _englishName = TEXT("Invariant Culture"); return; } - for (int32 i = 0; i < NUM_CULTURE_ENTRIES; i++) + const CultureInfoEntry* e = FindEntry(name); + if (!e && name.Find('-') != -1) { - auto& e = culture_entries[i]; - if (name == idx2string(e.name)) - { - _data = (void*)&e; - _lcid = (int32)e.lcid; - _lcidParent = (int32)e.parent_lcid; - _name.SetUTF8(name.Get(), name.Length()); - const char* nativename = idx2string(e.nativename); - _nativeName.SetUTF8(nativename, StringUtils::Length(nativename)); - const char* englishname = idx2string(e.englishname); - _englishName.SetUTF8(englishname, StringUtils::Length(englishname)); - break; - } + e = FindEntry(name.Substring(0, name.Find('-'))); } - if (!_data) + if (e) + { + _data = (void*)e; + _lcid = (int32)e->lcid; + _lcidParent = (int32)e->parent_lcid; + const char* ename = idx2string(e->name); + _name.SetUTF8(ename, StringUtils::Length(ename)); + const char* nativename = idx2string(e->nativename); + _nativeName.SetUTF8(nativename, StringUtils::Length(nativename)); + const char* englishname = idx2string(e->englishname); + _englishName.SetUTF8(englishname, StringUtils::Length(englishname)); + } + else { _lcid = 127; _lcidParent = 0; diff --git a/Source/Engine/Navigation/NavCrowd.cpp b/Source/Engine/Navigation/NavCrowd.cpp index 80055c3a4..fe1c2f728 100644 --- a/Source/Engine/Navigation/NavCrowd.cpp +++ b/Source/Engine/Navigation/NavCrowd.cpp @@ -3,7 +3,11 @@ #include "NavCrowd.h" #include "NavMesh.h" #include "NavMeshRuntime.h" +#include "Engine/Core/Log.h" +#include "Engine/Level/Level.h" +#include "Engine/Level/Scene/Scene.h" #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Threading/Threading.h" #include NavCrowd::NavCrowd(const SpawnParams& params) @@ -26,6 +30,15 @@ bool NavCrowd::Init(float maxAgentRadius, int32 maxAgents, NavMesh* navMesh) bool NavCrowd::Init(const NavAgentProperties& agentProperties, int32 maxAgents) { NavMeshRuntime* navMeshRuntime = NavMeshRuntime::Get(agentProperties); +#if !BUILD_RELEASE + if (!navMeshRuntime) + { + if (NavMeshRuntime::Get()) + LOG(Error, "Cannot create crowd. Failed to find a navmesh that matches a given agent properties."); + else + LOG(Error, "Cannot create crowd. No navmesh is loaded."); + } +#endif return Init(agentProperties.Radius * 3.0f, maxAgents, navMeshRuntime); } @@ -33,6 +46,41 @@ bool NavCrowd::Init(float maxAgentRadius, int32 maxAgents, NavMeshRuntime* navMe { if (!_crowd || !navMesh) return true; + + // This can happen on game start when no navmesh is loaded yet (eg. navmesh tiles data is during streaming) so wait for navmesh + if (navMesh->GetNavMesh() == nullptr) + { + PROFILE_CPU_NAMED("WaitForNavMesh"); + if (IsInMainThread()) + { + // Wait for any navmesh data load + for (const Scene* scene : Level::Scenes) + { + for (const NavMesh* actor : scene->Navigation.Meshes) + { + if (actor->DataAsset) + { + actor->DataAsset->WaitForLoaded(); + if (navMesh->GetNavMesh()) + break; + } + } + if (navMesh->GetNavMesh()) + break; + } + } + else + { + while (navMesh->GetNavMesh() == nullptr) + Platform::Sleep(1); + } + if (navMesh->GetNavMesh() == nullptr) + { + LOG(Error, "Cannot create crowd. Navmesh is not yet laoded."); + return true; + } + } + return !_crowd->init(maxAgents, maxAgentRadius, navMesh->GetNavMesh()); } @@ -48,7 +96,7 @@ Vector3 NavCrowd::GetAgentPosition(int32 id) const { Vector3 result = Vector3::Zero; const dtCrowdAgent* agent = _crowd->getAgent(id); - if (agent && agent->state != DT_CROWDAGENT_STATE_INVALID) + if (agent) { result = Float3(agent->npos); } @@ -59,7 +107,7 @@ Vector3 NavCrowd::GetAgentVelocity(int32 id) const { Vector3 result = Vector3::Zero; const dtCrowdAgent* agent = _crowd->getAgent(id); - if (agent && agent->state != DT_CROWDAGENT_STATE_INVALID) + if (agent) { result = Float3(agent->vel); } diff --git a/Source/Engine/Navigation/Navigation.cpp b/Source/Engine/Navigation/Navigation.cpp index 8bc94eaaa..73e11e630 100644 --- a/Source/Engine/Navigation/Navigation.cpp +++ b/Source/Engine/Navigation/Navigation.cpp @@ -9,6 +9,8 @@ #include "Engine/Content/JsonAsset.h" #include "Engine/Threading/Threading.h" #if USE_EDITOR +#include "Editor/Editor.h" +#include "Editor/Managed/ManagedEditor.h" #include "Engine/Level/Level.h" #include "Engine/Level/Scene/Scene.h" #endif @@ -220,6 +222,17 @@ void NavigationSettings::Apply() #endif } } + +#if USE_EDITOR + if (!Editor::IsPlayMode && Editor::Managed && Editor::Managed->CanAutoBuildNavMesh()) + { + // Rebuild all navmeshs after apply changes on navigation + for (auto scene : Level::Scenes) + { + Navigation::BuildNavMesh(scene); + } + } +#endif } void NavigationSettings::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) diff --git a/Source/Engine/Networking/Components/NetworkTransform.cpp b/Source/Engine/Networking/Components/NetworkTransform.cpp index af802649b..6f5943ff1 100644 --- a/Source/Engine/Networking/Components/NetworkTransform.cpp +++ b/Source/Engine/Networking/Components/NetworkTransform.cpp @@ -301,11 +301,8 @@ void NetworkTransform::Deserialize(NetworkStream* stream) _buffer.Clear(); _bufferHasDeltas = true; } - // TODO: items are added in order to do batch removal - for (int32 i = 0; i < _buffer.Count() && _buffer[i].SequenceIndex < sequenceIndex; i++) - { - _buffer.RemoveAtKeepOrder(i); - } + while (_buffer.Count() != 0 && _buffer[0].SequenceIndex < sequenceIndex) + _buffer.RemoveAtKeepOrder(0); // Use received authoritative actor transformation but re-apply all deltas not yet processed by the server due to lag (reconciliation) for (auto& e : _buffer) diff --git a/Source/Engine/Online/Online.cpp b/Source/Engine/Online/Online.cpp index 313497f98..d8f85baea 100644 --- a/Source/Engine/Online/Online.cpp +++ b/Source/Engine/Online/Online.cpp @@ -4,13 +4,16 @@ #include "IOnlinePlatform.h" #include "Engine/Core/Log.h" #include "Engine/Engine/EngineService.h" +#if USE_EDITOR +#include "Engine/Scripting/Scripting.h" +#endif #include "Engine/Scripting/ScriptingObject.h" class OnlineService : public EngineService { public: OnlineService() - : EngineService(TEXT("Online"), 100) + : EngineService(TEXT("Online"), 500) { } @@ -25,6 +28,16 @@ IOnlinePlatform* Online::Platform = nullptr; Action Online::PlatformChanged; OnlineService OnlineServiceInstance; +#if USE_EDITOR + +void OnOnlineScriptsReloading() +{ + // Dispose any active platform + Online::Initialize(nullptr); +} + +#endif + bool Online::Initialize(IOnlinePlatform* platform) { if (Platform == platform) @@ -34,6 +47,9 @@ bool Online::Initialize(IOnlinePlatform* platform) if (Platform) { +#if USE_EDITOR + Scripting::ScriptsReloading.Unbind(OnOnlineScriptsReloading); +#endif Platform->Deinitialize(); } Platform = platform; @@ -45,6 +61,9 @@ bool Online::Initialize(IOnlinePlatform* platform) LOG(Error, "Failed to initialize online platform."); return true; } +#if USE_EDITOR + Scripting::ScriptsReloading.Bind(OnOnlineScriptsReloading); +#endif } return false; diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp index 9d91d0013..cb2d7004e 100644 --- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp +++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.Particles.cpp @@ -7,7 +7,7 @@ #include "Engine/Graphics/RenderTask.h" #define GET_VIEW() auto mainViewTask = MainRenderTask::Instance && MainRenderTask::Instance->LastUsedFrame != 0 ? MainRenderTask::Instance : nullptr -#define ACCESS_PARTICLE_ATTRIBUTE(index) (context.Data->Buffer->GetParticleCPU(context.ParticleIndex) + context.Data->Buffer->Layout->Attributes[node->Attributes[index]].Offset) +#define ACCESS_PARTICLE_ATTRIBUTE(index) (context.Data->Buffer->GetParticleCPU(context.ParticleIndex) + context.Data->Buffer->Layout->Attributes[context.AttributesRemappingTable[node->Attributes[index]]].Offset) #define GET_PARTICLE_ATTRIBUTE(index, type) *(type*)ACCESS_PARTICLE_ATTRIBUTE(index) void ParticleEmitterGraphCPUExecutor::ProcessGroupParameters(Box* box, Node* node, Value& value) @@ -436,9 +436,19 @@ void ParticleEmitterGraphCPUExecutor::ProcessGroupParticles(Box* box, Node* node auto* functionOutputNode = &graph->Nodes[function->Outputs[outputIndex]]; Box* functionOutputBox = functionOutputNode->TryGetBox(0); + // Setup particle attributes remapping (so particle data access nodes inside the function will read data at proper offset, see macro ACCESS_PARTICLE_ATTRIBUTE) + byte attributesRemappingTable[PARTICLE_ATTRIBUTES_MAX_COUNT]; + Platform::MemoryCopy(attributesRemappingTable, context.AttributesRemappingTable, sizeof(attributesRemappingTable)); + for (int32 i = 0; i < graph->Layout.Attributes.Count(); i++) + { + const ParticleAttribute& e = graph->Layout.Attributes[i]; + context.AttributesRemappingTable[i] = context.Data->Buffer->Layout->FindAttribute(e.Name, e.ValueType); + } + // Evaluate the function output context.GraphStack.Push(graph); value = functionOutputBox && functionOutputBox->HasConnection() ? eatBox(nodeBase, functionOutputBox->FirstConnection()) : Value::Zero; + Platform::MemoryCopy(context.AttributesRemappingTable, attributesRemappingTable, sizeof(attributesRemappingTable)); context.GraphStack.Pop(); break; } diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp index d86bc6d54..a8f7898e1 100644 --- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp +++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp @@ -133,6 +133,8 @@ void ParticleEmitterGraphCPUExecutor::Init(ParticleEmitter* emitter, ParticleEff context.ViewTask = effect->GetRenderTask(); context.CallStackSize = 0; context.Functions.Clear(); + for (int32 i = 0; i < PARTICLE_ATTRIBUTES_MAX_COUNT; i++) + context.AttributesRemappingTable[i] = i; } bool ParticleEmitterGraphCPUExecutor::ComputeBounds(ParticleEmitter* emitter, ParticleEffect* effect, ParticleEmitterInstance& data, BoundingBox& result) diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h index 9d1d48c81..4f55da6e2 100644 --- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h +++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.h @@ -119,6 +119,7 @@ struct ParticleEmitterGraphCPUContext class SceneRenderTask* ViewTask; Array> GraphStack; Dictionary Functions; + byte AttributesRemappingTable[PARTICLE_ATTRIBUTES_MAX_COUNT]; // Maps node attribute indices to the current particle layout (used to support accessing particle data from function graph which has different layout). int32 CallStackSize = 0; VisjectExecutor::Node* CallStack[PARTICLE_EMITTER_MAX_CALL_STACK]; }; diff --git a/Source/Engine/Particles/Graph/GPU/GPUParticles.cpp b/Source/Engine/Particles/Graph/GPU/GPUParticles.cpp index 32f6b1e23..57ffa251a 100644 --- a/Source/Engine/Particles/Graph/GPU/GPUParticles.cpp +++ b/Source/Engine/Particles/Graph/GPU/GPUParticles.cpp @@ -6,6 +6,7 @@ #include "Engine/Particles/ParticleEmitter.h" #include "Engine/Particles/ParticleEffect.h" #include "Engine/Graphics/RenderTask.h" +#include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/GPUDevice.h" #include "Engine/Graphics/GPUBuffer.h" #include "Engine/Graphics/GPUContext.h" @@ -168,7 +169,7 @@ void GPUParticles::Execute(GPUContext* context, ParticleEmitter* emitter, Partic if (viewTask) { bindMeta.Buffers = viewTask->Buffers; - bindMeta.CanSampleDepth = bindMeta.CanSampleGBuffer = true; + bindMeta.CanSampleDepth = bindMeta.CanSampleGBuffer = bindMeta.Buffers && bindMeta.Buffers->GetWidth() != 0; } else { @@ -179,7 +180,7 @@ void GPUParticles::Execute(GPUContext* context, ParticleEmitter* emitter, Partic for (int32 i = 0; i < data.Parameters.Count(); i++) { // Copy instance parameters values - _params[i].SetValue(data.Parameters[i]); + _params[i].SetValue(data.Parameters.Get()[i]); } MaterialParamsLink link; link.This = &_params; diff --git a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp index de142827c..22512481b 100644 --- a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp +++ b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp @@ -40,7 +40,7 @@ namespace ParticleEmitterGPUGenerator::Value ParticleEmitterGPUGenerator::AccessParticleAttribute(Node* caller, const StringView& name, ParticleAttribute::ValueTypes valueType, AccessMode mode) { // Find this attribute - return AccessParticleAttribute(caller, ((ParticleEmitterGraphGPU*)_graphStack.Peek())->Layout.FindAttribute(name, valueType), mode); + return AccessParticleAttribute(caller, GetRootGraph()->Layout.FindAttribute(name, valueType), mode); } ParticleEmitterGPUGenerator::Value ParticleEmitterGPUGenerator::AccessParticleAttribute(Node* caller, int32 index, AccessMode mode) @@ -55,7 +55,7 @@ ParticleEmitterGPUGenerator::Value ParticleEmitterGPUGenerator::AccessParticleAt if (value.Variable.Type != VariantType::Null) return value.Variable; - auto& attribute = ((ParticleEmitterGraphGPU*)_graphStack.Peek())->Layout.Attributes[index]; + auto& attribute = GetRootGraph()->Layout.Attributes[index]; const VariantType::Types type = GetValueType(attribute.ValueType); // Generate local variable name that matches the attribute name for easier shader source debugging @@ -77,7 +77,7 @@ ParticleEmitterGPUGenerator::Value ParticleEmitterGPUGenerator::AccessParticleAt else if (_contextType == ParticleContextType::Initialize) { // Initialize with default value - const Value defaultValue(((ParticleEmitterGraphGPU*)_graphStack.Peek())->AttributesDefaults[index]); + const Value defaultValue(GetRootGraph()->AttributesDefaults[index]); value.Variable = writeLocal(type, defaultValue.Value, caller, localName); } else @@ -251,10 +251,10 @@ void ParticleEmitterGPUGenerator::ProcessGroupParticles(Box* box, Node* node, Va { const Char* format; auto valueType = static_cast(node->Values[1].AsInt); - const int32 attributeIndex = ((ParticleEmitterGraphGPU*)_graphStack.Peek())->Layout.FindAttribute((StringView)node->Values[0], valueType); + const int32 attributeIndex = GetRootGraph()->Layout.FindAttribute((StringView)node->Values[0], valueType); if (attributeIndex == -1) return; - auto& attribute = ((ParticleEmitterGraphGPU*)_graphStack.Peek())->Layout.Attributes[attributeIndex]; + auto& attribute = GetRootGraph()->Layout.Attributes[attributeIndex]; const auto particleIndex = Value::Cast(tryGetValue(node->GetBox(1), Value(VariantType::Uint, TEXT("context.ParticleIndex"))), VariantType::Uint); switch (valueType) { diff --git a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.cpp b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.cpp index 513252e80..024823293 100644 --- a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.cpp +++ b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.cpp @@ -30,10 +30,10 @@ void ParticleEmitterGraphGPU::ClearCache() { for (int32 i = 0; i < Nodes.Count(); i++) { - auto& node = Nodes[i]; + ParticleEmitterGraphGPUNode& node = Nodes.Get()[i]; for (int32 j = 0; j < node.Boxes.Count(); j++) { - node.Boxes[j].Cache.Clear(); + node.Boxes.Get()[j].Cache.Clear(); } } } @@ -86,9 +86,7 @@ bool ParticleEmitterGPUGenerator::Generate(WriteStream& source, BytesContainer& auto& layout = baseGraph->Layout; _attributeValues.Resize(layout.Attributes.Count()); for (int32 i = 0; i < _attributeValues.Count(); i++) - { _attributeValues[i] = AttributeCache(); - } _contextUsesKill = false; // Cache attributes @@ -112,7 +110,7 @@ bool ParticleEmitterGPUGenerator::Generate(WriteStream& source, BytesContainer& for (int32 i = 0; i < baseGraph->InitModules.Count(); i++) { - ProcessModule(baseGraph->InitModules[i]); + ProcessModule(baseGraph->InitModules.Get()[i]); } WriteParticleAttributesWrites(); @@ -135,7 +133,7 @@ bool ParticleEmitterGPUGenerator::Generate(WriteStream& source, BytesContainer& for (int32 i = 0; i < baseGraph->UpdateModules.Count(); i++) { - ProcessModule(baseGraph->UpdateModules[i]); + ProcessModule(baseGraph->UpdateModules.Get()[i]); } // Dead particles removal @@ -321,22 +319,22 @@ void ParticleEmitterGPUGenerator::clearCache() // Reset cached boxes values for (int32 i = 0; i < _graphs.Count(); i++) { - _graphs[i]->ClearCache(); + _graphs.Get()[i]->ClearCache(); } - for (auto& e : _functions) + for (const auto& e : _functions) { - for (auto& node : e.Value->Nodes) + auto& nodes = ((ParticleEmitterGraphGPU*)e.Value)->Nodes; + for (int32 i = 0; i < nodes.Count(); i++) { + ParticleEmitterGraphGPUNode& node = nodes.Get()[i]; for (int32 j = 0; j < node.Boxes.Count(); j++) - node.Boxes[j].Cache.Clear(); + node.Boxes.Get()[j].Cache.Clear(); } } // Reset cached attributes for (int32 i = 0; i < _attributeValues.Count(); i++) - { - _attributeValues[i] = AttributeCache(); - } + _attributeValues.Get()[i] = AttributeCache(); _contextUsesKill = false; } @@ -344,10 +342,11 @@ void ParticleEmitterGPUGenerator::clearCache() void ParticleEmitterGPUGenerator::WriteParticleAttributesWrites() { bool hadAnyWrite = false; + ParticleEmitterGraphGPU* graph = GetRootGraph(); for (int32 i = 0; i < _attributeValues.Count(); i++) { auto& value = _attributeValues[i]; - auto& attribute = ((ParticleEmitterGraphGPU*)_graphStack.Peek())->Layout.Attributes[i]; + auto& attribute = graph->Layout.Attributes[i]; // Skip not used attributes or read-only attributes if (value.Variable.Type == VariantType::Null || ((int)value.Access & (int)AccessMode::Write) == 0) diff --git a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h index 82f9c2c93..967dc0e0c 100644 --- a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h +++ b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h @@ -5,7 +5,7 @@ /// /// Current GPU particles emitter shader version. /// -#define PARTICLE_GPU_GRAPH_VERSION 9 +#define PARTICLE_GPU_GRAPH_VERSION 10 #if COMPILE_WITH_PARTICLE_GPU_GRAPH @@ -94,7 +94,6 @@ public: /// /// Gets the root graph. /// - /// The base graph. FORCE_INLINE ParticleEmitterGraphGPU* GetRootGraph() const { return _graphs.First(); @@ -154,12 +153,12 @@ private: bool IsLocalSimulationSpace() const { - return ((ParticleEmitterGraphGPU*)_graphStack.Peek())->SimulationSpace == ParticlesSimulationSpace::Local; + return GetRootGraph()->SimulationSpace == ParticlesSimulationSpace::Local; } bool IsWorldSimulationSpace() const { - return ((ParticleEmitterGraphGPU*)_graphStack.Peek())->SimulationSpace == ParticlesSimulationSpace::World; + return GetRootGraph()->SimulationSpace == ParticlesSimulationSpace::World; } }; diff --git a/Source/Engine/Particles/Graph/ParticleEmitterGraph.h b/Source/Engine/Particles/Graph/ParticleEmitterGraph.h index f5946c226..91dae0ed2 100644 --- a/Source/Engine/Particles/Graph/ParticleEmitterGraph.h +++ b/Source/Engine/Particles/Graph/ParticleEmitterGraph.h @@ -37,12 +37,12 @@ public: /// /// Flag valid for used particle nodes that need per-particle data to evaluate its value (including dependant nodes linked to input boxes). Used to skip per-particle graph evaluation if graph uses the same value for all particles (eg. is not using per-particle seed or position node). /// - bool UsesParticleData; + bool UsesParticleData = false; /// /// Flag valid for used particle nodes that result in constant data (nothing random nor particle data). /// - bool IsConstant; + bool IsConstant = true; /// /// The cached particle attribute indices used by the simulation graph to access particle properties. @@ -50,6 +50,8 @@ public: int32 Attributes[PARTICLE_EMITTER_MAX_ATTRIBUTES_REFS_PER_NODE]; }; +void InitParticleEmitterFunctionCall(const Guid& assetId, AssetReference& asset, bool& usesParticleData, ParticleLayout& layout); + /// /// The Particle Emitter Graph used to simulate particles. /// @@ -157,8 +159,6 @@ public: if (node->Used) return; node->Used = true; - node->UsesParticleData = false; - node->IsConstant = true; #define USE_ATTRIBUTE(name, valueType, slot) \ { \ @@ -292,14 +292,11 @@ public: case GRAPH_NODE_MAKE_TYPE(14, 214): case GRAPH_NODE_MAKE_TYPE(14, 215): case GRAPH_NODE_MAKE_TYPE(14, 216): - { node->IsConstant = false; break; - } // Particle Emitter Function case GRAPH_NODE_MAKE_TYPE(14, 300): - node->Assets[0] = Content::LoadAsync((Guid)node->Values[0]); - node->UsesParticleData = true; // TODO: analyze emitter function graph to detect if it's actually using any particle data at all (even from inputs after inline) + InitParticleEmitterFunctionCall((Guid)node->Values[0], node->Assets[0], node->UsesParticleData, Layout); break; // Particle Index case GRAPH_NODE_MAKE_TYPE(14, 301): @@ -564,7 +561,7 @@ public: return true; // Compute particle data layout and initialize used nodes (for only used nodes, start depth searching rom the modules) - Layout.AddAttribute(TEXT("Position"), ParticleAttribute::ValueTypes::Float3); + //Layout.AddAttribute(TEXT("Position"), ParticleAttribute::ValueTypes::Float3); #define PROCESS_MODULES(modules) for (int32 i = 0; i < modules.Count(); i++) { modules[i]->Used = false; InitializeNode(modules[i]); } PROCESS_MODULES(SpawnModules); PROCESS_MODULES(InitModules); diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp index 788a4263f..e461f332e 100644 --- a/Source/Engine/Particles/ParticleEffect.cpp +++ b/Source/Engine/Particles/ParticleEffect.cpp @@ -284,6 +284,7 @@ void ParticleEffect::UpdateSimulation(bool singleFrame) void ParticleEffect::Play() { _isPlaying = true; + _isStopped = false; } void ParticleEffect::Pause() @@ -293,6 +294,7 @@ void ParticleEffect::Pause() void ParticleEffect::Stop() { + _isStopped = true; _isPlaying = false; ResetSimulation(); } @@ -448,7 +450,7 @@ void ParticleEffect::Update() void ParticleEffect::UpdateExecuteInEditor() { // Auto-play in Editor - if (!Editor::IsPlayMode) + if (!Editor::IsPlayMode && !_isStopped) { _isPlaying = true; Update(); diff --git a/Source/Engine/Particles/ParticleEffect.h b/Source/Engine/Particles/ParticleEffect.h index 9e9792a4c..d3f0cc9d0 100644 --- a/Source/Engine/Particles/ParticleEffect.h +++ b/Source/Engine/Particles/ParticleEffect.h @@ -133,7 +133,7 @@ public: /// /// The particle system instance that plays the particles simulation in the game. /// -API_CLASS(Attributes="ActorContextMenu(\"New/Visuals/Particle Effects\"), ActorToolbox(\"Visuals\")") +API_CLASS(Attributes="ActorContextMenu(\"New/Visuals/Particle Effect\"), ActorToolbox(\"Visuals\")") class FLAXENGINE_API ParticleEffect : public Actor { DECLARE_SCENE_OBJECT(ParticleEffect); @@ -185,6 +185,7 @@ private: Array _parameters; // Cached for scripting API Array _parametersOverrides; // Cached parameter modifications to be applied to the parameters bool _isPlaying = false; + bool _isStopped = false; public: /// diff --git a/Source/Engine/Particles/ParticleEmitter.cpp b/Source/Engine/Particles/ParticleEmitter.cpp index 20e4c0490..ea1161c6b 100644 --- a/Source/Engine/Particles/ParticleEmitter.cpp +++ b/Source/Engine/Particles/ParticleEmitter.cpp @@ -19,6 +19,7 @@ #include "Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.h" #if BUILD_DEBUG #include "Engine/Engine/Globals.h" +#include "Engine/Scripting/BinaryModule.h" #endif #endif #if COMPILE_WITH_GPU_PARTICLES @@ -185,7 +186,9 @@ Asset::LoadResult ParticleEmitter::load() #if BUILD_DEBUG && USE_EDITOR // Dump generated shader source to the temporary file + BinaryModule::Locker.Lock(); source.SaveToFile(Globals::ProjectCacheFolder / TEXT("particle_emitter.txt")); + BinaryModule::Locker.Unlock(); #endif // Encrypt source code diff --git a/Source/Engine/Particles/ParticleEmitterFunction.cpp b/Source/Engine/Particles/ParticleEmitterFunction.cpp index c91bc0d82..5abb4a434 100644 --- a/Source/Engine/Particles/ParticleEmitterFunction.cpp +++ b/Source/Engine/Particles/ParticleEmitterFunction.cpp @@ -9,6 +9,27 @@ #endif #include "Engine/Content/Factories/BinaryAssetFactory.h" +void InitParticleEmitterFunctionCall(const Guid& assetId, AssetReference& asset, bool& usesParticleData, ParticleLayout& layout) +{ + const auto function = Content::Load(assetId); + asset = function; + if (function) + { + // Insert any used particle data into the calling graph + for (const ParticleAttribute& e : function->Graph.Layout.Attributes) + { + if (layout.FindAttribute(e.Name, e.ValueType) == -1) + layout.AddAttribute(e.Name, e.ValueType); + } + + // Detect if function needs to be evaluated per-particle + for (int32 i = 0; i < function->Outputs.Count() && !usesParticleData; i++) + { + usesParticleData = function->Graph.Nodes[function->Outputs.Get()[i]].UsesParticleData; + } + } +} + REGISTER_BINARY_ASSET(ParticleEmitterFunction, "FlaxEngine.ParticleEmitterFunction", false); ParticleEmitterFunction::ParticleEmitterFunction(const SpawnParams& params, const AssetInfo* info) diff --git a/Source/Engine/Particles/Particles.cpp b/Source/Engine/Particles/Particles.cpp index 5850b6736..784f28c98 100644 --- a/Source/Engine/Particles/Particles.cpp +++ b/Source/Engine/Particles/Particles.cpp @@ -1318,6 +1318,8 @@ void ParticlesSystem::Job(int32 index) emitterInstance.Buffer = nullptr; } } + // Stop playing effect. + effect->Stop(); return; } } @@ -1332,7 +1334,8 @@ void ParticlesSystem::Job(int32 index) auto emitter = particleSystem->Emitters[track.AsEmitter.Index].Get(); auto& data = instance.Emitters[track.AsEmitter.Index]; ASSERT(emitter && emitter->IsLoaded()); - ASSERT(emitter->Capacity != 0 && emitter->Graph.Layout.Size != 0); + if (emitter->Capacity == 0 || emitter->Graph.Layout.Size == 0) + continue; PROFILE_CPU_ASSET(emitter); // Calculate new time position diff --git a/Source/Engine/Physics/Actors/Cloth.cpp b/Source/Engine/Physics/Actors/Cloth.cpp index 7c180d98d..b2db80b0b 100644 --- a/Source/Engine/Physics/Actors/Cloth.cpp +++ b/Source/Engine/Physics/Actors/Cloth.cpp @@ -95,14 +95,17 @@ void Cloth::SetFabric(const FabricSettings& value) void Cloth::Rebuild() { #if WITH_CLOTH - // Remove old - if (IsDuringPlay()) - PhysicsBackend::RemoveCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); - DestroyCloth(); + if (_cloth) + { + // Remove old + if (IsDuringPlay()) + PhysicsBackend::RemoveCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); + DestroyCloth(); + } // Create new CreateCloth(); - if (IsDuringPlay()) + if (IsDuringPlay() && _cloth) PhysicsBackend::AddCloth(GetPhysicsScene()->GetPhysicsScene(), _cloth); #endif } diff --git a/Source/Engine/Physics/Actors/Cloth.h b/Source/Engine/Physics/Actors/Cloth.h index 4dc81ce3f..6137ec3b6 100644 --- a/Source/Engine/Physics/Actors/Cloth.h +++ b/Source/Engine/Physics/Actors/Cloth.h @@ -232,13 +232,13 @@ private: public: /// - /// Gets the mesh to use for the cloth simulation (single mesh from specific LOD). + /// Gets the mesh to use for the cloth simulation (single mesh from specific LOD). Always from the parent static or animated model actor. /// API_PROPERTY(Attributes="EditorOrder(0), EditorDisplay(\"Cloth\")") ModelInstanceActor::MeshReference GetMesh() const; /// - /// Sets the mesh to use for the cloth simulation (single mesh from specific LOD). + /// Sets the mesh to use for the cloth simulation (single mesh from specific LOD). Always from the parent static or animated model actor. /// API_PROPERTY() void SetMesh(const ModelInstanceActor::MeshReference& value); diff --git a/Source/Engine/Physics/Actors/PhysicsColliderActor.cpp b/Source/Engine/Physics/Actors/PhysicsColliderActor.cpp index 02645943c..f2ab5d8a2 100644 --- a/Source/Engine/Physics/Actors/PhysicsColliderActor.cpp +++ b/Source/Engine/Physics/Actors/PhysicsColliderActor.cpp @@ -1,7 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #include "PhysicsColliderActor.h" -#include "Engine/Scripting/Script.h" #include "RigidBody.h" PhysicsColliderActor::PhysicsColliderActor(const SpawnParams& params) diff --git a/Source/Engine/Physics/Actors/RigidBody.h b/Source/Engine/Physics/Actors/RigidBody.h index e203e70fc..e7b929483 100644 --- a/Source/Engine/Physics/Actors/RigidBody.h +++ b/Source/Engine/Physics/Actors/RigidBody.h @@ -41,19 +41,8 @@ protected: public: /// - /// Enables kinematic mode for the rigidbody. + /// Enables kinematic mode for the rigidbody. Kinematic rigidbodies are special dynamic actors that are not influenced by forces(such as gravity), and have no momentum. They are considered to have infinite mass and can push regular dynamic actors out of the way. Kinematics will not collide with static or other kinematic objects but are great for moving platforms or characters, where direct motion control is desired. /// - /// - /// Kinematic rigidbodies are special dynamic actors that are not influenced by forces(such as gravity), and have no momentum. - /// They are considered to have infinite mass and can push regular dynamic actors out of the way. - /// Kinematics will not collide with static or other kinematic objects. - /// - /// Kinematic rigidbodies are great for moving platforms or characters, where direct motion control is desired. - /// - /// - /// Kinematic rigidbodies are incompatible with CCD. - /// - /// API_PROPERTY(Attributes="EditorOrder(10), DefaultValue(false), EditorDisplay(\"Rigid Body\")") FORCE_INLINE bool GetIsKinematic() const { @@ -61,26 +50,13 @@ public: } /// - /// Enables kinematic mode for the rigidbody. + /// Enables kinematic mode for the rigidbody. Kinematic rigidbodies are special dynamic actors that are not influenced by forces(such as gravity), and have no momentum. They are considered to have infinite mass and can push regular dynamic actors out of the way. Kinematics will not collide with static or other kinematic objects but are great for moving platforms or characters, where direct motion control is desired. /// - /// - /// Kinematic rigidbodies are special dynamic actors that are not influenced by forces(such as gravity), and have no momentum. - /// They are considered to have infinite mass and can push regular dynamic actors out of the way. - /// Kinematics will not collide with static or other kinematic objects. - /// - /// Kinematic rigidbodies are great for moving platforms or characters, where direct motion control is desired. - /// - /// - /// Kinematic rigidbodies are incompatible with CCD. - /// - /// - /// The value. API_PROPERTY() void SetIsKinematic(const bool value); /// - /// Gets the 'drag' force added to reduce linear movement. + /// Gets the 'drag' force added to reduce linear movement. Linear damping can be used to slow down an object. The higher the drag the more the object slows down. /// - /// Linear damping can be used to slow down an object. The higher the drag the more the object slows down. API_PROPERTY(Attributes="EditorOrder(60), DefaultValue(0.01f), Limit(0), EditorDisplay(\"Rigid Body\")") FORCE_INLINE float GetLinearDamping() const { @@ -88,16 +64,13 @@ public: } /// - /// Sets the 'drag' force added to reduce linear movement. + /// Sets the 'drag' force added to reduce linear movement. Linear damping can be used to slow down an object. The higher the drag the more the object slows down. /// - /// Linear damping can be used to slow down an object. The higher the drag the more the object slows down. - /// The value. API_PROPERTY() void SetLinearDamping(float value); /// - /// Gets the 'drag' force added to reduce angular movement. + /// Gets the 'drag' force added to reduce angular movement. Angular damping can be used to slow down the rotation of an object. The higher the drag the more the rotation slows down. /// - /// Angular damping can be used to slow down the rotation of an object. The higher the drag the more the rotation slows down. API_PROPERTY(Attributes="EditorOrder(70), DefaultValue(0.05f), Limit(0), EditorDisplay(\"Rigid Body\")") FORCE_INLINE float GetAngularDamping() const { @@ -105,9 +78,8 @@ public: } /// - /// Sets the 'drag' force added to reduce angular movement. + /// Sets the 'drag' force added to reduce angular movement. Angular damping can be used to slow down the rotation of an object. The higher the drag the more the rotation slows down. /// - /// Angular damping can be used to slow down the rotation of an object. The higher the drag the more the rotation slows down. /// The value. API_PROPERTY() void SetAngularDamping(float value); @@ -123,7 +95,6 @@ public: /// /// If true simulation and collisions detection will be enabled for the rigidbody. /// - /// The value. API_PROPERTY() void SetEnableSimulation(bool value); /// @@ -138,7 +109,6 @@ public: /// /// If true Continuous Collision Detection (CCD) will be used for this component. /// - /// The value. API_PROPERTY() void SetUseCCD(const bool value); /// @@ -153,7 +123,6 @@ public: /// /// If object should have the force of gravity applied. /// - /// The value. API_PROPERTY() void SetEnableGravity(bool value); /// @@ -168,7 +137,6 @@ public: /// /// If object should start awake, or if it should initially be sleeping. /// - /// The value. API_PROPERTY() void SetStartAwake(bool value); /// @@ -183,16 +151,11 @@ public: /// /// If true, it will update mass when actor scale changes. /// - /// The value. API_PROPERTY() void SetUpdateMassWhenScaleChanges(bool value); /// - /// Gets the maximum angular velocity that a simulated object can achieve. + /// Gets the maximum angular velocity that a simulated object can achieve. The angular velocity of rigidbodies is clamped to MaxAngularVelocity to avoid numerical instability with fast rotating bodies. Because this may prevent intentional fast rotations on objects such as wheels, you can override this value per rigidbody. /// - /// - /// The angular velocity of rigidbodies is clamped to MaxAngularVelocity to avoid numerical instability with fast rotating bodies. - /// Because this may prevent intentional fast rotations on objects such as wheels, you can override this value per rigidbody. - /// API_PROPERTY(Attributes="EditorOrder(90), DefaultValue(7.0f), Limit(0), EditorDisplay(\"Rigid Body\")") FORCE_INLINE float GetMaxAngularVelocity() const { @@ -200,13 +163,8 @@ public: } /// - /// Sets the maximum angular velocity that a simulated object can achieve. + /// Sets the maximum angular velocity that a simulated object can achieve. The angular velocity of rigidbodies is clamped to MaxAngularVelocity to avoid numerical instability with fast rotating bodies. Because this may prevent intentional fast rotations on objects such as wheels, you can override this value per rigidbody. /// - /// - /// The angular velocity of rigidbodies is clamped to MaxAngularVelocity to avoid numerical instability with fast rotating bodies. - /// Because this may prevent intentional fast rotations on objects such as wheels, you can override this value per rigidbody. - /// - /// The value. API_PROPERTY() void SetMaxAngularVelocity(float value); /// @@ -218,7 +176,6 @@ public: /// /// Override the auto computed mass. /// - /// The value. API_PROPERTY() void SetOverrideMass(bool value); /// @@ -230,8 +187,6 @@ public: /// /// Sets the mass value measured in kilograms (use override value only if OverrideMass is checked). /// - /// If set auto enables mass override. - /// The value. API_PROPERTY() void SetMass(float value); /// @@ -243,7 +198,6 @@ public: /// /// Sets the per-instance scaling of the mass. /// - /// The value. API_PROPERTY() void SetMassScale(float value); /// @@ -258,7 +212,6 @@ public: /// /// Sets the user specified offset for the center of mass of this object, from the calculated location. /// - /// The value. API_PROPERTY() void SetCenterOfMassOffset(const Float3& value); /// @@ -273,28 +226,27 @@ public: /// /// Sets the object movement constraint flags that define degrees of freedom are allowed for the simulation of object. /// - /// The value. API_PROPERTY() void SetConstraints(const RigidbodyConstraints value); public: /// /// Gets the linear velocity of the rigidbody. /// - /// It's used mostly to get the current velocity. Manual modifications may result in unrealistic behaviour. + /// It's used mostly to get the current velocity. Manual modifications may result in unrealistic behaviour. API_PROPERTY(Attributes="HideInEditor") Vector3 GetLinearVelocity() const; /// /// Sets the linear velocity of the rigidbody. /// - /// It's used mostly to get the current velocity. Manual modifications may result in unrealistic behaviour. + /// It's used mostly to get the current velocity. Manual modifications may result in unrealistic behaviour. /// The value. API_PROPERTY() void SetLinearVelocity(const Vector3& value) const; /// /// Gets the angular velocity of the rigidbody measured in radians per second. /// - /// It's used mostly to get the current angular velocity. Manual modifications may result in unrealistic behaviour. + /// It's used mostly to get the current angular velocity. Manual modifications may result in unrealistic behaviour. API_PROPERTY(Attributes="HideInEditor") Vector3 GetAngularVelocity() const; diff --git a/Source/Engine/Physics/Colliders/BoxCollider.cpp b/Source/Engine/Physics/Colliders/BoxCollider.cpp index 90f0dab38..fde3b4632 100644 --- a/Source/Engine/Physics/Colliders/BoxCollider.cpp +++ b/Source/Engine/Physics/Colliders/BoxCollider.cpp @@ -81,6 +81,13 @@ void BoxCollider::OnDebugDrawSelected() const Color color = Color::GreenYellow; DEBUG_DRAW_WIRE_BOX(_bounds, color * 0.3f, 0, false); + if (_contactOffset > 0) + { + OrientedBoundingBox contactBounds = _bounds; + contactBounds.Extents += Vector3(_contactOffset) / contactBounds.Transformation.Scale; + DEBUG_DRAW_WIRE_BOX(contactBounds, Color::Blue.AlphaMultiplied(0.2f), 0, false); + } + Vector3 corners[8]; _bounds.GetCorners(corners); const float margin = 1.0f; diff --git a/Source/Engine/Physics/Colliders/CapsuleCollider.cpp b/Source/Engine/Physics/Colliders/CapsuleCollider.cpp index ba133e8b6..98346b0a9 100644 --- a/Source/Engine/Physics/Colliders/CapsuleCollider.cpp +++ b/Source/Engine/Physics/Colliders/CapsuleCollider.cpp @@ -42,27 +42,33 @@ void CapsuleCollider::DrawPhysicsDebug(RenderView& view) const BoundingSphere sphere(_sphere.Center - view.Origin, _sphere.Radius); if (!view.CullingFrustum.Intersects(sphere)) return; - Quaternion rot; - Quaternion::Multiply(_transform.Orientation, Quaternion::Euler(0, 90, 0), rot); + Quaternion rotation; + Quaternion::Multiply(_transform.Orientation, Quaternion::Euler(0, 90, 0), rotation); const float scaling = _cachedScale.GetAbsolute().MaxValue(); const float minSize = 0.001f; const float radius = Math::Max(Math::Abs(_radius) * scaling, minSize); const float height = Math::Max(Math::Abs(_height) * scaling, minSize); if (view.Mode == ViewMode::PhysicsColliders && !GetIsTrigger()) - DEBUG_DRAW_TUBE(_transform.LocalToWorld(_center), rot, radius, height, _staticActor ? Color::CornflowerBlue : Color::Orchid, 0, true); + DEBUG_DRAW_TUBE(_transform.LocalToWorld(_center), rotation, radius, height, _staticActor ? Color::CornflowerBlue : Color::Orchid, 0, true); else - DEBUG_DRAW_WIRE_TUBE(_transform.LocalToWorld(_center), rot, radius, height, Color::GreenYellow * 0.8f, 0, true); + DEBUG_DRAW_WIRE_TUBE(_transform.LocalToWorld(_center), rotation, radius, height, Color::GreenYellow * 0.8f, 0, true); } void CapsuleCollider::OnDebugDrawSelected() { - Quaternion rot; - Quaternion::Multiply(_transform.Orientation, Quaternion::Euler(0, 90, 0), rot); + Quaternion rotation; + Quaternion::Multiply(_transform.Orientation, Quaternion::Euler(0, 90, 0), rotation); const float scaling = _cachedScale.GetAbsolute().MaxValue(); const float minSize = 0.001f; const float radius = Math::Max(Math::Abs(_radius) * scaling, minSize); const float height = Math::Max(Math::Abs(_height) * scaling, minSize); - DEBUG_DRAW_WIRE_TUBE(_transform.LocalToWorld(_center), rot, radius, height, Color::GreenYellow, 0, false); + const Vector3 position = _transform.LocalToWorld(_center); + DEBUG_DRAW_WIRE_TUBE(position, rotation, radius, height, Color::GreenYellow, 0, false); + + if (_contactOffset > 0) + { + DEBUG_DRAW_WIRE_TUBE(position, rotation, radius + _contactOffset, height, Color::Blue.AlphaMultiplied(0.2f), 0, false); + } // Base Collider::OnDebugDrawSelected(); diff --git a/Source/Engine/Physics/Colliders/CharacterController.cpp b/Source/Engine/Physics/Colliders/CharacterController.cpp index 59521e51c..41ee95d04 100644 --- a/Source/Engine/Physics/Colliders/CharacterController.cpp +++ b/Source/Engine/Physics/Colliders/CharacterController.cpp @@ -23,6 +23,7 @@ CharacterController::CharacterController(const SpawnParams& params) , _nonWalkableMode(NonWalkableModes::PreventClimbing) , _lastFlags(CollisionFlags::None) { + _contactOffset = 10.0f; } float CharacterController::GetRadius() const diff --git a/Source/Engine/Physics/Colliders/Collider.cpp b/Source/Engine/Physics/Colliders/Collider.cpp index 49de1f799..6c292bf20 100644 --- a/Source/Engine/Physics/Colliders/Collider.cpp +++ b/Source/Engine/Physics/Colliders/Collider.cpp @@ -19,7 +19,7 @@ Collider::Collider(const SpawnParams& params) , _shape(nullptr) , _staticActor(nullptr) , _cachedScale(1.0f) - , _contactOffset(10.0f) + , _contactOffset(2.0f) { Material.Changed.Bind(this); } diff --git a/Source/Engine/Physics/Colliders/Collider.h b/Source/Engine/Physics/Colliders/Collider.h index 6b9dfcb9d..d3bae9407 100644 --- a/Source/Engine/Physics/Colliders/Collider.h +++ b/Source/Engine/Physics/Colliders/Collider.h @@ -35,11 +35,8 @@ public: void* GetPhysicsShape() const; /// - /// Gets the 'IsTrigger' flag. + /// Gets the 'IsTrigger' flag. A trigger doesn't register a collision with an incoming Rigidbody. Instead, it sends OnTriggerEnter and OnTriggerExit message when a rigidbody enters or exits the trigger volume. /// - /// - /// A trigger doesn't register a collision with an incoming Rigidbody. Instead, it sends OnTriggerEnter, OnTriggerExit and OnTriggerStay message when a rigidbody enters or exits the trigger volume. - /// API_PROPERTY(Attributes="EditorOrder(0), DefaultValue(false), EditorDisplay(\"Collider\")") FORCE_INLINE bool GetIsTrigger() const { @@ -47,11 +44,8 @@ public: } /// - /// Sets the `IsTrigger` flag. A trigger doesn't register a collision with an incoming Rigidbody. Instead, it sends OnTriggerEnter, OnTriggerExit and OnTriggerStay message when a rigidbody enters or exits the trigger volume. + /// Sets the `IsTrigger` flag. A trigger doesn't register a collision with an incoming Rigidbody. Instead, it sends OnTriggerEnter and OnTriggerExit message when a rigidbody enters or exits the trigger volume. /// - /// - /// A trigger doesn't register a collision with an incoming Rigidbody. Instead, it sends OnTriggerEnter, OnTriggerExit and OnTriggerStay message when a rigidbody enters or exits the trigger volume. - /// API_PROPERTY() void SetIsTrigger(bool value); /// @@ -69,23 +63,17 @@ public: API_PROPERTY() void SetCenter(const Vector3& value); /// - /// Gets the contact offset. + /// Gets the contact offset. Colliders whose distance is less than the sum of their ContactOffset values will generate contacts. The contact offset must be positive. Contact offset allows the collision detection system to predictively enforce the contact constraint even when the objects are slightly separated. /// - /// - /// Colliders whose distance is less than the sum of their ContactOffset values will generate contacts. The contact offset must be positive. Contact offset allows the collision detection system to predictively enforce the contact constraint even when the objects are slightly separated. - /// - API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(10.0f), Limit(0, 100), EditorDisplay(\"Collider\")") + API_PROPERTY(Attributes="EditorOrder(1), DefaultValue(2.0f), Limit(0, 100), EditorDisplay(\"Collider\")") FORCE_INLINE float GetContactOffset() const { return _contactOffset; } /// - /// Sets the contact offset. + /// Sets the contact offset. Colliders whose distance is less than the sum of their ContactOffset values will generate contacts. The contact offset must be positive. Contact offset allows the collision detection system to predictively enforce the contact constraint even when the objects are slightly separated. /// - /// - /// Colliders whose distance is less than the sum of their ContactOffset values will generate contacts. The contact offset must be positive. Contact offset allows the collision detection system to predictively enforce the contact constraint even when the objects are slightly separated. - /// API_PROPERTY() void SetContactOffset(float value); /// diff --git a/Source/Engine/Physics/Colliders/SphereCollider.cpp b/Source/Engine/Physics/Colliders/SphereCollider.cpp index e163484e8..fa2fb1cca 100644 --- a/Source/Engine/Physics/Colliders/SphereCollider.cpp +++ b/Source/Engine/Physics/Colliders/SphereCollider.cpp @@ -40,6 +40,13 @@ void SphereCollider::OnDebugDrawSelected() { DEBUG_DRAW_WIRE_SPHERE(_sphere, Color::GreenYellow, 0, false); + if (_contactOffset > 0) + { + BoundingSphere contactBounds = _sphere; + contactBounds.Radius += _contactOffset; + DEBUG_DRAW_WIRE_SPHERE(contactBounds, Color::Blue.AlphaMultiplied(0.2f), 0, false); + } + // Base Collider::OnDebugDrawSelected(); } diff --git a/Source/Engine/Physics/CollisionCooking.cpp b/Source/Engine/Physics/CollisionCooking.cpp index dc2c42584..4c8cffdcc 100644 --- a/Source/Engine/Physics/CollisionCooking.cpp +++ b/Source/Engine/Physics/CollisionCooking.cpp @@ -127,6 +127,8 @@ bool CollisionCooking::CookCollision(const Argument& arg, CollisionData::Seriali const auto& mesh = *meshes[i]; if ((arg.MaterialSlotsMask & (1 << mesh.GetMaterialSlotIndex())) == 0) continue; + if (mesh.GetVertexCount() == 0) + continue; int32 count; if (mesh.DownloadDataCPU(MeshBufferType::Vertex0, vertexBuffers[i], count)) @@ -159,6 +161,8 @@ bool CollisionCooking::CookCollision(const Argument& arg, CollisionData::Seriali const auto& mesh = *meshes[i]; if ((arg.MaterialSlotsMask & (1 << mesh.GetMaterialSlotIndex())) == 0) continue; + if (mesh.GetVertexCount() == 0) + continue; auto task = mesh.DownloadDataGPUAsync(MeshBufferType::Vertex0, vertexBuffers[i]); if (task == nullptr) @@ -208,6 +212,8 @@ bool CollisionCooking::CookCollision(const Argument& arg, CollisionData::Seriali const int32 firstVertexIndex = vertexCounter; const int32 vertexCount = vertexCounts[i]; + if (vertexCount == 0) + continue; Platform::MemoryCopy(finalVertexData.Get() + firstVertexIndex, vData.Get(), vertexCount * sizeof(Float3)); vertexCounter += vertexCount; diff --git a/Source/Engine/Physics/CollisionData.cpp b/Source/Engine/Physics/CollisionData.cpp index 36f28c111..54a49671d 100644 --- a/Source/Engine/Physics/CollisionData.cpp +++ b/Source/Engine/Physics/CollisionData.cpp @@ -23,12 +23,16 @@ CollisionData::CollisionData(const SpawnParams& params, const AssetInfo* info) bool CollisionData::CookCollision(CollisionDataType type, ModelBase* modelObj, int32 modelLodIndex, uint32 materialSlotsMask, ConvexMeshGenerationFlags convexFlags, int32 convexVertexLimit) { - // Validate state if (!IsVirtual()) { LOG(Warning, "Only virtual assets can be modified at runtime."); return true; } + if (IsInMainThread() && modelObj && modelObj->IsVirtual()) + { + LOG(Error, "Cannot cook collision data for virtual models on a main thread (virtual models data is stored on GPU only). Use thread pool or async task."); + return true; + } // Prepare CollisionCooking::Argument arg; @@ -43,18 +47,12 @@ bool CollisionData::CookCollision(CollisionDataType type, ModelBase* modelObj, i SerializedOptions options; BytesContainer outputData; if (CollisionCooking::CookCollision(arg, options, outputData)) - { return true; - } - - // Clear state - unload(true); // Load data + unload(true); if (load(&options, outputData.Get(), outputData.Length()) != LoadResult::Ok) - { return true; - } // Mark as loaded (eg. Mesh Colliders using this asset will update shape for physics simulation) onLoaded(); diff --git a/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp b/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp index 7c9355099..5781e4641 100644 --- a/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp +++ b/Source/Engine/Physics/PhysX/SimulationEventCallbackPhysX.cpp @@ -117,9 +117,7 @@ void SimulationEventCallback::onContact(const PxContactPairHeader& pairHeader, c { // Skip sending events to removed actors if (pairHeader.flags & (PxContactPairHeaderFlag::eREMOVED_ACTOR_0 | PxContactPairHeaderFlag::eREMOVED_ACTOR_1)) - { return; - } Collision c; PxContactPairExtraDataIterator j(pairHeader.extraDataStream, pairHeader.extraDataStreamSize); @@ -132,13 +130,18 @@ void SimulationEventCallback::onContact(const PxContactPairHeader& pairHeader, c const PxReal* impulses = pair.contactImpulses; //const PxU32 flippedContacts = (pair.flags & PxContactPairFlag::eINTERNAL_CONTACTS_ARE_FLIPPED); - const PxU32 hasImpulses = (pair.flags & PxContactPairFlag::eINTERNAL_HAS_IMPULSES); + const bool hasImpulses = pair.flags.isSet(PxContactPairFlag::eINTERNAL_HAS_IMPULSES); + const bool hasPostVelocities = !pair.flags.isSet(PxContactPairFlag::eACTOR_PAIR_LOST_TOUCH); PxU32 nbContacts = 0; PxVec3 totalImpulse(0.0f); c.ThisActor = static_cast(pair.shapes[0]->userData); c.OtherActor = static_cast(pair.shapes[1]->userData); - ASSERT_LOW_LAYER(c.ThisActor && c.OtherActor); + if (c.ThisActor == nullptr || c.OtherActor == nullptr) + { + // One of the actors was deleted (eg. via RigidBody destroyed by gameplay) then skip processing this collision + continue; + } // Extract contact points while (i.hasNextPatch()) @@ -166,7 +169,8 @@ void SimulationEventCallback::onContact(const PxContactPairHeader& pairHeader, c } // Extract velocities - if (j.nextItemSet()) + c.ThisVelocity = c.OtherVelocity = Vector3::Zero; + if (hasPostVelocities && j.nextItemSet()) { ASSERT(j.contactPairIndex == pairIndex); if (j.postSolverVelocity) @@ -177,10 +181,6 @@ void SimulationEventCallback::onContact(const PxContactPairHeader& pairHeader, c c.ThisVelocity = P2C(linearVelocityActor0); c.OtherVelocity = P2C(linearVelocityActor1); } - else - { - c.ThisVelocity = c.OtherVelocity = Vector3::Zero; - } } c.ContactsCount = nbContacts; @@ -195,6 +195,7 @@ void SimulationEventCallback::onContact(const PxContactPairHeader& pairHeader, c RemovedCollisions.Add(c); } } + //ASSERT(!j.nextItemSet()); } void SimulationEventCallback::onTrigger(PxTriggerPair* pairs, PxU32 count) diff --git a/Source/Engine/Platform/Android/AndroidFileSystem.cpp b/Source/Engine/Platform/Android/AndroidFileSystem.cpp index 78fa8953d..a04fc0229 100644 --- a/Source/Engine/Platform/Android/AndroidFileSystem.cpp +++ b/Source/Engine/Platform/Android/AndroidFileSystem.cpp @@ -7,6 +7,7 @@ #include "Engine/Core/Types/String.h" #include "Engine/Core/Types/StringView.h" #include "Engine/Core/Types/TimeSpan.h" +#include "Engine/Core/Collections/Array.h" #include "Engine/Core/Math/Math.h" #include "Engine/Engine/Globals.h" #include "Engine/Utilities/StringConverter.h" diff --git a/Source/Engine/Platform/Android/AndroidPlatform.cpp b/Source/Engine/Platform/Android/AndroidPlatform.cpp index c61e2aadd..c844ab81d 100644 --- a/Source/Engine/Platform/Android/AndroidPlatform.cpp +++ b/Source/Engine/Platform/Android/AndroidPlatform.cpp @@ -665,6 +665,7 @@ void AndroidPlatform::PreInit(android_app* app) app->onInputEvent = OnAppInput; ANativeActivity_setWindowFlags(app->activity, AWINDOW_FLAG_KEEP_SCREEN_ON | AWINDOW_FLAG_TURN_SCREEN_ON | AWINDOW_FLAG_FULLSCREEN | AWINDOW_FLAG_DISMISS_KEYGUARD, 0); ANativeActivity_setWindowFormat(app->activity, WINDOW_FORMAT_RGBA_8888); + pthread_setname_np(pthread_self(), "Main"); } bool AndroidPlatform::Is64BitPlatform() diff --git a/Source/Engine/Platform/Apple/AppleFileSystem.cpp b/Source/Engine/Platform/Apple/AppleFileSystem.cpp index cc18fc3ed..9e306019a 100644 --- a/Source/Engine/Platform/Apple/AppleFileSystem.cpp +++ b/Source/Engine/Platform/Apple/AppleFileSystem.cpp @@ -8,6 +8,7 @@ #include "Engine/Core/Types/String.h" #include "Engine/Core/Types/StringView.h" #include "Engine/Core/Types/TimeSpan.h" +#include "Engine/Core/Collections/Array.h" #include "Engine/Core/Math/Math.h" #include "Engine/Core/Log.h" #include "Engine/Utilities/StringConverter.h" diff --git a/Source/Engine/Platform/Apple/ApplePlatform.cpp b/Source/Engine/Platform/Apple/ApplePlatform.cpp index bf79c95c2..c939c1af2 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.cpp +++ b/Source/Engine/Platform/Apple/ApplePlatform.cpp @@ -19,6 +19,7 @@ #include "Engine/Platform/StringUtils.h" #include "Engine/Platform/WindowsManager.h" #include "Engine/Platform/Clipboard.h" +#include "Engine/Platform/Thread.h" #include "Engine/Platform/IGuiData.h" #include "Engine/Platform/Base/PlatformUtils.h" #include "Engine/Utilities/StringConverter.h" @@ -48,6 +49,7 @@ CPUInfo Cpu; String UserLocale; double SecondsPerCycle; NSAutoreleasePool* AutoreleasePool = nullptr; +int32 AutoreleasePoolInterval = 0; float ApplePlatform::ScreenScale = 1.0f; @@ -175,12 +177,21 @@ uint64 ApplePlatform::GetCurrentThreadID() void ApplePlatform::SetThreadPriority(ThreadPriority priority) { - // TODO: impl this + struct sched_param sched; + Platform::MemoryClear(&sched, sizeof(struct sched_param)); + int32 policy = SCHED_RR; + pthread_getschedparam(pthread_self(), &policy, &sched); + sched.sched_priority = AppleThread::GetAppleThreadPriority(priority); + pthread_setschedparam(pthread_self(), policy, &sched); } void ApplePlatform::SetThreadAffinityMask(uint64 affinityMask) { - // TODO: impl this +#if PLATFORM_MAC + thread_affinity_policy policy; + policy.affinity_tag = affinityMask; + thread_policy_set(pthread_mach_thread_np(pthread_self()), THREAD_AFFINITY_POLICY, (integer_t*)&policy, THREAD_AFFINITY_POLICY_COUNT); +#endif } void ApplePlatform::Sleep(int32 milliseconds) @@ -245,13 +256,40 @@ void ApplePlatform::GetUTCTime(int32& year, int32& month, int32& dayOfWeek, int3 millisecond = time.tv_usec / 1000; } +#if !BUILD_RELEASE + void ApplePlatform::Log(const StringView& msg) { -#if !BUILD_RELEASE && !USE_EDITOR +#if !USE_EDITOR NSLog(@"%s", StringAsANSI<>(*msg, msg.Length()).Get()); #endif } +bool ApplePlatform::IsDebuggerPresent() +{ + // Reference: https://developer.apple.com/library/archive/qa/qa1361/_index.html + int mib[4]; + struct kinfo_proc info; + + // Initialize the flags so that, if sysctl fails for some bizarre reason, we get a predictable result + info.kp_proc.p_flag = 0; + + // Initialize mib, which tells sysctl the info we want, in this case we're looking for information about a specific process ID + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + + // Call sysctl + size_t size = sizeof(info); + sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); + + // We're being debugged if the P_TRACED flag is set + return ((info.kp_proc.p_flag & P_TRACED) != 0); +} + +#endif + bool ApplePlatform::Init() { if (UnixPlatform::Init()) @@ -332,9 +370,13 @@ bool ApplePlatform::Init() void ApplePlatform::Tick() { - // TODO: do it once every X fames - [AutoreleasePool drain]; - AutoreleasePool = [[NSAutoreleasePool alloc] init]; + AutoreleasePoolInterval++; + if (AutoreleasePoolInterval >= 60) + { + AutoreleasePoolInterval = 0; + [AutoreleasePool drain]; + AutoreleasePool = [[NSAutoreleasePool alloc] init]; + } } void ApplePlatform::BeforeExit() @@ -343,6 +385,11 @@ void ApplePlatform::BeforeExit() void ApplePlatform::Exit() { + if (AutoreleasePool) + { + [AutoreleasePool drain]; + AutoreleasePool = nullptr; + } } void ApplePlatform::SetHighDpiAwarenessEnabled(bool enable) @@ -369,9 +416,7 @@ bool ApplePlatform::GetHasFocus() if (window->IsFocused()) return true; } - - // Default to true if has no windows open - return WindowsManager::Windows.IsEmpty(); + return false; } void ApplePlatform::CreateGuid(Guid& result) diff --git a/Source/Engine/Platform/Apple/ApplePlatform.h b/Source/Engine/Platform/Apple/ApplePlatform.h index f1148d0b6..94b6534d6 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.h +++ b/Source/Engine/Platform/Apple/ApplePlatform.h @@ -79,7 +79,10 @@ public: static uint64 GetClockFrequency(); static void GetSystemTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond); static void GetUTCTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond); +#if !BUILD_RELEASE static void Log(const StringView& msg); + static bool IsDebuggerPresent(); +#endif static bool Init(); static void Tick(); static void BeforeExit(); diff --git a/Source/Engine/Platform/Apple/AppleThread.h b/Source/Engine/Platform/Apple/AppleThread.h index 43d9d2966..a578092b6 100644 --- a/Source/Engine/Platform/Apple/AppleThread.h +++ b/Source/Engine/Platform/Apple/AppleThread.h @@ -40,10 +40,7 @@ public: return (AppleThread*)Setup(New(runnable, name, priority), stackSize); } -protected: - - // [UnixThread] - int32 GetThreadPriority(ThreadPriority priority) override + static int32 GetAppleThreadPriority(ThreadPriority priority) { switch (priority) { @@ -60,6 +57,14 @@ protected: } return 31; } + +protected: + + // [UnixThread] + int32 GetThreadPriority(ThreadPriority priority) override + { + return GetAppleThreadPriority(priority); + } int32 Start(pthread_attr_t& attr) override { return pthread_create(&_thread, &attr, ThreadProc, this); diff --git a/Source/Engine/Platform/Base/DragDropHelper.h b/Source/Engine/Platform/Base/DragDropHelper.h new file mode 100644 index 000000000..a5382afd6 --- /dev/null +++ b/Source/Engine/Platform/Base/DragDropHelper.h @@ -0,0 +1,47 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Threading/ThreadPoolTask.h" +#include "Engine/Threading/ThreadPool.h" +#include "Engine/Scripting/Scripting.h" +#include "Engine/Scripting/ManagedCLR/MDomain.h" +#include "Engine/Engine/Engine.h" +#include "Engine/Platform/Platform.h" +#if USE_EDITOR +#if !COMPILE_WITH_DEBUG_DRAW +#define COMPILE_WITH_DEBUG_DRAW 1 +#define COMPILE_WITH_DEBUG_DRAW_HACK +#endif +#include "Engine/Debug/DebugDraw.h" +#ifdef COMPILE_WITH_DEBUG_DRAW_HACK +#undef COMPILE_WITH_DEBUG_DRAW_HACK +#undef COMPILE_WITH_DEBUG_DRAW +#define COMPILE_WITH_DEBUG_DRAW 0 +#endif +#endif + +/// +/// Async DoDragDrop helper (used for rendering frames during main thread stall). +/// +class DoDragDropJob : public ThreadPoolTask +{ +public: + int64 ExitFlag = 0; + + // [ThreadPoolTask] + bool Run() override + { + Scripting::GetScriptsDomain()->Dispatch(); + while (Platform::AtomicRead(&ExitFlag) == 0) + { +#if USE_EDITOR + // Flush any single-frame shapes to prevent memory leaking (eg. via terrain collision debug during scene drawing with PhysicsColliders or PhysicsDebug flag) + DebugDraw::UpdateContext(nullptr, 0.0f); +#endif + Engine::OnDraw(); + Platform::Sleep(20); + } + return false; + } +}; diff --git a/Source/Engine/Platform/Base/FileBase.h b/Source/Engine/Platform/Base/FileBase.h index c6f76c610..5587e20a9 100644 --- a/Source/Engine/Platform/Base/FileBase.h +++ b/Source/Engine/Platform/Base/FileBase.h @@ -7,7 +7,6 @@ #include "Engine/Core/NonCopyable.h" #include "Engine/Core/Types/BaseTypes.h" #include "Engine/Core/Types/DateTime.h" -#include "Engine/Core/Collections/Array.h" class StringBuilder; template @@ -16,17 +15,51 @@ class DataContainer; /// /// Specifies how the operating system should open a file. /// -DECLARE_ENUM_FLAGS_5(FileMode, uint32, CreateAlways, 2, CreateNew, 1, OpenAlways, 4, OpenExisting, 3, TruncateExisting, 5); +enum class FileMode : uint32 +{ + // Creates a new file, only if it does not already exist. + CreateNew = 1, + // Creates a new file, always. + CreateAlways = 2, + // Opens a file, only if it exists. Fails if file doesn't exist. + OpenExisting = 3, + // Opens a file, always. + OpenAlways = 4, + // Opens a file and truncates it so that its size is zero bytes, only if it exists. Fails if file doesn't exist. + TruncateExisting = 5, +}; /// /// Defines constants for read, write, or read/write access to a file. /// -DECLARE_ENUM_FLAGS_3(FileAccess, uint32, Read, 0x80000000, Write, 0x40000000, ReadWrite, (uint32)FileAccess::Read | (uint32)FileAccess::Write); +enum class FileAccess : uint32 +{ + // Enables reading data from the file. + Read = 0x80000000, + // Enables writing data to the file. + Write = 0x40000000, + // Enables both data read and write operations on the file. + ReadWrite = Read | Write, +}; /// /// Contains constants for controlling the kind of access other objects can have to the same file. /// -DECLARE_ENUM_FLAGS_6(FileShare, uint32, Delete, 0x00000004, None, 0x00000000, Read, 0x00000001, Write, 0x00000002, ReadWrite, (uint32)FileShare::Read | (uint32)FileShare::Write, All, (uint32)FileShare::ReadWrite | (uint32)FileShare::Delete); +enum class FileShare : uint32 +{ + // Prevents any operations on the file file it's opened. + None = 0x00000000, + // Allows read operations on a file. + Read = 0x00000001, + // Allows write operations on a file. + Write = 0x00000002, + // Allows delete operations on a file. + Delete = 0x00000004, + // Allows read and write operations on a file. + ReadWrite = Read | Write, + // Allows any operations on a file. + All = ReadWrite | Delete, +}; /// /// The base class for file objects. @@ -34,7 +67,6 @@ DECLARE_ENUM_FLAGS_6(FileShare, uint32, Delete, 0x00000004, None, 0x00000000, Re class FLAXENGINE_API FileBase : public NonCopyable { public: - /// /// Finalizes an instance of the class. /// @@ -43,7 +75,6 @@ public: } public: - /// /// Reads data from a file. /// @@ -68,7 +99,6 @@ public: virtual void Close() = 0; public: - /// /// Gets size of the file (in bytes). /// @@ -100,14 +130,13 @@ public: virtual bool IsOpened() const = 0; public: - static bool ReadAllBytes(const StringView& path, byte* data, int32 length); - static bool ReadAllBytes(const StringView& path, Array& data); + static bool ReadAllBytes(const StringView& path, Array& data); static bool ReadAllBytes(const StringView& path, DataContainer& data); static bool ReadAllText(const StringView& path, String& data); static bool ReadAllText(const StringView& path, StringAnsi& data); static bool WriteAllBytes(const StringView& path, const byte* data, int32 length); - static bool WriteAllBytes(const StringView& path, const Array& data); + static bool WriteAllBytes(const StringView& path, const Array& data); static bool WriteAllText(const StringView& path, const String& data, Encoding encoding); static bool WriteAllText(const StringView& path, const StringBuilder& data, Encoding encoding); static bool WriteAllText(const StringView& path, const Char* data, int32 length, Encoding encoding); diff --git a/Source/Engine/Platform/Base/FileSystemBase.cpp b/Source/Engine/Platform/Base/FileSystemBase.cpp index f2f45a6d4..3d0c053d5 100644 --- a/Source/Engine/Platform/Base/FileSystemBase.cpp +++ b/Source/Engine/Platform/Base/FileSystemBase.cpp @@ -5,6 +5,7 @@ #include "Engine/Core/Types/Guid.h" #include "Engine/Core/Types/String.h" #include "Engine/Core/Types/StringView.h" +#include "Engine/Core/Collections/Array.h" #include "Engine/Core/Math/Math.h" #include "Engine/Engine/Globals.h" diff --git a/Source/Engine/Platform/Base/WindowsManager.cpp b/Source/Engine/Platform/Base/WindowsManager.cpp index 0d58008d8..34381b3d2 100644 --- a/Source/Engine/Platform/Base/WindowsManager.cpp +++ b/Source/Engine/Platform/Base/WindowsManager.cpp @@ -59,9 +59,11 @@ void WindowsManagerService::Update() // Update windows const float deltaTime = Time::Update.UnscaledDeltaTime.GetTotalSeconds(); WindowsManager::WindowsLocker.Lock(); - for (Window* win : WindowsManager::Windows) + Array> windows; + windows.Add(WindowsManager::Windows); + for (Window* win : windows) { - if (win && win->IsVisible()) + if (win->IsVisible()) win->OnUpdate(deltaTime); } WindowsManager::WindowsLocker.Unlock(); @@ -71,7 +73,8 @@ void WindowsManagerService::Dispose() { // Close remaining windows WindowsManager::WindowsLocker.Lock(); - auto windows = WindowsManager::Windows; + Array> windows; + windows.Add(WindowsManager::Windows); for (Window* win : windows) { win->Close(ClosingReason::EngineExit); diff --git a/Source/Engine/Platform/CreateWindowSettings.cs b/Source/Engine/Platform/CreateWindowSettings.cs index d4d9ce727..8f7cd8c0d 100644 --- a/Source/Engine/Platform/CreateWindowSettings.cs +++ b/Source/Engine/Platform/CreateWindowSettings.cs @@ -12,7 +12,7 @@ namespace FlaxEngine Position = new Float2(100, 100), Size = new Float2(640, 480), MinimumSize = Float2.One, - MaximumSize = new Float2(4100, 4100), + MaximumSize = Float2.Zero, // Unlimited size StartPosition = WindowStartPosition.CenterParent, HasBorder = true, ShowInTaskbar = true, diff --git a/Source/Engine/Platform/CreateWindowSettings.h b/Source/Engine/Platform/CreateWindowSettings.h index 1ff596df9..9c543a05d 100644 --- a/Source/Engine/Platform/CreateWindowSettings.h +++ b/Source/Engine/Platform/CreateWindowSettings.h @@ -59,9 +59,9 @@ DECLARE_SCRIPTING_TYPE_MINIMAL(CreateWindowSettings); API_FIELD() Float2 MinimumSize = Float2(1, 1); /// - /// The maximum size. + /// The maximum size. Set to 0 to use unlimited size. /// - API_FIELD() Float2 MaximumSize = Float2(8192, 4096); + API_FIELD() Float2 MaximumSize = Float2(0, 0); /// /// The start position mode. diff --git a/Source/Engine/Platform/Linux/LinuxFileSystem.cpp b/Source/Engine/Platform/Linux/LinuxFileSystem.cpp index 3f33a8f65..97cde4a1c 100644 --- a/Source/Engine/Platform/Linux/LinuxFileSystem.cpp +++ b/Source/Engine/Platform/Linux/LinuxFileSystem.cpp @@ -9,6 +9,7 @@ #include "Engine/Core/Types/StringBuilder.h" #include "Engine/Core/Types/StringView.h" #include "Engine/Core/Types/TimeSpan.h" +#include "Engine/Core/Collections/Array.h" #include "Engine/Core/Math/Math.h" #include "Engine/Core/Log.h" #include "Engine/Utilities/StringConverter.h" @@ -678,8 +679,14 @@ void LinuxFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& res result = TEXT("/usr/share"); break; case SpecialFolder::LocalAppData: - result = home; + { + String dataHome; + if (!Platform::GetEnvironmentVariable(TEXT("XDG_DATA_HOME"), dataHome)) + result = dataHome; + else + result = home / TEXT(".local/share"); break; + } case SpecialFolder::ProgramData: result = String::Empty; break; diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.cpp b/Source/Engine/Platform/Linux/LinuxPlatform.cpp index 4787b4549..36affd8c8 100644 --- a/Source/Engine/Platform/Linux/LinuxPlatform.cpp +++ b/Source/Engine/Platform/Linux/LinuxPlatform.cpp @@ -2622,9 +2622,7 @@ bool LinuxPlatform::GetHasFocus() if (window->IsFocused()) return true; } - - // Default to true if has no windows open - return WindowsManager::Windows.IsEmpty(); + return false; } bool LinuxPlatform::CanOpenUrl(const StringView& url) diff --git a/Source/Engine/Platform/Linux/LinuxWindow.cpp b/Source/Engine/Platform/Linux/LinuxWindow.cpp index a232dea55..b3bae0276 100644 --- a/Source/Engine/Platform/Linux/LinuxWindow.cpp +++ b/Source/Engine/Platform/Linux/LinuxWindow.cpp @@ -150,9 +150,9 @@ LinuxWindow::LinuxWindow(const CreateWindowSettings& settings) { // Set resizing range hints.min_width = (int)settings.MinimumSize.X; - hints.max_width = (int)settings.MaximumSize.X; + hints.max_width = settings.MaximumSize.X > 0 ? (int)settings.MaximumSize.X : MAX_uint16; hints.min_height = (int)settings.MinimumSize.Y; - hints.max_height = (int)settings.MaximumSize.Y; + hints.max_height = settings.MaximumSize.Y > 0 ? (int)settings.MaximumSize.Y : MAX_uint16; hints.flags |= USSize; } // honor the WM placement except for manual (overriding) placements @@ -594,6 +594,12 @@ void LinuxWindow::OnButtonPress(void* event) case Button3: mouseButton = MouseButton::Right; break; + case 8: + mouseButton = MouseButton::Extended2; + break; + case 9: + mouseButton = MouseButton::Extended1; + break; default: return; } @@ -641,6 +647,12 @@ void LinuxWindow::OnButtonRelease(void* event) case Button5: Input::Mouse->OnMouseWheel(ClientToScreen(mousePos), -1.0f, this); break; + case 8: + Input::Mouse->OnMouseUp(ClientToScreen(mousePos), MouseButton::Extended2, this); + break; + case 9: + Input::Mouse->OnMouseUp(ClientToScreen(mousePos), MouseButton::Extended1, this); + break; default: return; } diff --git a/Source/Engine/Platform/Mac/MacFileSystem.cpp b/Source/Engine/Platform/Mac/MacFileSystem.cpp index c765feef4..e2ed24f13 100644 --- a/Source/Engine/Platform/Mac/MacFileSystem.cpp +++ b/Source/Engine/Platform/Mac/MacFileSystem.cpp @@ -9,6 +9,7 @@ #include "Engine/Core/Types/String.h" #include "Engine/Core/Types/StringView.h" #include "Engine/Core/Types/TimeSpan.h" +#include "Engine/Core/Collections/Array.h" #include "Engine/Core/Math/Math.h" #include "Engine/Core/Log.h" #include "Engine/Utilities/StringConverter.h" diff --git a/Source/Engine/Platform/Mac/MacWindow.cpp b/Source/Engine/Platform/Mac/MacWindow.cpp index 000accbfa..373d19bae 100644 --- a/Source/Engine/Platform/Mac/MacWindow.cpp +++ b/Source/Engine/Platform/Mac/MacWindow.cpp @@ -4,7 +4,12 @@ #include "../Window.h" #include "Engine/Platform/Apple/AppleUtils.h" +#include "Engine/Platform/WindowsManager.h" #include "Engine/Platform/IGuiData.h" +#if USE_EDITOR +#include "Engine/Platform/CriticalSection.h" +#include "Engine/Platform/Base/DragDropHelper.h" +#endif #include "Engine/Core/Log.h" #include "Engine/Input/Input.h" #include "Engine/Input/Mouse.h" @@ -14,6 +19,21 @@ #include #include +#if USE_EDITOR +// Data for drawing window while doing drag&drop on Mac (engine is paused during platform tick) +CriticalSection MacDragLocker; +NSDraggingSession* MacDragSession = nullptr; +DoDragDropJob* MacDragJob = nullptr; +#endif + +inline bool IsWindowInvalid(Window* win) +{ + WindowsManager::WindowsLocker.Lock(); + const bool hasWindow = WindowsManager::Windows.Contains(win); + WindowsManager::WindowsLocker.Unlock(); + return !hasWindow || !win; +} + KeyboardKeys GetKey(NSEvent* event) { switch ([event keyCode]) @@ -268,17 +288,20 @@ NSDragOperation GetDragDropOperation(DragDropEffect dragDropEffect) // Handle resizing to be sure that content has valid size when window was resized [self windowDidResize:notification]; + if (IsWindowInvalid(Window)) return; Window->OnGotFocus(); } - (void)windowDidResignKey:(NSNotification*)notification { + if (IsWindowInvalid(Window)) return; Window->OnLostFocus(); } - (void)windowWillClose:(NSNotification*)notification { [self setDelegate: nil]; + if (IsWindowInvalid(Window)) return; Window->Close(ClosingReason::User); } @@ -311,7 +334,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) @end -@interface MacViewImpl : NSView +@interface MacViewImpl : NSView { MacWindow* Window; NSTrackingArea* TrackingArea; @@ -375,6 +398,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)keyDown:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; KeyboardKeys key = GetKey(event); if (key != KeyboardKeys::None) Input::Keyboard->OnKeyDown(key, Window); @@ -405,6 +429,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)keyUp:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; KeyboardKeys key = GetKey(event); if (key != KeyboardKeys::None) Input::Keyboard->OnKeyUp(key, Window); @@ -412,6 +437,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)flagsChanged:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; int32 modMask; int32 keyCode = [event keyCode]; if (keyCode == 0x36 || keyCode == 0x37) @@ -437,6 +463,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)scrollWheel:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); double deltaX = [event scrollingDeltaX]; double deltaY = [event scrollingDeltaY]; @@ -451,32 +478,39 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)mouseMoved:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); - if (!Window->IsMouseTracking() && !IsMouseOver) + if (Window->IsMouseTracking()) + return; // Skip mouse events when tracking mouse (handled in MacWindow::OnUpdate) + if (!IsMouseOver) return; Input::Mouse->OnMouseMove(Window->ClientToScreen(mousePos), Window); } - (void)mouseEntered:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; IsMouseOver = true; Window->SetIsMouseOver(true); } - (void)mouseExited:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; IsMouseOver = false; Window->SetIsMouseOver(false); } - (void)mouseDown:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); + mousePos = Window->ClientToScreen(mousePos); MouseButton mouseButton = MouseButton::Left; if ([event clickCount] == 2) - Input::Mouse->OnMouseDoubleClick(Window->ClientToScreen(mousePos), mouseButton, Window); + Input::Mouse->OnMouseDoubleClick(mousePos, mouseButton, Window); else - Input::Mouse->OnMouseDown(Window->ClientToScreen(mousePos), mouseButton, Window); + Input::Mouse->OnMouseDown(mousePos, mouseButton, Window); } - (void)mouseDragged:(NSEvent*)event @@ -486,13 +520,28 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)mouseUp:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); + mousePos = Window->ClientToScreen(mousePos); MouseButton mouseButton = MouseButton::Left; - Input::Mouse->OnMouseUp(Window->ClientToScreen(mousePos), mouseButton, Window); + Input::Mouse->OnMouseUp(mousePos, mouseButton, Window); + + // Redirect event to any window that tracks the mouse (eg. dock window in Editor) + WindowsManager::WindowsLocker.Lock(); + for (auto* win : WindowsManager::Windows) + { + if (win->IsVisible() && win->IsMouseTracking() && win != Window) + { + Input::Mouse->OnMouseUp(mousePos, mouseButton, win); + break; + } + } + WindowsManager::WindowsLocker.Unlock(); } - (void)rightMouseDown:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); MouseButton mouseButton = MouseButton::Right; if ([event clickCount] == 2) @@ -508,6 +557,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)rightMouseUp:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); MouseButton mouseButton = MouseButton::Right; Input::Mouse->OnMouseUp(Window->ClientToScreen(mousePos), mouseButton, Window); @@ -515,6 +565,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)otherMouseDown:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); MouseButton mouseButton; switch ([event buttonNumber]) @@ -544,6 +595,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)otherMouseUp:(NSEvent*)event { + if (IsWindowInvalid(Window)) return; Float2 mousePos = GetMousePosition(Window, event); MouseButton mouseButton; switch ([event buttonNumber]) @@ -565,6 +617,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (NSDragOperation)draggingEntered:(id)sender { + if (IsWindowInvalid(Window)) return NSDragOperationNone; Float2 mousePos; MacDropData dropData; GetDragDropData(Window, sender, mousePos, dropData); @@ -580,6 +633,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (NSDragOperation)draggingUpdated:(id)sender { + if (IsWindowInvalid(Window)) return NSDragOperationNone; Float2 mousePos; MacDropData dropData; GetDragDropData(Window, sender, mousePos, dropData); @@ -590,6 +644,7 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (BOOL)performDragOperation:(id)sender { + if (IsWindowInvalid(Window)) return NO; Float2 mousePos; MacDropData dropData; GetDragDropData(Window, sender, mousePos, dropData); @@ -600,9 +655,38 @@ static void ConvertNSRect(NSScreen *screen, NSRect *r) - (void)draggingExited:(id)sender { + if (IsWindowInvalid(Window)) return; Window->OnDragLeave(); } +- (NSDragOperation)draggingSession:(NSDraggingSession*)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context +{ + if (IsWindowInvalid(Window)) return NSDragOperationNone; + return NSDragOperationMove; +} + +- (void)draggingSession:(NSDraggingSession*)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation +{ +#if USE_EDITOR + // Stop background worker once the drag ended + MacDragLocker.Lock(); + if (MacDragSession && MacDragSession == session) + { + Platform::AtomicStore(&MacDragJob->ExitFlag, 1); + MacDragJob->Wait(); + MacDragSession = nullptr; + MacDragJob = nullptr; + } + MacDragLocker.Unlock(); +#endif +} + +- (void)pasteboard:(nullable NSPasteboard*)pasteboard item:(NSPasteboardItem*)item provideDataForType:(NSPasteboardType)type +{ + if (IsWindowInvalid(Window)) return; + [pasteboard setString:(NSString*)AppleUtils::ToString(Window->GetDragText()) forType:NSPasteboardTypeString]; +} + @end MacWindow::MacWindow(const CreateWindowSettings& settings) @@ -624,6 +708,8 @@ MacWindow::MacWindow(const CreateWindowSettings& settings) { styleMask |= NSWindowStyleMaskBorderless; } + if (settings.Fullscreen) + styleMask |= NSWindowStyleMaskFullScreen; if (settings.HasBorder) { styleMask |= NSWindowStyleMaskTitled; @@ -647,12 +733,15 @@ MacWindow::MacWindow(const CreateWindowSettings& settings) [window setWindow:this]; [window setReleasedWhenClosed:NO]; [window setMinSize:NSMakeSize(settings.MinimumSize.X, settings.MinimumSize.Y)]; - [window setMaxSize:NSMakeSize(settings.MaximumSize.X, settings.MaximumSize.Y)]; + if (settings.MaximumSize.SumValues() > 0) + [window setMaxSize:NSMakeSize(settings.MaximumSize.X, settings.MaximumSize.Y)]; [window setOpaque:!settings.SupportsTransparency]; [window setContentView:view]; - [window setAcceptsMouseMovedEvents:YES]; + if (settings.AllowInput) + [window setAcceptsMouseMovedEvents:YES]; [window setDelegate:window]; _window = window; + _view = view; if (settings.AllowDragAndDrop) { [view registerForDraggedTypes:@[NSPasteboardTypeFileURL, NSPasteboardTypeString]]; @@ -664,8 +753,6 @@ MacWindow::MacWindow(const CreateWindowSettings& settings) layer.contentsScale = screenScale; // TODO: impl Parent for MacWindow - // TODO: impl StartPosition for MacWindow - // TODO: impl Fullscreen for MacWindow // TODO: impl ShowInTaskbar for MacWindow // TODO: impl IsTopmost for MacWindow } @@ -676,6 +763,7 @@ MacWindow::~MacWindow() [window close]; [window release]; _window = nullptr; + _view = nullptr; } void MacWindow::CheckForResize(float width, float height) @@ -699,7 +787,6 @@ void MacWindow::SetIsMouseOver(bool value) // Refresh cursor typet SetCursor(CursorType::Default); SetCursor(cursor); - } else { @@ -714,6 +801,22 @@ void* MacWindow::GetNativePtr() const return _window; } +void MacWindow::OnUpdate(float dt) +{ + if (IsMouseTracking()) + { + // Keep sending mouse movement events no matter if window has focus + Float2 mousePos = Platform::GetMousePosition(); + if (_mouseTrackPos != mousePos) + { + _mouseTrackPos = mousePos; + Input::Mouse->OnMouseMove(mousePos, this); + } + } + + WindowBase::OnUpdate(dt); +} + void MacWindow::Show() { if (!_visible) @@ -728,7 +831,10 @@ void MacWindow::Show() // Show NSWindow* window = (NSWindow*)_window; - [window makeKeyAndOrderFront:window]; + if (_settings.AllowInput) + [window makeKeyAndOrderFront:window]; + else + [window orderFront:window]; if (_settings.ActivateWhenFirstShown) [NSApp activateIgnoringOtherApps:YES]; _focused = true; @@ -746,7 +852,7 @@ void MacWindow::Hide() // Hide NSWindow* window = (NSWindow*)_window; - [window orderOut:nil]; + [window orderOut:window]; // Base WindowBase::Hide(); @@ -782,14 +888,9 @@ void MacWindow::Restore() [window zoom:nil]; } -bool MacWindow::IsClosed() const -{ - return _window != nullptr; -} - bool MacWindow::IsForegroundWindow() const { - return Platform::GetHasFocus() && IsFocused(); + return IsFocused() && Platform::GetHasFocus(); } void MacWindow::BringToFront(bool force) @@ -808,14 +909,13 @@ void MacWindow::SetClientBounds(const Rectangle& clientArea) NSWindow* window = (NSWindow*)_window; if (!window) return; + const float screenScale = MacPlatform::ScreenScale; + NSRect oldRect = [window frame]; - NSRect newRect = NSMakeRect(0, 0, clientArea.Size.X, clientArea.Size.Y); + NSRect newRect = NSMakeRect(0, 0, clientArea.Size.X / screenScale, clientArea.Size.Y / screenScale); newRect = [window frameRectForContentRect:newRect]; - //newRect.origin.x = oldRect.origin.x; - //newRect.origin.y = NSMaxY(oldRect) - newRect.size.height; - - Float2 pos = AppleUtils::PosToCoca(clientArea.Location); + Float2 pos = AppleUtils::PosToCoca(clientArea.Location) / screenScale; Float2 titleSize = GetWindowTitleSize(this); newRect.origin.x = pos.X + titleSize.X; newRect.origin.y = pos.Y - newRect.size.height + titleSize.Y; @@ -909,8 +1009,63 @@ void MacWindow::SetTitle(const StringView& title) DragDropEffect MacWindow::DoDragDrop(const StringView& data) { - // TODO: implement using beginDraggingSession and NSDraggingSource - return DragDropEffect::None; + NSWindow* window = (NSWindow*)_window; + MacViewImpl* view = (MacViewImpl*)_view; + _dragText = data; + + // Create mouse drag event + NSEvent* event = [NSEvent + mouseEventWithType:NSEventTypeLeftMouseDragged + location:window.mouseLocationOutsideOfEventStream + modifierFlags:0 + timestamp:NSApp.currentEvent.timestamp + windowNumber:window.windowNumber + context:nil + eventNumber:0 + clickCount:1 + pressure:1.0]; + + // Create drag item + NSPasteboardItem* pasteItem = [NSPasteboardItem new]; + [pasteItem setDataProvider:view forTypes:[NSArray arrayWithObjects:NSPasteboardTypeString, nil]]; + NSDraggingItem* dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:pasteItem]; + [dragItem setDraggingFrame:NSMakeRect(event.locationInWindow.x, event.locationInWindow.y, 1, 1) contents:nil]; + + // Start dragging session + NSDraggingSession* draggingSession = [view beginDraggingSessionWithItems:[NSArray arrayWithObject:dragItem] event:event source:view]; + DragDropEffect result = DragDropEffect::None; + +#if USE_EDITOR + // Create background worker that will keep updating GUI (perform rendering) + MacDragLocker.Lock(); + ASSERT(!MacDragSession && !MacDragJob); + MacDragSession = draggingSession; + MacDragJob = New(); + Task::StartNew(MacDragJob); + MacDragLocker.Unlock(); + while (MacDragJob->GetState() == TaskState::Queued) + Platform::Sleep(1); + // TODO: maybe wait for the drag end to return result? +#endif + + return result; +} + +void MacWindow::StartTrackingMouse(bool useMouseScreenOffset) +{ + if (_isTrackingMouse || !_window) + return; + _isTrackingMouse = true; + _trackingMouseOffset = Float2::Zero; + _isUsingMouseOffset = useMouseScreenOffset; + _mouseTrackPos = Float2::Minimum; +} + +void MacWindow::EndTrackingMouse() +{ + if (!_isTrackingMouse || !_window) + return; + _isTrackingMouse = false; } void MacWindow::SetCursor(CursorType type) diff --git a/Source/Engine/Platform/Mac/MacWindow.h b/Source/Engine/Platform/Mac/MacWindow.h index 8850f80ba..ccd43b354 100644 --- a/Source/Engine/Platform/Mac/MacWindow.h +++ b/Source/Engine/Platform/Mac/MacWindow.h @@ -6,6 +6,7 @@ #include "Engine/Platform/Base/WindowBase.h" #include "Engine/Platform/Platform.h" +#include "Engine/Core/Math/Vector2.h" /// /// Implementation of the window class for Mac platform. @@ -13,28 +14,32 @@ class FLAXENGINE_API MacWindow : public WindowBase { private: - - void* _window; + void* _window = nullptr; + void* _view = nullptr; bool _isMouseOver = false; + Float2 _mouseTrackPos = Float2::Minimum; + String _dragText; public: - MacWindow(const CreateWindowSettings& settings); ~MacWindow(); void CheckForResize(float width, float height); void SetIsMouseOver(bool value); + const String& GetDragText() const + { + return _dragText; + } public: - // [WindowBase] void* GetNativePtr() const override; + void OnUpdate(float dt) override; void Show() override; void Hide() override; void Minimize() override; void Maximize() override; void Restore() override; - bool IsClosed() const override; bool IsForegroundWindow() const override; void BringToFront(bool force = false) override; void SetClientBounds(const Rectangle& clientArea) override; @@ -50,6 +55,8 @@ public: void Focus() override; void SetTitle(const StringView& title) override; DragDropEffect DoDragDrop(const StringView& data) override; + void StartTrackingMouse(bool useMouseScreenOffset) override; + void EndTrackingMouse() override; void SetCursor(CursorType type) override; }; diff --git a/Source/Engine/Platform/Unix/UnixNetwork.cpp b/Source/Engine/Platform/Unix/UnixNetwork.cpp index 5ffdb2c01..ae2d8d9f2 100644 --- a/Source/Engine/Platform/Unix/UnixNetwork.cpp +++ b/Source/Engine/Platform/Unix/UnixNetwork.cpp @@ -96,7 +96,11 @@ static bool CreateEndPointFromAddr(sockaddr* addr, NetworkEndPoint& endPoint) return true; } char strPort[6]; +#if __APPLE__ + snprintf(strPort, sizeof(strPort), "%d", port); +#else sprintf(strPort, "%d", port); +#endif endPoint.IPVersion = addr->sa_family == AF_INET6 ? NetworkIPVersion::IPv6 : NetworkIPVersion::IPv4; memcpy(endPoint.Data, addr, size); return false; diff --git a/Source/Engine/Platform/Unix/UnixThread.cpp b/Source/Engine/Platform/Unix/UnixThread.cpp index f662bdba8..29dc5f881 100644 --- a/Source/Engine/Platform/Unix/UnixThread.cpp +++ b/Source/Engine/Platform/Unix/UnixThread.cpp @@ -26,6 +26,12 @@ int32 UnixThread::Start(pthread_attr_t& attr) void* UnixThread::ThreadProc(void* pThis) { auto thread = (UnixThread*)pThis; +#if PLATFORM_APPLE_FAMILY + // Apple doesn't support creating named thread so assign name here + { + pthread_setname_np(StringAnsi(thread->GetName()).Get()); + } +#endif const int32 exitCode = thread->Run(); return (void*)(uintptr)exitCode; } diff --git a/Source/Engine/Platform/Win32/Win32CriticalSection.h b/Source/Engine/Platform/Win32/Win32CriticalSection.h index 2c103ef85..95d85efac 100644 --- a/Source/Engine/Platform/Win32/Win32CriticalSection.h +++ b/Source/Engine/Platform/Win32/Win32CriticalSection.h @@ -16,16 +16,13 @@ class FLAXENGINE_API Win32CriticalSection friend Win32ConditionVariable; private: - mutable Windows::CRITICAL_SECTION _criticalSection; private: - Win32CriticalSection(const Win32CriticalSection&); Win32CriticalSection& operator=(const Win32CriticalSection&); public: - /// /// Initializes a new instance of the class. /// @@ -43,17 +40,12 @@ public: } public: - /// /// Locks the critical section. /// void Lock() const { - // Spin first before entering critical section, causing ring-0 transition and context switch - if (Windows::TryEnterCriticalSection(&_criticalSection) == 0) - { - Windows::EnterCriticalSection(&_criticalSection); - } + Windows::EnterCriticalSection(&_criticalSection); } /// diff --git a/Source/Engine/Platform/Windows/WindowsFileSystem.cpp b/Source/Engine/Platform/Windows/WindowsFileSystem.cpp index 38384f05f..d07c333e0 100644 --- a/Source/Engine/Platform/Windows/WindowsFileSystem.cpp +++ b/Source/Engine/Platform/Windows/WindowsFileSystem.cpp @@ -8,6 +8,7 @@ #include "Engine/Platform/Windows/ComPtr.h" #include "Engine/Platform/CreateProcessSettings.h" #include "Engine/Core/Types/StringView.h" +#include "Engine/Core/Collections/Array.h" #include "../Win32/IncludeWindowsHeaders.h" // Hack this stuff (the problem is that GDI has function named Rectangle -> like one of the Flax core types) diff --git a/Source/Engine/Platform/Windows/WindowsPlatform.cpp b/Source/Engine/Platform/Windows/WindowsPlatform.cpp index c5bc44ab9..cc2e52725 100644 --- a/Source/Engine/Platform/Windows/WindowsPlatform.cpp +++ b/Source/Engine/Platform/Windows/WindowsPlatform.cpp @@ -451,6 +451,7 @@ DialogResult MessageBox::Show(Window* parent, const StringView& text, const Stri default: break; } + flags |= MB_TASKMODAL; // Show dialog int result = MessageBoxW(parent ? static_cast(parent->GetNativePtr()) : nullptr, String(text).GetText(), String(caption).GetText(), flags); @@ -1200,8 +1201,8 @@ void* WindowsPlatform::LoadLibrary(const Char* filename) folder = StringView::Empty; if (folder.HasChars()) { - String folderNullTerminated(folder); - SetDllDirectoryW(folderNullTerminated.Get()); + const String folderNullTerminated(folder); + AddDllDirectory(folderNullTerminated.Get()); } // Avoiding windows dialog boxes if missing @@ -1209,7 +1210,10 @@ void* WindowsPlatform::LoadLibrary(const Char* filename) DWORD prevErrorMode = 0; const BOOL hasErrorMode = SetThreadErrorMode(errorMode, &prevErrorMode); - // Load the DLL + // Ensure that dll is properly searched + SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_SYSTEM32 | LOAD_LIBRARY_SEARCH_USER_DIRS); + + // Load the library void* handle = ::LoadLibraryW(filename); if (!handle) { @@ -1220,10 +1224,6 @@ void* WindowsPlatform::LoadLibrary(const Char* filename) { SetThreadErrorMode(prevErrorMode, nullptr); } - if (folder.HasChars()) - { - SetDllDirectoryW(nullptr); - } #if CRASH_LOG_ENABLE // Refresh modules info during next stack trace collecting to have valid debug symbols information diff --git a/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp b/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp deleted file mode 100644 index b6e636697..000000000 --- a/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp +++ /dev/null @@ -1,698 +0,0 @@ -// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. - -#if PLATFORM_WINDOWS - -#include "WindowsWindow.h" - -#if USE_EDITOR - -#include "Engine/Core/Collections/Array.h" -#include "Engine/Engine/Engine.h" -#include "Engine/Platform/IGuiData.h" -#include "Engine/Input/Input.h" -#include "Engine/Input/Mouse.h" -#include "Engine/Threading/ThreadPoolTask.h" -#include "Engine/Threading/ThreadPool.h" -#include "Engine/Scripting/Scripting.h" -#include "Engine/Scripting/ManagedCLR/MDomain.h" -#include "../Win32/IncludeWindowsHeaders.h" -#include -#include - -HGLOBAL duplicateGlobalMem(HGLOBAL hMem) -{ - auto len = GlobalSize(hMem); - auto source = GlobalLock(hMem); - auto dest = GlobalAlloc(GMEM_FIXED, len); - Platform::MemoryCopy(dest, source, len); - GlobalUnlock(hMem); - return dest; -} - -DWORD dropEffect2OleEnum(DragDropEffect effect) -{ - DWORD result; - switch (effect) - { - case DragDropEffect::None: - result = DROPEFFECT_NONE; - break; - case DragDropEffect::Copy: - result = DROPEFFECT_COPY; - break; - case DragDropEffect::Move: - result = DROPEFFECT_MOVE; - break; - case DragDropEffect::Link: - result = DROPEFFECT_LINK; - break; - default: - result = DROPEFFECT_NONE; - break; - } - return result; -} - -DragDropEffect dropEffectFromOleEnum(DWORD effect) -{ - DragDropEffect result; - switch (effect) - { - case DROPEFFECT_NONE: - result = DragDropEffect::None; - break; - case DROPEFFECT_COPY: - result = DragDropEffect::Copy; - break; - case DROPEFFECT_MOVE: - result = DragDropEffect::Move; - break; - case DROPEFFECT_LINK: - result = DragDropEffect::Link; - break; - default: - result = DragDropEffect::None; - break; - } - return result; -} - -HANDLE StringToHandle(const StringView& str) -{ - // Allocate and lock a global memory buffer. - // Make it fixed data so we don't have to use GlobalLock - const int32 length = str.Length(); - char* ptr = static_cast(GlobalAlloc(GMEM_FIXED, length + 1)); - - // Copy the string into the buffer as ANSI text - StringUtils::ConvertUTF162ANSI(str.Get(), ptr, length); - ptr[length] = '\0'; - - return ptr; -} - -void DeepCopyFormatEtc(FORMATETC* dest, FORMATETC* source) -{ - // Copy the source FORMATETC into dest - *dest = *source; - - if (source->ptd) - { - // Allocate memory for the DVTARGETDEVICE if necessary - dest->ptd = static_cast(CoTaskMemAlloc(sizeof(DVTARGETDEVICE))); - - // Copy the contents of the source DVTARGETDEVICE into dest->ptd - *(dest->ptd) = *(source->ptd); - } -} - -HRESULT CreateEnumFormatEtc(UINT nNumFormats, FORMATETC* pFormatEtc, IEnumFORMATETC** ppEnumFormatEtc); - -/// -/// GUI data for Windows platform -/// -class WindowsGuiData : public IGuiData -{ -private: - - Type _type; - Array _data; - -public: - - /// - /// Init - /// - WindowsGuiData() - : _type(Type::Unknown) - , _data(1) - { - } - -public: - - /// - /// Init from Ole IDataObject - /// - /// Object - void Init(IDataObject* pDataObj) - { - // Temporary data - FORMATETC fmtetc; - STGMEDIUM stgmed; - - // Clear - _type = Type::Unknown; - _data.Clear(); - - // Check type - fmtetc = { CF_TEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; - if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK) - { - // Text - _type = Type::Text; - - // Get data - char* text = static_cast(GlobalLock(stgmed.hGlobal)); - _data.Add(String(text)); - GlobalUnlock(stgmed.hGlobal); - ReleaseStgMedium(&stgmed); - } - else - { - fmtetc = { CF_UNICODETEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; - if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK) - { - // Unicode Text - _type = Type::Text; - - // Get data - Char* text = static_cast(GlobalLock(stgmed.hGlobal)); - _data.Add(String(text)); - GlobalUnlock(stgmed.hGlobal); - ReleaseStgMedium(&stgmed); - } - else - { - fmtetc = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; - if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK) - { - // Files - _type = Type::Files; - - // Get data - Char item[MAX_PATH]; - HDROP hdrop = static_cast(GlobalLock(stgmed.hGlobal)); - UINT filesCount = DragQueryFileW(hdrop, 0xFFFFFFFF, nullptr, 0); - for (UINT i = 0; i < filesCount; i++) - { - if (DragQueryFileW(hdrop, i, item, MAX_PATH) != 0) - { - _data.Add(String(item)); - } - } - GlobalUnlock(stgmed.hGlobal); - ReleaseStgMedium(&stgmed); - } - } - } - } - -public: - - // [IGuiData] - Type GetType() const override - { - return _type; - } - - String GetAsText() const override - { - String result; - if (_type == Type::Text) - { - result = _data[0]; - } - return result; - } - - void GetAsFiles(Array* files) const override - { - if (_type == Type::Files) - { - files->Add(_data); - } - } -}; - -/// -/// Tool class for Windows Ole support -/// -class WindowsEnumFormatEtc : public IEnumFORMATETC -{ -private: - - ULONG _refCount; - ULONG _index; - ULONG _formatsCount; - FORMATETC* _formatEtc; - -public: - - WindowsEnumFormatEtc(FORMATETC* pFormatEtc, int32 nNumFormats) - : _refCount(1) - , _index(0) - , _formatsCount(nNumFormats) - , _formatEtc(nullptr) - { - // Allocate memory - _formatEtc = new FORMATETC[nNumFormats]; - - // Copy the FORMATETC structures - for (int32 i = 0; i < nNumFormats; i++) - { - DeepCopyFormatEtc(&_formatEtc[i], &pFormatEtc[i]); - } - } - - ~WindowsEnumFormatEtc() - { - if (_formatEtc) - { - for (uint32 i = 0; i < _formatsCount; i++) - { - if (_formatEtc[i].ptd) - { - CoTaskMemFree(_formatEtc[i].ptd); - } - } - - delete[] _formatEtc; - } - } - -public: - - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, _COM_Outptr_ void __RPC_FAR *__RPC_FAR * ppvObject) override - { - // Check to see what interface has been requested - if (riid == IID_IEnumFORMATETC || riid == IID_IUnknown) - { - AddRef(); - *ppvObject = this; - return S_OK; - } - - // No interface - *ppvObject = nullptr; - return E_NOINTERFACE; - } - - ULONG STDMETHODCALLTYPE AddRef() override - { - _InterlockedIncrement(&_refCount); - return _refCount; - } - - ULONG STDMETHODCALLTYPE Release() override - { - ULONG ulRefCount = _InterlockedDecrement(&_refCount); - if (_refCount == 0) - { - delete this; - } - return ulRefCount; - } - - // [IEnumFormatEtc] - HRESULT STDMETHODCALLTYPE Next(ULONG celt, FORMATETC* pFormatEtc, ULONG* pceltFetched) override - { - ULONG copied = 0; - - // validate arguments - if (celt == 0 || pFormatEtc == nullptr) - return E_INVALIDARG; - - // copy FORMATETC structures into caller's buffer - while (_index < _formatsCount && copied < celt) - { - DeepCopyFormatEtc(&pFormatEtc[copied], &_formatEtc[_index]); - copied++; - _index++; - } - - // store result - if (pceltFetched != nullptr) - *pceltFetched = copied; - - // did we copy all that was requested? - return (copied == celt) ? S_OK : S_FALSE; - } - - HRESULT STDMETHODCALLTYPE Skip(ULONG celt) override - { - _index += celt; - return (_index <= _formatsCount) ? S_OK : S_FALSE; - } - - HRESULT STDMETHODCALLTYPE Reset() override - { - _index = 0; - return S_OK; - } - - HRESULT STDMETHODCALLTYPE Clone(IEnumFORMATETC** ppEnumFormatEtc) override - { - HRESULT result; - - // Make a duplicate enumerator - result = CreateEnumFormatEtc(_formatsCount, _formatEtc, ppEnumFormatEtc); - - if (result == S_OK) - { - // Manually set the index state - static_cast(*ppEnumFormatEtc)->_index = _index; - } - - return result; - } -}; - -HRESULT CreateEnumFormatEtc(UINT nNumFormats, FORMATETC* pFormatEtc, IEnumFORMATETC** ppEnumFormatEtc) -{ - if (nNumFormats == 0 || pFormatEtc == nullptr || ppEnumFormatEtc == nullptr) - return E_INVALIDARG; - - *ppEnumFormatEtc = new WindowsEnumFormatEtc(pFormatEtc, nNumFormats); - - return (*ppEnumFormatEtc) ? S_OK : E_OUTOFMEMORY; -} - -/// -/// Drag drop source and data container for Ole -/// -class WindowsDragSource : public IDataObject, public IDropSource -{ -private: - - ULONG _refCount; - int32 _formatsCount; - FORMATETC* _formatEtc; - STGMEDIUM* _stgMedium; - -public: - - WindowsDragSource(FORMATETC* fmtetc, STGMEDIUM* stgmed, int32 count) - : _refCount(1) - , _formatsCount(count) - , _formatEtc(nullptr) - , _stgMedium(nullptr) - { - // Allocate memory - _formatEtc = new FORMATETC[count]; - _stgMedium = new STGMEDIUM[count]; - - // Copy descriptors - for (int32 i = 0; i < count; i++) - { - _formatEtc[i] = fmtetc[i]; - _stgMedium[i] = stgmed[i]; - } - } - - virtual ~WindowsDragSource() - { - if (_formatEtc) - delete[] _formatEtc; - if (_stgMedium) - delete[] _stgMedium; - } - -public: - - // [IUnknown] - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, _COM_Outptr_ void __RPC_FAR *__RPC_FAR * ppvObject) override - { - // Check to see what interface has been requested - if (riid == IID_IDataObject || riid == IID_IUnknown || riid == IID_IDropSource) - { - AddRef(); - *ppvObject = this; - return S_OK; - } - - // No interface - *ppvObject = nullptr; - return E_NOINTERFACE; - } - - ULONG STDMETHODCALLTYPE AddRef() override - { - _InterlockedIncrement(&_refCount); - return _refCount; - } - - ULONG STDMETHODCALLTYPE Release() override - { - ULONG ulRefCount = _InterlockedDecrement(&_refCount); - if (_refCount == 0) - { - delete this; - } - return ulRefCount; - } - - // [IDropSource] - HRESULT STDMETHODCALLTYPE QueryContinueDrag(_In_ BOOL fEscapePressed, _In_ DWORD grfKeyState) override - { - // If the Escape key has been pressed since the last call, cancel the drop - if (fEscapePressed == TRUE || grfKeyState & MK_RBUTTON) - return DRAGDROP_S_CANCEL; - - // If the LeftMouse button has been released, then do the drop! - if ((grfKeyState & MK_LBUTTON) == 0) - return DRAGDROP_S_DROP; - - // Continue with the drag-drop - return S_OK; - } - - HRESULT STDMETHODCALLTYPE GiveFeedback(_In_ DWORD dwEffect) override - { - // TODO: allow to use custom mouse cursor during drop and drag operation - return DRAGDROP_S_USEDEFAULTCURSORS; - } - - // [IDataObject] - HRESULT STDMETHODCALLTYPE GetData(_In_ FORMATETC* pformatetcIn, _Out_ STGMEDIUM* pmedium) override - { - if (pformatetcIn == nullptr || pmedium == nullptr) - return E_INVALIDARG; - - // Try to match the specified FORMATETC with one of our supported formats - int32 index = lookupFormatEtc(pformatetcIn); - if (index == INVALID_INDEX) - return DV_E_FORMATETC; - - // Found a match - transfer data into supplied storage medium - pmedium->tymed = _formatEtc[index].tymed; - pmedium->pUnkForRelease = nullptr; - - // Copy the data into the caller's storage medium - switch (_formatEtc[index].tymed) - { - case TYMED_HGLOBAL: - pmedium->hGlobal = duplicateGlobalMem(_stgMedium[index].hGlobal); - break; - - default: - return DV_E_FORMATETC; - } - - return S_OK; - } - - HRESULT STDMETHODCALLTYPE GetDataHere(_In_ FORMATETC* pformatetc, _Inout_ STGMEDIUM* pmedium) override - { - return DATA_E_FORMATETC; - } - - HRESULT STDMETHODCALLTYPE QueryGetData(__RPC__in_opt FORMATETC* pformatetc) override - { - return lookupFormatEtc(pformatetc) == INVALID_INDEX ? DV_E_FORMATETC : S_OK; - } - - HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(__RPC__in_opt FORMATETC* pformatectIn, __RPC__out FORMATETC* pformatetcOut) override - { - // Apparently we have to set this field to NULL even though we don't do anything else - pformatetcOut->ptd = nullptr; - return E_NOTIMPL; - } - - HRESULT STDMETHODCALLTYPE SetData(_In_ FORMATETC* pformatetc, _In_ STGMEDIUM* pmedium, BOOL fRelease) override - { - return E_NOTIMPL; - } - - HRESULT STDMETHODCALLTYPE EnumFormatEtc(DWORD dwDirection, __RPC__deref_out_opt IEnumFORMATETC** ppenumFormatEtc) override - { - // Only the get direction is supported for OLE - if (dwDirection == DATADIR_GET) - { - // TODO: use SHCreateStdEnumFmtEtc API call - return CreateEnumFormatEtc(_formatsCount, _formatEtc, ppenumFormatEtc); - } - - // The direction specified is not supported for drag+drop - return E_NOTIMPL; - } - - HRESULT STDMETHODCALLTYPE DAdvise(__RPC__in FORMATETC* pformatetc, DWORD advf, __RPC__in_opt IAdviseSink* pAdvSink, __RPC__out DWORD* pdwConnection) override - { - return OLE_E_ADVISENOTSUPPORTED; - } - - HRESULT STDMETHODCALLTYPE DUnadvise(DWORD dwConnection) override - { - return OLE_E_ADVISENOTSUPPORTED; - } - - HRESULT STDMETHODCALLTYPE EnumDAdvise(__RPC__deref_out_opt IEnumSTATDATA** ppenumAdvise) override - { - return OLE_E_ADVISENOTSUPPORTED; - } - -private: - - int32 lookupFormatEtc(FORMATETC* pFormatEtc) const - { - // Check each of our formats in turn to see if one matches - for (int32 i = 0; i < _formatsCount; i++) - { - if ((_formatEtc[i].tymed & pFormatEtc->tymed) && - _formatEtc[i].cfFormat == pFormatEtc->cfFormat && - _formatEtc[i].dwAspect == pFormatEtc->dwAspect) - { - // Return index of stored format - return i; - } - } - - // Format not found - return INVALID_INDEX; - } -}; - -WindowsGuiData GuiDragDropData; - -/// -/// Async DoDragDrop helper (used for rendering frames during main thread stall). -/// -class DoDragDropJob : public ThreadPoolTask -{ -public: - - int64 ExitFlag = 0; - - // [ThreadPoolTask] - bool Run() override - { - Scripting::GetScriptsDomain()->Dispatch(); - while (Platform::AtomicRead(&ExitFlag) == 0) - { - Engine::OnDraw(); - Platform::Sleep(20); - } - return false; - } -}; - -DragDropEffect WindowsWindow::DoDragDrop(const StringView& data) -{ - // Create background worker that will keep updating GUI (perform rendering) - const auto task = New(); - Task::StartNew(task); - while (task->GetState() == TaskState::Queued) - { - Platform::Sleep(1); - } - - // Create descriptors - FORMATETC fmtetc = { CF_TEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; - STGMEDIUM stgmed = { TYMED_HGLOBAL, { nullptr }, nullptr }; - - // Create a HGLOBAL inside the storage medium - stgmed.hGlobal = StringToHandle(data); - - // Create drop source - auto dropSource = new WindowsDragSource(&fmtetc, &stgmed, 1); - - // Do the drag drop operation - DWORD dwEffect; - HRESULT result = ::DoDragDrop(dropSource, dropSource, DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK | DROPEFFECT_SCROLL, &dwEffect); - - // Wait for job end - Platform::AtomicStore(&task->ExitFlag, 1); - task->Wait(); - - // Release allocated data - dropSource->Release(); - ReleaseStgMedium(&stgmed); - - // Fix hanging mouse state (Windows doesn't send WM_LBUTTONUP when we end the drag and drop) - if (Input::GetMouseButton(MouseButton::Left)) - { - ::POINT point; - ::GetCursorPos(&point); - Input::Mouse->OnMouseUp(Float2((float)point.x, (float)point.y), MouseButton::Left, this); - } - - return SUCCEEDED(result) ? dropEffectFromOleEnum(dwEffect) : DragDropEffect::None; -} - -HRESULT WindowsWindow::DragEnter(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) -{ - // Call GUI - POINT p = { pt.x, pt.y }; - ::ScreenToClient(_handle, &p); - GuiDragDropData.Init((IDataObject*)pDataObj); - DragDropEffect effect = DragDropEffect::None; - OnDragEnter(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); - - // Focus - Focus(); - - // Translate effect into Ole Api type - *pdwEffect = dropEffect2OleEnum(effect); - - return S_OK; -} - -HRESULT WindowsWindow::DragOver(Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) -{ - // Call GUI - POINT p = { pt.x, pt.y }; - ::ScreenToClient(_handle, &p); - DragDropEffect effect = DragDropEffect::None; - OnDragOver(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); - - // Translate effect into Ole Api type - *pdwEffect = dropEffect2OleEnum(effect); - - return S_OK; -} - -HRESULT WindowsWindow::DragLeave() -{ - // Call GUI - OnDragLeave(); - - return S_OK; -} - -HRESULT WindowsWindow::Drop(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) -{ - // Call GUI - POINT p = { pt.x, pt.y }; - ::ScreenToClient(_handle, &p); - GuiDragDropData.Init((IDataObject*)pDataObj); - DragDropEffect effect = DragDropEffect::None; - OnDragDrop(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); - - // Translate effect into Ole Api type - *pdwEffect = dropEffect2OleEnum(effect); - - return S_OK; -} - -#else - -DragDropEffect WindowsWindow::DoDragDrop(const StringView& data) -{ - // Not supported - return DragDropEffect::None; -} - -#endif - -#endif diff --git a/Source/Engine/Platform/Windows/WindowsWindow.cpp b/Source/Engine/Platform/Windows/WindowsWindow.cpp index fb9fbe09d..181077125 100644 --- a/Source/Engine/Platform/Windows/WindowsWindow.cpp +++ b/Source/Engine/Platform/Windows/WindowsWindow.cpp @@ -10,8 +10,19 @@ #include "Engine/Graphics/GPUSwapChain.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Graphics/GPUDevice.h" +#if USE_EDITOR +#include "Engine/Core/Collections/Array.h" +#include "Engine/Platform/IGuiData.h" +#include "Engine/Platform/Base/DragDropHelper.h" +#include "Engine/Input/Input.h" +#include "Engine/Input/Mouse.h" +#endif #include "../Win32/IncludeWindowsHeaders.h" #include +#if USE_EDITOR +#include +#include +#endif #define DefaultDPI 96 @@ -139,7 +150,7 @@ WindowsWindow::WindowsWindow(const CreateWindowSettings& settings) const HMODULE user32Dll = LoadLibraryW(L"user32.dll"); if (user32Dll) { - typedef UINT (STDAPICALLTYPE* GetDpiForWindowProc)(HWND hwnd); + typedef UINT (STDAPICALLTYPE*GetDpiForWindowProc)(HWND hwnd); const GetDpiForWindowProc getDpiForWindowProc = (GetDpiForWindowProc)GetProcAddress(user32Dll, "GetDpiForWindow"); if (getDpiForWindowProc) { @@ -262,7 +273,7 @@ void WindowsWindow::Maximize() void WindowsWindow::SetBorderless(bool isBorderless, bool maximized) { ASSERT(HasHWND()); - + if (IsFullscreen()) SetIsFullscreen(false); @@ -278,7 +289,7 @@ void WindowsWindow::SetBorderless(bool isBorderless, bool maximized) { LONG lStyle = GetWindowLong(_handle, GWL_STYLE); lStyle &= ~(WS_THICKFRAME | WS_SYSMENU | WS_OVERLAPPED | WS_BORDER | WS_CAPTION); - lStyle |= WS_POPUP; + lStyle |= WS_POPUP; lStyle |= WS_CLIPCHILDREN | WS_CLIPSIBLINGS; #if WINDOWS_USE_NEW_BORDER_LESS if (_settings.IsRegularWindow) @@ -289,8 +300,8 @@ void WindowsWindow::SetBorderless(bool isBorderless, bool maximized) #endif SetWindowLong(_handle, GWL_STYLE, lStyle); - SetWindowPos(_handle, HWND_TOP, 0, 0,0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); - + SetWindowPos(_handle, HWND_TOP, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + if (maximized) { ShowWindow(_handle, SW_SHOWMAXIMIZED); @@ -311,10 +322,10 @@ void WindowsWindow::SetBorderless(bool isBorderless, bool maximized) if (_settings.HasSizingFrame) lStyle |= WS_THICKFRAME; lStyle |= WS_OVERLAPPED | WS_SYSMENU | WS_BORDER | WS_CAPTION; - + SetWindowLong(_handle, GWL_STYLE, lStyle); - SetWindowPos(_handle, nullptr, 0, 0, (int)_settings.Size.X, (int)_settings.Size.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); - + SetWindowPos(_handle, nullptr, 0, 0, (int)_settings.Size.X, (int)_settings.Size.Y, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); + if (maximized) { Maximize(); @@ -532,6 +543,12 @@ Float2 WindowsWindow::ClientToScreen(const Float2& clientPos) const { ASSERT(HasHWND()); + if (_minimized) + { + // Return cached position when window is not on screen + return _minimizedScreenPosition + clientPos; + } + POINT p; p.x = static_cast(clientPos.X); p.y = static_cast(clientPos.Y); @@ -721,7 +738,7 @@ void WindowsWindow::CheckForWindowResize() MONITORINFO monitorInfo; monitorInfo.cbSize = sizeof(MONITORINFO); GetMonitorInfoW(monitor, &monitorInfo); - + auto cwidth = monitorInfo.rcWork.right - monitorInfo.rcWork.left; auto cheight = monitorInfo.rcWork.bottom - monitorInfo.rcWork.top; if (width > cwidth && height > cheight) @@ -1050,22 +1067,23 @@ LRESULT WindowsWindow::WndProc(UINT msg, WPARAM wParam, LPARAM lParam) case WM_GETMINMAXINFO: { const auto minMax = reinterpret_cast(lParam); - - int32 borderWidth = 0, borderHeight = 0; - if (_settings.HasBorder) - { - const DWORD windowStyle = GetWindowLongW(_handle, GWL_STYLE); - const DWORD windowExStyle = GetWindowLongW(_handle, GWL_EXSTYLE); - RECT borderRect = { 0, 0, 0, 0 }; - AdjustWindowRectEx(&borderRect, windowStyle, false, windowExStyle); - borderWidth = borderRect.right - borderRect.left; - borderHeight = borderRect.bottom - borderRect.top; - } - minMax->ptMinTrackSize.x = (int32)_settings.MinimumSize.X; minMax->ptMinTrackSize.y = (int32)_settings.MinimumSize.Y; - minMax->ptMaxTrackSize.x = (int32)_settings.MaximumSize.X + borderWidth; - minMax->ptMaxTrackSize.y = (int32)_settings.MaximumSize.Y + borderHeight; + if (_settings.MaximumSize.SumValues() > 0) + { + int32 borderWidth = 0, borderHeight = 0; + if (_settings.HasBorder) + { + const DWORD windowStyle = GetWindowLongW(_handle, GWL_STYLE); + const DWORD windowExStyle = GetWindowLongW(_handle, GWL_EXSTYLE); + RECT borderRect = { 0, 0, 0, 0 }; + AdjustWindowRectEx(&borderRect, windowStyle, false, windowExStyle); + borderWidth = borderRect.right - borderRect.left; + borderHeight = borderRect.bottom - borderRect.top; + } + minMax->ptMaxTrackSize.x = (int32)_settings.MaximumSize.X + borderWidth; + minMax->ptMaxTrackSize.y = (int32)_settings.MaximumSize.Y + borderHeight; + } // Include Windows task bar size into maximized tool window WINDOWPLACEMENT e; @@ -1109,6 +1127,28 @@ LRESULT WindowsWindow::WndProc(UINT msg, WPARAM wParam, LPARAM lParam) { if (SIZE_MINIMIZED == wParam) { + // Get the minimized window position in workspace coordinates + WINDOWPLACEMENT placement; + placement.length = sizeof(WINDOWPLACEMENT); + GetWindowPlacement(_handle, &placement); + + // Calculate client offsets from window borders and title bar + RECT winRect = { 0, 0, static_cast(_clientSize.X), static_cast(_clientSize.Y) }; + LONG style = GetWindowLong(_handle, GWL_STYLE); + LONG exStyle = GetWindowLong(_handle, GWL_EXSTYLE); + AdjustWindowRectEx(&winRect, style, FALSE, exStyle); + + // Calculate monitor offsets from taskbar position + const HMONITOR monitor = MonitorFromWindow(_handle, MONITOR_DEFAULTTONEAREST); + MONITORINFO monitorInfo; + monitorInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfoW(monitor, &monitorInfo); + + // Convert the workspace coordinates to screen space and store it + _minimizedScreenPosition = Float2( + static_cast(placement.rcNormalPosition.left + monitorInfo.rcWork.left - monitorInfo.rcMonitor.left - winRect.left), + static_cast(placement.rcNormalPosition.top + monitorInfo.rcWork.top - monitorInfo.rcMonitor.top - winRect.top)); + _minimized = true; _maximized = false; } @@ -1268,4 +1308,610 @@ LRESULT WindowsWindow::WndProc(UINT msg, WPARAM wParam, LPARAM lParam) return DefWindowProc(_handle, msg, wParam, lParam); } +#if USE_EDITOR + +HGLOBAL duplicateGlobalMem(HGLOBAL hMem) +{ + auto len = GlobalSize(hMem); + auto source = GlobalLock(hMem); + auto dest = GlobalAlloc(GMEM_FIXED, len); + Platform::MemoryCopy(dest, source, len); + GlobalUnlock(hMem); + return dest; +} + +DWORD dropEffect2OleEnum(DragDropEffect effect) +{ + DWORD result; + switch (effect) + { + case DragDropEffect::None: + result = DROPEFFECT_NONE; + break; + case DragDropEffect::Copy: + result = DROPEFFECT_COPY; + break; + case DragDropEffect::Move: + result = DROPEFFECT_MOVE; + break; + case DragDropEffect::Link: + result = DROPEFFECT_LINK; + break; + default: + result = DROPEFFECT_NONE; + break; + } + return result; +} + +DragDropEffect dropEffectFromOleEnum(DWORD effect) +{ + DragDropEffect result; + switch (effect) + { + case DROPEFFECT_NONE: + result = DragDropEffect::None; + break; + case DROPEFFECT_COPY: + result = DragDropEffect::Copy; + break; + case DROPEFFECT_MOVE: + result = DragDropEffect::Move; + break; + case DROPEFFECT_LINK: + result = DragDropEffect::Link; + break; + default: + result = DragDropEffect::None; + break; + } + return result; +} + +HANDLE StringToHandle(const StringView& str) +{ + // Allocate and lock a global memory buffer. + // Make it fixed data so we don't have to use GlobalLock + const int32 length = str.Length(); + char* ptr = static_cast(GlobalAlloc(GMEM_FIXED, length + 1)); + + // Copy the string into the buffer as ANSI text + StringUtils::ConvertUTF162ANSI(str.Get(), ptr, length); + ptr[length] = '\0'; + + return ptr; +} + +void DeepCopyFormatEtc(FORMATETC* dest, FORMATETC* source) +{ + // Copy the source FORMATETC into dest + *dest = *source; + + if (source->ptd) + { + // Allocate memory for the DVTARGETDEVICE if necessary + dest->ptd = static_cast(CoTaskMemAlloc(sizeof(DVTARGETDEVICE))); + + // Copy the contents of the source DVTARGETDEVICE into dest->ptd + *(dest->ptd) = *(source->ptd); + } +} + +HRESULT CreateEnumFormatEtc(UINT nNumFormats, FORMATETC* pFormatEtc, IEnumFORMATETC** ppEnumFormatEtc); + +/// +/// GUI data for Windows platform +/// +class WindowsGuiData : public IGuiData +{ +private: + Type _type; + Array _data; + +public: + /// + /// Init + /// + WindowsGuiData() + : _type(Type::Unknown) + , _data(1) + { + } + +public: + /// + /// Init from Ole IDataObject + /// + /// Object + void Init(IDataObject* pDataObj) + { + // Temporary data + FORMATETC fmtetc; + STGMEDIUM stgmed; + + // Clear + _type = Type::Unknown; + _data.Clear(); + + // Check type + fmtetc = { CF_TEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK) + { + // Text + _type = Type::Text; + + // Get data + char* text = static_cast(GlobalLock(stgmed.hGlobal)); + _data.Add(String(text)); + GlobalUnlock(stgmed.hGlobal); + ReleaseStgMedium(&stgmed); + } + else + { + fmtetc = { CF_UNICODETEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK) + { + // Unicode Text + _type = Type::Text; + + // Get data + Char* text = static_cast(GlobalLock(stgmed.hGlobal)); + _data.Add(String(text)); + GlobalUnlock(stgmed.hGlobal); + ReleaseStgMedium(&stgmed); + } + else + { + fmtetc = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + if (pDataObj->GetData(&fmtetc, &stgmed) == S_OK) + { + // Files + _type = Type::Files; + + // Get data + Char item[MAX_PATH]; + HDROP hdrop = static_cast(GlobalLock(stgmed.hGlobal)); + UINT filesCount = DragQueryFileW(hdrop, 0xFFFFFFFF, nullptr, 0); + for (UINT i = 0; i < filesCount; i++) + { + if (DragQueryFileW(hdrop, i, item, MAX_PATH) != 0) + { + _data.Add(String(item)); + } + } + GlobalUnlock(stgmed.hGlobal); + ReleaseStgMedium(&stgmed); + } + } + } + } + +public: + // [IGuiData] + Type GetType() const override + { + return _type; + } + String GetAsText() const override + { + String result; + if (_type == Type::Text) + { + result = _data[0]; + } + return result; + } + void GetAsFiles(Array* files) const override + { + if (_type == Type::Files) + { + files->Add(_data); + } + } +}; + +/// +/// Tool class for Windows Ole support +/// +class WindowsEnumFormatEtc : public IEnumFORMATETC +{ +private: + ULONG _refCount; + ULONG _index; + ULONG _formatsCount; + FORMATETC* _formatEtc; + +public: + WindowsEnumFormatEtc(FORMATETC* pFormatEtc, int32 nNumFormats) + : _refCount(1) + , _index(0) + , _formatsCount(nNumFormats) + , _formatEtc(nullptr) + { + // Allocate memory + _formatEtc = new FORMATETC[nNumFormats]; + + // Copy the FORMATETC structures + for (int32 i = 0; i < nNumFormats; i++) + { + DeepCopyFormatEtc(&_formatEtc[i], &pFormatEtc[i]); + } + } + + ~WindowsEnumFormatEtc() + { + if (_formatEtc) + { + for (uint32 i = 0; i < _formatsCount; i++) + { + if (_formatEtc[i].ptd) + { + CoTaskMemFree(_formatEtc[i].ptd); + } + } + + delete[] _formatEtc; + } + } + +public: + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, _COM_Outptr_ void __RPC_FAR *__RPC_FAR * ppvObject) override + { + // Check to see what interface has been requested + if (riid == IID_IEnumFORMATETC || riid == IID_IUnknown) + { + AddRef(); + *ppvObject = this; + return S_OK; + } + + // No interface + *ppvObject = nullptr; + return E_NOINTERFACE; + } + ULONG STDMETHODCALLTYPE AddRef() override + { + _InterlockedIncrement(&_refCount); + return _refCount; + } + ULONG STDMETHODCALLTYPE Release() override + { + ULONG ulRefCount = _InterlockedDecrement(&_refCount); + if (_refCount == 0) + { + delete this; + } + return ulRefCount; + } + + // [IEnumFormatEtc] + HRESULT STDMETHODCALLTYPE Next(ULONG celt, FORMATETC* pFormatEtc, ULONG* pceltFetched) override + { + ULONG copied = 0; + + // validate arguments + if (celt == 0 || pFormatEtc == nullptr) + return E_INVALIDARG; + + // copy FORMATETC structures into caller's buffer + while (_index < _formatsCount && copied < celt) + { + DeepCopyFormatEtc(&pFormatEtc[copied], &_formatEtc[_index]); + copied++; + _index++; + } + + // store result + if (pceltFetched != nullptr) + *pceltFetched = copied; + + // did we copy all that was requested? + return (copied == celt) ? S_OK : S_FALSE; + } + HRESULT STDMETHODCALLTYPE Skip(ULONG celt) override + { + _index += celt; + return (_index <= _formatsCount) ? S_OK : S_FALSE; + } + HRESULT STDMETHODCALLTYPE Reset() override + { + _index = 0; + return S_OK; + } + HRESULT STDMETHODCALLTYPE Clone(IEnumFORMATETC** ppEnumFormatEtc) override + { + HRESULT result; + + // Make a duplicate enumerator + result = CreateEnumFormatEtc(_formatsCount, _formatEtc, ppEnumFormatEtc); + + if (result == S_OK) + { + // Manually set the index state + static_cast(*ppEnumFormatEtc)->_index = _index; + } + + return result; + } +}; + +HRESULT CreateEnumFormatEtc(UINT nNumFormats, FORMATETC* pFormatEtc, IEnumFORMATETC** ppEnumFormatEtc) +{ + if (nNumFormats == 0 || pFormatEtc == nullptr || ppEnumFormatEtc == nullptr) + return E_INVALIDARG; + *ppEnumFormatEtc = new WindowsEnumFormatEtc(pFormatEtc, nNumFormats); + return *ppEnumFormatEtc ? S_OK : E_OUTOFMEMORY; +} + +/// +/// Drag drop source and data container for Ole +/// +class WindowsDragSource : public IDataObject, public IDropSource +{ +private: + ULONG _refCount; + int32 _formatsCount; + FORMATETC* _formatEtc; + STGMEDIUM* _stgMedium; + +public: + WindowsDragSource(FORMATETC* fmtetc, STGMEDIUM* stgmed, int32 count) + : _refCount(1) + , _formatsCount(count) + , _formatEtc(nullptr) + , _stgMedium(nullptr) + { + // Allocate memory + _formatEtc = new FORMATETC[count]; + _stgMedium = new STGMEDIUM[count]; + + // Copy descriptors + for (int32 i = 0; i < count; i++) + { + _formatEtc[i] = fmtetc[i]; + _stgMedium[i] = stgmed[i]; + } + } + + virtual ~WindowsDragSource() + { + delete[] _formatEtc; + delete[] _stgMedium; + } + +public: + // [IUnknown] + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, _COM_Outptr_ void __RPC_FAR *__RPC_FAR * ppvObject) override + { + // Check to see what interface has been requested + if (riid == IID_IDataObject || riid == IID_IUnknown || riid == IID_IDropSource) + { + AddRef(); + *ppvObject = this; + return S_OK; + } + + // No interface + *ppvObject = nullptr; + return E_NOINTERFACE; + } + + ULONG STDMETHODCALLTYPE AddRef() override + { + _InterlockedIncrement(&_refCount); + return _refCount; + } + + ULONG STDMETHODCALLTYPE Release() override + { + ULONG ulRefCount = _InterlockedDecrement(&_refCount); + if (_refCount == 0) + { + delete this; + } + return ulRefCount; + } + + // [IDropSource] + HRESULT STDMETHODCALLTYPE QueryContinueDrag(_In_ BOOL fEscapePressed, _In_ DWORD grfKeyState) override + { + // If the Escape key has been pressed since the last call, cancel the drop + if (fEscapePressed == TRUE || grfKeyState & MK_RBUTTON) + return DRAGDROP_S_CANCEL; + + // If the LeftMouse button has been released, then do the drop! + if ((grfKeyState & MK_LBUTTON) == 0) + return DRAGDROP_S_DROP; + + // Continue with the drag-drop + return S_OK; + } + HRESULT STDMETHODCALLTYPE GiveFeedback(_In_ DWORD dwEffect) override + { + // TODO: allow to use custom mouse cursor during drop and drag operation + return DRAGDROP_S_USEDEFAULTCURSORS; + } + + // [IDataObject] + HRESULT STDMETHODCALLTYPE GetData(_In_ FORMATETC* pformatetcIn, _Out_ STGMEDIUM* pmedium) override + { + if (pformatetcIn == nullptr || pmedium == nullptr) + return E_INVALIDARG; + + // Try to match the specified FORMATETC with one of our supported formats + int32 index = lookupFormatEtc(pformatetcIn); + if (index == INVALID_INDEX) + return DV_E_FORMATETC; + + // Found a match - transfer data into supplied storage medium + pmedium->tymed = _formatEtc[index].tymed; + pmedium->pUnkForRelease = nullptr; + + // Copy the data into the caller's storage medium + switch (_formatEtc[index].tymed) + { + case TYMED_HGLOBAL: + pmedium->hGlobal = duplicateGlobalMem(_stgMedium[index].hGlobal); + break; + + default: + return DV_E_FORMATETC; + } + + return S_OK; + } + HRESULT STDMETHODCALLTYPE GetDataHere(_In_ FORMATETC* pformatetc, _Inout_ STGMEDIUM* pmedium) override + { + return DATA_E_FORMATETC; + } + HRESULT STDMETHODCALLTYPE QueryGetData(__RPC__in_opt FORMATETC* pformatetc) override + { + return lookupFormatEtc(pformatetc) == INVALID_INDEX ? DV_E_FORMATETC : S_OK; + } + HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(__RPC__in_opt FORMATETC* pformatectIn, __RPC__out FORMATETC* pformatetcOut) override + { + // Apparently we have to set this field to NULL even though we don't do anything else + pformatetcOut->ptd = nullptr; + return E_NOTIMPL; + } + HRESULT STDMETHODCALLTYPE SetData(_In_ FORMATETC* pformatetc, _In_ STGMEDIUM* pmedium, BOOL fRelease) override + { + return E_NOTIMPL; + } + HRESULT STDMETHODCALLTYPE EnumFormatEtc(DWORD dwDirection, __RPC__deref_out_opt IEnumFORMATETC** ppenumFormatEtc) override + { + // Only the get direction is supported for OLE + if (dwDirection == DATADIR_GET) + { + // TODO: use SHCreateStdEnumFmtEtc API call + return CreateEnumFormatEtc(_formatsCount, _formatEtc, ppenumFormatEtc); + } + + // The direction specified is not supported for drag+drop + return E_NOTIMPL; + } + HRESULT STDMETHODCALLTYPE DAdvise(__RPC__in FORMATETC* pformatetc, DWORD advf, __RPC__in_opt IAdviseSink* pAdvSink, __RPC__out DWORD* pdwConnection) override + { + return OLE_E_ADVISENOTSUPPORTED; + } + HRESULT STDMETHODCALLTYPE DUnadvise(DWORD dwConnection) override + { + return OLE_E_ADVISENOTSUPPORTED; + } + HRESULT STDMETHODCALLTYPE EnumDAdvise(__RPC__deref_out_opt IEnumSTATDATA** ppenumAdvise) override + { + return OLE_E_ADVISENOTSUPPORTED; + } + +private: + int32 lookupFormatEtc(FORMATETC* pFormatEtc) const + { + // Check each of our formats in turn to see if one matches + for (int32 i = 0; i < _formatsCount; i++) + { + if ((_formatEtc[i].tymed & pFormatEtc->tymed) && + _formatEtc[i].cfFormat == pFormatEtc->cfFormat && + _formatEtc[i].dwAspect == pFormatEtc->dwAspect) + { + // Return index of stored format + return i; + } + } + + // Format not found + return INVALID_INDEX; + } +}; + +WindowsGuiData GuiDragDropData; + +DragDropEffect WindowsWindow::DoDragDrop(const StringView& data) +{ + // Create background worker that will keep updating GUI (perform rendering) + const auto task = New(); + Task::StartNew(task); + while (task->GetState() == TaskState::Queued) + Platform::Sleep(1); + + // Create descriptors + FORMATETC fmtetc = { CF_TEXT, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + STGMEDIUM stgmed = { TYMED_HGLOBAL, { nullptr }, nullptr }; + + // Create a HGLOBAL inside the storage medium + stgmed.hGlobal = StringToHandle(data); + + // Create drop source + auto dropSource = new WindowsDragSource(&fmtetc, &stgmed, 1); + + // Do the drag drop operation + DWORD dwEffect; + HRESULT result = ::DoDragDrop(dropSource, dropSource, DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK | DROPEFFECT_SCROLL, &dwEffect); + + // Wait for job end + Platform::AtomicStore(&task->ExitFlag, 1); + task->Wait(); + + // Release allocated data + dropSource->Release(); + ReleaseStgMedium(&stgmed); + + // Fix hanging mouse state (Windows doesn't send WM_LBUTTONUP when we end the drag and drop) + if (Input::GetMouseButton(MouseButton::Left)) + { + ::POINT point; + ::GetCursorPos(&point); + Input::Mouse->OnMouseUp(Float2((float)point.x, (float)point.y), MouseButton::Left, this); + } + + return SUCCEEDED(result) ? dropEffectFromOleEnum(dwEffect) : DragDropEffect::None; +} + +HRESULT WindowsWindow::DragEnter(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) +{ + POINT p = { pt.x, pt.y }; + ::ScreenToClient(_handle, &p); + GuiDragDropData.Init((IDataObject*)pDataObj); + DragDropEffect effect = DragDropEffect::None; + OnDragEnter(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); + Focus(); + *pdwEffect = dropEffect2OleEnum(effect); + return S_OK; +} + +HRESULT WindowsWindow::DragOver(Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) +{ + POINT p = { pt.x, pt.y }; + ::ScreenToClient(_handle, &p); + DragDropEffect effect = DragDropEffect::None; + OnDragOver(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); + *pdwEffect = dropEffect2OleEnum(effect); + return S_OK; +} + +HRESULT WindowsWindow::DragLeave() +{ + OnDragLeave(); + return S_OK; +} + +HRESULT WindowsWindow::Drop(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, Windows::POINTL pt, Windows::DWORD* pdwEffect) +{ + POINT p = { pt.x, pt.y }; + ::ScreenToClient(_handle, &p); + GuiDragDropData.Init((IDataObject*)pDataObj); + DragDropEffect effect = DragDropEffect::None; + OnDragDrop(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); + *pdwEffect = dropEffect2OleEnum(effect); + return S_OK; +} + +#else + +DragDropEffect WindowsWindow::DoDragDrop(const StringView& data) +{ + return DragDropEffect::None; +} + +#endif + #endif diff --git a/Source/Engine/Platform/Windows/WindowsWindow.h b/Source/Engine/Platform/Windows/WindowsWindow.h index e15f86a45..186793b1e 100644 --- a/Source/Engine/Platform/Windows/WindowsWindow.h +++ b/Source/Engine/Platform/Windows/WindowsWindow.h @@ -32,6 +32,7 @@ private: Windows::HANDLE _monitor = nullptr; Windows::LONG _clipCursorRect[4]; int32 _regionWidth = 0, _regionHeight = 0; + Float2 _minimizedScreenPosition = Float2::Zero; public: diff --git a/Source/Engine/Platform/iOS/iOSPlatform.cpp b/Source/Engine/Platform/iOS/iOSPlatform.cpp index 7ca27c481..26c3b8f28 100644 --- a/Source/Engine/Platform/iOS/iOSPlatform.cpp +++ b/Source/Engine/Platform/iOS/iOSPlatform.cpp @@ -410,7 +410,7 @@ bool iOSWindow::IsClosed() const bool iOSWindow::IsForegroundWindow() const { - return Platform::GetHasFocus() && IsFocused(); + return IsFocused() && Platform::GetHasFocus(); } void iOSWindow::BringToFront(bool force) diff --git a/Source/Engine/Profiler/ProfilerGPU.cpp b/Source/Engine/Profiler/ProfilerGPU.cpp index 3054abe67..6944441be 100644 --- a/Source/Engine/Profiler/ProfilerGPU.cpp +++ b/Source/Engine/Profiler/ProfilerGPU.cpp @@ -74,6 +74,7 @@ void ProfilerGPU::EventBuffer::Clear() _data.Clear(); _isResolved = false; FrameIndex = 0; + PresentTime = 0.0f; } GPUTimerQuery* ProfilerGPU::GetTimerQuery() @@ -133,7 +134,9 @@ void ProfilerGPU::BeginFrame() // Clear stats RenderStatsData::Counter = RenderStatsData(); _depth = 0; - Buffers[CurrentBuffer].FrameIndex = Engine::FrameCount; + auto& buffer = Buffers[CurrentBuffer]; + buffer.FrameIndex = Engine::FrameCount; + buffer.PresentTime = 0.0f; // Try to resolve previous frames for (int32 i = 0; i < PROFILER_GPU_EVENTS_FRAMES; i++) @@ -149,6 +152,12 @@ void ProfilerGPU::OnPresent() buffer.EndAll(); } +void ProfilerGPU::OnPresentTime(float time) +{ + auto& buffer = Buffers[CurrentBuffer]; + buffer.PresentTime += time; +} + void ProfilerGPU::EndFrame() { if (_depth) @@ -164,7 +173,7 @@ void ProfilerGPU::EndFrame() buffer.Clear(); } -bool ProfilerGPU::GetLastFrameData(float& drawTimeMs, RenderStatsData& statsData) +bool ProfilerGPU::GetLastFrameData(float& drawTimeMs, float& presentTimeMs, RenderStatsData& statsData) { uint64 maxFrame = 0; int32 maxFrameIndex = -1; @@ -177,17 +186,19 @@ bool ProfilerGPU::GetLastFrameData(float& drawTimeMs, RenderStatsData& statsData maxFrameIndex = i; } } - if (maxFrameIndex != -1) { auto& frame = frames[maxFrameIndex]; const auto root = frame.Get(0); drawTimeMs = root->Time; + presentTimeMs = frame.PresentTime; statsData = root->Stats; return true; } + // No data drawTimeMs = 0.0f; + presentTimeMs = 0.0f; Platform::MemoryClear(&statsData, sizeof(statsData)); return false; } diff --git a/Source/Engine/Profiler/ProfilerGPU.h b/Source/Engine/Profiler/ProfilerGPU.h index 29392b7a5..94ed345b9 100644 --- a/Source/Engine/Profiler/ProfilerGPU.h +++ b/Source/Engine/Profiler/ProfilerGPU.h @@ -20,6 +20,8 @@ class GPUTimerQuery; API_CLASS(Static) class FLAXENGINE_API ProfilerGPU { DECLARE_SCRIPTING_TYPE_NO_SPAWN(ProfilerGPU); + friend class Engine; + friend class GPUDevice; public: /// /// Represents single CPU profiling event data. @@ -69,6 +71,11 @@ public: /// uint64 FrameIndex; + /// + /// Sum of all present events duration on CPU during this frame (in milliseconds). + /// + float PresentTime; + /// /// Determines whether this buffer has ready data (resolved and not empty). /// @@ -125,7 +132,7 @@ public: /// /// True if GPU profiling is enabled, otherwise false to disable events collecting and GPU timer queries usage. Can be changed during rendering. /// - static bool Enabled; + API_FIELD() static bool Enabled; /// /// The current frame buffer to collect events. @@ -151,32 +158,20 @@ public: /// The event token index returned by the BeginEvent method. static void EndEvent(int32 index); - /// - /// Begins the new frame rendering. Called by the engine to sync profiling data. - /// - static void BeginFrame(); - - /// - /// Called when just before flushing current frame GPU commands (via Present or Flush). Call active timer queries should be ended now. - /// - static void OnPresent(); - - /// - /// Ends the frame rendering. Called by the engine to sync profiling data. - /// - static void EndFrame(); - /// /// Tries to get the rendering stats from the last frame drawing (that has been resolved and has valid data). /// /// The draw execution time on a GPU (in milliseconds). + /// The final frame present time on a CPU (in milliseconds). Time game waited for vsync or GPU to finish previous frame rendering. /// The rendering stats data. /// True if got the data, otherwise false. - static bool GetLastFrameData(float& drawTimeMs, RenderStatsData& statsData); + API_FUNCTION() static bool GetLastFrameData(float& drawTimeMs, float& presentTimeMs, RenderStatsData& statsData); - /// - /// Releases resources. Calls to the profiling API after Dispose are not valid - /// +private: + static void BeginFrame(); + static void OnPresent(); + static void OnPresentTime(float time); + static void EndFrame(); static void Dispose(); }; diff --git a/Source/Engine/Profiler/ProfilingTools.cpp b/Source/Engine/Profiler/ProfilingTools.cpp index 61c0e2c33..113a04ec8 100644 --- a/Source/Engine/Profiler/ProfilingTools.cpp +++ b/Source/Engine/Profiler/ProfilingTools.cpp @@ -48,7 +48,9 @@ void ProfilingToolsService::Update() stats.PhysicsTimeMs = static_cast(Time::Physics.LastLength * 1000.0); stats.DrawCPUTimeMs = static_cast(Time::Draw.LastLength * 1000.0); - ProfilerGPU::GetLastFrameData(stats.DrawGPUTimeMs, stats.DrawStats); + float presentTime; + ProfilerGPU::GetLastFrameData(stats.DrawGPUTimeMs, presentTime, stats.DrawStats); + stats.DrawCPUTimeMs = Math::Max(stats.DrawCPUTimeMs - presentTime, 0.0f); // Remove swapchain present wait time to exclude from drawing on CPU } // Extract CPU profiler events diff --git a/Source/Engine/Render2D/SpriteAtlas.cpp b/Source/Engine/Render2D/SpriteAtlas.cpp index 0e0aa071f..ac711b8b8 100644 --- a/Source/Engine/Render2D/SpriteAtlas.cpp +++ b/Source/Engine/Render2D/SpriteAtlas.cpp @@ -47,10 +47,16 @@ int32 SpriteAtlas::GetSpritesCount() const Sprite SpriteAtlas::GetSprite(int32 index) const { - CHECK_RETURN(index >= 0 && index < Sprites.Count(), Sprite()) + CHECK_RETURN(index >= 0 && index < Sprites.Count(), Sprite()); return Sprites.Get()[index]; } +void SpriteAtlas::GetSpriteArea(int32 index, Rectangle& result) const +{ + CHECK(index >= 0 && index < Sprites.Count()); + result = Sprites.Get()[index].Area; +} + void SpriteAtlas::SetSprite(int32 index, const Sprite& value) { CHECK(index >= 0 && index < Sprites.Count()); diff --git a/Source/Engine/Render2D/SpriteAtlas.cs b/Source/Engine/Render2D/SpriteAtlas.cs index b3796ffd3..93cdf68b2 100644 --- a/Source/Engine/Render2D/SpriteAtlas.cs +++ b/Source/Engine/Render2D/SpriteAtlas.cs @@ -70,7 +70,13 @@ namespace FlaxEngine [NoSerialize] public Float2 Size { - get => Area.Size * Atlas.Size; + get + { + if (Atlas == null) + throw new InvalidOperationException("Cannot use invalid sprite."); + Atlas.GetSpriteArea(Index, out var area); + return area.Size * Atlas.Size; + } set { var area = Area; @@ -89,7 +95,8 @@ namespace FlaxEngine { if (Atlas == null) throw new InvalidOperationException("Cannot use invalid sprite."); - return Atlas.GetSprite(Index).Area; + Atlas.GetSpriteArea(Index, out var area); + return area; } set { diff --git a/Source/Engine/Render2D/SpriteAtlas.h b/Source/Engine/Render2D/SpriteAtlas.h index 6fcda5162..533440c68 100644 --- a/Source/Engine/Render2D/SpriteAtlas.h +++ b/Source/Engine/Render2D/SpriteAtlas.h @@ -120,6 +120,14 @@ public: /// The sprite data. API_FUNCTION() Sprite GetSprite(int32 index) const; + /// + /// Gets the sprite area. + /// + /// The index. + /// The output sprite area. + /// The sprite data. + API_FUNCTION() void GetSpriteArea(int32 index, API_PARAM(Out) Rectangle& result) const; + /// /// Sets the sprite data. /// diff --git a/Source/Engine/Renderer/Config.h b/Source/Engine/Renderer/Config.h index 659369a71..876da3100 100644 --- a/Source/Engine/Renderer/Config.h +++ b/Source/Engine/Renderer/Config.h @@ -114,6 +114,3 @@ PACK_STRUCT(struct ProbeData { // Maximum amount of directional light cascades (using CSM technique) #define MAX_CSM_CASCADES 4 - -// Default format for the shadow map textures -#define SHADOW_MAPS_FORMAT PixelFormat::D16_UNorm diff --git a/Source/Engine/Renderer/DepthOfFieldPass.cpp b/Source/Engine/Renderer/DepthOfFieldPass.cpp index 4c43ccdbf..707a441e6 100644 --- a/Source/Engine/Renderer/DepthOfFieldPass.cpp +++ b/Source/Engine/Renderer/DepthOfFieldPass.cpp @@ -202,16 +202,14 @@ GPUTexture* DepthOfFieldPass::getDofBokehShape(DepthOfFieldSettings& dofSettings void DepthOfFieldPass::Render(RenderContext& renderContext, GPUTexture*& frame, GPUTexture*& tmp) { - if (!_platformSupportsDoF || checkIfSkipPass()) + DepthOfFieldSettings& dofSettings = renderContext.List->Settings.DepthOfField; + const bool useDoF = EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::DepthOfField) && dofSettings.Enabled; + if (!useDoF || !_platformSupportsDoF || checkIfSkipPass()) return; auto device = GPUDevice::Instance; auto context = device->GetMainContext(); const auto depthBuffer = renderContext.Buffers->DepthBuffer; const auto shader = _shader->GetShader(); - DepthOfFieldSettings& dofSettings = renderContext.List->Settings.DepthOfField; - const bool useDoF = _platformSupportsDoF && EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::DepthOfField) && dofSettings.Enabled; - if (!useDoF) - return; PROFILE_GPU_CPU("Depth Of Field"); context->ResetSR(); diff --git a/Source/Engine/Renderer/EyeAdaptationPass.cpp b/Source/Engine/Renderer/EyeAdaptationPass.cpp index 12b26ea41..3a840ff01 100644 --- a/Source/Engine/Renderer/EyeAdaptationPass.cpp +++ b/Source/Engine/Renderer/EyeAdaptationPass.cpp @@ -35,7 +35,6 @@ PACK_STRUCT(struct EyeAdaptationData { void EyeAdaptationPass::Render(RenderContext& renderContext, GPUTexture* colorBuffer) { - // Cache data auto device = GPUDevice::Instance; auto context = device->GetMainContext(); auto& view = renderContext.View; @@ -45,12 +44,8 @@ void EyeAdaptationPass::Render(RenderContext& renderContext, GPUTexture* colorBu //const float frameDelta = Time::ElapsedGameTime.GetTotalSeconds(); const float frameDelta = time - renderContext.Buffers->LastEyeAdaptationTime; renderContext.Buffers->LastEyeAdaptationTime = 0.0f; - - // Optionally skip the rendering - if (checkIfSkipPass() || (view.Flags & ViewFlags::EyeAdaptation) == ViewFlags::None || settings.Mode == EyeAdaptationMode::None) - { + if ((view.Flags & ViewFlags::EyeAdaptation) == ViewFlags::None || settings.Mode == EyeAdaptationMode::None || checkIfSkipPass()) return; - } PROFILE_GPU_CPU("Eye Adaptation"); diff --git a/Source/Engine/Renderer/MotionBlurPass.cpp b/Source/Engine/Renderer/MotionBlurPass.cpp index d425df80f..2030027ed 100644 --- a/Source/Engine/Renderer/MotionBlurPass.cpp +++ b/Source/Engine/Renderer/MotionBlurPass.cpp @@ -244,7 +244,7 @@ void MotionBlurPass::RenderDebug(RenderContext& renderContext, GPUTextureView* f { auto context = GPUDevice::Instance->GetMainContext(); const auto motionVectors = renderContext.Buffers->MotionVectors; - if (!motionVectors->IsAllocated() || checkIfSkipPass()) + if (!motionVectors || !motionVectors->IsAllocated() || checkIfSkipPass()) { context->Draw(frame); return; diff --git a/Source/Engine/Renderer/PostProcessingPass.cpp b/Source/Engine/Renderer/PostProcessingPass.cpp index 345147b61..006927639 100644 --- a/Source/Engine/Renderer/PostProcessingPass.cpp +++ b/Source/Engine/Renderer/PostProcessingPass.cpp @@ -195,7 +195,7 @@ void PostProcessingPass::Render(RenderContext& renderContext, GPUTexture* input, bool useLensFlares = EnumHasAnyFlags(view.Flags, ViewFlags::LensFlares) && settings.LensFlares.Intensity > 0.0f && useBloom; // Ensure to have valid data and if at least one effect should be applied - if (checkIfSkipPass() || !(useBloom || useToneMapping || useCameraArtifacts)) + if (!(useBloom || useToneMapping || useCameraArtifacts) || checkIfSkipPass()) { // Resources are missing. Do not perform rendering. Just copy raw frame context->SetViewportAndScissors((float)output->Width(), (float)output->Height()); diff --git a/Source/Engine/Renderer/ProbesRenderer.cpp b/Source/Engine/Renderer/ProbesRenderer.cpp index e24022b07..a965dee09 100644 --- a/Source/Engine/Renderer/ProbesRenderer.cpp +++ b/Source/Engine/Renderer/ProbesRenderer.cpp @@ -105,7 +105,7 @@ class ProbesRendererService : public EngineService { public: ProbesRendererService() - : EngineService(TEXT("Probes Renderer"), 70) + : EngineService(TEXT("Probes Renderer"), 500) { } diff --git a/Source/Engine/Renderer/Renderer.cpp b/Source/Engine/Renderer/Renderer.cpp index edac958bc..833daed8e 100644 --- a/Source/Engine/Renderer/Renderer.cpp +++ b/Source/Engine/Renderer/Renderer.cpp @@ -343,6 +343,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont // Prepare renderContext.View.Prepare(renderContext); renderContext.Buffers->Prepare(); + ShadowsPass::Instance()->Prepare(); // Build batch of render contexts (main view and shadow projections) { diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp index 7e0cd1053..ba326b728 100644 --- a/Source/Engine/Renderer/ShadowsPass.cpp +++ b/Source/Engine/Renderer/ShadowsPass.cpp @@ -4,11 +4,11 @@ #include "GBufferPass.h" #include "VolumetricFogPass.h" #include "Engine/Graphics/Graphics.h" +#include "Engine/Graphics/GPUContext.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Graphics/RenderBuffers.h" #include "Engine/Graphics/PixelFormatExtensions.h" #include "Engine/Content/Content.h" -#include "Engine/Graphics/GPUContext.h" #include "Engine/Scripting/Enums.h" #if USE_EDITOR #include "Engine/Renderer/Lightmaps.h" @@ -81,19 +81,22 @@ bool ShadowsPass::Init() _shader.Get()->OnReloading.Bind(this); #endif - // If GPU doesn't support linear sampling for the shadow map then fallback to the single sample on lowest quality - const auto formatTexture = PixelFormatExtensions::FindShaderResourceFormat(SHADOW_MAPS_FORMAT, false); - const auto formatFeaturesDepth = GPUDevice::Instance->GetFormatFeatures(SHADOW_MAPS_FORMAT); - const auto formatFeaturesTexture = GPUDevice::Instance->GetFormatFeatures(formatTexture); - _supportsShadows = EnumHasAllFlags(formatFeaturesDepth.Support, FormatSupport::DepthStencil | FormatSupport::Texture2D) - && EnumHasAllFlags(formatFeaturesTexture.Support, FormatSupport::ShaderSample | FormatSupport::ShaderSampleComparison); - // TODO: fallback to 32-bit shadow map format if 16-bit is not supported - if (!_supportsShadows) + // Select format for shadow maps + _shadowMapFormat = PixelFormat::Unknown; + for (const PixelFormat format : { PixelFormat::D16_UNorm, PixelFormat::D24_UNorm_S8_UInt, PixelFormat::D32_Float }) { - LOG(Warning, "GPU doesn't support shadows rendering"); - LOG(Warning, "Format: {0}, features support: {1}", ScriptingEnum::ToString(SHADOW_MAPS_FORMAT), (uint32)formatFeaturesDepth.Support); - LOG(Warning, "Format: {0}, features support: {1}", ScriptingEnum::ToString(formatTexture), (uint32)formatFeaturesTexture.Support); + const auto formatTexture = PixelFormatExtensions::FindShaderResourceFormat(format, false); + const auto formatFeaturesDepth = GPUDevice::Instance->GetFormatFeatures(format); + const auto formatFeaturesTexture = GPUDevice::Instance->GetFormatFeatures(formatTexture); + if (EnumHasAllFlags(formatFeaturesDepth.Support, FormatSupport::DepthStencil | FormatSupport::Texture2D) && + EnumHasAllFlags(formatFeaturesTexture.Support, FormatSupport::ShaderSample | FormatSupport::ShaderSampleComparison)) + { + _shadowMapFormat = format; + break; + } } + if (_shadowMapFormat == PixelFormat::Unknown) + LOG(Warning, "GPU doesn't support shadows rendering"); return false; } @@ -148,7 +151,7 @@ void ShadowsPass::updateShadowMapSize() // Select new size _currentShadowMapsQuality = Graphics::ShadowMapsQuality; - if (_supportsShadows) + if (_shadowMapFormat != PixelFormat::Unknown) { switch (_currentShadowMapsQuality) { @@ -174,18 +177,18 @@ void ShadowsPass::updateShadowMapSize() // Check if size will change if (newSizeCSM > 0 && newSizeCSM != _shadowMapsSizeCSM) { - if (_shadowMapCSM->Init(GPUTextureDescription::New2D(newSizeCSM, newSizeCSM, SHADOW_MAPS_FORMAT, GPUTextureFlags::ShaderResource | GPUTextureFlags::DepthStencil, 1, MAX_CSM_CASCADES))) + if (_shadowMapCSM->Init(GPUTextureDescription::New2D(newSizeCSM, newSizeCSM, _shadowMapFormat, GPUTextureFlags::ShaderResource | GPUTextureFlags::DepthStencil, 1, MAX_CSM_CASCADES))) { - LOG(Fatal, "Cannot setup shadow map '{0}' Size: {1}, format: {2}.", TEXT("CSM"), newSizeCSM, (int32)SHADOW_MAPS_FORMAT); + LOG(Fatal, "Cannot setup shadow map '{0}' Size: {1}, format: {2}.", TEXT("CSM"), newSizeCSM, ScriptingEnum::ToString(_shadowMapFormat)); return; } _shadowMapsSizeCSM = newSizeCSM; } if (newSizeCube > 0 && newSizeCube != _shadowMapsSizeCube) { - if (_shadowMapCube->Init(GPUTextureDescription::NewCube(newSizeCube, SHADOW_MAPS_FORMAT, GPUTextureFlags::ShaderResource | GPUTextureFlags::DepthStencil))) + if (_shadowMapCube->Init(GPUTextureDescription::NewCube(newSizeCube, _shadowMapFormat, GPUTextureFlags::ShaderResource | GPUTextureFlags::DepthStencil))) { - LOG(Fatal, "Cannot setup shadow map '{0}' Size: {1}, format: {2}.", TEXT("Cube"), newSizeCube, (int32)SHADOW_MAPS_FORMAT); + LOG(Fatal, "Cannot setup shadow map '{0}' Size: {1}, format: {2}.", TEXT("Cube"), newSizeCube, ScriptingEnum::ToString(_shadowMapFormat)); return; } _shadowMapsSizeCube = newSizeCube; @@ -546,10 +549,17 @@ void ShadowsPass::Dispose() SAFE_DELETE_GPU_RESOURCE(_shadowMapCube); } +void ShadowsPass::Prepare() +{ + // Clear cached data + _shadowData.Clear(); + LastDirLightIndex = -1; + LastDirLightShadowMap = nullptr; +} + void ShadowsPass::SetupShadows(RenderContext& renderContext, RenderContextBatch& renderContextBatch) { PROFILE_CPU(); - _shadowData.Clear(); auto& view = renderContext.View; // Update shadow map @@ -586,7 +596,7 @@ bool ShadowsPass::CanRenderShadow(const RenderContext& renderContext, const Rend const float fadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f); const float fade = 1 - Math::Saturate((dstLightToView - light.Radius - light.ShadowsDistance + fadeDistance) / fadeDistance); - return fade > ZeroTolerance && _supportsShadows; + return fade > ZeroTolerance && _shadowMapFormat != PixelFormat::Unknown; } bool ShadowsPass::CanRenderShadow(const RenderContext& renderContext, const RendererSpotLightData& light) @@ -598,12 +608,12 @@ bool ShadowsPass::CanRenderShadow(const RenderContext& renderContext, const Rend const float fadeDistance = Math::Max(light.ShadowsFadeDistance, 0.1f); const float fade = 1 - Math::Saturate((dstLightToView - light.Radius - light.ShadowsDistance + fadeDistance) / fadeDistance); - return fade > ZeroTolerance && _supportsShadows; + return fade > ZeroTolerance && _shadowMapFormat != PixelFormat::Unknown; } bool ShadowsPass::CanRenderShadow(const RenderContext& renderContext, const RendererDirectionalLightData& light) { - return _supportsShadows; + return _shadowMapFormat != PixelFormat::Unknown; } void ShadowsPass::RenderShadow(RenderContextBatch& renderContextBatch, RendererPointLightData& light, GPUTextureView* shadowMask) diff --git a/Source/Engine/Renderer/ShadowsPass.h b/Source/Engine/Renderer/ShadowsPass.h index 176fbd9a0..d3589d008 100644 --- a/Source/Engine/Renderer/ShadowsPass.h +++ b/Source/Engine/Renderer/ShadowsPass.h @@ -55,7 +55,7 @@ private: GPUPipelineStatePermutationsPs(Quality::MAX) * 2 * 2> _psShadowDir; GPUPipelineStatePermutationsPs(Quality::MAX) * 2> _psShadowPoint; GPUPipelineStatePermutationsPs(Quality::MAX) * 2> _psShadowSpot; - bool _supportsShadows; + PixelFormat _shadowMapFormat; // Shadow maps stuff int32 _shadowMapsSizeCSM; @@ -94,6 +94,8 @@ public: LightShadowData LastDirLight; public: + void Prepare(); + /// /// Setups the shadows rendering for batched scene drawing. Checks which lights will cast a shadow. /// diff --git a/Source/Engine/Scripting/Attributes/PluginLoadOrder.cs b/Source/Engine/Scripting/Attributes/PluginLoadOrder.cs new file mode 100644 index 000000000..18a6a1dac --- /dev/null +++ b/Source/Engine/Scripting/Attributes/PluginLoadOrder.cs @@ -0,0 +1,21 @@ +using System; + +namespace FlaxEngine; + +/// +/// This attribute allows for specifying initialization and deinitialization order for plugins. +/// +[Serializable] +[AttributeUsage(AttributeTargets.Class)] +public class PluginLoadOrderAttribute : Attribute +{ + /// + /// The plugin type to initialize this plugin after. + /// + public Type InitializeAfter; + + /// + /// The plugin type to deinitialize this plugin before. + /// + public Type DeinitializeBefore; +} diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp index efd147917..679ead4e3 100644 --- a/Source/Engine/Scripting/BinaryModule.cpp +++ b/Source/Engine/Scripting/BinaryModule.cpp @@ -1270,7 +1270,11 @@ bool ManagedBinaryModule::InvokeMethod(void* method, const Variant& instance, Sp // Invoke the method MObject* exception = nullptr; +#if USE_NETCORE // NetCore uses the same path for both virtual and non-virtual calls + MObject* resultObject = mMethod->Invoke(mInstance, params, &exception); +#else MObject* resultObject = withInterfaces ? mMethod->InvokeVirtual((MObject*)mInstance, params, &exception) : mMethod->Invoke(mInstance, params, &exception); +#endif if (exception) { MException ex(exception); diff --git a/Source/Engine/Scripting/Object.cs b/Source/Engine/Scripting/Object.cs index c64532c2b..9fc7d8039 100644 --- a/Source/Engine/Scripting/Object.cs +++ b/Source/Engine/Scripting/Object.cs @@ -320,7 +320,7 @@ namespace FlaxEngine internal static partial Object Internal_Create2(string typeName); [LibraryImport("FlaxEngine", EntryPoint = "ObjectInternal_ManagedInstanceCreated", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(Interop.StringMarshaller))] - internal static partial void Internal_ManagedInstanceCreated(Object managedInstance, IntPtr theKlass); + internal static partial void Internal_ManagedInstanceCreated(Object managedInstance, IntPtr typeClass); [LibraryImport("FlaxEngine", EntryPoint = "ObjectInternal_ManagedInstanceDeleted", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(Interop.StringMarshaller))] internal static partial void Internal_ManagedInstanceDeleted(IntPtr nativeInstance); diff --git a/Source/Engine/Scripting/Plugins/PluginManager.cpp b/Source/Engine/Scripting/Plugins/PluginManager.cpp index 328afa69c..61d7bc698 100644 --- a/Source/Engine/Scripting/Plugins/PluginManager.cpp +++ b/Source/Engine/Scripting/Plugins/PluginManager.cpp @@ -13,6 +13,7 @@ #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Engine/EngineService.h" #include "Engine/Core/Log.h" +#include "Engine/Scripting/ManagedCLR/MField.h" Plugin::Plugin(const SpawnParams& params) : ScriptingObject(params) @@ -74,14 +75,57 @@ Action PluginManager::PluginsChanged; namespace PluginManagerImpl { + bool Initialized = false; Array GamePlugins; Array EditorPlugins; - void LoadPlugin(MClass* klass, bool isEditor); void OnAssemblyLoaded(MAssembly* assembly); void OnAssemblyUnloading(MAssembly* assembly); void OnBinaryModuleLoaded(BinaryModule* module); void OnScriptsReloading(); + void InitializePlugins(); + void DeinitializePlugins(); + + template + Array SortPlugins(Array plugins, MClass* pluginLoadOrderAttribute, MField* typeField) + { + // Sort plugins + Array newPlugins; + for (int i = 0; i < plugins.Count(); i++) + { + PluginType* plugin = plugins[i]; + int32 insertIndex = -1; + for (int j = 0; j < newPlugins.Count(); j++) + { + // Get first instance where a game plugin needs another one before it + auto attribute = newPlugins[j]->GetClass()->GetAttribute(pluginLoadOrderAttribute); + if (attribute == nullptr || MCore::Object::GetClass(attribute) != pluginLoadOrderAttribute) + continue; + + // Check if attribute references a valid class + MTypeObject* refType = nullptr; + typeField->GetValue(attribute, &refType); + if (refType == nullptr) + continue; + + MType* type = INTERNAL_TYPE_OBJECT_GET(refType); + if (type == nullptr) + continue; + MClass* typeClass = MCore::Type::GetClass(type); + + if (plugin->GetClass() == typeClass) + { + insertIndex = j; + break; + } + } + if (insertIndex == -1) + newPlugins.Add(plugin); + else + newPlugins.Insert(insertIndex, plugin); + } + return newPlugins; + } } using namespace PluginManagerImpl; @@ -90,7 +134,7 @@ class PluginManagerService : public EngineService { public: PluginManagerService() - : EngineService(TEXT("Plugin Manager"), 130) + : EngineService(TEXT("Plugin Manager"), 100) { } @@ -107,7 +151,7 @@ void PluginManagerService::InvokeInitialize(Plugin* plugin) { if (plugin->_initialized) return; - StringAnsiView typeName = plugin->GetType().GetName(); + const StringAnsiView typeName = plugin->GetType().GetName(); PROFILE_CPU(); ZoneName(typeName.Get(), typeName.Length()); @@ -125,7 +169,7 @@ void PluginManagerService::InvokeDeinitialize(Plugin* plugin) { if (!plugin->_initialized) return; - StringAnsiView typeName = plugin->GetType().GetName(); + const StringAnsiView typeName = plugin->GetType().GetName(); PROFILE_CPU(); ZoneName(typeName.Get(), typeName.Length()); @@ -139,30 +183,6 @@ void PluginManagerService::InvokeDeinitialize(Plugin* plugin) PluginManager::PluginUnloaded(plugin); } -void PluginManagerImpl::LoadPlugin(MClass* klass, bool isEditor) -{ - // Create and check if use it - auto plugin = (Plugin*)Scripting::NewObject(klass); - if (!plugin) - return; - - if (!isEditor) - { - GamePlugins.Add((GamePlugin*)plugin); -#if !USE_EDITOR - PluginManagerService::InvokeInitialize(plugin); -#endif - } -#if USE_EDITOR - else - { - EditorPlugins.Add(plugin); - PluginManagerService::InvokeInitialize(plugin); - } -#endif - PluginManager::PluginsChanged(); -} - void PluginManagerImpl::OnAssemblyLoaded(MAssembly* assembly) { PROFILE_CPU_NAMED("Load Assembly Plugins"); @@ -183,6 +203,7 @@ void PluginManagerImpl::OnAssemblyLoaded(MAssembly* assembly) #endif // Process all classes to find plugins + bool loadedAnyPlugin = false; auto& classes = assembly->GetClasses(); for (auto i = classes.Begin(); i.IsNotEnd(); ++i) { @@ -192,40 +213,69 @@ void PluginManagerImpl::OnAssemblyLoaded(MAssembly* assembly) if (mclass->IsGeneric() || mclass->IsStatic() || mclass->IsAbstract()) continue; - if (mclass->IsSubClassOf(gamePluginClass)) - { - LoadPlugin(mclass, false); - } - + if (mclass->IsSubClassOf(gamePluginClass) #if USE_EDITOR - if (mclass->IsSubClassOf(editorPluginClass)) - { - LoadPlugin(mclass, true); - } + || mclass->IsSubClassOf(editorPluginClass) #endif + ) + { + auto plugin = (Plugin*)Scripting::NewObject(mclass); + if (plugin) + { +#if USE_EDITOR + if (mclass->IsSubClassOf(editorPluginClass)) + { + EditorPlugins.Add(plugin); + } + else +#endif + { + GamePlugins.Add((GamePlugin*)plugin); + } + loadedAnyPlugin = true; + } + } + } + + // Send event and initialize newly added plugins (ignore during startup) + if (loadedAnyPlugin && Initialized) + { + InitializePlugins(); + PluginManager::PluginsChanged(); } } void PluginManagerImpl::OnAssemblyUnloading(MAssembly* assembly) { bool changed = false; - for (int32 i = EditorPlugins.Count() - 1; i >= 0 && EditorPlugins.Count() > 0; i--) + + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto beforeTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); + ASSERT(beforeTypeField); + +#if USE_EDITOR + auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, beforeTypeField); + for (int32 i = editorPlugins.Count() - 1; i >= 0 && editorPlugins.Count() > 0; i--) { - auto plugin = EditorPlugins[i]; + auto plugin = editorPlugins[i]; if (plugin->GetType().ManagedClass->GetAssembly() == assembly) { PluginManagerService::InvokeDeinitialize(plugin); - EditorPlugins.RemoveAtKeepOrder(i); + EditorPlugins.Remove(plugin); changed = true; } } - for (int32 i = GamePlugins.Count() - 1; i >= 0 && GamePlugins.Count() > 0; i--) +#endif + + auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); + for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) { - auto plugin = GamePlugins[i]; + auto plugin = gamePlugins[i]; if (plugin->GetType().ManagedClass->GetAssembly() == assembly) { PluginManagerService::InvokeDeinitialize(plugin); - GamePlugins.RemoveAtKeepOrder(i); + GamePlugins.Remove(plugin); changed = true; } } @@ -260,38 +310,83 @@ void PluginManagerImpl::OnBinaryModuleLoaded(BinaryModule* module) void PluginManagerImpl::OnScriptsReloading() { // When scripting is reloading (eg. for hot-reload in Editor) we have to deinitialize plugins (Scripting service destroys C# objects later on) - bool changed = false; - for (int32 i = EditorPlugins.Count() - 1; i >= 0 && EditorPlugins.Count() > 0; i--) + DeinitializePlugins(); +} + +void PluginManagerImpl::InitializePlugins() +{ + if (EditorPlugins.Count() + GamePlugins.Count() == 0) + return; + PROFILE_CPU_NAMED("InitializePlugins"); + + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto afterTypeField = pluginLoadOrderAttribute->GetField("InitializeAfter"); + ASSERT(afterTypeField); + +#if USE_EDITOR + auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, afterTypeField); + for (auto plugin : editorPlugins) { - auto plugin = EditorPlugins[i]; - { - PluginManagerService::InvokeDeinitialize(plugin); - EditorPlugins.RemoveAtKeepOrder(i); - changed = true; - } + PluginManagerService::InvokeInitialize(plugin); } - for (int32 i = GamePlugins.Count() - 1; i >= 0 && GamePlugins.Count() > 0; i--) +#else + // Game plugins are managed via InitializeGamePlugins/DeinitializeGamePlugins by Editor for play mode + auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, afterTypeField); + for (auto plugin : gamePlugins) { - auto plugin = GamePlugins[i]; - { - PluginManagerService::InvokeDeinitialize(plugin); - GamePlugins.RemoveAtKeepOrder(i); - changed = true; - } + PluginManagerService::InvokeInitialize(plugin); } - if (changed) - PluginManager::PluginsChanged(); +#endif +} + +void PluginManagerImpl::DeinitializePlugins() +{ + if (EditorPlugins.Count() + GamePlugins.Count() == 0) + return; + PROFILE_CPU_NAMED("DeinitializePlugins"); + + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto beforeTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); + ASSERT(beforeTypeField); + +#if USE_EDITOR + auto editorPlugins = SortPlugins(EditorPlugins, pluginLoadOrderAttribute, beforeTypeField); + for (int32 i = editorPlugins.Count() - 1; i >= 0 && editorPlugins.Count() > 0; i--) + { + auto plugin = editorPlugins[i]; + PluginManagerService::InvokeDeinitialize(plugin); + EditorPlugins.Remove(plugin); + } +#endif + + auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); + for (int32 i = gamePlugins.Count() - 1; i >= 0 && gamePlugins.Count() > 0; i--) + { + auto plugin = gamePlugins[i]; + PluginManagerService::InvokeDeinitialize(plugin); + GamePlugins.Remove(plugin); + } + + PluginManager::PluginsChanged(); } bool PluginManagerService::Init() { + Initialized = false; + // Process already loaded modules for (auto module : BinaryModule::GetModules()) { OnBinaryModuleLoaded(module); } + // Invoke plugins initialization for all of them + InitializePlugins(); + // Register for new binary modules load actions + Initialized = true; Scripting::BinaryModuleLoaded.Bind(&OnBinaryModuleLoaded); Scripting::ScriptsReloading.Bind(&OnScriptsReloading); @@ -300,28 +395,13 @@ bool PluginManagerService::Init() void PluginManagerService::Dispose() { + // Unregister from new modules loading + Initialized = false; Scripting::BinaryModuleLoaded.Unbind(&OnBinaryModuleLoaded); Scripting::ScriptsReloading.Unbind(&OnScriptsReloading); // Cleanup all plugins - PROFILE_CPU_NAMED("Dispose Plugins"); - const int32 pluginsCount = EditorPlugins.Count() + GamePlugins.Count(); - if (pluginsCount == 0) - return; - LOG(Info, "Unloading {0} plugins", pluginsCount); - for (int32 i = EditorPlugins.Count() - 1; i >= 0 && EditorPlugins.Count() > 0; i--) - { - auto plugin = EditorPlugins[i]; - InvokeDeinitialize(plugin); - EditorPlugins.RemoveAtKeepOrder(i); - } - for (int32 i = GamePlugins.Count() - 1; i >= 0 && GamePlugins.Count() > 0; i--) - { - auto plugin = GamePlugins[i]; - InvokeDeinitialize(plugin); - GamePlugins.RemoveAtKeepOrder(i); - } - PluginManager::PluginsChanged(); + DeinitializePlugins(); } const Array& PluginManager::GetGamePlugins() @@ -336,11 +416,13 @@ const Array& PluginManager::GetEditorPlugins() Plugin* PluginManager::GetPlugin(const StringView& name) { +#if USE_EDITOR for (Plugin* p : EditorPlugins) { if (p->GetDescription().Name == name) return p; } +#endif for (GamePlugin* gp : GamePlugins) { if (gp->GetDescription().Name == name) @@ -352,11 +434,13 @@ Plugin* PluginManager::GetPlugin(const StringView& name) Plugin* PluginManager::GetPlugin(const MClass* type) { CHECK_RETURN(type, nullptr); +#if USE_EDITOR for (Plugin* p : EditorPlugins) { if (p->GetClass()->IsSubClassOf(type)) return p; } +#endif for (GamePlugin* gp : GamePlugins) { if (gp->GetClass()->IsSubClassOf(type)) @@ -368,11 +452,13 @@ Plugin* PluginManager::GetPlugin(const MClass* type) Plugin* PluginManager::GetPlugin(const ScriptingTypeHandle& type) { CHECK_RETURN(type, nullptr); +#if USE_EDITOR for (Plugin* p : EditorPlugins) { if (p->Is(type)) return p; } +#endif for (GamePlugin* gp : GamePlugins) { if (gp->Is(type)) @@ -386,18 +472,32 @@ Plugin* PluginManager::GetPlugin(const ScriptingTypeHandle& type) void PluginManager::InitializeGamePlugins() { PROFILE_CPU(); - for (int32 i = 0; i < GamePlugins.Count(); i++) + + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto afterTypeField = pluginLoadOrderAttribute->GetField("InitializeAfter"); + ASSERT(afterTypeField); + + auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, afterTypeField); + for (int32 i = 0; i < gamePlugins.Count(); i++) { - PluginManagerService::InvokeInitialize(GamePlugins[i]); + PluginManagerService::InvokeInitialize(gamePlugins[i]); } } void PluginManager::DeinitializeGamePlugins() { PROFILE_CPU(); - for (int32 i = GamePlugins.Count() - 1; i >= 0; i--) + + auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly; + auto pluginLoadOrderAttribute = engineAssembly->GetClass("FlaxEngine.PluginLoadOrderAttribute"); + auto beforeTypeField = pluginLoadOrderAttribute->GetField("DeinitializeBefore"); + ASSERT(beforeTypeField); + + auto gamePlugins = SortPlugins(GamePlugins, pluginLoadOrderAttribute, beforeTypeField); + for (int32 i = gamePlugins.Count() - 1; i >= 0; i--) { - PluginManagerService::InvokeDeinitialize(GamePlugins[i]); + PluginManagerService::InvokeDeinitialize(gamePlugins[i]); } } diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index a111ae337..200efa650 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include typedef char char_t; #define DOTNET_HOST_MONO_DEBUG 0 @@ -182,30 +183,23 @@ Dictionary CachedAssemblyHandles; /// void* GetStaticMethodPointer(const String& methodName); -/// -/// Calls the managed static method in NativeInterop class with given parameters. -/// -template -FORCE_INLINE RetType CallStaticMethodByName(const String& methodName, Args... args) -{ - typedef RetType (CORECLR_DELEGATE_CALLTYPE* fun)(Args...); - return ((fun)GetStaticMethodPointer(methodName))(args...); -} - /// /// Calls the managed static method with given parameters. /// template FORCE_INLINE RetType CallStaticMethod(void* methodPtr, Args... args) { +#if DOTNET_HOST_MONO + ASSERT_LOW_LAYER(mono_domain_get()); // Ensure that Mono runtime has been attached to this thread +#endif typedef RetType (CORECLR_DELEGATE_CALLTYPE* fun)(Args...); return ((fun)methodPtr)(args...); } -void RegisterNativeLibrary(const char* moduleName, const char* modulePath) +void RegisterNativeLibrary(const char* moduleName, const Char* modulePath) { static void* RegisterNativeLibraryPtr = GetStaticMethodPointer(TEXT("RegisterNativeLibrary")); - CallStaticMethod(RegisterNativeLibraryPtr, moduleName, modulePath); + CallStaticMethod(RegisterNativeLibraryPtr, moduleName, modulePath); } bool InitHostfxr(); @@ -273,7 +267,7 @@ bool MCore::LoadEngine() return true; // Prepare managed side - CallStaticMethodByName(TEXT("Init")); + CallStaticMethod(GetStaticMethodPointer(TEXT("Init"))); #ifdef MCORE_MAIN_MODULE_NAME // MCORE_MAIN_MODULE_NAME define is injected by Scripting.Build.cs on platforms that use separate shared library for engine symbols ::String flaxLibraryPath(Platform::GetMainDirectory() / TEXT(MACRO_TO_STR(MCORE_MAIN_MODULE_NAME))); @@ -287,12 +281,13 @@ bool MCore::LoadEngine() flaxLibraryPath = ::String(StringUtils::GetDirectoryName(Platform::GetExecutableFilePath())) / StringUtils::GetFileName(flaxLibraryPath); } #endif - RegisterNativeLibrary("FlaxEngine", StringAnsi(flaxLibraryPath).Get()); + RegisterNativeLibrary("FlaxEngine", flaxLibraryPath.Get()); MRootDomain = New("Root"); MDomains.Add(MRootDomain); - char* buildInfo = CallStaticMethodByName(TEXT("GetRuntimeInformation")); + void* GetRuntimeInformationPtr = GetStaticMethodPointer(TEXT("GetRuntimeInformation")); + char* buildInfo = CallStaticMethod(GetRuntimeInformationPtr); LOG(Info, ".NET runtime version: {0}", ::String(buildInfo)); MCore::GC::FreeMemory(buildInfo); @@ -304,7 +299,7 @@ void MCore::UnloadEngine() if (!MRootDomain) return; PROFILE_CPU(); - CallStaticMethodByName(TEXT("Exit")); + CallStaticMethod(GetStaticMethodPointer(TEXT("Exit"))); MDomains.ClearDelete(); MRootDomain = nullptr; ShutdownHostfxr(); @@ -730,7 +725,6 @@ DEFINE_INTERNAL_CALL(void) NativeInterop_CreateClass(NativeClassDefinitions* man StringAnsi assemblyName; StringAnsi assemblyFullName; GetAssemblyName(assemblyHandle, assemblyName, assemblyFullName); - assembly = New(nullptr, assemblyName, assemblyFullName, assemblyHandle); CachedAssemblyHandles.Add(assemblyHandle, assembly); } @@ -797,13 +791,13 @@ bool MAssembly::LoadImage(const String& assemblyPath, const StringView& nativePa if (nativePath.HasChars()) { StringAnsi nativeName = _name.EndsWith(".CSharp") ? StringAnsi(_name.Get(), _name.Length() - 7) : StringAnsi(_name); - RegisterNativeLibrary(nativeName.Get(), StringAnsi(nativePath).Get()); + RegisterNativeLibrary(nativeName.Get(), nativePath.Get()); } #if USE_EDITOR // Register the editor module location for Assembly resolver else { - RegisterNativeLibrary(_name.Get(), StringAnsi(assemblyPath).Get()); + RegisterNativeLibrary(_name.Get(), assemblyPath.Get()); } #endif @@ -1223,9 +1217,9 @@ MException::~MException() MField::MField(MClass* parentClass, void* handle, const char* name, void* type, int fieldOffset, MFieldAttributes attributes) : _handle(handle) , _type(type) + , _fieldOffset(fieldOffset) , _parentClass(parentClass) , _name(name) - , _fieldOffset(fieldOffset) , _hasCachedAttributes(false) { switch (attributes & MFieldAttributes::FieldAccessMask) @@ -1366,19 +1360,22 @@ MMethod::MMethod(MClass* parentClass, StringAnsi&& name, void* handle, int32 par void MMethod::CacheSignature() const { - _hasCachedSignature = true; + ScopeLock lock(BinaryModule::Locker); + if (_hasCachedSignature) + return; static void* GetMethodReturnTypePtr = GetStaticMethodPointer(TEXT("GetMethodReturnType")); static void* GetMethodParameterTypesPtr = GetStaticMethodPointer(TEXT("GetMethodParameterTypes")); - _returnType = CallStaticMethod(GetMethodReturnTypePtr, _handle); + if (_paramsCount != 0) + { + void** parameterTypeHandles; + CallStaticMethod(GetMethodParameterTypesPtr, _handle, ¶meterTypeHandles); + _parameterTypes.Set(parameterTypeHandles, _paramsCount); + MCore::GC::FreeMemory(parameterTypeHandles); + } - if (_paramsCount == 0) - return; - void** parameterTypeHandles; - CallStaticMethod(GetMethodParameterTypesPtr, _handle, ¶meterTypeHandles); - _parameterTypes.Set(parameterTypeHandles, _paramsCount); - MCore::GC::FreeMemory(parameterTypeHandles); + _hasCachedSignature = true; } MObject* MMethod::Invoke(void* instance, void** params, MObject** exception) const @@ -1434,7 +1431,7 @@ MType* MMethod::GetParameterType(int32 paramIdx) const if (!_hasCachedSignature) CacheSignature(); ASSERT_LOW_LAYER(paramIdx >= 0 && paramIdx < _paramsCount); - return (MType*)_parameterTypes[paramIdx]; + return (MType*)_parameterTypes.Get()[paramIdx]; } bool MMethod::GetParameterIsOut(int32 paramIdx) const @@ -1789,6 +1786,7 @@ void* GetStaticMethodPointer(const String& methodName) void* fun; if (CachedFunctions.TryGet(methodName, fun)) return fun; + PROFILE_CPU(); const int rc = get_function_pointer(NativeInteropTypeName, FLAX_CORECLR_STRING(methodName).Get(), UNMANAGEDCALLERSONLY_METHOD, nullptr, nullptr, &fun); if (rc != 0) LOG(Fatal, "Failed to get unmanaged function pointer for method {0}: 0x{1:x}", methodName.Get(), (unsigned int)rc); @@ -2010,6 +2008,9 @@ bool InitHostfxr() //Platform::SetEnvironmentVariable(TEXT("MONO_GC_DEBUG"), TEXT("6:gc-log.txt,check-remset-consistency,nursery-canaries")); #endif + // Adjust GC threads suspending mode to not block attached native threads (eg. Job System) + Platform::SetEnvironmentVariable(TEXT("MONO_THREADS_SUSPEND"), TEXT("preemptive")); + #if defined(USE_MONO_AOT_MODE) // Enable AOT mode (per-platform) mono_jit_set_aot_mode(USE_MONO_AOT_MODE); @@ -2054,7 +2055,7 @@ bool InitHostfxr() // Setup debugger { int32 debuggerLogLevel = 0; - if (CommandLine::Options.MonoLog.IsTrue()) + if (CommandLine::Options.MonoLog.IsTrue() || DOTNET_HOST_MONO_DEBUG) { LOG(Info, "Using detailed Mono logging"); mono_trace_set_level_string("debug"); @@ -2137,6 +2138,7 @@ bool InitHostfxr() LOG(Fatal, "Failed to initialize Mono."); return true; } + mono_gc_init_finalizer_thread(); // Log info char* buildInfo = mono_get_runtime_build_info(); @@ -2161,6 +2163,7 @@ void* GetStaticMethodPointer(const String& methodName) void* fun; if (CachedFunctions.TryGet(methodName, fun)) return fun; + PROFILE_CPU(); static MonoClass* nativeInteropClass = nullptr; if (!nativeInteropClass) diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp index e1494cd04..bdf9f60f5 100644 --- a/Source/Engine/Scripting/Scripting.cpp +++ b/Source/Engine/Scripting/Scripting.cpp @@ -28,8 +28,8 @@ #include "Engine/Content/Content.h" #include "Engine/Engine/EngineService.h" #include "Engine/Engine/Globals.h" +#include "Engine/Engine/Time.h" #include "Engine/Graphics/RenderTask.h" -#include "Engine/Platform/MemoryStats.h" #include "Engine/Serialization/JsonTools.h" extern void registerFlaxEngineInternalCalls(); @@ -193,6 +193,14 @@ void ScriptingService::Update() { PROFILE_CPU_NAMED("Scripting::Update"); INVOKE_EVENT(Update); + +#ifdef USE_NETCORE + // Force GC to run in background periodically to avoid large blocking collections causing hitches + if (Time::Update.TicksCount % 60 == 0) + { + MCore::GC::Collect(MCore::GC::MaxGeneration(), MGCCollectionMode::Forced, false, false); + } +#endif } void ScriptingService::LateUpdate() @@ -474,30 +482,32 @@ bool Scripting::Load() // Load FlaxEngine const String flaxEnginePath = Globals::BinariesFolder / TEXT("FlaxEngine.CSharp.dll"); auto* flaxEngineModule = (NativeBinaryModule*)GetBinaryModuleFlaxEngine(); - if (flaxEngineModule->Assembly->Load(flaxEnginePath)) + if (!flaxEngineModule->Assembly->IsLoaded()) { - LOG(Error, "Failed to load FlaxEngine C# assembly."); - return true; - } + if (flaxEngineModule->Assembly->Load(flaxEnginePath)) + { + LOG(Error, "Failed to load FlaxEngine C# assembly."); + return true; + } + onEngineLoaded(flaxEngineModule->Assembly); - onEngineLoaded(flaxEngineModule->Assembly); - - // Insert type aliases for vector types that don't exist in C++ but are just typedef (properly redirect them to actual types) - // TODO: add support for automatic typedef aliases setup for scripting module to properly lookup type from the alias typename + // Insert type aliases for vector types that don't exist in C++ but are just typedef (properly redirect them to actual types) + // TODO: add support for automatic typedef aliases setup for scripting module to properly lookup type from the alias typename #if USE_LARGE_WORLDS - flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector2"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Double2"]; - flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector3"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Double3"]; - flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector4"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Double4"]; + flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector2"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Double2"]; + flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector3"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Double3"]; + flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector4"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Double4"]; #else - flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector2"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Float2"]; - flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector3"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Float3"]; - flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector4"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Float4"]; + flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector2"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Float2"]; + flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector3"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Float3"]; + flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector4"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Float4"]; #endif #if USE_CSHARP - flaxEngineModule->ClassToTypeIndex[flaxEngineModule->Assembly->GetClass("FlaxEngine.Vector2")] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector2"]; - flaxEngineModule->ClassToTypeIndex[flaxEngineModule->Assembly->GetClass("FlaxEngine.Vector3")] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector3"]; - flaxEngineModule->ClassToTypeIndex[flaxEngineModule->Assembly->GetClass("FlaxEngine.Vector4")] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector4"]; + flaxEngineModule->ClassToTypeIndex[flaxEngineModule->Assembly->GetClass("FlaxEngine.Vector2")] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector2"]; + flaxEngineModule->ClassToTypeIndex[flaxEngineModule->Assembly->GetClass("FlaxEngine.Vector3")] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector3"]; + flaxEngineModule->ClassToTypeIndex[flaxEngineModule->Assembly->GetClass("FlaxEngine.Vector4")] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector4"]; #endif + } #if USE_EDITOR // Skip loading game modules in Editor on startup - Editor loads them later during splash screen (eg. after first compilation) diff --git a/Source/Engine/Scripting/Scripting.cs b/Source/Engine/Scripting/Scripting.cs index 8347fe7f8..0630985f3 100644 --- a/Source/Engine/Scripting/Scripting.cs +++ b/Source/Engine/Scripting/Scripting.cs @@ -135,13 +135,10 @@ namespace FlaxEngine { if (e.ExceptionObject is Exception exception) { + Debug.LogError($"Unhandled Exception: {exception.Message}"); + Debug.LogException(exception); if (e.IsTerminating && !System.Diagnostics.Debugger.IsAttached) Platform.Fatal($"Unhandled Exception: {exception}"); - else - { - Debug.LogError($"Unhandled Exception: {exception.Message}"); - Debug.LogException(exception); - } } } @@ -278,19 +275,30 @@ namespace FlaxEngine BackgroundNormal = Color.FromBgra(0xFF3F3F46), BorderNormal = Color.FromBgra(0xFF54545C), TextBoxBackground = Color.FromBgra(0xFF333337), - ProgressNormal = Color.FromBgra(0xFF0ad328), TextBoxBackgroundSelected = Color.FromBgra(0xFF3F3F46), CollectionBackgroundColor = Color.FromBgra(0x14CCCCCC), - SharedTooltip = new Tooltip(), - Statusbar = new Style.StatusbarStyle() + ProgressNormal = Color.FromBgra(0xFF0ad328), + Statusbar = new Style.StatusbarStyle { PlayMode = Color.FromBgra(0xFF2F9135), Failed = Color.FromBgra(0xFF9C2424), - Loading = Color.FromBgra(0xFF2D2D30) - } + Loading = Color.FromBgra(0xFF2D2D30), + }, + + SharedTooltip = new Tooltip(), }; style.DragWindow = style.BackgroundSelected * 0.7f; + // Use optionally bundled default font (matches Editor) + var defaultFont = Content.LoadAsyncInternal("Editor/Fonts/Roboto-Regular"); + if (defaultFont) + { + style.FontTitle = defaultFont.CreateFont(18); + style.FontLarge = defaultFont.CreateFont(14); + style.FontMedium = defaultFont.CreateFont(9); + style.FontSmall = defaultFont.CreateFont(9); + } + Style.Current = style; } diff --git a/Source/Engine/Scripting/ScriptingObjectReference.h b/Source/Engine/Scripting/ScriptingObjectReference.h index 0012c89ba..0945d6fc2 100644 --- a/Source/Engine/Scripting/ScriptingObjectReference.h +++ b/Source/Engine/Scripting/ScriptingObjectReference.h @@ -47,8 +47,12 @@ public: /// ~ScriptingObjectReferenceBase() { - if (_object) - _object->Deleted.Unbind(this); + ScriptingObject* obj = _object; + if (obj) + { + _object = nullptr; + obj->Deleted.Unbind(this); + } } public: diff --git a/Source/Engine/Scripting/SoftObjectReference.h b/Source/Engine/Scripting/SoftObjectReference.h index 3366dbbc7..174cf4188 100644 --- a/Source/Engine/Scripting/SoftObjectReference.h +++ b/Source/Engine/Scripting/SoftObjectReference.h @@ -38,8 +38,12 @@ public: /// ~SoftObjectReferenceBase() { - if (_object) - _object->Deleted.Unbind(this); + ScriptingObject* obj = _object; + if (obj) + { + _object = nullptr; + obj->Deleted.Unbind(this); + } } public: diff --git a/Source/Engine/Serialization/Serialization.h b/Source/Engine/Serialization/Serialization.h index 94acd1d79..f9dc0870a 100644 --- a/Source/Engine/Serialization/Serialization.h +++ b/Source/Engine/Serialization/Serialization.h @@ -405,14 +405,14 @@ namespace Serialization // ISerializable - inline bool ShouldSerialize(ISerializable& v, const void* otherObj) + inline bool ShouldSerialize(const ISerializable& v, const void* otherObj) { return true; } - inline void Serialize(ISerializable::SerializeStream& stream, ISerializable& v, const void* otherObj) + inline void Serialize(ISerializable::SerializeStream& stream, const ISerializable& v, const void* otherObj) { stream.StartObject(); - v.Serialize(stream, otherObj); + const_cast(&v)->Serialize(stream, otherObj); stream.EndObject(); } inline void Deserialize(ISerializable::DeserializeStream& stream, ISerializable& v, ISerializeModifier* modifier) @@ -421,15 +421,15 @@ namespace Serialization } template - inline typename TEnableIf::Value, bool>::Type ShouldSerialize(ISerializable& v, const void* otherObj) + inline typename TEnableIf::Value, bool>::Type ShouldSerialize(const ISerializable& v, const void* otherObj) { return true; } template - inline typename TEnableIf::Value>::Type Serialize(ISerializable::SerializeStream& stream, ISerializable& v, const void* otherObj) + inline typename TEnableIf::Value>::Type Serialize(ISerializable::SerializeStream& stream, const ISerializable& v, const void* otherObj) { stream.StartObject(); - v.Serialize(stream, otherObj); + const_cast(&v)->Serialize(stream, otherObj); stream.EndObject(); } template @@ -441,12 +441,12 @@ namespace Serialization // Scripting Object template - inline typename TEnableIf::Value, bool>::Type ShouldSerialize(T*& v, const void* otherObj) + inline typename TEnableIf::Value, bool>::Type ShouldSerialize(const T*& v, const void* otherObj) { return !otherObj || v != *(T**)otherObj; } template - inline typename TEnableIf::Value>::Type Serialize(ISerializable::SerializeStream& stream, T*& v, const void* otherObj) + inline typename TEnableIf::Value>::Type Serialize(ISerializable::SerializeStream& stream, const T*& v, const void* otherObj) { stream.Guid(v ? v->GetID() : Guid::Empty); } @@ -568,12 +568,13 @@ namespace Serialization { if (!otherObj) return true; - const auto other = (Array*)otherObj; + const auto other = (const Array*)otherObj; if (v.Count() != other->Count()) return true; + const T* vPtr = v.Get(); for (int32 i = 0; i < v.Count(); i++) { - if (ShouldSerialize((T&)v[i], (const void*)&other->At(i))) + if (ShouldSerialize(vPtr[i], (const void*)&other->At(i))) return true; } return false; @@ -582,8 +583,9 @@ namespace Serialization inline void Serialize(ISerializable::SerializeStream& stream, const Array& v, const void* otherObj) { stream.StartArray(); + const T* vPtr = v.Get(); for (int32 i = 0; i < v.Count(); i++) - Serialize(stream, (T&)v[i], nullptr); + Serialize(stream, vPtr[i], nullptr); stream.EndArray(); } template @@ -593,8 +595,9 @@ namespace Serialization { const auto& streamArray = stream.GetArray(); v.Resize(streamArray.Size()); + T* vPtr = v.Get(); for (int32 i = 0; i < v.Count(); i++) - Deserialize(streamArray[i], v[i], modifier); + Deserialize(streamArray[i], vPtr[i], modifier); } else if (TIsPODType::Value && stream.IsString()) { diff --git a/Source/Engine/ShadersCompilation/ShaderCompiler.cpp b/Source/Engine/ShadersCompilation/ShaderCompiler.cpp index 291910b11..da67ef3b4 100644 --- a/Source/Engine/ShadersCompilation/ShaderCompiler.cpp +++ b/Source/Engine/ShadersCompilation/ShaderCompiler.cpp @@ -3,6 +3,7 @@ #if COMPILE_WITH_SHADER_COMPILER #include "ShaderCompiler.h" +#include "ShadersCompilation.h" #include "Engine/Core/Log.h" #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Engine/Globals.h" @@ -14,10 +15,6 @@ #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Serialization/MemoryWriteStream.h" #include "Engine/Utilities/StringConverter.h" -#if USE_EDITOR -#include "Editor/Editor.h" -#include "Editor/ProjectInfo.h" -#endif namespace IncludedFiles { @@ -30,31 +27,6 @@ namespace IncludedFiles CriticalSection Locker; Dictionary Files; - -#if USE_EDITOR - bool FindProject(const ProjectInfo* project, HashSet& projects, const StringView& projectName, String& path) - { - if (!project || projects.Contains(project)) - return false; - projects.Add(project); - - // Check the project name - if (project->Name == projectName) - { - path = project->ProjectFolderPath; - return true; - } - - // Initialize referenced projects - for (const auto& reference : project->References) - { - if (reference.Project && FindProject(reference.Project, projects, projectName, path)) - return true; - } - - return false; - } -#endif } bool ShaderCompiler::Compile(ShaderCompilationContext* context) @@ -124,7 +96,8 @@ bool ShaderCompiler::Compile(ShaderCompilationContext* context) output->WriteInt32(context->Includes.Count()); for (auto& include : context->Includes) { - output->WriteString(include.Item, 11); + String compactPath = ShadersCompilation::CompactShaderPath(include.Item); + output->WriteString(compactPath, 11); const auto date = FileSystem::GetFileLastEditTime(include.Item); output->Write(date); } @@ -138,75 +111,17 @@ bool ShaderCompiler::GetIncludedFileSource(ShaderCompilationContext* context, co source = nullptr; sourceLength = 0; - // Skip to the last root start './' but preserve the leading one - const int32 includedFileLength = StringUtils::Length(includedFile); - for (int32 i = includedFileLength - 2; i >= 2; i--) + // Get actual file path + const String includedFileName(includedFile); + String path = ShadersCompilation::ResolveShaderPath(includedFileName); + if (!FileSystem::FileExists(path)) { - if (StringUtils::Compare(includedFile + i, "./", 2) == 0) - { - includedFile = includedFile + i; - break; - } + LOG(Error, "Unknown shader source file '{0}' included in '{1}'.{2}", includedFileName, String(sourceFile), String::Empty); + return true; } ScopeLock lock(IncludedFiles::Locker); - // Find the included file path - String path; -#if USE_EDITOR - if (StringUtils::Compare(includedFile, "./", 2) == 0) - { - int32 projectNameEnd = -1; - for (int32 i = 2; i < includedFileLength; i++) - { - if (includedFile[i] == '/') - { - projectNameEnd = i; - break; - } - } - if (projectNameEnd == -1) - { - LOG(Error, "Unknown shader source file '{0}' included in '{1}'.{2}", String(includedFile), String(sourceFile), TEXT("Missing project name after root path.")); - return true; - } - const StringAsUTF16<120> projectName(includedFile + 2, projectNameEnd - 2); - if (StringUtils::Compare(projectName.Get(), TEXT("FlaxPlatforms")) == 0) - { - // Hard-coded redirect to platform-specific includes - path /= Globals::StartupFolder / TEXT("Source/Platforms"); - } - else - { - HashSet projects; - if (!IncludedFiles::FindProject(Editor::Project, projects, StringView(projectName.Get(), projectNameEnd - 2), path)) - { - LOG(Error, "Unknown shader source file '{0}' included in '{1}'.{2}", String(includedFile), String(sourceFile), TEXT("Failed to find the project of the given name.")); - return true; - } - path /= TEXT("Source/Shaders/"); - } - const StringAsUTF16<250> localPath(includedFile + projectNameEnd + 1, includedFileLength - projectNameEnd - 1); - path /= localPath.Get(); - } -#else - if (StringUtils::Compare(includedFile, "./Flax/", 7) == 0) - { - // Engine project relative shader path - const auto includedFileStr = String(includedFile + 6); - path = Globals::StartupFolder / TEXT("Source/Shaders") / includedFileStr; - } -#endif - else if (FileSystem::FileExists(path = String(includedFile))) - { - // Absolute shader path - } - else - { - LOG(Error, "Unknown shader source file '{0}' included in '{1}'.{2}", String(includedFile), String(sourceFile), String::Empty); - return true; - } - // Try to reuse file IncludedFiles::File* result = nullptr; if (!IncludedFiles::Files.TryGet(path, result) || FileSystem::GetFileLastEditTime(path) > result->LastEditTime) @@ -460,16 +375,15 @@ bool ShaderCompiler::WriteCustomDataVS(ShaderCompilationContext* context, Shader auto& element = layout[a]; if (!layoutVisible[a]) continue; - - // TODO: serialize whole struct? - - output->WriteByte(static_cast(element.Type)); - output->WriteByte(element.Index); - output->WriteByte(static_cast(element.Format)); - output->WriteByte(element.InputSlot); - output->WriteUint32(element.AlignedByteOffset); - output->WriteByte(element.InputSlotClass); - output->WriteUint32(element.InstanceDataStepRate); + GPUShaderProgramVS::InputElement data; + data.Type = static_cast(element.Type); + data.Index = element.Index; + data.Format = static_cast(element.Format); + data.InputSlot = element.InputSlot; + data.AlignedByteOffset = element.AlignedByteOffset; + data.InputSlotClass = element.InputSlotClass; + data.InstanceDataStepRate = element.InstanceDataStepRate; + output->Write(data); } return false; diff --git a/Source/Engine/ShadersCompilation/ShadersCompilation.cpp b/Source/Engine/ShadersCompilation/ShadersCompilation.cpp index ce4116134..793a3c1c3 100644 --- a/Source/Engine/ShadersCompilation/ShadersCompilation.cpp +++ b/Source/Engine/ShadersCompilation/ShadersCompilation.cpp @@ -28,10 +28,11 @@ #include "Engine/ContentImporters/AssetsImportingManager.h" #include "Engine/Platform/FileSystemWatcher.h" #include "Engine/Platform/FileSystem.h" +#include "Engine/Platform/File.h" +#include "Engine/Engine/Globals.h" #include "Editor/Editor.h" #include "Editor/ProjectInfo.h" #endif - #if COMPILE_WITH_D3D_SHADER_COMPILER #include "DirectX/ShaderCompilerD3D.h" #endif @@ -53,6 +54,49 @@ namespace ShadersCompilationImpl CriticalSection Locker; Array Compilers; Array ReadyCompilers; + +#if USE_EDITOR + const ProjectInfo* FindProjectByName(const ProjectInfo* project, HashSet& projects, const StringView& projectName) + { + if (!project || projects.Contains(project)) + return nullptr; + projects.Add(project); + + // Check the project name + if (project->Name == projectName) + return project; + + // Search referenced projects + for (const auto& reference : project->References) + { + const ProjectInfo* result = FindProjectByName(reference.Project, projects, projectName); + if (result) + return result; + } + return nullptr; + } + + const ProjectInfo* FindProjectByPath(const ProjectInfo* project, HashSet& projects, const StringView& projectPath) + { + if (!project || projects.Contains(project)) + return nullptr; + projects.Add(project); + + // Search referenced projects (depth first to handle plugin projects first) + for (const auto& reference : project->References) + { + const ProjectInfo* result = FindProjectByPath(reference.Project, projects, projectPath); + if (result) + return result; + } + + // Check the project path + if (projectPath.StartsWith(project->ProjectFolderPath)) + return project; + + return nullptr; + } +#endif } using namespace ShadersCompilationImpl; @@ -143,9 +187,22 @@ bool ShadersCompilation::Compile(ShaderCompilationOptions& options) #endif } - // Print info if succeed - if (result == false) + if (result) { +#if USE_EDITOR + // Output shader source to easily investigate errors (eg. for generated shaders like materials or particles) + const String outputSourceFolder = Globals::ProjectCacheFolder / TEXT("/Shaders/Source"); + const String outputSourcePath = outputSourceFolder / options.TargetName + TEXT(".hlsl"); + if (!FileSystem::DirectoryExists(outputSourceFolder)) + FileSystem::CreateDirectory(outputSourceFolder); + File::WriteAllBytes(outputSourcePath, (const byte*)options.Source, options.SourceLength); + LOG(Error, "Shader compilation '{0}' failed (profile: {1})", options.TargetName, ::ToString(options.Profile)); + LOG(Error, "Source: {0}", outputSourcePath); +#endif + } + else + { + // Success const DateTime endTime = DateTime::NowUTC(); LOG(Info, "Shader compilation '{0}' succeed in {1} ms (profile: {2})", options.TargetName, Math::CeilToInt(static_cast((endTime - startTime).GetTotalMilliseconds())), ::ToString(options.Profile)); } @@ -346,11 +403,89 @@ void ShadersCompilation::ExtractShaderIncludes(byte* shaderCache, int32 shaderCa { String& include = includes.AddOne(); stream.ReadString(&include, 11); + include = ShadersCompilation::ResolveShaderPath(include); DateTime lastEditTime; stream.Read(lastEditTime); } } +String ShadersCompilation::ResolveShaderPath(StringView path) +{ + // Skip to the last root start './' but preserve the leading one + for (int32 i = path.Length() - 2; i >= 2; i--) + { + if (StringUtils::Compare(path.Get() + i, TEXT("./"), 2) == 0) + { + path = path.Substring(i); + break; + } + } + + // Find the included file path + String result; +#if USE_EDITOR + if (path.StartsWith(StringView(TEXT("./"), 2))) + { + int32 projectNameEnd = -1; + for (int32 i = 2; i < path.Length(); i++) + { + if (path[i] == '/') + { + projectNameEnd = i; + break; + } + } + if (projectNameEnd == -1) + return String::Empty; // Invalid project path + StringView projectName = path.Substring(2, projectNameEnd - 2); + if (projectName.StartsWith(StringView(TEXT("FlaxPlatforms")))) + { + // Hard-coded redirect to platform-specific includes + result = Globals::StartupFolder / TEXT("Source/Platforms"); + } + else + { + HashSet projects; + const ProjectInfo* project = FindProjectByName(Editor::Project, projects, StringView(projectName.Get(), projectNameEnd - 2)); + if (project) + result = project->ProjectFolderPath / TEXT("/Source/Shaders/"); + else + return String::Empty; + } + result /= path.Substring(projectNameEnd + 1); + } +#else + if (path.StartsWith(StringView(TEXT("./Flax/"), 7))) + { + // Engine project relative shader path + result = Globals::StartupFolder / TEXT("Source/Shaders") / path.Substring(6); + } +#endif + else + { + // Absolute shader path + result = path; + } + + return result; +} + +String ShadersCompilation::CompactShaderPath(StringView path) +{ +#if USE_EDITOR + // Try to use file path relative to the project shader sources folder + HashSet projects; + const ProjectInfo* project = FindProjectByPath(Editor::Project, projects, path); + if (project) + { + String projectSourcesPath = project->ProjectFolderPath / TEXT("/Source/Shaders/"); + if (path.StartsWith(projectSourcesPath)) + return String::Format(TEXT("./{}/{}"), project->Name, path.Substring(projectSourcesPath.Length())); + } +#endif + return String(path); +} + #if USE_EDITOR namespace diff --git a/Source/Engine/ShadersCompilation/ShadersCompilation.h b/Source/Engine/ShadersCompilation/ShadersCompilation.h index fca6a6ebe..fd907acf7 100644 --- a/Source/Engine/ShadersCompilation/ShadersCompilation.h +++ b/Source/Engine/ShadersCompilation/ShadersCompilation.h @@ -14,7 +14,6 @@ class Asset; class FLAXENGINE_API ShadersCompilation { public: - /// /// Compiles the shader. /// @@ -43,6 +42,11 @@ public: /// The output included. static void ExtractShaderIncludes(byte* shaderCache, int32 shaderCacheLength, Array& includes); + // Resolves shader path name into absolute file path. Resolves './/ShaderFile.hlsl' cases into a full path. + static String ResolveShaderPath(StringView path); + // Compacts the full shader file path into portable format with project name prefix such as './/ShaderFile.hlsl'. + static String CompactShaderPath(StringView path); + private: static ShaderCompiler* CreateCompiler(ShaderProfile profile); diff --git a/Source/Engine/Tests/TestCollections.cpp b/Source/Engine/Tests/TestCollections.cpp index 21aeb1c36..ff5f34bf6 100644 --- a/Source/Engine/Tests/TestCollections.cpp +++ b/Source/Engine/Tests/TestCollections.cpp @@ -3,6 +3,8 @@ #include "Engine/Core/RandomStream.h" #include "Engine/Core/Collections/Array.h" #include "Engine/Core/Collections/BitArray.h" +#include "Engine/Core/Collections/HashSet.h" +#include "Engine/Core/Collections/Dictionary.h" #include TEST_CASE("Array") @@ -107,4 +109,185 @@ TEST_CASE("BitArray") a1 = testData; CHECK(a1 == testData); } + + SECTION("Test Set All") + { + BitArray<> a1; + a1.Resize(9); + CHECK(a1.Count() == 9); + a1.SetAll(true); + for (int32 i = 0; i < a1.Count(); i++) + CHECK(a1[i] == true); + a1.SetAll(false); + for (int32 i = 0; i < a1.Count(); i++) + CHECK(a1[i] == false); + } +} + +TEST_CASE("HashSet") +{ + SECTION("Test Allocators") + { + HashSet a1; + HashSet> a2; + HashSet> a3; + for (int32 i = 0; i < 7; i++) + { + a1.Add(i); + a2.Add(i); + a3.Add(i); + } + CHECK(a1.Count() == 7); + CHECK(a2.Count() == 7); + CHECK(a3.Count() == 7); + for (int32 i = 0; i < 7; i++) + { + CHECK(a1.Contains(i)); + CHECK(a2.Contains(i)); + CHECK(a3.Contains(i)); + } + } + + SECTION("Test Resizing") + { + HashSet a1; + for (int32 i = 0; i < 4000; i++) + a1.Add(i); + CHECK(a1.Count() == 4000); + int32 capacity = a1.Capacity(); + for (int32 i = 0; i < 4000; i++) + { + CHECK(a1.Contains(i)); + } + a1.Clear(); + CHECK(a1.Count() == 0); + CHECK(a1.Capacity() == capacity); + for (int32 i = 0; i < 4000; i++) + a1.Add(i); + CHECK(a1.Count() == 4000); + CHECK(a1.Capacity() == capacity); + for (int32 i = 0; i < 4000; i++) + a1.Remove(i); + CHECK(a1.Count() == 0); + CHECK(a1.Capacity() == capacity); + for (int32 i = 0; i < 4000; i++) + a1.Add(i); + CHECK(a1.Count() == 4000); + CHECK(a1.Capacity() == capacity); + } + + SECTION("Test Default Capacity") + { + HashSet a1; + a1.Add(1); + CHECK(a1.Capacity() <= DICTIONARY_DEFAULT_CAPACITY); + } + + SECTION("Test Add/Remove") + { + HashSet a1; + for (int32 i = 0; i < 4000; i++) + { + a1.Add(i); + a1.Remove(i); + } + CHECK(a1.Count() == 0); + CHECK(a1.Capacity() <= DICTIONARY_DEFAULT_CAPACITY); + a1.Clear(); + for (int32 i = 1; i <= 10; i++) + a1.Add(-i); + for (int32 i = 0; i < 4000; i++) + { + a1.Add(i); + a1.Remove(i); + } + CHECK(a1.Count() == 10); + CHECK(a1.Capacity() <= DICTIONARY_DEFAULT_CAPACITY); + } +} + +TEST_CASE("Dictionary") +{ + SECTION("Test Allocators") + { + Dictionary a1; + Dictionary> a2; + Dictionary> a3; + for (int32 i = 0; i < 7; i++) + { + a1.Add(i, i); + a2.Add(i, i); + a3.Add(i, i); + } + CHECK(a1.Count() == 7); + CHECK(a2.Count() == 7); + CHECK(a3.Count() == 7); + for (int32 i = 0; i < 7; i++) + { + CHECK(a1.ContainsKey(i)); + CHECK(a2.ContainsKey(i)); + CHECK(a3.ContainsKey(i)); + CHECK(a1.ContainsValue(i)); + CHECK(a2.ContainsValue(i)); + CHECK(a3.ContainsValue(i)); + } + } + + SECTION("Test Resizing") + { + Dictionary a1; + for (int32 i = 0; i < 4000; i++) + a1.Add(i, i); + CHECK(a1.Count() == 4000); + int32 capacity = a1.Capacity(); + for (int32 i = 0; i < 4000; i++) + { + CHECK(a1.ContainsKey(i)); + CHECK(a1.ContainsValue(i)); + } + a1.Clear(); + CHECK(a1.Count() == 0); + CHECK(a1.Capacity() == capacity); + for (int32 i = 0; i < 4000; i++) + a1.Add(i, i); + CHECK(a1.Count() == 4000); + CHECK(a1.Capacity() == capacity); + for (int32 i = 0; i < 4000; i++) + a1.Remove(i); + CHECK(a1.Count() == 0); + CHECK(a1.Capacity() == capacity); + for (int32 i = 0; i < 4000; i++) + a1.Add(i, i); + CHECK(a1.Count() == 4000); + CHECK(a1.Capacity() == capacity); + } + + SECTION("Test Default Capacity") + { + Dictionary a1; + a1.Add(1, 1); + CHECK(a1.Capacity() <= DICTIONARY_DEFAULT_CAPACITY); + } + + SECTION("Test Add/Remove") + { + Dictionary a1; + for (int32 i = 0; i < 4000; i++) + { + a1.Add(i, i); + a1.Remove(i); + } + CHECK(a1.Count() == 0); + CHECK(a1.Capacity() <= DICTIONARY_DEFAULT_CAPACITY); + a1.Clear(); + for (int32 i = 1; i <= 10; i++) + a1.Add(-i, -i); + for (int32 i = 0; i < 4000; i++) + { + a1.Add(i, i); + a1.Remove(i); + } + CHECK(a1.Count() == 10); + CHECK(a1.Capacity() <= DICTIONARY_DEFAULT_CAPACITY); + } } diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Layers.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Layers.cpp index 8db8a0d0d..fe45b8b96 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Layers.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Layers.cpp @@ -147,7 +147,7 @@ void MaterialGenerator::ProcessGroupLayers(Box* box, Node* node, Value& value) case 8: { const Value defaultValue = MaterialValue::InitForZero(VariantType::Void); - const Value alpha = tryGetValue(node->GetBox(2), 0, Value::Zero); + Value alpha = tryGetValue(node->GetBox(2), 0, Value::Zero).AsFloat(); if (alpha.IsZero()) { // Bottom-only @@ -178,6 +178,7 @@ void MaterialGenerator::ProcessGroupLayers(Box* box, Node* node, Value& value) auto topHeightScaled = writeLocal(VariantType::Float, String::Format(TEXT("{0} * {1}"), topHeight.Value, alpha.Value), node); auto heightStart = writeLocal(VariantType::Float, String::Format(TEXT("max({0}, {1}) - 0.05"), bottomHeightScaled.Value, topHeightScaled.Value), node); auto bottomLevel = writeLocal(VariantType::Float, String::Format(TEXT("max({0} - {1}, 0.0001)"), topHeightScaled.Value, heightStart.Value), node); + alpha = writeLocal(VariantType::Float, alpha.Value, node); _writer.Write(TEXT("\t{0} = {1} / (max({2} - {3}, 0) + {4});\n"), alpha.Value, bottomLevel.Value, bottomHeightScaled.Value, heightStart.Value, bottomLevel.Value); } #define EAT_BOX(type) writeBlending(MaterialGraphBoxes::type, value, bottom, top, alpha) diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index 8aa300731..f669d36d2 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp @@ -393,8 +393,8 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) // Sphere Mask case 28: { - const auto a = tryGetValue(node->GetBox(0), 0, Value::Zero); - const auto b = tryGetValue(node->GetBox(1), 1, Value::Zero).Cast(a.Type); + const auto a = tryGetValue(node->GetBox(0), getUVs); + const auto b = tryGetValue(node->GetBox(1), Value::Half).Cast(a.Type); const auto radius = tryGetValue(node->GetBox(2), node->Values[0]).AsFloat(); const auto hardness = tryGetValue(node->GetBox(3), node->Values[1]).AsFloat(); const auto invert = tryGetValue(node->GetBox(4), node->Values[2]).AsBool(); @@ -519,6 +519,40 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value) } break; } + // Rectangle Mask + case 40: + { + const auto uv = tryGetValue(node->GetBox(0), getUVs).AsFloat2(); + const auto rectangle = tryGetValue(node->GetBox(1), node->Values[0]).AsFloat2(); + auto d = writeLocal(ValueType::Float2, String::Format(TEXT("abs({0} * 2 - 1) - {1}"), uv.Value, rectangle.Value), node); + auto d2 = writeLocal(ValueType::Float2, String::Format(TEXT("1 - {0} / fwidth({0})"), d.Value), node); + value = writeLocal(ValueType::Float, String::Format(TEXT("saturate(min({0}.x, {0}.y))"), d2.Value), node); + break; + } + // FWidth + case 41: + { + const auto inValue = tryGetValue(node->GetBox(0), 0, Value::Zero); + value = writeLocal(inValue.Type, String::Format(TEXT("fwidth({0})"), inValue.Value), node); + break; + } + // AA Step + case 42: + { + // Reference: https://www.ronja-tutorials.com/post/046-fwidth/#a-better-step + + const auto compValue = tryGetValue(node->GetBox(0), getUVs).AsFloat(); + const auto gradient = tryGetValue(node->GetBox(1), node->Values[0]).AsFloat(); + + auto change = writeLocal(ValueType::Float, String::Format(TEXT("fwidth({0})"), gradient.Value), node); + + // Base the range of the inverse lerp on the change over two pixels + auto lowerEdge = writeLocal(ValueType::Float, String::Format(TEXT("{0} - {1}"), compValue.Value, change.Value), node); + auto upperEdge = writeLocal(ValueType::Float, String::Format(TEXT("{0} + {1}"), compValue.Value, change.Value), node); + + // Do the inverse interpolation and saturate it + value = writeLocal(ValueType::Float, String::Format(TEXT("saturate((({0} - {1}) / ({2} - {1})))"), gradient.Value, lowerEdge.Value, upperEdge.Value), node); + } default: break; } diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp index 01eb8d369..3d9be61d2 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp @@ -648,6 +648,60 @@ bool ImportMesh(int32 i, ImportedModelData& result, AssimpImporterData& data, St result.LODs.Resize(lodIndex + 1); result.LODs[lodIndex].Meshes.Add(meshData); } + + auto root = data.Scene->mRootNode; + Array points; + if (root->mNumChildren == 0) + { + aiQuaternion aiQuat; + aiVector3D aiPos; + aiVector3D aiScale; + root->mTransformation.Decompose(aiScale, aiQuat, aiPos); + auto quat = ToQuaternion(aiQuat); + auto pos = ToFloat3(aiPos); + auto scale = ToFloat3(aiScale); + Transform trans = Transform(pos, quat, scale); + points.Add(trans); + } + else + { + for (unsigned int j = 0; j < root->mNumChildren; j++) + { + aiQuaternion aiQuat; + aiVector3D aiPos; + aiVector3D aiScale; + root->mChildren[j]->mTransformation.Decompose(aiScale, aiQuat, aiPos); + auto quat = ToQuaternion(aiQuat); + auto pos = ToFloat3(aiPos); + auto scale = ToFloat3(aiScale); + Transform trans = Transform(pos, quat, scale); + points.Add(trans); + } + } + + Float3 translation = Float3::Zero; + Float3 scale = Float3::Zero; + Quaternion orientation = Quaternion::Identity; + for (auto point : points) + { + translation += point.Translation; + scale += point.Scale; + orientation *= point.Orientation; + } + + if (points.Count() > 0) + { + meshData->OriginTranslation = translation / (float)points.Count(); + meshData->OriginOrientation = Quaternion::Invert(orientation); + meshData->Scaling = scale / (float)points.Count(); + } + else + { + meshData->OriginTranslation = translation; + meshData->OriginOrientation = Quaternion::Invert(orientation); + meshData->Scaling = Float3(1); + } + return false; } diff --git a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp index f1e89d6bc..c7ee11b42 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp @@ -836,6 +836,20 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb mesh.TransformBuffer(geometryTransform); }*/ + // Get local transform for origin shifting translation + auto translation = ToMatrix(aMesh->getGlobalTransform()).GetTranslation(); + auto scale = data.GlobalSettings.UnitScaleFactor; + if (data.GlobalSettings.CoordAxis == ofbx::CoordSystem_RightHanded) + mesh.OriginTranslation = scale * Vector3(translation.X, translation.Y, -translation.Z); + else + mesh.OriginTranslation = scale * Vector3(translation.X, translation.Y, translation.Z); + + auto rot = aMesh->getLocalRotation(); + auto quat = Quaternion::Euler(-(float)rot.x, -(float)rot.y, -(float)rot.z); + mesh.OriginOrientation = quat; + + auto scaling = aMesh->getLocalScaling(); + mesh.Scaling = Vector3(scale * (float)scaling.x, scale * (float)scaling.y, scale * (float)scaling.z); return false; } diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index b703b47d1..a07cd8f23 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -22,6 +22,7 @@ #include "Engine/Core/Types/DateTime.h" #include "Engine/Core/Types/TimeSpan.h" #include "Engine/Core/Types/Pair.h" +#include "Engine/Core/Types/Variant.h" #include "Engine/Graphics/Models/SkeletonUpdater.h" #include "Engine/Graphics/Models/SkeletonMapping.h" #include "Engine/Core/Utilities.h" @@ -366,11 +367,13 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj SERIALIZE(ImportLODs); SERIALIZE(ImportVertexColors); SERIALIZE(ImportBlendShapes); + SERIALIZE(CalculateBoneOffsetMatrices); SERIALIZE(LightmapUVsSource); SERIALIZE(CollisionMeshesPrefix); SERIALIZE(Scale); SERIALIZE(Rotation); SERIALIZE(Translation); + SERIALIZE(UseLocalOrigin); SERIALIZE(CenterGeometry); SERIALIZE(Duration); SERIALIZE(FramesRange); @@ -396,6 +399,7 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj SERIALIZE(SDFResolution); SERIALIZE(SplitObjects); SERIALIZE(ObjectIndex); + SERIALIZE(SubAssetFolder); } void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) @@ -411,11 +415,13 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(ImportLODs); DESERIALIZE(ImportVertexColors); DESERIALIZE(ImportBlendShapes); + DESERIALIZE(CalculateBoneOffsetMatrices); DESERIALIZE(LightmapUVsSource); DESERIALIZE(CollisionMeshesPrefix); DESERIALIZE(Scale); DESERIALIZE(Rotation); DESERIALIZE(Translation); + DESERIALIZE(UseLocalOrigin); DESERIALIZE(CenterGeometry); DESERIALIZE(Duration); DESERIALIZE(FramesRange); @@ -441,6 +447,7 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(SDFResolution); DESERIALIZE(SplitObjects); DESERIALIZE(ObjectIndex); + DESERIALIZE(SubAssetFolder); // [Deprecated on 23.11.2021, expires on 21.11.2023] int32 AnimationIndex = -1; @@ -744,6 +751,32 @@ void MeshOptDeallocate(void* ptr) Allocator::Free(ptr); } +void TrySetupMaterialParameter(MaterialInstance* instance, Span paramNames, const Variant& value, MaterialParameterType type) +{ + for (const Char* name : paramNames) + { + for (MaterialParameter& param : instance->Params) + { + const MaterialParameterType paramType = param.GetParameterType(); + if (type != paramType) + { + if (type == MaterialParameterType::Color) + { + if (paramType != MaterialParameterType::Vector3 || + paramType != MaterialParameterType::Vector4) + continue; + } + else + continue; + } + if (StringUtils::CompareIgnoreCase(name, param.GetName().Get()) != 0) + continue; + param.SetValue(value); + return; + } + } +} + bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& options, String& errorMsg, const String& autoImportOutput) { LOG(Info, "Importing model from \'{0}\'", path); @@ -1014,9 +1047,18 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op { // Create material instance AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialInstanceTag, assetPath, material.AssetID); - if (MaterialInstance* materialInstance = Content::Load(assetPath)) + if (auto* materialInstance = Content::Load(assetPath)) { materialInstance->SetBaseMaterial(options.InstanceToImportAs); + + // Customize base material based on imported material (blind guess based on the common names used in materials) + const Char* diffuseColorNames[] = { TEXT("color"), TEXT("col"), TEXT("diffuse"), TEXT("basecolor"), TEXT("base color") }; + TrySetupMaterialParameter(materialInstance, ToSpan(diffuseColorNames, ARRAY_COUNT(diffuseColorNames)), material.Diffuse.Color, MaterialParameterType::Color); + const Char* emissiveColorNames[] = { TEXT("emissive"), TEXT("emission"), TEXT("light") }; + TrySetupMaterialParameter(materialInstance, ToSpan(emissiveColorNames, ARRAY_COUNT(emissiveColorNames)), material.Emissive.Color, MaterialParameterType::Color); + const Char* opacityValueNames[] = { TEXT("opacity"), TEXT("alpha") }; + TrySetupMaterialParameter(materialInstance, ToSpan(opacityValueNames, ARRAY_COUNT(opacityValueNames)), material.Opacity.Value, MaterialParameterType::Float); + materialInstance->Save(); } else @@ -1051,11 +1093,16 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op // Prepare import transformation Transform importTransform(options.Translation, options.Rotation, Float3(options.Scale)); + if (options.UseLocalOrigin && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems()) + { + importTransform.Translation -= importTransform.Orientation * data.LODs[0].Meshes[0]->OriginTranslation * importTransform.Scale; + } if (options.CenterGeometry && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems()) { // Calculate the bounding box (use LOD0 as a reference) BoundingBox box = data.LODs[0].GetBox(); - importTransform.Translation -= box.GetCenter(); + auto center = data.LODs[0].Meshes[0]->OriginOrientation * importTransform.Orientation * box.GetCenter() * importTransform.Scale * data.LODs[0].Meshes[0]->Scaling; + importTransform.Translation -= center; } const bool applyImportTransform = !importTransform.IsIdentity(); @@ -1380,6 +1427,15 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op SkeletonUpdater hierarchyUpdater(data.Nodes); hierarchyUpdater.UpdateMatrices(); + if (options.CalculateBoneOffsetMatrices) + { + // Calculate offset matrix (inverse bind pose transform) for every bone manually + for (SkeletonBone& bone : data.Skeleton.Bones) + { + CalculateBoneOffsetMatrix(data.Skeleton.Nodes, bone.OffsetMatrix, bone.NodeIndex); + } + } + // Move meshes in the new nodes for (int32 lodIndex = 0; lodIndex < data.LODs.Count(); lodIndex++) { @@ -1401,15 +1457,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op } } - // TODO: allow to link skeleton asset to model to retarget model bones skeleton for an animation - // use SkeletonMapping to map bones? - - // Calculate offset matrix (inverse bind pose transform) for every bone manually - /*for (SkeletonBone& bone : data.Skeleton.Bones) - { - CalculateBoneOffsetMatrix(data.Skeleton.Nodes, bone.OffsetMatrix, bone.NodeIndex); - }*/ - #if USE_SKELETON_NODES_SORTING // Sort skeleton nodes and bones hierarchy (parents first) // Then it can be used with a simple linear loop update diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index 36bb9b12c..c7876e826 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -258,6 +258,9 @@ public: // Enable/disable importing blend shapes (morph targets). API_FIELD(Attributes="EditorOrder(85), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowSkinnedModel))") bool ImportBlendShapes = false; + // Enable skeleton bones offset matrices recalculating. + API_FIELD(Attributes="EditorOrder(86), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowSkinnedModel))") + bool CalculateBoneOffsetMatrices = false; // The lightmap UVs source. API_FIELD(Attributes="EditorOrder(90), EditorDisplay(\"Geometry\", \"Lightmap UVs Source\"), VisibleIf(nameof(ShowModel))") ModelLightmapUVsSource LightmapUVsSource = ModelLightmapUVsSource::Disable; @@ -279,8 +282,11 @@ public: // Custom import geometry offset. API_FIELD(Attributes="EditorOrder(520), EditorDisplay(\"Transform\")") Float3 Translation = Float3::Zero; - // If checked, the imported geometry will be shifted to the center of mass. + // If checked, the imported geometry will be shifted to its local transform origin. API_FIELD(Attributes="EditorOrder(530), EditorDisplay(\"Transform\")") + bool UseLocalOrigin = false; + // If checked, the imported geometry will be shifted to the center of mass. + API_FIELD(Attributes="EditorOrder(540), EditorDisplay(\"Transform\")") bool CenterGeometry = false; public: // Animation @@ -370,6 +376,12 @@ public: API_FIELD(Attributes="EditorOrder(2010), EditorDisplay(\"Splitting\")") int32 ObjectIndex = -1; + public: // Other + + // If specified, will be used as sub-directory name for automatically imported sub assets such as textures and materials. Set to whitespace (single space) to import to the same directory. + API_FIELD(Attributes="EditorOrder(3030), EditorDisplay(\"Other\")") + String SubAssetFolder = TEXT(""); + // Runtime data for objects splitting during import (used internally) void* SplitContext = nullptr; Function OnSplitImport; diff --git a/Source/Engine/UI/GUI/CanvasContainer.cs b/Source/Engine/UI/GUI/CanvasContainer.cs index f032a99d3..084a22503 100644 --- a/Source/Engine/UI/GUI/CanvasContainer.cs +++ b/Source/Engine/UI/GUI/CanvasContainer.cs @@ -41,11 +41,12 @@ namespace FlaxEngine.GUI protected override void DrawChildren() { // Draw all screen space canvases + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = 0; i < _children.Count; i++) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Is2D) + if (child.Visible && child.Is2D && layerMask.HasLayer(child.Canvas.Layer)) { child.Draw(); } @@ -69,10 +70,11 @@ namespace FlaxEngine.GUI UICanvas.CalculateRay(ref location, out Ray ray); // Test 3D + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Enabled && child.Is3D) + if (child.Visible && child.Enabled && child.Is3D && layerMask.HasLayer(child.Canvas.Layer)) { if (child.Intersects3D(ref ray, out var childLocation)) { @@ -91,10 +93,11 @@ namespace FlaxEngine.GUI // Check all children collisions with mouse and fire events for them bool isFirst3DHandled = false; + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Enabled) + if (child.Visible && child.Enabled && layerMask.HasLayer(child.Canvas.Layer)) { // Fire events if (child.Is2D) @@ -156,10 +159,11 @@ namespace FlaxEngine.GUI UICanvas.CalculateRay(ref location, out Ray ray); // Test 3D + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Enabled && child.Is3D) + if (child.Visible && child.Enabled && child.Is3D && layerMask.HasLayer(child.Canvas.Layer)) { if (child.Intersects3D(ref ray, out var childLocation)) { @@ -183,10 +187,11 @@ namespace FlaxEngine.GUI UICanvas.CalculateRay(ref location, out Ray ray); // Test 3D + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Enabled && child.Is3D) + if (child.Visible && child.Enabled && child.Is3D && layerMask.HasLayer(child.Canvas.Layer)) { if (child.Intersects3D(ref ray, out var childLocation)) { @@ -210,10 +215,11 @@ namespace FlaxEngine.GUI UICanvas.CalculateRay(ref location, out Ray ray); // Test 3D + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Enabled && child.Is3D) + if (child.Visible && child.Enabled && child.Is3D && layerMask.HasLayer(child.Canvas.Layer)) { if (child.Intersects3D(ref ray, out var childLocation)) { @@ -237,10 +243,11 @@ namespace FlaxEngine.GUI UICanvas.CalculateRay(ref location, out Ray ray); // Test 3D + var layerMask = MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default; for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = (CanvasRootControl)_children[i]; - if (child.Visible && child.Enabled && child.Is3D) + if (child.Visible && child.Enabled && child.Is3D && layerMask.HasLayer(child.Canvas.Layer)) { if (child.Intersects3D(ref ray, out var childLocation)) { diff --git a/Source/Engine/UI/GUI/CanvasRootControl.cs b/Source/Engine/UI/GUI/CanvasRootControl.cs index b4fa067e3..b2ea9aaa0 100644 --- a/Source/Engine/UI/GUI/CanvasRootControl.cs +++ b/Source/Engine/UI/GUI/CanvasRootControl.cs @@ -74,6 +74,8 @@ namespace FlaxEngine.GUI return false; } + private bool SkipEvents => !_canvas.ReceivesEvents || !_canvas.IsVisible(); + /// public override CursorType Cursor { @@ -197,7 +199,7 @@ namespace FlaxEngine.GUI public override void Update(float deltaTime) { // UI navigation - if (_canvas.ReceivesEvents) + if (SkipEvents) { UpdateNavigation(deltaTime, _canvas.NavigateUp.Name, NavDirection.Up, ref _navigationHeldTimeUp, ref _navigationRateTimeUp); UpdateNavigation(deltaTime, _canvas.NavigateDown.Name, NavDirection.Down, ref _navigationHeldTimeDown, ref _navigationRateTimeDown); @@ -267,7 +269,7 @@ namespace FlaxEngine.GUI /// public override bool OnCharInput(char c) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnCharInput(c); @@ -276,7 +278,7 @@ namespace FlaxEngine.GUI /// public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return DragDropEffect.None; return base.OnDragDrop(ref location, data); @@ -285,7 +287,7 @@ namespace FlaxEngine.GUI /// public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return DragDropEffect.None; return base.OnDragEnter(ref location, data); @@ -294,7 +296,7 @@ namespace FlaxEngine.GUI /// public override void OnDragLeave() { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; base.OnDragLeave(); @@ -303,7 +305,7 @@ namespace FlaxEngine.GUI /// public override DragDropEffect OnDragMove(ref Float2 location, DragData data) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return DragDropEffect.None; return base.OnDragMove(ref location, data); @@ -312,7 +314,7 @@ namespace FlaxEngine.GUI /// public override bool OnKeyDown(KeyboardKeys key) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnKeyDown(key); @@ -321,7 +323,7 @@ namespace FlaxEngine.GUI /// public override void OnKeyUp(KeyboardKeys key) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; base.OnKeyUp(key); @@ -330,7 +332,7 @@ namespace FlaxEngine.GUI /// public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnMouseDoubleClick(location, button); @@ -339,7 +341,7 @@ namespace FlaxEngine.GUI /// public override bool OnMouseDown(Float2 location, MouseButton button) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnMouseDown(location, button); @@ -348,7 +350,7 @@ namespace FlaxEngine.GUI /// public override void OnMouseEnter(Float2 location) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; _mousePosition = location; @@ -359,8 +361,7 @@ namespace FlaxEngine.GUI public override void OnMouseLeave() { _mousePosition = Float2.Zero; - - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; base.OnMouseLeave(); @@ -369,7 +370,7 @@ namespace FlaxEngine.GUI /// public override void OnMouseMove(Float2 location) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; _mousePosition = location; @@ -379,7 +380,7 @@ namespace FlaxEngine.GUI /// public override bool OnMouseUp(Float2 location, MouseButton button) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnMouseUp(location, button); @@ -388,7 +389,7 @@ namespace FlaxEngine.GUI /// public override bool OnMouseWheel(Float2 location, float delta) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnMouseWheel(location, delta); @@ -397,7 +398,7 @@ namespace FlaxEngine.GUI /// public override void OnTouchEnter(Float2 location, int pointerId) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; base.OnTouchEnter(location, pointerId); @@ -406,7 +407,7 @@ namespace FlaxEngine.GUI /// public override bool OnTouchDown(Float2 location, int pointerId) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnTouchDown(location, pointerId); @@ -415,7 +416,7 @@ namespace FlaxEngine.GUI /// public override void OnTouchMove(Float2 location, int pointerId) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; base.OnTouchMove(location, pointerId); @@ -424,7 +425,7 @@ namespace FlaxEngine.GUI /// public override bool OnTouchUp(Float2 location, int pointerId) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return false; return base.OnTouchUp(location, pointerId); @@ -433,7 +434,7 @@ namespace FlaxEngine.GUI /// public override void OnTouchLeave(int pointerId) { - if (!_canvas.ReceivesEvents) + if (SkipEvents) return; base.OnTouchLeave(pointerId); diff --git a/Source/Engine/UI/GUI/Common/TextBoxBase.cs b/Source/Engine/UI/GUI/Common/TextBoxBase.cs index c08b59f48..55b58634d 100644 --- a/Source/Engine/UI/GUI/Common/TextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/TextBoxBase.cs @@ -1399,6 +1399,12 @@ namespace FlaxEngine.GUI } case KeyboardKeys.Escape: { + if (IsReadOnly) + { + SetSelection(_selectionEnd); + return true; + } + RestoreTextFromStart(); if (!IsNavFocused) diff --git a/Source/Engine/UI/UICanvas.cs b/Source/Engine/UI/UICanvas.cs index edbec7f7d..bc8c360d1 100644 --- a/Source/Engine/UI/UICanvas.cs +++ b/Source/Engine/UI/UICanvas.cs @@ -66,6 +66,8 @@ namespace FlaxEngine /// public override void Render(GPUContext context, ref RenderContext renderContext, GPUTexture input, GPUTexture output) { + if (!Canvas.IsVisible(renderContext.View.RenderLayersMask)) + return; var bounds = Canvas.Bounds; bounds.Transformation.Translation -= renderContext.View.Origin; if (renderContext.View.Frustum.Contains(bounds.GetBoundingBox()) == ContainmentType.Disjoint) @@ -873,6 +875,20 @@ namespace FlaxEngine } } + internal bool IsVisible() + { + return IsVisible(MainRenderTask.Instance?.ViewLayersMask ?? LayersMask.Default); + } + + internal bool IsVisible(LayersMask layersMask) + { +#if FLAX_EDITOR + if (_editorTask != null || _editorRoot != null) + return true; +#endif + return layersMask.HasLayer(Layer); + } + #if FLAX_EDITOR private SceneRenderTask _editorTask; private ContainerControl _editorRoot; diff --git a/Source/Engine/Visject/ShaderGraphValue.cpp b/Source/Engine/Visject/ShaderGraphValue.cpp index 56d7c9d06..6564cde2b 100644 --- a/Source/Engine/Visject/ShaderGraphValue.cpp +++ b/Source/Engine/Visject/ShaderGraphValue.cpp @@ -40,11 +40,15 @@ ShaderGraphValue::ShaderGraphValue(const Variant& v) break; case VariantType::Float: Type = VariantType::Types::Float; - Value = String::Format(TEXT("{:.8f}"), v.AsFloat); + Value = String::Format(TEXT("{}"), v.AsFloat); + if (Value.Find('.') == -1) + Value = String::Format(TEXT("{:.1f}"), v.AsFloat); break; case VariantType::Double: Type = VariantType::Types::Float; - Value = String::Format(TEXT("{:.8f}"), (float)v.AsDouble); + Value = String::Format(TEXT("{}"), (float)v.AsDouble); + if (Value.Find('.') == -1) + Value = String::Format(TEXT("{:.1f}"), (float)v.AsDouble); break; case VariantType::Float2: { @@ -134,6 +138,29 @@ bool ShaderGraphValue::IsOne() const } } +bool ShaderGraphValue::IsLiteral() const +{ + switch (Type) + { + case VariantType::Types::Bool: + case VariantType::Types::Int: + case VariantType::Types::Uint: + case VariantType::Types::Float: + if (Value.HasChars()) + { + for (int32 i = 0; i < Value.Length(); i++) + { + const Char c = Value[i]; + if (!StringUtils::IsDigit(c) && c != '.') + return false; + } + return true; + } + default: + return false; + } +} + ShaderGraphValue ShaderGraphValue::InitForZero(VariantType::Types type) { const Char* v; diff --git a/Source/Engine/Visject/ShaderGraphValue.h b/Source/Engine/Visject/ShaderGraphValue.h index 7c7e25154..439b59077 100644 --- a/Source/Engine/Visject/ShaderGraphValue.h +++ b/Source/Engine/Visject/ShaderGraphValue.h @@ -143,8 +143,7 @@ public: /// /// Returns true if value is valid. /// - /// True if is valid, otherwise false. - bool IsValid() const + FORCE_INLINE bool IsValid() const { return Type != VariantType::Types::Null; } @@ -152,8 +151,7 @@ public: /// /// Returns true if value is invalid. /// - /// True if is invalid, otherwise false. - bool IsInvalid() const + FORCE_INLINE bool IsInvalid() const { return Type == VariantType::Types::Null; } @@ -161,15 +159,18 @@ public: /// /// Checks if value contains static part with zero. /// - /// True if contains zero number. bool IsZero() const; /// /// Checks if value contains static part with one. /// - /// True if contains one number. bool IsOne() const; + /// + /// Checks if value is a compile-time constant literal (eg. int, bool or float). + /// + bool IsLiteral() const; + /// /// Clears this instance. /// diff --git a/Source/Engine/Visject/VisjectGraph.cpp b/Source/Engine/Visject/VisjectGraph.cpp index 1cee5d46e..4e94473f6 100644 --- a/Source/Engine/Visject/VisjectGraph.cpp +++ b/Source/Engine/Visject/VisjectGraph.cpp @@ -685,7 +685,7 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value) case 36: { // Get value with structure data - const Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection()); + Variant structureValue = eatBox(node, node->GetBox(0)->FirstConnection()); if (!node->GetBox(0)->HasConnection()) return; @@ -741,7 +741,9 @@ void VisjectExecutor::ProcessGroupPacking(Box* box, Node* node, Value& value) return; } const ScriptingType& type = typeHandle.GetType(); - if (structureValue.Type.Type != VariantType::Structure || StringUtils::Compare(typeNameAnsi.Get(), structureValue.Type.TypeName) != 0) + structureValue.InvertInline(); // Extract any Float3/Int32 into Structure type from inlined format + const ScriptingTypeHandle structureValueTypeHandle = Scripting::FindScriptingType(structureValue.Type.GetTypeName()); + if (structureValue.Type.Type != VariantType::Structure || typeHandle != structureValueTypeHandle) { OnError(node, box, String::Format(TEXT("Cannot unpack value of type {0} to structure of type {1}"), structureValue.Type, typeName)); return; diff --git a/Source/Engine/Visject/VisjectGraph.h b/Source/Engine/Visject/VisjectGraph.h index 5836afb2f..085c05de0 100644 --- a/Source/Engine/Visject/VisjectGraph.h +++ b/Source/Engine/Visject/VisjectGraph.h @@ -262,11 +262,11 @@ protected: FORCE_INLINE Value tryGetValue(Box* box) { - return box && box->HasConnection() ? eatBox(box->GetParent(), box->FirstConnection()) : Value::Zero; + return box && box->Connections.HasItems() ? eatBox(box->GetParent(), (VisjectGraphBox*)box->Connections.Get()[0]) : Value::Zero; } FORCE_INLINE Value tryGetValue(Box* box, const Value& defaultValue) { - return box && box->HasConnection() ? eatBox(box->GetParent(), box->FirstConnection()) : defaultValue; + return box && box->Connections.HasItems() ? eatBox(box->GetParent(), (VisjectGraphBox*)box->Connections.Get()[0]) : defaultValue; } }; diff --git a/Source/Platforms/Mac/Default.icns b/Source/Platforms/Mac/Default.icns index 455acd991..911276d8e 100644 Binary files a/Source/Platforms/Mac/Default.icns and b/Source/Platforms/Mac/Default.icns differ diff --git a/Source/Platforms/iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj b/Source/Platforms/iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj index 1dd5d4e01..3b8889487 100644 --- a/Source/Platforms/iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj +++ b/Source/Platforms/iOS/Binaries/Project/FlaxGame.xcodeproj/project.pbxproj @@ -222,7 +222,7 @@ ${PBXResourcesGroup} GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - HEADER_SEARCH_PATHS = ${HeaderSearchPaths}; + HEADER_SEARCH_PATHS = "${HeaderSearchPaths}"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -275,7 +275,7 @@ ${PBXResourcesGroup} GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - HEADER_SEARCH_PATHS = ${HeaderSearchPaths}; + HEADER_SEARCH_PATHS = "${HeaderSearchPaths}"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; diff --git a/Source/Shaders/GlobalSignDistanceField.shader b/Source/Shaders/GlobalSignDistanceField.shader index 3d4f7dd46..bc4f272fc 100644 --- a/Source/Shaders/GlobalSignDistanceField.shader +++ b/Source/Shaders/GlobalSignDistanceField.shader @@ -211,7 +211,7 @@ float SampleSDF(uint3 voxelCoordMip, int3 offset) float result = GlobalSDFTex[voxelCoordMip].r; // Extend by distance to the sampled texel location - float distanceInWorldUnits = length(offset) * (MaxDistance / (float)GenerateMipTexResolution); + float distanceInWorldUnits = length((float3)offset) * (MaxDistance / (float)GenerateMipTexResolution); float distanceToVoxel = distanceInWorldUnits / MaxDistance; result = CombineDistanceToSDF(result, distanceToVoxel); diff --git a/Source/Shaders/Math.hlsl b/Source/Shaders/Math.hlsl index ad3168016..67ddc1887 100644 --- a/Source/Shaders/Math.hlsl +++ b/Source/Shaders/Math.hlsl @@ -218,10 +218,10 @@ float4 ClampedPow(float4 x, float4 y) float4 FindQuatBetween(float3 from, float3 to) { + // http://lolengine.net/blog/2014/02/24/quaternion-from-two-vectors-final float normAB = 1.0f; float w = normAB + dot(from, to); float4 result; - if (w >= 1e-6f * normAB) { result = float4 @@ -234,12 +234,10 @@ float4 FindQuatBetween(float3 from, float3 to) } else { - w = 0.f; result = abs(from.x) > abs(from.y) - ? float4(-from.z, 0.f, from.x, w) - : float4(0.f, -from.z, from.y, w); + ? float4(-from.z, 0.f, from.x, 0.0f) + : float4(0.f, -from.z, from.y, 0.0f); } - return normalize(result); } diff --git a/Source/ThirdParty/stb/stb_image_write.h b/Source/ThirdParty/stb/stb_image_write.h index 95943eb60..8cae247eb 100644 --- a/Source/ThirdParty/stb/stb_image_write.h +++ b/Source/ThirdParty/stb/stb_image_write.h @@ -758,6 +758,8 @@ static int stbi_write_hdr_core(stbi__write_context *s, int x, int y, int comp, f #ifdef __STDC_WANT_SECURE_LIB__ len = sprintf_s(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#elif __APPLE__ + len = snprintf(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); #else len = sprintf(buffer, "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); #endif diff --git a/Source/ThirdParty/tracy/TracyClient.cpp b/Source/ThirdParty/tracy/TracyClient.cpp index 3548c5752..1fbd78f96 100644 --- a/Source/ThirdParty/tracy/TracyClient.cpp +++ b/Source/ThirdParty/tracy/TracyClient.cpp @@ -11,10 +11,10 @@ // Define TRACY_ENABLE to enable profiler. -#ifdef TRACY_ENABLE - #include "common/TracySystem.cpp" +#ifdef TRACY_ENABLE + #ifdef _MSC_VER # pragma warning(push, 0) #endif @@ -22,12 +22,13 @@ #include #include "client/TracyProfiler.cpp" #include "client/TracyCallstack.cpp" +#include "client/TracySysPower.cpp" #include "client/TracySysTime.cpp" #include "client/TracySysTrace.cpp" #include "common/TracySocket.cpp" #include "client/tracy_rpmalloc.cpp" #include "client/TracyAlloc.cpp" - +#include "client/TracyOverride.cpp" #if TRACY_HAS_CALLSTACK == 2 || TRACY_HAS_CALLSTACK == 3 || TRACY_HAS_CALLSTACK == 4 || TRACY_HAS_CALLSTACK == 6 # include "libbacktrace/alloc.cpp" diff --git a/Source/ThirdParty/tracy/client/TracyCallstack.cpp b/Source/ThirdParty/tracy/client/TracyCallstack.cpp index ca19a543b..0de7c9d2e 100644 --- a/Source/ThirdParty/tracy/client/TracyCallstack.cpp +++ b/Source/ThirdParty/tracy/client/TracyCallstack.cpp @@ -154,7 +154,6 @@ void InitCallstack() DBGHELP_LOCK; #endif - //SymInitialize( GetCurrentProcess(), "C:\\Flax\\FlaxEngine\\Binaries\\Editor\\Win64\\Debug;C:\\Flax\\FlaxEngine\\Cache\\Projects", true ); SymInitialize( GetCurrentProcess(), nullptr, true ); SymSetOptions( SYMOPT_LOAD_LINES ); @@ -228,6 +227,10 @@ void InitCallstack() const auto res = GetModuleFileNameA( mod[i], name, 1021 ); if( res > 0 ) { + // This may be a new module loaded since our call to SymInitialize. + // Just in case, force DbgHelp to load its pdb ! + SymLoadModuleEx(proc, NULL, name, NULL, (DWORD64)info.lpBaseOfDll, info.SizeOfImage, NULL, 0); + auto ptr = name + res; while( ptr > name && *ptr != '\\' && *ptr != '/' ) ptr--; if( ptr > name ) ptr++; @@ -683,7 +686,9 @@ void InitCallstackCritical() void InitCallstack() { cb_bts = backtrace_create_state( nullptr, 0, nullptr, nullptr ); +#ifndef TRACY_DEMANGLE ___tracy_init_demangle_buffer(); +#endif #ifdef __linux InitKernelSymbols(); @@ -758,7 +763,9 @@ debuginfod_client* GetDebuginfodClient() void EndCallstack() { +#ifndef TRACY_DEMANGLE ___tracy_free_demangle_buffer(); +#endif #ifdef TRACY_DEBUGINFOD ClearDebugInfoVector( s_di_known ); debuginfod_end( s_debuginfod ); diff --git a/Source/ThirdParty/tracy/client/TracyCallstack.hpp b/Source/ThirdParty/tracy/client/TracyCallstack.hpp index 8cfede8fb..96bee3f51 100644 --- a/Source/ThirdParty/tracy/client/TracyCallstack.hpp +++ b/Source/ThirdParty/tracy/client/TracyCallstack.hpp @@ -10,7 +10,14 @@ #endif -#ifdef TRACY_HAS_CALLSTACK +#ifndef TRACY_HAS_CALLSTACK + +namespace tracy +{ +static tracy_force_inline void* Callstack( int depth ) { return nullptr; } +} + +#else #ifdef TRACY_DEBUGINFOD # include diff --git a/Source/ThirdParty/tracy/client/TracyLock.hpp b/Source/ThirdParty/tracy/client/TracyLock.hpp index 296a41ba1..d12a3c16d 100644 --- a/Source/ThirdParty/tracy/client/TracyLock.hpp +++ b/Source/ThirdParty/tracy/client/TracyLock.hpp @@ -21,7 +21,7 @@ public: , m_active( false ) #endif { - assert( m_id != std::numeric_limits::max() ); + assert( m_id != (std::numeric_limits::max)() ); auto item = Profiler::QueueSerial(); MemWrite( &item->hdr.type, QueueType::LockAnnounce ); @@ -154,7 +154,7 @@ public: tracy_force_inline void CustomName( const char* name, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); auto ptr = (char*)tracy_malloc( size ); memcpy( ptr, name, size ); auto item = Profiler::QueueSerial(); @@ -235,7 +235,7 @@ public: , m_active( false ) #endif { - assert( m_id != std::numeric_limits::max() ); + assert( m_id != (std::numeric_limits::max)() ); auto item = Profiler::QueueSerial(); MemWrite( &item->hdr.type, QueueType::LockAnnounce ); @@ -450,7 +450,7 @@ public: tracy_force_inline void CustomName( const char* name, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); auto ptr = (char*)tracy_malloc( size ); memcpy( ptr, name, size ); auto item = Profiler::QueueSerial(); diff --git a/Source/ThirdParty/tracy/client/TracyOverride.cpp b/Source/ThirdParty/tracy/client/TracyOverride.cpp new file mode 100644 index 000000000..591508a7f --- /dev/null +++ b/Source/ThirdParty/tracy/client/TracyOverride.cpp @@ -0,0 +1,26 @@ +#ifdef TRACY_ENABLE +# ifdef __linux__ +# include "TracyDebug.hpp" +# ifdef TRACY_VERBOSE +# include +# include +# endif + +extern "C" int dlclose( void* hnd ) +{ +#ifdef TRACY_VERBOSE + struct link_map* lm; + if( dlinfo( hnd, RTLD_DI_LINKMAP, &lm ) == 0 ) + { + TracyDebug( "Overriding dlclose for %s\n", lm->l_name ); + } + else + { + TracyDebug( "Overriding dlclose for unknown object (%s)\n", dlerror() ); + } +#endif + return 0; +} + +# endif +#endif diff --git a/Source/ThirdParty/tracy/client/TracyProfiler.cpp b/Source/ThirdParty/tracy/client/TracyProfiler.cpp index dfbb22a83..59c31d6f8 100644 --- a/Source/ThirdParty/tracy/client/TracyProfiler.cpp +++ b/Source/ThirdParty/tracy/client/TracyProfiler.cpp @@ -81,7 +81,9 @@ #endif #ifdef __APPLE__ -# define TRACY_DELAYED_INIT +# ifndef TRACY_DELAYED_INIT +# define TRACY_DELAYED_INIT +# endif #else # ifdef __GNUC__ # define init_order( val ) __attribute__ ((init_priority(val))) @@ -1074,7 +1076,9 @@ static void CrashHandler( int signal, siginfo_t* info, void* /*ucontext*/ ) } closedir( dp ); +#ifdef TRACY_HAS_CALLSTACK if( selfTid == s_symbolTid ) s_symbolThreadGone.store( true, std::memory_order_release ); +#endif TracyLfqPrepare( QueueType::Crash ); TracyLfqCommit; @@ -1355,6 +1359,7 @@ Profiler::Profiler() , m_queryImage( nullptr ) , m_queryData( nullptr ) , m_crashHandlerInstalled( false ) + , m_programName( nullptr ) { assert( !s_instance ); s_instance = this; @@ -1417,7 +1422,9 @@ void Profiler::SpawnWorkerThreads() #if defined _WIN32 && !defined TRACY_UWP && !defined TRACY_NO_CRASH_HANDLER s_profilerThreadId = GetThreadId( s_thread->Handle() ); +# ifdef TRACY_HAS_CALLSTACK s_symbolThreadId = GetThreadId( s_symbolThread->Handle() ); +# endif m_exceptionHandler = AddVectoredExceptionHandler( 1, CrashFilter ); #endif @@ -1456,7 +1463,7 @@ Profiler::~Profiler() if( m_crashHandlerInstalled ) RemoveVectoredExceptionHandler( m_exceptionHandler ); #endif -#ifdef __linux__ +#if defined __linux__ && !defined TRACY_NO_CRASH_HANDLER if( m_crashHandlerInstalled ) { sigaction( TRACY_CRASH_SIGNAL, &m_prevSignal.pwr, nullptr ); @@ -1522,7 +1529,7 @@ bool Profiler::ShouldExit() void Profiler::Worker() { -#ifdef __linux__ +#if defined __linux__ && !defined TRACY_NO_CRASH_HANDLER s_profilerTid = syscall( SYS_gettid ); #endif @@ -1711,6 +1718,9 @@ void Profiler::Worker() if( m_sock ) break; #ifndef TRACY_ON_DEMAND ProcessSysTime(); +# ifdef TRACY_HAS_SYSPOWER + m_sysPower.Tick(); +# endif #endif if( m_broadcast ) @@ -1718,6 +1728,14 @@ void Profiler::Worker() const auto t = std::chrono::high_resolution_clock::now().time_since_epoch().count(); if( t - lastBroadcast > 3000000000 ) // 3s { + m_programNameLock.lock(); + if( m_programName ) + { + broadcastMsg = GetBroadcastMessage( m_programName, strlen( m_programName ), broadcastLen, dataPort ); + m_programName = nullptr; + } + m_programNameLock.unlock(); + lastBroadcast = t; const auto ts = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ).count(); broadcastMsg.activeTime = int32_t( ts - m_epoch ); @@ -1828,6 +1846,9 @@ void Profiler::Worker() for(;;) { ProcessSysTime(); +#ifdef TRACY_HAS_SYSPOWER + m_sysPower.Tick(); +#endif const auto status = Dequeue( token ); const auto serialStatus = DequeueSerial(); if( status == DequeueStatus::ConnectionLost || serialStatus == DequeueStatus::ConnectionLost ) @@ -3026,9 +3047,9 @@ void Profiler::SendSourceLocation( uint64_t ptr ) MemWrite( &item.srcloc.file, (uint64_t)srcloc->file ); MemWrite( &item.srcloc.function, (uint64_t)srcloc->function ); MemWrite( &item.srcloc.line, srcloc->line ); - MemWrite( &item.srcloc.r, uint8_t( ( srcloc->color ) & 0xFF ) ); + MemWrite( &item.srcloc.b, uint8_t( ( srcloc->color ) & 0xFF ) ); MemWrite( &item.srcloc.g, uint8_t( ( srcloc->color >> 8 ) & 0xFF ) ); - MemWrite( &item.srcloc.b, uint8_t( ( srcloc->color >> 16 ) & 0xFF ) ); + MemWrite( &item.srcloc.r, uint8_t( ( srcloc->color >> 16 ) & 0xFF ) ); AppendData( &item, QueueDataSize[(int)QueueType::SourceLocation] ); } @@ -3331,10 +3352,8 @@ bool Profiler::HandleServerQuery() uint8_t type; uint64_t ptr; - uint32_t extra; memcpy( &type, &payload.type, sizeof( payload.type ) ); memcpy( &ptr, &payload.ptr, sizeof( payload.ptr ) ); - memcpy( &extra, &payload.extra, sizeof( payload.extra ) ); switch( type ) { @@ -3381,7 +3400,7 @@ bool Profiler::HandleServerQuery() break; #ifndef TRACY_NO_CODE_TRANSFER case ServerQuerySymbolCode: - HandleSymbolCodeQuery( ptr, extra ); + HandleSymbolCodeQuery( ptr, payload.extra ); break; #endif case ServerQuerySourceCode: @@ -3398,7 +3417,7 @@ bool Profiler::HandleServerQuery() break; case ServerQueryDataTransferPart: memcpy( m_queryDataPtr, &ptr, 8 ); - memcpy( m_queryDataPtr+8, &extra, 4 ); + memcpy( m_queryDataPtr+8, &payload.extra, 4 ); m_queryDataPtr += 12; AckServerQuery(); break; @@ -3753,7 +3772,7 @@ void Profiler::SendFrameImage( const void* image, uint16_t w, uint16_t h, uint8_ { #ifndef TRACY_NO_FRAME_IMAGE auto& profiler = GetProfiler(); - assert( profiler.m_frameCount.load( std::memory_order_relaxed ) < std::numeric_limits::max() ); + assert( profiler.m_frameCount.load( std::memory_order_relaxed ) < (std::numeric_limits::max)() ); # ifdef TRACY_ON_DEMAND if( !profiler.IsConnected() ) return; # endif @@ -3770,6 +3789,12 @@ void Profiler::SendFrameImage( const void* image, uint16_t w, uint16_t h, uint8_ fi->flip = flip; profiler.m_fiQueue.commit_next(); profiler.m_fiLock.unlock(); +#else + static_cast(image); // unused + static_cast(w); // unused + static_cast(h); // unused + static_cast(offset); // unused + static_cast(flip); // unused #endif } @@ -3827,7 +3852,7 @@ void Profiler::ConfigurePlot( const char* name, PlotFormatType type, bool step, void Profiler::Message( const char* txt, size_t size, int callstack ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); #ifdef TRACY_ON_DEMAND if( !GetProfiler().IsConnected() ) return; #endif @@ -3864,7 +3889,7 @@ void Profiler::Message( const char* txt, int callstack ) void Profiler::MessageColor( const char* txt, size_t size, uint32_t color, int callstack ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); #ifdef TRACY_ON_DEMAND if( !GetProfiler().IsConnected() ) return; #endif @@ -3879,9 +3904,9 @@ void Profiler::MessageColor( const char* txt, size_t size, uint32_t color, int c TracyQueuePrepare( callstack == 0 ? QueueType::MessageColor : QueueType::MessageColorCallstack ); MemWrite( &item->messageColorFat.time, GetTime() ); MemWrite( &item->messageColorFat.text, (uint64_t)ptr ); - MemWrite( &item->messageColorFat.r, uint8_t( ( color ) & 0xFF ) ); + MemWrite( &item->messageColorFat.b, uint8_t( ( color ) & 0xFF ) ); MemWrite( &item->messageColorFat.g, uint8_t( ( color >> 8 ) & 0xFF ) ); - MemWrite( &item->messageColorFat.b, uint8_t( ( color >> 16 ) & 0xFF ) ); + MemWrite( &item->messageColorFat.r, uint8_t( ( color >> 16 ) & 0xFF ) ); MemWrite( &item->messageColorFat.size, (uint16_t)size ); TracyQueueCommit( messageColorFatThread ); } @@ -3899,15 +3924,15 @@ void Profiler::MessageColor( const char* txt, uint32_t color, int callstack ) TracyQueuePrepare( callstack == 0 ? QueueType::MessageLiteralColor : QueueType::MessageLiteralColorCallstack ); MemWrite( &item->messageColorLiteral.time, GetTime() ); MemWrite( &item->messageColorLiteral.text, (uint64_t)txt ); - MemWrite( &item->messageColorLiteral.r, uint8_t( ( color ) & 0xFF ) ); + MemWrite( &item->messageColorLiteral.b, uint8_t( ( color ) & 0xFF ) ); MemWrite( &item->messageColorLiteral.g, uint8_t( ( color >> 8 ) & 0xFF ) ); - MemWrite( &item->messageColorLiteral.b, uint8_t( ( color >> 16 ) & 0xFF ) ); + MemWrite( &item->messageColorLiteral.r, uint8_t( ( color >> 16 ) & 0xFF ) ); TracyQueueCommit( messageColorLiteralThread ); } void Profiler::MessageAppInfo( const char* txt, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); auto ptr = (char*)tracy_malloc( size ); memcpy( ptr, txt, size ); TracyLfqPrepare( QueueType::MessageAppInfo ); @@ -3922,7 +3947,7 @@ void Profiler::MessageAppInfo( const char* txt, size_t size ) TracyLfqCommit; } -void Profiler::MemAlloc(const void* ptr, size_t size, bool secure) +void Profiler::MemAlloc( const void* ptr, size_t size, bool secure ) { if( secure && !ProfilerAvailable() ) return; #ifdef TRACY_ON_DEMAND @@ -4085,7 +4110,6 @@ void Profiler::SendCallstack( int depth ) #endif } -void Profiler::ParameterRegister( ParameterCallback cb ) { GetProfiler().m_paramCallback = cb; } void Profiler::ParameterRegister( ParameterCallback cb, void* data ) { auto& profiler = GetProfiler(); @@ -4301,486 +4325,4 @@ int64_t Profiler::GetTimeQpc() } -#if 0 -#ifdef __cplusplus -extern "C" { -#endif - -TRACY_API TracyCZoneCtx ___tracy_emit_zone_begin( const struct ___tracy_source_location_data* srcloc, int active ) -{ - ___tracy_c_zone_context ctx; -#ifdef TRACY_ON_DEMAND - ctx.active = active && tracy::GetProfiler().IsConnected(); -#else - ctx.active = active; -#endif - if( !ctx.active ) return ctx; - const auto id = tracy::GetProfiler().GetNextZoneId(); - ctx.id = id; - -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneBegin ); - tracy::MemWrite( &item->zoneBegin.time, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->zoneBegin.srcloc, (uint64_t)srcloc ); - TracyQueueCommitC( zoneBeginThread ); - } - return ctx; -} - -TRACY_API TracyCZoneCtx ___tracy_emit_zone_begin_callstack( const struct ___tracy_source_location_data* srcloc, int depth, int active ) -{ - ___tracy_c_zone_context ctx; -#ifdef TRACY_ON_DEMAND - ctx.active = active && tracy::GetProfiler().IsConnected(); -#else - ctx.active = active; -#endif - if( !ctx.active ) return ctx; - const auto id = tracy::GetProfiler().GetNextZoneId(); - ctx.id = id; - -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - tracy::GetProfiler().SendCallstack( depth ); - { - TracyQueuePrepareC( tracy::QueueType::ZoneBeginCallstack ); - tracy::MemWrite( &item->zoneBegin.time, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->zoneBegin.srcloc, (uint64_t)srcloc ); - TracyQueueCommitC( zoneBeginThread ); - } - return ctx; -} - -TRACY_API TracyCZoneCtx ___tracy_emit_zone_begin_alloc( uint64_t srcloc, int active ) -{ - ___tracy_c_zone_context ctx; -#ifdef TRACY_ON_DEMAND - ctx.active = active && tracy::GetProfiler().IsConnected(); -#else - ctx.active = active; -#endif - if( !ctx.active ) - { - tracy::tracy_free( (void*)srcloc ); - return ctx; - } - const auto id = tracy::GetProfiler().GetNextZoneId(); - ctx.id = id; - -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneBeginAllocSrcLoc ); - tracy::MemWrite( &item->zoneBegin.time, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->zoneBegin.srcloc, srcloc ); - TracyQueueCommitC( zoneBeginThread ); - } - return ctx; -} - -TRACY_API TracyCZoneCtx ___tracy_emit_zone_begin_alloc_callstack( uint64_t srcloc, int depth, int active ) -{ - ___tracy_c_zone_context ctx; -#ifdef TRACY_ON_DEMAND - ctx.active = active && tracy::GetProfiler().IsConnected(); -#else - ctx.active = active; -#endif - if( !ctx.active ) - { - tracy::tracy_free( (void*)srcloc ); - return ctx; - } - const auto id = tracy::GetProfiler().GetNextZoneId(); - ctx.id = id; - -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - tracy::GetProfiler().SendCallstack( depth ); - { - TracyQueuePrepareC( tracy::QueueType::ZoneBeginAllocSrcLocCallstack ); - tracy::MemWrite( &item->zoneBegin.time, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->zoneBegin.srcloc, srcloc ); - TracyQueueCommitC( zoneBeginThread ); - } - return ctx; -} - -TRACY_API void ___tracy_emit_zone_end( TracyCZoneCtx ctx ) -{ - if( !ctx.active ) return; -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, ctx.id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneEnd ); - tracy::MemWrite( &item->zoneEnd.time, tracy::Profiler::GetTime() ); - TracyQueueCommitC( zoneEndThread ); - } -} - -TRACY_API void ___tracy_emit_zone_text( TracyCZoneCtx ctx, const char* txt, size_t size ) -{ - assert( size < std::numeric_limits::max() ); - if( !ctx.active ) return; - auto ptr = (char*)tracy::tracy_malloc( size ); - memcpy( ptr, txt, size ); -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, ctx.id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneText ); - tracy::MemWrite( &item->zoneTextFat.text, (uint64_t)ptr ); - tracy::MemWrite( &item->zoneTextFat.size, (uint16_t)size ); - TracyQueueCommitC( zoneTextFatThread ); - } -} - -TRACY_API void ___tracy_emit_zone_name( TracyCZoneCtx ctx, const char* txt, size_t size ) -{ - assert( size < std::numeric_limits::max() ); - if( !ctx.active ) return; - auto ptr = (char*)tracy::tracy_malloc( size ); - memcpy( ptr, txt, size ); -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, ctx.id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneName ); - tracy::MemWrite( &item->zoneTextFat.text, (uint64_t)ptr ); - tracy::MemWrite( &item->zoneTextFat.size, (uint16_t)size ); - TracyQueueCommitC( zoneTextFatThread ); - } -} - -TRACY_API void ___tracy_emit_zone_color( TracyCZoneCtx ctx, uint32_t color ) { - if( !ctx.active ) return; -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, ctx.id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneColor ); - tracy::MemWrite( &item->zoneColor.r, uint8_t( ( color ) & 0xFF ) ); - tracy::MemWrite( &item->zoneColor.g, uint8_t( ( color >> 8 ) & 0xFF ) ); - tracy::MemWrite( &item->zoneColor.b, uint8_t( ( color >> 16 ) & 0xFF ) ); - TracyQueueCommitC( zoneColorThread ); - } -} - -TRACY_API void ___tracy_emit_zone_value( TracyCZoneCtx ctx, uint64_t value ) -{ - if( !ctx.active ) return; -#ifndef TRACY_NO_VERIFY - { - TracyQueuePrepareC( tracy::QueueType::ZoneValidation ); - tracy::MemWrite( &item->zoneValidation.id, ctx.id ); - TracyQueueCommitC( zoneValidationThread ); - } -#endif - { - TracyQueuePrepareC( tracy::QueueType::ZoneValue ); - tracy::MemWrite( &item->zoneValue.value, value ); - TracyQueueCommitC( zoneValueThread ); - } -} - -TRACY_API void ___tracy_emit_memory_alloc( const void* ptr, size_t size, int secure ) { tracy::Profiler::MemAlloc( ptr, size, secure != 0 ); } -TRACY_API void ___tracy_emit_memory_alloc_callstack( const void* ptr, size_t size, int depth, int secure ) { tracy::Profiler::MemAllocCallstack( ptr, size, depth, secure != 0 ); } -TRACY_API void ___tracy_emit_memory_free( const void* ptr, int secure ) { tracy::Profiler::MemFree( ptr, secure != 0 ); } -TRACY_API void ___tracy_emit_memory_free_callstack( const void* ptr, int depth, int secure ) { tracy::Profiler::MemFreeCallstack( ptr, depth, secure != 0 ); } -TRACY_API void ___tracy_emit_memory_alloc_named( const void* ptr, size_t size, int secure, const char* name ) { tracy::Profiler::MemAllocNamed( ptr, size, secure != 0, name ); } -TRACY_API void ___tracy_emit_memory_alloc_callstack_named( const void* ptr, size_t size, int depth, int secure, const char* name ) { tracy::Profiler::MemAllocCallstackNamed( ptr, size, depth, secure != 0, name ); } -TRACY_API void ___tracy_emit_memory_free_named( const void* ptr, int secure, const char* name ) { tracy::Profiler::MemFreeNamed( ptr, secure != 0, name ); } -TRACY_API void ___tracy_emit_memory_free_callstack_named( const void* ptr, int depth, int secure, const char* name ) { tracy::Profiler::MemFreeCallstackNamed( ptr, depth, secure != 0, name ); } -TRACY_API void ___tracy_emit_frame_mark( const char* name ) { tracy::Profiler::SendFrameMark( name ); } -TRACY_API void ___tracy_emit_frame_mark_start( const char* name ) { tracy::Profiler::SendFrameMark( name, tracy::QueueType::FrameMarkMsgStart ); } -TRACY_API void ___tracy_emit_frame_mark_end( const char* name ) { tracy::Profiler::SendFrameMark( name, tracy::QueueType::FrameMarkMsgEnd ); } -TRACY_API void ___tracy_emit_frame_image( const void* image, uint16_t w, uint16_t h, uint8_t offset, int flip ) { tracy::Profiler::SendFrameImage( image, w, h, offset, flip ); } -TRACY_API void ___tracy_emit_plot( const char* name, double val ) { tracy::Profiler::PlotData( name, val ); } -TRACY_API void ___tracy_emit_message( const char* txt, size_t size, int callstack ) { tracy::Profiler::Message( txt, size, callstack ); } -TRACY_API void ___tracy_emit_messageL( const char* txt, int callstack ) { tracy::Profiler::Message( txt, callstack ); } -TRACY_API void ___tracy_emit_messageC( const char* txt, size_t size, uint32_t color, int callstack ) { tracy::Profiler::MessageColor( txt, size, color, callstack ); } -TRACY_API void ___tracy_emit_messageLC( const char* txt, uint32_t color, int callstack ) { tracy::Profiler::MessageColor( txt, color, callstack ); } -TRACY_API void ___tracy_emit_message_appinfo( const char* txt, size_t size ) { tracy::Profiler::MessageAppInfo( txt, size ); } - -TRACY_API uint64_t ___tracy_alloc_srcloc( uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz ) { - return tracy::Profiler::AllocSourceLocation( line, source, sourceSz, function, functionSz ); -} - -TRACY_API uint64_t ___tracy_alloc_srcloc_name( uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz, const char* name, size_t nameSz ) { - return tracy::Profiler::AllocSourceLocation( line, source, sourceSz, function, functionSz, name, nameSz ); -} - -TRACY_API void ___tracy_emit_gpu_zone_begin( const struct ___tracy_gpu_zone_begin_data data ) -{ - TracyLfqPrepareC( tracy::QueueType::GpuZoneBegin ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_callstack( const struct ___tracy_gpu_zone_begin_callstack_data data ) -{ - tracy::GetProfiler().SendCallstack( data.depth ); - TracyLfqPrepareC( tracy::QueueType::GpuZoneBeginCallstack ); - tracy::MemWrite( &item->gpuZoneBegin.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_alloc( const struct ___tracy_gpu_zone_begin_data data ) -{ - TracyLfqPrepareC( tracy::QueueType::GpuZoneBeginAllocSrcLoc ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_alloc_callstack( const struct ___tracy_gpu_zone_begin_callstack_data data ) -{ - tracy::GetProfiler().SendCallstack( data.depth ); - TracyLfqPrepareC( tracy::QueueType::GpuZoneBeginAllocSrcLocCallstack ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_time( const struct ___tracy_gpu_time_data data ) -{ - TracyLfqPrepareC( tracy::QueueType::GpuTime ); - tracy::MemWrite( &item->gpuTime.gpuTime, data.gpuTime ); - tracy::MemWrite( &item->gpuTime.queryId, data.queryId ); - tracy::MemWrite( &item->gpuTime.context, data.context ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_zone_end( const struct ___tracy_gpu_zone_end_data data ) -{ - TracyLfqPrepareC( tracy::QueueType::GpuZoneEnd ); - tracy::MemWrite( &item->gpuZoneEnd.cpuTime, tracy::Profiler::GetTime() ); - memset( &item->gpuZoneEnd.thread, 0, sizeof( item->gpuZoneEnd.thread ) ); - tracy::MemWrite( &item->gpuZoneEnd.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneEnd.context, data.context ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_new_context( ___tracy_gpu_new_context_data data ) -{ - TracyLfqPrepareC( tracy::QueueType::GpuNewContext ); - tracy::MemWrite( &item->gpuNewContext.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuNewContext.gpuTime, data.gpuTime ); - tracy::MemWrite( &item->gpuNewContext.period, data.period ); - tracy::MemWrite( &item->gpuNewContext.context, data.context ); - tracy::MemWrite( &item->gpuNewContext.flags, data.flags ); - tracy::MemWrite( &item->gpuNewContext.type, data.type ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_context_name( const struct ___tracy_gpu_context_name_data data ) -{ - auto ptr = (char*)tracy::tracy_malloc( data.len ); - memcpy( ptr, data.name, data.len ); - - TracyLfqPrepareC( tracy::QueueType::GpuContextName ); - tracy::MemWrite( &item->gpuContextNameFat.context, data.context ); - tracy::MemWrite( &item->gpuContextNameFat.ptr, (uint64_t)ptr ); - tracy::MemWrite( &item->gpuContextNameFat.size, data.len ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_calibration( const struct ___tracy_gpu_calibration_data data ) -{ - TracyLfqPrepareC( tracy::QueueType::GpuCalibration ); - tracy::MemWrite( &item->gpuCalibration.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuCalibration.gpuTime, data.gpuTime ); - tracy::MemWrite( &item->gpuCalibration.cpuDelta, data.cpuDelta ); - tracy::MemWrite( &item->gpuCalibration.context, data.context ); - TracyLfqCommitC; -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_serial( const struct ___tracy_gpu_zone_begin_data data ) -{ - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuZoneBeginSerial ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_callstack_serial( const struct ___tracy_gpu_zone_begin_callstack_data data ) -{ - auto item = tracy::Profiler::QueueSerialCallstack( tracy::Callstack( data.depth ) ); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuZoneBeginCallstackSerial ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_alloc_serial( const struct ___tracy_gpu_zone_begin_data data ) -{ - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuZoneBeginAllocSrcLocSerial ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_zone_begin_alloc_callstack_serial( const struct ___tracy_gpu_zone_begin_callstack_data data ) -{ - auto item = tracy::Profiler::QueueSerialCallstack( tracy::Callstack( data.depth ) ); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuZoneBeginAllocSrcLocCallstackSerial ); - tracy::MemWrite( &item->gpuZoneBegin.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuZoneBegin.srcloc, data.srcloc ); - tracy::MemWrite( &item->gpuZoneBegin.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneBegin.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_time_serial( const struct ___tracy_gpu_time_data data ) -{ - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuTime ); - tracy::MemWrite( &item->gpuTime.gpuTime, data.gpuTime ); - tracy::MemWrite( &item->gpuTime.queryId, data.queryId ); - tracy::MemWrite( &item->gpuTime.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_zone_end_serial( const struct ___tracy_gpu_zone_end_data data ) -{ - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuZoneEndSerial ); - tracy::MemWrite( &item->gpuZoneEnd.cpuTime, tracy::Profiler::GetTime() ); - memset( &item->gpuZoneEnd.thread, 0, sizeof( item->gpuZoneEnd.thread ) ); - tracy::MemWrite( &item->gpuZoneEnd.queryId, data.queryId ); - tracy::MemWrite( &item->gpuZoneEnd.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_new_context_serial( ___tracy_gpu_new_context_data data ) -{ - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuNewContext ); - tracy::MemWrite( &item->gpuNewContext.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuNewContext.thread, tracy::GetThreadHandle() ); - tracy::MemWrite( &item->gpuNewContext.gpuTime, data.gpuTime ); - tracy::MemWrite( &item->gpuNewContext.period, data.period ); - tracy::MemWrite( &item->gpuNewContext.context, data.context ); - tracy::MemWrite( &item->gpuNewContext.flags, data.flags ); - tracy::MemWrite( &item->gpuNewContext.type, data.type ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_context_name_serial( const struct ___tracy_gpu_context_name_data data ) -{ - auto ptr = (char*)tracy::tracy_malloc( data.len ); - memcpy( ptr, data.name, data.len ); - - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuContextName ); - tracy::MemWrite( &item->gpuContextNameFat.context, data.context ); - tracy::MemWrite( &item->gpuContextNameFat.ptr, (uint64_t)ptr ); - tracy::MemWrite( &item->gpuContextNameFat.size, data.len ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API void ___tracy_emit_gpu_calibration_serial( const struct ___tracy_gpu_calibration_data data ) -{ - auto item = tracy::Profiler::QueueSerial(); - tracy::MemWrite( &item->hdr.type, tracy::QueueType::GpuCalibration ); - tracy::MemWrite( &item->gpuCalibration.cpuTime, tracy::Profiler::GetTime() ); - tracy::MemWrite( &item->gpuCalibration.gpuTime, data.gpuTime ); - tracy::MemWrite( &item->gpuCalibration.cpuDelta, data.cpuDelta ); - tracy::MemWrite( &item->gpuCalibration.context, data.context ); - tracy::Profiler::QueueSerialFinish(); -} - -TRACY_API int ___tracy_connected( void ) -{ - return tracy::GetProfiler().IsConnected(); -} - -#ifdef TRACY_FIBERS -TRACY_API void ___tracy_fiber_enter( const char* fiber ){ tracy::Profiler::EnterFiber( fiber ); } -TRACY_API void ___tracy_fiber_leave( void ){ tracy::Profiler::LeaveFiber(); } -#endif - -# ifdef TRACY_MANUAL_LIFETIME -TRACY_API void ___tracy_startup_profiler( void ) -{ - tracy::StartupProfiler(); -} - -TRACY_API void ___tracy_shutdown_profiler( void ) -{ - tracy::ShutdownProfiler(); -} -# endif - -#ifdef __cplusplus -} -#endif -#endif - #endif diff --git a/Source/ThirdParty/tracy/client/TracyProfiler.hpp b/Source/ThirdParty/tracy/client/TracyProfiler.hpp index 99ae63e4f..8892fb14f 100644 --- a/Source/ThirdParty/tracy/client/TracyProfiler.hpp +++ b/Source/ThirdParty/tracy/client/TracyProfiler.hpp @@ -10,6 +10,7 @@ #include "tracy_concurrentqueue.h" #include "tracy_SPSCQueue.h" #include "TracyCallstack.hpp" +#include "TracySysPower.hpp" #include "TracySysTime.hpp" #include "TracyFastVector.hpp" #include "../common/TracyQueue.hpp" @@ -190,7 +191,22 @@ public: if( HardwareSupportsInvariantTSC() ) { uint64_t rax, rdx; +#ifdef TRACY_PATCHABLE_NOPSLEDS + // Some external tooling (such as rr) wants to patch our rdtsc and replace it by a + // branch to control the external input seen by a program. This kind of patching is + // not generally possible depending on the surrounding code and can lead to significant + // slowdowns if the compiler generated unlucky code and rr and tracy are used together. + // To avoid this, use the rr-safe `nopl 0(%rax, %rax, 1); rdtsc` instruction sequence, + // which rr promises will be patchable independent of the surrounding code. + asm volatile ( + // This is nopl 0(%rax, %rax, 1), but assemblers are inconsistent about whether + // they emit that as a 4 or 5 byte sequence and we need to be guaranteed to use + // the 5 byte one. + ".byte 0x0f, 0x1f, 0x44, 0x00, 0x00\n\t" + "rdtsc" : "=a" (rax), "=d" (rdx) ); +#else asm volatile ( "rdtsc" : "=a" (rax), "=d" (rdx) ); +#endif return (int64_t)(( rdx << 32 ) + rax); } # else @@ -240,6 +256,30 @@ public: p.m_serialLock.unlock(); } + static void SendFrameMark( const char* name ); + static void SendFrameMark( const char* name, QueueType type ); + static void SendFrameImage( const void* image, uint16_t w, uint16_t h, uint8_t offset, bool flip ); + static void PlotData( const char* name, int64_t val ); + static void PlotData( const char* name, float val ); + static void PlotData( const char* name, double val ); + static void ConfigurePlot( const char* name, PlotFormatType type, bool step, bool fill, uint32_t color ); + static void Message( const char* txt, size_t size, int callstack ); + static void Message( const char* txt, int callstack ); + static void MessageColor( const char* txt, size_t size, uint32_t color, int callstack ); + static void MessageColor( const char* txt, uint32_t color, int callstack ); + static void MessageAppInfo( const char* txt, size_t size ); + static void MemAlloc( const void* ptr, size_t size, bool secure ); + static void MemFree( const void* ptr, bool secure ); + static void MemAllocCallstack( const void* ptr, size_t size, int depth, bool secure ); + static void MemFreeCallstack( const void* ptr, int depth, bool secure ); + static void MemAllocNamed( const void* ptr, size_t size, bool secure, const char* name ); + static void MemFreeNamed( const void* ptr, bool secure, const char* name ); + static void MemAllocCallstackNamed( const void* ptr, size_t size, int depth, bool secure, const char* name ); + static void MemFreeCallstackNamed( const void* ptr, int depth, bool secure, const char* name ); + static void SendCallstack( int depth ); + static void ParameterRegister( ParameterCallback cb, void* data ); + static void ParameterSetup( uint32_t idx, const char* name, bool isBool, int32_t val ); + static tracy_force_inline void SourceCallbackRegister( SourceContentsCallback cb, void* data ) { auto& profiler = GetProfiler(); @@ -264,31 +304,6 @@ public: } #endif - static void SendFrameMark( const char* name ); - static void SendFrameMark( const char* name, QueueType type ); - static void SendFrameImage( const void* image, uint16_t w, uint16_t h, uint8_t offset, bool flip ); - static void PlotData( const char* name, int64_t val ); - static void PlotData( const char* name, float val ); - static void PlotData( const char* name, double val ); - static void ConfigurePlot( const char* name, PlotFormatType type, bool step, bool fill, uint32_t color ); - static void Message( const char* txt, size_t size, int callstack ); - static void Message( const char* txt, int callstack ); - static void MessageColor( const char* txt, size_t size, uint32_t color, int callstack ); - static void MessageColor( const char* txt, uint32_t color, int callstack ); - static void MessageAppInfo( const char* txt, size_t size ); - static void MemAlloc( const void* ptr, size_t size, bool secure ); - static void MemFree( const void* ptr, bool secure ); - static void MemAllocCallstack( const void* ptr, size_t size, int depth, bool secure ); - static void MemFreeCallstack( const void* ptr, int depth, bool secure ); - static void MemAllocNamed( const void* ptr, size_t size, bool secure, const char* name ); - static void MemFreeNamed( const void* ptr, bool secure, const char* name ); - static void MemAllocCallstackNamed( const void* ptr, size_t size, int depth, bool secure, const char* name ); - static void MemFreeCallstackNamed( const void* ptr, int depth, bool secure, const char* name ); - static void SendCallstack( int depth ); - static void ParameterRegister( ParameterCallback cb ); - static void ParameterRegister( ParameterCallback cb, void* data ); - static void ParameterSetup( uint32_t idx, const char* name, bool isBool, int32_t val ); - void SendCallstack( int depth, const char* skipBefore ); static void CutCallstack( void* callstack, const char* skipBefore ); @@ -299,6 +314,13 @@ public: return m_isConnected.load( std::memory_order_acquire ); } + tracy_force_inline void SetProgramName( const char* name ) + { + m_programNameLock.lock(); + m_programName = name; + m_programNameLock.unlock(); + } + #ifdef TRACY_ON_DEMAND tracy_force_inline uint64_t ConnectionId() const { @@ -347,13 +369,13 @@ public: static tracy_force_inline uint64_t AllocSourceLocation( uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz ) { - return AllocSourceLocation( line, source, sourceSz, function, functionSz, (const char*)nullptr, 0 ); + return AllocSourceLocation( line, source, sourceSz, function, functionSz, nullptr, 0 ); } static tracy_force_inline uint64_t AllocSourceLocation( uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz, const char* name, size_t nameSz ) { const auto sz32 = uint32_t( 2 + 4 + 4 + functionSz + 1 + sourceSz + 1 + nameSz ); - assert( sz32 <= std::numeric_limits::max() ); + assert( sz32 <= (std::numeric_limits::max)() ); const auto sz = uint16_t( sz32 ); auto ptr = (char*)tracy_malloc( sz ); memcpy( ptr, &sz, 2 ); @@ -370,28 +392,6 @@ public: return uint64_t( ptr ); } - static tracy_force_inline uint64_t AllocSourceLocation( uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz, const Char* name, size_t nameSz ) - { - const auto sz32 = uint32_t( 2 + 4 + 4 + functionSz + 1 + sourceSz + 1 + nameSz ); - assert( sz32 <= std::numeric_limits::max() ); - const auto sz = uint16_t( sz32 ); - auto ptr = (char*)tracy_malloc( sz ); - memcpy( ptr, &sz, 2 ); - memset( ptr + 2, 0, 4 ); - memcpy( ptr + 6, &line, 4 ); - memcpy( ptr + 10, function, functionSz ); - ptr[10 + functionSz] = '\0'; - memcpy( ptr + 10 + functionSz + 1, source, sourceSz ); - ptr[10 + functionSz + 1 + sourceSz] = '\0'; - if( nameSz != 0 ) - { - char* dst = ptr + 10 + functionSz + 1 + sourceSz + 1; - for ( size_t i = 0; i < nameSz; i++) - dst[i] = (char)name[i]; - } - return uint64_t( ptr ); - } - private: enum class DequeueStatus { DataDequeued, ConnectionLost, QueueEmpty }; enum class ThreadCtxStatus { Same, Changed, ConnectionLost }; @@ -586,6 +586,10 @@ private: void ProcessSysTime() {} #endif +#ifdef TRACY_HAS_SYSPOWER + SysPower m_sysPower; +#endif + ParameterCallback m_paramCallback; void* m_paramCallbackData; SourceContentsCallback m_sourceCallback; @@ -604,6 +608,9 @@ private: } m_prevSignal; #endif bool m_crashHandlerInstalled; + + const char* m_programName; + TracyMutex m_programNameLock; }; } diff --git a/Source/ThirdParty/tracy/client/TracyScoped.hpp b/Source/ThirdParty/tracy/client/TracyScoped.hpp index 1e5b6c809..bb916aa57 100644 --- a/Source/ThirdParty/tracy/client/TracyScoped.hpp +++ b/Source/ThirdParty/tracy/client/TracyScoped.hpp @@ -20,19 +20,7 @@ void ScopedZone::Begin(const SourceLocationData* srcloc) TracyLfqPrepare( QueueType::ZoneBegin ); MemWrite( &item->zoneBegin.time, Profiler::GetTime() ); MemWrite( &item->zoneBegin.srcloc, (uint64_t)srcloc ); - TracyLfqCommit; -} - -void ScopedZone::Begin(uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz, const Char* name, size_t nameSz) -{ -#ifdef TRACY_ON_DEMAND - if (!GetProfiler().IsConnected()) return; -#endif - TracyLfqPrepare( QueueType::ZoneBeginAllocSrcLoc ); - const auto srcloc = Profiler::AllocSourceLocation( line, source, sourceSz, function, functionSz, name, nameSz ); - MemWrite( &item->zoneBegin.time, Profiler::GetTime() ); - MemWrite( &item->zoneBegin.srcloc, srcloc ); - TracyLfqCommit; + TracyQueueCommit( zoneBeginThread ); } void ScopedZone::End() @@ -42,7 +30,7 @@ void ScopedZone::End() #endif TracyLfqPrepare( QueueType::ZoneEnd ); MemWrite( &item->zoneEnd.time, Profiler::GetTime() ); - TracyLfqCommit; + TracyQueueCommit( zoneEndThread ); } ScopedZone::ScopedZone( const SourceLocationData* srcloc, bool is_active ) @@ -132,7 +120,7 @@ ScopedZone::~ScopedZone() void ScopedZone::Text( const char* txt, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); if( !m_active ) return; #ifdef TRACY_ON_DEMAND if( GetProfiler().ConnectionId() != m_connectionId ) return; @@ -147,7 +135,7 @@ void ScopedZone::Text( const char* txt, size_t size ) void ScopedZone::Text( const Char* txt, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); if( !m_active ) return; #ifdef TRACY_ON_DEMAND if( GetProfiler().ConnectionId() != m_connectionId ) return; @@ -163,7 +151,7 @@ void ScopedZone::Text( const Char* txt, size_t size ) void ScopedZone::Name( const char* txt, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); if( !m_active ) return; #ifdef TRACY_ON_DEMAND if( GetProfiler().ConnectionId() != m_connectionId ) return; @@ -178,7 +166,7 @@ void ScopedZone::Name( const char* txt, size_t size ) void ScopedZone::Name( const Char* txt, size_t size ) { - assert( size < std::numeric_limits::max() ); + assert( size < (std::numeric_limits::max)() ); if( !m_active ) return; #ifdef TRACY_ON_DEMAND if( GetProfiler().ConnectionId() != m_connectionId ) return; @@ -199,9 +187,9 @@ void ScopedZone::Color( uint32_t color ) if( GetProfiler().ConnectionId() != m_connectionId ) return; #endif TracyQueuePrepare( QueueType::ZoneColor ); - MemWrite( &item->zoneColor.r, uint8_t( ( color ) & 0xFF ) ); + MemWrite( &item->zoneColor.b, uint8_t( ( color ) & 0xFF ) ); MemWrite( &item->zoneColor.g, uint8_t( ( color >> 8 ) & 0xFF ) ); - MemWrite( &item->zoneColor.b, uint8_t( ( color >> 16 ) & 0xFF ) ); + MemWrite( &item->zoneColor.r, uint8_t( ( color >> 16 ) & 0xFF ) ); TracyQueueCommit( zoneColorThread ); } diff --git a/Source/ThirdParty/tracy/client/TracySysPower.cpp b/Source/ThirdParty/tracy/client/TracySysPower.cpp new file mode 100644 index 000000000..bd5939da2 --- /dev/null +++ b/Source/ThirdParty/tracy/client/TracySysPower.cpp @@ -0,0 +1,164 @@ +#include "TracySysPower.hpp" + +#ifdef TRACY_HAS_SYSPOWER + +#include +#include +#include +#include +#include +#include + +#include "TracyDebug.hpp" +#include "TracyProfiler.hpp" +#include "../common/TracyAlloc.hpp" + +namespace tracy +{ + +SysPower::SysPower() + : m_domains( 4 ) + , m_lastTime( 0 ) +{ + ScanDirectory( "/sys/devices/virtual/powercap/intel-rapl", -1 ); +} + +SysPower::~SysPower() +{ + for( auto& v : m_domains ) + { + fclose( v.handle ); + // Do not release v.name, as it may be still needed + } +} + +void SysPower::Tick() +{ + auto t = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + if( t - m_lastTime > 10000000 ) // 10 ms + { + m_lastTime = t; + for( auto& v : m_domains ) + { + char tmp[32]; + if( fread( tmp, 1, 32, v.handle ) > 0 ) + { + rewind( v.handle ); + auto p = (uint64_t)atoll( tmp ); + uint64_t delta; + if( p >= v.value ) + { + delta = p - v.value; + } + else + { + delta = v.overflow - v.value + p; + } + v.value = p; + + TracyLfqPrepare( QueueType::SysPowerReport ); + MemWrite( &item->sysPower.time, Profiler::GetTime() ); + MemWrite( &item->sysPower.delta, delta ); + MemWrite( &item->sysPower.name, (uint64_t)v.name ); + TracyLfqCommit; + } + } + } +} + +void SysPower::ScanDirectory( const char* path, int parent ) +{ + DIR* dir = opendir( path ); + if( !dir ) return; + struct dirent* ent; + uint64_t maxRange = 0; + char* name = nullptr; + FILE* handle = nullptr; + while( ( ent = readdir( dir ) ) ) + { + if( ent->d_type == DT_REG ) + { + if( strcmp( ent->d_name, "max_energy_range_uj" ) == 0 ) + { + char tmp[PATH_MAX]; + snprintf( tmp, PATH_MAX, "%s/max_energy_range_uj", path ); + FILE* f = fopen( tmp, "r" ); + if( f ) + { + fscanf( f, "%" PRIu64, &maxRange ); + fclose( f ); + } + } + else if( strcmp( ent->d_name, "name" ) == 0 ) + { + char tmp[PATH_MAX]; + snprintf( tmp, PATH_MAX, "%s/name", path ); + FILE* f = fopen( tmp, "r" ); + if( f ) + { + char ntmp[128]; + if( fgets( ntmp, 128, f ) ) + { + // Last character is newline, skip it + const auto sz = strlen( ntmp ) - 1; + if( parent < 0 ) + { + name = (char*)tracy_malloc( sz + 1 ); + memcpy( name, ntmp, sz ); + name[sz] = '\0'; + } + else + { + const auto p = m_domains[parent]; + const auto psz = strlen( p.name ); + name = (char*)tracy_malloc( psz + sz + 2 ); + memcpy( name, p.name, psz ); + name[psz] = ':'; + memcpy( name+psz+1, ntmp, sz ); + name[psz+sz+1] = '\0'; + } + } + fclose( f ); + } + } + else if( strcmp( ent->d_name, "energy_uj" ) == 0 ) + { + char tmp[PATH_MAX]; + snprintf( tmp, PATH_MAX, "%s/energy_uj", path ); + handle = fopen( tmp, "r" ); + } + } + if( name && handle && maxRange > 0 ) break; + } + if( name && handle && maxRange > 0 ) + { + parent = (int)m_domains.size(); + Domain* domain = m_domains.push_next(); + domain->value = 0; + domain->overflow = maxRange; + domain->handle = handle; + domain->name = name; + TracyDebug( "Power domain id %i, %s found at %s\n", parent, name, path ); + } + else + { + if( name ) tracy_free( name ); + if( handle ) fclose( handle ); + } + + rewinddir( dir ); + while( ( ent = readdir( dir ) ) ) + { + if( ent->d_type == DT_DIR && strncmp( ent->d_name, "intel-rapl:", 11 ) == 0 ) + { + char tmp[PATH_MAX]; + snprintf( tmp, PATH_MAX, "%s/%s", path, ent->d_name ); + ScanDirectory( tmp, parent ); + } + } + closedir( dir ); +} + +} + +#endif diff --git a/Source/ThirdParty/tracy/client/TracySysPower.hpp b/Source/ThirdParty/tracy/client/TracySysPower.hpp new file mode 100644 index 000000000..210123bce --- /dev/null +++ b/Source/ThirdParty/tracy/client/TracySysPower.hpp @@ -0,0 +1,44 @@ +#ifndef __TRACYSYSPOWER_HPP__ +#define __TRACYSYSPOWER_HPP__ + +#if defined __linux__ +# define TRACY_HAS_SYSPOWER +#endif + +#ifdef TRACY_HAS_SYSPOWER + +#include +#include + +#include "TracyFastVector.hpp" + +namespace tracy +{ + +class SysPower +{ + struct Domain + { + uint64_t value; + uint64_t overflow; + FILE* handle; + const char* name; + }; + +public: + SysPower(); + ~SysPower(); + + void Tick(); + +private: + void ScanDirectory( const char* path, int parent ); + + FastVector m_domains; + uint64_t m_lastTime; +}; + +} +#endif + +#endif diff --git a/Source/ThirdParty/tracy/client/TracySysTrace.cpp b/Source/ThirdParty/tracy/client/TracySysTrace.cpp index 23b1020a5..af0641fef 100644 --- a/Source/ThirdParty/tracy/client/TracySysTrace.cpp +++ b/Source/ThirdParty/tracy/client/TracySysTrace.cpp @@ -409,6 +409,7 @@ bool SysTraceStart( int64_t& samplingPeriod ) return false; } +#ifndef TRACY_NO_SAMPLING if( isOs64Bit ) { CLASSIC_EVENT_ID stackId[2] = {}; @@ -423,6 +424,7 @@ bool SysTraceStart( int64_t& samplingPeriod ) return false; } } +#endif #ifdef UNICODE WCHAR KernelLoggerName[sizeof( KERNEL_LOGGER_NAME )]; @@ -768,6 +770,13 @@ bool SysTraceStart( int64_t& samplingPeriod ) TracyDebug( "sched_wakeup id: %i\n", wakeupId ); TracyDebug( "drm_vblank_event id: %i\n", vsyncId ); +#ifdef TRACY_NO_SAMPLING + const bool noSoftwareSampling = true; +#else + const char* noSoftwareSamplingEnv = GetEnvVar( "TRACY_NO_SAMPLING" ); + const bool noSoftwareSampling = noSoftwareSamplingEnv && noSoftwareSamplingEnv[0] == '1'; +#endif + #ifdef TRACY_NO_SAMPLE_RETIREMENT const bool noRetirement = true; #else @@ -837,28 +846,31 @@ bool SysTraceStart( int64_t& samplingPeriod ) pe.clockid = CLOCK_MONOTONIC_RAW; #endif - TracyDebug( "Setup software sampling\n" ); - ProbePreciseIp( pe, currentPid ); - for( int i=0; i static inline bool circular_less_than(T a, T b) { static_assert(std::is_integral::value && !std::numeric_limits::is_signed, "circular_less_than is intended to be used only with unsigned integer types"); - return static_cast(a - b) > (static_cast(static_cast(1) << static_cast(sizeof(T) * CHAR_BIT - 1))); + return static_cast(a - b) > static_cast(static_cast(1) << (static_cast(sizeof(T) * CHAR_BIT - 1))); + // Note: extra parens around rhs of operator<< is MSVC bug: https://developercommunity2.visualstudio.com/t/C4554-triggers-when-both-lhs-and-rhs-is/10034931 + // silencing the bug requires #pragma warning(disable: 4554) around the calling code and has no effect when done here. } -#ifdef _MSC_VER -#pragma warning(pop) -#endif template static inline char* align_for(char* ptr) diff --git a/Source/ThirdParty/tracy/client/tracy_rpmalloc.cpp b/Source/ThirdParty/tracy/client/tracy_rpmalloc.cpp index 8efa626a9..711505d21 100644 --- a/Source/ThirdParty/tracy/client/tracy_rpmalloc.cpp +++ b/Source/ThirdParty/tracy/client/tracy_rpmalloc.cpp @@ -147,7 +147,7 @@ # if defined(__APPLE__) # include # if !TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR -# include +# include # include # endif # include diff --git a/Source/ThirdParty/tracy/common/TracyProtocol.hpp b/Source/ThirdParty/tracy/common/TracyProtocol.hpp index dd30e5391..5eb1639db 100644 --- a/Source/ThirdParty/tracy/common/TracyProtocol.hpp +++ b/Source/ThirdParty/tracy/common/TracyProtocol.hpp @@ -9,14 +9,14 @@ namespace tracy constexpr unsigned Lz4CompressBound( unsigned isize ) { return isize + ( isize / 255 ) + 16; } -enum : uint32_t { ProtocolVersion = 63 }; +enum : uint32_t { ProtocolVersion = 64 }; enum : uint16_t { BroadcastVersion = 3 }; using lz4sz_t = uint32_t; enum { TargetFrameSize = 256 * 1024 }; enum { LZ4Size = Lz4CompressBound( TargetFrameSize ) }; -static_assert( LZ4Size <= std::numeric_limits::max(), "LZ4Size greater than lz4sz_t" ); +static_assert( LZ4Size <= (std::numeric_limits::max)(), "LZ4Size greater than lz4sz_t" ); static_assert( TargetFrameSize * 2 >= 64 * 1024, "Not enough space for LZ4 stream buffer" ); enum { HandshakeShibbolethSize = 8 }; diff --git a/Source/ThirdParty/tracy/common/TracyQueue.hpp b/Source/ThirdParty/tracy/common/TracyQueue.hpp index 092d26969..051d412ab 100644 --- a/Source/ThirdParty/tracy/common/TracyQueue.hpp +++ b/Source/ThirdParty/tracy/common/TracyQueue.hpp @@ -1,6 +1,7 @@ #ifndef __TRACYQUEUE_HPP__ #define __TRACYQUEUE_HPP__ +#include #include namespace tracy @@ -89,6 +90,7 @@ enum class QueueType : uint8_t GpuNewContext, CallstackFrame, SysTimeReport, + SysPowerReport, TidToPid, HwSampleCpuCycle, HwSampleInstructionRetired, @@ -165,9 +167,9 @@ struct QueueZoneValidationThread : public QueueZoneValidation struct QueueZoneColor { - uint8_t r; - uint8_t g; uint8_t b; + uint8_t g; + uint8_t r; }; struct QueueZoneColorThread : public QueueZoneColor @@ -221,9 +223,9 @@ struct QueueSourceLocation uint64_t function; // ptr uint64_t file; // ptr uint32_t line; - uint8_t r; - uint8_t g; uint8_t b; + uint8_t g; + uint8_t r; }; struct QueueZoneTextFat @@ -324,7 +326,7 @@ struct QueuePlotDataInt : public QueuePlotDataBase int64_t val; }; -struct QueuePlotDataFloat : public QueuePlotDataBase +struct QueuePlotDataFloat : public QueuePlotDataBase { float val; }; @@ -341,9 +343,9 @@ struct QueueMessage struct QueueMessageColor : public QueueMessage { - uint8_t r; - uint8_t g; uint8_t b; + uint8_t g; + uint8_t r; }; struct QueueMessageLiteral : public QueueMessage @@ -562,6 +564,13 @@ struct QueueSysTime float sysTime; }; +struct QueueSysPower +{ + int64_t time; + uint64_t delta; + uint64_t name; // ptr +}; + struct QueueContextSwitch { int64_t time; @@ -590,6 +599,13 @@ struct QueueHwSample int64_t time; }; +enum class PlotFormatType : uint8_t +{ + Number, + Memory, + Percentage +}; + struct QueuePlotConfig { uint64_t name; // ptr @@ -721,6 +737,7 @@ struct QueueItem QueueCrashReport crashReport; QueueCrashReportThread crashReportThread; QueueSysTime sysTime; + QueueSysPower sysPower; QueueContextSwitch contextSwitch; QueueThreadWakeup threadWakeup; QueueTidToPid tidToPid; @@ -824,6 +841,7 @@ static constexpr size_t QueueDataSize[] = { sizeof( QueueHeader ) + sizeof( QueueGpuNewContext ), sizeof( QueueHeader ) + sizeof( QueueCallstackFrame ), sizeof( QueueHeader ) + sizeof( QueueSysTime ), + sizeof( QueueHeader ) + sizeof( QueueSysPower ), sizeof( QueueHeader ) + sizeof( QueueTidToPid ), sizeof( QueueHeader ) + sizeof( QueueHwSample ), // cpu cycle sizeof( QueueHeader ) + sizeof( QueueHwSample ), // instruction retired diff --git a/Source/ThirdParty/tracy/common/TracySocket.cpp b/Source/ThirdParty/tracy/common/TracySocket.cpp index 176bbc7aa..259678989 100644 --- a/Source/ThirdParty/tracy/common/TracySocket.cpp +++ b/Source/ThirdParty/tracy/common/TracySocket.cpp @@ -353,7 +353,7 @@ int Socket::Recv( void* _buf, int len, int timeout ) } } -int Socket::ReadUpTo( void* _buf, int len, int timeout ) +int Socket::ReadUpTo( void* _buf, int len ) { const auto sock = m_sock.load( std::memory_order_relaxed ); auto buf = (char*)_buf; @@ -678,10 +678,10 @@ bool UdpListen::Listen( uint16_t port ) #endif #if defined _WIN32 unsigned long reuse = 1; - setsockopt( m_sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof( reuse ) ); + setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof( reuse ) ); #else int reuse = 1; - setsockopt( m_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) ); + setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) ); #endif #if defined _WIN32 unsigned long broadcast = 1; diff --git a/Source/ThirdParty/tracy/common/TracySocket.hpp b/Source/ThirdParty/tracy/common/TracySocket.hpp index 4b3075e29..f7713aac6 100644 --- a/Source/ThirdParty/tracy/common/TracySocket.hpp +++ b/Source/ThirdParty/tracy/common/TracySocket.hpp @@ -29,7 +29,7 @@ public: int Send( const void* buf, int len ); int GetSendBufSize(); - int ReadUpTo( void* buf, int len, int timeout ); + int ReadUpTo( void* buf, int len ); bool Read( void* buf, int len, int timeout ); template diff --git a/Source/ThirdParty/tracy/common/TracySystem.cpp b/Source/ThirdParty/tracy/common/TracySystem.cpp index 5ca8e1f45..9a477aa31 100644 --- a/Source/ThirdParty/tracy/common/TracySystem.cpp +++ b/Source/ThirdParty/tracy/common/TracySystem.cpp @@ -205,61 +205,60 @@ TRACY_API const char* GetThreadName( uint32_t id ) } ptr = ptr->next; } -#else -# if defined _WIN32 -# ifdef TRACY_UWP - static auto _GetThreadDescription = &::GetThreadDescription; -# else - static auto _GetThreadDescription = (t_GetThreadDescription)GetProcAddress( GetModuleHandleA( "kernel32.dll" ), "GetThreadDescription" ); -# endif +#endif + +#if defined _WIN32 +# ifdef TRACY_UWP + static auto _GetThreadDescription = &::GetThreadDescription; +# else + static auto _GetThreadDescription = (t_GetThreadDescription)GetProcAddress( GetModuleHandleA( "kernel32.dll" ), "GetThreadDescription" ); +# endif if( _GetThreadDescription ) { auto hnd = OpenThread( THREAD_QUERY_LIMITED_INFORMATION, FALSE, (DWORD)id ); if( hnd != 0 ) { PWSTR tmp; - _GetThreadDescription( hnd, &tmp ); - auto ret = wcstombs( buf, tmp, 256 ); - CloseHandle( hnd ); - if( ret != 0 ) + if( SUCCEEDED( _GetThreadDescription( hnd, &tmp ) ) ) { - return buf; + auto ret = wcstombs( buf, tmp, 256 ); + CloseHandle( hnd ); + LocalFree( tmp ); + if( ret != static_cast( -1 ) ) + { + return buf; + } } } } -# elif defined __linux__ - int cs, fd; - char path[32]; -# ifdef __ANDROID__ - int tid = gettid(); -# else - int tid = (int) syscall( SYS_gettid ); -# endif - snprintf( path, sizeof( path ), "/proc/self/task/%d/comm", tid ); - sprintf( buf, "%" PRIu32, id ); -# ifndef __ANDROID__ - pthread_setcancelstate( PTHREAD_CANCEL_DISABLE, &cs ); -# endif - if ( ( fd = open( path, O_RDONLY ) ) > 0) { - int len = read( fd, buf, 255 ); - if( len > 0 ) - { - buf[len] = 0; - if( len > 1 && buf[len-1] == '\n' ) - { - buf[len-1] = 0; - } - } - close( fd ); - } -# ifndef __ANDROID__ - pthread_setcancelstate( cs, 0 ); -# endif - return buf; -# endif +#elif defined __linux__ + int cs, fd; + char path[32]; + snprintf( path, sizeof( path ), "/proc/self/task/%d/comm", id ); + sprintf( buf, "%" PRIu32, id ); +# ifndef __ANDROID__ + pthread_setcancelstate( PTHREAD_CANCEL_DISABLE, &cs ); +# endif + if ( ( fd = open( path, O_RDONLY ) ) > 0) { + int len = read( fd, buf, 255 ); + if( len > 0 ) + { + buf[len] = 0; + if( len > 1 && buf[len-1] == '\n' ) + { + buf[len-1] = 0; + } + } + close( fd ); + } +# ifndef __ANDROID__ + pthread_setcancelstate( cs, 0 ); +# endif + return buf; #endif - sprintf( buf, "%" PRIu32, id ); - return buf; + + sprintf( buf, "%" PRIu32, id ); + return buf; } TRACY_API const char* GetEnvVar( const char* name ) @@ -295,3 +294,13 @@ TRACY_API const char* GetEnvVar( const char* name ) } } + +#ifdef __cplusplus +extern "C" { +#endif + +TRACY_API void ___tracy_set_thread_name( const char* name ) { tracy::SetThreadName( name ); } + +#ifdef __cplusplus +} +#endif diff --git a/Source/ThirdParty/tracy/common/TracySystem.hpp b/Source/ThirdParty/tracy/common/TracySystem.hpp index edcc5cf31..7a88a00b1 100644 --- a/Source/ThirdParty/tracy/common/TracySystem.hpp +++ b/Source/ThirdParty/tracy/common/TracySystem.hpp @@ -25,13 +25,6 @@ namespace tracy { -enum class PlotFormatType : uint8_t -{ - Number, - Memory, - Percentage -}; - typedef void(*ParameterCallback)( void* data, uint32_t idx, int32_t val ); struct TRACY_API SourceLocationData @@ -47,7 +40,6 @@ class TRACY_API ScopedZone { public: static void Begin( const SourceLocationData* srcloc ); - static void Begin( uint32_t line, const char* source, size_t sourceSz, const char* function, size_t functionSz, const Char* name, size_t nameSz ); static void End(); ScopedZone( const ScopedZone& ) = delete; @@ -68,7 +60,6 @@ public: void Name( const Char* txt, size_t size ); void Color( uint32_t color ); void Value( uint64_t value ); - bool IsActive() const; private: const bool m_active; diff --git a/Source/ThirdParty/tracy/common/TracyVersion.hpp b/Source/ThirdParty/tracy/common/TracyVersion.hpp index 983d1c51f..2355279f7 100644 --- a/Source/ThirdParty/tracy/common/TracyVersion.hpp +++ b/Source/ThirdParty/tracy/common/TracyVersion.hpp @@ -6,7 +6,7 @@ namespace tracy namespace Version { enum { Major = 0 }; -enum { Minor = 9 }; +enum { Minor = 10 }; enum { Patch = 0 }; } } diff --git a/Source/ThirdParty/tracy/libbacktrace/config.h b/Source/ThirdParty/tracy/libbacktrace/config.h index aa3259d11..87e38a95b 100644 --- a/Source/ThirdParty/tracy/libbacktrace/config.h +++ b/Source/ThirdParty/tracy/libbacktrace/config.h @@ -1,4 +1,8 @@ #include +#if defined(__linux__) && !defined(__GLIBC__) && !defined(__WORDSIZE) +// include __WORDSIZE headers for musl +# include +#endif #if __WORDSIZE == 64 # define BACKTRACE_ELF_SIZE 64 #else diff --git a/Source/ThirdParty/tracy/libbacktrace/dwarf.cpp b/Source/ThirdParty/tracy/libbacktrace/dwarf.cpp index 246cb9f36..f3899cbce 100644 --- a/Source/ThirdParty/tracy/libbacktrace/dwarf.cpp +++ b/Source/ThirdParty/tracy/libbacktrace/dwarf.cpp @@ -473,7 +473,7 @@ enum attr_val_encoding /* An address. */ ATTR_VAL_ADDRESS, /* An index into the .debug_addr section, whose value is relative to - * the DW_AT_addr_base attribute of the compilation unit. */ + the DW_AT_addr_base attribute of the compilation unit. */ ATTR_VAL_ADDRESS_INDEX, /* A unsigned integer. */ ATTR_VAL_UINT, @@ -611,8 +611,8 @@ struct function struct function_addrs { /* Range is LOW <= PC < HIGH. */ - uint64_t low; - uint64_t high; + uintptr_t low; + uintptr_t high; /* Function for this address range. */ struct function *function; }; @@ -693,8 +693,8 @@ struct unit struct unit_addrs { /* Range is LOW <= PC < HIGH. */ - uint64_t low; - uint64_t high; + uintptr_t low; + uintptr_t high; /* Compilation unit for this address range. */ struct unit *u; }; @@ -1431,7 +1431,7 @@ resolve_addr_index (const struct dwarf_sections *dwarf_sections, uint64_t addr_base, int addrsize, int is_bigendian, uint64_t addr_index, backtrace_error_callback error_callback, void *data, - uint64_t *address) + uintptr_t *address) { uint64_t offset; struct dwarf_buf addr_buf; @@ -1452,7 +1452,7 @@ resolve_addr_index (const struct dwarf_sections *dwarf_sections, addr_buf.data = data; addr_buf.reported_underflow = 0; - *address = read_address (&addr_buf, addrsize); + *address = (uintptr_t) read_address (&addr_buf, addrsize); return 1; } @@ -1531,7 +1531,7 @@ function_addrs_search (const void *vkey, const void *ventry) static int add_unit_addr (struct backtrace_state *state, void *rdata, - uint64_t lowpc, uint64_t highpc, + uintptr_t lowpc, uintptr_t highpc, backtrace_error_callback error_callback, void *data, void *pvec) { @@ -1867,10 +1867,10 @@ lookup_abbrev (struct abbrevs *abbrevs, uint64_t code, lowpc/highpc is set or ranges is set. */ struct pcrange { - uint64_t lowpc; /* The low PC value. */ + uintptr_t lowpc; /* The low PC value. */ int have_lowpc; /* Whether a low PC value was found. */ int lowpc_is_addr_index; /* Whether lowpc is in .debug_addr. */ - uint64_t highpc; /* The high PC value. */ + uintptr_t highpc; /* The high PC value. */ int have_highpc; /* Whether a high PC value was found. */ int highpc_is_relative; /* Whether highpc is relative to lowpc. */ int highpc_is_addr_index; /* Whether highpc is in .debug_addr. */ @@ -1890,12 +1890,12 @@ update_pcrange (const struct attr* attr, const struct attr_val* val, case DW_AT_low_pc: if (val->encoding == ATTR_VAL_ADDRESS) { - pcrange->lowpc = val->u.uint; + pcrange->lowpc = (uintptr_t) val->u.uint; pcrange->have_lowpc = 1; } else if (val->encoding == ATTR_VAL_ADDRESS_INDEX) { - pcrange->lowpc = val->u.uint; + pcrange->lowpc = (uintptr_t) val->u.uint; pcrange->have_lowpc = 1; pcrange->lowpc_is_addr_index = 1; } @@ -1904,18 +1904,18 @@ update_pcrange (const struct attr* attr, const struct attr_val* val, case DW_AT_high_pc: if (val->encoding == ATTR_VAL_ADDRESS) { - pcrange->highpc = val->u.uint; + pcrange->highpc = (uintptr_t) val->u.uint; pcrange->have_highpc = 1; } else if (val->encoding == ATTR_VAL_UINT) { - pcrange->highpc = val->u.uint; + pcrange->highpc = (uintptr_t) val->u.uint; pcrange->have_highpc = 1; pcrange->highpc_is_relative = 1; } else if (val->encoding == ATTR_VAL_ADDRESS_INDEX) { - pcrange->highpc = val->u.uint; + pcrange->highpc = (uintptr_t) val->u.uint; pcrange->have_highpc = 1; pcrange->highpc_is_addr_index = 1; } @@ -1950,16 +1950,16 @@ add_low_high_range (struct backtrace_state *state, uintptr_t base_address, int is_bigendian, struct unit *u, const struct pcrange *pcrange, int (*add_range) (struct backtrace_state *state, - void *rdata, uint64_t lowpc, - uint64_t highpc, + void *rdata, uintptr_t lowpc, + uintptr_t highpc, backtrace_error_callback error_callback, void *data, void *vec), void *rdata, backtrace_error_callback error_callback, void *data, void *vec) { - uint64_t lowpc; - uint64_t highpc; + uintptr_t lowpc; + uintptr_t highpc; lowpc = pcrange->lowpc; if (pcrange->lowpc_is_addr_index) @@ -1997,10 +1997,10 @@ add_ranges_from_ranges ( struct backtrace_state *state, const struct dwarf_sections *dwarf_sections, uintptr_t base_address, int is_bigendian, - struct unit *u, uint64_t base, + struct unit *u, uintptr_t base, const struct pcrange *pcrange, int (*add_range) (struct backtrace_state *state, void *rdata, - uint64_t lowpc, uint64_t highpc, + uintptr_t lowpc, uintptr_t highpc, backtrace_error_callback error_callback, void *data, void *vec), void *rdata, @@ -2039,12 +2039,12 @@ add_ranges_from_ranges ( break; if (is_highest_address (low, u->addrsize)) - base = high; + base = (uintptr_t) high; else { if (!add_range (state, rdata, - low + base + base_address, - high + base + base_address, + (uintptr_t) low + base + base_address, + (uintptr_t) high + base + base_address, error_callback, data, vec)) return 0; } @@ -2064,10 +2064,10 @@ add_ranges_from_rnglists ( struct backtrace_state *state, const struct dwarf_sections *dwarf_sections, uintptr_t base_address, int is_bigendian, - struct unit *u, uint64_t base, + struct unit *u, uintptr_t base, const struct pcrange *pcrange, int (*add_range) (struct backtrace_state *state, void *rdata, - uint64_t lowpc, uint64_t highpc, + uintptr_t lowpc, uintptr_t highpc, backtrace_error_callback error_callback, void *data, void *vec), void *rdata, @@ -2133,8 +2133,8 @@ add_ranges_from_rnglists ( case DW_RLE_startx_endx: { uint64_t index; - uint64_t low; - uint64_t high; + uintptr_t low; + uintptr_t high; index = read_uleb128 (&rnglists_buf); if (!resolve_addr_index (dwarf_sections, u->addr_base, @@ -2156,8 +2156,8 @@ add_ranges_from_rnglists ( case DW_RLE_startx_length: { uint64_t index; - uint64_t low; - uint64_t length; + uintptr_t low; + uintptr_t length; index = read_uleb128 (&rnglists_buf); if (!resolve_addr_index (dwarf_sections, u->addr_base, @@ -2187,16 +2187,16 @@ add_ranges_from_rnglists ( break; case DW_RLE_base_address: - base = read_address (&rnglists_buf, u->addrsize); + base = (uintptr_t) read_address (&rnglists_buf, u->addrsize); break; case DW_RLE_start_end: { - uint64_t low; - uint64_t high; + uintptr_t low; + uintptr_t high; - low = read_address (&rnglists_buf, u->addrsize); - high = read_address (&rnglists_buf, u->addrsize); + low = (uintptr_t) read_address (&rnglists_buf, u->addrsize); + high = (uintptr_t) read_address (&rnglists_buf, u->addrsize); if (!add_range (state, rdata, low + base_address, high + base_address, error_callback, data, vec)) @@ -2206,11 +2206,11 @@ add_ranges_from_rnglists ( case DW_RLE_start_length: { - uint64_t low; - uint64_t length; + uintptr_t low; + uintptr_t length; - low = read_address (&rnglists_buf, u->addrsize); - length = read_uleb128 (&rnglists_buf); + low = (uintptr_t) read_address (&rnglists_buf, u->addrsize); + length = (uintptr_t) read_uleb128 (&rnglists_buf); low += base_address; if (!add_range (state, rdata, low, low + length, error_callback, data, vec)) @@ -2240,9 +2240,9 @@ static int add_ranges (struct backtrace_state *state, const struct dwarf_sections *dwarf_sections, uintptr_t base_address, int is_bigendian, - struct unit *u, uint64_t base, const struct pcrange *pcrange, + struct unit *u, uintptr_t base, const struct pcrange *pcrange, int (*add_range) (struct backtrace_state *state, void *rdata, - uint64_t lowpc, uint64_t highpc, + uintptr_t lowpc, uintptr_t highpc, backtrace_error_callback error_callback, void *data, void *vec), void *rdata, @@ -3520,7 +3520,7 @@ read_referenced_name (struct dwarf_data *ddata, struct unit *u, static int add_function_range (struct backtrace_state *state, void *rdata, - uint64_t lowpc, uint64_t highpc, + uintptr_t lowpc, uintptr_t highpc, backtrace_error_callback error_callback, void *data, void *pvec) { @@ -3560,7 +3560,7 @@ add_function_range (struct backtrace_state *state, void *rdata, static int read_function_entry (struct backtrace_state *state, struct dwarf_data *ddata, - struct unit *u, uint64_t base, struct dwarf_buf *unit_buf, + struct unit *u, uintptr_t base, struct dwarf_buf *unit_buf, const struct line_header *lhdr, backtrace_error_callback error_callback, void *data, struct function_vector *vec_function, @@ -3624,7 +3624,7 @@ read_function_entry (struct backtrace_state *state, struct dwarf_data *ddata, && abbrev->attrs[i].name == DW_AT_low_pc) { if (val.encoding == ATTR_VAL_ADDRESS) - base = val.u.uint; + base = (uintptr_t) val.u.uint; else if (val.encoding == ATTR_VAL_ADDRESS_INDEX) { if (!resolve_addr_index (&ddata->dwarf_sections, diff --git a/Source/ThirdParty/tracy/libbacktrace/elf.cpp b/Source/ThirdParty/tracy/libbacktrace/elf.cpp index 9e62f090d..c65bc4e76 100644 --- a/Source/ThirdParty/tracy/libbacktrace/elf.cpp +++ b/Source/ThirdParty/tracy/libbacktrace/elf.cpp @@ -193,6 +193,7 @@ dl_iterate_phdr (int (*callback) (struct dl_phdr_info *, #undef STT_FUNC #undef NT_GNU_BUILD_ID #undef ELFCOMPRESS_ZLIB +#undef ELFCOMPRESS_ZSTD /* Basic types. */ @@ -350,6 +351,7 @@ typedef struct #endif /* BACKTRACE_ELF_SIZE != 32 */ #define ELFCOMPRESS_ZLIB 1 +#define ELFCOMPRESS_ZSTD 2 /* Names of sections, indexed by enum dwarf_section in internal.h. */ @@ -1130,7 +1132,7 @@ elf_uncompress_failed(void) on error. */ static int -elf_zlib_fetch (const unsigned char **ppin, const unsigned char *pinend, +elf_fetch_bits (const unsigned char **ppin, const unsigned char *pinend, uint64_t *pval, unsigned int *pbits) { unsigned int bits; @@ -1177,6 +1179,118 @@ elf_zlib_fetch (const unsigned char **ppin, const unsigned char *pinend, return 1; } +/* This is like elf_fetch_bits, but it fetchs the bits backward, and ensures at + least 16 bits. This is for zstd. */ + +static int +elf_fetch_bits_backward (const unsigned char **ppin, + const unsigned char *pinend, + uint64_t *pval, unsigned int *pbits) +{ + unsigned int bits; + const unsigned char *pin; + uint64_t val; + uint32_t next; + + bits = *pbits; + if (bits >= 16) + return 1; + pin = *ppin; + val = *pval; + + if (unlikely (pin <= pinend)) + { + if (bits == 0) + { + elf_uncompress_failed (); + return 0; + } + return 1; + } + + pin -= 4; + +#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) \ + && defined(__ORDER_BIG_ENDIAN__) \ + && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ \ + || __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + /* We've ensured that PIN is aligned. */ + next = *(const uint32_t *)pin; + +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ + next = __builtin_bswap32 (next); +#endif +#else + next = pin[0] | (pin[1] << 8) | (pin[2] << 16) | (pin[3] << 24); +#endif + + val <<= 32; + val |= next; + bits += 32; + + if (unlikely (pin < pinend)) + { + val >>= (pinend - pin) * 8; + bits -= (pinend - pin) * 8; + } + + *ppin = pin; + *pval = val; + *pbits = bits; + return 1; +} + +/* Initialize backward fetching when the bitstream starts with a 1 bit in the + last byte in memory (which is the first one that we read). This is used by + zstd decompression. Returns 1 on success, 0 on error. */ + +static int +elf_fetch_backward_init (const unsigned char **ppin, + const unsigned char *pinend, + uint64_t *pval, unsigned int *pbits) +{ + const unsigned char *pin; + unsigned int stream_start; + uint64_t val; + unsigned int bits; + + pin = *ppin; + stream_start = (unsigned int)*pin; + if (unlikely (stream_start == 0)) + { + elf_uncompress_failed (); + return 0; + } + val = 0; + bits = 0; + + /* Align to a 32-bit boundary. */ + while ((((uintptr_t)pin) & 3) != 0) + { + val <<= 8; + val |= (uint64_t)*pin; + bits += 8; + --pin; + } + + val <<= 8; + val |= (uint64_t)*pin; + bits += 8; + + *ppin = pin; + *pval = val; + *pbits = bits; + if (!elf_fetch_bits_backward (ppin, pinend, pval, pbits)) + return 0; + + *pbits -= __builtin_clz (stream_start) - (sizeof (unsigned int) - 1) * 8 + 1; + + if (!elf_fetch_bits_backward (ppin, pinend, pval, pbits)) + return 0; + + return 1; +} + /* Huffman code tables, like the rest of the zlib format, are defined by RFC 1951. We store a Huffman code table as a series of tables stored sequentially in memory. Each entry in a table is 16 bits. @@ -1211,14 +1325,14 @@ elf_zlib_fetch (const unsigned char **ppin, const unsigned char *pinend, /* Number of entries we allocate to for one code table. We get a page for the two code tables we need. */ -#define HUFFMAN_TABLE_SIZE (1024) +#define ZLIB_HUFFMAN_TABLE_SIZE (1024) /* Bit masks and shifts for the values in the table. */ -#define HUFFMAN_VALUE_MASK 0x01ff -#define HUFFMAN_BITS_SHIFT 9 -#define HUFFMAN_BITS_MASK 0x7 -#define HUFFMAN_SECONDARY_SHIFT 12 +#define ZLIB_HUFFMAN_VALUE_MASK 0x01ff +#define ZLIB_HUFFMAN_BITS_SHIFT 9 +#define ZLIB_HUFFMAN_BITS_MASK 0x7 +#define ZLIB_HUFFMAN_SECONDARY_SHIFT 12 /* For working memory while inflating we need two code tables, we need an array of code lengths (max value 15, so we use unsigned char), @@ -1226,17 +1340,17 @@ elf_zlib_fetch (const unsigned char **ppin, const unsigned char *pinend, latter two arrays must be large enough to hold the maximum number of code lengths, which RFC 1951 defines as 286 + 30. */ -#define ZDEBUG_TABLE_SIZE \ - (2 * HUFFMAN_TABLE_SIZE * sizeof (uint16_t) \ +#define ZLIB_TABLE_SIZE \ + (2 * ZLIB_HUFFMAN_TABLE_SIZE * sizeof (uint16_t) \ + (286 + 30) * sizeof (uint16_t) \ + (286 + 30) * sizeof (unsigned char)) -#define ZDEBUG_TABLE_CODELEN_OFFSET \ - (2 * HUFFMAN_TABLE_SIZE * sizeof (uint16_t) \ +#define ZLIB_TABLE_CODELEN_OFFSET \ + (2 * ZLIB_HUFFMAN_TABLE_SIZE * sizeof (uint16_t) \ + (286 + 30) * sizeof (uint16_t)) -#define ZDEBUG_TABLE_WORK_OFFSET \ - (2 * HUFFMAN_TABLE_SIZE * sizeof (uint16_t)) +#define ZLIB_TABLE_WORK_OFFSET \ + (2 * ZLIB_HUFFMAN_TABLE_SIZE * sizeof (uint16_t)) #ifdef BACKTRACE_GENERATE_FIXED_HUFFMAN_TABLE @@ -1269,7 +1383,7 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, next value after VAL with the same bit length. */ next = (uint16_t *) (((unsigned char *) zdebug_table) - + ZDEBUG_TABLE_WORK_OFFSET); + + ZLIB_TABLE_WORK_OFFSET); memset (&count[0], 0, 16 * sizeof (uint16_t)); for (i = 0; i < codes_len; ++i) @@ -1297,7 +1411,7 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, /* For each length, fill in the table for the codes of that length. */ - memset (table, 0, HUFFMAN_TABLE_SIZE * sizeof (uint16_t)); + memset (table, 0, ZLIB_HUFFMAN_TABLE_SIZE * sizeof (uint16_t)); /* Handle the values that do not require a secondary table. */ @@ -1331,13 +1445,13 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, /* In the compressed bit stream, the value VAL is encoded as J bits with the value C. */ - if (unlikely ((val & ~HUFFMAN_VALUE_MASK) != 0)) + if (unlikely ((val & ~ZLIB_HUFFMAN_VALUE_MASK) != 0)) { elf_uncompress_failed (); return 0; } - tval = val | ((j - 1) << HUFFMAN_BITS_SHIFT); + tval = val | ((j - 1) << ZLIB_HUFFMAN_BITS_SHIFT); /* The table lookup uses 8 bits. If J is less than 8, we don't know what the other bits will be. We need to fill @@ -1487,7 +1601,7 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, { /* Start a new secondary table. */ - if (unlikely ((next_secondary & HUFFMAN_VALUE_MASK) + if (unlikely ((next_secondary & ZLIB_HUFFMAN_VALUE_MASK) != next_secondary)) { elf_uncompress_failed (); @@ -1498,22 +1612,23 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, secondary_bits = j - 8; next_secondary += 1 << secondary_bits; table[primary] = (secondary - + ((j - 8) << HUFFMAN_BITS_SHIFT) - + (1U << HUFFMAN_SECONDARY_SHIFT)); + + ((j - 8) << ZLIB_HUFFMAN_BITS_SHIFT) + + (1U << ZLIB_HUFFMAN_SECONDARY_SHIFT)); } else { /* There is an existing entry. It had better be a secondary table with enough bits. */ - if (unlikely ((tprimary & (1U << HUFFMAN_SECONDARY_SHIFT)) + if (unlikely ((tprimary + & (1U << ZLIB_HUFFMAN_SECONDARY_SHIFT)) == 0)) { elf_uncompress_failed (); return 0; } - secondary = tprimary & HUFFMAN_VALUE_MASK; - secondary_bits = ((tprimary >> HUFFMAN_BITS_SHIFT) - & HUFFMAN_BITS_MASK); + secondary = tprimary & ZLIB_HUFFMAN_VALUE_MASK; + secondary_bits = ((tprimary >> ZLIB_HUFFMAN_BITS_SHIFT) + & ZLIB_HUFFMAN_BITS_MASK); if (unlikely (secondary_bits < j - 8)) { elf_uncompress_failed (); @@ -1524,7 +1639,7 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, /* Fill in secondary table entries. */ - tval = val | ((j - 8) << HUFFMAN_BITS_SHIFT); + tval = val | ((j - 8) << ZLIB_HUFFMAN_BITS_SHIFT); for (ind = code >> 8; ind < (1U << secondary_bits); @@ -1567,7 +1682,7 @@ elf_zlib_inflate_table (unsigned char *codes, size_t codes_len, #include -static uint16_t table[ZDEBUG_TABLE_SIZE]; +static uint16_t table[ZLIB_TABLE_SIZE]; static unsigned char codes[288]; int @@ -1795,7 +1910,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, const uint16_t *tlit; const uint16_t *tdist; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; last = val & 1; @@ -1883,7 +1998,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, /* Read a Huffman encoding table. The various magic numbers here are from RFC 1951. */ - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; nlit = (val & 0x1f) + 257; @@ -1908,7 +2023,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, /* There are always at least 4 elements in the table. */ - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; codebits[16] = val & 7; @@ -1928,7 +2043,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, if (nclen == 5) goto codebitsdone; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; codebits[7] = val & 7; @@ -1966,7 +2081,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, if (nclen == 10) goto codebitsdone; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; codebits[11] = val & 7; @@ -2004,7 +2119,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, if (nclen == 15) goto codebitsdone; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; codebits[2] = val & 7; @@ -2043,7 +2158,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, at the end of zdebug_table to hold them. */ plenbase = (((unsigned char *) zdebug_table) - + ZDEBUG_TABLE_CODELEN_OFFSET); + + ZLIB_TABLE_CODELEN_OFFSET); plen = plenbase; plenend = plen + nlit + ndist; while (plen < plenend) @@ -2052,24 +2167,25 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, unsigned int b; uint16_t v; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; t = zdebug_table[val & 0xff]; /* The compression here uses bit lengths up to 7, so a secondary table is never necessary. */ - if (unlikely ((t & (1U << HUFFMAN_SECONDARY_SHIFT)) != 0)) + if (unlikely ((t & (1U << ZLIB_HUFFMAN_SECONDARY_SHIFT)) + != 0)) { elf_uncompress_failed (); return 0; } - b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK; + b = (t >> ZLIB_HUFFMAN_BITS_SHIFT) & ZLIB_HUFFMAN_BITS_MASK; val >>= b + 1; bits -= b + 1; - v = t & HUFFMAN_VALUE_MASK; + v = t & ZLIB_HUFFMAN_VALUE_MASK; if (v < 16) *plen++ = v; else if (v == 16) @@ -2086,7 +2202,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, } /* We used up to 7 bits since the last - elf_zlib_fetch, so we have at least 8 bits + elf_fetch_bits, so we have at least 8 bits available here. */ c = 3 + (val & 0x3); @@ -2121,7 +2237,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, /* Store zero 3 to 10 times. */ /* We used up to 7 bits since the last - elf_zlib_fetch, so we have at least 8 bits + elf_fetch_bits, so we have at least 8 bits available here. */ c = 3 + (val & 0x7); @@ -2167,7 +2283,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, /* Store zero 11 to 138 times. */ /* We used up to 7 bits since the last - elf_zlib_fetch, so we have at least 8 bits + elf_fetch_bits, so we have at least 8 bits available here. */ c = 11 + (val & 0x7f); @@ -2204,10 +2320,11 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, zdebug_table)) return 0; if (!elf_zlib_inflate_table (plen + nlit, ndist, zdebug_table, - zdebug_table + HUFFMAN_TABLE_SIZE)) + (zdebug_table + + ZLIB_HUFFMAN_TABLE_SIZE))) return 0; tlit = zdebug_table; - tdist = zdebug_table + HUFFMAN_TABLE_SIZE; + tdist = zdebug_table + ZLIB_HUFFMAN_TABLE_SIZE; } /* Inflate values until the end of the block. This is the @@ -2220,14 +2337,14 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, uint16_t v; unsigned int lit; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; t = tlit[val & 0xff]; - b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK; - v = t & HUFFMAN_VALUE_MASK; + b = (t >> ZLIB_HUFFMAN_BITS_SHIFT) & ZLIB_HUFFMAN_BITS_MASK; + v = t & ZLIB_HUFFMAN_VALUE_MASK; - if ((t & (1U << HUFFMAN_SECONDARY_SHIFT)) == 0) + if ((t & (1U << ZLIB_HUFFMAN_SECONDARY_SHIFT)) == 0) { lit = v; val >>= b + 1; @@ -2236,8 +2353,8 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, else { t = tlit[v + 0x100 + ((val >> 8) & ((1U << b) - 1))]; - b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK; - lit = t & HUFFMAN_VALUE_MASK; + b = (t >> ZLIB_HUFFMAN_BITS_SHIFT) & ZLIB_HUFFMAN_BITS_MASK; + lit = t & ZLIB_HUFFMAN_VALUE_MASK; val >>= b + 8; bits -= b + 8; } @@ -2282,7 +2399,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, { unsigned int extra; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; /* This is an expression for the table of length @@ -2297,14 +2414,14 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, bits -= extra; } - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; t = tdist[val & 0xff]; - b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK; - v = t & HUFFMAN_VALUE_MASK; + b = (t >> ZLIB_HUFFMAN_BITS_SHIFT) & ZLIB_HUFFMAN_BITS_MASK; + v = t & ZLIB_HUFFMAN_VALUE_MASK; - if ((t & (1U << HUFFMAN_SECONDARY_SHIFT)) == 0) + if ((t & (1U << ZLIB_HUFFMAN_SECONDARY_SHIFT)) == 0) { dist = v; val >>= b + 1; @@ -2313,8 +2430,9 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, else { t = tdist[v + 0x100 + ((val >> 8) & ((1U << b) - 1))]; - b = (t >> HUFFMAN_BITS_SHIFT) & HUFFMAN_BITS_MASK; - dist = t & HUFFMAN_VALUE_MASK; + b = ((t >> ZLIB_HUFFMAN_BITS_SHIFT) + & ZLIB_HUFFMAN_BITS_MASK); + dist = t & ZLIB_HUFFMAN_VALUE_MASK; val >>= b + 8; bits -= b + 8; } @@ -2354,7 +2472,7 @@ elf_zlib_inflate (const unsigned char *pin, size_t sin, uint16_t *zdebug_table, { unsigned int extra; - if (!elf_zlib_fetch (&pin, pinend, &val, &bits)) + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) return 0; /* This is an expression for the table of @@ -2559,6 +2677,2354 @@ elf_zlib_inflate_and_verify (const unsigned char *pin, size_t sin, return 1; } +/* For working memory during zstd compression, we need + - a literal length FSE table: 512 64-bit values == 4096 bytes + - a match length FSE table: 512 64-bit values == 4096 bytes + - a offset FSE table: 256 64-bit values == 2048 bytes + - a Huffman tree: 2048 uint16_t values == 4096 bytes + - scratch space, one of + - to build an FSE table: 512 uint16_t values == 1024 bytes + - to build a Huffman tree: 512 uint16_t + 256 uint32_t == 2048 bytes +*/ + +#define ZSTD_TABLE_SIZE \ + (2 * 512 * sizeof (struct elf_zstd_fse_baseline_entry) \ + + 256 * sizeof (struct elf_zstd_fse_baseline_entry) \ + + 2048 * sizeof (uint16_t) \ + + 512 * sizeof (uint16_t) + 256 * sizeof (uint32_t)) + +#define ZSTD_TABLE_LITERAL_FSE_OFFSET (0) + +#define ZSTD_TABLE_MATCH_FSE_OFFSET \ + (512 * sizeof (struct elf_zstd_fse_baseline_entry)) + +#define ZSTD_TABLE_OFFSET_FSE_OFFSET \ + (ZSTD_TABLE_MATCH_FSE_OFFSET \ + + 512 * sizeof (struct elf_zstd_fse_baseline_entry)) + +#define ZSTD_TABLE_HUFFMAN_OFFSET \ + (ZSTD_TABLE_OFFSET_FSE_OFFSET \ + + 256 * sizeof (struct elf_zstd_fse_baseline_entry)) + +#define ZSTD_TABLE_WORK_OFFSET \ + (ZSTD_TABLE_HUFFMAN_OFFSET + 2048 * sizeof (uint16_t)) + +/* An entry in a zstd FSE table. */ + +struct elf_zstd_fse_entry +{ + /* The value that this FSE entry represents. */ + unsigned char symbol; + /* The number of bits to read to determine the next state. */ + unsigned char bits; + /* Add the bits to this base to get the next state. */ + uint16_t base; +}; + +static int +elf_zstd_build_fse (const int16_t *, int, uint16_t *, int, + struct elf_zstd_fse_entry *); + +/* Read a zstd FSE table and build the decoding table in *TABLE, updating *PPIN + as it reads. ZDEBUG_TABLE is scratch space; it must be enough for 512 + uint16_t values (1024 bytes). MAXIDX is the maximum number of symbols + permitted. *TABLE_BITS is the maximum number of bits for symbols in the + table: the size of *TABLE is at least 1 << *TABLE_BITS. This updates + *TABLE_BITS to the actual number of bits. Returns 1 on success, 0 on + error. */ + +static int +elf_zstd_read_fse (const unsigned char **ppin, const unsigned char *pinend, + uint16_t *zdebug_table, int maxidx, + struct elf_zstd_fse_entry *table, int *table_bits) +{ + const unsigned char *pin; + int16_t *norm; + uint16_t *next; + uint64_t val; + unsigned int bits; + int accuracy_log; + uint32_t remaining; + uint32_t threshold; + int bits_needed; + int idx; + int prev0; + + pin = *ppin; + + norm = (int16_t *) zdebug_table; + next = zdebug_table + 256; + + if (unlikely (pin + 3 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + + /* Align PIN to a 32-bit boundary. */ + + val = 0; + bits = 0; + while ((((uintptr_t) pin) & 3) != 0) + { + val |= (uint64_t)*pin << bits; + bits += 8; + ++pin; + } + + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) + return 0; + + accuracy_log = (val & 0xf) + 5; + if (accuracy_log > *table_bits) + { + elf_uncompress_failed (); + return 0; + } + *table_bits = accuracy_log; + val >>= 4; + bits -= 4; + + /* This code is mostly copied from the reference implementation. */ + + /* The number of remaining probabilities, plus 1. This sets the number of + bits that need to be read for the next value. */ + remaining = (1 << accuracy_log) + 1; + + /* The current difference between small and large values, which depends on + the number of remaining values. Small values use one less bit. */ + threshold = 1 << accuracy_log; + + /* The number of bits used to compute threshold. */ + bits_needed = accuracy_log + 1; + + /* The next character value. */ + idx = 0; + + /* Whether the last count was 0. */ + prev0 = 0; + + while (remaining > 1 && idx <= maxidx) + { + uint32_t max; + int32_t count; + + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) + return 0; + + if (prev0) + { + int zidx; + + /* Previous count was 0, so there is a 2-bit repeat flag. If the + 2-bit flag is 0b11, it adds 3 and then there is another repeat + flag. */ + zidx = idx; + while ((val & 0xfff) == 0xfff) + { + zidx += 3 * 6; + val >>= 12; + bits -= 12; + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) + return 0; + } + while ((val & 3) == 3) + { + zidx += 3; + val >>= 2; + bits -= 2; + if (!elf_fetch_bits (&pin, pinend, &val, &bits)) + return 0; + } + /* We have at least 13 bits here, don't need to fetch. */ + zidx += val & 3; + val >>= 2; + bits -= 2; + + if (unlikely (zidx > maxidx)) + { + elf_uncompress_failed (); + return 0; + } + + for (; idx < zidx; idx++) + norm[idx] = 0; + + prev0 = 0; + continue; + } + + max = (2 * threshold - 1) - remaining; + if ((val & (threshold - 1)) < max) + { + /* A small value. */ + count = (int32_t) ((uint32_t) val & (threshold - 1)); + val >>= bits_needed - 1; + bits -= bits_needed - 1; + } + else + { + /* A large value. */ + count = (int32_t) ((uint32_t) val & (2 * threshold - 1)); + if (count >= (int32_t) threshold) + count -= (int32_t) max; + val >>= bits_needed; + bits -= bits_needed; + } + + count--; + if (count >= 0) + remaining -= count; + else + remaining--; + if (unlikely (idx >= 256)) + { + elf_uncompress_failed (); + return 0; + } + norm[idx] = (int16_t) count; + ++idx; + + prev0 = count == 0; + + while (remaining < threshold) + { + bits_needed--; + threshold >>= 1; + } + } + + if (unlikely (remaining != 1)) + { + elf_uncompress_failed (); + return 0; + } + + /* If we've read ahead more than a byte, back up. */ + while (bits >= 8) + { + --pin; + bits -= 8; + } + + *ppin = pin; + + for (; idx <= maxidx; idx++) + norm[idx] = 0; + + return elf_zstd_build_fse (norm, idx, next, *table_bits, table); +} + +/* Build the FSE decoding table from a list of probabilities. This reads from + NORM of length IDX, uses NEXT as scratch space, and writes to *TABLE, whose + size is TABLE_BITS. */ + +static int +elf_zstd_build_fse (const int16_t *norm, int idx, uint16_t *next, + int table_bits, struct elf_zstd_fse_entry *table) +{ + int table_size; + int high_threshold; + int i; + int pos; + int step; + int mask; + + table_size = 1 << table_bits; + high_threshold = table_size - 1; + for (i = 0; i < idx; i++) + { + int16_t n; + + n = norm[i]; + if (n >= 0) + next[i] = (uint16_t) n; + else + { + table[high_threshold].symbol = (unsigned char) i; + high_threshold--; + next[i] = 1; + } + } + + pos = 0; + step = (table_size >> 1) + (table_size >> 3) + 3; + mask = table_size - 1; + for (i = 0; i < idx; i++) + { + int n; + int j; + + n = (int) norm[i]; + for (j = 0; j < n; j++) + { + table[pos].symbol = (unsigned char) i; + pos = (pos + step) & mask; + while (unlikely (pos > high_threshold)) + pos = (pos + step) & mask; + } + } + if (unlikely (pos != 0)) + { + elf_uncompress_failed (); + return 0; + } + + for (i = 0; i < table_size; i++) + { + unsigned char sym; + uint16_t next_state; + int high_bit; + int bits; + + sym = table[i].symbol; + next_state = next[sym]; + ++next[sym]; + + if (next_state == 0) + { + elf_uncompress_failed (); + return 0; + } + high_bit = 31 - __builtin_clz (next_state); + + bits = table_bits - high_bit; + table[i].bits = (unsigned char) bits; + table[i].base = (uint16_t) ((next_state << bits) - table_size); + } + + return 1; +} + +/* Encode the baseline and bits into a single 32-bit value. */ + +#define ZSTD_ENCODE_BASELINE_BITS(baseline, basebits) \ + ((uint32_t)(baseline) | ((uint32_t)(basebits) << 24)) + +#define ZSTD_DECODE_BASELINE(baseline_basebits) \ + ((uint32_t)(baseline_basebits) & 0xffffff) + +#define ZSTD_DECODE_BASEBITS(baseline_basebits) \ + ((uint32_t)(baseline_basebits) >> 24) + +/* Given a literal length code, we need to read a number of bits and add that + to a baseline. For states 0 to 15 the baseline is the state and the number + of bits is zero. */ + +#define ZSTD_LITERAL_LENGTH_BASELINE_OFFSET (16) + +static const uint32_t elf_zstd_literal_length_base[] = +{ + ZSTD_ENCODE_BASELINE_BITS(16, 1), + ZSTD_ENCODE_BASELINE_BITS(18, 1), + ZSTD_ENCODE_BASELINE_BITS(20, 1), + ZSTD_ENCODE_BASELINE_BITS(22, 1), + ZSTD_ENCODE_BASELINE_BITS(24, 2), + ZSTD_ENCODE_BASELINE_BITS(28, 2), + ZSTD_ENCODE_BASELINE_BITS(32, 3), + ZSTD_ENCODE_BASELINE_BITS(40, 3), + ZSTD_ENCODE_BASELINE_BITS(48, 4), + ZSTD_ENCODE_BASELINE_BITS(64, 6), + ZSTD_ENCODE_BASELINE_BITS(128, 7), + ZSTD_ENCODE_BASELINE_BITS(256, 8), + ZSTD_ENCODE_BASELINE_BITS(512, 9), + ZSTD_ENCODE_BASELINE_BITS(1024, 10), + ZSTD_ENCODE_BASELINE_BITS(2048, 11), + ZSTD_ENCODE_BASELINE_BITS(4096, 12), + ZSTD_ENCODE_BASELINE_BITS(8192, 13), + ZSTD_ENCODE_BASELINE_BITS(16384, 14), + ZSTD_ENCODE_BASELINE_BITS(32768, 15), + ZSTD_ENCODE_BASELINE_BITS(65536, 16) +}; + +/* The same applies to match length codes. For states 0 to 31 the baseline is + the state + 3 and the number of bits is zero. */ + +#define ZSTD_MATCH_LENGTH_BASELINE_OFFSET (32) + +static const uint32_t elf_zstd_match_length_base[] = +{ + ZSTD_ENCODE_BASELINE_BITS(35, 1), + ZSTD_ENCODE_BASELINE_BITS(37, 1), + ZSTD_ENCODE_BASELINE_BITS(39, 1), + ZSTD_ENCODE_BASELINE_BITS(41, 1), + ZSTD_ENCODE_BASELINE_BITS(43, 2), + ZSTD_ENCODE_BASELINE_BITS(47, 2), + ZSTD_ENCODE_BASELINE_BITS(51, 3), + ZSTD_ENCODE_BASELINE_BITS(59, 3), + ZSTD_ENCODE_BASELINE_BITS(67, 4), + ZSTD_ENCODE_BASELINE_BITS(83, 4), + ZSTD_ENCODE_BASELINE_BITS(99, 5), + ZSTD_ENCODE_BASELINE_BITS(131, 7), + ZSTD_ENCODE_BASELINE_BITS(259, 8), + ZSTD_ENCODE_BASELINE_BITS(515, 9), + ZSTD_ENCODE_BASELINE_BITS(1027, 10), + ZSTD_ENCODE_BASELINE_BITS(2051, 11), + ZSTD_ENCODE_BASELINE_BITS(4099, 12), + ZSTD_ENCODE_BASELINE_BITS(8195, 13), + ZSTD_ENCODE_BASELINE_BITS(16387, 14), + ZSTD_ENCODE_BASELINE_BITS(32771, 15), + ZSTD_ENCODE_BASELINE_BITS(65539, 16) +}; + +/* An entry in an FSE table used for literal/match/length values. For these we + have to map the symbol to a baseline value, and we have to read zero or more + bits and add that value to the baseline value. Rather than look the values + up in a separate table, we grow the FSE table so that we get better memory + caching. */ + +struct elf_zstd_fse_baseline_entry +{ + /* The baseline for the value that this FSE entry represents.. */ + uint32_t baseline; + /* The number of bits to read to add to the baseline. */ + unsigned char basebits; + /* The number of bits to read to determine the next state. */ + unsigned char bits; + /* Add the bits to this base to get the next state. */ + uint16_t base; +}; + +/* Convert the literal length FSE table FSE_TABLE to an FSE baseline table at + BASELINE_TABLE. Note that FSE_TABLE and BASELINE_TABLE will overlap. */ + +static int +elf_zstd_make_literal_baseline_fse ( + const struct elf_zstd_fse_entry *fse_table, + int table_bits, + struct elf_zstd_fse_baseline_entry *baseline_table) +{ + size_t count; + const struct elf_zstd_fse_entry *pfse; + struct elf_zstd_fse_baseline_entry *pbaseline; + + /* Convert backward to avoid overlap. */ + + count = 1U << table_bits; + pfse = fse_table + count; + pbaseline = baseline_table + count; + while (pfse > fse_table) + { + unsigned char symbol; + unsigned char bits; + uint16_t base; + + --pfse; + --pbaseline; + symbol = pfse->symbol; + bits = pfse->bits; + base = pfse->base; + if (symbol < ZSTD_LITERAL_LENGTH_BASELINE_OFFSET) + { + pbaseline->baseline = (uint32_t)symbol; + pbaseline->basebits = 0; + } + else + { + unsigned int idx; + uint32_t basebits; + + if (unlikely (symbol > 35)) + { + elf_uncompress_failed (); + return 0; + } + idx = symbol - ZSTD_LITERAL_LENGTH_BASELINE_OFFSET; + basebits = elf_zstd_literal_length_base[idx]; + pbaseline->baseline = ZSTD_DECODE_BASELINE(basebits); + pbaseline->basebits = ZSTD_DECODE_BASEBITS(basebits); + } + pbaseline->bits = bits; + pbaseline->base = base; + } + + return 1; +} + +/* Convert the offset length FSE table FSE_TABLE to an FSE baseline table at + BASELINE_TABLE. Note that FSE_TABLE and BASELINE_TABLE will overlap. */ + +static int +elf_zstd_make_offset_baseline_fse ( + const struct elf_zstd_fse_entry *fse_table, + int table_bits, + struct elf_zstd_fse_baseline_entry *baseline_table) +{ + size_t count; + const struct elf_zstd_fse_entry *pfse; + struct elf_zstd_fse_baseline_entry *pbaseline; + + /* Convert backward to avoid overlap. */ + + count = 1U << table_bits; + pfse = fse_table + count; + pbaseline = baseline_table + count; + while (pfse > fse_table) + { + unsigned char symbol; + unsigned char bits; + uint16_t base; + + --pfse; + --pbaseline; + symbol = pfse->symbol; + bits = pfse->bits; + base = pfse->base; + if (unlikely (symbol > 31)) + { + elf_uncompress_failed (); + return 0; + } + + /* The simple way to write this is + + pbaseline->baseline = (uint32_t)1 << symbol; + pbaseline->basebits = symbol; + + That will give us an offset value that corresponds to the one + described in the RFC. However, for offset values > 3, we have to + subtract 3. And for offset values 1, 2, 3 we use a repeated offset. + The baseline is always a power of 2, and is never 0, so for these low + values we will see one entry that is baseline 1, basebits 0, and one + entry that is baseline 2, basebits 1. All other entries will have + baseline >= 4 and basebits >= 2. + + So we can check for RFC offset <= 3 by checking for basebits <= 1. + And that means that we can subtract 3 here and not worry about doing + it in the hot loop. */ + + pbaseline->baseline = (uint32_t)1 << symbol; + if (symbol >= 2) + pbaseline->baseline -= 3; + pbaseline->basebits = symbol; + pbaseline->bits = bits; + pbaseline->base = base; + } + + return 1; +} + +/* Convert the match length FSE table FSE_TABLE to an FSE baseline table at + BASELINE_TABLE. Note that FSE_TABLE and BASELINE_TABLE will overlap. */ + +static int +elf_zstd_make_match_baseline_fse ( + const struct elf_zstd_fse_entry *fse_table, + int table_bits, + struct elf_zstd_fse_baseline_entry *baseline_table) +{ + size_t count; + const struct elf_zstd_fse_entry *pfse; + struct elf_zstd_fse_baseline_entry *pbaseline; + + /* Convert backward to avoid overlap. */ + + count = 1U << table_bits; + pfse = fse_table + count; + pbaseline = baseline_table + count; + while (pfse > fse_table) + { + unsigned char symbol; + unsigned char bits; + uint16_t base; + + --pfse; + --pbaseline; + symbol = pfse->symbol; + bits = pfse->bits; + base = pfse->base; + if (symbol < ZSTD_MATCH_LENGTH_BASELINE_OFFSET) + { + pbaseline->baseline = (uint32_t)symbol + 3; + pbaseline->basebits = 0; + } + else + { + unsigned int idx; + uint32_t basebits; + + if (unlikely (symbol > 52)) + { + elf_uncompress_failed (); + return 0; + } + idx = symbol - ZSTD_MATCH_LENGTH_BASELINE_OFFSET; + basebits = elf_zstd_match_length_base[idx]; + pbaseline->baseline = ZSTD_DECODE_BASELINE(basebits); + pbaseline->basebits = ZSTD_DECODE_BASEBITS(basebits); + } + pbaseline->bits = bits; + pbaseline->base = base; + } + + return 1; +} + +#ifdef BACKTRACE_GENERATE_ZSTD_FSE_TABLES + +/* Used to generate the predefined FSE decoding tables for zstd. */ + +#include + +/* These values are straight from RFC 8878. */ + +static int16_t lit[36] = +{ + 4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2, 1, 1, 1, 1, 1, + -1,-1,-1,-1 +}; + +static int16_t match[53] = +{ + 1, 4, 3, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,-1,-1, + -1,-1,-1,-1,-1 +}; + +static int16_t offset[29] = +{ + 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1,-1,-1,-1,-1,-1 +}; + +static uint16_t next[256]; + +static void +print_table (const struct elf_zstd_fse_baseline_entry *table, size_t size) +{ + size_t i; + + printf ("{\n"); + for (i = 0; i < size; i += 3) + { + int j; + + printf (" "); + for (j = 0; j < 3 && i + j < size; ++j) + printf (" { %u, %d, %d, %d },", table[i + j].baseline, + table[i + j].basebits, table[i + j].bits, + table[i + j].base); + printf ("\n"); + } + printf ("};\n"); +} + +int +main () +{ + struct elf_zstd_fse_entry lit_table[64]; + struct elf_zstd_fse_baseline_entry lit_baseline[64]; + struct elf_zstd_fse_entry match_table[64]; + struct elf_zstd_fse_baseline_entry match_baseline[64]; + struct elf_zstd_fse_entry offset_table[32]; + struct elf_zstd_fse_baseline_entry offset_baseline[32]; + + if (!elf_zstd_build_fse (lit, sizeof lit / sizeof lit[0], next, + 6, lit_table)) + { + fprintf (stderr, "elf_zstd_build_fse failed\n"); + exit (EXIT_FAILURE); + } + + if (!elf_zstd_make_literal_baseline_fse (lit_table, 6, lit_baseline)) + { + fprintf (stderr, "elf_zstd_make_literal_baseline_fse failed\n"); + exit (EXIT_FAILURE); + } + + printf ("static const struct elf_zstd_fse_baseline_entry " + "elf_zstd_lit_table[64] =\n"); + print_table (lit_baseline, + sizeof lit_baseline / sizeof lit_baseline[0]); + printf ("\n"); + + if (!elf_zstd_build_fse (match, sizeof match / sizeof match[0], next, + 6, match_table)) + { + fprintf (stderr, "elf_zstd_build_fse failed\n"); + exit (EXIT_FAILURE); + } + + if (!elf_zstd_make_match_baseline_fse (match_table, 6, match_baseline)) + { + fprintf (stderr, "elf_zstd_make_match_baseline_fse failed\n"); + exit (EXIT_FAILURE); + } + + printf ("static const struct elf_zstd_fse_baseline_entry " + "elf_zstd_match_table[64] =\n"); + print_table (match_baseline, + sizeof match_baseline / sizeof match_baseline[0]); + printf ("\n"); + + if (!elf_zstd_build_fse (offset, sizeof offset / sizeof offset[0], next, + 5, offset_table)) + { + fprintf (stderr, "elf_zstd_build_fse failed\n"); + exit (EXIT_FAILURE); + } + + if (!elf_zstd_make_offset_baseline_fse (offset_table, 5, offset_baseline)) + { + fprintf (stderr, "elf_zstd_make_offset_baseline_fse failed\n"); + exit (EXIT_FAILURE); + } + + printf ("static const struct elf_zstd_fse_baseline_entry " + "elf_zstd_offset_table[32] =\n"); + print_table (offset_baseline, + sizeof offset_baseline / sizeof offset_baseline[0]); + printf ("\n"); + + return 0; +} + +#endif + +/* The fixed tables generated by the #ifdef'ed out main function + above. */ + +static const struct elf_zstd_fse_baseline_entry elf_zstd_lit_table[64] = +{ + { 0, 0, 4, 0 }, { 0, 0, 4, 16 }, { 1, 0, 5, 32 }, + { 3, 0, 5, 0 }, { 4, 0, 5, 0 }, { 6, 0, 5, 0 }, + { 7, 0, 5, 0 }, { 9, 0, 5, 0 }, { 10, 0, 5, 0 }, + { 12, 0, 5, 0 }, { 14, 0, 6, 0 }, { 16, 1, 5, 0 }, + { 20, 1, 5, 0 }, { 22, 1, 5, 0 }, { 28, 2, 5, 0 }, + { 32, 3, 5, 0 }, { 48, 4, 5, 0 }, { 64, 6, 5, 32 }, + { 128, 7, 5, 0 }, { 256, 8, 6, 0 }, { 1024, 10, 6, 0 }, + { 4096, 12, 6, 0 }, { 0, 0, 4, 32 }, { 1, 0, 4, 0 }, + { 2, 0, 5, 0 }, { 4, 0, 5, 32 }, { 5, 0, 5, 0 }, + { 7, 0, 5, 32 }, { 8, 0, 5, 0 }, { 10, 0, 5, 32 }, + { 11, 0, 5, 0 }, { 13, 0, 6, 0 }, { 16, 1, 5, 32 }, + { 18, 1, 5, 0 }, { 22, 1, 5, 32 }, { 24, 2, 5, 0 }, + { 32, 3, 5, 32 }, { 40, 3, 5, 0 }, { 64, 6, 4, 0 }, + { 64, 6, 4, 16 }, { 128, 7, 5, 32 }, { 512, 9, 6, 0 }, + { 2048, 11, 6, 0 }, { 0, 0, 4, 48 }, { 1, 0, 4, 16 }, + { 2, 0, 5, 32 }, { 3, 0, 5, 32 }, { 5, 0, 5, 32 }, + { 6, 0, 5, 32 }, { 8, 0, 5, 32 }, { 9, 0, 5, 32 }, + { 11, 0, 5, 32 }, { 12, 0, 5, 32 }, { 15, 0, 6, 0 }, + { 18, 1, 5, 32 }, { 20, 1, 5, 32 }, { 24, 2, 5, 32 }, + { 28, 2, 5, 32 }, { 40, 3, 5, 32 }, { 48, 4, 5, 32 }, + { 65536, 16, 6, 0 }, { 32768, 15, 6, 0 }, { 16384, 14, 6, 0 }, + { 8192, 13, 6, 0 }, +}; + +static const struct elf_zstd_fse_baseline_entry elf_zstd_match_table[64] = +{ + { 3, 0, 6, 0 }, { 4, 0, 4, 0 }, { 5, 0, 5, 32 }, + { 6, 0, 5, 0 }, { 8, 0, 5, 0 }, { 9, 0, 5, 0 }, + { 11, 0, 5, 0 }, { 13, 0, 6, 0 }, { 16, 0, 6, 0 }, + { 19, 0, 6, 0 }, { 22, 0, 6, 0 }, { 25, 0, 6, 0 }, + { 28, 0, 6, 0 }, { 31, 0, 6, 0 }, { 34, 0, 6, 0 }, + { 37, 1, 6, 0 }, { 41, 1, 6, 0 }, { 47, 2, 6, 0 }, + { 59, 3, 6, 0 }, { 83, 4, 6, 0 }, { 131, 7, 6, 0 }, + { 515, 9, 6, 0 }, { 4, 0, 4, 16 }, { 5, 0, 4, 0 }, + { 6, 0, 5, 32 }, { 7, 0, 5, 0 }, { 9, 0, 5, 32 }, + { 10, 0, 5, 0 }, { 12, 0, 6, 0 }, { 15, 0, 6, 0 }, + { 18, 0, 6, 0 }, { 21, 0, 6, 0 }, { 24, 0, 6, 0 }, + { 27, 0, 6, 0 }, { 30, 0, 6, 0 }, { 33, 0, 6, 0 }, + { 35, 1, 6, 0 }, { 39, 1, 6, 0 }, { 43, 2, 6, 0 }, + { 51, 3, 6, 0 }, { 67, 4, 6, 0 }, { 99, 5, 6, 0 }, + { 259, 8, 6, 0 }, { 4, 0, 4, 32 }, { 4, 0, 4, 48 }, + { 5, 0, 4, 16 }, { 7, 0, 5, 32 }, { 8, 0, 5, 32 }, + { 10, 0, 5, 32 }, { 11, 0, 5, 32 }, { 14, 0, 6, 0 }, + { 17, 0, 6, 0 }, { 20, 0, 6, 0 }, { 23, 0, 6, 0 }, + { 26, 0, 6, 0 }, { 29, 0, 6, 0 }, { 32, 0, 6, 0 }, + { 65539, 16, 6, 0 }, { 32771, 15, 6, 0 }, { 16387, 14, 6, 0 }, + { 8195, 13, 6, 0 }, { 4099, 12, 6, 0 }, { 2051, 11, 6, 0 }, + { 1027, 10, 6, 0 }, +}; + +static const struct elf_zstd_fse_baseline_entry elf_zstd_offset_table[32] = +{ + { 1, 0, 5, 0 }, { 61, 6, 4, 0 }, { 509, 9, 5, 0 }, + { 32765, 15, 5, 0 }, { 2097149, 21, 5, 0 }, { 5, 3, 5, 0 }, + { 125, 7, 4, 0 }, { 4093, 12, 5, 0 }, { 262141, 18, 5, 0 }, + { 8388605, 23, 5, 0 }, { 29, 5, 5, 0 }, { 253, 8, 4, 0 }, + { 16381, 14, 5, 0 }, { 1048573, 20, 5, 0 }, { 1, 2, 5, 0 }, + { 125, 7, 4, 16 }, { 2045, 11, 5, 0 }, { 131069, 17, 5, 0 }, + { 4194301, 22, 5, 0 }, { 13, 4, 5, 0 }, { 253, 8, 4, 16 }, + { 8189, 13, 5, 0 }, { 524285, 19, 5, 0 }, { 2, 1, 5, 0 }, + { 61, 6, 4, 16 }, { 1021, 10, 5, 0 }, { 65533, 16, 5, 0 }, + { 268435453, 28, 5, 0 }, { 134217725, 27, 5, 0 }, { 67108861, 26, 5, 0 }, + { 33554429, 25, 5, 0 }, { 16777213, 24, 5, 0 }, +}; + +/* Read a zstd Huffman table and build the decoding table in *TABLE, reading + and updating *PPIN. This sets *PTABLE_BITS to the number of bits of the + table, such that the table length is 1 << *TABLE_BITS. ZDEBUG_TABLE is + scratch space; it must be enough for 512 uint16_t values + 256 32-bit values + (2048 bytes). Returns 1 on success, 0 on error. */ + +static int +elf_zstd_read_huff (const unsigned char **ppin, const unsigned char *pinend, + uint16_t *zdebug_table, uint16_t *table, int *ptable_bits) +{ + const unsigned char *pin; + unsigned char hdr; + unsigned char *weights; + size_t count; + uint32_t *weight_mark; + size_t i; + uint32_t weight_mask; + size_t table_bits; + + pin = *ppin; + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + hdr = *pin; + ++pin; + + weights = (unsigned char *) zdebug_table; + + if (hdr < 128) + { + /* Table is compressed using FSE. */ + + struct elf_zstd_fse_entry *fse_table; + int fse_table_bits; + uint16_t *scratch; + const unsigned char *pfse; + const unsigned char *pback; + uint64_t val; + unsigned int bits; + unsigned int state1, state2; + + /* SCRATCH is used temporarily by elf_zstd_read_fse. It overlaps + WEIGHTS. */ + scratch = zdebug_table; + fse_table = (struct elf_zstd_fse_entry *) (scratch + 512); + fse_table_bits = 6; + + pfse = pin; + if (!elf_zstd_read_fse (&pfse, pinend, scratch, 255, fse_table, + &fse_table_bits)) + return 0; + + if (unlikely (pin + hdr > pinend)) + { + elf_uncompress_failed (); + return 0; + } + + /* We no longer need SCRATCH. Start recording weights. We need up to + 256 bytes of weights and 64 bytes of rank counts, so it won't overlap + FSE_TABLE. */ + + pback = pin + hdr - 1; + + if (!elf_fetch_backward_init (&pback, pfse, &val, &bits)) + return 0; + + bits -= fse_table_bits; + state1 = (val >> bits) & ((1U << fse_table_bits) - 1); + bits -= fse_table_bits; + state2 = (val >> bits) & ((1U << fse_table_bits) - 1); + + /* There are two independent FSE streams, tracked by STATE1 and STATE2. + We decode them alternately. */ + + count = 0; + while (1) + { + struct elf_zstd_fse_entry *pt; + uint64_t v; + + pt = &fse_table[state1]; + + if (unlikely (pin < pinend) && bits < pt->bits) + { + if (unlikely (count >= 254)) + { + elf_uncompress_failed (); + return 0; + } + weights[count] = (unsigned char) pt->symbol; + weights[count + 1] = (unsigned char) fse_table[state2].symbol; + count += 2; + break; + } + + if (unlikely (pt->bits == 0)) + v = 0; + else + { + if (!elf_fetch_bits_backward (&pback, pfse, &val, &bits)) + return 0; + + bits -= pt->bits; + v = (val >> bits) & (((uint64_t)1 << pt->bits) - 1); + } + + state1 = pt->base + v; + + if (unlikely (count >= 255)) + { + elf_uncompress_failed (); + return 0; + } + + weights[count] = pt->symbol; + ++count; + + pt = &fse_table[state2]; + + if (unlikely (pin < pinend && bits < pt->bits)) + { + if (unlikely (count >= 254)) + { + elf_uncompress_failed (); + return 0; + } + weights[count] = (unsigned char) pt->symbol; + weights[count + 1] = (unsigned char) fse_table[state1].symbol; + count += 2; + break; + } + + if (unlikely (pt->bits == 0)) + v = 0; + else + { + if (!elf_fetch_bits_backward (&pback, pfse, &val, &bits)) + return 0; + + bits -= pt->bits; + v = (val >> bits) & (((uint64_t)1 << pt->bits) - 1); + } + + state2 = pt->base + v; + + if (unlikely (count >= 255)) + { + elf_uncompress_failed (); + return 0; + } + + weights[count] = pt->symbol; + ++count; + } + + pin += hdr; + } + else + { + /* Table is not compressed. Each weight is 4 bits. */ + + count = hdr - 127; + if (unlikely (pin + ((count + 1) / 2) >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + for (i = 0; i < count; i += 2) + { + unsigned char b; + + b = *pin; + ++pin; + weights[i] = b >> 4; + weights[i + 1] = b & 0xf; + } + } + + weight_mark = (uint32_t *) (weights + 256); + memset (weight_mark, 0, 13 * sizeof (uint32_t)); + weight_mask = 0; + for (i = 0; i < count; ++i) + { + unsigned char w; + + w = weights[i]; + if (unlikely (w > 12)) + { + elf_uncompress_failed (); + return 0; + } + ++weight_mark[w]; + if (w > 0) + weight_mask += 1U << (w - 1); + } + if (unlikely (weight_mask == 0)) + { + elf_uncompress_failed (); + return 0; + } + + table_bits = 32 - __builtin_clz (weight_mask); + if (unlikely (table_bits > 11)) + { + elf_uncompress_failed (); + return 0; + } + + /* Work out the last weight value, which is omitted because the weights must + sum to a power of two. */ + { + uint32_t left; + uint32_t high_bit; + + left = ((uint32_t)1 << table_bits) - weight_mask; + if (left == 0) + { + elf_uncompress_failed (); + return 0; + } + high_bit = 31 - __builtin_clz (left); + if (((uint32_t)1 << high_bit) != left) + { + elf_uncompress_failed (); + return 0; + } + + if (unlikely (count >= 256)) + { + elf_uncompress_failed (); + return 0; + } + + weights[count] = high_bit + 1; + ++count; + ++weight_mark[high_bit + 1]; + } + + if (weight_mark[1] < 2 || (weight_mark[1] & 1) != 0) + { + elf_uncompress_failed (); + return 0; + } + + /* Change WEIGHT_MARK from a count of weights to the index of the first + symbol for that weight. We shift the indexes to also store how many we + have seen so far, below. */ + { + uint32_t next; + + next = 0; + for (i = 0; i < table_bits; ++i) + { + uint32_t cur; + + cur = next; + next += weight_mark[i + 1] << i; + weight_mark[i + 1] = cur; + } + } + + for (i = 0; i < count; ++i) + { + unsigned char weight; + uint32_t length; + uint16_t tval; + size_t start; + uint32_t j; + + weight = weights[i]; + if (weight == 0) + continue; + + length = 1U << (weight - 1); + tval = (i << 8) | (table_bits + 1 - weight); + start = weight_mark[weight]; + for (j = 0; j < length; ++j) + table[start + j] = tval; + weight_mark[weight] += length; + } + + *ppin = pin; + *ptable_bits = (int)table_bits; + + return 1; +} + +/* Read and decompress the literals and store them ending at POUTEND. This + works because we are going to use all the literals in the output, so they + must fit into the output buffer. HUFFMAN_TABLE, and PHUFFMAN_TABLE_BITS + store the Huffman table across calls. SCRATCH is used to read a Huffman + table. Store the start of the decompressed literals in *PPLIT. Update + *PPIN. Return 1 on success, 0 on error. */ + +static int +elf_zstd_read_literals (const unsigned char **ppin, + const unsigned char *pinend, + unsigned char *pout, + unsigned char *poutend, + uint16_t *scratch, + uint16_t *huffman_table, + int *phuffman_table_bits, + unsigned char **pplit) +{ + const unsigned char *pin; + unsigned char *plit; + unsigned char hdr; + uint32_t regenerated_size; + uint32_t compressed_size; + int streams; + uint32_t total_streams_size; + unsigned int huffman_table_bits; + uint64_t huffman_mask; + + pin = *ppin; + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + hdr = *pin; + ++pin; + + if ((hdr & 3) == 0 || (hdr & 3) == 1) + { + int raw; + + /* Raw_Literals_Block or RLE_Literals_Block */ + + raw = (hdr & 3) == 0; + + switch ((hdr >> 2) & 3) + { + case 0: case 2: + regenerated_size = hdr >> 3; + break; + case 1: + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + regenerated_size = (hdr >> 4) + ((uint32_t)(*pin) << 4); + ++pin; + break; + case 3: + if (unlikely (pin + 1 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + regenerated_size = ((hdr >> 4) + + ((uint32_t)*pin << 4) + + ((uint32_t)pin[1] << 12)); + pin += 2; + break; + default: + elf_uncompress_failed (); + return 0; + } + + if (unlikely ((size_t)(poutend - pout) < regenerated_size)) + { + elf_uncompress_failed (); + return 0; + } + + plit = poutend - regenerated_size; + + if (raw) + { + if (unlikely (pin + regenerated_size >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + memcpy (plit, pin, regenerated_size); + pin += regenerated_size; + } + else + { + if (pin >= pinend) + { + elf_uncompress_failed (); + return 0; + } + memset (plit, *pin, regenerated_size); + ++pin; + } + + *ppin = pin; + *pplit = plit; + + return 1; + } + + /* Compressed_Literals_Block or Treeless_Literals_Block */ + + switch ((hdr >> 2) & 3) + { + case 0: case 1: + if (unlikely (pin + 1 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + regenerated_size = (hdr >> 4) | ((uint32_t)(*pin & 0x3f) << 4); + compressed_size = (uint32_t)*pin >> 6 | ((uint32_t)pin[1] << 2); + pin += 2; + streams = ((hdr >> 2) & 3) == 0 ? 1 : 4; + break; + case 2: + if (unlikely (pin + 2 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + regenerated_size = (((uint32_t)hdr >> 4) + | ((uint32_t)*pin << 4) + | (((uint32_t)pin[1] & 3) << 12)); + compressed_size = (((uint32_t)pin[1] >> 2) + | ((uint32_t)pin[2] << 6)); + pin += 3; + streams = 4; + break; + case 3: + if (unlikely (pin + 3 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + regenerated_size = (((uint32_t)hdr >> 4) + | ((uint32_t)*pin << 4) + | (((uint32_t)pin[1] & 0x3f) << 12)); + compressed_size = (((uint32_t)pin[1] >> 6) + | ((uint32_t)pin[2] << 2) + | ((uint32_t)pin[3] << 10)); + pin += 4; + streams = 4; + break; + default: + elf_uncompress_failed (); + return 0; + } + + if (unlikely (pin + compressed_size > pinend)) + { + elf_uncompress_failed (); + return 0; + } + + pinend = pin + compressed_size; + *ppin = pinend; + + if (unlikely ((size_t)(poutend - pout) < regenerated_size)) + { + elf_uncompress_failed (); + return 0; + } + + plit = poutend - regenerated_size; + + *pplit = plit; + + total_streams_size = compressed_size; + if ((hdr & 3) == 2) + { + const unsigned char *ptable; + + /* Compressed_Literals_Block. Read Huffman tree. */ + + ptable = pin; + if (!elf_zstd_read_huff (&ptable, pinend, scratch, huffman_table, + phuffman_table_bits)) + return 0; + + if (unlikely (total_streams_size < (size_t)(ptable - pin))) + { + elf_uncompress_failed (); + return 0; + } + + total_streams_size -= ptable - pin; + pin = ptable; + } + else + { + /* Treeless_Literals_Block. Reuse previous Huffman tree. */ + if (unlikely (*phuffman_table_bits == 0)) + { + elf_uncompress_failed (); + return 0; + } + } + + /* Decompress COMPRESSED_SIZE bytes of data at PIN using the huffman table, + storing REGENERATED_SIZE bytes of decompressed data at PLIT. */ + + huffman_table_bits = (unsigned int)*phuffman_table_bits; + huffman_mask = ((uint64_t)1 << huffman_table_bits) - 1; + + if (streams == 1) + { + const unsigned char *pback; + const unsigned char *pbackend; + uint64_t val; + unsigned int bits; + uint32_t i; + + pback = pin + total_streams_size - 1; + pbackend = pin; + if (!elf_fetch_backward_init (&pback, pbackend, &val, &bits)) + return 0; + + /* This is one of the inner loops of the decompression algorithm, so we + put some effort into optimization. We can't get more than 64 bytes + from a single call to elf_fetch_bits_backward, and we can't subtract + more than 11 bits at a time. */ + + if (regenerated_size >= 64) + { + unsigned char *plitstart; + unsigned char *plitstop; + + plitstart = plit; + plitstop = plit + regenerated_size - 64; + while (plit < plitstop) + { + uint16_t t; + + if (!elf_fetch_bits_backward (&pback, pbackend, &val, &bits)) + return 0; + + if (bits < 16) + break; + + while (bits >= 33) + { + t = huffman_table[(val >> (bits - huffman_table_bits)) + & huffman_mask]; + *plit = t >> 8; + ++plit; + bits -= t & 0xff; + + t = huffman_table[(val >> (bits - huffman_table_bits)) + & huffman_mask]; + *plit = t >> 8; + ++plit; + bits -= t & 0xff; + + t = huffman_table[(val >> (bits - huffman_table_bits)) + & huffman_mask]; + *plit = t >> 8; + ++plit; + bits -= t & 0xff; + } + + while (bits > 11) + { + t = huffman_table[(val >> (bits - huffman_table_bits)) + & huffman_mask]; + *plit = t >> 8; + ++plit; + bits -= t & 0xff; + } + } + + regenerated_size -= plit - plitstart; + } + + for (i = 0; i < regenerated_size; ++i) + { + uint16_t t; + + if (!elf_fetch_bits_backward (&pback, pbackend, &val, &bits)) + return 0; + + if (unlikely (bits < huffman_table_bits)) + { + t = huffman_table[(val << (huffman_table_bits - bits)) + & huffman_mask]; + if (unlikely (bits < (t & 0xff))) + { + elf_uncompress_failed (); + return 0; + } + } + else + t = huffman_table[(val >> (bits - huffman_table_bits)) + & huffman_mask]; + + *plit = t >> 8; + ++plit; + bits -= t & 0xff; + } + + return 1; + } + + { + uint32_t stream_size1, stream_size2, stream_size3, stream_size4; + uint32_t tot; + const unsigned char *pback1, *pback2, *pback3, *pback4; + const unsigned char *pbackend1, *pbackend2, *pbackend3, *pbackend4; + uint64_t val1, val2, val3, val4; + unsigned int bits1, bits2, bits3, bits4; + unsigned char *plit1, *plit2, *plit3, *plit4; + uint32_t regenerated_stream_size; + uint32_t regenerated_stream_size4; + uint16_t t1, t2, t3, t4; + uint32_t i; + uint32_t limit; + + /* Read jump table. */ + if (unlikely (pin + 5 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + stream_size1 = (uint32_t)*pin | ((uint32_t)pin[1] << 8); + pin += 2; + stream_size2 = (uint32_t)*pin | ((uint32_t)pin[1] << 8); + pin += 2; + stream_size3 = (uint32_t)*pin | ((uint32_t)pin[1] << 8); + pin += 2; + tot = stream_size1 + stream_size2 + stream_size3; + if (unlikely (tot > total_streams_size - 6)) + { + elf_uncompress_failed (); + return 0; + } + stream_size4 = total_streams_size - 6 - tot; + + pback1 = pin + stream_size1 - 1; + pbackend1 = pin; + + pback2 = pback1 + stream_size2; + pbackend2 = pback1 + 1; + + pback3 = pback2 + stream_size3; + pbackend3 = pback2 + 1; + + pback4 = pback3 + stream_size4; + pbackend4 = pback3 + 1; + + if (!elf_fetch_backward_init (&pback1, pbackend1, &val1, &bits1)) + return 0; + if (!elf_fetch_backward_init (&pback2, pbackend2, &val2, &bits2)) + return 0; + if (!elf_fetch_backward_init (&pback3, pbackend3, &val3, &bits3)) + return 0; + if (!elf_fetch_backward_init (&pback4, pbackend4, &val4, &bits4)) + return 0; + + regenerated_stream_size = (regenerated_size + 3) / 4; + + plit1 = plit; + plit2 = plit1 + regenerated_stream_size; + plit3 = plit2 + regenerated_stream_size; + plit4 = plit3 + regenerated_stream_size; + + regenerated_stream_size4 = regenerated_size - regenerated_stream_size * 3; + + /* We can't get more than 64 literal bytes from a single call to + elf_fetch_bits_backward. The fourth stream can be up to 3 bytes less, + so use as the limit. */ + + limit = regenerated_stream_size4 <= 64 ? 0 : regenerated_stream_size4 - 64; + i = 0; + while (i < limit) + { + if (!elf_fetch_bits_backward (&pback1, pbackend1, &val1, &bits1)) + return 0; + if (!elf_fetch_bits_backward (&pback2, pbackend2, &val2, &bits2)) + return 0; + if (!elf_fetch_bits_backward (&pback3, pbackend3, &val3, &bits3)) + return 0; + if (!elf_fetch_bits_backward (&pback4, pbackend4, &val4, &bits4)) + return 0; + + /* We can't subtract more than 11 bits at a time. */ + + do + { + t1 = huffman_table[(val1 >> (bits1 - huffman_table_bits)) + & huffman_mask]; + t2 = huffman_table[(val2 >> (bits2 - huffman_table_bits)) + & huffman_mask]; + t3 = huffman_table[(val3 >> (bits3 - huffman_table_bits)) + & huffman_mask]; + t4 = huffman_table[(val4 >> (bits4 - huffman_table_bits)) + & huffman_mask]; + + *plit1 = t1 >> 8; + ++plit1; + bits1 -= t1 & 0xff; + + *plit2 = t2 >> 8; + ++plit2; + bits2 -= t2 & 0xff; + + *plit3 = t3 >> 8; + ++plit3; + bits3 -= t3 & 0xff; + + *plit4 = t4 >> 8; + ++plit4; + bits4 -= t4 & 0xff; + + ++i; + } + while (bits1 > 11 && bits2 > 11 && bits3 > 11 && bits4 > 11); + } + + while (i < regenerated_stream_size) + { + int use4; + + use4 = i < regenerated_stream_size4; + + if (!elf_fetch_bits_backward (&pback1, pbackend1, &val1, &bits1)) + return 0; + if (!elf_fetch_bits_backward (&pback2, pbackend2, &val2, &bits2)) + return 0; + if (!elf_fetch_bits_backward (&pback3, pbackend3, &val3, &bits3)) + return 0; + if (use4) + { + if (!elf_fetch_bits_backward (&pback4, pbackend4, &val4, &bits4)) + return 0; + } + + if (unlikely (bits1 < huffman_table_bits)) + { + t1 = huffman_table[(val1 << (huffman_table_bits - bits1)) + & huffman_mask]; + if (unlikely (bits1 < (t1 & 0xff))) + { + elf_uncompress_failed (); + return 0; + } + } + else + t1 = huffman_table[(val1 >> (bits1 - huffman_table_bits)) + & huffman_mask]; + + if (unlikely (bits2 < huffman_table_bits)) + { + t2 = huffman_table[(val2 << (huffman_table_bits - bits2)) + & huffman_mask]; + if (unlikely (bits2 < (t2 & 0xff))) + { + elf_uncompress_failed (); + return 0; + } + } + else + t2 = huffman_table[(val2 >> (bits2 - huffman_table_bits)) + & huffman_mask]; + + if (unlikely (bits3 < huffman_table_bits)) + { + t3 = huffman_table[(val3 << (huffman_table_bits - bits3)) + & huffman_mask]; + if (unlikely (bits3 < (t3 & 0xff))) + { + elf_uncompress_failed (); + return 0; + } + } + else + t3 = huffman_table[(val3 >> (bits3 - huffman_table_bits)) + & huffman_mask]; + + if (use4) + { + if (unlikely (bits4 < huffman_table_bits)) + { + t4 = huffman_table[(val4 << (huffman_table_bits - bits4)) + & huffman_mask]; + if (unlikely (bits4 < (t4 & 0xff))) + { + elf_uncompress_failed (); + return 0; + } + } + else + t4 = huffman_table[(val4 >> (bits4 - huffman_table_bits)) + & huffman_mask]; + + *plit4 = t4 >> 8; + ++plit4; + bits4 -= t4 & 0xff; + } + + *plit1 = t1 >> 8; + ++plit1; + bits1 -= t1 & 0xff; + + *plit2 = t2 >> 8; + ++plit2; + bits2 -= t2 & 0xff; + + *plit3 = t3 >> 8; + ++plit3; + bits3 -= t3 & 0xff; + + ++i; + } + } + + return 1; +} + +/* The information used to decompress a sequence code, which can be a literal + length, an offset, or a match length. */ + +struct elf_zstd_seq_decode +{ + const struct elf_zstd_fse_baseline_entry *table; + int table_bits; +}; + +/* Unpack a sequence code compression mode. */ + +static int +elf_zstd_unpack_seq_decode (int mode, + const unsigned char **ppin, + const unsigned char *pinend, + const struct elf_zstd_fse_baseline_entry *predef, + int predef_bits, + uint16_t *scratch, + int maxidx, + struct elf_zstd_fse_baseline_entry *table, + int table_bits, + int (*conv)(const struct elf_zstd_fse_entry *, + int, + struct elf_zstd_fse_baseline_entry *), + struct elf_zstd_seq_decode *decode) +{ + switch (mode) + { + case 0: + decode->table = predef; + decode->table_bits = predef_bits; + break; + + case 1: + { + struct elf_zstd_fse_entry entry; + + if (unlikely (*ppin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + entry.symbol = **ppin; + ++*ppin; + entry.bits = 0; + entry.base = 0; + decode->table_bits = 0; + if (!conv (&entry, 0, table)) + return 0; + } + break; + + case 2: + { + struct elf_zstd_fse_entry *fse_table; + + /* We use the same space for the simple FSE table and the baseline + table. */ + fse_table = (struct elf_zstd_fse_entry *)table; + decode->table_bits = table_bits; + if (!elf_zstd_read_fse (ppin, pinend, scratch, maxidx, fse_table, + &decode->table_bits)) + return 0; + if (!conv (fse_table, decode->table_bits, table)) + return 0; + decode->table = table; + } + break; + + case 3: + if (unlikely (decode->table_bits == -1)) + { + elf_uncompress_failed (); + return 0; + } + break; + + default: + elf_uncompress_failed (); + return 0; + } + + return 1; +} + +/* Decompress a zstd stream from PIN/SIN to POUT/SOUT. Code based on RFC 8878. + Return 1 on success, 0 on error. */ + +static int +elf_zstd_decompress (const unsigned char *pin, size_t sin, + unsigned char *zdebug_table, unsigned char *pout, + size_t sout) +{ + const unsigned char *pinend; + unsigned char *poutstart; + unsigned char *poutend; + struct elf_zstd_seq_decode literal_decode; + struct elf_zstd_fse_baseline_entry *literal_fse_table; + struct elf_zstd_seq_decode match_decode; + struct elf_zstd_fse_baseline_entry *match_fse_table; + struct elf_zstd_seq_decode offset_decode; + struct elf_zstd_fse_baseline_entry *offset_fse_table; + uint16_t *huffman_table; + int huffman_table_bits; + uint32_t repeated_offset1; + uint32_t repeated_offset2; + uint32_t repeated_offset3; + uint16_t *scratch; + unsigned char hdr; + int has_checksum; + uint64_t content_size; + int last_block; + + pinend = pin + sin; + poutstart = pout; + poutend = pout + sout; + + literal_decode.table = NULL; + literal_decode.table_bits = -1; + literal_fse_table = ((struct elf_zstd_fse_baseline_entry *) + (zdebug_table + ZSTD_TABLE_LITERAL_FSE_OFFSET)); + + match_decode.table = NULL; + match_decode.table_bits = -1; + match_fse_table = ((struct elf_zstd_fse_baseline_entry *) + (zdebug_table + ZSTD_TABLE_MATCH_FSE_OFFSET)); + + offset_decode.table = NULL; + offset_decode.table_bits = -1; + offset_fse_table = ((struct elf_zstd_fse_baseline_entry *) + (zdebug_table + ZSTD_TABLE_OFFSET_FSE_OFFSET)); + huffman_table = ((uint16_t *) + (zdebug_table + ZSTD_TABLE_HUFFMAN_OFFSET)); + huffman_table_bits = 0; + scratch = ((uint16_t *) + (zdebug_table + ZSTD_TABLE_WORK_OFFSET)); + + repeated_offset1 = 1; + repeated_offset2 = 4; + repeated_offset3 = 8; + + if (unlikely (sin < 4)) + { + elf_uncompress_failed (); + return 0; + } + + /* These values are the zstd magic number. */ + if (unlikely (pin[0] != 0x28 + || pin[1] != 0xb5 + || pin[2] != 0x2f + || pin[3] != 0xfd)) + { + elf_uncompress_failed (); + return 0; + } + + pin += 4; + + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + + hdr = *pin++; + + /* We expect a single frame. */ + if (unlikely ((hdr & (1 << 5)) == 0)) + { + elf_uncompress_failed (); + return 0; + } + /* Reserved bit must be zero. */ + if (unlikely ((hdr & (1 << 3)) != 0)) + { + elf_uncompress_failed (); + return 0; + } + /* We do not expect a dictionary. */ + if (unlikely ((hdr & 3) != 0)) + { + elf_uncompress_failed (); + return 0; + } + has_checksum = (hdr & (1 << 2)) != 0; + switch (hdr >> 6) + { + case 0: + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + content_size = (uint64_t) *pin++; + break; + case 1: + if (unlikely (pin + 1 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + content_size = (((uint64_t) pin[0]) | (((uint64_t) pin[1]) << 8)) + 256; + pin += 2; + break; + case 2: + if (unlikely (pin + 3 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + content_size = ((uint64_t) pin[0] + | (((uint64_t) pin[1]) << 8) + | (((uint64_t) pin[2]) << 16) + | (((uint64_t) pin[3]) << 24)); + pin += 4; + break; + case 3: + if (unlikely (pin + 7 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + content_size = ((uint64_t) pin[0] + | (((uint64_t) pin[1]) << 8) + | (((uint64_t) pin[2]) << 16) + | (((uint64_t) pin[3]) << 24) + | (((uint64_t) pin[4]) << 32) + | (((uint64_t) pin[5]) << 40) + | (((uint64_t) pin[6]) << 48) + | (((uint64_t) pin[7]) << 56)); + pin += 8; + break; + default: + elf_uncompress_failed (); + return 0; + } + + if (unlikely (content_size != (size_t) content_size + || (size_t) content_size != sout)) + { + elf_uncompress_failed (); + return 0; + } + + last_block = 0; + while (!last_block) + { + uint32_t block_hdr; + int block_type; + uint32_t block_size; + + if (unlikely (pin + 2 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + block_hdr = ((uint32_t) pin[0] + | (((uint32_t) pin[1]) << 8) + | (((uint32_t) pin[2]) << 16)); + pin += 3; + + last_block = block_hdr & 1; + block_type = (block_hdr >> 1) & 3; + block_size = block_hdr >> 3; + + switch (block_type) + { + case 0: + /* Raw_Block */ + if (unlikely ((size_t) block_size > (size_t) (pinend - pin))) + { + elf_uncompress_failed (); + return 0; + } + if (unlikely ((size_t) block_size > (size_t) (poutend - pout))) + { + elf_uncompress_failed (); + return 0; + } + memcpy (pout, pin, block_size); + pout += block_size; + pin += block_size; + break; + + case 1: + /* RLE_Block */ + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + if (unlikely ((size_t) block_size > (size_t) (poutend - pout))) + { + elf_uncompress_failed (); + return 0; + } + memset (pout, *pin, block_size); + pout += block_size; + pin++; + break; + + case 2: + { + const unsigned char *pblockend; + unsigned char *plitstack; + unsigned char *plit; + uint32_t literal_count; + unsigned char seq_hdr; + size_t seq_count; + size_t seq; + const unsigned char *pback; + uint64_t val; + unsigned int bits; + unsigned int literal_state; + unsigned int offset_state; + unsigned int match_state; + + /* Compressed_Block */ + if (unlikely ((size_t) block_size > (size_t) (pinend - pin))) + { + elf_uncompress_failed (); + return 0; + } + + pblockend = pin + block_size; + + /* Read the literals into the end of the output space, and leave + PLIT pointing at them. */ + + if (!elf_zstd_read_literals (&pin, pblockend, pout, poutend, + scratch, huffman_table, + &huffman_table_bits, + &plitstack)) + return 0; + plit = plitstack; + literal_count = poutend - plit; + + seq_hdr = *pin; + pin++; + if (seq_hdr < 128) + seq_count = seq_hdr; + else if (seq_hdr < 255) + { + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + seq_count = ((seq_hdr - 128) << 8) + *pin; + pin++; + } + else + { + if (unlikely (pin + 1 >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + seq_count = *pin + (pin[1] << 8) + 0x7f00; + pin += 2; + } + + if (seq_count > 0) + { + int (*pfn)(const struct elf_zstd_fse_entry *, + int, struct elf_zstd_fse_baseline_entry *); + + if (unlikely (pin >= pinend)) + { + elf_uncompress_failed (); + return 0; + } + seq_hdr = *pin; + ++pin; + + pfn = elf_zstd_make_literal_baseline_fse; + if (!elf_zstd_unpack_seq_decode ((seq_hdr >> 6) & 3, + &pin, pinend, + &elf_zstd_lit_table[0], 6, + scratch, 35, + literal_fse_table, 9, pfn, + &literal_decode)) + return 0; + + pfn = elf_zstd_make_offset_baseline_fse; + if (!elf_zstd_unpack_seq_decode ((seq_hdr >> 4) & 3, + &pin, pinend, + &elf_zstd_offset_table[0], 5, + scratch, 31, + offset_fse_table, 8, pfn, + &offset_decode)) + return 0; + + pfn = elf_zstd_make_match_baseline_fse; + if (!elf_zstd_unpack_seq_decode ((seq_hdr >> 2) & 3, + &pin, pinend, + &elf_zstd_match_table[0], 6, + scratch, 52, + match_fse_table, 9, pfn, + &match_decode)) + return 0; + } + + pback = pblockend - 1; + if (!elf_fetch_backward_init (&pback, pin, &val, &bits)) + return 0; + + bits -= literal_decode.table_bits; + literal_state = ((val >> bits) + & ((1U << literal_decode.table_bits) - 1)); + + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + bits -= offset_decode.table_bits; + offset_state = ((val >> bits) + & ((1U << offset_decode.table_bits) - 1)); + + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + bits -= match_decode.table_bits; + match_state = ((val >> bits) + & ((1U << match_decode.table_bits) - 1)); + + seq = 0; + while (1) + { + const struct elf_zstd_fse_baseline_entry *pt; + uint32_t offset_basebits; + uint32_t offset_baseline; + uint32_t offset_bits; + uint32_t offset_base; + uint32_t offset; + uint32_t match_baseline; + uint32_t match_bits; + uint32_t match_base; + uint32_t match; + uint32_t literal_baseline; + uint32_t literal_bits; + uint32_t literal_base; + uint32_t literal; + uint32_t need; + uint32_t add; + + pt = &offset_decode.table[offset_state]; + offset_basebits = pt->basebits; + offset_baseline = pt->baseline; + offset_bits = pt->bits; + offset_base = pt->base; + + /* This case can be more than 16 bits, which is all that + elf_fetch_bits_backward promises. */ + need = offset_basebits; + add = 0; + if (unlikely (need > 16)) + { + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + bits -= 16; + add = (val >> bits) & ((1U << 16) - 1); + need -= 16; + add <<= need; + } + if (need > 0) + { + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + bits -= need; + add += (val >> bits) & ((1U << need) - 1); + } + + offset = offset_baseline + add; + + pt = &match_decode.table[match_state]; + need = pt->basebits; + match_baseline = pt->baseline; + match_bits = pt->bits; + match_base = pt->base; + + add = 0; + if (need > 0) + { + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + bits -= need; + add = (val >> bits) & ((1U << need) - 1); + } + + match = match_baseline + add; + + pt = &literal_decode.table[literal_state]; + need = pt->basebits; + literal_baseline = pt->baseline; + literal_bits = pt->bits; + literal_base = pt->base; + + add = 0; + if (need > 0) + { + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + bits -= need; + add = (val >> bits) & ((1U << need) - 1); + } + + literal = literal_baseline + add; + + /* See the comment in elf_zstd_make_offset_baseline_fse. */ + if (offset_basebits > 1) + { + repeated_offset3 = repeated_offset2; + repeated_offset2 = repeated_offset1; + repeated_offset1 = offset; + } + else + { + if (unlikely (literal == 0)) + ++offset; + switch (offset) + { + case 1: + offset = repeated_offset1; + break; + case 2: + offset = repeated_offset2; + repeated_offset2 = repeated_offset1; + repeated_offset1 = offset; + break; + case 3: + offset = repeated_offset3; + repeated_offset3 = repeated_offset2; + repeated_offset2 = repeated_offset1; + repeated_offset1 = offset; + break; + case 4: + offset = repeated_offset1 - 1; + repeated_offset3 = repeated_offset2; + repeated_offset2 = repeated_offset1; + repeated_offset1 = offset; + break; + } + } + + ++seq; + if (seq < seq_count) + { + uint32_t v; + + /* Update the three states. */ + + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + + need = literal_bits; + bits -= need; + v = (val >> bits) & (((uint32_t)1 << need) - 1); + + literal_state = literal_base + v; + + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + + need = match_bits; + bits -= need; + v = (val >> bits) & (((uint32_t)1 << need) - 1); + + match_state = match_base + v; + + if (!elf_fetch_bits_backward (&pback, pin, &val, &bits)) + return 0; + + need = offset_bits; + bits -= need; + v = (val >> bits) & (((uint32_t)1 << need) - 1); + + offset_state = offset_base + v; + } + + /* The next sequence is now in LITERAL, OFFSET, MATCH. */ + + /* Copy LITERAL bytes from the literals. */ + + if (unlikely ((size_t)(poutend - pout) < literal)) + { + elf_uncompress_failed (); + return 0; + } + + if (unlikely (literal_count < literal)) + { + elf_uncompress_failed (); + return 0; + } + + literal_count -= literal; + + /* Often LITERAL is small, so handle small cases quickly. */ + switch (literal) + { + case 8: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 7: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 6: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 5: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 4: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 3: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 2: + *pout++ = *plit++; + /* FALLTHROUGH */ + case 1: + *pout++ = *plit++; + break; + + case 0: + break; + + default: + if (unlikely ((size_t)(plit - pout) < literal)) + { + uint32_t move; + + move = plit - pout; + while (literal > move) + { + memcpy (pout, plit, move); + pout += move; + plit += move; + literal -= move; + } + } + + memcpy (pout, plit, literal); + pout += literal; + plit += literal; + } + + if (match > 0) + { + /* Copy MATCH bytes from the decoded output at OFFSET. */ + + if (unlikely ((size_t)(poutend - pout) < match)) + { + elf_uncompress_failed (); + return 0; + } + + if (unlikely ((size_t)(pout - poutstart) < offset)) + { + elf_uncompress_failed (); + return 0; + } + + if (offset >= match) + { + memcpy (pout, pout - offset, match); + pout += match; + } + else + { + while (match > 0) + { + uint32_t copy; + + copy = match < offset ? match : offset; + memcpy (pout, pout - offset, copy); + match -= copy; + pout += copy; + } + } + } + + if (unlikely (seq >= seq_count)) + { + /* Copy remaining literals. */ + if (literal_count > 0 && plit != pout) + { + if (unlikely ((size_t)(poutend - pout) + < literal_count)) + { + elf_uncompress_failed (); + return 0; + } + + if ((size_t)(plit - pout) < literal_count) + { + uint32_t move; + + move = plit - pout; + while (literal_count > move) + { + memcpy (pout, plit, move); + pout += move; + plit += move; + literal_count -= move; + } + } + + memcpy (pout, plit, literal_count); + } + + pout += literal_count; + + break; + } + } + + pin = pblockend; + } + break; + + case 3: + default: + elf_uncompress_failed (); + return 0; + } + } + + if (has_checksum) + { + if (unlikely (pin + 4 > pinend)) + { + elf_uncompress_failed (); + return 0; + } + + /* We don't currently verify the checksum. Currently running GNU ld with + --compress-debug-sections=zstd does not seem to generate a + checksum. */ + + pin += 4; + } + + if (pin != pinend) + { + elf_uncompress_failed (); + return 0; + } + + return 1; +} + +#define ZDEBUG_TABLE_SIZE \ + (ZLIB_TABLE_SIZE > ZSTD_TABLE_SIZE ? ZLIB_TABLE_SIZE : ZSTD_TABLE_SIZE) + /* Uncompress the old compressed debug format, the one emitted by --compress-debug-sections=zlib-gnu. The compressed data is in COMPRESSED / COMPRESSED_SIZE, and the function writes to @@ -2628,6 +5094,8 @@ elf_uncompress_chdr (struct backtrace_state *state, unsigned char **uncompressed, size_t *uncompressed_size) { const b_elf_chdr *chdr; + char *alc; + size_t alc_len; unsigned char *po; *uncompressed = NULL; @@ -2639,31 +5107,50 @@ elf_uncompress_chdr (struct backtrace_state *state, chdr = (const b_elf_chdr *) compressed; - if (chdr->ch_type != ELFCOMPRESS_ZLIB) - { - /* Unsupported compression algorithm. */ - return 1; - } - + alc = NULL; + alc_len = 0; if (*uncompressed != NULL && *uncompressed_size >= chdr->ch_size) po = *uncompressed; else { - po = (unsigned char *) backtrace_alloc (state, chdr->ch_size, - error_callback, data); - if (po == NULL) + alc_len = chdr->ch_size; + alc = (char*)backtrace_alloc (state, alc_len, error_callback, data); + if (alc == NULL) return 0; + po = (unsigned char *) alc; } - if (!elf_zlib_inflate_and_verify (compressed + sizeof (b_elf_chdr), - compressed_size - sizeof (b_elf_chdr), - zdebug_table, po, chdr->ch_size)) - return 1; + switch (chdr->ch_type) + { + case ELFCOMPRESS_ZLIB: + if (!elf_zlib_inflate_and_verify (compressed + sizeof (b_elf_chdr), + compressed_size - sizeof (b_elf_chdr), + zdebug_table, po, chdr->ch_size)) + goto skip; + break; + + case ELFCOMPRESS_ZSTD: + if (!elf_zstd_decompress (compressed + sizeof (b_elf_chdr), + compressed_size - sizeof (b_elf_chdr), + (unsigned char *)zdebug_table, po, + chdr->ch_size)) + goto skip; + break; + + default: + /* Unsupported compression algorithm. */ + goto skip; + } *uncompressed = po; *uncompressed_size = chdr->ch_size; return 1; + + skip: + if (alc != NULL && alc_len > 0) + backtrace_free (state, alc, alc_len, error_callback, data); + return 1; } /* This function is a hook for testing the zlib support. It is only @@ -2692,6 +5179,31 @@ backtrace_uncompress_zdebug (struct backtrace_state *state, return ret; } +/* This function is a hook for testing the zstd support. It is only used by + tests. */ + +int +backtrace_uncompress_zstd (struct backtrace_state *state, + const unsigned char *compressed, + size_t compressed_size, + backtrace_error_callback error_callback, + void *data, unsigned char *uncompressed, + size_t uncompressed_size) +{ + unsigned char *zdebug_table; + int ret; + + zdebug_table = ((unsigned char *) backtrace_alloc (state, ZDEBUG_TABLE_SIZE, + error_callback, data)); + if (zdebug_table == NULL) + return 0; + ret = elf_zstd_decompress (compressed, compressed_size, + zdebug_table, uncompressed, uncompressed_size); + backtrace_free (state, zdebug_table, ZDEBUG_TABLE_SIZE, + error_callback, data); + return ret; +} + /* Number of LZMA states. */ #define LZMA_STATES (12) @@ -4688,7 +7200,7 @@ elf_add (struct backtrace_state *state, const char *filename, int descriptor, if (zdebug_table == NULL) { zdebug_table = ((uint16_t *) - backtrace_alloc (state, ZDEBUG_TABLE_SIZE, + backtrace_alloc (state, ZLIB_TABLE_SIZE, error_callback, data)); if (zdebug_table == NULL) goto fail; @@ -4714,8 +7226,15 @@ elf_add (struct backtrace_state *state, const char *filename, int descriptor, } } + if (zdebug_table != NULL) + { + backtrace_free (state, zdebug_table, ZLIB_TABLE_SIZE, + error_callback, data); + zdebug_table = NULL; + } + /* Uncompress the official ELF format - (--compress-debug-sections=zlib-gabi). */ + (--compress-debug-sections=zlib-gabi, --compress-debug-sections=zstd). */ for (i = 0; i < (int) DEBUG_MAX; ++i) { unsigned char *uncompressed_data; diff --git a/Source/ThirdParty/tracy/libbacktrace/internal.hpp b/Source/ThirdParty/tracy/libbacktrace/internal.hpp index 96c097e02..f871844b6 100644 --- a/Source/ThirdParty/tracy/libbacktrace/internal.hpp +++ b/Source/ThirdParty/tracy/libbacktrace/internal.hpp @@ -371,6 +371,15 @@ extern int backtrace_uncompress_zdebug (struct backtrace_state *, unsigned char **uncompressed, size_t *uncompressed_size); +/* A test-only hook for elf_zstd_decompress. */ + +extern int backtrace_uncompress_zstd (struct backtrace_state *, + const unsigned char *compressed, + size_t compressed_size, + backtrace_error_callback, void *data, + unsigned char *uncompressed, + size_t uncompressed_size); + /* A test-only hook for elf_uncompress_lzma. */ extern int backtrace_uncompress_lzma (struct backtrace_state *, diff --git a/Source/ThirdParty/tracy/tracy/Tracy.hpp b/Source/ThirdParty/tracy/tracy/Tracy.hpp index 8ef26d59e..e9c943d2f 100644 --- a/Source/ThirdParty/tracy/tracy/Tracy.hpp +++ b/Source/ThirdParty/tracy/tracy/Tracy.hpp @@ -1,6 +1,18 @@ #ifndef __TRACY_HPP__ #define __TRACY_HPP__ +#ifndef TracyFunction +# define TracyFunction __FUNCTION__ +#endif + +#ifndef TracyFile +# define TracyFile __FILE__ +#endif + +#ifndef TracyLine +# define TracyLine __LINE__ +#endif + #ifndef TRACY_ENABLE #define ZoneNamed(x,y) @@ -80,6 +92,8 @@ #define TracySourceCallbackRegister(x,y) #define TracyParameterRegister(x,y) #define TracyParameterSetup(x,y,z,w) +#define TracyIsConnected false +#define TracySetProgramName(x) #define TracyFiberEnter(x) #define TracyFiberLeave @@ -124,21 +138,21 @@ public: } #if defined TRACY_HAS_CALLSTACK && defined TRACY_CALLSTACK -# define ZoneNamed( varname, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), TRACY_CALLSTACK, active ) -# define ZoneNamedN( varname, name, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { name, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), TRACY_CALLSTACK, active ) -# define ZoneNamedC( varname, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), TRACY_CALLSTACK, active ) -# define ZoneNamedNC( varname, name, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { name, __FUNCTION__, __FILE__, (uint32_t)__LINE__, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), TRACY_CALLSTACK, active ) +# define ZoneNamed( varname, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), TRACY_CALLSTACK, active ) +# define ZoneNamedN( varname, name, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { name, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), TRACY_CALLSTACK, active ) +# define ZoneNamedC( varname, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), TRACY_CALLSTACK, active ) +# define ZoneNamedNC( varname, name, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { name, TracyFunction, TracyFile, (uint32_t)TracyLine, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), TRACY_CALLSTACK, active ) -# define ZoneTransient( varname, active ) tracy::ScopedZone varname( __LINE__, __FILE__, strlen( __FILE__ ), __FUNCTION__, strlen( __FUNCTION__ ), nullptr, 0, TRACY_CALLSTACK, active ) -# define ZoneTransientN( varname, name, active ) tracy::ScopedZone varname( __LINE__, __FILE__, strlen( __FILE__ ), __FUNCTION__, strlen( __FUNCTION__ ), name, strlen( name ), TRACY_CALLSTACK, active ) +# define ZoneTransient( varname, active ) tracy::ScopedZone varname( TracyLine, TracyFile, strlen( TracyFile ), TracyFunction, strlen( TracyFunction ), nullptr, 0, TRACY_CALLSTACK, active ) +# define ZoneTransientN( varname, name, active ) tracy::ScopedZone varname( TracyLine, TracyFile, strlen( TracyFile ), TracyFunction, strlen( TracyFunction ), name, strlen( name ), TRACY_CALLSTACK, active ) #else -# define ZoneNamed( varname, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), active ) -# define ZoneNamedN( varname, name, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { name, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), active ) -# define ZoneNamedC( varname, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), active ) -# define ZoneNamedNC( varname, name, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { name, __FUNCTION__, __FILE__, (uint32_t)__LINE__, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), active ) +# define ZoneNamed( varname, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), active ) +# define ZoneNamedN( varname, name, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { name, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), active ) +# define ZoneNamedC( varname, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), active ) +# define ZoneNamedNC( varname, name, color, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { name, TracyFunction, TracyFile, (uint32_t)TracyLine, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), active ) -# define ZoneTransient( varname, active ) tracy::ScopedZone varname( __LINE__, __FILE__, strlen( __FILE__ ), __FUNCTION__, strlen( __FUNCTION__ ), nullptr, 0, active ) -# define ZoneTransientN( varname, name, active ) tracy::ScopedZone varname( __LINE__, __FILE__, strlen( __FILE__ ), __FUNCTION__, strlen( __FUNCTION__ ), name, strlen( name ), active ) +# define ZoneTransient( varname, active ) tracy::ScopedZone varname( TracyLine, TracyFile, strlen( TracyFile ), TracyFunction, strlen( TracyFunction ), nullptr, 0, active ) +# define ZoneTransientN( varname, name, active ) tracy::ScopedZone varname( TracyLine, TracyFile, strlen( TracyFile ), TracyFunction, strlen( TracyFunction ), name, strlen( name ), active ) #endif #define ZoneScoped ZoneNamed( ___tracy_scoped_zone, true ) @@ -164,13 +178,13 @@ public: #define FrameImage( image, width, height, offset, flip ) tracy::Profiler::SendFrameImage( image, width, height, offset, flip ) -#define TracyLockable( type, varname ) tracy::Lockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, #type " " #varname, __FILE__, __LINE__, 0 }; return &srcloc; }() } -#define TracyLockableN( type, varname, desc ) tracy::Lockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, desc, __FILE__, __LINE__, 0 }; return &srcloc; }() } -#define TracySharedLockable( type, varname ) tracy::SharedLockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, #type " " #varname, __FILE__, __LINE__, 0 }; return &srcloc; }() } -#define TracySharedLockableN( type, varname, desc ) tracy::SharedLockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, desc, __FILE__, __LINE__, 0 }; return &srcloc; }() } +#define TracyLockable( type, varname ) tracy::Lockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, #type " " #varname, TracyFile, TracyLine, 0 }; return &srcloc; }() } +#define TracyLockableN( type, varname, desc ) tracy::Lockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, desc, TracyFile, TracyLine, 0 }; return &srcloc; }() } +#define TracySharedLockable( type, varname ) tracy::SharedLockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, #type " " #varname, TracyFile, TracyLine, 0 }; return &srcloc; }() } +#define TracySharedLockableN( type, varname, desc ) tracy::SharedLockable varname { [] () -> const tracy::SourceLocationData* { static constexpr tracy::SourceLocationData srcloc { nullptr, desc, TracyFile, TracyLine, 0 }; return &srcloc; }() } #define LockableBase( type ) tracy::Lockable #define SharedLockableBase( type ) tracy::SharedLockable -#define LockMark( varname ) static constexpr tracy::SourceLocationData __tracy_lock_location_##varname { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; varname.Mark( &__tracy_lock_location_##varname ) +#define LockMark( varname ) static constexpr tracy::SourceLocationData __tracy_lock_location_##varname { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; varname.Mark( &__tracy_lock_location_##varname ) #define LockableName( varname, txt, size ) varname.CustomName( txt, size ) #define TracyPlot( name, val ) tracy::Profiler::PlotData( name, val ) @@ -211,13 +225,13 @@ public: #endif #ifdef TRACY_HAS_CALLSTACK -# define ZoneNamedS( varname, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), depth, active ) -# define ZoneNamedNS( varname, name, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { name, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), depth, active ) -# define ZoneNamedCS( varname, color, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), depth, active ) -# define ZoneNamedNCS( varname, name, color, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,__LINE__) { name, __FUNCTION__, __FILE__, (uint32_t)__LINE__, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,__LINE__), depth, active ) +# define ZoneNamedS( varname, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), depth, active ) +# define ZoneNamedNS( varname, name, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { name, TracyFunction, TracyFile, (uint32_t)TracyLine, 0 }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), depth, active ) +# define ZoneNamedCS( varname, color, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { nullptr, TracyFunction, TracyFile, (uint32_t)TracyLine, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), depth, active ) +# define ZoneNamedNCS( varname, name, color, depth, active ) static constexpr tracy::SourceLocationData TracyConcat(__tracy_source_location,TracyLine) { name, TracyFunction, TracyFile, (uint32_t)TracyLine, color }; tracy::ScopedZone varname( &TracyConcat(__tracy_source_location,TracyLine), depth, active ) -# define ZoneTransientS( varname, depth, active ) tracy::ScopedZone varname( __LINE__, __FILE__, strlen( __FILE__ ), __FUNCTION__, strlen( __FUNCTION__ ), nullptr, 0, depth, active ) -# define ZoneTransientNS( varname, name, depth, active ) tracy::ScopedZone varname( __LINE__, __FILE__, strlen( __FILE__ ), __FUNCTION__, strlen( __FUNCTION__ ), name, strlen( name ), depth, active ) +# define ZoneTransientS( varname, depth, active ) tracy::ScopedZone varname( TracyLine, TracyFile, strlen( TracyFile ), TracyFunction, strlen( TracyFunction ), nullptr, 0, depth, active ) +# define ZoneTransientNS( varname, name, depth, active ) tracy::ScopedZone varname( TracyLine, TracyFile, strlen( TracyFile ), TracyFunction, strlen( TracyFunction ), name, strlen( name ), depth, active ) # define ZoneScopedS( depth ) ZoneNamedS( ___tracy_scoped_zone, depth, true ) # define ZoneScopedNS( name, depth ) ZoneNamedNS( ___tracy_scoped_zone, name, depth, true ) @@ -272,6 +286,7 @@ public: #define TracyParameterRegister( cb, data ) tracy::Profiler::ParameterRegister( cb, data ) #define TracyParameterSetup( idx, name, isBool, val ) tracy::Profiler::ParameterSetup( idx, name, isBool, val ) #define TracyIsConnected tracy::GetProfiler().IsConnected() +#define TracySetProgramName( name ) tracy::GetProfiler().SetProgramName( name ); #ifdef TRACY_FIBERS # define TracyFiberEnter( fiber ) tracy::Profiler::EnterFiber( fiber ) diff --git a/Source/ThirdParty/volk/volk.Build.cs b/Source/ThirdParty/volk/volk.Build.cs index 2f4cfa0d6..67f7593d9 100644 --- a/Source/ThirdParty/volk/volk.Build.cs +++ b/Source/ThirdParty/volk/volk.Build.cs @@ -1,5 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. +using System.Collections.Generic; using System.IO; using Flax.Build; using Flax.Build.NativeCpp; @@ -62,4 +63,12 @@ public class volk : ThirdPartyModule Log.ErrorOnce("Missing VulkanSDK.", ref _missingSDKError); } } + + /// + public override void GetFilesToDeploy(List files) + { + base.GetFilesToDeploy(files); + + files.Add(Path.Combine(FolderPath, "volk.h")); + } } diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 90e96d075..10e4d5846 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -3135,14 +3135,17 @@ namespace Flax.Build.Bindings contents.AppendLine("#pragma once"); contents.AppendLine(); contents.AppendLine($"#define {binaryModuleNameUpper}_NAME \"{binaryModuleName}\""); - if (version.Build == -1) + if (version.Build <= 0) contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION Version({version.Major}, {version.Minor})"); - else + else if (version.Revision <= 0) contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION Version({version.Major}, {version.Minor}, {version.Build})"); + else + contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION Version({version.Major}, {version.Minor}, {version.Build}, {version.Revision})"); contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION_TEXT \"{version}\""); contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION_MAJOR {version.Major}"); contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION_MINOR {version.Minor}"); contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION_BUILD {version.Build}"); + contents.AppendLine($"#define {binaryModuleNameUpper}_VERSION_REVISION {version.Revision}"); contents.AppendLine($"#define {binaryModuleNameUpper}_COMPANY \"{project.Company}\""); contents.AppendLine($"#define {binaryModuleNameUpper}_COPYRIGHT \"{project.Copyright}\""); contents.AppendLine(); diff --git a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs index c64ee38b3..6e9fbea83 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs @@ -128,7 +128,7 @@ namespace Flax.Build sourceFiles.Sort(); // Build assembly - BuildDotNet(graph, buildData, targetBuildOptions, target.OutputName, sourceFiles); + BuildDotNet(graph, buildData, targetBuildOptions, target.OutputName, sourceFiles, optimizeAssembly: buildData.TargetOptions.ScriptingAPI.Optimization); } // Deploy files @@ -152,7 +152,7 @@ namespace Flax.Build } } - private static void BuildDotNet(TaskGraph graph, BuildData buildData, BuildOptions buildOptions, string name, List sourceFiles, HashSet fileReferences = null, IGrouping binaryModule = null) + private static void BuildDotNet(TaskGraph graph, BuildData buildData, BuildOptions buildOptions, string name, List sourceFiles, HashSet fileReferences = null, IGrouping binaryModule = null, bool? optimizeAssembly = null) { // Setup build options var buildPlatform = Platform.BuildTargetPlatform; @@ -257,10 +257,8 @@ namespace Flax.Build if (buildOptions.ScriptingAPI.IgnoreMissingDocumentationWarnings) args.Add("-nowarn:1591"); - // Optimizations prevent debugging, only enable in release builds - var optimize = buildData.Configuration == TargetConfiguration.Release; - if (buildData.TargetOptions.ScriptingAPI.Optimization.HasValue) - optimize = buildData.TargetOptions.ScriptingAPI.Optimization.Value; + // Optimizations prevent debugging, only enable in release builds by default + var optimize = optimizeAssembly.HasValue ? optimizeAssembly.Value : buildData.Configuration == TargetConfiguration.Release; args.Add(optimize ? "/optimize+" : "/optimize-"); #if !USE_NETCORE args.Add(string.Format("/reference:\"{0}mscorlib.dll\"", referenceAssemblies)); @@ -374,11 +372,15 @@ namespace Flax.Build // Get references fileReferences.Clear(); + bool? optimizeAssembly = null; foreach (var module in binaryModule) { if (!buildData.Modules.TryGetValue(module, out var moduleBuildOptions)) continue; + if (moduleBuildOptions.ScriptingAPI.Optimization.HasValue) + optimizeAssembly |= moduleBuildOptions.ScriptingAPI.Optimization; + // Find references based on the modules dependencies foreach (var dependencyName in moduleBuildOptions.PublicDependencies.Concat(moduleBuildOptions.PrivateDependencies)) { @@ -408,7 +410,7 @@ namespace Flax.Build } // Build assembly - BuildDotNet(graph, buildData, buildOptions, binaryModuleName + ".CSharp", sourceFiles, fileReferences, binaryModule); + BuildDotNet(graph, buildData, buildOptions, binaryModuleName + ".CSharp", sourceFiles, fileReferences, binaryModule, optimizeAssembly); } } } diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs index 72cefbfca..e161024b4 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs @@ -225,7 +225,7 @@ namespace Flax.Build.NativeCpp public CSharpNullableReferences CSharpNullableReferences = CSharpNullableReferences.Disable; /// - /// Enable code optimization. + /// Enable code optimizations for the managed module assembly. /// public bool? Optimization; @@ -237,19 +237,13 @@ namespace Flax.Build.NativeCpp /// Adds the other options into this. /// /// The other. - public void Add(ScriptingAPIOptions other, bool addBuildOptions = true) + public void Add(ScriptingAPIOptions other) { Defines.AddRange(other.Defines); SystemReferences.AddRange(other.SystemReferences); FileReferences.AddRange(other.FileReferences); Analyzers.AddRange(other.Analyzers); IgnoreMissingDocumentationWarnings |= other.IgnoreMissingDocumentationWarnings; - - if (addBuildOptions) - { - if (other.Optimization.HasValue) - Optimization |= other.Optimization; - } } } diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs index 1e8550c4e..225e46ba7 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/Builder.NativeCpp.cs @@ -403,7 +403,7 @@ namespace Flax.Build moduleOptions.PrivateIncludePaths.AddRange(dependencyOptions.PublicIncludePaths); moduleOptions.Libraries.AddRange(dependencyOptions.Libraries); moduleOptions.DelayLoadLibraries.AddRange(dependencyOptions.DelayLoadLibraries); - moduleOptions.ScriptingAPI.Add(dependencyOptions.ScriptingAPI, false); + moduleOptions.ScriptingAPI.Add(dependencyOptions.ScriptingAPI); moduleOptions.ExternalModules.AddRange(dependencyOptions.ExternalModules); } } @@ -418,7 +418,7 @@ namespace Flax.Build moduleOptions.PublicIncludePaths.AddRange(dependencyOptions.PublicIncludePaths); moduleOptions.Libraries.AddRange(dependencyOptions.Libraries); moduleOptions.DelayLoadLibraries.AddRange(dependencyOptions.DelayLoadLibraries); - moduleOptions.ScriptingAPI.Add(dependencyOptions.ScriptingAPI, false); + moduleOptions.ScriptingAPI.Add(dependencyOptions.ScriptingAPI); moduleOptions.ExternalModules.AddRange(dependencyOptions.ExternalModules); } } diff --git a/Source/Tools/Flax.Build/Deploy/Deployer.cs b/Source/Tools/Flax.Build/Deploy/Deployer.cs index 5882bd366..4f12e769f 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployer.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployer.cs @@ -44,6 +44,12 @@ namespace Flax.Build /// [CommandLine("deployCertPass", "Certificate file password for binaries signing.")] public static string DeployCertPass; + + /// + /// Apple keychain profile name to use for app notarize action (installed locally). + /// + [CommandLine("deployKeychainProfile", "Apple keychain profile name to use for app notarize action (installed locally).")] + public static string DeployKeychainProfile; } } @@ -66,12 +72,6 @@ namespace Flax.Deploy { Initialize(); - if (Configuration.DeployEditor) - { - BuildEditor(); - Deployment.Editor.Package(); - } - if (Configuration.DeployPlatforms) { if (Configuration.BuildPlatforms == null || Configuration.BuildPlatforms.Length == 0) @@ -94,6 +94,12 @@ namespace Flax.Deploy } } } + + if (Configuration.DeployEditor) + { + BuildEditor(); + Deployment.Editor.Package(); + } } catch (Exception ex) { @@ -183,6 +189,10 @@ namespace Flax.Deploy { if (Platform.IsPlatformSupported(platform, architecture)) { + Log.Info(string.Empty); + Log.Info($"Build {platform} {architecture} platform"); + Log.Info(string.Empty); + foreach (var configuration in Configurations) { FlaxBuild.Build(Globals.EngineRoot, "FlaxGame", platform, architecture, configuration); diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs index 91da6d83d..e1f2ab4e6 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs @@ -127,6 +127,33 @@ namespace Flax.Deploy // Deploy project DeployFile(RootPath, OutputPath, "Flax.flaxproj"); + // When deploying Editor with Platforms at once then bundle them inside it + if (Configuration.DeployPlatforms && Platforms.PackagedPlatforms != null) + { + foreach (var platform in Platforms.PackagedPlatforms) + { + Log.Info(string.Empty); + Log.Info($"Bunding {platform} platform with Editor"); + Log.Info(string.Empty); + + string platformName = platform.ToString(); + string platformFiles = Path.Combine(Deployer.PackageOutputPath, platformName); + string platformData = Path.Combine(OutputPath, "Source", "Platforms", platformName); + if (Directory.Exists(platformFiles)) + { + // Copy deployed files + Utilities.DirectoryCopy(platformFiles, platformData); + } + else + { + // Extract deployed files + var packageZipPath = Path.Combine(Deployer.PackageOutputPath, platformName + ".zip"); + Log.Verbose(packageZipPath + " -> " + platformData); + System.IO.Compression.ZipFile.ExtractToDirectory(packageZipPath, platformData, true); + } + } + } + // Package Editor into macOS app if (Platform.BuildTargetPlatform == TargetPlatform.Mac) { @@ -164,12 +191,34 @@ namespace Flax.Deploy var defaultEditorConfig = "Development"; var ediotrBinariesPath = Path.Combine(appContentsPath, "Binaries/Editor/Mac", defaultEditorConfig); Utilities.DirectoryCopy(ediotrBinariesPath, appBinariesPath, true, true); + + // Sign app resources + CodeSign(appPath); + + // Build a disk image + var dmgPath = Path.Combine(Deployer.PackageOutputPath, "FlaxEditor.dmg"); + Log.Info(string.Empty); + Log.Info("Building disk image..."); + if (File.Exists(dmgPath)) + File.Delete(dmgPath); + Utilities.Run("hdiutil", $"create -srcFolder \"{appPath}\" -o \"{dmgPath}\"", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError); + CodeSign(dmgPath); + Log.Info("Output disk image size: " + Utilities.GetFileSize(dmgPath)); + + // Notarize disk image + if (!string.IsNullOrEmpty(Configuration.DeployKeychainProfile)) + { + Log.Info(string.Empty); + Log.Info("Notarizing disk image..."); + Utilities.Run("xcrun", $"notarytool submit \"{dmgPath}\" --wait --keychain-profile \"{Configuration.DeployKeychainProfile}\"", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError); + Utilities.Run("xcrun", $"stapler staple \"{dmgPath}\"", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError); + Log.Info("App notarized for macOS distribution!"); + } } // Compress if (Configuration.DontCompress) return; - Log.Info(string.Empty); Log.Info("Compressing editor files..."); string editorPackageZipPath; @@ -299,6 +348,7 @@ namespace Flax.Deploy Utilities.Run("strip", "FlaxEditor.dylib", null, dst, Utilities.RunOptions.None); Utilities.Run("strip", "libMoltenVK.dylib", null, dst, Utilities.RunOptions.None); + // Sign binaries CodeSign(Path.Combine(dst, "FlaxEditor")); CodeSign(Path.Combine(dst, "FlaxEditor.dylib")); CodeSign(Path.Combine(dst, "libMoltenVK.dylib")); diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Platforms.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Platforms.cs index 832d6deb3..e1d159ed5 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Platforms.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Platforms.cs @@ -10,8 +10,13 @@ namespace Flax.Deploy { public class Platforms { + internal static List PackagedPlatforms; + public static void Package(TargetPlatform platform) { + if (PackagedPlatforms == null) + PackagedPlatforms = new List(); + PackagedPlatforms.Add(platform); var platformsRoot = Path.Combine(Globals.EngineRoot, "Source", "Platforms"); Log.Info(string.Empty); @@ -50,6 +55,20 @@ namespace Flax.Deploy CodeSign(Path.Combine(binaries, "FlaxGame.exe")); CodeSign(Path.Combine(binaries, "FlaxEngine.CSharp.dll")); } + else if (platform == TargetPlatform.Mac) + { + var binaries = Path.Combine(dst, "Binaries", "Game", "arm64", "Debug"); + CodeSign(Path.Combine(binaries, "FlaxGame")); + CodeSign(Path.Combine(binaries, "FlaxGame.dylib")); + + binaries = Path.Combine(dst, "Binaries", "Game", "arm64", "Development"); + CodeSign(Path.Combine(binaries, "FlaxGame")); + CodeSign(Path.Combine(binaries, "FlaxGame.dylib")); + + binaries = Path.Combine(dst, "Binaries", "Game", "arm64", "Release"); + CodeSign(Path.Combine(binaries, "FlaxGame")); + CodeSign(Path.Combine(binaries, "FlaxGame.dylib")); + } // Don't distribute engine deps Utilities.DirectoryDelete(Path.Combine(dst, "Binaries", "ThirdParty")); diff --git a/Source/Tools/Flax.Build/Deploy/FlaxBuild.cs b/Source/Tools/Flax.Build/Deploy/FlaxBuild.cs index 7d3a31061..b4e8b8680 100644 --- a/Source/Tools/Flax.Build/Deploy/FlaxBuild.cs +++ b/Source/Tools/Flax.Build/Deploy/FlaxBuild.cs @@ -20,6 +20,7 @@ namespace Flax.Deploy if (!string.IsNullOrEmpty(Configuration.Compiler)) cmdLine += " -compiler=" + Configuration.Compiler; + Log.Info($"Building {target} for {platform} {architecture} {configuration}..."); int result = Utilities.Run(flaxBuildTool, cmdLine, null, root); if (result != 0) { diff --git a/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs b/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs index 3be0beb76..4a8115fcd 100644 --- a/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs +++ b/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs @@ -53,10 +53,19 @@ namespace Flax.Build.Platforms /// App code signing idenity name (from local Mac keychain). Use 'security find-identity -v -p codesigning' to list possible options. public static void CodeSign(string file, string signIdenity) { - if (!File.Exists(file)) + var isDirectory = Directory.Exists(file); + if (!isDirectory && !File.Exists(file)) throw new FileNotFoundException("Missing file to sign.", file); string cmdLine = string.Format("--force --timestamp -s \"{0}\" \"{1}\"", signIdenity, file); - if (string.IsNullOrEmpty(Path.GetExtension(file))) + if (isDirectory) + { + // Automatically sign contents + cmdLine += " --deep"; + } + { + // Enable the hardened runtime + cmdLine += " --options=runtime"; + } { // Add entitlements file with some settings for the app execution cmdLine += string.Format(" --entitlements \"{0}\"", Path.Combine(Globals.EngineRoot, "Source/Platforms/Mac/Default.entitlements")); diff --git a/Source/Tools/Flax.Build/ProjectInfo.cs b/Source/Tools/Flax.Build/ProjectInfo.cs index 7830f59c1..ec615fc49 100644 --- a/Source/Tools/Flax.Build/ProjectInfo.cs +++ b/Source/Tools/Flax.Build/ProjectInfo.cs @@ -14,9 +14,9 @@ namespace Flax.Build /// /// Writes the JSON representation of the object. /// - /// The to write to. + /// The to write to. /// The value. - /// The calling serializer. + /// The calling serializer. public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); @@ -25,73 +25,68 @@ namespace Flax.Build /// /// Reads the JSON representation of the object. /// - /// The to read from. - /// Type of the object. - /// The existing property value of the JSON that is being converted. - /// The calling serializer. + /// The to read from. + /// Type of the object. + /// The serializer options. /// The object value. public override Version? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) - { return null; - } - else + + if (reader.TokenType == JsonTokenType.StartObject) { - if (reader.TokenType == JsonTokenType.StartObject) + try { - try + reader.Read(); + var values = new Dictionary(); + while (reader.TokenType == JsonTokenType.PropertyName) { + var key = reader.GetString(); reader.Read(); - Dictionary values = new Dictionary(); - while (reader.TokenType == JsonTokenType.PropertyName) - { - var key = reader.GetString(); - reader.Read(); - var val = reader.GetInt32(); - reader.Read(); - values.Add(key, val); - } + var val = reader.GetInt32(); + reader.Read(); + values.Add(key, val); + } - int major = 0, minor = 0, build = 0; - values.TryGetValue("Major", out major); - values.TryGetValue("Minor", out minor); - values.TryGetValue("Build", out build); + values.TryGetValue("Major", out var major); + values.TryGetValue("Minor", out var minor); + if (!values.TryGetValue("Build", out var build)) + build = -1; + if (!values.TryGetValue("Revision", out var revision)) + revision = -1; - Version v = new Version(major, minor, build); - return v; - } - catch (Exception ex) - { - throw new Exception(String.Format("Error parsing version string: {0}", reader.GetString()), ex); - } + if (build <= 0) + return new Version(major, minor); + if (revision <= 0) + return new Version(major, minor, build); + return new Version(major, minor, build, revision); } - else if (reader.TokenType == JsonTokenType.String) + catch (Exception ex) { - try - { - Version v = new Version((string)reader.GetString()!); - return v; - } - catch (Exception ex) - { - throw new Exception(String.Format("Error parsing version string: {0}", reader.GetString()), ex); - } - } - else - { - throw new Exception(String.Format("Unexpected token or value when parsing version. Token: {0}, Value: {1}", reader.TokenType, reader.GetString())); + throw new Exception(string.Format("Error parsing version string: {0}", reader.GetString()), ex); } } + + if (reader.TokenType == JsonTokenType.String) + { + try + { + return new Version((string)reader.GetString()!); + } + catch (Exception ex) + { + throw new Exception(string.Format("Error parsing version string: {0}", reader.GetString()), ex); + } + } + throw new Exception(string.Format("Unexpected token or value when parsing version. Token: {0}, Value: {1}", reader.TokenType, reader.GetString())); } /// /// Determines whether this instance can convert the specified object type. /// /// Type of the object. - /// - /// true if this instance can convert the specified object type; otherwise, false. - /// + /// true if this instance can convert the specified object type; otherwise, false. public override bool CanConvert(Type objectType) { return objectType == typeof(Version); @@ -318,7 +313,7 @@ namespace Flax.Build Log.Verbose("Loading project file from \"" + path + "\"..."); var contents = File.ReadAllText(path); var project = JsonSerializer.Deserialize(contents.AsSpan(), - new JsonSerializerOptions() { Converters = { new FlaxVersionConverter() }, IncludeFields = true, TypeInfoResolver = ProjectInfoSourceGenerationContext.Default }); + new JsonSerializerOptions() { Converters = { new FlaxVersionConverter() }, IncludeFields = true, TypeInfoResolver = ProjectInfoSourceGenerationContext.Default }); project.ProjectPath = path; project.ProjectFolderPath = Path.GetDirectoryName(path); diff --git a/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs index 887197258..5c4b2dbe0 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudioCode/VisualStudioCodeProjectGenerator.cs @@ -166,13 +166,13 @@ namespace Flax.Build.Projects.VisualStudioCode foreach (var configuration in project.Configurations) { var target = configuration.Target; - var name = project.Name + '|' + configuration.Name; + var name = solution.Name + '|' + configuration.Name; json.BeginObject(); json.AddField("label", name); - bool isDefaultTask = defaultTask && configuration.Configuration == TargetConfiguration.Development && configuration.Platform == Platform.BuildPlatform.Target; + bool isDefaultTask = defaultTask && configuration.Configuration == TargetConfiguration.Development && configuration.Platform == Platform.BuildPlatform.Target && configuration.Architecture == Platform.BuildTargetArchitecture; json.BeginObject("group"); { @@ -294,132 +294,121 @@ namespace Flax.Build.Projects.VisualStudioCode json.BeginArray("configurations"); { - foreach (var project in solution.Projects) + var cppProject = solution.Projects.FirstOrDefault(x => x.BaseName == solution.Name || x.Name == solution.Name); + var csharpProject = solution.Projects.FirstOrDefault(x => x.BaseName == solution.MainProject.Targets[0].Modules[0] || x.Name == solution.MainProject.Targets[0].Modules[0]); + + if (cppProject != null) { - if (project.Name == "BuildScripts") - continue; // C++ project - if (project.Type == TargetType.NativeCpp) + foreach (var configuration in cppProject.Configurations) { - // Skip generating launch profiles for plugins and dependencies - if (solution.MainProject.Name != "Flax" && project.Name != "Flax.Build" && solution.MainProject.WorkspaceRootPath != project.WorkspaceRootPath) - continue; + var name = solution.Name + '|' + configuration.Name + " (C++)"; + var target = configuration.Target; + var outputType = cppProject.OutputType ?? target.OutputType; + var outputTargetFilePath = target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable); - foreach (var configuration in project.Configurations) + json.BeginObject(); { - var name = project.Name + '|' + configuration.Name; - var target = configuration.Target; + if (configuration.Platform == TargetPlatform.Windows) + json.AddField("type", "cppvsdbg"); + else + json.AddField("type", "cppdbg"); + json.AddField("name", name); + json.AddField("request", "launch"); + json.AddField("preLaunchTask", solution.Name + '|' + configuration.Name); + json.AddField("cwd", buildToolWorkspace); - var outputType = project.OutputType ?? target.OutputType; - var outputTargetFilePath = target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable); + WriteNativePlatformLaunchSettings(json, configuration.Platform); - json.BeginObject(); + if (outputType != TargetOutputType.Executable && configuration.Name.StartsWith("Editor.")) { if (configuration.Platform == TargetPlatform.Windows) - json.AddField("type", "cppvsdbg"); - else - json.AddField("type", "cppdbg"); - json.AddField("name", name); - json.AddField("request", "launch"); - json.AddField("preLaunchTask", name); - json.AddField("cwd", buildToolWorkspace); - - WriteNativePlatformLaunchSettings(json, configuration.Platform); - - if (outputType != TargetOutputType.Executable && configuration.Name.StartsWith("Editor.")) { - if (configuration.Platform == TargetPlatform.Windows) - { - var editorFolder = configuration.Architecture == TargetArchitecture.x64 ? "Win64" : "Win32"; - json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", editorFolder, configuration.ConfigurationName, "FlaxEditor.exe")); - } - else if (configuration.Platform == TargetPlatform.Linux) - json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", "Linux", configuration.ConfigurationName, "FlaxEditor")); - else if (configuration.Platform == TargetPlatform.Mac) - json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", "Mac", configuration.ConfigurationName, "FlaxEditor")); - - json.BeginArray("args"); - { - json.AddUnnamedField("-project"); - json.AddUnnamedField(buildToolWorkspace); - json.AddUnnamedField("-skipCompile"); - if (hasMonoProjects) - { - json.AddUnnamedField("-debug"); - json.AddUnnamedField("127.0.0.1:55555"); - } - } - json.EndArray(); + var editorFolder = configuration.Architecture == TargetArchitecture.x64 ? "Win64" : "Win32"; + json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", editorFolder, configuration.ConfigurationName, "FlaxEditor.exe")); } - else + else if (configuration.Platform == TargetPlatform.Linux) + json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", "Linux", configuration.ConfigurationName, "FlaxEditor")); + else if (configuration.Platform == TargetPlatform.Mac) + json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", "Mac", configuration.ConfigurationName, "FlaxEditor")); + + json.BeginArray("args"); { - json.AddField("program", outputTargetFilePath); - json.BeginArray("args"); + json.AddUnnamedField("-project"); + json.AddUnnamedField(buildToolWorkspace); + json.AddUnnamedField("-skipCompile"); + if (hasMonoProjects) { - if (configuration.Platform == TargetPlatform.Linux || configuration.Platform == TargetPlatform.Mac) - json.AddUnnamedField("--std"); - if (hasMonoProjects) - { - json.AddUnnamedField("-debug"); - json.AddUnnamedField("127.0.0.1:55555"); - } + json.AddUnnamedField("-debug"); + json.AddUnnamedField("127.0.0.1:55555"); } - json.EndArray(); } - - json.AddField("visualizerFile", Path.Combine(Globals.EngineRoot, "Source", "flax.natvis")); + json.EndArray(); } - json.EndObject(); - } - } - // C# project - else if (project.Type == TargetType.DotNetCore) - { - // Skip generating launch profiles for plugins and dependencies - if (solution.MainProject.WorkspaceRootPath != project.WorkspaceRootPath) - continue; - - foreach (var configuration in project.Configurations) - { - var name = project.Name + '|' + configuration.Name + " (C#)"; - var outputTargetFilePath = configuration.Target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable); - - json.BeginObject(); + else { - json.AddField("type", "coreclr"); - json.AddField("name", name); - json.AddField("request", "launch"); - json.AddField("preLaunchTask", solution.Name + '|' + configuration.Name); - json.AddField("cwd", buildToolWorkspace); - if (configuration.Platform == Platform.BuildPlatform.Target) + json.AddField("program", outputTargetFilePath); + json.BeginArray("args"); { - var editorFolder = configuration.Platform == TargetPlatform.Windows ? (configuration.Architecture == TargetArchitecture.x64 ? "Win64" : "Win32") : configuration.Platform.ToString(); - json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", editorFolder, configuration.ConfigurationName, "FlaxEditor")); - json.BeginArray("args"); + if (configuration.Platform == TargetPlatform.Linux || configuration.Platform == TargetPlatform.Mac) + json.AddUnnamedField("--std"); + if (hasMonoProjects) { - json.AddUnnamedField("-project"); - json.AddUnnamedField(buildToolWorkspace); - json.AddUnnamedField("-skipCompile"); + json.AddUnnamedField("-debug"); + json.AddUnnamedField("127.0.0.1:55555"); } - json.EndArray(); - } - else - { - json.AddField("program", configuration.Target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable)); - } - - switch (configuration.Platform) - { - case TargetPlatform.Windows: - json.AddField("stopAtEntry", false); - json.AddField("externalConsole", true); - break; - case TargetPlatform.Linux: - break; } + json.EndArray(); } - json.EndObject(); + + json.AddField("visualizerFile", Path.Combine(Globals.EngineRoot, "Source", "flax.natvis")); } + json.EndObject(); + } + } + if (csharpProject != null) + { + // C# project + foreach (var configuration in csharpProject.Configurations) + { + var name = solution.Name + '|' + configuration.Name + " (C#)"; + var outputTargetFilePath = configuration.Target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable); + + json.BeginObject(); + { + json.AddField("type", "coreclr"); + json.AddField("name", name); + json.AddField("request", "launch"); + json.AddField("preLaunchTask", solution.Name + '|' + configuration.Name); + json.AddField("cwd", buildToolWorkspace); + if (configuration.Platform == Platform.BuildPlatform.Target) + { + var editorFolder = configuration.Platform == TargetPlatform.Windows ? (configuration.Architecture == TargetArchitecture.x64 ? "Win64" : "Win32") : configuration.Platform.ToString(); + json.AddField("program", Path.Combine(Globals.EngineRoot, "Binaries", "Editor", editorFolder, configuration.ConfigurationName, "FlaxEditor")); + json.BeginArray("args"); + { + json.AddUnnamedField("-project"); + json.AddUnnamedField(buildToolWorkspace); + json.AddUnnamedField("-skipCompile"); + } + json.EndArray(); + } + else + { + json.AddField("program", configuration.Target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable)); + } + + switch (configuration.Platform) + { + case TargetPlatform.Windows: + json.AddField("stopAtEntry", false); + json.AddField("externalConsole", true); + break; + case TargetPlatform.Linux: + break; + } + } + json.EndObject(); } } } @@ -428,6 +417,9 @@ namespace Flax.Build.Projects.VisualStudioCode { foreach (var platform in solution.Projects.SelectMany(x => x.Configurations).Select(x => x.Platform).Distinct()) { + if (platform != TargetPlatform.Windows && platform != TargetPlatform.Linux && platform != TargetPlatform.Mac) + continue; + json.BeginObject(); { if (platform == TargetPlatform.Windows)