From 004339b81e8d6633c820a3d703d40c8818e21464 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Fri, 2 May 2025 13:48:53 +0300 Subject: [PATCH 01/28] Improve XML documentation reference parsing and overall performance --- .../SourceCodeEditing/CodeDocsModule.cs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs index 48cbda951..09b1f25d1 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs @@ -310,6 +310,7 @@ namespace FlaxEditor.Modules.SourceCodeEditing using (var xmlReader = XmlReader.Create(new StreamReader(xmlFilePath))) { result = new Dictionary(); + StringBuilder content = new StringBuilder(2048); while (xmlReader.Read()) { if (xmlReader.NodeType == XmlNodeType.Element && string.Equals(xmlReader.Name, "member", StringComparison.Ordinal)) @@ -318,9 +319,39 @@ namespace FlaxEditor.Modules.SourceCodeEditing var memberReader = xmlReader.ReadSubtree(); if (memberReader.ReadToDescendant("summary")) { - // Remove and replace them with the captured group (the content of the cref). Additionally, getting rid of prefixes - const string crefPattern = @""; - result[rawName] = Regex.Replace(memberReader.ReadInnerXml(), crefPattern, "$1").Replace('\n', ' ').Trim(); + content.Clear(); + do + { + if (memberReader.NodeType == XmlNodeType.Element && memberReader.Read()) + { + while (memberReader.NodeType == XmlNodeType.Text) + { + content.Append(memberReader.Value); + if (memberReader.Read() && memberReader.NodeType == XmlNodeType.Element) + { + var nodeRef = TrimRef(memberReader.GetAttribute("cref")); // + if (nodeRef == null) + nodeRef = memberReader.GetAttribute("name"); // + content.Append(nodeRef); + memberReader.Read(); + + string TrimRef(string str) + { + if (str == null) + return null; + if (str.IndexOf(":FlaxEngine.") == 1) + return str.Substring("T:FlaxEngine.".Length); + return str.Substring("T:".Length); + } + } + } + } + + if (memberReader.NodeType == XmlNodeType.EndElement && memberReader.Name == "summary") + break; + } while (memberReader.Read()); + + result[rawName] = content.ToString().Trim(' ', '\r', '\n'); } } } From c916fb18444969bafdbe9639430ff749abf3abe7 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Fri, 2 May 2025 13:49:41 +0300 Subject: [PATCH 02/28] Avoid clearing cached XML documentation data for Editor assembly --- Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs index 09b1f25d1..fe50e022f 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs @@ -374,7 +374,12 @@ namespace FlaxEditor.Modules.SourceCodeEditing { _typeCache.Clear(); _memberCache.Clear(); - _xmlCache.Clear(); + + foreach (var asm in _xmlCache.Keys.ToArray()) + { + if (asm.IsCollectible) + _xmlCache.Remove(asm); + } } /// From c2e8e492d76c6013ce2a8e398add1204e152af4b Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Fri, 2 May 2025 14:03:14 +0300 Subject: [PATCH 03/28] Defer loading XML documentation during while loading the editor --- Source/Editor/CustomEditors/CustomEditorPresenter.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/Editor/CustomEditors/CustomEditorPresenter.cs b/Source/Editor/CustomEditors/CustomEditorPresenter.cs index 62db53cab..058e712d0 100644 --- a/Source/Editor/CustomEditors/CustomEditorPresenter.cs +++ b/Source/Editor/CustomEditors/CustomEditorPresenter.cs @@ -325,6 +325,7 @@ namespace FlaxEditor.CustomEditors } private bool _buildOnUpdate; + private bool _initialized; private bool _readOnly; /// @@ -412,6 +413,7 @@ namespace FlaxEditor.CustomEditors ClearLayout(); _buildOnUpdate = false; + _initialized = true; Editor.Setup(this); Panel.IsLayoutLocked = false; @@ -488,7 +490,11 @@ namespace FlaxEditor.CustomEditors /// protected virtual void OnSelectionChanged() { - BuildLayout(); + // Defer building the layout after we have initialized to improve initial loading times + if (!_initialized) + _buildOnUpdate = true; + else + BuildLayout(); SelectionChanged?.Invoke(); } From a7c9eff959af3b4cdf1517b1e52a0fbd19eb1357 Mon Sep 17 00:00:00 2001 From: Saas Date: Sat, 21 Mar 2026 14:49:34 +0100 Subject: [PATCH 04/28] add zooming in and out (FOV) in editor viewport using C and Z --- Source/Editor/Options/InputOptions.cs | 8 +++++++ Source/Editor/Viewport/Cameras/FPSCamera.cs | 23 ++++++++++++++++++++- Source/Editor/Viewport/EditorViewport.cs | 19 ++++++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index 9b15c0c09..899822dab 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -347,6 +347,14 @@ namespace FlaxEditor.Options [EditorDisplay("Viewport"), EditorOrder(1550)] public InputBinding Down = new InputBinding(KeyboardKeys.Q); + [DefaultValue(typeof(InputBinding), "C")] + [EditorDisplay("Viewport"), EditorOrder(1551)] + public InputBinding ZoomIn = new InputBinding(KeyboardKeys.C); + + [DefaultValue(typeof(InputBinding), "Z")] + [EditorDisplay("Viewport"), EditorOrder(1552)] + public InputBinding ZoomOut = new InputBinding(KeyboardKeys.Z); + [DefaultValue(typeof(InputBinding), "None")] [EditorDisplay("Viewport", "Toggle Camera Rotation"), EditorOrder(1560)] public InputBinding CameraToggleRotation = new InputBinding(KeyboardKeys.None); diff --git a/Source/Editor/Viewport/Cameras/FPSCamera.cs b/Source/Editor/Viewport/Cameras/FPSCamera.cs index 26e996c41..228994929 100644 --- a/Source/Editor/Viewport/Cameras/FPSCamera.cs +++ b/Source/Editor/Viewport/Cameras/FPSCamera.cs @@ -21,6 +21,7 @@ namespace FlaxEditor.Viewport.Cameras private Transform _startMove; private Transform _endMove; private float _moveStartTime = -1; + private float _additionalFOV; /// /// Gets a value indicating whether this viewport is animating movement. @@ -32,6 +33,15 @@ namespace FlaxEditor.Viewport.Cameras /// public Vector3 TargetPoint = new Vector3(-200); + /// + /// Additional field of view used for zooming the camera in and out. + /// + public float AdditionalZoomFOV + { + get => _additionalFOV; + private set => _additionalFOV = Mathf.Clamp(value, 5 - Viewport.FieldOfView, 160f - Viewport.FieldOfView); + } + /// /// Sets view. /// @@ -216,7 +226,7 @@ namespace FlaxEditor.Viewport.Cameras pitch += mouseDelta.Y; } - // Zoom in/out + // Zoom in/out with mouse wheel if (input.IsZooming && !input.IsRotating) { position += forward * (Viewport.MouseWheelZoomSpeedFactor * input.MouseWheelDelta * 25.0f); @@ -226,6 +236,17 @@ namespace FlaxEditor.Viewport.Cameras } } + // Zoom in and out by changing FOV + if (input.IsRotating && (input.ZoomInDown || input.ZoomOutDown)) + { + float delta = (input.ZoomInDown ? -0.8f : 0.8f); + AdditionalZoomFOV += delta; + } + else if (!input.IsRotating) + { + AdditionalZoomFOV = 0f; + } + // Move camera with the gizmo if (input.IsOrbiting && isUsingGizmo) { diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 2182c2f55..7c628aa7f 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -50,6 +50,16 @@ namespace FlaxEditor.Viewport /// public bool IsOrbiting; + /// + /// The zoom in state. + /// + public bool ZoomInDown; + + /// + /// The zoom out state. + /// + public bool ZoomOutDown; + /// /// The is control down flag. /// @@ -108,6 +118,10 @@ namespace FlaxEditor.Viewport IsAltDown = window.GetKey(KeyboardKeys.Alt); WasAltDownBefore = prevInput.WasAltDownBefore || prevInput.IsAltDown; + InputOptions inputOptions = Editor.Instance.Options.Options.Input; + ZoomInDown = window.GetKey(inputOptions.ZoomIn.Key); + ZoomOutDown = window.GetKey(inputOptions.ZoomOut.Key); + IsMouseRightDown = useMouse && window.GetMouseButton(MouseButton.Right); IsMouseMiddleDown = useMouse && window.GetMouseButton(MouseButton.Middle); IsMouseLeftDown = useMouse && window.GetMouseButton(MouseButton.Left); @@ -1428,7 +1442,10 @@ namespace FlaxEditor.Viewport else { float aspect = Width / Height; - Matrix.PerspectiveFov(_fieldOfView * Mathf.DegreesToRadians, aspect, _nearPlane, _farPlane, out result); + float fov = _fieldOfView; + if (_camera is FPSCamera fpsCam) + fov += fpsCam.AdditionalZoomFOV; + Matrix.PerspectiveFov(fov * Mathf.DegreesToRadians, aspect, _nearPlane, _farPlane, out result); } } From 913892f3c1678d3df18965588d1b71ad8c36495f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 27 Apr 2026 14:27:32 +0200 Subject: [PATCH 05/28] Fix unnecessary memory allocations within D3D object debug name assignment --- Source/Engine/GraphicsDevice/DirectX/RenderToolsDX.h | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Source/Engine/GraphicsDevice/DirectX/RenderToolsDX.h b/Source/Engine/GraphicsDevice/DirectX/RenderToolsDX.h index f9da07686..f16206c3b 100644 --- a/Source/Engine/GraphicsDevice/DirectX/RenderToolsDX.h +++ b/Source/Engine/GraphicsDevice/DirectX/RenderToolsDX.h @@ -175,16 +175,13 @@ inline void SetDebugObjectName(T* resource, const Char* data, UINT size) if (data && size > 0) resource->SetName(data); #else - char* ansi = (char*)Allocator::Allocate(size + 1); - StringUtils::ConvertUTF162ANSI(data, ansi, size); - ansi[size] = '\0'; - SetDebugObjectName(resource, ansi, size); - Allocator::Free(ansi); + const StringAsANSI<> nameANSI(data, size); + SetDebugObjectName(resource, nameANSI.Get(), size); #endif } template -inline void SetDebugObjectName(T* resource, const String& name) +inline void SetDebugObjectName(T* resource, const StringView& name) { SetDebugObjectName(resource, *name, name.Length()); } @@ -208,7 +205,7 @@ inline void SetDebugObjectName(ComPtr resource, const Char (&name)[NameLength } template -inline void SetDebugObjectName(ComPtr resource, const String& name) +inline void SetDebugObjectName(ComPtr resource, const StringView& name) { SetDebugObjectName(resource.Get(), *name, name.Length()); } From a39b3f486ba3b6f89097b39efe2edc42bdcd0a69 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 27 Apr 2026 14:25:23 +0200 Subject: [PATCH 06/28] Improve foliage count display in editor tab --- Source/Editor/Tools/Foliage/FoliageTypesTab.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Tools/Foliage/FoliageTypesTab.cs b/Source/Editor/Tools/Foliage/FoliageTypesTab.cs index 8e431aa2e..33c3c1fb5 100644 --- a/Source/Editor/Tools/Foliage/FoliageTypesTab.cs +++ b/Source/Editor/Tools/Foliage/FoliageTypesTab.cs @@ -302,7 +302,12 @@ namespace FlaxEditor.Tools.Foliage var proxyObject = (ProxyObject)Values[0]; proxyObject.SyncOptions(); - _info.Text = string.Format("Instances: {0}, Total: {1}", proxyObject.Foliage.GetFoliageTypeInstancesCount(proxyObject.SelectedFoliageTypeIndex), proxyObject.Foliage.InstancesCount); + var instancesCount = proxyObject.Foliage.GetFoliageTypeInstancesCount(proxyObject.SelectedFoliageTypeIndex); + var totalCount = proxyObject.Foliage.InstancesCount; + if (instancesCount == totalCount) + _info.Text = string.Format("Instances: {0:###,###,###}", instancesCount); + else + _info.Text = string.Format("Instances: {0:###,###,###}, Total: {1:###,###,###}", instancesCount, totalCount); base.Refresh(); } From caef258e1a44d4848f08b23ecb7df07d9faad9df Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 27 Apr 2026 16:39:15 +0200 Subject: [PATCH 07/28] Fix missing `ChannelMask` parameter type clone for Material Instance #4070 --- Source/Engine/Graphics/Materials/MaterialParams.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Graphics/Materials/MaterialParams.cpp b/Source/Engine/Graphics/Materials/MaterialParams.cpp index 05bb29c4a..082ac497b 100644 --- a/Source/Engine/Graphics/Materials/MaterialParams.cpp +++ b/Source/Engine/Graphics/Materials/MaterialParams.cpp @@ -507,6 +507,7 @@ void MaterialParameter::clone(const MaterialParameter* param) break; case MaterialParameterType::Integer: case MaterialParameterType::SceneTexture: + case MaterialParameterType::ChannelMask: case MaterialParameterType::TextureGroupSampler: _asInteger = param->_asInteger; break; @@ -647,10 +648,7 @@ bool MaterialParams::Load(ReadStream* stream) PROFILE_MEM(GraphicsMaterials); bool result = false; - // Release - Resize(0); - - // Check for not empty params + Clear(); if (stream != nullptr && stream->CanRead()) { // Version From 94e529e80129fed4da50e397e7cd788f98b9549d Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 1 May 2026 16:27:15 -0500 Subject: [PATCH 08/28] Add way to get blackboard by type. --- Source/Engine/AI/Behavior.h | 9 +++++++++ Source/Engine/AI/BehaviorKnowledge.h | 13 +++++++++++++ Source/Engine/AI/BehaviorTree.cs | 22 ++++++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/Source/Engine/AI/Behavior.h b/Source/Engine/AI/Behavior.h index b1c39ffc6..cdad7331b 100644 --- a/Source/Engine/AI/Behavior.h +++ b/Source/Engine/AI/Behavior.h @@ -57,6 +57,15 @@ public: { return &_knowledge; } + + /// + /// Gets the blackboard of a given type. + /// + template + FORCE_INLINE T* GetBlackboard() + { + return _knowledge.GetBlackboard(); + } /// /// Gets the last behavior tree execution result. diff --git a/Source/Engine/AI/BehaviorKnowledge.h b/Source/Engine/AI/BehaviorKnowledge.h index cd5aeab80..80f04b6e9 100644 --- a/Source/Engine/AI/BehaviorKnowledge.h +++ b/Source/Engine/AI/BehaviorKnowledge.h @@ -124,6 +124,19 @@ public: RemoveGoal(T::TypeInitializer); } + /// + /// Gets the blackboard of a given type. + /// + template + FORCE_INLINE T* GetBlackboard() + { + auto* structure = Blackboard.AsStructure(); + if (structure) + return structure; + + return Blackboard.AsObject(T::TypeInitializer); + } + public: /// /// Compares two values and returns the comparision result. diff --git a/Source/Engine/AI/BehaviorTree.cs b/Source/Engine/AI/BehaviorTree.cs index b7925aee0..aebc07343 100644 --- a/Source/Engine/AI/BehaviorTree.cs +++ b/Source/Engine/AI/BehaviorTree.cs @@ -11,6 +11,18 @@ using FlaxEngine.GUI; namespace FlaxEngine { + partial class Behavior + { + /// + /// Gets the blackboard of the given type. + /// + /// The blackboard type. + public T GetBlackboard() + { + return Knowledge.GetBlackboard(); + } + } + partial class BehaviorKnowledge { /// @@ -33,6 +45,16 @@ namespace FlaxEngine { RemoveGoal(typeof(T)); } + + /// + /// Gets the blackboard of the given type. + /// + /// The blackboard type. + [Unmanaged] + public T GetBlackboard() + { + return (T)Blackboard; + } } partial class BehaviorTreeRootNode From da716dde6a550b1bb88efae90ca75886c7b61627 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 4 May 2026 08:46:43 +0200 Subject: [PATCH 09/28] Fix directional light cascaded shadow maps rendering stability --- Source/Engine/Renderer/ShadowsPass.cpp | 64 +++++++++++++++----------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp index 1404b8525..da9f3909a 100644 --- a/Source/Engine/Renderer/ShadowsPass.cpp +++ b/Source/Engine/Renderer/ShadowsPass.cpp @@ -370,6 +370,7 @@ public: void DirtyStaticBounds(const BoundingSphere& bounds) { + PROFILE_CPU(); // TODO: use octree to improve bounds-testing // TODO: build list of modified bounds and dirty them in batch on next frame start (ideally in async within shadows setup job) for (auto& e : Lights) @@ -773,7 +774,6 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render { SetupLight(shadows, renderContext, renderContextBatch, (RenderLightData&)light, atlasLight); - const RenderView& view = renderContext.View; const int32 csmCount = atlasLight.TilesCount; const auto shadowMapsSize = (float)atlasLight.Resolution; atlasLight.BlendCSM = Graphics::AllowCSMBlending; @@ -787,9 +787,9 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render #endif // Calculate cascade splits - const float minDistance = view.Near; - const float maxDistance = view.Near + atlasLight.Distance; - const float viewRange = view.Far - view.Near; + const float minDistance = renderContext.View.Near; + const float maxDistance = renderContext.View.Near + atlasLight.Distance; + const float viewRange = renderContext.View.Far - renderContext.View.Near; float cascadeSplits[MAX_CSM_CASCADES]; { PartitionMode partitionMode = light.PartitionMode; @@ -843,9 +843,9 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render // Convert distance splits to ratios cascade in the range [0, 1] for (int32 i = 0; i < MAX_CSM_CASCADES; i++) - cascadeSplits[i] = (cascadeSplits[i] - view.Near) / viewRange; + cascadeSplits[i] = (cascadeSplits[i] - renderContext.View.Near) / viewRange; } - atlasLight.CascadeSplits = view.Near + Float4(cascadeSplits) * viewRange; + atlasLight.CascadeSplits = renderContext.View.Near + Float4(cascadeSplits) * viewRange; // Update cached state (invalidate it if the light changed) atlasLight.ValidateCache(renderContext.View, light); @@ -881,21 +881,29 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render renderContextBatch.Contexts.AddDefault(atlasLight.ContextCount); atlasLight.Cache.Set(renderContext.View, light, atlasLight.CascadeSplits); - // Calculate view frustum corners (un-jittered) in view-space - Float3 frustumCorners[8]; + // Get the 8 points of the view frustum in view-space (unproject from clip-space) + Float3 frustumCornersVs[8]; { - BoundingFrustum stableViewFrustum; - Matrix m; - Matrix::Multiply(renderContext.View.View, renderContext.View.NonJitteredProjection, m); - stableViewFrustum.SetMatrix(m); - stableViewFrustum.GetCorners(frustumCorners); + Float3 frustumCornersCs[8] = + { + Float3(-1.0f, 1.0f, 0.0f), + Float3(1.0f, 1.0f, 0.0f), + Float3(1.0f, -1.0f, 0.0f), + Float3(-1.0f, -1.0f, 0.0f), + Float3(-1.0f, 1.0f, 1.0f), + Float3(1.0f, 1.0f, 1.0f), + Float3(1.0f, -1.0f, 1.0f), + Float3(-1.0f, -1.0f, 1.0f), + }; + Matrix invProjectionMatrix; + Matrix::Invert(renderContext.View.NonJitteredProjection, invProjectionMatrix); + for (int32 i = 0; i < 8; i++) + Float3::TransformCoordinate(frustumCornersCs[i], invProjectionMatrix, frustumCornersVs[i]); } - for (int32 i = 0; i < 8; i++) - Float3::Transform(frustumCorners[i], renderContext.View.View, frustumCorners[i]); // Create the different view and projection matrices for each split float splitMinRatio = 0; - float splitMaxRatio = (minDistance - view.Near) / viewRange; + float splitMaxRatio = (minDistance - renderContext.View.Near) / viewRange; int32 contextIndex = 0; for (int32 cascadeIndex = 0; cascadeIndex < csmCount; cascadeIndex++) { @@ -908,31 +916,31 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render continue; // Calculate cascade split frustum corners in view space - Float3 frustumCornersVs[8]; + Float3 cascadeCornersVs[8]; + float csmOverlap = atlasLight.BlendCSM ? 0.2f : 0.1f; for (int32 j = 0; j < 4; j++) { - float csmOverlap = atlasLight.BlendCSM ? 0.2f : 0.1f; float overlapWithPrevSplit = csmOverlap * (splitMinRatio - oldSplitMinRatio); - const auto frustumRangeVS = frustumCorners[j + 4] - frustumCorners[j]; - frustumCornersVs[j] = frustumCorners[j] + frustumRangeVS * (splitMinRatio - overlapWithPrevSplit); - frustumCornersVs[j + 4] = frustumCorners[j] + frustumRangeVS * splitMaxRatio; + const Float3 frustumRangeVS = frustumCornersVs[j + 4] - frustumCornersVs[j]; + cascadeCornersVs[j] = frustumCornersVs[j] + frustumRangeVS * (splitMinRatio - overlapWithPrevSplit); + cascadeCornersVs[j + 4] = frustumCornersVs[j] + frustumRangeVS * splitMaxRatio; } // Transform the frustum from camera view space to world-space - Float3 frustumCornersWs[8]; + Float3 cascadeCornersWs[8]; for (int32 i = 0; i < 8; i++) - Float3::Transform(frustumCornersVs[i], renderContext.View.IV, frustumCornersWs[i]); + Float3::Transform(cascadeCornersVs[i], renderContext.View.IV, cascadeCornersWs[i]); // Calculate the centroid of the view frustum slice Float3 frustumCenter = Float3::Zero; for (int32 i = 0; i < 8; i++) - frustumCenter += frustumCornersWs[i]; + frustumCenter += cascadeCornersWs[i]; frustumCenter *= 1.0f / 8.0f; // Calculate the radius of a bounding sphere surrounding the frustum corners float frustumRadius = 0.0f; for (int32 i = 0; i < 8; i++) - frustumRadius = Math::Max(frustumRadius, (frustumCornersWs[i] - frustumCenter).LengthSquared()); + frustumRadius = Math::Max(frustumRadius, (cascadeCornersWs[i] - frustumCenter).LengthSquared()); frustumRadius = Math::Ceil(Math::Sqrt(frustumRadius) * 16.0f) / 16.0f; // Snap cascade center to the texel size @@ -955,7 +963,7 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render Matrix::LookAt(frustumCenter + light.Direction * minExtents.Z, frustumCenter, up, shadowView); // Create viewport for culling with extended near/far planes due to culling issues (aka pancaking) - const float cullRangeExtent = 100000.0f; + const float cullRangeExtent = METERS_TO_UNITS(1000.0f); Matrix::OrthoOffCenter(minExtents.X, maxExtents.X, minExtents.Y, maxExtents.Y, -cullRangeExtent, cascadeExtents.Z + cullRangeExtent, shadowProjection); Matrix::Multiply(shadowView, shadowProjection, cullingVP); @@ -981,11 +989,11 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render // Setup context for cascade auto& shadowContext = renderContextBatch.Contexts[atlasLight.ContextIndex + contextIndex++]; SetupRenderContext(renderContext, shadowContext); - shadowContext.View.Position = light.Direction * -atlasLight.Distance + view.Position; + shadowContext.View.Position = light.Direction * -atlasLight.Distance + renderContext.View.Position; shadowContext.View.Direction = light.Direction; shadowContext.View.SetUp(shadowView, shadowProjection); shadowContext.View.CullingFrustum.SetMatrix(cullingVP); - shadowContext.View.PrepareCache(shadowContext, shadowMapsSize, shadowMapsSize, Float2::Zero, &view); + shadowContext.View.PrepareCache(shadowContext, shadowMapsSize, shadowMapsSize, Float2::Zero, &renderContext.View); } } From 864f3a3d4259cc17bcb05a7230a09f09386b4500 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 4 May 2026 23:05:25 +0200 Subject: [PATCH 10/28] Fix scene search performance regression --- Source/Editor/SceneGraph/GUI/ActorTreeNode.cs | 58 +++++++++++++------ Source/Editor/Windows/SceneTreeWindow.cs | 2 +- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index 4607a4f63..072491c5a 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -182,7 +182,7 @@ namespace FlaxEditor.SceneGraph.GUI _highlights?.Clear(); isThisVisible = true; } - else + else if (filterText.Contains(',')) { var splitFilter = filterText.Split(','); var hasAllFilters = true; @@ -199,21 +199,17 @@ namespace FlaxEditor.SceneGraph.GUI // Check for any scripts if (trimmedFilter.Equals("s:", StringComparison.OrdinalIgnoreCase)) { - if (Actor != null) - { - if (Actor.ScriptsCount > 0) - { - hasFilter = true; - } - } + if (actor != null && actor.ScriptsCount > 0) + hasFilter = true; } else { var scriptText = trimmedFilter.Replace("s:", "", StringComparison.OrdinalIgnoreCase).Trim(); var scriptFound = false; - if (Actor != null) + if (actor != null) { - foreach (var script in Actor.Scripts) + var scripts = actor.Scripts; + foreach (var script in scripts) { var name = TypeUtils.GetTypeDisplayName(script.GetType()); var nameNoSpaces = name.Replace(" ", ""); @@ -233,15 +229,15 @@ namespace FlaxEditor.SceneGraph.GUI { if (trimmedFilter.Equals("a:", StringComparison.OrdinalIgnoreCase)) { - if (Actor != null) + if (actor != null) hasFilter = true; } else { - if (Actor != null) + if (actor != null) { var actorTypeText = trimmedFilter.Replace("a:", "", StringComparison.OrdinalIgnoreCase).Trim(); - var name = TypeUtils.GetTypeDisplayName(Actor.GetType()); + var name = TypeUtils.GetTypeDisplayName(actor.GetType()); var nameNoSpaces = name.Replace(" ", ""); if (name.Contains(actorTypeText, StringComparison.OrdinalIgnoreCase) || nameNoSpaces.Contains(actorTypeText, StringComparison.OrdinalIgnoreCase)) hasFilter = true; @@ -253,15 +249,15 @@ namespace FlaxEditor.SceneGraph.GUI { if (trimmedFilter.Equals("c:", StringComparison.OrdinalIgnoreCase)) { - if (Actor != null) + if (actor != null) hasFilter = true; } else { - if (Actor != null && Actor is UIControl uic && uic.Control != null) + if (actor is UIControl uiControl && uiControl.Control != null) { var controlTypeText = trimmedFilter.Replace("c:", "", StringComparison.OrdinalIgnoreCase).Trim(); - var name = TypeUtils.GetTypeDisplayName(uic.Control.GetType()); + var name = TypeUtils.GetTypeDisplayName(uiControl.Control.GetType()); var nameNoSpaces = name.Replace(" ", ""); if (name.Contains(controlTypeText, StringComparison.OrdinalIgnoreCase) || nameNoSpaces.Contains(controlTypeText, StringComparison.OrdinalIgnoreCase)) hasFilter = true; @@ -283,8 +279,9 @@ namespace FlaxEditor.SceneGraph.GUI var textRect = TextRect; for (int i = 0; i < ranges.Length; i++) { - var start = font.GetCharPosition(text, ranges[i].StartIndex); - var end = font.GetCharPosition(text, ranges[i].EndIndex); + var range = ranges[i]; + var start = font.GetCharPosition(text, range.StartIndex); + var end = font.GetCharPosition(text, range.EndIndex); _highlights.Add(new Rectangle(start.X + textRect.X, textRect.Y, end.X - start.X, textRect.Height)); } hasFilter = true; @@ -302,6 +299,31 @@ namespace FlaxEditor.SceneGraph.GUI if (!hasAllFilters) _highlights?.Clear(); } + else if (QueryFilterHelper.Match(filterText, Text, out QueryFilterHelper.Range[] ranges)) + { + // Update highlights + if (_highlights == null) + _highlights = new List(ranges.Length); + else + _highlights.Clear(); + var font = Style.Current.FontSmall; + var textRect = TextRect; + var text = Text; + for (int i = 0; i < ranges.Length; i++) + { + var range = ranges[i]; + var start = font.GetCharPosition(text, range.StartIndex); + var end = font.GetCharPosition(text, range.EndIndex); + _highlights.Add(new Rectangle(start.X + textRect.X, textRect.Y, end.X - start.X, textRect.Height)); + } + isThisVisible = true; + } + else + { + // Hide + _highlights?.Clear(); + isThisVisible = false; + } // Update children bool isAnyChildVisible = false; diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index fba4e5a71..e7dcb3319 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -175,8 +175,8 @@ namespace FlaxEditor.Windows if (IsLayoutLocked) return; - _tree.LockChildrenRecursive(); PerformLayout(); + _tree.LockChildrenRecursive(); // Update tree var query = _searchBox.Text; From 378ce066cd85f457654953f7d44c8810da933285 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 May 2026 12:39:32 +0200 Subject: [PATCH 11/28] Fix error when drag&drop new actor on Windows in Editor --- Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp b/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp index 1aa5c903e..61d4afd31 100644 --- a/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp +++ b/Source/Engine/Platform/Windows/WindowsWindow.DragDrop.cpp @@ -13,13 +13,13 @@ #include "Engine/Core/Log.h" #include "Engine/Platform/IGuiData.h" #include "Engine/Platform/Base/DragDropHelper.h" +#include "Engine/Graphics/GPUDevice.h" #include "Engine/Input/Input.h" #include "Engine/Input/Mouse.h" #endif #include "Engine/Platform/Win32/IncludeWindowsHeaders.h" #include - #if USE_EDITOR #include #include @@ -648,6 +648,7 @@ HRESULT Window::Drop(Windows::IDataObject* pDataObj, Windows::DWORD grfKeyState, ::ScreenToClient((HWND)_handle, &p); GuiDragDropData.Init((IDataObject*)pDataObj); DragDropEffect effect = DragDropEffect::None; + ScopeLock gpuLock(GPUDevice::Instance->Locker); // Avoid issues when DoDragDropJob is during frame painting OnDragDrop(&GuiDragDropData, Float2(static_cast(p.x), static_cast(p.y)), effect); *pdwEffect = dropEffect2OleEnum(effect); return S_OK; From 99571667232f4a9ed1b64452ed77fd178fc549f4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Apr 2026 21:55:52 +0200 Subject: [PATCH 12/28] Optimize `RendererAllocation` by reducing fragmentation with operating on power-of-2 blocks --- Source/Engine/Renderer/RenderList.cpp | 2 ++ Source/Engine/Renderer/RendererAllocation.h | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Source/Engine/Renderer/RenderList.cpp b/Source/Engine/Renderer/RenderList.cpp index 6687bdc97..80ad67164 100644 --- a/Source/Engine/Renderer/RenderList.cpp +++ b/Source/Engine/Renderer/RenderList.cpp @@ -220,6 +220,7 @@ void RenderFogData::Init(const RenderView& view, IFogRenderer* renderer) void* RendererAllocation::Allocate(uintptr size) { PROFILE_CPU(); + size = AllocationUtils::AlignToPowerOf2((int32)size); // Reduce fragmentation by operating on power-of-2 blocks void* result = nullptr; MemPoolLocker.Lock(); for (int32 i = 0; i < MemPool.Count(); i++) @@ -240,6 +241,7 @@ void* RendererAllocation::Allocate(uintptr size) void RendererAllocation::Free(void* ptr, uintptr size) { PROFILE_CPU(); + size = AllocationUtils::AlignToPowerOf2((int32)size); // Reduce fragmentation by operating on power-of-2 blocks MemPoolLocker.Lock(); MemPool.Add({ ptr, size }); MemPoolLocker.Unlock(); diff --git a/Source/Engine/Renderer/RendererAllocation.h b/Source/Engine/Renderer/RendererAllocation.h index 30e55fbd8..80dc40b6e 100644 --- a/Source/Engine/Renderer/RendererAllocation.h +++ b/Source/Engine/Renderer/RendererAllocation.h @@ -4,6 +4,9 @@ #include "Engine/Core/Memory/SimpleHeapAllocation.h" +/// +/// Shared pool allocator for the renderer used for large chunks of memory. +/// class RendererAllocation : public SimpleHeapAllocation { public: From a421effd1b8218ed41c6ce7931e0b39e20574a70 Mon Sep 17 00:00:00 2001 From: Phantom Date: Thu, 7 May 2026 20:53:35 +0200 Subject: [PATCH 13/28] Add Video playback value --- Source/Engine/Video/VideoPlayer.h | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Source/Engine/Video/VideoPlayer.h b/Source/Engine/Video/VideoPlayer.h index 4447337ea..ecae37e61 100644 --- a/Source/Engine/Video/VideoPlayer.h +++ b/Source/Engine/Video/VideoPlayer.h @@ -174,6 +174,30 @@ public: return _state; } + /// + /// Gets the value that determines whether the video playback is playing. + /// + API_PROPERTY() FORCE_INLINE bool IsPlaying() const + { + return _state == States::Playing; + } + + /// + /// Gets the value that determines whether the video playback is paused. + /// + API_PROPERTY() FORCE_INLINE bool IsPaused() const + { + return _state == States::Paused; + } + + /// + /// Gets the value that determines whether the video playback is stopped. + /// + API_PROPERTY() FORCE_INLINE bool IsStopped() const + { + return _state == States::Stopped; + } + /// /// Gets the current time of playback. The time is in seconds, in range [0, Duration]. /// From af3836d611bf2945d2be052e7e4b4242fce04cb2 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Thu, 7 May 2026 18:04:50 -0500 Subject: [PATCH 14/28] Add GetValue by type in c# for gameplay globals --- Source/Engine/Engine/GameplayGlobals.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Source/Engine/Engine/GameplayGlobals.cs diff --git a/Source/Engine/Engine/GameplayGlobals.cs b/Source/Engine/Engine/GameplayGlobals.cs new file mode 100644 index 000000000..9dabae525 --- /dev/null +++ b/Source/Engine/Engine/GameplayGlobals.cs @@ -0,0 +1,18 @@ +namespace FlaxEngine; + +partial class GameplayGlobals +{ + /// + /// Gets a value of a given type from the global variables. (it must be added first). + /// + /// The name of the variable to retrieve. + /// The type of the variable to retrieve. + /// The value of the variable, or default if not found or type mismatch. + public T GetValue(string name) + { + var obj = GetValue(name); + if (obj is T t) + return t; + return default; + } +} From 64708a14d9de957a6534a5fee7519020d8625f7b Mon Sep 17 00:00:00 2001 From: Jake Young Date: Fri, 8 May 2026 12:22:45 -0400 Subject: [PATCH 15/28] Update LookingAt to be a more descriptive name. --- Source/Engine/Level/Actor.cpp | 10 ++++++++++ Source/Engine/Level/Actor.h | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 3f1ab905b..5f60ad2bd 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -1725,6 +1725,11 @@ void Actor::LookAt(const Vector3& worldPos, const Vector3& worldUp) } Quaternion Actor::LookingAt(const Vector3& worldPos) const +{ + return GetLookAtDirection(worldPos); +} + +Quaternion Actor::GetLookAtDirection(const Vector3& worldPos) const { const Vector3 direction = worldPos - _transform.Translation; if (direction.LengthSquared() < ZeroTolerance) @@ -1752,6 +1757,11 @@ Quaternion Actor::LookingAt(const Vector3& worldPos) const } Quaternion Actor::LookingAt(const Vector3& worldPos, const Vector3& worldUp) const +{ + return GetLookAtDirection(worldPos, worldUp); +} + +Quaternion Actor::GetLookAtDirection(const Vector3& worldPos, const Vector3& worldUp) const { const Vector3 direction = worldPos - _transform.Translation; if (direction.LengthSquared() < ZeroTolerance) diff --git a/Source/Engine/Level/Actor.h b/Source/Engine/Level/Actor.h index cd596e86c..d17726c28 100644 --- a/Source/Engine/Level/Actor.h +++ b/Source/Engine/Level/Actor.h @@ -897,16 +897,33 @@ public: /// /// Gets rotation of the actor oriented towards the specified world position. + /// [Deprecated in v1.13] /// /// The world position to orient towards. + DEPRECATED("Use GetLookAtDirection instead.") API_FUNCTION() Quaternion LookingAt(const Vector3& worldPos) const; + + /// + /// Gets rotation of the actor oriented towards the specified world position. + /// + /// The world position to orient towards. + API_FUNCTION() Quaternion GetLookAtDirection(const Vector3& worldPos) const; + /// + /// Gets rotation of the actor oriented towards the specified world position with upwards direction. + /// [Deprecated in v1.13] + /// + /// The world position to orient towards. + /// The up direction that constrains up axis orientation to a plane this vector lies on. This rule might be broken if forward and up direction are nearly parallel. + DEPRECATED("Use GetLookAtDirection instead.") + API_FUNCTION() Quaternion LookingAt(const Vector3& worldPos, const Vector3& worldUp) const; + /// /// Gets rotation of the actor oriented towards the specified world position with upwards direction. /// /// The world position to orient towards. /// The up direction that constrains up axis orientation to a plane this vector lies on. This rule might be broken if forward and up direction are nearly parallel. - API_FUNCTION() Quaternion LookingAt(const Vector3& worldPos, const Vector3& worldUp) const; + API_FUNCTION() Quaternion GetLookAtDirection(const Vector3& worldPos, const Vector3& worldUp) const; public: /// From ba48b2e4f38d75e9c19a3ad91ec15b9571be0ace Mon Sep 17 00:00:00 2001 From: Jake Young Date: Fri, 8 May 2026 12:10:42 -0400 Subject: [PATCH 16/28] Deprecate the Direction properties and replace with forward. Also give more descriptive description of what they do. --- Source/Engine/Level/Actor.cpp | 5 +++++ Source/Engine/Level/Actor.h | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 3f1ab905b..19fef4ca1 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -822,6 +822,11 @@ void Actor::SetRotation(const Matrix& value) } void Actor::SetDirection(const Float3& value) +{ + SetForward(value); +} + +void Actor::SetForward(const Float3& value) { CHECK(!value.IsNanOrInfinity()); Quaternion orientation; diff --git a/Source/Engine/Level/Actor.h b/Source/Engine/Level/Actor.h index cd596e86c..0ed5cc822 100644 --- a/Source/Engine/Level/Actor.h +++ b/Source/Engine/Level/Actor.h @@ -549,18 +549,36 @@ public: /// /// Gets actor direction vector (forward vector). + /// [Deprecated in v1.13] /// + DEPRECATED("Use GetForward instead.") API_PROPERTY(Attributes="HideInEditor, NoSerialize") FORCE_INLINE Float3 GetDirection() const + { + return GetForward(); + } + + /// + /// Gets the actor's forward vector. + /// + API_PROPERTY(Attributes="HideInEditor, NoSerialize") FORCE_INLINE Float3 GetForward() const { return Float3::Transform(Float3::Forward, GetOrientation()); } /// /// Sets actor direction vector (forward) + /// [Deprecated in v1.13] /// /// The value to set. + DEPRECATED("Use SetForward instead.") API_PROPERTY() void SetDirection(const Float3& value); + /// + /// Rotates the actor to align its forward vector with the passed in value. + /// + /// The value to align to. + API_PROPERTY() void SetForward(const Float3& value); + public: /// /// Resets the actor local transform. From 8281e743cd9426864ec7c4dac5b8944c0e201eed Mon Sep 17 00:00:00 2001 From: Saas Date: Tue, 12 May 2026 14:05:04 +0200 Subject: [PATCH 17/28] rename Sphere Mask node A and B inputs --- Source/Editor/Surface/Archetypes/Material.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 5b3aaf310..c5579ff9f 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -807,8 +807,8 @@ namespace FlaxEditor.Surface.Archetypes }, Elements = new[] { - NodeElementArchetype.Factory.Input(0, "A", true, null, 0), - NodeElementArchetype.Factory.Input(1, "B", true, null, 1), + NodeElementArchetype.Factory.Input(0, "UV", true, null, 0), + NodeElementArchetype.Factory.Input(1, "Center", true, null, 1), NodeElementArchetype.Factory.Input(2, "Radius", true, typeof(float), 2, 0), NodeElementArchetype.Factory.Input(3, "Hardness", true, typeof(float), 3, 1), NodeElementArchetype.Factory.Input(4, "Invert", true, typeof(bool), 4, 2), From 746bd2273f2ace5a4079dbeb5298d3821bd4e107 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 12 May 2026 17:10:28 +0200 Subject: [PATCH 18/28] Fix memory alloc when renaming GPU resource on Vulkan --- Source/Engine/GraphicsDevice/Vulkan/RenderToolsVulkan.cpp | 4 ++-- Source/Engine/GraphicsDevice/Vulkan/RenderToolsVulkan.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Engine/GraphicsDevice/Vulkan/RenderToolsVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/RenderToolsVulkan.cpp index 7a504c93f..c43cab271 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/RenderToolsVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/RenderToolsVulkan.cpp @@ -153,10 +153,10 @@ VkCompareOp RenderToolsVulkan::ComparisonFuncToVkCompareOp[9] = #if GPU_ENABLE_RESOURCE_NAMING -void RenderToolsVulkan::SetObjectName(VkDevice device, uint64 objectHandle, VkObjectType objectType, const String& name) +void RenderToolsVulkan::SetObjectName(VkDevice device, uint64 objectHandle, VkObjectType objectType, const StringView& name) { #if VK_EXT_debug_utils - auto str = name.ToStringAnsi(); + StringAsANSI<> str(name.Get(), name.Length()); SetObjectName(device, objectHandle, objectType, str.Get()); #endif } diff --git a/Source/Engine/GraphicsDevice/Vulkan/RenderToolsVulkan.h b/Source/Engine/GraphicsDevice/Vulkan/RenderToolsVulkan.h index 82167fd6c..d359442f4 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/RenderToolsVulkan.h +++ b/Source/Engine/GraphicsDevice/Vulkan/RenderToolsVulkan.h @@ -35,7 +35,7 @@ private: public: #if GPU_ENABLE_RESOURCE_NAMING - static void SetObjectName(VkDevice device, uint64 objectHandle, VkObjectType objectType, const String& name); + static void SetObjectName(VkDevice device, uint64 objectHandle, VkObjectType objectType, const StringView& name); static void SetObjectName(VkDevice device, uint64 objectHandle, VkObjectType objectType, const char* name); #endif From 2a95e8e9700f0233ce6ba3d6190265e83b65ef47 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 12 May 2026 17:44:27 +0200 Subject: [PATCH 19/28] Fix scene search regression from 864f3a3d4259cc17bcb05a7230a09f09386b4500 #4089 --- Source/Editor/SceneGraph/GUI/ActorTreeNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index 072491c5a..61464ad77 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -182,7 +182,7 @@ namespace FlaxEditor.SceneGraph.GUI _highlights?.Clear(); isThisVisible = true; } - else if (filterText.Contains(',')) + else if (filterText.Contains(':')) { var splitFilter = filterText.Split(','); var hasAllFilters = true; From e71f43ea793d4ddceeb6197655ee77d03ffafa2b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 12 May 2026 18:19:10 +0200 Subject: [PATCH 20/28] Fix additional FOV to be included in Direction Gizmo #4016 --- Source/Editor/Gizmo/DirectionGizmo.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Gizmo/DirectionGizmo.cs b/Source/Editor/Gizmo/DirectionGizmo.cs index b76cd0607..aedfe73d0 100644 --- a/Source/Editor/Gizmo/DirectionGizmo.cs +++ b/Source/Editor/Gizmo/DirectionGizmo.cs @@ -222,7 +222,10 @@ internal class DirectionGizmo : ContainerControl else { // This could be some actual math expression, not that hack - var fov = _owner.Viewport.FieldOfView / 60.0f; + float fov = _owner.Viewport.FieldOfView; + if (_owner.Viewport.ViewportCamera is Viewport.Cameras.FPSCamera fpsCam) + fov += fpsCam.AdditionalZoomFOV; + fov /= 60.0f; float scaleAt30 = 0.1f, scaleAt60 = 1.0f, scaleAt120 = 1.5f, scaleAt180 = 3.0f; heightNormalization /= Mathf.Lerp(scaleAt30, scaleAt60, fov); heightNormalization /= Mathf.Lerp(scaleAt60, scaleAt120, Mathf.Saturate(fov - 1)); From de76d3623ebc23f19c86c1dfe69bc97a92cee8a8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 12 May 2026 18:24:59 +0200 Subject: [PATCH 21/28] Fix BehaviorKnowledge of object type casting #4072 --- Source/Engine/AI/BehaviorKnowledge.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Source/Engine/AI/BehaviorKnowledge.h b/Source/Engine/AI/BehaviorKnowledge.h index 80f04b6e9..8924ca0e7 100644 --- a/Source/Engine/AI/BehaviorKnowledge.h +++ b/Source/Engine/AI/BehaviorKnowledge.h @@ -133,8 +133,7 @@ public: auto* structure = Blackboard.AsStructure(); if (structure) return structure; - - return Blackboard.AsObject(T::TypeInitializer); + return Cast((ScriptingObject*)Blackboard); } public: From d697bd74021916ef25074d8c4358ab55fc9b6caf Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 12 May 2026 18:40:21 +0200 Subject: [PATCH 22/28] Update old API usage #4080 #4081 --- Source/Engine/Graphics/RenderView.cpp | 2 +- Source/Engine/Graphics/RenderView.cs | 2 +- Source/Engine/Level/Actor.cpp | 9 ++++----- Source/Engine/Level/Actor.h | 18 ++++++++---------- Source/Engine/Level/Actors/Camera.cpp | 2 +- .../Engine/Level/Actors/DirectionalLight.cpp | 2 +- .../Level/Actors/ExponentialHeightFog.cpp | 2 +- Source/Engine/Level/Actors/Sky.cpp | 2 +- Source/Engine/Level/Actors/SpotLight.cpp | 8 ++++---- Source/Engine/UI/UICanvas.cs | 2 +- .../Bindings/BindingsGenerator.Parsing.cs | 3 ++- 11 files changed, 25 insertions(+), 27 deletions(-) diff --git a/Source/Engine/Graphics/RenderView.cpp b/Source/Engine/Graphics/RenderView.cpp index 0472ce346..aa2e94604 100644 --- a/Source/Engine/Graphics/RenderView.cpp +++ b/Source/Engine/Graphics/RenderView.cpp @@ -195,7 +195,7 @@ void RenderView::CopyFrom(const Camera* camera, const Viewport* viewport) const Vector3 cameraPos = camera->GetPosition(); LargeWorlds::UpdateOrigin(Origin, cameraPos); Position = cameraPos - Origin; - Direction = camera->GetDirection(); + Direction = camera->GetForward(); Near = camera->GetNearPlane(); Far = camera->GetFarPlane(); camera->GetMatrices(View, Projection, viewport ? *viewport : camera->GetViewport(), Origin); diff --git a/Source/Engine/Graphics/RenderView.cs b/Source/Engine/Graphics/RenderView.cs index 2cd27ad83..b71b2d19b 100644 --- a/Source/Engine/Graphics/RenderView.cs +++ b/Source/Engine/Graphics/RenderView.cs @@ -96,7 +96,7 @@ namespace FlaxEngine Vector3 cameraPos = camera.Position; LargeWorlds.UpdateOrigin(ref Origin, cameraPos); Position = cameraPos - Origin; - Direction = camera.Direction; + Direction = camera.Forward; Near = camera.NearPlane; Far = camera.FarPlane; camera.GetMatrices(out View, out Projection, ref viewport, ref Origin); diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 29aaff502..8ce79fea4 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -1719,13 +1719,13 @@ Actor* Actor::Intersects(const Ray& ray, Real& distance, Vector3& normal) void Actor::LookAt(const Vector3& worldPos) { - const Quaternion orientation = LookingAt(worldPos); + const Quaternion orientation = GetLookAtDirection(worldPos); SetOrientation(orientation); } void Actor::LookAt(const Vector3& worldPos, const Vector3& worldUp) { - const Quaternion orientation = LookingAt(worldPos, worldUp); + const Quaternion orientation = GetLookAtDirection(worldPos, worldUp); SetOrientation(orientation); } @@ -1771,12 +1771,11 @@ Quaternion Actor::GetLookAtDirection(const Vector3& worldPos, const Vector3& wor const Vector3 direction = worldPos - _transform.Translation; if (direction.LengthSquared() < ZeroTolerance) return _parent ? _parent->GetOrientation() : Quaternion::Identity; + const Float3 forward = Vector3::Normalize(direction); const Float3 up = Vector3::Normalize(worldUp); if (Math::IsOne(Float3::Dot(forward, up))) - { - return LookingAt(worldPos); - } + return GetLookAtDirection(worldPos); Quaternion orientation; Quaternion::LookRotation(direction, up, orientation); diff --git a/Source/Engine/Level/Actor.h b/Source/Engine/Level/Actor.h index 16b0c8ce3..a1ca01cc6 100644 --- a/Source/Engine/Level/Actor.h +++ b/Source/Engine/Level/Actor.h @@ -551,14 +551,14 @@ public: /// Gets actor direction vector (forward vector). /// [Deprecated in v1.13] /// - DEPRECATED("Use GetForward instead.") - API_PROPERTY(Attributes="HideInEditor, NoSerialize") FORCE_INLINE Float3 GetDirection() const + API_PROPERTY(Attributes="HideInEditor, NoSerialize") DEPRECATED("Use GetForward instead.") + FORCE_INLINE Float3 GetDirection() const { return GetForward(); } /// - /// Gets the actor's forward vector. + /// Gets the actor's forward vector (direction). /// API_PROPERTY(Attributes="HideInEditor, NoSerialize") FORCE_INLINE Float3 GetForward() const { @@ -570,11 +570,11 @@ public: /// [Deprecated in v1.13] /// /// The value to set. - DEPRECATED("Use SetForward instead.") - API_PROPERTY() void SetDirection(const Float3& value); + API_PROPERTY() DEPRECATED("Use SetForward instead.") + void SetDirection(const Float3& value); /// - /// Rotates the actor to align its forward vector with the passed in value. + /// Rotates the actor to align its forward vector with the passed in value (direction). /// /// The value to align to. API_PROPERTY() void SetForward(const Float3& value); @@ -918,8 +918,7 @@ public: /// [Deprecated in v1.13] /// /// The world position to orient towards. - DEPRECATED("Use GetLookAtDirection instead.") - API_FUNCTION() Quaternion LookingAt(const Vector3& worldPos) const; + API_FUNCTION() DEPRECATED("Use GetLookAtDirection instead.") Quaternion LookingAt(const Vector3& worldPos) const; /// /// Gets rotation of the actor oriented towards the specified world position. @@ -933,8 +932,7 @@ public: /// /// The world position to orient towards. /// The up direction that constrains up axis orientation to a plane this vector lies on. This rule might be broken if forward and up direction are nearly parallel. - DEPRECATED("Use GetLookAtDirection instead.") - API_FUNCTION() Quaternion LookingAt(const Vector3& worldPos, const Vector3& worldUp) const; + API_FUNCTION() DEPRECATED("Use GetLookAtDirection instead.") Quaternion LookingAt(const Vector3& worldPos, const Vector3& worldUp) const; /// /// Gets rotation of the actor oriented towards the specified world position with upwards direction. diff --git a/Source/Engine/Level/Actors/Camera.cpp b/Source/Engine/Level/Actors/Camera.cpp index b5b30e52d..fa4c2abfb 100644 --- a/Source/Engine/Level/Actors/Camera.cpp +++ b/Source/Engine/Level/Actors/Camera.cpp @@ -210,7 +210,7 @@ Ray Camera::ConvertMouseToRay(const Float2& mousePosition, const Viewport& viewp { Vector3 position = GetPosition(); if (viewport.Width < ZeroTolerance || viewport.Height < ZeroTolerance || mousePosition.IsNaN()) - return Ray(position, GetDirection()); + return Ray(position, GetForward()); // Use different logic in orthographic projection if (!_usePerspective) diff --git a/Source/Engine/Level/Actors/DirectionalLight.cpp b/Source/Engine/Level/Actors/DirectionalLight.cpp index e6644c1f7..f677d0828 100644 --- a/Source/Engine/Level/Actors/DirectionalLight.cpp +++ b/Source/Engine/Level/Actors/DirectionalLight.cpp @@ -30,7 +30,7 @@ void DirectionalLight::Draw(RenderContext& renderContext) data.ShadowsDistance = ShadowsDistance; data.Color = Color.ToFloat3() * (Color.A * brightness); data.ShadowsStrength = ShadowsStrength; - data.Direction = GetDirection(); + data.Direction = GetForward(); data.ShadowsFadeDistance = ShadowsFadeDistance; data.ShadowsNormalOffsetScale = ShadowsNormalOffsetScale; data.ShadowsDepthBias = ShadowsDepthBias; diff --git a/Source/Engine/Level/Actors/ExponentialHeightFog.cpp b/Source/Engine/Level/Actors/ExponentialHeightFog.cpp index 2a89771c9..cfb5217a0 100644 --- a/Source/Engine/Level/Actors/ExponentialHeightFog.cpp +++ b/Source/Engine/Level/Actors/ExponentialHeightFog.cpp @@ -163,7 +163,7 @@ void ExponentialHeightFog::GetExponentialHeightFogData(const RenderView& view, S result.FogCutoffDistance = FogCutoffDistance >= 0 ? FogCutoffDistance : view.Far + FogCutoffDistance; if (useDirectionalLightInscattering) { - result.InscatteringLightDirection = -DirectionalInscatteringLight->GetDirection(); + result.InscatteringLightDirection = -DirectionalInscatteringLight->GetForward(); result.DirectionalInscatteringColor = DirectionalInscatteringColor.ToFloat3(); result.DirectionalInscatteringExponent = Math::Clamp(DirectionalInscatteringExponent, 0.000001f, 1000.0f); result.DirectionalInscatteringStartDistance = Math::Min(DirectionalInscatteringStartDistance, view.Far - 1.0f); diff --git a/Source/Engine/Level/Actors/Sky.cpp b/Source/Engine/Level/Actors/Sky.cpp index 7354f3bcf..daa024ab3 100644 --- a/Source/Engine/Level/Actors/Sky.cpp +++ b/Source/Engine/Level/Actors/Sky.cpp @@ -73,7 +73,7 @@ void Sky::InitConfig(ShaderAtmosphericFogData& config) const if (SunLight) { - config.AtmosphericFogSunDirection = -SunLight->GetDirection(); + config.AtmosphericFogSunDirection = -SunLight->GetForward(); config.AtmosphericFogSunColor = SunLight->Color.ToFloat3() * SunLight->Color.A; } else diff --git a/Source/Engine/Level/Actors/SpotLight.cpp b/Source/Engine/Level/Actors/SpotLight.cpp index 38502cf1b..b0888e46c 100644 --- a/Source/Engine/Level/Actors/SpotLight.cpp +++ b/Source/Engine/Level/Actors/SpotLight.cpp @@ -27,7 +27,7 @@ SpotLight::SpotLight(const SpawnParams& params) _cosInnerCone = Math::Cos(_innerConeAngle * DegreesToRadians); _invCosConeDifference = 1.0f / (_cosInnerCone - _cosOuterCone); const float boundsRadius = Math::Sqrt(1.25f * _radius * _radius - _radius * _radius * _cosOuterCone); - _sphere = BoundingSphere(GetPosition() + 0.5f * GetDirection() * _radius, boundsRadius); + _sphere = BoundingSphere(GetPosition() + 0.5f * GetForward() * _radius, boundsRadius); BoundingBox::FromSphere(_sphere, _box); } @@ -113,7 +113,7 @@ void SpotLight::UpdateBounds() // Note: we use the law of cosines to find the distance to the furthest edge of the spotlight cone from a position that is halfway down the spotlight direction const float radius = GetScaledRadius(); const float boundsRadius = Math::Sqrt(1.25f * radius * radius - radius * radius * _cosOuterCone); - _sphere = BoundingSphere(GetPosition() + 0.5f * GetDirection() * radius, boundsRadius); + _sphere = BoundingSphere(GetPosition() + 0.5f * GetForward() * radius, boundsRadius); BoundingBox::FromSphere(_sphere, _box); if (_sceneRenderingKey != -1) @@ -199,7 +199,7 @@ void SpotLight::OnDebugDrawSelected() const auto color = Color::Yellow; Vector3 right = _transform.GetRight(); Vector3 up = _transform.GetUp(); - Vector3 forward = GetDirection(); + Vector3 forward = GetForward(); float radius = GetScaledRadius(); float discRadius = radius * Math::Tan(_outerConeAngle * DegreesToRadians); float falloffDiscRadius = radius * Math::Tan(_innerConeAngle * DegreesToRadians); @@ -231,7 +231,7 @@ void SpotLight::DrawLightsDebug(RenderView& view) const auto color = Color::Yellow; Vector3 right = _transform.GetRight(); Vector3 up = _transform.GetUp(); - Vector3 forward = GetDirection(); + Vector3 forward = GetForward(); float radius = GetScaledRadius(); float discRadius = radius * Math::Tan(_outerConeAngle * DegreesToRadians); float falloffDiscRadius = radius * Math::Tan(_innerConeAngle * DegreesToRadians); diff --git a/Source/Engine/UI/UICanvas.cs b/Source/Engine/UI/UICanvas.cs index 4a6bb7ce1..225213dba 100644 --- a/Source/Engine/UI/UICanvas.cs +++ b/Source/Engine/UI/UICanvas.cs @@ -465,7 +465,7 @@ namespace FlaxEngine Quaternion.Euler(180, 180, 0, out var quat); Matrix.RotationQuaternion(ref quat, out m2); Matrix.Multiply(ref m3, ref m2, out m1); - m2 = Matrix.Transformation(Vector3.One, Quaternion.FromDirection(-camera.Direction), translation); + m2 = Matrix.Transformation(Vector3.One, Quaternion.FromDirection(-camera.Forward), translation); Matrix.Multiply(ref m1, ref m2, out world); } else if (_renderMode == CanvasRenderMode.CameraSpace && camera) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs index a2016c8cb..871530e30 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs @@ -1052,7 +1052,8 @@ namespace Flax.Build.Bindings propertyInfo.Getter = functionInfo; else propertyInfo.Setter = functionInfo; - propertyInfo.DeprecatedMessage = functionInfo.DeprecatedMessage; + if (propertyInfo.DeprecatedMessage == null) + propertyInfo.DeprecatedMessage = functionInfo.DeprecatedMessage; propertyInfo.IsHidden |= functionInfo.IsHidden; if (propertyInfo.Getter != null && propertyInfo.Setter != null) From 5739c0bef4916ee1b3d75f438784b6dff1ebc613 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 12 May 2026 22:53:29 +0200 Subject: [PATCH 23/28] Fix assertion on shader load failure #2702 --- Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp b/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp index eae1f9882..4bec6440e 100644 --- a/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp +++ b/Source/Engine/Graphics/Shaders/Cache/ShaderAssetBase.cpp @@ -360,14 +360,12 @@ bool ShaderAssetBase::LoadShaderCache(ShaderCacheResult& result) return true; } - ASSERT(result.Data.IsValid()); - #if COMPILE_WITH_SHADER_COMPILER // Read includes from cache IsValidShaderCache(result.Data, result.Includes); #endif - return false; + return result.Data.IsInvalid(); } #if COMPILE_WITH_SHADER_COMPILER From c10cfc8e451d4d219c205459aa87036f22dae69b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 13 May 2026 10:42:56 +0200 Subject: [PATCH 24/28] Fix output log history popup management to smoother usage --- .../Editor/GUI/ContextMenu/ContextMenuBase.cs | 2 +- Source/Editor/Windows/OutputLogWindow.cs | 35 +++++++++++++++---- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs index d3af7b5cd..3eace0363 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs @@ -355,7 +355,7 @@ namespace FlaxEditor.GUI.ContextMenu if (_previouslyFocused != null) { _previouslyFocused.RootWindow?.Focus(); - _previouslyFocused.Focus(); + _previouslyFocused?.Focus(); _previouslyFocused = null; } diff --git a/Source/Editor/Windows/OutputLogWindow.cs b/Source/Editor/Windows/OutputLogWindow.cs index b88c06b27..1478a9ac9 100644 --- a/Source/Editor/Windows/OutputLogWindow.cs +++ b/Source/Editor/Windows/OutputLogWindow.cs @@ -176,7 +176,8 @@ namespace FlaxEditor.Windows if (Owner != null && (!Owner._searchPopup?.Visible ?? true)) { // Focus back the input field as user want to modify command from history - Owner._searchPopup?.Hide(); + Owner.HideHistory(); + Owner.HideSearch(); Owner.RootWindow.Focus(); Owner.Focus(); Owner.OnKeyDown(key); @@ -209,6 +210,7 @@ namespace FlaxEditor.Windows private OutputLogWindow _window; private ItemsListContextMenu _searchPopup; + private ItemsListContextMenu _historyPopup; private bool _isSettingText; public CommandLineBox(float x, float y, float width, OutputLogWindow window) @@ -226,6 +228,24 @@ namespace FlaxEditor.Windows _isSettingText = false; } + private void HideSearch() + { + if (_searchPopup != null) + { + _searchPopup.Hide(); + _searchPopup = null; + } + } + + private void HideHistory() + { + if (_historyPopup != null) + { + _historyPopup.Dispose(); + _historyPopup = null; + } + } + private void ShowPopup(ref ItemsListContextMenu cm, IEnumerable commands, string searchText = null) { if (cm == null) @@ -295,7 +315,7 @@ namespace FlaxEditor.Windows private void OnRootWindowLostFocus() { // Prevent popup from staying active when editor window looses focus - _searchPopup?.Hide(); + HideSearch(); if (RootWindow?.Window != null) RootWindow.Window.LostFocus -= OnRootWindowLostFocus; } @@ -330,6 +350,7 @@ namespace FlaxEditor.Windows if (isWhitespaceOnly) DebugCommands.GetAllCommands(out commands); + HideHistory(); ShowPopup(ref _searchPopup, isWhitespaceOnly ? commands : matches, text); if (isWhitespaceOnly) @@ -342,7 +363,7 @@ namespace FlaxEditor.Windows return; } } - _searchPopup?.Hide(); + HideSearch(); } /// @@ -353,7 +374,8 @@ namespace FlaxEditor.Windows case KeyboardKeys.Return: { // Run command - _searchPopup?.Hide(); + HideSearch(); + HideHistory(); var command = Text.Trim(); if (command.Length == 0) return true; @@ -430,9 +452,8 @@ namespace FlaxEditor.Windows if (_window._commandHistory != null && _window._commandHistory.Count != 0) { // Show command history popup - _searchPopup?.Hide(); - ItemsListContextMenu cm = null; - ShowPopup(ref cm, _window._commandHistory); + HideSearch(); + ShowPopup(ref _historyPopup, _window._commandHistory); } } return true; From 40413edbabb80bd85dc2cc5d2647c499f29bf59d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 13 May 2026 10:43:03 +0200 Subject: [PATCH 25/28] Fix property get/set function name length in dotnet bindings --- Source/Engine/Scripting/Runtime/DotNet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 0c1901e4b..3eea438a0 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -1636,7 +1636,7 @@ FORCE_INLINE StringAnsiView GetPropertyMethodName(MProperty* property, StringAns Platform::MemoryCopy(mem, prefix.Get(), prefix.Length()); Platform::MemoryCopy(mem + prefix.Length(), name.Get(), name.Length()); mem[name.Length() + prefix.Length()] = 0; - return StringAnsiView(mem, name.Length() + prefix.Length() + 1); + return StringAnsiView(mem, name.Length() + prefix.Length()); } MProperty::MProperty(MClass* parentClass, const char* name, void* handle, void* getterHandle, void* setterHandle, MMethodAttributes getterAttributes, MMethodAttributes setterAttributes) From c8912ad100f697718dfa75f184a3af08e0890122 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 13 May 2026 10:47:21 +0200 Subject: [PATCH 26/28] Add help support to Debug Commands via XML docs parsing --- .../SourceCodeEditing/CodeDocsModule.cs | 281 +-------------- Source/Engine/Debug/Debug.Build.cs | 8 + Source/Engine/Debug/DebugCommands.cpp | 97 ++++-- Source/Engine/Debug/DebugCommands.cs | 328 ++++++++++++++++++ Source/Engine/Debug/DebugCommands.h | 9 + 5 files changed, 429 insertions(+), 294 deletions(-) diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs index fe50e022f..26032152a 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs @@ -2,12 +2,8 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml; using FlaxEditor.Scripting; using FlaxEngine; @@ -21,7 +17,6 @@ namespace FlaxEditor.Modules.SourceCodeEditing { private Dictionary _typeCache = new Dictionary(); private Dictionary _memberCache = new Dictionary(); - private Dictionary> _xmlCache = new Dictionary>(); internal CodeDocsModule(Editor editor) : base(editor) @@ -61,13 +56,9 @@ namespace FlaxEditor.Modules.SourceCodeEditing else if (type.Type != null) { // Try to use xml docs for managed type - var xml = GetXmlDocs(type.Type.Assembly); - if (xml != null) - { - var key = "T:" + GetXmlKey(type.Type.FullName); - if (xml.TryGetValue(key, out var xmlDoc)) - text += '\n' + FilterWhitespaces(xmlDoc); - } + var xmlDoc = DebugCommands.GetXml(type.Type); + if (xmlDoc != null) + text += '\n' + xmlDoc; } _typeCache.Add(type, text); @@ -108,278 +99,20 @@ namespace FlaxEditor.Modules.SourceCodeEditing else if (member.Type != null) { // Try to use xml docs for managed member - var memberInfo = member.Type; - var xml = GetXmlDocs(memberInfo.DeclaringType.Assembly); - if (xml != null) - { - // [Reference: MSDN Magazine, October 2019, Volume 34 Number 10, "Accessing XML Documentation via Reflection"] - // https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/october/csharp-accessing-xml-documentation-via-reflection - var memberType = memberInfo.MemberType; - string key = null; - if (memberType.HasFlag(MemberTypes.Field)) - { - var fieldInfo = (FieldInfo)memberInfo; - key = "F:" + GetXmlKey(fieldInfo.DeclaringType.FullName) + "." + fieldInfo.Name; - } - else if (memberType.HasFlag(MemberTypes.Property)) - { - var propertyInfo = (PropertyInfo)memberInfo; - key = "P:" + GetXmlKey(propertyInfo.DeclaringType.FullName) + "." + propertyInfo.Name; - } - else if (memberType.HasFlag(MemberTypes.Event)) - { - var eventInfo = (EventInfo)memberInfo; - key = "E:" + GetXmlKey(eventInfo.DeclaringType.FullName) + "." + eventInfo.Name; - } - else if (memberType.HasFlag(MemberTypes.Constructor)) - { - var constructorInfo = (ConstructorInfo)memberInfo; - key = GetXmlKey(constructorInfo); - } - else if (memberType.HasFlag(MemberTypes.Method)) - { - var methodInfo = (MethodInfo)memberInfo; - key = GetXmlKey(methodInfo); - } - else if (memberType.HasFlag(MemberTypes.TypeInfo) || memberType.HasFlag(MemberTypes.NestedType)) - { - var typeInfo = (TypeInfo)memberInfo; - key = "T:" + GetXmlKey(typeInfo.FullName); - } - 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]); - } - } - } + var xmlDoc = DebugCommands.GetXml(member.Type); + if (xmlDoc != null) + text = xmlDoc; } _memberCache.Add(member, text); return text; } - // [Reference: MSDN Magazine, October 2019, Volume 34 Number 10, "Accessing XML Documentation via Reflection"] - // https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/october/csharp-accessing-xml-documentation-via-reflection - - private string GetXmlKey(MethodInfo methodInfo) - { - var typeGenericMap = new Dictionary(); - var methodGenericMap = new Dictionary(); - ParameterInfo[] parameterInfos = methodInfo.GetParameters(); - - if (methodInfo.DeclaringType.IsGenericType) - { - var methods = methodInfo.DeclaringType.GetGenericTypeDefinition().GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic); - methodInfo = methods.First(x => x.MetadataToken == methodInfo.MetadataToken); - } - - Type[] typeGenericArguments = methodInfo.DeclaringType.GetGenericArguments(); - for (int i = 0; i < typeGenericArguments.Length; i++) - { - Type typeGeneric = typeGenericArguments[i]; - typeGenericMap[typeGeneric.Name] = i; - } - - Type[] methodGenericArguments = methodInfo.GetGenericArguments(); - for (int i = 0; i < methodGenericArguments.Length; i++) - { - Type methodGeneric = methodGenericArguments[i]; - methodGenericMap[methodGeneric.Name] = i; - } - - string declarationTypeString = GetXmlKey(methodInfo.DeclaringType, false, typeGenericMap, methodGenericMap); - string memberNameString = methodInfo.Name; - string methodGenericArgumentsString = methodGenericMap.Count > 0 ? "``" + methodGenericMap.Count : string.Empty; - string parametersString = parameterInfos.Length > 0 ? "(" + string.Join(",", methodInfo.GetParameters().Select(x => GetXmlKey(x.ParameterType, true, typeGenericMap, methodGenericMap))) + ")" : string.Empty; - - string key = "M:" + declarationTypeString + "." + memberNameString + methodGenericArgumentsString + parametersString; - if (methodInfo.Name is "op_Implicit" || methodInfo.Name is "op_Explicit") - { - key += "~" + GetXmlKey(methodInfo.ReturnType, true, typeGenericMap, methodGenericMap); - } - return key; - } - - private string GetXmlKey(ConstructorInfo constructorInfo) - { - var typeGenericMap = new Dictionary(); - var methodGenericMap = new Dictionary(); - ParameterInfo[] parameterInfos = constructorInfo.GetParameters(); - - Type[] typeGenericArguments = constructorInfo.DeclaringType.GetGenericArguments(); - for (int i = 0; i < typeGenericArguments.Length; i++) - { - Type typeGeneric = typeGenericArguments[i]; - typeGenericMap[typeGeneric.Name] = i; - } - - string declarationTypeString = GetXmlKey(constructorInfo.DeclaringType, false, typeGenericMap, methodGenericMap); - string methodGenericArgumentsString = methodGenericMap.Count > 0 ? "``" + methodGenericMap.Count : string.Empty; - string parametersString = parameterInfos.Length > 0 ? "(" + string.Join(",", constructorInfo.GetParameters().Select(x => GetXmlKey(x.ParameterType, true, typeGenericMap, methodGenericMap))) + ")" : string.Empty; - - return "M:" + declarationTypeString + "." + "#ctor" + methodGenericArgumentsString + parametersString; - } - - internal static string GetXmlKey(Type type, bool isMethodParameter, Dictionary typeGenericMap, Dictionary methodGenericMap) - { - if (type.IsGenericParameter) - { - if (methodGenericMap.TryGetValue(type.Name, out var methodIndex)) - return "``" + methodIndex; - if (typeGenericMap.TryGetValue(type.Name, out var typeKey)) - return "`" + typeKey; - return "`"; - } - if (type.HasElementType) - { - string elementTypeString = GetXmlKey(type.GetElementType(), isMethodParameter, typeGenericMap, methodGenericMap); - if (type.IsPointer) - return elementTypeString + "*"; - if (type.IsByRef) - return elementTypeString + "@"; - if (type.IsArray) - { - int rank = type.GetArrayRank(); - string arrayDimensionsString = rank > 1 ? "[" + string.Join(",", Enumerable.Repeat("0:", rank)) + "]" : "[]"; - return elementTypeString + arrayDimensionsString; - } - throw new Exception(); - } - - string prefaceString = type.IsNested ? GetXmlKey(type.DeclaringType, isMethodParameter, typeGenericMap, methodGenericMap) + "." : type.Namespace + "."; - string typeNameString = isMethodParameter ? Regex.Replace(type.Name, @"`\d+", string.Empty) : type.Name; - string genericArgumentsString = type.IsGenericType && isMethodParameter ? "{" + string.Join(",", type.GetGenericArguments().Select(argument => GetXmlKey(argument, true, typeGenericMap, methodGenericMap))) + "}" : string.Empty; - return prefaceString + typeNameString + genericArgumentsString; - } - - private static string GetXmlKey(string typeFullNameString) - { - return Regex.Replace(typeFullNameString, @"\[.*\]", string.Empty).Replace('+', '.'); - } - - private static string FilterWhitespaces(string str) - { - if (str.Contains(" ", StringComparison.Ordinal)) - { - var sb = new StringBuilder(); - var prev = str[0]; - sb.Append(prev); - for (int i = 1; i < str.Length; i++) - { - var c = str[i]; - if (prev != ' ' || c != ' ') - { - sb.Append(c); - } - prev = c; - } - str = sb.ToString(); - } - return str; - } - - private Dictionary GetXmlDocs(Assembly assembly) - { - if (!_xmlCache.TryGetValue(assembly, out var result)) - { - Profiler.BeginEvent("GetXmlDocs"); - - var assemblyPath = Utils.GetAssemblyLocation(assembly); - var assemblyName = assembly.GetName().Name; - var xmlFilePath = Path.ChangeExtension(assemblyPath, ".xml"); - if (!File.Exists(assemblyPath) && !string.IsNullOrEmpty(assemblyPath)) - { - var uri = new UriBuilder(assemblyPath); - var path = Uri.UnescapeDataString(uri.Path); - xmlFilePath = Path.Combine(Path.GetDirectoryName(path), assemblyName + ".xml"); - } - if (File.Exists(xmlFilePath)) - { - Profiler.BeginEvent(assemblyName); - try - { - // Parse xml documentation - using (var xmlReader = XmlReader.Create(new StreamReader(xmlFilePath))) - { - result = new Dictionary(); - StringBuilder content = new StringBuilder(2048); - while (xmlReader.Read()) - { - if (xmlReader.NodeType == XmlNodeType.Element && string.Equals(xmlReader.Name, "member", StringComparison.Ordinal)) - { - string rawName = xmlReader["name"]; - var memberReader = xmlReader.ReadSubtree(); - if (memberReader.ReadToDescendant("summary")) - { - content.Clear(); - do - { - if (memberReader.NodeType == XmlNodeType.Element && memberReader.Read()) - { - while (memberReader.NodeType == XmlNodeType.Text) - { - content.Append(memberReader.Value); - if (memberReader.Read() && memberReader.NodeType == XmlNodeType.Element) - { - var nodeRef = TrimRef(memberReader.GetAttribute("cref")); // - if (nodeRef == null) - nodeRef = memberReader.GetAttribute("name"); // - content.Append(nodeRef); - memberReader.Read(); - - string TrimRef(string str) - { - if (str == null) - return null; - if (str.IndexOf(":FlaxEngine.") == 1) - return str.Substring("T:FlaxEngine.".Length); - return str.Substring("T:".Length); - } - } - } - } - - if (memberReader.NodeType == XmlNodeType.EndElement && memberReader.Name == "summary") - break; - } while (memberReader.Read()); - - result[rawName] = content.ToString().Trim(' ', '\r', '\n'); - } - } - } - } - } - catch - { - // Ignore errors - } - Profiler.EndEvent(); - } - - _xmlCache[assembly] = result; - Profiler.EndEvent(); - } - return result; - } - private void OnTypesCleared() { _typeCache.Clear(); _memberCache.Clear(); - - foreach (var asm in _xmlCache.Keys.ToArray()) - { - if (asm.IsCollectible) - _xmlCache.Remove(asm); - } + DebugCommands.ClearXml(); } /// diff --git a/Source/Engine/Debug/Debug.Build.cs b/Source/Engine/Debug/Debug.Build.cs index 76dd20137..2208d80bd 100644 --- a/Source/Engine/Debug/Debug.Build.cs +++ b/Source/Engine/Debug/Debug.Build.cs @@ -19,6 +19,14 @@ public class Debug : EngineModule { options.PublicDefinitions.Add("COMPILE_WITH_DEBUG_DRAW"); } + + if (options.Target.IsEditor || options.Configuration != TargetConfiguration.Release) + { + // Used by DebugCommands to parse Xml documentation + options.ScriptingAPI.SystemReferences.Add("System.Xml"); + options.ScriptingAPI.SystemReferences.Add("System.Xml.ReaderWriter"); + options.ScriptingAPI.SystemReferences.Add("System.Text.RegularExpressions"); + } } /// diff --git a/Source/Engine/Debug/DebugCommands.cpp b/Source/Engine/Debug/DebugCommands.cpp index 58cf2894b..feb985cd4 100644 --- a/Source/Engine/Debug/DebugCommands.cpp +++ b/Source/Engine/Debug/DebugCommands.cpp @@ -16,8 +16,11 @@ #include "Engine/Scripting/ManagedCLR/MMethod.h" #include "Engine/Scripting/ManagedCLR/MField.h" #include "Engine/Scripting/ManagedCLR/MProperty.h" +#include "Engine/Scripting/ManagedCLR/MUtils.h" #include "FlaxEngine.Gen.h" +#define WITH_HELP (USE_EDITOR || !BUILD_RELEASE) && USE_CSHARP + struct CommandData { String Name; @@ -26,6 +29,38 @@ struct CommandData void* MethodGet = nullptr; void* MethodSet = nullptr; void* Field = nullptr; +#if WITH_HELP + mutable String Help; + + StringView GetHelp() const + { + if (Help.IsEmpty()) + { + if (dynamic_cast(Module)) + { + // Get C# type and member name + const MClass* mclass = nullptr; + StringAnsiView name; + if (auto field = (MField*)Field) + { + mclass = field->GetParentClass(); + name = field->GetName(); + } + else if (auto method = (MMethod*)(Method ? Method : (MethodGet ? MethodGet : MethodSet))) + { + mclass = method->GetParentClass(); + name = method->GetName(); + } + + // Use Xml docs reader used by Editor to get tooltips + auto getXmlInternal = DebugCommands::TypeInitializer.GetClass()->GetMethod("GetXmlInternal", 2); + void* params[2] = { INTERNAL_TYPE_GET_OBJECT(mclass->GetType()), MUtils::ToString(name) }; + Help = MUtils::ToString((MString*)getXmlInternal->Invoke(nullptr, params, nullptr)); + } + } + return Help; + } +#endif static void PrettyPrint(StringBuilder& sb, const Variant& value) { @@ -122,7 +157,11 @@ struct CommandData // Parse arguments if (args == StringView(TEXT("?"), 1)) { - LOG(Warning, "TODO: debug commands help/docs printing"); // TODO: debug commands help/docs printing (use CodeDocsModule that parses XML docs) +#if WITH_HELP + // Print command description + LOG(Info, "> {} ?", Name); + LOG_STR(Info, GetHelp()); +#endif return; } Array params; @@ -356,6 +395,22 @@ namespace InitCommands(); Locker.Unlock(); } + + const CommandData* GetCommand(StringView command) + { + if (command.FindLast(' ') != -1) + command = command.Left(command.Find(' ')); + // TODO: fix missing string handle on 1st command execution (command gets invalid after InitCommands due to dotnet GC or dotnet interop handles flush) + String commandCopy = command; + command = commandCopy; + EnsureInited(); + for (auto& e : Commands) + { + if (e.Name == command) + return &e; + } + return nullptr; + } } class DebugCommandsService : public EngineService @@ -475,30 +530,32 @@ void DebugCommands::GetAllCommands(Array& commands) DebugCommands::CommandFlags DebugCommands::GetCommandFlags(StringView command) { CommandFlags result = CommandFlags::None; - if (command.FindLast(' ') != -1) - command = command.Left(command.Find(' ')); - // TODO: fix missing string handle on 1st command execution (command gets invalid after InitCommands due to dotnet GC or dotnet interop handles flush) - String commandCopy = command; - command = commandCopy; - EnsureInited(); - for (auto& e : Commands) + if (auto cmd = GetCommand(command)) { - if (e.Name == command) - { - if (e.Method) - result |= CommandFlags::Exec; - else if (e.Field) - result |= CommandFlags::ReadWrite; - if (e.MethodGet) - result |= CommandFlags::Read; - if (e.MethodSet) - result |= CommandFlags::Write; - break; - } + if (cmd->Method) + result |= CommandFlags::Exec; + else if (cmd->Field) + result |= CommandFlags::ReadWrite; + if (cmd->MethodGet) + result |= CommandFlags::Read; + if (cmd->MethodSet) + result |= CommandFlags::Write; } return result; } +StringView DebugCommands::GetCommandHelp(StringView command) +{ + StringView result; +#if WITH_HELP + if (auto cmd = GetCommand(command)) + { + result = cmd->GetHelp(); + } +#endif + return result; +} + bool DebugCommands::Iterate(const StringView& searchText, int32& index) { EnsureInited(); diff --git a/Source/Engine/Debug/DebugCommands.cs b/Source/Engine/Debug/DebugCommands.cs index 93381bd0f..3680c6a97 100644 --- a/Source/Engine/Debug/DebugCommands.cs +++ b/Source/Engine/Debug/DebugCommands.cs @@ -1,6 +1,17 @@ // Copyright (c) Wojciech Figat. All rights reserved. +#if USE_EDITOR || !BUILD_RELEASE +#define WITH_HELP +#endif + using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; namespace FlaxEngine { @@ -12,4 +23,321 @@ namespace FlaxEngine public sealed class DebugCommand : Attribute { } + + partial class DebugCommands + { +#if WITH_HELP + private static Dictionary> _xmlCache = new(); + + internal static void ClearXml() + { + foreach (var asm in _xmlCache.Keys.ToArray()) + { + if (asm.IsCollectible) + _xmlCache.Remove(asm); + } + } + + internal static string GetXmlInternal(Type type, string memberName) + { + // Redirect into type when no member specified + if (string.IsNullOrEmpty(memberName)) + return GetXml(type); + + // Redirect property function getter/setter into owning property docs + if (memberName.StartsWith("get_") || memberName.StartsWith("set_")) + memberName = memberName.Substring(4); + + // Find member of that name + var members = type.GetMember(memberName, BindingFlags.Static | BindingFlags.Public); + if (members.Length == 0) + return null; + return GetXml(members[0]); + } + + /// + /// Gets the XML docs text for the type. + /// + /// The type. + /// The documentation help. + public static string GetXml(Type type) + { + var xml = GetXmlDocs(type.Assembly); + if (xml != null) + { + var key = "T:" + GetXmlKey(type.FullName); + if (xml.TryGetValue(key, out var xmlDoc)) + return FilterWhitespaces(xmlDoc); + } + return null; + } + + /// + /// Gets the XML docs text for the type member. + /// + /// The type member. + /// The documentation help. + public static string GetXml(MemberInfo member) + { + string text = null; + var xml = GetXmlDocs(member.DeclaringType.Assembly); + if (xml != null) + { + // [Reference: MSDN Magazine, October 2019, Volume 34 Number 10, "Accessing XML Documentation via Reflection"] + // https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/october/csharp-accessing-xml-documentation-via-reflection + var memberType = member.MemberType; + string key = null; + if (memberType.HasFlag(MemberTypes.Field)) + { + var fieldInfo = (FieldInfo)member; + key = "F:" + GetXmlKey(fieldInfo.DeclaringType.FullName) + "." + fieldInfo.Name; + } + else if (memberType.HasFlag(MemberTypes.Property)) + { + var propertyInfo = (PropertyInfo)member; + key = "P:" + GetXmlKey(propertyInfo.DeclaringType.FullName) + "." + propertyInfo.Name; + } + else if (memberType.HasFlag(MemberTypes.Event)) + { + var eventInfo = (EventInfo)member; + key = "E:" + GetXmlKey(eventInfo.DeclaringType.FullName) + "." + eventInfo.Name; + } + else if (memberType.HasFlag(MemberTypes.Constructor)) + { + var constructorInfo = (ConstructorInfo)member; + key = GetXmlKey(constructorInfo); + } + else if (memberType.HasFlag(MemberTypes.Method)) + { + var methodInfo = (MethodInfo)member; + key = GetXmlKey(methodInfo); + } + else if (memberType.HasFlag(MemberTypes.TypeInfo) || memberType.HasFlag(MemberTypes.NestedType)) + { + var typeInfo = (TypeInfo)member; + key = "T:" + GetXmlKey(typeInfo.FullName); + } + 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]); + } + } + } + return text; + } + + private static string FilterWhitespaces(string str) + { + if (str.Contains(" ", StringComparison.Ordinal)) + { + var sb = new StringBuilder(); + var prev = str[0]; + sb.Append(prev); + for (int i = 1; i < str.Length; i++) + { + var c = str[i]; + if (prev != ' ' || c != ' ') + { + sb.Append(c); + } + prev = c; + } + str = sb.ToString(); + } + return str; + } + + // [Reference: MSDN Magazine, October 2019, Volume 34 Number 10, "Accessing XML Documentation via Reflection"] + // https://docs.microsoft.com/en-us/archive/msdn-magazine/2019/october/csharp-accessing-xml-documentation-via-reflection + + private static string GetXmlKey(MethodInfo methodInfo) + { + var typeGenericMap = new Dictionary(); + var methodGenericMap = new Dictionary(); + ParameterInfo[] parameterInfos = methodInfo.GetParameters(); + + if (methodInfo.DeclaringType.IsGenericType) + { + var methods = methodInfo.DeclaringType.GetGenericTypeDefinition().GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic); + methodInfo = methods.First(x => x.MetadataToken == methodInfo.MetadataToken); + } + + Type[] typeGenericArguments = methodInfo.DeclaringType.GetGenericArguments(); + for (int i = 0; i < typeGenericArguments.Length; i++) + { + Type typeGeneric = typeGenericArguments[i]; + typeGenericMap[typeGeneric.Name] = i; + } + + Type[] methodGenericArguments = methodInfo.GetGenericArguments(); + for (int i = 0; i < methodGenericArguments.Length; i++) + { + Type methodGeneric = methodGenericArguments[i]; + methodGenericMap[methodGeneric.Name] = i; + } + + string declarationTypeString = GetXmlKey(methodInfo.DeclaringType, false, typeGenericMap, methodGenericMap); + string memberNameString = methodInfo.Name; + string methodGenericArgumentsString = methodGenericMap.Count > 0 ? "``" + methodGenericMap.Count : string.Empty; + string parametersString = parameterInfos.Length > 0 ? "(" + string.Join(",", methodInfo.GetParameters().Select(x => GetXmlKey(x.ParameterType, true, typeGenericMap, methodGenericMap))) + ")" : string.Empty; + + string key = "M:" + declarationTypeString + "." + memberNameString + methodGenericArgumentsString + parametersString; + if (methodInfo.Name is "op_Implicit" || methodInfo.Name is "op_Explicit") + { + key += "~" + GetXmlKey(methodInfo.ReturnType, true, typeGenericMap, methodGenericMap); + } + return key; + } + + private static string GetXmlKey(ConstructorInfo constructorInfo) + { + var typeGenericMap = new Dictionary(); + var methodGenericMap = new Dictionary(); + ParameterInfo[] parameterInfos = constructorInfo.GetParameters(); + + Type[] typeGenericArguments = constructorInfo.DeclaringType.GetGenericArguments(); + for (int i = 0; i < typeGenericArguments.Length; i++) + { + Type typeGeneric = typeGenericArguments[i]; + typeGenericMap[typeGeneric.Name] = i; + } + + string declarationTypeString = GetXmlKey(constructorInfo.DeclaringType, false, typeGenericMap, methodGenericMap); + string methodGenericArgumentsString = methodGenericMap.Count > 0 ? "``" + methodGenericMap.Count : string.Empty; + string parametersString = parameterInfos.Length > 0 ? "(" + string.Join(",", constructorInfo.GetParameters().Select(x => GetXmlKey(x.ParameterType, true, typeGenericMap, methodGenericMap))) + ")" : string.Empty; + + return "M:" + declarationTypeString + "." + "#ctor" + methodGenericArgumentsString + parametersString; + } + + internal static string GetXmlKey(Type type, bool isMethodParameter, Dictionary typeGenericMap, Dictionary methodGenericMap) + { + if (type.IsGenericParameter) + { + if (methodGenericMap.TryGetValue(type.Name, out var methodIndex)) + return "``" + methodIndex; + if (typeGenericMap.TryGetValue(type.Name, out var typeKey)) + return "`" + typeKey; + return "`"; + } + if (type.HasElementType) + { + string elementTypeString = GetXmlKey(type.GetElementType(), isMethodParameter, typeGenericMap, methodGenericMap); + if (type.IsPointer) + return elementTypeString + "*"; + if (type.IsByRef) + return elementTypeString + "@"; + if (type.IsArray) + { + int rank = type.GetArrayRank(); + string arrayDimensionsString = rank > 1 ? "[" + string.Join(",", Enumerable.Repeat("0:", rank)) + "]" : "[]"; + return elementTypeString + arrayDimensionsString; + } + throw new Exception(); + } + + string prefaceString = type.IsNested ? GetXmlKey(type.DeclaringType, isMethodParameter, typeGenericMap, methodGenericMap) + "." : type.Namespace + "."; + string typeNameString = isMethodParameter ? Regex.Replace(type.Name, @"`\d+", string.Empty) : type.Name; + string genericArgumentsString = type.IsGenericType && isMethodParameter ? "{" + string.Join(",", type.GetGenericArguments().Select(argument => GetXmlKey(argument, true, typeGenericMap, methodGenericMap))) + "}" : string.Empty; + return prefaceString + typeNameString + genericArgumentsString; + } + + private static string GetXmlKey(string typeFullNameString) + { + return Regex.Replace(typeFullNameString, @"\[.*\]", string.Empty).Replace('+', '.'); + } + + private static Dictionary GetXmlDocs(Assembly assembly) + { + if (!_xmlCache.TryGetValue(assembly, out var result)) + { + Profiler.BeginEvent("GetXmlDocs"); + + // Find XML file path (based on assembly location) + var assemblyPath = Utils.GetAssemblyLocation(assembly); + var assemblyName = assembly.GetName().Name; + var xmlFilePath = Path.ChangeExtension(assemblyPath, ".xml"); + if (!File.Exists(assemblyPath) && !string.IsNullOrEmpty(assemblyPath)) + { + var uri = new UriBuilder(assemblyPath); + var path = Uri.UnescapeDataString(uri.Path); + xmlFilePath = Path.Combine(Path.GetDirectoryName(path), assemblyName + ".xml"); + } + if (File.Exists(xmlFilePath)) + { + Profiler.BeginEvent(assemblyName); + try + { + // Parse xml documentation + using (var xmlReader = XmlReader.Create(new StreamReader(xmlFilePath))) + { + result = new Dictionary(); + StringBuilder content = new StringBuilder(2048); + while (xmlReader.Read()) + { + if (xmlReader.NodeType == XmlNodeType.Element && string.Equals(xmlReader.Name, "member", StringComparison.Ordinal)) + { + string rawName = xmlReader["name"]; + var memberReader = xmlReader.ReadSubtree(); + if (memberReader.ReadToDescendant("summary")) + { + content.Clear(); + do + { + if (memberReader.NodeType == XmlNodeType.Element && memberReader.Read()) + { + while (memberReader.NodeType == XmlNodeType.Text) + { + content.Append(memberReader.Value); + if (memberReader.Read() && memberReader.NodeType == XmlNodeType.Element) + { + var nodeRef = TrimRef(memberReader.GetAttribute("cref")); // + if (nodeRef == null) + nodeRef = memberReader.GetAttribute("name"); // + content.Append(nodeRef); + memberReader.Read(); + + string TrimRef(string str) + { + if (str == null) + return null; + if (str.IndexOf(":FlaxEngine.") == 1) + return str.Substring("T:FlaxEngine.".Length); + return str.Substring("T:".Length); + } + } + } + } + + if (memberReader.NodeType == XmlNodeType.EndElement && memberReader.Name == "summary") + break; + } while (memberReader.Read()); + + result[rawName] = content.ToString().Trim(' ', '\r', '\n'); + } + } + } + } + } + catch + { + // Ignore errors + } + Profiler.EndEvent(); + } + + _xmlCache[assembly] = result; + Profiler.EndEvent(); + } + return result; + } +#endif + } } diff --git a/Source/Engine/Debug/DebugCommands.h b/Source/Engine/Debug/DebugCommands.h index f25fe0581..71c90f30c 100644 --- a/Source/Engine/Debug/DebugCommands.h +++ b/Source/Engine/Debug/DebugCommands.h @@ -56,8 +56,17 @@ public: /// Returns flags of the command. /// /// The full name of the command. + /// Command flags. API_FUNCTION() static CommandFlags GetCommandFlags(StringView command); + /// + /// Returns help text of the command (from documentation comment). + /// + /// Only available in non-Release builds and Editor. + /// The full name of the command. + /// Command help text or empty if failed to get it. + API_FUNCTION() static StringView GetCommandHelp(StringView command); + public: static bool Iterate(const StringView& searchText, int32& index); static StringView GetCommandName(int32 index); From 253442abd18ee5f32daffc04a2ff64a5c6e83272 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 13 May 2026 10:58:50 +0200 Subject: [PATCH 27/28] Add filtering whitespaces to Xml doc comments of type members and optimize filtering --- Source/Engine/Debug/DebugCommands.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Debug/DebugCommands.cs b/Source/Engine/Debug/DebugCommands.cs index 3680c6a97..a75a3cbb2 100644 --- a/Source/Engine/Debug/DebugCommands.cs +++ b/Source/Engine/Debug/DebugCommands.cs @@ -27,10 +27,13 @@ namespace FlaxEngine partial class DebugCommands { #if WITH_HELP - private static Dictionary> _xmlCache = new(); + private static Dictionary> _xmlCache; + private static StringBuilder _sb; internal static void ClearXml() { + if (_xmlCache == null) + return; foreach (var asm in _xmlCache.Keys.ToArray()) { if (asm.IsCollectible) @@ -118,7 +121,10 @@ namespace FlaxEngine key = "T:" + GetXmlKey(typeInfo.FullName); } if (key != null) + { xml.TryGetValue(key, out text); + text = FilterWhitespaces(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)) @@ -138,16 +144,18 @@ namespace FlaxEngine { if (str.Contains(" ", StringComparison.Ordinal)) { - var sb = new StringBuilder(); + if (_sb == null) + _sb = new StringBuilder(); + else + _sb.Clear(); + var sb = _sb; var prev = str[0]; sb.Append(prev); for (int i = 1; i < str.Length; i++) { var c = str[i]; if (prev != ' ' || c != ' ') - { sb.Append(c); - } prev = c; } str = sb.ToString(); @@ -256,6 +264,8 @@ namespace FlaxEngine private static Dictionary GetXmlDocs(Assembly assembly) { + if (_xmlCache == null) + _xmlCache = new Dictionary>(); if (!_xmlCache.TryGetValue(assembly, out var result)) { Profiler.BeginEvent("GetXmlDocs"); From 97bcdacd9a69f31b26da965916dbddd7af619939 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 13 May 2026 15:43:44 +0200 Subject: [PATCH 28/28] Fix minor doc issues --- Source/Engine/Debug/DebugCommands.cs | 2 +- Source/Engine/Engine/Globals.h | 8 -------- Source/Engine/Graphics/Graphics.h | 16 ++++++++-------- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/Source/Engine/Debug/DebugCommands.cs b/Source/Engine/Debug/DebugCommands.cs index a75a3cbb2..afe7e3cf6 100644 --- a/Source/Engine/Debug/DebugCommands.cs +++ b/Source/Engine/Debug/DebugCommands.cs @@ -142,7 +142,7 @@ namespace FlaxEngine private static string FilterWhitespaces(string str) { - if (str.Contains(" ", StringComparison.Ordinal)) + if (str != null && str.Contains(" ", StringComparison.Ordinal)) { if (_sb == null) _sb = new StringBuilder(); diff --git a/Source/Engine/Engine/Globals.h b/Source/Engine/Engine/Globals.h index 16e2c2ce3..62c510f60 100644 --- a/Source/Engine/Engine/Globals.h +++ b/Source/Engine/Engine/Globals.h @@ -13,8 +13,6 @@ API_CLASS(Static, Attributes="DebugCommand") class FLAXENGINE_API Globals DECLARE_SCRIPTING_TYPE_NO_SPAWN(Globals); public: - // Paths - // Main engine directory path. API_FIELD(ReadOnly) static String StartupFolder; @@ -54,8 +52,6 @@ public: #endif public: - // State - // True if fatal error occurred (engine is exiting). // [Deprecated in v1.10] DEPRECATED("Use Engine::FatalError instead.") static bool FatalErrorOccurred; @@ -91,14 +87,10 @@ public: DEPRECATED("Use Engine::ExitCode instead.") static int32 ExitCode; public: - // Threading - // Main Engine thread id. API_FIELD(ReadOnly) static uint64 MainThreadID; public: - // Config - /// /// The full engine version. /// diff --git a/Source/Engine/Graphics/Graphics.h b/Source/Engine/Graphics/Graphics.h index aed1232f6..a518f6d6d 100644 --- a/Source/Engine/Graphics/Graphics.h +++ b/Source/Engine/Graphics/Graphics.h @@ -19,32 +19,32 @@ public: API_FIELD() static bool UseVSync; /// - /// Anti Aliasing quality setting. + /// Anti Aliasing quality setting. Available values are: Low, Medium, High, Ultra (or 0, 1, 2, 3). /// API_FIELD() static Quality AAQuality; /// - /// Screen Space Reflections quality setting. + /// Screen Space Reflections quality setting. Available values are: Low, Medium, High, Ultra (or 0, 1, 2, 3). /// API_FIELD() static Quality SSRQuality; /// - /// Screen Space Ambient Occlusion quality setting. + /// Screen Space Ambient Occlusion quality setting. Available values are: Low, Medium, High, Ultra (or 0, 1, 2, 3). /// API_FIELD() static Quality SSAOQuality; /// - /// Volumetric Fog quality setting. + /// Volumetric Fog quality setting. Available values are: Low, Medium, High, Ultra (or 0, 1, 2, 3). /// API_FIELD() static Quality VolumetricFogQuality; /// - /// The shadows quality. + /// The shadows quality. Available values are: Low, Medium, High, Ultra (or 0, 1, 2, 3). /// API_FIELD() static Quality ShadowsQuality; /// - /// The shadow maps quality (textures resolution). + /// The shadow maps quality (textures resolution). Available values are: Low, Medium, High, Ultra (or 0, 1, 2, 3). /// API_FIELD() static Quality ShadowMapsQuality; @@ -59,12 +59,12 @@ public: API_FIELD() static bool AllowCSMBlending; /// - /// The Global SDF quality. Controls the volume texture resolution and amount of cascades to use. + /// The Global SDF quality. Controls the volume texture resolution and amount of cascades to use. Available values are: Low, Medium, High, Ultra (or 0, 1, 2, 3). /// API_FIELD() static Quality GlobalSDFQuality; /// - /// The Global Illumination quality. Controls the quality of the GI effect. + /// The Global Illumination quality. Controls the quality of the GI effect. Available values are: Low, Medium, High, Ultra (or 0, 1, 2, 3). /// API_FIELD() static Quality GIQuality;