diff --git a/Content/Shaders/SSR.flax b/Content/Shaders/SSR.flax index a422ce5c0..d7cfdca1a 100644 --- a/Content/Shaders/SSR.flax +++ b/Content/Shaders/SSR.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0317fb2ca888fee6d32c7754d6f83e2f1e58924725a8221c5131e974501168ac -size 10912 +oid sha256:d3c3ceba0aa0b79f5619147a0ec8ac30d87c142322bd256dc235b7b4f890fc94 +size 11142 diff --git a/Source/Editor/Content/Proxy/SceneProxy.cs b/Source/Editor/Content/Proxy/SceneProxy.cs index 004c2aed7..d959b524a 100644 --- a/Source/Editor/Content/Proxy/SceneProxy.cs +++ b/Source/Editor/Content/Proxy/SceneProxy.cs @@ -1,6 +1,7 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; +using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Windows; using FlaxEngine; @@ -68,5 +69,15 @@ namespace FlaxEditor.Content { return new SceneItem(path, id); } + + /// + public override void OnContentWindowContextMenu(ContextMenu menu, ContentItem item) + { + var id = ((SceneItem)item).ID; + if (Level.FindScene(id) == null) + { + menu.AddButton("Open (additive)", () => { Editor.Instance.Scene.OpenScene(id, true); }); + } + } } } diff --git a/Source/Editor/Content/Thumbnails/ThumbnailRequest.cs b/Source/Editor/Content/Thumbnails/ThumbnailRequest.cs index 6e60602ff..40ad17720 100644 --- a/Source/Editor/Content/Thumbnails/ThumbnailRequest.cs +++ b/Source/Editor/Content/Thumbnails/ThumbnailRequest.cs @@ -35,6 +35,11 @@ namespace FlaxEditor.Content.Thumbnails /// The finalized state. /// Disposed, + + /// + /// The request has failed (eg. asset cannot be loaded). + /// + Failed, }; /// @@ -78,6 +83,14 @@ namespace FlaxEditor.Content.Thumbnails Proxy = proxy; } + internal void Update() + { + if (State == States.Prepared && (!Asset || Asset.LastLoadFailed)) + { + State = States.Failed; + } + } + /// /// Prepares this request. /// @@ -85,11 +98,8 @@ namespace FlaxEditor.Content.Thumbnails { if (State != States.Created) throw new InvalidOperationException(); - - // Prepare Asset = FlaxEngine.Content.LoadAsync(Item.Path); Proxy.OnThumbnailDrawPrepare(this); - State = States.Prepared; } @@ -101,9 +111,7 @@ namespace FlaxEditor.Content.Thumbnails { if (State != States.Prepared) throw new InvalidOperationException(); - Item.Thumbnail = icon; - State = States.Rendered; } diff --git a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs index 2b830c933..b213cb798 100644 --- a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs +++ b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs @@ -120,6 +120,8 @@ namespace FlaxEditor.Content.Thumbnails internal static bool HasMinimumQuality(TextureBase asset) { + if (asset.HasStreamingError) + return true; // Don't block thumbnails queue when texture fails to stream in (eg. unsupported format) var mipLevels = asset.MipLevels; var minMipLevels = Mathf.Min(mipLevels, 7); return asset.IsLoaded && asset.ResidentMipLevels >= Mathf.Max(minMipLevels, (int)(mipLevels * MinimumRequiredResourcesQuality)); @@ -499,6 +501,7 @@ namespace FlaxEditor.Content.Thumbnails var request = _requests[i]; try { + request.Update(); if (request.IsReady) { isAnyReady = true; @@ -507,6 +510,10 @@ namespace FlaxEditor.Content.Thumbnails { request.Prepare(); } + else if (request.State == ThumbnailRequest.States.Failed) + { + _requests.RemoveAt(i--); + } } catch (Exception ex) { diff --git a/Source/Editor/Cooker/Steps/DeployDataStep.cpp b/Source/Editor/Cooker/Steps/DeployDataStep.cpp index 80cb73c2b..59ff0d744 100644 --- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp +++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp @@ -48,21 +48,21 @@ bool DeployDataStep::Perform(CookingData& data) } if (buildSettings.SkipDotnetPackaging && data.Tools->UseSystemDotnet()) { - // Use system-installed .Net Runtime + // Use system-installed .NET Runtime FileSystem::DeleteDirectory(dstDotnet); } else { - // Deploy .Net Runtime files + // Deploy .NET Runtime files FileSystem::CreateDirectory(dstDotnet); String srcDotnet = depsRoot / TEXT("Dotnet"); if (FileSystem::DirectoryExists(srcDotnet)) { - // Use prebuilt .Net installation for that platform - LOG(Info, "Using .Net Runtime {} at {}", data.Tools->GetName(), srcDotnet); + // Use prebuilt .NET installation for that platform + LOG(Info, "Using .NET Runtime {} at {}", data.Tools->GetName(), srcDotnet); if (EditorUtilities::CopyDirectoryIfNewer(dstDotnet, srcDotnet, true)) { - data.Error(TEXT("Failed to copy .Net runtime data files.")); + data.Error(TEXT("Failed to copy .NET runtime data files.")); return true; } } @@ -85,7 +85,7 @@ bool DeployDataStep::Perform(CookingData& data) } if (canUseSystemDotnet && (aotMode == DotNetAOTModes::None || aotMode == DotNetAOTModes::ILC)) { - // Ask Flax.Build to provide .Net SDK location for the current platform + // Ask Flax.Build to provide .NET SDK location for the current platform String sdks; bool failed = ScriptsBuilder::RunBuildTool(String::Format(TEXT("-log -logMessagesOnly -logFileWithConsole -logfile=SDKs.txt -printSDKs {}"), GAME_BUILD_DOTNET_VER), data.CacheDirectory); failed |= File::ReadAllText(data.CacheDirectory / TEXT("SDKs.txt"), sdks); @@ -101,7 +101,7 @@ bool DeployDataStep::Perform(CookingData& data) } if (failed || !FileSystem::DirectoryExists(srcDotnet)) { - data.Error(TEXT("Failed to get .Net SDK location for a current platform.")); + data.Error(TEXT("Failed to get .NET SDK location for the current host platform.")); return true; } @@ -110,19 +110,25 @@ bool DeployDataStep::Perform(CookingData& data) FileSystem::GetChildDirectories(versions, srcDotnet / TEXT("host/fxr")); if (versions.Count() == 0) { - data.Error(TEXT("Failed to get .Net SDK location for a current platform.")); + data.Error(TEXT("Failed to find any .NET hostfxr versions for the current host platform.")); return true; } for (String& version : versions) { version = String(StringUtils::GetFileName(version)); - if (!version.StartsWith(TEXT("7."))) + if (!version.StartsWith(TEXT("7.")) && !version.StartsWith(TEXT("8."))) // .NET 7 or .NET 8 version.Clear(); } Sorting::QuickSort(versions); const String version = versions.Last(); + if (version.IsEmpty()) + { + data.Error(TEXT("Failed to find supported .NET hostfxr version for the current host platform.")); + return true; + } + FileSystem::NormalizePath(srcDotnet); - LOG(Info, "Using .Net Runtime {} at {}", version, srcDotnet); + LOG(Info, "Using .NET Runtime {} at {}", version, srcDotnet); // Check if previously deployed files are valid (eg. system-installed .NET was updated from version 7.0.3 to 7.0.5) { @@ -158,13 +164,13 @@ bool DeployDataStep::Perform(CookingData& data) } if (failed) { - data.Error(TEXT("Failed to copy .Net runtime data files.")); + data.Error(TEXT("Failed to copy .NET runtime data files.")); return true; } } else { - // Ask Flax.Build to provide .Net Host Runtime location for the target platform + // Ask Flax.Build to provide .NET Host Runtime location for the target platform String sdks; const Char *platformName, *archName; data.GetBuildPlatformName(platformName, archName); @@ -180,11 +186,11 @@ bool DeployDataStep::Perform(CookingData& data) } if (failed || !FileSystem::DirectoryExists(srcDotnet)) { - data.Error(TEXT("Failed to get .Net SDK location for a current platform.")); + data.Error(TEXT("Failed to get .NET SDK location for the current host platform.")); return true; } FileSystem::NormalizePath(srcDotnet); - LOG(Info, "Using .Net Runtime {} at {}", TEXT("Host"), srcDotnet); + LOG(Info, "Using .NET Runtime {} at {}", TEXT("Host"), srcDotnet); // Deploy runtime files const Char* corlibPrivateName = TEXT("System.Private.CoreLib.dll"); @@ -249,7 +255,7 @@ bool DeployDataStep::Perform(CookingData& data) DEPLOY_NATIVE_FILE("libmonosgen-2.0.dylib"); DEPLOY_NATIVE_FILE("libSystem.IO.Compression.Native.dylib"); DEPLOY_NATIVE_FILE("libSystem.Native.dylib"); - DEPLOY_NATIVE_FILE("libSystem.Net.Security.Native.dylib"); + DEPLOY_NATIVE_FILE("libSystem.NET.Security.Native.dylib"); DEPLOY_NATIVE_FILE("libSystem.Security.Cryptography.Native.Apple.dylib"); break; #undef DEPLOY_NATIVE_FILE @@ -257,7 +263,7 @@ bool DeployDataStep::Perform(CookingData& data) } if (failed) { - data.Error(TEXT("Failed to copy .Net runtime data files.")); + data.Error(TEXT("Failed to copy .NET runtime data files.")); return true; } } @@ -278,7 +284,7 @@ bool DeployDataStep::Perform(CookingData& data) } if (ScriptsBuilder::RunBuildTool(args)) { - data.Error(TEXT("Failed to optimize .Net class library.")); + data.Error(TEXT("Failed to optimize .NET class library.")); return true; } } diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs index 9de330213..a519b1da1 100644 --- a/Source/Editor/CustomEditors/CustomEditor.cs +++ b/Source/Editor/CustomEditors/CustomEditor.cs @@ -7,9 +7,8 @@ using FlaxEditor.CustomEditors.GUI; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; +using FlaxEngine.Json; using FlaxEngine.Utilities; -using Newtonsoft.Json; -using JsonSerializer = FlaxEngine.Json.JsonSerializer; namespace FlaxEditor.CustomEditors { @@ -386,22 +385,22 @@ namespace FlaxEditor.CustomEditors LinkedLabel = label; } - private void RevertDiffToDefault(CustomEditor editor) - { - if (editor.ChildrenEditors.Count == 0) - { - // Skip if no change detected - if (!editor.Values.IsDefaultValueModified) - return; + /// + /// If true, the value reverting to default/reference will be handled via iteration over children editors, instead of for a whole object at once. + /// + public virtual bool RevertValueWithChildren => ChildrenEditors.Count != 0; - editor.SetValueToDefault(); + private void RevertDiffToDefault() + { + if (RevertValueWithChildren) + { + foreach (var child in ChildrenEditors) + child.RevertDiffToDefault(); } else { - for (int i = 0; i < editor.ChildrenEditors.Count; i++) - { - RevertDiffToDefault(editor.ChildrenEditors[i]); - } + if (Values.IsDefaultValueModified) + SetValueToDefault(); } } @@ -414,11 +413,6 @@ namespace FlaxEditor.CustomEditors { if (!Values.IsDefaultValueModified) return false; - - // Skip array items (show diff only on a bottom level properties and fields) - if (ParentEditor is Editors.ArrayEditor) - return false; - return true; } } @@ -430,7 +424,7 @@ namespace FlaxEditor.CustomEditors { if (!Values.HasDefaultValue) return; - RevertDiffToDefault(this); + RevertDiffToDefault(); } /// @@ -468,22 +462,17 @@ namespace FlaxEditor.CustomEditors } } - private void RevertDiffToReference(CustomEditor editor) + private void RevertDiffToReference() { - if (editor.ChildrenEditors.Count == 0) + if (RevertValueWithChildren) { - // Skip if no change detected - if (!editor.Values.IsReferenceValueModified) - return; - - editor.SetValueToReference(); + foreach (var child in ChildrenEditors) + child.RevertDiffToReference(); } else { - for (int i = 0; i < editor.ChildrenEditors.Count; i++) - { - RevertDiffToReference(editor.ChildrenEditors[i]); - } + if (Values.IsReferenceValueModified) + SetValueToReference(); } } @@ -496,11 +485,6 @@ namespace FlaxEditor.CustomEditors { if (!Values.IsReferenceValueModified) return false; - - // Skip array items (show diff only on a bottom level properties and fields) - if (ParentEditor is Editors.ArrayEditor) - return false; - return true; } } @@ -512,7 +496,7 @@ namespace FlaxEditor.CustomEditors { if (!Values.HasReferenceValue) return; - RevertDiffToReference(this); + RevertDiffToReference(); } /// @@ -657,7 +641,7 @@ namespace FlaxEditor.CustomEditors // Default try { - obj = JsonConvert.DeserializeObject(text, TypeUtils.GetType(Values.Type), JsonSerializer.Settings); + obj = Newtonsoft.Json.JsonConvert.DeserializeObject(text, TypeUtils.GetType(Values.Type), JsonSerializer.Settings); } catch { @@ -762,7 +746,7 @@ namespace FlaxEditor.CustomEditors /// public void SetValueToDefault() { - SetValue(Values.DefaultValue); + SetValueCloned(Values.DefaultValue); } /// @@ -799,7 +783,19 @@ namespace FlaxEditor.CustomEditors return; } - SetValue(Values.ReferenceValue); + SetValueCloned(Values.ReferenceValue); + } + + private void SetValueCloned(object value) + { + // For objects (eg. arrays) we need to clone them to prevent editing default/reference value within editor + if (value != null && !value.GetType().IsValueType) + { + var json = JsonSerializer.Serialize(value); + value = JsonSerializer.Deserialize(json, value.GetType()); + } + + SetValue(value); } /// @@ -811,7 +807,6 @@ namespace FlaxEditor.CustomEditors { if (_isSetBlocked) return; - if (OnDirty(this, value, token)) { _hasValueDirty = true; diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 6f623fb23..919da4301 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -87,6 +87,7 @@ namespace FlaxEditor.CustomEditors.Editors protected bool NotNullItems; private IntegerValueElement _size; + private PropertyNameLabel _sizeLabel; private Color _background; private int _elementsCount; private bool _readOnly; @@ -109,6 +110,9 @@ namespace FlaxEditor.CustomEditors.Editors } } + /// + public override bool RevertValueWithChildren => false; // Always revert value for a whole collection + /// public override void Initialize(LayoutElementsContainer layout) { @@ -174,7 +178,9 @@ namespace FlaxEditor.CustomEditors.Editors } else { - _size = dragArea.IntegerValue("Size"); + var sizeProperty = dragArea.AddPropertyItem("Size"); + _sizeLabel = sizeProperty.Labels.Last(); + _size = sizeProperty.IntegerValue(); _size.IntValue.MinValue = 0; _size.IntValue.MaxValue = ushort.MaxValue; _size.IntValue.Value = size; @@ -274,6 +280,15 @@ namespace FlaxEditor.CustomEditors.Editors } } + /// + protected override void Deinitialize() + { + _size = null; + _sizeLabel = null; + + base.Deinitialize(); + } + /// /// Rebuilds the parent layout if its collection. /// @@ -296,7 +311,6 @@ namespace FlaxEditor.CustomEditors.Editors { if (IsSetBlocked) return; - Resize(_size.IntValue.Value); } @@ -311,11 +325,9 @@ namespace FlaxEditor.CustomEditors.Editors return; var cloned = CloneValues(); - var tmp = cloned[dstIndex]; cloned[dstIndex] = cloned[srcIndex]; cloned[srcIndex] = tmp; - SetValue(cloned); } @@ -371,6 +383,17 @@ namespace FlaxEditor.CustomEditors.Editors if (HasDifferentValues || HasDifferentTypes) return; + // Update reference/default value indicator + if (_sizeLabel != null) + { + var color = Color.Transparent; + if (Values.HasReferenceValue && Values.ReferenceValue is IList referenceValue && referenceValue.Count != Count) + color = FlaxEngine.GUI.Style.Current.BackgroundSelected; + else if (Values.HasDefaultValue && Values.DefaultValue is IList defaultValue && defaultValue.Count != Count) + color = Color.Yellow * 0.8f; + _sizeLabel.HighlightStripColor = color; + } + // Check if collection has been resized (by UI or from external source) if (Count != _elementsCount) { diff --git a/Source/Editor/Editor.cpp b/Source/Editor/Editor.cpp index 527bfeb6d..2cf9b9b8f 100644 --- a/Source/Editor/Editor.cpp +++ b/Source/Editor/Editor.cpp @@ -404,13 +404,23 @@ int32 Editor::LoadProduct() // Create new project option if (CommandLine::Options.NewProject) + { + Array projectFiles; + FileSystem::DirectoryGetFiles(projectFiles, projectPath, TEXT("*.flaxproj"), DirectorySearchOption::TopDirectoryOnly); + if (projectFiles.Count() == 1) + { + // Skip creating new project if it already exists + LOG(Info, "Skip creatinng new project because it already exists"); + CommandLine::Options.NewProject.Reset(); + } + } + if (CommandLine::Options.NewProject) { if (projectPath.IsEmpty()) projectPath = Platform::GetWorkingDirectory(); else if (!FileSystem::DirectoryExists(projectPath)) FileSystem::CreateDirectory(projectPath); FileSystem::NormalizePath(projectPath); - String folderName = StringUtils::GetFileName(projectPath); String tmpName; for (int32 i = 0; i < folderName.Length(); i++) diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs index b384b6515..393bf564e 100644 --- a/Source/Editor/Editor.cs +++ b/Source/Editor/Editor.cs @@ -1343,108 +1343,6 @@ namespace FlaxEditor public float AutoRebuildNavMeshTimeoutMs; } - [StructLayout(LayoutKind.Sequential)] - [NativeMarshalling(typeof(VisualScriptLocalMarshaller))] - internal struct VisualScriptLocal - { - public string Value; - public string ValueTypeName; - public uint NodeId; - public int BoxId; - } - - [CustomMarshaller(typeof(VisualScriptLocal), MarshalMode.Default, typeof(VisualScriptLocalMarshaller))] - internal static class VisualScriptLocalMarshaller - { - [StructLayout(LayoutKind.Sequential)] - internal struct VisualScriptLocalNative - { - public IntPtr Value; - public IntPtr ValueTypeName; - public uint NodeId; - public int BoxId; - } - - internal static VisualScriptLocal ConvertToManaged(VisualScriptLocalNative unmanaged) => ToManaged(unmanaged); - internal static VisualScriptLocalNative ConvertToUnmanaged(VisualScriptLocal managed) => ToNative(managed); - - internal static VisualScriptLocal ToManaged(VisualScriptLocalNative managed) - { - return new VisualScriptLocal() - { - Value = ManagedString.ToManaged(managed.Value), - ValueTypeName = ManagedString.ToManaged(managed.ValueTypeName), - NodeId = managed.NodeId, - BoxId = managed.BoxId, - }; - } - - internal static VisualScriptLocalNative ToNative(VisualScriptLocal managed) - { - return new VisualScriptLocalNative() - { - Value = ManagedString.ToNative(managed.Value), - ValueTypeName = ManagedString.ToNative(managed.ValueTypeName), - NodeId = managed.NodeId, - BoxId = managed.BoxId, - }; - } - - internal static void Free(VisualScriptLocalNative unmanaged) - { - ManagedString.Free(unmanaged.Value); - ManagedString.Free(unmanaged.ValueTypeName); - } - } - - [StructLayout(LayoutKind.Sequential)] - [NativeMarshalling(typeof(VisualScriptStackFrameMarshaller))] - internal struct VisualScriptStackFrame - { - public VisualScript Script; - public uint NodeId; - public int BoxId; - } - - [CustomMarshaller(typeof(VisualScriptStackFrame), MarshalMode.Default, typeof(VisualScriptStackFrameMarshaller))] - internal static class VisualScriptStackFrameMarshaller - { - [StructLayout(LayoutKind.Sequential)] - internal struct VisualScriptStackFrameNative - { - public IntPtr Script; - public uint NodeId; - public int BoxId; - } - - internal static VisualScriptStackFrame ConvertToManaged(VisualScriptStackFrameNative unmanaged) => ToManaged(unmanaged); - internal static VisualScriptStackFrameNative ConvertToUnmanaged(VisualScriptStackFrame managed) => ToNative(managed); - - internal static VisualScriptStackFrame ToManaged(VisualScriptStackFrameNative managed) - { - return new VisualScriptStackFrame() - { - Script = VisualScriptMarshaller.ConvertToManaged(managed.Script), - NodeId = managed.NodeId, - BoxId = managed.BoxId, - }; - } - - internal static VisualScriptStackFrameNative ToNative(VisualScriptStackFrame managed) - { - return new VisualScriptStackFrameNative() - { - Script = VisualScriptMarshaller.ConvertToUnmanaged(managed.Script), - NodeId = managed.NodeId, - BoxId = managed.BoxId, - }; - } - - internal static void Free(VisualScriptStackFrameNative unmanaged) - { - } - } - internal void BuildCommand(string arg) { if (TryBuildCommand(arg)) @@ -1723,21 +1621,6 @@ namespace FlaxEditor [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_RunVisualScriptBreakpointLoopTick", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] internal static partial void Internal_RunVisualScriptBreakpointLoopTick(float deltaTime); - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_GetVisualScriptLocals", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] - [return: MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = "localsCount")] - internal static partial VisualScriptLocal[] Internal_GetVisualScriptLocals(out int localsCount); - - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_GetVisualScriptStackFrames", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] - [return: MarshalUsing(typeof(FlaxEngine.Interop.ArrayMarshaller<,>), CountElementName = "stackFrameCount")] - internal static partial VisualScriptStackFrame[] Internal_GetVisualScriptStackFrames(out int stackFrameCount); - - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_GetVisualScriptPreviousScopeFrame", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] - internal static partial VisualScriptStackFrame Internal_GetVisualScriptPreviousScopeFrame(); - - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_EvaluateVisualScriptLocal", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] - [return: MarshalAs(UnmanagedType.U1)] - internal static partial bool Internal_EvaluateVisualScriptLocal(IntPtr script, ref VisualScriptLocal local); - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_DeserializeSceneObject", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] internal static partial void Internal_DeserializeSceneObject(IntPtr sceneObject, string json); diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs index 27878a763..66512236c 100644 --- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs +++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs @@ -3,6 +3,8 @@ using FlaxEditor.GUI.Input; using FlaxEngine; using FlaxEngine.GUI; +using FlaxEngine.Json; +using System.Collections.Generic; namespace FlaxEditor.GUI.Dialogs { @@ -30,6 +32,8 @@ namespace FlaxEditor.GUI.Dialogs private const float HSVMargin = 0.0f; private const float ChannelsMargin = 4.0f; private const float ChannelTextWidth = 12.0f; + private const float SavedColorButtonWidth = 20.0f; + private const float SavedColorButtonHeight = 20.0f; private Color _initialValue; private Color _value; @@ -52,6 +56,9 @@ namespace FlaxEditor.GUI.Dialogs private Button _cOK; private Button _cEyedropper; + private List _savedColors = new List(); + private List /// True if release unused render targets by force, otherwise will use a few frames of delay. - static void Flush(bool force = false); + /// Amount of previous frames that should persist in the pool after flush. Resources used more than given value wil be freed. Use value of -1 to auto pick default duration. + static void Flush(bool force = false, int32 framesOffset = -1); /// /// Gets a temporary render target. diff --git a/Source/Engine/Graphics/Textures/GPUTexture.cpp b/Source/Engine/Graphics/Textures/GPUTexture.cpp index ccdf4be95..403f545ba 100644 --- a/Source/Engine/Graphics/Textures/GPUTexture.cpp +++ b/Source/Engine/Graphics/Textures/GPUTexture.cpp @@ -480,6 +480,16 @@ bool GPUTexture::Init(const GPUTextureDescription& desc) break; } } + const bool isCompressed = PixelFormatExtensions::IsCompressed(desc.Format); + if (isCompressed) + { + const int32 blockSize = PixelFormatExtensions::ComputeBlockSize(desc.Format); + if (desc.Width < blockSize || desc.Height < blockSize) + { + LOG(Warning, "Cannot create texture. Invalid dimensions. Description: {0}", desc.ToString()); + return true; + } + } // Release previous data ReleaseGPU(); @@ -487,7 +497,7 @@ bool GPUTexture::Init(const GPUTextureDescription& desc) // Initialize _desc = desc; _sRGB = PixelFormatExtensions::IsSRGB(desc.Format); - _isBlockCompressed = PixelFormatExtensions::IsCompressed(desc.Format); + _isBlockCompressed = isCompressed; if (OnInit()) { ReleaseGPU(); diff --git a/Source/Engine/Graphics/Textures/StreamingTexture.cpp b/Source/Engine/Graphics/Textures/StreamingTexture.cpp index 015386fff..ea70a9d9e 100644 --- a/Source/Engine/Graphics/Textures/StreamingTexture.cpp +++ b/Source/Engine/Graphics/Textures/StreamingTexture.cpp @@ -114,9 +114,9 @@ bool StreamingTexture::Create(const TextureHeader& header) { // Ensure that streaming doesn't go too low because the hardware expects the texture to be min in size of compressed texture block int32 lastMip = header.MipLevels - 1; - while ((header.Width >> lastMip) < 4 && (header.Height >> lastMip) < 4) + while ((header.Width >> lastMip) < 4 && (header.Height >> lastMip) < 4 && lastMip > 0) lastMip--; - _minMipCountBlockCompressed = header.MipLevels - lastMip + 1; + _minMipCountBlockCompressed = Math::Min(header.MipLevels - lastMip + 1, header.MipLevels); } // Request resource streaming @@ -296,6 +296,7 @@ Task* StreamingTexture::UpdateAllocation(int32 residency) // Setup texture if (texture->Init(desc)) { + Streaming.Error = true; LOG(Error, "Cannot allocate texture {0}.", ToString()); } if (allocatedResidency != 0) diff --git a/Source/Engine/Graphics/Textures/TextureBase.cpp b/Source/Engine/Graphics/Textures/TextureBase.cpp index 181955fce..c3d8898a5 100644 --- a/Source/Engine/Graphics/Textures/TextureBase.cpp +++ b/Source/Engine/Graphics/Textures/TextureBase.cpp @@ -223,6 +223,11 @@ void TextureBase::SetTextureGroup(int32 textureGroup) } } +bool TextureBase::HasStreamingError() const +{ + return _texture.Streaming.Error; +} + BytesContainer TextureBase::GetMipData(int32 mipIndex, int32& rowPitch, int32& slicePitch) { BytesContainer result; diff --git a/Source/Engine/Graphics/Textures/TextureBase.h b/Source/Engine/Graphics/Textures/TextureBase.h index e47bb548d..befe16069 100644 --- a/Source/Engine/Graphics/Textures/TextureBase.h +++ b/Source/Engine/Graphics/Textures/TextureBase.h @@ -148,6 +148,11 @@ public: /// API_PROPERTY() void SetTextureGroup(int32 textureGroup); + /// + /// Returns true if texture streaming failed (eg. pixel format is unsupported or texture data cannot be uploaded to GPU due to memory limit). + /// + API_PROPERTY() bool HasStreamingError() const; + public: /// /// Gets the mip data. diff --git a/Source/Engine/Graphics/Textures/TextureUtils.h b/Source/Engine/Graphics/Textures/TextureUtils.h deleted file mode 100644 index b021d27fe..000000000 --- a/Source/Engine/Graphics/Textures/TextureUtils.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. - -#pragma once - -#include "Engine/Core/Types/BaseTypes.h" -#include "Types.h" - -/// -/// Texture utilities class -/// -class TextureUtils -{ -public: - static PixelFormat ToPixelFormat(const TextureFormatType format, int32 width, int32 height, bool canCompress) - { - const bool canUseBlockCompression = width % 4 == 0 && height % 4 == 0; - if (canCompress && canUseBlockCompression) - { - switch (format) - { - case TextureFormatType::ColorRGB: - return PixelFormat::BC1_UNorm; - case TextureFormatType::ColorRGBA: - return PixelFormat::BC3_UNorm; - case TextureFormatType::NormalMap: - return PixelFormat::BC5_UNorm; - case TextureFormatType::GrayScale: - return PixelFormat::BC4_UNorm; - case TextureFormatType::HdrRGBA: - return PixelFormat::BC7_UNorm; - case TextureFormatType::HdrRGB: -#if PLATFORM_LINUX - // TODO: support BC6H compression for Linux Editor - return PixelFormat::BC7_UNorm; -#else - return PixelFormat::BC6H_Uf16; -#endif - default: - return PixelFormat::Unknown; - } - } - - switch (format) - { - case TextureFormatType::ColorRGB: - return PixelFormat::R8G8B8A8_UNorm; - case TextureFormatType::ColorRGBA: - return PixelFormat::R8G8B8A8_UNorm; - case TextureFormatType::NormalMap: - return PixelFormat::R16G16_UNorm; - case TextureFormatType::GrayScale: - return PixelFormat::R8_UNorm; - case TextureFormatType::HdrRGBA: - return PixelFormat::R16G16B16A16_Float; - case TextureFormatType::HdrRGB: - return PixelFormat::R11G11B10_Float; - default: - return PixelFormat::Unknown; - } - } -}; diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index bc568458e..00ca485d2 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -1034,7 +1034,10 @@ void Actor::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) } else if (!parent && parentId.IsValid()) { - LOG(Warning, "Missing parent actor {0} for \'{1}\'", parentId, ToString()); + if (_prefabObjectID.IsValid()) + LOG(Warning, "Missing parent actor {0} for \'{1}\', prefab object {2}", parentId, ToString(), _prefabObjectID); + else + LOG(Warning, "Missing parent actor {0} for \'{1}\'", parentId, ToString()); } } } diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index 5f1432d31..41033876d 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -439,7 +439,7 @@ public: bool Do() const override { - auto scene = Scripting::FindObject(TargetScene); + auto scene = Level::FindScene(TargetScene); if (!scene) return true; return unloadScene(scene); @@ -934,13 +934,13 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou // Loaded scene objects list CollectionPoolCache::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get(); - const int32 objectsCount = (int32)data.Size(); - sceneObjects->Resize(objectsCount); + const int32 dataCount = (int32)data.Size(); + sceneObjects->Resize(dataCount); sceneObjects->At(0) = scene; // Spawn all scene objects SceneObjectsFactory::Context context(modifier.Value); - context.Async = JobSystem::GetThreadsCount() > 1 && objectsCount > 10; + context.Async = JobSystem::GetThreadsCount() > 1 && dataCount > 10; { PROFILE_CPU_NAMED("Spawn"); SceneObject** objects = sceneObjects->Get(); @@ -963,12 +963,12 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou } else SceneObjectsFactory::HandleObjectDeserializationError(stream); - }, objectsCount - 1); + }, dataCount - 1); ScenesLock.Lock(); } else { - for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene + for (int32 i = 1; i < dataCount; i++) // start from 1. at index [0] was scene { auto& stream = data[i]; auto obj = SceneObjectsFactory::Spawn(context, stream); @@ -1012,13 +1012,13 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou SceneObjectsFactory::Deserialize(context, obj, data[i]); idMapping = nullptr; } - }, objectsCount - 1); + }, dataCount - 1); ScenesLock.Lock(); } else { Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping); - for (int32 i = 1; i < objectsCount; i++) // start from 1. at index [0] was scene + for (int32 i = 1; i < dataCount; i++) // start from 1. at index [0] was scene { auto& objData = data[i]; auto obj = objects[i]; @@ -1049,7 +1049,7 @@ bool Level::loadScene(rapidjson_flax::Value& data, int32 engineBuild, Scene** ou PROFILE_CPU_NAMED("Initialize"); SceneObject** objects = sceneObjects->Get(); - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount; i++) { SceneObject* obj = objects[i]; if (obj) diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index 569a7ce9b..35b02bf40 100644 --- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp @@ -1230,14 +1230,14 @@ bool Prefab::SyncChangesInternal(PrefabInstancesData& prefabInstancesData) { ScopeLock lock(Locker); _isCreatingDefaultInstance = true; - _defaultInstance = PrefabManager::SpawnPrefab(this, Transform::Identity, nullptr, &ObjectsCache, true); + _defaultInstance = PrefabManager::SpawnPrefab(this, nullptr, &ObjectsCache, true); _isCreatingDefaultInstance = false; } // Instantiate prefab instance from prefab (default spawning logic) // Note: it will get any added or removed objects from the nested prefabs // TODO: try to optimize by using recreated default instance to ApplyAllInternal (will need special path there if apply is done with default instance to unlink it instead of destroying) - const auto targetActor = PrefabManager::SpawnPrefab(this, Transform::Identity, nullptr, nullptr, true); + const auto targetActor = PrefabManager::SpawnPrefab(this, nullptr, nullptr, true); if (targetActor == nullptr) { LOG(Warning, "Failed to instantiate default prefab instance from changes synchronization."); diff --git a/Source/Engine/Level/Prefabs/Prefab.cpp b/Source/Engine/Level/Prefabs/Prefab.cpp index 902bcf06c..25a2bd086 100644 --- a/Source/Engine/Level/Prefabs/Prefab.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.cpp @@ -76,7 +76,7 @@ Actor* Prefab::GetDefaultInstance() _isCreatingDefaultInstance = true; // Instantiate objects from prefab (default spawning logic) - _defaultInstance = PrefabManager::SpawnPrefab(this, Transform(Vector3::Minimum), nullptr, &ObjectsCache); + _defaultInstance = PrefabManager::SpawnPrefab(this, nullptr, &ObjectsCache); _isCreatingDefaultInstance = false; return _defaultInstance; @@ -87,17 +87,12 @@ SceneObject* Prefab::GetDefaultInstance(const Guid& objectId) const auto result = GetDefaultInstance(); if (!result) return nullptr; - if (objectId.IsValid()) { SceneObject* object; if (ObjectsCache.TryGet(objectId, object)) - { - // Actor or Script return object; - } } - return result; } diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp index bb4710bc7..d99a89e39 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.cpp +++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp @@ -94,8 +94,8 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac LOG(Warning, "Waiting for prefab asset be loaded failed. {0}", prefab->ToString()); return nullptr; } - const int32 objectsCount = prefab->ObjectsCount; - if (objectsCount == 0) + const int32 dataCount = prefab->ObjectsCount; + if (dataCount == 0) { LOG(Warning, "Prefab has no objects. {0}", prefab->ToString()); return nullptr; @@ -107,7 +107,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac // Prepare CollectionPoolCache::ScopeCache sceneObjects = ActorsCache::SceneObjectsListCache.Get(); - sceneObjects->Resize(objectsCount); + sceneObjects->Resize(dataCount); CollectionPoolCache::ScopeCache modifier = Cache::ISerializeModifier.Get(); modifier->EngineBuild = prefab->DataEngineBuild; modifier->IdsMapping.EnsureCapacity(prefab->ObjectsIds.Count() * 4); @@ -126,7 +126,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac // Deserialize prefab objects auto prevIdMapping = Scripting::ObjectsLookupIdMapping.Get(); Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping); - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount; i++) { auto& stream = data[i]; SceneObject* obj = SceneObjectsFactory::Spawn(context, stream); @@ -145,7 +145,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac SceneObjectsFactory::SynchronizeNewPrefabInstances(context, prefabSyncData); Scripting::ObjectsLookupIdMapping.Set(&modifier.Value->IdsMapping); } - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount; i++) { auto& stream = data[i]; SceneObject* obj = sceneObjects->At(i); @@ -154,28 +154,6 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac } Scripting::ObjectsLookupIdMapping.Set(prevIdMapping); - // Pick prefab root object - if (sceneObjects->IsEmpty()) - { - LOG(Warning, "No valid objects in prefab."); - return nullptr; - } - Actor* root = nullptr; - const Guid prefabRootObjectId = prefab->GetRootObjectId(); - for (int32 i = 0; i < objectsCount; i++) - { - if (JsonTools::GetGuid(data[i], "ID") == prefabRootObjectId) - { - root = dynamic_cast(sceneObjects->At(i)); - break; - } - } - if (!root) - { - LOG(Warning, "Missing prefab root object."); - return nullptr; - } - // Synchronize prefab instances (prefab may have new objects added or some removed so deserialized instances need to synchronize with it) if (withSynchronization) { @@ -183,6 +161,30 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac SceneObjectsFactory::SynchronizePrefabInstances(context, prefabSyncData); } + // Pick prefab root object + Actor* root = nullptr; + const Guid prefabRootObjectId = prefab->GetRootObjectId(); + for (int32 i = 0; i < dataCount && !root; i++) + { + if (JsonTools::GetGuid(data[i], "ID") == prefabRootObjectId) + root = dynamic_cast(sceneObjects->At(i)); + } + if (!root) + { + // Fallback to the first actor that has no parent + for (int32 i = 0; i < sceneObjects->Count() && !root; i++) + { + SceneObject* obj = sceneObjects->At(i); + if (obj && !obj->GetParent()) + root = dynamic_cast(obj); + } + } + if (!root) + { + LOG(Warning, "Missing prefab root object. {0}", prefab->ToString()); + return nullptr; + } + // Prepare parent linkage for prefab root actor if (root->_parent) root->_parent->Children.Remove(root); @@ -264,7 +266,7 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Ac } // Link objects to prefab (only deserialized from prefab data) - for (int32 i = 0; i < objectsCount; i++) + for (int32 i = 0; i < dataCount; i++) { auto& stream = data[i]; SceneObject* obj = sceneObjects->At(i); diff --git a/Source/Engine/Level/Prefabs/PrefabManager.h b/Source/Engine/Level/Prefabs/PrefabManager.h index e5600bac4..ad990da9f 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.h +++ b/Source/Engine/Level/Prefabs/PrefabManager.h @@ -89,7 +89,7 @@ API_CLASS(Static) class FLAXENGINE_API PrefabManager /// The options output objects cache that can be filled with prefab object id mapping to deserialized object (actor or script). /// True if perform prefab changes synchronization for the spawned objects. It will check if need to add new objects due to nested prefab modifications. /// The created actor (root) or null if failed. - static Actor* SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false); + static Actor* SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization = true); /// /// Spawns the instance of the prefab objects. If parent actor is specified then created actors are fully initialized (OnLoad event and BeginPlay is called if parent actor is already during gameplay). @@ -100,7 +100,7 @@ API_CLASS(Static) class FLAXENGINE_API PrefabManager /// The options output objects cache that can be filled with prefab object id mapping to deserialized object (actor or script). /// True if perform prefab changes synchronization for the spawned objects. It will check if need to add new objects due to nested prefab modifications. /// The created actor (root) or null if failed. - static Actor* SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false); + static Actor* SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization = true); #if USE_EDITOR diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index df30257f9..d9c86d250 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -138,7 +138,7 @@ SceneObject* SceneObjectsFactory::Spawn(Context& context, const ISerializable::D auto prefab = Content::LoadAsync(prefabId); if (prefab == nullptr) { - LOG(Warning, "Missing prefab with id={0}.", prefabId); + LOG(Warning, "Missing prefab {0}.", prefabId); return nullptr; } if (prefab->WaitForLoaded()) @@ -264,7 +264,7 @@ void SceneObjectsFactory::Deserialize(Context& context, SceneObject* obj, ISeria auto prefab = Content::LoadAsync(prefabId); if (prefab == nullptr) { - LOG(Warning, "Missing prefab with id={0}.", prefabId); + LOG(Warning, "Missing prefab {0}.", prefabId); return; } if (prefab->WaitForLoaded()) @@ -317,11 +317,13 @@ void SceneObjectsFactory::HandleObjectDeserializationError(const ISerializable:: { #if USE_EDITOR // Add dummy script - auto* dummyScript = parent->AddScript(); const auto typeNameMember = value.FindMember("TypeName"); if (typeNameMember != value.MemberEnd() && typeNameMember->value.IsString()) + { + auto* dummyScript = parent->AddScript(); dummyScript->MissingTypeName = typeNameMember->value.GetString(); - dummyScript->Data = MoveTemp(bufferStr); + dummyScript->Data = MoveTemp(bufferStr); + } #endif LOG(Warning, "Parent actor of the missing object: {0}", parent->GetName()); } @@ -424,9 +426,6 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyn ASSERT(count <= data.SceneObjects.Count()); for (int32 i = 0; i < count; i++) { - const SceneObject* obj = data.SceneObjects[i]; - if (!obj) - continue; const auto& stream = data.Data[i]; Guid prefabObjectId, prefabId; if (!JsonTools::GetGuidIfValid(prefabObjectId, stream, "PrefabObjectID")) @@ -436,15 +435,18 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyn Guid parentId = JsonTools::GetGuid(stream, "ParentID"); for (int32 j = i - 1; j >= 0; j--) { - // Find instance ID of the parent to this object (use data in json for relationship) + // Find ID of the parent to this object (use data in json for relationship) if (parentId == JsonTools::GetGuid(data.Data[j], "ID") && data.SceneObjects[j]) { parentId = data.SceneObjects[j]->GetID(); break; } } - const Guid id = obj->GetID(); + const SceneObject* obj = data.SceneObjects[i]; + const Guid id = obj ? obj->GetID() : JsonTools::GetGuid(stream, "ID"); auto prefab = Content::LoadAsync(prefabId); + if (!prefab) + continue; // Check if it's parent is in the same prefab int32 index; @@ -459,6 +461,8 @@ void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyn auto& e = context.Instances.AddOne(); e.Prefab = prefab; e.RootId = id; + e.RootIndex = i; + e.StatIndex = i; } context.ObjectToInstance[id] = index; @@ -490,6 +494,85 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, Prefab Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping); data.InitialCount = data.SceneObjects.Count(); + // Recreate any missing prefab root objects that were deleted (eg. spawned prefab got its root changed and deleted so old prefab instance needs to respawn it) + for (int32 instanceIndex = 0; instanceIndex < context.Instances.Count(); instanceIndex++) + { + PrefabInstance& instance = context.Instances[instanceIndex]; + SceneObject* root = data.SceneObjects[instance.RootIndex]; + if (!root && instance.Prefab) + { + instance.FixRootParent = true; + + // Check if current prefab root existed in the deserialized data + const auto& oldRootData = data.Data[instance.RootIndex]; + const Guid oldRootId = JsonTools::GetGuid(oldRootData, "ID"); + const Guid prefabObjectId = JsonTools::GetGuid(oldRootData, "PrefabObjectID"); + const Guid prefabRootId = instance.Prefab->GetRootObjectId(); + Guid id; + int32 idInstance = -1; + bool syncNewRoot = false; + if (instance.IdsMapping.TryGet(prefabRootId, id) && context.ObjectToInstance.TryGet(id, idInstance) && idInstance == instanceIndex) + { + // Update the missing root with the valid object from this prefab instance + LOG(Warning, "Changed prefab instance root from ID={0}, PrefabObjectID={1} to ID={2}, PrefabObjectID={3} ({4})", instance.RootId, prefabObjectId, id, prefabRootId, instance.Prefab->ToString()); + } + else + { + LOG(Warning, "Missing prefab instance root (ID={0}, PrefabObjectID={1}, {2})", instance.RootId, prefabObjectId, instance.Prefab->ToString()); + + // Get prefab object data from the prefab + const ISerializable::DeserializeStream* prefabData; + if (!instance.Prefab->ObjectsDataCache.TryGet(prefabRootId, prefabData)) + { + LOG(Warning, "Missing object {1} data in prefab {0}.", instance.Prefab->ToString(), prefabObjectId); + continue; + } + + // Map prefab object ID to the new prefab object instance + id = Guid::New(); + data.Modifier->IdsMapping[prefabRootId] = id; + + // Create prefab instance (recursive prefab loading to support nested prefabs) + root = Spawn(context, *prefabData); + if (!root) + { + LOG(Warning, "Failed to create object {1} from prefab {0}.", instance.Prefab->ToString(), prefabRootId); + continue; + } + + // Register object + root->RegisterObject(); + data.SceneObjects.Add(root); + auto& newObj = data.NewObjects.AddOne(); + newObj.Prefab = instance.Prefab; + newObj.PrefabData = prefabData; + newObj.PrefabObjectId = prefabRootId; + newObj.Id = id; + context.ObjectToInstance[id] = instanceIndex; + syncNewRoot = true; + } + + // Update prefab root info + instance.RootId = id; + instance.RootIndex = data.SceneObjects.Find(Scripting::FindObject(id)); + CHECK(instance.RootIndex != -1); + + // Remap removed prefab root into the current root (can be different type but is needed for proper hierarchy linkage) + instance.IdsMapping[prefabObjectId] = id; + instance.IdsMapping[prefabRootId] = id; + instance.IdsMapping[oldRootId] = id; + data.Modifier->IdsMapping[prefabObjectId] = id; + data.Modifier->IdsMapping[prefabRootId] = id; + data.Modifier->IdsMapping[oldRootId] = id; + + // Add any sub-objects that are missing (in case new root was created) + if (syncNewRoot) + { + SynchronizeNewPrefabInstances(context, data, instance.Prefab, (Actor*)root, prefabRootId, instance.RootIndex, oldRootData); + } + } + } + // Check all actors with prefab linkage for adding missing objects for (int32 i = 0; i < data.InitialCount; i++) { @@ -504,13 +587,12 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, Prefab continue; if (!JsonTools::GetGuidIfValid(actorId, stream, "ID")) continue; - const Guid actorParentId = JsonTools::GetGuid(stream, "ParentID"); // Load prefab auto prefab = Content::LoadAsync(prefabId); if (prefab == nullptr) { - LOG(Warning, "Missing prefab with id={0}.", prefabId); + LOG(Warning, "Missing prefab {0}.", prefabId); continue; } if (prefab->WaitForLoaded()) @@ -519,66 +601,7 @@ void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, Prefab continue; } - // Check for RemovedObjects list - const auto removedObjects = SERIALIZE_FIND_MEMBER(stream, "RemovedObjects"); - - // Check if the given actor has new children or scripts added (inside the prefab that it uses) - // TODO: consider caching prefab objects structure maybe to boost this logic? - for (auto it = prefab->ObjectsDataCache.Begin(); it.IsNotEnd(); ++it) - { - // Use only objects that are linked to the current actor - const Guid parentId = JsonTools::GetGuid(*it->Value, "ParentID"); - if (parentId != actorPrefabObjectId) - continue; - - // Skip if object was marked to be removed per instance - const Guid prefabObjectId = JsonTools::GetGuid(*it->Value, "ID"); - if (removedObjects != stream.MemberEnd()) - { - auto& list = removedObjects->value; - const int32 size = static_cast(list.Size()); - bool removed = false; - for (int32 j = 0; j < size; j++) - { - if (JsonTools::GetGuid(list[j]) == prefabObjectId) - { - removed = true; - break; - } - } - if (removed) - continue; - } - - // Use only objects that are missing - bool spawned = false; - for (int32 j = i + 1; j < data.InitialCount; j++) - { - const auto& jData = data.Data[j]; - const Guid jParentId = JsonTools::GetGuid(jData, "ParentID"); - //if (jParentId == actorParentId) - // break; - //if (jParentId != actorId) - // continue; - const Guid jPrefabObjectId = JsonTools::GetGuid(jData, "PrefabObjectID"); - if (jPrefabObjectId != prefabObjectId) - continue; - - // This object exists in the saved scene objects list - spawned = true; - break; - } - if (spawned) - continue; - - // Map prefab object id to this actor's prefab instance so the new objects gets added to it - context.SetupIdsMapping(actor, data.Modifier); - data.Modifier->IdsMapping[actorPrefabObjectId] = actor->GetID(); - Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping); - - // Create instance (including all children) - SynchronizeNewPrefabInstance(context, data, prefab, actor, prefabObjectId); - } + SynchronizeNewPrefabInstances(context, data, prefab, actor, actorPrefabObjectId, i, stream); } Scripting::ObjectsLookupIdMapping.Set(nullptr); @@ -605,7 +628,7 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn auto prefab = Content::LoadAsync(prefabId); if (prefab == nullptr) { - LOG(Warning, "Missing prefab with id={0}.", prefabId); + LOG(Warning, "Missing prefab {0}.", prefabId); continue; } if (prefab->WaitForLoaded()) @@ -626,7 +649,7 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn // Invalid connection object found! LOG(Info, "Object {0} has invalid parent object {4} -> {5} (PrefabObjectID: {1}, PrefabID: {2}, Path: {3})", obj->GetSceneObjectId(), prefabObjectId, prefab->GetID(), prefab->GetPath(), parentPrefabObjectId, actualParentPrefabId); - // Map actual prefab object id to the current scene objects collection + // Map actual prefab object ID to the current scene objects collection context.SetupIdsMapping(obj, data.Modifier); data.Modifier->IdsMapping.TryGet(actualParentPrefabId, actualParentPrefabId); @@ -646,15 +669,12 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn if (i != 0) { const auto defaultInstance = prefab->GetDefaultInstance(obj->GetPrefabObjectID()); - if (defaultInstance) - { - obj->SetOrderInParent(defaultInstance->GetOrderInParent()); - } + const int32 order = defaultInstance ? defaultInstance->GetOrderInParent() : -1; + if (order != -1) + obj->SetOrderInParent(order); } } - Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping); - // Synchronize new prefab objects for (int32 i = 0; i < data.NewObjects.Count(); i++) { @@ -662,20 +682,97 @@ void SceneObjectsFactory::SynchronizePrefabInstances(Context& context, PrefabSyn auto& newObj = data.NewObjects[i]; // Deserialize object with prefab data + Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping); Deserialize(context, obj, *(ISerializable::DeserializeStream*)newObj.PrefabData); obj->LinkPrefab(newObj.Prefab->GetID(), newObj.PrefabObjectId); // Preserve order in parent (values from prefab are used) const auto defaultInstance = newObj.Prefab->GetDefaultInstance(newObj.PrefabObjectId); - if (defaultInstance) + const int32 order = defaultInstance ? defaultInstance->GetOrderInParent() : -1; + if (order != -1) + obj->SetOrderInParent(order); + } + + // Setup hierarchy for the prefab instances (ensure any new objects are connected) + for (const auto& instance : context.Instances) + { + const auto& prefabStartData = data.Data[instance.StatIndex]; + Guid prefabStartParentId; + if (instance.FixRootParent && JsonTools::GetGuidIfValid(prefabStartParentId, prefabStartData, "ParentID")) { - obj->SetOrderInParent(defaultInstance->GetOrderInParent()); + auto* root = data.SceneObjects[instance.RootIndex]; + const auto rootParent = Scripting::FindObject(prefabStartParentId); + root->SetParent(rootParent, false); } } Scripting::ObjectsLookupIdMapping.Set(nullptr); } +void SceneObjectsFactory::SynchronizeNewPrefabInstances(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& actorPrefabObjectId, int32 i, const ISerializable::DeserializeStream& stream) +{ + // Check for RemovedObjects list + const auto removedObjects = SERIALIZE_FIND_MEMBER(stream, "RemovedObjects"); + + // Check if the given actor has new children or scripts added (inside the prefab that it uses) + // TODO: consider caching prefab objects structure maybe to boost this logic? + for (auto it = prefab->ObjectsDataCache.Begin(); it.IsNotEnd(); ++it) + { + // Use only objects that are linked to the current actor + const Guid parentId = JsonTools::GetGuid(*it->Value, "ParentID"); + if (parentId != actorPrefabObjectId) + continue; + + // Skip if object was marked to be removed per instance + const Guid prefabObjectId = JsonTools::GetGuid(*it->Value, "ID"); + if (removedObjects != stream.MemberEnd()) + { + auto& list = removedObjects->value; + const int32 size = static_cast(list.Size()); + bool removed = false; + for (int32 j = 0; j < size; j++) + { + if (JsonTools::GetGuid(list[j]) == prefabObjectId) + { + removed = true; + break; + } + } + if (removed) + continue; + } + + // Use only objects that are missing + bool spawned = false; + int32 childSearchStart = i + 1; // Objects are serialized with parent followed by its children + int32 instanceIndex = -1; + if (context.ObjectToInstance.TryGet(actor->GetID(), instanceIndex) && context.Instances[instanceIndex].Prefab == prefab) + { + // Start searching from the beginning of that prefab instance (eg. in case prefab objects were reordered) + childSearchStart = Math::Min(childSearchStart, context.Instances[instanceIndex].StatIndex); + } + for (int32 j = childSearchStart; j < data.InitialCount; j++) + { + if (JsonTools::GetGuid(data.Data[j], "PrefabObjectID") == prefabObjectId) + { + // This object exists in the saved scene objects list + spawned = true; + break; + } + } + if (spawned) + continue; + + // Map prefab object ID to this actor's prefab instance so the new objects gets added to it + context.SetupIdsMapping(actor, data.Modifier); + data.Modifier->IdsMapping[actorPrefabObjectId] = actor->GetID(); + Scripting::ObjectsLookupIdMapping.Set(&data.Modifier->IdsMapping); + + // Create instance (including all children) + SynchronizeNewPrefabInstance(context, data, prefab, actor, prefabObjectId); + } +} + void SceneObjectsFactory::SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId) { PROFILE_CPU_NAMED("SynchronizeNewPrefabInstance"); diff --git a/Source/Engine/Level/SceneObjectsFactory.h b/Source/Engine/Level/SceneObjectsFactory.h index 86fd5364a..e152c21b2 100644 --- a/Source/Engine/Level/SceneObjectsFactory.h +++ b/Source/Engine/Level/SceneObjectsFactory.h @@ -15,8 +15,11 @@ class FLAXENGINE_API SceneObjectsFactory public: struct PrefabInstance { + int32 StatIndex; + int32 RootIndex; Guid RootId; Prefab* Prefab; + bool FixRootParent = false; Dictionary IdsMapping; }; @@ -70,6 +73,8 @@ public: struct PrefabSyncData { friend SceneObjectsFactory; + friend class PrefabManager; + // The created scene objects. Collection can be modified (eg. for spawning missing objects). Array& SceneObjects; // The scene objects data. @@ -124,5 +129,6 @@ public: static void SynchronizePrefabInstances(Context& context, PrefabSyncData& data); private: + static void SynchronizeNewPrefabInstances(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& actorPrefabObjectId, int32 i, const ISerializable::DeserializeStream& stream); static void SynchronizeNewPrefabInstance(Context& context, PrefabSyncData& data, Prefab* prefab, Actor* actor, const Guid& prefabObjectId); }; diff --git a/Source/Engine/Level/Tags.cs b/Source/Engine/Level/Tags.cs index 1ff9acef4..11815aa9d 100644 --- a/Source/Engine/Level/Tags.cs +++ b/Source/Engine/Level/Tags.cs @@ -45,7 +45,7 @@ namespace FlaxEngine /// True if both values are not equal, otherwise false. public static bool operator !=(Tag left, Tag right) { - return left.Index == right.Index; + return left.Index != right.Index; } /// @@ -213,11 +213,11 @@ namespace FlaxEngine } /// - /// Checks if the list of tags contains all of the given tags (including parent tags check). For example, HasAll({"A.B", "C"}, {"A", "C"}) returns true, for exact check use HasAllExact. + /// Checks if the list of tags contains all the given tags (including parent tags check). For example, HasAll({"A.B", "C"}, {"A", "C"}) returns true, for exact check use HasAllExact. /// /// The tags list to use. /// The tags to check. - /// True if all of the given tags are contained by the list of tags. Returns true for empty list. + /// True if all the given tags are contained by the list of tags. Returns true for empty list. public static bool HasAll(this Tag[] list, Tag[] tags) { if (tags == null || tags.Length == 0) @@ -233,11 +233,11 @@ namespace FlaxEngine } /// - /// Checks if the list of tags contains all of the given tags (exact match). For example, HasAllExact({"A.B", "C"}, {"A", "C"}) returns false, for parents check use HasAll. + /// Checks if the list of tags contains all the given tags (exact match). For example, HasAllExact({"A.B", "C"}, {"A", "C"}) returns false, for parents check use HasAll. /// /// The tags list to use. /// The tags to check. - /// True if all of the given tags are contained by the list of tags. Returns true for empty list. + /// True if all the given tags are contained by the list of tags. Returns true for empty list. public static bool HasAllExact(this Tag[] list, Tag[] tags) { if (tags == null || tags.Length == 0) diff --git a/Source/Engine/Networking/NetworkReplicator.cpp b/Source/Engine/Networking/NetworkReplicator.cpp index 1e9ea5b79..c6311cd7a 100644 --- a/Source/Engine/Networking/NetworkReplicator.cpp +++ b/Source/Engine/Networking/NetworkReplicator.cpp @@ -817,7 +817,7 @@ void InvokeObjectSpawn(const NetworkMessageObjectSpawn& msgData, const NetworkMe NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to find prefab {}", msgData.PrefabId.ToString()); return; } - prefabInstance = PrefabManager::SpawnPrefab(prefab, Transform::Identity, nullptr, nullptr); + prefabInstance = PrefabManager::SpawnPrefab(prefab, nullptr, nullptr); if (!prefabInstance) { NETWORK_REPLICATOR_LOG(Error, "[NetworkReplicator] Failed to spawn object type {}", msgData.PrefabId.ToString()); diff --git a/Source/Engine/Physics/Colliders/BoxCollider.cpp b/Source/Engine/Physics/Colliders/BoxCollider.cpp index b80b68de0..d74b3efba 100644 --- a/Source/Engine/Physics/Colliders/BoxCollider.cpp +++ b/Source/Engine/Physics/Colliders/BoxCollider.cpp @@ -2,6 +2,7 @@ #include "BoxCollider.h" #include "Engine/Physics/PhysicsBackend.h" +#include "Engine/Level/Scene/Scene.h" BoxCollider::BoxCollider(const SpawnParams& params) : Collider(params) @@ -19,6 +20,32 @@ void BoxCollider::SetSize(const Float3& value) UpdateBounds(); } +void BoxCollider::AutoResize() +{ + Actor* parent = GetParent(); + if (Cast(parent)) + return; + + // Get bounds of all siblings (excluding itself) + const Vector3 parentScale = parent->GetScale(); + if (parentScale.IsAnyZero()) + return; // Avoid division by zero + BoundingBox parentBox = parent->GetBox(); + for (const Actor* sibling : parent->Children) + { + if (sibling != this) + BoundingBox::Merge(parentBox, sibling->GetBoxWithChildren(), parentBox); + } + const Vector3 parentSize = parentBox.GetSize(); + const Vector3 parentCenter = parentBox.GetCenter() - parent->GetPosition(); + + // Update bounds + SetLocalPosition(Vector3::Zero); + SetSize(parentSize / parentScale); + SetCenter(parentCenter / parentScale); + SetOrientation(GetOrientation() * Quaternion::Invert(GetOrientation())); +} + #if USE_EDITOR #include "Engine/Debug/DebugDraw.h" diff --git a/Source/Engine/Physics/Colliders/BoxCollider.h b/Source/Engine/Physics/Colliders/BoxCollider.h index 5bcc21b45..3c15ce640 100644 --- a/Source/Engine/Physics/Colliders/BoxCollider.h +++ b/Source/Engine/Physics/Colliders/BoxCollider.h @@ -43,6 +43,11 @@ public: return _bounds; } + /// + /// Resizes the collider based on the bounds of it's parent to contain it whole (including any siblings). + /// + API_FUNCTION() void AutoResize(); + public: // [Collider] #if USE_EDITOR diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.cpp b/Source/Engine/Platform/Linux/LinuxPlatform.cpp index a7634710f..41a2a33f6 100644 --- a/Source/Engine/Platform/Linux/LinuxPlatform.cpp +++ b/Source/Engine/Platform/Linux/LinuxPlatform.cpp @@ -94,6 +94,7 @@ X11::XcursorImage* CursorsImg[(int32)CursorType::MAX]; Dictionary KeyNameMap; Array KeyCodeMap; Delegate LinuxPlatform::xEventRecieved; +Window* MouseTrackingWindow = nullptr; // Message boxes configuration #define LINUX_DIALOG_MIN_BUTTON_WIDTH 64 @@ -1916,7 +1917,6 @@ bool LinuxPlatform::Init() { if (PlatformBase::Init()) return true; - char fileNameBuffer[1024]; // Init timing @@ -2398,7 +2398,7 @@ void LinuxPlatform::Tick() // Update input context focus X11::XSetICFocus(IC); window = WindowsManager::GetByNativePtr((void*)event.xfocus.window); - if (window) + if (window && MouseTrackingWindow == nullptr) { window->OnGotFocus(); } @@ -2407,7 +2407,7 @@ void LinuxPlatform::Tick() // Update input context focus X11::XUnsetICFocus(IC); window = WindowsManager::GetByNativePtr((void*)event.xfocus.window); - if (window) + if (window && MouseTrackingWindow == nullptr) { window->OnLostFocus(); } @@ -2514,23 +2514,32 @@ void LinuxPlatform::Tick() break; case ButtonPress: window = WindowsManager::GetByNativePtr((void*)event.xbutton.window); - if (window) + if (MouseTrackingWindow) + MouseTrackingWindow->OnButtonPress(&event.xbutton); + else if (window) window->OnButtonPress(&event.xbutton); break; case ButtonRelease: window = WindowsManager::GetByNativePtr((void*)event.xbutton.window); - if (window) + if (MouseTrackingWindow) + MouseTrackingWindow->OnButtonRelease(&event.xbutton); + else if (window) window->OnButtonRelease(&event.xbutton); break; case MotionNotify: window = WindowsManager::GetByNativePtr((void*)event.xmotion.window); - if (window) + if (MouseTrackingWindow) + MouseTrackingWindow->OnMotionNotify(&event.xmotion); + else if (window) window->OnMotionNotify(&event.xmotion); break; case EnterNotify: + // nothing? break; case LeaveNotify: window = WindowsManager::GetByNativePtr((void*)event.xcrossing.window); + if (MouseTrackingWindow) + MouseTrackingWindow->OnLeaveNotify(&event.xcrossing); if (window) window->OnLeaveNotify(&event.xcrossing); break; diff --git a/Source/Engine/Platform/Linux/LinuxWindow.cpp b/Source/Engine/Platform/Linux/LinuxWindow.cpp index b3bae0276..9004ed84c 100644 --- a/Source/Engine/Platform/Linux/LinuxWindow.cpp +++ b/Source/Engine/Platform/Linux/LinuxWindow.cpp @@ -40,6 +40,7 @@ extern X11::Atom xAtomWmName; extern Dictionary KeyNameMap; extern Array KeyCodeMap; extern X11::Cursor Cursors[(int32)CursorType::MAX]; +extern Window* MouseTrackingWindow; static constexpr uint32 MouseDoubleClickTime = 500; static constexpr uint32 MaxDoubleClickDistanceSquared = 10; @@ -54,7 +55,7 @@ LinuxWindow::LinuxWindow(const CreateWindowSettings& settings) return; auto screen = XDefaultScreen(display); - // Cache data + // Cache data int32 width = Math::TruncToInt(settings.Size.X); int32 height = Math::TruncToInt(settings.Size.Y); _clientSize = Float2((float)width, (float)height); @@ -111,6 +112,12 @@ LinuxWindow::LinuxWindow(const CreateWindowSettings& settings) windowAttributes.border_pixel = XBlackPixel(display, screen); windowAttributes.event_mask = KeyPressMask | KeyReleaseMask | StructureNotifyMask | ExposureMask; + if (!settings.IsRegularWindow) + { + windowAttributes.save_under = true; + windowAttributes.override_redirect = true; + } + // TODO: implement all window settings /* bool Fullscreen; @@ -118,11 +125,16 @@ LinuxWindow::LinuxWindow(const CreateWindowSettings& settings) bool AllowMaximize; */ + unsigned long valueMask = CWBackPixel | CWBorderPixel | CWEventMask | CWColormap; + if (!settings.IsRegularWindow) + { + valueMask |= CWOverrideRedirect | CWSaveUnder; + } const X11::Window window = X11::XCreateWindow( display, X11::XRootWindow(display, screen), x, y, width, height, 0, visualInfo->depth, InputOutput, visualInfo->visual, - CWBackPixel | CWBorderPixel | CWEventMask | CWColormap, &windowAttributes); + valueMask, &windowAttributes); _window = window; LinuxWindow::SetTitle(settings.Title); @@ -811,12 +823,13 @@ void LinuxWindow::SetTitle(const StringView& title) void LinuxWindow::StartTrackingMouse(bool useMouseScreenOffset) { - // TODO: impl this + MouseTrackingWindow = this; } void LinuxWindow::EndTrackingMouse() { - // TODO: impl this + if (MouseTrackingWindow == this) + MouseTrackingWindow = nullptr; } void LinuxWindow::SetCursor(CursorType type) diff --git a/Source/Engine/Scripting/BinaryModule.cpp b/Source/Engine/Scripting/BinaryModule.cpp index e9465096f..44ef32503 100644 --- a/Source/Engine/Scripting/BinaryModule.cpp +++ b/Source/Engine/Scripting/BinaryModule.cpp @@ -1235,17 +1235,17 @@ bool ManagedBinaryModule::InvokeMethod(void* method, const Variant& instance, Sp const bool withInterfaces = !mMethod->IsStatic() && mMethod->GetParentClass()->IsInterface(); if (!mMethod->IsStatic()) { - // Box instance into C# object + // Box instance into C# object (and validate the type) MObject* instanceObject = MUtils::BoxVariant(instance); - const MClass* instanceObjectClass = MCore::Object::GetClass(instanceObject); - - // Validate instance - if (!instanceObject || !instanceObjectClass->IsSubClassOf(mMethod->GetParentClass(), withInterfaces)) + if (!instanceObject) { - if (!instanceObject) - LOG(Error, "Failed to call method '{0}.{1}' (args count: {2}) without object instance", String(mMethod->GetParentClass()->GetFullName()), String(mMethod->GetName()), parametersCount); - else - LOG(Error, "Failed to call method '{0}.{1}' (args count: {2}) with invalid object instance of type '{3}'", String(mMethod->GetParentClass()->GetFullName()), String(mMethod->GetName()), parametersCount, String(MUtils::GetClassFullname(instanceObject))); + LOG(Error, "Failed to call method '{0}.{1}' (args count: {2}) without object instance", String(mMethod->GetParentClass()->GetFullName()), String(mMethod->GetName()), parametersCount); + return true; + } + const MClass* instanceObjectClass = MCore::Object::GetClass(instanceObject); + if (!instanceObjectClass->IsSubClassOf(mMethod->GetParentClass(), withInterfaces)) + { + LOG(Error, "Failed to call method '{0}.{1}' (args count: {2}) with invalid object instance of type '{3}'", String(mMethod->GetParentClass()->GetFullName()), String(mMethod->GetName()), parametersCount, String(MUtils::GetClassFullname(instanceObject))); return true; } diff --git a/Source/Engine/Serialization/JsonWriter.cpp b/Source/Engine/Serialization/JsonWriter.cpp index 35e4412d4..27ecb672b 100644 --- a/Source/Engine/Serialization/JsonWriter.cpp +++ b/Source/Engine/Serialization/JsonWriter.cpp @@ -481,7 +481,7 @@ void JsonWriter::SceneObject(::SceneObject* obj) } else { - LOG(Warning, "Missing prefab with id={0}.", obj->GetPrefabID()); + LOG(Warning, "Missing prefab {0}.", obj->GetPrefabID()); } } diff --git a/Source/Engine/Streaming/StreamableResource.h b/Source/Engine/Streaming/StreamableResource.h index 866ec8469..27ae0a089 100644 --- a/Source/Engine/Streaming/StreamableResource.h +++ b/Source/Engine/Streaming/StreamableResource.h @@ -111,8 +111,9 @@ public: struct StreamingCache { int64 LastUpdate = 0; - int32 TargetResidency = 0; int64 TargetResidencyChange = 0; + int32 TargetResidency = 0; + bool Error = false; SamplesBuffer QualitySamples; }; @@ -131,7 +132,8 @@ public: /// /// Stops the streaming (eg. on streaming fail). /// - void ResetStreaming(); + /// True if streaming failed. + void ResetStreaming(bool error = true); protected: diff --git a/Source/Engine/Streaming/Streaming.cpp b/Source/Engine/Streaming/Streaming.cpp index 19b845fb1..a7b2cad44 100644 --- a/Source/Engine/Streaming/Streaming.cpp +++ b/Source/Engine/Streaming/Streaming.cpp @@ -84,8 +84,9 @@ void StreamableResource::RequestStreamingUpdate() Streaming.LastUpdate = 0; } -void StreamableResource::ResetStreaming() +void StreamableResource::ResetStreaming(bool error) { + Streaming.Error = error; Streaming.TargetResidency = 0; Streaming.LastUpdate = DateTime::MaxValue().Ticks; } diff --git a/Source/Engine/Terrain/TerrainChunk.cpp b/Source/Engine/Terrain/TerrainChunk.cpp index 737ce08f8..9bf1c1208 100644 --- a/Source/Engine/Terrain/TerrainChunk.cpp +++ b/Source/Engine/Terrain/TerrainChunk.cpp @@ -84,6 +84,8 @@ void TerrainChunk::Draw(const RenderContext& renderContext) const DrawCall drawCall; if (TerrainManager::GetChunkGeometry(drawCall, chunkSize, lod)) return; + if (!_neighbors[0]) + const_cast(this)->CacheNeighbors(); drawCall.InstanceCount = 1; drawCall.Material = _cachedDrawMaterial; renderContext.View.GetWorldMatrix(_transform, drawCall.World); diff --git a/Source/Engine/Tests/TestPrefabs.cpp b/Source/Engine/Tests/TestPrefabs.cpp index 211fea930..ec3eb4af6 100644 --- a/Source/Engine/Tests/TestPrefabs.cpp +++ b/Source/Engine/Tests/TestPrefabs.cpp @@ -412,4 +412,149 @@ TEST_CASE("Prefabs") Content::DeleteAsset(prefabNested); Content::DeleteAsset(prefabBase); } + SECTION("Test Loading Nested Prefab After Changing and Deleting Root") + { + // https://github.com/FlaxEngine/FlaxEngine/issues/2050 + + // Create base prefab with 1 object + AssetReference prefabBase = Content::CreateVirtualAsset(); + REQUIRE(prefabBase); + Guid id; + Guid::Parse("3b3334524c696dcfa93cabacd2a4f404", id); + prefabBase->ChangeID(id); + auto prefabBaseInit = prefabBase->Init(Prefab::TypeName, + "[" + "{" + "\"ID\": \"82ce814f4d913e58eb35ab8b0b7e2eef\"," + "\"TypeName\": \"FlaxEngine.DirectionalLight\"," + "\"Name\": \"New Root\"" + "}," + "{" + "\"ID\": \"f8fbee1349f749396ab6c2ad34f3afec\"," + "\"TypeName\": \"FlaxEngine.Camera\"," + "\"Name\": \"Child 1\"," + "\"ParentID\": \"82ce814f4d913e58eb35ab8b0b7e2eef\"" + "}," + "{" + "\"ID\": \"5632561847cf96fe2e8919848b7eca79\"," + "\"TypeName\": \"FlaxEngine.EmptyActor\"," + "\"Name\": \"Child 1.Child\"," + "\"ParentID\": \"f8fbee1349f749396ab6c2ad34f3afec\"" + "}," + "{" + "\"ID\": \"4e4f3a1847cf96fe2e8919848b7eca79\"," + "\"TypeName\": \"FlaxEngine.UICanvas\"," + "\"Name\": \"Child 2\"," + "\"ParentID\": \"82ce814f4d913e58eb35ab8b0b7e2eef\"" + "}" + "]"); + REQUIRE(!prefabBaseInit); + + // Create nested prefab but with 'old' state where root object is different + AssetReference prefabNested1 = Content::CreateVirtualAsset(); + REQUIRE(prefabNested1); + Guid::Parse("671447e947cbd2deea018a8377636ce6", id); + prefabNested1->ChangeID(id); + auto prefabNestedInit1 = prefabNested1->Init(Prefab::TypeName, + "[" + "{" + "\"ID\": \"597ab8ea43a5c58b8d06f58f9364d261\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"589bcfaa4bd1a53435129480e5bbdb3b\"," + "\"Name\": \"Old Root\"" + "}," + "{" + "\"ID\": \"1a6228d84897ff3b2f444ea263c3657e\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"f8fbee1349f749396ab6c2ad34f3afec\"," + "\"ParentID\": \"597ab8ea43a5c58b8d06f58f9364d261\"" + "}," + "{" + "\"ID\": \"1212124f4d913e58eb35ab8b0b7e2eef\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"82ce814f4d913e58eb35ab8b0b7e2eef\"," + "\"ParentID\": \"597ab8ea43a5c58b8d06f58f9364d261\"," + "\"Name\": \"New Root\"" + "}," + "{" + "\"ID\": \"468028d84897ff3b2f444ea263c3657e\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"2468902349f749396ab6c2ad34f3afec\"," + "\"ParentID\": \"597ab8ea43a5c58b8d06f58f9364d261\"," + "\"Name\": \"Old Child\"" + "}" + "]"); + REQUIRE(!prefabNestedInit1); + + // Create nested prefab but with 'old' state where root object is different and doesn't exist anymore + AssetReference prefabNested2 = Content::CreateVirtualAsset(); + REQUIRE(prefabNested2); + Guid::Parse("b71447e947cbd2deea018a8377636ce6", id); + prefabNested2->ChangeID(id); + auto prefabNestedInit2 = prefabNested2->Init(Prefab::TypeName, + "[" + "{" + "\"ID\": \"597ab8ea43a5c58b8d06f58f9364d261\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"589bcfaa4bd1a53435129480e5bbdb3b\"," + "\"Name\": \"Old Root\"" + "}," + "{" + "\"ID\": \"1a6228d84897ff3b2f444ea263c3657e\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"f8fbee1349f749396ab6c2ad34f3afec\"," + "\"ParentID\": \"597ab8ea43a5c58b8d06f58f9364d261\"" + "}," + "{" + "\"ID\": \"468028d84897ff3b2f444ea263c3657e\"," + "\"PrefabID\": \"3b3334524c696dcfa93cabacd2a4f404\"," + "\"PrefabObjectID\": \"2468902349f749396ab6c2ad34f3afec\"," + "\"ParentID\": \"597ab8ea43a5c58b8d06f58f9364d261\"," + "\"Name\": \"Old Child\"" + "}" + "]"); + REQUIRE(!prefabNestedInit2); + + // Spawn test instances of both prefabs + ScriptingObjectReference instanceBase = PrefabManager::SpawnPrefab(prefabBase); + ScriptingObjectReference instanceNested1 = PrefabManager::SpawnPrefab(prefabNested1); + ScriptingObjectReference instanceNested2 = PrefabManager::SpawnPrefab(prefabNested2); + + // Verify scenario + REQUIRE(instanceBase); + REQUIRE(instanceBase->GetName() == TEXT("New Root")); + REQUIRE(instanceBase->GetChildrenCount() == 2); + REQUIRE(instanceBase->Children[0]->GetName() == TEXT("Child 1")); + REQUIRE(instanceBase->Children[0]->GetChildrenCount() == 1); + REQUIRE(instanceBase->Children[1]->GetName() == TEXT("Child 2")); + REQUIRE(instanceBase->Children[1]->GetChildrenCount() == 0); + REQUIRE(instanceBase->Children[0]->Children[0]->GetName() == TEXT("Child 1.Child")); + REQUIRE(instanceBase->Children[0]->Children[0]->GetChildrenCount() == 0); + REQUIRE(instanceNested1); + REQUIRE(instanceNested1->GetName() == TEXT("New Root")); + REQUIRE(instanceNested1->GetChildrenCount() == 2); + REQUIRE(instanceNested1->Children[0]->GetName() == TEXT("Child 1")); + REQUIRE(instanceNested1->Children[0]->GetChildrenCount() == 1); + REQUIRE(instanceNested1->Children[1]->GetName() == TEXT("Child 2")); + REQUIRE(instanceNested1->Children[1]->GetChildrenCount() == 0); + REQUIRE(instanceNested1->Children[0]->Children[0]->GetName() == TEXT("Child 1.Child")); + REQUIRE(instanceNested1->Children[0]->Children[0]->GetChildrenCount() == 0); + REQUIRE(instanceNested2); + REQUIRE(instanceNested2->GetName() == TEXT("New Root")); + REQUIRE(instanceNested2->GetChildrenCount() == 2); + REQUIRE(instanceNested2->Children[0]->GetName() == TEXT("Child 1")); + REQUIRE(instanceNested2->Children[0]->GetChildrenCount() == 1); + REQUIRE(instanceNested2->Children[1]->GetName() == TEXT("Child 2")); + REQUIRE(instanceNested2->Children[1]->GetChildrenCount() == 0); + REQUIRE(instanceNested2->Children[0]->Children[0]->GetName() == TEXT("Child 1.Child")); + REQUIRE(instanceNested2->Children[0]->Children[0]->GetChildrenCount() == 0); + + // Cleanup + instanceNested2->DeleteObject(); + instanceNested1->DeleteObject(); + instanceBase->DeleteObject(); + Content::DeleteAsset(prefabNested2); + Content::DeleteAsset(prefabNested1); + Content::DeleteAsset(prefabBase); + } } diff --git a/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp b/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp index c8febf0f8..47988f4e8 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp +++ b/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp @@ -10,7 +10,6 @@ #include "Engine/Platform/ConditionVariable.h" #include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/Async/GPUTask.h" -#include "Engine/Graphics/Textures/TextureUtils.h" #include "Engine/Graphics/Textures/TextureData.h" #include "Engine/Graphics/PixelFormatExtensions.h" #if USE_EDITOR @@ -318,7 +317,7 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path textureData.Width = (int32)meta.width; textureData.Height = (int32)meta.height; textureData.Depth = (int32)meta.depth; - textureData.Format = ToPixelFormat(meta.format); + textureData.Format = ::ToPixelFormat(meta.format); textureData.Items.Resize(1); textureData.Items.Resize((int32)meta.arraySize); for (int32 arrayIndex = 0; arrayIndex < (int32)meta.arraySize; arrayIndex++) @@ -598,7 +597,7 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path float alphaThreshold = 0.3f; bool isPowerOfTwo = Math::IsPowerOfTwo(width) && Math::IsPowerOfTwo(height); DXGI_FORMAT sourceDxgiFormat = currentImage->GetMetadata().format; - PixelFormat targetFormat = TextureUtils::ToPixelFormat(options.Type, width, height, options.Compress); + PixelFormat targetFormat = TextureTool::ToPixelFormat(options.Type, width, height, options.Compress); if (options.sRGB) targetFormat = PixelFormatExtensions::TosRGB(targetFormat); DXGI_FORMAT targetDxgiFormat = ToDxgiFormat(targetFormat); @@ -625,19 +624,26 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path } bool keepAsIs = false; - if (!options.FlipY && !options.InvertGreenChannel && options.Compress && type == ImageType::DDS && mipLevels == sourceMipLevels && DirectX::IsCompressed(sourceDxgiFormat) && !DirectX::IsSRGB(sourceDxgiFormat)) + if (!options.FlipY && + !options.InvertGreenChannel && + options.Compress && + type == ImageType::DDS && + mipLevels == sourceMipLevels && + DirectX::IsCompressed(sourceDxgiFormat) && + !DirectX::IsSRGB(sourceDxgiFormat) && + width >= 4 && + height >= 4) { // Keep image in the current compressed format (artist choice) so we don't have to run the slow mipmap generation keepAsIs = true; targetDxgiFormat = sourceDxgiFormat; - targetFormat = ToPixelFormat(currentImage->GetMetadata().format); + targetFormat = ::ToPixelFormat(currentImage->GetMetadata().format); } // Decompress if texture is compressed (next steps need decompressed input data, for eg. mip maps generation or format changing) if (!keepAsIs && DirectX::IsCompressed(sourceDxgiFormat)) { auto& tmpImg = GET_TMP_IMG(); - sourceDxgiFormat = DXGI_FORMAT_R16G16B16A16_FLOAT; result = Decompress(currentImage->GetImages(), currentImage->GetImageCount(), currentImage->GetMetadata(), sourceDxgiFormat, tmpImg); if (FAILED(result)) @@ -645,14 +651,13 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path errorMsg = String::Format(TEXT("Cannot decompress texture, error: {0:x}"), static_cast(result)); return true; } - SET_CURRENT_IMG(tmpImg); } // Fix sRGB problem if (!keepAsIs && DirectX::IsSRGB(sourceDxgiFormat)) { - sourceDxgiFormat = ToDxgiFormat(PixelFormatExtensions::ToNonsRGB(ToPixelFormat(sourceDxgiFormat))); + sourceDxgiFormat = ToDxgiFormat(PixelFormatExtensions::ToNonsRGB(::ToPixelFormat(sourceDxgiFormat))); ((DirectX::TexMetadata&)currentImage->GetMetadata()).format = sourceDxgiFormat; for (size_t i = 0; i < currentImage->GetImageCount(); i++) ((DirectX::Image*)currentImage->GetImages())[i].format = sourceDxgiFormat; @@ -662,12 +667,10 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path if (!keepAsIs && DirectX::HasAlpha(sourceDxgiFormat) && options.Type == TextureFormatType::ColorRGB && options.Compress) { auto& tmpImg = GET_TMP_IMG(); - result = TransformImage(currentImage->GetImages(), currentImage->GetImageCount(), currentImage->GetMetadata(), [](DirectX::XMVECTOR* outPixels, const DirectX::XMVECTOR* inPixels, size_t width, size_t y) { UNREFERENCED_PARAMETER(y); - for (size_t j = 0; j < width; j++) { outPixels[j] = DirectX::XMVectorSelect(DirectX::g_XMOne, inPixels[j], DirectX::g_XMSelect1110); @@ -678,8 +681,6 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path errorMsg = String::Format(TEXT("Cannot transform texture to remove unwanted alpha channel, error: {0:x}"), static_cast(result)); return true; } - - // Use converted image SET_CURRENT_IMG(tmpImg); } @@ -687,7 +688,6 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path if (!keepAsIs && options.FlipY) { auto& tmpImg = GET_TMP_IMG(); - DWORD flags = DirectX::TEX_FR_FLIP_VERTICAL; result = FlipRotate(currentImage->GetImages(), currentImage->GetImageCount(), currentImage->GetMetadata(), flags, tmpImg); if (FAILED(result)) @@ -695,8 +695,6 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path errorMsg = String::Format(TEXT("Cannot rotate/flip texture, error: {0:x}"), static_cast(result)); return true; } - - // Use converted image SET_CURRENT_IMG(tmpImg); } @@ -704,7 +702,6 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path if (!keepAsIs && options.InvertGreenChannel) { auto& timage = GET_TMP_IMG(); - result = TransformImage(currentImage->GetImages(), currentImage->GetImageCount(), currentImage->GetMetadata(), [&](DirectX::XMVECTOR* outPixels, const DirectX::XMVECTOR* inPixels, size_t w, size_t y) { @@ -726,7 +723,6 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path errorMsg = String::Format(TEXT("Cannot invert green channel in texture, error: {0:x}"), static_cast(result)); return true; } - // Use converted image SET_CURRENT_IMG(timage); } @@ -749,7 +745,6 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path errorMsg = String::Format(TEXT("Cannot generate texture mip maps chain, error: {1:x}"), *path, static_cast(result)); return true; } - SET_CURRENT_IMG(tmpImg); } diff --git a/Source/Engine/Tools/TextureTool/TextureTool.cpp b/Source/Engine/Tools/TextureTool/TextureTool.cpp index c1257df55..72e9f4558 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.cpp +++ b/Source/Engine/Tools/TextureTool/TextureTool.cpp @@ -679,6 +679,54 @@ Color TextureTool::SampleLinear(const PixelFormatSampler* sampler, const Float2& return Color::Lerp(Color::Lerp(v00, v01, uvFraction.X), Color::Lerp(v10, v11, uvFraction.X), uvFraction.Y); } +PixelFormat TextureTool::ToPixelFormat(TextureFormatType format, int32 width, int32 height, bool canCompress) +{ + const bool canUseBlockCompression = width % 4 == 0 && height % 4 == 0; + if (canCompress && canUseBlockCompression) + { + switch (format) + { + case TextureFormatType::ColorRGB: + return PixelFormat::BC1_UNorm; + case TextureFormatType::ColorRGBA: + return PixelFormat::BC3_UNorm; + case TextureFormatType::NormalMap: + return PixelFormat::BC5_UNorm; + case TextureFormatType::GrayScale: + return PixelFormat::BC4_UNorm; + case TextureFormatType::HdrRGBA: + return PixelFormat::BC7_UNorm; + case TextureFormatType::HdrRGB: +#if PLATFORM_LINUX + // TODO: support BC6H compression for Linux Editor + return PixelFormat::BC7_UNorm; +#else + return PixelFormat::BC6H_Uf16; +#endif + default: + return PixelFormat::Unknown; + } + } + + switch (format) + { + case TextureFormatType::ColorRGB: + return PixelFormat::R8G8B8A8_UNorm; + case TextureFormatType::ColorRGBA: + return PixelFormat::R8G8B8A8_UNorm; + case TextureFormatType::NormalMap: + return PixelFormat::R16G16_UNorm; + case TextureFormatType::GrayScale: + return PixelFormat::R8_UNorm; + case TextureFormatType::HdrRGBA: + return PixelFormat::R16G16B16A16_Float; + case TextureFormatType::HdrRGB: + return PixelFormat::R11G11B10_Float; + default: + return PixelFormat::Unknown; + } +} + bool TextureTool::GetImageType(const StringView& path, ImageType& type) { const auto extension = FileSystem::GetExtension(path).ToLower(); diff --git a/Source/Engine/Tools/TextureTool/TextureTool.h b/Source/Engine/Tools/TextureTool/TextureTool.h index 061fc2466..913d1b930 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.h +++ b/Source/Engine/Tools/TextureTool/TextureTool.h @@ -110,14 +110,12 @@ API_CLASS(Namespace="FlaxEngine.Tools", Static) class FLAXENGINE_API TextureTool public: #if USE_EDITOR - /// /// Checks whenever the given texture file contains alpha channel data with values different than solid fill of 1 (non fully opaque). /// /// The file path. /// True if has alpha channel, otherwise false. static bool HasAlpha(const StringView& path); - #endif /// @@ -240,6 +238,8 @@ public: /// The sampled color (linear). static Color SampleLinear(const PixelFormatSampler* sampler, const Float2& uv, const void* data, const Int2& size, int32 rowPitch); + static PixelFormat ToPixelFormat(TextureFormatType format, int32 width, int32 height, bool canCompress); + private: enum class ImageType { diff --git a/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp b/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp index ce10a7c61..d45c9d465 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp +++ b/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp @@ -9,7 +9,6 @@ #include "Engine/Serialization/FileWriteStream.h" #include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/Textures/TextureData.h" -#include "Engine/Graphics/Textures/TextureUtils.h" #include "Engine/Graphics/PixelFormatExtensions.h" #include "Engine/Platform/File.h" @@ -459,7 +458,7 @@ bool TextureTool::ImportTextureStb(ImageType type, const StringView& path, Textu // Cache data float alphaThreshold = 0.3f; bool isPowerOfTwo = Math::IsPowerOfTwo(width) && Math::IsPowerOfTwo(height); - PixelFormat targetFormat = TextureUtils::ToPixelFormat(options.Type, width, height, options.Compress); + PixelFormat targetFormat = ToPixelFormat(options.Type, width, height, options.Compress); if (options.sRGB) targetFormat = PixelFormatExtensions::TosRGB(targetFormat); diff --git a/Source/Engine/UI/GUI/ContainerControl.cs b/Source/Engine/UI/GUI/ContainerControl.cs index 69a75aacb..e5b92430f 100644 --- a/Source/Engine/UI/GUI/ContainerControl.cs +++ b/Source/Engine/UI/GUI/ContainerControl.cs @@ -448,6 +448,8 @@ namespace FlaxEngine.GUI internal virtual void AddChildInternal(Control child) { Assert.IsNotNull(child, "Invalid control."); + if (Parent == child) + throw new InvalidOperationException(); // Add child _children.Add(child); @@ -820,13 +822,14 @@ namespace FlaxEngine.GUI protected virtual void DrawChildren() { // Draw all visible child controls + var children = _children; if (_cullChildren) { Render2D.PeekClip(out var globalClipping); Render2D.PeekTransform(out var globalTransform); - for (int i = 0; i < _children.Count; i++) + for (int i = 0; i < children.Count; i++) { - var child = _children[i]; + var child = children[i]; if (child.Visible) { Matrix3x3.Multiply(ref child._cachedTransform, ref globalTransform, out var globalChildTransform); @@ -842,9 +845,9 @@ namespace FlaxEngine.GUI } else { - for (int i = 0; i < _children.Count; i++) + for (int i = 0; i < children.Count; i++) { - var child = _children[i]; + var child = children[i]; if (child.Visible) { Render2D.PushTransform(ref child._cachedTransform); diff --git a/Source/Engine/UI/UIControl.cs b/Source/Engine/UI/UIControl.cs index 219a4fa4e..bf78bf2fd 100644 --- a/Source/Engine/UI/UIControl.cs +++ b/Source/Engine/UI/UIControl.cs @@ -58,10 +58,11 @@ namespace FlaxEngine if (containerControl != null && IsActiveInHierarchy) { var children = ChildrenCount; + var parent = Parent; for (int i = 0; i < children; i++) { var child = GetChild(i) as UIControl; - if (child != null && child.IsActiveInHierarchy && child.HasControl) + if (child != null && child.IsActiveInHierarchy && child.HasControl && child != parent) { child.Control.Parent = containerControl; } diff --git a/Source/Shaders/SSR.shader b/Source/Shaders/SSR.shader index 999dd93a3..91122f2ee 100644 --- a/Source/Shaders/SSR.shader +++ b/Source/Shaders/SSR.shader @@ -106,17 +106,20 @@ float4 PS_RayTracePass(Quad_VS2PS input) : SV_Target0 // SRV 7-8 Global SDF // SRV 9-13 Global Surface Atlas + // Base layer color with reflections from probes but empty alpha so SSR blur will have valid bacground values to smooth with + float4 base = float4(Texture0.SampleLevel(SamplerLinearClamp, input.TexCoord, 0).rgb, 0); + // Sample GBuffer GBufferData gBufferData = GetGBufferData(); GBufferSample gBuffer = SampleGBuffer(gBufferData, input.TexCoord); // Reject invalid pixels if (gBuffer.ShadingModel == SHADING_MODEL_UNLIT || gBuffer.Roughness > RoughnessFade || gBuffer.ViewPos.z > FadeOutDistance) - return 0; + return base; // Trace depth buffer to find intersection float3 screenHit = TraceScreenSpaceReflection(input.TexCoord, gBuffer, Depth, gBufferData.ViewPos, ViewMatrix, ViewProjectionMatrix, RayTraceStep, MaxTraceSamples, TemporalEffect, TemporalTime, WorldAntiSelfOcclusionBias, BRDFBias, FadeOutDistance, RoughnessFade, EdgeFadeFactor); - float4 result = 0; + float4 result = base; if (screenHit.z > 0) { float3 viewVector = normalize(gBufferData.ViewPos - gBuffer.WorldPos); diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs index c678b2521..2d4b0cc02 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs @@ -91,11 +91,6 @@ namespace Flax.Build.Projects.VisualStudio var baseConfiguration = project.Configurations.First(); var baseOutputDir = Utilities.MakePathRelativeTo(project.CSharp.OutputPath ?? baseConfiguration.TargetBuildOptions.OutputFolder, projectDirectory); var baseIntermediateOutputPath = Utilities.MakePathRelativeTo(project.CSharp.IntermediateOutputPath ?? Path.Combine(baseConfiguration.TargetBuildOptions.IntermediateFolder, "CSharp"), projectDirectory); - - bool isMainProject = Globals.Project.ProjectFolderPath == project.WorkspaceRootPath && project.Name != "BuildScripts" && (Globals.Project.Name != "Flax" || project.Name != "FlaxEngine"); - var flaxBuildTargetsFilename = isMainProject ? "Flax.Build.CSharp.targets" : "Flax.Build.CSharp.SkipBuild.targets"; - var cacheProjectsPath = Utilities.MakePathRelativeTo(Path.Combine(Globals.Root, "Cache", "Projects"), projectDirectory); - var flaxBuildTargetsPath = !string.IsNullOrEmpty(cacheProjectsPath) ? Path.Combine(cacheProjectsPath, flaxBuildTargetsFilename) : flaxBuildTargetsFilename; csProjectFileContent.AppendLine($" net{dotnetSdk.Version.Major}.{dotnetSdk.Version.Minor}"); csProjectFileContent.AppendLine(" disable"); @@ -114,7 +109,11 @@ namespace Flax.Build.Projects.VisualStudio //csProjectFileContent.AppendLine(" false"); // TODO: use it to reduce burden of framework libs - // Custom .targets file for overriding MSBuild build tasks + // Custom .targets file for overriding MSBuild build tasks, only invoke Flax.Build once per Flax project + bool isMainProject = Globals.Project.IsCSharpOnlyProject && Globals.Project.ProjectFolderPath == project.WorkspaceRootPath && project.Name != "BuildScripts" && (Globals.Project.Name != "Flax" || project.Name != "FlaxEngine"); + var flaxBuildTargetsFilename = isMainProject ? "Flax.Build.CSharp.targets" : "Flax.Build.CSharp.SkipBuild.targets"; + var cacheProjectsPath = Utilities.MakePathRelativeTo(Path.Combine(Globals.Root, "Cache", "Projects"), projectDirectory); + var flaxBuildTargetsPath = !string.IsNullOrEmpty(cacheProjectsPath) ? Path.Combine(cacheProjectsPath, flaxBuildTargetsFilename) : flaxBuildTargetsFilename; csProjectFileContent.AppendLine(string.Format(" $(MSBuildThisFileDirectory){0}", flaxBuildTargetsPath)); // Hide annoying warnings during build diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs index b04531d9f..5cc1b2205 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/VisualStudioProjectGenerator.cs @@ -702,7 +702,7 @@ namespace Flax.Build.Projects.VisualStudio // Override MSBuild build tasks to run Flax.Build in C#-only projects { // Build command for the build tool - var buildToolPath = Path.ChangeExtension(Utilities.MakePathRelativeTo(typeof(Builder).Assembly.Location, Path.GetDirectoryName(solution.MainProject.Path)), null); + var buildToolPath = Path.ChangeExtension(typeof(Builder).Assembly.Location, null); var targetsFileContent = new StringBuilder(); targetsFileContent.AppendLine(""); @@ -724,16 +724,16 @@ namespace Flax.Build.Projects.VisualStudio { foreach (var configuration in solution.MainProject.Configurations) { - var cmdLine = string.Format("{0} -log -mutex -workspace={1} -arch={2} -configuration={3} -platform={4} -buildTargets={5}", - FixPath(buildToolPath), - FixPath(solution.MainProject.WorkspaceRootPath), + var cmdLine = string.Format("\"{0}\" -log -mutex -workspace=\"{1}\" -arch={2} -configuration={3} -platform={4} -buildTargets={5}", + buildToolPath, + solution.MainProject.WorkspaceRootPath, configuration.Architecture, configuration.Configuration, configuration.Platform, configuration.Target); Configuration.PassArgs(ref cmdLine); - str.AppendLine(string.Format(" ", cmdLine, extraArgs, configuration.Name)); + str.AppendLine(string.Format(" ", cmdLine, extraArgs, configuration.Name)); } } } @@ -774,14 +774,5 @@ namespace Flax.Build.Projects.VisualStudio projects.Add(project); } } - - private static string FixPath(string path) - { - if (path.Contains(' ')) - { - path = "\"" + path + "\""; - } - return path; - } } }