From f12ad5c874ed0cfcc9cd255f3e4c07e6dbf82ff5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 14 Feb 2026 00:07:21 +0100 Subject: [PATCH] Add **Web platform with Emscripten** --- Content/Shaders/Reflections.flax | 4 +- Source/Editor/Cooker/CookingData.h | 5 + Source/Editor/Cooker/GameCooker.cpp | 18 ++ Source/Editor/Cooker/GameCooker.cs | 1 + .../Cooker/Platform/Web/WebPlatformTools.cpp | 60 ++++ .../Cooker/Platform/Web/WebPlatformTools.h | 26 ++ Source/Editor/Cooker/Steps/CookAssetsStep.cpp | 8 + Source/Editor/Editor.Build.cs | 1 + Source/Editor/GUI/PlatformSelector.cs | 1 + Source/Editor/Surface/Archetypes/Tools.cs | 5 +- Source/Editor/Windows/GameCookerWindow.cs | 10 + Source/Engine/Audio/Audio.Build.cs | 3 + Source/Engine/Audio/AudioClip.cpp | 6 + Source/Engine/Core/Config/GameSettings.cpp | 3 + Source/Engine/Core/Config/GameSettings.cs | 18 ++ Source/Engine/Core/Config/GameSettings.h | 1 + Source/Engine/Core/Config/PlatformSettings.h | 3 + Source/Engine/Core/Log.cpp | 2 +- Source/Engine/Core/Memory/Allocation.h | 6 +- Source/Engine/Engine/Game.h | 2 + Source/Engine/Engine/Web/WebGame.h | 19 ++ Source/Engine/Graphics/Config.h | 4 + Source/Engine/Graphics/Graphics.Build.cs | 4 + Source/Engine/Graphics/RenderBuffers.cpp | 2 +- Source/Engine/Main/Main.Build.cs | 3 + Source/Engine/Main/Web/main.cpp | 12 + Source/Engine/Physics/Physics.Build.cs | 6 + .../Engine/Platform/Android/AndroidPlatform.h | 5 - Source/Engine/Platform/Base/PlatformBase.cpp | 13 +- Source/Engine/Platform/Base/PlatformBase.h | 8 +- Source/Engine/Platform/Base/ThreadBase.cpp | 2 +- Source/Engine/Platform/ConditionVariable.h | 2 +- Source/Engine/Platform/CriticalSection.h | 2 +- Source/Engine/Platform/Defines.h | 10 + Source/Engine/Platform/File.h | 2 +- Source/Engine/Platform/FileSystem.h | 2 + Source/Engine/Platform/Linux/LinuxPlatform.h | 5 - Source/Engine/Platform/Network.h | 2 +- Source/Engine/Platform/Platform.Build.cs | 16 +- Source/Engine/Platform/Platform.h | 2 + Source/Engine/Platform/ReadWriteLock.h | 2 +- .../Engine/Platform/SDL/SDLPlatform.Web.cpp | 207 ++++++++++++ Source/Engine/Platform/SDL/SDLPlatform.cpp | 8 +- Source/Engine/Platform/SDL/SDLPlatform.h | 20 +- Source/Engine/Platform/SDL/SDLWindow.cpp | 3 + Source/Engine/Platform/Thread.h | 2 + Source/Engine/Platform/Types.h | 23 ++ Source/Engine/Platform/Web/WebDefines.h | 30 ++ Source/Engine/Platform/Web/WebFileSystem.h | 19 ++ Source/Engine/Platform/Web/WebPlatform.cpp | 254 +++++++++++++++ Source/Engine/Platform/Web/WebPlatform.h | 116 +++++++ .../Engine/Platform/Web/WebPlatformSettings.h | 27 ++ Source/Engine/Platform/Web/WebThread.h | 63 ++++ Source/Engine/Profiler/Profiler.Build.cs | 2 + .../Renderer/ScreenSpaceReflectionsPass.cpp | 2 +- Source/Engine/Renderer/ShadowsPass.cpp | 2 +- Source/Engine/Threading/JobSystem.cpp | 5 +- .../Tools/TextureTool/TextureTool.Build.cs | 1 + Source/Engine/Video/Video.Build.cs | 4 + Source/Engine/Visject/ShaderGraph.cpp | 1 + Source/Engine/Visject/VisjectGraph.cpp | 3 + Source/Shaders/Reflections.shader | 2 +- Source/ThirdParty/NvCloth/NvCloth.Build.cs | 1 + Source/ThirdParty/SDL/SDL.Build.cs | 3 + Source/ThirdParty/freetype/freetype.Build.cs | 3 + Source/ThirdParty/ogg/ogg.Build.cs | 3 + Source/ThirdParty/vorbis/vorbis.Build.cs | 3 + Source/Tools/Flax.Build/Build/DepsModule.cs | 1 + Source/Tools/Flax.Build/Build/Module.cs | 1 + Source/Tools/Flax.Build/Build/Platform.cs | 3 +- .../Tools/Flax.Build/Build/TargetPlatform.cs | 5 + Source/Tools/Flax.Build/Configuration.cs | 4 + Source/Tools/Flax.Build/Globals.cs | 1 + .../Platforms/Linux/LinuxPlatform.cs | 1 + .../Flax.Build/Platforms/Mac/MacPlatform.cs | 1 + .../Platforms/Unix/UnixToolchain.cs | 2 +- .../Flax.Build/Platforms/Web/EmscriptenSdk.cs | 87 +++++ .../Flax.Build/Platforms/Web/WebPlatform.cs | 68 ++++ .../Flax.Build/Platforms/Web/WebToolchain.cs | 302 ++++++++++++++++++ .../Platforms/Windows/WindowsPlatform.cs | 1 + 80 files changed, 1529 insertions(+), 61 deletions(-) create mode 100644 Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp create mode 100644 Source/Editor/Cooker/Platform/Web/WebPlatformTools.h create mode 100644 Source/Engine/Engine/Web/WebGame.h create mode 100644 Source/Engine/Main/Web/main.cpp create mode 100644 Source/Engine/Platform/SDL/SDLPlatform.Web.cpp create mode 100644 Source/Engine/Platform/Web/WebDefines.h create mode 100644 Source/Engine/Platform/Web/WebFileSystem.h create mode 100644 Source/Engine/Platform/Web/WebPlatform.cpp create mode 100644 Source/Engine/Platform/Web/WebPlatform.h create mode 100644 Source/Engine/Platform/Web/WebPlatformSettings.h create mode 100644 Source/Engine/Platform/Web/WebThread.h create mode 100644 Source/Tools/Flax.Build/Platforms/Web/EmscriptenSdk.cs create mode 100644 Source/Tools/Flax.Build/Platforms/Web/WebPlatform.cs create mode 100644 Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs diff --git a/Content/Shaders/Reflections.flax b/Content/Shaders/Reflections.flax index a18cf4a83..b260430e0 100644 --- a/Content/Shaders/Reflections.flax +++ b/Content/Shaders/Reflections.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83514233fd2a22bbfe1a35520bcb279c9370b8d6715381201d83ea32ddc3b488 -size 3654 +oid sha256:1a6c6343f6963656c2cac6554302f0ffc15706e83d3f196153b92406712a01fc +size 3680 diff --git a/Source/Editor/Cooker/CookingData.h b/Source/Editor/Cooker/CookingData.h index 1ecfaf353..88b23d6bc 100644 --- a/Source/Editor/Cooker/CookingData.h +++ b/Source/Editor/Cooker/CookingData.h @@ -141,6 +141,11 @@ API_ENUM() enum class BuildPlatform /// API_ENUM(Attributes="EditorDisplay(null, \"Windows ARM64\")") WindowsARM64 = 15, + + /// + /// Web + /// + Web = 16, }; /// diff --git a/Source/Editor/Cooker/GameCooker.cpp b/Source/Editor/Cooker/GameCooker.cpp index da2d74f87..4dbe680cf 100644 --- a/Source/Editor/Cooker/GameCooker.cpp +++ b/Source/Editor/Cooker/GameCooker.cpp @@ -69,6 +69,10 @@ #include "Platform/iOS/iOSPlatformTools.h" #include "Engine/Platform/iOS/iOSPlatformSettings.h" #endif +#if PLATFORM_TOOLS_WEB +#include "Platform/Web/WebPlatformTools.h" +#include "Engine/Platform/Web/WebPlatformSettings.h" +#endif namespace GameCookerImpl { @@ -151,6 +155,8 @@ const Char* ToString(const BuildPlatform platform) return TEXT("iOS ARM64"); case BuildPlatform::WindowsARM64: return TEXT("Windows ARM64"); + case BuildPlatform::Web: + return TEXT("Web"); default: return TEXT(""); } @@ -307,6 +313,10 @@ void CookingData::GetBuildPlatformName(const Char*& platform, const Char*& archi platform = TEXT("Windows"); architecture = TEXT("ARM64"); break; + case BuildPlatform::Web: + platform = TEXT("Web"); + architecture = TEXT("x86"); + break; default: LOG(Fatal, "Unknown or unsupported build platform."); } @@ -461,6 +471,11 @@ PlatformTools* GameCooker::GetTools(BuildPlatform platform) case BuildPlatform::iOSARM64: result = New(); break; +#endif +#if PLATFORM_TOOLS_WEB + case BuildPlatform::Web: + result = New(); + break; #endif } Tools.Add(platform, result); @@ -604,6 +619,9 @@ void GameCooker::GetCurrentPlatform(PlatformType& platform, BuildPlatform& build case PlatformType::iOS: buildPlatform = BuildPlatform::iOSARM64; break; + case PlatformType::Web: + buildPlatform = BuildPlatform::Web; + break; default: ; } } diff --git a/Source/Editor/Cooker/GameCooker.cs b/Source/Editor/Cooker/GameCooker.cs index 70a9f4dac..ef91dd5f9 100644 --- a/Source/Editor/Cooker/GameCooker.cs +++ b/Source/Editor/Cooker/GameCooker.cs @@ -106,6 +106,7 @@ namespace FlaxEditor case BuildPlatform.MacOSARM64: case BuildPlatform.MacOSx64: return PlatformType.Mac; case BuildPlatform.iOSARM64: return PlatformType.iOS; + case BuildPlatform.Web: return PlatformType.Web; default: throw new ArgumentOutOfRangeException(nameof(buildPlatform), buildPlatform, null); } } diff --git a/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp b/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp new file mode 100644 index 000000000..d1ed7fb9a --- /dev/null +++ b/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp @@ -0,0 +1,60 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#if PLATFORM_TOOLS_WEB + +#include "WebPlatformTools.h" +#include "Engine/Platform/File.h" +#include "Engine/Platform/FileSystem.h" +#include "Engine/Platform/Web/WebPlatformSettings.h" +#include "Engine/Core/Config/GameSettings.h" +#include "Engine/Core/Config/BuildSettings.h" +#include "Engine/Content/Content.h" +#include "Engine/Content/JsonAsset.h" +#include "Engine/Graphics/PixelFormatExtensions.h" + +IMPLEMENT_SETTINGS_GETTER(WebPlatformSettings, WebPlatform); + +const Char* WebPlatformTools::GetDisplayName() const +{ + return TEXT("Web"); +} + +const Char* WebPlatformTools::GetName() const +{ + return TEXT("Web"); +} + +PlatformType WebPlatformTools::GetPlatform() const +{ + return PlatformType::Web; +} + +ArchitectureType WebPlatformTools::GetArchitecture() const +{ + return ArchitectureType::x86; +} + +DotNetAOTModes WebPlatformTools::UseAOT() const +{ + return DotNetAOTModes::MonoAOTStatic; +} + +PixelFormat WebPlatformTools::GetTextureFormat(CookingData& data, TextureBase* texture, PixelFormat format) +{ + // TODO: texture compression for Web (eg. ASTC for mobile and BC for others?) + return PixelFormatExtensions::FindUncompressedFormat(format); +} + +bool WebPlatformTools::IsNativeCodeFile(CookingData& data, const String& file) +{ + String extension = FileSystem::GetExtension(file); + return extension.IsEmpty() || extension == TEXT("html") || extension == TEXT("js") || extension == TEXT("wams"); +} + +bool WebPlatformTools::OnPostProcess(CookingData& data) +{ + // TODO: customizable HTML templates + return false; +} + +#endif diff --git a/Source/Editor/Cooker/Platform/Web/WebPlatformTools.h b/Source/Editor/Cooker/Platform/Web/WebPlatformTools.h new file mode 100644 index 000000000..7b14dcd10 --- /dev/null +++ b/Source/Editor/Cooker/Platform/Web/WebPlatformTools.h @@ -0,0 +1,26 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#if PLATFORM_TOOLS_WEB + +#include "../../PlatformTools.h" + +/// +/// The Web platform support tools. +/// +class WebPlatformTools : public PlatformTools +{ +public: + // [PlatformTools] + const Char* GetDisplayName() const override; + const Char* GetName() const override; + PlatformType GetPlatform() const override; + ArchitectureType GetArchitecture() const override; + DotNetAOTModes UseAOT() const override; + PixelFormat GetTextureFormat(CookingData& data, TextureBase* texture, PixelFormat format) override; + bool IsNativeCodeFile(CookingData& data, const String& file) override; + bool OnPostProcess(CookingData& data) override; +}; + +#endif diff --git a/Source/Editor/Cooker/Steps/CookAssetsStep.cpp b/Source/Editor/Cooker/Steps/CookAssetsStep.cpp index c95306505..fc06f0153 100644 --- a/Source/Editor/Cooker/Steps/CookAssetsStep.cpp +++ b/Source/Editor/Cooker/Steps/CookAssetsStep.cpp @@ -572,6 +572,14 @@ bool ProcessShaderBase(CookAssetsStep::AssetCookData& data, ShaderAssetBase* ass COMPILE_PROFILE(Vulkan_SM5, SHADER_FILE_CHUNK_INTERNAL_VULKAN_SM5_CACHE); break; } +#endif +#if PLATFORM_TOOLS_WEB + case BuildPlatform::Web: + { + const char* platformDefineName = "PLATFORM_WEB"; + // TODO: compile shaders for WebGPU + break; + } #endif default: { diff --git a/Source/Editor/Editor.Build.cs b/Source/Editor/Editor.Build.cs index e75954909..5dad98458 100644 --- a/Source/Editor/Editor.Build.cs +++ b/Source/Editor/Editor.Build.cs @@ -87,6 +87,7 @@ public class Editor : EditorModule AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "Android", "PLATFORM_TOOLS_ANDROID"); AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "iOS", "PLATFORM_TOOLS_IOS"); } + AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "Web", "PLATFORM_TOOLS_WEB"); // Visual Studio integration if (options.Platform.Target == TargetPlatform.Windows && Flax.Build.Platform.BuildTargetPlatform == TargetPlatform.Windows) diff --git a/Source/Editor/GUI/PlatformSelector.cs b/Source/Editor/GUI/PlatformSelector.cs index ee72a5973..8c1f89e45 100644 --- a/Source/Editor/GUI/PlatformSelector.cs +++ b/Source/Editor/GUI/PlatformSelector.cs @@ -93,6 +93,7 @@ namespace FlaxEditor.GUI new PlatformData(PlatformType.PS5, icons.PS5Icon128, "PlayStation 5"), new PlatformData(PlatformType.Mac, icons.MacOSIcon128, "macOS"), new PlatformData(PlatformType.iOS, icons.IOSIcon128, "iOS"), + new PlatformData(PlatformType.Web, icons.Flax64, "Web"), }; const float IconSize = 64.0f; diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index 68a733197..60ecfaba6 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -1603,9 +1603,9 @@ namespace FlaxEditor.Surface.Archetypes Title = "Platform Switch", Description = "Gets the input value based on the runtime-platform type", Flags = NodeFlags.AllGraphs, - Size = new Float2(220, 240), + Size = new Float2(220, 260), ConnectionsHints = ConnectionsHint.Value, - IndependentBoxes = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, + IndependentBoxes = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 }, DependentBoxes = new[] { 0 }, Elements = new[] { @@ -1622,6 +1622,7 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Input(9, "PlayStation 5", true, null, 10), NodeElementArchetype.Factory.Input(10, "Mac", true, null, 11), NodeElementArchetype.Factory.Input(11, "iOS", true, null, 12), + NodeElementArchetype.Factory.Input(12, "Web", true, null, 13), } }, new NodeArchetype diff --git a/Source/Editor/Windows/GameCookerWindow.cs b/Source/Editor/Windows/GameCookerWindow.cs index 8aab5d759..65107e105 100644 --- a/Source/Editor/Windows/GameCookerWindow.cs +++ b/Source/Editor/Windows/GameCookerWindow.cs @@ -52,6 +52,7 @@ namespace FlaxEditor.Windows { PlatformType.PS5, new PS5() }, { PlatformType.Mac, new Mac() }, { PlatformType.iOS, new iOS() }, + { PlatformType.Web, new Web() }, }; public BuildTabProxy(GameCookerWindow win, PlatformSelector platformSelector) @@ -165,6 +166,7 @@ namespace FlaxEditor.Windows case BuildPlatform.UWPx64: case BuildPlatform.LinuxX64: case BuildPlatform.AndroidARM64: + case BuildPlatform.Web: text += "\nUse Flax Launcher and download the required package."; break; #endif @@ -538,6 +540,11 @@ namespace FlaxEditor.Windows protected override BuildPlatform BuildPlatform => BuildPlatform.iOSARM64; } + class Web : Platform + { + protected override BuildPlatform BuildPlatform => BuildPlatform.Web; + } + class Editor : CustomEditor { private PlatformType _platform; @@ -592,6 +599,9 @@ namespace FlaxEditor.Windows case PlatformType.iOS: name = "iOS"; break; + case PlatformType.Web: + name = "Web"; + break; default: name = Utilities.Utils.GetPropertyNameUI(_platform.ToString()); break; diff --git a/Source/Engine/Audio/Audio.Build.cs b/Source/Engine/Audio/Audio.Build.cs index e38e9b527..d2e628957 100644 --- a/Source/Engine/Audio/Audio.Build.cs +++ b/Source/Engine/Audio/Audio.Build.cs @@ -60,6 +60,9 @@ public class Audio : EngineModule case TargetPlatform.iOS: useOpenAL = true; break; + case TargetPlatform.Web: + // TODO: audio playback on Web (OpenAL) + break; default: throw new InvalidPlatformException(options.Platform.Target); } diff --git a/Source/Engine/Audio/AudioClip.cpp b/Source/Engine/Audio/AudioClip.cpp index 4e258e81a..4529aaa83 100644 --- a/Source/Engine/Audio/AudioClip.cpp +++ b/Source/Engine/Audio/AudioClip.cpp @@ -194,6 +194,7 @@ bool AudioClip::ExtractData(Array& resultData, AudioDataInfo& resultDataIn bool AudioClip::ExtractDataFloat(Array& resultData, AudioDataInfo& resultDataInfo) { +#if COMPILE_WITH_AUDIO_TOOL // Extract PCM data Array data; if (ExtractDataRaw(data, resultDataInfo)) @@ -205,6 +206,9 @@ bool AudioClip::ExtractDataFloat(Array& resultData, AudioDataInfo& result resultDataInfo.BitDepth = 32; return false; +#else + return true; +#endif } bool AudioClip::ExtractDataRaw(Array& resultData, AudioDataInfo& resultDataInfo) @@ -475,6 +479,7 @@ bool AudioClip::WriteBuffer(int32 chunkIndex) } info.NumSamples = Math::AlignDown(data.Length() / bytesPerSample, info.NumChannels * bytesPerSample); +#if COMPILE_WITH_AUDIO_TOOL // Convert to Mono if used as 3D source and backend doesn't support it if (Is3D() && info.NumChannels > 1 && EnumHasNoneFlags(AudioBackend::Features(), AudioBackend::FeatureFlags::SpatialMultiChannel)) { @@ -486,6 +491,7 @@ bool AudioClip::WriteBuffer(int32 chunkIndex) info.NumSamples = samplesPerChannel; data = Span(tmp2.Get(), tmp2.Count()); } +#endif // Write samples to the audio buffer (create one if missing) Locker.Lock(); // StreamingTask loads buffers without lock so do it here diff --git a/Source/Engine/Core/Config/GameSettings.cpp b/Source/Engine/Core/Config/GameSettings.cpp index c91567d62..c75d2e8cb 100644 --- a/Source/Engine/Core/Config/GameSettings.cpp +++ b/Source/Engine/Core/Config/GameSettings.cpp @@ -95,6 +95,8 @@ IMPLEMENT_ENGINE_SETTINGS_GETTER(SwitchPlatformSettings, SwitchPlatform); IMPLEMENT_ENGINE_SETTINGS_GETTER(MacPlatformSettings, MacPlatform); #elif PLATFORM_IOS IMPLEMENT_ENGINE_SETTINGS_GETTER(iOSPlatformSettings, iOSPlatform); +#elif PLATFORM_WEB +IMPLEMENT_ENGINE_SETTINGS_GETTER(WebPlatformSettings, WebPlatform); #else #error Unknown platform #endif @@ -280,6 +282,7 @@ void GameSettings::Deserialize(DeserializeStream& stream, ISerializeModifier* mo DESERIALIZE(PS5Platform); DESERIALIZE(MacPlatform); DESERIALIZE(iOSPlatform); + DESERIALIZE(WebPlatform); } #if USE_EDITOR diff --git a/Source/Engine/Core/Config/GameSettings.cs b/Source/Engine/Core/Config/GameSettings.cs index 2f6ee40c1..4f66261be 100644 --- a/Source/Engine/Core/Config/GameSettings.cs +++ b/Source/Engine/Core/Config/GameSettings.cs @@ -210,6 +210,14 @@ namespace FlaxEditor.Content.Settings public JsonAsset iOSPlatform; #endif +#if FLAX_EDITOR || PLATFORM_WEB + /// + /// Reference to asset. Used to apply configuration on Web platform. + /// + [EditorOrder(2110), EditorDisplay("Platform Settings", "Web"), AssetReference(typeof(WebPlatformSettings), true), Tooltip("Reference to Web Platform Settings asset")] + public JsonAsset WebPlatform; +#endif + /// /// Gets the absolute path to the game settings asset file. /// @@ -345,6 +353,10 @@ namespace FlaxEditor.Content.Settings if (type == typeof(iOSPlatformSettings)) return Load(gameSettings.iOSPlatform) as T; #endif +#if FLAX_EDITOR || PLATFORM_WEB + if (type == typeof(WebPlatformSettings)) + return Load(gameSettings.WebPlatform) as T; +#endif if (gameSettings.CustomSettings != null) { @@ -443,6 +455,10 @@ namespace FlaxEditor.Content.Settings if (type == typeof(iOSPlatformSettings)) return gameSettings.iOSPlatform; #endif +#if FLAX_EDITOR || PLATFORM_WEB + if (type == typeof(WebPlatformSettings)) + return gameSettings.WebPlatform; +#endif if (gameSettings.CustomSettings != null) { @@ -557,6 +573,8 @@ namespace FlaxEditor.Content.Settings return SaveAsset(gameSettings, ref gameSettings.MacPlatform, obj); if (type == typeof(iOSPlatformSettings)) return SaveAsset(gameSettings, ref gameSettings.iOSPlatform, obj); + if (type == typeof(WebPlatformSettings)) + return SaveAsset(gameSettings, ref gameSettings.WebPlatform, obj); return true; } diff --git a/Source/Engine/Core/Config/GameSettings.h b/Source/Engine/Core/Config/GameSettings.h index db1f8ac53..7c3969f2b 100644 --- a/Source/Engine/Core/Config/GameSettings.h +++ b/Source/Engine/Core/Config/GameSettings.h @@ -90,6 +90,7 @@ public: Guid PS5Platform; Guid MacPlatform; Guid iOSPlatform; + Guid WebPlatform; public: /// diff --git a/Source/Engine/Core/Config/PlatformSettings.h b/Source/Engine/Core/Config/PlatformSettings.h index 6f8de1afa..b485e42c6 100644 --- a/Source/Engine/Core/Config/PlatformSettings.h +++ b/Source/Engine/Core/Config/PlatformSettings.h @@ -38,3 +38,6 @@ #if PLATFORM_IOS #include "Engine/Platform/iOS/iOSPlatformSettings.h" #endif +#if PLATFORM_WEB +#include "Engine/Platform/Web/WebPlatformSettings.h" +#endif diff --git a/Source/Engine/Core/Log.cpp b/Source/Engine/Core/Log.cpp index 013031ced..8241097b1 100644 --- a/Source/Engine/Core/Log.cpp +++ b/Source/Engine/Core/Log.cpp @@ -17,7 +17,7 @@ #endif #include -#define LOG_ENABLE_FILE (!PLATFORM_SWITCH) +#define LOG_ENABLE_FILE (!PLATFORM_SWITCH && !PLATFORM_WEB) #define LOG_ENABLE_WINDOWS_SINGLE_NEW_LINE_CHAR (PLATFORM_WINDOWS && PLATFORM_DESKTOP && (USE_EDITOR || !BUILD_RELEASE)) namespace diff --git a/Source/Engine/Core/Memory/Allocation.h b/Source/Engine/Core/Memory/Allocation.h index f274d37a5..63c9dbcc3 100644 --- a/Source/Engine/Core/Memory/Allocation.h +++ b/Source/Engine/Core/Memory/Allocation.h @@ -58,7 +58,7 @@ public: typedef void* Tag; template - class alignas(sizeof(void*)) Data + class alignas(uint64) Data { private: byte _data[Capacity * sizeof(T)]; @@ -200,13 +200,13 @@ public: typedef void* Tag; template - class alignas(sizeof(void*)) Data + class alignas(sizeof(uint64)) Data { private: typedef typename FallbackAllocation::template Data FallbackData; bool _useFallback = false; - alignas(sizeof(void*)) byte _data[Capacity * sizeof(T)]; + alignas(sizeof(uint64)) byte _data[Capacity * sizeof(T)]; FallbackData _fallback; public: diff --git a/Source/Engine/Engine/Game.h b/Source/Engine/Engine/Game.h index 571258ed2..c12b25683 100644 --- a/Source/Engine/Engine/Game.h +++ b/Source/Engine/Engine/Game.h @@ -26,6 +26,8 @@ #include "Mac/MacGame.h" #elif PLATFORM_IOS #include "iOS/iOSGame.h" +#elif PLATFORM_WEB +#include "Web/WebGame.h" #else #error Missing Game implementation! #endif diff --git a/Source/Engine/Engine/Web/WebGame.h b/Source/Engine/Engine/Web/WebGame.h new file mode 100644 index 000000000..f65ba4a93 --- /dev/null +++ b/Source/Engine/Engine/Web/WebGame.h @@ -0,0 +1,19 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#if PLATFORM_WEB + +#include "../Base/GameBase.h" + +/// +/// The game class implementation for Web platform. +/// +/// +class WebGame : public GameBase +{ +}; + +typedef WebGame Game; + +#endif diff --git a/Source/Engine/Graphics/Config.h b/Source/Engine/Graphics/Config.h index 43fb319bf..2059fb0a9 100644 --- a/Source/Engine/Graphics/Config.h +++ b/Source/Engine/Graphics/Config.h @@ -45,7 +45,9 @@ #define GPU_USE_WINDOW_SRV 1 // True if allow graphics profile events and markers +#ifndef GPU_ALLOW_PROFILE_EVENTS #define GPU_ALLOW_PROFILE_EVENTS (!BUILD_RELEASE) +#endif // True if allow hardware tessellation shaders (Hull and Domain shaders) #ifndef GPU_ALLOW_TESSELLATION_SHADERS @@ -58,7 +60,9 @@ #endif // Enable/disable creating GPU resources on separate threads (otherwise only the main thread can be used) +#ifndef GPU_ENABLE_ASYNC_RESOURCES_CREATION #define GPU_ENABLE_ASYNC_RESOURCES_CREATION 1 +#endif // Enable/disable force shaders recompilation #define GPU_FORCE_RECOMPILE_SHADERS 0 diff --git a/Source/Engine/Graphics/Graphics.Build.cs b/Source/Engine/Graphics/Graphics.Build.cs index 51b37144b..caebebe50 100644 --- a/Source/Engine/Graphics/Graphics.Build.cs +++ b/Source/Engine/Graphics/Graphics.Build.cs @@ -96,6 +96,10 @@ public class Graphics : EngineModule else Log.WarningOnce(string.Format("Building for {0} without Vulkan rendering backend (Vulkan SDK is missing)", options.Platform.Target), ref _logMissingVulkanSDK); break; + case TargetPlatform.Web: + options.PrivateDependencies.Add("GraphicsDeviceNull"); + // TODO: add WebGPU + break; default: throw new InvalidPlatformException(options.Platform.Target); } diff --git a/Source/Engine/Graphics/RenderBuffers.cpp b/Source/Engine/Graphics/RenderBuffers.cpp index 8b67a64b3..65fc01ea2 100644 --- a/Source/Engine/Graphics/RenderBuffers.cpp +++ b/Source/Engine/Graphics/RenderBuffers.cpp @@ -127,7 +127,7 @@ GPUTexture* RenderBuffers::RequestHiZ(GPUContext* context, bool fullRes, int32 m // Allocate or resize buffer (with full mip-chain) // TODO: migrate to inverse depth and try using r16 again as default (should have no artifacts anymore) - auto format = PLATFORM_ANDROID || PLATFORM_IOS || PLATFORM_SWITCH ? PixelFormat::R16_UInt : PixelFormat::R32_Float; + auto format = PLATFORM_WEB || PLATFORM_ANDROID || PLATFORM_IOS || PLATFORM_SWITCH ? PixelFormat::R16_UInt : PixelFormat::R32_Float; auto width = fullRes ? _width : Math::Max(_width >> 1, 1); auto height = fullRes ? _height : Math::Max(_height >> 1, 1); auto desc = GPUTextureDescription::New2D(width, height, mipLevels, format, GPUTextureFlags::ShaderResource); diff --git a/Source/Engine/Main/Main.Build.cs b/Source/Engine/Main/Main.Build.cs index 194f268a1..20a4dad75 100644 --- a/Source/Engine/Main/Main.Build.cs +++ b/Source/Engine/Main/Main.Build.cs @@ -74,6 +74,9 @@ public class Main : EngineModule case TargetPlatform.iOS: options.SourcePaths.Add(Path.Combine(FolderPath, "Default")); break; + case TargetPlatform.Web: + options.SourcePaths.Add(Path.Combine(FolderPath, "Web")); + break; default: throw new InvalidPlatformException(options.Platform.Target); } } diff --git a/Source/Engine/Main/Web/main.cpp b/Source/Engine/Main/Web/main.cpp new file mode 100644 index 000000000..5ea325542 --- /dev/null +++ b/Source/Engine/Main/Web/main.cpp @@ -0,0 +1,12 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#if PLATFORM_WEB + +#include "Engine/Engine/Engine.h" + +int main() +{ + return Engine::Main(TEXT("")); +} + +#endif diff --git a/Source/Engine/Physics/Physics.Build.cs b/Source/Engine/Physics/Physics.Build.cs index f735a31d2..f31afcffa 100644 --- a/Source/Engine/Physics/Physics.Build.cs +++ b/Source/Engine/Physics/Physics.Build.cs @@ -39,6 +39,12 @@ public class Physics : EngineModule { base.Setup(options); + if (options.Platform.Target == TargetPlatform.Web) // TODO: build PhysX for Web + { + options.PrivateDefinitions.Add("COMPILE_WITH_EMPTY_PHYSICS"); + return; + } + SetupPhysicsBackend(this, options); if (WithCooking) diff --git a/Source/Engine/Platform/Android/AndroidPlatform.h b/Source/Engine/Platform/Android/AndroidPlatform.h index a1491fb06..b55deadd9 100644 --- a/Source/Engine/Platform/Android/AndroidPlatform.h +++ b/Source/Engine/Platform/Android/AndroidPlatform.h @@ -99,11 +99,6 @@ public: static void Yield(); static double GetTimeSeconds(); static uint64 GetTimeCycles(); - FORCE_INLINE static uint64 GetClockFrequency() - { - // Dummy value - return 1000000; - } static void GetSystemTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond); static void GetUTCTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond); static bool Init(); diff --git a/Source/Engine/Platform/Base/PlatformBase.cpp b/Source/Engine/Platform/Base/PlatformBase.cpp index 29d13a2b0..290863fce 100644 --- a/Source/Engine/Platform/Base/PlatformBase.cpp +++ b/Source/Engine/Platform/Base/PlatformBase.cpp @@ -44,7 +44,7 @@ static_assert(sizeof(double) == 8, "Invalid double type size."); // Check configuration static_assert((PLATFORM_THREADS_LIMIT & (PLATFORM_THREADS_LIMIT - 1)) == 0, "Threads limit must be power of two."); -static_assert(PLATFORM_THREADS_LIMIT % 4 == 0, "Threads limit must be multiple of 4."); +static_assert(PLATFORM_THREADS_LIMIT % 4 == 0 || PLATFORM_THREADS_LIMIT == 1, "Threads limit must be multiple of 4."); const Char* PlatformBase::ApplicationClassName = TEXT("FlaxWindow"); float PlatformBase::CustomDpiScale = 1.0f; @@ -301,6 +301,15 @@ bool PlatformBase::Is64BitApp() #endif } +bool PlatformBase::Is64BitPlatform() +{ +#if PLATFORM_64BITS + return true; +#else + return false; +#endif +} + int32 PlatformBase::GetCacheLineSize() { return (int32)Platform::GetCPUInfo().CacheLineSize; @@ -820,6 +829,8 @@ const Char* ToString(PlatformType type) return TEXT("Mac"); case PlatformType::iOS: return TEXT("iOS"); + case PlatformType::Web: + return TEXT("Web"); default: return TEXT(""); } diff --git a/Source/Engine/Platform/Base/PlatformBase.h b/Source/Engine/Platform/Base/PlatformBase.h index 535c825a1..f8e072f2f 100644 --- a/Source/Engine/Platform/Base/PlatformBase.h +++ b/Source/Engine/Platform/Base/PlatformBase.h @@ -378,7 +378,7 @@ public: /// Returns true if running on 64-bit computer /// /// True if running on 64-bit computer, otherwise false. - API_PROPERTY() static bool Is64BitPlatform() = delete; + API_PROPERTY() static bool Is64BitPlatform(); /// /// Gets the name of the operating system. @@ -470,7 +470,11 @@ public: /// Gets the system clock frequency. /// /// The clock frequency. - API_PROPERTY() static uint64 GetClockFrequency() = delete; + API_PROPERTY() static uint64 GetClockFrequency() + { + // Dummy value + return 1000000; + } /// /// Gets current system time based on current computer settings. diff --git a/Source/Engine/Platform/Base/ThreadBase.cpp b/Source/Engine/Platform/Base/ThreadBase.cpp index 6776bc818..6cbb32f8b 100644 --- a/Source/Engine/Platform/Base/ThreadBase.cpp +++ b/Source/Engine/Platform/Base/ThreadBase.cpp @@ -48,7 +48,7 @@ void ThreadBase::Kill(bool waitForJoin) return; } ASSERT(GetID()); - const auto thread = static_cast(this); + Thread* thread = (Thread*)this; // Stop runnable object if (_callAfterWork && _runnable) diff --git a/Source/Engine/Platform/ConditionVariable.h b/Source/Engine/Platform/ConditionVariable.h index 0bb707317..69b19b407 100644 --- a/Source/Engine/Platform/ConditionVariable.h +++ b/Source/Engine/Platform/ConditionVariable.h @@ -4,7 +4,7 @@ #if PLATFORM_WINDOWS || PLATFORM_UWP || PLATFORM_XBOX_ONE || PLATFORM_XBOX_SCARLETT #include "Win32/Win32ConditionVariable.h" -#elif PLATFORM_LINUX || PLATFORM_ANDROID || PLATFORM_PS4 || PLATFORM_PS5 || PLATFORM_MAC || PLATFORM_IOS +#elif PLATFORM_LINUX || PLATFORM_ANDROID || PLATFORM_PS4 || PLATFORM_PS5 || PLATFORM_MAC || PLATFORM_IOS || PLATFORM_WEB #include "Unix/UnixConditionVariable.h" #elif PLATFORM_SWITCH #include "Platforms/Switch/Engine/Platform/SwitchConditionVariable.h" diff --git a/Source/Engine/Platform/CriticalSection.h b/Source/Engine/Platform/CriticalSection.h index 317aff994..f9637b38d 100644 --- a/Source/Engine/Platform/CriticalSection.h +++ b/Source/Engine/Platform/CriticalSection.h @@ -4,7 +4,7 @@ #if PLATFORM_WINDOWS || PLATFORM_UWP || PLATFORM_XBOX_ONE || PLATFORM_XBOX_SCARLETT #include "Win32/Win32CriticalSection.h" -#elif PLATFORM_LINUX || PLATFORM_ANDROID || PLATFORM_PS4 || PLATFORM_PS5 || PLATFORM_MAC || PLATFORM_IOS +#elif PLATFORM_LINUX || PLATFORM_ANDROID || PLATFORM_PS4 || PLATFORM_PS5 || PLATFORM_MAC || PLATFORM_IOS || PLATFORM_WEB #include "Unix/UnixCriticalSection.h" #elif PLATFORM_SWITCH #include "Platforms/Switch/Engine/Platform/SwitchCriticalSection.h" diff --git a/Source/Engine/Platform/Defines.h b/Source/Engine/Platform/Defines.h index d6e817bc6..ce4fbcb5c 100644 --- a/Source/Engine/Platform/Defines.h +++ b/Source/Engine/Platform/Defines.h @@ -67,6 +67,11 @@ API_ENUM() enum class PlatformType /// API_ENUM(Attributes="EditorDisplay(null, \"iOS\")") iOS = 11, + + /// + /// Running on Web. + /// + Web = 12, }; /// @@ -143,6 +148,9 @@ API_ENUM() enum class ArchitectureType #if !defined(PLATFORM_SDL) #define PLATFORM_SDL 0 #endif +#if !defined(PLATFORM_WEB) +#define PLATFORM_WEB 0 +#endif #if PLATFORM_WINDOWS #include "Windows/WindowsDefines.h" @@ -166,6 +174,8 @@ API_ENUM() enum class ArchitectureType #include "Mac/MacDefines.h" #elif PLATFORM_IOS #include "iOS/iOSDefines.h" +#elif PLATFORM_WEB +#include "Web/WebDefines.h" #else #error Missing Defines implementation! #endif diff --git a/Source/Engine/Platform/File.h b/Source/Engine/Platform/File.h index 3177ead03..e03d5902a 100644 --- a/Source/Engine/Platform/File.h +++ b/Source/Engine/Platform/File.h @@ -4,7 +4,7 @@ #if PLATFORM_WINDOWS || PLATFORM_UWP || PLATFORM_XBOX_ONE || PLATFORM_XBOX_SCARLETT #include "Win32/Win32File.h" -#elif PLATFORM_LINUX || PLATFORM_PS4 || PLATFORM_PS5 || PLATFORM_MAC +#elif PLATFORM_LINUX || PLATFORM_PS4 || PLATFORM_PS5 || PLATFORM_MAC || PLATFORM_WEB #include "Unix/UnixFile.h" #elif PLATFORM_IOS #include "iOS/iOSFile.h" diff --git a/Source/Engine/Platform/FileSystem.h b/Source/Engine/Platform/FileSystem.h index 3d7b94860..a2a0e4e17 100644 --- a/Source/Engine/Platform/FileSystem.h +++ b/Source/Engine/Platform/FileSystem.h @@ -24,6 +24,8 @@ #include "Mac/MacFileSystem.h" #elif PLATFORM_IOS #include "iOS/iOSFileSystem.h" +#elif PLATFORM_WEB +#include "Web/WebFileSystem.h" #else #error Missing File System implementation! #endif diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.h b/Source/Engine/Platform/Linux/LinuxPlatform.h index 6c857ccf9..841dcb1e3 100644 --- a/Source/Engine/Platform/Linux/LinuxPlatform.h +++ b/Source/Engine/Platform/Linux/LinuxPlatform.h @@ -114,11 +114,6 @@ public: static void Yield(); static double GetTimeSeconds(); static uint64 GetTimeCycles(); - FORCE_INLINE static uint64 GetClockFrequency() - { - // Dummy value - return 1000000; - } static void GetSystemTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond); static void GetUTCTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond); #if !BUILD_RELEASE diff --git a/Source/Engine/Platform/Network.h b/Source/Engine/Platform/Network.h index 7beb42622..26a754525 100644 --- a/Source/Engine/Platform/Network.h +++ b/Source/Engine/Platform/Network.h @@ -4,7 +4,7 @@ #if PLATFORM_WINDOWS || PLATFORM_UWP || PLATFORM_XBOX_ONE || PLATFORM_XBOX_SCARLETT #include "Win32/Win32Network.h" -#elif PLATFORM_LINUX || PLATFORM_ANDROID || PLATFORM_MAC || PLATFORM_IOS +#elif PLATFORM_LINUX || PLATFORM_ANDROID || PLATFORM_MAC || PLATFORM_IOS || PLATFORM_WEB #include "Unix/UnixNetwork.h" #elif PLATFORM_PS4 #include "Platforms/PS4/Engine/Platform/PS4Network.h" diff --git a/Source/Engine/Platform/Platform.Build.cs b/Source/Engine/Platform/Platform.Build.cs index 7272270c0..dc976088c 100644 --- a/Source/Engine/Platform/Platform.Build.cs +++ b/Source/Engine/Platform/Platform.Build.cs @@ -87,20 +87,17 @@ public class Platform : EngineModule options.SourcePaths.Add(Path.Combine(FolderPath, "Apple")); options.SourcePaths.Add(Path.Combine(FolderPath, "iOS")); break; + case TargetPlatform.Web: + options.SourcePaths.Add(Path.Combine(FolderPath, "Unix")); + options.SourcePaths.Add(Path.Combine(FolderPath, "Web")); + break; default: throw new InvalidPlatformException(options.Platform.Target); } if (EngineConfiguration.WithSDL(options)) { - switch (options.Platform.Target) - { - case TargetPlatform.Windows: - case TargetPlatform.Linux: - case TargetPlatform.Mac: - options.PublicDependencies.Add("SDL"); - options.SourcePaths.Add(Path.Combine(FolderPath, "SDL")); - break; - } + options.PublicDependencies.Add("SDL"); + options.SourcePaths.Add(Path.Combine(FolderPath, "SDL")); if (options.Platform.Target == TargetPlatform.Linux) options.PublicDependencies.Add("Wayland"); } @@ -115,6 +112,7 @@ public class Platform : EngineModule options.SourceFiles.Add(Path.Combine(FolderPath, "Apple", "ApplePlatformSettings.h")); options.SourceFiles.Add(Path.Combine(FolderPath, "Mac", "MacPlatformSettings.h")); options.SourceFiles.Add(Path.Combine(FolderPath, "iOS", "iOSPlatformSettings.h")); + options.SourceFiles.Add(Path.Combine(FolderPath, "Web", "WebPlatformSettings.h")); AddSourceFileIfExists(options, Path.Combine(Globals.EngineRoot, "Source", "Platforms", "XboxOne", "Engine", "Platform", "XboxOnePlatformSettings.h")); AddSourceFileIfExists(options, Path.Combine(Globals.EngineRoot, "Source", "Platforms", "XboxScarlett", "Engine", "Platform", "XboxScarlettPlatformSettings.h")); AddSourceFileIfExists(options, Path.Combine(Globals.EngineRoot, "Source", "Platforms", "PS4", "Engine", "Platform", "PS4PlatformSettings.h")); diff --git a/Source/Engine/Platform/Platform.h b/Source/Engine/Platform/Platform.h index 279e4141f..e2c6808c9 100644 --- a/Source/Engine/Platform/Platform.h +++ b/Source/Engine/Platform/Platform.h @@ -32,6 +32,8 @@ #include "Mac/MacPlatform.h" #elif PLATFORM_IOS #include "iOS/iOSPlatform.h" +#elif PLATFORM_WEB +#include "Web/WebPlatform.h" #else #error Missing Platform implementation! #endif diff --git a/Source/Engine/Platform/ReadWriteLock.h b/Source/Engine/Platform/ReadWriteLock.h index 5386e6ab3..a037a5b22 100644 --- a/Source/Engine/Platform/ReadWriteLock.h +++ b/Source/Engine/Platform/ReadWriteLock.h @@ -4,7 +4,7 @@ #if PLATFORM_WINDOWS || PLATFORM_UWP || PLATFORM_XBOX_ONE || PLATFORM_XBOX_SCARLETT #include "Win32/Win32ReadWriteLock.h" -#elif PLATFORM_LINUX || PLATFORM_ANDROID || PLATFORM_PS4 || PLATFORM_PS5 || PLATFORM_MAC || PLATFORM_IOS +#elif PLATFORM_LINUX || PLATFORM_ANDROID || PLATFORM_PS4 || PLATFORM_PS5 || PLATFORM_MAC || PLATFORM_IOS || PLATFORM_WEB #include "Unix/UnixReadWriteLock.h" #elif PLATFORM_SWITCH #include "Platforms/Switch/Engine/Platform/SwitchReadWriteLock.h" diff --git a/Source/Engine/Platform/SDL/SDLPlatform.Web.cpp b/Source/Engine/Platform/SDL/SDLPlatform.Web.cpp new file mode 100644 index 000000000..cf9523e50 --- /dev/null +++ b/Source/Engine/Platform/SDL/SDLPlatform.Web.cpp @@ -0,0 +1,207 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#if PLATFORM_SDL && PLATFORM_WEB + +#include "SDLWindow.h" +#include "Engine/Platform/MessageBox.h" +#include "Engine/Core/Log.h" +#include +#include + +bool SDLPlatform::InitInternal() +{ + return false; +} + +bool SDLPlatform::UsesWindows() +{ + return false; +} + +bool SDLPlatform::UsesWayland() +{ + return false; +} + +bool SDLPlatform::UsesX11() +{ + return false; +} + +void SDLPlatform::PreHandleEvents() +{ +} + +void SDLPlatform::PostHandleEvents() +{ +} + +bool SDLWindow::HandleEventInternal(SDL_Event& event) +{ + return false; +} + +void SDLPlatform::SetHighDpiAwarenessEnabled(bool enable) +{ +} + +DragDropEffect SDLWindow::DoDragDrop(const StringView& data) +{ + return DragDropEffect::None; +} + +DragDropEffect SDLWindow::DoDragDrop(const StringView& data, const Float2& offset, Window* dragSourceWindow) +{ + Show(); + return DragDropEffect::None; +} + +DialogResult MessageBox::Show(Window* parent, const StringView& text, const StringView& caption, MessageBoxButtons buttons, MessageBoxIcon icon) +{ + StringAnsi textAnsi(text); + StringAnsi captionAnsi(caption); + + SDL_MessageBoxData data; + SDL_MessageBoxButtonData dataButtons[3]; + data.window = nullptr; + data.title = captionAnsi.GetText(); + data.message = textAnsi.GetText(); + data.colorScheme = nullptr; + + switch (icon) + { + case MessageBoxIcon::Error: + case MessageBoxIcon::Hand: + case MessageBoxIcon::Stop: + data.flags |= SDL_MESSAGEBOX_ERROR; + break; + case MessageBoxIcon::Asterisk: + case MessageBoxIcon::Information: + case MessageBoxIcon::Question: + data.flags |= SDL_MESSAGEBOX_INFORMATION; + break; + case MessageBoxIcon::Exclamation: + case MessageBoxIcon::Warning: + data.flags |= SDL_MESSAGEBOX_WARNING; + break; + default: + break; + } + + switch (buttons) + { + case MessageBoxButtons::AbortRetryIgnore: + dataButtons[0] = + { + SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, + (int)DialogResult::Abort, + "Abort" + }; + dataButtons[1] = + { + SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, + (int)DialogResult::Retry, + "Retry" + }; + dataButtons[2] = + { + 0, + (int)DialogResult::Ignore, + "Ignore" + }; + data.numbuttons = 3; + break; + case MessageBoxButtons::OK: + dataButtons[0] = + { + SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT | SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, + (int)DialogResult::OK, + "OK" + }; + data.numbuttons = 1; + break; + case MessageBoxButtons::OKCancel: + dataButtons[0] = + { + SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, + (int)DialogResult::OK, + "OK" + }; + dataButtons[1] = + { + SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, + (int)DialogResult::Cancel, + "Cancel" + }; + data.numbuttons = 2; + break; + case MessageBoxButtons::RetryCancel: + dataButtons[0] = + { + SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, + (int)DialogResult::Retry, + "Retry" + }; + dataButtons[1] = + { + SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, + (int)DialogResult::Cancel, + "Cancel" + }; + data.numbuttons = 2; + break; + case MessageBoxButtons::YesNo: + dataButtons[0] = + { + SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, + (int)DialogResult::Yes, + "Yes" + }; + dataButtons[1] = + { + SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, + (int)DialogResult::No, + "No" + }; + data.numbuttons = 2; + break; + case MessageBoxButtons::YesNoCancel: + { + dataButtons[0] = + { + SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, + (int)DialogResult::Yes, + "Yes" + }; + dataButtons[1] = + { + 0, + (int)DialogResult::No, + "No" + }; + dataButtons[2] = + { + SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, + (int)DialogResult::Cancel, + "Cancel" + }; + data.numbuttons = 3; + break; + } + default: + break; + } + data.buttons = dataButtons; + + int result = -1; + if (!SDL_ShowMessageBox(&data, &result)) + { + LOG(Error, "Failed to show SDL message box: {0}", String(SDL_GetError())); + return DialogResult::Abort; + } + if (result < 0) + return DialogResult::None; + return (DialogResult)result; +} + +#endif diff --git a/Source/Engine/Platform/SDL/SDLPlatform.cpp b/Source/Engine/Platform/SDL/SDLPlatform.cpp index 8f9d4e967..3a15e116f 100644 --- a/Source/Engine/Platform/SDL/SDLPlatform.cpp +++ b/Source/Engine/Platform/SDL/SDLPlatform.cpp @@ -32,7 +32,7 @@ namespace SDLImpl { int32 SystemDpi = 96; -#if PLATFORM_LINUX +#if PLATFORM_LINUX || PLATFORM_WEB String UserLocale("en"); #endif bool WindowDecorationsSupported = true; @@ -109,7 +109,7 @@ bool SDLPlatform::Init() if (!SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) Platform::Fatal(String::Format(TEXT("Failed to initialize SDL: {0}."), String(SDL_GetError()))); -#if PLATFORM_LINUX +#if PLATFORM_LINUX || PLATFORM_WEB int localesCount = 0; auto locales = SDL_GetPreferredLocales(&localesCount); for (int i = 0; i < localesCount; i++) @@ -241,7 +241,7 @@ int32 SDLPlatform::GetDpi() return SDLImpl::SystemDpi; } -#if PLATFORM_LINUX +#if PLATFORM_LINUX || PLATFORM_WEB String SDLPlatform::GetUserLocaleName() { return SDLImpl::UserLocale; @@ -435,8 +435,10 @@ int32 SDLPlatform::CreateProcess(CreateProcessSettings& settings) SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, cmd.Get()); SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, env); SDL_SetBooleanProperty(props, SDL_PROP_PROCESS_CREATE_BACKGROUND_BOOLEAN, background); +#if !PLATFORM_WEB if (workingDirectory.HasChars()) SDL_SetStringProperty(props, SDL_PROP_PROCESS_CREATE_WORKING_DIRECTORY_STRING, workingDirectory.Get()); +#endif if (captureStdOut) { SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_APP); diff --git a/Source/Engine/Platform/SDL/SDLPlatform.h b/Source/Engine/Platform/SDL/SDLPlatform.h index bb8211485..526a58875 100644 --- a/Source/Engine/Platform/SDL/SDLPlatform.h +++ b/Source/Engine/Platform/SDL/SDLPlatform.h @@ -7,29 +7,25 @@ #include "Engine/Platform/Base/Enums.h" #if PLATFORM_WINDOWS #include "Engine/Platform/Windows/WindowsPlatform.h" +typedef WindowsPlatform SDLPlatformBase; typedef struct tagMSG MSG; #elif PLATFORM_LINUX #include "Engine/Platform/Linux/LinuxPlatform.h" +typedef LinuxPlatform SDLPlatformBase; union _XEvent; #elif PLATFORM_MAC #include "Engine/Platform/Mac/MacPlatform.h" +typedef MacPlatform SDLPlatformBase; +#elif PLATFORM_WEB +#include "Engine/Platform/Web/WebPlatform.h" +typedef WebPlatform SDLPlatformBase; #else -static_assert(false, "Unsupported Platform"); +static_assert(false, "Unsupported SDL platform."); #endif class SDLWindow; union SDL_Event; -#if PLATFORM_WINDOWS -typedef WindowsPlatform SDLPlatformBase; -#elif PLATFORM_LINUX -typedef LinuxPlatform SDLPlatformBase; -#elif PLATFORM_MAC -typedef MacPlatform SDLPlatformBase; -#else -static_assert(false, "Unsupported SDL platform."); -#endif - /// /// The SDL platform implementation and application management utilities. /// @@ -84,7 +80,7 @@ public: static BatteryInfo GetBatteryInfo(); #endif static int32 GetDpi(); -#if PLATFORM_LINUX +#if PLATFORM_LINUX || PLATFORM_WEB static String GetUserLocaleName(); #endif static bool CanOpenUrl(const StringView& url); diff --git a/Source/Engine/Platform/SDL/SDLWindow.cpp b/Source/Engine/Platform/SDL/SDLWindow.cpp index 45e686148..0b83d9c0b 100644 --- a/Source/Engine/Platform/SDL/SDLWindow.cpp +++ b/Source/Engine/Platform/SDL/SDLWindow.cpp @@ -38,6 +38,7 @@ #include "Engine/Platform/Linux/IncludeX11.h" #elif PLATFORM_MAC #include +#elif PLATFORM_WEB #else static_assert(false, "Unsupported Platform"); #endif @@ -77,6 +78,8 @@ void* GetNativeWindowPointer(SDL_Window* window) windowPtr = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_ANDROID_WINDOW_POINTER, nullptr); #elif PLATFORM_IOS windowPtr = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, nullptr); +#elif PLATFORM_WEB + windowPtr = (void*)1; // Mock value (TODO: consider SDL_PROP_WINDOW_EMSCRIPTEN_CANVAS_ID_STRING) #else static_assert(false, "unsupported platform"); #endif diff --git a/Source/Engine/Platform/Thread.h b/Source/Engine/Platform/Thread.h index 99a7daf25..b6408aa4c 100644 --- a/Source/Engine/Platform/Thread.h +++ b/Source/Engine/Platform/Thread.h @@ -16,6 +16,8 @@ #include "Platforms/Switch/Engine/Platform/SwitchThread.h" #elif PLATFORM_MAC || PLATFORM_IOS #include "Apple/AppleThread.h" +#elif PLATFORM_WEB +#include "Web/WebThread.h" #else #error Missing Thread implementation! #endif diff --git a/Source/Engine/Platform/Types.h b/Source/Engine/Platform/Types.h index 7a008abc8..cd5c5ce23 100644 --- a/Source/Engine/Platform/Types.h +++ b/Source/Engine/Platform/Types.h @@ -305,6 +305,29 @@ typedef UnixNetwork Network; class UserBase; typedef UserBase User; +#elif PLATFORM_WEB + +class UnixCriticalSection; +typedef UnixCriticalSection CriticalSection; +class UnixReadWriteLock; +typedef UnixReadWriteLock ReadWriteLock; +class UnixConditionVariable; +typedef UnixConditionVariable ConditionVariable; +class WebFileSystem; +typedef WebFileSystem FileSystem; +class FileSystemWatcherBase; +typedef FileSystemWatcherBase FileSystemWatcher; +class UnixFile; +typedef UnixFile File; +class WebThread; +typedef WebThread Thread; +class ClipboardBase; +typedef ClipboardBase Clipboard; +class UnixNetwork; +typedef UnixNetwork Network; +class UserBase; +typedef UserBase User; + #else #error Missing Types implementation! diff --git a/Source/Engine/Platform/Web/WebDefines.h b/Source/Engine/Platform/Web/WebDefines.h new file mode 100644 index 000000000..edfff3c04 --- /dev/null +++ b/Source/Engine/Platform/Web/WebDefines.h @@ -0,0 +1,30 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#if PLATFORM_WEB + +#include "../Unix/UnixDefines.h" + +// Platform description +#define PLATFORM_TYPE PlatformType::Web +#define PLATFORM_64BITS 0 +#define PLATFORM_ARCH ArchitectureType::x86 +#define PLATFORM_CACHE_LINE_SIZE 64 +#define PLATFORM_DEBUG_BREAK +#define PLATFORM_OUT_OF_MEMORY_BUFFER_SIZE 0 + +// Configure graphics +#define GPU_ALLOW_TESSELLATION_SHADERS 0 +#define GPU_ALLOW_GEOMETRY_SHADERS 0 +#define GPU_ALLOW_PROFILE_EVENTS 0 + +// Threading is optional +#ifdef __EMSCRIPTEN_PTHREADS__ +#define PLATFORM_THREADS_LIMIT 4 +#else +#define PLATFORM_THREADS_LIMIT 1 +#define GPU_ENABLE_ASYNC_RESOURCES_CREATION 0 +#endif + +#endif diff --git a/Source/Engine/Platform/Web/WebFileSystem.h b/Source/Engine/Platform/Web/WebFileSystem.h new file mode 100644 index 000000000..0d196f233 --- /dev/null +++ b/Source/Engine/Platform/Web/WebFileSystem.h @@ -0,0 +1,19 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#if PLATFORM_WEB + +#include "Engine/Platform/Unix/UnixFileSystem.h" + +/// +/// Web platform implementation of filesystem service. +/// +class FLAXENGINE_API WebFileSystem : public UnixFileSystem +{ +public: + // [UnixFileSystem] + static void GetSpecialFolderPath(const SpecialFolder type, String& result); +}; + +#endif diff --git a/Source/Engine/Platform/Web/WebPlatform.cpp b/Source/Engine/Platform/Web/WebPlatform.cpp new file mode 100644 index 000000000..8c7aed48f --- /dev/null +++ b/Source/Engine/Platform/Web/WebPlatform.cpp @@ -0,0 +1,254 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#if PLATFORM_WEB + +#include "WebPlatform.h" +#include "WebFileSystem.h" +#include "Engine/Core/Log.h" +#include "Engine/Core/Types/String.h" +#include "Engine/Core/Types/Version.h" +#include "Engine/Core/Types/TimeSpan.h" +#include "Engine/Core/Types/Guid.h" +#include "Engine/Platform/CPUInfo.h" +#include "Engine/Platform/MemoryStats.h" +#if !BUILD_RELEASE +#include "Engine/Core/Types/StringView.h" +#include "Engine/Utilities/StringConverter.h" +#endif +#include +#include +#include +#include +#include + +namespace +{ + CPUInfo Cpu; +}; + +void WebFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& result) +{ + result = TEXT("/"); +} + +String WebPlatform::GetSystemName() +{ + return TEXT("Browser"); +} + +Version WebPlatform::GetSystemVersion() +{ + return Version(1, 0); +} + +CPUInfo WebPlatform::GetCPUInfo() +{ + return Cpu; +} + +MemoryStats WebPlatform::GetMemoryStats() +{ + // Mock memory stats + MemoryStats result; + result.TotalPhysicalMemory = emscripten_get_heap_max(); + result.UsedPhysicalMemory = emscripten_get_heap_size(); + result.TotalVirtualMemory = result.TotalPhysicalMemory; + result.UsedVirtualMemory = result.UsedPhysicalMemory; + result.ProgramSizeMemory = 0; + return result; +} + +ProcessMemoryStats WebPlatform::GetProcessMemoryStats() +{ + // Mock memory stats + ProcessMemoryStats result; + result.UsedPhysicalMemory = 1 * 1024 * 1024; + result.UsedVirtualMemory = result.UsedPhysicalMemory; + return result; +} + +void WebPlatform::SetThreadPriority(ThreadPriority priority) +{ + // Not supported +} + +void WebPlatform::SetThreadAffinityMask(uint64 affinityMask) +{ + // Not supported +} + +void WebPlatform::Sleep(int32 milliseconds) +{ + //emscripten_sleep(milliseconds); + emscripten_thread_sleep(milliseconds); +} + +void WebPlatform::Yield() +{ + Sleep(0); +} + +double WebPlatform::GetTimeSeconds() +{ + double time = emscripten_get_now(); + return time * 0.001; +} + +uint64 WebPlatform::GetTimeCycles() +{ + return (uint64)(emscripten_get_now() * 1000.0); +} + +void WebPlatform::GetSystemTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond) +{ + // Get local time + using namespace std::chrono; + system_clock::time_point now = system_clock::now(); + time_t tt = system_clock::to_time_t(now); + tm time = *localtime(&tt); + + // Extract time + year = time.tm_year + 1900; + month = time.tm_mon + 1; + dayOfWeek = time.tm_wday; + day = time.tm_mday; + hour = time.tm_hour; + minute = time.tm_min; + second = time.tm_sec; + millisecond = (int64)emscripten_get_now() % 1000; // Fake it based on other timer +} + +void WebPlatform::GetUTCTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond) +{ + // Get UTC time + using namespace std::chrono; + system_clock::time_point now = system_clock::now(); + time_t tt = system_clock::to_time_t(now); + tm time = *gmtime(&tt); + + // Extract time + year = time.tm_year + 1900; + month = time.tm_mon + 1; + dayOfWeek = time.tm_wday; + day = time.tm_mday; + hour = time.tm_hour; + minute = time.tm_min; + second = time.tm_sec; + millisecond = (int64)emscripten_get_now() % 1000; // Fake it based on other timer +} + +#if !BUILD_RELEASE + +void WebPlatform::Log(const StringView& msg) +{ + const StringAsANSI<512> msgAnsi(*msg, msg.Length()); + + // Fix % characters that should not be formatted + auto buffer = (char*)msgAnsi.Get(); + for (int32 i = 0; buffer[i]; i++) + { + if (buffer[i] == '%') + buffer[i] = 'p'; + } + + emscripten_log(EM_LOG_CONSOLE, buffer); +} + +bool WebPlatform::IsDebuggerPresent() +{ + return false; +} + +#endif + +String WebPlatform::GetComputerName() +{ + return TEXT("Web"); +} + +bool WebPlatform::GetHasFocus() +{ + return true; +} + +String WebPlatform::GetMainDirectory() +{ + return TEXT("/"); +} + +String WebPlatform::GetExecutableFilePath() +{ + return TEXT("/index.html"); +} + +Guid WebPlatform::GetUniqueDeviceId() +{ + return Guid(1, 2, 3, 4); +} + +String WebPlatform::GetWorkingDirectory() +{ + return GetMainDirectory(); +} + +bool WebPlatform::SetWorkingDirectory(const String& path) +{ + return true; +} + +bool WebPlatform::Init() +{ + if (PlatformBase::Init()) + return true; + + // Set info about the CPU + Platform::MemoryClear(&Cpu, sizeof(Cpu)); + Cpu.ProcessorPackageCount = 1; + Cpu.ProcessorCoreCount = Math::Min(emscripten_num_logical_cores(), PLATFORM_THREADS_LIMIT); + Cpu.LogicalProcessorCount = Cpu.ProcessorCoreCount; + Cpu.ClockSpeed = GetClockFrequency(); + + return false; +} + +void WebPlatform::LogInfo() +{ + PlatformBase::LogInfo(); + +#ifdef __EMSCRIPTEN_major__ + LOG(Info, "Emscripten {}.{}.{}", __EMSCRIPTEN_major__, __EMSCRIPTEN_minor__, __EMSCRIPTEN_tiny__); +#elif defined(__EMSCRIPTEN_MAJOR__) + LOG(Info, "Emscripten {}.{}.{}", __EMSCRIPTEN_MAJOR__, __EMSCRIPTEN_MINOR__, __EMSCRIPTEN_TINY__); +#else + LOG(Info, "Emscripten"); +#endif +#ifdef __EMSCRIPTEN_PTHREADS__ + LOG(Info, "Threading: pthreads"); +#else + LOG(Info, "Threading: disabled"); +#endif +} + +void WebPlatform::Tick() +{ +} + +void WebPlatform::Exit() +{ +} + +void* WebPlatform::LoadLibrary(const Char* filename) +{ + return nullptr; +} + +void WebPlatform::FreeLibrary(void* handle) +{ +} + +void* WebPlatform::GetProcAddress(void* handle, const char* symbol) +{ + return nullptr; +} + +#endif diff --git a/Source/Engine/Platform/Web/WebPlatform.h b/Source/Engine/Platform/Web/WebPlatform.h new file mode 100644 index 000000000..af4f3c146 --- /dev/null +++ b/Source/Engine/Platform/Web/WebPlatform.h @@ -0,0 +1,116 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#if PLATFORM_WEB + +#include "../Unix/UnixPlatform.h" +#ifdef __EMSCRIPTEN_PTHREADS__ +#include +#endif + +/// +/// The Web platform implementation and application management utilities. +/// +class FLAXENGINE_API WebPlatform : public UnixPlatform +{ +public: + // [UnixPlatform] + FORCE_INLINE static void MemoryBarrier() + { +#ifdef __EMSCRIPTEN_PTHREADS__ + // Fake a fence with an arbitrary atomic operation (from emscripten_atomic_fence to avoid including it for less header bloat) + uint8 temp = 0; + __c11_atomic_fetch_or((_Atomic uint8*)&temp, 0, __ATOMIC_SEQ_CST); +#endif + } + FORCE_INLINE static void MemoryPrefetch(void const* ptr) + { + __builtin_prefetch(static_cast(ptr)); + } + FORCE_INLINE static int64 InterlockedExchange(int64 volatile* dst, int64 exchange) + { + return __sync_lock_test_and_set(dst, exchange); + } + FORCE_INLINE static int32 InterlockedCompareExchange(int32 volatile* dst, int32 exchange, int32 comperand) + { + return __sync_val_compare_and_swap(dst, comperand, exchange); + } + FORCE_INLINE static int64 InterlockedCompareExchange(int64 volatile* dst, int64 exchange, int64 comperand) + { + return __sync_val_compare_and_swap(dst, comperand, exchange); + } + FORCE_INLINE static int64 InterlockedIncrement(int64 volatile* dst) + { + return __sync_add_and_fetch(dst, 1); + } + FORCE_INLINE static int64 InterlockedDecrement(int64 volatile* dst) + { + return __sync_sub_and_fetch(dst, 1); + } + FORCE_INLINE static int64 InterlockedAdd(int64 volatile* dst, int64 value) + { + return __sync_fetch_and_add(dst, value); + } + FORCE_INLINE static int32 AtomicRead(int32 const volatile* dst) + { + int32 result; + __atomic_load(dst, &result, __ATOMIC_SEQ_CST); + return result; + } + FORCE_INLINE static int64 AtomicRead(int64 const volatile* dst) + { + int64 result; + __atomic_load(dst, &result, __ATOMIC_SEQ_CST); + return result; + } + FORCE_INLINE static void AtomicStore(int32 volatile* dst, int32 value) + { + __atomic_store(dst, &value, __ATOMIC_SEQ_CST); + } + FORCE_INLINE static void AtomicStore(int64 volatile* dst, int64 value) + { + __atomic_store(dst, &value, __ATOMIC_SEQ_CST); + } + FORCE_INLINE static uint64 GetCurrentThreadID() + { +#ifdef __EMSCRIPTEN_PTHREADS__ + return (uint64)pthread_self(); +#else + return 0; +#endif + } + static String GetSystemName(); + static Version GetSystemVersion(); + static CPUInfo GetCPUInfo(); + static MemoryStats GetMemoryStats(); + static ProcessMemoryStats GetProcessMemoryStats(); + static void SetThreadPriority(ThreadPriority priority); + static void SetThreadAffinityMask(uint64 affinityMask); + static void Sleep(int32 milliseconds); + static void Yield(); + static double GetTimeSeconds(); + static uint64 GetTimeCycles(); + static void GetSystemTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond); + static void GetUTCTime(int32& year, int32& month, int32& dayOfWeek, int32& day, int32& hour, int32& minute, int32& second, int32& millisecond); +#if !BUILD_RELEASE + static void Log(const StringView& msg); + static bool IsDebuggerPresent(); +#endif + static String GetComputerName(); + static bool GetHasFocus(); + static String GetMainDirectory(); + static String GetExecutableFilePath(); + static Guid GetUniqueDeviceId(); + static String GetWorkingDirectory(); + static bool SetWorkingDirectory(const String& path); + static bool Init(); + static void LogInfo(); + static void Tick(); + static void Exit(); + static void* LoadLibrary(const Char* filename); + static void FreeLibrary(void* handle); + static void* GetProcAddress(void* handle, const char* symbol); +}; + +#endif diff --git a/Source/Engine/Platform/Web/WebPlatformSettings.h b/Source/Engine/Platform/Web/WebPlatformSettings.h new file mode 100644 index 000000000..8408e3bf7 --- /dev/null +++ b/Source/Engine/Platform/Web/WebPlatformSettings.h @@ -0,0 +1,27 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#if PLATFORM_WEB || USE_EDITOR + +#include "Engine/Core/Config/PlatformSettingsBase.h" + +/// +/// Web platform settings. +/// +API_CLASS(Sealed, Namespace="FlaxEditor.Content.Settings") class FLAXENGINE_API WebPlatformSettings : public SettingsBase +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(WebPlatformSettings); + API_AUTO_SERIALIZATION(); + + /// + /// Gets the instance of the settings asset (default value if missing). Object returned by this method is always loaded with valid data to use. + /// + static WebPlatformSettings* Get(); +}; + +#if PLATFORM_WEB +typedef WebPlatformSettings PlatformSettings; +#endif + +#endif diff --git a/Source/Engine/Platform/Web/WebThread.h b/Source/Engine/Platform/Web/WebThread.h new file mode 100644 index 000000000..a5086787b --- /dev/null +++ b/Source/Engine/Platform/Web/WebThread.h @@ -0,0 +1,63 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#if PLATFORM_WEB + +#include "../Base/ThreadBase.h" + +/// +/// Thread object for Web platform. +/// +class FLAXENGINE_API WebThread : public ThreadBase +{ +public: + /// + /// Initializes a new instance of the class. + /// + /// The runnable. + /// The thread name. + /// The thread priority. + WebThread(IRunnable* runnable, const String& name, ThreadPriority priority) + : ThreadBase(runnable, name, priority) + { + } + +public: + /// + /// Factory method to create a thread with the specified stack size and thread priority + /// + /// The runnable object to execute + /// Name of the thread + /// Tells the thread whether it needs to adjust its priority or not. Defaults to normal priority + /// The size of the stack to create. 0 means use the current thread's stack size + /// Pointer to the new thread or null if cannot create it + static WebThread* Create(IRunnable* runnable, const String& name, ThreadPriority priority = ThreadPriority::Normal, uint32 stackSize = 0) + { + return New(runnable, name, priority); + } + +public: + // [ThreadBase] + void Join() override + { + // TOOD: impl this + } + +protected: + // [ThreadBase] + void ClearHandleInternal() override + { + // TOOD: impl this + } + void SetPriorityInternal(ThreadPriority priority) override + { + // TOOD: impl this + } + void KillInternal(bool waitForJoin) override + { + // TOOD: impl this + } +}; + +#endif diff --git a/Source/Engine/Profiler/Profiler.Build.cs b/Source/Engine/Profiler/Profiler.Build.cs index 013336181..b1bdd85b8 100644 --- a/Source/Engine/Profiler/Profiler.Build.cs +++ b/Source/Engine/Profiler/Profiler.Build.cs @@ -15,6 +15,8 @@ public class Profiler : EngineModule /// True if use profiler, otherwise false. public static bool Use(BuildOptions options) { + if (options.Platform.Target == TargetPlatform.Web) + return false; return options.Configuration != TargetConfiguration.Release || options.Target.IsEditor; } diff --git a/Source/Engine/Renderer/ScreenSpaceReflectionsPass.cpp b/Source/Engine/Renderer/ScreenSpaceReflectionsPass.cpp index 38d4e295c..be8970e7b 100644 --- a/Source/Engine/Renderer/ScreenSpaceReflectionsPass.cpp +++ b/Source/Engine/Renderer/ScreenSpaceReflectionsPass.cpp @@ -304,7 +304,7 @@ GPUTexture* ScreenSpaceReflectionsPass::Render(RenderContext& renderContext, GPU // Also for high surface roughness values it adds more blur to the reflection tail which looks more realistic. // Downscale with gaussian blur - auto filterMode = PLATFORM_ANDROID || PLATFORM_IOS || PLATFORM_SWITCH ? MultiScaler::FilterMode::GaussianBlur5 : MultiScaler::FilterMode::GaussianBlur9; + auto filterMode = PLATFORM_WEB || PLATFORM_ANDROID || PLATFORM_IOS || PLATFORM_SWITCH ? MultiScaler::FilterMode::GaussianBlur5 : MultiScaler::FilterMode::GaussianBlur9; for (int32 mipLevel = 1; mipLevel < colorBufferMips; mipLevel++) { const int32 mipWidth = Math::Max(colorBufferWidth >> mipLevel, 1); diff --git a/Source/Engine/Renderer/ShadowsPass.cpp b/Source/Engine/Renderer/ShadowsPass.cpp index 8825d2e8d..91f2d3832 100644 --- a/Source/Engine/Renderer/ShadowsPass.cpp +++ b/Source/Engine/Renderer/ShadowsPass.cpp @@ -784,7 +784,7 @@ void ShadowsPass::SetupLight(ShadowsCustomBuffer& shadows, RenderContext& render // Disable cascades blending when baking lightmaps if (IsRunningRadiancePass) atlasLight.BlendCSM = false; -#elif PLATFORM_SWITCH || PLATFORM_IOS || PLATFORM_ANDROID +#elif PLATFORM_WEB || PLATFORM_SWITCH || PLATFORM_IOS || PLATFORM_ANDROID // Disable cascades blending on low-end platforms atlasLight.BlendCSM = false; #endif diff --git a/Source/Engine/Threading/JobSystem.cpp b/Source/Engine/Threading/JobSystem.cpp index 8d62aa8e3..3cc9acc62 100644 --- a/Source/Engine/Threading/JobSystem.cpp +++ b/Source/Engine/Threading/JobSystem.cpp @@ -6,16 +6,13 @@ #include "Engine/Platform/Thread.h" #include "Engine/Platform/ConditionVariable.h" #include "Engine/Core/Types/Span.h" -#include "Engine/Core/Types/Pair.h" -#include "Engine/Core/Memory/SimpleHeapAllocation.h" -#include "Engine/Core/Collections/RingBuffer.h" #include "Engine/Engine/EngineService.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Profiler/ProfilerMemory.h" #if USE_CSHARP #include "Engine/Scripting/ManagedCLR/MCore.h" -#include "Engine/Scripting/Internal/InternalCalls.h" #endif +#include "Engine/Scripting/Internal/InternalCalls.h" #define JOB_SYSTEM_ENABLED 1 diff --git a/Source/Engine/Tools/TextureTool/TextureTool.Build.cs b/Source/Engine/Tools/TextureTool/TextureTool.Build.cs index ae15af8c3..bfb8fc0d7 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.Build.cs +++ b/Source/Engine/Tools/TextureTool/TextureTool.Build.cs @@ -38,6 +38,7 @@ public class TextureTool : EngineModule case TargetPlatform.Switch: case TargetPlatform.Mac: case TargetPlatform.iOS: + case TargetPlatform.Web: useStb = true; break; default: throw new InvalidPlatformException(options.Platform.Target); diff --git a/Source/Engine/Video/Video.Build.cs b/Source/Engine/Video/Video.Build.cs index 3975480ee..ed9ed7c4e 100644 --- a/Source/Engine/Video/Video.Build.cs +++ b/Source/Engine/Video/Video.Build.cs @@ -63,6 +63,10 @@ public class Video : EngineModule options.SourcePaths.Add(Path.Combine(FolderPath, "Android")); options.CompileEnv.PreprocessorDefinitions.Add("VIDEO_API_ANDROID"); break; + case TargetPlatform.Web: + // No implemented + break; + default: throw new InvalidPlatformException(options.Platform.Target); } } diff --git a/Source/Engine/Visject/ShaderGraph.cpp b/Source/Engine/Visject/ShaderGraph.cpp index faf344263..b297ad91e 100644 --- a/Source/Engine/Visject/ShaderGraph.cpp +++ b/Source/Engine/Visject/ShaderGraph.cpp @@ -796,6 +796,7 @@ void ShaderGenerator::ProcessGroupTools(Box* box, Node* node, Value& value) PLATFORM_CASE(10, "PLATFORM_PS5"); PLATFORM_CASE(11, "PLATFORM_MAC"); PLATFORM_CASE(12, "PLATFORM_IOS"); + PLATFORM_CASE(13, "PLATFORM_WEB"); #undef PLATFORM_CASE break; } diff --git a/Source/Engine/Visject/VisjectGraph.cpp b/Source/Engine/Visject/VisjectGraph.cpp index 7d3505d41..ec4f51364 100644 --- a/Source/Engine/Visject/VisjectGraph.cpp +++ b/Source/Engine/Visject/VisjectGraph.cpp @@ -1015,6 +1015,9 @@ void VisjectExecutor::ProcessGroupTools(Box* box, Node* node, Value& value) case PlatformType::iOS: boxId = 12; break; + case PlatformType::Web: + boxId = 13; + break; default: ; } value = tryGetValue(node->GetBox(node->GetBox(boxId)->HasConnection() ? boxId : 1), Value::Zero); diff --git a/Source/Shaders/Reflections.shader b/Source/Shaders/Reflections.shader index 891411af1..11478c7d1 100644 --- a/Source/Shaders/Reflections.shader +++ b/Source/Shaders/Reflections.shader @@ -8,7 +8,7 @@ #include "./Flax/ReflectionsCommon.hlsl" // Enable/disable blurring SSR during sampling results and mixing with reflections buffer -#define SSR_MIX_BLUR (!defined(PLATFORM_ANDROID) && !defined(PLATFORM_IOS) && !defined(PLATFORM_SWITCH)) +#define SSR_MIX_BLUR (!defined(PLATFORM_WEB) && !defined(PLATFORM_ANDROID) && !defined(PLATFORM_IOS) && !defined(PLATFORM_SWITCH)) META_CB_BEGIN(0, Data) EnvProbeData PData; diff --git a/Source/ThirdParty/NvCloth/NvCloth.Build.cs b/Source/ThirdParty/NvCloth/NvCloth.Build.cs index af629c084..71e9a7e03 100644 --- a/Source/ThirdParty/NvCloth/NvCloth.Build.cs +++ b/Source/ThirdParty/NvCloth/NvCloth.Build.cs @@ -38,6 +38,7 @@ public class NvCloth : DepsModule case TargetPlatform.Mac: case TargetPlatform.iOS: case TargetPlatform.Linux: + case TargetPlatform.Web: libName = "NvCloth"; break; case TargetPlatform.Switch: diff --git a/Source/ThirdParty/SDL/SDL.Build.cs b/Source/ThirdParty/SDL/SDL.Build.cs index 91ce939f7..8b55f56c3 100644 --- a/Source/ThirdParty/SDL/SDL.Build.cs +++ b/Source/ThirdParty/SDL/SDL.Build.cs @@ -48,6 +48,9 @@ public class SDL : DepsModule case TargetPlatform.Mac: options.OutputFiles.Add(Path.Combine(depsRoot, "libSDL3.a")); break; + case TargetPlatform.Web: + options.OutputFiles.Add("--use-port=sdl3"); + return; default: throw new InvalidPlatformException(options.Platform.Target); } diff --git a/Source/ThirdParty/freetype/freetype.Build.cs b/Source/ThirdParty/freetype/freetype.Build.cs index f6c3380ec..85c1d8e89 100644 --- a/Source/ThirdParty/freetype/freetype.Build.cs +++ b/Source/ThirdParty/freetype/freetype.Build.cs @@ -45,6 +45,9 @@ public class freetype : DepsModule case TargetPlatform.iOS: options.OutputFiles.Add(Path.Combine(depsRoot, "libfreetype.a")); break; + case TargetPlatform.Web: + options.OutputFiles.Add("--use-port=freetype"); + break; default: throw new InvalidPlatformException(options.Platform.Target); } diff --git a/Source/ThirdParty/ogg/ogg.Build.cs b/Source/ThirdParty/ogg/ogg.Build.cs index 1a3a67fc0..f8b14bffa 100644 --- a/Source/ThirdParty/ogg/ogg.Build.cs +++ b/Source/ThirdParty/ogg/ogg.Build.cs @@ -45,6 +45,9 @@ public class ogg : DepsModule case TargetPlatform.iOS: options.OutputFiles.Add(Path.Combine(depsRoot, "libogg.a")); break; + case TargetPlatform.Web: + options.OutputFiles.Add("--use-port=ogg"); + break; default: throw new InvalidPlatformException(options.Platform.Target); } } diff --git a/Source/ThirdParty/vorbis/vorbis.Build.cs b/Source/ThirdParty/vorbis/vorbis.Build.cs index afdc04e60..edd55d886 100644 --- a/Source/ThirdParty/vorbis/vorbis.Build.cs +++ b/Source/ThirdParty/vorbis/vorbis.Build.cs @@ -51,6 +51,9 @@ public class vorbis : DepsModule case TargetPlatform.PS5: options.OutputFiles.Add(Path.Combine(depsRoot, "libvorbis.a")); break; + case TargetPlatform.Web: + options.OutputFiles.Add("--use-port=vorbis"); + break; default: throw new InvalidPlatformException(options.Platform.Target); } } diff --git a/Source/Tools/Flax.Build/Build/DepsModule.cs b/Source/Tools/Flax.Build/Build/DepsModule.cs index bdf9c47ac..8039eb36d 100644 --- a/Source/Tools/Flax.Build/Build/DepsModule.cs +++ b/Source/Tools/Flax.Build/Build/DepsModule.cs @@ -35,6 +35,7 @@ namespace Flax.Build case TargetPlatform.Switch: case TargetPlatform.Mac: case TargetPlatform.iOS: + case TargetPlatform.Web: options.OutputFiles.Add(Path.Combine(path, string.Format("lib{0}.a", name))); break; default: throw new InvalidPlatformException(options.Platform.Target); diff --git a/Source/Tools/Flax.Build/Build/Module.cs b/Source/Tools/Flax.Build/Build/Module.cs index 89a6ffa7f..bb11b2857 100644 --- a/Source/Tools/Flax.Build/Build/Module.cs +++ b/Source/Tools/Flax.Build/Build/Module.cs @@ -108,6 +108,7 @@ namespace Flax.Build case TargetPlatform.Switch: return "PLATFORM_SWITCH"; case TargetPlatform.Mac: return "PLATFORM_MAC"; case TargetPlatform.iOS: return "PLATFORM_IOS"; + case TargetPlatform.Web: return "PLATFORM_WEB"; default: throw new InvalidPlatformException(platform); } } diff --git a/Source/Tools/Flax.Build/Build/Platform.cs b/Source/Tools/Flax.Build/Build/Platform.cs index 59572d669..f64dc6ab9 100644 --- a/Source/Tools/Flax.Build/Build/Platform.cs +++ b/Source/Tools/Flax.Build/Build/Platform.cs @@ -170,7 +170,7 @@ namespace Flax.Build /// /// Gets the default project format used by the given platform. /// - public abstract Projects.ProjectFormat DefaultProjectFormat { get; } + public virtual Projects.ProjectFormat DefaultProjectFormat => Projects.ProjectFormat.VisualStudioCode; /// /// Creates the toolchain for a given architecture. @@ -343,6 +343,7 @@ namespace Flax.Build case TargetPlatform.Switch: return targetArchitecture == TargetArchitecture.ARM64; case TargetPlatform.Mac: return targetArchitecture == TargetArchitecture.ARM64 || targetArchitecture == TargetArchitecture.x64; case TargetPlatform.iOS: return targetArchitecture == TargetArchitecture.ARM64; + case TargetPlatform.Web: return targetArchitecture == TargetArchitecture.x86; default: return false; } } diff --git a/Source/Tools/Flax.Build/Build/TargetPlatform.cs b/Source/Tools/Flax.Build/Build/TargetPlatform.cs index 47889c1bf..e51c92674 100644 --- a/Source/Tools/Flax.Build/Build/TargetPlatform.cs +++ b/Source/Tools/Flax.Build/Build/TargetPlatform.cs @@ -61,6 +61,11 @@ namespace Flax.Build /// Running on iPhone. /// iOS = 11, + + /// + /// Running on Web. + /// + Web = 12, } /// diff --git a/Source/Tools/Flax.Build/Configuration.cs b/Source/Tools/Flax.Build/Configuration.cs index 5cd643b8f..9f2796767 100644 --- a/Source/Tools/Flax.Build/Configuration.cs +++ b/Source/Tools/Flax.Build/Configuration.cs @@ -296,6 +296,8 @@ namespace Flax.Build public static bool WithCSharp(NativeCpp.BuildOptions options) { + if (options.Platform.Target == TargetPlatform.Web) + return false; // TODO: implement .NET for WebAssembly return UseCSharp || options.Target.IsEditor; } @@ -318,6 +320,8 @@ namespace Flax.Build case TargetPlatform.Linux: case TargetPlatform.Mac: return UseSDL; + case TargetPlatform.Web: + return true; default: return false; } } diff --git a/Source/Tools/Flax.Build/Globals.cs b/Source/Tools/Flax.Build/Globals.cs index ad5c1f74e..033d2f40e 100644 --- a/Source/Tools/Flax.Build/Globals.cs +++ b/Source/Tools/Flax.Build/Globals.cs @@ -37,6 +37,7 @@ namespace Flax.Build TargetPlatform.Switch, TargetPlatform.Mac, TargetPlatform.iOS, + TargetPlatform.Web, }; /// diff --git a/Source/Tools/Flax.Build/Platforms/Linux/LinuxPlatform.cs b/Source/Tools/Flax.Build/Platforms/Linux/LinuxPlatform.cs index 5c8561106..2a9773f47 100644 --- a/Source/Tools/Flax.Build/Platforms/Linux/LinuxPlatform.cs +++ b/Source/Tools/Flax.Build/Platforms/Linux/LinuxPlatform.cs @@ -104,6 +104,7 @@ namespace Flax.Build.Platforms { case TargetPlatform.Linux: return HasRequiredSDKsInstalled; case TargetPlatform.Android: return AndroidSdk.Instance.IsValid && AndroidNdk.Instance.IsValid; + case TargetPlatform.Web: return EmscriptenSdk.Instance.IsValid; default: return false; } } diff --git a/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs b/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs index 8bd6b30ac..2da5f2494 100644 --- a/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs +++ b/Source/Tools/Flax.Build/Platforms/Mac/MacPlatform.cs @@ -42,6 +42,7 @@ namespace Flax.Build.Platforms { case TargetPlatform.iOS: case TargetPlatform.Mac: return HasRequiredSDKsInstalled; + case TargetPlatform.Web: return EmscriptenSdk.Instance.IsValid; default: return false; } } diff --git a/Source/Tools/Flax.Build/Platforms/Unix/UnixToolchain.cs b/Source/Tools/Flax.Build/Platforms/Unix/UnixToolchain.cs index 990219a89..2c0929c72 100644 --- a/Source/Tools/Flax.Build/Platforms/Unix/UnixToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Unix/UnixToolchain.cs @@ -414,7 +414,7 @@ namespace Flax.Build.Platforms // Language for the file args.Add("-x"); - if (Path.GetExtension(sourceFile).Equals(".c", StringComparison.OrdinalIgnoreCase)) + if (sourceFile.EndsWith(".c", StringComparison.OrdinalIgnoreCase)) args.Add("c"); else { diff --git a/Source/Tools/Flax.Build/Platforms/Web/EmscriptenSdk.cs b/Source/Tools/Flax.Build/Platforms/Web/EmscriptenSdk.cs new file mode 100644 index 000000000..946fc0c87 --- /dev/null +++ b/Source/Tools/Flax.Build/Platforms/Web/EmscriptenSdk.cs @@ -0,0 +1,87 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using System; +using System.IO; +using System.Linq; + +namespace Flax.Build.Platforms +{ + /// + /// The Emscripten SDK (https://emscripten.org/). + /// + /// + public sealed class EmscriptenSdk : Sdk + { + /// + /// The singleton instance. + /// + public static readonly EmscriptenSdk Instance = new EmscriptenSdk(); + + /// + public override TargetPlatform[] Platforms => new[] + { + TargetPlatform.Windows, + TargetPlatform.Linux, + TargetPlatform.Mac, + }; + + /// + /// Full path to the current SDK folder with binaries, tools and sources (eg. '%EMSDK%\upstream'). + /// + public string EmscriptenPath; + + /// + /// Initializes a new instance of the class. + /// + public EmscriptenSdk() + { + if (!Platforms.Contains(Platform.BuildTargetPlatform)) + return; + + // Find Emscripten SDK folder path + var sdkPath = Environment.GetEnvironmentVariable("EMSDK"); + if (string.IsNullOrEmpty(sdkPath)) + { + Log.Warning("Missing Emscripten SDK. Cannot build for Web platform."); + } + else if (!Directory.Exists(sdkPath)) + { + Log.Warning(string.Format("Specified Emscripten SDK folder in EMSDK env variable doesn't exist ({0})", sdkPath)); + } + else + { + RootPath = sdkPath; + EmscriptenPath = Path.Combine(sdkPath, "upstream"); + var versionPath = Path.Combine(EmscriptenPath, "emscripten", "emscripten-version.txt"); + if (File.Exists(versionPath)) + { + try + { + // Read version + var versionStr = File.ReadAllLines(versionPath)[0]; + versionStr = versionStr.Trim(); + if (versionStr.StartsWith('\"') && versionStr.EndsWith('\"')) + versionStr = versionStr.Substring(1, versionStr.Length - 2); + Version = new Version(versionStr); + + var minVersion = new Version(4, 0); + if (Version < minVersion) + { + Log.Error(string.Format("Unsupported Emscripten SDK version {0}. Minimum supported is {1}.", Version, minVersion)); + return; + } + Log.Info(string.Format("Found Emscripten SDK {0} at {1}", Version, RootPath)); + IsValid = true; + } + catch (Exception ex) + { + Log.Error($"Failed to read Emscripten SDK version from file '{versionPath}'"); + Log.Exception(ex); + } + } + else + Log.Warning($"Missing file {versionPath}"); + } + } + } +} diff --git a/Source/Tools/Flax.Build/Platforms/Web/WebPlatform.cs b/Source/Tools/Flax.Build/Platforms/Web/WebPlatform.cs new file mode 100644 index 000000000..dd1540c5e --- /dev/null +++ b/Source/Tools/Flax.Build/Platforms/Web/WebPlatform.cs @@ -0,0 +1,68 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using Flax.Build.Projects; + +namespace Flax.Build.Platforms +{ + /// + /// The build platform for Web with Emscripten. + /// + /// + public sealed class WebPlatform : Platform, IProjectCustomizer + { + /// + public override TargetPlatform Target => TargetPlatform.Web; + + /// + public override bool HasRequiredSDKsInstalled { get; } + + /// + public override bool HasSharedLibrarySupport => false; + + /// + public override bool HasModularBuildSupport => false; + + /// + public override bool HasDynamicCodeExecutionSupport => false; + + /// + public override string ExecutableFileExtension => ".html"; + + /// + public override string SharedLibraryFileExtension => ".so"; + + /// + public override string StaticLibraryFileExtension => ".a"; + + /// + public override string ProgramDatabaseFileExtension => string.Empty; + + /// + /// Initializes a new instance of the class. + /// + public WebPlatform() + { + HasRequiredSDKsInstalled = EmscriptenSdk.Instance.IsValid; + } + + /// + protected override Toolchain CreateToolchain(TargetArchitecture architecture) + { + if (architecture != TargetArchitecture.x86) + throw new InvalidArchitectureException(architecture, "Web is compiled into WebAssembly and doesn't have specific architecture but is mocked as x86."); + return new WebToolchain(this, architecture); + } + + /// + void IProjectCustomizer.GetSolutionArchitectureName(TargetArchitecture architecture, ref string name) + { + name = "Web"; + } + + /// + void IProjectCustomizer.GetProjectArchitectureName(Project project, Platform platform, TargetArchitecture architecture, ref string name) + { + name = "Win32"; + } + } +} diff --git a/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs b/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs new file mode 100644 index 000000000..a0e22c7e5 --- /dev/null +++ b/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs @@ -0,0 +1,302 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Flax.Build.Graph; +using Flax.Build.NativeCpp; + +namespace Flax.Build.Platforms +{ + /// + /// The build toolchain for Web with Emscripten. + /// + /// + public sealed class WebToolchain : Toolchain + { + private string _sysrootPath; + private string _compilerPath; + private Version _compilerVersion; + + /// + /// Initializes a new instance of the class. + /// + /// The platform. + /// The target architecture. + public WebToolchain(WebPlatform platform, TargetArchitecture architecture) + : base(platform, architecture) + { + var sdkPath = EmscriptenSdk.Instance.EmscriptenPath; + + // Setup tools + _compilerPath = Path.Combine(sdkPath, "emscripten", "emcc"); + var clangPath = Path.Combine(sdkPath, "bin", "clang++"); + if (Platform.BuildTargetPlatform == TargetPlatform.Windows) + { + _compilerPath += ".bat"; + clangPath += ".exe"; + } + + // Determinate compiler version + _compilerVersion = UnixToolchain.GetClangVersion(platform.Target, clangPath); + + // Setup system paths + SystemIncludePaths.Add(Path.Combine(sdkPath, "lib", "clang", _compilerVersion.Major.ToString(), "include")); + SystemIncludePaths.Add(Path.Combine(sdkPath, "emscripten", "system", "include")); + SystemIncludePaths.Add(Path.Combine(sdkPath, "emscripten", "system", "lib")); + _sysrootPath = Path.Combine(sdkPath, "emscripten/cache/sysroot/include"); + } + + public static string GetLibName(string path) + { + var libName = Path.GetFileNameWithoutExtension(path); + if (libName.StartsWith("lib")) + libName = libName.Substring(3); + return libName; + } + + /// + public override string DllExport => "__attribute__((__visibility__(\\\"default\\\")))"; + + /// + public override string DllImport => ""; + + /// + public override TargetCompiler Compiler => TargetCompiler.Clang; + + /// + public override string NativeCompilerPath => _compilerPath; + + /// + public override void LogInfo() + { + Log.Info("Clang version: " + _compilerVersion); + } + + /// + public override void SetupEnvironment(BuildOptions options) + { + base.SetupEnvironment(options); + + options.CompileEnv.PreprocessorDefinitions.Add("PLATFORM_WEB"); + options.CompileEnv.PreprocessorDefinitions.Add("PLATFORM_UNIX"); + options.CompileEnv.PreprocessorDefinitions.Add("__EMSCRIPTEN__"); + options.CompileEnv.EnableExceptions = false; + options.CompileEnv.CpuArchitecture = CpuArchitecture.None; // TODO: try SIMD support in Emscripten + } + + private void AddSharedArgs(List args, bool debugInformation, bool optimization, FavorSizeOrSpeed favorSizeOrSpeed) + { + if (debugInformation) + args.Add("-g2"); + else + args.Add("-g0"); + + if (favorSizeOrSpeed == FavorSizeOrSpeed.SmallCode) + args.Add("-Os"); + if (favorSizeOrSpeed == FavorSizeOrSpeed.FastCode) + args.Add("-O3"); + else if (optimization) + args.Add("-O2"); + else + args.Add("-O0"); + } + + /// + public override CompileOutput CompileCppFiles(TaskGraph graph, BuildOptions options, List sourceFiles, string outputPath) + { + var output = new CompileOutput(); + + // Setup arguments shared by all source files + var commonArgs = new List(); + commonArgs.AddRange(options.CompileEnv.CustomArgs); + { + commonArgs.Add("-c"); + + AddSharedArgs(commonArgs, options.CompileEnv.DebugInformation, options.CompileEnv.Optimization, options.CompileEnv.FavorSizeOrSpeed); + } + + // Add preprocessor definitions + foreach (var definition in options.CompileEnv.PreprocessorDefinitions) + { + commonArgs.Add(string.Format("-D \"{0}\"", definition)); + } + + // Add include paths + foreach (var includePath in options.CompileEnv.IncludePaths) + { + if (SystemIncludePaths.Contains(includePath)) // TODO: fix SystemIncludePaths so this chack can be removed + continue; // Skip system includes as those break compilation (need to fix sys root linking for emscripten) + commonArgs.Add(string.Format("-I\"{0}\"", includePath.Replace('\\', '/'))); + } + + // Hack for sysroot includes + commonArgs.Add(string.Format("-I\"{0}\"", _sysrootPath.Replace('\\', '/'))); + + // Compile all C/C++ files + var args = new List(); + foreach (var sourceFile in sourceFiles) + { + var sourceFilename = Path.GetFileNameWithoutExtension(sourceFile); + var task = graph.Add(); + + // Use shared arguments + args.Clear(); + args.AddRange(commonArgs); + + // Language for the file + args.Add("-x"); + if (sourceFile.EndsWith(".c", StringComparison.OrdinalIgnoreCase)) + args.Add("c"); + else + { + args.Add("c++"); + + // C++ version + switch (options.CompileEnv.CppVersion) + { + case CppVersion.Cpp14: + args.Add("-std=c++14"); + break; + case CppVersion.Cpp17: + case CppVersion.Latest: + args.Add("-std=c++17"); + break; + case CppVersion.Cpp20: + args.Add("-std=c++20"); + break; + } + } + + // Object File Name + var objFile = Path.Combine(outputPath, sourceFilename + ".o"); + args.Add(string.Format("-o \"{0}\"", objFile.Replace('\\', '/'))); + output.ObjectFiles.Add(objFile); + task.ProducedFiles.Add(objFile); + + // Source File Name + args.Add("\"" + sourceFile.Replace('\\', '/') + "\""); + + // Request included files to exist + var includes = IncludesCache.FindAllIncludedFiles(sourceFile); + task.PrerequisiteFiles.AddRange(includes); + + // Compile + task.WorkingDirectory = options.WorkingDirectory; + task.CommandPath = _compilerPath; + task.CommandArguments = string.Join(" ", args); + task.PrerequisiteFiles.Add(sourceFile); + task.InfoMessage = Path.GetFileName(sourceFile); + task.Cost = task.PrerequisiteFiles.Count; // TODO: include source file size estimation to improve tasks sorting + } + + return output; + } + + private Task CreateBinary(TaskGraph graph, BuildOptions options, string outputFilePath) + { + var task = graph.Add(); + + // Setup arguments + var args = new List(); + args.AddRange(options.LinkEnv.CustomArgs); + { + args.Add(string.Format("-o \"{0}\"", outputFilePath.Replace('\\', '/'))); + + AddSharedArgs(args, options.LinkEnv.DebugInformation, options.LinkEnv.Optimization, options.CompileEnv.FavorSizeOrSpeed); + } + + args.Add("-Wl,--start-group"); + + // Input libraries + var libraryPaths = new HashSet(); + var dynamicLibExt = Platform.SharedLibraryFileExtension; + foreach (var library in options.LinkEnv.InputLibraries.Concat(options.Libraries)) + { + var dir = Path.GetDirectoryName(library); + var ext = Path.GetExtension(library); + if (library.StartsWith("--use-port=")) + { + // Ports (https://emscripten.org/docs/compiling/Building-Projects.html#emscripten-ports) + args.Add(library); + } + else if (string.IsNullOrEmpty(dir)) + { + args.Add(string.Format("\"-l{0}\"", library)); + } + else if (string.IsNullOrEmpty(ext)) + { + // Skip executable + } + else if (ext == dynamicLibExt) + { + // Link against dynamic library + task.PrerequisiteFiles.Add(library); + libraryPaths.Add(dir); + args.Add(string.Format("\"-l{0}\"", GetLibName(library))); + } + else + { + task.PrerequisiteFiles.Add(library); + args.Add(string.Format("\"-l{0}\"", GetLibName(library))); + } + } + + // Input files (link static libraries last) + task.PrerequisiteFiles.AddRange(options.LinkEnv.InputFiles); + foreach (var file in options.LinkEnv.InputFiles.Where(x => !x.EndsWith(".a")).Concat(options.LinkEnv.InputFiles.Where(x => x.EndsWith(".a")))) + { + args.Add(string.Format("\"{0}\"", file.Replace('\\', '/'))); + } + + // Additional lib paths + libraryPaths.AddRange(options.LinkEnv.LibraryPaths); + foreach (var path in libraryPaths) + { + args.Add(string.Format("-L\"{0}\"", path.Replace('\\', '/'))); + } + + args.Add("-Wl,--end-group"); + + // Use a response file (it can contain any commands that you would specify on the command line) + bool useResponseFile = true; + string responseFile = null; + if (useResponseFile) + { + responseFile = Path.Combine(options.IntermediateFolder, Path.GetFileName(outputFilePath) + ".response"); + task.PrerequisiteFiles.Add(responseFile); + Utilities.WriteFileIfChanged(responseFile, string.Join(Environment.NewLine, args)); + } + + // Link + task.WorkingDirectory = options.WorkingDirectory; + task.CommandPath = _compilerPath; + task.CommandArguments = useResponseFile ? string.Format("@\"{0}\"", responseFile) : string.Join(" ", args); + task.InfoMessage = "Linking " + outputFilePath; + task.Cost = task.PrerequisiteFiles.Count; + task.ProducedFiles.Add(outputFilePath); + + return task; + } + + /// + public override void LinkFiles(TaskGraph graph, BuildOptions options, string outputFilePath) + { + outputFilePath = Utilities.NormalizePath(outputFilePath); + + Task linkTask; + switch (options.LinkEnv.Output) + { + case LinkerOutput.Executable: + case LinkerOutput.SharedLibrary: + linkTask = CreateBinary(graph, options, outputFilePath); + break; + case LinkerOutput.StaticLibrary: + case LinkerOutput.ImportLibrary: + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/Source/Tools/Flax.Build/Platforms/Windows/WindowsPlatform.cs b/Source/Tools/Flax.Build/Platforms/Windows/WindowsPlatform.cs index a78f8c24f..fb4b55bf9 100644 --- a/Source/Tools/Flax.Build/Platforms/Windows/WindowsPlatform.cs +++ b/Source/Tools/Flax.Build/Platforms/Windows/WindowsPlatform.cs @@ -67,6 +67,7 @@ namespace Flax.Build.Platforms case TargetPlatform.Switch: return Sdk.HasValid("SwitchSdk"); case TargetPlatform.XboxOne: case TargetPlatform.XboxScarlett: return GetPlatform(platform, true)?.HasRequiredSDKsInstalled ?? false; + case TargetPlatform.Web: return EmscriptenSdk.Instance.IsValid; default: return false; } }