diff --git a/.github/actions/vulkan/action.yml b/.github/actions/vulkan/action.yml
index 9c547539c..0ede7f696 100644
--- a/.github/actions/vulkan/action.yml
+++ b/.github/actions/vulkan/action.yml
@@ -3,7 +3,7 @@ description: Downloads and installs Vulkan SDK.
inputs:
vulkan-version:
description: 'Vulkan SDK release version (e.g. 1.2.198.1).'
- default: '1.4.350.0'
+ default: '1.4.363.0'
required: false
runs:
using: "composite"
@@ -17,7 +17,7 @@ runs:
case `uname -s` in
Linux)
export VULKAN_SDK_ROOT=`pwd`/../VulkanSDK
- wget "https://sdk.lunarg.com/sdk/download/$VULKAN_SDK_VER/linux/vulkan-sdk.tar.gz" --no-verbose -O vulkan-sdk.tar.gz
+ wget "https://sdk.lunarg.com/sdk/download/$VULKAN_SDK_VER/linux/vulkansdk-linux-x86_64-$VULKAN_SDK_VER.tar.xz" --no-verbose -O vulkan-sdk.tar.gz
mkdir $VULKAN_SDK_ROOT
tar -xf vulkan-sdk.tar.gz -C $VULKAN_SDK_ROOT
export VULKAN_SDK=$VULKAN_SDK_ROOT/$VULKAN_SDK_VER/x86_64
diff --git a/Flax.sln.DotSettings b/Flax.sln.DotSettings
index 777ddbf8d..b4fd6eb3e 100644
--- a/Flax.sln.DotSettings
+++ b/Flax.sln.DotSettings
@@ -256,6 +256,7 @@
True
True
True
+ True
True
True
DisabledByUser
diff --git a/Source/Editor/Content/Items/BinaryAssetItem.cs b/Source/Editor/Content/Items/BinaryAssetItem.cs
index c8167d947..ba573ca61 100644
--- a/Source/Editor/Content/Items/BinaryAssetItem.cs
+++ b/Source/Editor/Content/Items/BinaryAssetItem.cs
@@ -120,7 +120,7 @@ namespace FlaxEditor.Content
///
public override Actor OnEditorDrop(object context)
{
- return new StaticModel { Model = FlaxEngine.Content.LoadAsync(ID) };
+ return new StaticModel { Model = FlaxEngine.Content.Load(ID) };
}
///
@@ -171,7 +171,7 @@ namespace FlaxEditor.Content
///
public override Actor OnEditorDrop(object context)
{
- return new AnimatedModel { SkinnedModel = FlaxEngine.Content.LoadAsync(ID) };
+ return new AnimatedModel { SkinnedModel = FlaxEngine.Content.Load(ID) };
}
///
diff --git a/Source/Editor/Content/Items/PrefabItem.cs b/Source/Editor/Content/Items/PrefabItem.cs
index a0cd23e88..5a3565f39 100644
--- a/Source/Editor/Content/Items/PrefabItem.cs
+++ b/Source/Editor/Content/Items/PrefabItem.cs
@@ -32,7 +32,7 @@ namespace FlaxEditor.Content
///
public override Actor OnEditorDrop(object context)
{
- return PrefabManager.SpawnPrefab(FlaxEngine.Content.LoadAsync(ID), null);
+ return PrefabManager.SpawnPrefab(FlaxEngine.Content.Load(ID), null);
}
///
diff --git a/Source/Editor/Cooker/CookingData.h b/Source/Editor/Cooker/CookingData.h
index 038aa1650..166f6adda 100644
--- a/Source/Editor/Cooker/CookingData.h
+++ b/Source/Editor/Cooker/CookingData.h
@@ -67,14 +67,16 @@ API_ENUM() enum class BuildPlatform
///
/// Universal Windows Platform (UWP) (x86 architecture)
+ /// [Deprecated and unsupported platform]
///
- API_ENUM(Attributes="EditorDisplay(null, \"Windows Store x86\")")
+ API_ENUM(Attributes="EditorDisplay(null, \"Windows Store x86\"), Obsolete")
UWPx86 = 3,
///
/// Universal Windows Platform (UWP) (x64 architecture)
+ /// [Deprecated and unsupported platform]
///
- API_ENUM(Attributes="EditorDisplay(null, \"Windows Store x64\")")
+ API_ENUM(Attributes="EditorDisplay(null, \"Windows Store x64\"), Obsolete")
UWPx64 = 4,
///
diff --git a/Source/Editor/Cooker/GameCooker.cs b/Source/Editor/Cooker/GameCooker.cs
index ef91dd5f9..5c84b0b0b 100644
--- a/Source/Editor/Cooker/GameCooker.cs
+++ b/Source/Editor/Cooker/GameCooker.cs
@@ -94,8 +94,10 @@ namespace FlaxEditor
{
case BuildPlatform.Windows32:
case BuildPlatform.Windows64: return PlatformType.Windows;
+#pragma warning disable CS0612
case BuildPlatform.UWPx86:
case BuildPlatform.UWPx64: return PlatformType.UWP;
+#pragma warning restore CS0612
case BuildPlatform.XboxOne: return PlatformType.XboxOne;
case BuildPlatform.LinuxX64: return PlatformType.Linux;
case BuildPlatform.PS4: return PlatformType.PS4;
diff --git a/Source/Editor/CustomEditorWindow.cs b/Source/Editor/CustomEditorWindow.cs
index 4a94e0a34..4711ee9b2 100644
--- a/Source/Editor/CustomEditorWindow.cs
+++ b/Source/Editor/CustomEditorWindow.cs
@@ -1,6 +1,5 @@
// Copyright (c) Wojciech Figat. All rights reserved.
-using System;
using FlaxEditor.CustomEditors;
using FlaxEditor.GUI.Docking;
using FlaxEditor.Windows;
@@ -92,19 +91,7 @@ namespace FlaxEditor
Editor.Instance.Windows.AddToRestore(this);
}
Window.Close(ClosingReason.ScriptsReload);
- }
-
- ///
- /// Reattaches the window control to existing floating window.
- ///
- /// The window handle.
- /// Initial window state.
- /// The panel to dock to, if any.
- /// Only used if is set. If true the window will be selected after docking it.
- /// The splitter value to use if toDock is not null. If not specified, a default value will be used.
- public void Restore(IntPtr windowHandle, DockState state = DockState.Float, DockPanel toDock = null, bool autoSelect = true, float? splitterValue = null)
- {
- _win.Restore(windowHandle, state, toDock, autoSelect, splitterValue);
+ Window.Dispose();
}
///
diff --git a/Source/Editor/CustomEditors/Editors/TypeEditor.cs b/Source/Editor/CustomEditors/Editors/TypeEditor.cs
index fec571017..8a77fe581 100644
--- a/Source/Editor/CustomEditors/Editors/TypeEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/TypeEditor.cs
@@ -147,6 +147,7 @@ namespace FlaxEditor.CustomEditors.Editors
var nameRect = new Rectangle(2, 1, frameRect.Width - 4, 14);
var button1Rect = new Rectangle(nameRect.Right + 2, 1, 14, 14);
var button2Rect = new Rectangle(button1Rect.Right + 2, 1, 14, 14);
+ var enabled = EnabledInHierarchy;
// Draw frame
Render2D.DrawRectangle(frameRect, IsMouseOver ? style.BorderHighlighted : style.BorderNormal);
@@ -156,17 +157,17 @@ namespace FlaxEditor.CustomEditors.Editors
{
// Draw deselect button
if (_type == ScriptType.Null)
- Render2D.DrawSprite(style.Cross, button1Rect, button1Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey);
+ Render2D.DrawSprite(style.Cross, button1Rect, enabled ? (button1Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey) : style.ForegroundDisabled);
// Draw name
Render2D.PushClip(nameRect);
- Render2D.DrawText(style.FontMedium, _valueName, nameRect, style.Foreground, TextAlignment.Near, TextAlignment.Center);
+ Render2D.DrawText(style.FontMedium, _valueName, nameRect, enabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center);
Render2D.PopClip();
}
else
{
// Draw info
- Render2D.DrawText(style.FontMedium, "-", nameRect, Color.OrangeRed, TextAlignment.Near, TextAlignment.Center);
+ Render2D.DrawText(style.FontMedium, "-", nameRect, enabled ? Color.OrangeRed : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center);
}
// Draw picker button
diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs
index 9d7a1484e..65370a928 100644
--- a/Source/Editor/Editor.cs
+++ b/Source/Editor/Editor.cs
@@ -1479,6 +1479,16 @@ namespace FlaxEditor
if (preset == null)
{
Editor.LogWarning("Missing preset.");
+ if (settings.Presets != null)
+ {
+ Editor.LogWarning("Available presets:");
+ foreach (var p in settings.Presets)
+ {
+ Editor.LogWarning($" > {p.Name}");
+ foreach (var t in p.Targets)
+ Editor.LogWarning($" * {t.Name} ({t.Platform}, {t.Mode})");
+ }
+ }
return true;
}
diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs
index 7d0e6c20b..006653a7e 100644
--- a/Source/Editor/GUI/AssetPicker.cs
+++ b/Source/Editor/GUI/AssetPicker.cs
@@ -220,9 +220,8 @@ namespace FlaxEditor.GUI
// Check if drag is over
if (IsDragOver && _dragOverElement != null && _dragOverElement.HasValidDrag)
{
- var bounds = new Rectangle(Float2.Zero, Size);
- Render2D.FillRectangle(bounds, style.Selection);
- Render2D.DrawRectangle(bounds, style.SelectionBorder);
+ Render2D.FillRectangle(iconRect, style.Selection);
+ Render2D.DrawRectangle(iconRect, style.SelectionBorder);
}
// Navigation focus highlight
diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs b/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs
index 595dfa76d..8ff118f65 100644
--- a/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs
+++ b/Source/Editor/GUI/ContextMenu/ContextMenuButton.cs
@@ -40,6 +40,11 @@ namespace FlaxEditor.GUI.ContextMenu
///
public string ShortKeys;
+ ///
+ /// The text color.
+ ///
+ public Color TextColor;
+
///
/// Item icon (best is 16x16).
///
@@ -71,6 +76,7 @@ namespace FlaxEditor.GUI.ContextMenu
{
Text = text;
ShortKeys = shortKeys;
+ TextColor = Style.Current.Foreground;
}
///
@@ -122,7 +128,7 @@ namespace FlaxEditor.GUI.ContextMenu
var style = Style.Current;
var backgroundRect = new Rectangle(-X + 3, 0, Parent.Width - 6, Height);
var textRect = new Rectangle(0, 0, Width - 8, Height);
- var textColor = Enabled ? style.Foreground : style.ForegroundDisabled;
+ var textColor = Enabled ? TextColor : style.ForegroundDisabled;
// Draw background
if (IsMouseOver && Enabled)
diff --git a/Source/Editor/GUI/CurveEditor.Base.cs b/Source/Editor/GUI/CurveEditor.Base.cs
index 04b4b2f72..a48f36943 100644
--- a/Source/Editor/GUI/CurveEditor.Base.cs
+++ b/Source/Editor/GUI/CurveEditor.Base.cs
@@ -100,6 +100,11 @@ namespace FlaxEditor.GUI
///
public abstract Type ValueType { get; }
+ ///
+ /// Gets the main container control that holds the curve editor UI. Can be used to add custom controls to the curve editor.
+ ///
+ public abstract Panel MainPanel { get; }
+
///
/// The amount of frames per second of the curve animation (optional). Can be used to restrict the keyframes time values to the given time quantization rate.
///
diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs
index 092cda707..948e3fb57 100644
--- a/Source/Editor/GUI/CurveEditor.cs
+++ b/Source/Editor/GUI/CurveEditor.cs
@@ -633,6 +633,9 @@ namespace FlaxEditor.GUI
///
public override Type ValueType => typeof(T);
+ ///
+ public override Panel MainPanel => _mainPanel;
+
///
public override float? FPS
{
@@ -1026,7 +1029,10 @@ namespace FlaxEditor.GUI
zoomMask &= ~UseMode.Horizontal;
if (Mathf.IsZero(posMargin.Y))
zoomMask &= ~UseMode.Vertical;
- ViewScale = ApplyUseModeMask(zoomMask, viewRect.Size / contentsSize, ViewScale);
+ var viewScale = ApplyUseModeMask(zoomMask, viewRect.Size / contentsSize, ViewScale);
+ if (_showCollapsed)
+ viewScale.Y = 1.0f;
+ ViewScale = viewScale;
// Update scroll (attempt to center the area when it's smaller than the view)
Float2 viewOffset = -posMin;
@@ -1034,6 +1040,8 @@ namespace FlaxEditor.GUI
Float2 viewSizeLeft = viewSize - Float2.Clamp(posMax - posMin, Float2.Zero, viewSize);
viewOffset += viewSizeLeft * 0.5f;
viewOffset = ApplyUseModeMask(EnablePanning, viewOffset, _mainPanel.ViewOffset);
+ if (_showCollapsed)
+ viewOffset.Y = 0;
_mainPanel.ViewOffset = viewOffset;
// Do it multiple times so the view offset can be properly calculate once the view scale gets changes
@@ -1341,6 +1349,7 @@ namespace FlaxEditor.GUI
}
base.Draw();
+
Render2D.PushClip(ref viewRect);
DrawTangentHandles();
DrawSelectedKeyframeLabels();
diff --git a/Source/Editor/GUI/Docking/DockWindow.cs b/Source/Editor/GUI/Docking/DockWindow.cs
index bf08695d8..a6c98a1f6 100644
--- a/Source/Editor/GUI/Docking/DockWindow.cs
+++ b/Source/Editor/GUI/Docking/DockWindow.cs
@@ -1,9 +1,7 @@
// Copyright (c) Wojciech Figat. All rights reserved.
-using System;
using System.Xml;
using System.Globalization;
-using System.Linq;
using FlaxEngine;
using FlaxEngine.Assertions;
using FlaxEngine.GUI;
@@ -234,46 +232,6 @@ namespace FlaxEditor.GUI.Docking
}
}
- ///
- /// Reattaches the window control to existing floating window.
- ///
- /// The window handle.
- /// Initial window state.
- /// Panel to dock to it.
- /// Only used if is set. If true the window will be selected after docking it.
- /// Only used if is set. The splitter value to use. If not specified, a default value will be used.
- public void Restore(IntPtr windowHandle, DockState state = DockState.Float, DockPanel toDock = null, bool autoSelect = true, float? splitterValue = null)
- {
- if (state != DockState.Float)
- {
- Show(state, toDock, autoSelect, splitterValue);
- return;
- }
-
- Undock();
-
- // Find the existing window and remove all controls from it
- var window = Editor.GetWindows().First(x => x.NativePtr == windowHandle);
- var windowGUI = window.GUI;
- while (windowGUI.Children.Count > 0)
- windowGUI.Children[^1].Dispose();
- windowGUI.EndTrackingMouse();
-
- // Create dock panel for the window
- var dockPanel = new FloatWindowDockPanel(_masterPanel, windowGUI);
- dockPanel.DockWindowInternal(DockState.DockFill, this);
-
- // Perform layout
- Visible = true;
- windowGUI.UnlockChildrenRecursive();
- windowGUI.PerformLayout();
-
- OnShow();
-
- // Perform layout again
- windowGUI.PerformLayout();
- }
-
///
/// Shows the window.
///
@@ -374,12 +332,6 @@ namespace FlaxEditor.GUI.Docking
}
else
{
- if (reason == ClosingReason.ScriptsReload && _dockedTo is FloatWindowDockPanel floatPanel)
- {
- // Unlink the window to keep it alive during scripts reload
- floatPanel.UnlinkWindow();
- }
-
// Undock
Undock();
diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs
index f8eb26ded..211715f5b 100644
--- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs
+++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs
@@ -270,8 +270,8 @@ namespace FlaxEditor.GUI.Docking
public override void OnDestroy()
{
_masterPanel?.FloatingPanels.Remove(this);
-
UnlinkWindow();
+
base.OnDestroy();
}
}
diff --git a/Source/Editor/GUI/EnumComboBox.cs b/Source/Editor/GUI/EnumComboBox.cs
index a463f8b7c..e13c5455d 100644
--- a/Source/Editor/GUI/EnumComboBox.cs
+++ b/Source/Editor/GUI/EnumComboBox.cs
@@ -9,6 +9,7 @@ using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Scripting;
using FlaxEngine;
+using FlaxEngine.GUI;
namespace FlaxEditor.GUI
{
@@ -58,17 +59,24 @@ namespace FlaxEditor.GUI
///
public long Value;
+ ///
+ /// Marks enum entry as deprecated and should not be used by users anymore (displayed in grey).
+ ///
+ public bool IsObsolete;
+
///
/// Initializes a new instance of the struct.
///
/// The name.
/// The tooltip.
/// The value.
- public Entry(string name, long value, string tooltip = null)
+ /// The deprecated entry flag.
+ public Entry(string name, long value, string tooltip = null, bool isObsolete = false)
{
Name = name;
Tooltip = tooltip;
Value = value;
+ IsObsolete = isObsolete;
}
}
@@ -279,10 +287,9 @@ namespace FlaxEditor.GUI
default: throw new ArgumentOutOfRangeException(nameof(formatMode), formatMode, null);
}
}
-
string tooltip = Editor.Instance.CodeDocs.GetTooltip(new ScriptMemberInfo(field), attributes);
-
- entries.Add(new Entry(name, Convert.ToInt64(field.GetRawConstantValue()), tooltip));
+ bool isObsolete = attributes.Any(x => x is ObsoleteAttribute);
+ entries.Add(new Entry(name, Convert.ToInt64(field.GetRawConstantValue()), tooltip, isObsolete));
}
}
@@ -290,8 +297,11 @@ namespace FlaxEditor.GUI
protected override void OnLayoutMenuButton(ContextMenuButton button, int index, bool construct = false)
{
base.OnLayoutMenuButton(button, index, construct);
+
if (IsFlags)
button.CloseMenuOnClick = false;
+ if (index < _entries.Count && _entries[index].IsObsolete)
+ button.TextColor = Style.Current.ForegroundGrey;
}
///
diff --git a/Source/Editor/GUI/Input/SliderControl.cs b/Source/Editor/GUI/Input/SliderControl.cs
index 4703da73d..b63358f02 100644
--- a/Source/Editor/GUI/Input/SliderControl.cs
+++ b/Source/Editor/GUI/Input/SliderControl.cs
@@ -57,8 +57,6 @@ namespace FlaxEditor.GUI.Input
if (value != _value)
{
_value = value;
-
- // Update
UpdateThumb();
ValueChanged?.Invoke();
}
@@ -122,7 +120,6 @@ namespace FlaxEditor.GUI.Input
private void UpdateThumb()
{
- // Cache data
float trackSize = TrackSize;
float range = Maximum - Minimum;
_thumbSize = Mathf.Min(trackSize, Mathf.Max(trackSize / range * 10.0f, 30.0f));
@@ -290,10 +287,7 @@ namespace FlaxEditor.GUI.Input
value = Mathf.Clamp(value, _min, _max);
if (Math.Abs(_value - value) > Mathf.Epsilon)
{
- // Set value
_value = value;
-
- // Update
_valueIsChanging = true;
UpdateText();
UpdateSlider();
@@ -315,7 +309,6 @@ namespace FlaxEditor.GUI.Input
{
if (value > _max)
throw new ArgumentException();
-
_min = value;
Value = Value;
}
@@ -334,7 +327,6 @@ namespace FlaxEditor.GUI.Input
{
if (value < _min)
throw new ArgumentException();
-
_max = value;
Value = Value;
}
@@ -434,6 +426,7 @@ namespace FlaxEditor.GUI.Input
_min = limits.Min;
_max = Mathf.Max(_min, limits.Max);
Value = Value;
+ UpdateSlider();
}
///
@@ -445,6 +438,7 @@ namespace FlaxEditor.GUI.Input
_min = limits.Min;
_max = Mathf.Max(_min, limits.Max);
Value = Value;
+ UpdateSlider();
}
///
diff --git a/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs b/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs
index 7e974b54f..4fdc5bd89 100644
--- a/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs
+++ b/Source/Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs
@@ -292,7 +292,15 @@ namespace FlaxEditor.GUI.Timeline.Tracks
Height = IsExpanded ? _expandedHeight : CollapsedHeight;
UpdateCurve();
if (IsExpanded)
+ {
+ // Layout bug: whole curve showing after track resize needs panel scrolls bars s smoothing disabled and showing twice
+ Curve.MainPanel.VScrollBar?.EnableSmoothing = false;
+ Curve.MainPanel.HScrollBar?.EnableSmoothing = false;
Curve.ShowWholeCurve();
+ Curve.ShowWholeCurve();
+ Curve.MainPanel.VScrollBar?.EnableSmoothing = true;
+ Curve.MainPanel.HScrollBar?.EnableSmoothing = true;
+ }
base.OnExpandedChanged();
}
@@ -311,7 +319,7 @@ namespace FlaxEditor.GUI.Timeline.Tracks
UpdatePreviewValue();
}
- ///
+ ///
public override void OnUndo()
{
base.OnUndo();
diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs
index 26032152a..c36c460f6 100644
--- a/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs
+++ b/Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs
@@ -50,6 +50,8 @@ namespace FlaxEditor.Modules.SourceCodeEditing
if (attributes == null)
attributes = type.GetAttributes(false);
text = type.TypeName;
+ if (type.IScriptType is Content.VisualScriptType visualScriptType && visualScriptType.Asset)
+ text = $"{System.IO.Path.GetFileNameWithoutExtension(visualScriptType.Asset.Path)}\n{text}";
var tooltip = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute);
if (tooltip != null)
text += '\n' + tooltip.Text;
diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs
index 4b0ebbf77..162614676 100644
--- a/Source/Editor/Modules/UIModule.cs
+++ b/Source/Editor/Modules/UIModule.cs
@@ -304,7 +304,7 @@ namespace FlaxEditor.Modules
//
_toolStripBuildScenes.Enabled = (canEditScene && !isPlayMode) || Editor.StateMachine.BuildingScenesState.IsActive;
_toolStripBuildScenes.Visible = Editor.Options.Options.General.BuildActions?.Length != 0;
- _toolStripCook.Enabled = Editor.Windows.GameCookerWin.CanBuild(Platform.PlatformType) && !GameCooker.IsRunning;
+ _toolStripCook.Enabled = Editor.Windows.GameCookerWin.CanBuild(Platform.PlatformType) && Editor.Windows.GameCookerWin.CanCook();
//
var play = _toolStripPlay;
var pause = _toolStripPause;
diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs
index b667e3879..b4f8089e2 100644
--- a/Source/Editor/Modules/WindowsModule.cs
+++ b/Source/Editor/Modules/WindowsModule.cs
@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
-using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
@@ -34,12 +33,11 @@ namespace FlaxEditor.Modules
private float _projectIconScreenshotTimeout = -1;
private string _windowsLayoutPath;
- private class WindowRestoreData
+ private struct WindowRestoreData
{
public string AssemblyName;
public string TypeName;
- public IntPtr WindowHandle;
public DockState DockState;
public DockPanel DockedTo;
public int DockedTabIndex;
@@ -47,10 +45,20 @@ namespace FlaxEditor.Modules
public bool SelectOnShow = false;
+ public bool Maximize;
+ public bool Minimize;
+ public Float2 FloatSize;
+ public Float2 FloatPosition;
+
public Guid AssetItemID;
+
+ // Constructor, to allow for default values
+ public WindowRestoreData()
+ {
+ }
}
- private readonly Dictionary _restoreWindows = new();
+ private readonly List _restoreWindows = new List();
///
/// The main editor window.
@@ -810,14 +818,10 @@ namespace FlaxEditor.Modules
internal void AddToRestore(AssetEditorWindow win)
{
- var assetItemId = win.Item.ID;
- if (!_restoreWindows.TryGetValue(assetItemId, out var winData))
+ AddToRestore(win, win.GetType(), new WindowRestoreData
{
- winData = new WindowRestoreData();
- _restoreWindows.Add(assetItemId, winData);
- }
- winData.AssetItemID = assetItemId;
- AddToRestore(win, win.GetType(), winData);
+ AssetItemID = win.Item.ID,
+ });
}
internal void AddToRestore(CustomEditorWindow win)
@@ -828,65 +832,62 @@ namespace FlaxEditor.Modules
if (constructor == null || type.IsGenericType)
return;
- // TODO: Restore data for custom editors
- var assetItemId = Guid.NewGuid();
- if (!_restoreWindows.TryGetValue(assetItemId, out var winData))
- {
- winData = new WindowRestoreData();
- _restoreWindows.Add(assetItemId, winData);
- }
- winData.AssetItemID = assetItemId;
- AddToRestore(win.Window, type, winData);
+ AddToRestore(win.Window, type, new WindowRestoreData());
}
private void AddToRestore(EditorWindow win, Type type, WindowRestoreData winData)
{
+ // Ensure that this window is only selected following recompilation
+ // if it was the active tab in its dock panel. Otherwise, there is a
+ // risk of interrupting the user's workflow by potentially selecting
+ // background tabs.
var window = win.RootWindow?.Window;
var panel = win.ParentDockPanel;
- winData.AssemblyName = type.Assembly.GetName().Name;
- winData.TypeName = type.FullName;
- if (panel is FloatWindowDockPanel)
+ winData.SelectOnShow = panel.SelectedTab == win;
+ winData.DockedTabIndex = 0;
+ if (panel is FloatWindowDockPanel && window != null && panel.TabsCount == 1)
{
- // Populate data for other tabs now, the tab index may change after tab is destroyed
- if (winData.DockedTo == null)
- {
- for (int i = 0; i < panel.Tabs.Count; i++)
- {
- if (panel.Tabs[i] is AssetEditorWindow assetEditorWindow)
- {
- window ??= assetEditorWindow.RootWindow?.Window; // The window handle is sometimes missing in some tabs
- var assetItemId = assetEditorWindow.Item.ID;
- if (!_restoreWindows.TryGetValue(assetItemId, out var tabWinData))
- {
- tabWinData = new WindowRestoreData();
- _restoreWindows.Add(assetItemId, tabWinData);
- }
-
- tabWinData.DockedTabIndex = i;
- tabWinData.SelectOnShow = panel.SelectedTab == assetEditorWindow;
- tabWinData.DockState = DockState.DockFill;
- tabWinData.DockedTo = panel;
- }
- }
- winData.DockState = DockState.Float;
- winData.WindowHandle = window?.NativePtr ?? IntPtr.Zero;
- }
+ winData.DockState = DockState.Float;
+ winData.FloatPosition = window.Position;
+ winData.FloatSize = window.ClientSize;
+ winData.Maximize = window.IsMaximized;
+ winData.Minimize = window.IsMinimized;
+ winData.DockedTo = panel;
}
else
{
- winData.DockState = panel.TryGetDockState(out var splitterValue);
- winData.DockedTo = panel.ParentDockPanel;
- winData.SplitterValue = splitterValue;
+ for (int i = 0; i < panel.Tabs.Count; i++)
+ {
+ if (panel.Tabs[i] == win)
+ {
+ winData.DockedTabIndex = i;
+ break;
+ }
+ }
+ if (panel.TabsCount > 1)
+ {
+ winData.DockState = DockState.DockFill;
+ winData.DockedTo = panel;
+ }
+ else
+ {
+ winData.DockState = panel.TryGetDockState(out var splitterValue);
+ winData.DockedTo = panel.ParentDockPanel;
+ winData.SplitterValue = splitterValue;
+ }
}
+ winData.AssemblyName = type.Assembly.GetName().Name;
+ winData.TypeName = type.FullName;
+ _restoreWindows.Add(winData);
}
private void OnWorkspaceRebuilt()
{
- var assetEditorWindows = new Dictionary();
-
- // Create float windows first, then add docked windows in order of the tabs
- foreach (var (_, winData) in _restoreWindows)
+ // Go in reverse order to create floating Prefab windows first before docked windows
+ for (int i = _restoreWindows.Count - 1; i >= 0; i--)
{
+ var winData = _restoreWindows[i];
+
try
{
var assembly = Utils.GetAssemblyByName(winData.AssemblyName);
@@ -903,24 +904,75 @@ namespace FlaxEditor.Modules
var assetType = assetItem.GetType();
var ctor = type.GetConstructor(new Type[] { typeof(Editor), assetType });
var win = (AssetEditorWindow)ctor.Invoke(new object[] { Editor.Instance, assetItem });
- var previouslyDockedTo = winData.DockedTo;
- win.Restore(winData.WindowHandle, winData.DockState, winData.DockState != DockState.Float ? winData.DockedTo : null, winData.SelectOnShow, winData.SplitterValue);
+
+ win.Show(winData.DockState, winData.DockState != DockState.Float ? winData.DockedTo : null, winData.SelectOnShow, winData.SplitterValue);
if (winData.DockState == DockState.Float)
{
- // Update panel reference in other windows docked to this panel
- foreach (var key in _restoreWindows.Keys)
+ var window = win.RootWindow.Window;
+ window.Position = winData.FloatPosition;
+ if (winData.Maximize)
{
- ref var otherData = ref CollectionsMarshal.GetValueRefOrNullRef(_restoreWindows, key);
- if (otherData.DockedTo == previouslyDockedTo)
+ window.Maximize();
+ }
+ else if (winData.Minimize)
+ {
+ window.Minimize();
+ }
+ else
+ {
+ window.ClientSize = winData.FloatSize;
+ }
+
+ // Update panel reference in other windows docked to this panel
+ foreach (ref var otherData in CollectionsMarshal.AsSpan(_restoreWindows))
+ {
+ if (otherData.DockedTo == winData.DockedTo)
otherData.DockedTo = win.ParentDockPanel;
}
}
- assetEditorWindows.Add(winData.AssetItemID, win);
+ var panel = win.ParentDockPanel;
+ int currentTabIndex = 0;
+ for (int pi = 0; pi < panel.TabsCount; pi++)
+ {
+ if (panel.Tabs[pi] == win)
+ {
+ currentTabIndex = pi;
+ break;
+ }
+ }
+ while (currentTabIndex > winData.DockedTabIndex)
+ {
+ win.ParentDockPanel.MoveTabLeft(currentTabIndex);
+ currentTabIndex--;
+ }
+ while (currentTabIndex < winData.DockedTabIndex)
+ {
+ win.ParentDockPanel.MoveTabRight(currentTabIndex);
+ currentTabIndex++;
+ }
+ panel.PerformLayout(true);
}
else
{
var win = (CustomEditorWindow)Activator.CreateInstance(type);
- win.Restore(winData.WindowHandle, winData.DockState, winData.DockedTo, winData.SelectOnShow, winData.SplitterValue);
+ win.Show(winData.DockState, winData.DockedTo, winData.SelectOnShow, winData.SplitterValue);
+ if (winData.DockState == DockState.Float)
+ {
+ var window = win.Window.RootWindow.Window;
+ window.Position = winData.FloatPosition;
+ if (winData.Maximize)
+ {
+ window.Maximize();
+ }
+ else if (winData.Minimize)
+ {
+ window.Minimize();
+ }
+ else
+ {
+ window.ClientSize = winData.FloatSize;
+ }
+ }
}
}
catch (Exception ex)
@@ -929,46 +981,6 @@ namespace FlaxEditor.Modules
Editor.LogWarning(string.Format("Failed to restore window {0} (assembly: {1})", winData.TypeName, winData.AssemblyName));
}
}
-
- // Reorder tabs to previous order
- foreach (var (_, winData) in _restoreWindows)
- {
- var win = assetEditorWindows.GetValueOrDefault(winData.AssetItemID);
- if (win == null)
- continue;
-
- var panel = win.ParentDockPanel;
- int currentTabIndex = 0;
- for (int pi = 0; pi < panel.TabsCount; pi++)
- {
- if (panel.Tabs[pi] == win)
- {
- currentTabIndex = pi;
- break;
- }
- }
- while (currentTabIndex > winData.DockedTabIndex)
- {
- win.ParentDockPanel.MoveTabLeft(currentTabIndex);
- currentTabIndex--;
- }
- while (currentTabIndex < winData.DockedTabIndex)
- {
- win.ParentDockPanel.MoveTabRight(currentTabIndex);
- currentTabIndex++;
- }
- }
-
- // Restore last selected tab
- foreach (var (_, winData) in _restoreWindows)
- {
- var win = assetEditorWindows.GetValueOrDefault(winData.AssetItemID);
- if (win != null && winData.SelectOnShow)
- {
- win.ParentDockPanel.SelectTab(win, false);
- win.ParentDockPanel.PerformLayout(true);
- }
- }
// Restored windows stole the focus from Editor
if (_restoreWindows.Count > 0)
diff --git a/Source/Editor/Options/OptionsModule.cs b/Source/Editor/Options/OptionsModule.cs
index 77fb116f9..e1f847cf1 100644
--- a/Source/Editor/Options/OptionsModule.cs
+++ b/Source/Editor/Options/OptionsModule.cs
@@ -302,6 +302,7 @@ namespace FlaxEditor.Options
TextBoxBackgroundSelected = Color.FromBgra(0xFF3F3F46),
CollectionBackgroundColor = Color.FromBgra(0x14CCCCCC),
ProgressNormal = Color.FromBgra(0xFF0ad328),
+ ProgressError = new Color(0.79f, 0.146f, 0.0f, 1.0f),
Selection = Color.Orange * 0.4f,
SelectionBorder = Color.Orange,
@@ -365,6 +366,7 @@ namespace FlaxEditor.Options
TextBoxBackgroundSelected = new Color(0.73f, 0.73f, 0.80f, 1f),
CollectionBackgroundColor = new Color(0.85f, 0.85f, 0.88f, 1f),
ProgressNormal = new Color(0.03f, 0.65f, 0.12f, 1f),
+ ProgressError = new Color(0.79f, 0.146f, 0.0f, 1.0f),
Selection = Color.Orange * 0.4f,
SelectionBorder = Color.Orange,
diff --git a/Source/Editor/Scripting/ScriptType.cs b/Source/Editor/Scripting/ScriptType.cs
index ebab808d1..a5daee439 100644
--- a/Source/Editor/Scripting/ScriptType.cs
+++ b/Source/Editor/Scripting/ScriptType.cs
@@ -914,7 +914,7 @@ namespace FlaxEditor.Scripting
get
{
if (_managed != null)
- return _managed.IsValueType || _managed.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null) != null;
+ return (_managed.IsValueType || _managed.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null) != null) && !_managed.IsByRefLike;
return _custom?.CanCreateInstance ?? false;
}
}
diff --git a/Source/Editor/States/EditingSceneState.cs b/Source/Editor/States/EditingSceneState.cs
index 72ba37d02..b2980312a 100644
--- a/Source/Editor/States/EditingSceneState.cs
+++ b/Source/Editor/States/EditingSceneState.cs
@@ -33,6 +33,9 @@ namespace FlaxEditor.States
///
public override bool CanReloadScripts => true;
+ ///
+ public override bool CanCookGame => true;
+
///
public override string Status => AutoSaveStatus;
diff --git a/Source/Editor/States/EditorState.cs b/Source/Editor/States/EditorState.cs
index 8579492a2..15000d341 100644
--- a/Source/Editor/States/EditorState.cs
+++ b/Source/Editor/States/EditorState.cs
@@ -48,15 +48,20 @@ namespace FlaxEditor.States
public virtual bool CanChangeScene => false;
///
- /// Checks if can enter play mode in this state.
+ /// Checks if can start play mode in this state.
///
public virtual bool CanEnterPlayMode => false;
///
- /// Checks if can enter recompile scripts in this state.
+ /// Checks if can recompile scripts in this state.
///
public virtual bool CanReloadScripts => false;
+ ///
+ /// Checks if can cook game in this state.
+ ///
+ public virtual bool CanCookGame => false;
+
///
/// Checks if static is valid for Editor UI calls and other stuff.
///
diff --git a/Source/Editor/States/PlayingState.cs b/Source/Editor/States/PlayingState.cs
index a81487e43..87ff72c59 100644
--- a/Source/Editor/States/PlayingState.cs
+++ b/Source/Editor/States/PlayingState.cs
@@ -37,6 +37,9 @@ namespace FlaxEditor.States
///
public override bool CanEnterPlayMode => true;
+ ///
+ public override bool CanCookGame => true;
+
///
/// Occurs when play mode is starting (before scene duplicating).
///
diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs
index f804b5623..6adba88f1 100644
--- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs
+++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs
@@ -30,6 +30,7 @@ namespace FlaxEditor.Surface.Archetypes
protected const float DecoratorsMarginY = 2.0f;
protected bool _debugRelevant;
+ protected int _debugDecoratorResult = -1;
protected string _debugInfo;
protected Float2 _debugInfoSize;
protected ScriptType _type;
@@ -90,6 +91,7 @@ namespace FlaxEditor.Surface.Archetypes
protected virtual void UpdateDebugInfo(BehaviorTreeNode instance = null, Behavior behavior = null)
{
_debugRelevant = false;
+ _debugDecoratorResult = -1;
_debugInfo = null;
_debugInfoSize = Float2.Zero;
if (!instance)
@@ -98,6 +100,8 @@ namespace FlaxEditor.Surface.Archetypes
{
// Get debug description for the node based on the current settings
_debugRelevant = Behavior.GetNodeDebugRelevancy(instance, behavior);
+ if (instance is BehaviorTreeDecorator decorator && behavior && behavior.IsDuringPlay && behavior.Result == BehaviorUpdateResult.Running)
+ _debugDecoratorResult = Behavior.GetDecoratorDebugResult(decorator, behavior);
_debugInfo = Behavior.GetNodeDebugInfo(instance, behavior);
if (!string.IsNullOrEmpty(_debugInfo))
_debugInfoSize = Style.Current.FontSmall.MeasureText(_debugInfo);
@@ -215,7 +219,7 @@ namespace FlaxEditor.Surface.Archetypes
var headerColor = style.BackgroundHighlighted;
if (_headerRect.Contains(ref _mousePosition) && !Surface.IsConnecting && !Surface.IsSelecting)
headerColor *= 1.07f;
- Render2D.FillRectangle(_headerRect, style.BackgroundHighlighted);
+ Render2D.FillRectangle(_headerRect, headerColor);
Render2D.DrawText(style.FontLarge, Title, _headerTextRect, style.Foreground, TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1f, FlaxEditor.Surface.Constants.NodeHeaderTextScale);
// Close button
@@ -227,14 +231,6 @@ namespace FlaxEditor.Surface.Archetypes
DrawChildren();
- // Selection outline
- if (_isSelected)
- {
- var colorTop = Color.Orange;
- var colorBottom = Color.OrangeRed;
- Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom, 2.5f);
- }
-
// Breakpoint dot
if (Breakpoint.Set)
{
@@ -242,16 +238,21 @@ namespace FlaxEditor.Surface.Archetypes
Render2D.DrawSprite(icon, new Rectangle(-7, -7, 16, 16), new Color(0.9f, 0.9f, 0.9f));
Render2D.DrawSprite(icon, new Rectangle(-6, -6, 14, 14), new Color(0.894117647f, 0.0784313725f, 0.0f));
}
+ }
+ protected virtual void DrawOverlays()
+ {
+ var style = Style.Current;
if (highlightBox != null)
Render2D.DrawRectangle(highlightBox.Bounds, style.BorderHighlighted, 2f);
- // Debug Info
- if (!string.IsNullOrEmpty(_debugInfo))
+ // Selection outline
+ if (_isSelected)
{
- // Draw an extra background to cover the archetype color colored node background and make text more legible
- Render2D.FillRectangle(new Rectangle(0, _headerRect.Bottom + 4, Width, Height - _headerRect.Bottom - 4), style.BackgroundHighlighted);
- Render2D.DrawText(style.FontSmall, _debugInfo, new Rectangle(4, _headerRect.Bottom + 7, _debugInfoSize), style.Foreground, scale: 0.8f);
+ var colorTop = Color.Orange;
+ var colorBottom = Color.OrangeRed;
+ var rect = new Rectangle(Float2.Zero, Size);
+ Render2D.DrawRectangle(rect, colorTop, colorTop, colorBottom, colorBottom, 2.5f);
}
// Debug relevancy outline
@@ -259,8 +260,9 @@ namespace FlaxEditor.Surface.Archetypes
{
var colorTop = Color.LightYellow;
var colorBottom = Color.Yellow;
- backgroundRect = new Rectangle(Float2.One, Size - new Float2(2.0f));
- Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom);
+ var rect = new Rectangle(Float2.One, Size - new Float2(2.0f));
+ //Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom);
+ Render2D.DrawRectangle(rect, style.ProgressNormal);
}
}
@@ -546,14 +548,30 @@ namespace FlaxEditor.Surface.Archetypes
}
}
+ public override void Draw()
+ {
+ base.Draw();
+
+ var style = Style.Current;
+
+ // Debug Info
+ if (!string.IsNullOrEmpty(_debugInfo))
+ {
+ // Draw an extra background to cover the archetype color colored node background and make text more legible
+ Render2D.FillRectangle(new Rectangle(0, _headerRect.Bottom + 4, Width, Height - _headerRect.Bottom - 4), style.BackgroundHighlighted);
+ Render2D.DrawText(style.FontSmall, _debugInfo, new Rectangle(4, _headerRect.Bottom + 7, _debugInfoSize), style.Foreground, scale: 0.8f);
+ }
+
+ DrawOverlays();
+ }
+
public override void ResizeAuto()
{
if (Surface == null)
return;
- var width = 0.0f;
+ var width = 90.0f;
var height = 0.0f;
var titleLabelFont = Style.Current.FontLarge;
- width = Mathf.Max(width, 100.0f);
width = Mathf.Max(width, titleLabelFont.MeasureText(Title).X + 30);
if (_debugInfoSize.X > 0)
{
@@ -569,7 +587,7 @@ namespace FlaxEditor.Surface.Archetypes
{
decorator.ResizeAuto();
height += decorator.Height + DecoratorsMarginY;
- width = Mathf.Max(width, decorator.Width - FlaxEditor.Surface.Constants.NodeCloseButtonSize - 2 * DecoratorsMarginX);
+ width = Mathf.Max(width, decorator.Width - FlaxEditor.Surface.Constants.NodeCloseButtonSize);
}
Size = new Float2(width + FlaxEditor.Surface.Constants.NodeMarginX * 2 + FlaxEditor.Surface.Constants.NodeCloseButtonSize, height + FlaxEditor.Surface.Constants.NodeHeaderHeight);
UpdateRectangles();
@@ -732,9 +750,9 @@ namespace FlaxEditor.Surface.Archetypes
if (_debugInfoSize.X > 0)
{
width = Mathf.Max(width, _debugInfoSize.X + 8.0f);
- height += _debugInfoSize.Y + 8.0f;
+ height += _debugInfoSize.Y + 1.0f;
}
- return new Float2(width + FlaxEditor.Surface.Constants.NodeCloseButtonSize * 2 + DecoratorsMarginX * 2, height + FlaxEditor.Surface.Constants.NodeHeaderHeight);
+ return new Float2(width + FlaxEditor.Surface.Constants.NodeCloseButtonSize * 2, height + FlaxEditor.Surface.Constants.NodeHeaderHeight);
}
protected override void UpdateRectangles()
@@ -799,6 +817,19 @@ namespace FlaxEditor.Surface.Archetypes
var style = Style.Current;
+ // Debug Info
+ if (!string.IsNullOrEmpty(_debugInfo))
+ {
+ var color = style.Foreground;
+ if (_debugDecoratorResult == 0)
+ color = style.ProgressError;
+ else if (_debugDecoratorResult == 1)
+ color = style.ProgressNormal;
+ Render2D.DrawText(style.FontSmall, _debugInfo, new Rectangle(4, _headerRect.Bottom, _debugInfoSize), color, TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1, 0.8f);
+ }
+
+ DrawOverlays();
+
// Outline
if (!_isSelected)
{
diff --git a/Source/Editor/Surface/AttributesEditor.cs b/Source/Editor/Surface/AttributesEditor.cs
index 68504b736..43529d666 100644
--- a/Source/Editor/Surface/AttributesEditor.cs
+++ b/Source/Editor/Surface/AttributesEditor.cs
@@ -124,19 +124,18 @@ namespace FlaxEditor.Surface
};
editor.Select(_proxy);
- _presenter.Modified += OnPresenterModified;
- OnPresenterModified();
+ _presenter.AfterLayout += OnPresenterLayer;
}
- private void OnPresenterModified()
+ private void OnPresenterLayer(LayoutElementsContainer layout)
{
if (_proxy.Value.Length == 0)
{
- var label = _presenter.Label("No attributes.\nPress the \"+\" button to add a new one and then select an attribute type using the \"Type\" dropdown.", TextAlignment.Center);
- label.Label.Wrapping = TextWrapping.WrapWords;
- label.Control.Height = 35f;
- label.Label.Margin = new Margin(10f);
- label.Label.TextColor = label.Label.TextColorHighlighted = Style.Current.ForegroundGrey;
+ var label = layout.Label("No attributes.\nPress the \"+\" button to add a new one and then select an attribute type using the \"Type\" dropdown.", TextAlignment.Center).Label;
+ label.Wrapping = TextWrapping.WrapWords;
+ label.Height = 45f;
+ label.Margin = new Margin(10f);
+ label.TextColor = label.TextColorHighlighted = Style.Current.ForegroundGrey;
}
}
@@ -186,11 +185,14 @@ namespace FlaxEditor.Surface
///
public override void OnDestroy()
{
- _presenter = null;
+ if (_presenter != null)
+ {
+ _presenter.AfterLayout -= OnPresenterLayer;
+ _presenter = null;
+ }
_oldData = null;
_proxy = null;
Edited = null;
- _presenter.Modified -= OnPresenterModified;
base.OnDestroy();
}
diff --git a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs
index 83e8917fb..7a06c18f1 100644
--- a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs
+++ b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs
@@ -226,6 +226,9 @@ namespace FlaxEditor.Viewport.Previews
};
_nodeNameSizeButton.Enabled = ShowNodesNames;
nodeNameSizeValue.ValueChanged += () => NodeNamesSize = nodeNameSizeValue.Value;
+
+ // Hide not needed buttons
+ ViewWidgetButtonMenu.GetChildMenu("View Layers").Visible = false;
}
// Enable shadows
diff --git a/Source/Editor/Viewport/Previews/ModelPreview.cs b/Source/Editor/Viewport/Previews/ModelPreview.cs
index 026cd94ed..de889412d 100644
--- a/Source/Editor/Viewport/Previews/ModelPreview.cs
+++ b/Source/Editor/Viewport/Previews/ModelPreview.cs
@@ -251,6 +251,9 @@ namespace FlaxEditor.Viewport.Previews
Parent = previewLODsMode,
};
previewLODsMode.Parent = this;
+
+ // Hide not needed buttons
+ ViewWidgetButtonMenu.GetChildMenu("View Layers").Visible = false;
}
}
diff --git a/Source/Editor/Viewport/ViewportDraggingHelper.cs b/Source/Editor/Viewport/ViewportDraggingHelper.cs
index 846eeb001..4f0ff383c 100644
--- a/Source/Editor/Viewport/ViewportDraggingHelper.cs
+++ b/Source/Editor/Viewport/ViewportDraggingHelper.cs
@@ -186,7 +186,7 @@ namespace FlaxEditor.Viewport
private Vector3 PostProcessSpawnedActorLocation(Actor actor, ref Vector3 hitLocation)
{
- // Refresh actor position to ensure that cached bounds are valid
+ // Refresh actor position to ensure that cached bounds are valid (in Actor::OnTransformChanged)
actor.Position = Vector3.One;
actor.Position = Vector3.Zero;
diff --git a/Source/Editor/Windows/Assets/JsonAssetWindow.cs b/Source/Editor/Windows/Assets/JsonAssetWindow.cs
index 233bca18a..041c4badf 100644
--- a/Source/Editor/Windows/Assets/JsonAssetWindow.cs
+++ b/Source/Editor/Windows/Assets/JsonAssetWindow.cs
@@ -148,6 +148,7 @@ namespace FlaxEditor.Windows.Assets
protected override void OnScriptsReloadBegin()
{
base.OnScriptsReloadBegin();
+
Close(ClosingReason.ScriptsReload);
}
diff --git a/Source/Editor/Windows/GameCookerWindow.cs b/Source/Editor/Windows/GameCookerWindow.cs
index 92575e9e5..4d99fdb75 100644
--- a/Source/Editor/Windows/GameCookerWindow.cs
+++ b/Source/Editor/Windows/GameCookerWindow.cs
@@ -915,6 +915,15 @@ namespace FlaxEditor.Windows
_exitOnBuildEnd = true;
}
+ ///
+ /// Returns true if can cook game now (based on editor state).
+ ///
+ /// True if can cook, otherwise false.
+ public bool CanCook()
+ {
+ return !GameCooker.IsRunning && Editor.StateMachine.CurrentState.CanCookGame;
+ }
+
///
/// Returns true if can build for the given platform (both supported and available).
///
@@ -1278,7 +1287,7 @@ namespace FlaxEditor.Windows
public override void OnUpdate()
{
// Building queue
- if (!GameCooker.IsRunning)
+ if (CanCook())
{
if (_buildingQueue.Count > 0)
{
diff --git a/Source/Engine/AI/Behavior.cpp b/Source/Engine/AI/Behavior.cpp
index 55d96e415..4b316110c 100644
--- a/Source/Engine/AI/Behavior.cpp
+++ b/Source/Engine/AI/Behavior.cpp
@@ -107,16 +107,25 @@ void Behavior::UpdateAsync()
context.DeltaTime = updateDeltaTime;
context.Time = _totalTime;
const BehaviorUpdateResult result = tree->Graph.Root->InvokeUpdate(context);
- if (result != BehaviorUpdateResult::Running)
+ if (_result == BehaviorUpdateResult::Running && result != BehaviorUpdateResult::Running)
+ {
+ // Update result
_result = result;
- if (_result != BehaviorUpdateResult::Running && tree->Graph.Root->Loop)
- {
- // Reset State
- _result = BehaviorUpdateResult::Running;
+ if (_result != BehaviorUpdateResult::Running && tree->Graph.Root->Loop)
+ {
+ // Reset State
+ _result = BehaviorUpdateResult::Running;
+ }
+ else if (_result != BehaviorUpdateResult::Running)
+ {
+ // End
+ Finished();
+ }
}
- else if (_result != BehaviorUpdateResult::Running)
+ else if (_result != BehaviorUpdateResult::Running && tree->Graph.Root->Loop)
{
- Finished();
+ // Restart on end (eg. after Force Success node during Update)
+ StartLogic();
}
}
@@ -209,7 +218,25 @@ String Behavior::GetNodeDebugInfo(const BehaviorTreeNode* node, Behavior* behavi
if (!node)
return String::Empty;
BehaviorUpdateContext context;
+ InitNodeDebugContext(node, behavior, context);
+ return node->GetDebugInfo(context);
+}
+
+int32 Behavior::GetDecoratorDebugResult(BehaviorTreeDecorator* node, Behavior* behavior)
+{
+ if (!node)
+ return -1;
+ BehaviorUpdateContext context;
+ InitNodeDebugContext(node, behavior, context);
+ return node->CanUpdate(context) ? 1 : 0;
+}
+
+void Behavior::InitNodeDebugContext(const BehaviorTreeNode* node, Behavior* behavior, BehaviorUpdateContext& context)
+{
+ // Zero out context
Platform::MemoryClear(&context, sizeof(context));
+
+ // Check if the node is relevant (active in tree with state created)
if (GetNodeDebugRelevancy(node, behavior))
{
// Pass behavior and knowledge data only for relevant nodes to properly access it
@@ -219,7 +246,6 @@ String Behavior::GetNodeDebugInfo(const BehaviorTreeNode* node, Behavior* behavi
context.RelevantNodes = &behavior->_knowledge.RelevantNodes;
context.Time = behavior->_totalTime;
}
- return node->GetDebugInfo(context);
}
#endif
diff --git a/Source/Engine/AI/Behavior.h b/Source/Engine/AI/Behavior.h
index cdad7331b..c99abe192 100644
--- a/Source/Engine/AI/Behavior.h
+++ b/Source/Engine/AI/Behavior.h
@@ -110,5 +110,7 @@ private:
// Editor-only utilities to debug nodes state.
API_FUNCTION(Internal) static bool GetNodeDebugRelevancy(const BehaviorTreeNode* node, const Behavior* behavior);
API_FUNCTION(Internal) static String GetNodeDebugInfo(const BehaviorTreeNode* node, Behavior* behavior);
+ API_FUNCTION(Internal) static int32 GetDecoratorDebugResult(class BehaviorTreeDecorator* node, Behavior* behavior);
+ static void InitNodeDebugContext(const BehaviorTreeNode* node, Behavior* behavior, BehaviorUpdateContext& context);
#endif
};
diff --git a/Source/Engine/AI/BehaviorKnowledgeSelector.h b/Source/Engine/AI/BehaviorKnowledgeSelector.h
index 635244d07..9cb0578a8 100644
--- a/Source/Engine/AI/BehaviorKnowledgeSelector.h
+++ b/Source/Engine/AI/BehaviorKnowledgeSelector.h
@@ -34,6 +34,11 @@ API_STRUCT(NoDefault, MarshalAs=StringAnsi) struct FLAXENGINE_API BehaviorKnowle
return Path == other.Path;
}
+ FORCE_INLINE operator bool() const
+ {
+ return Path.HasChars();
+ }
+
BehaviorKnowledgeSelectorAny& operator=(const StringAnsiView& other) noexcept
{
Path = other;
@@ -46,12 +51,12 @@ API_STRUCT(NoDefault, MarshalAs=StringAnsi) struct FLAXENGINE_API BehaviorKnowle
return *this;
}
- operator StringAnsi() const
+ FORCE_INLINE operator StringAnsi() const
{
return Path;
}
- String ToString() const
+ FORCE_INLINE String ToString() const
{
return Path.ToString();
}
diff --git a/Source/Engine/AI/BehaviorTreeNode.h b/Source/Engine/AI/BehaviorTreeNode.h
index 857b134f9..e1d2b40e7 100644
--- a/Source/Engine/AI/BehaviorTreeNode.h
+++ b/Source/Engine/AI/BehaviorTreeNode.h
@@ -31,7 +31,7 @@ private:
public:
///
- /// Node user name (eg. Follow Enemy, or Pick up Weapon).
+ /// Custom node name (eg. Follow Enemy, or Pick up Weapon).
///
API_FIELD() String Name;
diff --git a/Source/Engine/AI/BehaviorTreeNodes.cpp b/Source/Engine/AI/BehaviorTreeNodes.cpp
index 21cfe4c95..f69b10d19 100644
--- a/Source/Engine/AI/BehaviorTreeNodes.cpp
+++ b/Source/Engine/AI/BehaviorTreeNodes.cpp
@@ -48,6 +48,31 @@ bool IsAssignableFrom(const StringAnsiView& to, const StringAnsiView& from)
return false;
}
+#if USE_EDITOR
+
+const Char* ComparisonChars(BehaviorValueComparison comp)
+{
+ switch (comp)
+ {
+ case BehaviorValueComparison::Equal:
+ return TEXT("==");
+ case BehaviorValueComparison::NotEqual:
+ return TEXT("!=");
+ case BehaviorValueComparison::Less:
+ return TEXT("<");
+ case BehaviorValueComparison::LessEqual:
+ return TEXT("<=");
+ case BehaviorValueComparison::Greater:
+ return TEXT(">");
+ case BehaviorValueComparison::GreaterEqual:
+ return TEXT(">=");
+ default:
+ return TEXT("?");
+ }
+}
+
+#endif
+
BehaviorUpdateResult BehaviorTreeNode::InvokeUpdate(const BehaviorUpdateContext& context)
{
ASSERT_LOW_LAYER(_executionIndex != -1);
@@ -87,6 +112,8 @@ BehaviorUpdateResult BehaviorTreeNode::InvokeUpdate(const BehaviorUpdateContext&
result = Update(context);
if ((int32)result < 0 || (int32)result > (int32)BehaviorUpdateResult::Failed)
result = BehaviorUpdateResult::Failed; // Invalid value is a failure
+ if (context.Behavior->GetResult() != BehaviorUpdateResult::Running)
+ return result; // Behavior is already finished, no need to continue
// Post-process result from decorators
for (BehaviorTreeDecorator* decorator : _decorators)
@@ -334,7 +361,7 @@ BehaviorUpdateResult BehaviorTreeSubTreeNode::Update(const BehaviorUpdateContext
{
// Validate if nested tree blackboard data matches (the same type or base type)
const VariantType& blackboardType = context.Knowledge->Blackboard.Type;
- if (IsAssignableFrom(treeBlackboardType, StringAnsiView(blackboardType.GetTypeName())))
+ if (!IsAssignableFrom(treeBlackboardType, StringAnsiView(blackboardType.GetTypeName())))
{
LOG(Error, "Cannot use nested '{}' with Blackboard of type '{}' inside '{}' with Blackboard of type '{}'",
tree->ToString(), String(treeBlackboardType),
@@ -353,6 +380,19 @@ BehaviorUpdateResult BehaviorTreeSubTreeNode::Update(const BehaviorUpdateContext
return tree->Graph.Root->InvokeUpdate(subContext);
}
+#if USE_EDITOR
+
+String BehaviorTreeSubTreeNode::GetDebugInfo(const BehaviorUpdateContext& context) const
+{
+ if (Tree)
+ {
+ return StringUtils::GetFileNameWithoutExtension(Tree->GetPath());
+ }
+ return String::Empty;
+}
+
+#endif
+
BehaviorUpdateResult BehaviorTreeForceFinishNode::Update(const BehaviorUpdateContext& context)
{
context.Behavior->StopLogic(Result);
@@ -374,7 +414,7 @@ void BehaviorTreeMoveToNode::GetAgentSize(Actor* agent, float& outRadius, float&
{
if (const auto* characterController = Cast(agent))
{
- // Character Controller is an capsule
+ // Character Controller is a capsule
outRadius = characterController->GetRadius();
outHeight = characterController->GetHeight() + 2 * outRadius;
return;
@@ -525,7 +565,7 @@ String BehaviorTreeMoveToNode::GetDebugInfo(const BehaviorUpdateContext& context
Real distanceLeft = state->Path.Count() > state->TargetPathIndex ? Vector3::Distance(state->Path[state->TargetPathIndex], agentLocationOnPath) : 0;
for (int32 i = state->TargetPathIndex; i < state->Path.Count(); i++)
distanceLeft += Vector3::Distance(state->Path[i - 1], state->Path[i]);
- return String::Format(TEXT("Agent: '{}'\nGoal: '{}'\nDistance: {}"), agent, goal, (int32)distanceLeft);
+ return String::Format(TEXT("Agent: '{}'\nGoal: '{}'\nDistance: {}"), agent, goal, distanceLeft);
}
}
return String::Empty;
@@ -699,11 +739,33 @@ bool BehaviorTreeKnowledgeConditionalDecorator::CanUpdate(const BehaviorUpdateCo
return BehaviorKnowledge::CompareValues((float)ValueA.Get(context.Knowledge), ValueB, Comparison);
}
+#if USE_EDITOR
+
+String BehaviorTreeKnowledgeConditionalDecorator::GetDebugInfo(const BehaviorUpdateContext& context) const
+{
+ if (ValueA)
+ return String::Format(TEXT("'{}' {} {}"), ValueA.ToString(), ComparisonChars(Comparison), ValueB);
+ return String::Empty;
+}
+
+#endif
+
bool BehaviorTreeKnowledgeValuesConditionalDecorator::CanUpdate(const BehaviorUpdateContext& context)
{
return BehaviorKnowledge::CompareValues((float)ValueA.Get(context.Knowledge), (float)ValueB.Get(context.Knowledge), Comparison);
}
+#if USE_EDITOR
+
+String BehaviorTreeKnowledgeValuesConditionalDecorator::GetDebugInfo(const BehaviorUpdateContext& context) const
+{
+ if (ValueA && ValueB)
+ return String::Format(TEXT("'{}' {} '{}'"), ValueA.ToString(), ComparisonChars(Comparison), ValueB.ToString());
+ return String::Empty;
+}
+
+#endif
+
bool BehaviorTreeKnowledgeBooleanDecorator::CanUpdate(const BehaviorUpdateContext& context)
{
Variant value = Value.Get(context.Knowledge);
@@ -712,6 +774,22 @@ bool BehaviorTreeKnowledgeBooleanDecorator::CanUpdate(const BehaviorUpdateContex
return result;
}
+#if USE_EDITOR
+
+String BehaviorTreeKnowledgeBooleanDecorator::GetDebugInfo(const BehaviorUpdateContext& context) const
+{
+ String result;
+ if (Value)
+ {
+ result = Value.ToString();
+ if (Invert)
+ result = TEXT("Not ") + result;
+ }
+ return result;
+}
+
+#endif
+
bool BehaviorTreeHasTagDecorator::CanUpdate(const BehaviorUpdateContext& context)
{
bool result = false;
@@ -722,8 +800,30 @@ bool BehaviorTreeHasTagDecorator::CanUpdate(const BehaviorUpdateContext& context
return result;
}
+#if USE_EDITOR
+
+String BehaviorTreeHasTagDecorator::GetDebugInfo(const BehaviorUpdateContext& context) const
+{
+ if (Actor && Tag)
+ return String::Format(TEXT("'{}' Has Tag '{}'"), Actor.ToString(), Tag.ToString());
+ return String::Empty;
+}
+
+#endif
+
bool BehaviorTreeHasGoalDecorator::CanUpdate(const BehaviorUpdateContext& context)
{
Variant value; // TODO: use HasGoal in Knowledge to optimize this (goal struct is copied by selector accessor)
return Goal.TryGet(context.Knowledge, value);
}
+
+#if USE_EDITOR
+
+String BehaviorTreeHasGoalDecorator::GetDebugInfo(const BehaviorUpdateContext& context) const
+{
+ if (Goal)
+ return String::Format(TEXT("Has Goal '{}'"), Goal.ToString());
+ return String::Empty;
+}
+
+#endif
diff --git a/Source/Engine/AI/BehaviorTreeNodes.h b/Source/Engine/AI/BehaviorTreeNodes.h
index 05a472da7..bcc2bce91 100644
--- a/Source/Engine/AI/BehaviorTreeNodes.h
+++ b/Source/Engine/AI/BehaviorTreeNodes.h
@@ -156,6 +156,9 @@ public:
void InitState(const BehaviorUpdateContext& context) override;
void ReleaseState(const BehaviorUpdateContext& context) override;
BehaviorUpdateResult Update(const BehaviorUpdateContext& context) override;
+#if USE_EDITOR
+ String GetDebugInfo(const BehaviorUpdateContext& context) const override;
+#endif
private:
struct State
@@ -422,6 +425,9 @@ API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeKnowledgeConditionalDecorator
public:
// [BehaviorTreeNode]
bool CanUpdate(const BehaviorUpdateContext& context) override;
+#if USE_EDITOR
+ String GetDebugInfo(const BehaviorUpdateContext& context) const override;
+#endif
};
///
@@ -447,6 +453,9 @@ API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeKnowledgeValuesConditionalDec
public:
// [BehaviorTreeNode]
bool CanUpdate(const BehaviorUpdateContext& context) override;
+#if USE_EDITOR
+ String GetDebugInfo(const BehaviorUpdateContext& context) const override;
+#endif
};
///
@@ -468,6 +477,9 @@ API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeKnowledgeBooleanDecorator : p
public:
// [BehaviorTreeNode]
bool CanUpdate(const BehaviorUpdateContext& context) override;
+#if USE_EDITOR
+ String GetDebugInfo(const BehaviorUpdateContext& context) const override;
+#endif
};
///
@@ -493,6 +505,9 @@ API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeHasTagDecorator : public Beha
public:
// [BehaviorTreeNode]
bool CanUpdate(const BehaviorUpdateContext& context) override;
+#if USE_EDITOR
+ String GetDebugInfo(const BehaviorUpdateContext& context) const override;
+#endif
};
///
@@ -510,4 +525,7 @@ API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeHasGoalDecorator : public Beh
public:
// [BehaviorTreeNode]
bool CanUpdate(const BehaviorUpdateContext& context) override;
+#if USE_EDITOR
+ String GetDebugInfo(const BehaviorUpdateContext& context) const override;
+#endif
};
diff --git a/Source/Engine/Animations/AnimationData.h b/Source/Engine/Animations/AnimationData.h
index 8df86d766..096c544e3 100644
--- a/Source/Engine/Animations/AnimationData.h
+++ b/Source/Engine/Animations/AnimationData.h
@@ -96,6 +96,8 @@ API_ENUM(Attributes="Flags") enum class AnimationRootMotionFlags : byte
RootPosition = RootPositionXZ | RootPositionY,
// Root node position and rotation.
RootTransform = RootPosition | RootRotation,
+ // Marks that the root node position is affected by the skeleton transform and should be extracted with transformed mask. Use this option for models that don't use Y-up coordinate system.
+ LocalPositionMask = 1 << 3,
};
DECLARE_ENUM_OPERATORS(AnimationRootMotionFlags);
diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h
index e9cf466ec..a31fed6c8 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.h
+++ b/Source/Engine/Animations/Graph/AnimGraph.h
@@ -186,7 +186,9 @@ struct FLAXENGINE_API AnimGraphSlot
float BlendOutTime = 0.0f;
int32 LoopCount = 0;
bool Pause = false;
- bool Reset = false;
+ bool Stop = false;
+ bool Rewind = false;
+ bool ActiveBlend = false;
};
///
diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
index afebe60aa..272c61b7e 100644
--- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
+++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
@@ -51,7 +51,7 @@ struct MultiBlendAnimData
}
}
- if (speed < 0.0f && bucket.LastUpdateFrame < context.CurrentFrameIndex - 1)
+ if (speed * context.DeltaTime < 0.0f && bucket.LastUpdateFrame < context.CurrentFrameIndex - 1)
{
// If speed is negative and it's the first node update then start playing from end
sample.PrevTimePos = sample.Length;
@@ -236,6 +236,7 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
const float eventTime = (float)(animPos / anim->Data.FramesPerSecond);
const float eventDeltaTime = (float)((animPos - animPrevPos) / anim->Data.FramesPerSecond);
+ const int32 animDuration = Math::CeilToInt(anim->GetDuration());
for (const auto& track : anim->Events)
{
for (const auto& k : track.Second.GetKeyframes())
@@ -245,18 +246,18 @@ void AnimGraphExecutor::ProcessAnimEvents(AnimGraphNode* node, bool loop, float
const float duration = k.Value.Duration > 1 ? k.Value.Duration : 0.0f;
#define ADD_OUTGOING_EVENT(type) context.Data->OutgoingEvents.Add({ k.Value.Instance, (AnimatedModel*)context.Data->Object, anim, eventTime, eventDeltaTime, AnimGraphInstanceData::OutgoingEvent::type })
if ((k.Time <= eventTimeMax && eventTimeMin <= k.Time + duration
- && (Math::FloorToInt(animPos) != 0 && Math::CeilToInt(animPrevPos) != Math::CeilToInt(anim->GetDuration())
- && Math::FloorToInt(animPrevPos) != 0 && Math::CeilToInt(animPos) != Math::CeilToInt(anim->GetDuration())))
+ && (Math::FloorToInt(animPos) != 0 && Math::CeilToInt(animPrevPos) != animDuration
+ && Math::FloorToInt(animPrevPos) != 0 && Math::CeilToInt(animPos) != animDuration))
// Handle the edge case of an event on 0 or on max animation duration during looping
- || (!loop && duration == 0.0f && Math::CeilToInt(animPos) == Math::CeilToInt(anim->GetDuration()) && Math::CeilToInt(animPrevPos) == Math::CeilToInt(anim->GetDuration()) - 1 && Math::NearEqual(k.Time, anim->GetDuration()))
- || (loop && Math::FloorToInt(animPos) == 0 && Math::CeilToInt(animPrevPos) == Math::CeilToInt(anim->GetDuration()) && k.Time == 0.0f)
- || (loop && Math::FloorToInt(animPrevPos) == 0 && Math::CeilToInt(animPos) == Math::CeilToInt(anim->GetDuration()) && k.Time == 0.0f)
- || (loop && Math::FloorToInt(animPos) == 0 && Math::CeilToInt(animPrevPos) == Math::CeilToInt(anim->GetDuration()) && Math::NearEqual(k.Time, anim->GetDuration()))
- || (loop && Math::FloorToInt(animPrevPos) == 0 && Math::CeilToInt(animPos) == Math::CeilToInt(anim->GetDuration()) && Math::NearEqual(k.Time, anim->GetDuration()))
+ || (!loop && duration == 0.0f && Math::CeilToInt(animPos) == animDuration && Math::CeilToInt(animPrevPos) == animDuration - 1 && Math::NearEqual(k.Time, anim->GetDuration()))
+ || (loop && Math::FloorToInt(animPos) == 0 && Math::CeilToInt(animPrevPos) == animDuration && k.Time == 0.0f)
+ || (loop && Math::FloorToInt(animPrevPos) == 0 && Math::CeilToInt(animPos) == animDuration && k.Time == 0.0f)
+ || (loop && Math::FloorToInt(animPos) == 0 && Math::CeilToInt(animPrevPos) == animDuration && Math::NearEqual(k.Time, anim->GetDuration()))
+ || (loop && Math::FloorToInt(animPrevPos) == 0 && Math::CeilToInt(animPos) == animDuration && Math::NearEqual(k.Time, anim->GetDuration()))
|| (Math::FloorToInt(animPos) == 1 && Math::FloorToInt(animPrevPos) == 0 && k.Time == 1.0f)
|| (Math::FloorToInt(animPos) == 0 && Math::FloorToInt(animPrevPos) == 1 && k.Time == 1.0f)
- || (Math::CeilToInt(animPos) == Math::CeilToInt(anim->GetDuration()) && Math::CeilToInt(animPrevPos) == Math::CeilToInt(anim->GetDuration()) - 1 && Math::NearEqual(k.Time, anim->GetDuration() - 1.0f))
- || (Math::CeilToInt(animPos) == Math::CeilToInt(anim->GetDuration()) - 1 && Math::CeilToInt(animPrevPos) == Math::CeilToInt(anim->GetDuration()) && Math::NearEqual(k.Time, anim->GetDuration() - 1.0f))
+ || (Math::CeilToInt(animPos) == animDuration && Math::CeilToInt(animPrevPos) == animDuration - 1 && Math::NearEqual(k.Time, anim->GetDuration() - 1.0f))
+ || (Math::CeilToInt(animPos) == animDuration - 1 && Math::CeilToInt(animPrevPos) == animDuration && Math::NearEqual(k.Time, anim->GetDuration() - 1.0f))
|| (Math::FloorToInt(animPos) == 0 && Math::FloorToInt(animPrevPos) == 0 && k.Time == 0.0f)
)
{
@@ -314,11 +315,11 @@ float GetAnimPos(float& timePos, float startTimePos, float speed, bool loop, flo
if (loop)
{
// Animation looped (revered playback)
- result = length - result;
+ result = length + Math::Mod(result, length);
}
else
{
- // Animation ended (revered playback)
+ // Animation ended (reversed playback)
result = 0;
}
timePos = result;
@@ -346,12 +347,14 @@ float GetAnimSamplePos(float length, Animation* anim, float pos)
{
// Convert into animation local time (track length may be bigger so fill the gaps with animation clip and include playback speed)
// Also, scale the animation to fit the total animation node length without cut in a middle
- const auto animLength = anim->GetLength();
+ const float animLength = anim->GetLength();
+ if (animLength <= ANIM_GRAPH_BLEND_THRESHOLD)
+ return 0;
const int32 cyclesCount = Math::Max(Math::FloorToInt(length / animLength), 1);
const float cycleLength = animLength * (float)cyclesCount;
const float adjustRateScale = length / cycleLength;
- auto animPos = pos * adjustRateScale;
- while (animPos > animLength)
+ float animPos = pos * adjustRateScale;
+ animPos = Math::Mod(animPos, animLength);
{
animPos -= animLength;
}
@@ -496,9 +499,23 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
const bool motionPositionXZ = EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::RootPositionXZ);
const bool motionPositionY = EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::RootPositionY);
const bool motionRotation = EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::RootRotation);
- const Vector3 motionPositionMask(motionPositionXZ ? 1.0f : 0.0f, motionPositionY ? 1.0f : 0.0f, motionPositionXZ ? 1.0f : 0.0f);
const bool motionPosition = motionPositionXZ | motionPositionY;
+ const Vector3 motionPositionMaskRef(motionPositionXZ ? 1.0f : 0.0f, motionPositionY ? 1.0f : 0.0f, motionPositionXZ ? 1.0f : 0.0f);
+ Vector3 motionPositionMask = motionPositionMaskRef;
const int32 rootNodeIndex = GetRootNodeIndex(anim);
+ auto& skeleton = _graph.BaseModel->Skeleton;
+ if (motionPosition && EnumHasAnyFlags(anim->Data.RootMotionFlags, AnimationRootMotionFlags::LocalPositionMask))
+ {
+ // Rotate position mask by the rotation of the root node to extract motion in the local space of the model (eg. when skeleton has different coordinate system)
+ Quaternion rootOrientation = Quaternion::Identity;
+ int32 parentIndex = skeleton.Nodes[rootNodeIndex].ParentIndex;
+ while (parentIndex != -1)
+ {
+ rootOrientation = rootOrientation * skeleton.Nodes[parentIndex].LocalTransform.Orientation;
+ parentIndex = skeleton.Nodes[parentIndex].ParentIndex;
+ }
+ Vector3::Transform(motionPositionMask, rootOrientation, motionPositionMask);
+ }
const Transform& refPose = emptyNodes->Nodes[rootNodeIndex];
Transform& rootNode = nodes->Nodes[rootNodeIndex];
Transform& dstNode = nodes->RootMotion;
@@ -542,12 +559,10 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
}
// Convert root motion from local-space to the actor-space (eg. if root node is not actually a root and its parents have rotation/scale)
- auto& skeleton = _graph.BaseModel->Skeleton;
int32 parentIndex = skeleton.Nodes[rootNodeIndex].ParentIndex;
while (parentIndex != -1)
{
- const Transform& parentNode = nodes->Nodes[parentIndex];
- srcNode.Translation = parentNode.LocalToWorld(srcNode.Translation);
+ srcNode.Translation = nodes->Nodes[parentIndex].LocalToWorld(srcNode.Translation);
parentIndex = skeleton.Nodes[parentIndex].ParentIndex;
}
}
@@ -562,28 +577,28 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
if (mode == ProcessAnimationMode::BlendAdditive)
{
if (motionPosition)
- dstNode.Translation += srcNode.Translation * weight * motionPositionMask;
+ dstNode.Translation += srcNode.Translation * weight * motionPositionMaskRef;
if (motionRotation)
BlendAdditiveWeightedRotation(dstNode.Orientation, srcNode.Orientation, weight);
}
else if (mode == ProcessAnimationMode::Add)
{
if (motionPosition)
- dstNode.Translation += srcNode.Translation * weight * motionPositionMask;
+ dstNode.Translation += srcNode.Translation * weight * motionPositionMaskRef;
if (motionRotation)
dstNode.Orientation += srcNode.Orientation * weight;
}
else if (weighted)
{
if (motionPosition)
- dstNode.Translation = srcNode.Translation * weight * motionPositionMask;
+ dstNode.Translation = srcNode.Translation * weight * motionPositionMaskRef;
if (motionRotation)
dstNode.Orientation = srcNode.Orientation * weight;
}
else
{
if (motionPosition)
- dstNode.Translation = srcNode.Translation * motionPositionMask;
+ dstNode.Translation = srcNode.Translation * motionPositionMaskRef;
if (motionRotation)
dstNode.Orientation = srcNode.Orientation;
}
@@ -1020,7 +1035,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
const float length = anim ? anim->GetLength() : 0.0f;
// Calculate new time position
- if (speed < 0.0f && bucket.LastUpdateFrame < context.CurrentFrameIndex - 1)
+ if (speed * context.DeltaTime < 0.0f && bucket.LastUpdateFrame < context.CurrentFrameIndex - 1)
{
// If speed is negative and it's the first node update then start playing from end
bucket.TimePosition = length;
@@ -1425,11 +1440,12 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
{
const auto aIndex = data.IndicesSorted[i];
const auto bIndex = data.IndicesSorted[i + 1];
+ ASSERT_LOW_LAYER(aIndex != ANIM_GRAPH_MULTI_BLEND_INVALID);
const auto aData = node->Values[4 + aIndex * 2].AsFloat4();
AnimSampleData a(node->Assets[aIndex].As(), aData.W, aIndex);
// Check single A case
- if (x <= aData.X + ANIM_GRAPH_BLEND_THRESHOLD)
+ if (x <= aData.X + ANIM_GRAPH_BLEND_THRESHOLD || bIndex == ANIM_GRAPH_MULTI_BLEND_INVALID)
{
MultiBlendAnimData::BeforeSample(context, bucket, prevList, a, speed);
value = SampleAnimation(node, loop, startTimePos, a);
@@ -1440,6 +1456,8 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
// Get B animation data
auto bData = node->Values[4 + bIndex * 2].AsFloat4();
AnimSampleData b(node->Assets[bIndex].As(), bData.W, bIndex);
+ if (syncLength)
+ a.Length = b.Length = data.Length;
// Check single B edge case
if (Math::NearEqual(bData.X, x, ANIM_GRAPH_BLEND_THRESHOLD))
@@ -2356,7 +2374,8 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
{
// Start playing animation
bucket.Index = i;
- // Keep bucket time position and blend in time for if blending between two anims in the same slot.
+
+ // Keep bucket time position and blend in time for if blending between two anims in the same slot
bucket.TimePosition = bucket.TimePosition;
bucket.BlendInPosition = bucket.BlendInPosition;
bucket.BlendOutPosition = 0.0f;
@@ -2368,7 +2387,8 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
if (bucket.Index == -1 || !slots[bucket.Index].Animation->IsLoaded())
{
value = tryGetValue(node->GetBox(1), Value::Null);
- // Reset times if time is left over from playing between different anims in the same slot.
+
+ // Reset times if time is left over from playing between different anims in the same slot
if (bucket.BlendInPosition > 0)
{
bucket.TimePosition = 0;
@@ -2411,30 +2431,39 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
bucket.TimePosition = newTimePos;
// On animation slot stop
- if (slot.Reset)
+ if (slot.Stop)
{
- // Blend between last anim and new anim if found, otherwise blend back to input.
- Animation* sAnim = nullptr;
+ // Blend between last anim and new anim if found, otherwise blend back to input
+ Animation* otherAnim = nullptr;
for (int32 i = 0; i < slots.Count(); i++)
{
if (bucket.Index == i)
continue;
-
- auto& s = slots[i];
- if (s.Animation && s.Name == slotName)
+ auto& other = slots[i];
+ if (other.Animation && other.Name == slotName)
{
- sAnim = s.Animation;
+ otherAnim = other.Animation;
+ other.ActiveBlend = true;
+ if (other.Rewind)
+ {
+ other.Rewind = false;
+ bucket.BlendOutPosition = 0;
+ }
+ }
+ else if (other.Name == slotName)
+ {
+ other.ActiveBlend = false;
}
}
float oldTimePos = bucket.BlendOutPosition;
bucket.BlendOutPosition += deltaTime;
bucket.BlendInPosition = bucket.BlendOutPosition;
- const float alpha = bucket.BlendOutPosition / slot.BlendOutTime;
- if (sAnim != nullptr)
+ const float alpha = slot.BlendOutTime > ANIM_GRAPH_BLEND_THRESHOLD ? bucket.BlendOutPosition / slot.BlendOutTime : 1.0f;
+ if (otherAnim != nullptr)
{
- auto sValue = SampleAnimation(node, false, sAnim->GetLength(), 0.0f, oldTimePos, bucket.BlendInPosition, sAnim, 1);
+ auto otherValue = SampleAnimation(node, false, otherAnim->GetLength(), 0.0f, oldTimePos, bucket.BlendInPosition, otherAnim, 1);
//value = SampleAnimationsWithBlend(node, false, length, 0.0f, bucket.TimePosition, newTimePos, anim, sAnim, 1, 1, alpha);
- value = Blend(node, value, sValue, alpha, AlphaBlendMode::HermiteCubic);
+ value = Blend(node, value, otherValue, alpha, AlphaBlendMode::HermiteCubic);
}
else
{
@@ -2444,10 +2473,11 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
if (bucket.BlendOutPosition >= slot.BlendOutTime)
{
- // Start from the beginning or the blend in position if next anim found.
+ // Start from the beginning or the blend in position if next anim found
slot.Animation = nullptr;
- slot.Reset = false;
- if (!sAnim)
+ slot.Stop = false;
+ slot.ActiveBlend = false;
+ if (!otherAnim)
{
bucket.TimePosition = 0;
bucket.BlendInPosition = 0;
@@ -2460,7 +2490,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
break;
}
- if (bucket.LoopsLeft == 0 && slot.BlendOutTime > 0.0f && length - slot.BlendOutTime < bucket.TimePosition)
+ if (bucket.LoopsLeft == 0 && slot.BlendOutTime > ANIM_GRAPH_BLEND_THRESHOLD && length - slot.BlendOutTime < bucket.TimePosition)
{
// Blend out
auto input = tryGetValue(node->GetBox(1), Value::Null);
@@ -2468,7 +2498,7 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu
const float alpha = bucket.BlendOutPosition / slot.BlendOutTime;
value = Blend(node, value, input, alpha, AlphaBlendMode::HermiteCubic);
}
- else if (bucket.LoopsDone == 0 && slot.BlendInTime > 0.0f && bucket.BlendInPosition < slot.BlendInTime)
+ else if (bucket.LoopsDone == 0 && slot.BlendInTime > ANIM_GRAPH_BLEND_THRESHOLD && bucket.BlendInPosition < slot.BlendInTime)
{
// Blend in
auto input = tryGetValue(node->GetBox(1), Value::Null);
diff --git a/Source/Engine/Audio/Audio.cpp b/Source/Engine/Audio/Audio.cpp
index 3fea63ceb..ee71ec27d 100644
--- a/Source/Engine/Audio/Audio.cpp
+++ b/Source/Engine/Audio/Audio.cpp
@@ -41,6 +41,8 @@ Array Audio::Sources;
Array Audio::Devices;
Action Audio::DevicesChanged;
Action Audio::ActiveDeviceChanged;
+Action Audio::DeviceAdded;
+Action Audio::DeviceRemoved;
AudioBackend* AudioBackend::Instance = nullptr;
namespace
@@ -50,6 +52,8 @@ namespace
int32 ActiveDeviceIndex = -1;
bool MuteOnFocusLoss = true;
bool EnableHRTF = true;
+ StringAnsi ExplicitDeviceName;
+ StringAnsi SystemDefaultDeviceName;
}
class AudioService : public EngineService
@@ -93,7 +97,22 @@ void AudioSettings::Apply()
AudioDevice* Audio::GetActiveDevice()
{
- return &Devices[ActiveDeviceIndex];
+ if (ActiveDeviceIndex >= 0 && ActiveDeviceIndex < Devices.Count())
+ return &Devices[ActiveDeviceIndex];
+
+ if (!SystemDefaultDeviceName.IsEmpty() && Devices.HasItems())
+ {
+ for (int32 i = 0; i < Devices.Count(); i++)
+ {
+ if (Devices[i].InternalName == SystemDefaultDeviceName)
+ return &Devices[i];
+ }
+ }
+
+ if (Devices.HasItems())
+ return &Devices[0];
+
+ return nullptr;
}
int32 Audio::GetActiveDeviceIndex()
@@ -101,9 +120,28 @@ int32 Audio::GetActiveDeviceIndex()
return ActiveDeviceIndex;
}
+int32 Audio::GetActiveDeviceResolvedIndex()
+{
+ AudioDevice* active = GetActiveDevice();
+ if (active != nullptr)
+ {
+ for (int32 i = 0; i < Devices.Count(); i++)
+ {
+ if (&Devices[i] == active)
+ return i;
+ }
+ }
+ return -1;
+}
+
void Audio::SetActiveDeviceIndex(int32 index)
{
index = Math::Clamp(index, -1, Devices.Count() - 1);
+ if (index >= 0 && index < Devices.Count())
+ ExplicitDeviceName = Devices[index].InternalName;
+ else
+ ExplicitDeviceName.Clear();
+
if (ActiveDeviceIndex == index)
return;
@@ -114,6 +152,21 @@ void Audio::SetActiveDeviceIndex(int32 index)
ActiveDeviceChanged();
}
+void Audio::SetActiveDeviceIndexSilent(int32 index)
+{
+ ActiveDeviceIndex = Math::Clamp(index, -1, Devices.Count() - 1);
+}
+
+const StringAnsi& Audio::GetExplicitDeviceName()
+{
+ return ExplicitDeviceName;
+}
+
+void Audio::SetSystemDefaultDeviceName(const StringAnsi& name)
+{
+ SystemDefaultDeviceName = name;
+}
+
float Audio::GetMasterVolume()
{
return MasterVolume;
diff --git a/Source/Engine/Audio/Audio.h b/Source/Engine/Audio/Audio.h
index 36b5b3551..f9f03624b 100644
--- a/Source/Engine/Audio/Audio.h
+++ b/Source/Engine/Audio/Audio.h
@@ -43,6 +43,16 @@ public:
///
API_EVENT() static Action ActiveDeviceChanged;
+ ///
+ /// Event called when a new audio device is connected to the system.
+ ///
+ API_EVENT() static Action DeviceAdded;
+
+ ///
+ /// Event called when an audio device is disconnected from the system.
+ ///
+ API_EVENT() static Action DeviceRemoved;
+
public:
///
/// Gets the active device.
@@ -51,17 +61,39 @@ public:
API_PROPERTY() static AudioDevice* GetActiveDevice();
///
- /// Gets the index of the active device.
+ /// Gets the index of the active device. Returns -1 if set to system default mode.
///
/// The active device index.
API_PROPERTY() static int32 GetActiveDeviceIndex();
+ ///
+ /// Gets the resolved index in Devices array of the device currently playing (resolves system default mode to the actual device index).
+ ///
+ /// The resolved active device index.
+ API_PROPERTY() static int32 GetActiveDeviceResolvedIndex();
+
///
/// Sets the index of the active device.
///
/// The index.
API_PROPERTY() static void SetActiveDeviceIndex(int32 index);
+ ///
+ /// Sets the index of the active device without notifying backend.
+ ///
+ /// The index.
+ static void SetActiveDeviceIndexSilent(int32 index);
+
+ ///
+ /// Gets the internal device name explicitly selected by the user (empty if using dynamic system default device).
+ ///
+ static const StringAnsi& GetExplicitDeviceName();
+
+ ///
+ /// Sets the system default internal device name.
+ ///
+ static void SetSystemDefaultDeviceName(const StringAnsi& name);
+
public:
///
/// Gets the master volume applied to all the audio sources (normalized to range 0-1).
diff --git a/Source/Engine/Audio/AudioBackend.h b/Source/Engine/Audio/AudioBackend.h
index bd91185a4..53b207854 100644
--- a/Source/Engine/Audio/AudioBackend.h
+++ b/Source/Engine/Audio/AudioBackend.h
@@ -102,6 +102,9 @@ public:
class Source
{
public:
+ // Helper accessors for Audio Source playback restoring after audio device change
+ static void SetStreamingFirstChunk(AudioSource* source, int32 chunkIndex);
+
FORCE_INLINE static uint32 Add(const AudioDataInfo& format, const Vector3& position, const Quaternion& orientation, float volume, float pitch, float pan, bool loop, bool spatial, float attenuation, float minDistance, float doppler)
{
return Instance->Source_Add(format, position, orientation, volume, pitch, pan, loop, spatial, attenuation, minDistance, doppler);
diff --git a/Source/Engine/Audio/AudioSource.cpp b/Source/Engine/Audio/AudioSource.cpp
index c8b0f3bab..8629da70e 100644
--- a/Source/Engine/Audio/AudioSource.cpp
+++ b/Source/Engine/Audio/AudioSource.cpp
@@ -12,6 +12,11 @@
#include "AudioBackend.h"
#include "Audio.h"
+void AudioBackend::Source::SetStreamingFirstChunk(AudioSource* source, int32 chunkIndex)
+{
+ source->_streamingFirstChunk = chunkIndex;
+}
+
AudioSource::AudioSource(const SpawnParams& params)
: Actor(params)
, _velocity(Vector3::Zero)
@@ -195,13 +200,14 @@ void AudioSource::Stop()
_state = States::Stopped;
_isActuallyPlayingSth = false;
_streamingFirstChunk = 0;
+ _savedTime = 0.0f;
if (SourceID)
AudioBackend::Source::Stop(SourceID);
}
float AudioSource::GetTime() const
{
- if (_state == States::Stopped || SourceID == 0 || !Clip->IsLoaded())
+ if (_state == States::Stopped || SourceID == 0 || Clip == nullptr || !Clip->IsLoaded())
return 0.0f;
float time = AudioBackend::Source::GetCurrentBufferTime(SourceID);
@@ -216,6 +222,11 @@ float AudioSource::GetTime() const
time = Math::Clamp(time, 0.0f, Clip->GetLength());
+ if (time <= 0.0001f && _savedTime > 0.0001f)
+ {
+ time = _savedTime;
+ }
+
return time;
}
@@ -233,6 +244,9 @@ void AudioSource::SetTime(float time)
Stop();
}
+ // Restore _savedTime after Stop() resets it
+ _savedTime = time;
+
if (UseStreaming())
{
// Update the first audio clip chunk to use for streaming and peek the relative time offset from the chunk start
@@ -402,16 +416,24 @@ void AudioSource::Update()
AudioBackend::Source::VelocityChanged(SourceID, _velocity);
}
+ float curTime = 0.0f;
+ if (_state != States::Stopped && SourceID && Clip && Clip->IsLoaded())
+ {
+ curTime = GetTime();
+ if (curTime > 0.0001f)
+ _savedTime = curTime;
+ }
+
// Reset starting to play value once time is greater than zero
- if (_startingToPlay && GetTime() > 0.0f)
+ if (_startingToPlay && curTime > 0.0f)
{
_startingToPlay = false;
}
- if (Math::NearEqual(GetTime(), _startTime) && _isActuallyPlayingSth && _startingToPlay)
+ if (Math::NearEqual(curTime, _startTime) && _isActuallyPlayingSth && _startingToPlay)
ClipStarted();
- if (!UseStreaming() && Math::NearEqual(GetTime(), 0.0f) && _isActuallyPlayingSth && !_startingToPlay)
+ if (!UseStreaming() && Math::NearEqual(curTime, 0.0f) && _isActuallyPlayingSth && !_startingToPlay)
{
int32 queuedBuffers;
AudioBackend::Source::GetQueuedBuffersCount(SourceID, queuedBuffers);
diff --git a/Source/Engine/Audio/AudioSource.h b/Source/Engine/Audio/AudioSource.h
index db698f3ba..00adc35e5 100644
--- a/Source/Engine/Audio/AudioSource.h
+++ b/Source/Engine/Audio/AudioSource.h
@@ -18,6 +18,7 @@ class FLAXENGINE_API AudioSource : public Actor, IAssetReference
DECLARE_SCENE_OBJECT(AudioSource);
friend class AudioStreamingHandler;
friend class AudioClip;
+ friend class AudioBackend;
public:
///
diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
index f32c34d30..501ebbb66 100644
--- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
+++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
@@ -3,11 +3,13 @@
#if AUDIO_API_OPENAL
#include "AudioBackendOAL.h"
+#include "Engine/Platform/Platform.h"
#include "Engine/Platform/StringUtils.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Tools/AudioTool/AudioTool.h"
#include "Engine/Engine/Units.h"
+#include "Engine/Engine/Time.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Audio/Audio.h"
@@ -204,23 +206,57 @@ namespace ALC
Listener::Rebuild(listener);
}
- void RebuildSources(const Array& states)
+ void RebuildSources(const Array& states, float elapsedTime = 0.0f)
{
for (int32 i = 0; i < states.Count(); i++)
{
AudioSource* source = Audio::Sources[i];
Source::Rebuild(source->SourceID, source->GetPosition(), source->GetOrientation(), source->GetVolume(), source->GetPitch(), source->GetPan(), source->GetIsLooping() && !source->UseStreaming(), source->Is3D(), source->GetAttenuation(), source->GetMinDistance(), source->GetDopplerFactor());
- if (states.HasItems() && source->SourceID)
+ if (source->SourceID)
{
- // Restore playback state
+ // Restore playback state and time position without restarting or pausing
auto& state = states[i];
if (state.State != AudioSource::States::Stopped)
- source->Play();
- if (state.State == AudioSource::States::Paused)
- source->Pause();
- if (state.State != AudioSource::States::Stopped)
- source->SetTime(state.Time);
+ {
+ if (source->Clip && source->Clip->IsLoaded())
+ {
+ float targetTime = state.Time;
+ if (state.State == AudioSource::States::Playing && elapsedTime > 0.0f)
+ {
+ targetTime += elapsedTime;
+ const float clipLength = source->Clip->GetLength();
+ if (clipLength > 0.0001f)
+ {
+ if (source->GetIsLooping())
+ targetTime = fmodf(targetTime, clipLength);
+ else
+ targetTime = Math::Min(targetTime, clipLength);
+ }
+ }
+
+ if (source->UseStreaming())
+ {
+ float relativeTime = 0;
+ const int32 chunkIndex = source->Clip->GetFirstBufferIndex(targetTime, relativeTime);
+ AudioBackend::Source::SetStreamingFirstChunk(source, chunkIndex);
+ source->RequestStreamingBuffersUpdate();
+ }
+ else
+ {
+ if (source->Clip->Buffers.HasItems() && source->Clip->Buffers[0] != 0)
+ {
+ AudioBackend::Source::SetNonStreamingBuffer(source->SourceID, source->Clip->Buffers[0]);
+ AudioBackend::Source::SetCurrentBufferTime(source->SourceID, targetTime);
+ }
+ }
+
+ if (state.State == AudioSource::States::Playing)
+ source->Play();
+ else if (state.State == AudioSource::States::Paused)
+ source->Pause();
+ }
+ }
}
}
}
@@ -658,7 +694,6 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged()
{
PROFILE_CPU();
PROFILE_MEM(Audio);
-
// Fast-path on startup
if (!ALC::Inited && ALC::Device)
{
@@ -666,6 +701,8 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged()
return;
}
+ const double startTime = Platform::GetTimeSeconds();
+
// Cleanup
Array states;
states.EnsureCapacity(Audio::Sources.Count());
@@ -687,15 +724,41 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged()
}
// Open device
- const StringAnsi& name = Audio::GetActiveDevice()->InternalName;
- ALC::Device = alcOpenDevice(name.Get());
+ const StringAnsi* namePtr = (Audio::Devices.HasItems() && Audio::GetActiveDeviceIndex() >= 0 && Audio::GetActiveDeviceIndex() < Audio::Devices.Count()) ? &Audio::GetActiveDevice()->InternalName : nullptr;
+ if (namePtr)
+ {
+ ALC::Device = alcOpenDevice(namePtr->Get());
+ if (ALC::Device == nullptr)
+ {
+ LOG(Warning, "Failed to open active OpenAL device ({0}). Trying default device...", String(*namePtr));
+ ALC::Device = alcOpenDevice(nullptr);
+ }
+ }
+ else
+ {
+ ALC::Device = alcOpenDevice(nullptr);
+ }
+
+ if (ALC::Device == nullptr && Audio::Devices.HasItems())
+ {
+ for (int32 i = 0; i < Audio::Devices.Count(); i++)
+ {
+ ALC::Device = alcOpenDevice(Audio::Devices[i].InternalName.Get());
+ if (ALC::Device != nullptr)
+ break;
+ }
+ }
+
if (ALC::Device == nullptr)
{
- LOG(Fatal, "Failed to open OpenAL device ({0}).", String(name));
+ LOG(Warning, "Failed to open any OpenAL audio device.");
return;
}
if (ALC::Inited)
- LOG(Info, "Changed audio device to: {}", String(Audio::GetActiveDevice()->Name));
+ {
+ const AudioDevice* activeDev = Audio::GetActiveDevice();
+ LOG(Info, "Changed audio device to: {}", activeDev ? String(activeDev->Name) : TEXT("Default"));
+ }
// Rebuild context
ALC::RebuildContext();
@@ -719,11 +782,16 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged()
else
{
// Reload audio clip
- auto assetLock = audioClip->Storage->Lock();
- audioClip->LoadChunk(0);
- audioClip->Buffers[0] = AudioBackend::Buffer::Create();
- audioClip->WriteBuffer(0);
-
+ if (audioClip->Storage)
+ {
+ auto assetLock = audioClip->Storage->Lock();
+ audioClip->LoadChunk(0);
+ }
+ if (audioClip->Buffers.HasItems())
+ {
+ audioClip->Buffers[0] = AudioBackend::Buffer::Create();
+ audioClip->WriteBuffer(0);
+ }
}
}
@@ -739,8 +807,9 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged()
player.AudioSource = 0;
}
}
+ const float elapsedTime = static_cast(Platform::GetTimeSeconds() - startTime);
ALC::RebuildListeners();
- ALC::RebuildSources(states);
+ ALC::RebuildSources(states, elapsedTime);
}
void AudioBackendOAL::Base_SetDopplerFactor(float value)
@@ -786,6 +855,7 @@ bool AudioBackendOAL::Base_Init()
const ALCchar* devicesStr = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER);
const StringAnsi defaultDeviceName(defaultDevice);
+ Audio::SetSystemDefaultDeviceName(defaultDeviceName);
devices.Clear();
devices.EnsureCapacity(8);
@@ -848,13 +918,9 @@ bool AudioBackendOAL::Base_Init()
}
// Init
- alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); // Default attenuation model
- if (Audio::GetActiveDeviceIndex() == Math::Clamp(activeDeviceIndex, -1, Audio::Devices.Count() - 1))
- {
- // Manually create context if SetActiveDeviceIndex won't call it
- Base_OnActiveDeviceChanged();
- }
- Audio::SetActiveDeviceIndex(activeDeviceIndex);
+ // Create context and open initial device
+ Base_OnActiveDeviceChanged();
+ Audio::SetActiveDeviceIndexSilent(activeDeviceIndex);
#ifdef AL_SOFT_source_spatialize
if (ALC::IsExtensionSupported("AL_SOFT_source_spatialize"))
ALC::Features = EnumAddFlags(ALC::Features, FeatureFlags::SpatialMultiChannel);
@@ -875,8 +941,204 @@ bool AudioBackendOAL::Base_Init()
return false;
}
+#ifndef ALC_CONNECTED
+#define ALC_CONNECTED 0x313
+#endif
+
void AudioBackendOAL::Base_Update()
{
+ // Check if active audio device got disconnected or invalidated (WASAPI AUDCLNT_E_DEVICE_INVALIDATED 0x88890004)
+ bool forceCheck = false;
+ if (ALC::Device != nullptr)
+ {
+ ALCint connected = ALC_TRUE;
+ alcGetIntegerv(ALC::Device, ALC_CONNECTED, 1, &connected);
+ if (connected == ALC_FALSE || alcGetError(ALC::Device) != ALC_NO_ERROR)
+ {
+ LOG(Warning, "Active audio device disconnected. Swapping active audio device...");
+ forceCheck = true;
+ }
+ }
+
+#if ALC_ENUMERATE_ALL_EXT
+
+ // Update audio devices list periodically (throttled every 0.25 seconds / 250ms, or immediately if disconnected)
+ static float deviceCheckTimer = 0.0f;
+ static StringAnsi lastDefaultDeviceName;
+ deviceCheckTimer += Time::Update.UnscaledDeltaTime.GetTotalSeconds();
+ if (deviceCheckTimer >= 0.25f || forceCheck)
+ {
+ deviceCheckTimer = 0.0f;
+ if (ALC::IsExtensionSupported("ALC_ENUMERATE_ALL_EXT"))
+ {
+ const ALCchar* defaultDevice = alcGetString(nullptr, ALC_DEFAULT_ALL_DEVICES_SPECIFIER);
+ const StringAnsi defaultDeviceName(defaultDevice);
+ Audio::SetSystemDefaultDeviceName(defaultDeviceName);
+ const ALCchar* devicesStr = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER);
+ Array newDevices;
+ int32 defaultDeviceIndex = -1;
+
+ while (devicesStr && *devicesStr)
+ {
+ const int32 i = newDevices.Count();
+ auto& device = newDevices.AddOne();
+ device.InternalName = devicesStr;
+ device.Name = String(device.InternalName).TrimTrailing();
+ device.Name.Replace(TEXT("OpenAL Soft on "), TEXT(""));
+
+ if (device.InternalName == defaultDeviceName)
+ {
+ defaultDeviceIndex = i;
+ }
+
+ devicesStr += (device.InternalName.Length() + 1) * sizeof(ALCchar);
+ }
+
+ if (defaultDeviceIndex == -1 && !newDevices.IsEmpty())
+ defaultDeviceIndex = 0;
+
+ bool devicesListChanged = newDevices.Count() != Audio::Devices.Count();
+ if (!devicesListChanged)
+ {
+ for (int32 i = 0; i < newDevices.Count(); i++)
+ {
+ if (newDevices[i].InternalName != Audio::Devices[i].InternalName)
+ {
+ devicesListChanged = true;
+ break;
+ }
+ }
+ }
+
+ const int32 currentActiveIndex = Audio::GetActiveDeviceIndex();
+ const StringAnsi& explicitDeviceName = Audio::GetExplicitDeviceName();
+
+ // Determine the internal name of the currently active device.
+ // When in System Default Mode (index -1), fall back to the OS default name so
+ // comparisons correctly identify "no change" and avoid spurious device swaps.
+ StringAnsi currentActiveInternalName;
+ if (currentActiveIndex >= 0 && currentActiveIndex < Audio::Devices.Count())
+ currentActiveInternalName = Audio::Devices[currentActiveIndex].InternalName;
+ else if (explicitDeviceName.IsEmpty())
+ currentActiveInternalName = defaultDeviceName; // System Default Mode: treat current as OS default
+
+ int32 currentActiveNewIndex = -1;
+ int32 explicitDeviceNewIndex = -1;
+
+ for (int32 i = 0; i < newDevices.Count(); i++)
+ {
+ if (!currentActiveInternalName.IsEmpty() && newDevices[i].InternalName == currentActiveInternalName)
+ {
+ currentActiveNewIndex = i;
+ }
+ if (!explicitDeviceName.IsEmpty() && newDevices[i].InternalName == explicitDeviceName)
+ {
+ explicitDeviceNewIndex = i;
+ }
+ }
+
+ bool defaultDeviceChanged = (!lastDefaultDeviceName.IsEmpty() && lastDefaultDeviceName != defaultDeviceName);
+ lastDefaultDeviceName = defaultDeviceName;
+
+ int32 targetActiveIndex = -1;
+ if (!explicitDeviceName.IsEmpty())
+ {
+ // Explicit Device Lock Mode: User selected a specific audio device
+ if (explicitDeviceNewIndex != -1)
+ {
+ // Explicit device is available (stay locked or re-claim explicit device)
+ targetActiveIndex = explicitDeviceNewIndex;
+ }
+ else if (forceCheck || currentActiveNewIndex == -1)
+ {
+ // Explicit device disconnected -> Fallback to system default
+ targetActiveIndex = (defaultDeviceIndex != -1) ? defaultDeviceIndex : 0;
+ }
+ else
+ {
+ targetActiveIndex = currentActiveNewIndex;
+ }
+ }
+ else
+ {
+ // Dynamic System Default Mode: Automatically follow OS default device changes
+ if (forceCheck)
+ {
+ targetActiveIndex = (defaultDeviceIndex != -1) ? defaultDeviceIndex : 0;
+ }
+ else if (defaultDeviceChanged && defaultDeviceIndex != -1)
+ {
+ targetActiveIndex = defaultDeviceIndex;
+ }
+ else if (currentActiveNewIndex != -1)
+ {
+ targetActiveIndex = currentActiveNewIndex;
+ }
+ else if (defaultDeviceIndex != -1)
+ {
+ targetActiveIndex = defaultDeviceIndex;
+ }
+ }
+
+ if (devicesListChanged)
+ {
+ // Fire DeviceAdded for each device in newDevices not present in old list
+ for (int32 i = 0; i < newDevices.Count(); i++)
+ {
+ bool found = false;
+ for (int32 j = 0; j < Audio::Devices.Count(); j++)
+ {
+ if (Audio::Devices[j].InternalName == newDevices[i].InternalName)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ Audio::DeviceAdded();
+ }
+
+ // Fire DeviceRemoved for each device in old list not present in newDevices
+ for (int32 i = 0; i < Audio::Devices.Count(); i++)
+ {
+ bool found = false;
+ for (int32 j = 0; j < newDevices.Count(); j++)
+ {
+ if (newDevices[j].InternalName == Audio::Devices[i].InternalName)
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ Audio::DeviceRemoved();
+ }
+
+ Audio::Devices = newDevices;
+ Audio::DevicesChanged();
+ }
+
+ if (targetActiveIndex != -1)
+ {
+ StringAnsi targetActiveInternalName = newDevices[targetActiveIndex].InternalName;
+ if (forceCheck)
+ {
+ Audio::SetActiveDeviceIndexSilent(targetActiveIndex);
+ Base_OnActiveDeviceChanged();
+ }
+ else if (targetActiveInternalName != currentActiveInternalName)
+ {
+ Audio::SetActiveDeviceIndexSilent(targetActiveIndex);
+ Base_OnActiveDeviceChanged();
+ }
+ else
+ {
+ Audio::SetActiveDeviceIndexSilent(targetActiveIndex);
+ }
+ }
+ }
+ }
+#endif
}
void AudioBackendOAL::Base_Dispose()
diff --git a/Source/Engine/Graphics/Models/ModelData.Tool.cpp b/Source/Engine/Graphics/Models/ModelData.Tool.cpp
index 01504f8ce..55fad0c6e 100644
--- a/Source/Engine/Graphics/Models/ModelData.Tool.cpp
+++ b/Source/Engine/Graphics/Models/ModelData.Tool.cpp
@@ -214,6 +214,7 @@ int32 FindVertex(const MeshData& mesh, int32 vertexIndex, int32 startIndex, int3
#if USE_SPATIAL_SORT
, const Assimp::SpatialSort& spatialSort
, std::vector& spatialSortCache
+ , float posEpsilon
#endif
)
{
@@ -225,7 +226,7 @@ int32 FindVertex(const MeshData& mesh, int32 vertexIndex, int32 startIndex, int3
#if USE_SPATIAL_SORT
const Float3 vPosition = mesh.Positions[vertexIndex];
- spatialSort.FindPositions(*(aiVector3D*)&vPosition, 1e-5f, spatialSortCache);
+ spatialSort.FindPositions(*(aiVector3D*)&vPosition, posEpsilon, spatialSortCache);
if (spatialSortCache.empty())
return INVALID_INDEX;
@@ -319,6 +320,14 @@ void MeshData::BuildIndexBuffer()
Assimp::SpatialSort vertexFinder;
vertexFinder.Fill((const aiVector3D*)Positions.Get(), vertexCount, sizeof(Float3));
std::vector spatialSortCache;
+
+ // Ensure that the weld radius stays above float precision based on the mesh's largest coordinate
+ float maxAbs = 0.0f;
+ for (int32 i = 0; i < vertexCount; i++)
+ {
+ maxAbs = Math::Max(maxAbs, Positions[i].GetAbsolute().MaxValue());
+ }
+ const float posEpsilon = Math::Max(1e-5f, maxAbs * 1e-6f);
#endif
// Build index buffer
@@ -327,7 +336,7 @@ void MeshData::BuildIndexBuffer()
// Find duplicated vertex before the current one
const int32 reuseVertexIndex = FindVertex(*this, vertexIndex, 0, vertexIndex, mapping
#if USE_SPATIAL_SORT
- , vertexFinder, spatialSortCache
+ , vertexFinder, spatialSortCache, posEpsilon
#endif
);
if (reuseVertexIndex == INVALID_INDEX)
diff --git a/Source/Engine/Graphics/Textures/StreamingTexture.cpp b/Source/Engine/Graphics/Textures/StreamingTexture.cpp
index 54e30c7d7..8d13bfd5b 100644
--- a/Source/Engine/Graphics/Textures/StreamingTexture.cpp
+++ b/Source/Engine/Graphics/Textures/StreamingTexture.cpp
@@ -115,7 +115,7 @@ bool StreamingTexture::Create(const TextureHeader& header)
// Ensure that streaming doesn't go too low because the hardware expects the texture to be min in size of compressed texture block
const int32 blockSize = PixelFormatExtensions::ComputeBlockSize(_header.Format);
int32 lastMip = header.MipLevels - 1;
- while ((header.Width >> lastMip) < blockSize && (header.Height >> lastMip) < blockSize && lastMip > 0)
+ while (Math::Min(header.Width >> lastMip, header.Height >> lastMip) < blockSize && lastMip > 0)
lastMip--;
_minMipCountBlockCompressed = Math::Min(header.MipLevels - lastMip + 1, header.MipLevels);
}
diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp
index 3cc2afc59..4f6567db4 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.cpp
+++ b/Source/Engine/Level/Actors/AnimatedModel.cpp
@@ -778,17 +778,42 @@ void AnimatedModel::ClearBlendShapeWeights()
void AnimatedModel::PlaySlotAnimation(const StringView& slotName, Animation* anim, float speed, float blendInTime, float blendOutTime, int32 loopCount)
{
+ PROFILE_MEM(Animations);
CHECK(anim);
+ int32 counts = 0;
for (auto& slot : GraphInstance.Slots)
{
if (slot.Animation == anim && slot.Name == slotName)
{
- slot.Pause = false;
- slot.BlendInTime = blendInTime;
- slot.LoopCount = loopCount;
- return;
+ if (slot.Stop)
+ continue; // Stopped playing (ignore to avoid using its playback state)
+ if (slot.Pause)
+ {
+ // Resume paused animation
+ slot.Pause = false;
+ slot.Rewind = false;
+ slot.BlendInTime = blendInTime;
+ slot.LoopCount = loopCount;
+ return;
+ }
+ counts++;
}
}
+ if (counts >= 2)
+ {
+ // Don't play more than 2 instances of the same animation on the same slot (they can blend together) but don't spam queue
+ // Instead rewind the last one to the beginning
+ for (int32 i = GraphInstance.Slots.Count() - 1; i >= 0; i--)
+ {
+ auto& slot = GraphInstance.Slots[i];
+ if (slot.Animation == anim && slot.Name == slotName && slot.ActiveBlend)
+ {
+ slot.Rewind = true;
+ break;
+ }
+ }
+ return;
+ }
int32 index = 0;
for (; index < GraphInstance.Slots.Count(); index++)
{
@@ -804,11 +829,18 @@ void AnimatedModel::PlaySlotAnimation(const StringView& slotName, Animation* ani
slot.BlendInTime = blendInTime;
slot.BlendOutTime = blendOutTime;
slot.LoopCount = loopCount;
+ slot.Pause = false;
+ slot.Stop = false;
+ slot.Rewind = false;
}
void AnimatedModel::StopSlotAnimation()
{
- GraphInstance.Slots.Clear();
+ for (auto& slot : GraphInstance.Slots)
+ {
+ if (slot.Animation != nullptr)
+ slot.Stop = true;
+ }
}
void AnimatedModel::StopSlotAnimation(const StringView& slotName, Animation* anim)
@@ -817,10 +849,8 @@ void AnimatedModel::StopSlotAnimation(const StringView& slotName, Animation* ani
{
if ((slot.Animation == anim || anim == nullptr) && slot.Name == slotName)
{
- //slot.Animation = nullptr; // TODO: make an immediate version of this method and set the animation to nullptr.
if (slot.Animation != nullptr)
- slot.Reset = true;
- break;
+ slot.Stop = true;
}
}
}
@@ -1382,7 +1412,19 @@ BoundingBox AnimatedModel::GetEditorBoundingBox() const
{
if (SkinnedModel)
SkinnedModel->WaitForLoaded(100);
- return BoundingBox::MakeScaled(_box, 1.0f / BoundsScale);
+ BoundingBox box = _box;
+ if (CustomBounds.GetSize().LengthSquared() <= 0.01f)
+ {
+ if (!IsDuringPlay())
+ {
+ Matrix world;
+ GetLocalToWorldMatrix(world);
+ box = SkinnedModel->GetBox(world);
+ }
+ else
+ box = BoundingBox::MakeScaled(box, 1.0f / BoundsScale);
+ }
+ return box;
}
#endif
diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h
index b8dfac94f..adcd141dd 100644
--- a/Source/Engine/Level/Actors/AnimatedModel.h
+++ b/Source/Engine/Level/Actors/AnimatedModel.h
@@ -415,6 +415,7 @@ public:
///
/// Plays the animation on the slot in Anim Graph.
///
+ /// If animation is paused, it will be resumed. If animation is not playing, it will be started. If animation is playing, it will be blended out (call StopSlotAnimation before to do an immediate transition).
/// The name of the slot.
/// The animation to play.
/// The playback speed.
@@ -432,7 +433,7 @@ public:
/// Stops the animation playback on the slot in Anim Graph.
///
/// The name of the slot.
- /// The animation to check. Null to use slot name only.
+ /// The animation to check. Null to use slot name only. Can be used to explicitly stop a specific animation (if it's playing right now, otherwise doesn't stop other one).
API_FUNCTION() void StopSlotAnimation(const StringView& slotName, Animation* anim = nullptr);
///
diff --git a/Source/Engine/Level/Actors/EmptyActor.cpp b/Source/Engine/Level/Actors/EmptyActor.cpp
index 477e04d0a..bd2ab788b 100644
--- a/Source/Engine/Level/Actors/EmptyActor.cpp
+++ b/Source/Engine/Level/Actors/EmptyActor.cpp
@@ -7,16 +7,6 @@ EmptyActor::EmptyActor(const SpawnParams& params)
{
}
-#if USE_EDITOR
-
-BoundingBox EmptyActor::GetEditorBoundingBox() const
-{
- const Vector3 size(50);
- return BoundingBox(_transform.Translation - size, _transform.Translation + size);
-}
-
-#endif
-
void EmptyActor::OnTransformChanged()
{
// Base
diff --git a/Source/Engine/Level/Actors/EmptyActor.h b/Source/Engine/Level/Actors/EmptyActor.h
index 6515a70e4..3e3543b4a 100644
--- a/Source/Engine/Level/Actors/EmptyActor.h
+++ b/Source/Engine/Level/Actors/EmptyActor.h
@@ -11,11 +11,6 @@ API_CLASS(Attributes="ActorContextMenu(\"New/Actor\"), ActorToolbox(\"Other\")")
class FLAXENGINE_API EmptyActor : public Actor
{
DECLARE_SCENE_OBJECT(EmptyActor);
-public:
- // [Actor]
-#if USE_EDITOR
- BoundingBox GetEditorBoundingBox() const override;
-#endif
protected:
// [Actor]
diff --git a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp
index 6ae810d82..aaae6432c 100644
--- a/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp
+++ b/Source/Engine/Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp
@@ -461,7 +461,7 @@ void ParticleEmitterGPUGenerator::ProcessGroupParticles(Box* box, Node* node, Va
// Peek the function output (function->Outputs maps the functions outputs to output nodes indices)
const int32 outputIndex = box->ID - 16;
- if (outputIndex < 0 || outputIndex >= function->Outputs.Count())
+ if (!function->Outputs.IsValidIndex(outputIndex) || !graph->Nodes.IsValidIndex(function->Outputs[outputIndex]))
{
OnError(node, box, TEXT("Invalid function output box."));
value = Value::Zero;
diff --git a/Source/Engine/Platform/Defines.h b/Source/Engine/Platform/Defines.h
index ce4fbcb5c..7f7bd69f1 100644
--- a/Source/Engine/Platform/Defines.h
+++ b/Source/Engine/Platform/Defines.h
@@ -21,8 +21,9 @@ API_ENUM() enum class PlatformType
///
/// Running Windows Store App (Universal Windows Platform).
+ /// [Deprecated and unsupported platform]
///
- API_ENUM(Attributes="EditorDisplay(null, \"UWP\")")
+ API_ENUM(Attributes="EditorDisplay(null, \"UWP\"), Obsolete")
UWP = 3,
///
diff --git a/Source/Engine/Platform/SDL/SDLPlatform.Linux.cpp b/Source/Engine/Platform/SDL/SDLPlatform.Linux.cpp
index 51a843157..470b531a4 100644
--- a/Source/Engine/Platform/SDL/SDLPlatform.Linux.cpp
+++ b/Source/Engine/Platform/SDL/SDLPlatform.Linux.cpp
@@ -148,11 +148,16 @@ namespace WaylandImpl
wl_data_offer* DataOffer = nullptr; // The last accepted offer
wl_data_offer* SelectionOffer = nullptr;
+ wl_event_queue* EventQueue = nullptr;
+ bool DraggingWindow = false;
wl_data_device_listener DataDeviceListener =
{
[](void* data, wl_data_device* data_device, wl_data_offer* id) { }, // Data offer event
[](void* data, wl_data_device* data_device, uint32 serial, wl_surface* surface, wl_fixed_t x, wl_fixed_t y, wl_data_offer* id) // Enter event
{
+ if (!DraggingWindow)
+ return;
+
DataOffer = id;
SDLWindow* sourceWindow = (SDLWindow*)data;
@@ -168,14 +173,22 @@ namespace WaylandImpl
},
[](void* data, wl_data_device* data_device) // Leave event
{
+ if (!DraggingWindow)
+ return;
+
// The cursor left the surface area
if (DataOffer != nullptr)
+ {
wl_data_offer_destroy(DataOffer);
- DataOffer = nullptr;
+ DataOffer = nullptr;
+ }
},
[](void* data, wl_data_device* data_device, uint32_t time, wl_fixed_t x, wl_fixed_t y) { }, // Motion event
[](void* data, wl_data_device* data_device) // Drop event
{
+ if (!DraggingWindow)
+ return;
+
// The drop is accepted
if (DataOffer != nullptr)
{
@@ -186,6 +199,9 @@ namespace WaylandImpl
},
[](void* data, wl_data_device* data_device, wl_data_offer* id) // Selection event
{
+ if (!DraggingWindow)
+ return;
+
// Clipboard: We can read the clipboard content
if (SelectionOffer != nullptr)
wl_data_offer_destroy(SelectionOffer);
@@ -193,27 +209,31 @@ namespace WaylandImpl
},
};
+ bool DataSourceValid = false;
wl_data_source_listener DataSourceListener =
{
[](void* data, wl_data_source* source, const char* mime_type) { }, // Target event
[](void* data, wl_data_source* source, const char* mime_type, int32_t fd) // Send event
{
// Clipboard: The other end has accepted and is requesting the data
+ UnixFile file(fd);
IGuiData* inputData = static_cast(data);
- if (inputData->GetType() == IGuiData::Type::Text)
+ if (inputData != nullptr && inputData->GetType() == IGuiData::Type::Text)
{
- UnixFile file(fd);
- StringAnsi text = StringAnsi(inputData->GetAsText());
- file.Write(text.Get(), text.Length() * sizeof(StringAnsi::CharType));
- file.Close();
+ if (StringUtils::Compare(mime_type, "text/plain;charset=utf-8") == 0)
+ {
+ StringAnsi text = StringAnsi(inputData->GetAsText());
+ file.Write(text.Get(), text.Length() * sizeof(StringAnsi::CharType));
+ }
}
+ file.Close();
},
[](void* data, wl_data_source* source) // Cancelled event
{
// Clipboard: other application has replaced the content in clipboad
wl_data_source_destroy(source);
-
- IGuiData* inputData = static_cast(data);
+ DataSourceValid = false;
+
Platform::AtomicStore(&WaylandImpl::DragOverFlag, 1);
},
[](void* data, wl_data_source* source) { }, // DnD drop performed event
@@ -221,6 +241,7 @@ namespace WaylandImpl
{
// The destination has finally accepted the last given dnd_action
wl_data_source_destroy(source);
+ DataSourceValid = false;
IGuiData* inputData = static_cast(data);
Platform::AtomicStore(&WaylandImpl::DragOverFlag, 1);
@@ -229,11 +250,9 @@ namespace WaylandImpl
};
wl_data_device* DataDevice = nullptr;
- wl_event_queue* EventQueue = nullptr;
wl_data_device_manager* WrappedDataDeviceManager = nullptr;
wl_data_device* WrappedDataDevice = nullptr;
bool DraggingActive = false;
- bool DraggingWindow = false;
StringView DraggingData = nullptr;
class DragDropJob : public ThreadPoolTask
{
@@ -279,8 +298,7 @@ namespace WaylandImpl
// Offer data for consumption, the data source is destroyed elsewhere
wl_data_source* dataSource = wl_data_device_manager_create_data_source(WrappedDataDeviceManager);
- wl_data_source* wrappedDataSource = (wl_data_source*)wl_proxy_create_wrapper(dataSource);
- wl_proxy_set_queue(reinterpret_cast(wrappedDataSource), EventQueue);
+ DataSourceValid = true;
if (dragWindow)
{
wl_data_source_offer(dataSource, "flaxengine/window");
@@ -354,8 +372,11 @@ namespace WaylandImpl
Platform::AtomicStore(&DragOverFlag, 1);
- if (wrappedDataSource != nullptr)
- wl_proxy_wrapper_destroy(wrappedDataSource);
+ if (DataSourceValid)
+ {
+ wl_data_source_destroy(dataSource);
+ DataSourceValid = false;
+ }
if (SelectionOffer != nullptr)
{
diff --git a/Source/Engine/Scripting/Scripting.cs b/Source/Engine/Scripting/Scripting.cs
index 229e411f3..7d3aba4cf 100644
--- a/Source/Engine/Scripting/Scripting.cs
+++ b/Source/Engine/Scripting/Scripting.cs
@@ -300,6 +300,7 @@ namespace FlaxEngine
TextBoxBackgroundSelected = Color.FromBgra(0xFF3F3F46),
CollectionBackgroundColor = Color.FromBgra(0x14CCCCCC),
ProgressNormal = Color.FromBgra(0xFF0ad328),
+ ProgressError = new Color(0.79f, 0.146f, 0.0f, 1.0f),
Statusbar = new Style.StatusbarStyle
{
PlayMode = Color.FromBgra(0xFF2F9135),
diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp
index 71ede26c8..3c001f2b7 100644
--- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp
+++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp
@@ -323,7 +323,7 @@ void MaterialGenerator::ProcessGroupMaterial(Box* box, Node* node, Value& value)
// Peek the function output (function->Outputs maps the functions outputs to output nodes indices)
const int32 outputIndex = box->ID - 16;
- if (outputIndex < 0 || outputIndex >= function->Outputs.Count())
+ if (!function->Outputs.IsValidIndex(outputIndex) || !graph->Nodes.IsValidIndex(function->Outputs[outputIndex]))
{
OnError(node, box, TEXT("Invalid function output box."));
value = Value::Zero;
diff --git a/Source/Engine/Tools/ModelTool/SpatialSort.cpp b/Source/Engine/Tools/ModelTool/SpatialSort.cpp
index de13084dc..2cc64a7d6 100644
--- a/Source/Engine/Tools/ModelTool/SpatialSort.cpp
+++ b/Source/Engine/Tools/ModelTool/SpatialSort.cpp
@@ -164,7 +164,7 @@ void SpatialSort::FindPositions(const aiVector3D& pPosition,
// Add all positions inside the distance range within the given radius to the result aray
std::vector::const_iterator it = mPositions.begin() + index;
const ai_real pSquared = pRadius * pRadius;
- while (it->mDistance < maxDist)
+ while (it->mDistance <= maxDist)
{
if ((it->mPosition - pPosition).SquareLength() < pSquared)
poResults.push_back(it->mIndex);
diff --git a/Source/Engine/UI/GUI/Style.cs b/Source/Engine/UI/GUI/Style.cs
index 5424d9f29..ae9691bdd 100644
--- a/Source/Engine/UI/GUI/Style.cs
+++ b/Source/Engine/UI/GUI/Style.cs
@@ -170,6 +170,12 @@ namespace FlaxEngine.GUI
[EditorOrder(200)]
public Color ProgressNormal;
+ ///
+ /// The progress error color.
+ ///
+ [EditorOrder(201)]
+ public Color ProgressError;
+
///
/// The selection and drag drop highlights colors.
///
diff --git a/Source/ThirdParty/SDL/SDL3/SDL.h b/Source/ThirdParty/SDL/SDL3/SDL.h
index 489c5ad61..51848b51b 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL.h
@@ -20,7 +20,7 @@
*/
/**
- * Main include header for the SDL library, version 3.4.4
+ * Main include header for the SDL library, version 3.4.16
*
* It is almost always best to include just this one header instead of
* picking out individual headers included here. There are exceptions to
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_assert.h b/Source/ThirdParty/SDL/SDL3/SDL_assert.h
index fb6746235..e65836a0d 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_assert.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_assert.h
@@ -126,10 +126,13 @@ extern "C" {
*/
#define SDL_TriggerBreakpoint() TriggerABreakpointInAPlatformSpecificManner
-#elif defined(__MINGW32__) || (defined(_MSC_VER) && _MSC_VER >= 1310)
+#elif defined(_MSC_VER) && _MSC_VER >= 1310
/* Don't include intrin.h here because it contains C++ code */
extern void __cdecl __debugbreak(void);
#define SDL_TriggerBreakpoint() __debugbreak()
+#elif defined(__MINGW32__)
+ #include
+ #define SDL_TriggerBreakpoint() __debugbreak()
#elif defined(_MSC_VER) && defined(_M_IX86)
#define SDL_TriggerBreakpoint() { _asm { int 0x03 } }
#elif SDL_HAS_BUILTIN(__builtin_debugtrap)
@@ -229,11 +232,16 @@ extern "C" {
#define SDL_LINE __LINE__
/*
-sizeof (x) makes the compiler still parse the expression even without
-assertions enabled, so the code is always checked at compile time, but
+sizeof ((x) ? 1 : 0) makes the compiler still parse the expression even
+without assertions enabled, so the code is always checked at compile time, but
doesn't actually generate code for it, so there are no side effects or
-expensive checks at run time, just the constant size of what x WOULD be,
+expensive checks at run time, just the constant size of what int would be,
which presumably gets optimized out as unused.
+
+(The `? 1 : 0` is so this always dithers down to `sizeof (int)`...otherwise
+you'll have problems when bitfields are used as the condition, since the
+compiler will error out on the sizeof.)
+
This also solves the problem of...
int somevalue = blah();
@@ -265,7 +273,7 @@ disable assertions.
*/
#define SDL_NULL_WHILE_LOOP_CONDITION (0)
-#elif defined(_MSC_VER) /* Avoid /W4 warnings. */
+#elif defined(_MSC_VER) && !defined(__clang__) /* Avoid /W4 warnings. */
/* "while (0,0)" fools Microsoft's compiler's /W4 warning level into thinking
this condition isn't constant. And looks like an owl's face! */
#define SDL_NULL_WHILE_LOOP_CONDITION (0,0)
@@ -281,14 +289,14 @@ disable assertions.
*
* The code does nothing, but wraps `condition` in a sizeof operator, which
* generates no code and has no side effects, but avoid compiler warnings
- * about unused variables.
+ * about unused variables, and still checks syntax even when disabled.
*
* \param condition the condition to assert (but not actually run here).
*
* \since This macro is available since SDL 3.2.0.
*/
#define SDL_disabled_assert(condition) \
- do { (void) sizeof ((condition)); } while (SDL_NULL_WHILE_LOOP_CONDITION)
+ do { (void) sizeof ((condition) ? 1 : 0); } while (SDL_NULL_WHILE_LOOP_CONDITION)
/**
* Possible outcomes from a triggered assertion.
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_atomic.h b/Source/ThirdParty/SDL/SDL3/SDL_atomic.h
index 1ec0753da..40521c1db 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_atomic.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_atomic.h
@@ -156,6 +156,8 @@ extern SDL_DECLSPEC void SDLCALL SDL_UnlockSpinlock(SDL_SpinLock *lock);
*/
#define SDL_CompilerBarrier() DoCompilerSpecificReadWriteBarrier()
+#elif SDL_HAS_BUILTIN(__atomic_signal_fence) || (defined(__GNUC__) && (__GNUC__ >= 5))
+#define SDL_CompilerBarrier() __atomic_signal_fence(__ATOMIC_SEQ_CST)
#elif defined(_MSC_VER) && (_MSC_VER > 1200) && !defined(__clang__)
void _ReadWriteBarrier(void);
#pragma intrinsic(_ReadWriteBarrier)
@@ -167,8 +169,9 @@ void _ReadWriteBarrier(void);
extern __inline void SDL_CompilerBarrier(void);
#pragma aux SDL_CompilerBarrier = "" parm [] modify exact [];
#else
+/* We don't unlock here to avoid possible infinite recursion */
#define SDL_CompilerBarrier() \
-{ SDL_SpinLock _tmp = 0; SDL_LockSpinlock(&_tmp); SDL_UnlockSpinlock(&_tmp); }
+{ SDL_SpinLock _tmp = 0; SDL_LockSpinlock(&_tmp); }
#endif
/**
@@ -275,12 +278,19 @@ extern SDL_DECLSPEC void SDLCALL SDL_MemoryBarrierAcquireFunction(void);
*/
#define SDL_MemoryBarrierAcquire() SDL_MemoryBarrierAcquireFunction()
+#elif SDL_HAS_BUILTIN(__atomic_thread_fence) || (defined(__GNUC__) && (__GNUC__ >= 5))
+#define SDL_MemoryBarrierRelease() __atomic_thread_fence(__ATOMIC_RELEASE)
+#define SDL_MemoryBarrierAcquire() __atomic_thread_fence(__ATOMIC_ACQUIRE)
#elif defined(__GNUC__) && (defined(__powerpc__) || defined(__ppc__))
#define SDL_MemoryBarrierRelease() __asm__ __volatile__ ("lwsync" : : : "memory")
#define SDL_MemoryBarrierAcquire() __asm__ __volatile__ ("lwsync" : : : "memory")
#elif defined(__GNUC__) && defined(__aarch64__)
#define SDL_MemoryBarrierRelease() __asm__ __volatile__ ("dmb ish" : : : "memory")
-#define SDL_MemoryBarrierAcquire() __asm__ __volatile__ ("dmb ish" : : : "memory")
+#define SDL_MemoryBarrierAcquire() __asm__ __volatile__ ("dmb ishld" : : : "memory")
+#elif defined(_MSC_VER) && (defined(_M_ARM64) || defined(_M_ARM64EC))
+#include
+#define SDL_MemoryBarrierRelease() __dmb(_ARM64_BARRIER_ISH)
+#define SDL_MemoryBarrierAcquire() __dmb(_ARM64_BARRIER_ISHLD)
#elif defined(__GNUC__) && defined(__arm__)
#if 0 /* defined(SDL_PLATFORM_LINUX) || defined(SDL_PLATFORM_ANDROID) */
/* Information from:
@@ -345,7 +355,7 @@ typedef void (*SDL_KernelMemoryBarrierFunc)();
*/
#define SDL_CPUPauseInstruction() DoACPUPauseInACompilerAndArchitectureSpecificWay
-#elif (defined(__GNUC__) || defined(__clang__)) && (defined(__i386__) || defined(__x86_64__))
+#elif (defined(__GNUC__) || defined(__clang__)) && (defined(__i386__) || defined(__x86_64__)) && !defined(__arm64ec__)
#define SDL_CPUPauseInstruction() __asm__ __volatile__("pause\n") /* Some assemblers can't do REP NOP, so go with PAUSE. */
#elif (defined(__arm__) && defined(__ARM_ARCH) && __ARM_ARCH >= 7) || defined(__aarch64__)
#define SDL_CPUPauseInstruction() __asm__ __volatile__("yield" ::: "memory")
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_audio.h b/Source/ThirdParty/SDL/SDL3/SDL_audio.h
index c72fe04cc..1e1af26d7 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_audio.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_audio.h
@@ -434,7 +434,8 @@ typedef struct SDL_AudioSpec
* - It can handle incoming data in any variable size.
* - It can handle input/output format changes on the fly.
* - It can remap audio channels between inputs and outputs.
- * - You push data as you have it, and pull it when you need it
+ * - You push data as you have it, and pull it when you need it; the
+ * stream will buffer data as needed.
* - It can also function as a basic audio data queue even if you just have
* sound that needs to pass from one place to another.
* - You can hook callbacks up to them when more data is added or requested,
@@ -1054,8 +1055,15 @@ extern SDL_DECLSPEC SDL_AudioDeviceID SDLCALL SDL_GetAudioStreamDevice(SDL_Audio
/**
* Create a new audio stream.
*
- * \param src_spec the format details of the input audio.
- * \param dst_spec the format details of the output audio.
+ * Note that `src_spec` or `dst_spec` may be NULL, but any attempts to
+ * put or get data from an audio stream will fail until it has valid
+ * specs assigned to both ends of the stream. Specs can be assigned later
+ * through SDL_SetAudioStreamFormat(), or binding the stream to an audio
+ * device (which will set the format of only the input or output,
+ * depending on what kind of device the stream was bound to).
+ *
+ * \param src_spec the format details of the input audio. May be NULL.
+ * \param dst_spec the format details of the output audio. May be NULL.
* \returns a new audio stream on success or NULL on failure; call
* SDL_GetError() for more information.
*
@@ -1137,6 +1145,9 @@ extern SDL_DECLSPEC bool SDLCALL SDL_GetAudioStreamFormat(SDL_AudioStream *strea
* be ignored, but this will not report an error. The other side's format can
* be changed.
*
+ * `src_spec` and `dst_spec` may each be NULL; a NULL spec signals not to
+ * change the current format for that side of the stream.
+ *
* \param stream the stream the format is being changed.
* \param src_spec the new format of the audio input; if NULL, it is not
* changed.
@@ -1843,6 +1854,8 @@ extern SDL_DECLSPEC bool SDLCALL SDL_UnlockAudioStream(SDL_AudioStream *stream);
* appropriate, but the system goes on with the data currently available to it
* if this callback does nothing.
*
+ * Do not call SDL_DestroyAudioStream() on `stream` during this callback.
+ *
* \param stream the SDL audio stream associated with this callback.
* \param additional_amount the amount of data, in bytes, that is needed right
* now.
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_camera.h b/Source/ThirdParty/SDL/SDL3/SDL_camera.h
index 14ea647a7..220acd1bf 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_camera.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_camera.h
@@ -147,7 +147,7 @@ typedef enum SDL_CameraPermissionState
{
SDL_CAMERA_PERMISSION_STATE_DENIED = -1,
SDL_CAMERA_PERMISSION_STATE_PENDING,
- SDL_CAMERA_PERMISSION_STATE_APPROVED,
+ SDL_CAMERA_PERMISSION_STATE_APPROVED
} SDL_CameraPermissionState;
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_clipboard.h b/Source/ThirdParty/SDL/SDL3/SDL_clipboard.h
index 696825376..487f9746c 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_clipboard.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_clipboard.h
@@ -252,7 +252,7 @@ typedef void (SDLCALL *SDL_ClipboardCleanupCallback)(void *userdata);
* \sa SDL_GetClipboardData
* \sa SDL_HasClipboardData
*/
-extern SDL_DECLSPEC bool SDLCALL SDL_SetClipboardData(SDL_ClipboardDataCallback callback, SDL_ClipboardCleanupCallback cleanup, void *userdata, const char **mime_types, size_t num_mime_types);
+extern SDL_DECLSPEC bool SDLCALL SDL_SetClipboardData(SDL_ClipboardDataCallback callback, SDL_ClipboardCleanupCallback cleanup, void *userdata, const char *const *mime_types, size_t num_mime_types);
/**
* Clear the clipboard data.
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_events.h b/Source/ThirdParty/SDL/SDL3/SDL_events.h
index 398c67709..4fb626813 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_events.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_events.h
@@ -456,7 +456,7 @@ typedef struct SDL_MouseMotionEvent
Uint32 reserved;
Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */
SDL_WindowID windowID; /**< The window with mouse focus, if any */
- SDL_MouseID which; /**< The mouse instance id in relative mode, SDL_TOUCH_MOUSEID for touch events, or 0 */
+ SDL_MouseID which; /**< The mouse instance id in relative mode, SDL_TOUCH_MOUSEID for touch events, SDL_PEN_MOUSEID for pen events, or 0 */
SDL_MouseButtonFlags state; /**< The current button state */
float x; /**< X coordinate, relative to window */
float y; /**< Y coordinate, relative to window */
@@ -830,6 +830,7 @@ typedef struct SDL_PenProximityEvent
Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */
SDL_WindowID windowID; /**< The window with pen focus, if any */
SDL_PenID which; /**< The pen instance id */
+ SDL_PenInputFlags pen_state; /**< Complete pen input state at time of event (added in 3.4.16). */
} SDL_PenProximityEvent;
/**
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_gamepad.h b/Source/ThirdParty/SDL/SDL3/SDL_gamepad.h
index 113452535..9c88a770a 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_gamepad.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_gamepad.h
@@ -167,11 +167,11 @@ typedef enum SDL_GamepadButton
SDL_GAMEPAD_BUTTON_DPAD_DOWN,
SDL_GAMEPAD_BUTTON_DPAD_LEFT,
SDL_GAMEPAD_BUTTON_DPAD_RIGHT,
- SDL_GAMEPAD_BUTTON_MISC1, /**< Additional button (e.g. Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Amazon Luna microphone button, Google Stadia capture button) */
- SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1, /**< Upper or primary paddle, under your right hand (e.g. Xbox Elite paddle P1, DualSense Edge RB button, Right Joy-Con SR button) */
- SDL_GAMEPAD_BUTTON_LEFT_PADDLE1, /**< Upper or primary paddle, under your left hand (e.g. Xbox Elite paddle P3, DualSense Edge LB button, Left Joy-Con SL button) */
- SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2, /**< Lower or secondary paddle, under your right hand (e.g. Xbox Elite paddle P2, DualSense Edge right Fn button, Right Joy-Con SL button) */
- SDL_GAMEPAD_BUTTON_LEFT_PADDLE2, /**< Lower or secondary paddle, under your left hand (e.g. Xbox Elite paddle P4, DualSense Edge left Fn button, Left Joy-Con SR button) */
+ SDL_GAMEPAD_BUTTON_MISC1, /**< Additional button (e.g. Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Steam Controller QAM button, Amazon Luna microphone button, Google Stadia capture button) */
+ SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1, /**< Upper or primary paddle, under your right hand (e.g. Xbox Elite paddle P1, DualSense Edge RB button, Right Joy-Con SR button, Steam Controller R4 button) */
+ SDL_GAMEPAD_BUTTON_LEFT_PADDLE1, /**< Upper or primary paddle, under your left hand (e.g. Xbox Elite paddle P3, DualSense Edge LB button, Left Joy-Con SL button, Steam Controller L4 button) */
+ SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2, /**< Lower or secondary paddle, under your right hand (e.g. Xbox Elite paddle P2, DualSense Edge right Fn button, Right Joy-Con SL button, Steam Controller R5 button) */
+ SDL_GAMEPAD_BUTTON_LEFT_PADDLE2, /**< Lower or secondary paddle, under your left hand (e.g. Xbox Elite paddle P4, DualSense Edge left Fn button, Left Joy-Con SR button, Steam Controller L5 button) */
SDL_GAMEPAD_BUTTON_TOUCHPAD, /**< PS4/PS5 touchpad button */
SDL_GAMEPAD_BUTTON_MISC2, /**< Additional button */
SDL_GAMEPAD_BUTTON_MISC3, /**< Additional button (e.g. Nintendo GameCube left trigger click) */
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_gpu.h b/Source/ThirdParty/SDL/SDL3/SDL_gpu.h
index dfe5994aa..2236cb79a 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_gpu.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_gpu.h
@@ -884,8 +884,8 @@ typedef enum SDL_GPUTextureFormat
/**
* Specifies how a texture is intended to be used by the client.
*
- * A texture must have at least one usage flag. Note that some usage flag
- * combinations are invalid.
+ * A texture must have at least one usage flag.
+ * Note that combining SAMPLER with STORAGE_READ flags is invalid.
*
* With regards to compute storage usage, READ | WRITE means that you can have
* shader A that only writes into the texture and shader B that only reads
@@ -967,8 +967,10 @@ typedef enum SDL_GPUCubeMapFace
/**
* Specifies how a buffer is intended to be used by the client.
*
- * A buffer must have at least one usage flag. Note that some usage flag
- * combinations are invalid.
+ * A buffer must have at least one usage flag.
+ *
+ * If a buffer has multiple read usages, this may lead to a performance penalty
+ * due to more conservative memory barriers, but it also may not necessarily affect the performance.
*
* Unlike textures, READ | WRITE can be used for simultaneous read-write
* usage. The same data synchronization concerns as textures apply.
@@ -1409,6 +1411,7 @@ typedef struct SDL_GPUViewport
*
* \sa SDL_UploadToGPUTexture
* \sa SDL_DownloadFromGPUTexture
+ * \sa SDL_GPUTransferBuffer
*/
typedef struct SDL_GPUTextureTransferInfo
{
@@ -1427,6 +1430,7 @@ typedef struct SDL_GPUTextureTransferInfo
*
* \sa SDL_UploadToGPUBuffer
* \sa SDL_DownloadFromGPUBuffer
+ * \sa SDL_GPUTransferBuffer
*/
typedef struct SDL_GPUTransferBufferLocation
{
@@ -1442,6 +1446,7 @@ typedef struct SDL_GPUTransferBufferLocation
* \since This struct is available since SDL 3.2.0.
*
* \sa SDL_CopyGPUTextureToTexture
+ * \sa SDL_GPUTexture
*/
typedef struct SDL_GPUTextureLocation
{
@@ -1463,6 +1468,7 @@ typedef struct SDL_GPUTextureLocation
* \sa SDL_UploadToGPUTexture
* \sa SDL_DownloadFromGPUTexture
* \sa SDL_CreateGPUTexture
+ * \sa SDL_GPUTexture
*/
typedef struct SDL_GPUTextureRegion
{
@@ -1483,6 +1489,7 @@ typedef struct SDL_GPUTextureRegion
* \since This struct is available since SDL 3.2.0.
*
* \sa SDL_BlitGPUTexture
+ * \sa SDL_GPUTexture
*/
typedef struct SDL_GPUBlitRegion
{
@@ -1707,6 +1714,7 @@ typedef struct SDL_GPUStencilOpState
*
* \since This struct is available since SDL 3.2.0.
*
+ * \sa SDL_SetGPUBlendConstants
* \sa SDL_GPUColorTargetDescription
* \sa SDL_GPUBlendFactor
* \sa SDL_GPUBlendOp
@@ -2098,6 +2106,11 @@ typedef struct SDL_GPUDepthStencilTargetInfo
* \since This struct is available since SDL 3.2.0.
*
* \sa SDL_BlitGPUTexture
+ * \sa SDL_GPUBlitRegion
+ * \sa SDL_GPULoadOp
+ * \sa SDL_FColor
+ * \sa SDL_FlipMode
+ * \sa SDL_GPUFilter
*/
typedef struct SDL_GPUBlitInfo {
SDL_GPUBlitRegion source; /**< The source region for the blit. */
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_hints.h b/Source/ThirdParty/SDL/SDL3/SDL_hints.h
index 74f5bca07..a38abf02d 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_hints.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_hints.h
@@ -117,6 +117,33 @@ extern "C" {
*/
#define SDL_HINT_ANDROID_LOW_LATENCY_AUDIO "SDL_ANDROID_LOW_LATENCY_AUDIO"
+/**
+ * A variable to control Android's AAudio input preset.
+ *
+ * This hint only applies to SDL's "aaudio" backend on Android 9+ devices.
+ *
+ * Some devices choose the wrong microphone by default (between the one meant
+ * to be spoken in when the phone is held to the user's ear for a phone call,
+ * or an external microphone that's meant to be used when recording video),
+ * or have DSP effects applied to the recorded audio, and changing the input
+ * preset can help control this.
+ *
+ * This can be any number that maps to an `AAUDIO_INPUT_PRESET_*` enum from
+ * the Android NDK headers. The most reasonable choices are 5 ("camcorder",
+ * for external microphones) and 7 ("voice communication", for speaking
+ * directly into the device like a mobile phone). 6 ("voice recognition")
+ * might also be a useful choice.
+ *
+ * If unset (the default), SDL will not specify an input preset at all, which
+ * lets the system choose. This is usually the correct thing to do unless
+ * your app is having problems.
+ *
+ * This hint should be set before a recording audio device is opened.
+ *
+ * \since This hint is available since SDL 3.4.16.
+ */
+#define SDL_HINT_ANDROID_AAUDIO_INPUT_PRESET "SDL_ANDROID_AAUDIO_INPUT_PRESET"
+
/**
* A variable to control whether we trap the Android back button to handle it
* manually.
@@ -809,6 +836,25 @@ extern "C" {
*/
#define SDL_HINT_ENABLE_SCREEN_KEYBOARD "SDL_ENABLE_SCREEN_KEYBOARD"
+/**
+ * A variable that controls whether the Steam on-screen keyboard should be
+ * shown when text input is active.
+ *
+ * Steam will set this hint via environment variable for games launched in Big
+ * Picture mode. To override this you should call SDL_SetHintWithPriority()
+ * with priority `SDL_HINT_OVERRIDE`.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Do not show the Steam on-screen keyboard.
+ * - "1": Show the Steam on-screen keyboard.
+ *
+ * This hint should be set before SDL is initialized.
+ *
+ * \since This hint is available since SDL 3.4.12.
+ */
+#define SDL_HINT_ENABLE_STEAM_SCREEN_KEYBOARD "SDL_ENABLE_STEAM_SCREEN_KEYBOARD"
+
/**
* A variable containing a list of evdev devices to use if udev is not
* available.
@@ -2150,7 +2196,8 @@ extern "C" {
* - "0": Assume this is a generic controller.
* - "1": Reset the controller to get metadata.
*
- * By default the controller is not reset.
+ * By default the controller is reset. This is so we can properly detect
+ * the controller type.
*
* This hint should be set before initializing joysticks and gamepads.
*
@@ -2772,7 +2819,7 @@ extern "C" {
* (default)
* - "1": Cursors will automatically match the display content scale (e.g. a
* 2x sized cursor will be used when the window is on a monitor with 200%
- * scale). This is currently implemented on Windows and Wayland.
+ * scale). This is currently implemented on Windows.
*
* This hint needs to be set before creating cursors.
*
@@ -4044,6 +4091,31 @@ extern "C" {
*/
#define SDL_HINT_VIDEO_WIN_D3DCOMPILER "SDL_VIDEO_WIN_D3DCOMPILER"
+/**
+ * A variable controlling whether the X Synchronization Extension is enabled.
+ *
+ * If set, this can result in smoother window resizing when rendering using
+ * OpenGL, however, there are some conditions:
+ *
+ * - It is only activated on windows created with the `SDL_WINDOW_OPENGL` flag
+ * (windows using an SDL OpenGL renderer have this automatically set).
+ * - When activated, presentation must be done with `SDL_GL_SwapWindow()`
+ * (`SDL_RenderPresent()` calls this internally for OpenGL renderers as well).
+ *
+ * Enabling this and presenting via an external mechanism will result in sync
+ * requests not being acked, and hangs and other odd window behavior may result.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": The X Synchronization Extension is disabled. (default)
+ * - "1": The X Synchronization Extension is enabled.
+ *
+ * This hint should be set before creating a window.
+ *
+ * \since This hint is available since SDL 3.4.10.
+ */
+#define SDL_HINT_VIDEO_X11_ENABLE_XSYNC_EXT "SDL_VIDEO_X11_ENABLE_XSYNC_EXT"
+
/**
* A variable controlling whether SDL should call XSelectInput() to enable
* input events on X11 windows wrapped by SDL windows.
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_opengl_glext.h b/Source/ThirdParty/SDL/SDL3/SDL_opengl_glext.h
index fa0f6c2a5..ff6ad12ce 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_opengl_glext.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_opengl_glext.h
@@ -9807,15 +9807,15 @@ typedef void (APIENTRYP PFNGLUPLOADGPUMASKNVXPROC) (GLbitfield mask);
typedef void (APIENTRYP PFNGLMULTICASTVIEWPORTARRAYVNVXPROC) (GLuint gpu, GLuint first, GLsizei count, const GLfloat *v);
typedef void (APIENTRYP PFNGLMULTICASTVIEWPORTPOSITIONWSCALENVXPROC) (GLuint gpu, GLuint index, GLfloat xcoeff, GLfloat ycoeff);
typedef void (APIENTRYP PFNGLMULTICASTSCISSORARRAYVNVXPROC) (GLuint gpu, GLuint first, GLsizei count, const GLint *v);
-typedef GLuint (APIENTRYP PFNGLASYNCCOPYBUFFERSUBDATANVXPROC) (GLsizei waitSemaphoreCount, const GLuint *waitSemaphoreArray, const GLuint64 *fenceValueArray, GLuint readGPU, GLbitfield writeGPUMask, GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size, GLsizei signalSemaphoreCount, const GLuint *signalSemaphoreArray, const GLuint64 *signalValueArray);
-typedef GLuint (APIENTRYP PFNGLASYNCCOPYIMAGESUBDATANVXPROC) (GLsizei waitSemaphoreCount, const GLuint *waitSemaphoreArray, const GLuint64 *waitValueArray, GLuint srcGPU, GLbitfield dstGPUMask, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth, GLsizei signalSemaphoreCount, const GLuint *signalSemaphoreArray, const GLuint64 *signalValueArray);
+typedef GLuint (APIENTRYP PFNGLASYNCCOPYBUFFERSUBDATANVXPROC) (GLsizei waitSemaphoreCount, const GLuint *waitSemaphoreArray, const GLuint64 *fenceValueArray, GLuint readGpu, GLbitfield writeGpuMask, GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size, GLsizei signalSemaphoreCount, const GLuint *signalSemaphoreArray, const GLuint64 *signalValueArray);
+typedef GLuint (APIENTRYP PFNGLASYNCCOPYIMAGESUBDATANVXPROC) (GLsizei waitSemaphoreCount, const GLuint *waitSemaphoreArray, const GLuint64 *waitValueArray, GLuint srcGpu, GLbitfield dstGpuMask, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth, GLsizei signalSemaphoreCount, const GLuint *signalSemaphoreArray, const GLuint64 *signalValueArray);
#ifdef GL_GLEXT_PROTOTYPES
-GLAPI void APIENTRY glUploadGPUMaskNVX (GLbitfield mask);
+GLAPI void APIENTRY glUploadGpuMaskNVX (GLbitfield mask);
GLAPI void APIENTRY glMulticastViewportArrayvNVX (GLuint gpu, GLuint first, GLsizei count, const GLfloat *v);
GLAPI void APIENTRY glMulticastViewportPositionWScaleNVX (GLuint gpu, GLuint index, GLfloat xcoeff, GLfloat ycoeff);
GLAPI void APIENTRY glMulticastScissorArrayvNVX (GLuint gpu, GLuint first, GLsizei count, const GLint *v);
-GLAPI GLuint APIENTRY glAsyncCopyBufferSubDataNVX (GLsizei waitSemaphoreCount, const GLuint *waitSemaphoreArray, const GLuint64 *fenceValueArray, GLuint readGPU, GLbitfield writeGPUMask, GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size, GLsizei signalSemaphoreCount, const GLuint *signalSemaphoreArray, const GLuint64 *signalValueArray);
-GLAPI GLuint APIENTRY glAsyncCopyImageSubDataNVX (GLsizei waitSemaphoreCount, const GLuint *waitSemaphoreArray, const GLuint64 *waitValueArray, GLuint srcGPU, GLbitfield dstGPUMask, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth, GLsizei signalSemaphoreCount, const GLuint *signalSemaphoreArray, const GLuint64 *signalValueArray);
+GLAPI GLuint APIENTRY glAsyncCopyBufferSubDataNVX (GLsizei waitSemaphoreCount, const GLuint *waitSemaphoreArray, const GLuint64 *fenceValueArray, GLuint readGpu, GLbitfield writeGpuMask, GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size, GLsizei signalSemaphoreCount, const GLuint *signalSemaphoreArray, const GLuint64 *signalValueArray);
+GLAPI GLuint APIENTRY glAsyncCopyImageSubDataNVX (GLsizei waitSemaphoreCount, const GLuint *waitSemaphoreArray, const GLuint64 *waitValueArray, GLuint srcGpu, GLbitfield dstGpuMask, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth, GLsizei signalSemaphoreCount, const GLuint *signalSemaphoreArray, const GLuint64 *signalValueArray);
#endif
#endif /* GL_NVX_gpu_multicast2 */
@@ -9824,11 +9824,11 @@ GLAPI GLuint APIENTRY glAsyncCopyImageSubDataNVX (GLsizei waitSemaphoreCount, co
#define GL_LGPU_SEPARATE_STORAGE_BIT_NVX 0x0800
#define GL_MAX_LGPU_GPUS_NVX 0x92BA
typedef void (APIENTRYP PFNGLLGPUNAMEDBUFFERSUBDATANVXPROC) (GLbitfield gpuMask, GLuint buffer, GLintptr offset, GLsizeiptr size, const void *data);
-typedef void (APIENTRYP PFNGLLGPUCOPYIMAGESUBDATANVXPROC) (GLuint sourceGPU, GLbitfield destinationGPUMask, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srxY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei width, GLsizei height, GLsizei depth);
+typedef void (APIENTRYP PFNGLLGPUCOPYIMAGESUBDATANVXPROC) (GLuint sourceGpu, GLbitfield destinationGpuMask, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srxY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei width, GLsizei height, GLsizei depth);
typedef void (APIENTRYP PFNGLLGPUINTERLOCKNVXPROC) (void);
#ifdef GL_GLEXT_PROTOTYPES
GLAPI void APIENTRY glLGPUNamedBufferSubDataNVX (GLbitfield gpuMask, GLuint buffer, GLintptr offset, GLsizeiptr size, const void *data);
-GLAPI void APIENTRY glLGPUCopyImageSubDataNVX (GLuint sourceGPU, GLbitfield destinationGPUMask, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srxY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei width, GLsizei height, GLsizei depth);
+GLAPI void APIENTRY glLGPUCopyImageSubDataNVX (GLuint sourceGpu, GLbitfield destinationGpuMask, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srxY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei width, GLsizei height, GLsizei depth);
GLAPI void APIENTRY glLGPUInterlockNVX (void);
#endif
#endif /* GL_NVX_linked_gpu_multicast */
@@ -9836,13 +9836,13 @@ GLAPI void APIENTRY glLGPUInterlockNVX (void);
#ifndef GL_NVX_progress_fence
#define GL_NVX_progress_fence 1
typedef GLuint (APIENTRYP PFNGLCREATEPROGRESSFENCENVXPROC) (void);
-typedef void (APIENTRYP PFNGLSIGNALSEMAPHOREUI64NVXPROC) (GLuint signalGPU, GLsizei fenceObjectCount, const GLuint *semaphoreArray, const GLuint64 *fenceValueArray);
-typedef void (APIENTRYP PFNGLWAITSEMAPHOREUI64NVXPROC) (GLuint waitGPU, GLsizei fenceObjectCount, const GLuint *semaphoreArray, const GLuint64 *fenceValueArray);
+typedef void (APIENTRYP PFNGLSIGNALSEMAPHOREUI64NVXPROC) (GLuint signalGpu, GLsizei fenceObjectCount, const GLuint *semaphoreArray, const GLuint64 *fenceValueArray);
+typedef void (APIENTRYP PFNGLWAITSEMAPHOREUI64NVXPROC) (GLuint waitGpu, GLsizei fenceObjectCount, const GLuint *semaphoreArray, const GLuint64 *fenceValueArray);
typedef void (APIENTRYP PFNGLCLIENTWAITSEMAPHOREUI64NVXPROC) (GLsizei fenceObjectCount, const GLuint *semaphoreArray, const GLuint64 *fenceValueArray);
#ifdef GL_GLEXT_PROTOTYPES
GLAPI GLuint APIENTRY glCreateProgressFenceNVX (void);
-GLAPI void APIENTRY glSignalSemaphoreui64NVX (GLuint signalGPU, GLsizei fenceObjectCount, const GLuint *semaphoreArray, const GLuint64 *fenceValueArray);
-GLAPI void APIENTRY glWaitSemaphoreui64NVX (GLuint waitGPU, GLsizei fenceObjectCount, const GLuint *semaphoreArray, const GLuint64 *fenceValueArray);
+GLAPI void APIENTRY glSignalSemaphoreui64NVX (GLuint signalGpu, GLsizei fenceObjectCount, const GLuint *semaphoreArray, const GLuint64 *fenceValueArray);
+GLAPI void APIENTRY glWaitSemaphoreui64NVX (GLuint waitGpu, GLsizei fenceObjectCount, const GLuint *semaphoreArray, const GLuint64 *fenceValueArray);
GLAPI void APIENTRY glClientWaitSemaphoreui64NVX (GLsizei fenceObjectCount, const GLuint *semaphoreArray, const GLuint64 *fenceValueArray);
#endif
#endif /* GL_NVX_progress_fence */
@@ -10433,25 +10433,25 @@ GLAPI void APIENTRY glFramebufferTextureFaceEXT (GLenum target, GLenum attachmen
#define GL_MULTICAST_PROGRAMMABLE_SAMPLE_LOCATION_NV 0x9549
typedef void (APIENTRYP PFNGLRENDERGPUMASKNVPROC) (GLbitfield mask);
typedef void (APIENTRYP PFNGLMULTICASTBUFFERSUBDATANVPROC) (GLbitfield gpuMask, GLuint buffer, GLintptr offset, GLsizeiptr size, const void *data);
-typedef void (APIENTRYP PFNGLMULTICASTCOPYBUFFERSUBDATANVPROC) (GLuint readGPU, GLbitfield writeGPUMask, GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size);
-typedef void (APIENTRYP PFNGLMULTICASTCOPYIMAGESUBDATANVPROC) (GLuint srcGPU, GLbitfield dstGPUMask, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
-typedef void (APIENTRYP PFNGLMULTICASTBLITFRAMEBUFFERNVPROC) (GLuint srcGPU, GLuint dstGPU, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
+typedef void (APIENTRYP PFNGLMULTICASTCOPYBUFFERSUBDATANVPROC) (GLuint readGpu, GLbitfield writeGpuMask, GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size);
+typedef void (APIENTRYP PFNGLMULTICASTCOPYIMAGESUBDATANVPROC) (GLuint srcGpu, GLbitfield dstGpuMask, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
+typedef void (APIENTRYP PFNGLMULTICASTBLITFRAMEBUFFERNVPROC) (GLuint srcGpu, GLuint dstGpu, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
typedef void (APIENTRYP PFNGLMULTICASTFRAMEBUFFERSAMPLELOCATIONSFVNVPROC) (GLuint gpu, GLuint framebuffer, GLuint start, GLsizei count, const GLfloat *v);
typedef void (APIENTRYP PFNGLMULTICASTBARRIERNVPROC) (void);
-typedef void (APIENTRYP PFNGLMULTICASTWAITSYNCNVPROC) (GLuint signalGPU, GLbitfield waitGPUMask);
+typedef void (APIENTRYP PFNGLMULTICASTWAITSYNCNVPROC) (GLuint signalGpu, GLbitfield waitGpuMask);
typedef void (APIENTRYP PFNGLMULTICASTGETQUERYOBJECTIVNVPROC) (GLuint gpu, GLuint id, GLenum pname, GLint *params);
typedef void (APIENTRYP PFNGLMULTICASTGETQUERYOBJECTUIVNVPROC) (GLuint gpu, GLuint id, GLenum pname, GLuint *params);
typedef void (APIENTRYP PFNGLMULTICASTGETQUERYOBJECTI64VNVPROC) (GLuint gpu, GLuint id, GLenum pname, GLint64 *params);
typedef void (APIENTRYP PFNGLMULTICASTGETQUERYOBJECTUI64VNVPROC) (GLuint gpu, GLuint id, GLenum pname, GLuint64 *params);
#ifdef GL_GLEXT_PROTOTYPES
-GLAPI void APIENTRY glRenderGPUMaskNV (GLbitfield mask);
+GLAPI void APIENTRY glRenderGpuMaskNV (GLbitfield mask);
GLAPI void APIENTRY glMulticastBufferSubDataNV (GLbitfield gpuMask, GLuint buffer, GLintptr offset, GLsizeiptr size, const void *data);
-GLAPI void APIENTRY glMulticastCopyBufferSubDataNV (GLuint readGPU, GLbitfield writeGPUMask, GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size);
-GLAPI void APIENTRY glMulticastCopyImageSubDataNV (GLuint srcGPU, GLbitfield dstGPUMask, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
-GLAPI void APIENTRY glMulticastBlitFramebufferNV (GLuint srcGPU, GLuint dstGPU, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
+GLAPI void APIENTRY glMulticastCopyBufferSubDataNV (GLuint readGpu, GLbitfield writeGpuMask, GLuint readBuffer, GLuint writeBuffer, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size);
+GLAPI void APIENTRY glMulticastCopyImageSubDataNV (GLuint srcGpu, GLbitfield dstGpuMask, GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
+GLAPI void APIENTRY glMulticastBlitFramebufferNV (GLuint srcGpu, GLuint dstGpu, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
GLAPI void APIENTRY glMulticastFramebufferSampleLocationsfvNV (GLuint gpu, GLuint framebuffer, GLuint start, GLsizei count, const GLfloat *v);
GLAPI void APIENTRY glMulticastBarrierNV (void);
-GLAPI void APIENTRY glMulticastWaitSyncNV (GLuint signalGPU, GLbitfield waitGPUMask);
+GLAPI void APIENTRY glMulticastWaitSyncNV (GLuint signalGpu, GLbitfield waitGpuMask);
GLAPI void APIENTRY glMulticastGetQueryObjectivNV (GLuint gpu, GLuint id, GLenum pname, GLint *params);
GLAPI void APIENTRY glMulticastGetQueryObjectuivNV (GLuint gpu, GLuint id, GLenum pname, GLuint *params);
GLAPI void APIENTRY glMulticastGetQueryObjecti64vNV (GLuint gpu, GLuint id, GLenum pname, GLint64 *params);
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_openxr.h b/Source/ThirdParty/SDL/SDL3/SDL_openxr.h
deleted file mode 100644
index ceb9790c5..000000000
--- a/Source/ThirdParty/SDL/SDL3/SDL_openxr.h
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- Simple DirectMedia Layer
- Copyright (C) 1997-2026 Sam Lantinga
-
- This software is provided 'as-is', without any express or implied
- warranty. In no event will the authors be held liable for any damages
- arising from the use of this software.
-
- Permission is granted to anyone to use this software for any purpose,
- including commercial applications, and to alter it and redistribute it
- freely, subject to the following restrictions:
-
- 1. The origin of this software must not be misrepresented; you must not
- claim that you wrote the original software. If you use this software
- in a product, an acknowledgment in the product documentation would be
- appreciated but is not required.
- 2. Altered source versions must be plainly marked as such, and must not be
- misrepresented as being the original software.
- 3. This notice may not be removed or altered from any source distribution.
-*/
-
-/**
- * # CategoryOpenXR
- *
- * Functions for creating OpenXR handles for SDL_gpu contexts.
- *
- * For the most part, OpenXR operates independent of SDL, but
- * the graphics initialization depends on direct support from SDL_gpu.
- *
- */
-
-#ifndef SDL_openxr_h_
-#define SDL_openxr_h_
-
-#include
-#include
-
-#include
-/* Set up for C function definitions, even when using C++ */
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-#if defined(OPENXR_H_)
-#define NO_SDL_OPENXR_TYPEDEFS 1
-#endif /* OPENXR_H_ */
-
-#if !defined(NO_SDL_OPENXR_TYPEDEFS)
-#define XR_NULL_HANDLE 0
-
-#if !defined(XR_DEFINE_HANDLE)
- #define XR_DEFINE_HANDLE(object) typedef Uint64 object;
-#endif /* XR_DEFINE_HANDLE */
-
-typedef enum XrStructureType {
- XR_TYPE_SESSION_CREATE_INFO = 8,
- XR_TYPE_SWAPCHAIN_CREATE_INFO = 9,
-} XrStructureType;
-
-XR_DEFINE_HANDLE(XrInstance)
-XR_DEFINE_HANDLE(XrSystemId)
-XR_DEFINE_HANDLE(XrSession)
-XR_DEFINE_HANDLE(XrSwapchain)
-
-typedef struct {
- XrStructureType type;
- const void* next;
-} XrSessionCreateInfo;
-typedef struct {
- XrStructureType type;
- const void* next;
-} XrSwapchainCreateInfo;
-
-typedef enum XrResult {
- XR_ERROR_FUNCTION_UNSUPPORTED = -7,
- XR_ERROR_HANDLE_INVALID = -12,
-} XrResult;
-
-#define PFN_xrGetInstanceProcAddr SDL_FunctionPointer
-#endif /* NO_SDL_OPENXR_TYPEDEFS */
-
-/**
- * Creates an OpenXR session.
- *
- * The OpenXR system ID is pulled from the passed GPU context.
- *
- * \param device a GPU context.
- * \param createinfo the create info for the OpenXR session, sans the system
- * ID.
- * \param session a pointer filled in with an OpenXR session created for the
- * given device.
- * \returns the result of the call.
- *
- * \since This function is available since SDL 3.6.0.
- *
- * \sa SDL_CreateGPUDeviceWithProperties
- */
-extern SDL_DECLSPEC XrResult SDLCALL SDL_CreateGPUXRSession(SDL_GPUDevice *device, const XrSessionCreateInfo *createinfo, XrSession *session);
-
-/**
- * Queries the GPU device for supported XR swapchain image formats.
- *
- * The returned pointer should be allocated with SDL_malloc() and will be
- * passed to SDL_free().
- *
- * \param device a GPU context.
- * \param session an OpenXR session created for the given device.
- * \param num_formats a pointer filled with the number of supported XR
- * swapchain formats.
- * \returns a 0 terminated array of supported formats or NULL on failure; call
- * SDL_GetError() for more information. This should be freed with
- * SDL_free() when it is no longer needed.
- *
- * \since This function is available since SDL 3.6.0.
- *
- * \sa SDL_CreateGPUXRSwapchain
- */
-extern SDL_DECLSPEC SDL_GPUTextureFormat * SDLCALL SDL_GetGPUXRSwapchainFormats(SDL_GPUDevice *device, XrSession session, int *num_formats);
-
-/**
- * Creates an OpenXR swapchain.
- *
- * The array returned via `textures` is sized according to
- * `xrEnumerateSwapchainImages`, and thus should only be accessed via index
- * values returned from `xrAcquireSwapchainImage`.
- *
- * Applications are still allowed to call `xrEnumerateSwapchainImages` on the
- * returned XrSwapchain if they need to get the exact size of the array.
- *
- * \param device a GPU context.
- * \param session an OpenXR session created for the given device.
- * \param createinfo the create info for the OpenXR swapchain, sans the
- * format.
- * \param format a supported format for the OpenXR swapchain.
- * \param swapchain a pointer filled in with the created OpenXR swapchain.
- * \param textures a pointer filled in with the array of created swapchain
- * images.
- * \returns the result of the call.
- *
- * \since This function is available since SDL 3.6.0.
- *
- * \sa SDL_CreateGPUDeviceWithProperties
- * \sa SDL_CreateGPUXRSession
- * \sa SDL_GetGPUXRSwapchainFormats
- * \sa SDL_DestroyGPUXRSwapchain
- */
-extern SDL_DECLSPEC XrResult SDLCALL SDL_CreateGPUXRSwapchain(
- SDL_GPUDevice *device,
- XrSession session,
- const XrSwapchainCreateInfo *createinfo,
- SDL_GPUTextureFormat format,
- XrSwapchain *swapchain,
- SDL_GPUTexture ***textures);
-
-/**
- * Destroys and OpenXR swapchain previously returned by
- * SDL_CreateGPUXRSwapchain.
- *
- * \param device a GPU context.
- * \param swapchain a swapchain previously returned by
- * SDL_CreateGPUXRSwapchain.
- * \param swapchainImages an array of swapchain images returned by the same
- * call to SDL_CreateGPUXRSwapchain.
- * \returns the result of the call.
- *
- * \since This function is available since SDL 3.6.0.
- *
- * \sa SDL_CreateGPUDeviceWithProperties
- * \sa SDL_CreateGPUXRSession
- * \sa SDL_CreateGPUXRSwapchain
- */
-extern SDL_DECLSPEC XrResult SDLCALL SDL_DestroyGPUXRSwapchain(SDL_GPUDevice *device, XrSwapchain swapchain, SDL_GPUTexture **swapchainImages);
-
-/**
- * Dynamically load the OpenXR loader.
- *
- * This can be called at any time.
- *
- * SDL keeps a reference count of the OpenXR loader, calling this function
- * multiple times will increment that count, rather than loading the library
- * multiple times.
- *
- * If not called, this will be implicitly called when creating a GPU device
- * with OpenXR.
- *
- * This function will use the platform default OpenXR loader name, unless the
- * `SDL_HINT_OPENXR_LIBRARY` hint is set.
- *
- * \returns true on success or false on failure; call SDL_GetError() for more
- * information.
- *
- * \threadsafety This function is not thread safe.
- *
- * \since This function is available since SDL 3.6.0.
- *
- * \sa SDL_HINT_OPENXR_LIBRARY
- */
-extern SDL_DECLSPEC bool SDLCALL SDL_OpenXR_LoadLibrary(void);
-
-/**
- * Unload the OpenXR loader previously loaded by SDL_OpenXR_LoadLibrary.
- *
- * SDL keeps a reference count of the OpenXR loader, calling this function
- * will decrement that count. Once the reference count reaches zero, the
- * library is unloaded.
- *
- * \threadsafety This function is not thread safe.
- *
- * \since This function is available since SDL 3.6.0.
- */
-extern SDL_DECLSPEC void SDLCALL SDL_OpenXR_UnloadLibrary(void);
-
-/**
- * Get the address of the `xrGetInstanceProcAddr` function.
- *
- * This should be called after either calling SDL_OpenXR_LoadLibrary() or
- * creating an OpenXR SDL_GPUDevice.
- *
- * The actual type of the returned function pointer is
- * PFN_xrGetInstanceProcAddr, but that isn't always available. You should
- * include the OpenXR headers before this header, or cast the return value of
- * this function to the correct type.
- *
- * \returns the function pointer for `xrGetInstanceProcAddr` or NULL on
- * failure; call SDL_GetError() for more information.
- *
- * \since This function is available since SDL 3.6.0.
- */
-extern SDL_DECLSPEC PFN_xrGetInstanceProcAddr SDLCALL SDL_OpenXR_GetXrGetInstanceProcAddr(void);
-
-/* Ends C function definitions when using C++ */
-#ifdef __cplusplus
-}
-#endif
-#include
-
-#endif /* SDL_openxr_h_ */
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_render.h b/Source/ThirdParty/SDL/SDL3/SDL_render.h
index 986130ce4..a4ea58025 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_render.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_render.h
@@ -402,7 +402,7 @@ extern SDL_DECLSPEC SDL_GPUDevice * SDLCALL SDL_GetGPURendererDevice(SDL_Rendere
/**
* Create a 2D software rendering context for a surface.
*
- * Two other API which can be used to create SDL_Renderer:
+ * Two other APIs which can be used to create SDL_Renderer:
* SDL_CreateRenderer() and SDL_CreateWindowAndRenderer(). These can _also_
* create a software renderer, but they are intended to be used with an
* SDL_Window as the final destination and not an SDL_Surface.
@@ -1221,6 +1221,8 @@ extern SDL_DECLSPEC bool SDLCALL SDL_GetTextureAlphaModFloat(SDL_Texture *textur
/**
* Set the blend mode for a texture, used by SDL_RenderTexture().
*
+ * This blend mode is used for any drawing that involves this texture.
+ *
* If the blend mode is not supported, the closest supported mode is chosen
* and this function returns false.
*
@@ -1234,6 +1236,7 @@ extern SDL_DECLSPEC bool SDLCALL SDL_GetTextureAlphaModFloat(SDL_Texture *textur
* \since This function is available since SDL 3.2.0.
*
* \sa SDL_GetTextureBlendMode
+ * \sa SDL_SetRenderDrawBlendMode
*/
extern SDL_DECLSPEC bool SDLCALL SDL_SetTextureBlendMode(SDL_Texture *texture, SDL_BlendMode blendMode);
@@ -2054,7 +2057,9 @@ extern SDL_DECLSPEC bool SDLCALL SDL_SetRenderColorScale(SDL_Renderer *renderer,
extern SDL_DECLSPEC bool SDLCALL SDL_GetRenderColorScale(SDL_Renderer *renderer, float *scale);
/**
- * Set the blend mode used for drawing operations (Fill and Line).
+ * Set the blend mode used for drawing operations.
+ *
+ * This blend mode is used for any drawing that doesn't involve textures.
*
* If the blend mode is not supported, the closest supported mode is chosen.
*
@@ -2068,6 +2073,7 @@ extern SDL_DECLSPEC bool SDLCALL SDL_GetRenderColorScale(SDL_Renderer *renderer,
* \since This function is available since SDL 3.2.0.
*
* \sa SDL_GetRenderDrawBlendMode
+ * \sa SDL_SetTextureBlendMode
*/
extern SDL_DECLSPEC bool SDLCALL SDL_SetRenderDrawBlendMode(SDL_Renderer *renderer, SDL_BlendMode blendMode);
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_revision.h b/Source/ThirdParty/SDL/SDL3/SDL_revision.h
index a23e4fca1..893290082 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_revision.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_revision.h
@@ -31,9 +31,9 @@
/* #undef SDL_VENDOR_INFO */
#ifdef SDL_VENDOR_INFO
-#define SDL_REVISION "SDL-3.4.4-release-3.4.4 (" SDL_VENDOR_INFO ")"
+#define SDL_REVISION "SDL-3.4.16-release-3.4.16 (" SDL_VENDOR_INFO ")"
#else
-#define SDL_REVISION "SDL-3.4.4-release-3.4.4"
+#define SDL_REVISION "SDL-3.4.16-release-3.4.16"
#endif
#endif /* SDL_revision_h_ */
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_stdinc.h b/Source/ThirdParty/SDL/SDL3/SDL_stdinc.h
index 205f9bc3b..b6403bea7 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_stdinc.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_stdinc.h
@@ -1290,8 +1290,9 @@ extern "C" {
/**
* Free memory previously allocated with SDL_stack_alloc.
*
- * If SDL used alloca() to allocate this memory, this macro does nothing and
- * the allocated memory will be automatically released when the function that
+ * If SDL used alloca() to allocate this memory, this macro does nothing (other
+ * than insert `((void)(data)` so the compiler sees an expression) and the
+ * allocated memory will be automatically released when the function that
* called SDL_stack_alloc() returns. If SDL used SDL_malloc(), it will
* SDL_free the memory immediately.
*
@@ -1303,10 +1304,10 @@ extern "C" {
*
* \sa SDL_stack_alloc
*/
-#define SDL_stack_free(data)
+#define SDL_stack_free(data) ((void)(data))
#elif !defined(SDL_DISABLE_ALLOCA)
#define SDL_stack_alloc(type, count) (type*)alloca(sizeof(type)*(count))
-#define SDL_stack_free(data)
+#define SDL_stack_free(data) ((void)(data))
#else
#define SDL_stack_alloc(type, count) (type*)SDL_malloc(sizeof(type)*(count))
#define SDL_stack_free(data) SDL_free(data)
diff --git a/Source/ThirdParty/SDL/SDL3/SDL_version.h b/Source/ThirdParty/SDL/SDL3/SDL_version.h
index 12f157f8f..6b7816e0d 100644
--- a/Source/ThirdParty/SDL/SDL3/SDL_version.h
+++ b/Source/ThirdParty/SDL/SDL3/SDL_version.h
@@ -62,7 +62,7 @@ extern "C" {
*
* \since This macro is available since SDL 3.2.0.
*/
-#define SDL_MICRO_VERSION 4
+#define SDL_MICRO_VERSION 16
/**
* This macro turns the version numbers into a numeric value.
diff --git a/Source/ThirdParty/libportal/libportal.Build.cs b/Source/ThirdParty/libportal/libportal.Build.cs
index 839ca5f8e..d6e093aed 100644
--- a/Source/ThirdParty/libportal/libportal.Build.cs
+++ b/Source/ThirdParty/libportal/libportal.Build.cs
@@ -32,7 +32,10 @@ public class libportal : EngineDepsModule
options.PublicIncludePaths.Add("/usr/include/glib-2.0");
options.PublicIncludePaths.Add("/usr/lib/glib-2.0/include");
if (options.Architecture == TargetArchitecture.x64)
+ {
options.PublicIncludePaths.Add("/usr/lib/x86_64-linux-gnu/glib-2.0/include");
+ options.PublicIncludePaths.Add("/usr/lib64/glib-2.0/include");
+ }
//options.SourceFiles.Add(Path.Combine(FolderPath, "portal-enums.c"));
options.SourceFiles.AddRange(Directory.GetFiles(FolderPath, "*.c", SearchOption.TopDirectoryOnly));
diff --git a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs
index 0d2889368..45da51e8e 100644
--- a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs
+++ b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs
@@ -136,7 +136,7 @@ namespace Flax.Build
///
/// The maximum SDK version.
///
- public static Version MaximumVersion => new Version(10, 0);
+ public static Version MaximumVersion => new Version(11, 0);
///
public override TargetPlatform[] Platforms
diff --git a/Source/Tools/Flax.Build/Deps/Dependencies/SDL.cs b/Source/Tools/Flax.Build/Deps/Dependencies/SDL.cs
index 54fe19565..d010be0a2 100644
--- a/Source/Tools/Flax.Build/Deps/Dependencies/SDL.cs
+++ b/Source/Tools/Flax.Build/Deps/Dependencies/SDL.cs
@@ -89,7 +89,7 @@ namespace Flax.Deps.Dependencies
CloneGitRepo(root, "https://github.com/libsdl-org/SDL");
GitFetch(root);
- GitResetToCommit(root, "5848e584a1b606de26e3dbd1c7e4ecbc34f807a6"); // 3.4.4
+ GitResetToCommit(root, "fa2c02bb6e21974a89ea9824bc53c9932abe5f9c"); // 3.4.16
foreach (var platform in options.Platforms)
{