From 679eb2b76fdf9e7778cd2c7f9100af1216be040d Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sun, 26 Jul 2026 21:13:02 -0500 Subject: [PATCH 01/37] Audio device change detection --- Source/Engine/Audio/Audio.cpp | 41 ++- Source/Engine/Audio/Audio.h | 26 ++ Source/Engine/Audio/AudioBackend.h | 13 + Source/Engine/Audio/AudioSource.cpp | 25 +- Source/Engine/Audio/AudioSource.h | 3 + .../Engine/Audio/OpenAL/AudioBackendOAL.cpp | 318 ++++++++++++++++-- 6 files changed, 395 insertions(+), 31 deletions(-) diff --git a/Source/Engine/Audio/Audio.cpp b/Source/Engine/Audio/Audio.cpp index 3fea63ceb..75645d4e8 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() @@ -104,6 +123,11 @@ int32 Audio::GetActiveDeviceIndex() 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 +138,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..45a09df74 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. @@ -62,6 +72,22 @@ public: /// 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..981e9c34d 100644 --- a/Source/Engine/Audio/AudioBackend.h +++ b/Source/Engine/Audio/AudioBackend.h @@ -5,6 +5,7 @@ #include "Config.h" #include "Types.h" #include "Engine/Core/Types/BaseTypes.h" +#include "AudioSource.h" /// /// The helper class for that handles active audio backend operations. @@ -102,6 +103,18 @@ public: class Source { public: + FORCE_INLINE static void SetState(AudioSource* source, AudioSource::States state) + { + if (source) + source->_state = state; + } + + FORCE_INLINE static void SetStreamingFirstChunk(AudioSource* source, int32 chunkIndex) + { + if (source) + source->_streamingFirstChunk = 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..d50707ad8 100644 --- a/Source/Engine/Audio/AudioSource.cpp +++ b/Source/Engine/Audio/AudioSource.cpp @@ -195,13 +195,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 +217,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 +239,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 +411,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 07762be2f..f22e83593 100644 --- a/Source/Engine/Audio/AudioSource.h +++ b/Source/Engine/Audio/AudioSource.h @@ -6,6 +6,8 @@ #include "Engine/Content/AssetReference.h" #include "AudioClip.h" +class AudioBackend; + /// /// Represents a source for emitting audio. Audio can be played spatially (gun shot), or normally (music). Each audio source must have an AudioClip to play - back, and it can also have a position in the case of spatial (3D) audio. /// @@ -18,6 +20,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..16d977c89 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -3,6 +3,7 @@ #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" @@ -10,6 +11,7 @@ #include "Engine/Engine/Units.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Profiler/ProfilerMemory.h" +#include "Engine/Engine/Time.h" #include "Engine/Audio/Audio.h" #include "Engine/Audio/AudioListener.h" #include "Engine/Audio/AudioSource.h" @@ -204,23 +206,61 @@ 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 +698,6 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged() { PROFILE_CPU(); PROFILE_MEM(Audio); - // Fast-path on startup if (!ALC::Inited && ALC::Device) { @@ -666,6 +705,8 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged() return; } + const double startTime = Platform::GetTimeSeconds(); + // Cleanup Array states; states.EnsureCapacity(Audio::Sources.Count()); @@ -687,15 +728,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 +786,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 +811,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 +859,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 +922,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 +945,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() From b646c07bf1dfeb61935faaab9381d3ca11d18282 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sun, 26 Jul 2026 21:22:37 -0500 Subject: [PATCH 02/37] Add method to get device index if using system default mode. --- Source/Engine/Audio/Audio.cpp | 14 ++++++++++++++ Source/Engine/Audio/Audio.h | 8 +++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Audio/Audio.cpp b/Source/Engine/Audio/Audio.cpp index 75645d4e8..ee71ec27d 100644 --- a/Source/Engine/Audio/Audio.cpp +++ b/Source/Engine/Audio/Audio.cpp @@ -120,6 +120,20 @@ 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); diff --git a/Source/Engine/Audio/Audio.h b/Source/Engine/Audio/Audio.h index 45a09df74..f9f03624b 100644 --- a/Source/Engine/Audio/Audio.h +++ b/Source/Engine/Audio/Audio.h @@ -61,11 +61,17 @@ 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. /// From b09af9cfb95b62c55d320116bf253b3690b465c1 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sun, 26 Jul 2026 21:24:21 -0500 Subject: [PATCH 03/37] Small code cleanup --- Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index 16d977c89..6f3e851d0 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -229,13 +229,9 @@ namespace ALC if (clipLength > 0.0001f) { if (source->GetIsLooping()) - { targetTime = fmodf(targetTime, clipLength); - } else - { targetTime = Math::Min(targetTime, clipLength); - } } } From 4071390ca5413a24007d5fc354325d6a0d500e38 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 22 Sep 2026 22:59:43 +0200 Subject: [PATCH 04/37] Fix streaming of block-compressed wide/tall textures --- Source/Engine/Graphics/Textures/StreamingTexture.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Graphics/Textures/StreamingTexture.cpp b/Source/Engine/Graphics/Textures/StreamingTexture.cpp index 93cfb4565..dddd1c636 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); } From f3dddf6cacc140e6b77505fce421055e4de13842 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 23 Sep 2026 12:42:53 +0200 Subject: [PATCH 05/37] Add max .NET version to 11 --- Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs index de0fa755c..a8a5a77f8 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 From 07046a94c13272d87bce180bf0e6e7360eb5e74c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 23 Sep 2026 22:36:23 +0200 Subject: [PATCH 06/37] Revert "Merge branch 'GoaLitiuM-asset_window_reload_fix'" Caused regression on various windows being misplaces after hot-reload (eg. BT editor). This reverts commit 470a2220eeb5e31810fb3ad5b667c1b77160ef81, reversing changes made to 4d6fe760c1319300bca0438bc0f6c5061ea68be8. # Conflicts: # Source/Editor/GUI/Docking/FloatWindowDockPanel.cs --- Source/Editor/CustomEditorWindow.cs | 15 +- Source/Editor/GUI/Docking/DockWindow.cs | 48 ---- .../GUI/Docking/FloatWindowDockPanel.cs | 2 +- Source/Editor/Modules/WindowsModule.cs | 216 +++++++++--------- .../Editor/Windows/Assets/JsonAssetWindow.cs | 1 + 5 files changed, 117 insertions(+), 165 deletions(-) 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/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/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index 8074ab814..1a03c7c57 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. @@ -814,14 +822,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) @@ -832,65 +836,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); @@ -907,24 +908,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) @@ -933,46 +985,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/Windows/Assets/JsonAssetWindow.cs b/Source/Editor/Windows/Assets/JsonAssetWindow.cs index 75acd74dd..5cbf78f2a 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); } From 9ea61030edbc0b57648f04f7da38f55f88885be9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 23 Sep 2026 23:17:02 +0200 Subject: [PATCH 07/37] Fix BT node size when renaming decorator --- Source/Editor/Surface/Archetypes/BehaviorTree.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index cdc223a8d..b90bcdc55 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -550,10 +550,9 @@ namespace FlaxEditor.Surface.Archetypes { 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 +568,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(); @@ -734,7 +733,7 @@ namespace FlaxEditor.Surface.Archetypes width = Mathf.Max(width, _debugInfoSize.X + 8.0f); height += _debugInfoSize.Y + 8.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() From 328b7a75289ce90208fc4411865928204cb2a666 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 23 Sep 2026 23:30:00 +0200 Subject: [PATCH 08/37] Fix `TypeEditor` draw colors when disabled --- Source/Editor/CustomEditors/Editors/TypeEditor.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Editor/CustomEditors/Editors/TypeEditor.cs b/Source/Editor/CustomEditors/Editors/TypeEditor.cs index 462902181..c997e9375 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 From d56fdcce57af428582ec8eadb0fd546c316fa79a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 24 Sep 2026 22:32:52 +0200 Subject: [PATCH 09/37] Add more useful titles for BT decorator nodes --- .../Editor/Surface/Archetypes/BehaviorTree.cs | 31 +++++-- Source/Engine/AI/BehaviorKnowledgeSelector.h | 9 +- Source/Engine/AI/BehaviorTreeNodes.cpp | 85 +++++++++++++++++++ Source/Engine/AI/BehaviorTreeNodes.h | 15 ++++ 4 files changed, 129 insertions(+), 11 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index b90bcdc55..c6fc4c35d 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -246,14 +246,6 @@ namespace FlaxEditor.Surface.Archetypes if (highlightBox != null) Render2D.DrawRectangle(highlightBox.Bounds, style.BorderHighlighted, 2f); - // 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); - } - // Debug relevancy outline if (_debugRelevant) { @@ -546,6 +538,21 @@ 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); + } + } + public override void ResizeAuto() { if (Surface == null) @@ -731,7 +738,7 @@ 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, height + FlaxEditor.Surface.Constants.NodeHeaderHeight); } @@ -798,6 +805,12 @@ namespace FlaxEditor.Surface.Archetypes var style = Style.Current; + // Debug Info + if (!string.IsNullOrEmpty(_debugInfo)) + { + Render2D.DrawText(style.FontSmall, _debugInfo, new Rectangle(4, _headerRect.Bottom, _debugInfoSize), style.Foreground, TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1, 0.8f); + } + // Outline if (!_isSelected) { 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/BehaviorTreeNodes.cpp b/Source/Engine/AI/BehaviorTreeNodes.cpp index 21cfe4c95..08c7403c0 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); @@ -699,11 +724,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 +759,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("!") + result; + } + return result; +} + +#endif + bool BehaviorTreeHasTagDecorator::CanUpdate(const BehaviorUpdateContext& context) { bool result = false; @@ -722,8 +785,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..154363dea 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.h +++ b/Source/Engine/AI/BehaviorTreeNodes.h @@ -422,6 +422,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 +450,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 +474,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 +502,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 +522,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 }; From 0d4ea64caa889bc060227bc64088a5f7894e703d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 24 Sep 2026 22:57:06 +0200 Subject: [PATCH 10/37] Add red/green highlights to debug BT node decorator result --- Source/Editor/Options/OptionsModule.cs | 2 ++ .../Editor/Surface/Archetypes/BehaviorTree.cs | 11 ++++++++++- Source/Engine/AI/Behavior.cpp | 19 ++++++++++++++++++- Source/Engine/AI/Behavior.h | 2 ++ Source/Engine/Scripting/Scripting.cs | 1 + Source/Engine/UI/GUI/Style.cs | 6 ++++++ 6 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Options/OptionsModule.cs b/Source/Editor/Options/OptionsModule.cs index 49e30a53d..70600d026 100644 --- a/Source/Editor/Options/OptionsModule.cs +++ b/Source/Editor/Options/OptionsModule.cs @@ -299,6 +299,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, @@ -362,6 +363,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/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index c6fc4c35d..2ecb0f20c 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); @@ -808,7 +812,12 @@ namespace FlaxEditor.Surface.Archetypes // Debug Info if (!string.IsNullOrEmpty(_debugInfo)) { - Render2D.DrawText(style.FontSmall, _debugInfo, new Rectangle(4, _headerRect.Bottom, _debugInfoSize), style.Foreground, TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1, 0.8f); + 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); } // Outline diff --git a/Source/Engine/AI/Behavior.cpp b/Source/Engine/AI/Behavior.cpp index 55d96e415..4a38efb60 100644 --- a/Source/Engine/AI/Behavior.cpp +++ b/Source/Engine/AI/Behavior.cpp @@ -209,7 +209,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 +237,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/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/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. /// From 0c63c91a7dad5f1c47f2da4bbd82b93ac3fe97c3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 24 Sep 2026 23:38:50 +0200 Subject: [PATCH 11/37] Fix crash when looping BT with Force Success node --- Source/Engine/AI/Behavior.cpp | 23 ++++++++++++++++------- Source/Engine/AI/BehaviorTreeNodes.cpp | 2 ++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Source/Engine/AI/Behavior.cpp b/Source/Engine/AI/Behavior.cpp index 4a38efb60..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(); } } diff --git a/Source/Engine/AI/BehaviorTreeNodes.cpp b/Source/Engine/AI/BehaviorTreeNodes.cpp index 08c7403c0..bae799eab 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.cpp +++ b/Source/Engine/AI/BehaviorTreeNodes.cpp @@ -112,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) From f4f8e4e0056b3618f1f3725bdb2e65ef8662d7c8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 25 Sep 2026 00:29:18 +0200 Subject: [PATCH 12/37] Fix C# 'IsByRefLike' types that cannot be created on heap --- Source/Editor/Scripting/ScriptType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Scripting/ScriptType.cs b/Source/Editor/Scripting/ScriptType.cs index 1d26fc162..ab43daaf4 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; } } From 3feed2ad8fa8eff14b7737cfa2e83ccaf5697b31 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 25 Sep 2026 00:30:00 +0200 Subject: [PATCH 13/37] Fix bug in BT sub tree knowledge type validation --- Source/Engine/AI/BehaviorTreeNodes.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/AI/BehaviorTreeNodes.cpp b/Source/Engine/AI/BehaviorTreeNodes.cpp index bae799eab..710f99159 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.cpp +++ b/Source/Engine/AI/BehaviorTreeNodes.cpp @@ -361,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), From 208eebdf89fe6eb7747bea4d1ffa86e3fc77f757 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 25 Sep 2026 00:30:24 +0200 Subject: [PATCH 14/37] Various fixes and improvements to BT --- Flax.sln.DotSettings | 1 + .../Editor/Surface/Archetypes/BehaviorTree.cs | 32 ++++++++++++------- Source/Engine/AI/BehaviorTreeNode.h | 2 +- Source/Engine/AI/BehaviorTreeNodes.cpp | 19 +++++++++-- Source/Engine/AI/BehaviorTreeNodes.h | 3 ++ 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/Flax.sln.DotSettings b/Flax.sln.DotSettings index 5a027a4d2..b5594c3f2 100644 --- a/Flax.sln.DotSettings +++ b/Flax.sln.DotSettings @@ -256,6 +256,7 @@ True True True + True True DisabledByUser True diff --git a/Source/Editor/Surface/Archetypes/BehaviorTree.cs b/Source/Editor/Surface/Archetypes/BehaviorTree.cs index 2ecb0f20c..dfd9f3d46 100644 --- a/Source/Editor/Surface/Archetypes/BehaviorTree.cs +++ b/Source/Editor/Surface/Archetypes/BehaviorTree.cs @@ -219,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 @@ -231,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) { @@ -246,17 +238,31 @@ 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); + // Selection outline + if (_isSelected) + { + 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 if (_debugRelevant) { 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); } } @@ -555,6 +561,8 @@ namespace FlaxEditor.Surface.Archetypes 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() @@ -820,6 +828,8 @@ namespace FlaxEditor.Surface.Archetypes 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/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 710f99159..f69b10d19 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.cpp +++ b/Source/Engine/AI/BehaviorTreeNodes.cpp @@ -380,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); @@ -401,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; @@ -552,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; @@ -770,7 +783,7 @@ String BehaviorTreeKnowledgeBooleanDecorator::GetDebugInfo(const BehaviorUpdateC { result = Value.ToString(); if (Invert) - result = TEXT("!") + result; + result = TEXT("Not ") + result; } return result; } diff --git a/Source/Engine/AI/BehaviorTreeNodes.h b/Source/Engine/AI/BehaviorTreeNodes.h index 154363dea..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 From 559235125368f665838a74877a5e74f244facb97 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 25 Sep 2026 23:39:28 +0200 Subject: [PATCH 15/37] Fix animation slot bugs and stabilize scripting playback #3813 #4245 #4006 --- Source/Engine/Animations/Graph/AnimGraph.h | 4 +- .../Animations/Graph/AnimGroup.Animation.cpp | 48 ++++++++++++------- Source/Engine/Level/Actors/AnimatedModel.cpp | 46 ++++++++++++++---- Source/Engine/Level/Actors/AnimatedModel.h | 3 +- 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/Source/Engine/Animations/Graph/AnimGraph.h b/Source/Engine/Animations/Graph/AnimGraph.h index e2fe1ab83..f5a178f48 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..c879610e7 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -2356,7 +2356,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 +2369,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 +2413,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 +2455,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 +2472,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 +2480,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/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index 90d978af0..a2955644c 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -587,17 +587,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++) { @@ -613,11 +638,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) @@ -626,10 +658,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; } } } diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index 806e0fcdf..ee497e6a4 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -395,6 +395,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. @@ -412,7 +413,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); /// From 330df109025b27f7c259b7f68ffbeef248ae3243 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 26 Sep 2026 23:01:15 +0200 Subject: [PATCH 16/37] Fix spawning actors in viewport to have better origin placement Empty Actor is a building block of hierarchies so don't include dummy bounds there. Wait for dragged asset to be loaded for proper bounds. Use mesh bounds for non-active Animated Model for accurate placement. --- Source/Editor/Content/Items/BinaryAssetItem.cs | 4 ++-- Source/Editor/Content/Items/PrefabItem.cs | 2 +- Source/Editor/Viewport/ViewportDraggingHelper.cs | 2 +- Source/Engine/Level/Actors/AnimatedModel.cpp | 14 +++++++++++++- Source/Engine/Level/Actors/EmptyActor.cpp | 10 ---------- Source/Engine/Level/Actors/EmptyActor.h | 5 ----- 6 files changed, 17 insertions(+), 20 deletions(-) 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/Viewport/ViewportDraggingHelper.cs b/Source/Editor/Viewport/ViewportDraggingHelper.cs index bdd85024a..5f74904f2 100644 --- a/Source/Editor/Viewport/ViewportDraggingHelper.cs +++ b/Source/Editor/Viewport/ViewportDraggingHelper.cs @@ -179,7 +179,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/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index a2955644c..b9e545150 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -1250,7 +1250,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/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] From a15838e0c94599dda1f623f75a46d727481d2d6b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 26 Sep 2026 23:15:00 +0200 Subject: [PATCH 17/37] Fix attributes editor empty hint text display --- Source/Editor/Surface/AttributesEditor.cs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Source/Editor/Surface/AttributesEditor.cs b/Source/Editor/Surface/AttributesEditor.cs index 29d5860c3..4f98ef81b 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(); } From 2b4347996c655d693d331ae4c1d528e44c437923 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 26 Sep 2026 23:19:00 +0200 Subject: [PATCH 18/37] Fix slider control with negative minimum range value --- Source/Editor/GUI/Input/SliderControl.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) 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(); } /// From 160d400bf6d1e3a96d09c669edb26de57814a69f Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 27 Sep 2026 21:03:49 +0300 Subject: [PATCH 19/37] Update SDL to `3.4.16` --- Source/ThirdParty/SDL/SDL3/SDL.h | 2 +- Source/ThirdParty/SDL/SDL3/SDL_assert.h | 22 +- Source/ThirdParty/SDL/SDL3/SDL_atomic.h | 16 +- Source/ThirdParty/SDL/SDL3/SDL_audio.h | 19 +- Source/ThirdParty/SDL/SDL3/SDL_camera.h | 2 +- Source/ThirdParty/SDL/SDL3/SDL_clipboard.h | 2 +- Source/ThirdParty/SDL/SDL3/SDL_events.h | 3 +- Source/ThirdParty/SDL/SDL3/SDL_gamepad.h | 10 +- Source/ThirdParty/SDL/SDL3/SDL_gpu.h | 21 +- Source/ThirdParty/SDL/SDL3/SDL_hints.h | 76 +++++- Source/ThirdParty/SDL/SDL3/SDL_opengl_glext.h | 40 +-- Source/ThirdParty/SDL/SDL3/SDL_openxr.h | 237 ------------------ Source/ThirdParty/SDL/SDL3/SDL_render.h | 10 +- Source/ThirdParty/SDL/SDL3/SDL_revision.h | 4 +- Source/ThirdParty/SDL/SDL3/SDL_stdinc.h | 9 +- Source/ThirdParty/SDL/SDL3/SDL_version.h | 2 +- .../Tools/Flax.Build/Deps/Dependencies/SDL.cs | 2 +- 17 files changed, 182 insertions(+), 295 deletions(-) delete mode 100644 Source/ThirdParty/SDL/SDL3/SDL_openxr.h 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/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) { From df6008b0b596f21864b886e49bb838fed48ec4f9 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 27 Sep 2026 21:06:34 +0300 Subject: [PATCH 20/37] Fix Wayland dragging events racing with SDL data offers --- .../Engine/Platform/SDL/SDLPlatform.Linux.cpp | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) 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) { From 1a13526f9861e51c6a47e63f1611366fdef380a6 Mon Sep 17 00:00:00 2001 From: vthirulo <224445468+vthirulo@users.noreply.github.com> Date: Wed, 30 Sep 2026 20:58:22 +0530 Subject: [PATCH 21/37] Fix Linux build on Fedora - glibconfig.h --- Source/ThirdParty/libportal/libportal.Build.cs | 3 +++ 1 file changed, 3 insertions(+) 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)); From 6448ae8e37fb8c0b5a5dc7afa51d0a755d2d8bb0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 30 Sep 2026 20:50:30 +0200 Subject: [PATCH 22/37] Fix Multi Blend animations reversed playback when using negative speed #4036 #4035 #4037 --- .../Animations/Graph/AnimGroup.Animation.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index c879610e7..4ab0ebed2 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; @@ -314,11 +314,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 +346,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; } @@ -1020,7 +1022,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; @@ -1440,6 +1442,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)) From 21e17c3d9b7022efacc6a8c3eccf1fb3b69eae80 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 30 Sep 2026 20:51:05 +0200 Subject: [PATCH 23/37] Add improved tooltips to Visual Script types with file name --- Source/Editor/Modules/SourceCodeEditing/CodeDocsModule.cs | 2 ++ 1 file changed, 2 insertions(+) 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; From 95b201eb201098541698105fdd18f5c1c6346de5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 30 Sep 2026 21:10:00 +0200 Subject: [PATCH 24/37] Update to the latest Vulkan SDK `1.4.363` 1.13 f55ac81fe4f9ed74daa5c5618400225bd898ab02 --- .github/actions/vulkan/action.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/actions/vulkan/action.yml b/.github/actions/vulkan/action.yml index 4ab4adb59..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.3.290.0' + default: '1.4.363.0' required: false runs: using: "composite" @@ -17,20 +17,20 @@ 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 ;; Darwin) export VULKAN_SDK_ROOT=`pwd`/../VulkanSDK - curl -LO "https://sdk.lunarg.com/sdk/download/$VULKAN_SDK_VER/mac/vulkan-sdk.dmg" - hdiutil attach vulkan-sdk.dmg -mountpoint /Volumes/vulkan-sdk - sudo "/Volumes/vulkan-sdk/InstallVulkan.app/Contents/MacOS/InstallVulkan" --root $VULKAN_SDK_ROOT --accept-licenses --default-answer --confirm-command install + curl -LO "https://sdk.lunarg.com/sdk/download/$VULKAN_SDK_VER/mac/vulkansdk-macos-$VULKAN_SDK_VER.zip" + unzip vulkansdk-macos-$VULKAN_SDK_VER.zip + sudo "vulkansdk-macos-$VULKAN_SDK_VER.app/Contents/MacOS/vulkansdk-macOS-$VULKAN_SDK_VER" --root $VULKAN_SDK_ROOT --accept-licenses --default-answer --confirm-command install export VULKAN_SDK=$VULKAN_SDK_ROOT/macOS ;; *) # Windows - curl -L -o vulkan-sdk.exe https://sdk.lunarg.com/sdk/download/$VULKAN_SDK_VER/windows/VulkanSDK-$VULKAN_SDK_VER-Installer.exe + curl -L -o vulkan-sdk.exe https://sdk.lunarg.com/sdk/download/$VULKAN_SDK_VER/windows/vulkansdk-windows-X64-$VULKAN_SDK_VER.exe ./vulkan-sdk.exe --root "C:\VulkanSDK" --accept-licenses --default-answer --confirm-command install export VULKAN_SDK="C:\VulkanSDK" ;; From 971dcd6cf0ae5624e2c22d6340920e9edeb8fc58 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 30 Sep 2026 21:49:59 +0200 Subject: [PATCH 25/37] Add grey display for deprecated enum options --- .../GUI/ContextMenu/ContextMenuButton.cs | 8 +++++++- Source/Editor/GUI/EnumComboBox.cs | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) 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/EnumComboBox.cs b/Source/Editor/GUI/EnumComboBox.cs index f327f3e17..5020c28a7 100644 --- a/Source/Editor/GUI/EnumComboBox.cs +++ b/Source/Editor/GUI/EnumComboBox.cs @@ -10,6 +10,7 @@ using FlaxEditor.CustomEditors.Elements; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Scripting; using FlaxEngine; +using FlaxEngine.GUI; namespace FlaxEditor.GUI { @@ -59,17 +60,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; } } @@ -280,10 +288,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)); } } @@ -291,8 +298,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; } /// From f78a70e1cd5f67f2828126832b64ceb6cb548317 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 30 Sep 2026 21:56:54 +0200 Subject: [PATCH 26/37] Fix Game Cooker wait on code compile/load end before cooking starts #4277 --- Source/Editor/Modules/UIModule.cs | 2 +- Source/Editor/States/EditingSceneState.cs | 3 +++ Source/Editor/States/EditorState.cs | 9 +++++++-- Source/Editor/States/PlayingState.cs | 3 +++ Source/Editor/Windows/GameCookerWindow.cs | 11 ++++++++++- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 22f6e0233..85afca248 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/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/Windows/GameCookerWindow.cs b/Source/Editor/Windows/GameCookerWindow.cs index bfbbad0ac..ad43a3ee0 100644 --- a/Source/Editor/Windows/GameCookerWindow.cs +++ b/Source/Editor/Windows/GameCookerWindow.cs @@ -927,6 +927,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). /// @@ -1290,7 +1299,7 @@ namespace FlaxEditor.Windows public override void OnUpdate() { // Building queue - if (!GameCooker.IsRunning) + if (CanCook()) { if (_buildingQueue.Count > 0) { From 6c5321d2257c8dbc92b2000ae8fe556cf5dbee21 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 30 Sep 2026 22:03:48 +0200 Subject: [PATCH 27/37] Add deprecation attributes to old UWP platform --- Source/Editor/Cooker/CookingData.h | 6 ++++-- Source/Editor/Cooker/GameCooker.cs | 2 ++ Source/Editor/Editor.cs | 10 ++++++++++ Source/Editor/GUI/PlatformSelector.cs | 2 ++ Source/Editor/Windows/GameCookerWindow.cs | 8 ++++++++ Source/Engine/Platform/Defines.h | 3 ++- 6 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Cooker/CookingData.h b/Source/Editor/Cooker/CookingData.h index ddc02fa67..3c6408ffe 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/Editor.cs b/Source/Editor/Editor.cs index 7e624efd3..c305e3d05 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/PlatformSelector.cs b/Source/Editor/GUI/PlatformSelector.cs index 3b1b85e90..5b9ec9a74 100644 --- a/Source/Editor/GUI/PlatformSelector.cs +++ b/Source/Editor/GUI/PlatformSelector.cs @@ -84,7 +84,9 @@ namespace FlaxEditor.GUI { new PlatformData(PlatformType.Windows, icons.WindowsIcon128, "Windows"), new PlatformData(PlatformType.XboxOne, icons.XBoxOne128, "Xbox One"), +#pragma warning disable CS0612 new PlatformData(PlatformType.UWP, icons.UWPStore128, "Windows Store"), +#pragma warning restore CS0612 new PlatformData(PlatformType.Linux, icons.LinuxIcon128, "Linux"), new PlatformData(PlatformType.PS4, icons.PS4Icon128, "PlayStation 4"), new PlatformData(PlatformType.XboxScarlett, icons.XBoxScarletIcon128, "Xbox Scarlett"), diff --git a/Source/Editor/Windows/GameCookerWindow.cs b/Source/Editor/Windows/GameCookerWindow.cs index ad43a3ee0..38252ef1e 100644 --- a/Source/Editor/Windows/GameCookerWindow.cs +++ b/Source/Editor/Windows/GameCookerWindow.cs @@ -43,7 +43,9 @@ namespace FlaxEditor.Windows { { PlatformType.Windows, new Windows() }, { PlatformType.XboxOne, new XboxOne() }, +#pragma warning disable CS0612 { PlatformType.UWP, new UWP() }, +#pragma warning restore CS0612 { PlatformType.Linux, new Linux() }, { PlatformType.PS4, new PS4() }, { PlatformType.XboxScarlett, new XboxScarlett() }, @@ -164,8 +166,10 @@ namespace FlaxEditor.Windows #if PLATFORM_WINDOWS case BuildPlatform.Windows32: case BuildPlatform.Windows64: +#pragma warning disable CS0612 case BuildPlatform.UWPx86: case BuildPlatform.UWPx64: +#pragma warning restore CS0612 case BuildPlatform.LinuxX64: case BuildPlatform.AndroidARM64: case BuildPlatform.Web: @@ -217,7 +221,9 @@ namespace FlaxEditor.Windows class UWP : Platform { +#pragma warning disable CS0612 protected override BuildPlatform BuildPlatform => BuildPlatform.UWPx64; +#pragma warning restore CS0612 } class XboxOne : Platform @@ -573,10 +579,12 @@ namespace FlaxEditor.Windows case PlatformType.XboxOne: name = "Xbox One"; break; +#pragma warning disable CS0612 case PlatformType.UWP: name = "Windows Store"; layout.Label("UWP (Windows Store) platform has been deprecated and is no longer supported", TextAlignment.Center).Label.TextColor = Color.Red; break; +#pragma warning restore CS0612 case PlatformType.Linux: name = "Linux"; break; 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, /// From 96f9fcb00966fe9784f9a8eebaf208277aa6a889 Mon Sep 17 00:00:00 2001 From: Murry Lancashire Date: Thu, 1 Oct 2026 12:18:59 +1000 Subject: [PATCH 28/37] vert weld fix. --- Source/Engine/Graphics/Models/ModelData.Tool.cpp | 13 +++++++++++-- Source/Engine/Tools/ModelTool/SpatialSort.cpp | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Graphics/Models/ModelData.Tool.cpp b/Source/Engine/Graphics/Models/ModelData.Tool.cpp index f1c84cfb7..86e67ec0d 100644 --- a/Source/Engine/Graphics/Models/ModelData.Tool.cpp +++ b/Source/Engine/Graphics/Models/ModelData.Tool.cpp @@ -193,6 +193,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 ) { @@ -204,7 +205,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; @@ -298,6 +299,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 @@ -306,7 +315,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/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); From 9f4bce7ea312ffbc309bf436d956a9e51d469224 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 1 Oct 2026 09:43:51 +0200 Subject: [PATCH 29/37] Fix crash on invalid Multi Blend 1D indices #4176 --- Source/Engine/Animations/Graph/AnimGroup.Animation.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index 4ab0ebed2..acc30b6c6 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -1427,11 +1427,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); From f3ce2efdf2bb015f7963f129aba3b5ebce876e39 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 1 Oct 2026 10:13:41 +0200 Subject: [PATCH 30/37] Minor adjustments to code #4186 --- Source/Engine/Audio/AudioBackend.h | 14 ++------------ Source/Engine/Audio/AudioSource.cpp | 5 +++++ Source/Engine/Audio/AudioSource.h | 2 -- Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp | 2 +- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/Source/Engine/Audio/AudioBackend.h b/Source/Engine/Audio/AudioBackend.h index 981e9c34d..53b207854 100644 --- a/Source/Engine/Audio/AudioBackend.h +++ b/Source/Engine/Audio/AudioBackend.h @@ -5,7 +5,6 @@ #include "Config.h" #include "Types.h" #include "Engine/Core/Types/BaseTypes.h" -#include "AudioSource.h" /// /// The helper class for that handles active audio backend operations. @@ -103,17 +102,8 @@ public: class Source { public: - FORCE_INLINE static void SetState(AudioSource* source, AudioSource::States state) - { - if (source) - source->_state = state; - } - - FORCE_INLINE static void SetStreamingFirstChunk(AudioSource* source, int32 chunkIndex) - { - if (source) - source->_streamingFirstChunk = chunkIndex; - } + // 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) { diff --git a/Source/Engine/Audio/AudioSource.cpp b/Source/Engine/Audio/AudioSource.cpp index d50707ad8..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) diff --git a/Source/Engine/Audio/AudioSource.h b/Source/Engine/Audio/AudioSource.h index f2ecb6729..00adc35e5 100644 --- a/Source/Engine/Audio/AudioSource.h +++ b/Source/Engine/Audio/AudioSource.h @@ -6,8 +6,6 @@ #include "Engine/Content/AssetReference.h" #include "AudioClip.h" -class AudioBackend; - /// /// Represents a source for emitting audio. Audio can be played spatially (gun shot), or normally (music). Each audio source must have an AudioClip to play - back, and it can also have a position in the case of spatial (3D) audio. /// diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index 6f3e851d0..501ebbb66 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -9,9 +9,9 @@ #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/Engine/Time.h" #include "Engine/Audio/Audio.h" #include "Engine/Audio/AudioListener.h" #include "Engine/Audio/AudioSource.h" From 5f726dc00acb2cb6ccd430f7b7ec6b711ff0b1c1 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 1 Oct 2026 15:15:50 +0200 Subject: [PATCH 31/37] Fix asset picker drag area --- Source/Editor/GUI/AssetPicker.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index 98e9a0730..7650dbe03 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 From aac6a3a4b83d52290f1047eacf532c59c52c3966 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 1 Oct 2026 15:16:44 +0200 Subject: [PATCH 32/37] Fix crash when material/particle function has invalid graph output index --- .../Particles/Graph/GPU/ParticleEmitterGraph.GPU.Particles.cpp | 2 +- .../Tools/MaterialGenerator/MaterialGenerator.Material.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/Tools/MaterialGenerator/MaterialGenerator.Material.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Material.cpp index 481cf2df7..b60d9d389 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; From 3ea2a4df70e0392f140415040864ad009b5e2045 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 1 Oct 2026 15:18:45 +0200 Subject: [PATCH 33/37] Fix timeline curve expanding to properly show whole curve area --- Source/Editor/GUI/CurveEditor.Base.cs | 5 +++++ Source/Editor/GUI/CurveEditor.cs | 3 +++ .../Editor/GUI/Timeline/Tracks/CurvePropertyTrack.cs | 10 +++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) 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..6693dec30 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 { 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(); From d584b87d3567da2be21e7141456064b045649ee5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 1 Oct 2026 20:32:21 +0200 Subject: [PATCH 34/37] Fix collapsed curve keyframes display bug --- Source/Editor/GUI/CurveEditor.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs index 6693dec30..948e3fb57 100644 --- a/Source/Editor/GUI/CurveEditor.cs +++ b/Source/Editor/GUI/CurveEditor.cs @@ -1029,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; @@ -1037,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 @@ -1344,6 +1349,7 @@ namespace FlaxEditor.GUI } base.Draw(); + Render2D.PushClip(ref viewRect); DrawTangentHandles(); DrawSelectedKeyframeLabels(); From c294bf6403563f2b6adb195df4a4c0637bb58cf0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 1 Oct 2026 20:37:13 +0200 Subject: [PATCH 35/37] Hide not needed buttons by model asset previews --- Source/Editor/Viewport/Previews/AnimatedModelPreview.cs | 3 +++ Source/Editor/Viewport/Previews/ModelPreview.cs | 3 +++ 2 files changed, 6 insertions(+) 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; } } From 52620b9d804a9976d2480e2a988006fe7e0bade3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 1 Oct 2026 21:37:51 +0200 Subject: [PATCH 36/37] Cleanup some code --- .../Animations/Graph/AnimGroup.Animation.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index acc30b6c6..ae2b250e1 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -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) ) { From 093a49c76ab9c82800b97a611c3d874b0d3d8659 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 1 Oct 2026 22:55:27 +0200 Subject: [PATCH 37/37] Fix root motion on skeletons with different coordinate system #3847 --- Source/Engine/Animations/AnimationData.h | 2 ++ .../Animations/Graph/AnimGroup.Animation.cpp | 28 +++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) 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/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index ae2b250e1..272c61b7e 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -499,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; @@ -545,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; } } @@ -565,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; }